Major rendering updates

This commit is contained in:
Zachary Levy
2026-04-22 14:49:37 -07:00
parent 37da2ea068
commit e1938f1c42
15 changed files with 1770 additions and 1736 deletions

View File

@@ -5,8 +5,13 @@ import "core:log"
import "core:mem"
import sdl "vendor:sdl3"
// Vertex layout for tessellated and text geometry.
// IMPORTANT: `color` must be premultiplied alpha (RGB channels pre-scaled by alpha).
// The tessellated fragment shader passes vertex color through directly — it does NOT
// premultiply. The blend state is ONE, ONE_MINUS_SRC_ALPHA (premultiplied-over).
// Use `premultiply_color` when constructing vertices manually for `prepare_shape`.
Vertex :: struct {
position: [2]f32,
position: Vec2,
uv: [2]f32,
color: Color,
}
@@ -23,99 +28,127 @@ TextBatch :: struct {
// ----- SDF primitive types -----------
// ----------------------------------------------------------------------------------------------------------------
// The SDF path evaluates one of four signed distance functions per primitive, dispatched
// by Shape_Kind encoded in the low byte of Primitive.flags:
//
// RRect — rounded rectangle with per-corner radii (sdRoundedBox). Also covers circles
// (uniform radii = half-size), capsule-style line segments (rotated, max rounding),
// and other RRect-reducible shapes.
// NGon — regular polygon with N sides and optional rounding.
// Ellipse — approximate ellipse (non-exact SDF, suitable for UI but not for shape merging).
// Ring_Arc — annular ring with optional angular clipping. Covers full rings, partial arcs,
// pie slices (inner_radius = 0), and loading spinners.
Shape_Kind :: enum u8 {
Solid = 0,
Solid = 0, // tessellated path (mode marker; not a real SDF kind)
RRect = 1,
Circle = 2,
NGon = 2,
Ellipse = 3,
Segment = 4,
Ring_Arc = 5,
NGon = 6,
Ring_Arc = 4,
}
Shape_Flag :: enum u8 {
Stroke,
Textured,
Textured, // bit 0: sample texture using uv.uv_rect (mutually exclusive with Gradient)
Gradient, // bit 1: 2-color gradient using uv.effects.gradient_color as end/outer color
Gradient_Radial, // bit 2: if set with Gradient, radial from center; else linear at angle
Outline, // bit 3: outer outline band using uv.effects.outline_color; CPU expands bounds by outline_width
Rotated, // bit 4: shape has non-zero rotation; rotation_sc contains packed sin/cos
Arc_Narrow, // bit 5: ring arc span ≤ π — intersect half-planes. Neither Arc bit = full ring.
Arc_Wide, // bit 6: ring arc span > π — union half-planes. Neither Arc bit = full ring.
}
Shape_Flags :: bit_set[Shape_Flag;u8]
RRect_Params :: struct {
half_size: [2]f32,
radii: [4]f32,
soft_px: f32,
stroke_px: f32,
}
Circle_Params :: struct {
radius: f32,
soft_px: f32,
stroke_px: f32,
_: [5]f32,
}
Ellipse_Params :: struct {
radii: [2]f32,
soft_px: f32,
stroke_px: f32,
_: [4]f32,
}
Segment_Params :: struct {
a: [2]f32,
b: [2]f32,
width: f32,
soft_px: f32,
_: [2]f32,
}
Ring_Arc_Params :: struct {
inner_radius: f32,
outer_radius: f32,
start_rad: f32,
end_rad: f32,
soft_px: f32,
_: [3]f32,
half_size: [2]f32,
radii: [4]f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: f32,
}
NGon_Params :: struct {
radius: f32,
rotation: f32,
sides: f32,
soft_px: f32,
stroke_px: f32,
_: [3]f32,
radius: f32,
sides: f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32,
}
Ellipse_Params :: struct {
radii: [2]f32,
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: [5]f32,
}
Ring_Arc_Params :: struct {
inner_radius: f32, // inner radius in physical pixels (0 for pie slice)
outer_radius: f32, // outer radius in physical pixels
normal_start: [2]f32, // pre-computed outward normal of start edge: (sin(start), -cos(start))
normal_end: [2]f32, // pre-computed outward normal of end edge: (-sin(end), cos(end))
half_feather: f32, // feather_px * 0.5; shader uses smoothstep(-h, h, d)
_: f32,
}
Shape_Params :: struct #raw_union {
rrect: RRect_Params,
circle: Circle_Params,
ellipse: Ellipse_Params,
segment: Segment_Params,
ring_arc: Ring_Arc_Params,
ngon: NGon_Params,
ellipse: Ellipse_Params,
ring_arc: Ring_Arc_Params,
raw: [8]f32,
}
#assert(size_of(Shape_Params) == 32)
// GPU layout: 64 bytes, std430-compatible. The shader declares this as a storage buffer struct.
// GPU-side storage for 2-color gradient parameters and/or outline parameters.
// Packed into 16 bytes to alias with uv_rect in the Uv_Or_Effects raw union.
// The shader reads gradient_color and outline_color via unpackUnorm4x8.
// gradient_dir_sc stores the pre-computed gradient direction as (cos, sin) in f16 pair
// via unpackHalf2x16. outline_packed stores outline_width as f16 via unpackHalf2x16.
Gradient_Outline :: struct {
gradient_color: Color, // 0: end (linear) or outer (radial) gradient color
outline_color: Color, // 4: outline band color
gradient_dir_sc: u32, // 8: packed f16 pair: low = cos(angle), high = sin(angle) — pre-computed gradient direction
outline_packed: u32, // 12: packed f16 pair: low = outline_width (f16, physical pixels), high = reserved
}
#assert(size_of(Gradient_Outline) == 16)
// Uv_Or_Effects aliases the final 16 bytes of a Primitive. When .Textured is set,
// uv_rect holds texture-atlas coordinates. When .Gradient or .Outline is set,
// effects holds 2-color gradient parameters and/or outline parameters.
// Textured and Gradient are mutually exclusive; if both are set, Gradient takes precedence.
Uv_Or_Effects :: struct #raw_union {
uv_rect: [4]f32, // u_min, v_min, u_max, v_max (default {0,0,1,1})
effects: Gradient_Outline, // gradient + outline parameters
}
// GPU layout: 80 bytes, std430-compatible. The shader declares this as a storage buffer struct.
// The low byte of `flags` encodes the Shape_Kind (0 = tessellated, 1-4 = SDF kinds).
// Bits 8-15 encode Shape_Flags (Textured, Gradient, Gradient_Radial, Outline, Rotated, Arc_Narrow, Arc_Wide).
// rotation_sc stores pre-computed sin/cos of the rotation angle as a packed f16 pair,
// avoiding per-pixel trigonometry in the fragment shader. Only read when .Rotated is set.
Primitive :: struct {
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8
kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8)
rotation: f32, // 24: shader self-rotation in radians (used by RRect, Ellipse)
_pad: f32, // 28: alignment to vec4 boundary
params: Shape_Params, // 32: two vec4s of shape params
uv_rect: [4]f32, // 64: u_min, v_min, u_max, v_max (default {0,0,1,1})
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
color: Color, // 16: u8x4, fill color / gradient start color / texture tint
flags: u32, // 20: low byte = Shape_Kind, bits 8+ = Shape_Flags
rotation_sc: u32, // 24: packed f16 pair: low = sin(angle), high = cos(angle). Requires .Rotated flag.
_pad: f32, // 28: reserved for future use
params: Shape_Params, // 32: per-kind shape parameters (raw union, 32 bytes)
uv: Uv_Or_Effects, // 64: texture coords or gradient/outline parameters
}
#assert(size_of(Primitive) == 80)
// Pack shape kind and flags into the Primitive.flags field. The low byte encodes the Shape_Kind
// (which also serves as the SDF mode marker — kind > 0 means SDF path). The tessellated path
// leaves the field at 0 (Solid kind, set by vertex shader zero-initialization).
pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 {
return u32(kind) | (u32(transmute(u8)flags) << 8)
}
// Pack two f16 values into a single u32 for GPU consumption via unpackHalf2x16.
// Used to pack gradient_dir_sc (cos/sin) and outline_packed (width/reserved) in Gradient_Outline.
pack_f16_pair :: #force_inline proc(low, high: f16) -> u32 {
return u32(transmute(u16)low) | (u32(transmute(u16)high) << 16)
}
Pipeline_2D_Base :: struct {
sdl_pipeline: ^sdl.GPUGraphicsPipeline,
vertex_buffer: Buffer,
@@ -208,19 +241,23 @@ create_pipeline_2d_base :: proc(
target_info = sdl.GPUGraphicsPipelineTargetInfo {
color_target_descriptions = &sdl.GPUColorTargetDescription {
format = sdl.GetGPUSwapchainTextureFormat(device, window),
// Premultiplied-alpha blending: src outputs RGB pre-multiplied by alpha,
// so src factor is ONE (not SRC_ALPHA). This eliminates the per-pixel
// divide in the outline path and is the standard blend mode used by
// Skia, Flutter, and GPUI.
blend_state = sdl.GPUColorTargetBlendState {
enable_blend = true,
enable_color_write_mask = true,
src_color_blendfactor = .SRC_ALPHA,
src_color_blendfactor = .ONE,
dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
color_blend_op = .ADD,
src_alpha_blendfactor = .SRC_ALPHA,
src_alpha_blendfactor = .ONE,
dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA,
alpha_blend_op = .ADD,
color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A},
},
},
num_color_targets = 1,
num_color_targets = 1,
},
vertex_input_state = sdl.GPUVertexInputState {
vertex_buffer_descriptions = &sdl.GPUVertexBufferDescription {
@@ -300,7 +337,7 @@ create_pipeline_2d_base :: proc(
}
// Upload white pixel and unit quad data in a single command buffer
white_pixel := [4]u8{255, 255, 255, 255}
white_pixel := Color{255, 255, 255, 255}
white_transfer_buf := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(white_pixel)},
@@ -578,7 +615,7 @@ draw_layer :: proc(
for &batch in GLOB.tmp_sub_batches[scissor.sub_batch_start:][:scissor.sub_batch_len] {
switch batch.kind {
case .Shapes:
case .Tessellated:
if current_mode != .Tessellated {
push_globals(cmd_buffer, width, height, .Tessellated)
current_mode = .Tessellated