Texture Rendering (#9)
Co-authored-by: Zachary Levy <zachary@sunforge.is> Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
414
draw/textures.odin
Normal file
414
draw/textures.odin
Normal file
@@ -0,0 +1,414 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user