Root-causes the 0.32.0 field reports of "frame losses and skips whenever the stream moves" ending in permanently soft video — two paired client+host log sets (Windows client over ZeroTier, Android over Wi-Fi) plus a local repro all show the same signature.
The phantom encoder ceiling (0.32.0 regression)
Phase 4 (#408) made the bitrate a wire budget: the encoder is handed enc_kbps(budget) and the applied read-back maps through budget_kbps(). That roundtrip deflates 1–2 kbps by design (budget_kbps_for_encoder never inflates), so the applied < requested compare in the stream loop recorded a phantom encoder_ceiling_kbps on every successful retarget (14000→13998, 9798→9797, …). The control task then clamps every later SetBitrate to it, so every climb is acked short at the current rate; the client controller learns it as a host cap ("host cap learned — climbs stop here", reprobe backoff 16→32 windows), and each descent lowers the "ceiling" further. Result: one loss episode ratchets an Automatic session 20 → 2.3 Mbps with no way back until reconnect. 0.31.4 was immune — its read-back stayed in encoder units, so an in-range apply compared equal.
Fix: EncDerive::applied_budget_kbps reads back in the request's own truncated terms — only an apply short of what the request itself derives to (a genuine driver clamp, the ABR-overdrive case) records a ceiling. Both the in-place and rebuild arms use it. The regression test was proven against the backed-out fix (red without, green with).
The startup capacity probe collides with bring-up
The client armed the probe on a flat 2 s timer, but host bring-up can take 6–8 s (IDD display acquisition), so the 940/464 Mbps × 800 ms burst landed exactly on the first IDR: black video for 5–11 s, the decoder refusing AUs ("slice references PPS 0, which has not been seen" ×21), 2101 audio underruns — and the result missed the 6 s timeout anyway (bring-up plus the burst's own queue drain and QUIC loss-recovery backoff, 8.6 s measured), so it was discarded as "old host?": all disturbance, zero measurement. The probe now arms only once a frame has completed, and the timeout is 15 s so a slow answer still teaches the ceiling.
punktfunk-host: compiles in the Linux container gate (its only compile signal); new test native::tests::full_apply_readback_is_the_request_not_the_deflated_roundtrip passes, and fails with the fix backed out.
cargo fmt --check clean.
Not addressed here (link-side, not ours): the reporter's host streams over its Wi-Fi uplink, and the Windows client picked its ZeroTier address although both machines share the LAN — motion-onset burst loss is real on that path; with this PR the controller rides it instead of collapsing.
Root-causes the 0.32.0 field reports of "frame losses and skips whenever the stream moves" ending in permanently soft video — two paired client+host log sets (Windows client over ZeroTier, Android over Wi-Fi) plus a local repro all show the same signature.
## The phantom encoder ceiling (0.32.0 regression)
Phase 4 (#408) made the bitrate a wire budget: the encoder is handed `enc_kbps(budget)` and the applied read-back maps through `budget_kbps()`. That roundtrip **deflates 1–2 kbps by design** (`budget_kbps_for_encoder` never inflates), so the `applied < requested` compare in the stream loop recorded a phantom `encoder_ceiling_kbps` on **every** successful retarget (14000→13998, 9798→9797, …). The control task then clamps every later `SetBitrate` to it, so every climb is acked short at the current rate; the client controller learns it as a host cap ("host cap learned — climbs stop here", reprobe backoff 16→32 windows), and each descent lowers the "ceiling" further. Result: one loss episode ratchets an Automatic session 20 → 2.3 Mbps with no way back until reconnect. 0.31.4 was immune — its read-back stayed in encoder units, so an in-range apply compared equal.
Fix: `EncDerive::applied_budget_kbps` reads back in the request's own truncated terms — only an apply short of what the request itself derives to (a genuine driver clamp, the ABR-overdrive case) records a ceiling. Both the in-place and rebuild arms use it. The regression test was proven against the backed-out fix (red without, green with).
## The startup capacity probe collides with bring-up
The client armed the probe on a flat 2 s timer, but host bring-up can take 6–8 s (IDD display acquisition), so the 940/464 Mbps × 800 ms burst landed exactly on the first IDR: black video for 5–11 s, the decoder refusing AUs ("slice references PPS 0, which has not been seen" ×21), 2101 audio underruns — and the result missed the 6 s timeout anyway (bring-up plus the burst's own queue drain and QUIC loss-recovery backoff, 8.6 s measured), so it was discarded as "old host?": all disturbance, zero measurement. The probe now arms only once a frame has completed, and the timeout is 15 s so a slow answer still teaches the ceiling.
## Verification
- `punktfunk-core` `--features quic`: 512 tests green (natively, non-vacuous — `Checking punktfunk-core` present).
- `punktfunk-host`: compiles in the Linux container gate (its only compile signal); new test `native::tests::full_apply_readback_is_the_request_not_the_deflated_roundtrip` passes, and fails with the fix backed out.
- `cargo fmt --check` clean.
Not addressed here (link-side, not ours): the reporter's host streams over its Wi-Fi uplink, and the Windows client picked its ZeroTier address although both machines share the LAN — motion-onset burst loss is real on that path; with this PR the controller rides it instead of collapsing.
Two field sessions on 0.32.0 (Windows client over ZeroTier, Android over
Wi-Fi) plus a local repro showed every Automatic session ratcheting
20 -> 2.3 Mbps on the first loss episode and never climbing back.
Phase 4 made the bitrate a wire budget: the encoder is handed
enc_kbps(budget) and the read-back maps through budget_kbps(). That
roundtrip deflates 1-2 kbps by design (the inverse never inflates), and
the applied < requested compare then recorded a phantom
encoder_ceiling_kbps on EVERY successful apply. The control task clamps
each later SetBitrate to it, so every climb was acked short at the
current rate, the client controller learned it as a host cap
(reprobe backoff 16 -> 32 windows), and each descent lowered the
"ceiling" further: a one-way ratchet to the floor. 0.31.4 was immune -
its read-back stayed in encoder units, so an in-range apply compared
equal. EncDerive::applied_budget_kbps now reads back in the request's
own truncated terms: only an apply short of what the request itself
derives to - a genuine driver clamp - records a ceiling. Regression
test proven against the backed-out fix.
The startup link-capacity probe fired on a flat 2 s timer, but host
bring-up can take 6-8 s (IDD display acquisition), so the 940/464 Mbps
burst landed exactly on the first IDR - black video for 5-11 s, the
decoder refusing AUs ("PPS 0 not seen"), 2101 audio underruns - and the
result missed the 6 s timeout anyway (bring-up plus the burst's own
queue drain and QUIC loss-recovery backoff: 8.6 s measured), so the
ceiling stayed negotiated: all disturbance, no measurement. The probe
now arms only once a frame has completed, and the timeout is 15 s so a
slow answer still teaches the ceiling.
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.
Root-causes the 0.32.0 field reports of "frame losses and skips whenever the stream moves" ending in permanently soft video — two paired client+host log sets (Windows client over ZeroTier, Android over Wi-Fi) plus a local repro all show the same signature.
The phantom encoder ceiling (0.32.0 regression)
Phase 4 (#408) made the bitrate a wire budget: the encoder is handed
enc_kbps(budget)and the applied read-back maps throughbudget_kbps(). That roundtrip deflates 1–2 kbps by design (budget_kbps_for_encodernever inflates), so theapplied < requestedcompare in the stream loop recorded a phantomencoder_ceiling_kbpson every successful retarget (14000→13998, 9798→9797, …). The control task then clamps every laterSetBitrateto it, so every climb is acked short at the current rate; the client controller learns it as a host cap ("host cap learned — climbs stop here", reprobe backoff 16→32 windows), and each descent lowers the "ceiling" further. Result: one loss episode ratchets an Automatic session 20 → 2.3 Mbps with no way back until reconnect. 0.31.4 was immune — its read-back stayed in encoder units, so an in-range apply compared equal.Fix:
EncDerive::applied_budget_kbpsreads back in the request's own truncated terms — only an apply short of what the request itself derives to (a genuine driver clamp, the ABR-overdrive case) records a ceiling. Both the in-place and rebuild arms use it. The regression test was proven against the backed-out fix (red without, green with).The startup capacity probe collides with bring-up
The client armed the probe on a flat 2 s timer, but host bring-up can take 6–8 s (IDD display acquisition), so the 940/464 Mbps × 800 ms burst landed exactly on the first IDR: black video for 5–11 s, the decoder refusing AUs ("slice references PPS 0, which has not been seen" ×21), 2101 audio underruns — and the result missed the 6 s timeout anyway (bring-up plus the burst's own queue drain and QUIC loss-recovery backoff, 8.6 s measured), so it was discarded as "old host?": all disturbance, zero measurement. The probe now arms only once a frame has completed, and the timeout is 15 s so a slow answer still teaches the ceiling.
Verification
punktfunk-core--features quic: 512 tests green (natively, non-vacuous —Checking punktfunk-corepresent).punktfunk-host: compiles in the Linux container gate (its only compile signal); new testnative::tests::full_apply_readback_is_the_request_not_the_deflated_roundtrippasses, and fails with the fix backed out.cargo fmt --checkclean.Not addressed here (link-side, not ours): the reporter's host streams over its Wi-Fi uplink, and the Windows client picked its ZeroTier address although both machines share the LAN — motion-onset burst loss is real on that path; with this PR the controller rides it instead of collapsing.
Two field sessions on 0.32.0 (Windows client over ZeroTier, Android over Wi-Fi) plus a local repro showed every Automatic session ratcheting 20 -> 2.3 Mbps on the first loss episode and never climbing back. Phase 4 made the bitrate a wire budget: the encoder is handed enc_kbps(budget) and the read-back maps through budget_kbps(). That roundtrip deflates 1-2 kbps by design (the inverse never inflates), and the applied < requested compare then recorded a phantom encoder_ceiling_kbps on EVERY successful apply. The control task clamps each later SetBitrate to it, so every climb was acked short at the current rate, the client controller learned it as a host cap (reprobe backoff 16 -> 32 windows), and each descent lowered the "ceiling" further: a one-way ratchet to the floor. 0.31.4 was immune - its read-back stayed in encoder units, so an in-range apply compared equal. EncDerive::applied_budget_kbps now reads back in the request's own truncated terms: only an apply short of what the request itself derives to - a genuine driver clamp - records a ceiling. Regression test proven against the backed-out fix. The startup link-capacity probe fired on a flat 2 s timer, but host bring-up can take 6-8 s (IDD display acquisition), so the 940/464 Mbps burst landed exactly on the first IDR - black video for 5-11 s, the decoder refusing AUs ("PPS 0 not seen"), 2101 audio underruns - and the result missed the 6 s timeout anyway (bring-up plus the burst's own queue drain and QUIC loss-recovery backoff: 8.6 s measured), so the ceiling stayed negotiated: all disturbance, no measurement. The probe now arms only once a frame has completed, and the timeout is 15 s so a slow answer still teaches the ceiling.