Major reorg

This commit is contained in:
Zachary Levy
2026-04-30 18:49:38 -07:00
parent fd64bc01bf
commit 87d4c9a0b5
16 changed files with 2293 additions and 2259 deletions
+22 -22
View File
@@ -5,7 +5,7 @@ Clay UI integration.
## Current state ## Current state
The renderer uses a single unified `Pipeline_2D_Base` (`TRIANGLELIST` pipeline) with two submission The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with two submission
modes dispatched by a push constant: modes dispatched by a push constant:
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry. Used for text (indexed draws into - **Mode 0 (Tessellated):** Vertex buffer contains real geometry. Used for text (indexed draws into
@@ -15,10 +15,10 @@ modes dispatched by a push constant:
shader premultiplies the texture sample (`t.rgb *= t.a`) and computes `out = color * t`. shader premultiplies the texture sample (`t.rgb *= t.a`) and computes `out = color * t`.
- **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
`Base_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
shader reads `primitives[gl_InstanceIndex]`, computes world-space position from unit quad corners + shader reads `primitives[gl_InstanceIndex]`, computes world-space position from unit quad corners +
primitive bounds. The fragment shader dispatches on `Shape_Kind` (encoded in the low byte of primitive bounds. The fragment shader dispatches on `Shape_Kind` (encoded in the low byte of
`Base_2D_Primitive.flags`) to evaluate one of four signed distance functions: `Core_2D_Primitive.flags`) to evaluate one of four signed distance functions:
- **RRect** (kind 1) — `sdRoundedBox` with per-corner radii. Covers rectangles (sharp or rounded), - **RRect** (kind 1) — `sdRoundedBox` with per-corner radii. Covers rectangles (sharp or rounded),
circles (uniform radii = half-size), and line segments / capsules (rotated RRect with uniform circles (uniform radii = half-size), and line segments / capsules (rotated RRect with uniform
radii = half-thickness). Covers filled, outlined, textured, and gradient-filled variants. radii = half-thickness). Covers filled, outlined, textured, and gradient-filled variants.
@@ -28,9 +28,9 @@ modes dispatched by a push constant:
normals. Covers full rings, partial arcs, and pie slices (`inner_radius = 0`). normals. Covers full rings, partial arcs, and pie slices (`inner_radius = 0`).
All SDF shapes support fill, outline, solid color, 2-color linear gradients, 2-color radial All SDF shapes support fill, outline, solid color, 2-color linear gradients, 2-color radial
gradients, and texture fills via `Shape_Flags` (see `pipeline_2d_base.odin`). The texture UV rect gradients, and texture fills via `Shape_Flags` (see `core_2d.odin`). The texture UV rect
(`uv_rect: [4]f32`) and the gradient/outline parameters (`effects: Gradient_Outline`) live in their (`uv_rect: [4]f32`) and the gradient/outline parameters (`effects: Gradient_Outline`) live in their
own 16-byte slots in `Base_2D_Primitive`, so a primitive can carry texture and outline simultaneously. own 16-byte slots in `Core_2D_Primitive`, so a primitive can carry texture and outline simultaneously.
Gradient and texture remain mutually exclusive at the fill-source level (a Brush variant chooses one Gradient and texture remain mutually exclusive at the fill-source level (a Brush variant chooses one
or the other) since they share the worst-case fragment-shader register path. or the other) since they share the worst-case fragment-shader register path.
@@ -433,19 +433,19 @@ our design:
### Main pipeline: SDF + tessellated (unified) ### Main pipeline: SDF + tessellated (unified)
The main pipeline serves two submission modes through a single `TRIANGLELIST` pipeline and a single The main pipeline serves two submission modes through a single `TRIANGLELIST` pipeline and a single
vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms` push constant vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push constant
(`Draw_Mode.Tessellated = 0`, `Draw_Mode.SDF = 1`), pushed per draw call via `push_globals`. The (`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`), pushed per draw call via `push_globals`. The
vertex shader branches on this uniform to select the tessellated or SDF code path. vertex shader branches on this uniform to select the tessellated or SDF code path.
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry. Used for text - **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry. Used for text
(SDL_ttf atlas sampling), triangles, triangle fans/strips, single-pixel points, and any (SDL_ttf atlas sampling), triangles, triangle fans/strips, 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
`Base_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. functions.
Both modes use the same fragment shader. The fragment shader checks `Shape_Kind` (low byte of Both modes use the same fragment shader. The fragment shader checks `Shape_Kind` (low byte of
`Base_2D_Primitive.flags`): kind 0 (`Solid`) is the tessellated path, which premultiplies the texture `Core_2D_Primitive.flags`): kind 0 (`Solid`) is the tessellated path, which premultiplies the texture
sample and computes `out = color * t`; kinds 14 dispatch to one of four SDF functions (RRect, NGon, sample and computes `out = color * t`; kinds 14 dispatch to one of four SDF functions (RRect, NGon,
Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based on `Shape_Flags` bits. Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based on `Shape_Flags` bits.
@@ -454,7 +454,7 @@ Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based on `Shap
CPU-side adaptive tessellation for curved shapes (the current approach) has three problems: CPU-side adaptive tessellation for curved shapes (the current approach) has three problems:
1. **Vertex bandwidth.** A rounded rectangle with four corner arcs produces ~250 vertices × 20 bytes 1. **Vertex bandwidth.** A rounded rectangle with four corner arcs produces ~250 vertices × 20 bytes
= 5 KB. An SDF rounded rectangle is one `Base_2D_Primitive` struct (96 bytes) plus 4 shared = 5 KB. An SDF rounded rectangle is one `Core_2D_Primitive` struct (96 bytes) plus 4 shared
unit-quad vertices. That is roughly a 50× reduction per shape. unit-quad vertices. That is roughly a 50× reduction per shape.
2. **Quality.** Tessellated curves are piecewise-linear approximations. At high DPI or under 2. **Quality.** Tessellated curves are piecewise-linear approximations. At high DPI or under
@@ -486,7 +486,7 @@ SDF primitives are submitted via a GPU storage buffer indexed by `gl_InstanceInd
shader, rather than encoding per-primitive data redundantly in vertex attributes. This follows the shader, rather than encoding per-primitive data redundantly in vertex attributes. This follows the
pattern used by both Zed GPUI and vger-rs. pattern used by both Zed GPUI and vger-rs.
Each SDF shape is described by a single `Base_2D_Primitive` struct (96 bytes) in the storage Each SDF shape is described by a single `Core_2D_Primitive` struct (96 bytes) in the storage
buffer. The vertex shader reads `primitives[gl_InstanceIndex]`, computes the quad corner position buffer. The vertex shader reads `primitives[gl_InstanceIndex]`, computes the quad corner position
from the unit vertex and the primitive's bounds, and passes shape parameters to the fragment shader from the unit vertex and the primitive's bounds, and passes shape parameters to the fragment shader
via `flat` interpolated varyings. via `flat` interpolated varyings.
@@ -501,10 +501,10 @@ in a draw call has the same mode — so it is effectively free on all modern GPU
#### Shape kinds and SDF dispatch #### Shape kinds and SDF dispatch
The fragment shader dispatches on `Shape_Kind` (low byte of `Base_2D_Primitive.flags`) to evaluate The fragment shader dispatches on `Shape_Kind` (low byte of `Core_2D_Primitive.flags`) to evaluate
one of four signed distance functions. The `Shape_Kind` enum and per-kind `*_Params` structs are one of four signed distance functions. The `Shape_Kind` enum, per-kind `*_Params` structs, and
defined in `pipeline_2d_base.odin`. CPU-side drawing procs in `shapes.odin` build the appropriate CPU-side drawing procs all live in `core_2d.odin`. The drawing procs build the appropriate
`Base_2D_Primitive` and set the kind automatically: `Core_2D_Primitive` and set the kind automatically:
Each user-facing shape proc accepts a `Brush` union (color, linear gradient, radial gradient, Each user-facing shape proc accepts a `Brush` union (color, linear gradient, radial gradient,
or textured fill) as its fill source, plus optional outline parameters. The procs map to SDF or textured fill) as its fill source, plus optional outline parameters. The procs map to SDF
@@ -522,7 +522,7 @@ kinds as follows:
| `ring` (pie slice) | `Ring_Arc` | Annular radial SDF | `inner_radius = 0`, angular clipping via `start/end_angle` | | `ring` (pie slice) | `Ring_Arc` | Annular radial SDF | `inner_radius = 0`, angular clipping via `start/end_angle` |
The `Shape_Flags` bit set controls per-primitive rendering mode (outline, gradient, texture, rotation, The `Shape_Flags` bit set controls per-primitive rendering mode (outline, gradient, texture, rotation,
arc geometry). See the `Shape_Flag` enum in `pipeline_2d_base.odin` for the authoritative flag arc geometry). See the `Shape_Flag` enum in `core_2d.odin` for the authoritative flag
definitions and bit assignments. definitions and bit assignments.
**What stays tessellated:** **What stays tessellated:**
@@ -662,7 +662,7 @@ for the factor-selection table and rationale).
#### Submission-order trade-off #### Submission-order trade-off
Within Pass A and Pass B, sub-batches render in the user's submission order. What the bracket model 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 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 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: bracket), not at its submission position. Worked example:
@@ -675,7 +675,7 @@ draw.gaussian_blur(layer, panelB, sigma=12) // 4 Backdrop → Bracket
draw.text(layer, "label", ...) // 5 Text → Pass B (drawn ON TOP of both panels) draw.text(layer, "label", ...) // 5 Text → Pass B (drawn ON TOP of both panels)
``` ```
In this layer, panelB does *not* see card_red — even though card_red was submitted before panelB — In this layer, panelB does _not_ see card_red — even though card_red was submitted before panelB —
because both backdrops sample `source_texture` as it stood at the bracket entry, which is after because both backdrops sample `source_texture` as it stood at the bracket entry, which is after
Pass A and before card_red has rendered. card_red ends up on top of panelA, not underneath it. Pass A and before card_red has rendered. card_red ends up on top of panelA, not underneath it.
@@ -709,7 +709,7 @@ abstraction. This matches the cost/complexity envelope of iOS `UIVisualEffectVie
The vertex struct is unchanged from the current 20-byte layout: The vertex struct is unchanged from the current 20-byte layout:
``` ```
Vertex :: struct { Vertex_2D :: struct {
position: [2]f32, // 0: screen-space position position: [2]f32, // 0: screen-space position
uv: [2]f32, // 8: atlas UV (text) or unused (shapes) uv: [2]f32, // 8: atlas UV (text) or unused (shapes)
color: Color, // 16: u8x4, GPU-normalized to float color: Color, // 16: u8x4, GPU-normalized to float
@@ -721,10 +721,10 @@ draws, `position` carries actual world-space geometry. For SDF draws, `position`
corners (0,0 to 1,1) and the vertex shader computes world-space position from the storage-buffer corners (0,0 to 1,1) and the vertex shader computes world-space position from the storage-buffer
primitive's bounds. primitive's bounds.
The `Base_2D_Primitive` struct for SDF shapes lives in the storage buffer, not in vertex attributes: The `Core_2D_Primitive` struct for SDF shapes lives in the storage buffer, not in vertex attributes:
``` ```
Base_2D_Primitive :: struct { Core_2D_Primitive :: struct {
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y bounds: [4]f32, // 0: min_x, min_y, max_x, max_y
color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8 color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
@@ -738,7 +738,7 @@ Base_2D_Primitive :: struct {
``` ```
`Shape_Params` is a `#raw_union` over `RRect_Params`, `NGon_Params`, `Ellipse_Params`, and `Shape_Params` is a `#raw_union` over `RRect_Params`, `NGon_Params`, `Ellipse_Params`, and
`Ring_Arc_Params` (plus a `raw: [8]f32` view), defined in `pipeline_2d_base.odin`. Each SDF kind `Ring_Arc_Params` (plus a `raw: [8]f32` view), defined in `core_2d.odin`. Each SDF kind
writes its own params variant; the fragment shader reads the appropriate fields based on `Shape_Kind`. writes its own params variant; the fragment shader reads the appropriate fields based on `Shape_Kind`.
`Gradient_Outline` is a 16-byte struct containing `gradient_color: Color`, `outline_color: Color`, `Gradient_Outline` is a 16-byte struct containing `gradient_color: Color`, `outline_color: Color`,
`gradient_dir_sc: u32` (packed f16 cos/sin pair), and `outline_packed: u32` (packed f16 outline `gradient_dir_sc: u32` (packed f16 cos/sin pair), and `outline_packed: u32` (packed f16 outline
+260 -236
View File
@@ -5,151 +5,79 @@ import "core:math"
import "core:mem" import "core:mem"
import sdl "vendor:sdl3" import sdl "vendor:sdl3"
// Adaptive downsample design (Flutter-style). // This file hosts the backdrop subsystem: any visual effect that samples the current
// framebuffer as input. Today the only implemented effect is Gaussian blur (frosted glass);
// future effects (refraction, mirror, etc.) will live here too.
// //
// The bracket picks a downsample factor per-sigma-group, not as a global constant. The choice // The file is split into two top-level sections:
// is driven by Flutter's `CalculateScale` formula in
// impeller/entity/contents/filters/gaussian_blur_filter_contents.cc (originally from Skia's
// GrBlurUtils): downsample so that the sigma in working-resolution pixels stays in the
// 2..4 range. This keeps the kernel reach wide enough to hide high-frequency artifacts from
// the bilinear upsample at the composite, while keeping the kernel's discrete tap count
// small (≤3σ reach → ≈12 paired taps).
// //
// The full table, in physical pixels (sigma_logical * dpi_scaling): // 1. Shared backdrop infrastructure — bracket coordination, source_texture lifecycle,
// sub-batch scanners. These are general to any backdrop effect: every backdrop effect
// needs a snapshot of the framebuffer (source_texture) and needs to participate in the
// bracket render-pass-boundary scheduling. When a second effect is added, its
// per-effect resources go in their own section like the Gaussian blur one below; this
// shared section stays.
// //
// sigma_phys ≤ 4 → factor = 1 (no downsample; source is sampled directly) // 2. Gaussian blur — the only effect implemented today. Owns its own PSOs, working
// sigma_phys ≤ 8 → factor = 2 // textures (downsample / h_blur), per-primitive storage layout, kernel math, and
// sigma_phys > 8 → factor = 4 (capped) // bracket-runner inner loop. None of this is shared with future backdrop effects: a
// refraction shader would have its own PSO, its own primitive struct, and likely
// wouldn't need the downsample/h_blur intermediates at all.
// //
// Capped at factor=4 to favor visual quality over bandwidth at the high end. Larger factors // The `Backdrop` struct currently holds resources from both categories; field-group
// (8 and 16) would lose more high-frequency detail than the kernel can mask even with the // comments inside it mark which are which. When a second effect lands the struct will be
// H+V split, and the bandwidth saving is small (the work region also shrinks quadratically, // split, but doing that pre-emptively means inventing a per-effect dispatch protocol on
// so most of the savings are already captured at factor=4). // speculation. Better to keep the conflation visible (and labeled) until concrete needs
// // shape the design.
// Working textures are sized at full swapchain resolution to support factor=1. Larger factors
// just write to a smaller sub-rect via viewport-limited rendering. Memory cost: full-res
// working textures (2 textures, RGBA8) is roughly 16 MB at 1080p, 64 MB at 4K. On modern
// GPUs this is well within budget; on Mali Valhall SBCs it's negligible against unified-
// memory headroom.
//
// The shaders read the factor as a uniform. The downsample shader has three paths (factor=1
// identity, factor=2 single bilinear tap, factor>=4 four bilinear taps with offsets scaling
// by factor/4). The V-composite mode of backdrop_blur.frag uses inv_downsample_factor to
// scale full-res frag coords down to working-res UV.
// Maximum number of (weight, offset) pairs in a single blur kernel. Each pair represents
// the linear-sampling pair adjustment (one bilinear fetch covering two adjacent texels);
// pair[0] is the center weight with offset 0. With 32 pairs we cover up to 63 input texels
// (1 center + 31 paired symmetric taps × 2 texels each), enough for sigma values well past
// the 4..24 typical UI range. Must match MAX_KERNEL_PAIRS in shaders/source/backdrop_blur.frag.
MAX_BACKDROP_KERNEL_PAIRS :: 32
// Backdrop_Primitive is the GPU-side per-primitive storage layout. Mirrors the GLSL std430
// struct in shaders/source/backdrop_blur.vert. Field order is chosen so std430 alignment
// rules pack the struct to a clean 48-byte natural layout (no implicit padding): vec4
// members come first (16-byte aligned at any offset), then vec2, then scalars. The total is
// a multiple of 16 so the std430 array stride matches size_of(...) exactly.
//
// Backdrop primitives are RRect-only: rectangles, rounded rectangles, and circles
// (via uniform_radii) are all expressible. Rotation is intentionally omitted — backdrop
// sampling is in screen space, so a rotated mask over a stationary blur sample would look
// visually wrong. iOS, CSS backdrop-filter, and Flutter BackdropFilter all enforce this
// implicitly; we enforce it explicitly by leaving no rotation field.
//
// Outline is also intentionally omitted. A specialized edge effect (e.g. liquid-glass-style
// refraction outlines) would be implemented as a dedicated primitive type with its own
// pipeline rather than tacked onto this one as a flag bit.
Backdrop_Primitive :: struct {
bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy)
radii: [4]f32, // 16: 16 — per-corner radii in physical pixels (BR, TR, BL, TL)
half_size: [2]f32, // 32: 8 — RRect half extents (physical px)
half_feather: f32, // 40: 4 — feather_px * 0.5 (SDF anti-aliasing)
color: Color, // 44: 4 — tint, packed RGBA u8x4
}
#assert(size_of(Backdrop_Primitive) == 48)
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Uniform blocks ---------------- // ----- Shared backdrop infrastructure ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// Vertex uniforms for the unified blur PSO (mode 0 = H-blur, mode 1 = V-composite). //INTERNAL
// Matches the GLSL Uniforms block in shaders/source/backdrop_blur.vert. The downsample Backdrop :: struct {
// PSO has no vertex uniforms. // -- Shared across all backdrop effects --
Backdrop_Vert_Uniforms :: struct {
projection: matrix[4, 4]f32, // 0: 64 — screen-space ortho (mode 1 only; mode 0 ignores)
dpi_scale: f32, // 64: 4
mode: u32, // 68: 4 — 0 = H-blur fullscreen tri; 1 = V-composite instanced quads
_pad0: [2]f32, // 72: 8 — std140 vec4 alignment pad
}
// Fragment uniforms for the downsample PSO. Matches Uniforms block in // When any backdrop draw exists this frame, the entire frame renders into source_texture
// shaders/source/backdrop_downsample.frag. // instead of the swapchain. Acts as the bracket's snapshot input by virtue of already
Backdrop_Downsample_Frag_Uniforms :: struct { // containing the pre-bracket frame. Copied to the swapchain at frame end.
inv_source_size: [2]f32, // 0: 8 — 1.0 / source_texture pixel dimensions (full-res) source_texture: ^sdl.GPUTexture,
downsample_factor: u32, // 8: 4 — 1, 2, or 4 (selects identity / 1-tap / 4-tap path in shader)
_pad0: u32, // 12: 4
}
// Fragment uniforms for the unified blur PSO (mode 0 + mode 1). Matches the GLSL Uniforms // Cached pixel dimensions for resize-detection in `ensure_backdrop_textures`.
// block in shaders/source/backdrop_blur.frag. The kernel array holds the linear-sampling cached_width: u32,
// pair coefficients computed CPU-side via `compute_blur_kernel`. cached_height: u32,
Backdrop_Frag_Uniforms :: struct {
inv_working_size: [2]f32, // 0: 8 — 1.0 / working-resolution texture dimensions
pair_count: u32, // 8: 4 — number of (weight, offset) pairs; pair[0] is center
mode: u32, // 12: 4 — 0 = H-blur, 1 = V-composite (must match vert mode)
direction: [2]f32, // 16: 8 — (1,0) for H-blur, (0,1) for V-composite
inv_downsample_factor: f32, // 24: 4 — 1.0 / downsample_factor (mode 1 only; mode 0 ignores)
_pad0: f32, // 28: 4
kernel: [MAX_BACKDROP_KERNEL_PAIRS][4]f32, // 32: 512 — .x = weight, .y = offset (texels)
}
// --------------------------------------------------------------------------------------------------------------------- // Linear-clamp sampler used for sampling source_texture (and Gaussian blur's working
// ----- Pipeline --------------- // textures). Linear filtering is required by the Gaussian linear-sampling pair trick;
// --------------------------------------------------------------------------------------------------------------------- // any future backdrop effect that samples source_texture with bilinear interpolation
// can reuse this sampler. Clamp avoids edge-bleed at work-region boundaries.
sampler: ^sdl.GPUSampler,
// -- Gaussian blur effect --
Pipeline_2D_Backdrop :: struct {
// Two graphics pipelines. The downsample PSO is a single-bilinear-sample fullscreen pass; // Two graphics pipelines. The downsample PSO is a single-bilinear-sample fullscreen pass;
// the blur PSO is mode-branched (H-blur fullscreen + V-composite instanced) and shares // the blur PSO is mode-branched (H-blur fullscreen + V-composite instanced) and shares
// one shader program for both modes via a uniform `mode` selector. // one shader program for both modes via a uniform `mode` selector.
downsample_pipeline: ^sdl.GPUGraphicsPipeline, downsample_pipeline: ^sdl.GPUGraphicsPipeline,
blur_pipeline: ^sdl.GPUGraphicsPipeline, blur_pipeline: ^sdl.GPUGraphicsPipeline,
// Per-instance Backdrop_Primitive storage buffer. Grows on demand via grow_buffer_if_needed. // Per-instance Gaussian_Blur_Primitive storage buffer. Grows on demand via grow_buffer_if_needed.
// All backdrop primitives across all layers in a frame share this single buffer; sub-batches // All backdrop primitives across all layers in a frame share this single buffer; sub-batches
// reference into it by offset. // reference into it by offset.
primitive_buffer: Buffer, primitive_buffer: Buffer,
// Working textures, allocated once at swapchain resolution and recreated only on resize. // Working textures, allocated once at swapchain resolution and recreated only on resize.
// All three are sized at full swapchain resolution and single-sample. Larger downsample // Both are sized at full swapchain resolution and single-sample. Larger downsample
// factors fill only a sub-rect via viewport-limited rendering (see file-header comment). // factors fill only a sub-rect via viewport-limited rendering (see file-header comment
// source_texture — when any backdrop draw exists this frame, the entire frame renders // on adaptive downsampling in the Gaussian blur section below).
// here instead of the swapchain. Copied to the swapchain at frame
// end. Acts as the bracket's snapshot input by virtue of already
// containing the pre-bracket frame.
// downsample_texture — written by the downsample PSO. Read by the blur PSO in mode 0. // downsample_texture — written by the downsample PSO. Read by the blur PSO in mode 0.
// h_blur_texture — written by the blur PSO in mode 0. Read by the blur PSO in mode 1. // h_blur_texture — written by the blur PSO in mode 0. Read by the blur PSO in mode 1.
source_texture: ^sdl.GPUTexture,
downsample_texture: ^sdl.GPUTexture, downsample_texture: ^sdl.GPUTexture,
h_blur_texture: ^sdl.GPUTexture, h_blur_texture: ^sdl.GPUTexture,
// Cached pixel dimensions for resize-detection in `ensure_backdrop_textures`.
cached_width: u32,
cached_height: u32,
// Linear-clamp sampler used for all backdrop sampling. Linear filtering is required by the
// linear-sampling pair trick (one bilinear fetch covers two adjacent texels). Clamp avoids
// edge-bleed at the work-region boundary.
sampler: ^sdl.GPUSampler,
} }
@(private) //INTERNAL
create_pipeline_2d_backdrop :: proc( create_backdrop :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> (pipeline: Backdrop, ok: bool) {
device: ^sdl.GPUDevice,
window: ^sdl.Window,
) -> (
pipeline: Pipeline_2D_Backdrop,
ok: bool,
) {
// On failure, clean up any partially-created resources. // On failure, clean up any partially-created resources.
defer if !ok { defer if !ok {
if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler) if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler)
@@ -307,10 +235,10 @@ create_pipeline_2d_backdrop :: proc(
return pipeline, false return pipeline, false
} }
//----- Storage buffer for Backdrop_Primitive instances ------------- //----- Storage buffer for Gaussian_Blur_Primitive instances -------------
pipeline.primitive_buffer = create_buffer( pipeline.primitive_buffer = create_buffer(
device, device,
size_of(Backdrop_Primitive) * BUFFER_INIT_SIZE, size_of(Gaussian_Blur_Primitive) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
) or_return ) or_return
@@ -331,12 +259,12 @@ create_pipeline_2d_backdrop :: proc(
return pipeline, false return pipeline, false
} }
log.debug("Done creating backdrop pipeline") log.debug("Done creating backdrop subsystem")
return pipeline, true return pipeline, true
} }
@(private) //INTERNAL
destroy_pipeline_2d_backdrop :: proc(device: ^sdl.GPUDevice, pipeline: ^Pipeline_2D_Backdrop) { destroy_backdrop :: proc(device: ^sdl.GPUDevice, pipeline: ^Backdrop) {
if pipeline.h_blur_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.h_blur_texture) if pipeline.h_blur_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.h_blur_texture)
if pipeline.downsample_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.downsample_texture) if pipeline.downsample_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.downsample_texture)
if pipeline.source_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.source_texture) if pipeline.source_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.source_texture)
@@ -346,20 +274,22 @@ destroy_pipeline_2d_backdrop :: proc(device: ^sdl.GPUDevice, pipeline: ^Pipeline
if pipeline.downsample_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.downsample_pipeline) if pipeline.downsample_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.downsample_pipeline)
} }
// --------------------------------------------------------------------------------------------------------------------- //----- Working texture management ----------------------------------
// ----- Working texture management ----
// ---------------------------------------------------------------------------------------------------------------------
// Allocate (or reallocate, on resize) the three working textures that the backdrop bracket // Allocate (or reallocate, on resize) the three working textures that the backdrop bracket
// uses. All three are sized at full swapchain resolution, single-sample, share the swapchain // uses. All three are sized at full swapchain resolution, single-sample, share the swapchain
// format, and need {.COLOR_TARGET, .SAMPLER} usage so they can be written by render passes // format, and need {.COLOR_TARGET, .SAMPLER} usage so they can be written by render passes
// and read by subsequent passes. // and read by subsequent passes.
// //
// `source_texture` is shared infrastructure (used by every backdrop effect).
// `downsample_texture` and `h_blur_texture` are Gaussian-blur-specific intermediates; a
// future backdrop effect with no downsample/blur prep would skip them.
//
// Recreates on dimension change only — same-size frames hit the early-out and skip GPU // Recreates on dimension change only — same-size frames hit the early-out and skip GPU
// resource churn. // resource churn.
@(private) //INTERNAL
ensure_backdrop_textures :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, width, height: u32) { ensure_backdrop_textures :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, width, height: u32) {
pipeline := &GLOB.pipeline_2d_backdrop pipeline := &GLOB.backdrop
if pipeline.source_texture != nil && pipeline.cached_width == width && pipeline.cached_height == height { if pipeline.source_texture != nil && pipeline.cached_width == width && pipeline.cached_height == height {
return return
} }
@@ -449,10 +379,138 @@ ensure_backdrop_textures :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureF
pipeline.cached_height = height pipeline.cached_height = height
} }
//----- Frame / layer scanners ----------------------------------
// Returns true if any sub-batch in any layer this frame is .Backdrop kind. Called once at the
// top of `end()` to decide whether to route the whole frame to source_texture.
// O(total sub-batches) but with an early-exit on the first hit, so typical cost is tiny.
//INTERNAL
frame_has_backdrop :: proc() -> bool {
for &batch in GLOB.tmp_sub_batches {
if batch.kind == .Backdrop do return true
}
return false
}
// Returns the absolute index of the first .Backdrop sub-batch in the layer's sub-batch range,
// or -1 if the layer has no backdrops. The index is into GLOB.tmp_sub_batches (not relative to
// layer.sub_batch_start), to match how draw_layer's render-range helpers consume it.
//INTERNAL
find_first_backdrop_in_layer :: proc(layer: ^Layer) -> int {
for i in 0 ..< layer.sub_batch_len {
abs_idx := layer.sub_batch_start + i
if GLOB.tmp_sub_batches[abs_idx].kind == .Backdrop do return int(abs_idx)
}
return -1
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Kernel computation ------------ // ----- Gaussian blur ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// Adaptive downsample design (Flutter-style).
//
// The bracket picks a downsample factor per-sigma-group, not as a global constant. The choice
// is driven by Flutter's `CalculateScale` formula in
// impeller/entity/contents/filters/gaussian_blur_filter_contents.cc (originally from Skia's
// GrBlurUtils): downsample so that the sigma in working-resolution pixels stays in the
// 2..4 range. This keeps the kernel reach wide enough to hide high-frequency artifacts from
// the bilinear upsample at the composite, while keeping the kernel's discrete tap count
// small (≤3σ reach → ≈12 paired taps).
//
// The full table, in physical pixels (sigma_logical * dpi_scaling):
//
// sigma_phys ≤ 4 → factor = 1 (no downsample; source is sampled directly)
// sigma_phys ≤ 8 → factor = 2
// sigma_phys > 8 → factor = 4 (capped)
//
// Capped at factor=4 to favor visual quality over bandwidth at the high end. Larger factors
// (8 and 16) would lose more high-frequency detail than the kernel can mask even with the
// H+V split, and the bandwidth saving is small (the work region also shrinks quadratically,
// so most of the savings are already captured at factor=4).
//
// Working textures are sized at full swapchain resolution to support factor=1. Larger factors
// just write to a smaller sub-rect via viewport-limited rendering. Memory cost: full-res
// working textures (2 textures, RGBA8) is roughly 16 MB at 1080p, 64 MB at 4K. On modern
// GPUs this is well within budget; on Mali Valhall SBCs it's negligible against unified-
// memory headroom.
//
// The shaders read the factor as a uniform. The downsample shader has three paths (factor=1
// identity, factor=2 single bilinear tap, factor>=4 four bilinear taps with offsets scaling
// by factor/4). The V-composite mode of backdrop_blur.frag uses inv_downsample_factor to
// scale full-res frag coords down to working-res UV.
//----- GPU types ----------------------------------
// Maximum number of (weight, offset) pairs in a single blur kernel. Each pair represents
// the linear-sampling pair adjustment (one bilinear fetch covering two adjacent texels);
// pair[0] is the center weight with offset 0. With 32 pairs we cover up to 63 input texels
// (1 center + 31 paired symmetric taps × 2 texels each), enough for sigma values well past
// the 4..24 typical UI range. Must match MAX_KERNEL_PAIRS in shaders/source/backdrop_blur.frag.
//INTERNAL
MAX_GAUSSIAN_BLUR_KERNEL_PAIRS :: 32
// Gaussian_Blur_Primitive is the GPU-side per-primitive storage layout. Mirrors the GLSL std430
// struct in shaders/source/backdrop_blur.vert. Field order is chosen so std430 alignment
// rules pack the struct to a clean 48-byte natural layout (no implicit padding): vec4
// members come first (16-byte aligned at any offset), then vec2, then scalars. The total is
// a multiple of 16 so the std430 array stride matches size_of(...) exactly.
//
// Gaussian blur primitives are RRect-only: rectangles, rounded rectangles, and circles
// (via uniform_radii) are all expressible. Rotation is intentionally omitted — backdrop
// sampling is in screen space, so a rotated mask over a stationary blur sample would look
// visually wrong. iOS, CSS backdrop-filter, and Flutter BackdropFilter all enforce this
// implicitly; we enforce it explicitly by leaving no rotation field.
//
// Outline is also intentionally omitted. A specialized edge effect (e.g. liquid-glass-style
// refraction outlines) would be implemented as a dedicated primitive type with its own
// pipeline rather than tacked onto this one as a flag bit.
//INTERNAL
Gaussian_Blur_Primitive :: struct {
bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy)
radii: [4]f32, // 16: 16 — per-corner radii in physical pixels (BR, TR, BL, TL)
half_size: [2]f32, // 32: 8 — RRect half extents (physical px)
half_feather: f32, // 40: 4 — feather_px * 0.5 (SDF anti-aliasing)
color: Color, // 44: 4 — tint, packed RGBA u8x4
}
#assert(size_of(Gaussian_Blur_Primitive) == 48)
// Vertex uniforms for the unified blur PSO (mode 0 = H-blur, mode 1 = V-composite).
// Matches the GLSL Uniforms block in shaders/source/backdrop_blur.vert. The downsample
// PSO has no vertex uniforms.
//INTERNAL
Gaussian_Blur_Vert_Uniforms :: struct {
projection: matrix[4, 4]f32, // 0: 64 — screen-space ortho (mode 1 only; mode 0 ignores)
dpi_scale: f32, // 64: 4
mode: u32, // 68: 4 — 0 = H-blur fullscreen tri; 1 = V-composite instanced quads
_pad0: [2]f32, // 72: 8 — std140 vec4 alignment pad
}
// Fragment uniforms for the downsample PSO. Matches Uniforms block in
// shaders/source/backdrop_downsample.frag.
//INTERNAL
Gaussian_Blur_Downsample_Frag_Uniforms :: struct {
inv_source_size: [2]f32, // 0: 8 — 1.0 / source_texture pixel dimensions (full-res)
downsample_factor: u32, // 8: 4 — 1, 2, or 4 (selects identity / 1-tap / 4-tap path in shader)
_pad0: u32, // 12: 4
}
// Fragment uniforms for the unified blur PSO (mode 0 + mode 1). Matches the GLSL Uniforms
// block in shaders/source/backdrop_blur.frag. The kernel array holds the linear-sampling
// pair coefficients computed CPU-side via `compute_blur_kernel`.
//INTERNAL
Gaussian_Blur_Frag_Uniforms :: struct {
inv_working_size: [2]f32, // 0: 8 — 1.0 / working-resolution texture dimensions
pair_count: u32, // 8: 4 — number of (weight, offset) pairs; pair[0] is center
mode: u32, // 12: 4 — 0 = H-blur, 1 = V-composite (must match vert mode)
direction: [2]f32, // 16: 8 — (1,0) for H-blur, (0,1) for V-composite
inv_downsample_factor: f32, // 24: 4 — 1.0 / downsample_factor (mode 1 only; mode 0 ignores)
_pad0: f32, // 28: 4
kernel: [MAX_GAUSSIAN_BLUR_KERNEL_PAIRS][4]f32, // 32: 512 — .x = weight, .y = offset (texels)
}
//----- Kernel computation ----------------------------------
// Compute Gaussian blur kernel weights with the linear-sampling pair adjustment. // Compute Gaussian blur kernel weights with the linear-sampling pair adjustment.
// Adapted from RAD Debugger's r_d3d11_g_blur_shader_src CPU-side coefficient generation // Adapted from RAD Debugger's r_d3d11_g_blur_shader_src CPU-side coefficient generation
// and Daniel Rákos's "Efficient Gaussian blur with linear sampling" article. // and Daniel Rákos's "Efficient Gaussian blur with linear sampling" article.
@@ -480,18 +538,23 @@ ensure_backdrop_textures :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureF
// from it, rather than the inverse (RAD Debugger's algorithm passes a tap count and derives // from it, rather than the inverse (RAD Debugger's algorithm passes a tap count and derives
// `stdev = (blur_count-1)/2`). Taking σ directly matches what callers expect when they read // `stdev = (blur_count-1)/2`). Taking σ directly matches what callers expect when they read
// "gaussian_sigma" — passing tap count under that name was a footgun. // "gaussian_sigma" — passing tap count under that name was a footgun.
@(private) //INTERNAL
compute_blur_kernel :: proc(sigma: f32, kernel: ^[MAX_BACKDROP_KERNEL_PAIRS][4]f32) -> (pair_count: u32) { compute_blur_kernel :: proc(
sigma: f32,
kernel: ^[MAX_GAUSSIAN_BLUR_KERNEL_PAIRS][4]f32,
) -> (
pair_count: u32,
) {
if sigma <= 0 { if sigma <= 0 {
kernel[0] = {1, 0, 0, 0} kernel[0] = {1, 0, 0, 0}
return 1 return 1
} }
// Per-side discrete tap count: ceil(3*sigma) + 1 (center + 3σ reach on each side). // Per-side discrete tap count: ceil(3*sigma) + 1 (center + 3σ reach on each side).
// Cap at the storage budget. With MAX_BACKDROP_KERNEL_PAIRS=32 each pair collapses 2 // Cap at the storage budget. With MAX_GAUSSIAN_BLUR_KERNEL_PAIRS=32 each pair collapses 2
// discrete taps via linear-sampling, so max discrete taps per side = 1 + 31*2 = 63. // discrete taps via linear-sampling, so max discrete taps per side = 1 + 31*2 = 63.
discrete_taps := u32(math.ceil(3 * sigma)) + 1 discrete_taps := u32(math.ceil(3 * sigma)) + 1
max_taps := u32(MAX_BACKDROP_KERNEL_PAIRS - 1) * 2 + 1 max_taps := u32(MAX_GAUSSIAN_BLUR_KERNEL_PAIRS - 1) * 2 + 1
if discrete_taps > max_taps do discrete_taps = max_taps if discrete_taps > max_taps do discrete_taps = max_taps
if discrete_taps < 2 { if discrete_taps < 2 {
// Sigma was so small that 3σ < 1 texel; degenerate to a sharp sample. // Sigma was so small that 3σ < 1 texel; degenerate to a sharp sample.
@@ -501,7 +564,7 @@ compute_blur_kernel :: proc(sigma: f32, kernel: ^[MAX_BACKDROP_KERNEL_PAIRS][4]f
// Compute discrete weights[i] = exp(-i² / (2σ²)). The inv_root prefactor cancels in the // Compute discrete weights[i] = exp(-i² / (2σ²)). The inv_root prefactor cancels in the
// final normalization, so we skip it. // final normalization, so we skip it.
weights: [MAX_BACKDROP_KERNEL_PAIRS * 2]f32 = {} weights: [MAX_GAUSSIAN_BLUR_KERNEL_PAIRS * 2]f32 = {}
two_sigma_sq := 2 * sigma * sigma two_sigma_sq := 2 * sigma * sigma
total: f32 = 0 total: f32 = 0
for i in 0 ..< discrete_taps { for i in 0 ..< discrete_taps {
@@ -535,38 +598,9 @@ compute_blur_kernel :: proc(sigma: f32, kernel: ^[MAX_BACKDROP_KERNEL_PAIRS][4]f
return pair_count return pair_count
} }
// ---------------------------------------------------------------------------------------------------------------------
// ----- Uniform push helpers ----------
// ---------------------------------------------------------------------------------------------------------------------
// Push the Backdrop_Vert_Uniforms block to the vertex stage at slot 0.
@(private)
push_backdrop_vert_globals :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, width: f32, height: f32, mode: u32) {
uniforms := Backdrop_Vert_Uniforms {
projection = ortho_rh(left = 0.0, top = 0.0, right = width, bottom = height, near = -1.0, far = 1.0),
dpi_scale = GLOB.dpi_scaling,
mode = mode,
}
sdl.PushGPUVertexUniformData(cmd_buffer, 0, &uniforms, size_of(Backdrop_Vert_Uniforms))
}
// Push the Backdrop_Downsample_Frag_Uniforms block to the fragment stage at slot 0.
@(private)
push_backdrop_downsample_frag_globals :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer,
source_width, source_height: u32,
downsample_factor: u32,
) {
uniforms := Backdrop_Downsample_Frag_Uniforms {
inv_source_size = {1.0 / f32(source_width), 1.0 / f32(source_height)},
downsample_factor = downsample_factor,
}
sdl.PushGPUFragmentUniformData(cmd_buffer, 0, &uniforms, size_of(Backdrop_Downsample_Frag_Uniforms))
}
// Pick a downsample factor for a given sigma. See the file-header comment for the table and // Pick a downsample factor for a given sigma. See the file-header comment for the table and
// rationale. Returned values: {1, 2, 4}. // rationale. Returned values: {1, 2, 4}.
@(private) //INTERNAL
compute_backdrop_downsample_factor :: proc(sigma_logical: f32) -> u32 { compute_backdrop_downsample_factor :: proc(sigma_logical: f32) -> u32 {
sigma_phys := sigma_logical * GLOB.dpi_scaling sigma_phys := sigma_logical * GLOB.dpi_scaling
switch { switch {
@@ -576,80 +610,76 @@ compute_backdrop_downsample_factor :: proc(sigma_logical: f32) -> u32 {
} }
} }
// Push the Backdrop_Frag_Uniforms block (kernel + pass mode/direction) to the fragment stage at slot 0. //----- Uniform push helpers ----------------------------------
@(private)
push_backdrop_blur_frag_globals :: proc( // Push the Gaussian_Blur_Vert_Uniforms block to the vertex stage at slot 0.
cmd_buffer: ^sdl.GPUCommandBuffer, //INTERNAL
uniforms: ^Backdrop_Frag_Uniforms, push_backdrop_vert_globals :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, width: f32, height: f32, mode: u32) {
) { uniforms := Gaussian_Blur_Vert_Uniforms {
sdl.PushGPUFragmentUniformData(cmd_buffer, 0, uniforms, size_of(Backdrop_Frag_Uniforms)) projection = ortho_rh(left = 0.0, top = 0.0, right = width, bottom = height, near = -1.0, far = 1.0),
dpi_scale = GLOB.dpi_scaling,
mode = mode,
}
sdl.PushGPUVertexUniformData(cmd_buffer, 0, &uniforms, size_of(Gaussian_Blur_Vert_Uniforms))
} }
// --------------------------------------------------------------------------------------------------------------------- // Push the Gaussian_Blur_Downsample_Frag_Uniforms block to the fragment stage at slot 0.
// ----- Storage-buffer upload --------- //INTERNAL
// --------------------------------------------------------------------------------------------------------------------- push_backdrop_downsample_frag_globals :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer,
source_width, source_height: u32,
downsample_factor: u32,
) {
uniforms := Gaussian_Blur_Downsample_Frag_Uniforms {
inv_source_size = {1.0 / f32(source_width), 1.0 / f32(source_height)},
downsample_factor = downsample_factor,
}
sdl.PushGPUFragmentUniformData(cmd_buffer, 0, &uniforms, size_of(Gaussian_Blur_Downsample_Frag_Uniforms))
}
// Upload all Backdrop_Primitive instances staged this frame to the backdrop pipeline's storage // Push the Gaussian_Blur_Frag_Uniforms block (kernel + pass mode/direction) to the fragment stage at slot 0.
// buffer. Mirrors the SDF primitive upload in pipeline_2d_base.odin's `upload`. Called from //INTERNAL
push_backdrop_blur_frag_globals :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer,
uniforms: ^Gaussian_Blur_Frag_Uniforms,
) {
sdl.PushGPUFragmentUniformData(cmd_buffer, 0, uniforms, size_of(Gaussian_Blur_Frag_Uniforms))
}
//----- Storage-buffer upload ----------------------------------
// Upload all Gaussian_Blur_Primitive instances staged this frame to the backdrop subsystem's storage
// buffer. Mirrors the SDF primitive upload in core_2d.odin's `upload`. Called from
// `end()` inside the same copy pass that uploads vertices/indices/SDF primitives. // `end()` inside the same copy pass that uploads vertices/indices/SDF primitives.
@(private) //INTERNAL
upload_backdrop_primitives :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) { upload_backdrop_primitives :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
prim_count := u32(len(GLOB.tmp_backdrop_primitives)) prim_count := u32(len(GLOB.tmp_gaussian_blur_primitives))
if prim_count == 0 do return if prim_count == 0 do return
prim_size := prim_count * size_of(Backdrop_Primitive) prim_size := prim_count * size_of(Gaussian_Blur_Primitive)
grow_buffer_if_needed( grow_buffer_if_needed(
device, device,
&GLOB.pipeline_2d_backdrop.primitive_buffer, &GLOB.backdrop.primitive_buffer,
prim_size, prim_size,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
) )
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_backdrop.primitive_buffer.transfer, false) prim_array := sdl.MapGPUTransferBuffer(device, GLOB.backdrop.primitive_buffer.transfer, false)
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())
} }
mem.copy(prim_array, raw_data(GLOB.tmp_backdrop_primitives), int(prim_size)) mem.copy(prim_array, raw_data(GLOB.tmp_gaussian_blur_primitives), int(prim_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_backdrop.primitive_buffer.transfer) sdl.UnmapGPUTransferBuffer(device, GLOB.backdrop.primitive_buffer.transfer)
sdl.UploadToGPUBuffer( sdl.UploadToGPUBuffer(
pass, pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_backdrop.primitive_buffer.transfer}, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.backdrop.primitive_buffer.transfer},
sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_backdrop.primitive_buffer.gpu, offset = 0, size = prim_size}, sdl.GPUBufferRegion{buffer = GLOB.backdrop.primitive_buffer.gpu, offset = 0, size = prim_size},
false, false,
) )
} }
// --------------------------------------------------------------------------------------------------------------------- //----- Bracket scheduler ----------------------------------
// ----- Frame / layer scanners --------
// ---------------------------------------------------------------------------------------------------------------------
// Returns true if any sub-batch in any layer this frame is .Backdrop kind. Called once at the
// top of `end()` to decide whether to route the whole frame to source_texture.
// O(total sub-batches) but with an early-exit on the first hit, so typical cost is tiny.
@(private)
frame_has_backdrop :: proc() -> bool {
for &batch in GLOB.tmp_sub_batches {
if batch.kind == .Backdrop do return true
}
return false
}
// Returns the absolute index of the first .Backdrop sub-batch in the layer's sub-batch range,
// or -1 if the layer has no backdrops. The index is into GLOB.tmp_sub_batches (not relative to
// layer.sub_batch_start), to match how draw_layer's render-range helpers consume it.
@(private)
find_first_backdrop_in_layer :: proc(layer: ^Layer) -> int {
for i in 0 ..< layer.sub_batch_len {
abs_idx := layer.sub_batch_start + i
if GLOB.tmp_sub_batches[abs_idx].kind == .Backdrop do return int(abs_idx)
}
return -1
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Bracket scheduler -------------
// ---------------------------------------------------------------------------------------------------------------------
// Compute the union AABB of the backdrop primitives in a contiguous-same-sigma sub-batch run // Compute the union AABB of the backdrop primitives in a contiguous-same-sigma sub-batch run
// (one "sigma group"), expanded by 6 sigmas of blur reach (the kernel weight beyond 3σ is // (one "sigma group"), expanded by 6 sigmas of blur reach (the kernel weight beyond 3σ is
@@ -661,7 +691,7 @@ find_first_backdrop_in_layer :: proc(layer: ^Layer) -> int {
// Per-group (rather than per-layer) because the adaptive downsample picks a different factor // Per-group (rather than per-layer) because the adaptive downsample picks a different factor
// per sigma, and the kernel reach is also per-sigma. A tighter region per group means less // per sigma, and the kernel reach is also per-sigma. A tighter region per group means less
// fragment work in the downsample and H-blur passes. // fragment work in the downsample and H-blur passes.
@(private) //INTERNAL
compute_backdrop_group_work_region :: proc( compute_backdrop_group_work_region :: proc(
group_start, group_end: u32, group_start, group_end: u32,
sigma_logical: f32, sigma_logical: f32,
@@ -680,7 +710,7 @@ compute_backdrop_group_work_region :: proc(
batch := GLOB.tmp_sub_batches[i] batch := GLOB.tmp_sub_batches[i]
if batch.kind != .Backdrop do continue if batch.kind != .Backdrop do continue
for p in batch.offset ..< batch.offset + batch.count { for p in batch.offset ..< batch.offset + batch.count {
prim := GLOB.tmp_backdrop_primitives[p] prim := GLOB.tmp_gaussian_blur_primitives[p]
// prim.bounds is in logical pixels (world space). // prim.bounds is in logical pixels (world space).
if !has_any { if !has_any {
min_x = prim.bounds[0] min_x = prim.bounds[0]
@@ -751,13 +781,13 @@ compute_backdrop_group_work_region :: proc(
// On exit, source_texture contains the pre-bracket contents plus all backdrop primitives // On exit, source_texture contains the pre-bracket contents plus all backdrop primitives
// composited on top. The caller then runs Pass B (post-bracket non-backdrop sub-batches) on // composited on top. The caller then runs Pass B (post-bracket non-backdrop sub-batches) on
// source_texture with LOAD. // source_texture with LOAD.
@(private) //INTERNAL
run_backdrop_bracket :: proc( run_backdrop_bracket :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer, cmd_buffer: ^sdl.GPUCommandBuffer,
layer: ^Layer, layer: ^Layer,
swapchain_width, swapchain_height: u32, swapchain_width, swapchain_height: u32,
) { ) {
pipeline := &GLOB.pipeline_2d_backdrop pipeline := &GLOB.backdrop
full_viewport := sdl.GPUViewport { full_viewport := sdl.GPUViewport {
x = 0, x = 0,
@@ -852,7 +882,7 @@ run_backdrop_bracket :: proc(
// Convert the user's logical-pixel sigma into the kernel's working space. // Convert the user's logical-pixel sigma into the kernel's working space.
// sigma_working_texels = sigma_logical * dpi_scaling / downsample_factor. // sigma_working_texels = sigma_logical * dpi_scaling / downsample_factor.
effective_sigma := sigma * GLOB.dpi_scaling / f32(downsample_factor) effective_sigma := sigma * GLOB.dpi_scaling / f32(downsample_factor)
frag_uniforms := Backdrop_Frag_Uniforms { frag_uniforms := Gaussian_Blur_Frag_Uniforms {
inv_working_size = inv_working_size, inv_working_size = inv_working_size,
inv_downsample_factor = 1.0 / f32(downsample_factor), inv_downsample_factor = 1.0 / f32(downsample_factor),
} }
@@ -1002,24 +1032,20 @@ run_backdrop_bracket :: proc(
} }
} }
// --------------------------------------------------------------------------------------------------------------------- //----- Primitive builders ----------------------------------
// ----- Primitive builders ------------
// ---------------------------------------------------------------------------------------------------------------------
// Internal // Build a Gaussian_Blur_Primitive with bounds, radii, and feather computed from rectangle
//
// Build a Backdrop_Primitive with bounds, radii, and feather computed from rectangle
// geometry. The caller sets `color` (tint) on the returned primitive before submitting. // geometry. The caller sets `color` (tint) on the returned primitive before submitting.
// //
// No rotation, no outline — backdrop primitives are intentionally limited to axis-aligned // No rotation, no outline — gaussian blur primitives are intentionally limited to axis-aligned
// RRects. Rotation breaks screen-space blur sampling visually; outline would be a specialized // RRects. Rotation breaks screen-space blur sampling visually; outline would be a specialized
// edge effect that belongs in its own primitive type. // edge effect that belongs in its own primitive type.
@(private) //INTERNAL
build_backdrop_primitive :: proc( build_backdrop_primitive :: proc(
rect: Rectangle, rect: Rectangle,
radii: Rectangle_Radii, radii: Rectangle_Radii,
feather_px: f32, feather_px: f32,
) -> Backdrop_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)
clamped_top_right := clamp(radii.top_right, 0, max_radius) clamped_top_right := clamp(radii.top_right, 0, max_radius)
@@ -1035,7 +1061,7 @@ build_backdrop_primitive :: proc(
center_x := rect.x + half_width center_x := rect.x + half_width
center_y := rect.y + half_height center_y := rect.y + half_height
return Backdrop_Primitive { return Gaussian_Blur_Primitive {
bounds = { bounds = {
center_x - half_width - padding, center_x - half_width - padding,
center_y - half_height - padding, center_y - half_height - padding,
@@ -1057,13 +1083,13 @@ build_backdrop_primitive :: proc(
} }
} }
// Internal — append a Backdrop_Primitive to the staging array and emit a .Backdrop sub-batch // Append a Gaussian_Blur_Primitive to the staging array and emit a .Backdrop sub-batch
// carrying the requested gaussian_sigma. Sub-batch coalescing in append_or_extend_sub_batch // carrying the requested gaussian_sigma. Sub-batch coalescing in append_or_extend_sub_batch
// will merge contiguous backdrops that share a sigma into a single instanced draw. // will merge contiguous backdrops that share a sigma into a single instanced draw.
@(private) //INTERNAL
prepare_backdrop_primitive :: proc(layer: ^Layer, prim: Backdrop_Primitive, gaussian_sigma: f32) { prepare_backdrop_primitive :: proc(layer: ^Layer, prim: Gaussian_Blur_Primitive, gaussian_sigma: f32) {
offset := u32(len(GLOB.tmp_backdrop_primitives)) offset := u32(len(GLOB.tmp_gaussian_blur_primitives))
append(&GLOB.tmp_backdrop_primitives, prim) append(&GLOB.tmp_gaussian_blur_primitives, prim)
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1] scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
append_or_extend_sub_batch( append_or_extend_sub_batch(
scissor, scissor,
@@ -1075,9 +1101,7 @@ prepare_backdrop_primitive :: proc(layer: ^Layer, prim: Backdrop_Primitive, gaus
) )
} }
// --------------------------------------------------------------------------------------------------------------------- //----- Public API ----------------------------------
// ----- Public API --------------------
// ---------------------------------------------------------------------------------------------------------------------
// Draw a rectangle whose interior samples a Gaussian-blurred snapshot of the framebuffer // Draw a rectangle whose interior samples a Gaussian-blurred snapshot of the framebuffer
// behind it. RRect-only — covers rectangles, rounded rectangles, and circles via // behind it. RRect-only — covers rectangles, rounded rectangles, and circles via
+1601
View File
File diff suppressed because it is too large Load Diff
+332 -347
View File
@@ -10,6 +10,11 @@ import sdl_ttf "vendor:sdl3/ttf"
import clay "../vendor/clay" import clay "../vendor/clay"
// ---------------------------------------------------------------------------------------------------------------------
// ----- Shader format ------------
// ---------------------------------------------------------------------------------------------------------------------
//INTERNAL (each constant in the when-block below)
when ODIN_OS == .Darwin { when ODIN_OS == .Darwin {
PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.MSL PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.MSL
SHADER_ENTRY :: cstring("main0") SHADER_ENTRY :: cstring("main0")
@@ -29,10 +34,18 @@ when ODIN_OS == .Darwin {
BACKDROP_BLUR_VERT_RAW :: #load("shaders/generated/backdrop_blur.vert.spv") BACKDROP_BLUR_VERT_RAW :: #load("shaders/generated/backdrop_blur.vert.spv")
BACKDROP_BLUR_FRAG_RAW :: #load("shaders/generated/backdrop_blur.frag.spv") BACKDROP_BLUR_FRAG_RAW :: #load("shaders/generated/backdrop_blur.frag.spv")
} }
PLATFORM_SHADER_FORMAT :: sdl.GPUShaderFormat{PLATFORM_SHADER_FORMAT_FLAG} PLATFORM_SHADER_FORMAT :: sdl.GPUShaderFormat{PLATFORM_SHADER_FORMAT_FLAG}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Defaults and config ------------
// ---------------------------------------------------------------------------------------------------------------------
//INTERNAL
BUFFER_INIT_SIZE :: 256 BUFFER_INIT_SIZE :: 256
//INTERNAL
INITIAL_LAYER_SIZE :: 5 INITIAL_LAYER_SIZE :: 5
//INTERNAL
INITIAL_SCISSOR_SIZE :: 10 INITIAL_SCISSOR_SIZE :: 10
// ----- Default parameter values ----- // ----- Default parameter values -----
@@ -48,64 +61,70 @@ DFT_TEXT_COLOR :: BLACK // Default text color.
DFT_CLEAR_COLOR :: BLACK // Default clear color for end(). DFT_CLEAR_COLOR :: BLACK // Default clear color for end().
DFT_SAMPLER :: Sampler_Preset.Linear_Clamp // Default texture sampler preset. DFT_SAMPLER :: Sampler_Preset.Linear_Clamp // Default texture sampler preset.
// ---------------------------------------------------------------------------------------------------------------------
// ----- Global state ------------
// ---------------------------------------------------------------------------------------------------------------------
//INTERNAL
GLOB: Global GLOB: Global
//INTERNAL
Global :: struct { Global :: struct {
// -- Per-frame staging (hottest — touched by every prepare/upload/clear cycle) -- // -- Per-frame staging (hottest — touched by every prepare/upload/clear cycle) --
tmp_shape_verts: [dynamic]Vertex, // Tessellated shape vertices staged for GPU upload. tmp_shape_verts: [dynamic]Vertex_2D, // Tessellated shape vertices staged for GPU upload.
tmp_text_verts: [dynamic]Vertex, // Text vertices staged for GPU upload. tmp_text_verts: [dynamic]Vertex_2D, // Text vertices staged for GPU upload.
tmp_text_indices: [dynamic]c.int, // Text index buffer staged for GPU upload. tmp_text_indices: [dynamic]c.int, // Text index buffer staged for GPU upload.
tmp_text_batches: [dynamic]TextBatch, // Text atlas batch metadata for indexed drawing. tmp_text_batches: [dynamic]Text_Batch, // Text atlas batch metadata for indexed drawing.
tmp_primitives: [dynamic]Base_2D_Primitive, // SDF primitives staged for GPU storage buffer upload (base 2D pipeline). tmp_primitives: [dynamic]Core_2D_Primitive, // SDF primitives staged for GPU storage buffer upload (core 2D subsystem).
tmp_sub_batches: [dynamic]Sub_Batch, // Sub-batch records that drive draw call dispatch. tmp_sub_batches: [dynamic]Sub_Batch, // Sub-batch records that drive draw call dispatch.
tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects destroyed after end() submits. tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects destroyed after end() submits.
tmp_backdrop_primitives: [dynamic]Backdrop_Primitive, // Backdrop primitives staged for GPU storage buffer upload. tmp_gaussian_blur_primitives: [dynamic]Gaussian_Blur_Primitive, // Gaussian blur primitives staged for GPU storage buffer upload.
layers: [dynamic]Layer, // Draw layers, each with its own scissor stack. layers: [dynamic]Layer, // Draw layers, each with its own scissor stack.
scissors: [dynamic]Scissor, // Scissor rects that clip drawing within each layer. scissors: [dynamic]Scissor, // Scissor rects that clip drawing within each layer.
// -- Per-frame scalars (accessed during prepare and draw_layer) -- // -- Per-frame scalars (accessed during prepare and draw_layer) --
curr_layer_index: uint, // Index of the currently active layer. curr_layer_index: uint, // Index of the currently active layer.
dpi_scaling: f32, // Window DPI scale factor applied to all pixel coordinates. dpi_scaling: f32, // Window DPI scale factor applied to all pixel coordinates.
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.
// -- Pipeline (accessed every draw_layer call) -- // -- Subsystems (accessed every draw_layer call) --
pipeline_2d_base: Pipeline_2D_Base, // The unified 2D GPU pipeline (shaders, buffers, samplers). core_2d: Core_2D, // The unified 2D GPU pipeline (shaders, buffers, samplers).
pipeline_2d_backdrop: Pipeline_2D_Backdrop, // Frosted-glass backdrop blur pipeline (downsample + blur PSOs, working textures). backdrop: Backdrop, // Frosted-glass backdrop blur subsystem (downsample + blur PSOs, working textures).
device: ^sdl.GPUDevice, // GPU device handle, stored at init. device: ^sdl.GPUDevice, // GPU device handle, stored at init.
samplers: [SAMPLER_PRESET_COUNT]^sdl.GPUSampler, // Lazily-created sampler objects, one per Sampler_Preset. samplers: [SAMPLER_PRESET_COUNT]^sdl.GPUSampler, // Lazily-created sampler objects, one per Sampler_Preset.
// -- Deferred release (processed once per frame at frame boundary) -- // -- Deferred release (processed once per frame at frame boundary) --
pending_texture_releases: [dynamic]Texture_Id, // Deferred GPU texture releases, processed next frame. pending_texture_releases: [dynamic]Texture_Id, // Deferred GPU texture releases, processed next frame.
pending_text_releases: [dynamic]^sdl_ttf.Text, // Deferred TTF_Text destroys, processed next frame. pending_text_releases: [dynamic]^sdl_ttf.Text, // Deferred TTF_Text destroys, processed next frame.
// -- Textures (registration is occasional, binding is per draw call) -- // -- Textures (registration is occasional, binding is per draw call) --
texture_slots: [dynamic]Texture_Slot, // Registered texture slots indexed by Texture_Id. texture_slots: [dynamic]Texture_Slot, // Registered texture slots indexed by Texture_Id.
texture_free_list: [dynamic]u32, // Recycled slot indices available for reuse. texture_free_list: [dynamic]u32, // Recycled slot indices available for reuse.
// -- 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.
// -- 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.
// -- Resize tracking (cold — checked once per frame in resize_global) -- // -- Resize tracking (cold — checked once per frame in resize_global) --
max_layers: int, // High-water marks for dynamic array shrink heuristic. max_layers: int, // High-water marks for dynamic array shrink heuristic.
max_scissors: int, max_scissors: int,
max_shape_verts: int, max_shape_verts: int,
max_text_verts: int, max_text_verts: int,
max_text_indices: int, max_text_indices: int,
max_text_batches: int, max_text_batches: int,
max_primitives: int, max_primitives: int,
max_sub_batches: int, max_sub_batches: int,
max_backdrop_primitives: int, max_gaussian_blur_primitives: int,
// -- Init-only (coldest — set once at init, never written again) -- // -- Init-only (coldest — set once at init, never written again) --
odin_context: runtime.Context, // Odin context captured at init for use in callbacks. odin_context: runtime.Context, // Odin context captured at init for use in callbacks.
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Core types -------------------- // ----- Core types ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// 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}
@@ -128,7 +147,7 @@ Vec2 :: [2]f32
// transparent. This matches the GPU-side layout: the shader unpacks via unpackUnorm4x8 which // transparent. This matches the GPU-side layout: the shader unpacks via unpackUnorm4x8 which
// reads the bytes in memory order as R, G, B, A and normalizes each to [0, 1]. // reads the bytes in memory order as R, G, B, A and normalizes each to [0, 1].
// //
// When used in the Base_2D_Primitive or Backdrop_Primitive structs (e.g. .color), the 4 bytes // 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. // are stored as a u32 in native byte order and unpacked by the shader.
Color :: [4]u8 Color :: [4]u8
@@ -139,6 +158,13 @@ GREEN :: Color{0, 255, 0, 255}
BLUE :: Color{0, 0, 255, 255} BLUE :: Color{0, 0, 255, 255}
BLANK :: Color{0, 0, 0, 0} BLANK :: Color{0, 0, 0, 0}
Rectangle :: struct {
x: f32,
y: f32,
width: f32,
height: f32,
}
// Per-corner rounding radii for rectangles, specified clockwise from top-left. // Per-corner rounding radii for rectangles, specified clockwise from top-left.
// All values are in logical pixels (pre-DPI-scaling). // All values are in logical pixels (pre-DPI-scaling).
Rectangle_Radii :: struct { Rectangle_Radii :: struct {
@@ -201,7 +227,7 @@ color_to_f32 :: proc(color: Color) -> [4]f32 {
// Pre-multiply RGB channels by alpha. The tessellated vertex path and text path require // Pre-multiply RGB channels by alpha. The tessellated vertex path and text path require
// premultiplied colors because the blend state is ONE, ONE_MINUS_SRC_ALPHA and the // premultiplied colors because the blend state is ONE, ONE_MINUS_SRC_ALPHA and the
// tessellated fragment shader passes vertex color through without further modification. // tessellated fragment shader passes vertex color through without further modification.
// Users who construct Vertex structs manually for prepare_shape must premultiply their colors. // 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 {
@@ -212,22 +238,21 @@ premultiply_color :: #force_inline proc(color: Color) -> Color {
} }
} }
Rectangle :: struct { // ---------------------------------------------------------------------------------------------------------------------
x: f32, // ----- Frame layout types ------------
y: f32, // ---------------------------------------------------------------------------------------------------------------------
width: f32,
height: f32,
}
//INTERNAL
Sub_Batch_Kind :: enum u8 { Sub_Batch_Kind :: enum u8 {
Tessellated, // non-indexed, white texture or user texture, base 2D mode 0 Tessellated, // non-indexed, white texture or user texture, Core_2D_Mode.Tessellated
Text, // indexed, atlas texture, base 2D mode 0 Text, // indexed, atlas texture, Core_2D_Mode.Tessellated
SDF, // instanced unit quad, base 2D mode 1 SDF, // instanced unit quad, Core_2D_Mode.SDF
// instanced unit quad, backdrop pipeline V-composite (indexes Backdrop_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.
Backdrop, Backdrop,
} }
//INTERNAL
Sub_Batch :: struct { Sub_Batch :: struct {
kind: Sub_Batch_Kind, kind: Sub_Batch_Kind,
offset: u32, // Tessellated: vertex offset; Text: text_batch index; SDF/Backdrop: primitive index offset: u32, // Tessellated: vertex offset; Text: text_batch index; SDF/Backdrop: primitive index
@@ -248,12 +273,17 @@ Layer :: struct {
scissor_len: u32, scissor_len: u32,
} }
//INTERNAL
Scissor :: struct { Scissor :: struct {
bounds: sdl.Rect, bounds: sdl.Rect,
sub_batch_start: u32, sub_batch_start: u32,
sub_batch_len: u32, sub_batch_len: u32,
} }
// ---------------------------------------------------------------------------------------------------------------------
// ----- Lifecycle ------------
// ---------------------------------------------------------------------------------------------------------------------
// 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 // MSAA is intentionally NOT supported. SDF text and shapes compute coverage analytically via
@@ -272,46 +302,56 @@ init :: proc(
) { ) {
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize() min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
pipeline, pipeline_ok := create_pipeline_2d_base(device, window) core, core_ok := create_core_2d(device, window)
if !pipeline_ok { if !core_ok {
return false return false
} }
backdrop_pipeline, backdrop_pipeline_ok := create_pipeline_2d_backdrop(device, window) backdrop, backdrop_ok := create_backdrop(device, window)
if !backdrop_pipeline_ok { if !backdrop_ok {
destroy_pipeline_2d_base(device, &pipeline) destroy_core_2d(device, &core)
return false return false
} }
text_cache, text_ok := init_text_cache(device, allocator) text_cache, text_ok := init_text_cache(device, allocator)
if !text_ok { if !text_ok {
destroy_pipeline_2d_backdrop(device, &backdrop_pipeline) destroy_backdrop(device, &backdrop)
destroy_pipeline_2d_base(device, &pipeline) destroy_core_2d(device, &core)
return false return false
} }
GLOB = Global { GLOB = Global {
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator), layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator), scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_shape_verts = make([dynamic]Vertex_2D, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_verts = make([dynamic]Vertex_2D, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_batches = make([dynamic]Text_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_primitives = make([dynamic]Base_2D_Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_primitives = make(
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator), [dynamic]Core_2D_Primitive,
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator), 0,
tmp_backdrop_primitives = make([dynamic]Backdrop_Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator), BUFFER_INIT_SIZE,
device = device, allocator = allocator,
texture_slots = make([dynamic]Texture_Slot, 0, 16, allocator = allocator), ),
texture_free_list = make([dynamic]u32, 0, 16, allocator = allocator), tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
pending_texture_releases = make([dynamic]Texture_Id, 0, 16, allocator = allocator), tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
pending_text_releases = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator), tmp_gaussian_blur_primitives = make(
odin_context = odin_context, [dynamic]Gaussian_Blur_Primitive,
dpi_scaling = sdl.GetWindowDisplayScale(window), 0,
clay_memory = make([^]u8, min_memory_size, allocator = allocator), BUFFER_INIT_SIZE,
pipeline_2d_base = pipeline, allocator = allocator,
pipeline_2d_backdrop = backdrop_pipeline, ),
text_cache = text_cache, device = device,
texture_slots = make([dynamic]Texture_Slot, 0, 16, allocator = allocator),
texture_free_list = make([dynamic]u32, 0, 16, allocator = allocator),
pending_texture_releases = make([dynamic]Texture_Id, 0, 16, allocator = allocator),
pending_text_releases = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
odin_context = odin_context,
dpi_scaling = sdl.GetWindowDisplayScale(window),
clay_memory = make([^]u8, min_memory_size, allocator = allocator),
core_2d = core,
backdrop = backdrop,
text_cache = text_cache,
} }
// Reserve slot 0 for INVALID_TEXTURE // Reserve slot 0 for INVALID_TEXTURE
@@ -345,8 +385,8 @@ resize_global :: proc() {
shrink(&GLOB.tmp_primitives, GLOB.max_primitives) shrink(&GLOB.tmp_primitives, GLOB.max_primitives)
if len(GLOB.tmp_sub_batches) > GLOB.max_sub_batches do GLOB.max_sub_batches = len(GLOB.tmp_sub_batches) if len(GLOB.tmp_sub_batches) > GLOB.max_sub_batches do GLOB.max_sub_batches = len(GLOB.tmp_sub_batches)
shrink(&GLOB.tmp_sub_batches, GLOB.max_sub_batches) shrink(&GLOB.tmp_sub_batches, GLOB.max_sub_batches)
if len(GLOB.tmp_backdrop_primitives) > GLOB.max_backdrop_primitives do GLOB.max_backdrop_primitives = len(GLOB.tmp_backdrop_primitives) if len(GLOB.tmp_gaussian_blur_primitives) > GLOB.max_gaussian_blur_primitives do GLOB.max_gaussian_blur_primitives = len(GLOB.tmp_gaussian_blur_primitives)
shrink(&GLOB.tmp_backdrop_primitives, GLOB.max_backdrop_primitives) shrink(&GLOB.tmp_gaussian_blur_primitives, GLOB.max_gaussian_blur_primitives)
} }
destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) { destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
@@ -358,7 +398,7 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
delete(GLOB.tmp_text_batches) delete(GLOB.tmp_text_batches)
delete(GLOB.tmp_primitives) delete(GLOB.tmp_primitives)
delete(GLOB.tmp_sub_batches) delete(GLOB.tmp_sub_batches)
delete(GLOB.tmp_backdrop_primitives) 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)
free(GLOB.clay_memory, allocator) free(GLOB.clay_memory, allocator)
@@ -367,12 +407,12 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
destroy_sampler_pool() destroy_sampler_pool()
for ttf_text in GLOB.pending_text_releases do sdl_ttf.DestroyText(ttf_text) for ttf_text in GLOB.pending_text_releases do sdl_ttf.DestroyText(ttf_text)
delete(GLOB.pending_text_releases) delete(GLOB.pending_text_releases)
destroy_pipeline_2d_backdrop(device, &GLOB.pipeline_2d_backdrop) destroy_backdrop(device, &GLOB.backdrop)
destroy_pipeline_2d_base(device, &GLOB.pipeline_2d_base) destroy_core_2d(device, &GLOB.core_2d)
destroy_text_cache() destroy_text_cache()
} }
// Internal //INTERNAL
clear_global :: proc() { clear_global :: proc() {
// Process deferred texture releases from the previous frame // Process deferred texture releases from the previous frame
process_pending_texture_releases() process_pending_texture_releases()
@@ -394,33 +434,11 @@ clear_global :: proc() {
clear(&GLOB.tmp_text_batches) clear(&GLOB.tmp_text_batches)
clear(&GLOB.tmp_primitives) clear(&GLOB.tmp_primitives)
clear(&GLOB.tmp_sub_batches) clear(&GLOB.tmp_sub_batches)
clear(&GLOB.tmp_backdrop_primitives) clear(&GLOB.tmp_gaussian_blur_primitives)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Text measurement (Clay) ------- // ----- Frame ------------
// ---------------------------------------------------------------------------------------------------------------------
@(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}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Frame lifecycle ---------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// 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.
@@ -472,133 +490,89 @@ new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
return &GLOB.layers[GLOB.curr_layer_index] return &GLOB.layers[GLOB.curr_layer_index]
} }
// --------------------------------------------------------------------------------------------------------------------- // Render primitives. clear_color is the background fill before any layers are drawn.
// ----- Built-in primitive processing -- end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DFT_CLEAR_COLOR) {
// --------------------------------------------------------------------------------------------------------------------- cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if cmd_buffer == nil {
// Submit shape vertices (colored triangles) to the given layer for rendering. log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError())
// TODO: Should probably be renamed to better match tesselated naming conventions in the library.
prepare_shape :: proc(layer: ^Layer, vertices: []Vertex) {
if len(vertices) == 0 do return
offset := u32(len(GLOB.tmp_shape_verts))
append(&GLOB.tmp_shape_verts, ..vertices)
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
append_or_extend_sub_batch(scissor, layer, .Tessellated, offset, u32(len(vertices)))
}
// Submit an SDF primitive to the given layer for rendering.
prepare_sdf_primitive :: proc(layer: ^Layer, prim: Base_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 a text element to the given layer for rendering.
// Copies SDL_ttf vertices directly (with baked position) and copies indices for indexed drawing.
prepare_text :: proc(layer: ^Layer, text: Text) {
data := sdl_ttf.GetGPUTextDrawData(text.sdl_text)
if data == nil {
return // nil is normal for empty text
} }
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1] // 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-
// frame texture copy. Frames without any backdrop hit the existing fast path and never
// touch the backdrop pipeline's working textures.
has_backdrop := frame_has_backdrop()
// Snap base position to integer physical pixels to avoid atlas sub-pixel // Upload primitives to GPU (vertices, indices, SDF prims, and backdrop prims share one
// sampling blur (and the off-by-one bottom-row clip that comes with it). // copy pass so we pay the BeginGPUCopyPass / EndGPUCopyPass cost once per frame).
base_x := math.round(text.position[0] * GLOB.dpi_scaling) copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
base_y := math.round(text.position[1] * GLOB.dpi_scaling) upload(device, copy_pass)
if has_backdrop {
upload_backdrop_primitives(device, copy_pass)
}
sdl.EndGPUCopyPass(copy_pass)
// Premultiply text color once — reused across all glyph vertices. swapchain_texture: ^sdl.GPUTexture
pm_color := premultiply_color(text.color) width, height: u32
if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &width, &height) {
log.panicf("Failed to acquire swapchain texture: %s", sdl.GetError())
}
for data != nil { if swapchain_texture == nil {
vertex_start := u32(len(GLOB.tmp_text_verts)) // Window is minimized or not visible — submit and skip this frame
index_start := u32(len(GLOB.tmp_text_indices)) if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
log.panicf("Failed to submit GPU command buffer (minimized window): %s", sdl.GetError())
// Copy vertices with baked position offset
for i in 0 ..< data.num_vertices {
pos := data.xy[i]
uv := data.uv[i]
append(
&GLOB.tmp_text_verts,
Vertex{position = {pos.x + base_x, -pos.y + base_y}, uv = {uv.x, uv.y}, color = pm_color},
)
} }
// Copy indices directly
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
batch_idx := u32(len(GLOB.tmp_text_batches))
append(
&GLOB.tmp_text_batches,
TextBatch {
atlas_texture = data.atlas_texture,
vertex_start = vertex_start,
vertex_count = u32(data.num_vertices),
index_start = index_start,
index_count = u32(data.num_indices),
},
)
// Each atlas chunk is a separate sub-batch (different atlas textures can't coalesce)
append_or_extend_sub_batch(scissor, layer, .Text, batch_idx, 1)
data = data.next
}
}
// Submit a text element with a 2D affine transform applied to vertices.
// Used by the high-level `text` proc when rotation or a non-zero origin is specified.
// NOTE: xform must be in physical (DPI-scaled) pixel space — the caller pre-scales
// pos and origin by GLOB.dpi_scaling before building the transform.
prepare_text_transformed :: proc(layer: ^Layer, text: Text, transform: Transform_2D) {
data := sdl_ttf.GetGPUTextDrawData(text.sdl_text)
if data == nil {
return return
} }
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1] render_texture := swapchain_texture
if has_backdrop {
ensure_backdrop_textures(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
render_texture = GLOB.backdrop.source_texture
}
// Premultiply text color once — reused across all glyph vertices. // Premultiply clear color: the blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied),
pm_color := premultiply_color(text.color) // so the clear color must also be premultiplied for correct background compositing.
clear_color_straight := color_to_f32(clear_color)
clear_alpha := clear_color_straight[3]
clear_color_f32 := [4]f32 {
clear_color_straight[0] * clear_alpha,
clear_color_straight[1] * clear_alpha,
clear_color_straight[2] * clear_alpha,
clear_alpha,
}
for data != nil { // Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
vertex_start := u32(len(GLOB.tmp_text_verts)) for &layer, index in GLOB.layers {
index_start := u32(len(GLOB.tmp_text_indices)) draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer)
}
for i in 0 ..< data.num_vertices { // When we rendered into source_texture, copy it to the swapchain. Single
pos := data.xy[i] // CopyGPUTextureToTexture call per frame, only when backdrop content was present.
uv := data.uv[i] if has_backdrop {
// SDL_ttf gives glyph positions in physical pixels relative to text origin. copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
// The transform is already in physical-pixel space (caller pre-scaled), sdl.CopyGPUTextureToTexture(
// so we apply directly — no per-vertex DPI divide/multiply. copy_pass,
append( sdl.GPUTextureLocation{texture = GLOB.backdrop.source_texture},
&GLOB.tmp_text_verts, sdl.GPUTextureLocation{texture = swapchain_texture},
Vertex{position = apply_transform(transform, {pos.x, -pos.y}), uv = {uv.x, uv.y}, color = pm_color}, width,
) height,
} 1,
false,
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
batch_idx := u32(len(GLOB.tmp_text_batches))
append(
&GLOB.tmp_text_batches,
TextBatch {
atlas_texture = data.atlas_texture,
vertex_start = vertex_start,
vertex_count = u32(data.num_vertices),
index_start = index_start,
index_count = u32(data.num_indices),
},
) )
sdl.EndGPUCopyPass(copy_pass)
}
append_or_extend_sub_batch(scissor, layer, .Text, batch_idx, 1) if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
log.panicf("Failed to submit GPU command buffer: %s", sdl.GetError())
data = data.next
} }
} }
// ---------------------------------------------------------------------------------------------------------------------
// ----- Sub-batch dispatch ------------
// ---------------------------------------------------------------------------------------------------------------------
// Append a new sub-batch or extend the last one if same kind and contiguous. // Append a new sub-batch or extend the last one if same kind and contiguous.
// //
// `gaussian_sigma` is only consulted for kind == .Backdrop; two .Backdrop sub-batches with // `gaussian_sigma` is only consulted for kind == .Backdrop; two .Backdrop sub-batches with
@@ -606,7 +580,7 @@ prepare_text_transformed :: proc(layer: ^Layer, text: Text, transform: Transform
// bracket scheduler. Float equality is intentional — user-supplied literal sigmas (e.g. // bracket scheduler. Float equality is intentional — user-supplied literal sigmas (e.g.
// `sigma = 12`) produce bit-identical floats, and the worst case for two sigmas that differ // `sigma = 12`) produce bit-identical floats, and the worst case for two sigmas that differ
// only by a ulp is one extra pass pair (correct, just slightly suboptimal). // only by a ulp is one extra pass pair (correct, just slightly suboptimal).
@(private) //INTERNAL
append_or_extend_sub_batch :: proc( append_or_extend_sub_batch :: proc(
scissor: ^Scissor, scissor: ^Scissor,
layer: ^Layer, layer: ^Layer,
@@ -645,7 +619,7 @@ append_or_extend_sub_batch :: proc(
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Clay ------------------------ // ----- Clay ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@(private = "file") @(private = "file")
@@ -654,6 +628,24 @@ clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
log.error("Clay error:", errorData.errorType, errorData.errorText) 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 // Called for each Clay `RenderCommandType.Custom` render command that
// `prepare_clay_batch` encounters. // `prepare_clay_batch` encounters.
// //
@@ -822,142 +814,18 @@ prepare_clay_batch :: proc(
} }
} }
// 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) {
cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if cmd_buffer == nil {
log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError())
}
// 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-
// frame texture copy. Frames without any backdrop hit the existing fast path and never
// touch the backdrop pipeline's working textures.
has_backdrop := frame_has_backdrop()
// Upload primitives to GPU (vertices, indices, SDF prims, and backdrop prims share one
// copy pass so we pay the BeginGPUCopyPass / EndGPUCopyPass cost once per frame).
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
upload(device, copy_pass)
if has_backdrop {
upload_backdrop_primitives(device, copy_pass)
}
sdl.EndGPUCopyPass(copy_pass)
swapchain_texture: ^sdl.GPUTexture
width, height: u32
if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &width, &height) {
log.panicf("Failed to acquire swapchain texture: %s", sdl.GetError())
}
if swapchain_texture == nil {
// Window is minimized or not visible — submit and skip this frame
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
log.panicf("Failed to submit GPU command buffer (minimized window): %s", sdl.GetError())
}
return
}
render_texture := swapchain_texture
if has_backdrop {
ensure_backdrop_textures(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
render_texture = GLOB.pipeline_2d_backdrop.source_texture
}
// Premultiply clear color: the blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied),
// so the clear color must also be premultiplied for correct background compositing.
clear_color_straight := color_to_f32(clear_color)
clear_alpha := clear_color_straight[3]
clear_color_f32 := [4]f32 {
clear_color_straight[0] * clear_alpha,
clear_color_straight[1] * clear_alpha,
clear_color_straight[2] * clear_alpha,
clear_alpha,
}
// Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
for &layer, index in GLOB.layers {
draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer)
}
// When we rendered into source_texture, copy it to the swapchain. Single
// CopyGPUTextureToTexture call per frame, only when backdrop content was present.
if has_backdrop {
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
sdl.CopyGPUTextureToTexture(
copy_pass,
sdl.GPUTextureLocation{texture = GLOB.pipeline_2d_backdrop.source_texture},
sdl.GPUTextureLocation{texture = swapchain_texture},
width,
height,
1,
false,
)
sdl.EndGPUCopyPass(copy_pass)
}
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
log.panicf("Failed to submit GPU command buffer: %s", sdl.GetError())
}
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Utility ----------------------- // ----- Buffer ------------
// ---------------------------------------------------------------------------------------------------------------------
ortho_rh :: proc(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> matrix[4, 4]f32 {
return matrix[4, 4]f32{
2.0 / (right - left), 0.0, 0.0, -(right + left) / (right - left),
0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / (top - bottom),
0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near),
0.0, 0.0, 0.0, 1.0,
}
}
Draw_Mode :: enum u32 {
Tessellated = 0,
SDF = 1,
}
Vertex_Uniforms :: struct {
projection: matrix[4, 4]f32,
scale: f32,
mode: Draw_Mode,
}
// Push projection, dpi scale, and rendering mode as a single uniform block (slot 0).
push_globals :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer,
width: f32,
height: f32,
mode: Draw_Mode = .Tessellated,
) {
globals := Vertex_Uniforms {
projection = ortho_rh(
left = 0.0,
top = 0.0,
right = f32(width),
bottom = f32(height),
near = -1.0,
far = 1.0,
),
scale = GLOB.dpi_scaling,
mode = mode,
}
sdl.PushGPUVertexUniformData(cmd_buffer, 0, &globals, size_of(Vertex_Uniforms))
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Buffer ------------------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
//INTERNAL
Buffer :: struct { Buffer :: struct {
gpu: ^sdl.GPUBuffer, gpu: ^sdl.GPUBuffer,
transfer: ^sdl.GPUTransferBuffer, transfer: ^sdl.GPUTransferBuffer,
size: u32, size: u32,
} }
//INTERNAL
@(require_results) @(require_results)
create_buffer :: proc( create_buffer :: proc(
device: ^sdl.GPUDevice, device: ^sdl.GPUDevice,
@@ -984,6 +852,7 @@ create_buffer :: proc(
return Buffer{gpu, transfer, size}, true return Buffer{gpu, transfer, size}, true
} }
//INTERNAL
grow_buffer_if_needed :: proc( grow_buffer_if_needed :: proc(
device: ^sdl.GPUDevice, device: ^sdl.GPUDevice,
buffer: ^Buffer, buffer: ^Buffer,
@@ -1008,15 +877,26 @@ grow_buffer_if_needed :: proc(
} }
} }
//INTERNAL
destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) { destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) {
sdl.ReleaseGPUBuffer(device, buffer.gpu) sdl.ReleaseGPUBuffer(device, buffer.gpu)
sdl.ReleaseGPUTransferBuffer(device, buffer.transfer) sdl.ReleaseGPUTransferBuffer(device, buffer.transfer)
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Transform ------------------------ // ----- Math ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
//INTERNAL
ortho_rh :: proc(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> matrix[4, 4]f32 {
return matrix[4, 4]f32{
2.0 / (right - left), 0.0, 0.0, -(right + left) / (right - left),
0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / (top - bottom),
0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near),
0.0, 0.0, 0.0, 1.0,
}
}
// 2x3 affine transform for 2D pivot-rotation. // 2x3 affine transform for 2D pivot-rotation.
// Used internally by rotation-aware drawing procs. // Used internally by rotation-aware drawing procs.
Transform_2D :: struct { Transform_2D :: struct {
@@ -1078,9 +958,114 @@ needs_transform :: #force_inline proc(origin: Vec2, rotation: f32) -> bool {
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Procedure Groups ------------------------ // ----- Anchors ------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// Return Vec2 pixel offsets for use as the `origin` parameter of draw calls.
// Composable with normal vector +/- arithmetic.
//
// Text anchor helpers are in text.odin (they depend on measure_text / SDL_ttf).
// Returns uniform radii (all corners the same) as a fraction of the shorter side.
// `roundness` is clamped to [0, 1]; 0 = sharp corners, 1 = fully rounded (stadium or circle).
uniform_radii :: #force_inline proc(rect: Rectangle, roundness: f32) -> Rectangle_Radii {
cr := min(rect.width, rect.height) * clamp(roundness, 0, 1) * 0.5
return {cr, cr, cr, cr}
}
//----- Rectangle anchors (origin measured from rectangle's top-left) ----------------------------------
center_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, rectangle.height * 0.5}
}
top_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, 0}
}
top_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, 0}
}
top_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, 0}
}
left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, rectangle.height * 0.5}
}
right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, rectangle.height * 0.5}
}
bottom_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, rectangle.height}
}
bottom_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, rectangle.height}
}
bottom_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, rectangle.height}
}
//----- Triangle anchors (origin measured from AABB top-left) ----------------------------------
center_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
return (v1 + v2 + v3) / 3 - bounds_min
}
top_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
return {0, 0}
}
top_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_x := min(v1.x, v2.x, v3.x)
max_x := max(v1.x, v2.x, v3.x)
return {(max_x - min_x) * 0.5, 0}
}
top_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_x := min(v1.x, v2.x, v3.x)
max_x := max(v1.x, v2.x, v3.x)
return {max_x - min_x, 0}
}
left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_y := min(v1.y, v2.y, v3.y)
max_y := max(v1.y, v2.y, v3.y)
return {0, (max_y - min_y) * 0.5}
}
right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {bounds_max.x - bounds_min.x, (bounds_max.y - bounds_min.y) * 0.5}
}
bottom_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_y := min(v1.y, v2.y, v3.y)
max_y := max(v1.y, v2.y, v3.y)
return {0, max_y - min_y}
}
bottom_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {(bounds_max.x - bounds_min.x) * 0.5, bounds_max.y - bounds_min.y}
}
bottom_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return bounds_max - bounds_min
}
//----- Procedure groups ----------------------------------
center_of :: proc { center_of :: proc {
center_of_rectangle, center_of_rectangle,
center_of_triangle, center_of_triangle,
-827
View File
@@ -1,827 +0,0 @@
package draw
import "core:c"
import "core:log"
import "core:mem"
import sdl "vendor:sdl3"
// Vertex layout for tessellated and text geometry.
// 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 :: struct {
position: Vec2,
uv: [2]f32,
color: Color,
}
TextBatch :: struct {
atlas_texture: ^sdl.GPUTexture,
vertex_start: u32,
vertex_count: u32,
index_start: u32,
index_count: u32,
}
// ----------------------------------------------------------------------------------------------------------------
// ----- SDF primitive types -----------
// ----------------------------------------------------------------------------------------------------------------
// The SDF path evaluates one of four signed distance functions per primitive, dispatched
// by Shape_Kind encoded in the low byte of Base_2D_Primitive.flags:
//
// RRect — rounded rectangle with per-corner radii (sdRoundedBox). Also covers circles
// (uniform radii = half-size), capsule-style line segments (rotated, max rounding),
// and other RRect-reducible shapes.
// NGon — regular polygon with N sides and optional rounding.
// Ellipse — approximate ellipse (non-exact SDF, suitable for UI but not for shape merging).
// Ring_Arc — annular ring with optional angular clipping. Covers full rings, partial arcs,
// pie slices (inner_radius = 0), and loading spinners.
Shape_Kind :: enum u8 {
Solid = 0, // tessellated path (mode marker; not a real SDF kind)
RRect = 1,
NGon = 2,
Ellipse = 3,
Ring_Arc = 4,
}
Shape_Flag :: enum u8 {
Textured, // bit 0: sample texture using uv_rect (mutually exclusive with Gradient via Brush union)
Gradient, // bit 1: 2-color gradient using effects.gradient_color as end/outer color
Gradient_Radial, // bit 2: if set with Gradient, radial from center; else linear at angle
Outline, // bit 3: outer outline band using effects.outline_color; CPU expands bounds by outline_width
Rotated, // bit 4: shape has non-zero rotation; rotation_sc contains packed sin/cos
Arc_Narrow, // bit 5: ring arc span ≤ π — intersect half-planes. Neither Arc bit = full ring.
Arc_Wide, // bit 6: ring arc span > π — union half-planes. Neither Arc bit = full ring.
}
Shape_Flags :: bit_set[Shape_Flag;u8]
RRect_Params :: struct {
half_size: [2]f32,
radii: [4]f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: f32,
}
NGon_Params :: struct {
radius: f32,
sides: f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32,
}
Ellipse_Params :: struct {
radii: [2]f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32,
}
Ring_Arc_Params :: struct {
inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
outer_radius: f32, // outer radius in physical pixels
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))
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: f32,
}
Shape_Params :: struct #raw_union {
rrect: RRect_Params,
ngon: NGon_Params,
ellipse: Ellipse_Params,
ring_arc: Ring_Arc_Params,
raw: [8]f32,
}
#assert(size_of(Shape_Params) == 32)
// GPU-side storage for 2-color gradient parameters and/or outline parameters.
// Packed into 16 bytes. Independent from uv_rect — texture and outline can coexist.
// The shader reads gradient_color and outline_color via unpackUnorm4x8.
// gradient_dir_sc stores the pre-computed gradient direction as (cos, sin) in f16 pair
// via unpackHalf2x16. outline_packed stores outline_width as f16 via unpackHalf2x16.
Gradient_Outline :: struct {
gradient_color: Color, // 0: end (linear) or outer (radial) gradient color
outline_color: Color, // 4: outline band color
gradient_dir_sc: u32, // 8: packed f16 pair: low = cos(angle), high = sin(angle) — pre-computed gradient direction
outline_packed: u32, // 12: packed f16 pair: low = outline_width (f16, physical pixels), high = reserved
}
#assert(size_of(Gradient_Outline) == 16)
// GPU layout: 96 bytes, std430-compatible. The shader declares this as a storage buffer struct.
// The low byte of `flags` encodes the Shape_Kind (0 = tessellated, 1-4 = SDF kinds).
// Bits 8-15 encode Shape_Flags (Textured, Gradient, Gradient_Radial, Outline, Rotated, Arc_Narrow, Arc_Wide).
// rotation_sc stores pre-computed sin/cos of the rotation angle as a packed f16 pair,
// avoiding per-pixel trigonometry in the fragment shader. Only read when .Rotated is set.
//
// Named Base_2D_Primitive (not just Primitive) to disambiguate from Backdrop_Primitive in
// backdrop.odin. The two pipelines have unrelated GPU layouts and unrelated fragment-shader
// contracts; pairing each with its own primitive type keeps cross-references unambiguous
// when grepping the codebase.
Base_2D_Primitive :: struct {
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
color: Color, // 16: u8x4, fill color / gradient start color / texture tint
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
rotation_sc: u32, // 24: packed f16 pair: low = sin(angle), high = cos(angle). Requires .Rotated flag.
_pad: f32, // 28: reserved for future use
params: Shape_Params, // 32: per-kind shape parameters (raw union, 32 bytes)
uv_rect: [4]f32, // 64: texture UV coordinates (u_min, v_min, u_max, v_max). Read when .Textured.
effects: Gradient_Outline, // 80: gradient and/or outline parameters. Read when .Gradient and/or .Outline.
}
#assert(size_of(Base_2D_Primitive) == 96)
// Pack shape kind and flags into the Base_2D_Primitive.flags field. The low byte encodes the
// Shape_Kind (which also serves as the SDF mode marker — kind > 0 means SDF path). The
// tessellated path leaves the field at 0 (Solid kind, set by vertex shader zero-initialization).
pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 {
return u32(kind) | (u32(transmute(u8)flags) << 8)
}
// Pack two f16 values into a single u32 for GPU consumption via unpackHalf2x16.
// Used to pack gradient_dir_sc (cos/sin) and outline_packed (width/reserved) in Gradient_Outline.
pack_f16_pair :: #force_inline proc(low, high: f16) -> u32 {
return u32(transmute(u16)low) | (u32(transmute(u16)high) << 16)
}
Pipeline_2D_Base :: struct {
sdl_pipeline: ^sdl.GPUGraphicsPipeline,
vertex_buffer: Buffer,
index_buffer: Buffer,
unit_quad_buffer: ^sdl.GPUBuffer,
primitive_buffer: Buffer,
white_texture: ^sdl.GPUTexture,
sampler: ^sdl.GPUSampler,
}
// MSAA is not supported by levlib (see init's doc comment in draw.odin); the PSO is hard-wired
// to single-sample. SDF text and shapes provide analytical AA via smoothstep; tessellated user
// geometry is not anti-aliased.
@(private)
create_pipeline_2d_base :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
) -> (
pipeline: Pipeline_2D_Base,
ok: bool,
) {
// On failure, clean up any partially-created resources
defer if !ok {
if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler)
if pipeline.white_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.white_texture)
if pipeline.unit_quad_buffer != nil do sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer)
if pipeline.primitive_buffer.gpu != nil do destroy_buffer(device, &pipeline.primitive_buffer)
if pipeline.index_buffer.gpu != nil do destroy_buffer(device, &pipeline.index_buffer)
if pipeline.vertex_buffer.gpu != nil do destroy_buffer(device, &pipeline.vertex_buffer)
if pipeline.sdl_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
}
active_shader_formats := sdl.GetGPUShaderFormats(device)
if PLATFORM_SHADER_FORMAT_FLAG not_in active_shader_formats {
log.errorf(
"draw: no embedded shader matches active GPU formats; this build supports %v but device reports %v",
PLATFORM_SHADER_FORMAT,
active_shader_formats,
)
return pipeline, false
}
log.debug("Loaded", len(BASE_VERT_2D_RAW), "vert bytes")
log.debug("Loaded", len(BASE_FRAG_2D_RAW), "frag bytes")
vert_info := sdl.GPUShaderCreateInfo {
code_size = len(BASE_VERT_2D_RAW),
code = raw_data(BASE_VERT_2D_RAW),
entrypoint = SHADER_ENTRY,
format = {PLATFORM_SHADER_FORMAT_FLAG},
stage = .VERTEX,
num_uniform_buffers = 1,
num_storage_buffers = 1,
}
frag_info := sdl.GPUShaderCreateInfo {
code_size = len(BASE_FRAG_2D_RAW),
code = raw_data(BASE_FRAG_2D_RAW),
entrypoint = SHADER_ENTRY,
format = {PLATFORM_SHADER_FORMAT_FLAG},
stage = .FRAGMENT,
num_samplers = 1,
}
vert_shader := sdl.CreateGPUShader(device, vert_info)
if vert_shader == nil {
log.errorf("Could not create draw vertex shader: %s", sdl.GetError())
return pipeline, false
}
frag_shader := sdl.CreateGPUShader(device, frag_info)
if frag_shader == nil {
sdl.ReleaseGPUShader(device, vert_shader)
log.errorf("Could not create draw fragment shader: %s", sdl.GetError())
return pipeline, false
}
vertex_attributes: [3]sdl.GPUVertexAttribute = {
// position (GLSL location 0)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 0, format = .FLOAT2, offset = 0},
// uv (GLSL location 1)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 1, format = .FLOAT2, offset = size_of([2]f32)},
// color (GLSL location 2, u8x4 normalized to float by GPU)
sdl.GPUVertexAttribute{buffer_slot = 0, location = 2, format = .UBYTE4_NORM, offset = size_of([2]f32) * 2},
}
pipeline_info := sdl.GPUGraphicsPipelineCreateInfo {
vertex_shader = vert_shader,
fragment_shader = frag_shader,
primitive_type = .TRIANGLELIST,
multisample_state = sdl.GPUMultisampleState{sample_count = ._1},
target_info = sdl.GPUGraphicsPipelineTargetInfo {
color_target_descriptions = &sdl.GPUColorTargetDescription {
format = sdl.GetGPUSwapchainTextureFormat(device, window),
// Premultiplied-alpha blending: src outputs RGB pre-multiplied by alpha,
// so src factor is ONE (not SRC_ALPHA). This eliminates the per-pixel
// divide in the outline path and is the standard blend mode used by
// Skia, Flutter, and GPUI.
blend_state = sdl.GPUColorTargetBlendState {
enable_blend = true,
enable_color_write_mask = true,
src_color_blendfactor = .ONE,
dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
color_blend_op = .ADD,
src_alpha_blendfactor = .ONE,
dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA,
alpha_blend_op = .ADD,
color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A},
},
},
num_color_targets = 1,
},
vertex_input_state = sdl.GPUVertexInputState {
vertex_buffer_descriptions = &sdl.GPUVertexBufferDescription {
slot = 0,
input_rate = .VERTEX,
pitch = size_of(Vertex),
},
num_vertex_buffers = 1,
vertex_attributes = raw_data(vertex_attributes[:]),
num_vertex_attributes = 3,
},
}
pipeline.sdl_pipeline = sdl.CreateGPUGraphicsPipeline(device, pipeline_info)
// Shaders are no longer needed regardless of pipeline creation success
sdl.ReleaseGPUShader(device, vert_shader)
sdl.ReleaseGPUShader(device, frag_shader)
if pipeline.sdl_pipeline == nil {
log.errorf("Failed to create draw graphics pipeline: %s", sdl.GetError())
return pipeline, false
}
// Create vertex buffer
vert_buf_ok: bool
pipeline.vertex_buffer, vert_buf_ok = create_buffer(
device,
size_of(Vertex) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.VERTEX},
)
if !vert_buf_ok do return pipeline, false
// Create index buffer (used by text)
idx_buf_ok: bool
pipeline.index_buffer, idx_buf_ok = create_buffer(
device,
size_of(c.int) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.INDEX},
)
if !idx_buf_ok do return pipeline, false
// Create primitive storage buffer (used by SDF instanced drawing)
prim_buf_ok: bool
pipeline.primitive_buffer, prim_buf_ok = create_buffer(
device,
size_of(Base_2D_Primitive) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
)
if !prim_buf_ok do return pipeline, false
// Create static 6-vertex unit quad buffer (two triangles, TRIANGLELIST)
pipeline.unit_quad_buffer = sdl.CreateGPUBuffer(
device,
sdl.GPUBufferCreateInfo{usage = {.VERTEX}, size = 6 * size_of(Vertex)},
)
if pipeline.unit_quad_buffer == nil {
log.errorf("Failed to create unit quad buffer: %s", sdl.GetError())
return pipeline, false
}
// Create 1x1 white pixel texture
pipeline.white_texture = sdl.CreateGPUTexture(
device,
sdl.GPUTextureCreateInfo {
type = .D2,
format = .R8G8B8A8_UNORM,
usage = {.SAMPLER},
width = 1,
height = 1,
layer_count_or_depth = 1,
num_levels = 1,
sample_count = ._1,
},
)
if pipeline.white_texture == nil {
log.errorf("Failed to create white pixel texture: %s", sdl.GetError())
return pipeline, false
}
// Upload white pixel and unit quad data in a single command buffer
white_pixel := Color{255, 255, 255, 255}
white_transfer_buf := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(white_pixel)},
)
if white_transfer_buf == nil {
log.errorf("Failed to create white pixel transfer buffer: %s", sdl.GetError())
return pipeline, false
}
defer sdl.ReleaseGPUTransferBuffer(device, white_transfer_buf)
white_ptr := sdl.MapGPUTransferBuffer(device, white_transfer_buf, false)
if white_ptr == nil {
log.errorf("Failed to map white pixel transfer buffer: %s", sdl.GetError())
return pipeline, false
}
mem.copy(white_ptr, &white_pixel, size_of(white_pixel))
sdl.UnmapGPUTransferBuffer(device, white_transfer_buf)
quad_verts := [6]Vertex {
{position = {0, 0}},
{position = {1, 0}},
{position = {0, 1}},
{position = {0, 1}},
{position = {1, 0}},
{position = {1, 1}},
}
quad_transfer_buf := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(quad_verts)},
)
if quad_transfer_buf == nil {
log.errorf("Failed to create unit quad transfer buffer: %s", sdl.GetError())
return pipeline, false
}
defer sdl.ReleaseGPUTransferBuffer(device, quad_transfer_buf)
quad_ptr := sdl.MapGPUTransferBuffer(device, quad_transfer_buf, false)
if quad_ptr == nil {
log.errorf("Failed to map unit quad transfer buffer: %s", sdl.GetError())
return pipeline, false
}
mem.copy(quad_ptr, &quad_verts, size_of(quad_verts))
sdl.UnmapGPUTransferBuffer(device, quad_transfer_buf)
upload_cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if upload_cmd_buffer == nil {
log.errorf("Failed to acquire command buffer for init upload: %s", sdl.GetError())
return pipeline, false
}
upload_pass := sdl.BeginGPUCopyPass(upload_cmd_buffer)
sdl.UploadToGPUTexture(
upload_pass,
sdl.GPUTextureTransferInfo{transfer_buffer = white_transfer_buf},
sdl.GPUTextureRegion{texture = pipeline.white_texture, w = 1, h = 1, d = 1},
false,
)
sdl.UploadToGPUBuffer(
upload_pass,
sdl.GPUTransferBufferLocation{transfer_buffer = quad_transfer_buf},
sdl.GPUBufferRegion{buffer = pipeline.unit_quad_buffer, offset = 0, size = size_of(quad_verts)},
false,
)
sdl.EndGPUCopyPass(upload_pass)
if !sdl.SubmitGPUCommandBuffer(upload_cmd_buffer) {
log.errorf("Failed to submit init upload command buffer: %s", sdl.GetError())
return pipeline, false
}
log.debug("White pixel texture and unit quad buffer created and uploaded")
// Create sampler (shared by shapes and text)
pipeline.sampler = sdl.CreateGPUSampler(
device,
sdl.GPUSamplerCreateInfo {
min_filter = .LINEAR,
mag_filter = .LINEAR,
mipmap_mode = .LINEAR,
address_mode_u = .CLAMP_TO_EDGE,
address_mode_v = .CLAMP_TO_EDGE,
address_mode_w = .CLAMP_TO_EDGE,
},
)
if pipeline.sampler == nil {
log.errorf("Could not create GPU sampler: %s", sdl.GetError())
return pipeline, false
}
log.debug("Done creating unified draw pipeline")
return pipeline, true
}
@(private)
upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
// Upload vertices (shapes then text into one buffer)
shape_vert_count := u32(len(GLOB.tmp_shape_verts))
text_vert_count := u32(len(GLOB.tmp_text_verts))
total_vert_count := shape_vert_count + text_vert_count
if total_vert_count > 0 {
total_vert_size := total_vert_count * size_of(Vertex)
shape_vert_size := shape_vert_count * size_of(Vertex)
text_vert_size := text_vert_count * size_of(Vertex)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.vertex_buffer,
total_vert_size,
sdl.GPUBufferUsageFlags{.VERTEX},
)
vert_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer, false)
if vert_array == nil {
log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError())
}
if shape_vert_size > 0 {
mem.copy(vert_array, raw_data(GLOB.tmp_shape_verts), int(shape_vert_size))
}
if text_vert_size > 0 {
mem.copy(
rawptr(uintptr(vert_array) + uintptr(shape_vert_size)),
raw_data(GLOB.tmp_text_verts),
int(text_vert_size),
)
}
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.vertex_buffer.transfer},
sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.vertex_buffer.gpu, offset = 0, size = total_vert_size},
false,
)
}
// Upload text indices
index_count := u32(len(GLOB.tmp_text_indices))
if index_count > 0 {
index_size := index_count * size_of(c.int)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.index_buffer,
index_size,
sdl.GPUBufferUsageFlags{.INDEX},
)
idx_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer, false)
if idx_array == nil {
log.panicf("Failed to map index transfer buffer: %s", sdl.GetError())
}
mem.copy(idx_array, raw_data(GLOB.tmp_text_indices), int(index_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.index_buffer.transfer},
sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0, size = index_size},
false,
)
}
// Upload SDF primitives
prim_count := u32(len(GLOB.tmp_primitives))
if prim_count > 0 {
prim_size := prim_count * size_of(Base_2D_Primitive)
grow_buffer_if_needed(
device,
&GLOB.pipeline_2d_base.primitive_buffer,
prim_size,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
)
prim_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer, false)
if prim_array == nil {
log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError())
}
mem.copy(prim_array, raw_data(GLOB.tmp_primitives), int(prim_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer)
sdl.UploadToGPUBuffer(
pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.primitive_buffer.transfer},
sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.primitive_buffer.gpu, offset = 0, size = prim_size},
false,
)
}
}
@(private)
draw_layer :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
cmd_buffer: ^sdl.GPUCommandBuffer,
render_texture: ^sdl.GPUTexture,
swapchain_width: u32,
swapchain_height: u32,
clear_color: [4]f32,
layer: ^Layer,
) {
if layer.sub_batch_len == 0 {
if !GLOB.cleared {
pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
load_op = .CLEAR,
store_op = .STORE,
},
1,
nil,
)
sdl.EndGPURenderPass(pass)
GLOB.cleared = true
}
return
}
bracket_start_abs := find_first_backdrop_in_layer(layer)
layer_end_abs := int(layer.sub_batch_start + layer.sub_batch_len)
if bracket_start_abs < 0 {
// Fast path: no backdrop in this layer; render the whole sub-batch range in one pass.
render_layer_sub_batch_range(
cmd_buffer,
render_texture,
swapchain_width,
swapchain_height,
clear_color,
layer,
int(layer.sub_batch_start),
layer_end_abs,
)
return
}
// Bracketed layer: Pass A → backdrop bracket → Pass B.
// See README.md § "Backdrop pipeline" for the full ordering semantics.
render_layer_sub_batch_range(
cmd_buffer,
render_texture,
swapchain_width,
swapchain_height,
clear_color,
layer,
int(layer.sub_batch_start),
bracket_start_abs,
)
run_backdrop_bracket(cmd_buffer, layer, swapchain_width, swapchain_height)
// Pass B: render the [bracket_start_abs, layer_end_abs) range. .Backdrop sub-batches in
// this range are dispatched by the bracket above and ignored here (the .Backdrop case in
// the inner switch is a no-op). LOAD is implied because Pass A or the bracket's V-
// composite has already touched render_texture.
render_layer_sub_batch_range(
cmd_buffer,
render_texture,
swapchain_width,
swapchain_height,
clear_color,
layer,
bracket_start_abs,
layer_end_abs,
)
}
// Render a sub-range of a layer's sub-batches in a single render pass. Iterates the layer's
// scissors and walks each scissor's sub-batches, dispatching by kind. The `range_start_abs`
// and `range_end_abs` parameters are absolute indices into GLOB.tmp_sub_batches; only sub-
// batches within `[range_start_abs, range_end_abs)` are drawn.
//
// .Backdrop sub-batches in the range are always silently skipped — they are dispatched by
// run_backdrop_bracket, not here. The empty .Backdrop case in the inner switch enforces this.
//
// Render-pass setup mirrors the original draw_layer: clear-or-load based on GLOB.cleared,
// pipeline + storage + index buffer bound up front, then per-batch state tracking. After this
// proc returns, GLOB.cleared is guaranteed true.
//
// If the range is empty after filtering (no eligible sub-batches at all), this proc still
// honors the no-clear-yet contract by issuing a clear-only pass when needed; otherwise it
// returns without opening a render pass.
@(private)
render_layer_sub_batch_range :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer,
render_texture: ^sdl.GPUTexture,
swapchain_width: u32,
swapchain_height: u32,
clear_color: [4]f32,
layer: ^Layer,
range_start_abs: int,
range_end_abs: int,
) {
if range_start_abs >= range_end_abs {
// Empty range. If we still owe a clear, do a clear-only pass; otherwise nothing to do.
if !GLOB.cleared {
pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
load_op = .CLEAR,
store_op = .STORE,
},
1,
nil,
)
sdl.EndGPURenderPass(pass)
GLOB.cleared = true
}
return
}
render_pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
load_op = GLOB.cleared ? .LOAD : .CLEAR,
store_op = .STORE,
},
1,
nil,
)
GLOB.cleared = true
sdl.BindGPUGraphicsPipeline(render_pass, GLOB.pipeline_2d_base.sdl_pipeline)
// Bind storage buffer (read by vertex shader in SDF mode)
sdl.BindGPUVertexStorageBuffers(
render_pass,
0,
([^]^sdl.GPUBuffer)(&GLOB.pipeline_2d_base.primitive_buffer.gpu),
1,
)
// Always bind index buffer — harmless if no indexed draws are issued
sdl.BindGPUIndexBuffer(
render_pass,
sdl.GPUBufferBinding{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0},
._32BIT,
)
// Shorthand aliases for frequently-used pipeline resources
main_vert_buf := GLOB.pipeline_2d_base.vertex_buffer.gpu
unit_quad := GLOB.pipeline_2d_base.unit_quad_buffer
white_texture := GLOB.pipeline_2d_base.white_texture
sampler := GLOB.pipeline_2d_base.sampler
width := f32(swapchain_width)
height := f32(swapchain_height)
// Initial GPU state: tessellated mode, main vertex buffer, no atlas bound yet
push_globals(cmd_buffer, width, height, .Tessellated)
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
current_mode: Draw_Mode = .Tessellated
current_vert_buf := main_vert_buf
current_atlas: ^sdl.GPUTexture
current_sampler := sampler
// Text vertices live after shape vertices in the GPU vertex buffer
text_vertex_gpu_base := u32(len(GLOB.tmp_shape_verts))
for &scissor in GLOB.scissors[layer.scissor_start:][:layer.scissor_len] {
// Intersect this scissor's sub-batch span with the requested range.
scissor_start := int(scissor.sub_batch_start)
scissor_end := scissor_start + int(scissor.sub_batch_len)
effective_start := max(scissor_start, range_start_abs)
effective_end := min(scissor_end, range_end_abs)
if effective_start >= effective_end do continue
sdl.SetGPUScissor(render_pass, scissor.bounds)
for abs_idx in effective_start ..< effective_end {
batch := &GLOB.tmp_sub_batches[abs_idx]
switch batch.kind {
case .Tessellated:
if current_mode != .Tessellated {
push_globals(cmd_buffer, width, height, .Tessellated)
current_mode = .Tessellated
}
if current_vert_buf != main_vert_buf {
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
current_vert_buf = main_vert_buf
}
// Determine texture and sampler for this batch
batch_texture: ^sdl.GPUTexture = white_texture
batch_sampler: ^sdl.GPUSampler = sampler
if batch.texture_id != INVALID_TEXTURE {
if bound_texture := texture_gpu_handle(batch.texture_id); bound_texture != nil {
batch_texture = bound_texture
}
batch_sampler = get_sampler(batch.sampler)
}
if current_atlas != batch_texture || current_sampler != batch_sampler {
sdl.BindGPUFragmentSamplers(
render_pass,
0,
&sdl.GPUTextureSamplerBinding{texture = batch_texture, sampler = batch_sampler},
1,
)
current_atlas = batch_texture
current_sampler = batch_sampler
}
sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)
case .Text:
if current_mode != .Tessellated {
push_globals(cmd_buffer, width, height, .Tessellated)
current_mode = .Tessellated
}
if current_vert_buf != main_vert_buf {
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
current_vert_buf = main_vert_buf
}
text_batch := &GLOB.tmp_text_batches[batch.offset]
if current_atlas != text_batch.atlas_texture {
sdl.BindGPUFragmentSamplers(
render_pass,
0,
&sdl.GPUTextureSamplerBinding{texture = text_batch.atlas_texture, sampler = sampler},
1,
)
current_atlas = text_batch.atlas_texture
}
sdl.DrawGPUIndexedPrimitives(
render_pass,
text_batch.index_count,
1,
text_batch.index_start,
i32(text_vertex_gpu_base + text_batch.vertex_start),
0,
)
case .SDF:
if current_mode != .SDF {
push_globals(cmd_buffer, width, height, .SDF)
current_mode = .SDF
}
if current_vert_buf != unit_quad {
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = unit_quad, offset = 0}, 1)
current_vert_buf = unit_quad
}
// Determine texture and sampler for this batch
batch_texture: ^sdl.GPUTexture = white_texture
batch_sampler: ^sdl.GPUSampler = sampler
if batch.texture_id != INVALID_TEXTURE {
if bound_texture := texture_gpu_handle(batch.texture_id); bound_texture != nil {
batch_texture = bound_texture
}
batch_sampler = get_sampler(batch.sampler)
}
if current_atlas != batch_texture || current_sampler != batch_sampler {
sdl.BindGPUFragmentSamplers(
render_pass,
0,
&sdl.GPUTextureSamplerBinding{texture = batch_texture, sampler = batch_sampler},
1,
)
current_atlas = batch_texture
current_sampler = batch_sampler
}
sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset)
case .Backdrop:
// Always a no-op here. Backdrop sub-batches are dispatched by run_backdrop_bracket;
// when this proc encounters one (only possible in Pass B, since Pass A and the no-
// backdrop fast path both stop their range before any .Backdrop index), we skip it.
}
}
}
sdl.EndGPURenderPass(render_pass)
}
destroy_pipeline_2d_base :: proc(device: ^sdl.GPUDevice, pipeline: ^Pipeline_2D_Base) {
destroy_buffer(device, &pipeline.vertex_buffer)
destroy_buffer(device, &pipeline.index_buffer)
destroy_buffer(device, &pipeline.primitive_buffer)
if pipeline.unit_quad_buffer != nil {
sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer)
}
sdl.ReleaseGPUTexture(device, pipeline.white_texture)
sdl.ReleaseGPUSampler(device, pipeline.sampler)
sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
}
@@ -52,7 +52,7 @@ struct Uniforms
float2 _pad0; float2 _pad0;
}; };
struct Backdrop_Primitive struct Gaussian_Blur_Primitive
{ {
float4 bounds; float4 bounds;
float4 radii; float4 radii;
@@ -61,7 +61,7 @@ struct Backdrop_Primitive
uint color; uint color;
}; };
struct Backdrop_Primitive_1 struct Gaussian_Blur_Primitive_1
{ {
float4 bounds; float4 bounds;
float4 radii; float4 radii;
@@ -70,9 +70,9 @@ struct Backdrop_Primitive_1
uint color; uint color;
}; };
struct Backdrop_Primitives struct Gaussian_Blur_Primitives
{ {
Backdrop_Primitive_1 primitives[1]; Gaussian_Blur_Primitive_1 primitives[1];
}; };
constant spvUnsafeArray<float2, 6> _97 = spvUnsafeArray<float2, 6>({ float2(0.0), float2(1.0, 0.0), float2(0.0, 1.0), float2(0.0, 1.0), float2(1.0, 0.0), float2(1.0) }); constant spvUnsafeArray<float2, 6> _97 = spvUnsafeArray<float2, 6>({ float2(0.0), float2(1.0, 0.0), float2(0.0, 1.0), float2(0.0, 1.0), float2(1.0, 0.0), float2(1.0) });
@@ -87,7 +87,7 @@ struct main0_out
float4 gl_Position [[position]]; float4 gl_Position [[position]];
}; };
vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Backdrop_Primitives& _69 [[buffer(1)]], uint gl_VertexIndex [[vertex_id]], uint gl_InstanceIndex [[instance_id]]) vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Gaussian_Blur_Primitives& _69 [[buffer(1)]], uint gl_VertexIndex [[vertex_id]], uint gl_InstanceIndex [[instance_id]])
{ {
main0_out out = {}; main0_out out = {};
if (_13.mode == 0u) if (_13.mode == 0u)
@@ -102,7 +102,7 @@ vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Backdr
} }
else else
{ {
Backdrop_Primitive p; Gaussian_Blur_Primitive p;
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds; p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
p.radii = _69.primitives[int(gl_InstanceIndex)].radii; p.radii = _69.primitives[int(gl_InstanceIndex)].radii;
p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size; p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size;
Binary file not shown.
+6 -6
View File
@@ -10,7 +10,7 @@ struct Uniforms
uint mode; uint mode;
}; };
struct Base_2D_Primitive struct Core_2D_Primitive
{ {
float4 bounds; float4 bounds;
uint color; uint color;
@@ -23,7 +23,7 @@ struct Base_2D_Primitive
uint4 effects; uint4 effects;
}; };
struct Base_2D_Primitive_1 struct Core_2D_Primitive_1
{ {
float4 bounds; float4 bounds;
uint color; uint color;
@@ -36,9 +36,9 @@ struct Base_2D_Primitive_1
uint4 effects; uint4 effects;
}; };
struct Base_2D_Primitives struct Core_2D_Primitives
{ {
Base_2D_Primitive_1 primitives[1]; Core_2D_Primitive_1 primitives[1];
}; };
struct main0_out struct main0_out
@@ -60,7 +60,7 @@ struct main0_in
float4 v_color [[attribute(2)]]; float4 v_color [[attribute(2)]];
}; };
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Base_2D_Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]]) vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Core_2D_Primitives& _75 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
{ {
main0_out out = {}; main0_out out = {};
if (_12.mode == 0u) if (_12.mode == 0u)
@@ -76,7 +76,7 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
} }
else else
{ {
Base_2D_Primitive p; Core_2D_Primitive p;
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds; p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
p.color = _75.primitives[int(gl_InstanceIndex)].color; p.color = _75.primitives[int(gl_InstanceIndex)].color;
p.flags = _75.primitives[int(gl_InstanceIndex)].flags; p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
Binary file not shown.
+10 -10
View File
@@ -3,7 +3,7 @@
// Unified backdrop blur vertex shader. // Unified backdrop blur vertex shader.
// Handles both the 1D separable blur passes (fullscreen triangle, mode 0; used for // Handles both the 1D separable blur passes (fullscreen triangle, mode 0; used for
// BOTH the H-pass and V-pass) and the composite pass (instanced unit-quad over // BOTH the H-pass and V-pass) and the composite pass (instanced unit-quad over
// Backdrop_Primitive storage buffer, mode 1) for the second PSO of the backdrop bracket. // Gaussian_Blur_Primitive storage buffer, mode 1) for the second PSO of the backdrop bracket.
// The first PSO (downsample) uses backdrop_fullscreen.vert. // The first PSO (downsample) uses backdrop_fullscreen.vert.
// //
// No vertex buffer for either mode. Mode 0 uses gl_VertexIndex 0..2 for a single // No vertex buffer for either mode. Mode 0 uses gl_VertexIndex 0..2 for a single
@@ -33,7 +33,7 @@ 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
// Vertex_Uniforms. `mode` selects between H-blur (0) and V-composite (1). // Vertex_Uniforms_2D. `mode` selects between H-blur (0) and V-composite (1).
layout(set = 1, binding = 0) uniform Uniforms { layout(set = 1, binding = 0) uniform Uniforms {
mat4 projection; mat4 projection;
float dpi_scale; float dpi_scale;
@@ -41,18 +41,18 @@ layout(set = 1, binding = 0) uniform Uniforms {
vec2 _pad0; vec2 _pad0;
}; };
// --- Backdrop primitive storage buffer (set 0) --- // --- Gaussian blur primitive storage buffer (set 0) ---
// 48 bytes, std430-natural layout (no implicit padding). vec4 members are // 48 bytes, std430-natural layout (no implicit padding). vec4 members are
// front-loaded so their 16-byte alignment is satisfied without holes; the // front-loaded so their 16-byte alignment is satisfied without holes; the
// vec2 and scalar tail packs tight to land the struct at a clean 48-byte // vec2 and scalar tail packs tight to land the struct at a clean 48-byte
// stride (a multiple of 16, so the array stride needs no rounding either). // stride (a multiple of 16, so the array stride needs no rounding either).
// Field semantics match the CPU-side Backdrop_Primitive declared in // Field semantics match the CPU-side Gaussian_Blur_Primitive declared in
// levlib/draw/backdrop.odin; keep both in sync. // levlib/draw/backdrop.odin; keep both in sync.
// //
// Backdrop primitives are tint-only: outline is intentionally absent. Specialized // Gaussian blur primitives are tint-only: outline is intentionally absent. Specialized
// 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 Backdrop_Primitive { struct Gaussian_Blur_Primitive {
vec4 bounds; // 0-15: min_xy, max_xy (world-space) vec4 bounds; // 0-15: min_xy, max_xy (world-space)
vec4 radii; // 16-31: per-corner radii (physical px) vec4 radii; // 16-31: per-corner radii (physical px)
vec2 half_size; // 32-39: RRect half extents (physical px) vec2 half_size; // 32-39: RRect half extents (physical px)
@@ -60,8 +60,8 @@ struct Backdrop_Primitive {
uint color; // 44-47: tint, packed RGBA u8x4 uint color; // 44-47: tint, packed RGBA u8x4
}; };
layout(std430, set = 0, binding = 0) readonly buffer Backdrop_Primitives { layout(std430, set = 0, binding = 0) readonly buffer Gaussian_Blur_Primitives {
Backdrop_Primitive primitives[]; Gaussian_Blur_Primitive primitives[];
}; };
void main() { void main() {
@@ -82,8 +82,8 @@ void main() {
f_radii = vec4(0.0); f_radii = vec4(0.0);
f_half_feather = 0.0; f_half_feather = 0.0;
} else { } else {
// ---- Mode 1: V-composite instanced unit-quad over Backdrop_Primitive ---- // ---- Mode 1: V-composite instanced unit-quad over Gaussian_Blur_Primitive ----
Backdrop_Primitive p = primitives[gl_InstanceIndex]; Gaussian_Blur_Primitive p = primitives[gl_InstanceIndex];
// Unit-quad corners for TRIANGLELIST (2 triangles, 6 vertices): // Unit-quad corners for TRIANGLELIST (2 triangles, 6 vertices):
// index 0 -> (0,0) index 3 -> (0,1) // index 0 -> (0,0) index 3 -> (0,1)
+8 -8
View File
@@ -26,8 +26,8 @@
// working-texture coords (work_region_phys / factor), clamped to the texture bounds. // working-texture coords (work_region_phys / factor), clamped to the texture bounds.
layout(set = 3, binding = 0) uniform Uniforms { layout(set = 3, binding = 0) uniform Uniforms {
vec2 inv_source_size; // 1.0 / source_texture pixel dimensions vec2 inv_source_size; // 1.0 / source_texture pixel dimensions
uint downsample_factor; // 1, 2, 4, 8, or 16 uint downsample_factor; // 1, 2, 4, 8, or 16
uint _pad0; uint _pad0;
}; };
@@ -55,13 +55,13 @@ void main() {
// bilinear level), giving a 4-tap = 16-source-pixel uniform sample of the block. // bilinear level), giving a 4-tap = 16-source-pixel uniform sample of the block.
float off = float(downsample_factor) * 0.25; float off = float(downsample_factor) * 0.25;
vec2 uv_tl = (src_block_center + vec2(-off, -off)) * inv_source_size; vec2 uv_tl = (src_block_center + vec2(-off, -off)) * inv_source_size;
vec2 uv_tr = (src_block_center + vec2( off, -off)) * inv_source_size; vec2 uv_tr = (src_block_center + vec2(off, -off)) * inv_source_size;
vec2 uv_bl = (src_block_center + vec2(-off, off)) * inv_source_size; vec2 uv_bl = (src_block_center + vec2(-off, off)) * inv_source_size;
vec2 uv_br = (src_block_center + vec2( off, off)) * inv_source_size; vec2 uv_br = (src_block_center + vec2(off, off)) * inv_source_size;
vec4 c = texture(source_tex, uv_tl) vec4 c = texture(source_tex, uv_tl)
+ texture(source_tex, uv_tr) + texture(source_tex, uv_tr)
+ texture(source_tex, uv_bl) + texture(source_tex, uv_bl)
+ texture(source_tex, uv_br); + texture(source_tex, uv_br);
out_color = c * 0.25; out_color = c * 0.25;
} }
} }
+6 -6
View File
@@ -23,10 +23,10 @@ layout(set = 1, binding = 0) uniform Uniforms {
}; };
// ---------- SDF primitive storage buffer ---------- // ---------- SDF primitive storage buffer ----------
// Mirrors the CPU-side Base_2D_Primitive in pipeline_2d_base.odin. Named with the // Mirrors the CPU-side Core_2D_Primitive in core_2d.odin. Named with the
// pipeline prefix so a project-wide grep on the type name matches both the GLSL // subsystem prefix so a project-wide grep on the type name matches both the GLSL
// declaration and the Odin declaration. // declaration and the Odin declaration.
struct Base_2D_Primitive { struct Core_2D_Primitive {
vec4 bounds; // 0-15 vec4 bounds; // 0-15
uint color; // 16-19 uint color; // 16-19
uint flags; // 20-23 uint flags; // 20-23
@@ -38,8 +38,8 @@ struct Base_2D_Primitive {
uvec4 effects; // 80-95: gradient/outline parameters (read when .Gradient/.Outline) uvec4 effects; // 80-95: gradient/outline parameters (read when .Gradient/.Outline)
}; };
layout(std430, set = 0, binding = 0) readonly buffer Base_2D_Primitives { layout(std430, set = 0, binding = 0) readonly buffer Core_2D_Primitives {
Base_2D_Primitive primitives[]; Core_2D_Primitive primitives[];
}; };
// ---------- Entry point ---------- // ---------- Entry point ----------
@@ -57,7 +57,7 @@ void main() {
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0); gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
} else { } else {
// ---- Mode 1: SDF instanced quads ---- // ---- Mode 1: SDF instanced quads ----
Base_2D_Primitive p = primitives[gl_InstanceIndex]; Core_2D_Primitive p = primitives[gl_InstanceIndex];
vec2 corner = v_position; // unit quad corners: (0,0)-(1,1) vec2 corner = v_position; // unit quad corners: (0,0)-(1,1)
vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner); vec2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
-765
View File
@@ -1,765 +0,0 @@
package draw
import "core:math"
// ----- Internal helpers ----
// Internal
extrude_line :: proc(
start, end_pos: Vec2,
thickness: f32,
color: Color,
vertices: []Vertex,
offset: int,
) -> int {
direction := end_pos - start
delta_x := direction[0]
delta_y := direction[1]
length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
if length < 0.0001 do return 0
scale := thickness / (2 * length)
perpendicular := Vec2{-delta_y * scale, delta_x * scale}
p0 := start + perpendicular
p1 := start - perpendicular
p2 := end_pos - perpendicular
p3 := end_pos + perpendicular
vertices[offset + 0] = solid_vertex(p0, color)
vertices[offset + 1] = solid_vertex(p1, color)
vertices[offset + 2] = solid_vertex(p2, color)
vertices[offset + 3] = solid_vertex(p0, color)
vertices[offset + 4] = solid_vertex(p2, color)
vertices[offset + 5] = solid_vertex(p3, color)
return 6
}
// Create a vertex for solid-color shape drawing (no texture, UV defaults to zero).
// Color is premultiplied: the tessellated fragment shader passes it through directly
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
solid_vertex :: proc(position: Vec2, color: Color) -> Vertex {
return Vertex{position = position, color = premultiply_color(color)}
}
emit_rectangle :: proc(x, y, width, height: f32, color: Color, vertices: []Vertex, offset: int) {
vertices[offset + 0] = solid_vertex({x, y}, color)
vertices[offset + 1] = solid_vertex({x + width, y}, color)
vertices[offset + 2] = solid_vertex({x + width, y + height}, color)
vertices[offset + 3] = solid_vertex({x, y}, color)
vertices[offset + 4] = solid_vertex({x + width, y + height}, color)
vertices[offset + 5] = solid_vertex({x, y + height}, color)
}
// Internal — submit an SDF primitive with optional texture binding.
// The texture-aware counterpart of `draw.prepare_sdf_primitive`; lets shape procs route a
// texture_id and sampler into the sub-batch without growing the public API.
@(private)
prepare_sdf_primitive_ex :: proc(
layer: ^Layer,
prim: Base_2D_Primitive,
texture_id: Texture_Id = INVALID_TEXTURE,
sampler: Sampler_Preset = DFT_SAMPLER,
) {
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, texture_id, sampler)
}
// Internal — resolve Texture_Fill zero-initialized fields to their defaults.
// Odin structs zero-initialize; Color{} and Rectangle{} are all-zero which is not a
// useful tint or UV rect. This proc substitutes sensible defaults for zero values.
@(private)
resolve_texture_defaults :: #force_inline proc(
tf: Texture_Fill,
) -> (
tint: Color,
uv: Rectangle,
sampler: Sampler_Preset,
) {
tint = tf.tint == Color{} ? DFT_TINT : tf.tint
uv = tf.uv_rect == Rectangle{} ? DFT_UV_RECT : tf.uv_rect
sampler = tf.sampler
return
}
//Internal
//
// Compute the visual center of a center-parametrized shape after applying
// Convention B origin semantics: `center` is where the origin-point lands in
// world space; the visual center is offset by -origin and then rotated around
// the landing point.
// visual_center = center + R(θ) · (-origin)
// When θ=0: visual_center = center - origin (pure positioning shift).
// When origin={0,0}: visual_center = center (no change).
compute_pivot_center :: proc(center: Vec2, origin: Vec2, sin_angle, cos_angle: f32) -> Vec2 {
if origin == {0, 0} do return center
return(
center +
{cos_angle * (-origin.x) - sin_angle * (-origin.y), sin_angle * (-origin.x) + cos_angle * (-origin.y)} \
)
}
// Compute the AABB half-extents of a rectangle with half-size (half_width, half_height) rotated by the given cos/sin.
rotated_aabb_half_extents :: proc(half_width, half_height, cos_angle, sin_angle: f32) -> [2]f32 {
cos_abs := abs(cos_angle)
sin_abs := abs(sin_angle)
return {half_width * cos_abs + half_height * sin_abs, half_width * sin_abs + half_height * cos_abs}
}
// Pack sin/cos into the Base_2D_Primitive.rotation_sc field as two f16 values.
pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 {
return pack_f16_pair(f16(sin_angle), f16(cos_angle))
}
// Internal
//
// Build an RRect Base_2D_Primitive with bounds, params, and rotation computed from rectangle geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_rrect_primitive :: proc(
rect: Rectangle,
radii: Rectangle_Radii,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> Base_2D_Primitive {
max_radius := min(rect.width, rect.height) * 0.5
clamped_top_left := clamp(radii.top_left, 0, max_radius)
clamped_top_right := clamp(radii.top_right, 0, max_radius)
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
half_width := rect.width * 0.5
half_height := rect.height * 0.5
center_x := rect.x + half_width - origin.x
center_y := rect.y + half_height - origin.y
sin_angle: f32 = 0
cos_angle: f32 = 1
has_rotation := false
if needs_transform(origin, rotation) {
rotation_radians := math.to_radians(rotation)
sin_angle, cos_angle = math.sincos(rotation_radians)
has_rotation = rotation != 0
transform := build_pivot_rotation_sc({rect.x + origin.x, rect.y + origin.y}, origin, cos_angle, sin_angle)
new_center := apply_transform(transform, {half_width, half_height})
center_x = new_center.x
center_y = new_center.y
}
bounds_half_width, bounds_half_height := half_width, half_height
if has_rotation {
expanded := rotated_aabb_half_extents(half_width, half_height, cos_angle, sin_angle)
bounds_half_width = expanded.x
bounds_half_height = expanded.y
}
prim := Base_2D_Primitive {
bounds = {
center_x - bounds_half_width - padding,
center_y - bounds_half_height - padding,
center_x + bounds_half_width + padding,
center_y + bounds_half_height + padding,
},
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
}
prim.params.rrect = RRect_Params {
half_size = {half_width * dpi_scale, half_height * dpi_scale},
radii = {
clamped_bottom_right * dpi_scale,
clamped_top_right * dpi_scale,
clamped_bottom_left * dpi_scale,
clamped_top_left * dpi_scale,
},
half_feather = half_feather,
}
return prim
}
// Internal
//
// Build an RRect Base_2D_Primitive for a circle (fully-rounded square RRect).
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_circle_primitive :: proc(
center: Vec2,
radius: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> Base_2D_Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
if origin != {0, 0} {
sin_a, cos_a := math.sincos(math.to_radians(rotation))
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
}
prim := Base_2D_Primitive {
bounds = {
actual_center.x - radius - padding,
actual_center.y - radius - padding,
actual_center.x + radius + padding,
actual_center.y + radius + padding,
},
}
scaled_radius := radius * dpi_scale
prim.params.rrect = RRect_Params {
half_size = {scaled_radius, scaled_radius},
radii = {scaled_radius, scaled_radius, scaled_radius, scaled_radius},
half_feather = half_feather,
}
return prim
}
// Internal
//
// Build an Ellipse Base_2D_Primitive with bounds, params, and rotation computed from ellipse geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_ellipse_primitive :: proc(
center: Vec2,
radius_horizontal, radius_vertical: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> Base_2D_Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
sin_angle: f32 = 0
cos_angle: f32 = 1
has_rotation := false
if needs_transform(origin, rotation) {
rotation_radians := math.to_radians(rotation)
sin_angle, cos_angle = math.sincos(rotation_radians)
actual_center = compute_pivot_center(center, origin, sin_angle, cos_angle)
has_rotation = rotation != 0
}
bound_horizontal, bound_vertical := radius_horizontal, radius_vertical
if has_rotation {
expanded := rotated_aabb_half_extents(radius_horizontal, radius_vertical, cos_angle, sin_angle)
bound_horizontal = expanded.x
bound_vertical = expanded.y
}
prim := Base_2D_Primitive {
bounds = {
actual_center.x - bound_horizontal - padding,
actual_center.y - bound_vertical - padding,
actual_center.x + bound_horizontal + padding,
actual_center.y + bound_vertical + padding,
},
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
}
prim.params.ellipse = Ellipse_Params {
radii = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
half_feather = half_feather,
}
return prim
}
// Internal
//
// Build an NGon Base_2D_Primitive with bounds, params, and rotation computed from polygon geometry.
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_polygon_primitive :: proc(
center: Vec2,
sides: int,
radius: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> Base_2D_Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
if origin != {0, 0} && rotation != 0 {
sin_a, cos_a := math.sincos(math.to_radians(rotation))
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
}
rotation_radians := math.to_radians(rotation)
sin_rot, cos_rot := math.sincos(rotation_radians)
prim := Base_2D_Primitive {
bounds = {
actual_center.x - radius - padding,
actual_center.y - radius - padding,
actual_center.x + radius + padding,
actual_center.y + radius + padding,
},
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
sides = f32(sides),
half_feather = half_feather,
}
return prim
}
// Internal
//
// Build a Ring_Arc Base_2D_Primitive with bounds and params computed from ring/arc geometry.
// Pre-computes the angular boundary normals on the CPU so the fragment shader needs
// no per-pixel sin/cos. The radial SDF uses max(inner-r, r-outer) which correctly
// handles pie slices (inner_radius = 0) and full rings.
// The caller sets color, flags, and uv fields on the returned primitive before submitting.
build_ring_arc_primitive :: proc(
center: Vec2,
inner_radius, outer_radius: f32,
start_angle: f32,
end_angle: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
) -> (
Base_2D_Primitive,
Shape_Flags,
) {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
rotation_offset: f32 = 0
if needs_transform(origin, rotation) {
sin_a, cos_a := math.sincos(math.to_radians(rotation))
actual_center = compute_pivot_center(center, origin, sin_a, cos_a)
rotation_offset = math.to_radians(rotation)
}
start_rad := math.to_radians(start_angle) + rotation_offset
end_rad := math.to_radians(end_angle) + rotation_offset
// Normalize arc span to [0, 2π]
arc_span := end_rad - start_rad
if arc_span < 0 {
arc_span += 2 * math.PI
}
// Pre-compute edge normals and arc flags on CPU — no per-pixel trig needed.
// arc_flags: {} = full ring, {.Arc_Narrow} = span ≤ π (intersect), {.Arc_Wide} = span > π (union)
arc_flags: Shape_Flags = {}
normal_start: [2]f32 = {}
normal_end: [2]f32 = {}
if arc_span < 2 * math.PI - 0.001 {
sin_start, cos_start := math.sincos(start_rad)
sin_end, cos_end := math.sincos(end_rad)
normal_start = {sin_start, -cos_start}
normal_end = {-sin_end, cos_end}
arc_flags = arc_span <= math.PI ? {.Arc_Narrow} : {.Arc_Wide}
}
prim := Base_2D_Primitive {
bounds = {
actual_center.x - outer_radius - padding,
actual_center.y - outer_radius - padding,
actual_center.x + outer_radius + padding,
actual_center.y + outer_radius + padding,
},
}
prim.params.ring_arc = Ring_Arc_Params {
inner_radius = inner_radius * dpi_scale,
outer_radius = outer_radius * dpi_scale,
normal_start = normal_start,
normal_end = normal_end,
half_feather = half_feather,
}
return prim, arc_flags
}
// Apply brush fill and outline to a primitive, then submit it.
// Dispatches to the correct sub-batch based on the Brush variant.
// All parameters (outline_width) are in logical pixels, matching the rest of the public API.
// The helper converts to physical pixels for GPU packing internally.
@(private)
apply_brush_and_outline :: proc(
layer: ^Layer,
prim: ^Base_2D_Primitive,
kind: Shape_Kind,
brush: Brush,
outline_color: Color,
outline_width: f32,
extra_flags: Shape_Flags = {},
) {
flags: Shape_Flags = extra_flags
// Fill — determined by the Brush variant.
texture_id := INVALID_TEXTURE
sampler := DFT_SAMPLER
switch b in brush {
case Color: prim.color = b
case Linear_Gradient:
flags += {.Gradient}
prim.color = b.start_color
prim.effects.gradient_color = b.end_color
rad := math.to_radians(b.angle)
sin_a, cos_a := math.sincos(rad)
prim.effects.gradient_dir_sc = pack_f16_pair(f16(cos_a), f16(sin_a))
case Radial_Gradient:
flags += {.Gradient, .Gradient_Radial}
prim.color = b.inner_color
prim.effects.gradient_color = b.outer_color
case Texture_Fill:
flags += {.Textured}
tint, uv, sam := resolve_texture_defaults(b)
prim.color = tint
prim.uv_rect = {uv.x, uv.y, uv.width, uv.height}
texture_id = b.id
sampler = sam
}
// Outline — orthogonal to all Brush variants.
if outline_width > 0 {
flags += {.Outline}
prim.effects.outline_color = outline_color
prim.effects.outline_packed = pack_f16_pair(f16(outline_width * GLOB.dpi_scaling), 0)
// Expand bounds to contain the outline (bounds are in logical pixels)
prim.bounds[0] -= outline_width
prim.bounds[1] -= outline_width
prim.bounds[2] += outline_width
prim.bounds[3] += outline_width
}
// Set .Rotated flag if rotation_sc was populated by the build proc
if prim.rotation_sc != 0 {
flags += {.Rotated}
}
prim.flags = pack_kind_flags(kind, flags)
prepare_sdf_primitive_ex(layer, prim^, texture_id, sampler)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Rectangle procs -----------
// ---------------------------------------------------------------------------------------------------------------------
// Draw a filled rectangle via SDF with optional per-corner rounding radii.
// Use `uniform_radii(rect, roundness)` to compute uniform radii from a 01 fraction.
//
// Origin semantics:
// `origin` is a local offset from the rect's top-left corner that selects both the positioning
// anchor and the rotation pivot. `rect.x, rect.y` specifies where that anchor point lands in
// world space. When `origin = {0, 0}` (default), `rect.x, rect.y` is the top-left corner.
// Rotation always occurs around the anchor point.
rectangle :: proc(
layer: ^Layer,
rect: Rectangle,
brush: Brush,
outline_color: Color = {},
outline_width: f32 = 0,
radii: Rectangle_Radii = {},
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Circle procs (emit RRect primitives) ------
// ---------------------------------------------------------------------------------------------------------------------
// Draw a filled circle via SDF (emitted as a fully-rounded RRect).
//
// Origin semantics (Convention B):
// `origin` is a local offset from the shape's center that selects both the positioning anchor
// and the rotation pivot. The `center` parameter specifies where that anchor point lands in
// world space. When `origin = {0, 0}` (default), `center` is the visual center.
// When `origin = {r, 0}`, the point `r` pixels to the right of the shape center lands at
// `center`, shifting the shape left by `r`.
circle :: proc(
layer: ^Layer,
center: Vec2,
radius: f32,
brush: Brush,
outline_color: Color = {},
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Ellipse procs (emit Ellipse primitives) ---
// ---------------------------------------------------------------------------------------------------------------------
// Draw a filled ellipse via SDF.
// Origin semantics: see `circle`.
ellipse :: proc(
layer: ^Layer,
center: Vec2,
radius_horizontal, radius_vertical: f32,
brush: Brush,
outline_color: Color = {},
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
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)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Polygon procs (emit NGon primitives) ------
// ---------------------------------------------------------------------------------------------------------------------
// Draw a filled regular polygon via SDF.
// `sides` must be >= 3. The polygon is inscribed in a circle of the given `radius`.
// Origin semantics: see `circle`.
polygon :: proc(
layer: ^Layer,
center: Vec2,
sides: int,
radius: f32,
brush: Brush,
outline_color: Color = {},
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
if sides < 3 do return
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Ring / Arc procs (emit Ring_Arc primitives) ----
// ---------------------------------------------------------------------------------------------------------------------
// Draw a ring, arc, or pie slice via SDF.
// Full ring by default. Pass start_angle/end_angle (degrees) for partial arcs.
// Use inner_radius = 0 for pie slices (sectors).
// Origin semantics: see `circle`.
ring :: proc(
layer: ^Layer,
center: Vec2,
inner_radius, outer_radius: f32,
brush: Brush,
outline_color: Color = {},
outline_width: f32 = 0,
start_angle: f32 = 0,
end_angle: f32 = DFT_CIRC_END_ANGLE,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
prim, arc_flags := build_ring_arc_primitive(
center,
inner_radius,
outer_radius,
start_angle,
end_angle,
origin,
rotation,
feather_px,
)
apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF Line procs (emit rotated RRect primitives) ----
// ---------------------------------------------------------------------------------------------------------------------
// Draw a line segment via SDF (emitted as a rotated capsule-shaped RRect).
// Round caps are produced by setting corner radii equal to half the thickness.
line :: proc(
layer: ^Layer,
start_position, end_position: Vec2,
brush: Brush,
thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {},
outline_width: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
delta_x := end_position.x - start_position.x
delta_y := end_position.y - start_position.y
seg_length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
if seg_length < 0.0001 do return
rotation_radians := math.atan2(delta_y, delta_x)
sin_angle, cos_angle := math.sincos(rotation_radians)
center_x := (start_position.x + end_position.x) * 0.5
center_y := (start_position.y + end_position.y) * 0.5
half_length := seg_length * 0.5
half_thickness := thickness * 0.5
cap_radius := half_thickness
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
// Expand bounds for rotation
bounds_half := rotated_aabb_half_extents(half_length + cap_radius, half_thickness, cos_angle, sin_angle)
prim := Base_2D_Primitive {
bounds = {
center_x - bounds_half.x - padding,
center_y - bounds_half.y - padding,
center_x + bounds_half.x + padding,
center_y + bounds_half.y + padding,
},
rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
}
prim.params.rrect = RRect_Params {
half_size = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
radii = {
cap_radius * dpi_scale,
cap_radius * dpi_scale,
cap_radius * dpi_scale,
cap_radius * dpi_scale,
},
half_feather = half_feather,
}
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
}
// Draw a line strip via decomposed SDF line segments.
line_strip :: proc(
layer: ^Layer,
points: []Vec2,
brush: Brush,
thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {},
outline_width: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
) {
if len(points) < 2 do return
for i in 0 ..< len(points) - 1 {
line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_px)
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Helpers ----------------
// ---------------------------------------------------------------------------------------------------------------------
// Returns uniform radii (all corners the same) as a fraction of the shorter side.
// `roundness` is clamped to [0, 1]; 0 = sharp corners, 1 = fully rounded (stadium or circle).
uniform_radii :: #force_inline proc(rect: Rectangle, roundness: f32) -> Rectangle_Radii {
cr := min(rect.width, rect.height) * clamp(roundness, 0, 1) * 0.5
return {cr, cr, cr, cr}
}
// Return Vec2 pixel offsets for use as the `origin` parameter of draw calls.
// Composable with normal vector +/- arithmetic.
//
// Text anchor helpers are in text.odin (they depend on measure_text / SDL_ttf).
// ----- Rectangle anchors (origin measured from rectangle's top-left) ---------------------------------------------
center_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, rectangle.height * 0.5}
}
top_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, 0}
}
top_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, 0}
}
top_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, 0}
}
left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, rectangle.height * 0.5}
}
right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, rectangle.height * 0.5}
}
bottom_left_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {0, rectangle.height}
}
bottom_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width * 0.5, rectangle.height}
}
bottom_right_of_rectangle :: #force_inline proc(rectangle: Rectangle) -> Vec2 {
return {rectangle.width, rectangle.height}
}
// ----- Triangle anchors (origin measured from AABB top-left) -----------------------------------------------------
center_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
return (v1 + v2 + v3) / 3 - bounds_min
}
top_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
return {0, 0}
}
top_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_x := min(v1.x, v2.x, v3.x)
max_x := max(v1.x, v2.x, v3.x)
return {(max_x - min_x) * 0.5, 0}
}
top_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_x := min(v1.x, v2.x, v3.x)
max_x := max(v1.x, v2.x, v3.x)
return {max_x - min_x, 0}
}
left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_y := min(v1.y, v2.y, v3.y)
max_y := max(v1.y, v2.y, v3.y)
return {0, (max_y - min_y) * 0.5}
}
right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {bounds_max.x - bounds_min.x, (bounds_max.y - bounds_min.y) * 0.5}
}
bottom_left_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
min_y := min(v1.y, v2.y, v3.y)
max_y := max(v1.y, v2.y, v3.y)
return {0, max_y - min_y}
}
bottom_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return {(bounds_max.x - bounds_min.x) * 0.5, bounds_max.y - bounds_min.y}
}
bottom_right_of_triangle :: #force_inline proc(v1, v2, v3: Vec2) -> Vec2 {
bounds_min := Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
bounds_max := Vec2{max(v1.x, v2.x, v3.x), max(v1.y, v2.y, v3.y)}
return bounds_max - bounds_min
}
+20 -11
View File
@@ -4,6 +4,7 @@ import "core:math"
import draw ".." import draw ".."
//INTERNAL
SMOOTH_CIRCLE_ERROR_RATE :: 0.1 SMOOTH_CIRCLE_ERROR_RATE :: 0.1
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int { auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
@@ -22,11 +23,18 @@ auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
// Color is premultiplied: the tessellated fragment shader passes it through directly // Color is premultiplied: the tessellated fragment shader passes it through directly
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA. // and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex { //INTERNAL
return draw.Vertex{position = position, color = draw.premultiply_color(color)} solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex_2D {
return draw.Vertex_2D{position = position, color = draw.premultiply_color(color)}
} }
emit_rectangle :: proc(x, y, width, height: f32, color: draw.Color, vertices: []draw.Vertex, offset: int) { //INTERNAL
emit_rectangle :: proc(
x, y, width, height: f32,
color: draw.Color,
vertices: []draw.Vertex_2D,
offset: int,
) {
vertices[offset + 0] = solid_vertex({x, y}, color) vertices[offset + 0] = solid_vertex({x, y}, color)
vertices[offset + 1] = solid_vertex({x + width, y}, color) vertices[offset + 1] = solid_vertex({x + width, y}, color)
vertices[offset + 2] = solid_vertex({x + width, y + height}, color) vertices[offset + 2] = solid_vertex({x + width, y + height}, color)
@@ -35,11 +43,12 @@ emit_rectangle :: proc(x, y, width, height: f32, color: draw.Color, vertices: []
vertices[offset + 5] = solid_vertex({x, y + height}, color) vertices[offset + 5] = solid_vertex({x, y + height}, color)
} }
//INTERNAL
extrude_line :: proc( extrude_line :: proc(
start, end_pos: draw.Vec2, start, end_pos: draw.Vec2,
thickness: f32, thickness: f32,
color: draw.Color, color: draw.Color,
vertices: []draw.Vertex, vertices: []draw.Vertex_2D,
offset: int, offset: int,
) -> int { ) -> int {
direction := end_pos - start direction := end_pos - start
@@ -69,7 +78,7 @@ extrude_line :: proc(
// ----- Public draw ----- // ----- Public draw -----
pixel :: proc(layer: ^draw.Layer, pos: draw.Vec2, color: draw.Color) { pixel :: proc(layer: ^draw.Layer, pos: draw.Vec2, color: draw.Color) {
vertices: [6]draw.Vertex vertices: [6]draw.Vertex_2D
emit_rectangle(pos[0], pos[1], 1, 1, color, vertices[:], 0) emit_rectangle(pos[0], pos[1], 1, 1, color, vertices[:], 0)
draw.prepare_shape(layer, vertices[:]) draw.prepare_shape(layer, vertices[:])
} }
@@ -82,7 +91,7 @@ triangle :: proc(
rotation: f32 = 0, rotation: f32 = 0,
) { ) {
if !draw.needs_transform(origin, rotation) { if !draw.needs_transform(origin, rotation) {
vertices := [3]draw.Vertex{solid_vertex(v1, color), solid_vertex(v2, color), solid_vertex(v3, color)} vertices := [3]draw.Vertex_2D{solid_vertex(v1, color), solid_vertex(v2, color), solid_vertex(v3, color)}
draw.prepare_shape(layer, vertices[:]) draw.prepare_shape(layer, vertices[:])
return return
} }
@@ -91,7 +100,7 @@ triangle :: proc(
local_v1 := v1 - bounds_min local_v1 := v1 - bounds_min
local_v2 := v2 - bounds_min local_v2 := v2 - bounds_min
local_v3 := v3 - bounds_min local_v3 := v3 - bounds_min
vertices := [3]draw.Vertex { vertices := [3]draw.Vertex_2D {
solid_vertex(draw.apply_transform(transform, local_v1), color), solid_vertex(draw.apply_transform(transform, local_v1), color),
solid_vertex(draw.apply_transform(transform, local_v2), color), solid_vertex(draw.apply_transform(transform, local_v2), color),
solid_vertex(draw.apply_transform(transform, local_v3), color), solid_vertex(draw.apply_transform(transform, local_v3), color),
@@ -170,7 +179,7 @@ triangle_aa :: proc(
transparent := draw.BLANK transparent := draw.BLANK
// 3 interior + 6 × 3 edge-quad = 21 vertices // 3 interior + 6 × 3 edge-quad = 21 vertices
vertices: [21]draw.Vertex vertices: [21]draw.Vertex_2D
// Interior triangle // Interior triangle
vertices[0] = solid_vertex(p0, color) vertices[0] = solid_vertex(p0, color)
@@ -213,7 +222,7 @@ triangle_lines :: proc(
rotation: f32 = 0, rotation: f32 = 0,
temp_allocator := context.temp_allocator, temp_allocator := context.temp_allocator,
) { ) {
vertices := make([]draw.Vertex, 18, temp_allocator) vertices := make([]draw.Vertex_2D, 18, temp_allocator)
defer delete(vertices, temp_allocator) defer delete(vertices, temp_allocator)
write_offset := 0 write_offset := 0
@@ -249,7 +258,7 @@ triangle_fan :: proc(
triangle_count := len(points) - 2 triangle_count := len(points) - 2
vertex_count := triangle_count * 3 vertex_count := triangle_count * 3
vertices := make([]draw.Vertex, vertex_count, temp_allocator) vertices := make([]draw.Vertex_2D, vertex_count, temp_allocator)
defer delete(vertices, temp_allocator) defer delete(vertices, temp_allocator)
if !draw.needs_transform(origin, rotation) { if !draw.needs_transform(origin, rotation) {
@@ -289,7 +298,7 @@ triangle_strip :: proc(
triangle_count := len(points) - 2 triangle_count := len(points) - 2
vertex_count := triangle_count * 3 vertex_count := triangle_count * 3
vertices := make([]draw.Vertex, vertex_count, temp_allocator) vertices := make([]draw.Vertex_2D, vertex_count, temp_allocator)
defer delete(vertices, temp_allocator) defer delete(vertices, temp_allocator)
if !draw.needs_transform(origin, rotation) { if !draw.needs_transform(origin, rotation) {
+11 -3
View File
@@ -8,21 +8,25 @@ import sdl_ttf "vendor:sdl3/ttf"
Font_Id :: u16 Font_Id :: u16
//INTERNAL
Font_Key :: struct { Font_Key :: struct {
id: Font_Id, id: Font_Id,
size: u16, size: u16,
} }
//INTERNAL
Cache_Source :: enum u8 { Cache_Source :: enum u8 {
Custom, Custom,
Clay, Clay,
} }
//INTERNAL
Cache_Key :: struct { Cache_Key :: struct {
id: u32, id: u32,
source: Cache_Source, source: Cache_Source,
} }
//INTERNAL
Text_Cache :: struct { Text_Cache :: struct {
engine: ^sdl_ttf.TextEngine, engine: ^sdl_ttf.TextEngine,
font_bytes: [dynamic][]u8, font_bytes: [dynamic][]u8,
@@ -30,7 +34,8 @@ Text_Cache :: struct {
cache: map[Cache_Key]^sdl_ttf.Text, cache: map[Cache_Key]^sdl_ttf.Text,
} }
// Internal for fetching SDL TTF font pointer for rendering // Fetch SDL TTF font pointer for rendering.
//INTERNAL
get_font :: proc(id: Font_Id, size: u16) -> ^sdl_ttf.Font { get_font :: proc(id: Font_Id, size: u16) -> ^sdl_ttf.Font {
assert(int(id) < len(GLOB.text_cache.font_bytes), "Invalid font ID.") assert(int(id) < len(GLOB.text_cache.font_bytes), "Invalid font ID.")
key := Font_Key{id, size} key := Font_Key{id, size}
@@ -77,6 +82,7 @@ register_font :: proc(bytes: []u8) -> (id: Font_Id, ok: bool) #optional_ok {
return Font_Id(len(GLOB.text_cache.font_bytes) - 1), true return Font_Id(len(GLOB.text_cache.font_bytes) - 1), true
} }
//INTERNAL
Text :: struct { Text :: struct {
sdl_text: ^sdl_ttf.Text, sdl_text: ^sdl_ttf.Text,
position: Vec2, position: Vec2,
@@ -89,7 +95,7 @@ Text :: struct {
// Shared cache lookup/create/update logic used by both the `text` proc and the Clay render path. // Shared cache lookup/create/update logic used by both the `text` proc and the Clay render path.
// Returns the cached (or newly created) TTF_Text pointer. // Returns the cached (or newly created) TTF_Text pointer.
@(private) //INTERNAL
cache_get_or_update :: proc(key: Cache_Key, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text { cache_get_or_update :: proc(key: Cache_Key, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text {
existing, found := GLOB.text_cache.cache[key] existing, found := GLOB.text_cache.cache[key]
if !found { if !found {
@@ -268,7 +274,8 @@ clear_text_cache_entry :: proc(id: u32) {
// ----- Internal cache lifecycle ------ // ----- Internal cache lifecycle ------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@(private, require_results) //INTERNAL
@(require_results)
init_text_cache :: proc( init_text_cache :: proc(
device: ^sdl.GPUDevice, device: ^sdl.GPUDevice,
allocator := context.allocator, allocator := context.allocator,
@@ -299,6 +306,7 @@ init_text_cache :: proc(
return text_cache, true return text_cache, true
} }
//INTERNAL
destroy_text_cache :: proc() { destroy_text_cache :: proc() {
for _, font in GLOB.text_cache.sdl_fonts { for _, font in GLOB.text_cache.sdl_fonts {
sdl_ttf.CloseFont(font) sdl_ttf.CloseFont(font)
+11 -12
View File
@@ -41,8 +41,7 @@ Texture_Desc :: struct {
kind: Texture_Kind, kind: Texture_Kind,
} }
// Internal slot — not exported. //INTERNAL
@(private)
Texture_Slot :: struct { Texture_Slot :: struct {
gpu_texture: ^sdl.GPUTexture, gpu_texture: ^sdl.GPUTexture,
desc: Texture_Desc, desc: Texture_Desc,
@@ -319,8 +318,8 @@ texture_kind :: proc(id: Texture_Id) -> Texture_Kind {
return GLOB.texture_slots[u32(id)].desc.kind return GLOB.texture_slots[u32(id)].desc.kind
} }
// Internal: get the raw GPU texture pointer for binding during draw. // Get the raw GPU texture pointer for binding during draw.
@(private) //INTERNAL
texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture { texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture {
if id == INVALID_TEXTURE do return nil if id == INVALID_TEXTURE do return nil
idx := u32(id) idx := u32(id)
@@ -328,8 +327,8 @@ texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture {
return GLOB.texture_slots[idx].gpu_texture return GLOB.texture_slots[idx].gpu_texture
} }
// Deferred release (called from draw.end / clear_global) // Deferred release (called from end / clear_global).
@(private) //INTERNAL
process_pending_texture_releases :: proc() { process_pending_texture_releases :: proc() {
device := GLOB.device device := GLOB.device
for id in GLOB.pending_texture_releases { for id in GLOB.pending_texture_releases {
@@ -346,7 +345,7 @@ process_pending_texture_releases :: proc() {
clear(&GLOB.pending_texture_releases) clear(&GLOB.pending_texture_releases)
} }
@(private) //INTERNAL
get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler { get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler {
idx := int(preset) idx := int(preset)
if GLOB.samplers[idx] != nil do return GLOB.samplers[idx] if GLOB.samplers[idx] != nil do return GLOB.samplers[idx]
@@ -379,15 +378,15 @@ get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler {
) )
if sampler == nil { if sampler == nil {
log.errorf("Failed to create sampler preset %v: %s", preset, sdl.GetError()) log.errorf("Failed to create sampler preset %v: %s", preset, sdl.GetError())
return GLOB.pipeline_2d_base.sampler // fallback to existing default sampler return GLOB.core_2d.sampler // fallback to existing default sampler
} }
GLOB.samplers[idx] = sampler GLOB.samplers[idx] = sampler
return sampler return sampler
} }
// Internal: destroy all sampler pool entries. Called from draw.destroy(). // Destroy all sampler pool entries. Called from destroy().
@(private) //INTERNAL
destroy_sampler_pool :: proc() { destroy_sampler_pool :: proc() {
device := GLOB.device device := GLOB.device
for &s in GLOB.samplers { for &s in GLOB.samplers {
@@ -398,8 +397,8 @@ destroy_sampler_pool :: proc() {
} }
} }
// Internal: destroy all registered textures. Called from draw.destroy(). // Destroy all registered textures. Called from destroy().
@(private) //INTERNAL
destroy_all_textures :: proc() { destroy_all_textures :: proc() {
device := GLOB.device device := GLOB.device
for &slot in GLOB.texture_slots { for &slot in GLOB.texture_slots {