render3d/vk: a buffer re-filled every frame ping-pongs between two instead of being made again

With one frame in flight, a buffer filled every frame (the interface's vertex
buffer, ov_vbo) was still being read by the frame on the GPU when the next
frame filled it, so gvk_buf_reserve released it and made a new VkBuffer and
its memory every frame - and every new buffer took a prime submit of its own.
Each handle now keeps a second buffer (gvk_bsp_*): a busy one swaps with it
when it is free, else the busy one becomes the second and one new buffer is
made; releasing a handle lets both go.

The launcher window (--launcher-ui, R3D_VK_PROF, per 120 frames): buffers
made/destroyed 120/120 -> 0/0, command buffers 240 -> 120 (one a frame). Its
footprint does not move with it (358 MB at 40 s, both): the churn was work,
not memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 14:45:02 +03:00
parent 5297996640
commit 2bb7ae1883
2 changed files with 81 additions and 1 deletions

View file

@ -382,6 +382,11 @@ export state Render3dState {
gvk_buf_size: []int = null
gvk_buf_map: []pointer = null # host-visible buffers stay mapped for their whole life
gvk_buf_used: []int = null # the frame (gvk_frame_no) a draw last bound the buffer in
gvk_bsp_buf: []long = null # per handle, a second buffer a re-filled one swaps with (gvk_buf_reserve)
gvk_bsp_mem: []long = null
gvk_bsp_size: []int = null
gvk_bsp_map: []pointer = null
gvk_bsp_used: []int = null
gvk_frame_no: int = 1
@max(4096) gvk_retired_buf: []long = null
@max(4096) gvk_retired_mem: []long = null

View file

@ -643,8 +643,11 @@ function gvk_buf_new(render3d_st: mut Render3dState) -> int {
if render3d_st.gvk_buf == null {
render3d_st.gvk_buf = new []long; render3d_st.gvk_buf_mem = new []long; render3d_st.gvk_buf_size = new []int; render3d_st.gvk_buf_map = new []pointer
render3d_st.gvk_buf_used = new []int; render3d_st.gvk_retired_buf = new []long; render3d_st.gvk_retired_mem = new []long
render3d_st.gvk_bsp_buf = new []long; render3d_st.gvk_bsp_mem = new []long; render3d_st.gvk_bsp_size = new []int
render3d_st.gvk_bsp_map = new []pointer; render3d_st.gvk_bsp_used = new []int
# handle 0 is "no buffer", as it is on OpenGL
push(render3d_st.gvk_buf, zero); push(render3d_st.gvk_buf_mem, zero); push(render3d_st.gvk_buf_size, 0); push(render3d_st.gvk_buf_map, null); push(render3d_st.gvk_buf_used, 0)
gvk_bsp_grow(render3d_st)
}
# a handle a deleted buffer gave back first (gvk_buf_delete)
if render3d_st.gvk_buf_spare != null and len(render3d_st.gvk_buf_spare) > 0 {
@ -654,6 +657,7 @@ function gvk_buf_new(render3d_st: mut Render3dState) -> int {
return b
}
push(render3d_st.gvk_buf, zero); push(render3d_st.gvk_buf_mem, zero); push(render3d_st.gvk_buf_size, 0); push(render3d_st.gvk_buf_map, null); push(render3d_st.gvk_buf_used, 0)
gvk_bsp_grow(render3d_st)
return len(render3d_st.gvk_buf) - 1
}
@ -670,6 +674,7 @@ function gvk_buf_delete(render3d_st: mut Render3dState, b: int) -> void {
}
function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void {
gvk_bsp_drop(render3d_st, b)
if render3d_st.gvk_buf[b] == 0 { return }
let zero: long = 0
if gvk_buf_busy(render3d_st, b) {
@ -740,7 +745,15 @@ function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool
if b <= 0 or b >= len(render3d_st.gvk_buf) { return false }
# already big enough, and no draw this frame reads what is there: fill it in place
if render3d_st.gvk_buf[b] != 0 and render3d_st.gvk_buf_size[b] >= n and (not gvk_buf_busy(render3d_st, b) or gvk_buf_is_gpu(render3d_st, b)) { return true }
gvk_buf_release(render3d_st, b)
# big enough but read by a frame still on the GPU: its second buffer, when that one is free, and
# else the busy one becomes the second and a new one is made - a buffer re-filled every frame (the
# interface's vertices) ping-pongs between two, where it was made and destroyed every frame
if render3d_st.gvk_buf[b] != 0 and render3d_st.gvk_buf_size[b] >= n {
if gvk_bsp_swap(render3d_st, b, n) { return true }
gvk_bsp_park(render3d_st, b)
} else {
gvk_buf_release(render3d_st, b)
}
var size = n
if size < 64 { size = 64 }
let size_l: long = size
@ -853,3 +866,65 @@ function gvk_prime_tables(render3d_st: mut Render3dState) -> void {
let none: long = 0
for i in 0 .. GVK_PRIME_MAX { push(hs, none) }
}
# ---- a re-filled buffer's second: gvk_buf_reserve swaps to it while the first is still being read ----
@alloc_ok("one slot per buffer handle, beside the handle's own (gvk_buf_new)")
function gvk_bsp_grow(render3d_st: mut Render3dState) -> void {
let zero: long = 0
push(render3d_st.gvk_bsp_buf, zero)
push(render3d_st.gvk_bsp_mem, zero)
push(render3d_st.gvk_bsp_size, 0)
push(render3d_st.gvk_bsp_map, null)
push(render3d_st.gvk_bsp_used, 0)
}
function gvk_bsp_busy(render3d_st: Render3dState, b: int) -> bool {
let u = render3d_st.gvk_bsp_used[b]
return u == render3d_st.gvk_frame_no or (render3d_st.gvk_frame_pending and u == render3d_st.gvk_frame_pending_no)
}
# the second in place of the first when it is there, big enough and read by nothing still running
function gvk_bsp_swap(render3d_st: mut Render3dState, b: int, n: int) -> bool {
if render3d_st.gvk_bsp_buf == null or b >= len(render3d_st.gvk_bsp_buf) { return false }
if render3d_st.gvk_bsp_buf[b] == 0 or render3d_st.gvk_bsp_size[b] < n or gvk_bsp_busy(render3d_st, b) { return false }
let tb = render3d_st.gvk_buf[b]
let tm = render3d_st.gvk_buf_mem[b]
let ts = render3d_st.gvk_buf_size[b]
let tp = render3d_st.gvk_buf_map[b]
let tu = render3d_st.gvk_buf_used[b]
render3d_st.gvk_buf[b] = render3d_st.gvk_bsp_buf[b]; render3d_st.gvk_buf_mem[b] = render3d_st.gvk_bsp_mem[b]
render3d_st.gvk_buf_size[b] = render3d_st.gvk_bsp_size[b]; render3d_st.gvk_buf_map[b] = render3d_st.gvk_bsp_map[b]
render3d_st.gvk_buf_used[b] = render3d_st.gvk_bsp_used[b]
render3d_st.gvk_bsp_buf[b] = tb; render3d_st.gvk_bsp_mem[b] = tm; render3d_st.gvk_bsp_size[b] = ts
render3d_st.gvk_bsp_map[b] = tp; render3d_st.gvk_bsp_used[b] = tu
return true
}
# the busy first becomes the second (the old second let go), leaving the handle empty for a new one
function gvk_bsp_park(render3d_st: mut Render3dState, b: int) -> void {
let zero: long = 0
gvk_bsp_drop(render3d_st, b)
render3d_st.gvk_bsp_buf[b] = render3d_st.gvk_buf[b]; render3d_st.gvk_bsp_mem[b] = render3d_st.gvk_buf_mem[b]
render3d_st.gvk_bsp_size[b] = render3d_st.gvk_buf_size[b]; render3d_st.gvk_bsp_map[b] = render3d_st.gvk_buf_map[b]
render3d_st.gvk_bsp_used[b] = render3d_st.gvk_buf_used[b]
render3d_st.gvk_buf[b] = zero; render3d_st.gvk_buf_mem[b] = zero; render3d_st.gvk_buf_size[b] = 0
render3d_st.gvk_buf_map[b] = null; render3d_st.gvk_buf_used[b] = 0
}
# the second let go: destroyed now, or retired until the frame that read it has finished
function gvk_bsp_drop(render3d_st: mut Render3dState, b: int) -> void {
if render3d_st.gvk_bsp_buf == null or b >= len(render3d_st.gvk_bsp_buf) or render3d_st.gvk_bsp_buf[b] == 0 { return }
let zero: long = 0
if gvk_bsp_busy(render3d_st, b) {
@alloc_ok("within the retire lists' room made at start-up (@max 4096)")
push(render3d_st.gvk_retired_buf, render3d_st.gvk_bsp_buf[b])
@alloc_ok("within the retire lists' room made at start-up (@max 4096)")
push(render3d_st.gvk_retired_mem, render3d_st.gvk_bsp_mem[b])
@alloc_ok("within the retire lists' room made at start-up (@max 4096)")
while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) - 1 { push(render3d_st.gvk_retired_frame, 0) }
@alloc_ok("within the retire lists' room made at start-up (@max 4096)")
push(render3d_st.gvk_retired_frame, render3d_st.gvk_bsp_used[b])
} else {
render3d_st.gvk_mk_x_buf += 1
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_bsp_buf[b], render3d_st.gvk_ac)
gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_bsp_mem[b]))
}
render3d_st.gvk_bsp_buf[b] = zero; render3d_st.gvk_bsp_mem[b] = zero; render3d_st.gvk_bsp_size[b] = 0
render3d_st.gvk_bsp_map[b] = null; render3d_st.gvk_bsp_used[b] = 0
}