697 lines
25 KiB
Odin
697 lines
25 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.packedchar,
|
|
font_extra: map[rune]stbtt.packedchar,
|
|
font_advance: f32,
|
|
vertices: [dynamic]GPU_Vertex,
|
|
text_vertices: [dynamic]GPU_Text_Vertex,
|
|
batches: [dynamic]GPU_Batch,
|
|
max_vertices: int,
|
|
max_text_vertices: int,
|
|
width: int,
|
|
height: int,
|
|
}
|
|
|
|
// Draw batches preserve submission order across the two pipelines, so text
|
|
// queued before an opaque rect stays underneath it. Consecutive quads of the
|
|
// same kind merge into one draw call.
|
|
GPU_Batch_Kind :: enum {
|
|
Rect,
|
|
Text,
|
|
}
|
|
|
|
GPU_Batch :: struct {
|
|
kind: GPU_Batch_Kind,
|
|
first: int,
|
|
count: 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 :: 1024
|
|
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.batches)
|
|
delete(gpu.font_extra)
|
|
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)
|
|
clear(&gpu.batches)
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Draws a single glyph, returning false when the font has no glyph so the
|
|
// caller can substitute an ASCII approximation.
|
|
gpu_rune :: proc(gpu: ^GPU_Renderer, x, y: f32, r: rune, red, green, blue, a: u8) -> bool {
|
|
if r >= 32 && r < 127 {
|
|
buf := [1]u8{u8(r)}
|
|
gpu_text(gpu, x, y, string(buf[:]), red, green, blue, a)
|
|
return true
|
|
}
|
|
glyph, ok := gpu.font_extra[r]
|
|
if !ok do return false
|
|
|
|
glyph_w := f32(glyph.x1 - glyph.x0)
|
|
glyph_h := f32(glyph.y1 - glyph.y0)
|
|
if glyph_w <= 0 || glyph_h <= 0 do return true
|
|
|
|
xpos := round_f32(x)
|
|
ypos := y + GPU_FONT_BASELINE_OFFSET
|
|
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_f32(red, green, blue, a))
|
|
return true
|
|
}
|
|
|
|
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)
|
|
uniforms := GPU_Uniforms{viewport = {f32(gpu.width), f32(gpu.height)}}
|
|
bound := false
|
|
bound_kind := GPU_Batch_Kind.Rect
|
|
for batch in gpu.batches {
|
|
if batch.count == 0 do continue
|
|
if !bound || bound_kind != batch.kind {
|
|
SDL.PushGPUVertexUniformData(command, 0, &uniforms, size_of(uniforms))
|
|
switch batch.kind {
|
|
case .Rect:
|
|
SDL.BindGPUGraphicsPipeline(pass, gpu.pipeline)
|
|
SDL.BindGPUVertexBuffers(pass, 0, &(SDL.GPUBufferBinding{buffer = gpu.vertex_buffer}), 1)
|
|
case .Text:
|
|
binding := SDL.GPUTextureSamplerBinding{texture = gpu.font_texture, sampler = gpu.font_sampler}
|
|
SDL.BindGPUGraphicsPipeline(pass, gpu.text_pipeline)
|
|
SDL.BindGPUVertexBuffers(pass, 0, &(SDL.GPUBufferBinding{buffer = gpu.text_vertex_buffer}), 1)
|
|
SDL.BindGPUFragmentSamplers(pass, 0, &binding, 1)
|
|
}
|
|
bound = true
|
|
bound_kind = batch.kind
|
|
}
|
|
SDL.DrawGPUPrimitives(pass, u32(batch.count), 1, u32(batch.first), 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)
|
|
|
|
// Extra ranges cover what terminal programs commonly draw: Latin-1,
|
|
// general punctuation, arrows, box drawing/blocks/geometric shapes,
|
|
// check marks and braille (spinners).
|
|
Extra_Range :: struct {
|
|
first: int,
|
|
count: int,
|
|
}
|
|
extra_ranges := [?]Extra_Range{
|
|
{0x00A1, 95},
|
|
{0x2010, 24},
|
|
{0x2190, 4},
|
|
{0x2500, 256},
|
|
{0x2713, 6},
|
|
{0x2800, 256},
|
|
}
|
|
total_extra := 0
|
|
for r in extra_ranges {
|
|
total_extra += r.count
|
|
}
|
|
extra_chars := make([]stbtt.packedchar, total_extra)
|
|
defer delete(extra_chars)
|
|
|
|
ranges: [1 + len(extra_ranges)]stbtt.pack_range
|
|
ranges[0] = stbtt.pack_range{
|
|
font_size = GPU_FONT_PIXEL_HEIGHT,
|
|
first_unicode_codepoint_in_range = 32,
|
|
num_chars = i32(len(gpu.font_chars)),
|
|
chardata_for_range = &gpu.font_chars[0],
|
|
}
|
|
offset := 0
|
|
for r, index in extra_ranges {
|
|
ranges[index + 1] = stbtt.pack_range{
|
|
font_size = GPU_FONT_PIXEL_HEIGHT,
|
|
first_unicode_codepoint_in_range = i32(r.first),
|
|
num_chars = i32(r.count),
|
|
chardata_for_range = &extra_chars[offset],
|
|
}
|
|
offset += r.count
|
|
}
|
|
|
|
pack: stbtt.pack_context
|
|
if stbtt.PackBegin(&pack, raw_data(atlas), GPU_FONT_ATLAS_SIZE, GPU_FONT_ATLAS_SIZE, 0, 1, nil) == 0 {
|
|
fmt.println("font pack failed:", font_path)
|
|
return false
|
|
}
|
|
stbtt.PackSetOversampling(&pack, 1, 1)
|
|
// Returns 0 when some codepoints are missing from the font; those are
|
|
// filtered out below via FindGlyphIndex, so partial packs are fine.
|
|
_ = stbtt.PackFontRanges(&pack, raw_data(font_bytes), 0, &ranges[0], i32(len(ranges)))
|
|
stbtt.PackEnd(&pack)
|
|
|
|
font: stbtt.fontinfo
|
|
has_font_info := bool(stbtt.InitFont(&font, raw_data(font_bytes), stbtt.GetFontOffsetForIndex(raw_data(font_bytes), 0)))
|
|
offset = 0
|
|
for r in extra_ranges {
|
|
for i in 0 ..< r.count {
|
|
code := rune(r.first + i)
|
|
if has_font_info && stbtt.FindGlyphIndex(&font, code) == 0 do continue
|
|
gpu.font_extra[code] = extra_chars[offset + i]
|
|
}
|
|
offset += r.count
|
|
}
|
|
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_batch_current :: proc(gpu: ^GPU_Renderer, kind: GPU_Batch_Kind, first: int) -> ^GPU_Batch {
|
|
if len(gpu.batches) > 0 {
|
|
last := &gpu.batches[len(gpu.batches) - 1]
|
|
if last.kind == kind do return last
|
|
}
|
|
append(&gpu.batches, GPU_Batch{kind = kind, first = first})
|
|
return &gpu.batches[len(gpu.batches) - 1]
|
|
}
|
|
|
|
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
|
|
batch := gpu_batch_current(gpu, .Rect, len(gpu.vertices))
|
|
batch.count += 6
|
|
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
|
|
batch := gpu_batch_current(gpu, .Text, len(gpu.text_vertices))
|
|
batch.count += 6
|
|
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)
|
|
}
|