feat(render3d): R3D_DRAWSTATS - every draw a frame makes, by pass, program and kind
Measurement only: a tally at each of gpu.ludic's five draw doors, program changes and texture binds, keyed by the open profiler pass. Off unless R3D_DRAWSTATS is set; frames byte-identical with it off and on (Mac, OpenGL). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5b28063b52
commit
b9eda646fc
4 changed files with 215 additions and 1 deletions
204
packages/ludic.render3d/drawstats.ludic
Normal file
204
packages/ludic.render3d/drawstats.ludic
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# ============================================================================
|
||||
# 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
|
||||
|
||||
var ds_on: int = -1 # -1: not read yet
|
||||
var ds_counting: bool = false
|
||||
var ds_from: int = 90
|
||||
var ds_frames: int = 60
|
||||
var ds_frame_no: int = 0
|
||||
var ds_outside: pointer = "(outside passes)"
|
||||
|
||||
# passes, in the order the frame first opens them
|
||||
var ds_pass_names: []pointer = null
|
||||
var ds_pass_prog: []long = null # program changes inside the pass
|
||||
var ds_pass_tex: []long = null # texture binds inside the pass
|
||||
var ds_pass_i: int = 0
|
||||
|
||||
# rows: (pass, program, kind)
|
||||
var ds_row_pass: []int = null
|
||||
var ds_row_prog: []int = null
|
||||
var ds_row_kind: []int = null
|
||||
var ds_row_draws: []long = null
|
||||
var ds_row_inst: []long = null
|
||||
var ds_row_idx: []long = null
|
||||
var ds_last: int = -1
|
||||
|
||||
function ds_enabled() -> bool {
|
||||
if ds_on < 0 {
|
||||
ds_on = 0
|
||||
if Os.has_env("R3D_DRAWSTATS") {
|
||||
ds_on = 1
|
||||
ds_frames = Text.to_int(Os.env("R3D_DRAWSTATS"))
|
||||
if ds_frames < 2 { ds_frames = 60 }
|
||||
if Os.has_env("R3D_DRAWSTATS_FROM") { ds_from = Text.to_int(Os.env("R3D_DRAWSTATS_FROM")) }
|
||||
ds_pass_names = new []pointer; ds_pass_prog = new []long; ds_pass_tex = new []long
|
||||
ds_row_pass = new []int; ds_row_prog = new []int; ds_row_kind = new []int
|
||||
ds_row_draws = new []long; ds_row_inst = new []long; ds_row_idx = new []long
|
||||
ds_set_pass(ds_outside)
|
||||
}
|
||||
}
|
||||
return ds_on == 1
|
||||
}
|
||||
|
||||
# the profiler's pass boundaries (prof_begin / prof_end), whether or not R3D_PROF is on
|
||||
function ds_set_pass(name: pointer) -> void {
|
||||
if ds_on != 1 { return }
|
||||
ds_last = -1
|
||||
var i = 0
|
||||
while i < len(ds_pass_names) {
|
||||
if ds_pass_names[i] == name { ds_pass_i = i; return }
|
||||
i += 1
|
||||
}
|
||||
push(ds_pass_names, name); push(ds_pass_prog, 0); push(ds_pass_tex, 0)
|
||||
ds_pass_i = len(ds_pass_names) - 1
|
||||
}
|
||||
|
||||
# once per render frame, before any of its work (prof_gen_frame)
|
||||
function ds_frame() -> void {
|
||||
if not ds_enabled() { return }
|
||||
ds_frame_no += 1
|
||||
ds_set_pass(ds_outside)
|
||||
if ds_frame_no == ds_from {
|
||||
ds_counting = true
|
||||
print(`r3d: drawstats counting frames {ds_from}..{ds_from + ds_frames - 1}`)
|
||||
}
|
||||
if ds_frame_no == ds_from + ds_frames {
|
||||
ds_counting = false
|
||||
ds_report()
|
||||
}
|
||||
}
|
||||
|
||||
function ds_draw(kind: int, instances: int, indices: int) -> void {
|
||||
if ds_on != 1 or not ds_counting { return }
|
||||
let p = gpu_prog_cur
|
||||
var r = ds_last
|
||||
if r < 0 or ds_row_prog[r] != p or ds_row_kind[r] != kind or ds_row_pass[r] != ds_pass_i {
|
||||
r = -1
|
||||
var i = 0
|
||||
while r < 0 and i < len(ds_row_pass) {
|
||||
if ds_row_pass[i] == ds_pass_i and ds_row_prog[i] == p and ds_row_kind[i] == kind { r = i }
|
||||
i += 1
|
||||
}
|
||||
if r < 0 {
|
||||
push(ds_row_pass, ds_pass_i); push(ds_row_prog, p); push(ds_row_kind, kind)
|
||||
push(ds_row_draws, 0); push(ds_row_inst, 0); push(ds_row_idx, 0)
|
||||
r = len(ds_row_pass) - 1
|
||||
}
|
||||
ds_last = r
|
||||
}
|
||||
ds_row_draws[r] = ds_row_draws[r] + 1
|
||||
ds_row_inst[r] = ds_row_inst[r] + instances
|
||||
ds_row_idx[r] = ds_row_idx[r] + indices
|
||||
}
|
||||
|
||||
function ds_program_change(from: int, to: int) -> void {
|
||||
if ds_on != 1 or not ds_counting or from == to { return }
|
||||
ds_pass_prog[ds_pass_i] = ds_pass_prog[ds_pass_i] + 1
|
||||
}
|
||||
function ds_tex_bind() -> void {
|
||||
if ds_on != 1 or not ds_counting { return }
|
||||
ds_pass_tex[ds_pass_i] = ds_pass_tex[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
|
||||
function ds_per(v: long) -> string {
|
||||
let x = (v * 10) / ds_frames
|
||||
return `{string(x / 10)}.{string(x % 10)}`
|
||||
}
|
||||
function ds_backend() -> pointer {
|
||||
if gpu_kind == GPU_VK { return "vulkan" }
|
||||
return "opengl"
|
||||
}
|
||||
|
||||
function ds_report() -> void {
|
||||
let n_rows = len(ds_row_pass)
|
||||
let n_pass = len(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 + ds_row_draws[r]
|
||||
by_kind[ds_row_kind[r]] = by_kind[ds_row_kind[r]] + ds_row_draws[r]
|
||||
pass_draws[ds_row_pass[r]] = pass_draws[ds_row_pass[r]] + ds_row_draws[r]
|
||||
pass_inst[ds_row_pass[r]] = pass_inst[ds_row_pass[r]] + ds_row_inst[r]
|
||||
}
|
||||
print("")
|
||||
print(`draw calls per frame ({ds_backend()}, {gl_w}x{gl_h}, mean of {ds_frames} frames from {ds_from}): {ds_per(total)}`)
|
||||
var kinds = ""
|
||||
for k in 0 .. DS_KINDS { kinds = kinds + ` {ds_kind_name(k)} {ds_per(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(ds_pass_names[s], 24)} {Text.pad_left(ds_per(pass_draws[s]), 9)} {Text.pad_left(ds_per(pass_inst[s]), 11)} {Text.pad_left(ds_per(ds_pass_prog[s]), 17)} {Text.pad_left(ds_per(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 ds_row_draws[order[b]] < 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(ds_row_draws[r]), 9)} {Text.pad_left(ds_per(ds_row_inst[r]), 11)} {Text.pad_left(ds_per(ds_row_idx[r]), 12)} {Text.pad_right(ds_pass_names[ds_row_pass[r]], 24)} {Text.pad_right(ds_kind_name(ds_row_kind[r]), 14)} {gpu_program_key(ds_row_prog[r])}`)
|
||||
shown += 1
|
||||
}
|
||||
var path = "build/drawstats.csv"
|
||||
if Os.has_env("R3D_DRAWSTATS_CSV") { path = Os.env("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 = ds_row_pass[r]
|
||||
let line = `{ds_backend()},{gl_w},{gl_h},{ds_frames},"{ds_pass_names[s]}",{ds_kind_name(ds_row_kind[r])},"{gpu_program_key(ds_row_prog[r])}",{ds_per(ds_row_draws[r])},{ds_per(ds_row_inst[r])},{ds_per(ds_row_idx[r])},{ds_per(ds_pass_prog[s])},{ds_per(ds_pass_tex[s])}\n`
|
||||
file_write(f, line, len(line))
|
||||
}
|
||||
file_close(f)
|
||||
print(` wrote {path} ({n_rows} rows)`)
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ function gpu_program_key(p: int) -> string {
|
|||
for i in 0 .. len(gpu_prog_ids) { if gpu_prog_ids[i] == p { return gpu_prog_keys[i] } }
|
||||
return ""
|
||||
}
|
||||
function gpu_use_program(p: int) -> void { if gpu_kind == GPU_VK { gpu_prog_cur = p; return }; gl_use_program(p); gpu_prog_cur = p; gpu_glcheck_after("use program") }
|
||||
function gpu_use_program(p: int) -> void { ds_program_change(gpu_prog_cur, p); if gpu_kind == GPU_VK { gpu_prog_cur = p; return }; gl_use_program(p); gpu_prog_cur = p; gpu_glcheck_after("use program") }
|
||||
function gpu_program_free(p: int) -> void {
|
||||
if gpu_kind == GPU_VK { return }
|
||||
if p == 0 { return }
|
||||
|
|
@ -534,6 +534,7 @@ function gpu_finish() -> void { if gpu_kind == GPU_VK { gvk_flush() } }
|
|||
# (bytes); each record's firstInstance selects its instances out of the bound instance buffer.
|
||||
# With count_buf > 0 the GPU's own count (a uint at count_off) is used, up to n.
|
||||
function gpu_draw_mesh_indirect(m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void {
|
||||
if m != null { ds_draw(DS_INDIRECT, n, m.count) }
|
||||
if gpu_kind == GPU_VK { gvk_draw_indirect_now(m, cmds, offset, n, count_buf, count_off) }
|
||||
}
|
||||
|
||||
|
|
@ -541,6 +542,7 @@ function gpu_draw_mesh_indirect(m: Mesh, cmds: int, offset: int, n: int, count_b
|
|||
function gpu_mesh_bind(m: Mesh) -> void { if gpu_kind == GPU_VK { return }; gl_bind_vertex_array(m.vao) }
|
||||
function gpu_mesh_unbind() -> void { if gpu_kind == GPU_VK { return }; gl_bind_vertex_array(0) }
|
||||
function gpu_draw_mesh(m: Mesh) -> void {
|
||||
ds_draw(DS_MESH, 1, m.count)
|
||||
if gpu_kind == GPU_VK { gvk_draw_now(m, 0, 0, 1); return }
|
||||
gpu_glcheck_before("a draw")
|
||||
gl_bind_vertex_array(m.vao)
|
||||
|
|
@ -549,6 +551,7 @@ function gpu_draw_mesh(m: Mesh) -> void {
|
|||
gpu_glcheck_after("a draw")
|
||||
}
|
||||
function gpu_draw_mesh_instanced(m: Mesh, n: int) -> void {
|
||||
ds_draw(DS_INSTANCED, n, m.count * n)
|
||||
if gpu_kind == GPU_VK { gvk_draw_now(m, 0, 0, n); return }
|
||||
gpu_glcheck_before("an instanced draw")
|
||||
gl_bind_vertex_array(m.vao)
|
||||
|
|
@ -558,6 +561,7 @@ function gpu_draw_mesh_instanced(m: Mesh, n: int) -> void {
|
|||
}
|
||||
# the bound mesh's indices again (a patch mesh drawn once per terrain node)
|
||||
function gpu_draw_bound_elements(m: Mesh) -> void {
|
||||
ds_draw(DS_PATCH, 1, m.count)
|
||||
if gpu_kind == GPU_VK { gvk_draw_now(m, 0, 0, 1); return }
|
||||
gpu_glcheck_before("a terrain patch")
|
||||
gl_draw_elements(m.mode, m.count, m.itype, null)
|
||||
|
|
@ -565,6 +569,7 @@ function gpu_draw_bound_elements(m: Mesh) -> void {
|
|||
}
|
||||
# vertices [first, first + count) of the bound mesh, as triangles (the overlay's ranges)
|
||||
function gpu_draw_range(m: Mesh, first: int, count: int) -> void {
|
||||
ds_draw(DS_RANGE, 1, count)
|
||||
if gpu_kind == GPU_VK { gvk_draw_now(m, first, count, 1); return }
|
||||
gpu_glcheck_before("an overlay draw")
|
||||
gl_draw_arrays(GL_TRIANGLES, first, count)
|
||||
|
|
@ -623,6 +628,7 @@ var gpu_unit_2d: words = null
|
|||
var gpu_unit_cur: int = 0
|
||||
function gpu_tex_unit(unit: int) -> void { if gpu_kind == GPU_VK { gpu_unit_cur = unit; return }; gl_active_texture(GL_TEXTURE0 + unit); gpu_unit_cur = unit }
|
||||
function gpu_tex_bind(kind: int, tex: int) -> void {
|
||||
ds_tex_bind()
|
||||
if gpu_kind != GPU_VK { gl_bind_texture(gpu_gl_target(kind), tex) }
|
||||
if kind == GPU_TEX2D_ARRAY { gpu_bound_array = tex } else { gpu_bound_2d = tex }
|
||||
if kind != GPU_TEX2D_ARRAY and gpu_unit_cur < 32 {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ function prof_slot(name: pointer) -> int {
|
|||
}
|
||||
|
||||
function prof_begin(name: pointer) -> void {
|
||||
ds_set_pass(name)
|
||||
if not prof_on { return }
|
||||
if prof_active >= 0 { return } # GL_TIME_ELAPSED queries cannot nest
|
||||
let s = prof_slot(name)
|
||||
|
|
@ -63,6 +64,7 @@ function prof_begin(name: pointer) -> void {
|
|||
}
|
||||
|
||||
function prof_end() -> void {
|
||||
ds_set_pass(ds_outside)
|
||||
if not prof_on { return }
|
||||
if prof_active < 0 { return }
|
||||
gpu_query_end()
|
||||
|
|
@ -241,6 +243,7 @@ function prof_chunk_report() -> void {
|
|||
}
|
||||
|
||||
function prof_gen_frame() -> void {
|
||||
ds_frame()
|
||||
if not prof_on { return }
|
||||
if prof_gen == null {
|
||||
prof_gen = new []int; prof_ft = new []long
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import "gpu_manifest.ludic"
|
|||
import "gpu_vk.ludic"
|
||||
import "gpu_vk_res.ludic"
|
||||
import "prof.ludic"
|
||||
import "drawstats.ludic"
|
||||
import "programs.ludic"
|
||||
import "texture.ludic"
|
||||
import "mesh.ludic"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue