Break the GPU draw total down by render pass

Rendering at a quarter resolution changed the GPU time not at all, which rules
out fill rate, fragment shading and tile traffic in one measurement, since all
three scale with pixels. What is left inside the passes is geometry, binning and
per-draw cost. It also retires the tile bandwidth theory the previous two
attempts were built on: that traffic would have fallen sixteen fold.

So the draw total needs splitting, and the timer already measures each pass
individually and only reports the sum. Report the distribution instead, keyed by
the pass ordinal within the frame: the frame structure is stable, so pass N is
the same logical pass each time, which is what makes it something to act on.

Count draws per pass alongside it, on the same ordinal. A pass that is expensive
holding few draws is expensive per draw; one holding most of the frame's draws
is carrying the geometry. Same milliseconds, opposite fixes.

Reporting only. No new timestamps and nothing recorded that was not already
being measured.
This commit is contained in:
jpolo1224
2026-08-10 03:04:10 -04:00
parent 6e15b16941
commit 5a6f32f93d
7 changed files with 136 additions and 2 deletions
+9 -1
View File
@@ -843,7 +843,15 @@ namespace rsx
// Counted here rather than at the backend call sites so every early return in
// VKGSRender::end is included; all of them route through this function.
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_draw_calls++;
if (rsx::prof::enabled()) [[unlikely]]
{
rsx::prof::g_draw_calls++;
if (rsx::prof::g_pass_ordinal < rsx::prof::pass_slot_count)
{
rsx::prof::g_pass_draws[rsx::prof::g_pass_ordinal]++;
}
}
method_registers.current_draw_clause.post_execute_cleanup(m_ctx);
+15
View File
@@ -552,6 +552,21 @@ void VKGSRender::flip(const rsx::display_flip_info_t& info)
static_cast<double>(counts[i]) / static_cast<double>(timer.collected_frames()));
}
// Per-pass, so the draw total stops being a single number that only says "inside
// render passes". One pass carrying most of it is a target; thirty even ones mean
// the pass and draw count is the wall.
if (const auto passes = timer.draw_pass_costs(); !passes.empty())
{
std::string list;
for (u32 i = 0; i < passes.size() && i < 8; i++)
{
fmt::append(list, "\n\t pass #%-3u %7.3f ms/frame seen %llu",
passes[i].ordinal, passes[i].ms_per_frame, passes[i].samples);
}
fmt::append(report, "\n\tdraw by pass (%u distinct)%s", ::size32(passes), list);
}
if (const u64 dropped = timer.dropped_events())
{
// Untimed events mean the regions below are an underestimate, so say so
+1
View File
@@ -429,6 +429,7 @@ namespace vk
if (rsx::prof::enabled()) [[unlikely]]
{
rsx::prof::g_render_passes++;
rsx::prof::g_pass_ordinal++;
}
// The draw region was declared and never recorded anywhere, so the one figure that
+42 -1
View File
@@ -333,8 +333,16 @@ namespace vk
continue;
}
m_totals_ns[i] += static_cast<u64>(static_cast<double>(t1 - t0) * m_period_ns);
const u64 ns = static_cast<u64>(static_cast<double>(t1 - t0) * m_period_ns);
m_totals_ns[i] += ns;
m_events_seen[i]++;
if (r == region::draw)
{
m_draw_pass_ns[e] += ns;
m_draw_pass_samples[e]++;
}
}
m_dropped += state.dropped[i];
@@ -362,10 +370,43 @@ namespace vk
return out;
}
std::vector<gpu_timer::pass_cost> gpu_timer::draw_pass_costs() const
{
std::vector<pass_cost> out;
if (!m_frames)
{
return out;
}
for (u32 e = 0; e < max_events; e++)
{
if (!m_draw_pass_samples[e])
{
continue;
}
// Divided by frames, not by samples, so the entries sum to the draw region total
// and each one reads as its share of the frame rather than its cost when present.
out.push_back({
e,
static_cast<double>(m_draw_pass_ns[e]) / 1'000'000.0 / static_cast<double>(m_frames),
m_draw_pass_samples[e]
});
}
std::sort(out.begin(), out.end(),
[](const auto& a, const auto& b) { return a.ms_per_frame > b.ms_per_frame; });
return out;
}
void gpu_timer::reset()
{
m_totals_ns = {};
m_events_seen = {};
m_draw_pass_ns = {};
m_draw_pass_samples = {};
m_dropped = 0;
m_frames = 0;
}
+24
View File
@@ -83,6 +83,26 @@ namespace vk
u64 flips() const { return m_flips; }
/**
* Per-pass cost for the draw region, keyed by the pass's ordinal within the frame.
*
* The sum says the GPU is inside render passes and nothing more. Whether that is one
* expensive pass or thirty even ones decides what to do about it, and those want
* opposite fixes: a single heavy pass is a target, an even spread means the draw count
* itself is the wall.
*
* Ordinal rather than identity because the frame structure is stable, so pass N is the
* same logical pass from frame to frame, which is what makes the number actionable.
*/
struct pass_cost
{
u32 ordinal = 0;
double ms_per_frame = 0.0;
u64 samples = 0;
};
std::vector<pass_cost> draw_pass_costs() const;
private:
// A frame issues many readbacks and blits, and the interesting number is what they
// cost in total, so each region gets room for several timed events per frame rather
@@ -142,6 +162,10 @@ namespace vk
std::array<u64, region_count> m_totals_ns{};
std::array<u64, region_count> m_events_seen{};
// Draw region only, indexed by pass ordinal within the frame.
std::array<u64, max_events> m_draw_pass_ns{};
std::array<u64, max_events> m_draw_pass_samples{};
u64 m_dropped = 0;
u64 m_frames = 0;
u64 m_flips = 0;
+34
View File
@@ -26,6 +26,8 @@ namespace rsx::prof
u64 g_frame_cleanups = 0;
u64 g_fence_polls = 0;
u64 g_fence_polls_not_ready = 0;
u32 g_pass_ordinal = 0;
u64 g_pass_draws[pass_slot_count] = {};
u64 g_xform_program_calls = 0;
u64 g_xform_program_words = 0;
u64 g_xform_const_calls = 0;
@@ -192,6 +194,11 @@ namespace rsx::prof
g_acc.frames++;
// Restart pass numbering so an ordinal means the same pass here as it does in the GPU
// timer, whose event index resets on the same boundary. Wraps to zero on the first
// pass; anything counted before one opens lands out of range and is discarded.
g_pass_ordinal = umax;
// Report on a frame boundary rather than a timer, so per-frame costs divide by a
// whole number of frames and a long stall lands in the window that contains it.
if (g_acc.frames >= 300)
@@ -350,6 +357,32 @@ namespace rsx::prof
level == 0 ? "direct caller" : "change_layout caller", list);
}
{
// Draws per pass, by the same ordinal the GPU timer reports its per-pass cost
// against, so the two can be read side by side.
std::pair<u32, u64> top[6] = {};
for (u32 i = 0; i < pass_slot_count; i++)
{
if (!g_pass_draws[i]) continue;
if (g_pass_draws[i] <= top[5].second) continue;
top[5] = { i, g_pass_draws[i] };
std::sort(std::begin(top), std::end(top),
[](const auto& a, const auto& b) { return a.second > b.second; });
}
if (top[0].second)
{
std::string list;
for (const auto& [ordinal, count] : top)
{
if (!count) continue;
fmt::append(list, "%s#%u %.0f", list.empty() ? "" : ", ", ordinal,
static_cast<double>(count) / frames);
}
fmt::append(report, "\n\tdraws by pass %s", list);
}
}
if (g_xform_program_calls || g_xform_const_calls)
{
const auto ns_each = [&](bucket b, u64 calls)
@@ -487,6 +520,7 @@ namespace rsx::prof
g_mprotect_bytes = 0;
g_access_violations = 0;
for (auto& c : g_rp_sites) c = 0;
for (auto& c : g_pass_draws) c = 0;
for (auto& level : g_rp_caller_counts) for (auto& c : level) c = 0;
for (auto& level : g_rp_callers) for (auto& a : level) a = nullptr;
for (auto& c : g_flush_sites) c = 0;
+11
View File
@@ -249,6 +249,17 @@ namespace rsx::prof
* methods, not calls, and dividing the bucket by the histogram would price a batch as if
* it were one method. These are the call counts the buckets actually divide by.
*/
/**
* Draws issued inside each render pass, keyed by the pass's ordinal within the frame.
*
* Pairs with the GPU timer's per-pass cost. A pass that is expensive with few draws is
* expensive per draw; one that is expensive with most of the frame's draws in it is
* carrying the geometry. Same milliseconds, opposite fixes.
*/
inline constexpr u32 pass_slot_count = 96;
extern u32 g_pass_ordinal;
extern u64 g_pass_draws[pass_slot_count];
extern u64 g_xform_program_calls;
extern u64 g_xform_program_words;
extern u64 g_xform_const_calls;