From 3f0bb07c16f4e8590e414d8378bb10e334e04269 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 17:27:03 +0300 Subject: [PATCH] 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 --- packages/ludic.render3d/layer_chunks.ludic | 65 ++++++++++++++++++++++ packages/ludic.render3d/r3d.ludic | 1 + packages/ludic.render3d/scatter.ludic | 2 + packages/ludic.render3d/stream_baked.ludic | 54 +++++++++++++----- 4 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 packages/ludic.render3d/layer_chunks.ludic diff --git a/packages/ludic.render3d/layer_chunks.ludic b/packages/ludic.render3d/layer_chunks.ludic new file mode 100644 index 00000000..70a0b4ce --- /dev/null +++ b/packages/ludic.render3d/layer_chunks.ludic @@ -0,0 +1,65 @@ +# layer_chunks.ludic — a fixed layer (trees, boulders, stones) held as the baked chunks that are in, not +# the whole map: a slab per chunk in the layer's instance list, put when the ring brings the chunk in +# and dropped when it goes, the list re-uploaded at most once a frame. A record is 12 bytes: u16 x, u16 z +# (1/65536 of the chunk), u16 y (cm above y_base), u8 scale (the kind's lo..hi), u8 yaw, u8 seed, u8 wind. + +const LC_REC: int = 12 + +# the layer made ready for chunks of an n x n grid: its slab index, once at map load, and nothing in it +@alloc_ok("a map being loaded: a layer's chunk index, once") +export function layer_chunks_begin(l: Layer, n: int) -> void { + if l.ck_off == null or len(l.ck_off) != n * n { + if l.ck_off != null { free(l.ck_off); free(l.ck_cnt) } + l.ck_off = words(n * n); l.ck_cnt = words(n * n) + } + for i in 0 .. n * n { l.ck_off[i] = -1; l.ck_cnt[i] = 0 } + l.count = 0 + layer_chunks_changed(l) +} + +# chunk `id`'s records put into the layer (replacing what it had); as many as the layer has room for +export function layer_chunk_put(l: Layer, id: int, recs: pointer, count: int, size: float, x0: float, z0: float, y_base: float, lo: float, hi: float) -> void { + if l.ck_off == null or id < 0 or id >= len(l.ck_off) { return } + if l.ck_off[id] >= 0 { layer_chunk_drop(l, id) } + let n = min(count, l.cap - l.count) + if n <= 0 { return } + layer_room(l, l.count + n) + let span = size / 65536.0 + let sr = (hi - lo) / 255.0 + for i in 0 .. n { + let r = i * LC_REC + let o = (l.count + i) * INST_FLOATS + let yaw = float(recs[r + 7]) * (2.0 * PI / 256.0) + l.inst[o] = x0 + float(recs[r] | (recs[r + 1] << 8)) * span + l.inst[o + 1] = y_base + float(recs[r + 4] | (recs[r + 5] << 8)) * 0.01 + l.inst[o + 2] = z0 + float(recs[r + 2] | (recs[r + 3] << 8)) * span + l.inst[o + 3] = lo + float(recs[r + 6]) * sr + l.inst[o + 4] = Math.sin(yaw); l.inst[o + 5] = Math.cos(yaw) + l.inst[o + 6] = float(recs[r + 8]) / 255.0; l.inst[o + 7] = float(recs[r + 9]) / 255.0 + } + l.ck_off[id] = l.count + l.ck_cnt[id] = n + l.count += n + layer_chunks_changed(l) +} + +# chunk `id`'s slab taken out: the instances after it move down, and the slabs after it with them +export function layer_chunk_drop(l: Layer, id: int) -> void { + if l.ck_off == null or id < 0 or id >= len(l.ck_off) or l.ck_off[id] < 0 { return } + let off = l.ck_off[id] + let n = l.ck_cnt[id] + for k in (off + n) * INST_FLOATS .. l.count * INST_FLOATS { l.inst[k - n * INST_FLOATS] = l.inst[k] } + for c in 0 .. len(l.ck_off) { if l.ck_off[c] > off { l.ck_off[c] = l.ck_off[c] - n } } + l.ck_off[id] = -1 + l.ck_cnt[id] = 0 + l.count -= n + layer_chunks_changed(l) +} + +# the layer's list changed: the GPU cull's buffers and the view's partition are made again next frame. +# A layer on the CPU path keeps the grid it had (a baked layer is meant for the GPU cull). +function layer_chunks_changed(l: Layer) -> void { + l.g_on = false + l.g_n = -1 + l.view_gen = -1 +} diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 07d98a96..707a11a5 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -44,6 +44,7 @@ import "gltf_cached.ludic" import "skin.ludic" import "scatter.ludic" import "fog_casters.ludic" +import "layer_chunks.ludic" import "fog_impostors.ludic" import "actor.ludic" import "stream.ludic" diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 8e2c9cb7..9956b336 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -39,6 +39,8 @@ property Layer { fog_buf: int = 0, # the casters inside the fog wall (fog_casters.ludic) fog_n: int = 0, fog_src: int = -1, + ck_off: words, # per baked chunk: where its slab starts in inst, or -1 (layer_chunks.ludic) + ck_cnt: words, fog_all: bool = false, # the wall keeps (nearly) all of it: casters from sh_buf, no rebuild fog_wall: float = 0.0, fog_x: float = 0.0, diff --git a/packages/ludic.render3d/stream_baked.ludic b/packages/ludic.render3d/stream_baked.ludic index 4c05a8bb..40ac01af 100644 --- a/packages/ludic.render3d/stream_baked.ludic +++ b/packages/ludic.render3d/stream_baked.ludic @@ -1,10 +1,7 @@ # stream_baked.ludic — a stream filled from baked chunks instead of generated ones (plan 26 of # maroon-lake). The bake stores every instance of a kind once per 64 m chunk, at full density and in a # shuffled order, so a distance band draws a PREFIX of it: a nearer band holds everything a farther one -# does. A record is 12 bytes: u16 x, u16 z (1/65536 of the chunk), u16 y (cm above the chunk's y_base), -# u8 scale (lo..hi, the kind's range), u8 yaw, u8 seed, u8 wind. - -const SB_REC: int = 12 +# does. The stream kinds' records are 7 bytes (below); the fixed layers' 12 (layer_chunks.ludic). # the stream's cells laid on the baked chunks' grid: `size` metres from the corner (ox, oz) export function stream_set_grid(s: Stream, ox: float, oz: float) -> void { @@ -12,19 +9,46 @@ export function stream_set_grid(s: Stream, ox: float, oz: float) -> void { s.oz = oz } -# the first `keep` records of a chunk emitted into the chunk being filled (inside r3d_stream_fill) -export function stream_emit_baked(render3d_st: mut Render3dState, s: Stream, recs: pointer, count: int, keep: int, x0: float, z0: float, y_base: float, lo: float, hi: float) -> void { +# ---- the stream kinds' 7-byte record (the one pack and unpack both the bake and the game use) ---- +# 56 bits, least significant first, bytes little-endian: x 12 (1/4096 of the chunk), z 12, y 12 (over +# the chunk's y span: y_base + v / 4095 * y_span), scale 6 (lo + v / 63 * (hi - lo)), yaw 6 (v / 64 of a +# turn), seed 4 (v / 15), wind 4 (v / 15). Read as a low word (bytes 0-3) and a high one (4-6), so no +# field needs a 64-bit int. +const SB7_REC: int = 7 + +# one record's fields (already quantized) written at `at` in `out` +export function sb7_pack(out: []byte, at: int, x: int, z: int, y: int, scale: int, yaw: int, seed: int, wind: int) -> void { + let lo = (x & 4095) | ((z & 4095) << 12) | ((y & 255) << 24) + let hi = ((y >> 8) & 15) | ((scale & 63) << 4) | ((yaw & 63) << 10) | ((seed & 15) << 16) | ((wind & 15) << 20) + out[at] = lo & 255; out[at + 1] = (lo >> 8) & 255; out[at + 2] = (lo >> 16) & 255; out[at + 3] = (lo >> 24) & 255 + out[at + 4] = hi & 255; out[at + 5] = (hi >> 8) & 255; out[at + 6] = (hi >> 16) & 255 +} + +# field k of the record at `at` (0 x, 1 z, 2 y, 3 scale, 4 yaw, 5 seed, 6 wind), as its integer +export function sb7_field(recs: pointer, at: int, k: int) -> int { + let lo = recs[at] | (recs[at + 1] << 8) | (recs[at + 2] << 16) | ((recs[at + 3] & 255) << 24) + let hi = recs[at + 4] | (recs[at + 5] << 8) | (recs[at + 6] << 16) + if k == 0 { return lo & 4095 } + if k == 1 { return (lo >> 12) & 4095 } + if k == 2 { return ((lo >> 24) & 255) | ((hi & 15) << 8) } + if k == 3 { return (hi >> 4) & 63 } + if k == 4 { return (hi >> 10) & 63 } + if k == 5 { return (hi >> 16) & 15 } + return (hi >> 20) & 15 +} + +# the first `keep` 7-byte records of a stream kind's chunk emitted into the chunk being filled +export function stream_emit_baked(render3d_st: mut Render3dState, s: Stream, recs: pointer, count: int, keep: int, x0: float, z0: float, y_base: float, y_span: float, lo: float, hi: float) -> void { let n = min(keep, count) - let span = s.size / 65536.0 - let sr = (hi - lo) / 255.0 + let span = s.size / 4096.0 for i in 0 .. n { - let o = i * SB_REC - let x = x0 + float(recs[o] | (recs[o + 1] << 8)) * span - let z = z0 + float(recs[o + 2] | (recs[o + 3] << 8)) * span - let y = y_base + float(recs[o + 4] | (recs[o + 5] << 8)) * 0.01 - let scale = lo + float(recs[o + 6]) * sr - let yaw = float(recs[o + 7]) * (2.0 * PI / 256.0) - stream_emit(render3d_st, s, x, y, z, scale, yaw, float(recs[o + 8]) / 255.0, float(recs[o + 9]) / 255.0) + let o = i * SB7_REC + let x = x0 + float(sb7_field(recs, o, 0)) * span + let z = z0 + float(sb7_field(recs, o, 1)) * span + let y = y_base + float(sb7_field(recs, o, 2)) / 4095.0 * y_span + let scale = lo + float(sb7_field(recs, o, 3)) / 63.0 * (hi - lo) + let yaw = float(sb7_field(recs, o, 4)) * (2.0 * PI / 64.0) + stream_emit(render3d_st, s, x, y, z, scale, yaw, float(sb7_field(recs, o, 5)) / 15.0, float(sb7_field(recs, o, 6)) / 15.0) } }