summaryrefslogtreecommitdiff
path: root/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp
diff options
context:
space:
mode:
authorLéo Lam <leo@leolam.fr>2021-06-07 12:15:15 +0200
committerGitHub <noreply@github.com>2021-06-07 12:15:15 +0200
commit8ca6ffd908aa95ffb5c7665083463c4ea1139b0d (patch)
tree5a058d8aa001d2577f7e65d5aa44ec86737c3386 /Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp
parentebe3fbe04c329f9bc100ded4dcf22720f375e2f4 (diff)
parent83ea16f40238fa82981221ba65061a7094b2a64b (diff)
Merge pull request #9702 from Filoppi/controller_interface_fixes
Controller Interface refactor
Diffstat (limited to 'Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp')
-rw-r--r--Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp230
1 files changed, 183 insertions, 47 deletions
diff --git a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp
index 0cae47c741..7c17a61448 100644
--- a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp
+++ b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp
@@ -6,6 +6,7 @@
#include <algorithm>
+#include "Common/Assert.h"
#include "Common/Logging/Log.h"
#include "Core/HW/WiimoteReal/WiimoteReal.h"
@@ -37,6 +38,10 @@
ControllerInterface g_controller_interface;
+// We need to save which input channel we are in by thread, so we can access the correct input
+// update values in different threads by input channel. We start from InputChannel::Host on all
+// threads as hotkeys are updated from a worker thread, but UI can read from the main thread. This
+// will never interfere with game threads.
static thread_local ciface::InputChannel tls_input_channel = ciface::InputChannel::Host;
void ControllerInterface::Initialize(const WindowSystemInfo& wsi)
@@ -44,12 +49,13 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi)
if (m_is_init)
return;
+ std::lock_guard lk_population(m_devices_population_mutex);
+
m_wsi = wsi;
- // Allow backends to add devices as soon as they are initialized.
- m_is_init = true;
+ m_populating_devices_counter = 1;
- m_is_populating_devices = true;
+ m_devices_mutex.lock();
#ifdef CIFACE_USE_WIN32
ciface::Win32::Init(wsi.render_window);
@@ -58,9 +64,7 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi)
// nothing needed
#endif
#ifdef CIFACE_USE_OSX
- if (m_wsi.type == WindowSystemType::MacOS)
- ciface::OSX::Init(wsi.render_window);
-// nothing needed for Quartz
+// nothing needed for OSX and Quartz
#endif
#ifdef CIFACE_USE_SDL
ciface::SDL::Init();
@@ -78,33 +82,95 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi)
ciface::DualShockUDPClient::Init();
#endif
+ // Don't allow backends to add devices before the first RefreshDevices() as they will be cleaned
+ // there. Or they'd end up waiting on the devices mutex if populated from another thread.
+ m_is_init = true;
+
RefreshDevices();
+
+ const bool devices_empty = m_devices.empty();
+
+ m_devices_mutex.unlock();
+
+ if (m_populating_devices_counter.fetch_sub(1) == 1 && !devices_empty)
+ InvokeDevicesChangedCallbacks();
}
-void ControllerInterface::ChangeWindow(void* hwnd)
+void ControllerInterface::ChangeWindow(void* hwnd, WindowChangeReason reason)
{
if (!m_is_init)
return;
// This shouldn't use render_surface so no need to update it.
m_wsi.render_window = hwnd;
- RefreshDevices();
+
+ // No need to re-add devices if this is an application exit request
+ if (reason == WindowChangeReason::Exit)
+ ClearDevices();
+ else
+ RefreshDevices(RefreshReason::WindowChangeOnly);
}
-void ControllerInterface::RefreshDevices()
+void ControllerInterface::RefreshDevices(RefreshReason reason)
{
if (!m_is_init)
return;
+#ifdef CIFACE_USE_OSX
+ if (m_wsi.type == WindowSystemType::MacOS)
{
- std::lock_guard lk(m_devices_mutex);
- m_devices.clear();
+ std::lock_guard lk_pre_population(m_pre_population_mutex);
+ // This is needed to stop its threads before locking our mutexes, to avoid deadlocks
+ // (in case it tried to add a device after we had locked m_devices_population_mutex).
+ // There doesn't seem to be an easy to way to repopulate OSX devices without restarting
+ // its hotplug thread. This will not release its devices, that's still done below.
+ ciface::OSX::DeInit();
}
+#endif
- m_is_populating_devices = true;
+ // This lock has two main functions:
+ // -Avoid a deadlock between m_devices_mutex and ControllerEmu::s_state_mutex when
+ // InvokeDevicesChangedCallbacks() is called concurrently by two different threads.
+ // -Avoid devices being destroyed while others of the same type are being created.
+ // This wasn't thread safe in multiple device sources.
+ std::lock_guard lk_population(m_devices_population_mutex);
+
+#if defined(CIFACE_USE_WIN32) && !defined(CIFACE_USE_XLIB) && !defined(CIFACE_USE_OSX)
+ // If only the window changed, avoid removing and re-adding all devices.
+ // Instead only refresh devices that require the window handle.
+ if (reason == RefreshReason::WindowChangeOnly)
+ {
+ m_populating_devices_counter.fetch_add(1);
+
+ {
+ std::lock_guard lk(m_devices_mutex);
+ // No need to do anything else in this case.
+ // Only (Win32) DInput needs the window handle to be updated.
+ ciface::Win32::ChangeWindow(m_wsi.render_window);
+ }
+
+ if (m_populating_devices_counter.fetch_sub(1) == 1)
+ InvokeDevicesChangedCallbacks();
+ return;
+ }
+#endif
+
+ m_populating_devices_counter.fetch_add(1);
+
+ // We lock m_devices_mutex here to make everything simpler.
+ // Multiple devices classes have their own "hotplug" thread, and can add/remove devices at any
+ // time, while actual writes to "m_devices" are safe, the order in which they happen is not. That
+ // means a thread could be adding devices while we are removing them, or removing them as we are
+ // populating them (causing missing or duplicate devices).
+ m_devices_mutex.lock();
// Make sure shared_ptr<Device> objects are released before repopulating.
- InvokeDevicesChangedCallbacks();
+ ClearDevices();
+
+ // Some of these calls won't immediately populate devices, but will do it async
+ // with their own PlatformPopulateDevices().
+ // This means that devices might end up in different order, unless we override their priority.
+ // It also means they might appear as "disconnected" in the Qt UI for a tiny bit of time.
#ifdef CIFACE_USE_WIN32
ciface::Win32::PopulateDevices(m_wsi.render_window);
@@ -116,7 +182,10 @@ void ControllerInterface::RefreshDevices()
#ifdef CIFACE_USE_OSX
if (m_wsi.type == WindowSystemType::MacOS)
{
- ciface::OSX::PopulateDevices(m_wsi.render_window);
+ {
+ std::lock_guard lk_pre_population(m_pre_population_mutex);
+ ciface::OSX::Init();
+ }
ciface::Quartz::PopulateDevices(m_wsi.render_window);
}
#endif
@@ -136,10 +205,12 @@ void ControllerInterface::RefreshDevices()
ciface::DualShockUDPClient::PopulateDevices();
#endif
- WiimoteReal::ProcessWiimotePool();
+ WiimoteReal::PopulateDevices();
- m_is_populating_devices = false;
- InvokeDevicesChangedCallbacks();
+ m_devices_mutex.unlock();
+
+ if (m_populating_devices_counter.fetch_sub(1) == 1)
+ InvokeDevicesChangedCallbacks();
}
void ControllerInterface::PlatformPopulateDevices(std::function<void()> callback)
@@ -147,12 +218,18 @@ void ControllerInterface::PlatformPopulateDevices(std::function<void()> callback
if (!m_is_init)
return;
- m_is_populating_devices = true;
+ std::lock_guard lk_population(m_devices_population_mutex);
- callback();
+ m_populating_devices_counter.fetch_add(1);
- m_is_populating_devices = false;
- InvokeDevicesChangedCallbacks();
+ {
+ std::lock_guard lk(m_devices_mutex);
+
+ callback();
+ }
+
+ if (m_populating_devices_counter.fetch_sub(1) == 1)
+ InvokeDevicesChangedCallbacks();
}
// Remove all devices and call library cleanup functions
@@ -163,23 +240,11 @@ void ControllerInterface::Shutdown()
// Prevent additional devices from being added during shutdown.
m_is_init = false;
+ // Additional safety measure to avoid InvokeDevicesChangedCallbacks()
+ m_populating_devices_counter = 1;
- {
- std::lock_guard lk(m_devices_mutex);
-
- for (const auto& d : m_devices)
- {
- // Set outputs to ZERO before destroying device
- for (ciface::Core::Device::Output* o : d->Outputs())
- o->SetState(0);
- }
-
- m_devices.clear();
- }
-
- // This will update control references so shared_ptr<Device>s are freed up
- // BEFORE we shutdown the backends.
- InvokeDevicesChangedCallbacks();
+ // Update control references so shared_ptr<Device>s are freed up BEFORE we shutdown the backends.
+ ClearDevices();
#ifdef CIFACE_USE_WIN32
ciface::Win32::DeInit();
@@ -203,13 +268,48 @@ void ControllerInterface::Shutdown()
#ifdef CIFACE_USE_DUALSHOCKUDPCLIENT
ciface::DualShockUDPClient::DeInit();
#endif
+
+ // Make sure no devices had been added within Shutdown() in the time
+ // between checking they checked atomic m_is_init bool and we changed it.
+ // We couldn't have locked m_devices_mutex nor m_devices_population_mutex for the whole Shutdown()
+ // as they could cause deadlocks. Note that this is still not 100% safe as some backends are
+ // shut down in other places, possibly adding devices after we have shut down, but the chances of
+ // that happening are basically zero.
+ ClearDevices();
+}
+
+void ControllerInterface::ClearDevices()
+{
+ std::lock_guard lk_population(m_devices_population_mutex);
+
+ {
+ std::lock_guard lk(m_devices_mutex);
+
+ if (m_devices.empty())
+ return;
+
+ for (const auto& d : m_devices)
+ {
+ // Set outputs to ZERO before destroying device
+ for (ciface::Core::Device::Output* o : d->Outputs())
+ o->SetState(0);
+ }
+
+ // Devices will still be alive after this: there are shared ptrs around the code holding them,
+ // but InvokeDevicesChangedCallbacks() will clean all of them.
+ m_devices.clear();
+ }
+
+ InvokeDevicesChangedCallbacks();
}
-void ControllerInterface::AddDevice(std::shared_ptr<ciface::Core::Device> device)
+bool ControllerInterface::AddDevice(std::shared_ptr<ciface::Core::Device> device)
{
// If we are shutdown (or in process of shutting down) ignore this request:
if (!m_is_init)
- return;
+ return false;
+
+ std::lock_guard lk_population(m_devices_population_mutex);
{
std::lock_guard lk(m_devices_mutex);
@@ -239,14 +339,36 @@ void ControllerInterface::AddDevice(std::shared_ptr<ciface::Core::Device> device
NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Added device: {}", device->GetQualifiedName());
m_devices.emplace_back(std::move(device));
+
+ // We can't (and don't want) to control the order in which devices are added, but we
+ // need their order to be consistent, and we need the same one to always be the first, where
+ // present (the keyboard and mouse device usually). This is because when defaulting a
+ // controller profile, it will automatically select the first device in the list as its default.
+ std::stable_sort(m_devices.begin(), m_devices.end(),
+ [](const std::shared_ptr<ciface::Core::Device>& a,
+ const std::shared_ptr<ciface::Core::Device>& b) {
+ // It would be nice to sort devices by Source then Name then ID but it's
+ // better to leave them sorted by the add order, which also avoids breaking
+ // the order on other platforms that are less tested.
+ return a->GetSortPriority() > b->GetSortPriority();
+ });
}
- if (!m_is_populating_devices)
+ if (!m_populating_devices_counter)
InvokeDevicesChangedCallbacks();
+ return true;
}
-void ControllerInterface::RemoveDevice(std::function<bool(const ciface::Core::Device*)> callback)
+void ControllerInterface::RemoveDevice(std::function<bool(const ciface::Core::Device*)> callback,
+ bool force_devices_release)
{
+ // If we are shutdown (or in process of shutting down) ignore this request:
+ if (!m_is_init)
+ return;
+
+ std::lock_guard lk_population(m_devices_population_mutex);
+
+ bool any_removed;
{
std::lock_guard lk(m_devices_mutex);
auto it = std::remove_if(m_devices.begin(), m_devices.end(), [&callback](const auto& dev) {
@@ -257,22 +379,34 @@ void ControllerInterface::RemoveDevice(std::function<bool(const ciface::Core::De
}
return false;
});
+ const size_t prev_size = m_devices.size();
m_devices.erase(it, m_devices.end());
+ any_removed = m_devices.size() != prev_size;
}
- if (!m_is_populating_devices)
+ if (any_removed && (!m_populating_devices_counter || force_devices_release))
InvokeDevicesChangedCallbacks();
}
// Update input for all devices if lock can be acquired without waiting.
void ControllerInterface::UpdateInput()
{
- // Don't block the UI or CPU thread (to avoid a short but noticeable frame drop)
+ // This should never happen
+ ASSERT(m_is_init);
+ if (!m_is_init)
+ return;
+
+ // TODO: if we are an emulation input channel, we should probably always lock
+ // Prefer outdated values over blocking UI or CPU thread (avoids short but noticeable frame drop)
if (m_devices_mutex.try_lock())
{
std::lock_guard lk(m_devices_mutex, std::adopt_lock);
for (const auto& d : m_devices)
+ {
+ // Theoretically we could avoid updating input on devices that don't have any references to
+ // them, but in practice a few devices types could break in different ways, so we don't
d->UpdateInput();
+ }
}
}
@@ -307,7 +441,7 @@ Common::Vec2 ControllerInterface::GetWindowInputScale() const
ControllerInterface::HotplugCallbackHandle
ControllerInterface::RegisterDevicesChangedCallback(std::function<void()> callback)
{
- std::lock_guard<std::mutex> lk(m_callbacks_mutex);
+ std::lock_guard lk(m_callbacks_mutex);
m_devices_changed_callbacks.emplace_back(std::move(callback));
return std::prev(m_devices_changed_callbacks.end());
}
@@ -315,14 +449,16 @@ ControllerInterface::RegisterDevicesChangedCallback(std::function<void()> callba
// Unregister a device callback.
void ControllerInterface::UnregisterDevicesChangedCallback(const HotplugCallbackHandle& handle)
{
- std::lock_guard<std::mutex> lk(m_callbacks_mutex);
+ std::lock_guard lk(m_callbacks_mutex);
m_devices_changed_callbacks.erase(handle);
}
// Invoke all callbacks that were registered
void ControllerInterface::InvokeDevicesChangedCallbacks() const
{
- std::lock_guard<std::mutex> lk(m_callbacks_mutex);
- for (const auto& callback : m_devices_changed_callbacks)
+ m_callbacks_mutex.lock();
+ const auto devices_changed_callbacks = m_devices_changed_callbacks;
+ m_callbacks_mutex.unlock();
+ for (const auto& callback : devices_changed_callbacks)
callback();
}