render3d(vulkan): GPU-driven trees - one merged mesh per material, one draw per material; on by default

Every level of a kit tree or rock carries the same materials, so a GPU-culled layer merges each
material's levels into one mesh (layer_arena_build) and scatter_cull.comp writes a record per
(material, level) naming that level's index and vertex range; one indirect draw covers them all.
A conifer's lit pass and prepass go from 8 draws to 2, its shadow LOD from 6 to 2. Layers that do
not fit (card levels, other materials or attributes, 32-bit indices) keep the CPU path.

GPU culling is now the Vulkan default (R3D_GPU_CULL=0 turns it off). Camp benchmark, 400 frames:
PC 2791 -> 2657 draws, 4.3 -> 4.1 s (both runs); Mac 2791 -> 2657, 8.0/7.4 -> 7.7/7.2 s.
Self-tests 59/59 on both machines, PC validation 0 errors; frames within run-to-run noise; OpenGL
frames unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 16:44:00 +03:00
parent bda53f759e
commit d8300a7bf9
4 changed files with 130 additions and 26 deletions

View file

@ -10,6 +10,7 @@ property Prim {
diff: int = 0,
nrm: int = 0,
arm: int = 0,
verts: int = 0, # vertices in the mesh (each attribute buffer holds this many)
name: string # the material's name (a part to tint: "hk_jacket")
}
property Model {
@ -134,6 +135,7 @@ function gltf_prim(p: Val) -> Prim {
let m = gpu_mesh_new()
let attrs = value_get(p, "attributes")
gltf_attrib(m, attrs, "POSITION", 0)
let nverts = gltf_count
gltf_attrib(m, attrs, "NORMAL", 1)
gltf_attrib(m, attrs, "TEXCOORD_0", 2)
skin_attribs(m, attrs) # JOINTS_0 / WEIGHTS_0 onto 5 / 6, when the mesh has them
@ -145,6 +147,7 @@ function gltf_prim(p: Val) -> Prim {
m.count = gltf_count
gpu_mesh_done(m)
pr.mesh = m
pr.verts = nverts
# material textures
if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) }
pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white