From 45c9f9bbb31ed06976bfacbc7bbf8851191ffb84 Mon Sep 17 00:00:00 2001 From: crueter Date: Sun, 15 Feb 2026 19:22:19 +0100 Subject: [PATCH] [desktop] Add basic Frametime/FPS overlay (#3537) Just displays min, max, avg frametime/fps, alongside a chart of FPS in the last 30 seconds. Notes: - Qt Charts is now required - FPS/frametime collector now runs 2x as often. TODO: keep status bar at 500ms, but put perf overlay at 250ms Signed-off-by: crueter Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3537 Reviewed-by: Lizzie Reviewed-by: MaranBr --- CMakeLists.txt | 4 +- src/qt_common/config/uisettings.h | 70 +++--- src/yuzu/CMakeLists.txt | 4 +- .../configuration/configure_filesystem.cpp | 4 +- src/yuzu/main.ui | 9 + src/yuzu/main_window.cpp | 39 +++- src/yuzu/main_window.h | 11 + src/yuzu/render/performance_overlay.cpp | 207 ++++++++++++++++++ src/yuzu/render/performance_overlay.h | 73 ++++++ src/yuzu/render/performance_overlay.ui | 181 +++++++++++++++ 10 files changed, 563 insertions(+), 39 deletions(-) create mode 100644 src/yuzu/render/performance_overlay.cpp create mode 100644 src/yuzu/render/performance_overlay.h create mode 100644 src/yuzu/render/performance_overlay.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 0568a49ec8..b0a5ad9a51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -641,7 +641,7 @@ if (ENABLE_QT) list(APPEND CMAKE_PREFIX_PATH "${Qt6_DIR}") endif() - find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets Concurrent) + find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets Charts Concurrent) if (YUZU_USE_QT_MULTIMEDIA) find_package(Qt6 REQUIRED COMPONENTS Multimedia) @@ -680,7 +680,7 @@ if (ENABLE_QT) ## Components ## # Best practice is to ask for all components at once, so they are from the same version - set(YUZU_QT_COMPONENTS Core Widgets Concurrent) + set(YUZU_QT_COMPONENTS Core Widgets Charts Concurrent) if (PLATFORM_LINUX) list(APPEND YUZU_QT_COMPONENTS DBus) endif() diff --git a/src/qt_common/config/uisettings.h b/src/qt_common/config/uisettings.h index b52c673305..49205c5b84 100644 --- a/src/qt_common/config/uisettings.h +++ b/src/qt_common/config/uisettings.h @@ -219,6 +219,9 @@ struct Values { QVector favorited_ids; QMap ryujinx_link_paths; + // perf overlay + Setting show_perf_overlay{linkage, false, "show_perf_overlay", Category::UiGameList}; + // Compatibility List Setting show_compat{linkage, true, "show_compat", Category::UiGameList}; @@ -249,39 +252,40 @@ void RestoreWindowState(std::unique_ptr& qtConfig); // This must be in alphabetical order according to action name as it must have the same order as // UISetting::values.shortcuts, which is alphabetically ordered. // clang-format off -const std::array default_hotkeys{{ - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Mute/Unmute")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+M"), std::string("Home+Dpad_Right"), Qt::WindowShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Down")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("-"), std::string("Home+Dpad_Down"), Qt::ApplicationShortcut, true}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Up")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("="), std::string("Home+Dpad_Up"), Qt::ApplicationShortcut, true}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Capture Screenshot")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+P"), std::string("Screenshot"), Qt::WidgetWithChildrenShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Change Adapting Filter")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F8"), std::string("Home+L"), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Change Docked Mode")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F10"), std::string("Home+X"), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Change GPU Mode")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F9"), std::string("Home+R"), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Configure")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+,"), std::string(""), Qt::WidgetWithChildrenShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Configure Current Game")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+."), std::string(""), Qt::WidgetWithChildrenShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Continue/Pause Emulation")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F4"), std::string("Home+Plus"), Qt::WindowShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Exit Fullscreen")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Esc"), std::string(""), Qt::WindowShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Exit Eden")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+Q"), std::string("Home+Minus"), Qt::WindowShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Fullscreen")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F11"), std::string("Home+B"), Qt::WindowShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load File")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+O"), std::string(""), Qt::WidgetWithChildrenShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load/Remove Amiibo")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F2"), std::string("Home+A"), Qt::WidgetWithChildrenShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Browse Public Game Lobby")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+B"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Create Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+N"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Direct Connect to Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+C"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Leave Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+L"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Show Current Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+R"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Restart Emulation")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F6"), std::string("R+Plus+Minus"), Qt::WindowShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Stop Emulation")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F5"), std::string("L+Plus+Minus"), Qt::WindowShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Record")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F7"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Reset")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F6"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Start/Stop")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F5"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Filter Bar")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F"), std::string(""), Qt::WindowShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Framerate Limit")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+U"), std::string("Home+Y"), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Turbo Speed")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+Z"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Slow Speed")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+X"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Mouse Panning")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F9"), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Renderdoc Capture")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string(""), std::string(""), Qt::ApplicationShortcut, false}}, - {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Status Bar")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+S"), std::string(""), Qt::WindowShortcut, false}}, +const std::array default_hotkeys{{ + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Mute/Unmute")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+M"), std::string("Home+Dpad_Right"), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Down")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("-"), std::string("Home+Dpad_Down"), Qt::ApplicationShortcut, true}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Up")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("="), std::string("Home+Dpad_Up"), Qt::ApplicationShortcut, true}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Capture Screenshot")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+P"), std::string("Screenshot"), Qt::WidgetWithChildrenShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Change Adapting Filter")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F8"), std::string("Home+L"), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Change Docked Mode")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F10"), std::string("Home+X"), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Change GPU Mode")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F9"), std::string("Home+R"), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Configure")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+,"), std::string(""), Qt::WidgetWithChildrenShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Configure Current Game")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+."), std::string(""), Qt::WidgetWithChildrenShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Continue/Pause Emulation")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F4"), std::string("Home+Plus"), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Exit Fullscreen")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Esc"), std::string(""), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Exit Eden")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+Q"), std::string("Home+Minus"), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Fullscreen")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F11"), std::string("Home+B"), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load File")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+O"), std::string(""), Qt::WidgetWithChildrenShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load/Remove Amiibo")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F2"), std::string("Home+A"), Qt::WidgetWithChildrenShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Browse Public Game Lobby")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+B"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Create Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+N"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Direct Connect to Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+C"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Leave Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+L"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Show Current Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+R"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Restart Emulation")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F6"), std::string("R+Plus+Minus"), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Stop Emulation")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F5"), std::string("L+Plus+Minus"), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Record")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F7"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Reset")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F6"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "TAS Start/Stop")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F5"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Filter Bar")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F"), std::string(""), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Framerate Limit")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+U"), std::string("Home+Y"), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Turbo Speed")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+Z"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Slow Speed")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+X"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Mouse Panning")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+F9"), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Renderdoc Capture")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string(""), std::string(""), Qt::ApplicationShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Status Bar")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+S"), std::string(""), Qt::WindowShortcut, false}}, + {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Toggle Performance Overlay")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+V"), std::string(""), Qt::WindowShortcut, false}}, }}; // clang-format on diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index dad32f2316..178bb540f5 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -242,6 +242,8 @@ add_executable(yuzu configuration/system/new_user_dialog.h configuration/system/new_user_dialog.cpp configuration/system/new_user_dialog.ui configuration/system/profile_avatar_dialog.h configuration/system/profile_avatar_dialog.cpp configuration/addon/mod_select_dialog.h configuration/addon/mod_select_dialog.cpp configuration/addon/mod_select_dialog.ui + + render/performance_overlay.h render/performance_overlay.cpp render/performance_overlay.ui ) set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden") @@ -399,7 +401,7 @@ endif() target_link_libraries(yuzu PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(yuzu PRIVATE common core input_common frontend_common network video_core qt_common) -target_link_libraries(yuzu PRIVATE Boost::headers glad Qt6::Widgets Qt6::Concurrent) +target_link_libraries(yuzu PRIVATE Boost::headers glad Qt6::Widgets Qt6::Charts Qt6::Concurrent) target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) if (UNIX AND NOT APPLE) diff --git a/src/yuzu/configuration/configure_filesystem.cpp b/src/yuzu/configuration/configure_filesystem.cpp index f0310a30bd..27af4c8055 100644 --- a/src/yuzu/configuration/configure_filesystem.cpp +++ b/src/yuzu/configuration/configure_filesystem.cpp @@ -38,9 +38,9 @@ ConfigureFilesystem::ConfigureFilesystem(QWidget* parent) connect(ui->reset_game_list_cache, &QPushButton::pressed, this, &ConfigureFilesystem::ResetMetadata); - connect(ui->gamecard_inserted, &QCheckBox::stateChanged, this, + connect(ui->gamecard_inserted, &QCheckBox::STATE_CHANGED, this, &ConfigureFilesystem::UpdateEnabledControls); - connect(ui->gamecard_current_game, &QCheckBox::stateChanged, this, + connect(ui->gamecard_current_game, &QCheckBox::STATE_CHANGED, this, &ConfigureFilesystem::UpdateEnabledControls); } diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 327a7dffd2..59aab0ef93 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -146,6 +146,7 @@ + @@ -611,6 +612,14 @@ Show Game &Name + + + true + + + Show &Performance Overlay + + diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 0c6f6f04d0..836aacb0fa 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -7,6 +7,7 @@ #include "common/settings_enums.h" #include "frontend_common/settings_generator.h" #include "qt_common/qt_string_lookup.h" +#include "render/performance_overlay.h" #if defined(QT_STATICPLUGIN) && !defined(__APPLE__) #undef VMA_IMPLEMENTATION #endif @@ -1401,6 +1402,7 @@ void MainWindow::InitializeHotkeys() { LinkActionShortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); LinkActionShortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); 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); @@ -1511,6 +1513,9 @@ void MainWindow::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_perf_overlay.GetValue()); + if (perf_overlay) perf_overlay->setVisible(ui->action_Show_Performance_Overlay->isChecked()); Debugger::ToggleConsole(); } @@ -1630,6 +1635,7 @@ void MainWindow::ConnectMenuEvents() { connect_menu(ui->action_Single_Window_Mode, &MainWindow::ToggleWindowMode); connect_menu(ui->action_Show_Filter_Bar, &MainWindow::OnToggleFilterBar); connect_menu(ui->action_Show_Status_Bar, &MainWindow::OnToggleStatusBar); + connect_menu(ui->action_Show_Performance_Overlay, &MainWindow::OnTogglePerfOverlay); connect_menu(ui->action_Reset_Window_Size_720, &MainWindow::ResetWindowSize720); connect_menu(ui->action_Reset_Window_Size_900, &MainWindow::ResetWindowSize900); @@ -2136,7 +2142,7 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa game_list->hide(); game_list_placeholder->hide(); } - status_bar_update_timer.start(500); + status_bar_update_timer.start(250); renderer_status_button->setDisabled(true); refresh_button->setDisabled(true); @@ -2210,6 +2216,10 @@ bool MainWindow::OnShutdownBegin() { return false; } + perf_overlay->hide(); + perf_overlay->deleteLater(); + perf_overlay = nullptr; + QtCommon::system->SetShuttingDown(true); discord_rpc->Pause(); @@ -3211,6 +3221,13 @@ bool MainWindow::ConfirmShutdownGame() { void MainWindow::OnLoadComplete() { loading_screen->OnLoadComplete(); + + perf_overlay = new PerformanceOverlay(this); + perf_overlay->setVisible(ui->action_Show_Performance_Overlay->isChecked()); + + connect(perf_overlay, &PerformanceOverlay::closed, perf_overlay, [this]() { + ui->action_Show_Performance_Overlay->setChecked(false); + }); } void MainWindow::OnExecuteProgram(std::size_t program_index) { @@ -4032,6 +4049,12 @@ void MainWindow::OnToggleStatusBar() { statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); } +void MainWindow::OnTogglePerfOverlay() { + if (perf_overlay) { + perf_overlay->setVisible(ui->action_Show_Performance_Overlay->isChecked()); + } +} + void MainWindow::OnGameListRefresh() { // Resets metadata cache and reloads QtCommon::Game::ResetMetadata(false); @@ -4256,6 +4279,8 @@ void MainWindow::UpdateStatusBar() { auto& shader_notify = QtCommon::system->GPU().ShaderNotify(); const int shaders_building = shader_notify.ShadersBuilding(); + emit statsUpdated(results, shader_notify); + if (shaders_building > 0) { shader_building_label->setText(tr("Building: %n shader(s)", "", shaders_building)); shader_building_label->setVisible(true); @@ -4350,6 +4375,7 @@ void MainWindow::UpdateStatusButtons() { UpdateVolumeUI(); } +// TODO(crueter): Use this for game list stuff void MainWindow::UpdateUISettings() { if (!ui->action_Fullscreen->isChecked()) { UISettings::values.geometry = saveGeometry(); @@ -4360,6 +4386,8 @@ void MainWindow::UpdateUISettings() { UISettings::values.fullscreen = ui->action_Fullscreen->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_perf_overlay = ui->action_Show_Performance_Overlay->isChecked(); + UISettings::values.first_start = false; Settings::values.enable_overlay = ui->action_Enable_Overlay_Applet->isChecked(); @@ -4588,6 +4616,15 @@ void MainWindow::closeEvent(QCloseEvent* event) { QWidget::closeEvent(event); } +void MainWindow::resizeEvent(QResizeEvent* event) { + emit sizeChanged(event->size()); +} + +void MainWindow::moveEvent(QMoveEvent* event) { + auto window_frame_height = frameGeometry().height() - geometry().height(); + emit positionChanged(event->pos() - QPoint{0, window_frame_height}); +} + static bool IsSingleFileDropEvent(const QMimeData* mime) { return mime->hasUrls() && mime->urls().length() == 1; } diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index 2b2e3108da..4ebfdbcb1e 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -55,6 +55,7 @@ class QProgressDialog; class QSlider; class QHBoxLayout; class WaitTreeWidget; +class PerformanceOverlay; enum class GameListOpenTarget; enum class DumpRomFSTarget; class GameListPlaceholder; @@ -70,6 +71,9 @@ enum class StartGameType { Global, // Only uses global configuration }; +namespace VideoCore { +class ShaderNotify; +} namespace Core { enum class SystemResultStatus : u32; } // namespace Core @@ -214,6 +218,9 @@ signals: void WebBrowserClosed(Service::AM::Frontend::WebExitReason exit_reason, std::string last_url); void SigInterrupt(); + void sizeChanged(const QSize &size); + void positionChanged(const QPoint &pos); + void statsUpdated(const Core::PerfStatsResults &results, const VideoCore::ShaderNotify &shaders); public slots: void OnLoadComplete(); @@ -310,6 +317,8 @@ private: void RequestGameExit(); void changeEvent(QEvent* event) override; void closeEvent(QCloseEvent* event) override; + void resizeEvent(QResizeEvent *event) override; + void moveEvent(QMoveEvent *event) override; std::string CreateTASFramesString( std::array frames) const; @@ -392,6 +401,7 @@ private slots: void OnDataDialog(); void OnToggleFilterBar(); void OnToggleStatusBar(); + void OnTogglePerfOverlay(); void OnGameListRefresh(); void InitializeHotkeys(); void ToggleFullscreen(); @@ -493,6 +503,7 @@ private: LoadingScreen* loading_screen = nullptr; QTimer shutdown_timer; OverlayDialog* shutdown_dialog{}; + PerformanceOverlay *perf_overlay = nullptr; GameListPlaceholder* game_list_placeholder = nullptr; diff --git a/src/yuzu/render/performance_overlay.cpp b/src/yuzu/render/performance_overlay.cpp new file mode 100644 index 0000000000..13003dce7b --- /dev/null +++ b/src/yuzu/render/performance_overlay.cpp @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/perf_stats.h" +#include "performance_overlay.h" +#include "ui_performance_overlay.h" + +#include "main_window.h" + +#include +#include +#include +#include +#include +#include +#include + +// TODO(crueter): Reset samples when user changes turbo, slow, etc. +PerformanceOverlay::PerformanceOverlay(MainWindow* parent) + : QWidget(parent), m_mainWindow{parent}, ui(new Ui::PerformanceOverlay) { + ui->setupUi(this); + + setAttribute(Qt::WA_TranslucentBackground); + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); + raise(); + + // chart setup + m_fpsSeries = new QLineSeries(this); + + QPen pen(Qt::red); + pen.setWidth(2); + m_fpsSeries->setPen(pen); + + m_fpsChart = new QChart; + m_fpsChart->addSeries(m_fpsSeries); + m_fpsChart->legend()->hide(); + m_fpsChart->setBackgroundBrush(Qt::black); + m_fpsChart->setBackgroundVisible(true); + m_fpsChart->layout()->setContentsMargins(2, 2, 2, 2); + m_fpsChart->setMargins(QMargins{4, 4, 4, 4}); + + // axes + m_fpsX = new QValueAxis(this); + m_fpsX->setRange(0, NUM_FPS_SAMPLES); + m_fpsX->setVisible(false); + + m_fpsY = new QValueAxis(this); + m_fpsY->setRange(0, 60); + m_fpsY->setLabelFormat(QStringLiteral("%d")); + m_fpsY->setLabelsColor(Qt::white); + + QFont axisFont = m_fpsY->labelsFont(); + axisFont.setPixelSize(10); + m_fpsY->setLabelsFont(axisFont); + m_fpsY->setTickCount(3); + + // gray-ish label w/ white lines + m_fpsY->setLabelsVisible(true); + m_fpsY->setGridLineColor(QColor(50, 50, 50)); + m_fpsY->setLinePenColor(Qt::white); + + m_fpsChart->addAxis(m_fpsX, Qt::AlignBottom); + m_fpsChart->addAxis(m_fpsY, Qt::AlignLeft); + m_fpsSeries->attachAxis(m_fpsX); + m_fpsSeries->attachAxis(m_fpsY); + + // chart view + m_fpsChartView = new QChartView(m_fpsChart, this); + m_fpsChartView->setRenderHint(QPainter::Antialiasing); + m_fpsChartView->setMinimumHeight(100); + + ui->verticalLayout->addWidget(m_fpsChartView, 1); + + // thanks Debian. + QFont font = ui->fps->font(); + font.setWeight(QFont::DemiBold); + + ui->fps->setFont(font); + ui->frametime->setFont(font); + + // pos/stats + resetPosition(m_mainWindow->pos()); + connect(parent, &MainWindow::positionChanged, this, &PerformanceOverlay::resetPosition); + connect(m_mainWindow, &MainWindow::statsUpdated, this, &PerformanceOverlay::updateStats); +} + +PerformanceOverlay::~PerformanceOverlay() { + delete ui; +} + +void PerformanceOverlay::resetPosition(const QPoint& _) { + auto pos = m_mainWindow->pos(); + move(pos.x() + m_offset.x(), pos.y() + m_offset.y()); +} + +void PerformanceOverlay::updateStats(const Core::PerfStatsResults& results, + const VideoCore::ShaderNotify& shaders) { + auto fps = results.average_game_fps; + if (!std::isnan(fps)) { + // don't sample measurements < 3 fps because they are probably outliers or freezes + static constexpr double FPS_SAMPLE_THRESHOLD = 3.0; + + QString fpsText = tr("%1 fps").arg(std::round(fps), 0, 'f', 0); + // if (!m_fpsSuffix.isEmpty()) fpsText = fpsText % QStringLiteral(" (%1)").arg(m_fpsSuffix); + ui->fps->setText(fpsText); + + // sampling + if (fps > FPS_SAMPLE_THRESHOLD) { + m_fpsSamples.push_back(fps); + m_fpsPoints.push_back(QPointF{m_xPos++, fps}); + } + + if (m_fpsSamples.size() > NUM_FPS_SAMPLES) { + m_fpsSamples.pop_front(); + m_fpsPoints.pop_front(); + } + + // For the average only go back 10 samples max + if (m_fpsSamples.size() >= 2) { + const int back_search = std::min(size_t(10), m_fpsSamples.size() - 1); + double sum = std::accumulate(m_fpsSamples.end() - back_search, m_fpsSamples.end(), 0.0); + double avg = sum / back_search; + + ui->fps_avg->setText(tr("Avg: %1").arg(avg, 0, 'f', 0)); + } + + // chart it :) + if (!m_fpsPoints.empty()) { + auto [min_it, max_it] = std::minmax_element(m_fpsSamples.begin(), m_fpsSamples.end()); + double min_fps = *min_it; + double max_fps = *max_it; + + ui->fps_min->setText(tr("Min: %1").arg(min_fps, 0, 'f', 0)); + ui->fps_max->setText(tr("Max: %1").arg(max_fps, 0, 'f', 0)); + + m_fpsSeries->replace(QList(m_fpsPoints.begin(), m_fpsPoints.end())); + + qreal x_min = std::max(0.0, m_xPos - NUM_FPS_SAMPLES); + qreal x_max = std::max(qreal(10), m_xPos); + m_fpsX->setRange(x_min, x_max); + m_fpsY->setRange(0.0, max_fps); + } + } + + auto ft = results.frametime; + if (!std::isnan(ft)) { + // don't sample measurements > 500 ms because they are probably outliers + static constexpr double FT_SAMPLE_THRESHOLD = 500.0; + + double ft_ms = results.frametime * 1000.0; + ui->frametime->setText(tr("%1 ms").arg(ft_ms, 0, 'f', 2)); + + // sampling + if (ft_ms <= FT_SAMPLE_THRESHOLD) + m_frametimeSamples.push_back(ft_ms); + + if (m_frametimeSamples.size() > NUM_FRAMETIME_SAMPLES) + m_frametimeSamples.pop_front(); + + if (!m_frametimeSamples.empty()) { + auto [min_it, max_it] = + std::minmax_element(m_frametimeSamples.begin(), m_frametimeSamples.end()); + ui->ft_min->setText(tr("Min: %1").arg(*min_it, 0, 'f', 1)); + ui->ft_max->setText(tr("Max: %1").arg(*max_it, 0, 'f', 1)); + } + + // For the average only go back 10 samples max + if (m_frametimeSamples.size() >= 2) { + const int back_search = std::min(size_t(10), m_frametimeSamples.size() - 1); + double sum = std::accumulate(m_frametimeSamples.end() - back_search, + m_frametimeSamples.end(), 0.0); + double avg = sum / back_search; + + ui->ft_avg->setText(tr("Avg: %1").arg(avg, 0, 'f', 1)); + } + } +} + +void PerformanceOverlay::paintEvent(QPaintEvent* event) { + QPainter painter(this); + + painter.setRenderHint(QPainter::Antialiasing); + + painter.setBrush(m_background); + painter.setPen(Qt::NoPen); + + painter.drawRoundedRect(rect(), 10.0, 10.0); +} + +void PerformanceOverlay::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton) { + m_drag_start_pos = event->pos(); + } +} + +void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) { + // drag + if (event->buttons() & Qt::LeftButton) { + QPoint new_global_pos = event->globalPosition().toPoint() - m_drag_start_pos; + m_offset = new_global_pos - m_mainWindow->pos(); + move(new_global_pos); + } +} + +void PerformanceOverlay::closeEvent(QCloseEvent* event) { + emit closed(); +} diff --git a/src/yuzu/render/performance_overlay.h b/src/yuzu/render/performance_overlay.h new file mode 100644 index 0000000000..1d7fb46f26 --- /dev/null +++ b/src/yuzu/render/performance_overlay.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace VideoCore { +class ShaderNotify; +} +namespace Core { +struct PerfStatsResults; +} +namespace Ui { +class PerformanceOverlay; +} + +class QLineSeries; +class QChart; +class QChartView; +class QValueAxis; +class MainWindow; + +class PerformanceOverlay : public QWidget { + Q_OBJECT + +public: + explicit PerformanceOverlay(MainWindow* parent = nullptr); + ~PerformanceOverlay(); + +protected: + void paintEvent(QPaintEvent* event) override; + + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void closeEvent(QCloseEvent *event) override; + +private: + void resetPosition(const QPoint& pos); + void updateStats(const Core::PerfStatsResults &results, const VideoCore::ShaderNotify &shaders); + + MainWindow *m_mainWindow = nullptr; + Ui::PerformanceOverlay* ui; + + // colors + QColor m_background{127, 127, 127, 190}; + + QPoint m_offset{25, 75}; + + // frametime + const size_t NUM_FRAMETIME_SAMPLES = 300; + std::deque m_frametimeSamples; + + // fps + const size_t NUM_FPS_SAMPLES = 120; + qreal m_xPos = 0; + std::deque m_fpsSamples; + std::deque m_fpsPoints; + + // drag + QPoint m_drag_start_pos; + + // fps chart + QLineSeries *m_fpsSeries = nullptr; + QChart *m_fpsChart = nullptr; + QChartView *m_fpsChartView = nullptr; + QValueAxis *m_fpsX = nullptr; + QValueAxis *m_fpsY = nullptr; + +signals: + void closed(); +}; diff --git a/src/yuzu/render/performance_overlay.ui b/src/yuzu/render/performance_overlay.ui new file mode 100644 index 0000000000..215a80220d --- /dev/null +++ b/src/yuzu/render/performance_overlay.ui @@ -0,0 +1,181 @@ + + + PerformanceOverlay + + + + 0 + 0 + 225 + 250 + + + + Form + + + + + + + + + Sans Serif + 12 + true + + + + color: #0000ff; + + + Frametime + + + + + + + + Sans Serif + 12 + + + + 0 ms + + + + + + + + + + + + Sans Serif + 10 + false + + + + Min: 0 + + + + + + + + Sans Serif + 10 + false + + + + Max: 0 + + + + + + + + Sans Serif + 10 + false + + + + Avg: 0 + + + + + + + + + + + + Sans Serif + 12 + true + + + + color: #ff0000; + + + FPS + + + + + + + + Sans Serif + 12 + + + + 0 fps + + + + + + + + + + + + Sans Serif + 10 + false + + + + Min: 0 + + + + + + + + Sans Serif + 10 + false + + + + Max: 0 + + + + + + + + Sans Serif + 10 + false + + + + Avg: 0 + + + + + + + + + +