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>
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>
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>
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>
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>
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>
- 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>
- model_release(model) frees each primitive's mesh, and each texture once no other model uses it:
the glTF texture cache counts the primitives using each texture (a path loaded again, and a LOD
chain borrowing its LOD0's material, each take a reference); the last one frees the texture and
forgets it along with any remembered material naming it, so a later load is a fresh one
- a scatter layer's instance and sort arrays start at 256 and double as layer_add or a stream
fills them, to the layer's cap, instead of the whole cap up front (0.4 GB in Maroon Lake)
- gvk_tex_read's copy struct comes from the scratch ring
- examples/rendering/release.ludic holds it (RELEASE OK on Vulkan and OpenGL), in the suite
smooth renders pixel-identical before and after (max |d| 0). ludic-dev test 306/306.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- grass_cull.comp decides each candidate blade once a frame (place, ground, density, water,
slope, frustum, colour field) and writes survivors into three bands by distance, one indirect
draw each; grass_inst.vert only bends and places the vertices. OpenGL keeps grass.vert's
per-vertex path; R3D_GRASS_GPU=0 compares
- compute programs take sampled textures after their buffers (gpu_compute_tex / gpu_dispatch_tex),
and every dispatch now records a compute-to-draw memory barrier
- the blades as a sward: 4 m cells, bands with five, three and one-quad blades, spacing doubling
every 18 m to 70 m, never narrower than a pixel; lit facing the sun and leaning to the sky,
shadow looked up above the ground (it read the terrain as its caster), a colour ramp that
leaves only the sheath dark, clumps, dry patches and a tussock shade worked out per blade
- scatter layers flagged grass are skipped while the blades draw (and under R3D_NOGRASS);
R3D_BLADES, R3D_NOBLADES win over the game's setting; R3D_GRASS_S0/D0 for measuring
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- a CAMetalLayer on the view (cocoa.ll win_metal_layer), VK_EXT_metal_surface, QuartzCore linked
with Vk.*; the drawable measured after the layer sets the backing scale
- vk_mac.ll opens MoltenVK directly after any loader: a bundle ships only libMoltenVK.dylib
- a covered window is not presented to (win_visible); one frame in flight on macOS
(R3D_VK_INFLIGHT), with images, buffers and descriptor pools held until it is done
- win_held / win_mouse / win_pad / win_touch / win_text / win_present pass slice elements (arg_buf):
every windowed program died on its first input poll
- variants.list and SPIR-V for the three NEAR_FADE foliage programs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
collide.ludic (circles in a cell grid that could not be removed) and its Render3dState fields are
deleted, and the reload example stops asserting colliders. A game still calling col_* must move to
ludic.physics first (Maroon Lake's src/solid).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic
1804 vars into 126 states, 64 into lets; 23498 edits in 460 files
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every R3D_* read goes through r3d_env_has / r3d_env (env.ludic): a headless
build honours them as before, a windowed build only when R3D_DEV is set to
anything but "0", so a shipped game never reaches a debug view, feature kill,
file writer or hardware fake. Documented in docs/SHIPPING.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>