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>
thing_put and thing_spawn made a new record for every Thing placed, and the world places and removes
them all day (an animal's sign and bed, a drop, a fish), so play grew by a record per placement.
Removed records wait in a queue compacted in place; once 32 wait, the oldest is set back as new
with a new uid. ludic.base's grid_reserve makes the buckets for n rows now, since a rehash in play
leaves the old bucket array behind. Tests: a record comes back only past the lag, as new, with a
new uid, the indexes agree, and the spare queue stays bounded.
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>
nav_load_index indexes a saved mesh's tiles (cell, offset, size, ref) from their headers and keeps the file open, with no tile in. nav_tiles_keep reads in the tiles within rin of any player and removes those past rout of all; the file is read through Ludic's pack-aware file_open. The file format is unchanged. tiles_test: tiles come and go with the point, a path works across the seams once they are in, and a reset closes all. Measured on the baked maps (the 900 / 1100 m ring round the start): Maroon 617 of 1807 tiles in, 8.9 MB instead of about 27 MB for a person and 5.4 MB of about 14 for a large walker; Lamar 391 of 1147. Both libraries are rebuilt; nav 17/17 on the Mac and the PC, DLL KERNEL32 only.
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 per-frame sweep over physics, character, vehicles and nav found three fact makers that made a record per fact: contacts and splashes, the character's and the vehicles'. Each keeps a pool now, as wildlife's and npc's do (q_unheld). phys_sink, asked on every removal, built two new lists each time; it filters into a kept spare pair and swaps. ludic.nav's paths and crowds already allocate nothing, and none of the four builds a string per call. What remains at load, reset or a world swap is not per frame. vehicle_feed takes its state mut. vehicles_test: 1000 fed facts, drained turn by turn, use at most 4 records. All packages 421.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dispatch lowered to ludic_act_push(k, new A { ... }): a fresh record every dispatch, and an input
system dispatches Move and FrameTime every frame. The queue now keeps a list per action
(kept<k>, used<k>); ludic_act_new__A hands out the next (made only when all are queued), new's
emitter fills it field by field as it fills a fresh one (every field, default or given), and
drain_actions sets every used<k> back to 0 once the queue is empty. A reducer only ever reads
its action during the drain, so nothing sees a record after it is reused. Actions are visible
to the program's file, where the queue lives, as reducers already were.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wl_fact made a new WildFact per fact, about one a frame, and Ludic never gives one back. ludic.base's q_unheld(q, pool, out) lists the records a queue no longer holds: neither waiting nor handed out by its last drain, which is good until the next drain. ludic.wildlife and ludic.npc now keep a pool, hand out a free record, and make a new one only when every record is held. Taking a record writes the state, so wl_fact / np_fact and the few callers holding their state read-only take it mut. Tests: q_unheld's rules (an empty drain holds on, the safe side); 3000 prints drained frame by frame use at most 4 records; 500 arrivals drained turn by turn use at most 4. base 37, wildlife 27, npc 17; all packages 420.
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>
nav_crowd_add_fixed adds a walker with no steering, and nav_crowd_place puts it (snapped to the mesh) where the player is, with the velocity they have, each frame. The others plan round it by that velocity, and whatever it was pushed by is undone at the next placement. crowd_test: six walkers aimed through a standing player come no closer than 1.71 m and all get past, and the player stays where it was put. nav 16/16 on the Mac and the PC; DLL KERNEL32 only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nav_crowd_add takes how hard a walker keeps its distance, and WildSpecies.spacing (default 2) is a species' room from others walking by, which the valley hands the crowd. Both libraries are rebuilt; nav 15/15 on the Mac and the PC, DLL KERNEL32 only. wildlife 26/26.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>