From 617ac10c5aac04801f598abc6d2981fdf3778ac4 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 17:13:11 +0300 Subject: [PATCH] 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 --- packages/ludic.render3d/env.ludic | 29 ++++ packages/ludic.render3d/gpu_vk.ludic | 2 +- packages/ludic.render3d/gpu_vk_layers.ludic | 94 +++++++++++ packages/ludic.render3d/grass_gpu.ludic | 19 ++- packages/ludic.render3d/r3d.ludic | 5 + packages/ludic.render3d/render.ludic | 2 + .../ludic.render3d/shaders/grass_cull.comp | 48 +++++- .../shaders/spv/grass_cull.comp.spv | Bin 24440 -> 28356 bytes packages/ludic.render3d/terrain.ludic | 6 + packages/ludic.render3d/terrain_pages.ludic | 148 ++++++++++++++++++ .../ludic.render3d/terrain_pages_coarse.ludic | 30 ++++ .../ludic.render3d/terrain_pages_fill.ludic | 74 +++++++++ .../ludic.render3d/terrain_pages_frame.ludic | 124 +++++++++++++++ .../ludic.render3d/terrain_tiles_cut.ludic | 1 + packages/ludic.render3d/vkmem_report.ludic | 1 + 15 files changed, 571 insertions(+), 12 deletions(-) create mode 100644 packages/ludic.render3d/gpu_vk_layers.ludic create mode 100644 packages/ludic.render3d/terrain_pages.ludic create mode 100644 packages/ludic.render3d/terrain_pages_coarse.ludic create mode 100644 packages/ludic.render3d/terrain_pages_fill.ludic create mode 100644 packages/ludic.render3d/terrain_pages_frame.ludic diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 4479bcf9..7d4ed7c8 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -848,6 +848,35 @@ export state Render3dState { tt_reads: int = 0 # tiles read from the file, over the run and this frame tt_frame_reads: int = 0 tt_warned: bool = false + tp_on: bool = false # the GPU pages the fine tiles round the camera (terrain_pages.ludic) + tp_page_tex: int = 0 # R32F tt_n a side: slot + 1 of each resident tile, 0 = the coarse level + tp_h_tex: int = 0 # the pool: heights, normals and photograph, a layer a slot + tp_n_tex: int = 0 + tp_o_tex: int = 0 + tp_dummy_page: int = 0 # bound while paging is off: an array sampler needs an array + tp_dummy_h: int = 0 + tp_dummy_n: int = 0 + tp_dummy_o: int = 0 + tp_slots: int = 0 + tp_reach: float = 0.0 # metres the pool was sized for + tp_page: floats = null # the page table's CPU mirror, uploaded whole when it changes + tp_tile_of: words = null # per slot: its tile, or -1 + tp_free: words = null # the free slots, a stack + tp_nfree: int = 0 + tp_dirty: bool = false + tp_cam_t: int = -1 # the camera's tile when residency last ran + tp_short: bool = false # a wanted tile was left for the next frame + tp_cand: words = null # this frame's loads, nearest first: their tiles, distances, slots + tp_cand_d: floats = null + tp_cand_s: words = null + tp_zero: words = null # the one layer a 2-D texture has, for gpu_layers_copy + tp_st_buf: long = 0 # the staging buffer the loads are written into, two halves + tp_st_mem: long = 0 + tp_st_ptr: pointer = null + tp_st_bytes: int = 0 + tp_loads: int = 0 # tiles loaded over the run, and this frame + tp_frame_loads: int = 0 + tp_bytes: int = 0 # the pool's and the page table's GPU bytes ter_shadow_prog_aux: int = 0 # the bake of the aux target (TS_AUX) ter_shadow_aux: int = 0 # beside it, RG16F: the occluder distance and the cloud mask ter_shadow_yaw: float = 1000000000.0 # the sky yaw it was baked for diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index ac0008e1..4de95209 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -815,7 +815,7 @@ function gvk_startup_state(render3d_st: mut Render3dState) -> void { render3d_st.gvk_pc_last = words(4096) for i in 0 .. 4096 { render3d_st.gvk_pc_last[i] = -1 } render3d_st.gg_rb = words(4); render3d_st.gg_cb = words(1); render3d_st.gg_bufs = words(3) - render3d_st.gg_texs = words(3); render3d_st.gg_pr = words(48) + render3d_st.gg_texs = words(7); render3d_st.gg_pr = words(52) if render3d_st.gpu_unit_2d == null { render3d_st.gpu_unit_2d = words(32) for i in 0 .. 32 { render3d_st.gpu_unit_2d[i] = -1 } diff --git a/packages/ludic.render3d/gpu_vk_layers.ludic b/packages/ludic.render3d/gpu_vk_layers.ludic new file mode 100644 index 00000000..78e8e7a9 --- /dev/null +++ b/packages/ludic.render3d/gpu_vk_layers.ludic @@ -0,0 +1,94 @@ +# gpu_vk_layers.ludic — what the terrain's page pool asks of Vulkan (terrain_pages.ludic): a staging +# buffer kept for the process, regions of it copied into chosen layers of an array texture inside the +# frame's own command buffer (no submit of their own), and a one-off linear blit for a coarse level. + +# a host-visible staging buffer of n bytes kept for good (the pool's uploads reuse it every frame) +@alloc_ok("made once per resource and kept for its life: the page pool's staging buffer") +function gpu_staging_keep(render3d_st: mut Render3dState, n: int) -> pointer { + gvk_flush(render3d_st) + let p = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_SRC_BIT) + if p == null { return null } + render3d_st.tp_st_buf = render3d_st.gvk_st_buf + render3d_st.tp_st_mem = render3d_st.gvk_st_mem + render3d_st.tp_st_bytes = n + return p +} +# ... and let go (a map whose tiles need a larger one) +function gpu_staging_drop(render3d_st: mut Render3dState) -> void { + if render3d_st.tp_st_buf == 0 { return } + gvk_flush(render3d_st) + let buf = render3d_st.gvk_st_buf + let mem = render3d_st.gvk_st_mem + render3d_st.gvk_st_buf = render3d_st.tp_st_buf + render3d_st.gvk_st_mem = render3d_st.tp_st_mem + gvk_staging_free(render3d_st) + render3d_st.gvk_st_buf = buf + render3d_st.gvk_st_mem = mem + let zero: long = 0 + render3d_st.tp_st_buf = zero; render3d_st.tp_st_mem = zero; render3d_st.tp_st_ptr = null; render3d_st.tp_st_bytes = 0 +} + +# the frame's command buffer, outside any pass, for copies recorded before the frame's draws +function gpu_upload_cb(render3d_st: mut Render3dState) -> pointer { + gvk_pass_end(render3d_st) + return gvk_frame_cb(render3d_st) +} + +# n regions of the kept staging buffer, each w x h texels starting at base + k * stride, into layer +# layers[k] of tex's level 0: one barrier each way around them all, recorded into cb +function gpu_layers_copy(render3d_st: mut Render3dState, cb: pointer, tex: int, w: int, h: int, base: int, stride: int, layers: words, n: int) -> void { + if n <= 0 or tex <= 0 or cb == null { return } + let image = render3d_st.gvk_tex_image[tex] + let all = render3d_st.gvk_tex_layers[tex] + let levels = render3d_st.gvk_tex_levels[tex] + gvk_barrier(render3d_st, cb, image, false, 0, levels, all, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) + let sz = VkBufferImageCopy_sizeof + let bic = gvk_tmp(render3d_st, sz * n) + Vk.zero(bic, sz * n) + let sub = VkBufferImageCopy_imageSubresource + for k in 0 .. n { + let at: long = base + k * stride + Vk.put_i64(bic, k * sz + VkBufferImageCopy_bufferOffset, at) + Vk.put_i32(bic, k * sz + sub + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) + Vk.put_i32(bic, k * sz + sub + VkImageSubresourceLayers_baseArrayLayer, layers[k]) + Vk.put_i32(bic, k * sz + sub + VkImageSubresourceLayers_layerCount, 1) + Vk.put_i32(bic, k * sz + VkBufferImageCopy_imageExtent + VkExtent3D_width, w) + Vk.put_i32(bic, k * sz + VkBufferImageCopy_imageExtent + VkExtent3D_height, h) + Vk.put_i32(bic, k * sz + VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1) + } + Vk.cmd_copy_buffer_to_image(cb, render3d_st.tp_st_buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, n, bic) + gvk_barrier(render3d_st, cb, image, false, 0, levels, all, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) +} + +# level 0 of src (sw x sh) filtered into level 0 of dst (dw x dh), once: a coarse level made on the GPU +function gpu_tex_blit(render3d_st: mut Render3dState, src: int, sw: int, sh: int, dst: int, dw: int, dh: int) -> bool { + gvk_flush(render3d_st) + let si = render3d_st.gvk_tex_image[src] + let di = render3d_st.gvk_tex_image[dst] + if si == 0 or di == 0 { return false } + let cb = gvk_once_begin(render3d_st) + gvk_barrier(render3d_st, cb, si, false, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) + gvk_barrier(render3d_st, cb, di, false, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) + let blit = gvk_tmp(render3d_st, VkImageBlit_sizeof) + Vk.zero(blit, VkImageBlit_sizeof) + Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) + Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_layerCount, 1) + Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_x, sw) + Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_y, sh) + Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1) + Vk.put_i32(blit, VkImageBlit_dstSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) + Vk.put_i32(blit, VkImageBlit_dstSubresource + VkImageSubresourceLayers_layerCount, 1) + Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_x, dw) + Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, dh) + Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1) + Vk.cmd_blit_image(cb, si, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, di, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR) + gvk_barrier(render3d_st, cb, si, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + gvk_barrier(render3d_st, cb, di, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + return gvk_once_end(render3d_st, cb) +} + +# pixels into level 0 of a texture made without them (tex_target), laid out as gvk_tex_upload takes them +function gpu_tex_fill(render3d_st: mut Render3dState, tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, data: pointer) -> bool { + gvk_flush(render3d_st) + return gvk_tex_upload(render3d_st, tex, ifmt, w, h, 1, fmt, ty, data) +} diff --git a/packages/ludic.render3d/grass_gpu.ludic b/packages/ludic.render3d/grass_gpu.ludic index 6849d712..732d0275 100644 --- a/packages/ludic.render3d/grass_gpu.ludic +++ b/packages/ludic.render3d/grass_gpu.ludic @@ -38,7 +38,7 @@ function gg_init__t(render3d_st: mut Render3dState) -> void { if not gpu_has_compute(render3d_st) or render3d_st.grass_prog == 0 { return } if r3d_env_has(render3d_st, "R3D_GRASS_GPU") and r3d_env(render3d_st, "R3D_GRASS_GPU") == "0" { return } render3d_st.gg_reset = gpu_compute(render3d_st, "grass_reset", 1) - render3d_st.gg_cull = gpu_compute_tex(render3d_st, "grass_cull", 3, 3) + render3d_st.gg_cull = gpu_compute_tex(render3d_st, "grass_cull", 3, 7) render3d_st.gg_prog = r3d_program(render3d_st, "grass_inst.vert", "model.frag", "#define FOLIAGE\n#define BLADE\n#define GBLADE\n#define GINST\n") if render3d_st.gg_reset == 0 or render3d_st.gg_cull == 0 or render3d_st.gg_prog == 0 { return } render3d_st.gg_tiles_buf = new []int @@ -125,13 +125,21 @@ function gg_cull_frame(render3d_st: mut Render3dState) -> void { bufs[0] = tb; bufs[1] = render3d_st.gg_out; bufs[2] = render3d_st.gg_cmds let texs = render3d_st.gg_texs texs[0] = render3d_st.ter_height_tex; texs[1] = render3d_st.ter_ortho_tex; texs[2] = render3d_st.ter_normal_tex - gpu_dispatch_tex(render3d_st, render3d_st.gg_cull, data_of(pr), 192, bufs, texs, (render3d_st.gg_max + 63) / 64, render3d_st.gg_n) + gg_page_texs(render3d_st, texs) + gpu_dispatch_tex(render3d_st, render3d_st.gg_cull, data_of(pr), 208, bufs, texs, (render3d_st.gg_max + 63) / 64, render3d_st.gg_n) } -# grass_cull.comp's Params, std140: 12 vec4s, into the block made once in gg_cull_frame +# the page pool's four, after the three whole-map ones (the stand-ins while paging is off) +function gg_page_texs(render3d_st: mut Render3dState, texs: words) -> void { + tp_dummies(render3d_st) + texs[3] = render3d_st.tp_dummy_page; texs[4] = render3d_st.tp_dummy_h; texs[5] = render3d_st.tp_dummy_n; texs[6] = render3d_st.tp_dummy_o + if render3d_st.tp_on { texs[3] = render3d_st.tp_page_tex; texs[4] = render3d_st.tp_h_tex; texs[5] = render3d_st.tp_n_tex; texs[6] = render3d_st.tp_o_tex } +} + +# grass_cull.comp's Params, std140: 13 vec4s, into the block made once in gg_cull_frame function gg_params(render3d_st: mut Render3dState) -> words { let pr = render3d_st.gg_pr - for i in 0 .. 48 { pr[i] = 0 } + for i in 0 .. 52 { pr[i] = 0 } if render3d_st.cam_planes != null { for i in 0 .. 16 { pr[i] = float_bits(render3d_st.cam_planes[i]) } } pr[16] = float_bits(render3d_st.cam_pos[0]); pr[17] = float_bits(render3d_st.cam_pos[1]); pr[18] = float_bits(render3d_st.cam_pos[2]); pr[19] = float_bits(grass_reach(render3d_st)) var ph = render3d_st.post_h @@ -148,6 +156,9 @@ function gg_params(render3d_st: mut Render3dState) -> words { pr[32] = float_bits(render3d_st.ter_ox); pr[33] = float_bits(render3d_st.ter_oz); pr[34] = float_bits(float(render3d_st.TERRAIN_HALF)) pr[36] = float_bits(GG_NEAR); pr[37] = float_bits(GG_MID) for b in 0 .. GG_BANDS { pr[40 + b] = gg_base(b); pr[44 + b] = gg_cap(b) } + # the page pool (terrain_pages.ludic): on in lev.w, and its dims as the terrain's shaders take them + if render3d_st.tp_on { pr[31] = float_bits(1.0) } + pr[48] = float_bits(float(render3d_st.tt_n)); pr[49] = float_bits(float(TT_TEX)); pr[50] = float_bits(float(render3d_st.tt_oside)); pr[51] = float_bits(float(TERRAIN_RES)) return pr } diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 69eb70e3..abc6c9ac 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -12,6 +12,7 @@ import "gpu_manifest.ludic" import "gpu_vk.ludic" import "vkmem_report.ludic" import "gpu_vk_res.ludic" +import "gpu_vk_layers.ludic" import "gpu_vk_bc.ludic" import "texture_dds.ludic" import "prof.ludic" @@ -27,6 +28,10 @@ import "terrain.ludic" import "terrain_chunks.ludic" import "terrain_tiles.ludic" import "terrain_tiles_cut.ludic" +import "terrain_pages.ludic" +import "terrain_pages_coarse.ludic" +import "terrain_pages_frame.ludic" +import "terrain_pages_fill.ludic" import "overlay.ludic" import "shadow.ludic" import "post.ludic" diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index acf1e2d7..69aa0fa1 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -62,6 +62,7 @@ export function r3d_fog_wall(render3d_st: mut Render3dState, dist: float) -> voi fog_impostors(render3d_st) shadow_fog(render3d_st) fog_streams(render3d_st) + terrain_pages_fog(render3d_st) } # how far anything is drawn: the fog wall and a margin, or `far` (0: no limit of its own) without one function r3d_reach(render3d_st: Render3dState, far: float) -> float { @@ -227,6 +228,7 @@ function r3d_frame__t(render3d_st: mut Render3dState, time: float) -> void { render3d_st.r3d_test_frame += 1 vkmem_tick(render3d_st) tt_frame(render3d_st) + tp_frame(render3d_st) fog_at_tick(render3d_st) if render3d_st.r3d_test_resize == 0 and r3d_env_has(render3d_st, "R3D_RESIZE_AT") { render3d_st.r3d_test_resize = Text.to_int(r3d_env(render3d_st, "R3D_RESIZE_AT")) } # R3D_RESIZE_AT=: from frame n on, rebuild every screen-sized buffer every few diff --git a/packages/ludic.render3d/shaders/grass_cull.comp b/packages/ludic.render3d/shaders/grass_cull.comp index e8cde392..76d8445f 100644 --- a/packages/ludic.render3d/shaders/grass_cull.comp +++ b/packages/ludic.render3d/shaders/grass_cull.comp @@ -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; diff --git a/packages/ludic.render3d/shaders/spv/grass_cull.comp.spv b/packages/ludic.render3d/shaders/spv/grass_cull.comp.spv index 46bcf895c95be2cef8714ccb2dda372b346be172..30f2434f262dd7484a668746d5a0a35f11e878c8 100644 GIT binary patch literal 28356 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 literal 24440 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 void { # The height field for shaders that place things on the ground (model.vert's u_ground) function terrain_bind_height(render3d_st: mut Render3dState, p: int) -> void { + tp_bind(render3d_st, p) r3d_bind_2d(render3d_st, p, "u_ts_height", 5, render3d_st.ter_height_tex) r3d_bind_2d(render3d_st, p, "u_ter_normal", 6, render3d_st.ter_normal_tex) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_ts_half"), float(render3d_st.TERRAIN_HALF)) @@ -287,6 +288,7 @@ function terrain_bake_pass(render3d_st: mut Render3dState, p: int, target: int) gpu_depth_test(render3d_st, false) gpu_blend(render3d_st, false) gpu_use_program(render3d_st, p) + tp_bind(render3d_st, p) r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex) r3d_bind_2d(render3d_st, p, "u_ter_normal", 7, render3d_st.ter_normal_tex) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF)) @@ -400,6 +402,7 @@ function terrain_init(render3d_st: mut Render3dState) -> void { render3d_st.gvk_tag = was } function terrain_init__t(render3d_st: mut Render3dState) -> void { + tp_dummies(render3d_st) for i in 0 .. TERRAIN_INIT_STEPS { terrain_init_step(render3d_st, i) } } @@ -473,6 +476,7 @@ function terrain_draw_shadow(light_vp: floats) -> void { # selection can switch between them per patch without re-binding anything but the node. function terrain_bind_prog(render3d_st: mut Render3dState, p: int) -> void { gpu_use_program(render3d_st, p) + tp_bind(render3d_st, p) r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex) r3d_bind_2d(render3d_st, p, "u_ter_normal", 7, render3d_st.ter_normal_tex) r3d_bind_2d(render3d_st, p, "u_grass_d", 1, render3d_st.ter_tex[0]); r3d_bind_2d(render3d_st, p, "u_grass_n", 2, render3d_st.ter_tex[1]); r3d_bind_2d(render3d_st, p, "u_grass_a", 3, render3d_st.ter_tex[2]) @@ -582,6 +586,7 @@ function terrain_sun_pass(render3d_st: mut Render3dState, w: int, h: int, depth: gpu_clear(render3d_st, GL_COLOR_BUFFER_BIT) let p = render3d_st.ter_sun_prog gpu_use_program(render3d_st, p) + tp_bind(render3d_st, p) r3d_bind_2d(render3d_st, p, "u_height", 0, render3d_st.ter_height_tex) r3d_bind_2d(render3d_st, p, "u_ter_normal", 7, render3d_st.ter_normal_tex) u_f(render3d_st, gpu_uniform(render3d_st, p, "u_half"), float(render3d_st.TERRAIN_HALF)) @@ -822,6 +827,7 @@ function terrain_unload(render3d_st: mut Render3dState) -> void { if render3d_st.ter_normal_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_normal_tex); render3d_st.ter_normal_tex = 0 } if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex); render3d_st.ter_dem_tex = 0 } if render3d_st.ter_ortho_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex); render3d_st.ter_ortho_tex = 0 } + tp_pool_free(render3d_st) if render3d_st.ter_heights != null { free(render3d_st.ter_heights); render3d_st.ter_heights = null } tt_close(render3d_st) if render3d_st.ter_ortho_px != null { free(render3d_st.ter_ortho_px); render3d_st.ter_ortho_px = null } diff --git a/packages/ludic.render3d/terrain_pages.ludic b/packages/ludic.render3d/terrain_pages.ludic new file mode 100644 index 00000000..2a0e76ca --- /dev/null +++ b/packages/ludic.render3d/terrain_pages.ludic @@ -0,0 +1,148 @@ +# 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}`) } diff --git a/packages/ludic.render3d/terrain_pages_coarse.ludic b/packages/ludic.render3d/terrain_pages_coarse.ludic new file mode 100644 index 00000000..4f084272 --- /dev/null +++ b/packages/ludic.render3d/terrain_pages_coarse.ludic @@ -0,0 +1,30 @@ +# terrain_pages_coarse.ludic — the whole-map level the page pool falls back to (terrain_pages.ludic), +# made on the GPU from the whole textures at the cut, before they go. + +# at the cut (terrain_tiles_cut): the coarse level made from the whole textures, which then go - the +# patch bounds and the sun's bake have already read them - and the pool made +function tp_cut(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TERRAIN + tp_cut__t(render3d_st) + render3d_st.gvk_tag = was +} +function tp_cut__t(render3d_st: mut Render3dState) -> void { + if render3d_st.tt_file == null or render3d_st.tt_coarse == null or render3d_st.ter_ortho_tex == 0 { return } + let c = TT_COARSE + let h = tex_target(render3d_st, c, c, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR) + gpu_tex_fill(render3d_st, h, GL_R32F, c, c, GL_RED, GL_FLOAT, data_of(render3d_st.tt_coarse)) + let nm = tex_target(render3d_st, c, c, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR) + gpu_tex_blit(render3d_st, render3d_st.ter_normal_tex, TERRAIN_RES, TERRAIN_RES, nm, c, c) + let o = tex_target(render3d_st, c, c, GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR) + gpu_tex_blit(render3d_st, render3d_st.ter_ortho_tex, render3d_st.ter_ortho_w, render3d_st.ter_ortho_w, o, c, c) + gpu_tex_bind(render3d_st, GPU_TEX2D, o) + gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) + gpu_tex_mips(render3d_st, GPU_TEX2D) + gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, render3d_st.tex_anisotropy) + gpu_tex_free(render3d_st, render3d_st.ter_height_tex) + gpu_tex_free(render3d_st, render3d_st.ter_normal_tex) + gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex) + render3d_st.ter_height_tex = h; render3d_st.ter_normal_tex = nm; render3d_st.ter_ortho_tex = o + tp_pool(render3d_st) +} diff --git a/packages/ludic.render3d/terrain_pages_fill.ludic b/packages/ludic.render3d/terrain_pages_fill.ludic new file mode 100644 index 00000000..e4980b4a --- /dev/null +++ b/packages/ludic.render3d/terrain_pages_fill.ludic @@ -0,0 +1,74 @@ +# terrain_pages_fill.ludic — a tile's three layers written into the staging, border and all, and the +# frame's copies recorded (terrain_pages_frame.ludic). The staging half is laid out as the page table, +# then TP_BUDGET heights, TP_BUDGET normals and TP_BUDGET photographs. + +# the k-th load of the frame: tile t's heights and normals, (TT_TEX + 2) a side, each texel the one +# the whole copy held at that place, clamped at the map's edge +function tp_fill(render3d_st: mut Render3dState, t: int, base: int, k: int) -> void { + let n = render3d_st.tt_n + let hs = TT_TEX + 2 + let hb = hs * hs * 4 + let ho = base + n * n * 4 + k * hb + let no = base + n * n * 4 + TP_BUDGET * hb + k * hb + let i0 = (t % n) * TT_TEX - 1 + let j0 = (t / n) * TT_TEX - 1 + let p = render3d_st.tp_st_ptr + for b in 0 .. hs { + let tz = min(max(j0 + b, 0), TERRAIN_RES - 1) + for a in 0 .. hs { + let tx = min(max(i0 + a, 0), TERRAIN_RES - 1) + let o = (b * hs + a) * 4 + Vk.put_i32(p, ho + o, float_bits(ter_h(render3d_st, tx, tz))) + Vk.put_i32(p, no + o, ter_n(render3d_st, tx, tz)) + } + } + tp_fill_ortho(render3d_st, t, base + n * n * 4 + 2 * TP_BUDGET * hb, k) +} + +# ... and its photograph, (tt_oside + 2) a side, as sRGB RGBA8 bytes with alpha 255 +function tp_fill_ortho(render3d_st: mut Render3dState, t: int, at: int, k: int) -> void { + let n = render3d_st.tt_n + let side = render3d_st.tt_oside + let os = side + 2 + let top = n * side - 1 + let o0 = at + k * os * os * 4 + let i0 = (t % n) * side - 1 + let j0 = (t / n) * side - 1 + let p: pointer = render3d_st.tp_st_ptr + for b in 0 .. os { + let pz = min(max(j0 + b, 0), top) + for a in 0 .. os { + let v = ter_o(render3d_st, min(max(i0 + a, 0), top), pz) + let o = o0 + (b * os + a) * 4 + p[o] = (v >> 16) & 255 + p[o + 1] = (v >> 8) & 255 + p[o + 2] = v & 255 + p[o + 3] = 255 + } + } +} + +# the frame's k loads into their slots, then the page table if it changed: all in cb, no submit +function tp_upload(render3d_st: mut Render3dState, cb: pointer, base: int, k: int) -> void { + let n = render3d_st.tt_n + let hs = TT_TEX + 2 + let hb = hs * hs * 4 + let os = render3d_st.tt_oside + 2 + let p0 = base + n * n * 4 + let slots = render3d_st.tp_cand_s + gpu_layers_copy(render3d_st, cb, render3d_st.tp_h_tex, hs, hs, p0, hb, slots, k) + gpu_layers_copy(render3d_st, cb, render3d_st.tp_n_tex, hs, hs, p0 + TP_BUDGET * hb, hb, slots, k) + gpu_layers_copy(render3d_st, cb, render3d_st.tp_o_tex, os, os, p0 + 2 * TP_BUDGET * hb, os * os * 4, slots, k) + if not render3d_st.tp_dirty { return } + mem_copy(mem_off(render3d_st.tp_st_ptr, base), data_of(render3d_st.tp_page), n * n * 4) + gpu_layers_copy(render3d_st, cb, render3d_st.tp_page_tex, n, n, base, 0, render3d_st.tp_zero, 1) + render3d_st.tp_dirty = false +} + +# R3D_VKMEM: one line on the pool, next to the rest of the report +function tp_report(render3d_st: Render3dState) -> void { + if not render3d_st.tp_on { return } + tp_say_report(render3d_st.tp_slots - render3d_st.tp_nfree, render3d_st.tp_slots, render3d_st.tp_frame_loads, render3d_st.tp_loads, render3d_st.tp_bytes, render3d_st.tp_reach) +} +@alloc_ok("a report printed once, under R3D_VKMEM") +function tp_say_report(res: int, slots: int, now: int, all: int, bytes: int, r: float) -> void { print(`vkmem: terrain pages: {res} of {slots} slots resident, {now} loaded this frame, {all} over the run, {bytes / 1024} KB on the GPU, {int(r)} m round the camera`) } diff --git a/packages/ludic.render3d/terrain_pages_frame.ludic b/packages/ludic.render3d/terrain_pages_frame.ludic new file mode 100644 index 00000000..d39f1854 --- /dev/null +++ b/packages/ludic.render3d/terrain_pages_frame.ludic @@ -0,0 +1,124 @@ +# 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) +} diff --git a/packages/ludic.render3d/terrain_tiles_cut.ludic b/packages/ludic.render3d/terrain_tiles_cut.ludic index 8b4e9f08..f2d51f7a 100644 --- a/packages/ludic.render3d/terrain_tiles_cut.ludic +++ b/packages/ludic.render3d/terrain_tiles_cut.ludic @@ -35,6 +35,7 @@ function terrain_tiles_cut(render3d_st: mut Render3dState) -> void { if r3d_env_has(render3d_st, "R3D_TT_CHECK") { tt_check(render3d_st) } free(render3d_st.ter_heights); render3d_st.ter_heights = null free(render3d_st.ter_ortho_px); render3d_st.ter_ortho_px = null + tp_cut(render3d_st) tt_say_cut(path, TT_HEAD + n * n * (TT_TEX * TT_TEX * 2 + side * side) * 4, (gl_now_us() - t0) / 1000) } diff --git a/packages/ludic.render3d/vkmem_report.ludic b/packages/ludic.render3d/vkmem_report.ludic index b34696de..173d0569 100644 --- a/packages/ludic.render3d/vkmem_report.ludic +++ b/packages/ludic.render3d/vkmem_report.ludic @@ -50,6 +50,7 @@ function vkmem_report(render3d_st: Render3dState) -> void { vkmem_layers(render3d_st) vkmem_big_bufs(render3d_st) vkmem_say_ring(render3d_st.gvk_ring_peak, gvk_ring_bytes(render3d_st)) + tp_report(render3d_st) } # the largest images, with their owner, size and format