Follow-up to #101, from its first on-glass run. One commit, be86cfcd, branched from #101's tip (now in main).
What the first on-glass run showed
audio buffer 272 ms · a/v +284 ms, stable. The overhaul itself was working — it measured the +284 ms correctly — but was not allowed to act on it.
Why
The playback process callback sized every write from the mapped buffer's capacity (data.len() / stride). PipeWire sizes stream buffers for the worst case — quantum-limit, 8192 frames ≈ 170 ms — not for the cycle, and the graph's actual per-cycle ask (pw_buffer.requested) was never read. Two consequences:
Up to 170 ms of PCM queued per buffer downstream of the ring — invisible to buffered_ahead, so the HUD's a/v figure understates real ear latency in this state.
want ≈ 170 ms feeds JitterPolicy's own safety math: the underrun floor becomes want + one frame ≈ 175 ms, and step()'s cap becomes target + want ≈ 345 ms. A 272 ms ring is legal and stable inside that band, and by the continuity-outranks-sync rule the A/V loop is forbidden from draining it. The +284 residual on the HUD is the design honestly reporting exactly this.
The tell for next time: a steady ring abovehard_cap_ms (80 on PipeWire) is impossible while step() runs with honest inputs — it hard-trims instantly. A stable deep ring means the cap itself was inflated; re-derive want before chasing shed rates.
This also retroactively explains the original Deck field report: the old client overfilled identically, just without a HUD to show it.
The fix
Honor buffer.requested(); capacity stays as both the ceiling and the fallback for requested == 0 (no adapter suggestion).
One-time audio playback quantum info log (requested vs capacity vs write) per stream, mirroring the host's per-capture-open quantum line, so the next on-glass report can say which one is sizing the writes.
pipewire = { version = "0.9", features = ["v0_3_49"] } — pure cfg gate for the requested field; runtime floor libpipewire ≥ 0.3.49 (2022-03), which every ship target clears.
New info line at stream start: audio playback quantum requested_frames=… capacity_frames=…. If requested_frames=0, the graph gives no hint and we need plan B (cap writes by node.latency).
audio buffer should land ~15–30 ms; a/v should converge into the ±10 ms deadband.
Follow-up to #101, from its first on-glass run. One commit, `be86cfcd`, branched from #101's tip (now in main).
## What the first on-glass run showed
`audio buffer 272 ms · a/v +284 ms`, stable. The overhaul itself was working — it *measured* the +284 ms correctly — but was not allowed to act on it.
## Why
The playback `process` callback sized every write from the mapped buffer's capacity (`data.len() / stride`). PipeWire sizes stream buffers for the worst case — `quantum-limit`, 8192 frames ≈ **170 ms** — not for the cycle, and the graph's actual per-cycle ask (`pw_buffer.requested`) was never read. Two consequences:
1. Up to 170 ms of PCM queued per buffer *downstream* of the ring — invisible to `buffered_ahead`, so the HUD's a/v figure **understates** real ear latency in this state.
2. `want` ≈ 170 ms feeds `JitterPolicy`'s own safety math: the underrun floor becomes `want` + one frame ≈ 175 ms, and `step()`'s cap becomes `target + want` ≈ 345 ms. A 272 ms ring is **legal and stable** inside that band, and by the continuity-outranks-sync rule the A/V loop is *forbidden* from draining it. The +284 residual on the HUD is the design honestly reporting exactly this.
The tell for next time: a steady ring **above** `hard_cap_ms` (80 on PipeWire) is impossible while `step()` runs with honest inputs — it hard-trims instantly. A stable deep ring means the cap itself was inflated; re-derive `want` before chasing shed rates.
This also retroactively explains the original Deck field report: the old client overfilled identically, just without a HUD to show it.
## The fix
- Honor `buffer.requested()`; capacity stays as both the ceiling and the fallback for `requested == 0` (no adapter suggestion).
- One-time `audio playback quantum` info log (requested vs capacity vs write) per stream, mirroring the host's per-capture-open quantum line, so the next on-glass report can say which one is sizing the writes.
- `pipewire = { version = "0.9", features = ["v0_3_49"] }` — pure cfg gate for the `requested` field; runtime floor libpipewire ≥ 0.3.49 (2022-03), which every ship target clears.
## Verified
On .21 (Linux): `cargo clippy -p pf-client-core --all-targets -- -D warnings` clean, 167 pf-client-core tests pass (1 ignored), `fmt --check` clean. macOS cannot compile-check this crate (`#[cfg(target_os = "linux")]`).
## On-glass retest expectations (Deck ← .21)
- New info line at stream start: `audio playback quantum requested_frames=… capacity_frames=…`. If `requested_frames=0`, the graph gives no hint and we need plan B (cap writes by `node.latency`).
- `audio buffer` should land ~15–30 ms; `a/v` should converge into the ±10 ms deadband.
The playback process callback sized its writes from the mapped buffer's
capacity — PipeWire's quantum-limit, 8192 frames ≈ 170 ms — instead of
the graph's per-cycle ask (pw_buffer.requested). Every cycle therefore
queued up to 170 ms of PCM downstream of the ring, and, worse, taught
JitterPolicy that the device drains 170 ms per callback: the underrun
floor (want + one frame) rose above any depth the A/V sync loop may
request, so sync measured audio ~280 ms late and was forbidden — by its
own continuity rule — from draining it. The first on-glass run of the
latency overhaul showed exactly that: audio buffer 272 ms, a/v +284 ms,
stable.
Honor requested (capacity remains both the ceiling and the fallback for
requested == 0), and log requested-vs-capacity once per stream in the
shape of the host's per-capture-open quantum line, so the next on-glass
report can say which one is sizing the writes.
Needs libpipewire >= 0.3.49 (2022-03) for the requested field; every
ship target clears that.
Verified on .21: cargo clippy -p pf-client-core --all-targets -D
warnings clean, 167 tests pass, fmt clean.
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.
Follow-up to #101, from its first on-glass run. One commit,
be86cfcd, branched from #101's tip (now in main).What the first on-glass run showed
audio buffer 272 ms · a/v +284 ms, stable. The overhaul itself was working — it measured the +284 ms correctly — but was not allowed to act on it.Why
The playback
processcallback sized every write from the mapped buffer's capacity (data.len() / stride). PipeWire sizes stream buffers for the worst case —quantum-limit, 8192 frames ≈ 170 ms — not for the cycle, and the graph's actual per-cycle ask (pw_buffer.requested) was never read. Two consequences:buffered_ahead, so the HUD's a/v figure understates real ear latency in this state.want≈ 170 ms feedsJitterPolicy's own safety math: the underrun floor becomeswant+ one frame ≈ 175 ms, andstep()'s cap becomestarget + want≈ 345 ms. A 272 ms ring is legal and stable inside that band, and by the continuity-outranks-sync rule the A/V loop is forbidden from draining it. The +284 residual on the HUD is the design honestly reporting exactly this.The tell for next time: a steady ring above
hard_cap_ms(80 on PipeWire) is impossible whilestep()runs with honest inputs — it hard-trims instantly. A stable deep ring means the cap itself was inflated; re-derivewantbefore chasing shed rates.This also retroactively explains the original Deck field report: the old client overfilled identically, just without a HUD to show it.
The fix
buffer.requested(); capacity stays as both the ceiling and the fallback forrequested == 0(no adapter suggestion).audio playback quantuminfo log (requested vs capacity vs write) per stream, mirroring the host's per-capture-open quantum line, so the next on-glass report can say which one is sizing the writes.pipewire = { version = "0.9", features = ["v0_3_49"] }— pure cfg gate for therequestedfield; runtime floor libpipewire ≥ 0.3.49 (2022-03), which every ship target clears.Verified
On .21 (Linux):
cargo clippy -p pf-client-core --all-targets -- -D warningsclean, 167 pf-client-core tests pass (1 ignored),fmt --checkclean. macOS cannot compile-check this crate (#[cfg(target_os = "linux")]).On-glass retest expectations (Deck ← .21)
audio playback quantum requested_frames=… capacity_frames=…. Ifrequested_frames=0, the graph gives no hint and we need plan B (cap writes bynode.latency).audio buffershould land ~15–30 ms;a/vshould converge into the ±10 ms deadband.