Compare commits

..

5 Commits

Author SHA1 Message Date
Zachary Levy 87d4c9a0b5 Major reorg 2026-04-30 22:23:51 -07:00
Zachary Levy fd64bc01bf Orgnaization & cleanup 2026-04-30 17:24:20 -07:00
Zachary Levy 16989cbb71 Added backdrop effects pipeline (blur) 2026-04-30 16:46:29 -07:00
Zachary Levy ff29dbd92f Update draw README 2026-04-28 16:04:44 -07:00
Zachary Levy c59858dcd4 Added Cybersteel theme 2026-04-28 13:22:27 -07:00
39 changed files with 1042 additions and 2655 deletions
-5
View File
@@ -75,11 +75,6 @@
"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 clay-borders example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- clay-borders",
"cwd": "$ZED_WORKTREE_ROOT",
},
{ {
"label": "Run draw gaussian-blur example", "label": "Run draw gaussian-blur example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur", "command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur",
+87 -142
View File
@@ -5,27 +5,14 @@ Clay UI integration.
## Current state ## Current state
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with three submission The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with two submission
modes dispatched by a push constant. The split is by **vertex coordinate space**, not by what the modes dispatched by a push constant:
fragment shader does — modes 0 and 2 share the same fragment-shader path (kind 0) and differ only
in whether the vertex shader applies `dpi_scale` to incoming positions:
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry in _logical_ pixels. The vertex - **Mode 0 (Tessellated):** Vertex buffer contains real geometry. Used for text (indexed draws into
shader scales by `dpi_scale` before projecting. Used for single-pixel points (`tess.pixel`), SDL_ttf atlas textures), single-pixel points (`tess.pixel`), arbitrary user geometry
arbitrary user geometry (`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`, (`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`, `tess.triangle_fan`,
`tess.triangle_fan`, `tess.triangle_strip`), and any raw vertex geometry submitted via `tess.triangle_strip`), and any raw vertex geometry submitted via `prepare_shape`. The fragment
`prepare_shape`. The fragment shader premultiplies the texture sample (`t.rgb *= t.a`) and shader premultiplies the texture sample (`t.rgb *= t.a`) and computes `out = color * t`.
computes `out = color * t`.
- **Mode 2 (Text):** Vertex buffer contains real geometry in _physical_ pixels. SDL_ttf's GPU text
engine lays out glyphs in physical pixels (`TTF_SetFontSizeDPI` is called with `72 * dpi_scale`),
so `prepare_text` adds an anchor offset that is itself snapped to integer physical pixels for
atlas-aligned bilinear sampling, then writes vertices straight to the buffer. The vertex shader
must NOT rescale these vertices. Same fragment-shader kind as Tessellated; same indexed draws
into SDL_ttf atlas textures; the only difference is the coordinate space of the input. Mode 2
exists because integer-physical-pixel snapping is the load-bearing property of crisp glyph
rendering and CPU is the only place that snap can happen once-per-text-element instead of
per-vertex.
- **Mode 1 (SDF):** A static 6-vertex unit-quad buffer is drawn instanced, with per-primitive - **Mode 1 (SDF):** A static 6-vertex unit-quad buffer is drawn instanced, with per-primitive
`Core_2D_Primitive` structs (96 bytes each) uploaded each frame to a GPU storage buffer. The vertex `Core_2D_Primitive` structs (96 bytes each) uploaded each frame to a GPU storage buffer. The vertex
@@ -56,8 +43,8 @@ in the pipeline plan below for the full cliff/margin analysis and SBC architectu
The fragment shader's estimated peak footprint is ~2226 fp32 VGPRs (~1622 fp16 VGPRs on architectures The fragment shader's estimated peak footprint is ~2226 fp32 VGPRs (~1622 fp16 VGPRs on architectures
with native mediump) via manual live-range analysis. The dominant peak is the Ring_Arc kind path with native mediump) via manual live-range analysis. The dominant peak is the Ring_Arc kind path
(wedge normals + inner/outer radii + dot-product temporaries live simultaneously with carried state (wedge normals + inner/outer radii + dot-product temporaries live simultaneously with carried state
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size_ppx`). RRect is 12 regs lower like `f_color`, `f_uv_rect`/`f_effects`, and `half_size`). RRect is 12 regs lower (`corner_radii` vec4
(`corner_radii_ppx` vec4 replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers
apply live-range coalescing, mediump-to-fp16 promotion, and rematerialization that typically shave apply live-range coalescing, mediump-to-fp16 promotion, and rematerialization that typically shave
24 regs from hand-counted estimates — the conservative 26-reg upper bound is expected to compile 24 regs from hand-counted estimates — the conservative 26-reg upper bound is expected to compile
down to within the 24-register budget, but this must be verified with `malioc` (see "Verifying down to within the 24-register budget, but this must be verified with `malioc` (see "Verifying
@@ -445,32 +432,22 @@ our design:
### Main pipeline: SDF + tessellated (unified) ### Main pipeline: SDF + tessellated (unified)
The main pipeline serves three submission modes through a single `TRIANGLELIST` pipeline and a The main pipeline serves two submission modes through a single `TRIANGLELIST` pipeline and a single
single vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push constant
constant (`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`, `Core_2D_Mode.Text = 2`), pushed (`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`), pushed per draw call via `push_globals`. The
per draw call via `push_globals`. The vertex shader branches on this uniform to select the vertex shader branches on this uniform to select the tessellated or SDF code path.
appropriate code path.
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry in _logical_ - **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry. Used for text
pixels. Vertex shader scales positions by `dpi_scale`. Used for triangles, triangle fans/strips, (SDL_ttf atlas sampling), triangles, triangle fans/strips, single-pixel points, and any
single-pixel points, and any user-provided raw vertex geometry. user-provided raw vertex geometry.
- **SDF mode** (`mode = 1`): shared unit-quad vertex buffer + GPU storage buffer of - **SDF mode** (`mode = 1`): shared unit-quad vertex buffer + GPU storage buffer of
`Core_2D_Primitive` structs, drawn instanced. Used for all shapes with closed-form signed distance `Core_2D_Primitive` structs, drawn instanced. Used for all shapes with closed-form signed distance
functions. `Core_2D_Primitive.bounds` is in logical pixels; the vertex shader scales by functions.
`dpi_scale`.
- **Text mode** (`mode = 2`): direct vertex buffer with explicit geometry in _physical_ pixels.
Vertex shader does NOT scale. Used for SDL_ttf atlas sampling. The CPU-side anchor snap to
integer physical pixels (`prepare_text`/`prepare_text_transformed`) is what produces crisp glyphs
— sub-pixel anchors blur via the bilinear sampler. Mode 2 shares the fragment-shader path with
Tessellated (kind 0), so the only divergence between text and shape rasterization is the vertex
shader's `* dpi_scale` step.
All three modes use the same fragment shader. Modes 0 (Tessellated) and 2 (Text) take the same Both modes use the same fragment shader. The fragment shader checks `Shape_Kind` (low byte of
fragment-shader path (kind 0), which premultiplies the texture sample and computes `out = color * t`; `Core_2D_Primitive.flags`): kind 0 (`Solid`) is the tessellated path, which premultiplies the texture
they differ only in the vertex shader (whether positions are pre-scaled to physical pixels). Mode 1 sample and computes `out = color * t`; kinds 14 dispatch to one of four SDF functions (RRect, NGon,
(SDF) checks `Shape_Kind` (low byte of `Core_2D_Primitive.flags`): kinds 14 dispatch to one of four Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based on `Shape_Flags` bits.
SDF functions (RRect, NGon, Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based
on `Shape_Flags` bits.
#### Why SDF for shapes #### Why SDF for shapes
@@ -518,9 +495,9 @@ Compared to encoding per-primitive data in vertex attributes (the "fat vertex" a
buffer instancing eliminates the 46× data duplication across quad corners. A rounded rectangle costs buffer instancing eliminates the 46× data duplication across quad corners. A rounded rectangle costs
96 bytes instead of 4 vertices × 60+ bytes = 240+ bytes. 96 bytes instead of 4 vertices × 60+ bytes = 240+ bytes.
The tessellated and text paths retain the existing direct vertex buffer layout (20 bytes/vertex, no The tessellated path retains the existing direct vertex buffer layout (20 bytes/vertex, no storage
storage buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every invocation
invocation in a draw call has the same mode — so it is effectively free on all modern GPUs. in a draw call has the same mode — so it is effectively free on all modern GPUs.
#### Shape kinds and SDF dispatch #### Shape kinds and SDF dispatch
@@ -637,11 +614,11 @@ require, keeping each sub-pass well under the 32-register cliff.
frame renders into `source_texture` (a full-resolution single-sample texture owned by the frame renders into `source_texture` (a full-resolution single-sample texture owned by the
backdrop pipeline) instead of directly into the swapchain. At the end of the frame, backdrop pipeline) instead of directly into the swapchain. At the end of the frame,
`source_texture` is copied to the swapchain via a single `CopyGPUTextureToTexture` call. `source_texture` is copied to the swapchain via a single `CopyGPUTextureToTexture` call.
This means each bracket has no mid-frame texture copy: by the time a bracket runs, This means the bracket has no mid-frame texture copy: by the time the bracket runs,
`source_texture` already contains the contents written by everything that preceded it on the `source_texture` already contains the pre-bracket frame contents and is the natural sampler
timeline and is the natural sampler input. When no layer in the frame has a backdrop draw, input. When no layer in the frame has a backdrop draw, the existing fast path runs: the frame
the existing fast path runs: the frame renders directly to the swapchain and the backdrop renders directly to the swapchain and the backdrop pipeline's working textures are never
pipeline's working textures are never touched. Zero cost for backdrop-free frames. 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.
@@ -651,113 +628,81 @@ 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 #### Bracket scheduling model
Backdrop draws are scheduled via **explicit scopes**: every call to `backdrop_blur` must be wrapped The bracket is scheduled per layer, anchored at the first backdrop sub-batch in the layer's
in a `begin_backdrop` / `end_backdrop` pair (or the RAII-style `backdrop_scope` wrapper). Each submission order. Concretely, a layer with one or more backdrops splits into three groups:
scope produces exactly one bracket at render time. A layer may contain any number of scopes; draws
between scopes render at their submission position relative to the brackets, so the user controls
exactly which backdrops share a bracket.
At render time, `draw_layer` walks the layer's sub-batch list once, alternating between two run 1. **Pass A (pre-bracket)** — every non-backdrop sub-batch with index `< bracket_start_index`.
kinds: 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.
- **Non-backdrop runs** are rendered to `source_texture` in one render pass via `bracket_start_index` is the absolute index of the first `.Backdrop` kind in the layer's sub-batch
`render_layer_sub_batch_range`. Clear-vs-load is tracked frame-globally via `GLOB.cleared`. range. If the layer has no backdrops, none of this kicks in and the layer renders in a single render
- **Backdrop runs** are dispatched to `run_backdrop_bracket` with their index range. Each run is pass via the existing fast path.
one bracket; the bracket opens and closes its own render passes for downsample, H-blur, V-blur,
and composite stages.
Within a bracket, the scheduler groups contiguous same-sigma sub-batches and runs four sub-passes Per-sigma-group execution. The bracket walks each layer's sub-batches and groups contiguous
per group: downsample (`source_texture``downsample_texture`), H-blur (`downsample_texture` `.Backdrop` sub-batches that share a sigma; each group picks its own downsample factor (1, 2, or 4)
`h_blur_texture`), V-blur (`h_blur_texture``downsample_texture`, ping-pong reuse), and based on `compute_backdrop_downsample_factor`. For each group it runs four sub-passes: a downsample
composite (`downsample_texture``source_texture` with SDF mask and tint applied). Each group from `source_texture` to `downsample_texture`; an H-blur from `downsample_texture` to
picks its own downsample factor (1, 2, or 4) based on sigma; see the comment block at the top of `h_blur_texture`; a V-blur from `h_blur_texture` back into `downsample_texture` (ping-pong reuse);
`backdrop.odin` for the factor-selection table. and finally a composite that reads the fully-blurred `downsample_texture`, applies the SDF mask
and tint, and writes the result to `source_texture`. Sub-batch coalescing in
`append_or_extend_sub_batch` merges contiguous same-sigma backdrops into a single instanced
composite draw; non-contiguous same-sigma backdrops still share the blur output but issue separate
composite draws.
Sub-batch coalescing in `append_or_extend_sub_batch` merges contiguous same-sigma backdrops The working textures are sized at the full swapchain resolution; larger downsample factors only
sharing one scissor into a single instanced composite draw. Same-sigma backdrops separated by a fill a sub-rect via viewport-limited rendering (see the comment block at the top of `backdrop.odin`
`ScissorStart` boundary stay in one sigma group (one set of blur passes) but issue separate for the factor-selection table and rationale).
composite draws; the composite pass calls `SetGPUScissor` between draws when the active scissor
changes.
Working textures are sized at full swapchain resolution; larger downsample factors fill a sub-rect #### Submission-order trade-off
via viewport-limited rendering.
#### Scope contract 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:
Scope state is global: `GLOB.open_backdrop_layer` tracks the currently-open scope (or `nil`) for ```
the whole renderer. The five misuse cases panic via `log.panic` / `log.panicf`: draw.rectangle(layer, bg, GRAY) // 0 Tessellated → Pass A
draw.rectangle(layer, card_blue, BLUE) // 1 SDF → Pass A
draw.gaussian_blur(layer, panelA, sigma=12) // 2 Backdrop → Bracket (sees: bg + blue card)
draw.rectangle(layer, card_red, RED) // 3 SDF → Pass B (drawn ON TOP of panelA)
draw.gaussian_blur(layer, panelB, sigma=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)
```
1. `backdrop_blur` called outside an open scope. In this layer, panelB does _not_ see card_red — even though card_red was submitted before panelB —
2. A non-backdrop draw call issued on a layer with an open scope. Asserted at the top of because both backdrops sample `source_texture` as it stood at the bracket entry, which is after
`append_or_extend_sub_batch`. Pass A and before card_red has rendered. card_red ends up on top of panelA, not underneath it.
3. `new_layer` called while a scope is open.
4. `end()` called while a scope is open.
5. `begin_backdrop` while one is already open, or `end_backdrop` on the wrong layer.
Worked example with two scopes on the same layer: 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(...) base := draw.begin(...)
draw.rectangle(base, bg, GRAY) draw.rectangle(base, bg, GRAY)
draw.rectangle(base, card_blue, BLUE) draw.rectangle(base, card_blue, BLUE)
draw.gaussian_blur(base, panelA, sigma=12) // panelA in base layer's bracket
{ top := draw.new_layer(base, ...)
draw.backdrop_scope(base) draw.rectangle(top, card_red, RED)
draw.backdrop_blur(base, panelA, sigma=12) // bracket 1: sees bg + blue card draw.gaussian_blur(top, panelB, sigma=12) // top layer's bracket; sees base + card_red
} draw.text(top, "label", ...)
draw.rectangle(base, card_red, RED) // renders ON TOP of panelA's composite
{
draw.backdrop_scope(base)
draw.backdrop_blur(base, panelB, sigma=12) // bracket 2: sees bg + blue card + panelA + card_red
}
draw.text(base, "label", ...) // renders ON TOP of panelB's composite
``` ```
Each bracket adds four render passes (downsample + H-blur + V-blur + composite) plus tile-cache Why one bracket per layer and not one per backdrop? Each bracket adds three render passes
flushes on tilers like Mali Valhall, so users who don't need interleaving should group backdrops (downsample + H-blur + V-composite) and at least three tile-cache flushes on tilers like Mali
into a single scope to amortize: 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
draw.backdrop_scope(base) abstraction. This matches the cost/complexity envelope of iOS `UIVisualEffectView` and CSS
draw.backdrop_blur(base, panelA, sigma=12) // shares one bracket with panelB; `backdrop-filter` (both of which constrain backdrop ordering implicitly).
draw.backdrop_blur(base, panelB, sigma=12) // same sigma also coalesces into one
} // instanced composite draw call
```
#### Clay integration: `Backdrop_Marker`
Clay has no notion of backdrops. The integration uses Clay's only extension point — the opaque
`customData: rawptr` on `clay.CustomElementConfig` — to carry a magic-number-tagged struct that
`prepare_clay_batch` recognizes:
```
Backdrop_Marker :: struct {
magic: u32, // BACKDROP_MARKER_MAGIC (0x42445054, 'BDPT')
sigma: f32,
tint: Color,
radii: Rectangle_Radii,
feather_ppx: f32,
}
```
The user populates a `Backdrop_Marker` (with stable lifetime through the `prepare_clay_batch`
call) and points the corresponding `clay.CustomElementConfig.customData` at it.
`prepare_clay_batch` walks Clay's command stream once, calling `is_clay_backdrop` per command
(a u32 magic check on `customData`'s first 4 bytes). On a hit it opens a backdrop scope (or
extends an open one) and dispatches via `backdrop_blur`. Non-backdrop commands issued during an
open scope go to a deferred index buffer for replay after the scope closes; this preserves Clay's
painter's-algorithm ordering across backdrops without violating the scope contract.
The magic-number sentinel keeps the marker type self-describing in core dumps and decouples the
integration from Clay-side changes. Zero-init memory has `magic = 0`, so a marker with a forgotten
magic field gets routed through the regular `custom_draw` path and surfaces as "my custom draw
never fired" rather than as a silent backdrop schedule.
### Vertex layout ### Vertex layout
@@ -785,7 +730,7 @@ Core_2D_Primitive :: struct {
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 (sin, cos). Requires .Rotated flag. rotation_sc: u32, // 24: packed f16 pair (sin, cos). Requires .Rotated flag.
_pad: f32, // 28: reserved for future use _pad: f32, // 28: reserved for future use
params: Shape_Params, // 32: per-kind params union (half_feather_ppx, radii_ppx, etc.) (32 bytes) params: Shape_Params, // 32: per-kind params union (half_feather, radii, etc.) (32 bytes)
uv_rect: [4]f32, // 64: texture UV coordinates. Read when .Textured. uv_rect: [4]f32, // 64: texture UV coordinates. Read when .Textured.
effects: Gradient_Outline, // 80: gradient and/or outline parameters (16 bytes). effects: Gradient_Outline, // 80: gradient and/or outline parameters (16 bytes).
} }
+63 -102
View File
@@ -392,36 +392,16 @@ frame_has_backdrop :: proc() -> bool {
return false return false
} }
// Find the scissor that owns a given sub-batch index by linear scan over GLOB.scissors. // Returns the absolute index of the first .Backdrop sub-batch in the layer's sub-batch range,
// Used by `run_backdrop_bracket`'s composite pass when the bracket loses its layer-pointer // or -1 if the layer has no backdrops. The index is into GLOB.tmp_sub_batches (not relative to
// context: per-sub-batch scissor lookup is required to honor scissors set up upstream by // layer.sub_batch_start), to match how draw_layer's render-range helpers consume it.
// `prepare_clay_batch`'s ScissorStart handling. O(scissors) per sub-batch is acceptable
// because scissor counts are small (single digits in typical UI frames).
//
// Panics if no scissor owns the index. The renderer's invariant is that the scissor list
// forms a contiguous, disjoint cover over `[0, len(tmp_sub_batches))` because every
// sub-batch is created via `append_or_extend_sub_batch` (which increments the active
// scissor's `sub_batch_len` in lockstep with the global array's growth) and scissors are
// only created at the current end-of-array. A miss here means that invariant is broken —
// either by a future code change that bypasses `append_or_extend_sub_batch`, by a scissor
// constructed with the wrong `sub_batch_start`, or by external corruption — and silent
// degradation would mask the bug. The panic message includes the offending index and the
// scissor list shape so the failure is locatable.
//INTERNAL //INTERNAL
find_scissor_for_sub_batch :: proc(sub_batch_index: u32) -> sdl.Rect { find_first_backdrop_in_layer :: proc(layer: ^Layer) -> int {
for scissor in GLOB.scissors { for i in 0 ..< layer.sub_batch_len {
if sub_batch_index >= scissor.sub_batch_start && abs_idx := layer.sub_batch_start + i
sub_batch_index < scissor.sub_batch_start + scissor.sub_batch_len { if GLOB.tmp_sub_batches[abs_idx].kind == .Backdrop do return int(abs_idx)
return scissor.bounds
} }
} return -1
log.panicf(
"find_scissor_for_sub_batch: no scissor owns sub-batch index %d (scissor count=%d, total sub-batches=%d); " +
"the scissor list must form a contiguous cover over all sub-batches",
sub_batch_index,
len(GLOB.scissors),
len(GLOB.tmp_sub_batches),
)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -487,10 +467,10 @@ MAX_GAUSSIAN_BLUR_KERNEL_PAIRS :: 32
// pipeline rather than tacked onto this one as a flag bit. // pipeline rather than tacked onto this one as a flag bit.
//INTERNAL //INTERNAL
Gaussian_Blur_Primitive :: struct { Gaussian_Blur_Primitive :: struct {
bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy) in logical px bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy)
radii_ppx: [4]f32, // 16: 16 — per-corner radii (BR, TR, BL, TL) radii: [4]f32, // 16: 16 — per-corner radii in physical pixels (BR, TR, BL, TL)
half_size_ppx: [2]f32, // 32: 8 — RRect half extents half_size: [2]f32, // 32: 8 — RRect half extents (physical px)
half_feather_ppx: f32, // 40: 4 — feather_ppx * 0.5 (SDF anti-aliasing) half_feather: f32, // 40: 4 — feather_px * 0.5 (SDF anti-aliasing)
color: Color, // 44: 4 — tint, packed RGBA u8x4 color: Color, // 44: 4 — tint, packed RGBA u8x4
} }
#assert(size_of(Gaussian_Blur_Primitive) == 48) #assert(size_of(Gaussian_Blur_Primitive) == 48)
@@ -684,13 +664,7 @@ upload_backdrop_primitives :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPas
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
) )
// cycle=true: this is a persistent per-frame streaming transfer buffer. The previous prim_array := sdl.MapGPUTransferBuffer(device, GLOB.backdrop.primitive_buffer.transfer, false)
// frame's UploadToGPUBuffer is almost certainly still in flight when we map here
// (allowedFramesInFlight defaults to 2 on Metal). Without cycling, the CPU memcpy below
// races the GPU's blit read on the same MTLBuffer.contents. Cycling rebinds the
// container's active internal buffer to an unbound one (or allocates a new one) — O(1)
// in steady state, no fence wait. See SDL_gpu_metal.m's METAL_INTERNAL_PrepareBufferForWrite.
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.backdrop.primitive_buffer.transfer, true)
if prim_array == nil { if prim_array == nil {
log.panicf("Failed to map backdrop primitive transfer buffer: %s", sdl.GetError()) log.panicf("Failed to map backdrop primitive transfer buffer: %s", sdl.GetError())
} }
@@ -765,14 +739,11 @@ compute_backdrop_group_work_region :: proc(
max_x += halo_logical max_x += halo_logical
max_y += halo_logical max_y += halo_logical
// Clamp the min corner to 0, but let the max corner's 6σ halo extend past the swapchain edge // Convert to physical pixels and clamp to swapchain bounds.
// into the working texture's unused area (at factor > 1), capped to the texture extent. Keeps phys_min_x := math.max(min_x * dpi, 0)
// the composite's bilinear upsample off the unwritten texels just past a clamped edge. phys_min_y := math.max(min_y * dpi, 0)
downsample_factor := compute_backdrop_downsample_factor(sigma_logical) phys_max_x := math.min(max_x * dpi, f32(swapchain_width))
phys_min_x := max(min_x * dpi, 0) phys_max_y := math.min(max_y * dpi, f32(swapchain_height))
phys_min_y := max(min_y * dpi, 0)
phys_max_x := min(max_x * dpi, f32(swapchain_width * downsample_factor))
phys_max_y := min(max_y * dpi, f32(swapchain_height * downsample_factor))
if phys_max_x <= phys_min_x || phys_max_y <= phys_min_y do return 0, 0, 0, 0 if phys_max_x <= phys_min_x || phys_max_y <= phys_min_y do return 0, 0, 0, 0
@@ -783,16 +754,14 @@ compute_backdrop_group_work_region :: proc(
return return
} }
// Run one bracket over a contiguous range of pure-backdrop sub-batches. Assumes: // Run the backdrop bracket for one layer. Assumes:
// - source_texture currently holds the pre-bracket frame contents (everything submitted // - source_texture currently holds the pre-bracket frame contents (Pass A has already
// ahead of this bracket on the same layer has already been rendered). // rendered everything that should appear behind the backdrop).
// - The caller has invoked ensure_backdrop_textures with current swapchain dimensions. // - The caller has invoked ensure_backdrop_textures with current swapchain dimensions.
// - The half-open range `[sub_batch_start, sub_batch_end)` is non-empty and every // - At least one .Backdrop sub-batch exists in the layer (caller checked).
// sub-batch in it has kind == .Backdrop. The caller (draw_layer) guarantees this by
// splitting the layer into runs.
// //
// Per-sigma-group execution. The bracket walks the range in submission order, grouping // Per-sigma-group execution. The bracket walks the layer's sub-batches in submission order,
// contiguous-same-sigma .Backdrop sub-batches. For each group: // grouping contiguous-same-sigma .Backdrop sub-batches. For each group:
// 1. Pick a downsample factor using compute_backdrop_downsample_factor. // 1. Pick a downsample factor using compute_backdrop_downsample_factor.
// 2. Compute that group's work region (primitives' AABB + 6σ halo, clamped). // 2. Compute that group's work region (primitives' AABB + 6σ halo, clamped).
// 3. Downsample: source_texture → downsample_texture, viewport-limited to // 3. Downsample: source_texture → downsample_texture, viewport-limited to
@@ -801,22 +770,21 @@ compute_backdrop_group_work_region :: proc(
// 5. V-blur (mode 0, direction=V): h_blur_texture → downsample_texture (ping-pong reuse; // 5. V-blur (mode 0, direction=V): h_blur_texture → downsample_texture (ping-pong reuse;
// downsample_texture's data is no longer needed). Same viewport. // downsample_texture's data is no longer needed). Same viewport.
// 6. Composite (mode 1): downsample_texture (now holds H+V blur) → source_texture, full- // 6. Composite (mode 1): downsample_texture (now holds H+V blur) → source_texture, full-
// target viewport, per-primitive SDF discard handles masking and applies the tint. // target viewport, per-primitive SDF discard handles masking and applies the tint. Each
// Each sub-batch in the group issues an instanced draw under its own scissor (sub- // sub-batch in the group is one instanced draw.
// batches inherit scissor state from the surrounding ScissorStart/End at submission).
// //
// V-blur is run as its own working→working pass rather than folded into the composite. The // V-blur is run as its own working→working pass rather than folded into the composite. The
// folded variant produces a horizontal-vs-vertical asymmetry artifact (horizontal source // folded variant produces a horizontal-vs-vertical asymmetry artifact (horizontal source
// features end up looking sharper than vertical ones inside the panel). Matching V's // features end up looking sharper than vertical ones inside the panel). Matching V's
// structure exactly to H's restores symmetry. // structure exactly to H's restores symmetry.
// //
// On exit, source_texture contains the pre-bracket contents plus all backdrop primitives in // On exit, source_texture contains the pre-bracket contents plus all backdrop primitives
// this range composited on top. // composited on top. The caller then runs Pass B (post-bracket non-backdrop sub-batches) on
// source_texture with LOAD.
//INTERNAL //INTERNAL
run_backdrop_bracket :: proc( run_backdrop_bracket :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer, cmd_buffer: ^sdl.GPUCommandBuffer,
sub_batch_start: u32, layer: ^Layer,
sub_batch_end: u32,
swapchain_width, swapchain_height: u32, swapchain_width, swapchain_height: u32,
) { ) {
pipeline := &GLOB.backdrop pipeline := &GLOB.backdrop
@@ -829,23 +797,32 @@ run_backdrop_bracket :: proc(
min_depth = 0, min_depth = 0,
max_depth = 1, max_depth = 1,
} }
full_scissor := sdl.Rect {
x = 0,
y = 0,
w = i32(swapchain_width),
h = i32(swapchain_height),
}
// Working textures are at full swapchain resolution. Each per-group factor=N pass writes // Working textures are at full swapchain resolution. Each per-group factor=N pass writes
// only to a sub-rect of dimensions (work_region_phys / N), via viewport-limited rendering. // only to a sub-rect of dimensions (work_region_phys / N), via viewport-limited rendering.
layer_end := sub_batch_end layer_end := layer.sub_batch_start + layer.sub_batch_len
i := sub_batch_start i := layer.sub_batch_start
for i < layer_end { for i < layer_end {
// Caller guarantees this range is pure backdrop sub-batches.
assert(GLOB.tmp_sub_batches[i].kind == .Backdrop, "non-backdrop sub-batch inside bracket range")
batch := GLOB.tmp_sub_batches[i] batch := GLOB.tmp_sub_batches[i]
if batch.kind != .Backdrop {
i += 1
continue
}
// Find the contiguous run of .Backdrop sub-batches with this sigma. // Find the contiguous run of .Backdrop sub-batches with this sigma.
sigma := batch.gaussian_sigma sigma := batch.gaussian_sigma
group_start := i group_start := i
group_end := i + 1 group_end := i + 1
for group_end < layer_end { for group_end < layer_end {
if GLOB.tmp_sub_batches[group_end].gaussian_sigma != sigma do break next := GLOB.tmp_sub_batches[group_end]
if next.kind != .Backdrop || next.gaussian_sigma != sigma do break
group_end += 1 group_end += 1
} }
@@ -871,18 +848,12 @@ run_backdrop_bracket :: proc(
working_w := (region_w + downsample_factor - 1) / downsample_factor working_w := (region_w + downsample_factor - 1) / downsample_factor
working_h := (region_h + downsample_factor - 1) / downsample_factor working_h := (region_h + downsample_factor - 1) / downsample_factor
// Clamp to the full texture extent (not cached/factor): the working textures are full // Working textures are sized at min factor (2). At factor=4 we have only half the texture
// swapchain res, so a factor-N group's halo can spill into the unused remainder. Writing it // area available in each axis. Clamp to the texture extent for either case.
// keeps the composite's bilinear upsample off unwritten texels at the right/bottom edge. wt_w := pipeline.cached_width / downsample_factor
texture_width := pipeline.cached_width wt_h := pipeline.cached_height / downsample_factor
texture_height := pipeline.cached_height if working_x + working_w > wt_w do working_w = wt_w - working_x
// Skip fully off-screen groups; also guards the unsigned clamps below from underflow. if working_y + working_h > wt_h do working_h = wt_h - working_y
if working_x >= texture_width || working_y >= texture_height {
i = group_end
continue
}
if working_x + working_w > texture_width do working_w = texture_width - working_x
if working_y + working_h > texture_height do working_h = texture_height - working_y
if working_w == 0 || working_h == 0 { if working_w == 0 || working_h == 0 {
i = group_end i = group_end
continue continue
@@ -1026,10 +997,6 @@ run_backdrop_bracket :: proc(
// upsamples (via bilinear filtering on the read), applies the SDF mask, and applies the // upsamples (via bilinear filtering on the read), applies the SDF mask, and applies the
// tint. One render pass for the whole sigma group; each sub-batch issues its own draw // tint. One render pass for the whole sigma group; each sub-batch issues its own draw
// call because non-contiguous-but-same-sigma sub-batches couldn't coalesce upstream. // call because non-contiguous-but-same-sigma sub-batches couldn't coalesce upstream.
//
// Per-sub-batch scissor: sub-batches inherit scissor state from ScissorStart/End that
// surrounded their submission. Switching scissors mid-pass is cheap; what matters is
// that the composite respects the same clipping the caller set up.
{ {
frag_uniforms.mode = 1 frag_uniforms.mode = 1
// direction is unused in mode 1 but keep it set so reading the uniform doesn't see // direction is unused in mode 1 but keep it set so reading the uniform doesn't see
@@ -1044,6 +1011,7 @@ run_backdrop_bracket :: proc(
) )
sdl.BindGPUGraphicsPipeline(pass, pipeline.blur_pipeline) sdl.BindGPUGraphicsPipeline(pass, pipeline.blur_pipeline)
sdl.SetGPUViewport(pass, full_viewport) sdl.SetGPUViewport(pass, full_viewport)
sdl.SetGPUScissor(pass, full_scissor)
push_backdrop_vert_globals(cmd_buffer, f32(swapchain_width), f32(swapchain_height), 1) push_backdrop_vert_globals(cmd_buffer, f32(swapchain_width), f32(swapchain_height), 1)
push_backdrop_blur_frag_globals(cmd_buffer, &frag_uniforms) push_backdrop_blur_frag_globals(cmd_buffer, &frag_uniforms)
sdl.BindGPUVertexStorageBuffers(pass, 0, ([^]^sdl.GPUBuffer)(&pipeline.primitive_buffer.gpu), 1) sdl.BindGPUVertexStorageBuffers(pass, 0, ([^]^sdl.GPUBuffer)(&pipeline.primitive_buffer.gpu), 1)
@@ -1053,17 +1021,8 @@ run_backdrop_bracket :: proc(
&sdl.GPUTextureSamplerBinding{texture = pipeline.downsample_texture, sampler = pipeline.sampler}, &sdl.GPUTextureSamplerBinding{texture = pipeline.downsample_texture, sampler = pipeline.sampler},
1, 1,
) )
current_scissor: sdl.Rect = {0, 0, 0, 0}
scissor_set := false
for j in group_start ..< group_end { for j in group_start ..< group_end {
grp := GLOB.tmp_sub_batches[j] grp := GLOB.tmp_sub_batches[j]
sub_batch_scissor := find_scissor_for_sub_batch(j)
if !scissor_set || sub_batch_scissor != current_scissor {
sdl.SetGPUScissor(pass, sub_batch_scissor)
current_scissor = sub_batch_scissor
scissor_set = true
}
sdl.DrawGPUPrimitives(pass, 6, grp.count, 0, grp.offset) sdl.DrawGPUPrimitives(pass, 6, grp.count, 0, grp.offset)
} }
sdl.EndGPURenderPass(pass) sdl.EndGPURenderPass(pass)
@@ -1085,7 +1044,7 @@ run_backdrop_bracket :: proc(
build_backdrop_primitive :: proc( build_backdrop_primitive :: proc(
rect: Rectangle, rect: Rectangle,
radii: Rectangle_Radii, radii: Rectangle_Radii,
feather_ppx: f32, feather_px: f32,
) -> Gaussian_Blur_Primitive { ) -> Gaussian_Blur_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)
@@ -1093,8 +1052,8 @@ build_backdrop_primitive :: proc(
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius) clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius) clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
half_feather_ppx := feather_ppx * 0.5 half_feather := feather_px * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
half_width := rect.width * 0.5 half_width := rect.width * 0.5
@@ -1113,14 +1072,14 @@ build_backdrop_primitive :: proc(
// (p.x > 0) ? r.xy : r.zw picks right-vs-left half // (p.x > 0) ? r.xy : r.zw picks right-vs-left half
// then (p.y > 0) ? rxy.x : rxy.y picks bottom-vs-top within that half // then (p.y > 0) ? rxy.x : rxy.y picks bottom-vs-top within that half
// So slot 0 = bottom-right, slot 1 = top-right, slot 2 = bottom-left, slot 3 = top-left. // So slot 0 = bottom-right, slot 1 = top-right, slot 2 = bottom-left, slot 3 = top-left.
radii_ppx = { radii = {
clamped_bottom_right * dpi_scale, clamped_bottom_right * dpi_scale,
clamped_top_right * dpi_scale, clamped_top_right * dpi_scale,
clamped_bottom_left * dpi_scale, clamped_bottom_left * dpi_scale,
clamped_top_left * dpi_scale, clamped_top_left * dpi_scale,
}, },
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale}, half_size = {half_width * dpi_scale, half_height * dpi_scale},
half_feather_ppx = half_feather_ppx, half_feather = half_feather,
} }
} }
@@ -1170,16 +1129,18 @@ prepare_backdrop_primitive :: proc(layer: ^Layer, prim: Gaussian_Blur_Primitive,
// pass pair via sub-batch coalescing. Primitives with different sigmas in the same layer // pass pair via sub-batch coalescing. Primitives with different sigmas in the same layer
// trigger separate blur passes (cost scales with the number of unique sigmas). // trigger separate blur passes (cost scales with the number of unique sigmas).
// //
// Must be called inside a `begin_backdrop` / `end_backdrop` scope (or use `backdrop_scope`). // Submission ordering is asymmetric: a non-backdrop draw submitted between two backdrops in
backdrop_blur :: proc( // the same layer renders *on top of* both backdrops, not between them. Use `draw.new_layer`
// to interleave. See README.md § "Backdrop pipeline" for the full bracket scheduling model.
gaussian_blur :: proc(
layer: ^Layer, layer: ^Layer,
rect: Rectangle, rect: Rectangle,
gaussian_sigma: f32, gaussian_sigma: f32,
tint: Color = DFT_TINT, tint: Color = DFT_TINT,
radii: Rectangle_Radii = {}, radii: Rectangle_Radii = {},
feather_ppx: f32 = DFT_FEATHER_PPX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
prim := build_backdrop_primitive(rect, radii, feather_ppx) prim := build_backdrop_primitive(rect, radii, feather_px)
prim.color = tint prim.color = tint
prepare_backdrop_primitive(layer, prim, gaussian_sigma) prepare_backdrop_primitive(layer, prim, gaussian_sigma)
} }
-794
View File
@@ -1,794 +0,0 @@
// Clay UI integration for the `draw` package.
//
// All code in this file is dedicated to bridging Clay's render command stream into `draw`'s
// primitive/sub-batch pipeline. Nothing outside this file should reference the `clay` package
// directly; everything Clay-related (types, lifecycle helpers, render-command dispatch, the
// border-merge stack, the Clay backdrop bracket walker, the text measure/error callbacks,
// and the `Clay_Image_Data` user-facing helper) lives here. `draw.odin`'s lifecycle procs
// call `init_clay`, `destroy_clay`, and `clear_clay_per_frame` to drive the bits of state
// that necessarily live on the shared `Global` struct.
package draw
import "base:runtime"
import "core:c"
import "core:log"
import "core:strings"
import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"
import clay "../vendor/clay"
// ---------------------------------------------------------------------------------------------------------------------
// ----- Lifecycle ------------
// ---------------------------------------------------------------------------------------------------------------------
// Allocate the Clay arena, build the merge-candidate stack, hand the arena to Clay, and
// register the text-measurement and error callbacks. Called by `init` once `GLOB` has been
// populated with the device/window state Clay's callbacks read from.
//INTERNAL
init_clay :: proc(window: ^sdl.Window, allocator: runtime.Allocator) {
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
GLOB.clay_merge_open_stack = make([dynamic]Clay_Merge_Candidate, 0, 16, allocator = allocator)
GLOB.clay_memory = make([^]u8, min_memory_size, allocator = allocator)
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
window_width, window_height: c.int
sdl.GetWindowSize(window, &window_width, &window_height)
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
clay.SetMeasureTextFunction(measure_text_clay, nil)
}
// Free the Clay arena memory allocated in `init_clay`. Called by `destroy`. The merge stack
// is left to the package allocator's normal teardown to preserve historical behavior.
//INTERNAL
destroy_clay :: proc(allocator: runtime.Allocator) {
free(GLOB.clay_memory, allocator)
}
// Reset Clay per-frame state: the z-index high-water mark and the border-merge stack.
// Called by `clear_global` at the start of every frame.
//INTERNAL
clear_clay_per_frame :: proc() {
GLOB.clay_z_index = 0
clear(&GLOB.clay_merge_open_stack)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Image data (Clay RenderCommandType.Image payload) ------------
// ---------------------------------------------------------------------------------------------------------------------
Clay_Image_Data :: struct {
texture_id: Texture_Id,
fit: Fit_Mode,
tint: Color,
}
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
return {texture_id = id, fit = fit, tint = tint}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Callbacks (clay -> draw) ------------
// ---------------------------------------------------------------------------------------------------------------------
@(private = "file")
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
context = GLOB.odin_context
log.error("Clay error:", errorData.errorType, errorData.errorText)
}
@(private = "file")
measure_text_clay :: proc "c" (
text: clay.StringSlice,
config: ^clay.TextElementConfig,
user_data: rawptr,
) -> clay.Dimensions {
context = GLOB.odin_context
text := string(text.chars[:text.length])
c_text := strings.clone_to_cstring(text, context.temp_allocator)
defer delete(c_text, context.temp_allocator)
width, height: c.int
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
log.panicf("Failed to measure text: %s", sdl.GetError())
}
return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Custom draw + customData envelope ------------
// ---------------------------------------------------------------------------------------------------------------------
// Called for each Clay `RenderCommandType.Custom` render command that
// `prepare_clay_batch` encounters and which is NOT a levlib-managed variant
// (e.g. `Backdrop_Marker`).
//
// - `layer` is the layer the command belongs to (post-z-index promotion).
// - `bounds` is already translated into the active layer's coordinate system
// and pre-DPI, matching what the built-in shape procs expect.
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
// `backgroundColor` and `cornerRadius`. Its `customData` field has been
// unwrapped from the `Clay_Custom` envelope: it points at the user's own
// data (the value the user wrote into the `rawptr` variant), not at the
// `Clay_Custom` itself. If the union was zero-init (no variant set) or
// `customData` was originally nil, the callback receives nil.
//
// The callback must not call `new_layer` or `prepare_clay_batch`.
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
ClayBatch :: struct {
bounds: Rectangle,
cmds: clay.ClayArray(clay.RenderCommand),
}
// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point
// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by
// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is
// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before
// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData`
// exactly as if no wrapper were involved.
//
// Contract: `customData`, when non-nil, MUST point at storage holding a `Clay_Custom`
// value. The user owns that storage; its lifetime must span the Clay layout call and the
// matching `prepare_clay_batch` call. Pointing `customData` at a bare user struct violates
// the contract — the dispatcher will read its first bytes as a union tag and either route
// the draw incorrectly or panic on type assertion. There is no recovery path; this is a
// strict-discipline API by design.
//
// Construction notes (Odin implicit-conversion rules):
// - Backdrop variant: `bd: Clay_Custom = Backdrop_Marker{...}` works directly.
// Variant-to-union conversion is implicit.
// - User pointer: `up: Clay_Custom = rawptr(&my_struct)` — the explicit `rawptr(...)` is
// required because Odin does not chain `^T -> rawptr -> Clay_Custom` implicitly. A bare
// `up: Clay_Custom = &my_struct` is a compile error.
Clay_Custom :: union {
Backdrop_Marker,
rawptr,
}
// Per-primitive parameters for a backdrop blur dispatched through the Clay integration.
// Embedded as a `Clay_Custom` variant; `prepare_clay_batch` walks the command stream,
// opens/closes a backdrop scope around contiguous backdrop runs, and feeds these to
// `backdrop_blur` via `dispatch_clay_backdrop`. The discriminant is the union tag — no
// in-band magic field needed (compiler-enforced).
Backdrop_Marker :: struct {
sigma: f32,
tint: Color,
radii: Rectangle_Radii,
feather_ppx: f32,
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Border-merge stack ------------
// ---------------------------------------------------------------------------------------------------------------------
// One entry on the Clay merge stack. Pushed by `dispatch_clay_command` when emitting a
// Rectangle or an Image primitive, then popped by a matching Border to retroactively add
// the outline. See `try_dispatch_clay_border_merge` for the matching semantics.
//INTERNAL
Clay_Merge_Candidate :: struct {
primitive_index: u32, // Index into `GLOB.tmp_primitives` of the candidate primitive.
outer_bounds: Rectangle, // Clay's bounding box — keyed on for the bounds match check.
corner_radii: clay.CornerRadius, // Clay's corner radii — also keyed on for the match check.
image_data: Clay_Image_Data, // Only read when kind == .Fill_Texture (needed to refit UVs to inner_bounds).
kind: Clay_Merge_Candidate_Kind,
}
//INTERNAL
Clay_Merge_Candidate_Kind :: enum u8 {
// Solid Color brush. Used for Rectangle commands and for the bg primitive of an Image
// command that has `backgroundColor.a > 0`. Merge mutation: shrink shape + add outline.
Fill_Color,
// Texture_Fill brush. Used for the image primitive of an Image command with no bg, where
// `fit_params` returned `fit_rect == outer_bounds` (the image fully covers Clay's bounds).
// Merge mutation: shrink shape + add outline + refit UV against inner_bounds.
Fill_Texture,
}
// Returns true if this Clay render command represents a backdrop primitive — i.e. its
// `customData` points at a `Clay_Custom` whose active variant is `Backdrop_Marker`.
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
if cmd.commandType != .Custom do return false
p := cmd.renderData.custom.customData
if p == nil do return false
_, ok := (^Clay_Custom)(p).(Backdrop_Marker)
return ok
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Border emission ------------
// ---------------------------------------------------------------------------------------------------------------------
// Emit a Clay border drawn INSIDE `bounds` — the outer edge of each side aligns with
// `bounds`, the inner edge is `border_width.*` pixels inset. Matches Clay's layout model
// (CSS border-box) so the visible element occupies exactly Clay's allocated space.
//
// The fast path (uniform widths) uses `rectangle()` with the built-in SDF outline, which
// always extends outward from the shape it's given — we pre-shrink the shape by
// `border_width` so the outline lands precisely at Clay's bounds. The slow path (non-uniform
// widths) emits per-side rectangles and per-corner arcs directly, all positioned inside
// `bounds`. All-zero widths is a no-op.
//
// A corner is rounded iff its radius is positive AND both adjacent sides have positive
// width. Top corners take their thickness from `border_width.top`, bottom corners from
// `border_width.bottom`. When the two widths meeting at a corner differ there is a step at
// the side/corner junction (acceptable for the rare mixed-width case).
//
// When `border_width > corner_radius`, the inner corner clamps to zero (sharp inside, still
// rounded outside) — matches CSS-standard behavior.
//INTERNAL
clay_emit_partial_border :: proc(
layer: ^Layer,
bounds: Rectangle,
border_color: Color,
border_width: clay.BorderWidth,
corner_radii: clay.CornerRadius,
) {
// All-zero: nothing to draw.
if border_width.top == 0 && border_width.right == 0 && border_width.bottom == 0 && border_width.left == 0 {
return
}
// Convert side widths once (u16 -> f32) and cache for reuse.
width_top := f32(border_width.top)
width_right := f32(border_width.right)
width_bottom := f32(border_width.bottom)
width_left := f32(border_width.left)
// Fast path: all four sides have the same nonzero width. Pre-shrink the shape by the
// uniform width so the SDF outline (which always extends outward from the shape) lands
// exactly at Clay's `bounds` — the visible border ends up INSIDE Clay's allocation while
// the SDF mechanism keeps doing outward outlining. Single SDF primitive, exact curves,
// analytical AA.
if border_width.left == border_width.top &&
border_width.top == border_width.right &&
border_width.right == border_width.bottom {
uniform_width := width_top
inner_bounds := Rectangle {
x = bounds.x + uniform_width,
y = bounds.y + uniform_width,
width = bounds.width - 2 * uniform_width,
height = bounds.height - 2 * uniform_width,
}
inner_radii := Rectangle_Radii {
top_left = max(0, corner_radii.topLeft - uniform_width),
top_right = max(0, corner_radii.topRight - uniform_width),
bottom_right = max(0, corner_radii.bottomRight - uniform_width),
bottom_left = max(0, corner_radii.bottomLeft - uniform_width),
}
rectangle(
layer,
inner_bounds,
BLANK,
outline_color = border_color,
outline_width = uniform_width,
radii = inner_radii,
)
return
}
// A corner is drawn rounded only if its radius is positive AND both adjacent sides are present.
top_left_rounded := corner_radii.topLeft > 0 && border_width.top > 0 && border_width.left > 0
top_right_rounded := corner_radii.topRight > 0 && border_width.top > 0 && border_width.right > 0
bottom_left_rounded := corner_radii.bottomLeft > 0 && border_width.bottom > 0 && border_width.left > 0
bottom_right_rounded := corner_radii.bottomRight > 0 && border_width.bottom > 0 && border_width.right > 0
// Horizontal x-coordinates where the top/bottom side rectangles start/end. When the
// adjacent corner is rounded, the side stops at `bounds.x + radius` (where the corner
// arc takes over). When not rounded, the side runs to the bounds edge; the perpendicular
// side handles the inset to avoid overlap.
top_left_x: f32 = top_left_rounded ? bounds.x + corner_radii.topLeft : bounds.x
top_right_x: f32 =
top_right_rounded ? bounds.x + bounds.width - corner_radii.topRight : bounds.x + bounds.width
bottom_left_x: f32 = bottom_left_rounded ? bounds.x + corner_radii.bottomLeft : bounds.x
bottom_right_x: f32 =
bottom_right_rounded ? bounds.x + bounds.width - corner_radii.bottomRight : bounds.x + bounds.width
// Vertical y-coordinates where the left/right side rectangles start/end. When the
// adjacent corner is rounded, inset by the corner radius. When not rounded, inset by the
// adjacent horizontal width — the horizontal side owns the corner area (extending through
// it to the bounds edge), so the vertical side starts below it to avoid overdraw of
// translucent colors.
top_left_y: f32 = top_left_rounded ? bounds.y + corner_radii.topLeft : bounds.y + width_top
top_right_y: f32 = top_right_rounded ? bounds.y + corner_radii.topRight : bounds.y + width_top
bottom_left_y: f32 =
bottom_left_rounded ? bounds.y + bounds.height - corner_radii.bottomLeft : bounds.y + bounds.height - width_bottom
bottom_right_y: f32 =
bottom_right_rounded ? bounds.y + bounds.height - corner_radii.bottomRight : bounds.y + bounds.height - width_bottom
// Side rectangles drawn INSIDE `bounds`. Sharp corners, solid fill, no outline. Each
// gated on its own width — skipping zero-width sides saves the primitive upload.
if border_width.top > 0 {
top_side := Rectangle {
x = top_left_x,
y = bounds.y,
width = top_right_x - top_left_x,
height = width_top,
}
rectangle(layer, top_side, border_color)
}
if border_width.bottom > 0 {
bottom_side := Rectangle {
x = bottom_left_x,
y = bounds.y + bounds.height - width_bottom,
width = bottom_right_x - bottom_left_x,
height = width_bottom,
}
rectangle(layer, bottom_side, border_color)
}
if border_width.left > 0 {
left_side := Rectangle {
x = bounds.x,
y = top_left_y,
width = width_left,
height = bottom_left_y - top_left_y,
}
rectangle(layer, left_side, border_color)
}
if border_width.right > 0 {
right_side := Rectangle {
x = bounds.x + bounds.width - width_right,
y = top_right_y,
width = width_right,
height = bottom_right_y - top_right_y,
}
rectangle(layer, right_side, border_color)
}
// Corner arcs (90° quadrants) drawn INSIDE bounds: outer radius matches Clay's
// `corner_radii`, inner radius is the outer radius minus the relevant border thickness
// (clamped to 0 for thick borders — produces a filled pie slice when border > radius,
// matching CSS). Angle convention matches ring(): 0° = +x (right), 90° = +y (down),
// 180° = -x (left), 270° = -y (up).
if top_left_rounded {
radius := corner_radii.topLeft
inner_radius := max(0, radius - width_top)
center := Vec2{bounds.x + radius, bounds.y + radius}
ring(layer, center, inner_radius, radius, border_color, start_angle = 180, end_angle = 270)
}
if top_right_rounded {
radius := corner_radii.topRight
inner_radius := max(0, radius - width_top)
center := Vec2{bounds.x + bounds.width - radius, bounds.y + radius}
ring(layer, center, inner_radius, radius, border_color, start_angle = 270, end_angle = 360)
}
if bottom_right_rounded {
radius := corner_radii.bottomRight
inner_radius := max(0, radius - width_bottom)
center := Vec2{bounds.x + bounds.width - radius, bounds.y + bounds.height - radius}
ring(layer, center, inner_radius, radius, border_color, start_angle = 0, end_angle = 90)
}
if bottom_left_rounded {
radius := corner_radii.bottomLeft
inner_radius := max(0, radius - width_bottom)
center := Vec2{bounds.x + radius, bounds.y + bounds.height - radius}
ring(layer, center, inner_radius, radius, border_color, start_angle = 90, end_angle = 180)
}
}
// Try to retroactively merge this Border into a pending Rectangle/Image candidate on the
// merge stack. Returns true on success so the caller can skip the standalone Border emission.
//
// Clay emits a parent element's bg and border bracketing all the children's commands, so a
// simple "is the next command a Border?" check (the previous approach) only catches leaf
// elements. The stack approach lets us pair them across arbitrary nesting: every Rectangle/
// Image push registers itself; every Border pops down until it finds a geometric match.
//
// Pop semantics: non-matching candidates above the match are discarded — their elements had
// no border anyway, so their primitives stay in `tmp_primitives` as plain Rectangles. A
// Border that finds no match at all falls back to standalone `clay_emit_partial_border`.
//
// Predicates that decline a candidate:
// - non-uniform or zero border widths (can't be a single uniform outline)
// - translucent border (the unmerged path's bg-under-border blending differs)
// - mismatched bounds or cornerRadius (the candidate isn't from the same element)
//
// False-match risk: two unrelated elements with bit-identical bounds and corner radii.
// Requires geometric coincidence (rare in practice), and even when it fires, the misattributed
// outline still lands at the correct screen position with the correct color — the pixels
// match the unmerged ground truth for opaque borders (the only kind we merge).
//INTERNAL
try_dispatch_clay_border_merge :: proc(bounds: Rectangle, border_data: clay.BorderRenderData) -> bool {
border_width := border_data.width
uniform_nonzero :=
border_width.left == border_width.top &&
border_width.top == border_width.right &&
border_width.right == border_width.bottom &&
border_width.top > 0
if !uniform_nonzero do return false
if border_data.color[3] < 255 do return false
for len(GLOB.clay_merge_open_stack) > 0 {
candidate := pop(&GLOB.clay_merge_open_stack)
if candidate.outer_bounds != bounds do continue
if candidate.corner_radii != border_data.cornerRadius do continue
apply_clay_border_merge_to_primitive(candidate, border_data)
return true
}
return false
}
// Mutates `tmp_primitives[candidate.primitive_index]` in place: shrinks the SDF shape by
// the uniform border width so the (outward) outline lands at the outer bounds, sets the
// outline flag and params, and — for `Fill_Texture` candidates — refits the texture's UV
// against `inner_bounds` so the image doesn't overflow into the border strip.
//
// The primitive's `bounds` field stays at the outer bounds: the rasterized quad already
// covers the area the outline now occupies. Skipping the bounds expansion that
// `apply_brush_and_outline` would normally do is intentional — expanding here would push the
// rasterized quad past Clay's outer edge.
//INTERNAL
apply_clay_border_merge_to_primitive :: proc(
candidate: Clay_Merge_Candidate,
border_data: clay.BorderRenderData,
) {
prim := &GLOB.tmp_primitives[candidate.primitive_index]
uniform_width := f32(border_data.width.top)
dpi_scale := GLOB.dpi_scaling
inner_half_width := candidate.outer_bounds.width * 0.5 - uniform_width
inner_half_height := candidate.outer_bounds.height * 0.5 - uniform_width
prim.params.rrect.half_size_ppx = {inner_half_width * dpi_scale, inner_half_height * dpi_scale}
prim.params.rrect.radii_ppx = {
max(0, candidate.corner_radii.topLeft - uniform_width) * dpi_scale,
max(0, candidate.corner_radii.topRight - uniform_width) * dpi_scale,
max(0, candidate.corner_radii.bottomRight - uniform_width) * dpi_scale,
max(0, candidate.corner_radii.bottomLeft - uniform_width) * dpi_scale,
}
// Set the outline bit in the packed flags field (low byte = Shape_Kind, bits 8+ = Shape_Flags).
prim.flags |= u32(transmute(u8)Shape_Flags{.Outline}) << 8
prim.effects.outline_color = Color(border_data.color)
prim.effects.outline_packed = pack_f16_pair(f16(uniform_width * dpi_scale), 0)
if candidate.kind == .Fill_Texture {
// The candidate was only pushed if its `fit_rect == outer_bounds` at emission time, so the
// image fills the rasterized quad. Refit UVs against `inner_bounds` so the image is scoped
// to the area inside the new outline rather than overflowing into the border strip.
inner_bounds := Rectangle {
x = candidate.outer_bounds.x + uniform_width,
y = candidate.outer_bounds.y + uniform_width,
width = candidate.outer_bounds.width - 2 * uniform_width,
height = candidate.outer_bounds.height - 2 * uniform_width,
}
uv_rect, _, _ := fit_params(candidate.image_data.fit, inner_bounds, candidate.image_data.texture_id)
prim.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Command dispatch ------------
// ---------------------------------------------------------------------------------------------------------------------
// Dispatch a single non-backdrop Clay render command to the appropriate `draw` primitive.
// Extracted from the main `prepare_clay_batch` walk so that the deferred-buffer flush path
// can replay commands accumulated during an open backdrop scope without duplicating the
// per-command lowering code.
//INTERNAL
dispatch_clay_command :: proc(
layer: ^Layer,
render_command: ^clay.RenderCommand,
custom_draw: Custom_Draw,
temp_allocator: runtime.Allocator,
) {
// Translate bounding box of the primitive by the layer position
bounds := Rectangle {
x = render_command.boundingBox.x + layer.bounds.x,
y = render_command.boundingBox.y + layer.bounds.y,
width = render_command.boundingBox.width,
height = render_command.boundingBox.height,
}
switch render_command.commandType {
case clay.RenderCommandType.None:
log.errorf(
"Received render command with type None. This generally means we're in some kind of fucked up state.",
)
case clay.RenderCommandType.Text:
render_data := render_command.renderData.text
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
c_text := strings.clone_to_cstring(txt, temp_allocator)
defer delete(c_text, temp_allocator)
// Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
sdl_text := cache_get_or_update(
Cache_Key{render_command.id, .Clay},
c_text,
get_font(render_data.fontId, render_data.fontSize),
)
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, Color(render_data.textColor)})
case clay.RenderCommandType.Image:
// Any texture
render_data := render_command.renderData.image
if render_data.imageData == nil do return
img_data := (^Clay_Image_Data)(render_data.imageData)^
corner_radii_clay := render_data.cornerRadius
radii := Rectangle_Radii {
top_left = corner_radii_clay.topLeft,
top_right = corner_radii_clay.topRight,
bottom_right = corner_radii_clay.bottomRight,
bottom_left = corner_radii_clay.bottomLeft,
}
background_color := Color(render_data.backgroundColor)
uv_rect, sampler, fit_rect := fit_params(img_data.fit, bounds, img_data.texture_id)
if background_color.a > 0 {
// Bg behind image. Push the bg primitive as the merge candidate so a matching Border
// turns into a bg+border-merged primitive plus a separate image draw on top.
rectangle(layer, bounds, background_color, radii = radii)
bg_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
rectangle(
layer,
fit_rect,
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
radii = radii,
)
append(
&GLOB.clay_merge_open_stack,
Clay_Merge_Candidate {
primitive_index = bg_primitive_index,
outer_bounds = bounds,
corner_radii = corner_radii_clay,
kind = .Fill_Color,
},
)
} else {
// No bg: the image itself can host the outline if its fit fully covers Clay's bounds.
// `Fit_Mode.Fit` with aspect mismatch returns a sub-rect, which can't host an outline
// (the rasterized quad wouldn't reach Clay's outer edge), so we skip pushing.
rectangle(
layer,
fit_rect,
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
radii = radii,
)
if fit_rect == bounds {
img_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
append(
&GLOB.clay_merge_open_stack,
Clay_Merge_Candidate {
primitive_index = img_primitive_index,
outer_bounds = bounds,
corner_radii = corner_radii_clay,
image_data = img_data,
kind = .Fill_Texture,
},
)
}
}
case clay.RenderCommandType.ScissorStart:
if bounds.width == 0 || bounds.height == 0 do return
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
if curr_scissor.sub_batch_len != 0 {
// Scissor has some content, need to make a new scissor
new := Scissor {
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
bounds = sdl.Rect {
c.int(bounds.x * GLOB.dpi_scaling),
c.int(bounds.y * GLOB.dpi_scaling),
c.int(bounds.width * GLOB.dpi_scaling),
c.int(bounds.height * GLOB.dpi_scaling),
},
}
append(&GLOB.scissors, new)
layer.scissor_len += 1
} else {
curr_scissor.bounds = sdl.Rect {
c.int(bounds.x * GLOB.dpi_scaling),
c.int(bounds.y * GLOB.dpi_scaling),
c.int(bounds.width * GLOB.dpi_scaling),
c.int(bounds.height * GLOB.dpi_scaling),
}
}
case clay.RenderCommandType.ScissorEnd:
case clay.RenderCommandType.OverlayColorStart, clay.RenderCommandType.OverlayColorEnd:
unimplemented("Clay overlays not supported yet...")
case clay.RenderCommandType.Rectangle:
render_data := render_command.renderData.rectangle
corner_radii_clay := render_data.cornerRadius
background_color := Color(render_data.backgroundColor)
radii := Rectangle_Radii {
top_left = corner_radii_clay.topLeft,
top_right = corner_radii_clay.topRight,
bottom_right = corner_radii_clay.bottomRight,
bottom_left = corner_radii_clay.bottomLeft,
}
rectangle(layer, bounds, background_color, radii = radii)
// Register this primitive as a merge candidate. If the element has a matching Border
// later in the stream (after its children's commands), `try_dispatch_clay_border_merge`
// will pop this candidate and mutate the primitive in-place to add the outline.
primitive_index := u32(len(GLOB.tmp_primitives) - 1)
append(
&GLOB.clay_merge_open_stack,
Clay_Merge_Candidate {
primitive_index = primitive_index,
outer_bounds = bounds,
corner_radii = corner_radii_clay,
kind = .Fill_Color,
},
)
case clay.RenderCommandType.Border:
render_data := render_command.renderData.border
if try_dispatch_clay_border_merge(bounds, render_data) do return
clay_emit_partial_border(
layer,
bounds,
Color(render_data.color),
render_data.width,
render_data.cornerRadius,
)
case clay.RenderCommandType.Custom:
// Copy the CustomRenderData by value so we can patch its `customData` field for the
// user callback without mutating Clay-owned memory. After unwrapping, the callback
// sees its own pointer in `render_data.customData`, identical to what it would see
// if `Clay_Custom` did not exist as an intermediary.
patched := render_command.renderData.custom
// Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil
// `customData` both surface to the callback as `customData = nil`.
patched.customData = nil
if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil {
switch custom_value in (^Clay_Custom)(custom_data_pointer)^ {
case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
// them here; reaching this branch means either the walker logic is broken or the
// `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check
// and this re-check (heap corruption / lifetime bug in user-managed customData
// memory). Both are renderer-level bugs that warrant a hard failure rather than a
// silently-dropped panel.
log.panicf(
"backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame",
render_command.renderData.custom.customData,
)
case rawptr: patched.customData = custom_value
}
}
if custom_draw != nil {
custom_draw(layer, bounds, patched)
} else if patched.customData != nil {
log.panicf(
"Received clay render command of type custom with non-nil user data but no custom_draw proc provided.",
)
}
}
}
// Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer.
// Caller guarantees:
// - a backdrop scope is open on `layer` so the underlying `append_or_extend_sub_batch`
// contract assertion is satisfied;
// - the command's `customData` points at a `Clay_Custom` whose active variant is
// `Backdrop_Marker` (the walker has already verified this via `is_clay_backdrop`).
//INTERNAL
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
bounds := Rectangle {
x = cmd.boundingBox.x + layer.bounds.x,
y = cmd.boundingBox.y + layer.bounds.y,
width = cmd.boundingBox.width,
height = cmd.boundingBox.height,
}
// Type-asserting form (no `, ok`): panics loudly if the variant tag changed since
// `is_clay_backdrop`, which is the desired tripwire for a heap-corruption bug in
// user-managed customData.
marker := (^Clay_Custom)(cmd.renderData.custom.customData).(Backdrop_Marker)
backdrop_blur(
layer,
bounds,
gaussian_sigma = marker.sigma,
tint = marker.tint,
radii = marker.radii,
feather_ppx = marker.feather_ppx,
)
}
// Close the in-flight backdrop scope (if open) and replay every command accumulated in the
// deferred index buffer. Ordering: end_backdrop first so deferred non-backdrop draws land
// at submission position relative to the bracket they followed (the bracket is now closed,
// so these draws render after it). Used at every zIndex transition and at end of stream.
//INTERNAL
flush_deferred_and_close_backdrop_scope :: proc(
layer: ^Layer,
batch: ^ClayBatch,
deferred_indices: ^[dynamic]i32,
backdrop_scope_open: ^bool,
custom_draw: Custom_Draw,
temp_allocator: runtime.Allocator,
) {
if backdrop_scope_open^ {
end_backdrop(layer)
backdrop_scope_open^ = false
}
// Clear the merge stack at scope/stratum boundaries: any pending candidates from the
// pre-scope (or pre-transition) commands stay as plain primitives — they can't merge
// with Borders on the far side of the boundary because that would change draw order.
clear(&GLOB.clay_merge_open_stack)
for index in deferred_indices^ {
cmd := clay.RenderCommandArray_Get(&batch.cmds, index)
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
}
clear(deferred_indices)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Main entry point ------------
// ---------------------------------------------------------------------------------------------------------------------
// Process Clay render commands into shape, text, and backdrop primitives.
//
// Single-walk dispatcher with a deferred buffer. The walk does three things per command:
// 1. zIndex transitions: close the in-flight scope, flush any deferred non-backdrop
// commands into the current layer, then open a new layer seeded with `base_layer.bounds`
// (NOT the bumping element's bounds — Clay's floating elements with `clipTo = .None`
// should not be over-clipped, and `clipTo = .AttachedParent` floating elements get a
// Clay-emitted ScissorStart immediately afterward that narrows correctly).
// 2. Backdrop commands: open a scope on first encounter (extending it on subsequent ones),
// then dispatch the backdrop_blur call.
// 3. Non-backdrop commands during an open scope: append to the deferred buffer for replay
// after the scope closes. The buffer holds command indices, not pointers, so it stays
// valid even if the underlying ClayArray reallocates.
// At end of stream, flush whatever remains.
prepare_clay_batch :: proc(
base_layer: ^Layer,
batch: ^ClayBatch,
custom_draw: Custom_Draw = nil,
temp_allocator := context.temp_allocator,
) {
layer := base_layer
command_count := int(batch.cmds.length)
deferred_indices := make([dynamic]i32, 0, 16, temp_allocator)
backdrop_scope_open := false
// Seed from GLOB.clay_z_index so multi-batch frames preserve the original semantics: a
// later call to `prepare_clay_batch` doesn't re-trigger layer splits for zIndex values
// the previous batch already saw.
previous_z_index := GLOB.clay_z_index
// Start with a clean merge stack. The stack is also cleared by
// `flush_deferred_and_close_backdrop_scope` at every stratum boundary; both clears together
// ensure merge candidates never pair across a boundary that would shift draw order.
clear(&GLOB.clay_merge_open_stack)
for i in 0 ..< command_count {
cmd := clay.RenderCommandArray_Get(&batch.cmds, i32(i))
// zIndex transition: close out current stratum, create new layer, continue.
if cmd.zIndex > previous_z_index {
log.debug("Higher zIndex found, creating new layer & setting z_index to", cmd.zIndex)
flush_deferred_and_close_backdrop_scope(
layer,
batch,
&deferred_indices,
&backdrop_scope_open,
custom_draw,
temp_allocator,
)
layer = new_layer(layer, base_layer.bounds)
previous_z_index = cmd.zIndex
// Keep GLOB.clay_z_index in sync for any external readers (debug tooling, etc.).
GLOB.clay_z_index = cmd.zIndex
}
if is_clay_backdrop(cmd) {
if !backdrop_scope_open {
begin_backdrop(layer)
backdrop_scope_open = true
}
dispatch_clay_backdrop(layer, cmd)
} else if backdrop_scope_open {
append(&deferred_indices, i32(i))
} else {
// Rectangle/Image dispatches push merge candidates; Border dispatches pop the stack
// to retroactively add an outline to a matching candidate. See
// `try_dispatch_clay_border_merge` for the matching semantics.
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
}
}
// End-of-stream: flush whatever remains.
flush_deferred_and_close_backdrop_scope(
layer,
batch,
&deferred_indices,
&backdrop_scope_open,
custom_draw,
temp_allocator,
)
}
+126 -138
View File
@@ -9,8 +9,11 @@ import sdl_ttf "vendor:sdl3/ttf"
//----- Vertex layout ---------------------------------- //----- Vertex layout ----------------------------------
// Vertex layout for tessellated and text geometry. `color` must be premultiplied alpha; see // Vertex layout for tessellated and text geometry.
// the package doc's "Color and blending" section for the contract. // IMPORTANT: `color` must be premultiplied alpha (RGB channels pre-scaled by alpha).
// The tessellated fragment shader passes vertex color through directly — it does NOT
// premultiply. The blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied-over).
// Use `premultiply_color` when constructing vertices manually for `prepare_shape`.
Vertex_2D :: struct { Vertex_2D :: struct {
position: Vec2, position: Vec2,
uv: [2]f32, uv: [2]f32,
@@ -65,34 +68,34 @@ Shape_Flags :: bit_set[Shape_Flag;u8]
//INTERNAL //INTERNAL
RRect_Params :: struct { RRect_Params :: struct {
half_size_ppx: [2]f32, half_size: [2]f32,
radii_ppx: [4]f32, radii: [4]f32,
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d) half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: f32, _: f32,
} }
//INTERNAL //INTERNAL
NGon_Params :: struct { NGon_Params :: struct {
radius_ppx: f32, radius: f32,
sides: f32, sides: f32,
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d) half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32, _: [5]f32,
} }
//INTERNAL //INTERNAL
Ellipse_Params :: struct { Ellipse_Params :: struct {
radii_ppx: [2]f32, radii: [2]f32,
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d) half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32, _: [5]f32,
} }
//INTERNAL //INTERNAL
Ring_Arc_Params :: struct { Ring_Arc_Params :: struct {
inner_radius_ppx: f32, // 0 for pie slice inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
outer_radius_ppx: f32, outer_radius: f32, // outer radius in physical pixels
normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start)) normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start))
normal_end: [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end)) normal_end: [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end))
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d) half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: f32, _: f32,
} }
@@ -173,7 +176,9 @@ Core_2D :: struct {
sampler: ^sdl.GPUSampler, sampler: ^sdl.GPUSampler,
} }
// PSO is hard-wired to single-sample (no MSAA — see package doc's "Anti-aliasing" section). // 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.
//INTERNAL //INTERNAL
create_core_2d :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> (core_2d: Core_2D, ok: bool) { create_core_2d :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> (core_2d: Core_2D, ok: bool) {
// On failure, clean up any partially-created resources // On failure, clean up any partially-created resources
@@ -459,31 +464,10 @@ destroy_core_2d :: proc(device: ^sdl.GPUDevice, core: ^Core_2D) {
//----- Vertex uniforms ---------------------------------- //----- Vertex uniforms ----------------------------------
//
// Coordinate-space contract for the main pipeline's vertex shader:
//
// Tessellated (0) — `v_position` arrives in *logical* pixels. The vertex
// shader multiplies by `dpi_scale` before applying the
// ortho projection (which is sized to physical pixels).
// SDF (1) — `v_position` is a unit-quad corner (0..1). World-space
// coordinates come from `Core_2D_Primitive.bounds` in
// logical pixels; the shader scales by `dpi_scale`.
// Text (2) — `v_position` arrives in *physical* pixels already.
// `prepare_text` and `prepare_text_transformed` bake the
// anchor + glyph offsets (from SDL_ttf's GPU text engine,
// which lays glyphs out in physical pixels) into the
// vertex stream and snap the anchor to integer physical
// pixels for atlas-aligned bilinear sampling. The shader
// therefore must NOT rescale these vertices.
//
// The two raw-vertex modes (Tessellated, Text) share `prepare_shape`-style
// glue but their coord spaces diverge — see `base_2d.vert` for the shader-
// side branch.
//INTERNAL //INTERNAL
Core_2D_Mode :: enum u32 { Core_2D_Mode :: enum u32 {
Tessellated = 0, Tessellated = 0,
SDF = 1, SDF = 1,
Text = 2,
} }
//INTERNAL //INTERNAL
@@ -538,9 +522,7 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
sdl.GPUBufferUsageFlags{.VERTEX}, sdl.GPUBufferUsageFlags{.VERTEX},
) )
// cycle=true: see backdrop.odin upload_backdrop_primitives. Persistent per-frame vert_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.vertex_buffer.transfer, false)
// streaming buffer; previous frame's blit is still in flight at map time.
vert_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.vertex_buffer.transfer, true)
if vert_array == nil { if vert_array == nil {
log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError()) log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError())
} }
@@ -571,8 +553,7 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
grow_buffer_if_needed(device, &GLOB.core_2d.index_buffer, index_size, sdl.GPUBufferUsageFlags{.INDEX}) grow_buffer_if_needed(device, &GLOB.core_2d.index_buffer, index_size, sdl.GPUBufferUsageFlags{.INDEX})
// cycle=true: see vertex_buffer above. idx_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.index_buffer.transfer, false)
idx_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.index_buffer.transfer, true)
if idx_array == nil { if idx_array == nil {
log.panicf("Failed to map index transfer buffer: %s", sdl.GetError()) log.panicf("Failed to map index transfer buffer: %s", sdl.GetError())
} }
@@ -599,8 +580,7 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
) )
// cycle=true: see vertex_buffer above. prim_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.primitive_buffer.transfer, false)
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.primitive_buffer.transfer, true)
if prim_array == nil { if prim_array == nil {
log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError()) log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError())
} }
@@ -618,15 +598,6 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
//----- Layer dispatch ---------------------------------- //----- Layer dispatch ----------------------------------
// Walk the layer's sub-batches, alternating between non-backdrop runs (rendered to
// `render_texture` via `render_layer_sub_batch_range`) and backdrop runs (each closed by a
// `begin_backdrop`/`end_backdrop` scope at submission time, dispatched here as one bracket
// per run via `run_backdrop_bracket`).
//
// Multiple brackets per layer are allowed: each `begin_backdrop`/`end_backdrop` pair maps to
// one contiguous .Backdrop run in the sub-batch list, and non-backdrop draws between scopes
// render in their submission position relative to the brackets. This is the explicit-scope
// model that replaces the legacy single-bracket-per-layer scheduler.
//INTERNAL //INTERNAL
draw_layer :: proc( draw_layer :: proc(
device: ^sdl.GPUDevice, device: ^sdl.GPUDevice,
@@ -657,18 +628,11 @@ draw_layer :: proc(
return return
} }
// Walk sub-batches, alternating non-backdrop runs (rendered to render_texture) and bracket_start_abs := find_first_backdrop_in_layer(layer)
// backdrop runs (dispatched to run_backdrop_bracket). Each backdrop run corresponds to a layer_end_abs := int(layer.sub_batch_start + layer.sub_batch_len)
// single user-visible begin_backdrop/end_backdrop scope at submission time.
layer_start := int(layer.sub_batch_start) if bracket_start_abs < 0 {
layer_end := layer_start + int(layer.sub_batch_len) // Fast path: no backdrop in this layer; render the whole sub-batch range in one pass.
i := layer_start
for i < layer_end {
// Find next non-backdrop run [run_start, run_end).
run_start := i
for i < layer_end && GLOB.tmp_sub_batches[i].kind != .Backdrop do i += 1
run_end := i
if run_end > run_start {
render_layer_sub_batch_range( render_layer_sub_batch_range(
cmd_buffer, cmd_buffer,
render_texture, render_texture,
@@ -676,25 +640,41 @@ draw_layer :: proc(
swapchain_height, swapchain_height,
clear_color, clear_color,
layer, layer,
run_start, int(layer.sub_batch_start),
run_end, layer_end_abs,
) )
return
} }
// Find next backdrop run [backdrop_scope_start, backdrop_scope_end). Each run = one bracket. // Bracketed layer: Pass A → backdrop bracket → Pass B.
backdrop_scope_start := i // See README.md § "Backdrop pipeline" for the full ordering semantics.
for i < layer_end && GLOB.tmp_sub_batches[i].kind == .Backdrop do i += 1 render_layer_sub_batch_range(
backdrop_scope_end := i
if backdrop_scope_end > backdrop_scope_start {
run_backdrop_bracket(
cmd_buffer, cmd_buffer,
u32(backdrop_scope_start), render_texture,
u32(backdrop_scope_end),
swapchain_width, swapchain_width,
swapchain_height, 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 // Render a sub-range of a layer's sub-batches in a single render pass. Iterates the layer's
@@ -702,10 +682,8 @@ draw_layer :: proc(
// and `range_end_abs` parameters are absolute indices into GLOB.tmp_sub_batches; only sub- // 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. // batches within `[range_start_abs, range_end_abs)` are drawn.
// //
// The caller (`draw_layer`) splits the layer into pure-kind runs before calling this proc, // .Backdrop sub-batches in the range are always silently skipped — they are dispatched by
// so the range MUST NOT contain any .Backdrop sub-batches; backdrop dispatch is handled by // run_backdrop_bracket, not here. The empty .Backdrop case in the inner switch enforces this.
// `run_backdrop_bracket`. The .Backdrop case in the inner switch is `unreachable()` to
// surface contract violations as fast as possible.
// //
// Render-pass setup mirrors the original draw_layer: clear-or-load based on GLOB.cleared, // 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 // pipeline + storage + index buffer bound up front, then per-batch state tracking. After this
@@ -802,7 +780,7 @@ render_layer_sub_batch_range :: proc(
for abs_idx in effective_start ..< effective_end { for abs_idx in effective_start ..< effective_end {
batch := &GLOB.tmp_sub_batches[abs_idx] batch := &GLOB.tmp_sub_batches[abs_idx]
#partial switch batch.kind { switch batch.kind {
case .Tessellated: case .Tessellated:
if current_mode != .Tessellated { if current_mode != .Tessellated {
push_globals(cmd_buffer, width, height, .Tessellated) push_globals(cmd_buffer, width, height, .Tessellated)
@@ -834,12 +812,9 @@ render_layer_sub_batch_range :: proc(
sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0) sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)
case .Text: case .Text:
// Text vertices live in physical-pixel space (see Core_2D_Mode.Text if current_mode != .Tessellated {
// docs); mode 2 makes the shader skip the `* dpi_scale` step that push_globals(cmd_buffer, width, height, .Tessellated)
// the Tessellated path applies to logical-pixel input. current_mode = .Tessellated
if current_mode != .Text {
push_globals(cmd_buffer, width, height, .Text)
current_mode = .Text
} }
if current_vert_buf != main_vert_buf { if current_vert_buf != main_vert_buf {
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1) sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
@@ -894,7 +869,10 @@ render_layer_sub_batch_range :: proc(
} }
sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset) sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset)
case: log.panicf("Non core2d batch kind (%v) reached in core2d dispatch path.", batch.kind) 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.
} }
} }
} }
@@ -916,11 +894,21 @@ prepare_shape :: proc(layer: ^Layer, vertices: []Vertex_2D) {
append_or_extend_sub_batch(scissor, layer, .Tessellated, offset, u32(len(vertices))) append_or_extend_sub_batch(scissor, layer, .Tessellated, offset, u32(len(vertices)))
} }
// Submit an SDF primitive to the given layer for rendering. Requires the caller to build a
// Core_2D_Primitive directly, which is the internal GPU-layout struct.
//INTERNAL
prepare_sdf_primitive :: proc(layer: ^Layer, prim: Core_2D_Primitive) {
offset := u32(len(GLOB.tmp_primitives))
append(&GLOB.tmp_primitives, prim)
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
append_or_extend_sub_batch(scissor, layer, .SDF, offset, 1)
}
// Submit an SDF primitive with optional texture binding. // Submit an SDF primitive with optional texture binding.
// The texture-aware counterpart of `prepare_sdf_primitive`; lets shape procs route a // The texture-aware counterpart of `prepare_sdf_primitive`; lets shape procs route a
// texture_id and sampler into the sub-batch without growing the public API. // texture_id and sampler into the sub-batch without growing the public API.
//INTERNAL //INTERNAL
prepare_sdf_primitive :: proc( prepare_sdf_primitive_ex :: proc(
layer: ^Layer, layer: ^Layer,
prim: Core_2D_Primitive, prim: Core_2D_Primitive,
texture_id: Texture_Id = INVALID_TEXTURE, texture_id: Texture_Id = INVALID_TEXTURE,
@@ -945,8 +933,8 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
// Snap base position to integer physical pixels to avoid atlas sub-pixel // Snap base position to integer physical pixels to avoid atlas sub-pixel
// sampling blur (and the off-by-one bottom-row clip that comes with it). // sampling blur (and the off-by-one bottom-row clip that comes with it).
base_x_ppx := math.round(text.position[0] * GLOB.dpi_scaling) base_x := math.round(text.position[0] * GLOB.dpi_scaling)
base_y_ppx := math.round(text.position[1] * GLOB.dpi_scaling) base_y := math.round(text.position[1] * GLOB.dpi_scaling)
// Premultiply text color once — reused across all glyph vertices. // Premultiply text color once — reused across all glyph vertices.
pm_color := premultiply_color(text.color) pm_color := premultiply_color(text.color)
@@ -961,7 +949,7 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
uv := data.uv[i] uv := data.uv[i]
append( append(
&GLOB.tmp_text_verts, &GLOB.tmp_text_verts,
Vertex_2D{position = {pos.x + base_x_ppx, -pos.y + base_y_ppx}, uv = {uv.x, uv.y}, color = pm_color}, Vertex_2D{position = {pos.x + base_x, -pos.y + base_y}, uv = {uv.x, uv.y}, color = pm_color},
) )
} }
@@ -1102,7 +1090,7 @@ build_rrect_primitive :: proc(
radii: Rectangle_Radii, radii: Rectangle_Radii,
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_ppx: f32, feather_px: f32,
) -> Core_2D_Primitive { ) -> Core_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)
@@ -1110,8 +1098,8 @@ build_rrect_primitive :: proc(
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius) clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius) clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
half_feather_ppx := feather_ppx * 0.5 half_feather := feather_px * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
half_width := rect.width * 0.5 half_width := rect.width * 0.5
@@ -1149,14 +1137,14 @@ build_rrect_primitive :: proc(
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0, rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
} }
prim.params.rrect = RRect_Params { prim.params.rrect = RRect_Params {
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale}, half_size = {half_width * dpi_scale, half_height * dpi_scale},
radii_ppx = { radii = {
clamped_bottom_right * dpi_scale, clamped_bottom_right * dpi_scale,
clamped_top_right * dpi_scale, clamped_top_right * dpi_scale,
clamped_bottom_left * dpi_scale, clamped_bottom_left * dpi_scale,
clamped_top_left * dpi_scale, clamped_top_left * dpi_scale,
}, },
half_feather_ppx = half_feather_ppx, half_feather = half_feather,
} }
return prim return prim
} }
@@ -1169,10 +1157,10 @@ build_circle_primitive :: proc(
radius: f32, radius: f32,
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_ppx: f32, feather_px: f32,
) -> Core_2D_Primitive { ) -> Core_2D_Primitive {
half_feather_ppx := feather_ppx * 0.5 half_feather := feather_px * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
actual_center := center actual_center := center
@@ -1189,11 +1177,11 @@ build_circle_primitive :: proc(
actual_center.y + radius + padding, actual_center.y + radius + padding,
}, },
} }
radius_ppx := radius * dpi_scale scaled_radius := radius * dpi_scale
prim.params.rrect = RRect_Params { prim.params.rrect = RRect_Params {
half_size_ppx = {radius_ppx, radius_ppx}, half_size = {scaled_radius, scaled_radius},
radii_ppx = {radius_ppx, radius_ppx, radius_ppx, radius_ppx}, radii = {scaled_radius, scaled_radius, scaled_radius, scaled_radius},
half_feather_ppx = half_feather_ppx, half_feather = half_feather,
} }
return prim return prim
} }
@@ -1206,10 +1194,10 @@ build_ellipse_primitive :: proc(
radius_horizontal, radius_vertical: f32, radius_horizontal, radius_vertical: f32,
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_ppx: f32, feather_px: f32,
) -> Core_2D_Primitive { ) -> Core_2D_Primitive {
half_feather_ppx := feather_ppx * 0.5 half_feather := feather_px * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
actual_center := center actual_center := center
@@ -1241,8 +1229,8 @@ build_ellipse_primitive :: proc(
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0, rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
} }
prim.params.ellipse = Ellipse_Params { prim.params.ellipse = Ellipse_Params {
radii_ppx = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale}, radii = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
half_feather_ppx = half_feather_ppx, half_feather = half_feather,
} }
return prim return prim
} }
@@ -1256,10 +1244,10 @@ build_polygon_primitive :: proc(
radius: f32, radius: f32,
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_ppx: f32, feather_px: f32,
) -> Core_2D_Primitive { ) -> Core_2D_Primitive {
half_feather_ppx := feather_ppx * 0.5 half_feather := feather_px * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
actual_center := center actual_center := center
@@ -1281,9 +1269,9 @@ build_polygon_primitive :: proc(
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0, rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
} }
prim.params.ngon = NGon_Params { prim.params.ngon = NGon_Params {
radius_ppx = radius * math.cos(math.PI / f32(sides)) * dpi_scale, radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
sides = f32(sides), sides = f32(sides),
half_feather_ppx = half_feather_ppx, half_feather = half_feather,
} }
return prim return prim
} }
@@ -1301,13 +1289,13 @@ build_ring_arc_primitive :: proc(
end_angle: f32, end_angle: f32,
origin: Vec2, origin: Vec2,
rotation: f32, rotation: f32,
feather_ppx: f32, feather_px: f32,
) -> ( ) -> (
Core_2D_Primitive, Core_2D_Primitive,
Shape_Flags, Shape_Flags,
) { ) {
half_feather_ppx := feather_ppx * 0.5 half_feather := feather_px * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
actual_center := center actual_center := center
@@ -1350,11 +1338,11 @@ build_ring_arc_primitive :: proc(
}, },
} }
prim.params.ring_arc = Ring_Arc_Params { prim.params.ring_arc = Ring_Arc_Params {
inner_radius_ppx = inner_radius * dpi_scale, inner_radius = inner_radius * dpi_scale,
outer_radius_ppx = outer_radius * dpi_scale, outer_radius = outer_radius * dpi_scale,
normal_start = normal_start, normal_start = normal_start,
normal_end = normal_end, normal_end = normal_end,
half_feather_ppx = half_feather_ppx, half_feather = half_feather,
} }
return prim, arc_flags return prim, arc_flags
} }
@@ -1421,7 +1409,7 @@ apply_brush_and_outline :: proc(
} }
prim.flags = pack_kind_flags(kind, flags) prim.flags = pack_kind_flags(kind, flags)
prepare_sdf_primitive(layer, prim^, texture_id, sampler) prepare_sdf_primitive_ex(layer, prim^, texture_id, sampler)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -1445,9 +1433,9 @@ rectangle :: proc(
radii: Rectangle_Radii = {}, radii: Rectangle_Radii = {},
origin: Vec2 = {}, origin: Vec2 = {},
rotation: f32 = 0, rotation: f32 = 0,
feather_ppx: f32 = DFT_FEATHER_PPX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_ppx) prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width) apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
} }
@@ -1468,9 +1456,9 @@ circle :: proc(
outline_width: f32 = 0, outline_width: f32 = 0,
origin: Vec2 = {}, origin: Vec2 = {},
rotation: f32 = 0, rotation: f32 = 0,
feather_ppx: f32 = DFT_FEATHER_PPX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
prim := build_circle_primitive(center, radius, origin, rotation, feather_ppx) prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width) apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
} }
@@ -1485,9 +1473,9 @@ ellipse :: proc(
outline_width: f32 = 0, outline_width: f32 = 0,
origin: Vec2 = {}, origin: Vec2 = {},
rotation: f32 = 0, rotation: f32 = 0,
feather_ppx: f32 = DFT_FEATHER_PPX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_ppx) prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px)
apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width) apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width)
} }
@@ -1504,11 +1492,11 @@ polygon :: proc(
outline_width: f32 = 0, outline_width: f32 = 0,
origin: Vec2 = {}, origin: Vec2 = {},
rotation: f32 = 0, rotation: f32 = 0,
feather_ppx: f32 = DFT_FEATHER_PPX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
if sides < 3 do return if sides < 3 do return
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_ppx) prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width) apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width)
} }
@@ -1527,7 +1515,7 @@ ring :: proc(
end_angle: f32 = DFT_CIRC_END_ANGLE, end_angle: f32 = DFT_CIRC_END_ANGLE,
origin: Vec2 = {}, origin: Vec2 = {},
rotation: f32 = 0, rotation: f32 = 0,
feather_ppx: f32 = DFT_FEATHER_PPX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
prim, arc_flags := build_ring_arc_primitive( prim, arc_flags := build_ring_arc_primitive(
center, center,
@@ -1537,7 +1525,7 @@ ring :: proc(
end_angle, end_angle,
origin, origin,
rotation, rotation,
feather_ppx, feather_px,
) )
apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags) apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
} }
@@ -1551,7 +1539,7 @@ line :: proc(
thickness: f32 = DFT_STROKE_THICKNESS, thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
feather_ppx: f32 = DFT_FEATHER_PPX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
delta_x := end_position.x - start_position.x delta_x := end_position.x - start_position.x
delta_y := end_position.y - start_position.y delta_y := end_position.y - start_position.y
@@ -1567,8 +1555,8 @@ line :: proc(
half_thickness := thickness * 0.5 half_thickness := thickness * 0.5
cap_radius := half_thickness cap_radius := half_thickness
half_feather_ppx := feather_ppx * 0.5 half_feather := feather_px * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling
// Expand bounds for rotation // Expand bounds for rotation
@@ -1584,14 +1572,14 @@ line :: proc(
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 {
half_size_ppx = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale}, half_size = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
radii_ppx = { radii = {
cap_radius * dpi_scale, cap_radius * dpi_scale,
cap_radius * dpi_scale, cap_radius * dpi_scale,
cap_radius * dpi_scale, cap_radius * dpi_scale,
cap_radius * dpi_scale, cap_radius * dpi_scale,
}, },
half_feather_ppx = half_feather_ppx, half_feather = half_feather,
} }
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width) apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
} }
@@ -1604,10 +1592,10 @@ line_strip :: proc(
thickness: f32 = DFT_STROKE_THICKNESS, thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {}, outline_color: Color = {},
outline_width: f32 = 0, outline_width: f32 = 0,
feather_ppx: f32 = DFT_FEATHER_PPX, feather_px: f32 = DFT_FEATHER_PX,
) { ) {
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], brush, thickness, outline_color, outline_width, feather_ppx) line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_px)
} }
} }
+4 -4
View File
@@ -747,10 +747,10 @@ PRE_PAD_X :: SPACE_PANEL // 24
// ============================================================ // ============================================================
// SCANLINE OVERLAY (opt-in, terminal surfaces only) // SCANLINE OVERLAY (opt-in, terminal surfaces only)
// Repeating-stripe pattern at very low opacity. Stripe is 2 logical // Repeating-stripe pattern at very low opacity. Stripe is 2px
// pixels transparent + 2 logical pixels black-at-3% (TINT_SCANLINE). // transparent + 2px black-at-3% (TINT_SCANLINE).
// ============================================================ // ============================================================
SCANLINE_STRIPE_LPX :: 2 SCANLINE_STRIPE_PX :: 2
SCANLINE_GAP_LPX :: 2 SCANLINE_GAP_PX :: 2
SCANLINE_COLOR :: TINT_SCANLINE SCANLINE_COLOR :: TINT_SCANLINE
+270 -166
View File
@@ -1,75 +1,15 @@
// Rendering library built on SDL3 GPU.
//
// ----- Coordinate system -----
// Origin is the top-left corner of the window/layer. X increases rightward, Y increases
// downward. This matches SDL, HTML Canvas, and most 2D UI coordinate conventions. All
// public position parameters (`center`, `origin`, `start_position`, `end_position`, every
// `Vec2`-typed field, every `Rectangle.x/y`, etc.) live in this coordinate system.
//
// ----- Unit-suffix convention -----
// Public CPU-side dimensions are in *logical* pixels by default (CSS-style: a value of 200
// looks the same physical size on a 1× monitor and a 2× Retina display). Suffix rules:
//
// no suffix — logical pixels. Default for layout values (positions, sizes, radii,
// outline widths, line thicknesses, gradient endpoints, etc.).
// `_lpx` — logical pixels, *explicit*. Optional. Use when an identifier would
// otherwise be ambiguous about which kind of pixel it carries —
// typically standalone constants like `SCANLINE_STRIPE_LPX` where the
// context doesn't make the unit obvious from the surrounding code.
// Procedure parameters and struct fields named after a layout property
// (`width`, `radius`, ...) don't need this suffix.
// `_ppx` — physical (device) pixels. Required whenever a value is in physical
// pixels, regardless of context. Reserved for quantities whose
// right-feeling magnitude is a property of the device pixel grid rather
// than of the layout: anti-aliasing band widths, sub-pixel snap targets,
// MSDF screen-pixel-range parameters.
//
// Examples:
//
// width, height, radius, outline_width, thickness — logical px (no suffix)
// SCANLINE_STRIPE_LPX, SCANLINE_GAP_LPX — logical px (explicit `_lpx`)
// feather_ppx, aa_ppx — physical px (`_ppx`)
//
// Layout values scale with DPI; rasterization-grid values do not. The shader handles the
// logical-to-physical conversion at the rasterization boundary; CPU-side `_ppx` inputs that
// need to interact with logical-space data convert via `/ dpi_scaling` at the use site.
//
// ----- Anti-aliasing -----
// MSAA is intentionally NOT supported. SDF text and shapes compute fragment coverage
// analytically via `smoothstep`, so they don't benefit from multisampling. Tessellated
// user geometry submitted via `prepare_shape` is rendered without anti-aliasing — if AA is
// required for tessellated content, the caller must either render it to their own offscreen
// target and submit the result as a texture, or use the AA helpers in the `tess` subpackage
// (e.g. `tess.triangle_aa` extrudes 1-physical-pixel alpha-falloff edge bands). This
// decision aligns with the SBC target (Mali Valhall, where MSAA's per-tile bandwidth
// multiplier is expensive) and matches RAD Debugger's architecture.
//
// ----- Color and blending -----
// `Color` is RGBA8 in memory order (R, G, B, A at indices 0..3). The shader unpacks via
// `unpackUnorm4x8`, which reads bytes in that exact order. Alpha 255 = fully opaque, 0 =
// fully transparent.
//
// All rendering uses *premultiplied-over* blending (blend state ONE, ONE_MINUS_SRC_ALPHA —
// the standard mode used by Skia, Flutter, and GPUI). Three implications:
//
// - Public shape procs (`rectangle`, `circle`, `line`, etc.) accept straight-alpha
// `Color` values and the SDF fragment shaders premultiply internally; users of these
// procs don't need to think about premultiplication.
// - Vertex colors written to the shared vertex stream (the tessellated path — text and
// anything submitted via `prepare_shape`, including `tess.*` helpers) MUST be
// premultiplied at the CPU. The tessellated fragment shader passes vertex color through
// directly without further modification. The `premultiply_color` helper handles this.
// - The clear color passed to `end()` is also premultiplied internally before being
// handed to the GPU; callers pass straight-alpha `Color` here too.
package draw package draw
import "base:runtime" 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 sdl "vendor:sdl3" import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf" import sdl_ttf "vendor:sdl3/ttf"
import clay "../vendor/clay"
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Shader format ------------ // ----- Shader format ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -111,7 +51,7 @@ INITIAL_SCISSOR_SIZE :: 10
// ----- 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.
DFT_FEATHER_PPX :: 1 // Total AA feather width in physical pixels (half on each side of boundary). DFT_FEATHER_PX :: 1 // Total AA feather width in physical pixels (half on each side of boundary).
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).
@@ -148,10 +88,6 @@ Global :: struct {
clay_z_index: i16, // Tracks z-index for layer splitting during Clay batch processing. clay_z_index: i16, // Tracks z-index for layer splitting during Clay batch processing.
cleared: bool, // Whether the render target has been cleared this frame. cleared: bool, // Whether the render target has been cleared this frame.
// Per-frame: which layer (if any) currently has an open begin_backdrop scope.
// Reset to nil at frame start. end() panics if non-nil at frame end.
open_backdrop_layer: ^Layer,
// -- Subsystems (accessed every draw_layer call) -- // -- Subsystems (accessed every draw_layer call) --
core_2d: Core_2D, // The unified 2D GPU pipeline (shaders, buffers, samplers). core_2d: Core_2D, // The unified 2D GPU pipeline (shaders, buffers, samplers).
backdrop: Backdrop, // Frosted-glass backdrop blur subsystem (downsample + blur PSOs, working textures). backdrop: Backdrop, // Frosted-glass backdrop blur subsystem (downsample + blur PSOs, working textures).
@@ -168,7 +104,6 @@ Global :: struct {
// -- 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.
clay_merge_open_stack: [dynamic]Clay_Merge_Candidate, // Pending Rectangle/Image primitives waiting for a matching Border to merge with.
// -- Text (occasional — font registration and text cache lookups) -- // -- Text (occasional — font registration and text cache lookups) --
text_cache: Text_Cache, // Font registry, SDL_ttf engine, and cached TTF_Text objects. text_cache: Text_Cache, // Font registry, SDL_ttf engine, and cached TTF_Text objects.
@@ -193,13 +128,27 @@ Global :: struct {
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// A 2D position in world space. Non-distinct alias for [2]f32 — bare literals like {100, 200} // A 2D position in world space. Non-distinct alias for [2]f32 — bare literals like {100, 200}
// work at non-ambiguous call sites. See the package doc for coordinate-system and unit // work at non-ambiguous call sites.
// conventions. //
// Coordinate system: origin is the top-left corner of the window/layer. X increases rightward,
// Y increases downward. This matches SDL, HTML Canvas, and most 2D UI coordinate conventions.
// All position parameters in the draw API (center, origin, start_position, end_position, etc.)
// use this coordinate system.
//
// Units are logical pixels (pre-DPI-scaling). The renderer multiplies by dpi_scaling internally
// before uploading to the GPU. A Vec2{100, 50} refers to the same visual location regardless of
// display DPI.
Vec2 :: [2]f32 Vec2 :: [2]f32
// An RGBA color with 8 bits per channel. Distinct type over [4]u8 so that proc-group // An RGBA color with 8 bits per channel. Distinct type over [4]u8 so that proc-group
// overloads can disambiguate Color from other 4-byte structs. See the package doc for the // overloads can disambiguate Color from other 4-byte structs.
// memory layout and the premultiplied-over blending contract. //
// Channel order: R, G, B, A (indices 0, 1, 2, 3). Alpha 255 is fully opaque, 0 is fully
// 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].
//
// When used in the Core_2D_Primitive or Gaussian_Blur_Primitive structs (e.g. .color), the 4 bytes
// 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}
@@ -264,15 +213,21 @@ Brush :: union {
Texture_Fill, Texture_Fill,
} }
// Convert clay.Color ([4]c.float in 0255 range) to Color.
color_from_clay :: #force_inline proc(clay_color: clay.Color) -> Color {
return Color{u8(clay_color[0]), u8(clay_color[1]), u8(clay_color[2]), u8(clay_color[3])}
}
// Convert Color to [4]f32 in 0.01.0 range. Useful for SDL interop (e.g. clear color). // Convert Color to [4]f32 in 0.01.0 range. Useful for SDL interop (e.g. clear color).
color_to_f32 :: proc(color: Color) -> [4]f32 { color_to_f32 :: proc(color: Color) -> [4]f32 {
INV :: 1.0 / 255.0 INV :: 1.0 / 255.0
return {f32(color[0]) * INV, f32(color[1]) * INV, f32(color[2]) * INV, f32(color[3]) * INV} return {f32(color[0]) * INV, f32(color[1]) * INV, f32(color[2]) * INV, f32(color[3]) * INV}
} }
// Pre-multiply RGB channels by alpha. Required for any vertex written to the tessellated // Pre-multiply RGB channels by alpha. The tessellated vertex path and text path require
// vertex stream (text path or `prepare_shape`-style submissions); see the package doc's // premultiplied colors because the blend state is ONE, ONE_MINUS_SRC_ALPHA and the
// "Color and blending" section for the full contract. // tessellated fragment shader passes vertex color through without further modification.
// Users who construct Vertex_2D structs manually for prepare_shape must premultiply their colors.
premultiply_color :: #force_inline proc(color: Color) -> Color { premultiply_color :: #force_inline proc(color: Color) -> Color {
a := u32(color[3]) a := u32(color[3])
return Color { return Color {
@@ -290,7 +245,7 @@ premultiply_color :: #force_inline proc(color: Color) -> Color {
//INTERNAL //INTERNAL
Sub_Batch_Kind :: enum u8 { Sub_Batch_Kind :: enum u8 {
Tessellated, // non-indexed, white texture or user texture, Core_2D_Mode.Tessellated Tessellated, // non-indexed, white texture or user texture, Core_2D_Mode.Tessellated
Text, // indexed, atlas texture, Core_2D_Mode.Text (vertices already in physical-pixel space) Text, // indexed, atlas texture, Core_2D_Mode.Tessellated
SDF, // instanced unit quad, Core_2D_Mode.SDF SDF, // instanced unit quad, Core_2D_Mode.SDF
// instanced unit quad, backdrop subsystem V-composite (indexes Gaussian_Blur_Primitive). // instanced unit quad, backdrop subsystem V-composite (indexes Gaussian_Blur_Primitive).
// Bracket-scheduled per layer; see README.md § "Backdrop pipeline" for ordering semantics. // Bracket-scheduled per layer; see README.md § "Backdrop pipeline" for ordering semantics.
@@ -330,6 +285,12 @@ Scissor :: struct {
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// 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,
@@ -339,6 +300,8 @@ init :: proc(
) -> ( ) -> (
ok: bool, ok: bool,
) { ) {
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
core, core_ok := create_core_2d(device, window) core, core_ok := create_core_2d(device, window)
if !core_ok { if !core_ok {
return false return false
@@ -385,6 +348,7 @@ init :: proc(
pending_text_releases = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator), pending_text_releases = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
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),
core_2d = core, core_2d = core,
backdrop = backdrop, backdrop = backdrop,
text_cache = text_cache, text_cache = text_cache,
@@ -393,7 +357,12 @@ init :: proc(
// Reserve slot 0 for INVALID_TEXTURE // Reserve slot 0 for INVALID_TEXTURE
append(&GLOB.texture_slots, Texture_Slot{}) append(&GLOB.texture_slots, Texture_Slot{})
log.debug("Window DPI scaling:", GLOB.dpi_scaling) log.debug("Window DPI scaling:", GLOB.dpi_scaling)
init_clay(window, allocator) arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
window_width, window_height: c.int
sdl.GetWindowSize(window, &window_width, &window_height)
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
clay.SetMeasureTextFunction(measure_text_clay, nil)
return true return true
} }
@@ -432,7 +401,7 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
delete(GLOB.tmp_gaussian_blur_primitives) delete(GLOB.tmp_gaussian_blur_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)
destroy_clay(allocator) free(GLOB.clay_memory, allocator)
process_pending_texture_releases() process_pending_texture_releases()
destroy_all_textures() destroy_all_textures()
destroy_sampler_pool() destroy_sampler_pool()
@@ -452,8 +421,8 @@ clear_global :: proc() {
clear(&GLOB.pending_text_releases) clear(&GLOB.pending_text_releases)
GLOB.curr_layer_index = 0 GLOB.curr_layer_index = 0
GLOB.clay_z_index = 0
GLOB.cleared = false GLOB.cleared = false
GLOB.open_backdrop_layer = nil
// Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data) // Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data)
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)
clear(&GLOB.tmp_uncached_text) clear(&GLOB.tmp_uncached_text)
@@ -466,42 +435,12 @@ clear_global :: proc() {
clear(&GLOB.tmp_primitives) clear(&GLOB.tmp_primitives)
clear(&GLOB.tmp_sub_batches) clear(&GLOB.tmp_sub_batches)
clear(&GLOB.tmp_gaussian_blur_primitives) clear(&GLOB.tmp_gaussian_blur_primitives)
clear_clay_per_frame()
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Frame ------------ // ----- Frame ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// Creates a new layer
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
if GLOB.open_backdrop_layer != nil {
log.panicf("new_layer called while backdrop scope is open on layer %p", GLOB.open_backdrop_layer)
}
layer := Layer {
bounds = bounds,
sub_batch_start = prev_layer.sub_batch_start + prev_layer.sub_batch_len,
scissor_start = prev_layer.scissor_start + prev_layer.scissor_len,
scissor_len = 1,
}
append(&GLOB.layers, layer)
GLOB.curr_layer_index += 1
log.debug("Added new layer; curr index", GLOB.curr_layer_index)
scissor := Scissor {
sub_batch_start = u32(len(GLOB.tmp_sub_batches)),
bounds = sdl.Rect {
x = i32(bounds.x * GLOB.dpi_scaling),
y = i32(bounds.y * GLOB.dpi_scaling),
w = i32(bounds.width * GLOB.dpi_scaling),
h = i32(bounds.height * GLOB.dpi_scaling),
},
}
append(&GLOB.scissors, scissor)
return &GLOB.layers[GLOB.curr_layer_index]
}
// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for. // Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for.
begin :: proc(bounds: Rectangle) -> ^Layer { begin :: proc(bounds: Rectangle) -> ^Layer {
// Cleanup // Cleanup
@@ -526,6 +465,31 @@ begin :: proc(bounds: Rectangle) -> ^Layer {
return &GLOB.layers[GLOB.curr_layer_index] return &GLOB.layers[GLOB.curr_layer_index]
} }
// Creates a new layer
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
layer := Layer {
bounds = bounds,
sub_batch_start = prev_layer.sub_batch_start + prev_layer.sub_batch_len,
scissor_start = prev_layer.scissor_start + prev_layer.scissor_len,
scissor_len = 1,
}
append(&GLOB.layers, layer)
GLOB.curr_layer_index += 1
log.debug("Added new layer; curr index", GLOB.curr_layer_index)
scissor := Scissor {
sub_batch_start = u32(len(GLOB.tmp_sub_batches)),
bounds = sdl.Rect {
x = i32(bounds.x * GLOB.dpi_scaling),
y = i32(bounds.y * GLOB.dpi_scaling),
w = i32(bounds.width * GLOB.dpi_scaling),
h = i32(bounds.height * GLOB.dpi_scaling),
},
}
append(&GLOB.scissors, scissor)
return &GLOB.layers[GLOB.curr_layer_index]
}
// Render primitives. clear_color is the background fill before any layers are drawn. // Render primitives. clear_color is the background fill before any layers are drawn.
end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DFT_CLEAR_COLOR) { end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DFT_CLEAR_COLOR) {
cmd_buffer := sdl.AcquireGPUCommandBuffer(device) cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
@@ -533,13 +497,6 @@ 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())
} }
if GLOB.open_backdrop_layer != nil {
log.panicf(
"end() called with open backdrop scope on layer %p; missing end_backdrop",
GLOB.open_backdrop_layer,
)
}
// Pre-scan: if any layer this frame has a backdrop sub-batch, route the entire frame to // Pre-scan: if any layer this frame has a backdrop sub-batch, route the entire frame to
// source_texture so the bracket can sample the pre-bracket framebuffer without a mid- // source_texture 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 // frame texture copy. Frames without any backdrop hit the existing fast path and never
@@ -612,46 +569,6 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
} }
} }
// Open a backdrop scope on `layer`. All subsequent draws on `layer` until the matching
// `end_backdrop` must be backdrop primitives (currently only `backdrop_blur`). Non-backdrop
// draws inside a scope, or backdrop draws outside one, panic.
//
// Bracket scheduling: each scope produces one bracket at render time. Within the scope,
// per-sigma sub-batch coalescing still applies (two contiguous backdrop_blur calls with
// the same sigma share an instanced composite draw and a single H+V blur pass pair).
//
// Multiple begin/end pairs per layer are allowed: each pair is its own bracket, and
// non-backdrop draws between pairs render in their submission position relative to the
// brackets. Use this for layered frost effects.
begin_backdrop :: proc(layer: ^Layer) {
if GLOB.open_backdrop_layer != nil {
log.panicf("begin_backdrop called while a scope is already open on layer %p", GLOB.open_backdrop_layer)
}
GLOB.open_backdrop_layer = layer
}
// Close the backdrop scope opened by `begin_backdrop`. Must be called on the same layer that
// the scope was opened on; the layer pointer mismatch is a hard error rather than a silent
// recovery to surface integration bugs early.
end_backdrop :: proc(layer: ^Layer) {
if GLOB.open_backdrop_layer != layer {
log.panicf("end_backdrop on wrong layer (open=%p, ended=%p)", GLOB.open_backdrop_layer, layer)
}
GLOB.open_backdrop_layer = nil
}
// Convenience wrapper for the common case of a backdrop scope tied to a block. Use with
// defer-style block scoping:
//
// {
// draw.backdrop_scope(layer)
// draw.backdrop_blur(layer, ...)
// } // end_backdrop fires automatically
@(deferred_in = end_backdrop)
backdrop_scope :: #force_inline proc(layer: ^Layer) {
begin_backdrop(layer)
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Sub-batch dispatch ------------ // ----- Sub-batch dispatch ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -674,15 +591,6 @@ append_or_extend_sub_batch :: proc(
sampler: Sampler_Preset = DFT_SAMPLER, sampler: Sampler_Preset = DFT_SAMPLER,
gaussian_sigma: f32 = 0, gaussian_sigma: f32 = 0,
) { ) {
// Scope contract: backdrops only inside a scope, non-backdrops only outside.
in_scope := GLOB.open_backdrop_layer == layer
if kind == .Backdrop && !in_scope {
log.panic("backdrop draw outside begin_backdrop / end_backdrop scope")
}
if kind != .Backdrop && in_scope {
log.panicf("non-backdrop draw of kind %v inside backdrop scope on layer %p", kind, layer)
}
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]
if last.kind == kind && if last.kind == kind &&
@@ -710,6 +618,202 @@ append_or_extend_sub_batch :: proc(
layer.sub_batch_len += 1 layer.sub_batch_len += 1
} }
// ---------------------------------------------------------------------------------------------------------------------
// ----- Clay ------------
// ---------------------------------------------------------------------------------------------------------------------
@(private = "file")
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
context = GLOB.odin_context
log.error("Clay error:", errorData.errorType, errorData.errorText)
}
@(private = "file")
measure_text_clay :: proc "c" (
text: clay.StringSlice,
config: ^clay.TextElementConfig,
user_data: rawptr,
) -> clay.Dimensions {
context = GLOB.odin_context
text := string(text.chars[:text.length])
c_text := strings.clone_to_cstring(text, context.temp_allocator)
defer delete(c_text, context.temp_allocator)
width, height: c.int
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
log.panicf("Failed to measure text: %s", sdl.GetError())
}
return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
}
// Called for each Clay `RenderCommandType.Custom` render command that
// `prepare_clay_batch` encounters.
//
// - `layer` is the layer the command belongs to (post-z-index promotion).
// - `bounds` is already translated into the active layer's coordinate system
// and pre-DPI, matching what the built-in shape procs expect.
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
// `backgroundColor`, `cornerRadius`, and the `customData` pointer the caller
// attached to `clay.CustomElementConfig.customData`.
//
// The callback must not call `new_layer` or `prepare_clay_batch`.
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
ClayBatch :: struct {
bounds: Rectangle,
cmds: clay.ClayArray(clay.RenderCommand),
}
// Process Clay render commands into shape and text primitives.
prepare_clay_batch :: proc(
base_layer: ^Layer,
batch: ^ClayBatch,
mouse_wheel_delta: [2]f32,
frame_time: f32 = 0,
custom_draw: Custom_Draw = nil,
temp_allocator := context.temp_allocator,
) {
mouse_pos: [2]f32
mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y)
// Update clay internals
clay.SetPointerState(
clay.Vector2{mouse_pos.x - base_layer.bounds.x, mouse_pos.y - base_layer.bounds.y},
.LEFT in mouse_flags,
)
clay.UpdateScrollContainers(true, mouse_wheel_delta, frame_time)
layer := base_layer
// Parse render commands
for i in 0 ..< int(batch.cmds.length) {
render_command := clay.RenderCommandArray_Get(&batch.cmds, cast(i32)i)
// Translate bounding box of the primitive by the layer position
bounds := Rectangle {
x = render_command.boundingBox.x + layer.bounds.x,
y = render_command.boundingBox.y + layer.bounds.y,
width = render_command.boundingBox.width,
height = render_command.boundingBox.height,
}
if render_command.zIndex > GLOB.clay_z_index {
log.debug("Higher zIndex found, creating new layer & setting z_index to", render_command.zIndex)
layer = new_layer(layer, bounds)
// Update bounds to new layer offset
bounds.x = render_command.boundingBox.x + layer.bounds.x
bounds.y = render_command.boundingBox.y + layer.bounds.y
GLOB.clay_z_index = render_command.zIndex
}
switch (render_command.commandType) {
case clay.RenderCommandType.None:
log.errorf(
"Received render command with type None. This generally means we're in some kind of fucked up state.",
)
case clay.RenderCommandType.Text:
render_data := render_command.renderData.text
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
c_text := strings.clone_to_cstring(txt, temp_allocator)
defer delete(c_text, temp_allocator)
// Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
sdl_text := cache_get_or_update(
Cache_Key{render_command.id, .Clay},
c_text,
get_font(render_data.fontId, render_data.fontSize),
)
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
case clay.RenderCommandType.Image:
// Any texture
render_data := render_command.renderData.image
if render_data.imageData == nil do continue
img_data := (^Clay_Image_Data)(render_data.imageData)^
cr := render_data.cornerRadius
radii := Rectangle_Radii {
top_left = cr.topLeft,
top_right = cr.topRight,
bottom_right = cr.bottomRight,
bottom_left = cr.bottomLeft,
}
// Background color behind the image (Clay allows it)
bg := color_from_clay(render_data.backgroundColor)
if bg.a > 0 {
rectangle(layer, bounds, bg, radii = radii)
}
// Compute fit UVs
uv, sampler, inner := fit_params(img_data.fit, bounds, img_data.texture_id)
// Draw the image
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:
if bounds.width == 0 || bounds.height == 0 do continue
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
if curr_scissor.sub_batch_len != 0 {
// Scissor has some content, need to make a new scissor
new := Scissor {
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
bounds = sdl.Rect {
c.int(bounds.x * GLOB.dpi_scaling),
c.int(bounds.y * GLOB.dpi_scaling),
c.int(bounds.width * GLOB.dpi_scaling),
c.int(bounds.height * GLOB.dpi_scaling),
},
}
append(&GLOB.scissors, new)
layer.scissor_len += 1
} else {
curr_scissor.bounds = sdl.Rect {
c.int(bounds.x * GLOB.dpi_scaling),
c.int(bounds.y * GLOB.dpi_scaling),
c.int(bounds.width * GLOB.dpi_scaling),
c.int(bounds.height * GLOB.dpi_scaling),
}
}
case clay.RenderCommandType.ScissorEnd:
case clay.RenderCommandType.Rectangle:
render_data := render_command.renderData.rectangle
cr := render_data.cornerRadius
color := color_from_clay(render_data.backgroundColor)
radii := Rectangle_Radii {
top_left = cr.topLeft,
top_right = cr.topRight,
bottom_right = cr.bottomRight,
bottom_left = cr.bottomLeft,
}
rectangle(layer, bounds, color, radii = radii)
case clay.RenderCommandType.Border:
render_data := render_command.renderData.border
cr := render_data.cornerRadius
color := color_from_clay(render_data.color)
thickness := f32(render_data.width.top)
radii := Rectangle_Radii {
top_left = cr.topLeft,
top_right = cr.topRight,
bottom_right = cr.bottomRight,
bottom_left = cr.bottomLeft,
}
rectangle(layer, bounds, BLANK, outline_color = color, outline_width = thickness, radii = radii)
case clay.RenderCommandType.Custom: if custom_draw != nil {
custom_draw(layer, bounds, render_command.renderData.custom)
} else {
log.error("Received clay render command of type custom but no custom_draw proc provided.")
}
}
}
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Buffer ------------ // ----- Buffer ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
+27 -39
View File
@@ -1,71 +1,54 @@
package draw_qr package draw_qr
import "core:mem"
import "core:slice"
import draw ".." import draw ".."
import "../../qrcode" import "../../qrcode"
DFT_QR_DARK :: draw.BLACK // Default QR code dark module color. DFT_QR_DARK :: draw.BLACK // Default QR code dark module color.
DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color. DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color.
DFT_QR_BOOST_ECL :: true // Default QR error correction level boost. DFT_QR_BOOST_ECL :: true // Default QR error correction level boost.
DFT_QR_QUIET_ZONE :: 4 // Default light-pixel border on each side; 4 is the QR spec value.
// Returns the number of bytes to_texture will write. Equals dim*dim*4 where // Returns the number of bytes to_texture will write for the given encoded
// dim = qrcode.get_size(qrcode_buf) + 2*quiet_zone. // QR buffer. Equivalent to size*size*4 where size = qrcode.get_size(qrcode_buf).
texture_size :: #force_inline proc(qrcode_buf: []u8, quiet_zone: int = DFT_QR_QUIET_ZONE) -> int { texture_size :: #force_inline proc(qrcode_buf: []u8) -> int {
size := qrcode.get_size(qrcode_buf) size := qrcode.get_size(qrcode_buf)
if size == 0 || quiet_zone < 0 do return 0 return size * size * 4
padded_size := size + 2 * quiet_zone
return padded_size * padded_size * 4
} }
// Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to // Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to
// texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the // texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the
// caller should pass to draw.register_texture alongside texture_buf. // caller should pass to draw.register_texture alongside texture_buf.
// //
// quiet_zone adds that many `light` pixels on each side; the spec value is 4.
// Final dimension is qrcode.get_size + 2*quiet_zone on each axis.
//
// Returns ok=false when: // Returns ok=false when:
// - qrcode_buf is invalid (qrcode.get_size returns 0). // - qrcode_buf is invalid (qrcode.get_size returns 0).
// - quiet_zone is negative. // - texture_buf is smaller than texture_size(qrcode_buf).
// - texture_buf is smaller than texture_size(qrcode_buf, quiet_zone).
@(require_results) @(require_results)
to_texture :: proc( to_texture :: proc(
qrcode_buf: []u8, qrcode_buf: []u8,
texture_buf: []u8, texture_buf: []u8,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = DFT_QR_DARK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = DFT_QR_LIGHT,
quiet_zone: int = DFT_QR_QUIET_ZONE,
) -> ( ) -> (
desc: draw.Texture_Desc, desc: draw.Texture_Desc,
ok: bool, ok: bool,
) { ) {
size := qrcode.get_size(qrcode_buf) size := qrcode.get_size(qrcode_buf)
if size == 0 || quiet_zone < 0 do return if size == 0 do return {}, false
padded_size := size + 2 * quiet_zone if len(texture_buf) < size * size * 4 do return {}, false
if len(texture_buf) < padded_size * padded_size * 4 do return
// Type-pun to []Color so each store is a single 32-bit write.
pixels := mem.slice_data_cast([]draw.Color, texture_buf[:padded_size * padded_size * 4])
// Bulk-fill with light: handles the border and every light QR module at once.
slice.fill(pixels, light)
// Overwrite only the dark modules, offset by the quiet-zone border.
for y in 0 ..< size { for y in 0 ..< size {
row := (y + quiet_zone) * padded_size + quiet_zone
for x in 0 ..< size { for x in 0 ..< size {
if qrcode.get_module(qrcode_buf, x, y) { i := (y * size + x) * 4
pixels[row + x] = dark c := dark if qrcode.get_module(qrcode_buf, x, y) else light
} texture_buf[i + 0] = c[0]
texture_buf[i + 1] = c[1]
texture_buf[i + 2] = c[2]
texture_buf[i + 3] = c[3]
} }
} }
return draw.Texture_Desc { return draw.Texture_Desc {
width = u32(padded_size), width = u32(size),
height = u32(padded_size), height = u32(size),
depth_or_layers = 1, depth_or_layers = 1,
type = .D2, type = .D2,
format = .R8G8B8A8_UNORM, format = .R8G8B8A8_UNORM,
@@ -88,20 +71,19 @@ register_texture_from_raw :: proc(
qrcode_buf: []u8, qrcode_buf: []u8,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = DFT_QR_DARK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = DFT_QR_LIGHT,
quiet_zone: int = DFT_QR_QUIET_ZONE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
ok: bool, ok: bool,
) { ) {
tex_size := texture_size(qrcode_buf, quiet_zone) tex_size := texture_size(qrcode_buf)
if tex_size == 0 do return draw.INVALID_TEXTURE, false if tex_size == 0 do return draw.INVALID_TEXTURE, false
pixels, alloc_err := make([]u8, tex_size, temp_allocator) pixels, alloc_err := make([]u8, tex_size, temp_allocator)
if alloc_err != nil do return draw.INVALID_TEXTURE, false if alloc_err != nil do return draw.INVALID_TEXTURE, false
defer delete(pixels, temp_allocator) defer delete(pixels, temp_allocator)
desc := to_texture(qrcode_buf, pixels, dark, light, quiet_zone) or_return desc := to_texture(qrcode_buf, pixels, dark, light) or_return
return draw.register_texture(desc, pixels) return draw.register_texture(desc, pixels)
} }
@@ -121,7 +103,6 @@ register_texture_from_text :: proc(
boost_ecl: bool = DFT_QR_BOOST_ECL, boost_ecl: bool = DFT_QR_BOOST_ECL,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = DFT_QR_DARK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = DFT_QR_LIGHT,
quiet_zone: int = DFT_QR_QUIET_ZONE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
@@ -142,7 +123,7 @@ register_texture_from_text :: proc(
temp_allocator, temp_allocator,
) or_return ) or_return
return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator) return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator)
} }
// Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture. // Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture.
@@ -161,7 +142,6 @@ register_texture_from_binary :: proc(
boost_ecl: bool = DFT_QR_BOOST_ECL, boost_ecl: bool = DFT_QR_BOOST_ECL,
dark: draw.Color = DFT_QR_DARK, dark: draw.Color = DFT_QR_DARK,
light: draw.Color = DFT_QR_LIGHT, light: draw.Color = DFT_QR_LIGHT,
quiet_zone: int = DFT_QR_QUIET_ZONE,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) -> ( ) -> (
texture: draw.Texture_Id, texture: draw.Texture_Id,
@@ -182,10 +162,18 @@ register_texture_from_binary :: proc(
temp_allocator, temp_allocator,
) or_return ) or_return
return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator) return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator)
} }
register_texture_from :: proc { register_texture_from :: proc {
register_texture_from_text, register_texture_from_text,
register_texture_from_binary, register_texture_from_binary,
} }
// Default fit=.Fit preserves the QR's square aspect; override as needed.
clay_image :: #force_inline proc(
texture: draw.Texture_Id,
tint: draw.Color = draw.DFT_TINT,
) -> draw.Clay_Image_Data {
return draw.clay_image_data(texture, fit = .Fit, tint = tint)
}
+43 -70
View File
@@ -97,32 +97,13 @@ gaussian_blur :: proc() {
// Both panels share rounded corners. // Both panels share rounded corners.
panel_radii := draw.Rectangle_Radii{16, 16, 16, 16} panel_radii := draw.Rectangle_Radii{16, 16, 16, 16}
// Both zone1 panels share one scope. Different sigmas still trigger separate blur draw.gaussian_blur(
// passes (cost scales with unique sigmas, not with backdrop count); the scope just
// declares "these draws form one bracket." `backdrop_scope` is the RAII-style API:
// `end_backdrop` fires automatically when the block exits.
{
draw.backdrop_scope(base_layer)
draw.backdrop_blur(
base_layer, base_layer,
{60, 80, 320, 140}, {60, 80, 320, 140},
gaussian_sigma = 30, gaussian_sigma = 30,
tint = draw.Color{170, 200, 240, 200}, // cool blue, strong mix tint = draw.Color{170, 200, 240, 200}, // cool blue, strong mix
radii = panel_radii, radii = panel_radii,
) )
// Panel B: lighter blur, warm amber tint. sigma=6.
draw.backdrop_blur(
base_layer,
{420, 80, 320, 140},
gaussian_sigma = 6,
tint = draw.Color{255, 220, 160, 200}, // warm amber, strong mix
radii = panel_radii,
)
}
// Text labels for the two panels. Drawn AFTER `end_backdrop` (which fires at the
// scope-block exit above), so they composite on top of both panels.
draw.text( draw.text(
base_layer, base_layer,
"sigma = 20, cool tint", "sigma = 20, cool tint",
@@ -131,6 +112,15 @@ gaussian_blur :: proc() {
FONT_SIZE, FONT_SIZE,
color = draw.Color{30, 35, 50, 255}, 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( draw.text(
base_layer, base_layer,
"sigma = 6, warm tint", "sigma = 6, warm tint",
@@ -140,10 +130,10 @@ gaussian_blur :: proc() {
color = draw.Color{60, 40, 20, 255}, color = draw.Color{60, 40, 20, 255},
) )
// Post-bracket verification: a white stripe drawn AFTER `end_backdrop` in the same // Pass-B verification: a rectangle drawn AFTER the backdrops in the same layer
// layer. Should render ON TOP of both panels because the backdrop scope (and its // Per the bracket scheduling model, this should render ON TOP of both panels above.
// composite output) is now closed; any non-backdrop draw on this layer composites // If you see this stripe behind the panels instead of in front, something is wrong with
// with LOAD on top of whatever the bracket left in source_texture. // the Pass B post-bracket path.
draw.rectangle(base_layer, {WINDOW_W * 0.5 - 4, 70, 8, 160}, draw.Color{255, 255, 255, 230}) 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: second layer with its own backdrop --------------------------------
@@ -161,18 +151,14 @@ gaussian_blur :: proc() {
stripe_y := 320 + (math.sin(t * 0.05) * 0.5 + 0.5) * 200 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}) draw.rectangle(zone2, {30, stripe_y, WINDOW_W * 0.55 - 60, 18}, draw.Color{255, 100, 200, 200})
// Zone 2's frosted panel. Single-panel scope; `backdrop_scope` keeps the begin/end // Zone 2's frosted panel.
// pair tied to the block. draw.gaussian_blur(
{
draw.backdrop_scope(zone2)
draw.backdrop_blur(
zone2, zone2,
{60, 360, WINDOW_W * 0.55 - 120, 160}, {60, 360, WINDOW_W * 0.55 - 120, 160},
gaussian_sigma = 10, gaussian_sigma = 10,
tint = draw.WHITE, // pure blur (white tint with any alpha is a no-op) tint = draw.WHITE, // pure blur (white tint with any alpha is a no-op)
radii = draw.Rectangle_Radii{24, 24, 24, 24}, radii = draw.Rectangle_Radii{24, 24, 24, 24},
) )
}
draw.text( draw.text(
zone2, zone2,
"Layer 2 backdrop", "Layer 2 backdrop",
@@ -211,44 +197,15 @@ gaussian_blur :: proc() {
) )
} }
// All three Zone 3 backdrops share one scope. The sigma=0 mirror, then the two
// contiguous sigma=8 panels. The sigma=8 pair stays contiguous in the sub-batch list,
// so `append_or_extend_sub_batch` still coalesces them into a single instanced
// composite draw — scope boundaries don't affect coalescing, only kind/sigma identity.
{
draw.backdrop_scope(zone3)
// Edge case 1: sigma = 0 "mirror" — sharp framebuffer sample, no blur. Should reproduce // 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. // the underlying pixels exactly through the SDF mask. Tinted slightly so it's visible.
draw.backdrop_blur( draw.gaussian_blur(
zone3, zone3,
{WINDOW_W * 0.55 + 30, 310, 150, 70}, {WINDOW_W * 0.55 + 30, 310, 150, 70},
gaussian_sigma = 0, gaussian_sigma = 0,
tint = draw.WHITE, // pure mirror (no blur, no tint) tint = draw.WHITE, // pure mirror (no blur, no tint)
radii = draw.Rectangle_Radii{12, 12, 12, 12}, radii = draw.Rectangle_Radii{12, 12, 12, 12},
) )
// 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.backdrop_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.backdrop_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},
)
}
// Edge case 3: text drawn AFTER `end_backdrop` in the same layer. Composites on top of
// the bracket's V-composite output and should appear sharply over the green panels.
draw.text( draw.text(
zone3, zone3,
"sigma=0 (mirror)", "sigma=0 (mirror)",
@@ -257,6 +214,24 @@ gaussian_blur :: proc() {
FONT_SIZE, FONT_SIZE,
color = draw.Color{20, 20, 20, 255}, 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( draw.text(
zone3, zone3,
"sigma=8 (coalesced pair)", "sigma=8 (coalesced pair)",
@@ -265,9 +240,12 @@ gaussian_blur :: proc() {
FONT_SIZE, FONT_SIZE,
color = draw.Color{20, 40, 20, 255}, 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( draw.text(
zone3, zone3,
"Post-scope text overlay", "Pass B text overlay",
{WINDOW_W * 0.55 + 38, 480}, {WINDOW_W * 0.55 + 38, 480},
PLEX_SANS_REGULAR, PLEX_SANS_REGULAR,
FONT_SIZE, FONT_SIZE,
@@ -365,23 +343,18 @@ gaussian_blur_debug :: proc() {
// THE PANEL UNDER TEST. Square, centered, large enough to cover multiple grid cells and // 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 // the circle row. Square shape makes any horizontal-vs-vertical asymmetry purely
// renderer-driven (geometry can't introduce it). // renderer-driven (geometry can't introduce it).
//
// Uses the explicit begin/end form (instead of `backdrop_scope`) to exercise the
// alternative API surface in the diagnostic harness.
panel := draw.Rectangle{250, 150, 300, 300} panel := draw.Rectangle{250, 150, 300, 300}
draw.begin_backdrop(base_layer) draw.gaussian_blur(
draw.backdrop_blur(
base_layer, base_layer,
panel, panel,
gaussian_sigma = sigma, gaussian_sigma = sigma,
tint = draw.WHITE, tint = draw.WHITE,
radii = draw.Rectangle_Radii{20, 20, 20, 20}, radii = draw.Rectangle_Radii{20, 20, 20, 20},
) )
draw.end_backdrop(base_layer)
// Post-scope test: a bright rectangle drawn AFTER `end_backdrop` in the same layer. // Pass B test: a bright rectangle drawn AFTER the backdrop in the same layer. Should
// Should always render on top of the panel. If the panel ever shows a "ghost" of this // always render on top of the panel. If the panel ever shows a "ghost" of this rect
// rect inside its blur, the V-composite is sampling the wrong texture state. // inside its blur, the V-composite is sampling the wrong texture state.
if show_test_rect { if show_test_rect {
draw.rectangle(base_layer, {380, 280, 40, 40}, draw.Color{0, 200, 0, 255}) draw.rectangle(base_layer, {380, 280, 40, 40}, draw.Color{0, 200, 0, 255})
} }
-363
View File
@@ -1,363 +0,0 @@
package examples
import "core:os"
import sdl "vendor:sdl3"
import "../../draw"
import "../../vendor/clay"
import cyber "../cybersteel"
// Clay border debug example.
//
// Lays out a grid of bordered Clay elements that exercise every code path in
// `clay_emit_partial_border` and `try_dispatch_clay_rect_border_pair`:
//
// 1. Uniform borders (fast path) — sharp, rounded, and the border-thicker-than-radius
// edge case (inner corner clamps to 0).
// 2. Background + border combinations — opaque bg + opaque uniform border MERGES into one
// SDF primitive; translucent border DECLINES the merge to preserve blend fidelity;
// non-uniform border declines and falls through to the slow path; translucent bg with
// opaque border still merges (bg alpha doesn't affect merge correctness).
// 3. Single-side borders — top / right / bottom / left individually.
// 4. Two-side borders — parallel pairs (no corners drawn) and adjacent pairs (one corner
// rounds, others stay square).
// 5. Three-side borders + asymmetric widths.
// 6. Layout correctness — a vertical list with bottom-border separators (each border
// lives inside its own item, no bleed between siblings) and a row of adjacent fully
// bordered siblings (no border overlap, each in its own bounds).
clay_borders :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Clay Borders Debug", 1200, 900, {.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)
// Distinct colors so the fill, border, and translucent variants are visually unambiguous.
BG_PAGE :: draw.Color{25, 25, 30, 255}
FILL_OPAQUE :: draw.Color{80, 120, 200, 255}
FILL_TRANSLUCENT :: draw.Color{80, 120, 200, 128}
BORDER_OPAQUE :: draw.Color{255, 200, 100, 255}
BORDER_TRANSLUCENT :: draw.Color{255, 200, 100, 128}
label_config := clay.TextElementConfig {
fontId = PLEX_SANS_REGULAR,
fontSize = 12,
textColor = {220, 220, 220, 255},
}
header_config := clay.TextElementConfig {
fontId = PLEX_SANS_REGULAR,
fontSize = 16,
textColor = {255, 255, 255, 255},
}
title_config := clay.TextElementConfig {
fontId = PLEX_SANS_REGULAR,
fontSize = 22,
textColor = {255, 255, 255, 255},
}
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
base_layer := draw.begin({width = 1200, height = 900})
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
clay.BeginLayout()
if clay.UI(clay.ID("borders_page"))(
{
layout = {
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
padding = clay.PaddingAll(20),
childGap = 14,
layoutDirection = .TopToBottom,
},
backgroundColor = clay_color(BG_PAGE),
},
) {
clay.Text("Clay Borders Debug", title_config)
//----- Section 1: Uniform borders (fast path) -----------------------------------
clay.Text("Uniform borders (fast path)", header_config)
if clay.UI(clay.ID("row_uniform"))(border_row_layout()) {
border_test_card(
"1px sharp",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{left = 1, right = 1, top = 1, bottom = 1},
{},
)
border_test_card(
"2px, radius 8",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{left = 2, right = 2, top = 2, bottom = 2},
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
)
border_test_card(
"8px, radius 20",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{left = 8, right = 8, top = 8, bottom = 8},
{topLeft = 20, topRight = 20, bottomRight = 20, bottomLeft = 20},
)
border_test_card(
"10px > radius 5 (inner clamps)",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{left = 10, right = 10, top = 10, bottom = 10},
{topLeft = 5, topRight = 5, bottomRight = 5, bottomLeft = 5},
)
}
//----- Section 2: Background + border (merge optimization) ----------------------
clay.Text("Background + border (merge optimization)", header_config)
if clay.UI(clay.ID("row_bg_border"))(border_row_layout()) {
border_test_card(
"opaque bg + opaque (MERGES: 1 prim)",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{left = 2, right = 2, top = 2, bottom = 2},
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
)
border_test_card(
"translucent bg + opaque (MERGES)",
label_config,
FILL_TRANSLUCENT,
BORDER_OPAQUE,
{left = 3, right = 3, top = 3, bottom = 3},
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
)
border_test_card(
"opaque bg + translucent (NO merge)",
label_config,
FILL_OPAQUE,
BORDER_TRANSLUCENT,
{left = 4, right = 4, top = 4, bottom = 4},
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
)
border_test_card(
"opaque bg + non-uniform (NO merge)",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{left = 1, right = 4, top = 2, bottom = 3},
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
)
}
//----- Section 3: Single side borders -------------------------------------------
clay.Text("Single side", header_config)
if clay.UI(clay.ID("row_single_side"))(border_row_layout()) {
border_test_card("top only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {top = 4}, {})
border_test_card("right only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {right = 4}, {})
border_test_card(
"bottom only (4px, divider)",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{bottom = 4},
{},
)
border_test_card("left only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {left = 4}, {})
}
//----- Section 4: Two side borders ----------------------------------------------
clay.Text("Two sides", header_config)
if clay.UI(clay.ID("row_two_sides"))(border_row_layout()) {
border_test_card(
"T+B parallel (no corners)",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{top = 3, bottom = 3},
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
)
border_test_card(
"L+R parallel (no corners)",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{left = 3, right = 3},
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
)
border_test_card(
"T+L adjacent (TL rounds)",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{top = 3, left = 3},
{topLeft = 12, topRight = 12, bottomRight = 12, bottomLeft = 12},
)
border_test_card(
"B+R adjacent (BR rounds)",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{bottom = 3, right = 3},
{topLeft = 12, topRight = 12, bottomRight = 12, bottomLeft = 12},
)
}
//----- Section 5: Three sides + asymmetric widths -------------------------------
clay.Text("Three sides + asymmetric widths", header_config)
if clay.UI(clay.ID("row_advanced"))(border_row_layout()) {
border_test_card(
"T+R+B (no L), rounded",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{top = 3, right = 3, bottom = 3},
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
)
border_test_card(
"T+L+R (no B), rounded",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{top = 3, left = 3, right = 3},
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
)
border_test_card(
"asym 1/2/3/4 T/R/B/L",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{top = 1, right = 2, bottom = 3, left = 4},
{},
)
border_test_card(
"asym + rounded",
label_config,
FILL_OPAQUE,
BORDER_OPAQUE,
{top = 2, right = 4, bottom = 2, left = 4},
{topLeft = 10, topRight = 10, bottomRight = 10, bottomLeft = 10},
)
}
//----- Section 6: Layout correctness --------------------------------------------
clay.Text("Layout correctness", header_config)
if clay.UI(clay.ID("row_correctness"))(
{layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, childGap = 14}},
) {
// 6a: vertical list with per-item bottom-border separator. Each item's
// border draws INSIDE its own bounds, so adjacent items don't bleed.
if clay.UI(clay.ID("list_demo"))(
{
layout = {
sizing = {clay.SizingFixed(300), clay.SizingFit({})},
padding = clay.PaddingAll(6),
childGap = 6,
layoutDirection = .TopToBottom,
},
},
) {
clay.Text("List with bottom-border separators", label_config)
if clay.UI(clay.ID("list_outer"))(
{
layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, layoutDirection = .TopToBottom},
backgroundColor = clay_color(FILL_OPAQUE),
},
) {
for index in 0 ..< 5 {
if clay.UI(clay.ID("list_item", u32(index)))(
{
layout = {sizing = {clay.SizingGrow({}), clay.SizingFixed(28)}, padding = clay.PaddingAll(6)},
border = {color = clay_color(BORDER_OPAQUE), width = {bottom = 1}},
},
) {
clay.Text("Item", label_config)
}
}
}
}
// 6b: row of adjacent fully bordered siblings. With borders rendered
// INSIDE each element's bounds, the boundary between two siblings shows
// the natural 2*width sum (no overlap, no bleed).
if clay.UI(clay.ID("adj_demo"))(
{
layout = {
sizing = {clay.SizingFixed(380), clay.SizingFit({})},
padding = clay.PaddingAll(6),
childGap = 6,
layoutDirection = .TopToBottom,
},
},
) {
clay.Text("Adjacent bordered siblings (no gap)", label_config)
if clay.UI(clay.ID("adj_row"))({layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}}}) {
for index in 0 ..< 4 {
if clay.UI(clay.ID("adj_item", u32(index)))(
{
layout = {sizing = {clay.SizingFixed(80), clay.SizingFixed(60)}},
backgroundColor = clay_color(FILL_OPAQUE),
border = {color = clay_color(BORDER_OPAQUE), width = {left = 2, right = 2, top = 2, bottom = 2}},
},
) {}
}
}
}
}
}
clay_batch := draw.ClayBatch {
bounds = base_layer.bounds,
cmds = clay.EndLayout(0),
}
draw.prepare_clay_batch(base_layer, &clay_batch)
draw.end(gpu, window)
}
}
// Helper: convert a draw.Color (RGBA u8) to clay.Color (RGBA float in 0-255 range).
clay_color :: proc(c: draw.Color) -> clay.Color {
return clay.Color{f32(c[0]), f32(c[1]), f32(c[2]), f32(c[3])}
}
// Helper: shared row container declaration for the test sections.
border_row_layout :: proc() -> clay.ElementDeclaration {
return clay.ElementDeclaration{layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, childGap = 12}}
}
// One labeled test card: a fixed-width column with a caption above and a sample bordered
// rectangle below. Uses `clay.ID_LOCAL` for the inner element so each card gets a unique
// child ID without the caller passing one explicitly.
border_test_card :: proc(
label: string,
label_config: clay.TextElementConfig,
fill_color: draw.Color,
border_color: draw.Color,
border_width: clay.BorderWidth,
corner_radii: clay.CornerRadius,
) {
if clay.UI(clay.ID(label))(
{
layout = {
sizing = {clay.SizingFixed(275), clay.SizingFit({})},
padding = clay.PaddingAll(4),
childGap = 6,
layoutDirection = .TopToBottom,
},
},
) {
clay.Text(label, label_config)
if clay.UI(clay.ID_LOCAL("test_inner"))(
{
layout = {sizing = {clay.SizingGrow({}), clay.SizingFixed(64)}},
backgroundColor = clay_color(fill_color),
border = clay.BorderElementConfig{color = clay_color(border_color), width = border_width},
cornerRadius = corner_radii,
},
) {}
}
}
-4
View File
@@ -9,7 +9,6 @@ EX_HELLOPE_SHAPES :: "hellope-shapes"
EX_HELLOPE_TEXT :: "hellope-text" EX_HELLOPE_TEXT :: "hellope-text"
EX_HELLOPE_CLAY :: "hellope-clay" EX_HELLOPE_CLAY :: "hellope-clay"
EX_HELLOPE_CUSTOM :: "hellope-custom" EX_HELLOPE_CUSTOM :: "hellope-custom"
EX_CLAY_BORDERS :: "clay-borders"
EX_TEXTURES :: "textures" EX_TEXTURES :: "textures"
EX_GAUSSIAN_BLUR :: "gaussian-blur" EX_GAUSSIAN_BLUR :: "gaussian-blur"
EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug" EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug"
@@ -24,8 +23,6 @@ AVAILABLE_EXAMPLES_MSG ::
", " + ", " +
EX_HELLOPE_CUSTOM + EX_HELLOPE_CUSTOM +
", " + ", " +
EX_CLAY_BORDERS +
", " +
EX_TEXTURES + EX_TEXTURES +
", " + ", " +
EX_GAUSSIAN_BLUR + EX_GAUSSIAN_BLUR +
@@ -84,7 +81,6 @@ main :: proc() {
case EX_HELLOPE_CUSTOM: hellope_custom() case EX_HELLOPE_CUSTOM: hellope_custom()
case EX_HELLOPE_SHAPES: hellope_shapes() case EX_HELLOPE_SHAPES: hellope_shapes()
case EX_HELLOPE_TEXT: hellope_text() case EX_HELLOPE_TEXT: hellope_text()
case EX_CLAY_BORDERS: clay_borders()
case EX_TEXTURES: textures() case EX_TEXTURES: textures()
case EX_GAUSSIAN_BLUR: gaussian_blur() case EX_GAUSSIAN_BLUR: gaussian_blur()
case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug() case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug()
+27 -58
View File
@@ -63,7 +63,7 @@ hellope_shapes :: proc() {
outline_width = 2, outline_width = 2,
origin = draw.center_of(rect), origin = draw.center_of(rect),
rotation = spin_angle, rotation = spin_angle,
feather_ppx = 1, feather_px = 1,
) )
// Rounded rectangle rotating around its center // Rounded rectangle rotating around its center
@@ -244,8 +244,9 @@ hellope_clay :: proc() {
base_layer := draw.begin({width = 500, height = 500}) base_layer := draw.begin({width = 500, height = 500})
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height}) clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
clay.BeginLayout() clay.BeginLayout()
if clay.UI(clay.ID("outer"))( if clay.UI()(
{ {
id = clay.ID("outer"),
layout = { layout = {
sizing = {clay.SizingGrow({}), clay.SizingGrow({})}, sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
childAlignment = {x = .Center, y = .Center}, childAlignment = {x = .Center, y = .Center},
@@ -253,13 +254,13 @@ hellope_clay :: proc() {
backgroundColor = {127, 127, 127, 255}, backgroundColor = {127, 127, 127, 255},
}, },
) { ) {
clay.Text("Hellope!", text_config) clay.Text("Hellope!", &text_config)
} }
clay_batch := draw.ClayBatch { clay_batch := draw.ClayBatch {
bounds = base_layer.bounds, bounds = base_layer.bounds,
cmds = clay.EndLayout(0), cmds = clay.EndLayout(),
} }
draw.prepare_clay_batch(base_layer, &clay_batch) draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0})
draw.end(gpu, window) draw.end(gpu, window)
} }
} }
@@ -281,29 +282,11 @@ hellope_custom :: proc() {
gauge := Gauge { gauge := Gauge {
value = 0.73, value = 0.73,
color = {50, 200, 100, 255}, color = {50, 200, 100, 255},
bg_color = {80, 80, 80, 255},
} }
gauge2 := Gauge { gauge2 := Gauge {
value = 0.45, value = 0.45,
color = {200, 100, 50, 255}, color = {200, 100, 50, 255},
bg_color = {80, 80, 80, 255},
} }
// `clay.CustomElementConfig.customData` is a rawptr; the Clay integration in `draw`
// requires it to point at a `Clay_Custom` value. The explicit `rawptr(...)` cast is
// necessary because Odin does not chain `^Gauge -> rawptr -> Clay_Custom` implicitly
// (variant-to-union and ^T-to-rawptr are each implicit on their own, but not stacked).
gauge_custom: draw.Clay_Custom = rawptr(&gauge)
gauge2_custom: draw.Clay_Custom = rawptr(&gauge2)
// Backdrop variant: variant-to-union conversion is implicit, so no cast needed.
// `tint = draw.WHITE` is the no-op tint per the backdrop module's convention
// (matches `examples/backdrop.odin`'s "pure blur, no color" usage).
backdrop_custom: draw.Clay_Custom = draw.Backdrop_Marker {
sigma = 8,
tint = draw.WHITE,
}
spin_angle: f32 = 0 spin_angle: f32 = 0
for { for {
@@ -321,8 +304,9 @@ hellope_custom :: proc() {
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height}) clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
clay.BeginLayout() clay.BeginLayout()
if clay.UI(clay.ID("outer"))( if clay.UI()(
{ {
id = clay.ID("outer"),
layout = { layout = {
sizing = {clay.SizingGrow({}), clay.SizingGrow({})}, sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
childAlignment = {x = .Center, y = .Center}, childAlignment = {x = .Center, y = .Center},
@@ -332,68 +316,53 @@ hellope_custom :: proc() {
backgroundColor = {50, 50, 50, 255}, backgroundColor = {50, 50, 50, 255},
}, },
) { ) {
if clay.UI(clay.ID("title"))({layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) { if clay.UI()({id = clay.ID("title"), layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) {
clay.Text("Custom Draw Demo", text_config) clay.Text("Custom Draw Demo", &text_config)
} }
// gauge1 is BEHIND the backdrop — the backdrop is declared as a floating CHILD if clay.UI()(
// of gauge1, pinned to gauge1's LeftTop and sized 300x30 so it covers exactly
// gauge1's footprint. Clay emits a floating child's render command after the
// parent's, so the stream order is gauge1 → backdrop → gauge2: gauge1's pixels
// land in `source_texture` before the bracket samples (visible as a blurred
// reflection inside the strip), and gauge2 is deferred-replayed by
// `prepare_clay_batch` after the bracket closes (renders crisp on top of the
// bracket output — unrelated to the strip since they don't overlap).
// `backgroundColor` is omitted on the gauges; bg lives on `Gauge.bg_color`. See `draw_custom`.
if clay.UI(clay.ID("gauge"))(
{ {
id = clay.ID("gauge"),
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}}, layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
custom = {customData = &gauge_custom}, custom = {customData = &gauge},
}, backgroundColor = {80, 80, 80, 255},
) {
if clay.UI(clay.ID("backdrop"))(
{
floating = {attachTo = .Parent, attachment = {parent = .LeftTop, element = .LeftTop}},
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
custom = {customData = &backdrop_custom},
}, },
) {} ) {}
}
if clay.UI(clay.ID("gauge2"))( if clay.UI()(
{ {
id = clay.ID("gauge2"),
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}}, layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
custom = {customData = &gauge2_custom}, custom = {customData = &gauge2},
backgroundColor = {80, 80, 80, 255},
}, },
) {} ) {}
} }
clay_batch := draw.ClayBatch { clay_batch := draw.ClayBatch {
bounds = base_layer.bounds, bounds = base_layer.bounds,
cmds = clay.EndLayout(0), cmds = clay.EndLayout(),
} }
draw.prepare_clay_batch(base_layer, &clay_batch, custom_draw = draw_custom) draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0}, custom_draw = draw_custom)
draw.end(gpu, window) draw.end(gpu, window)
} }
Gauge :: struct { Gauge :: struct {
value: f32, value: f32,
color: draw.Color, color: draw.Color,
bg_color: draw.Color,
} }
draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) { draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) {
// `render_data.customData` has been unwrapped from the `Clay_Custom` envelope by
// `prepare_clay_batch` — it points at the Gauge directly, the same as it would have
// before the union refactor.
gauge := cast(^Gauge)render_data.customData gauge := cast(^Gauge)render_data.customData
// `gauge.bg_color` instead of `render_data.backgroundColor`: under Clay master, an
// element with both `custom.customData` and `backgroundColor` emits a Custom AND a
// Rectangle for the same bounds, in that order — the Rectangle paints over the
// callback's output. Carrying bg on user data sidesteps it.
border_width: f32 = 2 border_width: f32 = 2
draw.rectangle(layer, bounds, gauge.bg_color, outline_color = draw.WHITE, outline_width = border_width) draw.rectangle(
layer,
bounds,
draw.color_from_clay(render_data.backgroundColor),
outline_color = draw.WHITE,
outline_width = border_width,
)
fill := draw.Rectangle { fill := draw.Rectangle {
x = bounds.x, x = bounds.x,
+5 -15
View File
@@ -166,14 +166,12 @@ textures :: proc() {
ROW2_Y :: f32(190) ROW2_Y :: f32(190)
// QR code (RGBA texture with baked colors, nearest sampling) + thin framing border. // 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( draw.rectangle(
base_layer, base_layer,
{COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
draw.Texture_Fill{id = qr_texture, tint = draw.WHITE, uv_rect = {0, 0, 1, 1}, sampler = .Nearest_Clamp}, draw.Texture_Fill{id = qr_texture, tint = draw.WHITE, uv_rect = {0, 0, 1, 1}, sampler = .Nearest_Clamp},
outline_color = draw.WHITE,
outline_width = 2,
) )
draw.text( draw.text(
base_layer, base_layer,
@@ -184,7 +182,7 @@ textures :: proc() {
color = draw.WHITE, color = draw.WHITE,
) )
// Rounded corners + outline traces the rounded shape. // Rounded corners
draw.rectangle( draw.rectangle(
base_layer, base_layer,
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, {COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
@@ -194,8 +192,6 @@ textures :: proc() {
uv_rect = {0, 0, 1, 1}, uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
}, },
outline_color = draw.Color{255, 200, 100, 255},
outline_width = 3,
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(
@@ -207,7 +203,7 @@ textures :: proc() {
color = draw.WHITE, color = draw.WHITE,
) )
// Rotating + outline rotates with the texture. // 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( draw.rectangle(
base_layer, base_layer,
@@ -218,8 +214,6 @@ textures :: proc() {
uv_rect = {0, 0, 1, 1}, uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
}, },
outline_color = draw.WHITE,
outline_width = 2,
origin = draw.center_of(rot_rect), origin = draw.center_of(rot_rect),
rotation = spin_angle, rotation = spin_angle,
) )
@@ -288,7 +282,7 @@ textures :: proc() {
color = draw.WHITE, color = draw.WHITE,
) )
// Per-corner radii + outline traces the asymmetric corner shape. // Per-corner radii
draw.rectangle( draw.rectangle(
base_layer, base_layer,
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE}, {COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
@@ -298,8 +292,6 @@ textures :: proc() {
uv_rect = {0, 0, 1, 1}, uv_rect = {0, 0, 1, 1},
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
}, },
outline_color = draw.Color{255, 100, 100, 255},
outline_width = 3,
radii = {20, 0, 20, 0}, radii = {20, 0, 20, 0},
) )
draw.text( draw.text(
@@ -329,14 +321,12 @@ textures :: proc() {
sampler = .Nearest_Clamp, sampler = .Nearest_Clamp,
} }
// Textured circle + outline (textured shape with built-in border). // Textured circle
draw.circle( draw.circle(
base_layer, base_layer,
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2}, {SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
SHAPE_SIZE / 2, SHAPE_SIZE / 2,
checker_fill, checker_fill,
outline_color = draw.WHITE,
outline_width = 2,
) )
draw.text( draw.text(
base_layer, base_layer,
@@ -25,9 +25,9 @@ struct main0_in
{ {
float2 p_local [[user(locn0)]]; float2 p_local [[user(locn0)]];
float4 f_color [[user(locn1)]]; float4 f_color [[user(locn1)]];
float2 f_half_size_ppx [[user(locn2), flat]]; float2 f_half_size [[user(locn2), flat]];
float4 f_radii_ppx [[user(locn3), flat]]; float4 f_radii [[user(locn3), flat]];
float f_half_feather_ppx [[user(locn4), flat]]; float f_half_feather [[user(locn4), flat]];
}; };
static inline __attribute__((always_inline)) static inline __attribute__((always_inline))
@@ -96,16 +96,16 @@ fragment main0_out main0(main0_in in [[stage_in]], constant Uniforms& _108 [[buf
return out; return out;
} }
float2 param_1 = in.p_local; float2 param_1 = in.p_local;
float2 param_2 = in.f_half_size_ppx; float2 param_2 = in.f_half_size;
float4 param_3 = in.f_radii_ppx; float4 param_3 = in.f_radii;
float d = sdRoundedBox(param_1, param_2, param_3); float d = sdRoundedBox(param_1, param_2, param_3);
if (d > in.f_half_feather_ppx) if (d > in.f_half_feather)
{ {
discard_fragment(); discard_fragment();
} }
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07); float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
float d_n = d / grad_magnitude; float d_n = d / grad_magnitude;
float h_n = in.f_half_feather_ppx / 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; 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 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)); float3 tinted = mix(color_1, color_1 * in.f_color.xyz, float3(in.f_color.w));
Binary file not shown.
+18 -18
View File
@@ -55,18 +55,18 @@ struct Uniforms
struct Gaussian_Blur_Primitive struct Gaussian_Blur_Primitive
{ {
float4 bounds; float4 bounds;
float4 radii_ppx; float4 radii;
float2 half_size_ppx; float2 half_size;
float half_feather_ppx; float half_feather;
uint color; uint color;
}; };
struct Gaussian_Blur_Primitive_1 struct Gaussian_Blur_Primitive_1
{ {
float4 bounds; float4 bounds;
float4 radii_ppx; float4 radii;
float2 half_size_ppx; float2 half_size;
float half_feather_ppx; float half_feather;
uint color; uint color;
}; };
@@ -81,9 +81,9 @@ struct main0_out
{ {
float2 p_local [[user(locn0)]]; float2 p_local [[user(locn0)]];
float4 f_color [[user(locn1)]]; float4 f_color [[user(locn1)]];
float2 f_half_size_ppx [[user(locn2)]]; float2 f_half_size [[user(locn2)]];
float4 f_radii_ppx [[user(locn3)]]; float4 f_radii [[user(locn3)]];
float f_half_feather_ppx [[user(locn4)]]; float f_half_feather [[user(locn4)]];
float4 gl_Position [[position]]; float4 gl_Position [[position]];
}; };
@@ -96,26 +96,26 @@ vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Gaussi
out.gl_Position = float4(ndc, 0.0, 1.0); out.gl_Position = float4(ndc, 0.0, 1.0);
out.p_local = float2(0.0); out.p_local = float2(0.0);
out.f_color = float4(0.0); out.f_color = float4(0.0);
out.f_half_size_ppx = float2(0.0); out.f_half_size = float2(0.0);
out.f_radii_ppx = float4(0.0); out.f_radii = float4(0.0);
out.f_half_feather_ppx = 0.0; out.f_half_feather = 0.0;
} }
else else
{ {
Gaussian_Blur_Primitive p; Gaussian_Blur_Primitive p;
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds; p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
p.radii_ppx = _69.primitives[int(gl_InstanceIndex)].radii_ppx; p.radii = _69.primitives[int(gl_InstanceIndex)].radii;
p.half_size_ppx = _69.primitives[int(gl_InstanceIndex)].half_size_ppx; p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size;
p.half_feather_ppx = _69.primitives[int(gl_InstanceIndex)].half_feather_ppx; p.half_feather = _69.primitives[int(gl_InstanceIndex)].half_feather;
p.color = _69.primitives[int(gl_InstanceIndex)].color; p.color = _69.primitives[int(gl_InstanceIndex)].color;
float2 corner = _97[int(gl_VertexIndex)]; float2 corner = _97[int(gl_VertexIndex)];
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;
out.p_local = (world_pos - center) * _13.dpi_scale; out.p_local = (world_pos - center) * _13.dpi_scale;
out.f_color = unpack_unorm4x8_to_float(p.color); out.f_color = unpack_unorm4x8_to_float(p.color);
out.f_half_size_ppx = p.half_size_ppx; out.f_half_size = p.half_size;
out.f_radii_ppx = p.radii_ppx; out.f_radii = p.radii;
out.f_half_feather_ppx = p.half_feather_ppx; out.f_half_feather = p.half_feather;
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0); out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
} }
return out; return out;
Binary file not shown.
+24 -24
View File
@@ -107,57 +107,57 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
} }
float d = 1000000015047466219876688855040.0; float d = 1000000015047466219876688855040.0;
float h = 0.5; float h = 0.5;
float2 half_size_ppx = in.f_params.xy; float2 half_size = in.f_params.xy;
float2 p_local_ppx = in.f_local_or_uv; float2 p_local = in.f_local_or_uv;
if (kind == 1u) if (kind == 1u)
{ {
float4 corner_radii_ppx = float4(in.f_params.zw, in.f_params2.xy); float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy);
h = in.f_params2.z; h = in.f_params2.z;
float2 param = p_local_ppx; float2 param = p_local;
float2 param_1 = half_size_ppx; float2 param_1 = half_size;
float4 param_2 = corner_radii_ppx; float4 param_2 = corner_radii;
d = sdRoundedBox(param, param_1, param_2); d = sdRoundedBox(param, param_1, param_2);
} }
else else
{ {
if (kind == 2u) if (kind == 2u)
{ {
float radius_ppx = in.f_params.x; float radius = in.f_params.x;
float sides = in.f_params.y; float sides = in.f_params.y;
h = in.f_params.z; h = in.f_params.z;
float2 param_3 = p_local_ppx; float2 param_3 = p_local;
float param_4 = radius_ppx; float param_4 = radius;
float param_5 = sides; float param_5 = sides;
d = sdRegularPolygon(param_3, param_4, param_5); d = sdRegularPolygon(param_3, param_4, param_5);
half_size_ppx = float2(radius_ppx); half_size = float2(radius);
} }
else else
{ {
if (kind == 3u) if (kind == 3u)
{ {
float2 radii_ppx = in.f_params.xy; float2 ab = in.f_params.xy;
h = in.f_params.z; h = in.f_params.z;
float2 param_6 = p_local_ppx; float2 param_6 = p_local;
float2 param_7 = radii_ppx; float2 param_7 = ab;
d = sdEllipseApprox(param_6, param_7); d = sdEllipseApprox(param_6, param_7);
half_size_ppx = radii_ppx; half_size = ab;
} }
else else
{ {
if (kind == 4u) if (kind == 4u)
{ {
float inner_radius_ppx = in.f_params.x; float inner = in.f_params.x;
float outer_radius_ppx = in.f_params.y; float outer = in.f_params.y;
float2 n_start = in.f_params.zw; float2 n_start = in.f_params.zw;
float2 n_end = in.f_params2.xy; float2 n_end = in.f_params2.xy;
uint arc_bits = (flags >> 5u) & 3u; uint arc_bits = (flags >> 5u) & 3u;
h = in.f_params2.z; h = in.f_params2.z;
float r = length(p_local_ppx); float r = length(p_local);
d = fast::max(inner_radius_ppx - r, r - outer_radius_ppx); d = fast::max(inner - r, r - outer);
if (arc_bits != 0u) if (arc_bits != 0u)
{ {
float d_start = dot(p_local_ppx, n_start); float d_start = dot(p_local, n_start);
float d_end = dot(p_local_ppx, n_end); float d_end = dot(p_local, n_end);
float _338; float _338;
if (arc_bits == 1u) if (arc_bits == 1u)
{ {
@@ -170,7 +170,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
float d_wedge = _338; float d_wedge = _338;
d = fast::max(d, d_wedge); d = fast::max(d, d_wedge);
} }
half_size_ppx = float2(outer_radius_ppx); half_size = float2(outer);
} }
} }
} }
@@ -185,7 +185,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
float4 gradient_end = unpack_unorm4x8_to_float(in.f_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_ppx / half_size_ppx); float t_1 = length(p_local / half_size);
float4 param_8 = gradient_start; float4 param_8 = gradient_start;
float4 param_9 = gradient_end; float4 param_9 = gradient_end;
float param_10 = t_1; float param_10 = t_1;
@@ -194,7 +194,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
else else
{ {
float2 direction = float2(as_type<half2>(in.f_effects.z)); float2 direction = float2(as_type<half2>(in.f_effects.z));
float t_2 = (dot(p_local_ppx / half_size_ppx, 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;
float param_13 = t_2; float param_13 = t_2;
@@ -206,7 +206,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 = in.f_uv_rect; float4 uv_rect = in.f_uv_rect;
float2 local_uv = ((p_local_ppx / half_size_ppx) * 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);
} }
Binary file not shown.
+22 -32
View File
@@ -60,21 +60,32 @@ 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 Core_2D_Primitives& _31 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]]) vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Core_2D_Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
{ {
main0_out out = {}; main0_out out = {};
if (_12.mode == 1u) if (_12.mode == 0u)
{
out.f_color = in.v_color;
out.f_local_or_uv = in.v_uv;
out.f_params = float4(0.0);
out.f_params2 = float4(0.0);
out.f_flags = 0u;
out.f_uv_rect = float4(0.0);
out.f_effects = uint4(0u);
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
}
else
{ {
Core_2D_Primitive p; Core_2D_Primitive p;
p.bounds = _31.primitives[int(gl_InstanceIndex)].bounds; p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
p.color = _31.primitives[int(gl_InstanceIndex)].color; p.color = _75.primitives[int(gl_InstanceIndex)].color;
p.flags = _31.primitives[int(gl_InstanceIndex)].flags; p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
p.rotation_sc = _31.primitives[int(gl_InstanceIndex)].rotation_sc; p.rotation_sc = _75.primitives[int(gl_InstanceIndex)].rotation_sc;
p._pad = _31.primitives[int(gl_InstanceIndex)]._pad; p._pad = _75.primitives[int(gl_InstanceIndex)]._pad;
p.params = _31.primitives[int(gl_InstanceIndex)].params; p.params = _75.primitives[int(gl_InstanceIndex)].params;
p.params2 = _31.primitives[int(gl_InstanceIndex)].params2; p.params2 = _75.primitives[int(gl_InstanceIndex)].params2;
p.uv_rect = _31.primitives[int(gl_InstanceIndex)].uv_rect; p.uv_rect = _75.primitives[int(gl_InstanceIndex)].uv_rect;
p.effects = _31.primitives[int(gl_InstanceIndex)].effects; 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;
@@ -94,27 +105,6 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
out.f_effects = p.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);
} }
else
{
out.f_color = in.v_color;
out.f_local_or_uv = in.v_uv;
out.f_params = float4(0.0);
out.f_params2 = float4(0.0);
out.f_flags = 0u;
out.f_uv_rect = float4(0.0);
out.f_effects = uint4(0u);
float2 _199;
if (_12.mode == 2u)
{
_199 = in.v_position;
}
else
{
_199 = in.v_position * _12.dpi_scale;
}
float2 pos = _199;
out.gl_Position = _12.projection * float4(pos, 0.0, 1.0);
}
return out; return out;
} }
Binary file not shown.
+6 -6
View File
@@ -40,9 +40,9 @@ const uint MAX_KERNEL_PAIRS = 32;
// --- Inputs from vertex shader --- // --- Inputs from vertex shader ---
layout(location = 0) in vec2 p_local; layout(location = 0) in vec2 p_local;
layout(location = 1) in mediump vec4 f_color; layout(location = 1) in mediump vec4 f_color;
layout(location = 2) flat in vec2 f_half_size_ppx; layout(location = 2) flat in vec2 f_half_size;
layout(location = 3) flat in vec4 f_radii_ppx; layout(location = 3) flat in vec4 f_radii;
layout(location = 4) flat in float f_half_feather_ppx; layout(location = 4) flat in float f_half_feather;
// --- Output --- // --- Output ---
layout(location = 0) out vec4 out_color; layout(location = 0) out vec4 out_color;
@@ -123,15 +123,15 @@ void main() {
// ---- Mode 1: composite per-primitive. // ---- Mode 1: composite per-primitive.
// RRect SDF — early discard for fragments well outside the masked region. // RRect SDF — early discard for fragments well outside the masked region.
float d = sdRoundedBox(p_local, f_half_size_ppx, f_radii_ppx); float d = sdRoundedBox(p_local, f_half_size, f_radii);
if (d > f_half_feather_ppx) { if (d > f_half_feather) {
discard; discard;
} }
// fwidth-based normalization for AA (matches main pipeline approach). // fwidth-based normalization for AA (matches main pipeline approach).
float grad_magnitude = max(fwidth(d), 1e-6); float grad_magnitude = max(fwidth(d), 1e-6);
float d_n = d / grad_magnitude; float d_n = d / grad_magnitude;
float h_n = f_half_feather_ppx / grad_magnitude; float h_n = f_half_feather / grad_magnitude;
// Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to // 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 // working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
+16 -16
View File
@@ -24,12 +24,12 @@
layout(location = 0) out vec2 p_local; layout(location = 0) out vec2 p_local;
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1. // f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
layout(location = 1) out mediump vec4 f_color; layout(location = 1) out mediump vec4 f_color;
// f_half_size_ppx: RRect half extents in physical pixels (mode 1 only). // f_half_size: RRect half extents in physical pixels (mode 1 only).
layout(location = 2) flat out vec2 f_half_size_ppx; layout(location = 2) flat out vec2 f_half_size;
// f_radii_ppx: per-corner radii in physical pixels (mode 1 only). // f_radii: per-corner radii in physical pixels (mode 1 only).
layout(location = 3) flat out vec4 f_radii_ppx; layout(location = 3) flat out vec4 f_radii;
// f_half_feather_ppx: SDF anti-aliasing feather in physical pixels (mode 1 only). // f_half_feather: SDF anti-aliasing feather (mode 1 only).
layout(location = 4) flat out float f_half_feather_ppx; layout(location = 4) flat out float f_half_feather;
// --- Uniforms (set 1) --- // --- Uniforms (set 1) ---
// Backdrop pipeline's own uniform block — distinct from the main pipeline's // Backdrop pipeline's own uniform block — distinct from the main pipeline's
@@ -53,10 +53,10 @@ layout(set = 1, binding = 0) uniform Uniforms {
// edge effects (e.g. liquid-glass-style refraction outlines) would be a dedicated // edge effects (e.g. liquid-glass-style refraction outlines) would be a dedicated
// primitive type with its own pipeline rather than a flag bit here. // primitive type with its own pipeline rather than a flag bit here.
struct Gaussian_Blur_Primitive { struct Gaussian_Blur_Primitive {
vec4 bounds; // 0-15: min_xy, max_xy (world-space, logical px) vec4 bounds; // 0-15: min_xy, max_xy (world-space)
vec4 radii_ppx; // 16-31: per-corner radii vec4 radii; // 16-31: per-corner radii (physical px)
vec2 half_size_ppx; // 32-39: RRect half extents vec2 half_size; // 32-39: RRect half extents (physical px)
float half_feather_ppx; // 40-43: SDF anti-aliasing feather float half_feather; // 40-43: SDF anti-aliasing feather (physical px)
uint color; // 44-47: tint, packed RGBA u8x4 uint color; // 44-47: tint, packed RGBA u8x4
}; };
@@ -78,9 +78,9 @@ void main() {
// Mode 0 doesn't read the per-primitive varyings; zero-init for safety. // Mode 0 doesn't read the per-primitive varyings; zero-init for safety.
p_local = vec2(0.0); p_local = vec2(0.0);
f_color = vec4(0.0); f_color = vec4(0.0);
f_half_size_ppx = vec2(0.0); f_half_size = vec2(0.0);
f_radii_ppx = vec4(0.0); f_radii = vec4(0.0);
f_half_feather_ppx = 0.0; f_half_feather = 0.0;
} else { } else {
// ---- Mode 1: V-composite instanced unit-quad over Gaussian_Blur_Primitive ---- // ---- Mode 1: V-composite instanced unit-quad over Gaussian_Blur_Primitive ----
Gaussian_Blur_Primitive p = primitives[gl_InstanceIndex]; Gaussian_Blur_Primitive p = primitives[gl_InstanceIndex];
@@ -101,9 +101,9 @@ void main() {
p_local = (world_pos - center) * dpi_scale; p_local = (world_pos - center) * dpi_scale;
f_color = unpackUnorm4x8(p.color); f_color = unpackUnorm4x8(p.color);
f_half_size_ppx = p.half_size_ppx; f_half_size = p.half_size;
f_radii_ppx = p.radii_ppx; f_radii = p.radii;
f_half_feather_ppx = p.half_feather_ppx; f_half_feather = p.half_feather;
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0); gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
} }
+26 -26
View File
@@ -45,7 +45,7 @@ float sdRegularPolygon(vec2 p, float r, float n) {
return length(p) * cos(bn) - r; return length(p) * cos(bn) - r;
} }
// Coverage from SDF distance using half-feather width (feather_ppx * 0.5, pre-computed on CPU). // Coverage from SDF distance using half-feather width (feather_px * 0.5, pre-computed on CPU).
// Produces a symmetric transition centered on d=0: smoothstep(-h, h, d). // Produces a symmetric transition centered on d=0: smoothstep(-h, h, d).
float sdf_alpha(float d, float h) { float sdf_alpha(float d, float h) {
return 1.0 - smoothstep(-h, h, d); return 1.0 - smoothstep(-h, h, d);
@@ -80,56 +80,56 @@ void main() {
// SDF path — dispatch on kind // SDF path — dispatch on kind
float d = 1e30; float d = 1e30;
float h = 0.5; // half-feather width (physical px); overwritten per shape kind float h = 0.5; // half-feather width; overwritten per shape kind
vec2 half_size_ppx = 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_ppx = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated vec2 p_local = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
if (kind == 1u) { if (kind == 1u) {
// RRect — half_feather_ppx in params2.z // RRect — half_feather in params2.z
vec4 corner_radii_ppx = vec4(f_params.zw, f_params2.xy); vec4 corner_radii = vec4(f_params.zw, f_params2.xy);
h = f_params2.z; h = f_params2.z;
d = sdRoundedBox(p_local_ppx, half_size_ppx, corner_radii_ppx); d = sdRoundedBox(p_local, half_size, corner_radii);
} }
else if (kind == 2u) { else if (kind == 2u) {
// NGon — half_feather_ppx in params.z // NGon — half_feather in params.z
float radius_ppx = f_params.x; float radius = f_params.x;
float sides = f_params.y; float sides = f_params.y;
h = f_params.z; h = f_params.z;
d = sdRegularPolygon(p_local_ppx, radius_ppx, sides); d = sdRegularPolygon(p_local, radius, sides);
half_size_ppx = vec2(radius_ppx); // for gradient UV computation half_size = vec2(radius); // for gradient UV computation
} }
else if (kind == 3u) { else if (kind == 3u) {
// Ellipse — half_feather_ppx in params.z // Ellipse — half_feather in params.z
vec2 radii_ppx = f_params.xy; vec2 ab = f_params.xy;
h = f_params.z; h = f_params.z;
d = sdEllipseApprox(p_local_ppx, radii_ppx); d = sdEllipseApprox(p_local, ab);
half_size_ppx = radii_ppx; // for gradient UV computation half_size = ab; // for gradient UV computation
} }
else if (kind == 4u) { else if (kind == 4u) {
// Ring_Arc — half_feather_ppx in params2.z // Ring_Arc — half_feather in params2.z
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π) // Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π)
float inner_radius_ppx = f_params.x; float inner = f_params.x;
float outer_radius_ppx = f_params.y; float outer = f_params.y;
vec2 n_start = f_params.zw; vec2 n_start = f_params.zw;
vec2 n_end = f_params2.xy; vec2 n_end = f_params2.xy;
uint arc_bits = (flags >> 5u) & 3u; uint arc_bits = (flags >> 5u) & 3u;
h = f_params2.z; h = f_params2.z;
float r = length(p_local_ppx); float r = length(p_local);
d = max(inner_radius_ppx - r, r - outer_radius_ppx); d = max(inner - r, r - outer);
if (arc_bits != 0u) { if (arc_bits != 0u) {
float d_start = dot(p_local_ppx, n_start); float d_start = dot(p_local, n_start);
float d_end = dot(p_local_ppx, n_end); float d_end = dot(p_local, n_end);
float d_wedge = (arc_bits == 1u) float d_wedge = (arc_bits == 1u)
? max(d_start, d_end) // arc ≤ π: intersect half-planes ? max(d_start, d_end) // arc ≤ π: intersect half-planes
: min(d_start, d_end); // arc > π: union half-planes : min(d_start, d_end); // arc > π: union half-planes
d = max(d, d_wedge); d = max(d, d_wedge);
} }
half_size_ppx = vec2(outer_radius_ppx); // for gradient UV computation half_size = vec2(outer); // for gradient UV computation
} }
// --- fwidth-based normalization for correct AA and stroke width --- // --- fwidth-based normalization for correct AA and stroke width ---
@@ -146,18 +146,18 @@ void main() {
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
mediump float t = length(p_local_ppx / half_size_ppx); mediump float t = length(p_local / half_size);
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_effects.z); vec2 direction = unpackHalf2x16(f_effects.z);
mediump float t = dot(p_local_ppx / half_size_ppx, 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) // Textured (bit 0)
vec4 uv_rect = f_uv_rect; vec4 uv_rect = f_uv_rect;
vec2 local_uv = p_local_ppx / half_size_ppx * 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);
} else { } else {
+14 -31
View File
@@ -1,6 +1,6 @@
#version 450 core #version 450 core
// ---------- Vertex attributes (used in all modes) ---------- // ---------- Vertex attributes (used in both modes) ----------
layout(location = 0) in vec2 v_position; layout(location = 0) in vec2 v_position;
layout(location = 1) in vec2 v_uv; layout(location = 1) in vec2 v_uv;
layout(location = 2) in vec4 v_color; layout(location = 2) in vec4 v_color;
@@ -16,18 +16,10 @@ layout(location = 6) flat out vec4 f_uv_rect;
layout(location = 7) flat out uvec4 f_effects; 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) ----------
// Mode values mirror Core_2D_Mode in core_2d.odin:
// 0 = Tessellated v_position is in logical pixels; shader scales by dpi_scale.
// 1 = SDF v_position is a unit-quad corner; world-space comes from
// primitives[gl_InstanceIndex].bounds (logical px). Shader
// scales by dpi_scale.
// 2 = Text v_position is in *physical* pixels already (the CPU baked
// the anchor snap and SDL_ttf glyph offsets, both physical).
// Shader must NOT rescale.
layout(set = 1, binding = 0) uniform Uniforms { layout(set = 1, binding = 0) uniform Uniforms {
mat4 projection; mat4 projection;
float dpi_scale; float dpi_scale;
uint mode; uint mode; // 0 = tessellated, 1 = SDF
}; };
// ---------- SDF primitive storage buffer ---------- // ---------- SDF primitive storage buffer ----------
@@ -52,7 +44,18 @@ layout(std430, set = 0, binding = 0) readonly buffer Core_2D_Primitives {
// ---------- Entry point ---------- // ---------- Entry point ----------
void main() { void main() {
if (mode == 1u) { if (mode == 0u) {
// ---- Mode 0: Tessellated (used for text and arbitrary user geometry) ----
f_color = v_color;
f_local_or_uv = v_uv;
f_params = vec4(0.0);
f_params2 = vec4(0.0);
f_flags = 0u;
f_uv_rect = vec4(0.0);
f_effects = uvec4(0);
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
} else {
// ---- Mode 1: SDF instanced quads ---- // ---- Mode 1: SDF instanced quads ----
Core_2D_Primitive p = primitives[gl_InstanceIndex]; Core_2D_Primitive p = primitives[gl_InstanceIndex];
@@ -83,25 +86,5 @@ void main() {
f_effects = p.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);
} else {
// ---- Mode 0 (Tessellated) and Mode 2 (Text) ----
// Both feed the raw-vertex pipeline (kind 0 in the fragment shader).
// They differ only in what coord space `v_position` is in:
// Mode 0 — logical pixels, scale here by dpi_scale.
// Mode 2 — physical pixels (CPU pre-scaled and snapped to integer
// physical pixels for atlas-aligned bilinear sampling).
// Do NOT rescale.
// `mode` is uniform across the workgroup, so the select compiles to a
// uniform-controlled branch with no SIMT divergence cost.
f_color = v_color;
f_local_or_uv = v_uv;
f_params = vec4(0.0);
f_params2 = vec4(0.0);
f_flags = 0u;
f_uv_rect = vec4(0.0);
f_effects = uvec4(0);
vec2 pos = (mode == 2u) ? v_position : (v_position * dpi_scale);
gl_Position = projection * vec4(pos, 0.0, 1.0);
} }
} }
+10 -40
View File
@@ -21,8 +21,8 @@ auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
// ----- Internal helpers ----- // ----- Internal helpers -----
// Premultiplies the color before storing it on the vertex (see draw package doc's // Color is premultiplied: the tessellated fragment shader passes it through directly
// "Color and blending" section for why). // and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
//INTERNAL //INTERNAL
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex_2D { solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex_2D {
return draw.Vertex_2D{position = position, color = draw.premultiply_color(color)} return draw.Vertex_2D{position = position, color = draw.premultiply_color(color)}
@@ -108,23 +108,16 @@ triangle :: proc(
draw.prepare_shape(layer, vertices[:]) draw.prepare_shape(layer, vertices[:])
} }
// Draw an anti-aliased triangle via extruded edge quads plus corner fan caps. // Draw an anti-aliased triangle via extruded edge quads.
// Interior vertices get the full premultiplied color; outer fringe vertices get BLANK (0,0,0,0). // Interior vertices get the full premultiplied color; outer fringe vertices get BLANK (0,0,0,0).
// The rasterizer linearly interpolates between them, producing a smooth ~1-physical-pixel AA band. // The rasterizer linearly interpolates between them, producing a smooth 1-pixel AA band.
// `aa_ppx` controls the extrusion width in *physical* pixels (default 1.0). The CPU divides by // `aa_px` controls the extrusion width in logical pixels (default 1.0).
// `dpi_scaling` here so the vertex stream stays in logical px; the mode-0 vertex shader scales // This proc emits 21 vertices (3 interior + 6 edge quads × 3 verts each).
// back to physical at draw time. Net AA band is ~aa_ppx physical pixels regardless of DPI.
//
// Topology: 3 interior verts + 6 edge-quad triangles (×3 verts) + 3 corner-fan triangles (×3 verts)
// = 30 verts total. The corner fans plug the wedge gaps that would otherwise appear between
// adjacent edge fringes at each triangle vertex; without them, sharp corners show a small
// background-colored crescent. Apex vertex is full color, both fringe verts are BLANK, so the
// fan rasterizes as an alpha-falloff triangle that blends visually into the adjacent edge bands.
triangle_aa :: proc( triangle_aa :: proc(
layer: ^draw.Layer, layer: ^draw.Layer,
v1, v2, v3: draw.Vec2, v1, v2, v3: draw.Vec2,
color: draw.Color, color: draw.Color,
aa_ppx: f32 = draw.DFT_FEATHER_PPX, aa_px: f32 = draw.DFT_FEATHER_PX,
origin: draw.Vec2 = {}, origin: draw.Vec2 = {},
rotation: f32 = 0, rotation: f32 = 0,
) { ) {
@@ -171,9 +164,7 @@ triangle_aa :: proc(
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y) normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y) normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
// aa_ppx is in physical pixels; divide by dpi_scaling so the extrusion lives in logical-pixel extrude_distance := aa_px * draw.GLOB.dpi_scaling
// space (the mode-0 vertex shader will scale back to physical at draw time).
extrude_distance := aa_ppx / draw.GLOB.dpi_scaling
// Outer fringe vertices: each edge vertex extruded outward // Outer fringe vertices: each edge vertex extruded outward
outer_0_01 := p0 + normal_01 * extrude_distance outer_0_01 := p0 + normal_01 * extrude_distance
@@ -187,8 +178,8 @@ triangle_aa :: proc(
// Outer fringe is BLANK = {0,0,0,0} which is already premul. // Outer fringe is BLANK = {0,0,0,0} which is already premul.
transparent := draw.BLANK transparent := draw.BLANK
// 3 interior + 6 edge-quad tris (×3 verts) + 3 corner-fan tris (×3 verts) = 30 vertices // 3 interior + 6 × 3 edge-quad = 21 vertices
vertices: [30]draw.Vertex_2D vertices: [21]draw.Vertex_2D
// Interior triangle // Interior triangle
vertices[0] = solid_vertex(p0, color) vertices[0] = solid_vertex(p0, color)
@@ -219,27 +210,6 @@ triangle_aa :: proc(
vertices[19] = solid_vertex(outer_0_20, transparent) vertices[19] = solid_vertex(outer_0_20, transparent)
vertices[20] = solid_vertex(outer_2_20, transparent) vertices[20] = solid_vertex(outer_2_20, transparent)
// Corner fan caps: each fills the wedge gap between the two edge fringes meeting at a
// triangle vertex. Apex is full color; both fringe verts are BLANK, so the rasterizer
// produces a smooth alpha falloff across the wedge (matches the adjacent edge-band
// gradients at the shared edges, so the seams are invisible). Vertex order per fan:
// [apex, fringe-from-incoming-edge, fringe-from-outgoing-edge].
// Cap at p0 (between incoming edge p2→p0 and outgoing edge p0→p1)
vertices[21] = solid_vertex(p0, color)
vertices[22] = solid_vertex(outer_0_20, transparent)
vertices[23] = solid_vertex(outer_0_01, transparent)
// Cap at p1 (between incoming edge p0→p1 and outgoing edge p1→p2)
vertices[24] = solid_vertex(p1, color)
vertices[25] = solid_vertex(outer_1_01, transparent)
vertices[26] = solid_vertex(outer_1_12, transparent)
// Cap at p2 (between incoming edge p1→p2 and outgoing edge p2→p0)
vertices[27] = solid_vertex(p2, color)
vertices[28] = solid_vertex(outer_2_12, transparent)
vertices[29] = solid_vertex(outer_2_20, transparent)
draw.prepare_shape(layer, vertices[:]) draw.prepare_shape(layer, vertices[:])
} }
+10
View File
@@ -56,6 +56,16 @@ Texture_Slot :: struct {
// GLOB.pending_texture_releases : [dynamic]Texture_Id // GLOB.pending_texture_releases : [dynamic]Texture_Id
// GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler // GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler
Clay_Image_Data :: struct {
texture_id: Texture_Id,
fit: Fit_Mode,
tint: Color,
}
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
return {texture_id = id, fit = fit, tint = tint}
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Registration ------------- // ----- Registration -------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
+30 -105
View File
@@ -120,52 +120,10 @@ spinlock_try_lock :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool
return lock_acquired return lock_acquired
} }
// Spins until the lock is acquired, relaxing the CPU between attempts.
spinlock_lock :: #force_inline proc "contextless" (lock: ^Spinlock) {
for !spinlock_try_lock(lock) {
intrinsics.cpu_relax()
}
}
spinlock_unlock :: #force_inline proc "contextless" (lock: ^Spinlock) { spinlock_unlock :: #force_inline proc "contextless" (lock: ^Spinlock) {
intrinsics.atomic_store_explicit(lock, false, .Release) intrinsics.atomic_store_explicit(lock, false, .Release)
} }
// Spins until the lock is acquired, then unlocks at the end of the calling scope. Always returns
// true so it can guard a critical section from within an `if`:
//
// if spinlock_guard(&lock) {
// // critical section
// }
@(deferred_in = spinlock_unlock)
spinlock_guard :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool {
spinlock_lock(lock)
return true
}
// Tries to acquire the lock once without spinning. Returns true and unlocks at the end of the
// calling scope if acquired, otherwise returns false and does nothing:
//
// if spinlock_try_guard(&lock) {
// // critical section, entered only if the lock was acquired
// }
@(deferred_in_out = spinlock_try_guard_unlock)
spinlock_try_guard :: #force_inline proc "contextless" (lock: ^Spinlock) -> bool {
return spinlock_try_lock(lock)
}
// Deferred companion of `spinlock_try_guard`; unlocks only when the lock was actually acquired.
@(private)
spinlock_try_guard_unlock :: #force_inline proc "contextless" (lock: ^Spinlock, locked: bool) {
if locked {
spinlock_unlock(lock)
}
}
lock :: proc {
spinlock_lock,
}
try_lock :: proc { try_lock :: proc {
spinlock_try_lock, spinlock_try_lock,
} }
@@ -174,14 +132,6 @@ unlock :: proc {
spinlock_unlock, spinlock_unlock,
} }
guard :: proc {
spinlock_guard,
}
try_guard :: proc {
spinlock_try_guard,
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Tests ------------------------ // ----- Tests ------------------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -189,10 +139,10 @@ import "core:sync"
import "core:testing" import "core:testing"
import "core:thread" import "core:thread"
// Multiple threads will each add 1.0 this many times.
// If any updates are lost due to race conditions, the final sum will be wrong.
@(test) @(test)
test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) { test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
// Multiple threads will each add 1.0 this many times.
// If any updates are lost due to race conditions, the final sum will be wrong.
NUM_THREADS :: 8 NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 10_000 ITERATIONS_PER_THREAD :: 10_000
@@ -234,10 +184,10 @@ test_concurrent_atomic_add_no_lost_updates :: proc(t: ^testing.T) {
testing.expect_value(t, shared_value, expected) testing.expect_value(t, shared_value, expected)
} }
// Start with a known value, multiple threads subtract.
// If any updates are lost due to race conditions, the final result will be wrong.
@(test) @(test)
test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) { test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
// Start with a known value, multiple threads subtract.
// If any updates are lost due to race conditions, the final result will be wrong.
NUM_THREADS :: 8 NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 10_000 ITERATIONS_PER_THREAD :: 10_000
@@ -278,11 +228,11 @@ test_concurrent_atomic_sub_no_lost_updates :: proc(t: ^testing.T) {
testing.expect_value(t, shared_value, 0.0) testing.expect_value(t, shared_value, 0.0)
} }
@(test)
test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
// Each thread multiplies by 2.0 then divides by 2.0. // Each thread multiplies by 2.0 then divides by 2.0.
// Since these are inverses, the final value should equal the starting value // Since these are inverses, the final value should equal the starting value
// regardless of how operations interleave. // regardless of how operations interleave.
@(test)
test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
NUM_THREADS :: 8 NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 10_000 ITERATIONS_PER_THREAD :: 10_000
@@ -324,10 +274,10 @@ test_concurrent_atomic_mul_div_round_trip :: proc(t: ^testing.T) {
testing.expect_value(t, shared_value, 1000.0) testing.expect_value(t, shared_value, 1000.0)
} }
// Verify the f32 type dispatch works correctly under contention.
// Same approach as the f64 add test but with f32.
@(test) @(test)
test_atomic_add_with_f32 :: proc(t: ^testing.T) { test_atomic_add_with_f32 :: proc(t: ^testing.T) {
// Verify the f32 type dispatch works correctly under contention.
// Same approach as the f64 add test but with f32.
NUM_THREADS :: 8 NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 10_000 ITERATIONS_PER_THREAD :: 10_000
@@ -369,6 +319,8 @@ test_atomic_add_with_f32 :: proc(t: ^testing.T) {
testing.expect_value(t, shared_value, expected) testing.expect_value(t, shared_value, expected)
} }
@(test)
test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
// Tests that the memory order passed to atomic_float_op's CAS success condition // Tests that the memory order passed to atomic_float_op's CAS success condition
// provides full ordering guarantees for the entire float operation. // provides full ordering guarantees for the entire float operation.
// //
@@ -378,8 +330,6 @@ test_atomic_add_with_f32 :: proc(t: ^testing.T) {
// //
// NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model. // NOTE: This test may pass even with Relaxed ordering on x86 due to its strong memory model.
// On ARM or other weak-memory architectures, using Relaxed here would likely cause failures. // On ARM or other weak-memory architectures, using Relaxed here would likely cause failures.
@(test)
test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
NUM_READERS :: 4 NUM_READERS :: 4
Shared_State :: struct { Shared_State :: struct {
@@ -476,11 +426,10 @@ test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
} }
} }
// Stress test for every spinlock acquisition variant: N threads contend on a @(test)
// single lock and perform a deliberate non-atomic read-modify-write on shared test_spinlock_try_lock_mutual_exclusion :: proc(t: ^testing.T) {
// data. Each iteration rotates through spinlock_try_lock, spinlock_lock, // Stress test for spinlock_try_lock: N threads spin-acquire the lock and
// spinlock_guard, and spinlock_try_guard so every variant runs concurrently and // perform a deliberate non-atomic read-modify-write on shared data.
// must uphold mutual exclusion on the same lock.
// //
// If mutual exclusion holds: // If mutual exclusion holds:
// - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD // - `counter` ends at exactly NUM_THREADS * ITERATIONS_PER_THREAD
@@ -488,8 +437,6 @@ test_atomic_release_acquire_publish_visibility :: proc(t: ^testing.T) {
// //
// A multi-step RMW (read → relax → write) widens the critical section so // A multi-step RMW (read → relax → write) widens the critical section so
// any failure to exclude is virtually guaranteed to corrupt the counter. // any failure to exclude is virtually guaranteed to corrupt the counter.
@(test)
test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
NUM_THREADS :: 8 NUM_THREADS :: 8
ITERATIONS_PER_THREAD :: 50_000 ITERATIONS_PER_THREAD :: 50_000
@@ -514,9 +461,21 @@ test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
barrier: sync.Barrier barrier: sync.Barrier
sync.barrier_init(&barrier, NUM_THREADS) sync.barrier_init(&barrier, NUM_THREADS)
// The single critical section every acquisition variant must protect. Sharing thread_proc :: proc(th: ^thread.Thread) {
// it guarantees they all stress the exact same non-atomic read-modify-write. ctx := cast(^Thread_Data)th.data
critical_section :: proc(s: ^Shared) { s := ctx.shared
// All threads rendezvous here for maximum contention.
sync.barrier_wait(ctx.barrier)
for _ in 0 ..< ITERATIONS_PER_THREAD {
// Spin on try_lock until we acquire it.
for !spinlock_try_lock(&s.lock) {
intrinsics.cpu_relax()
}
// --- critical section start ---
// Atomically bump the holder count so we can detect overlapping holders. // Atomically bump the holder count so we can detect overlapping holders.
holders := intrinsics.atomic_add_explicit(&s.concurrent_holders, 1, .Relaxed) holders := intrinsics.atomic_add_explicit(&s.concurrent_holders, 1, .Relaxed)
@@ -535,44 +494,10 @@ test_spinlock_mutual_exclusion :: proc(t: ^testing.T) {
s.counter = val + 1 s.counter = val + 1
intrinsics.atomic_sub_explicit(&s.concurrent_holders, 1, .Relaxed) intrinsics.atomic_sub_explicit(&s.concurrent_holders, 1, .Relaxed)
}
thread_proc :: proc(th: ^thread.Thread) { // --- critical section end ---
ctx := cast(^Thread_Data)th.data
s := ctx.shared
// All threads rendezvous here for maximum contention.
sync.barrier_wait(ctx.barrier)
for i in 0 ..< ITERATIONS_PER_THREAD {
// Rotate through every acquisition variant so they all contend on the
// same lock simultaneously and must each uphold mutual exclusion.
switch i & 3 {
case 0:
// Manual spin on try_lock until we acquire it.
for !spinlock_try_lock(&s.lock) {
intrinsics.cpu_relax()
}
critical_section(s)
spinlock_unlock(&s.lock) spinlock_unlock(&s.lock)
case 1:
// Blocking lock that loops internally until acquired.
spinlock_lock(&s.lock)
critical_section(s)
spinlock_unlock(&s.lock)
case 2: // Scoped guard: unlocks automatically at the end of the block.
if spinlock_guard(&s.lock) {
critical_section(s)
}
case 3: // Scoped try-guard: retry until acquired, auto-unlocks on success.
for {
if spinlock_try_guard(&s.lock) {
critical_section(s)
break
}
intrinsics.cpu_relax()
}
}
} }
} }
-42
View File
@@ -2,8 +2,6 @@ package quantity
import "base:intrinsics" import "base:intrinsics"
LITERS_PER_GALLON :: 3.785411784
//----- Liters ---------------------------------- //----- Liters ----------------------------------
Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) { Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
v: V, v: V,
@@ -16,13 +14,6 @@ liters_to_milli_liters :: #force_inline proc "contextless" (
return Milli_Liters(V){liters.v * MILLI} return Milli_Liters(V){liters.v * MILLI}
} }
@(private = "file")
liters_to_gallons :: #force_inline proc "contextless" (
liters: Liters($V),
) -> Gallons(V) where intrinsics.type_is_float(V) {
return Gallons(V){liters.v / LITERS_PER_GALLON}
}
//----- Milliliters ---------------------------------- //----- Milliliters ----------------------------------
Milli_Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) { Milli_Liters :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
v: V, v: V,
@@ -35,34 +26,17 @@ milli_liters_to_liters :: #force_inline proc "contextless" (
return Liters(V){milli_liters.v / MILLI} return Liters(V){milli_liters.v / MILLI}
} }
//----- Gallons ----------------------------------
Gallons :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
v: V,
}
@(private = "file")
gallons_to_liters :: #force_inline proc "contextless" (
gallons: Gallons($V),
) -> Liters(V) where intrinsics.type_is_float(V) {
return Liters(V){gallons.v * LITERS_PER_GALLON}
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Conversion Overloads ------------------------ // ----- Conversion Overloads ------------------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
to_liters :: proc { to_liters :: proc {
milli_liters_to_liters, milli_liters_to_liters,
gallons_to_liters,
} }
to_milli_liters :: proc { to_milli_liters :: proc {
liters_to_milli_liters, liters_to_milli_liters,
} }
to_gallons :: proc {
liters_to_gallons,
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Tests ------------------------ // ----- Tests ------------------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -83,19 +57,3 @@ test_milli_liters_to_liters :: proc(t: ^testing.T) {
testing.expect_value(t, liters, Liters(int){12}) testing.expect_value(t, liters, Liters(int){12})
} }
@(test)
test_gallons_to_liters :: proc(t: ^testing.T) {
gallons := Gallons(f32){1}
liters := to_liters(gallons)
testing.expect(t, liters.v > 3.78 && liters.v < 3.79)
}
@(test)
test_liters_to_gallons :: proc(t: ^testing.T) {
liters := Liters(f32){3.785411784}
gallons := to_gallons(liters)
testing.expect(t, gallons.v > 0.99 && gallons.v < 1.01)
}
-52
View File
@@ -2,58 +2,6 @@ package quantity
import "base:intrinsics" import "base:intrinsics"
//----- Liters Per Minute ----------------------------------
Liters_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) { Liters_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
v: V, v: V,
} }
@(private = "file")
liters_per_minute_to_gallons_per_minute :: #force_inline proc "contextless" (
liters_per_minute: Liters_Per_Minute($V),
) -> Gallons_Per_Minute(V) where intrinsics.type_is_float(V) {
return Gallons_Per_Minute(V){liters_per_minute.v / LITERS_PER_GALLON}
}
//----- Gallons Per Minute ----------------------------------
Gallons_Per_Minute :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
v: V,
}
@(private = "file")
gallons_per_minute_to_liters_per_minute :: #force_inline proc "contextless" (
gallons_per_minute: Gallons_Per_Minute($V),
) -> Liters_Per_Minute(V) where intrinsics.type_is_float(V) {
return Liters_Per_Minute(V){gallons_per_minute.v * LITERS_PER_GALLON}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Conversion Overloads ------------------------
// ---------------------------------------------------------------------------------------------------------------------
to_liters_per_minute :: proc {
gallons_per_minute_to_liters_per_minute,
}
to_gallons_per_minute :: proc {
liters_per_minute_to_gallons_per_minute,
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Tests ------------------------
// ---------------------------------------------------------------------------------------------------------------------
import "core:testing"
@(test)
test_gallons_per_minute_to_liters_per_minute :: proc(t: ^testing.T) {
gallons_per_minute := Gallons_Per_Minute(f32){1}
liters_per_minute := to_liters_per_minute(gallons_per_minute)
testing.expect(t, liters_per_minute.v > 3.78 && liters_per_minute.v < 3.79)
}
@(test)
test_liters_per_minute_to_gallons_per_minute :: proc(t: ^testing.T) {
liters_per_minute := Liters_Per_Minute(f32){3.785411784}
gallons_per_minute := to_gallons_per_minute(liters_per_minute)
testing.expect(t, gallons_per_minute.v > 0.99 && gallons_per_minute.v < 1.01)
}
+22 -140
View File
@@ -57,6 +57,11 @@ CornerRadius :: struct {
bottomRight: c.float, bottomRight: c.float,
} }
BorderData :: struct {
width: u32,
color: Color,
}
ElementId :: struct { ElementId :: struct {
id: u32, id: u32,
offset: u32, offset: u32,
@@ -64,12 +69,6 @@ ElementId :: struct {
stringId: String, stringId: String,
} }
ElementIdArray :: struct {
capacity: i32,
length: i32,
internalArray: [^]ElementId,
}
when ODIN_OS == .Windows { when ODIN_OS == .Windows {
EnumBackingType :: u32 EnumBackingType :: u32
} else { } else {
@@ -84,8 +83,6 @@ RenderCommandType :: enum EnumBackingType {
Image, Image,
ScissorStart, ScissorStart,
ScissorEnd, ScissorEnd,
OverlayColorStart,
OverlayColorEnd,
Custom, Custom,
} }
@@ -141,92 +138,6 @@ BorderElementConfig :: struct {
width: BorderWidth, width: BorderWidth,
} }
TransitionData :: struct {
boundingBox: BoundingBox,
backgroundColor: Color,
overlayColor: Color,
borderColor: Color,
borderWidth: BorderWidth,
}
TransitionState :: enum c.int {
Idle,
Entering,
Transitioning,
Exiting,
}
TransitionProperty :: enum c.int {
X,
Y,
Width,
Height,
BackgroundColor,
OverlayColor,
CornerRadius,
BorderColor,
BorderWidth,
}
TransitionPropertyFlags :: bit_set[TransitionProperty;c.int]
TransitionPropertyPosition :: TransitionPropertyFlags{.X, .Y}
TransitionPropertyDimensions :: TransitionPropertyFlags{.Width, .Height}
TransitionPropertyBoundingBox :: TransitionPropertyPosition + TransitionPropertyDimensions
TransitionPropertyBorder :: TransitionPropertyFlags{.BorderColor, .BorderWidth}
TransitionCallbackArguments :: struct {
transitionState: TransitionState,
initial: TransitionData,
current: ^TransitionData,
target: TransitionData,
elapsedTime: f32,
duration: f32,
properties: TransitionPropertyFlags,
}
TransitionEnterTriggerType :: enum EnumBackingType {
SkipOnFirstParentFrame,
TriggerOnFirstParentFrame,
}
TransitionExitTriggerType :: enum EnumBackingType {
SkipWhenParentExits,
TriggerWhenParentExits,
}
TransitionInteractionHandlingType :: enum EnumBackingType {
DisableInteractionsWhileTransitioningPosition,
AllowInteractionsWhileTransitioningPosition,
}
ExitTransitionSiblingOrdering :: enum EnumBackingType {
UnderneathSiblings,
NaturalOrder,
AboveSiblings,
}
TransitionElementConfig :: struct {
handler: proc "c" (args: TransitionCallbackArguments) -> bool,
duration: f32,
properties: TransitionPropertyFlags,
interactionHandling: TransitionInteractionHandlingType,
enter: struct {
setInitialState: proc "c" (
initialState: TransitionData,
properties: TransitionPropertyFlags,
) -> TransitionData,
trigger: TransitionEnterTriggerType,
},
exit: struct {
setFinalState: proc "c" (
finalState: TransitionData,
properties: TransitionPropertyFlags,
) -> TransitionData,
trigger: TransitionExitTriggerType,
siblingOrdering: ExitTransitionSiblingOrdering,
},
}
ClipElementConfig :: struct { ClipElementConfig :: struct {
horizontal: bool, // clip overflowing elements on the "X" axis horizontal: bool, // clip overflowing elements on the "X" axis
vertical: bool, // clip overflowing elements on the "Y" axis vertical: bool, // clip overflowing elements on the "Y" axis
@@ -304,15 +215,6 @@ CustomRenderData :: struct {
customData: rawptr, customData: rawptr,
} }
ClipRenderData :: struct {
horizontal: bool,
vertical: bool,
}
OverlayColorRenderData :: struct {
color: Color,
}
BorderRenderData :: struct { BorderRenderData :: struct {
color: Color, color: Color,
cornerRadius: CornerRadius, cornerRadius: CornerRadius,
@@ -325,8 +227,6 @@ RenderCommandData :: struct #raw_union {
image: ImageRenderData, image: ImageRenderData,
custom: CustomRenderData, custom: CustomRenderData,
border: BorderRenderData, border: BorderRenderData,
clip: ClipRenderData,
overlayColor: OverlayColorRenderData,
} }
RenderCommand :: struct { RenderCommand :: struct {
@@ -438,9 +338,9 @@ ClayArray :: struct($type: typeid) {
} }
ElementDeclaration :: struct { ElementDeclaration :: struct {
id: ElementId,
layout: LayoutConfig, layout: LayoutConfig,
backgroundColor: Color, backgroundColor: Color,
overlayColor: Color,
cornerRadius: CornerRadius, cornerRadius: CornerRadius,
aspectRatio: AspectRatioElementConfig, aspectRatio: AspectRatioElementConfig,
image: ImageElementConfig, image: ImageElementConfig,
@@ -448,7 +348,6 @@ ElementDeclaration :: struct {
custom: CustomElementConfig, custom: CustomElementConfig,
clip: ClipElementConfig, clip: ClipElementConfig,
border: BorderElementConfig, border: BorderElementConfig,
transition: TransitionElementConfig,
userData: rawptr, userData: rawptr,
} }
@@ -461,7 +360,6 @@ ErrorType :: enum EnumBackingType {
FloatingContainerParentNotFound, FloatingContainerParentNotFound,
PercentageOver1, PercentageOver1,
InternalError, InternalError,
UnbalancedOpenClose,
} }
ErrorData :: struct { ErrorData :: struct {
@@ -480,27 +378,23 @@ Context :: struct {} // opaque structure, only use as a pointer
@(link_prefix = "Clay_", default_calling_convention = "c") @(link_prefix = "Clay_", default_calling_convention = "c")
foreign Clay { foreign Clay {
_OpenElement :: proc() --- _OpenElement :: proc() ---
_OpenElementWithId :: proc(id: ElementId) ---
_CloseElement :: proc() --- _CloseElement :: proc() ---
MinMemorySize :: proc() -> u32 --- MinMemorySize :: proc() -> u32 ---
CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena --- CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena ---
SetPointerState :: proc(position: Vector2, pointerDown: bool) --- SetPointerState :: proc(position: Vector2, pointerDown: bool) ---
GetPointerState :: proc() -> PointerData ---
Initialize :: proc(arena: Arena, layoutDimensions: Dimensions, errorHandler: ErrorHandler) -> ^Context --- Initialize :: proc(arena: Arena, layoutDimensions: Dimensions, errorHandler: ErrorHandler) -> ^Context ---
GetCurrentContext :: proc() -> ^Context --- GetCurrentContext :: proc() -> ^Context ---
SetCurrentContext :: proc(ctx: ^Context) --- SetCurrentContext :: proc(ctx: ^Context) ---
UpdateScrollContainers :: proc(enableDragScrolling: bool, scrollDelta: Vector2, deltaTime: c.float) --- UpdateScrollContainers :: proc(enableDragScrolling: bool, scrollDelta: Vector2, deltaTime: c.float) ---
SetLayoutDimensions :: proc(dimensions: Dimensions) --- SetLayoutDimensions :: proc(dimensions: Dimensions) ---
BeginLayout :: proc() --- BeginLayout :: proc() ---
EndLayout :: proc(deltaTime: c.float) -> ClayArray(RenderCommand) --- EndLayout :: proc() -> ClayArray(RenderCommand) ---
GetOpenElementId :: proc() -> u32 ---
GetElementId :: proc(id: String) -> ElementId --- GetElementId :: proc(id: String) -> ElementId ---
GetElementIdWithIndex :: proc(id: String, index: u32) -> ElementId --- GetElementIdWithIndex :: proc(id: String, index: u32) -> ElementId ---
GetElementData :: proc(id: ElementId) -> ElementData --- GetElementData :: proc(id: ElementId) -> ElementData ---
Hovered :: proc() -> bool --- Hovered :: proc() -> bool ---
OnHover :: proc(onHoverFunction: proc "c" (id: ElementId, pointerData: PointerData, userData: rawptr), userData: rawptr) --- OnHover :: proc(onHoverFunction: proc "c" (id: ElementId, pointerData: PointerData, userData: rawptr), userData: rawptr) ---
PointerOver :: proc(id: ElementId) -> bool --- PointerOver :: proc(id: ElementId) -> bool ---
GetPointerOverIds :: proc() -> ElementIdArray ---
GetScrollOffset :: proc() -> Vector2 --- GetScrollOffset :: proc() -> Vector2 ---
GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData --- GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData ---
SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) --- SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) ---
@@ -514,15 +408,15 @@ foreign Clay {
GetMaxMeasureTextCacheWordCount :: proc() -> i32 --- GetMaxMeasureTextCacheWordCount :: proc() -> i32 ---
SetMaxMeasureTextCacheWordCount :: proc(maxMeasureTextCacheWordCount: i32) --- SetMaxMeasureTextCacheWordCount :: proc(maxMeasureTextCacheWordCount: i32) ---
ResetMeasureTextCache :: proc() --- ResetMeasureTextCache :: proc() ---
EaseOut :: proc(arguments: TransitionCallbackArguments) -> bool ---
} }
@(link_prefix = "Clay_", default_calling_convention = "c", private) @(link_prefix = "Clay_", default_calling_convention = "c", private)
foreign Clay { foreign Clay {
_ConfigureOpenElement :: proc(config: ElementDeclaration) --- _ConfigureOpenElement :: proc(config: ElementDeclaration) ---
_HashString :: proc(key: String, seed: u32) -> ElementId --- _HashString :: proc(key: String, offset: u32, seed: u32) -> ElementId ---
_HashStringWithOffset :: proc(key: String, index: u32, seed: u32) -> ElementId --- _OpenTextElement :: proc(text: String, textConfig: ^TextElementConfig) ---
_OpenTextElement :: proc(text: String, textConfig: TextElementConfig) --- _StoreTextElementConfig :: proc(config: TextElementConfig) -> ^TextElementConfig ---
_GetParentElementId :: proc() -> u32 ---
} }
ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool { ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
@@ -531,37 +425,25 @@ ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
} }
@(deferred_none = _CloseElement) @(deferred_none = _CloseElement)
UI_WithId :: proc(id: ElementId) -> proc(config: ElementDeclaration) -> bool { UI :: proc() -> proc (config: ElementDeclaration) -> bool {
_OpenElementWithId(id)
return ConfigureOpenElement
}
@(deferred_none = _CloseElement)
UI_AutoId :: proc() -> proc(config: ElementDeclaration) -> bool {
_OpenElement() _OpenElement()
return ConfigureOpenElement return ConfigureOpenElement
} }
UI :: proc { Text :: proc($text: string, config: ^TextElementConfig) {
UI_WithId,
UI_AutoId,
}
Text :: proc {
TextStatic,
TextDynamic,
}
TextStatic :: proc($text: string, config: TextElementConfig) {
wrapped := MakeString(text) wrapped := MakeString(text)
wrapped.isStaticallyAllocated = true wrapped.isStaticallyAllocated = true
_OpenTextElement(wrapped, config) _OpenTextElement(wrapped, config)
} }
TextDynamic :: proc(text: string, config: TextElementConfig) { TextDynamic :: proc(text: string, config: ^TextElementConfig) {
_OpenTextElement(MakeString(text), config) _OpenTextElement(MakeString(text), config)
} }
TextConfig :: proc(config: TextElementConfig) -> ^TextElementConfig {
return _StoreTextElementConfig(config)
}
PaddingAll :: proc(allPadding: u16) -> Padding { PaddingAll :: proc(allPadding: u16) -> Padding {
return { left = allPadding, right = allPadding, top = allPadding, bottom = allPadding } return { left = allPadding, right = allPadding, top = allPadding, bottom = allPadding }
} }
@@ -578,11 +460,11 @@ CornerRadiusAll :: proc(radius: f32) -> CornerRadius {
return CornerRadius{radius, radius, radius, radius} return CornerRadius{radius, radius, radius, radius}
} }
SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax = {}) -> SizingAxis { SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
return SizingAxis{type = SizingType.Fit, constraints = {sizeMinMax = sizeMinMax}} return SizingAxis{type = SizingType.Fit, constraints = {sizeMinMax = sizeMinMax}}
} }
SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax = {}) -> SizingAxis { SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis {
return SizingAxis{type = SizingType.Grow, constraints = {sizeMinMax = sizeMinMax}} return SizingAxis{type = SizingType.Grow, constraints = {sizeMinMax = sizeMinMax}}
} }
@@ -599,9 +481,9 @@ MakeString :: proc(label: string) -> String {
} }
ID :: proc(label: string, index: u32 = 0) -> ElementId { ID :: proc(label: string, index: u32 = 0) -> ElementId {
return _HashString(MakeString(label), index) return _HashString(MakeString(label), index, 0)
} }
ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId { ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId {
return _HashStringWithOffset(MakeString(label), index, GetOpenElementId()) return _HashString(MakeString(label), index, _GetParentElementId())
} }
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
"character_width": 180,
"sort_imports": true,
"tabs": false
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+4 -4
View File
@@ -68,9 +68,9 @@ main :: proc() {
db_handle: mdb.Dbi db_handle: mdb.Dbi
// Put transaction // Put transaction
key := 7 key := 7
key_val := mdb.pod_val(&key) key_val := mdb.blittable_val(&key)
put_data := 12 put_data := 12
put_data_val := mdb.pod_val(&put_data) put_data_val := mdb.blittable_val(&put_data)
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle)) mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
mdb.panic_on_err(mdb.dbi_open(txn_handle, nil, {}, &db_handle)) mdb.panic_on_err(mdb.dbi_open(txn_handle, nil, {}, &db_handle))
mdb.panic_on_err(mdb.put(txn_handle, db_handle, &key_val, &put_data_val, {})) mdb.panic_on_err(mdb.put(txn_handle, db_handle, &key_val, &put_data_val, {}))
@@ -80,7 +80,7 @@ main :: proc() {
data_val: mdb.Val data_val: mdb.Val
mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle)) mdb.panic_on_err(mdb.txn_begin(environment, nil, {}, &txn_handle))
mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val, &data_val)) mdb.panic_on_err(mdb.get(txn_handle, db_handle, &key_val, &data_val))
data_cpy := mdb.pod_copy(data_val, int) data_cpy := mdb.blittable_copy(&data_val, int)
mdb.txn_abort(txn_handle) mdb.panic_on_err(mdb.txn_commit(txn_handle))
fmt.println("Get result:", data_cpy) fmt.println("Get result:", data_cpy)
} }
+15 -52
View File
@@ -169,86 +169,58 @@ import "core:fmt"
import "core:reflect" import "core:reflect"
import "core:sys/posix" import "core:sys/posix"
import b "../../basic"
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Added Odin Helpers ------------------------ // ----- Added Odin Helpers ------------------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// Wrap a POD value's bytes as an LMDB Val. // Wrap a blittable value's bytes as an LMDB Val.
// T must be a contiguous type with no indirection (no pointers, slices, strings, maps, etc.). // T must be a contiguous type with no indirection (no pointers, slices, strings, maps, etc.).
pod_val :: #force_inline proc(val_ptr: ^$T) -> Val { blittable_val :: #force_inline proc(val_ptr: ^$T) -> Val {
when ODIN_DEBUG {
fmt.assertf( fmt.assertf(
reflect.has_no_indirections(type_info_of(T)), reflect.has_no_indirections(type_info_of(T)),
"pod_val: type '%v' contains indirection and cannot be stored directly in LMDB", "blitval: type '%v' contains indirection and cannot be stored directly in LMDB",
typeid_of(T), typeid_of(T),
) )
}
return Val{size_of(T), val_ptr} return Val{size_of(T), val_ptr}
} }
// Reads a POD T out of the LMDB memory map by copying it into caller // Reads a blittable T out of the LMDB memory map by copying it into caller
// storage. The returned T has no lifetime tie to the transaction. // storage. The returned T has no lifetime tie to the transaction.
pod_copy :: #force_inline proc(val: Val, $T: typeid) -> T { blittable_copy :: #force_inline proc(val: ^Val, $T: typeid) -> T {
when ODIN_DEBUG {
fmt.assertf( fmt.assertf(
reflect.has_no_indirections(type_info_of(T)), reflect.has_no_indirections(type_info_of(T)),
"pod_copy: type '%v' contains indirection and cannot be read directly from LMDB", "blitval_copy: type '%v' contains indirection and cannot be read directly from LMDB",
typeid_of(T), typeid_of(T),
) )
}
when b.ODIN_BOUNDS_CHECK {
fmt.assertf(
val.size == size_of(T),
"size_of(%v) (%v) != val.size (%v)",
typeid_of(T),
size_of(T),
val.size,
)
}
return (cast(^T)val.data)^ return (cast(^T)val.data)^
} }
// Zero-copy pointer view into the LMDB memory map as a ^T. // Zero-copy pointer view into the LMDB memory map as a ^T.
// Useful for large POD types where you want to read individual fields // Useful for large blittable types where you want to read individual fields
// without copying the entire value (e.g. ptr.timestamp, ptr.flags). // without copying the entire value (e.g. ptr.timestamp, ptr.flags).
// MUST NOT be written through — writes either segfault (default env mode) // MUST NOT be written through — writes either segfault (default env mode)
// or silently corrupt the database (ENV_WRITEMAP). // or silently corrupt the database (ENV_WRITEMAP).
// MUST NOT be retained past txn_commit, txn_abort, or any subsequent write // MUST NOT be retained past txn_commit, txn_abort, or any subsequent write
// operation on the same env — the pointer is invalidated. // operation on the same env — the pointer is invalidated.
pod_view :: #force_inline proc(val: Val, $T: typeid) -> ^T { blittable_view :: #force_inline proc(val: ^Val, $T: typeid) -> ^T {
when ODIN_DEBUG {
fmt.assertf( fmt.assertf(
reflect.has_no_indirections(type_info_of(T)), reflect.has_no_indirections(type_info_of(T)),
"pod_view: type '%v' contains indirection and cannot be viewed directly from LMDB", "blitval_view: type '%v' contains indirection and cannot be viewed directly from LMDB",
typeid_of(T), typeid_of(T),
) )
}
when b.ODIN_BOUNDS_CHECK {
fmt.assertf(
val.size == size_of(T),
"size_of(%v) (%v) != val.size (%v)",
typeid_of(T),
size_of(T),
val.size,
)
}
return cast(^T)val.data return cast(^T)val.data
} }
// Wrap a slice of POD elements as an LMDB Val for use with put/get. // Wrap a slice of blittable elements as an LMDB Val for use with put/get.
// T must be a contiguous type with no indirection. // T must be a contiguous type with no indirection.
// The caller's slice must remain valid (not freed, not resized) for the // The caller's slice must remain valid (not freed, not resized) for the
// duration of the put call that consumes this Val. // duration of the put call that consumes this Val.
pod_slice_val :: #force_inline proc(s: []$T) -> Val { slice_val :: #force_inline proc(s: []$T) -> Val {
when ODIN_DEBUG {
fmt.assertf( fmt.assertf(
reflect.has_no_indirections(type_info_of(T)), reflect.has_no_indirections(type_info_of(T)),
"pod_slice_val: element type '%v' contains indirection and cannot be stored directly in LMDB", "slice_val: element type '%v' contains indirection and cannot be stored directly in LMDB",
typeid_of(T), typeid_of(T),
) )
}
return Val{uint(len(s) * size_of(T)), raw_data(s)} return Val{uint(len(s) * size_of(T)), raw_data(s)}
} }
@@ -259,21 +231,12 @@ pod_slice_val :: #force_inline proc(s: []$T) -> Val {
// MUST be copied (e.g. slice.clone) if it needs to outlive the current // MUST be copied (e.g. slice.clone) if it needs to outlive the current
// transaction; the view is invalidated by txn_commit, txn_abort, or any // transaction; the view is invalidated by txn_commit, txn_abort, or any
// subsequent write operation on the same env. // subsequent write operation on the same env.
pod_slice_view :: #force_inline proc(val: Val, $T: typeid) -> []T { slice_view :: #force_inline proc(val: ^Val, $T: typeid) -> []T {
when ODIN_DEBUG {
fmt.assertf( fmt.assertf(
reflect.has_no_indirections(type_info_of(T)), reflect.has_no_indirections(type_info_of(T)),
"pod_slice_view: element type '%v' contains indirection and cannot be read directly from LMDB", "slice_view: element type '%v' contains indirection and cannot be read directly from LMDB",
typeid_of(T), typeid_of(T),
) )
fmt.assertf(
val.size % size_of(T) == 0,
"pod_slice_view: val.size (%v) is not a multiple of size_of(%v) (%v)",
val.size,
typeid_of(T),
size_of(T),
)
}
return (cast([^]T)val.data)[:val.size / size_of(T)] return (cast([^]T)val.data)[:val.size / size_of(T)]
} }
@@ -290,7 +253,7 @@ string_val :: #force_inline proc(s: string) -> Val {
// MUST be copied (e.g. strings.clone) if it needs to outlive the current // MUST be copied (e.g. strings.clone) if it needs to outlive the current
// transaction; the view is invalidated by txn_commit, txn_abort, or any // transaction; the view is invalidated by txn_commit, txn_abort, or any
// subsequent write operation on the same env. // subsequent write operation on the same env.
string_view :: #force_inline proc(val: Val) -> string { string_view :: #force_inline proc(val: ^Val) -> string {
return string((cast([^]u8)val.data)[:val.size]) return string((cast([^]u8)val.data)[:val.size])
} }