Commit graph

732 commits

Author SHA1 Message Date
c8c230449e fix(character, physics, vehicles, wildlife): fact records made at the start (64) with a free list of room for all, as ludic.npc's
A frame holding a new peak of facts made a record and grew the free list; now nothing is made until
64 are held at once. The two record-count tests hold the 64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:02:42 +03:00
7f973f9a4f fix(ludic.physics): buoyant bodies in a table made with the state (256), a sink taking the last row - floating and sinking push nothing
ludic deps --allocs (25.2) found phys_float's five pushes per drop reaching the water and
phys_sink's filtered copies per removal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:02:42 +03:00
58f0dbbc84 fix(ludic.npc): a name picked and a name let go build nothing - the body's names counted in place, the last eight who left in a ring made with the state
ludic deps --allocs (25.2) found npc_pick_name making a list of the body's names at every spawn and
np_left making a new list of the recent names at every retire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:02:42 +03:00
dec64e8a59 Merge branch 'r3d/frame-allocs' into lang/foundations 2026-09-28 15:58:26 +03:00
5d0f83b81c render3d: 671 frame-reachable allocation sites to 161 - declared, split, or made nothing
@alloc_ok with its reason on what is made once per resource and kept (textures, programs, samplers,
views, layouts, memory blocks, the pipeline cache in gvk_pipe_build), on resize and swapchain
remakes, on screenshots and dumps, on a world being set up (streams, layers, water, post, bakes), on
loads (gltf_load, skin_load, tex_load*, png_decode, fonts) and on R3D_PROF / drawstats.
gltf_cached's hit path is its own and makes nothing; the miss (gltf_cached_load) is declared.
m4_look_at (now over m4_look_at_xyz) and m4_inverse work on scalars, and cam_update makes no scratch.
Left: lazy first-use starts, error and debug prints, and scratch made and freed each call (churn,
plan 25.3). Compiled (steady, and ludic deps over the game).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:58:18 +03:00
1e2a6bab5b frame allocs (25.2): @frame on a step list's field, @alloc_ok on a statement, and on an exported function
A field declared '@frame run: fn(...)' makes every function stored in it a frame root, as a System's
tick is. @alloc_ok("why") before a statement takes that statement out of frame_allocs and makes what
it allocates declared at run time; a function holding one keeps the fence's scope depth and puts it
back at its return, so a return inside the statement cannot leave the scope open. @alloc_ok above
'export function' was lost - export parses the declaration one call down - and is now carried to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:56:32 +03:00
b35409254e fence (25.1c/25.2): natives by library in the census, and @alloc_ok's allocations declared at run time
The census reads ludic.physics' jph_heap_bytes/_peak and the Vulkan runtime's lvk_ac_bytes/_peak by
name (dlsym, so nothing a package declares is declared twice) and prints jolt, vulkan and the rest
of the heap apart. An @alloc_ok function counts a scope in and out (@lp_fdecl): what it and its
callees make is marked declared in the side table, reported as 'declared' in the census and left
out of a frame's verdict and of Mem.kept().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:54:08 +03:00
f2dd27f443 Merge branch 'r3d/vk-alloc' into lang/foundations 2026-09-28 15:52:51 +03:00
c6ef4f51f2 reseed after the frame-alloc analysis
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:52:04 +03:00
dd55945670 Merge branch 'lang/memory-fence' into lang/foundations
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
2026-09-28 15:51:27 +03:00
03a8ee443d render3d: the pipeline cache grows in gvk_pipe_build alone
gvk_pipeline_fast's miss (the build and the six cache pushes) is its own function, so the fence can
declare it: @alloc_ok("pipeline cache: one per variant the game draws") once lang/memory-fence's
compiler is merged (the annotation is not known on this base). Compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:51:06 +03:00
a52fbc01a9 Merge branch 'lang/jolt-heap' into lang/foundations 2026-09-28 15:50:54 +03:00
76b1bd20ae frame allocs (25.2): what a frame can come to allocate, counted and ratcheted
deps_reach's graph gains the handlers and each @On body (an emit reaches its event's listeners).
Roots: a handler in a frame phase, every reducer, an @On body, and a function stored as a System's
tick. Every allocating construct in what they reach - new, a list literal, push (grow), text built
by + or a template, words/floats/buffer/bytes - is a falloc line with the shortest chain from a
root (root>..>last six), and the program's count is frame_allocs. @alloc_ok("why") on a function or
a handler takes it and what only it reaches out; the reason is required. ludic deps --allocs lists
them, and frame_allocs is a number --check ratchets. Maroon Lake starts at 2661.

Not yet: @frame on a step list's field (only 'tick' is a root field so far), statement-level
@alloc_ok, the three lints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:50:09 +03:00
e9de2b6f27 feat(ludic.physics): Jolt's heap counted in the shim (plan 25) - jph_heap_bytes/peak/allocs, phys_heap_bytes/peak/allocs
Jolt's Allocate/Reallocate/Free/AlignedAllocate/AlignedFree go to libc's malloc with the size in a
16-byte header, and atomic counters keep live bytes, the peak and the blocks asked for (its job
threads allocate too). The fence reads the three exports extern_weak to judge Jolt by its plateau.
jolt_heap_test: a ground in and out 1100 times holds 46,482,514 bytes after the first 100 and after
all of them, the peak does not move, and a closed world gives back every byte it took. The macOS
library is rebuilt; the Windows DLL still has to be rebuilt on the PC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:49:38 +03:00
f8358d117a Merge branch 'lang/npc-pool' into lang/leaks2 2026-09-28 15:49:07 +03:00
9ca357e109 Merge branch 'lang/ecs' into lang/leaks2 2026-09-28 15:47:08 +03:00
467c3f1bdd Merge branch 'r3d/vk-alloc' into lang/leaks2 2026-09-28 15:47:08 +03:00
1ea8f67662 reseed after merging the fence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:46:48 +03:00
665b689410 ludic.inventory: a change's record comes from a ring made once (512), not new per change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:46:31 +03:00
df6ea046c3 render3d: VkAllocationCallbacks counted (R3D_ALLOC_VK), pipeline create infos freed, actor pool at init
Plan 25.1c: vk_mac.ll's @lvk_ac (posix_memalign under a 16-byte header of scope/offset/size, atomic
counters) passed at every render3d create/destroy (49 sites; the caps probe keeps its own null pair);
lvk_ac_bytes/_peak/_allocs/_scope_bytes for the fence, Vk.alloc_bytes. vk_win.ll: null and 0.
MoltenVK 1.4.2 counted 0 live bytes through them in steady. Fence findings: gvk_pipeline freed its 17
create infos (and reuses one bufs list); actor_init fills ac_spare with 512 records (actor_fresh,
m4_new, v3_new at first placement in play). Compiled, not run (the user's call).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:46:19 +03:00
7fdd6582be fix(ludic.npc): the facts' records and the queue's lists made at their size at the start (64), so a frame with a new peak of facts makes nothing
The fence found np_fact_record making a record in five frames of every scenario: the pool grew to
each new peak of facts held at once, and q_unheld's free list and the queue's two lists grew with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:46:11 +03:00
84155274c4 Merge branch 'lang/memory-fence' into lang/leaks2
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
#	tools/ludic-cli/test.ludic
2026-09-28 15:46:11 +03:00
21b828fe5c Merge branch 'lang/ecs' into lang/leaks2 2026-09-28 15:45:28 +03:00
1cf132cb87 Merge branch 'lang/telemetry-ring' into lang/leaks2 2026-09-28 15:42:59 +03:00
045ab0cdf6 ludic.save save_write_tree: a tree straight to disk through Json.write_file, the backup and read-back kept; ludic.telemetry writes its id file the same way
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:42:59 +03:00
edb33400b3 Merge branch 'lang/json-out' into lang/leaks2 2026-09-28 15:40:00 +03:00
691964877b fence (25.1c): the census reads the heap outside Ludic's blocks; blocks counted at malloc's own size
The census's native line is malloc's live bytes over every zone since judging began less what
Ludic's tracked blocks kept - the libraries' and drivers' growth, read before the census file is
opened. A tracked block counts malloc_size(), not the size asked for, so kept is what the heap pays
and the residual carries no rounding. @malloc_zone_statistics is declared once, by the fence or by
Os.heap_bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:38:13 +03:00
a8d54e9878 fence (25.1): every allocation goes through the fence - sites, frame judging, census, callers
Every allocation the compiler emits goes through @lp_malloc/@lp_calloc/@lp_realloc/@lp_free, and a
Ludic-level one first stores its site (function, file, line, kind) in @lp_site. Off, that is one load
and a predictable branch (30 M allocations: 0.87-0.91 s against 0.87-0.90 s on leaks2).

On (the default in a headless build, and windowed under R3D_DEV), tracking starts at the first frame
on its own and judging once R3D_ALLOC_WARM frames in a row kept nothing (600) or R3D_ALLOC_WARM_MAX
after (re)start; Mem.play()/Mem.rewarm() sends a load back to its warm-up. A judged frame that ends
holding more than it began with is reported by site with its callers (the unwinder, taken only once
judging) and fails the run with exit 86 (R3D_ALLOC_FENCE=off|count|warn|fail). R3D_ALLOC_CENSUS
writes the totals and top sites at exit. The build's defaults are --fence=, --fence-warm=,
--fence-census= or a fence line in the program's package.ludic; the environment overrides them.

The runtime is IR (emit_fence_ir.ludic, generated from a template); tracking is a side table in one
calloc'd region, so no block carries a header and pointers crossing to natives stay safe. Examples
alloc_fence, alloc_fence_leak and alloc_fence_auto with cases in ludic-dev test; reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:35:29 +03:00
ae044685e3 Merge branch 'lang/ecs' into lang/leaks2 2026-09-28 15:33:18 +03:00
4499cedcb8 runtime: Json.write_file, and Json.encode through a kept buffer
The encoder appends into RtJsonState's buffer (grown only past the biggest document yet): ints and
Q16.16 fixeds written as digits in place, floats through string() and freed. Json.encode copies the
answer out once; Json.write_file hands the buffer to Fs.write_text (.tmp + rename) and keeps nothing.
json_saves.ludic: exact text, the file equals encode, parse round-trips, 1000 saves grow 0; clean
under MallocScribble. json_quote (the + builder) is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:32:36 +03:00
6cdef20cc2 ludic.ui: an unmounted component is mounted again rather than made again - its record renewed (a generated renew), its props and model kept; an action's call answers into a ring
A prompt that comes and goes as a player walks (co-op's netleak: in_get, cmp_*_new, bd_class, value_slot/put)
made a new record, props and model on every mount, and an action's call answered into a new Val (ev_call_with).
examples/library/ui_remount: two thousand comings and goings hold the heap at 0, and the counter starts at 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:25:55 +03:00
f74738a4e0 ludic.telemetry: telemetry_str_open - a named string property the caller writes into the kept buffer (a look's numbers)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:22:26 +03:00
78a70765af feat(ludic.telemetry): objects and lists inside an event's properties, an item made of numbers, and a signature of what was written - all in the kept buffer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:17:20 +03:00
d1bea7f10e fix(ludic.telemetry): nothing made per event - properties and the line written into kept buffers, the queue a ring of bytes made at the start
An event was a tree of values encoded into a new string, a dropped line was never given back, and
every batch and save joined the queue into another. Now: telemetry_props / telemetry_str / _int /
_bool write the properties as JSON into a buffer the state keeps; the line is written beside it,
its time worked out from the clock's seconds (TelemetryWorld.clock_s, was stamp); its bytes go
into one ring (ring_bytes, at most queue_max lines), the oldest overwritten past either; a batch
and the file are written into a third kept buffer and go as bytes (TelemetryTransport.send takes
the bytes and their length; Http.body_bytes, Fs.write_bytes). The facts are pooled.
tests/ring_test: ten thousand events past the cap in lines and in bytes, 0 bytes of heap; the line
exact JSON, escaped, 2000-02-29 right; the batch puts the id in; the file round-trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:15:12 +03:00
ae5df73e72 ludic.jobs jobs_posted_count, ludic.steps steps_my_shares_into - a count and a kept list for what a party asks every tick
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:12:24 +03:00
fc0f3d3790 Merge branch 'r3d/prime-fix2' into lang/leaks2 2026-09-28 14:24:42 +03:00
c736fd3b09 render3d: buffer primes in a command buffer of their own, submitted at once
A co-op guest loading its models segfaulted in copyBufferToBuffer under vkQueueSubmit (0x68, AGX
LegacyBlitContext): a buffer primed into the frame's command buffer was released later in that
frame, and gvk_buf_release destroys a buffer no draw has marked, so the copy read a freed one. The
copies now run in their own command buffer before the frame's begins, checked against the slot's
handle as they are recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:20:00 +03:00
e8dc823009 Merge branch 'lang/play-leaks-own' into lang/leaks2 2026-09-28 14:09:13 +03:00
62a703974d test(ludic.nav): an index loaded again twenty times, its tiles in each time, holds the heap (0 bytes), the open files and the tiles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:09:00 +03:00
33dc9b6a18 Merge branch 'lang/play-leaks-own' into lang/leaks2 2026-09-28 14:02:58 +03:00
364888bcc9 fix(ludic.physics): a hull owns its box, so a boat called and sent away takes its shape with it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:59:25 +03:00
5cd53d4024 feat(ludic.physics): phys_own - a body owns a shape made for it alone, and phys_remove frees it with the body
A chunk's trunks and boulders put in and taken out made a new shape each time and freed none. The
table is by the body's index, made once at the world's size. tests/reuse_test: 1000 owned bodies put
and removed hold no more shapes and 32 bytes of heap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:59:25 +03:00
254097657e runtime: Log, DateTime.format, Input.text, Path, Mime, Fs, Os and Text keep nothing per call
Found by reading every builtin (Os.platform's 8 KB per call started it). Log builds its line only at
or above the threshold and frees it; DateTime.format folds through + so its pieces go; Input.text
encodes into one buffer; Path/Mime/Fs/Os free their temporaries on every path; string results of
Text/Path/Mime/DateTime/Os dirs are fresh and Text frees a fresh argument. Reseeded.
runtime_temps.ludic: 19.8 MB -> 0 over 20,000 rounds, 64 KB -> 0 over 200 of file work; clean under
MallocScribble. string_temps still 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:55:49 +03:00
404ee2ee3b Merge branch 'lang/ecs' into lang/leaks2 2026-09-28 13:46:18 +03:00
f8fe157352 Merge branch 'r3d/no-cmd-pool' into lang/leaks2 2026-09-28 13:41:23 +03:00
61a6f3c827 ludic.base: TextRing - a fixed ring of text buffers written in place, for a line made per event
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:35:34 +03:00
0141f99dd1 Os.platform/arch uname once; render3d primes a new buffer's Metal buffer
lp_os_platform and lp_os_arch malloc'd 8 KB per call (the uname buffer) and kept none of it:
string_temps now asks both every round, 327 MB over 20,000 before, 0 after. Reseeded. render3d:
MoltenVK made a mapped buffer's MTLBuffer at its first bind (fn_gvk_draw +4 blocks in the boat
window); gvk_buf_reserve queues it and the next frame's command buffer copies 4 bytes out of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:34:45 +03:00
67474a5cfe Merge branch 'lang/play-leaks' into lang/leaks2 2026-09-28 13:33:48 +03:00
7e9fa4473c render3d: a stream's cache is made with the stream - records and an arena - and evicts in place
stream_update made a Chunk and a words copy per new chunk (fn_stream_update +39 blocks / 13.2 KB a
window with the hiker in the drifting boat). The pool holds STREAM_MAX_CHUNKS records; the arena
is twice the layer's cap; eviction compacts it; a chunk that cannot be kept is gathered from the
scratch. stream_clear_all frees records, lists and streams. steady: 300 new cells, cap 256: 156 KB
before, 0 / -4.9 KB after, and every kept chunk's data checked against its cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:26:59 +03:00
ca69a3f9ae ludic.net: a message's record, bytes and lists kept - queued ones back to a pool once sent or taken, the inbox's two drains on, a read text interned
np_queue made a NetMsg and a buffer per message, every flush a new keep list, and every message
that came in a NetMessage and a buffer; nr_text a new string per text read. A party plays at
dozens a second, so all of it is kept now: a pool of MTU-sized records for the queue (a peer's two
lists swapped by the flush), the inbox's records in two halves swapped when a message finds the
inbox empty, net_written's one record, a relay hello's and a STUN request's bytes in one scratch
buffer, and the texts read out interned. Tests: 6000 messages with the heap flat (Os.heap_bytes),
and an inbox record that holds still across a drain and comes back two drains on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:25:46 +03:00