render3d/vk: a sub-allocation takes a whole number of its alignments, so the free list stays small

The valley self-test died after the Lamar swap, loading Maroon's pine LODs:
gvk_fr_blk (the device suballocator's free ranges) grew past its @max(4096).
Nothing failed to coalesce: a range was carved at the buffer's bare size, so
what was left after it started unaligned and the next buffer, rounding its
start up, left a sliver before it that no later buffer could use. One host
block held 3,968 small buffers and 3,718 free ranges between them - 496 KB
free in all, 133 bytes a hole. Those merge away when a neighbour is freed, so
nothing leaked; the count simply rose with live buffers.

gvk_mem_new now carves `span`, the size rounded up to the alignment, and
records it as the allocation's length, so a free gives back exactly what was
carved and the rest of a range always starts aligned.

The same repro (the lab's `tests` scene, headless, a fresh test root): the list
never reached 256 slots (it passed 4096 before), the run completes (exit 0),
LAMAR OK and THINGS OK. r3d_small was 0 throughout, as ECS said.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 14:56:14 +03:00
parent 22a384a76a
commit 0c6dab39a8

View file

@ -424,6 +424,10 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
} }
let mp = render3d_st.gvk_map_slot let mp = render3d_st.gvk_map_slot
mp[0] = null mp[0] = null
# a range in a block is carved a whole number of alignments long, so what is left after it starts
# aligned: carved at the bare size, every small buffer left a sliver before the next one that no
# later one could use - 3,718 of them among 3,968 buffers, and the free list past its bound
let span = (size + align - 1) / align * align
if size <= gvk_own_over(render3d_st) { if size <= gvk_own_over(render3d_st) {
var kind = t * 2 var kind = t * 2
if image { kind += 1 } if image { kind += 1 }
@ -433,12 +437,12 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
if n > 0 and render3d_st.gvk_blk_kind[render3d_st.gvk_fr_blk[i]] == kind { if n > 0 and render3d_st.gvk_blk_kind[render3d_st.gvk_fr_blk[i]] == kind {
let start = (render3d_st.gvk_fr_off[i] + align - 1) / align * align let start = (render3d_st.gvk_fr_off[i] + align - 1) / align * align
let end = render3d_st.gvk_fr_off[i] + n let end = render3d_st.gvk_fr_off[i] + n
if start + size <= end { if start + span <= end {
blk = render3d_st.gvk_fr_blk[i]; off = start blk = render3d_st.gvk_fr_blk[i]; off = start
let front = start - render3d_st.gvk_fr_off[i] let front = start - render3d_st.gvk_fr_off[i]
let back = end - (start + size) let back = end - (start + span)
if front > 0 { render3d_st.gvk_fr_len[i] = front } else { render3d_st.gvk_fr_len[i] = 0 } if front > 0 { render3d_st.gvk_fr_len[i] = front } else { render3d_st.gvk_fr_len[i] = 0 }
gvk_fr_put(render3d_st, blk, start + size, back) gvk_fr_put(render3d_st, blk, start + span, back)
} }
} }
i += 1 i += 1
@ -459,7 +463,7 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
render3d_st.gvk_blk_mem[blk] = mem; render3d_st.gvk_blk_kind[blk] = kind; render3d_st.gvk_blk_size[blk] = bsize; render3d_st.gvk_blk_map[blk] = mp[0] render3d_st.gvk_blk_mem[blk] = mem; render3d_st.gvk_blk_kind[blk] = kind; render3d_st.gvk_blk_size[blk] = bsize; render3d_st.gvk_blk_map[blk] = mp[0]
} }
off = 0 off = 0
gvk_fr_put(render3d_st, blk, size, bsize - size) gvk_fr_put(render3d_st, blk, span, bsize - span)
} }
} }
} }
@ -481,7 +485,9 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
push(render3d_st.gvk_al_blk, -1); push(render3d_st.gvk_al_mem, zero); push(render3d_st.gvk_al_off, 0); push(render3d_st.gvk_al_len, 0); push(render3d_st.gvk_al_map, null) push(render3d_st.gvk_al_blk, -1); push(render3d_st.gvk_al_mem, zero); push(render3d_st.gvk_al_off, 0); push(render3d_st.gvk_al_len, 0); push(render3d_st.gvk_al_map, null)
a = len(render3d_st.gvk_al_blk) - 1 a = len(render3d_st.gvk_al_blk) - 1
} }
render3d_st.gvk_al_blk[a] = blk; render3d_st.gvk_al_mem[a] = own; render3d_st.gvk_al_off[a] = off; render3d_st.gvk_al_len[a] = size; render3d_st.gvk_al_map[a] = map var len_ = size
if blk >= 0 { len_ = span } # what the block gives back when it is freed
render3d_st.gvk_al_blk[a] = blk; render3d_st.gvk_al_mem[a] = own; render3d_st.gvk_al_off[a] = off; render3d_st.gvk_al_len[a] = len_; render3d_st.gvk_al_map[a] = map
return a return a
} }