editor/client/odin/gpu_renderer.odin
pavel 8d85568713 Embed compiled SPIR-V shaders in the client binary with #load
The GPU renderer previously read .spv files from a cwd-relative path at
runtime; when they were missing or the editor was launched from outside
the repo root, it silently fell back to the SDL renderer, losing syntax
highlighting. The shaders are now baked in at compile time, so the
binary is self-contained. The compiled .spv files are committed (no
longer gitignored) since they are required to build; regenerate with
scripts/compile-shaders.sh after editing the GLSL sources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:22:17 +02:00

577 lines
21 KiB
Odin

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
GPU_FONT_PATHS := [?]string{
"client/odin/assets/fonts/EditorMono.ttf",
"client/odin/assets/fonts/NotoSansMono-Regular.ttf",
"/usr/share/fonts/google-noto/NotoSansMono-Regular.ttf",
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf",
"/usr/share/fonts/dejavu/DejaVuSansMono.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, ok := gpu_read_font_file()
if !ok {
fmt.println("read font failed: no configured monospace font found")
return false
}
defer 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() -> ([]u8, string, bool) {
for path in GPU_FONT_PATHS {
bytes, err := os.read_entire_file(path, context.allocator)
if err == nil && len(bytes) > 0 {
return bytes, path, true
}
if err == nil {
delete(bytes)
}
}
return nil, "", 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)
}