My racing wheel is old enough to vote, and under Linux it had never once pushed back. The Saitek R660GT dates from around 2007: entry level even when new, built in the Immersion TouchSense 2 generation, USB ID 06a3:ff32. Plug it into a Linux laptop and hid-generic delivers three axes out of the box: steering, brake, accelerator. Games see it, calibration works, you can race. The only missing piece was the feature with the best name in PC hardware: force feedback.

This is the story of how I got Spring and Damper effects working, crashed my kernel one time more than strictly necessary, and then proved, three separate ways, that the rest of the force feedback catalogue was never in this wheel to begin with.
Attempt one: the wheel fights back
The in-tree hid-universal-pidff driver carries a hardcoded allowlist of recent direct-drive wheelbases, and a 2007 Saitek is not on it. So I wrote a minimal module, about sixty lines, cloning the universal-pidff probe and calling the exported hid_pidff_init_with_quirks() with zero quirks. Load, bind, wait for magic.
What I got was a reproducible kernel oops: a NULL pointer dereference in hid_hw_open/hid_hw_power, address 0xa8, right after dmesg printed « Error initialising force feedback ». I reproduced it twice, once through a live driver rebind, which additionally wedged systemd-udevd in an unkillable D-state and forced a reboot, and once on a clean boot via autoload, same oops without the udev drama.
My written diagnosis at the time: a latent bug in the kernel’s shared PID code, not fixable from my wrapper. I blacklisted the module, kept the source for reference, and moved on with the quiet satisfaction of a man who has decided something is Not His Fault.
The wrong culprit
A counter-analysis later challenged that conclusion, so I went back to the primary sources and checked everything by hand. My little clone had faithfully inherited the error path of the code it copied: when force feedback init fails after hid_hw_start() has already run, the probe returned without calling hid_hw_stop(). That leaves a ghost hidraw node attached to a half-torn-down device, and the next process to open it, udevd in my case, walks straight into a use-after-free. This exact defect has a real LKML patch against hid-universal-pidff, titled with admirable directness: « HID: universal-pidff: stop the device when force-feedback init fails ».
The crash also sits in a known family of upstream bugs from the same corner of the kernel. CVE-2025-37862 covers a NULL dereference in pidff_find_fields() when an expected HID report is missing from the device. CVE-2026-23349 covers a NULL dereference through badly cleaned conditional-effect bits and explicitly affects kernels 6.18 to 7.0-rc7, a window my host kernel sat squarely inside. So: not a structural hardware problem, and not the kernel being cursed in general. A boring, fixable error path, already patched upstream, that my wrapper reproduced with complete fidelity. The kernel was not gaslighting me; my module had simply forgotten to clean up after itself, exactly like the code it was cloned from.
The fix is one line of discipline: when hid_pidff_init_with_quirks() fails, call hid_hw_stop(). I ported it, added the five PID quirks aimed at non-compliant hardware, and tested everything in an isolated VM with real USB passthrough. Result: no crash, udevd healthy, and on init failure the device cleanly unregisters and falls back to hid-generic, so steering and pedals survive no matter what. The crash risk was gone. Force feedback was gone too, still: « Error initialising force feedback (quirks=0x1f) », reproduced across two bind cycles. Safe failure achieved. Actual force, zero.
Plot twist: the wheel speaks I-Force
The breakthrough came from watching the professionals. I ran the real Windows Saitek driver in a virtual machine, with the wheel passed through, and watched it play effects with usbmon on the Linux side. The bytes on the wire, sequences starting 41/42/01/02/03/04, matched no report in the wheel’s HID descriptor. They did match, exactly, the I-Force opcodes documented in the kernel tree at Documentation/input/devices/iforce-protocol.rst: FF_CMD_EFFECT (0x01), FF_CMD_ENVELOPE (0x02), FF_CMD_CONDITION (0x05). The wheel’s real protocol is Immersion I-Force, not generic HID-PID. In hindsight the descriptor had been hinting at this all along: the PID Block Load, Block Free, and Create New Effect items are simply absent.
And here is the delightful part. The mainline iforce driver already knows a cousin of this wheel, the Saitek R440 Force Wheel (06a3:ff04). Same vendor, same protocol family, so a dynamic bind through new_id, with nothing recompiled, made the wheel enumerate as « Saitek R440 Force Wheel » with the full EV_FF capability list. I started a Spring effect and the wheel pulled itself back to center. Damper added a smooth resistance to rotation. Hardware old enough to vote, pushing back, on a stock mainline driver.
Honest caveats: the physical buttons do not surface through iforce, because that driver assumes the R440 button layout, and in practice only Spring and Damper produced any physical force at all. Hold that thought.
For the final deployment I went with the out-of-tree universal-pidff driver, patched and built via DKMS with a proper entry for 06a3:ff32 plus device quirks, because it keeps the buttons alongside the forces. The fix chain, in the order I found the problems: the err_stop patch above; a new MISSING_BLOCK_REPORTS quirk that fabricates the effect block index driver-side and pins it to a single slot; a MISSING_GAIN quirk, because the Set Effect report has no Gain field at all; a generic fix in pidff_find_special_field() for the Direction field, which this firmware refuses to wrap in its own logical collection; a MISSING_EBI quirk, because the envelope and condition reports use Parameter Block Offset where the driver expects an Effect Block Index; and a bypass for the device-managed-pool check, which the wheel honestly admits it cannot handle. On the host, the result survives a full physical unplug and replug and a reboot, rebinding automatically, no manual steps.
Three proofs it is the hardware
Constant force and Periodic effects (sine, square, and friends) produce exactly nothing on this wheel, on every path I tried: the patched PID driver, mainline iforce, and replayed Windows traffic. Before concluding anything, I crossed off the usual suspects: wrong effect block index (tried fixed slots), magnitude too low (tried up to 100 percent), periodic frequency too fast for the motor (tried 60ms and 300ms periods), a bug in one specific driver (two fully independent protocol implementations gave identical physical results). Then I closed the case with three proofs.
Act one, raw replay. I wrote the exact bytes the Windows driver had sent, captured via usbmon, straight to the hidraw node with no kernel driver involved. The spring-like sequence produced a real, felt force, the wheel pulling to the left, which proves the replay method itself works. The sequence explicitly tagged Constant Force (type byte 0x26, the standard HID PID usage ID for it), replayed byte for byte with the calibration line included, produced nothing. Confirmed twice.
Act two, the official panel. The Force tab of the official Immersion TouchSense control panel under Windows, the real signed vendor driver, not a generic one, exposes exactly four things: Default Gain, Spring Gain, Damper Gain, and Default Spring with an always-on checkbox. There is no Constant Force or Periodic setting anywhere in the manufacturer’s own interface. The people who built the wheel do not consider these features of the product.
Act three, the decompiler. I pointed Ghidra at saiQFF32.dll, the Immersion physical interface driver shipped with the wheel. The function building the Set Constant Force report (usage 0x70, Magnitude) and the one building the Set Condition report (usages 0x60 to 0x65, the spring and damper parameters) are structurally identical: same HidP_Set(Scaled)UsageValue calls, same memcmp cache against the last report sent, same WriteFile at the end. No stub, no special case, no silent skip. The vendor’s own compiled code treats Constant Force as a first-class effect and sends it faithfully. If it does and the wheel stays silent, the silence lives in the firmware and the motor, not in any driver, protocol, or operating system.
Conclusion, slightly humbling: this is a hardware and firmware limit, not a Linux bug. An entry-level TouchSense 2 wheel whose HID descriptor advertises more than the firmware implements. By this evidence Windows never had those effects either (its own driver sends them, and the wheel ignores the very same bytes); I had simply never checked before assuming Linux was the problem.
What works today
Everything this wheel can physically do, it now does on Linux: Spring for center return, Damper for rotation resistance, overall gain, and autocenter. Axes and buttons come along, and hotplug plus reboot leave the driver bound where it belongs.
Two games are configured and ready. SuperTuxKart has force feedback enabled, the device matched by exact name, axes mapped to steering, brake, and accelerator, and its startup log confirms the wheel with « buttons: 7, axes: 3, hats: 0 ». Speed Dreams (a Flatpak, after fixing a crash in a graphics library) runs with its global effect plus autocenter, which it implements internally through Spring, so it works on this hardware; its engine-revving and low-speed constant-force effects are disabled, preemptively, because those channels go nowhere. Along the way I also fixed a steering preset that had put left steer and right steer on the same axis with identical ranges, and a pedal mapping that had throttle and brake swapped.
Done, and still to do
Done: a patched driver deployed on the host through DKMS, surviving hotplug and reboots. Spring, Damper, gain, and autocenter physically confirmed, twice, with two independent drivers. Both games configured. And a three-way proof that Constant and Periodic effects are absent because the hardware never had them.
Still to do, honestly listed. Validate the pedal direction in a real Speed Dreams session under real conditions, since the fix currently rests on the SuperTuxKart mapping rather than a live test. Optionally do the upstream work properly: add 06a3:ff32 to the iforce USB ID table and to the universal-pidff allowlist, and send the patches, so the next owner of this wheel gets force feedback out of the box. Optionally integrate three newer upstream PIDFF quirks covering missing coefficient and deadband handling, aimed at exactly this generation of optimistic firmware. Not retained: a split setup with hid-generic for buttons and iforce for forces on two separate devices, which worked on paper and nowhere in my heart.
The lasting lesson, purchased for one kernel oops and a fair amount of stubbornness: the vendor’s own Windows driver is the best documentation of the hardware you will ever get, and sometimes the hardware simply cannot do what you assumed. My wheel was never withholding force feedback from Linux out of spite. It was withholding it from everyone.