Commit graph

806 commits

Author SHA1 Message Date
1315baf332 Merge branch 'lang/memory-gaps' into lang/foundations 2026-09-28 17:13:53 +03:00
c8a588b2de escape (fix): the arena took ludic.ui's pooled nodes - a generic's call and an unknown callee now keep what they are handed
memory_final's gate crashed in all 13 scenarios at the first frame of play, R3D_ARENA_CHECK=1 reading
0xDD in ludic.ui's nd_take: a node the pool keeps had come from the frame's scratch. Two holes:

- a call to a generic (ui_kept(list, n)) names the generic, and the analysis knows only its instances
  (ui_kept$UiNode), so the callee looked unknown - and an unknown callee was taken to keep nothing.
  A generic's call now reaches every instance, and an unknown callee keeps everything it is handed,
  but for a short list of intrinsics known to keep nothing; view() shares its list's storage.
- a push's growth into a parameter's list was LOCAL whenever the list was not seen kept, though a
  parameter may be a state's list. A site is LOCAL now only when its class is neither ESC nor HEAP.

examples/lang/arena_pool.ludic is the shape (a pool keeping records across frames through a generic
push): built with --arena it prints '7 3498' poisoned on every reset and with the arena off, in
ludic-dev test. On the valley every ludic.ui pool site is kept; 5480 sites local, 6431 kept.

Also, from ECS: a record's field defaults are stored into it when it is made, a global's initializer
is kept, and a component's own functions are frame roots (they run while its page is open).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:13:05 +03:00
e91091d62b Merge branch 'lang/memory-census' into lang/foundations 2026-09-28 17:10:20 +03:00
5a5e5cb258 Merge branch 'r3d/owns' into lang/foundations 2026-09-28 17:10:20 +03:00
0a078c7822 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 17:10:20 +03:00
1bf3470873 render3d: a model owns its skin - @creates(Skin) skin_load, @releases(Skin) skin_free, @owns on Model.skin
resource_drops 0 and owned_leaks 0 over main. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:09:00 +03:00
bc50bd9b9a render3d: @owns on the handles records hold
Target's framebuffer and its colour and depth textures, a Mesh's index buffer (gvk_buf_new /
gvk_buf_delete now @creates / @releases GpuBuffer too), a Prim's mesh and an Actor's own skin clone.
ludic deps --resources over main: resource_drops 0, owned_leaks 0; a probe freeing a Target's fbo
alone was reported for its colour and depth (2), then removed. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:08:15 +03:00
8fbfeb7f9d ludic.base: core_undrained fills a list the caller keeps; hd_refuse's panic declared; ludic.photo's slug declared (a photograph taken)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:06:50 +03:00
a6364199da census by owner (25.5c): which state holds what, and how much it grew
The escape analysis now follows which state a kept value is stored into (a state parameter, a state
global - each its own class - through the flows to and from it), and every heap site in the fence's
table carries that owner. The census writes, per owning state, what its sites hold and how much that
grew since judging began: 'owner NotesState holds 6400 (+5600 since judging began)'. A keep() or
intern() is a site of its own for this, never scratch and never reported as a keep or a birth. It
needs the analysis, so the arena's (or --escape-report's) build; this is what a soak watches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:05:53 +03:00
f16ac4ef9e Merge branch 'lang/memory-owned' into lang/foundations 2026-09-28 17:04:40 +03:00
c5114a73fc Merge branch 'r3d/resources2' into lang/foundations 2026-09-28 17:04:40 +03:00
b87ee96805 owned fields (25.5e): @owns(Kind) on a record's field, and owned_leaks
A field marked @owns(PhysShape) holds a handle its record owns. A function that releases one owned
field of a record (body_free(w, s.body)) and neither releases nor hands on another owned field of
the same record type (s.shape) gives the first back and loses the second - the phys_remove bug, at
compile time. ludic deps --resources (or --owned) lists them; owned_leaks is a number --check
ratchets. A test: the function that frees a solid's body alone is the one found; the one that frees
both is not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:01:28 +03:00
90074bfe60 render3d: @creates(Pipeline) on gvk_pipeline
A pipeline has no release: it is kept in the cache for the program's life (gvk_pipe_build stores it).
With every render3d handle annotated, ludic deps --resources over main e447acdd reads 0 drops; a
GpuBuffer made and bound to a local in a game function was reported (1), so the 0 is a real one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:59:45 +03:00
0b89ed85b7 render3d: @creates / @releases on its handles
GpuBuffer (gpu_buffer_new/free), GpuTexture, GpuFramebuffer, GpuRenderbuffer, Target (target_new/free),
Model (gltf_load / model_release), Mesh (gpu_mesh_new / gpu_mesh_free, mesh_release), Actor
(actor_new / actor_release) and SkinClone (skin_clone / skin_clone_free), for ludic deps --resources.
Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:58:31 +03:00
64b361510d resources (25.5e): @creates/@releases on Physics' handles - every shape maker and phys_shape_free (PhysShape), the body adds and phys_remove (PhysBody), the walker (PhysWalker), and ludic.nav's crowd agents (NavAgent)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:57:39 +03:00
a0c6f8ce7f Merge branch 'lang/memory-resource' into lang/foundations 2026-09-28 16:55:04 +03:00
e009ea313b resources (25.5e, first half): @creates(Kind) / @releases(Kind), and resource_drops
A function marked @creates(PhysShape) makes a handle one marked @releases(PhysShape) gives back.
ludic deps --resources lists every creating call whose handle is thrown away, or bound to a local
that is never released, passed on, stored or returned, and resource_drops is a number --check
ratchets. A test: a thrown-away create and one bound and never handed on are the two found; one
stored in a state and one released are not. A record's owned fields and a borrow form (a shape
used by several scaled ones) are the second half, with a resource type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:54:27 +03:00
a8906b4f79 Merge branch 'lang/memory-prim' into lang/foundations
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
2026-09-28 16:53:21 +03:00
1cc507e181 escape: a value of a primitive type holds no reference - no flow, no store
The analysis gives each local its declared or inferred type (a record's field, a list's element,
a call's result, words/floats) and takes a value whose type is a number or a bool out of every flow
and store: a float copied out of a frame's floats into a state's no longer makes the frame's list
kept (shadow_fit, water_reflection_pass, layer_partition_lods). frame_keeps 190 -> 181 on the game;
birth_leaks 564 -> 581, the lists that copy was hiding now seen as made and dropped outside a frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:52:24 +03:00
6f16e96de2 Merge branch 'lang/allocs6' into lang/foundations 2026-09-28 16:52:04 +03:00
cf8cbb3afb Merge branch 'lang/ecs' into lang/foundations 2026-09-28 16:50:50 +03:00
2fca1056c6 capacities (25.5a): @max on Physics' bounded lists - every fact pool and free list (1024), the walker slots (64), the shape and body-owner tables (262144), npc's walkers and posts (64)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:49:58 +03:00
39af9cabc0 reachability scan (25.5b) and exit accounting; free() and print release what they are handed
Mem.scan() and R3D_ALLOC_SCAN=<frame> (at that frame's mark, when no function is running) walk
every heap block reachable from the program's globals - the states among them - conservatively: each
word that is a heap block's start (malloc_size says so) is followed, blocks made before tracking too.
A tracked block nothing reaches is a leak whatever a frame's totals say; they are summed by site and
printed ('alloc-scan: frame 39 - 32 bytes in 2 blocks ... reachable from no global or state', then
the sites). R3D_ALLOC_EXIT=1 runs the same scan as the program quits. A test: two records dropped in
frame 20 are the two found, the one pushed into a state is not.

The escape analysis now takes free(x) as giving x back (ES_FREED, flowing to what reached x) and a
print's argument as used up, so ludic deps --births lists only what is never given back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:49:27 +03:00
3908163bdf Merge branch 'lang/memory-caps' into lang/foundations 2026-09-28 16:49:06 +03:00
4480353cb2 Merge lang/foundations 3205408 into lang/ecs 2026-09-28 16:49:02 +03:00
7d85ea3432 @max on the bounded lists: TextRing's slots, the pack's counts and change ring, the net's fact ring, the compass pools, ludic.ui's memo (its 3/4 of MM_CAP), screen pools, answer and pointer rings; ludic.base's intern test holds the table to its cap now that past it is Mem.over
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:48:53 +03:00
3762453d83 reachability scan (25.5b) and exit accounting; free() and print release what they are handed
Mem.scan() and R3D_ALLOC_SCAN=<frame> (at that frame's mark, when no function is running) walk
every heap block reachable from the program's globals - the states among them - conservatively: each
word that is a heap block's start (malloc_size says so) is followed, blocks made before tracking too.
A tracked block nothing reaches is a leak whatever a frame's totals say; they are summed by site and
printed ('alloc-scan: frame 39 - 32 bytes in 2 blocks ... reachable from no global or state', then
the sites). R3D_ALLOC_EXIT=1 runs the same scan as the program quits. A test: two records dropped in
frame 20 are the two found, the one pushed into a state is not.

The escape analysis now takes free(x) as giving x back (ES_FREED, flowing to what reached x) and a
print's argument as used up, so ludic deps --births lists only what is never given back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:48:35 +03:00
3205408290 Merge branch 'r3d/keeps-max' into lang/foundations 2026-09-28 16:47:35 +03:00
c6bea826f6 render3d: the lists sized at start-up carry @max at their room
The retire, free-range and spare-id lists (4096), the per-buffer flags and the prime handles (16384),
the stage and the actor spares (2048), the per-program uniform offsets (4096) and a draw's set key
(130) are each @max'd at the room gvk_startup_state / actor_init made, so outgrowing one ends a dev
run with its name instead of quietly copying. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:47:20 +03:00
54c0feafc2 Merge lang/foundations 6fb5118 into lang/ecs 2026-09-28 16:46:20 +03:00
46000362ba render3d: frame keeps 32 to 16 - tables and queues sized at start-up, the rest declared; birth leaks freed
The texture and framebuffer tables grow to 4096/1024 and gvk_prime's queue is made in
gvk_startup_state; gpu_unit_2d's and the actor lists' lazy starts go. @alloc_ok on layer_room (a layer
outgrowing its cap), overlay_init, gvk_read_screen, gvk_hdr_metadata (a settings change), DLSS's
gsl_struct/gsl_fn, an actor's part tables (given back by actor_release) and the pool's fallback.
Birth leaks freed: gpu_caps_probe's create structs, gpu_caps_fake's text, gvk_note's line,
gvk_layout_key's table and each key it grew from, ov_text_wrap's slices, the default sky path.
(ludic deps --births still lists freed sites: its walk does not see free().) Compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:46:17 +03:00
f8825dc55a ludic.ui, ludic.i18n: frame_keeps and birth_leaks to 0 - :nth-child's a and b read once per argument, a border-image's miss its own function; typing, the dev dump, parsing, the pointer ring's making, Ln's counted line and the plural header declared
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:46:00 +03:00
6fb5118afd capacities (25.5a): @max(n) on a list field, and a full table is a failure
'@max(64) boxes: []Box' on a property's or a state's field is a promise: the grow path of a push to
that field (only the grow path, so nothing is paid until it doubles) checks it, and growing past n is
reported by the fence - 'PoolState.boxes grew past its @max(16) (it holds 16)' - counted under
count, said under warn, and under fail (a headless or dev build's default) the run ends with exit 87.
Mem.over("what") is the same for a package's own table: ludic.base's StrTable past its most
(sb_intern) and ludic.ui's memo past three quarters of MM_CAP no longer quietly copy per call. The
census counts overflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:43:02 +03:00
23d204b9fc Merge lang/foundations 69d1db0 into lang/ecs 2026-09-28 16:41:55 +03:00
429e419327 ludic.ui, ludic.i18n: frame_allocs to 0 - a wrap, an ellipsis, a tooltip's lines, a text-shadow's colour and a range's value kept by what made them; pointer events from a ring; inline text joined through the memo; the pool's and the state's lists through ui_kept; parse, memo miss, pool miss, lazy start and template errors declared; the translation cache clears in place
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:41:47 +03:00
69d1db06a5 Merge branch 'lang/memory-birth' into lang/foundations 2026-09-28 16:38:21 +03:00
ce2699dfee leak at birth (25.2d): an allocation nothing keeps, made where the arena does not take it
ludic deps --births lists every site the escape analysis finds kept by nothing and not the frame
arena's - boot and load code, a function spanning frames, frame code with the arena off - which is
made and dropped and never given back; birth_leaks is a number --check ratchets. A text used up by +
or == where it is made is freed at once and not counted; nor is what @alloc_ok covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:37:50 +03:00
a64713fc23 Merge branch 'r3d/floor' into lang/foundations 2026-09-28 16:37:09 +03:00
958a262ba1 render3d: to its floor - messages in declared helpers, post and DLSS set up at start, lists sized
Seventeen inline messages (allocation failures, missing conversions, the no-pipeline and compressed-
target warnings, resize and swapchain lines, the test-frame camera line, DLSS evaluate, shadow memory,
stream and terrain debug, the water test) are each a function of their own under @alloc_ok, taking
numbers, so the paths that say them hold no site. post_measure's two 1x1 exposure targets are made in
post_init and DLSS's evaluate structs in gsl_init; DLSS's camera up is a state field. The lists play
pushes into (retired, free ranges, spare ids, the per-buffer gpu flags, the stage and the actor spares)
get their room at start-up (gvk_room_*, actor_room), so a push fits. Compiled (steady).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:36:44 +03:00
f7e6859696 Merge branch 'lang/memory-keep' into lang/foundations 2026-09-28 16:35:21 +03:00
db3a3d80d1 frame allocs: the action queue's generated takers are its kept records, not frame allocations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:34:59 +03:00
a0b030290b region rule (25.3c): keep() and intern(), frame_keeps, and --arena-strict
keep(x) copies a string, a slice (header and elements) or a record (shallow) onto the heap; intern(s)
hands back one heap string per distinct text from a fixed table in the runtime (FNV-1a, 65536 slots,
copied the first time; past 49152 only copied). Both are how frame code keeps what it made on purpose:
the escape analysis takes the copy as the heap's and leaves the argument LOCAL.

The analysis now records why a class escapes (the store, the event, the global it reached) and
ludic deps lists every allocation frame code makes and keeps - fkeep lines, 'ludic deps --keeps',
the frame_keeps number --check ratchets - leaving out what is under @alloc_ok and a push's growth
(25.5's capacities). --arena-strict (or 'arena strict') makes each an error naming the store, before
anything is emitted. A test: a template stored into a state is the one error; keep and intern of the
next two, an @alloc_ok push and a scratch temporary are not; 195 frames of arena resets under
R3D_ARENA_CHECK=1 later the kept and interned texts read as made, and intern gives the same string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:33:53 +03:00
629579123a Merge branch 'lang/ecs' into lang/foundations 2026-09-28 16:31:09 +03:00
163aabea6c Merge branch 'r3d/lazy-starts' into lang/foundations 2026-09-28 16:30:58 +03:00
deb7c0d0c0 render3d: scratch the frame uses is made with the state, not on first use
Render3dState's cam_planes, gpu_u_tmp, q_scratch, ov_nine_buf, the caster test's four points,
ter_bw, ter_scr and water_saved default to their buffers (as ludic.anim's clip state does), so the
lazy starts in cam_planes_update, gpu_tmp, terrain_height_smooth, ter_scratch, the water pass and
gvk_startup_state go. q_euler makes its views of q_scratch once (guarded on the view now). Compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:30:17 +03:00
bdfe3f18fe ludic.compass, inventory, shop, base, things: the compass's lists are state defaults and push through kept_push; the pack clears in place; setup, bind and load paths say so in @alloc_ok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:29:33 +03:00
384324c85a frame allocs (25.2): ludic.anim's pose scratch made with its state (pose_part, the name no longer taken for udp_ip's part), a model's clips read at its load declared
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:29:15 +03:00
cd71da9330 Merge branch 'lang/memory-arena' into lang/foundations 2026-09-28 16:26:12 +03:00
bef0d6fbca arena (25.3b): LOCAL sites allocate from the frame's scratch; dispatch is not an allocation; @frame by property
Behind ludicc --arena (or 'arena on' in the program's package.ludic): the escape analysis runs and a
LOCAL site's allocation raises @lp_want for that one call, so it comes from the frame's arena. Two
halves in one mmap reservation (R3D_ARENA_MB each, 256 by default), bump-allocated with a 16-byte
size header, flipped at each frame mark: a frame's scratch is good through the next frame, then its
half is started again (R3D_ARENA_CHECK=1 fills it with 0xDD first). The heap takes over when no frame
is running, off the main thread, or past the half's end; lp_free ignores an arena block and
lp_realloc copies one out. R3D_ARENA=0 turns it off at run time; the census reports each half's
high-water mark. A program that builds text, a list and a record per frame: 29998 heap blocks made
and 18002 freed without it, 8 and 8 with it and 544 bytes of scratch a frame, the same output.

A dispatch's 'new' fills the queue's kept record (E_NEW.b), so 25.2 no longer counts it and 25.3
treats its fields as kept. '@frame' is keyed by property and field: a 'run' field is a root only in
a property that marks it, and 'tick' stays a System's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:25:32 +03:00
297b2beef8 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 16:24:35 +03:00