diff --git a/pcsx2-qt/Settings/OSDSettingsWidget.cpp b/pcsx2-qt/Settings/OSDSettingsWidget.cpp
index 610822f49d..9c3005fcde 100644
--- a/pcsx2-qt/Settings/OSDSettingsWidget.cpp
+++ b/pcsx2-qt/Settings/OSDSettingsWidget.cpp
@@ -62,6 +62,7 @@ OSDSettingsWidget::OSDSettingsWidget(SettingsWindow* settings_dialog, QWidget* p
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showUsageCPU, "EmuCore/GS", "OsdShowCPU", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showUsageGPU, "EmuCore/GS", "OsdShowGPU", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showDebugGPU, "EmuCore/GS", "OsdShowGPUDebug", false);
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showStatsGPU, "EmuCore/GS", "OsdShowGPUStats", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showStatusIndicators, "EmuCore/GS", "OsdShowIndicators", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showFrameTimes, "EmuCore/GS", "OsdShowFrameTimes", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showHardwareInfo, "EmuCore/GS", "OsdShowHardwareInfo", false);
@@ -127,6 +128,8 @@ OSDSettingsWidget::OSDSettingsWidget(SettingsWindow* settings_dialog, QWidget* p
dialog()->registerWidgetHelp(m_ui.showDebugGPU, tr("Show GPU Debug Info"),
tr("Unchecked"), tr("Shows debug information about the renderer."));
#endif
+ dialog()->registerWidgetHelp(m_ui.showDebugGPU, tr("Show GPU Pipeline Statistics"),
+ tr("Unchecked"), tr("Shows GPU vertex shader and pixels shader invocations."));
dialog()->registerWidgetHelp(m_ui.showFrameTimes, tr("Show Frame Times"), tr("Unchecked"),
tr("Displays a graph showing the average frametimes."));
@@ -243,6 +246,7 @@ void OSDSettingsWidget::onPerformancePosChanged()
m_ui.showGSStats->setEnabled(enabled);
m_ui.showUsageCPU->setEnabled(enabled);
m_ui.showUsageGPU->setEnabled(enabled);
+ m_ui.showStatsGPU->setEnabled(enabled);
m_ui.showStatusIndicators->setEnabled(enabled);
m_ui.showFrameTimes->setEnabled(enabled);
m_ui.showHardwareInfo->setEnabled(enabled);
@@ -265,6 +269,7 @@ void OSDSettingsWidget::setAllCheckboxes(bool checked)
m_ui.showGSStats,
m_ui.showUsageCPU,
m_ui.showUsageGPU,
+ m_ui.showStatsGPU,
m_ui.showFrameTimes,
m_ui.showHardwareInfo,
m_ui.showVersion,
diff --git a/pcsx2-qt/Settings/OSDSettingsWidget.ui b/pcsx2-qt/Settings/OSDSettingsWidget.ui
index 1fc86aacde..472b4de17d 100644
--- a/pcsx2-qt/Settings/OSDSettingsWidget.ui
+++ b/pcsx2-qt/Settings/OSDSettingsWidget.ui
@@ -492,6 +492,13 @@
+ -
+
+
+ Show GPU Pipeline Statistics
+
+
+
@@ -530,6 +537,7 @@
showGSStats
showUsageCPU
showUsageGPU
+ showStatsGPU
showStatusIndicators
showFrameTimes
diff --git a/pcsx2/Config.h b/pcsx2/Config.h
index 615c7ce26b..93bbb89e62 100644
--- a/pcsx2/Config.h
+++ b/pcsx2/Config.h
@@ -774,6 +774,7 @@ struct Pcsx2Config
OsdShowCPU : 1,
OsdShowGPU : 1,
OsdShowGPUDebug : 1,
+ OsdShowGPUStats : 1,
OsdShowIndicators : 1,
OsdShowFrameTimes : 1,
OsdShowHardwareInfo : 1,
diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp
index 6a541794d9..fec4df9f1b 100644
--- a/pcsx2/GS/GS.cpp
+++ b/pcsx2/GS/GS.cpp
@@ -163,6 +163,8 @@ static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool
if (!g_gs_device->SetGPUTimingEnabled(true))
GSConfig.OsdShowGPU = false;
+ if (!g_gs_device->SetGPUPipelineStatisticsEnabled(true))
+ GSConfig.OsdShowGPUStats = false;
Console.WriteLn(Color_StrongGreen, "%s Graphics Driver Info:", GSDevice::RenderAPIToString(new_api));
Console.WriteLn(g_gs_device->GetDriverInfo());
@@ -884,6 +886,12 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config)
if (!g_gs_device->SetGPUTimingEnabled(true))
GSConfig.OsdShowGPU = false;
}
+
+ if (GSConfig.OsdShowGPUStats != old_config.OsdShowGPUStats)
+ {
+ if (!g_gs_device->SetGPUPipelineStatisticsEnabled(GSConfig.OsdShowGPUStats))
+ GSConfig.OsdShowGPUStats = false;
+ }
}
void GSSetSoftwareRendering(bool software_renderer, GSInterlaceMode new_interlace)
diff --git a/pcsx2/GS/Renderers/Common/GSDevice.h b/pcsx2/GS/Renderers/Common/GSDevice.h
index 6dfb6ab287..4621115e5f 100644
--- a/pcsx2/GS/Renderers/Common/GSDevice.h
+++ b/pcsx2/GS/Renderers/Common/GSDevice.h
@@ -29,6 +29,12 @@ static inline constexpr Filter BilnIf(bool biln)
return biln ? Biln : Nearest;
}
+struct GPUPipelineStatistics
+{
+ u64 vs_invocations;
+ u64 ps_invocations;
+};
+
enum class ShaderConvert
{
COPY = 0,
@@ -1610,6 +1616,12 @@ public:
/// Returns the amount of GPU time utilized since the last time this method was called.
virtual float GetAndResetAccumulatedGPUTime() = 0;
+ /// Enables/disables GPU pipeline statistics.
+ virtual bool SetGPUPipelineStatisticsEnabled(bool enabled) = 0;
+
+ /// Get the pipeline statistics for the last frame.
+ virtual GPUPipelineStatistics GetAndResetAccumulatedGPUPipelineStatistics() = 0;
+
/// Returns true if not enough time has passed for present to not block.
bool ShouldSkipPresentingFrame();
diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.cpp b/pcsx2/GS/Renderers/Common/GSRenderer.cpp
index 71708ade6e..1d77040963 100644
--- a/pcsx2/GS/Renderers/Common/GSRenderer.cpp
+++ b/pcsx2/GS/Renderers/Common/GSRenderer.cpp
@@ -700,7 +700,9 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
EndPresentFrame();
- PerformanceMetrics::OnGPUPresent(g_gs_device->GetAndResetAccumulatedGPUTime());
+ const float gpu_time = g_gs_device->GetAndResetAccumulatedGPUTime();
+ GPUPipelineStatistics gpu_stats = g_gs_device->GetAndResetAccumulatedGPUPipelineStatistics();
+ PerformanceMetrics::OnGPUPresent(gpu_time, gpu_stats.vs_invocations, gpu_stats.ps_invocations);
}
PerformanceMetrics::Update(registers_written, fb_sprite_frame, false);
diff --git a/pcsx2/GS/Renderers/DX11/GSDevice11.cpp b/pcsx2/GS/Renderers/DX11/GSDevice11.cpp
index 1848b8efd0..9ce212193e 100644
--- a/pcsx2/GS/Renderers/DX11/GSDevice11.cpp
+++ b/pcsx2/GS/Renderers/DX11/GSDevice11.cpp
@@ -618,6 +618,7 @@ void GSDevice11::Destroy()
GSDevice::Destroy();
DestroySwapChain();
DestroyTimestampQueries();
+ DestroyPipelineStatisticsQueries();
m_convert = {};
m_present = {};
@@ -1082,6 +1083,10 @@ GSDevice::PresentResult GSDevice11::BeginPresent(bool frame_skip)
if (m_vsync_mode == GSVSyncMode::FIFO && m_gpu_timing_enabled)
PopTimestampQuery();
+ // Get the pipeline statistics for this frame before postprocessing.
+ if (m_gpu_pipeline_statistics_enabled)
+ PopPipelineStatisticsQuery();
+
m_ctx->ClearRenderTargetView(m_swap_chain_rtv.get(), s_present_clear_color.data());
m_ctx->OMSetRenderTargets(1, m_swap_chain_rtv.addressof(), nullptr);
if (m_state.rtv)
@@ -1113,6 +1118,9 @@ void GSDevice11::EndPresent()
if (m_vsync_mode != GSVSyncMode::FIFO && m_gpu_timing_enabled)
PopTimestampQuery();
+ if (m_gpu_pipeline_statistics_enabled)
+ PopPipelineStatisticsQuery();
+
// clear out the swap chain view, it might get resized..
OMSetRenderTargets(nullptr, nullptr, nullptr);
@@ -1122,6 +1130,9 @@ void GSDevice11::EndPresent()
if (m_gpu_timing_enabled)
KickTimestampQuery();
+
+ if (m_gpu_pipeline_statistics_enabled)
+ KickPipelineStatisticsQuery();
}
bool GSDevice11::CreateTimestampQueries()
@@ -1242,6 +1253,95 @@ float GSDevice11::GetAndResetAccumulatedGPUTime()
return value;
}
+bool GSDevice11::CreatePipelineStatisticsQueries()
+{
+ for (u32 i = 0; i < NUM_PIPELINE_STATISTICS_QUERIES; i++)
+ {
+ const CD3D11_QUERY_DESC qdesc(D3D11_QUERY_PIPELINE_STATISTICS);
+ const HRESULT hr = m_dev->CreateQuery(&qdesc, m_pipeline_statistics_queries[i].put());
+ if (FAILED(hr))
+ {
+ m_pipeline_statistics_queries = {};
+ return false;
+ }
+ }
+
+ KickPipelineStatisticsQuery();
+ return true;
+}
+
+void GSDevice11::KickPipelineStatisticsQuery()
+{
+ if (m_pipeline_statistics_query_started || !m_pipeline_statistics_queries[0] ||
+ m_waiting_pipeline_statistics_queries == NUM_PIPELINE_STATISTICS_QUERIES)
+ return;
+
+ m_ctx->Begin(m_pipeline_statistics_queries[m_write_pipeline_statistics_query].get());
+ m_pipeline_statistics_query_started = true;
+}
+
+void GSDevice11::DestroyPipelineStatisticsQueries()
+{
+ if (!m_pipeline_statistics_queries[0])
+ return;
+
+ if (m_pipeline_statistics_query_started)
+ m_ctx->End(m_pipeline_statistics_queries[m_write_timestamp_query].get());
+
+ m_timestamp_queries = {};
+ m_read_timestamp_query = 0;
+ m_write_timestamp_query = 0;
+ m_waiting_timestamp_queries = 0;
+ m_timestamp_query_started = 0;
+}
+
+void GSDevice11::PopPipelineStatisticsQuery()
+{
+ while (m_waiting_pipeline_statistics_queries > 0)
+ {
+ D3D11_QUERY_DATA_PIPELINE_STATISTICS stats{};
+ const HRESULT stats_hr = m_ctx->GetData(
+ m_pipeline_statistics_queries[m_read_pipeline_statistics_query].get(), &stats, sizeof(stats), 0);
+ if (stats_hr != S_OK)
+ break;
+
+ m_accumulated_gpu_pipeline_statistics.vs_invocations += stats.VSInvocations;
+ m_accumulated_gpu_pipeline_statistics.ps_invocations += stats.PSInvocations;
+ m_read_pipeline_statistics_query = (m_read_pipeline_statistics_query + 1) % NUM_PIPELINE_STATISTICS_QUERIES;
+ m_waiting_pipeline_statistics_queries--;
+ }
+
+ if (m_pipeline_statistics_query_started)
+ {
+ m_ctx->End(m_pipeline_statistics_queries[m_write_pipeline_statistics_query].get());
+ m_write_pipeline_statistics_query = (m_write_pipeline_statistics_query + 1) % NUM_PIPELINE_STATISTICS_QUERIES;
+ m_pipeline_statistics_query_started = false;
+ m_waiting_pipeline_statistics_queries++;
+ }
+}
+
+GPUPipelineStatistics GSDevice11::GetAndResetAccumulatedGPUPipelineStatistics()
+{
+ GPUPipelineStatistics stats = m_accumulated_gpu_pipeline_statistics;
+ m_accumulated_gpu_pipeline_statistics = {};
+ return stats;
+}
+
+bool GSDevice11::SetGPUPipelineStatisticsEnabled(bool enabled)
+{
+ m_gpu_pipeline_statistics_enabled = enabled;
+
+ if (m_gpu_pipeline_statistics_enabled)
+ {
+ return CreatePipelineStatisticsQueries();
+ }
+ else
+ {
+ DestroyPipelineStatisticsQueries();
+ return true;
+ }
+}
+
void GSDevice11::DrawPrimitive()
{
g_perfmon.Put(GSPerfMon::DrawCalls, 1);
diff --git a/pcsx2/GS/Renderers/DX11/GSDevice11.h b/pcsx2/GS/Renderers/DX11/GSDevice11.h
index 6c6e80f362..3da31414cf 100644
--- a/pcsx2/GS/Renderers/DX11/GSDevice11.h
+++ b/pcsx2/GS/Renderers/DX11/GSDevice11.h
@@ -93,6 +93,7 @@ private:
VERTEX_BUFFER_SIZE = 32 * 1024 * 1024,
INDEX_BUFFER_SIZE = 16 * 1024 * 1024,
NUM_TIMESTAMP_QUERIES = 5,
+ NUM_PIPELINE_STATISTICS_QUERIES = 5,
};
void SetFeatures(IDXGIAdapter1* adapter);
@@ -107,6 +108,11 @@ private:
void PopTimestampQuery();
void KickTimestampQuery();
+ bool CreatePipelineStatisticsQueries();
+ void DestroyPipelineStatisticsQueries();
+ void PopPipelineStatisticsQuery();
+ void KickPipelineStatisticsQuery();
+
void DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, GSVector4* dRect, const GSRegPMODE& PMODE, const GSRegEXTBUF& EXTBUF, u32 c, const Filter filter) override;
void DoInterlace(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect, ShaderInterlace shader, Filter filter, const InterlaceConstantBuffer& cb) override;
void DoFXAA(GSTexture* sTex, GSTexture* dTex) override;
@@ -192,6 +198,14 @@ private:
bool m_timestamp_query_started = false;
bool m_gpu_timing_enabled = false;
+ std::array, NUM_PIPELINE_STATISTICS_QUERIES> m_pipeline_statistics_queries = {};
+ GPUPipelineStatistics m_accumulated_gpu_pipeline_statistics{};
+ u8 m_read_pipeline_statistics_query = 0;
+ u8 m_write_pipeline_statistics_query = 0;
+ u8 m_waiting_pipeline_statistics_queries = 0;
+ bool m_gpu_pipeline_statistics_enabled = false;
+ bool m_pipeline_statistics_query_started = false;
+
struct
{
wil::com_ptr_nothrow il;
@@ -325,6 +339,9 @@ public:
bool SetGPUTimingEnabled(bool enabled) override;
float GetAndResetAccumulatedGPUTime() override;
+ bool SetGPUPipelineStatisticsEnabled(bool enabled) override;
+ GPUPipelineStatistics GetAndResetAccumulatedGPUPipelineStatistics() override;
+
// Helpers and utility draws.
void DrawPrimitive();
void DrawIndexedPrimitive();
diff --git a/pcsx2/GS/Renderers/DX12/GSDevice12.cpp b/pcsx2/GS/Renderers/DX12/GSDevice12.cpp
index 37f9721208..eb7fcc285c 100644
--- a/pcsx2/GS/Renderers/DX12/GSDevice12.cpp
+++ b/pcsx2/GS/Renderers/DX12/GSDevice12.cpp
@@ -519,6 +519,39 @@ void GSDevice12::MoveToNextCommandList()
m_current_command_list * NUM_TIMESTAMP_QUERIES_PER_CMDLIST);
}
+ if (res.pipeline_statistics_query == QueryState::Ready)
+ {
+ // Collect the pipeline statistics from the last time this cmdlist was used.
+ res.pipeline_statistics_query = QueryState::None;
+ const u32 offset = (m_current_command_list * sizeof(D3D12_QUERY_DATA_PIPELINE_STATISTICS));
+ const D3D12_RANGE read_range = { offset, offset + sizeof(D3D12_QUERY_DATA_PIPELINE_STATISTICS) };
+ void* map;
+ HRESULT hr = m_pipeline_statistics_query_buffer->Map(0, &read_range, &map);
+ if (SUCCEEDED(hr))
+ {
+ D3D12_QUERY_DATA_PIPELINE_STATISTICS stats;
+ std::memcpy(&stats, static_cast(map) + offset, sizeof(stats));
+
+ m_accumulated_gpu_pipeline_statistics.vs_invocations += stats.VSInvocations;
+ m_accumulated_gpu_pipeline_statistics.ps_invocations += stats.PSInvocations;
+
+ const D3D12_RANGE write_range = {};
+ m_pipeline_statistics_query_buffer->Unmap(0, &write_range);
+ }
+ else
+ {
+ Console.Warning("D3D12: Map() for pipeline statistics query failed: %08X", hr);
+ }
+ }
+
+ if (m_gpu_pipeline_statistics_enabled)
+ {
+ pxAssert(res.pipeline_statistics_query == QueryState::None);
+ res.pipeline_statistics_query = QueryState::Querying;
+ res.command_lists[1].list4->BeginQuery(
+ m_pipeline_statistics_query_heap.get(), D3D12_QUERY_TYPE_PIPELINE_STATISTICS, m_current_command_list);
+ }
+
ID3D12DescriptorHeap* heaps[2] = {
res.descriptor_allocator.GetDescriptorHeap(), res.sampler_allocator.GetDescriptorHeap()};
res.command_lists[1].list4->SetDescriptorHeaps(std::size(heaps), heaps);
@@ -564,6 +597,18 @@ bool GSDevice12::ExecuteCommandList(WaitType wait_for_completion)
m_timestamp_query_buffer.get(), m_current_command_list * (sizeof(u64) * NUM_TIMESTAMP_QUERIES_PER_CMDLIST));
}
+ if ((res.pipeline_statistics_query == QueryState::Querying) || (res.pipeline_statistics_query == QueryState::Ready))
+ {
+ if (res.pipeline_statistics_query == QueryState::Querying)
+ {
+ // Didn't end query in BeginPresent() so end it here.
+ res.pipeline_statistics_query = QueryState::Ready;
+ res.command_lists[1].list4->EndQuery(m_pipeline_statistics_query_heap.get(), D3D12_QUERY_TYPE_PIPELINE_STATISTICS, m_current_command_list);
+ }
+ res.command_lists[1].list4->ResolveQueryData(m_pipeline_statistics_query_heap.get(), D3D12_QUERY_TYPE_PIPELINE_STATISTICS,
+ m_current_command_list, 1, m_pipeline_statistics_query_buffer.get(), m_current_command_list * sizeof(D3D11_QUERY_DATA_PIPELINE_STATISTICS));
+ }
+
if (res.init_command_list_used)
{
hr = res.command_lists[0].list4->Close();
@@ -716,6 +761,36 @@ void GSDevice12::WaitForGPUIdle()
}
}
+bool GSDevice12::CreatePipelineStatisticsQuery()
+{
+ constexpr u32 QUERY_COUNT = NUM_COMMAND_LISTS;
+ constexpr u32 BUFFER_SIZE = sizeof(D3D12_QUERY_DATA_PIPELINE_STATISTICS) * QUERY_COUNT;
+
+ const D3D12_QUERY_HEAP_DESC desc = { D3D12_QUERY_HEAP_TYPE_PIPELINE_STATISTICS, QUERY_COUNT };
+ HRESULT hr = m_device->CreateQueryHeap(&desc, IID_PPV_ARGS(m_pipeline_statistics_query_heap.put()));
+ if (FAILED(hr))
+ {
+ Console.Error("D3D12: CreateQueryHeap() for pipeline statistics failed with %08X", hr);
+ return false;
+ }
+
+ const D3D12MA::ALLOCATION_DESC allocation_desc = { D3D12MA::ALLOCATION_FLAG_NONE, D3D12_HEAP_TYPE_READBACK };
+ const D3D12_RESOURCE_DESCU resource_desc = { {D3D12_RESOURCE_DIMENSION_BUFFER, 0, BUFFER_SIZE, 1, 1, 1,
+ DXGI_FORMAT_UNKNOWN, {1, 0}, D3D12_TEXTURE_LAYOUT_ROW_MAJOR, D3D12_RESOURCE_FLAG_NONE} };
+ if (m_enhanced_barriers)
+ hr = m_allocator->CreateResource3(&allocation_desc, &resource_desc.desc1, D3D12_BARRIER_LAYOUT_UNDEFINED, nullptr,
+ 0, nullptr, m_pipeline_statistics_query_allocation.put(), IID_PPV_ARGS(m_pipeline_statistics_query_buffer.put()));
+ else
+ hr = m_allocator->CreateResource(&allocation_desc, &resource_desc.desc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr,
+ m_pipeline_statistics_query_allocation.put(), IID_PPV_ARGS(m_pipeline_statistics_query_buffer.put()));
+ if (FAILED(hr))
+ {
+ Console.Error("D3D12: CreateResource() for pipeline statistics failed with %08X", hr);
+ return false;
+ }
+ return true;
+}
+
bool GSDevice12::CreateTimestampQuery()
{
constexpr u32 QUERY_COUNT = NUM_TIMESTAMP_QUERIES_PER_CMDLIST * NUM_COMMAND_LISTS;
@@ -769,6 +844,19 @@ bool GSDevice12::SetGPUTimingEnabled(bool enabled)
return true;
}
+GPUPipelineStatistics GSDevice12::GetAndResetAccumulatedGPUPipelineStatistics()
+{
+ GPUPipelineStatistics stats = m_accumulated_gpu_pipeline_statistics;
+ m_accumulated_gpu_pipeline_statistics = {};
+ return stats;
+}
+
+bool GSDevice12::SetGPUPipelineStatisticsEnabled(bool enabled)
+{
+ m_gpu_pipeline_statistics_enabled = enabled;
+ return true;
+}
+
bool GSDevice12::AllocatePreinitializedGPUBuffer(u32 size, ID3D12Resource** gpu_buffer,
D3D12MA::Allocation** gpu_allocation, const std::function& fill_callback)
{
@@ -863,7 +951,7 @@ bool GSDevice12::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
m_name = D3D::GetAdapterName(m_adapter.get());
- if (!CreateDescriptorHeaps() || !CreateCommandLists() || !CreateTimestampQuery())
+ if (!CreateDescriptorHeaps() || !CreateCommandLists() || !CreateTimestampQuery() || !CreatePipelineStatisticsQuery())
return false;
if (!AcquireWindow(true) || (m_window_info.type != WindowInfo::Type::Surfaceless && !CreateSwapChain()))
@@ -1275,6 +1363,14 @@ GSDevice::PresentResult GSDevice12::BeginPresent(bool frame_skip)
return PresentResult::FrameSkipped;
}
+ // End the pipeline statistics for this cmdlist before postprocessing.
+ CommandListResources& res = m_command_lists[m_current_command_list];
+ if (res.pipeline_statistics_query == QueryState::Querying)
+ {
+ res.pipeline_statistics_query = QueryState::Ready;
+ res.command_lists[1].list4->EndQuery(m_pipeline_statistics_query_heap.get(), D3D12_QUERY_TYPE_PIPELINE_STATISTICS, m_current_command_list);
+ }
+
GSTexture12* swap_chain_buf = m_swap_chain_buffers[m_current_swap_chain_buffer].get();
const D3D12CommandList& cmdlist = GetCommandList();
@@ -3091,6 +3187,8 @@ void GSDevice12::DestroyResources()
m_timestamp_query_buffer.reset();
m_timestamp_query_allocation.reset();
+ m_pipeline_statistics_query_buffer.reset();
+ m_pipeline_statistics_query_allocation.reset();
m_sampler_heap_manager.Destroy();
m_dsv_heap_manager.Destroy();
m_rtv_heap_manager.Destroy();
diff --git a/pcsx2/GS/Renderers/DX12/GSDevice12.h b/pcsx2/GS/Renderers/DX12/GSDevice12.h
index 938cfcb2bc..691e176afb 100644
--- a/pcsx2/GS/Renderers/DX12/GSDevice12.h
+++ b/pcsx2/GS/Renderers/DX12/GSDevice12.h
@@ -191,6 +191,14 @@ public:
void UploadIndices(D3D12StreamBuffer& buffer, const void* index, size_t count);
private:
+ // For pipeline statistics
+ enum class QueryState
+ {
+ None,
+ Querying,
+ Ready,
+ };
+
struct CommandListResources
{
std::array, 2> command_allocators;
@@ -202,6 +210,7 @@ private:
u64 ready_fence_value = 0;
bool init_command_list_used = false;
bool has_timestamp_query = false;
+ QueryState pipeline_statistics_query = QueryState::None;
};
void LoadAgilitySDK();
@@ -210,6 +219,7 @@ private:
bool CreateDescriptorHeaps();
bool CreateCommandLists();
bool CreateTimestampQuery();
+ bool CreatePipelineStatisticsQuery();
void MoveToNextCommandList();
void DestroyPendingResources(CommandListResources& cmdlist);
@@ -234,6 +244,12 @@ private:
bool m_gpu_timing_enabled = false;
bool m_programmable_sample_positions = false;
+ ComPtr m_pipeline_statistics_query_heap;
+ ComPtr m_pipeline_statistics_query_buffer;
+ ComPtr m_pipeline_statistics_query_allocation;
+ GPUPipelineStatistics m_accumulated_gpu_pipeline_statistics{};
+ bool m_gpu_pipeline_statistics_enabled = false;
+
D3D12DescriptorHeapManager m_descriptor_heap_manager;
D3D12DescriptorHeapManager m_rtv_heap_manager;
D3D12DescriptorHeapManager m_dsv_heap_manager;
@@ -510,6 +526,9 @@ public:
bool SetGPUTimingEnabled(bool enabled) override;
float GetAndResetAccumulatedGPUTime() override;
+ bool SetGPUPipelineStatisticsEnabled(bool enabled) override;
+ GPUPipelineStatistics GetAndResetAccumulatedGPUPipelineStatistics() override;
+
void PushDebugGroup(const char* fmt, ...) override;
void PopDebugGroup() override;
void InsertDebugMessage(DebugMessageCategory category, const char* fmt, ...) override;
diff --git a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h
index dfa0e5ecfc..dae95ae80c 100644
--- a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h
+++ b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h
@@ -414,6 +414,9 @@ public:
float GetAndResetAccumulatedGPUTime() override;
void AccumulateCommandBufferTime(id buffer);
+ bool SetGPUPipelineStatisticsEnabled(bool enabled) override { return false; }
+ GPUPipelineStatistics GetAndResetAccumulatedGPUPipelineStatistics() override { return {}; }
+
std::unique_ptr CreateDownloadTexture(u32 width, u32 height, GSTexture::Format format) override;
void ClearSamplerCache() override;
diff --git a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp
index 127108b7fd..4d96d13b59 100644
--- a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp
+++ b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp
@@ -657,6 +657,8 @@ bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
if (!CreateImGuiProgram())
return false;
+ m_gpu_pipeline_statistics_supported = (GLAD_GL_ARB_pipeline_statistics_query != 0);
+
// Basic to ensure structures are correctly packed
static_assert(sizeof(VSSelector) == 1, "Wrong VSSelector size");
static_assert(sizeof(PSSelector) == 16, "Wrong PSSelector size");
@@ -674,6 +676,7 @@ void GSDeviceOGL::Destroy()
if (m_gl_context)
{
DestroyTimestampQueries();
+ DestroyPipelineStatisticsQueries();
DestroyResources();
m_gl_context->DoneCurrent();
@@ -1084,6 +1087,10 @@ GSDevice::PresentResult GSDeviceOGL::BeginPresent(bool frame_skip)
if (frame_skip || m_window_info.type == WindowInfo::Type::Surfaceless)
return PresentResult::FrameSkipped;
+ // Get the pipeline statistics for this frame before postprocessing.
+ if (m_gpu_pipeline_statistics_enabled)
+ PopPipelineStatisticsQuery();
+
OMSetFBO(0);
OMSetColorMaskState();
@@ -1110,6 +1117,9 @@ void GSDeviceOGL::EndPresent()
if (m_gpu_timing_enabled)
KickTimestampQuery();
+
+ if (m_gpu_pipeline_statistics_enabled)
+ KickPipelineStatisticsQuery();
}
void GSDeviceOGL::CreateTimestampQueries()
@@ -1191,6 +1201,100 @@ float GSDeviceOGL::GetAndResetAccumulatedGPUTime()
return value;
}
+void GSDeviceOGL::PopPipelineStatisticsQuery()
+{
+ while (m_waiting_pipeline_statistics_queries > 0)
+ {
+ GLint available[2] = {};
+ glGetQueryObjectiv(m_pipeline_statistics_queries[m_read_pipeline_statistics_query][0], GL_QUERY_RESULT_AVAILABLE, &available[0]);
+ glGetQueryObjectiv(m_pipeline_statistics_queries[m_read_pipeline_statistics_query][1], GL_QUERY_RESULT_AVAILABLE, &available[1]);
+
+ if (!(available[0] && available[1]))
+ break;
+
+ GPUPipelineStatistics stats = {};
+ glGetQueryObjectui64v(m_pipeline_statistics_queries[m_read_pipeline_statistics_query][0], GL_QUERY_RESULT, &stats.vs_invocations);
+ glGetQueryObjectui64v(m_pipeline_statistics_queries[m_read_pipeline_statistics_query][1], GL_QUERY_RESULT, &stats.ps_invocations);
+ m_accumulated_gpu_pipeline_statistics.vs_invocations += stats.vs_invocations;
+ m_accumulated_gpu_pipeline_statistics.ps_invocations += stats.ps_invocations;
+ m_read_pipeline_statistics_query = (m_read_pipeline_statistics_query + 1) % NUM_PIPELINE_STATISTICS_QUERIES;
+ m_waiting_pipeline_statistics_queries--;
+ }
+
+ if (m_pipeline_statistics_query_started)
+ {
+ glEndQuery(GL_VERTEX_SHADER_INVOCATIONS_ARB);
+ glEndQuery(GL_FRAGMENT_SHADER_INVOCATIONS_ARB);
+
+ m_write_pipeline_statistics_query = (m_write_pipeline_statistics_query + 1) % NUM_TIMESTAMP_QUERIES;
+ m_pipeline_statistics_query_started = false;
+ m_waiting_pipeline_statistics_queries++;
+ }
+}
+
+void GSDeviceOGL::KickPipelineStatisticsQuery()
+{
+ if (m_pipeline_statistics_query_started || m_waiting_pipeline_statistics_queries == NUM_PIPELINE_STATISTICS_QUERIES)
+ return;
+
+ glBeginQuery(GL_VERTEX_SHADER_INVOCATIONS_ARB, m_pipeline_statistics_queries[m_write_pipeline_statistics_query][0]);
+ glBeginQuery(GL_FRAGMENT_SHADER_INVOCATIONS_ARB, m_pipeline_statistics_queries[m_write_pipeline_statistics_query][1]);
+ m_pipeline_statistics_query_started = true;
+}
+
+void GSDeviceOGL::CreatePipelineStatisticsQueries()
+{
+ for (int i = 0; i < NUM_PIPELINE_STATISTICS_QUERIES; i++)
+ {
+ glGenQueries(2, m_pipeline_statistics_queries[i].data());
+ }
+ KickPipelineStatisticsQuery();
+}
+
+void GSDeviceOGL::DestroyPipelineStatisticsQueries()
+{
+ if (m_pipeline_statistics_queries[0][0] == 0)
+ return;
+
+ if (m_pipeline_statistics_query_started)
+ {
+ glEndQuery(GL_VERTEX_SHADER_INVOCATIONS_ARB);
+ glEndQuery(GL_FRAGMENT_SHADER_INVOCATIONS_ARB);
+ }
+
+ for (int i = 0; i < m_pipeline_statistics_queries.size(); i++)
+ {
+ glDeleteQueries(2, m_pipeline_statistics_queries[i].data());
+ m_pipeline_statistics_queries[i].fill(0);
+ }
+ m_read_pipeline_statistics_query = 0;
+ m_write_pipeline_statistics_query = 0;
+ m_waiting_pipeline_statistics_queries = 0;
+ m_pipeline_statistics_query_started = false;
+}
+
+GPUPipelineStatistics GSDeviceOGL::GetAndResetAccumulatedGPUPipelineStatistics()
+{
+ GPUPipelineStatistics stats = m_accumulated_gpu_pipeline_statistics;
+ m_accumulated_gpu_pipeline_statistics = {};
+ return stats;
+}
+
+bool GSDeviceOGL::SetGPUPipelineStatisticsEnabled(bool enabled)
+{
+ if (m_gpu_pipeline_statistics_enabled == enabled)
+ return true;
+
+ m_gpu_pipeline_statistics_enabled = enabled && m_gpu_pipeline_statistics_supported;
+
+ if (m_gpu_pipeline_statistics_enabled)
+ CreatePipelineStatisticsQueries();
+ else
+ DestroyPipelineStatisticsQueries();
+
+ return true;
+}
+
void GSDeviceOGL::DrawPrimitive()
{
g_perfmon.Put(GSPerfMon::DrawCalls, 1);
diff --git a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h
index aff062881b..80a88c154f 100644
--- a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h
+++ b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h
@@ -147,6 +147,7 @@ public:
private:
static constexpr u8 NUM_TIMESTAMP_QUERIES = 5;
+ static constexpr u8 NUM_PIPELINE_STATISTICS_QUERIES = 5;
std::unique_ptr m_gl_context;
@@ -253,6 +254,15 @@ private:
bool m_timestamp_query_started = false;
bool m_gpu_timing_enabled = false;
+ std::array, NUM_PIPELINE_STATISTICS_QUERIES> m_pipeline_statistics_queries = {};
+ GPUPipelineStatistics m_accumulated_gpu_pipeline_statistics = {};
+ u8 m_read_pipeline_statistics_query = 0;
+ u8 m_write_pipeline_statistics_query = 0;
+ u8 m_waiting_pipeline_statistics_queries = 0;
+ bool m_pipeline_statistics_query_started = false;
+ bool m_gpu_pipeline_statistics_enabled = false;
+ bool m_gpu_pipeline_statistics_supported = false;
+
GSHWDrawConfig::VSConstantBuffer m_vs_cb_cache;
GSHWDrawConfig::PSConstantBuffer m_ps_cb_cache;
GSHWDrawConfig::VSPushConstants m_vs_pc_cache;
@@ -272,6 +282,11 @@ private:
GSTexture* CreateSurface(GSTexture::Usage usage, int width, int height, int levels, GSTexture::Format format) override;
+ void CreatePipelineStatisticsQueries();
+ void DestroyPipelineStatisticsQueries();
+ void PopPipelineStatisticsQuery();
+ void KickPipelineStatisticsQuery();
+
void DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, GSVector4* dRect, const GSRegPMODE& PMODE, const GSRegEXTBUF& EXTBUF, u32 c, const Filter filter) override;
void DoInterlace(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect, ShaderInterlace shader, Filter filter, const InterlaceConstantBuffer& cb) override;
@@ -337,6 +352,9 @@ public:
bool SetGPUTimingEnabled(bool enabled) override;
float GetAndResetAccumulatedGPUTime() override;
+ bool SetGPUPipelineStatisticsEnabled(bool enabled) override;
+ GPUPipelineStatistics GetAndResetAccumulatedGPUPipelineStatistics() override;
+
// Helpers and utility draws.
void DrawPrimitive();
void DrawIndexedPrimitive();
diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp
index 2d55f76e60..2e1cccc0b9 100644
--- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp
+++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp
@@ -462,6 +462,7 @@ bool GSDeviceVK::SelectDeviceFeatures()
m_device_features.textureCompressionBC = available_features.textureCompressionBC;
m_device_features.geometryShader = available_features.geometryShader;
m_device_features.fragmentStoresAndAtomics = available_features.fragmentStoresAndAtomics;
+ m_device_features.pipelineStatisticsQuery = available_features.pipelineStatisticsQuery;
return true;
}
@@ -697,6 +698,9 @@ bool GSDeviceVK::CreateDevice(VkSurfaceKHR surface, bool enable_validation_layer
queue_family_properties[m_graphics_queue_family_index].timestampValidBits,
m_device_properties.limits.timestampPeriod);
+ m_gpu_pipeline_statistics_supported = (m_device_features.pipelineStatisticsQuery != 0);
+ DevCon.WriteLn("GPU pipeline statistics is %s", m_gpu_pipeline_statistics_supported ? "supported" : "not supported");
+
if (!ProcessDeviceExtensions())
return false;
@@ -997,6 +1001,20 @@ bool GSDeviceVK::CreateGlobalDescriptorPool()
}
}
+ if (m_gpu_pipeline_statistics_supported)
+ {
+ const VkQueryPoolCreateInfo query_create_info = {
+ VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, nullptr, 0, VK_QUERY_TYPE_PIPELINE_STATISTICS, NUM_COMMAND_BUFFERS,
+ VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT | VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT };
+ res = vkCreateQueryPool(m_device, &query_create_info, nullptr, &m_pipeline_statistics_query_pool);
+ if (res != VK_SUCCESS)
+ {
+ LOG_VULKAN_ERROR(res, "vkCreateQueryPool failed: ");
+ m_gpu_pipeline_statistics_supported = false;
+ return false;
+ }
+ }
+
return true;
}
@@ -1122,6 +1140,19 @@ bool GSDeviceVK::SetGPUTimingEnabled(bool enabled)
return (enabled == m_gpu_timing_enabled);
}
+GPUPipelineStatistics GSDeviceVK::GetAndResetAccumulatedGPUPipelineStatistics()
+{
+ GPUPipelineStatistics stats = m_accumulated_gpu_pipeline_statistics;
+ m_accumulated_gpu_pipeline_statistics = {};
+ return stats;
+}
+
+bool GSDeviceVK::SetGPUPipelineStatisticsEnabled(bool enabled)
+{
+ m_gpu_pipeline_statistics_enabled = enabled && m_gpu_pipeline_statistics_supported;
+ return true;
+}
+
void GSDeviceVK::ScanForCommandBufferCompletion()
{
for (u32 check_index = (m_current_frame + 1) % NUM_COMMAND_BUFFERS; check_index != m_current_frame;
@@ -1198,6 +1229,13 @@ void GSDeviceVK::SubmitCommandBuffer(VKSwapChain* present_swap_chain)
m_current_frame * 2 + 1);
}
+ if (resources.pipeline_statistics_query == QueryState::Querying)
+ {
+ // Didn't end query in BeginPresent() so end it here.
+ resources.pipeline_statistics_query = QueryState::Ready;
+ vkCmdEndQuery(m_current_command_buffer, m_pipeline_statistics_query_pool, m_current_frame);
+ }
+
res = vkEndCommandBuffer(resources.command_buffers[1]);
if (res != VK_SUCCESS)
{
@@ -1404,6 +1442,33 @@ void GSDeviceVK::ActivateCommandBuffer(u32 index)
resources.command_buffers[1], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, m_timestamp_query_pool, index * 2);
}
+ if (resources.pipeline_statistics_query == QueryState::Ready)
+ {
+ // Collect the pipeline statistics from the last time this cmdbuffer was used.
+ resources.pipeline_statistics_query = QueryState::None;
+ GPUPipelineStatistics stats{};
+ VkResult res =
+ vkGetQueryPoolResults(m_device, m_pipeline_statistics_query_pool, index, 1,
+ sizeof(stats), &stats, sizeof(u64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
+ if (res == VK_SUCCESS)
+ {
+ m_accumulated_gpu_pipeline_statistics.vs_invocations += stats.vs_invocations;
+ m_accumulated_gpu_pipeline_statistics.ps_invocations += stats.ps_invocations;
+ }
+ else
+ {
+ LOG_VULKAN_ERROR(res, "vkGetQueryPoolResults failed: ");
+ }
+ }
+
+ if (m_gpu_pipeline_statistics_enabled)
+ {
+ pxAssert(resources.pipeline_statistics_query == QueryState::None);
+ resources.pipeline_statistics_query = QueryState::Querying;
+ vkCmdResetQueryPool(resources.command_buffers[1], m_pipeline_statistics_query_pool, index, 1);
+ vkCmdBeginQuery(resources.command_buffers[1], m_pipeline_statistics_query_pool, index, 0);
+ }
+
resources.fence_counter = m_next_fence_counter++;
resources.init_buffer_used = false;
resources.timestamp_written = wants_timestamp;
@@ -2367,6 +2432,14 @@ GSDevice::PresentResult GSDeviceVK::BeginPresent(bool frame_skip)
return PresentResult::FrameSkipped;
}
+ // End the pipeline statistics for this cmdbuffer before postprocessing.
+ FrameResources& resources = m_frame_resources[m_current_frame];
+ if (resources.pipeline_statistics_query == QueryState::Querying)
+ {
+ resources.pipeline_statistics_query = QueryState::Ready;
+ vkCmdEndQuery(m_current_command_buffer, m_pipeline_statistics_query_pool, m_current_frame);
+ }
+
VkResult res = m_resize_requested ? VK_ERROR_OUT_OF_DATE_KHR : m_swap_chain->AcquireNextImage();
if (res != VK_SUCCESS)
{
@@ -4819,6 +4892,9 @@ void GSDeviceVK::DestroyResources()
if (m_timestamp_query_pool != VK_NULL_HANDLE)
vkDestroyQueryPool(m_device, m_timestamp_query_pool, nullptr);
+ if (m_pipeline_statistics_query_pool != VK_NULL_HANDLE)
+ vkDestroyQueryPool(m_device, m_pipeline_statistics_query_pool, nullptr);
+
if (m_global_descriptor_pool != VK_NULL_HANDLE)
vkDestroyDescriptorPool(m_device, m_global_descriptor_pool, nullptr);
diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h
index 19d4f4fb7c..c245f34a92 100644
--- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h
+++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h
@@ -209,6 +209,14 @@ private:
void CalibrateSpinTimestamp();
u64 GetCPUTimestamp();
+ // For pipeline statistics
+ enum class QueryState
+ {
+ None,
+ Querying,
+ Ready,
+ };
+
struct FrameResources
{
// [0] - Init (upload) command buffer, [1] - draw command buffer
@@ -221,6 +229,7 @@ private:
bool init_buffer_used = false;
bool needs_fence_wait = false;
bool timestamp_written = false;
+ QueryState pipeline_statistics_query = QueryState::None;
std::vector> cleanup_resources;
};
@@ -277,6 +286,11 @@ private:
bool m_wants_new_timestamp_calibration = false;
VkTimeDomainEXT m_calibrated_timestamp_type = VK_TIME_DOMAIN_DEVICE_EXT;
+ VkQueryPool m_pipeline_statistics_query_pool = VK_NULL_HANDLE;
+ GPUPipelineStatistics m_accumulated_gpu_pipeline_statistics{};
+ bool m_gpu_pipeline_statistics_enabled = false;
+ bool m_gpu_pipeline_statistics_supported = false;
+
std::array m_frame_resources;
u64 m_next_fence_counter = 1;
u64 m_completed_fence_counter = 0;
@@ -563,6 +577,9 @@ public:
bool SetGPUTimingEnabled(bool enabled) override;
float GetAndResetAccumulatedGPUTime() override;
+ bool SetGPUPipelineStatisticsEnabled(bool enabled) override;
+ GPUPipelineStatistics GetAndResetAccumulatedGPUPipelineStatistics() override;
+
void PushDebugGroup(const char* fmt, ...) override;
void PopDebugGroup() override;
void InsertDebugMessage(DebugMessageCategory category, const char* fmt, ...) override;
diff --git a/pcsx2/ImGui/FullscreenUI_Settings.cpp b/pcsx2/ImGui/FullscreenUI_Settings.cpp
index 253af71a8c..8edec72252 100644
--- a/pcsx2/ImGui/FullscreenUI_Settings.cpp
+++ b/pcsx2/ImGui/FullscreenUI_Settings.cpp
@@ -3461,6 +3461,8 @@ void FullscreenUI::DrawOSDSettingsPage()
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_BUG, "Show GPU Debug Info"),
FSUI_CSTR("Shows debug information about the renderer."), "EmuCore/GS", "OsdShowGPUDebug", false);
#endif
+ DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_IMAGE, "Show GPU Stats"),
+ FSUI_CSTR("Shows the host's GPU pipeline statistics."), "EmuCore/GS", "OsdShowGPUStats", false);
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_PF_HEARTBEAT_ALT, "Show Frame Times"),
FSUI_CSTR("Shows a visual history of frame times."), "EmuCore/GS", "OsdShowFrameTimes", false);
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_SLIDERS, "Show Settings"),
diff --git a/pcsx2/ImGui/ImGuiOverlays.cpp b/pcsx2/ImGui/ImGuiOverlays.cpp
index a54c4725d0..f49f121e19 100644
--- a/pcsx2/ImGui/ImGuiOverlays.cpp
+++ b/pcsx2/ImGui/ImGuiOverlays.cpp
@@ -71,6 +71,7 @@ std::vector s_software_thread_lines;
SmallString s_capture_line;
SmallString s_gpu_usage_line;
SmallString s_gpu_debug_info_line;
+SmallString s_gpu_stats_line;
SmallString s_speed_icon;
constexpr ImU32 white_color = IM_COL32(255, 255, 255, 255);
@@ -516,6 +517,24 @@ __ri void ImGuiManager::DrawPerformanceOverlay(float& position_y, float scale, f
}
#endif
}
+
+ if (GSConfig.OsdShowGPUStats)
+ {
+ const auto FormatUnits = [](double val) {
+ if (val >= 1e9)
+ return fmt::format("{:.5}B", val / 1e9);
+ if (val >= 1e6)
+ return fmt::format("{:.5}M", val / 1e6);
+ if (val >= 1e3)
+ return fmt::format("{:.5}K", val / 1e3);
+ return fmt::format("{:.5}", val);
+ };
+
+ s_gpu_stats_line.format("VSI: {} | PSI: {}",
+ FormatUnits(PerformanceMetrics::GetGPUAverageVSInvocations()),
+ FormatUnits(PerformanceMetrics::GetGPUAveragePSInvocations()));
+ DRAW_LINE(osd_font, font_size, s_gpu_stats_line.c_str(), white_color);
+ }
}
// No refresh yet. Display cached lines.
else
@@ -568,6 +587,11 @@ __ri void ImGuiManager::DrawPerformanceOverlay(float& position_y, float scale, f
DRAW_LINE(osd_font, font_size, s_gpu_debug_info_line.c_str(), white_color);
#endif
}
+
+ if (GSConfig.OsdShowGPUStats)
+ {
+ DRAW_LINE(osd_font, font_size, s_gpu_stats_line.c_str(), white_color);
+ }
}
// Check every OSD frame because this is an animation.
diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp
index df57ca9341..6dba18bbc1 100644
--- a/pcsx2/Pcsx2Config.cpp
+++ b/pcsx2/Pcsx2Config.cpp
@@ -733,6 +733,7 @@ Pcsx2Config::GSOptions::GSOptions()
OsdShowCPU = false;
OsdShowGPU = false;
OsdShowGPUDebug = false;
+ OsdShowGPUStats = false;
OsdShowIndicators = true;
OsdShowFrameTimes = false;
OsdShowHardwareInfo = false;
@@ -966,6 +967,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap)
SettingsWrapBitBool(OsdShowCPU);
SettingsWrapBitBool(OsdShowGPU);
SettingsWrapBitBool(OsdShowGPUDebug);
+ SettingsWrapBitBool(OsdShowGPUStats);
SettingsWrapBitBool(OsdShowResolution);
SettingsWrapBitBool(OsdShowGSStats);
SettingsWrapBitBool(OsdShowIndicators);
diff --git a/pcsx2/PerformanceMetrics.cpp b/pcsx2/PerformanceMetrics.cpp
index 42a8409bfd..c1f3c543f2 100644
--- a/pcsx2/PerformanceMetrics.cpp
+++ b/pcsx2/PerformanceMetrics.cpp
@@ -70,6 +70,10 @@ static float s_average_gpu_time = 0.0f;
static float s_accumulated_gpu_time = 0.0f;
static float s_gpu_usage = 0.0f;
static u32 s_presents_since_last_update = 0;
+static double s_average_gpu_vs_invocations = 0.0;
+static double s_average_gpu_ps_invocations = 0.0;
+static u64 s_accumulated_gpu_vs_invocations = 0;
+static u64 s_accumulated_gpu_ps_invocations = 0;
void PerformanceMetrics::Clear()
{
@@ -156,8 +160,12 @@ void PerformanceMetrics::Update(bool gs_register_write, bool fb_blit, bool is_sk
s_maximum_frame_time = std::exchange(s_maximum_frame_time_accumulator, 0.0f);
s_fps = static_cast(s_frames_since_last_update) / time;
s_average_gpu_time = s_accumulated_gpu_time / static_cast(s_unskipped_frames_since_last_update);
+ s_average_gpu_vs_invocations = static_cast(s_accumulated_gpu_vs_invocations) / static_cast(s_unskipped_frames_since_last_update);
+ s_average_gpu_ps_invocations = static_cast(s_accumulated_gpu_ps_invocations) / static_cast(s_unskipped_frames_since_last_update);
s_gpu_usage = s_accumulated_gpu_time / (time * 10.0f);
s_accumulated_gpu_time = 0.0f;
+ s_accumulated_gpu_vs_invocations = 0;
+ s_accumulated_gpu_ps_invocations = 0;
// prefer privileged register write based framerate detection, it's less likely to have false positives
if (s_gs_privileged_register_writes_since_last_update > 0 && !EmuConfig.Gamefixes.BlitInternalFPSHack)
@@ -228,9 +236,11 @@ void PerformanceMetrics::Update(bool gs_register_write, bool fb_blit, bool is_sk
Host::OnPerformanceMetricsUpdated();
}
-void PerformanceMetrics::OnGPUPresent(float gpu_time)
+void PerformanceMetrics::OnGPUPresent(float gpu_time, u64 vs_invocations, u64 ps_invocations)
{
s_accumulated_gpu_time += gpu_time;
+ s_accumulated_gpu_vs_invocations += vs_invocations;
+ s_accumulated_gpu_ps_invocations += ps_invocations;
s_presents_since_last_update++;
}
@@ -362,6 +372,16 @@ float PerformanceMetrics::GetGPUAverageTime()
return s_average_gpu_time;
}
+double PerformanceMetrics::GetGPUAverageVSInvocations()
+{
+ return s_average_gpu_vs_invocations;
+}
+
+double PerformanceMetrics::GetGPUAveragePSInvocations()
+{
+ return s_average_gpu_ps_invocations;
+}
+
const PerformanceMetrics::FrameTimeHistory& PerformanceMetrics::GetFrameTimeHistory()
{
return s_frame_time_history;
diff --git a/pcsx2/PerformanceMetrics.h b/pcsx2/PerformanceMetrics.h
index 80c83e5d15..0efc488432 100644
--- a/pcsx2/PerformanceMetrics.h
+++ b/pcsx2/PerformanceMetrics.h
@@ -21,7 +21,7 @@ namespace PerformanceMetrics
void Clear();
void Reset();
void Update(bool gs_register_write, bool fb_blit, bool is_skipping_present);
- void OnGPUPresent(float gpu_time);
+ void OnGPUPresent(float gpu_time, u64 vs_invocations, u64 ps_invocations);
/// Sets the EE thread for CPU usage calculations.
void SetCPUThread(Threading::ThreadHandle thread);
@@ -57,6 +57,8 @@ namespace PerformanceMetrics
float GetGPUUsage();
float GetGPUAverageTime();
+ double GetGPUAverageVSInvocations();
+ double GetGPUAveragePSInvocations();
const FrameTimeHistory& GetFrameTimeHistory();
u32 GetFrameTimeHistoryPos();