# ============================================================================ # drawstats.ludic — every draw a frame makes, by pass, program and kind # (R3D_DRAWSTATS=). # # 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= count n frames (default 60 when not a number) # R3D_DRAWSTATS_FROM= start at render frame f (default 90: streaming settled) # R3D_DRAWSTATS_CSV= 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)`) }