R c5d2ad99d2 Support Steam Deck controls directly through hidraw
This reads Steam Deck controls bypassing Steam Input. This allows for access to
motion controls as well as independent access to thumb sticks, trackpads, and
back grip buttons.
2023-05-10 13:28:32 +01:00

467 lines
15 KiB
C++

// Copyright 2010 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include <algorithm>
#include "Common/Assert.h"
#include "Common/Logging/Log.h"
#include "Core/HW/WiimoteReal/WiimoteReal.h"
#ifdef CIFACE_USE_WIN32
#include "InputCommon/ControllerInterface/Win32/Win32.h"
#endif
#ifdef CIFACE_USE_XLIB
#include "InputCommon/ControllerInterface/Xlib/XInput2.h"
#endif
#ifdef CIFACE_USE_OSX
#include "InputCommon/ControllerInterface/OSX/OSX.h"
#include "InputCommon/ControllerInterface/Quartz/Quartz.h"
#endif
#ifdef CIFACE_USE_SDL
#include "InputCommon/ControllerInterface/SDL/SDL.h"
#endif
#ifdef CIFACE_USE_ANDROID
#include "InputCommon/ControllerInterface/Android/Android.h"
#endif
#ifdef CIFACE_USE_EVDEV
#include "InputCommon/ControllerInterface/evdev/evdev.h"
#endif
#ifdef CIFACE_USE_PIPES
#include "InputCommon/ControllerInterface/Pipes/Pipes.h"
#endif
#ifdef CIFACE_USE_DUALSHOCKUDPCLIENT
#include "InputCommon/ControllerInterface/DualShockUDPClient/DualShockUDPClient.h"
#endif
#ifdef CIFACE_USE_STEAMDECK
#include "InputCommon/ControllerInterface/SteamDeck/SteamDeck.h"
#endif
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)
{
if (m_is_init)
return;
std::lock_guard lk_population(m_devices_population_mutex);
m_wsi = wsi;
m_populating_devices_counter = 1;
#ifdef CIFACE_USE_WIN32
ciface::Win32::Init(wsi.render_window);
#endif
#ifdef CIFACE_USE_XLIB
// nothing needed
#endif
#ifdef CIFACE_USE_OSX
// nothing needed for OSX and Quartz
#endif
#ifdef CIFACE_USE_SDL
m_input_backends.emplace_back(ciface::SDL::CreateInputBackend(this));
#endif
#ifdef CIFACE_USE_ANDROID
ciface::Android::Init();
#endif
#ifdef CIFACE_USE_EVDEV
m_input_backends.emplace_back(ciface::evdev::CreateInputBackend(this));
#endif
#ifdef CIFACE_USE_PIPES
// nothing needed
#endif
#ifdef CIFACE_USE_DUALSHOCKUDPCLIENT
m_input_backends.emplace_back(ciface::DualShockUDPClient::CreateInputBackend(this));
#endif
#ifdef CIFACE_USE_STEAMDECK
m_input_backends.emplace_back(ciface::SteamDeck::CreateInputBackend(this));
#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();
// Devices writes are already protected by m_devices_population_mutex but this won't hurt
m_devices_mutex.lock();
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, WindowChangeReason reason)
{
if (!m_is_init)
return;
// This shouldn't use render_surface so no need to update it.
m_wsi.render_window = hwnd;
// 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(RefreshReason reason)
{
if (!m_is_init)
return;
#ifdef CIFACE_USE_OSX
if (m_wsi.type == WindowSystemType::MacOS)
{
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 should not remove its devices, and if it did, calls should be ignored.
ciface::OSX::DeInit();
}
#endif
// We lock m_devices_population_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).
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);
// 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);
// Make sure shared_ptr<Device> objects are released before repopulating.
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.
// This helps the emulation and host thread to not stall when repopulating devices for any reason.
// Every platform that adds a device that is meant to be used as default device should try to not
// do it async, to not risk the emulated controllers default config loading not finding a default
// device.
#ifdef CIFACE_USE_WIN32
ciface::Win32::PopulateDevices(m_wsi.render_window);
#endif
#ifdef CIFACE_USE_XLIB
if (m_wsi.type == WindowSystemType::X11)
ciface::XInput2::PopulateDevices(m_wsi.render_window);
#endif
#ifdef CIFACE_USE_OSX
if (m_wsi.type == WindowSystemType::MacOS)
{
{
std::lock_guard lk_pre_population(m_pre_population_mutex);
ciface::OSX::Init();
}
ciface::Quartz::PopulateDevices(m_wsi.render_window);
}
#endif
#ifdef CIFACE_USE_ANDROID
ciface::Android::PopulateDevices();
#endif
#ifdef CIFACE_USE_PIPES
ciface::Pipes::PopulateDevices();
#endif
for (auto& backend : m_input_backends)
backend->PopulateDevices();
WiimoteReal::PopulateDevices();
if (m_populating_devices_counter.fetch_sub(1) == 1)
InvokeDevicesChangedCallbacks();
}
void ControllerInterface::PlatformPopulateDevices(std::function<void()> callback)
{
if (!m_is_init)
return;
std::lock_guard lk_population(m_devices_population_mutex);
m_populating_devices_counter.fetch_add(1);
callback();
if (m_populating_devices_counter.fetch_sub(1) == 1)
InvokeDevicesChangedCallbacks();
}
// Remove all devices and call library cleanup functions
void ControllerInterface::Shutdown()
{
if (!m_is_init)
return;
// Prevent additional devices from being added during shutdown.
m_is_init = false;
// Additional safety measure to avoid InvokeDevicesChangedCallbacks()
m_populating_devices_counter = 1;
// Update control references so shared_ptr<Device>s are freed up BEFORE we shutdown the backends.
ClearDevices();
#ifdef CIFACE_USE_WIN32
ciface::Win32::DeInit();
#endif
#ifdef CIFACE_USE_XLIB
// nothing needed
#endif
#ifdef CIFACE_USE_OSX
ciface::OSX::DeInit();
ciface::Quartz::DeInit();
#endif
#ifdef CIFACE_USE_ANDROID
ciface::Android::Shutdown();
#endif
// Empty the container of input backends to deconstruct and deinitialize them.
m_input_backends.clear();
// 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_population_mutex for the whole Shutdown()
// as it 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 could still be alive after this as there might be shared ptrs around holding them.
// The InvokeDevicesChangedCallbacks() underneath should always clean all of them (it needs to).
m_devices.clear();
}
InvokeDevicesChangedCallbacks();
}
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 false;
std::lock_guard lk_population(m_devices_population_mutex);
{
std::lock_guard lk(m_devices_mutex);
const auto is_id_in_use = [&device, this](int id) {
return std::any_of(m_devices.begin(), m_devices.end(), [&device, &id](const auto& d) {
return d->GetSource() == device->GetSource() && d->GetName() == device->GetName() &&
d->GetId() == id;
});
};
const auto preferred_id = device->GetPreferredId();
if (preferred_id.has_value() && !is_id_in_use(*preferred_id))
{
// Use the device's preferred ID if available.
device->SetId(*preferred_id);
}
else
{
// Find the first available ID to use.
int id = 0;
while (is_id_in_use(id))
++id;
device->SetId(id);
}
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_populating_devices_counter)
InvokeDevicesChangedCallbacks();
return true;
}
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) {
if (callback(dev.get()))
{
NOTICE_LOG_FMT(CONTROLLERINTERFACE, "Removed device: {}", dev->GetQualifiedName());
return true;
}
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 (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()
{
// 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)
// Lock this first to avoid deadlock with m_devices_mutex in certain cases (such as a Wii Remote
// getting disconnected)
if (!m_devices_population_mutex.try_lock())
return;
std::lock_guard population_lock(m_devices_population_mutex, std::adopt_lock);
if (!m_devices_mutex.try_lock())
return;
std::lock_guard lk(m_devices_mutex, std::adopt_lock);
for (auto& backend : m_input_backends)
backend->UpdateInput();
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();
}
}
void ControllerInterface::SetCurrentInputChannel(ciface::InputChannel input_channel)
{
tls_input_channel = input_channel;
}
ciface::InputChannel ControllerInterface::GetCurrentInputChannel()
{
return tls_input_channel;
}
void ControllerInterface::SetAspectRatioAdjustment(float value)
{
m_aspect_ratio_adjustment = value;
}
Common::Vec2 ControllerInterface::GetWindowInputScale() const
{
const auto ar = m_aspect_ratio_adjustment.load();
if (ar > 1)
return {1.f, ar};
else
return {1 / ar, 1.f};
}
void ControllerInterface::SetMouseCenteringRequested(bool center)
{
m_requested_mouse_centering = center;
}
bool ControllerInterface::IsMouseCenteringRequested() const
{
return m_requested_mouse_centering.load();
}
// Register a callback to be called when a device is added or removed (as from the input backends'
// hotplug thread), or when devices are refreshed
// Returns a handle for later removing the callback.
ControllerInterface::HotplugCallbackHandle
ControllerInterface::RegisterDevicesChangedCallback(std::function<void()> callback)
{
std::lock_guard lk(m_callbacks_mutex);
m_devices_changed_callbacks.emplace_back(std::move(callback));
return std::prev(m_devices_changed_callbacks.end());
}
// Unregister a device callback.
void ControllerInterface::UnregisterDevicesChangedCallback(const HotplugCallbackHandle& handle)
{
std::lock_guard lk(m_callbacks_mutex);
m_devices_changed_callbacks.erase(handle);
}
// Invoke all callbacks that were registered
void ControllerInterface::InvokeDevicesChangedCallbacks() const
{
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();
}