Commit graph

333 commits

Author SHA1 Message Date
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
e6729b3123 Merge branch 'lang/ozz' into lang/foundations 2026-09-28 12:53:14 +03:00
d6b3242b25 ludic.base: an IntIndex slot's list made with room for 16 rows, so filing in play does not grow it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:45:40 +03:00
8a4f3f5ad6 ludic.things: a fact's record kept, in two halves swapped when a fact finds the queue empty
th_fact made a ThingFact per placement, removal and use, and the valley places and removes Things
all day. The records come back two drains after they were handed out, so a reader placing a Thing
while it reads the last drain's list never sees one change (the test holds exactly that).
thing_use takes ThingsState mut, since it pushes a fact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:45:40 +03:00
9a56ee9b9d feat(ludic.anim): ozz-animation 0.17.0 underneath - a skin's skeleton and each clip built at load, sampled to 1e-4 of anim_mix (phase 19.1)
No bake and no new file: the skeleton comes from the skin's parents and rest pose (its own joints
and their ancestors - a kit holds several rigs), a clip from anim_read_doc's channels. Built by
native/build.sh from the pinned release; the Windows DLL imports KERNEL32 alone and passes there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:44:49 +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
93e6954fb1 ludic.things: things_spare_warm - spare records made up front, so play takes them from the first placement
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:09:52 +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
be06477907 Merge branch 'lang/ecs' into lang/uifree 2026-09-28 11:57:31 +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
65b51c251e ludic.things: thing_slot - a placed Thing's reusable slot in the table
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:54:21 +03:00
1af62d0879 ludic.things: a removed Thing's record placed again, oldest first past a lag of 32; things_reserve sizes the grid once
thing_put and thing_spawn made a new record for every Thing placed, and the world places and removes
them all day (an animal's sign and bed, a drop, a fish), so play grew by a record per placement.
Removed records wait in a queue compacted in place; once 32 wait, the oldest is set back as new
with a new uid. ludic.base's grid_reserve makes the buckets for n rows now, since a rehash in play
leaves the old bucket array behind. Tests: a record comes back only past the lag, as new, with a
new uid, the indexes agree, and the spare queue stays bounded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:52:23 +03:00
5d71892b7c ludic.ui: every bar percentage (0% .. 100% to a tenth) made once when the memo starts, so a bar moving never makes its text in play
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:46:47 +03:00
0c3c0c5e6a feat(ludic.physics): phys_shape_free - a shape let go hands its id to the next, so a ground by chunk grows nothing (23.5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:43:40 +03:00
2fe4018d28 Merge branch 'r3d/zero-leak' into lang/uifree 2026-09-28 11:40:43 +03:00
96423634ad Merge branch 'r3d/chunk-heights' into lang/chunks 2026-09-28 11:39:50 +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
0068411946 ludic.ui: a float in a text's hole written to one decimal from kept texts (its whole part from mm_int, its tenth a literal) - a bar's width moving every frame made a new string every frame, and read 7.470598e-07%
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:29:11 +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
8fbd7c7e60 Merge branch 'lang/ecs' into lang/uifree 2026-09-28 11:22:25 +03:00
4c4d225c03 Merge branch 'lang/chunks' into lang/uifree 2026-09-28 11:21:22 +03:00
0ec39f8e4e Merge branch 'r3d/zero-leak' into lang/uifree 2026-09-28 11:21:22 +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
50f255eea4 ludic.ui, runtime: the last per-frame allocations in the interface - the top popover found without a list, the popover draw list and the clip stack kept, a select's options in a list the node keeps (and matched to its value without writing the index), a number's text kept on its Value until it changes (value_num_set / Value.num_text), a key's label kept per key code until the layout changes it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:11:48 +03:00
e807fd556c ludic.nav (23.5b): a baked mesh resident by chunk
nav_load_index indexes a saved mesh's tiles (cell, offset, size, ref) from their headers and keeps the file open, with no tile in. nav_tiles_keep reads in the tiles within rin of any player and removes those past rout of all; the file is read through Ludic's pack-aware file_open. The file format is unchanged. tiles_test: tiles come and go with the point, a path works across the seams once they are in, and a reset closes all. Measured on the baked maps (the 900 / 1100 m ring round the start): Maroon 617 of 1807 tiles in, 8.9 MB instead of about 27 MB for a person and 5.4 MB of about 14 for a large walker; Lamar 391 of 1147. Both libraries are rebuilt; nav 17/17 on the Mac and the PC, DLL KERNEL32 only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:07:27 +03:00
a26cc2eea7 ludic.base: StrBuf and StrTable - a line written into a kept buffer and interned, one string per distinct text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:04:07 +03:00
3915af34c2 Merge branch 'r3d/bc-target-fix' into lang/uifree 2026-09-28 10:43:44 +03:00
706c7abc55 ludic.ui: no number made text to be read back - a style handler returns before any text for a key not its own, grow/alpha/animations/scroll read numbers as numbers, el_secs split once per text, a tooltip translated when its title changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
5f2900ffda Merge branch 'lang/trail' into lang/uifree 2026-09-28 10:22:36 +03:00
0be4230487 Merge branch 'r3d/height-format' into lang/uifree 2026-09-28 10:12:48 +03:00
f294484c02 Merge branch 'lang/ecs' into lang/uifree 2026-09-28 10:12:48 +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
7b864a3201 Merge branch 'lang/foundations' into lang/nav 2026-09-28 02:03:09 +03:00
fffedf71d0 ludic.physics, ludic.character, ludic.vehicles: fact records reused, and phys_sink allocates nothing
The per-frame sweep over physics, character, vehicles and nav found three fact makers that made a record per fact: contacts and splashes, the character's and the vehicles'. Each keeps a pool now, as wildlife's and npc's do (q_unheld). phys_sink, asked on every removal, built two new lists each time; it filters into a kept spare pair and swaps. ludic.nav's paths and crowds already allocate nothing, and none of the four builds a string per call. What remains at load, reset or a world swap is not per frame. vehicle_feed takes its state mut. vehicles_test: 1000 fed facts, drained turn by turn, use at most 4 records. All packages 421.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:03:09 +03:00
e8a34255ac Merge commit 'f104995' into lang/ecs 2026-09-28 02:02:11 +03:00
93bc8a90db compiler(0.R): a dispatched action's record is the queue's to keep - one list per action, filled in place, all free again when the drain ends
dispatch lowered to ludic_act_push(k, new A { ... }): a fresh record every dispatch, and an input
system dispatches Move and FrameTime every frame. The queue now keeps a list per action
(kept<k>, used<k>); ludic_act_new__A hands out the next (made only when all are queued), new's
emitter fills it field by field as it fills a fresh one (every field, default or given), and
drain_actions sets every used<k> back to 0 once the queue is empty. A reducer only ever reads
its action during the drain, so nothing sees a record after it is reused. Actions are visible
to the program's file, where the queue lives, as reducers already were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:02:11 +03:00
fe2d2bacd7 ludic.base, ludic.wildlife, ludic.npc: fact records reused, not made per fact
wl_fact made a new WildFact per fact, about one a frame, and Ludic never gives one back. ludic.base's q_unheld(q, pool, out) lists the records a queue no longer holds: neither waiting nor handed out by its last drain, which is good until the next drain. ludic.wildlife and ludic.npc now keep a pool, hand out a free record, and make a new one only when every record is held. Taking a record writes the state, so wl_fact / np_fact and the few callers holding their state read-only take it mut. Tests: q_unheld's rules (an empty drain holds on, the safe side); 3000 prints drained frame by frame use at most 4 records; 500 arrivals drained turn by turn use at most 4. base 37, wildlife 27, npc 17; all packages 420.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:58:19 +03:00
f1049953c4 Merge branch 'lang/foundations' into lang/uifree 2026-09-28 01:51:56 +03:00
4d8526ad7e ludic.ui, runtime, compiler: a component call's answer is written into a pooled record while the screen is built (call(p, name, args, into); value_into_*), a component root's passes pooled, an icon's atlas and name read in place; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:51:56 +03:00
3e3c6088fb Merge branch 'r3d/bc-textures' into lang/foundations 2026-09-28 01:47:19 +03:00
862b048b17 Merge branch 'lang/foundations' into lang/nav 2026-09-28 01:38:54 +03:00
1a1110526e Merge branch 'lang/ecs' into lang/uifree 2026-09-28 01:29:02 +03:00
ea5eedb5a5 ludic.ui: a hole's text found by its pieces in the memo (built only when it first reads that way), avals left for the attributes read as text, a class's [root] made once, and a dev build's reload lets go of every file it read only to compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:29:02 +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
7a637967fe fix(leaks): effects_live_into / effects_live_count and hints_rail_into - a HUD's read-outs into lists it keeps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:21:53 +03:00