#1653·btop

macOS: GPU temperature not displayed on Apple Silicon M5 (SMC `flt ` data type not parsed)

Author: necoli1822Created May 12, 2026Updated Sep 17, 2026

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:

  1. getSMCTemp only parses the sp78 data type. Apple Silicon GPU thermal sensors are exposed as flt (4-byte IEEE float) via SMC. The legacy sp78 parser drops these silently and returns -1, so every SMC GPU key returns -1 regardless of whether it exists on the chip.
  2. No SMC-based GPU temperature reader. The existing GPU temperature path in btop_collect.cpp::get_gpu_temp_iohid filters IOHID sensors by name ("GPU" substring or PMU 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 (commit 6e39144).
  • 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), and gas gauge battery entries. 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 existing getSMCTemp, every key returned -1. Probing the same keys after extending getSMCTemp to parse flt produced 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

cpp
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

cpp
// 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

cpp
// 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 °C regardless of workload.
  • After patch: GPU box shows ~33 °C idle, climbs to ~70 °C under 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 tdie fallback).
  • No change to Intel Mac path (sp78 parser preserved as-is).

A complete unified diff against v1.4.7 (6e39144) is attached below and is ~75 lines including comments.


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 flt data-type oversight was identified through empirical SMC probing — is available on request.

Full unified diff against v1.4.7 (commit 6e39144)
diff
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);