-
Notifications
You must be signed in to change notification settings - Fork 8
Protocol Force Feedback
How force reaches the motor: the dedicated interface-2 endpoint stream, the packet encoding, and the driver's software effect engine. Part of the Protocol Specification; the audio-haptic layer that shares this endpoint is in TrueForce Protocol.
Endpoint 0x03 OUT (interface 2) carries one 64-byte type-0x01 packet
family. Byte 10, the "new samples this packet" count, and byte 11, its
0x0d marker, demultiplex it as a pair:
-
0x00/0x00: a pure constant-force ("KF") update, documented here. - nonzero/
0x0d: a unified force+audio ("TF") packet whose bytes 12-63 carry a rolling haptic sample window on top of the same force field; see TrueForce Protocol for the full framing.
The pair is strict: byte 10 = 0x04 with byte 11 = 0x00 never occurs on
the wire, and the wheel silently discards such a packet's samples.
Offset Size Type Description
------ ---- ---- -----------
0 1 u8 Report ID (0x01)
1-3 3 - Reserved (0x00 0x00 0x00)
4 1 u8 Command type (0x01 = stream/sample packet)
5 1 u8 Sequence counter (0x00-0xFF, wraps)
6-7 2 u16 LE Force value / motor torque target ("cur")
8-9 2 u16 LE Duplicate (must match bytes 6-7)
10 1 u8 New-sample count (0 here; nonzero = TF audio packet)
11 1 u8 Sample marker (0x00 here; 0x0d in a TF audio packet)
12-63 52 - Zero for a constant-force packet
Bytes 6-9 are the motor torque target ("cur" in TrueForce terms) and are honoured whether or not the packet also carries audio: a constant-force packet is a TF stream packet with an empty sample window. The sequence counter is a single byte wrapping at 255 (unlike the two-byte HID++ sequence) and is shared across all interface-2 packet types.
| Value | Force |
|---|---|
0x0000 |
maximum LEFT |
0x8000 |
neutral |
0xFFFF |
maximum RIGHT |
uint16_t offset_binary = (int16_t)signed_value + 0x8000;Games stream at 1000 Hz on Windows (median inter-packet gap ~1.0 ms in every
gameplay capture), and the kernel driver's own force stream matches that,
measured at 1000.2 Hz with a median period of 1.000 ms. It uses an hrtimer:
a self-rearming jiffies timer cannot hold this rate, because the timer wheel
never fires early, so every period comes back one jiffy long.
There is no idle keepalive: the endpoint is silent when no effect plays.
(An earlier revision documented an 05 07 "refresh" packet; capture
re-analysis proved that was a DualShock 4 lightbar report to a different
device that was plugged in at capture time, and the driver does not send it.)
The endpoint is an interrupt OUT with a 1 ms interval, so it carries one
packet per millisecond in total, not one per writer. Two senders at
1 kHz therefore take turns on it, one frame each, and since bytes 6-9 are a
level the wheel holds rather than an event, the motor then alternates
between their two targets every millisecond: a 500 Hz square wave, plus each
side's samples at half rate. Measured on an RS50 with the driver and a
userspace producer both streaming, with the before/after numbers and the
audible signature, in the repo's docs/TRUEFORCE_PROTOCOL.md ("One writer
at a time").
The rule that follows: take the stream, do not join it. The driver
implements its half of that. It detects a userspace writer on interface 2
and stops sending packets of its own (stream_yield, on by default), and
while yielding it does not go silent: when it has an effect of its own
running it writes that net force into bytes 6-9 of the owner's packet on the
way past, so one packet carries the owner's samples and the driver's force.
With no effect running it leaves those bytes alone, because a native SDK
session puts the game's force there and overwriting it would trade force
feedback for dead centre. Two userspace programs get no such help from the
endpoint and must arbitrate between themselves; the ones this project ships
do it with a per-wheel lease (see Simulated TrueForce).
The wheel firmware only understands raw constant forces on this endpoint, so the driver emulates the complete Linux FF effect set on top of it:
-
FF_CONSTANT,FF_RAMP,FF_PERIODIC(sine, square, triangle, saw up, saw down), and the four condition effectsFF_SPRING,FF_DAMPER,FF_FRICTION,FF_INERTIA. - A 1 kHz hrtimer walks the active-effect slots, sums each effect's
instantaneous contribution, applies
FF_GAIN, and sends one net force value. The timer keeps running whenever any effect is playing, even if the current net force is zero (a spring at exact centre must push the moment the wheel moves). - Condition effects read the live wheel state (position from the interface-0
report, velocity and acceleration derived at the tick) and apply the
standard
ff_condition_effectformula; waveform and envelope semantics matchDocumentation/input/ff.rst. - Emulated springs get synthetic damping (
wheel_spring_damping) because an undamped host-side spring on a low-friction direct-drive motor rings; see the Sysfs API Reference. - Vibration-class effects (rumble, periodics at 20 Hz or faster) can route
to the TrueForce audio channel instead of the steering sum
(
wheel_texture_route), matching the Windows KF/TF split. - The driver can also merge a telemetry-driven engine texture into a game's
own live TrueForce stream (
wheel_tf_merge), stamping the byte-11 sample marker so the wheel renders it. This is the same merge G HUB's stack performs on Windows for Assetto Corsa EVO, which drives force through the SDK but produces no texture itself; see the Sysfs API Reference. - FFB must be sent from process context (workqueue); the driver uses
hid_hw_output_report()on interface 2.
The direct-drive wheels expose two independent force paths, and which one a host uses is a choice, not a hardware constraint:
-
HID++ feature
0x8123fn2: the path Logitech's own Windows runtime uses for normal game FFB on these wheels. Report 0x11/0x12, device index 0xFF, signed int16 BE motor target at payload offset 10-11, sent at the game's rate (~140-333 Hz observed). Set-and-hold: the wheel maintains the last commanded force indefinitely with no keepalive. - Endpoint 0x03 (this page): the TrueForce session channel, used by SDK-native games and by this Linux driver for ALL force output. While a TrueForce session is active, the packet's "cur" field OVERRIDES the 0x8123 path, and the sample window plays additively on top.
The G920/G923 comparison in the Protocol Specification therefore describes defaults, not capabilities: the older wheels have only 0x8123, while the DD wheels have both. A minimal 0x8123-fn2 sender remains an untested alternative transport for this driver (potentially relevant if the wheel's FFB-filter smoothing turns out to apply only to that path); the G920-class slot-based 0x8123 engine, however, saturates the queue on these wheels and cannot be reused as-is.
#define HIDPP_DD_FF_REPORT_ID 0x01
#define HIDPP_DD_FF_REPORT_SIZE 64
struct hidpp_dd_ff_report {
u8 report_id; /* 0x01 */
u8 reserved[3]; /* 0x00, 0x00, 0x00 */
u8 effect_type; /* 0x01 = constant force */
u8 sequence; /* 0x00-0xFF, wraps */
__le16 force; /* 0x0000=left, 0x8000=center, 0xFFFF=right */
__le16 force_dup; /* duplicate of force value */
u8 padding[54]; /* zeros */
} __packed;These wheels have no hardware autocenter setting (G HUB exposes none); the
driver provides a real host-side centring spring instead, driven by the
autocenter sysfs attribute or the standard evdev FF_AUTOCENTER control,
summed in the same 1 kHz loop.