diff --git a/docs/releases/v0.24.0.md b/docs/releases/v0.24.0.md index 22e16a31..625fd03e 100644 --- a/docs/releases/v0.24.0.md +++ b/docs/releases/v0.24.0.md @@ -1,6 +1,6 @@ Wire-compatible with 0.23.x — everything you have already paired keeps working, and you can update one side at a time. Nothing here changes how a host and a client agree on what to send each other, so an old client on a new host (or the other way round) streams exactly as it does today. -This release is mostly about making 0.23.0 right. **If you stream to an Android phone or to the Linux or Windows desktop app, update the client** — 0.23.0's new send-a-frame-in-pieces delivery had a fault that could throw most of the video away on a busy link, and the fix lives in the client. Alongside that: the Linux and Windows app finally gets the frame scheduler the phone and Apple apps have had, the Steam Deck plugin reaches every setting instead of nine of them, controllers can be told *not* to be forwarded for couches that pass the pad through some other way, and plugin output shows up in the web console's log page instead of nowhere at all. +This release is mostly about making 0.23.0 right. **If you stream to an Android phone or to the Linux or Windows desktop app, update the client** — 0.23.0's new send-a-frame-in-pieces delivery had a fault that could throw most of the video away on a busy link, and the fix lives in the client. The other big one is **Automatic bitrate**, which could decide early in a session that your connection was worth 20 Mb/s and then hold you there for the rest of it: a sweep of that path fixed eleven separate faults, and a session that used to need seventeen minutes to climb out now does it in about sixteen seconds. Alongside those: the Linux and Windows app finally gets the frame scheduler the phone and Apple apps have had, the Steam Deck plugin reaches every setting instead of nine of them, controllers can be told *not* to be forwarded for couches that pass the pad through some other way, and plugin output shows up in the web console's log page instead of nowhere at all. ## New @@ -28,6 +28,14 @@ This release is mostly about making 0.23.0 right. **If you stream to an Android - **0.23.0 could freeze the picture on Android and on the Linux and Windows app.** 0.23.0 started cutting each frame into pieces and sending them as they were produced. The receiving side then sized every arriving frame at the largest a frame is ever allowed to be — many megabytes — so its budget for frames-in-progress ran out after about three. With normal traffic that meant twelve frames arriving and nine of them thrown away before a single byte could be placed: on any link that reorders packets at all, a loss storm that does not end. Frames never complete, the picture freezes, and the client keeps begging for a fresh one. A second fault in the same path killed one frame in roughly every 1408 outright — about once every twelve seconds at 120 fps, each costing a freeze and a recovery. Only Android and the Linux/Windows desktop app ever took this path, which is why it read as a platform-specific video fault in the field; the Apple apps and the Windows in-process client were never affected. **Updating the client is what fixes it** — a 0.23.0 client still has the bug whatever host it talks to. +- **Automatic bitrate could pin a session at 20 Mb/s for the rest of its life.** Sessions start at 20 Mb/s and climb, and a host refuses a climb while it is briefly behind on encoding. But an ordinary hitch at startup — which arrives while the rate is still at that 20 Mb/s floor — was enough to make the host refuse, and the client cannot tell a momentary refusal apart from an encoder that genuinely cannot go faster: both arrive looking identical. Two of them and the client concluded there was a permanent ceiling. Escaping cost 12.5% per minute, so crossing the gap to what a fast link could actually carry took upwards of twenty minutes, and in practice often never happened — which is why "my bitrate is stuck at 20" has been such a persistent report. Three things changed: the host no longer refuses climbs merely because it is running in its own high-effort mode (that mode exists so it *can* keep up — refusing climbs once it is working refuses the thing that worked), the client re-checks after 12 seconds instead of a minute and backs that off only if the limit proves real, and a request granted in full is taken as proof the limit is gone rather than nudging the guess upward. Measured end to end: a session pinned at 20 Mb/s under a 300 Mb/s connection now reaches 150 Mb/s in about sixteen seconds, where the same case previously needed around seventeen minutes. + +- **Automatic bitrate drew permanent conclusions from single moments.** Five more faults of the same shape. The reference points it compares against — network delay, decode time, encode time — could arm off *one* window, and since each is a rolling minimum that one window became the floor; a calm window followed by ordinary motion then read as congestion on a link that was never the problem. Changing resolution or refresh rate re-based only some of what it had learned, so switching *up* a mode was scored against the old mode's easier numbers and cratered the rate instead of raising it. The high-water mark that bounds how far every later climb may step never decayed and was raised by damaged windows — a stall's backlog arriving at once, or a flush's queue — which are exactly the windows that overstate what was delivered. The decoder ceiling latched *at* the rate that had just choked, authorising a climb straight back into the failure, and a network hiccup could be mistaken for a decoder one. And a manual bitrate cap bound only ceilings the speed test had learned, so it did nothing at all if the session already started above it. + +- **Your connection was measured including the redundancy sent to protect it.** Two checks compare what actually arrived against what the encoder was asked to produce, and both counted every byte accepted — packet headers, audio, and the extra error-correction data the host adds *in answer to* packet loss. So the measurement rose with the loss it was supposed to detect: at 25% redundancy the check passed while the encoder was emitting barely half its target, and the permanent high-water mark inherited that inflation for good. The signal was weakest on exactly the lossy links it exists for. Only the actual video payload is counted now. + +- **A host that re-chose the rate never told the client.** When a host rebuilds its pipeline it can legitimately re-pick an Automatic rate — a 1080p session mirroring a 4K panel needs roughly three times what it negotiated — but that number never reached the client, which kept its own stale copy as the basis for every later step. So a client believing 20 Mb/s while the host encoded 60 would compute its first climb from the stale figure and ask for 40: a request to go *down*, paying for an encoder rebuild to get there. The host now tells the client whenever the applied rate moves, using a message that already meant exactly that and which existing clients already handle arriving unprompted — no wire change, and older clients are unaffected. + - **On KDE hosts, a hidden mouse pointer stayed on screen.** Since 0.22.0 a KDE stream always had a cursor and it never went away again — not in a game, not in Big Picture, not with a controller in hand. The host was blending an arrow onto the picture forever because the signal KDE sends to say "the pointer is not here" was being ignored. It is honoured now, so a game that hides the pointer mid-stream actually hides it. GNOME hosts keep the behaviour they have, because there the same signal means something different and honouring it made the cursor flicker. - **A KDE host could stream a copy of your monitor instead of its own screen.** KDE remembers display arrangements per set of connected screens, and one of the things it remembers is "this screen mirrors that one". Because the streamed screen carries a stable name, any arrangement that had ever recorded it as a mirror got that re-applied on every later session that reproduced the same set of monitors — which is why it looked so arbitrary: the stream cloned the panel whenever exactly one monitor was live, and behaved normally the moment the others came back. A mirroring screen is not its own desktop; it takes the physical screen's size and viewport instead of the one you negotiated. The streamed screen now says outright that it mirrors nothing, and if it ever finds itself mirroring anyway it says so in the log instead of leaving that as something only you can see. @@ -70,7 +78,9 @@ This release is mostly about making 0.23.0 right. **If you stream to an Android - **`PanelGrid`, shared.** The panel-period learner is extracted to `punktfunk_core::phase::PanelGrid` and adopted by the Android, iOS and desktop presenters, replacing three copies with the same bug: a seed capped at the display mode's refresh, when the mode is only a *claim*. Narrowing is immediate; widening needs eight consecutive agreeing observations and then takes the narrowest of them. `preferredDisplayModeId` on Android is a request the system may refuse, which is the case that made this a session-length failure. -- **ABR probe measurement.** The reassembler stamps probe-scoped counters (bytes, packets, first/last arrival, monotonic ns) at `FLAG_PROBE` routing, so the throughput divisor is the client's first→last arrival interval and video around the burst contaminates neither numerator nor denominator; the host duration remains the fallback below two probe packets. `ProbeOutcome`/`PunktfunkProbeResult` layouts are unchanged. `PUNKTFUNK_ABR_MAX_MBPS` now clamps inside `set_ceiling` — the one funnel every learned ceiling passes through. The controller latches `decode_cap_kbps` when two *consecutive* backoffs carry decode-severe evidence at a similar pre-backoff rate (mirroring `host_cap_kbps`), re-probing on the `CAP_REPROBE_WINDOWS` clock; one spurious flush never latches. +- **ABR probe measurement.** The reassembler stamps probe-scoped counters (bytes, packets, first/last arrival, monotonic ns) at `FLAG_PROBE` routing, so the throughput divisor is the client's first→last arrival interval and video around the burst contaminates neither numerator nor denominator; the host duration remains the fallback below two probe packets. `ProbeOutcome`/`PunktfunkProbeResult` layouts are unchanged. The controller latches `decode_cap_kbps` when two *consecutive* backoffs carry decode-severe evidence at a similar pre-backoff rate (mirroring `host_cap_kbps`), re-probing on the `CAP_REPROBE_WINDOWS` clock; one spurious flush never latches. + +- **ABR sweep — eleven defects.** Wire format and ABI untouched throughout; 34 abr tests plus 2 host tests. Host side: `cadence_degraded` was latched true for as long as the session was *escalated* (adaptive capture depth or pipelined retrieve), independent of whether encode was still missing deadlines — and escalation needs only ~20 net behind-frames, which a startup hitch supplies while ABR is still in slow start at the 20 Mbps default. The rule moves into `encode_behind_cadence`: an escalated session is still judged strictly (any net behind-frame keeps it flagged, where an unescalated one gets the full bucket), but escalation alone no longer flags it. `adopt_built_bitrate` now publishes the rate a rebuilt pipeline actually opened at (`build_pipeline` re-resolves an Automatic rate whenever the source delivers an unnegotiated size — the mirrored-panel case — and the encoder's clamp can land below what control already acked), pushed to the control task as the existing 9-byte `BitrateChanged`. Client side: all three rolling baselines (OWD, decode, encode) now require `BASELINE_MIN_WINDOWS` of evidence via one shared `score_baseline` — the three copies had drifted apart, and `on_ack` clears the encode baseline after every self-requested decrease, re-opening the one-sample hole each time. A mode switch rebases decode and OWD as well as encode, and drops `proven_kbps` with them. `proven_kbps` is raised only by *clean* windows (it never decays and holds permanent authority over climb step size, and the windows that overstate delivered throughput are precisely the damaged ones). `decode_cap_kbps` latches just *under* the choke rate, inside the ±1/8 band the evidence already required, and credits a bare jump-to-live flush only where the decode signal is absent. The two throughput-driven gates are fed data-shard payload counted at the reassembler's routing decision rather than `bytes_received` (which includes headers, FEC parity, probe filler and audio — at 25% FEC the utilization gate passed with the encoder emitting ~55% of target). `PUNKTFUNK_ABR_MAX_MBPS` binds at construction, not only on probe-learned ceilings, and a session starting above its cap steps down to it (no congestion signal will ever find that — the link is fine, the cap is policy). Cap escape re-probes after 12 s, doubling each time the lift is immediately re-learned, and a request granted **in full** at or above the cap drops the cap outright rather than nudging it +12.5%. An ack above the current ceiling raises the ceiling to meet it (`set_ceiling` still only ever raises, still clamps to `PUNKTFUNK_ABR_MAX_MBPS`), so a host-initiated re-target is not immediately stepped back down. Finally, a `SetBitrate` dropped by a full control queue no longer counts toward `MAX_UNACKED` — three of those retired the controller for the session while blaming an "older host". - **Frame pacing under `PUNKTFUNK_VDISPLAY_HZ_MULT`.** The frame-driven trigger enforced its pace as a per-gap floor only (sleep to 0.9×interval, wake on arrival), so an always-ready source settled at 1.11× the negotiated rate. `PaceBudget` accrues one frame of credit per interval of *real elapsed* time, caps at 1.25 frames of post-stall burst, and charges one per submitted frame — the 0.9 floor keeps its jitter headroom while the long-run average cannot exceed the rate. Anchoring to real elapsed time preserves the synchronous-encode overlap the arrival-anchored floor bought and cannot fight the phase lock's submit grid. The charge sits under the same guard as the gate: the legacy fixed tick paces by its own grid, and charging without accruing would bank unbounded debt. diff --git a/docs/releases/whatsnew/v0.24.0.txt b/docs/releases/whatsnew/v0.24.0.txt index 0e15a038..7d373bb2 100644 --- a/docs/releases/whatsnew/v0.24.0.txt +++ b/docs/releases/whatsnew/v0.24.0.txt @@ -1,4 +1,5 @@ -• Fixes a fault from 0.23.0 that could freeze the picture on a busy connection. Worth updating for this alone. -• Fixes a refresh-rate mix-up that left the picture juddering for a whole session when a phone refused the rate the app asked for. +• Automatic bitrate no longer gets stuck at 20 Mbps for a whole session. It now climbs to what your connection really carries in seconds, not minutes. +• Fixes a fault from 0.23.0 that could freeze the picture on a busy connection. +• Fixes a refresh-rate mix-up that left the picture juddering for a whole session. • A decoder hiccup no longer snowballs into a burst of broken frames. -• New "Forward controllers" switch, for setups where your pad already reaches the PC another way. +• New "Forward controllers" switch, for pads that reach the PC another way.