From e8877daca3771b8abec4acf7b3a43f794fe07588 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 15 Oct 2022 21:20:05 +1000 Subject: [PATCH] Qt: Register for device notifications on Windows That way we know when a controller is connected/disconnected, and don't have to poll unconnected XInput controllers. --- pcsx2-qt/MainWindow.cpp | 60 ++++++++++++++++++ pcsx2-qt/MainWindow.h | 11 ++++ pcsx2-qt/QtHost.cpp | 22 +++++++ pcsx2-qt/QtHost.h | 2 + pcsx2/Frontend/DInputSource.cpp | 102 ++++++++++++++++++------------ pcsx2/Frontend/DInputSource.h | 15 ++--- pcsx2/Frontend/InputManager.cpp | 13 ++++ pcsx2/Frontend/InputManager.h | 4 ++ pcsx2/Frontend/InputSource.h | 1 + pcsx2/Frontend/SDLInputSource.cpp | 7 ++ pcsx2/Frontend/SDLInputSource.h | 1 + pcsx2/Frontend/XInputSource.cpp | 35 ++++++++++ pcsx2/Frontend/XInputSource.h | 1 + 13 files changed, 225 insertions(+), 49 deletions(-) diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index bffef8fe94..c55c94daf4 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -54,6 +54,11 @@ #include "svnrev.h" #include "Tools/InputRecording/NewInputRecordingDlg.h" +#ifdef _WIN32 +#include "common/RedtapeWindows.h" +#include +#endif + #ifdef ENABLE_RAINTEGRATION #include "pcsx2/Frontend/Achievements.h" #endif @@ -120,6 +125,9 @@ MainWindow::~MainWindow() // we compare here, since recreate destroys the window later if (g_main_window == this) g_main_window = nullptr; +#ifdef _WIN32 + unregisterForDeviceNotifications(); +#endif #ifdef __APPLE__ CocoaTools::RemoveThemeChangeHandler(this); #endif @@ -145,6 +153,10 @@ void MainWindow::initialize() switchToGameListView(); updateWindowTitle(); updateSaveStateMenus(QString(), QString(), 0); + +#ifdef _WIN32 + registerForDeviceNotifications(); +#endif } // TODO: Figure out how to set this in the .ui file @@ -421,6 +433,9 @@ void MainWindow::recreate() if (s_vm_valid) requestShutdown(false, true, EmuConfig.SaveStateOnShutdown); + // We need to close input sources, because e.g. DInput uses our window handle. + g_emu_thread->closeInputSources(); + close(); g_main_window = nullptr; @@ -429,6 +444,9 @@ void MainWindow::recreate() new_main_window->refreshGameList(false); new_main_window->show(); deleteLater(); + + // Reload the sources we just closed. + g_emu_thread->reloadInputSources(); } void MainWindow::recreateSettings() @@ -1808,6 +1826,48 @@ void MainWindow::dropEvent(QDropEvent* event) } } +void MainWindow::registerForDeviceNotifications() +{ +#ifdef _WIN32 + // We use these notifications to detect when a controller is connected or disconnected. + DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE}; + m_device_notification_handle = RegisterDeviceNotificationW((HANDLE)winId(), &filter, + DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES); +#endif +} + +void MainWindow::unregisterForDeviceNotifications() +{ +#ifdef _WIN32 + if (!m_device_notification_handle) + return; + + UnregisterDeviceNotification(static_cast(m_device_notification_handle)); + m_device_notification_handle = nullptr; +#endif +} + +#ifdef _WIN32 + +bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result) +{ + static constexpr const char win_type[] = "windows_generic_MSG"; + if (eventType == QByteArray(win_type, sizeof(win_type) - 1)) + { + const MSG* msg = static_cast(message); + if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED) + { + g_emu_thread->reloadInputDevices(); + *result = 1; + return true; + } + } + + return QMainWindow::nativeEvent(eventType, message, result); +} + +#endif + DisplayWidget* MainWindow::createDisplay(bool fullscreen, bool render_to_main) { DevCon.WriteLn("createDisplay(%u, %u)", static_cast(fullscreen), static_cast(render_to_main)); diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 95ff6f56a5..0b04092f7c 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -177,6 +177,10 @@ protected: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; +#ifdef _WIN32 + bool nativeEvent(const QByteArray& eventType, void* message, qintptr* result) override; +#endif + private: static void setStyleFromSettings(); static void setIconThemeFromStyle(); @@ -186,6 +190,9 @@ private: void recreate(); void recreateSettings(); + void registerForDeviceNotifications(); + void unregisterForDeviceNotifications(); + void saveStateToConfig(); void restoreStateFromConfig(); @@ -265,6 +272,10 @@ private: bool m_is_closing = false; QString m_last_fps_status; + +#ifdef _WIN32 + void* m_device_notification_handle = nullptr; +#endif }; extern MainWindow* g_main_window; diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index 3611f848fa..e3a23874df 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -701,6 +701,28 @@ void EmuThread::reloadInputBindings() InputManager::ReloadBindings(*si, *bindings_si); } +void EmuThread::reloadInputDevices() +{ + if (!isOnEmuThread()) + { + QMetaObject::invokeMethod(this, &EmuThread::reloadInputDevices, Qt::QueuedConnection); + return; + } + + InputManager::ReloadDevices(); +} + +void EmuThread::closeInputSources() +{ + if (!isOnEmuThread()) + { + QMetaObject::invokeMethod(this, &EmuThread::reloadInputDevices, Qt::BlockingQueuedConnection); + return; + } + + InputManager::CloseSources(); +} + void EmuThread::requestDisplaySize(float scale) { if (!isOnEmuThread()) diff --git a/pcsx2-qt/QtHost.h b/pcsx2-qt/QtHost.h index e1629c62a8..f23d5bb5c4 100644 --- a/pcsx2-qt/QtHost.h +++ b/pcsx2-qt/QtHost.h @@ -105,6 +105,8 @@ public Q_SLOTS: void reloadPatches(); void reloadInputSources(); void reloadInputBindings(); + void reloadInputDevices(); + void closeInputSources(); void requestDisplaySize(float scale); void enumerateInputDevices(); void enumerateVibrationMotors(); diff --git a/pcsx2/Frontend/DInputSource.cpp b/pcsx2/Frontend/DInputSource.cpp index e879e6999e..b86bb5d577 100644 --- a/pcsx2/Frontend/DInputSource.cpp +++ b/pcsx2/Frontend/DInputSource.cpp @@ -35,7 +35,7 @@ DInputSource::DInputSource() = default; DInputSource::~DInputSource() { m_controllers.clear(); - m_dinput.Reset(); + m_dinput.reset(); if (m_dinput_module) FreeLibrary(m_dinput_module); } @@ -85,7 +85,7 @@ bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock(m_dinput.GetAddressOf()), nullptr); + GetModuleHandleA(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W, reinterpret_cast(m_dinput.put()), nullptr); m_joystick_data_format = get_joystick_data_format(); if (FAILED(hr) || !m_joystick_data_format) { @@ -96,12 +96,15 @@ bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock toplevel_wi(Host::GetTopLevelWindowInfo()); - if (toplevel_wi.has_value() && toplevel_wi->type == WindowInfo::Type::Win32) - AddDevices(static_cast(toplevel_wi->window_handle)); - else + if (!toplevel_wi.has_value() || toplevel_wi->type != WindowInfo::Type::Win32) + { Console.Error("Missing top level window, cannot add DInput devices."); + return false; + } settings_lock.lock(); + m_toplevel_window = static_cast(toplevel_wi->window_handle); + ReloadDevices(); return true; } @@ -110,6 +113,56 @@ void DInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock*>(pvRef)->push_back(*lpddi); + return DIENUM_CONTINUE; +} + +bool DInputSource::ReloadDevices() +{ + // detect any removals + PollEvents(); + + // look for new devices + std::vector devices; + m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY); + + DevCon.WriteLn("Enumerated %zu devices", devices.size()); + + bool changed = false; + for (DIDEVICEINSTANCEW inst : devices) + { + // do we already have this one? + if (std::any_of( + m_controllers.begin(), m_controllers.end(), [&inst](const ControllerData& cd) { return inst.guidInstance == cd.guid; })) + { + // yup, so skip it + continue; + } + + ControllerData cd; + cd.guid = inst.guidInstance; + HRESULT hr = m_dinput->CreateDevice(inst.guidInstance, cd.device.put(), nullptr); + if (FAILED(hr)) + { + Console.Warning("Failed to create instance of device [%s, %s]", inst.tszProductName, inst.tszInstanceName); + continue; + } + + const std::string name(StringUtil::WideStringToUTF8String(inst.tszProductName)); + if (AddDevice(cd, name)) + { + const u32 index = static_cast(m_controllers.size()); + m_controllers.push_back(std::move(cd)); + Host::OnInputDeviceConnected(GetDeviceIdentifier(index), name); + changed = true; + } + } + + return changed; +} + void DInputSource::Shutdown() { while (!m_controllers.empty()) @@ -119,45 +172,12 @@ void DInputSource::Shutdown() } } -static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCEW lpddi, LPVOID pvRef) +bool DInputSource::AddDevice(ControllerData& cd, const std::string& name) { - static_cast*>(pvRef)->push_back(*lpddi); - return DIENUM_CONTINUE; -} - -void DInputSource::AddDevices(HWND toplevel_window) -{ - std::vector devices; - m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY); - - DevCon.WriteLn("Enumerated %zu devices", devices.size()); - - for (DIDEVICEINSTANCEW inst : devices) - { - ControllerData cd; - HRESULT hr = m_dinput->CreateDevice(inst.guidInstance, cd.device.GetAddressOf(), nullptr); - if (FAILED(hr)) - { - Console.Warning("Failed to create instance of device [%s, %s]", inst.tszProductName, inst.tszInstanceName); - continue; - } - - const std::string name(StringUtil::WideStringToUTF8String(inst.tszProductName)); - if (AddDevice(cd, toplevel_window, name)) - { - const u32 index = static_cast(m_controllers.size()); - m_controllers.push_back(std::move(cd)); - Host::OnInputDeviceConnected(GetDeviceIdentifier(index), name); - } - } -} - -bool DInputSource::AddDevice(ControllerData& cd, HWND toplevel_window, const std::string& name) -{ - HRESULT hr = cd.device->SetCooperativeLevel(toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE); + HRESULT hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE); if (FAILED(hr)) { - hr = cd.device->SetCooperativeLevel(toplevel_window, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE); + hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE); if (FAILED(hr)) { Console.Error("Failed to set cooperative level for '%s'", name.c_str()); diff --git a/pcsx2/Frontend/DInputSource.h b/pcsx2/Frontend/DInputSource.h index b5bd42a05c..1fb1415a01 100644 --- a/pcsx2/Frontend/DInputSource.h +++ b/pcsx2/Frontend/DInputSource.h @@ -22,7 +22,7 @@ #include #include #include -#include +#include class DInputSource final : public InputSource { @@ -46,6 +46,7 @@ public: bool Initialize(SettingsInterface& si, std::unique_lock& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) override; + bool ReloadDevices() override; void Shutdown() override; void PollEvents() override; @@ -59,13 +60,11 @@ public: std::string ConvertKeyToString(InputBindingKey key) override; private: - template - using ComPtr = Microsoft::WRL::ComPtr; - struct ControllerData { - ComPtr device; + wil::com_ptr_nothrow device; DIJOYSTATE last_state = {}; + GUID guid = {}; std::vector axis_offsets; u32 num_buttons = 0; @@ -80,14 +79,14 @@ private: static std::array GetHatButtons(DWORD hat); static std::string GetDeviceIdentifier(u32 index); - void AddDevices(HWND toplevel_window); - bool AddDevice(ControllerData& cd, HWND toplevel_window, const std::string& name); + bool AddDevice(ControllerData& cd, const std::string& name); void CheckForStateChanges(size_t index, const DIJOYSTATE& new_state); ControllerDataArray m_controllers; HMODULE m_dinput_module{}; + wil::com_ptr_nothrow m_dinput; LPCDIDATAFORMAT m_joystick_data_format{}; - ComPtr m_dinput; + HWND m_toplevel_window = NULL; }; diff --git a/pcsx2/Frontend/InputManager.cpp b/pcsx2/Frontend/InputManager.cpp index b4289f11c0..aae34b4b4e 100644 --- a/pcsx2/Frontend/InputManager.cpp +++ b/pcsx2/Frontend/InputManager.cpp @@ -1024,6 +1024,19 @@ void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& bind // Source Management // ------------------------------------------------------------------------ +bool InputManager::ReloadDevices() +{ + bool changed = false; + + for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++) + { + if (s_input_sources[i]) + changed |= s_input_sources[i]->ReloadDevices(); + } + + return changed; +} + void InputManager::CloseSources() { for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++) diff --git a/pcsx2/Frontend/InputManager.h b/pcsx2/Frontend/InputManager.h index 4eb53d5c89..2e738c2d34 100644 --- a/pcsx2/Frontend/InputManager.h +++ b/pcsx2/Frontend/InputManager.h @@ -256,6 +256,10 @@ namespace InputManager /// Re-parses the sources part of the config and initializes any backends. void ReloadSources(SettingsInterface& si, std::unique_lock& settings_lock); + /// Called when a device change is triggered by the system (DBT_DEVNODES_CHANGED on Windows). + /// Returns true if any device changes are detected. + bool ReloadDevices(); + /// Shuts down any enabled input sources. void CloseSources(); diff --git a/pcsx2/Frontend/InputSource.h b/pcsx2/Frontend/InputSource.h index fee3ebe46c..3f9ada3fac 100644 --- a/pcsx2/Frontend/InputSource.h +++ b/pcsx2/Frontend/InputSource.h @@ -33,6 +33,7 @@ public: virtual bool Initialize(SettingsInterface& si, std::unique_lock& settings_lock) = 0; virtual void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) = 0; + virtual bool ReloadDevices() = 0; virtual void Shutdown() = 0; virtual void PollEvents() = 0; diff --git a/pcsx2/Frontend/SDLInputSource.cpp b/pcsx2/Frontend/SDLInputSource.cpp index 7246eaa427..00fa048956 100644 --- a/pcsx2/Frontend/SDLInputSource.cpp +++ b/pcsx2/Frontend/SDLInputSource.cpp @@ -129,6 +129,13 @@ void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) override; + bool ReloadDevices() override; void Shutdown() override; void PollEvents() override; diff --git a/pcsx2/Frontend/XInputSource.cpp b/pcsx2/Frontend/XInputSource.cpp index 0691c060c9..7d2819252d 100644 --- a/pcsx2/Frontend/XInputSource.cpp +++ b/pcsx2/Frontend/XInputSource.cpp @@ -132,6 +132,7 @@ bool XInputSource::Initialize(SettingsInterface& si, std::unique_lock& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) override; + bool ReloadDevices() override; void Shutdown() override; void PollEvents() override;