Clay custom dispatch improvements & DPI scaling fixes (#26)

Co-authored-by: Zachary Levy <zachary@sunforge.is>
Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
2026-05-06 04:17:24 +00:00
parent e8ffa28de3
commit 43f08ed30c
19 changed files with 627 additions and 407 deletions
+100 -81
View File
@@ -9,11 +9,8 @@ import sdl_ttf "vendor:sdl3/ttf"
//----- Vertex layout ----------------------------------
// Vertex layout for tessellated and text geometry.
// IMPORTANT: `color` must be premultiplied alpha (RGB channels pre-scaled by alpha).
// The tessellated fragment shader passes vertex color through directly — it does NOT
// premultiply. The blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied-over).
// Use `premultiply_color` when constructing vertices manually for `prepare_shape`.
// Vertex layout for tessellated and text geometry. `color` must be premultiplied alpha; see
// the package doc's "Color and blending" section for the contract.
Vertex_2D :: struct {
position: Vec2,
uv: [2]f32,
@@ -68,35 +65,35 @@ Shape_Flags :: bit_set[Shape_Flag;u8]
//INTERNAL
RRect_Params :: struct {
half_size: [2]f32,
radii: [4]f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: f32,
half_size_ppx: [2]f32,
radii_ppx: [4]f32,
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
_: f32,
}
//INTERNAL
NGon_Params :: struct {
radius: f32,
sides: f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32,
radius_ppx: f32,
sides: f32,
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32,
}
//INTERNAL
Ellipse_Params :: struct {
radii: [2]f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32,
radii_ppx: [2]f32,
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32,
}
//INTERNAL
Ring_Arc_Params :: struct {
inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
outer_radius: f32, // outer radius in physical pixels
normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start))
normal_end: [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end))
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: f32,
inner_radius_ppx: f32, // 0 for pie slice
outer_radius_ppx: f32,
normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start))
normal_end: [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end))
half_feather_ppx: f32, // feather_ppx * 0.5; shader uses smoothstep(-h, h, d)
_: f32,
}
//INTERNAL
@@ -176,9 +173,7 @@ Core_2D :: struct {
sampler: ^sdl.GPUSampler,
}
// MSAA is not supported by levlib (see init's doc comment in draw.odin); the PSO is hard-wired
// to single-sample. SDF text and shapes provide analytical AA via smoothstep; tessellated user
// geometry is not anti-aliased.
// PSO is hard-wired to single-sample (no MSAA — see package doc's "Anti-aliasing" section).
//INTERNAL
create_core_2d :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> (core_2d: Core_2D, ok: bool) {
// On failure, clean up any partially-created resources
@@ -464,10 +459,31 @@ destroy_core_2d :: proc(device: ^sdl.GPUDevice, core: ^Core_2D) {
//----- Vertex uniforms ----------------------------------
//
// Coordinate-space contract for the main pipeline's vertex shader:
//
// Tessellated (0) — `v_position` arrives in *logical* pixels. The vertex
// shader multiplies by `dpi_scale` before applying the
// ortho projection (which is sized to physical pixels).
// SDF (1) — `v_position` is a unit-quad corner (0..1). World-space
// coordinates come from `Core_2D_Primitive.bounds` in
// logical pixels; the shader scales by `dpi_scale`.
// Text (2) — `v_position` arrives in *physical* pixels already.
// `prepare_text` and `prepare_text_transformed` bake the
// anchor + glyph offsets (from SDL_ttf's GPU text engine,
// which lays glyphs out in physical pixels) into the
// vertex stream and snap the anchor to integer physical
// pixels for atlas-aligned bilinear sampling. The shader
// therefore must NOT rescale these vertices.
//
// The two raw-vertex modes (Tessellated, Text) share `prepare_shape`-style
// glue but their coord spaces diverge — see `base_2d.vert` for the shader-
// side branch.
//INTERNAL
Core_2D_Mode :: enum u32 {
Tessellated = 0,
SDF = 1,
Text = 2,
}
//INTERNAL
@@ -814,9 +830,12 @@ render_layer_sub_batch_range :: proc(
sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)
case .Text:
if current_mode != .Tessellated {
push_globals(cmd_buffer, width, height, .Tessellated)
current_mode = .Tessellated
// Text vertices live in physical-pixel space (see Core_2D_Mode.Text
// docs); mode 2 makes the shader skip the `* dpi_scale` step that
// the Tessellated path applies to logical-pixel input.
if current_mode != .Text {
push_globals(cmd_buffer, width, height, .Text)
current_mode = .Text
}
if current_vert_buf != main_vert_buf {
sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
@@ -922,8 +941,8 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
// Snap base position to integer physical pixels to avoid atlas sub-pixel
// sampling blur (and the off-by-one bottom-row clip that comes with it).
base_x := math.round(text.position[0] * GLOB.dpi_scaling)
base_y := math.round(text.position[1] * GLOB.dpi_scaling)
base_x_ppx := math.round(text.position[0] * GLOB.dpi_scaling)
base_y_ppx := math.round(text.position[1] * GLOB.dpi_scaling)
// Premultiply text color once — reused across all glyph vertices.
pm_color := premultiply_color(text.color)
@@ -938,7 +957,7 @@ prepare_text :: proc(layer: ^Layer, text: Text) {
uv := data.uv[i]
append(
&GLOB.tmp_text_verts,
Vertex_2D{position = {pos.x + base_x, -pos.y + base_y}, uv = {uv.x, uv.y}, color = pm_color},
Vertex_2D{position = {pos.x + base_x_ppx, -pos.y + base_y_ppx}, uv = {uv.x, uv.y}, color = pm_color},
)
}
@@ -1079,7 +1098,7 @@ build_rrect_primitive :: proc(
radii: Rectangle_Radii,
origin: Vec2,
rotation: f32,
feather_px: f32,
feather_ppx: f32,
) -> Core_2D_Primitive {
max_radius := min(rect.width, rect.height) * 0.5
clamped_top_left := clamp(radii.top_left, 0, max_radius)
@@ -1087,8 +1106,8 @@ build_rrect_primitive :: proc(
clamped_bottom_right := clamp(radii.bottom_right, 0, max_radius)
clamped_bottom_left := clamp(radii.bottom_left, 0, max_radius)
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
half_feather_ppx := feather_ppx * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
half_width := rect.width * 0.5
@@ -1126,14 +1145,14 @@ build_rrect_primitive :: proc(
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
}
prim.params.rrect = RRect_Params {
half_size = {half_width * dpi_scale, half_height * dpi_scale},
radii = {
half_size_ppx = {half_width * dpi_scale, half_height * dpi_scale},
radii_ppx = {
clamped_bottom_right * dpi_scale,
clamped_top_right * dpi_scale,
clamped_bottom_left * dpi_scale,
clamped_top_left * dpi_scale,
},
half_feather = half_feather,
half_feather_ppx = half_feather_ppx,
}
return prim
}
@@ -1146,10 +1165,10 @@ build_circle_primitive :: proc(
radius: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
feather_ppx: f32,
) -> Core_2D_Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
half_feather_ppx := feather_ppx * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
@@ -1166,11 +1185,11 @@ build_circle_primitive :: proc(
actual_center.y + radius + padding,
},
}
scaled_radius := radius * dpi_scale
radius_ppx := radius * dpi_scale
prim.params.rrect = RRect_Params {
half_size = {scaled_radius, scaled_radius},
radii = {scaled_radius, scaled_radius, scaled_radius, scaled_radius},
half_feather = half_feather,
half_size_ppx = {radius_ppx, radius_ppx},
radii_ppx = {radius_ppx, radius_ppx, radius_ppx, radius_ppx},
half_feather_ppx = half_feather_ppx,
}
return prim
}
@@ -1183,10 +1202,10 @@ build_ellipse_primitive :: proc(
radius_horizontal, radius_vertical: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
feather_ppx: f32,
) -> Core_2D_Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
half_feather_ppx := feather_ppx * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
@@ -1218,8 +1237,8 @@ build_ellipse_primitive :: proc(
rotation_sc = has_rotation ? pack_rotation_sc(sin_angle, cos_angle) : 0,
}
prim.params.ellipse = Ellipse_Params {
radii = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
half_feather = half_feather,
radii_ppx = {radius_horizontal * dpi_scale, radius_vertical * dpi_scale},
half_feather_ppx = half_feather_ppx,
}
return prim
}
@@ -1233,10 +1252,10 @@ build_polygon_primitive :: proc(
radius: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
feather_ppx: f32,
) -> Core_2D_Primitive {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
half_feather_ppx := feather_ppx * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
@@ -1258,9 +1277,9 @@ build_polygon_primitive :: proc(
rotation_sc = rotation != 0 ? pack_rotation_sc(sin_rot, cos_rot) : 0,
}
prim.params.ngon = NGon_Params {
radius = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
sides = f32(sides),
half_feather = half_feather,
radius_ppx = radius * math.cos(math.PI / f32(sides)) * dpi_scale,
sides = f32(sides),
half_feather_ppx = half_feather_ppx,
}
return prim
}
@@ -1278,13 +1297,13 @@ build_ring_arc_primitive :: proc(
end_angle: f32,
origin: Vec2,
rotation: f32,
feather_px: f32,
feather_ppx: f32,
) -> (
Core_2D_Primitive,
Shape_Flags,
) {
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
half_feather_ppx := feather_ppx * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
actual_center := center
@@ -1327,11 +1346,11 @@ build_ring_arc_primitive :: proc(
},
}
prim.params.ring_arc = Ring_Arc_Params {
inner_radius = inner_radius * dpi_scale,
outer_radius = outer_radius * dpi_scale,
normal_start = normal_start,
normal_end = normal_end,
half_feather = half_feather,
inner_radius_ppx = inner_radius * dpi_scale,
outer_radius_ppx = outer_radius * dpi_scale,
normal_start = normal_start,
normal_end = normal_end,
half_feather_ppx = half_feather_ppx,
}
return prim, arc_flags
}
@@ -1422,9 +1441,9 @@ rectangle :: proc(
radii: Rectangle_Radii = {},
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
feather_ppx: f32 = DFT_FEATHER_PPX,
) {
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px)
prim := build_rrect_primitive(rect, radii, origin, rotation, feather_ppx)
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
}
@@ -1445,9 +1464,9 @@ circle :: proc(
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
feather_ppx: f32 = DFT_FEATHER_PPX,
) {
prim := build_circle_primitive(center, radius, origin, rotation, feather_px)
prim := build_circle_primitive(center, radius, origin, rotation, feather_ppx)
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
}
@@ -1462,9 +1481,9 @@ ellipse :: proc(
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
feather_ppx: f32 = DFT_FEATHER_PPX,
) {
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px)
prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_ppx)
apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width)
}
@@ -1481,11 +1500,11 @@ polygon :: proc(
outline_width: f32 = 0,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
feather_ppx: f32 = DFT_FEATHER_PPX,
) {
if sides < 3 do return
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px)
prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_ppx)
apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width)
}
@@ -1504,7 +1523,7 @@ ring :: proc(
end_angle: f32 = DFT_CIRC_END_ANGLE,
origin: Vec2 = {},
rotation: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
feather_ppx: f32 = DFT_FEATHER_PPX,
) {
prim, arc_flags := build_ring_arc_primitive(
center,
@@ -1514,7 +1533,7 @@ ring :: proc(
end_angle,
origin,
rotation,
feather_px,
feather_ppx,
)
apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags)
}
@@ -1528,7 +1547,7 @@ line :: proc(
thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {},
outline_width: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
feather_ppx: f32 = DFT_FEATHER_PPX,
) {
delta_x := end_position.x - start_position.x
delta_y := end_position.y - start_position.y
@@ -1544,8 +1563,8 @@ line :: proc(
half_thickness := thickness * 0.5
cap_radius := half_thickness
half_feather := feather_px * 0.5
padding := half_feather / GLOB.dpi_scaling
half_feather_ppx := feather_ppx * 0.5
padding := half_feather_ppx / GLOB.dpi_scaling
dpi_scale := GLOB.dpi_scaling
// Expand bounds for rotation
@@ -1561,14 +1580,14 @@ line :: proc(
rotation_sc = pack_rotation_sc(sin_angle, cos_angle),
}
prim.params.rrect = RRect_Params {
half_size = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
radii = {
half_size_ppx = {(half_length + cap_radius) * dpi_scale, half_thickness * dpi_scale},
radii_ppx = {
cap_radius * dpi_scale,
cap_radius * dpi_scale,
cap_radius * dpi_scale,
cap_radius * dpi_scale,
},
half_feather = half_feather,
half_feather_ppx = half_feather_ppx,
}
apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width)
}
@@ -1581,10 +1600,10 @@ line_strip :: proc(
thickness: f32 = DFT_STROKE_THICKNESS,
outline_color: Color = {},
outline_width: f32 = 0,
feather_px: f32 = DFT_FEATHER_PX,
feather_ppx: f32 = DFT_FEATHER_PPX,
) {
if len(points) < 2 do return
for i in 0 ..< len(points) - 1 {
line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_px)
line(layer, points[i], points[i + 1], brush, thickness, outline_color, outline_width, feather_ppx)
}
}