Commit graph

129 commits

Author SHA1 Message Date
5d0f83b81c render3d: 671 frame-reachable allocation sites to 161 - declared, split, or made nothing
@alloc_ok with its reason on what is made once per resource and kept (textures, programs, samplers,
views, layouts, memory blocks, the pipeline cache in gvk_pipe_build), on resize and swapchain
remakes, on screenshots and dumps, on a world being set up (streams, layers, water, post, bakes), on
loads (gltf_load, skin_load, tex_load*, png_decode, fonts) and on R3D_PROF / drawstats.
gltf_cached's hit path is its own and makes nothing; the miss (gltf_cached_load) is declared.
m4_look_at (now over m4_look_at_xyz) and m4_inverse work on scalars, and cam_update makes no scratch.
Left: lazy first-use starts, error and debug prints, and scratch made and freed each call (churn,
plan 25.3). Compiled (steady, and ludic deps over the game).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:58:18 +03:00
03a8ee443d render3d: the pipeline cache grows in gvk_pipe_build alone
gvk_pipeline_fast's miss (the build and the six cache pushes) is its own function, so the fence can
declare it: @alloc_ok("pipeline cache: one per variant the game draws") once lang/memory-fence's
compiler is merged (the annotation is not known on this base). Compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:51:06 +03:00
df6ea046c3 render3d: VkAllocationCallbacks counted (R3D_ALLOC_VK), pipeline create infos freed, actor pool at init
Plan 25.1c: vk_mac.ll's @lvk_ac (posix_memalign under a 16-byte header of scope/offset/size, atomic
counters) passed at every render3d create/destroy (49 sites; the caps probe keeps its own null pair);
lvk_ac_bytes/_peak/_allocs/_scope_bytes for the fence, Vk.alloc_bytes. vk_win.ll: null and 0.
MoltenVK 1.4.2 counted 0 live bytes through them in steady. Fence findings: gvk_pipeline freed its 17
create infos (and reuses one bufs list); actor_init fills ac_spare with 512 records (actor_fresh,
m4_new, v3_new at first placement in play). Compiled, not run (the user's call).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:46:19 +03:00
c736fd3b09 render3d: buffer primes in a command buffer of their own, submitted at once
A co-op guest loading its models segfaulted in copyBufferToBuffer under vkQueueSubmit (0x68, AGX
LegacyBlitContext): a buffer primed into the frame's command buffer was released later in that
frame, and gvk_buf_release destroys a buffer no draw has marked, so the copy read a freed one. The
copies now run in their own command buffer before the frame's begins, checked against the slot's
handle as they are recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:20:00 +03:00
0141f99dd1 Os.platform/arch uname once; render3d primes a new buffer's Metal buffer
lp_os_platform and lp_os_arch malloc'd 8 KB per call (the uname buffer) and kept none of it:
string_temps now asks both every round, 327 MB over 20,000 before, 0 after. Reseeded. render3d:
MoltenVK made a mapped buffer's MTLBuffer at its first bind (fn_gvk_draw +4 blocks in the boat
window); gvk_buf_reserve queues it and the next frame's command buffer copies 4 bytes out of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:34:45 +03:00
7e9fa4473c render3d: a stream's cache is made with the stream - records and an arena - and evicts in place
stream_update made a Chunk and a words copy per new chunk (fn_stream_update +39 blocks / 13.2 KB a
window with the hiker in the drifting boat). The pool holds STREAM_MAX_CHUNKS records; the arena
is twice the layer's cap; eviction compacts it; a chunk that cannot be kept is gathered from the
scratch. stream_clear_all frees records, lists and streams. steady: 300 new cells, cap 256: 156 KB
before, 0 / -4.9 KB after, and every kept chunk's data checked against its cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:26:59 +03:00
b4774a46ee render3d: turn MoltenVK's command pooling off; steady ramps the draw count
The pools kept every command object ever recorded, so the heap grew each time a frame drew more than
any before (fn_gvk_draw under vkCmdBindVertexBuffers/BindIndexBuffer/Draw in the leakcheck; ~650 B a
draw). Off: 3.7 ms a frame either way over 520 actors. steady: 20 to 200 actors grows 5-7 KB with it
off and 120 KB with it on (bound 16 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:57:08 +03:00
1adce5b58b render3d: free a texture's create infos and the .dds path it looked for; steady's model at 0
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>
2026-09-28 12:25:02 +03:00
db3fa68052 Merge branch 'r3d/recycle' into lang/uifree 2026-09-28 12:01:33 +03:00
9b482d609a render3d: recycle texture and buffer ids, free a released model whole, actor_release
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>
2026-09-28 12:00:53 +03:00
08a6fc8a3f Merge branch 'lang/chunks' into lang/uifree - the ground's heights by chunk and its Jolt ground per chunk; both new render checks kept (steady, chunks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:55:55 +03:00
2fe4018d28 Merge branch 'r3d/zero-leak' into lang/uifree 2026-09-28 11:40:43 +03:00
de85f201ad render3d: gltf_cached - a still model loaded once by dir, file and node
A game placing props while it plays (a sign an animal leaves, a fish, a bobber) loaded each through
gltf_load, which reads the file and parses its document every time. gltf_cached finds the same
(dir, file, node) by comparing the names in place and hands back the model the first load made;
a model whose document is read after the load (ludic.anim's clips) still goes through gltf_load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:39:41 +03:00
a465984c52 fix(render3d, runtime): a glTF document is freed whole, layouts found by number, block records reused
- Json.free_all (value_free_all): a parsed tree's nodes, lists and strings. render3d frees each
  glTF document that way at the next load; the names kept out of it are copies (a primitive's
  material, a skin's joints, an animation clip's name in ludic.anim) - a model's strings were
  ~640 KB left behind per load
- jp_number made a digits list per decimal in a document and never freed it
- gvk_layout_id matches a mesh's layout as numbers in a scratch made once, against the layouts
  known end to end; a new mesh no longer builds a key string
- gvk_mem_new puts a new block into the record of one given back rather than appending, so a
  buffer made again every few frames no longer grows the block lists

steady.ludic adds a glTF parsed and freed whole 200 times: 0 bytes (38,400 before the digits fix),
beside the buffer path and the frame, still 0. A model loaded and let go still keeps ~2 KB a round
(texture and buffer handles are not reused yet); it is bounded at 8 KB a round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:36:42 +03:00
ff658633fb feat(render3d): terrain_chunk_heights - the ground's B-spline heights a chunk at a time (23.5)
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>
2026-09-28 11:25:21 +03:00
866037a0d7 fix(render3d): nothing allocated in the steady state - the Vulkan allocator reuses its records
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>
2026-09-28 11:14:56 +03:00
3915af34c2 Merge branch 'r3d/bc-target-fix' into lang/uifree 2026-09-28 10:43:44 +03:00
1447304dd7 fix(render3d): nothing draws into a BC7 texture - sampler binding binds, and compressed images get no mips
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>
2026-09-28 10:34:23 +03:00
1675eb451f perf(render3d): the terrain's height and normal in R32F + RG16F, 128 MB where one RGBA32F was 256
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>
2026-09-28 02:31:18 +03:00
8cc4bdf66b feat(render3d): 23.3 - a .dds beside a .png is uploaded BC-compressed with its whole mip chain
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>
2026-09-28 01:23:45 +03:00
9425bdc4e5 feat(render3d): 22.10 - ludic.render3d carries MoltenVK, so a Mac program has its Vulkan driver
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>
2026-09-28 01:16:26 +03:00
6ee496ab1c fix(render3d): three leaks in Vulkan play - sampler keys, mip blits, a program per actor
- gvk_sampler built a key string on every call it was reached (a texture read two ways in turn
  misses its per-texture cache each time); samplers are found by their seven numbers instead
- gvk_tex_mips_into allocated a VkImageBlit per level, every frame (the exposure measure's
  chain); it comes from the scratch ring, as does gvk_tex_grow_mips's copy
- gvk_program_new made a new program - modules, pipelines - on every call, and every actor asks
  for one; a variant is now made once and shared (a program is immutable), and one that cannot
  be made stays 0 for every later asker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:30:11 +03:00
9a4137e9da wip(render3d): plan 22.14 - the OpenGL backend removed (suite 306/306, not yet handed over)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:30:11 +03:00
edaec666d1 Merge branch 'r3d/layer-hotfix' into lang/foundations 2026-09-27 23:57:19 +03:00
d8fa2ce6b0 fix(render3d): a scatter layer allocates its whole capacity up front again
e634177 started a layer at 256 instances and grew it in layer_add and the stream gather, but a
game writes l.inst directly (Maroon Lake's track prints, trees and rocks), past what had been
grown: "index out of range: 2048, len 2048" in play. layer_new takes its cap up front as before;
layer_reserve(l, n) is exported for a caller that writes l.inst itself once layers start small
again, opt-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:56:07 +03:00
bcdcc8fe8c Merge commit '5d9cbdd' into lang/ecs 2026-09-27 23:55:07 +03:00
231715bf98 fix(leaks): what play allocated every frame and never freed - a shadow uniform's name, the Tick, a ridden vehicle's moves
Found with malloc_history over 1200 frames of play without the interface (the first Ludic function
on each allocating stack, live bytes at two marks): sh_loc built `name + "[0]"` per program per pass
per frame - 24.6 MB of 33 in the window; its callers pass both names as literals now. tick_set fills
the frame loop's one Tick instead of tick_new making one a frame. A ridden boat or horse said
VEHICLE_MOVED as a new fact every frame; the metres are added up in VehiclesState and taken once
(vehicle_moved_take / _kind / _x / _z). Play's growth without the interface: 92 -> 14.7 MB a minute
(maroon-lake tests/leakcheck.sh); what is left is phys_push (Physics' fix on lang/nav) and the HUD's
strings. Packages 407, ludic-dev test 305 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:51:37 +03:00
e634177ca4 feat(render3d): 23.4 - model_release, shared textures counted, and scatter layers sized to what they hold
- 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>
2026-09-27 23:34:14 +03:00
70a92a8862 fix(render3d): a refused pipeline is not retried every draw, and an evicted chunk is freed whole
gvk_pipeline_fast cached only pipelines it made, so one the driver refused was attempted again on
every draw, each attempt building its key string and create structs anew; the refusal is cached
too (a Vulkan program's variants are fixed when it is made, so it would fail the same way).
stream_evict made a new chunk list per eviction and never freed the evicted chunks' records; it
compacts the list in place and frees each evicted chunk with its data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:34:13 +03:00
6dd1810038 fix(render3d): a map generated over another releases what it replaces
terrain_generate, terrain_use_dem and terrain_use_ortho each overwrote the previous height
texture (256 MB of RGBA32F at 4096^2), the heights read back (64 MB), the DEM texture and the
orthophoto's texture and pixels. terrain_reload unloads first, so the world swap was already
clean; any other caller building a map over a live one now lets the old go, and the read-back
array is reused, being the same size for every map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:34:13 +03:00
cfb7fa5ca9 Merge branch 'lang/foundations' into lang/ecs 2026-09-27 23:08:24 +03:00
c97e64c3c0 perf(render3d): an actor knows its row - removal swaps the last one in (O(1)) instead of building a new list of every other actor, never freed; actor_keep puts one back after a world swap; actor_clear_all clears in place
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:08:02 +03:00
80764f85c1 fix(render3d): the Vulkan renderer stops leaking as it loads and draws
- 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>
2026-09-27 23:03:38 +03:00
9539f7bcec perf(json): 23.2 - a parsed tree can be let go (Value.free / Json.free: its nodes and lists, not its strings), a string is parsed in one allocation instead of one per character, the parser's cursor is freed, and gltf_load frees the last file's tree and the text it parsed
300 parses of a 446 KB glTF: 2118 MB before, 528 MB with the one-allocation string, 94 MB freed.
A headless Maroon Lake at play: 2334 -> 2195 MB by footprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:17:27 +03:00
cc384efee0 feat(render3d): the meadow's blades are culled on the GPU - under 1.5 ms of a MoltenVK frame for all the grass
- grass_cull.comp decides each candidate blade once a frame (place, ground, density, water,
  slope, frustum, colour field) and writes survivors into three bands by distance, one indirect
  draw each; grass_inst.vert only bends and places the vertices. OpenGL keeps grass.vert's
  per-vertex path; R3D_GRASS_GPU=0 compares
- compute programs take sampled textures after their buffers (gpu_compute_tex / gpu_dispatch_tex),
  and every dispatch now records a compute-to-draw memory barrier
- the blades as a sward: 4 m cells, bands with five, three and one-quad blades, spacing doubling
  every 18 m to 70 m, never narrower than a pixel; lit facing the sun and leaning to the sky,
  shadow looked up above the ground (it read the terrain as its caster), a colour ramp that
  leaves only the sheath dark, clumps, dry patches and a tussock shade worked out per blade
- scatter layers flagged grass are skipped while the blades draw (and under R3D_NOGRASS);
  R3D_BLADES, R3D_NOBLADES win over the game's setting; R3D_GRASS_S0/D0 for measuring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:47:36 +03:00
f815f3e3cf wip(render3d): one frame recording while one draws on Vulkan (double-buffered ring and pools), R3D_VK_LABELS, grass measuring switches (R3D_NOGRASS, R3D_BLADES, R3D_GRASS_S0/D0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:47:36 +03:00
b247603b7e merge lang/foundations into lang/native-jolt (seeds regenerated)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:20:44 +03:00
38d4ea72b1 feat(render3d): Vulkan in a macOS window through MoltenVK; window built-ins take a slice's elements; the NEAR_FADE SPIR-V
- 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>
2026-09-27 00:52:45 +03:00
e53f3197ac refactor(render3d): phase 16 - the collider store is gone; still things are bodies in ludic.physics
collide.ludic (circles in a cell grid that could not be removed) and its Render3dState fields are
deleted, and the reload example stops asserting colliders. A game still calling col_* must move to
ludic.physics first (Maroon Lake's src/solid).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:08:36 +03:00
3a87d02696 packages: ludic.ui's render3d backend threads Render3dState; render3d's settable vars (r3d_dem_path, post_*, grass_*, wt_wade_*, ...) are Render3dState's fields again, not lets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:11:15 +03:00
7b17b4a1b9 wip(0.S3): outside the runtime, its drawable size, scale, screen and tile size are read and set through its functions; a migration reports such a reference instead of threading the runtime's state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:47:21 +03:00
19fcf60599 wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run
ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic
  1804 vars into 126 states, 64 into lets; 23498 edits in 460 files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:31:34 +03:00
5ffe50ed02 wip(0.S2): migrate - a var nothing writes becomes a let; a state is keyed by its module, directory or program, so every program's plan agrees; paths normalized; program states named SceneDemoState / scene_demo_st; the LSP reads state, mut and entry/handler parameters
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:31:34 +03:00
07505e7ef2 wip(0.S3): the packages migrated by ludic migrate state packages - every package test green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:02:21 +03:00
b401f7acc1 feat(ludic.lab): the visual lab as a package - a scene on a plate, never a world
A scene is an entry in the open registry LabScenes, shown on a plate: a
flat lit disc, the package's own small sky with the sun at one hour, and
a frame clock. Headless each camera (lab_shot, lab_shot_at) settles and
is written as build/lab/<scene>/<shot>.png (a stored PNG, written here);
in a window N and P step through them. tools/lab/run.sh and sheet.py
make the contact sheet; tools/lab/plate_build.py makes plate/.
ludic.render3d gains r3d_plate_mode (no terrain, grass or water is made)
and r3d_sky_path. quit() in a program with no frame loop links. The
example takes ~120 MB resident, 417 MiB peak footprint. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:48:43 +03:00
1dad96102a feat(ui): ludic.ui is a UI framework - input, controls, render3d backend, the CSS a game needs
A game no longer writes a host:
- The runtime reads Input itself: focus and keyboard navigation (Tab, arrows, Enter/Space,
  autofocus), the pointer (hover, :active, click on release, drag), and scroll boxes with the
  wheel, a draggable scrollbar, clipping and scroll-into-view.
- HTML's controls are built in (button, checkbox, radio, range, select, text, key capture), made
  of plain parts a stylesheet styles, each reporting with on-change and event.value.
- ludic.ui/render3d.ludic draws with render3d's overlay: textures, named atlases (icon:NAME),
  nine-slice border-image, rounded rects and rings, clipping, and a scale.
- Hooks for the program's language, sounds and clock (ui_translator, ui_sounds, ui_clock).
- ui_dev: hot reload, errors on screen, LUDIC_UI_DUMP.
- CSS:
  - colours as #rgb / #rrggbbaa / rgb() / rgba() / names;
  - border-radius and outline (following the radius), box-shadow, background-image and a tinted
    border-image;
  - group opacity, @keyframes / animation, transition;
  - :focus, :focus-visible and :focus-within.
- HTML mixed content, and boolean attributes.
- render3d gains tex_width / tex_height, and the XML reader keeps text runs in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:26:09 +03:00
b0b0b62bce feat(lang): L7 memory is safe unless it says unsafe
The typed buffers are slices: words/floats/fixeds/doubles/pointers(n) make
zeroed, bounds-checked []int/[]float/... and the type names mean them. buffer(n)
is a []byte, with text_of, Fs.read_bytes/write_bytes and view(xs, start, n).
bytes(), indexing a raw pointer or bytes, free, resize, Memory.*, raw file calls,
data_of and C externs are refused outside unsafe { } / unsafe function, and a
project's own files may write unsafe only with --unsafe; the runtime and packages
are the platform. A slice passed to an extern goes as its data.

What the change found: Sync's atomics on a slice header, words(n) uninitialised,
input's fixed axes in ints, truetype's fixed outlines as ints, skin matrices
typed int, gl_shader's source table made from raw bytes. render3d gets safe
entry points (safe_api.ludic). Rendering is byte-identical; a frame costs the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:53:27 +03:00
57b66bdf47 feat(lang): L4 type checker between parse and emit
selfhost/check/ walks every function, the entry, tests, globals' initializers and
@On listeners with real scopes, and refuses mixed number kinds, text and numbers,
two record types, mismatched slices and fn types, wrong argument counts, wrong
returns and wrong push elements - every mix-up at once, each at its line.
LUDIC_CHECK_REPORT=1 lists them by category. pointer stays untyped (L7's).

What it found is fixed: render3d's HDR scan calling the float-bits extern f_lt
with floats; ludic.shooter's right-stick aim overflowing past half a push;
prof.ludic storing longs in []int; extern arguments now coerced to their
parameters. Text-returning runtime functions say string; Assets.ready says bool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 01:34:49 +03:00
0c73287e35 feat(lang): functions are values (L2), and render3d takes its scene as callbacks
fn(int, float) -> bool is a type, fn name is any top-level function's value, and a call through
a local, a global, a record field, a slice element, a parameter or a result of a function type
is an indirect call; two function types mix only when equal, a call checks its argument count,
and a value may be null (examples/functions/values.ludic). Job.parallel_for keeps its worker
check.

render3d's scene is registered rather than required by name: r3d_on_draw, r3d_on_casters and
r3d_on_stream_fill (hooks.ludic). The two rendering examples register theirs - and had defined
scene_draw_casters with no parameter while the renderer passed one, which nothing checked.
render3d declares numbers float itself; smooth.ludic is converted to floats and returns when
r3d_init fails instead of running on into a segfault. Noise.* check their argument count (a call
one short crashed the compiler). selfhost-build says why it failed. The migration tool reads a
declared float as evidence. Seed regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 00:27:26 +03:00
cc89fc37a4 feat(lang): strict numbers in float files; render3d on float
A numbers float file adapts decimal literals to a fixed operand or slot, and refuses to
promote a computed int to a float implicitly: there it is almost always float bits. Explicit
float(x) is always allowed.

render3d's numbers are float, converted by tools/migrate/floatbits.py - a whole-program
inference of which ints carried IEEE bits (union-find over flows, calls, returns, buffers,
nested buffers and lexical scopes) and a rewriter to operators, Math.* and float literals,
with float_bits / float_from_bits left only where bits really cross (runtime scratch
buffers, mixed buffers). Seed regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 17:13:25 +03:00