- ludic.ui cv_join (units_pieces.ludic:93): a var() value's pieces were joined by +, a text left
behind per piece on every memo miss; it is written at its length at once (units_join.ludic), and
the miss is its own declared function (cs_vars_miss), bounded by the memo.
- ludic.hints hn_slots (rail.ludic:41): hints_reset made a new rail every time; the rail is
emptied in place, made again only for another count (rail_slots.ludic).
- value_spare_put (value.ludic:138): a list's spares grow only past the most it has ever cut off;
declared as such.
- ludic.save: save_read reads, parses and frees the file's text - the tree's strings are the
parser's own - and says whether the text was whole (SaveRead.intact) for the words of a refusal.
Maroon Lake's trip and home reads kept the whole file on every load and every menu card read.
Tests: ludic.save (10), ludic.hints (8); the 36 ui examples; value_list_regrow, json_saves and
alloc_fence_declared unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user's walk showed the footprint rising outside the Ludic heap. Jolt's bytes were already counted;
the navmesh's were not, so nothing could see them. Now:
- ludic.nav nav_heap_test: fifty crossings of a four-tile map by the resident index hold the tiles in and
the native bytes to the first crossing's; a thousand crowd walkers in and out grow nothing; a reset
gives back every byte the mesh took.
- ludic.physics cross_heap_test: a 1 km map of 64 m chunks kept to a ring round a player crossing
corner to corner and back, each chunk a heightfield, twelve owned posts and six owned scaled hulls
as the game makes them: six more crossings hold the bodies, the shapes and Jolt's bytes and peak
exactly. (A post made as an offset of a cylinder, with only the offset owned, leaked the cylinder
every time: the game's solid_pillar owns its cylinder directly.)
lib/macos-arm64 rebuilt; lib/windows-x64 needs native/build.sh run on the PC for the new exports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the user's play, reproduced on Maroon Lake's customisation screen with the pointer scripted over
each control (the lab's R3D_CLICKS): nothing on it was ever pressed or hovered.
- A screen's root <div> round a positioned panel (a component's root holding the kit Screen's
modal) lays out to 0x0, and the hit test (fr_pick) and :hover (fr_under) only walked into a child
whose box held the point - so nothing under it was reachable. A box with no size holds no point and
clips nothing: both look through it now (fr_empty), without hovering it.
- A control's parts (a select's < and >, a range's thumb) are made by ct_part, which never set
`hovered`: `select .ui-prev:hover` could not match anywhere. It is set as an element's is.
Golden ui_pointer_through: the panel's button pressed and hovered through the empty root, and a
select's > hovered (before this: pressed 0, hovered 0). The 36 ui examples pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user's play of the bundle: the character shook while seated. chr_sit_tick eased the body onto the
seat point at the ground's height, then the same frame's chr_travel handed it to the walker, which
pushed it off a log, a bench or a boulder beside the seat; the next frame pulled it back. Unwalked, the
game's walker parks as it does under a rider. A hold's turn toward the work fought the seat's yaw the
same way. The new test sits the body against the boulder: 4.63 m from a seat at 5.7 before, still after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every tree on the map was drawn as an impostor card into all the cascades,
the near two included, whose receivers (within 60 m) are shaded by the near
trees' own LOD2 meshes cast beside them. The cards now cast from cascade 2 on.
Aspen view, same main and foundations, 240 frames, with the figure capsule:
shadow instances 323,587 -> 202,743; shadow GPU 4808-4942 -> 4579/4578 us;
median frame 17200-17693 -> 17178/17205 us. The look held: shots of the view
taken twice a side are identical within a side (0.00% of pixels past 8) and
differ by 0.04% across (every pairing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In cascades 2 and out (receivers from 60 m) a skinned figure of more than
eight pieces - a dressed person, up to 86 of them - casts one capsule its own
height (radius 13% of it) instead of every visible piece: past 60 m nobody
can tell a garment's shadow from the body's. Animals (a piece or two), rigid
props and the near two cascades are unchanged. The capsule is made once.
Aspen view, same main (30ec1722) and foundations (15f1020), 240 frames, twice
a side: shadow draws 1889 -> 1799; shadow GPU 4808/4819 -> 4710/4714 us;
median frame 17200/17221 -> 16910/17128 us. Most of the far cascades' actor
draws turned out to be rigid props (the census's "skinned" counts draws, not
actors), so this is the smaller of the two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the user's play: clicking an item in the pack opened nothing, and the wardrobe's picks did
not respond - both are a component whose button says `emit click` (ItemCell, LookCell, ListRow),
answered by its user's on-click. An on-* attribute's name is kept as a browser would have it, so
on-click is on-press (tpl_event), and the emit looked for "click" and found nothing. An emitted
name now goes through the same tpl_event.
And a settings cycler's left arrow did the right one's job: any press let go on a select stepped it
forward. ct_activate_at steps back when it is let go over the select's .ui-prev; Enter and the rest
of the select still step forward.
Goldens ui_emit_click (the mouse and a press both reach on-click; with the toolchain before this,
neither does) and ui_select_arrows (1 -> 0 on <, then 2 on > >; before this, 1 -> 2 on <). The 33
ui examples pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the fence's kept frames in 22 minutes of play:
- A component field that is a record or a list of records was `value_put(o, k, view_val_T(x))`,
a whole new tree every frame (HudPrompt's notifications). view_fill.ludic generates view_set_T /
view_set_list_T / view_fill_T that fill the object and list under the key in place.
- value_list_fit dropped the items it cut off and value_item made new ones as the list grew back,
a Value per item per regrowth (value_item / value_set_strs); the cut-off items are now the
list's spares (Val.spare), and an item of another kind is turned rather than replaced.
- ludic.ui: a scroll box's "scroll" and a slider's "change" fired a fresh Value.float a frame
(sc_walk, scroll.ludic:36); ui_fire_float takes one from a ring kept with the state (fired.ludic).
ui_object_fit_into is exported, for a draw that keeps its list.
- Json.read_file(path): read, parsed, and the file's text given back - Json.parse(Fs.read_text())
kept the whole file on every read (Maroon Lake's settings peeks).
Golden value_list_regrow: a list alternating 6 and 2 items every frame keeps nothing (the toolchain
before this fails it: +128 B new Val from value_item). Game compiles; ludic.i18n/settings/hints/
base tests pass (ludic.ui has none); arena and fence goldens unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
settings_fill_json(v) writes every saved setting into an object the caller keeps, making a Value
only for a key seen the first time (value_set_str / value_set_int): Maroon Lake's Settings pages
rebuilt the whole tree on every change and dropped the old one. A text read from a file is interned,
and so is a muted card's key ludic.hints does not know, so the parsed tree can be let go whole.
Test: the store filled into a kept object changes its Values in place (6 tests pass).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
i18n_use re-read and re-parsed the .po on every change of language (ev_SettingsChanged, SetLang)
and dropped the old tables for good: the play of 22 minutes left lines_of 650 KB, fs_read_text
409 KB, PoEntry 113 KB and unquote 273 KB unreachable, and three of its @alloc_ok sites were
declared but unbounded. Each language's tables are now filed by index once built (tables.ludic,
I18nTables) and put back by reference; a new probe (i18n_init) or a different file for a code
(lang_add) reads it again.
Test: a language switched back to is its kept tables, not its file read again; a new probe reads
it (9 tests pass).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user saw kit grass, ferns and flowers lit up at night. Nothing was
emissive and every layer draw binds the scene's lighting; what differed was
occlusion. At night the sky's light is nearly all the light, and it is
scaled by ambient occlusion: the meadow's blades are heavily occluded near
the ground, while a kit plant's own AO map knows nothing of the grass around
it, so it took the full sky. By day the sun hides the difference.
model.vert hands on each vertex's height above the model's base (v_lh), and
a non-blade FOLIAGE plant under 2 m scales its AO from 0.35 at the ground to
1 at 0.9 m. Crowns and the blades are untouched.
Measured in the lab (R3D_AT=tree, same binary, old and new SPIR-V): the fern
against the meadow beside it was 2.05x at 23:00 and 1.66x at 13:00; now 1.58x
and 1.60x - the same relation by night as by day. The flowers' brightest
tenth at night 78 -> 52. The noon frame moves 8.4% of pixels past 8 (the fern
and flower bases a little darker; run-to-run noise is part of that).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user's play found the aspens' branches swinging like rope. Three terms:
- the whole-tree bend reached 22% of the tree's height at a gust's top (three
metres on a 14 m aspen); 4.5% now, and the side-to-side part at a quarter of
that and slower;
- a branch term: nothing within 0.35 m of the trunk, growing with the distance
out, phased by the direction the branch leaves the trunk (constant along a
branch, so it moves as one piece and its neighbours are out of step);
- the aspen's leaf flutter took its phase from each vertex at eleven radians a
metre, which bent every twig along its length; about three now, and half the
amplitude.
Measured on the lab's `aspen` view (10-frame bursts, crowns only, same binary
with the old and new SPIR-V): pixels moving more than 8 over ten frames 16.8%
-> 9.7%, frame to frame 4.4% -> 2.1%. SPIR-V rebuilt with `ludic-dev shaders`
(vertex stages only; the manifest is unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A called boat and one got out of drifted across the lake on the wind (the user's play of the bundle).
The tests hold a boat at its berth in a full wind for eight seconds, and one dropped out on the lake
where it was left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every read of the screen made bytes(screen_w * screen_h * 4) and dropped it:
the user's manual play left 22,970,368 B unreachable (exactly 3024x1898x4)
at gpu_vk_draw.ludic:1673 - one per photograph (shots' ph_grab, through
gpu_read_screen_bytes). The read-back is now gvk_read_px, made once and made
again (the old one freed) only when the screen's size changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A block took whatever @lp_site held when it was made. Only malloc, calloc, realloc, concat, the number
texts, the float text, a substring and Text.* set one, so every other runtime helper - intern,
Text.repeat and the string builders, the Fs, Os, unicode, uuid and crypto helpers - was charged to
whichever line had allocated last (walk 9 blamed gvk_tex_storage, m4_new, kept_push$int and
survey_op_reward for intern's 16 B copies).
- Any `call ptr @lp_*` now takes a site of its own, its kind the callee's name when no better one is
known (intern, str_repeat, fs_list, ...). What the helper makes, in however many blocks and
through whichever helpers it calls in turn, is that line's.
- When the call returns, @lp_site goes back to site 0, now named "(runtime) (no site) unsited": a
block made with no site of its own says so instead of borrowing the last one.
- The ECS stores' grows and a mod's registered stores - the only allocations emitted outside
emit_bind - take a site each.
Golden alloc_fence_sites: two lines take turns keeping memory, Text.repeat and a record, every frame
judged. Each report names its own line (25 x +32 B str_repeat at :11, 25 x +16 B new Box at :12);
the toolchain before this charges 23 of the 32 B texts to the new Box line. The other fence and arena
goldens are unchanged; ludic.base's tests pass; the game's frame ratchets are 0 on main 7cb2b164.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Past the 4 MB store lp_copystr falls back to the heap, and past the table
(49152 texts, or 64 probes) lp_intern copies on every call: either way a
program interning without bound would grow unseen. Both paths now call
lp_intern_over, which reports through lp_cap_over once ("intern (4 MB of
text, 49152 distinct texts) is full"), so warn says it and fail stops the
run (exit 87) like any capacity past its promise.
Checked with a compiler built from these sources (selfhost-build): 60000
distinct texts under R3D_ALLOC_FENCE=warn print the line once and every text
comes back right; under fail the run exits 87.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Walk 9's fence named 256 frames of +16 B at gvk_tex_storage:244 and
m4_new:92, always under HudDay's and HudGuide's models. Neither site
makes 16 B: the blocks were intern's copies (lp_copystr), which never set
lp_site and so were charged to whatever allocated last. The HUD's clock
and the guide's distance are a new text every few seconds, and each first
one was a malloc kept for good.
lp_copystr now copies into @lp_istore, 4 MB in the binary (untouched pages
cost nothing), and falls back to the heap only past it; lp_free ignores a
pointer into the store, so a text freed after it was interned is no fault.
value_num_text - the text a screen shows for a number, dropped whenever
the number changed - is interned the same way and its string() given back.
Checked: a program interning 100000 texts gets every one back right, the
same text as the same pointer, a freed one harmless; the headless valley
compiles, with frame_allocs, frame_keeps and birth_leaks at 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
water_reflection_pass made view(water_saved, 16, 16) and view(water_saved, 32, 16)
inside the block that makes the reflection target, so every time the target was
made again (a resize, a render scale, a settings change) two more 16-byte views
were made and the last two dropped: the windowed walk's fence caught one such
frame, +32 B. water_saved is the state's for good, so its views are made once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AVFAudio keeps a 64-byte AudioQueueOwner for good on every AVAudioPlayer play
after the clip has finished - measured one a play, whatever is called around
it (prepareToPlay, pause, no rewind) and still there after the player is
released; a stop before the play made one every time. The windowed walk showed
it as AudioQueueOwner 73 -> 87 in a minute.
A sound is now decoded once into a PCM buffer and played by a voice of its own
on one shared engine: a player node (the buffer scheduled again on each play,
looping for -1), a varispeed (the rate) and a small mixer (volume and pan).
snd_playing compares the uptime clock with the end worked out when the clip was
played (asking the node where it is made two AVAudioTime objects a call), and
snd_play drains an autorelease pool of its own. The C interface is unchanged.
A harness driving snd_* directly (400 plays past the end, the playing flag
checked during and after each, a loop and a stop, the setters) holds the heap
flat to a block and gets the flag right 400 of 400; the windowed valley
compiles and links against it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two blind spots from the windowed walk, where the heap grew about 1 MB a minute and the census read
`frames 0 bad 0 kept 0`:
- @alloc_ok memory was never held to its reason. Every R3D_ALLOC_DWIN judged frames (600) each site's
declared bytes are set against their high-water mark: a new high adds to a streak, a flat window
takes one off, a fall ends it. R3D_ALLOC_DRISE (6) is "declared but unbounded", said once per site
with its line; fail mode exits 86. The census adds `unbounded N` and a `dsite` row per declared
site by its growth since judging began. A list pushed forever grows by doubling, rising too seldom
to make a streak; a record or text made every time (a re-mount's defaults) is what it catches.
- Mem.play() (every screen opened) restarted the warm-up, so memory kept every frame was never flat,
the cap never came, and nothing was ever judged. A rewarm now keeps the first deadline, and past it
has R3D_ALLOC_REWARM frames (120) of grace.
Goldens: alloc_fence_unbounded (a record a frame under @alloc_ok: exit 86, named, census unbounded 1);
alloc_fence_rewarm (kept every frame, Mem.play() every 360: judged and failed at frame 3000 - the
toolchain before this runs all 6000 frames and exits 0). alloc_fence_leak, _declared, _auto,
alloc_ok_private and the four arena goldens unchanged; the game's frame ratchets 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
input_device_commit made words(6), words(IN_PADS * 6) and words(IN_TOUCH * 3)
every windowed frame and dropped them - the walk's exit scan found 7 MB of
them unreachable after ten minutes (headless never polls devices, so no
headless run saw it). They are made in in_init with the rest of the input
state and filled in place. input_text's UTF-8 buffer is the state's too,
sized for the most the window hands over (64 units, four bytes each): it
made one per keystroke.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>