Commit graph

923 commits

Author SHA1 Message Date
ef924b9e9e Merge commit '2ce2d99' into lang/foundations 2026-09-29 15:38:40 +03:00
2ce2d997be render3d: the fog wall culls on the CPU - nothing past it costs a draw, a vertex or a caster
cam_sphere_visible refuses a sphere wholly past the wall (terrain patches, scatter cells, stream
chunks, grass tiles, water), actors past it are skipped for draws, casters and outlines, the grass
reach and the streams' generate-and-gather reach end at it, and the card shadows cast only from
the wall's share of a layer (fog_casters.ludic, rebuilt every 4 m). r3d_beyond_fog is exported for
the game to skip animating what will not be drawn. Render-only: no query of the ground or the world
changes, and off is the old frame (0 pixels over 8 against 160ce96, twice per side).

Draws per frame at the overlook: 2757 off, 1104 at 175 m, 484 at 30 m.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:38:00 +03:00
2958539514 ludic.ui: a new component instance's first build is declared - a first show mid-play is not a frame's keep
Master's windowed fog profile failed the fence at walk t 35 s: +512 B from value_new / value_put /
value_slot / value_lists under bd_class and cmp_keycap_model - a KeyCap first shown mid-walk.

It is a first build, bounded, not a per-show leak: an unmounted instance goes to in_free and the next
show of its class reuses it (in_reuse, `renew`), its props and model objects kept and filled in place.
Only a NEW instance makes them - bounded by how many of that class are up at once, and the ones
unmounted less than two builds ago. in_reuse already declared the record; the props and model objects
and their first fill, made afterwards in bd_class, were not. They are now: in_reuse marks a new record
`fresh`, and bd_class's first fill of it goes through bd_first_fill (@alloc_ok) - a reused instance's
fill stays judged, so a real per-show leak would still fail.

Golden ui_first_show: a component first shown at frame 700, once the fence is judging, then hidden and
shown twice more: bad 0 (the toolchain before this fails frame 701: +464 B value_new from value_slot
under cmp_cell_model, value_put grow - the profile's failure). The 37 ui examples pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:18:22 +03:00
b9f9cb8495 render3d: r3d_fog_wall(dist) - fog that closes to the sky's horizon colour from 0.3 dist to dist, and nothing drawn past dist + 8 m
Every lit program blends toward the horizon colour (the prefiltered sky with the sun's inscatter,
so it carries the day's grade); the sky's horizon band is pulled to the same colour, wider the
closer the wall. The far plane, scatter and stream cull reach and the shadow cascades are clamped
to the wall; the reflection pass shares the shaders and the cull. 0 is off: every branch is gated
on u_fog_wall > 0 and r3d_reach hands back the far it was given. R3D_FOG_WALL=<m> for a shot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:07:40 +03:00
160ce96e4f Merge commit '0c6dab3' into lang/foundations 2026-09-29 14:56:32 +03:00
0c6dab39a8 render3d/vk: a sub-allocation takes a whole number of its alignments, so the free list stays small
The valley self-test died after the Lamar swap, loading Maroon's pine LODs:
gvk_fr_blk (the device suballocator's free ranges) grew past its @max(4096).
Nothing failed to coalesce: a range was carved at the buffer's bare size, so
what was left after it started unaligned and the next buffer, rounding its
start up, left a sliver before it that no later buffer could use. One host
block held 3,968 small buffers and 3,718 free ranges between them - 496 KB
free in all, 133 bytes a hole. Those merge away when a neighbour is freed, so
nothing leaked; the count simply rose with live buffers.

gvk_mem_new now carves `span`, the size rounded up to the alignment, and
records it as the allocation's length, so a free gives back exactly what was
carved and the rest of a range always starts aligned.

The same repro (the lab's `tests` scene, headless, a fresh test root): the list
never reached 256 slots (it passed 4096 before), the run completes (exit 0),
LAMAR OK and THINGS OK. r3d_small was 0 throughout, as ECS said.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:56:14 +03:00
9b1cd7d608 render3d/vk: a small window's screen has no depth image
A small window (r3d_small: the launcher's UI process) draws its interface flat
and tests no depth, but gvk_screen_make gave it a 32-bit depth image the size
of the screen - 8 MB at 1920 x 1080. In small mode the screen has none: the
pass and a clear with no depth attachment already leave depth alone (clearing
DEPTH_BUFFER_BIT with no attachment is skipped), and pipelines follow the
pass's formats.

With the launcher's own 1920 x 1080 backdrop (maroon-lake game/launcher-small),
the launcher window measured 378 -> 295 MB of footprint at 25 s; its headless
shot (R3D_SMALL=1, so no depth) differs from the unchanged build by 0.279% of
pixels, the resized backdrop, and two runs of each are identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:50:17 +03:00
22a384a76a Merge commit 'db7c21e' into lang/foundations 2026-09-29 14:45:29 +03:00
db7c21e5d8 render3d/overlay: the font atlas kept as its coverage, one byte a texel
The atlas is white glyphs on alpha and the overlay reads only the coverage,
but it was uploaded RGBA8: Maroon Lake's 3072 x 2688 atlas was 43 MB of Metal
memory with its mips, the largest single thing in the launcher window.
overlay_font now keeps the alpha alone as R8 (ov_font_tex; any other PNG
shape is uploaded as before) and overlay.frag reads .r - the white fallback
texture reads 1 there as it did from .a.

Launcher window, graphics regions: the atlas 43.0 -> 11.5 MB; process
footprint at 25 s 402 -> 378 MB. The headless launcher shot is identical to
the unchanged build's (0 pixels differ; two unchanged runs differ from each
other by 1.69%, and the new one by the same against them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:45:02 +03:00
2bb7ae1883 render3d/vk: a buffer re-filled every frame ping-pongs between two instead of being made again
With one frame in flight, a buffer filled every frame (the interface's vertex
buffer, ov_vbo) was still being read by the frame on the GPU when the next
frame filled it, so gvk_buf_reserve released it and made a new VkBuffer and
its memory every frame - and every new buffer took a prime submit of its own.
Each handle now keeps a second buffer (gvk_bsp_*): a busy one swaps with it
when it is free, else the busy one becomes the second and one new buffer is
made; releasing a handle lets both go.

The launcher window (--launcher-ui, R3D_VK_PROF, per 120 frames): buffers
made/destroyed 120/120 -> 0/0, command buffers 240 -> 120 (one a frame). Its
footprint does not move with it (358 MB at 40 s, both): the churn was work,
not memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:45:02 +03:00
e958d00373 Os.heap_relief(): the allocator's freed-but-cached memory handed back, once after a load
Beside Os.heap_bytes: malloc_zone_pressure_relief(NULL, 0) on macOS (weak, so a libc without it reads
0) and HeapCompact(GetProcessHeap(), 0) on Windows - kernel32 only, so the Windows build imports
nothing new; the bytes it says it released. Once after a load, never per frame.

Measured, for the record: on macOS it does NOT reach the large-block cache. A C program that frees six
15 MB blocks still holds 90 MB of MALLOC_LARGE (empty) after relief on every zone (it returns 0); only
MallocLargeCache=0 in the environment AT PROCESS START turns the cache off (read at malloc's init -
set later, it does nothing). Maroon Lake's watcher sets it for the processes it spawns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:30:20 +03:00
5297996640 Merge commit '8030cfd' into lang/foundations 2026-09-29 14:23:33 +03:00
8030cfdfde render3d: r3d_small, a renderer sized for a small UI window
Set before r3d_open: the frame's uniform ring is 2 x 4 MB (was 2 x 64), memory blocks are 8 MB with
anything over 4 MB on its own (was 64 and 16), and the descriptor pools are an eighth of a world's.
Maroon Lake's launcher window draws a picture and text and never becomes the game; its launcher home
and Settings page draw pixel-identical on it. The full-size path is unchanged (steady: STEADY OK).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:23:00 +03:00
84d754a4e9 a render3d program's window is opened by its renderer, not by the runtime before main
rt_init opened every windowed program's window before main, and render3d's gvk_open then only
retitled it. win_open makes the window when there is none (cocoa.ll and win32.ll alike), so for a
program that has gvk_open the runtime now leaves it (window_later(), an intrinsic: windowed and
render3d present): a process that never reaches the renderer - Maroon Lake's launcher watcher, which
only spawns the game and waits - never makes a window, an NSApplication or AppKit's heap.

Audited every window native reachable before the renderer opens (settings, telemetry, rescue, the
watcher reach App.* and Input.*): on macOS each that loads W_win / W_app / W_view / W_mtl / W_glctx
checks it for null; win_close, win_running, win_text, win_held, win_cursor_mode, win_gl_scale and the
pad and touch reads load none. On Windows each that loads W_hwnd / W_hdc checks it; the rest load none.

Measured, windowed, R3D_DEV=1 R3D_PLAYTEST=2, both killed after:
- the game straight to play: the window, the Vulkan swapchain (1920x1080) and the valley's models
  come up, alive at 30 s;
- the launcher (R3D_GAME=launcher): the watcher 11 MB -> 3.7 MB, its launcher window alive at 10 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:19:10 +03:00
30a641d249 Merge commit '12fdc07' into lang/foundations 2026-09-29 14:14:26 +03:00
12fdc0717e a program's states made on first injection, and the ECS stores started at 8 slots
Maroon Lake's launcher watcher - a process that only spawns the game and waits - held 58 MB, 48 MB of
it MALLOC_SMALL. Measured with malloc stack logging it was not the state defaults but L_grow: every
program sized every property type's per-entity store to MAX_ENT (1024) slots at start, 1,583 stores,
entities or none. And every state record was made with its defaults in L_init_globals before Boot.

- emit_lazy.ludic: a program's (not the runtime's) state global is left out of L_init_globals, and
  every read of it calls @S_<global>(), which makes it on first call from its own initializer - after
  every registry and plain global, so a default may read them (the init-order crash cannot come back
  through a state). What a getter makes is declared (@lp_fdecl): a state first touched in play is made
  once and not judged as a frame's keep. A function value's trampoline calls the getter too.
- L_grow starts the stores at ECS_FIRST (8) and doubles as entities come, as it always did past MAX_ENT.

The watcher (with the game's watch step moved before the systems' defs): 57 MB -> 11 MB; the only
state it makes is UiState (14 KB). What is left: AppKit's window, opened by rt_init before main for any
windowed program (~4 MB), and the runtime's font, image and 2D inits (~1.2 MB).

Goldens: the arena, fence, value and json goldens; the 36 ui examples; 30 of the 32 ECS test cases
(sprite_render and sprite_atlas time out under a plain runner with the toolchain before this too).
Package tests: ludic.base, save, settings, i18n.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:14:05 +03:00
5d8cbac06a render3d/vk: a swapchain rebuild gives back its scratch and the old image list
gvk_swap_make freed none of what it made - the surface caps, the count, the
format and present-mode lists, the create info, the handle out - and replaced
gvk_swap_images without freeing it: a few hundred bytes every time the window
changed size, went to or from Retina, or the chain came back suboptimal. Each is
now freed on every way out (the minimised return and the failed create included),
and the old image list before the new one is made.

A windowed memory walk built against it (main 5ee600d2, 300 s, the autopilot
opening screens every 6 s): footprint 2816 MB at 120 s, 2818 MB at 300 s; the
fence judged 0 frames kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:56:02 +03:00
90b23e11e9 Merge commit '3f685af' into lang/foundations 2026-09-29 13:43:43 +03:00
3f685af868 Json.free_all is safe on a parsed tree a migration and a loader have edited; sv_str keeps a copy
A load freed only the parsed trip's nodes (Json.free) and kept every string the parser made, because
a loader might keep one; free_all freed every string and every key, so on a tree a migration had
added a literal to (`Value.put(v, "sver", Value.str("2"))`) it freed the literal and aborted.

- The parser marks the string values it makes (JP_OWNED, in the node's otherwise unused num) and
  interns object keys (a few names, never freed); free_all frees only marked strings, never keys,
  and a list's spares too. A setter that gives a marked node other text (value_set_str/_strs,
  value_into_str/_strs, value_become) frees the parser's text first. value_as_int / _as_float read a
  string as 0 as before.
- ludic.base sv_str returns intern(...): every package load that keeps a text read from a section
  (minimap labels, a Thing's look, photo tags and files, an effect's label, ...) holds its own copy.

Golden json_free_edited: parse, put a literal key and string in, set a string, keep an interned copy,
free_all - 1100 loads: the copy reads on and nothing grows (the toolchain before this aborts, 134).
Tests: ludic.save, base, settings, minimap, things, photo, effects; json_saves, value_list_regrow; the
36 ui examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:43:24 +03:00
2bd6d30dcc Merge commit '99f8749' into lang/foundations 2026-09-29 13:41:50 +03:00
99f8749949 render3d/vk: the kept descriptor sets start over after 256 textures are freed
gvk_sc_make's cache (program + bound textures -> a set in the kept pool) evicted
nothing until the pool's 8192 sets filled. A texture freed - a photograph's
thumbnail, a wall frame, any picture a screen loads - left its entries and their
sets behind for good, since its handle's generation moved on and the key could
never match again: 262 KB over 12 minutes of the user's play, and bound only by
the pool, hours away.

gvk_tex_release counts the releases (gvk_sc_dead); at GVK_SC_DEAD_MAX (256) the
next frame's start resets the kept pool and empties the cache (gvk_kpool_reset,
which waits for the frame in flight), and each draw still in use makes its set
again the first time it is drawn - the budget is the live combinations plus at
most 256 textures' dead ones. The declared reason on gvk_sc_make was the
swapchain's, copied; it now says what the cache is and what bounds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:41:35 +03:00
6f39b50e63 Merge commit '338202c' into lang/foundations 2026-09-29 13:34:15 +03:00
338202c41b play 2's frame keeps and a dropped file: a var() join made at once, the hint rail emptied in place, save_read gives its text back
- 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>
2026-09-29 13:33:54 +03:00
b368912c78 nav, physics: Recast's and Detour's memory counted (nav_heap_bytes / _peak / _allocs, a counted allocator registered at load, tile bytes staged from it); package tests that crossing the map and back holds the native bytes
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>
2026-09-29 13:32:18 +03:00
7b91300962 photo: a frame's tags written into a kept buffer and interned, one string per distinct text - a viewfinder composes every frame, and each tag concatenated a new text the frame dropped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:29:00 +03:00
f38581754a Merge commit '9b3d3e3' into lang/foundations 2026-09-29 13:27:18 +03:00
9b3d3e3298 ludic.ui: the pointer looks through a box with no size; a control's parts answer :hover
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>
2026-09-29 13:26:33 +03:00
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