From ef35ece07e521f70673b48eece533a82da7d8ea5 Mon Sep 17 00:00:00 2001 From: jpchow26 Date: Fri, 11 Sep 2026 23:21:56 -0400 Subject: [PATCH] GPU: Add VRAM bandwidth and render/fill-rate statistics Add a 'Show Bandwidth Statistics' OSD option that displays per-frame VRAM memory bandwidth (writes, readbacks, copies, display extraction) and rendering fill-rate (total shaded pixels and textured shaded pixels), to help diagnose performance bottlenecks on low-bandwidth SoCs such as the Allwinner H700 (panfrost). --- src/core/fullscreenui_settings.cpp | 4 +++ src/core/gpu_backend.cpp | 38 ++++++++++++++++++++++- src/core/gpu_backend.h | 36 ++++++++++++++++++++++ src/core/gpu_hw.cpp | 39 +++++++++++++++++++++--- src/core/gpu_hw.h | 15 +++++++-- src/core/imgui_overlays.cpp | 17 +++++++++-- src/core/settings.cpp | 2 ++ src/core/settings.h | 1 + src/duckstation-qt/osdsettingswidget.cpp | 2 ++ src/duckstation-qt/osdsettingswidget.ui | 7 +++++ 10 files changed, 149 insertions(+), 12 deletions(-) diff --git a/src/core/fullscreenui_settings.cpp b/src/core/fullscreenui_settings.cpp index 5ba2e0bf6..50e0f4a4b 100644 --- a/src/core/fullscreenui_settings.cpp +++ b/src/core/fullscreenui_settings.cpp @@ -2795,6 +2795,10 @@ void FullscreenUI::DrawInterfaceSettingsPage() DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_CHART_BAR, "Show GPU Statistics"), FSUI_VSTR("Shows information about the emulated GPU in the top-right corner of the display."), "Display", "ShowGPUStatistics", false); + DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_CHART_BAR, "Show Bandwidth Statistics"), + FSUI_VSTR("Shows VRAM memory bandwidth (writes, readbacks, copies, display) per frame in the " + "top-right corner of the display."), + "Display", "ShowBandwidthStatistics", false); DrawToggleSetting( bsi, FSUI_ICONVSTR(ICON_FA_USER_CLOCK, "Show Latency Statistics"), FSUI_VSTR("Shows information about input and audio latency in the top-right corner of the display."), "Display", diff --git a/src/core/gpu_backend.cpp b/src/core/gpu_backend.cpp index 396a523b1..665b59e0a 100644 --- a/src/core/gpu_backend.cpp +++ b/src/core/gpu_backend.cpp @@ -117,7 +117,8 @@ bool GPUBackend::Initialize(bool clear_vram, Error* error) bool GPUBackend::UpdateSettings(const GPUSettings& old_settings, Error* error) { - if (g_gpu_settings.display_show_gpu_stats != old_settings.display_show_gpu_stats) + if (g_gpu_settings.display_show_gpu_stats != old_settings.display_show_gpu_stats || + g_gpu_settings.display_show_bandwidth_stats != old_settings.display_show_bandwidth_stats) GPUBackend::ResetStatistics(); return true; @@ -654,6 +655,31 @@ void GPUBackend::GetMemoryStatsString(SmallStringBase& str) const s_stats.host_num_copies, s_stats.host_num_uploads); } +void GPUBackend::GetBandwidthStatsString(SmallStringBase& str) const +{ + // VRAM memory bandwidth in KB per frame, by category. "BW" is the total; + // W = CPU->GPU writes, R = GPU->CPU readbacks, C = GPU->GPU copies, D = display extraction. + const u32 total_kb = static_cast((s_stats.vram_bandwidth.total() + (1024 - 1)) / 1024); + const u32 write_kb = static_cast((s_stats.vram_bandwidth.write + (1024 - 1)) / 1024); + const u32 read_kb = static_cast((s_stats.vram_bandwidth.read + (1024 - 1)) / 1024); + const u32 copy_kb = static_cast((s_stats.vram_bandwidth.copy + (1024 - 1)) / 1024); + const u32 display_kb = static_cast((s_stats.vram_bandwidth.display + (1024 - 1)) / 1024); + + str.format("{}KB " BOLD("BW") " | {} " BOLD("W") " | {} " BOLD("R") " | {} " BOLD("C") " | {} " BOLD("D"), + total_kb, write_kb, read_kb, copy_kb, display_kb); +} + +void GPUBackend::GetRenderStatsString(SmallStringBase& str) const +{ + // Rendering / fill-rate in thousands of shaded pixels per frame. "FP" is the total + // shaded-pixel count (bounding-box estimate, includes overdraw); "TX" is the subset + // that samples a texture (i.e. the texture-fetch cost). + const u32 fill_k = static_cast((s_stats.render.fill + 1000 - 1) / 1000); + const u32 tex_k = static_cast((s_stats.render.tex + 1000 - 1) / 1000); + + str.format("{}K " BOLD("FP") " | {}K " BOLD("TX"), fill_k, tex_k); +} + #undef BOLD void GPUBackend::ResetStatistics() @@ -688,6 +714,16 @@ void GPUBackend::UpdateStatistics(u32 frame_count) UPDATE_GPU_STAT(num_downloads); UPDATE_GPU_STAT(num_uploads); + // VRAM memory bandwidth (per-frame average). + s_stats.vram_bandwidth.write = (s_counters.vram_write_bytes + round) / frame_count; + s_stats.vram_bandwidth.read = (s_counters.vram_read_bytes + round) / frame_count; + s_stats.vram_bandwidth.copy = (s_counters.vram_copy_bytes + round) / frame_count; + s_stats.vram_bandwidth.display = (s_counters.vram_display_bytes + round) / frame_count; + + // Rendering / fill-rate (per-frame average). + s_stats.render.fill = (s_counters.fill_pixels + round) / frame_count; + s_stats.render.tex = (s_counters.tex_pixels + round) / frame_count; + #undef UPDATE_GPU_STAT #undef UPDATE_COUNTER diff --git a/src/core/gpu_backend.h b/src/core/gpu_backend.h index d6ee839f2..c3c527e2e 100644 --- a/src/core/gpu_backend.h +++ b/src/core/gpu_backend.h @@ -92,6 +92,8 @@ public: void GetStatsString(SmallStringBase& str) const; void GetMemoryStatsString(SmallStringBase& str) const; + void GetBandwidthStatsString(SmallStringBase& str) const; + void GetRenderStatsString(SmallStringBase& str) const; void ResetStatistics(); void UpdateStatistics(u32 frame_count); @@ -130,6 +132,20 @@ protected: u32 num_vertices; u32 num_primitives; u32 num_depth_buffer_clears; + + // VRAM memory bandwidth in bytes (hardware backend only). These measure the actual + // memory-bus traffic of VRAM operations, which is the bottleneck on low-bandwidth + // SoCs (e.g. Allwinner H700 / panfrost). + size_t vram_write_bytes; // CPU->GPU uploads + framebuffer writes (+ reads when checking mask) + size_t vram_read_bytes; // GPU->CPU readbacks + size_t vram_copy_bytes; // GPU->GPU VRAM copies (read + write) + size_t vram_display_bytes; // display extraction (read + write) + + // Rendering / fill-rate in shaded pixels (hardware backend only). These measure the + // GPU rasterization cost, which (unlike VRAM bandwidth) scales with what is actually + // on screen - the likely bottleneck for animated-texture scenes on weak GPUs. + size_t fill_pixels; // total shaded pixels (bounding-box estimate, includes overdraw) + size_t tex_pixels; // shaded pixels that sample a texture (texture-fetch cost) }; struct Stats : Counters @@ -143,6 +159,26 @@ protected: u32 host_num_uploads; u8 gpu_busy_pct; + + /// Per-frame average VRAM memory bandwidth in bytes, by category. + struct Bandwidth + { + size_t write; + size_t read; + size_t copy; + size_t display; + + ALWAYS_INLINE size_t total() const { return (write + read + copy + display); } + }; + Bandwidth vram_bandwidth; + + /// Per-frame average rendering / fill-rate in shaded pixels. + struct Render + { + size_t fill; + size_t tex; + }; + Render render; }; virtual void ReadVRAM(u32 x, u32 y, u32 width, u32 height) = 0; diff --git a/src/core/gpu_hw.cpp b/src/core/gpu_hw.cpp index 375e22ab2..cd76ab330 100644 --- a/src/core/gpu_hw.cpp +++ b/src/core/gpu_hw.cpp @@ -2804,6 +2804,8 @@ void GPU_HW::DrawSprite(const GPUBackendDrawRectangleCommand* cmd) return; } + AccountRenderPixels(clamped_rect, cmd->texture_enable); + // Treat non-textured sprite draws as fills, so we don't break the TC on framebuffer clears. bool draw_with_software_renderer = m_draw_with_software_renderer; if (m_use_texture_cache && !cmd->transparency_enable && !cmd->shading_enable && !cmd->texture_enable && @@ -2936,6 +2938,11 @@ void GPU_HW::DrawPolygon(const GPUBackendDrawPolygonCommand* cmd) { SetBatchDepthBuffer(cmd, false); + if (!clamped_draw_rect_012.rempty()) + AccountRenderPixels(clamped_draw_rect_012, cmd->texture_enable); + if (!clamped_draw_rect_123.rempty()) + AccountRenderPixels(clamped_draw_rect_123, cmd->texture_enable); + FinishPolygonDraw(cmd, vertices, num_vertices, false, false, clamped_draw_rect_012, clamped_draw_rect_123); } @@ -2983,6 +2990,11 @@ void GPU_HW::DrawPrecisePolygon(const GPUBackendDrawPrecisePolygonCommand* cmd) CheckForDepthClear(cmd, average_z); } + if (!clamped_draw_rect_012.rempty()) + AccountRenderPixels(clamped_draw_rect_012, cmd->texture_enable); + if (!clamped_draw_rect_123.rempty()) + AccountRenderPixels(clamped_draw_rect_123, cmd->texture_enable); + FinishPolygonDraw(cmd, vertices, num_vertices, true, is_3d, clamped_draw_rect_012, clamped_draw_rect_123); } @@ -3516,6 +3528,9 @@ void GPU_HW::DownloadVRAMFromGPU(u32 x, u32 y, u32 width, u32 height) VRAM_WIDTH * sizeof(u16)); } + // VRAM bandwidth: framebuffer read (4 bytes/pixel) + CPU readback copy (2 bytes/pixel). + GPUBackend::s_counters.vram_read_bytes += static_cast(copy_rect.width()) * copy_rect.height() * 6; + RestoreDeviceContext(); } @@ -3618,11 +3633,16 @@ void GPU_HW::UpdateVRAMOnGPU(u32 x, u32 y, u32 width, u32 height, const void* da g_gpu_device->SetTextureSampler(0, upload_texture.get(), g_gpu_device->GetNearestSampler()); else g_gpu_device->SetTextureBuffer(0, m_vram_upload_buffer.get()); - - DrawScreenQuad(scaled_bounds, m_vram_texture->GetSizeVec(), GSVector4::zero(), &uniforms, sizeof(uniforms)); - - RestoreDeviceContext(); -} + + DrawScreenQuad(scaled_bounds, m_vram_texture->GetSizeVec(), GSVector4::zero(), &uniforms, sizeof(uniforms)); + + // VRAM bandwidth: upload (2 bytes/pixel) + framebuffer write (4 bytes/pixel) + + // framebuffer read for the mask check (4 bytes/pixel). + GPUBackend::s_counters.vram_write_bytes += + static_cast(width) * height * (2 + 4 + (check_mask ? 4 : 0)); + + RestoreDeviceContext(); + } void GPU_HW::CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 width, u32 height, bool set_mask, bool check_mask) { @@ -3677,6 +3697,10 @@ void GPU_HW::CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 width, u32 return; } + // VRAM bandwidth: a copy reads and writes the framebuffer (4 bytes/pixel each). + // (The recursive chunk path above counts itself; the local-memory TC path is CPU-side.) + GPUBackend::s_counters.vram_copy_bytes += static_cast(width) * height * 8; + if (use_shader || IsUsingMultisampling()) { if (intersect_with_draw || intersect_with_write) @@ -4172,6 +4196,11 @@ void GPU_HW::UpdateDisplay(const GPUBackendUpdateDisplayCommand* cmd) static_cast(line_skip ? 2 : 1)}; g_gpu_device->DrawWithPushConstants(3, 0, &uniforms, sizeof(uniforms)); + // VRAM bandwidth: the display extraction reads the VRAM framebuffer and writes the + // extract texture (4 bytes/pixel each, at the scaled resolution). + GPUBackend::s_counters.vram_display_bytes += + static_cast(scaled_display_width) * scaled_display_height * 8; + m_vram_extract_texture->MakeReadyForSampling(); if (depth_source) { diff --git a/src/core/gpu_hw.h b/src/core/gpu_hw.h index e27484150..151b1949e 100644 --- a/src/core/gpu_hw.h +++ b/src/core/gpu_hw.h @@ -99,9 +99,18 @@ public: void UpdateDisplay(const GPUBackendUpdateDisplayCommand* cmd) override; private: - enum : u32 - { - MAX_BATCH_VERTEX_COUNTER_IDS = 65536 - 2, + /// Accounts for the shaded-pixel (fill-rate) cost of a drawn rectangle. + ALWAYS_INLINE_RELEASE void AccountRenderPixels(const GSVector4i& rect, bool textured) + { + const size_t pixels = static_cast(rect.width()) * rect.height(); + s_counters.fill_pixels += pixels; + if (textured) + s_counters.tex_pixels += pixels; + } + + enum : u32 + { + MAX_BATCH_VERTEX_COUNTER_IDS = 65536 - 2, MAX_VERTICES_FOR_RECTANGLE = 6 * (((MAX_PRIMITIVE_WIDTH + (TEXTURE_PAGE_WIDTH - 1)) / TEXTURE_PAGE_WIDTH) + 1u) * (((MAX_PRIMITIVE_HEIGHT + (TEXTURE_PAGE_HEIGHT - 1)) / TEXTURE_PAGE_HEIGHT) + 1u), NUM_TEXTURE_MODES = static_cast(BatchTextureMode::MaxCount), diff --git a/src/core/imgui_overlays.cpp b/src/core/imgui_overlays.cpp index 2a5052b9b..3dd852c1c 100644 --- a/src/core/imgui_overlays.cpp +++ b/src/core/imgui_overlays.cpp @@ -389,9 +389,9 @@ void ImGuiManager::DrawPerformanceOverlay(const GPUBackend* gpu, float& position #define COLOR(text) "\x04" text "\x03" if (!(g_gpu_settings.display_show_fps || g_gpu_settings.display_show_speed || g_gpu_settings.display_show_gpu_stats || - g_gpu_settings.display_show_resolution || g_gpu_settings.display_show_latency_stats || - g_gpu_settings.display_show_cpu_usage || g_gpu_settings.display_show_gpu_usage || - g_gpu_settings.display_show_frame_times || + g_gpu_settings.display_show_bandwidth_stats || g_gpu_settings.display_show_resolution || + g_gpu_settings.display_show_latency_stats || g_gpu_settings.display_show_cpu_usage || + g_gpu_settings.display_show_gpu_usage || g_gpu_settings.display_show_frame_times || (g_gpu_settings.display_show_status_indicators && (VideoThread::IsSystemPaused() || System::IsFastForwardEnabled() || System::IsTurboEnabled())))) { @@ -456,6 +456,17 @@ void ImGuiManager::DrawPerformanceOverlay(const GPUBackend* gpu, float& position position_y += spacing; } + if (g_gpu_settings.display_show_bandwidth_stats) + { + gpu->GetBandwidthStatsString(text); + DrawPerformanceStat(dl, position_y, fixed_font, fixed_font_size, FIXED_BOLD_WEIGHT, 0, rbound, text); + position_y += spacing; + + gpu->GetRenderStatsString(text); + DrawPerformanceStat(dl, position_y, fixed_font, fixed_font_size, FIXED_BOLD_WEIGHT, 0, rbound, text); + position_y += spacing; + } + if (g_gpu_settings.display_show_resolution) { const u32 resolution_scale = gpu->GetResolutionScale(); diff --git a/src/core/settings.cpp b/src/core/settings.cpp index cdf857da0..67524f11a 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -429,6 +429,7 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro display_show_fps = si.GetBoolValue("Display", "ShowFPS", false); display_show_speed = si.GetBoolValue("Display", "ShowSpeed", false); display_show_gpu_stats = si.GetBoolValue("Display", "ShowGPUStatistics", false); + display_show_bandwidth_stats = si.GetBoolValue("Display", "ShowBandwidthStatistics", false); display_show_resolution = si.GetBoolValue("Display", "ShowResolution", false); display_show_latency_stats = si.GetBoolValue("Display", "ShowLatencyStatistics", false); display_show_cpu_usage = si.GetBoolValue("Display", "ShowCPU", false); @@ -806,6 +807,7 @@ void Settings::Save(SettingsInterface& si, bool for_copy) const si.SetBoolValue("Display", "ShowResolution", display_show_resolution); si.SetBoolValue("Display", "ShowLatencyStatistics", display_show_latency_stats); si.SetBoolValue("Display", "ShowGPUStatistics", display_show_gpu_stats); + si.SetBoolValue("Display", "ShowBandwidthStatistics", display_show_bandwidth_stats); si.SetBoolValue("Display", "ShowCPU", display_show_cpu_usage); si.SetBoolValue("Display", "ShowGPU", display_show_gpu_usage); si.SetBoolValue("Display", "ShowFrameTimes", display_show_frame_times); diff --git a/src/core/settings.h b/src/core/settings.h index fd908ee9e..85f9ae402 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -111,6 +111,7 @@ struct GPUSettings bool display_show_fps : 1 = false; bool display_show_speed : 1 = false; bool display_show_gpu_stats : 1 = false; + bool display_show_bandwidth_stats : 1 = false; bool display_show_resolution : 1 = false; bool display_show_latency_stats : 1 = false; bool display_show_cpu_usage : 1 = false; diff --git a/src/duckstation-qt/osdsettingswidget.cpp b/src/duckstation-qt/osdsettingswidget.cpp index 4d283c507..4da399dc0 100644 --- a/src/duckstation-qt/osdsettingswidget.cpp +++ b/src/duckstation-qt/osdsettingswidget.cpp @@ -72,6 +72,8 @@ OSDSettingsWidget::OSDSettingsWidget(SettingsWindow* dialog, QWidget* parent) : SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showGPU, "Display", "ShowGPU", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showInput, "Display", "ShowInputs", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showGPUStatistics, "Display", "ShowGPUStatistics", false); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showBandwidthStatistics, "Display", "ShowBandwidthStatistics", + false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showLatencyStatistics, "Display", "ShowLatencyStatistics", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showFrameTimes, "Display", "ShowFrameTimes", false); diff --git a/src/duckstation-qt/osdsettingswidget.ui b/src/duckstation-qt/osdsettingswidget.ui index b740cd6d4..e70f9ffdf 100644 --- a/src/duckstation-qt/osdsettingswidget.ui +++ b/src/duckstation-qt/osdsettingswidget.ui @@ -331,6 +331,13 @@ + + + + Show Bandwidth Statistics + + +