ludic/packages/ludic.render3d
Orkuncakilkaya 7918103b8b render3d: ground_fill_golden.json is valid JSON (the cover and solid blocks each closed one brace too many)
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>
2026-09-30 15:36:08 +03:00
..
lib/macos-arm64 runtime (macOS): MoltenVK opened first, so a process holds one - the SDK's loader in /usr/local/lib loaded its own MoltenVK beside the linked one and the program drew through it; the loader only when a layer is asked for, pinned to ours (VK_DRIVER_FILES, lib/macos-arm64/MoltenVK_icd.json) unless a driver is named. steady's stream round: a 300-cell warm-up, then the least of three 150-cell windows 2026-09-30 00:41:27 +03:00
native feat(render3d): 22.10 - ludic.render3d carries MoltenVK, so a Mac program has its Vulkan driver 2026-09-28 01:16:26 +03:00
shaders render3d: heightgen's plain and SMOOTH variants compiled (61 programs) - a smoothed or noise-only terrain had no height field on Vulkan; run-time defines as pure functions (program_defs.ludic) and manifest_test holds every program the source can ask for, literal or computed, to the manifest 2026-09-29 22:30:35 +03:00
tests render3d: ground_fill_golden.json is valid JSON (the cover and solid blocks each closed one brace too many) 2026-09-30 15:36:08 +03:00
actor.ludic render3d: a glTF material's factors are drawn - base colour, roughness, metallic and emission 2026-09-29 18:52:42 +03:00
assets.manifest feat(gl): OpenGL 4.1 and the ludic.render3d renderer 2026-09-10 03:31:12 +03:00
bake_load.ludic render3d: an impostor's bake is BC7 with its mips - both atlases uploaded compressed, as baked 2026-09-29 19:18:48 +03:00
bake_parts.ludic render3d + lab: bakes as PNGs, and impostors, the sky's light and the carpet read from their bakes 2026-09-29 19:10:06 +03:00
baked_tex.ludic render3d: baked textures before the PNG - a png_decode takes assets/baked/png, a cut-out load assets/baked/cutouts 2026-09-29 17:30:57 +03:00
camera.ludic render3d: the fog wall culls on the CPU - nothing past it costs a draw, a vertex or a caster 2026-09-29 15:38:00 +03:00
daylight.ludic 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 2026-09-25 17:11:15 +03:00
drawstats.ludic r3d/runtime: allocs, keeps and births at 0 on this side 2026-09-28 20:10:59 +03:00
env.ludic render3d: ground_fill's candidate is a function of its own; solid layers with stable ids; the trample as clearing discs 2026-09-30 15:23:53 +03:00
FEATURES.md r3d/runtime: allocs, keeps and births at 0 on this side 2026-09-28 20:10:59 +03:00
fmath.ludic r3d/runtime: allocs, keeps and births at 0 on this side 2026-09-28 20:10:59 +03:00
fog_casters.ludic 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 2026-09-29 17:00:27 +03:00
fog_impostors.ludic render3d + lab: bakes as PNGs, and impostors, the sky's light and the carpet read from their bakes 2026-09-29 19:10:06 +03:00
fog_streams.ludic 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 2026-09-29 16:22:45 +03:00
gltf.ludic render3d: a glTF material's factors are drawn - base colour, roughness, metallic and emission 2026-09-29 18:52:42 +03:00
gltf_cached.ludic render3d: 671 frame-reachable allocation sites to 161 - declared, split, or made nothing 2026-09-28 15:58:18 +03:00
gltf_release.ludic render3d: @creates / @releases on its handles 2026-09-28 16:58:31 +03:00
gpu.ludic render3d: a renderer that cannot draw says so and stops - LUDIC_HOME's leading ~ expanded (an unexpanded one left a run without shaders, logging 27 missing variants and drawing on), the SPIR-V manifest ends with E <count> and a cut or miscounted one is refused, and every such failure is a fault: r3d_open / r3d_load_step fail on it, r3d_fault() / r3d_fault_exit(code) for the game; tests/manifest_test 2026-09-29 22:12:39 +03:00
gpu_fault.ludic render3d: a renderer that cannot draw says so and stops - LUDIC_HOME's leading ~ expanded (an unexpanded one left a run without shaders, logging 27 missing variants and drawing on), the SPIR-V manifest ends with E <count> and a cut or miscounted one is refused, and every such failure is a fault: r3d_open / r3d_load_step fail on it, r3d_fault() / r3d_fault_exit(code) for the game; tests/manifest_test 2026-09-29 22:12:39 +03:00
gpu_manifest.ludic render3d: a renderer that cannot draw says so and stops - LUDIC_HOME's leading ~ expanded (an unexpanded one left a run without shaders, logging 27 missing variants and drawing on), the SPIR-V manifest ends with E <count> and a cut or miscounted one is refused, and every such failure is a fault: r3d_open / r3d_load_step fail on it, r3d_fault() / r3d_fault_exit(code) for the game; tests/manifest_test 2026-09-29 22:12:39 +03:00
gpu_vk.ludic render3d: the GPU blades grow by a painted density (a layer of the ground densities, grass_density_layer) where a map has one - a 5x5-tile window round the camera in a storage buffer the cull samples - and by today's rules where not; grass_rule_at is the rules on the CPU for the migration 2026-09-29 21:14:22 +03:00
gpu_vk_bc.ludic feat(render3d): 23.3 - a .dds beside a .png is uploaded BC-compressed with its whole mip chain 2026-09-28 01:23:45 +03:00
gpu_vk_draw.ludic render3d: a renderer that cannot draw says so and stops - LUDIC_HOME's leading ~ expanded (an unexpanded one left a run without shaders, logging 27 missing variants and drawing on), the SPIR-V manifest ends with E <count> and a cut or miscounted one is refused, and every such failure is a fault: r3d_open / r3d_load_step fail on it, r3d_fault() / r3d_fault_exit(code) for the game; tests/manifest_test 2026-09-29 22:12:39 +03:00
gpu_vk_layers.ludic render3d: the terrain's GPU maps paged round the camera while the tiles are on 2026-09-29 17:13:11 +03:00
gpu_vk_readback.ludic render3d: gpu_tex_read_all and gpu_tex_write_all flush the frame first - the read-back is a submit of its own and took the image before the draws that fill it (the sky bake's BRDF table was all zeros at one width in three) 2026-09-29 23:18:05 +03:00
gpu_vk_res.ludic render3d/vk: a buffer re-filled every frame ping-pongs between two instead of being made again 2026-09-29 14:45:02 +03:00
grass.ludic render3d: heightgen's plain and SMOOTH variants compiled (61 programs) - a smoothed or noise-only terrain had no height field on Vulkan; run-time defines as pure functions (program_defs.ludic) and manifest_test holds every program the source can ask for, literal or computed, to the manifest 2026-09-29 22:30:35 +03:00
grass_bind.ludic render3d: a grass kind - the GPU blades' per-kind numbers are a GrassKind record the game hands over (grass_kind_set), not constants 2026-09-29 19:15:39 +03:00
grass_density.ludic render3d: the blades' density window filled apart from its upload - gb_window_fill (the tiles round the camera's, zeros off the map, the corner) and gb_frame sends it; the same bytes, now readable by a test without a GPU 2026-09-30 02:03:43 +03:00
grass_gpu.ludic render3d: the GPU blades grow by a painted density (a layer of the ground densities, grass_density_layer) where a map has one - a 5x5-tile window round the camera in a storage buffer the cull samples - and by today's rules where not; grass_rule_at is the rules on the CPU for the migration 2026-09-29 21:14:22 +03:00
grass_kind.ludic render3d: a grass kind - the GPU blades' per-kind numbers are a GrassKind record the game hands over (grass_kind_set), not constants 2026-09-29 19:15:39 +03:00
grass_rule.ludic render3d: the GPU blades grow by a painted density (a layer of the ground densities, grass_density_layer) where a map has one - a 5x5-tile window round the camera in a storage buffer the cull samples - and by today's rules where not; grass_rule_at is the rules on the CPU for the migration 2026-09-29 21:14:22 +03:00
ground_cand.ludic render3d: ground_fill's candidate is a function of its own; solid layers with stable ids; the trample as clearing discs 2026-09-30 15:23:53 +03:00
ground_clear.ludic render3d: ground_fill's candidate is a function of its own; solid layers with stable ids; the trample as clearing discs 2026-09-30 15:23:53 +03:00
ground_density.ludic render3d: gd_fill tests a deflated tile's top bit as written, (b & 0x80000000) != 0, now that the compiler emits the literal (lang/hex-literal, reseeded) - the b < 0 stand-in goes 2026-09-29 22:20:20 +03:00
ground_density_write.ludic render3d: ground densities as LGD2 - empty tiles free, one-value tiles a word, deflated tiles, quantized 2026-09-29 21:55:28 +03:00
ground_fill.ludic render3d: ground_fill's candidate is a function of its own; solid layers with stable ids; the trample as clearing discs 2026-09-30 15:23:53 +03:00
ground_solid.ludic render3d: ground_fill's candidate is a function of its own; solid layers with stable ids; the trample as clearing discs 2026-09-30 15:23:53 +03:00
ground_solid_list.ludic render3d: ground_fill's candidate is a function of its own; solid layers with stable ids; the trample as clearing discs 2026-09-30 15:23:53 +03:00
hooks.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
layer_chunks.ludic render3d: place_rec.ludic, the one encoder and decoder of a baked placement 2026-09-29 17:28:11 +03:00
mesh.ludic render3d: @owns on the handles records hold 2026-09-28 17:08:15 +03:00
overlay.ludic render3d/overlay: the font atlas kept as its coverage, one byte a texel 2026-09-29 14:45:02 +03:00
package.ludic feat(render3d): 22.10 - ludic.render3d carries MoltenVK, so a Mac program has its Vulkan driver 2026-09-28 01:16:26 +03:00
place_rec.ludic render3d: place_rec.ludic, the one encoder and decoder of a baked placement 2026-09-29 17:28:11 +03:00
post.ludic 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 2026-09-29 17:00:27 +03:00
prof.ludic render3d: start-up declared, ov_nine and the caster test on buffers made once - 362 frame sites to 122 2026-09-28 16:21:51 +03:00
program_defs.ludic render3d: heightgen's plain and SMOOTH variants compiled (61 programs) - a smoothed or noise-only terrain had no height field on Vulkan; run-time defines as pure functions (program_defs.ludic) and manifest_test holds every program the source can ask for, literal or computed, to the manifest 2026-09-29 22:30:35 +03:00
programs.ludic render3d: a renderer that cannot draw says so and stops - LUDIC_HOME's leading ~ expanded (an unexpanded one left a run without shaders, logging 27 missing variants and drawing on), the SPIR-V manifest ends with E <count> and a cut or miscounted one is refused, and every such failure is a fault: r3d_open / r3d_load_step fail on it, r3d_fault() / r3d_fault_exit(code) for the game; tests/manifest_test 2026-09-29 22:12:39 +03:00
quat.ludic r3d/runtime: allocs, keeps and births at 0 on this side 2026-09-28 20:10:59 +03:00
r3d.ludic render3d: ground_fill's candidate is a function of its own; solid layers with stable ids; the trample as clearing discs 2026-09-30 15:23:53 +03:00
r3d_root.ludic render3d: a renderer that cannot draw says so and stops - LUDIC_HOME's leading ~ expanded (an unexpanded one left a run without shaders, logging 27 missing variants and drawing on), the SPIR-V manifest ends with E <count> and a cut or miscounted one is refused, and every such failure is a fault: r3d_open / r3d_load_step fail on it, r3d_fault() / r3d_fault_exit(code) for the game; tests/manifest_test 2026-09-29 22:12:39 +03:00
README.md render3d: a README note on painted ground layers, and the changeset 2026-09-30 15:24:23 +03:00
render.ludic render3d: a renderer that cannot draw says so and stops - LUDIC_HOME's leading ~ expanded (an unexpanded one left a run without shaders, logging 27 missing variants and drawing on), the SPIR-V manifest ends with E <count> and a cut or miscounted one is refused, and every such failure is a fault: r3d_open / r3d_load_step fail on it, r3d_fault() / r3d_fault_exit(code) for the game; tests/manifest_test 2026-09-29 22:12:39 +03:00
safe_api.ludic render3d: cut-out padding on every core - tex_dilate's rows per pass through Job.parallel_for, bytes unchanged 2026-09-29 17:11:21 +03:00
scatter.ludic Merge commit 'e908672' into lang/foundations 2026-09-29 19:22:23 +03:00
shadow.ludic render3d: the survey DEM is let go once the height field is made, and the terrain sun shadow is R32F + RG16F 2026-09-29 16:48:26 +03:00
skin.ludic r3d/runtime: allocs, keeps and births at 0 on this side 2026-09-28 20:10:59 +03:00
sky.ludic render3d + lab: bakes as PNGs, and impostors, the sky's light and the carpet read from their bakes 2026-09-29 19:10:06 +03:00
stream.ludic render3d: a stream can be laid on the baked chunks' grid and filled from their records 2026-09-29 17:25:18 +03:00
stream_baked.ludic render3d: stream_capture runs the game's own fill for a chunk outside any frame, for the bake 2026-09-29 17:34:16 +03:00
streamline.ludic 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 2026-09-29 15:07:40 +03:00
terrain.ludic render3d: heightgen's plain and SMOOTH variants compiled (61 programs) - a smoothed or noise-only terrain had no height field on Vulkan; run-time defines as pure functions (program_defs.ludic) and manifest_test holds every program the source can ask for, literal or computed, to the manifest 2026-09-29 22:30:35 +03:00
terrain_baked_boot.ludic render3d: terrain_baked_at - a map's terrain and sun shadow taken from its bakes at start and on a swap 2026-09-29 17:42:16 +03:00
terrain_chunks.ludic render3d: the height field and photograph on the CPU as tiles read from a file (off until the game asks) 2026-09-29 16:53:28 +03:00
terrain_height_file.ludic render3d: the terrain's questions never page - ter_h and ter_o read a resident tile in place and any other through the file scratch (heights and now the photograph, four tiles each, sized when the tiles open, forgotten when they close), so terrain_height, _smooth, terrain_ortho, the chunk heightfields, ground_fill, grass, water and shadow no longer take slots or move the clock. Only the loaders page: tp_fill reads through tt_h_load / tt_n_load / tt_o_load, and terrain_tiles_prefetch as before. terrain_height_file is now terrain_height, kept as the name callers wrote against. No answer changes: every read was already the whole copy's texel. tests/height_file_test: heights and photograph equal the texels written, resident or not; a whole-map sweep of the questions, and a ground layer's fill over the map (a one-layer LGD2 written in the test), leave the slots, their contents, the clock and the read counts unchanged 2026-09-30 02:43:17 +03:00
terrain_ltt2_read.ludic render3d: the terrain's questions never page - ter_h and ter_o read a resident tile in place and any other through the file scratch (heights and now the photograph, four tiles each, sized when the tiles open, forgotten when they close), so terrain_height, _smooth, terrain_ortho, the chunk heightfields, ground_fill, grass, water and shadow no longer take slots or move the clock. Only the loaders page: tp_fill reads through tt_h_load / tt_n_load / tt_o_load, and terrain_tiles_prefetch as before. terrain_height_file is now terrain_height, kept as the name callers wrote against. No answer changes: every read was already the whole copy's texel. tests/height_file_test: heights and photograph equal the texels written, resident or not; a whole-map sweep of the questions, and a ground layer's fill over the map (a one-layer LGD2 written in the test), leave the slots, their contents, the clock and the read counts unchanged 2026-09-30 02:43:17 +03:00
terrain_ltt2_write.ludic render3d: the bake and tile magics are "LBAK" and "LTT2" (they were "LAAK" and "LDT2") 2026-09-29 18:28:11 +03:00
terrain_pages.ludic render3d: the terrain's GPU maps paged round the camera while the tiles are on 2026-09-29 17:13:11 +03:00
terrain_pages_fill.ludic render3d: the terrain's questions never page - ter_h and ter_o read a resident tile in place and any other through the file scratch (heights and now the photograph, four tiles each, sized when the tiles open, forgotten when they close), so terrain_height, _smooth, terrain_ortho, the chunk heightfields, ground_fill, grass, water and shadow no longer take slots or move the clock. Only the loaders page: tp_fill reads through tt_h_load / tt_n_load / tt_o_load, and terrain_tiles_prefetch as before. terrain_height_file is now terrain_height, kept as the name callers wrote against. No answer changes: every read was already the whole copy's texel. tests/height_file_test: heights and photograph equal the texels written, resident or not; a whole-map sweep of the questions, and a ground layer's fill over the map (a one-layer LGD2 written in the test), leave the slots, their contents, the clock and the read counts unchanged 2026-09-30 02:43:17 +03:00
terrain_pages_frame.ludic render3d: the terrain's GPU maps paged round the camera while the tiles are on 2026-09-29 17:13:11 +03:00
terrain_tiles.ludic render3d: the terrain's questions never page - ter_h and ter_o read a resident tile in place and any other through the file scratch (heights and now the photograph, four tiles each, sized when the tiles open, forgotten when they close), so terrain_height, _smooth, terrain_ortho, the chunk heightfields, ground_fill, grass, water and shadow no longer take slots or move the clock. Only the loaders page: tp_fill reads through tt_h_load / tt_n_load / tt_o_load, and terrain_tiles_prefetch as before. terrain_height_file is now terrain_height, kept as the name callers wrote against. No answer changes: every read was already the whole copy's texel. tests/height_file_test: heights and photograph equal the texels written, resident or not; a whole-map sweep of the questions, and a ground layer's fill over the map (a one-layer LGD2 written in the test), leave the slots, their contents, the clock and the read counts unchanged 2026-09-30 02:43:17 +03:00
terrain_tiles_cut.ludic render3d: terrain_height_file - terrain_height's exact bilinear that never pages: a resident tile is read in place, any other from tt_file into four tiles of scratch of its own (in the state's defaults, forgotten when the file closes), so no slot is taken and no recency bumped - a sweep at boot no longer changes which tiles the cache holds, and so what is drawn from it. With the whole copy it is terrain_height. tests/height_file_test: equal to terrain_height over a grid across many tiles, resident and not, at tile corners and past the edge; a whole-map sweep leaves slot_of, tile_in, ref, the heights, the hand and the read counts unchanged 2026-09-30 02:29:55 +03:00
terrain_tiles_open.ludic render3d: terrain_tiles_open_file(path, key, version, half, ox, oz) - a map's baked tiles opened for the CPU questions alone (terrain_height, _file, _smooth, terrain_ortho), with no device and no renderer booted: terrain_from_baked's opening without its coarse level on the GPU. False and nothing changed for another bake's key or version, a missing file, or a whole copy already kept (it answers first). tests/tiles_open_test: a bake written with LBAK's header answers its heights and photograph at a map origin of (300, -120), nothing on a device; the refusals leave no file open, and a bake opened after them answers. The hand-written tiles move to tests/fakes/tiles_file.ludic (payload, raw file, bake, the expected answer at an origin, the cache's snapshot), shared with height_file_test 2026-09-30 03:13:18 +03:00
texture.ludic render3d: baked textures before the PNG - a png_decode takes assets/baked/png, a cut-out load assets/baked/cutouts 2026-09-29 17:30:57 +03:00
texture_dds.ludic render3d: baked textures before the PNG - a png_decode takes assets/baked/png, a cut-out load assets/baked/cutouts 2026-09-29 17:30:57 +03:00
vkmem_report.ludic render3d: the terrain's GPU maps paged round the camera while the tiles are on 2026-09-29 17:13:11 +03:00
water.ludic render3d: the height field and photograph on the CPU as tiles read from a file (off until the game asks) 2026-09-29 16:53:28 +03:00

ludic.render3d

A physically based 3D renderer over Vulkan. FEATURES.md lists what is implemented and where.

Painted ground layers

A ground layer is a density painted over the map (R the chance a cell keeps one, G its scale, 2 m texels baked into an LGD2 file, ground_density.ludic) and a GroundFill row of numbers. Everything it grows is a pure function of those two - the same on every machine and in the editor's preview.

  • The candidate (ground_cand.ludic): for (layer, chunk, band, cell) ground_candidate answers whether one stands there and its x, z, scale, yaw, seed and wind, into a GroundCand the caller holds. Its steps are pure: gf_along (the jitter, draws 0 and 1), gf_keeps (draw 2 under the chance), gf_scale (G and draw 6), gf_yaw (3), gf_seed (4), gf_wind (5). ground_fill draws exactly these. Numbers are IEEE single precision, Math.lerp is a + (b - a) * t, gf_hash is 32-bit integer arithmetic with a logical >>, and the model shader turns local (x, z) to (c x + s z, -s x + c z) - three.js's rotation about +Y.
  • Cover and solid. A cover layer (grass, flowers, pebbles) is filled at its band's step, so a far band is a different, sparser sample. A layer with solid: true (rocks, trees) is filled at step0 with band 0's hashes whatever the band, and a far band draws the subset whose draw 7 falls under (step0 / step_band)^2; its scale ignores band_grow. Nothing solid moves with the camera.
  • Solid ids (ground_solid.ludic): ground_solid_id(layer, cx, cz, i, j, cells) = ((layer * 256 + cz + 128) * 256 + cx + 128) * 512 + i * cells + j, a positive 31-bit int, stable while the painting under it is. It holds chunks -128 .. 127 about the stream's corner and at most 22 cells a chunk side (a step of at least 32 / 22 m on 32 m chunks); a cell past that has no id and is not grown. ground_solid_list fills a caller's GroundSolids (made once, ground_solids_new) with a chunk's things for physics, the nav bake or a save; ground_solid_at answers one by id.
  • The trample as data (ground_clear.ludic): r3d_ground_clearing(x, z, r_in, r_out, floor) hands over a clearing - floor inside r_in, back to full at r_out on a smoothstep, the shape of the camp's ring and the town's yard. A layer with trample multiplies its chance by the game's callback (r3d_on_ground_trample, kept for now) and then by every clearing. A game should use one or the other.
  • Conformance: tests/ground_fill_test.ludic holds the package to tests/ground_fill_golden.json (gf_hash, lerp, yaw, one cover chunk and one solid chunk over a hand-written density), which the studio's own test reads. After a deliberate change, rebuild it from the repository root: bin/ludic run packages/ludic.render3d/tests/gen/ground_fill_golden.ludic --headless, and commit it.