Files
punktfunk/clients
enricobuehler 2032c48ffa
ci / web (pull_request) Successful in 1m19s
ci / docs-site (pull_request) Successful in 2m49s
ci / rust-arm64 (pull_request) Successful in 3m9s
android / android (pull_request) Failing after 4m23s
ci / rust (pull_request) Successful in 6m54s
apple / swift (pull_request) Successful in 1m24s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Failing after 2m22s
windows / build (x86_64-pc-windows-msvc) (pull_request) Failing after 55s
fix(android/pad-audio): a game that only rumbles keeps rumbling
Three faults that between them silence a wired DualSense.

The trade was committed without asking whether the host can send pad audio at
all. Against every released host — no HOST_CAP_PAD_AUDIO — the renderer claimed
the interface, took the pad off wire rumble, and then rendered nothing, with
`pad_haptics` defaulting on and no UI to turn it off. The capability is now
checked before `sink::open`, so nothing is claimed and nothing is traded.

Arming was unconditional, so a speaker-only setup took the motors away too. The
speaker pair is channels 0/1 and no rumble write can disturb it; only the
haptics lane arms now.

And the suppression itself was wrong for the case that matters most: a title
driving classic rumble and no haptics audio. Suppressing on "a stream is open"
assumed the game's rumble rides the haptics mix, which for such a title is
false — it renders no haptics audio at all, so the host's -60 dBFS gate emits
nothing on 0xD1 and the pad was left with neither. Ownership is now decided by
evidence: the coils belong to haptics only while haptics frames are actually
arriving, and to wire rumble otherwise. Frames are stamped on arrival rather
than after decode, so a decoder hiccup cannot hand the coils back mid-effect,
and concealment does not count as evidence. Liveness is dropped at every
teardown, because wire indices are recycled and a stale stamp would let a fresh
pad inherit the previous occupant's ownership.

Arbitrating on evidence rather than on a prediction about the hardware is
deliberate, and the module doc now says why. It used to assert that the coils
and the rumble motors are the same physical actuators — "a firmware constraint,
not a preference". Nothing establishes that: it traces to one reverse-engineered
comment in SDL, whose own modern path sets HAPTICS_SELECT alone with amplitude
on ucEnableBits3, which reads more like an independent mute than a shared-
actuator interlock. The combination that would settle it — rumble with
HAPTICS_SELECT cleared — is emitted by no code anywhere, and nothing here writes
it either. The evidence rule is correct under either hypothesis.

The liveness clock is 1-based so that 0 stays an unambiguous "never stamped":
without it a frame arriving in the process's first millisecond read as
never-arrived and handed the coils back mid-effect. Its test caught that.

Verified: clippy -p punktfunk-client-android --all-targets --locked -D warnings
= 0; 15 tests pass.

Owed: the desktop twin of the arbiter, and the coil restore — the Android stop
write still asserts HAPTICS_SELECT with zero amplitude, where SDL's all-zero
stop restores the audio path.

From the 2026-08-03 force-feedback sweep (B4, B5; B6 partly).
2026-08-03 19:44:52 +02:00
..