Skip to content

Client-driven dynamic resolution: gate, advertise, and safely apply 0x5506 RESOLUTION requests#729

Open
AsafMah wants to merge 5 commits into
AlkaidLab:masterfrom
AsafMah:feat/client-driven-resolution
Open

Client-driven dynamic resolution: gate, advertise, and safely apply 0x5506 RESOLUTION requests#729
AsafMah wants to merge 5 commits into
AlkaidLab:masterfrom
AsafMah:feat/client-driven-resolution

Conversation

@AsafMah

@AsafMah AsafMah commented Jun 14, 2026

Copy link
Copy Markdown

Summary

Implements the server side of client-driven ("RDP-style") dynamic resolution: when a client requests a stream resolution change mid-session, the host validates and applies it, then notifies the client. Closes #728.

This builds on the existing 0x5506/0x5507 machinery (PRs #424/#669) and adds the gating, capability advertisement, and safety pieces that were missing for a client to safely drive it.

What it does

  • Opt-in config video.allow_client_resolution_change (default off) — accept client 0x5506 RESOLUTION requests.
  • Capability advertisement in /serverinfo: <ClientResolutionChange> is emitted as 1 only when allow_client_resolution_change && dynamic_resolution_follow_display, since the encoder-resolution update + 0x5507 notify in the capture loop are gated on dynamic_resolution_follow_display. Advertising honestly avoids telling clients to send requests the host would only half-apply.
  • Apply path (handle_resolution_change): even-clamp, no-op dedupe, leading-edge 250 ms rate-limit (storm protection; clients also debounce), then configure_display. Session resolution is mutated and an IDR raised only on configure_result_t::result_e::successdeferred_retry and failures keep the old state and allow a retry.
  • Correct display routing: the temporary launch session preserves the original launch env (client display name, client cert / VDD identity), use_vdd, and custom_screen_mode, and forces resolution_change/refresh_rate_change to automatic so arbitrary client sizes inject into the VDD mode list.
  • Decodes the 0x5506 payload with explicit little-endian reads (no unaligned reinterpret_cast).

Client side

Requires the shared common-c API and the matching clients:

Known limitation

Client-driven resolution currently takes effect end-to-end only when dynamic_resolution_follow_display=true (the capture loop's encoder-size update + 0x5507 notify path is gated on that flag). The capability is advertised accordingly. Wiring the capture loop to also fire for client-driven changes when follow-display is off is a sensible follow-up.

Testing

Reviewed across two independent adversarial + correctness passes; all raised findings addressed. Not built in this environment (no local MSVC/CUDA/boost toolchain) — static review only. A maintainer build + a live client-resize smoke test (physical display and VDD) is the recommended gate before merge, per this repo's AI-code testing guidance.

Opened by a regular contributor (AsafMah).

AsafMah added 4 commits June 14, 2026 17:45
Implements the server-side half of AlkaidLab#728.

Changes:
- config: add video.allow_client_resolution_change (bool, default false).
  Distinct from dynamic_resolution_follow_display (host->client follow);
  this gates the reverse direction (client->host 0x5506 RESOLUTION).
- stream: gate the IDX_DYNAMIC_PARAM_CHANGE / RESOLUTION branch on
  allow_client_resolution_change; log+ignore when disabled.
- stream: clamp requested dims to even values (H.264/HEVC require even
  dimensions; done after range validation, before any other checks).
- stream: add per-session leading-edge rate-limit (250 ms window) via
  session_t::client_res_last_apply to drop rapid duplicate requests.
  Clients (moonlight-qt, vplus) already debounce ~400 ms before sending
  one settled packet, so the first request always applies immediately.
- stream: note that dd_config_revert_on_disconnect restores display mode
  on disconnect — no extra teardown needed here.
- nvhttp: advertise <ClientResolutionChange> in /serverinfo response so
  clients can gate outbound 0x5506 RESOLUTION packets on host capability.

The host->client follow path (dynamic_resolution_follow_display /
send_resolution_change / 0x5507) is entirely unchanged.

Ref: AlkaidLab#728
 AlkaidLab#9 AlkaidLab#16 for client-driven resolution

AlkaidLab#16 (native-int parser): Replace all reinterpret_cast<const int*> reads in the
IDX_DYNAMIC_PARAM_CHANGE handler with util::endian::little() reads to guarantee
correct little-endian decoding on any host regardless of native byte order.
Float (FPS) is bit-cast through a LE u32 via memcpy.

AlkaidLab#3 (display failures logged as success): Check the configure_result_t returned
by configure_display().  On hard failure: log result code + message + hint,
keep old session->config.monitor, skip IDR raise so the encoder stays
consistent, and do not update any dedup timestamp (future retries are allowed).
On success or deferred_retry: proceed as before.

AlkaidLab#4 (capability can advertise no-op paths): Client-driven resolution requests
are independent of the host's passive-follow toggle
(dynamic_resolution_follow_display) and of the global video.resolution_change
option.  Fix: build a local config::video_t copy with resolution_change forced
to automatic before passing it to configure_display, so the requested size is
always applied when allow_client_resolution_change is on, regardless of whether
the user set resolution_change=no_operation.  Capability advertisement on
allow_client_resolution_change alone is therefore correct and sufficient.

AlkaidLab#5 (temp launch session loses display semantics): Save launch_use_vdd and
launch_custom_screen_mode from the original launch_session_t in session_t
during alloc().  Build the temp launch session with these saved values so
configure_display targets the correct device (physical vs VDD) and honours the
original custom screen-mode override.  custom_screen_mode defaults to -1 (no
override) — not 0 (which is a valid screen-mode index).  enable_sops is
forced true in the temp session because the client is explicitly requesting the
change; the automatic path's enable_sops gate would otherwise silently drop it.

AlkaidLab#9 (host rate-limit drops final size): Remove the 250 ms leading-edge
rate-limit entirely.  Clients (moonlight-qt, moonlight-vplus) already debounce
~400 ms before emitting one settled packet; a host-side drop window is both
redundant and harmful — it would swallow the final settled size if it arrived
within 250 ms of a prior one.  Correctness beats incomplete storm protection.
Decode 0x5506 dynamic-resolution fields by copying bytes before little-endian conversion instead of dereferencing payload storage as int pointers. This removes the alignment/aliasing hazard from the client-driven resolution parser while keeping the existing wire format.
 AlkaidLab#5 AlkaidLab#6 AlkaidLab#7 for client-driven resolution

Fix AlkaidLab#1: gate ClientResolutionChange advertisement on BOTH
allow_client_resolution_change AND dynamic_resolution_follow_display.
Without follow_display the encoder-size update and 0x5507 notify never
fire, so advertising the capability is misleading and would produce a
broken client experience.

Fix AlkaidLab#4: preserve launch_session.env in session_t (new launch_env field,
saved in alloc()) and copy it into temp_launch_session before
configure_display.  Fixes SUNSHINE_CLIENT_DISPLAY_NAME routing
(parsed_config.cpp:535) and SUNSHINE_CLIENT_CERT_UUID VDD client-id
matching (session.cpp:189/548) which were silently lost in the zero-init
temp session.

Fix AlkaidLab#5: gate session->config.monitor mutation + IDR on result_e::success
only.  operator bool() returns true for deferred_retry too; mutating
session state and raising IDR before the mode applied was premature.
On deferred_retry or hard failure: log, keep old config, no IDR, allow
future retry (client_res_last_apply already set so the rate-limit still
applies but the state is not corrupted).

Fix AlkaidLab#6: leading-edge 250 ms rate-limit in handle_resolution_change,
checked after even-clamp + no-op dedupe and before configure_display.
Sheds resolution-change storms; a single settled packet still applies
immediately.  Residual: a sub-250 ms burst of DISTINCT sizes may have
one dropped.

Fix AlkaidLab#7: force client_driven_video_cfg.refresh_rate_change = automatic
alongside resolution_change = automatic.  Without this a host setting of
refresh_rate_change=no_operation would skip VDD custom-mode injection
and the new fps from temp_launch_session would be ignored.
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 787a3640-ed38-443d-b736-3ec61f09f1d6

📥 Commits

Reviewing files that changed from the base of the PR and between b999f21 and b86e892.

📒 Files selected for processing (1)
  • src/stream.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/stream.cpp

Summary by CodeRabbit

发行说明

  • New Features
    • 新增 allow_client_resolution_change 配置项,默认不启用,用于控制是否接受客户端分辨率变更请求。
    • 服务端在 serverinfo 中通告客户端是否可发送分辨率变更请求。
    • 分辨率变更请求加入 250ms 前沿限流保护,并对尺寸做校验与规范化后再应用。
  • Bug Fixes
    • 改进动态参数的端序与数值解析,提升跨平台兼容性。
    • 仅在显示配置成功时才更新当前监视器并触发相关刷新行为,失败/延迟不改动现有设置。

功能概览

新增 allow_client_resolution_change 配置项(默认 false),在 serverinfo 中广播该能力;重写 handle_resolution_change,加入门控、偶数对齐、250ms 前沿限流、显示路由与 configure_display 结果分支处理;修复 IDX_DYNAMIC_PARAM_CHANGE 中的 little-endian 解析;session::alloc 持久化 launch 路由信息供运行时重配使用。

变更详情

客户端驱动分辨率变更完整路径

Layer / File(s) Summary
配置项定义与解析
src/config.h, src/config.cpp
config::video_t 新增 allow_client_resolution_change 布尔字段,默认 falseapply_config 补充该键的布尔解析与写入。
serverinfo 能力广播
src/nvhttp.cpp
serverinfo 响应新增 ClientResolutionChange 字段,仅当 allow_client_resolution_changedynamic_resolution_follow_display 均为 true 时返回 1,否则返回 0
session_t 路由与限流状态
src/stream.cpp
session_t 新增 launch_use_vddlaunch_custom_screen_modelaunch_envclient_res_last_apply 四个字段;session::alloc 在初始化时从 launch session 填充前三个字段。
handle_resolution_change 重写
src/stream.cpp
增加门控、宽高偶数对齐与范围校验、无变化忽略、250ms 前沿限流;构建临时 launch_session_t 保留路由字段并强制 enable_sops/automatic 模式;按 configure_display 返回的 success/deferred_retry/失败决定是否更新 session->config.monitor 与触发 IDR。
IDX_DYNAMIC_PARAM_CHANGE 端序安全解析
src/stream.cpp
新增 read_le_i32 helper 替代 reinterpret_cast;RESOLUTION 分支改用该 helper 读取宽高;FPS 分支改为位级 little-endian 重组 float;通用整型参数同步改用 read_le_i32

交互序列图

sequenceDiagram
    participant Client as 客户端
    participant nvhttp as serverinfo (nvhttp)
    participant stream as stream (handle_resolution_change)
    participant dd as configure_display

    Client->>nvhttp: GET /serverinfo
    nvhttp-->>Client: ClientResolutionChange=1 (当两项配置均启用)

    Client->>stream: IDX_DYNAMIC_PARAM_CHANGE (0x5506 RESOLUTION, w, h)
    stream->>stream: read_le_i32 解析宽高
    stream->>stream: 门控检查 (allow_client_resolution_change)
    stream->>stream: 偶数对齐 / 范围校验 / 无变化检测
    stream->>stream: 250ms 前沿限流检查
    stream->>dd: configure_display(临时 launch_session_t)
    alt success
        dd-->>stream: success
        stream->>stream: 更新 session->config.monitor
        stream->>Client: 触发 IDR
    else deferred_retry / 失败
        dd-->>stream: deferred_retry / failure
        stream->>stream: 不更新监视配置,不触发 IDR
    end
Loading

代码审查工作量估计

🎯 4 (复杂) | ⏱️ ~50 分钟

可能相关的 PR

  • AlkaidLab/foundation-sunshine#669:本 PR 通过 allow_client_resolution_changedynamic_resolution_follow_display 的双重门控,直接耦合了该 PR 中引入的动态分辨率跟随显示的功能。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: gating client-driven resolution requests, advertising the capability, and safely applying 0x5506 RESOLUTION requests.
Description check ✅ Passed The description provides comprehensive context about client-driven dynamic resolution implementation, configuration options, request processing, and display routing, all directly related to the changeset.
Linked Issues check ✅ Passed The PR fully addresses issue #728 requirements: adds opt-in config, advertises capability in /serverinfo, implements rate-limiting and validation, handles little-endian decoding, and preserves launch session state for correct display routing.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to issue #728: configuration parsing, capability advertisement, request validation/rate-limiting, and display routing—no unrelated modifications present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/stream.cpp`:
- Around line 1402-1407: The capability gate for client resolution changes in
the request handler is inconsistent with the capability declaration in
src/nvhttp.cpp. Currently, the code only checks allow_client_resolution_change,
but the capability declaration specifies that ClientResolutionChange requires
both allow_client_resolution_change AND dynamic_resolution_follow_display to be
enabled. Update the condition in the if statement (where
allow_client_resolution_change is checked) to also require
dynamic_resolution_follow_display to be true. Both config options must be
enabled for the resolution change to be processed; if either is disabled, log
the rejection and return early.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b496cb4-8b17-4b5b-a715-ea096ec24c09

📥 Commits

Reviewing files that changed from the base of the PR and between 2f3442e and b999f21.

📒 Files selected for processing (4)
  • src/config.cpp
  • src/config.h
  • src/nvhttp.cpp
  • src/stream.cpp
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.{cpp,c,h}

⚙️ CodeRabbit configuration file

src/**/*.{cpp,c,h}: Sunshine 核心 C++ 源码,自托管游戏串流服务器。审查要点:内存安全、 线程安全、RAII 资源管理、安全漏洞。注意预处理宏控制的平台相关代码。

Files:

  • src/nvhttp.cpp
  • src/config.h
  • src/config.cpp
  • src/stream.cpp

Comment thread src/stream.cpp Outdated
The serverinfo capability is advertised only when both allow_client_resolution_change and dynamic_resolution_follow_display are enabled, but the 0x5506 handler gated on the former alone. A client that ignores or caches the capability bit (or crafts the packet) could reach configure_display() with follow_display off, where the capture loop keeps the original stream resolution and never notifies the client -- a half-changed host state. Require both flags in the handler so it matches the advertised contract.

Addresses CodeRabbit review on AlkaidLab#729.
@AsafMah

AsafMah commented Jun 15, 2026

Copy link
Copy Markdown
Author

Good catch — fixed in b86e892. The handle_resolution_change gate now requires both allow_client_resolution_change && dynamic_resolution_follow_display, matching the ClientResolutionChange serverinfo advertisement in nvhttp.cpp. This closes the gap where a client ignoring/caching the capability bit (or crafting a 0x5506 packet) could reach configure_display() while follow_display is off — which would change the host display without the capture loop following or notifying the client.

@AsafMah

AsafMah commented Jun 25, 2026

Copy link
Copy Markdown
Author

@qiin2333 anything I can do to promote this and the other PRs?

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.

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

1 participant