package draw import "core:c" import "core:log" import "core:math" import "core:mem" import sdl "vendor:sdl3" 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_2D :: struct { position: Vec2, uv: [2]f32, color: Color, } //INTERNAL Text_Batch :: struct { atlas_texture: ^sdl.GPUTexture, vertex_start: u32, vertex_count: u32, index_start: u32, index_count: u32, } // --------------------------------------------------------------------------------------------------------------------- // ----- Primitive types ------------ // --------------------------------------------------------------------------------------------------------------------- // The SDF path evaluates one of four signed distance functions per primitive, dispatched // by Shape_Kind encoded in the low byte of Core_2D_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. //INTERNAL Shape_Kind :: enum u8 { Solid = 0, // tessellated path (mode marker; not a real SDF kind) RRect = 1, NGon = 2, Ellipse = 3, Ring_Arc = 4, } //INTERNAL Shape_Flag :: enum u8 { Textured, // bit 0: sample texture using uv_rect (mutually exclusive with Gradient via Brush union) Gradient, // bit 1: 2-color gradient using 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 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. } //INTERNAL 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, } //INTERNAL NGon_Params :: struct { radius: f32, sides: f32, half_feather: f32, // feather_px * 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, } //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, } //INTERNAL Shape_Params :: struct #raw_union { rrect: RRect_Params, ngon: NGon_Params, ellipse: Ellipse_Params, ring_arc: Ring_Arc_Params, raw: [8]f32, } #assert(size_of(Shape_Params) == 32) // GPU-side storage for 2-color gradient parameters and/or outline parameters. // Packed into 16 bytes. Independent from uv_rect — texture and outline can coexist. // 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. //INTERNAL 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) // GPU layout: 96 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. // // Named Core_2D_Primitive (not just Primitive) to disambiguate from Gaussian_Blur_Primitive // (and any future per-effect primitive types) in backdrop.odin. Each path/effect's primitive // type has its own GPU layout and fragment-shader contract; pairing each with its own // primitive type keeps cross-references unambiguous when grepping the codebase. //INTERNAL Core_2D_Primitive :: struct { 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_rect: [4]f32, // 64: texture UV coordinates (u_min, v_min, u_max, v_max). Read when .Textured. effects: Gradient_Outline, // 80: gradient and/or outline parameters. Read when .Gradient and/or .Outline. } #assert(size_of(Core_2D_Primitive) == 96) // Pack shape kind and flags into the Core_2D_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). //INTERNAL 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. //INTERNAL pack_f16_pair :: #force_inline proc(low, high: f16) -> u32 { return u32(transmute(u16)low) | (u32(transmute(u16)high) << 16) } // --------------------------------------------------------------------------------------------------------------------- // ----- Subsystem lifecycle ------------ // --------------------------------------------------------------------------------------------------------------------- //INTERNAL Core_2D :: struct { sdl_pipeline: ^sdl.GPUGraphicsPipeline, vertex_buffer: Buffer, index_buffer: Buffer, unit_quad_buffer: ^sdl.GPUBuffer, primitive_buffer: Buffer, white_texture: ^sdl.GPUTexture, 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. //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 defer if !ok { if core_2d.sampler != nil do sdl.ReleaseGPUSampler(device, core_2d.sampler) if core_2d.white_texture != nil do sdl.ReleaseGPUTexture(device, core_2d.white_texture) if core_2d.unit_quad_buffer != nil do sdl.ReleaseGPUBuffer(device, core_2d.unit_quad_buffer) if core_2d.primitive_buffer.gpu != nil do destroy_buffer(device, &core_2d.primitive_buffer) if core_2d.index_buffer.gpu != nil do destroy_buffer(device, &core_2d.index_buffer) if core_2d.vertex_buffer.gpu != nil do destroy_buffer(device, &core_2d.vertex_buffer) if core_2d.sdl_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, core_2d.sdl_pipeline) } active_shader_formats := sdl.GetGPUShaderFormats(device) if PLATFORM_SHADER_FORMAT_FLAG not_in active_shader_formats { log.errorf( "draw: no embedded shader matches active GPU formats; this build supports %v but device reports %v", PLATFORM_SHADER_FORMAT, active_shader_formats, ) return core_2d, false } log.debug("Loaded", len(BASE_VERT_2D_RAW), "vert bytes") log.debug("Loaded", len(BASE_FRAG_2D_RAW), "frag bytes") vert_info := sdl.GPUShaderCreateInfo { code_size = len(BASE_VERT_2D_RAW), code = raw_data(BASE_VERT_2D_RAW), entrypoint = SHADER_ENTRY, format = {PLATFORM_SHADER_FORMAT_FLAG}, stage = .VERTEX, num_uniform_buffers = 1, num_storage_buffers = 1, } frag_info := sdl.GPUShaderCreateInfo { code_size = len(BASE_FRAG_2D_RAW), code = raw_data(BASE_FRAG_2D_RAW), entrypoint = SHADER_ENTRY, format = {PLATFORM_SHADER_FORMAT_FLAG}, stage = .FRAGMENT, num_samplers = 1, } vert_shader := sdl.CreateGPUShader(device, vert_info) if vert_shader == nil { log.errorf("Could not create draw vertex shader: %s", sdl.GetError()) return core_2d, false } frag_shader := sdl.CreateGPUShader(device, frag_info) if frag_shader == nil { sdl.ReleaseGPUShader(device, vert_shader) log.errorf("Could not create draw fragment shader: %s", sdl.GetError()) return core_2d, false } vertex_attributes: [3]sdl.GPUVertexAttribute = { // position (GLSL location 0) sdl.GPUVertexAttribute{buffer_slot = 0, location = 0, format = .FLOAT2, offset = 0}, // uv (GLSL location 1) sdl.GPUVertexAttribute{buffer_slot = 0, location = 1, format = .FLOAT2, offset = size_of([2]f32)}, // color (GLSL location 2, u8x4 normalized to float by GPU) sdl.GPUVertexAttribute{buffer_slot = 0, location = 2, format = .UBYTE4_NORM, offset = size_of([2]f32) * 2}, } pipeline_info := sdl.GPUGraphicsPipelineCreateInfo { vertex_shader = vert_shader, fragment_shader = frag_shader, primitive_type = .TRIANGLELIST, multisample_state = sdl.GPUMultisampleState{sample_count = ._1}, 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 = .ONE, dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA, color_blend_op = .ADD, 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, }, vertex_input_state = sdl.GPUVertexInputState { vertex_buffer_descriptions = &sdl.GPUVertexBufferDescription { slot = 0, input_rate = .VERTEX, pitch = size_of(Vertex_2D), }, num_vertex_buffers = 1, vertex_attributes = raw_data(vertex_attributes[:]), num_vertex_attributes = 3, }, } core_2d.sdl_pipeline = sdl.CreateGPUGraphicsPipeline(device, pipeline_info) // Shaders are no longer needed regardless of pipeline creation success sdl.ReleaseGPUShader(device, vert_shader) sdl.ReleaseGPUShader(device, frag_shader) if core_2d.sdl_pipeline == nil { log.errorf("Failed to create draw graphics pipeline: %s", sdl.GetError()) return core_2d, false } // Create vertex buffer vert_buf_ok: bool core_2d.vertex_buffer, vert_buf_ok = create_buffer( device, size_of(Vertex_2D) * BUFFER_INIT_SIZE, sdl.GPUBufferUsageFlags{.VERTEX}, ) if !vert_buf_ok do return core_2d, false // Create index buffer (used by text) idx_buf_ok: bool core_2d.index_buffer, idx_buf_ok = create_buffer( device, size_of(c.int) * BUFFER_INIT_SIZE, sdl.GPUBufferUsageFlags{.INDEX}, ) if !idx_buf_ok do return core_2d, false // Create primitive storage buffer (used by SDF instanced drawing) prim_buf_ok: bool core_2d.primitive_buffer, prim_buf_ok = create_buffer( device, size_of(Core_2D_Primitive) * BUFFER_INIT_SIZE, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, ) if !prim_buf_ok do return core_2d, false // Create static 6-vertex unit quad buffer (two triangles, TRIANGLELIST) core_2d.unit_quad_buffer = sdl.CreateGPUBuffer( device, sdl.GPUBufferCreateInfo{usage = {.VERTEX}, size = 6 * size_of(Vertex_2D)}, ) if core_2d.unit_quad_buffer == nil { log.errorf("Failed to create unit quad buffer: %s", sdl.GetError()) return core_2d, false } // Create 1x1 white pixel texture core_2d.white_texture = sdl.CreateGPUTexture( device, sdl.GPUTextureCreateInfo { type = .D2, format = .R8G8B8A8_UNORM, usage = {.SAMPLER}, width = 1, height = 1, layer_count_or_depth = 1, num_levels = 1, sample_count = ._1, }, ) if core_2d.white_texture == nil { log.errorf("Failed to create white pixel texture: %s", sdl.GetError()) return core_2d, false } // Upload white pixel and unit quad data in a single command buffer white_pixel := Color{255, 255, 255, 255} white_transfer_buf := sdl.CreateGPUTransferBuffer( device, sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(white_pixel)}, ) if white_transfer_buf == nil { log.errorf("Failed to create white pixel transfer buffer: %s", sdl.GetError()) return core_2d, false } defer sdl.ReleaseGPUTransferBuffer(device, white_transfer_buf) white_ptr := sdl.MapGPUTransferBuffer(device, white_transfer_buf, false) if white_ptr == nil { log.errorf("Failed to map white pixel transfer buffer: %s", sdl.GetError()) return core_2d, false } mem.copy(white_ptr, &white_pixel, size_of(white_pixel)) sdl.UnmapGPUTransferBuffer(device, white_transfer_buf) quad_verts := [6]Vertex_2D { {position = {0, 0}}, {position = {1, 0}}, {position = {0, 1}}, {position = {0, 1}}, {position = {1, 0}}, {position = {1, 1}}, } quad_transfer_buf := sdl.CreateGPUTransferBuffer( device, sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(quad_verts)}, ) if quad_transfer_buf == nil { log.errorf("Failed to create unit quad transfer buffer: %s", sdl.GetError()) return core_2d, false } defer sdl.ReleaseGPUTransferBuffer(device, quad_transfer_buf) quad_ptr := sdl.MapGPUTransferBuffer(device, quad_transfer_buf, false) if quad_ptr == nil { log.errorf("Failed to map unit quad transfer buffer: %s", sdl.GetError()) return core_2d, false } mem.copy(quad_ptr, &quad_verts, size_of(quad_verts)) sdl.UnmapGPUTransferBuffer(device, quad_transfer_buf) upload_cmd_buffer := sdl.AcquireGPUCommandBuffer(device) if upload_cmd_buffer == nil { log.errorf("Failed to acquire command buffer for init upload: %s", sdl.GetError()) return core_2d, false } upload_pass := sdl.BeginGPUCopyPass(upload_cmd_buffer) sdl.UploadToGPUTexture( upload_pass, sdl.GPUTextureTransferInfo{transfer_buffer = white_transfer_buf}, sdl.GPUTextureRegion{texture = core_2d.white_texture, w = 1, h = 1, d = 1}, false, ) sdl.UploadToGPUBuffer( upload_pass, sdl.GPUTransferBufferLocation{transfer_buffer = quad_transfer_buf}, sdl.GPUBufferRegion{buffer = core_2d.unit_quad_buffer, offset = 0, size = size_of(quad_verts)}, false, ) sdl.EndGPUCopyPass(upload_pass) if !sdl.SubmitGPUCommandBuffer(upload_cmd_buffer) { log.errorf("Failed to submit init upload command buffer: %s", sdl.GetError()) return core_2d, false } log.debug("White pixel texture and unit quad buffer created and uploaded") // Create sampler (shared by shapes and text) core_2d.sampler = sdl.CreateGPUSampler( device, sdl.GPUSamplerCreateInfo { min_filter = .LINEAR, mag_filter = .LINEAR, mipmap_mode = .LINEAR, address_mode_u = .CLAMP_TO_EDGE, address_mode_v = .CLAMP_TO_EDGE, address_mode_w = .CLAMP_TO_EDGE, }, ) if core_2d.sampler == nil { log.errorf("Could not create GPU sampler: %s", sdl.GetError()) return core_2d, false } log.debug("Done creating core 2D subsystem") return core_2d, true } //INTERNAL destroy_core_2d :: proc(device: ^sdl.GPUDevice, core: ^Core_2D) { destroy_buffer(device, &core.vertex_buffer) destroy_buffer(device, &core.index_buffer) destroy_buffer(device, &core.primitive_buffer) if core.unit_quad_buffer != nil { sdl.ReleaseGPUBuffer(device, core.unit_quad_buffer) } sdl.ReleaseGPUTexture(device, core.white_texture) sdl.ReleaseGPUSampler(device, core.sampler) sdl.ReleaseGPUGraphicsPipeline(device, core.sdl_pipeline) } // --------------------------------------------------------------------------------------------------------------------- // ----- Upload and render ------------ // --------------------------------------------------------------------------------------------------------------------- //----- Vertex uniforms ---------------------------------- //INTERNAL Core_2D_Mode :: enum u32 { Tessellated = 0, SDF = 1, } //INTERNAL Vertex_Uniforms_2D :: struct { projection: matrix[4, 4]f32, scale: f32, mode: Core_2D_Mode, } // Push projection, dpi scale, and rendering mode as a single uniform block (slot 0). //INTERNAL push_globals :: proc( cmd_buffer: ^sdl.GPUCommandBuffer, width: f32, height: f32, mode: Core_2D_Mode = .Tessellated, ) { globals := Vertex_Uniforms_2D { 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_2D)) } //----- Per-frame upload ---------------------------------- //INTERNAL upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) { // Upload vertices (shapes then text into one buffer) shape_vert_count := u32(len(GLOB.tmp_shape_verts)) text_vert_count := u32(len(GLOB.tmp_text_verts)) total_vert_count := shape_vert_count + text_vert_count if total_vert_count > 0 { total_vert_size := total_vert_count * size_of(Vertex_2D) shape_vert_size := shape_vert_count * size_of(Vertex_2D) text_vert_size := text_vert_count * size_of(Vertex_2D) grow_buffer_if_needed( device, &GLOB.core_2d.vertex_buffer, total_vert_size, sdl.GPUBufferUsageFlags{.VERTEX}, ) vert_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.vertex_buffer.transfer, false) if vert_array == nil { log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError()) } if shape_vert_size > 0 { mem.copy(vert_array, raw_data(GLOB.tmp_shape_verts), int(shape_vert_size)) } if text_vert_size > 0 { mem.copy( rawptr(uintptr(vert_array) + uintptr(shape_vert_size)), raw_data(GLOB.tmp_text_verts), int(text_vert_size), ) } sdl.UnmapGPUTransferBuffer(device, GLOB.core_2d.vertex_buffer.transfer) sdl.UploadToGPUBuffer( pass, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.core_2d.vertex_buffer.transfer}, sdl.GPUBufferRegion{buffer = GLOB.core_2d.vertex_buffer.gpu, offset = 0, size = total_vert_size}, false, ) } // Upload text indices index_count := u32(len(GLOB.tmp_text_indices)) if index_count > 0 { index_size := index_count * size_of(c.int) grow_buffer_if_needed(device, &GLOB.core_2d.index_buffer, index_size, sdl.GPUBufferUsageFlags{.INDEX}) idx_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.index_buffer.transfer, false) if idx_array == nil { log.panicf("Failed to map index transfer buffer: %s", sdl.GetError()) } mem.copy(idx_array, raw_data(GLOB.tmp_text_indices), int(index_size)) sdl.UnmapGPUTransferBuffer(device, GLOB.core_2d.index_buffer.transfer) sdl.UploadToGPUBuffer( pass, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.core_2d.index_buffer.transfer}, sdl.GPUBufferRegion{buffer = GLOB.core_2d.index_buffer.gpu, offset = 0, size = index_size}, false, ) } // Upload SDF primitives prim_count := u32(len(GLOB.tmp_primitives)) if prim_count > 0 { prim_size := prim_count * size_of(Core_2D_Primitive) grow_buffer_if_needed( device, &GLOB.core_2d.primitive_buffer, prim_size, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, ) prim_array := sdl.MapGPUTransferBuffer(device, GLOB.core_2d.primitive_buffer.transfer, false) if prim_array == nil { log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError()) } mem.copy(prim_array, raw_data(GLOB.tmp_primitives), int(prim_size)) sdl.UnmapGPUTransferBuffer(device, GLOB.core_2d.primitive_buffer.transfer) sdl.UploadToGPUBuffer( pass, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.core_2d.primitive_buffer.transfer}, sdl.GPUBufferRegion{buffer = GLOB.core_2d.primitive_buffer.gpu, offset = 0, size = prim_size}, false, ) } } //----- Layer dispatch ---------------------------------- //INTERNAL draw_layer :: proc( device: ^sdl.GPUDevice, window: ^sdl.Window, cmd_buffer: ^sdl.GPUCommandBuffer, render_texture: ^sdl.GPUTexture, swapchain_width: u32, swapchain_height: u32, clear_color: [4]f32, layer: ^Layer, ) { if layer.sub_batch_len == 0 { if !GLOB.cleared { pass := sdl.BeginGPURenderPass( cmd_buffer, &sdl.GPUColorTargetInfo { texture = render_texture, clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]}, load_op = .CLEAR, store_op = .STORE, }, 1, nil, ) sdl.EndGPURenderPass(pass) GLOB.cleared = true } return } bracket_start_abs := find_first_backdrop_in_layer(layer) layer_end_abs := int(layer.sub_batch_start + layer.sub_batch_len) if bracket_start_abs < 0 { // Fast path: no backdrop in this layer; render the whole sub-batch range in one pass. render_layer_sub_batch_range( cmd_buffer, render_texture, swapchain_width, swapchain_height, clear_color, layer, int(layer.sub_batch_start), layer_end_abs, ) return } // Bracketed layer: Pass A → backdrop bracket → Pass B. // See README.md § "Backdrop pipeline" for the full ordering semantics. render_layer_sub_batch_range( cmd_buffer, render_texture, swapchain_width, swapchain_height, clear_color, layer, int(layer.sub_batch_start), bracket_start_abs, ) run_backdrop_bracket(cmd_buffer, layer, swapchain_width, swapchain_height) // Pass B: render the [bracket_start_abs, layer_end_abs) range. .Backdrop sub-batches in // this range are dispatched by the bracket above and ignored here (the .Backdrop case in // the inner switch is a no-op). LOAD is implied because Pass A or the bracket's V- // composite has already touched render_texture. render_layer_sub_batch_range( cmd_buffer, render_texture, swapchain_width, swapchain_height, clear_color, layer, bracket_start_abs, layer_end_abs, ) } // Render a sub-range of a layer's sub-batches in a single render pass. Iterates the layer's // scissors and walks each scissor's sub-batches, dispatching by kind. The `range_start_abs` // and `range_end_abs` parameters are absolute indices into GLOB.tmp_sub_batches; only sub- // batches within `[range_start_abs, range_end_abs)` are drawn. // // .Backdrop sub-batches in the range are always silently skipped — they are dispatched by // run_backdrop_bracket, not here. The empty .Backdrop case in the inner switch enforces this. // // Render-pass setup mirrors the original draw_layer: clear-or-load based on GLOB.cleared, // pipeline + storage + index buffer bound up front, then per-batch state tracking. After this // proc returns, GLOB.cleared is guaranteed true. // // If the range is empty after filtering (no eligible sub-batches at all), this proc still // honors the no-clear-yet contract by issuing a clear-only pass when needed; otherwise it // returns without opening a render pass. //INTERNAL render_layer_sub_batch_range :: proc( cmd_buffer: ^sdl.GPUCommandBuffer, render_texture: ^sdl.GPUTexture, swapchain_width: u32, swapchain_height: u32, clear_color: [4]f32, layer: ^Layer, range_start_abs: int, range_end_abs: int, ) { if range_start_abs >= range_end_abs { // Empty range. If we still owe a clear, do a clear-only pass; otherwise nothing to do. if !GLOB.cleared { pass := sdl.BeginGPURenderPass( cmd_buffer, &sdl.GPUColorTargetInfo { texture = render_texture, clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]}, load_op = .CLEAR, store_op = .STORE, }, 1, nil, ) sdl.EndGPURenderPass(pass) GLOB.cleared = true } return } render_pass := sdl.BeginGPURenderPass( cmd_buffer, &sdl.GPUColorTargetInfo { texture = render_texture, clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]}, load_op = GLOB.cleared ? .LOAD : .CLEAR, store_op = .STORE, }, 1, nil, ) GLOB.cleared = true sdl.BindGPUGraphicsPipeline(render_pass, GLOB.core_2d.sdl_pipeline) // Bind storage buffer (read by vertex shader in SDF mode) sdl.BindGPUVertexStorageBuffers(render_pass, 0, ([^]^sdl.GPUBuffer)(&GLOB.core_2d.primitive_buffer.gpu), 1) // Always bind index buffer — harmless if no indexed draws are issued sdl.BindGPUIndexBuffer( render_pass, sdl.GPUBufferBinding{buffer = GLOB.core_2d.index_buffer.gpu, offset = 0}, ._32BIT, ) // Shorthand aliases for frequently-used pipeline resources main_vert_buf := GLOB.core_2d.vertex_buffer.gpu unit_quad := GLOB.core_2d.unit_quad_buffer white_texture := GLOB.core_2d.white_texture sampler := GLOB.core_2d.sampler width := f32(swapchain_width) height := f32(swapchain_height) // Initial GPU state: tessellated mode, main vertex buffer, no atlas bound yet push_globals(cmd_buffer, width, height, .Tessellated) sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1) current_mode: Core_2D_Mode = .Tessellated current_vert_buf := main_vert_buf current_atlas: ^sdl.GPUTexture current_sampler := sampler // Text vertices live after shape vertices in the GPU vertex buffer text_vertex_gpu_base := u32(len(GLOB.tmp_shape_verts)) for &scissor in GLOB.scissors[layer.scissor_start:][:layer.scissor_len] { // Intersect this scissor's sub-batch span with the requested range. scissor_start := int(scissor.sub_batch_start) scissor_end := scissor_start + int(scissor.sub_batch_len) effective_start := max(scissor_start, range_start_abs) effective_end := min(scissor_end, range_end_abs) if effective_start >= effective_end do continue sdl.SetGPUScissor(render_pass, scissor.bounds) for abs_idx in effective_start ..< effective_end { batch := &GLOB.tmp_sub_batches[abs_idx] switch batch.kind { case .Tessellated: if current_mode != .Tessellated { push_globals(cmd_buffer, width, height, .Tessellated) current_mode = .Tessellated } if current_vert_buf != main_vert_buf { sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1) current_vert_buf = main_vert_buf } // Determine texture and sampler for this batch batch_texture: ^sdl.GPUTexture = white_texture batch_sampler: ^sdl.GPUSampler = sampler if batch.texture_id != INVALID_TEXTURE { if bound_texture := texture_gpu_handle(batch.texture_id); bound_texture != nil { batch_texture = bound_texture } batch_sampler = get_sampler(batch.sampler) } if current_atlas != batch_texture || current_sampler != batch_sampler { sdl.BindGPUFragmentSamplers( render_pass, 0, &sdl.GPUTextureSamplerBinding{texture = batch_texture, sampler = batch_sampler}, 1, ) current_atlas = batch_texture current_sampler = batch_sampler } 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 } if current_vert_buf != main_vert_buf { sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1) current_vert_buf = main_vert_buf } text_batch := &GLOB.tmp_text_batches[batch.offset] if current_atlas != text_batch.atlas_texture { sdl.BindGPUFragmentSamplers( render_pass, 0, &sdl.GPUTextureSamplerBinding{texture = text_batch.atlas_texture, sampler = sampler}, 1, ) current_atlas = text_batch.atlas_texture } sdl.DrawGPUIndexedPrimitives( render_pass, text_batch.index_count, 1, text_batch.index_start, i32(text_vertex_gpu_base + text_batch.vertex_start), 0, ) case .SDF: if current_mode != .SDF { push_globals(cmd_buffer, width, height, .SDF) current_mode = .SDF } if current_vert_buf != unit_quad { sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = unit_quad, offset = 0}, 1) current_vert_buf = unit_quad } // Determine texture and sampler for this batch batch_texture: ^sdl.GPUTexture = white_texture batch_sampler: ^sdl.GPUSampler = sampler if batch.texture_id != INVALID_TEXTURE { if bound_texture := texture_gpu_handle(batch.texture_id); bound_texture != nil { batch_texture = bound_texture } batch_sampler = get_sampler(batch.sampler) } if current_atlas != batch_texture || current_sampler != batch_sampler { sdl.BindGPUFragmentSamplers( render_pass, 0, &sdl.GPUTextureSamplerBinding{texture = batch_texture, sampler = batch_sampler}, 1, ) current_atlas = batch_texture current_sampler = batch_sampler } sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset) case .Backdrop: // Always a no-op here. Backdrop sub-batches are dispatched by run_backdrop_bracket; // when this proc encounters one (only possible in Pass B, since Pass A and the no- // backdrop fast path both stop their range before any .Backdrop index), we skip it. } } } sdl.EndGPURenderPass(render_pass) } // --------------------------------------------------------------------------------------------------------------------- // ----- Submission helpers ------------ // --------------------------------------------------------------------------------------------------------------------- // Submit shape vertices (colored triangles) to the given layer for rendering. // TODO: Should probably be renamed to better match tesselated naming conventions in the library. prepare_shape :: proc(layer: ^Layer, vertices: []Vertex_2D) { 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, .Tessellated, offset, u32(len(vertices))) } // Submit an SDF primitive to the given layer for rendering. Requires the caller to build a // Core_2D_Primitive directly, which is the internal GPU-layout struct. //INTERNAL prepare_sdf_primitive :: proc(layer: ^Layer, prim: Core_2D_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 an SDF primitive with optional texture binding. // The texture-aware counterpart of `prepare_sdf_primitive`; lets shape procs route a // texture_id and sampler into the sub-batch without growing the public API. //INTERNAL prepare_sdf_primitive_ex :: proc( layer: ^Layer, prim: Core_2D_Primitive, texture_id: Texture_Id = INVALID_TEXTURE, sampler: Sampler_Preset = DFT_SAMPLER, ) { 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, texture_id, sampler) } // Submit a text element to the given layer for rendering. // Copies SDL_ttf vertices directly (with baked position) and copies indices for indexed drawing. //INTERNAL 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) // Premultiply text color once — reused across all glyph vertices. pm_color := premultiply_color(text.color) 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_2D{position = {pos.x + base_x, -pos.y + base_y}, uv = {uv.x, uv.y}, color = pm_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, Text_Batch { 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. //INTERNAL 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] // Premultiply text color once — reused across all glyph vertices. pm_color := premultiply_color(text.color) 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_2D{position = apply_transform(transform, {pos.x, -pos.y}), uv = {uv.x, uv.y}, color = pm_color}, ) } append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices]) batch_idx := u32(len(GLOB.tmp_text_batches)) append( &GLOB.tmp_text_batches, Text_Batch { 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 } } // --------------------------------------------------------------------------------------------------------------------- // ----- Primitive builders ------------ // --------------------------------------------------------------------------------------------------------------------- //----- Internal helpers ---------------------------------- // Resolve Texture_Fill zero-initialized fields to their defaults. // Odin structs zero-initialize; Color{} and Rectangle{} are all-zero which is not a // useful tint or UV rect. This proc substitutes sensible defaults for zero values. //INTERNAL resolve_texture_defaults :: #force_inline proc( tf: Texture_Fill, ) -> ( tint: Color, uv: Rectangle, sampler: Sampler_Preset, ) { tint = tf.tint == Color{} ? DFT_TINT : tf.tint uv = tf.uv_rect == Rectangle{} ? DFT_UV_RECT : tf.uv_rect sampler = tf.sampler return } // Compute the visual center of a center-parametrized shape after applying // Convention B origin semantics: `center` is where the origin-point lands in // world space; the visual center is offset by -origin and then rotated around // the landing point. // visual_center = center + R(θ) · (-origin) // When θ=0: visual_center = center - origin (pure positioning shift). // When origin={0,0}: visual_center = center (no change). //INTERNAL compute_pivot_center :: proc(center: Vec2, origin: Vec2, sin_angle, cos_angle: f32) -> Vec2 { if origin == {0, 0} do return center return( center + {cos_angle * (-origin.x) - sin_angle * (-origin.y), sin_angle * (-origin.x) + cos_angle * (-origin.y)} \ ) } // Compute the AABB half-extents of a rectangle with half-size (half_width, half_height) rotated by the given cos/sin. //INTERNAL rotated_aabb_half_extents :: proc(half_width, half_height, cos_angle, sin_angle: f32) -> [2]f32 { cos_abs := abs(cos_angle) sin_abs := abs(sin_angle) return {half_width * cos_abs + half_height * sin_abs, half_width * sin_abs + half_height * cos_abs} } // Pack sin/cos into the Core_2D_Primitive.rotation_sc field as two f16 values. //INTERNAL pack_rotation_sc :: #force_inline proc(sin_angle, cos_angle: f32) -> u32 { return pack_f16_pair(f16(sin_angle), f16(cos_angle)) } //----- Shape builders ---------------------------------- // Build an RRect Core_2D_Primitive with bounds, params, and rotation computed from rectangle geometry. // The caller sets color, flags, and uv fields on the returned primitive before submitting. //INTERNAL build_rrect_primitive :: proc( rect: Rectangle, radii: Rectangle_Radii, origin: Vec2, rotation: f32, feather_px: f32, ) -> Core_2D_Primitive { max_radius := min(rect.width, rect.height) * 0.5 clamped_top_left := clamp(radii.top_left, 0, max_radius) clamped_top_right := clamp(radii.top_right, 0, max_radius) 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 dpi_scale := GLOB.dpi_scaling half_width := rect.width * 0.5 half_height := rect.height * 0.5 center_x := rect.x + half_width - origin.x center_y := rect.y + half_height - origin.y sin_angle: f32 = 0 cos_angle: f32 = 1 has_rotation := false if needs_transform(origin, rotation) { rotation_radians := math.to_radians(rotation) sin_angle, cos_angle = math.sincos(rotation_radians) has_rotation = rotation != 0 transform := build_pivot_rotation_sc({rect.x + origin.x, rect.y + origin.y}, origin, cos_angle, sin_angle) new_center := apply_transform(transform, {half_width, half_height}) center_x = new_center.x center_y = new_center.y } bounds_half_width, bounds_half_height := half_width, half_height if has_rotation { expanded := rotated_aabb_half_extents(half_width, half_height, cos_angle, sin_angle) bounds_half_width = expanded.x bounds_half_height = expanded.y } prim := Core_2D_Primitive { bounds = { center_x - bounds_half_width - padding, center_y - bounds_half_height - padding, center_x + bounds_half_width + padding, center_y + bounds_half_height + padding, }, 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 = { clamped_bottom_right * dpi_scale, clamped_top_right * dpi_scale, clamped_bottom_left * dpi_scale, clamped_top_left * dpi_scale, }, half_feather = half_feather, } return prim } // Build an RRect Core_2D_Primitive for a circle (fully-rounded square RRect). // The caller sets color, flags, and uv fields on the returned primitive before submitting. //INTERNAL build_circle_primitive :: proc( center: Vec2, radius: f32, origin: Vec2, rotation: f32, feather_px: f32, ) -> Core_2D_Primitive { half_feather := feather_px * 0.5 padding := half_feather / GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling actual_center := center if origin != {0, 0} { sin_a, cos_a := math.sincos(math.to_radians(rotation)) actual_center = compute_pivot_center(center, origin, sin_a, cos_a) } prim := Core_2D_Primitive { bounds = { actual_center.x - radius - padding, actual_center.y - radius - padding, actual_center.x + radius + padding, actual_center.y + radius + padding, }, } scaled_radius := 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, } return prim } // Build an Ellipse Core_2D_Primitive with bounds, params, and rotation computed from ellipse geometry. // The caller sets color, flags, and uv fields on the returned primitive before submitting. //INTERNAL build_ellipse_primitive :: proc( center: Vec2, radius_horizontal, radius_vertical: f32, origin: Vec2, rotation: f32, feather_px: f32, ) -> Core_2D_Primitive { half_feather := feather_px * 0.5 padding := half_feather / GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling actual_center := center sin_angle: f32 = 0 cos_angle: f32 = 1 has_rotation := false if needs_transform(origin, rotation) { rotation_radians := math.to_radians(rotation) sin_angle, cos_angle = math.sincos(rotation_radians) actual_center = compute_pivot_center(center, origin, sin_angle, cos_angle) has_rotation = rotation != 0 } bound_horizontal, bound_vertical := radius_horizontal, radius_vertical if has_rotation { expanded := rotated_aabb_half_extents(radius_horizontal, radius_vertical, cos_angle, sin_angle) bound_horizontal = expanded.x bound_vertical = expanded.y } prim := Core_2D_Primitive { bounds = { actual_center.x - bound_horizontal - padding, actual_center.y - bound_vertical - padding, actual_center.x + bound_horizontal + padding, actual_center.y + bound_vertical + padding, }, 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, } return prim } // Build an NGon Core_2D_Primitive with bounds, params, and rotation computed from polygon geometry. // The caller sets color, flags, and uv fields on the returned primitive before submitting. //INTERNAL build_polygon_primitive :: proc( center: Vec2, sides: int, radius: f32, origin: Vec2, rotation: f32, feather_px: f32, ) -> Core_2D_Primitive { half_feather := feather_px * 0.5 padding := half_feather / GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling actual_center := center if origin != {0, 0} && rotation != 0 { sin_a, cos_a := math.sincos(math.to_radians(rotation)) actual_center = compute_pivot_center(center, origin, sin_a, cos_a) } rotation_radians := math.to_radians(rotation) sin_rot, cos_rot := math.sincos(rotation_radians) prim := Core_2D_Primitive { bounds = { actual_center.x - radius - padding, actual_center.y - radius - padding, actual_center.x + radius + padding, actual_center.y + radius + padding, }, 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, } return prim } // Build a Ring_Arc Core_2D_Primitive with bounds and params computed from ring/arc geometry. // Pre-computes the angular boundary normals on the CPU so the fragment shader needs // no per-pixel sin/cos. The radial SDF uses max(inner-r, r-outer) which correctly // handles pie slices (inner_radius = 0) and full rings. // The caller sets color, flags, and uv fields on the returned primitive before submitting. //INTERNAL build_ring_arc_primitive :: proc( center: Vec2, inner_radius, outer_radius: f32, start_angle: f32, end_angle: f32, origin: Vec2, rotation: f32, feather_px: f32, ) -> ( Core_2D_Primitive, Shape_Flags, ) { half_feather := feather_px * 0.5 padding := half_feather / GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling actual_center := center rotation_offset: f32 = 0 if needs_transform(origin, rotation) { sin_a, cos_a := math.sincos(math.to_radians(rotation)) actual_center = compute_pivot_center(center, origin, sin_a, cos_a) rotation_offset = math.to_radians(rotation) } start_rad := math.to_radians(start_angle) + rotation_offset end_rad := math.to_radians(end_angle) + rotation_offset // Normalize arc span to [0, 2π] arc_span := end_rad - start_rad if arc_span < 0 { arc_span += 2 * math.PI } // Pre-compute edge normals and arc flags on CPU — no per-pixel trig needed. // arc_flags: {} = full ring, {.Arc_Narrow} = span ≤ π (intersect), {.Arc_Wide} = span > π (union) arc_flags: Shape_Flags = {} normal_start: [2]f32 = {} normal_end: [2]f32 = {} if arc_span < 2 * math.PI - 0.001 { sin_start, cos_start := math.sincos(start_rad) sin_end, cos_end := math.sincos(end_rad) normal_start = {sin_start, -cos_start} normal_end = {-sin_end, cos_end} arc_flags = arc_span <= math.PI ? {.Arc_Narrow} : {.Arc_Wide} } prim := Core_2D_Primitive { bounds = { actual_center.x - outer_radius - padding, actual_center.y - outer_radius - padding, actual_center.x + outer_radius + padding, actual_center.y + outer_radius + padding, }, } 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, } return prim, arc_flags } //----- Brush and outline ---------------------------------- // Apply brush fill and outline to a primitive, then submit it. // Dispatches to the correct sub-batch based on the Brush variant. // All parameters (outline_width) are in logical pixels, matching the rest of the public API. // The helper converts to physical pixels for GPU packing internally. //INTERNAL apply_brush_and_outline :: proc( layer: ^Layer, prim: ^Core_2D_Primitive, kind: Shape_Kind, brush: Brush, outline_color: Color, outline_width: f32, extra_flags: Shape_Flags = {}, ) { flags: Shape_Flags = extra_flags // Fill — determined by the Brush variant. texture_id := INVALID_TEXTURE sampler := DFT_SAMPLER switch b in brush { case Color: prim.color = b case Linear_Gradient: flags += {.Gradient} prim.color = b.start_color prim.effects.gradient_color = b.end_color rad := math.to_radians(b.angle) sin_a, cos_a := math.sincos(rad) prim.effects.gradient_dir_sc = pack_f16_pair(f16(cos_a), f16(sin_a)) case Radial_Gradient: flags += {.Gradient, .Gradient_Radial} prim.color = b.inner_color prim.effects.gradient_color = b.outer_color case Texture_Fill: flags += {.Textured} tint, uv, sam := resolve_texture_defaults(b) prim.color = tint prim.uv_rect = {uv.x, uv.y, uv.width, uv.height} texture_id = b.id sampler = sam } // Outline — orthogonal to all Brush variants. if outline_width > 0 { flags += {.Outline} prim.effects.outline_color = outline_color prim.effects.outline_packed = pack_f16_pair(f16(outline_width * GLOB.dpi_scaling), 0) // Expand bounds to contain the outline (bounds are in logical pixels) prim.bounds[0] -= outline_width prim.bounds[1] -= outline_width prim.bounds[2] += outline_width prim.bounds[3] += outline_width } // Set .Rotated flag if rotation_sc was populated by the build proc if prim.rotation_sc != 0 { flags += {.Rotated} } prim.flags = pack_kind_flags(kind, flags) prepare_sdf_primitive_ex(layer, prim^, texture_id, sampler) } // --------------------------------------------------------------------------------------------------------------------- // ----- Public draw procs ------------ // --------------------------------------------------------------------------------------------------------------------- // Draw a filled rectangle via SDF with optional per-corner rounding radii. // Use `uniform_radii(rect, roundness)` to compute uniform radii from a 0–1 fraction. // // Origin semantics: // `origin` is a local offset from the rect's top-left corner that selects both the positioning // anchor and the rotation pivot. `rect.x, rect.y` specifies where that anchor point lands in // world space. When `origin = {0, 0}` (default), `rect.x, rect.y` is the top-left corner. // Rotation always occurs around the anchor point. rectangle :: proc( layer: ^Layer, rect: Rectangle, brush: Brush, outline_color: Color = {}, outline_width: f32 = 0, radii: Rectangle_Radii = {}, origin: Vec2 = {}, rotation: f32 = 0, feather_px: f32 = DFT_FEATHER_PX, ) { prim := build_rrect_primitive(rect, radii, origin, rotation, feather_px) apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width) } // Draw a filled circle via SDF (emitted as a fully-rounded RRect). // // Origin semantics (Convention B): // `origin` is a local offset from the shape's center that selects both the positioning anchor // and the rotation pivot. The `center` parameter specifies where that anchor point lands in // world space. When `origin = {0, 0}` (default), `center` is the visual center. // When `origin = {r, 0}`, the point `r` pixels to the right of the shape center lands at // `center`, shifting the shape left by `r`. circle :: proc( layer: ^Layer, center: Vec2, radius: f32, brush: Brush, outline_color: Color = {}, outline_width: f32 = 0, origin: Vec2 = {}, rotation: f32 = 0, feather_px: f32 = DFT_FEATHER_PX, ) { prim := build_circle_primitive(center, radius, origin, rotation, feather_px) apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width) } // Draw a filled ellipse via SDF. // Origin semantics: see `circle`. ellipse :: proc( layer: ^Layer, center: Vec2, radius_horizontal, radius_vertical: f32, brush: Brush, outline_color: Color = {}, outline_width: f32 = 0, origin: Vec2 = {}, rotation: f32 = 0, feather_px: f32 = DFT_FEATHER_PX, ) { prim := build_ellipse_primitive(center, radius_horizontal, radius_vertical, origin, rotation, feather_px) apply_brush_and_outline(layer, &prim, .Ellipse, brush, outline_color, outline_width) } // Draw a filled regular polygon via SDF. // `sides` must be >= 3. The polygon is inscribed in a circle of the given `radius`. // Origin semantics: see `circle`. polygon :: proc( layer: ^Layer, center: Vec2, sides: int, radius: f32, brush: Brush, outline_color: Color = {}, outline_width: f32 = 0, origin: Vec2 = {}, rotation: f32 = 0, feather_px: f32 = DFT_FEATHER_PX, ) { if sides < 3 do return prim := build_polygon_primitive(center, sides, radius, origin, rotation, feather_px) apply_brush_and_outline(layer, &prim, .NGon, brush, outline_color, outline_width) } // Draw a ring, arc, or pie slice via SDF. // Full ring by default. Pass start_angle/end_angle (degrees) for partial arcs. // Use inner_radius = 0 for pie slices (sectors). // Origin semantics: see `circle`. ring :: proc( layer: ^Layer, center: Vec2, inner_radius, outer_radius: f32, brush: Brush, outline_color: Color = {}, outline_width: f32 = 0, start_angle: f32 = 0, end_angle: f32 = DFT_CIRC_END_ANGLE, origin: Vec2 = {}, rotation: f32 = 0, feather_px: f32 = DFT_FEATHER_PX, ) { prim, arc_flags := build_ring_arc_primitive( center, inner_radius, outer_radius, start_angle, end_angle, origin, rotation, feather_px, ) apply_brush_and_outline(layer, &prim, .Ring_Arc, brush, outline_color, outline_width, arc_flags) } // Draw a line segment via SDF (emitted as a rotated capsule-shaped RRect). // Round caps are produced by setting corner radii equal to half the thickness. line :: proc( layer: ^Layer, start_position, end_position: Vec2, brush: Brush, thickness: f32 = DFT_STROKE_THICKNESS, outline_color: Color = {}, outline_width: f32 = 0, feather_px: f32 = DFT_FEATHER_PX, ) { delta_x := end_position.x - start_position.x delta_y := end_position.y - start_position.y seg_length := math.sqrt(delta_x * delta_x + delta_y * delta_y) if seg_length < 0.0001 do return rotation_radians := math.atan2(delta_y, delta_x) sin_angle, cos_angle := math.sincos(rotation_radians) center_x := (start_position.x + end_position.x) * 0.5 center_y := (start_position.y + end_position.y) * 0.5 half_length := seg_length * 0.5 half_thickness := thickness * 0.5 cap_radius := half_thickness half_feather := feather_px * 0.5 padding := half_feather / GLOB.dpi_scaling dpi_scale := GLOB.dpi_scaling // Expand bounds for rotation bounds_half := rotated_aabb_half_extents(half_length + cap_radius, half_thickness, cos_angle, sin_angle) prim := Core_2D_Primitive { bounds = { center_x - bounds_half.x - padding, center_y - bounds_half.y - padding, center_x + bounds_half.x + padding, center_y + bounds_half.y + padding, }, 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 = { cap_radius * dpi_scale, cap_radius * dpi_scale, cap_radius * dpi_scale, cap_radius * dpi_scale, }, half_feather = half_feather, } apply_brush_and_outline(layer, &prim, .RRect, brush, outline_color, outline_width) } // Draw a line strip via decomposed SDF line segments. line_strip :: proc( layer: ^Layer, points: []Vec2, brush: Brush, thickness: f32 = DFT_STROKE_THICKNESS, outline_color: Color = {}, outline_width: f32 = 0, feather_px: f32 = DFT_FEATHER_PX, ) { 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) } }