diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp index 27c17ce5fb..6d5c149024 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp @@ -9,8 +9,6 @@ #include #include -#include -#include #include #include #include @@ -31,6 +29,7 @@ #include "DolphinQt/Config/Mapping/MappingWindow.h" #include "DolphinQt/QtUtils/BlockUserInputFilter.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" +#include "DolphinQt/Settings.h" #include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControlReference/ExpressionParser.h" @@ -220,6 +219,8 @@ IOWindow::IOWindow(MappingWidget* parent, ControllerEmu::EmulatedController* con CreateMainLayout(); connect(parent, &MappingWidget::Update, this, &IOWindow::Update); + connect(parent->GetParent(), &MappingWindow::ConfigChanged, this, &IOWindow::ConfigChanged); + connect(&Settings::Instance(), &Settings::ConfigChanged, this, &IOWindow::ConfigChanged); setWindowTitle(type == IOWindow::Type::Input ? tr("Configure Input") : tr("Configure Output")); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -229,7 +230,7 @@ IOWindow::IOWindow(MappingWidget* parent, ControllerEmu::EmulatedController* con ConnectWidgets(); } -std::shared_ptr IOWindow::GetSelectedDevice() +std::shared_ptr IOWindow::GetSelectedDevice() const { return m_selected_device; } @@ -258,7 +259,7 @@ void IOWindow::CreateMainLayout() m_expression_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); new ControlExpressionSyntaxHighlighter(m_expression_text->document()); - m_operators_combo = new QComboBoxWithMouseWheelDisabled(); + m_operators_combo = new QComboBoxWithMouseWheelDisabled(this); m_operators_combo->addItem(tr("Operators")); m_operators_combo->insertSeparator(1); if (m_type == Type::Input) @@ -340,6 +341,7 @@ void IOWindow::CreateMainLayout() m_option_list->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); m_option_list->setItemDelegate(new InputStateDelegate(this, 1, [&](int row) { + std::lock_guard lock(m_selected_device_mutex); // Clamp off negative values but allow greater than one in the text display. return std::max(GetSelectedDevice()->Inputs()[row]->GetState(), 0.0); })); @@ -400,7 +402,7 @@ void IOWindow::CreateMainLayout() void IOWindow::ConfigChanged() { const QSignalBlocker blocker(this); - const auto lock = m_controller->GetStateLock(); + const auto lock = ControllerEmu::EmulatedController::GetStateLock(); // ensure m_parse_text is in the right state UpdateExpression(m_reference->GetExpression(), UpdateMode::Force); @@ -410,10 +412,10 @@ void IOWindow::ConfigChanged() m_range_spinbox->setValue(m_reference->range * SLIDER_TICK_COUNT); m_range_slider->setValue(m_reference->range * SLIDER_TICK_COUNT); - m_devq = m_controller->GetDefaultDevice(); + if (m_devq.ToString().empty()) + m_devq = m_controller->GetDefaultDevice(); UpdateDeviceList(); - UpdateOptionList(); } void IOWindow::Update() @@ -425,6 +427,8 @@ void IOWindow::Update() void IOWindow::ConnectWidgets() { connect(m_select_button, &QPushButton::clicked, [this] { AppendSelectedOption(); }); + connect(&Settings::Instance(), &Settings::ReleaseDevices, this, &IOWindow::ReleaseDevices); + connect(&Settings::Instance(), &Settings::DevicesChanged, this, &IOWindow::UpdateDeviceList); connect(m_detect_button, &QPushButton::clicked, this, &IOWindow::OnDetectButtonPressed); connect(m_test_button, &QPushButton::clicked, this, &IOWindow::OnTestButtonPressed); @@ -479,16 +483,19 @@ void IOWindow::ConnectWidgets() void IOWindow::AppendSelectedOption() { - if (m_option_list->currentItem() == nullptr) + if (m_option_list->currentRow() < 0) return; m_expression_text->insertPlainText(MappingCommon::GetExpressionForControl( - m_option_list->currentItem()->text(), m_devq, m_controller->GetDefaultDevice())); + m_option_list->item(m_option_list->currentRow(), 0)->text(), m_devq, + m_controller->GetDefaultDevice())); } -void IOWindow::OnDeviceChanged(const QString& device) +void IOWindow::OnDeviceChanged() { - m_devq.FromString(device.toStdString()); + const std::string device_name = + m_devices_combo->count() > 0 ? m_devices_combo->currentData().toString().toStdString() : ""; + m_devq.FromString(device_name); UpdateOptionList(); } @@ -500,7 +507,7 @@ void IOWindow::OnDialogButtonPressed(QAbstractButton* button) return; } - const auto lock = m_controller->GetStateLock(); + const auto lock = ControllerEmu::EmulatedController::GetStateLock(); UpdateExpression(m_expression_text->toPlainText().toStdString()); m_original_expression = m_reference->GetExpression(); @@ -525,6 +532,7 @@ void IOWindow::OnDetectButtonPressed() const auto list = m_option_list->findItems(expression, Qt::MatchFixedString); + // Try to select the first. If this fails, the last selected item would still appear as such if (!list.empty()) m_option_list->setCurrentItem(list[0]); } @@ -541,8 +549,15 @@ void IOWindow::OnRangeChanged(int value) m_range_slider->setValue(m_reference->range * SLIDER_TICK_COUNT); } +void IOWindow::ReleaseDevices() +{ + std::lock_guard lock(m_selected_device_mutex); + m_selected_device = nullptr; +} + void IOWindow::UpdateOptionList() { + std::lock_guard lock(m_selected_device_mutex); m_selected_device = g_controller_interface.FindDevice(m_devq); m_option_list->setRowCount(0); @@ -575,13 +590,62 @@ void IOWindow::UpdateOptionList() void IOWindow::UpdateDeviceList() { + const QSignalBlocker blocker(m_devices_combo); + + const auto previous_device_name = m_devices_combo->currentData().toString().toStdString(); + m_devices_combo->clear(); + // Default to the default device or to the first device if there isn't a default. + // Try to the keep the previous selected device, mark it as disconnected if it's gone, as it could + // reconnect soon after if this is a devices refresh and it would be annoying to lose the value. + const auto default_device_name = m_controller->GetDefaultDevice().ToString(); + int default_device_index = -1; + int previous_device_index = -1; for (const auto& name : g_controller_interface.GetAllDeviceStrings()) - m_devices_combo->addItem(QString::fromStdString(name)); + { + QString qname = QString(); + if (name == default_device_name) + { + default_device_index = m_devices_combo->count(); + // Specify "default" even if we only have one device + qname.append(QLatin1Char{'['} + tr("default") + QStringLiteral("] ")); + } + if (name == previous_device_name) + { + previous_device_index = m_devices_combo->count(); + } + qname.append(QString::fromStdString(name)); + m_devices_combo->addItem(qname, QString::fromStdString(name)); + } - m_devices_combo->setCurrentText( - QString::fromStdString(m_controller->GetDefaultDevice().ToString())); + if (previous_device_index >= 0) + { + m_devices_combo->setCurrentIndex(previous_device_index); + } + else if (!previous_device_name.empty()) + { + const QString qname = QString::fromStdString(previous_device_name); + QString adjusted_qname; + if (previous_device_name == default_device_name) + { + adjusted_qname.append(QLatin1Char{'['} + tr("default") + QStringLiteral("] ")); + } + adjusted_qname.append(QLatin1Char{'['} + tr("disconnected") + QStringLiteral("] ")) + .append(qname); + m_devices_combo->addItem(adjusted_qname, qname); + m_devices_combo->setCurrentIndex(m_devices_combo->count() - 1); + } + else if (default_device_index >= 0) + { + m_devices_combo->setCurrentIndex(default_device_index); + } + else if (m_devices_combo->count() > 0) + { + m_devices_combo->setCurrentIndex(0); + } + // The device object might have changed so we need to always refresh it + OnDeviceChanged(); } void IOWindow::UpdateExpression(std::string new_expression, UpdateMode mode) diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.h b/Source/Core/DolphinQt/Config/Mapping/IOWindow.h index 320ebefde4..7d22ddbc0d 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.h +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -69,16 +70,16 @@ public: explicit IOWindow(MappingWidget* parent, ControllerEmu::EmulatedController* m_controller, ControlReference* ref, Type type); - std::shared_ptr GetSelectedDevice(); - private: + std::shared_ptr GetSelectedDevice() const; + void CreateMainLayout(); void ConnectWidgets(); void ConfigChanged(); void Update(); void OnDialogButtonPressed(QAbstractButton* button); - void OnDeviceChanged(const QString& device); + void OnDeviceChanged(); void OnDetectButtonPressed(); void OnTestButtonPressed(); void OnRangeChanged(int range); @@ -86,6 +87,7 @@ private: void AppendSelectedOption(); void UpdateOptionList(); void UpdateDeviceList(); + void ReleaseDevices(); enum class UpdateMode { @@ -135,4 +137,5 @@ private: ciface::Core::DeviceQualifier m_devq; Type m_type; std::shared_ptr m_selected_device; + std::mutex m_selected_device_mutex; }; diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp index ac6230cec4..f3aed4eea0 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp @@ -328,7 +328,7 @@ bool MappingWindow::IsMappingAllDevices() const void MappingWindow::RefreshDevices() { - Core::RunAsCPUThread([&] { g_controller_interface.RefreshDevices(); }); + g_controller_interface.RefreshDevices(); } void MappingWindow::OnGlobalDevicesChanged() diff --git a/Source/Core/DolphinQt/Host.cpp b/Source/Core/DolphinQt/Host.cpp index b0cfb507bd..f85b0b7262 100644 --- a/Source/Core/DolphinQt/Host.cpp +++ b/Source/Core/DolphinQt/Host.cpp @@ -35,6 +35,8 @@ #include "VideoCommon/RenderBase.h" #include "VideoCommon/VideoConfig.h" +static thread_local bool tls_is_host_thread = false; + Host::Host() { State::SetOnAfterLoadCallback([] { Host_UpdateDisasmDialog(); }); @@ -51,6 +53,16 @@ Host* Host::GetInstance() return s_instance; } +void Host::DeclareAsHostThread() +{ + tls_is_host_thread = true; +} + +bool Host::IsHostThread() +{ + return tls_is_host_thread; +} + void Host::SetRenderHandle(void* handle) { m_render_to_main = Config::Get(Config::MAIN_RENDER_TO_MAIN); @@ -62,8 +74,7 @@ void Host::SetRenderHandle(void* handle) if (g_renderer) { g_renderer->ChangeSurface(handle); - if (g_controller_interface.IsInit()) - g_controller_interface.ChangeWindow(handle); + g_controller_interface.ChangeWindow(handle); } } diff --git a/Source/Core/DolphinQt/Host.h b/Source/Core/DolphinQt/Host.h index 6be28d5464..90c8b7042d 100644 --- a/Source/Core/DolphinQt/Host.h +++ b/Source/Core/DolphinQt/Host.h @@ -22,6 +22,9 @@ public: static Host* GetInstance(); + void DeclareAsHostThread(); + bool IsHostThread(); + bool GetRenderFocus(); bool GetRenderFullFocus(); bool GetRenderFullscreen(); diff --git a/Source/Core/DolphinQt/Main.cpp b/Source/Core/DolphinQt/Main.cpp index 9efad48c35..1399a40391 100644 --- a/Source/Core/DolphinQt/Main.cpp +++ b/Source/Core/DolphinQt/Main.cpp @@ -120,6 +120,8 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine } #endif + Host::GetInstance()->DeclareAsHostThread(); + #ifdef __APPLE__ // On macOS, a command line option matching the format "-psn_X_XXXXXX" is passed when // the application is launched for the first time. This is to set the "ProcessSerialNumber", diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 3b01fec11b..265155f2b9 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -789,7 +789,7 @@ void MainWindow::TogglePause() void MainWindow::OnStopComplete() { m_stop_requested = false; - HideRenderWidget(); + HideRenderWidget(true, m_exit_requested); #ifdef USE_DISCORD_PRESENCE if (!m_netplay_dialog->isVisible()) Discord::UpdateDiscordPresence(); @@ -1099,7 +1099,7 @@ void MainWindow::ShowRenderWidget() } } -void MainWindow::HideRenderWidget(bool reinit) +void MainWindow::HideRenderWidget(bool reinit, bool is_exit) { if (m_rendering_to_main) { @@ -1136,7 +1136,9 @@ void MainWindow::HideRenderWidget(bool reinit) // The controller interface will still be registered to the old render widget, if the core // has booted. Therefore, we should re-bind it to the main window for now. When the core // is next started, it will be swapped back to the new render widget. - g_controller_interface.ChangeWindow(GetWindowSystemInfo(windowHandle()).render_window); + g_controller_interface.ChangeWindow(GetWindowSystemInfo(windowHandle()).render_window, + is_exit ? ControllerInterface::WindowChangeReason::Exit : + ControllerInterface::WindowChangeReason::Other); } } diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index 79383b3541..7d9e97de28 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -142,7 +142,7 @@ private: const std::optional& savestate_path = {}); void StartGame(std::unique_ptr&& parameters); void ShowRenderWidget(); - void HideRenderWidget(bool reinit = true); + void HideRenderWidget(bool reinit = true, bool is_exit = false); void ShowSettingsWindow(); void ShowGeneralWindow(); diff --git a/Source/Core/DolphinQt/Settings.cpp b/Source/Core/DolphinQt/Settings.cpp index 7541a9087f..3f9fb663f0 100644 --- a/Source/Core/DolphinQt/Settings.cpp +++ b/Source/Core/DolphinQt/Settings.cpp @@ -25,6 +25,7 @@ #include "Core/NetPlayClient.h" #include "Core/NetPlayServer.h" +#include "DolphinQt/Host.h" #include "DolphinQt/QtUtils/QueueOnObject.h" #include "InputCommon/ControllerInterface/ControllerInterface.h" @@ -44,8 +45,24 @@ Settings::Settings() Config::AddConfigChangedCallback( [this] { QueueOnObject(this, [this] { emit ConfigChanged(); }); }); - g_controller_interface.RegisterDevicesChangedCallback( - [this] { QueueOnObject(this, [this] { emit DevicesChanged(); }); }); + g_controller_interface.RegisterDevicesChangedCallback([this] { + if (Host::GetInstance()->IsHostThread()) + { + emit DevicesChanged(); + } + else + { + // Any device shared_ptr in the host thread needs to be released immediately as otherwise + // they'd continue living until the queued event has run, but some devices can't be recreated + // until they are destroyed. + // This is safe from any thread. Devices will be refreshed and re-acquired and in + // DevicesChanged(). Waiting on QueueOnObject() to have finished running was not feasible as + // it would cause deadlocks without heavy workarounds. + emit ReleaseDevices(); + + QueueOnObject(this, [this] { emit DevicesChanged(); }); + } + }); } Settings::~Settings() = default; diff --git a/Source/Core/DolphinQt/Settings.h b/Source/Core/DolphinQt/Settings.h index d5fd90989c..e2c5dbde9e 100644 --- a/Source/Core/DolphinQt/Settings.h +++ b/Source/Core/DolphinQt/Settings.h @@ -192,6 +192,7 @@ signals: void AutoUpdateTrackChanged(const QString& mode); void FallbackRegionChanged(const DiscIO::Region& region); void AnalyticsToggled(bool enabled); + void ReleaseDevices(); void DevicesChanged(); void SDCardInsertionChanged(bool inserted); void USBKeyboardConnectionChanged(bool connected);