Repository navigation
Description
Pre-submit: searched existing issues (no open/closed request for client-driven resolution), reproduced on the latest version, this is a server-side request, host is Windows 10/11.
Feature Category
🖥️ Virtual Display
Priority
🟡 Medium — Nice to have
Problem Statement
When a client's viewport changes mid-stream — a desktop Moonlight window being resized, or an Android client rotating / docking into DeX — the stream resolution stays fixed at whatever was negotiated at connect time, so the picture is letterboxed or stretched instead of the host re-rendering at the new resolution/aspect (the way RDP reflows when you resize its window).
The host already does the opposite direction well: when the host display changes, video.dynamic_resolution_follow_display (PR #669) re-inits the encoder and notifies the client over 0x5507 (send_resolution_change, PR #424). And the inbound half already exists too: the 0x5506 IDX_DYNAMIC_PARAM_CHANGE handler has a RESOLUTION branch (param_type 0, payload [int32 type, int32 width, int32 height]) that calls handle_resolution_change() → display_device::session_t::get().configure_display(...), which changes the Windows display mode and triggers the capture/encoder reinit, then raises an IDR.
What's missing is the glue that makes a client safely drive this. The wire format and the encoder-reinit machinery already exist; this request is about exposing, gating, and finishing the path.
Proposed Solution
-
Capability advertisement. Add a flag a client can read (a serverinfo field, or a small capabilities endpoint mirroring the existing ABR capabilities pattern) so clients only surface a "match my window/display" option when the host actually accepts client-driven resolution. Today there is no way for a client to know this is allowed. (On the client side this would map naturally onto moonlight-common-c's
LiGetHostFeatureFlags()/LI_FF_*bitmask.) -
Allow/deny config toggle. A new
videooption, e.g.allow_client_resolution_change(default off), separate fromdynamic_resolution_follow_display(which is host→client follow). Off by default for safety; users who want RDP-style behavior opt in. -
Arbitrary resolutions via the virtual display.
configure_displaycan only apply modes the display actually exposes, so physical monitors are limited to their EDID mode list. When client-driven resolution is enabled, route the requested size to the bundled virtual display and inject it as a custom mode (even dimensions, clamped to bounds) so non-standard sizes like 1463×911 work. (I'm inferring the custom-mode injection for arbitrary client values isn't wired today — please correct if it already is.) -
Host-side debounce/coalesce. A window drag or a fold/unfold emits many intermediate sizes; coalesce requests and only mode-set after the size settles (~200–300 ms) to avoid display-mode-set storms.
-
Validation + revert. Clamp to even dimensions / min / max / encoder limits, reject unsupported sizes, and ensure a client-driven mode is reverted on disconnect (tie into the existing
dd_config_revert_on_disconnect).
The existing 0x5506 RESOLUTION receive path and the 0x5507 acknowledge-and-IDR path can stay exactly as they are — this is mostly capability advertisement + gating + virtual-display custom-mode injection + debounce around code that already exists.
Alternative Solutions
- Leave it host-only via
dynamic_resolution_follow_display(client never drives it) — doesn't solve the window-resize / rotation case. - Offer only a fixed set of selectable resolutions instead of arbitrary sizes — simpler, but doesn't reflow to an arbitrary window aspect like RDP.
Use Case
Streaming a Foundation virtual display to a resizable Moonlight window, or to an Android client that rotates / docks into DeX. Resize/rotate the client and the host re-renders at the matching resolution instead of letterboxing — no disconnect/reconnect.
Willing to Test?
Yes, I can build/test and provide feedback.
Additional Information
Per this repo's template I'm keeping this server-side only. The matching client work is filed as companion issues (linked below once opened): the PC client extends the prior request qiin2333/moonlight-qt#84 ("Change resolution on resize"), which was closed as effectively impossible — but Foundation's own 0x5506/0x5507 + configure_display work shows the hard parts already exist on the host.
References in this repo: 0x5506/0x5507 defs and send_resolution_change in src/stream.cpp; handle_resolution_change + configure_display (PR #424); dynamic_resolution_follow_display (PR #669).
I verified the client side against qiin2333/moonlight-common-c: the mic branch already parses 0x5506/0x5507 inbound and fires the resolutionChanged(width,height) callback that drives the decoder reinit, so the host→client half is already understood by the client. The only client-side gap is an outbound request API (no LiSend* for resolution exists yet).
Host Operating System
Windows
Windows Version
Windows 11 (24H2)
GPU Vendor
NVIDIA
Activity
AsafMah commentedon Jun 14, 2026
Client-side companion requests now open:
Both share one new
moonlight-common-coutbound API (LiSendResolutionChangeRequest→ 0x5506 RESOLUTION); the inbound 0x5507 apply/decoder-reinit path is already present in the client core.Yundi339 commentedon Jun 26, 2026
Hi, thank you for your PRs. We noticed recently that implementing these PRs is more complex than I imagined.
I'm not comfortable with the change of
moonlight-common-c. I think we should first add a new interface for quick adjustment of screen resolution. In this way, different moonlight will consider less.feat(stream): gate, advertise and safely apply client-driven resoluti…
AsafMah commentedon Oct 19, 2026
@qiin2333 @Yundi339 The client-driven resolution PRs are rebased onto the latest branches and reworked around your feedback. Could you take a look when you have time?
What changed since the last round
moonlight-common-cchange is now minimal and follows an existing pattern. Add LI_FF_CLIENT_RESOLUTION_CHANGE and a wire test for LiRequestResolutionChange qiin2333/moonlight-common-c#10 adds one function,LiSendResolutionChangeRequest(width, height), and one feature bit,LI_FF_CLIENT_RESOLUTION_CHANGE(0x400). The function checks the bit itself and returns-2when the host doesn't support it, the same way the cursor-shape sender (LiSetCursorMode/LI_FF_CURSOR_SHAPE) works. It's purely additive: nothing changes for a client that doesn't call it./serverinfofield. The host advertises support through the per-streamx-ss-general.featureFlagsbit, like cursor shape and remote text context. A Moonlight client only checksLiGetHostFeatureFlags()and calls one function. I think this addresses the concern that different Moonlight clients shouldn't need to handle much: there's nothing extra to parse or negotiate.0x5506RESOLUTION →configure_display→0x5507is unchanged (and/stream/settingsalready says RESOLUTION belongs on the control stream). Client-driven dynamic resolution: gate, advertise, and safely apply 0x5506 RESOLUTION requests #729 only adds an opt-invideo.allow_client_resolution_changesetting (off by default, with a web UI checkbox), validation and clamping, a rate limit, and the feature bit. master already does part of this (LE payload decode, VDD identity routing), so the PR is smaller than before.PRs
LiSendResolutionChangeRequest+LI_FF_CLIENT_RESOLUTION_CHANGEFor vplus, the PR doesn't add a second rotation option. It keeps the existing Follow device rotation (
/rotate-display). When the host supports client-driven resolution, the size request takes over from/rotate-displayso the two don't conflict.Suggested merge order: common-c #10 (and #13 whenever you like). After that, the two clients point their submodule back from my fork to the merged commit, and I'll update them. #729 is independent and off by default.
Testing: all five PRs build and pass their test suites (sunshine via MSYS2: the full gtest suite plus new tests; common-c on gcc and MSVC, with wire-format and teardown-race tests; qt on MSVC + Qt 6.12; vplus with 1012 unit tests, including new ones for the resize logic). The qt and vplus CI runs are waiting for workflow approval because they're fork PRs. I haven't run an end-to-end live resize against a real host yet. I'll post results when I have them, and I'm happy to help test on your side.
If you'd prefer a different shape (another feature bit, or a different interface), tell me and I'll adapt.
简要说明
LiSendResolutionChangeRequest和一个特性位LI_FF_CLIENT_RESOLUTION_CHANGE(0x400),与光标形状(LI_FF_CURSOR_SHAPE)的模式一致。纯新增,不影响现有客户端。x-ss-general.featureFlags声明支持,不需要新的 HTTP 接口或 serverinfo 字段。客户端只需检查LiGetHostFeatureFlags()并调用一个函数。allow_client_resolution_change),复用已有的 0x5506 →configure_display→ 0x5507 路径。AsafMah commentedon Oct 19, 2026
Live end-to-end smoke test update. Host is #729 at
966bbb2dand the PC client is qiin2333/moonlight-qt#104 at2cd3d7e0, on Windows with the ZakoVDD virtual display.Result: passes. Every client resize request was applied at the exact size and even-rounded (e.g. 1463x911 → 1462x910). Each request produced exactly one 0x5507 and one client decoder rebuild. Frames kept flowing (Rx FPS == decoded FPS) with 0 decode errors.
Fixed during testing (now in #729): when the VDD doesn't already advertise the requested size, it briefly came up at its preferred 3840x2160 before the requested mode applied. Capture followed that step, which caused an extra 4K encoder init, 0x5507 and decoder rebuild. The host now records the pending requested size per session and doesn't follow other display sizes until the display reaches it. That wait ends when apply fails, follows the actually-applied mode if it differs, and has a 10 s deadline. Only the 0x5506 path sets it. Covered by 9 new unit tests; the full suite is 774 pass / 0 fail. In the re-run: zero intermediate 4K reports or encoder inits.
Not yet covered live: the physical-display (non-VDD) path, in-session reconnect after a network drop, input coordinate mapping, Android (moonlight-vplus#354) on-device, macOS and Linux.
feat(stream): gate, advertise and safely apply client-driven resoluti…
feat(stream): gate, advertise and safely apply client-driven resoluti…
AsafMah commentedon Nov 1, 2026
@qiin2333 @Yundi339 感谢 #48 / V+ #780!客户端分辨率相关 PR 已全部对齐到
LiRequestResolutionChange:common-c #10(0x400 能力位,仅提示,不影响 #780)、#13(拆除竞争修复,已覆盖新接口)、Foundation #729(默认开启,#780 可直接使用)、qt #104 和 V+ #354(只做自动跟随)。CI 都等 fork 审批,有空看一下就行 🙏feat(stream): gate, advertise and safely apply client-driven resoluti…
P2-A1: managed virtual head resolution change (fakes, provisional pen…
P2-A2: managed head resolution change with admission make-before-break
GW-H3d: member per-head mode, and the managed lease admits its own re…
feat(stream): gate, advertise and safely apply client-driven resoluti…