From f032f507c3773c13391ebeab467d228faaeb2c48 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Tue, 24 Mar 2026 00:22:24 -0500 Subject: [PATCH] Triforce: FFB wheel connected to SDL and evdev. --- Source/Core/Core/HW/Triforce/FZeroAX.cpp | 20 +++++- Source/Core/Core/HW/Triforce/MarioKartGP.cpp | 24 ++++++- Source/Core/Core/HW/Triforce/MarioKartGP.h | 5 ++ .../ControllerInterface/CoreDevice.cpp | 4 ++ .../ControllerInterface/CoreDevice.h | 2 + .../ControllerInterface/SDL/SDLGamepad.cpp | 65 ++++++++++++++++++ .../ControllerInterface/SDL/SDLGamepad.h | 4 ++ .../ControllerInterface/evdev/evdev.cpp | 66 +++++++++++++++++++ .../ControllerInterface/evdev/evdev.h | 8 ++- 9 files changed, 194 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/HW/Triforce/FZeroAX.cpp b/Source/Core/Core/HW/Triforce/FZeroAX.cpp index dde106882c..0431d50a8c 100644 --- a/Source/Core/Core/HW/Triforce/FZeroAX.cpp +++ b/Source/Core/Core/HW/Triforce/FZeroAX.cpp @@ -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 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(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; } diff --git a/Source/Core/Core/HW/Triforce/MarioKartGP.cpp b/Source/Core/Core/HW/Triforce/MarioKartGP.cpp index 9b7d2cb0ef..174eca91ad 100644 --- a/Source/Core/Core/HW/Triforce/MarioKartGP.cpp +++ b/Source/Core/Core/HW/Triforce/MarioKartGP.cpp @@ -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 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(s16(centering_force), {}); + const auto center_position = ControllerEmu::MapToFloat(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) { diff --git a/Source/Core/Core/HW/Triforce/MarioKartGP.h b/Source/Core/Core/HW/Triforce/MarioKartGP.h index 163bfbb61b..dc72a3b769 100644 --- a/Source/Core/Core/HW/Triforce/MarioKartGP.h +++ b/Source/Core/Core/HW/Triforce/MarioKartGP.h @@ -6,6 +6,11 @@ #include "Core/HW/Triforce/IOPorts.h" #include "Core/HW/Triforce/SerialDevice.h" +namespace ciface::Core +{ +class Device; +} + namespace Triforce { diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp index a10edd13c9..d2d284af80 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp @@ -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; diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h index c8be0aeb65..52b10ab246 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h @@ -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); diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp index 09457598c6..0da36e619d 100644 --- a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp +++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp @@ -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(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::max(); + condition.right_coeff[0] = coeff; + condition.left_coeff[0] = coeff; + + condition.center[0] = ControllerEmu::MapFloat(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; diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h index 0a431866e7..34090f4a60 100644 --- a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h +++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h @@ -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 diff --git a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp index bb51d0026e..87482f4e0f 100644 --- a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp +++ b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp @@ -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(!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, + }; + + // + // "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(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::max(); + condition[0].right_coeff = coeff; + condition[0].left_coeff = coeff; + + condition[0].center = ControllerEmu::MapFloat(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::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; diff --git a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.h b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.h index f32567af8c..f569999101 100644 --- a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.h +++ b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.h @@ -3,10 +3,11 @@ #pragma once -#include #include #include +#include + #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 m_nodes; InputBackend& m_input_backend; + + int m_ffb_wheel_fd = -1; + s16 m_centering_effect_id = -1; }; } // namespace ciface::evdev