fix(client/apple): acceleration was upside down — measured on glass
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.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user