From 99f8749949017be60c9501b5dbb3d68c475f0d9e Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 13:41:35 +0300 Subject: [PATCH] render3d/vk: the kept descriptor sets start over after 256 textures are freed gvk_sc_make's cache (program + bound textures -> a set in the kept pool) evicted nothing until the pool's 8192 sets filled. A texture freed - a photograph's thumbnail, a wall frame, any picture a screen loads - left its entries and their sets behind for good, since its handle's generation moved on and the key could never match again: 262 KB over 12 minutes of the user's play, and bound only by the pool, hours away. gvk_tex_release counts the releases (gvk_sc_dead); at GVK_SC_DEAD_MAX (256) the next frame's start resets the kept pool and empties the cache (gvk_kpool_reset, which waits for the frame in flight), and each draw still in use makes its set again the first time it is drawn - the budget is the live combinations plus at most 256 textures' dead ones. The declared reason on gvk_sc_make was the swapchain's, copied; it now says what the cache is and what bounds it. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/env.ludic | 1 + packages/ludic.render3d/gpu_vk_draw.ludic | 8 +++++++- packages/ludic.render3d/gpu_vk_res.ludic | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 740379da..046ff6b1 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -169,6 +169,7 @@ export state Render3dState { gvk_family: int = -1 gvk_mp: bytes = null # VkPhysicalDeviceMemoryProperties gvk_device_name: string = "" + gvk_sc_dead: int = 0 # textures released since the kept-set cache was emptied gvk_read_px: bytes = null # gvk_read_screen's read-back, gvk_read_cap bytes, made again only at a new size gvk_read_cap: int = 0 gvk_renderer_name: string = "" # " (Vulkan)", made once with the device diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index ff212e57..6d51aa22 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -765,6 +765,10 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool { function gvk_frame_reset(render3d_st: mut Render3dState) -> void { # what MoltenVK autoreleased last frame goes back here (a no-op off macOS); R3D_VK_NOPOOL=1 for an A/B if not render3d_st.gvk_nopool { Vk.frame_pool() } + # a freed texture's kept sets never match again (its generation moved on) and were held until the + # pool filled - 8192 sets, hours of play: past GVK_SC_DEAD_MAX releases the pool starts over, and a + # draw still in use makes its set again the first time it is drawn + if render3d_st.gvk_sc_dead >= GVK_SC_DEAD_MAX { gvk_kpool_reset(render3d_st) } let slot = render3d_st.gvk_frame_no & 1 render3d_st.gvk_ring_off = slot * GVK_RING_BYTES render3d_st.gvk_ring_end = (slot + 1) * GVK_RING_BYTES @@ -790,6 +794,7 @@ function gvk_ring_put(render3d_st: mut Render3dState, blk: pointer, n: int) -> i # texture's handle and generation (bumped when the image behind a handle is replaced) and its # sampler, so a set is never matched against a texture it no longer describes. const GVK_KPOOL_SETS: int = 8192 +const GVK_SC_DEAD_MAX: int = 256 # textures released before the kept sets start over @alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") function gvk_kpool_make(render3d_st: mut Render3dState) -> bool { @@ -822,6 +827,7 @@ function gvk_sc_clear(render3d_st: mut Render3dState) -> void { } List.clear(render3d_st.gvk_sc_prog); List.clear(render3d_st.gvk_sc_koff); List.clear(render3d_st.gvk_sc_set); List.clear(render3d_st.gvk_sc_next) List.clear(render3d_st.gvk_sc_keys) + render3d_st.gvk_sc_dead = 0 if render3d_st.gvk_sc_head == null { render3d_st.gvk_sc_head = words(4096) } for i in 0 .. 4096 { render3d_st.gvk_sc_head[i] = -1 } } @@ -901,7 +907,7 @@ function gvk_draw_set(render3d_st: mut Render3dState, p: int, tx: words, tx_w: i } # a new kept set for program p with the textures in gvk_sc_tmp, written and cached -@alloc_ok("the window changed size or a surface was lost: the screen, the swapchain and its images are made again") +@alloc_ok("the kept-set cache: at most GVK_KPOOL_SETS (8192) sets and their keys, then gvk_kpool_reset empties the pool and the cache and it fills again") function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long { let zero: long = 0 let v = render3d_st.gvk_prog_var[p] diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index ec403195..94b27e96 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -493,6 +493,7 @@ function gvk_tex_release(render3d_st: mut Render3dState, tex: int) -> void { render3d_st.gvk_tex_image[tex] = zero; render3d_st.gvk_tex_view[tex] = zero; render3d_st.gvk_tex_mem[tex] = zero render3d_st.gvk_tex_levels[tex] = 0; render3d_st.gvk_tex_layers[tex] = 0; render3d_st.gvk_tex_vkfmt[tex] = 0 render3d_st.gvk_tex_gen[tex] = render3d_st.gvk_tex_gen[tex] + 1 + render3d_st.gvk_sc_dead += 1 # its kept sets are dead now (gvk_frame_reset) } # the per-level and per-layer views gvk_view_of made of a texture go with it: kept, each held its