diff --git a/src/citron/CMakeLists.txt b/src/citron/CMakeLists.txt index 1b18cd17d..c78d36b09 100644 --- a/src/citron/CMakeLists.txt +++ b/src/citron/CMakeLists.txt @@ -391,6 +391,10 @@ if (APPLE) elseif(WIN32) # compile as a win32 gui application instead of a console application target_link_libraries(citron PRIVATE Qt6::EntryPointPrivate) + + # Add Windows-specific libraries for WMI/COM to read hardware sensors + target_link_libraries(citron PRIVATE ole32 oleaut32 wbemuuid) + if(MSVC) target_link_libraries(citron PRIVATE version.lib) set_target_properties(citron PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") diff --git a/src/citron/util/performance_overlay.cpp b/src/citron/util/performance_overlay.cpp index 4c8b8db72..528de4a78 100644 --- a/src/citron/util/performance_overlay.cpp +++ b/src/citron/util/performance_overlay.cpp @@ -14,6 +14,20 @@ #include #include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#include +#pragma comment(lib, "wbemuuid.lib") // For MSVC, helps the linker find the library +#endif + +#ifdef Q_OS_ANDROID +#include +#endif #include "citron/main.h" #include "citron/util/performance_overlay.h" @@ -39,6 +53,7 @@ PerformanceOverlay::PerformanceOverlay(GMainWindow* parent) border_color = QColor(60, 60, 60, 120); // Subtle border text_color = QColor(220, 220, 220, 255); // Light gray text fps_color = QColor(76, 175, 80, 255); // Material Design green + temperature_color = QColor(76, 175, 80, 255); // Default to green // Graph colors graph_background_color = QColor(40, 40, 40, 100); @@ -199,6 +214,11 @@ void PerformanceOverlay::UpdatePerformanceStats() { } } + // Update hardware temperatures every 4th update (every 2 seconds) + if (update_counter % 4 == 0) { + UpdateHardwareTemperatures(); + } + // If we don't have valid data yet, use defaults if (std::isnan(current_fps) || current_fps <= 0.0) { current_fps = 60.0; @@ -215,13 +235,120 @@ void PerformanceOverlay::UpdatePerformanceStats() { AddFrameTime(current_frame_time); } - // Update FPS color based on performance + // Update FPS and Temperature colors based on performance fps_color = GetFpsColor(current_fps); + temperature_color = GetTemperatureColor(std::max({cpu_temperature, gpu_temperature, battery_temperature})); // Trigger a repaint update(); } +void PerformanceOverlay::UpdateHardwareTemperatures() { + // Reset data + cpu_temperature = 0.0f; + gpu_temperature = 0.0f; + cpu_sensor_type.clear(); + gpu_sensor_type.clear(); + battery_percentage = 0; + battery_temperature = 0.0f; + +#if defined(Q_OS_LINUX) + // --- Standard Linux Thermal Zone Reading --- + QDir thermal_dir(QString::fromUtf8("/sys/class/thermal/")); + QStringList filters{QString::fromUtf8("thermal_zone*")}; + QStringList thermal_zones = thermal_dir.entryList(filters, QDir::Dirs); + + for (const QString& zone_name : thermal_zones) { + QFile type_file(thermal_dir.filePath(zone_name + QString::fromUtf8("/type"))); + if (!type_file.open(QIODevice::ReadOnly | QIODevice::Text)) continue; + QString type = QString::fromUtf8(type_file.readAll()).trimmed(); + type_file.close(); + + QFile temp_file(thermal_dir.filePath(zone_name + QString::fromUtf8("/temp"))); + if (!temp_file.open(QIODevice::ReadOnly | QIODevice::Text)) continue; + float temp = temp_file.readAll().trimmed().toFloat() / 1000.0f; + temp_file.close(); + + if (type.contains(QString::fromUtf8("x86_pkg_temp")) || type.contains(QString::fromUtf8("cpu"))) { + if (temp > cpu_temperature) { + cpu_temperature = temp; + cpu_sensor_type = QString::fromUtf8("CPU"); + } + } else if (type.contains(QString::fromUtf8("radeon")) || type.contains(QString::fromUtf8("amdgpu")) || type.contains(QString::fromUtf8("nvidia")) || type.contains(QString::fromUtf8("nouveau"))) { + if (temp > gpu_temperature) { + gpu_temperature = temp; + gpu_sensor_type = QString::fromUtf8("GPU"); + } + } + } +#endif + +#if defined(Q_OS_ANDROID) + // This uses QtAndroid Extras to get battery info from the Android system. + // NOTE: This requires the QtAndroidExtras module to be linked in the build. + QJniObject battery_status = QJniObject::callStaticObjectMethod( + "android/content/CONTEXT", "registerReceiver", + "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;", + nullptr, new QJniObject("android.content.IntentFilter", "(Ljava/lang/String;)V", "android.intent.action.BATTERY_CHANGED")); + + if (battery_status.isValid()) { + int level = battery_status.callMethod("getIntExtra", "(Ljava/lang/String;I)I", + QJniObject::fromString("level").object(), -1); + int scale = battery_status.callMethod("getIntExtra", "(Ljava/lang/String;I)I", + QJniObject::fromString("scale").object(), -1); + int temp_tenths = battery_status.callMethod("getIntExtra", "(Ljava/lang/String;I)I", + QJniObject::fromString("temperature").object(), -1); + + if (scale > 0) { + battery_percentage = (level * 100) / scale; + } + if (temp_tenths > 0) { + battery_temperature = static_cast(temp_tenths) / 10.0f; + } + } +#endif + +#if defined(Q_OS_WIN) + HRESULT hres; + IWbemLocator* pLoc = nullptr; + IWbemServices* pSvc = nullptr; + + hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLoc); + + if (SUCCEEDED(hres)) { + hres = pLoc->ConnectServer(_bstr_t(L"ROOT\\WMI"), NULL, NULL, 0, NULL, 0, 0, &pSvc); + if (SUCCEEDED(hres)) { + hres = CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, + RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); + if (SUCCEEDED(hres)) { + IEnumWbemClassObject* pEnumerator = nullptr; + hres = pSvc->ExecQuery(bstr_t("WQL"), bstr_t("SELECT * FROM MSAcpi_ThermalZoneTemperature"), + WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pEnumerator); + if (SUCCEEDED(hres)) { + IWbemClassObject* pclsObj = nullptr; + ULONG uReturn = 0; + while (pEnumerator) { + pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn); + if (uReturn == 0) break; + + VARIANT vtProp; + pclsObj->Get(L"CurrentTemperature", 0, &vtProp, 0, 0); + float temp_kelvin = vtProp.uintVal / 10.0f; + cpu_temperature = temp_kelvin - 273.15f; + cpu_sensor_type = QString::fromUtf8("CPU"); + VariantClear(&vtProp); + pclsObj->Release(); + } + pEnumerator->Release(); + } + } + } + } + if(pSvc) pSvc->Release(); + if(pLoc) pLoc->Release(); +#endif +} + void PerformanceOverlay::UpdatePosition() { if (!main_window) { return; @@ -237,17 +364,44 @@ void PerformanceOverlay::UpdatePosition() { void PerformanceOverlay::DrawPerformanceInfo(QPainter& painter) { painter.setRenderHint(QPainter::TextAntialiasing, true); - int y_offset = padding + 12; - const int line_height = 22; - const int section_spacing = 4; + int y_offset = padding; + const int line_height = 20; - // Draw title with subtle styling + // Draw title painter.setFont(title_font); painter.setPen(text_color); - painter.drawText(padding, y_offset, QString::fromUtf8("CITRON")); - y_offset += line_height + section_spacing; + painter.drawText(padding, y_offset + 12, QString::fromUtf8("CITRON")); - // Draw FPS with larger, more prominent display + int y_offset_right = padding; + const int line_height_right = 18; + + // Draw Temperatures + painter.setFont(small_font); + + float core_temp_to_display = std::max(cpu_temperature, gpu_temperature); + if (core_temp_to_display > 0.0f) { + QString core_label = gpu_temperature > cpu_temperature ? gpu_sensor_type : cpu_sensor_type; + QString core_temp_text = QString::fromUtf8("%1: %2°C").arg(core_label).arg(core_temp_to_display, 0, 'f', 0); + painter.setPen(GetTemperatureColor(core_temp_to_display)); + int text_width = painter.fontMetrics().horizontalAdvance(core_temp_text); + painter.drawText(width() - padding - text_width, y_offset_right + 12, core_temp_text); + } + y_offset_right += line_height_right; + + // Draw Battery info + if (battery_percentage > 0) { + QString batt_text = QString::fromUtf8("Batt: %1%").arg(battery_percentage); + if (battery_temperature > 0.0f) { + batt_text += QString::fromUtf8(" (%1°C)").arg(battery_temperature, 0, 'f', 0); + } + painter.setPen(text_color); + int text_width = painter.fontMetrics().horizontalAdvance(batt_text); + painter.drawText(width() - padding - text_width, y_offset_right + 12, batt_text); + } + + y_offset += line_height + 4; + + // Draw FPS painter.setFont(value_font); painter.setPen(fps_color); QString fps_text = QString::fromUtf8("%1 FPS").arg(FormatFps(current_fps)); @@ -314,7 +468,7 @@ void PerformanceOverlay::DrawFrameGraph(QPainter& painter) { QPainterPath graph_path; const int point_count = static_cast(frame_times.size()); - const double x_step = static_cast(graph_width) / (point_count - 1); + const double x_step = static_cast(graph_width) / (std::max(1, point_count - 1)); for (int i = 0; i < point_count; ++i) { const double frame_time = frame_times[i]; @@ -379,6 +533,16 @@ QColor PerformanceOverlay::GetFpsColor(double fps) const { } } +QColor PerformanceOverlay::GetTemperatureColor(float temperature) const { + if (temperature > 70.0f) { + return QColor(244, 67, 54, 255); // Material Design red + } else if (temperature > 60.0f) { + return QColor(255, 152, 0, 255); // Material Design orange + } else { + return QColor(76, 175, 80, 255); // Material Design green + } +} + QString PerformanceOverlay::FormatFps(double fps) const { if (std::isnan(fps) || fps < 0.0) { return QString::fromUtf8("0.0"); @@ -391,4 +555,4 @@ QString PerformanceOverlay::FormatFrameTime(double frame_time_ms) const { return QString::fromUtf8("0.00"); } return QString::number(frame_time_ms, 'f', 2); -} \ No newline at end of file +} diff --git a/src/citron/util/performance_overlay.h b/src/citron/util/performance_overlay.h index 90cafb7e6..98b33b3aa 100644 --- a/src/citron/util/performance_overlay.h +++ b/src/citron/util/performance_overlay.h @@ -37,9 +37,11 @@ private slots: private: void UpdatePosition(); + void UpdateHardwareTemperatures(); void DrawPerformanceInfo(QPainter& painter); void DrawFrameGraph(QPainter& painter); QColor GetFpsColor(double fps) const; + QColor GetTemperatureColor(float temperature) const; QString FormatFps(double fps) const; QString FormatFrameTime(double frame_time_ms) const; void AddFrameTime(double frame_time_ms); @@ -52,6 +54,12 @@ private: double current_frame_time = 0.0; int shaders_building = 0; double emulation_speed = 0.0; + float cpu_temperature = 0.0f; + float gpu_temperature = 0.0f; + QString cpu_sensor_type; + QString gpu_sensor_type; + int battery_percentage = 0; + float battery_temperature = 0.0f; // Frame graph data static constexpr size_t MAX_FRAME_HISTORY = 120; // 2 seconds at 60 FPS @@ -71,6 +79,7 @@ private: QColor border_color; QColor text_color; QColor fps_color; + QColor temperature_color; QColor graph_background_color; QColor graph_line_color; QColor graph_fill_color; @@ -87,4 +96,4 @@ private: bool has_been_moved = false; QPoint drag_start_pos; QPoint widget_start_pos; -}; \ No newline at end of file +};