ludic/packages/ludic.render3d/terrain_pages_frame.ludic
Orkuncakilkaya 617ac10c5a render3d: the terrain's GPU maps paged round the camera while the tiles are on
At the cut the whole 4096 height, normal and photograph textures go, replaced by a coarse
2048 level (the CPU's tt_coarse uploaded; the normal and photograph blitted down on the GPU,
the photograph mipmapped) and a pool of fine tiles in three array textures - heights, normals,
photograph, each layer a tile with a one-texel border - addressed through a tt_n^2 page table
(u_tp_page: slot + 1, 0 = the coarse level). The pool holds the tiles within the reach (900 m,
or the fog wall's when nearer) and a ring, and is made again when the fog wall changes its size
(terrain_pages_fog). Once a frame (tp_frame, beside tt_frame) tiles past the reach and two tiles
go and the wanted ones come in nearest first, 8 a frame, written into a staging buffer kept for
the process (two halves, one per frame in flight) and copied into their layers inside the frame's
own command buffer - no submit of their own - with the page table re-uploaded only when it
changed. tp_bind binds the pool (or 1-layer stand-in arrays while paging is off, u_tp_on = 0) for
every program that reads the ground: terrain_bind_height (models, scatter, shadow_bind, grass),
terrain_bind_prog, the sun pass and the shadow bake. grass_cull.comp reads through the same page
table. R3D_VKMEM prints the pool's line. Tiles off, nothing changes. Compile-only: not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:13:11 +03:00

124 lines
5 KiB
Text

# terrain_pages_frame.ludic — which fine tiles the GPU holds, once a frame (terrain_pages.ludic). Tiles
# within the reach and a tile of the camera are wanted, nearest first, at most TP_BUDGET loaded a frame;
# a resident tile goes only past the reach and two tiles, so walking a tile edge back and forth loads
# nothing. A load is written into the kept staging (the half the frame in flight is not reading) and
# copied into its slot inside the frame's own command buffer. Nothing here allocates.
const TP_BUDGET: int = 8
function tp_frame(render3d_st: mut Render3dState) -> void {
render3d_st.tp_frame_loads = 0
if not render3d_st.tp_on or render3d_st.cam_pos == null { return }
let tile_m = tp_tile_m(render3d_st)
let fx = (render3d_st.cam_pos[0] - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) / tile_m
let fz = (render3d_st.cam_pos[2] - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) / tile_m
let n = render3d_st.tt_n
let ct = min(max(int(Math.floor(fz)), 0), n - 1) * n + min(max(int(Math.floor(fx)), 0), n - 1)
# the same tile as last time with nothing left over: every wanted tile is in already
if ct == render3d_st.tp_cam_t and not render3d_st.tp_short { return }
render3d_st.tp_cam_t = ct
let rt = render3d_st.tp_reach / tile_m
tp_evict(render3d_st, fx, fz, rt + 2.0)
let m = tp_pick(render3d_st, fx, fz, rt + 1.0)
if m > 0 or render3d_st.tp_dirty { tp_load(render3d_st, m, fx, fz, rt + 1.0) }
}
# tile t's centre from (fx, fz), in tiles
function tp_dist(t: int, n: int, fx: float, fz: float) -> float {
let dx = float(t % n) + 0.5 - fx
let dz = float(t / n) + 0.5 - fz
return Math.sqrt(dx * dx + dz * dz)
}
# every resident tile farther than `out` let go
function tp_evict(render3d_st: mut Render3dState, fx: float, fz: float, out: float) -> void {
for s in 0 .. render3d_st.tp_slots {
let t = render3d_st.tp_tile_of[s]
if t >= 0 and tp_dist(t, render3d_st.tt_n, fx, fz) > out { tp_drop(render3d_st, s) }
}
}
function tp_drop(render3d_st: mut Render3dState, s: int) -> void {
render3d_st.tp_page[render3d_st.tp_tile_of[s]] = 0.0
render3d_st.tp_tile_of[s] = -1
render3d_st.tp_free[render3d_st.tp_nfree] = s
render3d_st.tp_nfree += 1
render3d_st.tp_dirty = true
}
# the nearest TP_BUDGET wanted tiles not in, into tp_cand; whether more were left is tp_short
function tp_pick(render3d_st: mut Render3dState, fx: float, fz: float, want: float) -> int {
let n = render3d_st.tt_n
var m = 0
var missing = 0
for j in max(int(Math.floor(fz - want)), 0) .. min(int(fz + want) + 1, n) {
for i in max(int(Math.floor(fx - want)), 0) .. min(int(fx + want) + 1, n) {
let t = j * n + i
if render3d_st.tp_page[t] != 0.0 { continue }
let d = tp_dist(t, n, fx, fz)
if d > want { continue }
missing += 1
m = tp_cand_put(render3d_st, m, t, d)
}
}
render3d_st.tp_short = missing > m
return m
}
# tile t at distance d into the sorted candidates, the farthest falling off the end
function tp_cand_put(render3d_st: mut Render3dState, m: int, t: int, d: float) -> int {
var k = m
if k == TP_BUDGET {
if d >= render3d_st.tp_cand_d[k - 1] { return m }
k = TP_BUDGET - 1
}
while k > 0 and render3d_st.tp_cand_d[k - 1] > d {
render3d_st.tp_cand[k] = render3d_st.tp_cand[k - 1]
render3d_st.tp_cand_d[k] = render3d_st.tp_cand_d[k - 1]
k -= 1
}
render3d_st.tp_cand[k] = t
render3d_st.tp_cand_d[k] = d
return min(m + 1, TP_BUDGET)
}
# a free slot, or the farthest resident tile past `keep` given up for one; -1 when every slot is wanted
function tp_take(render3d_st: mut Render3dState, fx: float, fz: float, keep: float) -> int {
if render3d_st.tp_nfree == 0 {
var far = -1
var fd = keep
for s in 0 .. render3d_st.tp_slots {
let t = render3d_st.tp_tile_of[s]
if t < 0 { continue }
let d = tp_dist(t, render3d_st.tt_n, fx, fz)
if d > fd { fd = d; far = s }
}
if far < 0 { return -1 }
tp_drop(render3d_st, far)
}
render3d_st.tp_nfree -= 1
return render3d_st.tp_free[render3d_st.tp_nfree]
}
# the frame's loads written and copied, and the page table after them when it changed
function tp_load(render3d_st: mut Render3dState, m: int, fx: float, fz: float, keep: float) -> void {
# the frame's command buffer first: opening it waits for the frame that read this half last
let cb = gpu_upload_cb(render3d_st)
let base = (render3d_st.gvk_frame_no & 1) * tp_half_bytes(render3d_st)
# the tiles' reads from the file are the pool's own, not a frame asking too much (tt_frame)
let reads = render3d_st.tt_frame_reads
var k = 0
for c in 0 .. m {
let s = tp_take(render3d_st, fx, fz, keep)
if s < 0 { render3d_st.tp_short = true; break }
let t = render3d_st.tp_cand[c]
tp_fill(render3d_st, t, base, k)
render3d_st.tp_cand_s[k] = s
render3d_st.tp_tile_of[s] = t
render3d_st.tp_page[t] = float(s + 1)
render3d_st.tp_dirty = true
k += 1
}
render3d_st.tt_frame_reads = reads
render3d_st.tp_frame_loads = k
render3d_st.tp_loads += k
tp_upload(render3d_st, cb, base, k)
}