From 6289564382e7414c23ddbb104137c959abd146d8 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 15:58:59 +0300 Subject: [PATCH] render3d: R3D_VKMEM= says where the GPU memory is, by owner, and the renderer's big CPU arrays Every allocation carries the owner its maker was wrapped in (models, terrain, impostor atlases, scatter, grass, shadow maps, frame targets, sky, water, game textures, made in a frame), and the report prints each owner's images and buffers, the 25 largest images and the scatter layers', streams' and terrain's CPU copies. Nothing drawn changes. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/env.ludic | 3 + packages/ludic.render3d/gltf.ludic | 10 +- packages/ludic.render3d/gpu_vk.ludic | 8 +- packages/ludic.render3d/grass.ludic | 9 +- packages/ludic.render3d/grass_gpu.ludic | 9 +- packages/ludic.render3d/post.ludic | 9 +- packages/ludic.render3d/r3d.ludic | 1 + packages/ludic.render3d/render.ludic | 15 +++ packages/ludic.render3d/scatter.ludic | 28 +++++- packages/ludic.render3d/shadow.ludic | 9 +- packages/ludic.render3d/sky.ludic | 10 +- packages/ludic.render3d/stream.ludic | 10 +- packages/ludic.render3d/terrain.ludic | 46 ++++++++- packages/ludic.render3d/texture.ludic | 28 +++++- packages/ludic.render3d/texture_dds.ludic | 18 +++- packages/ludic.render3d/vkmem_report.ludic | 110 +++++++++++++++++++++ packages/ludic.render3d/water.ludic | 7 ++ 17 files changed, 315 insertions(+), 15 deletions(-) create mode 100644 packages/ludic.render3d/vkmem_report.ludic diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index d1258cfc..be635281 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -196,6 +196,9 @@ export state Render3dState { gvk_al_off: []int = null gvk_al_len: []int = null gvk_al_map: []pointer = null + gvk_al_tag: []int = null # per allocation id: owner * 4 + image * 2 + host (vkmem_report.ludic) + gvk_tag: int = 0 # the owner allocations are made for now (VKM_*) + vkmem_at: int = 0 # R3D_VKMEM=: the report is printed at that frame @max(4096) gvk_al_spare: []int = null # ids to reuse gvk_pool: long = 0 gvk_fence: bytes = null diff --git a/packages/ludic.render3d/gltf.ludic b/packages/ludic.render3d/gltf.ludic index f3804237..8710c4f5 100644 --- a/packages/ludic.render3d/gltf.ludic +++ b/packages/ludic.render3d/gltf.ludic @@ -175,9 +175,17 @@ function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim { } # Load the mesh of the node called `node_name` from dir/file. +# the GPU memory it makes is counted as VKM_MODEL (R3D_VKMEM) +function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_MODEL + let r = gltf_load__t(render3d_st, dir, file, node_name) + render3d_st.gvk_tag = was + return r +} @alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") @creates(Model) -function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model { +function gltf_load__t(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model { render3d_st.gltf_dir = dir let at = dir + "/" + file let text = Fs.read_text(at) diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index 4360ceb5..cd558857 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -358,7 +358,8 @@ function gvk_mem_init(render3d_st: mut Render3dState) -> void { render3d_st.gvk_blk_mem = new []long; render3d_st.gvk_blk_kind = new []int; render3d_st.gvk_blk_size = new []int; render3d_st.gvk_blk_map = new []pointer render3d_st.gvk_fr_blk = new []int; render3d_st.gvk_fr_off = new []int; render3d_st.gvk_fr_len = new []int render3d_st.gvk_al_blk = new []int; render3d_st.gvk_al_mem = new []long; render3d_st.gvk_al_off = new []int; render3d_st.gvk_al_len = new []int - render3d_st.gvk_al_map = new []pointer; render3d_st.gvk_al_spare = new []int + render3d_st.gvk_al_map = new []pointer; render3d_st.gvk_al_spare = new []int; render3d_st.gvk_al_tag = new []int + push(render3d_st.gvk_al_tag, 0) push(render3d_st.gvk_al_blk, -1); push(render3d_st.gvk_al_mem, zero); push(render3d_st.gvk_al_off, 0); push(render3d_st.gvk_al_len, 0); push(render3d_st.gvk_al_map, null) } @@ -483,10 +484,15 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag List.pop(spare) } else { push(render3d_st.gvk_al_blk, -1); push(render3d_st.gvk_al_mem, zero); push(render3d_st.gvk_al_off, 0); push(render3d_st.gvk_al_len, 0); push(render3d_st.gvk_al_map, null) + push(render3d_st.gvk_al_tag, 0) a = len(render3d_st.gvk_al_blk) - 1 } var len_ = size if blk >= 0 { len_ = span } # what the block gives back when it is freed + var tg = render3d_st.gvk_tag * 4 + if image { tg += 2 } + if host { tg += 1 } + render3d_st.gvk_al_tag[a] = tg render3d_st.gvk_al_blk[a] = blk; render3d_st.gvk_al_mem[a] = own; render3d_st.gvk_al_off[a] = off; render3d_st.gvk_al_len[a] = len_; render3d_st.gvk_al_map[a] = map return a } diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index 319ec959..6059d5d3 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -87,8 +87,15 @@ function grass_blade_mesh(render3d_st: mut Render3dState, rows: int) -> Mesh { return m } -@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +# the GPU memory it makes is counted as VKM_GRASS (R3D_VKMEM) function grass_init(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_GRASS + grass_init__t(render3d_st) + render3d_st.gvk_tag = was +} +@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +function grass_init__t(render3d_st: mut Render3dState) -> void { render3d_st.grass_merge = gpu_has_mdi(render3d_st) and not r3d_env_has(render3d_st, "R3D_GRASS_TILES") var defs = "#define FOLIAGE\n#define BLADE\n#define GBLADE\n" if render3d_st.grass_merge { diff --git a/packages/ludic.render3d/grass_gpu.ludic b/packages/ludic.render3d/grass_gpu.ludic index add91d4c..6849d712 100644 --- a/packages/ludic.render3d/grass_gpu.ludic +++ b/packages/ludic.render3d/grass_gpu.ludic @@ -26,8 +26,15 @@ function gg_base(b: int) -> int { } # once, after grass_init: the programs, the buffers and the three instanced blades -@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +# the GPU memory it makes is counted as VKM_GRASS (R3D_VKMEM) function gg_init(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_GRASS + gg_init__t(render3d_st) + render3d_st.gvk_tag = was +} +@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +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) diff --git a/packages/ludic.render3d/post.ludic b/packages/ludic.render3d/post.ludic index f00dc685..83c45632 100644 --- a/packages/ludic.render3d/post.ludic +++ b/packages/ludic.render3d/post.ludic @@ -54,8 +54,15 @@ function post_set_msaa(render3d_st: mut Render3dState, n: int) -> void { } } -@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame") +# the GPU memory it makes is counted as VKM_TARGET (R3D_VKMEM) function post_init(render3d_st: mut Render3dState, w: int, h: int) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TARGET + post_init__t(render3d_st, w, h) + render3d_st.gvk_tag = was +} +@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame") +function post_init__t(render3d_st: mut Render3dState, w: int, h: int) -> void { render3d_st.post_w = w; render3d_st.post_h = h # the exposure's two 1x1 targets: made once, here, rather than by the first frame that measured if render3d_st.post_adapt_t == null { diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 4a9237bc..5b29e05e 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -10,6 +10,7 @@ import "fmath.ludic" import "gpu.ludic" import "gpu_manifest.ludic" import "gpu_vk.ludic" +import "vkmem_report.ludic" import "gpu_vk_res.ludic" import "gpu_vk_bc.ludic" import "texture_dds.ludic" diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 9532fe44..42959189 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -190,7 +190,14 @@ function r3d_screenshot(render3d_st: mut Render3dState, path: string) -> bool { function r3d_present(render3d_st: mut Render3dState) -> void { gpu_present(render3d_st) } # the drawable changed size: the camera's aspect and every screen-sized target follow +# the GPU memory it makes is counted as VKM_TARGET (R3D_VKMEM) function r3d_resize(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TARGET + r3d_resize__t(render3d_st) + render3d_st.gvk_tag = was +} +function r3d_resize__t(render3d_st: mut Render3dState) -> void { render3d_st.cam_aspect = float(gl_width()) / float(gl_height()) cam_update(render3d_st) post_free(render3d_st) @@ -198,7 +205,14 @@ function r3d_resize(render3d_st: mut Render3dState) -> void { if render3d_st.water_refl != null { target_free(render3d_st, render3d_st.water_refl); render3d_st.water_refl = null } r3d_say_resized() } +# the GPU memory it makes is counted as VKM_FRAME (R3D_VKMEM) function r3d_frame(render3d_st: mut Render3dState, time: float) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_FRAME + r3d_frame__t(render3d_st, time) + render3d_st.gvk_tag = was +} +function r3d_frame__t(render3d_st: mut Render3dState, time: float) -> void { outline_frame(render3d_st) if not render3d_st.r3d_ready { return } if gpu_resize_check(render3d_st) { r3d_resize(render3d_st) } @@ -206,6 +220,7 @@ function r3d_frame(render3d_st: mut Render3dState, time: float) -> void { # or a fullscreen change does. Headless has no window to resize, and this path is where # a stale attachment or a texture freed twice shows up. render3d_st.r3d_test_frame += 1 + vkmem_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 # frames at a different size, as dragging a window edge or entering fullscreen does. diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 5bcfac85..9f489019 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -248,8 +248,16 @@ function scatter_attach(render3d_st: Render3dState, m: Mesh, buf: int) -> void { gpu_mesh_done(render3d_st, m) } -@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame") +# the GPU memory it makes is counted as VKM_SCATTER (R3D_VKMEM) function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, foliage: bool, wind: float, near: float, cull: float) -> Layer { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_SCATTER + let r = layer_new__t(render3d_st, model, cap, foliage, wind, near, cull) + render3d_st.gvk_tag = was + return r +} +@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame") +function layer_new__t(render3d_st: mut Render3dState, model: Model, cap: int, foliage: bool, wind: float, near: float, cull: float) -> Layer { let l = new Layer l.model = model l.cap = cap @@ -301,8 +309,16 @@ function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: fl } # ---- impostors --------------------------------------------------------------------- -@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame") +# the GPU memory it makes is counted as VKM_IMPOSTOR (R3D_VKMEM) function impostor_bake(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int) -> Impostor { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_IMPOSTOR + let r = impostor_bake__t(render3d_st, model, tiles, tw, th) + render3d_st.gvk_tag = was + return r +} +@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame") +function impostor_bake__t(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int) -> Impostor { let im = new Impostor im.tiles = tiles im.radius = model.radius * 1.02 @@ -1113,7 +1129,15 @@ function cb_rnd(render3d_st: mut Render3dState) -> float { render3d_st.cb_state = (render3d_st.cb_state * 1103515245 + 12345) & 0x7FFFFFFF return float((render3d_st.cb_state >> 8) & 0xFFFF) / 65536.0 } +# the GPU memory it makes is counted as VKM_SCATTER (R3D_VKMEM) function carpet_bake(render3d_st: mut Render3dState, layers: []Layer, count: int, tile: float, res: int) -> int { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_SCATTER + let r = carpet_bake__t(render3d_st, layers, count, tile, res) + render3d_st.gvk_tag = was + return r +} +function carpet_bake__t(render3d_st: mut Render3dState, layers: []Layer, count: int, tile: float, res: int) -> int { let tex = tex_target(render3d_st, res, res, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR) let fbo = gpu_fb_new(render3d_st) gpu_fb_bind(render3d_st, fbo) diff --git a/packages/ludic.render3d/shadow.ludic b/packages/ludic.render3d/shadow.ludic index ddbd024a..94c75eaf 100644 --- a/packages/ludic.render3d/shadow.ludic +++ b/packages/ludic.render3d/shadow.ludic @@ -8,8 +8,15 @@ const SHADOW_CASCADES: int = 5 -@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +# the GPU memory it makes is counted as VKM_SHADOW (R3D_VKMEM) function shadow_init(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_SHADOW + shadow_init__t(render3d_st) + render3d_st.gvk_tag = was +} +@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +function shadow_init__t(render3d_st: mut Render3dState) -> void { shadow_make_tex(render3d_st) render3d_st.sh_fbo = gpu_fb_new(render3d_st) gpu_fb_bind(render3d_st, render3d_st.sh_fbo) diff --git a/packages/ludic.render3d/sky.ludic b/packages/ludic.render3d/sky.ludic index acc40811..06f03381 100644 --- a/packages/ludic.render3d/sky.ludic +++ b/packages/ludic.render3d/sky.ludic @@ -37,8 +37,16 @@ function sky_dir_from_uv(o: floats, u: float, v: float) -> void { v3_set(o, st * Math.sin(phi), Math.cos(theta), -(st * Math.cos(phi))) } -@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +# the GPU memory it makes is counted as VKM_SKY (R3D_VKMEM) function sky_load(render3d_st: mut Render3dState, path: string) -> bool { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_SKY + let r = sky_load__t(render3d_st, path) + render3d_st.gvk_tag = was + return r +} +@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +function sky_load__t(render3d_st: mut Render3dState, path: string) -> bool { render3d_st.sky_tex = tex_load_hdr(render3d_st, path) if render3d_st.sky_tex == 0 { return false } render3d_st.sky_w = render3d_st.tex_w; render3d_st.sky_h = render3d_st.tex_h diff --git a/packages/ludic.render3d/stream.ludic b/packages/ludic.render3d/stream.ludic index b1ffc818..8e00126b 100644 --- a/packages/ludic.render3d/stream.ludic +++ b/packages/ludic.render3d/stream.ludic @@ -44,8 +44,16 @@ property Stream { # microseconds spent per frame, split so the hitch can be attributed (R3D_PROF=1) -@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame") +# the GPU memory it makes is counted as VKM_SCATTER (R3D_VKMEM) function stream_new(render3d_st: mut Render3dState, layer: Layer, size: float, reach: float, b0: float, b1: float, b2: float, b3: float) -> Stream { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_SCATTER + let r = stream_new__t(render3d_st, layer, size, reach, b0, b1, b2, b3) + render3d_st.gvk_tag = was + return r +} +@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame") +function stream_new__t(render3d_st: mut Render3dState, layer: Layer, size: float, reach: float, b0: float, b1: float, b2: float, b3: float) -> Stream { if not render3d_st.stream_cap_read { render3d_st.stream_cap_read = true if r3d_env_has(render3d_st, "R3D_STREAM_CAP") { render3d_st.STREAM_MAX_CHUNKS = Text.to_int(r3d_env(render3d_st, "R3D_STREAM_CAP")) } diff --git a/packages/ludic.render3d/terrain.ludic b/packages/ludic.render3d/terrain.ludic index 01706e83..be41d0cf 100644 --- a/packages/ludic.render3d/terrain.ludic +++ b/packages/ludic.render3d/terrain.ludic @@ -142,7 +142,14 @@ function terrain_ortho_forest(render3d_st: mut Render3dState, x: float, z: float return 0.0 } +# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM) function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TERRAIN + terrain_use_ortho__t(render3d_st, path) + render3d_st.gvk_tag = was +} +function terrain_use_ortho__t(render3d_st: mut Render3dState, path: string) -> void { let px = png_decode(render3d_st, path) if px == null { return } # a map set up over another lets go of the last one's photograph first @@ -156,7 +163,14 @@ function terrain_use_ortho(render3d_st: mut Render3dState, path: string) -> void gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) } +# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM) function terrain_use_dem(render3d_st: mut Render3dState, path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TERRAIN + terrain_use_dem__t(render3d_st, path, emin, emax, base, ox, oz) + render3d_st.gvk_tag = was +} +function terrain_use_dem__t(render3d_st: mut Render3dState, path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void { if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex) } render3d_st.ter_dem_tex = tex_load(render3d_st, path, false) render3d_st.ter_dem_min = emin; render3d_st.ter_dem_max = emax; render3d_st.ter_dem_base = base @@ -166,7 +180,14 @@ function terrain_use_dem(render3d_st: mut Render3dState, path: string, emin: flo gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) } +# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM) function terrain_generate(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TERRAIN + terrain_generate__t(render3d_st) + render3d_st.gvk_tag = was +} +function terrain_generate__t(render3d_st: mut Render3dState) -> void { var defs = "" if render3d_st.ter_dem_tex != 0 { defs = "#define DEM\n" } if render3d_st.ter_smooth { defs = "#define SMOOTH\n" } @@ -324,8 +345,15 @@ function terrain_height_smooth(render3d_st: mut Render3dState, x: float, z: floa return h } -@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM) function terrain_load_textures(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TERRAIN + terrain_load_textures__t(render3d_st) + render3d_st.gvk_tag = was +} +@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +function terrain_load_textures__t(render3d_st: mut Render3dState) -> void { render3d_st.ter_tex = words(15) let a = render3d_st.r3d_assets + "/textures/" render3d_st.ter_tex[0] = tex_load(render3d_st, a + "aerial_grass_rock_diff_2k.png", true) @@ -346,7 +374,14 @@ function terrain_load_textures(render3d_st: mut Render3dState) -> void { if r3d_env_has(render3d_st, "R3D_TEXDBG") { for i in 0 .. 15 { print(`ter_tex[{string(i)}] = {string(render3d_st.ter_tex[i])}`) } } } +# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM) function terrain_init(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TERRAIN + terrain_init__t(render3d_st) + render3d_st.gvk_tag = was +} +function terrain_init__t(render3d_st: mut Render3dState) -> void { for i in 0 .. TERRAIN_INIT_STEPS { terrain_init_step(render3d_st, i) } } @@ -361,8 +396,15 @@ function terrain_init_step(render3d_st: mut Render3dState, i: int) -> void { terrain_init_finish(render3d_st) } -@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +# the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM) function terrain_init_finish(render3d_st: mut Render3dState) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_TERRAIN + terrain_init_finish__t(render3d_st) + render3d_st.gvk_tag = was +} +@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") +function terrain_init_finish__t(render3d_st: mut Render3dState) -> void { cdlod_init(render3d_st) render3d_st.ter_wire = r3d_env_has(render3d_st, "R3D_WIRE") render3d_st.ter_force_far = r3d_env_has(render3d_st, "R3D_TFARONLY") diff --git a/packages/ludic.render3d/texture.ludic b/packages/ludic.render3d/texture.ludic index e7a7f60c..48ab06f6 100644 --- a/packages/ludic.render3d/texture.ludic +++ b/packages/ludic.render3d/texture.ludic @@ -265,8 +265,16 @@ function tex_upload(render3d_st: mut Render3dState, px: pointer, srgb: bool, mip @alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") function tex_load(render3d_st: mut Render3dState, path: pointer, srgb: bool) -> int { return tex_load_ex(render3d_st, path, srgb, 0) } # ... with `dilate` passes of edge padding for a cut-out atlas (0 = none) -@alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") +# the GPU memory it makes is counted as VKM_TEXTURE (R3D_VKMEM) function tex_load_ex(render3d_st: mut Render3dState, path: pointer, srgb: bool, dilate: int) -> int { + let was = render3d_st.gvk_tag + if was == VKM_OTHER { render3d_st.gvk_tag = VKM_TEXTURE } + let r = tex_load_ex__t(render3d_st, path, srgb, dilate) + render3d_st.gvk_tag = was + return r +} +@alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") +function tex_load_ex__t(render3d_st: mut Render3dState, path: pointer, srgb: bool, dilate: int) -> int { # a .dds encoded beside the .png at build time goes to the GPU compressed, its mips included # (not for a cut-out atlas: its edge padding is made here, from the .png) if render3d_st.gvk_has_bc and dilate == 0 { @@ -410,7 +418,15 @@ function hdr_decode(render3d_st: mut Render3dState, path: pointer) -> floats { } # Load an equirectangular .hdr as an RGB16F texture with mips (clamped in v). +# the GPU memory it makes is counted as VKM_TEXTURE (R3D_VKMEM) function tex_load_hdr(render3d_st: mut Render3dState, path: pointer) -> int { + let was = render3d_st.gvk_tag + if was == VKM_OTHER { render3d_st.gvk_tag = VKM_TEXTURE } + let r = tex_load_hdr__t(render3d_st, path) + render3d_st.gvk_tag = was + return r +} +function tex_load_hdr__t(render3d_st: mut Render3dState, path: pointer) -> int { let px = hdr_decode(render3d_st, path) if px == null { return 0 } let id = gpu_tex_new(render3d_st) @@ -484,8 +500,16 @@ function tex_dump(render3d_st: mut Render3dState, tex: int, w: int, h: int, path # A binary PPM (P6, what Gl.screenshot writes) as an RGB8 texture, box-filtered down by # `shrink` (a photo thumbnail); 0 when the file is missing. -@alloc_ok("a load: an image read from disk") +# the GPU memory it makes is counted as VKM_TEXTURE (R3D_VKMEM) function tex_load_ppm(render3d_st: mut Render3dState, path: pointer, shrink: int) -> int { + let was = render3d_st.gvk_tag + if was == VKM_OTHER { render3d_st.gvk_tag = VKM_TEXTURE } + let r = tex_load_ppm__t(render3d_st, path, shrink) + render3d_st.gvk_tag = was + return r +} +@alloc_ok("a load: an image read from disk") +function tex_load_ppm__t(render3d_st: mut Render3dState, path: pointer, shrink: int) -> int { let d = r3d_read_file(render3d_st, path) if d == null { return 0 } let size = render3d_st.tex_file_len diff --git a/packages/ludic.render3d/texture_dds.ludic b/packages/ludic.render3d/texture_dds.ludic index 71b90e06..58a4b024 100644 --- a/packages/ludic.render3d/texture_dds.ludic +++ b/packages/ludic.render3d/texture_dds.ludic @@ -18,8 +18,16 @@ function dds_path_of(path: string) -> string { } # Load the .dds at `path` as a texture (srgb: colour, sampled as sRGB), or 0 -@alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") +# the GPU memory it makes is counted as VKM_TEXTURE (R3D_VKMEM) function tex_load_dds(render3d_st: mut Render3dState, path: string, srgb: bool) -> int { + let was = render3d_st.gvk_tag + if was == VKM_OTHER { render3d_st.gvk_tag = VKM_TEXTURE } + let r = tex_load_dds__t(render3d_st, path, srgb) + render3d_st.gvk_tag = was + return r +} +@alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") +function tex_load_dds__t(render3d_st: mut Render3dState, path: string, srgb: bool) -> int { let b = Fs.read_bytes(path) if b == null { return 0 } let id = tex_load_dds_bytes(render3d_st, b, srgb) @@ -28,7 +36,15 @@ function tex_load_dds(render3d_st: mut Render3dState, path: string, srgb: bool) return id } +# the GPU memory it makes is counted as VKM_TEXTURE (R3D_VKMEM) function tex_load_dds_bytes(render3d_st: mut Render3dState, b: []byte, srgb: bool) -> int { + let was = render3d_st.gvk_tag + if was == VKM_OTHER { render3d_st.gvk_tag = VKM_TEXTURE } + let r = tex_load_dds_bytes__t(render3d_st, b, srgb) + render3d_st.gvk_tag = was + return r +} +function tex_load_dds_bytes__t(render3d_st: mut Render3dState, b: []byte, srgb: bool) -> int { if len(b) < 148 or b[0] != 68 or b[1] != 68 or b[2] != 83 or b[3] != 32 { return 0 } if b[84] != 68 or b[85] != 88 or b[86] != 49 or b[87] != 48 { return 0 } # "DX10" let h = dds_u32(b, 12) diff --git a/packages/ludic.render3d/vkmem_report.ludic b/packages/ludic.render3d/vkmem_report.ludic new file mode 100644 index 00000000..b8688f59 --- /dev/null +++ b/packages/ludic.render3d/vkmem_report.ludic @@ -0,0 +1,110 @@ +# vkmem_report.ludic — where the GPU memory is, by owner (R3D_VKMEM=, printed once at that frame). +# Every allocation carries the owner its maker was wrapped in (gvk_tag); a block's unused room is +# counted apart, since it is memory the driver holds for us whoever it will be given to. + +const VKM_OTHER: int = 0 +const VKM_MODEL: int = 1 +const VKM_TERRAIN: int = 2 +const VKM_SCATTER: int = 3 +const VKM_GRASS: int = 4 +const VKM_SHADOW: int = 5 +const VKM_TARGET: int = 6 +const VKM_SKY: int = 7 +const VKM_WATER: int = 8 +const VKM_TEXTURE: int = 9 +const VKM_FRAME: int = 10 +const VKM_IMPOSTOR: int = 11 +const VKM_N: int = 12 + +function vkmem_tick(render3d_st: mut Render3dState) -> void { + if render3d_st.vkmem_at == 0 { + render3d_st.vkmem_at = -1 + if r3d_env_has(render3d_st, "R3D_VKMEM") { render3d_st.vkmem_at = Text.to_int(r3d_env(render3d_st, "R3D_VKMEM")) } + } + if render3d_st.vkmem_at > 0 and render3d_st.r3d_test_frame == render3d_st.vkmem_at { vkmem_report(render3d_st) } +} + +@alloc_ok("a report printed once, under R3D_VKMEM") +function vkmem_report(render3d_st: Render3dState) -> void { + if render3d_st.gvk_al_tag == null { return } + let img = words(VKM_N); let buf = words(VKM_N); let host = words(VKM_N); let cnt = words(VKM_N) + for k in 0 .. VKM_N { img[k] = 0; buf[k] = 0; host[k] = 0; cnt[k] = 0 } + var used = 0 + var own = 0 + for a in 1 .. len(render3d_st.gvk_al_len) { + let n = render3d_st.gvk_al_len[a] + if n <= 0 { continue } + let t = render3d_st.gvk_al_tag[a] + let who = Math.min(t / 4, VKM_N - 1) + if (t & 1) != 0 { host[who] += n } else if (t & 2) != 0 { img[who] += n } else { buf[who] += n } + cnt[who] += 1 + used += n + if render3d_st.gvk_al_blk[a] < 0 { own += n } + } + var blocks = 0 + for b in 0 .. len(render3d_st.gvk_blk_size) { blocks += render3d_st.gvk_blk_size[b] } + vkmem_say_head(used, blocks + own, blocks + own - used) + for k in 0 .. VKM_N { if cnt[k] > 0 { vkmem_say_row(vkmem_name(k), img[k], buf[k], host[k], cnt[k]) } } + vkmem_big(render3d_st) + vkmem_cpu(render3d_st) +} + +# the largest images, with their owner, size and format +@alloc_ok("a report printed once, under R3D_VKMEM") +function vkmem_big(render3d_st: Render3dState) -> void { + let n = len(render3d_st.gvk_tex_mem) + let done = words(n) + for i in 0 .. n { done[i] = 0 } + for r in 0 .. 25 { + var best = -1 + var bn = 0 + for i in 1 .. n { + let a = int(render3d_st.gvk_tex_mem[i]) + if done[i] != 0 or a <= 0 or a >= len(render3d_st.gvk_al_len) { continue } + if render3d_st.gvk_al_len[a] > bn { bn = render3d_st.gvk_al_len[a]; best = i } + } + if best < 0 { return } + done[best] = 1 + let a = int(render3d_st.gvk_tex_mem[best]) + vkmem_say_img(vkmem_name(render3d_st.gvk_al_tag[a] / 4), bn, render3d_st.gvk_tex_dims_w[best], render3d_st.gvk_tex_dims_h[best], render3d_st.gvk_tex_layers[best], render3d_st.gvk_tex_levels[best], render3d_st.gvk_tex_vkfmt[best]) + } +} + +function vkmem_name(k: int) -> string { + if k == VKM_MODEL { return "models (gltf_load)" } + if k == VKM_TERRAIN { return "terrain" } + if k == VKM_SCATTER { return "scatter + streams" } + if k == VKM_GRASS { return "grass" } + if k == VKM_SHADOW { return "shadow maps" } + if k == VKM_TARGET { return "frame targets" } + if k == VKM_SKY { return "sky" } + if k == VKM_WATER { return "water" } + if k == VKM_TEXTURE { return "textures (game)" } + if k == VKM_FRAME { return "made in a frame" } + if k == VKM_IMPOSTOR { return "impostor atlases" } + return "other" +} +function vkmem_mb(n: int) -> int { return n / 1048576 } +function vkmem_say_head(used: int, held: int, slack: int) -> void { print(`vkmem: {vkmem_mb(used)} MB in use, {vkmem_mb(held)} MB held from the driver, {vkmem_mb(slack)} MB unused room in blocks`) } +function vkmem_say_row(name: string, img: int, buf: int, host: int, n: int) -> void { print(`vkmem: {name}: images {vkmem_mb(img)} MB, device buffers {vkmem_mb(buf)} MB, host buffers {vkmem_mb(host)} MB ({n} allocations)`) } +function vkmem_say_img(name: string, n: int, w: int, h: int, layers: int, levels: int, fmt: int) -> void { print(`vkmem: image {n / 1024} KB {w}x{h}x{layers} mips {levels} vkformat {fmt} {name}`) } + +# the renderer's own big arrays on the CPU heap +function vkmem_cpu(render3d_st: Render3dState) -> void { + var lay = 0 + var cap = 0 + for i in 0 .. len(render3d_st.sc_layers) { + let l = render3d_st.sc_layers[i] + lay += vkmem_fl(l.inst) + vkmem_fl(l.scratch) + vkmem_fl(l.gsorted) + vkmem_fl(l.vis) + cap += l.cap + } + var str = 0 + if render3d_st.stream_all != null { for i in 0 .. len(render3d_st.stream_all) { str += len(render3d_st.stream_all[i].arena) * 4 } } + let ter = vkmem_fl(render3d_st.ter_heights) + TERRAIN_RES * TERRAIN_RES * 3 + vkmem_say_cpu(len(render3d_st.sc_layers), cap, lay, str, ter, vkmem_fl(render3d_st.wb_cells)) +} +function vkmem_fl(f: floats) -> int { + if f == null { return 0 } + return len(f) * 4 +} +function vkmem_say_cpu(n: int, cap: int, lay: int, str: int, ter: int, water: int) -> void { print(`vkmem: cpu: {n} scatter layers (cap {cap} instances) {vkmem_mb(lay)} MB, stream arenas {vkmem_mb(str)} MB, terrain copies {vkmem_mb(ter)} MB, water cells {vkmem_mb(water)} MB`) } diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index a078ab2b..4892cac7 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -294,7 +294,14 @@ function water_body_add(render3d_st: mut Render3dState, level: float, cx: float, } # one reflecting plane: what this function always meant, without a new mesh and program # every time it is called +# the GPU memory it makes is counted as VKM_WATER (R3D_VKMEM) function water_init(render3d_st: mut Render3dState, level: float, cx: float, cz: float, ex: float, ez: float) -> void { + let was = render3d_st.gvk_tag + render3d_st.gvk_tag = VKM_WATER + water_init__t(render3d_st, level, cx, cz, ex, ez) + render3d_st.gvk_tag = was +} +function water_init__t(render3d_st: mut Render3dState, level: float, cx: float, cz: float, ex: float, ez: float) -> void { water_bodies_clear(render3d_st) water_body_add(render3d_st, level, cx, cz, ex, ez, true) }