diff --git a/rpcs3/Emu/Io/LogitechG27.cpp b/rpcs3/Emu/Io/LogitechG27.cpp index 1612c567d6..15106714b7 100644 --- a/rpcs3/Emu/Io/LogitechG27.cpp +++ b/rpcs3/Emu/Io/LogitechG27.cpp @@ -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& 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 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;