Merge branch 'r3d/frame-allocs' into lang/foundations

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 15:58:26 +03:00
commit dec64e8a59
21 changed files with 118 additions and 42 deletions

View file

@ -245,6 +245,7 @@ property OutlineReq {
# on the actor; a queue does not, if the first pass empties it. So a flush CLOSES the
# batch rather than clearing it, and the next add opens a new one: every pass in a frame
# sees the same requests, and the caller needs no frame hook.
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function outline_model(render3d_st: mut Render3dState, m: Model, mat: floats, width: float, r: float, g: float, b: float) -> void {
if m == null or mat == null or width == 0.0 { return }
if render3d_st.ac_oq_closed { render3d_st.ac_oq_n = 0; render3d_st.ac_oq_closed = false }

View file

@ -33,10 +33,10 @@ function cam_update(render3d_st: mut Render3dState) -> void {
let cp = Math.cos(render3d_st.cam_pitch); let sp = Math.sin(render3d_st.cam_pitch)
v3_set(render3d_st.cam_fwd, -(sy * cp), sp, -(cy * cp))
v3_set(render3d_st.cam_right, cy, 0.0, -sy)
let at = floats(3)
v3_add(at, render3d_st.cam_pos, render3d_st.cam_fwd)
let up = v3_new(0.0, 1.0, 0.0)
m4_look_at(render3d_st.cam_view, render3d_st.cam_pos, at, up)
let px = render3d_st.cam_pos[0]
let py = render3d_st.cam_pos[1]
let pz = render3d_st.cam_pos[2]
m4_look_at_xyz(render3d_st.cam_view, px, py, pz, px + render3d_st.cam_fwd[0], py + render3d_st.cam_fwd[1], pz + render3d_st.cam_fwd[2], 0.0, 1.0, 0.0)
m4_perspective(render3d_st.cam_proj, render3d_st.cam_fov, render3d_st.cam_aspect, render3d_st.cam_near, render3d_st.cam_far)
m4_mul(render3d_st.cam_vp_clean, render3d_st.cam_proj, render3d_st.cam_view)
m4_inverse(render3d_st.cam_inv_vp_clean, render3d_st.cam_vp_clean)
@ -49,7 +49,6 @@ function cam_update(render3d_st: mut Render3dState) -> void {
m4_mul(render3d_st.cam_vp, render3d_st.cam_proj, render3d_st.cam_view)
m4_inverse(render3d_st.cam_inv_vp, render3d_st.cam_vp)
m4_inverse(render3d_st.cam_inv_proj, render3d_st.cam_proj)
free(at); free(up)
cam_planes_update(render3d_st)
}
function cam_planes_update(render3d_st: mut Render3dState) -> void {

View file

@ -31,6 +31,7 @@ const DS_KINDS: int = 5
# rows: (pass, program, kind)
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function ds_enabled(render3d_st: mut Render3dState) -> bool {
if render3d_st.ds_on < 0 {
render3d_st.ds_on = 0
@ -49,6 +50,7 @@ function ds_enabled(render3d_st: mut Render3dState) -> bool {
}
# the profiler's pass boundaries (prof_begin / prof_end), whether or not R3D_PROF is on
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function ds_set_pass(render3d_st: mut Render3dState, name: pointer) -> void {
if render3d_st.ds_on != 1 { return }
render3d_st.ds_last = -1
@ -76,6 +78,7 @@ function ds_frame(render3d_st: mut Render3dState) -> void {
}
}
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function ds_draw(render3d_st: mut Render3dState, kind: int, instances: int, indices: int) -> void {
if render3d_st.ds_on != 1 or not render3d_st.ds_counting { return }
let p = render3d_st.gpu_prog_cur
@ -125,6 +128,7 @@ function ds_backend(render3d_st: Render3dState) -> pointer {
return "opengl"
}
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function ds_report(render3d_st: mut Render3dState) -> void {
let n_rows = len(render3d_st.ds_row_pass)
let n_pass = len(render3d_st.ds_pass_names)

View file

@ -157,45 +157,55 @@ function m4_ortho(m: floats, l: float, r: float, b: float, t: float, n: float, f
m[14] = -((f + n) / (f - n))
}
function m4_look_at(m: floats, eye: floats, at: floats, up: floats) -> void {
let fwd = floats(3); let side = floats(3); let u = floats(3); let t = floats(3)
v3_sub(t, at, eye)
v3_normalize(fwd, t)
v3_cross(t, fwd, up)
v3_normalize(side, t)
v3_cross(u, side, fwd)
m4_look_at_xyz(m, eye[0], eye[1], eye[2], at[0], at[1], at[2], up[0], up[1], up[2])
}
# the same from nine numbers, on scalars: the camera asks it every frame, and four scratch vectors
# made and freed per call were churn
function m4_look_at_xyz(m: floats, ex: float, ey: float, ez: float, ax: float, ay: float, az: float, ux: float, uy: float, uz: float) -> void {
var fx = ax - ex
var fy = ay - ey
var fz = az - ez
var fl = Math.sqrt(fx * fx + fy * fy + fz * fz)
if fl > 0.0 { fx = fx / fl; fy = fy / fl; fz = fz / fl }
var sx = fy * uz - fz * uy
var sy = fz * ux - fx * uz
var sz = fx * uy - fy * ux
let sl = Math.sqrt(sx * sx + sy * sy + sz * sz)
if sl > 0.0 { sx = sx / sl; sy = sy / sl; sz = sz / sl }
let vx = sy * fz - sz * fy
let vy = sz * fx - sx * fz
let vz = sx * fy - sy * fx
m4_identity(m)
m[0] = side[0]; m[4] = side[1]; m[8] = side[2]
m[1] = u[0]; m[5] = u[1]; m[9] = u[2]
m[2] = -fwd[0]; m[6] = -fwd[1]; m[10] = -fwd[2]
m[12] = -v3_dot(side, eye)
m[13] = -v3_dot(u, eye)
m[14] = v3_dot(fwd, eye)
free(fwd); free(side); free(u); free(t)
m[0] = sx; m[4] = sy; m[8] = sz
m[1] = vx; m[5] = vy; m[9] = vz
m[2] = -fx; m[6] = -fy; m[10] = -fz
m[12] = -(sx * ex + sy * ey + sz * ez)
m[13] = -(vx * ex + vy * ey + vz * ez)
m[14] = fx * ex + fy * ey + fz * ez
}
# general 4x4 inverse (cofactor expansion); o may not alias a
function m4_inverse(o: floats, a: floats) -> bool {
let inv = floats(16)
inv[0] = a[5] * (a[10] * a[15]) + -(a[5] * (a[11] * a[14])) - a[9] * (a[6] * a[15]) + (a[9] * (a[7] * a[14]) + (a[13] * (a[6] * a[11]) - a[13] * (a[7] * a[10])))
inv[4] = -(a[4] * (a[10] * a[15])) + a[4] * (a[11] * a[14]) - a[8] * (a[7] * a[14]) + (a[8] * (a[6] * a[15]) + (a[12] * (a[7] * a[10]) - a[12] * (a[6] * a[11])))
inv[8] = a[4] * (a[9] * a[15]) + -(a[4] * (a[11] * a[13])) - a[8] * (a[5] * a[15]) + (a[8] * (a[7] * a[13]) + (a[12] * (a[5] * a[11]) - a[12] * (a[7] * a[9])))
inv[12] = -(a[4] * (a[9] * a[14])) + a[4] * (a[10] * a[13]) - a[8] * (a[6] * a[13]) + (a[8] * (a[5] * a[14]) + (a[12] * (a[6] * a[9]) - a[12] * (a[5] * a[10])))
inv[1] = -(a[1] * (a[10] * a[15])) + a[1] * (a[11] * a[14]) - a[9] * (a[3] * a[14]) + (a[9] * (a[2] * a[15]) + (a[13] * (a[3] * a[10]) - a[13] * (a[2] * a[11])))
inv[5] = a[0] * (a[10] * a[15]) + -(a[0] * (a[11] * a[14])) - a[8] * (a[2] * a[15]) + (a[8] * (a[3] * a[14]) + (a[12] * (a[2] * a[11]) - a[12] * (a[3] * a[10])))
inv[9] = -(a[0] * (a[9] * a[15])) + a[0] * (a[11] * a[13]) - a[8] * (a[3] * a[13]) + (a[8] * (a[1] * a[15]) + (a[12] * (a[3] * a[9]) - a[12] * (a[1] * a[11])))
inv[13] = a[0] * (a[9] * a[14]) + -(a[0] * (a[10] * a[13])) - a[8] * (a[1] * a[14]) + (a[8] * (a[2] * a[13]) + (a[12] * (a[1] * a[10]) - a[12] * (a[2] * a[9])))
inv[2] = a[1] * (a[6] * a[15]) + -(a[1] * (a[7] * a[14])) - a[5] * (a[2] * a[15]) + (a[5] * (a[3] * a[14]) + (a[13] * (a[2] * a[7]) - a[13] * (a[3] * a[6])))
inv[6] = -(a[0] * (a[6] * a[15])) + a[0] * (a[7] * a[14]) - a[4] * (a[3] * a[14]) + (a[4] * (a[2] * a[15]) + (a[12] * (a[3] * a[6]) - a[12] * (a[2] * a[7])))
inv[10] = a[0] * (a[5] * a[15]) + -(a[0] * (a[7] * a[13])) - a[4] * (a[1] * a[15]) + (a[4] * (a[3] * a[13]) + (a[12] * (a[1] * a[7]) - a[12] * (a[3] * a[5])))
inv[14] = -(a[0] * (a[5] * a[14])) + a[0] * (a[6] * a[13]) - a[4] * (a[2] * a[13]) + (a[4] * (a[1] * a[14]) + (a[12] * (a[2] * a[5]) - a[12] * (a[1] * a[6])))
inv[3] = -(a[1] * (a[6] * a[11])) + a[1] * (a[7] * a[10]) - a[5] * (a[3] * a[10]) + (a[5] * (a[2] * a[11]) + (a[9] * (a[3] * a[6]) - a[9] * (a[2] * a[7])))
inv[7] = a[0] * (a[6] * a[11]) + -(a[0] * (a[7] * a[10])) - a[4] * (a[2] * a[11]) + (a[4] * (a[3] * a[10]) + (a[8] * (a[2] * a[7]) - a[8] * (a[3] * a[6])))
inv[11] = -(a[0] * (a[5] * a[11])) + a[0] * (a[7] * a[9]) - a[4] * (a[3] * a[9]) + (a[4] * (a[1] * a[11]) + (a[8] * (a[3] * a[5]) - a[8] * (a[1] * a[7])))
inv[15] = a[0] * (a[5] * a[10]) + -(a[0] * (a[6] * a[9])) - a[4] * (a[1] * a[10]) + (a[4] * (a[2] * a[9]) + (a[8] * (a[1] * a[6]) - a[8] * (a[2] * a[5])))
let det = a[0] * inv[0] + a[1] * inv[4] + (a[2] * inv[8] + a[3] * inv[12])
if det == 0.0 { free(inv); return false }
let i0 = a[5] * (a[10] * a[15]) + -(a[5] * (a[11] * a[14])) - a[9] * (a[6] * a[15]) + (a[9] * (a[7] * a[14]) + (a[13] * (a[6] * a[11]) - a[13] * (a[7] * a[10])))
let i4 = -(a[4] * (a[10] * a[15])) + a[4] * (a[11] * a[14]) - a[8] * (a[7] * a[14]) + (a[8] * (a[6] * a[15]) + (a[12] * (a[7] * a[10]) - a[12] * (a[6] * a[11])))
let i8 = a[4] * (a[9] * a[15]) + -(a[4] * (a[11] * a[13])) - a[8] * (a[5] * a[15]) + (a[8] * (a[7] * a[13]) + (a[12] * (a[5] * a[11]) - a[12] * (a[7] * a[9])))
let i12 = -(a[4] * (a[9] * a[14])) + a[4] * (a[10] * a[13]) - a[8] * (a[6] * a[13]) + (a[8] * (a[5] * a[14]) + (a[12] * (a[6] * a[9]) - a[12] * (a[5] * a[10])))
let i1 = -(a[1] * (a[10] * a[15])) + a[1] * (a[11] * a[14]) - a[9] * (a[3] * a[14]) + (a[9] * (a[2] * a[15]) + (a[13] * (a[3] * a[10]) - a[13] * (a[2] * a[11])))
let i5 = a[0] * (a[10] * a[15]) + -(a[0] * (a[11] * a[14])) - a[8] * (a[2] * a[15]) + (a[8] * (a[3] * a[14]) + (a[12] * (a[2] * a[11]) - a[12] * (a[3] * a[10])))
let i9 = -(a[0] * (a[9] * a[15])) + a[0] * (a[11] * a[13]) - a[8] * (a[3] * a[13]) + (a[8] * (a[1] * a[15]) + (a[12] * (a[3] * a[9]) - a[12] * (a[1] * a[11])))
let i13 = a[0] * (a[9] * a[14]) + -(a[0] * (a[10] * a[13])) - a[8] * (a[1] * a[14]) + (a[8] * (a[2] * a[13]) + (a[12] * (a[1] * a[10]) - a[12] * (a[2] * a[9])))
let i2 = a[1] * (a[6] * a[15]) + -(a[1] * (a[7] * a[14])) - a[5] * (a[2] * a[15]) + (a[5] * (a[3] * a[14]) + (a[13] * (a[2] * a[7]) - a[13] * (a[3] * a[6])))
let i6 = -(a[0] * (a[6] * a[15])) + a[0] * (a[7] * a[14]) - a[4] * (a[3] * a[14]) + (a[4] * (a[2] * a[15]) + (a[12] * (a[3] * a[6]) - a[12] * (a[2] * a[7])))
let i10 = a[0] * (a[5] * a[15]) + -(a[0] * (a[7] * a[13])) - a[4] * (a[1] * a[15]) + (a[4] * (a[3] * a[13]) + (a[12] * (a[1] * a[7]) - a[12] * (a[3] * a[5])))
let i14 = -(a[0] * (a[5] * a[14])) + a[0] * (a[6] * a[13]) - a[4] * (a[2] * a[13]) + (a[4] * (a[1] * a[14]) + (a[12] * (a[2] * a[5]) - a[12] * (a[1] * a[6])))
let i3 = -(a[1] * (a[6] * a[11])) + a[1] * (a[7] * a[10]) - a[5] * (a[3] * a[10]) + (a[5] * (a[2] * a[11]) + (a[9] * (a[3] * a[6]) - a[9] * (a[2] * a[7])))
let i7 = a[0] * (a[6] * a[11]) + -(a[0] * (a[7] * a[10])) - a[4] * (a[2] * a[11]) + (a[4] * (a[3] * a[10]) + (a[8] * (a[2] * a[7]) - a[8] * (a[3] * a[6])))
let i11 = -(a[0] * (a[5] * a[11])) + a[0] * (a[7] * a[9]) - a[4] * (a[3] * a[9]) + (a[4] * (a[1] * a[11]) + (a[8] * (a[3] * a[5]) - a[8] * (a[1] * a[7])))
let i15 = a[0] * (a[5] * a[10]) + -(a[0] * (a[6] * a[9])) - a[4] * (a[1] * a[10]) + (a[4] * (a[2] * a[9]) + (a[8] * (a[1] * a[6]) - a[8] * (a[2] * a[5])))
let det = a[0] * i0 + a[1] * i4 + (a[2] * i8 + a[3] * i12)
if det == 0.0 { return false }
let id = 1.0 / det
for i in 0 .. 16 { o[i] = inv[i] * id }
free(inv)
o[0] = i0 * id; o[1] = i1 * id; o[2] = i2 * id; o[3] = i3 * id; o[4] = i4 * id; o[5] = i5 * id; o[6] = i6 * id; o[7] = i7 * id
o[8] = i8 * id; o[9] = i9 * id; o[10] = i10 * id; o[11] = i11 * id; o[12] = i12 * id; o[13] = i13 * id; o[14] = i14 * id; o[15] = i15 * id
return true
}
# transform a point (w = 1) by m: o = m * (x, y, z, 1), returns w

View file

@ -51,6 +51,7 @@ function gltf_mat_remember(render3d_st: mut Render3dState, name: string, diff: i
render3d_st.gltf_mat_n += 1
}
@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 gltf_texture(render3d_st: mut Render3dState, uri: string, srgb: bool) -> int {
if render3d_st.gltf_tex_paths == null { render3d_st.gltf_tex_paths = new []pointer; render3d_st.gltf_tex_ids = words(256); render3d_st.gltf_tex_refs = words(256) }
var png: string = uri
@ -174,6 +175,7 @@ function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim {
}
# Load the mesh of the node called `node_name` from dir/file.
@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 gltf_load(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model {
render3d_st.gltf_dir = dir
let at = dir + "/" + file

View file

@ -4,15 +4,22 @@
# read after the load (ludic.anim's clips) still goes through gltf_load. A model loaded here is shared
# by every actor drawing it; model_release takes it out of the cache as it lets it go.
export function gltf_cached(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model {
if render3d_st.gltf_kept != null {
for i in 0 .. len(render3d_st.gltf_kept) {
if render3d_st.gltf_kept_node[i] == node_name and render3d_st.gltf_kept_file[i] == file and render3d_st.gltf_kept_dir[i] == dir { return render3d_st.gltf_kept[i] }
}
}
return gltf_cached_load(render3d_st, dir, file, node_name)
}
# the first ask for a model: loaded, and kept for every ask after it
@alloc_ok("a model loaded the first time it is asked for, kept for every ask after it")
function gltf_cached_load(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model {
if render3d_st.gltf_kept == null {
render3d_st.gltf_kept = new []Model
render3d_st.gltf_kept_dir = new []string
render3d_st.gltf_kept_file = new []string
render3d_st.gltf_kept_node = new []string
}
for i in 0 .. len(render3d_st.gltf_kept) {
if render3d_st.gltf_kept_node[i] == node_name and render3d_st.gltf_kept_file[i] == file and render3d_st.gltf_kept_dir[i] == dir { return render3d_st.gltf_kept[i] }
}
let m = gltf_load(render3d_st, dir, file, node_name)
push(render3d_st.gltf_kept, m)
push(render3d_st.gltf_kept_dir, dir)

View file

@ -357,6 +357,7 @@ function gpu_gl_usage(u: int) -> int {
}
# a new mesh, its vertex array bound: the vertex, attribute and index calls below describe it
@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 gpu_mesh_new(render3d_st: Render3dState) -> Mesh {
let m = new Mesh
m.attrs = words(GPU_MAX_ATTRS * GPU_ATTR_W)

View file

@ -336,6 +336,7 @@ const GVK_BLOCK_DEVICE: int = 67108864
const GVK_BLOCK_HOST: int = 67108864
const GVK_OWN_OVER: int = 16777216
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_mem_init(render3d_st: mut Render3dState) -> void {
if render3d_st.gvk_al_blk != null { return }
let zero: long = 0
@ -377,6 +378,7 @@ function gvk_fr_put(render3d_st: mut Render3dState, b: int, off: int, len_: int)
}
# Memory for a resource from its requirements (a VkMemoryRequirements): an allocation id, 0 if none.
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, image: bool) -> int {
gvk_mem_init(render3d_st)
let allowed = Vk.get_i32(req, VkMemoryRequirements_memoryTypeBits)

View file

@ -25,6 +25,7 @@ function gvk_read_spv(render3d_st: mut Render3dState, path: string) -> bytes {
return b
}
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_module(render3d_st: mut Render3dState, path: string) -> long {
let zero: long = 0
let spv = gvk_read_spv(render3d_st, path)
@ -49,6 +50,7 @@ function gvk_stage_first(render3d_st: Render3dState, p: int) -> int {
if render3d_st.gvk_prog_mesh != null and p < len(render3d_st.gvk_prog_mesh) and render3d_st.gvk_prog_mesh[p] == 1 { return VK_SHADER_STAGE_MESH_BIT_EXT }
return VK_SHADER_STAGE_VERTEX_BIT
}
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_dir: string) -> bool {
let zero: long = 0
if render3d_st.gvk_prog_var == null {
@ -125,6 +127,7 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di
function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: int) -> int { return gvk_compute_new_tex(render3d_st, name, n_bufs, 0) }
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_compute_new_tex(render3d_st: mut Render3dState, name: string, n_bufs: int, n_tex: int) -> int {
let zero: long = 0
if render3d_st.gvk_cp_pipe == null {
@ -592,6 +595,7 @@ function gvk_same(a: pointer, b: pointer, n: int) -> bool {
return true
}
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_uniform_blocks(render3d_st: mut Render3dState, p: int) -> void {
if render3d_st.gvk_ublk_v == null {
render3d_st.gvk_ublk_v = new []bytes; render3d_st.gvk_ublk_f = new []bytes; render3d_st.gvk_prog_tex = new []words; render3d_st.gvk_prog_unit = new []words
@ -861,6 +865,7 @@ function gvk_draw_set(render3d_st: mut Render3dState, p: int, tx: words, tx_w: i
}
# a new kept set for program p with the textures in gvk_sc_tmp, written and cached
@alloc_ok("the window changed size or a surface was lost: the screen, the swapchain and its images are made again")
function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
let zero: long = 0
let v = render3d_st.gvk_prog_var[p]
@ -995,6 +1000,7 @@ function gvk_hdr_metadata(render3d_st: Render3dState) -> void {
Vk.set_hdr_metadata_ext(render3d_st.gvk_dev, 1, chains, md)
}
@alloc_ok("the window changed size or a surface was lost: the screen, the swapchain and its images are made again")
function gvk_screen_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
if render3d_st.gvk_vp == null {
render3d_st.gvk_vp = words(4); render3d_st.gvk_sc = words(5); render3d_st.gvk_clear_rgba = floats(4)
@ -1024,6 +1030,7 @@ function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer {
# The view a pass draws into: level 0 only (an attachment view has exactly one level, and a target
# the exposure measure mipmaps has several), and one layer of an array image for a cascade drawn
# on its own. Keyed by the image's generation, so a replaced image never reuses a stale view.
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_view_of(render3d_st: mut Render3dState, tex: int, layer1: int) -> long {
if layer1 == 0 and render3d_st.gvk_tex_levels[tex] <= 1 and render3d_st.gvk_tex_layers[tex] <= 1 { return render3d_st.gvk_tex_view[tex] }
# keyed by numbers, not a string built on every call: the texture, its generation and the layer
@ -1383,6 +1390,7 @@ function gvk_present(render3d_st: mut Render3dState) -> void {
# The screen as a binary PPM, top row first. The frame so far is finished first, then read back;
# row 0 of the image is OpenGL's bottom row, so rows are written last to first, as gl_screenshot does.
@alloc_ok("asked for by the player or a tool, not by the frame")
function gvk_screenshot(render3d_st: mut Render3dState, path: string) -> bool {
# the screen image is PQ-encoded 10-bit while HDR is on, which an 8-bit PPM would misread
if render3d_st.gvk_hdr_on { print("r3d: vulkan: screenshots are SDR only - turn HDR output off to take one"); return false }
@ -1441,6 +1449,7 @@ function gvk_open(render3d_st: mut Render3dState, w: int, h: int, title: string)
# A variant is made once and shared: a program is immutable here, so every actor asking for the
# one it wants (actor.ludic, one per actor) gets the same handle, not its own modules and pipelines.
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_program_new(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> int {
let key = `{vs}|{fs}|{Text.replace(defines, "\n", ";")}`
if render3d_st.gpu_prog_ids == null { render3d_st.gpu_prog_ids = new []int; render3d_st.gpu_prog_keys = new []string }
@ -1655,6 +1664,7 @@ function gvk_surface_os(render3d_st: mut Render3dState, out: pointer) -> bool {
return true
}
@alloc_ok("the window changed size or a surface was lost: the screen, the swapchain and its images are made again")
function gvk_surface_make(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_surface != 0 { return true }
let out = bytes(8)
@ -1672,6 +1682,7 @@ function gvk_surface_make(render3d_st: mut Render3dState) -> bool {
}
# (Re)make the swapchain for a w x h client area. The old one is handed over and then destroyed.
@alloc_ok("the window changed size or a surface was lost: the screen, the swapchain and its images are made again")
function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
if not gvk_surface_make(render3d_st) { return false }
gvk_frame_wait(render3d_st)
@ -1854,6 +1865,7 @@ function gvk_prof(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_prof_state < 0 { render3d_st.gvk_prof_state = 0; if r3d_env_has(render3d_st, "R3D_VK_PROF") { render3d_st.gvk_prof_state = 1 } }
return render3d_st.gvk_prof_state == 1
}
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function gvk_prof_frame(render3d_st: mut Render3dState) -> void {
if not gvk_prof(render3d_st) { return }
render3d_st.gvk_prof_frames += 1
@ -1876,6 +1888,7 @@ function gvk_prof_frame(render3d_st: mut Render3dState) -> void {
# what was made and destroyed over those frames, and how long the caches are: a kind made every
# frame and never destroyed, or a cache that only grows, is a leak
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function gvk_prof_made(render3d_st: mut Render3dState) -> void {
let r = render3d_st
print(`r3d: vulkan made/destroyed: images {r.gvk_mk_img}/{r.gvk_mk_x_img}, views {r.gvk_mk_view}/{r.gvk_mk_x_view}, buffers {r.gvk_mk_buf}/{r.gvk_mk_x_buf}, memory {r.gvk_mk_mem}/{r.gvk_mk_x_mem}, samplers {r.gvk_mk_smp}, sets {r.gvk_mk_set}, pools {r.gvk_mk_dpool}, cmds {r.gvk_mk_cmd}`)
@ -1894,6 +1907,7 @@ function gvk_len_l(xs: []long) -> int { if xs == null { return 0 }; return len(
# state packs into one int, and the pass's formats into another; the program's last hit is tried
# first, then the entries it owns.
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_layout_id(render3d_st: mut Render3dState, m: Mesh) -> int {
if m == null or m.attrs == null { return 1 }
if m.vk_layout > 0 { return m.vk_layout }
@ -1975,6 +1989,7 @@ function gvk_pipeline_fast(render3d_st: mut Render3dState, p: int, m: Mesh, st:
}
# The one place the pipeline cache grows: once per variant the game draws (program, vertex layout,
# render state, formats), kept for the program's life - the fence's declared warm-up (plan 25.2)
@alloc_ok("pipeline cache: one per variant the game draws")
function gvk_pipe_build(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int, layout: int, state: int, bias: int, pass: int) -> long {
let pipe = gvk_pipeline(render3d_st, p, m, st, n_color, color_fmt, depth_fmt, samples)
push(render3d_st.gvk_pc_prog, p); push(render3d_st.gvk_pc_layout, layout); push(render3d_st.gvk_pc_state, state)

View file

@ -65,6 +65,7 @@ function gvk_tex_give_back(render3d_st: mut Render3dState, tex: int) -> void {
if render3d_st.gvk_tex_spare == null { render3d_st.gvk_tex_spare = new []int }
push(render3d_st.gvk_tex_spare, tex)
}
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_tex_new(render3d_st: mut Render3dState) -> int {
let zero: long = 0
if render3d_st.gvk_tex_image == null {
@ -176,6 +177,7 @@ function gvk_staging_free(render3d_st: mut Render3dState) -> void {
}
# (Re)make the image behind a handle: glTexImage2D on a texture that already has one replaces it.
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool, ifmt: int, w: int, h: int, layers: int, with_mips: bool) -> bool {
gvk_frame_wait(render3d_st) # the frame in flight may still read it
if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) { return false }
@ -571,6 +573,7 @@ function gvk_tex_sampler(render3d_st: mut Render3dState, tex: int, min_f: int, m
if tex > 0 and tex < len(render3d_st.gvk_tex_smp_sig) { render3d_st.gvk_tex_smp_sig[tex] = sig; render3d_st.gvk_tex_smp[tex] = s }
return s
}
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
let bias = float_bits(gvk_mip_bias(render3d_st))
# looked up by the numbers themselves: a key string built on every call (and a texture read two
@ -631,6 +634,7 @@ function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wra
# Storage a buffer was moved off, or freed, while the frame that used it has not been submitted:
# destroyed at gvk_retire_flush, after the frame's work is done.
@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 gvk_buf_new(render3d_st: mut Render3dState) -> int {
let zero: long = 0
if render3d_st.gvk_buf == null {

View file

@ -69,6 +69,7 @@ function overlay_init(render3d_st: mut Render3dState, font_dir: string) -> bool
# Load (or swap to) the font atlas in `font_dir`: font.png and font.json. A game calls it
# again to change fonts at run time - a language whose script the default atlas does not
# carry brings its own. Returns false, and keeps the font it had, if there is none there.
@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 overlay_font(render3d_st: mut Render3dState, font_dir: string) -> bool {
let meta = Fs.read_text(font_dir + "/font.json")
if meta == null { return false }

View file

@ -54,6 +54,7 @@ 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")
function post_init(render3d_st: mut Render3dState, w: int, h: int) -> void {
render3d_st.post_w = w; render3d_st.post_h = h
render3d_st.post_hdr = target_new(render3d_st, w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)

View file

@ -30,6 +30,7 @@ function prof_init(render3d_st: mut Render3dState) -> void {
}
# the slot index for `name`, registering it on first sight (order = frame order)
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_slot(render3d_st: mut Render3dState, name: pointer) -> int {
var i = 0
while i < render3d_st.prof_n {
@ -82,6 +83,7 @@ function prof_collect(render3d_st: mut Render3dState) -> void {
}
}
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_report(render3d_st: Render3dState) -> void {
if not render3d_st.prof_on { return }
print("")
@ -130,6 +132,7 @@ function prof_before_swap(render3d_st: mut Render3dState) -> void { if render3d_
# name it without keeping a timeline.
function prof_mark_start(render3d_st: mut Render3dState) -> void { if render3d_st.prof_on { render3d_st.prof_mark_last = gl_now_us(); render3d_st.prof_mark_best = 0; render3d_st.prof_mark_name = null } }
# ... and every phase's share of the average frame, past the first eight frames (loading, first fills)
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_cpu_mark(render3d_st: mut Render3dState, name: pointer) -> void {
if not render3d_st.prof_on { return }
let now = gl_now_us()
@ -158,6 +161,7 @@ function prof_layer_add(render3d_st: mut Render3dState, us: long, bytes: long) -
}
function prof_bake_add(render3d_st: mut Render3dState, us: long) -> void { if render3d_st.prof_on { render3d_st.prof_bake_us_cur = render3d_st.prof_bake_us_cur + us } }
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_chunk(render3d_st: mut Render3dState, kind: int, band: int, count: int, us: long) -> void {
if not render3d_st.prof_on { return }
if render3d_st.prof_chunk_us == null {
@ -169,6 +173,7 @@ function prof_chunk(render3d_st: mut Render3dState, kind: int, band: int, count:
render3d_st.prof_gen_us_cur += us
}
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_chunk_report(render3d_st: Render3dState) -> void {
if not render3d_st.prof_on or render3d_st.prof_chunk_us == null { return }
print("")
@ -220,6 +225,7 @@ function prof_chunk_report(render3d_st: Render3dState) -> void {
print(` generation per frame (us): p95 {string(sorted[(n * 95) / 100])} p99 {string(sorted[(n * 99) / 100])} worst {string(sorted[n - 1])} frames that generated: {string(busy)} of {string(n)} total {string(t / 1000)} ms`)
}
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_gen_frame(render3d_st: mut Render3dState) -> void {
ds_frame(render3d_st)
if not render3d_st.prof_on { return }
@ -256,6 +262,7 @@ function prof_gen_frame(render3d_st: mut Render3dState) -> void {
}
# the distribution of REAL frame times: stutter lives in the tail, not the mean
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_ft_report(render3d_st: Render3dState) -> void {
if not render3d_st.prof_on { return }
if render3d_st.prof_ft == null or len(render3d_st.prof_ft) < 16 { return }
@ -320,6 +327,7 @@ function prof_ft_report(render3d_st: Render3dState) -> void {
# The slowest frames of the run, with the once-in-a-while CPU work that landed in them.
# An average never shows a hitch; this is the list of the frames you actually felt.
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_hitch_report(render3d_st: Render3dState) -> void {
if not render3d_st.prof_on or render3d_st.prof_dt == null or len(render3d_st.prof_dt) < 32 { return }
let n = len(render3d_st.prof_dt)
@ -357,6 +365,7 @@ function prof_hitch_report(render3d_st: Render3dState) -> void {
}
}
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function prof_gen_report(render3d_st: Render3dState) -> void {
if not render3d_st.prof_on { return }
if render3d_st.prof_gen == null or len(render3d_st.prof_gen) < 8 { return }

View file

@ -37,6 +37,7 @@ function r3d_find_root(render3d_st: mut Render3dState) -> void {
# Build a program from a vertex + fragment file pair (defines apply to both).
# R3D_PROGRAMS_LOG=<file>: append every program built (vertex, fragment, defines) - the list a
# backend that cannot compile shaders at run time (Vulkan: SPIR-V is built ahead) must cover
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function r3d_program_log(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> void {
if render3d_st.r3d_prog_log < 0 { render3d_st.r3d_prog_log = 0; if r3d_env_has(render3d_st, "R3D_PROGRAMS_LOG") { render3d_st.r3d_prog_log = 1 } }
if render3d_st.r3d_prog_log == 0 { return }
@ -48,6 +49,7 @@ function r3d_program_log(render3d_st: mut Render3dState, vs: string, fs: string,
file_write(f, line, len(line))
file_close(f)
}
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function r3d_program(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> int {
r3d_program_log(render3d_st, vs, fs, defines)
let p = gpu_program(render3d_st, vs, fs, `{render3d_st.r3d_global_defs}{defines}`)
@ -67,6 +69,7 @@ property Target {
w: int = 0,
h: int = 0
}
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
function target_new(render3d_st: mut Render3dState, w: int, h: int, ifmt: int, fmt: int, ty: int, with_depth: bool, filter: int) -> Target {
let t = new Target
t.w = w; t.h = h

View file

@ -94,6 +94,7 @@ function sc_model_layout(render3d_st: Render3dState, m: Mesh) -> void {
# Two crossed unit quads (x in [-0.5, 0.5], y in [0, 1]), attribute 0 = pos,
# 1 = the quad's facing normal, 2 = uv. Scaled per layer to the atlas card size.
@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame")
function model_cross_card(render3d_st: mut Render3dState) -> Model {
let model = new Model
model.prims = new []Prim
@ -378,6 +379,7 @@ 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")
function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, foliage: bool, wind: float, near: float, cull: float) -> Layer {
let l = new Layer
l.model = model
@ -428,6 +430,7 @@ 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")
function impostor_bake(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int) -> Impostor {
let im = new Impostor
im.tiles = tiles
@ -583,6 +586,7 @@ function layer_arena_ok(render3d_st: Render3dState, l: Layer) -> bool {
return true
}
@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_arena_build(render3d_st: mut Render3dState, l: Layer) -> void {
let n_mat = len(l.lods[0].prims)
l.g_arena = new []Prim
@ -706,6 +710,7 @@ function layer_gpu_cull(render3d_st: mut Render3dState, l: Layer) -> void {
# Sort a static layer's instances into square cells (call once, after placement; a
# large layer that was never gridded gets a 96 m grid on its first update). The
# shadow buffer is uploaded here once — casters are never culled by the view.
@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_grid_build(render3d_st: mut Render3dState, l: Layer, cell: float) -> void {
if l.count == 0 { return }
var minx = l.inst[0]; var maxx = minx; var minz = l.inst[2]; var maxz = minz

View file

@ -185,6 +185,7 @@ function shadow_pass(render3d_st: mut Render3dState) -> void {
}
# Debug: cascade depths as grey PPMs (build/dbg_shadow_<c>.ppm)
@alloc_ok("asked for by the player or a tool, not by the frame")
function shadow_dump(render3d_st: mut Render3dState) -> void {
let n = render3d_st.shadow_res * render3d_st.shadow_res
let buf = floats(n * SHADOW_CASCADES)

View file

@ -72,6 +72,7 @@ function skin_attribs(render3d_st: mut Render3dState, m: Mesh, attrs: Val) -> bo
}
# the skin `idx` of the document being loaded (gltf_load holds gltf_doc / gltf_bin open)
@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 skin_load(render3d_st: mut Render3dState, idx: int) -> Skin {
let sk = new Skin
let nodes = value_get(render3d_st.gltf_doc, "nodes")

View file

@ -44,6 +44,7 @@ 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")
function stream_new(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
@ -315,6 +316,7 @@ function stream_chunk_visible(render3d_st: Render3dState, s: Stream, cx: int, cz
}
# what the caches hold, and whether they are being churned (R3D_PROF)
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
function stream_census(render3d_st: Render3dState) -> void {
if render3d_st.stream_all == null { return }
print("")

View file

@ -112,6 +112,7 @@ function png_unfilter(raw: pointer, cur: int, prev: int, stride: int, fbpp: int,
}
}
@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 png_decode(render3d_st: mut Render3dState, path: pointer) -> pointer {
let d = r3d_read_file(render3d_st, path)
if d == null { print(`png: cannot read {path}`); return null }
@ -260,8 +261,10 @@ function tex_upload(render3d_st: mut Render3dState, px: pointer, srgb: bool, mip
}
# Load a PNG as a mipmapped, anisotropic texture (0 on failure). srgb for albedo.
@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)")
function tex_load_ex(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)
@ -306,6 +309,7 @@ function tex_height(render3d_st: Render3dState, id: int) -> int {
}
# A small solid-colour fallback texture (linear rgb 0..255), for missing maps.
@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_solid(render3d_st: mut Render3dState, r: int, g: int, b: int, a: int) -> int {
let px = bytes(16)
for i in 0 .. 4 { px[i * 4] = r; px[i * 4 + 1] = g; px[i * 4 + 2] = b; px[i * 4 + 3] = a }
@ -453,6 +457,7 @@ function tex_max(render3d_st: mut Render3dState, tex: int, w: int, h: int, tag:
free(buf)
}
# Debug: write a 2D texture's level 0 (RGBA8, alpha dropped) as a binary PPM.
@alloc_ok("asked for by the player or a tool, not by the frame")
function tex_dump(render3d_st: mut Render3dState, tex: int, w: int, h: int, path: pointer) -> void {
let f = file_open(path, "wb")
if f == null { return }

View file

@ -18,6 +18,7 @@ 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)")
function tex_load_dds(render3d_st: mut Render3dState, path: string, srgb: bool) -> int {
let b = Fs.read_bytes(path)
if b == null { return 0 }

View file

@ -196,6 +196,7 @@ function wb_wet(render3d_st: mut Render3dState, x: float, z: float, off: float)
# walk of every rectangle, every frame. The body beyond the map (a sea runs far past it) is 64 x 64
# coarse rectangles tested at their centre, in one last block, which only ever matter kilometres away.
const WB_BLOCK: int = 8
@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame")
function water_cells_build(render3d_st: mut Render3dState) -> void {
let bx0 = render3d_st.water_cx - render3d_st.water_ex; let bx1 = render3d_st.water_cx + render3d_st.water_ex
let bz0 = render3d_st.water_cz - render3d_st.water_ez; let bz1 = render3d_st.water_cz + render3d_st.water_ez
@ -256,6 +257,7 @@ function water_cells_build(render3d_st: mut Render3dState) -> void {
}
# the program and the plane are the process's, built once; the bodies are the map's
@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame")
function water_setup(render3d_st: mut Render3dState) -> void {
if render3d_st.water_prog != 0 { return }
render3d_st.water_mesh = mesh_grid(render3d_st, 2, 0.5)