From fbfffd9b847d3510042df1059680dbcb51239e79 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:23:21 +0900 Subject: [PATCH] [UI] Switch from QWidget to QWindow and ImGui in the game process Re-implement all the game overlays in imgui and remove unused Qt ones. Qt overlays (and even the multimedia playback which is currently still there) interfere with d3d12 rendering in hard to track down ways, so moving back to imgui and closer to native render window implementation without the hacks that were needed previously. --- src/xenia/app/emulator_window.cc | 313 ++++--- src/xenia/app/emulator_window.h | 18 +- src/xenia/kernel/xam/achievement_manager.cc | 22 +- src/xenia/kernel/xam/xam_ui.cc | 97 ++- src/xenia/kernel/xam/xam_ui.h | 60 +- src/xenia/ui/audio_helper_qt.cc | 82 ++ src/xenia/ui/audio_helper_qt.h | 50 ++ src/xenia/ui/confirm_dialog_widget_qt.cc | 200 ----- src/xenia/ui/confirm_dialog_widget_qt.h | 63 -- src/xenia/ui/controller_hotkeys_dialog_qt.cc | 155 ---- src/xenia/ui/controller_hotkeys_dialog_qt.h | 49 -- src/xenia/ui/game_list_dialog_qt.cc | 46 +- src/xenia/ui/imgui_confirm_dialog.cc | 213 +++++ src/xenia/ui/imgui_confirm_dialog.h | 63 ++ src/xenia/ui/imgui_context_menu.cc | 266 ++++++ src/xenia/ui/imgui_context_menu.h | 89 ++ .../ui/imgui_controller_hotkeys_dialog.cc | 96 +++ .../ui/imgui_controller_hotkeys_dialog.h | 51 ++ src/xenia/ui/imgui_drawer.cc | 13 +- src/xenia/ui/imgui_drawer.h | 3 + src/xenia/ui/imgui_gamepad_dialog.cc | 79 ++ src/xenia/ui/imgui_gamepad_dialog.h | 57 ++ src/xenia/ui/imgui_performance_dialog.cc | 314 +++++++ src/xenia/ui/imgui_performance_dialog.h | 74 ++ src/xenia/ui/imgui_postprocessing_dialog.cc | 356 ++++++++ src/xenia/ui/imgui_postprocessing_dialog.h | 77 ++ src/xenia/ui/imgui_xmp_dialog.cc | 176 ++++ src/xenia/ui/imgui_xmp_dialog.h | 51 ++ src/xenia/ui/performance_tuning_dialog_qt.cc | 422 --------- src/xenia/ui/performance_tuning_dialog_qt.h | 76 -- src/xenia/ui/postprocessing_dialog_qt.cc | 801 ------------------ src/xenia/ui/postprocessing_dialog_qt.h | 112 --- src/xenia/ui/profile_dialogs.cc | 14 +- src/xenia/ui/profile_dialogs.h | 23 +- src/xenia/ui/window_qt.cc | 545 +++++++----- src/xenia/ui/window_qt.h | 42 +- src/xenia/ui/xmp_dialog_qt.cc | 299 ------- src/xenia/ui/xmp_dialog_qt.h | 64 -- 38 files changed, 2854 insertions(+), 2677 deletions(-) create mode 100644 src/xenia/ui/audio_helper_qt.cc create mode 100644 src/xenia/ui/audio_helper_qt.h delete mode 100644 src/xenia/ui/confirm_dialog_widget_qt.cc delete mode 100644 src/xenia/ui/confirm_dialog_widget_qt.h delete mode 100644 src/xenia/ui/controller_hotkeys_dialog_qt.cc delete mode 100644 src/xenia/ui/controller_hotkeys_dialog_qt.h create mode 100644 src/xenia/ui/imgui_confirm_dialog.cc create mode 100644 src/xenia/ui/imgui_confirm_dialog.h create mode 100644 src/xenia/ui/imgui_context_menu.cc create mode 100644 src/xenia/ui/imgui_context_menu.h create mode 100644 src/xenia/ui/imgui_controller_hotkeys_dialog.cc create mode 100644 src/xenia/ui/imgui_controller_hotkeys_dialog.h create mode 100644 src/xenia/ui/imgui_gamepad_dialog.cc create mode 100644 src/xenia/ui/imgui_gamepad_dialog.h create mode 100644 src/xenia/ui/imgui_performance_dialog.cc create mode 100644 src/xenia/ui/imgui_performance_dialog.h create mode 100644 src/xenia/ui/imgui_postprocessing_dialog.cc create mode 100644 src/xenia/ui/imgui_postprocessing_dialog.h create mode 100644 src/xenia/ui/imgui_xmp_dialog.cc create mode 100644 src/xenia/ui/imgui_xmp_dialog.h delete mode 100644 src/xenia/ui/performance_tuning_dialog_qt.cc delete mode 100644 src/xenia/ui/performance_tuning_dialog_qt.h delete mode 100644 src/xenia/ui/postprocessing_dialog_qt.cc delete mode 100644 src/xenia/ui/postprocessing_dialog_qt.h delete mode 100644 src/xenia/ui/xmp_dialog_qt.cc delete mode 100644 src/xenia/ui/xmp_dialog_qt.h diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index f0664c3bf..f92a6eb36 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -34,17 +34,16 @@ #include "xenia/base/threading.h" #include "xenia/config.h" #include "xenia/ui/config_dialog_qt.h" -#include "xenia/ui/confirm_dialog_widget_qt.h" -#include "xenia/ui/context_menu_widget_qt.h" -#include "xenia/ui/controller_hotkeys_dialog_qt.h" #include "xenia/ui/game_list_dialog_qt.h" -#include "xenia/ui/notification_widget_qt.h" -#include "xenia/ui/performance_tuning_dialog_qt.h" -#include "xenia/ui/postprocessing_dialog_qt.h" -#include "xenia/ui/profile_dialog_qt.h" +#include "xenia/ui/imgui_confirm_dialog.h" +#include "xenia/ui/imgui_context_menu.h" +#include "xenia/ui/imgui_controller_hotkeys_dialog.h" +#include "xenia/ui/imgui_performance_dialog.h" +#include "xenia/ui/imgui_postprocessing_dialog.h" +#include "xenia/ui/imgui_xmp_dialog.h" +#include "xenia/ui/profile_dialogs.h" #include "xenia/ui/qt_util.h" #include "xenia/ui/simple_config_dialog_qt.h" -#include "xenia/ui/xmp_dialog_qt.h" #if XE_PLATFORM_WIN32 #include @@ -82,6 +81,25 @@ #ifdef Bool #undef Bool #endif +// Undefine X11 macros that conflict with Qt's QEvent enum +#ifdef KeyPress +#undef KeyPress +#endif +#ifdef KeyRelease +#undef KeyRelease +#endif +#ifdef FocusIn +#undef FocusIn +#endif +#ifdef FocusOut +#undef FocusOut +#endif +#ifdef FontChange +#undef FontChange +#endif +#ifdef Expose +#undef Expose +#endif #endif #include "xenia/cpu/processor.h" @@ -1111,84 +1129,76 @@ void EmulatorWindow::OnMouseDown(const ui::MouseEvent& e) { void EmulatorWindow::ToggleContextMenu(bool use_cursor_position) { // If menu is already open, close it instead of opening a new one - if (context_menu_widget_qt_) { - context_menu_widget_qt_->close(); - context_menu_widget_qt_ = nullptr; + if (context_menu_) { + context_menu_->CloseMenu(); + context_menu_ = nullptr; return; } - // Show context menu - auto* qt_window = dynamic_cast(window_.get()); - if (qt_window) { - // Get input system for gamepad navigation support - auto input_sys = emulator()->input_system(); + // Get input system for gamepad navigation support + auto input_sys = emulator()->input_system(); - // Create new menu widget each time - just like notification widget - auto* context_menu = - new ContextMenuWidgetQt(qt_window->qwindow(), input_sys); - context_menu_widget_qt_ = context_menu; + // Create new ImGui context menu + auto* context_menu = new ui::ImGuiContextMenu(imgui_drawer(), input_sys); + context_menu_ = context_menu; - context_menu->AddAction( - window_->IsFullscreen() ? "Exit Fullscreen" : "Fullscreen", - [this]() { ToggleFullscreen(); }, "F11"); + // Set callback to clear our pointer when menu closes + context_menu->SetOnCloseCallback([this]() { context_menu_ = nullptr; }); - context_menu->AddSeparator(); + context_menu->AddAction( + window_->IsFullscreen() ? "Exit Fullscreen" : "Fullscreen", + [this]() { ToggleFullscreen(); }, "F11"); - context_menu->AddAction( - "Post-Processing...", [this]() { ToggleDisplayConfigDialog(); }, "F6"); + context_menu->AddSeparator(); - context_menu->AddAction( - "Performance Settings...", - [this]() { TogglePerformanceTuningDialog(); }, "F7"); + context_menu->AddAction( + "Post-Processing...", [this]() { ToggleDisplayConfigDialog(); }, "F6"); - // Get current vibration state - bool vibration_enabled = false; - if (input_sys) { - vibration_enabled = input_sys->GetVibrationCvar(); - } + context_menu->AddAction( + "Performance Settings...", [this]() { TogglePerformanceTuningDialog(); }, + "F7"); - QString vibration_text = - QString("Vibration: %1").arg(vibration_enabled ? "On" : "Off"); - context_menu->AddAction(vibration_text, - [this]() { ToggleControllerVibration(); }); - - context_menu->AddAction("Controller Hotkeys...", - [this]() { ToggleControllerHotkeysDialog(); }); - - context_menu->AddSeparator(); - - context_menu->AddAction( - "Take Screenshot", [this]() { TakeScreenshot(); }, "F12"); - - context_menu->AddAction("Profiles Menu", - [this]() { ToggleProfilesConfigDialog(); }); - - context_menu->AddAction("XMP Audio Player", - [this]() { ToggleXMPConfigDialog(); }); - - context_menu->AddSeparator(); - - context_menu->AddAction("Quit Game", [this, qt_window, input_sys]() { - if (ConfirmDialogWidgetQt::Confirm( - qt_window->qwindow(), input_sys, "Quit Game", - "Are you sure you want to quit?\n\nAny unsaved progress will be " - "lost.")) { - window_->RequestClose(); - } - }); - - // Show menu at cursor position or center of window - QPoint global_pos; - if (use_cursor_position) { - global_pos = QCursor::pos(); - } else { - // Center of window - QWidget* qwindow = qt_window->qwindow(); - global_pos = qwindow->mapToGlobal( - QPoint(qwindow->width() / 2, qwindow->height() / 2)); - } - context_menu->ShowAt(global_pos); + // Get current vibration state + bool vibration_enabled = false; + if (input_sys) { + vibration_enabled = input_sys->GetVibrationCvar(); } + + std::string vibration_text = + std::string("Vibration: ") + (vibration_enabled ? "On" : "Off"); + context_menu->AddAction(vibration_text, + [this]() { ToggleControllerVibration(); }); + + context_menu->AddAction("Controller Hotkeys...", + [this]() { ToggleControllerHotkeysDialog(); }); + + context_menu->AddSeparator(); + + context_menu->AddAction( + "Take Screenshot", [this]() { TakeScreenshot(); }, "F12"); + + context_menu->AddAction("Profiles Menu", + [this]() { ToggleProfilesConfigDialog(); }); + + context_menu->AddAction("XMP Audio Player", + [this]() { ToggleXMPConfigDialog(); }); + + context_menu->AddSeparator(); + + context_menu->AddAction("Quit Game", [this, input_sys]() { + new ui::ImGuiConfirmDialog( + imgui_drawer(), "Quit Game", + "Are you sure you want to quit?\n\nAny unsaved progress will be lost.", + [this](bool confirmed) { + if (confirmed) { + window_->RequestClose(); + } + }, + input_sys); + }); + + // Show menu centered (ImGui handles positioning) + context_menu->Show(); } void EmulatorWindow::OnMouseUp(const ui::MouseEvent& e) { @@ -1243,12 +1253,9 @@ void EmulatorWindow::ExportScreenshot(const xe::ui::RawImage& image) { fmt::format("Screenshot saved: {}", filename); app_context_.CallInUIThread([this, notification_text]() { - auto* qt_window = dynamic_cast(window_.get()); - if (qt_window) { - auto* notification = - new NotificationWidgetQt(qt_window->qwindow(), "Screenshot Created!", - SafeQString(notification_text), 3000); - notification->Show(); + if (imgui_drawer()) { + new ui::HostNotificationWindow(imgui_drawer(), "Screenshot Created!", + notification_text, 0); } }); } @@ -1658,56 +1665,46 @@ void EmulatorWindow::ToggleFullscreen() { } void EmulatorWindow::ToggleDisplayConfigDialog() { - auto* qt_window = dynamic_cast(window_.get()); - if (!qt_window) { + if (postprocessing_dialog_) { + postprocessing_dialog_->CloseDialog(); + postprocessing_dialog_ = nullptr; return; } - if (postprocessing_dialog_qt_) { - postprocessing_dialog_qt_->close(); - return; - } - - postprocessing_dialog_qt_ = new PostProcessingDialogQt( - qt_window->qwindow(), this, emulator()->input_system()); - postprocessing_dialog_qt_->show(); - postprocessing_dialog_qt_->raise(); - postprocessing_dialog_qt_->activateWindow(); + postprocessing_dialog_ = new ui::ImGuiPostProcessingDialog( + imgui_drawer(), this, emulator()->input_system()); + postprocessing_dialog_->SetOnCloseCallback( + [this]() { postprocessing_dialog_ = nullptr; }); } void EmulatorWindow::TogglePerformanceTuningDialog() { - auto* qt_window = dynamic_cast(window_.get()); - if (!qt_window) { + if (performance_dialog_) { + performance_dialog_->CloseDialog(); + performance_dialog_ = nullptr; return; } - if (performance_tuning_dialog_qt_) { - performance_tuning_dialog_qt_->close(); - return; - } - - performance_tuning_dialog_qt_ = new PerformanceTuningDialogQt( - qt_window->qwindow(), this, emulator()->input_system()); - performance_tuning_dialog_qt_->show(); - performance_tuning_dialog_qt_->raise(); - performance_tuning_dialog_qt_->activateWindow(); + performance_dialog_ = new ui::ImGuiPerformanceDialog( + imgui_drawer(), this, emulator()->input_system()); + performance_dialog_->SetOnCloseCallback( + [this]() { performance_dialog_ = nullptr; }); } void EmulatorWindow::ToggleProfilesConfigDialog() { - if (!profile_dialog_qt_) { - profile_dialog_qt_ = - new ProfileDialogQt(nullptr, this, emulator()->input_system()); - // Refresh game list icons when profile dialog closes - QObject::connect(profile_dialog_qt_, &QDialog::finished, [this]() { - if (game_list_dialog_qt_) { - game_list_dialog_qt_->RefreshIcons(); - } - }); + if (profile_dialog_) { + profile_dialog_->CloseDialog(); + return; } - profile_dialog_qt_->show(); - profile_dialog_qt_->raise(); - profile_dialog_qt_->activateWindow(); + profile_dialog_ = + new ProfileConfigDialog(imgui_drawer(), this, emulator()->input_system()); + profile_dialog_->SetOnCloseCallback([this]() { + profile_dialog_ = nullptr; + // Refresh game list icons when profile dialog closes + if (game_list_dialog_qt_) { + game_list_dialog_qt_->RefreshIcons(); + } + }); } void EmulatorWindow::ToggleConfigDialog() { @@ -1737,21 +1734,15 @@ void EmulatorWindow::OpenConfigDialog(const std::string& category) { } void EmulatorWindow::ToggleXMPConfigDialog() { - auto* qt_window = dynamic_cast(window_.get()); - if (!qt_window) { + if (xmp_dialog_) { + xmp_dialog_->CloseDialog(); + xmp_dialog_ = nullptr; return; } - if (xmp_dialog_qt_) { - xmp_dialog_qt_->close(); - return; - } - - xmp_dialog_qt_ = - new XmpDialogQt(qt_window->qwindow(), this, emulator()->input_system()); - xmp_dialog_qt_->show(); - xmp_dialog_qt_->raise(); - xmp_dialog_qt_->activateWindow(); + xmp_dialog_ = + new ui::ImGuiXmpDialog(imgui_drawer(), this, emulator()->input_system()); + xmp_dialog_->SetOnCloseCallback([this]() { xmp_dialog_ = nullptr; }); } void EmulatorWindow::ToggleControllerVibration() { @@ -1772,15 +1763,13 @@ void EmulatorWindow::ToggleControllerVibration() { // Show notification bool vibration_enabled = input_sys->GetVibrationCvar(); - QString status = vibration_enabled ? "On" : "Off"; + std::string status = vibration_enabled ? "On" : "Off"; app_context_.CallInUIThread([this, status]() { - auto* qt_window = dynamic_cast(window_.get()); - if (qt_window) { - auto* notification = new NotificationWidgetQt( - qt_window->qwindow(), "Controller Vibration", - QString("Vibration is now %1").arg(status), 2000); - notification->Show(); + if (imgui_drawer()) { + new ui::HostNotificationWindow( + imgui_drawer(), "Controller Vibration", + fmt::format("Vibration is now {}", status), 0); } }); } @@ -2169,12 +2158,9 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey( if (!notificationTitle.empty()) { app_context_.CallInUIThread([this, notificationTitle, notificationDesc]() { - auto* qt_window = dynamic_cast(window_.get()); - if (qt_window) { - auto* notification = new NotificationWidgetQt( - qt_window->qwindow(), SafeQString(notificationTitle), - SafeQString(notificationDesc), 3000); - notification->Show(); + if (imgui_drawer()) { + new ui::HostNotificationWindow(imgui_drawer(), notificationTitle, + notificationDesc, 0); } }); } @@ -2313,21 +2299,15 @@ void EmulatorWindow::CycleReadbackResolve() { } void EmulatorWindow::ToggleControllerHotkeysDialog() { - auto* qt_window = dynamic_cast(window_.get()); - if (!qt_window) { + if (controller_hotkeys_dialog_) { + controller_hotkeys_dialog_->CloseDialog(); return; } - if (controller_hotkeys_dialog_qt_) { - controller_hotkeys_dialog_qt_->close(); - return; - } - - controller_hotkeys_dialog_qt_ = new ControllerHotkeysDialogQt( - qt_window->qwindow(), this, emulator()->input_system()); - controller_hotkeys_dialog_qt_->show(); - controller_hotkeys_dialog_qt_->raise(); - controller_hotkeys_dialog_qt_->activateWindow(); + controller_hotkeys_dialog_ = new ui::ImGuiControllerHotkeysDialog( + imgui_drawer(), this, emulator()->input_system()); + controller_hotkeys_dialog_->SetOnCloseCallback( + [this]() { controller_hotkeys_dialog_ = nullptr; }); } std::vector> @@ -2546,10 +2526,6 @@ xe::X_STATUS EmulatorWindow::RunTitle( disable_hotkeys_ = false; - if (postprocessing_dialog_qt_) { - postprocessing_dialog_qt_->deleteLater(); - } - ClearDialogs(); if (result) { @@ -2698,8 +2674,25 @@ void EmulatorWindow::AddRecentlyLaunchedTitle( } void EmulatorWindow::ClearDialogs() { - if (postprocessing_dialog_qt_) { - postprocessing_dialog_qt_->deleteLater(); + if (postprocessing_dialog_) { + postprocessing_dialog_->CloseDialog(); + postprocessing_dialog_ = nullptr; + } + if (performance_dialog_) { + performance_dialog_->CloseDialog(); + performance_dialog_ = nullptr; + } + if (controller_hotkeys_dialog_) { + controller_hotkeys_dialog_->CloseDialog(); + controller_hotkeys_dialog_ = nullptr; + } + if (xmp_dialog_) { + xmp_dialog_->CloseDialog(); + xmp_dialog_ = nullptr; + } + if (profile_dialog_) { + profile_dialog_->CloseDialog(); + profile_dialog_ = nullptr; } imgui_drawer_.get()->ClearDialogs(); diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index a2940b58f..53601f7de 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -19,8 +19,14 @@ class QTimer; #include "xenia/emulator.h" #include "xenia/gpu/command_processor.h" +#include "xenia/ui/imgui_confirm_dialog.h" +#include "xenia/ui/imgui_context_menu.h" +#include "xenia/ui/imgui_controller_hotkeys_dialog.h" #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/imgui_drawer.h" +#include "xenia/ui/imgui_performance_dialog.h" +#include "xenia/ui/imgui_postprocessing_dialog.h" +#include "xenia/ui/imgui_xmp_dialog.h" #include "xenia/ui/immediate_drawer.h" #include "xenia/ui/menu_item.h" #include "xenia/ui/presenter.h" @@ -271,15 +277,15 @@ class EmulatorWindow { // Disc number after disc swap (0 = use XEX header value) uint8_t swapped_disc_number_ = 0; - QPointer postprocessing_dialog_qt_; - QPointer performance_tuning_dialog_qt_; - QPointer controller_hotkeys_dialog_qt_; + ui::ImGuiPostProcessingDialog* postprocessing_dialog_ = nullptr; + ui::ImGuiPerformanceDialog* performance_dialog_ = nullptr; + ui::ImGuiControllerHotkeysDialog* controller_hotkeys_dialog_ = nullptr; QPointer game_list_dialog_qt_; - QPointer profile_dialog_qt_; + ProfileConfigDialog* profile_dialog_ = nullptr; QPointer simple_config_dialog_qt_; QPointer config_dialog_qt_; - QPointer context_menu_widget_qt_; - QPointer xmp_dialog_qt_; + ui::ImGuiContextMenu* context_menu_ = nullptr; + ui::ImGuiXmpDialog* xmp_dialog_ = nullptr; std::vector recently_launched_titles_; diff --git a/src/xenia/kernel/xam/achievement_manager.cc b/src/xenia/kernel/xam/achievement_manager.cc index f485404ca..6a6aca664 100644 --- a/src/xenia/kernel/xam/achievement_manager.cc +++ b/src/xenia/kernel/xam/achievement_manager.cc @@ -14,8 +14,8 @@ #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xam/achievement_backends/gpd_achievement_backend.h" #include "xenia/kernel/xam/xdbf/gpd_info.h" -#include "xenia/ui/notification_widget_qt.h" -#include "xenia/ui/window_qt.h" +#include "xenia/ui/audio_helper_qt.h" +#include "xenia/ui/imgui_guest_notification.h" DEFINE_bool(show_achievement_notification, true, "Show achievement notification on screen.", "UI"); @@ -149,16 +149,16 @@ void AchievementManager::ShowAchievementEarnedNotification( const Emulator* emulator = kernel_state()->emulator(); ui::WindowedAppContext& app_context = emulator->display_window()->app_context(); + ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); - app_context.CallInUIThread([emulator, description]() { - auto* qt_window = dynamic_cast(emulator->display_window()); - if (qt_window) { - auto* notification = new app::NotificationWidgetQt( - qt_window->qwindow(), QString("Achievement unlocked"), - QString::fromUtf8(description.c_str()), 4500, - true); // is_achievement = true, plays sound if configured - notification->Show(); - } + app_context.CallInUIThread([imgui_drawer, description]() { + // Play achievement sound + ui::AudioHelperQt::Instance().PlayAchievementSound(); + + // Show notification + new ui::AchievementNotificationWindow( + imgui_drawer, "Achievement unlocked", description, 0, + kernel_state()->notification_position_); }); } diff --git a/src/xenia/kernel/xam/xam_ui.cc b/src/xenia/kernel/xam/xam_ui.cc index b6981bcd0..c0e5bc897 100644 --- a/src/xenia/kernel/xam/xam_ui.cc +++ b/src/xenia/kernel/xam/xam_ui.cc @@ -234,7 +234,26 @@ X_RESULT xeXamDispatchHeadlessAsync(std::function run_callback) { return X_ERROR_SUCCESS; } +void MessageBoxDialog::OnGamepadButtonA() { + chosen_button_ = focused_button_; + Close(); +} + +void MessageBoxDialog::OnGamepadDPadLeft() { + if (focused_button_ > 0) { + focused_button_--; + } +} + +void MessageBoxDialog::OnGamepadDPadRight() { + if (focused_button_ < buttons_.size() - 1) { + focused_button_++; + } +} + void MessageBoxDialog::OnDraw(ImGuiIO& io) { + PollGamepad(); + bool first_draw = false; if (!has_opened_) { ImGui::OpenPopup(title_.c_str()); @@ -250,11 +269,20 @@ void MessageBoxDialog::OnDraw(ImGuiIO& io) { ImGui::SetKeyboardFocusHere(); } for (size_t i = 0; i < buttons_.size(); ++i) { + // Highlight focused button for gamepad navigation + bool is_focused = (i == focused_button_); + if (is_focused) { + ImGui::PushStyleColor(ImGuiCol_Button, + ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + } if (ImGui::Button(buttons_[i].c_str())) { chosen_button_ = static_cast(i); ImGui::CloseCurrentPopup(); Close(); } + if (is_focused) { + ImGui::PopStyleColor(); + } ImGui::SameLine(); } ImGui::Spacing(); @@ -265,7 +293,34 @@ void MessageBoxDialog::OnDraw(ImGuiIO& io) { } } +void KeyboardInputDialog::OnGamepadButtonA() { + if (focused_button_ == 0) { + // OK + text_ = std::string(text_buffer_.data(), text_buffer_.size()); + cancelled_ = false; + } else { + // Cancel + text_ = ""; + cancelled_ = true; + } + Close(); +} + +void KeyboardInputDialog::OnGamepadDPadLeft() { + if (focused_button_ > 0) { + focused_button_--; + } +} + +void KeyboardInputDialog::OnGamepadDPadRight() { + if (focused_button_ < 1) { + focused_button_++; + } +} + void KeyboardInputDialog::OnDraw(ImGuiIO& io) { + PollGamepad(); + bool first_draw = false; if (!has_opened_) { ImGui::OpenPopup(title_.c_str()); @@ -302,19 +357,36 @@ void KeyboardInputDialog::OnDraw(ImGuiIO& io) { ImGui::CloseCurrentPopup(); Close(); } + // Highlight focused button for gamepad navigation + bool ok_focused = (focused_button_ == 0); + if (ok_focused) { + ImGui::PushStyleColor(ImGuiCol_Button, + ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + } if (ImGui::Button("OK")) { text_ = std::string(text_buffer_.data(), text_buffer_.size()); cancelled_ = false; ImGui::CloseCurrentPopup(); Close(); } + if (ok_focused) { + ImGui::PopStyleColor(); + } ImGui::SameLine(); + bool cancel_focused = (focused_button_ == 1); + if (cancel_focused) { + ImGui::PushStyleColor(ImGuiCol_Button, + ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + } if (ImGui::Button("Cancel")) { text_ = ""; cancelled_ = true; ImGui::CloseCurrentPopup(); Close(); } + if (cancel_focused) { + ImGui::PopStyleColor(); + } ImGui::Spacing(); ImGui::EndPopup(); } else { @@ -361,6 +433,7 @@ static dword_result_t XamShowMessageBoxUi( const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::hid::InputSystem* input_system = emulator->input_system(); if (flags & XMBox_PASSCODEMODE || flags & XMBox_VERIFYPASSCODEMODE) { auto close = [result_ptr, @@ -383,7 +456,7 @@ static dword_result_t XamShowMessageBoxUi( }; result = xeXamDispatchDialog( - new MessageBoxDialog(imgui_drawer, title, text, buttons, + new MessageBoxDialog(imgui_drawer, input_system, title, text, buttons, static_cast(active_button)), close, overlapped); } @@ -492,6 +565,7 @@ dword_result_t XamShowKeyboardUI_entry( }; const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::hid::InputSystem* input_system = emulator->input_system(); std::string title_str = title ? xe::to_utf8(title.value()) : ""; std::string desc_str = description ? xe::to_utf8(description.value()) : ""; @@ -499,8 +573,8 @@ dword_result_t XamShowKeyboardUI_entry( default_text ? xe::to_utf8(default_text.value()) : ""; result = xeXamDispatchDialogEx( - new KeyboardInputDialog(imgui_drawer, title_str, desc_str, def_text_str, - buffer_length), + new KeyboardInputDialog(imgui_drawer, input_system, title_str, desc_str, + def_text_str, buffer_length), close, overlapped); } return result; @@ -556,9 +630,10 @@ dword_result_t XamShowDeviceSelectorUI_entry( const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::hid::InputSystem* input_system = emulator->input_system(); return xeXamDispatchDialog( - new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close, - overlapped); + new MessageBoxDialog(imgui_drawer, input_system, title, desc, buttons, 0), + close, overlapped); } DECLARE_XAM_EXPORT1(XamShowDeviceSelectorUI, kUI, kImplemented); @@ -576,8 +651,9 @@ void XamShowDirtyDiscErrorUI_entry(dword_t user_index) { const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::hid::InputSystem* input_system = emulator->input_system(); xeXamDispatchDialog( - new MessageBoxDialog(imgui_drawer, title, desc, {"OK"}, 0), + new MessageBoxDialog(imgui_drawer, input_system, title, desc, {"OK"}, 0), [](MessageBoxDialog*) -> X_RESULT { return X_ERROR_SUCCESS; }, 0); // This is death, and should never return. // TODO(benvanik): cleaner exit. @@ -745,8 +821,10 @@ dword_result_t XamShowMarketplaceUIEx_entry(dword_t user_index, dword_t ui_type, const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::hid::InputSystem* input_system = emulator->input_system(); return xeXamDispatchDialogAsync( - new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close); + new MessageBoxDialog(imgui_drawer, input_system, title, desc, buttons, 0), + close); } DECLARE_XAM_EXPORT1(XamShowMarketplaceUIEx, kUI, kSketchy); @@ -823,9 +901,10 @@ dword_result_t XamShowMarketplaceDownloadItemsUI_entry( const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); + xe::hid::InputSystem* input_system = emulator->input_system(); return xeXamDispatchDialog( - new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close, - overlapped); + new MessageBoxDialog(imgui_drawer, input_system, title, desc, buttons, 0), + close, overlapped); } DECLARE_XAM_EXPORT1(XamShowMarketplaceDownloadItemsUI, kUI, kSketchy); diff --git a/src/xenia/kernel/xam/xam_ui.h b/src/xenia/kernel/xam/xam_ui.h index 4716826e3..b056750f9 100644 --- a/src/xenia/kernel/xam/xam_ui.h +++ b/src/xenia/kernel/xam/xam_ui.h @@ -13,6 +13,13 @@ #include "xenia/kernel/util/shim_utils.h" #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/imgui_drawer.h" +#include "xenia/ui/imgui_gamepad_dialog.h" + +namespace xe { +namespace hid { +class InputSystem; +} // namespace hid +} // namespace xe namespace xe { namespace kernel { @@ -40,17 +47,43 @@ class XamDialog : public xe::ui::ImGuiDialog { std::function close_callback_ = nullptr; }; -class MessageBoxDialog : public XamDialog { +// XamDialog with gamepad support for game-triggered dialogs +class XamGamepadDialog : public xe::ui::ImGuiGamepadDialog { public: - MessageBoxDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title, + void set_close_callback(std::function close_callback) { + close_callback_ = close_callback; + } + + protected: + XamGamepadDialog(xe::ui::ImGuiDrawer* imgui_drawer, + xe::hid::InputSystem* input_system) + : xe::ui::ImGuiGamepadDialog(imgui_drawer, input_system, true) {} + + virtual ~XamGamepadDialog() {}; + + void OnClose() override { + if (close_callback_) { + close_callback_(); + } + } + + private: + std::function close_callback_ = nullptr; +}; + +class MessageBoxDialog : public XamGamepadDialog { + public: + MessageBoxDialog(xe::ui::ImGuiDrawer* imgui_drawer, + xe::hid::InputSystem* input_system, std::string& title, std::string& description, std::vector buttons, uint32_t default_button) - : XamDialog(imgui_drawer), + : XamGamepadDialog(imgui_drawer, input_system), title_(title), description_(description), buttons_(std::move(buttons)), default_button_(default_button), - chosen_button_(default_button) { + chosen_button_(default_button), + focused_button_(default_button) { if (!title_.size()) { title_ = "Message Box"; } @@ -61,6 +94,11 @@ class MessageBoxDialog : public XamDialog { void OnDraw(ImGuiIO& io) override; virtual ~MessageBoxDialog() {} + protected: + void OnGamepadButtonA() override; + void OnGamepadDPadLeft() override; + void OnGamepadDPadRight() override; + private: bool has_opened_ = false; std::string title_; @@ -68,14 +106,16 @@ class MessageBoxDialog : public XamDialog { std::vector buttons_; uint32_t default_button_ = 0; uint32_t chosen_button_ = 0; + uint32_t focused_button_ = 0; }; -class KeyboardInputDialog : public XamDialog { +class KeyboardInputDialog : public XamGamepadDialog { public: - KeyboardInputDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title, + KeyboardInputDialog(xe::ui::ImGuiDrawer* imgui_drawer, + xe::hid::InputSystem* input_system, std::string& title, std::string& description, std::string& default_text, size_t max_length) - : XamDialog(imgui_drawer), + : XamGamepadDialog(imgui_drawer, input_system), title_(title), description_(description), default_text_(default_text), @@ -101,6 +141,11 @@ class KeyboardInputDialog : public XamDialog { void OnDraw(ImGuiIO& io) override; + protected: + void OnGamepadButtonA() override; + void OnGamepadDPadLeft() override; + void OnGamepadDPadRight() override; + private: bool has_opened_ = false; std::string title_; @@ -110,6 +155,7 @@ class KeyboardInputDialog : public XamDialog { std::vector text_buffer_; std::string text_ = ""; bool cancelled_ = true; + int focused_button_ = 0; // 0 = OK, 1 = Cancel }; bool xeDrawProfileContent(xe::ui::ImGuiDrawer* imgui_drawer, diff --git a/src/xenia/ui/audio_helper_qt.cc b/src/xenia/ui/audio_helper_qt.cc new file mode 100644 index 000000000..7784fb8bc --- /dev/null +++ b/src/xenia/ui/audio_helper_qt.cc @@ -0,0 +1,82 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/audio_helper_qt.h" + +#include +#include + +#include "xenia/base/cvar.h" +#include "xenia/base/string.h" + +DECLARE_path(achievement_sound_path); + +namespace xe { +namespace ui { + +AudioHelperQt& AudioHelperQt::Instance() { + static AudioHelperQt instance; + return instance; +} + +AudioHelperQt::AudioHelperQt() : QObject(nullptr) {} + +AudioHelperQt::~AudioHelperQt() { + if (media_player_) { + media_player_->stop(); + delete media_player_; + media_player_ = nullptr; + } + if (audio_output_) { + delete audio_output_; + audio_output_ = nullptr; + } +} + +void AudioHelperQt::InitializeMediaPlayer() { + if (initialized_) { + return; + } + initialized_ = true; + + if (cvars::achievement_sound_path.empty()) { + return; + } + + std::filesystem::path sound_path = cvars::achievement_sound_path; + if (!std::filesystem::exists(sound_path)) { + return; + } + + // Create without parent to avoid interfering with Qt's widget event loop + // This prevents FPS drops when the media player is instantiated + media_player_ = new QMediaPlayer(nullptr); + audio_output_ = new QAudioOutput(nullptr); + media_player_->setAudioOutput(audio_output_); + media_player_->setSource(QUrl::fromLocalFile( + QString::fromStdString(xe::path_to_utf8(sound_path)))); + audio_output_->setVolume(1.0); +} + +void AudioHelperQt::PlayAchievementSound() { + // Lazy initialization - only create media player when first needed + if (!initialized_) { + InitializeMediaPlayer(); + } + + if (media_player_) { + // Stop any currently playing sound and restart + media_player_->stop(); + media_player_->setPosition(0); + media_player_->play(); + } +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/audio_helper_qt.h b/src/xenia/ui/audio_helper_qt.h new file mode 100644 index 000000000..d221ad5a2 --- /dev/null +++ b/src/xenia/ui/audio_helper_qt.h @@ -0,0 +1,50 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_AUDIO_HELPER_QT_H_ +#define XENIA_UI_AUDIO_HELPER_QT_H_ + +#include +#include +#include + +namespace xe { +namespace ui { + +// Singleton audio helper for playing notification sounds. +// Uses QMediaPlayer which doesn't require a QWidget parent, +// so it works with both QWidget and QWindow-based windows. +class AudioHelperQt : public QObject { + Q_OBJECT + + public: + static AudioHelperQt& Instance(); + + // Play the achievement unlock sound if configured + void PlayAchievementSound(); + + private: + AudioHelperQt(); + ~AudioHelperQt(); + + // Non-copyable + AudioHelperQt(const AudioHelperQt&) = delete; + AudioHelperQt& operator=(const AudioHelperQt&) = delete; + + void InitializeMediaPlayer(); + + QMediaPlayer* media_player_ = nullptr; + QAudioOutput* audio_output_ = nullptr; + bool initialized_ = false; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_AUDIO_HELPER_QT_H_ diff --git a/src/xenia/ui/confirm_dialog_widget_qt.cc b/src/xenia/ui/confirm_dialog_widget_qt.cc deleted file mode 100644 index c10350fe9..000000000 --- a/src/xenia/ui/confirm_dialog_widget_qt.cc +++ /dev/null @@ -1,200 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2026 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/ui/confirm_dialog_widget_qt.h" - -#include -#include -#include - -#include "xenia/hid/input_system.h" - -namespace xe { -namespace app { - -ConfirmDialogWidgetQt::ConfirmDialogWidgetQt(QWidget* parent, - hid::InputSystem* input_system, - const QString& title, - const QString& message) - : QDialog(parent), - input_system_(input_system), - poll_timer_(nullptr), - focused_index_(0), // Start with "No" focused (safer default) - prev_buttons_(0) { - setWindowTitle(title); - setModal(true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - - // Match theme from postprocessing dialog - setStyleSheet( - "QDialog { background-color: rgb(30, 30, 30); }" - "QLabel { color: #d0d0d0; background: transparent; }"); - - auto* main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(24, 20, 24, 20); - main_layout->setSpacing(16); - - // Message - auto* message_label = new QLabel(message, this); - message_label->setStyleSheet("color: #d0d0d0; font-size: 12px;"); - message_label->setAlignment(Qt::AlignCenter); - message_label->setWordWrap(true); - main_layout->addWidget(message_label); - - main_layout->addSpacing(8); - - // Buttons - auto* button_layout = new QHBoxLayout(); - button_layout->setSpacing(12); - - no_button_ = new QPushButton("No", this); - no_button_->setMinimumSize(90, 32); - no_button_->setCursor(Qt::PointingHandCursor); - connect(no_button_, &QPushButton::clicked, this, &QDialog::reject); - - yes_button_ = new QPushButton("Yes", this); - yes_button_->setMinimumSize(90, 32); - yes_button_->setCursor(Qt::PointingHandCursor); - connect(yes_button_, &QPushButton::clicked, this, &QDialog::accept); - - button_layout->addStretch(); - button_layout->addWidget(no_button_); - button_layout->addWidget(yes_button_); - button_layout->addStretch(); - - main_layout->addLayout(button_layout); - - // Start gamepad polling if input system is available - if (input_system_) { - input_system_->AddUIInputBlocker(); - - // Initialize prev_buttons_ to current state - hid::X_INPUT_STATE state; - for (uint32_t user_index = 0; user_index < 4; user_index++) { - if (input_system_->GetStateForUI(user_index, 1, &state) == 0) { - prev_buttons_ = state.gamepad.buttons; - break; - } - } - - poll_timer_ = new QTimer(this); - connect(poll_timer_, &QTimer::timeout, this, - &ConfirmDialogWidgetQt::PollGamepad); - poll_timer_->start(16); // ~60fps polling - } - - // Set initial button focus - UpdateFocusedButton(focused_index_); -} - -ConfirmDialogWidgetQt::~ConfirmDialogWidgetQt() { - if (poll_timer_) { - poll_timer_->stop(); - } - if (input_system_) { - input_system_->RemoveUIInputBlocker(); - } -} - -bool ConfirmDialogWidgetQt::Confirm(QWidget* parent, - hid::InputSystem* input_system, - const QString& title, - const QString& message) { - ConfirmDialogWidgetQt dialog(parent, input_system, title, message); - return dialog.exec() == QDialog::Accepted; -} - -void ConfirmDialogWidgetQt::keyPressEvent(QKeyEvent* event) { - switch (event->key()) { - case Qt::Key_Escape: - reject(); - break; - case Qt::Key_Left: - case Qt::Key_Right: - UpdateFocusedButton(focused_index_ == 0 ? 1 : 0); - break; - case Qt::Key_Return: - case Qt::Key_Enter: - if (focused_index_ == 1) { - accept(); - } else { - reject(); - } - break; - default: - QDialog::keyPressEvent(event); - } -} - -void ConfirmDialogWidgetQt::PollGamepad() { - if (!input_system_) { - return; - } - - for (uint32_t i = 0; i < 4; ++i) { - hid::X_INPUT_STATE state; - if (input_system_->GetStateForUI(i, 1, &state) == 0) { - uint16_t buttons = state.gamepad.buttons; - uint16_t pressed = buttons & ~prev_buttons_; - - // D-pad left/right to switch buttons - if (pressed & 0x0004) { // D-pad left - UpdateFocusedButton(0); // No - } - if (pressed & 0x0008) { // D-pad right - UpdateFocusedButton(1); // Yes - } - - // A button to confirm selection - if (pressed & 0x1000) { - if (focused_index_ == 1) { - accept(); - } else { - reject(); - } - } - - // B button to cancel (same as No) - if (pressed & 0x2000) { - reject(); - } - - prev_buttons_ = buttons; - break; - } - } -} - -void ConfirmDialogWidgetQt::UpdateFocusedButton(int index) { - focused_index_ = index; - - // Theme-consistent button styles - QString unfocused_style = - "QPushButton { background-color: rgba(50, 50, 50, 200); color: #d0d0d0; " - "border: 2px solid #909090; border-radius: 4px; " - "font-size: 12px; padding: 6px 16px; }" - "QPushButton:hover { background-color: rgba(70, 70, 70, 200); " - "border-color: #b0b0b0; }"; - - QString focused_style = - "QPushButton { background-color: rgba(70, 70, 70, 200); color: #f0f0f0; " - "border: 2px solid #107c10; border-radius: 4px; " - "font-size: 12px; padding: 6px 16px; }"; - - if (focused_index_ == 0) { - no_button_->setStyleSheet(focused_style); - yes_button_->setStyleSheet(unfocused_style); - } else { - no_button_->setStyleSheet(unfocused_style); - yes_button_->setStyleSheet(focused_style); - } -} - -} // namespace app -} // namespace xe diff --git a/src/xenia/ui/confirm_dialog_widget_qt.h b/src/xenia/ui/confirm_dialog_widget_qt.h deleted file mode 100644 index 3514a81e4..000000000 --- a/src/xenia/ui/confirm_dialog_widget_qt.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2026 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_UI_CONFIRM_DIALOG_WIDGET_QT_H_ -#define XENIA_UI_CONFIRM_DIALOG_WIDGET_QT_H_ - -#include -#include -#include -#include -#include - -namespace xe { -namespace hid { -class InputSystem; -} // namespace hid -} // namespace xe - -namespace xe { -namespace app { - -// Confirmation dialog with gamepad support -class ConfirmDialogWidgetQt : public QDialog { - Q_OBJECT - - public: - ConfirmDialogWidgetQt(QWidget* parent, hid::InputSystem* input_system, - const QString& title, const QString& message); - ~ConfirmDialogWidgetQt() override; - - // Static blocking method - returns true if confirmed - static bool Confirm(QWidget* parent, hid::InputSystem* input_system, - const QString& title, const QString& message); - - protected: - void keyPressEvent(QKeyEvent* event) override; - - private slots: - void PollGamepad(); - - private: - void UpdateFocusedButton(int index); - - QPushButton* yes_button_; - QPushButton* no_button_; - - // Gamepad support - hid::InputSystem* input_system_; - QTimer* poll_timer_; - int focused_index_; // 0 = No, 1 = Yes - uint16_t prev_buttons_; -}; - -} // namespace app -} // namespace xe - -#endif // XENIA_UI_CONFIRM_DIALOG_WIDGET_QT_H_ diff --git a/src/xenia/ui/controller_hotkeys_dialog_qt.cc b/src/xenia/ui/controller_hotkeys_dialog_qt.cc deleted file mode 100644 index fd14242f4..000000000 --- a/src/xenia/ui/controller_hotkeys_dialog_qt.cc +++ /dev/null @@ -1,155 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/ui/controller_hotkeys_dialog_qt.h" - -#include -#include -#include -#include - -#include "xenia/app/emulator_window.h" - -namespace xe { -namespace app { - -ControllerHotkeysDialogQt::ControllerHotkeysDialogQt( - QWidget* parent, EmulatorWindow* emulator_window, - hid::InputSystem* input_system) - : ui::GamepadDialog(parent, input_system), - emulator_window_(emulator_window) { - SetupUI(); - - // Position near top, centered horizontally - if (parent) { - QPoint parent_pos = parent->mapToGlobal(QPoint(0, 0)); - int center_x = parent_pos.x() + (parent->width() - width()) / 2; - move(center_x, parent_pos.y() + 20); - } -} - -ControllerHotkeysDialogQt::~ControllerHotkeysDialogQt() = default; - -void ControllerHotkeysDialogQt::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Escape) { - close(); - return; - } - QDialog::keyPressEvent(event); -} - -void ControllerHotkeysDialogQt::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - QWidget* child = childAt(event->position().toPoint()); - if (!child || child == this) { - drag_position_ = - event->globalPosition().toPoint() - frameGeometry().topLeft(); - dragging_ = true; - event->accept(); - return; - } - } - QDialog::mousePressEvent(event); -} - -void ControllerHotkeysDialogQt::mouseMoveEvent(QMouseEvent* event) { - if (dragging_ && (event->buttons() & Qt::LeftButton)) { - move(event->globalPosition().toPoint() - drag_position_); - event->accept(); - return; - } - QDialog::mouseMoveEvent(event); -} - -void ControllerHotkeysDialogQt::mouseReleaseEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - dragging_ = false; - } - QDialog::mouseReleaseEvent(event); -} - -void ControllerHotkeysDialogQt::SetupUI() { - setWindowTitle("Controller Hotkeys"); - setModal(false); - setAttribute(Qt::WA_DeleteOnClose); - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); - setMinimumWidth(400); - - setWindowOpacity(0.92); - - setStyleSheet(R"( - QDialog { - background-color: rgb(30, 30, 30); - border: 1px solid rgba(100, 100, 100, 180); - padding: 0px; - } - QLabel { - color: #d0d0d0; - background-color: transparent; - } - )"); - - auto* content_layout = new QVBoxLayout(this); - content_layout->setContentsMargins(16, 16, 16, 16); - content_layout->setSpacing(8); - - // Top bar with title and close button - auto* top_bar_layout = new QHBoxLayout(); - auto* title_label = new QLabel("Controller Hotkeys", this); - title_label->setStyleSheet( - "color: #f0f0f0; font-weight: bold; font-size: 14px;"); - top_bar_layout->addWidget(title_label); - top_bar_layout->addStretch(); - - auto* close_button = new QPushButton(this); - close_button->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton)); - close_button->setIconSize(QSize(16, 16)); - close_button->setFlat(true); - close_button->setStyleSheet(R"( - QPushButton { - background-color: transparent; - border: none; - min-width: 24px; - max-width: 24px; - min-height: 24px; - max-height: 24px; - padding: 2px; - } - QPushButton:hover { - background-color: rgba(200, 50, 50, 180); - border-radius: 4px; - } - QPushButton:pressed { - background-color: rgba(150, 30, 30, 220); - border-radius: 4px; - } - )"); - close_button->setToolTip("Close"); - connect(close_button, &QPushButton::clicked, this, &QDialog::close); - top_bar_layout->addWidget(close_button, 0, Qt::AlignTop); - - content_layout->addLayout(top_bar_layout); - content_layout->addSpacing(8); - - // Get hotkeys from emulator window - auto hotkeys = emulator_window_->GetControllerHotkeysList(); - - for (const auto& [text, enabled] : hotkeys) { - auto* label = new QLabel(QString::fromStdString(text), this); - if (!enabled) { - label->setStyleSheet("color: #666666;"); - } - content_layout->addWidget(label); - } - - content_layout->addStretch(); -} - -} // namespace app -} // namespace xe diff --git a/src/xenia/ui/controller_hotkeys_dialog_qt.h b/src/xenia/ui/controller_hotkeys_dialog_qt.h deleted file mode 100644 index b3d08031c..000000000 --- a/src/xenia/ui/controller_hotkeys_dialog_qt.h +++ /dev/null @@ -1,49 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_UI_CONTROLLER_HOTKEYS_DIALOG_QT_H_ -#define XENIA_UI_CONTROLLER_HOTKEYS_DIALOG_QT_H_ - -#include -#include -#include - -#include "xenia/ui/gamepad_dialog_qt.h" - -namespace xe { -namespace app { - -class EmulatorWindow; - -class ControllerHotkeysDialogQt : public ui::GamepadDialog { - Q_OBJECT - - public: - ControllerHotkeysDialogQt(QWidget* parent, EmulatorWindow* emulator_window, - hid::InputSystem* input_system); - ~ControllerHotkeysDialogQt() override; - - protected: - void keyPressEvent(QKeyEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - - private: - void SetupUI(); - - EmulatorWindow* emulator_window_; - QPoint drag_position_; - bool dragging_ = false; -}; - -} // namespace app -} // namespace xe - -#endif // XENIA_UI_CONTROLLER_HOTKEYS_DIALOG_QT_H_ diff --git a/src/xenia/ui/game_list_dialog_qt.cc b/src/xenia/ui/game_list_dialog_qt.cc index 6de876f94..1ceaa3a55 100644 --- a/src/xenia/ui/game_list_dialog_qt.cc +++ b/src/xenia/ui/game_list_dialog_qt.cc @@ -1297,18 +1297,25 @@ void GameListDialogQt::OnSettingsClicked() { } void GameListDialogQt::OnProfileClicked() { - // If logged in, show context menu instead - if (current_profile_xuid_ != 0) { - // Show context menu at the cursor position - QPoint global_pos = QCursor::pos(); - QPoint local_pos = profile_button_->mapFromGlobal(global_pos); - OnProfileContextMenu(local_pos); + // Left click always opens profile dialog (Qt version for UI process) + if (profile_dialog_) { + profile_dialog_->raise(); + profile_dialog_->activateWindow(); return; } - // Not logged in, open profile dialog via emulator window - if (emulator_window_) { - emulator_window_->ToggleProfilesConfigDialog(); + if (emulator_window_ && emulator_window_->emulator()) { + profile_dialog_ = + new ProfileDialogQt(nullptr, emulator_window_, + emulator_window_->emulator()->input_system()); + connect(profile_dialog_, &QDialog::finished, this, [this]() { + profile_dialog_ = nullptr; + UpdateProfileButtonState(); + RefreshIcons(); + }); + profile_dialog_->show(); + profile_dialog_->raise(); + profile_dialog_->activateWindow(); } } @@ -1515,8 +1522,25 @@ void GameListDialogQt::OnProfileContextMenu(const QPoint& pos) { // Profiles Menu QAction* profiles_menu_action = context_menu.addAction("Profiles Menu"); connect(profiles_menu_action, &QAction::triggered, [this]() { - if (emulator_window_) { - emulator_window_->ToggleProfilesConfigDialog(); + // Use Qt profile dialog directly for UI process + if (profile_dialog_) { + profile_dialog_->raise(); + profile_dialog_->activateWindow(); + return; + } + + if (emulator_window_ && emulator_window_->emulator()) { + profile_dialog_ = + new ProfileDialogQt(nullptr, emulator_window_, + emulator_window_->emulator()->input_system()); + connect(profile_dialog_, &QDialog::finished, this, [this]() { + profile_dialog_ = nullptr; + UpdateProfileButtonState(); + RefreshIcons(); + }); + profile_dialog_->show(); + profile_dialog_->raise(); + profile_dialog_->activateWindow(); } }); diff --git a/src/xenia/ui/imgui_confirm_dialog.cc b/src/xenia/ui/imgui_confirm_dialog.cc new file mode 100644 index 000000000..30f0e52b5 --- /dev/null +++ b/src/xenia/ui/imgui_confirm_dialog.cc @@ -0,0 +1,213 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/imgui_confirm_dialog.h" + +#include "third_party/imgui/imgui.h" +#include "xenia/hid/input_system.h" + +namespace xe { +namespace ui { + +ImGuiConfirmDialog::ImGuiConfirmDialog(ImGuiDrawer* drawer, + const std::string& title, + const std::string& message, + Callback callback, + hid::InputSystem* input_system) + : ImGuiDialog(drawer), + title_(title), + message_(message), + callback_(std::move(callback)), + input_system_(input_system) { + if (input_system_) { + input_system_->AddUIInputBlocker(); + + // Initialize prev_buttons_ to current state so held buttons aren't + // detected as "just pressed" when the dialog opens + hid::X_INPUT_STATE state; + for (uint32_t user_index = 0; user_index < 4; user_index++) { + if (input_system_->GetStateForUI(user_index, 1, &state) == 0) { + prev_buttons_ = state.gamepad.buttons; + break; + } + } + } +} + +ImGuiConfirmDialog::~ImGuiConfirmDialog() { + if (input_system_) { + input_system_->RemoveUIInputBlocker(); + } +} + +void ImGuiConfirmDialog::PollGamepad() { + if (!input_system_) { + return; + } + + for (uint32_t i = 0; i < 4; ++i) { + hid::X_INPUT_STATE state; + if (input_system_->GetStateForUI(i, 1, &state) == 0) { + uint16_t buttons = state.gamepad.buttons; + uint16_t pressed = buttons & ~prev_buttons_; + + // D-pad left/right to switch buttons + if (pressed & 0x0004) { // D-pad left + focused_button_ = 0; // No + } + if (pressed & 0x0008) { // D-pad right + focused_button_ = 1; // Yes + } + + // A button to confirm selection + if (pressed & 0x1000) { + Confirm(focused_button_ == 1); + } + + // B button to cancel (same as No) + if (pressed & 0x2000) { + Confirm(false); + } + + prev_buttons_ = buttons; + break; + } + } +} + +void ImGuiConfirmDialog::Confirm(bool result) { + if (!callback_invoked_) { + callback_invoked_ = true; + if (callback_) { + callback_(result); + } + Close(); + } +} + +void ImGuiConfirmDialog::OnDraw(ImGuiIO& io) { + // Poll gamepad input + PollGamepad(); + + // Open popup on first draw + if (!has_opened_) { + ImGui::OpenPopup(title_.c_str()); + has_opened_ = true; + } + + // Style the popup + ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0.12f, 0.12f, 0.12f, 0.95f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.33f, 0.33f, 0.33f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24, 20)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + + // Center the popup on screen + ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + + bool is_open = true; + if (ImGui::BeginPopupModal(title_.c_str(), &is_open, + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoMove)) { + // Handle keyboard input + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + Confirm(false); + } + if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) { + focused_button_ = 0; + } + if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) { + focused_button_ = 1; + } + if (ImGui::IsKeyPressed(ImGuiKey_Enter) || + ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) { + Confirm(focused_button_ == 1); + } + + // Message text + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.82f, 0.82f, 0.82f, 1.0f)); + ImGui::TextWrapped("%s", message_.c_str()); + ImGui::PopStyleColor(); + + ImGui::Spacing(); + ImGui::Spacing(); + + // Buttons - centered + float button_width = 90.0f; + float spacing = 12.0f; + float total_width = button_width * 2 + spacing; + float start_x = (ImGui::GetWindowWidth() - total_width) * 0.5f; + + ImGui::SetCursorPosX(start_x); + + // No button + ImVec4 no_bg = focused_button_ == 0 ? ImVec4(0.27f, 0.27f, 0.27f, 1.0f) + : ImVec4(0.20f, 0.20f, 0.20f, 1.0f); + ImVec4 no_border = focused_button_ == 0 + ? ImVec4(0.06f, 0.49f, 0.06f, 1.0f) // Xbox green + : ImVec4(0.56f, 0.56f, 0.56f, 1.0f); + + ImGui::PushStyleColor(ImGuiCol_Button, no_bg); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + ImVec4(0.27f, 0.27f, 0.27f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, + ImVec4(0.27f, 0.27f, 0.27f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + ImGui::PushStyleColor(ImGuiCol_Border, no_border); + + if (ImGui::Button("No", ImVec2(button_width, 32))) { + Confirm(false); + } + if (ImGui::IsItemHovered()) { + focused_button_ = 0; + } + + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(); + + ImGui::SameLine(0, spacing); + + // Yes button + ImVec4 yes_bg = focused_button_ == 1 ? ImVec4(0.27f, 0.27f, 0.27f, 1.0f) + : ImVec4(0.20f, 0.20f, 0.20f, 1.0f); + ImVec4 yes_border = focused_button_ == 1 + ? ImVec4(0.06f, 0.49f, 0.06f, 1.0f) // Xbox green + : ImVec4(0.56f, 0.56f, 0.56f, 1.0f); + + ImGui::PushStyleColor(ImGuiCol_Button, yes_bg); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + ImVec4(0.27f, 0.27f, 0.27f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, + ImVec4(0.27f, 0.27f, 0.27f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + ImGui::PushStyleColor(ImGuiCol_Border, yes_border); + + if (ImGui::Button("Yes", ImVec2(button_width, 32))) { + Confirm(true); + } + if (ImGui::IsItemHovered()) { + focused_button_ = 1; + } + + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(); + + ImGui::EndPopup(); + } else { + // Popup was closed (clicked outside or X button) + Confirm(false); + } + + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(2); +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/imgui_confirm_dialog.h b/src/xenia/ui/imgui_confirm_dialog.h new file mode 100644 index 000000000..160f9aee5 --- /dev/null +++ b/src/xenia/ui/imgui_confirm_dialog.h @@ -0,0 +1,63 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_IMGUI_CONFIRM_DIALOG_H_ +#define XENIA_UI_IMGUI_CONFIRM_DIALOG_H_ + +#include +#include + +#include "xenia/ui/imgui_dialog.h" + +namespace xe { +namespace hid { +class InputSystem; +} // namespace hid +} // namespace xe + +namespace xe { +namespace ui { + +// ImGui-based confirmation dialog with gamepad support. +// Uses async callback pattern - the callback is invoked when the user +// confirms or cancels the dialog. +class ImGuiConfirmDialog : public ImGuiDialog { + public: + using Callback = std::function; + + // Creates a confirm dialog. The callback will be invoked with true if + // the user confirms, false if they cancel. + // If input_system is provided, gamepad input will be supported. + ImGuiConfirmDialog(ImGuiDrawer* drawer, const std::string& title, + const std::string& message, Callback callback, + hid::InputSystem* input_system = nullptr); + + ~ImGuiConfirmDialog() override; + + protected: + void OnDraw(ImGuiIO& io) override; + + private: + void PollGamepad(); + void Confirm(bool result); + + std::string title_; + std::string message_; + Callback callback_; + hid::InputSystem* input_system_; + int focused_button_ = 0; // 0 = No, 1 = Yes + uint16_t prev_buttons_ = 0; + bool has_opened_ = false; + bool callback_invoked_ = false; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_IMGUI_CONFIRM_DIALOG_H_ diff --git a/src/xenia/ui/imgui_context_menu.cc b/src/xenia/ui/imgui_context_menu.cc new file mode 100644 index 000000000..8d647b6ef --- /dev/null +++ b/src/xenia/ui/imgui_context_menu.cc @@ -0,0 +1,266 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/imgui_context_menu.h" + +#include + +#include "third_party/imgui/imgui.h" +#include "xenia/hid/input_system.h" + +namespace xe { +namespace ui { + +ImGuiContextMenu::ImGuiContextMenu(ImGuiDrawer* drawer, + hid::InputSystem* input_system) + : ImGuiDialog(drawer), input_system_(input_system) { + if (input_system_) { + input_system_->AddUIInputBlocker(); + + // Initialize prev_buttons_ to current state so held buttons aren't + // detected as "just pressed" when the menu opens + hid::X_INPUT_STATE state; + for (uint32_t user_index = 0; user_index < 4; user_index++) { + if (input_system_->GetStateForUI(user_index, 1, &state) == 0) { + prev_buttons_ = state.gamepad.buttons; + break; + } + } + } +} + +ImGuiContextMenu::~ImGuiContextMenu() { + if (input_system_) { + input_system_->RemoveUIInputBlocker(); + } +} + +void ImGuiContextMenu::OnClose() { + if (on_close_callback_) { + on_close_callback_(); + } + // Defer action callback to execute after the current frame completes + // This prevents issues with operations like fullscreen that need the + // ImGui state to be fully cleaned up first + if (pending_callback_) { + imgui_drawer()->PostDeferredCallback(std::move(pending_callback_)); + } +} + +void ImGuiContextMenu::AddAction(const std::string& text, + std::function callback, + const std::string& shortcut) { + items_.push_back({text, shortcut, callback, false}); +} + +void ImGuiContextMenu::AddSeparator() { + items_.push_back({"", "", nullptr, true}); +} + +void ImGuiContextMenu::Show() { + center_on_screen_ = true; + // Find first selectable item + focused_index_ = GetNextSelectableItem(-1, 1); +} + +void ImGuiContextMenu::ShowAt(float x, float y) { + center_on_screen_ = false; + position_x_ = x; + position_y_ = y; + // Find first selectable item + focused_index_ = GetNextSelectableItem(-1, 1); +} + +int ImGuiContextMenu::GetNextSelectableItem(int current, int direction) { + if (items_.empty()) return -1; + + int count = static_cast(items_.size()); + int next = current; + + for (int i = 0; i < count; i++) { + next += direction; + if (next < 0) next = count - 1; + if (next >= count) next = 0; + + if (!items_[next].is_separator) { + return next; + } + } + + return current; // No selectable items found +} + +void ImGuiContextMenu::PollGamepad() { + if (!input_system_ || items_.empty()) { + return; + } + + for (uint32_t i = 0; i < 4; ++i) { + hid::X_INPUT_STATE state; + if (input_system_->GetStateForUI(i, 1, &state) == 0) { + uint16_t buttons = state.gamepad.buttons; + uint16_t pressed = buttons & ~prev_buttons_; + + // D-pad navigation + if (pressed & 0x0001) { // D-pad up + focused_index_ = GetNextSelectableItem(focused_index_, -1); + } + if (pressed & 0x0002) { // D-pad down + focused_index_ = GetNextSelectableItem(focused_index_, 1); + } + + // A button is handled by ImGui's nav system via the Selectable + + // B button, Back button, or Guide button to close + if (pressed & (0x2000 | 0x0020 | 0x0400)) { + Close(); + } + + prev_buttons_ = buttons; + break; + } + } +} + +void ImGuiContextMenu::ActivateItem(int index) { + if (index >= 0 && index < static_cast(items_.size()) && + !items_[index].is_separator && items_[index].callback) { + // Store callback to execute after dialog is fully closed + pending_callback_ = items_[index].callback; + Close(); + } +} + +void ImGuiContextMenu::OnDraw(ImGuiIO& io) { + // Poll gamepad input + PollGamepad(); + + // Open popup on first draw + if (!has_opened_) { + ImGui::OpenPopup("##ContextMenu"); + has_opened_ = true; + } + + // Style the popup - white background, black text + ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.7f, 0.7f, 0.7f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 4)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + + // Position the popup + if (center_on_screen_) { + ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + } else { + ImGui::SetNextWindowPos(ImVec2(position_x_, position_y_), + ImGuiCond_Appearing); + } + + // Set minimum width for the menu (30% wider than default) + ImGui::SetNextWindowSizeConstraints(ImVec2(280, 0), ImVec2(FLT_MAX, FLT_MAX)); + + bool is_open = true; + if (ImGui::BeginPopupModal( + "##ContextMenu", &is_open, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { + // Handle keyboard input + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + ImGui::CloseCurrentPopup(); + Close(); + } + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) { + focused_index_ = GetNextSelectableItem(focused_index_, -1); + } + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { + focused_index_ = GetNextSelectableItem(focused_index_, 1); + } + if (ImGui::IsKeyPressed(ImGuiKey_Enter) || + ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) { + ActivateItem(focused_index_); + } + + // Draw menu items + for (int i = 0; i < static_cast(items_.size()); i++) { + const auto& item = items_[i]; + + if (item.is_separator) { + ImGui::PushStyleColor(ImGuiCol_Separator, + ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::Separator(); + ImGui::PopStyleColor(); + continue; + } + + bool is_focused = (i == focused_index_); + + // Item background - Xbox green (#107C10) for highlight + const ImVec4 xbox_green(0.063f, 0.486f, 0.063f, 1.0f); + if (is_focused) { + ImGui::PushStyleColor(ImGuiCol_Header, xbox_green); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, xbox_green); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, xbox_green); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + } else { + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, xbox_green); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, xbox_green); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + } + + // Create selectable item + ImGui::PushID(i); + if (ImGui::Selectable("##item", is_focused, + ImGuiSelectableFlags_SpanAllColumns, + ImVec2(0, 0))) { + ActivateItem(i); + } + + // Update focus on hover + if (ImGui::IsItemHovered()) { + focused_index_ = i; + } + + // Draw text on same line + ImGui::SameLine(); + ImGui::SetCursorPosX(12); + ImGui::TextUnformatted(item.text.c_str()); + + // Draw shortcut if present + if (!item.shortcut.empty()) { + float shortcut_width = + ImGui::CalcTextSize(item.shortcut.c_str()).x + 12; + float window_width = ImGui::GetWindowWidth(); + ImGui::SameLine(window_width - shortcut_width); + // Lighter text for shortcut, white if focused for contrast on green + ImGui::PushStyleColor(ImGuiCol_Text, + is_focused ? ImVec4(0.85f, 0.85f, 0.85f, 1.0f) + : ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::TextUnformatted(item.shortcut.c_str()); + ImGui::PopStyleColor(); + } + + ImGui::PopID(); + ImGui::PopStyleColor(4); + } + + ImGui::EndPopup(); + } else { + // Popup was closed (clicked outside) + Close(); + } + + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(3); +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/imgui_context_menu.h b/src/xenia/ui/imgui_context_menu.h new file mode 100644 index 000000000..3299d0631 --- /dev/null +++ b/src/xenia/ui/imgui_context_menu.h @@ -0,0 +1,89 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_IMGUI_CONTEXT_MENU_H_ +#define XENIA_UI_IMGUI_CONTEXT_MENU_H_ + +#include +#include +#include + +#include "xenia/ui/imgui_dialog.h" + +namespace xe { +namespace hid { +class InputSystem; +} // namespace hid +} // namespace xe + +namespace xe { +namespace ui { + +// ImGui-based context menu with gamepad support. +// This is a modal popup that blocks game input while open. +class ImGuiContextMenu : public ImGuiDialog { + public: + struct MenuItem { + std::string text; + std::string shortcut; + std::function callback; + bool is_separator; + }; + + // Creates a context menu. If input_system is provided, gamepad input + // will be supported and game input will be blocked while menu is open. + ImGuiContextMenu(ImGuiDrawer* drawer, hid::InputSystem* input_system); + ~ImGuiContextMenu() override; + + // Add a menu item with optional keyboard shortcut hint + void AddAction(const std::string& text, std::function callback, + const std::string& shortcut = ""); + + // Add a visual separator line + void AddSeparator(); + + // Show the menu centered on screen + void Show(); + + // Show the menu at a specific position (in screen coordinates) + void ShowAt(float x, float y); + + // Close the menu programmatically + void CloseMenu() { Close(); } + + // Set a callback to be invoked when the menu closes + void SetOnCloseCallback(std::function callback) { + on_close_callback_ = std::move(callback); + } + + protected: + void OnClose() override; + void OnDraw(ImGuiIO& io) override; + + private: + void PollGamepad(); + void ActivateItem(int index); + int GetNextSelectableItem(int current, int direction); + + std::vector items_; + hid::InputSystem* input_system_; + std::function on_close_callback_; + std::function pending_callback_; // Callback to execute after close + int focused_index_ = 0; + uint16_t prev_buttons_ = 0; + bool has_opened_ = false; + bool center_on_screen_ = true; + float position_x_ = 0; + float position_y_ = 0; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_IMGUI_CONTEXT_MENU_H_ diff --git a/src/xenia/ui/imgui_controller_hotkeys_dialog.cc b/src/xenia/ui/imgui_controller_hotkeys_dialog.cc new file mode 100644 index 000000000..365e259cb --- /dev/null +++ b/src/xenia/ui/imgui_controller_hotkeys_dialog.cc @@ -0,0 +1,96 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/imgui_controller_hotkeys_dialog.h" + +#include "third_party/imgui/imgui.h" +#include "xenia/app/emulator_window.h" + +namespace xe { +namespace ui { + +ImGuiControllerHotkeysDialog::ImGuiControllerHotkeysDialog( + ImGuiDrawer* drawer, app::EmulatorWindow* emulator_window, + hid::InputSystem* input_system) + : ImGuiGamepadDialog(drawer, input_system), + emulator_window_(emulator_window) {} + +void ImGuiControllerHotkeysDialog::OnClose() { + if (on_close_callback_) { + on_close_callback_(); + } +} + +void ImGuiControllerHotkeysDialog::OnDraw(ImGuiIO& io) { + PollGamepad(); + + // Style - white background, black text, Xbox green accents + const ImVec4 xbox_green(0.063f, 0.486f, 0.063f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.7f, 0.7f, 0.7f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_TitleBg, xbox_green); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, xbox_green); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16, 16)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + + // Center on screen + ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always); + + bool is_open = true; + if (ImGui::Begin("Controller Hotkeys", &is_open, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse)) { + // Handle keyboard + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + Close(); + } + + // Colors for content + ImVec4 disabled_text = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); + + // Title + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Available Hotkeys"); + ImGui::PopStyleColor(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Get hotkeys from emulator window + auto hotkeys = emulator_window_->GetControllerHotkeysList(); + + for (const auto& [text, enabled] : hotkeys) { + if (!enabled) { + ImGui::PushStyleColor(ImGuiCol_Text, disabled_text); + } + ImGui::Text("%s", text.c_str()); + if (!enabled) { + ImGui::PopStyleColor(); + } + } + + ImGui::End(); + } + + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(6); + + if (!is_open) { + Close(); + } +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/imgui_controller_hotkeys_dialog.h b/src/xenia/ui/imgui_controller_hotkeys_dialog.h new file mode 100644 index 000000000..060676e91 --- /dev/null +++ b/src/xenia/ui/imgui_controller_hotkeys_dialog.h @@ -0,0 +1,51 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_IMGUI_CONTROLLER_HOTKEYS_DIALOG_H_ +#define XENIA_UI_IMGUI_CONTROLLER_HOTKEYS_DIALOG_H_ + +#include + +#include "xenia/ui/imgui_gamepad_dialog.h" + +namespace xe { +namespace app { +class EmulatorWindow; +} // namespace app +} // namespace xe + +namespace xe { +namespace ui { + +// ImGui-based controller hotkeys dialog. +class ImGuiControllerHotkeysDialog : public ImGuiGamepadDialog { + public: + ImGuiControllerHotkeysDialog(ImGuiDrawer* drawer, + app::EmulatorWindow* emulator_window, + hid::InputSystem* input_system); + + void CloseDialog() { Close(); } + + void SetOnCloseCallback(std::function callback) { + on_close_callback_ = std::move(callback); + } + + protected: + void OnClose() override; + void OnDraw(ImGuiIO& io) override; + + private: + app::EmulatorWindow* emulator_window_; + std::function on_close_callback_; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_IMGUI_CONTROLLER_HOTKEYS_DIALOG_H_ diff --git a/src/xenia/ui/imgui_drawer.cc b/src/xenia/ui/imgui_drawer.cc index ed7b44f8e..4b06d16c4 100644 --- a/src/xenia/ui/imgui_drawer.cc +++ b/src/xenia/ui/imgui_drawer.cc @@ -126,6 +126,10 @@ void ImGuiDrawer::AddNotification(ImGuiNotification* dialog) { } } notifications_.push_back(dialog); + // Request a paint to start the draw cycle for the new notification + if (presenter_) { + presenter_->RequestUIPaintFromUIThread(); + } } void ImGuiDrawer::RemoveNotification(ImGuiNotification* dialog) { @@ -227,6 +231,12 @@ void ImGuiDrawer::SetGuideButtonAction(std::function func) { onGuidePressFunction_ = func; } +void ImGuiDrawer::PostDeferredCallback(std::function callback) { + if (window_) { + window_->app_context().CallInUIThreadDeferred(std::move(callback)); + } +} + std::optional ImGuiDrawer::VirtualKeyToImGuiKey(VirtualKey vkey) { static const std::map map = { {ui::VirtualKey::kTab, ImGuiKey_Tab}, @@ -931,7 +941,8 @@ void ImGuiDrawer::UpdateGamepads() { uint8_t controller_to_poke = XUserIndexNone; hid::X_INPUT_STATE gamepad_state; for (uint8_t i = 0; i < XUserMaxUserCount; i++) { - if (input_system_->GetState(i, 1, &gamepad_state) == X_ERROR_SUCCESS) { + // Use GetStateForUI so ImGui navigation works even when input is blocked + if (input_system_->GetStateForUI(i, 1, &gamepad_state) == X_ERROR_SUCCESS) { if (gamepad_state.gamepad.buttons != 0) { controller_to_poke = i; break; diff --git a/src/xenia/ui/imgui_drawer.h b/src/xenia/ui/imgui_drawer.h index 080f3897f..6a00010d2 100644 --- a/src/xenia/ui/imgui_drawer.h +++ b/src/xenia/ui/imgui_drawer.h @@ -94,6 +94,9 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { void LoadInputSystem(hid::InputSystem* input_system); void SetGuideButtonAction(std::function func); + // Post a callback to be executed after the current frame completes + void PostDeferredCallback(std::function callback); + protected: void OnKeyDown(KeyEvent& e) override; void OnKeyUp(KeyEvent& e) override; diff --git a/src/xenia/ui/imgui_gamepad_dialog.cc b/src/xenia/ui/imgui_gamepad_dialog.cc new file mode 100644 index 000000000..6cbcbda42 --- /dev/null +++ b/src/xenia/ui/imgui_gamepad_dialog.cc @@ -0,0 +1,79 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/imgui_gamepad_dialog.h" + +#include "xenia/hid/input.h" +#include "xenia/hid/input_system.h" + +namespace xe { +namespace ui { + +ImGuiGamepadDialog::ImGuiGamepadDialog(ImGuiDrawer* drawer, + hid::InputSystem* input_system, + bool block_input) + : ImGuiDialog(drawer), input_system_(input_system) { + if (input_system_) { + if (block_input) { + input_system_->AddUIInputBlocker(); + blocking_input_ = true; + } + + // Initialize prev_buttons_ to current state to avoid spurious triggers + hid::X_INPUT_STATE state; + for (uint32_t user_index = 0; user_index < 4; user_index++) { + if (input_system_->GetStateForUI(user_index, 1, &state) == 0) { + prev_buttons_ = state.gamepad.buttons; + break; + } + } + } +} + +ImGuiGamepadDialog::~ImGuiGamepadDialog() { + if (input_system_ && blocking_input_) { + input_system_->RemoveUIInputBlocker(); + } +} + +void ImGuiGamepadDialog::PollGamepad() { + if (!input_system_) { + return; + } + + for (uint32_t i = 0; i < 4; ++i) { + hid::X_INPUT_STATE state; + if (input_system_->GetStateForUI(i, 1, &state) == 0) { + uint16_t buttons = state.gamepad.buttons; + uint16_t pressed = buttons & ~prev_buttons_; + + // D-pad + if (pressed & 0x0001) OnGamepadDPadUp(); + if (pressed & 0x0002) OnGamepadDPadDown(); + if (pressed & 0x0004) OnGamepadDPadLeft(); + if (pressed & 0x0008) OnGamepadDPadRight(); + + // Face buttons + if (pressed & 0x1000) OnGamepadButtonA(); + if (pressed & 0x2000) OnGamepadButtonB(); + if (pressed & 0x4000) OnGamepadButtonX(); + if (pressed & 0x8000) OnGamepadButtonY(); + + // Start/Back + if (pressed & 0x0010) OnGamepadStart(); + if (pressed & 0x0020) OnGamepadBack(); + + prev_buttons_ = buttons; + break; + } + } +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/imgui_gamepad_dialog.h b/src/xenia/ui/imgui_gamepad_dialog.h new file mode 100644 index 000000000..461eeb82e --- /dev/null +++ b/src/xenia/ui/imgui_gamepad_dialog.h @@ -0,0 +1,57 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_IMGUI_GAMEPAD_DIALOG_H_ +#define XENIA_UI_IMGUI_GAMEPAD_DIALOG_H_ + +#include "xenia/ui/imgui_dialog.h" + +namespace xe { +namespace hid { +class InputSystem; +} // namespace hid +} // namespace xe + +namespace xe { +namespace ui { + +// Base class for ImGui dialogs with gamepad support. +// Provides common gamepad polling and virtual methods for button handling. +class ImGuiGamepadDialog : public ImGuiDialog { + public: + // Set block_input to false if the dialog needs ImGui's built-in navigation + ImGuiGamepadDialog(ImGuiDrawer* drawer, hid::InputSystem* input_system, + bool block_input = true); + ~ImGuiGamepadDialog() override; + + protected: + // Call this at the start of OnDraw() to poll gamepad input + void PollGamepad(); + + // Override to customize button behavior + virtual void OnGamepadButtonA() {} + virtual void OnGamepadButtonB() { Close(); } + virtual void OnGamepadButtonX() {} + virtual void OnGamepadButtonY() {} + virtual void OnGamepadStart() {} + virtual void OnGamepadBack() { Close(); } + virtual void OnGamepadDPadUp() {} + virtual void OnGamepadDPadDown() {} + virtual void OnGamepadDPadLeft() {} + virtual void OnGamepadDPadRight() {} + + hid::InputSystem* input_system_; + uint16_t prev_buttons_ = 0; + bool blocking_input_ = false; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_IMGUI_GAMEPAD_DIALOG_H_ diff --git a/src/xenia/ui/imgui_performance_dialog.cc b/src/xenia/ui/imgui_performance_dialog.cc new file mode 100644 index 000000000..2a0f755ca --- /dev/null +++ b/src/xenia/ui/imgui_performance_dialog.cc @@ -0,0 +1,314 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/imgui_performance_dialog.h" + +#include "third_party/imgui/imgui.h" +#include "xenia/app/emulator_window.h" +#include "xenia/base/cvar.h" +#include "xenia/config.h" +#include "xenia/emulator.h" +#include "xenia/gpu/command_processor.h" +#include "xenia/gpu/gpu_flags.h" +#include "xenia/gpu/graphics_system.h" +#include "xenia/ui/imgui_host_notification.h" + +DECLARE_bool(clear_memory_page_state); +DECLARE_bool(readback_memexport); +DECLARE_bool(readback_memexport_fast); +DECLARE_string(readback_resolve); +DECLARE_bool(guest_display_refresh_cap); +DECLARE_bool(occlusion_query_enable); + +namespace xe { +namespace ui { + +ImGuiPerformanceDialog::ImGuiPerformanceDialog( + ImGuiDrawer* drawer, app::EmulatorWindow* emulator_window, + hid::InputSystem* input_system) + : ImGuiGamepadDialog(drawer, input_system), + emulator_window_(emulator_window) { + LoadCurrentSettings(); + + // Initialize highlight positions to match current selections + resolve_highlight_ = readback_resolve_mode_; + memexport_highlight_ = readback_memexport_mode_; +} + +void ImGuiPerformanceDialog::OnClose() { + if (on_close_callback_) { + on_close_callback_(); + } +} + +void ImGuiPerformanceDialog::LoadCurrentSettings() { + // Load Emulated Display Uncapped (inverted from guest_display_refresh_cap) + display_uncapped_ = !cvars::guest_display_refresh_cap; + + // Load Occlusion Query setting + occlusion_query_ = cvars::occlusion_query_enable; + + // Load Readback Resolve setting (0=none, 1=some, 2=fast, 3=full) + const std::string& resolve_mode = cvars::readback_resolve; + if (resolve_mode == "none") { + readback_resolve_mode_ = 0; + } else if (resolve_mode == "some") { + readback_resolve_mode_ = 1; + } else if (resolve_mode == "full") { + readback_resolve_mode_ = 3; + } else { + readback_resolve_mode_ = 2; // Default to "fast" + } + + // Load Readback Memexport setting (0=none, 1=fast, 2=full) + if (!cvars::readback_memexport) { + readback_memexport_mode_ = 0; + } else if (cvars::readback_memexport_fast) { + readback_memexport_mode_ = 1; + } else { + readback_memexport_mode_ = 2; + } + + // Load Clear Memory Page State setting + clear_memory_page_state_ = cvars::clear_memory_page_state; +} + +void ImGuiPerformanceDialog::ShowNotification(const std::string& title, + const std::string& description) { + // Position 10 = RIGHT-BOTTOM (default for HostNotificationWindow) + new HostNotificationWindow(imgui_drawer(), title, description, 0); +} + +void ImGuiPerformanceDialog::OnReadbackResolveChanged(int value) { + auto emulator = emulator_window_->emulator(); + if (!emulator) return; + + auto graphics_system = emulator->graphics_system(); + if (!graphics_system) return; + + auto command_processor = graphics_system->command_processor(); + if (!command_processor) return; + + gpu::ReadbackResolveMode mode; + switch (value) { + case 0: + mode = gpu::ReadbackResolveMode::kDisabled; + break; + case 1: + mode = gpu::ReadbackResolveMode::kSome; + break; + case 3: + mode = gpu::ReadbackResolveMode::kFull; + break; + default: + mode = gpu::ReadbackResolveMode::kFast; + break; + } + + command_processor->SetReadbackResolveMode(mode); + + const char* mode_names[] = {"None", "Some", "Fast", "Full"}; + ShowNotification("Readback Resolve", mode_names[value]); +} + +void ImGuiPerformanceDialog::OnReadbackMemexportChanged(int value) { + bool memexport_enabled = true; + bool memexport_fast = true; + + switch (value) { + case 0: + memexport_enabled = false; + break; + case 1: + memexport_fast = true; + break; + case 2: + memexport_fast = false; + break; + } + + gpu::SaveGPUSetting(gpu::GPUSetting::ReadbackMemexport, memexport_enabled); + gpu::SaveGPUSetting(gpu::GPUSetting::ReadbackMemexportFast, memexport_fast); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "readback_memexport", memexport_enabled); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "readback_memexport_fast", memexport_fast); + + const char* mode_names[] = {"None", "Fast", "Full"}; + ShowNotification("Readback Memexport", mode_names[value]); +} + +void ImGuiPerformanceDialog::OnEmulatedDisplayUncappedChanged(bool uncapped) { + SetGuestDisplayRefreshCap(!uncapped); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "guest_display_refresh_cap", !uncapped); + ShowNotification("Emulated Display", uncapped ? "Uncapped" : "Capped"); +} + +void ImGuiPerformanceDialog::OnOcclusionQueryChanged(bool enabled) { + SetOcclusionQueryEnable(enabled); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "occlusion_query_enable", enabled); + ShowNotification("Occlusion Queries", enabled ? "Enabled" : "Disabled"); +} + +void ImGuiPerformanceDialog::OnClearMemoryPageStateChanged(bool enabled) { + gpu::SaveGPUSetting(gpu::GPUSetting::ClearMemoryPageState, enabled); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "clear_memory_page_state", enabled); + ShowNotification("Clear Memory Page State", enabled ? "Enabled" : "Disabled"); +} + +void ImGuiPerformanceDialog::OnDraw(ImGuiIO& io) { + PollGamepad(); + + // Style - white background, black text, Xbox green accents + const ImVec4 xbox_green(0.063f, 0.486f, 0.063f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.7f, 0.7f, 0.7f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_TitleBg, xbox_green); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, xbox_green); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.9f, 0.9f, 0.9f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, + ImVec4(0.85f, 0.85f, 0.85f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, xbox_green); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16, 16)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + + // Center on screen + ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(500, 0), ImGuiCond_Always); + + bool is_open = true; + if (ImGui::Begin("Performance Settings", &is_open, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse)) { + // Handle keyboard + if (ImGui::IsKeyPressed(ImGuiKey_Escape) || + ImGui::IsKeyPressed(ImGuiKey_F7)) { + Close(); + } + + // Colors - highlight uses lighter green for non-selected options + ImVec4 highlight_color = ImVec4(0.1f, 0.6f, 0.1f, 1.0f); + + // Readback Resolve section + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Readback Resolve"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + ImGui::PushID("resolve"); + const char* resolve_labels[] = {"None", "Some", "Fast", "Full"}; + for (int i = 0; i < 4; i++) { + bool is_selected = (readback_resolve_mode_ == i); + bool is_highlighted = (resolve_highlight_ == i); + + if (is_highlighted && !is_selected) { + ImGui::PushStyleColor(ImGuiCol_Text, highlight_color); + } + + if (ImGui::RadioButton(resolve_labels[i], is_selected)) { + if (!is_selected) { + readback_resolve_mode_ = i; + resolve_highlight_ = i; + OnReadbackResolveChanged(i); + } + } + + if (is_highlighted && !is_selected) { + ImGui::PopStyleColor(); + } + + if (i < 3) ImGui::SameLine(); + } + ImGui::PopID(); + ImGui::Unindent(10); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Readback Memexport section + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Readback Memexport"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + ImGui::PushID("memexport"); + const char* memexport_labels[] = {"None", "Fast", "Full"}; + for (int i = 0; i < 3; i++) { + bool is_selected = (readback_memexport_mode_ == i); + bool is_highlighted = (memexport_highlight_ == i); + + if (is_highlighted && !is_selected) { + ImGui::PushStyleColor(ImGuiCol_Text, highlight_color); + } + + if (ImGui::RadioButton(memexport_labels[i], is_selected)) { + if (!is_selected) { + readback_memexport_mode_ = i; + memexport_highlight_ = i; + OnReadbackMemexportChanged(i); + } + } + + if (is_highlighted && !is_selected) { + ImGui::PopStyleColor(); + } + + if (i < 2) ImGui::SameLine(); + } + ImGui::PopID(); + ImGui::Unindent(10); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Other section + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Other"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + + if (ImGui::Checkbox("Emulated Display Uncapped", &display_uncapped_)) { + OnEmulatedDisplayUncappedChanged(display_uncapped_); + } + + if (ImGui::Checkbox("Enable hardware occlusion queries", + &occlusion_query_)) { + OnOcclusionQueryChanged(occlusion_query_); + } + + if (ImGui::Checkbox("Clear memory page state on GPU cache invalidation", + &clear_memory_page_state_)) { + OnClearMemoryPageStateChanged(clear_memory_page_state_); + } + + ImGui::Unindent(10); + + ImGui::End(); + } + + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(9); + + if (!is_open) { + Close(); + } +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/imgui_performance_dialog.h b/src/xenia/ui/imgui_performance_dialog.h new file mode 100644 index 000000000..151fd8e6f --- /dev/null +++ b/src/xenia/ui/imgui_performance_dialog.h @@ -0,0 +1,74 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_IMGUI_PERFORMANCE_DIALOG_H_ +#define XENIA_UI_IMGUI_PERFORMANCE_DIALOG_H_ + +#include +#include + +#include "xenia/ui/imgui_gamepad_dialog.h" + +namespace xe { +namespace app { +class EmulatorWindow; +} // namespace app +} // namespace xe + +namespace xe { +namespace ui { + +// ImGui-based performance tuning dialog with gamepad support. +class ImGuiPerformanceDialog : public ImGuiGamepadDialog { + public: + ImGuiPerformanceDialog(ImGuiDrawer* drawer, + app::EmulatorWindow* emulator_window, + hid::InputSystem* input_system); + + void CloseDialog() { Close(); } + + void SetOnCloseCallback(std::function callback) { + on_close_callback_ = std::move(callback); + } + + protected: + void OnClose() override; + void OnDraw(ImGuiIO& io) override; + + private: + void LoadCurrentSettings(); + void ShowNotification(const std::string& title, + const std::string& description); + + // Setting change handlers + void OnReadbackResolveChanged(int value); + void OnReadbackMemexportChanged(int value); + void OnEmulatedDisplayUncappedChanged(bool uncapped); + void OnOcclusionQueryChanged(bool enabled); + void OnClearMemoryPageStateChanged(bool enabled); + + app::EmulatorWindow* emulator_window_; + std::function on_close_callback_; + + // Current settings state (selected values) + int readback_resolve_mode_ = 2; // 0=None, 1=Some, 2=Fast, 3=Full + int readback_memexport_mode_ = 1; // 0=None, 1=Fast, 2=Full + bool display_uncapped_ = false; + bool occlusion_query_ = false; + bool clear_memory_page_state_ = false; + + // Highlight positions for navigation + int resolve_highlight_ = 2; + int memexport_highlight_ = 1; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_IMGUI_PERFORMANCE_DIALOG_H_ diff --git a/src/xenia/ui/imgui_postprocessing_dialog.cc b/src/xenia/ui/imgui_postprocessing_dialog.cc new file mode 100644 index 000000000..8e3bf7f3d --- /dev/null +++ b/src/xenia/ui/imgui_postprocessing_dialog.cc @@ -0,0 +1,356 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/imgui_postprocessing_dialog.h" + +#include "third_party/imgui/imgui.h" +#include "xenia/app/emulator_window.h" +#include "xenia/base/cvar.h" +#include "xenia/emulator.h" +#include "xenia/gpu/command_processor.h" +#include "xenia/gpu/graphics_system.h" +#include "xenia/ui/imgui_host_notification.h" +#include "xenia/ui/presenter.h" + +DECLARE_string(postprocess_antialiasing); +DECLARE_string(postprocess_scaling_and_sharpening); +DECLARE_double(postprocess_ffx_fsr_sharpness_reduction); +DECLARE_double(postprocess_ffx_cas_additional_sharpness); +DECLARE_uint32(postprocess_ffx_fsr_max_upsampling_passes); +DECLARE_bool(postprocess_dither); + +namespace xe { +namespace ui { + +ImGuiPostProcessingDialog::ImGuiPostProcessingDialog( + ImGuiDrawer* drawer, app::EmulatorWindow* emulator_window, + hid::InputSystem* input_system) + : ImGuiGamepadDialog(drawer, input_system), + emulator_window_(emulator_window) { + LoadCurrentSettings(); + + // Initialize highlight positions to match current selections + aa_highlight_ = anti_aliasing_mode_; + resampling_highlight_ = resampling_mode_; +} + +void ImGuiPostProcessingDialog::OnClose() { + if (on_close_callback_) { + on_close_callback_(); + } +} + +void ImGuiPostProcessingDialog::LoadCurrentSettings() { + // Load anti-aliasing setting + auto aa_effect = app::EmulatorWindow::GetSwapPostEffectForCvarValue( + cvars::postprocess_antialiasing); + switch (aa_effect) { + case gpu::CommandProcessor::SwapPostEffect::kFxaa: + anti_aliasing_mode_ = 1; + break; + case gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme: + anti_aliasing_mode_ = 2; + break; + default: + anti_aliasing_mode_ = 0; + break; + } + + // Load resampling/scaling effect + auto paint_config = app::EmulatorWindow::GetGuestOutputPaintConfigForCvars(); + switch (paint_config.GetEffect()) { + case Presenter::GuestOutputPaintConfig::Effect::kCas: + resampling_mode_ = 1; + break; + case Presenter::GuestOutputPaintConfig::Effect::kFsr: + resampling_mode_ = 2; + break; + default: + resampling_mode_ = 0; + break; + } + + // Load slider values + fsr_sharpness_ = paint_config.GetFsrSharpnessReduction(); + fsr_max_upsampling_passes_ = + static_cast(paint_config.GetFsrMaxUpsamplingPasses()); + cas_additional_sharpness_ = paint_config.GetCasAdditionalSharpness(); + + // Load dithering + dither_ = paint_config.GetDither(); +} + +void ImGuiPostProcessingDialog::ShowNotification( + const std::string& title, const std::string& description) { + // Position 10 = RIGHT-BOTTOM (default for HostNotificationWindow) + new HostNotificationWindow(imgui_drawer(), title, description, 0); +} + +void ImGuiPostProcessingDialog::OnAntiAliasingChanged(int value) { + gpu::CommandProcessor::SwapPostEffect effect; + const char* mode_name; + + switch (value) { + case 1: + effect = gpu::CommandProcessor::SwapPostEffect::kFxaa; + mode_name = "FXAA"; + break; + case 2: + effect = gpu::CommandProcessor::SwapPostEffect::kFxaaExtreme; + mode_name = "FXAA Extreme"; + break; + default: + effect = gpu::CommandProcessor::SwapPostEffect::kNone; + mode_name = "None"; + break; + } + + emulator_window_->UpdateAntiAliasingCvar(effect); + emulator_window_->ApplyDisplayConfigForCvars(); + ShowNotification("Anti-Aliasing", mode_name); +} + +void ImGuiPostProcessingDialog::OnResamplingChanged(int value) { + Presenter::GuestOutputPaintConfig::Effect effect; + const char* mode_name; + + switch (value) { + case 1: + effect = Presenter::GuestOutputPaintConfig::Effect::kCas; + mode_name = "CAS"; + break; + case 2: + effect = Presenter::GuestOutputPaintConfig::Effect::kFsr; + mode_name = "FSR"; + break; + default: + effect = Presenter::GuestOutputPaintConfig::Effect::kBilinear; + mode_name = "Bilinear"; + break; + } + + emulator_window_->UpdateScalingAndSharpeningCvar(effect); + emulator_window_->ApplyDisplayConfigForCvars(); + ShowNotification("Resampling", mode_name); +} + +void ImGuiPostProcessingDialog::OnFsrSharpnessChanged(float value) { + emulator_window_->UpdateFsrSharpnessCvar(value); + emulator_window_->ApplyDisplayConfigForCvars(); +} + +void ImGuiPostProcessingDialog::OnFsrMaxUpsamplingPassesChanged(int value) { + emulator_window_->UpdateFsrMaxUpsamplingPassesCvar( + static_cast(value)); + emulator_window_->ApplyDisplayConfigForCvars(); +} + +void ImGuiPostProcessingDialog::OnCasSharpnessChanged(float value) { + emulator_window_->UpdateCasSharpnessCvar(value); + emulator_window_->ApplyDisplayConfigForCvars(); +} + +void ImGuiPostProcessingDialog::OnDitherChanged(bool value) { + emulator_window_->UpdateDitherCvar(value); + emulator_window_->ApplyDisplayConfigForCvars(); + ShowNotification("Dithering", value ? "Enabled" : "Disabled"); +} + +void ImGuiPostProcessingDialog::OnDraw(ImGuiIO& io) { + PollGamepad(); + + // Style - white background, black text, Xbox green accents + const ImVec4 xbox_green(0.063f, 0.486f, 0.063f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.7f, 0.7f, 0.7f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_TitleBg, xbox_green); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, xbox_green); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.9f, 0.9f, 0.9f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, + ImVec4(0.85f, 0.85f, 0.85f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, xbox_green); + ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, + ImVec4(0.1f, 0.6f, 0.1f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, xbox_green); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.9f, 0.9f, 0.9f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + ImVec4(0.85f, 0.85f, 0.85f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16, 16)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + + // Center on screen + ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(500, 0), ImGuiCond_Always); + + bool is_open = true; + if (ImGui::Begin("Post-Processing Settings", &is_open, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse)) { + // Handle keyboard + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + Close(); + } + + // Colors - highlight uses lighter green for non-selected options + ImVec4 highlight_color = ImVec4(0.1f, 0.6f, 0.1f, 1.0f); + + // Anti-aliasing section + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Anti-Aliasing"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + const char* aa_labels[] = {"None", "FXAA", "FXAA Extreme"}; + for (int i = 0; i < 3; i++) { + bool is_selected = (anti_aliasing_mode_ == i); + bool is_highlighted = (aa_highlight_ == i); + + if (is_highlighted && !is_selected) { + ImGui::PushStyleColor(ImGuiCol_Text, highlight_color); + } + + if (ImGui::RadioButton(aa_labels[i], is_selected)) { + if (!is_selected) { + anti_aliasing_mode_ = i; + aa_highlight_ = i; + OnAntiAliasingChanged(i); + } + } + + if (is_highlighted && !is_selected) { + ImGui::PopStyleColor(); + } + + if (i < 2) ImGui::SameLine(); + } + ImGui::Unindent(10); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Resampling / Scaling section + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Resampling / Scaling"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + const char* resampling_labels[] = {"Bilinear", "CAS", "FSR"}; + for (int i = 0; i < 3; i++) { + bool is_selected = (resampling_mode_ == i); + bool is_highlighted = (resampling_highlight_ == i); + + if (is_highlighted && !is_selected) { + ImGui::PushStyleColor(ImGuiCol_Text, highlight_color); + } + + if (ImGui::RadioButton(resampling_labels[i], is_selected)) { + if (!is_selected) { + resampling_mode_ = i; + resampling_highlight_ = i; + OnResamplingChanged(i); + } + } + + if (is_highlighted && !is_selected) { + ImGui::PopStyleColor(); + } + + if (i < 2) ImGui::SameLine(); + } + ImGui::Unindent(10); + + // FSR-specific options (only show when FSR is selected) + if (resampling_mode_ == 2) { + ImGui::Spacing(); + + ImGui::Indent(10); + ImGui::Text("FSR Sharpness Reduction"); + ImGui::SetNextItemWidth(200); + if (ImGui::SliderFloat("##fsr_sharpness", &fsr_sharpness_, 0.0f, 2.0f, + "%.2f")) { + OnFsrSharpnessChanged(fsr_sharpness_); + } + ImGui::SameLine(); + if (ImGui::Button("Reset##fsr_sharp")) { + fsr_sharpness_ = + Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionDefault; + OnFsrSharpnessChanged(fsr_sharpness_); + } + + ImGui::Text("FSR Max Upsampling Passes"); + ImGui::SetNextItemWidth(200); + if (ImGui::SliderInt( + "##fsr_passes", &fsr_max_upsampling_passes_, 1, + Presenter::GuestOutputPaintConfig::kFsrMaxUpscalingPassesMax)) { + OnFsrMaxUpsamplingPassesChanged(fsr_max_upsampling_passes_); + } + ImGui::SameLine(); + if (ImGui::Button("Reset##fsr_passes")) { + fsr_max_upsampling_passes_ = + Presenter::GuestOutputPaintConfig::kFsrMaxUpscalingPassesMax; + OnFsrMaxUpsamplingPassesChanged(fsr_max_upsampling_passes_); + } + ImGui::Unindent(10); + } + + // CAS-specific options (only show when CAS or FSR is selected) + if (resampling_mode_ >= 1) { + ImGui::Spacing(); + + ImGui::Indent(10); + ImGui::Text("CAS Additional Sharpness"); + ImGui::SetNextItemWidth(200); + if (ImGui::SliderFloat("##cas_sharpness", &cas_additional_sharpness_, + 0.0f, 1.0f, "%.2f")) { + OnCasSharpnessChanged(cas_additional_sharpness_); + } + ImGui::SameLine(); + if (ImGui::Button("Reset##cas_sharp")) { + cas_additional_sharpness_ = + Presenter::GuestOutputPaintConfig::kCasAdditionalSharpnessDefault; + OnCasSharpnessChanged(cas_additional_sharpness_); + } + ImGui::Unindent(10); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Dithering section + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Output"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + if (ImGui::Checkbox("Dithering", &dither_)) { + OnDitherChanged(dither_); + } + ImGui::Unindent(10); + + ImGui::End(); + } + + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(15); + + if (!is_open) { + Close(); + } +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/imgui_postprocessing_dialog.h b/src/xenia/ui/imgui_postprocessing_dialog.h new file mode 100644 index 000000000..f940e5e7d --- /dev/null +++ b/src/xenia/ui/imgui_postprocessing_dialog.h @@ -0,0 +1,77 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_IMGUI_POSTPROCESSING_DIALOG_H_ +#define XENIA_UI_IMGUI_POSTPROCESSING_DIALOG_H_ + +#include +#include + +#include "xenia/ui/imgui_gamepad_dialog.h" + +namespace xe { +namespace app { +class EmulatorWindow; +} // namespace app +} // namespace xe + +namespace xe { +namespace ui { + +// ImGui-based post-processing settings dialog. +// Based on the original DisplayConfigDialog from upstream. +class ImGuiPostProcessingDialog : public ImGuiGamepadDialog { + public: + ImGuiPostProcessingDialog(ImGuiDrawer* drawer, + app::EmulatorWindow* emulator_window, + hid::InputSystem* input_system); + + void CloseDialog() { Close(); } + + void SetOnCloseCallback(std::function callback) { + on_close_callback_ = std::move(callback); + } + + protected: + void OnClose() override; + void OnDraw(ImGuiIO& io) override; + + private: + void LoadCurrentSettings(); + void ShowNotification(const std::string& title, + const std::string& description); + + // Setting change handlers + void OnAntiAliasingChanged(int value); + void OnResamplingChanged(int value); + void OnFsrSharpnessChanged(float value); + void OnFsrMaxUpsamplingPassesChanged(int value); + void OnCasSharpnessChanged(float value); + void OnDitherChanged(bool value); + + app::EmulatorWindow* emulator_window_; + std::function on_close_callback_; + + // Current settings state + int anti_aliasing_mode_ = 0; // 0=None, 1=FXAA, 2=FXAA Extreme + int resampling_mode_ = 0; // 0=Bilinear, 1=CAS, 2=FSR + float fsr_sharpness_ = 0.0f; + int fsr_max_upsampling_passes_ = 5; + float cas_additional_sharpness_ = 0.0f; + bool dither_ = false; + + // Highlight positions for navigation + int aa_highlight_ = 0; + int resampling_highlight_ = 0; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_IMGUI_POSTPROCESSING_DIALOG_H_ diff --git a/src/xenia/ui/imgui_xmp_dialog.cc b/src/xenia/ui/imgui_xmp_dialog.cc new file mode 100644 index 000000000..a8f6ed7e6 --- /dev/null +++ b/src/xenia/ui/imgui_xmp_dialog.cc @@ -0,0 +1,176 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/imgui_xmp_dialog.h" + +#include "third_party/imgui/imgui.h" +#include "xenia/app/emulator_window.h" +#include "xenia/apu/audio_media_player.h" +#include "xenia/emulator.h" +#include "xenia/kernel/xam/apps/xmp_app.h" + +namespace xe { +namespace ui { + +ImGuiXmpDialog::ImGuiXmpDialog(ImGuiDrawer* drawer, + app::EmulatorWindow* emulator_window, + hid::InputSystem* input_system) + : ImGuiGamepadDialog(drawer, input_system), + emulator_window_(emulator_window) { + // Initialize volume from current audio player state + auto emulator = emulator_window_->emulator(); + if (emulator && emulator->audio_media_player()) { + volume_percent_ = static_cast( + emulator->audio_media_player()->GetVolume()->load() * 100.0f); + } +} + +void ImGuiXmpDialog::OnClose() { + if (on_close_callback_) { + on_close_callback_(); + } +} + +void ImGuiXmpDialog::OnDraw(ImGuiIO& io) { + PollGamepad(); + + // Style - white background, black text, Xbox green accents + const ImVec4 xbox_green(0.063f, 0.486f, 0.063f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.7f, 0.7f, 0.7f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_TitleBg, xbox_green); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, xbox_green); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.9f, 0.9f, 0.9f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, + ImVec4(0.85f, 0.85f, 0.85f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, xbox_green); + ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, + ImVec4(0.1f, 0.6f, 0.1f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.9f, 0.9f, 0.9f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + ImVec4(0.85f, 0.85f, 0.85f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16, 16)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + + // Center on screen + ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always); + + bool is_open = true; + if (ImGui::Begin("XMP Audio Player", &is_open, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse)) { + // Handle keyboard + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + Close(); + } + + auto emulator = emulator_window_->emulator(); + auto audio_player = emulator ? emulator->audio_media_player() : nullptr; + + if (audio_player) { + using xmp_state = kernel::xam::apps::XmpApp::State; + + // Status display + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Status"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + ImGui::Text("Audio player status:"); + ImGui::SameLine(); + + switch (audio_player->GetState()) { + case xmp_state::kIdle: + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Idle"); + break; + case xmp_state::kPaused: + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Paused"); + break; + case xmp_state::kPlaying: + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Playing"); + break; + default: + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Unknown"); + break; + } + ImGui::Unindent(10); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Playback controls + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Playback"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + if (audio_player->IsPlaying()) { + if (ImGui::Button("Pause", ImVec2(100, 30))) { + audio_player->Pause(); + } + } else if (audio_player->IsPaused()) { + if (ImGui::Button("Resume", ImVec2(100, 30))) { + audio_player->Continue(); + } + } else { + ImGui::BeginDisabled(); + ImGui::Button("Resume", ImVec2(100, 30)); + ImGui::EndDisabled(); + } + ImGui::Unindent(10); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Volume control + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Volume"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + ImGui::SetNextItemWidth(250); + if (ImGui::SliderInt("##volume", &volume_percent_, 0, 100, "%d%%", + ImGuiSliderFlags_None)) { + // Convert 0-100 back to 0.0-1.0 for the audio player + audio_player->SetVolume(volume_percent_ / 100.0f); + } + // Only sync from player when slider is not being interacted with + if (!ImGui::IsItemActive()) { + volume_percent_ = + static_cast(audio_player->GetVolume()->load() * 100.0f); + } + ImGui::Unindent(10); + + } else { + ImGui::TextColored(ImVec4(0.8f, 0.4f, 0.4f, 1.0f), + "No audio player available"); + } + + ImGui::End(); + } + + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(14); + + if (!is_open) { + Close(); + } +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/imgui_xmp_dialog.h b/src/xenia/ui/imgui_xmp_dialog.h new file mode 100644 index 000000000..9ddd8954f --- /dev/null +++ b/src/xenia/ui/imgui_xmp_dialog.h @@ -0,0 +1,51 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_IMGUI_XMP_DIALOG_H_ +#define XENIA_UI_IMGUI_XMP_DIALOG_H_ + +#include + +#include "xenia/ui/imgui_gamepad_dialog.h" + +namespace xe { +namespace app { +class EmulatorWindow; +} // namespace app +} // namespace xe + +namespace xe { +namespace ui { + +// ImGui-based XMP audio player dialog. +class ImGuiXmpDialog : public ImGuiGamepadDialog { + public: + ImGuiXmpDialog(ImGuiDrawer* drawer, app::EmulatorWindow* emulator_window, + hid::InputSystem* input_system); + + void CloseDialog() { Close(); } + + void SetOnCloseCallback(std::function callback) { + on_close_callback_ = std::move(callback); + } + + protected: + void OnClose() override; + void OnDraw(ImGuiIO& io) override; + + private: + app::EmulatorWindow* emulator_window_; + std::function on_close_callback_; + int volume_percent_ = 0; // 0-100 for proper slider display +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_IMGUI_XMP_DIALOG_H_ diff --git a/src/xenia/ui/performance_tuning_dialog_qt.cc b/src/xenia/ui/performance_tuning_dialog_qt.cc deleted file mode 100644 index ff85f30ab..000000000 --- a/src/xenia/ui/performance_tuning_dialog_qt.cc +++ /dev/null @@ -1,422 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/ui/performance_tuning_dialog_qt.h" - -#include -#include -#include -#include -#include - -#include "xenia/app/emulator_window.h" -#include "xenia/base/cvar.h" -#include "xenia/config.h" -#include "xenia/emulator.h" -#include "xenia/gpu/command_processor.h" -#include "xenia/gpu/gpu_flags.h" -#include "xenia/gpu/graphics_system.h" -#include "xenia/ui/notification_widget_qt.h" - -DECLARE_bool(clear_memory_page_state); -DECLARE_bool(readback_memexport); -DECLARE_bool(readback_memexport_fast); -DECLARE_string(readback_resolve); -DECLARE_bool(guest_display_refresh_cap); -DECLARE_bool(occlusion_query_enable); - -namespace xe { -namespace app { - -PerformanceTuningDialogQt::PerformanceTuningDialogQt( - QWidget* parent, EmulatorWindow* emulator_window, - hid::InputSystem* input_system) - : ui::GamepadDialog(parent, input_system), - emulator_window_(emulator_window) { - SetupUI(); - LoadCurrentSettings(); - - // Position near top, centered horizontally - if (parent) { - QPoint parent_pos = parent->mapToGlobal(QPoint(0, 0)); - int center_x = parent_pos.x() + (parent->width() - width()) / 2; - move(center_x, parent_pos.y() + 20); - } -} - -PerformanceTuningDialogQt::~PerformanceTuningDialogQt() = default; - -void PerformanceTuningDialogQt::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - // Only start drag if clicking directly on the dialog background - QWidget* child = childAt(event->position().toPoint()); - if (!child || child == this) { - drag_position_ = - event->globalPosition().toPoint() - frameGeometry().topLeft(); - dragging_ = true; - event->accept(); - return; - } - } - QDialog::mousePressEvent(event); -} - -void PerformanceTuningDialogQt::mouseMoveEvent(QMouseEvent* event) { - if (dragging_ && (event->buttons() & Qt::LeftButton)) { - move(event->globalPosition().toPoint() - drag_position_); - event->accept(); - return; - } - QDialog::mouseMoveEvent(event); -} - -void PerformanceTuningDialogQt::mouseReleaseEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - dragging_ = false; - } - QDialog::mouseReleaseEvent(event); -} - -void PerformanceTuningDialogQt::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_F7) { - close(); - return; - } - QDialog::keyPressEvent(event); -} - -void PerformanceTuningDialogQt::SetupUI() { - setWindowTitle("Performance Settings"); - setModal(false); - setAttribute(Qt::WA_DeleteOnClose); - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); - setMinimumWidth(500); - - // Set window opacity (0.92 = 92% opaque) - setWindowOpacity(0.92); - - // Set semi-transparent background similar to ImGui - setStyleSheet(R"( - QDialog { - background-color: rgb(30, 30, 30); - border: 1px solid rgba(100, 100, 100, 180); - padding: 0px; - } - QGroupBox { - background-color: rgba(40, 40, 40, 200); - border: 1px solid rgba(80, 80, 80, 150); - margin-top: 12px; - padding-top: 16px; - font-weight: bold; - color: #e0e0e0; - } - QGroupBox::title { - subcontrol-origin: margin; - subcontrol-position: top left; - padding: 4px 8px; - color: #f0f0f0; - } - QLabel { - color: #d0d0d0; - background-color: transparent; - } - QCheckBox { - color: #d0d0d0; - } - QCheckBox::indicator { - width: 16px; - height: 16px; - border-radius: 3px; - border: 2px solid #909090; - background-color: rgba(50, 50, 50, 200); - } - QCheckBox::indicator:hover { - border-color: #b0b0b0; - background-color: rgba(70, 70, 70, 200); - } - QCheckBox::indicator:checked { - border-color: #107c10; - background-color: #107c10; - } - QRadioButton { - color: #d0d0d0; - } - QRadioButton::indicator { - width: 14px; - height: 14px; - border-radius: 7px; - border: 2px solid #909090; - background-color: rgba(50, 50, 50, 200); - } - QRadioButton::indicator:hover { - border-color: #b0b0b0; - background-color: rgba(70, 70, 70, 200); - } - QRadioButton::indicator:checked { - border-color: #107c10; - background-color: #107c10; - } - )"); - - auto* content_layout = new QVBoxLayout(this); - content_layout->setContentsMargins(16, 16, 16, 16); - content_layout->setSpacing(12); - - // Top bar with title and close button - auto* top_bar_layout = new QHBoxLayout(); - auto* title_label = new QLabel("Performance Settings", this); - title_label->setStyleSheet( - "color: #f0f0f0; font-weight: bold; font-size: 14px;"); - top_bar_layout->addWidget(title_label); - top_bar_layout->addStretch(); - - auto* close_button = new QPushButton(this); - close_button->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton)); - close_button->setIconSize(QSize(16, 16)); - close_button->setFlat(true); - close_button->setStyleSheet(R"( - QPushButton { - background-color: transparent; - border: none; - min-width: 24px; - max-width: 24px; - min-height: 24px; - max-height: 24px; - padding: 2px; - } - QPushButton:hover { - background-color: rgba(200, 50, 50, 180); - border-radius: 4px; - } - QPushButton:pressed { - background-color: rgba(150, 30, 30, 220); - border-radius: 4px; - } - )"); - close_button->setToolTip("Close"); - connect(close_button, &QPushButton::clicked, this, &QDialog::close); - top_bar_layout->addWidget(close_button, 0, Qt::AlignTop); - - content_layout->addLayout(top_bar_layout); - - // Readback Resolve group - readback_resolve_group_ = new QGroupBox("Readback Resolve", this); - auto* resolve_layout = new QHBoxLayout(readback_resolve_group_); - resolve_layout->setSpacing(16); - - readback_resolve_button_group_ = new QButtonGroup(this); - const char* resolve_labels[] = {"None", "Some", "Fast", "Full"}; - for (int i = 0; i < 4; i++) { - auto* radio = new QRadioButton(resolve_labels[i], readback_resolve_group_); - readback_resolve_button_group_->addButton(radio, i); - resolve_layout->addWidget(radio); - } - resolve_layout->addStretch(); - - connect(readback_resolve_button_group_, &QButtonGroup::idClicked, this, - &PerformanceTuningDialogQt::OnReadbackResolveChanged); - - content_layout->addWidget(readback_resolve_group_); - - // Readback Memexport group - readback_memexport_group_ = new QGroupBox("Readback Memexport", this); - auto* memexport_layout = new QHBoxLayout(readback_memexport_group_); - memexport_layout->setSpacing(16); - - readback_memexport_button_group_ = new QButtonGroup(this); - const char* memexport_labels[] = {"None", "Fast", "Full"}; - for (int i = 0; i < 3; i++) { - auto* radio = - new QRadioButton(memexport_labels[i], readback_memexport_group_); - readback_memexport_button_group_->addButton(radio, i); - memexport_layout->addWidget(radio); - } - memexport_layout->addStretch(); - - connect(readback_memexport_button_group_, &QButtonGroup::idClicked, this, - &PerformanceTuningDialogQt::OnReadbackMemexportChanged); - - content_layout->addWidget(readback_memexport_group_); - - // Other settings group - other_group_ = new QGroupBox("Other", this); - auto* other_layout = new QVBoxLayout(other_group_); - other_layout->setSpacing(6); - - emulated_display_uncapped_checkbox_ = - new QCheckBox("Emulated Display Uncapped", other_group_); - other_layout->addWidget(emulated_display_uncapped_checkbox_); - connect(emulated_display_uncapped_checkbox_, &QCheckBox::checkStateChanged, - this, &PerformanceTuningDialogQt::OnEmulatedDisplayUncappedChanged); - - occlusion_query_checkbox_ = - new QCheckBox("Enable hardware occlusion queries", other_group_); - other_layout->addWidget(occlusion_query_checkbox_); - connect(occlusion_query_checkbox_, &QCheckBox::checkStateChanged, this, - &PerformanceTuningDialogQt::OnOcclusionQueryChanged); - - clear_memory_checkbox_ = new QCheckBox( - "Clear memory page state on GPU cache invalidation", other_group_); - other_layout->addWidget(clear_memory_checkbox_); - connect(clear_memory_checkbox_, &QCheckBox::checkStateChanged, this, - &PerformanceTuningDialogQt::OnClearMemoryPageStateChanged); - - content_layout->addWidget(other_group_); - - content_layout->addStretch(); -} - -void PerformanceTuningDialogQt::LoadCurrentSettings() { - // Block signals to prevent notifications during initial load - emulated_display_uncapped_checkbox_->blockSignals(true); - occlusion_query_checkbox_->blockSignals(true); - readback_resolve_button_group_->blockSignals(true); - readback_memexport_button_group_->blockSignals(true); - clear_memory_checkbox_->blockSignals(true); - - // Load Emulated Display Uncapped setting (inverted from - // guest_display_refresh_cap) - emulated_display_uncapped_checkbox_->setChecked( - !cvars::guest_display_refresh_cap); - - // Load Occlusion Query setting - occlusion_query_checkbox_->setChecked(cvars::occlusion_query_enable); - - // Load Readback Resolve setting (0=none, 1=some, 2=fast, 3=full) - const std::string& resolve_mode = cvars::readback_resolve; - int resolve_index = 2; // Default to "fast" - if (resolve_mode == "none") { - resolve_index = 0; - } else if (resolve_mode == "some") { - resolve_index = 1; - } else if (resolve_mode == "full") { - resolve_index = 3; - } - if (auto* button = readback_resolve_button_group_->button(resolve_index)) { - button->setChecked(true); - } - - // Load Readback Memexport setting (0=none, 1=fast, 2=full) - int memexport_index = 1; // Default to "fast" - if (!cvars::readback_memexport) { - memexport_index = 0; - } else if (!cvars::readback_memexport_fast) { - memexport_index = 2; - } - if (auto* button = - readback_memexport_button_group_->button(memexport_index)) { - button->setChecked(true); - } - - // Load Clear Memory Page State setting - clear_memory_checkbox_->setChecked(cvars::clear_memory_page_state); - - // Restore signals - emulated_display_uncapped_checkbox_->blockSignals(false); - occlusion_query_checkbox_->blockSignals(false); - readback_resolve_button_group_->blockSignals(false); - readback_memexport_button_group_->blockSignals(false); - clear_memory_checkbox_->blockSignals(false); -} - -void PerformanceTuningDialogQt::ShowNotification(const QString& title, - const QString& description) { - auto* notification = - new NotificationWidgetQt(parentWidget(), title, description, 2000); - notification->Show(); -} - -void PerformanceTuningDialogQt::OnEmulatedDisplayUncappedChanged(int state) { - bool uncapped = (state == Qt::Checked); - // Inverted: uncapped = guest_display_refresh_cap disabled - SetGuestDisplayRefreshCap(!uncapped); - config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", - "guest_display_refresh_cap", !uncapped); - ShowNotification("Emulated Display", uncapped ? "Uncapped" : "Capped"); -} - -void PerformanceTuningDialogQt::OnOcclusionQueryChanged(int state) { - bool enabled = (state == Qt::Checked); - SetOcclusionQueryEnable(enabled); - config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", - "occlusion_query_enable", enabled); - ShowNotification("Occlusion Queries", enabled ? "Enabled" : "Disabled"); -} - -void PerformanceTuningDialogQt::OnReadbackResolveChanged(int value) { - auto emulator = emulator_window_->emulator(); - if (!emulator) return; - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) return; - - auto command_processor = graphics_system->command_processor(); - if (!command_processor) return; - - // Slider: 0=none, 1=some, 2=fast, 3=full - gpu::ReadbackResolveMode mode; - switch (value) { - case 0: - mode = gpu::ReadbackResolveMode::kDisabled; - break; - case 1: - mode = gpu::ReadbackResolveMode::kSome; - break; - case 3: - mode = gpu::ReadbackResolveMode::kFull; - break; - default: - mode = gpu::ReadbackResolveMode::kFast; - break; - } - - command_processor->SetReadbackResolveMode(mode); - - const char* mode_names[] = {"None", "Some", "Fast", "Full"}; - ShowNotification("Readback Resolve", mode_names[value]); -} - -void PerformanceTuningDialogQt::OnReadbackMemexportChanged(int value) { - // Slider: 0=none, 1=fast, 2=full - bool memexport_enabled = true; - bool memexport_fast = true; - - switch (value) { - case 0: // None - memexport_enabled = false; - break; - case 1: // Fast - memexport_fast = true; - break; - case 2: // Full - memexport_fast = false; - break; - } - - gpu::SaveGPUSetting(gpu::GPUSetting::ReadbackMemexport, memexport_enabled); - gpu::SaveGPUSetting(gpu::GPUSetting::ReadbackMemexportFast, memexport_fast); - config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", - "readback_memexport", memexport_enabled); - config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", - "readback_memexport_fast", memexport_fast); - - const char* mode_names[] = {"None", "Fast", "Full"}; - ShowNotification("Readback Memexport", mode_names[value]); -} - -void PerformanceTuningDialogQt::OnClearMemoryPageStateChanged(int state) { - bool enabled = (state == Qt::Checked); - gpu::SaveGPUSetting(gpu::GPUSetting::ClearMemoryPageState, enabled); - config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", - "clear_memory_page_state", enabled); - ShowNotification("Clear Memory Page State", enabled ? "Enabled" : "Disabled"); -} - -} // namespace app -} // namespace xe diff --git a/src/xenia/ui/performance_tuning_dialog_qt.h b/src/xenia/ui/performance_tuning_dialog_qt.h deleted file mode 100644 index 4cf374fba..000000000 --- a/src/xenia/ui/performance_tuning_dialog_qt.h +++ /dev/null @@ -1,76 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_UI_PERFORMANCE_TUNING_DIALOG_QT_H_ -#define XENIA_UI_PERFORMANCE_TUNING_DIALOG_QT_H_ - -#include -#include -#include -#include -#include -#include -#include - -#include "xenia/ui/gamepad_dialog_qt.h" - -namespace xe { -namespace app { - -class EmulatorWindow; - -class PerformanceTuningDialogQt : public ui::GamepadDialog { - Q_OBJECT - - public: - PerformanceTuningDialogQt(QWidget* parent, EmulatorWindow* emulator_window, - hid::InputSystem* input_system); - ~PerformanceTuningDialogQt() override; - - private slots: - void OnEmulatedDisplayUncappedChanged(int state); - void OnOcclusionQueryChanged(int state); - void OnReadbackResolveChanged(int value); - void OnReadbackMemexportChanged(int value); - void OnClearMemoryPageStateChanged(int state); - - protected: - void keyPressEvent(QKeyEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - - private: - void SetupUI(); - void LoadCurrentSettings(); - void ShowNotification(const QString& title, const QString& description); - - EmulatorWindow* emulator_window_; - QPoint drag_position_; - bool dragging_ = false; - - // Readback Resolve widgets - QGroupBox* readback_resolve_group_; - QButtonGroup* readback_resolve_button_group_; - - // Readback Memexport widgets - QGroupBox* readback_memexport_group_; - QButtonGroup* readback_memexport_button_group_; - - // Other settings widgets - QGroupBox* other_group_; - QCheckBox* emulated_display_uncapped_checkbox_; - QCheckBox* occlusion_query_checkbox_; - QCheckBox* clear_memory_checkbox_; -}; - -} // namespace app -} // namespace xe - -#endif // XENIA_UI_PERFORMANCE_TUNING_DIALOG_QT_H_ diff --git a/src/xenia/ui/postprocessing_dialog_qt.cc b/src/xenia/ui/postprocessing_dialog_qt.cc deleted file mode 100644 index 920462294..000000000 --- a/src/xenia/ui/postprocessing_dialog_qt.cc +++ /dev/null @@ -1,801 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/ui/postprocessing_dialog_qt.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "third_party/fmt/include/fmt/format.h" -#include "xenia/app/emulator_window.h" -#include "xenia/base/cvar.h" -#include "xenia/config.h" -#include "xenia/gpu/graphics_system.h" -#include "xenia/ui/qt_util.h" - -DECLARE_bool(postprocess_dither); -DECLARE_double(postprocess_ffx_cas_additional_sharpness); -DECLARE_double(postprocess_ffx_fsr_sharpness_reduction); -DECLARE_uint32(postprocess_ffx_fsr_max_upsampling_passes); -DECLARE_string(postprocess_antialiasing); -DECLARE_string(postprocess_scaling_and_sharpening); - -namespace xe { -namespace app { - -using xe::ui::SafeQString; - -PostProcessingDialogQt::PostProcessingDialogQt(QWidget* parent, - EmulatorWindow* emulator_window, - hid::InputSystem* input_system) - : ui::GamepadDialog(parent, input_system), - emulator_window_(emulator_window) { - SetupUI(); - LoadCurrentSettings(); - - // Position near top, centered horizontally - if (parent) { - QPoint parent_pos = parent->mapToGlobal(QPoint(0, 0)); - int center_x = parent_pos.x() + (parent->width() - width()) / 2; - move(center_x, parent_pos.y() + 20); - } -} - -PostProcessingDialogQt::~PostProcessingDialogQt() = default; - -void PostProcessingDialogQt::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - drag_position_ = event->position().toPoint(); - event->accept(); - } -} - -void PostProcessingDialogQt::mouseMoveEvent(QMouseEvent* event) { - if (event->buttons() & Qt::LeftButton) { - QPoint global_pos = event->globalPosition().toPoint(); - move(global_pos - drag_position_); - event->accept(); - } -} - -void PostProcessingDialogQt::SetupUI() { - setWindowTitle("Post-processing"); - setModal(false); - setAttribute(Qt::WA_DeleteOnClose); - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); - setMinimumWidth(500); - - // Set window opacity (0.92 = 92% opaque) - setWindowOpacity(0.92); - - // Set semi-transparent background similar to ImGui - setStyleSheet(R"( - QDialog { - background-color: rgb(30, 30, 30); - border: 1px solid rgba(100, 100, 100, 180); - padding: 0px; - } - QGroupBox { - background-color: rgba(40, 40, 40, 200); - border: 1px solid rgba(80, 80, 80, 150); - margin-top: 12px; - padding-top: 16px; - font-weight: bold; - color: #e0e0e0; - } - QGroupBox::title { - subcontrol-origin: margin; - subcontrol-position: top left; - padding: 4px 8px; - color: #f0f0f0; - } - QLabel { - color: #d0d0d0; - background-color: transparent; - } - QRadioButton::indicator { - width: 16px; - height: 16px; - border-radius: 8px; - border: 2px solid #909090; - background-color: rgba(50, 50, 50, 200); - } - QRadioButton::indicator:hover { - border-color: #b0b0b0; - background-color: rgba(70, 70, 70, 200); - } - QRadioButton::indicator:checked { - border-color: #107c10; - background-color: #107c10; - } - QCheckBox::indicator { - width: 16px; - height: 16px; - border-radius: 3px; - border: 2px solid #909090; - background-color: rgba(50, 50, 50, 200); - } - QCheckBox::indicator:hover { - border-color: #b0b0b0; - background-color: rgba(70, 70, 70, 200); - } - QCheckBox::indicator:checked { - border-color: #107c10; - background-color: #107c10; - } - )"); - - auto* content_layout = new QVBoxLayout(this); - content_layout->setContentsMargins(16, 16, 16, 16); - content_layout->setSpacing(12); - - // Top bar with close button - auto* top_bar_layout = new QHBoxLayout(); - auto* info_label = - new QLabel("All effects can be used on GPUs of any brand.", this); - info_label->setWordWrap(true); - info_label->setStyleSheet("color: #b0b0b0; font-style: italic;"); - top_bar_layout->addWidget(info_label); - - auto* close_button = new QPushButton(this); - close_button->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton)); - close_button->setIconSize(QSize(16, 16)); - close_button->setFlat(true); - close_button->setStyleSheet(R"( - QPushButton { - background-color: transparent; - border: none; - min-width: 24px; - max-width: 24px; - min-height: 24px; - max-height: 24px; - padding: 2px; - } - QPushButton:hover { - background-color: rgba(200, 50, 50, 180); - border-radius: 4px; - } - QPushButton:pressed { - background-color: rgba(150, 30, 30, 220); - border-radius: 4px; - } - )"); - close_button->setToolTip("Close (F6)"); - connect(close_button, &QPushButton::clicked, this, &QDialog::close); - top_bar_layout->addWidget(close_button, 0, Qt::AlignTop); - - content_layout->addLayout(top_bar_layout); - - content_layout->addSpacing(6); - - // Anti-aliasing group - aa_group_ = new QGroupBox("Anti-aliasing", this); - auto* aa_layout = new QVBoxLayout(aa_group_); - aa_layout->setSpacing(6); - - aa_button_group_ = new QButtonGroup(this); - aa_none_radio_ = new QRadioButton("None", aa_group_); - aa_fxaa_radio_ = new QRadioButton( - "NVIDIA Fast Approximate Anti-Aliasing (FXAA) [Normal Quality]", - aa_group_); - aa_fxaa_extreme_radio_ = new QRadioButton( - "NVIDIA Fast Approximate Anti-Aliasing (FXAA) [Extreme Quality]", - aa_group_); - - aa_button_group_->addButton(aa_none_radio_, 0); - aa_button_group_->addButton(aa_fxaa_radio_, 1); - aa_button_group_->addButton(aa_fxaa_extreme_radio_, 2); - - aa_layout->addWidget(aa_none_radio_); - aa_layout->addWidget(aa_fxaa_radio_); - aa_layout->addWidget(aa_fxaa_extreme_radio_); - - connect(aa_button_group_, QOverload::of(&QButtonGroup::idClicked), this, - &PostProcessingDialogQt::OnAntiAliasingChanged); - - content_layout->addWidget(aa_group_); - - // Resampling and sharpening group - resampling_group_ = new QGroupBox("Resampling and sharpening", this); - auto* resampling_layout = new QVBoxLayout(resampling_group_); - resampling_layout->setSpacing(8); - - resampling_button_group_ = new QButtonGroup(this); - - // None / Bilinear option (no controls) - effect_bilinear_radio_ = - new QRadioButton("None / Bilinear", resampling_group_); - resampling_button_group_->addButton( - effect_bilinear_radio_, - static_cast( - ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear)); - resampling_layout->addWidget(effect_bilinear_radio_); - - // CAS option with controls as children - auto* cas_container = new QWidget(resampling_group_); - auto* cas_container_layout = new QVBoxLayout(cas_container); - cas_container_layout->setContentsMargins(0, 0, 0, 0); - cas_container_layout->setSpacing(4); - - effect_cas_radio_ = new QRadioButton( - "AMD FidelityFX Contrast Adaptive Sharpening (CAS)", cas_container); - resampling_button_group_->addButton( - effect_cas_radio_, - static_cast(ui::Presenter::GuestOutputPaintConfig::Effect::kCas)); - cas_container_layout->addWidget(effect_cas_radio_); - - // CAS controls as children of cas_container - cas_sharpness_widget_ = new QWidget(cas_container); - auto* cas_layout = new QVBoxLayout(cas_sharpness_widget_); - cas_layout->setContentsMargins(20, 4, 0, 0); - - cas_sharpness_label_ = new QLabel( - "CAS additional sharpness (higher is sharper):", cas_sharpness_widget_); - cas_layout->addWidget(cas_sharpness_label_); - - auto* cas_slider_layout = new QHBoxLayout(); - cas_sharpness_slider_ = new QSlider(Qt::Horizontal, cas_sharpness_widget_); - cas_sharpness_slider_->setRange(0, 100); // Will be mapped to 0.0-1.0 - cas_sharpness_slider_->setFocusPolicy(Qt::StrongFocus); - cas_sharpness_value_label_ = new QLabel("0%", cas_sharpness_widget_); - cas_sharpness_value_label_->setMinimumWidth(50); - cas_reset_button_ = new QPushButton("Reset", cas_sharpness_widget_); - cas_reset_button_->setMaximumWidth(50); - cas_reset_button_->setFocusPolicy(Qt::StrongFocus); - - cas_slider_layout->addWidget(cas_sharpness_slider_); - cas_slider_layout->addWidget(cas_sharpness_value_label_); - cas_slider_layout->addWidget(cas_reset_button_); - cas_layout->addLayout(cas_slider_layout); - - cas_container_layout->addWidget(cas_sharpness_widget_); - resampling_layout->addWidget(cas_container); - - // FSR option with controls as children - auto* fsr_container = new QWidget(resampling_group_); - auto* fsr_container_layout = new QVBoxLayout(fsr_container); - fsr_container_layout->setContentsMargins(0, 0, 0, 0); - fsr_container_layout->setSpacing(4); - - effect_fsr_radio_ = new QRadioButton( - "AMD FidelityFX Super Resolution 1.0 (FSR)", fsr_container); - resampling_button_group_->addButton( - effect_fsr_radio_, - static_cast(ui::Presenter::GuestOutputPaintConfig::Effect::kFsr)); - fsr_container_layout->addWidget(effect_fsr_radio_); - - // FSR sharpness controls as children of fsr_container - fsr_sharpness_widget_ = new QWidget(fsr_container); - auto* fsr_layout = new QVBoxLayout(fsr_sharpness_widget_); - fsr_layout->setContentsMargins(20, 4, 0, 0); - - fsr_sharpness_label_ = - new QLabel("FSR sharpness reduction when upscaling (lower is sharper):", - fsr_sharpness_widget_); - fsr_layout->addWidget(fsr_sharpness_label_); - - auto* fsr_slider_layout = new QHBoxLayout(); - fsr_sharpness_slider_ = new QSlider(Qt::Horizontal, fsr_sharpness_widget_); - fsr_sharpness_slider_->setRange(0, 200); // Will be mapped to 0.0-2.0 - fsr_sharpness_slider_->setFocusPolicy(Qt::StrongFocus); - fsr_sharpness_value_label_ = new QLabel("0%", fsr_sharpness_widget_); - fsr_sharpness_value_label_->setMinimumWidth(50); - fsr_reset_button_ = new QPushButton("Reset", fsr_sharpness_widget_); - fsr_reset_button_->setMaximumWidth(50); - fsr_reset_button_->setFocusPolicy(Qt::StrongFocus); - - fsr_slider_layout->addWidget(fsr_sharpness_slider_); - fsr_slider_layout->addWidget(fsr_sharpness_value_label_); - fsr_slider_layout->addWidget(fsr_reset_button_); - fsr_layout->addLayout(fsr_slider_layout); - - fsr_container_layout->addWidget(fsr_sharpness_widget_); - - // FSR max upsampling controls as children of fsr_container - fsr_max_upsampling_widget_ = new QWidget(fsr_container); - auto* fsr_upsampling_layout = new QVBoxLayout(fsr_max_upsampling_widget_); - fsr_upsampling_layout->setContentsMargins(20, 4, 0, 0); - - fsr_max_upsampling_label_ = new QLabel( - "FSR max upsampling passes (lower is faster, higher is better quality):", - fsr_max_upsampling_widget_); - fsr_upsampling_layout->addWidget(fsr_max_upsampling_label_); - - fsr_max_upsampling_button_group_ = new QButtonGroup(this); - fsr_max_upsampling_radio_1_ = - new QRadioButton("1", fsr_max_upsampling_widget_); - fsr_max_upsampling_radio_1_->setFocusPolicy(Qt::StrongFocus); - fsr_max_upsampling_radio_2_ = - new QRadioButton("2", fsr_max_upsampling_widget_); - fsr_max_upsampling_radio_2_->setFocusPolicy(Qt::StrongFocus); - fsr_max_upsampling_radio_3_ = - new QRadioButton("3", fsr_max_upsampling_widget_); - fsr_max_upsampling_radio_3_->setFocusPolicy(Qt::StrongFocus); - fsr_max_upsampling_radio_4_ = - new QRadioButton("4", fsr_max_upsampling_widget_); - fsr_max_upsampling_radio_4_->setFocusPolicy(Qt::StrongFocus); - - fsr_max_upsampling_button_group_->addButton(fsr_max_upsampling_radio_1_, 1); - fsr_max_upsampling_button_group_->addButton(fsr_max_upsampling_radio_2_, 2); - fsr_max_upsampling_button_group_->addButton(fsr_max_upsampling_radio_3_, 3); - fsr_max_upsampling_button_group_->addButton(fsr_max_upsampling_radio_4_, 4); - - auto* fsr_upsampling_radio_layout = new QHBoxLayout(); - fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_radio_1_); - fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_radio_2_); - fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_radio_3_); - fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_radio_4_); - fsr_max_upsampling_reset_button_ = - new QPushButton("Reset", fsr_max_upsampling_widget_); - fsr_max_upsampling_reset_button_->setMaximumWidth(50); - fsr_max_upsampling_reset_button_->setFocusPolicy(Qt::StrongFocus); - fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_reset_button_); - - fsr_upsampling_layout->addLayout(fsr_upsampling_radio_layout); - fsr_container_layout->addWidget(fsr_max_upsampling_widget_); - - resampling_layout->addWidget(fsr_container); - - connect(resampling_button_group_, - QOverload::of(&QButtonGroup::idClicked), this, - &PostProcessingDialogQt::OnResamplingEffectChanged); - - // Connect all the signal handlers - connect(fsr_sharpness_slider_, &QSlider::valueChanged, this, - &PostProcessingDialogQt::OnFsrSharpnessChanged); - connect(fsr_reset_button_, &QPushButton::clicked, this, - &PostProcessingDialogQt::OnResetFsrSharpness); - - connect(fsr_max_upsampling_button_group_, - QOverload::of(&QButtonGroup::idClicked), this, - &PostProcessingDialogQt::OnFsrMaxUpsamplingPassesChanged); - connect(fsr_max_upsampling_reset_button_, &QPushButton::clicked, this, - &PostProcessingDialogQt::OnResetFsrMaxUpsamplingPasses); - - connect(cas_sharpness_slider_, &QSlider::valueChanged, this, - &PostProcessingDialogQt::OnCasSharpnessChanged); - connect(cas_reset_button_, &QPushButton::clicked, this, - &PostProcessingDialogQt::OnResetCasSharpness); - - // Effect description - effect_description_label_ = new QLabel(resampling_group_); - effect_description_label_->setWordWrap(true); - effect_description_label_->setStyleSheet( - "QLabel { color: #999; margin-top: 8px; font-size: 11px; }"); - resampling_layout->addWidget(effect_description_label_); - - // FXAA recommendation label - fxaa_recommendation_label_ = new QLabel( - "FXAA is highly recommended when using CAS or FSR.", resampling_group_); - fxaa_recommendation_label_->setWordWrap(true); - fxaa_recommendation_label_->setStyleSheet( - "QLabel { margin-top: 10px; color: #aad4ff; font-weight: bold; }"); - resampling_layout->addWidget(fxaa_recommendation_label_); - - content_layout->addWidget(resampling_group_); - - // Dithering group - dither_group_ = new QGroupBox("Dithering", this); - auto* dither_layout = new QVBoxLayout(dither_group_); - dither_layout->setSpacing(6); - - dither_checkbox_ = new QCheckBox( - "Dither the final output to 8bpc to make gradients smoother", - dither_group_); - dither_layout->addWidget(dither_checkbox_); - - connect(dither_checkbox_, &QCheckBox::checkStateChanged, this, - &PostProcessingDialogQt::OnDitherChanged); - - content_layout->addWidget(dither_group_); - - content_layout->addStretch(); -} - -void PostProcessingDialogQt::LoadCurrentSettings() { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - // Block signals while loading to prevent triggering save handlers - aa_button_group_->blockSignals(true); - resampling_button_group_->blockSignals(true); - fsr_sharpness_slider_->blockSignals(true); - fsr_max_upsampling_button_group_->blockSignals(true); - cas_sharpness_slider_->blockSignals(true); - dither_checkbox_->blockSignals(true); - - // Load anti-aliasing settings - auto command_processor = graphics_system->command_processor(); - if (command_processor) { - auto current_aa = command_processor->GetDesiredSwapPostEffect(); - aa_button_group_->button(static_cast(current_aa))->setChecked(true); - } - - // Load resampling and sharpening settings - auto presenter = graphics_system->presenter(); - if (presenter) { - const auto& config = presenter->GetGuestOutputPaintConfigFromUIThread(); - - // Set effect radio button - resampling_button_group_->button(static_cast(config.GetEffect())) - ->setChecked(true); - - // Set FSR sharpness (convert from 0.0-2.0 to 0-200) - float fsr_sharpness = config.GetFsrSharpnessReduction(); - // Apply power 2.0 scaling as done in ImGui version for slider position - float slider_position = sqrt(2.f * fsr_sharpness); - fsr_sharpness_slider_->setValue(static_cast(slider_position * 100)); - // Update label to show actual FSR sharpness percentage - fsr_sharpness_value_label_->setText(SafeQString( - fmt::format("{} %", static_cast(fsr_sharpness * 100)))); - - // Set FSR max upsampling passes - uint32_t fsr_max_upsampling = config.GetFsrMaxUpsamplingPasses(); - // Clamp to valid range 1-4 - fsr_max_upsampling = - std::max(uint32_t(1), std::min(uint32_t(4), fsr_max_upsampling)); - fsr_max_upsampling_button_group_ - ->button(static_cast(fsr_max_upsampling)) - ->setChecked(true); - - // Set CAS sharpness (convert from 0.0-1.0 to 0-100) - float cas_sharpness = config.GetCasAdditionalSharpness(); - cas_sharpness_slider_->setValue(static_cast(cas_sharpness * 100)); - // Update label to match slider value - cas_sharpness_value_label_->setText(SafeQString( - fmt::format("{} %", static_cast(cas_sharpness * 100)))); - - // Set dithering - dither_checkbox_->setChecked(config.GetDither()); - } - - // Re-enable signals - aa_button_group_->blockSignals(false); - resampling_button_group_->blockSignals(false); - fsr_sharpness_slider_->blockSignals(false); - fsr_max_upsampling_button_group_->blockSignals(false); - cas_sharpness_slider_->blockSignals(false); - dither_checkbox_->blockSignals(false); - - UpdateEffectDescription(); - UpdateSharpnessControls(); -} - -void PostProcessingDialogQt::OnAntiAliasingChanged(int index) { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - auto command_processor = graphics_system->command_processor(); - if (!command_processor) { - return; - } - - auto new_effect = static_cast(index); - command_processor->SetDesiredSwapPostEffect(new_effect); - - // Update cvar - emulator_window_->UpdateAntiAliasingCvar(new_effect); - - // Save to game config - const char* effect_str = - EmulatorWindow::GetCvarValueForSwapPostEffect(new_effect); - config::SaveGameConfigSetting(emulator, "Display", "postprocess_antialiasing", - std::string(effect_str)); -} - -void PostProcessingDialogQt::OnResamplingEffectChanged(int index) { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - auto presenter = graphics_system->presenter(); - if (!presenter) { - return; - } - - auto config = presenter->GetGuestOutputPaintConfigFromUIThread(); - auto new_effect = - static_cast(index); - config.SetEffect(new_effect); - presenter->SetGuestOutputPaintConfigFromUIThread(config); - - // Update cvar - emulator_window_->UpdateScalingAndSharpeningCvar(new_effect); - - // Save to game config - const char* effect_str = - EmulatorWindow::GetCvarValueForGuestOutputPaintEffect(new_effect); - config::SaveGameConfigSetting(emulator, "Display", - "postprocess_scaling_and_sharpening", - std::string(effect_str)); - - UpdateEffectDescription(); - UpdateSharpnessControls(); -} - -void PostProcessingDialogQt::OnFsrSharpnessChanged(int value) { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - auto presenter = graphics_system->presenter(); - if (!presenter) { - return; - } - - // Convert slider value (0-200) to FSR sharpness (0.0-2.0) with power 2.0 - // scaling - float slider_value = value / 100.0f; - float fsr_sharpness = 0.5f * slider_value * slider_value; - - auto config = presenter->GetGuestOutputPaintConfigFromUIThread(); - config.SetFsrSharpnessReduction(fsr_sharpness); - presenter->SetGuestOutputPaintConfigFromUIThread(config); - - // Update label - fsr_sharpness_value_label_->setText( - SafeQString(fmt::format("{} %", static_cast(fsr_sharpness * 100)))); - - // Update cvar - emulator_window_->UpdateFsrSharpnessCvar(fsr_sharpness); - - // Save to game config - config::SaveGameConfigSetting(emulator, "Display", - "postprocess_ffx_fsr_sharpness_reduction", - fsr_sharpness); -} - -void PostProcessingDialogQt::OnCasSharpnessChanged(int value) { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - auto presenter = graphics_system->presenter(); - if (!presenter) { - return; - } - - // Convert slider value (0-100) to CAS sharpness (0.0-1.0) - float cas_sharpness = value / 100.0f; - - auto config = presenter->GetGuestOutputPaintConfigFromUIThread(); - config.SetCasAdditionalSharpness(cas_sharpness); - presenter->SetGuestOutputPaintConfigFromUIThread(config); - - // Update label - cas_sharpness_value_label_->setText( - SafeQString(fmt::format("{} %", static_cast(cas_sharpness * 100)))); - - // Update cvar - emulator_window_->UpdateCasSharpnessCvar(cas_sharpness); - - // Save to game config - config::SaveGameConfigSetting(emulator, "Display", - "postprocess_ffx_cas_additional_sharpness", - cas_sharpness); -} - -void PostProcessingDialogQt::OnDitherChanged(int state) { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - auto presenter = graphics_system->presenter(); - if (!presenter) { - return; - } - - bool dither = (state == Qt::Checked); - - auto config = presenter->GetGuestOutputPaintConfigFromUIThread(); - config.SetDither(dither); - presenter->SetGuestOutputPaintConfigFromUIThread(config); - - // Update cvar - emulator_window_->UpdateDitherCvar(dither); - - // Save to game config - config::SaveGameConfigSetting(emulator, "Display", "postprocess_dither", - dither); -} - -void PostProcessingDialogQt::OnResetFsrSharpness() { - float default_value = - ui::Presenter::GuestOutputPaintConfig::kFsrSharpnessReductionDefault; - - // Apply power 2.0 scaling - float slider_value = sqrt(2.f * default_value); - fsr_sharpness_slider_->setValue(static_cast(slider_value * 100)); -} - -void PostProcessingDialogQt::OnResetCasSharpness() { - float default_value = - ui::Presenter::GuestOutputPaintConfig::kCasAdditionalSharpnessDefault; - cas_sharpness_slider_->setValue(static_cast(default_value * 100)); -} - -void PostProcessingDialogQt::OnFsrMaxUpsamplingPassesChanged(int value) { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - auto presenter = graphics_system->presenter(); - if (!presenter) { - return; - } - - // Convert slider value (1-8) to FSR max upsampling passes - uint32_t max_upsampling_passes = static_cast(value); - - auto config = presenter->GetGuestOutputPaintConfigFromUIThread(); - config.SetFsrMaxUpsamplingPasses(max_upsampling_passes); - presenter->SetGuestOutputPaintConfigFromUIThread(config); - - // Update cvar - emulator_window_->UpdateFsrMaxUpsamplingPassesCvar(max_upsampling_passes); - - // Save to game config - config::SaveGameConfigSetting(emulator, "Display", - "postprocess_ffx_fsr_max_upsampling_passes", - max_upsampling_passes); -} - -void PostProcessingDialogQt::OnResetFsrMaxUpsamplingPasses() { - uint32_t default_value = - ui::Presenter::GuestOutputPaintConfig::kFsrMaxUpscalingPassesMax; - fsr_max_upsampling_button_group_->button(static_cast(default_value)) - ->setChecked(true); -} - -void PostProcessingDialogQt::UpdateEffectDescription() { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - auto presenter = graphics_system->presenter(); - if (!presenter) { - return; - } - - const auto& config = presenter->GetGuestOutputPaintConfigFromUIThread(); - - const char* description = nullptr; - switch (config.GetEffect()) { - case ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear: - description = - "Simple bilinear filtering is done if resampling is needed.\n" - "Otherwise, only anti-aliasing is done if enabled, or displaying as " - "is."; - break; - case ui::Presenter::GuestOutputPaintConfig::Effect::kCas: - description = - "Sharpening and resampling to up to 2x2 to improve the fidelity of " - "details.\n" - "For scaling by more than 2x2, bilinear stretching is done " - "afterwards."; - break; - case ui::Presenter::GuestOutputPaintConfig::Effect::kFsr: - description = - "High-quality edge-preserving upscaling to arbitrary target " - "resolutions.\n" - "For scaling by more than 2x2, multiple upsampling passes are done.\n" - "If not upscaling, Contrast Adaptive Sharpening (CAS) is used " - "instead."; - break; - } - - if (description) { - effect_description_label_->setText(description); - effect_description_label_->setVisible(true); - } else { - effect_description_label_->setVisible(false); - } -} - -void PostProcessingDialogQt::UpdateSharpnessControls() { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto graphics_system = emulator->graphics_system(); - if (!graphics_system) { - return; - } - - auto presenter = graphics_system->presenter(); - if (!presenter) { - return; - } - - const auto& config = presenter->GetGuestOutputPaintConfigFromUIThread(); - - bool is_cas = - config.GetEffect() == ui::Presenter::GuestOutputPaintConfig::Effect::kCas; - bool is_fsr = - config.GetEffect() == ui::Presenter::GuestOutputPaintConfig::Effect::kFsr; - - // Show/hide FXAA recommendation - fxaa_recommendation_label_->setVisible(is_cas || is_fsr); - - // With the new widget hierarchy, we can simply show/hide the control widgets - fsr_sharpness_widget_->setVisible(is_fsr); - fsr_max_upsampling_widget_->setVisible(is_fsr); - cas_sharpness_widget_->setVisible(is_cas); - - // CAS label text (only shown for pure CAS, not FSR) - if (is_cas) { - cas_sharpness_label_->setText( - "CAS additional sharpness (higher is sharper):"); - } - - // Resize dialog to fit content after showing/hiding widgets - adjustSize(); - - // Update the list of focusable widgets since visibility has changed - UpdateFocusableWidgets(); -} - -} // namespace app -} // namespace xe diff --git a/src/xenia/ui/postprocessing_dialog_qt.h b/src/xenia/ui/postprocessing_dialog_qt.h deleted file mode 100644 index 564bb71df..000000000 --- a/src/xenia/ui/postprocessing_dialog_qt.h +++ /dev/null @@ -1,112 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_UI_POSTPROCESSING_DIALOG_QT_H_ -#define XENIA_UI_POSTPROCESSING_DIALOG_QT_H_ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "xenia/gpu/command_processor.h" -#include "xenia/ui/gamepad_dialog_qt.h" -#include "xenia/ui/presenter.h" - -namespace xe { -namespace app { - -class EmulatorWindow; - -class PostProcessingDialogQt : public ui::GamepadDialog { - Q_OBJECT - - public: - PostProcessingDialogQt(QWidget* parent, EmulatorWindow* emulator_window, - hid::InputSystem* input_system); - ~PostProcessingDialogQt() override; - - private slots: - void OnAntiAliasingChanged(int index); - void OnResamplingEffectChanged(int index); - void OnFsrSharpnessChanged(int value); - void OnCasSharpnessChanged(int value); - void OnFsrMaxUpsamplingPassesChanged(int value); - void OnDitherChanged(int state); - void OnResetFsrSharpness(); - void OnResetCasSharpness(); - void OnResetFsrMaxUpsamplingPasses(); - - protected: - void mousePressEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; - - private: - void SetupUI(); - void LoadCurrentSettings(); - void UpdateEffectDescription(); - void UpdateSharpnessControls(); - - EmulatorWindow* emulator_window_; - QPoint drag_position_; - - // Anti-aliasing widgets - QGroupBox* aa_group_; - QButtonGroup* aa_button_group_; - QRadioButton* aa_none_radio_; - QRadioButton* aa_fxaa_radio_; - QRadioButton* aa_fxaa_extreme_radio_; - - // Resampling and sharpening widgets - QGroupBox* resampling_group_; - QButtonGroup* resampling_button_group_; - QRadioButton* effect_bilinear_radio_; - QRadioButton* effect_cas_radio_; - QRadioButton* effect_fsr_radio_; - - QLabel* effect_description_label_; - QLabel* fxaa_recommendation_label_; - - // FSR sharpness widgets - QWidget* fsr_sharpness_widget_; - QLabel* fsr_sharpness_label_; - QSlider* fsr_sharpness_slider_; - QLabel* fsr_sharpness_value_label_; - QPushButton* fsr_reset_button_; - - // CAS sharpness widgets - QWidget* cas_sharpness_widget_; - QLabel* cas_sharpness_label_; - QSlider* cas_sharpness_slider_; - QLabel* cas_sharpness_value_label_; - QPushButton* cas_reset_button_; - - // FSR max upsampling passes widgets - QWidget* fsr_max_upsampling_widget_; - QLabel* fsr_max_upsampling_label_; - QButtonGroup* fsr_max_upsampling_button_group_; - QRadioButton* fsr_max_upsampling_radio_1_; - QRadioButton* fsr_max_upsampling_radio_2_; - QRadioButton* fsr_max_upsampling_radio_3_; - QRadioButton* fsr_max_upsampling_radio_4_; - QPushButton* fsr_max_upsampling_reset_button_; - - // Dithering widgets - QGroupBox* dither_group_; - QCheckBox* dither_checkbox_; -}; - -} // namespace app -} // namespace xe - -#endif // XENIA_APP_POSTPROCESSING_DIALOG_QT_H_ diff --git a/src/xenia/ui/profile_dialogs.cc b/src/xenia/ui/profile_dialogs.cc index 1d3a53706..9ba380358 100644 --- a/src/xenia/ui/profile_dialogs.cc +++ b/src/xenia/ui/profile_dialogs.cc @@ -140,6 +140,8 @@ void ProfileConfigDialog::LoadProfileIcon(const uint64_t xuid) { } void ProfileConfigDialog::OnDraw(ImGuiIO& io) { + PollGamepad(); + if (!emulator_window_->emulator() || !emulator_window_->emulator()->kernel_state() || !emulator_window_->emulator()->kernel_state()->xam_state()) { @@ -168,13 +170,15 @@ void ProfileConfigDialog::OnDraw(ImGuiIO& io) { return; } - // For whatever reason dialog wasn't opened. It's probably in closing state. - // We need to handle it here before it will make icons allocation. + // Handle keyboard escape + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + dialog_open = false; + } + + // Handle close button or escape if (!dialog_open) { - ImGui::CloseCurrentPopup(); - Close(); ImGui::End(); - emulator_window_->ToggleProfilesConfigDialog(); + Close(); return; } diff --git a/src/xenia/ui/profile_dialogs.h b/src/xenia/ui/profile_dialogs.h index 3542f81b2..c7de8f391 100644 --- a/src/xenia/ui/profile_dialogs.h +++ b/src/xenia/ui/profile_dialogs.h @@ -10,8 +10,11 @@ #ifndef XENIA_UI_PROFILE_DIALOGS_H_ #define XENIA_UI_PROFILE_DIALOGS_H_ +#include + #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/imgui_drawer.h" +#include "xenia/ui/imgui_gamepad_dialog.h" #include "xenia/xbox.h" namespace xe { @@ -31,16 +34,29 @@ class NoProfileDialog final : public ui::ImGuiDialog { EmulatorWindow* emulator_window_; }; -class ProfileConfigDialog final : public ui::ImGuiDialog { +class ProfileConfigDialog final : public ui::ImGuiGamepadDialog { public: ProfileConfigDialog(ui::ImGuiDrawer* imgui_drawer, - EmulatorWindow* emulator_window) - : ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) { + EmulatorWindow* emulator_window, + hid::InputSystem* input_system) + : ui::ImGuiGamepadDialog(imgui_drawer, input_system), + emulator_window_(emulator_window) { LoadProfileIcon(); } + void CloseDialog() { Close(); } + + void SetOnCloseCallback(std::function callback) { + on_close_callback_ = std::move(callback); + } + protected: void OnDraw(ImGuiIO& io) override; + void OnClose() override { + if (on_close_callback_) { + on_close_callback_(); + } + } private: void LoadProfileIcon(); @@ -50,6 +66,7 @@ class ProfileConfigDialog final : public ui::ImGuiDialog { uint64_t selected_xuid_ = 0; EmulatorWindow* emulator_window_; + std::function on_close_callback_; }; } // namespace app diff --git a/src/xenia/ui/window_qt.cc b/src/xenia/ui/window_qt.cc index 8ff452956..ba933ed38 100644 --- a/src/xenia/ui/window_qt.cc +++ b/src/xenia/ui/window_qt.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -51,86 +53,7 @@ inline void InitializeUIResources() { Q_INIT_RESOURCE(ui_resources); } namespace xe { namespace ui { -// Custom widget for external rendering (Vulkan/D3D12) -class ExternalRenderWidget : public QWidget { - public: - explicit ExternalRenderWidget(QtWindow* window, QWidget* parent = nullptr) - : QWidget(parent), window_(window) { - setAttribute(Qt::WA_PaintOnScreen); - setAttribute(Qt::WA_NoSystemBackground); - setAttribute(Qt::WA_OpaquePaintEvent); - - // Enable mouse tracking for ImGui hover - setMouseTracking(true); - // Don't accept keyboard focus - let the main window handle keyboard events - setFocusPolicy(Qt::NoFocus); - } - - QPaintEngine* paintEngine() const override { - // Return nullptr to indicate external rendering - return nullptr; - } - -#if defined(XE_PLATFORM_WIN32) - bool nativeEvent(const QByteArray& eventType, void* message, - qintptr* result) override { - if (eventType == "windows_generic_MSG") { - MSG* msg = static_cast(message); - if (msg->message == WM_ERASEBKGND) { - // Don't erase background - D3D12 handles all rendering - *result = 1; - return true; - } - } - return QWidget::nativeEvent(eventType, message, result); - } -#endif - - protected: - void paintEvent(QPaintEvent*) override { - // Trigger the presenter to paint (Vulkan/D3D12 renders directly) - if (window_) { - window_->TriggerPaint(); - } - } - - void mouseMoveEvent(QMouseEvent* event) override { - if (window_) { - window_->OnMouseMoveEvent(event); - } - } - - void mousePressEvent(QMouseEvent* event) override { - if (window_) { - window_->OnMousePressEvent(event); - } - event->accept(); - } - - void mouseReleaseEvent(QMouseEvent* event) override { - if (window_) { - window_->OnMouseReleaseEvent(event); - } - event->accept(); - } - - void mouseDoubleClickEvent(QMouseEvent* event) override { - if (window_) { - window_->OnMouseDoubleClickEvent(event); - } - event->accept(); - } - - void wheelEvent(QWheelEvent* event) override { - if (window_) { - window_->OnWheelEvent(event); - } - } - - private: - QtWindow* window_; -}; - +// Internal QMainWindow for UI process (game list, menu bar) class QtWindowInternal : public QMainWindow { public: explicit QtWindowInternal(QtWindow* window) : window_(window) {} @@ -173,6 +96,105 @@ class QtWindowInternal : public QMainWindow { QtWindow* window_; }; +// GameQWindow implementation - native QWindow for game process rendering +GameQWindow::GameQWindow(QtWindow* window) : QWindow(), window_(window) { + // RasterSurface is appropriate for D3D12/Vulkan external rendering + setSurfaceType(QSurface::RasterSurface); +} + +void GameQWindow::exposeEvent(QExposeEvent* event) { + QWindow::exposeEvent(event); + if (isExposed() && window_) { + window_->TriggerPaint(); + } +} + +void GameQWindow::resizeEvent(QResizeEvent* event) { + QWindow::resizeEvent(event); + if (window_) { + window_->OnResizeEvent(event); + } +} + +void GameQWindow::keyPressEvent(QKeyEvent* event) { + if (window_) { + window_->OnKeyPressEvent(event); + } +} + +void GameQWindow::keyReleaseEvent(QKeyEvent* event) { + if (window_) { + window_->OnKeyReleaseEvent(event); + } +} + +void GameQWindow::mouseMoveEvent(QMouseEvent* event) { + if (window_) { + window_->OnMouseMoveEvent(event); + } +} + +void GameQWindow::mousePressEvent(QMouseEvent* event) { + if (window_) { + window_->OnMousePressEvent(event); + } +} + +void GameQWindow::mouseReleaseEvent(QMouseEvent* event) { + if (window_) { + window_->OnMouseReleaseEvent(event); + } +} + +void GameQWindow::mouseDoubleClickEvent(QMouseEvent* event) { + if (window_) { + window_->OnMouseDoubleClickEvent(event); + } +} + +void GameQWindow::wheelEvent(QWheelEvent* event) { + if (window_) { + window_->OnWheelEvent(event); + } +} + +void GameQWindow::focusInEvent(QFocusEvent* event) { + QWindow::focusInEvent(event); + if (window_) { + window_->OnFocusInEvent(event); + } +} + +void GameQWindow::focusOutEvent(QFocusEvent* event) { + QWindow::focusOutEvent(event); + if (window_) { + window_->OnFocusOutEvent(event); + } +} + +bool GameQWindow::event(QEvent* event) { + switch (event->type()) { + case QEvent::Close: + if (window_) { + window_->OnCloseEvent(static_cast(event)); + } + return true; + case QEvent::WindowActivate: + if (window_) { + window_->OnFocusInEvent(nullptr); + } + break; + case QEvent::WindowDeactivate: + if (window_) { + window_->OnFocusOutEvent(nullptr); + } + break; + default: + break; + } + return QWindow::event(event); +} + std::unique_ptr MenuItem::Create(Type type, const std::string& text, const std::string& hotkey, std::function callback) { @@ -201,10 +223,13 @@ QtWindow::~QtWindow() { delete cursor_auto_hide_timer_; cursor_auto_hide_timer_ = nullptr; } + if (game_qwindow_) { + delete game_qwindow_; + game_qwindow_ = nullptr; + } if (qwindow_) { delete qwindow_; qwindow_ = nullptr; - drawing_widget_ = nullptr; } } @@ -212,6 +237,96 @@ bool QtWindow::OpenImpl() { // Initialize Qt resources (needed for app icon) InitializeUIResources(); + // Branch based on window type + if (is_game_process_) { + return OpenGameWindow(); + } else { + return OpenUIWindow(); + } +} + +bool QtWindow::OpenGameWindow() { + // Create native QWindow for game rendering (no Qt widget overhead) + game_qwindow_ = new GameQWindow(this); + game_qwindow_->setTitle( + QString::fromUtf8(GetTitle().data(), GetTitle().size())); + + // Set default application icon + QIcon app_icon(":/xenia/icon.png"); + if (!app_icon.isNull()) { + game_qwindow_->setIcon(app_icon); + QGuiApplication::setWindowIcon(app_icon); + } + + // Use the screen where the cursor is, falling back to primary + QScreen* screen = QGuiApplication::screenAt(QCursor::pos()); + if (!screen) { + screen = QGuiApplication::primaryScreen(); + } + + // Position window on target screen + if (screen) { + game_qwindow_->setScreen(screen); + QRect screen_geometry = screen->availableGeometry(); + int x = screen_geometry.x() + + (screen_geometry.width() - GetDesiredLogicalWidth()) / 2; + int y = screen_geometry.y() + + (screen_geometry.height() - GetDesiredLogicalHeight()) / 2; + game_qwindow_->setPosition(x, y); + } + + game_qwindow_->resize(GetDesiredLogicalWidth(), GetDesiredLogicalHeight()); + + if (IsFullscreen()) { + game_qwindow_->showFullScreen(); + } else { + game_qwindow_->show(); + } + + // Bring window to foreground + game_qwindow_->requestActivate(); + game_qwindow_->raise(); + + // Ensure native window is created + game_qwindow_->winId(); + + { + WindowDestructionReceiver destruction_receiver(this); + + // Notify that the window is on a monitor (required for presenter to paint) + MonitorUpdateEvent monitor_event(this, false); + OnMonitorUpdate(monitor_event); + + OnActualSizeUpdate(uint32_t(game_qwindow_->width()), + uint32_t(game_qwindow_->height()), destruction_receiver); + if (destruction_receiver.IsWindowDestroyedOrClosed()) { + return true; + } + + if (game_qwindow_->isActive()) { + OnFocusUpdate(true, destruction_receiver); + if (destruction_receiver.IsWindowDestroyedOrClosed()) { + return true; + } + } + } + + // Initialize cursor visibility state + cursor_currently_auto_hidden_ = false; + CursorVisibility cursor_visibility = GetCursorVisibility(); + if (cursor_visibility == CursorVisibility::kAutoHidden) { + cursor_auto_hide_last_pos_ = QCursor::pos(); + SetCursorAutoHideTimer(); + } else if (cursor_visibility == CursorVisibility::kHidden) { + UpdateCursorVisibility(); + } + + return true; +} + +bool QtWindow::OpenUIWindow() { + // Create QMainWindow for UI process (with menu bar support) + // UI process uses pure Qt widgets - no rendering surface needed qwindow_ = new QtWindowInternal(this); qwindow_->setWindowTitle( QString::fromUtf8(GetTitle().data(), GetTitle().size())); @@ -238,6 +353,7 @@ bool QtWindow::OpenImpl() { XELOGW("Failed to load window icon from Qt resources"); } + // Create simple central widget with layout for game list QWidget* central_widget = new QWidget(qwindow_); qwindow_->setCentralWidget(central_widget); @@ -245,17 +361,8 @@ bool QtWindow::OpenImpl() { layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - drawing_widget_ = new ExternalRenderWidget(this, central_widget); - drawing_widget_->setMinimumSize(GetDesiredLogicalWidth(), - GetDesiredLogicalHeight()); - - // Force creation of native window for external rendering - drawing_widget_->setAttribute(Qt::WA_NativeWindow); - drawing_widget_->setAutoFillBackground(false); - - drawing_widget_->installEventFilter(qwindow_); - - layout->addWidget(drawing_widget_); + // Note: No ExternalRenderWidget created - UI process uses pure Qt widgets + // The GameListDialogQt will be added to this layout in OnEmulatorInitialized const auto* main_menu = dynamic_cast(GetMainMenu()); if (main_menu) { @@ -274,7 +381,7 @@ bool QtWindow::OpenImpl() { screen = QGuiApplication::primaryScreen(); } - // Position window on target screen (required for correct fullscreen monitor) + // Position window on target screen if (screen) { QRect screen_geometry = screen->availableGeometry(); int x = screen_geometry.x() + @@ -295,64 +402,40 @@ bool QtWindow::OpenImpl() { qwindow_->activateWindow(); qwindow_->raise(); - // Ensure native window is created by accessing winId() - // This must be done after show() so Qt creates the platform window - drawing_widget_->winId(); - - drawing_widget_->setMinimumSize(0, 0); - - { - WindowDestructionReceiver destruction_receiver(this); - - // Notify that the window is on a monitor (required for presenter to paint) - MonitorUpdateEvent monitor_event(this, false); - OnMonitorUpdate(monitor_event); - - OnActualSizeUpdate(uint32_t(drawing_widget_->width()), - uint32_t(drawing_widget_->height()), - destruction_receiver); - if (destruction_receiver.IsWindowDestroyedOrClosed()) { - return true; - } - - if (qwindow_->isActiveWindow()) { - OnFocusUpdate(true, destruction_receiver); - if (destruction_receiver.IsWindowDestroyedOrClosed()) { - return true; - } - } - } - - // Initialize cursor visibility state - cursor_currently_auto_hidden_ = false; - CursorVisibility cursor_visibility = GetCursorVisibility(); - if (cursor_visibility == CursorVisibility::kAutoHidden) { - cursor_auto_hide_last_pos_ = QCursor::pos(); - // Start the auto-hide timer immediately - SetCursorAutoHideTimer(); - } else if (cursor_visibility == CursorVisibility::kHidden) { - UpdateCursorVisibility(); - } - return true; } void QtWindow::RequestCloseImpl() { - if (qwindow_) qwindow_->close(); + if (game_qwindow_) { + game_qwindow_->close(); + } else if (qwindow_) { + qwindow_->close(); + } } void QtWindow::ApplyNewFullscreen() { - if (!qwindow_) return; WindowDestructionReceiver destruction_receiver(this); - if (IsFullscreen()) { - qwindow_->menuBar()->hide(); - qwindow_->showFullScreen(); - } else { - qwindow_->showNormal(); - if (GetMainMenu()) { - qwindow_->menuBar()->show(); + if (game_qwindow_) { + // Game window (QWindow) - simple fullscreen toggle + if (IsFullscreen()) { + game_qwindow_->showFullScreen(); + } else { + game_qwindow_->showNormal(); } + } else if (qwindow_) { + // UI window (QMainWindow) - handle menu bar visibility + if (IsFullscreen()) { + qwindow_->menuBar()->hide(); + qwindow_->showFullScreen(); + } else { + qwindow_->showNormal(); + if (GetMainMenu()) { + qwindow_->menuBar()->show(); + } + } + } else { + return; } if (!destruction_receiver.IsWindowDestroyedOrClosed()) { @@ -361,15 +444,17 @@ void QtWindow::ApplyNewFullscreen() { } void QtWindow::ApplyNewTitle() { - if (qwindow_) { - qwindow_->setWindowTitle( - QString::fromUtf8(GetTitle().data(), GetTitle().size())); + QString title = QString::fromUtf8(GetTitle().data(), GetTitle().size()); + if (game_qwindow_) { + game_qwindow_->setTitle(title); + } else if (qwindow_) { + qwindow_->setWindowTitle(title); } } void QtWindow::LoadAndApplyIcon(const void* buffer, size_t size, bool can_apply_state_in_current_phase) { - if (!qwindow_) { + if (!game_qwindow_ && !qwindow_) { return; } @@ -377,28 +462,44 @@ void QtWindow::LoadAndApplyIcon(const void* buffer, size_t size, if (reset) { // Reset to default/no icon - qwindow_->setWindowIcon(QIcon()); + if (game_qwindow_) { + game_qwindow_->setIcon(QIcon()); + } else if (qwindow_) { + qwindow_->setWindowIcon(QIcon()); + } } else { // Load icon from buffer (typically from game executable) QPixmap pixmap; if (pixmap.loadFromData(static_cast(buffer), static_cast(size))) { QIcon icon(pixmap); - qwindow_->setWindowIcon(icon); - // Also set the application-level icon for taskbar/dock QGuiApplication::setWindowIcon(icon); + if (game_qwindow_) { + game_qwindow_->setIcon(icon); #if defined(XE_PLATFORM_WIN32) - // On Windows, also set via Win32 API for taskbar - HWND hwnd = reinterpret_cast(qwindow_->winId()); - if (hwnd) { - HICON hicon = icon.pixmap(32, 32).toImage().toHICON(); - if (hicon) { - SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hicon); - SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hicon); + HWND hwnd = reinterpret_cast(game_qwindow_->winId()); + if (hwnd) { + HICON hicon = icon.pixmap(32, 32).toImage().toHICON(); + if (hicon) { + SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hicon); + SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hicon); + } } - } #endif + } else if (qwindow_) { + qwindow_->setWindowIcon(icon); +#if defined(XE_PLATFORM_WIN32) + HWND hwnd = reinterpret_cast(qwindow_->winId()); + if (hwnd) { + HICON hicon = icon.pixmap(32, 32).toImage().toHICON(); + if (hicon) { + SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hicon); + SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hicon); + } + } +#endif + } } } } @@ -445,7 +546,10 @@ void QtWindow::ApplyNewCursorVisibility( } void QtWindow::FocusImpl() { - if (qwindow_) { + if (game_qwindow_) { + game_qwindow_->requestActivate(); + game_qwindow_->raise(); + } else if (qwindow_) { qwindow_->activateWindow(); qwindow_->raise(); } @@ -453,52 +557,40 @@ void QtWindow::FocusImpl() { std::unique_ptr QtWindow::CreateSurfaceImpl( Surface::TypeFlags allowed_types) { - if (!drawing_widget_) { - XELOGE("CreateSurfaceImpl: drawing_widget_ is null"); + // UI process doesn't do any rendering - pure Qt widgets + if (!is_game_process_) { + return nullptr; + } + + // Game process needs a rendering surface + if (!game_qwindow_) { + XELOGE("CreateSurfaceImpl: game_qwindow_ is null for game process"); return nullptr; } #if defined(XE_PLATFORM_LINUX) - XELOGI( - "CreateSurfaceImpl: is_game_process={}, platform={}, " - "allowed_types=0x{:x}", - is_game_process_, QGuiApplication::platformName().toStdString(), - allowed_types); + XELOGI("CreateSurfaceImpl: platform={}, allowed_types=0x{:x}", + QGuiApplication::platformName().toStdString(), allowed_types); - // UI process on Wayland: Don't create surfaces, Qt handles rendering natively - if (!is_game_process_ && QGuiApplication::platformName() == "wayland") { - XELOGI( - "CreateSurfaceImpl: UI process on Wayland, skipping surface creation " - "(Qt renders natively)"); - return nullptr; - } - - // Game process on Wayland: Use native Wayland surfaces for Vulkan - if (is_game_process_ && (allowed_types & Surface::kTypeFlag_WaylandWindow)) { - XELOGI( - "CreateSurfaceImpl: Attempting Wayland surface creation for game " - "process"); + // Try Wayland surface first + if (allowed_types & Surface::kTypeFlag_WaylandWindow) { auto* waylandApp = qApp->nativeInterface(); if (waylandApp) { wl_display* display = waylandApp->display(); if (display) { - QWindow* window = drawing_widget_->windowHandle(); - if (window) { - auto* waylandWindow = window->nativeInterface< - QNativeInterface::Private::QWaylandWindow>(); - if (waylandWindow) { - wl_surface* surface = waylandWindow->surface(); - if (surface) { - uint32_t width = drawing_widget_->width(); - uint32_t height = drawing_widget_->height(); - XELOGI( - "CreateSurfaceImpl: Created Wayland surface (game process), " - "size={}x{}", - width, height); - return std::make_unique(display, surface, - width, height); - } + auto* waylandWindow = + game_qwindow_ + ->nativeInterface(); + if (waylandWindow) { + wl_surface* surface = waylandWindow->surface(); + if (surface) { + uint32_t width = game_qwindow_->width(); + uint32_t height = game_qwindow_->height(); + XELOGI("CreateSurfaceImpl: Created Wayland surface, size={}x{}", + width, height); + return std::make_unique(display, surface, + width, height); } } XELOGW( @@ -508,7 +600,7 @@ std::unique_ptr QtWindow::CreateSurfaceImpl( } } - // Use XCB for UI process or as fallback + // Fall back to XCB if (allowed_types & Surface::kTypeFlag_XcbWindow) { auto* x11App = qApp->nativeInterface(); if (!x11App) { @@ -520,20 +612,21 @@ std::unique_ptr QtWindow::CreateSurfaceImpl( XELOGE("CreateSurfaceImpl: XCB connection is null"); return nullptr; } - xcb_window_t window = static_cast(drawing_widget_->winId()); + xcb_window_t window = static_cast(game_qwindow_->winId()); XELOGI("CreateSurfaceImpl: Created XCB surface with window ID: 0x{:x}", window); return std::make_unique(connection, window); } - XELOGE("CreateSurfaceImpl: XcbWindow not in allowed_types ({})", + + XELOGE("CreateSurfaceImpl: No supported surface type in allowed_types ({})", allowed_types); return nullptr; #elif defined(XE_PLATFORM_WIN32) if (allowed_types & Surface::kTypeFlag_Win32Hwnd) { auto& qt_context = static_cast(app_context()); - HWND hwnd = reinterpret_cast(drawing_widget_->winId()); + HWND hwnd = reinterpret_cast(game_qwindow_->winId()); if (!hwnd || !IsWindow(hwnd)) { - XELOGE("CreateSurfaceImpl: Invalid HWND from drawing_widget_->winId()"); + XELOGE("CreateSurfaceImpl: Invalid HWND"); return nullptr; } return std::make_unique(qt_context.hinstance(), hwnd); @@ -545,22 +638,31 @@ std::unique_ptr QtWindow::CreateSurfaceImpl( } void QtWindow::RequestPaintImpl() { - // Trigger a paint event which will call OnPaint() via - // ExternalRenderWidget::paintEvent - if (drawing_widget_) { - drawing_widget_->update(); + // Only game process does rendering + if (!game_qwindow_) { + return; } + + // For QWindow, we need to trigger a paint as immediately as possible. + // QTimer::singleShot(0, ...) executes on the next event loop iteration, + // which is more immediate than requestUpdate() which may be batched. + QTimer::singleShot(0, game_qwindow_, [this]() { + if (game_qwindow_) { + TriggerPaint(); + } + }); } void QtWindow::HandleSizeUpdate( WindowDestructionReceiver& destruction_receiver) { - if (in_size_update_ || !drawing_widget_) { + // Only game process needs size updates for rendering + if (in_size_update_ || !game_qwindow_) { return; } in_size_update_ = true; - OnActualSizeUpdate(uint32_t(drawing_widget_->width()), - uint32_t(drawing_widget_->height()), destruction_receiver); + OnActualSizeUpdate(uint32_t(game_qwindow_->width()), + uint32_t(game_qwindow_->height()), destruction_receiver); in_size_update_ = false; } @@ -962,10 +1064,16 @@ void QtWindow::SetCursorAutoHideTimer() { cursor_auto_hide_timer_ = nullptr; } - // Create new single-shot timer - cursor_auto_hide_timer_ = new QTimer(qwindow_); + // Create new single-shot timer - parent to the active window object + QObject* parent = game_qwindow_ ? static_cast(game_qwindow_) + : static_cast(qwindow_); + if (!parent) { + return; + } + + cursor_auto_hide_timer_ = new QTimer(parent); cursor_auto_hide_timer_->setSingleShot(true); - QObject::connect(cursor_auto_hide_timer_, &QTimer::timeout, qwindow_, + QObject::connect(cursor_auto_hide_timer_, &QTimer::timeout, parent, [this]() { OnCursorAutoHideTimeout(); }); cursor_auto_hide_timer_->start(kDefaultCursorAutoHideMilliseconds); } @@ -984,20 +1092,17 @@ void QtWindow::OnCursorAutoHideTimeout() { } void QtWindow::UpdateCursorVisibility() { - if (!qwindow_ || !drawing_widget_) { - return; - } - CursorVisibility visibility = GetCursorVisibility(); + bool should_hide = (visibility == CursorVisibility::kHidden || + (visibility == CursorVisibility::kAutoHidden && + cursor_currently_auto_hidden_)); - if (visibility == CursorVisibility::kHidden || - (visibility == CursorVisibility::kAutoHidden && - cursor_currently_auto_hidden_)) { - drawing_widget_->setCursor(Qt::BlankCursor); - qwindow_->setCursor(Qt::BlankCursor); - } else { - drawing_widget_->setCursor(Qt::ArrowCursor); - qwindow_->setCursor(Qt::ArrowCursor); + Qt::CursorShape cursor = should_hide ? Qt::BlankCursor : Qt::ArrowCursor; + + if (game_qwindow_) { + game_qwindow_->setCursor(cursor); + } else if (qwindow_) { + qwindow_->setCursor(cursor); } } diff --git a/src/xenia/ui/window_qt.h b/src/xenia/ui/window_qt.h index 6a8fed12a..e4033e620 100644 --- a/src/xenia/ui/window_qt.h +++ b/src/xenia/ui/window_qt.h @@ -15,6 +15,7 @@ #include #include +#include #include "xenia/ui/menu_item.h" #include "xenia/ui/window.h" @@ -25,6 +26,7 @@ namespace ui { class QtWindowedAppContext; class QtWindowInternal; +class GameQWindow; class QtWindow : public Window { using super = Window; @@ -35,11 +37,14 @@ class QtWindow : public Window { bool is_game_process = false); ~QtWindow() override; + // UI process window (QMainWindow with menu bar) QMainWindow* qwindow() const { return qwindow_; } + // Game process window (native QWindow) + GameQWindow* game_window() const { return game_qwindow_; } void TriggerPaint() { OnPaint(); } - // Public accessors for event handling from ExternalRenderWidget + // Public accessors for event handling from GameQWindow void OnMouseMoveEvent(QMouseEvent* event); void OnMousePressEvent(QMouseEvent* event); void OnMouseReleaseEvent(QMouseEvent* event); @@ -65,7 +70,11 @@ class QtWindow : public Window { private: friend class QtWindowInternal; - friend class ExternalRenderWidget; + friend class GameQWindow; + + // Separate open implementations for UI vs game process + bool OpenUIWindow(); + bool OpenGameWindow(); void HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver); static VirtualKey TranslateVirtualKey(int qt_key); @@ -82,8 +91,11 @@ class QtWindow : public Window { void OnCursorAutoHideTimeout(); void UpdateCursorVisibility(); + // UI process window (QMainWindow with menu bar, pure Qt widgets) QMainWindow* qwindow_ = nullptr; - QWidget* drawing_widget_ = nullptr; + + // Game process window (native QWindow for rendering) + GameQWindow* game_qwindow_ = nullptr; bool in_size_update_ = false; bool is_game_process_ = false; @@ -114,6 +126,30 @@ class QtMenuItem : public MenuItem { QMenu* menu_ = nullptr; }; +// Native QWindow for game process rendering (similar to RPCS3's gs_frame) +// Used instead of QWidget to avoid Qt painting system overhead +class GameQWindow : public QWindow { + public: + explicit GameQWindow(QtWindow* window); + + protected: + void exposeEvent(QExposeEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + void focusInEvent(QFocusEvent* event) override; + void focusOutEvent(QFocusEvent* event) override; + bool event(QEvent* event) override; + + private: + QtWindow* window_; +}; + } // namespace ui } // namespace xe diff --git a/src/xenia/ui/xmp_dialog_qt.cc b/src/xenia/ui/xmp_dialog_qt.cc deleted file mode 100644 index 853237adc..000000000 --- a/src/xenia/ui/xmp_dialog_qt.cc +++ /dev/null @@ -1,299 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/ui/xmp_dialog_qt.h" - -#include -#include -#include -#include - -#include "third_party/fmt/include/fmt/format.h" -#include "xenia/app/emulator_window.h" -#include "xenia/ui/qt_util.h" - -namespace xe { -namespace app { - -using xe::ui::SafeQString; - -XmpDialogQt::XmpDialogQt(QWidget* parent, EmulatorWindow* emulator_window, - hid::InputSystem* input_system) - : ui::GamepadDialog(parent, input_system), - emulator_window_(emulator_window) { - SetupUI(); - - // Position near top, centered horizontally - if (parent) { - QPoint parent_pos = parent->mapToGlobal(QPoint(0, 0)); - int center_x = parent_pos.x() + (parent->width() - width()) / 2; - move(center_x, parent_pos.y() + 20); - } -} - -XmpDialogQt::~XmpDialogQt() = default; - -void XmpDialogQt::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - drag_position_ = event->position().toPoint(); - event->accept(); - } -} - -void XmpDialogQt::mouseMoveEvent(QMouseEvent* event) { - if (event->buttons() & Qt::LeftButton) { - QPoint global_pos = event->globalPosition().toPoint(); - move(global_pos - drag_position_); - event->accept(); - } -} - -void XmpDialogQt::showEvent(QShowEvent* event) { - GamepadDialog::showEvent(event); - UpdatePlayerState(); -} - -void XmpDialogQt::SetupUI() { - setWindowTitle("Audio Player Menu"); - setModal(false); - setAttribute(Qt::WA_DeleteOnClose); - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); - setMinimumWidth(400); - - // Set window opacity (0.92 = 92% opaque) - same as post-processing dialog - setWindowOpacity(0.92); - - // Set semi-transparent background similar to post-processing dialog - setStyleSheet(R"( - QDialog { - background-color: rgb(30, 30, 30); - border: 1px solid rgba(100, 100, 100, 180); - padding: 0px; - } - QLabel { - color: #d0d0d0; - background-color: transparent; - } - QPushButton { - background-color: rgba(60, 60, 60, 200); - border: 1px solid rgba(100, 100, 100, 150); - border-radius: 4px; - color: #e0e0e0; - padding: 8px 16px; - font-weight: bold; - } - QPushButton:hover { - background-color: rgba(80, 80, 80, 220); - border-color: #b0b0b0; - } - QPushButton:pressed { - background-color: rgba(50, 50, 50, 240); - border-color: #707070; - } - QSlider::groove:horizontal { - border: 1px solid rgba(100, 100, 100, 150); - height: 8px; - background: rgba(50, 50, 50, 200); - margin: 2px 0; - border-radius: 4px; - } - QSlider::handle:horizontal { - background: #107c10; - border: 1px solid rgba(100, 100, 100, 150); - width: 18px; - margin: -5px 0; - border-radius: 9px; - } - QSlider::handle:horizontal:hover { - background: #138f13; - border-color: #b0b0b0; - } - QSlider::sub-page:horizontal { - background: #107c10; - border: 1px solid rgba(100, 100, 100, 150); - height: 8px; - border-radius: 4px; - } - )"); - - auto* content_layout = new QVBoxLayout(this); - content_layout->setContentsMargins(16, 16, 16, 16); - content_layout->setSpacing(12); - - // Top bar with close button - auto* top_bar_layout = new QHBoxLayout(); - auto* title_label = new QLabel("XMP Audio Player", this); - title_label->setStyleSheet("font-weight: bold; font-size: 14px;"); - top_bar_layout->addWidget(title_label); - top_bar_layout->addStretch(); - - auto* close_button = new QPushButton(this); - close_button->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton)); - close_button->setIconSize(QSize(16, 16)); - close_button->setFlat(true); - close_button->setStyleSheet(R"( - QPushButton { - background-color: transparent; - border: none; - min-width: 24px; - max-width: 24px; - min-height: 24px; - max-height: 24px; - padding: 2px; - } - QPushButton:hover { - background-color: rgba(200, 50, 50, 180); - border-radius: 4px; - } - QPushButton:pressed { - background-color: rgba(150, 30, 30, 220); - border-radius: 4px; - } - )"); - close_button->setToolTip("Close"); - connect(close_button, &QPushButton::clicked, this, &QDialog::close); - top_bar_layout->addWidget(close_button, 0, Qt::AlignTop); - - content_layout->addLayout(top_bar_layout); - content_layout->addSpacing(6); - - // Status display - auto* status_layout = new QHBoxLayout(); - status_label_ = new QLabel("Audio player status:", this); - status_value_label_ = new QLabel("Idle", this); - status_value_label_->setStyleSheet("color: #f0f0f0; font-weight: bold;"); - status_layout->addWidget(status_label_); - status_layout->addWidget(status_value_label_); - status_layout->addStretch(); - content_layout->addLayout(status_layout); - - content_layout->addSpacing(8); - - // Play/Pause button - play_pause_button_ = new QPushButton("Resume", this); - play_pause_button_->setMinimumHeight(36); - connect(play_pause_button_, &QPushButton::clicked, this, - &XmpDialogQt::OnPlayPauseClicked); - content_layout->addWidget(play_pause_button_); - - content_layout->addSpacing(8); - - // Volume control - volume_label_ = new QLabel("Audio player volume:", this); - content_layout->addWidget(volume_label_); - - auto* volume_layout = new QHBoxLayout(); - volume_slider_ = new QSlider(Qt::Horizontal, this); - volume_slider_->setRange(0, 100); // Will be mapped to 0.0-1.0 - volume_slider_->setValue(0); - volume_value_label_ = new QLabel("0%", this); - volume_value_label_->setMinimumWidth(50); - volume_value_label_->setAlignment(Qt::AlignRight | Qt::AlignVCenter); - - volume_layout->addWidget(volume_slider_); - volume_layout->addWidget(volume_value_label_); - content_layout->addLayout(volume_layout); - - connect(volume_slider_, &QSlider::valueChanged, this, - &XmpDialogQt::OnVolumeChanged); - - content_layout->addStretch(); -} - -void XmpDialogQt::UpdatePlayerState() { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto audio_player = emulator->audio_media_player(); - if (!audio_player) { - return; - } - - using xmp_state = kernel::xam::apps::XmpApp::State; - - // Update status label - switch (audio_player->GetState()) { - case xmp_state::kIdle: - status_value_label_->setText("Idle"); - break; - case xmp_state::kPaused: - status_value_label_->setText("Paused"); - break; - case xmp_state::kPlaying: - status_value_label_->setText("Playing"); - break; - default: - status_value_label_->setText("Unknown"); - break; - } - - // Update play/pause button - if (audio_player->IsPlaying()) { - play_pause_button_->setText("Pause"); - play_pause_button_->setEnabled(true); - } else if (audio_player->IsPaused()) { - play_pause_button_->setText("Resume"); - play_pause_button_->setEnabled(true); - } else { - play_pause_button_->setText("Resume"); - play_pause_button_->setEnabled(false); - } - - // Update volume slider - float volume = audio_player->GetVolume()->load(); - volume_slider_->blockSignals(true); - volume_slider_->setValue(static_cast(volume * 100)); - volume_value_label_->setText( - SafeQString(fmt::format("{}%", static_cast(volume * 100)))); - volume_slider_->blockSignals(false); -} - -void XmpDialogQt::OnPlayPauseClicked() { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto audio_player = emulator->audio_media_player(); - if (!audio_player) { - return; - } - - if (audio_player->IsPlaying()) { - audio_player->Pause(); - } else if (audio_player->IsPaused()) { - audio_player->Continue(); - } - - UpdatePlayerState(); -} - -void XmpDialogQt::OnVolumeChanged(int value) { - auto emulator = emulator_window_->emulator(); - if (!emulator) { - return; - } - - auto audio_player = emulator->audio_media_player(); - if (!audio_player) { - return; - } - - // Convert slider value (0-100) to volume (0.0-1.0) - float volume = value / 100.0f; - audio_player->SetVolume(volume); - - // Update label - volume_value_label_->setText(SafeQString(fmt::format("{}%", value))); -} - -} // namespace app -} // namespace xe diff --git a/src/xenia/ui/xmp_dialog_qt.h b/src/xenia/ui/xmp_dialog_qt.h deleted file mode 100644 index 2d701fb72..000000000 --- a/src/xenia/ui/xmp_dialog_qt.h +++ /dev/null @@ -1,64 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Ben Vanik. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_UI_XMP_DIALOG_QT_H_ -#define XENIA_UI_XMP_DIALOG_QT_H_ - -#include -#include -#include - -#include "xenia/ui/gamepad_dialog_qt.h" - -namespace xe { -namespace app { - -class EmulatorWindow; - -class XmpDialogQt : public ui::GamepadDialog { - Q_OBJECT - - public: - XmpDialogQt(QWidget* parent, EmulatorWindow* emulator_window, - hid::InputSystem* input_system); - ~XmpDialogQt() override; - - private slots: - void OnPlayPauseClicked(); - void OnVolumeChanged(int value); - - protected: - void mousePressEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; - void showEvent(QShowEvent* event) override; - - private: - void SetupUI(); - void UpdatePlayerState(); - - EmulatorWindow* emulator_window_; - QPoint drag_position_; - - // Status widgets - QLabel* status_label_; - QLabel* status_value_label_; - - // Playback control widgets - QPushButton* play_pause_button_; - - // Volume widgets - QLabel* volume_label_; - QSlider* volume_slider_; - QLabel* volume_value_label_; -}; - -} // namespace app -} // namespace xe - -#endif // XENIA_UI_XMP_DIALOG_QT_H_