Skip to content

Commit

Permalink
InputManager: Add ForceFeedbackDevice interface
Browse files Browse the repository at this point in the history
  • Loading branch information
stenzek committed Dec 1, 2024
1 parent d7d028a commit f9c125c
Show file tree
Hide file tree
Showing 14 changed files with 263 additions and 9 deletions.
5 changes: 4 additions & 1 deletion src/core/fullscreen_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/core/input_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
3 changes: 2 additions & 1 deletion src/duckstation-qt/controllerbindingwidgets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
12 changes: 12 additions & 0 deletions src/util/dinput_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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];
Expand Down
3 changes: 3 additions & 0 deletions src/util/dinput_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ class DInputSource final : public InputSource
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>;
Expand Down
38 changes: 31 additions & 7 deletions src/util/input_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
// ------------------------------------------------------------------------
Expand Down Expand Up @@ -2104,3 +2124,7 @@ void InputManager::ReloadSources(const SettingsInterface& si, std::unique_lock<s

UpdatePointerCount();
}

ForceFeedbackDevice::~ForceFeedbackDevice()
{
}
20 changes: 20 additions & 0 deletions src/util/input_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "core/input_types.h"
#include "window_info.h"

class Error;
class SmallStringBase;

/// Class, or source of an input event.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions src/util/input_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
#include "common/types.h"
#include "input_manager.h"

class Error;
class SettingsInterface;

class ForceFeedbackDevice;

class InputSource
{
public:
Expand All @@ -29,6 +32,7 @@ class InputSource

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;
Expand All @@ -50,6 +54,9 @@ class InputSource
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);

Expand Down
129 changes: 129 additions & 0 deletions src/util/sdl_input_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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;
}
}
Loading

0 comments on commit f9c125c

Please sign in to comment.