471 lines
28 KiB
C
471 lines
28 KiB
C
// igneum-gpu-telemetry: power, temperature, fan and clocks of every AMD GPU, one line per card per sample.
|
|
// 5 October 2026, after the project lead watched a 9070 XT at 90% usage with its fans barely turning and the app could not say
|
|
// what it drew (the app's draw, temperature and MH per watt line came from nvidia-smi only).
|
|
//
|
|
// igneum-gpu-telemetry [-l SECONDS] one sample (default), or one every SECONDS until stdin closes or SIGTERM
|
|
// igneum-gpu-telemetry --tune what each card's manual tuning allows: the max GPU clock range and value,
|
|
// the power limit range and value (percent offset from the default), one `tune` line per card
|
|
// igneum-gpu-telemetry --card N --set-gmax MHZ | --set-plimit PCT | --reset
|
|
// set the max GPU clock, the power limit (offset percent, 0 = default, -20 = 80%),
|
|
// or every tuning value back to factory, on card N (the `amd N` ordinal); prints the
|
|
// `tune` line read back after the change. ADLX manual tuning needs no elevation
|
|
// (confirmed on PC 1, 5 October 2026, from an unelevated job). Linux: pp_od_clk_voltage
|
|
// ("s 1 MHZ" then "c") and hwmon power1_cap (microwatts), which need root.
|
|
//
|
|
// Windows: ADLX (the AMD Device Library eXtra, amdadlx64.dll, shipped with Adrenalin; vendor/adlx is the SDK clone,
|
|
// MIT) for the metrics, keyed by the card's PCI bus from SetupAPI (the display class, matched by the same name ADLX
|
|
// reports). Without ADLX (no AMD driver, an old one, or the DLL missing) only the utilisation is read, from the
|
|
// GPU Engine performance counters through PDH, keyed by the adapter LUID that Windows uses there.
|
|
// Linux: the amdgpu sysfs (/sys/class/drm/card*/device: hwmon power1_average, temp1_input, fan1_input, pwm1,
|
|
// pp_dpm_mclk, gpu_busy_percent), keyed by the PCI address of the device link.
|
|
//
|
|
// Line format (space separated, every field present, a value the source cannot give prints as -):
|
|
// amd <ordinal> bus <pci bus or address> kind integrated|discrete name "<name>" watts <W> temp_c <C> fan_rpm <rpm>
|
|
// fan_pct <%> mclk_mhz <MHz> gclk_mhz <MHz> util_pct <%> plimit_pct <100 + offset> gmax_mhz <MHz> source adlx|sysfs|perfcounter
|
|
// tune <ordinal> name "<name>" gmax <MHz> gmax_range <min> <max> plimit <offset %> plimit_range <min> <max> factory 0|1 ok|<error>
|
|
// then one `end <ms>` line per sample. The app (engine.rs amd_telemetry_line) parses it; parsers are unit-tested
|
|
// against lines captured on PC 1.
|
|
#define _CRT_SECURE_NO_WARNINGS
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <signal.h>
|
|
|
|
static volatile int gStop = 0;
|
|
static void onSignal(int s) { (void)s; gStop = 1; }
|
|
|
|
typedef struct {
|
|
char bus[64];
|
|
char kind[16];
|
|
char name[128];
|
|
double watts, tempC, fanRpm, fanPct, mclk, gclk, util; /* -1 = not available */
|
|
double plimitPct, gmax; /* the limits in force: 100 + the power offset; the max GPU clock */
|
|
const char* source;
|
|
} Sample;
|
|
|
|
static void sampleInit(Sample* s) { memset(s, 0, sizeof(*s)); strcpy(s->bus, "-"); strcpy(s->kind, "-"); strcpy(s->name, "-"); s->watts = s->tempC = s->fanRpm = s->fanPct = s->mclk = s->gclk = s->util = s->plimitPct = s->gmax = -1.0; s->source = "-"; }
|
|
static void printNum(double v, const char* fmt) { if (v < 0) printf(" -"); else printf(fmt, v); }
|
|
static void printSample(int ordinal, const Sample* s) {
|
|
printf("amd %d bus %s kind %s name \"%s\" watts", ordinal, s->bus, s->kind, s->name);
|
|
printNum(s->watts, " %.1f"); printf(" temp_c"); printNum(s->tempC, " %.1f"); printf(" fan_rpm"); printNum(s->fanRpm, " %.0f");
|
|
printf(" fan_pct"); printNum(s->fanPct, " %.0f"); printf(" mclk_mhz"); printNum(s->mclk, " %.0f"); printf(" gclk_mhz"); printNum(s->gclk, " %.0f");
|
|
printf(" util_pct"); printNum(s->util, " %.0f"); printf(" plimit_pct"); printNum(s->plimitPct, " %.0f"); printf(" gmax_mhz"); printNum(s->gmax, " %.0f");
|
|
printf(" source %s\n", s->source);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#include <setupapi.h>
|
|
#include <pdh.h>
|
|
#include "../vendor/adlx/SDK/ADLXHelper/Windows/C/ADLXHelper.h"
|
|
#include "../vendor/adlx/SDK/Include/IPerformanceMonitoring.h"
|
|
#include "../vendor/adlx/SDK/Include/IGPUTuning.h"
|
|
#include "../vendor/adlx/SDK/Include/IGPUManualGFXTuning.h"
|
|
#include "../vendor/adlx/SDK/Include/IGPUManualPowerTuning.h"
|
|
|
|
/* The SDK declares these three and leaves them to the platform file of each sample. */
|
|
adlx_handle ADLX_CDECL_CALL adlx_load_library(const TCHAR* filename) { return (adlx_handle)LoadLibrary(filename); }
|
|
int ADLX_CDECL_CALL adlx_free_library(adlx_handle module) { return FreeLibrary((HMODULE)module) ? 1 : 0; }
|
|
void* ADLX_CDECL_CALL adlx_get_proc_address(adlx_handle module, const char* procName) { return (void*)GetProcAddress((HMODULE)module, procName); }
|
|
|
|
static double nowMs(void) { LARGE_INTEGER f, c; QueryPerformanceFrequency(&f); QueryPerformanceCounter(&c); return (double)c.QuadPart * 1000.0 / (double)f.QuadPart; }
|
|
|
|
/* The PCI bus of every display-class device, by its name (SetupAPI; the names are the ones ADLX reports). */
|
|
typedef struct { char name[128]; int bus; } BusEntry;
|
|
static int listBuses(BusEntry* out, int cap) {
|
|
static const GUID DISPLAY = { 0x4d36e968, 0xe325, 0x11ce, { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } };
|
|
HDEVINFO set = SetupDiGetClassDevsA(&DISPLAY, NULL, NULL, DIGCF_PRESENT);
|
|
SP_DEVINFO_DATA d;
|
|
DWORD i;
|
|
int n = 0;
|
|
if (set == INVALID_HANDLE_VALUE) return 0;
|
|
d.cbSize = sizeof(d);
|
|
for (i = 0; SetupDiEnumDeviceInfo(set, i, &d) && n < cap; ++i) {
|
|
char name[128] = { 0 };
|
|
DWORD bus = 0, type = 0, got = 0;
|
|
if (!SetupDiGetDeviceRegistryPropertyA(set, &d, SPDRP_DEVICEDESC, &type, (BYTE*)name, sizeof(name) - 1, &got)) continue;
|
|
if (!SetupDiGetDeviceRegistryPropertyA(set, &d, SPDRP_BUSNUMBER, &type, (BYTE*)&bus, sizeof(bus), &got)) continue;
|
|
snprintf(out[n].name, sizeof(out[n].name), "%s", name); out[n].bus = (int)bus; ++n;
|
|
}
|
|
SetupDiDestroyDeviceInfoList(set);
|
|
return n;
|
|
}
|
|
static int busOf(const BusEntry* b, int n, const char* name, int* taken) {
|
|
int i;
|
|
for (i = 0; i < n; ++i) if (!taken[i] && strcmp(b[i].name, name) == 0) { taken[i] = 1; return b[i].bus; }
|
|
return -1;
|
|
}
|
|
|
|
/* ADLX: one sample of every GPU. Returns the number of lines printed, -1 when ADLX is not usable (reason printed). */
|
|
static IADLXSystem* gSys = NULL;
|
|
static IADLXPerformanceMonitoringServices* gPerf = NULL;
|
|
static IADLXGPUTuningServices* gTune = NULL; /* NULL when the driver has no tuning services */
|
|
|
|
/* The manual tuning interfaces of one GPU; either may be NULL (not supported on the card, or an iGPU). */
|
|
typedef struct { IADLXManualGraphicsTuning2* gfx; IADLXManualPowerTuning* power; } Tuning;
|
|
static Tuning tuningOf(IADLXGPU* gpu) {
|
|
Tuning tu = { NULL, NULL };
|
|
adlx_bool ok = 0;
|
|
IADLXInterface* raw = NULL;
|
|
if (!gTune) return tu;
|
|
if (ADLX_SUCCEEDED(gTune->pVtbl->IsSupportedManualGFXTuning(gTune, gpu, &ok)) && ok && ADLX_SUCCEEDED(gTune->pVtbl->GetManualGFXTuning(gTune, gpu, &raw)) && raw) {
|
|
if (!ADLX_SUCCEEDED(raw->pVtbl->QueryInterface(raw, IID_IADLXManualGraphicsTuning2(), (void**)&tu.gfx))) tu.gfx = NULL;
|
|
raw->pVtbl->Release(raw); raw = NULL;
|
|
}
|
|
if (ADLX_SUCCEEDED(gTune->pVtbl->IsSupportedManualPowerTuning(gTune, gpu, &ok)) && ok && ADLX_SUCCEEDED(gTune->pVtbl->GetManualPowerTuning(gTune, gpu, &raw)) && raw) {
|
|
if (!ADLX_SUCCEEDED(raw->pVtbl->QueryInterface(raw, IID_IADLXManualPowerTuning(), (void**)&tu.power))) tu.power = NULL;
|
|
raw->pVtbl->Release(raw);
|
|
}
|
|
return tu;
|
|
}
|
|
static void tuningRelease(Tuning* tu) { if (tu->gfx) tu->gfx->pVtbl->Release(tu->gfx); if (tu->power) tu->power->pVtbl->Release(tu->power); tu->gfx = NULL; tu->power = NULL; }
|
|
|
|
/* The limits in force, for a sample line (-1 when the card has no manual tuning). */
|
|
static void tuningNow(IADLXGPU* gpu, double* plimitPct, double* gmax) {
|
|
Tuning tu = tuningOf(gpu);
|
|
adlx_int v = 0;
|
|
*plimitPct = -1.0; *gmax = -1.0;
|
|
if (tu.power && ADLX_SUCCEEDED(tu.power->pVtbl->GetPowerLimit(tu.power, &v))) *plimitPct = 100.0 + v;
|
|
if (tu.gfx && ADLX_SUCCEEDED(tu.gfx->pVtbl->GetGPUMaxFrequency(tu.gfx, &v))) *gmax = v;
|
|
tuningRelease(&tu);
|
|
}
|
|
|
|
/* One `tune` line: ranges, values, whether the card is at factory settings, and the verdict of the last request. */
|
|
static void printTune(int ordinal, IADLXGPU* gpu, const char* verdict) {
|
|
Tuning tu = tuningOf(gpu);
|
|
const char* name = NULL;
|
|
ADLX_IntRange r = { 0, 0, 0 };
|
|
adlx_int v = 0;
|
|
adlx_bool factory = 0;
|
|
char buf[128] = "-";
|
|
if (ADLX_SUCCEEDED(gpu->pVtbl->Name(gpu, &name)) && name) snprintf(buf, sizeof(buf), "%s", name);
|
|
printf("tune %d name \"%s\"", ordinal, buf);
|
|
if (tu.gfx && ADLX_SUCCEEDED(tu.gfx->pVtbl->GetGPUMaxFrequency(tu.gfx, &v)) && ADLX_SUCCEEDED(tu.gfx->pVtbl->GetGPUMaxFrequencyRange(tu.gfx, &r))) printf(" gmax %d gmax_range %d %d", (int)v, (int)r.minValue, (int)r.maxValue);
|
|
else printf(" gmax - gmax_range - -");
|
|
if (tu.power && ADLX_SUCCEEDED(tu.power->pVtbl->GetPowerLimit(tu.power, &v)) && ADLX_SUCCEEDED(tu.power->pVtbl->GetPowerLimitRange(tu.power, &r))) printf(" plimit %d plimit_range %d %d", (int)v, (int)r.minValue, (int)r.maxValue);
|
|
else printf(" plimit - plimit_range - -");
|
|
if (gTune && ADLX_SUCCEEDED(gTune->pVtbl->IsAtFactory(gTune, gpu, &factory))) printf(" factory %d", factory ? 1 : 0); else printf(" factory -");
|
|
printf(" %s\n", verdict);
|
|
tuningRelease(&tu);
|
|
}
|
|
|
|
/* --card N with --set-gmax, --set-plimit or --reset: apply, then print the tune line read back. */
|
|
static int tuneCard(int card, int setGmax, int gmax, int setPlimit, int plimit, int reset) {
|
|
IADLXGPUList* gpus = NULL;
|
|
adlx_uint it;
|
|
int ordinal = 0, done = 0;
|
|
if (!gTune) { printf("tune %d error no tuning services (ADLX too old, or no AMD driver)\n", card); return 2; }
|
|
if (!ADLX_SUCCEEDED(gSys->pVtbl->GetGPUs(gSys, &gpus)) || !gpus) { printf("tune %d error GetGPUs failed\n", card); return 2; }
|
|
for (it = gpus->pVtbl->Begin(gpus); it != gpus->pVtbl->End(gpus); ++it, ++ordinal) {
|
|
IADLXGPU* gpu = NULL;
|
|
char verdict[160] = "ok";
|
|
ADLX_RESULT r = ADLX_OK;
|
|
if (ordinal != card) continue;
|
|
if (!ADLX_SUCCEEDED(gpus->pVtbl->At_GPUList(gpus, it, &gpu)) || !gpu) continue;
|
|
if (reset) {
|
|
r = gTune->pVtbl->ResetToFactory(gTune, gpu);
|
|
if (!ADLX_SUCCEEDED(r)) snprintf(verdict, sizeof(verdict), "error ResetToFactory returned %d", (int)r);
|
|
} else {
|
|
Tuning tu = tuningOf(gpu);
|
|
if (setGmax) {
|
|
if (!tu.gfx) snprintf(verdict, sizeof(verdict), "error no manual GFX tuning on this card");
|
|
else { r = tu.gfx->pVtbl->SetGPUMaxFrequency(tu.gfx, gmax); if (!ADLX_SUCCEEDED(r)) snprintf(verdict, sizeof(verdict), "error SetGPUMaxFrequency(%d) returned %d", gmax, (int)r); }
|
|
}
|
|
if (setPlimit && verdict[0] == 'o') {
|
|
if (!tu.power) snprintf(verdict, sizeof(verdict), "error no manual power tuning on this card");
|
|
else { r = tu.power->pVtbl->SetPowerLimit(tu.power, plimit); if (!ADLX_SUCCEEDED(r)) snprintf(verdict, sizeof(verdict), "error SetPowerLimit(%d) returned %d", plimit, (int)r); }
|
|
}
|
|
tuningRelease(&tu);
|
|
}
|
|
printTune(ordinal, gpu, verdict);
|
|
done = verdict[0] == 'o';
|
|
gpu->pVtbl->Release(gpu);
|
|
}
|
|
gpus->pVtbl->Release(gpus);
|
|
if (!done && ordinal <= card) printf("tune %d error no such card (%d listed)\n", card, ordinal);
|
|
return done ? 0 : 1;
|
|
}
|
|
static int tuneReport(void) {
|
|
IADLXGPUList* gpus = NULL;
|
|
adlx_uint it;
|
|
int ordinal = 0;
|
|
if (!gTune) { printf("info adlx: no tuning services\n"); return 1; }
|
|
if (!ADLX_SUCCEEDED(gSys->pVtbl->GetGPUs(gSys, &gpus)) || !gpus) return 1;
|
|
for (it = gpus->pVtbl->Begin(gpus); it != gpus->pVtbl->End(gpus); ++it, ++ordinal) {
|
|
IADLXGPU* gpu = NULL;
|
|
if (!ADLX_SUCCEEDED(gpus->pVtbl->At_GPUList(gpus, it, &gpu)) || !gpu) continue;
|
|
printTune(ordinal, gpu, "ok");
|
|
gpu->pVtbl->Release(gpu);
|
|
}
|
|
gpus->pVtbl->Release(gpus);
|
|
return 0;
|
|
}
|
|
static int adlxOpen(void) {
|
|
ADLX_RESULT r = ADLXHelper_Initialize();
|
|
if (!ADLX_SUCCEEDED(r)) { printf("info adlx: ADLXHelper_Initialize returned %d (no AMD driver with ADLX; amdadlx64.dll missing or too old)\n", (int)r); return 0; }
|
|
gSys = ADLXHelper_GetSystemServices();
|
|
if (!gSys) { printf("info adlx: no system services\n"); return 0; }
|
|
r = gSys->pVtbl->GetPerformanceMonitoringServices(gSys, &gPerf);
|
|
if (!ADLX_SUCCEEDED(r) || !gPerf) { printf("info adlx: GetPerformanceMonitoringServices returned %d\n", (int)r); return 0; }
|
|
r = gSys->pVtbl->GetGPUTuningServices(gSys, &gTune);
|
|
if (!ADLX_SUCCEEDED(r) || !gTune) { printf("info adlx: GetGPUTuningServices returned %d (no manual tuning)\n", (int)r); gTune = NULL; }
|
|
return 1;
|
|
}
|
|
static int adlxSample(const BusEntry* buses, int nBuses) {
|
|
IADLXGPUList* gpus = NULL;
|
|
adlx_uint it;
|
|
int ordinal = 0;
|
|
int taken[32] = { 0 };
|
|
ADLX_RESULT r = gSys->pVtbl->GetGPUs(gSys, &gpus);
|
|
if (!ADLX_SUCCEEDED(r) || !gpus) { printf("info adlx: GetGPUs returned %d\n", (int)r); return 0; }
|
|
for (it = gpus->pVtbl->Begin(gpus); it != gpus->pVtbl->End(gpus); ++it) {
|
|
IADLXGPU* gpu = NULL;
|
|
IADLXGPUMetrics* m = NULL;
|
|
Sample s;
|
|
const char* name = NULL;
|
|
ADLX_GPU_TYPE type = GPUTYPE_UNDEFINED;
|
|
adlx_double dv = 0; adlx_int iv = 0;
|
|
if (!ADLX_SUCCEEDED(gpus->pVtbl->At_GPUList(gpus, it, &gpu)) || !gpu) continue;
|
|
sampleInit(&s);
|
|
s.source = "adlx";
|
|
if (ADLX_SUCCEEDED(gpu->pVtbl->Name(gpu, &name)) && name) snprintf(s.name, sizeof(s.name), "%s", name);
|
|
if (ADLX_SUCCEEDED(gpu->pVtbl->Type(gpu, &type))) strcpy(s.kind, type == GPUTYPE_INTEGRATED ? "integrated" : type == GPUTYPE_DISCRETE ? "discrete" : "-");
|
|
{ int b = busOf(buses, nBuses, s.name, taken); if (b >= 0) snprintf(s.bus, sizeof(s.bus), "%d", b); }
|
|
r = gPerf->pVtbl->GetCurrentGPUMetrics(gPerf, gpu, &m);
|
|
if (ADLX_SUCCEEDED(r) && m) {
|
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUPower(m, &dv))) s.watts = dv;
|
|
if (s.watts < 0 && ADLX_SUCCEEDED(m->pVtbl->GPUTotalBoardPower(m, &dv))) s.watts = dv;
|
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUTemperature(m, &dv))) s.tempC = dv;
|
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUFanSpeed(m, &iv))) s.fanRpm = iv;
|
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUVRAMClockSpeed(m, &iv))) s.mclk = iv;
|
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUClockSpeed(m, &iv))) s.gclk = iv;
|
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUUsage(m, &dv))) s.util = dv;
|
|
m->pVtbl->Release(m);
|
|
} else {
|
|
printf("info adlx: GetCurrentGPUMetrics for \"%s\" returned %d\n", s.name, (int)r);
|
|
}
|
|
/* fan percent: ADLX gives rpm only here; the tuning interface has the range, the app shows rpm when pct is - */
|
|
tuningNow(gpu, &s.plimitPct, &s.gmax);
|
|
printSample(ordinal++, &s);
|
|
gpu->pVtbl->Release(gpu);
|
|
}
|
|
gpus->pVtbl->Release(gpus);
|
|
return ordinal;
|
|
}
|
|
|
|
/* PDH fallback: GPU engine utilisation per adapter LUID, summed over the engines (no power, no temperature). */
|
|
static int pdhSample(void) {
|
|
PDH_HQUERY q = NULL;
|
|
PDH_HCOUNTER c = NULL;
|
|
DWORD size = 0, count = 0, i;
|
|
PDH_FMT_COUNTERVALUE_ITEM_A* items;
|
|
int ordinal = 0;
|
|
if (PdhOpenQueryA(NULL, 0, &q) != ERROR_SUCCESS) { printf("info perfcounter: PdhOpenQuery failed\n"); return 0; }
|
|
if (PdhAddEnglishCounterA(q, "\\GPU Engine(*)\\Utilization Percentage", 0, &c) != ERROR_SUCCESS) { printf("info perfcounter: no GPU Engine counters\n"); PdhCloseQuery(q); return 0; }
|
|
PdhCollectQueryData(q); Sleep(1000); PdhCollectQueryData(q);
|
|
PdhGetFormattedCounterArrayA(c, PDH_FMT_DOUBLE, &size, &count, NULL);
|
|
items = (PDH_FMT_COUNTERVALUE_ITEM_A*)malloc(size ? size : 1);
|
|
if (PdhGetFormattedCounterArrayA(c, PDH_FMT_DOUBLE, &size, &count, items) == ERROR_SUCCESS) {
|
|
/* instance names: pid_1234_luid_0x00000000_0x0000D4E3_phys_0_eng_0_engtype_3D; sum per luid */
|
|
char luids[16][40]; double sums[16]; int n = 0, k;
|
|
for (i = 0; i < count; ++i) {
|
|
const char* p = strstr(items[i].szName, "luid_");
|
|
char luid[40];
|
|
if (!p) continue;
|
|
snprintf(luid, sizeof(luid), "%.39s", p); { char* e = strstr(luid, "_phys"); if (e) *e = 0; }
|
|
for (k = 0; k < n; ++k) if (strcmp(luids[k], luid) == 0) break;
|
|
if (k == n && n < 16) { strcpy(luids[n], luid); sums[n] = 0; ++n; }
|
|
if (k < 16) sums[k] += items[i].FmtValue.doubleValue;
|
|
}
|
|
for (k = 0; k < n; ++k) {
|
|
Sample s; sampleInit(&s); s.source = "perfcounter";
|
|
snprintf(s.bus, sizeof(s.bus), "%s", luids[k]);
|
|
s.util = sums[k] > 100.0 ? 100.0 : sums[k];
|
|
printSample(ordinal++, &s);
|
|
}
|
|
}
|
|
free(items);
|
|
PdhCloseQuery(q);
|
|
return ordinal;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
int every = 0, i, haveAdlx, tune = 0, card = -1, setGmax = 0, gmax = 0, setPlimit = 0, plimit = 0, reset = 0;
|
|
BusEntry buses[32];
|
|
int nBuses;
|
|
for (i = 1; i < argc; ++i) {
|
|
if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) every = atoi(argv[++i]);
|
|
else if (strcmp(argv[i], "--tune") == 0) tune = 1;
|
|
else if (strcmp(argv[i], "--card") == 0 && i + 1 < argc) card = atoi(argv[++i]);
|
|
else if (strcmp(argv[i], "--set-gmax") == 0 && i + 1 < argc) { setGmax = 1; gmax = atoi(argv[++i]); }
|
|
else if (strcmp(argv[i], "--set-plimit") == 0 && i + 1 < argc) { setPlimit = 1; plimit = atoi(argv[++i]); }
|
|
else if (strcmp(argv[i], "--reset") == 0) reset = 1;
|
|
}
|
|
signal(SIGINT, onSignal); signal(SIGTERM, onSignal);
|
|
setvbuf(stdout, NULL, _IOLBF, 0);
|
|
nBuses = listBuses(buses, 32);
|
|
for (i = 0; i < nBuses; ++i) printf("info display device \"%s\" bus %d\n", buses[i].name, buses[i].bus);
|
|
haveAdlx = adlxOpen();
|
|
if (card >= 0 || tune) {
|
|
int rc;
|
|
if (!haveAdlx) { printf("tune %d error no ADLX\n", card); return 2; }
|
|
rc = (card >= 0 && (setGmax || setPlimit || reset)) ? tuneCard(card, setGmax, gmax, setPlimit, plimit, reset) : tuneReport();
|
|
fflush(stdout);
|
|
if (gTune) gTune->pVtbl->Release(gTune);
|
|
if (gPerf) gPerf->pVtbl->Release(gPerf);
|
|
ADLXHelper_Terminate();
|
|
return rc;
|
|
}
|
|
do {
|
|
double t0 = nowMs();
|
|
int n = haveAdlx ? adlxSample(buses, nBuses) : pdhSample();
|
|
printf("end %.1f ms %d card(s)\n", nowMs() - t0, n);
|
|
fflush(stdout); /* a redirected stdout is fully buffered on the Windows CRT whatever setvbuf asks (PC 1 lost 60 s of samples at the kill) */
|
|
if (every > 0) Sleep((DWORD)every * 1000);
|
|
} while (every > 0 && !gStop);
|
|
if (haveAdlx) { if (gTune) gTune->pVtbl->Release(gTune); if (gPerf) gPerf->pVtbl->Release(gPerf); ADLXHelper_Terminate(); }
|
|
return 0;
|
|
}
|
|
#else
|
|
#include <dirent.h>
|
|
#include <unistd.h>
|
|
#include <time.h>
|
|
static double nowMs(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6; }
|
|
static int readText(const char* path, char* out, size_t cap) { FILE* f = fopen(path, "r"); size_t n; if (!f) return 0; n = fread(out, 1, cap - 1, f); fclose(f); out[n] = 0; return 1; }
|
|
static double readNumber(const char* path) { char b[64]; if (!readText(path, b, sizeof(b))) return -1.0; return atof(b); }
|
|
/* pp_dpm_mclk: lines "0: 96Mhz", "3: 1258Mhz *"; the starred line is the current state */
|
|
static double dpmCurrent(const char* text) {
|
|
const char* p = text;
|
|
while (p && *p) {
|
|
const char* nl = strchr(p, '\n');
|
|
size_t len = nl ? (size_t)(nl - p) : strlen(p);
|
|
const char* star = memchr(p, '*', len);
|
|
if (star) { const char* colon = memchr(p, ':', len); if (colon) return atof(colon + 1); }
|
|
p = nl ? nl + 1 : NULL;
|
|
}
|
|
return -1.0;
|
|
}
|
|
static int writeText(const char* path, const char* text) { FILE* f = fopen(path, "w"); if (!f) return 0; fputs(text, f); return fclose(f) == 0; }
|
|
/* The device directory of the n-th amdgpu card under root (the `amd N` ordinal of the sample lines), or 0. */
|
|
static int sysfsCardDir(const char* root, int card, char* dev, size_t cap) {
|
|
DIR* d = opendir(root);
|
|
struct dirent* e;
|
|
int ordinal = 0, found = 0;
|
|
if (!d) return 0;
|
|
while ((e = readdir(d)) != NULL) {
|
|
char path[640], text[64];
|
|
if (strncmp(e->d_name, "card", 4) != 0 || strchr(e->d_name + 4, '-')) continue;
|
|
snprintf(path, sizeof(path), "%s/%s/device/vendor", root, e->d_name);
|
|
if (!readText(path, text, sizeof(text)) || strtol(text, NULL, 16) != 0x1002) continue;
|
|
if (ordinal++ == card) { snprintf(dev, cap, "%s/%s/device", root, e->d_name); found = 1; break; }
|
|
}
|
|
closedir(d);
|
|
return found;
|
|
}
|
|
/* --card N --set-gmax MHZ: pp_od_clk_voltage "s 1 MHZ" then "c"; --set-plimit PCT: hwmon power1_cap = default x (100 + PCT) / 100;
|
|
* --reset: "r" then "c" and power1_cap = power1_cap_default. Root is needed for every write; the verdict says when it is not. */
|
|
static int sysfsTune(const char* root, int card, int setGmax, int gmax, int setPlimit, int plimit, int reset) {
|
|
char dev[640], path[900], text[4096], hw[700] = "";
|
|
DIR* d;
|
|
struct dirent* e;
|
|
const char* verdict = "ok";
|
|
if (!sysfsCardDir(root, card, dev, sizeof(dev))) { printf("tune %d error no such card\n", card); return 1; }
|
|
snprintf(path, sizeof(path), "%s/hwmon", dev);
|
|
d = opendir(path);
|
|
if (d) { while ((e = readdir(d)) != NULL) if (strncmp(e->d_name, "hwmon", 5) == 0) { snprintf(hw, sizeof(hw), "%s/%s", path, e->d_name); break; } closedir(d); }
|
|
snprintf(path, sizeof(path), "%s/pp_od_clk_voltage", dev);
|
|
if (reset) {
|
|
if (!writeText(path, "r\n") || !writeText(path, "c\n")) verdict = "error pp_od_clk_voltage reset (root needed)";
|
|
if (hw[0]) { char cap[64], dp[900]; snprintf(dp, sizeof(dp), "%s/power1_cap_default", hw); if (readText(dp, cap, sizeof(cap))) { snprintf(dp, sizeof(dp), "%s/power1_cap", hw); if (!writeText(dp, cap)) verdict = "error power1_cap reset (root needed)"; } }
|
|
} else {
|
|
if (setGmax) { char cmd[64]; snprintf(cmd, sizeof(cmd), "s 1 %d\n", gmax); if (!writeText(path, cmd) || !writeText(path, "c\n")) verdict = "error pp_od_clk_voltage write (root needed, or the clock is outside the card's range)"; }
|
|
if (setPlimit && verdict[0] == 'o') {
|
|
char dp[900], cap[64];
|
|
if (!hw[0]) verdict = "error no hwmon";
|
|
else { snprintf(dp, sizeof(dp), "%s/power1_cap_default", hw); if (!readText(dp, cap, sizeof(cap))) verdict = "error no power1_cap_default"; else { double uw = atof(cap) * (100.0 + plimit) / 100.0; snprintf(cap, sizeof(cap), "%.0f\n", uw); snprintf(dp, sizeof(dp), "%s/power1_cap", hw); if (!writeText(dp, cap)) verdict = "error power1_cap write (root needed)"; } }
|
|
}
|
|
}
|
|
{
|
|
double g = -1, pc = -1, pd = -1;
|
|
snprintf(path, sizeof(path), "%s/pp_od_clk_voltage", dev); if (readText(path, text, sizeof(text))) { const char* s1 = strstr(text, "1:"); if (s1) g = atof(s1 + 2); }
|
|
if (hw[0]) { snprintf(path, sizeof(path), "%s/power1_cap", hw); pc = readNumber(path); snprintf(path, sizeof(path), "%s/power1_cap_default", hw); pd = readNumber(path); }
|
|
printf("tune %d name \"amdgpu\" gmax %.0f gmax_range - - plimit %.0f plimit_range - - factory - %s\n", card, g, (pc > 0 && pd > 0) ? 100.0 * pc / pd - 100.0 : -1.0, verdict);
|
|
}
|
|
return verdict[0] == 'o' ? 0 : 1;
|
|
}
|
|
static int sysfsSample(const char* root) {
|
|
DIR* d = opendir(root);
|
|
struct dirent* e;
|
|
int ordinal = 0;
|
|
if (!d) { printf("info sysfs: no %s\n", root); return 0; }
|
|
while ((e = readdir(d)) != NULL) {
|
|
char dev[512], path[640], text[4096], link[512];
|
|
ssize_t ln;
|
|
Sample s;
|
|
DIR* hw; struct dirent* he;
|
|
if (strncmp(e->d_name, "card", 4) != 0 || strchr(e->d_name + 4, '-')) continue;
|
|
snprintf(dev, sizeof(dev), "%s/%s/device", root, e->d_name);
|
|
snprintf(path, sizeof(path), "%s/vendor", dev);
|
|
if (!readText(path, text, sizeof(text)) || strtol(text, NULL, 16) != 0x1002) continue;
|
|
sampleInit(&s);
|
|
s.source = "sysfs";
|
|
ln = readlink(dev, link, sizeof(link) - 1);
|
|
if (ln > 0) { link[ln] = 0; { const char* base = strrchr(link, '/'); snprintf(s.bus, sizeof(s.bus), "%.63s", base ? base + 1 : link); } }
|
|
snprintf(path, sizeof(path), "%s/product_name", dev);
|
|
if (readText(path, text, sizeof(text))) { text[strcspn(text, "\n")] = 0; snprintf(s.name, sizeof(s.name), "%s", text); }
|
|
else { snprintf(path, sizeof(path), "%s/device", dev); if (readText(path, text, sizeof(text))) { text[strcspn(text, "\n")] = 0; snprintf(s.name, sizeof(s.name), "amdgpu %s", text); } }
|
|
snprintf(path, sizeof(path), "%s/boot_vga", dev);
|
|
strcpy(s.kind, "discrete");
|
|
snprintf(path, sizeof(path), "%s/hwmon", dev);
|
|
hw = opendir(path);
|
|
if (hw) {
|
|
while ((he = readdir(hw)) != NULL) {
|
|
char hp[900];
|
|
if (strncmp(he->d_name, "hwmon", 5) != 0) continue;
|
|
snprintf(hp, sizeof(hp), "%s/%s/power1_average", path, he->d_name); s.watts = readNumber(hp); if (s.watts < 0) { snprintf(hp, sizeof(hp), "%s/%s/power1_input", path, he->d_name); s.watts = readNumber(hp); } if (s.watts >= 0) s.watts /= 1e6;
|
|
snprintf(hp, sizeof(hp), "%s/%s/temp1_input", path, he->d_name); s.tempC = readNumber(hp); if (s.tempC >= 0) s.tempC /= 1000.0;
|
|
snprintf(hp, sizeof(hp), "%s/%s/fan1_input", path, he->d_name); s.fanRpm = readNumber(hp);
|
|
{ double pwm, pwmMax; snprintf(hp, sizeof(hp), "%s/%s/pwm1", path, he->d_name); pwm = readNumber(hp); snprintf(hp, sizeof(hp), "%s/%s/pwm1_max", path, he->d_name); pwmMax = readNumber(hp); if (pwm >= 0 && pwmMax > 0) s.fanPct = 100.0 * pwm / pwmMax; else if (pwm >= 0) s.fanPct = 100.0 * pwm / 255.0; }
|
|
break;
|
|
}
|
|
closedir(hw);
|
|
}
|
|
snprintf(path, sizeof(path), "%s/pp_dpm_mclk", dev); if (readText(path, text, sizeof(text))) s.mclk = dpmCurrent(text);
|
|
snprintf(path, sizeof(path), "%s/pp_dpm_sclk", dev); if (readText(path, text, sizeof(text))) s.gclk = dpmCurrent(text);
|
|
snprintf(path, sizeof(path), "%s/gpu_busy_percent", dev); s.util = readNumber(path);
|
|
{ /* the limits in force: power1_cap against its default, the OD max clock from pp_od_clk_voltage */
|
|
char hp[900]; double pc = -1, pd = -1;
|
|
snprintf(hp, sizeof(hp), "%s/hwmon", dev); hw = opendir(hp);
|
|
if (hw) { while ((he = readdir(hw)) != NULL) if (strncmp(he->d_name, "hwmon", 5) == 0) { char q[1000]; snprintf(q, sizeof(q), "%s/%s/power1_cap", hp, he->d_name); pc = readNumber(q); snprintf(q, sizeof(q), "%s/%s/power1_cap_default", hp, he->d_name); pd = readNumber(q); break; } closedir(hw); }
|
|
if (pc > 0 && pd > 0) s.plimitPct = 100.0 * pc / pd;
|
|
snprintf(hp, sizeof(hp), "%s/pp_od_clk_voltage", dev); if (readText(hp, text, sizeof(text))) { const char* s1 = strstr(text, "1:"); if (s1) s.gmax = atof(s1 + 2); }
|
|
}
|
|
printSample(ordinal++, &s);
|
|
}
|
|
closedir(d);
|
|
return ordinal;
|
|
}
|
|
int main(int argc, char** argv) {
|
|
int every = 0, i, card = -1, setGmax = 0, gmax = 0, setPlimit = 0, plimit = 0, reset = 0;
|
|
const char* root = getenv("IGNEUM_DRM_ROOT") ? getenv("IGNEUM_DRM_ROOT") : "/sys/class/drm"; /* a fixture tree for tests */
|
|
for (i = 1; i < argc; ++i) {
|
|
if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) every = atoi(argv[++i]);
|
|
else if (strcmp(argv[i], "--card") == 0 && i + 1 < argc) card = atoi(argv[++i]);
|
|
else if (strcmp(argv[i], "--set-gmax") == 0 && i + 1 < argc) { setGmax = 1; gmax = atoi(argv[++i]); }
|
|
else if (strcmp(argv[i], "--set-plimit") == 0 && i + 1 < argc) { setPlimit = 1; plimit = atoi(argv[++i]); }
|
|
else if (strcmp(argv[i], "--reset") == 0) reset = 1;
|
|
}
|
|
signal(SIGINT, onSignal); signal(SIGTERM, onSignal);
|
|
setvbuf(stdout, NULL, _IOLBF, 0);
|
|
if (card >= 0) return sysfsTune(root, card, setGmax, gmax, setPlimit, plimit, reset);
|
|
do {
|
|
double t0 = nowMs();
|
|
int n = sysfsSample(root);
|
|
printf("end %.1f ms %d card(s)\n", nowMs() - t0, n);
|
|
fflush(stdout); /* a redirected stdout is fully buffered on the Windows CRT whatever setvbuf asks (PC 1 lost 60 s of samples at the kill) */
|
|
if (every > 0) sleep((unsigned)every);
|
|
} while (every > 0 && !gStop);
|
|
return 0;
|
|
}
|
|
#endif
|