Major reorg
This commit is contained in:
+22
-22
@@ -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 1–4 dispatch to one of four SDF functions (RRect, NGon,
|
sample and computes `out = color * t`; kinds 1–4 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
@@ -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
File diff suppressed because it is too large
Load Diff
+291
-306
@@ -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,18 +61,24 @@ 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.
|
||||||
|
|
||||||
@@ -69,9 +88,9 @@ Global :: struct {
|
|||||||
clay_z_index: i16, // Tracks z-index for layer splitting during Clay batch processing.
|
clay_z_index: i16, // Tracks z-index for layer splitting during Clay batch processing.
|
||||||
cleared: bool, // Whether the render target has been cleared this frame.
|
cleared: bool, // Whether the render target has been cleared this frame.
|
||||||
|
|
||||||
// -- 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.
|
||||||
|
|
||||||
@@ -98,14 +117,14 @@ Global :: struct {
|
|||||||
max_text_batches: int,
|
max_text_batches: int,
|
||||||
max_primitives: int,
|
max_primitives: int,
|
||||||
max_sub_batches: int,
|
max_sub_batches: int,
|
||||||
max_backdrop_primitives: int,
|
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,35 +302,45 @@ 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(
|
||||||
|
[dynamic]Core_2D_Primitive,
|
||||||
|
0,
|
||||||
|
BUFFER_INIT_SIZE,
|
||||||
|
allocator = allocator,
|
||||||
|
),
|
||||||
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||||
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
||||||
tmp_backdrop_primitives = make([dynamic]Backdrop_Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
tmp_gaussian_blur_primitives = make(
|
||||||
|
[dynamic]Gaussian_Blur_Primitive,
|
||||||
|
0,
|
||||||
|
BUFFER_INIT_SIZE,
|
||||||
|
allocator = allocator,
|
||||||
|
),
|
||||||
device = device,
|
device = device,
|
||||||
texture_slots = make([dynamic]Texture_Slot, 0, 16, allocator = allocator),
|
texture_slots = make([dynamic]Texture_Slot, 0, 16, allocator = allocator),
|
||||||
texture_free_list = make([dynamic]u32, 0, 16, allocator = allocator),
|
texture_free_list = make([dynamic]u32, 0, 16, allocator = allocator),
|
||||||
@@ -309,8 +349,8 @@ init :: proc(
|
|||||||
odin_context = odin_context,
|
odin_context = odin_context,
|
||||||
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
||||||
clay_memory = make([^]u8, min_memory_size, allocator = allocator),
|
clay_memory = make([^]u8, min_memory_size, allocator = allocator),
|
||||||
pipeline_2d_base = pipeline,
|
core_2d = core,
|
||||||
pipeline_2d_backdrop = backdrop_pipeline,
|
backdrop = backdrop,
|
||||||
text_cache = text_cache,
|
text_cache = text_cache,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
||||||
for data != nil {
|
log.panicf("Failed to acquire swapchain texture: %s", sdl.GetError())
|
||||||
vertex_start := u32(len(GLOB.tmp_text_verts))
|
|
||||||
index_start := u32(len(GLOB.tmp_text_indices))
|
|
||||||
|
|
||||||
// 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
|
if swapchain_texture == nil {
|
||||||
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
|
// Window is minimized or not visible — submit and skip this frame
|
||||||
|
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
|
||||||
batch_idx := u32(len(GLOB.tmp_text_batches))
|
log.panicf("Failed to submit GPU command buffer (minimized window): %s", sdl.GetError())
|
||||||
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 {
|
||||||
// Premultiply text color once — reused across all glyph vertices.
|
ensure_backdrop_textures(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
|
||||||
pm_color := premultiply_color(text.color)
|
render_texture = GLOB.backdrop.source_texture
|
||||||
|
|
||||||
for data != nil {
|
|
||||||
vertex_start := u32(len(GLOB.tmp_text_verts))
|
|
||||||
index_start := u32(len(GLOB.tmp_text_indices))
|
|
||||||
|
|
||||||
for i in 0 ..< data.num_vertices {
|
|
||||||
pos := data.xy[i]
|
|
||||||
uv := data.uv[i]
|
|
||||||
// SDL_ttf gives glyph positions in physical pixels relative to text origin.
|
|
||||||
// The transform is already in physical-pixel space (caller pre-scaled),
|
|
||||||
// so we apply directly — no per-vertex DPI divide/multiply.
|
|
||||||
append(
|
|
||||||
&GLOB.tmp_text_verts,
|
|
||||||
Vertex{position = apply_transform(transform, {pos.x, -pos.y}), uv = {uv.x, uv.y}, color = pm_color},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
|
// 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,
|
||||||
|
}
|
||||||
|
|
||||||
batch_idx := u32(len(GLOB.tmp_text_batches))
|
// Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
|
||||||
append(
|
for &layer, index in GLOB.layers {
|
||||||
&GLOB.tmp_text_batches,
|
draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer)
|
||||||
TextBatch {
|
}
|
||||||
atlas_texture = data.atlas_texture,
|
|
||||||
vertex_start = vertex_start,
|
// When we rendered into source_texture, copy it to the swapchain. Single
|
||||||
vertex_count = u32(data.num_vertices),
|
// CopyGPUTextureToTexture call per frame, only when backdrop content was present.
|
||||||
index_start = index_start,
|
if has_backdrop {
|
||||||
index_count = u32(data.num_indices),
|
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
|
||||||
},
|
sdl.CopyGPUTextureToTexture(
|
||||||
|
copy_pass,
|
||||||
|
sdl.GPUTextureLocation{texture = GLOB.backdrop.source_texture},
|
||||||
|
sdl.GPUTextureLocation{texture = swapchain_texture},
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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.
@@ -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.
@@ -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)
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ 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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 0–1 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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user