refactor(render3d): vertex data and draws behind gpu.ludic

Every vertex array, vertex and index buffer, attribute pointer, instance divisor, stream
upload and draw call now goes through gpu_mesh_* / gpu_buffer_* / gpu_draw_*, and no other
file in the package names them. A Mesh records its layout as it is built - which buffer
feeds which attribute at what stride and offset, per vertex or per instance - so a backend
that bakes vertex input into a pipeline can read it back. On OpenGL each call is the GL it
replaces, in the same order: the five fixed viewpoints render bit-identically and the game's
self-tests report exactly what they did before.

scatter_attach takes the mesh rather than its vertex array; a mesh now frees every vertex
buffer it owns (glTF meshes used to keep all but the first); the two helpers nothing called,
mesh_grid_patches and mesh_instance_buffer, are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 10:21:55 +03:00
parent 3fb1b7cd87
commit 43c379aa0f
8 changed files with 271 additions and 267 deletions

View file

@ -1,34 +1,31 @@
# ============================================================================
# mesh.ludic — vertex data on the GPU: a Mesh record (VAO + buffers + a draw
# call), and the procedural meshes the renderer needs (a grid for the terrain,
# a full-screen triangle, a unit quad for instanced cards).
# mesh.ludic — vertex data on the GPU: a Mesh record (buffers + a draw call), and
# the procedural meshes the renderer needs (a grid for the terrain, a full-screen
# triangle, a unit quad for instanced cards). Built and drawn through gpu.ludic.
# ============================================================================
property Mesh {
vao: int = 0,
vbo: int = 0,
vao: int = 0, # OpenGL's vertex array object (gpu.ludic's, on that backend)
vbo: int = 0, # the first vertex buffer
ebo: int = 0,
count: int = 0, # indices (ebo != 0) or vertices
mode: int = 4, # GL_TRIANGLES
itype: int = 0x1405 # GL_UNSIGNED_INT
itype: int = 0x1405, # GL_UNSIGNED_INT
attrs: words, # the layout, GPU_ATTR_W words per attribute index (gpu.ludic)
n_attrs: int = 0,
vbufs: words, # every vertex buffer the mesh owns, freed with it
n_vbufs: int = 0,
cur_buf: int = 0, # the buffer the next attribute reads (while building)
ibuf: int = 0 # the instance buffer attached last
}
function mesh_draw(m: Mesh) -> void {
gl_bind_vertex_array(m.vao)
if m.ebo != 0 { gl_draw_elements(m.mode, m.count, m.itype, null) }
else { gl_draw_arrays(m.mode, 0, m.count) }
}
function mesh_draw_instanced(m: Mesh, n: int) -> void {
gl_bind_vertex_array(m.vao)
if m.ebo != 0 { gl_draw_elements_instanced(m.mode, m.count, m.itype, null, n) }
else { gl_draw_arrays_instanced(m.mode, 0, m.count, n) }
}
function mesh_draw(m: Mesh) -> void { gpu_draw_mesh(m) }
function mesh_draw_instanced(m: Mesh, n: int) -> void { gpu_draw_mesh_instanced(m, n) }
# A flat n x n vertex grid over [-half, half]^2 in x/z, y = 0. Attribute 0 = (x, z).
# The terrain vertex shader lifts it with the height map.
function mesh_grid(n: int, half: int) -> Mesh {
let m = new Mesh
m.vao = gl_vao()
let m = gpu_mesh_new()
let nv = n * n
let v = gl_floats(nv * 2)
var k = 0
@ -40,11 +37,8 @@ function mesh_grid(n: int, half: int) -> Mesh {
k += 2
}
}
m.vbo = gl_buffer()
gl_bind_buffer(GL_ARRAY_BUFFER, m.vbo)
gl_buffer_data(GL_ARRAY_BUFFER, gl_bytes_of(nv * 2), v, GL_STATIC_DRAW)
gl_enable_vertex_attrib_array(0)
gl_vertex_attrib_pointer(0, 2, GL_FLOAT, 0, 8, null)
gpu_mesh_vertices(m, v, gl_bytes_of(nv * 2), GPU_STATIC)
gpu_mesh_attr(m, 0, 2, GPU_F32, 8, 0, false)
free(v)
let ni = (n - 1) * (n - 1) * 6
let idx = words(ni)
@ -57,103 +51,41 @@ function mesh_grid(n: int, half: int) -> Mesh {
k += 6
}
}
m.ebo = gl_buffer()
gl_bind_buffer(GL_ELEMENT_ARRAY_BUFFER, m.ebo)
gl_buffer_data(GL_ELEMENT_ARRAY_BUFFER, ni * 4, idx, GL_STATIC_DRAW)
gpu_mesh_indices(m, idx, ni * 4, 4)
free(idx)
m.count = ni
gl_bind_vertex_array(0)
return m
}
# The grid as patches (4 control points per cell) for tessellation shaders.
function mesh_grid_patches(n: int, half: int) -> Mesh {
let m = mesh_grid(n, half)
# rebuild the index buffer as quads
let nq = (n - 1) * (n - 1) * 4
let idx = words(nq)
var k = 0
for j in 0 .. n - 1 {
for i in 0 .. n - 1 {
let a = j * n + i
idx[k] = a; idx[k + 1] = a + 1; idx[k + 2] = a + n + 1; idx[k + 3] = a + n
k += 4
}
}
gl_bind_vertex_array(m.vao)
gl_bind_buffer(GL_ELEMENT_ARRAY_BUFFER, m.ebo)
gl_buffer_data(GL_ELEMENT_ARRAY_BUFFER, nq * 4, idx, GL_STATIC_DRAW)
gl_bind_vertex_array(0)
free(idx)
m.count = nq
m.mode = GL_PATCHES
gpu_mesh_done(m)
return m
}
# A full-screen triangle with no attributes (the vertex shader uses gl_VertexID).
function mesh_fullscreen() -> Mesh {
let m = new Mesh
m.vao = gl_vao()
gl_bind_vertex_array(0)
let m = gpu_mesh_new()
gpu_mesh_done(m)
m.count = 3
return m
}
# A unit quad in x/y ([-0.5, 0.5] x [0, 1]) with uv, attribute 0 = xy, 1 = uv.
function mesh_card() -> Mesh {
let m = new Mesh
m.vao = gl_vao()
let m = gpu_mesh_new()
let v = gl_floats(16)
gl_put(v, 0, -0.5); gl_put(v, 1, 0.0); gl_put(v, 2, 0.0); gl_put(v, 3, 0.0)
gl_put(v, 4, 0.5); gl_put(v, 5, 0.0); gl_put(v, 6, 1.0); gl_put(v, 7, 0.0)
gl_put(v, 8, 0.5); gl_put(v, 9, 1.0); gl_put(v, 10, 1.0); gl_put(v, 11, 1.0)
gl_put(v, 12, -0.5); gl_put(v, 13, 1.0); gl_put(v, 14, 0.0); gl_put(v, 15, 1.0)
m.vbo = gl_buffer()
gl_bind_buffer(GL_ARRAY_BUFFER, m.vbo)
gl_buffer_data(GL_ARRAY_BUFFER, 64, v, GL_STATIC_DRAW)
gl_enable_vertex_attrib_array(0)
gl_vertex_attrib_pointer(0, 2, GL_FLOAT, 0, 16, null)
gl_enable_vertex_attrib_array(1)
gl_vertex_attrib_pointer(1, 2, GL_FLOAT, 0, 16, gl_ptr(null, 8))
gpu_mesh_vertices(m, v, 64, GPU_STATIC)
gpu_mesh_attr(m, 0, 2, GPU_F32, 16, 0, false)
gpu_mesh_attr(m, 1, 2, GPU_F32, 16, 8, false)
free(v)
let idx = words(6)
idx[0] = 0; idx[1] = 1; idx[2] = 2; idx[3] = 0; idx[4] = 2; idx[5] = 3
m.ebo = gl_buffer()
gl_bind_buffer(GL_ELEMENT_ARRAY_BUFFER, m.ebo)
gl_buffer_data(GL_ELEMENT_ARRAY_BUFFER, 24, idx, GL_STATIC_DRAW)
gpu_mesh_indices(m, idx, 24, 4)
free(idx)
m.count = 6
gl_bind_vertex_array(0)
gpu_mesh_done(m)
return m
}
# Attach a per-instance float buffer (n floats per instance, split into vec4
# attributes from `first_attr`) to a mesh's VAO. Returns the buffer id.
function mesh_instance_buffer(m: Mesh, first_attr: int, floats_per: int, data: pointer, count: int) -> int {
gl_bind_vertex_array(m.vao)
let b = gl_buffer()
gl_bind_buffer(GL_ARRAY_BUFFER, b)
gl_buffer_data(GL_ARRAY_BUFFER, gl_bytes_of(floats_per * count), data, GL_STATIC_DRAW)
var a = 0
var off = 0
while off < floats_per {
var sz = floats_per - off
if sz > 4 { sz = 4 }
gl_enable_vertex_attrib_array(first_attr + a)
gl_vertex_attrib_pointer(first_attr + a, sz, GL_FLOAT, 0, floats_per * 4, gl_ptr(null, off * 4))
gl_vertex_attrib_divisor(first_attr + a, 1)
a += 1
off += 4
}
gl_bind_vertex_array(0)
return b
}
# release a mesh's GL objects (a mesh this package built for something being thrown away)
function mesh_free(m: Mesh) -> void {
if m == null { return }
let ids = gl_scratch()
if m.vbo != 0 { ids[0] = m.vbo; gl_delete_buffers(1, ids); m.vbo = 0 }
if m.ebo != 0 { ids[0] = m.ebo; gl_delete_buffers(1, ids); m.ebo = 0 }
if m.vao != 0 { ids[0] = m.vao; gl_delete_vertex_arrays(1, ids); m.vao = 0 }
}
# release a mesh's GPU objects (a mesh this package built for something being thrown away)
function mesh_free(m: Mesh) -> void { gpu_mesh_free(m) }