Repository navigation
Pace a headless output and the stream to the client's refresh rate - #117
londospark wants to merge 4 commits into
Conversation
The ladder judged a rung affordable at 0.03 bits per pixel per stream, so
a 2560x1600 Dual420 stream at 15 Mbit/s ran at 60 fps with visibly blocky
text; sharp desktop text wants about 0.1. The rungs were also coarse
(60, 30, 15, 10, 5), and the bottom two halved the resolution.
The ladder now chooses a rung { aux, fps } directly: the highest frame
rate the measured rate pays for at --quality-bpp (0.08 by default),
keeping the 4:4:4 chroma stream while that is at least 30 fps, then
Single420, down to 5 fps. The stream is always the window's size.
Hysteresis is unchanged, plus no move smaller than 15% of the frame rate.
The bitrate ceiling is raised from 80 to 250 Mbit/s (still bounded by
pixels x streams x fps x 0.1) so a large wired stream can pay for full
quality.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The PR appears safe to merge, with the remaining direct-connection cap issue non-blocking. Findings
SummaryThe PR follows the client's monitor refresh for headless outputs and uses the output refresh as the stream ceiling.
|
| if cli.max_fps > 0 { | ||
| args.push("--max-fps".into()); | ||
| args.push(cli.max_fps.to_string()); |
There was a problem hiding this comment.
Direct connections ignore the cap
The client's --max-fps has no effect with --connect. Its only use is adding arguments to an SSH-launched server, while direct connections still send the uncapped monitor refresh. Users can receive a higher frame rate than they requested. Apply the cap for direct connections too, or reject this option combination instead of silently ignoring it.
There was a problem hiding this comment.
Fixed in a0e32d6: the client caps the refresh it sends at --max-fps, so the cap also holds with --connect. Over SSH it is still passed to the server as well, where it also caps a mirrored output.
…return After the ladder dropped to Single420, RateController bounded the target by one stream (6 x pixels at 60 fps), but restoring Dual420 at 30 fps with 1.25x headroom needs 7.06 x pixels, so a recovered link never got full chroma back. Bound it by the most streams the session has used; the per-stream bitrate still divides by the active count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A headless session ran at 60 Hz and at most 60 fps whatever the client's display: Resize carried only size and scale, headless outputs were moded at a hard-coded 60 Hz, and MAX_FPS = 60 capped every session. - Resize gains refresh_hz (Option, so older clients decode as None): the refresh of the Hyprland monitor holding the client's window, re-sent when the window moves to another monitor. - The server modes a headless output at that refresh and uses the output's refresh as the session's frame-rate ceiling, for mirrored outputs too (a 165 Hz monitor streams at up to 165 fps). The ladder picks the frame rate below it from the link. - --max-fps (client and server) caps the output mode and the stream. - The client polls decoded frames every 2 ms instead of 8, so a stream above ~120 fps is not held to the poll rate. - gliff-probe stream-bench --refresh sends the new field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- --max-fps below 5 no longer panics the sustainable-fps clamp. - A refresh-only resize rebounds the rate controller to the new ceiling (apply_stream_params reconfigures it on the no-rebuild path). - A Resize without refresh_hz keeps the current refresh but still applies --max-fps, so the cap holds for older clients. - The client reads its monitor's refresh on a worker thread; the GTK thread only takes the result. --max-fps caps the refresh it sends, so it also holds with --connect. - gliff-probe stream-bench --refresh works without --width/--height. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b52a050 to
a0e32d6
Compare
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Builds on #116. This branch contains its commits, so review or merge that first. The changes in this PR are the last two commits,
f249b9aanda0e32d6(review fixes).Fixes #114
What happens
A headless session runs at 60 Hz and at most 60 fps whatever the client's display runs at:
Resizecarries no refresh rate.MAX_FPS = 60caps every session.The change
ClientMsg::Resizegainsrefresh_hz: Option<u32>at#[n(3)], following the protocol rules. An older client decodes asNone, and an older server ignores the extra field. A test covers the older client'sResize.hypr_ipc::Instance::clients. It checks once a second and re-sendsResizewhen the window moves to a monitor with a different refresh.resize. A refresh-only change re-paces without rebuilding the encoder.MAX_FPS, for mirrored outputs too: a 165 Hz monitor streams at up to 165 fps.MAX_FPS_LIMIT = 480only bounds nonsense values. The ladder from Pick the frame rate from the link, never reduce resolution #116 picks the frame rate below the ceiling from the link.--max-fps N(client and server) caps the output mode and the stream.gliff-probe stream-bench --refreshsends the new field.On Omarchy and other Lua configs, the mode change needs #110 (Hyprland 0.57 answers
keywordwithunknown request). Without #110 the refresh still reaches the session's ceiling, but the output keeps its mode.Testing
scripts/check.shpasses.gliff-probe stream-bench --refresh 240moves the headless output from @60 to @240, andfps_capinStreamConfigfollows.scripts/e2e.sh.🤖 Generated with Claude Code