package draw import "core:c" import "core:log" import "core:mem" import sdl "vendor:sdl3" Vertex :: struct { position: [2]f32, uv: [2]f32, color: Color, } TextBatch :: struct { atlas_texture: ^sdl.GPUTexture, vertex_start: u32, vertex_count: u32, index_start: u32, index_count: u32, } // ---------------------------------------------------------------------------------------------------------------- // ----- SDF primitive types ----------- // ---------------------------------------------------------------------------------------------------------------- Shape_Kind :: enum u8 { Solid = 0, RRect = 1, Circle = 2, Ellipse = 3, Segment = 4, Ring_Arc = 5, NGon = 6, } Shape_Flag :: enum u8 { Stroke, } Shape_Flags :: bit_set[Shape_Flag;u8] RRect_Params :: struct { half_size: [2]f32, radii: [4]f32, soft_px: f32, stroke_px: f32, } Circle_Params :: struct { radius: f32, soft_px: f32, stroke_px: f32, _: [5]f32, } Ellipse_Params :: struct { radii: [2]f32, soft_px: f32, stroke_px: f32, _: [4]f32, } Segment_Params :: struct { a: [2]f32, b: [2]f32, width: f32, soft_px: f32, _: [2]f32, } Ring_Arc_Params :: struct { inner_radius: f32, outer_radius: f32, start_rad: f32, end_rad: f32, soft_px: f32, _: [3]f32, } NGon_Params :: struct { radius: f32, rotation: f32, sides: f32, soft_px: f32, stroke_px: f32, _: [3]f32, } Shape_Params :: struct #raw_union { rrect: RRect_Params, circle: Circle_Params, ellipse: Ellipse_Params, segment: Segment_Params, ring_arc: Ring_Arc_Params, ngon: NGon_Params, raw: [8]f32, } #assert(size_of(Shape_Params) == 32) // GPU layout: 64 bytes, std430-compatible. The shader declares this as a storage buffer struct. Primitive :: struct { bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI) color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8 kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8) rotation: f32, // 24: shader self-rotation in radians (used by RRect, Ellipse) _pad: f32, // 28: alignment to vec4 boundary params: Shape_Params, // 32: two vec4s of shape params } #assert(size_of(Primitive) == 64) pack_kind_flags :: #force_inline proc(kind: Shape_Kind, flags: Shape_Flags) -> u32 { return u32(kind) | (u32(transmute(u8)flags) << 8) } Pipeline_2D_Base :: 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, } @(private) create_pipeline_2d_base :: proc( device: ^sdl.GPUDevice, window: ^sdl.Window, sample_count: sdl.GPUSampleCount, ) -> ( pipeline: Pipeline_2D_Base, ok: bool, ) { // On failure, clean up any partially-created resources defer if !ok { if pipeline.sampler != nil do sdl.ReleaseGPUSampler(device, pipeline.sampler) if pipeline.white_texture != nil do sdl.ReleaseGPUTexture(device, pipeline.white_texture) if pipeline.unit_quad_buffer != nil do sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer) if pipeline.primitive_buffer.gpu != nil do destroy_buffer(device, &pipeline.primitive_buffer) if pipeline.index_buffer.gpu != nil do destroy_buffer(device, &pipeline.index_buffer) if pipeline.vertex_buffer.gpu != nil do destroy_buffer(device, &pipeline.vertex_buffer) if pipeline.sdl_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.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 pipeline, 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 pipeline, 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 pipeline, 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 = sample_count}, target_info = sdl.GPUGraphicsPipelineTargetInfo { color_target_descriptions = &sdl.GPUColorTargetDescription { format = sdl.GetGPUSwapchainTextureFormat(device, window), blend_state = sdl.GPUColorTargetBlendState { enable_blend = true, enable_color_write_mask = true, src_color_blendfactor = .SRC_ALPHA, dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA, color_blend_op = .ADD, src_alpha_blendfactor = .SRC_ALPHA, 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), }, num_vertex_buffers = 1, vertex_attributes = raw_data(vertex_attributes[:]), num_vertex_attributes = 3, }, } pipeline.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 pipeline.sdl_pipeline == nil { log.errorf("Failed to create draw graphics pipeline: %s", sdl.GetError()) return pipeline, false } // Create vertex buffer vert_buf_ok: bool pipeline.vertex_buffer, vert_buf_ok = create_buffer( device, size_of(Vertex) * BUFFER_INIT_SIZE, sdl.GPUBufferUsageFlags{.VERTEX}, ) if !vert_buf_ok do return pipeline, false // Create index buffer (used by text) idx_buf_ok: bool pipeline.index_buffer, idx_buf_ok = create_buffer( device, size_of(c.int) * BUFFER_INIT_SIZE, sdl.GPUBufferUsageFlags{.INDEX}, ) if !idx_buf_ok do return pipeline, false // Create primitive storage buffer (used by SDF instanced drawing) prim_buf_ok: bool pipeline.primitive_buffer, prim_buf_ok = create_buffer( device, size_of(Primitive) * BUFFER_INIT_SIZE, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, ) if !prim_buf_ok do return pipeline, false // Create static 6-vertex unit quad buffer (two triangles, TRIANGLELIST) pipeline.unit_quad_buffer = sdl.CreateGPUBuffer( device, sdl.GPUBufferCreateInfo{usage = {.VERTEX}, size = 6 * size_of(Vertex)}, ) if pipeline.unit_quad_buffer == nil { log.errorf("Failed to create unit quad buffer: %s", sdl.GetError()) return pipeline, false } // Create 1x1 white pixel texture pipeline.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 pipeline.white_texture == nil { log.errorf("Failed to create white pixel texture: %s", sdl.GetError()) return pipeline, false } // Upload white pixel and unit quad data in a single command buffer white_pixel := [4]u8{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 pipeline, 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 pipeline, false } mem.copy(white_ptr, &white_pixel, size_of(white_pixel)) sdl.UnmapGPUTransferBuffer(device, white_transfer_buf) quad_verts := [6]Vertex { {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 pipeline, 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 pipeline, 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 pipeline, false } upload_pass := sdl.BeginGPUCopyPass(upload_cmd_buffer) sdl.UploadToGPUTexture( upload_pass, sdl.GPUTextureTransferInfo{transfer_buffer = white_transfer_buf}, sdl.GPUTextureRegion{texture = pipeline.white_texture, w = 1, h = 1, d = 1}, false, ) sdl.UploadToGPUBuffer( upload_pass, sdl.GPUTransferBufferLocation{transfer_buffer = quad_transfer_buf}, sdl.GPUBufferRegion{buffer = pipeline.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 pipeline, false } log.debug("White pixel texture and unit quad buffer created and uploaded") // Create sampler (shared by shapes and text) pipeline.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 pipeline.sampler == nil { log.errorf("Could not create GPU sampler: %s", sdl.GetError()) return pipeline, false } log.debug("Done creating unified draw pipeline") return pipeline, true } @(private) 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) shape_vert_size := shape_vert_count * size_of(Vertex) text_vert_size := text_vert_count * size_of(Vertex) grow_buffer_if_needed( device, &GLOB.pipeline_2d_base.vertex_buffer, total_vert_size, sdl.GPUBufferUsageFlags{.VERTEX}, ) vert_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.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.pipeline_2d_base.vertex_buffer.transfer) sdl.UploadToGPUBuffer( pass, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.vertex_buffer.transfer}, sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.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.pipeline_2d_base.index_buffer, index_size, sdl.GPUBufferUsageFlags{.INDEX}, ) idx_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.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.pipeline_2d_base.index_buffer.transfer) sdl.UploadToGPUBuffer( pass, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.index_buffer.transfer}, sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.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(Primitive) grow_buffer_if_needed( device, &GLOB.pipeline_2d_base.primitive_buffer, prim_size, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, ) prim_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.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.pipeline_2d_base.primitive_buffer.transfer) sdl.UploadToGPUBuffer( pass, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.primitive_buffer.transfer}, sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.primitive_buffer.gpu, offset = 0, size = prim_size}, false, ) } } @(private) 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 } 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.pipeline_2d_base.sdl_pipeline) // Bind storage buffer (read by vertex shader in SDF mode) sdl.BindGPUVertexStorageBuffers( render_pass, 0, ([^]^sdl.GPUBuffer)(&GLOB.pipeline_2d_base.primitive_buffer.gpu), 1, ) // Always bind index buffer — harmless if no indexed draws are issued sdl.BindGPUIndexBuffer( render_pass, sdl.GPUBufferBinding{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0}, ._32BIT, ) // Shorthand aliases for frequently-used pipeline resources main_vert_buf := GLOB.pipeline_2d_base.vertex_buffer.gpu unit_quad := GLOB.pipeline_2d_base.unit_quad_buffer white_texture := GLOB.pipeline_2d_base.white_texture sampler := GLOB.pipeline_2d_base.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: Draw_Mode = .Tessellated current_vert_buf := main_vert_buf current_atlas: ^sdl.GPUTexture // 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] { sdl.SetGPUScissor(render_pass, scissor.bounds) for &batch in GLOB.tmp_sub_batches[scissor.sub_batch_start:][:scissor.sub_batch_len] { switch batch.kind { case .Shapes: 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 } if current_atlas != white_texture { sdl.BindGPUFragmentSamplers( render_pass, 0, &sdl.GPUTextureSamplerBinding{texture = white_texture, sampler = sampler}, 1, ) current_atlas = white_texture } 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 } if current_atlas != white_texture { sdl.BindGPUFragmentSamplers( render_pass, 0, &sdl.GPUTextureSamplerBinding{texture = white_texture, sampler = sampler}, 1, ) current_atlas = white_texture } sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset) } } } sdl.EndGPURenderPass(render_pass) } destroy_pipeline_2d_base :: proc(device: ^sdl.GPUDevice, pipeline: ^Pipeline_2D_Base) { destroy_buffer(device, &pipeline.vertex_buffer) destroy_buffer(device, &pipeline.index_buffer) destroy_buffer(device, &pipeline.primitive_buffer) if pipeline.unit_quad_buffer != nil { sdl.ReleaseGPUBuffer(device, pipeline.unit_quad_buffer) } sdl.ReleaseGPUTexture(device, pipeline.white_texture) sdl.ReleaseGPUSampler(device, pipeline.sampler) sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline) }