415 lines
13 KiB
Odin
415 lines
13 KiB
Odin
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)
|
|
}
|