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>
148 lines
8.1 KiB
Text
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}`) }
|