mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The overlay's rate, lows and graph were fed from the game's present calls. With the replay on the submitting thread, work held for an event is replayed with the next frame, so the calls come in uneven steps although each frame costs about the same: 36 and 9 ms apart in turn in a village, a sawtooth in the graph and a 1% low of 14 fps, while the pictures went out 19.7 ms apart on average and changed by 3.9 ms from one to the next. The present thread now reports each picture (perfmon_shown) and the overlay takes its intervals from that; the game's calls are used only where no present thread reports. A picture that arrives early now waits until seven eighths of the recent interval have passed since the one before (vk_backend.c pace_smooth, NX2NAVI_PACE_SMOOTH, on by default; not with frame generation). In that village the change from one picture to the next went from 3.9 to 3.0 ms at 51 fps both ways, in a graveyard from 3.1 to 2.6 ms at 54. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
432 lines
20 KiB
C
432 lines
20 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.
|
|
//
|
|
// Contains code Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors, Copyright (c) 2026 Peiikai (MIT; see
|
|
// THIRD_PARTY_NOTICES.md). GPU load and video memory come from the system's counters for every vendor; the sampler
|
|
// stops when the overlay leaves full.
|
|
//
|
|
// The ADL declarations (AdapterInfo, ADLSingleSensorData, ADLPMLogDataOutput, the ADL_PMLOG_* sensor numbers and the
|
|
// ADL2_* signatures) contain code Copyright (c) 2016 - 2022 Advanced Micro Devices, Inc. (MIT; see THIRD_PARTY_NOTICES.md).
|
|
#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;
|
|
|
|
// The frame times the overlay shows (the rate, the lows, the graph) are those of the pictures as they go out: the
|
|
// present thread reports each one (perfmon_shown). The game's present calls come at other times: with the replay on
|
|
// the submitting thread, work held for an event is replayed in the next frame's first submission, so the calls came
|
|
// 36 and 9 ms apart in turn in a village while the pictures went out every 17 to 27 ms, and the graph drew a sawtooth
|
|
// the screen did not show. A frame held back (vk_backend.c frame_hold) is not shown and lengthens its interval.
|
|
// Without a present thread reporting (another back end) the game's present calls are taken, as before.
|
|
static uint64_t g_shown_last; // when the last picture went out; 0: none yet
|
|
static uint64_t g_shown_report; // when perfmon_shown last ran: perfmon_frame leaves the intervals to it for a second
|
|
|
|
static void interval_add(uint64_t now, uint64_t interval_ns) { // (lock held)
|
|
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)++;
|
|
}
|
|
|
|
void perfmon_shown(uint64_t now) {
|
|
plat_mutex_lock(&g_lock);
|
|
// a gap of a second or more is a load or a pause, not a frame time
|
|
if (g_shown_last && now > g_shown_last && now - g_shown_last < 1000000000ull) interval_add(now, now - g_shown_last);
|
|
g_shown_last = g_shown_report = now;
|
|
plat_mutex_unlock(&g_lock);
|
|
}
|
|
|
|
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 (g_shown_report && now - g_shown_report < 1000000000ull) interval_ns = 0; // perfmon_shown has them
|
|
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;
|
|
static float (*g_vram_game)(void);
|
|
|
|
void perfmon_set_vram_game(float (*used_mb)(void)) { __atomic_store_n(&g_vram_game, used_mb, __ATOMIC_RELEASE); }
|
|
|
|
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;
|
|
s->vram_game_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;
|
|
d->vram_game_mb = s->vram_game_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;
|
|
float (*game)(void) = __atomic_load_n(&g_vram_game, __ATOMIC_ACQUIRE);
|
|
if (game) s.vram_game_mb = game();
|
|
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) {
|
|
// the time under the lock: a frame pushed between the two would be newer than now, and now - at wrapping would
|
|
// end the frame window at once (0 fps on the overlay)
|
|
plat_mutex_lock(&g_lock);
|
|
uint64_t now = plat_now_ns();
|
|
frames_summary(&g_stats, now);
|
|
counters_summary(now);
|
|
*out = g_stats;
|
|
plat_mutex_unlock(&g_lock);
|
|
}
|