diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index 77a99f247..ca8bf079b 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -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; diff --git a/src/core/input_types.h b/src/core/input_types.h index a4ec75d6c..0e805759e 100644 --- a/src/core/input_types.h +++ b/src/core/input_types.h @@ -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, }; diff --git a/src/duckstation-qt/controllerbindingwidgets.cpp b/src/duckstation-qt/controllerbindingwidgets.cpp index b0a79fd99..500956015 100644 --- a/src/duckstation-qt/controllerbindingwidgets.cpp +++ b/src/duckstation-qt/controllerbindingwidgets.cpp @@ -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) { diff --git a/src/util/dinput_source.cpp b/src/util/dinput_source.cpp index 0a3cba4f9..09289ca8d 100644 --- a/src/util/dinput_source.cpp +++ b/src/util/dinput_source.cpp @@ -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 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 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]; diff --git a/src/util/dinput_source.h b/src/util/dinput_source.h index f22dd8c73..458a6716e 100644 --- a/src/util/dinput_source.h +++ b/src/util/dinput_source.h @@ -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 ParseKeyString(std::string_view device, std::string_view binding) override; TinyString ConvertKeyToString(InputBindingKey key) override; TinyString ConvertKeyToIcon(InputBindingKey key) override; + std::unique_ptr CreateForceFeedbackDevice(std::string_view device, Error* error) override; + private: template using ComPtr = Microsoft::WRL::ComPtr; diff --git a/src/util/input_manager.cpp b/src/util/input_manager.cpp index 8253e50ae..b8e8e22ae 100644 --- a/src/util/input_manager.cpp +++ b/src/util/input_manager.cpp @@ -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 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 CreateForceFeedbackDevice(const std::string_view device, Error* error = nullptr); } // namespace InputManager namespace Host { diff --git a/src/util/input_source.h b/src/util/input_source.h index 29fcf3689..0c3e0d07c 100644 --- a/src/util/input_source.h +++ b/src/util/input_source.h @@ -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 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 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); diff --git a/src/util/sdl_input_source.cpp b/src/util/sdl_input_source.cpp index 7ae97599c..353441ff5 100644 --- a/src/util/sdl_input_source.cpp +++ b/src/util/sdl_input_source.cpp @@ -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> SDLInputSource::EnumerateDevice return ret; } +bool SDLInputSource::ContainsDevice(std::string_view device) const +{ + return device.starts_with("SDL-"); +} + std::optional 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::CreateSDLSource() { return std::make_unique(); } + +std::unique_ptr 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(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 +[[maybe_unused]] static u16 ClampU16(T val) +{ + return static_cast(std::clamp(val, 0, 65535)); +} + +template +[[maybe_unused]] static u16 ClampS16(T val) +{ + return static_cast(std::clamp(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; + } +} diff --git a/src/util/sdl_input_source.h b/src/util/sdl_input_source.h index 6f0940b90..6d7259284 100644 --- a/src/util/sdl_input_source.h +++ b/src/util/sdl_input_source.h @@ -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 ParseKeyString(std::string_view device, std::string_view binding) override; TinyString ConvertKeyToString(InputBindingKey key) override; TinyString ConvertKeyToIcon(InputBindingKey key) override; + std::unique_ptr 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; +}; diff --git a/src/util/win32_raw_input_source.cpp b/src/util/win32_raw_input_source.cpp index 30ce21844..e34a7fd71 100644 --- a/src/util/win32_raw_input_source.cpp +++ b/src/util/win32_raw_input_source.cpp @@ -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 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 Win32RawInputSource::CreateForceFeedbackDevice(std::string_view device, + Error* error) +{ + Error::SetStringView(error, "Not supported on this input source."); + return {}; +} + std::vector Win32RawInputSource::EnumerateMotors() { return {}; diff --git a/src/util/win32_raw_input_source.h b/src/util/win32_raw_input_source.h index bac1574f1..43f85bf1d 100644 --- a/src/util/win32_raw_input_source.h +++ b/src/util/win32_raw_input_source.h @@ -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 ParseKeyString(std::string_view device, std::string_view binding) override; TinyString ConvertKeyToString(InputBindingKey key) override; TinyString ConvertKeyToIcon(InputBindingKey key) override; + std::unique_ptr CreateForceFeedbackDevice(std::string_view device, Error* error) override; + private: struct MouseState { diff --git a/src/util/xinput_source.cpp b/src/util/xinput_source.cpp index 22a687a3d..ff766604f 100644 --- a/src/util/xinput_source.cpp +++ b/src/util/xinput_source.cpp @@ -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> XInputSource::EnumerateDevices( return ret; } +bool XInputSource::ContainsDevice(std::string_view device) const +{ + return device.starts_with("XInput-"); +} + std::optional 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 XInputSource::CreateForceFeedbackDevice(std::string_view device, Error* error) +{ + Error::SetStringView(error, "Not supported on this input source."); + return {}; +} + std::vector XInputSource::EnumerateMotors() { std::vector ret; diff --git a/src/util/xinput_source.h b/src/util/xinput_source.h index dd087d56e..1e1e558d2 100644 --- a/src/util/xinput_source.h +++ b/src/util/xinput_source.h @@ -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 ParseKeyString(std::string_view device, std::string_view binding) override; TinyString ConvertKeyToString(InputBindingKey key) override; TinyString ConvertKeyToIcon(InputBindingKey key) override; + std::unique_ptr CreateForceFeedbackDevice(std::string_view device, Error* error) override; + private: struct ControllerData {