Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/input/gamepad_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#ifndef SERVER_ONLY
#include "input/sdl_controller.hpp"
#include <algorithm>
#include <array>

enum AxisWithDirection : unsigned
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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()=="")
Expand All @@ -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 << "</gamepad>\n\n";
} // save
Expand Down
11 changes: 11 additions & 0 deletions src/input/gamepad_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::tuple<int, Input::AxisDirection>, int> m_sdl_mapping;

std::set<int> m_digital_axes;
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/input/input_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
220 changes: 208 additions & 12 deletions src/input/sdl_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,38 @@
#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"
#include "utils/time.hpp"
#include "utils/string_utils.hpp"
#include "utils/translation.hpp"

#include <SDL_timer.h>
#include <SDL_version.h>
#include <algorithm>
#include <stdexcept>
#include <string>

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)
Expand All @@ -44,6 +65,18 @@ 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_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;

if (SDL_IsGameController(device_id))
Expand Down Expand Up @@ -201,23 +234,44 @@ 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 = steeringEffect(0);
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

// ----------------------------------------------------------------------------
SDLController::~SDLController()
{
Log::info("SDLController", "%s unplugged.", SDL_JoystickName(m_joystick));
// Closing the joystick can free the haptic device on some backends.
if (m_haptic)
{
stopSteeringEffect();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now stops and destroys the effect and sets autocenter after the joystick has already been closed above. SDL_HapticOpenFromJoystick() docs say to close the haptic device before the joystick, because on some backends closing the joystick frees the haptic device too. Move this block above the joystick close.

if (m_steering_effect >= 0)
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);
if (m_haptic)
SDL_HapticClose(m_haptic);
m_gamepad->getConfiguration()->unPlugged();
m_gamepad->setIrrIndex(-1);
m_gamepad->setConnected(false);
Expand Down Expand Up @@ -261,26 +315,168 @@ 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 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 spring, float damper, float offset)
{
m_steering_spring = spring;
m_steering_damper = damper;
m_steering_offset = offset;
m_steering_force_time = StkTime::getMonoTimeMs();
} // setSteeringForce

// ----------------------------------------------------------------------------
/** 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()
{
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;
}

// 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
// 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;
}

// 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<GamepadConfig*>(m_gamepad->getConfiguration());
force *= cfg->getForceFeedbackStrength() / 100.0f;
if (cfg->isForceFeedbackInverted())
force = -force;
force = std::max(-1.0f, std::min(1.0f, force));

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);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Linux, hid-lg4ff sits on ff-memless, which replays a playing effect when it's re-uploaded. So every call here becomes a USB output report. The frame rate isn't capped (vsync off, menus), so this can easily run at several hundred Hz while the noise and rumble change the level on every frame, and that fills the usbhid output queue (output queue full). I'd skip the update when the level hasn't changed and cap the update rate at something like 250-500 Hz.

m_uploaded_level = level;

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;
m_prev_ffb_update = 0;
updateAutoCenter(m_gamepad->getAutoCenterStrength());
} // stopSteeringEffect

// ----------------------------------------------------------------------------
#ifdef ANDROID
void SDLController::handleDirectScanCode(const SDL_Event& event)
Expand Down
Loading