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.
This commit is contained in:
Connor McLaughlin 2022-10-15 21:20:05 +10:00 committed by refractionpcsx2
parent b9dffcb069
commit e8877daca3
13 changed files with 225 additions and 49 deletions

View File

@ -54,6 +54,11 @@
#include "svnrev.h"
#include "Tools/InputRecording/NewInputRecordingDlg.h"
#ifdef _WIN32
#include "common/RedtapeWindows.h"
#include <Dbt.h>
#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<HDEVNOTIFY>(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<const MSG*>(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<u32>(fullscreen), static_cast<u32>(render_to_main));

View File

@ -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;

View File

@ -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())

View File

@ -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();

View File

@ -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<std::mutex
}
HRESULT hr = create(
GetModuleHandleA(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W, reinterpret_cast<LPVOID*>(m_dinput.GetAddressOf()), nullptr);
GetModuleHandleA(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W, reinterpret_cast<LPVOID*>(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<std::mutex
// need to release the lock while we're enumerating, because we call winId().
settings_lock.unlock();
const std::optional<WindowInfo> toplevel_wi(Host::GetTopLevelWindowInfo());
if (toplevel_wi.has_value() && toplevel_wi->type == WindowInfo::Type::Win32)
AddDevices(static_cast<HWND>(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<HWND>(toplevel_wi->window_handle);
ReloadDevices();
return true;
}
@ -110,6 +113,56 @@ void DInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::m
// noop
}
static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCEW lpddi, LPVOID pvRef)
{
static_cast<std::vector<DIDEVICEINSTANCEW>*>(pvRef)->push_back(*lpddi);
return DIENUM_CONTINUE;
}
bool DInputSource::ReloadDevices()
{
// detect any removals
PollEvents();
// look for new devices
std::vector<DIDEVICEINSTANCEW> 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<u32>(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<std::vector<DIDEVICEINSTANCEW>*>(pvRef)->push_back(*lpddi);
return DIENUM_CONTINUE;
}
void DInputSource::AddDevices(HWND toplevel_window)
{
std::vector<DIDEVICEINSTANCEW> 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<u32>(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());

View File

@ -22,7 +22,7 @@
#include <functional>
#include <mutex>
#include <vector>
#include <wrl/client.h>
#include <wil/com.h>
class DInputSource final : public InputSource
{
@ -46,6 +46,7 @@ public:
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& 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 <typename T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
struct ControllerData
{
ComPtr<IDirectInputDevice8W> device;
wil::com_ptr_nothrow<IDirectInputDevice8W> device;
DIJOYSTATE last_state = {};
GUID guid = {};
std::vector<u32> axis_offsets;
u32 num_buttons = 0;
@ -80,14 +79,14 @@ private:
static std::array<bool, NUM_HAT_DIRECTIONS> 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<IDirectInput8W> m_dinput;
LPCDIDATAFORMAT m_joystick_data_format{};
ComPtr<IDirectInput8W> m_dinput;
HWND m_toplevel_window = NULL;
};

View File

@ -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++)

View File

@ -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<std::mutex>& 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();

View File

@ -33,6 +33,7 @@ public:
virtual bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) = 0;
virtual void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) = 0;
virtual bool ReloadDevices() = 0;
virtual void Shutdown() = 0;
virtual void PollEvents() = 0;

View File

@ -129,6 +129,13 @@ void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std:
}
}
bool SDLInputSource::ReloadDevices()
{
// We'll get a GC added/removed event here.
PollEvents();
return false;
}
void SDLInputSource::Shutdown()
{
ShutdownSubsystem();

View File

@ -31,6 +31,7 @@ public:
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
bool ReloadDevices() override;
void Shutdown() override;
void PollEvents() override;

View File

@ -132,6 +132,7 @@ bool XInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex
return false;
}
ReloadDevices();
return true;
}
@ -139,6 +140,38 @@ void XInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::m
{
}
bool XInputSource::ReloadDevices()
{
bool changed = false;
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
{
XINPUT_STATE new_state;
SCP_EXTN new_state_scp;
DWORD result = m_xinput_get_extended ? m_xinput_get_extended(i, &new_state_scp) : ERROR_NOT_SUPPORTED;
if (result != ERROR_SUCCESS)
result = m_xinput_get_state(i, &new_state);
if (result == ERROR_SUCCESS)
{
if (m_controllers[i].connected)
continue;
HandleControllerConnection(i);
changed = true;
}
else if (result == ERROR_DEVICE_NOT_CONNECTED)
{
if (!m_controllers[i].connected)
continue;
HandleControllerDisconnection(i);
changed = true;
}
}
return changed;
}
void XInputSource::Shutdown()
{
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
@ -164,6 +197,8 @@ void XInputSource::PollEvents()
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
{
const bool was_connected = m_controllers[i].connected;
if (!was_connected)
continue;
SCP_EXTN new_state_scp;
DWORD result = m_xinput_get_extended ? m_xinput_get_extended(i, &new_state_scp) : ERROR_NOT_SUPPORTED;

View File

@ -65,6 +65,7 @@ public:
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
bool ReloadDevices() override;
void Shutdown() override;
void PollEvents() override;