// 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 // // 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 bus kind integrated|discrete name "" watts temp_c fan_rpm // fan_pct <%> mclk_mhz gclk_mhz util_pct <%> source adlx|sysfs|perfcounter // then one `end ` 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 #include #include #include 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 */ 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 = -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(" source %s\n", s->source); } #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #include #include #include "../vendor/adlx/SDK/ADLXHelper/Windows/C/ADLXHelper.h" #include "../vendor/adlx/SDK/Include/IPerformanceMonitoring.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 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; } 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 - */ 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; BusEntry buses[32]; int nBuses; for (i = 1; i < argc; ++i) if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) every = atoi(argv[++i]); 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(); 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 (gPerf) gPerf->pVtbl->Release(gPerf); ADLXHelper_Terminate(); } return 0; } #else #include #include #include 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 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); printSample(ordinal++, &s); } closedir(d); return ordinal; } int main(int argc, char** argv) { int every = 0, i; 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]); signal(SIGINT, onSignal); signal(SIGTERM, onSignal); setvbuf(stdout, NULL, _IOLBF, 0); 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