Set before r3d_open: the frame's uniform ring is 2 x 4 MB (was 2 x 64), memory blocks are 8 MB with
anything over 4 MB on its own (was 64 and 16), and the descriptor pools are an eighth of a world's.
Maroon Lake's launcher window draws a picture and text and never becomes the game; its launcher home
and Settings page draw pixel-identical on it. The full-size path is unchanged (steady: STEADY OK).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gvk_swap_make freed none of what it made - the surface caps, the count, the
format and present-mode lists, the create info, the handle out - and replaced
gvk_swap_images without freeing it: a few hundred bytes every time the window
changed size, went to or from Retina, or the chain came back suboptimal. Each is
now freed on every way out (the minimised return and the failed create included),
and the old image list before the new one is made.
A windowed memory walk built against it (main 5ee600d2, 300 s, the autopilot
opening screens every 6 s): footprint 2816 MB at 120 s, 2818 MB at 300 s; the
fence judged 0 frames kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Every tree on the map was drawn as an impostor card into all the cascades,
the near two included, whose receivers (within 60 m) are shaded by the near
trees' own LOD2 meshes cast beside them. The cards now cast from cascade 2 on.
Aspen view, same main and foundations, 240 frames, with the figure capsule:
shadow instances 323,587 -> 202,743; shadow GPU 4808-4942 -> 4579/4578 us;
median frame 17200-17693 -> 17178/17205 us. The look held: shots of the view
taken twice a side are identical within a side (0.00% of pixels past 8) and
differ by 0.04% across (every pairing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In cascades 2 and out (receivers from 60 m) a skinned figure of more than
eight pieces - a dressed person, up to 86 of them - casts one capsule its own
height (radius 13% of it) instead of every visible piece: past 60 m nobody
can tell a garment's shadow from the body's. Animals (a piece or two), rigid
props and the near two cascades are unchanged. The capsule is made once.
Aspen view, same main (30ec1722) and foundations (15f1020), 240 frames, twice
a side: shadow draws 1889 -> 1799; shadow GPU 4808/4819 -> 4710/4714 us;
median frame 17200/17221 -> 16910/17128 us. Most of the far cascades' actor
draws turned out to be rigid props (the census's "skinned" counts draws, not
actors), so this is the smaller of the two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user saw kit grass, ferns and flowers lit up at night. Nothing was
emissive and every layer draw binds the scene's lighting; what differed was
occlusion. At night the sky's light is nearly all the light, and it is
scaled by ambient occlusion: the meadow's blades are heavily occluded near
the ground, while a kit plant's own AO map knows nothing of the grass around
it, so it took the full sky. By day the sun hides the difference.
model.vert hands on each vertex's height above the model's base (v_lh), and
a non-blade FOLIAGE plant under 2 m scales its AO from 0.35 at the ground to
1 at 0.9 m. Crowns and the blades are untouched.
Measured in the lab (R3D_AT=tree, same binary, old and new SPIR-V): the fern
against the meadow beside it was 2.05x at 23:00 and 1.66x at 13:00; now 1.58x
and 1.60x - the same relation by night as by day. The flowers' brightest
tenth at night 78 -> 52. The noon frame moves 8.4% of pixels past 8 (the fern
and flower bases a little darker; run-to-run noise is part of that).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user's play found the aspens' branches swinging like rope. Three terms:
- the whole-tree bend reached 22% of the tree's height at a gust's top (three
metres on a 14 m aspen); 4.5% now, and the side-to-side part at a quarter of
that and slower;
- a branch term: nothing within 0.35 m of the trunk, growing with the distance
out, phased by the direction the branch leaves the trunk (constant along a
branch, so it moves as one piece and its neighbours are out of step);
- the aspen's leaf flutter took its phase from each vertex at eleven radians a
metre, which bent every twig along its length; about three now, and half the
amplitude.
Measured on the lab's `aspen` view (10-frame bursts, crowns only, same binary
with the old and new SPIR-V): pixels moving more than 8 over ten frames 16.8%
-> 9.7%, frame to frame 4.4% -> 2.1%. SPIR-V rebuilt with `ludic-dev shaders`
(vertex stages only; the manifest is unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every read of the screen made bytes(screen_w * screen_h * 4) and dropped it:
the user's manual play left 22,970,368 B unreachable (exactly 3024x1898x4)
at gpu_vk_draw.ludic:1673 - one per photograph (shots' ph_grab, through
gpu_read_screen_bytes). The read-back is now gvk_read_px, made once and made
again (the old one freed) only when the screen's size changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
water_reflection_pass made view(water_saved, 16, 16) and view(water_saved, 32, 16)
inside the block that makes the reflection target, so every time the target was
made again (a resize, a render scale, a settings change) two more 16-byte views
were made and the last two dropped: the windowed walk's fence caught one such
frame, +32 B. water_saved is the state's for good, so its views are made once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
render3d: shadow_fit, water_reflection_pass, layer_partition_lods and the
GPU cull's scratch are made with the state; v3_dist is scalar; the pushes
into lists sized at start-up, the caps probe, the table growth, the loads
and the constructors declared with their bounds (one statement a line);
the renderer's name made once with the device; the two error messages
given back; the dead lupine models removed.
runtime: a component's text is held interned in its value cell (one copy
per distinct text), so the getter's own text goes with its frame instead
of being kept by ludic.ui's model - 80 of the 83 keeps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gvk_screenshot kept a buffer the size of the screen per shot (the valley scan's largest unreachable
site), and its PPM header and row buffer; each goes on every way out now. Compiled (steady).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The valley's reachability scan (R3D_ALLOC_SCAN) listed render3d start-up objects nothing held; all were
dropped after being handed to the driver or copied into a key: gvk_module's SPIR-V bytes, create info
and handle slot; gvk_program's key parts, .spv paths, bindings and layout create infos; the compute
program's the same; gvk_sampler's and gvk_view_of's create infos; gvk_zero_vbuf_get's 64 KB of zeros;
gvk_layout_key's result (always a copy now, freed by gvk_pipeline once its key holds it) and
r3d_program's defines. Each goes once the handle it made has been read. Compiled (steady).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Target's framebuffer and its colour and depth textures, a Mesh's index buffer (gvk_buf_new /
gvk_buf_delete now @creates / @releases GpuBuffer too), a Prim's mesh and an Actor's own skin clone.
ludic deps --resources over main: resource_drops 0, owned_leaks 0; a probe freeing a Target's fbo
alone was reported for its colour and depth (2), then removed. Compiled (steady).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pipeline has no release: it is kept in the cache for the program's life (gvk_pipe_build stores it).
With every render3d handle annotated, ludic deps --resources over main e447acdd reads 0 drops; a
GpuBuffer made and bound to a local in a game function was reported (1), so the 0 is a real one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The retire, free-range and spare-id lists (4096), the per-buffer flags and the prime handles (16384),
the stage and the actor spares (2048), the per-program uniform offsets (4096) and a draw's set key
(130) are each @max'd at the room gvk_startup_state / actor_init made, so outgrowing one ends a dev
run with its name instead of quietly copying. Compiled (steady).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The texture and framebuffer tables grow to 4096/1024 and gvk_prime's queue is made in
gvk_startup_state; gpu_unit_2d's and the actor lists' lazy starts go. @alloc_ok on layer_room (a layer
outgrowing its cap), overlay_init, gvk_read_screen, gvk_hdr_metadata (a settings change), DLSS's
gsl_struct/gsl_fn, an actor's part tables (given back by actor_release) and the pool's fallback.
Birth leaks freed: gpu_caps_probe's create structs, gpu_caps_fake's text, gvk_note's line,
gvk_layout_key's table and each key it grew from, ov_text_wrap's slices, the default sky path.
(ludic deps --births still lists freed sites: its walk does not see free().) Compiled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seventeen inline messages (allocation failures, missing conversions, the no-pipeline and compressed-
target warnings, resize and swapchain lines, the test-frame camera line, DLSS evaluate, shadow memory,
stream and terrain debug, the water test) are each a function of their own under @alloc_ok, taking
numbers, so the paths that say them hold no site. post_measure's two 1x1 exposure targets are made in
post_init and DLSS's evaluate structs in gsl_init; DLSS's camera up is a state field. The lists play
pushes into (retired, free ranges, spare ids, the per-buffer gpu flags, the stage and the actor spares)
get their room at start-up (gvk_room_*, actor_room), so a push fits. Compiled (steady).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Render3dState's cam_planes, gpu_u_tmp, q_scratch, ov_nine_buf, the caster test's four points,
ter_bw, ter_scr and water_saved default to their buffers (as ludic.anim's clip state does), so the
lazy starts in cam_planes_update, gpu_tmp, terrain_height_smooth, ter_scratch, the water pass and
gvk_startup_state go. q_euler makes its views of q_scratch once (guarded on the view now). Compiled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shadow_pass's fbo status and probe block, shadow_bind's two location prints, layer_partition_lods'
LOD line and layer_update's dump are each a function of their own under @alloc_ok (debug switches
only), so the passes themselves hold no allocation site. Compiled (steady).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The renderer starts itself from the first frame (gpu_select), so the device, its tables, the
manifest and programs, grass, shadows, sky, terrain textures and the actor pool read as frame
allocations: each is @alloc_ok as start-up, with gvk_fail (a failure) and the actor census and
texture dump (debug switches). ov_nine's four corner/uv arrays are one floats(16) made in
gvk_startup_state; ac_in_light's four points are made there too. Compiled (steady, and ludic deps
over main 4316ff97).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
actor_own_skin(a) gives an actor its own clone of its model's skeleton and actor_release frees it
(skin_clone_free: the pose, matrices, views and scratch the clone made; the rest data stays its
source's). gvk_startup_state, at the end of gvk_init, makes the scratch and tables a draw used to
make on first need (gvk_tmp_buf, the pipeline fast cache, the set key and per-program uniform
offsets pre-sized to 4096 programs, skip/seen/size words, spare and retired lists, the grass cull's
words), and those frame paths no longer start them. Compiled (steady).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alloc_ok with its reason on what is made once per resource and kept (textures, programs, samplers,
views, layouts, memory blocks, the pipeline cache in gvk_pipe_build), on resize and swapchain
remakes, on screenshots and dumps, on a world being set up (streams, layers, water, post, bakes), on
loads (gltf_load, skin_load, tex_load*, png_decode, fonts) and on R3D_PROF / drawstats.
gltf_cached's hit path is its own and makes nothing; the miss (gltf_cached_load) is declared.
m4_look_at (now over m4_look_at_xyz) and m4_inverse work on scalars, and cam_update makes no scratch.
Left: lazy first-use starts, error and debug prints, and scratch made and freed each call (churn,
plan 25.3). Compiled (steady, and ludic deps over the game).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gvk_pipeline_fast's miss (the build and the six cache pushes) is its own function, so the fence can
declare it: @alloc_ok("pipeline cache: one per variant the game draws") once lang/memory-fence's
compiler is merged (the annotation is not known on this base). Compiled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plan 25.1c: vk_mac.ll's @lvk_ac (posix_memalign under a 16-byte header of scope/offset/size, atomic
counters) passed at every render3d create/destroy (49 sites; the caps probe keeps its own null pair);
lvk_ac_bytes/_peak/_allocs/_scope_bytes for the fence, Vk.alloc_bytes. vk_win.ll: null and 0.
MoltenVK 1.4.2 counted 0 live bytes through them in steady. Fence findings: gvk_pipeline freed its 17
create infos (and reuses one bufs list); actor_init fills ac_spare with 512 records (actor_fresh,
m4_new, v3_new at first placement in play). Compiled, not run (the user's call).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A co-op guest loading its models segfaulted in copyBufferToBuffer under vkQueueSubmit (0x68, AGX
LegacyBlitContext): a buffer primed into the frame's command buffer was released later in that
frame, and gvk_buf_release destroys a buffer no draw has marked, so the copy read a freed one. The
copies now run in their own command buffer before the frame's begins, checked against the slot's
handle as they are recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lp_os_platform and lp_os_arch malloc'd 8 KB per call (the uname buffer) and kept none of it:
string_temps now asks both every round, 327 MB over 20,000 before, 0 after. Reseeded. render3d:
MoltenVK made a mapped buffer's MTLBuffer at its first bind (fn_gvk_draw +4 blocks in the boat
window); gvk_buf_reserve queues it and the next frame's command buffer copies 4 bytes out of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stream_update made a Chunk and a words copy per new chunk (fn_stream_update +39 blocks / 13.2 KB a
window with the hiker in the drifting boat). The pool holds STREAM_MAX_CHUNKS records; the arena
is twice the layer's cap; eviction compacts it; a chunk that cannot be kept is gathered from the
scratch. stream_clear_all frees records, lists and streams. steady: 300 new cells, cap 256: 156 KB
before, 0 / -4.9 KB after, and every kept chunk's data checked against its cell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pools kept every command object ever recorded, so the heap grew each time a frame drew more than
any before (fn_gvk_draw under vkCmdBindVertexBuffers/BindIndexBuffer/Draw in the leakcheck; ~650 B a
draw). Off: 3.7 ms a frame either way over 520 actors. steady: 20 to 200 actors grows 5-7 KB with it
off and 120 KB with it on (bound 16 KB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gvk_tex_storage freed none of ici, out, req and vci (8 blocks a two-texture model); tex_load_ex kept
dds_path_of's string. Found with malloc_history over 400 load/release rounds (712 bytes a round, all
of it these). steady: the model's bound is 4 KB over 200 (was 1.6 MB), and the frame is the least of
three settled windows - a valley self-test beside it read 87 KB once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A freed texture or buffer id goes on a spare list the next one takes; tex_note_size keeps sizes by id.
model_release frees prims, meshes, material names, the skin and leaves gltf_cached. actor_release
takes an actor off the stage and actor_new reuses its record (ECS's Things come and go all day).
steady.ludic: an actor round at 0 bytes over 2000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game placing props while it plays (a sign an animal leaves, a fish, a bobber) loaded each through
gltf_load, which reads the file and parses its document every time. gltf_cached finds the same
(dir, file, node) by comparing the names in place and hands back the model the first load made;
a model whose document is read after the load (ludic.anim's clips) still goes through gltf_load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Json.free_all (value_free_all): a parsed tree's nodes, lists and strings. render3d frees each
glTF document that way at the next load; the names kept out of it are copies (a primitive's
material, a skin's joints, an animation clip's name in ludic.anim) - a model's strings were
~640 KB left behind per load
- jp_number made a digits list per decimal in a document and never freed it
- gvk_layout_id matches a mesh's layout as numbers in a scratch made once, against the layouts
known end to end; a new mesh no longer builds a key string
- gvk_mem_new puts a new block into the record of one given back rather than appending, so a
buffer made again every few frames no longer grows the block lists
steady.ludic adds a glTF parsed and freed whole 200 times: 0 bytes (38,400 before the digits fix),
beside the buffer path and the frame, still 0. A model loaded and let go still keeps ~2 KB a round
(texture and buffer handles are not reused yet); it is bounded at 8 KB a round.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
n x n samples of chunk (i, j) of a size_m grid from the terrain's corner, row-major into the
caller's buffer with nothing allocated, each the (1 4 1) / 6 B-spline filter of the texels under it
(ter_spline_at): what ludic.physics' jph_shape_heightfield_bspline makes of the whole map, so a
chunk's physics ground matches the drawn one and its neighbours' to the bit. The read-back lives
for the whole map (terrain_generate to terrain_unload). examples/rendering/chunks.ludic, in the
suite, checks the shared edges, a sample against the filter by hand, and a short buffer refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gvk_mem_new made a one-slot []pointer per allocation, and turned the requirement's size and
alignment into strings to read them as ints; gvk_list_drop_last rebuilt the spare-record list to
drop its last entry; gvk_mem_id did the string round trip on every free. One slot is kept
(gvk_map_slot), int() truncates a long, the spare list pops. Every Text.to_int(string(x)) in
render3d is int(x) now.
Vk.heap_bytes() (vk_mac.ll: malloc_zone_statistics' size_in_use; 0 on Windows) and
examples/rendering/steady.ludic, in the suite: a buffer released and made again 5000 times and
600 whole frames gain 0 bytes each - the allocator before this, 1,120,000 over the 5000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Metal aborted a net.sh guest ("MTLPixelFormatBC7_RGBAUnorm is not color renderable"): the crash
report's main thread was in vkQueueSubmit from gvk_once_end, MoltenVK encoding a vkCmdBlitImage
through a render pipeline. water.ludic bound its reflection by sampler name and then asked the
BOUND texture for mips - on Vulkan the bound texture was not the reflection but the last one
bound, since 23.3 a BC7 kit texture. gpu_bind_sampler now makes its texture the bound one, as
OpenGL did (and water binds it explicitly); gvk_tex_mips and gvk_mips_now skip compressed images;
gvk_pass_begin leaves out a compressed colour attachment and says so.
tests/net.sh passes (main e515bd87 built against it; host and guest agree on 196 animals, the
people, the board and the lost hiker).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first generation pass's R32F height is kept as ter_height_tex (not copied into an RGBA32F),
so every reader of the height - placement, the read-back, physics, selftest16's 9 mm - sees the
same 32 bits. ternormal.frag writes the baked normal's x and z into ter_normal_tex (RG16F), and
the six places that read it (terrain.frag twice, tersun.frag, grass.vert, grass.mesh,
grass_cull.comp, which takes a third texture at binding 6) rebuild y. SPIR-V regenerated.
Maroon Lake's play, headless Vulkan: 2793 -> 2647 MB. The camp's frame: 0.066% of pixels differ by
more than 8 (mean 0.024/255), isolated grass blades at the slope gate. ludic-dev test 307/307.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The device's textureCompressionBC is asked for and remembered (gvk_has_bc). tex_load_ex prefers a
DX10 .dds with the full chain beside the .png (not for an edge-padded cut-out atlas):
texture_dds.ludic reads BC7 / BC5 / BC4, gpu_tex_compressed makes the image with every level and
no colour-attachment use (a compressed image is only sampled and copied into), and
gvk_tex_upload_blocks copies each level's blocks from one staging buffer. A colour map is BC7
sampled as sRGB, a data map BC7 read as it is. examples/rendering/bc.ludic holds it, in the suite;
ludic.lab's plate carries its .dds (its three shots render at 56-60 dB against the .png's).
ludic-dev test 307/307, no Vulkan SDK in the environment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
native/build.sh builds MoltenVK v1.4.2 from its pinned, checksummed tag (its dependencies at the
commits its ExternalRevisions pins), thinned to arm64, id @rpath/libMoltenVK.dylib, signed ad hoc;
its licence goes in native/LICENSE-MoltenVK. Every render3d program links it through its rpath -
the package's lib/ while developing, Contents/Frameworks in a bundle, where ludic bundle puts and
signs it - and vk_mac.ll also looks for @rpath/libMoltenVK.dylib (after an SDK loader, so the
validation layer still stacks in development). The suite's SDK stand-in (vk_env) is gone: the
render checks draw on the MoltenVK the package carries. gpu_is_gl() removed; nothing calls it.
ludic-dev test 306/306 with no Vulkan SDK in the environment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- gvk_sampler built a key string on every call it was reached (a texture read two ways in turn
misses its per-texture cache each time); samplers are found by their seven numbers instead
- gvk_tex_mips_into allocated a VkImageBlit per level, every frame (the exposure measure's
chain); it comes from the scratch ring, as does gvk_tex_grow_mips's copy
- gvk_program_new made a new program - modules, pipelines - on every call, and every actor asks
for one; a variant is now made once and shared (a program is immutable), and one that cannot
be made stays 0 for every later asker
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>