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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 17:13:11 +03:00
parent 329439cf61
commit 617ac10c5a
15 changed files with 571 additions and 12 deletions

View file

@ -15,11 +15,12 @@ layout(set = 0, binding = 0) uniform Params {
vec4 cam; // xyz the camera, w no blades past this (u_radius)
vec4 dens; // x s0, y d0, z one pixel in radians, w the snow line
vec4 lake; // the carved lake: centre x/z, half extents (z = 0: none)
vec4 lev; // x the lake's level, y the sea's, z the photograph on (1), w unused
vec4 lev; // x the lake's level, y the sea's, z the photograph on (1), w the page pool on (1)
vec4 ts; // the height texture: origin x/z, half extent, unused
vec4 bands; // outer distance of bands 0 and 1 (x, y); band 2 is the rest
uvec4 base; // each band's first record in Out
uvec4 cap; // ... and how many it holds
vec4 tp; // the page pool (u_tp_dims): tiles a side, height texels a tile, photograph texels a tile, height texels a side
} pr;
layout(set = 0, binding = 1) readonly buffer Tiles { vec4 tiles[]; }; // corner x/z, indices per cell, cells per side
@ -28,6 +29,12 @@ layout(set = 0, binding = 3) buffer Cmds { uint cmds[]; }; // VkDra
layout(set = 0, binding = 4) uniform sampler2D u_height; // the height (terrain's u_ts_height)
layout(set = 0, binding = 5) uniform sampler2D u_ortho; // the photograph
layout(set = 0, binding = 6) uniform sampler2D u_ter_normal; // the normal: x and z, y rebuilt
// the page pool (terrain_pages.ludic): while it is on, the three above are the coarse whole-map level
// and the tiles round the camera are layers of these, addressed through the page table
layout(set = 0, binding = 7) uniform sampler2D u_tp_page; // slot + 1 per tile, 0 = coarse
layout(set = 0, binding = 8) uniform sampler2DArray u_tp_h; // (T + 2)^2 a layer, a texel of border
layout(set = 0, binding = 9) uniform sampler2DArray u_tp_nrm;
layout(set = 0, binding = 10) uniform sampler2DArray u_tp_ortho; // (S + 2)^2 a layer
const float CELL = 4.0; // grass.ludic GRASS_CELL
@ -36,8 +43,35 @@ float bladeHash(ivec2 cell, int j, int k) {
uint h = pcg(uint(cell.x + 32768) * 73856093u ^ uint(cell.y + 32768) * 19349663u ^ uint(j) * 83492791u ^ uint(k) * 2654435761u);
return float(h) * (1.0 / 4294967295.0);
}
// the resident tile's slot under a full-map uv, or -1 (then the coarse level answers)
float tpSlot(vec2 uv) {
if (pr.lev.w < 0.5) return -1.0;
ivec2 t = ivec2(floor(clamp(uv, 0.0, 0.999999) * pr.tp.x));
return texelFetch(u_tp_page, t, 0).r - 1.0;
}
// where a full-map uv falls in its slot's layer of side T (+ a border of one)
vec3 tpUv(vec2 uv, float side, float slot) {
vec2 f = fract(clamp(uv, 0.0, 0.999999) * pr.tp.x);
return vec3((1.0 + f * side) / (side + 2.0), slot);
}
float groundH(vec2 uv) {
float s = tpSlot(uv);
if (s >= 0.0) return textureLod(u_tp_h, tpUv(uv, pr.tp.y, s), 0.0).r;
return textureLod(u_height, uv, 0.0).r;
}
vec2 groundN(vec2 uv) {
float s = tpSlot(uv);
if (s >= 0.0) return textureLod(u_tp_nrm, tpUv(uv, pr.tp.y, s), 0.0).rg;
return textureLod(u_ter_normal, uv, 0.0).rg;
}
// the photograph: a resident tile's layer has no mips, so it is read at its own level
vec3 groundO(vec2 uv) {
float s = tpSlot(uv);
if (s >= 0.0) return textureLod(u_tp_ortho, tpUv(uv, pr.tp.z, s), 0.0).rgb;
return textureLod(u_ortho, uv, 1.5).rgb;
}
float heightSmooth(vec2 uv) {
vec2 res = vec2(textureSize(u_height, 0));
vec2 res = pr.lev.w > 0.5 ? vec2(pr.tp.w) : vec2(textureSize(u_height, 0));
vec2 t = uv * res - 0.5;
vec2 f = fract(t);
vec2 i = floor(t);
@ -48,8 +82,8 @@ float heightSmooth(vec2 uv) {
vec2 s0 = w0 + w1, s1 = w2 + w3;
vec2 o0 = (i - 1.0 + w1 / s0 + 0.5) / res;
vec2 o1 = (i + 1.0 + w3 / s1 + 0.5) / res;
return (textureLod(u_height, vec2(o0.x, o0.y), 0.0).r * s0.x + textureLod(u_height, vec2(o1.x, o0.y), 0.0).r * s1.x) * s0.y
+ (textureLod(u_height, vec2(o0.x, o1.y), 0.0).r * s0.x + textureLod(u_height, vec2(o1.x, o1.y), 0.0).r * s1.x) * s1.y;
return (groundH(vec2(o0.x, o0.y)) * s0.x + groundH(vec2(o1.x, o0.y)) * s1.x) * s0.y
+ (groundH(vec2(o0.x, o1.y)) * s0.x + groundH(vec2(o1.x, o1.y)) * s1.x) * s1.y;
}
// grass.vert's bladeField: the region, the patchiness, the dry patches and the tussocks' shade
vec3 bladeField(vec2 xz, float y) {
@ -89,18 +123,18 @@ void main() {
float life = 1.0 - smoothstep(keep * 0.75, keep, r);
vec2 huv = (xz - pr.ts.xy) / (2.0 * pr.ts.z) + 0.5;
if (huv.x < 0.0 || huv.x > 1.0 || huv.y < 0.0 || huv.y > 1.0) return;
vec4 ht = textureLod(u_height, huv, 0.0);
vec4 ht = vec4(groundH(huv));
// the frustum, on a sphere round the blade (it stands at most 0.9 m tall)
vec3 root = vec3(xz.x, ht.r, xz.y);
for (int k = 0; k < 4; k++) { if (dot(pr.planes[k].xyz, root) + pr.planes[k].w < -1.0) return; }
vec2 gxz = textureLod(u_ter_normal, huv, 0.0).rg;
vec2 gxz = groundN(huv);
vec3 gn = vec3(gxz.x, sqrt(max(1.0 - dot(gxz, gxz), 0.0)), gxz.y);
float h3 = bladeHash(ci, j, 2), h4 = bladeHash(ci, j, 3);
float wl = pr.lev.y;
if (pr.lake.z > 0.0) { vec2 q = (xz - pr.lake.xy) / pr.lake.zw; if (dot(q, q) < 1.0) wl = max(wl, pr.lev.x); }
float ok = (1.0 - smoothstep(0.30, 0.55, 1.0 - gn.y)) * smoothstep(0.0, 0.6, ht.r - wl - 0.15) * smoothstep(pr.dens.w - 80.0, pr.dens.w - 200.0, ht.r);
if (pr.lev.z > 0.5) {
vec3 oc = textureLod(u_ortho, huv, 1.5).rgb;
vec3 oc = groundO(huv);
ok *= 0.40 + 0.60 * smoothstep(0.0, 0.025, oc.g - oc.b);
}
if (h4 > ok) return;