Skip to content

[Feature]: Client-driven dynamic resolution (RDP-style) — finish the existing 0x5506 RESOLUTION path #728

Open

Description

@AsafMah

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

  1. 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.)

  2. Allow/deny config toggle. A new video option, e.g. allow_client_resolution_change (default off), separate from dynamic_resolution_follow_display (which is host→client follow). Off by default for safety; users who want RDP-style behavior opt in.

  3. Arbitrary resolutions via the virtual display. configure_display can 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.)

  4. 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.

  5. 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

  1. AsafMah commented on Jun 14, 2026

    @AsafMah
    ContributorAuthor

    Client-side companion requests now open:

    Both share one new moonlight-common-c outbound API (LiSendResolutionChangeRequest → 0x5506 RESOLUTION); the inbound 0x5507 apply/decoder-reinit path is already present in the client core.

  2. Yundi339 commented on Jun 26, 2026

    @Yundi339
    Member

    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.

  3. AsafMah commented on Oct 19, 2026

    @AsafMah
    ContributorAuthor

    @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

    PRs

    Repo PR What
    foundation-sunshine #729 Opt-in setting, validation, rate limit, feature bit
    moonlight-common-c qiin2333/moonlight-common-c#10 LiSendResolutionChangeRequest + LI_FF_CLIENT_RESOLUTION_CHANGE
    moonlight-common-c qiin2333/moonlight-common-c#13 Control-stream send/teardown race fix (independent)
    moonlight-qt qiin2333/moonlight-qt#104 Resize the stream to match the window (opt-in)
    moonlight-vplus qiin2333/moonlight-vplus#354 Follow window / external display (opt-in)

    For 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-display so 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.


    简要说明

    • moonlight-common-c 的改动已缩减为一个新增函数 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 路径。
    • 五个 PR 均已 rebase 到最新分支,并通过构建和测试。qt 和 vplus 的 CI 需要维护者批准 fork PR 的工作流后才能运行。
  4. AsafMah commented on Oct 19, 2026

    @AsafMah
    ContributorAuthor

    Live end-to-end smoke test update. Host is #729 at 966bbb2d and the PC client is qiin2333/moonlight-qt#104 at 2cd3d7e0, 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.

    Step Result
    Launch 1280x720 windowed 1:1; the stream-start 0x5507 is only recorded
    Resize to 1600x900, 1463x911 (1462x910) applied
    Maximize (3840x2054) and restore applied
    Fullscreen enter (configured 1280x720) / leave (window size) applied
    Close + resume, then resize starts 1:1, resize applied
    Quit VDD removed, display settings restored
    Feature off (allow disabled) no 0x400 in featureFlags; the client sends nothing; the host receives zero 0x5506

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions