You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On a ROG Ally X, no InputPlumber target gives SDL applications usable motion data, although the IMU source
itself is fine (a DSU bridge reading the same IIO device works). Four independent problems, all still present
in v0.80.0:
Steam Deck targets (deck, deck-uhid): accelerometer not scaled. Values in m/s² are written with x as i16, so gravity reads about ±10 instead of about ±16384. Any sensor fusion (tilt, steering) fails.
Steam Deck targets: axes not remapped. SDL reads the Deck report as (sX, sZ, -sY), so writing
InputPlumber's (x, y, z) unchanged rotates the data 90° about X after SDL.
DualSense targets: fake sensor timestamp.sensor_timestamp advances by 3 (= 1 µs) on every write_event, not with real time. SDL forwards it as the sensor timestamp, so apps that integrate the gyro
with it integrate over microseconds and effectively ignore the gyro.
DualSense targets: calibration feature report is one byte short in the bias block. SDL rejects the
calibration and falls back to defaults.
Related: #262 and draft PR #612 (SI normalization). #612 fixes the scale but not problems 2–4; details below.
Environment
Device: ASUS ROG Ally X, IMU bmi323-imu via IIO (200 Hz)
(the gyro uses the same (X, Z, -Y) order, with 32768 = 2000 °/s). SDL_hidapi_ps5.c reads the DualSense
unrotated, which is why the same (x, y, z) is correct for DualSense but not for Deck.
Measured from the raw deck-uhid hidraw report (type 9, bytes 24–35), 10 s of shaking and rotating, 4451 reports:
acc X min= -11 max= 8 <- about 1600x too small
acc Y min= -9 max= 16
acc Z min= -27 max= 32
gyro X min=-9227 max= 7301 <- gyro data present
Tested fix. Rescaling the accelerometer and writing both sensors as (X, -Z, Y) in all four arms:
Gamepad::Accelerometer => {
if let InputValue::Vector3 { x, y, z } = value {
+ // Steam Deck IMU axes are (X, -Z, Y) relative to InputPlumber's (X, Y, Z).
if let Some(x) = x {
- self.state.accel_x = Integer::from_primitive(x as i16);+ self.state.accel_x = Integer::from_primitive(deck_accel_value(x));
}
- if let Some(y) = y {- self.state.accel_y = Integer::from_primitive(y as i16);- }
if let Some(z) = z {
- self.state.accel_z = Integer::from_primitive(z as i16);+ self.state.accel_y = Integer::from_primitive(deck_accel_value(-z));+ }+ if let Some(y) = y {+ self.state.accel_z = Integer::from_primitive(deck_accel_value(y));
}
}
}
Gamepad::Gyro => {
if let InputValue::Vector3 { x, y, z } = value {
+ // Steam Deck IMU axes are (X, -Z, Y) relative to InputPlumber's (X, Y, Z).
if let Some(x) = x {
self.state.pitch = Integer::from_primitive(x as i16);
}
- if let Some(y) = y {- self.state.yaw = Integer::from_primitive(y as i16);- }
if let Some(z) = z {
- self.state.roll = Integer::from_primitive(z as i16);+ self.state.yaw = Integer::from_primitive((-z) as i16);+ }+ if let Some(y) = y {+ self.state.roll = Integer::from_primitive(y as i16);
}
}
}
/// Steam Deck accelerometer: 16384 units per G. InputPlumber reports m/s².fndeck_accel_value(value_meters_sec:f64) -> i16{(value_meters_sec / 9.80665*16384.0)asi16}
Built from the v0.79.0 tag and verified on the device with static poses (raw deck-uhid report):
Pose
Expected (SDL convention)
Measured
Flat, screen up
acc Y ≈ −16384
acc Y = −16545
Upright, screen facing user
acc Z ≈ +16384
acc Z = +16350
Right edge up
acc X ≈ +16384
acc X = +16230
These now match the DualSense convention after SDL.
Two further notes, from source reading and not fixed by the patch above:
Gyro scale.bmi_imu.rs (lines 112–121) intentionally provides °/s × 12, while the Deck report expects
32768 / 2000 = 16.384 LSB per °/s, so after SDL the gyro is about 73% of the real rate.
Report cadence. The Deck report has no sensor timestamp; SDL assumes update_rate_us = 4000 per report.
I measured about 143 reports/s on deck (vhci) and about 440 reports/s on deck-uhid, so SDL integrates the
gyro with the wrong dt either way. For motion to be correct through SDL, the target has to send a state report
every 4 ms.
This runs once per write_event, i.e. per input event. SDL (SDL_hidapi_ps5.c) treats the 32-bit field as
0.33 µs units and passes the accumulated value to SDL_SendJoystickSensor. Eden
(src/input_common/drivers/sdl_driver.cpp) uses the difference between consecutive sensor timestamps as the
motion dt, so the gyro is integrated over ~1 µs per report and steering comes almost only from gravity
(in Mario Kart 8 the device had to be tilted about 90° to steer fully).
Suggested fix: derive the field from a monotonic clock at report time, e.g. (start.elapsed().as_micros() * 3) as u32.
4. DualSense targets: calibration report 0x05
src/input/target/dualsense.rs line 864 returns:
05 | ff fc ff fe ff | 83 22 78 dd 92 22 5f dd 95 22 6d dd | 1c 02 1c 02 | f2 1f ed df e3 20 da e0 ee 1f df df | 0b 00 ...
SDL (HIDAPI_DriverPS5_LoadCalibrationData) and hid-playstation read three 16-bit gyro biases at bytes 1–6
and the gyro plus/minus values from byte 7. Here the bias block has only 5 bytes, so byte 6 (0x83) completes
the roll bias (0x83ff = −31745) and every later field is read one byte off. SDL flags the calibration as bad
and uses its defaults; combined with the °/s × 12 source scale the gyro ends up at about 75%.
Suggested fix: make the bias block 6 bytes. Since the virtual device already sends bias-free data, zero biases
seem right: 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x22, 0x78, 0xdd, ....
Also: the Capability::Accelerometer(_) arm (line 652) writes x as i16 without denormalize_accel_value,
unlike the Gamepad::Accelerometer arm (line 559).
Deck constants: DECK_SI_TO_ACCEL = 1632.65 is 16011 LSB/g, but SDL uses 16384 LSB/g
(32768 / (2 · 9.80665) = 1670.7 per m/s²). DECK_RADS_TO_GYRO = 916.73 is 16.0 LSB per °/s; SDL uses
16.384 (938.7 per rad/s). Both are about 2.4% low.
The Deck targets still write x → accel_x, y → accel_y, z → accel_z, so problem 2 remains.
The DualSense timestamp and calibration report are not touched.
Impact
SteamOS also drives third-party handheld controllers through InputPlumber's Deck targets via steamos-manager
(ValveSoftware/SteamOS#1891, #1908), so SteamOS users on these devices are likely affected too.
Happy to test builds on the Ally X.
Disclosure: this investigation and write-up, including the proposed patch, were done with the help of Claude (Anthropic). I built and tested the patch and took the measurements on my ROG Ally X.
Summary
On a ROG Ally X, no InputPlumber target gives SDL applications usable motion data, although the IMU source
itself is fine (a DSU bridge reading the same IIO device works). Four independent problems, all still present
in v0.80.0:
deck,deck-uhid): accelerometer not scaled. Values in m/s² are written withx as i16, so gravity reads about ±10 instead of about ±16384. Any sensor fusion (tilt, steering) fails.(sX, sZ, -sY), so writingInputPlumber's
(x, y, z)unchanged rotates the data 90° about X after SDL.sensor_timestampadvances by 3 (= 1 µs) on everywrite_event, not with real time. SDL forwards it as the sensor timestamp, so apps that integrate the gyrowith it integrate over microseconds and effectively ignore the gyro.
calibration and falls back to defaults.
Related: #262 and draft PR #612 (SI normalization). #612 fixes the scale but not problems 2–4; details below.
Environment
bmi323-imuvia IIO (200 Hz)44.20260908), InputPlumber0.79.0-4; code checked unchanged inv0.80.01 + 2. Steam Deck targets: accelerometer scale and axes
Code (v0.80.0):
src/input/target/steam_deck.rslines 681–705 (Gamepad::Accelerometer/Gamepad::Gyro)and 772–796 (
Capability::Gyroscope(_)/Capability::Accelerometer(_));src/input/target/steam_deck_uhid.rslines 286–310 and 377–401.
SDL3
src/joystick/hidapi/SDL_hidapi_steamdeck.c:(the gyro uses the same
(X, Z, -Y)order, with 32768 = 2000 °/s).SDL_hidapi_ps5.creads the DualSenseunrotated, which is why the same
(x, y, z)is correct for DualSense but not for Deck.Measured from the raw
deck-uhidhidraw report (type 9, bytes 24–35), 10 s of shaking and rotating, 4451 reports:Tested fix. Rescaling the accelerometer and writing both sensors as
(X, -Z, Y)in all four arms:Gamepad::Accelerometer => { if let InputValue::Vector3 { x, y, z } = value { + // Steam Deck IMU axes are (X, -Z, Y) relative to InputPlumber's (X, Y, Z). if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(x as i16); + self.state.accel_x = Integer::from_primitive(deck_accel_value(x)); } - if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(y as i16); - } if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(z as i16); + self.state.accel_y = Integer::from_primitive(deck_accel_value(-z)); + } + if let Some(y) = y { + self.state.accel_z = Integer::from_primitive(deck_accel_value(y)); } } } Gamepad::Gyro => { if let InputValue::Vector3 { x, y, z } = value { + // Steam Deck IMU axes are (X, -Z, Y) relative to InputPlumber's (X, Y, Z). if let Some(x) = x { self.state.pitch = Integer::from_primitive(x as i16); } - if let Some(y) = y { - self.state.yaw = Integer::from_primitive(y as i16); - } if let Some(z) = z { - self.state.roll = Integer::from_primitive(z as i16); + self.state.yaw = Integer::from_primitive((-z) as i16); + } + if let Some(y) = y { + self.state.roll = Integer::from_primitive(y as i16); } } }Built from the v0.79.0 tag and verified on the device with static poses (raw
deck-uhidreport):These now match the DualSense convention after SDL.
Two further notes, from source reading and not fixed by the patch above:
bmi_imu.rs(lines 112–121) intentionally provides °/s × 12, while the Deck report expects32768 / 2000 = 16.384 LSB per °/s, so after SDL the gyro is about 73% of the real rate.
update_rate_us = 4000per report.I measured about 143 reports/s on
deck(vhci) and about 440 reports/s ondeck-uhid, so SDL integrates thegyro with the wrong dt either way. For motion to be correct through SDL, the target has to send a state report
every 4 ms.
deck-uhiduses PID0x12FD, which SDL does not treat as a Deck (only0x1205). Understood from deck target issues (was IMU Support for Steam Deck (on Gentoo)) #433 thatthis is intentional; mentioning it because it means only
deckcan carry motion to SDL apps.3. DualSense targets: sensor timestamp
src/input/target/dualsense.rsline 974 (v0.80.0):This runs once per
write_event, i.e. per input event. SDL (SDL_hidapi_ps5.c) treats the 32-bit field as0.33 µs units and passes the accumulated value to
SDL_SendJoystickSensor. Eden(
src/input_common/drivers/sdl_driver.cpp) uses the difference between consecutive sensor timestamps as themotion dt, so the gyro is integrated over ~1 µs per report and steering comes almost only from gravity
(in Mario Kart 8 the device had to be tilted about 90° to steer fully).
Suggested fix: derive the field from a monotonic clock at report time, e.g.
(start.elapsed().as_micros() * 3) as u32.4. DualSense targets: calibration report 0x05
src/input/target/dualsense.rsline 864 returns:SDL (
HIDAPI_DriverPS5_LoadCalibrationData) andhid-playstationread three 16-bit gyro biases at bytes 1–6and the gyro plus/minus values from byte 7. Here the bias block has only 5 bytes, so byte 6 (
0x83) completesthe roll bias (
0x83ff= −31745) and every later field is read one byte off. SDL flags the calibration as badand uses its defaults; combined with the °/s × 12 source scale the gyro ends up at about 75%.
Suggested fix: make the bias block 6 bytes. Since the virtual device already sends bias-free data, zero biases
seem right:
0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x22, 0x78, 0xdd, ....Also: the
Capability::Accelerometer(_)arm (line 652) writesx as i16withoutdenormalize_accel_value,unlike the
Gamepad::Accelerometerarm (line 559).Notes on draft PR #612
DECK_SI_TO_ACCEL = 1632.65is 16011 LSB/g, but SDL uses 16384 LSB/g(32768 / (2 · 9.80665) = 1670.7 per m/s²).
DECK_RADS_TO_GYRO = 916.73is 16.0 LSB per °/s; SDL uses16.384 (938.7 per rad/s). Both are about 2.4% low.
x → accel_x, y → accel_y, z → accel_z, so problem 2 remains.Impact
SteamOS also drives third-party handheld controllers through InputPlumber's Deck targets via
steamos-manager(ValveSoftware/SteamOS#1891, #1908), so SteamOS users on these devices are likely affected too.
Happy to test builds on the Ally X.
Disclosure: this investigation and write-up, including the proposed patch, were done with the help of Claude (Anthropic). I built and tested the patch and took the measurements on my ROG Ally X.