From 60d85ff76b3bf5618185b569ca5ff46d534c0d2b Mon Sep 17 00:00:00 2001 From: Aron Novak Date: Mon, 12 Oct 2026 09:53:33 +0200 Subject: [PATCH 1/2] Add steering force feedback for wheels Wheels like the Logitech G29 only support constant force on Linux, so the existing SDL rumble never played on them. Synthesize a speed dependent centering spring, damper, drift countersteer pull, offroad bumps and emulated rumble through a single constant force effect. --- src/input/gamepad_config.cpp | 13 +- src/input/gamepad_config.hpp | 11 ++ src/input/input_manager.cpp | 3 + src/input/sdl_controller.cpp | 135 ++++++++++++++++-- src/input/sdl_controller.hpp | 28 ++++ .../controller/local_player_controller.cpp | 110 ++++++++++++-- .../controller/local_player_controller.hpp | 6 + 7 files changed, 283 insertions(+), 23 deletions(-) diff --git a/src/input/gamepad_config.cpp b/src/input/gamepad_config.cpp index 4733cff4b30..88e47e06797 100644 --- a/src/input/gamepad_config.cpp +++ b/src/input/gamepad_config.cpp @@ -30,6 +30,7 @@ #ifndef SERVER_ONLY #include "input/sdl_controller.hpp" +#include #include enum AxisWithDirection : unsigned @@ -65,6 +66,8 @@ GamepadConfig::GamepadConfig( const std::string &name, m_desensitize = false; m_use_force_feedback = true; m_auto_center = 20; + m_force_feedback_strength = 70; + m_force_feedback_invert = false; setDefaultBinds(); } // GamepadConfig @@ -79,6 +82,8 @@ GamepadConfig::GamepadConfig() : DeviceConfig() m_desensitize = false; m_use_force_feedback = true; m_auto_center = 20; + m_force_feedback_strength = 70; + m_force_feedback_invert = false; setDefaultBinds(); } // GamepadConfig @@ -93,6 +98,10 @@ bool GamepadConfig::load(const XMLNode *config) config->get("desensitize", &m_desensitize ); config->get("force-feedback", &m_use_force_feedback); config->get("auto-center", &m_auto_center); + config->get("force-feedback-strength", &m_force_feedback_strength); + config->get("force-feedback-invert", &m_force_feedback_invert); + m_force_feedback_strength = + std::max(0, std::min(100, m_force_feedback_strength)); bool ok = DeviceConfig::load(config); if(getName()=="") @@ -115,7 +124,9 @@ void GamepadConfig::save (std::ofstream& stream) << "\" deadzone=\"" << m_deadzone << "\" desensitize=\"" << m_desensitize << "\" auto-center=\"" << m_auto_center - << "\" force-feedback=\"" << m_use_force_feedback << "\" "; + << "\" force-feedback=\"" << m_use_force_feedback + << "\" force-feedback-strength=\"" << m_force_feedback_strength + << "\" force-feedback-invert=\"" << m_force_feedback_invert << "\" "; DeviceConfig::save(stream); stream << "\n\n"; } // save diff --git a/src/input/gamepad_config.hpp b/src/input/gamepad_config.hpp index d0b992fefab..e9e1121980b 100644 --- a/src/input/gamepad_config.hpp +++ b/src/input/gamepad_config.hpp @@ -62,6 +62,13 @@ class GamepadConfig : public DeviceConfig bool m_use_force_feedback; int m_auto_center; + /** Overall steering force in percent, 0-100. */ + int m_force_feedback_strength; + + /** Flips the steering force direction for wheels whose driver reports + * the axis the other way round. */ + bool m_force_feedback_invert; + std::map, int> m_sdl_mapping; std::set m_digital_axes; @@ -128,6 +135,10 @@ class GamepadConfig : public DeviceConfig // ------------------------------------------------------------------------ int getAutoCenterStrength() const { return m_auto_center; } // ------------------------------------------------------------------------ + int getForceFeedbackStrength() const { return m_force_feedback_strength; } + // ------------------------------------------------------------------------ + bool isForceFeedbackInverted() const { return m_force_feedback_invert; } + // ------------------------------------------------------------------------ void setAutoCenter(bool val) { m_auto_center = val; } }; // class GamepadConfig diff --git a/src/input/input_manager.cpp b/src/input/input_manager.cpp index 2ba106af78f..44400d5e630 100644 --- a/src/input/input_manager.cpp +++ b/src/input/input_manager.cpp @@ -271,7 +271,10 @@ void InputManager::update(float dt) #ifndef SERVER_ONLY for (auto& controller : m_sdl_controller) + { controller.second->checkPowerLevel(); + controller.second->updateForceFeedback(); + } #endif } diff --git a/src/input/sdl_controller.cpp b/src/input/sdl_controller.cpp index 19534d886c5..b2c9e085411 100644 --- a/src/input/sdl_controller.cpp +++ b/src/input/sdl_controller.cpp @@ -21,6 +21,7 @@ #include "guiengine/message_queue.hpp" #include "input/device_config.hpp" #include "input/device_manager.hpp" +#include "input/gamepad_config.hpp" #include "input/gamepad_device.hpp" #include "input/input_manager.hpp" #include "utils/log.hpp" @@ -29,6 +30,7 @@ #include "utils/translation.hpp" #include +#include #include #include @@ -44,6 +46,13 @@ SDLController::SDLController(int device_id) m_game_controller = NULL; m_joystick = NULL; m_haptic = NULL; + m_native_rumble = false; + m_steering_effect = -1; + m_steering_effect_running = false; + m_steering_force = 0.0f; + m_steering_force_time = 0; + m_rumble_strength = 0.0f; + m_rumble_end_time = 0; m_id = -1; if (SDL_IsGameController(device_id)) @@ -201,11 +210,32 @@ SDLController::SDLController(int device_id) finish: m_haptic = SDL_HapticOpenFromJoystick(m_joystick); - if (m_haptic) + if (!m_haptic) + return; + + m_native_rumble = SDL_HapticRumbleSupported(m_haptic) == SDL_TRUE && + SDL_HapticRumbleInit(m_haptic) == 0; + updateAutoCenter(getGamePadDevice()->getAutoCenterStrength()); + + if ((SDL_HapticQuery(m_haptic) & SDL_HAPTIC_CONSTANT) == 0) + return; + + SDL_HapticEffect effect = {}; + effect.type = SDL_HAPTIC_CONSTANT; + effect.constant.direction.type = SDL_HAPTIC_CARTESIAN; + effect.constant.direction.dir[0] = 1; + // The effect is stopped explicitly once the game stops feeding it, see + // updateForceFeedback(). + effect.constant.length = SDL_HAPTIC_INFINITY; + m_steering_effect = SDL_HapticNewEffect(m_haptic, &effect); + if (m_steering_effect < 0) { - SDL_HapticRumbleInit(m_haptic); - updateAutoCenter(getGamePadDevice()->getAutoCenterStrength()); + Log::warn("SDLController", "%s: cannot create constant force: %s", + name.c_str(), SDL_GetError()); + return; } + Log::info("SDLController", "%s supports steering force feedback.", + name.c_str()); } // SDLController // ---------------------------------------------------------------------------- @@ -217,7 +247,12 @@ SDLController::~SDLController() else SDL_JoystickClose(m_joystick); if (m_haptic) + { + stopSteeringEffect(); + if (m_steering_effect >= 0) + SDL_HapticDestroyEffect(m_haptic, m_steering_effect); SDL_HapticClose(m_haptic); + } m_gamepad->getConfiguration()->unPlugged(); m_gamepad->setIrrIndex(-1); m_gamepad->setConnected(false); @@ -261,26 +296,106 @@ void SDLController::checkPowerLevel() void SDLController::doRumble(float strength_low, float strength_high, uint32_t duration_ms) { - if (m_haptic) + float strength = std::min(1.0f, (strength_low + strength_high) / 2); + if (m_native_rumble) { - SDL_HapticRumblePlay(m_haptic, (strength_low + strength_high) / 2, duration_ms); + SDL_HapticRumblePlay(m_haptic, strength, duration_ms); + return; } - else + if (m_steering_effect >= 0) { + m_rumble_strength = strength; + m_rumble_end_time = StkTime::getMonoTimeMs() + duration_ms; + return; + } #if SDL_VERSION_ATLEAST(2,0,9) - uint16_t scaled_low = strength_low * pow(2, 16); - uint16_t scaled_high = strength_high * pow(2, 16); - SDL_GameControllerRumble(m_game_controller, scaled_low, scaled_high, duration_ms); + if (!m_game_controller) + return; + uint16_t scaled_low = std::min(1.0f, strength_low) * 65535; + uint16_t scaled_high = std::min(1.0f, strength_high) * 65535; + SDL_GameControllerRumble(m_game_controller, scaled_low, scaled_high, duration_ms); #endif - } } +// ---------------------------------------------------------------------------- void SDLController::updateAutoCenter(int state) { m_auto_center = state; SDL_HapticSetAutocenter(m_haptic, m_auto_center); } +// ---------------------------------------------------------------------------- +/** Sets the steering force, in [-1, 1] where positive pushes the wheel to the + * right. It has to be called continuously, the force fades out shortly after + * the game stops calling it (pause, race end, menus). + */ +void SDLController::setSteeringForce(float force) +{ + m_steering_force = std::max(-1.0f, std::min(1.0f, force)); + m_steering_force_time = StkTime::getMonoTimeMs(); +} // setSteeringForce + +// ---------------------------------------------------------------------------- +/** Sends the current steering force and emulated rumble to the device. Called + * once per frame, so the device is not flooded when several physics steps + * run in one frame. + */ +void SDLController::updateForceFeedback() +{ + if (m_steering_effect < 0) + return; + + const uint64_t now = StkTime::getMonoTimeMs(); + const bool steering = now < m_steering_force_time + 100; + const bool rumbling = now < m_rumble_end_time; + if (!steering && !rumbling) + { + stopSteeringEffect(); + return; + } + + float force = steering ? m_steering_force : 0.0f; + if (rumbling) + { + // Alternate the push direction quickly, the wheel motor can't do a + // real vibration. 50 ms per half wave stays readable at 60 fps. + force += ((now / 50) % 2 == 0 ? 1.0f : -1.0f) * m_rumble_strength * 0.6f; + } + + GamepadConfig* cfg = + static_cast(m_gamepad->getConfiguration()); + force *= cfg->getForceFeedbackStrength() / 100.0f; + if (cfg->isForceFeedbackInverted()) + force = -force; + force = std::max(-1.0f, std::min(1.0f, force)); + + SDL_HapticEffect effect = {}; + effect.type = SDL_HAPTIC_CONSTANT; + effect.constant.direction.type = SDL_HAPTIC_CARTESIAN; + effect.constant.direction.dir[0] = 1; + effect.constant.length = SDL_HAPTIC_INFINITY; + effect.constant.level = (Sint16)(force * 32767); + SDL_HapticUpdateEffect(m_haptic, m_steering_effect, &effect); + + if (m_steering_effect_running) + return; + // The game provides its own centering spring while driving, the device + // autocenter on top of it would make the wheel feel stiff and dead. + updateAutoCenter(0); + SDL_HapticRunEffect(m_haptic, m_steering_effect, 1); + m_steering_effect_running = true; +} // updateForceFeedback + +// ---------------------------------------------------------------------------- +void SDLController::stopSteeringEffect() +{ + if (!m_steering_effect_running) + return; + SDL_HapticStopEffect(m_haptic, m_steering_effect); + m_steering_effect_running = false; + updateAutoCenter(m_gamepad->getAutoCenterStrength()); +} // stopSteeringEffect + // ---------------------------------------------------------------------------- #ifdef ANDROID void SDLController::handleDirectScanCode(const SDL_Event& event) diff --git a/src/input/sdl_controller.hpp b/src/input/sdl_controller.hpp index dc3fb0c2566..86accaf7d2e 100644 --- a/src/input/sdl_controller.hpp +++ b/src/input/sdl_controller.hpp @@ -44,6 +44,27 @@ class SDLController SDL_Haptic* m_haptic; int m_auto_center; + /** True if the device can play SDL's simple rumble effect. Wheels such + * as the Logitech G29 on the mainline Linux driver only support + * constant force, so rumble has to be emulated with it. */ + bool m_native_rumble; + + /** Id of the constant force effect which carries all steering forces, + * -1 if the device has no constant force support. */ + int m_steering_effect; + + bool m_steering_effect_running; + + /** Last steering force requested by the game, in [-1, 1], positive + * pushes the wheel to the right. */ + float m_steering_force; + + uint64_t m_steering_force_time; + + float m_rumble_strength; + + uint64_t m_rumble_end_time; + int m_buttons; int m_axes; @@ -62,6 +83,7 @@ class SDLController #endif void updateAutoCenter(int state); + void stopSteeringEffect(); public: // ------------------------------------------------------------------------ SDLController(int device_id); @@ -162,6 +184,12 @@ class SDLController void checkPowerLevel(); // ------------------------------------------------------------------------ void doRumble(float strength_low, float strength_high, uint32_t duration_ms); + // ------------------------------------------------------------------------ + bool hasSteeringForce() const { return m_steering_effect >= 0; } + // ------------------------------------------------------------------------ + void setSteeringForce(float force); + // ------------------------------------------------------------------------ + void updateForceFeedback(); GamePadDevice* getGamePadDevice() const { return m_gamepad; } }; diff --git a/src/karts/controller/local_player_controller.cpp b/src/karts/controller/local_player_controller.cpp index cc0bda814c3..9cbcc613223 100644 --- a/src/karts/controller/local_player_controller.cpp +++ b/src/karts/controller/local_player_controller.cpp @@ -26,6 +26,7 @@ #include "graphics/camera/camera.hpp" #include "graphics/camera/camera_normal.hpp" #include "graphics/irr_driver.hpp" +#include "graphics/material.hpp" #include "graphics/particle_emitter.hpp" #include "graphics/particle_kind.hpp" #include "input/input_manager.hpp" @@ -70,6 +71,7 @@ LocalPlayerController::LocalPlayerController(AbstractKart *kart, : PlayerController(kart) { m_last_crash = 0; + m_ffb_prev_wheel = 0.0f; m_has_started = false; m_handicap = h; m_player = StateManager::get()->getActivePlayer(local_player_id); @@ -137,6 +139,7 @@ void LocalPlayerController::reset() { PlayerController::reset(); m_last_crash = 0; + m_ffb_prev_wheel = 0.0f; m_sound_schedule = false; m_has_started = false; } // reset @@ -301,6 +304,7 @@ void LocalPlayerController::update(int ticks) m_sound_schedule = false; m_kart->playSound(m_bzzt_sound); } + updateForceFeedback(ticks); } // update //----------------------------------------------------------------------------- @@ -397,7 +401,7 @@ void LocalPlayerController::handleZipper(bool play_sound) if (!GUIEngine::isNoGraphics()) irr_driver->giveBoost(m_camera_index); #endif - + rumble(0.3f, 0.3f, 150); } // handleZipper //----------------------------------------------------------------------------- @@ -489,25 +493,107 @@ void LocalPlayerController::doCrashHaptics() { #endif } -void LocalPlayerController::rumble(float strength_low, float strength_high, uint16_t duration) { +// ---------------------------------------------------------------------------- +/** Returns the gamepad driving this kart, or NULL if there is none or the + * player turned force feedback off for it. + */ +SDLController* LocalPlayerController::getForceFeedbackController() const +{ #ifndef SERVER_ONLY - if (RewindManager::get()->isRewinding()) - return; int count = input_manager->getGamepadCount(); - while(count--) + while (count--) { SDLController* controller = input_manager->getSDLController(count); - if (controller && controller->getGamePadDevice()->getPlayer() == m_player) - { - if (!controller->getGamePadDevice()->useForceFeedback()) - return; - controller->doRumble(strength_low, strength_high, duration); - break; - } + if (!controller || controller->getGamePadDevice()->getPlayer() != m_player) + continue; + if (!controller->getGamePadDevice()->useForceFeedback()) + return NULL; + return controller; } +#endif + return NULL; +} // getForceFeedbackController + +// ---------------------------------------------------------------------------- +void LocalPlayerController::rumble(float strength_low, float strength_high, uint16_t duration) { +#ifndef SERVER_ONLY + if (RewindManager::get()->isRewinding()) + return; + SDLController* controller = getForceFeedbackController(); + if (controller) + controller->doRumble(strength_low, strength_high, duration); #endif } +// ---------------------------------------------------------------------------- +/** Computes the steering force for wheels with constant force support. All + * forces are relative to full lock, positive pushes the wheel to the right. + */ +void LocalPlayerController::updateForceFeedback(int ticks) +{ +#ifndef SERVER_ONLY + if (RewindManager::get()->isRewinding()) + return; + SDLController* controller = getForceFeedbackController(); + if (!controller || !controller->hasSteeringForce()) + return; + + // The raw input is used instead of KartControl::getSteer(), which lags + // behind the wheel position and would make the spring feel spongy. + const float wheel = -m_steer_val / 32767.0f; + const float dt = stk_config->ticks2Time(ticks); + float wheel_speed = dt > 0.0f ? (wheel - m_ffb_prev_wheel) / dt : 0.0f; + wheel_speed = std::max(-5.0f, std::min(5.0f, wheel_speed)); + m_ffb_prev_wheel = wheel; + + const float max_speed = m_kart->getKartProperties()->getEngineMaxSpeed(); + const float speed = max_speed > 0.0f ? + std::min(1.0f, fabsf(m_kart->getSpeed()) / max_speed) : 0.0f; + + // Tyres only resist the wheel while they touch the ground. + float grip = 1.0f; + if (m_kart->getKartAnimation() || !m_kart->isOnGround()) + grip = 0.0f; + + const Skidding* skidding = m_kart->getSkidding(); + const Skidding::SkidState skid = skidding->getSkidState(); + float drift_pull = 0.0f; + if (skid == Skidding::SKID_ACCUMULATE_LEFT) + drift_pull = 1.0f; + else if (skid == Skidding::SKID_ACCUMULATE_RIGHT) + drift_pull = -1.0f; + // A drifting kart has less front grip, which makes the wheel lighter. + if (drift_pull != 0.0f) + grip *= 0.6f; + + // Even a stationary kart has some weight on the wheel, it gets heavier + // with speed like a real car. + const float spring = 0.15f + 0.55f * speed * grip; + const float damper = 0.02f + 0.06f * speed * grip; + float force = -spring * wheel - damper * wheel_speed; + + // The rear stepping out pulls the wheel into countersteer. + force += drift_pull * 0.2f * speed * grip; + + // Offroad materials (grass, sand, ...) slow the kart down, use that as a + // measure of how bumpy they are. + const Material* material = m_kart->getMaterial(); + if (grip > 0.0f && material && material->getMaxSpeedFraction() < 1.0f) + { + const float roughness = 1.0f - material->getMaxSpeedFraction(); + const float t = + stk_config->ticks2Time(World::getWorld()->getTicksSinceStart()); + // Two unrelated frequencies feel like an uneven surface rather than + // a motor buzz. + const float bumps = sinf(t * 2.0f * M_PI * 9.0f) + + 0.5f * sinf(t * 2.0f * M_PI * 23.0f); + force += bumps * roughness * speed * 0.25f; + } + + controller->setSteeringForce(force); +#endif +} // updateForceFeedback + void LocalPlayerController::crashed(const AbstractKart* k) { doCrashHaptics(); diff --git a/src/karts/controller/local_player_controller.hpp b/src/karts/controller/local_player_controller.hpp index ea346048a46..61433e6ffc4 100644 --- a/src/karts/controller/local_player_controller.hpp +++ b/src/karts/controller/local_player_controller.hpp @@ -28,6 +28,7 @@ class AbstractKart; class SFXBase; class SFXBuffer; class btTransform; +class SDLController; #ifndef SERVER_ONLY class ParticleEmitter; @@ -59,6 +60,9 @@ class LocalPlayerController : public PlayerController int m_last_crash; + /** Wheel position at the previous update, to damp fast wheel movement. */ + float m_ffb_prev_wheel; + HandicapLevel m_handicap; SFXBase *m_wee_sound; @@ -74,6 +78,8 @@ class LocalPlayerController : public PlayerController void nitroNotFullSound(); void doCrashHaptics(); + void updateForceFeedback(int ticks); + SDLController* getForceFeedbackController() const; void setParticleEmitterPosition(const btTransform& t); public: From 1991552718143d8eb2db99163c344178d50558ce Mon Sep 17 00:00:00 2001 From: Aron Novak Date: Mon, 12 Oct 2026 10:02:33 +0200 Subject: [PATCH 2/2] Compute steering spring and damper on the device side Fix the constant force direction, read the raw steering axis so the deadzone and axis binding don't affect the spring, compute the wheel speed from the real frame time, cap device uploads at 400 Hz and skip unchanged levels, and close the haptic device before the joystick. --- src/input/sdl_controller.cpp | 135 ++++++++++++++---- src/input/sdl_controller.hpp | 22 ++- .../controller/local_player_controller.cpp | 27 ++-- .../controller/local_player_controller.hpp | 5 +- 4 files changed, 136 insertions(+), 53 deletions(-) diff --git a/src/input/sdl_controller.cpp b/src/input/sdl_controller.cpp index b2c9e085411..8b467b9d006 100644 --- a/src/input/sdl_controller.cpp +++ b/src/input/sdl_controller.cpp @@ -29,11 +29,30 @@ #include "utils/string_utils.hpp" #include "utils/translation.hpp" +#include #include #include #include #include +namespace +{ +SDL_HapticEffect steeringEffect(Sint16 level) +{ + SDL_HapticEffect effect = {}; + effect.type = SDL_HAPTIC_CONSTANT; + effect.constant.direction.type = SDL_HAPTIC_CARTESIAN; + // SDL directions tell where the force comes from, so a force from the + // left makes a positive level push the wheel to the right. + effect.constant.direction.dir[0] = -1; + // The effect is stopped explicitly once the game stops feeding it, see + // SDLController::updateForceFeedback(). + effect.constant.length = SDL_HAPTIC_INFINITY; + effect.constant.level = level; + return effect; +} // steeringEffect +} + // ---------------------------------------------------------------------------- SDLController::SDLController(int device_id) : m_gamepad(NULL) @@ -49,8 +68,13 @@ SDLController::SDLController(int device_id) m_native_rumble = false; m_steering_effect = -1; m_steering_effect_running = false; - m_steering_force = 0.0f; + m_steering_spring = 0.0f; + m_steering_damper = 0.0f; + m_steering_offset = 0.0f; m_steering_force_time = 0; + m_prev_wheel = 0.0f; + m_prev_ffb_update = 0; + m_uploaded_level = 0; m_rumble_strength = 0.0f; m_rumble_end_time = 0; m_id = -1; @@ -220,13 +244,7 @@ SDLController::SDLController(int device_id) if ((SDL_HapticQuery(m_haptic) & SDL_HAPTIC_CONSTANT) == 0) return; - SDL_HapticEffect effect = {}; - effect.type = SDL_HAPTIC_CONSTANT; - effect.constant.direction.type = SDL_HAPTIC_CARTESIAN; - effect.constant.direction.dir[0] = 1; - // The effect is stopped explicitly once the game stops feeding it, see - // updateForceFeedback(). - effect.constant.length = SDL_HAPTIC_INFINITY; + SDL_HapticEffect effect = steeringEffect(0); m_steering_effect = SDL_HapticNewEffect(m_haptic, &effect); if (m_steering_effect < 0) { @@ -242,10 +260,7 @@ SDLController::SDLController(int device_id) SDLController::~SDLController() { Log::info("SDLController", "%s unplugged.", SDL_JoystickName(m_joystick)); - if (m_game_controller) - SDL_GameControllerClose(m_game_controller); - else - SDL_JoystickClose(m_joystick); + // Closing the joystick can free the haptic device on some backends. if (m_haptic) { stopSteeringEffect(); @@ -253,6 +268,10 @@ SDLController::~SDLController() SDL_HapticDestroyEffect(m_haptic, m_steering_effect); SDL_HapticClose(m_haptic); } + if (m_game_controller) + SDL_GameControllerClose(m_game_controller); + else + SDL_JoystickClose(m_joystick); m_gamepad->getConfiguration()->unPlugged(); m_gamepad->setIrrIndex(-1); m_gamepad->setConnected(false); @@ -325,20 +344,49 @@ void SDLController::updateAutoCenter(int state) } // ---------------------------------------------------------------------------- -/** Sets the steering force, in [-1, 1] where positive pushes the wheel to the - * right. It has to be called continuously, the force fades out shortly after - * the game stops calling it (pause, race end, menus). +/** Sets the steering force as F = offset - spring * wheel - damper * speed, + * with the wheel position in [-1, 1] and its speed in full locks per second, + * where positive pushes the wheel to the right. It has to be called + * continuously, the force fades out shortly after the game stops calling it + * (pause, race end, menus). */ -void SDLController::setSteeringForce(float force) +void SDLController::setSteeringForce(float spring, float damper, float offset) { - m_steering_force = std::max(-1.0f, std::min(1.0f, force)); + m_steering_spring = spring; + m_steering_damper = damper; + m_steering_offset = offset; m_steering_force_time = StkTime::getMonoTimeMs(); } // setSteeringForce // ---------------------------------------------------------------------------- -/** Sends the current steering force and emulated rumble to the device. Called - * once per frame, so the device is not flooded when several physics steps - * run in one frame. +/** Finds the axis bound to steering. The sign converts the raw axis value to + * game steering, where positive is right. + */ +bool SDLController::getSteeringAxis(int* axis, float* sign) const +{ + const DeviceConfig* cfg = m_gamepad->getConfiguration(); + const Binding& right = cfg->getBinding(PA_STEER_RIGHT); + if (right.getType() == Input::IT_STICKMOTION && right.getId() < m_axes) + { + *axis = right.getId(); + *sign = right.getDirection() == Input::AD_POSITIVE ? 1.0f : -1.0f; + return true; + } + const Binding& left = cfg->getBinding(PA_STEER_LEFT); + if (left.getType() == Input::IT_STICKMOTION && left.getId() < m_axes) + { + *axis = left.getId(); + *sign = left.getDirection() == Input::AD_POSITIVE ? -1.0f : 1.0f; + return true; + } + return false; +} // getSteeringAxis + +// ---------------------------------------------------------------------------- +/** Sends the current steering force and emulated rumble to the device. The + * wheel position is read here instead of from the game input, so it is free + * of the gamepad deadzone and the wheel speed uses the real frame time + * instead of the physics step. */ void SDLController::updateForceFeedback() { @@ -354,7 +402,37 @@ void SDLController::updateForceFeedback() return; } - float force = steering ? m_steering_force : 0.0f; + // On Linux every upload becomes a USB report (ff-memless replays the + // effect), uncapped frame rates would overflow the usbhid output queue. + const uint64_t counter = SDL_GetPerformanceCounter(); + const uint64_t frequency = SDL_GetPerformanceFrequency(); + if (m_prev_ffb_update != 0 && + (counter - m_prev_ffb_update) * 400 < frequency) + return; + const float dt = m_prev_ffb_update == 0 ? 0.0f : + float(counter - m_prev_ffb_update) / frequency; + m_prev_ffb_update = counter; + + int axis = 0; + float axis_sign = 1.0f; + const bool has_axis = getSteeringAxis(&axis, &axis_sign); + float wheel = 0.0f; + float wheel_speed = 0.0f; + if (has_axis) + { + wheel = axis_sign * SDL_JoystickGetAxis(m_joystick, axis) / 32767.0f; + if (dt > 0.0f) + wheel_speed = (wheel - m_prev_wheel) / dt; + wheel_speed = std::max(-5.0f, std::min(5.0f, wheel_speed)); + m_prev_wheel = wheel; + } + + float force = 0.0f; + if (steering) + { + force = m_steering_offset - m_steering_spring * wheel - + m_steering_damper * wheel_speed; + } if (rumbling) { // Alternate the push direction quickly, the wheel motor can't do a @@ -362,6 +440,9 @@ void SDLController::updateForceFeedback() force += ((now / 50) % 2 == 0 ? 1.0f : -1.0f) * m_rumble_strength * 0.6f; } + // The effect pushes towards the positive end of the axis, which is left + // in the game if the player bound the axis the other way round. + force *= axis_sign; GamepadConfig* cfg = static_cast(m_gamepad->getConfiguration()); force *= cfg->getForceFeedbackStrength() / 100.0f; @@ -369,13 +450,12 @@ void SDLController::updateForceFeedback() force = -force; force = std::max(-1.0f, std::min(1.0f, force)); - SDL_HapticEffect effect = {}; - effect.type = SDL_HAPTIC_CONSTANT; - effect.constant.direction.type = SDL_HAPTIC_CARTESIAN; - effect.constant.direction.dir[0] = 1; - effect.constant.length = SDL_HAPTIC_INFINITY; - effect.constant.level = (Sint16)(force * 32767); + const Sint16 level = (Sint16)(force * 32767); + if (m_steering_effect_running && level == m_uploaded_level) + return; + SDL_HapticEffect effect = steeringEffect(level); SDL_HapticUpdateEffect(m_haptic, m_steering_effect, &effect); + m_uploaded_level = level; if (m_steering_effect_running) return; @@ -393,6 +473,7 @@ void SDLController::stopSteeringEffect() return; SDL_HapticStopEffect(m_haptic, m_steering_effect); m_steering_effect_running = false; + m_prev_ffb_update = 0; updateAutoCenter(m_gamepad->getAutoCenterStrength()); } // stopSteeringEffect diff --git a/src/input/sdl_controller.hpp b/src/input/sdl_controller.hpp index 86accaf7d2e..0294d5ece95 100644 --- a/src/input/sdl_controller.hpp +++ b/src/input/sdl_controller.hpp @@ -55,12 +55,25 @@ class SDLController bool m_steering_effect_running; - /** Last steering force requested by the game, in [-1, 1], positive - * pushes the wheel to the right. */ - float m_steering_force; + /** Steering force parameters last requested by the game, see + * setSteeringForce(). */ + float m_steering_spring; + float m_steering_damper; + float m_steering_offset; uint64_t m_steering_force_time; + /** Wheel position at the previous device update, in [-1, 1] with + * positive to the right, to damp fast wheel movement. */ + float m_prev_wheel; + + /** Performance counter value of the previous device update, 0 if the + * effect was not playing then. */ + uint64_t m_prev_ffb_update; + + /** Level last uploaded to the device, to skip redundant uploads. */ + int16_t m_uploaded_level; + float m_rumble_strength; uint64_t m_rumble_end_time; @@ -84,6 +97,7 @@ class SDLController void updateAutoCenter(int state); void stopSteeringEffect(); + bool getSteeringAxis(int* axis, float* sign) const; public: // ------------------------------------------------------------------------ SDLController(int device_id); @@ -187,7 +201,7 @@ class SDLController // ------------------------------------------------------------------------ bool hasSteeringForce() const { return m_steering_effect >= 0; } // ------------------------------------------------------------------------ - void setSteeringForce(float force); + void setSteeringForce(float spring, float damper, float offset); // ------------------------------------------------------------------------ void updateForceFeedback(); GamePadDevice* getGamePadDevice() const { return m_gamepad; } diff --git a/src/karts/controller/local_player_controller.cpp b/src/karts/controller/local_player_controller.cpp index 9cbcc613223..43ce4de8d67 100644 --- a/src/karts/controller/local_player_controller.cpp +++ b/src/karts/controller/local_player_controller.cpp @@ -71,7 +71,6 @@ LocalPlayerController::LocalPlayerController(AbstractKart *kart, : PlayerController(kart) { m_last_crash = 0; - m_ffb_prev_wheel = 0.0f; m_has_started = false; m_handicap = h; m_player = StateManager::get()->getActivePlayer(local_player_id); @@ -139,7 +138,6 @@ void LocalPlayerController::reset() { PlayerController::reset(); m_last_crash = 0; - m_ffb_prev_wheel = 0.0f; m_sound_schedule = false; m_has_started = false; } // reset @@ -304,7 +302,7 @@ void LocalPlayerController::update(int ticks) m_sound_schedule = false; m_kart->playSound(m_bzzt_sound); } - updateForceFeedback(ticks); + updateForceFeedback(); } // update //----------------------------------------------------------------------------- @@ -526,10 +524,12 @@ void LocalPlayerController::rumble(float strength_low, float strength_high, uint } // ---------------------------------------------------------------------------- -/** Computes the steering force for wheels with constant force support. All - * forces are relative to full lock, positive pushes the wheel to the right. +/** Computes the steering force parameters for wheels with constant force + * support. All forces are relative to full lock, positive pushes the wheel + * to the right. The wheel position itself is read by SDLController, so the + * spring works on the same, undelayed value it outputs. */ -void LocalPlayerController::updateForceFeedback(int ticks) +void LocalPlayerController::updateForceFeedback() { #ifndef SERVER_ONLY if (RewindManager::get()->isRewinding()) @@ -538,14 +538,6 @@ void LocalPlayerController::updateForceFeedback(int ticks) if (!controller || !controller->hasSteeringForce()) return; - // The raw input is used instead of KartControl::getSteer(), which lags - // behind the wheel position and would make the spring feel spongy. - const float wheel = -m_steer_val / 32767.0f; - const float dt = stk_config->ticks2Time(ticks); - float wheel_speed = dt > 0.0f ? (wheel - m_ffb_prev_wheel) / dt : 0.0f; - wheel_speed = std::max(-5.0f, std::min(5.0f, wheel_speed)); - m_ffb_prev_wheel = wheel; - const float max_speed = m_kart->getKartProperties()->getEngineMaxSpeed(); const float speed = max_speed > 0.0f ? std::min(1.0f, fabsf(m_kart->getSpeed()) / max_speed) : 0.0f; @@ -570,10 +562,9 @@ void LocalPlayerController::updateForceFeedback(int ticks) // with speed like a real car. const float spring = 0.15f + 0.55f * speed * grip; const float damper = 0.02f + 0.06f * speed * grip; - float force = -spring * wheel - damper * wheel_speed; // The rear stepping out pulls the wheel into countersteer. - force += drift_pull * 0.2f * speed * grip; + float offset = drift_pull * 0.2f * speed * grip; // Offroad materials (grass, sand, ...) slow the kart down, use that as a // measure of how bumpy they are. @@ -587,10 +578,10 @@ void LocalPlayerController::updateForceFeedback(int ticks) // a motor buzz. const float bumps = sinf(t * 2.0f * M_PI * 9.0f) + 0.5f * sinf(t * 2.0f * M_PI * 23.0f); - force += bumps * roughness * speed * 0.25f; + offset += bumps * roughness * speed * 0.25f; } - controller->setSteeringForce(force); + controller->setSteeringForce(spring, damper, offset); #endif } // updateForceFeedback diff --git a/src/karts/controller/local_player_controller.hpp b/src/karts/controller/local_player_controller.hpp index 61433e6ffc4..ffa152cb2de 100644 --- a/src/karts/controller/local_player_controller.hpp +++ b/src/karts/controller/local_player_controller.hpp @@ -60,9 +60,6 @@ class LocalPlayerController : public PlayerController int m_last_crash; - /** Wheel position at the previous update, to damp fast wheel movement. */ - float m_ffb_prev_wheel; - HandicapLevel m_handicap; SFXBase *m_wee_sound; @@ -78,7 +75,7 @@ class LocalPlayerController : public PlayerController void nitroNotFullSound(); void doCrashHaptics(); - void updateForceFeedback(int ticks); + void updateForceFeedback(); SDLController* getForceFeedbackController() const; void setParticleEmitterPosition(const btTransform& t);