# 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}`) }