diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 59eb573b..c3936e6b 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -36,6 +36,7 @@ import "terrain_height_file.ludic" import "terrain_tiles_cut.ludic" import "terrain_ltt2_write.ludic" import "terrain_ltt2_read.ludic" +import "terrain_tiles_open.ludic" import "terrain_baked_boot.ludic" import "terrain_pages.ludic" import "terrain_pages_frame.ludic" diff --git a/packages/ludic.render3d/terrain_tiles_open.ludic b/packages/ludic.render3d/terrain_tiles_open.ludic new file mode 100644 index 00000000..ad38151f --- /dev/null +++ b/packages/ludic.render3d/terrain_tiles_open.ludic @@ -0,0 +1,19 @@ +# terrain_tiles_open.ludic — a map's baked tiles opened for the CPU questions alone (terrain_height, +# terrain_height_file, _smooth, terrain_ortho), with no device and no renderer booted: for a bake, a +# migration or a test that asks the ground of a map it will never draw. + +# the bake at `path` (its key and version as terrain_from_baked takes them) opened for reading, the map +# `half` metres each way from (ox, oz); false (nothing changed) when it is not this bake, or when the +# whole copy is kept - that one answers first, so opening tiles under it would be read by nothing +@alloc_ok("a map's tiles opened once, for a bake or a test") +export function terrain_tiles_open_file(render3d_st: mut Render3dState, path: string, key: string, version: int, half: int, ox: float, oz: float) -> bool { + if render3d_st.ter_heights != null { return false } + let base = ltt2_bake_base(path, key, version) + if base < 0 { return false } + render3d_st.TERRAIN_HALF = half + render3d_st.ter_ox = ox + render3d_st.ter_oz = oz + render3d_st.ter_h_scale = 0.0 + render3d_st.ter_o_scale = 0.0 + return tt_open_ltt2(render3d_st, path, base) +} diff --git a/packages/ludic.render3d/tests/fakes/tiles_file.ludic b/packages/ludic.render3d/tests/fakes/tiles_file.ludic new file mode 100644 index 00000000..220dfc73 --- /dev/null +++ b/packages/ludic.render3d/tests/fakes/tiles_file.ludic @@ -0,0 +1,136 @@ +# fakes/tiles_file.ludic - a map's tiles written by hand for render3d's tests: an LTT2 payload of heights and +# a photograph from formulas (hf_q, hf_rgb), raw or inside a bake's header, what terrain_height must answer +# over it, and the page cache's state to hold a question to having changed nothing +const HF_SIDE: int = 2 # photograph texels a tile a side + +# the u16 written at texel i of tile t, and what texel (tx, tz) decodes to +function hf_q(t: int, i: int) -> int { return (i * 37 + (i >> 5) * 11 + t * 5) & 65535 } +function hf_texel(tx: int, tz: int) -> float { + let n = TERRAIN_RES / TT_TEX + let t = (tz >> 5) * n + (tx >> 5) + let q = hf_q(t, (tz & 31) * TT_TEX + (tx & 31)) + return (float(t % 50) * 3.5 - 40.0) + float(q) * (0.01 + float(t % 7) * 0.001) +} +# the photograph texel (px, pz) as written +function hf_rgb(px: int, pz: int) -> int { + let n = TERRAIN_RES / TT_TEX + let t = (pz / HF_SIDE) * n + px / HF_SIDE + let i = (pz % HF_SIDE) * HF_SIDE + px % HF_SIDE + return (((t * 3 + i) & 255) << 16) | (((t >> 3) & 255) << 8) | (i * 60) +} + +# an LTT2 payload of heights and a photograph (the normals and the coarse level read short, unused here) +function hf_payload() -> []byte { + let n = TERRAIN_RES / TT_TEX + let n2 = TT_TEX * TT_TEX + let o2 = HF_SIDE * HF_SIDE + let hat = LT_HEAD + n * n * 8 + let oat = hat + n * n * n2 * 2 + let size = oat + n * n * o2 * 3 + let b = buffer(size) + for k in 0 .. LT_HEAD { b[k] = 0 } + hf_put(b, 0, LT_MAGIC) + hf_put(b, 4, LT_VERSION) + hf_put(b, 8, TT_TEX) + hf_put(b, 12, n) + hf_put(b, 16, HF_SIDE) + hf_put(b, 20, TERRAIN_RES) + hf_put(b, 24, n * HF_SIDE) + hf_put(b, 28, TT_COARSE) + hf_put(b, 32, LT_HEAD) + hf_put(b, 40, size) + hf_put(b, 44, oat) + for k in 12 .. 15 { hf_put(b, k * 4, size) } + for t in 0 .. n * n { + hf_put(b, LT_HEAD + t * 8, float_bits(float(t % 50) * 3.5 - 40.0)) + hf_put(b, LT_HEAD + t * 8 + 4, float_bits(0.01 + float(t % 7) * 0.001)) + for i in 0 .. n2 { + b[hat + (t * n2 + i) * 2] = hf_q(t, i) & 255 + b[hat + (t * n2 + i) * 2 + 1] = hf_q(t, i) >> 8 + } + for i in 0 .. o2 { + let c = hf_rgb((t % n) * HF_SIDE + i % HF_SIDE, (t / n) * HF_SIDE + i / HF_SIDE) + b[oat + (t * o2 + i) * 3] = c >> 16 + b[oat + (t * o2 + i) * 3 + 1] = (c >> 8) & 255 + b[oat + (t * o2 + i) * 3 + 2] = c & 255 + } + } + return b +} +function hf_write(path: string) -> bool { + let b = hf_payload() + return Fs.write_bytes(path, b, len(b)) +} +# the payload inside a bake's header (ludic.base's LBAK: format 1, the version, the payload's offset after +# the key and its zero padded to 8, the inputs' hash, the length), as terrain_tiles_bake writes it +function hf_bake(path: string, key: string, version: int) -> bool { + let p = hf_payload() + let base = 32 + (len(key) + 1 + 7) / 8 * 8 + let b = buffer(base + len(p)) + for k in 0 .. base { b[k] = 0 } + hf_put(b, 0, BAKE_MAGIC) + hf_put(b, 4, 1) + hf_put(b, 8, version) + hf_put(b, 12, base) + hf_put(b, 24, len(p)) + for k in 0 .. len(key) { b[32 + k] = key[k] } + for k in 0 .. len(p) { b[base + k] = p[k] } + return Fs.write_bytes(path, b, base + len(p)) +} +function hf_put(b: []byte, at: int, v: int) -> void { + for k in 0 .. 4 { b[at + k] = (v >> (k * 8)) & 255 } +} + +# terrain_height's bilinear, over the texels as written +function hf_expect(x: float, z: float) -> float { return hf_expect_at(x, z, 0.0, 0.0) } +function hf_expect_at(x: float, z: float, ox: float, oz: float) -> float { + let fx = (x - ox + 4096.0) * float(TERRAIN_RES) / 8192.0 + let fz = (z - oz + 4096.0) * float(TERRAIN_RES) / 8192.0 + let ix = min(max(int(Math.floor(fx)), 0), TERRAIN_RES - 2) + let iz = min(max(int(Math.floor(fz)), 0), TERRAIN_RES - 2) + let tx = Math.clamp(fx - float(ix), 0.0, 1.0) + let tz = Math.clamp(fz - float(iz), 0.0, 1.0) + let a = Math.lerp(hf_texel(ix, iz), hf_texel(ix + 1, iz), tx) + return Math.lerp(a, Math.lerp(hf_texel(ix, iz + 1), hf_texel(ix + 1, iz + 1), tx), tz) +} + +# the page cache as it stands: which tile each slot holds, the clock, the heights, the read counts +property HfSnap { + slot_of: words = null, + tile_in: words = null, + refs: words = null, + h: floats = null, + o: words = null, + hand: int = 0, + reads: int = 0, + frame_reads: int = 0 +} +function hf_snap(render3d_st: Render3dState) -> HfSnap { + let a = new HfSnap + a.slot_of = words(len(render3d_st.tt_slot_of)) + for t in 0 .. len(a.slot_of) { a.slot_of[t] = render3d_st.tt_slot_of[t] } + a.tile_in = words(len(render3d_st.tt_tile_in)) + a.refs = words(len(render3d_st.tt_tile_in)) + for s in 0 .. len(a.tile_in) { + a.tile_in[s] = render3d_st.tt_tile_in[s] + a.refs[s] = render3d_st.tt_ref[s] + } + a.h = floats(len(render3d_st.tt_h)) + for i in 0 .. len(a.h) { a.h[i] = render3d_st.tt_h[i] } + a.o = words(len(render3d_st.tt_o)) + for i in 0 .. len(a.o) { a.o[i] = render3d_st.tt_o[i] } + a.hand = render3d_st.tt_hand + a.reads = render3d_st.tt_reads + a.frame_reads = render3d_st.tt_frame_reads + return a +} +# how many of those differ now +function hf_moved(render3d_st: Render3dState, a: HfSnap) -> int { + var moved = 0 + for t in 0 .. len(a.slot_of) { if render3d_st.tt_slot_of[t] != a.slot_of[t] { moved += 1 } } + for s in 0 .. len(a.tile_in) { if render3d_st.tt_tile_in[s] != a.tile_in[s] or render3d_st.tt_ref[s] != a.refs[s] { moved += 1 } } + for i in 0 .. len(a.h) { if render3d_st.tt_h[i] != a.h[i] { moved += 1 } } + for i in 0 .. len(a.o) { if render3d_st.tt_o[i] != a.o[i] { moved += 1 } } + if render3d_st.tt_hand != a.hand or render3d_st.tt_reads != a.reads or render3d_st.tt_frame_reads != a.frame_reads { moved += 1 } + return moved +} \ No newline at end of file diff --git a/packages/ludic.render3d/tests/height_file_test.ludic b/packages/ludic.render3d/tests/height_file_test.ludic index a3199bd3..acb281a9 100644 --- a/packages/ludic.render3d/tests/height_file_test.ludic +++ b/packages/ludic.render3d/tests/height_file_test.ludic @@ -4,69 +4,10 @@ import "ludic.render3d/r3d.ludic" program HeightFileTest { numbers float + import "fakes/tiles_file.ludic" # never called: naming Input links the engine's runtime, which render3d's textures stand on function links_runtime() -> bool { return Input.key_pressed(0) } - const HF_SIDE: int = 2 # photograph texels a tile a side - - # the u16 written at texel i of tile t, and what texel (tx, tz) decodes to - function hf_q(t: int, i: int) -> int { return (i * 37 + (i >> 5) * 11 + t * 5) & 65535 } - function hf_texel(tx: int, tz: int) -> float { - let n = TERRAIN_RES / TT_TEX - let t = (tz >> 5) * n + (tx >> 5) - let q = hf_q(t, (tz & 31) * TT_TEX + (tx & 31)) - return (float(t % 50) * 3.5 - 40.0) + float(q) * (0.01 + float(t % 7) * 0.001) - } - # the photograph texel (px, pz) as written - function hf_rgb(px: int, pz: int) -> int { - let n = TERRAIN_RES / TT_TEX - let t = (pz / HF_SIDE) * n + px / HF_SIDE - let i = (pz % HF_SIDE) * HF_SIDE + px % HF_SIDE - return (((t * 3 + i) & 255) << 16) | (((t >> 3) & 255) << 8) | (i * 60) - } - - # an LTT2 file of heights and a photograph (the normals and the coarse level read short, unused here) - function hf_write(path: string) -> bool { - let n = TERRAIN_RES / TT_TEX - let n2 = TT_TEX * TT_TEX - let o2 = HF_SIDE * HF_SIDE - let hat = LT_HEAD + n * n * 8 - let oat = hat + n * n * n2 * 2 - let size = oat + n * n * o2 * 3 - let b = buffer(size) - for k in 0 .. LT_HEAD { b[k] = 0 } - hf_put(b, 0, LT_MAGIC) - hf_put(b, 4, LT_VERSION) - hf_put(b, 8, TT_TEX) - hf_put(b, 12, n) - hf_put(b, 16, HF_SIDE) - hf_put(b, 20, TERRAIN_RES) - hf_put(b, 24, n * HF_SIDE) - hf_put(b, 28, TT_COARSE) - hf_put(b, 32, LT_HEAD) - hf_put(b, 40, size) - hf_put(b, 44, oat) - for k in 12 .. 15 { hf_put(b, k * 4, size) } - for t in 0 .. n * n { - hf_put(b, LT_HEAD + t * 8, float_bits(float(t % 50) * 3.5 - 40.0)) - hf_put(b, LT_HEAD + t * 8 + 4, float_bits(0.01 + float(t % 7) * 0.001)) - for i in 0 .. n2 { - b[hat + (t * n2 + i) * 2] = hf_q(t, i) & 255 - b[hat + (t * n2 + i) * 2 + 1] = hf_q(t, i) >> 8 - } - for i in 0 .. o2 { - let c = hf_rgb((t % n) * HF_SIDE + i % HF_SIDE, (t / n) * HF_SIDE + i / HF_SIDE) - b[oat + (t * o2 + i) * 3] = c >> 16 - b[oat + (t * o2 + i) * 3 + 1] = (c >> 8) & 255 - b[oat + (t * o2 + i) * 3 + 2] = c & 255 - } - } - return Fs.write_bytes(path, b, size) - } - function hf_put(b: []byte, at: int, v: int) -> void { - for k in 0 .. 4 { b[at + k] = (v >> (k * 8)) & 255 } - } - function hf_open(render3d_st: mut Render3dState, tag: string) -> bool { let path = Os.temp_dir() + "/r3d_height_file_" + tag + ".ltt2" if not hf_write(path) { return false } @@ -78,18 +19,6 @@ program HeightFileTest { return tt_open_ltt2(render3d_st, path, 0) } - # terrain_height's bilinear, over the texels as written - function hf_expect(x: float, z: float) -> float { - let fx = (x + 4096.0) * float(TERRAIN_RES) / 8192.0 - let fz = (z + 4096.0) * float(TERRAIN_RES) / 8192.0 - let ix = min(max(int(Math.floor(fx)), 0), TERRAIN_RES - 2) - let iz = min(max(int(Math.floor(fz)), 0), TERRAIN_RES - 2) - let tx = Math.clamp(fx - float(ix), 0.0, 1.0) - let tz = Math.clamp(fz - float(iz), 0.0, 1.0) - let a = Math.lerp(hf_texel(ix, iz), hf_texel(ix + 1, iz), tx) - return Math.lerp(a, Math.lerp(hf_texel(ix, iz + 1), hf_texel(ix + 1, iz + 1), tx), tz) - } - test "heights and the photograph are the file's texels, whether their tile is resident or not" (render3d_st: mut Render3dState) { expect(hf_open(render3d_st, "same")) terrain_tiles_prefetch(render3d_st, 1000.0, -500.0, 200.0, 100) @@ -198,44 +127,4 @@ program HeightFileTest { } return Fs.write_bytes(path, b, size) and gd_open_at(render3d_st, path, 0) } - # the page cache as it stands: which tile each slot holds, the clock, the heights, the read counts - property HfSnap { - slot_of: words = null, - tile_in: words = null, - refs: words = null, - h: floats = null, - o: words = null, - hand: int = 0, - reads: int = 0, - frame_reads: int = 0 - } - function hf_snap(render3d_st: Render3dState) -> HfSnap { - let a = new HfSnap - a.slot_of = words(len(render3d_st.tt_slot_of)) - for t in 0 .. len(a.slot_of) { a.slot_of[t] = render3d_st.tt_slot_of[t] } - a.tile_in = words(len(render3d_st.tt_tile_in)) - a.refs = words(len(render3d_st.tt_tile_in)) - for s in 0 .. len(a.tile_in) { - a.tile_in[s] = render3d_st.tt_tile_in[s] - a.refs[s] = render3d_st.tt_ref[s] - } - a.h = floats(len(render3d_st.tt_h)) - for i in 0 .. len(a.h) { a.h[i] = render3d_st.tt_h[i] } - a.o = words(len(render3d_st.tt_o)) - for i in 0 .. len(a.o) { a.o[i] = render3d_st.tt_o[i] } - a.hand = render3d_st.tt_hand - a.reads = render3d_st.tt_reads - a.frame_reads = render3d_st.tt_frame_reads - return a - } - # how many of those differ now - function hf_moved(render3d_st: Render3dState, a: HfSnap) -> int { - var moved = 0 - for t in 0 .. len(a.slot_of) { if render3d_st.tt_slot_of[t] != a.slot_of[t] { moved += 1 } } - for s in 0 .. len(a.tile_in) { if render3d_st.tt_tile_in[s] != a.tile_in[s] or render3d_st.tt_ref[s] != a.refs[s] { moved += 1 } } - for i in 0 .. len(a.h) { if render3d_st.tt_h[i] != a.h[i] { moved += 1 } } - for i in 0 .. len(a.o) { if render3d_st.tt_o[i] != a.o[i] { moved += 1 } } - if render3d_st.tt_hand != a.hand or render3d_st.tt_reads != a.reads or render3d_st.tt_frame_reads != a.frame_reads { moved += 1 } - return moved - } } diff --git a/packages/ludic.render3d/tests/tiles_open_test.ludic b/packages/ludic.render3d/tests/tiles_open_test.ludic new file mode 100644 index 00000000..eabb26f6 --- /dev/null +++ b/packages/ludic.render3d/tests/tiles_open_test.ludic @@ -0,0 +1,49 @@ +# tiles_open_test.ludic - terrain_tiles_open_file: a map's baked tiles opened with no device and no renderer +# booted answer the heights and the photograph written, at the map's own origin; another bake's key or +# version, or a whole copy already kept, is refused and changes nothing +import "ludic.render3d/r3d.ludic" +program TilesOpenTest { + numbers float + import "fakes/tiles_file.ludic" + # never called: naming Input links the engine's runtime, which render3d's textures stand on + function links_runtime() -> bool { return Input.key_pressed(0) } + + function baked(tag: string) -> string { + let path = Os.temp_dir() + "/r3d_tiles_open_" + tag + ".tiles" + expect(hf_bake(path, "terrain", 1)) + return path + } + + test "a bake opened with no device answers its heights and photograph at the map's origin" (render3d_st: mut Render3dState) { + expect(terrain_tiles_open_file(render3d_st, baked("answers"), "terrain", 1, 4096, 300.0, -120.0)) + var off = 0 + for j in 0 .. 90 { + for i in 0 .. 90 { + let x = 300.0 - 4000.0 + float(i) * 89.3 + let z = -120.0 - 4000.0 + float(j) * 88.9 + if terrain_height(render3d_st, x, z) != hf_expect_at(x, z, 300.0, -120.0) { off += 1 } + if terrain_height_file(render3d_st, x, z) != hf_expect_at(x, z, 300.0, -120.0) { off += 1 } + let px = min(max(int(Math.floor((x - 300.0 + 4096.0) * 256.0 / 8192.0)), 0), 255) + let pz = min(max(int(Math.floor((z + 120.0 + 4096.0) * 256.0 / 8192.0)), 0), 255) + if terrain_ortho(render3d_st, x, z) != hf_rgb(px, pz) { off += 1 } + } + } + expect_eq(off, 0) + expect(terrain_height_smooth(render3d_st, 300.0, -120.0) != 0.0) + expect_eq(render3d_st.gvk_frame_no, 0) # nothing drew, nothing was made on a device + } + + test "another bake's key or version, a missing file, or a whole copy kept is refused and changes nothing" (render3d_st: mut Render3dState) { + let path = baked("refused") + expect(not terrain_tiles_open_file(render3d_st, path, "ground", 1, 4096, 0.0, 0.0)) + expect(not terrain_tiles_open_file(render3d_st, path, "terrain", 2, 4096, 0.0, 0.0)) + expect(not terrain_tiles_open_file(render3d_st, Os.temp_dir() + "/r3d_tiles_open_none.tiles", "terrain", 1, 4096, 0.0, 0.0)) + expect(render3d_st.tt_file == null) + render3d_st.ter_heights = floats(16) + expect(not terrain_tiles_open_file(render3d_st, path, "terrain", 1, 4096, 0.0, 0.0)) + expect(render3d_st.tt_file == null) + render3d_st.ter_heights = null + expect(terrain_tiles_open_file(render3d_st, path, "terrain", 1, 4096, 0.0, 0.0)) + expect_eq(terrain_height(render3d_st, 10.0, 10.0), hf_expect(10.0, 10.0)) + } +}