Skip to content

Add option to match the client display's resolution and frame rate - #2008

Open
benjweaver wants to merge 1 commit into
moonlight-stream:masterfrom
benjweaver:auto-display-mode-pr
Open

benjweaver wants to merge 1 commit into
moonlight-stream:masterfrom
benjweaver:auto-display-mode-pr

Conversation

@benjweaver

@benjweaver benjweaver commented Sep 9, 2026 •

Copy link
Copy Markdown

What this does

The resolution and frame rate settings each gain an Automatic (Match Client Display) entry, which sits at the top of its list.

The difference from picking a specific resolution is when it's resolved. A fixed choice is baked in when the setting is saved. Automatic is resolved when the stream starts, so moving the window to another display, docking a laptop, or connecting a different monitor doesn't require visiting settings first.

This is aimed at the case where one client is used with more than one display, which today means either editing the setting each time or streaming at the wrong resolution.

Behavior

Resolution uses the display's safe area rather than its full native resolution, so video isn't rendered underneath a notch. On displays without one the two are identical.

It is capped at the decoder's maximum. The settings page already refuses to offer fixed resolutions above that limit, and automatic mode honors the same limit rather than silently handing the decoder something it will reject.

Frame rate matches the rate the display is running at right now, not the highest rate it advertises. Picking the highest supported rate would change the stream's frame rate on a display the user has deliberately set lower.

If detection fails, the saved resolution or frame rate is used and a warning is logged. Automatic mode never leaves the stream without a usable value.

Bitrate is recalculated for the mode actually selected, since the saved default was computed for the saved resolution and frame rate. A bitrate the user set themselves is left alone.

The recalculated value is clamped to the maximum the bitrate slider offers, so automatic mode can't arrive at a bitrate the user couldn't have chosen in the UI. That ceiling was a literal in the QML, and is now StreamingPreferences::getMaxBitrate() so the slider and this path share one definition rather than repeating the numbers.

Display selection reuses the display the Qt UI is on, matched to an SDL display by origin, which is the same display the stream window will be created on.

Command line

--auto-resolution and --auto-fps, each with a --no- counterpart. An explicit --resolution or --fps on the same command line wins, so existing invocations are unaffected.

The GUI probes the decoder's maximum resolution asynchronously at startup, but the command line stream path doesn't run that probe. Rather than skip the cap there, the probe runs on demand the first time automatic resolution needs it.

Implementation notes

Most of the change is the settings page and the new Session::overrideStreamConfigForClientDisplay(), which runs after the stream config is populated from preferences and before the window is created.

Automatic frame rate calls one new helper, StreamUtils::getCurrentRefreshRate(), which reports the rate the display is running at now. Not the highest it supports, for the reason given under Behavior above.

It rounds that result the same way the settings page rounds the rates it lists, snapping 58 to 62 onto 60 and 28 to 32 onto 30. A panel running at 59.94 Hz is reported by SDL as 59, and without this it would produce a 59 FPS stream.

That rounding is currently an if/else inline in SystemProperties::refreshDisplays(), so it becomes StreamUtils::normalizeRefreshRate() and both callers use it. refreshDisplays() keeps its own loop and everything else about it; only the inline rounding is replaced, by one call.

SDL_InitSubSystem(SDL_INIT_VIDEO) moves to the top of Session::initialize(). Automatic mode needs SDL display information before it can compute the stream config, and the config has to be settled before the window is created. The macOS notch check that previously ran before SDL initialization now runs after it, and compares against the resolution actually being streamed rather than the saved preference, which is the point of the reordering.

getNativeDesktopMode() is deliberately untouched.

Testing

Streamed with automatic resolution and frame rate enabled on Linux and macOS.

On a Steam Deck OLED, using both the Flatpak and the AppImage, streaming to the built-in display at its native 1280×800 and 90 Hz, and to a 4K TV at 3840×2160 and 60 Hz. Automatic mode picked up each display's resolution and frame rate without changing the setting in between, which is the case this is for. The two differ in both dimensions, so this exercises the frame rate path as well as the resolution one.

On macOS on Apple silicon, including with a Sidecar display.

Notch handling was checked separately on a built-in Retina display, which reports a native resolution of 3456×2234 against a safe area of 3456×2160. Automatic resolution selects the safe area, so video is placed below the notch rather than under it. Detection was also exercised with mirroring enabled.

CI builds pass on macOS, Windows x64 and arm64, the Linux AppImage, and Steam Link.

Not covered: Windows at runtime, a display larger than the decoder's maximum, and the detection-failure fallbacks. The last two are exercised only by inspection.

Prebuilt binaries

If trying it is easier than building it:

https://github.com/benjweaver/moonlight-qt/releases/tag/v0.9

macOS, Windows x64 and arm64, Linux AppImage, Flatpak, and Steam Link. They are built from this change on the same upstream base, plus fork-only commits that add the release workflow and tag the version string so the settings page identifies which build is running. The feature code is identical to this branch. The Windows and macOS builds are unsigned: SmartScreen will warn on first launch, and macOS needs the app allowed under Settings > Privacy & Security.

These are unofficial builds from my fork, offered only to save reviewers a build.

The resolution and frame rate settings each gain an "Automatic (Match
Client Display)" option. Unlike picking a specific resolution, automatic
mode is resolved when the stream starts rather than when the setting is
saved, so moving the window to another display or connecting a different
one doesn't require changing the setting first.

Resolution uses the display's safe area so video isn't rendered under a
notch, and is capped at the decoder's maximum, which the settings page
already enforces for the fixed entries. Frame rate matches the rate the
display is running at now rather than the highest it supports. If either
can't be detected, the saved value is used and a warning is logged.

The default bitrate is recalculated for whatever mode we end up with,
unless the user set their own bitrate.

Also adds --auto-resolution and --auto-fps for streaming from the command
line. That path doesn't run the startup decoder probe the GUI does, so the
decoder maximum is probed on demand there.
@benjweaver
benjweaver force-pushed the auto-display-mode-pr branch from 20a920f to 3c2d163 Compare October 4, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant