render3d: R3D_VKMEM=<frame> 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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 15:58:59 +03:00
parent f65bd1aca1
commit 6289564382
17 changed files with 315 additions and 15 deletions

View file

@ -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=<frame>: 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

View file

@ -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)

View file

@ -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
}

View file

@ -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 {

View file

@ -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)

View file

@ -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 {

View file

@ -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"

View file

@ -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=<n>: 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.

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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")) }

View file

@ -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")

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,110 @@
# vkmem_report.ludic — where the GPU memory is, by owner (R3D_VKMEM=<frame>, 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`) }

View file

@ -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)
}