From 427199768b4bbe990c29f96f430f8965d4029698 Mon Sep 17 00:00:00 2001 From: JohnnySun Date: Wed, 24 Jun 2026 10:59:49 +0800 Subject: [PATCH 1/4] feat(gpu_fdinfo): read Intel iGPU power from RAPL --- README.md | 6 ++- src/gpu_fdinfo.cpp | 95 ++++++++++++++++++++++++++++++++++++++++++++++ src/gpu_fdinfo.h | 19 +++++++++- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ff8ad2a66a..0ef6274476 100644 --- a/README.md +++ b/README.md @@ -658,7 +658,7 @@ Example output: 🟢 🟢 🟢 - 🔴 + 🟢 🔴 @@ -698,7 +698,9 @@ Example output: - GPU usage and memory usage shows usage of current process, not total system usage (it's an issue on intel's side) - https://gitlab.freedesktop.org/drm/i915/kernel/-/issues/14153 - https://gitlab.freedesktop.org/drm/xe/kernel/-/issues/4861 -- Integrated Intel GPUs are **limited** due to lack of hwmon interface (it's an issue on intel's side, [i915 source](https://github.com/torvalds/linux/blob/5fc31936081919a8572a3d644f3fbb258038f337/drivers/gpu/drm/i915/i915_hwmon.c#L914-L916), [xe source](https://github.com/torvalds/linux/blob/5fc31936081919a8572a3d644f3fbb258038f337/drivers/gpu/drm/xe/xe_hwmon.c#L824-L826)) +- Integrated Intel GPU power uses the RAPL `uncore` powercap energy counter when + it is present and readable by the current user. +- Integrated Intel GPUs are otherwise **limited** due to lack of hwmon interface (it's an issue on intel's side, [i915 source](https://github.com/torvalds/linux/blob/5fc31936081919a8572a3d644f3fbb258038f337/drivers/gpu/drm/i915/i915_hwmon.c#L914-L916), [xe source](https://github.com/torvalds/linux/blob/5fc31936081919a8572a3d644f3fbb258038f337/drivers/gpu/drm/xe/xe_hwmon.c#L824-L826)) #### Panfrost and Panthor notes - GPU usage requires `echo N | sudo tee /sys/class/drm/renderD*/device/profiling` diff --git a/src/gpu_fdinfo.cpp b/src/gpu_fdinfo.cpp index 0fdd99e836..d5e0297257 100644 --- a/src/gpu_fdinfo.cpp +++ b/src/gpu_fdinfo.cpp @@ -1,4 +1,5 @@ #include "gpu_fdinfo.h" +#include "file_utils.h" #ifndef TEST_ONLY #include "hud_elements.h" @@ -303,8 +304,102 @@ void GPU_fdinfo::get_current_hwmon_readings() } } +bool GPU_fdinfo::has_hwmon_sensor(const std::string& key) const +{ + auto sensor = hwmon_sensors.find(key); + return sensor != hwmon_sensors.end() && !sensor->second.filename.empty(); +} + +void GPU_fdinfo::find_intel_gpu_rapl_power() +{ + std::string powercap = "/sys/class/powercap/"; + auto powercap_dirs = ls(powercap.c_str()); + + for (auto& dir : powercap_dirs) { + auto path = powercap + dir; + auto name = read_line(path + "/name"); + + if (name != "uncore") + continue; + + auto energy_path = path + "/energy_uj"; + auto energy_stream = std::ifstream(energy_path); + + if (!energy_stream.good()) { + SPDLOG_DEBUG( + "powercap: Intel GPU RAPL energy is not accessible at {}", + energy_path + ); + continue; + } + + intel_gpu_rapl_energy_stream = std::move(energy_stream); + + try { + auto max_energy_range = read_line(path + "/max_energy_range_uj"); + if (!max_energy_range.empty()) + intel_gpu_rapl_max_energy_range_uj = std::stoull(max_energy_range); + } catch (...) { + intel_gpu_rapl_max_energy_range_uj = 0; + } + + SPDLOG_DEBUG("powercap: using Intel GPU RAPL energy at {}", energy_path); + return; + } +} + +bool GPU_fdinfo::read_intel_gpu_rapl_energy(uint64_t& energy_uj) +{ + if (!intel_gpu_rapl_energy_stream.is_open()) + return false; + + intel_gpu_rapl_energy_stream.clear(); + intel_gpu_rapl_energy_stream.seekg(0); + + std::string energy; + std::getline(intel_gpu_rapl_energy_stream, energy); + + if (energy.empty()) + return false; + + try { + energy_uj = std::stoull(energy); + } catch (...) { + return false; + } + + return true; +} + +float GPU_fdinfo::get_intel_gpu_rapl_power_usage() +{ + uint64_t now = 0; + if (!read_intel_gpu_rapl_energy(now)) + return 0.f; + + if (!intel_gpu_rapl_energy_initialized) { + intel_gpu_rapl_last_energy_uj = now; + intel_gpu_rapl_energy_initialized = true; + return 0.f; + } + + uint64_t delta = 0; + if (now >= intel_gpu_rapl_last_energy_uj) { + delta = now - intel_gpu_rapl_last_energy_uj; + } else if (intel_gpu_rapl_max_energy_range_uj > intel_gpu_rapl_last_energy_uj) { + delta = intel_gpu_rapl_max_energy_range_uj - intel_gpu_rapl_last_energy_uj + now; + } + + intel_gpu_rapl_last_energy_uj = now; + + return (static_cast(delta) / (METRICS_UPDATE_PERIOD_MS / 1000.f)) / 1'000'000; +} + float GPU_fdinfo::get_power_usage() { + if (intel_gpu_rapl_energy_stream.is_open()) + return get_intel_gpu_rapl_power_usage(); + if (!hwmon_sensors["power"].filename.empty()) return static_cast(hwmon_sensors["power"].val) / 1'000'000; diff --git a/src/gpu_fdinfo.h b/src/gpu_fdinfo.h index 97e62e5265..7fa747f8d7 100644 --- a/src/gpu_fdinfo.h +++ b/src/gpu_fdinfo.h @@ -86,10 +86,19 @@ class GPU_fdinfo { void find_hwmon_sensors(); std::string find_hwmon_dir(); std::string find_hwmon_sensor_dir(std::string name); + bool has_hwmon_sensor(const std::string& key) const; void get_current_hwmon_readings(); float get_power_usage(); float last_power = 0; + bool intel_gpu_integrated = false; + std::ifstream intel_gpu_rapl_energy_stream; + uint64_t intel_gpu_rapl_last_energy_uj = 0; + uint64_t intel_gpu_rapl_max_energy_range_uj = 0; + bool intel_gpu_rapl_energy_initialized = false; + void find_intel_gpu_rapl_power(); + bool read_intel_gpu_rapl_energy(uint64_t& energy_uj); + float get_intel_gpu_rapl_power_usage(); std::ifstream gpu_clock_stream; void find_i915_gt_dir(); @@ -161,10 +170,13 @@ class GPU_fdinfo { { auto old_type = drm_memory_type; - if (module == "i915") + if (module == "i915") { drm_memory_type = "drm-resident-system0"; - else if (module == "xe") + intel_gpu_integrated = fdinfo_data[0].find(drm_memory_type) != fdinfo_data[0].end(); + } else if (module == "xe") { drm_memory_type = "drm-resident-gtt"; + intel_gpu_integrated = fdinfo_data[0].find(drm_memory_type) != fdinfo_data[0].end(); + } SPDLOG_DEBUG( "\"{}\" is not found, you probably have an integrated GPU. " @@ -208,6 +220,9 @@ class GPU_fdinfo { find_hwmon_sensors(); + if (intel_gpu_integrated && !has_hwmon_sensor("energy") && !has_hwmon_sensor("power")) + find_intel_gpu_rapl_power(); + if (module == "i915") find_i915_gt_dir(); else if (module == "xe") From 7d9c04b9ee38bbe0266a06c4dee0f496c2c07396 Mon Sep 17 00:00:00 2001 From: JohnnySun Date: Wed, 24 Jun 2026 13:24:43 +0800 Subject: [PATCH 2/4] fix(mangoapp): declare OpenGL symbols used by app --- src/app/main.cpp | 1 + src/gl/gl.h | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/app/main.cpp b/src/app/main.cpp index 19c465fbea..a2b318dcda 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" diff --git a/src/gl/gl.h b/src/gl/gl.h index 76a621230c..4df7190f6a 100644 --- a/src/gl/gl.h +++ b/src/gl/gl.h @@ -2,10 +2,30 @@ #ifndef MANGOHUD_GL_GL_H #define MANGOHUD_GL_GL_H +#include + +typedef unsigned int GLenum; +typedef unsigned int GLbitfield; +typedef unsigned char GLubyte; +typedef float GLfloat; + +#define GL_DEPTH_TEST 0x0B71 +#define GL_BLEND 0x0BE2 +#define GL_SRC_ALPHA 0x0302 +#define GL_ONE_MINUS_SRC_ALPHA 0x0303 +#define GL_COLOR_BUFFER_BIT 0x00004000 +#define GL_RENDERER 0x1F01 + #ifdef __cplusplus extern "C" { #endif //__cplusplus +const GLubyte* glGetString(GLenum name); +void glEnable(GLenum cap); +void glBlendFunc(GLenum sfactor, GLenum dfactor); +void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); +void glClear(GLbitfield mask); + void * glXCreateContext(void *, void *, void *, int); void glXDestroyContext(void *, void*); void glXSwapBuffers(void*, void*); From d6846ec128d866b5f0a038beaa11b45c40f301e0 Mon Sep 17 00:00:00 2001 From: JohnnySun Date: Thu, 25 Jun 2026 00:22:08 +0800 Subject: [PATCH 3/4] feat(gpu_fdinfo): support Intel integrated GPU memory Use i915 system0 and xe gtt fdinfo memory when Intel integrated GPUs do not expose local or VRAM memory keys, so SteamOS mangoapp presets can display current-process shared GPU memory in the vram row. Treat missing fdinfo hwmon metrics as unavailable instead of rendering zero-valued temperature, power, fan, voltage, or memory clock readings. --- README.md | 5 +++ src/gpu.h | 7 ++++ src/gpu_fdinfo.cpp | 89 ++++++++++++++++++++++++++++++++++-------- src/gpu_fdinfo.h | 26 ++++-------- src/gpu_metrics_util.h | 22 +++++------ src/hud_elements.cpp | 54 ++++++++++++++++++------- 6 files changed, 144 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 0ef6274476..b8d668297d 100644 --- a/README.md +++ b/README.md @@ -698,6 +698,11 @@ Example output: - GPU usage and memory usage shows usage of current process, not total system usage (it's an issue on intel's side) - https://gitlab.freedesktop.org/drm/i915/kernel/-/issues/14153 - https://gitlab.freedesktop.org/drm/xe/kernel/-/issues/4861 +- For integrated Intel GPUs, the `vram` overlay row uses current-process + fdinfo shared memory (`drm-resident-system0` on `i915`, + `drm-resident-gtt` on `xe`) when local or VRAM fdinfo memory is not + available. This keeps SteamOS `mangoapp` presets useful without claiming + total system VRAM support. - Integrated Intel GPU power uses the RAPL `uncore` powercap energy counter when it is present and readable by the current user. - Integrated Intel GPUs are otherwise **limited** due to lack of hwmon interface (it's an issue on intel's side, [i915 source](https://github.com/torvalds/linux/blob/5fc31936081919a8572a3d644f3fbb258038f337/drivers/gpu/drm/i915/i915_hwmon.c#L914-L916), [xe source](https://github.com/torvalds/linux/blob/5fc31936081919a8572a3d644f3fbb258038f337/drivers/gpu/drm/xe/xe_hwmon.c#L824-L826)) diff --git a/src/gpu.h b/src/gpu.h index 675d64f8a9..4480930601 100644 --- a/src/gpu.h +++ b/src/gpu.h @@ -100,6 +100,13 @@ class GPU { return false; } + bool uses_integrated_memory() const { + if (fdinfo) + return fdinfo->uses_integrated_memory(); + else + return false; + } + std::shared_ptr throttling() { if (nvidia) return nvidia->throttling; diff --git a/src/gpu_fdinfo.cpp b/src/gpu_fdinfo.cpp index d5e0297257..4e59448460 100644 --- a/src/gpu_fdinfo.cpp +++ b/src/gpu_fdinfo.cpp @@ -110,6 +110,39 @@ void GPU_fdinfo::gather_fdinfo_data() { } } +bool GPU_fdinfo::fdinfo_data_has_key(const std::string& key) const +{ + for (const auto& fd : fdinfo_data) { + if (fd.find(key) != fd.end()) + return true; + } + + return false; +} + +void GPU_fdinfo::update_intel_memory_type() +{ + if (module != "i915" && module != "xe") + return; + + if (fdinfo_data.empty() || fdinfo_data_has_key(drm_memory_type)) + return; + + const auto old_type = drm_memory_type; + const auto integrated_type = module == "i915" ? "drm-resident-system0" : "drm-resident-gtt"; + + if (!fdinfo_data_has_key(integrated_type)) + return; + + drm_memory_type = integrated_type; + intel_gpu_integrated = true; + + SPDLOG_DEBUG( + "\"{}\" is not found, you probably have an integrated GPU. " + "Using \"{}\"", old_type, drm_memory_type + ); +} + uint64_t GPU_fdinfo::get_gpu_time() { uint64_t total = 0; @@ -307,7 +340,7 @@ void GPU_fdinfo::get_current_hwmon_readings() bool GPU_fdinfo::has_hwmon_sensor(const std::string& key) const { auto sensor = hwmon_sensors.find(key); - return sensor != hwmon_sensors.end() && !sensor->second.filename.empty(); + return sensor != hwmon_sensors.end() && sensor->second.stream.is_open(); } void GPU_fdinfo::find_intel_gpu_rapl_power() @@ -400,9 +433,12 @@ float GPU_fdinfo::get_power_usage() if (intel_gpu_rapl_energy_stream.is_open()) return get_intel_gpu_rapl_power_usage(); - if (!hwmon_sensors["power"].filename.empty()) + if (has_hwmon_sensor("power")) return static_cast(hwmon_sensors["power"].val) / 1'000'000; + if (!has_hwmon_sensor("energy")) + return -1.0f; + float now = hwmon_sensors["energy"].val; // Initialize value for the first time, otherwise delta will be very large @@ -635,7 +671,7 @@ int GPU_fdinfo::get_gpu_clock() return get_gpu_clock_mali(); if (!gpu_clock_stream.is_open()) - return 0; + return -1; std::string clock_str; @@ -644,14 +680,14 @@ int GPU_fdinfo::get_gpu_clock() std::getline(gpu_clock_stream, clock_str); if (clock_str.empty()) - return 0; + return -1; return std::stoi(clock_str); } int GPU_fdinfo::get_gpu_clock_mali() { if (fdinfo_data.empty()) - return 0; + return -1; std::string key; @@ -663,7 +699,7 @@ int GPU_fdinfo::get_gpu_clock_mali() { std::string freq_str = fdinfo_data[0][key]; if (freq_str.empty()) - return 0; + return -1; float freq = std::stoull(freq_str) / 1'000'000; @@ -783,7 +819,7 @@ int GPU_fdinfo::get_kgsl_temp() { std::ifstream* s = &kgsl_streams["temp"]; if (!s->is_open()) - return 0; + return -1; std::string temp_str; @@ -792,7 +828,7 @@ int GPU_fdinfo::get_kgsl_temp() { std::getline(*s, temp_str); if (temp_str.empty()) - return 0; + return -1; return std::round(std::stoi(temp_str) / 1'000.f); } @@ -821,26 +857,47 @@ void GPU_fdinfo::main_thread() } gather_fdinfo_data(); + update_intel_memory_type(); get_current_hwmon_readings(); metrics.load = get_gpu_load(); - metrics.proc_vram_used = get_memory_used(); + const float memory_used = get_memory_used(); + metrics.proc_vram_used = memory_used; + metrics.gtt_used = 0.0f; + if (intel_gpu_integrated) + metrics.gtt_used = memory_used; metrics.powerUsage = get_power_usage(); - metrics.powerLimit = static_cast(hwmon_sensors["power_limit"].val) / 1'000'000; + if (has_hwmon_sensor("power_limit")) + metrics.powerLimit = static_cast(hwmon_sensors["power_limit"].val) / 1'000'000; + else + metrics.powerLimit = -1.0f; metrics.CoreClock = get_gpu_clock(); - metrics.voltage = hwmon_sensors["voltage"].val; + if (has_hwmon_sensor("voltage")) + metrics.voltage = hwmon_sensors["voltage"].val; + else + metrics.voltage = -1; if (module == "msm_drm") metrics.temp = get_kgsl_temp(); - else + else if (has_hwmon_sensor("temp")) metrics.temp = hwmon_sensors["temp"].val / 1000.f; + else + metrics.temp = -1; - metrics.memory_temp = hwmon_sensors["vram_temp"].val / 1000.f; - - metrics.fan_speed = hwmon_sensors["fan_speed"].val; - metrics.fan_rpm = true; // Fan data is pulled from hwmon + if (has_hwmon_sensor("vram_temp")) + metrics.memory_temp = hwmon_sensors["vram_temp"].val / 1000.f; + else + metrics.memory_temp = -1; + + if (has_hwmon_sensor("fan_speed")) { + metrics.fan_speed = hwmon_sensors["fan_speed"].val; + metrics.fan_rpm = true; // Fan data is pulled from hwmon + } else { + metrics.fan_speed = -1; + metrics.fan_rpm = false; + } int throttling = get_throttling_status(); metrics.is_power_throttled = throttling & GPU_throttle_status::POWER; diff --git a/src/gpu_fdinfo.h b/src/gpu_fdinfo.h index 7fa747f8d7..957f9e0380 100644 --- a/src/gpu_fdinfo.h +++ b/src/gpu_fdinfo.h @@ -66,6 +66,8 @@ class GPU_fdinfo { std::vector> fdinfo_data; void gather_fdinfo_data(); + bool fdinfo_data_has_key(const std::string& key) const; + void update_intel_memory_type(); void main_thread(); @@ -165,24 +167,7 @@ class GPU_fdinfo { drm_memory_type = "drm-resident-memory"; } - if (fdinfo_data.size() > 0 && - fdinfo_data[0].find(drm_memory_type) == fdinfo_data[0].end()) - { - auto old_type = drm_memory_type; - - if (module == "i915") { - drm_memory_type = "drm-resident-system0"; - intel_gpu_integrated = fdinfo_data[0].find(drm_memory_type) != fdinfo_data[0].end(); - } else if (module == "xe") { - drm_memory_type = "drm-resident-gtt"; - intel_gpu_integrated = fdinfo_data[0].find(drm_memory_type) != fdinfo_data[0].end(); - } - - SPDLOG_DEBUG( - "\"{}\" is not found, you probably have an integrated GPU. " - "Using \"{}\"", old_type, drm_memory_type - ); - } + update_intel_memory_type(); SPDLOG_DEBUG( "drm_engine_type = {}, drm_memory_type = {}", @@ -257,4 +242,9 @@ class GPU_fdinfo { } float amdgpu_helper_get_proc_vram(); + + bool uses_integrated_memory() const + { + return intel_gpu_integrated; + } }; diff --git a/src/gpu_metrics_util.h b/src/gpu_metrics_util.h index a36411d73f..0b87a92bf6 100644 --- a/src/gpu_metrics_util.h +++ b/src/gpu_metrics_util.h @@ -3,16 +3,16 @@ struct gpu_metrics { int load; - int temp; + int temp {-1}; int junction_temp {-1}; int memory_temp {-1}; float sys_vram_used; float proc_vram_used; float memoryTotal; - int MemClock; - int CoreClock; - float powerUsage; - float powerLimit; + int MemClock {-1}; + int CoreClock {-1}; + float powerUsage {-1.0f}; + float powerLimit {-1.0f}; float apu_cpu_power; int apu_cpu_temp; bool is_power_throttled; @@ -20,17 +20,17 @@ struct gpu_metrics { bool is_temp_throttled; bool is_other_throttled; float gtt_used; - int fan_speed; - int voltage; + int fan_speed {-1}; + int voltage {-1}; bool fan_rpm; gpu_metrics() - : load(0), temp(0), junction_temp(0), memory_temp(0), - sys_vram_used(0.0f), proc_vram_used(0.0f), memoryTotal(0.0f), MemClock(0), CoreClock(0), - powerUsage(0.0f), powerLimit(0.0f), apu_cpu_power(0.0f), apu_cpu_temp(0), + : load(0), temp(-1), junction_temp(-1), memory_temp(-1), + sys_vram_used(0.0f), proc_vram_used(0.0f), memoryTotal(0.0f), MemClock(-1), CoreClock(-1), + powerUsage(-1.0f), powerLimit(-1.0f), apu_cpu_power(0.0f), apu_cpu_temp(0), is_power_throttled(false), is_current_throttled(false), is_temp_throttled(false), is_other_throttled(false), - gtt_used(0.0f), fan_speed(0), voltage(0), fan_rpm(false) {} + gtt_used(0.0f), fan_speed(-1), voltage(-1), fan_rpm(false) {} }; #define METRICS_UPDATE_PERIOD_MS 500 diff --git a/src/hud_elements.cpp b/src/hud_elements.cpp index 4dbf838acc..7268dd8fc6 100644 --- a/src/hud_elements.cpp +++ b/src/hud_elements.cpp @@ -311,7 +311,10 @@ void HudElements::gpu_stats(){ // ImGui::Text("%s", "%"); } - if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_temp]){ + if ( + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_temp] && + gpu->metrics.temp > -1 + ){ ImguiNextColumnOrNewRow(); if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_temp_fahrenheit]) right_aligned_text(text_color, HUDElements.ralign_width, "%i", HUDElements.convert_to_fahrenheit(gpu->metrics.temp)); @@ -344,7 +347,11 @@ void HudElements::gpu_stats(){ ImGui::PopFont(); } - if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_fan] && !gpu->is_apu()){ + if ( + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_fan] && + gpu->metrics.fan_speed > -1 && + !gpu->is_apu() + ){ ImguiNextColumnOrNewRow(); right_aligned_text(text_color, HUDElements.ralign_width, "%i", gpu->metrics.fan_speed); ImGui::SameLine(0, 1.0f); @@ -360,7 +367,10 @@ void HudElements::gpu_stats(){ ImGui::PopFont(); } - if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_core_clock]){ + if ( + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_core_clock] && + gpu->metrics.CoreClock > -1 + ){ ImguiNextColumnOrNewRow(); right_aligned_text(text_color, HUDElements.ralign_width, "%i", gpu->metrics.CoreClock); ImGui::SameLine(0, 1.0f); @@ -369,7 +379,10 @@ void HudElements::gpu_stats(){ ImGui::PopFont(); } - if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_power]) { + if ( + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_power] && + gpu->metrics.powerUsage > -1 + ) { ImguiNextColumnOrNewRow(); char str[16]; snprintf(str, sizeof(str), "%.1f", gpu->metrics.powerUsage); @@ -379,14 +392,20 @@ void HudElements::gpu_stats(){ right_aligned_text(text_color, HUDElements.ralign_width, "%.1f", gpu->metrics.powerUsage); ImGui::SameLine(0, 1.0f); ImGui::PushFont(HUDElements.sw_stats->font_small); - if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_power_limit]) + if ( + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_power_limit] && + gpu->metrics.powerLimit > 0 + ) HUDElements.TextColored(HUDElements.colors.text, "/%.0fW", gpu->metrics.powerLimit); else HUDElements.TextColored(HUDElements.colors.text, "W"); ImGui::PopFont(); } - if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_efficiency]) { + if ( + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_efficiency] && + gpu->metrics.powerUsage > 0 + ) { ImguiNextColumnOrNewRow(); float efficiency; const char* efficiency_unit; @@ -404,7 +423,10 @@ void HudElements::gpu_stats(){ ImGui::PopFont(); } - if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_voltage]) { + if ( + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_voltage] && + gpu->metrics.voltage > -1 + ) { ImguiNextColumnOrNewRow(); right_aligned_text(text_color, HUDElements.ralign_width, "%i", gpu->metrics.voltage); ImGui::SameLine(0, 1.0f); @@ -647,11 +669,11 @@ void HudElements::vram(){ HUDElements.TextColored(HUDElements.colors.vram, gpu->vram_text().c_str()); ImguiNextColumnOrNewRow(); - // Add gtt_used to vram usage for APUs - if (gpu->is_apu()) - right_aligned_text(HUDElements.colors.text, HUDElements.ralign_width, "%.1f", gpu->metrics.sys_vram_used + gpu->metrics.gtt_used); - else - right_aligned_text(HUDElements.colors.text, HUDElements.ralign_width, "%.1f", gpu->metrics.sys_vram_used); + // Add shared memory usage for APUs and Intel integrated GPUs. + float vram_used = gpu->metrics.sys_vram_used; + if (gpu->is_apu() || gpu->uses_integrated_memory()) + vram_used += gpu->metrics.gtt_used; + right_aligned_text(HUDElements.colors.text, HUDElements.ralign_width, "%.1f", vram_used); if (!HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_hud_compact]){ ImGui::SameLine(0,1.0f); ImGui::PushFont(HUDElements.sw_stats->font_small); @@ -672,7 +694,10 @@ void HudElements::vram(){ HUDElements.TextColored(HUDElements.colors.text, "°C"); } - if (HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_mem_clock]){ + if ( + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_mem_clock] && + gpu->metrics.MemClock > 0 + ){ ImguiNextColumnOrNewRow(); right_aligned_text(HUDElements.colors.text, HUDElements.ralign_width, "%i", gpu->metrics.MemClock); ImGui::SameLine(0, 1.0f); @@ -723,7 +748,8 @@ void HudElements::proc_vram() { // show only if vram is not enabled if ( !HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_vram] && - HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_mem_temp] + HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_gpu_mem_temp] && + gpu->metrics.memory_temp > -1 ) { ImguiNextColumnOrNewRow(); From 4ca735ac289ce2ae5b601868115b9494ff9deeac Mon Sep 17 00:00:00 2001 From: JohnnySun Date: Tue, 7 Jul 2026 03:38:58 +0800 Subject: [PATCH 4/4] mangoapp: add optional frame-feed JSON exporter When MANGOAPP_FRAME_FEED=1, export a rolling 2s frame summary (avg fps, p95 frame ms, spike count/worst) as atomic JSON at ~2Hz to $XDG_RUNTIME_DIR/steamos-intel-handheld/frame-feed.json (path override: MANGOAPP_FRAME_FEED_FILE) for the steamos-intel-handheld game power governor. Hooked on the gamescope msg frame path outside the overlay display guard, so the feed flows with the overlay hidden. Zero overhead when the env var is unset; I/O errors disable the feed for the session. Validated live on the MSI Claw 8 AI+ (2Hz cadence, sane values, daemon consumed it as a high-confidence frame source). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AJSepardSVWMwEuwBi9WTZ --- src/app/frame_feed.cpp | 287 +++++++++++++++++++++++++++++++++++++++++ src/app/frame_feed.h | 20 +++ src/app/main.cpp | 8 ++ src/meson.build | 1 + 4 files changed, 316 insertions(+) create mode 100644 src/app/frame_feed.cpp create mode 100644 src/app/frame_feed.h diff --git a/src/app/frame_feed.cpp b/src/app/frame_feed.cpp new file mode 100644 index 0000000000..37ad971184 --- /dev/null +++ b/src/app/frame_feed.cpp @@ -0,0 +1,287 @@ +// Frame-feed exporter for the steamos-intel-handheld game-power daemon. +// See frame_feed.h and docs/game-power-v10-framework-plan.md (contract 1.1). +// +// Design notes: +// * Zero cost when MANGOAPP_FRAME_FEED != "1": the first statement is a +// cached state check. +// * Single-threaded: only mangoapp's msgrcv loop calls +// frame_feed_record_frame(), so the rolling window needs no locking. +// * No timer thread: the 2 Hz cadence is driven off the frame callbacks +// themselves (write on the first frame >= 500 ms after the last write). +// * Fail-closed: any I/O error logs once to stderr and disables the feed +// for the rest of the session. No exception escapes into the caller. + +#include "frame_feed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +// Rolling window bounds (contract 1.1: last 2.0 s or last N=256 frames, +// whichever is smaller). +constexpr double kWindowSeconds = 2.0; +constexpr size_t kMaxFrames = 256; + +// Minimum spacing between file writes (~2 Hz). +constexpr double kWriteIntervalSeconds = 0.5; + +// A frame that exceeds this multiple of the window median counts as a spike. +constexpr double kSpikeRatio = 1.5; + +// Guard against absurd frame times poisoning the window (matches the spirit +// of the overlay's own >100 s guard; here 10 s in ms). +constexpr float kMaxSaneFrameMs = 10000.0f; + +enum class FeedState { kUninitialized, kEnabled, kDisabled }; + +struct Sample { + double t_s; // CLOCK_MONOTONIC timestamp of the frame + float ms; // visible frame time in milliseconds +}; + +FeedState g_state = FeedState::kUninitialized; +std::string g_path; // final target path +std::string g_tmp_path; // temp path in the same directory +double g_last_write_s = 0.0; +std::deque g_window; + +double monotonic_seconds() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) + + static_cast(ts.tv_nsec) * 1e-9; +} + +void disable_with_error(const char* what) { + // Logged exactly once (state flips to kDisabled here). + fprintf(stderr, + "mangoapp frame-feed: disabled for this session (%s: %s)\n", + what, strerror(errno)); + g_state = FeedState::kDisabled; +} + +// Create every missing component of the directory that holds the feed file, +// each with mode 0700. Returns false only on a real error (EEXIST is fine). +bool ensure_parent_dir(const std::string& file_path) { + std::string::size_type slash = file_path.find_last_of('/'); + if (slash == std::string::npos || slash == 0) + return true; // relative name or root-level file: nothing to make + std::string dir = file_path.substr(0, slash); + + // Walk the path creating each component. + for (std::string::size_type i = 1; i <= dir.size(); ++i) { + if (i == dir.size() || dir[i] == '/') { + std::string component = dir.substr(0, i); + if (mkdir(component.c_str(), 0700) != 0 && errno != EEXIST) + return false; + } + } + return true; +} + +// Resolve the target path and prepare the directory. Returns false to leave +// the feed disabled. Returns silently (no log) when the env var is unset; +// logs once on a genuine setup failure. +bool feed_init() { + const char* enable = getenv("MANGOAPP_FRAME_FEED"); + if (!enable || strcmp(enable, "1") != 0) + return false; // feature off: stay silent + + const char* override_path = getenv("MANGOAPP_FRAME_FEED_FILE"); + if (override_path && override_path[0] != '\0') { + g_path = override_path; + } else { + const char* runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (!runtime_dir || runtime_dir[0] == '\0') { + fprintf(stderr, + "mangoapp frame-feed: disabled (MANGOAPP_FRAME_FEED=1 but " + "XDG_RUNTIME_DIR is unset and MANGOAPP_FRAME_FEED_FILE was " + "not provided)\n"); + return false; + } + g_path = std::string(runtime_dir) + + "/steamos-intel-handheld/frame-feed.json"; + } + + if (!ensure_parent_dir(g_path)) { + disable_with_error("mkdir"); + // disable_with_error already set kDisabled; return false so the caller + // does not flip us back to kEnabled. + return false; + } + + // Temp file lives in the same directory (same filesystem) so rename() is + // atomic. Namespace it by pid to avoid collisions between processes. + g_tmp_path = g_path + "." + std::to_string(static_cast(getpid())) + + ".tmp"; + return true; +} + +void trim_window(double now_s) { + const double cutoff = now_s - kWindowSeconds; + while (!g_window.empty() && + (g_window.front().t_s < cutoff || g_window.size() > kMaxFrames)) { + g_window.pop_front(); + } +} + +// Atomically replace g_path with `json` via temp-file + rename. Returns +// false on any I/O error (caller disables the feed). +bool write_atomic(const std::string& json) { + int fd = open(g_tmp_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) + return false; + + const char* p = json.data(); + size_t remaining = json.size(); + while (remaining > 0) { + ssize_t w = write(fd, p, remaining); + if (w < 0) { + if (errno == EINTR) + continue; + close(fd); + unlink(g_tmp_path.c_str()); + return false; + } + p += w; + remaining -= static_cast(w); + } + close(fd); + + if (rename(g_tmp_path.c_str(), g_path.c_str()) != 0) { + unlink(g_tmp_path.c_str()); + return false; + } + return true; +} + +// Compute stats over the current window and, if valid, write them. Returns +// false only on an I/O error that should disable the feed. +bool compute_and_write(double now_s, uint32_t app_pid) { + const size_t n = g_window.size(); + if (n == 0) + return true; // nothing to report this interval; not an error + + std::vector sorted; + sorted.reserve(n); + double sum_ms = 0.0; + float worst_ms = 0.0f; + for (const Sample& s : g_window) { + sorted.push_back(s.ms); + sum_ms += s.ms; + if (s.ms > worst_ms) + worst_ms = s.ms; + } + std::sort(sorted.begin(), sorted.end()); + + const float last_frame_ms = g_window.back().ms; + + // p95 frame time: value at the 95th percentile (ascending, high = slow). + size_t p95_idx = static_cast(std::lround(0.95 * (n - 1))); + if (p95_idx >= n) + p95_idx = n - 1; + const float p95_frame_ms = sorted[p95_idx]; + + // Median for the spike threshold. + const float median_ms = + (n % 2 == 1) ? sorted[n / 2] + : 0.5f * (sorted[n / 2 - 1] + sorted[n / 2]); + + const double mean_ms = sum_ms / static_cast(n); + const double avg_fps = (mean_ms > 0.0) ? (1000.0 / mean_ms) : 0.0; + + // Skip this interval if the core metrics are not finite positive. + if (!std::isfinite(avg_fps) || avg_fps <= 0.0 || + !std::isfinite(p95_frame_ms) || p95_frame_ms <= 0.0f) + return true; + + // Spike count: frames exceeding kSpikeRatio * median. + unsigned spike_count = 0; + if (median_ms > 0.0f) { + const float threshold = kSpikeRatio * median_ms; + for (const Sample& s : g_window) + if (s.ms > threshold) + ++spike_count; + } + + // Actual span covered by the window (target 2.0 s). + const double window_s = g_window.back().t_s - g_window.front().t_s; + + char buf[512]; + int len = snprintf( + buf, sizeof(buf), + "{" + "\"schema\":\"steamos-intel-handheld-frame-feed-v1\"," + "\"pid\":%ld," + "\"updated_monotonic_s\":%.3f," + "\"window_s\":%.3f," + "\"frame_count\":%zu," + "\"avg_fps\":%.2f," + "\"p95_frame_ms\":%.2f," + "\"last_frame_ms\":%.2f," + "\"spike\":{\"count\":%u,\"worst_ms\":%.2f}" + "}\n", + static_cast(app_pid != 0 ? app_pid + : static_cast(getpid())), + now_s, window_s, n, avg_fps, static_cast(p95_frame_ms), + static_cast(last_frame_ms), spike_count, + static_cast(worst_ms)); + if (len < 0 || len >= static_cast(sizeof(buf))) + return true; // formatting glitch; skip this interval, keep the feed + + return write_atomic(std::string(buf, static_cast(len))); +} + +} // namespace + +void frame_feed_record_frame(uint64_t visible_frametime_ns, uint32_t app_pid) { + if (g_state == FeedState::kDisabled) + return; + + try { + if (g_state == FeedState::kUninitialized) { + if (!feed_init()) { + // feed_init leaves g_state as kUninitialized when the feature + // is simply off, or kDisabled after logging a real error. + if (g_state != FeedState::kDisabled) + g_state = FeedState::kDisabled; // off: never retry + return; + } + g_state = FeedState::kEnabled; + } + + const float frame_ms = static_cast(visible_frametime_ns) / 1e6f; + if (!(frame_ms > 0.0f) || frame_ms > kMaxSaneFrameMs) + return; // drop non-positive / absurd frames, keep the feed live + + const double now_s = monotonic_seconds(); + g_window.push_back(Sample{now_s, frame_ms}); + trim_window(now_s); + + if (now_s - g_last_write_s < kWriteIntervalSeconds) + return; + g_last_write_s = now_s; + + if (!compute_and_write(now_s, app_pid)) + disable_with_error("write"); + } catch (...) { + // Nothing may propagate into the render/message path. + fprintf(stderr, + "mangoapp frame-feed: disabled after unexpected exception\n"); + g_state = FeedState::kDisabled; + } +} diff --git a/src/app/frame_feed.h b/src/app/frame_feed.h new file mode 100644 index 0000000000..314b299ab8 --- /dev/null +++ b/src/app/frame_feed.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +// Frame-feed exporter for the steamos-intel-handheld game-power daemon. +// +// Entirely guarded by the env var MANGOAPP_FRAME_FEED=1 (read once and +// cached). When the feed is disabled the call is a single cached-state +// comparison and returns immediately: no allocation, no I/O, no overhead. +// +// When enabled it maintains a rolling window of the most recent frame +// times and, at ~2 Hz, atomically writes a compact JSON summary (see +// docs/game-power-v10-framework-plan.md contract 1.1) to +// $XDG_RUNTIME_DIR/steamos-intel-handheld/frame-feed.json (overridable via +// MANGOAPP_FRAME_FEED_FILE). +// +// Must be called once per visible game frame from the mangoapp message +// read path. It is single-threaded by contract (only the msgrcv loop calls +// it) and never throws into the caller. +void frame_feed_record_frame(uint64_t visible_frametime_ns, uint32_t app_pid); diff --git a/src/app/main.cpp b/src/app/main.cpp index a2b318dcda..43986b102a 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -18,6 +18,7 @@ #include "notify.h" #include "mangoapp.h" #include "mangoapp_proto.h" +#include "frame_feed.h" #include #ifdef __linux__ #include "implot.h" @@ -207,6 +208,13 @@ static void msg_read_thread(){ should_new_frame = true; } + // Export a rolling frame summary for the steamos-intel-handheld + // game-power daemon. No-op unless MANGOAPP_FRAME_FEED=1. Runs + // regardless of overlay visibility so the daemon still gets frame + // data when the HUD is hidden. + if (mangoapp_v1->visible_frametime_ns != ~(0lu)) + frame_feed_record_frame(mangoapp_v1->visible_frametime_ns, mangoapp_v1->pid); + if (msg_size > offsetof(mangoapp_msg_v1, fsrUpscale)){ HUDElements.g_fsrUpscale = mangoapp_v1->fsrUpscale; if (real_params->fsr_steam_sharpness < 0) diff --git a/src/meson.build b/src/meson.build index d7e4b362d0..74800c4021 100644 --- a/src/meson.build +++ b/src/meson.build @@ -263,6 +263,7 @@ if get_option('mangoapp') 'mangoapp', files( 'app/main.cpp', + 'app/frame_feed.cpp', ), c_args : [ pre_args,