Closes the last hole in DualSense support on Linux hosts: the 0xD1 pad-audio plane was Windows-only — host_cap() answered false and spawn() was a stub off-Windows, so a tier-A client (Android USB DS5) against a Linux host negotiated the cap off and stayed on wire rumble.
What ships
audio/linux/pad_sink.rs — one Audio/Sink stream node per DualSense-family pad, minted with the identity the matchers read: ALSA-style node.name carrying the pad's pairing MAC, node.description "Wireless Controller", device.bus/vendor.id/product.id/form_factor proplist, per-pad serial. No default-sink claim, priority.session 50. The process() callback is the capture (we own the sink). PUNKTFUNK_PAD_SINK_NAME/_DESC override the strings for field debugging ({pad}/{mac} expand).
native/pad_audio.rs — the framer/gate/lane machinery compiles on Linux; the streamer thread is generic over the capturer (same open-with-backoff); Linux host_cap = client asked + PUNKTFUNK_PAD_AUDIO≠0 + a reachable PipeWire socket; Linux spawn mints the sink lazily in-thread. spawn gains an edge flag for the Edge identity.
pad-sink-test devtest — mints one sink and captures from it with per-pair peak reporting (speaker pair vs coil pair), no client needed.
Docs: PUNKTFUNK_PAD_AUDIO{,_SLOTS} lose the "(Windows)" marker; the roadmap non-goal narrows to Bluetooth client pads.
The AUX0–3 channel shape (second commit)
A field report ("GE-Proton 11-5 fixed DualSense audio — you only need the controller audio set to Pro Audio") led into GE's proton-ds5-haptic patch set: GE ≥11-4 opens the pad's node through its bundled pipewire-alsa plugin as pipewire:NODE=<name> with aux_channels=1, and its pulse fallback forces a PA AUX0..3 map with stream.dont-remix — "the hidden PipeWire parent for a DualSense output exposes AUX0 through AUX3". Pro Audio is what makes a real pad present that shape; our sink mints it directly, so no profile step exists for users. A positioned FL FR RL RR sink would put those writers through position channelmix instead of index passthrough.
Also confirmed from the patch set: device matching is bus/vid/pid proplist or the Sony name substrings (we carry both), and on GE ≥11-4 the MMDevice ContainerId is synthesized from the wine-side HID USB parent — the pure-PipeWire-node GUID_NULL risk from the design doc no longer applies.
Validation (on-glass, Bazzite 44 / .41)
cargo build --release --locked (nvenc+vulkan-encode), clippy --all-targets -D warnings, pad_audio+pad_sink tests 11/11, fmt — in a fedora:44 container on the box.
Sink serves the full identity through pipewire-pulse; an AUX-mapped rear-pair-only tone captures index-exact (speaker pair 0.0000, coil pair 0.3662 = bit-exact input level) over both the native and pulse legs; a positioned stray stream folds into the speaker pair and never excites the coils.
The box currently runs this branch via a systemd drop-in for the upcoming Android-client field test.
Closes the last hole in DualSense support on Linux hosts: the 0xD1 pad-audio plane was Windows-only — `host_cap()` answered `false` and `spawn()` was a stub off-Windows, so a tier-A client (Android USB DS5) against a Linux host negotiated the cap off and stayed on wire rumble.
## What ships
- **`audio/linux/pad_sink.rs`** — one `Audio/Sink` stream node per DualSense-family pad, minted with the identity the matchers read: ALSA-style `node.name` carrying the pad's pairing MAC, `node.description "Wireless Controller"`, `device.bus/vendor.id/product.id/form_factor` proplist, per-pad serial. No default-sink claim, `priority.session 50`. The `process()` callback *is* the capture (we own the sink). `PUNKTFUNK_PAD_SINK_NAME/_DESC` override the strings for field debugging (`{pad}`/`{mac}` expand).
- **`native/pad_audio.rs`** — the framer/gate/lane machinery compiles on Linux; the streamer thread is generic over the capturer (same open-with-backoff); Linux `host_cap` = client asked + `PUNKTFUNK_PAD_AUDIO`≠0 + a reachable PipeWire socket; Linux `spawn` mints the sink lazily in-thread. `spawn` gains an `edge` flag for the Edge identity.
- **`pad-sink-test` devtest** — mints one sink and captures from it with per-pair peak reporting (speaker pair vs coil pair), no client needed.
- Docs: `PUNKTFUNK_PAD_AUDIO{,_SLOTS}` lose the "(Windows)" marker; the roadmap non-goal narrows to Bluetooth client pads.
## The AUX0–3 channel shape (second commit)
A field report ("GE-Proton 11-5 fixed DualSense audio — you only need the controller audio set to Pro Audio") led into GE's `proton-ds5-haptic` patch set: GE ≥11-4 opens the pad's node through its bundled pipewire-alsa plugin as `pipewire:NODE=<name>` with `aux_channels=1`, and its pulse fallback forces a `PA AUX0..3` map with `stream.dont-remix` — "the hidden PipeWire parent for a DualSense output exposes AUX0 through AUX3". Pro Audio is what makes a *real* pad present that shape; our sink mints it directly, so no profile step exists for users. A positioned FL FR RL RR sink would put those writers through position channelmix instead of index passthrough.
Also confirmed from the patch set: device matching is bus/vid/pid proplist **or** the Sony name substrings (we carry both), and on GE ≥11-4 the MMDevice ContainerId is synthesized from the wine-side HID USB parent — the pure-PipeWire-node `GUID_NULL` risk from the design doc no longer applies.
## Validation (on-glass, Bazzite 44 / .41)
- `cargo build --release --locked` (nvenc+vulkan-encode), `clippy --all-targets -D warnings`, pad_audio+pad_sink tests 11/11, `fmt` — in a fedora:44 container on the box.
- Sink serves the full identity through pipewire-pulse; an AUX-mapped rear-pair-only tone captures **index-exact** (speaker pair 0.0000, coil pair 0.3662 = bit-exact input level) over both the native and pulse legs; a positioned stray stream folds into the speaker pair and never excites the coils.
- The box currently runs this branch via a systemd drop-in for the upcoming Android-client field test.
The 0xD1 plane was Windows-host-only: host_cap() answered false and spawn()
was a stub everywhere else, so an Android tier-A client against a Linux host
negotiated the cap off and stayed on wire rumble. The whole downstream
machinery (framer, silence gate, lanes, 0xD1 send) was already capture-
agnostic — only the capturer was WASAPI.
- audio/linux/pad_sink.rs: one Audio/Sink stream node per DualSense-family
pad, minted with the identity the matchers read (ALSA-style node.name with
the pad's pairing MAC, description "Wireless Controller", bus/vendor/
product/form-factor proplist, per-pad serial), 4-ch F32 48 kHz FL FR RL RR,
no default-sink claim, priority.session 50. The process() callback IS the
capture. PUNKTFUNK_PAD_SINK_NAME/_DESC override the strings for field
debugging ({pad}/{mac} expand).
- native/pad_audio.rs: the shared logic and lanes compile on Linux;
pad_audio_thread is generic over the capturer (open-with-backoff kept);
host_cap() Linux arm = client asked + PUNKTFUNK_PAD_AUDIO + a reachable
PipeWire socket; spawn() Linux arm mints the sink lazily in the streamer
thread. spawn() gains an edge flag (Edge identity; ignored on Windows).
- devtest pad-sink-test: mint one sink and capture from it, no client — the
WP3 on-glass gate. Verified on a Bazzite 44 host: identity served through
pipewire-pulse, rear-pair (voice-coil) tone captured bit-exact over both
the native and pulse legs.
- docs: PUNKTFUNK_PAD_AUDIO{,_SLOTS} are no longer (Windows); the roadmap
non-goal narrows to Bluetooth client pads.
Gates (fedora:44 container, natively on the .41 box): cargo build --release
--locked (nvenc+vulkan-encode), clippy --all-targets -D warnings, cargo test
pad_audio+pad_sink 11/11, cargo fmt.
A field report (GE-Proton 11-5, real DualSense on-host) surfaced the missing
constraint: haptics only work when the pad's card runs the Pro Audio profile —
because GE's route opens the node through its bundled pipewire-alsa plugin
with aux_channels=1, and its pulse fallback forces a PA AUX0..3 map with
stream.dont-remix (proton-ds5-haptic patches 0013/0115/0116: "the hidden
PipeWire parent for a DualSense output exposes AUX0 through AUX3"). A
positioned FL FR RL RR sink puts those writers through position channelmix
instead of index passthrough.
The sink now advertises AUX0..AUX3. Proven on the box: an AUX-mapped
rear-pair-only tone captures index-exact (speaker pair 0.0000, coil pair
0.3662); a positioned stray stream folds into the speaker pair and never
excites the coils. The devtest reports per-pair peaks so exactly this class
of remix bug is visible.
Also confirmed from the GE patch set while here: device matching is
device.bus/vendor.id/product.id + the Sony/Wireless_Controller name
substrings (both of which the sink carries), and the MMDevice container is
now synthesized from the wine-side HID USB parent (patch 0112) — the old
pure-PW-node GUID_NULL concern no longer applies on GE >= 11-4.
enricobuehler
scheduled this pull request to auto merge when all checks succeed 2026-08-12 16:55:04 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes the last hole in DualSense support on Linux hosts: the 0xD1 pad-audio plane was Windows-only —
host_cap()answeredfalseandspawn()was a stub off-Windows, so a tier-A client (Android USB DS5) against a Linux host negotiated the cap off and stayed on wire rumble.What ships
audio/linux/pad_sink.rs— oneAudio/Sinkstream node per DualSense-family pad, minted with the identity the matchers read: ALSA-stylenode.namecarrying the pad's pairing MAC,node.description "Wireless Controller",device.bus/vendor.id/product.id/form_factorproplist, per-pad serial. No default-sink claim,priority.session 50. Theprocess()callback is the capture (we own the sink).PUNKTFUNK_PAD_SINK_NAME/_DESCoverride the strings for field debugging ({pad}/{mac}expand).native/pad_audio.rs— the framer/gate/lane machinery compiles on Linux; the streamer thread is generic over the capturer (same open-with-backoff); Linuxhost_cap= client asked +PUNKTFUNK_PAD_AUDIO≠0 + a reachable PipeWire socket; Linuxspawnmints the sink lazily in-thread.spawngains anedgeflag for the Edge identity.pad-sink-testdevtest — mints one sink and captures from it with per-pair peak reporting (speaker pair vs coil pair), no client needed.PUNKTFUNK_PAD_AUDIO{,_SLOTS}lose the "(Windows)" marker; the roadmap non-goal narrows to Bluetooth client pads.The AUX0–3 channel shape (second commit)
A field report ("GE-Proton 11-5 fixed DualSense audio — you only need the controller audio set to Pro Audio") led into GE's
proton-ds5-hapticpatch set: GE ≥11-4 opens the pad's node through its bundled pipewire-alsa plugin aspipewire:NODE=<name>withaux_channels=1, and its pulse fallback forces aPA AUX0..3map withstream.dont-remix— "the hidden PipeWire parent for a DualSense output exposes AUX0 through AUX3". Pro Audio is what makes a real pad present that shape; our sink mints it directly, so no profile step exists for users. A positioned FL FR RL RR sink would put those writers through position channelmix instead of index passthrough.Also confirmed from the patch set: device matching is bus/vid/pid proplist or the Sony name substrings (we carry both), and on GE ≥11-4 the MMDevice ContainerId is synthesized from the wine-side HID USB parent — the pure-PipeWire-node
GUID_NULLrisk from the design doc no longer applies.Validation (on-glass, Bazzite 44 / .41)
cargo build --release --locked(nvenc+vulkan-encode),clippy --all-targets -D warnings, pad_audio+pad_sink tests 11/11,fmt— in a fedora:44 container on the box.The 0xD1 plane was Windows-host-only: host_cap() answered false and spawn() was a stub everywhere else, so an Android tier-A client against a Linux host negotiated the cap off and stayed on wire rumble. The whole downstream machinery (framer, silence gate, lanes, 0xD1 send) was already capture- agnostic — only the capturer was WASAPI. - audio/linux/pad_sink.rs: one Audio/Sink stream node per DualSense-family pad, minted with the identity the matchers read (ALSA-style node.name with the pad's pairing MAC, description "Wireless Controller", bus/vendor/ product/form-factor proplist, per-pad serial), 4-ch F32 48 kHz FL FR RL RR, no default-sink claim, priority.session 50. The process() callback IS the capture. PUNKTFUNK_PAD_SINK_NAME/_DESC override the strings for field debugging ({pad}/{mac} expand). - native/pad_audio.rs: the shared logic and lanes compile on Linux; pad_audio_thread is generic over the capturer (open-with-backoff kept); host_cap() Linux arm = client asked + PUNKTFUNK_PAD_AUDIO + a reachable PipeWire socket; spawn() Linux arm mints the sink lazily in the streamer thread. spawn() gains an edge flag (Edge identity; ignored on Windows). - devtest pad-sink-test: mint one sink and capture from it, no client — the WP3 on-glass gate. Verified on a Bazzite 44 host: identity served through pipewire-pulse, rear-pair (voice-coil) tone captured bit-exact over both the native and pulse legs. - docs: PUNKTFUNK_PAD_AUDIO{,_SLOTS} are no longer (Windows); the roadmap non-goal narrows to Bluetooth client pads. Gates (fedora:44 container, natively on the .41 box): cargo build --release --locked (nvenc+vulkan-encode), clippy --all-targets -D warnings, cargo test pad_audio+pad_sink 11/11, cargo fmt.