Triforce: FFB wheel connected to SDL and evdev.

This commit is contained in:
Jordan Woyak
2026-03-24 00:22:24 -05:00
parent 8230647205
commit f032f507c3
9 changed files with 194 additions and 4 deletions

View File

@@ -14,7 +14,10 @@
#include "Core/HW/GCPad.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/GCPadStatus.h"
#include "InputCommon/InputConfig.h"
namespace Triforce
{
@@ -237,7 +240,22 @@ void FZeroAXSteeringWheel::ProcessRequest(std::span<const u8> request)
// This produces a value in the range around [-56, +56].
m_servo_position = s8(0x80 - (u8(request[1] << 7u) | request[2]));
DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: servo_position: {}", m_servo_position);
INFO_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: servo_position: {}", m_servo_position);
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: 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 ?
wheel_device->SetCenteringForce(force_strength, center_position);
break;
}

View File

@@ -12,7 +12,9 @@
#include "Core/HW/GCPad.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h"
#include "InputCommon/GCPadStatus.h"
#include "InputCommon/InputConfig.h"
namespace
{
@@ -108,12 +110,30 @@ void MarioKartGPSteeringWheel::ProcessRequest(std::span<const u8> request)
return;
}
// TODO: Handle friction !
const u16 centering_force = Common::swap16(request.data() + 4);
const u16 friction_force = Common::swap16(request.data() + 6);
const u16 roll = Common::swap16(request.data() + 8);
DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: FFB: {:04x} {:04x} {:04x}", centering_force,
friction_force, roll);
INFO_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: FFB: {:04x} {:04x} {:04x}", centering_force,
friction_force, roll);
// TODO: Is this sensible ?
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), {});
// 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());
wheel_device->SetCenteringForce(force_strength, center_position);
}
switch (m_init_state)
{

View File

@@ -6,6 +6,11 @@
#include "Core/HW/Triforce/IOPorts.h"
#include "Core/HW/Triforce/SerialDevice.h"
namespace ciface::Core
{
class Device;
}
namespace Triforce
{

View File

@@ -121,6 +121,10 @@ Device::Output* Device::FindOutput(std::string_view name) const
return nullptr;
}
void Device::SetCenteringForce(double gain, double center_position)
{
}
bool Device::Control::IsMatchingName(std::string_view name) const
{
return GetName() == name;

View File

@@ -160,6 +160,8 @@ public:
Input* FindInput(std::string_view name) const;
Output* FindOutput(std::string_view name) const;
virtual void SetCenteringForce(double gain, double center_position);
protected:
void AddInput(Input* const i);
void AddOutput(Output* const o);

View File

@@ -9,6 +9,7 @@
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h"
namespace ciface::SDL
{
@@ -220,6 +221,9 @@ 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);
}
}
@@ -272,6 +276,67 @@ SDL_JoystickID Gamepad::GetSDLInstanceID() const
return SDL_GetJoystickID(m_joystick);
}
void Gamepad::SetCenteringForce(double gain, double center_position)
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "SetCenteringForce: {:.2} {:.2}", gain, center_position);
if (m_haptic == nullptr)
return;
if (gain != 0.0)
{
SDL_HapticEffect effect{
.type = SDL_HAPTIC_SPRING,
};
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;
// condition.direction.dir[0] = 1;
// 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;
condition.center[0] = ControllerEmu::MapFloat<s16>(center_position, 0);
if (m_centering_haptic_effect == -1)
{
// Create and start a new effect.
m_centering_haptic_effect = SDL_CreateHapticEffect(m_haptic, &effect);
if (m_centering_haptic_effect == -1)
{
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_CreateHapticEffect: {}", SDL_GetError());
return;
}
if (!SDL_RunHapticEffect(m_haptic, m_centering_haptic_effect, 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))
ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL_UpdateHapticEffect: {}", SDL_GetError());
}
}
else if (m_centering_haptic_effect != -1)
{
SDL_DestroyHapticEffect(m_haptic, std::exchange(m_centering_haptic_effect, -1));
}
}
std::string Gamepad::Button::GetName() const
{
const auto button = m_binding.output.button;

View File

@@ -342,6 +342,8 @@ public:
return Core::DeviceRemoval::Keep;
}
void SetCenteringForce(double gain, double center_position) override;
private:
void UpdateRumble()
{
@@ -369,6 +371,8 @@ 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

@@ -22,6 +22,7 @@
#include "Common/StringUtil.h"
#include "Common/Thread.h"
#include "Common/WorkQueueThread.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
namespace ciface::evdev
@@ -587,6 +588,8 @@ bool evdevDevice::AddNode(std::string devnode, int fd, libevdev* dev)
ie.value = 0;
static_cast<void>(!write(fd, &ie, sizeof(ie)));
m_ffb_wheel_fd = fd;
}
// Constant FF effect
@@ -726,6 +729,69 @@ bool evdevDevice::IsValid() const
return true;
}
void evdevDevice::SetCenteringForce(double gain, double center_position)
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "SetCenteringForce: {:.2} {:.2}", gain, center_position);
if (m_ffb_wheel_fd == -1)
return;
if (gain != 0.0)
{
ff_effect effect{
.type = FF_SPRING,
.id = m_centering_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;
condition[0].center = ControllerEmu::MapFloat<s16>(center_position, 0);
if (ioctl(m_ffb_wheel_fd, EVIOCSFF, &effect) < 0)
{
ERROR_LOG_FMT(COMMON, "EVIOCSFF FF_SPRING: {}", Common::LastStrerrorString());
return;
}
if (m_centering_effect_id == -1)
{
m_centering_effect_id = effect.id;
const input_event play{
.type = EV_FF,
.code = u16(m_centering_effect_id),
.value = std::numeric_limits<s32>::max(),
};
if (write(m_ffb_wheel_fd, &play, sizeof(play)) < 0)
ERROR_LOG_FMT(COMMON, "EV_FF: {}", Common::LastStrerrorString());
}
}
else if (m_centering_effect_id != -1)
{
if (ioctl(m_ffb_wheel_fd, EVIOCRMFF, m_centering_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_centering_effect_id = -1;
}
}
evdevDevice::Effect::Effect(int fd) : m_fd(fd)
{
m_effect.id = -1;

View File

@@ -3,10 +3,11 @@
#pragma once
#include <libevdev/libevdev.h>
#include <string>
#include <vector>
#include <libevdev/libevdev.h>
#include "InputCommon/ControllerInterface/ControllerInterface.h"
namespace ciface::evdev
@@ -87,6 +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;
private:
std::string m_name;
@@ -100,5 +103,8 @@ private:
std::vector<Node> m_nodes;
InputBackend& m_input_backend;
int m_ffb_wheel_fd = -1;
s16 m_centering_effect_id = -1;
};
} // namespace ciface::evdev