Backdrop Path + Cybersteel #23

Merged
zack merged 5 commits from cybersteel into master 2026-05-01 05:43:10 +00:00
29 changed files with 2931 additions and 415 deletions
Showing only changes of commit 16989cbb71 - Show all commits
+10
View File
@@ -75,6 +75,16 @@
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures", "command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures",
"cwd": "$ZED_WORKTREE_ROOT", "cwd": "$ZED_WORKTREE_ROOT",
}, },
{
"label": "Run draw gaussian-blur example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run draw gaussian-blur-debug example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur-debug",
"cwd": "$ZED_WORKTREE_ROOT",
},
{ {
"label": "Run qrcode basic example", "label": "Run qrcode basic example",
"command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- basic", "command": "odin run qrcode/examples -debug -out=out/debug/qrcode-examples -- basic",
+92 -18
View File
@@ -52,9 +52,12 @@ statically allocates registers for the worst-case path (Ring_Arc) regardless of
fragment actually evaluates, so all fragments pay the occupancy cost of the heaviest branch. This is fragment actually evaluates, so all fragments pay the occupancy cost of the heaviest branch. This is
a documented limitation, not a design constraint (see "Known limitations: V3D and Bifrost" below). a documented limitation, not a design constraint (see "Known limitations: V3D and Bifrost" below).
MSAA is opt-in (default `._1`, no MSAA) via `Init_Options.msaa_samples`. SDF rendering does not MSAA is intentionally not supported. SDF text and shapes compute fragment coverage analytically
benefit from MSAA because fragment coverage is computed analytically. MSAA remains useful for text via `smoothstep`, so they don't benefit from multisampling. Tessellated user geometry submitted via
glyph edges and tessellated user geometry if desired. `prepare_shape` is rendered without anti-aliasing — if AA is required for tessellated content, the
caller must render it to their own offscreen target and submit the result as a texture. This
decision matches RAD Debugger's architecture and aligns with the SBC target (Mali Valhall, where
MSAA's per-tile bandwidth multiplier is expensive).
## 2D rendering pipeline plan ## 2D rendering pipeline plan
@@ -249,9 +252,9 @@ API where each layer draws shadows before quads before glyphs. Our design avoids
submission order is draw order, no layer juggling required. submission order is draw order, no layer juggling required.
**PSO compilation costs multiply.** Each pipeline takes 150ms to compile on Metal/Vulkan/D3D12 at **PSO compilation costs multiply.** Each pipeline takes 150ms to compile on Metal/Vulkan/D3D12 at
first use. 7 pipelines is ~175ms cold startup; 3 pipelines is ~75ms. Adding state axes (MSAA first use. 7 pipelines is ~175ms cold startup; 3 pipelines is ~75ms. Adding state axes (blend
variants, blend modes, color formats) multiplies combinatorially — a 2.3× larger variant matrix per modes, color formats) multiplies combinatorially — a 2.3× larger variant matrix per additional
additional axis with 7 pipelines vs 3. axis with 7 pipelines vs 3.
**Branching cost comparison: unified vs per-kind in the effects pipeline.** The effects pipeline is **Branching cost comparison: unified vs per-kind in the effects pipeline.** The effects pipeline is
the strongest candidate for per-kind splitting because effect branches are heavier than shape the strongest candidate for per-kind splitting because effect branches are heavier than shape
@@ -587,27 +590,29 @@ Wallace's variant) and vger-rs.
### Backdrop pipeline ### Backdrop pipeline
The backdrop pipeline handles effects that sample the current render target as input: frosted glass, The backdrop pipeline handles effects that sample the current render target as input: frosted glass,
refraction, mirror surfaces. It is separated from the effects pipeline for a structural reason, not refraction, mirror surfaces. It is separated from the main and effects pipelines for a structural
register pressure. reason, not register pressure.
**Render-pass boundary.** Before any backdrop-sampling fragment can run, the current render target **Render-pass boundary.** Before any backdrop-sampling fragment can run, the current render target
must be copied to a separate texture via `CopyGPUTextureToTexture`. This is a command-buffer-level must be in a sampler-readable state. A draw call that samples the render target it is also writing
operation that cannot happen mid-render-pass. The copy naturally creates a pipeline boundary that no to is a hard GPU constraint; the only way to satisfy it is to end the current render pass and start
amount of shader optimization can eliminate — it is a fundamental requirement of sampling a surface a new one. That render-pass boundary is what a “bracket” is.
while also writing to it.
**Multi-pass implementation.** Backdrop effects are implemented as separable multi-pass sequences **Multi-pass implementation.** Backdrop effects are implemented as separable multi-pass sequences
(downsample → horizontal blur → vertical blurcomposite), following the standard approach used by (downsample → horizontal blur → vertical-blur+composite), following the standard approach used by
iOS `UIVisualEffectView`, Android `RenderEffect`, and Flutter's `BackdropFilter`. Each individual iOS `UIVisualEffectView`, Android `RenderEffect`, and Flutter's `BackdropFilter`. Each individual
sub-pass is budgeted at **≤24 registers** (same as the main pipeline — full Valhall occupancy). The sub-pass is budgeted at **≤24 registers** (same as the main pipeline — full Valhall occupancy). The
multi-pass approach avoids the monolithic 70+ register shader that a single-pass Gaussian blur would multi-pass approach avoids the monolithic 70+ register shader that a single-pass Gaussian blur would
require, keeping each sub-pass well under the 32-register cliff. require, keeping each sub-pass well under the 32-register cliff.
**Bracketed execution.** All backdrop draws in a frame share a single bracketed region of the command **Approach B: render-target choice.** When any layer in the frame contains a backdrop draw, the
buffer: end the current render pass, copy the render target, execute all backdrop sub-passes, then entire frame renders into `source_texture` (a full-resolution single-sample texture owned by the
resume normal drawing. The entry/exit cost (texture copy + render-pass break) is paid once per frame backdrop pipeline) instead of directly into the swapchain. At the end of the frame, `source_texture`
regardless of how many backdrop effects are visible. When no backdrop effects are present, the bracket is copied to the swapchain via a single `CopyGPUTextureToTexture` call. This means the bracket has
is never entered and the texture copy never happens — zero cost. no mid-frame texture copy: by the time the bracket runs, `source_texture` already contains the pre-
bracket frame contents and is the natural sampler input. When no layer in the frame has a backdrop
draw, the existing fast path runs: the frame renders directly to the swapchain and the backdrop
pipeline's working textures are never touched. Zero cost for backdrop-free frames.
**Why not split the backdrop sub-passes into separate pipelines?** Each sub-pass is budgeted at ≤24 **Why not split the backdrop sub-passes into separate pipelines?** Each sub-pass is budgeted at ≤24
registers, well under Valhall's 32-register cliff, so there is no occupancy motivation for splitting. registers, well under Valhall's 32-register cliff, so there is no occupancy motivation for splitting.
@@ -617,6 +622,75 @@ all. Additionally, backdrop effects cover a small fraction of the frame's total
typical UI scales), so even if a sub-pass did cross a cliff, the occupancy variation within the typical UI scales), so even if a sub-pass did cross a cliff, the occupancy variation within the
bracket would have negligible impact on frame time. bracket would have negligible impact on frame time.
#### Bracket scheduling model
The bracket is scheduled per layer, anchored at the first backdrop sub-batch in the layer's
submission order. Concretely, a layer with one or more backdrops splits into three groups:
1. **Pass A (pre-bracket)** — every non-backdrop sub-batch with index `< bracket_start_index`.
Renders to `source_texture` in a single render pass.
2. **The bracket** — every backdrop sub-batch in the layer (regardless of index). Runs one
downsample pass, then one (H-blur + V-composite) pass pair per unique sigma.
3. **Pass B (post-bracket)** — every non-backdrop sub-batch with index `>= bracket_start_index`.
Renders to `source_texture` with `LOAD`, drawing on top of the composited backdrop output.
`bracket_start_index` is the absolute index of the first `.Backdrop` kind in the layer's sub-batch
range. If the layer has no backdrops, none of this kicks in and the layer renders in a single render
pass via the existing fast path.
The downsample runs once per layer, not once per sigma: it just copies `source_texture` to a ¼-
resolution working texture and doesn't depend on the kernel. Each unique sigma in the layer triggers
one H-blur (reads `downsample_texture`, writes `h_blur_texture`) and one V-composite (reads
`h_blur_texture`, writes `source_texture` per-primitive with the SDF mask). Sub-batch coalescing in
`append_or_extend_sub_batch` merges contiguous same-sigma backdrops into a single instanced V-
composite draw call; non-contiguous same-sigma backdrops still share the H-blur output but issue
separate V-composite draws.
#### Submission-order trade-off
Within Pass A and Pass B, sub-batches render in the user's submission order. What the bracket model
sacrifices is *interleaved* ordering between backdrop and non-backdrop content within a single
layer. A non-backdrop sub-batch submitted between two backdrops still renders in Pass B (after the
bracket), not at its submission position. Worked example:
```
draw.rectangle(layer, bg, GRAY) // 0 Tessellated → Pass A
draw.rectangle(layer, card_blue, BLUE) // 1 SDF → Pass A
draw.rectangle_backdrop(layer, panelA, 12) // 2 Backdrop → Bracket (sees: bg + blue card)
draw.rectangle(layer, card_red, RED) // 3 SDF → Pass B (drawn ON TOP of panelA)
draw.rectangle_backdrop(layer, panelB, 12) // 4 Backdrop → Bracket (sees: bg + blue card; same as panelA)
draw.text(layer, "label", ...) // 5 Text → Pass B (drawn ON TOP of both panels)
```
In this layer, panelB does *not* see card_red — even though card_red was submitted before panelB —
because both backdrops sample `source_texture` as it stood at the bracket entry, which is after
Pass A and before card_red has rendered. card_red ends up on top of panelA, not underneath it.
The user controls the alternative outcome by splitting layers. Putting card_red and panelB into a
new layer (via `draw.new_layer`) gives panelB a fresh source snapshot that includes panelA and
card_red:
```
base := draw.begin(...)
draw.rectangle(base, bg, GRAY)
draw.rectangle(base, card_blue, BLUE)
draw.rectangle_backdrop(base, panelA, 12) // panelA in base layer's bracket
top := draw.new_layer(base, ...)
draw.rectangle(top, card_red, RED)
draw.rectangle_backdrop(top, panelB, 12) // top layer's bracket; sees base + card_red
draw.text(top, "label", ...)
```
Why one bracket per layer and not one per backdrop? Each bracket adds three render passes
(downsample + H-blur + V-composite) and at least three tile-cache flushes on tilers like Mali
Valhall. Strict submission-order semantics would require one bracket per cluster of contiguous
backdrops, which scales the GPU cost linearly with how interleaved the user's submission happens
to be — a footgun. The current design caps the bracket cost per layer regardless of submission
interleave, and gives the user explicit control over ordering through the existing layer
abstraction. This matches the cost/complexity envelope of iOS `UIVisualEffectView` and CSS
`backdrop-filter` (both of which constrain backdrop ordering implicitly).
### Vertex layout ### Vertex layout
The vertex struct is unchanged from the current 20-byte layout: The vertex struct is unchanged from the current 20-byte layout:
+1121
View File
File diff suppressed because it is too large Load Diff
+125 -118
View File
@@ -4,7 +4,6 @@ import "base:runtime"
import "core:c" import "core:c"
import "core:log" import "core:log"
import "core:math" import "core:math"
import "core:strings" import "core:strings"
import sdl "vendor:sdl3" import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf" import sdl_ttf "vendor:sdl3/ttf"
@@ -16,11 +15,19 @@ when ODIN_OS == .Darwin {
SHADER_ENTRY :: cstring("main0") SHADER_ENTRY :: cstring("main0")
BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.metal") BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.metal")
BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.metal") BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.metal")
BACKDROP_FULLSCREEN_VERT_RAW :: #load("shaders/generated/backdrop_fullscreen.vert.metal")
BACKDROP_DOWNSAMPLE_FRAG_RAW :: #load("shaders/generated/backdrop_downsample.frag.metal")
BACKDROP_BLUR_VERT_RAW :: #load("shaders/generated/backdrop_blur.vert.metal")
BACKDROP_BLUR_FRAG_RAW :: #load("shaders/generated/backdrop_blur.frag.metal")
} else { } else {
PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.SPIRV PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.SPIRV
SHADER_ENTRY :: cstring("main") SHADER_ENTRY :: cstring("main")
BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.spv") BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.spv")
BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.spv") BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.spv")
BACKDROP_FULLSCREEN_VERT_RAW :: #load("shaders/generated/backdrop_fullscreen.vert.spv")
BACKDROP_DOWNSAMPLE_FRAG_RAW :: #load("shaders/generated/backdrop_downsample.frag.spv")
BACKDROP_BLUR_VERT_RAW :: #load("shaders/generated/backdrop_blur.vert.spv")
BACKDROP_BLUR_FRAG_RAW :: #load("shaders/generated/backdrop_blur.frag.spv")
} }
PLATFORM_SHADER_FORMAT :: sdl.GPUShaderFormat{PLATFORM_SHADER_FORMAT_FLAG} PLATFORM_SHADER_FORMAT :: sdl.GPUShaderFormat{PLATFORM_SHADER_FORMAT_FLAG}
@@ -28,10 +35,6 @@ BUFFER_INIT_SIZE :: 256
INITIAL_LAYER_SIZE :: 5 INITIAL_LAYER_SIZE :: 5
INITIAL_SCISSOR_SIZE :: 10 INITIAL_SCISSOR_SIZE :: 10
// Sentinel value: when passed as msaa_samples, `init` will use the maximum MSAA sample count
// supported by the GPU for the swapchain format.
MSAA_MAX :: sdl.GPUSampleCount(0xFF)
// ----- Default parameter values ----- // ----- Default parameter values -----
// Named constants for non-zero default procedure parameters. Centralizes magic numbers // Named constants for non-zero default procedure parameters. Centralizes magic numbers
// so they can be tuned in one place and referenced by name in proc signatures. // so they can be tuned in one place and referenced by name in proc signatures.
@@ -39,8 +42,8 @@ DFT_FEATHER_PX :: 1 // Total AA feather width in physical pixels (half on each s
DFT_STROKE_THICKNESS :: 1 // Default line/stroke thickness in logical pixels. DFT_STROKE_THICKNESS :: 1 // Default line/stroke thickness in logical pixels.
DFT_FONT_SIZE :: 44 // Default font size in points for text rendering. DFT_FONT_SIZE :: 44 // Default font size in points for text rendering.
DFT_CIRC_END_ANGLE :: 360 // Full-circle end angle in degrees (ring/arc). DFT_CIRC_END_ANGLE :: 360 // Full-circle end angle in degrees (ring/arc).
DFT_UV_RECT :: Rectangle{0, 0, 1, 1} // Full-texture UV rect (rectangle_texture). DFT_UV_RECT :: Rectangle{0, 0, 1, 1} // Full-texture UV rect (Texture_Fill default).
DFT_TINT :: WHITE // Default texture tint (rectangle_texture, clay_image). DFT_TINT :: WHITE // Default texture tint (Texture_Fill, clay_image).
DFT_TEXT_COLOR :: BLACK // Default text color. DFT_TEXT_COLOR :: BLACK // Default text color.
DFT_CLEAR_COLOR :: BLACK // Default clear color for end(). DFT_CLEAR_COLOR :: BLACK // Default clear color for end().
DFT_SAMPLER :: Sampler_Preset.Linear_Clamp // Default texture sampler preset. DFT_SAMPLER :: Sampler_Preset.Linear_Clamp // Default texture sampler preset.
@@ -53,9 +56,10 @@ Global :: struct {
tmp_text_verts: [dynamic]Vertex, // Text vertices staged for GPU upload. tmp_text_verts: [dynamic]Vertex, // Text vertices staged for GPU upload.
tmp_text_indices: [dynamic]c.int, // Text index buffer staged for GPU upload. tmp_text_indices: [dynamic]c.int, // Text index buffer staged for GPU upload.
tmp_text_batches: [dynamic]TextBatch, // Text atlas batch metadata for indexed drawing. tmp_text_batches: [dynamic]TextBatch, // Text atlas batch metadata for indexed drawing.
tmp_primitives: [dynamic]Primitive, // SDF primitives staged for GPU storage buffer upload. tmp_primitives: [dynamic]Base_2D_Primitive, // SDF primitives staged for GPU storage buffer upload (base 2D pipeline).
tmp_sub_batches: [dynamic]Sub_Batch, // Sub-batch records that drive draw call dispatch. tmp_sub_batches: [dynamic]Sub_Batch, // Sub-batch records that drive draw call dispatch.
tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects destroyed after end() submits. tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects destroyed after end() submits.
tmp_backdrop_primitives: [dynamic]Backdrop_Primitive, // Backdrop primitives staged for GPU storage buffer upload.
layers: [dynamic]Layer, // Draw layers, each with its own scissor stack. layers: [dynamic]Layer, // Draw layers, each with its own scissor stack.
scissors: [dynamic]Scissor, // Scissor rects that clip drawing within each layer. scissors: [dynamic]Scissor, // Scissor rects that clip drawing within each layer.
@@ -67,6 +71,7 @@ Global :: struct {
// -- Pipeline (accessed every draw_layer call) -- // -- Pipeline (accessed every draw_layer call) --
pipeline_2d_base: Pipeline_2D_Base, // The unified 2D GPU pipeline (shaders, buffers, samplers). pipeline_2d_base: Pipeline_2D_Base, // The unified 2D GPU pipeline (shaders, buffers, samplers).
pipeline_2d_backdrop: Pipeline_2D_Backdrop, // Frosted-glass backdrop blur pipeline (downsample + blur PSOs, working textures).
device: ^sdl.GPUDevice, // GPU device handle, stored at init. device: ^sdl.GPUDevice, // GPU device handle, stored at init.
samplers: [SAMPLER_PRESET_COUNT]^sdl.GPUSampler, // Lazily-created sampler objects, one per Sampler_Preset. samplers: [SAMPLER_PRESET_COUNT]^sdl.GPUSampler, // Lazily-created sampler objects, one per Sampler_Preset.
@@ -78,12 +83,6 @@ Global :: struct {
texture_slots: [dynamic]Texture_Slot, // Registered texture slots indexed by Texture_Id. texture_slots: [dynamic]Texture_Slot, // Registered texture slots indexed by Texture_Id.
texture_free_list: [dynamic]u32, // Recycled slot indices available for reuse. texture_free_list: [dynamic]u32, // Recycled slot indices available for reuse.
// -- MSAA (once per frame in end()) --
msaa_texture: ^sdl.GPUTexture, // Intermediate render target for multi-sample resolve.
msaa_width: u32, // Cached width to detect when MSAA texture needs recreation.
msaa_height: u32, // Cached height to detect when MSAA texture needs recreation.
sample_count: sdl.GPUSampleCount, // Sample count chosen at init (._1 means MSAA disabled).
// -- Clay (once per frame in prepare_clay_batch) -- // -- Clay (once per frame in prepare_clay_batch) --
clay_memory: [^]u8, // Raw memory block backing Clay's internal arena. clay_memory: [^]u8, // Raw memory block backing Clay's internal arena.
@@ -99,6 +98,7 @@ Global :: struct {
max_text_batches: int, max_text_batches: int,
max_primitives: int, max_primitives: int,
max_sub_batches: int, max_sub_batches: int,
max_backdrop_primitives: int,
// -- Init-only (coldest — set once at init, never written again) -- // -- Init-only (coldest — set once at init, never written again) --
odin_context: runtime.Context, // Odin context captured at init for use in callbacks. odin_context: runtime.Context, // Odin context captured at init for use in callbacks.
@@ -128,8 +128,8 @@ Vec2 :: [2]f32
// transparent. This matches the GPU-side layout: the shader unpacks via unpackUnorm4x8 which // transparent. This matches the GPU-side layout: the shader unpacks via unpackUnorm4x8 which
// reads the bytes in memory order as R, G, B, A and normalizes each to [0, 1]. // reads the bytes in memory order as R, G, B, A and normalizes each to [0, 1].
// //
// When used in the Primitive struct (Primitive.color), the 4 bytes are stored as a u32 in // When used in the Base_2D_Primitive or Backdrop_Primitive structs (e.g. .color), the 4 bytes
// native byte order and unpacked by the shader. // are stored as a u32 in native byte order and unpacked by the shader.
Color :: [4]u8 Color :: [4]u8
BLACK :: Color{0, 0, 0, 255} BLACK :: Color{0, 0, 0, 255}
@@ -149,29 +149,42 @@ Rectangle_Radii :: struct {
} }
// A linear gradient between two colors along an arbitrary angle. // A linear gradient between two colors along an arbitrary angle.
// The `end_color` is the color at the end of the gradient direction; the shape's fill `color` // `angle` is in degrees: 0 = left-to-right, 90 = top-to-bottom.
// parameter acts as the start color. `angle` is in degrees: 0 = left-to-right, 90 = top-to-bottom.
Linear_Gradient :: struct { Linear_Gradient :: struct {
start_color: Color,
end_color: Color, end_color: Color,
angle: f32, angle: f32,
} }
// A radial gradient between two colors from center to edge. // A radial gradient between two colors from center to edge.
// The `outer_color` is the color at the shape's edge; the shape's fill `color` parameter
// acts as the inner (center) color.
Radial_Gradient :: struct { Radial_Gradient :: struct {
inner_color: Color,
outer_color: Color, outer_color: Color,
} }
// Tagged union for specifying a gradient on any shape. Defaults to `nil` (no gradient). // Sample a registered texture as the shape's fill source.
// When a gradient is active, the shape's `color` parameter becomes the start/inner color, // `tint` modulates the sampled texels per-pixel (constant multiply); WHITE passes through
// and the gradient struct carries the end/outer color plus any type-specific parameters. // unchanged. Translucent tints fade the texture; non-white tints recolor it.
// // Zero-initialized fields are treated as defaults by the shape procs:
// Gradient and Textured are mutually exclusive on the same primitive. If a shape uses // tint == Color{} → WHITE
// `rectangle_texture`, gradients are not applicable — use the tint color instead. // uv_rect == Rectangle{} → {0, 0, 1, 1} (full texture)
Gradient :: union { // sampler == .Linear_Clamp (enum value 0)
Texture_Fill :: struct {
id: Texture_Id,
tint: Color,
uv_rect: Rectangle,
sampler: Sampler_Preset,
}
// Mutually exclusive fill sources for shape procs. Each shape proc accepts a Brush
// as its third positional parameter. Texture and gradient are mutually exclusive at
// the GPU level (they share the worst-case register path); outline is orthogonal and
// composes with any Brush variant.
Brush :: union {
Color,
Linear_Gradient, Linear_Gradient,
Radial_Gradient, Radial_Gradient,
Texture_Fill,
} }
// Convert clay.Color ([4]c.float in 0255 range) to Color. // Convert clay.Color ([4]c.float in 0255 range) to Color.
@@ -207,17 +220,24 @@ Rectangle :: struct {
} }
Sub_Batch_Kind :: enum u8 { Sub_Batch_Kind :: enum u8 {
Tessellated, // non-indexed, white texture or user texture, mode 0 Tessellated, // non-indexed, white texture or user texture, base 2D mode 0
Text, // indexed, atlas texture, mode 0 Text, // indexed, atlas texture, base 2D mode 0
SDF, // instanced unit quad, white texture or user texture, mode 1 SDF, // instanced unit quad, base 2D mode 1
// instanced unit quad, backdrop pipeline V-composite (indexes Backdrop_Primitive).
// Bracket-scheduled per layer; see README.md § "Backdrop pipeline" for ordering semantics.
Backdrop,
} }
Sub_Batch :: struct { Sub_Batch :: struct {
kind: Sub_Batch_Kind, kind: Sub_Batch_Kind,
offset: u32, // Tessellated: vertex offset; Text: text_batch index; SDF: primitive index offset: u32, // Tessellated: vertex offset; Text: text_batch index; SDF/Backdrop: primitive index
count: u32, // Tessellated: vertex count; Text: always 1; SDF: primitive count count: u32, // Tessellated: vertex count; Text: always 1; SDF/Backdrop: primitive count
texture_id: Texture_Id, texture_id: Texture_Id,
sampler: Sampler_Preset, sampler: Sampler_Preset,
// Backdrop only — Gaussian std-dev in logical pixels. Named with the
// distribution prefix because future kinds may want different sigma
// shapes (e.g. drop-shadow penumbra) without overloading this field.
gaussian_sigma: f32,
} }
Layer :: struct { Layer :: struct {
@@ -234,39 +254,38 @@ Scissor :: struct {
sub_batch_len: u32, sub_batch_len: u32,
} }
Init_Options :: struct {
// MSAA sample count. Default is ._1 (no MSAA). SDF rendering does not benefit from MSAA
// because SDF fragments compute coverage analytically via `smoothstep`. MSAA helps for
// text glyph edges and tessellated user geometry. Set to ._4 or ._8 for text-heavy UIs,
// or use `MSAA_MAX` to request the highest sample count the GPU supports for the swapchain
// format.
msaa_samples: sdl.GPUSampleCount,
}
// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails. // Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
//
// MSAA is intentionally NOT supported. SDF text and shapes compute coverage analytically via
// `smoothstep`, so they don't benefit from multisampling. Tessellated user geometry submitted
// via `prepare_shape` is not anti-aliased — if you need AA on tessellated content, render it
// to your own offscreen target and submit it as a texture. RAD Debugger and the SBC target
// (Mali Valhall, where MSAA's per-tile bandwidth multiplier is expensive) drove this decision.
@(require_results) @(require_results)
init :: proc( init :: proc(
device: ^sdl.GPUDevice, device: ^sdl.GPUDevice,
window: ^sdl.Window, window: ^sdl.Window,
options: Init_Options = {},
allocator := context.allocator, allocator := context.allocator,
odin_context := context, odin_context := context,
) -> ( ) -> (
ok: bool, ok: bool,
) { ) {
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize() min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
resolved_sample_count := options.msaa_samples
if resolved_sample_count == MSAA_MAX { pipeline, pipeline_ok := create_pipeline_2d_base(device, window)
resolved_sample_count = max_sample_count(device, window) if !pipeline_ok {
return false
} }
pipeline, pipeline_ok := create_pipeline_2d_base(device, window, resolved_sample_count) backdrop_pipeline, backdrop_pipeline_ok := create_pipeline_2d_backdrop(device, window)
if !pipeline_ok { if !backdrop_pipeline_ok {
destroy_pipeline_2d_base(device, &pipeline)
return false return false
} }
text_cache, text_ok := init_text_cache(device, allocator) text_cache, text_ok := init_text_cache(device, allocator)
if !text_ok { if !text_ok {
destroy_pipeline_2d_backdrop(device, &backdrop_pipeline)
destroy_pipeline_2d_base(device, &pipeline) destroy_pipeline_2d_base(device, &pipeline)
return false return false
} }
@@ -278,9 +297,10 @@ init :: proc(
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_primitives = make([dynamic]Base_2D_Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator), tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
tmp_backdrop_primitives = make([dynamic]Backdrop_Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
device = device, device = device,
texture_slots = make([dynamic]Texture_Slot, 0, 16, allocator = allocator), texture_slots = make([dynamic]Texture_Slot, 0, 16, allocator = allocator),
texture_free_list = make([dynamic]u32, 0, 16, allocator = allocator), texture_free_list = make([dynamic]u32, 0, 16, allocator = allocator),
@@ -289,8 +309,8 @@ init :: proc(
odin_context = odin_context, odin_context = odin_context,
dpi_scaling = sdl.GetWindowDisplayScale(window), dpi_scaling = sdl.GetWindowDisplayScale(window),
clay_memory = make([^]u8, min_memory_size, allocator = allocator), clay_memory = make([^]u8, min_memory_size, allocator = allocator),
sample_count = resolved_sample_count,
pipeline_2d_base = pipeline, pipeline_2d_base = pipeline,
pipeline_2d_backdrop = backdrop_pipeline,
text_cache = text_cache, text_cache = text_cache,
} }
@@ -325,6 +345,8 @@ resize_global :: proc() {
shrink(&GLOB.tmp_primitives, GLOB.max_primitives) shrink(&GLOB.tmp_primitives, GLOB.max_primitives)
if len(GLOB.tmp_sub_batches) > GLOB.max_sub_batches do GLOB.max_sub_batches = len(GLOB.tmp_sub_batches) if len(GLOB.tmp_sub_batches) > GLOB.max_sub_batches do GLOB.max_sub_batches = len(GLOB.tmp_sub_batches)
shrink(&GLOB.tmp_sub_batches, GLOB.max_sub_batches) shrink(&GLOB.tmp_sub_batches, GLOB.max_sub_batches)
if len(GLOB.tmp_backdrop_primitives) > GLOB.max_backdrop_primitives do GLOB.max_backdrop_primitives = len(GLOB.tmp_backdrop_primitives)
shrink(&GLOB.tmp_backdrop_primitives, GLOB.max_backdrop_primitives)
} }
destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) { destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
@@ -336,17 +358,16 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
delete(GLOB.tmp_text_batches) delete(GLOB.tmp_text_batches)
delete(GLOB.tmp_primitives) delete(GLOB.tmp_primitives)
delete(GLOB.tmp_sub_batches) delete(GLOB.tmp_sub_batches)
delete(GLOB.tmp_backdrop_primitives)
for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text) for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
delete(GLOB.tmp_uncached_text) delete(GLOB.tmp_uncached_text)
free(GLOB.clay_memory, allocator) free(GLOB.clay_memory, allocator)
if GLOB.msaa_texture != nil {
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
}
process_pending_texture_releases() process_pending_texture_releases()
destroy_all_textures() destroy_all_textures()
destroy_sampler_pool() destroy_sampler_pool()
for ttf_text in GLOB.pending_text_releases do sdl_ttf.DestroyText(ttf_text) for ttf_text in GLOB.pending_text_releases do sdl_ttf.DestroyText(ttf_text)
delete(GLOB.pending_text_releases) delete(GLOB.pending_text_releases)
destroy_pipeline_2d_backdrop(device, &GLOB.pipeline_2d_backdrop)
destroy_pipeline_2d_base(device, &GLOB.pipeline_2d_base) destroy_pipeline_2d_base(device, &GLOB.pipeline_2d_base)
destroy_text_cache() destroy_text_cache()
} }
@@ -373,6 +394,7 @@ clear_global :: proc() {
clear(&GLOB.tmp_text_batches) clear(&GLOB.tmp_text_batches)
clear(&GLOB.tmp_primitives) clear(&GLOB.tmp_primitives)
clear(&GLOB.tmp_sub_batches) clear(&GLOB.tmp_sub_batches)
clear(&GLOB.tmp_backdrop_primitives)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -465,7 +487,7 @@ prepare_shape :: proc(layer: ^Layer, vertices: []Vertex) {
} }
// Submit an SDF primitive to the given layer for rendering. // Submit an SDF primitive to the given layer for rendering.
prepare_sdf_primitive :: proc(layer: ^Layer, prim: Primitive) { prepare_sdf_primitive :: proc(layer: ^Layer, prim: Base_2D_Primitive) {
offset := u32(len(GLOB.tmp_primitives)) offset := u32(len(GLOB.tmp_primitives))
append(&GLOB.tmp_primitives, prim) append(&GLOB.tmp_primitives, prim)
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1] scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
@@ -578,6 +600,12 @@ prepare_text_transformed :: proc(layer: ^Layer, text: Text, transform: Transform
} }
// Append a new sub-batch or extend the last one if same kind and contiguous. // Append a new sub-batch or extend the last one if same kind and contiguous.
//
// `gaussian_sigma` is only consulted for kind == .Backdrop; two .Backdrop sub-batches with
// different sigmas cannot coalesce because they require separate H+V blur passes in the
// bracket scheduler. Float equality is intentional — user-supplied literal sigmas (e.g.
// `sigma = 12`) produce bit-identical floats, and the worst case for two sigmas that differ
// only by a ulp is one extra pass pair (correct, just slightly suboptimal).
@(private) @(private)
append_or_extend_sub_batch :: proc( append_or_extend_sub_batch :: proc(
scissor: ^Scissor, scissor: ^Scissor,
@@ -587,6 +615,7 @@ append_or_extend_sub_batch :: proc(
count: u32, count: u32,
texture_id: Texture_Id = INVALID_TEXTURE, texture_id: Texture_Id = INVALID_TEXTURE,
sampler: Sampler_Preset = DFT_SAMPLER, sampler: Sampler_Preset = DFT_SAMPLER,
gaussian_sigma: f32 = 0,
) { ) {
if scissor.sub_batch_len > 0 { if scissor.sub_batch_len > 0 {
last := &GLOB.tmp_sub_batches[scissor.sub_batch_start + scissor.sub_batch_len - 1] last := &GLOB.tmp_sub_batches[scissor.sub_batch_start + scissor.sub_batch_len - 1]
@@ -594,14 +623,22 @@ append_or_extend_sub_batch :: proc(
kind != .Text && kind != .Text &&
last.offset + last.count == offset && last.offset + last.count == offset &&
last.texture_id == texture_id && last.texture_id == texture_id &&
last.sampler == sampler { last.sampler == sampler &&
(kind != .Backdrop || last.gaussian_sigma == gaussian_sigma) {
last.count += count last.count += count
return return
} }
} }
append( append(
&GLOB.tmp_sub_batches, &GLOB.tmp_sub_batches,
Sub_Batch{kind = kind, offset = offset, count = count, texture_id = texture_id, sampler = sampler}, Sub_Batch {
kind = kind,
offset = offset,
count = count,
texture_id = texture_id,
sampler = sampler,
gaussian_sigma = gaussian_sigma,
},
) )
scissor.sub_batch_len += 1 scissor.sub_batch_len += 1
layer.sub_batch_len += 1 layer.sub_batch_len += 1
@@ -710,7 +747,7 @@ prepare_clay_batch :: proc(
// Background color behind the image (Clay allows it) // Background color behind the image (Clay allows it)
bg := color_from_clay(render_data.backgroundColor) bg := color_from_clay(render_data.backgroundColor)
if bg[3] > 0 { if bg.a > 0 {
rectangle(layer, bounds, bg, radii = radii) rectangle(layer, bounds, bg, radii = radii)
} }
@@ -718,7 +755,12 @@ prepare_clay_batch :: proc(
uv, sampler, inner := fit_params(img_data.fit, bounds, img_data.texture_id) uv, sampler, inner := fit_params(img_data.fit, bounds, img_data.texture_id)
// Draw the image // Draw the image
rectangle_texture(layer, inner, img_data.texture_id, img_data.tint, uv, sampler, radii) rectangle(
layer,
inner,
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv, sampler = sampler},
radii = radii,
)
case clay.RenderCommandType.ScissorStart: case clay.RenderCommandType.ScissorStart:
if bounds.width == 0 || bounds.height == 0 do continue if bounds.width == 0 || bounds.height == 0 do continue
@@ -787,9 +829,19 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError()) log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError())
} }
// Upload primitives to GPU // Pre-scan: if any layer this frame has a backdrop sub-batch, route the entire frame to
// source_texture (Approach B) so the bracket can sample the pre-bracket framebuffer
// without a mid-frame texture copy. Frames without any backdrop hit the existing fast
// path and never touch the backdrop pipeline's working textures.
has_backdrop := frame_has_backdrop()
// Upload primitives to GPU (vertices, indices, SDF prims, and backdrop prims share one
// copy pass so we pay the BeginGPUCopyPass / EndGPUCopyPass cost once per frame).
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer) copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
upload(device, copy_pass) upload(device, copy_pass)
if has_backdrop {
upload_backdrop_primitives(device, copy_pass)
}
sdl.EndGPUCopyPass(copy_pass) sdl.EndGPUCopyPass(copy_pass)
swapchain_texture: ^sdl.GPUTexture swapchain_texture: ^sdl.GPUTexture
@@ -806,12 +858,10 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
return return
} }
use_msaa := GLOB.sample_count != ._1
render_texture := swapchain_texture render_texture := swapchain_texture
if has_backdrop {
if use_msaa { ensure_backdrop_textures(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
ensure_msaa_texture(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height) render_texture = GLOB.pipeline_2d_backdrop.source_texture
render_texture = GLOB.msaa_texture
} }
// Premultiply clear color: the blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied), // Premultiply clear color: the blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied),
@@ -827,24 +877,23 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
// Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor. // Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
for &layer, index in GLOB.layers { for &layer, index in GLOB.layers {
log.debug("Drawing layer", index)
draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer) draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer)
} }
// Resolve MSAA render texture to the swapchain. // Approach B finalization: when we rendered into source_texture, copy it to the swapchain.
if use_msaa { // Single CopyGPUTextureToTexture call per frame, only when backdrop content was present.
resolve_pass := sdl.BeginGPURenderPass( if has_backdrop {
cmd_buffer, copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
&sdl.GPUColorTargetInfo { sdl.CopyGPUTextureToTexture(
texture = render_texture, copy_pass,
load_op = .LOAD, sdl.GPUTextureLocation{texture = GLOB.pipeline_2d_backdrop.source_texture},
store_op = .RESOLVE, sdl.GPUTextureLocation{texture = swapchain_texture},
resolve_texture = swapchain_texture, width,
}, height,
1, 1,
nil, false,
) )
sdl.EndGPURenderPass(resolve_pass) sdl.EndGPUCopyPass(copy_pass)
} }
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) { if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
@@ -852,48 +901,6 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
} }
} }
// ---------------------------------------------------------------------------------------------------------------------
// ----- MSAA --------------------------
// ---------------------------------------------------------------------------------------------------------------------
// Query the highest MSAA sample count supported by the GPU for the swapchain format.
max_sample_count :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> sdl.GPUSampleCount {
format := sdl.GetGPUSwapchainTextureFormat(device, window)
counts := [?]sdl.GPUSampleCount{._8, ._4, ._2}
for count in counts {
if sdl.GPUTextureSupportsSampleCount(device, format, count) do return count
}
return ._1
}
@(private = "file")
ensure_msaa_texture :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, width, height: u32) {
if GLOB.msaa_texture != nil && GLOB.msaa_width == width && GLOB.msaa_height == height {
return
}
if GLOB.msaa_texture != nil {
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
}
GLOB.msaa_texture = sdl.CreateGPUTexture(
device,
sdl.GPUTextureCreateInfo {
type = .D2,
format = format,
usage = {.COLOR_TARGET},
width = width,
height = height,
layer_count_or_depth = 1,
num_levels = 1,
sample_count = GLOB.sample_count,
},
)
if GLOB.msaa_texture == nil {
log.panicf("Failed to create MSAA texture (%dx%d): %s", width, height, sdl.GetError())
}
GLOB.msaa_width = width
GLOB.msaa_height = height
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Utility ----------------------- // ----- Utility -----------------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
+383
View File
@@ -0,0 +1,383 @@
package examples
import "core:fmt"
import "core:math"
import "core:os"
import sdl "vendor:sdl3"
import "../../draw"
import cyber "../cybersteel"
// Backdrop example.
//
// Verifies the Stage D bracket scheduler end-to-end. The demo is structured as three zones in
// one window so we can stress-test the cases that matter:
//
// Zone 1 (top, base layer): animated colorful background + two side-by-side frosted panels
// with DIFFERENT sigmas and DIFFERENT tints. Tests sigma grouping
// and per-primitive tint.
//
// Zone 2 (bottom-left, second layer): a small frosted panel in a NEW layer; its bracket sees
// Zone 1's full content (base layer's bracket output is
// carried forward via source_texture). Tests multi-layer
// backdrop sampling.
//
// Zone 3 (bottom-right, base layer): edge cases. A sigma=0 "mirror" panel (no blur), two
// same-sigma panels stacked (tests sub-batch coalescing
// via append_or_extend_sub_batch), and text drawn ON TOP
// of a backdrop (tests Pass B post-bracket rendering).
//
// Animation: an orbiting gradient stripe plus a few orbiting circles in Zone 1. Motion is the
// only way to visually confirm the blur is Gaussian; a static panel can't tell you whether the
// kernel coefficients are right.
gaussian_blur :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Backdrop blur", 800, 600, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW)
WINDOW_W :: f32(800)
WINDOW_H :: f32(600)
FONT_SIZE :: u16(14)
t: f32 = 0
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
t += 1
base_layer := draw.begin({width = WINDOW_W, height = WINDOW_H})
//----- Background fill ----------------------------------
draw.rectangle(base_layer, {0, 0, WINDOW_W, WINDOW_H}, draw.Color{20, 20, 28, 255})
//----- Zone 1: animated background for the top frosted panels ----------------------------------
// A wide rotating gradient stripe sweeps left-to-right across Zone 1. The angle changes
// over time so the gradient itself shifts visibly.
stripe_angle := t * 0.4
draw.rectangle(
base_layer,
{20, 20, WINDOW_W - 40, 240},
draw.Linear_Gradient {
start_color = {255, 80, 60, 255},
end_color = {60, 120, 255, 255},
angle = stripe_angle,
},
)
// Five orbiting circles inside Zone 1's strip. The blur should smooth their hard edges
// and the gradient behind them into a continuous wash.
for i in 0 ..< 5 {
phase := f32(i) * 1.2 + t * 0.04
cx := 100 + f32(i) * 140 + math.cos(phase) * 30
cy := 140 + math.sin(phase) * 50
circle_color := draw.Color {
u8(clamp(120 + math.cos(phase) * 100, 0, 255)),
u8(clamp(180 + math.sin(phase * 1.3) * 60, 0, 255)),
u8(clamp(220 - math.sin(phase) * 80, 0, 255)),
255,
}
draw.circle(base_layer, {cx, cy}, 22, circle_color)
}
// Bright accent rectangles to give the blur some sharp edges to munch on.
draw.rectangle(base_layer, {200, 60, 60, 12}, draw.Color{255, 255, 200, 255})
draw.rectangle(base_layer, {500, 200, 80, 16}, draw.Color{200, 255, 200, 255})
//----- Zone 1 frosted panels: different sigmas, different tints --------------------------------
// Panel A: heavy blur, cool blue-grey tint. sigma=14 in logical px.
// Both panels share rounded corners.
panel_radii := draw.Rectangle_Radii{16, 16, 16, 16}
draw.gaussian_blur(
base_layer,
{60, 80, 320, 140},
gaussian_sigma = 30,
tint = draw.Color{170, 200, 240, 200}, // cool blue, strong mix
radii = panel_radii,
)
draw.text(
base_layer,
"sigma = 20, cool tint",
{72, 90},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{30, 35, 50, 255},
)
// Panel B: lighter blur, warm amber tint. sigma=6.
draw.gaussian_blur(
base_layer,
{420, 80, 320, 140},
gaussian_sigma = 6,
tint = draw.Color{255, 220, 160, 200}, // warm amber, strong mix
radii = panel_radii,
)
draw.text(
base_layer,
"sigma = 6, warm tint",
{432, 90},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 40, 20, 255},
)
// Pass-B verification: a rectangle drawn AFTER the backdrops in the same layer
// Per the bracket scheduling model, this should render ON TOP of both panels above.
// If you see this stripe behind the panels instead of in front, something is wrong with
// the Pass B post-bracket path.
draw.rectangle(base_layer, {WINDOW_W * 0.5 - 4, 70, 8, 160}, draw.Color{255, 255, 255, 230})
//----- Zone 2: second layer with its own backdrop --------------------------------
// Zone 2's panel is in a NEW layer. Its bracket samples source_texture as it stands
// after the base layer fully finished (including the base layer's bracket V-composite
// output). So this panel sees Zone 1's frosted panels through its own blur.
zone2 := draw.new_layer(base_layer, {0, 280, WINDOW_W * 0.55, WINDOW_H - 280})
// Pass A content for zone2: a translucent darker overlay to make the panel pop.
draw.rectangle(zone2, {20, 300, WINDOW_W * 0.55 - 40, WINDOW_H - 320}, draw.Color{0, 0, 0, 80})
// Animated diagonal stripe in Zone 2 so the blur in this layer's panel has motion to
// smooth, not just the static base-layer content.
stripe_y := 320 + (math.sin(t * 0.05) * 0.5 + 0.5) * 200
draw.rectangle(zone2, {30, stripe_y, WINDOW_W * 0.55 - 60, 18}, draw.Color{255, 100, 200, 200})
// Zone 2's frosted panel.
draw.gaussian_blur(
zone2,
{60, 360, WINDOW_W * 0.55 - 120, 160},
gaussian_sigma = 10,
tint = draw.WHITE, // pure blur (white tint with any alpha is a no-op)
radii = draw.Rectangle_Radii{24, 24, 24, 24},
)
draw.text(
zone2,
"Layer 2 backdrop",
{72, 372},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{30, 30, 30, 255},
)
draw.text(
zone2,
"sigma = 10",
{72, 392},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 60, 60, 255},
)
//----- Zone 3: edge cases (back in base layer would also work, but we use zone2 to keep --------
// the demo's two-layer structure simple). Zone 3 lives in a third layer so it gets
// a fresh source snapshot too.
zone3 := draw.new_layer(zone2, {WINDOW_W * 0.55, 280, WINDOW_W * 0.45, WINDOW_H - 280})
// Animated background patch for Zone 3 so its mirror panel has something to reflect.
for i in 0 ..< 4 {
phase := f32(i) * 1.5 + t * 0.06
y := 310 + f32(i) * 60 + math.sin(phase) * 8
draw.rectangle(
zone3,
{WINDOW_W * 0.55 + 20, y, WINDOW_W * 0.45 - 40, 14},
draw.Color {
u8(clamp(200 + math.cos(phase) * 50, 0, 255)),
u8(clamp(150 + math.sin(phase) * 80, 0, 255)),
u8(clamp(220 - math.cos(phase * 1.7) * 60, 0, 255)),
255,
},
)
}
// Edge case 1: sigma = 0 "mirror" — sharp framebuffer sample, no blur. Should reproduce
// the underlying pixels exactly through the SDF mask. Tinted slightly so it's visible.
draw.gaussian_blur(
zone3,
{WINDOW_W * 0.55 + 30, 310, 150, 70},
gaussian_sigma = 0,
tint = draw.WHITE, // pure mirror (no blur, no tint)
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
draw.text(
zone3,
"sigma=0 (mirror)",
{WINDOW_W * 0.55 + 38, 318},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{20, 20, 20, 255},
)
// Edge case 2: two same-sigma panels submitted contiguously. The sub-batch coalescer
// should merge these into a single instanced V-composite draw. Visually, both should
// look identical (modulo position) — same blur radius, same tint.
draw.gaussian_blur(
zone3,
{WINDOW_W * 0.55 + 30, 400, 150, 70},
gaussian_sigma = 8,
tint = draw.Color{160, 255, 160, 200}, // green tint, strong mix
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
draw.gaussian_blur(
zone3,
{WINDOW_W * 0.55 + 200, 400, 150, 70},
gaussian_sigma = 8,
tint = draw.Color{160, 255, 160, 200}, // identical: tests sub-batch coalescing
radii = draw.Rectangle_Radii{12, 12, 12, 12},
)
draw.text(
zone3,
"sigma=8 (coalesced pair)",
{WINDOW_W * 0.55 + 38, 408},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{20, 40, 20, 255},
)
// Edge case 3: text drawn AFTER a backdrop in the same layer. Tests Pass B over a fresh
// V-composite output. The text should appear sharply on top of the green panels above.
draw.text(
zone3,
"Pass B text overlay",
{WINDOW_W * 0.55 + 38, 480},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
draw.end(gpu, window, draw.Color{15, 15, 22, 255})
}
}
// Backdrop diagnostic example.
//
// Minimal isolation harness for debugging the blur. ONE panel, ONE sigma, NO animation. The
// fixed background gives the eye a stable reference: the blur should smooth a *known* set of
// hard edges, and any artifacts (crisp circles, ghost mirrors, no apparent change with sigma)
// stand out clearly.
//
// Controls:
// UP / DOWN arrow : adjust sigma by ±1
// LEFT / RIGHT arrow : adjust sigma by ±5
// SPACE : reset to sigma=10
// T : toggle the test rectangle on top of the panel
//
// Sigma is printed to the console label and to the title bar so you can correlate visual
// behavior with kernel state (which is also logged via the [backdrop] debug print in
// backdrop.odin's compute_blur_kernel callsite).
gaussian_blur_debug :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Backdrop debug", 800, 600, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1)
defer draw.destroy(gpu)
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW)
WINDOW_W :: f32(800)
WINDOW_H :: f32(600)
FONT_SIZE :: u16(14)
sigma: f32 = 10
show_test_rect := true
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
if ev.type == .KEY_DOWN {
#partial switch ev.key.scancode {
case .UP: sigma += 1
case .DOWN: sigma = max(sigma - 1, 0)
case .RIGHT: sigma += 5
case .LEFT: sigma = max(sigma - 5, 0)
case .SPACE: sigma = 10
case .T: show_test_rect = !show_test_rect
}
}
}
// Update title with current sigma so we can correlate visuals to numbers.
title := fmt.ctprintf("Backdrop debug | sigma = %.1f", sigma)
sdl.SetWindowTitle(window, title)
base_layer := draw.begin({width = WINDOW_W, height = WINDOW_H})
// Background: deliberately high-contrast static content. The eye can verify whether
// hard edges (the black grid lines, the crisp circles, the fine vertical bars) get
// smoothed by the panel. NOTHING animates here — every difference between frames is
// caused by user input (sigma change), not by the demo itself.
draw.rectangle(base_layer, {0, 0, WINDOW_W, WINDOW_H}, draw.Color{255, 255, 255, 255})
// Black grid: 8x6 cells with thin lines. Each grid cell is 100x100 logical px.
for x: f32 = 0; x <= WINDOW_W; x += 100 {
draw.rectangle(base_layer, {x - 1, 0, 2, WINDOW_H}, draw.BLACK)
}
for y: f32 = 0; y <= WINDOW_H; y += 100 {
draw.rectangle(base_layer, {0, y - 1, WINDOW_W, 2}, draw.BLACK)
}
// A row of small bright circles across the middle. Their crisp edges are the most
// sensitive blur indicator.
for i in 0 ..< 8 {
cx := f32(i) * 100 + 50
color := draw.Color{u8((i * 32) & 0xff), u8((i * 64) & 0xff), u8(255 - (i * 32) & 0xff), 255}
draw.circle(base_layer, {cx, 350}, 25, color)
}
// Vertical fine-detail stripes on the left edge. At any meaningful sigma these should
// merge into a flat color through the panel.
for i in 0 ..< 20 {
x := 30 + f32(i) * 6
color := draw.RED if i % 2 == 0 else draw.BLUE
draw.rectangle(base_layer, {x, 200, 4, 200}, color)
}
// THE PANEL UNDER TEST. Square, centered, large enough to cover multiple grid cells and
// the circle row. Square shape makes any horizontal-vs-vertical asymmetry purely
// renderer-driven (geometry can't introduce it).
panel := draw.Rectangle{250, 150, 300, 300}
draw.gaussian_blur(
base_layer,
panel,
gaussian_sigma = sigma,
tint = draw.WHITE,
radii = draw.Rectangle_Radii{20, 20, 20, 20},
)
// Pass B test: a bright rectangle drawn AFTER the backdrop in the same layer. Should
// always render on top of the panel. If the panel ever shows a "ghost" of this rect
// inside its blur, the V-composite is sampling the wrong texture state.
if show_test_rect {
draw.rectangle(base_layer, {380, 280, 40, 40}, draw.Color{0, 200, 0, 255})
}
// Sigma label at the bottom in giant text so you can read it from across the room.
draw.text(
base_layer,
fmt.tprintf("sigma = %.1f", sigma),
{20, WINDOW_H - 40},
PLEX_SANS_REGULAR,
28,
color = draw.BLACK,
)
draw.text(
base_layer,
"UP/DOWN ±1 LEFT/RIGHT ±5 SPACE reset T toggle test rect",
{20, WINDOW_H - 70},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.Color{60, 60, 60, 255},
)
draw.end(gpu, window, draw.Color{255, 255, 255, 255})
}
}
+33 -10
View File
@@ -5,9 +5,32 @@ import "core:log"
import "core:mem" import "core:mem"
import "core:os" import "core:os"
EX_HELLOPE_SHAPES :: "hellope-shapes"
EX_HELLOPE_TEXT :: "hellope-text"
EX_HELLOPE_CLAY :: "hellope-clay"
EX_HELLOPE_CUSTOM :: "hellope-custom"
EX_TEXTURES :: "textures"
EX_GAUSSIAN_BLUR :: "gaussian-blur"
EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug"
AVAILABLE_EXAMPLES_MSG ::
"Available examples: " +
EX_HELLOPE_SHAPES +
", " +
EX_HELLOPE_TEXT +
", " +
EX_HELLOPE_CLAY +
", " +
EX_HELLOPE_CUSTOM +
", " +
EX_TEXTURES +
", " +
EX_GAUSSIAN_BLUR +
", " +
EX_GAUSSIAN_BLUR_DEBUG
main :: proc() { main :: proc() {
//----- General setup ---------------------------------- //----- General setup ----------------------------------
{
// Temp // Temp
track_temp: mem.Tracking_Allocator track_temp: mem.Tracking_Allocator
mem.tracking_allocator_init(&track_temp, context.temp_allocator) mem.tracking_allocator_init(&track_temp, context.temp_allocator)
@@ -43,27 +66,27 @@ main :: proc() {
} }
mem.tracking_allocator_destroy(&track) mem.tracking_allocator_destroy(&track)
} }
// Logger
context.logger = log.create_console_logger() context.logger = log.create_console_logger()
defer log.destroy_console_logger(context.logger) defer log.destroy_console_logger(context.logger)
}
args := os.args args := os.args
if len(args) < 2 { if len(args) < 2 {
fmt.eprintln("Usage: examples <example_name>") fmt.eprintln("Usage: examples <example_name>")
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom, textures") fmt.eprintln(AVAILABLE_EXAMPLES_MSG)
os.exit(1) os.exit(1)
} }
switch args[1] { switch args[1] {
case "hellope-clay": hellope_clay() case EX_HELLOPE_CLAY: hellope_clay()
case "hellope-custom": hellope_custom() case EX_HELLOPE_CUSTOM: hellope_custom()
case "hellope-shapes": hellope_shapes() case EX_HELLOPE_SHAPES: hellope_shapes()
case "hellope-text": hellope_text() case EX_HELLOPE_TEXT: hellope_text()
case "textures": textures() case EX_TEXTURES: textures()
case EX_GAUSSIAN_BLUR: gaussian_blur()
case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug()
case: case:
fmt.eprintf("Unknown example: %v\n", args[1]) fmt.eprintf("Unknown example: %v\n", args[1])
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom, textures") fmt.eprintln(AVAILABLE_EXAMPLES_MSG)
os.exit(1) os.exit(1)
} }
} }
+7 -15
View File
@@ -48,8 +48,7 @@ hellope_shapes :: proc() {
draw.rectangle( draw.rectangle(
base_layer, base_layer,
{20, 160, 460, 60}, {20, 160, 460, 60},
{255, 0, 0, 255}, draw.Linear_Gradient{start_color = {255, 0, 0, 255}, end_color = {0, 0, 255, 255}, angle = 0},
gradient = draw.Linear_Gradient{end_color = {0, 0, 255, 255}, angle = 0},
) )
// ----- Rotation demos ----- // ----- Rotation demos -----
@@ -79,18 +78,18 @@ hellope_shapes :: proc() {
) )
// Ellipse rotating around its center (tilted ellipse) // Ellipse rotating around its center (tilted ellipse)
draw.ellipse(base_layer, {410, 340}, 50, 30, {255, 200, 50, 255}, rotation = spin_angle) draw.ellipse(base_layer, {410, 340}, 50, 30, draw.Color{255, 200, 50, 255}, rotation = spin_angle)
// Circle orbiting a point (moon orbiting planet) // Circle orbiting a point (moon orbiting planet)
// Convention B: center = pivot point (planet), origin = offset from moon center to pivot. // Convention B: center = pivot point (planet), origin = offset from moon center to pivot.
// Moon's visual center at rotation=0: planet_pos - origin = (100, 450) - (0, 40) = (100, 410). // Moon's visual center at rotation=0: planet_pos - origin = (100, 450) - (0, 40) = (100, 410).
planet_pos := draw.Vec2{100, 450} planet_pos := draw.Vec2{100, 450}
draw.circle(base_layer, planet_pos, 8, {200, 200, 200, 255}) // planet (stationary) draw.circle(base_layer, planet_pos, 8, draw.Color{200, 200, 200, 255}) // planet (stationary)
draw.circle( draw.circle(
base_layer, base_layer,
planet_pos, planet_pos,
5, 5,
{100, 150, 255, 255}, draw.Color{100, 150, 255, 255},
origin = draw.Vec2{0, 40}, origin = draw.Vec2{0, 40},
rotation = spin_angle, rotation = spin_angle,
) // moon orbiting ) // moon orbiting
@@ -101,7 +100,7 @@ hellope_shapes :: proc() {
draw.Vec2{250, 450}, draw.Vec2{250, 450},
0, 0,
30, 30,
{100, 100, 220, 255}, draw.Color{100, 100, 220, 255},
start_angle = 0, start_angle = 0,
end_angle = 270, end_angle = 270,
rotation = spin_angle, rotation = spin_angle,
@@ -127,7 +126,7 @@ hellope_shapes :: proc() {
{460, 450}, {460, 450},
6, 6,
30, 30,
{180, 100, 220, 255}, draw.Color{180, 100, 220, 255},
outline_color = draw.WHITE, outline_color = draw.WHITE,
outline_width = 2, outline_width = 2,
rotation = spin_angle, rotation = spin_angle,
@@ -190,14 +189,7 @@ hellope_text :: proc() {
) )
// Uncached text (no id) — created and destroyed each frame, simplest usage // Uncached text (no id) — created and destroyed each frame, simplest usage
draw.text( draw.text(base_layer, "Top-left anchored", {20, 450}, PLEX_SANS_REGULAR, FONT_SIZE, color = draw.WHITE)
base_layer,
"Top-left anchored",
{20, 450},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Measure text for manual layout // Measure text for manual layout
size := draw.measure_text("Measured!", PLEX_SANS_REGULAR, FONT_SIZE) size := draw.measure_text("Measured!", PLEX_SANS_REGULAR, FONT_SIZE)
+158 -22
View File
@@ -9,7 +9,7 @@ import cyber "../cybersteel"
textures :: proc() { textures :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1) if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Textures", 800, 600, {.HIGH_PIXEL_DENSITY}) window := sdl.CreateWindow("Textures", 800, 750, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil) gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1) if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1) if !draw.init(gpu, window) do os.exit(1)
@@ -88,10 +88,10 @@ textures :: proc() {
} }
spin_angle += 1 spin_angle += 1
base_layer := draw.begin({width = 800, height = 600}) base_layer := draw.begin({width = 800, height = 750})
// Background // Background
draw.rectangle(base_layer, {0, 0, 800, 600}, draw.Color{30, 30, 30, 255}) draw.rectangle(base_layer, {0, 0, 800, 750}, draw.Color{30, 30, 30, 255})
//----- Row 1: Sampler presets (y=30) ---------------------------------- //----- Row 1: Sampler presets (y=30) ----------------------------------
@@ -103,11 +103,15 @@ textures :: proc() {
COL4 :: f32(480) COL4 :: f32(480)
// Nearest (sharp pixel edges) // Nearest (sharp pixel edges)
draw.rectangle_texture( draw.rectangle(
base_layer, base_layer,
{COL1, ROW1_Y, ITEM_SIZE, ITEM_SIZE}, {COL1, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
checker_texture, draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
},
) )
draw.text( draw.text(
base_layer, base_layer,
@@ -119,11 +123,15 @@ textures :: proc() {
) )
// Linear (bilinear blur) // Linear (bilinear blur)
draw.rectangle_texture( draw.rectangle(
base_layer, base_layer,
{COL2, ROW1_Y, ITEM_SIZE, ITEM_SIZE}, {COL2, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
checker_texture, draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Linear_Clamp, sampler = .Linear_Clamp,
},
) )
draw.text( draw.text(
base_layer, base_layer,
@@ -135,12 +143,15 @@ textures :: proc() {
) )
// Tiled (4x repeat) // Tiled (4x repeat)
draw.rectangle_texture( draw.rectangle(
base_layer, base_layer,
{COL3, ROW1_Y, ITEM_SIZE, ITEM_SIZE}, {COL3, ROW1_Y, ITEM_SIZE, ITEM_SIZE},
checker_texture, draw.Texture_Fill {
sampler = .Nearest_Repeat, id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 4, 4}, uv_rect = {0, 0, 4, 4},
sampler = .Nearest_Repeat,
},
) )
draw.text( draw.text(
base_layer, base_layer,
@@ -157,11 +168,10 @@ textures :: proc() {
// QR code (RGBA texture with baked colors, nearest sampling) // QR code (RGBA texture with baked colors, nearest sampling)
draw.rectangle(base_layer, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, draw.Color{255, 255, 255, 255}) // white bg draw.rectangle(base_layer, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, draw.Color{255, 255, 255, 255}) // white bg
draw.rectangle_texture( draw.rectangle(
base_layer, base_layer,
{COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
qr_texture, draw.Texture_Fill{id = qr_texture, tint = draw.WHITE, uv_rect = {0, 0, 1, 1}, sampler = .Nearest_Clamp},
sampler = .Nearest_Clamp,
) )
draw.text( draw.text(
base_layer, base_layer,
@@ -173,11 +183,15 @@ textures :: proc() {
) )
// Rounded corners // Rounded corners
draw.rectangle_texture( draw.rectangle(
base_layer, base_layer,
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, {COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
checker_texture, draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
},
radii = draw.uniform_radii({COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, 0.3), radii = draw.uniform_radii({COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, 0.3),
) )
draw.text( draw.text(
@@ -191,11 +205,15 @@ textures :: proc() {
// Rotating // Rotating
rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE} rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE}
draw.rectangle_texture( draw.rectangle(
base_layer, base_layer,
rot_rect, rot_rect,
checker_texture, draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
},
origin = draw.center_of(rot_rect), origin = draw.center_of(rot_rect),
rotation = spin_angle, rotation = spin_angle,
) )
@@ -216,7 +234,11 @@ textures :: proc() {
// Stretch // Stretch
uv_s, sampler_s, inner_s := draw.fit_params(.Stretch, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture) uv_s, sampler_s, inner_s := draw.fit_params(.Stretch, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
draw.rectangle(base_layer, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) // bg draw.rectangle(base_layer, {COL1, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) // bg
draw.rectangle_texture(base_layer, inner_s, stripe_texture, uv_rect = uv_s, sampler = sampler_s) draw.rectangle(
base_layer,
inner_s,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_s, sampler = sampler_s},
)
draw.text( draw.text(
base_layer, base_layer,
"Stretch", "Stretch",
@@ -229,7 +251,11 @@ textures :: proc() {
// Fill (center-crop) // Fill (center-crop)
uv_f, sampler_f, inner_f := draw.fit_params(.Fill, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture) uv_f, sampler_f, inner_f := draw.fit_params(.Fill, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
draw.rectangle(base_layer, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) draw.rectangle(base_layer, {COL2, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255})
draw.rectangle_texture(base_layer, inner_f, stripe_texture, uv_rect = uv_f, sampler = sampler_f) draw.rectangle(
base_layer,
inner_f,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_f, sampler = sampler_f},
)
draw.text( draw.text(
base_layer, base_layer,
"Fill", "Fill",
@@ -242,7 +268,11 @@ textures :: proc() {
// Fit (letterbox) // Fit (letterbox)
uv_ft, sampler_ft, inner_ft := draw.fit_params(.Fit, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture) uv_ft, sampler_ft, inner_ft := draw.fit_params(.Fit, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, stripe_texture)
draw.rectangle(base_layer, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) // visible margin bg draw.rectangle(base_layer, {COL3, ROW3_Y, FIT_SIZE, FIT_SIZE}, draw.Color{60, 60, 60, 255}) // visible margin bg
draw.rectangle_texture(base_layer, inner_ft, stripe_texture, uv_rect = uv_ft, sampler = sampler_ft) draw.rectangle(
base_layer,
inner_ft,
draw.Texture_Fill{id = stripe_texture, tint = draw.WHITE, uv_rect = uv_ft, sampler = sampler_ft},
)
draw.text( draw.text(
base_layer, base_layer,
"Fit", "Fit",
@@ -253,11 +283,15 @@ textures :: proc() {
) )
// Per-corner radii // Per-corner radii
draw.rectangle_texture( draw.rectangle(
base_layer, base_layer,
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE}, {COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
checker_texture, draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
},
radii = {20, 0, 20, 0}, radii = {20, 0, 20, 0},
) )
draw.text( draw.text(
@@ -269,6 +303,108 @@ textures :: proc() {
color = draw.WHITE, color = draw.WHITE,
) )
//----- Row 4: Textured shapes (y=520) ----------------------------------
ROW4_Y :: f32(520)
SHAPE_SIZE :: f32(80)
SHAPE_GAP :: f32(30)
SHAPE_COL1 :: f32(30)
SHAPE_COL2 :: SHAPE_COL1 + SHAPE_SIZE + SHAPE_GAP
SHAPE_COL3 :: SHAPE_COL2 + SHAPE_SIZE + SHAPE_GAP
SHAPE_COL4 :: SHAPE_COL3 + SHAPE_SIZE + SHAPE_GAP
SHAPE_COL5 :: SHAPE_COL4 + SHAPE_SIZE + SHAPE_GAP
checker_fill := draw.Texture_Fill {
id = checker_texture,
tint = draw.WHITE,
uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp,
}
// Textured circle
draw.circle(
base_layer,
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
SHAPE_SIZE / 2,
checker_fill,
)
draw.text(
base_layer,
"Circle",
{SHAPE_COL1, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Textured ellipse
draw.ellipse(
base_layer,
{SHAPE_COL2 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
SHAPE_SIZE / 2,
SHAPE_SIZE / 3,
checker_fill,
)
draw.text(
base_layer,
"Ellipse",
{SHAPE_COL2, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Textured polygon (hexagon)
draw.polygon(
base_layer,
{SHAPE_COL3 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
6,
SHAPE_SIZE / 2,
checker_fill,
)
draw.text(
base_layer,
"Polygon",
{SHAPE_COL3, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Textured ring
draw.ring(
base_layer,
{SHAPE_COL4 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
SHAPE_SIZE / 4,
SHAPE_SIZE / 2,
checker_fill,
)
draw.text(
base_layer,
"Ring",
{SHAPE_COL4, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Textured line (capsule)
draw.line(
base_layer,
{SHAPE_COL5, ROW4_Y + SHAPE_SIZE / 2},
{SHAPE_COL5 + SHAPE_SIZE, ROW4_Y + SHAPE_SIZE / 2},
checker_fill,
thickness = 20,
)
draw.text(
base_layer,
"Line",
{SHAPE_COL5, ROW4_Y + SHAPE_SIZE + LABEL_OFFSET},
PLEX_SANS_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
draw.end(gpu, window) draw.end(gpu, window)
} }
} }
+133 -28
View File
@@ -29,7 +29,7 @@ TextBatch :: struct {
// ---------------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------
// The SDF path evaluates one of four signed distance functions per primitive, dispatched // The SDF path evaluates one of four signed distance functions per primitive, dispatched
// by Shape_Kind encoded in the low byte of Primitive.flags: // by Shape_Kind encoded in the low byte of Base_2D_Primitive.flags:
// //
// RRect — rounded rectangle with per-corner radii (sdRoundedBox). Also covers circles // RRect — rounded rectangle with per-corner radii (sdRoundedBox). Also covers circles
// (uniform radii = half-size), capsule-style line segments (rotated, max rounding), // (uniform radii = half-size), capsule-style line segments (rotated, max rounding),
@@ -47,10 +47,10 @@ Shape_Kind :: enum u8 {
} }
Shape_Flag :: enum u8 { Shape_Flag :: enum u8 {
Textured, // bit 0: sample texture using uv.uv_rect (mutually exclusive with Gradient) Textured, // bit 0: sample texture using uv_rect (mutually exclusive with Gradient via Brush union)
Gradient, // bit 1: 2-color gradient using uv.effects.gradient_color as end/outer color Gradient, // bit 1: 2-color gradient using effects.gradient_color as end/outer color
Gradient_Radial, // bit 2: if set with Gradient, radial from center; else linear at angle Gradient_Radial, // bit 2: if set with Gradient, radial from center; else linear at angle
Outline, // bit 3: outer outline band using uv.effects.outline_color; CPU expands bounds by outline_width Outline, // bit 3: outer outline band using effects.outline_color; CPU expands bounds by outline_width
Rotated, // bit 4: shape has non-zero rotation; rotation_sc contains packed sin/cos Rotated, // bit 4: shape has non-zero rotation; rotation_sc contains packed sin/cos
Arc_Narrow, // bit 5: ring arc span ≤ π — intersect half-planes. Neither Arc bit = full ring. Arc_Narrow, // bit 5: ring arc span ≤ π — intersect half-planes. Neither Arc bit = full ring.
Arc_Wide, // bit 6: ring arc span > π — union half-planes. Neither Arc bit = full ring. Arc_Wide, // bit 6: ring arc span > π — union half-planes. Neither Arc bit = full ring.
@@ -97,7 +97,7 @@ Shape_Params :: struct #raw_union {
#assert(size_of(Shape_Params) == 32) #assert(size_of(Shape_Params) == 32)
// GPU-side storage for 2-color gradient parameters and/or outline parameters. // GPU-side storage for 2-color gradient parameters and/or outline parameters.
// Packed into 16 bytes to alias with uv_rect in the Uv_Or_Effects raw union. // Packed into 16 bytes. Independent from uv_rect — texture and outline can coexist.
// The shader reads gradient_color and outline_color via unpackUnorm4x8. // The shader reads gradient_color and outline_color via unpackUnorm4x8.
// gradient_dir_sc stores the pre-computed gradient direction as (cos, sin) in f16 pair // gradient_dir_sc stores the pre-computed gradient direction as (cos, sin) in f16 pair
// via unpackHalf2x16. outline_packed stores outline_width as f16 via unpackHalf2x16. // via unpackHalf2x16. outline_packed stores outline_width as f16 via unpackHalf2x16.
@@ -107,38 +107,33 @@ Gradient_Outline :: struct {
gradient_dir_sc: u32, // 8: packed f16 pair: low = cos(angle), high = sin(angle) — pre-computed gradient direction gradient_dir_sc: u32, // 8: packed f16 pair: low = cos(angle), high = sin(angle) — pre-computed gradient direction
outline_packed: u32, // 12: packed f16 pair: low = outline_width (f16, physical pixels), high = reserved outline_packed: u32, // 12: packed f16 pair: low = outline_width (f16, physical pixels), high = reserved
} }
#assert(size_of(Gradient_Outline) == 16) #assert(size_of(Gradient_Outline) == 16)
// Uv_Or_Effects aliases the final 16 bytes of a Primitive. When .Textured is set, // GPU layout: 96 bytes, std430-compatible. The shader declares this as a storage buffer struct.
// uv_rect holds texture-atlas coordinates. When .Gradient or .Outline is set,
// effects holds 2-color gradient parameters and/or outline parameters.
// Textured and Gradient are mutually exclusive; if both are set, Gradient takes precedence.
Uv_Or_Effects :: struct #raw_union {
uv_rect: [4]f32, // u_min, v_min, u_max, v_max (default {0,0,1,1})
effects: Gradient_Outline, // gradient + outline parameters
}
// GPU layout: 80 bytes, std430-compatible. The shader declares this as a storage buffer struct.
// The low byte of `flags` encodes the Shape_Kind (0 = tessellated, 1-4 = SDF kinds). // The low byte of `flags` encodes the Shape_Kind (0 = tessellated, 1-4 = SDF kinds).
// Bits 8-15 encode Shape_Flags (Textured, Gradient, Gradient_Radial, Outline, Rotated, Arc_Narrow, Arc_Wide). // Bits 8-15 encode Shape_Flags (Textured, Gradient, Gradient_Radial, Outline, Rotated, Arc_Narrow, Arc_Wide).
// rotation_sc stores pre-computed sin/cos of the rotation angle as a packed f16 pair, // rotation_sc stores pre-computed sin/cos of the rotation angle as a packed f16 pair,
// avoiding per-pixel trigonometry in the fragment shader. Only read when .Rotated is set. // avoiding per-pixel trigonometry in the fragment shader. Only read when .Rotated is set.
Primitive :: struct { //
// Named Base_2D_Primitive (not just Primitive) to disambiguate from Backdrop_Primitive in
// pipeline_2d_backdrop.odin. The two pipelines have unrelated GPU layouts and unrelated
// fragment-shader contracts; pairing each with its own primitive type keeps cross-references
// unambiguous when grepping the codebase.
Base_2D_Primitive :: struct {
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI) bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
color: Color, // 16: u8x4, fill color / gradient start color / texture tint color: Color, // 16: u8x4, fill color / gradient start color / texture tint
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
rotation_sc: u32, // 24: packed f16 pair: low = sin(angle), high = cos(angle). Requires .Rotated flag. rotation_sc: u32, // 24: packed f16 pair: low = sin(angle), high = cos(angle). Requires .Rotated flag.
_pad: f32, // 28: reserved for future use _pad: f32, // 28: reserved for future use
params: Shape_Params, // 32: per-kind shape parameters (raw union, 32 bytes) params: Shape_Params, // 32: per-kind shape parameters (raw union, 32 bytes)
uv: Uv_Or_Effects, // 64: texture coords or gradient/outline parameters uv_rect: [4]f32, // 64: texture UV coordinates (u_min, v_min, u_max, v_max). Read when .Textured.
effects: Gradient_Outline, // 80: gradient and/or outline parameters. Read when .Gradient and/or .Outline.
} }
#assert(size_of(Base_2D_Primitive) == 96)
#assert(size_of(Primitive) == 80) // Pack shape kind and flags into the Base_2D_Primitive.flags field. The low byte encodes the
// Shape_Kind (which also serves as the SDF mode marker — kind > 0 means SDF path). The
// Pack shape kind and flags into the Primitive.flags field. The low byte encodes the Shape_Kind // tessellated path leaves the field at 0 (Solid kind, set by vertex shader zero-initialization).
// (which also serves as the SDF mode marker — kind > 0 means SDF path). The tessellated path
// leaves the field at 0 (Solid kind, set by vertex shader zero-initialization).
pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 { pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 {
return u32(kind) | (u32(transmute(u8)flags) << 8) return u32(kind) | (u32(transmute(u8)flags) << 8)
} }
@@ -159,11 +154,13 @@ Pipeline_2D_Base :: struct {
sampler: ^sdl.GPUSampler, sampler: ^sdl.GPUSampler,
} }
// MSAA is not supported by levlib (see init's doc comment in draw.odin); the PSO is hard-wired
// to single-sample. SDF text and shapes provide analytical AA via smoothstep; tessellated user
// geometry is not anti-aliased.
@(private) @(private)
create_pipeline_2d_base :: proc( create_pipeline_2d_base :: proc(
device: ^sdl.GPUDevice, device: ^sdl.GPUDevice,
window: ^sdl.Window, window: ^sdl.Window,
sample_count: sdl.GPUSampleCount,
) -> ( ) -> (
pipeline: Pipeline_2D_Base, pipeline: Pipeline_2D_Base,
ok: bool, ok: bool,
@@ -237,7 +234,7 @@ create_pipeline_2d_base :: proc(
vertex_shader = vert_shader, vertex_shader = vert_shader,
fragment_shader = frag_shader, fragment_shader = frag_shader,
primitive_type = .TRIANGLELIST, primitive_type = .TRIANGLELIST,
multisample_state = sdl.GPUMultisampleState{sample_count = sample_count}, multisample_state = sdl.GPUMultisampleState{sample_count = ._1},
target_info = sdl.GPUGraphicsPipelineTargetInfo { target_info = sdl.GPUGraphicsPipelineTargetInfo {
color_target_descriptions = &sdl.GPUColorTargetDescription { color_target_descriptions = &sdl.GPUColorTargetDescription {
format = sdl.GetGPUSwapchainTextureFormat(device, window), format = sdl.GetGPUSwapchainTextureFormat(device, window),
@@ -302,7 +299,7 @@ create_pipeline_2d_base :: proc(
prim_buf_ok: bool prim_buf_ok: bool
pipeline.primitive_buffer, prim_buf_ok = create_buffer( pipeline.primitive_buffer, prim_buf_ok = create_buffer(
device, device,
size_of(Primitive) * BUFFER_INIT_SIZE, size_of(Base_2D_Primitive) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
) )
if !prim_buf_ok do return pipeline, false if !prim_buf_ok do return pipeline, false
@@ -505,7 +502,7 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
// Upload SDF primitives // Upload SDF primitives
prim_count := u32(len(GLOB.tmp_primitives)) prim_count := u32(len(GLOB.tmp_primitives))
if prim_count > 0 { if prim_count > 0 {
prim_size := prim_count * size_of(Primitive) prim_size := prim_count * size_of(Base_2D_Primitive)
grow_buffer_if_needed( grow_buffer_if_needed(
device, device,
@@ -560,6 +557,101 @@ draw_layer :: proc(
return return
} }
bracket_start_abs := find_first_backdrop_in_layer(layer)
layer_end_abs := int(layer.sub_batch_start + layer.sub_batch_len)
if bracket_start_abs < 0 {
// Fast path: no backdrop in this layer; render the whole sub-batch range in one pass.
render_layer_sub_batch_range(
cmd_buffer,
render_texture,
swapchain_width,
swapchain_height,
clear_color,
layer,
int(layer.sub_batch_start),
layer_end_abs,
)
return
}
// Bracketed layer: Pass A → backdrop bracket → Pass B.
// See README.md § "Backdrop pipeline" for the full ordering semantics.
render_layer_sub_batch_range(
cmd_buffer,
render_texture,
swapchain_width,
swapchain_height,
clear_color,
layer,
int(layer.sub_batch_start),
bracket_start_abs,
)
run_backdrop_bracket(cmd_buffer, layer, swapchain_width, swapchain_height)
// Pass B: render the [bracket_start_abs, layer_end_abs) range. .Backdrop sub-batches in
// this range are dispatched by the bracket above and ignored here (the .Backdrop case in
// the inner switch is a no-op). LOAD is implied because Pass A or the bracket's V-
// composite has already touched render_texture.
render_layer_sub_batch_range(
cmd_buffer,
render_texture,
swapchain_width,
swapchain_height,
clear_color,
layer,
bracket_start_abs,
layer_end_abs,
)
}
// Render a sub-range of a layer's sub-batches in a single render pass. Iterates the layer's
// scissors and walks each scissor's sub-batches, dispatching by kind. The `range_start_abs`
// and `range_end_abs` parameters are absolute indices into GLOB.tmp_sub_batches; only sub-
// batches within `[range_start_abs, range_end_abs)` are drawn.
//
// .Backdrop sub-batches in the range are always silently skipped — they are dispatched by
// run_backdrop_bracket, not here. The empty .Backdrop case in the inner switch enforces this.
//
// Render-pass setup mirrors the original draw_layer: clear-or-load based on GLOB.cleared,
// pipeline + storage + index buffer bound up front, then per-batch state tracking. After this
// proc returns, GLOB.cleared is guaranteed true.
//
// If the range is empty after filtering (no eligible sub-batches at all), this proc still
// honors the no-clear-yet contract by issuing a clear-only pass when needed; otherwise it
// returns without opening a render pass.
@(private)
render_layer_sub_batch_range :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer,
render_texture: ^sdl.GPUTexture,
swapchain_width: u32,
swapchain_height: u32,
clear_color: [4]f32,
layer: ^Layer,
range_start_abs: int,
range_end_abs: int,
) {
if range_start_abs >= range_end_abs {
// Empty range. If we still owe a clear, do a clear-only pass; otherwise nothing to do.
if !GLOB.cleared {
pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
load_op = .CLEAR,
store_op = .STORE,
},
1,
nil,
)
sdl.EndGPURenderPass(pass)
GLOB.cleared = true
}
return
}
render_pass := sdl.BeginGPURenderPass( render_pass := sdl.BeginGPURenderPass(
cmd_buffer, cmd_buffer,
&sdl.GPUColorTargetInfo { &sdl.GPUColorTargetInfo {
@@ -611,9 +703,17 @@ draw_layer :: proc(
text_vertex_gpu_base := u32(len(GLOB.tmp_shape_verts)) text_vertex_gpu_base := u32(len(GLOB.tmp_shape_verts))
for &scissor in GLOB.scissors[layer.scissor_start:][:layer.scissor_len] { for &scissor in GLOB.scissors[layer.scissor_start:][:layer.scissor_len] {
// Intersect this scissor's sub-batch span with the requested range.
scissor_start := int(scissor.sub_batch_start)
scissor_end := scissor_start + int(scissor.sub_batch_len)
effective_start := max(scissor_start, range_start_abs)
effective_end := min(scissor_end, range_end_abs)
if effective_start >= effective_end do continue
sdl.SetGPUScissor(render_pass, scissor.bounds) sdl.SetGPUScissor(render_pass, scissor.bounds)
for &batch in GLOB.tmp_sub_batches[scissor.sub_batch_start:][:scissor.sub_batch_len] { for abs_idx in effective_start ..< effective_end {
batch := &GLOB.tmp_sub_batches[abs_idx]
switch batch.kind { switch batch.kind {
case .Tessellated: case .Tessellated:
if current_mode != .Tessellated { if current_mode != .Tessellated {
@@ -702,6 +802,11 @@ draw_layer :: proc(
current_sampler = batch_sampler current_sampler = batch_sampler
} }
sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset) sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset)
case .Backdrop:
// Always a no-op here. Backdrop sub-batches are dispatched by run_backdrop_bracket;
// when this proc encounters one (only possible in Pass B, since Pass A and the no-
// backdrop fast path both stop their range before any .Backdrop index), we skip it.
} }
} }
} }
@@ -0,0 +1,118 @@
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct Uniforms
{
float2 inv_working_size;
uint pair_count;
uint mode;
float2 direction;
float inv_downsample_factor;
float _pad0;
float4 kernel0[32];
};
struct main0_out
{
float4 out_color [[color(0)]];
};
struct main0_in
{
float2 p_local [[user(locn0)]];
float4 f_color [[user(locn1)]];
float2 f_half_size [[user(locn2), flat]];
float4 f_radii [[user(locn3), flat]];
float f_half_feather [[user(locn4), flat]];
};
static inline __attribute__((always_inline))
float3 blur_sample(thread const float2& uv, constant Uniforms& _108, texture2d<float> blur_input_tex, sampler blur_input_texSmplr)
{
float3 color = blur_input_tex.sample(blur_input_texSmplr, uv).xyz * _108.kernel0[0].x;
float2 axis_step = _108.direction * _108.inv_working_size;
for (uint i = 1u; i < _108.pair_count; i++)
{
float w = _108.kernel0[i].x;
float off = _108.kernel0[i].y;
float2 step_uv = axis_step * off;
color += (blur_input_tex.sample(blur_input_texSmplr, (uv - step_uv)).xyz * w);
color += (blur_input_tex.sample(blur_input_texSmplr, (uv + step_uv)).xyz * w);
}
return color;
}
static inline __attribute__((always_inline))
float sdRoundedBox(thread const float2& p, thread const float2& b, thread const float4& r)
{
float2 _36;
if (p.x > 0.0)
{
_36 = r.xy;
}
else
{
_36 = r.zw;
}
float2 rxy = _36;
float _50;
if (p.y > 0.0)
{
_50 = rxy.x;
}
else
{
_50 = rxy.y;
}
float rr = _50;
float2 q = abs(p) - b;
if (rr == 0.0)
{
return fast::max(q.x, q.y);
}
q += float2(rr);
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - rr;
}
static inline __attribute__((always_inline))
float sdf_alpha(thread const float& d, thread const float& h)
{
return 1.0 - smoothstep(-h, h, d);
}
fragment main0_out main0(main0_in in [[stage_in]], constant Uniforms& _108 [[buffer(0)]], texture2d<float> blur_input_tex [[texture(0)]], sampler blur_input_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
{
main0_out out = {};
if (_108.mode == 0u)
{
float2 uv = gl_FragCoord.xy * _108.inv_working_size;
float2 param = uv;
float3 color = blur_sample(param, _108, blur_input_tex, blur_input_texSmplr);
out.out_color = float4(color, 1.0);
return out;
}
float2 param_1 = in.p_local;
float2 param_2 = in.f_half_size;
float4 param_3 = in.f_radii;
float d = sdRoundedBox(param_1, param_2, param_3);
if (d > in.f_half_feather)
{
discard_fragment();
}
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
float d_n = d / grad_magnitude;
float h_n = in.f_half_feather / grad_magnitude;
float2 uv_1 = (gl_FragCoord.xy * _108.inv_downsample_factor) * _108.inv_working_size;
float3 color_1 = blur_input_tex.sample(blur_input_texSmplr, uv_1).xyz;
float3 tinted = mix(color_1, color_1 * in.f_color.xyz, float3(in.f_color.w));
float param_4 = d_n;
float param_5 = h_n;
float coverage = sdf_alpha(param_4, param_5);
out.out_color = float4(tinted * coverage, coverage);
return out;
}
Binary file not shown.
@@ -0,0 +1,123 @@
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#pragma clang diagnostic ignored "-Wmissing-braces"
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
template<typename T, size_t Num>
struct spvUnsafeArray
{
T elements[Num ? Num : 1];
thread T& operator [] (size_t pos) thread
{
return elements[pos];
}
constexpr const thread T& operator [] (size_t pos) const thread
{
return elements[pos];
}
device T& operator [] (size_t pos) device
{
return elements[pos];
}
constexpr const device T& operator [] (size_t pos) const device
{
return elements[pos];
}
constexpr const constant T& operator [] (size_t pos) const constant
{
return elements[pos];
}
threadgroup T& operator [] (size_t pos) threadgroup
{
return elements[pos];
}
constexpr const threadgroup T& operator [] (size_t pos) const threadgroup
{
return elements[pos];
}
};
struct Uniforms
{
float4x4 projection;
float dpi_scale;
uint mode;
float2 _pad0;
};
struct Backdrop_Primitive
{
float4 bounds;
float4 radii;
float2 half_size;
float half_feather;
uint color;
};
struct Backdrop_Primitive_1
{
float4 bounds;
float4 radii;
float2 half_size;
float half_feather;
uint color;
};
struct Backdrop_Primitives
{
Backdrop_Primitive_1 primitives[1];
};
constant spvUnsafeArray<float2, 6> _97 = spvUnsafeArray<float2, 6>({ float2(0.0), float2(1.0, 0.0), float2(0.0, 1.0), float2(0.0, 1.0), float2(1.0, 0.0), float2(1.0) });
struct main0_out
{
float2 p_local [[user(locn0)]];
float4 f_color [[user(locn1)]];
float2 f_half_size [[user(locn2)]];
float4 f_radii [[user(locn3)]];
float f_half_feather [[user(locn4)]];
float4 gl_Position [[position]];
};
vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Backdrop_Primitives& _69 [[buffer(1)]], uint gl_VertexIndex [[vertex_id]], uint gl_InstanceIndex [[instance_id]])
{
main0_out out = {};
if (_13.mode == 0u)
{
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
out.gl_Position = float4(ndc, 0.0, 1.0);
out.p_local = float2(0.0);
out.f_color = float4(0.0);
out.f_half_size = float2(0.0);
out.f_radii = float4(0.0);
out.f_half_feather = 0.0;
}
else
{
Backdrop_Primitive p;
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
p.radii = _69.primitives[int(gl_InstanceIndex)].radii;
p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size;
p.half_feather = _69.primitives[int(gl_InstanceIndex)].half_feather;
p.color = _69.primitives[int(gl_InstanceIndex)].color;
float2 corner = _97[int(gl_VertexIndex)];
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
out.p_local = (world_pos - center) * _13.dpi_scale;
out.f_color = unpack_unorm4x8_to_float(p.color);
out.f_half_size = p.half_size;
out.f_radii = p.radii;
out.f_half_feather = p.half_feather;
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
}
return out;
}
Binary file not shown.
@@ -0,0 +1,47 @@
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct Uniforms
{
float2 inv_source_size;
uint downsample_factor;
uint _pad0;
};
struct main0_out
{
float4 out_color [[color(0)]];
};
fragment main0_out main0(constant Uniforms& _18 [[buffer(0)]], texture2d<float> source_tex [[texture(0)]], sampler source_texSmplr [[sampler(0)]], float4 gl_FragCoord [[position]])
{
main0_out out = {};
float2 src_block_center = gl_FragCoord.xy * float(_18.downsample_factor);
if (_18.downsample_factor == 1u)
{
float2 uv = src_block_center * _18.inv_source_size;
out.out_color = source_tex.sample(source_texSmplr, uv);
}
else
{
if (_18.downsample_factor == 2u)
{
float2 uv_1 = src_block_center * _18.inv_source_size;
out.out_color = source_tex.sample(source_texSmplr, uv_1);
}
else
{
float off = float(_18.downsample_factor) * 0.25;
float2 uv_tl = (src_block_center + float2(-off, -off)) * _18.inv_source_size;
float2 uv_tr = (src_block_center + float2(off, -off)) * _18.inv_source_size;
float2 uv_bl = (src_block_center + float2(-off, off)) * _18.inv_source_size;
float2 uv_br = (src_block_center + float2(off)) * _18.inv_source_size;
float4 c = ((source_tex.sample(source_texSmplr, uv_tl) + source_tex.sample(source_texSmplr, uv_tr)) + source_tex.sample(source_texSmplr, uv_bl)) + source_tex.sample(source_texSmplr, uv_br);
out.out_color = c * 0.25;
}
}
return out;
}
Binary file not shown.
@@ -0,0 +1,18 @@
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct main0_out
{
float4 gl_Position [[position]];
};
vertex main0_out main0(uint gl_VertexIndex [[vertex_id]])
{
main0_out out = {};
float2 ndc = float2((int(gl_VertexIndex) == 1) ? 3.0 : (-1.0), (int(gl_VertexIndex) == 2) ? 3.0 : (-1.0));
out.gl_Position = float4(ndc, 0.0, 1.0);
return out;
}
Binary file not shown.
+11 -16
View File
@@ -24,8 +24,8 @@ struct main0_in
float4 f_params [[user(locn2)]]; float4 f_params [[user(locn2)]];
float4 f_params2 [[user(locn3)]]; float4 f_params2 [[user(locn3)]];
uint f_flags [[user(locn4)]]; uint f_flags [[user(locn4)]];
uint f_rotation_sc [[user(locn5)]]; float4 f_uv_rect [[user(locn6), flat]];
uint4 f_uv_or_effects [[user(locn6)]]; uint4 f_effects [[user(locn7)]];
}; };
static inline __attribute__((always_inline)) static inline __attribute__((always_inline))
@@ -109,11 +109,6 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
float h = 0.5; float h = 0.5;
float2 half_size = in.f_params.xy; float2 half_size = in.f_params.xy;
float2 p_local = in.f_local_or_uv; float2 p_local = in.f_local_or_uv;
if ((flags & 16u) != 0u)
{
float2 sc = float2(as_type<half2>(in.f_rotation_sc));
p_local = float2((sc.y * p_local.x) + (sc.x * p_local.y), ((-sc.x) * p_local.x) + (sc.y * p_local.y));
}
if (kind == 1u) if (kind == 1u)
{ {
float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy); float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy);
@@ -163,16 +158,16 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
{ {
float d_start = dot(p_local, n_start); float d_start = dot(p_local, n_start);
float d_end = dot(p_local, n_end); float d_end = dot(p_local, n_end);
float _372; float _338;
if (arc_bits == 1u) if (arc_bits == 1u)
{ {
_372 = fast::max(d_start, d_end); _338 = fast::max(d_start, d_end);
} }
else else
{ {
_372 = fast::min(d_start, d_end); _338 = fast::min(d_start, d_end);
} }
float d_wedge = _372; float d_wedge = _338;
d = fast::max(d, d_wedge); d = fast::max(d, d_wedge);
} }
half_size = float2(outer); half_size = float2(outer);
@@ -187,7 +182,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
if ((flags & 2u) != 0u) if ((flags & 2u) != 0u)
{ {
float4 gradient_start = in.f_color; float4 gradient_start = in.f_color;
float4 gradient_end = unpack_unorm4x8_to_float(in.f_uv_or_effects.x); float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x);
if ((flags & 4u) != 0u) if ((flags & 4u) != 0u)
{ {
float t_1 = length(p_local / half_size); float t_1 = length(p_local / half_size);
@@ -198,7 +193,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
} }
else else
{ {
float2 direction = float2(as_type<half2>(in.f_uv_or_effects.z)); float2 direction = float2(as_type<half2>(in.f_effects.z));
float t_2 = (dot(p_local / half_size, direction) * 0.5) + 0.5; float t_2 = (dot(p_local / half_size, direction) * 0.5) + 0.5;
float4 param_11 = gradient_start; float4 param_11 = gradient_start;
float4 param_12 = gradient_end; float4 param_12 = gradient_end;
@@ -210,7 +205,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
{ {
if ((flags & 1u) != 0u) if ((flags & 1u) != 0u)
{ {
float4 uv_rect = as_type<float4>(in.f_uv_or_effects); float4 uv_rect = in.f_uv_rect;
float2 local_uv = ((p_local / half_size) * 0.5) + float2(0.5); float2 local_uv = ((p_local / half_size) * 0.5) + float2(0.5);
float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv); float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
shape_color = in.f_color * tex.sample(texSmplr, uv); shape_color = in.f_color * tex.sample(texSmplr, uv);
@@ -222,8 +217,8 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
} }
if ((flags & 8u) != 0u) if ((flags & 8u) != 0u)
{ {
float4 ol_color = unpack_unorm4x8_to_float(in.f_uv_or_effects.y); float4 ol_color = unpack_unorm4x8_to_float(in.f_effects.y);
float ol_width = float2(as_type<half2>(in.f_uv_or_effects.w)).x / grad_magnitude; float ol_width = float2(as_type<half2>(in.f_effects.w)).x / grad_magnitude;
float param_14 = d; float param_14 = d;
float param_15 = h; float param_15 = h;
float fill_cov = sdf_alpha(param_14, param_15); float fill_cov = sdf_alpha(param_14, param_15);
Binary file not shown.
+26 -16
View File
@@ -10,7 +10,7 @@ struct Uniforms
uint mode; uint mode;
}; };
struct Primitive struct Base_2D_Primitive
{ {
float4 bounds; float4 bounds;
uint color; uint color;
@@ -19,10 +19,11 @@ struct Primitive
float _pad; float _pad;
float4 params; float4 params;
float4 params2; float4 params2;
uint4 uv_or_effects; float4 uv_rect;
uint4 effects;
}; };
struct Primitive_1 struct Base_2D_Primitive_1
{ {
float4 bounds; float4 bounds;
uint color; uint color;
@@ -31,12 +32,13 @@ struct Primitive_1
float _pad; float _pad;
float4 params; float4 params;
float4 params2; float4 params2;
uint4 uv_or_effects; float4 uv_rect;
uint4 effects;
}; };
struct Primitives struct Base_2D_Primitives
{ {
Primitive_1 primitives[1]; Base_2D_Primitive_1 primitives[1];
}; };
struct main0_out struct main0_out
@@ -46,8 +48,8 @@ struct main0_out
float4 f_params [[user(locn2)]]; float4 f_params [[user(locn2)]];
float4 f_params2 [[user(locn3)]]; float4 f_params2 [[user(locn3)]];
uint f_flags [[user(locn4)]]; uint f_flags [[user(locn4)]];
uint f_rotation_sc [[user(locn5)]]; float4 f_uv_rect [[user(locn6)]];
uint4 f_uv_or_effects [[user(locn6)]]; uint4 f_effects [[user(locn7)]];
float4 gl_Position [[position]]; float4 gl_Position [[position]];
}; };
@@ -58,7 +60,7 @@ struct main0_in
float4 v_color [[attribute(2)]]; float4 v_color [[attribute(2)]];
}; };
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]]) vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Base_2D_Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
{ {
main0_out out = {}; main0_out out = {};
if (_12.mode == 0u) if (_12.mode == 0u)
@@ -68,13 +70,13 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
out.f_params = float4(0.0); out.f_params = float4(0.0);
out.f_params2 = float4(0.0); out.f_params2 = float4(0.0);
out.f_flags = 0u; out.f_flags = 0u;
out.f_rotation_sc = 0u; out.f_uv_rect = float4(0.0);
out.f_uv_or_effects = uint4(0u); out.f_effects = uint4(0u);
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0); out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
} }
else else
{ {
Primitive p; Base_2D_Primitive p;
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds; p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
p.color = _75.primitives[int(gl_InstanceIndex)].color; p.color = _75.primitives[int(gl_InstanceIndex)].color;
p.flags = _75.primitives[int(gl_InstanceIndex)].flags; p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
@@ -82,17 +84,25 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
p._pad = _75.primitives[int(gl_InstanceIndex)]._pad; p._pad = _75.primitives[int(gl_InstanceIndex)]._pad;
p.params = _75.primitives[int(gl_InstanceIndex)].params; p.params = _75.primitives[int(gl_InstanceIndex)].params;
p.params2 = _75.primitives[int(gl_InstanceIndex)].params2; p.params2 = _75.primitives[int(gl_InstanceIndex)].params2;
p.uv_or_effects = _75.primitives[int(gl_InstanceIndex)].uv_or_effects; p.uv_rect = _75.primitives[int(gl_InstanceIndex)].uv_rect;
p.effects = _75.primitives[int(gl_InstanceIndex)].effects;
float2 corner = in.v_position; float2 corner = in.v_position;
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner); float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5; float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
float2 local = (world_pos - center) * _12.dpi_scale;
uint flags = (p.flags >> 8u) & 255u;
if ((flags & 16u) != 0u)
{
float2 sc = float2(as_type<half2>(p.rotation_sc));
local = float2((sc.y * local.x) + (sc.x * local.y), ((-sc.x) * local.x) + (sc.y * local.y));
}
out.f_color = unpack_unorm4x8_to_float(p.color); out.f_color = unpack_unorm4x8_to_float(p.color);
out.f_local_or_uv = (world_pos - center) * _12.dpi_scale; out.f_local_or_uv = local;
out.f_params = p.params; out.f_params = p.params;
out.f_params2 = p.params2; out.f_params2 = p.params2;
out.f_flags = p.flags; out.f_flags = p.flags;
out.f_rotation_sc = p.rotation_sc; out.f_uv_rect = p.uv_rect;
out.f_uv_or_effects = p.uv_or_effects; out.f_effects = p.effects;
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0); out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
} }
return out; return out;
Binary file not shown.
+156
View File
@@ -0,0 +1,156 @@
#version 450 core
// Unified backdrop blur fragment shader.
// Handles both H-blur (mode 0, blurs the ¼-resolution downsample texture into
// the ¼-resolution h_blur texture) and V-blur+composite (mode 1, blurs h_blur
// vertically, masks via RRect SDF, applies tint, composites outline, and writes
// to the main render target with premultiplied alpha).
//
// Following RAD's pattern, V-mode replaces a separate composite pass: the SDF
// discard limits V-blur work to the masked region, and the per-primitive tint
// is folded in. Output blends with the main render target via the standard
// premultiplied-over blend state (ONE, ONE_MINUS_SRC_ALPHA).
//
// Backdrop primitives are tint-only — there is no outline. A specialized edge
// effect (e.g. liquid-glass-style refraction outlines) would be implemented
// as a dedicated primitive type with its own pipeline.
//
// Two modes, structurally distinct:
//
// Mode 0: 1D separable blur. Used for BOTH the H-pass and V-pass; `direction` (set in the
// per-pass uniforms) picks (1,0) for H or (0,1) for V. Reads the previous working-
// res texture and writes the next working-res texture. Fullscreen-triangle vertex
// output; gl_FragCoord.xy is in working-res target pixel space; UV =
// gl_FragCoord.xy * inv_working_size.
//
// Mode 1: composite. Reads the fully-blurred working-res texture, applies the SDF mask and
// tint, writes to source_texture. Instanced unit-quad vertex output covering the
// per-primitive bounds; gl_FragCoord.xy is in the full-resolution render target;
// UV into the blurred working texture =
// (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size.
// No kernel is applied here — the blur is already complete.
//
// Splitting V-blur out of the composite pass (an earlier version combined them) was needed
// to avoid a horizontal-vs-vertical asymmetry artifact: when the V-blur sampled the H-blur
// output through the bilinear-upsample/SDF-mask/tint pipeline in one shader invocation,
// horizontal source features ended up looking sharper than vertical ones. Running V-blur as
// its own working→working pass (matching H's structure exactly) restores symmetry.
const uint MAX_KERNEL_PAIRS = 32;
// --- Inputs from vertex shader ---
layout(location = 0) in vec2 p_local;
layout(location = 1) in mediump vec4 f_color;
layout(location = 2) flat in vec2 f_half_size;
layout(location = 3) flat in vec4 f_radii;
layout(location = 4) flat in float f_half_feather;
// --- Output ---
layout(location = 0) out vec4 out_color;
// --- Sampler ---
// Mode 0: bound to downsample_texture. Mode 1: bound to h_blur_texture.
layout(set = 2, binding = 0) uniform sampler2D blur_input_tex;
// --- Uniforms (set 3) ---
// Per-bracket-substage. `mode` matches the vertex shader's mode (0 = H, 1 = V).
// `direction` selects the kernel axis for blur offsets.
// `kernel` holds the per-sigma weight/offset pairs computed CPU-side using the
// linear-sampling pair adjustment (RAD/Rákos).
layout(set = 3, binding = 0) uniform Uniforms {
vec2 inv_working_size; // 1.0 / working-resolution texture dimensions
uint pair_count; // number of (weight, offset) pairs; pair[0] is the center
uint mode; // 0 = H-blur, 1 = V-composite
vec2 direction; // (1,0) for H, (0,1) for V — multiplied into the kernel offset
float inv_downsample_factor; // 1.0 / downsample_factor (mode 1 only; mode 0 ignores)
float _pad0;
vec4 kernel[MAX_KERNEL_PAIRS]; // .x = weight (paired-sum for idx>0), .y = offset (texels)
};
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF helper --------------------
// ---------------------------------------------------------------------------------------------------------------------
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
vec2 rxy = (p.x > 0.0) ? r.xy : r.zw;
float rr = (p.y > 0.0) ? rxy.x : rxy.y;
vec2 q = abs(p) - b;
if (rr == 0.0) {
return max(q.x, q.y);
}
q += rr;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
}
float sdf_alpha(float d, float h) {
return 1.0 - smoothstep(-h, h, d);
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Blur sample loop --------------
// ---------------------------------------------------------------------------------------------------------------------
vec3 blur_sample(vec2 uv) {
vec3 color = kernel[0].x * texture(blur_input_tex, uv).rgb;
// Per-pair offset in texel space, projected onto the active axis.
vec2 axis_step = direction * inv_working_size;
for (uint i = 1u; i < pair_count; i += 1u) {
float w = kernel[i].x;
float off = kernel[i].y;
vec2 step_uv = off * axis_step;
color += w * texture(blur_input_tex, uv - step_uv).rgb;
color += w * texture(blur_input_tex, uv + step_uv).rgb;
}
return color;
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Main --------------------------
// ---------------------------------------------------------------------------------------------------------------------
void main() {
if (mode == 0u) {
// ---- Mode 0: 1D separable blur (used for both H-pass and V-pass).
// gl_FragCoord is in working-res target pixel space; sample the previous working-res
// texture along `direction` with the kernel.
vec2 uv = gl_FragCoord.xy * inv_working_size;
vec3 color = blur_sample(uv);
out_color = vec4(color, 1.0);
return;
}
// ---- Mode 1: composite per-primitive.
// RRect SDF — early discard for fragments well outside the masked region.
float d = sdRoundedBox(p_local, f_half_size, f_radii);
if (d > f_half_feather) {
discard;
}
// fwidth-based normalization for AA (matches main pipeline approach).
float grad_magnitude = max(fwidth(d), 1e-6);
float d_n = d / grad_magnitude;
float h_n = f_half_feather / grad_magnitude;
// Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to
// working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
// already produced the final blurred image; this is just an upsample + tint.
vec2 uv = (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size;
vec3 color = texture(blur_input_tex, uv).rgb;
// Tint composition (Option B semantics): inside the masked region the panel is fully
// opaque — it completely hides the original framebuffer content, just like real frosted
// glass and like iOS UIBlurEffect / CSS backdrop-filter. f_color.rgb specifies the tint
// color; f_color.a specifies the tint *mix strength* (NOT panel opacity). At alpha=0 we
// see the pure blur; at alpha=255 we see the blur fully multiplied by the tint color.
//
// Output is premultiplied to match the ONE, ONE_MINUS_SRC_ALPHA blend state. Coverage
// (the SDF mask's edge AA) modulates only the alpha channel, never the panel-vs-source
// blend; that way edge pixels still feather correctly without re-introducing the bug
// where mid-panel pixels became semi-transparent.
mediump vec3 tinted = mix(color, color * f_color.rgb, f_color.a);
mediump float coverage = sdf_alpha(d_n, h_n);
out_color = vec4(tinted * coverage, coverage);
}
+109
View File
@@ -0,0 +1,109 @@
#version 450 core
// Unified backdrop blur vertex shader.
// Handles both H-blur (fullscreen triangle, mode 0) and V-blur+composite (instanced
// unit-quad over Backdrop_Primitive storage buffer, mode 1) for the second PSO of
// the backdrop bracket. The first PSO (downsample) uses backdrop_fullscreen.vert.
//
// No vertex buffer for either mode. Mode 0 uses gl_VertexIndex 0..2 for a single
// fullscreen triangle; mode 1 uses gl_VertexIndex 0..5 for a unit-quad (two
// triangles, TRIANGLELIST topology) and gl_InstanceIndex to select the primitive.
//
// Mode 0 viewport+scissor are CPU-set per layer-bracket to the work region (union
// AABB of backdrop primitives + 3*max_sigma, clamped to swapchain bounds). Mode 1
// renders into the main render target with the screen-space orthographic projection;
// the per-primitive bounds drive the quad in screen space.
//
// Backdrop primitives have NO rotation — backdrop sampling is in screen space, so
// a rotated mask over a stationary blur sample would look wrong.
// --- Outputs to fragment shader ---
// p_local: shape-local position in physical pixels (origin at shape center).
// Only meaningful in mode 1 (V-composite). Zero-init for mode 0.
layout(location = 0) out vec2 p_local;
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
layout(location = 1) out mediump vec4 f_color;
// f_half_size: RRect half extents in physical pixels (mode 1 only).
layout(location = 2) flat out vec2 f_half_size;
// f_radii: per-corner radii in physical pixels (mode 1 only).
layout(location = 3) flat out vec4 f_radii;
// f_half_feather: SDF anti-aliasing feather (mode 1 only).
layout(location = 4) flat out float f_half_feather;
// --- Uniforms (set 1) ---
// Backdrop pipeline's own uniform block — distinct from the main pipeline's
// Vertex_Uniforms. `mode` selects between H-blur (0) and V-composite (1).
layout(set = 1, binding = 0) uniform Uniforms {
mat4 projection;
float dpi_scale;
uint mode; // 0 = H-blur, 1 = V-composite
vec2 _pad0;
};
// --- Backdrop primitive storage buffer (set 0) ---
// 48 bytes, std430-natural layout (no implicit padding). vec4 members are
// front-loaded so their 16-byte alignment is satisfied without holes; the
// vec2 and scalar tail packs tight to land the struct at a clean 48-byte
// stride (a multiple of 16, so the array stride needs no rounding either).
// Field semantics match the CPU-side Backdrop_Primitive declared in
// levlib/draw/pipeline_2d_backdrop.odin; keep both in sync.
//
// Backdrop primitives are tint-only in v1: outline is intentionally absent.
// Future specialized effects (e.g. liquid-glass-style edges) would be a
// dedicated primitive type with its own pipeline rather than a flag bit here.
struct Backdrop_Primitive {
vec4 bounds; // 0-15: min_xy, max_xy (world-space)
vec4 radii; // 16-31: per-corner radii (physical px)
vec2 half_size; // 32-39: RRect half extents (physical px)
float half_feather; // 40-43: SDF anti-aliasing feather (physical px)
uint color; // 44-47: tint, packed RGBA u8x4
};
layout(std430, set = 0, binding = 0) readonly buffer Backdrop_Primitives {
Backdrop_Primitive primitives[];
};
void main() {
if (mode == 0u) {
// ---- Mode 0: H-blur fullscreen triangle ----
// gl_VertexIndex 0 -> ( -1, -1)
// gl_VertexIndex 1 -> ( 3, -1)
// gl_VertexIndex 2 -> ( -1, 3)
vec2 ndc = vec2(
(gl_VertexIndex == 1) ? 3.0 : -1.0,
(gl_VertexIndex == 2) ? 3.0 : -1.0);
gl_Position = vec4(ndc, 0.0, 1.0);
// Mode 0 doesn't read the per-primitive varyings; zero-init for safety.
p_local = vec2(0.0);
f_color = vec4(0.0);
f_half_size = vec2(0.0);
f_radii = vec4(0.0);
f_half_feather = 0.0;
} else {
// ---- Mode 1: V-composite instanced unit-quad over Backdrop_Primitive ----
Backdrop_Primitive p = primitives[gl_InstanceIndex];
// Unit-quad corners for TRIANGLELIST (2 triangles, 6 vertices):
// index 0 -> (0,0) index 3 -> (0,1)
// index 1 -> (1,0) index 4 -> (1,0)
// index 2 -> (0,1) index 5 -> (1,1)
vec2 quad_corners[6] = vec2[6](
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
vec2 corner = quad_corners[gl_VertexIndex];
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw);
// Shape-local position in physical pixels (no rotation for backdrops).
p_local = (world_pos - center) * dpi_scale;
f_color = unpackUnorm4x8(p.color);
f_half_size = p.half_size;
f_radii = p.radii;
f_half_feather = p.half_feather;
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
}
}
@@ -0,0 +1,70 @@
#version 450 core
// Backdrop downsample fragment shader.
// Reads source_texture (full-resolution snapshot of pre-bracket framebuffer contents) and
// writes a downsampled copy at factor 1, 2, 4, 8, or 16. The output is the working texture
// (sized at full swapchain resolution); larger factors only fill a sub-rect of it via the
// CPU-set viewport. See backdrop.odin for the factor selection table (Flutter-style).
//
// Shader paths by factor:
//
// factor=1: identity copy. One bilinear tap aligned to the source pixel center. Useful
// when sigma is small enough that any downsample round-trip would visibly soften
// the output (Flutter does this for sigma_phys ≤ 4).
//
// factor=2: each output covers a 2×2 source block. Single bilinear tap at the shared
// corner reads all 4 source pixels with 0.25 weight.
//
// factor>=4: each output covers a (factor)×(factor) source block. We use 4 bilinear taps,
// each at the shared corner of a (factor/2)×(factor/2) sub-block. Each tap reads
// 4 source pixels uniformly; combined, the 4 taps sample 16 source pixels arranged
// uniformly across the block. This is an approximation of a true (factor)² box
// filter — exact at factor=4 (16 pixels = full coverage), undersampled at factor=8
// (16 pixels of 64) and factor=16 (16 of 256). Flutter uses a richer 13-tap COD-
// style downsample shader at high factors; we accept the simpler 4-tap pattern
// for now since the high-factor cases come with large kernels that mask any
// residual aliasing.
//
// The viewport+scissor are set by the CPU to limit output to the layer's work region in
// working-texture coords (work_region_phys / factor), clamped to the texture bounds.
layout(set = 3, binding = 0) uniform Uniforms {
vec2 inv_source_size; // 1.0 / source_texture pixel dimensions
uint downsample_factor; // 1, 2, 4, 8, or 16
uint _pad0;
};
layout(set = 2, binding = 0) uniform sampler2D source_tex;
layout(location = 0) out vec4 out_color;
void main() {
// Output pixel index (i): gl_FragCoord.xy - 0.5. Source-pixel block top-left for this
// output: i * factor. Center of the block: i*factor + factor/2 = gl_FragCoord.xy * factor.
vec2 src_block_center = gl_FragCoord.xy * float(downsample_factor);
if (downsample_factor == 1u) {
// Identity copy. UV at src_block_center hits the source pixel center directly.
vec2 uv = src_block_center * inv_source_size;
out_color = texture(source_tex, uv);
} else if (downsample_factor == 2u) {
// Single tap at the shared corner of the 2×2 source block; one bilinear sample reads
// all 4 source pixels with equal 0.25 weights — uniform 2×2 box filter for free.
vec2 uv = src_block_center * inv_source_size;
out_color = texture(source_tex, uv);
} else {
// Four taps at offsets ±(factor/4) from the block center. Each tap lands on a corner
// shared by 4 source pixels of a (factor/2)×(factor/2) sub-block (equivalent at the
// bilinear level), giving a 4-tap = 16-source-pixel uniform sample of the block.
float off = float(downsample_factor) * 0.25;
vec2 uv_tl = (src_block_center + vec2(-off, -off)) * inv_source_size;
vec2 uv_tr = (src_block_center + vec2( off, -off)) * inv_source_size;
vec2 uv_bl = (src_block_center + vec2(-off, off)) * inv_source_size;
vec2 uv_br = (src_block_center + vec2( off, off)) * inv_source_size;
vec4 c = texture(source_tex, uv_tl)
+ texture(source_tex, uv_tr)
+ texture(source_tex, uv_bl)
+ texture(source_tex, uv_br);
out_color = c * 0.25;
}
}
@@ -0,0 +1,21 @@
#version 450 core
// Fullscreen-triangle vertex shader for the backdrop downsample and H-blur sub-passes.
// Emits a single triangle covering NDC [-1,1]^2; the rasterizer clips edges outside.
// No vertex buffer; uses gl_VertexIndex to pick corners.
//
// The CPU sets the viewport (and matching scissor) per layer-bracket to limit work to
// the union AABB of the layer's backdrop primitives, expanded by 3*max_sigma and
// clamped to swapchain bounds. The fragment shader uses gl_FragCoord (absolute pixel
// space in the bound target) plus an inv-size uniform to compute its own UVs — see
// each fragment shader for the per-pass sampling math.
void main() {
// gl_VertexIndex 0 -> ( -1, -1)
// gl_VertexIndex 1 -> ( 3, -1)
// gl_VertexIndex 2 -> ( -1, 3)
vec2 ndc = vec2(
(gl_VertexIndex == 1) ? 3.0 : -1.0,
(gl_VertexIndex == 2) ? 3.0 : -1.0);
gl_Position = vec4(ndc, 0.0, 1.0);
}
+10 -19
View File
@@ -6,8 +6,8 @@ layout(location = 1) in vec2 f_local_or_uv;
layout(location = 2) in vec4 f_params; layout(location = 2) in vec4 f_params;
layout(location = 3) in vec4 f_params2; layout(location = 3) in vec4 f_params2;
layout(location = 4) flat in uint f_flags; layout(location = 4) flat in uint f_flags;
layout(location = 5) flat in uint f_rotation_sc; layout(location = 6) flat in vec4 f_uv_rect;
layout(location = 6) flat in uvec4 f_uv_or_effects; layout(location = 7) flat in uvec4 f_effects;
// --- Output --- // --- Output ---
layout(location = 0) out vec4 out_color; layout(location = 0) out vec4 out_color;
@@ -83,16 +83,7 @@ void main() {
float h = 0.5; // half-feather width; overwritten per shape kind float h = 0.5; // half-feather width; overwritten per shape kind
vec2 half_size = f_params.xy; // used by RRect and as reference size for gradients vec2 half_size = f_params.xy; // used by RRect and as reference size for gradients
vec2 p_local = f_local_or_uv; vec2 p_local = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
// Apply inverse rotation using pre-computed sin/cos (no per-pixel trig).
// .Rotated flag = bit 4 = 16u
if ((flags & 16u) != 0u) {
vec2 sc = unpackHalf2x16(f_rotation_sc); // .x = sin(angle), .y = cos(angle)
// Inverse rotation matrix R(-angle) = [[cos, sin], [-sin, cos]]
p_local = vec2(sc.y * p_local.x + sc.x * p_local.y,
-sc.x * p_local.x + sc.y * p_local.y);
}
if (kind == 1u) { if (kind == 1u) {
// RRect — half_feather in params2.z // RRect — half_feather in params2.z
@@ -151,7 +142,7 @@ void main() {
if ((flags & 2u) != 0u) { if ((flags & 2u) != 0u) {
// Gradient active (bit 1) // Gradient active (bit 1)
mediump vec4 gradient_start = f_color; mediump vec4 gradient_start = f_color;
mediump vec4 gradient_end = unpackUnorm4x8(f_uv_or_effects.x); mediump vec4 gradient_end = unpackUnorm4x8(f_effects.x);
if ((flags & 4u) != 0u) { if ((flags & 4u) != 0u) {
// Radial gradient (bit 2): t from distance to center // Radial gradient (bit 2): t from distance to center
@@ -159,13 +150,13 @@ void main() {
shape_color = gradient_2color(gradient_start, gradient_end, t); shape_color = gradient_2color(gradient_start, gradient_end, t);
} else { } else {
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair // Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
vec2 direction = unpackHalf2x16(f_uv_or_effects.z); vec2 direction = unpackHalf2x16(f_effects.z);
mediump float t = dot(p_local / half_size, direction) * 0.5 + 0.5; mediump float t = dot(p_local / half_size, direction) * 0.5 + 0.5;
shape_color = gradient_2color(gradient_start, gradient_end, t); shape_color = gradient_2color(gradient_start, gradient_end, t);
} }
} else if ((flags & 1u) != 0u) { } else if ((flags & 1u) != 0u) {
// Textured (bit 0) — RRect only in practice // Textured (bit 0)
vec4 uv_rect = uintBitsToFloat(f_uv_or_effects); vec4 uv_rect = f_uv_rect;
vec2 local_uv = p_local / half_size * 0.5 + 0.5; vec2 local_uv = p_local / half_size * 0.5 + 0.5;
vec2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv); vec2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
shape_color = f_color * texture(tex, uv); shape_color = f_color * texture(tex, uv);
@@ -180,9 +171,9 @@ void main() {
// AA at d=ol_width. The outline band's coverage is total_cov - fill_cov. // AA at d=ol_width. The outline band's coverage is total_cov - fill_cov.
// Output is premultiplied: blend state is ONE, ONE_MINUS_SRC_ALPHA. // Output is premultiplied: blend state is ONE, ONE_MINUS_SRC_ALPHA.
if ((flags & 8u) != 0u) { if ((flags & 8u) != 0u) {
mediump vec4 ol_color = unpackUnorm4x8(f_uv_or_effects.y); mediump vec4 ol_color = unpackUnorm4x8(f_effects.y);
// Outline width in f_uv_or_effects.w (low f16 half) // Outline width in f_effects.w (low f16 half)
float ol_width = unpackHalf2x16(f_uv_or_effects.w).x / grad_magnitude; float ol_width = unpackHalf2x16(f_effects.w).x / grad_magnitude;
float fill_cov = sdf_alpha(d, h); float fill_cov = sdf_alpha(d, h);
float total_cov = sdf_alpha(d - ol_width, h); float total_cov = sdf_alpha(d - ol_width, h);
+31 -12
View File
@@ -11,8 +11,9 @@ layout(location = 1) out vec2 f_local_or_uv;
layout(location = 2) out vec4 f_params; layout(location = 2) out vec4 f_params;
layout(location = 3) out vec4 f_params2; layout(location = 3) out vec4 f_params2;
layout(location = 4) flat out uint f_flags; layout(location = 4) flat out uint f_flags;
layout(location = 5) flat out uint f_rotation_sc;
layout(location = 6) flat out uvec4 f_uv_or_effects; layout(location = 6) flat out vec4 f_uv_rect;
layout(location = 7) flat out uvec4 f_effects;
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ---------- // ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
layout(set = 1, binding = 0) uniform Uniforms { layout(set = 1, binding = 0) uniform Uniforms {
@@ -22,7 +23,10 @@ layout(set = 1, binding = 0) uniform Uniforms {
}; };
// ---------- SDF primitive storage buffer ---------- // ---------- SDF primitive storage buffer ----------
struct Primitive { // Mirrors the CPU-side Base_2D_Primitive in pipeline_2d_base.odin. Named with the
// pipeline prefix so a project-wide grep on the type name matches both the GLSL
// declaration and the Odin declaration.
struct Base_2D_Primitive {
vec4 bounds; // 0-15 vec4 bounds; // 0-15
uint color; // 16-19 uint color; // 16-19
uint flags; // 20-23 uint flags; // 20-23
@@ -30,11 +34,12 @@ struct Primitive {
float _pad; // 28-31 float _pad; // 28-31
vec4 params; // 32-47 vec4 params; // 32-47
vec4 params2; // 48-63 vec4 params2; // 48-63
uvec4 uv_or_effects; // 64-79 vec4 uv_rect; // 64-79: texture UV coordinates (read when .Textured)
uvec4 effects; // 80-95: gradient/outline parameters (read when .Gradient/.Outline)
}; };
layout(std430, set = 0, binding = 0) readonly buffer Primitives { layout(std430, set = 0, binding = 0) readonly buffer Base_2D_Primitives {
Primitive primitives[]; Base_2D_Primitive primitives[];
}; };
// ---------- Entry point ---------- // ---------- Entry point ----------
@@ -46,25 +51,39 @@ void main() {
f_params = vec4(0.0); f_params = vec4(0.0);
f_params2 = vec4(0.0); f_params2 = vec4(0.0);
f_flags = 0u; f_flags = 0u;
f_rotation_sc = 0u; f_uv_rect = vec4(0.0);
f_uv_or_effects = uvec4(0); f_effects = uvec4(0);
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0); gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
} else { } else {
// ---- Mode 1: SDF instanced quads ---- // ---- Mode 1: SDF instanced quads ----
Primitive p = primitives[gl_InstanceIndex]; Base_2D_Primitive p = primitives[gl_InstanceIndex];
vec2 corner = v_position; // unit quad corners: (0,0)-(1,1) vec2 corner = v_position; // unit quad corners: (0,0)-(1,1)
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner); vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw); vec2 center = 0.5 * (p.bounds.xy + p.bounds.zw);
// Compute shape-local position. Apply inverse rotation here in the vertex
// shader; the rasterizer interpolates the rotated values across the quad,
// which is mathematically equivalent to per-fragment rotation under 2D ortho
// projection. Frees one fragment-shader varying and per-pixel rotation math.
vec2 local = (world_pos - center) * dpi_scale;
uint flags = (p.flags >> 8u) & 0xFFu;
if ((flags & 16u) != 0u) {
// Rotated flag (bit 4); rotation_sc holds packed f16 (sin, cos).
// Inverse rotation matrix R(-angle) = [[cos, sin], [-sin, cos]].
vec2 sc = unpackHalf2x16(p.rotation_sc);
local = vec2(sc.y * local.x + sc.x * local.y,
-sc.x * local.x + sc.y * local.y);
}
f_color = unpackUnorm4x8(p.color); f_color = unpackUnorm4x8(p.color);
f_local_or_uv = (world_pos - center) * dpi_scale; // shape-centered physical pixels f_local_or_uv = local; // shape-local physical pixels (rotated if .Rotated set)
f_params = p.params; f_params = p.params;
f_params2 = p.params2; f_params2 = p.params2;
f_flags = p.flags; f_flags = p.flags;
f_rotation_sc = p.rotation_sc; f_uv_rect = p.uv_rect;
f_uv_or_effects = p.uv_or_effects; f_effects = p.effects;
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0); gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
} }
+84 -96
View File
@@ -52,12 +52,14 @@ emit_rectangle :: proc(x, y, width, height: f32, color: Color, vertices: []Verte
vertices[offset + 5] = solid_vertex({x, y + height}, color) vertices[offset + 5] = solid_vertex({x, y + height}, color)
} }
// Internal // Internal — submit an SDF primitive with optional texture binding.
prepare_sdf_primitive_textured :: proc( // Replaces the old prepare_sdf_primitive and prepare_sdf_primitive_textured.
@(private)
prepare_sdf_primitive_ex :: proc(
layer: ^Layer, layer: ^Layer,
prim: Primitive, prim: Base_2D_Primitive,
texture_id: Texture_Id, texture_id: Texture_Id = INVALID_TEXTURE,
sampler: Sampler_Preset, sampler: Sampler_Preset = DFT_SAMPLER,
) { ) {
offset := u32(len(GLOB.tmp_primitives)) offset := u32(len(GLOB.tmp_primitives))
append(&GLOB.tmp_primitives, prim) append(&GLOB.tmp_primitives, prim)
@@ -65,6 +67,23 @@ prepare_sdf_primitive_textured :: proc(
append_or_extend_sub_batch(scissor, layer, .SDF, offset, 1, texture_id, sampler) append_or_extend_sub_batch(scissor, layer, .SDF, offset, 1, texture_id, sampler)
} }
// Internal — resolve Texture_Fill zero-initialized fields to their defaults.
// Odin structs zero-initialize; Color{} and Rectangle{} are all-zero which is not a
// useful tint or UV rect. This proc substitutes sensible defaults for zero values.
@(private)
resolve_texture_defaults :: #force_inline proc(
tf: Texture_Fill,
) -> (
tint: Color,
uv: Rectangle,
sampler: Sampler_Preset,
) {
tint = tf.tint == Color{} ? DFT_TINT : tf.tint
uv = tf.uv_rect == Rectangle{} ? DFT_UV_RECT : tf.uv_rect
sampler = tf.sampler
return
}
//Internal //Internal
// //
// Compute the visual center of a center-parametrized shape after applying // Compute the visual center of a center-parametrized shape after applying
@@ -89,7 +108,7 @@ rotated_aabb_half_extents :: proc(half_width, half_height, cos_angle, sin_angle:
return {half_width * cos_abs + half_height * sin_abs, half_width * sin_abs + half_height * cos_abs} return {half_width * cos_abs + half_height * sin_abs, half_width * sin_abs + half_height * cos_abs}
} }
// Pack sin/cos into the Primitive.rotation_sc field as two f16 values. // Pack sin/cos into the Base_2D_Primitive.rotation_sc field as two f16 values.
pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 { pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 {
return pack_f16_pair(f16(sin_angle), f16(cos_angle)) return pack_f16_pair(f16(sin_angle), f16(cos_angle))
} }
@@ -97,7 +116,7 @@ pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 {
// Internal // Internal
// //
// Build an RRect Primitive with bounds, params, and rotation computed from rectangle geometry. // Build an RRect Base_2D_Primitive with bounds, params, and rotation computed from rectangle geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting. // The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_rrect_primitive :: proc( build_rrect_primitive :: proc(
rect: Rectangle, rect: Rectangle,
@@ -105,7 +124,7 @@ build_rrect_primitive :: proc(
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_px: f32, feather_px: f32,
) -> Primitive { ) -> Base_2D_Primitive {
max_radius := min(rect.width, rect.height) * 0.5 max_radius := min(rect.width, rect.height) * 0.5
clamped_top_left := clamp(radii.top_left, 0, max_radius) clamped_top_left := clamp(radii.top_left, 0, max_radius)
clamped_top_right := clamp(radii.top_right, 0, max_radius) clamped_top_right := clamp(radii.top_right, 0, max_radius)
@@ -141,7 +160,7 @@ build_rrect_primitive :: proc(
bounds_half_height = expanded.y bounds_half_height = expanded.y
} }
prim := Primitive { prim := Base_2D_Primitive {
bounds = { bounds = {
center_x - bounds_half_width - padding, center_x - bounds_half_width - padding,
center_y - bounds_half_height - padding, center_y - bounds_half_height - padding,
@@ -165,7 +184,7 @@ build_rrect_primitive :: proc(
// Internal // Internal
// //
// Build an RRect Primitive for a circle (fully-rounded square RRect). // Build an RRect Base_2D_Primitive for a circle (fully-rounded square RRect).
// The caller sets color, flags, and uv fields on the returned primitive before submitting. // The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_circle_primitive :: proc( build_circle_primitive :: proc(
center: Vec2, center: Vec2,
@@ -173,7 +192,7 @@ build_circle_primitive :: proc(
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_px: f32, feather_px: f32,
) -> Primitive { ) -> Base_2D_Primitive {
half_feather := feather_px * 0.5 half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
@@ -184,7 +203,7 @@ build_circle_primitive :: proc(
actual_center = compute_pivot_center(center, origin, sin_a, cos_a) actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
} }
prim := Primitive { prim := Base_2D_Primitive {
bounds = { bounds = {
actual_center.x - radius - padding, actual_center.x - radius - padding,
actual_center.y - radius - padding, actual_center.y - radius - padding,
@@ -203,7 +222,7 @@ build_circle_primitive :: proc(
// Internal // Internal
// //
// Build an Ellipse Primitive with bounds, params, and rotation computed from ellipse geometry. // Build an Ellipse Base_2D_Primitive with bounds, params, and rotation computed from ellipse geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting. // The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_ellipse_primitive :: proc( build_ellipse_primitive :: proc(
center: Vec2, center: Vec2,
@@ -211,7 +230,7 @@ build_ellipse_primitive :: proc(
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_px: f32, feather_px: f32,
) -> Primitive { ) -> Base_2D_Primitive {
half_feather := feather_px * 0.5 half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
@@ -235,7 +254,7 @@ build_ellipse_primitive :: proc(
bound_vertical = expanded.y bound_vertical = expanded.y
} }
prim := Primitive { prim := Base_2D_Primitive {
bounds = { bounds = {
actual_center.x - bound_horizontal - padding, actual_center.x - bound_horizontal - padding,
actual_center.y - bound_vertical - padding, actual_center.y - bound_vertical - padding,
@@ -253,7 +272,7 @@ build_ellipse_primitive :: proc(
// Internal // Internal
// //
// Build an NGon Primitive with bounds, params, and rotation computed from polygon geometry. // Build an NGon Base_2D_Primitive with bounds, params, and rotation computed from polygon geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting. // The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_polygon_primitive :: proc( build_polygon_primitive :: proc(
center: Vec2, center: Vec2,
@@ -262,7 +281,7 @@ build_polygon_primitive :: proc(
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_px: f32, feather_px: f32,
) -> Primitive { ) -> Base_2D_Primitive {
half_feather := feather_px * 0.5 half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
@@ -276,7 +295,7 @@ build_polygon_primitive :: proc(
rotation_radians := math.to_radians(rotation) rotation_radians := math.to_radians(rotation)
sin_rot, cos_rot := math.sincos(rotation_radians) sin_rot, cos_rot := math.sincos(rotation_radians)
prim := Primitive { prim := Base_2D_Primitive {
bounds = { bounds = {
actual_center.x - radius - padding, actual_center.x - radius - padding,
actual_center.y - radius - padding, actual_center.y - radius - padding,
@@ -295,7 +314,7 @@ build_polygon_primitive :: proc(
// Internal // Internal
// //
// Build a Ring_Arc Primitive with bounds and params computed from ring/arc geometry. // Build a Ring_Arc Base_2D_Primitive with bounds and params computed from ring/arc geometry.
// Pre-computes the angular boundary normals on the CPU so the fragment shader needs // Pre-computes the angular boundary normals on the CPU so the fragment shader needs
// no per-pixel sin/cos. The radial SDF uses max(inner-r, r-outer) which correctly // no per-pixel sin/cos. The radial SDF uses max(inner-r, r-outer) which correctly
// handles pie slices (inner_radius = 0) and full rings. // handles pie slices (inner_radius = 0) and full rings.
@@ -309,7 +328,7 @@ build_ring_arc_primitive :: proc(
rotation: f32, rotation: f32,
feather_px: f32, feather_px: f32,
) -> ( ) -> (
Primitive, Base_2D_Primitive,
Shape_Flags, Shape_Flags,
) { ) {
half_feather := feather_px * 0.5 half_feather := feather_px * 0.5
@@ -347,7 +366,7 @@ build_ring_arc_primitive :: proc(
arc_flags = arc_span <= math.PI ? {.Arc_Narrow} : {.Arc_Wide} arc_flags = arc_span <= math.PI ? {.Arc_Narrow} : {.Arc_Wide}
} }
prim := Primitive { prim := Base_2D_Primitive {
bounds = { bounds = {
actual_center.x - outer_radius - padding, actual_center.x - outer_radius - padding,
actual_center.y - outer_radius - padding, actual_center.y - outer_radius - padding,
@@ -365,39 +384,53 @@ build_ring_arc_primitive :: proc(
return prim, arc_flags return prim, arc_flags
} }
// Apply gradient and outline effects to a primitive. Sets flags, uv.effects, and expands bounds. // Apply brush fill and outline to a primitive, then submit it.
// Dispatches to the correct sub-batch based on the Brush variant.
// All parameters (outline_width) are in logical pixels, matching the rest of the public API. // All parameters (outline_width) are in logical pixels, matching the rest of the public API.
// The helper converts to physical pixels for GPU packing internally. // The helper converts to physical pixels for GPU packing internally.
@(private) @(private)
apply_shape_effects :: proc( apply_brush_and_outline :: proc(
prim: ^Primitive, layer: ^Layer,
prim: ^Base_2D_Primitive,
kind: Shape_Kind, kind: Shape_Kind,
gradient: Gradient, brush: Brush,
outline_color: Color, outline_color: Color,
outline_width: f32, outline_width: f32,
extra_flags: Shape_Flags = {}, extra_flags: Shape_Flags = {},
) { ) {
flags: Shape_Flags = extra_flags flags: Shape_Flags = extra_flags
gradient_dir_sc: u32 = 0
switch g in gradient { // Fill — determined by the Brush variant.
texture_id := INVALID_TEXTURE
sampler := DFT_SAMPLER
switch b in brush {
case Color: prim.color = b
case Linear_Gradient: case Linear_Gradient:
flags += {.Gradient} flags += {.Gradient}
prim.uv.effects.gradient_color = g.end_color prim.color = b.start_color
rad := math.to_radians(g.angle) prim.effects.gradient_color = b.end_color
rad := math.to_radians(b.angle)
sin_a, cos_a := math.sincos(rad) sin_a, cos_a := math.sincos(rad)
gradient_dir_sc = pack_f16_pair(f16(cos_a), f16(sin_a)) prim.effects.gradient_dir_sc = pack_f16_pair(f16(cos_a), f16(sin_a))
case Radial_Gradient: case Radial_Gradient:
flags += {.Gradient, .Gradient_Radial} flags += {.Gradient, .Gradient_Radial}
prim.uv.effects.gradient_color = g.outer_color prim.color = b.inner_color
case: prim.effects.gradient_color = b.outer_color
case Texture_Fill:
flags += {.Textured}
tint, uv, sam := resolve_texture_defaults(b)
prim.color = tint
prim.uv_rect = {uv.x, uv.y, uv.width, uv.height}
texture_id = b.id
sampler = sam
} }
outline_packed: u32 = 0 // Outline — orthogonal to all Brush variants.
if outline_width > 0 { if outline_width > 0 {
flags += {.Outline} flags += {.Outline}
prim.uv.effects.outline_color = outline_color prim.effects.outline_color = outline_color
outline_packed = pack_f16_pair(f16(outline_width * GLOB.dpi_scaling), 0) prim.effects.outline_packed = pack_f16_pair(f16(outline_width * GLOB.dpi_scaling), 0)
// Expand bounds to contain the outline (bounds are in logical pixels) // Expand bounds to contain the outline (bounds are in logical pixels)
prim.bounds[0] -= outline_width prim.bounds[0] -= outline_width
prim.bounds[1] -= outline_width prim.bounds[1] -= outline_width
@@ -410,9 +443,8 @@ apply_shape_effects :: proc(
flags += {.Rotated} flags += {.Rotated}
} }
prim.uv.effects.gradient_dir_sc = gradient_dir_sc
prim.uv.effects.outline_packed = outline_packed
prim.flags = pack_kind_flags(kind, flags) prim.flags = pack_kind_flags(kind, flags)
prepare_sdf_primitive_ex(layer, prim^, texture_id, sampler)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -430,8 +462,7 @@ apply_shape_effects :: proc(
rectangle :: proc( rectangle :: proc(
layer: ^Layer, layer: ^Layer,
rect: Rectangle, rect: Rectangle,
color: Color, brush: Brush,
gradient: Gradient = nil,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
radii: Rectangle_Radii = {}, radii: Rectangle_Radii = {},
@@ -440,36 +471,7 @@ rectangle :: proc(
feather_px: f32 = DFT_FEATHER_PX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px) prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
prim.color = color apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
apply_shape_effects(&prim, .RRect, gradient, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
}
// Draw a rectangle with a texture fill via SDF with optional per-corner rounding radii.
// Texture and gradient/outline are mutually exclusive (they share the same storage in the
// primitive). To outline a textured rect, draw the texture first, then a stroke-only rect on top.
// Origin semantics: see `rectangle`.
rectangle_texture :: proc(
layer: ^Layer,
rect: Rectangle,
id: Texture_Id,
tint: Color = DFT_TINT,
uv_rect: Rectangle = DFT_UV_RECT,
sampler: Sampler_Preset = DFT_SAMPLER,
radii: Rectangle_Radii = {},
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
prim.color = tint
tex_flags: Shape_Flags = {.Textured}
if prim.rotation_sc != 0 {
tex_flags += {.Rotated}
}
prim.flags = pack_kind_flags(.RRect, tex_flags)
prim.uv.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
prepare_sdf_primitive_textured(layer, prim, id, sampler)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -488,8 +490,7 @@ circle :: proc(
layer: ^Layer, layer: ^Layer,
center: Vec2, center: Vec2,
radius: f32, radius: f32,
color: Color, brush: Brush,
gradient: Gradient = nil,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
origin: Vec2 = {}, origin: Vec2 = {},
@@ -497,9 +498,7 @@ circle :: proc(
feather_px: f32 = DFT_FEATHER_PX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
prim := build_circle_primitive(center, radius, origin, rotation, feather_px) prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
prim.color = color apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
apply_shape_effects(&prim, .RRect, gradient, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -512,8 +511,7 @@ ellipse :: proc(
layer: ^Layer, layer: ^Layer,
center: Vec2, center: Vec2,
radius_horizontal, radius_vertical: f32, radius_horizontal, radius_vertical: f32,
color: Color, brush: Brush,
gradient: Gradient = nil,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
origin: Vec2 = {}, origin: Vec2 = {},
@@ -521,9 +519,7 @@ ellipse :: proc(
feather_px: f32 = DFT_FEATHER_PX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px) prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px)
prim.color = color apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width)
apply_shape_effects(&prim, .Ellipse, gradient, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -538,8 +534,7 @@ polygon :: proc(
center: Vec2, center: Vec2,
sides: int, sides: int,
radius: f32, radius: f32,
color: Color, brush: Brush,
gradient: Gradient = nil,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
origin: Vec2 = {}, origin: Vec2 = {},
@@ -549,9 +544,7 @@ polygon :: proc(
if sides < 3 do return if sides < 3 do return
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px) prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
prim.color = color apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width)
apply_shape_effects(&prim, .NGon, gradient, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -566,8 +559,7 @@ ring :: proc(
layer: ^Layer, layer: ^Layer,
center: Vec2, center: Vec2,
inner_radius, outer_radius: f32, inner_radius, outer_radius: f32,
color: Color, brush: Brush,
gradient: Gradient = nil,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
start_angle: f32 = 0, start_angle: f32 = 0,
@@ -586,9 +578,7 @@ ring :: proc(
rotation, rotation,
feather_px, feather_px,
) )
prim.color = color apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
apply_shape_effects(&prim, .Ring_Arc, gradient, outline_color, outline_width, arc_flags)
prepare_sdf_primitive(layer, prim)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -600,7 +590,7 @@ ring :: proc(
line :: proc( line :: proc(
layer: ^Layer, layer: ^Layer,
start_position, end_position: Vec2, start_position, end_position: Vec2,
color: Color, brush: Brush,
thickness: f32 = DFT_STROKE_THICKNESS, thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
@@ -627,14 +617,13 @@ line :: proc(
// Expand bounds for rotation // Expand bounds for rotation
bounds_half := rotated_aabb_half_extents(half_length + cap_radius, half_thickness, cos_angle, sin_angle) bounds_half := rotated_aabb_half_extents(half_length + cap_radius, half_thickness, cos_angle, sin_angle)
prim := Primitive { prim := Base_2D_Primitive {
bounds = { bounds = {
center_x - bounds_half.x - padding, center_x - bounds_half.x - padding,
center_y - bounds_half.y - padding, center_y - bounds_half.y - padding,
center_x + bounds_half.x + padding, center_x + bounds_half.x + padding,
center_y + bounds_half.y + padding, center_y + bounds_half.y + padding,
}, },
color = color,
rotation_sc = pack_rotation_sc(sin_angle, cos_angle), rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
} }
prim.params.rrect = RRect_Params { prim.params.rrect = RRect_Params {
@@ -647,15 +636,14 @@ line :: proc(
}, },
half_feather = half_feather, half_feather = half_feather,
} }
apply_shape_effects(&prim, .RRect, nil, outline_color, outline_width) apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
prepare_sdf_primitive(layer, prim)
} }
// Draw a line strip via decomposed SDF line segments. // Draw a line strip via decomposed SDF line segments.
line_strip :: proc( line_strip :: proc(
layer: ^Layer, layer: ^Layer,
points: []Vec2, points: []Vec2,
color: Color, brush: Brush,
thickness: f32 = DFT_STROKE_THICKNESS, thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
@@ -663,7 +651,7 @@ line_strip :: proc(
) { ) {
if len(points) < 2 do return if len(points) < 2 do return
for i in 0 ..< len(points) - 1 { for i in 0 ..< len(points) - 1 {
line(layer, points[i], points[i + 1], color, thickness, outline_color, outline_width, feather_px) line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_px)
} }
} }
+1 -1
View File
@@ -14,8 +14,8 @@ Texture_Kind :: enum u8 {
} }
Sampler_Preset :: enum u8 { Sampler_Preset :: enum u8 {
Nearest_Clamp,
Linear_Clamp, Linear_Clamp,
Nearest_Clamp,
Nearest_Repeat, Nearest_Repeat,
Linear_Repeat, Linear_Repeat,
} }