From 22c104f766951ce16d88b51980ac06406bfee5e7 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 13:59:00 +0300 Subject: [PATCH 1/2] render3d: a command buffer holds at most 512 draws, and that many are drawn at start-up Metal pools the command storage a command buffer grew to and grows it only when one encodes more than any before (IOGPU under vkQueueSubmit < fn_gvk_once_end: +2-7 KB a window while a frame's draws climbed, 192 KB in the trail scene). gvk_draw submits the frame's command buffer after gvk_cb_cap_of draws (R3D_CB_DRAWS, 0 = none) and r3d_warm_commands draws that many through the normal path once r3d is ready. steady: a frame drawing 20 to 200 actors grows 0 (was 2.7-7.5 KB). Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/actor.ludic | 28 +++++++++++++++++++++++ packages/ludic.render3d/env.ludic | 2 ++ packages/ludic.render3d/gpu_vk_draw.ludic | 16 +++++++++++++ packages/ludic.render3d/render.ludic | 1 + 4 files changed, 47 insertions(+) diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 40af73a1..65eef2b5 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -448,3 +448,31 @@ function actor_release(render3d_st: mut Render3dState, a: Actor) -> void { if render3d_st.ac_spare == null { render3d_st.ac_spare = new []Actor } push(render3d_st.ac_spare, a) } + +# One command buffer's worth of draws at start-up, through the path every draw takes (pipeline, set, +# buffers, draw), so Metal's pooled command storage is already the size a frame can need: a bigger +# frame than any before once grew it in play (IOGPU, under vkQueueSubmit). Drawn into the screen +# image before the first frame clears it. +function r3d_warm_commands(render3d_st: mut Render3dState) -> void { + let n = gvk_cb_cap_of(render3d_st) + if Os.platform() != "macos" or n <= 0 or render3d_st.ac_lit == null { return } + let m = gpu_mesh_new(render3d_st) + let v = floats(24) + for k in 0 .. 3 { v[k * 8 + k] = 0.01; v[k * 8 + 4] = 1.0 } + gpu_mesh_vertices(render3d_st, m, data_of(v), 96, GPU_STATIC) + gpu_mesh_attr(render3d_st, m, 0, 3, GPU_F32, 32, 0, false) + gpu_mesh_attr(render3d_st, m, 1, 3, GPU_F32, 32, 12, false) + gpu_mesh_attr(render3d_st, m, 2, 2, GPU_F32, 32, 24, false) + let ix = words(3) + ix[0] = 0; ix[1] = 1; ix[2] = 2 + gpu_mesh_indices(render3d_st, m, data_of(ix), 12, 4) + m.count = 3 + gpu_mesh_done(render3d_st, m) + gpu_fb_bind(render3d_st, 0) + gpu_use_program(render3d_st, render3d_st.ac_lit.prog) + for i in 0 .. n { gpu_draw_mesh(render3d_st, m) } + gvk_flush(render3d_st) + mesh_release(render3d_st, m) + free(v) + free(ix) +} diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 446217d9..125ab03c 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -339,6 +339,8 @@ export state Render3dState { gvk_us_set: long = 0 gvk_us_draw: long = 0 gvk_n_draws: int = 0 + gvk_cb_draws: int = 0 # draws recorded into the frame's command buffer so far + gvk_cb_cap: int = -1 # a command buffer is submitted after this many (0 = never; -1 unread) gvk_n_asked: int = 0 # draws that reached gvk_draw_now: every one the renderer asked for gvk_n_flush: int = 0 gvk_prof_frames: int = 0 diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index fdb3bd3e..7d608d1e 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -1248,6 +1248,10 @@ function gvk_set_view(render3d_st: mut Render3dState, cb: pointer) -> void { # first / count select vertices or indices; count 0 means the mesh's own count. instances >= 1. function gvk_draw(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, first: int, count: int, instances: int, tx: words, tx_w: int, tx_cap: int, rec: words, rec_o: int) -> void { if render3d_st.gvk_prog_var == null or p <= 0 or p >= len(render3d_st.gvk_prog_var) or render3d_st.gvk_prog_var[p] == null { return } + let cap = gvk_cb_cap_of(render3d_st) + if cap > 0 and render3d_st.gvk_cb_draws >= cap { gvk_flush(render3d_st) } + if render3d_st.gvk_cb == null { render3d_st.gvk_cb_draws = 0 } + render3d_st.gvk_cb_draws += 1 let prof = gvk_prof(render3d_st) var t0: long = 0 if prof { t0 = gl_now_us() } @@ -1974,3 +1978,15 @@ function gvk_pipeline_fast(render3d_st: mut Render3dState, p: int, m: Mesh, st: if p < 4096 { render3d_st.gvk_pc_last[p] = len(render3d_st.gvk_pc_prog) - 1 } return pipe } + +# Draws a command buffer holds before it is submitted and the frame goes on in the next. Metal pools +# the storage a command buffer grew to and grows it only when one encodes more than any before, so +# with a bound and that many drawn at start-up (r3d_warm_commands) play never grows it. 0: no bound. +const GVK_CB_DRAWS: int = 512 +function gvk_cb_cap_of(render3d_st: mut Render3dState) -> int { + if render3d_st.gvk_cb_cap < 0 { + render3d_st.gvk_cb_cap = GVK_CB_DRAWS + if r3d_env_has(render3d_st, "R3D_CB_DRAWS") { render3d_st.gvk_cb_cap = Text.to_int(r3d_env(render3d_st, "R3D_CB_DRAWS")) } + } + return render3d_st.gvk_cb_cap +} diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 611ffc73..66a2650a 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -127,6 +127,7 @@ function r3d_load_step(render3d_st: mut Render3dState, i: int) -> bool { render3d_st.r3d_fog_density = 0.00024 render3d_st.r3d_fog_falloff = 0.002 render3d_st.r3d_ready = true + r3d_warm_commands(render3d_st) gpu_check(render3d_st, "r3d init") } return true From e235ec11f076c271f908f9e285a29c7f713cdd76 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 14:03:36 +0300 Subject: [PATCH 2/2] render3d: buffer primes in a command buffer of their own (as r3d/prime-fix) Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/gpu_vk_draw.ludic | 2 +- packages/ludic.render3d/gpu_vk_res.ludic | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index 7d608d1e..7f01eca8 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -1012,8 +1012,8 @@ function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer { # the frame in flight uses the other half of the ring and the other pool, unless it has this slot if render3d_st.gvk_frame_pending and (render3d_st.gvk_frame_pending_no & 1) == (render3d_st.gvk_frame_no & 1) { gvk_frame_wait(render3d_st) } gvk_frame_reset(render3d_st) + gvk_prime_submit(render3d_st) render3d_st.gvk_cb = gvk_once_begin(render3d_st) - gvk_prime_flush(render3d_st, render3d_st.gvk_cb) } return render3d_st.gvk_cb } diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index bdcc6b3f..7eab6624 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -775,6 +775,14 @@ function gvk_prime(render3d_st: mut Render3dState, b: int, buf: long) -> void { render3d_st.gvk_prime_h[render3d_st.gvk_prime_n] = buf render3d_st.gvk_prime_n += 1 } +# In a command buffer of their own, submitted and waited for at once: recorded into the frame's, a +# buffer released later in that frame (a guest loading its models) was destroyed before the copy ran. +function gvk_prime_submit(render3d_st: mut Render3dState) -> void { + if render3d_st.gvk_prime_n == 0 { return } + let cb = gvk_once_begin(render3d_st) + gvk_prime_flush(render3d_st, cb) + gvk_once_end(render3d_st, cb) +} function gvk_prime_flush(render3d_st: mut Render3dState, cb: pointer) -> void { if render3d_st.gvk_prime_n == 0 { return } if render3d_st.gvk_prime_dst == 0 and not gvk_prime_target(render3d_st) {