Commit graph

896 commits

Author SHA1 Message Date
6029d10e62 character: a seated body is held by its seat alone - no step to the walker while sitting, and a hold no longer turns it off the seat's heading
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>
2026-09-29 13:12:22 +03:00
614f030407 Merge commit 'e691f07' into lang/foundations 2026-09-29 12:37:40 +03:00
e691f072ae render3d/shadow: tree impostors cast only into the far cascades
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>
2026-09-29 12:37:24 +03:00
a1a90e0626 render3d/shadow: a dressed figure casts one capsule in the far cascades
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>
2026-09-29 12:37:24 +03:00
06ecc5cd8e Merge commit 'e476a9d' into lang/foundations 2026-09-29 12:33:13 +03:00
e476a9d975 ludic.ui: emit click reaches on-click, and a select's < steps back
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>
2026-09-29 12:32:45 +03:00
15f1020fb6 Merge commit 'a299117' into lang/foundations 2026-09-29 12:22:42 +03:00
a299117858 frame keeps: a model's records filled in place, kept event numbers, list spares, Json.read_file
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>
2026-09-29 12:22:13 +03:00
9c24d707bc ludic.settings: the store filled into a kept object in place; texts read from a file kept apart from it
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>
2026-09-29 12:22:07 +03:00
1667d7f16e ludic.i18n: a language's lookups kept once read; switching back restores them
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>
2026-09-29 12:21:53 +03:00
6bd75e3bf2 Merge commit '6a82b49' into lang/foundations 2026-09-29 12:19:21 +03:00
6a82b4928b render3d/foliage: a small plant is occluded by the sward it stands in, so it no longer glows at night
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>
2026-09-29 12:19:10 +03:00
a64a403def Merge commit '3a2d594' into lang/foundations 2026-09-29 12:16:23 +03:00
3a2d594626 render3d/wind: trees bend a few per cent, branches move as one piece, the flutter is slower across a crown
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>
2026-09-29 12:16:11 +03:00
d117f2b8a1 Merge commit '66da424' into lang/foundations 2026-09-29 12:15:11 +03:00
0a239c4bed Merge commit '0b35f81' into lang/foundations 2026-09-29 12:14:20 +03:00
0b35f810b6 ludic.character: the look goes up and down to 89 degrees (was -55..35), and the orbit's heading holds when the camera is straight over or under the pivot; ludic.aim: aim_ahead, whether a point is before the view
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:14:04 +03:00
66da424b90 vehicles: a boat nobody rows is moored where it was called or left - its hull put back on the mooring (home_x, home_z, home_yaw) whenever it strays, and no wind on it; the wind drifts only a rowed boat
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>
2026-09-29 12:12:44 +03:00
b2a45c2fda audio: a volume per channel under the master (effects, wildlife, ambient, interface) - aud_channel_set / aud_emit_on, aud_ui on the interface channel, and Audio.music_volume for the ambience loop on the runtime's music channel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:11:29 +03:00
87587b00b6 render3d: gvk_read_screen reads back into one buffer kept in the state
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>
2026-09-29 12:09:11 +03:00
28ad6f62ea reseed after the site audit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:41:44 +03:00
b989f653a0 Merge branch 'lang/memory-sites' into lang/foundations
# Conflicts:
#	selfhost/ludicc.seed.ll
#	selfhost/ludicc.win.seed.ll
2026-09-29 00:40:53 +03:00
6ab98292d2 fence: every runtime call is its line's site, and site 0 comes back when it returns
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>
2026-09-29 00:40:28 +03:00
61e057f73e reseed after intern overflow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:39:06 +03:00
25c8c9279c intern: past its store or its table, the fence's overflow - said once
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>
2026-09-29 00:37:56 +03:00
48a8caa292 reseed after the intern store
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:34:35 +03:00
a99f75f2a4 Merge branch 'lang/intern-store' into lang/foundations 2026-09-29 00:33:43 +03:00
47bb0e5d4d intern: texts copied into one store in the binary, not a malloc each; number texts interned too
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>
2026-09-29 00:31:34 +03:00
765d800842 Xml parse gives back what no node keeps: a closing tag's name, and the text runs and joins an element's text leaves behind (a run a #text node shares is kept); ludic.ui frees an attribute's value once parsed (every reader copies what it keeps) and act_parse's reader
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:30:13 +03:00
7b20f305ff Xml.free gives back a parsed tree's records and lists (never its strings) and the parser's cursor is freed; ludic.ui frees a template's XML once built, sel_parse's and ex_parse's readers, lss_rule's declarations holder, and px_cmp no longer makes a list of its operators per call
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:11:40 +03:00
3816d9924b render3d/water: the saved camera's two views made once, not with each reflection target
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>
2026-09-28 23:47:25 +03:00
afc2d019a3 ludic.ui: a node is made with its UiNodeX, so nx() makes none in a frame; a screen's pool grows both generations together and by half again at once, so a screen's first frames and its reopenings make nothing past the first growth
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:20:38 +03:00
0ed66baed0 Merge branch 'lang/sb-stdout' into lang/foundations 2026-09-28 23:17:32 +03:00
90dd603cee fix(ludic.jobs): jobs_taken and jobs_shown fill a kept list per group (the map and the journal asked every frame, and the fence caught a list kept)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:01:53 +03:00
7187aaa4d5 Merge branch 'rt/input-bufs' into lang/foundations 2026-09-28 23:01:44 +03:00
a5f6c02a09 runtime/audio (macOS): voices on one AVAudioEngine instead of an AVAudioPlayer per sound
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>
2026-09-28 23:00:59 +03:00
f4233ba11d ludic.base: sb_stdout - a StrBuf's bytes to standard output with no string made (a per-frame log line the fence would otherwise see kept)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:58:37 +03:00
88580ca319 ludic.ui: reading a sheet gives back what no rule keeps - lss_strip writes into bytes of its own, a rule's selector and body texts, its selector pieces, each declaration's piece and raw key and value slices, a @keyframes or @media body and a keyframes reader freed once parsed (a kept tpl_words result, a keyframes name and a @media condition never)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:55:40 +03:00
beebbd0bc1 ludic.ui: lss_text frees the stripped sheet only when lss_strip made one - a sheet without a comment comes back as src itself since b307118, and freeing it aborted the game at start
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:40:33 +03:00
d4edd0f5ba Merge branch 'lang/memory-plateau' into lang/foundations 2026-09-28 22:36:50 +03:00
0f04b63516 fence: declared but unbounded, and a rewarm that keeps the warm-up's deadline
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>
2026-09-28 22:36:08 +03:00
dba978e22b Merge branch 'rt/input-bufs' into lang/foundations 2026-09-28 22:35:12 +03:00
34421b8015 runtime/input: the window's mouse, pad and touch buffers and the typed text made once
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>
2026-09-28 22:34:57 +03:00
01c2971ce7 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 22:34:24 +03:00
b30711877d ludic.ui: negative numbers' texts made once per value like the positive ones, big ones kept by value in a ring of 16 (mm_int made a string per call for every coordinate), and a stylesheet without a comment is not copied to strip one
Found by the windowed walk's exit scan (mm_int 1.2 MB, lss_strip 451 KB unreachable).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:34:19 +03:00
fc99cee9a9 ludic.minimap: minimap_seen_text writes the explored grid into bytes the state keeps (a string per cell before, every save)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:33:49 +03:00
2603a79cc4 Merge branch 'lang/ecs' into lang/foundations 2026-09-28 21:46:15 +03:00
a0f60e5296 ludic.wildlife: wildlife_reserve sizes both animal tables and their id maps at a world's start
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:45:55 +03:00
5ffa67b31b Merge branch 'lang/memory-alias' into lang/foundations 2026-09-28 21:17:05 +03:00
1786e144bb ludic.ui: ui_attr's miss is the node's own kept null (made with the pooled node), not a new one per ask - every control asking min/max/step/value per frame made one; a select's options without a value matched by index without asking
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:17:05 +03:00