Codex flagged that `bypass.set` only flipped `bypass_global` in
profile state and never touched the graph: `try_route_stream`
returned Skip but the daemon kept re-asserting
`default.audio.sink = headroom-processed`, so apps following
default still landed in the processor, and already-managed streams
kept their explicit links to the processed sink. The "kill switch"
killed nothing.
What the bypass now actually does
Three coupled effects, applied atomically by a single
`PwCommand::ReevaluateAll` post from the IPC handler:
1. **Routing decision flips.** `routing::evaluate` learned to
short-circuit to `Route(Bypass)` for every routable playback
stream when `bypass_global=true`. Surround's pre-existing
`>2ch -> Bypass` rule still applies; both share the same
output and pick up the same explicit-link machinery from 4k.
2. **Existing managed streams get re-routed.** A new
`known_streams: HashMap<u32, PwNodeInfo>` cache in
`RoutingState` (populated on `try_route_stream`, cleared in
`on_global_remove`) lets `reevaluate_all` iterate every
stream we've ever seen and re-run the decision. The
extracted `apply_bus_route` runs the same enqueue / unmanage
logic the registry callback uses, so the live-arrival path
and the bypass-toggle path stay in lockstep.
3. **`default.audio.sink` flips to the real sink.** Inside
`reevaluate_all`, the daemon writes default to the real sink
name under bypass, and back to `headroom-processed` when
bypass clears. The `reassert_default_processed` rate-limiter
is gated on bypass so we don't keep fighting WP for a sink
we no longer want as default. Apps that route to "default"
(which is most legacy code paths and a lot of GTK/Qt
widgets) now actually skip the processor under bypass.
Adjacent cleanups that fell out
- `try_route_stream` no longer carries the bypass branch
inline. The split — registry callback inserts cache + calls
`apply_bus_route` + maybe spawns Layer A — keeps the
re-evaluation path free of the `&GlobalObject` it doesn't
have. Layer A spawning stays at first-see time as before;
streams that arrived before the daemon doesn't get a
retroactive tap, which is fine since Layer A is orthogonal
to bus routing and tap creation requires the registry global.
- `RoutingDecision::Skip` now properly tears down any prior
bus state (`unmanage()` drops the Link proxies and removes
the IPC-visible `state.streams` entry).
- `PwCommand::ReevaluateAll` is a generic re-evaluation
trigger; F2 will reuse it for profile / rule changes.
Tests
- `routing::evaluate` signature picked up a `bypass_global:
bool` arg; 11 unit tests updated to pass `false`.
- ops::tests' `let PwCommand::RouteStream { .. } = cmd;` is
now `let ... else { panic!(..) }` (the enum is no longer
single-variant). 188 tests pass; clippy clean.
Live verification
A/B/A against a 1 kHz sine `--target headroom-processed`:
- bypass off (baseline): pw-cat → headroom-processed:playback;
default.audio.sink = headroom-processed.
- bypass on: pw-cat → Mbox:playback (the explicit link to
processed is gone, a new explicit link to the real sink is
in place); default.audio.sink = the Mbox.
- bypass off (back): pw-cat → headroom-processed:playback;
default.audio.sink = headroom-processed.
- Layer A tap link stays attached through both transitions —
orthogonal as designed.
|
||
|---|---|---|
| .. | ||
| headroom-cli | ||
| headroom-client | ||
| headroom-core | ||
| headroom-dsp | ||
| headroom-ipc | ||