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 { PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.MSL SHADER_ENTRY :: cstring("main0") BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.metal") BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.metal") } else { PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.SPIRV SHADER_ENTRY :: cstring("main") BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.spv") BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.spv") } PLATFORM_SHADER_FORMAT :: sdl.GPUShaderFormat{PLATFORM_SHADER_FORMAT_FLAG} 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] // 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) 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 + base_x, -pos.y + base_y}, 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) } // Called for each Clay `RenderCommandType.Custom` render command that // `prepare_clay_batch` encounters. // // - `layer` is the layer the command belongs to (post-z-index promotion). // - `bounds` is already translated into the active layer's coordinate system // and pre-DPI, matching what the built-in shape procs expect. // - `render_data` is Clay's `CustomRenderData` for the element, exposing // `backgroundColor`, `cornerRadius`, and the `customData` pointer the caller // attached to `clay.CustomElementConfig.customData`. // // The callback must not call `new_layer` or `prepare_clay_batch`. Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData) 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, custom_draw: Custom_Draw = nil, ) { 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 render-command IDs are derived via Clay's internal HashNumber (Jenkins-family) // and namespaced with .Clay so they can never collide with user-provided custom text IDs. sdl_text := cache_get_or_update( Cache_Key{render_command.id, .Clay}, 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: if custom_draw != nil { custom_draw(layer, bounds, render_command.renderData.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, }