macOS: GPU temperature not displayed on Apple Silicon M5 (SMC `flt ` data type not parsed)
Summary
On Apple Silicon M5 (MacBook Air M5, macOS 26.5), the GPU box in btop shows 0 °C for temperature, while utilisation, power, and memory all read correctly. CPU temperature is displayed correctly via the PMU tdie IOHID fallback already in HEAD.
Investigation shows two related causes:
getSMCTemponly parses thesp78data type. Apple Silicon GPU thermal sensors are exposed asflt(4-byte IEEE float) via SMC. The legacysp78parser drops these silently and returns-1, so every SMC GPU key returns-1regardless of whether it exists on the chip.- No SMC-based GPU temperature reader. The existing GPU temperature path in
btop_collect.cpp::get_gpu_temp_iohidfilters IOHID sensors by name ("GPU"substring orPMU TP*g). On M5 the IOHID sensors are named generically (PMU tdie1...PMU tdie14,PMU2 tdie1...PMU2 tdie10), so none match, and there is no fallback to SMC.
Reproduction
- MacBook Air M5 (2026), macOS 26.5, btop built from
v1.4.7(commit6e39144). - Launch btop. GPU box header shows utilisation/power/RAM values but the temperature column is blank or
0. - Verified the IOHID sensor set on M5 via Issac-Lopez/mac_temp_sensor's probe binary: the only thermal sensor names are
PMU tdie1...PMU tdie14,PMU2 tdie1...PMU2 tdie10,PMU tdev1-8,PMU2 tdev1-5,NAND CH0 temp,PMU tcal,PMU2 tcal(calibration constants), andgas gauge batteryentries. None contain"GPU"or end in"g". - Cross-checked with exelban/stats, which maps M5 GPU SMC keys as
Tg0U,Tg0X,Tg0d,Tg0g,Tg0j,Tg1Y,Tg1c,Tg1g. When called through btop's existinggetSMCTemp, every key returned-1. Probing the same keys after extendinggetSMCTempto parsefltproduced sensible values (~33 °C idle, ~70 °C under sustained Metal compute).
Suggested patch
Two small additions; minimal surface, no regression on existing sp78/IOHID paths.
src/osx/smc.cpp — parse additional data types
long long SMCConnection::getSMCTemp(char *key) {
SMCVal_t val;
kern_return_t result;
result = SMCReadKey(key, &val);
if (result != kIOReturnSuccess or val.dataSize == 0) return -1;
// sp78: 8-bit signed integer + 8-bit fraction (Intel Mac legacy)
if (strcmp(val.dataType, DATATYPE_SP78) == 0) {
int intValue = val.bytes[0] * 256 + (unsigned char)val.bytes[1];
return static_cast<long long>(intValue / 256.0);
}
// flt : 4-byte IEEE little-endian float (Apple Silicon GPU/temp sensors)
if (strcmp(val.dataType, "flt ") == 0 and val.dataSize >= 4) {
float f;
memcpy(&f, val.bytes, sizeof(f));
if (f > 0.0f and f < 150.0f) {
return static_cast<long long>(f + 0.5f);
}
}
// ui16: rare for temperature, included for completeness
if (strcmp(val.dataType, "ui16") == 0 and val.dataSize >= 2) {
unsigned int u = ((unsigned char)val.bytes[0] << 8) | (unsigned char)val.bytes[1];
if (u > 0 and u < 150) return static_cast<long long>(u);
}
return -1;
}src/osx/smc.hpp and src/osx/smc.cpp — public 4-char key reader
// in smc.hpp public section
long long getTempByKey(const char *key);
// in smc.cpp
long long SMCConnection::getTempByKey(const char *key) {
char k[5];
strncpy(k, key, 4);
k[4] = '\0';
long long t = getSMCTemp(k);
if (t <= 0 or t >= 150) return -1;
return t;
}src/osx/btop_collect.cpp — try SMC first for GPU temperature
// added before get_gpu_temp_iohid()
static long long get_gpu_temp_smc() {
static const char* keys_all[] = {
// M5
"Tg0U", "Tg0X", "Tg0d", "Tg0g", "Tg0j", "Tg1Y", "Tg1c", "Tg1g",
// M4
"Tg0G", "Tg0H", "Tg1U", "Tg1k", "Tg0K", "Tg0L", "Tg0e", "Tg0k",
// M2
"Tg0f", "Tg0j",
// M1
"Tg05", "Tg0D", "Tg0L", "Tg0T",
nullptr,
};
try {
Cpu::SMCConnection smc;
double sum = 0;
int count = 0;
for (size_t i = 0; keys_all[i] != nullptr; ++i) {
long long t = smc.getTempByKey(keys_all[i]);
if (t > 0 and t < 150) {
sum += static_cast<double>(t);
count++;
}
}
if (count > 0) {
return static_cast<long long>(round(sum / count));
}
} catch (...) {}
return -1;
}
// at the GPU temperature collection site
if (gpus_slice[0].supported_functions.temp_info and Config::getB("check_temp")) {
long long temp = get_gpu_temp_smc();
if (temp <= 0)
temp = get_gpu_temp_iohid();
if (temp > 0)
gpus_slice[0].temp.push_back(temp);
}The SMC key list draws from exelban/stats. M1/M2 are intentionally included so older chips that did not previously expose GPU MTR via IOHID also benefit. M3 is omitted only because I do not have one to test; if a maintainer adds those keys the pattern is identical.
Verification
Tested locally on M5:
- Before patch: GPU box shows
0 °Cregardless of workload. - After patch: GPU box shows
~33 °Cidle, climbs to~70 °Cunder sustained Metal compute (local LLM inference, GPU draw ~14 W). The reading tracks load reproducibly. - No change to CPU temperature path (still uses the existing IOHID
PMU tdiefallback). - No change to Intel Mac path (
sp78parser preserved as-is).
A complete unified diff against v1.4.7 (6e39144) is attached below and is ~75 lines including comments.
Full unified diff against v1.4.7 (commit 6e39144)Disclosure — AI-authored: this report, including the diagnostic narrative and the suggested patch, was drafted by an AI assistant (Claude Code) working interactively with the human submitter. The submitter tested the build on their own M5 hardware and confirmed correct behaviour before opening this issue. The full investigation trace — including how the
fltdata-type oversight was identified through empirical SMC probing — is available on request.
diff --git a/src/osx/btop_collect.cpp b/src/osx/btop_collect.cpp
index ed8a3d2..7bdb286 100644
--- a/src/osx/btop_collect.cpp
+++ b/src/osx/btop_collect.cpp
@@ -339,6 +339,42 @@ namespace Gpu {
return true;
}
+ //? Read GPU temperature via SMC using chip-specific sensor keys.
+ //? Apple Silicon exposes GPU thermal sensors as 4-character SMC keys.
+ //? Key sets change per chip generation. Lists below are from exelban/stats
+ //? (Modules/Sensors/values.swift), which is actively maintained per M-series chip.
+ static long long get_gpu_temp_smc() {
+ static const char* keys_all[] = {
+ // M5
+ "Tg0U", "Tg0X", "Tg0d", "Tg0g", "Tg0j", "Tg1Y", "Tg1c", "Tg1g",
+ // M4 (base + Pro/Max/Ultra)
+ "Tg0G", "Tg0H", "Tg1U", "Tg1k", "Tg0K", "Tg0L", "Tg0e", "Tg0k",
+ // M2
+ "Tg0f", "Tg0j",
+ // M1
+ "Tg05", "Tg0D", "Tg0L", "Tg0T",
+ nullptr,
+ };
+ try {
+ Cpu::SMCConnection smc;
+ double sum = 0;
+ int count = 0;
+ for (size_t i = 0; keys_all[i] != nullptr; ++i) {
+ long long t = smc.getTempByKey(keys_all[i]);
+ if (t > 0 and t < 150) {
+ sum += static_cast<double>(t);
+ count++;
+ }
+ }
+ if (count > 0) {
+ return static_cast<long long>(round(sum / count));
+ }
+ } catch (...) {
+ // SMC unavailable; fall through
+ }
+ return -1;
+ }
+
//? Read GPU temperature via IOHIDEventSystem thermal sensors
static long long get_gpu_temp_iohid() {
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504
@@ -546,9 +582,12 @@ namespace Gpu {
clamp(static_cast<long long>(round(static_cast<double>(gpus_slice[0].pwr_usage) * 100.0 / static_cast<double>(gpus_slice[0].pwr_max_usage))), 0ll, 100ll));
}
- //? GPU temperature
+ //? GPU temperature — try SMC first (chip-specific sensor keys; works on M5+
+ //? where IOHID exposes only generic die probes), then IOHID fallback.
if (gpus_slice[0].supported_functions.temp_info and Config::getB("check_temp")) {
- long long temp = get_gpu_temp_iohid();
+ long long temp = get_gpu_temp_smc();
+ if (temp <= 0)
+ temp = get_gpu_temp_iohid();
if (temp > 0)
gpus_slice[0].temp.push_back(temp);
}
diff --git a/src/osx/smc.cpp b/src/osx/smc.cpp
index cddf6de..94df964 100644
--- a/src/osx/smc.cpp
+++ b/src/osx/smc.cpp
@@ -73,18 +73,41 @@ namespace Cpu {
SMCVal_t val;
kern_return_t result;
result = SMCReadKey(key, &val);
- if (result == kIOReturnSuccess) {
- if (val.dataSize > 0) {
- if (strcmp(val.dataType, DATATYPE_SP78) == 0) {
- // convert sp78 value to temperature
- int intValue = val.bytes[0] * 256 + (unsigned char)val.bytes[1];
- return static_cast<long long>(intValue / 256.0);
- }
+ if (result != kIOReturnSuccess or val.dataSize == 0) return -1;
+
+ // sp78: 8-bit signed integer + 8-bit fraction (Intel Mac legacy)
+ if (strcmp(val.dataType, DATATYPE_SP78) == 0) {
+ int intValue = val.bytes[0] * 256 + (unsigned char)val.bytes[1];
+ return static_cast<long long>(intValue / 256.0);
+ }
+ // flt : 4-byte IEEE little-endian float (Apple Silicon GPU/temp sensors)
+ if (strcmp(val.dataType, "flt ") == 0 and val.dataSize >= 4) {
+ float f;
+ memcpy(&f, val.bytes, sizeof(f));
+ if (f > 0.0f and f < 150.0f) {
+ return static_cast<long long>(f + 0.5f);
}
}
+ // ui16 / fpe2: rare for temperature on Apple Silicon but seen historically
+ if (strcmp(val.dataType, "ui16") == 0 and val.dataSize >= 2) {
+ unsigned int u = ((unsigned char)val.bytes[0] << 8) | (unsigned char)val.bytes[1];
+ if (u > 0 and u < 150) return static_cast<long long>(u);
+ }
return -1;
}
+ // Read an arbitrary 4-char SMC key (e.g., "Tg0U" for M5 GPU sensor 1).
+ // Returns the temperature in °C as an integer, or -1 if the key is unsupported
+ // or the value is out of plausible range.
+ long long SMCConnection::getTempByKey(const char *key) {
+ char k[5];
+ strncpy(k, key, 4);
+ k[4] = '\0';
+ long long t = getSMCTemp(k);
+ if (t <= 0 or t >= 150) return -1;
+ return t;
+ }
+
// core means physical core in SMC, while in core map it's cpu threads :-/ Only an issue on hackintosh?
// this means we can only get the T per physical core
// another issue with the SMC API is that the key is always 4 chars -> what with systems with more than 9 physical cores?
diff --git a/src/osx/smc.hpp b/src/osx/smc.hpp
index 5717b64..a98ead7 100644
--- a/src/osx/smc.hpp
+++ b/src/osx/smc.hpp
@@ -102,6 +102,9 @@ namespace Cpu {
virtual ~SMCConnection();
long long getTemp(int core);
+ // Read an arbitrary 4-character SMC key as a temperature in °C.
+ // Returns -1 on failure or unsupported key.
+ long long getTempByKey(const char *key);
private:
kern_return_t SMCReadKey(UInt32Char_t key, SMCVal_t *val);Source: aristocratos/btop