DPI scaling fixes #26
+49
-26
@@ -5,14 +5,27 @@ Clay UI integration.
|
||||
|
||||
## Current state
|
||||
|
||||
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with two submission
|
||||
modes dispatched by a push constant:
|
||||
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with three submission
|
||||
modes dispatched by a push constant. The split is by **vertex coordinate space**, not by what the
|
||||
fragment shader does — modes 0 and 2 share the same fragment-shader path (kind 0) and differ only
|
||||
in whether the vertex shader applies `dpi_scale` to incoming positions:
|
||||
|
||||
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry. Used for text (indexed draws into
|
||||
SDL_ttf atlas textures), single-pixel points (`tess.pixel`), arbitrary user geometry
|
||||
(`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`, `tess.triangle_fan`,
|
||||
`tess.triangle_strip`), and any raw vertex geometry submitted via `prepare_shape`. The fragment
|
||||
shader premultiplies the texture sample (`t.rgb *= t.a`) and computes `out = color * t`.
|
||||
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry in _logical_ pixels. The vertex
|
||||
shader scales by `dpi_scale` before projecting. Used for single-pixel points (`tess.pixel`),
|
||||
arbitrary user geometry (`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`,
|
||||
`tess.triangle_fan`, `tess.triangle_strip`), and any raw vertex geometry submitted via
|
||||
`prepare_shape`. The fragment shader premultiplies the texture sample (`t.rgb *= t.a`) and
|
||||
computes `out = color * t`.
|
||||
|
||||
- **Mode 2 (Text):** Vertex buffer contains real geometry in _physical_ pixels. SDL_ttf's GPU text
|
||||
engine lays out glyphs in physical pixels (`TTF_SetFontSizeDPI` is called with `72 * dpi_scale`),
|
||||
so `prepare_text` adds an anchor offset that is itself snapped to integer physical pixels for
|
||||
atlas-aligned bilinear sampling, then writes vertices straight to the buffer. The vertex shader
|
||||
must NOT rescale these vertices. Same fragment-shader kind as Tessellated; same indexed draws
|
||||
into SDL_ttf atlas textures; the only difference is the coordinate space of the input. Mode 2
|
||||
exists because integer-physical-pixel snapping is the load-bearing property of crisp glyph
|
||||
rendering and CPU is the only place that snap can happen once-per-text-element instead of
|
||||
per-vertex.
|
||||
|
||||
- **Mode 1 (SDF):** A static 6-vertex unit-quad buffer is drawn instanced, with per-primitive
|
||||
`Core_2D_Primitive` structs (96 bytes each) uploaded each frame to a GPU storage buffer. The vertex
|
||||
@@ -43,8 +56,8 @@ in the pipeline plan below for the full cliff/margin analysis and SBC architectu
|
||||
The fragment shader's estimated peak footprint is ~22–26 fp32 VGPRs (~16–22 fp16 VGPRs on architectures
|
||||
with native mediump) via manual live-range analysis. The dominant peak is the Ring_Arc kind path
|
||||
(wedge normals + inner/outer radii + dot-product temporaries live simultaneously with carried state
|
||||
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size`). RRect is 1–2 regs lower (`corner_radii` vec4
|
||||
replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers
|
||||
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size_ppx`). RRect is 1–2 regs lower
|
||||
(`corner_radii_ppx` vec4 replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers
|
||||
apply live-range coalescing, mediump-to-fp16 promotion, and rematerialization that typically shave
|
||||
2–4 regs from hand-counted estimates — the conservative 26-reg upper bound is expected to compile
|
||||
down to within the 24-register budget, but this must be verified with `malioc` (see "Verifying
|
||||
@@ -432,22 +445,32 @@ our design:
|
||||
|
||||
### Main pipeline: SDF + tessellated (unified)
|
||||
|
||||
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_2D` push constant
|
||||
(`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.
|
||||
The main pipeline serves three submission modes through a single `TRIANGLELIST` pipeline and a
|
||||
single vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push
|
||||
constant (`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`, `Core_2D_Mode.Text = 2`), pushed
|
||||
per draw call via `push_globals`. The vertex shader branches on this uniform to select the
|
||||
appropriate code path.
|
||||
|
||||
- **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
|
||||
user-provided raw vertex geometry.
|
||||
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry in _logical_
|
||||
pixels. Vertex shader scales positions by `dpi_scale`. Used for triangles, triangle fans/strips,
|
||||
single-pixel points, and any user-provided raw vertex geometry.
|
||||
- **SDF mode** (`mode = 1`): shared unit-quad vertex buffer + GPU storage buffer of
|
||||
`Core_2D_Primitive` structs, drawn instanced. Used for all shapes with closed-form signed distance
|
||||
functions.
|
||||
functions. `Core_2D_Primitive.bounds` is in logical pixels; the vertex shader scales by
|
||||
`dpi_scale`.
|
||||
- **Text mode** (`mode = 2`): direct vertex buffer with explicit geometry in _physical_ pixels.
|
||||
Vertex shader does NOT scale. Used for SDL_ttf atlas sampling. The CPU-side anchor snap to
|
||||
integer physical pixels (`prepare_text`/`prepare_text_transformed`) is what produces crisp glyphs
|
||||
— sub-pixel anchors blur via the bilinear sampler. Mode 2 shares the fragment-shader path with
|
||||
Tessellated (kind 0), so the only divergence between text and shape rasterization is the vertex
|
||||
shader's `* dpi_scale` step.
|
||||
|
||||
Both modes use the same fragment shader. The fragment shader checks `Shape_Kind` (low byte of
|
||||
`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,
|
||||
Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based on `Shape_Flags` bits.
|
||||
All three modes use the same fragment shader. Modes 0 (Tessellated) and 2 (Text) take the same
|
||||
fragment-shader path (kind 0), which premultiplies the texture sample and computes `out = color * t`;
|
||||
they differ only in the vertex shader (whether positions are pre-scaled to physical pixels). Mode 1
|
||||
(SDF) checks `Shape_Kind` (low byte of `Core_2D_Primitive.flags`): 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.
|
||||
|
||||
#### Why SDF for shapes
|
||||
|
||||
@@ -495,9 +518,9 @@ Compared to encoding per-primitive data in vertex attributes (the "fat vertex" a
|
||||
buffer instancing eliminates the 4–6× data duplication across quad corners. A rounded rectangle costs
|
||||
96 bytes instead of 4 vertices × 60+ bytes = 240+ bytes.
|
||||
|
||||
The tessellated path retains the existing direct vertex buffer layout (20 bytes/vertex, no storage
|
||||
buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every invocation
|
||||
in a draw call has the same mode — so it is effectively free on all modern GPUs.
|
||||
The tessellated and text paths retain the existing direct vertex buffer layout (20 bytes/vertex, no
|
||||
storage buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every
|
||||
invocation in a draw call has the same mode — so it is effectively free on all modern GPUs.
|
||||
|
||||
#### Shape kinds and SDF dispatch
|
||||
|
||||
@@ -719,7 +742,7 @@ Backdrop_Marker :: struct {
|
||||
sigma: f32,
|
||||
tint: Color,
|
||||
radii: Rectangle_Radii,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -762,7 +785,7 @@ Core_2D_Primitive :: struct {
|
||||
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
|
||||
rotation_sc: u32, // 24: packed f16 pair (sin, cos). Requires .Rotated flag.
|
||||
_pad: f32, // 28: reserved for future use
|
||||
params: Shape_Params, // 32: per-kind params union (half_feather, radii, etc.) (32 bytes)
|
||||
params: Shape_Params, // 32: per-kind params union (half_feather_ppx, radii_ppx, etc.) (32 bytes)
|
||||
uv_rect: [4]f32, // 64: texture UV coordinates. Read when .Textured.
|
||||
effects: Gradient_Outline, // 80: gradient and/or outline parameters (16 bytes).
|
||||
}
|
||||
|
||||
+12
-12
@@ -487,10 +487,10 @@ MAX_GAUSSIAN_BLUR_KERNEL_PAIRS :: 32
|
||||
// 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)
|
||||
bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy) in logical px
|
||||
radii_ppx: [4]f32, // 16: 16 — per-corner radii (BR, TR, BL, TL)
|
||||
half_size_ppx: [2]f32, // 32: 8 — RRect half extents
|
||||
half_feather_ppx: f32, // 40: 4 — feather_ppx * 0.5 (SDF anti-aliasing)
|
||||
color: Color, // 44: 4 — tint, packed RGBA u8x4
|
||||
}
|
||||
#assert(size_of(Gaussian_Blur_Primitive) == 48)
|
||||
@@ -1070,7 +1070,7 @@ run_backdrop_bracket :: proc(
|
||||
build_backdrop_primitive :: proc(
|
||||
rect: Rectangle,
|
||||
radii: Rectangle_Radii,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Gaussian_Blur_Primitive {
|
||||
max_radius := min(rect.width, rect.height) * 0.5
|
||||
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
||||
@@ -1078,8 +1078,8 @@ build_backdrop_primitive :: proc(
|
||||
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
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
half_width := rect.width * 0.5
|
||||
@@ -1098,14 +1098,14 @@ build_backdrop_primitive :: proc(
|
||||
// (p.x > 0) ? r.xy : r.zw picks right-vs-left half
|
||||
// then (p.y > 0) ? rxy.x : rxy.y picks bottom-vs-top within that half
|
||||
// So slot 0 = bottom-right, slot 1 = top-right, slot 2 = bottom-left, slot 3 = top-left.
|
||||
radii = {
|
||||
radii_ppx = {
|
||||
clamped_bottom_right * dpi_scale,
|
||||
clamped_top_right * dpi_scale,
|
||||
clamped_bottom_left * dpi_scale,
|
||||
clamped_top_left * dpi_scale,
|
||||
},
|
||||
half_size = {half_width * dpi_scale, half_height * dpi_scale},
|
||||
half_feather = half_feather,
|
||||
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale},
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,9 +1162,9 @@ backdrop_blur :: proc(
|
||||
gaussian_sigma: f32,
|
||||
tint: Color = DFT_TINT,
|
||||
radii: Rectangle_Radii = {},
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim := build_backdrop_primitive(rect, radii, feather_px)
|
||||
prim := build_backdrop_primitive(rect, radii, feather_ppx)
|
||||
prim.color = tint
|
||||
prepare_backdrop_primitive(layer, prim, gaussian_sigma)
|
||||
}
|
||||
|
||||
+90
-71
@@ -9,11 +9,8 @@ import sdl_ttf "vendor:sdl3/ttf"
|
||||
|
||||
//----- Vertex layout ----------------------------------
|
||||
|
||||
// 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 layout for tessellated and text geometry. `color` must be premultiplied alpha; see
|
||||
// the package doc's "Color and blending" section for the contract.
|
||||
Vertex_2D :: struct {
|
||||
position: Vec2,
|
||||
uv: [2]f32,
|
||||
@@ -68,34 +65,34 @@ Shape_Flags :: bit_set[Shape_Flag;u8]
|
||||
|
||||
//INTERNAL
|
||||
RRect_Params :: struct {
|
||||
half_size: [2]f32,
|
||||
radii: [4]f32,
|
||||
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||
half_size_ppx: [2]f32,
|
||||
radii_ppx: [4]f32,
|
||||
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
|
||||
_: f32,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
NGon_Params :: struct {
|
||||
radius: f32,
|
||||
radius_ppx: f32,
|
||||
sides: f32,
|
||||
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
|
||||
_: [5]f32,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
Ellipse_Params :: struct {
|
||||
radii: [2]f32,
|
||||
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
|
||||
radii_ppx: [2]f32,
|
||||
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
|
||||
_: [5]f32,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
Ring_Arc_Params :: struct {
|
||||
inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
|
||||
outer_radius: f32, // outer radius in physical pixels
|
||||
inner_radius_ppx: f32, // 0 for pie slice
|
||||
outer_radius_ppx: f32,
|
||||
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)
|
||||
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
|
||||
_: f32,
|
||||
}
|
||||
|
||||
@@ -176,9 +173,7 @@ Core_2D :: struct {
|
||||
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.
|
||||
// PSO is hard-wired to single-sample (no MSAA — see package doc's "Anti-aliasing" section).
|
||||
//INTERNAL
|
||||
create_core_2d :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> (core_2d: Core_2D, ok: bool) {
|
||||
// On failure, clean up any partially-created resources
|
||||
@@ -464,10 +459,31 @@ destroy_core_2d :: proc(device: ^sdl.GPUDevice, core: ^Core_2D) {
|
||||
|
||||
//----- Vertex uniforms ----------------------------------
|
||||
|
||||
//
|
||||
// Coordinate-space contract for the main pipeline's vertex shader:
|
||||
//
|
||||
// Tessellated (0) — `v_position` arrives in *logical* pixels. The vertex
|
||||
// shader multiplies by `dpi_scale` before applying the
|
||||
// ortho projection (which is sized to physical pixels).
|
||||
// SDF (1) — `v_position` is a unit-quad corner (0..1). World-space
|
||||
// coordinates come from `Core_2D_Primitive.bounds` in
|
||||
// logical pixels; the shader scales by `dpi_scale`.
|
||||
// Text (2) — `v_position` arrives in *physical* pixels already.
|
||||
// `prepare_text` and `prepare_text_transformed` bake the
|
||||
// anchor + glyph offsets (from SDL_ttf's GPU text engine,
|
||||
// which lays glyphs out in physical pixels) into the
|
||||
// vertex stream and snap the anchor to integer physical
|
||||
// pixels for atlas-aligned bilinear sampling. The shader
|
||||
// therefore must NOT rescale these vertices.
|
||||
//
|
||||
// The two raw-vertex modes (Tessellated, Text) share `prepare_shape`-style
|
||||
// glue but their coord spaces diverge — see `base_2d.vert` for the shader-
|
||||
// side branch.
|
||||
//INTERNAL
|
||||
Core_2D_Mode :: enum u32 {
|
||||
Tessellated = 0,
|
||||
SDF = 1,
|
||||
Text = 2,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
@@ -814,9 +830,12 @@ render_layer_sub_batch_range :: proc(
|
||||
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
|
||||
// Text vertices live in physical-pixel space (see Core_2D_Mode.Text
|
||||
// docs); mode 2 makes the shader skip the `* dpi_scale` step that
|
||||
// the Tessellated path applies to logical-pixel input.
|
||||
if current_mode != .Text {
|
||||
push_globals(cmd_buffer, width, height, .Text)
|
||||
current_mode = .Text
|
||||
}
|
||||
if current_vert_buf != main_vert_buf {
|
||||
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
|
||||
@@ -922,8 +941,8 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
|
||||
|
||||
// Snap base position to integer physical pixels to avoid atlas sub-pixel
|
||||
// sampling blur (and the off-by-one bottom-row clip that comes with it).
|
||||
base_x := math.round(text.position[0] * GLOB.dpi_scaling)
|
||||
base_y := math.round(text.position[1] * GLOB.dpi_scaling)
|
||||
base_x_ppx := math.round(text.position[0] * GLOB.dpi_scaling)
|
||||
base_y_ppx := math.round(text.position[1] * GLOB.dpi_scaling)
|
||||
|
||||
// Premultiply text color once — reused across all glyph vertices.
|
||||
pm_color := premultiply_color(text.color)
|
||||
@@ -938,7 +957,7 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
|
||||
uv := data.uv[i]
|
||||
append(
|
||||
&GLOB.tmp_text_verts,
|
||||
Vertex_2D{position = {pos.x + base_x, -pos.y + base_y}, uv = {uv.x, uv.y}, color = pm_color},
|
||||
Vertex_2D{position = {pos.x + base_x_ppx, -pos.y + base_y_ppx}, uv = {uv.x, uv.y}, color = pm_color},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1079,7 +1098,7 @@ build_rrect_primitive :: proc(
|
||||
radii: Rectangle_Radii,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Core_2D_Primitive {
|
||||
max_radius := min(rect.width, rect.height) * 0.5
|
||||
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
||||
@@ -1087,8 +1106,8 @@ build_rrect_primitive :: proc(
|
||||
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
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
half_width := rect.width * 0.5
|
||||
@@ -1126,14 +1145,14 @@ build_rrect_primitive :: proc(
|
||||
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 = {
|
||||
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale},
|
||||
radii_ppx = {
|
||||
clamped_bottom_right * dpi_scale,
|
||||
clamped_top_right * dpi_scale,
|
||||
clamped_bottom_left * dpi_scale,
|
||||
clamped_top_left * dpi_scale,
|
||||
},
|
||||
half_feather = half_feather,
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim
|
||||
}
|
||||
@@ -1146,10 +1165,10 @@ build_circle_primitive :: proc(
|
||||
radius: f32,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Core_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
actual_center := center
|
||||
@@ -1166,11 +1185,11 @@ build_circle_primitive :: proc(
|
||||
actual_center.y + radius + padding,
|
||||
},
|
||||
}
|
||||
scaled_radius := radius * dpi_scale
|
||||
radius_ppx := 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,
|
||||
half_size_ppx = {radius_ppx, radius_ppx},
|
||||
radii_ppx = {radius_ppx, radius_ppx, radius_ppx, radius_ppx},
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim
|
||||
}
|
||||
@@ -1183,10 +1202,10 @@ build_ellipse_primitive :: proc(
|
||||
radius_horizontal, radius_vertical: f32,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Core_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
actual_center := center
|
||||
@@ -1218,8 +1237,8 @@ build_ellipse_primitive :: proc(
|
||||
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,
|
||||
radii_ppx = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim
|
||||
}
|
||||
@@ -1233,10 +1252,10 @@ build_polygon_primitive :: proc(
|
||||
radius: f32,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> Core_2D_Primitive {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
actual_center := center
|
||||
@@ -1258,9 +1277,9 @@ build_polygon_primitive :: proc(
|
||||
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,
|
||||
radius_ppx = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
|
||||
sides = f32(sides),
|
||||
half_feather = half_feather,
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim
|
||||
}
|
||||
@@ -1278,13 +1297,13 @@ build_ring_arc_primitive :: proc(
|
||||
end_angle: f32,
|
||||
origin: Vec2,
|
||||
rotation: f32,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
) -> (
|
||||
Core_2D_Primitive,
|
||||
Shape_Flags,
|
||||
) {
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
actual_center := center
|
||||
@@ -1327,11 +1346,11 @@ build_ring_arc_primitive :: proc(
|
||||
},
|
||||
}
|
||||
prim.params.ring_arc = Ring_Arc_Params {
|
||||
inner_radius = inner_radius * dpi_scale,
|
||||
outer_radius = outer_radius * dpi_scale,
|
||||
inner_radius_ppx = inner_radius * dpi_scale,
|
||||
outer_radius_ppx = outer_radius * dpi_scale,
|
||||
normal_start = normal_start,
|
||||
normal_end = normal_end,
|
||||
half_feather = half_feather,
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
return prim, arc_flags
|
||||
}
|
||||
@@ -1422,9 +1441,9 @@ rectangle :: proc(
|
||||
radii: Rectangle_Radii = {},
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
|
||||
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_ppx)
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
@@ -1445,9 +1464,9 @@ circle :: proc(
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
|
||||
prim := build_circle_primitive(center, radius, origin, rotation, feather_ppx)
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
@@ -1462,9 +1481,9 @@ ellipse :: proc(
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px)
|
||||
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_ppx)
|
||||
apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
@@ -1481,11 +1500,11 @@ polygon :: proc(
|
||||
outline_width: f32 = 0,
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
if sides < 3 do return
|
||||
|
||||
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
|
||||
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_ppx)
|
||||
apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width)
|
||||
}
|
||||
|
||||
@@ -1504,7 +1523,7 @@ ring :: proc(
|
||||
end_angle: f32 = DFT_CIRC_END_ANGLE,
|
||||
origin: Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
prim, arc_flags := build_ring_arc_primitive(
|
||||
center,
|
||||
@@ -1514,7 +1533,7 @@ ring :: proc(
|
||||
end_angle,
|
||||
origin,
|
||||
rotation,
|
||||
feather_px,
|
||||
feather_ppx,
|
||||
)
|
||||
apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
|
||||
}
|
||||
@@ -1528,7 +1547,7 @@ line :: proc(
|
||||
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
delta_x := end_position.x - start_position.x
|
||||
delta_y := end_position.y - start_position.y
|
||||
@@ -1544,8 +1563,8 @@ line :: proc(
|
||||
half_thickness := thickness * 0.5
|
||||
cap_radius := half_thickness
|
||||
|
||||
half_feather := feather_px * 0.5
|
||||
padding := half_feather / GLOB.dpi_scaling
|
||||
half_feather_ppx := feather_ppx * 0.5
|
||||
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
// Expand bounds for rotation
|
||||
@@ -1561,14 +1580,14 @@ line :: proc(
|
||||
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 = {
|
||||
half_size_ppx = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
|
||||
radii_ppx = {
|
||||
cap_radius * dpi_scale,
|
||||
cap_radius * dpi_scale,
|
||||
cap_radius * dpi_scale,
|
||||
cap_radius * dpi_scale,
|
||||
},
|
||||
half_feather = half_feather,
|
||||
half_feather_ppx = half_feather_ppx,
|
||||
}
|
||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||
}
|
||||
@@ -1581,10 +1600,10 @@ line_strip :: proc(
|
||||
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||
outline_color: Color = {},
|
||||
outline_width: f32 = 0,
|
||||
feather_px: f32 = DFT_FEATHER_PX,
|
||||
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||
) {
|
||||
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)
|
||||
line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_ppx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,10 +747,10 @@ PRE_PAD_X :: SPACE_PANEL // 24
|
||||
|
||||
// ============================================================
|
||||
// SCANLINE OVERLAY (opt-in, terminal surfaces only)
|
||||
// Repeating-stripe pattern at very low opacity. Stripe is 2px
|
||||
// transparent + 2px black-at-3% (TINT_SCANLINE).
|
||||
// Repeating-stripe pattern at very low opacity. Stripe is 2 logical
|
||||
// pixels transparent + 2 logical pixels black-at-3% (TINT_SCANLINE).
|
||||
// ============================================================
|
||||
|
||||
SCANLINE_STRIPE_PX :: 2
|
||||
SCANLINE_GAP_PX :: 2
|
||||
SCANLINE_STRIPE_LPX :: 2
|
||||
SCANLINE_GAP_LPX :: 2
|
||||
SCANLINE_COLOR :: TINT_SCANLINE
|
||||
|
||||
+206
-121
@@ -1,3 +1,66 @@
|
||||
// Rendering library built on SDL3 GPU.
|
||||
//
|
||||
// ----- Coordinate system -----
|
||||
// Origin is the top-left corner of the window/layer. X increases rightward, Y increases
|
||||
// downward. This matches SDL, HTML Canvas, and most 2D UI coordinate conventions. All
|
||||
// public position parameters (`center`, `origin`, `start_position`, `end_position`, every
|
||||
// `Vec2`-typed field, every `Rectangle.x/y`, etc.) live in this coordinate system.
|
||||
//
|
||||
// ----- Unit-suffix convention -----
|
||||
// Public CPU-side dimensions are in *logical* pixels by default (CSS-style: a value of 200
|
||||
// looks the same physical size on a 1× monitor and a 2× Retina display). Suffix rules:
|
||||
//
|
||||
// no suffix — logical pixels. Default for layout values (positions, sizes, radii,
|
||||
// outline widths, line thicknesses, gradient endpoints, etc.).
|
||||
// `_lpx` — logical pixels, *explicit*. Optional. Use when an identifier would
|
||||
// otherwise be ambiguous about which kind of pixel it carries —
|
||||
// typically standalone constants like `SCANLINE_STRIPE_LPX` where the
|
||||
// context doesn't make the unit obvious from the surrounding code.
|
||||
// Procedure parameters and struct fields named after a layout property
|
||||
// (`width`, `radius`, ...) don't need this suffix.
|
||||
// `_ppx` — physical (device) pixels. Required whenever a value is in physical
|
||||
// pixels, regardless of context. Reserved for quantities whose
|
||||
// right-feeling magnitude is a property of the device pixel grid rather
|
||||
// than of the layout: anti-aliasing band widths, sub-pixel snap targets,
|
||||
// MSDF screen-pixel-range parameters.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// width, height, radius, outline_width, thickness — logical px (no suffix)
|
||||
// SCANLINE_STRIPE_LPX, SCANLINE_GAP_LPX — logical px (explicit `_lpx`)
|
||||
// feather_ppx, aa_ppx — physical px (`_ppx`)
|
||||
//
|
||||
// Layout values scale with DPI; rasterization-grid values do not. The shader handles the
|
||||
// logical-to-physical conversion at the rasterization boundary; CPU-side `_ppx` inputs that
|
||||
// need to interact with logical-space data convert via `/ dpi_scaling` at the use site.
|
||||
//
|
||||
// ----- Anti-aliasing -----
|
||||
// MSAA is intentionally NOT supported. SDF text and shapes compute fragment coverage
|
||||
// analytically via `smoothstep`, so they don't benefit from multisampling. Tessellated
|
||||
// user geometry submitted via `prepare_shape` is rendered without anti-aliasing — if AA is
|
||||
// required for tessellated content, the caller must either render it to their own offscreen
|
||||
// target and submit the result as a texture, or use the AA helpers in the `tess` subpackage
|
||||
// (e.g. `tess.triangle_aa` extrudes 1-physical-pixel alpha-falloff edge bands). This
|
||||
// decision aligns with the SBC target (Mali Valhall, where MSAA's per-tile bandwidth
|
||||
// multiplier is expensive) and matches RAD Debugger's architecture.
|
||||
//
|
||||
// ----- Color and blending -----
|
||||
// `Color` is RGBA8 in memory order (R, G, B, A at indices 0..3). The shader unpacks via
|
||||
// `unpackUnorm4x8`, which reads bytes in that exact order. Alpha 255 = fully opaque, 0 =
|
||||
// fully transparent.
|
||||
//
|
||||
// All rendering uses *premultiplied-over* blending (blend state ONE, ONE_MINUS_SRC_ALPHA —
|
||||
// the standard mode used by Skia, Flutter, and GPUI). Three implications:
|
||||
//
|
||||
// - Public shape procs (`rectangle`, `circle`, `line`, etc.) accept straight-alpha
|
||||
// `Color` values and the SDF fragment shaders premultiply internally; users of these
|
||||
// procs don't need to think about premultiplication.
|
||||
// - Vertex colors written to the shared vertex stream (the tessellated path — text and
|
||||
// anything submitted via `prepare_shape`, including `tess.*` helpers) MUST be
|
||||
// premultiplied at the CPU. The tessellated fragment shader passes vertex color through
|
||||
// directly without further modification. The `premultiply_color` helper handles this.
|
||||
// - The clear color passed to `end()` is also premultiplied internally before being
|
||||
// handed to the GPU; callers pass straight-alpha `Color` here too.
|
||||
package draw
|
||||
|
||||
import "base:runtime"
|
||||
@@ -51,7 +114,7 @@ INITIAL_SCISSOR_SIZE :: 10
|
||||
// ----- Default parameter values -----
|
||||
// Named constants for non-zero default procedure parameters. Centralizes magic numbers
|
||||
// so they can be tuned in one place and referenced by name in proc signatures.
|
||||
DFT_FEATHER_PX :: 1 // Total AA feather width in physical pixels (half on each side of boundary).
|
||||
DFT_FEATHER_PPX :: 1 // Total AA feather width in physical pixels (half on each side of boundary).
|
||||
DFT_STROKE_THICKNESS :: 1 // Default line/stroke thickness in logical pixels.
|
||||
DFT_FONT_SIZE :: 44 // Default font size in points for text rendering.
|
||||
DFT_CIRC_END_ANGLE :: 360 // Full-circle end angle in degrees (ring/arc).
|
||||
@@ -132,27 +195,13 @@ Global :: struct {
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// A 2D position in world space. Non-distinct alias for [2]f32 — bare literals like {100, 200}
|
||||
// work at non-ambiguous call sites.
|
||||
//
|
||||
// Coordinate system: origin is the top-left corner of the window/layer. X increases rightward,
|
||||
// Y increases downward. This matches SDL, HTML Canvas, and most 2D UI coordinate conventions.
|
||||
// All position parameters in the draw API (center, origin, start_position, end_position, etc.)
|
||||
// use this coordinate system.
|
||||
//
|
||||
// Units are logical pixels (pre-DPI-scaling). The renderer multiplies by dpi_scaling internally
|
||||
// before uploading to the GPU. A Vec2{100, 50} refers to the same visual location regardless of
|
||||
// display DPI.
|
||||
// work at non-ambiguous call sites. See the package doc for coordinate-system and unit
|
||||
// conventions.
|
||||
Vec2 :: [2]f32
|
||||
|
||||
// An RGBA color with 8 bits per channel. Distinct type over [4]u8 so that proc-group
|
||||
// overloads can disambiguate Color from other 4-byte structs.
|
||||
//
|
||||
// Channel order: R, G, B, A (indices 0, 1, 2, 3). Alpha 255 is fully opaque, 0 is fully
|
||||
// transparent. This matches the GPU-side layout: the shader unpacks via unpackUnorm4x8 which
|
||||
// reads the bytes in memory order as R, G, B, A and normalizes each to [0, 1].
|
||||
//
|
||||
// When used in the Core_2D_Primitive or Gaussian_Blur_Primitive structs (e.g. .color), the 4 bytes
|
||||
// are stored as a u32 in native byte order and unpacked by the shader.
|
||||
// overloads can disambiguate Color from other 4-byte structs. See the package doc for the
|
||||
// memory layout and the premultiplied-over blending contract.
|
||||
Color :: [4]u8
|
||||
|
||||
BLACK :: Color{0, 0, 0, 255}
|
||||
@@ -228,10 +277,9 @@ color_to_f32 :: proc(color: Color) -> [4]f32 {
|
||||
return {f32(color[0]) * INV, f32(color[1]) * INV, f32(color[2]) * INV, f32(color[3]) * INV}
|
||||
}
|
||||
|
||||
// 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
|
||||
// tessellated fragment shader passes vertex color through without further modification.
|
||||
// Users who construct Vertex_2D structs manually for prepare_shape must premultiply their colors.
|
||||
// Pre-multiply RGB channels by alpha. Required for any vertex written to the tessellated
|
||||
// vertex stream (text path or `prepare_shape`-style submissions); see the package doc's
|
||||
// "Color and blending" section for the full contract.
|
||||
premultiply_color :: #force_inline proc(color: Color) -> Color {
|
||||
a := u32(color[3])
|
||||
return Color {
|
||||
@@ -249,7 +297,7 @@ premultiply_color :: #force_inline proc(color: Color) -> Color {
|
||||
//INTERNAL
|
||||
Sub_Batch_Kind :: enum u8 {
|
||||
Tessellated, // non-indexed, white texture or user texture, Core_2D_Mode.Tessellated
|
||||
Text, // indexed, atlas texture, Core_2D_Mode.Tessellated
|
||||
Text, // indexed, atlas texture, Core_2D_Mode.Text (vertices already in physical-pixel space)
|
||||
SDF, // instanced unit quad, Core_2D_Mode.SDF
|
||||
// instanced unit quad, backdrop subsystem V-composite (indexes Gaussian_Blur_Primitive).
|
||||
// Bracket-scheduled per layer; see README.md § "Backdrop pipeline" for ordering semantics.
|
||||
@@ -289,12 +337,6 @@ Scissor :: struct {
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
|
||||
//
|
||||
// MSAA is intentionally NOT supported. SDF text and shapes compute coverage analytically via
|
||||
// `smoothstep`, so they don't benefit from multisampling. Tessellated user geometry submitted
|
||||
// via `prepare_shape` is not anti-aliased — if you need AA on tessellated content, render it
|
||||
// to your own offscreen target and submit it as a texture. RAD Debugger and the SBC target
|
||||
// (Mali Valhall, where MSAA's per-tile bandwidth multiplier is expensive) drove this decision.
|
||||
@(require_results)
|
||||
init :: proc(
|
||||
device: ^sdl.GPUDevice,
|
||||
@@ -446,30 +488,6 @@ clear_global :: proc() {
|
||||
// ----- Frame ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for.
|
||||
begin :: proc(bounds: Rectangle) -> ^Layer {
|
||||
// Cleanup
|
||||
clear_global()
|
||||
|
||||
// Begin new layer + start a new scissor
|
||||
scissor := Scissor {
|
||||
bounds = sdl.Rect {
|
||||
x = i32(bounds.x * GLOB.dpi_scaling),
|
||||
y = i32(bounds.y * GLOB.dpi_scaling),
|
||||
w = i32(bounds.width * GLOB.dpi_scaling),
|
||||
h = i32(bounds.height * GLOB.dpi_scaling),
|
||||
},
|
||||
}
|
||||
append(&GLOB.scissors, scissor)
|
||||
|
||||
layer := Layer {
|
||||
bounds = bounds,
|
||||
scissor_len = 1,
|
||||
}
|
||||
append(&GLOB.layers, layer)
|
||||
return &GLOB.layers[GLOB.curr_layer_index]
|
||||
}
|
||||
|
||||
// Creates a new layer
|
||||
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
|
||||
if GLOB.open_backdrop_layer != nil {
|
||||
@@ -499,44 +517,28 @@ new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
|
||||
return &GLOB.layers[GLOB.curr_layer_index]
|
||||
}
|
||||
|
||||
// Open a backdrop scope on `layer`. All subsequent draws on `layer` until the matching
|
||||
// `end_backdrop` must be backdrop primitives (currently only `backdrop_blur`). Non-backdrop
|
||||
// draws inside a scope, or backdrop draws outside one, panic.
|
||||
//
|
||||
// Bracket scheduling: each scope produces one bracket at render time. Within the scope,
|
||||
// per-sigma sub-batch coalescing still applies (two contiguous backdrop_blur calls with
|
||||
// the same sigma share an instanced composite draw and a single H+V blur pass pair).
|
||||
//
|
||||
// Multiple begin/end pairs per layer are allowed: each pair is its own bracket, and
|
||||
// non-backdrop draws between pairs render in their submission position relative to the
|
||||
// brackets. Use this for layered frost effects.
|
||||
begin_backdrop :: proc(layer: ^Layer) {
|
||||
if GLOB.open_backdrop_layer != nil {
|
||||
log.panicf("begin_backdrop called while a scope is already open on layer %p", GLOB.open_backdrop_layer)
|
||||
}
|
||||
GLOB.open_backdrop_layer = layer
|
||||
}
|
||||
// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for.
|
||||
begin :: proc(bounds: Rectangle) -> ^Layer {
|
||||
// Cleanup
|
||||
clear_global()
|
||||
|
||||
// Close the backdrop scope opened by `begin_backdrop`. Must be called on the same layer that
|
||||
// the scope was opened on; the layer pointer mismatch is a hard error rather than a silent
|
||||
// recovery to surface integration bugs early.
|
||||
end_backdrop :: proc(layer: ^Layer) {
|
||||
if GLOB.open_backdrop_layer != layer {
|
||||
log.panicf("end_backdrop on wrong layer (open=%p, ended=%p)", GLOB.open_backdrop_layer, layer)
|
||||
// Begin new layer + start a new scissor
|
||||
scissor := Scissor {
|
||||
bounds = sdl.Rect {
|
||||
x = i32(bounds.x * GLOB.dpi_scaling),
|
||||
y = i32(bounds.y * GLOB.dpi_scaling),
|
||||
w = i32(bounds.width * GLOB.dpi_scaling),
|
||||
h = i32(bounds.height * GLOB.dpi_scaling),
|
||||
},
|
||||
}
|
||||
GLOB.open_backdrop_layer = nil
|
||||
}
|
||||
append(&GLOB.scissors, scissor)
|
||||
|
||||
// Convenience wrapper for the common case of a backdrop scope tied to a block. Use with
|
||||
// defer-style block scoping:
|
||||
//
|
||||
// {
|
||||
// draw.backdrop_scope(layer)
|
||||
// draw.backdrop_blur(layer, ...)
|
||||
// } // end_backdrop fires automatically
|
||||
@(deferred_in = end_backdrop)
|
||||
backdrop_scope :: #force_inline proc(layer: ^Layer) {
|
||||
begin_backdrop(layer)
|
||||
layer := Layer {
|
||||
bounds = bounds,
|
||||
scissor_len = 1,
|
||||
}
|
||||
append(&GLOB.layers, layer)
|
||||
return &GLOB.layers[GLOB.curr_layer_index]
|
||||
}
|
||||
|
||||
// Render primitives. clear_color is the background fill before any layers are drawn.
|
||||
@@ -625,6 +627,46 @@ end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = DF
|
||||
}
|
||||
}
|
||||
|
||||
// Open a backdrop scope on `layer`. All subsequent draws on `layer` until the matching
|
||||
// `end_backdrop` must be backdrop primitives (currently only `backdrop_blur`). Non-backdrop
|
||||
// draws inside a scope, or backdrop draws outside one, panic.
|
||||
//
|
||||
// Bracket scheduling: each scope produces one bracket at render time. Within the scope,
|
||||
// per-sigma sub-batch coalescing still applies (two contiguous backdrop_blur calls with
|
||||
// the same sigma share an instanced composite draw and a single H+V blur pass pair).
|
||||
//
|
||||
// Multiple begin/end pairs per layer are allowed: each pair is its own bracket, and
|
||||
// non-backdrop draws between pairs render in their submission position relative to the
|
||||
// brackets. Use this for layered frost effects.
|
||||
begin_backdrop :: proc(layer: ^Layer) {
|
||||
if GLOB.open_backdrop_layer != nil {
|
||||
log.panicf("begin_backdrop called while a scope is already open on layer %p", GLOB.open_backdrop_layer)
|
||||
}
|
||||
GLOB.open_backdrop_layer = layer
|
||||
}
|
||||
|
||||
// Close the backdrop scope opened by `begin_backdrop`. Must be called on the same layer that
|
||||
// the scope was opened on; the layer pointer mismatch is a hard error rather than a silent
|
||||
// recovery to surface integration bugs early.
|
||||
end_backdrop :: proc(layer: ^Layer) {
|
||||
if GLOB.open_backdrop_layer != layer {
|
||||
log.panicf("end_backdrop on wrong layer (open=%p, ended=%p)", GLOB.open_backdrop_layer, layer)
|
||||
}
|
||||
GLOB.open_backdrop_layer = nil
|
||||
}
|
||||
|
||||
// Convenience wrapper for the common case of a backdrop scope tied to a block. Use with
|
||||
// defer-style block scoping:
|
||||
//
|
||||
// {
|
||||
// draw.backdrop_scope(layer)
|
||||
// draw.backdrop_blur(layer, ...)
|
||||
// } // end_backdrop fires automatically
|
||||
@(deferred_in = end_backdrop)
|
||||
backdrop_scope :: #force_inline proc(layer: ^Layer) {
|
||||
begin_backdrop(layer)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ----- Sub-batch dispatch ------------
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -712,14 +754,18 @@ measure_text_clay :: proc "c" (
|
||||
}
|
||||
|
||||
// Called for each Clay `RenderCommandType.Custom` render command that
|
||||
// `prepare_clay_batch` encounters.
|
||||
// `prepare_clay_batch` encounters and which is NOT a levlib-managed variant
|
||||
// (e.g. `Backdrop_Marker`).
|
||||
//
|
||||
// - `layer` is the layer the command belongs to (post-z-index promotion).
|
||||
// - `bounds` is already translated into the active layer's coordinate system
|
||||
// and pre-DPI, matching what the built-in shape procs expect.
|
||||
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
|
||||
// `backgroundColor`, `cornerRadius`, and the `customData` pointer the caller
|
||||
// attached to `clay.CustomElementConfig.customData`.
|
||||
// `backgroundColor` and `cornerRadius`. Its `customData` field has been
|
||||
// unwrapped from the `Clay_Custom` envelope: it points at the user's own
|
||||
// data (the value the user wrote into the `rawptr` variant), not at the
|
||||
// `Clay_Custom` itself. If the union was zero-init (no variant set) or
|
||||
// `customData` was originally nil, the callback receives nil.
|
||||
//
|
||||
// The callback must not call `new_layer` or `prepare_clay_batch`.
|
||||
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
|
||||
@@ -729,33 +775,51 @@ ClayBatch :: struct {
|
||||
cmds: clay.ClayArray(clay.RenderCommand),
|
||||
}
|
||||
|
||||
// Magic-number-tagged struct that user app data points at via Clay's customData field.
|
||||
// `prepare_clay_batch` recognizes these and routes them through a backdrop scope automatically.
|
||||
// The user populates a `Backdrop_Marker`, points `clay.CustomElementConfig.customData` at it,
|
||||
// and the integration walks the command stream, opening/closing scopes around contiguous
|
||||
// backdrop runs. Magic-number sentinel chosen over a separate userData flag so the marker
|
||||
// type stays self-describing in core dumps and in any non-Odin debugger view of the heap.
|
||||
// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point
|
||||
// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by
|
||||
// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is
|
||||
// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before
|
||||
// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData`
|
||||
// exactly as if no wrapper were involved.
|
||||
//
|
||||
// Contract: `customData`, when non-nil, MUST point at storage holding a `Clay_Custom`
|
||||
// value. The user owns that storage; its lifetime must span the Clay layout call and the
|
||||
// matching `prepare_clay_batch` call. Pointing `customData` at a bare user struct violates
|
||||
// the contract — the dispatcher will read its first bytes as a union tag and either route
|
||||
// the draw incorrectly or panic on type assertion. There is no recovery path; this is a
|
||||
// strict-discipline API by design.
|
||||
//
|
||||
// Construction notes (Odin implicit-conversion rules):
|
||||
// - Backdrop variant: `bd: Clay_Custom = Backdrop_Marker{...}` works directly.
|
||||
// Variant-to-union conversion is implicit.
|
||||
// - User pointer: `up: Clay_Custom = rawptr(&my_struct)` — the explicit `rawptr(...)` is
|
||||
// required because Odin does not chain `^T -> rawptr -> Clay_Custom` implicitly. A bare
|
||||
// `up: Clay_Custom = &my_struct` is a compile error.
|
||||
Clay_Custom :: union {
|
||||
Backdrop_Marker,
|
||||
rawptr,
|
||||
}
|
||||
|
||||
// Per-primitive parameters for a backdrop blur dispatched through the Clay integration.
|
||||
// Embedded as a `Clay_Custom` variant; `prepare_clay_batch` walks the command stream,
|
||||
// opens/closes a backdrop scope around contiguous backdrop runs, and feeds these to
|
||||
// `backdrop_blur` via `dispatch_clay_backdrop`. The discriminant is the union tag — no
|
||||
// in-band magic field needed (compiler-enforced).
|
||||
Backdrop_Marker :: struct {
|
||||
magic: u32,
|
||||
sigma: f32,
|
||||
tint: Color,
|
||||
radii: Rectangle_Radii,
|
||||
feather_px: f32,
|
||||
feather_ppx: f32,
|
||||
}
|
||||
|
||||
// 'BDPT' in big-endian ASCII. Picked for greppability and to be obviously non-zero in
|
||||
// uninitialized memory; user code that forgets to set the magic field gets routed through
|
||||
// the regular custom_draw path and surfaces as "my custom draw never fired," not as a
|
||||
// silent backdrop schedule.
|
||||
BACKDROP_MARKER_MAGIC :: u32(0x42445054)
|
||||
|
||||
// Returns true if this Clay render command represents a backdrop primitive.
|
||||
// Identified by a magic-number sentinel in the first 4 bytes of customData.
|
||||
// Returns true if this Clay render command represents a backdrop primitive — i.e. its
|
||||
// `customData` points at a `Clay_Custom` whose active variant is `Backdrop_Marker`.
|
||||
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
|
||||
if cmd.commandType != .Custom do return false
|
||||
p := cmd.renderData.custom.customData
|
||||
if p == nil do return false
|
||||
return (^Backdrop_Marker)(p).magic == BACKDROP_MARKER_MAGIC
|
||||
_, ok := (^Clay_Custom)(p).(Backdrop_Marker)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Dispatch a single non-backdrop Clay render command to the appropriate `draw` primitive.
|
||||
@@ -876,28 +940,46 @@ dispatch_clay_command :: proc(
|
||||
}
|
||||
|
||||
rectangle(layer, bounds, BLANK, outline_color = color, outline_width = thickness, radii = radii)
|
||||
case clay.RenderCommandType.Custom: if is_clay_backdrop(render_command) {
|
||||
// The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
||||
case clay.RenderCommandType.Custom:
|
||||
// Copy the CustomRenderData by value so we can patch its `customData` field for the
|
||||
// user callback without mutating Clay-owned memory. After unwrapping, the callback
|
||||
// sees its own pointer in `render_data.customData`, identical to what it would see
|
||||
// if `Clay_Custom` did not exist as an intermediary.
|
||||
patched := render_command.renderData.custom
|
||||
// Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil
|
||||
// `customData` both surface to the callback as `customData = nil`.
|
||||
patched.customData = nil
|
||||
if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil {
|
||||
switch custom_value in (^Clay_Custom)(custom_data_pointer)^ {
|
||||
case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
||||
// them here; reaching this branch means either the walker logic is broken or the
|
||||
// `customData` pointee mutated between the walker's `is_clay_backdrop` check and
|
||||
// this re-check (heap corruption / lifetime bug in user-managed customData
|
||||
// `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check
|
||||
// and this re-check (heap corruption / lifetime bug in user-managed customData
|
||||
// memory). Both are renderer-level bugs that warrant a hard failure rather than a
|
||||
// silently-dropped panel.
|
||||
log.panicf(
|
||||
"backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame",
|
||||
render_command.renderData.custom.customData,
|
||||
)
|
||||
} else if custom_draw != nil {
|
||||
custom_draw(layer, bounds, render_command.renderData.custom)
|
||||
} else {
|
||||
log.panicf("Received clay render command of type custom but no custom_draw proc provided.")
|
||||
case rawptr: patched.customData = custom_value
|
||||
}
|
||||
}
|
||||
if custom_draw != nil {
|
||||
custom_draw(layer, bounds, patched)
|
||||
} else if patched.customData != nil {
|
||||
log.panicf(
|
||||
"Received clay render command of type custom with non-nil user data but no custom_draw proc provided.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer.
|
||||
// Caller guarantees a backdrop scope is open on `layer` so the underlying
|
||||
// `append_or_extend_sub_batch` contract assertion is satisfied.
|
||||
// Caller guarantees:
|
||||
// - a backdrop scope is open on `layer` so the underlying `append_or_extend_sub_batch`
|
||||
// contract assertion is satisfied;
|
||||
// - the command's `customData` points at a `Clay_Custom` whose active variant is
|
||||
// `Backdrop_Marker` (the walker has already verified this via `is_clay_backdrop`).
|
||||
//INTERNAL
|
||||
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
||||
bounds := Rectangle {
|
||||
@@ -906,14 +988,17 @@ dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
||||
width = cmd.boundingBox.width,
|
||||
height = cmd.boundingBox.height,
|
||||
}
|
||||
marker := (^Backdrop_Marker)(cmd.renderData.custom.customData)
|
||||
// Type-asserting form (no `, ok`): panics loudly if the variant tag changed since
|
||||
// `is_clay_backdrop`, which is the desired tripwire for a heap-corruption bug in
|
||||
// user-managed customData.
|
||||
marker := (^Clay_Custom)(cmd.renderData.custom.customData).(Backdrop_Marker)
|
||||
backdrop_blur(
|
||||
layer,
|
||||
bounds,
|
||||
gaussian_sigma = marker.sigma,
|
||||
tint = marker.tint,
|
||||
radii = marker.radii,
|
||||
feather_px = marker.feather_px,
|
||||
feather_ppx = marker.feather_ppx,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ hellope_shapes :: proc() {
|
||||
outline_width = 2,
|
||||
origin = draw.center_of(rect),
|
||||
rotation = spin_angle,
|
||||
feather_px = 1,
|
||||
feather_ppx = 1,
|
||||
)
|
||||
|
||||
// Rounded rectangle rotating around its center
|
||||
@@ -287,6 +287,22 @@ hellope_custom :: proc() {
|
||||
value = 0.45,
|
||||
color = {200, 100, 50, 255},
|
||||
}
|
||||
|
||||
// `clay.CustomElementConfig.customData` is a rawptr; the Clay integration in `draw`
|
||||
// requires it to point at a `Clay_Custom` value. The explicit `rawptr(...)` cast is
|
||||
// necessary because Odin does not chain `^Gauge -> rawptr -> Clay_Custom` implicitly
|
||||
// (variant-to-union and ^T-to-rawptr are each implicit on their own, but not stacked).
|
||||
gauge_custom: draw.Clay_Custom = rawptr(&gauge)
|
||||
gauge2_custom: draw.Clay_Custom = rawptr(&gauge2)
|
||||
|
||||
// Backdrop variant: variant-to-union conversion is implicit, so no cast needed.
|
||||
// `tint = draw.WHITE` is the no-op tint per the backdrop module's convention
|
||||
// (matches `examples/backdrop.odin`'s "pure blur, no color" usage).
|
||||
backdrop_custom: draw.Clay_Custom = draw.Backdrop_Marker {
|
||||
sigma = 8,
|
||||
tint = draw.WHITE,
|
||||
}
|
||||
|
||||
spin_angle: f32 = 0
|
||||
|
||||
for {
|
||||
@@ -320,20 +336,37 @@ hellope_custom :: proc() {
|
||||
clay.Text("Custom Draw Demo", &text_config)
|
||||
}
|
||||
|
||||
// gauge1 is BEHIND the backdrop — the backdrop is declared as a floating CHILD
|
||||
// of gauge1, pinned to gauge1's LeftTop and sized 300x30 so it covers exactly
|
||||
// gauge1's footprint. Clay emits a floating child's render command after the
|
||||
// parent's, so the stream order is gauge1 → backdrop → gauge2: gauge1's pixels
|
||||
// land in `source_texture` before the bracket samples (visible as a blurred
|
||||
// reflection inside the strip), and gauge2 is deferred-replayed by
|
||||
// `prepare_clay_batch` after the bracket closes (renders crisp on top of the
|
||||
// bracket output — unrelated to the strip since they don't overlap).
|
||||
if clay.UI()(
|
||||
{
|
||||
id = clay.ID("gauge"),
|
||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||
custom = {customData = &gauge},
|
||||
custom = {customData = &gauge_custom},
|
||||
backgroundColor = {80, 80, 80, 255},
|
||||
},
|
||||
) {
|
||||
if clay.UI()(
|
||||
{
|
||||
id = clay.ID("backdrop"),
|
||||
floating = {attachTo = .Parent, attachment = {parent = .LeftTop, element = .LeftTop}},
|
||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||
custom = {customData = &backdrop_custom},
|
||||
},
|
||||
) {}
|
||||
}
|
||||
|
||||
if clay.UI()(
|
||||
{
|
||||
id = clay.ID("gauge2"),
|
||||
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
|
||||
custom = {customData = &gauge2},
|
||||
custom = {customData = &gauge2_custom},
|
||||
backgroundColor = {80, 80, 80, 255},
|
||||
},
|
||||
) {}
|
||||
@@ -353,6 +386,9 @@ hellope_custom :: proc() {
|
||||
}
|
||||
|
||||
draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) {
|
||||
// `render_data.customData` has been unwrapped from the `Clay_Custom` envelope by
|
||||
// `prepare_clay_batch` — it points at the Gauge directly, the same as it would have
|
||||
// before the union refactor.
|
||||
gauge := cast(^Gauge)render_data.customData
|
||||
|
||||
border_width: f32 = 2
|
||||
|
||||
@@ -25,9 +25,9 @@ struct main0_in
|
||||
{
|
||||
float2 p_local [[user(locn0)]];
|
||||
float4 f_color [[user(locn1)]];
|
||||
float2 f_half_size [[user(locn2), flat]];
|
||||
float4 f_radii [[user(locn3), flat]];
|
||||
float f_half_feather [[user(locn4), flat]];
|
||||
float2 f_half_size_ppx [[user(locn2), flat]];
|
||||
float4 f_radii_ppx [[user(locn3), flat]];
|
||||
float f_half_feather_ppx [[user(locn4), flat]];
|
||||
};
|
||||
|
||||
static inline __attribute__((always_inline))
|
||||
@@ -96,16 +96,16 @@ fragment main0_out main0(main0_in in [[stage_in]], constant Uniforms& _108 [[buf
|
||||
return out;
|
||||
}
|
||||
float2 param_1 = in.p_local;
|
||||
float2 param_2 = in.f_half_size;
|
||||
float4 param_3 = in.f_radii;
|
||||
float2 param_2 = in.f_half_size_ppx;
|
||||
float4 param_3 = in.f_radii_ppx;
|
||||
float d = sdRoundedBox(param_1, param_2, param_3);
|
||||
if (d > in.f_half_feather)
|
||||
if (d > in.f_half_feather_ppx)
|
||||
{
|
||||
discard_fragment();
|
||||
}
|
||||
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
|
||||
float d_n = d / grad_magnitude;
|
||||
float h_n = in.f_half_feather / grad_magnitude;
|
||||
float h_n = in.f_half_feather_ppx / grad_magnitude;
|
||||
float2 uv_1 = (gl_FragCoord.xy * _108.inv_downsample_factor) * _108.inv_working_size;
|
||||
float3 color_1 = blur_input_tex.sample(blur_input_texSmplr, uv_1).xyz;
|
||||
float3 tinted = mix(color_1, color_1 * in.f_color.xyz, float3(in.f_color.w));
|
||||
|
||||
Binary file not shown.
@@ -55,18 +55,18 @@ struct Uniforms
|
||||
struct Gaussian_Blur_Primitive
|
||||
{
|
||||
float4 bounds;
|
||||
float4 radii;
|
||||
float2 half_size;
|
||||
float half_feather;
|
||||
float4 radii_ppx;
|
||||
float2 half_size_ppx;
|
||||
float half_feather_ppx;
|
||||
uint color;
|
||||
};
|
||||
|
||||
struct Gaussian_Blur_Primitive_1
|
||||
{
|
||||
float4 bounds;
|
||||
float4 radii;
|
||||
float2 half_size;
|
||||
float half_feather;
|
||||
float4 radii_ppx;
|
||||
float2 half_size_ppx;
|
||||
float half_feather_ppx;
|
||||
uint color;
|
||||
};
|
||||
|
||||
@@ -81,9 +81,9 @@ struct main0_out
|
||||
{
|
||||
float2 p_local [[user(locn0)]];
|
||||
float4 f_color [[user(locn1)]];
|
||||
float2 f_half_size [[user(locn2)]];
|
||||
float4 f_radii [[user(locn3)]];
|
||||
float f_half_feather [[user(locn4)]];
|
||||
float2 f_half_size_ppx [[user(locn2)]];
|
||||
float4 f_radii_ppx [[user(locn3)]];
|
||||
float f_half_feather_ppx [[user(locn4)]];
|
||||
float4 gl_Position [[position]];
|
||||
};
|
||||
|
||||
@@ -96,26 +96,26 @@ vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Gaussi
|
||||
out.gl_Position = float4(ndc, 0.0, 1.0);
|
||||
out.p_local = float2(0.0);
|
||||
out.f_color = float4(0.0);
|
||||
out.f_half_size = float2(0.0);
|
||||
out.f_radii = float4(0.0);
|
||||
out.f_half_feather = 0.0;
|
||||
out.f_half_size_ppx = float2(0.0);
|
||||
out.f_radii_ppx = float4(0.0);
|
||||
out.f_half_feather_ppx = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Gaussian_Blur_Primitive p;
|
||||
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
|
||||
p.radii = _69.primitives[int(gl_InstanceIndex)].radii;
|
||||
p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size;
|
||||
p.half_feather = _69.primitives[int(gl_InstanceIndex)].half_feather;
|
||||
p.radii_ppx = _69.primitives[int(gl_InstanceIndex)].radii_ppx;
|
||||
p.half_size_ppx = _69.primitives[int(gl_InstanceIndex)].half_size_ppx;
|
||||
p.half_feather_ppx = _69.primitives[int(gl_InstanceIndex)].half_feather_ppx;
|
||||
p.color = _69.primitives[int(gl_InstanceIndex)].color;
|
||||
float2 corner = _97[int(gl_VertexIndex)];
|
||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||
out.p_local = (world_pos - center) * _13.dpi_scale;
|
||||
out.f_color = unpack_unorm4x8_to_float(p.color);
|
||||
out.f_half_size = p.half_size;
|
||||
out.f_radii = p.radii;
|
||||
out.f_half_feather = p.half_feather;
|
||||
out.f_half_size_ppx = p.half_size_ppx;
|
||||
out.f_radii_ppx = p.radii_ppx;
|
||||
out.f_half_feather_ppx = p.half_feather_ppx;
|
||||
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
return out;
|
||||
|
||||
Binary file not shown.
@@ -107,57 +107,57 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
}
|
||||
float d = 1000000015047466219876688855040.0;
|
||||
float h = 0.5;
|
||||
float2 half_size = in.f_params.xy;
|
||||
float2 p_local = in.f_local_or_uv;
|
||||
float2 half_size_ppx = in.f_params.xy;
|
||||
float2 p_local_ppx = in.f_local_or_uv;
|
||||
if (kind == 1u)
|
||||
{
|
||||
float4 corner_radii = float4(in.f_params.zw, in.f_params2.xy);
|
||||
float4 corner_radii_ppx = float4(in.f_params.zw, in.f_params2.xy);
|
||||
h = in.f_params2.z;
|
||||
float2 param = p_local;
|
||||
float2 param_1 = half_size;
|
||||
float4 param_2 = corner_radii;
|
||||
float2 param = p_local_ppx;
|
||||
float2 param_1 = half_size_ppx;
|
||||
float4 param_2 = corner_radii_ppx;
|
||||
d = sdRoundedBox(param, param_1, param_2);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kind == 2u)
|
||||
{
|
||||
float radius = in.f_params.x;
|
||||
float radius_ppx = in.f_params.x;
|
||||
float sides = in.f_params.y;
|
||||
h = in.f_params.z;
|
||||
float2 param_3 = p_local;
|
||||
float param_4 = radius;
|
||||
float2 param_3 = p_local_ppx;
|
||||
float param_4 = radius_ppx;
|
||||
float param_5 = sides;
|
||||
d = sdRegularPolygon(param_3, param_4, param_5);
|
||||
half_size = float2(radius);
|
||||
half_size_ppx = float2(radius_ppx);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kind == 3u)
|
||||
{
|
||||
float2 ab = in.f_params.xy;
|
||||
float2 radii_ppx = in.f_params.xy;
|
||||
h = in.f_params.z;
|
||||
float2 param_6 = p_local;
|
||||
float2 param_7 = ab;
|
||||
float2 param_6 = p_local_ppx;
|
||||
float2 param_7 = radii_ppx;
|
||||
d = sdEllipseApprox(param_6, param_7);
|
||||
half_size = ab;
|
||||
half_size_ppx = radii_ppx;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kind == 4u)
|
||||
{
|
||||
float inner = in.f_params.x;
|
||||
float outer = in.f_params.y;
|
||||
float inner_radius_ppx = in.f_params.x;
|
||||
float outer_radius_ppx = in.f_params.y;
|
||||
float2 n_start = in.f_params.zw;
|
||||
float2 n_end = in.f_params2.xy;
|
||||
uint arc_bits = (flags >> 5u) & 3u;
|
||||
h = in.f_params2.z;
|
||||
float r = length(p_local);
|
||||
d = fast::max(inner - r, r - outer);
|
||||
float r = length(p_local_ppx);
|
||||
d = fast::max(inner_radius_ppx - r, r - outer_radius_ppx);
|
||||
if (arc_bits != 0u)
|
||||
{
|
||||
float d_start = dot(p_local, n_start);
|
||||
float d_end = dot(p_local, n_end);
|
||||
float d_start = dot(p_local_ppx, n_start);
|
||||
float d_end = dot(p_local_ppx, n_end);
|
||||
float _338;
|
||||
if (arc_bits == 1u)
|
||||
{
|
||||
@@ -170,7 +170,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
float d_wedge = _338;
|
||||
d = fast::max(d, d_wedge);
|
||||
}
|
||||
half_size = float2(outer);
|
||||
half_size_ppx = float2(outer_radius_ppx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x);
|
||||
if ((flags & 4u) != 0u)
|
||||
{
|
||||
float t_1 = length(p_local / half_size);
|
||||
float t_1 = length(p_local_ppx / half_size_ppx);
|
||||
float4 param_8 = gradient_start;
|
||||
float4 param_9 = gradient_end;
|
||||
float param_10 = t_1;
|
||||
@@ -194,7 +194,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
else
|
||||
{
|
||||
float2 direction = float2(as_type<half2>(in.f_effects.z));
|
||||
float t_2 = (dot(p_local / half_size, direction) * 0.5) + 0.5;
|
||||
float t_2 = (dot(p_local_ppx / half_size_ppx, direction) * 0.5) + 0.5;
|
||||
float4 param_11 = gradient_start;
|
||||
float4 param_12 = gradient_end;
|
||||
float param_13 = t_2;
|
||||
@@ -206,7 +206,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
||||
if ((flags & 1u) != 0u)
|
||||
{
|
||||
float4 uv_rect = in.f_uv_rect;
|
||||
float2 local_uv = ((p_local / half_size) * 0.5) + float2(0.5);
|
||||
float2 local_uv = ((p_local_ppx / half_size_ppx) * 0.5) + float2(0.5);
|
||||
float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||
shape_color = in.f_color * tex.sample(texSmplr, uv);
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -60,32 +60,21 @@ struct main0_in
|
||||
float4 v_color [[attribute(2)]];
|
||||
};
|
||||
|
||||
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]])
|
||||
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Core_2D_Primitives& _31 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
|
||||
{
|
||||
main0_out out = {};
|
||||
if (_12.mode == 0u)
|
||||
{
|
||||
out.f_color = in.v_color;
|
||||
out.f_local_or_uv = in.v_uv;
|
||||
out.f_params = float4(0.0);
|
||||
out.f_params2 = float4(0.0);
|
||||
out.f_flags = 0u;
|
||||
out.f_uv_rect = float4(0.0);
|
||||
out.f_effects = uint4(0u);
|
||||
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
else
|
||||
if (_12.mode == 1u)
|
||||
{
|
||||
Core_2D_Primitive p;
|
||||
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
|
||||
p.color = _75.primitives[int(gl_InstanceIndex)].color;
|
||||
p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
|
||||
p.rotation_sc = _75.primitives[int(gl_InstanceIndex)].rotation_sc;
|
||||
p._pad = _75.primitives[int(gl_InstanceIndex)]._pad;
|
||||
p.params = _75.primitives[int(gl_InstanceIndex)].params;
|
||||
p.params2 = _75.primitives[int(gl_InstanceIndex)].params2;
|
||||
p.uv_rect = _75.primitives[int(gl_InstanceIndex)].uv_rect;
|
||||
p.effects = _75.primitives[int(gl_InstanceIndex)].effects;
|
||||
p.bounds = _31.primitives[int(gl_InstanceIndex)].bounds;
|
||||
p.color = _31.primitives[int(gl_InstanceIndex)].color;
|
||||
p.flags = _31.primitives[int(gl_InstanceIndex)].flags;
|
||||
p.rotation_sc = _31.primitives[int(gl_InstanceIndex)].rotation_sc;
|
||||
p._pad = _31.primitives[int(gl_InstanceIndex)]._pad;
|
||||
p.params = _31.primitives[int(gl_InstanceIndex)].params;
|
||||
p.params2 = _31.primitives[int(gl_InstanceIndex)].params2;
|
||||
p.uv_rect = _31.primitives[int(gl_InstanceIndex)].uv_rect;
|
||||
p.effects = _31.primitives[int(gl_InstanceIndex)].effects;
|
||||
float2 corner = in.v_position;
|
||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||
@@ -105,6 +94,27 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
|
||||
out.f_effects = p.effects;
|
||||
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
out.f_color = in.v_color;
|
||||
out.f_local_or_uv = in.v_uv;
|
||||
out.f_params = float4(0.0);
|
||||
out.f_params2 = float4(0.0);
|
||||
out.f_flags = 0u;
|
||||
out.f_uv_rect = float4(0.0);
|
||||
out.f_effects = uint4(0u);
|
||||
float2 _199;
|
||||
if (_12.mode == 2u)
|
||||
{
|
||||
_199 = in.v_position;
|
||||
}
|
||||
else
|
||||
{
|
||||
_199 = in.v_position * _12.dpi_scale;
|
||||
}
|
||||
float2 pos = _199;
|
||||
out.gl_Position = _12.projection * float4(pos, 0.0, 1.0);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -40,9 +40,9 @@ const uint MAX_KERNEL_PAIRS = 32;
|
||||
// --- Inputs from vertex shader ---
|
||||
layout(location = 0) in vec2 p_local;
|
||||
layout(location = 1) in mediump vec4 f_color;
|
||||
layout(location = 2) flat in vec2 f_half_size;
|
||||
layout(location = 3) flat in vec4 f_radii;
|
||||
layout(location = 4) flat in float f_half_feather;
|
||||
layout(location = 2) flat in vec2 f_half_size_ppx;
|
||||
layout(location = 3) flat in vec4 f_radii_ppx;
|
||||
layout(location = 4) flat in float f_half_feather_ppx;
|
||||
|
||||
// --- Output ---
|
||||
layout(location = 0) out vec4 out_color;
|
||||
@@ -123,15 +123,15 @@ void main() {
|
||||
|
||||
// ---- Mode 1: composite per-primitive.
|
||||
// RRect SDF — early discard for fragments well outside the masked region.
|
||||
float d = sdRoundedBox(p_local, f_half_size, f_radii);
|
||||
if (d > f_half_feather) {
|
||||
float d = sdRoundedBox(p_local, f_half_size_ppx, f_radii_ppx);
|
||||
if (d > f_half_feather_ppx) {
|
||||
discard;
|
||||
}
|
||||
|
||||
// fwidth-based normalization for AA (matches main pipeline approach).
|
||||
float grad_magnitude = max(fwidth(d), 1e-6);
|
||||
float d_n = d / grad_magnitude;
|
||||
float h_n = f_half_feather / grad_magnitude;
|
||||
float h_n = f_half_feather_ppx / grad_magnitude;
|
||||
|
||||
// Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to
|
||||
// working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
layout(location = 0) out vec2 p_local;
|
||||
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
|
||||
layout(location = 1) out mediump vec4 f_color;
|
||||
// f_half_size: RRect half extents in physical pixels (mode 1 only).
|
||||
layout(location = 2) flat out vec2 f_half_size;
|
||||
// f_radii: per-corner radii in physical pixels (mode 1 only).
|
||||
layout(location = 3) flat out vec4 f_radii;
|
||||
// f_half_feather: SDF anti-aliasing feather (mode 1 only).
|
||||
layout(location = 4) flat out float f_half_feather;
|
||||
// f_half_size_ppx: RRect half extents in physical pixels (mode 1 only).
|
||||
layout(location = 2) flat out vec2 f_half_size_ppx;
|
||||
// f_radii_ppx: per-corner radii in physical pixels (mode 1 only).
|
||||
layout(location = 3) flat out vec4 f_radii_ppx;
|
||||
// f_half_feather_ppx: SDF anti-aliasing feather in physical pixels (mode 1 only).
|
||||
layout(location = 4) flat out float f_half_feather_ppx;
|
||||
|
||||
// --- Uniforms (set 1) ---
|
||||
// Backdrop pipeline's own uniform block — distinct from the main pipeline's
|
||||
@@ -53,10 +53,10 @@ layout(set = 1, binding = 0) uniform Uniforms {
|
||||
// edge effects (e.g. liquid-glass-style refraction outlines) would be a dedicated
|
||||
// primitive type with its own pipeline rather than a flag bit here.
|
||||
struct Gaussian_Blur_Primitive {
|
||||
vec4 bounds; // 0-15: min_xy, max_xy (world-space)
|
||||
vec4 radii; // 16-31: per-corner radii (physical px)
|
||||
vec2 half_size; // 32-39: RRect half extents (physical px)
|
||||
float half_feather; // 40-43: SDF anti-aliasing feather (physical px)
|
||||
vec4 bounds; // 0-15: min_xy, max_xy (world-space, logical px)
|
||||
vec4 radii_ppx; // 16-31: per-corner radii
|
||||
vec2 half_size_ppx; // 32-39: RRect half extents
|
||||
float half_feather_ppx; // 40-43: SDF anti-aliasing feather
|
||||
uint color; // 44-47: tint, packed RGBA u8x4
|
||||
};
|
||||
|
||||
@@ -78,9 +78,9 @@ void main() {
|
||||
// Mode 0 doesn't read the per-primitive varyings; zero-init for safety.
|
||||
p_local = vec2(0.0);
|
||||
f_color = vec4(0.0);
|
||||
f_half_size = vec2(0.0);
|
||||
f_radii = vec4(0.0);
|
||||
f_half_feather = 0.0;
|
||||
f_half_size_ppx = vec2(0.0);
|
||||
f_radii_ppx = vec4(0.0);
|
||||
f_half_feather_ppx = 0.0;
|
||||
} else {
|
||||
// ---- Mode 1: V-composite instanced unit-quad over Gaussian_Blur_Primitive ----
|
||||
Gaussian_Blur_Primitive p = primitives[gl_InstanceIndex];
|
||||
@@ -101,9 +101,9 @@ void main() {
|
||||
p_local = (world_pos - center) * dpi_scale;
|
||||
|
||||
f_color = unpackUnorm4x8(p.color);
|
||||
f_half_size = p.half_size;
|
||||
f_radii = p.radii;
|
||||
f_half_feather = p.half_feather;
|
||||
f_half_size_ppx = p.half_size_ppx;
|
||||
f_radii_ppx = p.radii_ppx;
|
||||
f_half_feather_ppx = p.half_feather_ppx;
|
||||
|
||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ float sdRegularPolygon(vec2 p, float r, float n) {
|
||||
return length(p) * cos(bn) - r;
|
||||
}
|
||||
|
||||
// Coverage from SDF distance using half-feather width (feather_px * 0.5, pre-computed on CPU).
|
||||
// Coverage from SDF distance using half-feather width (feather_ppx * 0.5, pre-computed on CPU).
|
||||
// Produces a symmetric transition centered on d=0: smoothstep(-h, h, d).
|
||||
float sdf_alpha(float d, float h) {
|
||||
return 1.0 - smoothstep(-h, h, d);
|
||||
@@ -80,56 +80,56 @@ void main() {
|
||||
|
||||
// SDF path — dispatch on kind
|
||||
float d = 1e30;
|
||||
float h = 0.5; // half-feather width; overwritten per shape kind
|
||||
vec2 half_size = f_params.xy; // used by RRect and as reference size for gradients
|
||||
float h = 0.5; // half-feather width (physical px); overwritten per shape kind
|
||||
vec2 half_size_ppx = f_params.xy; // used by RRect and as reference size for gradients
|
||||
|
||||
vec2 p_local = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
|
||||
vec2 p_local_ppx = f_local_or_uv; // arrives rotated; vertex shader handled .Rotated
|
||||
|
||||
if (kind == 1u) {
|
||||
// RRect — half_feather in params2.z
|
||||
vec4 corner_radii = vec4(f_params.zw, f_params2.xy);
|
||||
// RRect — half_feather_ppx in params2.z
|
||||
vec4 corner_radii_ppx = vec4(f_params.zw, f_params2.xy);
|
||||
h = f_params2.z;
|
||||
d = sdRoundedBox(p_local, half_size, corner_radii);
|
||||
d = sdRoundedBox(p_local_ppx, half_size_ppx, corner_radii_ppx);
|
||||
}
|
||||
else if (kind == 2u) {
|
||||
// NGon — half_feather in params.z
|
||||
float radius = f_params.x;
|
||||
// NGon — half_feather_ppx in params.z
|
||||
float radius_ppx = f_params.x;
|
||||
float sides = f_params.y;
|
||||
h = f_params.z;
|
||||
d = sdRegularPolygon(p_local, radius, sides);
|
||||
half_size = vec2(radius); // for gradient UV computation
|
||||
d = sdRegularPolygon(p_local_ppx, radius_ppx, sides);
|
||||
half_size_ppx = vec2(radius_ppx); // for gradient UV computation
|
||||
}
|
||||
else if (kind == 3u) {
|
||||
// Ellipse — half_feather in params.z
|
||||
vec2 ab = f_params.xy;
|
||||
// Ellipse — half_feather_ppx in params.z
|
||||
vec2 radii_ppx = f_params.xy;
|
||||
h = f_params.z;
|
||||
d = sdEllipseApprox(p_local, ab);
|
||||
half_size = ab; // for gradient UV computation
|
||||
d = sdEllipseApprox(p_local_ppx, radii_ppx);
|
||||
half_size_ppx = radii_ppx; // for gradient UV computation
|
||||
}
|
||||
else if (kind == 4u) {
|
||||
// Ring_Arc — half_feather in params2.z
|
||||
// Ring_Arc — half_feather_ppx in params2.z
|
||||
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π)
|
||||
float inner = f_params.x;
|
||||
float outer = f_params.y;
|
||||
float inner_radius_ppx = f_params.x;
|
||||
float outer_radius_ppx = f_params.y;
|
||||
vec2 n_start = f_params.zw;
|
||||
vec2 n_end = f_params2.xy;
|
||||
uint arc_bits = (flags >> 5u) & 3u;
|
||||
|
||||
h = f_params2.z;
|
||||
|
||||
float r = length(p_local);
|
||||
d = max(inner - r, r - outer);
|
||||
float r = length(p_local_ppx);
|
||||
d = max(inner_radius_ppx - r, r - outer_radius_ppx);
|
||||
|
||||
if (arc_bits != 0u) {
|
||||
float d_start = dot(p_local, n_start);
|
||||
float d_end = dot(p_local, n_end);
|
||||
float d_start = dot(p_local_ppx, n_start);
|
||||
float d_end = dot(p_local_ppx, n_end);
|
||||
float d_wedge = (arc_bits == 1u)
|
||||
? max(d_start, d_end) // arc ≤ π: intersect half-planes
|
||||
: min(d_start, d_end); // arc > π: union half-planes
|
||||
d = max(d, d_wedge);
|
||||
}
|
||||
|
||||
half_size = vec2(outer); // for gradient UV computation
|
||||
half_size_ppx = vec2(outer_radius_ppx); // for gradient UV computation
|
||||
}
|
||||
|
||||
// --- fwidth-based normalization for correct AA and stroke width ---
|
||||
@@ -146,18 +146,18 @@ void main() {
|
||||
|
||||
if ((flags & 4u) != 0u) {
|
||||
// Radial gradient (bit 2): t from distance to center
|
||||
mediump float t = length(p_local / half_size);
|
||||
mediump float t = length(p_local_ppx / half_size_ppx);
|
||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||
} else {
|
||||
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
|
||||
vec2 direction = unpackHalf2x16(f_effects.z);
|
||||
mediump float t = dot(p_local / half_size, direction) * 0.5 + 0.5;
|
||||
mediump float t = dot(p_local_ppx / half_size_ppx, direction) * 0.5 + 0.5;
|
||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||
}
|
||||
} else if ((flags & 1u) != 0u) {
|
||||
// Textured (bit 0)
|
||||
vec4 uv_rect = f_uv_rect;
|
||||
vec2 local_uv = p_local / half_size * 0.5 + 0.5;
|
||||
vec2 local_uv = p_local_ppx / half_size_ppx * 0.5 + 0.5;
|
||||
vec2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||
shape_color = f_color * texture(tex, uv);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#version 450 core
|
||||
|
||||
// ---------- Vertex attributes (used in both modes) ----------
|
||||
// ---------- Vertex attributes (used in all modes) ----------
|
||||
layout(location = 0) in vec2 v_position;
|
||||
layout(location = 1) in vec2 v_uv;
|
||||
layout(location = 2) in vec4 v_color;
|
||||
@@ -16,10 +16,18 @@ layout(location = 6) flat out vec4 f_uv_rect;
|
||||
layout(location = 7) flat out uvec4 f_effects;
|
||||
|
||||
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
|
||||
// Mode values mirror Core_2D_Mode in core_2d.odin:
|
||||
// 0 = Tessellated v_position is in logical pixels; shader scales by dpi_scale.
|
||||
// 1 = SDF v_position is a unit-quad corner; world-space comes from
|
||||
// primitives[gl_InstanceIndex].bounds (logical px). Shader
|
||||
// scales by dpi_scale.
|
||||
// 2 = Text v_position is in *physical* pixels already (the CPU baked
|
||||
// the anchor snap and SDL_ttf glyph offsets, both physical).
|
||||
// Shader must NOT rescale.
|
||||
layout(set = 1, binding = 0) uniform Uniforms {
|
||||
mat4 projection;
|
||||
float dpi_scale;
|
||||
uint mode; // 0 = tessellated, 1 = SDF
|
||||
uint mode;
|
||||
};
|
||||
|
||||
// ---------- SDF primitive storage buffer ----------
|
||||
@@ -44,18 +52,7 @@ layout(std430, set = 0, binding = 0) readonly buffer Core_2D_Primitives {
|
||||
|
||||
// ---------- Entry point ----------
|
||||
void main() {
|
||||
if (mode == 0u) {
|
||||
// ---- Mode 0: Tessellated (used for text and arbitrary user geometry) ----
|
||||
f_color = v_color;
|
||||
f_local_or_uv = v_uv;
|
||||
f_params = vec4(0.0);
|
||||
f_params2 = vec4(0.0);
|
||||
f_flags = 0u;
|
||||
f_uv_rect = vec4(0.0);
|
||||
f_effects = uvec4(0);
|
||||
|
||||
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
|
||||
} else {
|
||||
if (mode == 1u) {
|
||||
// ---- Mode 1: SDF instanced quads ----
|
||||
Core_2D_Primitive p = primitives[gl_InstanceIndex];
|
||||
|
||||
@@ -86,5 +83,25 @@ void main() {
|
||||
f_effects = p.effects;
|
||||
|
||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||
} else {
|
||||
// ---- Mode 0 (Tessellated) and Mode 2 (Text) ----
|
||||
// Both feed the raw-vertex pipeline (kind 0 in the fragment shader).
|
||||
// They differ only in what coord space `v_position` is in:
|
||||
// Mode 0 — logical pixels, scale here by dpi_scale.
|
||||
// Mode 2 — physical pixels (CPU pre-scaled and snapped to integer
|
||||
// physical pixels for atlas-aligned bilinear sampling).
|
||||
// Do NOT rescale.
|
||||
// `mode` is uniform across the workgroup, so the select compiles to a
|
||||
// uniform-controlled branch with no SIMT divergence cost.
|
||||
f_color = v_color;
|
||||
f_local_or_uv = v_uv;
|
||||
f_params = vec4(0.0);
|
||||
f_params2 = vec4(0.0);
|
||||
f_flags = 0u;
|
||||
f_uv_rect = vec4(0.0);
|
||||
f_effects = uvec4(0);
|
||||
|
||||
vec2 pos = (mode == 2u) ? v_position : (v_position * dpi_scale);
|
||||
gl_Position = projection * vec4(pos, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-10
@@ -21,8 +21,8 @@ auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
||||
// Color is premultiplied: the tessellated fragment shader passes it through directly
|
||||
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||||
// Premultiplies the color before storing it on the vertex (see draw package doc's
|
||||
// "Color and blending" section for why).
|
||||
//INTERNAL
|
||||
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex_2D {
|
||||
return draw.Vertex_2D{position = position, color = draw.premultiply_color(color)}
|
||||
@@ -108,16 +108,23 @@ triangle :: proc(
|
||||
draw.prepare_shape(layer, vertices[:])
|
||||
}
|
||||
|
||||
// Draw an anti-aliased triangle via extruded edge quads.
|
||||
// Draw an anti-aliased triangle via extruded edge quads plus corner fan caps.
|
||||
// Interior vertices get the full premultiplied color; outer fringe vertices get BLANK (0,0,0,0).
|
||||
// The rasterizer linearly interpolates between them, producing a smooth 1-pixel AA band.
|
||||
// `aa_px` controls the extrusion width in logical pixels (default 1.0).
|
||||
// This proc emits 21 vertices (3 interior + 6 edge quads × 3 verts each).
|
||||
// The rasterizer linearly interpolates between them, producing a smooth ~1-physical-pixel AA band.
|
||||
// `aa_ppx` controls the extrusion width in *physical* pixels (default 1.0). The CPU divides by
|
||||
// `dpi_scaling` here so the vertex stream stays in logical px; the mode-0 vertex shader scales
|
||||
// back to physical at draw time. Net AA band is ~aa_ppx physical pixels regardless of DPI.
|
||||
//
|
||||
// Topology: 3 interior verts + 6 edge-quad triangles (×3 verts) + 3 corner-fan triangles (×3 verts)
|
||||
// = 30 verts total. The corner fans plug the wedge gaps that would otherwise appear between
|
||||
// adjacent edge fringes at each triangle vertex; without them, sharp corners show a small
|
||||
// background-colored crescent. Apex vertex is full color, both fringe verts are BLANK, so the
|
||||
// fan rasterizes as an alpha-falloff triangle that blends visually into the adjacent edge bands.
|
||||
triangle_aa :: proc(
|
||||
layer: ^draw.Layer,
|
||||
v1, v2, v3: draw.Vec2,
|
||||
color: draw.Color,
|
||||
aa_px: f32 = draw.DFT_FEATHER_PX,
|
||||
aa_ppx: f32 = draw.DFT_FEATHER_PPX,
|
||||
origin: draw.Vec2 = {},
|
||||
rotation: f32 = 0,
|
||||
) {
|
||||
@@ -164,7 +171,9 @@ triangle_aa :: proc(
|
||||
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
|
||||
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
|
||||
|
||||
extrude_distance := aa_px * draw.GLOB.dpi_scaling
|
||||
// aa_ppx is in physical pixels; divide by dpi_scaling so the extrusion lives in logical-pixel
|
||||
// space (the mode-0 vertex shader will scale back to physical at draw time).
|
||||
extrude_distance := aa_ppx / draw.GLOB.dpi_scaling
|
||||
|
||||
// Outer fringe vertices: each edge vertex extruded outward
|
||||
outer_0_01 := p0 + normal_01 * extrude_distance
|
||||
@@ -178,8 +187,8 @@ triangle_aa :: proc(
|
||||
// Outer fringe is BLANK = {0,0,0,0} which is already premul.
|
||||
transparent := draw.BLANK
|
||||
|
||||
// 3 interior + 6 × 3 edge-quad = 21 vertices
|
||||
vertices: [21]draw.Vertex_2D
|
||||
// 3 interior + 6 edge-quad tris (×3 verts) + 3 corner-fan tris (×3 verts) = 30 vertices
|
||||
vertices: [30]draw.Vertex_2D
|
||||
|
||||
// Interior triangle
|
||||
vertices[0] = solid_vertex(p0, color)
|
||||
@@ -210,6 +219,27 @@ triangle_aa :: proc(
|
||||
vertices[19] = solid_vertex(outer_0_20, transparent)
|
||||
vertices[20] = solid_vertex(outer_2_20, transparent)
|
||||
|
||||
// Corner fan caps: each fills the wedge gap between the two edge fringes meeting at a
|
||||
// triangle vertex. Apex is full color; both fringe verts are BLANK, so the rasterizer
|
||||
// produces a smooth alpha falloff across the wedge (matches the adjacent edge-band
|
||||
// gradients at the shared edges, so the seams are invisible). Vertex order per fan:
|
||||
// [apex, fringe-from-incoming-edge, fringe-from-outgoing-edge].
|
||||
|
||||
// Cap at p0 (between incoming edge p2→p0 and outgoing edge p0→p1)
|
||||
vertices[21] = solid_vertex(p0, color)
|
||||
vertices[22] = solid_vertex(outer_0_20, transparent)
|
||||
vertices[23] = solid_vertex(outer_0_01, transparent)
|
||||
|
||||
// Cap at p1 (between incoming edge p0→p1 and outgoing edge p1→p2)
|
||||
vertices[24] = solid_vertex(p1, color)
|
||||
vertices[25] = solid_vertex(outer_1_01, transparent)
|
||||
vertices[26] = solid_vertex(outer_1_12, transparent)
|
||||
|
||||
// Cap at p2 (between incoming edge p1→p2 and outgoing edge p2→p0)
|
||||
vertices[27] = solid_vertex(p2, color)
|
||||
vertices[28] = solid_vertex(outer_2_12, transparent)
|
||||
vertices[29] = solid_vertex(outer_2_20, transparent)
|
||||
|
||||
draw.prepare_shape(layer, vertices[:])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user