# terrain_tiles.ludic — the height field and the photograph on the CPU as tiles read from a file, so # only the tiles something asks about are in memory (plan 26 of maroon-lake). The game turns it on with # terrain_tiles_to(dir, key) before the map is made; without that the whole copies stay, as before. # Every answer is the texel the whole copy held: a tile not in is read from the file there and then, # so what a query returns never depends on what happens to be resident - two machines, and the # placements at boot, get the same numbers to the bit. const TT_TEX: int = 32 # height texels a tile a side: 64 m at 2 m (a build-time constant) const TT_SHIFT: int = 5 const TT_MASK: int = 31 const TT_SLOTS: int = 2048 # tiles held at once: 8 MB each of heights, photograph and normals const TT_WARN_READS: int = 8 # a frame that reads more from the file than this is said, once # where a map's tiles are written, and under what name: set by the game before the map is made export function terrain_tiles_to(render3d_st: mut Render3dState, dir: string, key: string) -> void { render3d_st.tt_dir = intern(dir) render3d_st.tt_key = intern(key) } # 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)] } 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)) if s < 0 { return 0 } return render3d_st.tt_o[s * side * side + (pz % side) * side + (px % side)] } # the baked normal at height texel (tx, tz): x and z as two halves in a word (RG16F), for the GPU's tiles 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)] } # 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 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] if s >= 0 { render3d_st.tt_ref[s] = 1 return s } return tt_read(render3d_st, t) } # a slot let go (the clock: one asked for since the hand last passed keeps its place), and tile t read into it function tt_read(render3d_st: mut Render3dState, t: int) -> int { var s = render3d_st.tt_hand while render3d_st.tt_ref[s] != 0 { render3d_st.tt_ref[s] = 0 s = (s + 1) % TT_SLOTS } render3d_st.tt_hand = (s + 1) % TT_SLOTS let old = render3d_st.tt_tile_in[s] if old >= 0 { render3d_st.tt_slot_of[old] = -1 } tt_decode(render3d_st, t, s) render3d_st.tt_tile_in[s] = t render3d_st.tt_slot_of[t] = s render3d_st.tt_ref[s] = 1 render3d_st.tt_reads += 1 render3d_st.tt_frame_reads += 1 return s } # at a frame's start: say a frame that read too much from the file (once), and count the next function tt_frame(render3d_st: mut Render3dState) -> void { if render3d_st.tt_frame_reads > TT_WARN_READS and not render3d_st.tt_warned { render3d_st.tt_warned = true tt_say_reads(render3d_st.tt_frame_reads) } render3d_st.tt_frame_reads = 0 } function tt_say_reads(n: int) -> void { print(`r3d: terrain: a frame read {n} tiles from the file (said once; terrain_tiles_prefetch keeps them in ahead)`) } # the tiles within r of (x, z) read in ahead of being asked, at most `budget` a call export function terrain_tiles_prefetch(render3d_st: mut Render3dState, x: float, z: float, r: float, budget: int) -> void { if render3d_st.tt_file == null { return } let tile_m = float(TT_TEX) * 2.0 * float(render3d_st.TERRAIN_HALF) / float(TERRAIN_RES) let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) / tile_m let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) / tile_m let rt = r / tile_m + 1.0 let n = render3d_st.tt_n var left = budget for j in max(int(fz - rt), 0) .. min(int(fz + rt) + 1, n) { for i in max(int(fx - rt), 0) .. min(int(fx + rt) + 1, n) { let t = j * n + i if left > 0 and render3d_st.tt_slot_of[t] < 0 { tt_read(render3d_st, t) render3d_st.tt_frame_reads -= 1 left -= 1 } } } } # metres a height texel spans (the chunks' sample spacing), whether or not the whole copy is kept export function terrain_texel(render3d_st: Render3dState) -> float { return 2.0 * float(render3d_st.TERRAIN_HALF) / float(TERRAIN_RES) } # ---- a height without the cache: read-only ---------------------------------------------------- # For what asks often and only locally - a line of sight, the aim, a photograph's check - and must # not take render3d_st mut down its whole path: the tile if it is in, else the coarse whole-map level # (TT_COARSE a side, the mean of 2 x 2 texels). It never reads the file, so over ground nobody is near # it can be a few decimetres off the exact answer; anything two machines must agree on asks # terrain_height. const TT_COARSE: int = 2048 const TT_ABSENT: float = -1000000.0 # tt_peek: that texel is not in memory export function terrain_height_near(render3d_st: Render3dState, x: float, z: float) -> float { if render3d_st.ter_heights == null and render3d_st.tt_file == null { 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 = tt_peek(render3d_st, ix, iz) let h10 = tt_peek(render3d_st, ix + 1, iz) let h01 = tt_peek(render3d_st, ix, iz + 1) let h11 = tt_peek(render3d_st, ix + 1, iz + 1) if h00 == TT_ABSENT or h10 == TT_ABSENT or h01 == TT_ABSENT or h11 == TT_ABSENT { return tt_coarse_at(render3d_st, fx, fz) } return Math.lerp(Math.lerp(h00, h10, tx), Math.lerp(h01, h11, tx), tz) } # a height texel if it is in memory (the whole copy, or a resident tile), else TT_ABSENT function tt_peek(render3d_st: Render3dState, tx: int, tz: int) -> float { if render3d_st.ter_heights != null { return render3d_st.ter_heights[tz * TERRAIN_RES + tx] } let s = render3d_st.tt_slot_of[(tz >> TT_SHIFT) * render3d_st.tt_n + (tx >> TT_SHIFT)] if s < 0 { return TT_ABSENT } return render3d_st.tt_h[s * TT_TEX * TT_TEX + (tz & TT_MASK) * TT_TEX + (tx & TT_MASK)] } # the coarse level at fine texel position (fx, fz), bilinear: coarse texel c's centre is fine 2c + 0.5 function tt_coarse_at(render3d_st: Render3dState, fx: float, fz: float) -> float { let c = render3d_st.tt_coarse if c == null { return 0.0 } let cx = (fx - 0.5) * 0.5 let cz = (fz - 0.5) * 0.5 let ix = min(max(int(Math.floor(cx)), 0), TT_COARSE - 2) let iz = min(max(int(Math.floor(cz)), 0), TT_COARSE - 2) let tx = Math.clamp(cx - float(ix), 0.0, 1.0) let tz = Math.clamp(cz - float(iz), 0.0, 1.0) let a = Math.lerp(c[iz * TT_COARSE + ix], c[iz * TT_COARSE + ix + 1], tx) let b = Math.lerp(c[(iz + 1) * TT_COARSE + ix], c[(iz + 1) * TT_COARSE + ix + 1], tx) return Math.lerp(a, b, tz) } # the coarse level from the whole copy, before it goes @alloc_ok("a map being made: the coarse level, once") function tt_make_coarse(render3d_st: mut Render3dState) -> void { if render3d_st.tt_coarse == null { render3d_st.tt_coarse = floats(TT_COARSE * TT_COARSE) } let h = render3d_st.ter_heights let k = TERRAIN_RES / TT_COARSE for j in 0 .. TT_COARSE { for i in 0 .. TT_COARSE { var sum = 0.0 for b in 0 .. k { for a in 0 .. k { sum = sum + h[(j * k + b) * TERRAIN_RES + i * k + a] } } render3d_st.tt_coarse[j * TT_COARSE + i] = sum / float(k * k) } } }