fix(render3d): the Vulkan renderer stops leaking as it loads and draws

- every texture upload malloc'd a CPU copy of its pixels and never freed it (236 MB over the
  valley's boot, and again for every model loaded later): the pixels are converted straight
  into the mapped staging buffer
- the per-level and per-layer views gvk_view_of made outlived their texture, and on MoltenVK a
  view keeps its Metal texture alive: a released texture takes its views with it, and the cache
  is keyed by numbers instead of a string built on every call
- the Vulkan structs filled for a draw, pass, barrier, descriptor set, buffer, allocation or
  upload (about sixty call sites) come from a reused 1 MB scratch ring (gvk_tmp)
- the descriptor-set cache and the retired buffers are emptied in place, not replaced; the grass
  cull's dispatch arguments are made once
- macOS drains an autorelease pool each frame (Vk.frame_pool, lvk_frame_pool in vk_mac.ll)
- R3D_VK_PROF reports Vulkan objects made and destroyed by kind, and every cache's length
- examples/rendering/smooth presents through render3d, so it runs on Vulkan too

The full valley on headless Vulkan loads to 2.18 GB and holds (it passed 8 GB while loading
before). ludic-dev test 305/305, selfhost-test 33/33.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 23:03:38 +03:00
parent 9539f7bcec
commit 80764f85c1
11 changed files with 284 additions and 113 deletions

View file

@ -25,6 +25,9 @@ import "vk_api.ludic"
# Vk.sl_active() says whether it was. Each Vk.sl_* is the sl* export of the same name and returns
# its sl::Result (0 is eOk); -1 means there is no Streamline (macOS, or the DLL missing). The
# structs are plain memory, laid out as the SDK headers (include/sl_*.h, SDK 2.14.1) declare them.
# Once a frame: on macOS drains what MoltenVK and the layer autoreleased during the last one (a
# program pumping its own events has no other pool); nothing elsewhere.
extern function vk_frame_pool() = "lvk_frame_pool"
extern function vk_sl_prefer(on: int) = "lvk_sl_prefer"
extern function vk_sl_active() -> int = "lvk_sl_active"
extern function vk_sl_init(pref: pointer, sdk_version: long) -> int = "lsl_slInit"

View file

@ -225,3 +225,26 @@ entry:
%r = call i64 %fn(ptr %a0, ptr %a1)
ret i64 %r
}
; ---- the frame's autorelease pool --------------------------------------------------------------
; MoltenVK and CAMetalLayer autorelease objects on every frame (the drawable, each render pass's
; descriptor), and a program that pumps its own events never drains a pool: without this they are
; kept for the life of the process - 160 MB/s of small allocations in a windowed valley.
; lvk_frame_pool() pops the pool the last frame pushed and pushes the next one.
declare ptr @objc_autoreleasePoolPush()
declare void @objc_autoreleasePoolPop(ptr)
@lvk_pool = internal global ptr null
define void @lvk_frame_pool() {
entry:
%old = load ptr, ptr @lvk_pool
%have = icmp ne ptr %old, null
br i1 %have, label %pop, label %push
pop:
call void @objc_autoreleasePoolPop(ptr %old)
br label %push
push:
%p = call ptr @objc_autoreleasePoolPush()
store ptr %p, ptr @lvk_pool
ret void
}

View file

@ -363,3 +363,9 @@ entry:
%r = call i64 %fn(ptr %a0, ptr %a1)
ret i64 %r
}
; the frame's autorelease pool is a macOS matter (vk_mac.ll); Windows has nothing to drain
define void @lvk_frame_pool() {
entry:
ret void
}