Logitech G27 fixes and cleanups (#17170)
Some checks are pending
Generate Translation Template / Generate Translation Template (push) Waiting to run
Build RPCS3 / RPCS3 Linux ubuntu-24.04 gcc (push) Waiting to run
Build RPCS3 / RPCS3 Linux ubuntu-24.04-arm clang (push) Waiting to run
Build RPCS3 / RPCS3 Linux ubuntu-24.04 clang (push) Waiting to run
Build RPCS3 / RPCS3 Windows (push) Waiting to run

- revamp effect direction to avoid some windows driver issues
- ref: https://github.com/libsdl-org/SDL/issues/7941
- fix possible SDL_SetJoystickLED calls on closed joystick handle
- various comment cleanups
- make now static sdl effect direction a template
- fix formatting, shorten logitech_g27_force_to_level, revamp sawtooth effect abuse logging
- log and handle ramp effects with reversed min/max forces
- remove abandoned functions
This commit is contained in:
Katharine Chui 2025-05-07 19:20:35 +02:00 committed by GitHub
parent 2a50ac1891
commit e1d3cf57c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -19,6 +19,13 @@
LOG_CHANNEL(logitech_g27_log, "LOGIG27");
// ref: https://github.com/libsdl-org/SDL/issues/7941, need to use SDL_HAPTIC_STEERING_AXIS for some windows drivers
static const SDL_HapticDirection STEERING_DIRECTION =
{
.type = SDL_HAPTIC_STEERING_AXIS,
.dir = {0, 0, 0}
};
usb_device_logitech_g27::usb_device_logitech_g27(u32 controller_index, const std::array<u8, 7>& location)
: usb_device_emulated(location), m_controller_index(controller_index)
{
@ -34,11 +41,7 @@ usb_device_logitech_g27::usb_device_logitech_g27(u32 controller_index, const std
}
m_default_spring_effect.type = SDL_HAPTIC_SPRING;
m_default_spring_effect.condition.direction = SDL_HapticDirection
{
.type = SDL_HAPTIC_POLAR,
.dir = {27000, 0}
};
m_default_spring_effect.condition.direction = STEERING_DIRECTION;
m_default_spring_effect.condition.length = SDL_HAPTIC_INFINITY;
for (int i = 0; i < 1 /*3*/; i++)
{
@ -291,12 +294,6 @@ void usb_device_logitech_g27::sdl_refresh()
if (joysticks_changed || haptic_changed || led_joystick_changed)
{
const std::lock_guard<std::mutex> lock(m_sdl_handles_mutex);
if (joysticks_changed)
{
clear_sdl_joysticks(m_joysticks);
m_joysticks = new_joysticks;
}
// reset effects if the ffb device is changed
if (haptic_changed)
{
if (m_haptic_handle)
@ -304,6 +301,7 @@ void usb_device_logitech_g27::sdl_refresh()
SDL_CloseHaptic(m_haptic_handle);
m_haptic_handle = nullptr;
}
// reset effects if the ffb device is changed
for (logitech_g27_ffb_slot& slot : m_effect_slots)
{
slot.effect_id = -1;
@ -316,6 +314,12 @@ void usb_device_logitech_g27::sdl_refresh()
SDL_SetJoystickLED(m_led_joystick_handle, 0, 0, 0);
m_led_joystick_handle = new_led_joystick_handle;
}
if (joysticks_changed)
{
// finally clear out previous joystick handles
clear_sdl_joysticks(m_joysticks);
m_joysticks = new_joysticks;
}
}
if (!joysticks_changed)
@ -330,17 +334,27 @@ void usb_device_logitech_g27::sdl_refresh()
}
}
static inline s16 logitech_g27_force_to_level(u8 force)
static inline s16 logitech_g27_force_to_level(u8 force, bool reverse = false)
{
/*
* Wheel documentation:
* level 0, maximum clock wise force
* level 127-128, no force
* level 255, maximum anticlockwise force
*/
if (force == 127 || force == 128)
{
return 0;
}
if (force > 128)
{
return ((force - 128) * 0x7FFF) / (255 - 128);
}
return ((127 - force) * 0x7FFF * -1) / (127 - 0);
const s16 subtrahend = force > 128 ? 128 : 127;
s16 converted = ((force - subtrahend) * 0x7FFF) / 127;
if (reverse)
converted *= -1;
return converted;
}
static inline s16 logitech_g27_position_to_center(u8 left, u8 right)
@ -411,18 +425,6 @@ static inline u16 logitech_g27_loops_to_ms(u16 loops, bool afap)
return loops * 2;
}
static inline u16 axis_to_logitech_g27_steering(s16 axis)
{
const u16 unsigned_axis = axis + 0x8000;
return (unsigned_axis * (0xFFFF >> 2)) / 0xFFFF;
}
static inline u8 axis_to_logitech_g27_pedal(s16 axis)
{
const u16 unsigned_axis = axis + 0x8000;
return (unsigned_axis * (0xFF)) / 0xFFFF;
}
extern bool is_input_allowed();
static u8 sdl_hat_to_logitech_g27_hat(u8 sdl_hat)
@ -777,15 +779,6 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
// logitech_g27_log.error("%02x %02x %02x %02x %02x %02x %02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]);
// printf("%02x %02x %02x %02x %02x %02x %02x\n", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]);
SDL_HapticDirection direction = {
.type = SDL_HAPTIC_POLAR,
.dir = {27000, 0}
};
if (m_reverse_effects)
{
direction.dir[0] = 9000;
}
// TODO maybe force clipping from cfg
// Process effects
@ -898,9 +891,9 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
// Constant force
new_effect.type = SDL_HAPTIC_CONSTANT;
new_effect.constant.direction = direction;
new_effect.constant.direction = STEERING_DIRECTION;
new_effect.constant.length = SDL_HAPTIC_INFINITY;
new_effect.constant.level = logitech_g27_force_to_level(buf[2 + i]);
new_effect.constant.level = logitech_g27_force_to_level(buf[2 + i], m_reverse_effects);
break;
}
case 0x01:
@ -908,11 +901,10 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
// Spring/High resolution spring
new_effect.type = SDL_HAPTIC_SPRING;
new_effect.condition.direction = direction;
new_effect.condition.direction = STEERING_DIRECTION;
new_effect.condition.length = SDL_HAPTIC_INFINITY;
const u8 s1 = buf[5] & 1;
const u8 s2 = (buf[5] >> 4) & 1;
// TODO direction cfg
const u16 saturation = logitech_g27_clip_to_saturation(buf[6]);
s16 center = 0;
u16 deadband = 0;
@ -960,7 +952,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
// Damper/High resolution damper
new_effect.type = SDL_HAPTIC_DAMPER;
new_effect.condition.direction = direction;
new_effect.condition.direction = STEERING_DIRECTION;
new_effect.condition.length = SDL_HAPTIC_INFINITY;
const u8 s1 = buf[3] & 1;
const u8 s2 = buf[5] & 1;
@ -1000,13 +992,12 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
// Friction
new_effect.type = SDL_HAPTIC_FRICTION;
new_effect.condition.direction = direction;
new_effect.condition.direction = STEERING_DIRECTION;
new_effect.condition.length = SDL_HAPTIC_INFINITY;
const u8 k1 = buf[2];
const u8 k2 = buf[3];
const u8 s1 = buf[5] & 1;
const u8 s2 = (buf[5] >> 4) & 1;
// TODO direction cfg
s16 left_coeff = logitech_g27_friction_coeff_to_coeff(k1, s1);
s16 right_coeff = logitech_g27_friction_coeff_to_coeff(k2, s2);
const s16 saturation = logitech_g27_clip_to_saturation(buf[4]);
@ -1028,9 +1019,8 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
// Auto center spring/High resolution auto center spring
new_effect.type = SDL_HAPTIC_SPRING;
new_effect.condition.direction = direction;
new_effect.condition.direction = STEERING_DIRECTION;
new_effect.condition.length = SDL_HAPTIC_INFINITY;
// TODO direction cfg
const u16 saturation = logitech_g27_clip_to_saturation(buf[4]);
const u16 deadband = 2 * 0xFFFF / 255;
s16 center = 0;
@ -1069,30 +1059,53 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
case 0x05:
{
// Sawtooth up/Sawtooth down
// for reversing sawtooth, it should be mirroring the offset, then play the up/down counterpart
if (buf[1] == 0x04)
new_effect.type = !m_reverse_effects ? SDL_HAPTIC_SAWTOOTHUP : SDL_HAPTIC_SAWTOOTHDOWN;
else
new_effect.type = m_reverse_effects ? SDL_HAPTIC_SAWTOOTHUP : SDL_HAPTIC_SAWTOOTHDOWN;
new_effect.type = buf[1] == 0x04 ? SDL_HAPTIC_SAWTOOTHUP : SDL_HAPTIC_SAWTOOTHDOWN;
new_effect.periodic.direction = direction;
new_effect.periodic.direction = STEERING_DIRECTION;
new_effect.periodic.length = SDL_HAPTIC_INFINITY;
const u8 l1 = buf[2];
const u8 l2 = buf[3];
const u8 l0 = buf[4];
const u8 t3 = buf[6] >> 4;
const u8 inc = buf[6] & 0xf;
if (inc != 0)
new_effect.periodic.period = ((l1 - l2) * logitech_g27_loops_to_ms(t3, !m_fixed_loop)) / inc;
const s16 amplitude = (l1 - l2);
const s16 progress = buf[1] == 0x04 ? l0 - l2 : l1 - l0;
if (amplitude <= 0 || inc == 0 || t3 == 0)
{
logitech_g27_log.error("cannot evaluate period and phase for saw tooth effect, l1 %u l2 %u inc %u t3 %u", l1, l2, inc, t3);
new_effect.periodic.period = 0;
new_effect.periodic.phase = 0;
}
else
{
logitech_g27_log.error("cannot evaluate slope for saw tooth effect, loops per step %u level per step %u", t3, inc);
new_effect.periodic.period = 1000;
new_effect.periodic.period = (amplitude * logitech_g27_loops_to_ms(t3, !m_fixed_loop)) / inc;
if (progress < 0)
{
logitech_g27_log.error("cannot evaluate phase for saw tooth effect, l1 %u l2 %u l0 %u", l1, l2, l0);
new_effect.periodic.phase = 0;
}
else
new_effect.periodic.phase = 36000 * progress / amplitude;
}
new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2);
new_effect.periodic.magnitude = logitech_g27_force_to_level(l1) - new_effect.periodic.offset;
new_effect.periodic.phase = buf[1] == 0x04 ? 36000 * (l1 - l0) / (l1 - l2) : 36000 * (l0 - l2) / (l1 - l2);
// TODO implement fallback if we actually find games abusing the above fail cases for constant force on l0
new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2, m_reverse_effects);
new_effect.periodic.magnitude = logitech_g27_force_to_level(std::abs(amplitude) / 2);
break;
}
case 0x06:
{
// Trapezoid, convert to SDL_HAPTIC_SQUARE or SDL_HAPTIC_TRIANGLE
new_effect.periodic.direction = direction;
// TODO full accuracy will need some kind of rendering thread, cannot be represented with a single effect
new_effect.periodic.direction = STEERING_DIRECTION;
new_effect.periodic.length = SDL_HAPTIC_INFINITY;
const u8 l1 = buf[2];
const u8 l2 = buf[3];
@ -1111,15 +1124,17 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
new_effect.type = SDL_HAPTIC_TRIANGLE;
}
new_effect.periodic.period = total_slope_time + total_flat_time;
new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2);
new_effect.periodic.magnitude = logitech_g27_force_to_level(l1) - new_effect.periodic.offset;
// when mirroring waves, both offset and magnitude should be mirrored
new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2, m_reverse_effects);
new_effect.periodic.magnitude = logitech_g27_force_to_level(l1, m_reverse_effects) - new_effect.periodic.offset;
break;
}
case 0x07:
{
// Rectangle, convert to SDL_HAPTIC_SQUARE
// TODO full accuracy will need some kind of rendering thread, cannot be represented with a single effect
new_effect.type = SDL_HAPTIC_SQUARE;
new_effect.periodic.direction = direction;
new_effect.periodic.direction = STEERING_DIRECTION;
new_effect.periodic.length = SDL_HAPTIC_INFINITY;
const u8 l1 = buf[2];
const u8 l2 = buf[3];
@ -1127,8 +1142,8 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
const u8 t2 = buf[5];
const u8 p = buf[6];
new_effect.periodic.period = logitech_g27_loops_to_ms(t1, !m_fixed_loop) + logitech_g27_loops_to_ms(t2, !m_fixed_loop);
new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2);
new_effect.periodic.magnitude = logitech_g27_force_to_level(l1) - new_effect.periodic.offset;
new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2, m_reverse_effects);
new_effect.periodic.magnitude = logitech_g27_force_to_level(l1, m_reverse_effects) - new_effect.periodic.offset;
if (new_effect.periodic.period != 0)
new_effect.periodic.phase = 36000 * logitech_g27_loops_to_ms(p, !m_fixed_loop) / new_effect.periodic.period;
else
@ -1141,13 +1156,11 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
case 0x08:
case 0x09:
{
// Variable/Ramp, convert to SDL_HAPTIC_CONSTANT
// Variable/Ramp, convert to SDL_HAPTIC_CONSTANT / SDL_HAPTIC_RAMP
if (i % 2 != 0)
{
continue;
}
new_effect.type = SDL_HAPTIC_CONSTANT;
new_effect.constant.direction = direction;
const u8 l1 = buf[2];
const u8 l2 = buf[3];
const u8 t1 = buf[4] >> 4;
@ -1158,46 +1171,49 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
const u8 d2 = (buf[6] >> 4) & 1;
if (buf[1] == 0x08)
{
new_effect.type = SDL_HAPTIC_CONSTANT;
const u8 t = i == 0 ? t1 : t2;
const u8 s = i == 0 ? s1 : s2;
const u8 d = i == 0 ? d1 : d2;
const u8 l = i == 0 ? l1 : l2;
new_effect.constant.length = SDL_HAPTIC_INFINITY;
new_effect.constant.direction = STEERING_DIRECTION;
if (s == 0 || t == 0)
{
// gran turismo 6 does this, gives a variable force with no step so it just behaves as constant force
new_effect.constant.level = logitech_g27_force_to_level(l);
new_effect.constant.level = logitech_g27_force_to_level(l, m_reverse_effects);
// hack: gran turismo 6 spams download and play
update_hack = true;
}
else
{
new_effect.constant.attack_level = logitech_g27_force_to_level(l);
if (d)
new_effect.constant.level = d ? -0x7FFF : 0x7FFF;
if (m_reverse_effects)
new_effect.constant.level *= -1;
// TODO full accuracy will need some kind of rendering thread, cannot be represented with a single effect
const s16 begin_level = logitech_g27_force_to_level(l, m_reverse_effects);
if ((new_effect.constant.level > 0 && begin_level > 0) || (new_effect.constant.level < 0 && begin_level < 0))
{
new_effect.constant.level = 0;
new_effect.constant.attack_length = l * logitech_g27_loops_to_ms(t, !m_fixed_loop) / s;
}
else
{
new_effect.constant.level = 0x7FFF;
new_effect.constant.attack_length = (255 - l) * logitech_g27_loops_to_ms(t, !m_fixed_loop) / s;
new_effect.constant.attack_level = begin_level * 0x7FFF / new_effect.constant.level;
}
}
}
else
{
new_effect.type = SDL_HAPTIC_RAMP;
new_effect.ramp.direction = STEERING_DIRECTION;
if (l2 > l1)
logitech_g27_log.error("min force is larger than max force in ramp effect, l1 %u l2 %u", l1, l2);
const s16 l1_converted = logitech_g27_force_to_level(l1, m_reverse_effects);
const s16 l2_converted = logitech_g27_force_to_level(l2, m_reverse_effects);
new_effect.ramp.start = d1 ? l1_converted : l2_converted;
new_effect.ramp.end = d1 ? l2_converted : l1_converted;
new_effect.ramp.length = 0;
if (s2 == 0 || t2 == 0)
{
logitech_g27_log.error("cannot evaluate slope for ramp effect, loops per step %u level per step %u", t2, s2);
}
else
{
new_effect.constant.length = (l1 - l2) * logitech_g27_loops_to_ms(t2, !m_fixed_loop) / s2;
new_effect.constant.attack_length = new_effect.constant.length;
new_effect.constant.attack_level = d1 ? logitech_g27_force_to_level(l1) : logitech_g27_force_to_level(l2);
}
new_effect.constant.level = d1 ? logitech_g27_force_to_level(l2) : logitech_g27_force_to_level(l1);
new_effect.ramp.length = std::abs(l1 - l2) * logitech_g27_loops_to_ms(t2, !m_fixed_loop) / s2;
}
break;
}
@ -1205,7 +1221,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
{
// Square
new_effect.type = SDL_HAPTIC_SQUARE;
new_effect.periodic.direction = direction;
new_effect.periodic.direction = STEERING_DIRECTION;
const u8 a = buf[2];
const u8 tl = buf[3];
const u8 th = buf[4];
@ -1446,6 +1462,8 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp
case 0x09:
{
// Set LED
// TODO this is practically disabled at the moment, due to effect slot matching prior
// need to figure out what this command actually does first, since RPM light command has priority to SDL_SetJoystickLED
if (m_led_joystick_handle == nullptr)
{
break;