GS tooling: report GPU device and driver identity in stats output

Both `gsctl stats` and `gsrunner -stats-json` reported counters with no
indication of which GPU or driver produced them, so a stats blob from a
tester was not attributable — and driver identity is the axis nearly every
mobile GPU behaviour turns on.

This matters beyond provenance. Several GS features are force-overridden
per-driver (framebuffer fetch and texture barriers are forced on for Adreno
regardless of INI), so without knowing the driver you cannot tell whether a
settings A/B was applied at all or silently compared a config against itself.

gsrunner captures the strings on the GS thread at first present rather than
at shutdown, where the device may already be gone. Both emitters escape the
values, since GetDriverInfo() is multi-line on Vulkan.
This commit is contained in:
Brian Degenhardt
2026-07-22 21:16:45 -07:00
parent fca9018dce
commit 9a4bd0f56f
2 changed files with 59 additions and 3 deletions
+29
View File
@@ -148,6 +148,8 @@ static std::deque<std::function<void()>> s_cpu_thread_tasks;
static std::string s_stats_json_path;
static std::string s_drawlog_path;
static std::vector<FrameSample> s_frame_samples;
static std::string s_device_name;
static std::string s_driver_info;
static u64 s_frame_timer_last = 0;
static double s_last_prims = 0;
static double s_last_tc_source_hit = 0;
@@ -339,6 +341,15 @@ void Host::BeginPresentFrame()
if (GSIsHardwareRenderer())
{
// Captured here rather than at shutdown: this runs on the GS thread with the
// device definitely live, and it is the axis that decides whether a settings
// A/B was even applied (several GS features are force-overridden per-driver).
if (s_device_name.empty() && g_gs_device)
{
s_device_name = g_gs_device->GetName();
s_driver_info = g_gs_device->GetDriverInfo();
}
const u32 last_draws = s_total_internal_draws;
const u32 last_uploads = s_total_uploads;
@@ -1161,7 +1172,25 @@ static void WriteStatsJson(const std::string& path)
}
}
// GetDriverInfo() is multi-line on Vulkan, and neither string is JSON-safe as-is.
const auto json_escape = [](const std::string& in) {
std::string out;
out.reserve(in.size());
for (const char c : in)
{
if (c == '\n' || c == '\r' || c == '\t')
out.push_back(' ');
else if (c == '"' || c == '\\')
out.push_back('\'');
else
out.push_back(c);
}
return out;
};
std::fprintf(fp.get(), "{\n \"run\": {\n");
std::fprintf(fp.get(), " \"device_name\": \"%s\",\n \"driver_info\": \"%s\",\n",
json_escape(s_device_name).c_str(), json_escape(s_driver_info).c_str());
std::fprintf(fp.get(), " \"frames\": %u,\n \"drawn_frames\": %u,\n", s_total_frames, s_total_drawn_frames);
std::fprintf(fp.get(), " \"prims\": %" PRIu64 ",\n \"draws\": %" PRIu64 ",\n \"draw_calls\": %" PRIu64 ",\n",
s_total_prims, s_total_internal_draws, s_total_draws);
+30 -3
View File
@@ -7,6 +7,7 @@
#include "Elfheader.h"
#include "GS.h"
#include "GS/GSPerfMon.h"
#include "GS/Renderers/Common/GSDevice.h"
#include "MTGS.h"
#include "PerformanceMetrics.h"
#include "SaveState.h"
@@ -306,11 +307,35 @@ namespace PINEServer
static std::string BuildStatsJson()
{
SmallString gs_memory;
// Device identity is the axis nearly every mobile GPU bug turns on, so a stats
// blob without it is not attributable to a driver. Adreno tiers run drivers
// (Mesa Turnip vs Qualcomm proprietary) that behave oppositely for fbfetch and
// push descriptors, so the driver string matters more than the device name.
std::string device_name, driver_info;
if (MTGS::IsOpen())
{
MTGS::RunOnGSThread([&gs_memory]() { GSgetMemoryStats(gs_memory); });
MTGS::RunOnGSThread([&gs_memory, &device_name, &driver_info]() {
GSgetMemoryStats(gs_memory);
if (g_gs_device)
{
device_name = g_gs_device->GetName();
driver_info = g_gs_device->GetDriverInfo();
}
});
MTGS::WaitGS(false);
}
// Newlines and quotes would break the JSON; GetDriverInfo() is multi-line on Vulkan.
const auto sanitize = [](std::string& s) {
for (char& c : s)
{
if (c == '\n' || c == '\r' || c == '\t')
c = ' ';
else if (c == '"' || c == '\\')
c = '\'';
}
};
sanitize(device_name);
sanitize(driver_info);
const auto counter = [](GSPerfMon::counter_t c) { return g_perfmon.Get(c); };
@@ -330,7 +355,8 @@ namespace PINEServer
"\"tc_source_hit\":{:.1f},\"tc_source_miss\":{:.1f},"
"\"tc_target_hit\":{:.1f},\"tc_target_miss\":{:.1f},"
"\"hash_cache_hit\":{:.1f},\"hash_cache_miss\":{:.1f},"
"\"gs_memory\":\"{}\",\"frame_number\":{}"
"\"gs_memory\":\"{}\",\"frame_number\":{},"
"\"renderer\":\"{}\",\"device_name\":\"{}\",\"driver_info\":\"{}\""
"}}",
PerformanceMetrics::GetFPS(), PerformanceMetrics::GetInternalFPS(), PerformanceMetrics::GetSpeed(),
PerformanceMetrics::GetAverageFrameTime(), PerformanceMetrics::GetMinimumFrameTime(),
@@ -348,7 +374,8 @@ namespace PINEServer
counter(GSPerfMon::TCSourceHit), counter(GSPerfMon::TCSourceMiss),
counter(GSPerfMon::TCTargetHit), counter(GSPerfMon::TCTargetMiss),
counter(GSPerfMon::HashCacheHit), counter(GSPerfMon::HashCacheMiss),
gs_memory.view(), PerformanceMetrics::GetFrameNumber());
gs_memory.view(), PerformanceMetrics::GetFrameNumber(),
Pcsx2Config::GSOptions::GetRendererName(EmuConfig.GS.Renderer), device_name, driver_info);
}
} // namespace PINEServer