diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/DsDevice.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/DsDevice.kt index af2f8a31..4aedf25c 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/DsDevice.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/DsDevice.kt @@ -17,6 +17,11 @@ package io.unom.punktfunk.kit * reaches this code — an uncaptured pad stays on the ordinary InputDevice path. */ object DsDevice { + /** The pads' native acceleration resolution — `hid-playstation`'s `DS_ACC_RES_PER_G`. */ + private const val DS_RAW_ACCEL_LSB_PER_G = 8192L + /** The wire's, from `punktfunk_core::input::gamepad::MOTION_ACCEL_LSB_PER_G`. */ + private const val WIRE_ACCEL_LSB_PER_G = 10000L + const val VID_SONY = 0x054C const val PID_DUALSENSE = 0x0CE6 const val PID_DUALSENSE_EDGE = 0x0DF2 @@ -153,7 +158,7 @@ object DsDevice { out.buttons = w if (len >= 28) { for (i in 0 until 3) out.gyro[i] = i16(r, 16 + 2 * i) - for (i in 0 until 3) out.accel[i] = i16(r, 22 + 2 * i) + for (i in 0 until 3) out.accel[i] = accelToWire(i16(r, 22 + 2 * i)) } if (len >= 41) { unpackTouch(r, 33, out, 0) @@ -189,7 +194,7 @@ object DsDevice { out.buttons = w if (len >= 25) { for (i in 0 until 3) out.gyro[i] = i16(r, 13 + 2 * i) - for (i in 0 until 3) out.accel[i] = i16(r, 19 + 2 * i) + for (i in 0 until 3) out.accel[i] = accelToWire(i16(r, 19 + 2 * i)) } if (len >= 43) { unpackTouch(r, 35, out, 0) @@ -227,6 +232,28 @@ object DsDevice { private fun i16(r: ByteArray, o: Int): Int = ((r[o + 1].toInt() shl 8) or (r[o].toInt() and 0xFF)).toShort().toInt() + /** + * Raw DualSense/DualShock 4 acceleration → the wire's units. + * + * The pad reports acceleration in its own device units; the wire is fixed at + * `MOTION_ACCEL_LSB_PER_G` = 10000 LSB per g (`punktfunk_core::input::gamepad`). Forwarding the + * raw value verbatim — which this path did until 2026-08-07 — hands the host a number ~18 % + * short, because the pad's native resolution is the 8192 LSB/g that `hid-playstation` calls + * `DS_ACC_RES_PER_G`. Measured on glass: a DualSense flat and face up arrived as 0.811 g where + * 1.000 was owed, against 8192/10000 = 0.819 predicted. + * + * The residual ~1 % is this unit's factory bias, which only its calibration feature report can + * remove — that read is still owed (it also fixes gyro, whose factory calibration is emphatically + * NOT near-identity and so cannot be corrected by a nominal constant like this one). + * + * Clamped because the rescale is a >1 multiplier: a real ±4 g slam near full scale would + * otherwise wrap the i16 and read as an impossible acceleration in the opposite direction. + */ + private fun accelToWire(raw: Int): Int = + ((raw.toLong() * WIRE_ACCEL_LSB_PER_G) / DS_RAW_ACCEL_LSB_PER_G) + .coerceIn(-32768L, 32767L) + .toInt() + // Device stick byte (0..255, centre 0x80, +y down) → wire i16 (+y up) — the exact inverse of // the host's `to_u8` mapping (`lx = to_u8(x)`, `ly = 255 - to_u8(y)`). private fun stickX(raw: Int): Int = raw * 257 - 32768 diff --git a/include/punktfunk_core.h b/include/punktfunk_core.h index 1e6d31c0..8c25b9b9 100644 --- a/include/punktfunk_core.h +++ b/include/punktfunk_core.h @@ -497,6 +497,25 @@ #define PUNKTFUNK_AXIS_RT 5 +// Motion wire units — the DualSense convention, raw `i16` LSBs, carried by +// `RichInput::Motion`. Gyro is angular velocity, accel is proper acceleration. +// +// Every capture path scales *into* these units (`pf-client-core::gamepad`, Swift +// `GamepadWire`, the Android `DeviceGyro`) and every host backend decodes *from* them — +// but the two sides never meet in one crate, which is how a virtual pad shipped for +// months telling its consumers to read the same bytes 40× too fast. The host's virtual +// pads carry fixed calibration blobs, and the resolution a consumer derives from those +// blobs must land back on exactly these numbers; pf-inject's `motion_contract` test is +// what pins that, for every backend, against these constants. +// +// Gyro saturates at `i16::MAX / 20` ≈ ±1638 °/s, below a real DualSense's ±2000; accel at +// ±3.28 g against its ±4 g. Lifting those is a wire-v2 question, not a scale to quietly +// re-tune here. +#define MOTION_GYRO_LSB_PER_DEG_S 20 + +// See [`MOTION_GYRO_LSB_PER_DEG_S`]. +#define MOTION_ACCEL_LSB_PER_G 10000 + // Identifies a punktfunk video packet (vs. an input datagram, see [`crate::input`]). #define PUNKTFUNK_MAGIC 201