diff options
20 files changed, 570 insertions, 17 deletions
diff --git a/Source/Core/Core/CheatSearch.cpp b/Source/Core/Core/CheatSearch.cpp index 2cf509e880..063a09a582 100644 --- a/Source/Core/Core/CheatSearch.cpp +++ b/Source/Core/Core/CheatSearch.cpp @@ -284,6 +284,15 @@ void Cheats::CheatSearchSession<T>::ResetResults() } template <typename T> +void Cheats::CheatSearchSession<T>::RemoveResult(size_t index) +{ + if (index < m_search_results.size()) + { + m_search_results.erase(m_search_results.begin() + index); + } +} + +template <typename T> static std::function<bool(const T& new_value)> MakeCompareFunctionForSpecificValue(Cheats::CompareType op, const T& old_value) { diff --git a/Source/Core/Core/CheatSearch.h b/Source/Core/Core/CheatSearch.h index 7df135d51a..7d1c089835 100644 --- a/Source/Core/Core/CheatSearch.h +++ b/Source/Core/Core/CheatSearch.h @@ -168,6 +168,9 @@ public: virtual bool WriteValue(const Core::CPUThreadGuard& guard, std::span<u32> addresses) const = 0; + // User can delete a search result. + virtual void RemoveResult(size_t index) = 0; + // Create a complete copy of this search session. virtual std::unique_ptr<CheatSearchSessionBase> Clone() const = 0; @@ -195,6 +198,7 @@ public: bool SetValueFromString(const std::string& value_as_string, bool force_parse_as_hex) override; void ResetResults() override; + void RemoveResult(size_t index) override; SearchErrorCode RunSearch(const Core::CPUThreadGuard& guard) override; size_t GetMemoryRangeCount() const override; diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 6d5f260f07..441c5dd031 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -529,6 +529,23 @@ const Info<bool> MAIN_MOVIE_SHOW_OSD{{System::Main, "Movie", "ShowMovieWindow"}, const Info<bool> MAIN_INPUT_BACKGROUND_INPUT{{System::Main, "Input", "BackgroundInput"}, false}; +// Main.SDL_Hints + +// Defaults for these values are written in SDL.cpp so they appear in the config file, and thus show +// up in the SDL Hints config window (default values defined here would not be written). +const Info<std::string> MAIN_SDL_HINT_JOYSTICK_ENHANCED_REPORTS{ + {System::Main, "SDL_Hints", "SDL_JOYSTICK_ENHANCED_REPORTS"}, ""}; +const Info<std::string> MAIN_SDL_HINT_JOYSTICK_WGI{{System::Main, "SDL_Hints", "SDL_JOYSTICK_WGI"}, + ""}; +const Info<std::string> MAIN_SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED{ + {System::Main, "SDL_Hints", "SDL_JOYSTICK_HIDAPI_PS5_PLAYER_LED"}, ""}; +const Info<std::string> MAIN_SDL_HINT_JOYSTICK_DIRECTINPUT{ + {System::Main, "SDL_Hints", "SDL_JOYSTICK_DIRECTINPUT"}, ""}; +const Info<std::string> MAIN_SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS{ + {System::Main, "SDL_Hints", "SDL_JOYSTICK_HIDAPI_COMBINE_JOY_CONS"}, ""}; +const Info<std::string> MAIN_SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS{ + {System::Main, "SDL_Hints", "SDL_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS"}, ""}; + // Main.Debug const Info<bool> MAIN_DEBUG_JIT_OFF{{System::Main, "Debug", "JitOff"}, false}; diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 38c8fdc365..1946045e48 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -338,6 +338,13 @@ extern const Info<bool> MAIN_MOVIE_SHOW_OSD; extern const Info<bool> MAIN_INPUT_BACKGROUND_INPUT; +extern const Config::Info<std::string> MAIN_SDL_HINT_JOYSTICK_ENHANCED_REPORTS; +extern const Config::Info<std::string> MAIN_SDL_HINT_JOYSTICK_WGI; +extern const Config::Info<std::string> MAIN_SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED; +extern const Config::Info<std::string> MAIN_SDL_HINT_JOYSTICK_DIRECTINPUT; +extern const Config::Info<std::string> MAIN_SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS; +extern const Config::Info<std::string> MAIN_SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS; + // Main.Debug extern const Info<bool> MAIN_DEBUG_JIT_OFF; diff --git a/Source/Core/Core/PowerPC/BreakPoints.cpp b/Source/Core/Core/PowerPC/BreakPoints.cpp index 3cf564663e..e5dca73989 100644 --- a/Source/Core/Core/PowerPC/BreakPoints.cpp +++ b/Source/Core/Core/PowerPC/BreakPoints.cpp @@ -34,6 +34,9 @@ bool BreakPoints::IsAddressBreakPoint(u32 address) const bool BreakPoints::IsBreakPointEnable(u32 address) const { + if (!m_breaking_enabled) + return false; + const TBreakPoint* bp = GetBreakpoint(address); return bp != nullptr && bp->is_enabled; } @@ -184,6 +187,11 @@ bool BreakPoints::ToggleEnable(u32 address) return true; } +void BreakPoints::EnableBreaking(bool enable) +{ + m_breaking_enabled = enable; +} + bool BreakPoints::Remove(u32 address) { const auto iter = std::ranges::find(m_breakpoints, address, &TBreakPoint::address); @@ -318,6 +326,12 @@ bool MemChecks::ToggleEnable(u32 address) return true; } +void MemChecks::EnableBreaking(bool enabled) +{ + m_breaking_enabled = enabled; + Update(); +} + DelayedMemCheckUpdate MemChecks::Remove(u32 address) { const auto iter = std::ranges::find(m_mem_checks, address, &TMemCheck::start_address); diff --git a/Source/Core/Core/PowerPC/BreakPoints.h b/Source/Core/Core/PowerPC/BreakPoints.h index de22192f7a..da5e755dd3 100644 --- a/Source/Core/Core/PowerPC/BreakPoints.h +++ b/Source/Core/Core/PowerPC/BreakPoints.h @@ -87,6 +87,9 @@ public: bool ToggleBreakPoint(u32 address); bool ToggleEnable(u32 address); + void EnableBreaking(bool enable); + bool IsBreakingEnabled() const { return m_breaking_enabled; } + // Remove Breakpoint. Returns whether it was removed. bool Remove(u32 address); void Clear(); @@ -96,6 +99,7 @@ private: TBreakPoints m_breakpoints; std::optional<TBreakPoint> m_temp_breakpoint; Core::System& m_system; + bool m_breaking_enabled = true; }; class DelayedMemCheckUpdate; @@ -126,9 +130,12 @@ public: bool OverlapsMemcheck(u32 address, u32 length) const; DelayedMemCheckUpdate Remove(u32 address); + void EnableBreaking(bool enable); + bool IsBreakingEnabled() const { return m_breaking_enabled; } + void Update(); void Clear(); - bool HasAny() const { return !m_mem_checks.empty(); } + bool HasAny() const { return !m_mem_checks.empty() && m_breaking_enabled; } BitSet32 GetGPRsUsedInConditions() { return m_gprs_used_in_conditions; } BitSet32 GetFPRsUsedInConditions() { return m_fprs_used_in_conditions; } @@ -142,6 +149,7 @@ private: BitSet32 m_gprs_used_in_conditions; BitSet32 m_fprs_used_in_conditions; bool m_mem_breakpoints_set = false; + bool m_breaking_enabled = true; }; class DelayedMemCheckUpdate final diff --git a/Source/Core/Core/PowerPC/PowerPC.cpp b/Source/Core/Core/PowerPC/PowerPC.cpp index e5ce8ffcdb..7939b6d4bc 100644 --- a/Source/Core/Core/PowerPC/PowerPC.cpp +++ b/Source/Core/Core/PowerPC/PowerPC.cpp @@ -639,7 +639,8 @@ bool PowerPCManager::CheckBreakPoints() { const TBreakPoint* bp = m_breakpoints.GetBreakpoint(m_ppc_state.pc); - if (!bp || !bp->is_enabled || !EvaluateCondition(m_system, bp->condition)) + if (!m_breakpoints.IsBreakingEnabled() || !bp || !bp->is_enabled || + !EvaluateCondition(m_system, bp->condition)) return false; if (bp->log_on_hit) diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index c61edc244b..e7e70c508b 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -69,6 +69,8 @@ add_executable(dolphin-emu Config/ConfigControls/ConfigUserPath.h Config/ControllerInterface/ControllerInterfaceWindow.cpp Config/ControllerInterface/ControllerInterfaceWindow.h + Config/SDLHints/SDLHintsWindow.cpp + Config/SDLHints/SDLHintsWindow.h Config/ControllerInterface/DualShockUDPClientAddServerDialog.cpp Config/ControllerInterface/DualShockUDPClientAddServerDialog.h Config/ControllerInterface/DualShockUDPClientWidget.cpp diff --git a/Source/Core/DolphinQt/CheatSearchWidget.cpp b/Source/Core/DolphinQt/CheatSearchWidget.cpp index 629d8dbafb..35468d5fcc 100644 --- a/Source/Core/DolphinQt/CheatSearchWidget.cpp +++ b/Source/Core/DolphinQt/CheatSearchWidget.cpp @@ -3,7 +3,9 @@ #include "DolphinQt/CheatSearchWidget.h" +#include <algorithm> #include <optional> +#include <ranges> #include <string> #include <unordered_map> #include <utility> @@ -224,6 +226,7 @@ void CheatSearchWidget::CreateWidgets() m_address_table = new QTableWidget(); m_address_table->setContextMenuPolicy(Qt::CustomContextMenu); + m_address_table->setSelectionBehavior(QAbstractItemView::SelectRows); m_info_label_1 = new QLabel(tr("Waiting for first scan...")); m_info_label_2 = new QLabel(); @@ -483,6 +486,8 @@ void CheatSearchWidget::OnAddressTableContextMenu() if (m_address_table->selectedItems().isEmpty()) return; + std::vector<const QTableWidgetItem*> selected_items = GetSelectedAddressTableItems(); + QMenu* menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose, true); @@ -491,8 +496,8 @@ void CheatSearchWidget::OnAddressTableContextMenu() const u32 address = item->data(ADDRESS_TABLE_ADDRESS_ROLE).toUInt(); emit ShowMemory(address); }); - menu->addAction(tr("Add to watch"), this, [this] { - for (auto* const item : m_address_table->selectedItems()) + menu->addAction(tr("Add to watch"), this, [this, selected_items] { + for (auto* const item : selected_items) { const u32 address = item->data(ADDRESS_TABLE_ADDRESS_ROLE).toUInt(); const QString name = QStringLiteral("mem_%1").arg(address, 8, 16, QLatin1Char('0')); @@ -501,6 +506,15 @@ void CheatSearchWidget::OnAddressTableContextMenu() }); menu->addAction(tr("Generate Action Replay Code(s)"), this, &CheatSearchWidget::GenerateARCodes); menu->addAction(tr("Write value"), this, &CheatSearchWidget::WriteValue); + menu->addAction(tr("Delete Address"), this, [this, selected_items] { + // Process in reverse so removal won't change the index of items about to be processed. + for (auto* const item : selected_items | std::views::reverse) + { + const u32 index = item->data(ADDRESS_TABLE_RESULT_INDEX_ROLE).toUInt(); + m_last_value_session->RemoveResult(index); + } + RecreateGUITable(); + }); menu->exec(QCursor::pos()); } @@ -533,7 +547,7 @@ void CheatSearchWidget::GenerateARCodes() bool had_multiple_errors = false; std::optional<Cheats::GenerateActionReplayCodeErrorCode> error_code; - for (auto* const item : m_address_table->selectedItems()) + for (auto* const item : GetSelectedAddressTableItems()) { const u32 index = item->data(ADDRESS_TABLE_RESULT_INDEX_ROLE).toUInt(); const auto result = Cheats::GenerateActionReplayCode(*m_last_value_session, index); @@ -600,9 +614,9 @@ void CheatSearchWidget::WriteValue() return; } - auto items = m_address_table->selectedItems(); + auto items = GetSelectedAddressTableItems(); std::vector<u32> addresses(items.size()); - std::transform(items.begin(), items.end(), addresses.begin(), [](QTableWidgetItem* item) { + std::transform(items.begin(), items.end(), addresses.begin(), [](const QTableWidgetItem* item) { return item->data(ADDRESS_TABLE_ADDRESS_ROLE).toUInt(); }); Core::CPUThreadGuard guard{m_system}; @@ -610,6 +624,7 @@ void CheatSearchWidget::WriteValue() { m_info_label_1->setText(tr("There was an error writing (some) values.")); } + UpdateTableAllCurrentValues(UpdateSource::User); } size_t CheatSearchWidget::GetTableRowCount() const @@ -638,6 +653,26 @@ void CheatSearchWidget::RefreshGUICurrentValues(const size_t begin_index, const } } +const std::vector<const QTableWidgetItem*> CheatSearchWidget::GetSelectedAddressTableItems() const +{ + // Don't process each selectedItems(), as it can produce duplicate commands for one address when + // multiple items in the same row are selected. Instead, uses rows and gets one item from each + // row. All row items have identical data. + auto selected_rows = m_address_table->selectionModel()->selectedRows(); + + // Ascending address order. + std::sort(selected_rows.begin(), selected_rows.end(), + [](const QModelIndex& a, const QModelIndex& b) { return a.row() < b.row(); }); + + std::vector<const QTableWidgetItem*> selected_items; + for (const auto& index : selected_rows) + { + const int row = index.row(); + selected_items.push_back(m_address_table->item(row, 0)); + } + return selected_items; +} + void CheatSearchWidget::RecreateGUITable() { const QSignalBlocker blocker(m_address_table); diff --git a/Source/Core/DolphinQt/CheatSearchWidget.h b/Source/Core/DolphinQt/CheatSearchWidget.h index 18aa5a14b0..5dec20c3ed 100644 --- a/Source/Core/DolphinQt/CheatSearchWidget.h +++ b/Source/Core/DolphinQt/CheatSearchWidget.h @@ -75,6 +75,7 @@ private: int GetVisibleRowsBeginIndex() const; int GetVisibleRowsEndIndex() const; size_t GetTableRowCount() const; + const std::vector<const QTableWidgetItem*> GetSelectedAddressTableItems() const; Core::System& m_system; diff --git a/Source/Core/DolphinQt/Config/CommonControllersWidget.cpp b/Source/Core/DolphinQt/Config/CommonControllersWidget.cpp index 7a5e1256af..6d905eeb8c 100644 --- a/Source/Core/DolphinQt/Config/CommonControllersWidget.cpp +++ b/Source/Core/DolphinQt/Config/CommonControllersWidget.cpp @@ -11,6 +11,7 @@ #include "Core/Config/MainSettings.h" #include "DolphinQt/Config/ControllerInterface/ControllerInterfaceWindow.h" +#include "DolphinQt/Config/SDLHints/SDLHintsWindow.h" #include "DolphinQt/QtUtils/NonDefaultQPushButton.h" #include "DolphinQt/QtUtils/SignalBlocking.h" #include "DolphinQt/Settings.h" @@ -33,9 +34,11 @@ void CommonControllersWidget::CreateLayout() m_common_bg_input = new QCheckBox(tr("Background Input")); m_common_configure_controller_interface = new NonDefaultQPushButton(tr("Alternate Input Sources")); + m_common_configure_sdl_hints = new NonDefaultQPushButton(tr("SDL Controller Settings")); m_common_layout->addWidget(m_common_bg_input); m_common_layout->addWidget(m_common_configure_controller_interface); + m_common_layout->addWidget(m_common_configure_sdl_hints); m_common_box->setLayout(m_common_layout); @@ -51,6 +54,8 @@ void CommonControllersWidget::ConnectWidgets() connect(m_common_bg_input, &QCheckBox::toggled, this, &CommonControllersWidget::SaveSettings); connect(m_common_configure_controller_interface, &QPushButton::clicked, this, &CommonControllersWidget::OnControllerInterfaceConfigure); + connect(m_common_configure_sdl_hints, &QPushButton::clicked, this, + &CommonControllersWidget::OnSDLHintConfigure); } void CommonControllersWidget::OnControllerInterfaceConfigure() @@ -61,6 +66,14 @@ void CommonControllersWidget::OnControllerInterfaceConfigure() window->show(); } +void CommonControllersWidget::OnSDLHintConfigure() +{ + SDLHintsWindow* window = new SDLHintsWindow(this); + window->setAttribute(Qt::WA_DeleteOnClose, true); + window->setWindowModality(Qt::WindowModality::WindowModal); + window->show(); +} + void CommonControllersWidget::LoadSettings() { SignalBlocking(m_common_bg_input)->setChecked(Config::Get(Config::MAIN_INPUT_BACKGROUND_INPUT)); diff --git a/Source/Core/DolphinQt/Config/CommonControllersWidget.h b/Source/Core/DolphinQt/Config/CommonControllersWidget.h index 42d23f74a4..ee0d82513c 100644 --- a/Source/Core/DolphinQt/Config/CommonControllersWidget.h +++ b/Source/Core/DolphinQt/Config/CommonControllersWidget.h @@ -18,6 +18,7 @@ public: private: void OnControllerInterfaceConfigure(); + void OnSDLHintConfigure(); void CreateLayout(); void ConnectWidgets(); @@ -29,4 +30,5 @@ private: QVBoxLayout* m_common_layout; QCheckBox* m_common_bg_input; QPushButton* m_common_configure_controller_interface; + QPushButton* m_common_configure_sdl_hints; }; diff --git a/Source/Core/DolphinQt/Config/LogWidget.cpp b/Source/Core/DolphinQt/Config/LogWidget.cpp index d076b3a9ac..1b59411c08 100644 --- a/Source/Core/DolphinQt/Config/LogWidget.cpp +++ b/Source/Core/DolphinQt/Config/LogWidget.cpp @@ -182,8 +182,7 @@ void LogWidget::LoadSettings() Qt::ScrollBarAlwaysOn); // Log - Font Selection - // Currently "Debugger Font" is not supported as there is no Qt Debugger, defaulting to Monospace - m_log_font->setCurrentIndex(std::min(settings.value(QStringLiteral("logging/font")).toInt(), 1)); + m_log_font->setCurrentIndex(settings.value(QStringLiteral("logging/font")).toInt()); UpdateFont(); } diff --git a/Source/Core/DolphinQt/Config/SDLHints/SDLHintsWindow.cpp b/Source/Core/DolphinQt/Config/SDLHints/SDLHintsWindow.cpp new file mode 100644 index 0000000000..baa3a13be8 --- /dev/null +++ b/Source/Core/DolphinQt/Config/SDLHints/SDLHintsWindow.cpp @@ -0,0 +1,313 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/Config/SDLHints/SDLHintsWindow.h" + +#include "Common/Config/Config.h" + +#include "Core/Config/MainSettings.h" + +#include "DolphinQt/Config/ToolTipControls/ToolTipCheckBox.h" +#include "DolphinQt/QtUtils/NonDefaultQPushButton.h" +#include "DolphinQt/QtUtils/QtUtils.h" +#include "DolphinQt/QtUtils/SignalBlocking.h" + +#include <QDialogButtonBox> +#include <QFrame> +#include <QHeaderView> +#include <QLabel> +#include <QPushButton> +#include <QResizeEvent> +#include <QTabWidget> +#include <QTableWidget> +#include <QVBoxLayout> + +SDLHintsWindow::SDLHintsWindow(QWidget* parent) : QDialog(parent) +{ + CreateMainLayout(); + + setWindowTitle(tr("SDL Controller Settings")); +} + +QSize SDLHintsWindow::sizeHint() const +{ + return {450, 0}; +} + +void SDLHintsWindow::CreateMainLayout() +{ + setMinimumWidth(300); + setMinimumHeight(270); + + m_button_box = new QDialogButtonBox(QDialogButtonBox::Close); + connect(m_button_box, &QDialogButtonBox::rejected, this, &SDLHintsWindow::OnClose); + + // Create hints table + m_hints_table = new QTableWidget(0, 2); + QHeaderView* const hints_table_header = m_hints_table->horizontalHeader(); + m_hints_table->setHorizontalHeaderLabels({tr("Name"), tr("Value")}); + m_hints_table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_hints_table->setSelectionMode(QTableWidget::SingleSelection); + hints_table_header->setSectionResizeMode(0, QHeaderView::Interactive); + hints_table_header->setSectionResizeMode(1, QHeaderView::Fixed); + hints_table_header->setMinimumSectionSize(60); + m_hints_table->verticalHeader()->setVisible(false); + hints_table_header->installEventFilter(this); + QObject::connect(hints_table_header, &QHeaderView::sectionResized, this, + &SDLHintsWindow::SectionResized); + + PopulateTable(); + + // Create table buttons + auto* const add_row_btn = new NonDefaultQPushButton(tr("Add")); + connect(add_row_btn, &QPushButton::pressed, this, &SDLHintsWindow::AddRow); + + m_rem_row_btn = new NonDefaultQPushButton(tr("Remove")); + m_rem_row_btn->setEnabled(false); + connect(m_rem_row_btn, &QPushButton::pressed, this, &SDLHintsWindow::RemoveRow); + connect(m_hints_table, &QTableWidget::itemSelectionChanged, this, + &SDLHintsWindow::SelectionChanged); + + auto* const btns_layout = new QDialogButtonBox; + btns_layout->setContentsMargins(0, 0, 5, 5); + btns_layout->addButton(add_row_btn, QDialogButtonBox::ActionRole); + btns_layout->addButton(m_rem_row_btn, QDialogButtonBox::ActionRole); + + // Create advanced tab + auto* advanced_layout = new QVBoxLayout(); + advanced_layout->addWidget(m_hints_table); + advanced_layout->addWidget(btns_layout); + + auto* advanced_frame = new QFrame(); + advanced_frame->setLayout(advanced_layout); + + // Create default tab + m_directinput_detection = new ToolTipCheckBox(tr("Enable DirectInput Detection")); + m_directinput_detection->SetDescription( + tr("Controls whether SDL should use DirectInput for detecting controllers. Enabling this " + "fixes hotplug detection issues with DualSense controllers but causes Dolphin to hang up " + "on shutdown when using certain 8BitDo controllers.<br><br><dolphin_emphasis>If unsure, " + "leave this checked.</dolphin_emphasis>")); + connect(m_directinput_detection, &ToolTipCheckBox::toggled, [](bool checked) { + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_DIRECTINPUT, checked ? "1" : "0"); + }); + + m_combine_joy_cons = new ToolTipCheckBox(tr("Use Joy-Con Pairs as a Single Controller")); + m_combine_joy_cons->SetDescription( + tr("Controls whether SDL should treat a pair of Joy-Con as a single controller or as two " + "separate controllers.<br><br><dolphin_emphasis>If unsure, leave this " + "checked.</dolphin_emphasis>")); + connect(m_combine_joy_cons, &ToolTipCheckBox::toggled, [](bool checked) { + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, checked ? "1" : "0"); + }); + + m_horizontal_joy_cons = new ToolTipCheckBox(tr("Sideways Joy-Con")); + m_horizontal_joy_cons->SetDescription( + tr("Defines the default orientation for individual Joy-Con. This setting has no effect when " + "Use Joy-Con Pairs as a Single Controller is " + "enabled.<br><br><dolphin_emphasis>If unsure, " + "leave this checked.</dolphin_emphasis>")); + connect(m_horizontal_joy_cons, &ToolTipCheckBox::toggled, [](bool checked) { + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, checked ? "0" : "1"); + }); + + m_dualsense_player_led = new ToolTipCheckBox(tr("Enable DualSense Player LEDs")); + m_dualsense_player_led->SetDescription( + tr("Controls whether the player LEDs should be lit to indicate which player is associated " + "with a DualSense controller.<br><br><dolphin_emphasis>If unsure, leave this " + "unchecked.</dolphin_emphasis>")); + connect(m_dualsense_player_led, &ToolTipCheckBox::toggled, [](bool checked) { + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED, checked ? "1" : "0"); + }); + + auto* const default_layout = new QVBoxLayout(); + default_layout->setContentsMargins(10, 10, 10, 10); + default_layout->addWidget(m_directinput_detection); + default_layout->addWidget(m_combine_joy_cons); + default_layout->addWidget(m_horizontal_joy_cons); + default_layout->addWidget(m_dualsense_player_led); + default_layout->addStretch(1); + + auto* const default_frame = new QFrame(); + default_frame->setLayout(default_layout); + + PopulateChecklist(); + + // Create the tab widget + m_tab_widget = new QTabWidget(); + m_tab_widget->addTab(default_frame, tr("Main")); + m_tab_widget->addTab(advanced_frame, tr("Advanced")); + + m_current_tab_index = 0; + m_tab_widget->setCurrentIndex(m_current_tab_index); + + connect(m_tab_widget, &QTabWidget::currentChanged, this, &SDLHintsWindow::TabChanged); + + auto* const warning_text = + new QLabel(tr("Dolphin must be restarted for these changes to take effect.")); + warning_text->setWordWrap(true); + + // Create main layout + auto* const main_layout = new QVBoxLayout(); + main_layout->addWidget( + QtUtils::CreateIconWarning(this, QStyle::SP_MessageBoxWarning, warning_text), 0); + main_layout->addWidget(m_tab_widget, 1); + main_layout->addWidget(m_button_box, 0, Qt::AlignBottom | Qt::AlignRight); + setLayout(main_layout); +} + +void SDLHintsWindow::PopulateTable() +{ + m_hints_table->setRowCount(0); + + // Loop through all the values in the SDL_Hints settings section and load them into the table + std::shared_ptr<Config::Layer> layer = Config::GetLayer(Config::LayerType::Base); + const Config::Section& section = layer->GetSection(Config::System::Main, "SDL_Hints"); + for (auto& row_data : section) + { + const Config::Location& location = row_data.first; + const std::optional<std::string>& value = row_data.second; + + if (value) + { + m_hints_table->insertRow(m_hints_table->rowCount()); + m_hints_table->setItem(m_hints_table->rowCount() - 1, 0, + new QTableWidgetItem(QString::fromStdString(location.key))); + m_hints_table->setItem(m_hints_table->rowCount() - 1, 1, + new QTableWidgetItem(QString::fromStdString(*value))); + } + } +} + +void SDLHintsWindow::SaveTable() +{ + // Clear all the old values from the SDL_Hints section + std::shared_ptr<Config::Layer> layer = Config::GetLayer(Config::LayerType::Base); + Config::Section section = layer->GetSection(Config::System::Main, "SDL_Hints"); + + for (auto& row_data : section) + row_data.second.reset(); + + // Add each item still in the table to the config file + for (int row = 0; row < m_hints_table->rowCount(); ++row) + { + QTableWidgetItem* hint_name_item = m_hints_table->item(row, 0); + QTableWidgetItem* hint_value_item = m_hints_table->item(row, 1); + + if (hint_name_item != nullptr && hint_value_item != nullptr) + { + const QString& hint_name = hint_name_item->text().trimmed(); + const QString& hint_value = hint_value_item->text().trimmed(); + + if (!hint_name.isEmpty() && !hint_value.isEmpty()) + { + const Config::Info<std::string> setting{ + {Config::System::Main, "SDL_Hints", hint_name.toStdString()}, ""}; + Config::SetBase(setting, hint_value.toStdString()); + } + } + } +} + +void SDLHintsWindow::PopulateChecklist() +{ + // Populate the checklist and default to the SDL default for an invalid value + + // Default to checked if incorrectly set + SignalBlocking(m_directinput_detection) + ->setChecked(Config::GetBase(Config::MAIN_SDL_HINT_JOYSTICK_DIRECTINPUT) != "0"); + + // Default to checked if incorrectly set + SignalBlocking(m_combine_joy_cons) + ->setChecked(Config::GetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS) != "0"); + + // Default to checked if incorrectly set + SignalBlocking(m_horizontal_joy_cons) + ->setChecked(Config::GetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS) != "1"); + + // Default to checked if incorrectly set + SignalBlocking(m_dualsense_player_led) + ->setChecked(Config::GetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED) != "0"); +} + +void SDLHintsWindow::AddRow() +{ + m_hints_table->insertRow(m_hints_table->rowCount()); + m_hints_table->scrollToBottom(); +} + +void SDLHintsWindow::RemoveRow() +{ + QModelIndex index = m_hints_table->selectionModel()->currentIndex(); + m_hints_table->removeRow(index.row()); +} + +void SDLHintsWindow::SelectionChanged() +{ + m_rem_row_btn->setEnabled(m_hints_table->selectionModel()->hasSelection()); +} + +void SDLHintsWindow::OnClose() +{ + TabChanged(-1); // Pass -1 to indicate exit + + reject(); +} + +void SDLHintsWindow::TabChanged(int new_index) +{ + // Check which tab we're coming from, cur_tab_idx has not been updated yet + switch (m_current_tab_index) + { + case 1: // Coming from the advanced tab + SaveTable(); + break; + + default: + break; + } + + // Check which tab we're going to + switch (new_index) + { + case 0: // Going to the main tab + PopulateChecklist(); + break; + + case 1: // Going to the advanced tab + PopulateTable(); + break; + + default: + break; + } + + m_current_tab_index = new_index; +} + +void SDLHintsWindow::SectionResized(int logical_index, int old_size, int new_size) +{ + if (logical_index == 0 && old_size != new_size) + { + QHeaderView* const header = m_hints_table->horizontalHeader(); + header->setMaximumSectionSize(header->size().width() - header->minimumSectionSize()); + header->resizeSection(1, header->size().width() - new_size); + } +} + +bool SDLHintsWindow::eventFilter(QObject* obj, QEvent* event) +{ + auto* const table_widget = qobject_cast<QHeaderView*>(obj); + if (table_widget) + { + if (event->type() == QEvent::Resize) + { + auto* const resize_event = static_cast<QResizeEvent*>(event); + QHeaderView* header = m_hints_table->horizontalHeader(); + header->setMaximumSectionSize(resize_event->size().width() - header->minimumSectionSize()); + header->resizeSection(0, resize_event->size().width() - header->sectionSize(1)); + } + } + + return QDialog::eventFilter(obj, event); +} diff --git a/Source/Core/DolphinQt/Config/SDLHints/SDLHintsWindow.h b/Source/Core/DolphinQt/Config/SDLHints/SDLHintsWindow.h new file mode 100644 index 0000000000..33103f9314 --- /dev/null +++ b/Source/Core/DolphinQt/Config/SDLHints/SDLHintsWindow.h @@ -0,0 +1,48 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QDialog> + +class QTabWidget; +class QDialogButtonBox; +class QTableWidget; +class QPushButton; +class ToolTipCheckBox; + +class SDLHintsWindow final : public QDialog +{ + Q_OBJECT +public: + explicit SDLHintsWindow(QWidget* parent); + + QSize sizeHint() const override; + +private: + void CreateMainLayout(); + + void PopulateTable(); + void SaveTable(); + void PopulateChecklist(); + void AddRow(); + void RemoveRow(); + void SelectionChanged(); + void OnClose(); + void TabChanged(int new_index); + void SectionResized(int logical_index, int old_size, int new_size); + + bool eventFilter(QObject* obj, QEvent* event) override; + + QTabWidget* m_tab_widget; + QDialogButtonBox* m_button_box; + QTableWidget* m_hints_table; + QPushButton* m_rem_row_btn; + + ToolTipCheckBox* m_directinput_detection; + ToolTipCheckBox* m_combine_joy_cons; + ToolTipCheckBox* m_horizontal_joy_cons; + ToolTipCheckBox* m_dualsense_player_led; + + int m_current_tab_index; +}; diff --git a/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp b/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp index 09b045688c..fbb5c38d9b 100644 --- a/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp @@ -173,6 +173,7 @@ void BreakpointWidget::CreateWidgets() layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(0); + m_enabled = m_toolbar->addAction(tr("Disable"), this, &BreakpointWidget::OnToggleBreaking); m_new = m_toolbar->addAction(tr("New"), this, &BreakpointWidget::OnNewBreakpoint); m_clear = m_toolbar->addAction(tr("Clear"), this, &BreakpointWidget::OnClear); @@ -190,6 +191,10 @@ void BreakpointWidget::CreateWidgets() void BreakpointWidget::UpdateIcons() { + if (m_system.GetPowerPC().GetBreakPoints().IsBreakingEnabled()) + m_enabled->setIcon(Resources::GetThemeIcon("pause")); + else + m_enabled->setIcon(Resources::GetThemeIcon("play")); m_new->setIcon(Resources::GetThemeIcon("debugger_add_breakpoint")); m_clear->setIcon(Resources::GetThemeIcon("debugger_clear")); m_load->setIcon(Resources::GetThemeIcon("debugger_load")); @@ -289,6 +294,24 @@ void BreakpointWidget::Update() QPixmap enabled_icon = Resources::GetThemeIcon("debugger_breakpoint").pixmap(QSize(downscale, downscale)); + auto& power_pc = m_system.GetPowerPC(); + auto& breakpoints = power_pc.GetBreakPoints(); + + if (!breakpoints.IsBreakingEnabled()) + { + // Use QPainter to draw a transparent hole in the center + QImage image = enabled_icon.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + QPainter painter(&image); + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(Qt::NoPen); + painter.setBrush(Qt::transparent); + // Center and radius + painter.drawEllipse(QPoint(downscale / 2, downscale / 2), downscale / 4, downscale / 4); + painter.end(); + enabled_icon = QPixmap::fromImage(image); + } + const auto create_item = [](const QString& string = {}) { QTableWidgetItem* item = new QTableWidgetItem(string); item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); @@ -306,8 +329,6 @@ void BreakpointWidget::Update() Settings::Instance().IsThemeDark() ? QColor(75, 75, 75) : QColor(225, 225, 225); disabled_item.setBackground(disabled_color); - auto& power_pc = m_system.GetPowerPC(); - auto& breakpoints = power_pc.GetBreakPoints(); auto& memchecks = power_pc.GetMemChecks(); auto& ppc_symbol_db = power_pc.GetSymbolDB(); @@ -466,6 +487,36 @@ void BreakpointWidget::OnEditBreakpoint(u32 address, bool is_instruction_bp) emit Host::GetInstance()->PPCBreakpointsChanged(); } +void BreakpointWidget::OnToggleBreaking() +{ + auto& breakpoints = m_system.GetPowerPC().GetBreakPoints(); + auto& memchecks = m_system.GetPowerPC().GetMemChecks(); + // Memcheck's HasAny() will report no memchecks while breaking is disabled, so only check when + // breaking is true. + bool has_memory_bp; + + // Currently toggles all code and memory breakpoints. Could be split if needed. + if (breakpoints.IsBreakingEnabled()) + { + has_memory_bp = memchecks.HasAny(); + breakpoints.EnableBreaking(false); + memchecks.EnableBreaking(false); + m_enabled->setText(tr("Enable")); + m_enabled->setIcon(Resources::GetThemeIcon("play")); + } + else + { + breakpoints.EnableBreaking(true); + memchecks.EnableBreaking(true); + has_memory_bp = memchecks.HasAny(); + m_enabled->setText(tr("Disable")); + m_enabled->setIcon(Resources::GetThemeIcon("pause")); + } + + if (has_memory_bp || !breakpoints.GetBreakPoints().empty()) + emit Host::GetInstance()->PPCBreakpointsChanged(); +} + void BreakpointWidget::OnLoad() { Common::IniFile ini; diff --git a/Source/Core/DolphinQt/Debugger/BreakpointWidget.h b/Source/Core/DolphinQt/Debugger/BreakpointWidget.h index f48570f449..64dfcf1283 100644 --- a/Source/Core/DolphinQt/Debugger/BreakpointWidget.h +++ b/Source/Core/DolphinQt/Debugger/BreakpointWidget.h @@ -58,6 +58,7 @@ private: void OnClear(); void OnClicked(QTableWidgetItem* item); + void OnToggleBreaking(); void OnNewBreakpoint(); void OnEditBreakpoint(u32 address, bool is_instruction_bp); void OnLoad(); @@ -70,6 +71,7 @@ private: QToolBar* m_toolbar; QTableWidget* m_table; + QAction* m_enabled; QAction* m_new; QAction* m_clear; QAction* m_load; diff --git a/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp b/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp index ce02b6baa5..c7a74ee29d 100644 --- a/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp @@ -319,6 +319,7 @@ void CodeViewWidget::Update(const Core::CPUThreadGuard* guard) guard ? std::make_optional(power_pc.GetPPCState().pc) : std::nullopt; const bool dark_theme = Settings::Instance().IsThemeDark(); + const bool breaking_enabled = power_pc.GetBreakPoints().IsBreakingEnabled(); m_branches.clear(); @@ -400,7 +401,7 @@ void CodeViewWidget::Update(const Core::CPUThreadGuard* guard) if (bp != nullptr) { auto icon = Resources::GetThemeIcon("debugger_breakpoint").pixmap(QSize(rowh - 2, rowh - 2)); - if (!bp->is_enabled) + if (!breaking_enabled || !bp->is_enabled) { QPixmap disabled_icon(icon.size()); disabled_icon.fill(Qt::transparent); diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 9df5caf413..a9459c971a 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -67,6 +67,7 @@ <ClCompile Include="Config\ConfigControls\ConfigText.cpp" /> <ClCompile Include="Config\ConfigControls\ConfigUserPath.cpp" /> <ClCompile Include="Config\ControllerInterface\ControllerInterfaceWindow.cpp" /> + <ClCompile Include="Config\SDLHints\SDLHintsWindow.cpp" /> <ClCompile Include="Config\ControllerInterface\DualShockUDPClientAddServerDialog.cpp" /> <ClCompile Include="Config\ControllerInterface\DualShockUDPClientWidget.cpp" /> <ClCompile Include="Config\ControllerInterface\ServerStringValidator.cpp" /> @@ -302,6 +303,7 @@ <QtMoc Include="Config\ConfigControls\ConfigText.h" /> <QtMoc Include="Config\ConfigControls\ConfigUserPath.h" /> <QtMoc Include="Config\ControllerInterface\ControllerInterfaceWindow.h" /> + <QtMoc Include="Config\SDLHints\SDLHintsWindow.h" /> <QtMoc Include="Config\ControllerInterface\DualShockUDPClientAddServerDialog.h" /> <QtMoc Include="Config\ControllerInterface\DualShockUDPClientWidget.h" /> <QtMoc Include="Config\ControllerInterface\ServerStringValidator.h" /> diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp index b5057f09d7..f6912f67ce 100644 --- a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp +++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp @@ -135,19 +135,31 @@ InputBackend::InputBackend(ControllerInterface* controller_interface) { EnableSDLLogging(); - SDL_SetHint(SDL_HINT_JOYSTICK_ENHANCED_REPORTS, "1"); + if (Config::Get(Config::MAIN_SDL_HINT_JOYSTICK_ENHANCED_REPORTS) == "") + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_ENHANCED_REPORTS, "1"); // We have our own WGI backend. Enabling SDL's WGI handling creates even more redundant devices. - SDL_SetHint(SDL_HINT_JOYSTICK_WGI, "0"); + if (Config::Get(Config::MAIN_SDL_HINT_JOYSTICK_WGI) == "") + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_WGI, "0"); // Disable DualSense Player LEDs; We already colorize the Primary LED - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED, "0"); + if (Config::Get(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED) == "") + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED, "0"); // Disabling DirectInput support apparently solves hangs on shutdown for users with - // "8BitDo Ultimate 2" controllers. + // "8BitDo Ultimate 2" controllers, however, it also breaks hotplug support for Dual Sense + // and DS4 Controllers, so we leave it enabled for now. // It also works around a possibly related random hang on a IDirectInputDevice8_Acquire // call within SDL. - SDL_SetHint(SDL_HINT_JOYSTICK_DIRECTINPUT, "0"); + if (Config::Get(Config::MAIN_SDL_HINT_JOYSTICK_DIRECTINPUT) == "") + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_DIRECTINPUT, "1"); + + // Pre-populate the default Joy-Con hints so they can be easily changed + if (Config::Get(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS) == "") + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "1"); + + if (Config::Get(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS) == "") + Config::SetBase(Config::MAIN_SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, "0"); // Disable SDL's GC Adapter handling when we want to handle it ourselves. bool is_gc_adapter_configured = false; @@ -164,6 +176,18 @@ InputBackend::InputBackend(ControllerInterface* controller_interface) // and ControllerInterface isn't prepared for SDL to spontaneously re-initialize itself. SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_GAMECUBE, is_gc_adapter_configured ? "0" : "1"); + // Load all the hints from the config file + std::shared_ptr<Config::Layer> layer = Config::GetLayer(Config::LayerType::Base); + const Config::Section& section = layer->GetSection(Config::System::Main, "SDL_Hints"); + for (auto& row_data : section) + { + const Config::Location& location = row_data.first; + const std::optional<std::string>& value = row_data.second; + + if (value) + SDL_SetHint(location.key.c_str(), value->c_str()); + } + m_hotplug_thread = std::thread([this] { Common::SetCurrentThreadName("SDL Hotplug Thread"); |
