npc_set_crowd hands a person to a crowd. Its walk no longer steps or asks the way: its want is goal_x / goal_z / goal_speed (0 when it is not walking). npc_moved hands back the crowd's place: position, ground, heading from velocity, gait. Arrival, the act and the give-up after twenty seconds without progress are unchanged. Nothing allocates. npc 16/16.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wildlife_set_crowd(a, true) hands a walker to a crowd. Its step no longer moves it, and its want is two fields on it: goal_x / goal_z (the point it heads for or faces) and goal_speed (0 when it stands). A route reads those and hands back where the crowd put it through wildlife_moved(a, x, z, vx, vz). That sets its place, its ground, its heading from its velocity and its prints, and updates its row's columns so the spatial index finds it there. Nothing allocates per animal per frame: the want is the animal's own fields, and moved writes in place. The prints and the turn counters moved to walked.ludic (steer.ludic was at the 100-line limit). crowd_test holds it; with crowd off every old test is unchanged. wildlife 26/26.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shim builds DetourCrowd from the same pinned Recast & Detour tag. nav_crowd_start puts a crowd on a kind's mesh, with the mesh's tastes as its filters. Walkers are added (snapped to the mesh), sent and sped through verbs, stepped together, and read back into nav_agent_*. nav_crowd_us times the stepping with the OS clock. Dropping a mesh drops its crowd first. On the test meadow, twenty walkers crossing head-on never come closer than their two radii (0.7003 m), all arrive, and a step costs about 12 us. nav 15/15 on the Mac and the PC; the DLL still imports KERNEL32 alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e634177 started a layer at 256 instances and grew it in layer_add and the stream gather, but a
game writes l.inst directly (Maroon Lake's track prints, trees and rocks), past what had been
grown: "index out of range: 2048, len 2048" in play. layer_new takes its cap up front as before;
layer_reserve(l, n) is exported for a caller that writes l.inst itself once layers start small
again, opt-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
17.10 found almost every animal turn-back was for a climb, where the navmesh (40-45 degrees) is stricter than the step's 1.2 per metre. So the frame-sized test was refusing a few centimetres of steep ground: a pebble a walker steps across. It also refused sooner the faster the frame rate. wl_climb takes the ground half a metre ahead along the way the step actually goes. climb_test holds it at 60 and 240 Hz: a pebble is crossed and a wall is not. Both tests fail under the old rule. wildlife 24/24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found with malloc_history over 1200 frames of play without the interface (the first Ludic function
on each allocating stack, live bytes at two marks): sh_loc built `name + "[0]"` per program per pass
per frame - 24.6 MB of 33 in the window; its callers pass both names as literals now. tick_set fills
the frame loop's one Tick instead of tick_new making one a frame. A ridden boat or horse said
VEHICLE_MOVED as a new fact every frame; the metres are added up in VehiclesState and taken once
(vehicle_moved_take / _kind / _x / _z). Play's growth without the interface: 92 -> 14.7 MB a minute
(maroon-lake tests/leakcheck.sh); what is left is phys_push (Physics' fix on lang/nav) and the HUD's
strings. Packages 407, ludic-dev test 305 pass.
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>
gvk_pipeline_fast cached only pipelines it made, so one the driver refused was attempted again on
every draw, each attempt building its key string and create structs anew; the refusal is cached
too (a Vulkan program's variants are fixed when it is made, so it would fail the same way).
stream_evict made a new chunk list per eviction and never freed the evicted chunks' records; it
compacts the list in place and frees each evicted chunk with its data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
terrain_generate, terrain_use_dem and terrain_use_ortho each overwrote the previous height
texture (256 MB of RGBA32F at 4096^2), the heights read back (64 MB), the DEM texture and the
orthophoto's texture and pixels. terrain_reload unloads first, so the world swap was already
clean; any other caller building a map over a live one now lets the old go, and the read-back
array is reused, being the same size for every map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
phys_pose, phys_ray, phys_push and phys_walker_pose each made a new record per call, some several times a frame, and Ludic never gives one back. They now fill the caller's PhysPose / PhysHit / PhysPush / PhysWalk and return whether they found anything. phys_overlap returns a count; phys_overlap_id(i) reads each id back. The package's own uses (phys_resolve, phys_row, phys_walker_move) read the values buffer directly. Tests take small allocating helpers. physics 20, character 15, vehicles 8.
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>
The rest of the leak Physics found: a drain that carried facts gave its list away and made a new
one, every queue every frame something happened. The queue keeps two lists and hands one out while
the other fills; a drained list is good until the next drain of that queue (nothing in the game or
the packages keeps one past it). With ab34f82's empty drain and in-place clear, a queue allocates
nothing in steady state. queue_test holds the reuse; every package (403) and lab/unit (90) pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every per-entity store (@S_ components, @H_ flags, alive, kind, freelist, owners) is a heap block
L_grow doubles from 1024 as L_alloc hands out a slot past it, the new slots zeroed; each site loads
the store's base where it indexes it (ecs_base, its registers %ecsb* so a raw function's t0 labels
cannot collide). Prop.has bounds against @L_cap, Pool.capacity answers it, a mod's registered
stores grow with the rest, every main grows the stores once before anything reads them. A snapshot
records its slot count first and a load grows to it before reading back. The overflow stop of
1c7ce84 is gone with the wall. ludic-dev test 305 passed, selfhost-test 33 passed; 1000 / 5000 /
100000 entities spawn and count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nav_us() gives the microseconds spent in nav_path across the loaded meshes. The shim times each path with the OS's monotonic clock (clock_gettime, or QueryPerformanceCounter from KERNEL32), so the DLL still needs no C++ runtime. Both libraries are rebuilt, and 13 tests pass on the Mac and the PC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The count is wildlife_turned(), the 75-degree fallback an animal takes when the way was not enough. It is a measure only and is not saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A step round what is in the way, a target planned again after twenty seconds without progress, and a target given up are each pushed as NPC_F_STUCK (how, x, z), so the game can see where the way failed a person. The lake test expects one when a walker turns back from the water; the way test expects none when the world gives the way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It sets a target straight away from the threat (40 m when it flees, 20 m when it is wary) and heads for it with wl_head, so the way port takes it round water and cliffs. Where the world gives no way, it still runs straight away as before. way_test now has a flee round the wall's end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L_alloc handed out a fresh slot without a bound, so the 1025th spawn wrote past the end of every
component array. It stops with a located message naming the store's size and where many things
belong (ludic.base's Table). Growable stores are plan 24.8: the save, rollback snapshot and mod
table write the stores whole at a compile-time size, so that is a file-format change. Reseeded;
ludic-dev test 305 passed, selfhost-test 33 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
300 parses of a 446 KB glTF: 2118 MB before, 528 MB with the one-allocation string, 94 MB freed.
A headless Maroon Lake at play: 2334 -> 2195 MB by footprint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>