This commit is contained in:
pavel 2026-07-08 10:28:32 +02:00
commit 5b65c4c0e4
11 changed files with 4237 additions and 297 deletions

View file

@ -37,16 +37,32 @@ GPU_Renderer :: struct {
text_transfer_buffer: ^SDL.GPUTransferBuffer,
font_texture: ^SDL.GPUTexture,
font_sampler: ^SDL.GPUSampler,
font_chars: [95]stbtt.bakedchar,
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.
@ -57,7 +73,7 @@ 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_ATLAS_SIZE :: 1024
GPU_FONT_PIXEL_HEIGHT :: 15.0
GPU_FONT_BASELINE_OFFSET :: 11.0
GPU_FONT_CELL_PADDING :: 1.0
@ -226,6 +242,8 @@ gpu_renderer_destroy :: proc(gpu: ^GPU_Renderer) {
SDL.DestroyGPUDevice(gpu.device)
}
delete(gpu.vertices)
delete(gpu.batches)
delete(gpu.font_extra)
delete(gpu.text_vertices)
gpu^ = {}
}
@ -249,6 +267,7 @@ gpu_create_shader :: proc(device: ^SDL.GPUDevice, name: string, code: []u8, stag
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)
@ -323,6 +342,35 @@ gpu_text_limited :: proc(gpu: ^GPU_Renderer, x, y: f32, text: string, max_chars:
}
}
// 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
@ -415,21 +463,27 @@ gpu_present :: proc(gpu: ^GPU_Renderer) {
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)
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)
@ -443,11 +497,68 @@ gpu_create_font_atlas :: proc(gpu: ^GPU_Renderer) -> bool {
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)
// 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)
@ -518,8 +629,19 @@ gpu_read_font_file :: proc() -> (bytes: []u8, path: string, owned: bool) {
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})
@ -530,6 +652,8 @@ gpu_push_quad :: proc(gpu: ^GPU_Renderer, x0, y0, x1, y1: f32, c: [4]f32) {
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})