Implement friction effect and make code more OO.

This commit is contained in:
Jordan Woyak
2026-04-12 16:22:23 -05:00
parent f032f507c3
commit 252c68ee8f
10 changed files with 379 additions and 65 deletions

View File

@@ -187,6 +187,28 @@ void FZeroAXDeluxe_IOAdapter::DoState(PointerWrap& p)
p.Do(m_rx_reply);
}
FZeroAXSteeringWheel::FZeroAXSteeringWheel()
: m_config_changed_callback_id{CPUThreadConfigCallback::AddConfigChangedCallback(
[this] { HandleConfigChange(); })},
// TODO: Is this safe ?
m_devices_changed_hook{
g_controller_interface.RegisterDevicesChangedCallback([this] { HandleConfigChange(); })}
{
}
FZeroAXSteeringWheel::~FZeroAXSteeringWheel()
{
CPUThreadConfigCallback::RemoveConfigChangedCallback(m_config_changed_callback_id);
}
void FZeroAXSteeringWheel::HandleConfigChange()
{
auto* const controller = Pad::GetConfig()->GetController(0);
const auto wheel_device = g_controller_interface.FindDevice(controller->GetDefaultDevice());
m_spring_effect = wheel_device->CreateSpringEffect();
}
void FZeroAXSteeringWheel::Update()
{
constexpr std::size_t REQUEST_SIZE = 4;
@@ -242,19 +264,17 @@ void FZeroAXSteeringWheel::ProcessRequest(std::span<const u8> request)
INFO_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: servo_position: {}", m_servo_position);
constexpr auto force_strength = 1.0;
if (m_spring_effect != nullptr)
{
constexpr auto force_strength = 1.0;
// TODO: Is this a sensible calculation ?
const auto center_position = ControllerEmu::MapToFloat<double>(s8(m_servo_position * 2), {});
// TODO: Is this a sensible calculation ?
const auto center_position = ControllerEmu::MapToFloat<double>(s8(m_servo_position * 2), {});
// TODO: Acquire this object in the constructor and update it on config change !
// TODO: Also turn off the force appropriately on shutdown !
auto* const controller = Pad::GetConfig()->GetController(0);
const auto wheel_device = g_controller_interface.FindDevice(controller->GetDefaultDevice());
// TODO: Should we also set a friction force ?
// TODO: Should we also set a friction force ?
wheel_device->SetCenteringForce(force_strength, center_position);
m_spring_effect->SetForce(force_strength, center_position);
}
break;
}

View File

@@ -3,9 +3,16 @@
#pragma once
#include "Common/HookableEvent.h"
#include "Core/CPUThreadConfigCallback.h"
#include "Core/HW/Triforce/IOPorts.h"
#include "Core/HW/Triforce/SerialDevice.h"
namespace ciface::Core
{
class SpringEffect;
}
namespace Triforce
{
@@ -13,6 +20,9 @@ namespace Triforce
class FZeroAXSteeringWheel final : public SerialDevice
{
public:
FZeroAXSteeringWheel();
~FZeroAXSteeringWheel() override;
void Update() override;
bool IsInitializing() const;
@@ -22,11 +32,18 @@ public:
void DoState(PointerWrap&) override;
private:
void HandleConfigChange();
void ProcessRequest(std::span<const u8>);
u8 m_init_state = 0;
s8 m_servo_position = 0;
std::unique_ptr<ciface::Core::SpringEffect> m_spring_effect;
const CPUThreadConfigCallback::ConfigChangedCallbackID m_config_changed_callback_id;
const Common::EventHook m_devices_changed_hook;
};
// Used for both FZeroAX and FZeroAXMonster.

View File

@@ -80,6 +80,28 @@ void MarioKartGPCommon_IOAdapter::HandleGenericOutputsChanged(std::span<const u8
}
}
MarioKartGPSteeringWheel::MarioKartGPSteeringWheel()
: m_config_changed_callback_id{CPUThreadConfigCallback::AddConfigChangedCallback(
[this] { HandleConfigChange(); })},
// TODO: Is this safe ?
m_devices_changed_hook{
g_controller_interface.RegisterDevicesChangedCallback([this] { HandleConfigChange(); })}
{
}
MarioKartGPSteeringWheel::~MarioKartGPSteeringWheel()
{
CPUThreadConfigCallback::RemoveConfigChangedCallback(m_config_changed_callback_id);
}
void MarioKartGPSteeringWheel::HandleConfigChange()
{
auto* const controller = Pad::GetConfig()->GetController(0);
const auto wheel_device = g_controller_interface.FindDevice(controller->GetDefaultDevice());
m_spring_effect = wheel_device->CreateSpringEffect();
m_friction_effect = wheel_device->CreateFrictionEffect();
}
void MarioKartGPSteeringWheel::Update()
{
constexpr std::size_t REQUEST_SIZE = 10;
@@ -123,18 +145,23 @@ void MarioKartGPSteeringWheel::ProcessRequest(std::span<const u8> request)
const bool should_set_force = centering_force != 0;
if (should_set_force)
{
// TODO: Are these sensible calculations ?
const auto force_strength = ControllerEmu::MapToFloat<double>(s16(centering_force), {});
const auto center_position = ControllerEmu::MapToFloat<double>(s16(roll), {});
if (m_spring_effect != nullptr)
{
// TODO: Are these sensible calculations ?
const auto force_strength = ControllerEmu::MapToFloat<double>(s16(centering_force), {});
const auto center_position = 0.0 - ControllerEmu::MapToFloat<double>(s16(roll), {});
// TODO: Acquire this object in the constructor and update it on config change !
// TODO: Also turn off the force appropriately on shutdown !
auto* const controller = Pad::GetConfig()->GetController(0);
const auto wheel_device = g_controller_interface.FindDevice(controller->GetDefaultDevice());
m_spring_effect->SetForce(force_strength, center_position);
}
wheel_device->SetCenteringForce(force_strength, center_position);
if (m_friction_effect != nullptr)
{
// TODO: sensible calculation ?
const auto force_strength = ControllerEmu::MapToFloat<double>(s16(friction_force), {});
m_friction_effect->SetForce(force_strength);
}
}
switch (m_init_state)
{
case 0:

View File

@@ -3,13 +3,16 @@
#pragma once
#include "Common/HookableEvent.h"
#include "Core/CPUThreadConfigCallback.h"
#include "Core/HW/Triforce/IOPorts.h"
#include "Core/HW/Triforce/SerialDevice.h"
namespace ciface::Core
{
class Device;
}
class SpringEffect;
class FrictionEffect;
} // namespace ciface::Core
namespace Triforce
{
@@ -18,6 +21,9 @@ namespace Triforce
class MarioKartGPSteeringWheel final : public SerialDevice
{
public:
MarioKartGPSteeringWheel();
~MarioKartGPSteeringWheel() override;
void Update() override;
void Reset();
@@ -25,9 +31,17 @@ public:
void DoState(PointerWrap&) override;
private:
void HandleConfigChange();
void ProcessRequest(std::span<const u8>);
u8 m_init_state = 0;
std::unique_ptr<ciface::Core::SpringEffect> m_spring_effect;
std::unique_ptr<ciface::Core::FrictionEffect> m_friction_effect;
const CPUThreadConfigCallback::ConfigChangedCallbackID m_config_changed_callback_id;
const Common::EventHook m_devices_changed_hook;
};
// Used for both MarioKartGP and MarioKartGP2.

View File

@@ -121,8 +121,14 @@ Device::Output* Device::FindOutput(std::string_view name) const
return nullptr;
}
void Device::SetCenteringForce(double gain, double center_position)
std::unique_ptr<SpringEffect> Device::CreateSpringEffect()
{
return nullptr;
}
std::unique_ptr<FrictionEffect> Device::CreateFrictionEffect()
{
return nullptr;
}
bool Device::Control::IsMatchingName(std::string_view name) const
@@ -508,4 +514,10 @@ auto InputDetector::TakeResults() -> Results
return std::move(m_detections);
}
SpringEffect::SpringEffect() = default;
SpringEffect::~SpringEffect() = default;
FrictionEffect::FrictionEffect() = default;
FrictionEffect::~FrictionEffect() = default;
} // namespace ciface::Core

View File

@@ -44,6 +44,35 @@ enum class DeviceRemoval
Keep,
};
// A "spring" force feedback effect used for wheel centering.
class SpringEffect
{
public:
SpringEffect();
virtual ~SpringEffect();
SpringEffect(const SpringEffect&) = delete;
SpringEffect(SpringEffect&&) = delete;
SpringEffect& operator=(const SpringEffect&) = delete;
SpringEffect& operator=(SpringEffect&&) = delete;
virtual void SetForce(double gain, double center_position) = 0;
};
class FrictionEffect
{
public:
FrictionEffect();
virtual ~FrictionEffect();
FrictionEffect(const FrictionEffect&) = delete;
FrictionEffect(FrictionEffect&&) = delete;
FrictionEffect& operator=(const FrictionEffect&) = delete;
FrictionEffect& operator=(FrictionEffect&&) = delete;
virtual void SetForce(double gain) = 0;
};
class Device
{
public:
@@ -160,7 +189,8 @@ public:
Input* FindInput(std::string_view name) const;
Output* FindOutput(std::string_view name) const;
virtual void SetCenteringForce(double gain, double center_position);
virtual std::unique_ptr<SpringEffect> CreateSpringEffect();
virtual std::unique_ptr<FrictionEffect> CreateFrictionEffect();
protected:
void AddInput(Input* const i);

View File

@@ -221,9 +221,6 @@ Gamepad::Gamepad(SDL_Gamepad* const gamepad, SDL_Joystick* const joystick)
AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Strong));
AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Weak));
}
// TODO: Remove test hacks
// SetCenteringForce(0.8, 0.7);
}
}
@@ -276,15 +273,27 @@ SDL_JoystickID Gamepad::GetSDLInstanceID() const
return SDL_GetJoystickID(m_joystick);
}
void Gamepad::SetCenteringForce(double gain, double center_position)
class SpringEffect final : public Core::SpringEffect
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "SetCenteringForce: {:.2} {:.2}", gain, center_position);
public:
explicit SpringEffect(SDL_Haptic* haptic) : m_haptic{haptic} {}
if (m_haptic == nullptr)
return;
~SpringEffect() override { SetForce(0.0, 0.0); }
if (gain != 0.0)
void SetForce(double gain, double center_position) override
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "SpringEffect: {:.2} {:.2}", gain, center_position);
if (gain == 0.0)
{
if (m_effect_id != -1)
{
SDL_DestroyHapticEffect(m_haptic, std::exchange(m_effect_id, -1));
}
return;
}
SDL_HapticEffect effect{
.type = SDL_HAPTIC_SPRING,
};
@@ -293,9 +302,8 @@ void Gamepad::SetCenteringForce(double gain, double center_position)
// TODO: Why does only SDL_HAPTIC_POLAR do the correct thing for my Sidewinder joystick?
// I would expect SDL_HAPTIC_STEERING_AXIS to be more reliable..
condition.direction.type = SDL_HAPTIC_POLAR;
// condition.direction.type = SDL_HAPTIC_STEERING_AXIS;
// condition.direction.dir[0] = 1;
// condition.direction.type = SDL_HAPTIC_POLAR;
condition.direction.type = SDL_HAPTIC_STEERING_AXIS;
// Is "infinity" always supported ?
condition.length = SDL_HAPTIC_INFINITY;
@@ -311,30 +319,116 @@ void Gamepad::SetCenteringForce(double gain, double center_position)
condition.center[0] = ControllerEmu::MapFloat<s16>(center_position, 0);
if (m_centering_haptic_effect == -1)
if (m_effect_id == -1)
{
// Create and start a new effect.
m_centering_haptic_effect = SDL_CreateHapticEffect(m_haptic, &effect);
if (m_centering_haptic_effect == -1)
m_effect_id = SDL_CreateHapticEffect(m_haptic, &effect);
if (m_effect_id == -1)
{
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_CreateHapticEffect: {}", SDL_GetError());
return;
}
if (!SDL_RunHapticEffect(m_haptic, m_centering_haptic_effect, 1))
if (!SDL_RunHapticEffect(m_haptic, m_effect_id, 1))
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_RunHapticEffect: {}", SDL_GetError());
}
else
{
// Update an already running effect.
if (!SDL_UpdateHapticEffect(m_haptic, m_centering_haptic_effect, &effect))
if (!SDL_UpdateHapticEffect(m_haptic, m_effect_id, &effect))
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_UpdateHapticEffect: {}", SDL_GetError());
}
}
else if (m_centering_haptic_effect != -1)
private:
SDL_Haptic* const m_haptic;
int m_effect_id = -1;
};
class FrictionEffect final : public Core::FrictionEffect
{
public:
explicit FrictionEffect(SDL_Haptic* haptic) : m_haptic{haptic} {}
~FrictionEffect() override { SetForce(0.0); }
void SetForce(double gain) override
{
SDL_DestroyHapticEffect(m_haptic, std::exchange(m_centering_haptic_effect, -1));
INFO_LOG_FMT(CONTROLLERINTERFACE, "FrictionEffect: {:.2}", gain);
if (gain == 0.0)
{
if (m_effect_id != -1)
{
SDL_DestroyHapticEffect(m_haptic, std::exchange(m_effect_id, -1));
}
return;
}
SDL_HapticEffect effect{
.type = SDL_HAPTIC_FRICTION,
};
auto& condition = effect.condition;
// TODO: Why does only SDL_HAPTIC_POLAR do the correct thing for my Sidewinder joystick?
// I would expect SDL_HAPTIC_STEERING_AXIS to be more reliable..
// condition.direction.type = SDL_HAPTIC_POLAR;
condition.direction.type = SDL_HAPTIC_STEERING_AXIS;
// Is "infinity" always supported ?
condition.length = SDL_HAPTIC_INFINITY;
const auto unsigned_gain = ControllerEmu::MapFloat<u16>(gain, 0);
condition.right_sat[0] = unsigned_gain;
condition.left_sat[0] = unsigned_gain;
// TODO: Is this a sensible coeff value ?
constexpr auto coeff = std::numeric_limits<s16>::max();
condition.right_coeff[0] = coeff;
condition.left_coeff[0] = coeff;
if (m_effect_id == -1)
{
// Create and start a new effect.
m_effect_id = SDL_CreateHapticEffect(m_haptic, &effect);
if (m_effect_id == -1)
{
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_CreateHapticEffect: {}", SDL_GetError());
return;
}
if (!SDL_RunHapticEffect(m_haptic, m_effect_id, 1))
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_RunHapticEffect: {}", SDL_GetError());
}
else
{
// Update an already running effect.
if (!SDL_UpdateHapticEffect(m_haptic, m_effect_id, &effect))
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_UpdateHapticEffect: {}", SDL_GetError());
}
}
private:
SDL_Haptic* const m_haptic;
int m_effect_id = -1;
};
std::unique_ptr<Core::SpringEffect> Gamepad::CreateSpringEffect()
{
if (m_haptic == nullptr)
return nullptr;
return std::make_unique<SpringEffect>(m_haptic);
}
std::unique_ptr<Core::FrictionEffect> Gamepad::CreateFrictionEffect()
{
if (m_haptic == nullptr)
return nullptr;
return std::make_unique<FrictionEffect>(m_haptic);
}
std::string Gamepad::Button::GetName() const

View File

@@ -342,7 +342,8 @@ public:
return Core::DeviceRemoval::Keep;
}
void SetCenteringForce(double gain, double center_position) override;
std::unique_ptr<Core::SpringEffect> CreateSpringEffect() override;
std::unique_ptr<Core::FrictionEffect> CreateFrictionEffect() override;
private:
void UpdateRumble()
@@ -371,8 +372,6 @@ private:
float m_touchpad_x = 0.f;
float m_touchpad_y = 0.f;
float m_touchpad_pressure = 0.f;
int m_centering_haptic_effect = -1;
};
struct SDLMotionAxis

View File

@@ -589,7 +589,7 @@ bool evdevDevice::AddNode(std::string devnode, int fd, libevdev* dev)
static_cast<void>(!write(fd, &ie, sizeof(ie)));
m_ffb_wheel_fd = fd;
m_ffb_fd = fd;
}
// Constant FF effect
@@ -729,18 +729,36 @@ bool evdevDevice::IsValid() const
return true;
}
void evdevDevice::SetCenteringForce(double gain, double center_position)
class SpringEffect final : public Core::SpringEffect
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "SetCenteringForce: {:.2} {:.2}", gain, center_position);
public:
explicit SpringEffect(int ffb_fd) : m_ffb_fd{ffb_fd} {}
if (m_ffb_wheel_fd == -1)
return;
~SpringEffect() override { SetForce(0.0, 0.0); }
if (gain != 0.0)
void SetForce(double gain, double center_position) override
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "SpringEffect: {:.2} {:.2}", gain, center_position);
if (gain == 0.0)
{
if (m_effect_id != -1)
{
if (ioctl(m_ffb_fd, EVIOCRMFF, m_effect_id) < 0)
ERROR_LOG_FMT(COMMON, "EVIOCRMFF: {}", Common::LastStrerrorString());
// TODO: Is it correct to not remove this effect on destruction.
// Is it enough to close the parent fd ?
m_effect_id = -1;
}
return;
}
ff_effect effect{
.type = FF_SPRING,
.id = m_centering_effect_id,
.id = m_effect_id,
};
// <linux/input.h>
@@ -760,36 +778,119 @@ void evdevDevice::SetCenteringForce(double gain, double center_position)
condition[0].center = ControllerEmu::MapFloat<s16>(center_position, 0);
if (ioctl(m_ffb_wheel_fd, EVIOCSFF, &effect) < 0)
if (ioctl(m_ffb_fd, EVIOCSFF, &effect) < 0)
{
ERROR_LOG_FMT(COMMON, "EVIOCSFF FF_SPRING: {}", Common::LastStrerrorString());
return;
}
if (m_centering_effect_id == -1)
if (m_effect_id == -1)
{
m_centering_effect_id = effect.id;
m_effect_id = effect.id;
const input_event play{
.type = EV_FF,
.code = u16(m_centering_effect_id),
.code = u16(m_effect_id),
.value = std::numeric_limits<s32>::max(),
};
if (write(m_ffb_wheel_fd, &play, sizeof(play)) < 0)
if (write(m_ffb_fd, &play, sizeof(play)) < 0)
ERROR_LOG_FMT(COMMON, "EV_FF: {}", Common::LastStrerrorString());
}
}
else if (m_centering_effect_id != -1)
private:
const int m_ffb_fd;
s16 m_effect_id = -1;
};
class FrictionEffect final : public Core::FrictionEffect
{
public:
explicit FrictionEffect(int ffb_fd) : m_ffb_fd{ffb_fd} {}
~FrictionEffect() override { SetForce(0.0); }
void SetForce(double gain) override
{
if (ioctl(m_ffb_wheel_fd, EVIOCRMFF, m_centering_effect_id) < 0)
ERROR_LOG_FMT(COMMON, "EVIOCRMFF: {}", Common::LastStrerrorString());
INFO_LOG_FMT(CONTROLLERINTERFACE, "FrictionEffect: {:.2}", gain);
// TODO: Is it correct to not remove this effect on destruction.
// Is it enough to close the parent fd ?
if (gain == 0.0)
{
if (m_effect_id != -1)
{
if (ioctl(m_ffb_fd, EVIOCRMFF, m_effect_id) < 0)
ERROR_LOG_FMT(COMMON, "EVIOCRMFF: {}", Common::LastStrerrorString());
m_centering_effect_id = -1;
// TODO: Is it correct to not remove this effect on destruction.
// Is it enough to close the parent fd ?
m_effect_id = -1;
}
return;
}
ff_effect effect{
.type = FF_FRICTION,
.id = m_effect_id,
};
// <linux/input.h>
// "Values above 32767 ms (0x7fff) should not be used and have unspecified results."
effect.replay.length = 0x7fff;
auto& condition = effect.u.condition;
const auto unsigned_gain = ControllerEmu::MapFloat<u16>(gain, 0);
condition[0].right_saturation = unsigned_gain;
condition[0].left_saturation = unsigned_gain;
// TODO: Is this a sensible coeff value ?
constexpr auto coeff = std::numeric_limits<s16>::max();
condition[0].right_coeff = coeff;
condition[0].left_coeff = coeff;
if (ioctl(m_ffb_fd, EVIOCSFF, &effect) < 0)
{
ERROR_LOG_FMT(COMMON, "EVIOCSFF FF_SPRING: {}", Common::LastStrerrorString());
return;
}
if (m_effect_id == -1)
{
m_effect_id = effect.id;
const input_event play{
.type = EV_FF,
.code = u16(m_effect_id),
.value = std::numeric_limits<s32>::max(),
};
if (write(m_ffb_fd, &play, sizeof(play)) < 0)
ERROR_LOG_FMT(COMMON, "EV_FF: {}", Common::LastStrerrorString());
}
}
private:
const int m_ffb_fd;
s16 m_effect_id = -1;
};
std::unique_ptr<Core::SpringEffect> evdevDevice::CreateSpringEffect()
{
if (m_ffb_fd == -1)
return nullptr;
return std::make_unique<SpringEffect>(m_ffb_fd);
}
std::unique_ptr<Core::FrictionEffect> evdevDevice::CreateFrictionEffect()
{
if (m_ffb_fd == -1)
return nullptr;
return std::make_unique<FrictionEffect>(m_ffb_fd);
}
evdevDevice::Effect::Effect(int fd) : m_fd(fd)

View File

@@ -88,7 +88,8 @@ public:
std::string GetName() const override { return m_name; }
std::string GetSource() const override { return "evdev"; }
void SetCenteringForce(double gain, double center_position) override;
std::unique_ptr<Core::SpringEffect> CreateSpringEffect() override;
std::unique_ptr<Core::FrictionEffect> CreateFrictionEffect() override;
private:
std::string m_name;
@@ -104,7 +105,6 @@ private:
InputBackend& m_input_backend;
int m_ffb_wheel_fd = -1;
s16 m_centering_effect_id = -1;
int m_ffb_fd = -1;
};
} // namespace ciface::evdev