render3d: shadow_fit, water_reflection_pass, layer_partition_lods and the GPU cull's scratch are made with the state; v3_dist is scalar; the pushes into lists sized at start-up, the caps probe, the table growth, the loads and the constructors declared with their bounds (one statement a line); the renderer's name made once with the device; the two error messages given back; the dead lupine models removed. runtime: a component's text is held interned in its value cell (one copy per distinct text), so the getter's own text goes with its frame instead of being kept by ludic.ui's model - 80 of the 83 keeps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
9.7 KiB
Text
192 lines
9.7 KiB
Text
# ============================================================================
|
|
# drawstats.ludic — every draw a frame makes, by pass, program and kind
|
|
# (R3D_DRAWSTATS=<frames>).
|
|
#
|
|
# Measurement only. Every draw on both backends goes through the five doors in
|
|
# gpu.ludic (gpu_draw_mesh, _instanced, _bound_elements, _range, _mesh_indirect);
|
|
# each door tallies itself here under the profiler pass that is open (prof_begin)
|
|
# and the program that is bound. Nothing about the frame changes.
|
|
#
|
|
# R3D_DRAWSTATS=<n> count n frames (default 60 when not a number)
|
|
# R3D_DRAWSTATS_FROM=<f> start at render frame f (default 90: streaming settled)
|
|
# R3D_DRAWSTATS_CSV=<path> the per-row table (default build/drawstats.csv)
|
|
#
|
|
# The report prints when the window closes, so any run long enough to reach
|
|
# f + n frames produces it - a headless shot, a bench, a windowed walk.
|
|
#
|
|
# An indirect draw is one API call carrying n command records; its "instances"
|
|
# column is the record count, and whether the GPU's culling emptied a record is
|
|
# not known here (see R3D_VK_PROF for the Vulkan side's own count).
|
|
# ============================================================================
|
|
|
|
const DS_MESH: int = 0
|
|
const DS_INSTANCED: int = 1
|
|
const DS_PATCH: int = 2
|
|
const DS_RANGE: int = 3
|
|
const DS_INDIRECT: int = 4
|
|
const DS_KINDS: int = 5
|
|
|
|
|
|
# passes, in the order the frame first opens them
|
|
|
|
# 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
|
|
if r3d_env_has(render3d_st, "R3D_DRAWSTATS") {
|
|
render3d_st.ds_on = 1
|
|
render3d_st.ds_frames = Text.to_int(r3d_env(render3d_st, "R3D_DRAWSTATS"))
|
|
if render3d_st.ds_frames < 2 { render3d_st.ds_frames = 60 }
|
|
if r3d_env_has(render3d_st, "R3D_DRAWSTATS_FROM") { render3d_st.ds_from = Text.to_int(r3d_env(render3d_st, "R3D_DRAWSTATS_FROM")) }
|
|
render3d_st.ds_pass_names = new []pointer; render3d_st.ds_pass_prog = new []long; render3d_st.ds_pass_tex = new []long
|
|
render3d_st.ds_row_pass = new []int; render3d_st.ds_row_prog = new []int; render3d_st.ds_row_kind = new []int
|
|
render3d_st.ds_row_draws = new []long; render3d_st.ds_row_inst = new []long; render3d_st.ds_row_idx = new []long
|
|
ds_set_pass(render3d_st, render3d_st.ds_outside)
|
|
}
|
|
}
|
|
return render3d_st.ds_on == 1
|
|
}
|
|
|
|
# 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
|
|
var i = 0
|
|
while i < len(render3d_st.ds_pass_names) {
|
|
if render3d_st.ds_pass_names[i] == name { render3d_st.ds_pass_i = i; return }
|
|
i += 1
|
|
}
|
|
push(render3d_st.ds_pass_names, name); push(render3d_st.ds_pass_prog, 0); push(render3d_st.ds_pass_tex, 0)
|
|
render3d_st.ds_pass_i = len(render3d_st.ds_pass_names) - 1
|
|
}
|
|
|
|
# once per render frame, before any of its work (prof_gen_frame)
|
|
function ds_frame(render3d_st: mut Render3dState) -> void {
|
|
if not ds_enabled(render3d_st) { return }
|
|
render3d_st.ds_frame_no += 1
|
|
ds_set_pass(render3d_st, render3d_st.ds_outside)
|
|
if render3d_st.ds_frame_no == render3d_st.ds_from {
|
|
render3d_st.ds_counting = true
|
|
print(`r3d: drawstats counting frames {render3d_st.ds_from}..{render3d_st.ds_from + render3d_st.ds_frames - 1}`)
|
|
}
|
|
if render3d_st.ds_frame_no == render3d_st.ds_from + render3d_st.ds_frames {
|
|
render3d_st.ds_counting = false
|
|
ds_report(render3d_st)
|
|
}
|
|
}
|
|
|
|
@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
|
|
var r = render3d_st.ds_last
|
|
if r < 0 or render3d_st.ds_row_prog[r] != p or render3d_st.ds_row_kind[r] != kind or render3d_st.ds_row_pass[r] != render3d_st.ds_pass_i {
|
|
r = -1
|
|
var i = 0
|
|
while r < 0 and i < len(render3d_st.ds_row_pass) {
|
|
if render3d_st.ds_row_pass[i] == render3d_st.ds_pass_i and render3d_st.ds_row_prog[i] == p and render3d_st.ds_row_kind[i] == kind { r = i }
|
|
i += 1
|
|
}
|
|
if r < 0 {
|
|
push(render3d_st.ds_row_pass, render3d_st.ds_pass_i); push(render3d_st.ds_row_prog, p); push(render3d_st.ds_row_kind, kind)
|
|
push(render3d_st.ds_row_draws, 0); push(render3d_st.ds_row_inst, 0); push(render3d_st.ds_row_idx, 0)
|
|
r = len(render3d_st.ds_row_pass) - 1
|
|
}
|
|
render3d_st.ds_last = r
|
|
}
|
|
render3d_st.ds_row_draws[r] = render3d_st.ds_row_draws[r] + 1
|
|
render3d_st.ds_row_inst[r] = render3d_st.ds_row_inst[r] + instances
|
|
render3d_st.ds_row_idx[r] = render3d_st.ds_row_idx[r] + indices
|
|
}
|
|
|
|
function ds_program_change(render3d_st: mut Render3dState, from: int, to: int) -> void {
|
|
if render3d_st.ds_on != 1 or not render3d_st.ds_counting or from == to { return }
|
|
render3d_st.ds_pass_prog[render3d_st.ds_pass_i] = render3d_st.ds_pass_prog[render3d_st.ds_pass_i] + 1
|
|
}
|
|
function ds_tex_bind(render3d_st: mut Render3dState) -> void {
|
|
if render3d_st.ds_on != 1 or not render3d_st.ds_counting { return }
|
|
render3d_st.ds_pass_tex[render3d_st.ds_pass_i] = render3d_st.ds_pass_tex[render3d_st.ds_pass_i] + 1
|
|
}
|
|
|
|
function ds_kind_name(k: int) -> pointer {
|
|
if k == DS_MESH { return "mesh" }
|
|
if k == DS_INSTANCED { return "instanced" }
|
|
if k == DS_PATCH { return "terrain patch" }
|
|
if k == DS_RANGE { return "overlay range" }
|
|
return "indirect"
|
|
}
|
|
# a per-frame mean with one decimal
|
|
@alloc_ok("the draw-count report: printed once, at the end of a run that asked for it")
|
|
function ds_per(render3d_st: Render3dState, v: long) -> string {
|
|
let x = (v * 10) / render3d_st.ds_frames
|
|
return `{string(x / 10)}.{string(x % 10)}`
|
|
}
|
|
function ds_backend(render3d_st: Render3dState) -> pointer {
|
|
if render3d_st.gpu_kind == GPU_VK { return "vulkan" }
|
|
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)
|
|
var total: long = 0
|
|
let by_kind = new []long
|
|
for k in 0 .. DS_KINDS { push(by_kind, 0) }
|
|
let pass_draws = new []long
|
|
let pass_inst = new []long
|
|
for s in 0 .. n_pass { push(pass_draws, 0); push(pass_inst, 0) }
|
|
for r in 0 .. n_rows {
|
|
total = total + render3d_st.ds_row_draws[r]
|
|
by_kind[render3d_st.ds_row_kind[r]] = by_kind[render3d_st.ds_row_kind[r]] + render3d_st.ds_row_draws[r]
|
|
pass_draws[render3d_st.ds_row_pass[r]] = pass_draws[render3d_st.ds_row_pass[r]] + render3d_st.ds_row_draws[r]
|
|
pass_inst[render3d_st.ds_row_pass[r]] = pass_inst[render3d_st.ds_row_pass[r]] + render3d_st.ds_row_inst[r]
|
|
}
|
|
print("")
|
|
print(`draw calls per frame ({ds_backend(render3d_st)}, {gl_width()}x{gl_height()}, mean of {render3d_st.ds_frames} frames from {render3d_st.ds_from}): {ds_per(render3d_st, total)}`)
|
|
var kinds = ""
|
|
for k in 0 .. DS_KINDS { kinds = kinds + ` {ds_kind_name(k)} {ds_per(render3d_st, by_kind[k])}` }
|
|
print(` by kind:{kinds}`)
|
|
print("")
|
|
print(" pass draws instances program changes texture binds")
|
|
for s in 0 .. n_pass {
|
|
print(` {Text.pad_right(render3d_st.ds_pass_names[s], 24)} {Text.pad_left(ds_per(render3d_st, pass_draws[s]), 9)} {Text.pad_left(ds_per(render3d_st, pass_inst[s]), 11)} {Text.pad_left(ds_per(render3d_st, render3d_st.ds_pass_prog[s]), 17)} {Text.pad_left(ds_per(render3d_st, render3d_st.ds_pass_tex[s]), 15)}`)
|
|
}
|
|
# rows by draws, most first
|
|
let order = new []int
|
|
for r in 0 .. n_rows { push(order, r) }
|
|
var a = 1
|
|
while a < n_rows {
|
|
let v = order[a]
|
|
var b = a - 1
|
|
while b >= 0 and render3d_st.ds_row_draws[order[b]] < render3d_st.ds_row_draws[v] { order[b + 1] = order[b]; b -= 1 }
|
|
order[b + 1] = v
|
|
a += 1
|
|
}
|
|
print("")
|
|
print(" the 40 largest rows (pass / kind / program):")
|
|
print(" draws instances indices pass kind program")
|
|
var shown = 0
|
|
while shown < 40 and shown < n_rows {
|
|
let r = order[shown]
|
|
print(` {Text.pad_left(ds_per(render3d_st, render3d_st.ds_row_draws[r]), 9)} {Text.pad_left(ds_per(render3d_st, render3d_st.ds_row_inst[r]), 11)} {Text.pad_left(ds_per(render3d_st, render3d_st.ds_row_idx[r]), 12)} {Text.pad_right(render3d_st.ds_pass_names[render3d_st.ds_row_pass[r]], 24)} {Text.pad_right(ds_kind_name(render3d_st.ds_row_kind[r]), 14)} {gpu_program_key(render3d_st, render3d_st.ds_row_prog[r])}`)
|
|
shown += 1
|
|
}
|
|
var path = "build/drawstats.csv"
|
|
if r3d_env_has(render3d_st, "R3D_DRAWSTATS_CSV") { path = r3d_env(render3d_st, "R3D_DRAWSTATS_CSV") }
|
|
let f = file_open(path, "wb")
|
|
if f == null { print(`r3d: drawstats could not write {path}`); return }
|
|
let head = "backend,width,height,frames,pass,kind,program,draws_per_frame,instances_per_frame,indices_per_frame,pass_program_changes_per_frame,pass_texture_binds_per_frame\n"
|
|
file_write(f, head, len(head))
|
|
for i in 0 .. n_rows {
|
|
let r = order[i]
|
|
let s = render3d_st.ds_row_pass[r]
|
|
let line = `{ds_backend(render3d_st)},{gl_width()},{gl_height()},{render3d_st.ds_frames},"{render3d_st.ds_pass_names[s]}",{ds_kind_name(render3d_st.ds_row_kind[r])},"{gpu_program_key(render3d_st, render3d_st.ds_row_prog[r])}",{ds_per(render3d_st, render3d_st.ds_row_draws[r])},{ds_per(render3d_st, render3d_st.ds_row_inst[r])},{ds_per(render3d_st, render3d_st.ds_row_idx[r])},{ds_per(render3d_st, render3d_st.ds_pass_prog[s])},{ds_per(render3d_st, render3d_st.ds_pass_tex[s])}\n`
|
|
file_write(f, line, len(line))
|
|
}
|
|
file_close(f)
|
|
print(` wrote {path} ({n_rows} rows)`)
|
|
}
|