Files
Valiantsin DzerakhandClaude Opus 5.5 3422952e85 Show the versions, 1% low and 0.1% low in the full performance overlay (#587, #588)
The panel opens with the port version and the game update, so a screenshot in a bug report carries both. Under RAM,
after a gap, two rows on the same grid: FPS with the 1% and 0.1% lows, and the frame times. perfmon.c keeps a
histogram of frame times in 0.25 ms bins, one slice per second over the last ten seconds, and reads the lows from it
twice a second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:09:22 +03:00

394 lines
18 KiB
C

// Performance overlay numbers (runtime/perfmon.h). Frame times come from the game's presents (perf_present, only while
// the overlay is on); the per-frame waits from the perf counters prof_win.c keeps. The machine's numbers are sampled
// twice a second on a thread of their own (some calls take milliseconds), which runs only while the overlay is full.
// The OS calls sit behind platform.h (plat_cpu_load, plat_mem_info, plat_gpu_load). Temperature, power and clock come
// from the GPU vendor's driver library, loaded at run time: NVML on NVIDIA, ADL's PM log on AMD; on a machine without
// either their numbers stay -1.
//
// Adapted from SavageWindRecomp's runtime/perfmon.c (https://github.com/Revivedx/SavageWindRecomp @
// 575ad88e02f9a89a9a885d90039d48af33c982f4, MIT, Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors,
// Copyright (c) 2026 Peiikai): the frame ring and its one-second summary, the NVML calls, the sampler loop. The
// Windows, Linux and macOS sampling moved behind platform.h, GPU load and video memory come from the system's counters
// for every vendor, and the sampler stops when the overlay leaves full.
//
// The ADL declarations (AdapterInfo, ADLSingleSensorData, ADLPMLogDataOutput, the ADL_PMLOG_* sensor numbers and the
// ADL2_* signatures) are copied from AMD's display-library include/adl_structures.h, adl_defines.h and adl_sdk.h
// (https://github.com/GPUOpen-LibrariesAndSDKs/display-library @ 84c881cb2136f04287eb3af12de00a9e2867aa9e, MIT,
// Copyright (c) 2016 - 2022 Advanced Micro Devices, Inc.).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "host.h"
#include "perfmon.h"
#include "platform/platform.h"
#include "sysinfo.h"
static PlatMutex g_lock = PLAT_MUTEX_INIT; // everything below
static PerfStats g_stats; // the machine numbers the sampler wrote; the rest is made per snapshot
// ---- frames
static float g_ring[PERF_HISTORY];
static uint64_t g_ring_at[PERF_HISTORY]; // when each frame ended
static int g_ring_n, g_ring_pos;
// The lows: a histogram of the frame times (perf.c's 0.25 ms bins) over the last LOW_SLICES seconds, kept as one slice
// per second so the oldest second drops out whole. Per frame: one increment. The percentiles are read twice a second.
#define LOW_BINS 1024
#define LOW_SLICES 10
static uint16_t g_low_hist[LOW_SLICES][LOW_BINS];
static int g_low_cur;
static uint64_t g_low_t0;
// The per-frame counters, summed since t0 (the overlay's lines are averages over half a second).
static struct {
uint64_t t0, frames, gpu0;
double replay, pipe, xlate, tex;
int64_t up0, up;
} g_win;
void perfmon_frame(uint64_t now, uint64_t interval_ns, const uint64_t *wait_ns, uint64_t replay_ns,
int64_t tex_uploads) {
plat_mutex_lock(&g_lock);
if (interval_ns) {
g_ring[g_ring_pos] = (float)(interval_ns / 1e6);
g_ring_at[g_ring_pos] = now;
g_ring_pos = (g_ring_pos + 1) % PERF_HISTORY;
if (g_ring_n < PERF_HISTORY) g_ring_n++;
if (now - g_low_t0 >= 1000000000ull) { // the next second: its slice starts empty
g_low_cur = (g_low_cur + 1) % LOW_SLICES;
memset(g_low_hist[g_low_cur], 0, sizeof g_low_hist[0]);
g_low_t0 = now;
}
uint64_t bin = interval_ns / 250000ull;
uint16_t *h = &g_low_hist[g_low_cur][bin < LOW_BINS ? bin : LOW_BINS - 1];
if (*h < 65535) (*h)++;
}
g_win.frames++;
g_win.replay += replay_ns / 1e6;
g_win.pipe += wait_ns[PERF_PIPE] / 1e6;
g_win.xlate += wait_ns[PERF_XLATE] / 1e6;
g_win.tex += wait_ns[PERF_TEX] / 1e6;
g_win.up = tex_uploads;
plat_mutex_unlock(&g_lock);
}
static void frames_summary(PerfStats *s, uint64_t now) { // (lock held)
double sum = 0, mx = 0;
int n = 0;
for (int i = 0; i < g_ring_n; i++) {
int k = (g_ring_pos - 1 - i + PERF_HISTORY) % PERF_HISTORY;
if (now - g_ring_at[k] > 1000000000ull) break; // the last second
sum += g_ring[k];
if (g_ring[k] > mx) mx = g_ring[k];
n++;
}
s->fps = n && sum > 0 ? (float)(n * 1000.0 / sum) : 0;
s->frame_ms_avg = n ? (float)(sum / n) : 0;
s->frame_ms_max = (float)mx;
for (int i = 0; i < PERF_HISTORY; i++) { // oldest first; frames not seen yet read 0
int age = PERF_HISTORY - 1 - i;
s->frame_ms[i] = age < g_ring_n ? g_ring[(g_ring_pos - 1 - age + 2 * PERF_HISTORY) % PERF_HISTORY] : 0;
}
}
// The fps of the slowest 1% and 0.1% of the frames in the window (the bin's upper edge, as perf.c's summary does); -1
// when the window holds too few frames. (lock held)
static void lows_summary(PerfStats *s) {
uint32_t total = 0;
for (int k = 0; k < LOW_SLICES; k++)
for (int b = 0; b < LOW_BINS; b++) total += g_low_hist[k][b];
s->low1_fps = s->low01_fps = -1;
if (total < 100) return;
uint32_t cut1 = (total + 99) / 100, cut01 = (total + 999) / 1000, acc = 0;
for (int b = LOW_BINS - 1; b >= 0 && s->low1_fps < 0; b--) {
for (int k = 0; k < LOW_SLICES; k++) acc += g_low_hist[k][b];
float fps = 1000.0f / ((b + 1) * 0.25f);
if (s->low01_fps < 0 && acc >= cut01) s->low01_fps = fps;
if (acc >= cut1) s->low1_fps = fps;
}
}
// Every half second: the window's sums as per-frame averages into g_stats, and a new window. (lock held)
static void counters_summary(uint64_t now) {
if (now - g_win.t0 < 500000000ull) return;
lows_summary(&g_stats);
uint64_t gpu = perf_gpu_busy_total();
if (g_win.t0 && g_win.frames) {
double n = (double)g_win.frames;
g_stats.replay_ms = (float)(g_win.replay / n);
g_stats.pipe_wait_ms = (float)(g_win.pipe / n);
g_stats.xlate_ms = (float)(g_win.xlate / n);
g_stats.tex_ms = (float)(g_win.tex / n);
g_stats.tex_uploads = (float)((g_win.up - g_win.up0) / n);
// no GPU time over a window of frames: no timestamps on this device (or not yet set up)
g_stats.gpu_ms = gpu > g_win.gpu0 ? (float)((gpu - g_win.gpu0) / 1e6 / n) : -1;
}
g_win.t0 = now;
g_win.frames = 0;
g_win.gpu0 = gpu;
g_win.replay = g_win.pipe = g_win.xlate = g_win.tex = 0;
g_win.up0 = g_win.up;
}
// ---- NVML (NVIDIA): loaded once on the sampler thread, kept for the process
typedef struct { unsigned gpu, memory; } NvmlUtil;
typedef struct { unsigned long long total, free, used; } NvmlMem;
static struct {
int tried, ok;
void *dev;
int (*util)(void *, NvmlUtil *);
int (*temp)(void *, int, unsigned *);
int (*mem)(void *, NvmlMem *);
int (*power)(void *, unsigned *);
int (*clock)(void *, int, unsigned *);
} nv;
static char g_gpu_name[96];
static uint8_t g_gpu_luid[8];
static int g_gpu_luid_ok;
static int g_gpu_bus = -1;
static float g_vram_total_mb = -1;
void perfmon_set_gpu(const char *name, const uint8_t *luid, int pci_bus, float vram_total_mb) {
plat_mutex_lock(&g_lock);
snprintf(g_gpu_name, sizeof g_gpu_name, "%s", name ? name : "");
g_gpu_luid_ok = luid != NULL;
if (luid) memcpy(g_gpu_luid, luid, sizeof g_gpu_luid);
g_gpu_bus = pci_bus;
g_vram_total_mb = vram_total_mb > 0 ? vram_total_mb : -1;
plat_mutex_unlock(&g_lock);
}
static void nvml_open(const char *hint) {
nv.tried = 1;
#ifdef _WIN32
static const char *const names[] = {"nvml.dll", "C:\\Program Files\\NVIDIA Corporation\\NVSMI\\nvml.dll"};
#else
static const char *const names[] = {"libnvidia-ml.so.1"};
#endif
void *lib = NULL;
for (size_t i = 0; !lib && i < sizeof names / sizeof *names; i++) lib = plat_lib_open(names[i]);
if (!lib) return;
int (*init)(void) = (int (*)(void))plat_lib_sym(lib, "nvmlInit_v2");
int (*count)(unsigned *) = (int (*)(unsigned *))plat_lib_sym(lib, "nvmlDeviceGetCount_v2");
int (*handle)(unsigned, void **) = (int (*)(unsigned, void **))plat_lib_sym(lib, "nvmlDeviceGetHandleByIndex_v2");
int (*name)(void *, char *, unsigned) = (int (*)(void *, char *, unsigned))plat_lib_sym(lib, "nvmlDeviceGetName");
nv.util = (int (*)(void *, NvmlUtil *))plat_lib_sym(lib, "nvmlDeviceGetUtilizationRates");
nv.temp = (int (*)(void *, int, unsigned *))plat_lib_sym(lib, "nvmlDeviceGetTemperature");
nv.mem = (int (*)(void *, NvmlMem *))plat_lib_sym(lib, "nvmlDeviceGetMemoryInfo");
nv.power = (int (*)(void *, unsigned *))plat_lib_sym(lib, "nvmlDeviceGetPowerUsage");
nv.clock = (int (*)(void *, int, unsigned *))plat_lib_sym(lib, "nvmlDeviceGetClockInfo");
if (!init || !count || !handle || init() != 0) return;
unsigned n = 0;
if (count(&n) != 0 || !n) return;
// the device Vulkan renders on (by name; an NVIDIA name only matches an NVIDIA GPU), else none: on a machine
// that renders on another vendor's GPU, NVML's numbers would belong to the wrong card
for (unsigned i = 0; i < n && !nv.dev; i++) {
void *d = NULL;
char nm[96] = "";
if (handle(i, &d) != 0 || !name || name(d, nm, sizeof nm) != 0 || !nm[0]) continue;
if (hint[0] && (strstr(hint, nm) || strstr(nm, hint))) nv.dev = d;
}
nv.ok = nv.dev != NULL;
rt_log("perfmon: NVML %s", nv.ok ? "found the rendering GPU" : "has no device matching the rendering GPU");
}
static void nvml_sample(PerfStats *s) {
if (!nv.ok) return;
NvmlUtil u;
NvmlMem m;
unsigned v;
if (s->gpu_pct < 0 && nv.util && nv.util(nv.dev, &u) == 0) s->gpu_pct = (float)u.gpu; // the system's counters first
if (nv.mem && nv.mem(nv.dev, &m) == 0) {
if (s->vram_used_mb < 0) s->vram_used_mb = (float)(m.used >> 20);
s->vram_total_mb = (float)(m.total >> 20);
}
if (nv.temp && nv.temp(nv.dev, 0 /* NVML_TEMPERATURE_GPU */, &v) == 0) s->gpu_temp_c = (float)v;
if (nv.power && nv.power(nv.dev, &v) == 0) s->gpu_power_w = v / 1000.0f;
if (nv.clock && nv.clock(nv.dev, 0 /* NVML_CLOCK_GRAPHICS */, &v) == 0) s->gpu_clock_mhz = (float)v;
}
// ---- ADL (AMD): loaded once on the sampler thread, kept for the process. ADL2_New_QueryPMLogData_Get reads the PM log
// the driver keeps for Radeon RX 5000 and later (the numbers Adrenalin's overlay shows); older GPUs answer with an
// error and their numbers stay -1. Windows only: the Linux driver gives these through sysfs, which this does not read.
#ifdef _WIN32
#define ADL_MAX_PATH 256
#define ADL_PMLOG_MAX_SENSORS 256
enum {
ADL_PMLOG_CLK_GFXCLK = 1,
ADL_PMLOG_TEMPERATURE_EDGE = 8,
ADL_PMLOG_INFO_ACTIVITY_GFX = 19,
ADL_PMLOG_ASIC_POWER = 23,
ADL_PMLOG_TEMPERATURE_HOTSPOT = 27,
ADL_PMLOG_BOARD_POWER = 73,
};
typedef struct { // AdapterInfo, the Windows layout
int iSize, iAdapterIndex;
char strUDID[ADL_MAX_PATH];
int iBusNumber, iDeviceNumber, iFunctionNumber, iVendorID;
char strAdapterName[ADL_MAX_PATH], strDisplayName[ADL_MAX_PATH];
int iPresent, iExist;
char strDriverPath[ADL_MAX_PATH], strDriverPathExt[ADL_MAX_PATH], strPNPString[ADL_MAX_PATH];
int iOSDisplayIndex;
} AdlAdapterInfo;
typedef struct { int supported, value; } AdlSensor;
typedef struct { int size; AdlSensor sensors[ADL_PMLOG_MAX_SENSORS]; } AdlPmLog;
typedef void *(__stdcall *AdlAlloc)(int);
static struct {
int tried, ok, index;
void *ctx;
int (*pmlog)(void *, int, AdlPmLog *);
AdlPmLog log; // the sampler thread's buffer
} adl;
static void *__stdcall adl_alloc(int n) { return malloc((size_t)n); }
// The adapter Vulkan renders on: the PCI bus when the device gave one, else the name. ADL lists one index per display
// output of a GPU; the first whose PM log answers is taken.
static void adl_open(const char *hint, int bus) {
adl.tried = 1;
void *lib = plat_lib_open("atiadlxx.dll");
if (!lib) return;
int (*create)(AdlAlloc, int, void **) = (int (*)(AdlAlloc, int, void **))plat_lib_sym(lib, "ADL2_Main_Control_Create");
int (*count)(void *, int *) = (int (*)(void *, int *))plat_lib_sym(lib, "ADL2_Adapter_NumberOfAdapters_Get");
int (*info)(void *, AdlAdapterInfo *, int) =
(int (*)(void *, AdlAdapterInfo *, int))plat_lib_sym(lib, "ADL2_Adapter_AdapterInfo_Get");
adl.pmlog = (int (*)(void *, int, AdlPmLog *))plat_lib_sym(lib, "ADL2_New_QueryPMLogData_Get");
int n = 0;
if (!create || !count || !info || !adl.pmlog || create(adl_alloc, 1, &adl.ctx) != 0 || count(adl.ctx, &n) != 0 ||
n <= 0 || n > 256)
return;
AdlAdapterInfo *a = (AdlAdapterInfo *)calloc((size_t)n, sizeof *a);
if (a) {
for (int i = 0; i < n; i++) a[i].iSize = sizeof *a;
if (info(adl.ctx, a, n * (int)sizeof *a) == 0) {
for (int i = 0; i < n && !adl.ok; i++) {
if (a[i].iVendorID != 1002) continue; // ADL writes the PCI vendor id 0x1002 as decimal 1002
const char *nm = a[i].strAdapterName;
if (bus >= 0 ? a[i].iBusNumber != bus : !hint[0] || !nm[0] || !(strstr(hint, nm) || strstr(nm, hint)))
continue;
memset(&adl.log, 0, sizeof adl.log);
adl.log.size = sizeof adl.log;
if (adl.pmlog(adl.ctx, a[i].iAdapterIndex, &adl.log) != 0) continue;
adl.index = a[i].iAdapterIndex;
adl.ok = 1;
}
}
free(a);
}
rt_log("perfmon: ADL %s", adl.ok ? "found the rendering GPU" : "has no PM log for the rendering GPU");
}
static void adl_sample(PerfStats *s) {
if (!adl.ok) return;
memset(&adl.log, 0, sizeof adl.log);
adl.log.size = sizeof adl.log;
if (adl.pmlog(adl.ctx, adl.index, &adl.log) != 0) return;
const AdlSensor *v = adl.log.sensors;
if (s->gpu_pct < 0 && v[ADL_PMLOG_INFO_ACTIVITY_GFX].supported) // the system's counters first
s->gpu_pct = (float)v[ADL_PMLOG_INFO_ACTIVITY_GFX].value;
if (v[ADL_PMLOG_TEMPERATURE_EDGE].supported) s->gpu_temp_c = (float)v[ADL_PMLOG_TEMPERATURE_EDGE].value;
else if (v[ADL_PMLOG_TEMPERATURE_HOTSPOT].supported) s->gpu_temp_c = (float)v[ADL_PMLOG_TEMPERATURE_HOTSPOT].value;
// the whole card's draw where the driver reports it (RDNA 4 reports no ASIC power), else the GPU chip's
if (v[ADL_PMLOG_BOARD_POWER].supported) s->gpu_power_w = (float)v[ADL_PMLOG_BOARD_POWER].value;
else if (v[ADL_PMLOG_ASIC_POWER].supported) s->gpu_power_w = (float)v[ADL_PMLOG_ASIC_POWER].value;
if (v[ADL_PMLOG_CLK_GFXCLK].supported) s->gpu_clock_mhz = (float)v[ADL_PMLOG_CLK_GFXCLK].value;
}
#else
static struct { int tried; } adl;
static void adl_open(const char *hint, int bus) { (void)hint, (void)bus, adl.tried = 1; }
static void adl_sample(PerfStats *s) { (void)s; }
#endif
// ---- the machine sampler: runs while want is set, twice a second
static struct {
int want, alive;
PlatCond cv;
} g_smp = {0, 0, PLAT_COND_INIT};
static void machine_clear(PerfStats *s) {
s->cpu_total_pct = s->cpu_game_pct = s->cpu_temp_c = -1;
s->ram_used_mb = s->ram_total_mb = s->ram_game_mb = -1;
s->gpu_pct = s->gpu_temp_c = s->gpu_power_w = s->gpu_clock_mhz = s->vram_used_mb = s->vram_total_mb = -1;
}
static void machine_copy(PerfStats *d, const PerfStats *s) {
memcpy(d->cpu_name, s->cpu_name, sizeof d->cpu_name);
memcpy(d->gpu_name, s->gpu_name, sizeof d->gpu_name);
d->cpu_total_pct = s->cpu_total_pct, d->cpu_game_pct = s->cpu_game_pct, d->cpu_temp_c = s->cpu_temp_c;
d->ram_used_mb = s->ram_used_mb, d->ram_total_mb = s->ram_total_mb, d->ram_game_mb = s->ram_game_mb;
d->gpu_pct = s->gpu_pct, d->gpu_temp_c = s->gpu_temp_c, d->gpu_power_w = s->gpu_power_w;
d->gpu_clock_mhz = s->gpu_clock_mhz, d->vram_used_mb = s->vram_used_mb, d->vram_total_mb = s->vram_total_mb;
}
static void sampler(void *arg) {
(void)arg;
PerfStats s;
memset(&s, 0, sizeof s);
plat_mutex_lock(&g_lock);
uint8_t luid[8];
int have_luid = g_gpu_luid_ok;
memcpy(luid, g_gpu_luid, sizeof luid);
snprintf(s.gpu_name, sizeof s.gpu_name, "%s", g_gpu_name);
int bus = g_gpu_bus;
float vram_total = g_vram_total_mb;
plat_mutex_unlock(&g_lock);
sysinfo_cpu_name(s.cpu_name, sizeof s.cpu_name);
if (!nv.tried) nvml_open(s.gpu_name);
if (!nv.ok && !adl.tried) adl_open(s.gpu_name, bus);
PlatLoad *l = plat_load_open(have_luid ? luid : NULL, bus);
plat_mutex_lock(&g_lock);
while (g_smp.want) {
plat_mutex_unlock(&g_lock);
machine_clear(&s);
plat_cpu_load(l, &s.cpu_total_pct, &s.cpu_game_pct, &s.cpu_temp_c);
plat_mem_info(&s.ram_used_mb, &s.ram_total_mb, &s.ram_game_mb);
plat_gpu_load(l, &s.gpu_pct, &s.vram_used_mb);
s.vram_total_mb = vram_total;
nvml_sample(&s);
adl_sample(&s);
plat_mutex_lock(&g_lock);
machine_copy(&g_stats, &s);
if (g_smp.want) plat_cond_wait_ns(&g_smp.cv, &g_lock, 500000000ull);
}
g_smp.alive = 0; // in the same locked section as the last want check: a new perfmon_mode(2) starts a new thread
plat_mutex_unlock(&g_lock);
plat_load_close(l);
}
void perfmon_mode(int mode) {
perf_set_overlay(mode);
plat_mutex_lock(&g_lock);
if (mode == 2) {
g_smp.want = 1;
if (!g_smp.alive) {
machine_clear(&g_stats); // "-" until the first sample, not the numbers of the last time
PlatThread *t = plat_thread_new(sampler, NULL, "perfmon");
g_smp.alive = t != NULL;
if (t) plat_thread_detach(t);
else rt_log("perfmon: cannot start the sampling thread; the machine numbers stay empty");
}
} else {
g_smp.want = 0;
plat_cond_signal(&g_smp.cv);
}
if (!mode) { // the next start begins a new graph and a new counter window
g_ring_n = g_ring_pos = 0;
memset(g_low_hist, 0, sizeof g_low_hist);
g_low_t0 = 0;
memset(&g_win, 0, sizeof g_win);
}
plat_mutex_unlock(&g_lock);
}
void perfmon_snapshot(PerfStats *out) {
uint64_t now = plat_now_ns();
plat_mutex_lock(&g_lock);
frames_summary(&g_stats, now);
counters_summary(now);
*out = g_stats;
plat_mutex_unlock(&g_lock);
}