DPI scaling fixes
This commit is contained in:
+53
-30
@@ -5,14 +5,27 @@ Clay UI integration.
|
|||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with two submission
|
The renderer uses a single unified `Core_2D` (`TRIANGLELIST` pipeline) with three submission
|
||||||
modes dispatched by a push constant:
|
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
|
- **Mode 0 (Tessellated):** Vertex buffer contains real geometry in _logical_ pixels. The vertex
|
||||||
SDL_ttf atlas textures), single-pixel points (`tess.pixel`), arbitrary user geometry
|
shader scales by `dpi_scale` before projecting. Used for single-pixel points (`tess.pixel`),
|
||||||
(`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`, `tess.triangle_fan`,
|
arbitrary user geometry (`tess.triangle`, `tess.triangle_aa`, `tess.triangle_lines`,
|
||||||
`tess.triangle_strip`), and any raw vertex geometry submitted via `prepare_shape`. The fragment
|
`tess.triangle_fan`, `tess.triangle_strip`), and any raw vertex geometry submitted via
|
||||||
shader premultiplies the texture sample (`t.rgb *= t.a`) and computes `out = color * t`.
|
`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
|
- **Mode 1 (SDF):** A static 6-vertex unit-quad buffer is drawn instanced, with per-primitive
|
||||||
`Core_2D_Primitive` structs (96 bytes each) uploaded each frame to a GPU storage buffer. The vertex
|
`Core_2D_Primitive` structs (96 bytes each) uploaded each frame to a GPU storage buffer. The vertex
|
||||||
@@ -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
|
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
|
with native mediump) via manual live-range analysis. The dominant peak is the Ring_Arc kind path
|
||||||
(wedge normals + inner/outer radii + dot-product temporaries live simultaneously with carried state
|
(wedge normals + inner/outer radii + dot-product temporaries live simultaneously with carried state
|
||||||
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size`). RRect is 1–2 regs lower (`corner_radii` vec4
|
like `f_color`, `f_uv_rect`/`f_effects`, and `half_size_ppx`). RRect is 1–2 regs lower
|
||||||
replaces the separate inner/outer + normal pairs). NGon and Ellipse are lighter still. Real compilers
|
(`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
|
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
|
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
|
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)
|
### Main pipeline: SDF + tessellated (unified)
|
||||||
|
|
||||||
The main pipeline serves two submission modes through a single `TRIANGLELIST` pipeline and a single
|
The main pipeline serves three submission modes through a single `TRIANGLELIST` pipeline and a
|
||||||
vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push constant
|
single vertex input layout, distinguished by a `mode` field in the `Vertex_Uniforms_2D` push
|
||||||
(`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`), pushed per draw call via `push_globals`. The
|
constant (`Core_2D_Mode.Tessellated = 0`, `Core_2D_Mode.SDF = 1`, `Core_2D_Mode.Text = 2`), pushed
|
||||||
vertex shader branches on this uniform to select the tessellated or SDF code path.
|
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
|
- **Tessellated mode** (`mode = 0`): direct vertex buffer with explicit geometry in _logical_
|
||||||
(SDL_ttf atlas sampling), triangles, triangle fans/strips, single-pixel points, and any
|
pixels. Vertex shader scales positions by `dpi_scale`. Used for triangles, triangle fans/strips,
|
||||||
user-provided raw vertex geometry.
|
single-pixel points, and any user-provided raw vertex geometry.
|
||||||
- **SDF mode** (`mode = 1`): shared unit-quad vertex buffer + GPU storage buffer of
|
- **SDF mode** (`mode = 1`): shared unit-quad vertex buffer + GPU storage buffer of
|
||||||
`Core_2D_Primitive` structs, drawn instanced. Used for all shapes with closed-form signed distance
|
`Core_2D_Primitive` structs, drawn instanced. Used for all shapes with closed-form signed distance
|
||||||
functions.
|
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
|
All three modes use the same fragment shader. Modes 0 (Tessellated) and 2 (Text) take the same
|
||||||
`Core_2D_Primitive.flags`): kind 0 (`Solid`) is the tessellated path, which premultiplies the texture
|
fragment-shader path (kind 0), which premultiplies the texture sample and computes `out = color * t`;
|
||||||
sample and computes `out = color * t`; kinds 1–4 dispatch to one of four SDF functions (RRect, NGon,
|
they differ only in the vertex shader (whether positions are pre-scaled to physical pixels). Mode 1
|
||||||
Ellipse, Ring_Arc) and apply gradient/texture/outline/solid color based on `Shape_Flags` bits.
|
(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
|
#### 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
|
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.
|
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
|
The tessellated and text paths retain the existing direct vertex buffer layout (20 bytes/vertex, no
|
||||||
buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every invocation
|
storage buffer access). The vertex shader branch on `mode` (push constant) is warp-uniform — every
|
||||||
in a draw call has the same mode — so it is effectively free on all modern GPUs.
|
invocation in a draw call has the same mode — so it is effectively free on all modern GPUs.
|
||||||
|
|
||||||
#### Shape kinds and SDF dispatch
|
#### Shape kinds and SDF dispatch
|
||||||
|
|
||||||
@@ -715,11 +738,11 @@ Clay has no notion of backdrops. The integration uses Clay's only extension poin
|
|||||||
|
|
||||||
```
|
```
|
||||||
Backdrop_Marker :: struct {
|
Backdrop_Marker :: struct {
|
||||||
magic: u32, // BACKDROP_MARKER_MAGIC (0x42445054, 'BDPT')
|
magic: u32, // BACKDROP_MARKER_MAGIC (0x42445054, 'BDPT')
|
||||||
sigma: f32,
|
sigma: f32,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
radii: Rectangle_Radii,
|
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
|
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
|
||||||
rotation_sc: u32, // 24: packed f16 pair (sin, cos). Requires .Rotated flag.
|
rotation_sc: u32, // 24: packed f16 pair (sin, cos). Requires .Rotated flag.
|
||||||
_pad: f32, // 28: reserved for future use
|
_pad: f32, // 28: reserved for future use
|
||||||
params: Shape_Params, // 32: per-kind params union (half_feather, 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.
|
uv_rect: [4]f32, // 64: texture UV coordinates. Read when .Textured.
|
||||||
effects: Gradient_Outline, // 80: gradient and/or outline parameters (16 bytes).
|
effects: Gradient_Outline, // 80: gradient and/or outline parameters (16 bytes).
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -487,11 +487,11 @@ MAX_GAUSSIAN_BLUR_KERNEL_PAIRS :: 32
|
|||||||
// pipeline rather than tacked onto this one as a flag bit.
|
// pipeline rather than tacked onto this one as a flag bit.
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
Gaussian_Blur_Primitive :: struct {
|
Gaussian_Blur_Primitive :: struct {
|
||||||
bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy)
|
bounds: [4]f32, // 0: 16 — world-space quad (min_xy, max_xy) in logical px
|
||||||
radii: [4]f32, // 16: 16 — per-corner radii in physical pixels (BR, TR, BL, TL)
|
radii_ppx: [4]f32, // 16: 16 — per-corner radii (BR, TR, BL, TL)
|
||||||
half_size: [2]f32, // 32: 8 — RRect half extents (physical px)
|
half_size_ppx: [2]f32, // 32: 8 — RRect half extents
|
||||||
half_feather: f32, // 40: 4 — feather_px * 0.5 (SDF anti-aliasing)
|
half_feather_ppx: f32, // 40: 4 — feather_ppx * 0.5 (SDF anti-aliasing)
|
||||||
color: Color, // 44: 4 — tint, packed RGBA u8x4
|
color: Color, // 44: 4 — tint, packed RGBA u8x4
|
||||||
}
|
}
|
||||||
#assert(size_of(Gaussian_Blur_Primitive) == 48)
|
#assert(size_of(Gaussian_Blur_Primitive) == 48)
|
||||||
|
|
||||||
@@ -1070,7 +1070,7 @@ run_backdrop_bracket :: proc(
|
|||||||
build_backdrop_primitive :: proc(
|
build_backdrop_primitive :: proc(
|
||||||
rect: Rectangle,
|
rect: Rectangle,
|
||||||
radii: Rectangle_Radii,
|
radii: Rectangle_Radii,
|
||||||
feather_px: f32,
|
feather_ppx: f32,
|
||||||
) -> Gaussian_Blur_Primitive {
|
) -> Gaussian_Blur_Primitive {
|
||||||
max_radius := min(rect.width, rect.height) * 0.5
|
max_radius := min(rect.width, rect.height) * 0.5
|
||||||
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
||||||
@@ -1078,8 +1078,8 @@ build_backdrop_primitive :: proc(
|
|||||||
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
|
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
|
||||||
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
|
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
|
||||||
|
|
||||||
half_feather := feather_px * 0.5
|
half_feather_ppx := feather_ppx * 0.5
|
||||||
padding := half_feather / GLOB.dpi_scaling
|
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||||
dpi_scale := GLOB.dpi_scaling
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
half_width := rect.width * 0.5
|
half_width := rect.width * 0.5
|
||||||
@@ -1088,7 +1088,7 @@ build_backdrop_primitive :: proc(
|
|||||||
center_y := rect.y + half_height
|
center_y := rect.y + half_height
|
||||||
|
|
||||||
return Gaussian_Blur_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,
|
||||||
center_x + half_width + padding,
|
center_x + half_width + padding,
|
||||||
@@ -1098,14 +1098,14 @@ build_backdrop_primitive :: proc(
|
|||||||
// (p.x > 0) ? r.xy : r.zw picks right-vs-left half
|
// (p.x > 0) ? r.xy : r.zw picks right-vs-left half
|
||||||
// then (p.y > 0) ? rxy.x : rxy.y picks bottom-vs-top within that half
|
// then (p.y > 0) ? rxy.x : rxy.y picks bottom-vs-top within that half
|
||||||
// So slot 0 = bottom-right, slot 1 = top-right, slot 2 = bottom-left, slot 3 = top-left.
|
// So slot 0 = bottom-right, slot 1 = top-right, slot 2 = bottom-left, slot 3 = top-left.
|
||||||
radii = {
|
radii_ppx = {
|
||||||
clamped_bottom_right * dpi_scale,
|
clamped_bottom_right * dpi_scale,
|
||||||
clamped_top_right * dpi_scale,
|
clamped_top_right * dpi_scale,
|
||||||
clamped_bottom_left * dpi_scale,
|
clamped_bottom_left * dpi_scale,
|
||||||
clamped_top_left * dpi_scale,
|
clamped_top_left * dpi_scale,
|
||||||
},
|
},
|
||||||
half_size = {half_width * dpi_scale, half_height * dpi_scale},
|
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale},
|
||||||
half_feather = half_feather,
|
half_feather_ppx = half_feather_ppx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1162,9 +1162,9 @@ backdrop_blur :: proc(
|
|||||||
gaussian_sigma: f32,
|
gaussian_sigma: f32,
|
||||||
tint: Color = DFT_TINT,
|
tint: Color = DFT_TINT,
|
||||||
radii: Rectangle_Radii = {},
|
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
|
prim.color = tint
|
||||||
prepare_backdrop_primitive(layer, prim, gaussian_sigma)
|
prepare_backdrop_primitive(layer, prim, gaussian_sigma)
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-81
@@ -9,11 +9,8 @@ import sdl_ttf "vendor:sdl3/ttf"
|
|||||||
|
|
||||||
//----- Vertex layout ----------------------------------
|
//----- Vertex layout ----------------------------------
|
||||||
|
|
||||||
// Vertex layout for tessellated and text geometry.
|
// Vertex layout for tessellated and text geometry. `color` must be premultiplied alpha; see
|
||||||
// IMPORTANT: `color` must be premultiplied alpha (RGB channels pre-scaled by alpha).
|
// the package doc's "Color and blending" section for the contract.
|
||||||
// The tessellated fragment shader passes vertex color through directly — it does NOT
|
|
||||||
// premultiply. The blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied-over).
|
|
||||||
// Use `premultiply_color` when constructing vertices manually for `prepare_shape`.
|
|
||||||
Vertex_2D :: struct {
|
Vertex_2D :: struct {
|
||||||
position: Vec2,
|
position: Vec2,
|
||||||
uv: [2]f32,
|
uv: [2]f32,
|
||||||
@@ -68,35 +65,35 @@ Shape_Flags :: bit_set[Shape_Flag;u8]
|
|||||||
|
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
RRect_Params :: struct {
|
RRect_Params :: struct {
|
||||||
half_size: [2]f32,
|
half_size_ppx: [2]f32,
|
||||||
radii: [4]f32,
|
radii_ppx: [4]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)
|
||||||
_: f32,
|
_: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
NGon_Params :: struct {
|
NGon_Params :: struct {
|
||||||
radius: f32,
|
radius_ppx: f32,
|
||||||
sides: 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,
|
_: [5]f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
Ellipse_Params :: struct {
|
Ellipse_Params :: struct {
|
||||||
radii: [2]f32,
|
radii_ppx: [2]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,
|
_: [5]f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
Ring_Arc_Params :: struct {
|
Ring_Arc_Params :: struct {
|
||||||
inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
|
inner_radius_ppx: f32, // 0 for pie slice
|
||||||
outer_radius: f32, // outer radius in physical pixels
|
outer_radius_ppx: f32,
|
||||||
normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start))
|
normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start))
|
||||||
normal_end: [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end))
|
normal_end: [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end))
|
||||||
half_feather: 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,
|
_: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
@@ -176,9 +173,7 @@ Core_2D :: struct {
|
|||||||
sampler: ^sdl.GPUSampler,
|
sampler: ^sdl.GPUSampler,
|
||||||
}
|
}
|
||||||
|
|
||||||
// MSAA is not supported by levlib (see init's doc comment in draw.odin); the PSO is hard-wired
|
// PSO is hard-wired to single-sample (no MSAA — see package doc's "Anti-aliasing" section).
|
||||||
// to single-sample. SDF text and shapes provide analytical AA via smoothstep; tessellated user
|
|
||||||
// geometry is not anti-aliased.
|
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
create_core_2d :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> (core_2d: Core_2D, ok: bool) {
|
create_core_2d :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> (core_2d: Core_2D, ok: bool) {
|
||||||
// On failure, clean up any partially-created resources
|
// On failure, clean up any partially-created resources
|
||||||
@@ -464,10 +459,31 @@ destroy_core_2d :: proc(device: ^sdl.GPUDevice, core: ^Core_2D) {
|
|||||||
|
|
||||||
//----- Vertex uniforms ----------------------------------
|
//----- Vertex uniforms ----------------------------------
|
||||||
|
|
||||||
|
//
|
||||||
|
// Coordinate-space contract for the main pipeline's vertex shader:
|
||||||
|
//
|
||||||
|
// Tessellated (0) — `v_position` arrives in *logical* pixels. The vertex
|
||||||
|
// shader multiplies by `dpi_scale` before applying the
|
||||||
|
// ortho projection (which is sized to physical pixels).
|
||||||
|
// SDF (1) — `v_position` is a unit-quad corner (0..1). World-space
|
||||||
|
// coordinates come from `Core_2D_Primitive.bounds` in
|
||||||
|
// logical pixels; the shader scales by `dpi_scale`.
|
||||||
|
// Text (2) — `v_position` arrives in *physical* pixels already.
|
||||||
|
// `prepare_text` and `prepare_text_transformed` bake the
|
||||||
|
// anchor + glyph offsets (from SDL_ttf's GPU text engine,
|
||||||
|
// which lays glyphs out in physical pixels) into the
|
||||||
|
// vertex stream and snap the anchor to integer physical
|
||||||
|
// pixels for atlas-aligned bilinear sampling. The shader
|
||||||
|
// therefore must NOT rescale these vertices.
|
||||||
|
//
|
||||||
|
// The two raw-vertex modes (Tessellated, Text) share `prepare_shape`-style
|
||||||
|
// glue but their coord spaces diverge — see `base_2d.vert` for the shader-
|
||||||
|
// side branch.
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
Core_2D_Mode :: enum u32 {
|
Core_2D_Mode :: enum u32 {
|
||||||
Tessellated = 0,
|
Tessellated = 0,
|
||||||
SDF = 1,
|
SDF = 1,
|
||||||
|
Text = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
@@ -814,9 +830,12 @@ render_layer_sub_batch_range :: proc(
|
|||||||
sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)
|
sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)
|
||||||
|
|
||||||
case .Text:
|
case .Text:
|
||||||
if current_mode != .Tessellated {
|
// Text vertices live in physical-pixel space (see Core_2D_Mode.Text
|
||||||
push_globals(cmd_buffer, width, height, .Tessellated)
|
// docs); mode 2 makes the shader skip the `* dpi_scale` step that
|
||||||
current_mode = .Tessellated
|
// 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 {
|
if current_vert_buf != main_vert_buf {
|
||||||
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
|
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
|
||||||
@@ -922,8 +941,8 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
|
|||||||
|
|
||||||
// Snap base position to integer physical pixels to avoid atlas sub-pixel
|
// Snap base position to integer physical pixels to avoid atlas sub-pixel
|
||||||
// sampling blur (and the off-by-one bottom-row clip that comes with it).
|
// sampling blur (and the off-by-one bottom-row clip that comes with it).
|
||||||
base_x := math.round(text.position[0] * GLOB.dpi_scaling)
|
base_x_ppx := math.round(text.position[0] * GLOB.dpi_scaling)
|
||||||
base_y := math.round(text.position[1] * GLOB.dpi_scaling)
|
base_y_ppx := math.round(text.position[1] * GLOB.dpi_scaling)
|
||||||
|
|
||||||
// Premultiply text color once — reused across all glyph vertices.
|
// Premultiply text color once — reused across all glyph vertices.
|
||||||
pm_color := premultiply_color(text.color)
|
pm_color := premultiply_color(text.color)
|
||||||
@@ -938,7 +957,7 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
|
|||||||
uv := data.uv[i]
|
uv := data.uv[i]
|
||||||
append(
|
append(
|
||||||
&GLOB.tmp_text_verts,
|
&GLOB.tmp_text_verts,
|
||||||
Vertex_2D{position = {pos.x + base_x, -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,
|
radii: Rectangle_Radii,
|
||||||
origin: Vec2,
|
origin: Vec2,
|
||||||
rotation: f32,
|
rotation: f32,
|
||||||
feather_px: f32,
|
feather_ppx: f32,
|
||||||
) -> Core_2D_Primitive {
|
) -> Core_2D_Primitive {
|
||||||
max_radius := min(rect.width, rect.height) * 0.5
|
max_radius := min(rect.width, rect.height) * 0.5
|
||||||
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
clamped_top_left := clamp(radii.top_left, 0, max_radius)
|
||||||
@@ -1087,8 +1106,8 @@ build_rrect_primitive :: proc(
|
|||||||
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
|
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
|
||||||
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
|
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
|
||||||
|
|
||||||
half_feather := feather_px * 0.5
|
half_feather_ppx := feather_ppx * 0.5
|
||||||
padding := half_feather / GLOB.dpi_scaling
|
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||||
dpi_scale := GLOB.dpi_scaling
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
half_width := rect.width * 0.5
|
half_width := rect.width * 0.5
|
||||||
@@ -1126,14 +1145,14 @@ build_rrect_primitive :: proc(
|
|||||||
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
||||||
}
|
}
|
||||||
prim.params.rrect = RRect_Params {
|
prim.params.rrect = RRect_Params {
|
||||||
half_size = {half_width * dpi_scale, half_height * dpi_scale},
|
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale},
|
||||||
radii = {
|
radii_ppx = {
|
||||||
clamped_bottom_right * dpi_scale,
|
clamped_bottom_right * dpi_scale,
|
||||||
clamped_top_right * dpi_scale,
|
clamped_top_right * dpi_scale,
|
||||||
clamped_bottom_left * dpi_scale,
|
clamped_bottom_left * dpi_scale,
|
||||||
clamped_top_left * dpi_scale,
|
clamped_top_left * dpi_scale,
|
||||||
},
|
},
|
||||||
half_feather = half_feather,
|
half_feather_ppx = half_feather_ppx,
|
||||||
}
|
}
|
||||||
return prim
|
return prim
|
||||||
}
|
}
|
||||||
@@ -1146,10 +1165,10 @@ build_circle_primitive :: proc(
|
|||||||
radius: f32,
|
radius: f32,
|
||||||
origin: Vec2,
|
origin: Vec2,
|
||||||
rotation: f32,
|
rotation: f32,
|
||||||
feather_px: f32,
|
feather_ppx: f32,
|
||||||
) -> Core_2D_Primitive {
|
) -> Core_2D_Primitive {
|
||||||
half_feather := feather_px * 0.5
|
half_feather_ppx := feather_ppx * 0.5
|
||||||
padding := half_feather / GLOB.dpi_scaling
|
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||||
dpi_scale := GLOB.dpi_scaling
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
actual_center := center
|
actual_center := center
|
||||||
@@ -1166,11 +1185,11 @@ build_circle_primitive :: proc(
|
|||||||
actual_center.y + radius + padding,
|
actual_center.y + radius + padding,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
scaled_radius := radius * dpi_scale
|
radius_ppx := radius * dpi_scale
|
||||||
prim.params.rrect = RRect_Params {
|
prim.params.rrect = RRect_Params {
|
||||||
half_size = {scaled_radius, scaled_radius},
|
half_size_ppx = {radius_ppx, radius_ppx},
|
||||||
radii = {scaled_radius, scaled_radius, scaled_radius, scaled_radius},
|
radii_ppx = {radius_ppx, radius_ppx, radius_ppx, radius_ppx},
|
||||||
half_feather = half_feather,
|
half_feather_ppx = half_feather_ppx,
|
||||||
}
|
}
|
||||||
return prim
|
return prim
|
||||||
}
|
}
|
||||||
@@ -1183,10 +1202,10 @@ build_ellipse_primitive :: proc(
|
|||||||
radius_horizontal, radius_vertical: f32,
|
radius_horizontal, radius_vertical: f32,
|
||||||
origin: Vec2,
|
origin: Vec2,
|
||||||
rotation: f32,
|
rotation: f32,
|
||||||
feather_px: f32,
|
feather_ppx: f32,
|
||||||
) -> Core_2D_Primitive {
|
) -> Core_2D_Primitive {
|
||||||
half_feather := feather_px * 0.5
|
half_feather_ppx := feather_ppx * 0.5
|
||||||
padding := half_feather / GLOB.dpi_scaling
|
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||||
dpi_scale := GLOB.dpi_scaling
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
actual_center := center
|
actual_center := center
|
||||||
@@ -1218,8 +1237,8 @@ build_ellipse_primitive :: proc(
|
|||||||
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
|
||||||
}
|
}
|
||||||
prim.params.ellipse = Ellipse_Params {
|
prim.params.ellipse = Ellipse_Params {
|
||||||
radii = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
|
radii_ppx = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
|
||||||
half_feather = half_feather,
|
half_feather_ppx = half_feather_ppx,
|
||||||
}
|
}
|
||||||
return prim
|
return prim
|
||||||
}
|
}
|
||||||
@@ -1233,10 +1252,10 @@ build_polygon_primitive :: proc(
|
|||||||
radius: f32,
|
radius: f32,
|
||||||
origin: Vec2,
|
origin: Vec2,
|
||||||
rotation: f32,
|
rotation: f32,
|
||||||
feather_px: f32,
|
feather_ppx: f32,
|
||||||
) -> Core_2D_Primitive {
|
) -> Core_2D_Primitive {
|
||||||
half_feather := feather_px * 0.5
|
half_feather_ppx := feather_ppx * 0.5
|
||||||
padding := half_feather / GLOB.dpi_scaling
|
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||||
dpi_scale := GLOB.dpi_scaling
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
actual_center := center
|
actual_center := center
|
||||||
@@ -1258,9 +1277,9 @@ build_polygon_primitive :: proc(
|
|||||||
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
|
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
|
||||||
}
|
}
|
||||||
prim.params.ngon = NGon_Params {
|
prim.params.ngon = NGon_Params {
|
||||||
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
|
radius_ppx = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
|
||||||
sides = f32(sides),
|
sides = f32(sides),
|
||||||
half_feather = half_feather,
|
half_feather_ppx = half_feather_ppx,
|
||||||
}
|
}
|
||||||
return prim
|
return prim
|
||||||
}
|
}
|
||||||
@@ -1278,13 +1297,13 @@ build_ring_arc_primitive :: proc(
|
|||||||
end_angle: f32,
|
end_angle: f32,
|
||||||
origin: Vec2,
|
origin: Vec2,
|
||||||
rotation: f32,
|
rotation: f32,
|
||||||
feather_px: f32,
|
feather_ppx: f32,
|
||||||
) -> (
|
) -> (
|
||||||
Core_2D_Primitive,
|
Core_2D_Primitive,
|
||||||
Shape_Flags,
|
Shape_Flags,
|
||||||
) {
|
) {
|
||||||
half_feather := feather_px * 0.5
|
half_feather_ppx := feather_ppx * 0.5
|
||||||
padding := half_feather / GLOB.dpi_scaling
|
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||||
dpi_scale := GLOB.dpi_scaling
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
actual_center := center
|
actual_center := center
|
||||||
@@ -1327,11 +1346,11 @@ build_ring_arc_primitive :: proc(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
prim.params.ring_arc = Ring_Arc_Params {
|
prim.params.ring_arc = Ring_Arc_Params {
|
||||||
inner_radius = inner_radius * dpi_scale,
|
inner_radius_ppx = inner_radius * dpi_scale,
|
||||||
outer_radius = outer_radius * dpi_scale,
|
outer_radius_ppx = outer_radius * dpi_scale,
|
||||||
normal_start = normal_start,
|
normal_start = normal_start,
|
||||||
normal_end = normal_end,
|
normal_end = normal_end,
|
||||||
half_feather = half_feather,
|
half_feather_ppx = half_feather_ppx,
|
||||||
}
|
}
|
||||||
return prim, arc_flags
|
return prim, arc_flags
|
||||||
}
|
}
|
||||||
@@ -1422,9 +1441,9 @@ rectangle :: proc(
|
|||||||
radii: Rectangle_Radii = {},
|
radii: Rectangle_Radii = {},
|
||||||
origin: Vec2 = {},
|
origin: Vec2 = {},
|
||||||
rotation: f32 = 0,
|
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)
|
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1445,9 +1464,9 @@ circle :: proc(
|
|||||||
outline_width: f32 = 0,
|
outline_width: f32 = 0,
|
||||||
origin: Vec2 = {},
|
origin: Vec2 = {},
|
||||||
rotation: f32 = 0,
|
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)
|
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1462,9 +1481,9 @@ ellipse :: proc(
|
|||||||
outline_width: f32 = 0,
|
outline_width: f32 = 0,
|
||||||
origin: Vec2 = {},
|
origin: Vec2 = {},
|
||||||
rotation: f32 = 0,
|
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)
|
apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1481,11 +1500,11 @@ polygon :: proc(
|
|||||||
outline_width: f32 = 0,
|
outline_width: f32 = 0,
|
||||||
origin: Vec2 = {},
|
origin: Vec2 = {},
|
||||||
rotation: f32 = 0,
|
rotation: f32 = 0,
|
||||||
feather_px: f32 = DFT_FEATHER_PX,
|
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||||
) {
|
) {
|
||||||
if sides < 3 do return
|
if sides < 3 do return
|
||||||
|
|
||||||
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
|
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_ppx)
|
||||||
apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width)
|
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,
|
end_angle: f32 = DFT_CIRC_END_ANGLE,
|
||||||
origin: Vec2 = {},
|
origin: Vec2 = {},
|
||||||
rotation: f32 = 0,
|
rotation: f32 = 0,
|
||||||
feather_px: f32 = DFT_FEATHER_PX,
|
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||||
) {
|
) {
|
||||||
prim, arc_flags := build_ring_arc_primitive(
|
prim, arc_flags := build_ring_arc_primitive(
|
||||||
center,
|
center,
|
||||||
@@ -1514,7 +1533,7 @@ ring :: proc(
|
|||||||
end_angle,
|
end_angle,
|
||||||
origin,
|
origin,
|
||||||
rotation,
|
rotation,
|
||||||
feather_px,
|
feather_ppx,
|
||||||
)
|
)
|
||||||
apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
|
apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
|
||||||
}
|
}
|
||||||
@@ -1528,7 +1547,7 @@ line :: proc(
|
|||||||
thickness: f32 = DFT_STROKE_THICKNESS,
|
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||||
outline_color: Color = {},
|
outline_color: Color = {},
|
||||||
outline_width: f32 = 0,
|
outline_width: f32 = 0,
|
||||||
feather_px: f32 = DFT_FEATHER_PX,
|
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||||
) {
|
) {
|
||||||
delta_x := end_position.x - start_position.x
|
delta_x := end_position.x - start_position.x
|
||||||
delta_y := end_position.y - start_position.y
|
delta_y := end_position.y - start_position.y
|
||||||
@@ -1544,8 +1563,8 @@ line :: proc(
|
|||||||
half_thickness := thickness * 0.5
|
half_thickness := thickness * 0.5
|
||||||
cap_radius := half_thickness
|
cap_radius := half_thickness
|
||||||
|
|
||||||
half_feather := feather_px * 0.5
|
half_feather_ppx := feather_ppx * 0.5
|
||||||
padding := half_feather / GLOB.dpi_scaling
|
padding := half_feather_ppx / GLOB.dpi_scaling
|
||||||
dpi_scale := GLOB.dpi_scaling
|
dpi_scale := GLOB.dpi_scaling
|
||||||
|
|
||||||
// Expand bounds for rotation
|
// Expand bounds for rotation
|
||||||
@@ -1561,14 +1580,14 @@ line :: proc(
|
|||||||
rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
|
rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
|
||||||
}
|
}
|
||||||
prim.params.rrect = RRect_Params {
|
prim.params.rrect = RRect_Params {
|
||||||
half_size = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
|
half_size_ppx = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
|
||||||
radii = {
|
radii_ppx = {
|
||||||
cap_radius * dpi_scale,
|
cap_radius * dpi_scale,
|
||||||
cap_radius * dpi_scale,
|
cap_radius * dpi_scale,
|
||||||
cap_radius * dpi_scale,
|
cap_radius * dpi_scale,
|
||||||
cap_radius * dpi_scale,
|
cap_radius * dpi_scale,
|
||||||
},
|
},
|
||||||
half_feather = half_feather,
|
half_feather_ppx = half_feather_ppx,
|
||||||
}
|
}
|
||||||
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
|
||||||
}
|
}
|
||||||
@@ -1581,10 +1600,10 @@ line_strip :: proc(
|
|||||||
thickness: f32 = DFT_STROKE_THICKNESS,
|
thickness: f32 = DFT_STROKE_THICKNESS,
|
||||||
outline_color: Color = {},
|
outline_color: Color = {},
|
||||||
outline_width: f32 = 0,
|
outline_width: f32 = 0,
|
||||||
feather_px: f32 = DFT_FEATHER_PX,
|
feather_ppx: f32 = DFT_FEATHER_PPX,
|
||||||
) {
|
) {
|
||||||
if len(points) < 2 do return
|
if len(points) < 2 do return
|
||||||
for i in 0 ..< len(points) - 1 {
|
for i in 0 ..< len(points) - 1 {
|
||||||
line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_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)
|
// SCANLINE OVERLAY (opt-in, terminal surfaces only)
|
||||||
// Repeating-stripe pattern at very low opacity. Stripe is 2px
|
// Repeating-stripe pattern at very low opacity. Stripe is 2 logical
|
||||||
// transparent + 2px black-at-3% (TINT_SCANLINE).
|
// pixels transparent + 2 logical pixels black-at-3% (TINT_SCANLINE).
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
SCANLINE_STRIPE_PX :: 2
|
SCANLINE_STRIPE_LPX :: 2
|
||||||
SCANLINE_GAP_PX :: 2
|
SCANLINE_GAP_LPX :: 2
|
||||||
SCANLINE_COLOR :: TINT_SCANLINE
|
SCANLINE_COLOR :: TINT_SCANLINE
|
||||||
|
|||||||
+137
-95
@@ -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
|
package draw
|
||||||
|
|
||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
@@ -51,7 +114,7 @@ INITIAL_SCISSOR_SIZE :: 10
|
|||||||
// ----- Default parameter values -----
|
// ----- Default parameter values -----
|
||||||
// Named constants for non-zero default procedure parameters. Centralizes magic numbers
|
// Named constants for non-zero default procedure parameters. Centralizes magic numbers
|
||||||
// so they can be tuned in one place and referenced by name in proc signatures.
|
// so they can be tuned in one place and referenced by name in proc signatures.
|
||||||
DFT_FEATHER_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_STROKE_THICKNESS :: 1 // Default line/stroke thickness in logical pixels.
|
||||||
DFT_FONT_SIZE :: 44 // Default font size in points for text rendering.
|
DFT_FONT_SIZE :: 44 // Default font size in points for text rendering.
|
||||||
DFT_CIRC_END_ANGLE :: 360 // Full-circle end angle in degrees (ring/arc).
|
DFT_CIRC_END_ANGLE :: 360 // Full-circle end angle in degrees (ring/arc).
|
||||||
@@ -132,27 +195,13 @@ Global :: struct {
|
|||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// A 2D position in world space. Non-distinct alias for [2]f32 — bare literals like {100, 200}
|
// A 2D position in world space. Non-distinct alias for [2]f32 — bare literals like {100, 200}
|
||||||
// work at non-ambiguous call sites.
|
// work at non-ambiguous call sites. See the package doc for coordinate-system and unit
|
||||||
//
|
// conventions.
|
||||||
// Coordinate system: origin is the top-left corner of the window/layer. X increases rightward,
|
|
||||||
// Y increases downward. This matches SDL, HTML Canvas, and most 2D UI coordinate conventions.
|
|
||||||
// All position parameters in the draw API (center, origin, start_position, end_position, etc.)
|
|
||||||
// use this coordinate system.
|
|
||||||
//
|
|
||||||
// Units are logical pixels (pre-DPI-scaling). The renderer multiplies by dpi_scaling internally
|
|
||||||
// before uploading to the GPU. A Vec2{100, 50} refers to the same visual location regardless of
|
|
||||||
// display DPI.
|
|
||||||
Vec2 :: [2]f32
|
Vec2 :: [2]f32
|
||||||
|
|
||||||
// An RGBA color with 8 bits per channel. Distinct type over [4]u8 so that proc-group
|
// An RGBA color with 8 bits per channel. Distinct type over [4]u8 so that proc-group
|
||||||
// overloads can disambiguate Color from other 4-byte structs.
|
// overloads can disambiguate Color from other 4-byte structs. See the package doc for the
|
||||||
//
|
// memory layout and the premultiplied-over blending contract.
|
||||||
// Channel order: R, G, B, A (indices 0, 1, 2, 3). Alpha 255 is fully opaque, 0 is fully
|
|
||||||
// transparent. This matches the GPU-side layout: the shader unpacks via unpackUnorm4x8 which
|
|
||||||
// reads the bytes in memory order as R, G, B, A and normalizes each to [0, 1].
|
|
||||||
//
|
|
||||||
// When used in the Core_2D_Primitive or Gaussian_Blur_Primitive structs (e.g. .color), the 4 bytes
|
|
||||||
// are stored as a u32 in native byte order and unpacked by the shader.
|
|
||||||
Color :: [4]u8
|
Color :: [4]u8
|
||||||
|
|
||||||
BLACK :: Color{0, 0, 0, 255}
|
BLACK :: Color{0, 0, 0, 255}
|
||||||
@@ -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}
|
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
|
// Pre-multiply RGB channels by alpha. Required for any vertex written to the tessellated
|
||||||
// premultiplied colors because the blend state is ONE, ONE_MINUS_SRC_ALPHA and the
|
// vertex stream (text path or `prepare_shape`-style submissions); see the package doc's
|
||||||
// tessellated fragment shader passes vertex color through without further modification.
|
// "Color and blending" section for the full contract.
|
||||||
// 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 {
|
||||||
@@ -249,7 +297,7 @@ premultiply_color :: #force_inline proc(color: Color) -> Color {
|
|||||||
//INTERNAL
|
//INTERNAL
|
||||||
Sub_Batch_Kind :: enum u8 {
|
Sub_Batch_Kind :: enum u8 {
|
||||||
Tessellated, // non-indexed, white texture or user texture, Core_2D_Mode.Tessellated
|
Tessellated, // non-indexed, white texture or user texture, Core_2D_Mode.Tessellated
|
||||||
Text, // indexed, atlas texture, Core_2D_Mode.Tessellated
|
Text, // indexed, atlas texture, Core_2D_Mode.Text (vertices already in physical-pixel space)
|
||||||
SDF, // instanced unit quad, Core_2D_Mode.SDF
|
SDF, // instanced unit quad, Core_2D_Mode.SDF
|
||||||
// instanced unit quad, backdrop subsystem V-composite (indexes Gaussian_Blur_Primitive).
|
// instanced unit quad, backdrop subsystem V-composite (indexes Gaussian_Blur_Primitive).
|
||||||
// Bracket-scheduled per layer; see README.md § "Backdrop pipeline" for ordering semantics.
|
// Bracket-scheduled per layer; see README.md § "Backdrop pipeline" for ordering semantics.
|
||||||
@@ -289,12 +337,6 @@ Scissor :: struct {
|
|||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
|
// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
|
||||||
//
|
|
||||||
// MSAA is intentionally NOT supported. SDF text and shapes compute coverage analytically via
|
|
||||||
// `smoothstep`, so they don't benefit from multisampling. Tessellated user geometry submitted
|
|
||||||
// via `prepare_shape` is not anti-aliased — if you need AA on tessellated content, render it
|
|
||||||
// to your own offscreen target and submit it as a texture. RAD Debugger and the SBC target
|
|
||||||
// (Mali Valhall, where MSAA's per-tile bandwidth multiplier is expensive) drove this decision.
|
|
||||||
@(require_results)
|
@(require_results)
|
||||||
init :: proc(
|
init :: proc(
|
||||||
device: ^sdl.GPUDevice,
|
device: ^sdl.GPUDevice,
|
||||||
@@ -446,30 +488,6 @@ clear_global :: proc() {
|
|||||||
// ----- Frame ------------
|
// ----- 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
|
// Creates a new layer
|
||||||
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
|
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
|
||||||
if GLOB.open_backdrop_layer != nil {
|
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]
|
return &GLOB.layers[GLOB.curr_layer_index]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a backdrop scope on `layer`. All subsequent draws on `layer` until the matching
|
// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for.
|
||||||
// `end_backdrop` must be backdrop primitives (currently only `backdrop_blur`). Non-backdrop
|
begin :: proc(bounds: Rectangle) -> ^Layer {
|
||||||
// draws inside a scope, or backdrop draws outside one, panic.
|
// Cleanup
|
||||||
//
|
clear_global()
|
||||||
// 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
|
// Begin new layer + start a new scissor
|
||||||
// the scope was opened on; the layer pointer mismatch is a hard error rather than a silent
|
scissor := Scissor {
|
||||||
// recovery to surface integration bugs early.
|
bounds = sdl.Rect {
|
||||||
end_backdrop :: proc(layer: ^Layer) {
|
x = i32(bounds.x * GLOB.dpi_scaling),
|
||||||
if GLOB.open_backdrop_layer != layer {
|
y = i32(bounds.y * GLOB.dpi_scaling),
|
||||||
log.panicf("end_backdrop on wrong layer (open=%p, ended=%p)", GLOB.open_backdrop_layer, layer)
|
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
|
layer := Layer {
|
||||||
// defer-style block scoping:
|
bounds = bounds,
|
||||||
//
|
scissor_len = 1,
|
||||||
// {
|
}
|
||||||
// draw.backdrop_scope(layer)
|
append(&GLOB.layers, layer)
|
||||||
// draw.backdrop_blur(layer, ...)
|
return &GLOB.layers[GLOB.curr_layer_index]
|
||||||
// } // end_backdrop fires automatically
|
|
||||||
@(deferred_in = end_backdrop)
|
|
||||||
backdrop_scope :: #force_inline proc(layer: ^Layer) {
|
|
||||||
begin_backdrop(layer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render primitives. clear_color is the background fill before any layers are drawn.
|
// Render primitives. clear_color is the background fill before any layers are drawn.
|
||||||
@@ -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 ------------
|
// ----- Sub-batch dispatch ------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -736,11 +778,11 @@ ClayBatch :: struct {
|
|||||||
// backdrop runs. Magic-number sentinel chosen over a separate userData flag so the marker
|
// 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.
|
// type stays self-describing in core dumps and in any non-Odin debugger view of the heap.
|
||||||
Backdrop_Marker :: struct {
|
Backdrop_Marker :: struct {
|
||||||
magic: u32,
|
magic: u32,
|
||||||
sigma: f32,
|
sigma: f32,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
radii: Rectangle_Radii,
|
radii: Rectangle_Radii,
|
||||||
feather_px: f32,
|
feather_ppx: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'BDPT' in big-endian ASCII. Picked for greppability and to be obviously non-zero in
|
// 'BDPT' in big-endian ASCII. Picked for greppability and to be obviously non-zero in
|
||||||
@@ -913,7 +955,7 @@ dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
|||||||
gaussian_sigma = marker.sigma,
|
gaussian_sigma = marker.sigma,
|
||||||
tint = marker.tint,
|
tint = marker.tint,
|
||||||
radii = marker.radii,
|
radii = marker.radii,
|
||||||
feather_px = marker.feather_px,
|
feather_ppx = marker.feather_ppx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ hellope_shapes :: proc() {
|
|||||||
outline_width = 2,
|
outline_width = 2,
|
||||||
origin = draw.center_of(rect),
|
origin = draw.center_of(rect),
|
||||||
rotation = spin_angle,
|
rotation = spin_angle,
|
||||||
feather_px = 1,
|
feather_ppx = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rounded rectangle rotating around its center
|
// Rounded rectangle rotating around its center
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ struct main0_in
|
|||||||
{
|
{
|
||||||
float2 p_local [[user(locn0)]];
|
float2 p_local [[user(locn0)]];
|
||||||
float4 f_color [[user(locn1)]];
|
float4 f_color [[user(locn1)]];
|
||||||
float2 f_half_size [[user(locn2), flat]];
|
float2 f_half_size_ppx [[user(locn2), flat]];
|
||||||
float4 f_radii [[user(locn3), flat]];
|
float4 f_radii_ppx [[user(locn3), flat]];
|
||||||
float f_half_feather [[user(locn4), flat]];
|
float f_half_feather_ppx [[user(locn4), flat]];
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline __attribute__((always_inline))
|
static inline __attribute__((always_inline))
|
||||||
@@ -96,16 +96,16 @@ fragment main0_out main0(main0_in in [[stage_in]], constant Uniforms& _108 [[buf
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
float2 param_1 = in.p_local;
|
float2 param_1 = in.p_local;
|
||||||
float2 param_2 = in.f_half_size;
|
float2 param_2 = in.f_half_size_ppx;
|
||||||
float4 param_3 = in.f_radii;
|
float4 param_3 = in.f_radii_ppx;
|
||||||
float d = sdRoundedBox(param_1, param_2, param_3);
|
float d = sdRoundedBox(param_1, param_2, param_3);
|
||||||
if (d > in.f_half_feather)
|
if (d > in.f_half_feather_ppx)
|
||||||
{
|
{
|
||||||
discard_fragment();
|
discard_fragment();
|
||||||
}
|
}
|
||||||
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
|
float grad_magnitude = fast::max(fwidth(d), 9.9999999747524270787835121154785e-07);
|
||||||
float d_n = d / grad_magnitude;
|
float d_n = d / grad_magnitude;
|
||||||
float h_n = in.f_half_feather / 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;
|
float2 uv_1 = (gl_FragCoord.xy * _108.inv_downsample_factor) * _108.inv_working_size;
|
||||||
float3 color_1 = blur_input_tex.sample(blur_input_texSmplr, uv_1).xyz;
|
float3 color_1 = blur_input_tex.sample(blur_input_texSmplr, uv_1).xyz;
|
||||||
float3 tinted = mix(color_1, color_1 * in.f_color.xyz, float3(in.f_color.w));
|
float3 tinted = mix(color_1, color_1 * in.f_color.xyz, float3(in.f_color.w));
|
||||||
|
|||||||
Binary file not shown.
@@ -55,18 +55,18 @@ struct Uniforms
|
|||||||
struct Gaussian_Blur_Primitive
|
struct Gaussian_Blur_Primitive
|
||||||
{
|
{
|
||||||
float4 bounds;
|
float4 bounds;
|
||||||
float4 radii;
|
float4 radii_ppx;
|
||||||
float2 half_size;
|
float2 half_size_ppx;
|
||||||
float half_feather;
|
float half_feather_ppx;
|
||||||
uint color;
|
uint color;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Gaussian_Blur_Primitive_1
|
struct Gaussian_Blur_Primitive_1
|
||||||
{
|
{
|
||||||
float4 bounds;
|
float4 bounds;
|
||||||
float4 radii;
|
float4 radii_ppx;
|
||||||
float2 half_size;
|
float2 half_size_ppx;
|
||||||
float half_feather;
|
float half_feather_ppx;
|
||||||
uint color;
|
uint color;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,9 +81,9 @@ struct main0_out
|
|||||||
{
|
{
|
||||||
float2 p_local [[user(locn0)]];
|
float2 p_local [[user(locn0)]];
|
||||||
float4 f_color [[user(locn1)]];
|
float4 f_color [[user(locn1)]];
|
||||||
float2 f_half_size [[user(locn2)]];
|
float2 f_half_size_ppx [[user(locn2)]];
|
||||||
float4 f_radii [[user(locn3)]];
|
float4 f_radii_ppx [[user(locn3)]];
|
||||||
float f_half_feather [[user(locn4)]];
|
float f_half_feather_ppx [[user(locn4)]];
|
||||||
float4 gl_Position [[position]];
|
float4 gl_Position [[position]];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,26 +96,26 @@ vertex main0_out main0(constant Uniforms& _13 [[buffer(0)]], const device Gaussi
|
|||||||
out.gl_Position = float4(ndc, 0.0, 1.0);
|
out.gl_Position = float4(ndc, 0.0, 1.0);
|
||||||
out.p_local = float2(0.0);
|
out.p_local = float2(0.0);
|
||||||
out.f_color = float4(0.0);
|
out.f_color = float4(0.0);
|
||||||
out.f_half_size = float2(0.0);
|
out.f_half_size_ppx = float2(0.0);
|
||||||
out.f_radii = float4(0.0);
|
out.f_radii_ppx = float4(0.0);
|
||||||
out.f_half_feather = 0.0;
|
out.f_half_feather_ppx = 0.0;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Gaussian_Blur_Primitive p;
|
Gaussian_Blur_Primitive p;
|
||||||
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
|
p.bounds = _69.primitives[int(gl_InstanceIndex)].bounds;
|
||||||
p.radii = _69.primitives[int(gl_InstanceIndex)].radii;
|
p.radii_ppx = _69.primitives[int(gl_InstanceIndex)].radii_ppx;
|
||||||
p.half_size = _69.primitives[int(gl_InstanceIndex)].half_size;
|
p.half_size_ppx = _69.primitives[int(gl_InstanceIndex)].half_size_ppx;
|
||||||
p.half_feather = _69.primitives[int(gl_InstanceIndex)].half_feather;
|
p.half_feather_ppx = _69.primitives[int(gl_InstanceIndex)].half_feather_ppx;
|
||||||
p.color = _69.primitives[int(gl_InstanceIndex)].color;
|
p.color = _69.primitives[int(gl_InstanceIndex)].color;
|
||||||
float2 corner = _97[int(gl_VertexIndex)];
|
float2 corner = _97[int(gl_VertexIndex)];
|
||||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||||
out.p_local = (world_pos - center) * _13.dpi_scale;
|
out.p_local = (world_pos - center) * _13.dpi_scale;
|
||||||
out.f_color = unpack_unorm4x8_to_float(p.color);
|
out.f_color = unpack_unorm4x8_to_float(p.color);
|
||||||
out.f_half_size = p.half_size;
|
out.f_half_size_ppx = p.half_size_ppx;
|
||||||
out.f_radii = p.radii;
|
out.f_radii_ppx = p.radii_ppx;
|
||||||
out.f_half_feather = p.half_feather;
|
out.f_half_feather_ppx = p.half_feather_ppx;
|
||||||
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
|
out.gl_Position = _13.projection * float4(world_pos * _13.dpi_scale, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
Binary file not shown.
@@ -107,57 +107,57 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
}
|
}
|
||||||
float d = 1000000015047466219876688855040.0;
|
float d = 1000000015047466219876688855040.0;
|
||||||
float h = 0.5;
|
float h = 0.5;
|
||||||
float2 half_size = in.f_params.xy;
|
float2 half_size_ppx = in.f_params.xy;
|
||||||
float2 p_local = in.f_local_or_uv;
|
float2 p_local_ppx = in.f_local_or_uv;
|
||||||
if (kind == 1u)
|
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;
|
h = in.f_params2.z;
|
||||||
float2 param = p_local;
|
float2 param = p_local_ppx;
|
||||||
float2 param_1 = half_size;
|
float2 param_1 = half_size_ppx;
|
||||||
float4 param_2 = corner_radii;
|
float4 param_2 = corner_radii_ppx;
|
||||||
d = sdRoundedBox(param, param_1, param_2);
|
d = sdRoundedBox(param, param_1, param_2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (kind == 2u)
|
if (kind == 2u)
|
||||||
{
|
{
|
||||||
float radius = in.f_params.x;
|
float radius_ppx = in.f_params.x;
|
||||||
float sides = in.f_params.y;
|
float sides = in.f_params.y;
|
||||||
h = in.f_params.z;
|
h = in.f_params.z;
|
||||||
float2 param_3 = p_local;
|
float2 param_3 = p_local_ppx;
|
||||||
float param_4 = radius;
|
float param_4 = radius_ppx;
|
||||||
float param_5 = sides;
|
float param_5 = sides;
|
||||||
d = sdRegularPolygon(param_3, param_4, param_5);
|
d = sdRegularPolygon(param_3, param_4, param_5);
|
||||||
half_size = float2(radius);
|
half_size_ppx = float2(radius_ppx);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (kind == 3u)
|
if (kind == 3u)
|
||||||
{
|
{
|
||||||
float2 ab = in.f_params.xy;
|
float2 radii_ppx = in.f_params.xy;
|
||||||
h = in.f_params.z;
|
h = in.f_params.z;
|
||||||
float2 param_6 = p_local;
|
float2 param_6 = p_local_ppx;
|
||||||
float2 param_7 = ab;
|
float2 param_7 = radii_ppx;
|
||||||
d = sdEllipseApprox(param_6, param_7);
|
d = sdEllipseApprox(param_6, param_7);
|
||||||
half_size = ab;
|
half_size_ppx = radii_ppx;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (kind == 4u)
|
if (kind == 4u)
|
||||||
{
|
{
|
||||||
float inner = in.f_params.x;
|
float inner_radius_ppx = in.f_params.x;
|
||||||
float outer = in.f_params.y;
|
float outer_radius_ppx = in.f_params.y;
|
||||||
float2 n_start = in.f_params.zw;
|
float2 n_start = in.f_params.zw;
|
||||||
float2 n_end = in.f_params2.xy;
|
float2 n_end = in.f_params2.xy;
|
||||||
uint arc_bits = (flags >> 5u) & 3u;
|
uint arc_bits = (flags >> 5u) & 3u;
|
||||||
h = in.f_params2.z;
|
h = in.f_params2.z;
|
||||||
float r = length(p_local);
|
float r = length(p_local_ppx);
|
||||||
d = fast::max(inner - r, r - outer);
|
d = fast::max(inner_radius_ppx - r, r - outer_radius_ppx);
|
||||||
if (arc_bits != 0u)
|
if (arc_bits != 0u)
|
||||||
{
|
{
|
||||||
float d_start = dot(p_local, n_start);
|
float d_start = dot(p_local_ppx, n_start);
|
||||||
float d_end = dot(p_local, n_end);
|
float d_end = dot(p_local_ppx, n_end);
|
||||||
float _338;
|
float _338;
|
||||||
if (arc_bits == 1u)
|
if (arc_bits == 1u)
|
||||||
{
|
{
|
||||||
@@ -170,7 +170,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
float d_wedge = _338;
|
float d_wedge = _338;
|
||||||
d = fast::max(d, d_wedge);
|
d = fast::max(d, d_wedge);
|
||||||
}
|
}
|
||||||
half_size = 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);
|
float4 gradient_end = unpack_unorm4x8_to_float(in.f_effects.x);
|
||||||
if ((flags & 4u) != 0u)
|
if ((flags & 4u) != 0u)
|
||||||
{
|
{
|
||||||
float t_1 = length(p_local / half_size);
|
float t_1 = length(p_local_ppx / half_size_ppx);
|
||||||
float4 param_8 = gradient_start;
|
float4 param_8 = gradient_start;
|
||||||
float4 param_9 = gradient_end;
|
float4 param_9 = gradient_end;
|
||||||
float param_10 = t_1;
|
float param_10 = t_1;
|
||||||
@@ -194,7 +194,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
float2 direction = float2(as_type<half2>(in.f_effects.z));
|
float2 direction = float2(as_type<half2>(in.f_effects.z));
|
||||||
float t_2 = (dot(p_local / 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_11 = gradient_start;
|
||||||
float4 param_12 = gradient_end;
|
float4 param_12 = gradient_end;
|
||||||
float param_13 = t_2;
|
float param_13 = t_2;
|
||||||
@@ -206,7 +206,7 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
|
|||||||
if ((flags & 1u) != 0u)
|
if ((flags & 1u) != 0u)
|
||||||
{
|
{
|
||||||
float4 uv_rect = in.f_uv_rect;
|
float4 uv_rect = in.f_uv_rect;
|
||||||
float2 local_uv = ((p_local / 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);
|
float2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||||
shape_color = in.f_color * tex.sample(texSmplr, uv);
|
shape_color = in.f_color * tex.sample(texSmplr, uv);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -60,32 +60,21 @@ struct main0_in
|
|||||||
float4 v_color [[attribute(2)]];
|
float4 v_color [[attribute(2)]];
|
||||||
};
|
};
|
||||||
|
|
||||||
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Core_2D_Primitives& _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 = {};
|
main0_out out = {};
|
||||||
if (_12.mode == 0u)
|
if (_12.mode == 1u)
|
||||||
{
|
|
||||||
out.f_color = in.v_color;
|
|
||||||
out.f_local_or_uv = in.v_uv;
|
|
||||||
out.f_params = float4(0.0);
|
|
||||||
out.f_params2 = float4(0.0);
|
|
||||||
out.f_flags = 0u;
|
|
||||||
out.f_uv_rect = float4(0.0);
|
|
||||||
out.f_effects = uint4(0u);
|
|
||||||
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Core_2D_Primitive p;
|
Core_2D_Primitive p;
|
||||||
p.bounds = _75.primitives[int(gl_InstanceIndex)].bounds;
|
p.bounds = _31.primitives[int(gl_InstanceIndex)].bounds;
|
||||||
p.color = _75.primitives[int(gl_InstanceIndex)].color;
|
p.color = _31.primitives[int(gl_InstanceIndex)].color;
|
||||||
p.flags = _75.primitives[int(gl_InstanceIndex)].flags;
|
p.flags = _31.primitives[int(gl_InstanceIndex)].flags;
|
||||||
p.rotation_sc = _75.primitives[int(gl_InstanceIndex)].rotation_sc;
|
p.rotation_sc = _31.primitives[int(gl_InstanceIndex)].rotation_sc;
|
||||||
p._pad = _75.primitives[int(gl_InstanceIndex)]._pad;
|
p._pad = _31.primitives[int(gl_InstanceIndex)]._pad;
|
||||||
p.params = _75.primitives[int(gl_InstanceIndex)].params;
|
p.params = _31.primitives[int(gl_InstanceIndex)].params;
|
||||||
p.params2 = _75.primitives[int(gl_InstanceIndex)].params2;
|
p.params2 = _31.primitives[int(gl_InstanceIndex)].params2;
|
||||||
p.uv_rect = _75.primitives[int(gl_InstanceIndex)].uv_rect;
|
p.uv_rect = _31.primitives[int(gl_InstanceIndex)].uv_rect;
|
||||||
p.effects = _75.primitives[int(gl_InstanceIndex)].effects;
|
p.effects = _31.primitives[int(gl_InstanceIndex)].effects;
|
||||||
float2 corner = in.v_position;
|
float2 corner = in.v_position;
|
||||||
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
|
||||||
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
|
||||||
@@ -105,6 +94,27 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
|
|||||||
out.f_effects = p.effects;
|
out.f_effects = p.effects;
|
||||||
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.f_color = in.v_color;
|
||||||
|
out.f_local_or_uv = in.v_uv;
|
||||||
|
out.f_params = float4(0.0);
|
||||||
|
out.f_params2 = float4(0.0);
|
||||||
|
out.f_flags = 0u;
|
||||||
|
out.f_uv_rect = float4(0.0);
|
||||||
|
out.f_effects = uint4(0u);
|
||||||
|
float2 _199;
|
||||||
|
if (_12.mode == 2u)
|
||||||
|
{
|
||||||
|
_199 = in.v_position;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_199 = in.v_position * _12.dpi_scale;
|
||||||
|
}
|
||||||
|
float2 pos = _199;
|
||||||
|
out.gl_Position = _12.projection * float4(pos, 0.0, 1.0);
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -40,9 +40,9 @@ const uint MAX_KERNEL_PAIRS = 32;
|
|||||||
// --- Inputs from vertex shader ---
|
// --- Inputs from vertex shader ---
|
||||||
layout(location = 0) in vec2 p_local;
|
layout(location = 0) in vec2 p_local;
|
||||||
layout(location = 1) in mediump vec4 f_color;
|
layout(location = 1) in mediump vec4 f_color;
|
||||||
layout(location = 2) flat in vec2 f_half_size;
|
layout(location = 2) flat in vec2 f_half_size_ppx;
|
||||||
layout(location = 3) flat in vec4 f_radii;
|
layout(location = 3) flat in vec4 f_radii_ppx;
|
||||||
layout(location = 4) flat in float f_half_feather;
|
layout(location = 4) flat in float f_half_feather_ppx;
|
||||||
|
|
||||||
// --- Output ---
|
// --- Output ---
|
||||||
layout(location = 0) out vec4 out_color;
|
layout(location = 0) out vec4 out_color;
|
||||||
@@ -123,15 +123,15 @@ void main() {
|
|||||||
|
|
||||||
// ---- Mode 1: composite per-primitive.
|
// ---- Mode 1: composite per-primitive.
|
||||||
// RRect SDF — early discard for fragments well outside the masked region.
|
// RRect SDF — early discard for fragments well outside the masked region.
|
||||||
float d = sdRoundedBox(p_local, f_half_size, f_radii);
|
float d = sdRoundedBox(p_local, f_half_size_ppx, f_radii_ppx);
|
||||||
if (d > f_half_feather) {
|
if (d > f_half_feather_ppx) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fwidth-based normalization for AA (matches main pipeline approach).
|
// fwidth-based normalization for AA (matches main pipeline approach).
|
||||||
float grad_magnitude = max(fwidth(d), 1e-6);
|
float grad_magnitude = max(fwidth(d), 1e-6);
|
||||||
float d_n = d / grad_magnitude;
|
float d_n = d / grad_magnitude;
|
||||||
float h_n = f_half_feather / 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
|
// Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to
|
||||||
// working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
|
// working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
layout(location = 0) out vec2 p_local;
|
layout(location = 0) out vec2 p_local;
|
||||||
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
|
// f_color: tint, unpacked from primitive.color. Only meaningful in mode 1.
|
||||||
layout(location = 1) out mediump vec4 f_color;
|
layout(location = 1) out mediump vec4 f_color;
|
||||||
// f_half_size: RRect half extents in physical pixels (mode 1 only).
|
// f_half_size_ppx: RRect half extents in physical pixels (mode 1 only).
|
||||||
layout(location = 2) flat out vec2 f_half_size;
|
layout(location = 2) flat out vec2 f_half_size_ppx;
|
||||||
// f_radii: per-corner radii in physical pixels (mode 1 only).
|
// f_radii_ppx: per-corner radii in physical pixels (mode 1 only).
|
||||||
layout(location = 3) flat out vec4 f_radii;
|
layout(location = 3) flat out vec4 f_radii_ppx;
|
||||||
// f_half_feather: SDF anti-aliasing feather (mode 1 only).
|
// f_half_feather_ppx: SDF anti-aliasing feather in physical pixels (mode 1 only).
|
||||||
layout(location = 4) flat out float f_half_feather;
|
layout(location = 4) flat out float f_half_feather_ppx;
|
||||||
|
|
||||||
// --- Uniforms (set 1) ---
|
// --- Uniforms (set 1) ---
|
||||||
// Backdrop pipeline's own uniform block — distinct from the main pipeline's
|
// Backdrop pipeline's own uniform block — distinct from the main pipeline's
|
||||||
@@ -53,10 +53,10 @@ layout(set = 1, binding = 0) uniform Uniforms {
|
|||||||
// edge effects (e.g. liquid-glass-style refraction outlines) would be a dedicated
|
// edge effects (e.g. liquid-glass-style refraction outlines) would be a dedicated
|
||||||
// primitive type with its own pipeline rather than a flag bit here.
|
// primitive type with its own pipeline rather than a flag bit here.
|
||||||
struct Gaussian_Blur_Primitive {
|
struct Gaussian_Blur_Primitive {
|
||||||
vec4 bounds; // 0-15: min_xy, max_xy (world-space)
|
vec4 bounds; // 0-15: min_xy, max_xy (world-space, logical px)
|
||||||
vec4 radii; // 16-31: per-corner radii (physical px)
|
vec4 radii_ppx; // 16-31: per-corner radii
|
||||||
vec2 half_size; // 32-39: RRect half extents (physical px)
|
vec2 half_size_ppx; // 32-39: RRect half extents
|
||||||
float half_feather; // 40-43: SDF anti-aliasing feather (physical px)
|
float half_feather_ppx; // 40-43: SDF anti-aliasing feather
|
||||||
uint color; // 44-47: tint, packed RGBA u8x4
|
uint color; // 44-47: tint, packed RGBA u8x4
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,9 +78,9 @@ void main() {
|
|||||||
// Mode 0 doesn't read the per-primitive varyings; zero-init for safety.
|
// Mode 0 doesn't read the per-primitive varyings; zero-init for safety.
|
||||||
p_local = vec2(0.0);
|
p_local = vec2(0.0);
|
||||||
f_color = vec4(0.0);
|
f_color = vec4(0.0);
|
||||||
f_half_size = vec2(0.0);
|
f_half_size_ppx = vec2(0.0);
|
||||||
f_radii = vec4(0.0);
|
f_radii_ppx = vec4(0.0);
|
||||||
f_half_feather = 0.0;
|
f_half_feather_ppx = 0.0;
|
||||||
} else {
|
} else {
|
||||||
// ---- Mode 1: V-composite instanced unit-quad over Gaussian_Blur_Primitive ----
|
// ---- Mode 1: V-composite instanced unit-quad over Gaussian_Blur_Primitive ----
|
||||||
Gaussian_Blur_Primitive p = primitives[gl_InstanceIndex];
|
Gaussian_Blur_Primitive p = primitives[gl_InstanceIndex];
|
||||||
@@ -101,9 +101,9 @@ void main() {
|
|||||||
p_local = (world_pos - center) * dpi_scale;
|
p_local = (world_pos - center) * dpi_scale;
|
||||||
|
|
||||||
f_color = unpackUnorm4x8(p.color);
|
f_color = unpackUnorm4x8(p.color);
|
||||||
f_half_size = p.half_size;
|
f_half_size_ppx = p.half_size_ppx;
|
||||||
f_radii = p.radii;
|
f_radii_ppx = p.radii_ppx;
|
||||||
f_half_feather = p.half_feather;
|
f_half_feather_ppx = p.half_feather_ppx;
|
||||||
|
|
||||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
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;
|
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).
|
// Produces a symmetric transition centered on d=0: smoothstep(-h, h, d).
|
||||||
float sdf_alpha(float d, float h) {
|
float sdf_alpha(float d, float h) {
|
||||||
return 1.0 - smoothstep(-h, h, d);
|
return 1.0 - smoothstep(-h, h, d);
|
||||||
@@ -80,56 +80,56 @@ void main() {
|
|||||||
|
|
||||||
// SDF path — dispatch on kind
|
// SDF path — dispatch on kind
|
||||||
float d = 1e30;
|
float d = 1e30;
|
||||||
float h = 0.5; // half-feather width; overwritten per shape kind
|
float h = 0.5; // half-feather width (physical px); overwritten per shape kind
|
||||||
vec2 half_size = f_params.xy; // used by RRect and as reference size for gradients
|
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) {
|
if (kind == 1u) {
|
||||||
// RRect — half_feather in params2.z
|
// RRect — half_feather_ppx in params2.z
|
||||||
vec4 corner_radii = vec4(f_params.zw, f_params2.xy);
|
vec4 corner_radii_ppx = vec4(f_params.zw, f_params2.xy);
|
||||||
h = f_params2.z;
|
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) {
|
else if (kind == 2u) {
|
||||||
// NGon — half_feather in params.z
|
// NGon — half_feather_ppx in params.z
|
||||||
float radius = f_params.x;
|
float radius_ppx = f_params.x;
|
||||||
float sides = f_params.y;
|
float sides = f_params.y;
|
||||||
h = f_params.z;
|
h = f_params.z;
|
||||||
d = sdRegularPolygon(p_local, radius, sides);
|
d = sdRegularPolygon(p_local_ppx, radius_ppx, sides);
|
||||||
half_size = vec2(radius); // for gradient UV computation
|
half_size_ppx = vec2(radius_ppx); // for gradient UV computation
|
||||||
}
|
}
|
||||||
else if (kind == 3u) {
|
else if (kind == 3u) {
|
||||||
// Ellipse — half_feather in params.z
|
// Ellipse — half_feather_ppx in params.z
|
||||||
vec2 ab = f_params.xy;
|
vec2 radii_ppx = f_params.xy;
|
||||||
h = f_params.z;
|
h = f_params.z;
|
||||||
d = sdEllipseApprox(p_local, ab);
|
d = sdEllipseApprox(p_local_ppx, radii_ppx);
|
||||||
half_size = ab; // for gradient UV computation
|
half_size_ppx = radii_ppx; // for gradient UV computation
|
||||||
}
|
}
|
||||||
else if (kind == 4u) {
|
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 (>π)
|
// Arc mode from flag bits 5-6: 0 = full, 1 = narrow (≤π), 2 = wide (>π)
|
||||||
float inner = f_params.x;
|
float inner_radius_ppx = f_params.x;
|
||||||
float outer = f_params.y;
|
float outer_radius_ppx = f_params.y;
|
||||||
vec2 n_start = f_params.zw;
|
vec2 n_start = f_params.zw;
|
||||||
vec2 n_end = f_params2.xy;
|
vec2 n_end = f_params2.xy;
|
||||||
uint arc_bits = (flags >> 5u) & 3u;
|
uint arc_bits = (flags >> 5u) & 3u;
|
||||||
|
|
||||||
h = f_params2.z;
|
h = f_params2.z;
|
||||||
|
|
||||||
float r = length(p_local);
|
float r = length(p_local_ppx);
|
||||||
d = max(inner - r, r - outer);
|
d = max(inner_radius_ppx - r, r - outer_radius_ppx);
|
||||||
|
|
||||||
if (arc_bits != 0u) {
|
if (arc_bits != 0u) {
|
||||||
float d_start = dot(p_local, n_start);
|
float d_start = dot(p_local_ppx, n_start);
|
||||||
float d_end = dot(p_local, n_end);
|
float d_end = dot(p_local_ppx, n_end);
|
||||||
float d_wedge = (arc_bits == 1u)
|
float d_wedge = (arc_bits == 1u)
|
||||||
? max(d_start, d_end) // arc ≤ π: intersect half-planes
|
? max(d_start, d_end) // arc ≤ π: intersect half-planes
|
||||||
: min(d_start, d_end); // arc > π: union half-planes
|
: min(d_start, d_end); // arc > π: union half-planes
|
||||||
d = max(d, d_wedge);
|
d = max(d, d_wedge);
|
||||||
}
|
}
|
||||||
|
|
||||||
half_size = 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 ---
|
// --- fwidth-based normalization for correct AA and stroke width ---
|
||||||
@@ -146,18 +146,18 @@ void main() {
|
|||||||
|
|
||||||
if ((flags & 4u) != 0u) {
|
if ((flags & 4u) != 0u) {
|
||||||
// Radial gradient (bit 2): t from distance to center
|
// Radial gradient (bit 2): t from distance to center
|
||||||
mediump float t = length(p_local / half_size);
|
mediump float t = length(p_local_ppx / half_size_ppx);
|
||||||
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||||
} else {
|
} else {
|
||||||
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
|
// Linear gradient: direction pre-computed on CPU as (cos, sin) f16 pair
|
||||||
vec2 direction = unpackHalf2x16(f_effects.z);
|
vec2 direction = unpackHalf2x16(f_effects.z);
|
||||||
mediump float t = dot(p_local / 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);
|
shape_color = gradient_2color(gradient_start, gradient_end, t);
|
||||||
}
|
}
|
||||||
} else if ((flags & 1u) != 0u) {
|
} else if ((flags & 1u) != 0u) {
|
||||||
// Textured (bit 0)
|
// Textured (bit 0)
|
||||||
vec4 uv_rect = f_uv_rect;
|
vec4 uv_rect = f_uv_rect;
|
||||||
vec2 local_uv = p_local / 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);
|
vec2 uv = mix(uv_rect.xy, uv_rect.zw, local_uv);
|
||||||
shape_color = f_color * texture(tex, uv);
|
shape_color = f_color * texture(tex, uv);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#version 450 core
|
#version 450 core
|
||||||
|
|
||||||
// ---------- Vertex attributes (used in both modes) ----------
|
// ---------- Vertex attributes (used in all modes) ----------
|
||||||
layout(location = 0) in vec2 v_position;
|
layout(location = 0) in vec2 v_position;
|
||||||
layout(location = 1) in vec2 v_uv;
|
layout(location = 1) in vec2 v_uv;
|
||||||
layout(location = 2) in vec4 v_color;
|
layout(location = 2) in vec4 v_color;
|
||||||
@@ -16,10 +16,18 @@ layout(location = 6) flat out vec4 f_uv_rect;
|
|||||||
layout(location = 7) flat out uvec4 f_effects;
|
layout(location = 7) flat out uvec4 f_effects;
|
||||||
|
|
||||||
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
|
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
|
||||||
|
// Mode values mirror Core_2D_Mode in core_2d.odin:
|
||||||
|
// 0 = Tessellated v_position is in logical pixels; shader scales by dpi_scale.
|
||||||
|
// 1 = SDF v_position is a unit-quad corner; world-space comes from
|
||||||
|
// primitives[gl_InstanceIndex].bounds (logical px). Shader
|
||||||
|
// scales by dpi_scale.
|
||||||
|
// 2 = Text v_position is in *physical* pixels already (the CPU baked
|
||||||
|
// the anchor snap and SDL_ttf glyph offsets, both physical).
|
||||||
|
// Shader must NOT rescale.
|
||||||
layout(set = 1, binding = 0) uniform Uniforms {
|
layout(set = 1, binding = 0) uniform Uniforms {
|
||||||
mat4 projection;
|
mat4 projection;
|
||||||
float dpi_scale;
|
float dpi_scale;
|
||||||
uint mode; // 0 = tessellated, 1 = SDF
|
uint mode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- SDF primitive storage buffer ----------
|
// ---------- SDF primitive storage buffer ----------
|
||||||
@@ -44,18 +52,7 @@ layout(std430, set = 0, binding = 0) readonly buffer Core_2D_Primitives {
|
|||||||
|
|
||||||
// ---------- Entry point ----------
|
// ---------- Entry point ----------
|
||||||
void main() {
|
void main() {
|
||||||
if (mode == 0u) {
|
if (mode == 1u) {
|
||||||
// ---- Mode 0: Tessellated (used for text and arbitrary user geometry) ----
|
|
||||||
f_color = v_color;
|
|
||||||
f_local_or_uv = v_uv;
|
|
||||||
f_params = vec4(0.0);
|
|
||||||
f_params2 = vec4(0.0);
|
|
||||||
f_flags = 0u;
|
|
||||||
f_uv_rect = vec4(0.0);
|
|
||||||
f_effects = uvec4(0);
|
|
||||||
|
|
||||||
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
|
|
||||||
} else {
|
|
||||||
// ---- Mode 1: SDF instanced quads ----
|
// ---- Mode 1: SDF instanced quads ----
|
||||||
Core_2D_Primitive p = primitives[gl_InstanceIndex];
|
Core_2D_Primitive p = primitives[gl_InstanceIndex];
|
||||||
|
|
||||||
@@ -86,5 +83,25 @@ void main() {
|
|||||||
f_effects = p.effects;
|
f_effects = p.effects;
|
||||||
|
|
||||||
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
// ---- Mode 0 (Tessellated) and Mode 2 (Text) ----
|
||||||
|
// Both feed the raw-vertex pipeline (kind 0 in the fragment shader).
|
||||||
|
// They differ only in what coord space `v_position` is in:
|
||||||
|
// Mode 0 — logical pixels, scale here by dpi_scale.
|
||||||
|
// Mode 2 — physical pixels (CPU pre-scaled and snapped to integer
|
||||||
|
// physical pixels for atlas-aligned bilinear sampling).
|
||||||
|
// Do NOT rescale.
|
||||||
|
// `mode` is uniform across the workgroup, so the select compiles to a
|
||||||
|
// uniform-controlled branch with no SIMT divergence cost.
|
||||||
|
f_color = v_color;
|
||||||
|
f_local_or_uv = v_uv;
|
||||||
|
f_params = vec4(0.0);
|
||||||
|
f_params2 = vec4(0.0);
|
||||||
|
f_flags = 0u;
|
||||||
|
f_uv_rect = vec4(0.0);
|
||||||
|
f_effects = uvec4(0);
|
||||||
|
|
||||||
|
vec2 pos = (mode == 2u) ? v_position : (v_position * dpi_scale);
|
||||||
|
gl_Position = projection * vec4(pos, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-10
@@ -21,8 +21,8 @@ auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
|
|||||||
|
|
||||||
// ----- Internal helpers -----
|
// ----- Internal helpers -----
|
||||||
|
|
||||||
// Color is premultiplied: the tessellated fragment shader passes it through directly
|
// Premultiplies the color before storing it on the vertex (see draw package doc's
|
||||||
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
// "Color and blending" section for why).
|
||||||
//INTERNAL
|
//INTERNAL
|
||||||
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex_2D {
|
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex_2D {
|
||||||
return draw.Vertex_2D{position = position, color = draw.premultiply_color(color)}
|
return draw.Vertex_2D{position = position, color = draw.premultiply_color(color)}
|
||||||
@@ -108,16 +108,23 @@ triangle :: proc(
|
|||||||
draw.prepare_shape(layer, vertices[:])
|
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).
|
// 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.
|
// The rasterizer linearly interpolates between them, producing a smooth ~1-physical-pixel AA band.
|
||||||
// `aa_px` controls the extrusion width in logical pixels (default 1.0).
|
// `aa_ppx` controls the extrusion width in *physical* pixels (default 1.0). The CPU divides by
|
||||||
// This proc emits 21 vertices (3 interior + 6 edge quads × 3 verts each).
|
// `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(
|
triangle_aa :: proc(
|
||||||
layer: ^draw.Layer,
|
layer: ^draw.Layer,
|
||||||
v1, v2, v3: draw.Vec2,
|
v1, v2, v3: draw.Vec2,
|
||||||
color: draw.Color,
|
color: draw.Color,
|
||||||
aa_px: f32 = draw.DFT_FEATHER_PX,
|
aa_ppx: f32 = draw.DFT_FEATHER_PPX,
|
||||||
origin: draw.Vec2 = {},
|
origin: draw.Vec2 = {},
|
||||||
rotation: f32 = 0,
|
rotation: f32 = 0,
|
||||||
) {
|
) {
|
||||||
@@ -164,7 +171,9 @@ triangle_aa :: proc(
|
|||||||
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
|
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
|
||||||
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
|
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
|
||||||
|
|
||||||
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 fringe vertices: each edge vertex extruded outward
|
||||||
outer_0_01 := p0 + normal_01 * extrude_distance
|
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.
|
// Outer fringe is BLANK = {0,0,0,0} which is already premul.
|
||||||
transparent := draw.BLANK
|
transparent := draw.BLANK
|
||||||
|
|
||||||
// 3 interior + 6 × 3 edge-quad = 21 vertices
|
// 3 interior + 6 edge-quad tris (×3 verts) + 3 corner-fan tris (×3 verts) = 30 vertices
|
||||||
vertices: [21]draw.Vertex_2D
|
vertices: [30]draw.Vertex_2D
|
||||||
|
|
||||||
// Interior triangle
|
// Interior triangle
|
||||||
vertices[0] = solid_vertex(p0, color)
|
vertices[0] = solid_vertex(p0, color)
|
||||||
@@ -210,6 +219,27 @@ triangle_aa :: proc(
|
|||||||
vertices[19] = solid_vertex(outer_0_20, transparent)
|
vertices[19] = solid_vertex(outer_0_20, transparent)
|
||||||
vertices[20] = solid_vertex(outer_2_20, transparent)
|
vertices[20] = solid_vertex(outer_2_20, transparent)
|
||||||
|
|
||||||
|
// Corner fan caps: each fills the wedge gap between the two edge fringes meeting at a
|
||||||
|
// triangle vertex. Apex is full color; both fringe verts are BLANK, so the rasterizer
|
||||||
|
// produces a smooth alpha falloff across the wedge (matches the adjacent edge-band
|
||||||
|
// gradients at the shared edges, so the seams are invisible). Vertex order per fan:
|
||||||
|
// [apex, fringe-from-incoming-edge, fringe-from-outgoing-edge].
|
||||||
|
|
||||||
|
// Cap at p0 (between incoming edge p2→p0 and outgoing edge p0→p1)
|
||||||
|
vertices[21] = solid_vertex(p0, color)
|
||||||
|
vertices[22] = solid_vertex(outer_0_20, transparent)
|
||||||
|
vertices[23] = solid_vertex(outer_0_01, transparent)
|
||||||
|
|
||||||
|
// Cap at p1 (between incoming edge p0→p1 and outgoing edge p1→p2)
|
||||||
|
vertices[24] = solid_vertex(p1, color)
|
||||||
|
vertices[25] = solid_vertex(outer_1_01, transparent)
|
||||||
|
vertices[26] = solid_vertex(outer_1_12, transparent)
|
||||||
|
|
||||||
|
// Cap at p2 (between incoming edge p1→p2 and outgoing edge p2→p0)
|
||||||
|
vertices[27] = solid_vertex(p2, color)
|
||||||
|
vertices[28] = solid_vertex(outer_2_12, transparent)
|
||||||
|
vertices[29] = solid_vertex(outer_2_20, transparent)
|
||||||
|
|
||||||
draw.prepare_shape(layer, vertices[:])
|
draw.prepare_shape(layer, vertices[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user