917 lines
30 KiB
Odin
917 lines
30 KiB
Odin
package draw
|
||
|
||
import "base:runtime"
|
||
import "core:c"
|
||
import "core:log"
|
||
import "core:math"
|
||
import "core:strings"
|
||
import sdl "vendor:sdl3"
|
||
import sdl_ttf "vendor:sdl3/ttf"
|
||
|
||
import clay "../vendor/clay"
|
||
|
||
when ODIN_OS == .Darwin {
|
||
SHADER_TYPE :: sdl.GPUShaderFormat{.MSL}
|
||
ENTRY_POINT :: "main0"
|
||
} else {
|
||
SHADER_TYPE :: sdl.GPUShaderFormat{.SPIRV}
|
||
ENTRY_POINT :: "main"
|
||
}
|
||
|
||
BUFFER_INIT_SIZE :: 256
|
||
INITIAL_LAYER_SIZE :: 5
|
||
INITIAL_SCISSOR_SIZE :: 10
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Color -------------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
Color :: distinct [4]u8
|
||
|
||
BLACK :: Color{0, 0, 0, 255}
|
||
WHITE :: Color{255, 255, 255, 255}
|
||
RED :: Color{255, 0, 0, 255}
|
||
GREEN :: Color{0, 255, 0, 255}
|
||
BLUE :: Color{0, 0, 255, 255}
|
||
BLANK :: Color{0, 0, 0, 0}
|
||
|
||
// Convert clay.Color ([4]c.float in 0–255 range) to Color.
|
||
color_from_clay :: proc(clay_color: clay.Color) -> Color {
|
||
return Color{u8(clay_color[0]), u8(clay_color[1]), u8(clay_color[2]), u8(clay_color[3])}
|
||
}
|
||
|
||
// Convert Color to [4]f32 in 0.0–1.0 range. Useful for SDL interop (e.g. clear color).
|
||
color_to_f32 :: proc(color: Color) -> [4]f32 {
|
||
INV :: 1.0 / 255.0
|
||
return {f32(color[0]) * INV, f32(color[1]) * INV, f32(color[2]) * INV, f32(color[3]) * INV}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Core types --------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
Rectangle :: struct {
|
||
x: f32,
|
||
y: f32,
|
||
width: f32,
|
||
height: f32,
|
||
}
|
||
|
||
Sub_Batch_Kind :: enum u8 {
|
||
Shapes, // non-indexed, white texture, mode 0
|
||
Text, // indexed, atlas texture, mode 0
|
||
SDF, // instanced unit quad, white texture, mode 1
|
||
}
|
||
|
||
Sub_Batch :: struct {
|
||
kind: Sub_Batch_Kind,
|
||
offset: u32, // Shapes: vertex offset; Text: text_batch index; SDF: primitive index
|
||
count: u32, // Shapes: vertex count; Text: always 1; SDF: primitive count
|
||
}
|
||
|
||
Layer :: struct {
|
||
bounds: Rectangle,
|
||
sub_batch_start: u32,
|
||
sub_batch_len: u32,
|
||
scissor_start: u32,
|
||
scissor_len: u32,
|
||
}
|
||
|
||
Scissor :: struct {
|
||
bounds: sdl.Rect,
|
||
sub_batch_start: u32,
|
||
sub_batch_len: u32,
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Global state ------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
GLOB: Global
|
||
|
||
Global :: struct {
|
||
odin_context: runtime.Context,
|
||
pipeline_2d_base: Pipeline_2D_Base,
|
||
text_cache: Text_Cache,
|
||
layers: [dynamic]Layer,
|
||
scissors: [dynamic]Scissor,
|
||
tmp_shape_verts: [dynamic]Vertex,
|
||
tmp_text_verts: [dynamic]Vertex,
|
||
tmp_text_indices: [dynamic]c.int,
|
||
tmp_text_batches: [dynamic]TextBatch,
|
||
tmp_primitives: [dynamic]Primitive,
|
||
tmp_sub_batches: [dynamic]Sub_Batch,
|
||
tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects to destroy after end()
|
||
clay_memory: [^]u8,
|
||
msaa_texture: ^sdl.GPUTexture,
|
||
curr_layer_index: uint,
|
||
max_layers: int,
|
||
max_scissors: int,
|
||
max_shape_verts: int,
|
||
max_text_verts: int,
|
||
max_text_indices: int,
|
||
max_text_batches: int,
|
||
max_primitives: int,
|
||
max_sub_batches: int,
|
||
dpi_scaling: f32,
|
||
msaa_width: u32,
|
||
msaa_height: u32,
|
||
sample_count: sdl.GPUSampleCount,
|
||
clay_z_index: i16,
|
||
cleared: bool,
|
||
}
|
||
|
||
Init_Options :: struct {
|
||
// MSAA sample count. Default is ._1 (no MSAA). SDF rendering does not benefit from MSAA
|
||
// because SDF fragments compute coverage analytically via `smoothstep`. MSAA helps for
|
||
// text glyph edges and tessellated user geometry. Set to ._4 or ._8 for text-heavy UIs,
|
||
// or use `MSAA_MAX` to request the highest sample count the GPU supports for the swapchain
|
||
// format.
|
||
msaa_samples: sdl.GPUSampleCount,
|
||
}
|
||
|
||
// Sentinel value: when passed as msaa_samples, `init` will use the maximum MSAA sample count
|
||
// supported by the GPU for the swapchain format.
|
||
MSAA_MAX :: sdl.GPUSampleCount(0xFF)
|
||
|
||
// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
|
||
@(require_results)
|
||
init :: proc(
|
||
device: ^sdl.GPUDevice,
|
||
window: ^sdl.Window,
|
||
options: Init_Options = {},
|
||
allocator := context.allocator,
|
||
odin_context := context,
|
||
) -> (
|
||
ok: bool,
|
||
) {
|
||
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
|
||
resolved_sample_count := options.msaa_samples
|
||
if resolved_sample_count == MSAA_MAX {
|
||
resolved_sample_count = max_sample_count(device, window)
|
||
}
|
||
|
||
pipeline, pipeline_ok := create_pipeline_2d_base(device, window, resolved_sample_count)
|
||
if !pipeline_ok {
|
||
return false
|
||
}
|
||
|
||
text_cache, text_ok := init_text_cache(device, allocator)
|
||
if !text_ok {
|
||
destroy_pipeline_2d_base(device, &pipeline)
|
||
return false
|
||
}
|
||
|
||
GLOB = Global {
|
||
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
|
||
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
|
||
tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
||
odin_context = odin_context,
|
||
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
||
clay_memory = make([^]u8, min_memory_size, allocator = allocator),
|
||
sample_count = resolved_sample_count,
|
||
pipeline_2d_base = pipeline,
|
||
text_cache = text_cache,
|
||
}
|
||
log.debug("Window DPI scaling:", GLOB.dpi_scaling)
|
||
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
|
||
window_width, window_height: c.int
|
||
sdl.GetWindowSize(window, &window_width, &window_height)
|
||
|
||
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
||
clay.SetMeasureTextFunction(measure_text_clay, nil)
|
||
return true
|
||
}
|
||
|
||
// TODO Either every x frames nuke max values in case of edge cases where max gets set very high
|
||
// or leave to application code to decide the right time for resize
|
||
resize_global :: proc() {
|
||
if len(GLOB.layers) > GLOB.max_layers do GLOB.max_layers = len(GLOB.layers)
|
||
shrink(&GLOB.layers, GLOB.max_layers)
|
||
if len(GLOB.scissors) > GLOB.max_scissors do GLOB.max_scissors = len(GLOB.scissors)
|
||
shrink(&GLOB.scissors, GLOB.max_scissors)
|
||
if len(GLOB.tmp_shape_verts) > GLOB.max_shape_verts do GLOB.max_shape_verts = len(GLOB.tmp_shape_verts)
|
||
shrink(&GLOB.tmp_shape_verts, GLOB.max_shape_verts)
|
||
if len(GLOB.tmp_text_verts) > GLOB.max_text_verts do GLOB.max_text_verts = len(GLOB.tmp_text_verts)
|
||
shrink(&GLOB.tmp_text_verts, GLOB.max_text_verts)
|
||
if len(GLOB.tmp_text_indices) > GLOB.max_text_indices do GLOB.max_text_indices = len(GLOB.tmp_text_indices)
|
||
shrink(&GLOB.tmp_text_indices, GLOB.max_text_indices)
|
||
if len(GLOB.tmp_text_batches) > GLOB.max_text_batches do GLOB.max_text_batches = len(GLOB.tmp_text_batches)
|
||
shrink(&GLOB.tmp_text_batches, GLOB.max_text_batches)
|
||
if len(GLOB.tmp_primitives) > GLOB.max_primitives do GLOB.max_primitives = len(GLOB.tmp_primitives)
|
||
shrink(&GLOB.tmp_primitives, GLOB.max_primitives)
|
||
if len(GLOB.tmp_sub_batches) > GLOB.max_sub_batches do GLOB.max_sub_batches = len(GLOB.tmp_sub_batches)
|
||
shrink(&GLOB.tmp_sub_batches, GLOB.max_sub_batches)
|
||
}
|
||
|
||
destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
|
||
delete(GLOB.layers)
|
||
delete(GLOB.scissors)
|
||
delete(GLOB.tmp_shape_verts)
|
||
delete(GLOB.tmp_text_verts)
|
||
delete(GLOB.tmp_text_indices)
|
||
delete(GLOB.tmp_text_batches)
|
||
delete(GLOB.tmp_primitives)
|
||
delete(GLOB.tmp_sub_batches)
|
||
for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
|
||
delete(GLOB.tmp_uncached_text)
|
||
free(GLOB.clay_memory, allocator)
|
||
if GLOB.msaa_texture != nil {
|
||
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
|
||
}
|
||
destroy_pipeline_2d_base(device, &GLOB.pipeline_2d_base)
|
||
destroy_text_cache()
|
||
}
|
||
|
||
// Internal
|
||
clear_global :: proc() {
|
||
GLOB.curr_layer_index = 0
|
||
GLOB.clay_z_index = 0
|
||
GLOB.cleared = false
|
||
// Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data)
|
||
for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
|
||
clear(&GLOB.tmp_uncached_text)
|
||
clear(&GLOB.layers)
|
||
clear(&GLOB.scissors)
|
||
clear(&GLOB.tmp_shape_verts)
|
||
clear(&GLOB.tmp_text_verts)
|
||
clear(&GLOB.tmp_text_indices)
|
||
clear(&GLOB.tmp_text_batches)
|
||
clear(&GLOB.tmp_primitives)
|
||
clear(&GLOB.tmp_sub_batches)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Text measurement (Clay) -------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
@(private = "file")
|
||
measure_text_clay :: proc "c" (
|
||
text: clay.StringSlice,
|
||
config: ^clay.TextElementConfig,
|
||
user_data: rawptr,
|
||
) -> clay.Dimensions {
|
||
context = GLOB.odin_context
|
||
text := string(text.chars[:text.length])
|
||
c_text := strings.clone_to_cstring(text, context.temp_allocator)
|
||
width, height: c.int
|
||
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
|
||
log.panicf("Failed to measure text: %s", sdl.GetError())
|
||
}
|
||
|
||
return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Frame lifecycle ---------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for.
|
||
begin :: proc(bounds: Rectangle) -> ^Layer {
|
||
// Cleanup
|
||
clear_global()
|
||
|
||
// Begin new layer + start a new scissor
|
||
scissor := Scissor {
|
||
bounds = sdl.Rect {
|
||
x = i32(bounds.x * GLOB.dpi_scaling),
|
||
y = i32(bounds.y * GLOB.dpi_scaling),
|
||
w = i32(bounds.width * GLOB.dpi_scaling),
|
||
h = i32(bounds.height * GLOB.dpi_scaling),
|
||
},
|
||
}
|
||
append(&GLOB.scissors, scissor)
|
||
|
||
layer := Layer {
|
||
bounds = bounds,
|
||
scissor_len = 1,
|
||
}
|
||
append(&GLOB.layers, layer)
|
||
return &GLOB.layers[GLOB.curr_layer_index]
|
||
}
|
||
|
||
// Creates a new layer
|
||
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
|
||
layer := Layer {
|
||
bounds = bounds,
|
||
sub_batch_start = prev_layer.sub_batch_start + prev_layer.sub_batch_len,
|
||
scissor_start = prev_layer.scissor_start + prev_layer.scissor_len,
|
||
scissor_len = 1,
|
||
}
|
||
append(&GLOB.layers, layer)
|
||
GLOB.curr_layer_index += 1
|
||
log.debug("Added new layer; curr index", GLOB.curr_layer_index)
|
||
|
||
scissor := Scissor {
|
||
sub_batch_start = u32(len(GLOB.tmp_sub_batches)),
|
||
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)
|
||
return &GLOB.layers[GLOB.curr_layer_index]
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Built-in primitive processing --
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Submit shape vertices (colored triangles) to the given layer for rendering.
|
||
prepare_shape :: proc(layer: ^Layer, vertices: []Vertex) {
|
||
if len(vertices) == 0 do return
|
||
offset := u32(len(GLOB.tmp_shape_verts))
|
||
append(&GLOB.tmp_shape_verts, ..vertices)
|
||
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||
append_or_extend_sub_batch(scissor, layer, .Shapes, offset, u32(len(vertices)))
|
||
}
|
||
|
||
// Submit an SDF primitive to the given layer for rendering.
|
||
prepare_sdf_primitive :: proc(layer: ^Layer, prim: Primitive) {
|
||
offset := u32(len(GLOB.tmp_primitives))
|
||
append(&GLOB.tmp_primitives, prim)
|
||
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||
append_or_extend_sub_batch(scissor, layer, .SDF, offset, 1)
|
||
}
|
||
|
||
// Submit a text element to the given layer for rendering.
|
||
// Copies SDL_ttf vertices directly (with baked position) and copies indices for indexed drawing.
|
||
prepare_text :: proc(layer: ^Layer, text: Text) {
|
||
data := sdl_ttf.GetGPUTextDrawData(text.sdl_text)
|
||
if data == nil {
|
||
return // nil is normal for empty text
|
||
}
|
||
|
||
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||
|
||
for data != nil {
|
||
vertex_start := u32(len(GLOB.tmp_text_verts))
|
||
index_start := u32(len(GLOB.tmp_text_indices))
|
||
|
||
// Copy vertices with baked position offset
|
||
for i in 0 ..< data.num_vertices {
|
||
pos := data.xy[i]
|
||
uv := data.uv[i]
|
||
append(
|
||
&GLOB.tmp_text_verts,
|
||
Vertex {
|
||
position = {pos.x + text.position[0] * GLOB.dpi_scaling, -pos.y + text.position[1] * GLOB.dpi_scaling},
|
||
uv = {uv.x, uv.y},
|
||
color = text.color,
|
||
},
|
||
)
|
||
}
|
||
|
||
// Copy indices directly
|
||
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
|
||
|
||
batch_idx := u32(len(GLOB.tmp_text_batches))
|
||
append(
|
||
&GLOB.tmp_text_batches,
|
||
TextBatch {
|
||
atlas_texture = data.atlas_texture,
|
||
vertex_start = vertex_start,
|
||
vertex_count = u32(data.num_vertices),
|
||
index_start = index_start,
|
||
index_count = u32(data.num_indices),
|
||
},
|
||
)
|
||
|
||
// Each atlas chunk is a separate sub-batch (different atlas textures can't coalesce)
|
||
append_or_extend_sub_batch(scissor, layer, .Text, batch_idx, 1)
|
||
|
||
data = data.next
|
||
}
|
||
}
|
||
|
||
// Submit a text element with a 2D affine transform applied to vertices.
|
||
// Used by the high-level `text` proc when rotation or a non-zero origin is specified.
|
||
// NOTE: xform must be in physical (DPI-scaled) pixel space — the caller pre-scales
|
||
// pos and origin by GLOB.dpi_scaling before building the transform.
|
||
prepare_text_transformed :: proc(layer: ^Layer, text: Text, transform: Transform_2D) {
|
||
data := sdl_ttf.GetGPUTextDrawData(text.sdl_text)
|
||
if data == nil {
|
||
return
|
||
}
|
||
|
||
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||
|
||
for data != nil {
|
||
vertex_start := u32(len(GLOB.tmp_text_verts))
|
||
index_start := u32(len(GLOB.tmp_text_indices))
|
||
|
||
for i in 0 ..< data.num_vertices {
|
||
pos := data.xy[i]
|
||
uv := data.uv[i]
|
||
// SDL_ttf gives glyph positions in physical pixels relative to text origin.
|
||
// The transform is already in physical-pixel space (caller pre-scaled),
|
||
// so we apply directly — no per-vertex DPI divide/multiply.
|
||
append(
|
||
&GLOB.tmp_text_verts,
|
||
Vertex{position = apply_transform(transform, {pos.x, -pos.y}), uv = {uv.x, uv.y}, color = text.color},
|
||
)
|
||
}
|
||
|
||
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
|
||
|
||
batch_idx := u32(len(GLOB.tmp_text_batches))
|
||
append(
|
||
&GLOB.tmp_text_batches,
|
||
TextBatch {
|
||
atlas_texture = data.atlas_texture,
|
||
vertex_start = vertex_start,
|
||
vertex_count = u32(data.num_vertices),
|
||
index_start = index_start,
|
||
index_count = u32(data.num_indices),
|
||
},
|
||
)
|
||
|
||
append_or_extend_sub_batch(scissor, layer, .Text, batch_idx, 1)
|
||
|
||
data = data.next
|
||
}
|
||
}
|
||
|
||
// Append a new sub-batch or extend the last one if same kind and contiguous.
|
||
@(private)
|
||
append_or_extend_sub_batch :: proc(
|
||
scissor: ^Scissor,
|
||
layer: ^Layer,
|
||
kind: Sub_Batch_Kind,
|
||
offset: u32,
|
||
count: u32,
|
||
) {
|
||
if scissor.sub_batch_len > 0 {
|
||
last := &GLOB.tmp_sub_batches[scissor.sub_batch_start + scissor.sub_batch_len - 1]
|
||
if last.kind == kind && kind != .Text && last.offset + last.count == offset {
|
||
last.count += count
|
||
return
|
||
}
|
||
}
|
||
append(&GLOB.tmp_sub_batches, Sub_Batch{kind = kind, offset = offset, count = count})
|
||
scissor.sub_batch_len += 1
|
||
layer.sub_batch_len += 1
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Clay ------------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
@(private = "file")
|
||
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
||
context = GLOB.odin_context
|
||
log.error("Clay error:", errorData.errorType, errorData.errorText)
|
||
}
|
||
|
||
ClayBatch :: struct {
|
||
bounds: Rectangle,
|
||
cmds: clay.ClayArray(clay.RenderCommand),
|
||
}
|
||
|
||
// Process Clay render commands into shape and text primitives.
|
||
prepare_clay_batch :: proc(
|
||
base_layer: ^Layer,
|
||
batch: ^ClayBatch,
|
||
mouse_wheel_delta: [2]f32,
|
||
frame_time: f32 = 0,
|
||
) {
|
||
mouse_pos: [2]f32
|
||
mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y)
|
||
|
||
// Update clay internals
|
||
clay.SetPointerState(
|
||
clay.Vector2{mouse_pos.x - base_layer.bounds.x, mouse_pos.y - base_layer.bounds.y},
|
||
.LEFT in mouse_flags,
|
||
)
|
||
clay.UpdateScrollContainers(true, mouse_wheel_delta, frame_time)
|
||
|
||
layer := base_layer
|
||
|
||
// Parse render commands
|
||
for i in 0 ..< int(batch.cmds.length) {
|
||
render_command := clay.RenderCommandArray_Get(&batch.cmds, cast(i32)i)
|
||
|
||
// Translate bounding box of the primitive by the layer position
|
||
bounds := Rectangle {
|
||
x = render_command.boundingBox.x + layer.bounds.x,
|
||
y = render_command.boundingBox.y + layer.bounds.y,
|
||
width = render_command.boundingBox.width,
|
||
height = render_command.boundingBox.height,
|
||
}
|
||
|
||
if render_command.zIndex > GLOB.clay_z_index {
|
||
log.debug("Higher zIndex found, creating new layer & setting z_index to", render_command.zIndex)
|
||
layer = new_layer(layer, bounds)
|
||
// Update bounds to new layer offset
|
||
bounds.x = render_command.boundingBox.x + layer.bounds.x
|
||
bounds.y = render_command.boundingBox.y + layer.bounds.y
|
||
GLOB.clay_z_index = render_command.zIndex
|
||
}
|
||
|
||
switch (render_command.commandType) {
|
||
case clay.RenderCommandType.None:
|
||
case clay.RenderCommandType.Text:
|
||
render_data := render_command.renderData.text
|
||
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
||
c_text := strings.clone_to_cstring(txt, context.temp_allocator)
|
||
// Clay's render_command.id is already hashed with the same Jenkins algorithm
|
||
// as text_cache_hash, so it shares the same keyspace.
|
||
sdl_text := cache_get_or_update(
|
||
render_command.id,
|
||
c_text,
|
||
get_font(render_data.fontId, render_data.fontSize),
|
||
)
|
||
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
|
||
case clay.RenderCommandType.Image:
|
||
case clay.RenderCommandType.ScissorStart:
|
||
if bounds.width == 0 || bounds.height == 0 do continue
|
||
|
||
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||
|
||
if curr_scissor.sub_batch_len != 0 {
|
||
// Scissor has some content, need to make a new scissor
|
||
new := Scissor {
|
||
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
|
||
bounds = sdl.Rect {
|
||
c.int(bounds.x * GLOB.dpi_scaling),
|
||
c.int(bounds.y * GLOB.dpi_scaling),
|
||
c.int(bounds.width * GLOB.dpi_scaling),
|
||
c.int(bounds.height * GLOB.dpi_scaling),
|
||
},
|
||
}
|
||
append(&GLOB.scissors, new)
|
||
layer.scissor_len += 1
|
||
} else {
|
||
curr_scissor.bounds = sdl.Rect {
|
||
c.int(bounds.x * GLOB.dpi_scaling),
|
||
c.int(bounds.y * GLOB.dpi_scaling),
|
||
c.int(bounds.width * GLOB.dpi_scaling),
|
||
c.int(bounds.height * GLOB.dpi_scaling),
|
||
}
|
||
}
|
||
case clay.RenderCommandType.ScissorEnd:
|
||
case clay.RenderCommandType.Rectangle:
|
||
render_data := render_command.renderData.rectangle
|
||
cr := render_data.cornerRadius
|
||
color := color_from_clay(render_data.backgroundColor)
|
||
radii := [4]f32{cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft}
|
||
|
||
if radii == {0, 0, 0, 0} {
|
||
rectangle(layer, bounds, color)
|
||
} else {
|
||
rectangle_corners(layer, bounds, radii, color)
|
||
}
|
||
case clay.RenderCommandType.Border:
|
||
render_data := render_command.renderData.border
|
||
cr := render_data.cornerRadius
|
||
color := color_from_clay(render_data.color)
|
||
thickness := f32(render_data.width.top)
|
||
radii := [4]f32{cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft}
|
||
|
||
if radii == {0, 0, 0, 0} {
|
||
rectangle_lines(layer, bounds, color, thickness)
|
||
} else {
|
||
rectangle_corners_lines(layer, bounds, radii, color, thickness)
|
||
}
|
||
case clay.RenderCommandType.Custom:
|
||
}
|
||
}
|
||
}
|
||
|
||
// Render primitives. clear_color is the background fill before any layers are drawn.
|
||
end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = BLACK) {
|
||
cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
|
||
if cmd_buffer == nil {
|
||
log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError())
|
||
}
|
||
|
||
// Upload primitives to GPU
|
||
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
|
||
upload(device, copy_pass)
|
||
sdl.EndGPUCopyPass(copy_pass)
|
||
|
||
swapchain_texture: ^sdl.GPUTexture
|
||
width, height: u32
|
||
if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &width, &height) {
|
||
log.panicf("Failed to acquire swapchain texture: %s", sdl.GetError())
|
||
}
|
||
|
||
if swapchain_texture == nil {
|
||
// Window is minimized or not visible — submit and skip this frame
|
||
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
|
||
log.panicf("Failed to submit GPU command buffer (minimized window): %s", sdl.GetError())
|
||
}
|
||
return
|
||
}
|
||
|
||
use_msaa := GLOB.sample_count != ._1
|
||
render_texture := swapchain_texture
|
||
|
||
if use_msaa {
|
||
ensure_msaa_texture(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
|
||
render_texture = GLOB.msaa_texture
|
||
}
|
||
|
||
clear_color_f32 := color_to_f32(clear_color)
|
||
|
||
// Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
|
||
for &layer, index in GLOB.layers {
|
||
log.debug("Drawing layer", index)
|
||
draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer)
|
||
}
|
||
|
||
// Resolve MSAA render texture to the swapchain.
|
||
if use_msaa {
|
||
resolve_pass := sdl.BeginGPURenderPass(
|
||
cmd_buffer,
|
||
&sdl.GPUColorTargetInfo {
|
||
texture = render_texture,
|
||
load_op = .LOAD,
|
||
store_op = .RESOLVE,
|
||
resolve_texture = swapchain_texture,
|
||
},
|
||
1,
|
||
nil,
|
||
)
|
||
sdl.EndGPURenderPass(resolve_pass)
|
||
}
|
||
|
||
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
|
||
log.panicf("Failed to submit GPU command buffer: %s", sdl.GetError())
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- MSAA --------------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// Query the highest MSAA sample count supported by the GPU for the swapchain format.
|
||
max_sample_count :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> sdl.GPUSampleCount {
|
||
format := sdl.GetGPUSwapchainTextureFormat(device, window)
|
||
counts := [?]sdl.GPUSampleCount{._8, ._4, ._2}
|
||
for count in counts {
|
||
if sdl.GPUTextureSupportsSampleCount(device, format, count) do return count
|
||
}
|
||
return ._1
|
||
}
|
||
|
||
@(private = "file")
|
||
ensure_msaa_texture :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, width, height: u32) {
|
||
if GLOB.msaa_texture != nil && GLOB.msaa_width == width && GLOB.msaa_height == height {
|
||
return
|
||
}
|
||
if GLOB.msaa_texture != nil {
|
||
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
|
||
}
|
||
GLOB.msaa_texture = sdl.CreateGPUTexture(
|
||
device,
|
||
sdl.GPUTextureCreateInfo {
|
||
type = .D2,
|
||
format = format,
|
||
usage = {.COLOR_TARGET},
|
||
width = width,
|
||
height = height,
|
||
layer_count_or_depth = 1,
|
||
num_levels = 1,
|
||
sample_count = GLOB.sample_count,
|
||
},
|
||
)
|
||
if GLOB.msaa_texture == nil {
|
||
log.panicf("Failed to create MSAA texture (%dx%d): %s", width, height, sdl.GetError())
|
||
}
|
||
GLOB.msaa_width = width
|
||
GLOB.msaa_height = height
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Utility -----------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
ortho_rh :: proc(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> matrix[4, 4]f32 {
|
||
return matrix[4, 4]f32{
|
||
2.0 / (right - left), 0.0, 0.0, -(right + left) / (right - left),
|
||
0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / (top - bottom),
|
||
0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near),
|
||
0.0, 0.0, 0.0, 1.0,
|
||
}
|
||
}
|
||
|
||
Draw_Mode :: enum u32 {
|
||
Tessellated = 0,
|
||
SDF = 1,
|
||
}
|
||
|
||
Vertex_Uniforms :: struct {
|
||
projection: matrix[4, 4]f32,
|
||
scale: f32,
|
||
mode: Draw_Mode,
|
||
}
|
||
|
||
// Push projection, dpi scale, and rendering mode as a single uniform block (slot 0).
|
||
push_globals :: proc(
|
||
cmd_buffer: ^sdl.GPUCommandBuffer,
|
||
width: f32,
|
||
height: f32,
|
||
mode: Draw_Mode = .Tessellated,
|
||
) {
|
||
globals := Vertex_Uniforms {
|
||
projection = ortho_rh(
|
||
left = 0.0,
|
||
top = 0.0,
|
||
right = f32(width),
|
||
bottom = f32(height),
|
||
near = -1.0,
|
||
far = 1.0,
|
||
),
|
||
scale = GLOB.dpi_scaling,
|
||
mode = mode,
|
||
}
|
||
|
||
sdl.PushGPUVertexUniformData(cmd_buffer, 0, &globals, size_of(Vertex_Uniforms))
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Buffer ------------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
Buffer :: struct {
|
||
gpu: ^sdl.GPUBuffer,
|
||
transfer: ^sdl.GPUTransferBuffer,
|
||
size: u32,
|
||
}
|
||
|
||
@(require_results)
|
||
create_buffer :: proc(
|
||
device: ^sdl.GPUDevice,
|
||
size: u32,
|
||
gpu_usage: sdl.GPUBufferUsageFlags,
|
||
) -> (
|
||
buffer: Buffer,
|
||
ok: bool,
|
||
) {
|
||
gpu := sdl.CreateGPUBuffer(device, sdl.GPUBufferCreateInfo{usage = gpu_usage, size = size})
|
||
if gpu == nil {
|
||
log.errorf("Failed to create GPU buffer (size=%d): %s", size, sdl.GetError())
|
||
return buffer, false
|
||
}
|
||
transfer := sdl.CreateGPUTransferBuffer(
|
||
device,
|
||
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size},
|
||
)
|
||
if transfer == nil {
|
||
sdl.ReleaseGPUBuffer(device, gpu)
|
||
log.errorf("Failed to create GPU transfer buffer (size=%d): %s", size, sdl.GetError())
|
||
return buffer, false
|
||
}
|
||
return Buffer{gpu, transfer, size}, true
|
||
}
|
||
|
||
grow_buffer_if_needed :: proc(
|
||
device: ^sdl.GPUDevice,
|
||
buffer: ^Buffer,
|
||
new_size: u32,
|
||
gpu_usage: sdl.GPUBufferUsageFlags,
|
||
) {
|
||
if new_size > buffer.size {
|
||
log.debug("Resizing buffer from", buffer.size, "to", new_size)
|
||
destroy_buffer(device, buffer)
|
||
buffer.gpu = sdl.CreateGPUBuffer(device, sdl.GPUBufferCreateInfo{usage = gpu_usage, size = new_size})
|
||
if buffer.gpu == nil {
|
||
log.panicf("Failed to grow GPU buffer (new_size=%d): %s", new_size, sdl.GetError())
|
||
}
|
||
buffer.transfer = sdl.CreateGPUTransferBuffer(
|
||
device,
|
||
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = new_size},
|
||
)
|
||
if buffer.transfer == nil {
|
||
log.panicf("Failed to grow GPU transfer buffer (new_size=%d): %s", new_size, sdl.GetError())
|
||
}
|
||
buffer.size = new_size
|
||
}
|
||
}
|
||
|
||
destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) {
|
||
sdl.ReleaseGPUBuffer(device, buffer.gpu)
|
||
sdl.ReleaseGPUTransferBuffer(device, buffer.transfer)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Transform ------------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
// 2x3 affine transform for 2D pivot-rotation.
|
||
// Used internally by rotation-aware drawing procs.
|
||
Transform_2D :: struct {
|
||
m00, m01: f32, // row 0: rotation/scale
|
||
m10, m11: f32, // row 1: rotation/scale
|
||
tx, ty: f32, // translation
|
||
}
|
||
|
||
// Build a pivot-rotation transform.
|
||
//
|
||
// Semantics (raylib-style):
|
||
// The point whose local coordinates equal `origin` lands at `pos` in world space.
|
||
// The rest of the shape rotates around that pivot.
|
||
//
|
||
// Formula: p_world = pos + R(θ) · (p_local - origin)
|
||
//
|
||
// Parameters:
|
||
// pos – world-space position where the pivot lands.
|
||
// origin – pivot point in local space (measured from the shape's natural reference point).
|
||
// rotation_deg – rotation in degrees, counter-clockwise.
|
||
//
|
||
build_pivot_rotation :: proc(position: [2]f32, origin: [2]f32, rotation_deg: f32) -> Transform_2D {
|
||
radians := math.to_radians(rotation_deg)
|
||
cos_angle := math.cos(radians)
|
||
sin_angle := math.sin(radians)
|
||
return Transform_2D {
|
||
m00 = cos_angle,
|
||
m01 = -sin_angle,
|
||
m10 = sin_angle,
|
||
m11 = cos_angle,
|
||
tx = position.x - (cos_angle * origin.x - sin_angle * origin.y),
|
||
ty = position.y - (sin_angle * origin.x + cos_angle * origin.y),
|
||
}
|
||
}
|
||
|
||
// Apply the transform to a local-space point, producing a world-space point.
|
||
apply_transform :: #force_inline proc(transform: Transform_2D, point: [2]f32) -> [2]f32 {
|
||
return {
|
||
transform.m00 * point.x + transform.m01 * point.y + transform.tx,
|
||
transform.m10 * point.x + transform.m11 * point.y + transform.ty,
|
||
}
|
||
}
|
||
|
||
// Fast-path check callers use BEFORE building a transform.
|
||
// Returns true if either the origin is non-zero or rotation is non-zero,
|
||
// meaning a transform actually needs to be computed.
|
||
needs_transform :: #force_inline proc(origin: [2]f32, rotation: f32) -> bool {
|
||
return origin != {0, 0} || rotation != 0
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
// ----- Procedure Groups ------------------------
|
||
// ---------------------------------------------------------------------------------------------------------------------
|
||
|
||
center_of :: proc {
|
||
center_of_rectangle,
|
||
center_of_triangle,
|
||
center_of_text,
|
||
}
|
||
|
||
top_left_of :: proc {
|
||
top_left_of_rectangle,
|
||
top_left_of_triangle,
|
||
top_left_of_text,
|
||
}
|
||
|
||
top_of :: proc {
|
||
top_of_rectangle,
|
||
top_of_triangle,
|
||
top_of_text,
|
||
}
|
||
|
||
top_right_of :: proc {
|
||
top_right_of_rectangle,
|
||
top_right_of_triangle,
|
||
top_right_of_text,
|
||
}
|
||
|
||
left_of :: proc {
|
||
left_of_rectangle,
|
||
left_of_triangle,
|
||
left_of_text,
|
||
}
|
||
|
||
right_of :: proc {
|
||
right_of_rectangle,
|
||
right_of_triangle,
|
||
right_of_text,
|
||
}
|
||
|
||
bottom_left_of :: proc {
|
||
bottom_left_of_rectangle,
|
||
bottom_left_of_triangle,
|
||
bottom_left_of_text,
|
||
}
|
||
|
||
bottom_of :: proc {
|
||
bottom_of_rectangle,
|
||
bottom_of_triangle,
|
||
bottom_of_text,
|
||
}
|
||
|
||
bottom_right_of :: proc {
|
||
bottom_right_of_rectangle,
|
||
bottom_right_of_triangle,
|
||
bottom_right_of_text,
|
||
}
|