Commit graph

604 commits

Author SHA1 Message Date
95f2087148 ludic.wildlife (18.3): wildlife_listed - still one of this state's animals, so a crowd lets go of one a load or a clear removed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:36:31 +03:00
9ec1b3a9bf ludic.wildlife, ludic.npc (18.3): a crowd walker carries its walker's id (set_crowd(a, id), -1 off)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:32:25 +03:00
24c16ebee4 Merge branch 'lang/foundations' into lang/nav 2026-09-28 00:29:53 +03:00
074f10f302 ludic.npc (18.2): a person a crowd moves
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>
2026-09-28 00:27:59 +03:00
81b6782c9d ludic.wildlife (18.2): a walker a crowd moves
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>
2026-09-28 00:27:12 +03:00
521efe67db ludic.nav (18.1): a DetourCrowd per kind of walker
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>
2026-09-28 00:24:30 +03:00
c2a5df4be8 Merge branch 'lang/foundations' into lang/uifree 2026-09-28 00:20:51 +03:00
429eeb7754 ludic.ui, compiler: a component's model is filled in place into an object its instance keeps (value_set_*); colours, border-images and a class screen's stub made once; object-fit into a kept list - benchmark 20 -> 10 KB a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:17:47 +03:00
08ac2f1125 ludic.ui: var() values found by their pieces, tpl_words/url/border-image by their text, nine-slices, picking, lifecycle, focus, hover, the event queue and input without a list or record made a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:10:56 +03:00
c2902a2246 Merge branch 'lang/foundations' into lang/nav 2026-09-28 00:08:08 +03:00
edaec666d1 Merge branch 'r3d/layer-hotfix' into lang/foundations 2026-09-27 23:57:19 +03:00
d8fa2ce6b0 fix(render3d): a scatter layer allocates its whole capacity up front again
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>
2026-09-27 23:56:07 +03:00
bcdcc8fe8c Merge commit '5d9cbdd' into lang/ecs 2026-09-27 23:55:07 +03:00
bd998748ee fix(leaks): the compass keeps its marks - two lists swapped a frame, a pool of marks reused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:55:07 +03:00
a071196f89 Merge branch 'lang/foundations' into lang/nav 2026-09-27 23:55:04 +03:00
b1fff7c921 ludic.wildlife: a climb is judged over half a metre ahead, not one frame's step
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>
2026-09-27 23:52:49 +03:00
231715bf98 fix(leaks): what play allocated every frame and never freed - a shadow uniform's name, the Tick, a ridden vehicle's moves
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>
2026-09-27 23:51:37 +03:00
5d9cbddb72 Merge branch 'lang/foundations' into lang/uifree 2026-09-27 23:46:28 +03:00
51a5e06f5c ludic.ui: props kept an instance, a call's arguments a depth, a loop's index shared, an absent attribute's text without a Value; runtime Value.clear - benchmark 36 -> 20 KB a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:46:18 +03:00
74006849b5 Merge branch 'lang/foundations' into lang/nav 2026-09-27 23:43:42 +03:00
46888b6716 runtime, ludic.ui: a scalar Value is one allocation, not three (lists only for a list or an object); layout and cascade lists kept a depth; small numbers' text made once - benchmark 63 -> 36 KB a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:43:25 +03:00
1d40f6d95a ludic.ui: splits, words, gradients and keys are made once per text (a bounded memo), sides without a list - benchmark 110 -> 63 KB a frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:38:57 +03:00
d9b20f6eec Merge branch 'lang/foundations' into lang/nav 2026-09-27 23:37:52 +03:00
e634177ca4 feat(render3d): 23.4 - model_release, shared textures counted, and scatter layers sized to what they hold
- 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>
2026-09-27 23:34:14 +03:00
70a92a8862 fix(render3d): a refused pipeline is not retried every draw, and an evicted chunk is freed whole
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>
2026-09-27 23:34:13 +03:00
6dd1810038 fix(render3d): a map generated over another releases what it replaces
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>
2026-09-27 23:34:13 +03:00
978f128755 Merge branch 'lang/foundations' into lang/uifree 2026-09-27 23:33:48 +03:00
d1c9359e96 ludic.ui: a screen's nodes and envs are pooled and used again - two generations a screen, reset in place, their own lists emptied not replaced; a tree is good until its screen is built twice more (benchmark 256 -> 110 KB a frame)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:33:48 +03:00
83200a453b ludic.ui: string matching compares in place - at_alias, lk_in, colours and every s[a..b] == p made a string per position tried, per rule, per element, per frame (a benchmark page: ~3 MB -> 256 KB a frame)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:27:02 +03:00
c33522d1c7 ludic.wildlife (17.10): the turned-back steps counted by reason (water ahead, a climb, the rest a push)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:25:46 +03:00
f048ffb6ac ludic.physics: queries fill a record the caller keeps, and allocate nothing
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>
2026-09-27 23:18:26 +03:00
8fbb1f7d03 build: reseeded after merging lang/foundations (vk/leaks) into lang/ecs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:08:51 +03:00
cfb7fa5ca9 Merge branch 'lang/foundations' into lang/ecs 2026-09-27 23:08:24 +03:00
e2626a0687 perf(compiler): the generated drain_actions allocates nothing - an empty queue returns at once, and a drained one is cleared in place instead of replaced by two new lists (seven drains a frame, never freed)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:08:24 +03:00
c97e64c3c0 perf(render3d): an actor knows its row - removal swaps the last one in (O(1)) instead of building a new list of every other actor, never freed; actor_keep puts one back after a world swap; actor_clear_all clears in place
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:08:02 +03:00
8808dda605 Merge branch 'vk/leaks' into lang/foundations 2026-09-27 23:04:23 +03:00
80764f85c1 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>
2026-09-27 23:03:38 +03:00
b6c4d703a2 Merge branch 'lang/foundations' into lang/nav 2026-09-27 23:02:39 +03:00
b9d0cca281 Merge branch 'lang/foundations' into lang/ecs 2026-09-27 22:56:57 +03:00
c74099f5f8 fix(base): a queue drain allocates nothing - two lists, reused
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>
2026-09-27 22:55:43 +03:00
8384ad3215 feat(compiler): the built-in ECS's stores grow - 100 000 entities, where 1024 was the wall
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>
2026-09-27 22:53:24 +03:00
e227b75472 ludic.save, ludic.telemetry: a parsed tree they refuse is freed (23.2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:51:06 +03:00
f65eda8b47 ludic.nav (17.10): the time paths take, counted in the shim
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>
2026-09-27 22:41:30 +03:00
297578705e ludic.wildlife (17.10): count the steps refused and turned from
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>
2026-09-27 22:38:37 +03:00
ab84a5f936 ludic.npc (17.7): a person's fallback is a fact
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>
2026-09-27 22:33:25 +03:00
61f802e9ad ludic.wildlife (17.6): a fleeing or wary animal goes by the world's way
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>
2026-09-27 22:31:07 +03:00
f1153f59a2 Merge branch 'lang/foundations' into lang/nav 2026-09-27 22:26:11 +03:00
0c6235e55b feat(ecs): thing_nearest_within - the nearest of a kind strictly within a radius, from the grid or the kind's list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:25:34 +03:00
1c7ce84881 fix(compiler): the built-in ECS stops at its 1024th entity instead of writing past every store
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>
2026-09-27 22:22:45 +03:00
9539f7bcec perf(json): 23.2 - a parsed tree can be let go (Value.free / Json.free: its nodes and lists, not its strings), a string is parsed in one allocation instead of one per character, the parser's cursor is freed, and gltf_load frees the last file's tree and the text it parsed
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>
2026-09-27 22:17:27 +03:00