gg_cover_json and gg_solid_json ended in plain strings with "}}", which only a template literal reads as one
brace: the cover's closed the top-level object before "solid", and the solid's left a stray "}" at the end.
The committed golden is fixed by hand to what the generator now writes.
ground_fill_test also checks the golden as JSON: one value with nothing after it (Json.parse reads the
first value and ignores the rest, so it alone would not have caught this), an object, its eleven sections
in order. The generator notes what the studio asked: past the density patch an empty tile reads 0 for R and
G (only past the map's edge does a texel clamp), and several clearings multiply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/ground_fill_test.ludic compares gg_json (gf_hash over fixed inputs, Math.lerp cases whose order
matters, the yaw as model.vert turns by it, a hand-written density, one cover chunk with a clearing and
one solid chunk) line for line with tests/ground_fill_golden.json, and checks ground_fill draws exactly
the candidates and a solid layer's far bands a subset of band 0. The golden is written by
tests/gen/ground_fill_golden.ludic, run from the repository root; it is not committed here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ground_candidate answers (layer, chunk, band, cell) -> keep, x, z, scale, yaw, seed, wind from pure gf_*
steps and the density read between them; ground_fill draws its answers with the same operations in the
same order. A solid layer fills at step0 and band 0 whatever the camera, a far band drawing a stable
subset (draw 7 under (step0/step_b)^2), each thing an int id from (layer, chunk, cell), listed per chunk
into a caller's GroundSolids or answered by id. r3d_ground_clearing hands the trample over as discs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first cut (LGD1) stored every tile of every layer raw, scale channel and all: 288 MB a map, which
made Maroon's pack 803 MB. LGD2 keeps a tile index per layer (8 bytes a tile, read whole at the map's
load): 0 all zero, 1 one value in the word, else an offset and a length, the top bit set when the bake
deflated it (inflated into the tile cache with the runtime's z_inflate, a reused buffer; nothing per
frame beyond the cache). A layer keeps its scale only where it varies, the density 6 bits and the scale
4 (the same integer arithmetic as the bake). The bake is maroon-lake's tools/bake/ground_density.py;
the dev copy here writes the same format stored. Maroon 23.2 MB, Lamar 17.2 MB.
The deflated flag is tested as a negative length: `b & 0x80000000` compiled to broken IR (an i32
`and` with a bare `-`), a compiler fault to fix separately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GroundFill gains scale_var (an instance's scale x (1 +- var), from its own hash) and band_grow
(x (1 + grow * band)): a painted density carries the local mean, and without them every flower of a
kind was the same size and the far clumps lost the growth that kept the cover as the step widened.
Both 0: as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ground_density_bake(path, key, version, hash, pngs) cuts each layer's PNG (R the chance a cell keeps one,
G an optional scale) into the terrain's 64 m tiles, one layer decoded and let go at a time, as a bake's
file; ground_density_open(...) reads it - or, when it is missing or stale, a dev build's own copy cut from
the PNGs - a tile at a time into a 256-tile clock, never a layer whole. ground_fill(s, cx, cz, band, g)
places a GroundFill row's instances in a stream chunk: a candidate per cell of the band's step, jittered,
kept when a hash of (layer, chunk, band, cell) falls under the density (times the game's trample,
r3d_on_ground_trample, where the row asks); scale, yaw, seed and wind from the same hash. Nothing calls
it yet: behaviour unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>