Commit graph

234 commits

Author SHA1 Message Date
cfb7fa5ca9 Merge branch 'lang/foundations' into lang/ecs 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
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
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
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
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
649a65d854 feat(ecs): things_now / things_changed - a consumer keeps the tick it read up to and asks only for the Things written since (untouched 64-row blocks skipped)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:13:45 +03:00
61a6ab2e7b Merge branch 'lang/foundations' into lang/ecs 2026-09-27 22:08:40 +03:00
50aa535f88 docs(base): the table's observers, groups and chunks, sparse index values, block change stamps and the parallel-worker rule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:08:19 +03:00
f3fec88888 feat(ecs): chunks as a grouping - IntIndex takes sparse values; ludic.things' chunk column and things_drop_chunk
A streamed world loads and lets go of a chunk's entities together, so a chunk is a first-class
group: chunk_of(x, z, size) packs a chunk's cell into an int, tb_remove_all(tb, ix, v) removes every
row an index files under one value (the observers told of each). IntIndex kept one list per value
up to the largest, which a packed chunk number (hundreds of millions) turned into hundreds of
millions of empty lists and a 12 GB test run; a value past 1024 now gets its slot through an IntMap,
so a sparse value costs one list.

ludic.things files every Thing under its 256 m chunk (TH_CHUNK), kept by every move;
things_chunk_count and things_drop_chunk (the port and the facts told of each removal, as
thing_remove tells them) are what the memory budget's streamed chunks ask. The moves are their own
file (moves.ludic). Tests: ludic.base 35, ludic.things 8, all under a 2 GB cap (24 MB peak).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:07:26 +03:00
5e46afdb0f feat(nav): 17.10's measure - paths asked for, found and cut short since the meshes loaded (nav_asked / nav_found / nav_short)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:05:07 +03:00
de9c92b360 feat(nav): nav_next_corner - a way's next corner read back as the nearest point is, so a walker's question and a goal share one answer and a game needs no state of its own for them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:01:42 +03:00
6ccbed923e build(nav): the Windows library with the staged file read, rebuilt on the PC - 11 tests there
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:57:53 +03:00
ee0768bf3e feat(npc, nav): 17.5 - a person walks by the world's way where it has one (NpcWorld.way: the next corner toward the target, asked every half second, on reaching it or for a new target; the errand begins only at the target itself; the step round and the give-up stay the fallback), way_test round a wall; and ludic.nav reads a mesh file into the shim's own memory and frees it once parsed, so loading a map leaves nothing in Ludic's never-freed heap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:57:34 +03:00
7ceefa8c50 feat(ecs): wildlife_near / wildlife_near_of into the caller's list, wildlife_alive_of
The living animals around a point from the grid (a rare species from its own list), into a list
the caller keeps in its own state, so a frame's proximity question walks the cells it covers
rather than every animal ever made. wildlife_alive_of is the species index's count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:54:39 +03:00
4add35b4ca feat(wildlife): 17.5 - an animal walks by the world's way where it has one: WildlifeWorld.way (the next corner toward where it is going, asked every half second or on reaching it) steers wandering and going to water or forage, and goal (a spot it can reach) is a wander's target, seeded by the same two draws so the dice do not move; the defaults are the straight line and the old target. way_test: round a wall by its corners, and a wander's target where the world says
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:51:35 +03:00
045bf22e10 feat(ecs): ludic.vehicles on a Table<Vehicle>
The vehicles are rows of a ludic.base Table with kind and owner as columns, every player's own by
an owner index and a nid map: vehicle_of walks only that player's rows, vehicle_by_nid is a map
lookup, and a player who leaves has their rows removed instead of the whole list rebuilt. ve_add
takes the owner, so the owner is filed once and never assigned around the table. Positions stay
on the record: a boat moves every frame and there are at most two a player. vehicles_test holds
the table (9 tests); the game builds against it and lab/unit passes (88 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:50:23 +03:00
52270ab5d1 feat(ecs): observers and block change stamps on a Table
tb_on_add / tb_on_remove take a fn(int) (its states supplied, like a system's) told each handle as
its row is made and before it goes (ecs_hooks.ludic), so what follows a table - a drawing, a
message - does so without a scan. Every row's tick now also stamps its 64-row block, and
tb_changed_since / tb_added_since skip the blocks nobody wrote since: a delta over a large table
costs the blocks that moved. A row moved into a removal's gap keeps its own tick (it was not
written); a removal is told by the hook. ecs_test holds both. The ludic.wildlife table
(5ac3d48, written while builds were held) now builds and passes its 19 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:47:06 +03:00
537d3a0ab3 build(nav): the Windows library with the filters, rebuilt on the PC - 11 tests there
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:40:31 +03:00
4a285903e1 Merge branch 'lang/foundations' into lang/ecs 2026-09-27 21:37:20 +03:00
d4b6979426 feat(nav): 17.4 - a mesh has eight filters (a cost per kind of ground each) and every path, straight line and random point names the one it walks by, so a deer, a marmot and a person each take their own way over the same mesh; tested with a band of thicket walked round by the filter that dislikes it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:34:23 +03:00
f0485a20a8 build(nav): the Windows library rebuilt on the PC with the detail floor - 10 tests there, KERNEL32 alone
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:29:13 +03:00
6e90e178ed Merge branch 'lang/foundations' into lang/nav 2026-09-27 21:24:25 +03:00
950d3134f2 fix(nav): nothing allocated per build - the configuration and the empty lists live in NavState, because Ludic never gives an allocation back and a map is thousands of tile builds; nav_drop lets one kind's mesh go; the detail mesh's spacing is a NavConfig setting held to Recast's own floor of 0.9 m (0 took a 40 m test meadow to 3.5 GB)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:23:56 +03:00
5ac3d488a4 wip(ecs): ludic.wildlife on a Table<WildAnimal> - UNBUILT
Written while builds are held (agents share 16 GB; no build or run until the user lifts it), so
it has not been compiled or tested. The animals are rows of a ludic.base Table: x, z, species and
alive as columns, a 32 m grid over the living, the living by species, and a nid map.
wildlife_by_nid is a map lookup instead of a scan of every animal ever made, wildlife_count walks
only its species, wildlife_nearest asks the grid, wildlife_set_alive writes the flag and the row
together, and wildlife_refile files the tick's moves once after it (wildlife_verify holds the
columns to the records). A guest puts its whole table away and brings it back. A test case covers
them; to run once builds are allowed: ludic test packages/ludic.wildlife.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:21:29 +03:00
ab34f8229b fix(base): an empty queue drains and clears without allocating
Ludic frees nothing a safe program allocates, and every package's fact queue is drained once a
frame: q_drain handed back its list and made a new one each time, most often of an empty queue.
An empty drain now returns the queue's one shared empty list (never to be pushed to), and q_clear
clears in place - the live list is never one a drain handed out. A queue that held facts still
gives its list away, so what leaks is in proportion to what happened, not to the frame rate.
queue_test holds it (an empty drain is the same list twice; a drained list is untouched by later
pushes and clears).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:16:19 +03:00
f802bdd3ec feat(ecs): ludic.base Table<T> - dense rows of hot columns, generational handles, a spatial grid, kind indexes and an id map kept current by the setters; ludic.things on it
A mechanic that keeps many of something keeps them as rows of a Table<T> rather than a list it
scans. Removal swaps the last row in; a handle (22-bit slot, 9-bit generation) goes stale when its
entity is removed. tb_set_f / tb_set_xz / tb_set_i stamp a change tick and refile the row in every
index over that column in O(1): tb_grid (a doubly linked spatial hash, rings outward for nearest,
rehashing as it grows), tb_index (a cached query: the rows of each value of a kind column, gated by
an active column), tb_nearest_of / tb_within_of (a rare kind from its own list), tb_nearest_where
(a predicate on the record), tb_within_recs (into the caller's list), tb_changed_since /
tb_added_since, IntMap. No question allocates or writes: Ludic frees nothing, and the old lists'
per-call copies leaked every frame.

ludic.things keeps its Things as a Table<Thing> with x, z, kind and active as columns; every verb
writes the record and the row together (a Thing carries its handle and table, so thing_hide(t)
still needs no state), thing_set_on / thing_set_xz / thing_place_at are the silent forms the game
used to do by assignment, and things_verify holds the columns against the records.
things_near(_of) fill a caller's list, thing_of_kind walks a kind, things_count_of counts one, and
things_tick visits only the kinds that tick. thing_find answers the first-placed by uid.

Against a []Record scanned (M4 Pro): nearest 0.37 / 1.5 / 7.2 us at 10k / 100k / 1M (list 30 /
307 / 3075), by id 0.09 us at 100k (list 17), a move refiled in 11-44 ns. ecs_fuzz_test holds the
grid, the kind index and the record queries against a scan through 9000 random changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:52:51 +03:00
cc384efee0 feat(render3d): the meadow's blades are culled on the GPU - under 1.5 ms of a MoltenVK frame for all the grass
- 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>
2026-09-27 15:47:36 +03:00
f815f3e3cf wip(render3d): one frame recording while one draws on Vulkan (double-buffered ring and pools), R3D_VK_LABELS, grass measuring switches (R3D_NOGRASS, R3D_BLADES, R3D_GRASS_S0/D0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:47:36 +03:00
4e2087d6eb merge lang/nav: ludic.nav over Recast & Detour (17.1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:47:14 +03:00
e6893f58be feat(nav): 17.3 - a map in square tiles (nav_tiled, nav_tile_build; a tile must be a whole number of cells or its seams never join, and the shim refuses one that is not; 64-bit polygon refs for 16384 tiles), ground as a NavGround with boulders as convex footprints, one save format for one tile or many, and nav_random_near - a reachable point from the caller's seed, bit-identical on the Mac and the PC. 10 tests on both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:42:37 +03:00
9b8606483d feat(nav): 17.1/17.3 - ludic.nav over Recast & Detour v1.6.0 (zlib), built here from the pinned tag on the Mac and the PC (the DLL imports KERNEL32 alone): a navmesh per kind of walker from triangles with an area byte each and cylinders nothing stands in, saved and loaded as bytes; the nearest point, a path as corners (partial when the end cannot be reached), whether a straight line stays walkable, a cost per kind of ground. Tests on a hand-built meadow: round a post, over a ford, stopped at a bank, a saved mesh answering alike - Mac and PC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:29:11 +03:00
2c578c6f1c feat(jobs): phase 3 - a job can be given up: jobs_give_up puts a written job back on offer from the next day (the day back is saved with it, so giving up is never a reroll), jobs_post_give_up takes a post down for the morning to replace; an auto board's posts are not taken and not given up; JOBS_F_GIVEN_UP tells the game to take down what it placed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:28:06 +03:00
e397e4eee9 merge lang/horse-walker: the horse on legs (VehicleLegs), convex and scaled shapes in ludic.physics
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:24:07 +03:00
dd86dce171 feat(physics): 16 - a rock's own shape: phys_convex (Jolt's convex hull of a point cloud) and phys_scaled (one hull, every rock of that shape at its own size); the libraries rebuilt on the Mac and the PC (still KERNEL32 alone); hull_shape_test holds the scale and that a yaw turns a body the way a renderer turns an instance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:22:30 +03:00
e3813b1ea4 refactor(steps): 0.R5 - a seat's share is a question (steps__share): seat 0 read live, a teammate's column as sent for this chapter; only steps_share, the verb, clears a stale table - steps_met, steps_each, steps_lacking, steps_my_shares and steps_share_of read
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:06:33 +03:00
bb2a4c10af feat(vehicles): 16 - a horse walks on legs in the game's physics (VehicleLegs): the package keeps its pace, stamina, hay and its refusal of water; the legs meet the ground, its steps and slopes and whatever is in the way, and what they cover along its heading is its speed, so a fence stops it and a trunk it glances pushes it aside. VehicleWorld.blocked is gone; the hire, hay and follow verbs move to keep.ludic
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:05:03 +03:00
0e3cef813f feat(pkg): windows-x64 builds of ludic.physics (Jolt v5.6.0) and ludic.nativeecho, built on the PC by their native/build.sh
tools/native/lib.sh passes -implib rather than /implib: Git Bash rewrites an argument starting
with / into a Windows path, and lld-link was handed 'C:\Program Files\Git\implib;...'. Both DLLs
import KERNEL32 alone, so no C++ runtime ships beside them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:29:55 +03:00
87259f989d refactor(packages): 0.R5 - the shop's week is rolled by its tick and asking reads it; needs start filled and vehicles start with their list; character, physics, vehicles and nativeecho's lazy starts are defaults; --tighten over all four
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:25:57 +03:00
b247603b7e merge lang/foundations into lang/native-jolt (seeds regenerated)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:20:44 +03:00
f516148e46 refactor(packages): 0.R5 - the rest of the lazy starts are defaults - hints' rail (sized by hints_config), lab's CRC table, rpg's crafting / items / inventory / quests / dialog, shooter's weapons, prefs, and npc's and gameplay's empty ensures gone; gameplay's faction table sits beside its state, since a default names only what is declared before it; --tighten over their tests and examples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:07:29 +03:00
5c50755652 feat(character): 16.9 - char_knock, a knock-back the walker carries (never into a trunk); a landing keeps only the air's speed along the body's facing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:07:33 +03:00
3fae64f975 fix(physics): Jolt's own pair and contact limits (65536 / 20480) rather than the body count's, and a temporary allocator that falls back to the heap - a valley of 60,000 still trunks aborted its first step with a moving body in it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:02:56 +03:00
510360b002 feat(vehicles): 16.7 - a boat is a buoyant hull in the game's physics (VehicleHull), rowed along its bow and turned by the oars; the scripted bob, the wind's drift and the shore refusal are gone
The water floats the hull, the wind pushes every boat as a force, the lake bed stops it at the shore;
the swell takes the outward share of a stroke. ludic.physics: phys_hull_add (a buoyant box),
phys_row, phys_bow_speed, and a test that floats one a quarter under, rows, turns and grounds it.
The vehicles tests row a real Jolt hull on the fake shore; the horse moved to horse.ludic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:33:19 +03:00
9e4f4edb92 refactor(packages): 0.R5 - jobs, session, settings, hints and net make their tables when the state is made, not on the first question (jobs__ensure, session__init and ply_init, sg_init, hn_mute_ensure, net__init gone); their questions read
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:29:32 +03:00
ac815bf638 merge lang/physics-senses: animals and people slide round still things, the aim's, photo's and animals' sight lines are one Jolt ray
Conflicts with 0.R5's state defaults in ludic.wildlife resolved; wl_step writes the push's answer, so
its callers take WildlifeState mut again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:23:17 +03:00