From 8589a5a7b477a2fa92125de47cfa7f3389fe9d0b Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 17:28:11 +0300 Subject: [PATCH] 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 --- packages/ludic.render3d/env.ludic | 1 + packages/ludic.render3d/layer_chunks.ludic | 24 +++----- packages/ludic.render3d/place_rec.ludic | 68 ++++++++++++++++++++++ packages/ludic.render3d/r3d.ludic | 1 + packages/ludic.render3d/scatter.ludic | 1 + packages/ludic.render3d/stream_baked.ludic | 49 +++------------- 6 files changed, 86 insertions(+), 58 deletions(-) create mode 100644 packages/ludic.render3d/place_rec.ludic diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 4d4dde90..1e0a0e49 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -829,6 +829,7 @@ export state Render3dState { ter_ortho_tex: int = 0 # a photograph of the same window, draped with distance ter_carpet: int = 0 # the distant-grass carpet (carpet_bake), 0 = none ter_shadow_tex: int = 0 # height-field sun shadow: R32F, the lowest lit height + sb_rec: floats = floats(8) # one decoded baked record, reused (stream_baked.ludic) tt_dir: string = "" # terrain_tiles_to: where a map's tiles are written ("" keeps the whole copies) tt_key: string = "" tt_file: pointer = null # the tiles' file, open for reading (terrain_tiles.ludic) diff --git a/packages/ludic.render3d/layer_chunks.ludic b/packages/ludic.render3d/layer_chunks.ludic index 70a0b4ce..b2a3f1f0 100644 --- a/packages/ludic.render3d/layer_chunks.ludic +++ b/packages/ludic.render3d/layer_chunks.ludic @@ -1,9 +1,6 @@ # 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 +# and dropped when it goes, the list re-uploaded at most once a frame. A record's layout is place_rec.ludic's. # 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") @@ -12,30 +9,23 @@ export function layer_chunks_begin(l: Layer, n: int) -> void { 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) } + if l.lc_rec == null { l.lc_rec = floats(INST_FLOATS) } 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 { +# chunk `id`'s `count` records (format fmt, place_rec.ludic) at byte `at` of recs 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: []byte, at: int, fmt: int, 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 + prec_get(fmt, recs, at, i, x0, z0, size, y_base, lo, hi, l.lc_rec) + for k in 0 .. INST_FLOATS { l.inst[(l.count + i) * INST_FLOATS + k] = l.lc_rec[k] } } l.ck_off[id] = l.count l.ck_cnt[id] = n diff --git a/packages/ludic.render3d/place_rec.ludic b/packages/ludic.render3d/place_rec.ludic new file mode 100644 index 00000000..55541150 --- /dev/null +++ b/packages/ludic.render3d/place_rec.ludic @@ -0,0 +1,68 @@ +# place_rec.ludic — a baked placement's record, the ONE encoder and decoder the bake, the ring, the +# physics and the renderer all use (so they cannot drift). Two formats: FULL, 12 bytes, for the fixed +# kinds (trees, boulders, stones); PACKED, 7 bytes, for the stream kinds (grass, flowers, willow). An +# encode takes the cell a value falls in and a decode its centre, so decoding and encoding again is +# stable. A record is read from a []byte at a byte offset (the game cannot index a pointer). + +export const PREC_FULL: int = 0 +export const PREC_PACKED: int = 1 + +export function prec_size(fmt: int) -> int { + if fmt == PREC_PACKED { return 7 } + return 12 +} + +function prec_q(v: float, steps: int) -> int { return min(max(int(Math.floor(v * float(steps))), 0), steps - 1) } +function prec_r(v: float, top: int) -> int { return min(max(int(Math.floor(v * float(top) + 0.5)), 0), top) } +function prec_wrap(yaw: float) -> float { + let t = 2.0 * PI + return yaw - Math.floor(yaw / t) * t +} + +# one instance in world units into b at byte `at`, against its chunk (x0, z0, size, y_base) and kind (lo, hi) +export function prec_put(fmt: int, b: []byte, at: int, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float, x0: float, z0: float, size: float, y_base: float, lo: float, hi: float) -> void { + var sn = 0.0 + if hi > lo { sn = (scale - lo) / (hi - lo) } + if fmt == PREC_PACKED { + let qx = prec_q((x - x0) / size, 4096) + let qz = prec_q((z - z0) / size, 4096) + let qy = min(max(int(Math.floor((y - y_base) * 100.0 + 0.5)), 0), 4095) + let lw = qx | (qz << 12) | ((qy & 255) << 24) + let hw = ((qy >> 8) & 15) | (prec_r(sn, 63) << 4) | ((prec_r(prec_wrap(yaw) / (2.0 * PI), 64) & 63) << 10) | (prec_r(seed, 15) << 16) | (prec_r(wind, 15) << 20) + b[at] = lw & 255; b[at + 1] = (lw >> 8) & 255; b[at + 2] = (lw >> 16) & 255; b[at + 3] = (lw >> 24) & 255 + b[at + 4] = hw & 255; b[at + 5] = (hw >> 8) & 255; b[at + 6] = (hw >> 16) & 255 + return + } + let qx = prec_q((x - x0) / size, 65536) + let qz = prec_q((z - z0) / size, 65536) + let qy = min(max(int(Math.floor((y - y_base) * 100.0 + 0.5)), 0), 65535) + b[at] = qx & 255; b[at + 1] = qx >> 8; b[at + 2] = qz & 255; b[at + 3] = qz >> 8 + b[at + 4] = qy & 255; b[at + 5] = qy >> 8 + b[at + 6] = prec_r(sn, 255); b[at + 7] = prec_r(prec_wrap(yaw) / (2.0 * PI), 256) & 255 + b[at + 8] = prec_r(seed, 255); b[at + 9] = prec_r(wind, 255); b[at + 10] = 0; b[at + 11] = 0 +} + +# record i of the run at byte `at` into out[0 .. 8], the layers' instance layout: +# x, y, z, scale, sin yaw, cos yaw, seed, wind +export function prec_get(fmt: int, b: []byte, at: int, i: int, x0: float, z0: float, size: float, y_base: float, lo: float, hi: float, out: floats) -> void { + let o = at + i * prec_size(fmt) + var qx = 0; var qz = 0; var qy = 0; var qs = 0; var qa = 0; var qe = 0; var qw = 0 + var xs = 65536.0; var ss = 255.0; var as = 256.0; var es = 255.0 + if fmt == PREC_PACKED { + let lw = b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24) + let hw = b[o + 4] | (b[o + 5] << 8) | (b[o + 6] << 16) + qx = lw & 4095; qz = (lw >> 12) & 4095; qy = ((lw >> 24) & 255) | ((hw & 15) << 8) + qs = (hw >> 4) & 63; qa = (hw >> 10) & 63; qe = (hw >> 16) & 15; qw = (hw >> 20) & 15 + xs = 4096.0; ss = 63.0; as = 64.0; es = 15.0 + } else { + qx = b[o] | (b[o + 1] << 8); qz = b[o + 2] | (b[o + 3] << 8); qy = b[o + 4] | (b[o + 5] << 8) + qs = b[o + 6]; qa = b[o + 7]; qe = b[o + 8]; qw = b[o + 9] + } + let yaw = float(qa) / as * 2.0 * PI + out[0] = x0 + (float(qx) + 0.5) * size / xs + out[1] = y_base + float(qy) * 0.01 + out[2] = z0 + (float(qz) + 0.5) * size / xs + out[3] = lo + float(qs) / ss * (hi - lo) + out[4] = Math.sin(yaw); out[5] = Math.cos(yaw) + out[6] = float(qe) / es; out[7] = float(qw) / es +} diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 707a11a5..c870cf20 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -48,6 +48,7 @@ import "layer_chunks.ludic" import "fog_impostors.ludic" import "actor.ludic" import "stream.ludic" +import "place_rec.ludic" import "stream_baked.ludic" import "fog_streams.ludic" import "grass.ludic" diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 9956b336..c94577e3 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -41,6 +41,7 @@ property Layer { fog_src: int = -1, ck_off: words, # per baked chunk: where its slab starts in inst, or -1 (layer_chunks.ludic) ck_cnt: words, + lc_rec: floats, # one decoded record, reused 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 40ac01af..aadafa51 100644 --- a/packages/ludic.render3d/stream_baked.ludic +++ b/packages/ludic.render3d/stream_baked.ludic @@ -1,7 +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. The stream kinds' records are 7 bytes (below); the fixed layers' 12 (layer_chunks.ludic). +# does. A record's layout is place_rec.ludic's. # 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 { @@ -9,46 +9,13 @@ export function stream_set_grid(s: Stream, ox: float, oz: float) -> void { s.oz = oz } -# ---- 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 / 4096.0 - for i in 0 .. n { - 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) +# the first `keep` of a chunk's `count` records (format fmt, place_rec.ludic) at byte `at` of recs +# emitted into the chunk being filled +export function stream_emit_baked(render3d_st: mut Render3dState, s: Stream, recs: []byte, at: int, fmt: int, count: int, keep: int, x0: float, z0: float, y_base: float, lo: float, hi: float) -> void { + let v = render3d_st.sb_rec + for i in 0 .. min(keep, count) { + prec_get(fmt, recs, at, i, x0, z0, s.size, y_base, lo, hi, v) + stream_emit(render3d_st, s, v[0], v[1], v[2], v[3], Math.atan2(v[4], v[5]), v[6], v[7]) } }