From 9e9bb9f4666b27874c7a56af738679574facce7b Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 7 Aug 2026 14:56:27 +0200 Subject: [PATCH] =?UTF-8?q?fix(client/apple):=20acceleration=20was=20upsid?= =?UTF-8?q?e=20down=20=E2=80=94=20measured=20on=20glass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G16, first result. A DualSense paired to an iPhone, streaming to a Linux host, lying flat and face up: hid-playstation decoded z = −0.99 g where a DualSense owes +1.00. Vector magnitude was 1.006 g, so the scale was already correct — this is purely direction, and it was wrong for every accelerometer sample the Apple client has ever sent. The cause is a convention mismatch, not a sign typo. Apple reports acceleration as the gravity VECTOR, which points down: a device face-up on a table reads z = −1. An accelerometer physically measures proper acceleration, and at rest that is the +1 g normal force pushing UP — which is what a DualSense's report, and therefore our wire, carries. The two are exact negatives. Both branches were affected, because `m.acceleration` follows the same Apple convention as the gravity/userAcceleration split, so reading the "raw vector" was not an escape from it. `rotationRate` is a true angular rate and needs no flip. The same session confirmed that independently: rotating the pad clockwise seen from above produced a negative yaw, which is correct under the right-hand rule about an up-pointing Z. That asymmetry — accel wrong, gyro right — is itself evidence for this diagnosis rather than a blanket frame error, and it is why the fix is three negations at one site instead of a remap. The sweep predicted this ("Apple accel plausibly INVERTED — CoreMotion gravity -1 g vs DS +1 g up at rest") but could not confirm it without hardware. It is now measured, and the mechanism is confirmed in the code rather than inferred from the number. Method, for whoever repeats it: the readout is python-evdev on the host reading the virtual pad's own motion node, dividing by the axis `resolution` the kernel publishes, so it prints deg/s and g. That is downstream of the calibration blob — the same layer a game reads — which is what makes a sign error visible to a human at all. Two things this does NOT establish. The host was a KVM guest, so the DualSense could not be attached natively for a side-by-side reference reading; the test stands on the DualSense convention being a fixed property of the hardware, which is decisive for the at-rest sign but weaker for the gyro axis ORDER. And the fix itself is unverified on glass: confirming it needs a rebuilt client on the device, so someone should re-run the same at-rest reading and see +1.00. Gate: `swiftc -parse` clean. A full typecheck needs the gitignored PunktfunkCore.xcframework assembled first and has not been run. --- .../PunktfunkKit/Gamepad/GamepadCapture.swift | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/clients/apple/Sources/PunktfunkKit/Gamepad/GamepadCapture.swift b/clients/apple/Sources/PunktfunkKit/Gamepad/GamepadCapture.swift index 4ea7812b..01cd7a47 100644 --- a/clients/apple/Sources/PunktfunkKit/Gamepad/GamepadCapture.swift +++ b/clients/apple/Sources/PunktfunkKit/Gamepad/GamepadCapture.swift @@ -564,18 +564,32 @@ public final class GamepadCapture { let now = DispatchTime.now().uptimeNanoseconds guard now &- slot.lastMotionNs >= Self.motionIntervalNs else { return } slot.lastMotionNs = now - // Total acceleration in g: gravity + user when split, else the raw vector. + // Total acceleration in g: gravity + user when split, else the raw vector — then NEGATED + // into the wire's convention. + // + // Apple reports acceleration as the gravity VECTOR: a device lying flat face-up reads + // z = −1, because gravity points down. An accelerometer physically measures proper + // acceleration, which at rest is the +1 g normal force pushing UP, and that is what a + // DualSense's report — the wire's convention — carries. The two are exact negatives, so + // every sample we sent was upside down, on both branches (`m.acceleration` follows the + // same Apple convention as the gravity/user split). + // + // Measured on glass 2026-08-07 (G16): a DualSense flat and face-up, streamed from an + // iPhone to a Linux host, arrived at hid-playstation as z = −0.99 g where +1.00 was owed. + // Magnitude was 1.006 g, so the SCALE was already right — this is purely direction. + // `rotationRate` is a true angular rate and needs no flip; the same session confirmed yaw + // came through with the correct sign. let ax: Float let ay: Float let az: Float if m.hasGravityAndUserAcceleration { - ax = Float(m.gravity.x + m.userAcceleration.x) - ay = Float(m.gravity.y + m.userAcceleration.y) - az = Float(m.gravity.z + m.userAcceleration.z) + ax = -Float(m.gravity.x + m.userAcceleration.x) + ay = -Float(m.gravity.y + m.userAcceleration.y) + az = -Float(m.gravity.z + m.userAcceleration.z) } else { - ax = Float(m.acceleration.x) - ay = Float(m.acceleration.y) - az = Float(m.acceleration.z) + ax = -Float(m.acceleration.x) + ay = -Float(m.acceleration.y) + az = -Float(m.acceleration.z) } let gs = GamepadWire.gyroLSBPerRadS let as_ = GamepadWire.accelLSBPerG