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_