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>
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>
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>
Every pool thread runs the worker at once with the same states, so a mut state in a worker was a
race nothing reported. check_worker_ref refuses a worker whose leading states include a mut one;
threads.ludic's total moves into the words the worker is handed, under the mutex. The seeds are
regenerated (ludic-dev reseed). ludic-dev test 305 passed, selfhost-test 33 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
- 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>
Checked on both machines with a bundled Jolt probe: the licence and the library in place, the app
signed (Mac) and the probe's ball landing at the same height on both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
- ludic.wildlife: WildlifeWorld.push / pushed_x / pushed_z (defaults change nothing); a ground
walker's step is slid clear of a still thing with a body from its species' size and scale, and
a step that gets nowhere turns away as the water does. Birds are not pushed; no dice added.
- ludic.npc: NpcWorld.push / pushed_x / pushed_z (a person 0.35 m round) in npc_walk, and
NpcWorld.clear (default true), which npc_line_ok asks at 1 m so a detour does not cut a trunk.
- ludic.aim: AimView.clear replaces AimView.ground and the ground march; the pick asks the line
a body's width short of where the ray enters the thing (aim_clear_at(t, back)), so a solid
thing does not hide itself. probe.ludic (the ground-agreement probe) and AimView.dry are gone.
- ludic.photo: PhotoLens.clear replaces PhotoLens.ground, PhotoLens.trunk and the march.
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>
The package keeps the rules and hands them over each step: speed and heading, whether a jump
leaves, CHAR_STEP and the boots' slope limit. The walker (Jolt CharacterVirtual) does gravity,
landing, steps, slopes and following the ground down. Too steep is ground too steep facing the way
the body wanted to go; stepping off an edge is not. ludic.physics: phys_walker_move (follow the body
if something else moved it, then step), the slope limit, the jump always honoured. The character's
tests run on a real Jolt walker over the same fake world: 15 pass, a knee-high boulder now a step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five new tests: stand and walk at the asked speed, a low stone is a step and a tall one a wall, a
jump up and back, a crate shoved aside, a hull turned by a torque and carried by a force.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>