- The arrows, a press on the select and the pad step past a disabled option (ct_opt_step), wrapping
as before.
- A value naming a disabled option is shown as the nearest enabled one (the lower of two as near) and
set to it once the build is done: an event fired while the tree is built is cleared with the frame's,
so the clamp is queued (ct_clamp_n / _i) and fired after nt_fired_clear.
- The < or > whose next option is disabled is drawn disabled (its part's :disabled), so a cycler shows
where the options stop being on; it still steps past them.
- ct_build_select moves to controls_opts.ludic with the rest (controls_build.ludic 119 -> 104 lines).
Golden ui_select_disabled: options 3 and 4 disabled, a value of 4 clamps to 2, > is drawn disabled
there, > wraps to 0 and < from 0 steps back to 2. The 38 ui examples pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zones_set / zones_clear / zones_merge, zones_contains / zones_nearest and the clusters' bounding circles,
over fixed arrays of ZONES_MAX (16). WildlifeWorld.active freezes an animal outside (no state change, no
dice), NpcWorld.active a walker (and no birth outside), VehicleWorld.active a moored boat, and
ThingsWorld.restocks keeps a morning's restock inside; every default is "everywhere", so an unbound game
is unchanged. Tests: the merge (near players one cluster, far two, a player leaving splits a chain),
contains, nearest, the radius; wildlife, npc, vehicles and things pass as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For loading the world by each player's fog rather than a fixed 900 m. keep_near_test: a zone round
one corner brings in its tile, a bigger one its neighbours, an edge nudged back and forth twenty times
changes nothing, and a zone moved 2 km away lets them all go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whatever is cut at the wall is now cut inside full fog: at 0.3-1.0 the last metres before the cut
were 80-95% fogged against fully fogged ground behind, which drew a line of tufts at 70 m and a
dark band of blade tips at 30 m. The blades thin out while the fog is still coming in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>