InputManager: Add ForceFeedbackDevice interface

This commit is contained in:
Stenzek 2024-04-29 00:48:54 +10:00
parent d7d028ac5c
commit f9c125c1a1
No known key found for this signature in database
14 changed files with 263 additions and 9 deletions

View File

@ -1651,7 +1651,7 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, InputBindingIn
if (!visible)
return;
if (oneline)
if (oneline && type != InputBindingInfo::Type::Pointer && type != InputBindingInfo::Type::Device)
InputManager::PrettifyInputBinding(value);
if (show_type)
@ -1677,6 +1677,9 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, InputBindingIn
case InputBindingInfo::Type::Macro:
title.format(ICON_FA_PIZZA_SLICE " {}", display_name);
break;
case InputBindingInfo::Type::Device:
title.format(ICON_FA_GAMEPAD " {}", display_name);
break;
default:
title = display_name;
break;

View File

@ -17,6 +17,7 @@ struct InputBindingInfo
Motor,
Pointer, // Absolute pointer, does not receive any events, but is queryable.
RelativePointer, // Receive relative mouse movement events, bind_index is offset by the axis.
Device, // Used for special-purpose device selection, e.g. force feedback.
Macro,
};

View File

@ -405,7 +405,8 @@ void ControllerBindingWidget::createBindingWidgets(QWidget* parent)
for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
{
if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||
bi.type == InputBindingInfo::Type::Pointer || bi.type == InputBindingInfo::Type::RelativePointer)
bi.type == InputBindingInfo::Type::Pointer || bi.type == InputBindingInfo::Type::RelativePointer ||
bi.type == InputBindingInfo::Type::Device)
{
if (!axis_gbox)
{

View File

@ -8,6 +8,7 @@
#include "platform_misc.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/log.h"
#include "common/string_util.h"
@ -338,6 +339,11 @@ void DInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey s
// not supported
}
bool DInputSource::ContainsDevice(std::string_view device) const
{
return device.starts_with("DInput-");
}
std::optional<InputBindingKey> DInputSource::ParseKeyString(std::string_view device, std::string_view binding)
{
if (!device.starts_with("DInput-") || binding.empty())
@ -444,6 +450,12 @@ TinyString DInputSource::ConvertKeyToIcon(InputBindingKey key)
return {};
}
std::unique_ptr<ForceFeedbackDevice> DInputSource::CreateForceFeedbackDevice(std::string_view device, Error* error)
{
Error::SetStringView(error, "Not supported on this input source.");
return {};
}
void DInputSource::CheckForStateChanges(size_t index, const DIJOYSTATE& new_state)
{
ControllerData& cd = m_controllers[index];

View File

@ -46,10 +46,13 @@ public:
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
float small_intensity) override;
bool ContainsDevice(std::string_view device) const override;
std::optional<InputBindingKey> ParseKeyString(std::string_view device, std::string_view binding) override;
TinyString ConvertKeyToString(InputBindingKey key) override;
TinyString ConvertKeyToIcon(InputBindingKey key) override;
std::unique_ptr<ForceFeedbackDevice> CreateForceFeedbackDevice(std::string_view device, Error* error) override;
private:
template<typename T>
using ComPtr = Microsoft::WRL::ComPtr<T>;

View File

@ -2,17 +2,20 @@
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "input_manager.h"
#include "imgui_manager.h"
#include "input_source.h"
#include "core/controller.h"
#include "core/host.h"
#include "core/system.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/string_util.h"
#include "common/timer.h"
#include "core/controller.h"
#include "core/host.h"
#include "core/system.h"
#include "imgui_manager.h"
#include "input_source.h"
#include "IconsPromptFont.h"
@ -303,7 +306,8 @@ bool InputManager::ParseBindingAndGetSource(std::string_view binding, InputBindi
std::string InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key)
{
if (binding_type == InputBindingInfo::Type::Pointer || binding_type == InputBindingInfo::Type::RelativePointer)
if (binding_type == InputBindingInfo::Type::Pointer || binding_type == InputBindingInfo::Type::RelativePointer ||
binding_type == InputBindingInfo::Type::Device)
{
// pointer and device bindings don't have a data part
if (key.source_type == InputSourceType::Pointer)
@ -356,7 +360,8 @@ std::string InputManager::ConvertInputBindingKeysToString(InputBindingInfo::Type
const InputBindingKey* keys, size_t num_keys)
{
// can't have a chord of devices/pointers
if (binding_type == InputBindingInfo::Type::Pointer || binding_type == InputBindingInfo::Type::Pointer)
if (binding_type == InputBindingInfo::Type::Pointer || binding_type == InputBindingInfo::Type::RelativePointer ||
binding_type == InputBindingInfo::Type::Device)
{
// so only take the first
if (num_keys > 0)
@ -888,6 +893,8 @@ void InputManager::AddPadBindings(const SettingsInterface& si, const std::string
break;
case InputBindingInfo::Type::Pointer:
case InputBindingInfo::Type::Device:
// handled in device
break;
default:
@ -1583,6 +1590,19 @@ void InputManager::OnInputDeviceDisconnected(InputBindingKey key, std::string_vi
Host::OnInputDeviceDisconnected(key, identifier);
}
std::unique_ptr<ForceFeedbackDevice> InputManager::CreateForceFeedbackDevice(const std::string_view device,
Error* error)
{
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i] && s_input_sources[i]->ContainsDevice(device))
return s_input_sources[i]->CreateForceFeedbackDevice(device, error);
}
Error::SetStringFmt(error, "No input source matched device '{}'", device);
return {};
}
// ------------------------------------------------------------------------
// Vibration
// ------------------------------------------------------------------------
@ -2104,3 +2124,7 @@ void InputManager::ReloadSources(const SettingsInterface& si, std::unique_lock<s
UpdatePointerCount();
}
ForceFeedbackDevice::~ForceFeedbackDevice()
{
}

View File

@ -16,6 +16,7 @@
#include "core/input_types.h"
#include "window_info.h"
class Error;
class SmallStringBase;
/// Class, or source of an input event.
@ -170,6 +171,22 @@ enum class InputPointerAxis : u8
/// External input source class.
class InputSource;
/// Force feedback interface.
class ForceFeedbackDevice
{
public:
enum class Effect
{
Constant,
};
virtual ~ForceFeedbackDevice();
virtual void SetConstantForce(s32 level) = 0;
virtual void DisableForce(Effect force) = 0;
};
namespace InputManager {
/// Minimum interval between vibration updates when the effect is continuous.
static constexpr double VIBRATION_UPDATE_INTERVAL_SECONDS = 0.5; // 500ms
@ -360,6 +377,9 @@ void OnInputDeviceConnected(std::string_view identifier, std::string_view device
/// Called when an input device is disconnected.
void OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier);
/// Creates a force feedback device interface for the specified source and device.
std::unique_ptr<ForceFeedbackDevice> CreateForceFeedbackDevice(const std::string_view device, Error* error = nullptr);
} // namespace InputManager
namespace Host {

View File

@ -14,8 +14,11 @@
#include "common/types.h"
#include "input_manager.h"
class Error;
class SettingsInterface;
class ForceFeedbackDevice;
class InputSource
{
public:
@ -29,6 +32,7 @@ public:
virtual void PollEvents() = 0;
virtual bool ContainsDevice(std::string_view device) const = 0;
virtual std::optional<InputBindingKey> ParseKeyString(std::string_view device, std::string_view binding) = 0;
virtual TinyString ConvertKeyToString(InputBindingKey key) = 0;
virtual TinyString ConvertKeyToIcon(InputBindingKey key) = 0;
@ -50,6 +54,9 @@ public:
virtual void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
float small_intensity);
/// Creates a force-feedback device from this source.
virtual std::unique_ptr<ForceFeedbackDevice> CreateForceFeedbackDevice(std::string_view device, Error* error) = 0;
/// Creates a key for a generic controller axis event.
static InputBindingKey MakeGenericControllerAxisKey(InputSourceType clazz, u32 controller_index, s32 axis_index);

View File

@ -9,6 +9,7 @@
#include "common/assert.h"
#include "common/bitutils.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
@ -360,6 +361,11 @@ std::vector<std::pair<std::string, std::string>> SDLInputSource::EnumerateDevice
return ret;
}
bool SDLInputSource::ContainsDevice(std::string_view device) const
{
return device.starts_with("SDL-");
}
std::optional<InputBindingKey> SDLInputSource::ParseKeyString(std::string_view device, std::string_view binding)
{
if (!device.starts_with("SDL-") || binding.empty())
@ -1092,3 +1098,126 @@ std::unique_ptr<InputSource> InputSource::CreateSDLSource()
{
return std::make_unique<SDLInputSource>();
}
std::unique_ptr<ForceFeedbackDevice> SDLInputSource::CreateForceFeedbackDevice(std::string_view device, Error* error)
{
SDL_Joystick* joystick = GetJoystickForDevice(device);
if (!joystick)
{
Error::SetStringFmt(error, "No SDL_Joystick for {}", device);
return nullptr;
}
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
if (!haptic)
{
Error::SetStringFmt(error, "Haptic is not supported on {} ({})", device, SDL_JoystickName(joystick));
return nullptr;
}
return std::unique_ptr<SDLForceFeedbackDevice>(new SDLForceFeedbackDevice(joystick, haptic));
}
SDLForceFeedbackDevice::SDLForceFeedbackDevice(SDL_Joystick* joystick, SDL_Haptic* haptic) : m_haptic(haptic)
{
std::memset(&m_constant_effect, 0, sizeof(m_constant_effect));
}
SDLForceFeedbackDevice::~SDLForceFeedbackDevice()
{
if (m_haptic)
{
DestroyEffects();
SDL_HapticClose(m_haptic);
m_haptic = nullptr;
}
}
void SDLForceFeedbackDevice::CreateEffects(SDL_Joystick* joystick)
{
constexpr u32 length = 10000; // 10 seconds since NFS games seem to not issue new commands while rotating.
const unsigned int supported = SDL_HapticQuery(m_haptic);
if (supported & SDL_HAPTIC_CONSTANT)
{
m_constant_effect.type = SDL_HAPTIC_CONSTANT;
m_constant_effect.constant.direction.type = SDL_HAPTIC_STEERING_AXIS;
m_constant_effect.constant.length = length;
m_constant_effect_id = SDL_HapticNewEffect(m_haptic, &m_constant_effect);
if (m_constant_effect_id < 0)
ERROR_LOG("SDL_HapticNewEffect() for constant failed: {}", SDL_GetError());
}
else
{
WARNING_LOG("Constant effect is not supported on '{}'", SDL_JoystickName(joystick));
}
}
void SDLForceFeedbackDevice::DestroyEffects()
{
if (m_constant_effect_id >= 0)
{
if (m_constant_effect_running)
{
SDL_HapticStopEffect(m_haptic, m_constant_effect_id);
m_constant_effect_running = false;
}
SDL_HapticDestroyEffect(m_haptic, m_constant_effect_id);
m_constant_effect_id = -1;
}
}
template<typename T>
[[maybe_unused]] static u16 ClampU16(T val)
{
return static_cast<u16>(std::clamp<T>(val, 0, 65535));
}
template<typename T>
[[maybe_unused]] static u16 ClampS16(T val)
{
return static_cast<s16>(std::clamp<T>(val, -32768, 32767));
}
void SDLForceFeedbackDevice::SetConstantForce(s32 level)
{
if (m_constant_effect_id < 0)
return;
const s16 new_level = ClampS16(level);
if (m_constant_effect.constant.level != new_level)
{
m_constant_effect.constant.level = new_level;
if (SDL_HapticUpdateEffect(m_haptic, m_constant_effect_id, &m_constant_effect) != 0)
ERROR_LOG("SDL_HapticUpdateEffect() for constant failed: {}", SDL_GetError());
}
if (!m_constant_effect_running)
{
if (SDL_HapticRunEffect(m_haptic, m_constant_effect_id, SDL_HAPTIC_INFINITY) == 0)
m_constant_effect_running = true;
else
ERROR_LOG("SDL_HapticRunEffect() for constant failed: {}", SDL_GetError());
}
}
void SDLForceFeedbackDevice::DisableForce(Effect force)
{
switch (force)
{
case Effect::Constant:
{
if (m_constant_effect_running)
{
SDL_HapticStopEffect(m_haptic, m_constant_effect_id);
m_constant_effect_running = false;
}
}
break;
default:
break;
}
}

View File

@ -34,10 +34,13 @@ public:
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
float small_intensity) override;
bool ContainsDevice(std::string_view device) const override;
std::optional<InputBindingKey> ParseKeyString(std::string_view device, std::string_view binding) override;
TinyString ConvertKeyToString(InputBindingKey key) override;
TinyString ConvertKeyToIcon(InputBindingKey key) override;
std::unique_ptr<ForceFeedbackDevice> CreateForceFeedbackDevice(std::string_view device, Error* error) override;
bool ProcessSDLEvent(const SDL_Event* event);
SDL_Joystick* GetJoystickForDevice(std::string_view device);
@ -103,3 +106,23 @@ private:
bool m_enable_mfi_driver = false;
#endif
};
class SDLForceFeedbackDevice : public ForceFeedbackDevice
{
public:
SDLForceFeedbackDevice(SDL_Joystick* joystick, SDL_Haptic* haptic);
~SDLForceFeedbackDevice() override;
void SetConstantForce(s32 level) override;
void DisableForce(Effect force) override;
private:
void CreateEffects(SDL_Joystick* joystick);
void DestroyEffects();
SDL_Haptic* m_haptic = nullptr;
SDL_HapticEffect m_constant_effect;
int m_constant_effect_id = -1;
bool m_constant_effect_running = false;
};

View File

@ -5,6 +5,7 @@
#include "input_manager.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/log.h"
#include "common/string_util.h"
@ -90,6 +91,11 @@ void Win32RawInputSource::UpdateMotorState(InputBindingKey large_key, InputBindi
{
}
bool Win32RawInputSource::ContainsDevice(std::string_view device) const
{
return false;
}
std::optional<InputBindingKey> Win32RawInputSource::ParseKeyString(std::string_view device, std::string_view binding)
{
return std::nullopt;
@ -105,6 +111,13 @@ TinyString Win32RawInputSource::ConvertKeyToIcon(InputBindingKey key)
return {};
}
std::unique_ptr<ForceFeedbackDevice> Win32RawInputSource::CreateForceFeedbackDevice(std::string_view device,
Error* error)
{
Error::SetStringView(error, "Not supported on this input source.");
return {};
}
std::vector<InputBindingKey> Win32RawInputSource::EnumerateMotors()
{
return {};

View File

@ -30,10 +30,13 @@ public:
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
float small_intensity) override;
bool ContainsDevice(std::string_view device) const override;
std::optional<InputBindingKey> ParseKeyString(std::string_view device, std::string_view binding) override;
TinyString ConvertKeyToString(InputBindingKey key) override;
TinyString ConvertKeyToIcon(InputBindingKey key) override;
std::unique_ptr<ForceFeedbackDevice> CreateForceFeedbackDevice(std::string_view device, Error* error) override;
private:
struct MouseState
{

View File

@ -5,6 +5,7 @@
#include "input_manager.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/log.h"
#include "common/string_util.h"
@ -251,6 +252,11 @@ std::vector<std::pair<std::string, std::string>> XInputSource::EnumerateDevices(
return ret;
}
bool XInputSource::ContainsDevice(std::string_view device) const
{
return device.starts_with("XInput-");
}
std::optional<InputBindingKey> XInputSource::ParseKeyString(std::string_view device, std::string_view binding)
{
if (!device.starts_with("XInput-") || binding.empty())
@ -364,6 +370,12 @@ TinyString XInputSource::ConvertKeyToIcon(InputBindingKey key)
return ret;
}
std::unique_ptr<ForceFeedbackDevice> XInputSource::CreateForceFeedbackDevice(std::string_view device, Error* error)
{
Error::SetStringView(error, "Not supported on this input source.");
return {};
}
std::vector<InputBindingKey> XInputSource::EnumerateMotors()
{
std::vector<InputBindingKey> ret;

View File

@ -48,10 +48,13 @@ public:
void UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
float small_intensity) override;
bool ContainsDevice(std::string_view device) const override;
std::optional<InputBindingKey> ParseKeyString(std::string_view device, std::string_view binding) override;
TinyString ConvertKeyToString(InputBindingKey key) override;
TinyString ConvertKeyToIcon(InputBindingKey key) override;
std::unique_ptr<ForceFeedbackDevice> CreateForceFeedbackDevice(std::string_view device, Error* error) override;
private:
struct ControllerData
{