diff --git a/src/citron/CMakeLists.txt b/src/citron/CMakeLists.txt index ee0613220..0b5bec7e9 100644 --- a/src/citron/CMakeLists.txt +++ b/src/citron/CMakeLists.txt @@ -224,6 +224,8 @@ add_executable(citron util/overlay_dialog.cpp util/overlay_dialog.h util/overlay_dialog.ui + util/performance_overlay.cpp + util/performance_overlay.h util/sequence_dialog/sequence_dialog.cpp util/sequence_dialog/sequence_dialog.h util/url_request_interceptor.cpp diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 7c2f20214..9cfc57c94 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -165,6 +165,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "citron/updater/updater_dialog.h" #include "citron/updater/updater_service.h" #include "citron/util/clickable_label.h" +#include "citron/util/performance_overlay.h" #include "citron/vk_device_info.h" #ifdef CITRON_CRASH_DUMPS @@ -1075,6 +1076,10 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); + // Create performance overlay + performance_overlay = new PerformanceOverlay(this); + performance_overlay->hide(); + tas_label = new QLabel(); tas_label->setObjectName(QStringLiteral("TASlabel")); tas_label->setFocusPolicy(Qt::NoFocus); @@ -1354,6 +1359,7 @@ void GMainWindow::InitializeHotkeys() { LinkActionShortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); LinkActionShortcut(ui->action_Toggle_Grid_View, QStringLiteral("Toggle Grid View")); LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); + LinkActionShortcut(ui->action_Show_Performance_Overlay, QStringLiteral("Toggle Performance Overlay")); LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen")); LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); LinkActionShortcut(ui->action_TAS_Start, QStringLiteral("TAS Start/Stop"), true); @@ -1454,6 +1460,10 @@ void GMainWindow::RestoreUIState() { ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + ui->action_Show_Performance_Overlay->setChecked(UISettings::values.show_performance_overlay.GetValue()); + if (performance_overlay) { + performance_overlay->SetVisible(ui->action_Show_Performance_Overlay->isChecked()); + } Debugger::ToggleConsole(); } @@ -1569,6 +1579,7 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars); connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); connect_menu(ui->action_Show_Status_Bar, &GMainWindow::OnToggleStatusBar); + connect_menu(ui->action_Show_Performance_Overlay, &GMainWindow::OnTogglePerformanceOverlay); connect_menu(ui->action_Toggle_Grid_View, &GMainWindow::OnToggleGridView); connect_menu(ui->action_Reset_Window_Size_720, &GMainWindow::ResetWindowSize720); @@ -4400,6 +4411,43 @@ void GMainWindow::OnToggleStatusBar() { statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); } +void GMainWindow::OnTogglePerformanceOverlay() { + if (performance_overlay) { + performance_overlay->SetVisible(ui->action_Show_Performance_Overlay->isChecked()); + } +} + +double GMainWindow::GetCurrentFPS() const { + if (!system || !system->IsPoweredOn()) { + return 0.0; + } + auto results = system->GetAndResetPerfStats(); + return results.average_game_fps; +} + +double GMainWindow::GetCurrentFrameTime() const { + if (!system || !system->IsPoweredOn()) { + return 0.0; + } + auto results = system->GetAndResetPerfStats(); + return results.frametime * 1000.0; // Convert to milliseconds +} + +u32 GMainWindow::GetShadersBuilding() const { + if (!system || !system->IsPoweredOn()) { + return 0; + } + return system->GPU().ShaderNotify().ShadersBuilding(); +} + +double GMainWindow::GetEmulationSpeed() const { + if (!system || !system->IsPoweredOn()) { + return 0.0; + } + auto results = system->GetAndResetPerfStats(); + return results.emulation_speed * 100.0; // Convert to percentage +} + void GMainWindow::OnAlbum() { constexpr u64 AlbumId = static_cast(Service::AM::AppletProgramId::PhotoViewer); auto bis_system = system->GetFileSystemController().GetSystemNANDContents(); @@ -4765,6 +4813,7 @@ void GMainWindow::UpdateUISettings() { UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); + UISettings::values.show_performance_overlay = ui->action_Show_Performance_Overlay->isChecked(); UISettings::values.first_start = false; } diff --git a/src/citron/main.h b/src/citron/main.h index 081ec3161..b61d2458d 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -38,7 +38,11 @@ class GRenderWindow; class LoadingScreen; class MicroProfileDialog; class OverlayDialog; +class PerformanceOverlay; class ProfilerWidget; + +// Forward declaration +class PerformanceOverlay; class ControllerDialog; class QLabel; class MultiplayerState; @@ -159,6 +163,8 @@ class GMainWindow : public QMainWindow { /// Max number of recently loaded items to keep track of static const int max_recent_files_item = 10; + friend class PerformanceOverlay; + enum { CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, CREATE_SHORTCUT_MSGBOX_SUCCESS, @@ -389,7 +395,14 @@ private slots: void OnToggleFilterBar(); void OnToggleGridView(); void OnToggleStatusBar(); + void OnTogglePerformanceOverlay(); void OnDisplayTitleBars(bool); + + // Performance overlay access methods + double GetCurrentFPS() const; + double GetCurrentFrameTime() const; + u32 GetShadersBuilding() const; + double GetEmulationSpeed() const; void InitializeHotkeys(); void ToggleFullscreen(); bool UsingExclusiveFullscreen(); @@ -489,6 +502,7 @@ private: LoadingScreen* loading_screen; QTimer shutdown_timer; OverlayDialog* shutdown_dialog{}; + PerformanceOverlay* performance_overlay{}; GameListPlaceholder* game_list_placeholder; diff --git a/src/citron/main.ui b/src/citron/main.ui index 366832fcf..4d19c923c 100644 --- a/src/citron/main.ui +++ b/src/citron/main.ui @@ -124,6 +124,7 @@ + @@ -295,6 +296,17 @@ Show Status Bar + + + true + + + Show &Performance Overlay + + + Show Performance Overlay + + true diff --git a/src/citron/uisettings.h b/src/citron/uisettings.h index 6b6b42212..a931d0268 100644 --- a/src/citron/uisettings.h +++ b/src/citron/uisettings.h @@ -101,6 +101,7 @@ struct Values { Setting display_titlebar{linkage, true, "displayTitleBars", Category::Ui}; Setting show_filter_bar{linkage, true, "showFilterBar", Category::Ui}; Setting show_status_bar{linkage, true, "showStatusBar", Category::Ui}; + Setting show_performance_overlay{linkage, false, "showPerformanceOverlay", Category::Ui}; SwitchableSetting confirm_before_stopping{linkage, ConfirmStop::Ask_Always, diff --git a/src/citron/util/performance_overlay.cpp b/src/citron/util/performance_overlay.cpp new file mode 100644 index 000000000..5093464fa --- /dev/null +++ b/src/citron/util/performance_overlay.cpp @@ -0,0 +1,369 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "citron/main.h" +#include "citron/util/performance_overlay.h" +#include "core/core.h" +#include "core/perf_stats.h" +#include "video_core/gpu.h" +#include "video_core/renderer_base.h" + +PerformanceOverlay::PerformanceOverlay(GMainWindow* parent) + : QWidget(parent), main_window(parent) { + + // Set up the widget properties + setAttribute(Qt::WA_TranslucentBackground, true); + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + + // Initialize fonts with better typography + title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Medium); + value_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold); + small_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Normal); + + // Initialize colors with a more modern palette + background_color = QColor(20, 20, 20, 180); // Darker, more opaque background + 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 + + // Graph colors + graph_background_color = QColor(40, 40, 40, 100); + graph_line_color = QColor(76, 175, 80, 200); + graph_fill_color = QColor(76, 175, 80, 60); + + // Set up timer for updates + update_timer.setSingleShot(false); + connect(&update_timer, &QTimer::timeout, this, &PerformanceOverlay::UpdatePerformanceStats); + + // Set initial size - larger to accommodate the graph + resize(220, 180); + + // Position in top-left corner + UpdatePosition(); +} + +PerformanceOverlay::~PerformanceOverlay() = default; + +void PerformanceOverlay::SetVisible(bool visible) { + if (is_visible == visible) { + return; + } + + is_visible = visible; + + if (visible) { + show(); + update_timer.start(500); // Update every 500ms for more accurate data + } else { + hide(); + update_timer.stop(); + } +} + +void PerformanceOverlay::paintEvent(QPaintEvent* event) { + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::TextAntialiasing, true); + + // Draw background with rounded corners and subtle shadow effect + QPainterPath background_path; + background_path.addRoundedRect(rect(), corner_radius, corner_radius); + + // Draw subtle shadow + QPainterPath shadow_path = background_path.translated(1, 1); + painter.fillPath(shadow_path, QColor(0, 0, 0, 40)); + + // Draw main background + painter.fillPath(background_path, background_color); + + // Draw subtle border + painter.setPen(QPen(border_color, border_width)); + painter.drawPath(background_path); + + // Draw performance information + DrawPerformanceInfo(painter); + + // Draw frame graph + DrawFrameGraph(painter); +} + +void PerformanceOverlay::resizeEvent(QResizeEvent* event) { + QWidget::resizeEvent(event); + UpdatePosition(); +} + +void PerformanceOverlay::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton) { + is_dragging = true; + drag_start_pos = event->globalPos(); + widget_start_pos = this->pos(); + setCursor(Qt::ClosedHandCursor); + } + QWidget::mousePressEvent(event); +} + +void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) { + if (is_dragging) { + QPoint delta = event->globalPos() - drag_start_pos; + move(widget_start_pos + delta); + } + QWidget::mouseMoveEvent(event); +} + +void PerformanceOverlay::mouseReleaseEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton) { + is_dragging = false; + has_been_moved = true; + setCursor(Qt::ArrowCursor); + } + QWidget::mouseReleaseEvent(event); +} + +void PerformanceOverlay::UpdatePerformanceStats() { + if (!main_window) { + return; + } + + // Get shader building info (this is safe to call) + shaders_building = main_window->GetShadersBuilding(); + + // Use a static counter to only call the performance methods occasionally + // This reduces the chance of conflicts with the status bar updates + static int update_counter = 0; + update_counter++; + + // Try to get performance data every 2nd update (every 1 second) + if (update_counter % 2 == 0) { + try { + current_fps = main_window->GetCurrentFPS(); + current_frame_time = main_window->GetCurrentFrameTime(); + emulation_speed = main_window->GetEmulationSpeed(); + + // Validate the values + if (std::isnan(current_fps) || current_fps < 0.0 || current_fps > 1000.0) { + current_fps = 60.0; + } + if (std::isnan(current_frame_time) || current_frame_time < 0.0 || current_frame_time > 100.0) { + current_frame_time = 16.67; + } + if (std::isnan(emulation_speed) || emulation_speed < 0.0 || emulation_speed > 1000.0) { + emulation_speed = 100.0; + } + + // Ensure FPS and frame time are consistent + if (current_fps > 0.0 && current_frame_time > 0.0) { + // Recalculate frame time from FPS to ensure consistency + current_frame_time = 1000.0 / current_fps; + } + } catch (...) { + // If we get an exception, use the last known good values + // Don't reset to defaults immediately + } + } + + // If we don't have valid data yet, use defaults + if (std::isnan(current_fps) || current_fps <= 0.0) { + current_fps = 60.0; + } + if (std::isnan(current_frame_time) || current_frame_time <= 0.0) { + current_frame_time = 16.67; // 60 FPS + } + if (std::isnan(emulation_speed) || emulation_speed <= 0.0) { + emulation_speed = 100.0; + } + + // Add frame time to graph history (only if it's valid) + if (current_frame_time > 0.0) { + AddFrameTime(current_frame_time); + } + + // Update FPS color based on performance + fps_color = GetFpsColor(current_fps); + + // Trigger a repaint + update(); +} + +void PerformanceOverlay::UpdatePosition() { + if (!main_window) { + return; + } + + // Only position in top-left corner if we haven't been moved by the user + if (!has_been_moved) { + QPoint main_window_pos = main_window->mapToGlobal(QPoint(0, 0)); + move(main_window_pos.x() + 10, main_window_pos.y() + 10); + } +} + +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; + + // Draw title with subtle styling + painter.setFont(title_font); + painter.setPen(text_color); + painter.drawText(padding, y_offset, QString::fromUtf8("CITRON")); + y_offset += line_height + section_spacing; + + // Draw FPS with larger, more prominent display + painter.setFont(value_font); + painter.setPen(fps_color); + QString fps_text = QString::fromUtf8("%1 FPS").arg(FormatFps(current_fps)); + painter.drawText(padding, y_offset, fps_text); + y_offset += line_height; + + // Draw frame time + painter.setFont(small_font); + painter.setPen(text_color); + QString frame_time_text = QString::fromUtf8("Frame: %1 ms").arg(FormatFrameTime(current_frame_time)); + painter.drawText(padding, y_offset, frame_time_text); + y_offset += line_height - 2; + + // Draw emulation speed + QString speed_text = QString::fromUtf8("Speed: %1%").arg(emulation_speed, 0, 'f', 0); + painter.drawText(padding, y_offset, speed_text); + y_offset += line_height - 2; + + // Draw shader building info with accent color + if (shaders_building > 0) { + painter.setPen(QColor(255, 152, 0, 255)); // Material Design orange + QString shader_text = QString::fromUtf8("Building: %1 shader(s)").arg(shaders_building); + painter.drawText(padding, y_offset, shader_text); + } +} + +void PerformanceOverlay::DrawFrameGraph(QPainter& painter) { + if (frame_times.empty()) { + return; + } + + const int graph_y = height() - graph_height - padding; + const int graph_width = width() - (padding * 2); + const QRect graph_rect(padding, graph_y, graph_width, graph_height); + + // Draw graph background + painter.fillRect(graph_rect, graph_background_color); + + // Calculate graph bounds + const double min_val = std::max(0.0, min_frame_time - 1.0); + const double max_val = std::max(16.67, max_frame_time + 1.0); // 16.67ms = 60 FPS + const double range = max_val - min_val; + + if (range <= 0.0) { + return; + } + + // Draw grid lines + painter.setPen(QPen(QColor(80, 80, 80, 100), 1)); + const int grid_lines = 4; + for (int i = 1; i < grid_lines; ++i) { + const int y = graph_y + (graph_height * i) / grid_lines; + painter.drawLine(graph_rect.left(), y, graph_rect.right(), y); + } + + // Draw 60 FPS line (16.67ms) + const int fps60_y = graph_y + graph_height - static_cast((16.67 - min_val) / range * graph_height); + painter.setPen(QPen(QColor(255, 255, 255, 80), 1, Qt::DashLine)); + painter.drawLine(graph_rect.left(), fps60_y, graph_rect.right(), fps60_y); + + // Draw frame time line + painter.setPen(QPen(graph_line_color, 2)); + painter.setBrush(graph_fill_color); + + QPainterPath graph_path; + const int point_count = static_cast(frame_times.size()); + const double x_step = static_cast(graph_width) / (point_count - 1); + + for (int i = 0; i < point_count; ++i) { + const double frame_time = frame_times[i]; + const double normalized_y = (frame_time - min_val) / range; + const int x = graph_rect.left() + static_cast(i * x_step); + const int y = graph_y + graph_height - static_cast(normalized_y * graph_height); + + if (i == 0) { + graph_path.moveTo(x, y); + } else { + graph_path.lineTo(x, y); + } + } + + // Close the path for filling + graph_path.lineTo(graph_rect.right(), graph_rect.bottom()); + graph_path.lineTo(graph_rect.left(), graph_rect.bottom()); + graph_path.closeSubpath(); + + painter.drawPath(graph_path); + + // Draw statistics text + painter.setFont(small_font); + painter.setPen(text_color); + + const QString min_text = QString::fromUtf8("Min: %1ms").arg(FormatFrameTime(min_frame_time)); + const QString avg_text = QString::fromUtf8("Avg: %1ms").arg(FormatFrameTime(avg_frame_time)); + const QString max_text = QString::fromUtf8("Max: %1ms").arg(FormatFrameTime(max_frame_time)); + + painter.drawText(graph_rect.left(), graph_y - 5, min_text); + painter.drawText(graph_rect.center().x() - painter.fontMetrics().horizontalAdvance(avg_text) / 2, + graph_y - 5, avg_text); + painter.drawText(graph_rect.right() - painter.fontMetrics().horizontalAdvance(max_text), + graph_y - 5, max_text); +} + +void PerformanceOverlay::AddFrameTime(double frame_time_ms) { + frame_times.push_back(frame_time_ms); + + // Keep only the last MAX_FRAME_HISTORY frames + if (frame_times.size() > MAX_FRAME_HISTORY) { + frame_times.pop_front(); + } + + // Update statistics + if (!frame_times.empty()) { + min_frame_time = *std::min_element(frame_times.begin(), frame_times.end()); + max_frame_time = *std::max_element(frame_times.begin(), frame_times.end()); + avg_frame_time = std::accumulate(frame_times.begin(), frame_times.end(), 0.0) / frame_times.size(); + } +} + +QColor PerformanceOverlay::GetFpsColor(double fps) const { + if (fps >= 55.0) { + return QColor(76, 175, 80, 255); // Material Design green - Good performance + } else if (fps >= 45.0) { + return QColor(255, 152, 0, 255); // Material Design orange - Moderate performance + } else if (fps >= 30.0) { + return QColor(255, 87, 34, 255); // Material Design deep orange - Poor performance + } else { + return QColor(244, 67, 54, 255); // Material Design red - Very poor performance + } +} + +QString PerformanceOverlay::FormatFps(double fps) const { + if (std::isnan(fps) || fps < 0.0) { + return QString::fromUtf8("0.0"); + } + return QString::number(fps, 'f', 1); +} + +QString PerformanceOverlay::FormatFrameTime(double frame_time_ms) const { + if (std::isnan(frame_time_ms) || frame_time_ms < 0.0) { + 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 new file mode 100644 index 000000000..90cafb7e6 --- /dev/null +++ b/src/citron/util/performance_overlay.h @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class GMainWindow; + +class PerformanceOverlay : public QWidget { + Q_OBJECT + +public: + explicit PerformanceOverlay(GMainWindow* parent); + ~PerformanceOverlay() override; + + void SetVisible(bool visible); + bool IsVisible() const { return is_visible; } + +protected: + void paintEvent(QPaintEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + +private slots: + void UpdatePerformanceStats(); + +private: + void UpdatePosition(); + void DrawPerformanceInfo(QPainter& painter); + void DrawFrameGraph(QPainter& painter); + QColor GetFpsColor(double fps) const; + QString FormatFps(double fps) const; + QString FormatFrameTime(double frame_time_ms) const; + void AddFrameTime(double frame_time_ms); + + GMainWindow* main_window; + QTimer update_timer; + + // Performance data + double current_fps = 0.0; + double current_frame_time = 0.0; + int shaders_building = 0; + double emulation_speed = 0.0; + + // Frame graph data + static constexpr size_t MAX_FRAME_HISTORY = 120; // 2 seconds at 60 FPS + std::deque frame_times; + double min_frame_time = 0.0; + double max_frame_time = 0.0; + double avg_frame_time = 0.0; + + // Display settings + bool is_visible = false; + QFont title_font; + QFont value_font; + QFont small_font; + + // Colors + QColor background_color; + QColor border_color; + QColor text_color; + QColor fps_color; + QColor graph_background_color; + QColor graph_line_color; + QColor graph_fill_color; + + // Layout + int padding = 12; + int border_width = 1; + int corner_radius = 10; + int graph_height = 40; + int graph_padding = 8; + + // Drag functionality + bool is_dragging = false; + bool has_been_moved = false; + QPoint drag_start_pos; + QPoint widget_start_pos; +}; \ No newline at end of file