The pad sink wore a profile name GE-Proton matches nothing on, so neither haptics nor speaker could route #252

Merged
enricobuehler merged 2 commits from worktree-ds-pad-sink-pro-audio into main 2026-08-15 16:11:48 +00:00
Owner

A field report compared a real DualSense plugged into a Linux host — "DualSense wireless controller (PS5) Pro" with a Profile: Pro Audio dropdown — against our minted sink, which shows "Wireless Controller" and no Profile dropdown at all.

The missing dropdown is cosmetic: a real pad is a USB sound card and gets a profile combo, ours is a software device with no card, and the layout that combo exists to reach is the one we are already minted in. But reading GE-Proton's proton-ds5-haptic matchers from source (157 patches on master) rather than inferring them found three real defects, each costing a different piece of the feature.

The profile suffix — this is the one that killed the speaker half

We minted -00.analog-surround-40. GE's is_dualsense_speaker_sink() is a substring test for Speaker__sink, and three things hang off it:

  • the pad-speaker (mono controller-effect) streams only bind and retarget to a sink it accepts — every one of those call sites passes require_speaker=TRUE;
  • apply_windows_sony_audio_format() forces the wine endpoint to the Windows 4×48 kHz KSAUDIO_SPEAKER_QUAD layout DS5 titles probe for;
  • the endpoint lands on the identity Spider-Man's known-working path used.

analog-surround-40 matched none of it, so the speaker half of this feature had nothing to attach to. Now -00.HiFi__Speaker__sink.

The channels stay Pro Audio's four raw AUX, deliberately not the same real-pad profile the name comes from. Since alsa-ucm-conf gained ucm2/USB-Audio/Sony/DualSense-PS5.conf (2026-08-03) a real pad's profiles are UCM SplitPCM views of one 4-channel PCM — a mono Speaker, a stereo Headphones, a 4-channel Direct added "for wine compatibility" — and all of them are positioned. GE renders haptics as an AUX0..AUX3 stream, so on every one of those the graph re-mixes and the voice-coil pair is folded away; only ACP's always-present Pro Audio node passes it through by index. That is the entire mechanism behind the community advice "you only need the controller audio set to Pro Audio", and it matches our own .41 measurement (a positioned stream folds into the speaker pair and never excites the coils). No single real profile satisfies both halves, so we take the name from one and the layout from the other.

api.alsa.split.name was absent

GE reads that key off the sink it is about to render haptics into and opens that node through its bundled pipewire-alsa plugin as pipewire:NODE=<name> with aux_channels=1. Without it get_dualsense_haptic_target() returns NULL and the leg cannot engage at all — including for the titles GE auto-switches into "Windows Sony audio mode" after eight format probes. We have no split (the sink is the four-channel parent), so the honest value is our own node name. PUNKTFUNK_PAD_SINK_SPLIT_NAME drops or overrides it in the field.

Carrying Speaker__sink and a split parent at once is a real pad's shape rather than a contrivance — GE's is_dualsense_endpoint_speaker_sink says so in as many words:

Edge speaker sinks may also carry raw haptic metadata. They are valid routing targets, but using the shared mono endpoint id for them makes Spider-Man Remastered crash while enumerating endpoints at startup.

It handles the pair by keeping them as routing targets while withholding the shared mono endpoint id. The exclusion is_dualsense_speaker_sink itself once had is gone as of patch 0079.

The node name broke its own matcher

It carried an invented DualSense_ infix, but a plain DualSense's USB iProduct string is just Wireless Controller — only the Edge has a model word. That infix split the contiguous Sony_Interactive_Entertainment_Wireless_Controller substring the community WirePlumber rule and GE's own name-only fallback key on. The Edge keeps DualSense_Edge_Wireless_Controller.

Also

The proplist now states the shape (audio.channels, audio.position = AUX0,AUX1,AUX2,AUX3) and the ALSA card names; pipewire-pulse hands a sink's proplist to a Proton client verbatim (fill_sink_info_proplist), so every key reaches the matcher. api.alsa.path is deliberately not set — it must name a PCM that really opens 48k/S16/4ch — and neither is api.alsa.split.position, which is WirePlumber's split_nodes_om trigger.

node.description stays "Wireless Controller" on purpose: wine hands it straight to the endpoint's PKEY_Device_FriendlyName, and the title matchers do a case-sensitive wcsstr(name, L"Wireless Controller") that the hwdb form "DualSense wireless controller (PS5)" fails.

Mint-time logging and the pad-sink-test devtest now print the strings a title has to match, plus the channel-map a hand-driven test file needs to reach the coils at all.

New user page docs-site/content/docs/controller-audio.md — the first user-facing documentation for this feature. It answers the Pro Audio question directly, explains why there is no profile selector to switch, gives the three host log lines that say how far it got, and lists the per-game Proton launch options. PROTON_DUALSENSE_HAPTICS_PREFER_NON_EVENT=1 only now has anything to bind to.

Verification

Gated in punktfunk-rust-ci:latest on --platform linux/amd64: cargo clippy --all-targets -p punktfunk-host -- -D warnings clean, plain cargo build -p punktfunk-host clean, cargo test -p punktfunk-host audio::linux::pad_sink 4/4 — confirmed non-vacuous by Compiling punktfunk-host appearing in the log rather than trusting the exit code.

No on-glass validation. This is gated and reasoned against GE's source, not felt in a hand. The decisive field check is WINEDEBUG=+pulse on the game: lines beginning Routing DualSense name exactly which leg GE took and which device it chose.

A field report compared a real DualSense plugged into a Linux host — *"DualSense wireless controller (PS5) **Pro**"* with a **Profile: Pro Audio** dropdown — against our minted sink, which shows *"Wireless Controller"* and no Profile dropdown at all. The missing dropdown is cosmetic: a real pad is a USB sound card and gets a profile combo, ours is a software device with no card, and the layout that combo exists to reach is the one we are already minted in. But reading GE-Proton's `proton-ds5-haptic` matchers from source (157 patches on master) rather than inferring them found **three real defects**, each costing a different piece of the feature. ## The profile suffix — this is the one that killed the speaker half We minted `-00.analog-surround-40`. GE's `is_dualsense_speaker_sink()` is a substring test for `Speaker__sink`, and three things hang off it: - the pad-**speaker** (mono controller-effect) streams only bind and retarget to a sink it accepts — every one of those call sites passes `require_speaker=TRUE`; - `apply_windows_sony_audio_format()` forces the wine endpoint to the Windows 4×48 kHz `KSAUDIO_SPEAKER_QUAD` layout DS5 titles probe for; - the endpoint lands on the identity Spider-Man's known-working path used. `analog-surround-40` matched none of it, so the speaker half of this feature had nothing to attach to. Now `-00.HiFi__Speaker__sink`. **The channels stay Pro Audio's four raw AUX**, deliberately not the same real-pad profile the name comes from. Since alsa-ucm-conf gained `ucm2/USB-Audio/Sony/DualSense-PS5.conf` (2026-08-03) a real pad's profiles are UCM SplitPCM views of one 4-channel PCM — a mono `Speaker`, a stereo `Headphones`, a 4-channel `Direct` added *"for wine compatibility"* — and **all of them are positioned**. GE renders haptics as an `AUX0..AUX3` stream, so on every one of those the graph re-mixes and the voice-coil pair is folded away; only ACP's always-present Pro Audio node passes it through by index. That is the entire mechanism behind the community advice *"you only need the controller audio set to Pro Audio"*, and it matches our own .41 measurement (a positioned stream folds into the speaker pair and never excites the coils). No single real profile satisfies both halves, so we take the name from one and the layout from the other. ## `api.alsa.split.name` was absent GE reads that key off the sink it is about to render haptics into and opens **that** node through its bundled pipewire-alsa plugin as `pipewire:NODE=<name>` with `aux_channels=1`. Without it `get_dualsense_haptic_target()` returns NULL and the leg cannot engage at all — including for the titles GE auto-switches into "Windows Sony audio mode" after eight format probes. We have no split (the sink *is* the four-channel parent), so the honest value is our own node name. `PUNKTFUNK_PAD_SINK_SPLIT_NAME` drops or overrides it in the field. Carrying `Speaker__sink` **and** a split parent at once is a real pad's shape rather than a contrivance — GE's `is_dualsense_endpoint_speaker_sink` says so in as many words: > Edge speaker sinks may also carry raw haptic metadata. They are valid routing targets, but using the shared mono endpoint id for them makes Spider-Man Remastered crash while enumerating endpoints at startup. It handles the pair by keeping them as routing targets while withholding the *shared* mono endpoint id. The exclusion `is_dualsense_speaker_sink` itself once had is gone as of patch 0079. ## The node name broke its own matcher It carried an invented `DualSense_` infix, but a plain DualSense's USB `iProduct` string is just `Wireless Controller` — only the Edge has a model word. That infix split the contiguous `Sony_Interactive_Entertainment_Wireless_Controller` substring the community WirePlumber rule and GE's own name-only fallback key on. The Edge keeps `DualSense_Edge_Wireless_Controller`. ## Also The proplist now states the shape (`audio.channels`, `audio.position = AUX0,AUX1,AUX2,AUX3`) and the ALSA card names; pipewire-pulse hands a sink's proplist to a Proton client verbatim (`fill_sink_info_proplist`), so every key reaches the matcher. `api.alsa.path` is deliberately **not** set — it must name a PCM that really opens 48k/S16/4ch — and neither is `api.alsa.split.position`, which is WirePlumber's `split_nodes_om` trigger. `node.description` stays `"Wireless Controller"` on purpose: wine hands it straight to the endpoint's `PKEY_Device_FriendlyName`, and the title matchers do a case-sensitive `wcsstr(name, L"Wireless Controller")` that the hwdb form *"DualSense wireless controller (PS5)"* fails. Mint-time logging and the `pad-sink-test` devtest now print the strings a title has to match, plus the channel-map a hand-driven test file needs to reach the coils at all. New user page `docs-site/content/docs/controller-audio.md` — the first user-facing documentation for this feature. It answers the Pro Audio question directly, explains why there is no profile selector to switch, gives the three host log lines that say how far it got, and lists the per-game Proton launch options. `PROTON_DUALSENSE_HAPTICS_PREFER_NON_EVENT=1` only now has anything to bind to. ## Verification Gated in `punktfunk-rust-ci:latest` on `--platform linux/amd64`: `cargo clippy --all-targets -p punktfunk-host -- -D warnings` clean, plain `cargo build -p punktfunk-host` clean, `cargo test -p punktfunk-host audio::linux::pad_sink` 4/4 — confirmed non-vacuous by `Compiling punktfunk-host` appearing in the log rather than trusting the exit code. **No on-glass validation.** This is gated and reasoned against GE's source, not felt in a hand. The decisive field check is `WINEDEBUG=+pulse` on the game: lines beginning `Routing DualSense` name exactly which leg GE took and which device it chose.
enricobuehler added 2 commits 2026-08-15 15:37:48 +00:00
Everything a title needs to find the DualSense's audio device on a Linux host is a substring of
the sink's name or a key in its proplist, and ours were chosen against an older reading of
GE-Proton. Three of them were wrong, and each one cost a different piece of the feature.

**The profile suffix.** We minted `-00.analog-surround-40`. GE's `is_dualsense_speaker_sink()` is
a substring test for `Speaker__sink`, and three things hang off it: the pad-SPEAKER (mono
controller-effect) streams only bind and retarget to a sink it accepts, its
`apply_windows_sony_audio_format()` forces the wine endpoint to the Windows 4x48 kHz
`KSAUDIO_SPEAKER_QUAD` layout DS5 titles probe for, and the endpoint lands on the identity
Spider-Man's working path used. `analog-surround-40` matched none of it — the speaker half of
this feature had nothing to attach to at all. Now `-00.HiFi__Speaker__sink`.

The channels stay Pro Audio's four raw AUX, which is deliberately not the same real-pad profile
the name comes from. Since alsa-ucm-conf gained `USB-Audio/Sony/DualSense-PS5` a real pad's
profiles are UCM SplitPCM views of one 4-channel PCM — a mono Speaker, a stereo Headphones, a
4-channel Direct "for wine compatibility" — and all of them are POSITIONED. GE renders haptics as
an `AUX0..AUX3` stream, so on every one of those the graph re-mixes and the voice-coil pair is
folded away; only Pro Audio's raw AUX node passes it through by index. That is the whole content
of the field advice "you only need the controller audio set to Pro Audio". No single real profile
satisfies both halves, so we take the name from one and the layout from the other.

**`api.alsa.split.name` was absent.** GE reads it off the sink it is about to render haptics into
and opens THAT node through its bundled pipewire-alsa plugin as `pipewire:NODE=<name>` with
`aux_channels=1`. Without the key `get_dualsense_haptic_target()` returns NULL and the leg cannot
engage — including for the titles GE auto-switches into "Windows Sony audio mode" after eight
format probes. We have no split, so the honest value is our own node name;
`PUNKTFUNK_PAD_SINK_SPLIT_NAME` drops or overrides it in the field. Carrying `Speaker__sink` and
a split parent at once is a real pad's shape rather than a contrivance: GE's
`is_dualsense_endpoint_speaker_sink` notes that "Edge speaker sinks may also carry raw haptic
metadata" and handles the pair by keeping them as routing targets while withholding the shared
mono endpoint id.

**The node name broke its own matcher.** It carried an invented `DualSense_` infix, but a plain
DualSense's USB iProduct string is just "Wireless Controller" — only the Edge has a model word —
and that infix split the contiguous `Sony_Interactive_Entertainment_Wireless_Controller` substring
the community WirePlumber rule and GE's own name-only fallback key on.

The proplist now also states the shape (`audio.position`, `audio.channels`) and the ALSA card
names; pipewire-pulse hands every one of these to a Proton client verbatim. Mint-time logging and
the `pad-sink-test` devtest print the strings a title has to match, plus the channel-map a
hand-driven test file needs to reach the coils at all.
docs: answer the "set the controller audio to Pro Audio" question our sink cannot offer a switch for
android / android (pull_request) Successful in 4m38s
ci / web (pull_request) Successful in 2m3s
ci / rust-arm64 (pull_request) Successful in 2m6s
ci / docs-site (pull_request) Successful in 1m50s
ci / bun-nix (pull_request) Successful in 4m0s
ci / rust (pull_request) Failing after 15m36s
998e5d3379
The community fix for DualSense haptics on Linux is to open sound settings and flip the pad's
card profile to Pro Audio, and a field report compared that against our minted sink: theirs has a
Profile dropdown reading "Pro Audio", ours has no dropdown at all. That reads like something is
missing, and it is worth saying plainly that it is not — a real pad is a USB sound card and gets
a profile selector, ours is a software device with no card, and the layout the selector exists to
reach is the one we are already minted in.

New page covers the whole feature end to end: what a DualSense's speaker and voice coils actually
are (a four-channel audio stream, not rumble), the USB-only requirement on the client, GE-Proton
11-5 on the host, why every other card profile folds the coil channels away, the three host log
lines that say how far it got, and the per-game Proton launch options that force GE onto its most
direct route. That last one now has teeth: with the sink advertising a split parent, GE's
`PROTON_DUALSENSE_HAPTICS_PREFER_NON_EVENT=1` opens our node by name and writes the four channels
in with no remix anywhere in between.

Known limits are stated rather than left to be rediscovered: Bluetooth pads have no audio
interface, container-ID matching cannot work behind a uhid pad with no USB device to derive one
from, and a real pad plugged into the host itself can win the match against the one you are
streaming to.
enricobuehler merged commit 8c4a41913f into main 2026-08-15 16:11:48 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#252