package draw import "core:log" import "core:mem" import sdl "vendor:sdl3" Texture_Id :: distinct u32 INVALID_TEXTURE :: Texture_Id(0) // Slot 0 is reserved/unused Texture_Kind :: enum u8 { Static, // Uploaded once, never changes (QR codes, decoded PNGs, icons) Dynamic, // Updatable via update_texture_region Stream, // Frequent full re-uploads (video, procedural) } Sampler_Preset :: enum u8 { Nearest_Clamp, Linear_Clamp, Nearest_Repeat, Linear_Repeat, } SAMPLER_PRESET_COUNT :: 4 Fit_Mode :: enum u8 { Stretch, // Fill rect, may distort aspect ratio (default) Fit, // Preserve aspect, letterbox (may leave margins) Fill, // Preserve aspect, center-crop (may crop edges) Tile, // Repeat at native texture size Center, // 1:1 pixel size, centered, no scaling } Texture_Desc :: struct { width: u32, height: u32, depth_or_layers: u32, type: sdl.GPUTextureType, format: sdl.GPUTextureFormat, usage: sdl.GPUTextureUsageFlags, mip_levels: u32, kind: Texture_Kind, } // Internal slot — not exported. @(private) Texture_Slot :: struct { gpu_texture: ^sdl.GPUTexture, desc: Texture_Desc, generation: u32, } // State stored in GLOB // This file references: // GLOB.device : ^sdl.GPUDevice // GLOB.texture_slots : [dynamic]Texture_Slot // GLOB.texture_free_list : [dynamic]u32 // GLOB.pending_texture_releases : [dynamic]Texture_Id // GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler Clay_Image_Data :: struct { texture_id: Texture_Id, fit: Fit_Mode, tint: Color, } clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data { return {texture_id = id, fit = fit, tint = tint} } // --------------------------------------------------------------------------------------------------------------------- // ----- Registration ------------- // --------------------------------------------------------------------------------------------------------------------- // Register a texture. Draw owns the GPU resource and releases it on unregister. // `data` is tightly-packed row-major bytes matching desc.format. // The caller may free `data` immediately after this proc returns. @(require_results) register_texture :: proc(desc: Texture_Desc, data: []u8) -> (id: Texture_Id, ok: bool) { device := GLOB.device if device == nil { log.error("register_texture called before draw.init()") return INVALID_TEXTURE, false } assert(desc.width > 0, "Texture_Desc.width must be > 0") assert(desc.height > 0, "Texture_Desc.height must be > 0") assert(desc.depth_or_layers > 0, "Texture_Desc.depth_or_layers must be > 0") assert(desc.mip_levels > 0, "Texture_Desc.mip_levels must be > 0") assert(desc.usage != {}, "Texture_Desc.usage must not be empty (e.g. {.SAMPLER})") // Create the GPU texture gpu_texture := sdl.CreateGPUTexture( device, sdl.GPUTextureCreateInfo { type = desc.type, format = desc.format, usage = desc.usage, width = desc.width, height = desc.height, layer_count_or_depth = desc.depth_or_layers, num_levels = desc.mip_levels, sample_count = ._1, }, ) if gpu_texture == nil { log.errorf("Failed to create GPU texture (%dx%d): %s", desc.width, desc.height, sdl.GetError()) return INVALID_TEXTURE, false } // Upload pixel data via a transfer buffer if len(data) > 0 { data_size := u32(len(data)) transfer := sdl.CreateGPUTransferBuffer( device, sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = data_size}, ) if transfer == nil { log.errorf("Failed to create texture transfer buffer: %s", sdl.GetError()) sdl.ReleaseGPUTexture(device, gpu_texture) return INVALID_TEXTURE, false } defer sdl.ReleaseGPUTransferBuffer(device, transfer) mapped := sdl.MapGPUTransferBuffer(device, transfer, false) if mapped == nil { log.errorf("Failed to map texture transfer buffer: %s", sdl.GetError()) sdl.ReleaseGPUTexture(device, gpu_texture) return INVALID_TEXTURE, false } mem.copy(mapped, raw_data(data), int(data_size)) sdl.UnmapGPUTransferBuffer(device, transfer) cmd_buffer := sdl.AcquireGPUCommandBuffer(device) if cmd_buffer == nil { log.errorf("Failed to acquire command buffer for texture upload: %s", sdl.GetError()) sdl.ReleaseGPUTexture(device, gpu_texture) return INVALID_TEXTURE, false } copy_pass := sdl.BeginGPUCopyPass(cmd_buffer) sdl.UploadToGPUTexture( copy_pass, sdl.GPUTextureTransferInfo{transfer_buffer = transfer}, sdl.GPUTextureRegion{texture = gpu_texture, w = desc.width, h = desc.height, d = desc.depth_or_layers}, false, ) sdl.EndGPUCopyPass(copy_pass) if !sdl.SubmitGPUCommandBuffer(cmd_buffer) { log.errorf("Failed to submit texture upload: %s", sdl.GetError()) sdl.ReleaseGPUTexture(device, gpu_texture) return INVALID_TEXTURE, false } } // Allocate a slot (reuse from free list or append) slot_index: u32 if len(GLOB.texture_free_list) > 0 { slot_index = pop(&GLOB.texture_free_list) GLOB.texture_slots[slot_index] = Texture_Slot { gpu_texture = gpu_texture, desc = desc, generation = GLOB.texture_slots[slot_index].generation + 1, } } else { slot_index = u32(len(GLOB.texture_slots)) append(&GLOB.texture_slots, Texture_Slot{gpu_texture = gpu_texture, desc = desc, generation = 1}) } return Texture_Id(slot_index), true } // Queue a texture for release at the end of the current frame. // The GPU resource is not freed immediately — see "Deferred release" in the README. unregister_texture :: proc(id: Texture_Id) { if id == INVALID_TEXTURE do return append(&GLOB.pending_texture_releases, id) } // Re-upload a sub-region of a Dynamic texture. update_texture_region :: proc(id: Texture_Id, region: Rectangle, data: []u8) { if id == INVALID_TEXTURE do return slot := &GLOB.texture_slots[u32(id)] if slot.gpu_texture == nil do return device := GLOB.device data_size := u32(len(data)) if data_size == 0 do return transfer := sdl.CreateGPUTransferBuffer( device, sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = data_size}, ) if transfer == nil { log.errorf("Failed to create transfer buffer for texture region update: %s", sdl.GetError()) return } defer sdl.ReleaseGPUTransferBuffer(device, transfer) mapped := sdl.MapGPUTransferBuffer(device, transfer, false) if mapped == nil { log.errorf("Failed to map transfer buffer for texture region update: %s", sdl.GetError()) return } mem.copy(mapped, raw_data(data), int(data_size)) sdl.UnmapGPUTransferBuffer(device, transfer) cmd_buffer := sdl.AcquireGPUCommandBuffer(device) if cmd_buffer == nil { log.errorf("Failed to acquire command buffer for texture region update: %s", sdl.GetError()) return } copy_pass := sdl.BeginGPUCopyPass(cmd_buffer) sdl.UploadToGPUTexture( copy_pass, sdl.GPUTextureTransferInfo{transfer_buffer = transfer}, sdl.GPUTextureRegion { texture = slot.gpu_texture, x = u32(region.x), y = u32(region.y), w = u32(region.width), h = u32(region.height), d = 1, }, false, ) sdl.EndGPUCopyPass(copy_pass) if !sdl.SubmitGPUCommandBuffer(cmd_buffer) { log.errorf("Failed to submit texture region update: %s", sdl.GetError()) } } // --------------------------------------------------------------------------------------------------------------------- // ----- Helpers ------------- // --------------------------------------------------------------------------------------------------------------------- // Compute UV rect, recommended sampler, and inner rect for a given fit mode. // `rect` is the target drawing area; `texture_id` identifies the texture whose // pixel dimensions are looked up via texture_size(). // For Fit mode, `inner_rect` is smaller than `rect` (centered). For all other modes, `inner_rect == rect`. fit_params :: proc( fit: Fit_Mode, rect: Rectangle, texture_id: Texture_Id, ) -> ( uv_rect: Rectangle, sampler: Sampler_Preset, inner_rect: Rectangle, ) { size := texture_size(texture_id) texture_width := f32(size.x) texture_height := f32(size.y) rect_width := rect.width rect_height := rect.height inner_rect = rect if texture_width == 0 || texture_height == 0 || rect_width == 0 || rect_height == 0 { return {0, 0, 1, 1}, .Linear_Clamp, inner_rect } texture_aspect := texture_width / texture_height rect_aspect := rect_width / rect_height switch fit { case .Stretch: return {0, 0, 1, 1}, .Linear_Clamp, inner_rect case .Fill: if texture_aspect > rect_aspect { // Texture wider than rect — crop sides scale := rect_aspect / texture_aspect margin := (1 - scale) * 0.5 return {margin, 0, 1 - margin, 1}, .Linear_Clamp, inner_rect } else { // Texture taller than rect — crop top/bottom scale := texture_aspect / rect_aspect margin := (1 - scale) * 0.5 return {0, margin, 1, 1 - margin}, .Linear_Clamp, inner_rect } case .Fit: // Preserve aspect, fit inside rect. Returns a shrunken inner_rect. if texture_aspect > rect_aspect { // Image wider — letterbox top/bottom fit_height := rect_width / texture_aspect padding := (rect_height - fit_height) * 0.5 inner_rect = Rectangle{rect.x, rect.y + padding, rect_width, fit_height} } else { // Image taller — letterbox left/right fit_width := rect_height * texture_aspect padding := (rect_width - fit_width) * 0.5 inner_rect = Rectangle{rect.x + padding, rect.y, fit_width, rect_height} } return {0, 0, 1, 1}, .Linear_Clamp, inner_rect case .Tile: uv_width := rect_width / texture_width uv_height := rect_height / texture_height return {0, 0, uv_width, uv_height}, .Linear_Repeat, inner_rect case .Center: u_half := rect_width / (2 * texture_width) v_half := rect_height / (2 * texture_height) return {0.5 - u_half, 0.5 - v_half, 0.5 + u_half, 0.5 + v_half}, .Nearest_Clamp, inner_rect } return {0, 0, 1, 1}, .Linear_Clamp, inner_rect } texture_size :: proc(id: Texture_Id) -> [2]u32 { if id == INVALID_TEXTURE do return {0, 0} slot := &GLOB.texture_slots[u32(id)] return {slot.desc.width, slot.desc.height} } texture_format :: proc(id: Texture_Id) -> sdl.GPUTextureFormat { if id == INVALID_TEXTURE do return .INVALID return GLOB.texture_slots[u32(id)].desc.format } texture_kind :: proc(id: Texture_Id) -> Texture_Kind { if id == INVALID_TEXTURE do return .Static return GLOB.texture_slots[u32(id)].desc.kind } // Internal: get the raw GPU texture pointer for binding during draw. @(private) texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture { if id == INVALID_TEXTURE do return nil idx := u32(id) if idx >= u32(len(GLOB.texture_slots)) do return nil return GLOB.texture_slots[idx].gpu_texture } // Deferred release (called from draw.end / clear_global) @(private) process_pending_texture_releases :: proc() { device := GLOB.device for id in GLOB.pending_texture_releases { idx := u32(id) if idx >= u32(len(GLOB.texture_slots)) do continue slot := &GLOB.texture_slots[idx] if slot.gpu_texture != nil { sdl.ReleaseGPUTexture(device, slot.gpu_texture) slot.gpu_texture = nil } slot.generation += 1 append(&GLOB.texture_free_list, idx) } clear(&GLOB.pending_texture_releases) } @(private) get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler { idx := int(preset) if GLOB.samplers[idx] != nil do return GLOB.samplers[idx] // Lazily create min_filter, mag_filter: sdl.GPUFilter address_mode: sdl.GPUSamplerAddressMode switch preset { case .Nearest_Clamp: min_filter = .NEAREST; mag_filter = .NEAREST; address_mode = .CLAMP_TO_EDGE case .Linear_Clamp: min_filter = .LINEAR; mag_filter = .LINEAR; address_mode = .CLAMP_TO_EDGE case .Nearest_Repeat: min_filter = .NEAREST; mag_filter = .NEAREST; address_mode = .REPEAT case .Linear_Repeat: min_filter = .LINEAR; mag_filter = .LINEAR; address_mode = .REPEAT } sampler := sdl.CreateGPUSampler( GLOB.device, sdl.GPUSamplerCreateInfo { min_filter = min_filter, mag_filter = mag_filter, mipmap_mode = .LINEAR, address_mode_u = address_mode, address_mode_v = address_mode, address_mode_w = address_mode, }, ) if sampler == nil { log.errorf("Failed to create sampler preset %v: %s", preset, sdl.GetError()) return GLOB.pipeline_2d_base.sampler // fallback to existing default sampler } GLOB.samplers[idx] = sampler return sampler } // Internal: destroy all sampler pool entries. Called from draw.destroy(). @(private) destroy_sampler_pool :: proc() { device := GLOB.device for &s in GLOB.samplers { if s != nil { sdl.ReleaseGPUSampler(device, s) s = nil } } } // Internal: destroy all registered textures. Called from draw.destroy(). @(private) destroy_all_textures :: proc() { device := GLOB.device for &slot in GLOB.texture_slots { if slot.gpu_texture != nil { sdl.ReleaseGPUTexture(device, slot.gpu_texture) slot.gpu_texture = nil } } delete(GLOB.texture_slots) delete(GLOB.texture_free_list) delete(GLOB.pending_texture_releases) }