Vk.heap_bytes pulled the Vk module - and on lang/uifree the GL window path - into a plain program,
which then failed to link (_cgl_offscreen, lgl_GetError). Os.heap_bytes is malloc_zone_statistics
through a weak reference (0 where there is none, and on Windows). Reseeded. Docs for it and for
Json.free / Json.free_all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gvk_tex_storage freed none of ici, out, req and vci (8 blocks a two-texture model); tex_load_ex kept
dds_path_of's string. Found with malloc_history over 400 load/release rounds (712 bytes a round, all
of it these). steady: the model's bound is 4 KB over 200 (was 1.6 MB), and the frame is the least of
three settled windows - a valley self-test beside it read 87 KB once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
s[a .. b] is always a copy, so it is freed once a +, a comparison or print has read it. Reseeded.
string_temps.ludic adds a slice compared and a slice concatenated each round (960 KB over 20,000
before, 0 after) and a kept slice read after its +.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The left half of a + chain, a template's pieces and holes, a number's text and a side made only to be
compared are marked fresh and freed after the +, ==, != or print that reads them. lp_int_str and
lp_long_str move their digits to the start of the buffer, so the pointer they return is the one
malloc gave. Reseeded. examples/lang/string_temps.ludic: kept intermediates stay good, and 20,000
rounds grow the heap 0 bytes (2.9 MB before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A freed texture or buffer id goes on a spare list the next one takes; tex_note_size keeps sizes by id.
model_release frees prims, meshes, material names, the skin and leaves gltf_cached. actor_release
takes an actor off the stage and actor_new reuses its record (ECS's Things come and go all day).
steady.ludic: an actor round at 0 bytes over 2000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
n x n samples of chunk (i, j) of a size_m grid from the terrain's corner, row-major into the
caller's buffer with nothing allocated, each the (1 4 1) / 6 B-spline filter of the texels under it
(ter_spline_at): what ludic.physics' jph_shape_heightfield_bspline makes of the whole map, so a
chunk's physics ground matches the drawn one and its neighbours' to the bit. The read-back lives
for the whole map (terrain_generate to terrain_unload). examples/rendering/chunks.ludic, in the
suite, checks the shared edges, a sample against the filter by hand, and a short buffer refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gvk_mem_new made a one-slot []pointer per allocation, and turned the requirement's size and
alignment into strings to read them as ints; gvk_list_drop_last rebuilt the spare-record list to
drop its last entry; gvk_mem_id did the string round trip on every free. One slot is kept
(gvk_map_slot), int() truncates a long, the spare list pops. Every Text.to_int(string(x)) in
render3d is int(x) now.
Vk.heap_bytes() (vk_mac.ll: malloc_zone_statistics' size_in_use; 0 on Windows) and
examples/rendering/steady.ludic, in the suite: a buffer released and made again 5000 times and
600 whole frames gain 0 bytes each - the allocator before this, 1,120,000 over the 5000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Metal aborted a net.sh guest ("MTLPixelFormatBC7_RGBAUnorm is not color renderable"): the crash
report's main thread was in vkQueueSubmit from gvk_once_end, MoltenVK encoding a vkCmdBlitImage
through a render pipeline. water.ludic bound its reflection by sampler name and then asked the
BOUND texture for mips - on Vulkan the bound texture was not the reflection but the last one
bound, since 23.3 a BC7 kit texture. gpu_bind_sampler now makes its texture the bound one, as
OpenGL did (and water binds it explicitly); gvk_tex_mips and gvk_mips_now skip compressed images;
gvk_pass_begin leaves out a compressed colour attachment and says so.
tests/net.sh passes (main e515bd87 built against it; host and guest agree on 196 animals, the
people, the board and the lost hiker).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first generation pass's R32F height is kept as ter_height_tex (not copied into an RGBA32F),
so every reader of the height - placement, the read-back, physics, selftest16's 9 mm - sees the
same 32 bits. ternormal.frag writes the baked normal's x and z into ter_normal_tex (RG16F), and
the six places that read it (terrain.frag twice, tersun.frag, grass.vert, grass.mesh,
grass_cull.comp, which takes a third texture at binding 6) rebuild y. SPIR-V regenerated.
Maroon Lake's play, headless Vulkan: 2793 -> 2647 MB. The camp's frame: 0.066% of pixels differ by
more than 8 (mean 0.024/255), isolated grass blades at the slope gate. ludic-dev test 307/307.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The device's textureCompressionBC is asked for and remembered (gvk_has_bc). tex_load_ex prefers a
DX10 .dds with the full chain beside the .png (not for an edge-padded cut-out atlas):
texture_dds.ludic reads BC7 / BC5 / BC4, gpu_tex_compressed makes the image with every level and
no colour-attachment use (a compressed image is only sampled and copied into), and
gvk_tex_upload_blocks copies each level's blocks from one staging buffer. A colour map is BC7
sampled as sRGB, a data map BC7 read as it is. examples/rendering/bc.ludic holds it, in the suite;
ludic.lab's plate carries its .dds (its three shots render at 56-60 dB against the .png's).
ludic-dev test 307/307, no Vulkan SDK in the environment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
native/build.sh builds MoltenVK v1.4.2 from its pinned, checksummed tag (its dependencies at the
commits its ExternalRevisions pins), thinned to arm64, id @rpath/libMoltenVK.dylib, signed ad hoc;
its licence goes in native/LICENSE-MoltenVK. Every render3d program links it through its rpath -
the package's lib/ while developing, Contents/Frameworks in a bundle, where ludic bundle puts and
signs it - and vk_mac.ll also looks for @rpath/libMoltenVK.dylib (after an SDK loader, so the
validation layer still stacks in development). The suite's SDK stand-in (vk_env) is gone: the
render checks draw on the MoltenVK the package carries. gpu_is_gl() removed; nothing calls it.
ludic-dev test 306/306 with no Vulkan SDK in the environment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- model_release(model) frees each primitive's mesh, and each texture once no other model uses it:
the glTF texture cache counts the primitives using each texture (a path loaded again, and a LOD
chain borrowing its LOD0's material, each take a reference); the last one frees the texture and
forgets it along with any remembered material naming it, so a later load is a fresh one
- a scatter layer's instance and sort arrays start at 256 and double as layer_add or a stream
fills them, to the layer's cap, instead of the whole cap up front (0.4 GB in Maroon Lake)
- gvk_tex_read's copy struct comes from the scratch ring
- examples/rendering/release.ludic holds it (RELEASE OK on Vulkan and OpenGL), in the suite
smooth renders pixel-identical before and after (max |d| 0). ludic-dev test 306/306.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- every texture upload malloc'd a CPU copy of its pixels and never freed it (236 MB over the
valley's boot, and again for every model loaded later): the pixels are converted straight
into the mapped staging buffer
- the per-level and per-layer views gvk_view_of made outlived their texture, and on MoltenVK a
view keeps its Metal texture alive: a released texture takes its views with it, and the cache
is keyed by numbers instead of a string built on every call
- the Vulkan structs filled for a draw, pass, barrier, descriptor set, buffer, allocation or
upload (about sixty call sites) come from a reused 1 MB scratch ring (gvk_tmp)
- the descriptor-set cache and the retired buffers are emptied in place, not replaced; the grass
cull's dispatch arguments are made once
- macOS drains an autorelease pool each frame (Vk.frame_pool, lvk_frame_pool in vk_mac.ll)
- R3D_VK_PROF reports Vulkan objects made and destroyed by kind, and every cache's length
- examples/rendering/smooth presents through render3d, so it runs on Vulkan too
The full valley on headless Vulkan loads to 2.18 GB and holds (it passed 8 GB while loading
before). ludic-dev test 305/305, selfhost-test 33/33.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every per-entity store (@S_ components, @H_ flags, alive, kind, freelist, owners) is a heap block
L_grow doubles from 1024 as L_alloc hands out a slot past it, the new slots zeroed; each site loads
the store's base where it indexes it (ecs_base, its registers %ecsb* so a raw function's t0 labels
cannot collide). Prop.has bounds against @L_cap, Pool.capacity answers it, a mod's registered
stores grow with the rest, every main grows the stores once before anything reads them. A snapshot
records its slot count first and a load grows to it before reading back. The overflow stop of
1c7ce84 is gone with the wall. ludic-dev test 305 passed, selfhost-test 33 passed; 1000 / 5000 /
100000 entities spawn and count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L_alloc handed out a fresh slot without a bound, so the 1025th spawn wrote past the end of every
component array. It stops with a located message naming the store's size and where many things
belong (ludic.base's Table). Growable stores are plan 24.8: the save, rollback snapshot and mod
table write the stores whole at a compile-time size, so that is a file-format change. Reseeded;
ludic-dev test 305 passed, selfhost-test 33 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every pool thread runs the worker at once with the same states, so a mut state in a worker was a
race nothing reported. check_worker_ref refuses a worker whose leading states include a mut one;
threads.ludic's total moves into the words the worker is handed, under the mutex. The seeds are
regenerated (ludic-dev reseed). ludic-dev test 305 passed, selfhost-test 33 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked on both machines with a bundled Jolt probe: the licence and the library in place, the app
signed (Mac) and the probe's ball landing at the same height on both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- a CAMetalLayer on the view (cocoa.ll win_metal_layer), VK_EXT_metal_surface, QuartzCore linked
with Vk.*; the drawable measured after the layer sets the backing scale
- vk_mac.ll opens MoltenVK directly after any loader: a bundle ships only libMoltenVK.dylib
- a covered window is not presented to (win_visible); one frame in flight on macOS
(R3D_VK_INFLIGHT), with images, buffers and descriptor pools held until it is done
- win_held / win_mouse / win_pad / win_touch / win_text / win_present pass slice elements (arg_buf):
every windowed program died on its first input poll
- variants.list and SPIR-V for the three NEAR_FADE foliage programs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Our own shim (native/shim/jph_shim.cpp) over Jolt built from the pinned tag with cross-platform
determinism and no fp contraction: shapes as handles (box, sphere, capsule, cylinder, dome,
offset, heightfield, mesh), still/kinematic/dynamic bodies by id with real removal, rays,
top_at and push (what CharacterGround asks), overlaps, buoyancy against the PhysWater port,
contacts recorded in C and drained as PHYS_HIT / PHYS_SPLASH facts. Fixed 1/60 steps, at most four
a frame. Nine tests against the real library, including a pile run twice to the bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
native "<target>" "<path>" in a package's package.ludic; the compiler records the libraries of
every package a program imports and writes them into the IR (; ludic-native:), so ludicc -o,
ludic build, ludic test and ludic bundle all link one list. macOS: an rpath to the package and to
Contents/Frameworks, where ludic bundle copies and signs each library and drops the build
machine's rpath. Windows: the import library, the .dll copied beside the exe (--natives-out for
the bundle). tools/native/lib.sh builds from a pinned, checksummed source with clang on both
machines; ludic.nativeecho is the worked example; the shim rules are in packages/README.md.
Linked at build time rather than dlopen (docs/PACKAGES.md says why). Reseeded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reach through Port.member() follows that member's binding (or its default), and Registry[i].field -
or a local holding Registry[i] - follows that field in each entry, so a question asked of a port or a
table that also holds verbs no longer reaches the verbs. In Maroon Lake that took the valley's
'what is this Thing called' from 47 states it could change to 1. examples/state/write_reach.ludic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What only becomes known inside the drain - a value another reducer just set, the map in play - no longer
has to be faked into the action, so a verb that reads several systems while it changes one is a reducer
instead of an act handed its states. A second state to write is still refused, and the message says the
way out. examples/actions/reads.ludic; reseeded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ludic.ui runs a frame's presses after drawing it, and what they dispatched waited for the end of the
phase - after the host had presented - so a button's change showed a frame late. Decided: drain, not a
phase per action. ui_show and ui_press drain the queue once the presses have run, so the code after
them and the frame presented next see the change; a host that runs presses some other way calls
drain_actions() before it presents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game's exported UiAct collided with ludic.ui's, which nothing outside ludic.ui uses: it is private
to ludic.ui now, and a private record of one spelling in two modules never clashed. A real clash - a
type named like one a package exports - is still refused, and the message names the package and the
way out (`ludic_ui exports it, and exported names are one namespace - rename this one, or declare it
without export inside a module of your own`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A step list or a registry of fn values takes no state and still reaches every state its steps take.
The compiler now writes `reach <n> <function>` - every state a function can come to by a call, a
`fn f` it writes or a global holding fn values it reads, to a fixed point - and ludic deps reports
widest_reach beside widest_function, with how many of those states the function does not take
(Maroon Lake: app_boot, 72, all 72 through fn values). --widest N lists the N functions that take the
most states with what each reaches; --reach N orders them by reach. A baseline without widest_reach
does not hold it until rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`function kind_of(f: CharFact)` for a CharacterFact was taken on trust and failed in the code writer
as "member access on non-aggregate". A capitalised type - plain, in a slice, or a generic's argument -
must name a declared property, record, state, event, enum, action or packed value type, or a type
parameter of its declaration: `kind_of's parameter f: there is no type CharFact`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Maroon Lake the prune gave home_keep_records(base_app_st, home_st, r: RunRecords, save_app_st) a
second save_app_st at the front, and every call a second argument: a state declared after a plain
parameter was not counted as declared. It is now, and a call to such a function is never given the
state again. `ludic build --check` let the duplicate through and clang refused it; the checker now
refuses a function that names two parameters alike (`add names two parameters n`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ludic.base's Queue<T> carries a QueueTag (its name and pending count): queue_new(name), q_push(q, v),
q_drain(q), q_clear(q) take no BaseState, and core_undrained(tags) names the given queues still holding
facts. A reducer on a package's state can now call that package's verbs (wallet_earn(wallet_st, n)).
ludic migrate state --prune (ludicc --migrate-prune) takes out each state parameter a function no
longer uses, nor anything it calls, and the argument that fills it - including an argument for a
parameter the callee has dropped, which is taken out before the call is checked, so a generic's T is
told by the argument that says it. A reducer keeps its state. --dry-run now counts the edits it would
make. Every package was moved with it: 608 base_st parameters and their arguments, 1889 edits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the function that calls every reducer (and the action queue) is written in the program's own
file: in the first action's file it belonged to that module, depended on every module with a
reducer, and joined a game's modules into one 69-module cycle (examples/actions/modules and a
ludic deps case hold it); the state instances, the queue and the reducers make no deps edges
- ludicc --check / ludic build --check lower the program too and write nothing, so the code
writer's refusals are in it: a bind to a function that is gone, an unknown name (and the checker
now refuses fn <missing> itself); rejects bind_missing_fn, unknown_name, registry_count_key
- def R count is refused: its constant would be PREFIX_COUNT, the registry's size
- a file's module, package, trust and numbers-float are tables, and from the check on the lookups
of functions, enums, records, globals and externs are too (tagged enums kept as a list): Maroon
Lake's check-only build went from about 20 s to 7 s including lowering, its IR from about 2
minutes to under 10 s; duplicate declarations are found by table, not a pair of loops
- threads.ludic's pool check gives each call a little work, so a busy machine cannot run them all
on the caller before a worker wakes (it failed one run in three under load)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It lowered to an i32 compare of two pointers, which the IR refused. Two strings with the same text
are equal now, a null only to a null, and a failure says expect_eq failed (got "camp", want
"lake"). examples/library/testing_strings.ludic (two tests fail on purpose, and the output is
checked); ludic.base's actions_test uses it again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
action Name { fields } is a typed record; reducer State on Action(s: mut State, a: Action) { ... }
in the module that owns the state takes exactly that state and the action (a second state is
refused); dispatch Action { fields } queues one from anywhere, the queue supplied by the runtime.
The queue is drained at the end of every phase of the frame loop, after every phase of ludic.base's
core_tick_all, and by drain_actions(): in dispatch order, each action's reducers in the order of
their states' names, an action a reducer dispatches queued behind, a queue still growing after 64
rounds stopped with the action named. Examples actions/pack, phases, runaway; rejects for a second
state, a reducer on a non-action and an unknown dispatch; ludic.base's actions_test; LANGUAGE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- component Name (a: mut A, b: B) { ... }: every getter, default, function and event takes the
header's states before the instance; a member's call to another passes them on; the glue is
supplied them; the template never sees them; a read-only one is read-only in every member
- ludic migrate state: a component's members' needs go into its header (added to an existing one,
mut added where now changed); a field read or a member call inside a component is the compiler's,
so nothing is written inside a name and no ', )' is left; an entry point that declares states
already gets the rest after them
- a program's module named like a package gets <Name>AppState; a program's own file its own state;
a friend module's files go by directory; a package's settable var stays state
- it writes only under the programs and directories given (and runtime/ with --runtime), and
refuses the whole run naming any other file that would have to change
- a name a package already moved into its state is rewritten through it; a read of the runtime's
var through the runtime function that answers it (gl_w: gl_width())
- a state's instance supplied by the runtime is not a uses reference
- tests: state/component, rejected/state_component_ro, rendering/ui_render3d, migrate component
and foreign cases
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local bound straight from another module's global (let t =
thing_cur), or from such a local, is followed within its function, and
a write through its field or element is listed as a warning after the
counted writes. A reference from a function's result is not followed.
Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parse, the types and the module rules (export, uses, layers, ports,
registries) run and nothing is emitted or linked: about three seconds
on Maroon Lake. In this mode the checker asks vis_check at each
reference it resolves, with a global's initializer and a registry's
entries seen from the files the emitter would use. Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bind Purse { money: g_money } writes the getter bind_Purse_money in the
bind's own file, so a one-line wrapper per member is not needed; a
member that takes something is refused a variable. Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The modules of one layer use each other freely and may go round;
anything outside the layer is held to the module's uses, and a layered
module with no uses reaches nothing outside it. The cycle check walks
the graph with each layer as one node, so a cycle leaving a layer is
refused. ludic deps shows the layers and counts the largest cycle
without their own edges. Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every file is compiled, linked and every test block run as up to N jobs
at once (xargs -P; default the CPU count), each test with a TMPDIR of
its own, and the report is read back in file order. Windows keeps the
sequential path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With LUDIC_DEPS=<file> the compiler records every reference the
visibility pass resolves (from module, to module) and every assignment
to another module's global. ludic deps prints the modules,
dependencies, largest cycle, cross writes and globals written from
outside, with --graph, --dot, --writes, --uses MOD, --check FILE and
--baseline FILE. Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A property or event a module does not export no longer collides with
another module's of the same spelling: each private one is renamed for
its module, with the types, new, emit and @On written in that module.
Exported ones stay one namespace; two events of one spelling are now
refused. Generic records, entity components and component or view
records stay global. Reseed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>