package main import "core:fmt" import "core:mem" import "core:os" import stbtt "vendor:stb/truetype" import SDL "vendor:sdl3" GPU_Vertex :: struct { pos: [2]f32, color: [4]f32, } GPU_Text_Vertex :: struct { pos: [2]f32, uv: [2]f32, color: [4]f32, } GPU_Uniforms :: struct { viewport: [2]f32, } GPU_Renderer :: struct { available: bool, device: ^SDL.GPUDevice, window: ^SDL.Window, pipeline: ^SDL.GPUGraphicsPipeline, text_pipeline: ^SDL.GPUGraphicsPipeline, vertex_shader: ^SDL.GPUShader, fragment_shader: ^SDL.GPUShader, text_vertex_shader: ^SDL.GPUShader, text_fragment_shader: ^SDL.GPUShader, vertex_buffer: ^SDL.GPUBuffer, transfer_buffer: ^SDL.GPUTransferBuffer, text_vertex_buffer: ^SDL.GPUBuffer, text_transfer_buffer: ^SDL.GPUTransferBuffer, font_texture: ^SDL.GPUTexture, font_sampler: ^SDL.GPUSampler, font_chars: [95]stbtt.bakedchar, font_advance: f32, vertices: [dynamic]GPU_Vertex, text_vertices: [dynamic]GPU_Text_Vertex, max_vertices: int, max_text_vertices: int, width: int, height: int, } // Compiled SPIR-V is embedded at build time so the binary never depends on // finding shader files at runtime. Regenerate with scripts/compile-shaders.sh // after editing the GLSL sources, then rebuild. GPU_RECT_VERT_SPV :: #load("shaders/compiled/rect.vert.spv") GPU_RECT_FRAG_SPV :: #load("shaders/compiled/rect.frag.spv") GPU_TEXT_VERT_SPV :: #load("shaders/compiled/text.vert.spv") GPU_TEXT_FRAG_SPV :: #load("shaders/compiled/text.frag.spv") GPU_MAX_VERTICES :: 240000 GPU_MAX_TEXT_VERTICES :: 120000 GPU_FONT_ATLAS_SIZE :: 512 GPU_FONT_PIXEL_HEIGHT :: 15.0 GPU_FONT_BASELINE_OFFSET :: 11.0 GPU_FONT_CELL_PADDING :: 1.0 // The bundled font is embedded at build time so the binary always has a // working monospace font. EditorMono.ttf remains an optional on-disk override. @(rodata) GPU_EMBEDDED_FONT := #load("assets/fonts/NotoSansMono-Regular.ttf") GPU_FONT_OVERRIDE_PATHS := [?]string{ "client/odin/assets/fonts/EditorMono.ttf", } gpu_renderer_make :: proc(window: ^SDL.Window) -> GPU_Renderer { gpu: GPU_Renderer gpu.window = window gpu.max_vertices = GPU_MAX_VERTICES gpu.max_text_vertices = GPU_MAX_TEXT_VERTICES gpu.width = SDL_WINDOW_WIDTH gpu.height = SDL_WINDOW_HEIGHT gpu.vertices = make([dynamic]GPU_Vertex, 0, gpu.max_vertices) gpu.text_vertices = make([dynamic]GPU_Text_Vertex, 0, gpu.max_text_vertices) gpu.device = SDL.CreateGPUDevice(SDL.GPUShaderFormat{.SPIRV}, true, nil) if gpu.device == nil { fmt.println("SDL GPU device failed, falling back:", SDL.GetError()) return gpu } if !SDL.ClaimWindowForGPUDevice(gpu.device, window) { fmt.println("SDL GPU window claim failed, falling back:", SDL.GetError()) gpu_renderer_destroy(&gpu) return GPU_Renderer{} } gpu.vertex_shader = gpu_create_shader(gpu.device, "rect.vert", GPU_RECT_VERT_SPV, .VERTEX, 0) gpu.fragment_shader = gpu_create_shader(gpu.device, "rect.frag", GPU_RECT_FRAG_SPV, .FRAGMENT, 0) gpu.text_vertex_shader = gpu_create_shader(gpu.device, "text.vert", GPU_TEXT_VERT_SPV, .VERTEX, 0) gpu.text_fragment_shader = gpu_create_shader(gpu.device, "text.frag", GPU_TEXT_FRAG_SPV, .FRAGMENT, 1) if gpu.vertex_shader == nil || gpu.fragment_shader == nil || gpu.text_vertex_shader == nil || gpu.text_fragment_shader == nil { fmt.println("SDL GPU shader creation failed, falling back:", SDL.GetError()) gpu_renderer_destroy(&gpu) return GPU_Renderer{} } color_format := SDL.GetGPUSwapchainTextureFormat(gpu.device, window) if color_format == .INVALID { fmt.println("SDL GPU swapchain format failed, falling back:", SDL.GetError()) gpu_renderer_destroy(&gpu) return GPU_Renderer{} } vb_desc := SDL.GPUVertexBufferDescription{ slot = 0, pitch = size_of(GPU_Vertex), input_rate = .VERTEX, instance_step_rate = 0, } attrs := [?]SDL.GPUVertexAttribute{ {location = 0, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Vertex, pos))}, {location = 1, buffer_slot = 0, format = .FLOAT4, offset = u32(offset_of(GPU_Vertex, color))}, } blend := SDL.GPUColorTargetBlendState{ src_color_blendfactor = .SRC_ALPHA, dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA, color_blend_op = .ADD, src_alpha_blendfactor = .ONE, dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA, alpha_blend_op = .ADD, color_write_mask = SDL.GPUColorComponentFlags{.R, .G, .B, .A}, enable_blend = true, enable_color_write_mask = true, } color_target := SDL.GPUColorTargetDescription{format = color_format, blend_state = blend} pipeline_info := SDL.GPUGraphicsPipelineCreateInfo{ vertex_shader = gpu.vertex_shader, fragment_shader = gpu.fragment_shader, vertex_input_state = { vertex_buffer_descriptions = &vb_desc, num_vertex_buffers = 1, vertex_attributes = &attrs[0], num_vertex_attributes = len(attrs), }, primitive_type = .TRIANGLELIST, rasterizer_state = {fill_mode = .FILL, cull_mode = .NONE, front_face = .COUNTER_CLOCKWISE}, multisample_state = {sample_count = ._1}, depth_stencil_state = {}, target_info = { color_target_descriptions = &color_target, num_color_targets = 1, has_depth_stencil_target = false, }, } gpu.pipeline = SDL.CreateGPUGraphicsPipeline(gpu.device, pipeline_info) if gpu.pipeline == nil { fmt.println("SDL GPU pipeline failed, falling back:", SDL.GetError()) gpu_renderer_destroy(&gpu) return GPU_Renderer{} } text_vb_desc := SDL.GPUVertexBufferDescription{ slot = 0, pitch = size_of(GPU_Text_Vertex), input_rate = .VERTEX, instance_step_rate = 0, } text_attrs := [?]SDL.GPUVertexAttribute{ {location = 0, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Text_Vertex, pos))}, {location = 1, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Text_Vertex, uv))}, {location = 2, buffer_slot = 0, format = .FLOAT4, offset = u32(offset_of(GPU_Text_Vertex, color))}, } text_pipeline_info := pipeline_info text_pipeline_info.vertex_shader = gpu.text_vertex_shader text_pipeline_info.fragment_shader = gpu.text_fragment_shader text_pipeline_info.vertex_input_state = { vertex_buffer_descriptions = &text_vb_desc, num_vertex_buffers = 1, vertex_attributes = &text_attrs[0], num_vertex_attributes = len(text_attrs), } gpu.text_pipeline = SDL.CreateGPUGraphicsPipeline(gpu.device, text_pipeline_info) if gpu.text_pipeline == nil { fmt.println("SDL GPU text pipeline failed, falling back:", SDL.GetError()) gpu_renderer_destroy(&gpu) return GPU_Renderer{} } buffer_size := u32(gpu.max_vertices * size_of(GPU_Vertex)) gpu.vertex_buffer = SDL.CreateGPUBuffer(gpu.device, {usage = {.VERTEX}, size = buffer_size}) gpu.transfer_buffer = SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = buffer_size}) text_buffer_size := u32(gpu.max_text_vertices * size_of(GPU_Text_Vertex)) gpu.text_vertex_buffer = SDL.CreateGPUBuffer(gpu.device, {usage = {.VERTEX}, size = text_buffer_size}) gpu.text_transfer_buffer = SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = text_buffer_size}) if gpu.vertex_buffer == nil || gpu.transfer_buffer == nil || gpu.text_vertex_buffer == nil || gpu.text_transfer_buffer == nil { fmt.println("SDL GPU buffers failed, falling back:", SDL.GetError()) gpu_renderer_destroy(&gpu) return GPU_Renderer{} } if !gpu_create_font_atlas(&gpu) { gpu_renderer_destroy(&gpu) return GPU_Renderer{} } gpu.available = true return gpu } gpu_renderer_destroy :: proc(gpu: ^GPU_Renderer) { if gpu.device != nil { _ = SDL.WaitForGPUIdle(gpu.device) if gpu.window != nil { SDL.ReleaseWindowFromGPUDevice(gpu.device, gpu.window) } if gpu.transfer_buffer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, gpu.transfer_buffer) if gpu.text_transfer_buffer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer) if gpu.vertex_buffer != nil do SDL.ReleaseGPUBuffer(gpu.device, gpu.vertex_buffer) if gpu.text_vertex_buffer != nil do SDL.ReleaseGPUBuffer(gpu.device, gpu.text_vertex_buffer) if gpu.font_sampler != nil do SDL.ReleaseGPUSampler(gpu.device, gpu.font_sampler) if gpu.font_texture != nil do SDL.ReleaseGPUTexture(gpu.device, gpu.font_texture) if gpu.pipeline != nil do SDL.ReleaseGPUGraphicsPipeline(gpu.device, gpu.pipeline) if gpu.text_pipeline != nil do SDL.ReleaseGPUGraphicsPipeline(gpu.device, gpu.text_pipeline) if gpu.fragment_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.fragment_shader) if gpu.vertex_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.vertex_shader) if gpu.text_fragment_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.text_fragment_shader) if gpu.text_vertex_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.text_vertex_shader) SDL.DestroyGPUDevice(gpu.device) } delete(gpu.vertices) delete(gpu.text_vertices) gpu^ = {} } gpu_create_shader :: proc(device: ^SDL.GPUDevice, name: string, code: []u8, stage: SDL.GPUShaderStage, num_samplers: u32) -> ^SDL.GPUShader { shader := SDL.CreateGPUShader(device, { code_size = len(code), code = raw_data(code), entrypoint = "main", format = {.SPIRV}, stage = stage, num_uniform_buffers = 1 if stage == .VERTEX else 0, num_samplers = num_samplers, }) if shader == nil { fmt.println("CreateGPUShader failed:", name, SDL.GetError()) } return shader } gpu_begin :: proc(gpu: ^GPU_Renderer) { clear(&gpu.vertices) clear(&gpu.text_vertices) w, h: i32 if SDL.GetWindowSize(gpu.window, &w, &h) { gpu.width = int(w) gpu.height = int(h) } } gpu_rect :: proc(gpu: ^GPU_Renderer, x, y, w, h: f32, r, g, b, a: u8) { if w <= 0 || h <= 0 do return c := color_f32(r, g, b, a) gpu_push_quad(gpu, x, y, x + w, y + h, c) } gpu_line :: proc(gpu: ^GPU_Renderer, x1, y1, x2, y2: f32, r, g, b, a: u8) { if abs_f32(x2 - x1) < 1 { x := min_f32(x1, x2) y := min_f32(y1, y2) gpu_rect(gpu, x, y, 1, abs_f32(y2 - y1), r, g, b, a) } else if abs_f32(y2 - y1) < 1 { x := min_f32(x1, x2) y := min_f32(y1, y2) gpu_rect(gpu, x, y, abs_f32(x2 - x1), 1, r, g, b, a) } else { gpu_rect(gpu, x1, y1, max_f32(abs_f32(x2 - x1), 1), 1, r, g, b, a) } } gpu_text :: proc(gpu: ^GPU_Renderer, x, y: f32, text: string, r, g, b, a: u8) { if len(text) == 0 do return xpos := round_f32(x) ypos := y + GPU_FONT_BASELINE_OFFSET color := color_f32(r, g, b, a) for raw_ch in transmute([]u8)text { ch := raw_ch if ch == '\n' { xpos = x ypos += SDL_LINE_HEIGHT continue } if ch < 32 || ch > 126 { ch = '?' } if ch == ' ' { xpos += gpu.font_advance continue } glyph := gpu.font_chars[ch - 32] glyph_w := f32(glyph.x1 - glyph.x0) glyph_h := f32(glyph.y1 - glyph.y0) x0 := round_f32(xpos + glyph.xoff) y0 := round_f32(ypos + glyph.yoff) x1 := x0 + glyph_w y1 := y0 + glyph_h s0 := f32(glyph.x0) / GPU_FONT_ATLAS_SIZE t0 := f32(glyph.y0) / GPU_FONT_ATLAS_SIZE s1 := f32(glyph.x1) / GPU_FONT_ATLAS_SIZE t1 := f32(glyph.y1) / GPU_FONT_ATLAS_SIZE gpu_push_text_quad(gpu, x0, y0, x1, y1, s0, t0, s1, t1, color) xpos += gpu.font_advance } } gpu_text_limited :: proc(gpu: ^GPU_Renderer, x, y: f32, text: string, max_chars: int, r, g, b, a: u8) { if max_chars <= 0 do return if len(text) <= max_chars { gpu_text(gpu, x, y, text, r, g, b, a) } else if max_chars <= 3 { gpu_text(gpu, x, y, text[:max_chars], r, g, b, a) } else { clipped := fmt.tprintf("%s...", text[:max_chars - 3]) gpu_text(gpu, x, y, clipped, r, g, b, a) } } gpu_text_width :: proc(text: string) -> int { if len(text) == 0 do return 0 width: f32 = 0 max_width: f32 = 0 for raw_ch in transmute([]u8)text { ch := raw_ch if ch == '\n' { if width > max_width do max_width = width width = 0 continue } if ch < 32 || ch > 126 do ch = '?' width += gpu_global_font_advance(ch) } if width > max_width do max_width = width return int(max_width + 0.5) } gpu_font_text_advance :: proc(gpu: ^GPU_Renderer, text: string) -> f32 { if len(text) == 0 do return 0 start_x: f32 = 0 x: f32 = start_x y: f32 = GPU_FONT_BASELINE_OFFSET max_width: f32 = 0 for raw_ch in transmute([]u8)text { ch := raw_ch if ch == '\n' { if x - start_x > max_width do max_width = x - start_x x = start_x y += SDL_LINE_HEIGHT continue } if ch < 32 || ch > 126 do ch = '?' quad: stbtt.aligned_quad x += gpu.font_advance } if x - start_x > max_width do max_width = x - start_x return max_width } gpu_font_text_width :: proc(gpu: ^GPU_Renderer, text: string) -> int { return int(gpu_font_text_advance(gpu, text) + 0.5) } gpu_present :: proc(gpu: ^GPU_Renderer) { if !gpu.available do return command := SDL.AcquireGPUCommandBuffer(gpu.device) if command == nil do return texture: ^SDL.GPUTexture width, height: u32 if !SDL.WaitAndAcquireGPUSwapchainTexture(command, gpu.window, &texture, &width, &height) || texture == nil { _ = SDL.CancelGPUCommandBuffer(command) return } vertex_count := len(gpu.vertices) text_vertex_count := len(gpu.text_vertices) if vertex_count > 0 { size := vertex_count * size_of(GPU_Vertex) mapped := SDL.MapGPUTransferBuffer(gpu.device, gpu.transfer_buffer, true) if mapped != nil { mem.copy(transmute([^]u8)mapped, raw_data(gpu.vertices[:]), size) SDL.UnmapGPUTransferBuffer(gpu.device, gpu.transfer_buffer) copy_pass := SDL.BeginGPUCopyPass(command) SDL.UploadToGPUBuffer(copy_pass, {transfer_buffer = gpu.transfer_buffer}, {buffer = gpu.vertex_buffer, size = u32(size)}, true) SDL.EndGPUCopyPass(copy_pass) } } if text_vertex_count > 0 { size := text_vertex_count * size_of(GPU_Text_Vertex) mapped := SDL.MapGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer, true) if mapped != nil { mem.copy(transmute([^]u8)mapped, raw_data(gpu.text_vertices[:]), size) SDL.UnmapGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer) copy_pass := SDL.BeginGPUCopyPass(command) SDL.UploadToGPUBuffer(copy_pass, {transfer_buffer = gpu.text_transfer_buffer}, {buffer = gpu.text_vertex_buffer, size = u32(size)}, true) SDL.EndGPUCopyPass(copy_pass) } } target := SDL.GPUColorTargetInfo{ texture = texture, clear_color = SDL.FColor{15.0 / 255.0, 17.0 / 255.0, 22.0 / 255.0, 1}, load_op = .CLEAR, store_op = .STORE, } pass := SDL.BeginGPURenderPass(command, &target, 1, nil) if vertex_count > 0 { uniforms := GPU_Uniforms{viewport = {f32(gpu.width), f32(gpu.height)}} SDL.PushGPUVertexUniformData(command, 0, &uniforms, size_of(uniforms)) SDL.BindGPUGraphicsPipeline(pass, gpu.pipeline) SDL.BindGPUVertexBuffers(pass, 0, &(SDL.GPUBufferBinding{buffer = gpu.vertex_buffer}), 1) SDL.DrawGPUPrimitives(pass, u32(vertex_count), 1, 0, 0) } if text_vertex_count > 0 { uniforms := GPU_Uniforms{viewport = {f32(gpu.width), f32(gpu.height)}} binding := SDL.GPUTextureSamplerBinding{texture = gpu.font_texture, sampler = gpu.font_sampler} SDL.PushGPUVertexUniformData(command, 0, &uniforms, size_of(uniforms)) SDL.BindGPUGraphicsPipeline(pass, gpu.text_pipeline) SDL.BindGPUVertexBuffers(pass, 0, &(SDL.GPUBufferBinding{buffer = gpu.text_vertex_buffer}), 1) SDL.BindGPUFragmentSamplers(pass, 0, &binding, 1) SDL.DrawGPUPrimitives(pass, u32(text_vertex_count), 1, 0, 0) } SDL.EndGPURenderPass(pass) _ = SDL.SubmitGPUCommandBuffer(command) } gpu_create_font_atlas :: proc(gpu: ^GPU_Renderer) -> bool { font_bytes, font_path, owned := gpu_read_font_file() defer if owned do delete(font_bytes) atlas_size := GPU_FONT_ATLAS_SIZE * GPU_FONT_ATLAS_SIZE atlas := make([]u8, atlas_size) defer delete(atlas) baked := stbtt.BakeFontBitmap(raw_data(font_bytes), 0, GPU_FONT_PIXEL_HEIGHT, raw_data(atlas), GPU_FONT_ATLAS_SIZE, GPU_FONT_ATLAS_SIZE, 32, len(gpu.font_chars), &gpu.font_chars[0]) if baked <= 0 { fmt.println("font bake failed:", font_path) return false } widest_advance: f32 = 0 for char in gpu.font_chars { glyph_width := char.xoff + f32(char.x1 - char.x0) widest_advance = max_f32(widest_advance, max_f32(char.xadvance, glyph_width)) } gpu.font_advance = max_f32(ceil_f32(widest_advance + GPU_FONT_CELL_PADDING), 1) gpu.font_texture = SDL.CreateGPUTexture(gpu.device, { type = .D2, format = .R8_UNORM, usage = {.SAMPLER}, width = GPU_FONT_ATLAS_SIZE, height = GPU_FONT_ATLAS_SIZE, layer_count_or_depth = 1, num_levels = 1, sample_count = ._1, }) gpu.font_sampler = SDL.CreateGPUSampler(gpu.device, { min_filter = .NEAREST, mag_filter = .NEAREST, mipmap_mode = .NEAREST, address_mode_u = .CLAMP_TO_EDGE, address_mode_v = .CLAMP_TO_EDGE, address_mode_w = .CLAMP_TO_EDGE, }) font_transfer := SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = u32(atlas_size)}) if gpu.font_texture == nil || gpu.font_sampler == nil || font_transfer == nil { fmt.println("font GPU resources failed:", SDL.GetError()) if font_transfer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, font_transfer) return false } defer SDL.ReleaseGPUTransferBuffer(gpu.device, font_transfer) mapped := SDL.MapGPUTransferBuffer(gpu.device, font_transfer, false) if mapped == nil { fmt.println("font transfer map failed:", SDL.GetError()) return false } mem.copy(transmute([^]u8)mapped, raw_data(atlas), atlas_size) SDL.UnmapGPUTransferBuffer(gpu.device, font_transfer) command := SDL.AcquireGPUCommandBuffer(gpu.device) copy_pass := SDL.BeginGPUCopyPass(command) SDL.UploadToGPUTexture(copy_pass, { transfer_buffer = font_transfer, pixels_per_row = GPU_FONT_ATLAS_SIZE, rows_per_layer = GPU_FONT_ATLAS_SIZE, }, { texture = gpu.font_texture, w = GPU_FONT_ATLAS_SIZE, h = GPU_FONT_ATLAS_SIZE, d = 1, }, false) SDL.EndGPUCopyPass(copy_pass) return SDL.SubmitGPUCommandBuffer(command) } gpu_read_font_file :: proc() -> (bytes: []u8, path: string, owned: bool) { for override_path in GPU_FONT_OVERRIDE_PATHS { data, err := os.read_entire_file(override_path, context.allocator) if err == nil && len(data) > 0 { return data, override_path, true } if err == nil { delete(data) } } return GPU_EMBEDDED_FONT, "embedded NotoSansMono-Regular.ttf", false } gpu_push_quad :: proc(gpu: ^GPU_Renderer, x0, y0, x1, y1: f32, c: [4]f32) { if len(gpu.vertices) + 6 > gpu.max_vertices do return append(&gpu.vertices, GPU_Vertex{{x0, y0}, c}) append(&gpu.vertices, GPU_Vertex{{x1, y0}, c}) append(&gpu.vertices, GPU_Vertex{{x0, y1}, c}) append(&gpu.vertices, GPU_Vertex{{x0, y1}, c}) append(&gpu.vertices, GPU_Vertex{{x1, y0}, c}) append(&gpu.vertices, GPU_Vertex{{x1, y1}, c}) } gpu_push_text_quad :: proc(gpu: ^GPU_Renderer, x0, y0, x1, y1, s0, t0, s1, t1: f32, c: [4]f32) { if len(gpu.text_vertices) + 6 > gpu.max_text_vertices do return append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y0}, {s0, t0}, c}) append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y0}, {s1, t0}, c}) append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y1}, {s0, t1}, c}) append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y1}, {s0, t1}, c}) append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y0}, {s1, t0}, c}) append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y1}, {s1, t1}, c}) } gpu_global_font_advance :: proc(ch: u8) -> f32 { return 8 } color_f32 :: proc(r, g, b, a: u8) -> [4]f32 { return {f32(r) / 255.0, f32(g) / 255.0, f32(b) / 255.0, f32(a) / 255.0} } abs_f32 :: proc(v: f32) -> f32 { if v < 0 do return -v return v } min_f32 :: proc(a, b: f32) -> f32 { if a < b do return a return b } max_f32 :: proc(a, b: f32) -> f32 { if a > b do return a return b } round_f32 :: proc(v: f32) -> f32 { if v >= 0 do return f32(int(v + 0.5)) return f32(int(v - 0.5)) } ceil_f32 :: proc(v: f32) -> f32 { i := int(v) if f32(i) < v do return f32(i + 1) return f32(i) }