Files
levlib/draw/draw.odin
Zachary Levy 30b72128b2 Foramatting
2026-04-19 07:37:29 -07:00

752 lines
25 KiB
Odin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package draw
import clay "../vendor/clay"
import "base:runtime"
import "core:c"
import "core:log"
import "core:strings"
import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"
when ODIN_OS == .Darwin {
SHADER_TYPE :: sdl.GPUShaderFormat{.MSL}
ENTRY_POINT :: "main0"
} else {
SHADER_TYPE :: sdl.GPUShaderFormat{.SPIRV}
ENTRY_POINT :: "main"
}
BUFFER_INIT_SIZE :: 256
INITIAL_LAYER_SIZE :: 5
INITIAL_SCISSOR_SIZE :: 10
// ---------------------------------------------------------------------------------------------------------------------
// ----- Color -------------------------
// ---------------------------------------------------------------------------------------------------------------------
Color :: distinct [4]u8
BLACK :: Color{0, 0, 0, 255}
WHITE :: Color{255, 255, 255, 255}
RED :: Color{255, 0, 0, 255}
GREEN :: Color{0, 255, 0, 255}
BLUE :: Color{0, 0, 255, 255}
BLANK :: Color{0, 0, 0, 0}
// Convert clay.Color ([4]c.float in 0255 range) to Color.
color_from_clay :: proc(cc: clay.Color) -> Color {
return Color{u8(cc[0]), u8(cc[1]), u8(cc[2]), u8(cc[3])}
}
// Convert Color to [4]f32 in 0.01.0 range. Useful for SDL interop (e.g. clear color).
color_to_f32 :: proc(c: Color) -> [4]f32 {
INV :: 1.0 / 255.0
return {f32(c[0]) * INV, f32(c[1]) * INV, f32(c[2]) * INV, f32(c[3]) * INV}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Core types --------------------
// ---------------------------------------------------------------------------------------------------------------------
Rectangle :: struct {
x: f32,
y: f32,
w: f32,
h: f32,
}
Sub_Batch_Kind :: enum u8 {
Shapes, // non-indexed, white texture, mode 0
Text, // indexed, atlas texture, mode 0
SDF, // instanced unit quad, white texture, mode 1
}
Sub_Batch :: struct {
kind: Sub_Batch_Kind,
offset: u32, // Shapes: vertex offset; Text: text_batch index; SDF: primitive index
count: u32, // Shapes: vertex count; Text: always 1; SDF: primitive count
}
Layer :: struct {
bounds: Rectangle,
sub_batch_start: u32,
sub_batch_len: u32,
scissor_start: u32,
scissor_len: u32,
}
Scissor :: struct {
bounds: sdl.Rect,
sub_batch_start: u32,
sub_batch_len: u32,
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Global state ------------------
// ---------------------------------------------------------------------------------------------------------------------
GLOB: Global
Global :: struct {
odin_context: runtime.Context,
pipeline_2d_base: Pipeline_2D_Base,
text_cache: Text_Cache,
layers: [dynamic]Layer,
scissors: [dynamic]Scissor,
tmp_shape_verts: [dynamic]Vertex,
tmp_text_verts: [dynamic]Vertex,
tmp_text_indices: [dynamic]c.int,
tmp_text_batches: [dynamic]TextBatch,
tmp_primitives: [dynamic]Primitive,
tmp_sub_batches: [dynamic]Sub_Batch,
clay_mem: [^]u8,
msaa_texture: ^sdl.GPUTexture,
curr_layer_index: uint,
max_layers: int,
max_scissors: int,
max_shape_verts: int,
max_text_verts: int,
max_text_indices: int,
max_text_batches: int,
max_primitives: int,
max_sub_batches: int,
dpi_scaling: f32,
msaa_w: u32,
msaa_h: u32,
sample_count: sdl.GPUSampleCount,
clay_z_index: i16,
cleared: bool,
}
Init_Options :: struct {
// MSAA sample count. Default is ._1 (no MSAA). SDF rendering does not benefit from MSAA
// because SDF fragments compute coverage analytically via `smoothstep`. MSAA helps for
// text glyph edges and tessellated user geometry. Set to ._4 or ._8 for text-heavy UIs,
// or use `MSAA_MAX` to request the highest sample count the GPU supports for the swapchain
// format.
msaa_samples: sdl.GPUSampleCount,
}
// Sentinel value: when passed as msaa_samples, `init` will use the maximum MSAA sample count
// supported by the GPU for the swapchain format.
MSAA_MAX :: sdl.GPUSampleCount(0xFF)
// Initialize the renderer. Returns false if GPU pipeline or text engine creation fails.
@(require_results)
init :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
options: Init_Options = {},
allocator := context.allocator,
odin_context := context,
) -> (
ok: bool,
) {
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
resolved_sample_count := options.msaa_samples
if resolved_sample_count == MSAA_MAX {
resolved_sample_count = max_sample_count(device, window)
}
pipeline, pipeline_ok := create_pipeline_2d_base(device, window, resolved_sample_count)
if !pipeline_ok {
return false
}
text_cache, text_ok := init_text_cache(device, allocator)
if !text_ok {
destroy_pipeline_2d_base(device, &pipeline)
return false
}
GLOB = Global {
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
odin_context = odin_context,
dpi_scaling = sdl.GetWindowDisplayScale(window),
clay_mem = make([^]u8, min_memory_size, allocator = allocator),
sample_count = resolved_sample_count,
pipeline_2d_base = pipeline,
text_cache = text_cache,
}
log.debug("Window DPI scaling:", GLOB.dpi_scaling)
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_mem)
window_width, window_height: c.int
sdl.GetWindowSize(window, &window_width, &window_height)
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
clay.SetMeasureTextFunction(measure_text, nil)
return true
}
// TODO Either every x frames nuke max values in case of edge cases where max gets set very high
// or leave to application code to decide the right time for resize
resize_global :: proc() {
if len(GLOB.layers) > GLOB.max_layers do GLOB.max_layers = len(GLOB.layers)
shrink(&GLOB.layers, GLOB.max_layers)
if len(GLOB.scissors) > GLOB.max_scissors do GLOB.max_scissors = len(GLOB.scissors)
shrink(&GLOB.scissors, GLOB.max_scissors)
if len(GLOB.tmp_shape_verts) > GLOB.max_shape_verts do GLOB.max_shape_verts = len(GLOB.tmp_shape_verts)
shrink(&GLOB.tmp_shape_verts, GLOB.max_shape_verts)
if len(GLOB.tmp_text_verts) > GLOB.max_text_verts do GLOB.max_text_verts = len(GLOB.tmp_text_verts)
shrink(&GLOB.tmp_text_verts, GLOB.max_text_verts)
if len(GLOB.tmp_text_indices) > GLOB.max_text_indices do GLOB.max_text_indices = len(GLOB.tmp_text_indices)
shrink(&GLOB.tmp_text_indices, GLOB.max_text_indices)
if len(GLOB.tmp_text_batches) > GLOB.max_text_batches do GLOB.max_text_batches = len(GLOB.tmp_text_batches)
shrink(&GLOB.tmp_text_batches, GLOB.max_text_batches)
if len(GLOB.tmp_primitives) > GLOB.max_primitives do GLOB.max_primitives = len(GLOB.tmp_primitives)
shrink(&GLOB.tmp_primitives, GLOB.max_primitives)
if len(GLOB.tmp_sub_batches) > GLOB.max_sub_batches do GLOB.max_sub_batches = len(GLOB.tmp_sub_batches)
shrink(&GLOB.tmp_sub_batches, GLOB.max_sub_batches)
}
destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
delete(GLOB.layers)
delete(GLOB.scissors)
delete(GLOB.tmp_shape_verts)
delete(GLOB.tmp_text_verts)
delete(GLOB.tmp_text_indices)
delete(GLOB.tmp_text_batches)
delete(GLOB.tmp_primitives)
delete(GLOB.tmp_sub_batches)
free(GLOB.clay_mem, allocator)
if GLOB.msaa_texture != nil {
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
}
destroy_pipeline_2d_base(device, &GLOB.pipeline_2d_base)
destroy_text_cache()
}
// Internal
clear_global :: proc() {
GLOB.curr_layer_index = 0
GLOB.clay_z_index = 0
GLOB.cleared = false
clear(&GLOB.layers)
clear(&GLOB.scissors)
clear(&GLOB.tmp_shape_verts)
clear(&GLOB.tmp_text_verts)
clear(&GLOB.tmp_text_indices)
clear(&GLOB.tmp_text_batches)
clear(&GLOB.tmp_primitives)
clear(&GLOB.tmp_sub_batches)
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Text measurement (Clay) -------
// ---------------------------------------------------------------------------------------------------------------------
@(private = "file")
measure_text :: proc "c" (
text: clay.StringSlice,
config: ^clay.TextElementConfig,
user_data: rawptr,
) -> clay.Dimensions {
context = GLOB.odin_context
text := string(text.chars[:text.length])
c_text := strings.clone_to_cstring(text, context.temp_allocator)
w, h: c.int
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &w, &h) {
log.panicf("Failed to measure text: %s", sdl.GetError())
}
return clay.Dimensions{width = f32(w) / GLOB.dpi_scaling, height = f32(h) / GLOB.dpi_scaling}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Frame lifecycle ---------------
// ---------------------------------------------------------------------------------------------------------------------
// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for.
begin :: proc(bounds: Rectangle) -> ^Layer {
// Cleanup
clear_global()
// Begin new layer + start a new scissor
scissor := Scissor {
bounds = sdl.Rect {
x = i32(bounds.x * GLOB.dpi_scaling),
y = i32(bounds.y * GLOB.dpi_scaling),
w = i32(bounds.w * GLOB.dpi_scaling),
h = i32(bounds.h * GLOB.dpi_scaling),
},
}
append(&GLOB.scissors, scissor)
layer := Layer {
bounds = bounds,
scissor_len = 1,
}
append(&GLOB.layers, layer)
return &GLOB.layers[GLOB.curr_layer_index]
}
// Creates a new layer
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
layer := Layer {
bounds = bounds,
sub_batch_start = prev_layer.sub_batch_start + prev_layer.sub_batch_len,
scissor_start = prev_layer.scissor_start + prev_layer.scissor_len,
scissor_len = 1,
}
append(&GLOB.layers, layer)
GLOB.curr_layer_index += 1
log.debug("Added new layer; curr index", GLOB.curr_layer_index)
scissor := Scissor {
sub_batch_start = u32(len(GLOB.tmp_sub_batches)),
bounds = sdl.Rect {
x = i32(bounds.x * GLOB.dpi_scaling),
y = i32(bounds.y * GLOB.dpi_scaling),
w = i32(bounds.w * GLOB.dpi_scaling),
h = i32(bounds.h * GLOB.dpi_scaling),
},
}
append(&GLOB.scissors, scissor)
return &GLOB.layers[GLOB.curr_layer_index]
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Built-in primitive processing --
// ---------------------------------------------------------------------------------------------------------------------
// Submit shape vertices (colored triangles) to the given layer for rendering.
prepare_shape :: proc(layer: ^Layer, vertices: []Vertex) {
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, .Shapes, offset, u32(len(vertices)))
}
// Submit an SDF primitive to the given layer for rendering.
prepare_sdf_primitive :: proc(layer: ^Layer, prim: 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 a text element to the given layer for rendering.
// Copies SDL_ttf vertices directly (with baked position) and copies indices for indexed drawing.
prepare_text :: proc(layer: ^Layer, txt: Text) {
data := sdl_ttf.GetGPUTextDrawData(txt.ref)
if data == nil {
return // nil is normal for empty text
}
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
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 {
position = {pos.x + txt.position[0] * GLOB.dpi_scaling, -pos.y + txt.position[1] * GLOB.dpi_scaling},
uv = {uv.x, uv.y},
color = txt.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,
TextBatch {
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
}
}
// Append a new sub-batch or extend the last one if same kind and contiguous.
@(private)
append_or_extend_sub_batch :: proc(
scissor: ^Scissor,
layer: ^Layer,
kind: Sub_Batch_Kind,
offset: u32,
count: u32,
) {
if scissor.sub_batch_len > 0 {
last := &GLOB.tmp_sub_batches[scissor.sub_batch_start + scissor.sub_batch_len - 1]
if last.kind == kind && kind != .Text && last.offset + last.count == offset {
last.count += count
return
}
}
append(&GLOB.tmp_sub_batches, Sub_Batch{kind = kind, offset = offset, count = count})
scissor.sub_batch_len += 1
layer.sub_batch_len += 1
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Clay ------------------------
// ---------------------------------------------------------------------------------------------------------------------
@(private = "file")
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
context = GLOB.odin_context
log.error("Clay error:", errorData.errorType, errorData.errorText)
}
ClayBatch :: struct {
bounds: Rectangle,
cmds: clay.ClayArray(clay.RenderCommand),
}
// Process Clay render commands into shape and text primitives.
prepare_clay_batch :: proc(
base_layer: ^Layer,
batch: ^ClayBatch,
mouse_wheel_delta: [2]f32,
frame_time: f32 = 0,
) {
mouse_pos: [2]f32
mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y)
// Update clay internals
clay.SetPointerState(
clay.Vector2{mouse_pos.x - base_layer.bounds.x, mouse_pos.y - base_layer.bounds.y},
.LEFT in mouse_flags,
)
clay.UpdateScrollContainers(true, mouse_wheel_delta, frame_time)
layer := base_layer
// Parse render commands
for i in 0 ..< int(batch.cmds.length) {
render_command := clay.RenderCommandArray_Get(&batch.cmds, cast(i32)i)
// Translate bounding box of the primitive by the layer position
bounds := Rectangle {
x = render_command.boundingBox.x + layer.bounds.x,
y = render_command.boundingBox.y + layer.bounds.y,
w = render_command.boundingBox.width,
h = render_command.boundingBox.height,
}
if render_command.zIndex > GLOB.clay_z_index {
log.debug("Higher zIndex found, creating new layer & setting z_index to", render_command.zIndex)
layer = new_layer(layer, bounds)
// Update bounds to new layer offset
bounds.x = render_command.boundingBox.x + layer.bounds.x
bounds.y = render_command.boundingBox.y + layer.bounds.y
GLOB.clay_z_index = render_command.zIndex
}
switch (render_command.commandType) {
case clay.RenderCommandType.None:
case clay.RenderCommandType.Text:
render_data := render_command.renderData.text
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
c_text := strings.clone_to_cstring(txt, context.temp_allocator)
sdl_text := GLOB.text_cache.cache[render_command.id]
if sdl_text == nil {
// Cache a SDL text object
sdl_text = sdl_ttf.CreateText(
GLOB.text_cache.engine,
get_font(render_data.fontId, render_data.fontSize),
c_text,
0,
)
if sdl_text == nil {
log.panicf("Failed to create SDL text for clay render command: %s", sdl.GetError())
}
GLOB.text_cache.cache[render_command.id] = sdl_text
} else {
if !sdl_ttf.SetTextString(sdl_text, c_text, 0) {
log.panicf("Failed to update SDL text string: %s", sdl.GetError())
}
}
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
case clay.RenderCommandType.Image:
case clay.RenderCommandType.ScissorStart:
if bounds.w == 0 || bounds.h == 0 {
continue
}
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
if curr_scissor.sub_batch_len != 0 {
// Scissor has some content, need to make a new scissor
new := Scissor {
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
bounds = sdl.Rect {
c.int(bounds.x * GLOB.dpi_scaling),
c.int(bounds.y * GLOB.dpi_scaling),
c.int(bounds.w * GLOB.dpi_scaling),
c.int(bounds.h * GLOB.dpi_scaling),
},
}
append(&GLOB.scissors, new)
layer.scissor_len += 1
} else {
curr_scissor.bounds = sdl.Rect {
c.int(bounds.x * GLOB.dpi_scaling),
c.int(bounds.y * GLOB.dpi_scaling),
c.int(bounds.w * GLOB.dpi_scaling),
c.int(bounds.h * GLOB.dpi_scaling),
}
}
case clay.RenderCommandType.ScissorEnd:
case clay.RenderCommandType.Rectangle:
render_data := render_command.renderData.rectangle
cr := render_data.cornerRadius
color := color_from_clay(render_data.backgroundColor)
radii := [4]f32{cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft}
if radii == {0, 0, 0, 0} {
rectangle(layer, bounds, color)
} else {
rectangle_corners(layer, bounds, radii, color)
}
case clay.RenderCommandType.Border:
render_data := render_command.renderData.border
cr := render_data.cornerRadius
color := color_from_clay(render_data.color)
thick := f32(render_data.width.top)
radii := [4]f32{cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft}
if radii == {0, 0, 0, 0} {
rectangle_lines(layer, bounds, color, thick)
} else {
rectangle_corners_lines(layer, bounds, radii, color, thick)
}
case clay.RenderCommandType.Custom:
}
}
}
// Render primitives. clear_color is the background fill before any layers are drawn.
end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = BLACK) {
cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if cmd_buffer == nil {
log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError())
}
// Upload primitives to GPU
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
upload(device, copy_pass)
sdl.EndGPUCopyPass(copy_pass)
swapchain_texture: ^sdl.GPUTexture
w, h: u32
if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &w, &h) {
log.panicf("Failed to acquire swapchain texture: %s", sdl.GetError())
}
if swapchain_texture == nil {
// Window is minimized or not visible — submit and skip this frame
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
log.panicf("Failed to submit GPU command buffer (minimized window): %s", sdl.GetError())
}
return
}
use_msaa := GLOB.sample_count != ._1
render_texture := swapchain_texture
if use_msaa {
ensure_msaa_texture(device, sdl.GetGPUSwapchainTextureFormat(device, window), w, h)
render_texture = GLOB.msaa_texture
}
cc := color_to_f32(clear_color)
// Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
for &layer, index in GLOB.layers {
log.debug("Drawing layer", index)
draw_layer(device, window, cmd_buffer, render_texture, w, h, cc, &layer)
}
// Resolve MSAA render texture to the swapchain.
if use_msaa {
resolve_pass := sdl.BeginGPURenderPass(
cmd_buffer,
&sdl.GPUColorTargetInfo {
texture = render_texture,
load_op = .LOAD,
store_op = .RESOLVE,
resolve_texture = swapchain_texture,
},
1,
nil,
)
sdl.EndGPURenderPass(resolve_pass)
}
if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
log.panicf("Failed to submit GPU command buffer: %s", sdl.GetError())
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- MSAA --------------------------
// ---------------------------------------------------------------------------------------------------------------------
// Query the highest MSAA sample count supported by the GPU for the swapchain format.
max_sample_count :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> sdl.GPUSampleCount {
format := sdl.GetGPUSwapchainTextureFormat(device, window)
counts := [?]sdl.GPUSampleCount{._8, ._4, ._2}
for sc in counts {
if sdl.GPUTextureSupportsSampleCount(device, format, sc) do return sc
}
return ._1
}
@(private = "file")
ensure_msaa_texture :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, w, h: u32) {
if GLOB.msaa_texture != nil && GLOB.msaa_w == w && GLOB.msaa_h == h {
return
}
if GLOB.msaa_texture != nil {
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
}
GLOB.msaa_texture = sdl.CreateGPUTexture(
device,
sdl.GPUTextureCreateInfo {
type = .D2,
format = format,
usage = {.COLOR_TARGET},
width = w,
height = h,
layer_count_or_depth = 1,
num_levels = 1,
sample_count = GLOB.sample_count,
},
)
if GLOB.msaa_texture == nil {
log.panicf("Failed to create MSAA texture (%dx%d): %s", w, h, sdl.GetError())
}
GLOB.msaa_w = w
GLOB.msaa_h = h
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Utility -----------------------
// ---------------------------------------------------------------------------------------------------------------------
ortho_rh :: proc(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> matrix[4, 4]f32 {
return matrix[4, 4]f32{
2.0 / (right - left), 0.0, 0.0, -(right + left) / (right - left),
0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / (top - bottom),
0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near),
0.0, 0.0, 0.0, 1.0,
}
}
Draw_Mode :: enum u32 {
Tessellated = 0,
SDF = 1,
}
Vertex_Uniforms :: struct {
projection: matrix[4, 4]f32,
scale: f32,
mode: Draw_Mode,
}
// Push projection, dpi scale, and rendering mode as a single uniform block (slot 0).
push_globals :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, w: f32, h: f32, mode: Draw_Mode = .Tessellated) {
globals := Vertex_Uniforms {
projection = ortho_rh(left = 0.0, top = 0.0, right = f32(w), bottom = f32(h), near = -1.0, far = 1.0),
scale = GLOB.dpi_scaling,
mode = mode,
}
sdl.PushGPUVertexUniformData(cmd_buffer, 0, &globals, size_of(Vertex_Uniforms))
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Buffer ------------------------
// ---------------------------------------------------------------------------------------------------------------------
Buffer :: struct {
gpu: ^sdl.GPUBuffer,
transfer: ^sdl.GPUTransferBuffer,
size: u32,
}
@(require_results)
create_buffer :: proc(
device: ^sdl.GPUDevice,
size: u32,
gpu_usage: sdl.GPUBufferUsageFlags,
) -> (
buffer: Buffer,
ok: bool,
) {
gpu := sdl.CreateGPUBuffer(device, sdl.GPUBufferCreateInfo{usage = gpu_usage, size = size})
if gpu == nil {
log.errorf("Failed to create GPU buffer (size=%d): %s", size, sdl.GetError())
return buffer, false
}
transfer := sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size},
)
if transfer == nil {
sdl.ReleaseGPUBuffer(device, gpu)
log.errorf("Failed to create GPU transfer buffer (size=%d): %s", size, sdl.GetError())
return buffer, false
}
return Buffer{gpu, transfer, size}, true
}
grow_buffer_if_needed :: proc(
device: ^sdl.GPUDevice,
buffer: ^Buffer,
new_size: u32,
gpu_usage: sdl.GPUBufferUsageFlags,
) {
if new_size > buffer.size {
log.debug("Resizing buffer from", buffer.size, "to", new_size)
destroy_buffer(device, buffer)
buffer.gpu = sdl.CreateGPUBuffer(device, sdl.GPUBufferCreateInfo{usage = gpu_usage, size = new_size})
if buffer.gpu == nil {
log.panicf("Failed to grow GPU buffer (new_size=%d): %s", new_size, sdl.GetError())
}
buffer.transfer = sdl.CreateGPUTransferBuffer(
device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = new_size},
)
if buffer.transfer == nil {
log.panicf("Failed to grow GPU transfer buffer (new_size=%d): %s", new_size, sdl.GetError())
}
buffer.size = new_size
}
}
destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) {
sdl.ReleaseGPUBuffer(device, buffer.gpu)
sdl.ReleaseGPUTransferBuffer(device, buffer.transfer)
}