fix(client/apple): the phone-gyro mirror was left on the old motion convention
ci / docs-site (pull_request) Successful in 1m3s
apple / swift (pull_request) Successful in 1m37s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 1m30s
windows-drivers / driver-build (pull_request) Successful in 1m39s
ci / web (pull_request) Successful in 2m5s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 3m21s
android / android (pull_request) Successful in 4m54s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m6s
ci / rust (pull_request) Successful in 9m4s
windows-drivers / probe-and-proto (pull_request) Failing after 10m10s
ci / docs-site (pull_request) Successful in 1m3s
apple / swift (pull_request) Successful in 1m37s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 1m30s
windows-drivers / driver-build (pull_request) Successful in 1m39s
ci / web (pull_request) Successful in 2m5s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 3m21s
android / android (pull_request) Successful in 4m54s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m6s
ci / rust (pull_request) Successful in 9m4s
windows-drivers / probe-and-proto (pull_request) Failing after 10m10s
Surfaced by the merge. `DeviceGyro`'s header states the contract plainly — "units and axis semantics match `GamepadCapture.forwardMotion` exactly … the same convention, so a future sign/scale correction lands in one place for both sources" — and this branch made two such corrections in only one of the two places. That is a promise the code stopped keeping the moment the controller path was fixed. Both were true parity when #88 was written; both broke here. **The negation.** `GamepadCapture` sends `-(gravity + userAcceleration)` because Apple reports the gravity VECTOR, pointing down, while an accelerometer measures proper acceleration, pointing up at rest — and the wire carries the latter. The mirror sent it un-negated, so a phone lying still told the host it was accelerating downward at 1 g. The comment above that line even claimed the convention matched. **The frame.** The mirror's remap targets the controller frame its own header describes — x right, y up, z out of the screen — which is exactly GameController's frame, and that is not the DualSense report frame the wire is defined in. So the same change of basis the controller path now takes applies here, after the orientation remap rather than instead of it: the remap resolves which way the phone is being held, and the basis change translates the result into the pad's language. Two different jobs that happen to compose. Order matters for the closing sample too. `stop` replays `lastAccel` beside a zero gyro so "rotation stopped" does not also read as free fall; `lastAccel` is recorded after both conversions, so what gets parked is what was actually sent. Left alone deliberately: `DeviceGyroRemap` itself and `DeviceGyroRemapTests`. The orientation matrices answer a different question — which way is the phone being held — and nothing measured this evening bears on them. They remain derived-not-verified, as their own doc says, and the on-glass pass that owes the controller path a check owes them one too, in all four orientations. Gate: macOS `swift build` + full suite (215 tests, 5 skipped, 0 failures) and the iOS-triple typecheck green — the latter is what actually compiles this file, since the whole thing is `#if os(iOS)`.
This commit is contained in:
@@ -174,22 +174,37 @@ public final class DeviceGyro {
|
||||
state.lock.unlock()
|
||||
let rot = r.apply(
|
||||
x: Float(m.rotationRate.x), y: Float(m.rotationRate.y), z: Float(m.rotationRate.z))
|
||||
// Same total-acceleration convention as GamepadCapture.forwardMotion.
|
||||
// Total acceleration, NEGATED — the same convention as GamepadCapture.forwardMotion, which
|
||||
// this file's header promises to track. Apple reports the gravity VECTOR (pointing down);
|
||||
// an accelerometer measures proper acceleration (pointing up at rest), and the wire carries
|
||||
// the latter. Without the minus a still phone told the host it was accelerating downward at
|
||||
// 1 g.
|
||||
let acc = r.apply(
|
||||
x: Float(m.gravity.x + m.userAcceleration.x),
|
||||
y: Float(m.gravity.y + m.userAcceleration.y),
|
||||
z: Float(m.gravity.z + m.userAcceleration.z))
|
||||
x: -Float(m.gravity.x + m.userAcceleration.x),
|
||||
y: -Float(m.gravity.y + m.userAcceleration.y),
|
||||
z: -Float(m.gravity.z + m.userAcceleration.z))
|
||||
// Then the SAME change of basis the controller path takes. `r` puts the sample in the
|
||||
// controller frame this file's header describes — x right, y up, z out of the screen —
|
||||
// which is exactly GameController's frame, and that is not the DualSense report frame the
|
||||
// wire is defined in. Measured 2026-08-07; see `GamepadWire.appleMotionToWire`.
|
||||
//
|
||||
// Both corrections are here because the header states the intent plainly: units and axis
|
||||
// semantics match `GamepadCapture.forwardMotion` so a sign/scale fix lands in one place for
|
||||
// both sources. Fixing only the controller path would have left this one silently on the
|
||||
// old convention — a mirror that disagrees with the thing it mirrors.
|
||||
let g = GamepadWire.appleMotionToWire((rot.x, rot.y, rot.z))
|
||||
let a = GamepadWire.appleMotionToWire((acc.x, acc.y, acc.z))
|
||||
let gs = GamepadWire.gyroLSBPerRadS
|
||||
let as_ = GamepadWire.accelLSBPerG
|
||||
let gyro = (
|
||||
GamepadWire.motionRaw(rot.x, scale: gs),
|
||||
GamepadWire.motionRaw(rot.y, scale: gs),
|
||||
GamepadWire.motionRaw(rot.z, scale: gs)
|
||||
GamepadWire.motionRaw(g.0, scale: gs),
|
||||
GamepadWire.motionRaw(g.1, scale: gs),
|
||||
GamepadWire.motionRaw(g.2, scale: gs)
|
||||
)
|
||||
let accel = (
|
||||
GamepadWire.motionRaw(acc.x, scale: as_),
|
||||
GamepadWire.motionRaw(acc.y, scale: as_),
|
||||
GamepadWire.motionRaw(acc.z, scale: as_)
|
||||
GamepadWire.motionRaw(a.0, scale: as_),
|
||||
GamepadWire.motionRaw(a.1, scale: as_),
|
||||
GamepadWire.motionRaw(a.2, scale: as_)
|
||||
)
|
||||
state.lock.lock()
|
||||
state.lastAccel = accel
|
||||
|
||||
Reference in New Issue
Block a user