Commit graph

748 commits

Author SHA1 Message Date
8dfcccad41 Merge branch 'lang/ecs' into lang/final2 2026-09-28 16:13:17 +03:00
4b533b5ff6 ludic.shop, ludic.net: nothing made per tick - the week's lists and dice reused, the net's facts from a ring, the room's watch address made once, pools and kept lists through kept_push; what runs once per host, join, room or guest says so in @alloc_ok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:09:32 +03:00
0e2d2a6fab fix(ludic.wildlife): a newcomer takes the record of one of its species removed for good - its row, its key and so the game's drawing, every other field as new
Each morning's repopulation made a new WildAnimal (and the game a new drawing, actor and skeleton,
keyed by an ever-rising key) for every animal that had gone. A legend's record is never taken, nor
one still in a crowd. reuse_test: a hundred removes and makes are one record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:08:23 +03:00
3311269b23 Merge branch 'lang/memory-frame' into lang/foundations 2026-09-28 16:05:41 +03:00
8ca18725b6 escape (25.3a): which allocations never outlive their frame - the analysis, behind --escape-report; @alloc_ok on generics
emit_escape.ludic: every value is in a class, joined by flow edges (a let, an assignment, an argument
into its parameter, a result into the call) and store edges (a field, an element, a push). HEAP (a
parameter, a state, a global, what an unknown call hands back) flows forward; ESC (stored into
something HEAP, into a global, into an event's fields or named values, handed to an unknown callee)
flows backward, and from an ESC or HEAP target along a store. A load is its base's class. A site that
is neither ESC nor in a function reaching Mem.frame is LOCAL (Node.uns = ES_SCRATCH). ludicc
--escape-report prints each site and the totals; nothing is emitted differently yet - the arena that
allocates the LOCAL sites is next.

@alloc_ok on a generic now covers its instances (kept_push$NetFact is under kept_push's), and a
statement's @alloc_ok is carried on the node (Node.uns), so a generic's clone keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:05:07 +03:00
2bed060d13 Merge branch 'lang/allocs-physics' into lang/foundations 2026-09-28 16:04:11 +03:00
14140e26cd Merge branch 'r3d/skin-own' into lang/foundations 2026-09-28 16:04:00 +03:00
34664e867f frame allocs (25.2): physics, npc, vehicles and wildlife's bounded growth declared - id tables to their most, a walker slot, a guest's copies, a player's own vehicle, a table at a load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:02:42 +03:00
082fd26bf1 frame allocs (25.2): a fact record past the 64 made at the start is @alloc_ok, in its own function, in npc, character, physics, vehicles and wildlife
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:02:42 +03:00
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
f4e121ae94 render3d: actor_own_skin / skin_clone_free, and a frame's scratch and tables made with the device
actor_own_skin(a) gives an actor its own clone of its model's skeleton and actor_release frees it
(skin_clone_free: the pose, matrices, views and scratch the clone made; the rest data stays its
source's). gvk_startup_state, at the end of gvk_init, makes the scratch and tables a draw used to
make on first need (gvk_tmp_buf, the pipeline fast cache, the set key and per-program uniform
offsets pre-sized to 4096 programs, skip/seen/size words, spare and retired lists, the grass cull's
words), and those frame paths no longer start them. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:02:21 +03:00
f5834f8f63 Merge lang/foundations dec64e8 into lang/ecs 2026-09-28 16:01:40 +03:00
eb25f24c96 ludic.base: the growth paths push through kept_push, whose statement-level @alloc_ok says the bound
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:01:28 +03:00
3f32bfda7c ludic.wildlife: the table a guest draws the host's animals into is made once and emptied per join, not new each time
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:00:18 +03:00
07a6c35470 ludic.things: things_reserve sizes the rows, grid, kind index and uid map (tb_reserve, imap_reserve)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:59:41 +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
a3b2da86b5 Merge lang/foundations 1e2a6ba into lang/ecs 2026-09-28 15:58:12 +03:00
2d1d06899e ludic.base: tb_reserve and imap_reserve size a table up front; HandlePool (a slot and a generation, nothing made after hd_pool_new, past its capacity a panic naming it); kept_push is the one declared grow, and every growth path left says its bound in @alloc_ok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:58:11 +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