The virtual pad wore one AUX node where a real DualSense shows a three-node split #259

Merged
enricobuehler merged 1 commits from worktree-pad-sink-real-topology into main 2026-08-15 22:39:51 +00:00
1 Commits
Author SHA1 Message Date
enricobuehler fb05145e36 The virtual pad wore one AUX node where a real DualSense shows a three-node split
ci / rust-arm64 (pull_request) Successful in 1m51s
ci / web (pull_request) Successful in 1m47s
ci / docs-site (pull_request) Successful in 1m54s
android / android (pull_request) Successful in 5m49s
ci / bun-nix (pull_request) Successful in 27s
ci / rust (pull_request) Successful in 6m10s
A game that renders DS5 haptics writes a POSITIONED FL/FR/RL/RR quad, because that
is the only public 4-channel surface a physically connected pad publishes. We minted
a single AUX0..AUX3 node wearing the mono sink's `Speaker__sink` name, so that write
was position-remixed on arrival and the coil pair folded away — measured on .181,
`peak_speaker=0.2441` with `peak_coils=0.0000`. The haptics were discarded silently:
nothing errored, nothing logged, the sink looked healthy.

Measured a real DS5 (054c:0ce6, USB) on a SteamOS 3.7 Deck running alsa-ucm-conf
1.2.14-2.4 and minted what it actually presents — a card's worth of nodes, not one:

  alsa_output.hw_punktfunkpad<N>_0                 Audio/Sink/Internal  4ch AUX0..AUX3
  …-<NN>.HiFi__SpeakerHaptic__sink                 Audio/Sink           4ch FL FR RL RR
  …-<NN>.HiFi__Speaker__sink                       Audio/Sink           1ch MONO

with the public pair naming the hidden parent in `api.alsa.split.name` (GE-Proton's
`pipewire:NODE=` haptic leg) and the parent naming itself, exactly as the specimen
does. Everything written to any of the three is summed onto one hardware quad, which
a real pad gets free from ALSA SplitPCM and we have to do by hand — GE drives the
haptic leg and the controller-effect leg AT ONCE by design, so emitting each node's
buffers straight into the chunk channel would interleave them and gap both halves.

The UCM also settles two things this file had wrong:

- The four hardware channels: `Headphones` takes Channel0 0/Channel1 1, `Speaker`
  takes Channel0 1, both haptic devices take Channel2 2/Channel3 3. So ch1 is the
  built-in mono speaker, and a mono write landing on ch0 (what a bare AUX node does)
  would have played controller effects into the headphone LEFT channel with the
  speaker silent.
- A plain DualSense's USB iProduct is "DualSense Wireless Controller", model word
  included. The `DualSense_` infix was dropped here to keep
  `Sony_Interactive_Entertainment_Wireless_Controller` contiguous — a property no
  real pad has either, so the name now carries the infix and drops the invented MAC
  (a real pad has no USB iSerialNumber; the trailing ALSA card index disambiguates).

`device.vendor.id`/`device.product.id` also gain the `0x` prefix the specimen
publishes. `strtol(s,_,16)` and `strtoul(s,_,0)` both yield 0x054c for "0x054c",
while the bare "054c" we published is parse-dependent — base 0 reads it as octal
054, stops at the `c`, and yields 44, matching nothing.

On glass on .181, index-exact in every leg (amplitudes encode the source channel):

  positioned coils-only → SpeakerHaptic__sink   speaker 0.0000  coils 0.4883   (was 0.0000)
  AUX coils-only        → parent                speaker 0.0000  coils 0.4883   (unchanged)
  positioned front-only → SpeakerHaptic__sink   speaker 0.3052  coils 0.0000
  mono                  → Speaker__sink         speaker 0.2747  coils 0.0000
  both legs concurrently                        speaker 0.2747  coils 0.4883

Three deliberate deviations from the specimen stay, each documented at the head of
the module: `node.description` keeps "Wireless Controller" (FF14/FF7R case-sensitive
`wcsstr`), `priority.session` stays low (our nodes come and go with pad arrival and
must never win a default-sink election), and `api.alsa.split.position` is not set
(it is WirePlumber's own management trigger).

Gate: clippy --all-targets -D warnings clean, 9/9 pad_sink tests, fmt clean, all in
punktfunk-rust-ci linux/amd64.
2026-08-16 00:31:56 +02:00