diff options
| author | Léo Lam <leo@leolam.fr> | 2021-06-07 12:15:15 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-06-07 12:15:15 +0200 |
| commit | 8ca6ffd908aa95ffb5c7665083463c4ea1139b0d (patch) | |
| tree | 5a058d8aa001d2577f7e65d5aa44ec86737c3386 /Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp | |
| parent | ebe3fbe04c329f9bc100ded4dcf22720f375e2f4 (diff) | |
| parent | 83ea16f40238fa82981221ba65061a7094b2a64b (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.cpp | 230 |
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(); } |
