Commit graph

202 commits

Author SHA1 Message Date
28f7b4666b Merge commit 'e908672' into lang/foundations 2026-09-29 19:22:23 +03:00
3f2723582f render3d: an impostor's bake is BC7 with its mips - both atlases uploaded compressed, as baked
impostor_from_baked / impostor_refill read a bundle of two DX10 .dds (albedo, normal: BC7_UNORM, every
level) through tex_load_dds_at and gpu_tex_compressed - no gpu_tex_mips - and check each is the
impostor's size; sampled as a painted atlas is (clamped, no anisotropy). No BC on the GPU, or a bake
that does not fit, and the caller paints RGBA8 and makes mips as before; the fog re-open reads the BC7
file again. The handles and every draw are unchanged. Compile-only: nothing run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:18:48 +03:00
e908672034 render3d: a grass kind - the GPU blades' per-kind numbers are a GrassKind record the game hands over (grass_kind_set), not constants
GrassKind (grass_kind.ludic) carries what grass.ludic, grass_gpu.ludic, grass.vert, grass_cull.comp and
model.frag's BLADE path held as constants: the cell, s0 / d0 / radius, the row bands and rows, the blade
profile (neck, root, swell, tip, bend), the heights, clumps, widths and arch, where it grows (slope, snow,
the photograph's test), the root / tip / gone-to-seed colours, the far tufts, the root occlusion, the
roughness, the sheen and the wind. Its defaults are those constants exactly, so a game that sets nothing
draws as before. The shaders read them as u_gk_* (the cull as five more Params vec4s, 288 bytes);
grass_reset.comp writes each band's index count so a kind of other rows reaches the draw in order.
grass_quality holds the kind to a settings tier (never denser, never farther); R3D_GRASS_* still win.
grass_profile_w / _bend are the one profile, for the meshes and a game's preview. grass.mesh takes only
the cell: the rest of it is an older copy that already differs from grass.vert, left as it draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:15:39 +03:00
6c690c5db6 render3d + lab: bakes as PNGs, and impostors, the sky's light and the carpet read from their bakes
ludic.lab: lab_png_write / lab_png_write_from (raw 8-bit, 1-4 channels, stored deflate) in png_write.ludic,
importable alone with its own LabPngState; png_convert.ludic's previews of float textures (R32F min..max,
RG16F x255, HDR x/(1+x) + sRGB); lab_ppm_to_png on the same encoder.
render3d: bake_load.ludic - impostor_from_baked / impostor_source / impostor_refill (a fog re-open reads
the bake), sky_baked_in and sky_precompute trying the bake at the start yaw (sky_compute is the
convolution, and sky_ibl_bytes always uses it), carpet_from_baked / carpet_bytes / carpet_finish,
bake_part_count / _len / _off. Compile-only: nothing run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:10:06 +03:00
f79390838a render3d: a glTF material's factors are drawn - base colour, roughness, metallic and emission
gltf_factors reads pbrMetallicRoughness.baseColorFactor, metallicFactor, roughnessFactor and the
emissiveFactor into the primitive (pr.fac), and prim_factors hands them to every program that draws its
textures (actors, the scatter's meshes and levels, the impostor bake) as 1 - factor, so a program never
given them - or a material that gives none (pr.fac null) - multiplies by 1 and draws as before. An
untextured material with a colour (or a metal/roughness) samples a pure white for it, so the factor is
exactly what it draws: horse_cornea is its own colour, not the 200 grey every untextured part had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:52:42 +03:00
d2e1f804ce render3d: the bake and tile magics are "LBAK" and "LTT2" (they were "LAAK" and "LDT2")
Both constants were worked out by hand and a byte off each: the tiles' bake wrote "LAAK" and "LDT2", which
ludic bake --check refuses, and r3d_baked_read, holding the same wrong constant, would have refused
ludic.base's correct files (the sun's shadow among them) while accepting its own. Now 0x4B41424C and
0x3254544C, checked against the strings; the unused LTT1 constant is gone. A re-bake is needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:28:11 +03:00
31df4dba9f render3d: terrain_tiles_close - the tiles let go of their file before its map pack is unmounted
A world swap unmounts the old map pack before terrain_reload, and the tiles' file (a baked file in that
pack) stayed open until terrain_unload. terrain_tiles_close closes it and stops the page pool; the next
map's terrain_from_baked / terrain_reload opens its own and makes the pool again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:57:15 +03:00
f8d811aa97 Merge branch 'lang/foundations' into r3d/fog-wall 2026-09-29 17:43:14 +03:00
06d828a296 render3d: terrain_baked_at - a map's terrain and sun shadow taken from its bakes at start and on a swap
The game names the tiles' and the shadow's bake files (key, version) before the map is made; r3d_init's
terrain step and terrain_reload then open them with terrain_from_baked / terrain_shadow_from_bytes and
generate nothing, and fall back to the survey (saying so once) when a bake is missing or stale. Without
the call nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:42:16 +03:00
f02eab5c51 render3d: terrain tiles for a map with no photograph (Lamar)
The tiles' photograph side is 0 when the map has none: no photograph or coarse photograph sections, no
decode, ter_o answers 0, and no coarse photograph texture - as such a map has always drawn. The cut and
terrain_tiles_bake no longer require one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:40:39 +03:00
199109f1ae render3d: terrain_tiles_bake pads its key as ludic.base does (a zero after it) and makes its directory
baked_open refuses a header whose key is not followed by a zero byte; a key a multiple of 8 long got
none. Now the payload starts at 32 + ((len(key) + 1 + 7) / 8) * 8, as bake_write writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:39:40 +03:00
233bf152b9 Merge commit '82de673' into lang/foundations 2026-09-29 17:36:24 +03:00
82de673ce7 render3d: stream_capture runs the game's own fill for a chunk outside any frame, for the bake
stream_capture(s, cx, cz, band) points the stream at a chunk of its own (made on the first capture),
calls the registered r3d_stream_fill for that cell and band, and returns how many instances it
emitted; stream_captured(i, k) reads them from stream_scratch. Build-time only; nothing in a frame
calls it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:34:16 +03:00
9e6e7df3e6 Merge commit '967f15d' into lang/foundations 2026-09-29 17:31:50 +03:00
967f15ded9 render3d: baked textures before the PNG - a png_decode takes assets/baked/png, a cut-out load assets/baked/cutouts
baked_tex.ludic reads ludic.base's baked form by hand (render3d uses no package): the LBAK header, the
key ("png_sheets" / "cutouts", the Bakes rows) and the generator version, then the payload -
w, h, channels, depth and the samples for a PNG (the caller frees them as it would a decode), a whole
.dds for a cut-out (tex_load_dds_at: a .dds at an offset). Missing, or another key or version, and the
PNG path runs as before. Compiles with Maroon Lake (game, lab, lab/bake/textures).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:30:57 +03:00
305bd294a0 Merge commit '8589a5a' into lang/foundations 2026-09-29 17:28:33 +03:00
52ce5356cc Merge commit '3f0bb07' into lang/foundations 2026-09-29 17:28:33 +03:00
8589a5a7b4 render3d: place_rec.ludic, the one encoder and decoder of a baked placement
prec_put / prec_get / prec_size (Physics' spec): PREC_FULL, 12 bytes, for trees, boulders and stones
(u16 x, z in 1/65536 of the chunk; u16 y in cm above y_base; u8 scale over the kind's lo..hi, yaw, seed,
wind), and PREC_PACKED, 7 bytes, for the stream kinds (56 bits, least significant first: x 12, z 12,
y 12 in cm, scale 6, yaw 6, seed 4, wind 4). An encode takes the cell a value falls in, a decode its
centre, so a round trip is stable; a record is read from a []byte at an offset, never a pointer.
stream_emit_baked and layer_chunk_put take (recs, at, fmt) and decode through it; the earlier sb7_*
pair is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:28:11 +03:00
3f0bb07c16 render3d: the stream kinds' 7-byte baked record, and fixed layers held as the baked chunks that are in
sb7_pack / sb7_field are the one pack and unpack of the stream kinds' record (56 bits, least significant
first: x 12, z 12, y 12 over the chunk's y span, scale 6, yaw 6, seed 4, wind 4, read as a low and a high
word), which the bake and the game both import; stream_emit_baked decodes a prefix of it.
layer_chunks_begin / layer_chunk_put / layer_chunk_drop keep a fixed layer (trees, boulders, stones:
12-byte records) as a slab per resident chunk in its instance list, an n x n index made at load, the
list re-uploaded at most once a frame. Nothing calls them yet: behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:27:03 +03:00
104ba696c2 Merge commit '1fd87c7' into lang/foundations 2026-09-29 17:25:54 +03:00
1fd87c7376 render3d: a stream can be laid on the baked chunks' grid and filled from their records
stream_set_grid(s, ox, oz) puts a stream's cells on a grid from a corner (0 keeps world zero, today's);
stream_emit_baked(s, recs, count, keep, x0, z0, y_base, lo, hi) emits the first keep 12-byte records
of a baked chunk (the layout agreed with Physics: u16 x, z in 1/65536 of the chunk, u16 y in cm above
y_base, u8 scale over the kind's lo..hi, u8 yaw, seed, wind) into the chunk being filled, and
stream_band_keep(count, share) is a band's prefix. Nothing calls them yet: behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:25:18 +03:00
7f3b842d59 Merge commit 'a12f1b1' into lang/foundations 2026-09-29 17:24:52 +03:00
a12f1b1201 render3d: the terrain as quantized tiles (LTT2) that every reader answers from, baked or cut
Heights as u16 over each 64 m tile's own minimum and step (a tile spanning 200 m steps 3 mm), normals
octahedral 8 + 8, the photograph RGB8, and the coarse level (2048: heights f32, normals, photograph)
- 30 + 32 + 48 + 16 + 8 + 12 MB, about 146 MB a map where the float tiles were 192 plus nothing coarse.
The writer puts the whole copy back to the quantized values as it goes, so the build's own queries,
the physics, the placements' bake and every machine read the same numbers; a tile read decodes them
into the pools the queries and the page pool already use.

terrain_tiles_bake(path, key, version, inputs_hash) writes a bake's file (ludic.base's LBAK header,
the tiles as its payload) from a made map; terrain_from_baked(path, key, version, half, ox, oz) opens
one at boot in place of terrain_use_dem / terrain_use_ortho, and terrain_init then generates nothing
(and bakes the sun's shadow from the coarse level unless terrain_shadow_from_bytes gave it). Without a
bake the cut writes the same format to <dir>/<key>.tiles and reads it back. The GPU's coarse level is
made from the file's coarse sections (the blit from the whole maps is gone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:23:53 +03:00
253fe397f4 Merge commit '883ea8d' into lang/foundations 2026-09-29 17:20:05 +03:00
883ea8d48a render3d: the deterministic work as bytes a bake keeps, and the textures made from them
Each pair runs the work as the renderer always has and reads the result back, or makes the same
textures and fills them from bytes instead: impostor_bytes / impostor_from_bytes (and impostor_fill,
for a fog that opens past a layer's cards), terrain_shadow_bytes / terrain_shadow_from_bytes,
sky_ibl_bytes / sky_ibl_from_bytes (sky_precompute split into sky_ibl_make and the convolutions).
A bundle is a word count, a word length per part and the parts (bake_pack / bake_unpack), checked
against the textures' shapes before any byte is used. gpu_vk_readback.ludic reads a texture's level 0,
every layer, as stored; r3d_baked_read reads a bake's file (ludic.base's LBAK header: key and version
must match) since render3d cannot import ludic.base. Nothing calls them yet: behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:19:13 +03:00
d6122f0e31 Merge commit '7ffa0ec' into lang/foundations 2026-09-29 17:16:54 +03:00
7ffa0ecf6d render3d: the whole CPU height and photograph copies stay through the world's build; terrain_tiles_build_done lets them go
The build's placements ask exact heights over the whole map before any ring exists, and through the
tiles that read the file ~25 000 times in one frame. The copies now stay after the cut and the game
calls terrain_tiles_build_done() when the world is built (the end of world_things, and after a swap's
terrain_reload); every answer is the same before and after, from the copy or the tile. The GPU half
merged here (r3d/tp-rt, r3d/tp-shd: the page table, the pool and the shaders) is unchanged by it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:16:23 +03:00
c775e5b74e Merge branch 'r3d/tp-shd' into r3d/fog-wall 2026-09-29 17:14:08 +03:00
617ac10c5a render3d: the terrain's GPU maps paged round the camera while the tiles are on
At the cut the whole 4096 height, normal and photograph textures go, replaced by a coarse
2048 level (the CPU's tt_coarse uploaded; the normal and photograph blitted down on the GPU,
the photograph mipmapped) and a pool of fine tiles in three array textures - heights, normals,
photograph, each layer a tile with a one-texel border - addressed through a tt_n^2 page table
(u_tp_page: slot + 1, 0 = the coarse level). The pool holds the tiles within the reach (900 m,
or the fog wall's when nearer) and a ring, and is made again when the fog wall changes its size
(terrain_pages_fog). Once a frame (tp_frame, beside tt_frame) tiles past the reach and two tiles
go and the wanted ones come in nearest first, 8 a frame, written into a staging buffer kept for
the process (two halves, one per frame in flight) and copied into their layers inside the frame's
own command buffer - no submit of their own - with the page table re-uploaded only when it
changed. tp_bind binds the pool (or 1-layer stand-in arrays while paging is off, u_tp_on = 0) for
every program that reads the ground: terrain_bind_height (models, scatter, shadow_bind, grass),
terrain_bind_prog, the sun pass and the shadow bake. grass_cull.comp reads through the same page
table. R3D_VKMEM prints the pool's line. Tiles off, nothing changes. Compile-only: not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:13:11 +03:00
a9b7d216c8 Merge commit '7c74d95' into lang/foundations 2026-09-29 17:11:43 +03:00
7c74d95efa render3d: cut-out padding on every core - tex_dilate's rows per pass through Job.parallel_for, bytes unchanged
Within a pass a row writes only its own still-masked texels and reads only neighbours already let go,
which no row writes that pass, so the result is the single-threaded one. The worker takes a DilateJob
of plain buffers and allocates nothing. tex_dilate_bytes (safe_api) and examples/rendering/dilate.ludic,
which checks it against the old loop on RGB and RGBA atlases of sizes that do not divide (DILATE OK).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:11:21 +03:00
eee6a0906e render3d: the ground's maps read through a page table of fine tiles over a coarse map (shaders)
terpage.glsl is the reference block (tpSlot, tpUV, terHeight, terHeightSmooth, terNormalXZ,
terOrtho, tpOrthoRes, tpOrthoLod), pasted by section into terrain.vert, terrain.frag,
tersun.frag, model.vert, grass.vert and grass.mesh. u_tp_on = 0 reads the old samplers with the
old coordinates and filtering; on, a resident tile is read at level 0 from u_tp_h / u_tp_nrm /
u_tp_ortho, anything else from the coarse map now bound under the old names. The B-splines use
the FULL map's texel and take every tap through the page, so a tile edge stays one surface;
blurred photograph reads (lod 1-2.5) stay on u_ortho with the level moved down by the coarse
map's ratio. The fragment stages drop their unused u_height. SPIR-V rebuilt: terrain programs
carry 23 samplers (21 in the fragment stage), up from 19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:10:49 +03:00
329439cf61 render3d: under a fog wall the volumetric air is marched only to it, SSGI stops where it is solid, and card casters that fill the wall stop rebuilding
The volumetric pass marched 800 m in a fixed number of steps whatever the wall: it now stops at the
wall with steps in proportion (at least 8) - 0.57 -> 0.33 ms at 30 m by the pass timers. SSGI skipped
only past 900 m; it now also skips past 0.85 of a wall, where the fog is solid (u_ao_far). applyFog
samples the fog's colour only where its weight is above zero. A card layer whose casters inside the
wall are 80% or more of it (a kilometre's wall) casts from its static list and is not refiltered and
re-uploaded every 4 m of camera motion. Fog off: the frame unchanged (0 pixels over 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:00:27 +03:00
5b3cfac48e render3d: the height field and photograph on the CPU as tiles read from a file (off until the game asks)
terrain_tiles_to(dir, key) before a map is made: once made, its heights, photograph and baked normals
are written tile by tile (64 m, TT_TEX) to <dir>/<key>.tiles - fresh every time, header last, so no
other generator's or a torn file is ever read - and the whole copies go. Every read goes through the
tile: resident, else read from the file there and then into a 2048-tile clock, so terrain_height,
terrain_height_smooth, terrain_ortho* and terrain_chunk_heights answer exactly what the whole copy
did (R3D_TT_CHECK: worst 0.0 m over 4000 points) whatever is resident - two machines and the boot's
placements agree to the bit. terrain_chunk_heights takes render3d_st mut for it.

terrain_height_near(read-only): the tile if it is in, else a coarse 2048^2 level (worst 0.91 m), never
the file - for line checks that must not take render3d_st mut. terrain_texel() is the texel size,
terrain_tiles_prefetch(x, z, r, budget) reads ahead, and a frame that reads more than 8 tiles says so
once. R3D_TERRAIN_TILES=<dir> turns it on for a run.

At the overlook with MallocLargeCache=0: 1731 MB off, 1658 MB on; 192 MB written in ~200 ms; the
frame unchanged (0 pixels over 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:53:28 +03:00
c5e58d14ea render3d: the survey DEM is let go once the height field is made, and the terrain sun shadow is R32F + RG16F
The DEM (R16 with mips, 44 MB) was read only by terrain_generate and kept for the map's life; a
world swap loads its own again. The height-field sun shadow was one RGBA32F for three values and an
unused fourth: the lowest lit height stays 32-bit (a receiver 3000 m up compares against it to a
quarter metre), the occluder distance and the cloud mask go beside it in RG16F (64 -> 32 MB), baked
in a pass of their own (TS_AUX): a pipeline takes one colour format for all its targets, and drawn
together into R32F + RG16F the writes went nowhere and nothing was lit.

-75 MB at every fog level, off included (2008 -> 1933 MB at the overlook); the fog-off frame is
unchanged (0 pixels over 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:48:26 +03:00
f8c81e4066 render3d: the uniform ring is 16 MB a half and grows when a frame outgrows it; streamed layers are sized for the fog's reach
The ring held 2 x 64 MB of host memory for a frame that uses 3 MB at most (2761 draws at the
overlook, 1.7 MB in town): it starts at 16 MB a half, and a frame that ever runs out grows it for the
frames after (gvk_ring_grow, making the kept sets again; R3D_RING_KB=<n> starts small to watch it).
-95 MB at every fog level, off included.

A streamed layer's arrays and its stream's arena are made for the reach a fog wall leaves (twice its
area's share, at least 4096 instances) and emptied to refill within the frame budget
(fog_streams.ludic). The near streams' reach is already inside most walls, so it is -9 MB at 30 m.

R3D_VKMEM adds the ring's high-water mark, the largest buffers and the streamed layers' share.
Footprint at the overlook: 2008 MB off (2108 before this phase), 1748 MB at 175 m, 1682 MB at 30 m.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:22:45 +03:00
4d485a1778 render3d: the shadow array holds only the cascades a fog wall needs, and a thick fog takes the whole sky
A cascade that begins past the wall was fitted and cleared every frame for nothing: the array is now
made with the ones that begin inside it (2 at 30 m, 3 at 175 m, 5 without a fog) and made again when
the wall changes that count; shadow maps 80 -> 32 MB at 30 m, 48 MB at 175 m. A layer the array lacks
clamps to its last, read only past the wall where everything is fogged.

The sky: at walls of 110 m and nearer the whole sky is the fog's colour, overhead too, with a soft
glow at the sun or moon and no stars or clouds through it, easing out by 175 m; from 175 m on the
horizon band alone, and off as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:10:58 +03:00
7024f42789 render3d: a fog wall nearer than a layer's cards lets its impostor atlases go, and a fog that opens bakes them again
Inside the wall every instance is a mesh whose LOD2 casts its shadow, so a card is neither drawn nor
cast there: fog_impostors.ludic frees a layer's atlases while wall + margin < its near (trees 420-480
m, rocks 320 m) and paints them again from the model when it opens past it (impostor_paint, split out
of the bake). Impostor atlases get memory of their own, so a release goes back to the driver instead
of leaving a hole in a shared block. R3D_FOG_AT=<frame> with R3D_FOG_TO=<m> changes the wall mid-run.

At the overlook: 2102 MB off, 1824 MB at 30 m (249 -> 67 MB of atlases; the rest is the ground cover's
card atlases, drawn inside the wall); opening re-bakes 16 layers in 42-46 ms, back to 2102 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:05:16 +03:00
6289564382 render3d: R3D_VKMEM=<frame> says where the GPU memory is, by owner, and the renderer's big CPU arrays
Every allocation carries the owner its maker was wrapped in (models, terrain, impostor atlases,
scatter, grass, shadow maps, frame targets, sky, water, game textures, made in a frame), and the
report prints each owner's images and buffers, the 25 largest images and the scatter layers',
streams' and terrain's CPU copies. Nothing drawn changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:58:59 +03:00
f65bd1aca1 render3d: the fog wall is solid by 0.85 of it and the blades end at 0.75
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>
2026-09-29 15:41:25 +03:00
2ce2d997be render3d: the fog wall culls on the CPU - nothing past it costs a draw, a vertex or a caster
cam_sphere_visible refuses a sphere wholly past the wall (terrain patches, scatter cells, stream
chunks, grass tiles, water), actors past it are skipped for draws, casters and outlines, the grass
reach and the streams' generate-and-gather reach end at it, and the card shadows cast only from
the wall's share of a layer (fog_casters.ludic, rebuilt every 4 m). r3d_beyond_fog is exported for
the game to skip animating what will not be drawn. Render-only: no query of the ground or the world
changes, and off is the old frame (0 pixels over 8 against 160ce96, twice per side).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:56:02 +03:00