From 8d9abd396b573e7565254f3c7dd43ccb6158b7f9 Mon Sep 17 00:00:00 2001 From: Nico Franke Date: Fri, 5 Jun 2026 20:09:35 +0200 Subject: [PATCH] Make FFB direction encoding for emulated G27 configurable (#18776) This adds a dropdown in the emulated G27 settings dialog ("Force feedback direction encoding") with three options: Steering Axis (default), Cartesian, Polar. Default is Steering Axis so existing users see no change. RPCS3 right now hardcodes SDL_HAPTIC_STEERING_AXIS for the spring and constant-force effects it sends to the host wheel. That's SDL's recommended encoding for wheels and works on the hardware the code was written against. Some Windows DirectInput drivers (like my MOZA R3) do not react to these affects at all. This now allows changing it to an encoding that works. --- rpcs3/Emu/Io/LogitechG27.cpp | 54 ++++++++++++------- rpcs3/Emu/Io/LogitechG27.h | 1 + rpcs3/Emu/Io/LogitechG27Config.cpp | 16 ++++++ rpcs3/Emu/Io/LogitechG27Config.h | 8 +++ .../emulated_logitech_g27_settings_dialog.cpp | 15 ++++++ .../emulated_logitech_g27_settings_dialog.h | 1 + 6 files changed, 76 insertions(+), 19 deletions(-) diff --git a/rpcs3/Emu/Io/LogitechG27.cpp b/rpcs3/Emu/Io/LogitechG27.cpp index 3f07e92560..2d2662ea5e 100644 --- a/rpcs3/Emu/Io/LogitechG27.cpp +++ b/rpcs3/Emu/Io/LogitechG27.cpp @@ -266,12 +266,28 @@ static const std::map> 4) & 1; @@ -1405,7 +1421,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 = STEERING_DIRECTION; + new_effect.condition.direction = make_steering_direction(); new_effect.condition.length = SDL_HAPTIC_INFINITY; const u8 s1 = buf[3] & 1; const u8 s2 = buf[5] & 1; @@ -1445,7 +1461,7 @@ 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 = STEERING_DIRECTION; + new_effect.condition.direction = make_steering_direction(); new_effect.condition.length = SDL_HAPTIC_INFINITY; const u8 k1 = buf[2]; const u8 k2 = buf[3]; @@ -1472,7 +1488,7 @@ 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 = STEERING_DIRECTION; + new_effect.condition.direction = make_steering_direction(); new_effect.condition.length = SDL_HAPTIC_INFINITY; const u16 saturation = logitech_g27_clip_to_saturation(buf[4]); constexpr u16 deadband = 2 * 0xFFFF / 255; @@ -1518,7 +1534,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp 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 = STEERING_DIRECTION; + new_effect.periodic.direction = make_steering_direction(); new_effect.periodic.length = SDL_HAPTIC_INFINITY; const u8 l1 = buf[2]; const u8 l2 = buf[3]; @@ -1558,7 +1574,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp { // Trapezoid, convert to SDL_HAPTIC_SQUARE or SDL_HAPTIC_TRIANGLE // 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.direction = make_steering_direction(); new_effect.periodic.length = SDL_HAPTIC_INFINITY; const u8 l1 = buf[2]; const u8 l2 = buf[3]; @@ -1587,7 +1603,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp // 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 = STEERING_DIRECTION; + new_effect.periodic.direction = make_steering_direction(); new_effect.periodic.length = SDL_HAPTIC_INFINITY; const u8 l1 = buf[2]; const u8 l2 = buf[3]; @@ -1630,7 +1646,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp 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; + new_effect.constant.direction = make_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 @@ -1655,7 +1671,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp else { new_effect.type = SDL_HAPTIC_RAMP; - new_effect.ramp.direction = STEERING_DIRECTION; + new_effect.ramp.direction = make_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); @@ -1674,7 +1690,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 = STEERING_DIRECTION; + new_effect.periodic.direction = make_steering_direction(); const u8 a = buf[2]; const u8 tl = buf[3]; const u8 th = buf[4]; diff --git a/rpcs3/Emu/Io/LogitechG27.h b/rpcs3/Emu/Io/LogitechG27.h index ccb2f58908..e991472878 100644 --- a/rpcs3/Emu/Io/LogitechG27.h +++ b/rpcs3/Emu/Io/LogitechG27.h @@ -126,6 +126,7 @@ private: void transfer_dfgt(u32 buf_size, u8* buf, UsbTransfer* transfer) const; void transfer_g25(u32 buf_size, u8* buf, UsbTransfer* transfer) const; void transfer_g27(u32 buf_size, u8* buf, UsbTransfer* transfer) const; + SDL_HapticDirection make_steering_direction() const; u32 m_controller_index = 0; diff --git a/rpcs3/Emu/Io/LogitechG27Config.cpp b/rpcs3/Emu/Io/LogitechG27Config.cpp index aa1ebf2af7..4f84988651 100644 --- a/rpcs3/Emu/Io/LogitechG27Config.cpp +++ b/rpcs3/Emu/Io/LogitechG27Config.cpp @@ -39,6 +39,22 @@ void fmt_class_string::format(std::string& out, u64 arg) }); } +template <> +void fmt_class_string::format(std::string& out, u64 arg) +{ + format_enum(out, arg, [](g27_ffb_direction_type value) + { + switch (value) + { + case g27_ffb_direction_type::steering_axis: return "steering_axis"; + case g27_ffb_direction_type::cartesian: return "cartesian"; + case g27_ffb_direction_type::polar: return "polar"; + } + + return unknown; + }); +} + emulated_logitech_g27_config g_cfg_logitech_g27; LOG_CHANNEL(cfg_log, "CFG"); diff --git a/rpcs3/Emu/Io/LogitechG27Config.h b/rpcs3/Emu/Io/LogitechG27Config.h index 1a46a0fcc2..4f9ea45117 100644 --- a/rpcs3/Emu/Io/LogitechG27Config.h +++ b/rpcs3/Emu/Io/LogitechG27Config.h @@ -20,6 +20,13 @@ enum class hat_component right }; +enum class g27_ffb_direction_type +{ + steering_axis = 0, + cartesian = 1, + polar = 2, +}; + // this was a bitfield, but juggling at least 3 compilers and OSes means no bitfield // num_buttons:10 << 52 | num_hats:10 << 42 | num_axes:10 << 32 | vendor_id:16 << 16 | product_id:16 struct emulated_g27_device_type_id @@ -111,6 +118,7 @@ public: emulated_logitech_g27_mapping shifter_r{this, "shifter_r", 0, sdl_mapping_type::hat, 0, hat_component::left, false}; cfg::_bool reverse_effects{this, "reverse_effects", false}; + cfg::_enum ffb_direction_type{this, "ffb_direction_type", g27_ffb_direction_type::steering_axis}; cfg::uint<0, 4> compatibility_limit{this, "compatibility_limit", 4}; cfg::uint<0, 0xFFFFFFFFFFFFFFFF> ffb_device_type_id{this, "ffb_device_type_id", 0}; cfg::uint<0, 0xFFFFFFFFFFFFFFFF> led_device_type_id{this, "led_device_type_id", 0}; diff --git a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp index 5955e91b72..a0fa185f8e 100644 --- a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp @@ -547,6 +547,7 @@ void emulated_logitech_g27_settings_dialog::save_ui_state_to_config() g_cfg_logitech_g27.enabled.set(m_enabled->isChecked()); g_cfg_logitech_g27.reverse_effects.set(m_reverse_effects->isChecked()); + g_cfg_logitech_g27.ffb_direction_type.set(static_cast(m_ffb_direction_type->currentData().toInt())); g_cfg_logitech_g27.compatibility_limit.set(m_compatibility_limit->currentData().toInt()); if (m_ffb_device->get_device_choice() == mapping_device::NONE) @@ -597,6 +598,7 @@ void emulated_logitech_g27_settings_dialog::load_ui_state_from_config() m_enabled->setChecked(g_cfg_logitech_g27.enabled.get()); m_reverse_effects->setChecked(g_cfg_logitech_g27.reverse_effects.get()); + m_ffb_direction_type->setCurrentIndex(m_ffb_direction_type->findData(static_cast(g_cfg_logitech_g27.ffb_direction_type.get()))); m_compatibility_limit->setCurrentIndex(4 - g_cfg_logitech_g27.compatibility_limit.get()); } @@ -658,6 +660,18 @@ emulated_logitech_g27_settings_dialog::emulated_logitech_g27_settings_dialog(QWi m_reverse_effects = new QCheckBox(tr("Reverse force feedback effects"), this); v_layout->addWidget(m_reverse_effects); + QHBoxLayout* ffb_dir_layout = new QHBoxLayout(this); + ffb_dir_layout->setContentsMargins(0, 0, 0, 0); + QLabel* ffb_dir_label = new QLabel(tr("Force feedback direction encoding:"), this); + ffb_dir_layout->addWidget(ffb_dir_label); + m_ffb_direction_type = new QComboBox(this); + m_ffb_direction_type->addItem(tr("Steering Axis (Default)"), static_cast(g27_ffb_direction_type::steering_axis)); + m_ffb_direction_type->addItem(tr("Cartesian"), static_cast(g27_ffb_direction_type::cartesian)); + m_ffb_direction_type->addItem(tr("Polar"), static_cast(g27_ffb_direction_type::polar)); + m_ffb_direction_type->setToolTip(tr("Selects the direction encoding used for force feedback effects on the host wheel.\nSteering Axis is SDL's recommended encoding for steering wheels and the safest default.\nSwitch to Cartesian or Polar only if your wheel reports no force feedback with the default.")); + ffb_dir_layout->addWidget(m_ffb_direction_type); + v_layout->addLayout(ffb_dir_layout); + QHBoxLayout* compat_layout = new QHBoxLayout(this); compat_layout->setContentsMargins(0, 0, 0, 0); QLabel* compatibility_label = new QLabel(tr("Compatibility limit:"), this); @@ -880,6 +894,7 @@ void emulated_logitech_g27_settings_dialog::set_enable(bool enable) m_enabled->setEnabled(enable); m_reverse_effects->setEnabled(enable); + m_ffb_direction_type->setEnabled(enable); m_ffb_device->set_enable(enable); m_led_device->set_enable(enable); diff --git a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h index efd5cb589d..38900b2ba6 100644 --- a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h +++ b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h @@ -58,6 +58,7 @@ private: QCheckBox* m_enabled = nullptr; QCheckBox* m_reverse_effects = nullptr; + QComboBox* m_ffb_direction_type = nullptr; QComboBox* m_compatibility_limit = nullptr; std::map m_mappings;