ludic/packages/ludic.render3d/terrain_pages.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

148 lines
8.1 KiB
Text

# terrain_pages.ludic — the height field, its normals and the photograph on the GPU while the tiles are
# on (terrain_tiles.ludic): a coarse whole-map level (TT_COARSE a side) and, round the camera, a pool
# of fine tiles in three array textures addressed through a page table (u_tp_page: slot + 1 of a
# resident tile, 0 = read the coarse level). A shader reads a full-map uv as
# slot = texelFetch(u_tp_page, floor(uv * n)) - 1; slot uv = (1 + fract(uv * n) * T) / (T + 2)
# and each layer carries a one-texel border from its neighbours, so a bilinear tap never leaves it.
# Tiles off, nothing changes: the whole textures, u_tp_on = 0 and stand-in arrays bound.
const TP_REACH_MAX: float = 900.0 # metres of fine ground round the camera, fog or none
const TP_LAYERS_MAX: int = 2048 # what a desktop Vulkan device takes in an array (the floor is 256)
# the page pool's samplers and numbers for program p; every program that reads the ground calls it
function tp_bind(render3d_st: mut Render3dState, p: int) -> void {
tp_dummies(render3d_st)
var pg = render3d_st.tp_dummy_page
var h = render3d_st.tp_dummy_h
var nm = render3d_st.tp_dummy_n
var o = render3d_st.tp_dummy_o
var on = 0.0
if render3d_st.tp_on {
pg = render3d_st.tp_page_tex; h = render3d_st.tp_h_tex; nm = render3d_st.tp_n_tex; o = render3d_st.tp_o_tex; on = 1.0
}
r3d_bind_2d(render3d_st, p, "u_tp_page", 20, pg)
r3d_bind_tex(render3d_st, p, "u_tp_h", 21, GPU_TEX2D_ARRAY, h)
r3d_bind_tex(render3d_st, p, "u_tp_nrm", 22, GPU_TEX2D_ARRAY, nm)
r3d_bind_tex(render3d_st, p, "u_tp_ortho", 23, GPU_TEX2D_ARRAY, o)
u_f(render3d_st, gpu_uniform(render3d_st, p, "u_tp_on"), on)
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_tp_dims"), float(render3d_st.tt_n), float(TT_TEX), float(render3d_st.tt_oside), float(TERRAIN_RES))
}
# the stand-ins, once: an unbound sampler gets a white 2-D texture, which an array binding cannot take
@alloc_ok("made once: the stand-ins for the page pool's samplers")
function tp_dummies(render3d_st: mut Render3dState) -> void {
if render3d_st.tp_dummy_h != 0 { return }
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
let z = words(4)
for k in 0 .. 4 { z[k] = 0 }
render3d_st.tp_dummy_page = tex_target(render3d_st, 1, 1, GL_R32F, GL_RED, GL_FLOAT, GL_NEAREST)
gpu_tex_fill(render3d_st, render3d_st.tp_dummy_page, GL_R32F, 1, 1, GL_RED, GL_FLOAT, data_of(z))
free(z)
render3d_st.tp_dummy_h = tp_array(render3d_st, GL_R32F, 1, 1)
render3d_st.tp_dummy_n = tp_array(render3d_st, GL_RG16F, 1, 1)
render3d_st.tp_dummy_o = tp_array(render3d_st, GL_SRGB8_ALPHA8, 1, 1)
render3d_st.gvk_tag = was
}
# an array texture of `layers` side x side layers, no mips, read bilinear and clamped
function tp_array(render3d_st: mut Render3dState, ifmt: int, side: int, layers: int) -> int {
let t = gpu_tex_new(render3d_st)
gpu_tex_bind(render3d_st, GPU_TEX2D_ARRAY, t)
gpu_tex_image3d(render3d_st, ifmt, side, side, layers, GL_RED, GL_FLOAT, null)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
gpu_tex_param(render3d_st, GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
return t
}
# how far the fine tiles reach: the fog wall's reach when it is nearer than TP_REACH_MAX
function tp_reach_m(render3d_st: Render3dState) -> float {
if render3d_st.r3d_fog_wall > 0.0 { return Math.min(r3d_reach(render3d_st, 0.0), TP_REACH_MAX) }
return TP_REACH_MAX
}
function tp_tile_m(render3d_st: Render3dState) -> float { return float(TT_TEX) * terrain_texel(render3d_st) }
# the tiles within r and a one-tile ring, and a few spare
function tp_slots_for(render3d_st: Render3dState, r: float) -> int {
let a = r / tp_tile_m(render3d_st) + 1.5
let n = int(PI * a * a) + 9
return min(min(n, TP_LAYERS_MAX), render3d_st.tt_n * render3d_st.tt_n)
}
# the staging a frame's loads take: the page table, then TP_BUDGET tiles of each of the three
function tp_half_bytes(render3d_st: Render3dState) -> int {
let hs = TT_TEX + 2
let os = render3d_st.tt_oside + 2
return render3d_st.tt_n * render3d_st.tt_n * 4 + TP_BUDGET * (2 * hs * hs + os * os) * 4
}
# the pool, the page table (all 0) and the staging, (re)made for the reach the fog wall allows
@alloc_ok("a map made, or the fog wall moved: the page pool, once")
function tp_pool(render3d_st: mut Render3dState) -> void {
tp_pool_free(render3d_st)
let n = render3d_st.tt_n
let r = tp_reach_m(render3d_st)
let slots = tp_slots_for(render3d_st, r)
let hs = TT_TEX + 2
let os = render3d_st.tt_oside + 2
render3d_st.tp_h_tex = tp_array(render3d_st, GL_R32F, hs, slots)
render3d_st.tp_n_tex = tp_array(render3d_st, GL_RG16F, hs, slots)
render3d_st.tp_o_tex = tp_array(render3d_st, GL_SRGB8_ALPHA8, os, slots)
if render3d_st.tp_page == null or len(render3d_st.tp_page) != n * n {
if render3d_st.tp_page != null { free(render3d_st.tp_page) }
render3d_st.tp_page = floats(n * n)
}
for t in 0 .. n * n { render3d_st.tp_page[t] = 0.0 }
render3d_st.tp_page_tex = tex_target(render3d_st, n, n, GL_R32F, GL_RED, GL_FLOAT, GL_NEAREST)
gpu_tex_fill(render3d_st, render3d_st.tp_page_tex, GL_R32F, n, n, GL_RED, GL_FLOAT, data_of(render3d_st.tp_page))
tp_slot_lists(render3d_st, slots)
if render3d_st.tp_st_bytes < 2 * tp_half_bytes(render3d_st) {
gpu_staging_drop(render3d_st)
render3d_st.tp_st_ptr = gpu_staging_keep(render3d_st, 2 * tp_half_bytes(render3d_st))
}
render3d_st.tp_slots = slots; render3d_st.tp_reach = r
render3d_st.tp_cam_t = -1; render3d_st.tp_short = false; render3d_st.tp_dirty = false
render3d_st.tp_bytes = slots * (2 * hs * hs + os * os) * 4 + n * n * 4
render3d_st.tp_on = render3d_st.tp_st_ptr != null and gpu_tex_ok(render3d_st, render3d_st.tp_h_tex) and gpu_tex_ok(render3d_st, render3d_st.tp_o_tex)
tp_say_pool(slots, r, render3d_st.tp_bytes, render3d_st.tp_on)
}
# every slot free, and this frame's candidate lists (made once)
@alloc_ok("a map made, or the fog wall moved: the page pool's lists, once")
function tp_slot_lists(render3d_st: mut Render3dState, slots: int) -> void {
if render3d_st.tp_tile_of != null { free(render3d_st.tp_tile_of); free(render3d_st.tp_free) }
render3d_st.tp_tile_of = words(slots)
render3d_st.tp_free = words(slots)
for s in 0 .. slots { render3d_st.tp_tile_of[s] = -1; render3d_st.tp_free[s] = slots - 1 - s }
render3d_st.tp_nfree = slots
if render3d_st.tp_cand == null {
render3d_st.tp_cand = words(TP_BUDGET); render3d_st.tp_cand_s = words(TP_BUDGET); render3d_st.tp_cand_d = floats(TP_BUDGET)
render3d_st.tp_zero = words(1); render3d_st.tp_zero[0] = 0
}
}
# the pool and its page table let go (a world swap, or a remake); the coarse level is terrain_unload's
function tp_pool_free(render3d_st: mut Render3dState) -> void {
render3d_st.tp_on = false
if render3d_st.tp_page_tex != 0 { gpu_tex_free(render3d_st, render3d_st.tp_page_tex); render3d_st.tp_page_tex = 0 }
if render3d_st.tp_h_tex != 0 { gpu_tex_free(render3d_st, render3d_st.tp_h_tex); render3d_st.tp_h_tex = 0 }
if render3d_st.tp_n_tex != 0 { gpu_tex_free(render3d_st, render3d_st.tp_n_tex); render3d_st.tp_n_tex = 0 }
if render3d_st.tp_o_tex != 0 { gpu_tex_free(render3d_st, render3d_st.tp_o_tex); render3d_st.tp_o_tex = 0 }
render3d_st.tp_slots = 0; render3d_st.tp_bytes = 0
}
# r3d_fog_wall moved: a pool sized for another reach is made again (the page table starts empty)
function terrain_pages_fog(render3d_st: mut Render3dState) -> void {
if not render3d_st.tp_on { return }
let r = tp_reach_m(render3d_st)
render3d_st.tp_reach = r
render3d_st.tp_cam_t = -1
if tp_slots_for(render3d_st, r) == render3d_st.tp_slots { return }
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
tp_pool(render3d_st)
render3d_st.gvk_tag = was
}
@alloc_ok("a message, built only when it is said: a map made or the fog wall moved")
function tp_say_pool(slots: int, r: float, bytes: int, on: bool) -> void { print(`r3d: terrain: {slots} page slots for {int(r)} m round the camera ({bytes / 1024} KB on the GPU), paging {on}`) }