From 130b9091d146f7daff4df03a2ac8f2034dd15dbe Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Wed, 30 Sep 2026 02:43:17 +0300 Subject: [PATCH] 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 Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/env.ludic | 5 +- .../ludic.render3d/terrain_height_file.ludic | 67 ++--- .../ludic.render3d/terrain_ltt2_read.ludic | 1 + .../ludic.render3d/terrain_pages_fill.ludic | 6 +- packages/ludic.render3d/terrain_tiles.ludic | 37 ++- .../tests/height_file_test.ludic | 234 +++++++++++++----- 6 files changed, 249 insertions(+), 101 deletions(-) diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 0b10dc2d..991c1a53 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -889,10 +889,13 @@ export state Render3dState { tt_reads: int = 0 # tiles read from the file, over the run and this frame tt_frame_reads: int = 0 tt_warned: bool = false - tf_raw: []byte = buffer(2048) # terrain_height_file's own: one tile's bytes as read, + tf_raw: []byte = buffer(2048) # the questions' file scratch (terrain_height_file.ludic): a tile's bytes as read, tf_h: floats = floats(4096) # ... TF_KEEP tiles' heights, and which (tile + 1, 0: none) tf_tile: words = words(4) tf_next: int = 0 + tfo_px: words = words(4) # ... and TF_KEEP tiles' photograph (sized when a map opens), and which + tfo_tile: words = words(4) + tfo_next: int = 0 tp_on: bool = false # the GPU pages the fine tiles round the camera (terrain_pages.ludic) tp_page_tex: int = 0 # R32F tt_n a side: slot + 1 of each resident tile, 0 = the coarse level tp_h_tex: int = 0 # the pool: heights, normals and photograph, a layer a slot diff --git a/packages/ludic.render3d/terrain_height_file.ludic b/packages/ludic.render3d/terrain_height_file.ludic index 69d664ea..88767627 100644 --- a/packages/ludic.render3d/terrain_height_file.ludic +++ b/packages/ludic.render3d/terrain_height_file.ludic @@ -1,35 +1,11 @@ -# terrain_height_file.ludic — terrain_height's exact answer that never pages: a tile not resident is -# read from the file into a scratch of this query's own, so who asks and in what order never changes -# which tiles the cache holds (and so never moves what is drawn from it). For wide sweeps at boot. +# terrain_height_file.ludic — the file scratch the terrain's questions read a tile through when it is not +# resident (ter_h, ter_o), so a question never takes a slot: who asks and in what order leaves the cache +# as the loaders put it. terrain_height_file stays as the name callers wrote against; it is terrain_height. const TF_KEEP: int = 4 # tiles the scratch keeps: a texel square at a corner spans four -export function terrain_height_file(render3d_st: mut Render3dState, x: float, z: float) -> float { - if not ter_present(render3d_st) { return 0.0 } - let scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) - let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale - let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale - 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 h00 = tf_texel(render3d_st, ix, iz) - let h10 = tf_texel(render3d_st, ix + 1, iz) - let h01 = tf_texel(render3d_st, ix, iz + 1) - let h11 = tf_texel(render3d_st, ix + 1, iz + 1) - return Math.lerp(Math.lerp(h00, h10, tx), Math.lerp(h01, h11, tx), tz) -} +export function terrain_height_file(render3d_st: mut Render3dState, x: float, z: float) -> float { return terrain_height(render3d_st, x, z) } -# a height texel: the whole copy's, a resident tile's, else the scratch's (read from the file there) -function tf_texel(render3d_st: mut Render3dState, tx: int, tz: int) -> float { - if render3d_st.ter_heights != null { return render3d_st.ter_heights[tz * TERRAIN_RES + tx] } - let t = (tz >> TT_SHIFT) * render3d_st.tt_n + (tx >> TT_SHIFT) - let at = (tz & TT_MASK) * TT_TEX + (tx & TT_MASK) - let s = render3d_st.tt_slot_of[t] - if s >= 0 { return render3d_st.tt_h[s * TT_TEX * TT_TEX + at] } - return render3d_st.tf_h[tf_entry(render3d_st, t) * TT_TEX * TT_TEX + at] -} - -# the scratch entry holding tile t, read from the file (as tt_decode reads its heights) when none does +# the scratch entry holding tile t's heights, read from the file (as tt_decode reads them) when none does function tf_entry(render3d_st: mut Render3dState, t: int) -> int { for e in 0 .. TF_KEEP { if render3d_st.tf_tile[e] == t + 1 { return e } } let e = render3d_st.tf_next @@ -45,8 +21,39 @@ function tf_entry(render3d_st: mut Render3dState, t: int) -> int { return e } +# ... and of its photograph (the scratch sized to the map's tiles when they were opened, tf_size) +function tfo_entry(render3d_st: mut Render3dState, t: int) -> int { + for e in 0 .. TF_KEEP { if render3d_st.tfo_tile[e] == t + 1 { return e } } + let e = render3d_st.tfo_next + render3d_st.tfo_next = (e + 1) % TF_KEEP + let o2 = render3d_st.tt_oside * render3d_st.tt_oside + let raw = render3d_st.tf_raw + file_seek(render3d_st.tt_file, render3d_st.tt_ooff + t * o2 * 3, 0) + file_read(render3d_st.tt_file, data_of(raw), o2 * 3) + for i in 0 .. o2 { render3d_st.tfo_px[e * o2 + i] = (raw[i * 3] << 16) | (raw[i * 3 + 1] << 8) | raw[i * 3 + 2] } + render3d_st.tfo_tile[e] = t + 1 + return e +} + +# the photograph's scratch and the read buffer made for `side` texels a tile, once a size +@alloc_ok("a map being loaded: the questions' photograph scratch, once a size") +function tf_size(render3d_st: mut Render3dState, side: int) -> void { + if len(render3d_st.tfo_px) < TF_KEEP * side * side { + free(render3d_st.tfo_px) + render3d_st.tfo_px = words(TF_KEEP * side * side) + } + if len(render3d_st.tf_raw) < side * side * 3 { + free(render3d_st.tf_raw) + render3d_st.tf_raw = buffer(side * side * 3) + } +} + # the scratch forgotten: the tiles' file closed or replaced (another map's tile t is another tile) function tf_forget(render3d_st: mut Render3dState) -> void { - for e in 0 .. TF_KEEP { render3d_st.tf_tile[e] = 0 } + for e in 0 .. TF_KEEP { + render3d_st.tf_tile[e] = 0 + render3d_st.tfo_tile[e] = 0 + } render3d_st.tf_next = 0 + render3d_st.tfo_next = 0 } diff --git a/packages/ludic.render3d/terrain_ltt2_read.ludic b/packages/ludic.render3d/terrain_ltt2_read.ludic index 6f5a4f3d..5aaf2a35 100644 --- a/packages/ludic.render3d/terrain_ltt2_read.ludic +++ b/packages/ludic.render3d/terrain_ltt2_read.ludic @@ -55,6 +55,7 @@ function tt_pools(render3d_st: mut Render3dState, n: int, side: int) -> void { if render3d_st.tt_tile_in == null { render3d_st.tt_tile_in = words(TT_SLOTS); render3d_st.tt_ref = words(TT_SLOTS) } for s in 0 .. TT_SLOTS { render3d_st.tt_tile_in[s] = -1; render3d_st.tt_ref[s] = 0 } render3d_st.tt_hand = 0 + tf_size(render3d_st, side) } # tile t's heights, normals and photograph decoded into slot s: exactly the values ltt2_write put back diff --git a/packages/ludic.render3d/terrain_pages_fill.ludic b/packages/ludic.render3d/terrain_pages_fill.ludic index e4980b4a..e13352f0 100644 --- a/packages/ludic.render3d/terrain_pages_fill.ludic +++ b/packages/ludic.render3d/terrain_pages_fill.ludic @@ -18,8 +18,8 @@ function tp_fill(render3d_st: mut Render3dState, t: int, base: int, k: int) -> v for a in 0 .. hs { let tx = min(max(i0 + a, 0), TERRAIN_RES - 1) let o = (b * hs + a) * 4 - Vk.put_i32(p, ho + o, float_bits(ter_h(render3d_st, tx, tz))) - Vk.put_i32(p, no + o, ter_n(render3d_st, tx, tz)) + Vk.put_i32(p, ho + o, float_bits(tt_h_load(render3d_st, tx, tz))) + Vk.put_i32(p, no + o, tt_n_load(render3d_st, tx, tz)) } } tp_fill_ortho(render3d_st, t, base + n * n * 4 + 2 * TP_BUDGET * hb, k) @@ -38,7 +38,7 @@ function tp_fill_ortho(render3d_st: mut Render3dState, t: int, at: int, k: int) for b in 0 .. os { let pz = min(max(j0 + b, 0), top) for a in 0 .. os { - let v = ter_o(render3d_st, min(max(i0 + a, 0), top), pz) + let v = tt_o_load(render3d_st, min(max(i0 + a, 0), top), pz) let o = o0 + (b * os + a) * 4 p[o] = (v >> 16) & 255 p[o + 1] = (v >> 8) & 255 diff --git a/packages/ludic.render3d/terrain_tiles.ludic b/packages/ludic.render3d/terrain_tiles.ludic index 87b2364a..f392d4ff 100644 --- a/packages/ludic.render3d/terrain_tiles.ludic +++ b/packages/ludic.render3d/terrain_tiles.ludic @@ -17,16 +17,41 @@ export function terrain_tiles_to(render3d_st: mut Render3dState, dir: string, ke render3d_st.tt_key = intern(key) } -# the height texel (tx, tz): the whole copy's, or its tile's +# The texels as every question reads them: the whole copy, a resident tile, else the file through a +# scratch of the question's own (terrain_height_file.ludic). A question never takes a slot or moves the +# clock, so who asks and in what order leaves the cache as the loaders (tt_*_load) put it. + +# the height texel (tx, tz) function ter_h(render3d_st: mut Render3dState, tx: int, tz: int) -> float { + if render3d_st.ter_heights != null { return render3d_st.ter_heights[tz * TERRAIN_RES + tx] } + let t = (tz >> TT_SHIFT) * render3d_st.tt_n + (tx >> TT_SHIFT) + if render3d_st.tt_file == null or t < 0 or t >= len(render3d_st.tt_slot_of) { return 0.0 } + let at = (tz & TT_MASK) * TT_TEX + (tx & TT_MASK) + let s = render3d_st.tt_slot_of[t] + if s >= 0 { return render3d_st.tt_h[s * TT_TEX * TT_TEX + at] } + return render3d_st.tf_h[tf_entry(render3d_st, t) * TT_TEX * TT_TEX + at] +} + +# the photograph's texel (px, pz) as 0xRRGGBB +function ter_o(render3d_st: mut Render3dState, px: int, pz: int) -> int { + let side = render3d_st.tt_oside + if side == 0 or render3d_st.tt_file == null { return 0 } + let t = (pz / side) * render3d_st.tt_n + (px / side) + if t < 0 or t >= len(render3d_st.tt_slot_of) { return 0 } + let at = (pz % side) * side + (px % side) + let s = render3d_st.tt_slot_of[t] + if s >= 0 { return render3d_st.tt_o[s * side * side + at] } + return render3d_st.tfo_px[tfo_entry(render3d_st, t) * side * side + at] +} + +# ---- the loaders' reads: the GPU's pages fill from these, so what they need is kept resident ---- +function tt_h_load(render3d_st: mut Render3dState, tx: int, tz: int) -> float { if render3d_st.ter_heights != null { return render3d_st.ter_heights[tz * TERRAIN_RES + tx] } let s = tt_slot(render3d_st, (tz >> TT_SHIFT) * render3d_st.tt_n + (tx >> TT_SHIFT)) if s < 0 { return 0.0 } return render3d_st.tt_h[s * TT_TEX * TT_TEX + (tz & TT_MASK) * TT_TEX + (tx & TT_MASK)] } - -# the photograph's texel (px, pz) as 0xRRGGBB -function ter_o(render3d_st: mut Render3dState, px: int, pz: int) -> int { +function tt_o_load(render3d_st: mut Render3dState, px: int, pz: int) -> int { let side = render3d_st.tt_oside if side == 0 { return 0 } let s = tt_slot(render3d_st, (pz / side) * render3d_st.tt_n + (px / side)) @@ -35,7 +60,7 @@ function ter_o(render3d_st: mut Render3dState, px: int, pz: int) -> int { } # the baked normal at height texel (tx, tz): x and z as two halves in a word (RG16F), for the GPU's tiles -function ter_n(render3d_st: mut Render3dState, tx: int, tz: int) -> int { +function tt_n_load(render3d_st: mut Render3dState, tx: int, tz: int) -> int { let s = tt_slot(render3d_st, (tz >> TT_SHIFT) * render3d_st.tt_n + (tx >> TT_SHIFT)) if s < 0 { return 0 } return render3d_st.tt_nm[s * TT_TEX * TT_TEX + (tz & TT_MASK) * TT_TEX + (tx & TT_MASK)] @@ -44,7 +69,7 @@ function ter_n(render3d_st: mut Render3dState, tx: int, tz: int) -> int { # is there a height field to ask at all (a plate has none) function ter_present(render3d_st: Render3dState) -> bool { return render3d_st.ter_heights != null or render3d_st.tt_file != null } -# the slot tile t is in, read from the file first when it is not +# the slot tile t is in, read from the file first when it is not: the loaders' alone function tt_slot(render3d_st: mut Render3dState, t: int) -> int { if render3d_st.tt_file == null or t < 0 or t >= len(render3d_st.tt_slot_of) { return -1 } let s = render3d_st.tt_slot_of[t] diff --git a/packages/ludic.render3d/tests/height_file_test.ludic b/packages/ludic.render3d/tests/height_file_test.ludic index 24c33073..a3199bd3 100644 --- a/packages/ludic.render3d/tests/height_file_test.ludic +++ b/packages/ludic.render3d/tests/height_file_test.ludic @@ -1,35 +1,64 @@ -# height_file_test.ludic - terrain_height_file against terrain_height over a tiles file written here (no -# device): the same answer to the bit, resident or not, and a sweep of the whole map leaves the page -# cache - its slots, what they hold and the clock - exactly as it was +# height_file_test.ludic - the terrain's questions over a tiles file written here (no device): heights and +# the photograph are the file's texels to the bit, resident or not, and a sweep of the whole map by the +# questions and by a ground layer's fill leaves the page cache - its slots, what they hold, the clock - as it was import "ludic.render3d/r3d.ludic" program HeightFileTest { numbers float # never called: naming Input links the engine's runtime, which render3d's textures stand on function links_runtime() -> bool { return Input.key_pressed(0) } - # an LTT2 file with heights only (no photograph; normals and the coarse level read short, unused here) + 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 size = hat + n * n * n2 * 2 + 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) - for k in 10 .. 15 { hf_put(b, k * 4, size) } + 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 { - let q = (i * 37 + (i >> 5) * 11 + t * 5) & 65535 - b[hat + (t * n2 + i) * 2] = q & 255 - b[hat + (t * n2 + i) * 2 + 1] = q >> 8 + 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) @@ -45,75 +74,98 @@ program HeightFileTest { render3d_st.ter_ox = 0.0 render3d_st.ter_oz = 0.0 render3d_st.ter_h_scale = 0.0 + render3d_st.ter_o_scale = 0.0 return tt_open_ltt2(render3d_st, path, 0) } - test "the same answer as terrain_height, whether its 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) - let pts = floats(2 * 160 * 160) - let got = floats(160 * 160) - for j in 0 .. 160 { - for i in 0 .. 160 { - let k = j * 160 + i - pts[k * 2] = -3000.0 + float(i) * 37.3 - pts[k * 2 + 1] = -3100.0 + float(j) * 41.7 - got[k] = terrain_height_file(render3d_st, pts[k * 2], pts[k * 2 + 1]) - } - } - var off = 0 - for k in 0 .. 160 * 160 { if terrain_height(render3d_st, pts[k * 2], pts[k * 2 + 1]) != got[k] { off += 1 } } - expect_eq(off, 0) - # and again now that terrain_height has paged most of them in - for k in 0 .. 160 * 160 { if terrain_height_file(render3d_st, pts[k * 2], pts[k * 2 + 1]) != got[k] { off += 1 } } - expect_eq(off, 0) - # a texel square across a tile corner, and the map's edges - for c in 0 .. 5 { - let x = -4096.0 + 64.0 * float(c * 13 + 1) - 1.0 - let z = -4096.0 + 64.0 * float(c * 7 + 2) - 0.5 - expect_eq(terrain_height_file(render3d_st, x, z), terrain_height(render3d_st, x, z)) - } - expect_eq(terrain_height_file(render3d_st, -5000.0, 5000.0), terrain_height(render3d_st, -5000.0, 5000.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 "a sweep of the whole map leaves every slot, what it holds and the clock as they were" (render3d_st: mut Render3dState) { + 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) + var off = 0 + for j in 0 .. 160 { + for i in 0 .. 160 { + let x = -3000.0 + float(i) * 37.3 + let z = -3100.0 + float(j) * 41.7 + if terrain_height(render3d_st, x, z) != hf_expect(x, z) { off += 1 } + if terrain_height_file(render3d_st, x, z) != hf_expect(x, z) { off += 1 } + let px = min(max(int(Math.floor((x + 4096.0) * 256.0 / 8192.0)), 0), 255) + let pz = min(max(int(Math.floor((z + 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) + for c in 0 .. 5 { + let x = -4096.0 + 64.0 * float(c * 13 + 1) - 1.0 # a texel square across a tile corner + let z = -4096.0 + 64.0 * float(c * 7 + 2) - 0.5 + expect_eq(terrain_height(render3d_st, x, z), hf_expect(x, z)) + } + expect_eq(terrain_height(render3d_st, -5000.0, 5000.0), hf_expect(-5000.0, 5000.0)) + } + + test "a sweep of the whole map by the questions leaves every slot, what it holds and the clock as they were" (render3d_st: mut Render3dState) { expect(hf_open(render3d_st, "sweep")) terrain_tiles_prefetch(render3d_st, -200.0, 300.0, 300.0, 200) - expect(terrain_height(render3d_st, 2000.0, 2000.0) != 0.12345) # a tile paged the ordinary way too - let n2 = len(render3d_st.tt_slot_of) - let slots = len(render3d_st.tt_tile_in) - let slot_of = words(n2) - for t in 0 .. n2 { slot_of[t] = render3d_st.tt_slot_of[t] } - let tile_in = words(slots) - let refs = words(slots) - for s in 0 .. slots { - tile_in[s] = render3d_st.tt_tile_in[s] - refs[s] = render3d_st.tt_ref[s] - } - let h = floats(len(render3d_st.tt_h)) - for i in 0 .. len(h) { h[i] = render3d_st.tt_h[i] } - let hand = render3d_st.tt_hand - let reads = render3d_st.tt_reads - let frame_reads = render3d_st.tt_frame_reads + let was = hf_snap(render3d_st) var sum = 0.0 + var ortho = 0 var z = -4096.0 while z <= 4096.0 { var x = -4096.0 while x <= 4096.0 { - sum = sum + terrain_height_file(render3d_st, x, z) + sum = sum + terrain_height(render3d_st, x, z) + terrain_height_file(render3d_st, x, z) + terrain_height_smooth(render3d_st, x, z) + ortho = ortho ^ terrain_ortho(render3d_st, x, z) x = x + 16.0 } z = z + 16.0 } expect(sum != 0.0) - var moved = 0 - for t in 0 .. n2 { if render3d_st.tt_slot_of[t] != slot_of[t] { moved += 1 } } - for s in 0 .. slots { if render3d_st.tt_tile_in[s] != tile_in[s] or render3d_st.tt_ref[s] != refs[s] { moved += 1 } } - for i in 0 .. len(h) { if render3d_st.tt_h[i] != h[i] { moved += 1 } } - expect_eq(moved, 0) - expect_eq(render3d_st.tt_hand, hand) - expect_eq(render3d_st.tt_reads, reads) - expect_eq(render3d_st.tt_frame_reads, frame_reads) + expect(ortho != 0) + expect_eq(hf_moved(render3d_st, was), 0) + } + + test "a ground layer's fill over the whole map leaves the slots as they were" (render3d_st: mut Render3dState) { + expect(hf_open(render3d_st, "fill")) + expect(hf_density(render3d_st, Os.temp_dir() + "/r3d_height_file_fill.lgd2")) + terrain_tiles_prefetch(render3d_st, 0.0, 0.0, 250.0, 200) + let was = hf_snap(render3d_st) + if render3d_st.stream_scratch == null { render3d_st.stream_scratch = floats(4096 * INST_FLOATS) } + let s = new Stream + s.size = 64.0 + s.ox = -4096.0 + s.oz = -4096.0 + s.cur = new Chunk + let g = new GroundFill + g.step0 = 8.0 + g.trample = false + var n = 0 + var bad = 0 + for cz in 0 .. 32 { + for cx in 0 .. 32 { + s.cur.count = 0 + ground_fill(render3d_st, s, cx * 4, cz * 4, 0, g) + n += s.cur.count + for k in 0 .. s.cur.count { + let x = render3d_st.stream_scratch[k * INST_FLOATS] + let z = render3d_st.stream_scratch[k * INST_FLOATS + 2] + if render3d_st.stream_scratch[k * INST_FLOATS + 1] != hf_expect(x, z) - g.sink { bad += 1 } + } + } + } + expect(n > 32 * 32 * 32) + expect_eq(bad, 0) + expect_eq(hf_moved(render3d_st, was), 0) } test "with the whole copy kept it is terrain_height" (render3d_st: mut Render3dState) { @@ -126,4 +178,64 @@ program HeightFileTest { expect_eq(terrain_height_file(render3d_st, x, z), terrain_height(render3d_st, x, z)) } } + + # one layer, every tile one value (a word in the index): full density everywhere + function hf_density(render3d_st: mut Render3dState, path: string) -> bool { + let n = TERRAIN_RES / TT_TEX + let size = GD_HEADW * 4 + n * n * 8 + let b = buffer(size) + for k in 0 .. size { b[k] = 0 } + hf_put(b, 0, GD_MAGIC) + hf_put(b, 4, GD_VERSION) + hf_put(b, 8, n) + hf_put(b, 12, GD_SIDE) + hf_put(b, 16, 1) + hf_put(b, 32, 1) + hf_put(b, 36, GD_HEADW * 4) + for t in 0 .. n * n { + hf_put(b, GD_HEADW * 4 + t * 8, 1) + hf_put(b, GD_HEADW * 4 + t * 8 + 4, 255) + } + 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 + } }