Skip to content

perf(windows): drop trailing retry backoff and instrument DDA init - #862

Open
qiin2333 wants to merge 2 commits into
masterfrom
perf/dda-init-latency
Open

perf(windows): drop trailing retry backoff and instrument DDA init#862
qiin2333 wants to merge 2 commits into
masterfrom
perf/dda-init-latency

Conversation

@qiin2333

Copy link
Copy Markdown
Collaborator

Reduces DXGI capture reinit latency, and adds the measurement needed to decide what to do next.

The bug

All three DDA retry loops sleep 200 ms after every failed attempt — including the final one, where the loop exits immediately afterward so the delay cannot affect the outcome:

for (int x = 0; x < 2; ++x) {
  status = ...DuplicateOutput...;
  if (SUCCEEDED(status)) break;
  std::this_thread::sleep_for(200ms);   // x == 1: sleeps, then the loop ends
}

Sites: both DuplicateOutput paths in duplication_t::init, and test_dxgi_duplication.

test_dxgi_duplication is the expensive one. display_base_t::init wraps its output scan in a three-pass retry and calls the test once per desktop-attached output on every pass — so an attached but non-duplicatable output pays the useless 200 ms three times per init. And that init runs on every capture reinit, because the reinit path calls refresh_displays()platf::display_names() (video.cpp:1745).

Modeled cost of a fully failing scan:

failing outputs before after saved
1 1200 ms 600 ms 600 ms
2 2400 ms 1200 ms 1200 ms
3 3600 ms 1800 ms 1800 ms

Why this is safe

No success path changes. Both loop shapes were extracted into a native harness and compared across all 16 combinations of (attempt-1 result, attempt-2 result, enumeration_only, E_ACCESSDENIED):

  • outcome and attempt count identical in all 16
  • the new loop never sleeps more than the old
  • sleep counts match exactly on every path that succeeds or breaks early on E_ACCESSDENIED — including "succeeds on attempt 2", which still sleeps once

The only divergence is the all-attempts-failed path. That is the intent.

Instrumentation

One info line per init:

[Display Init] timing (ok): total=743.2ms, output_select=612.4ms (passes=2, dda_tests=3),
               device_create=8.1ms, hdr_probe=122.7ms (retries=1)

Emitted on success and on all three early-return failures.

Why: a3bd8799 attributed multi-second reinit to unfair-lock starvation and added short-timeout polling to mitigate it. But display_base_t::init also contains several seconds of worst-case fixed sleeps that compound inside the three-pass retry — Sleep(500) on the second pass, up to 700 ms of HDR metadata backoff, plus the per-output delays above. Which of the two actually dominates is not determinable by reading the code, so this measures it instead of guessing further.

Deliberately not done

Both are real but should be driven by the numbers above, not assumed:

  1. test_dxgi_duplication creates a fresh D3D11 device per output (display_base.cpp:473) where one per adapter would do. The device is only used for the DuplicateOutput probe.
  2. The HDR metadata backoff is not gated on the display being in HDR mode (display_base.cpp:873). is_hdr_metadata_valid only guards luminance values, which are consumed solely by get_hdr_metadata() under is_hdr() (G2084). An SDR display reporting zeroed luminance would burn the full 700 ms for nothing. The new hdr_probe/retries fields will show whether this fires in practice.

Testing

Not compile-verified — Windows-only translation unit, changed on macOS with no build tree. Verified structurally: braces balanced in all three edited functions (duplication_t::init, test_dxgi_duplication, display_base_t::init), and the short-circuit evaluation order is preserved in the one refactored condition (the output_accepted rewrite exists only to count probe calls and keeps is_rdp_session || / AttachedToDesktop && ordering intact, so the expensive probe still runs in exactly the same cases).

What a reviewer should check on Windows

  1. Build.
  2. Normal stream start and a capture reinit (change resolution or toggle HDR mid-stream) — confirm the new timing line appears and reinit still succeeds.
  3. Multi-monitor, especially with a display that fails duplication — this is where the saving lands. Confirm the correct output is still selected.
  4. A genuinely failing init (e.g. unplug the target display) — confirm it still fails cleanly, only faster.
  5. Worth noting from the logs: whether hdr_probe retries are non-zero on an SDR display. That decides item 2 above.

Not in scope

Dirty-rect capture (GetFrameDirtyRects/GetFrameMoveRects) is still unused — every frame is a full-screen CopyResource. That is a real gap but a design change: dirty regions must accumulate across frames when last_frame_variant is reused, and it is coupled to the cursor-blend state machine touched by #859. Should be its own change, driven by measurement.

🤖 Generated with Claude Code

The DDA retry loops sleep 200ms after *every* failed attempt, including
the final one, where the loop exits immediately afterward and the delay
cannot affect the outcome. Three sites do this: both DuplicateOutput
paths in duplication_t::init and the test in test_dxgi_duplication.

test_dxgi_duplication is the expensive one. display_base_t::init wraps
its output scan in a three-pass retry, and calls the test once per
desktop-attached output on every pass, so a system with an attached but
non-duplicatable output pays the useless 200ms three times per init. That
init runs on every capture reinit, since the reinit path calls
refresh_displays() -> platf::display_names() (video.cpp:1745).

Only sleep when another attempt follows. Modeled cost for a fully failing
scan: 1200ms -> 600ms with one such output, 3600ms -> 1800ms with three.

No success path changes. Verified by extracting both loop shapes into a
native harness and comparing across all 16 combinations of
(attempt-1 result, attempt-2 result, enumeration_only, E_ACCESSDENIED):
outcome and attempt count are identical everywhere, the new loop never
sleeps more than the old, and sleep counts match exactly on every path
that returns success or breaks early on E_ACCESSDENIED. The only
divergence is the all-attempts-failed path, which is the intent.

Also add phase timing to display_base_t::init, logged once per init as a
single info line: total, output_select (with pass and duplication-test
counts), device_create, and hdr_probe (with retry count). Logged on
success and on the three early-return failures.

The motivation is that a3bd879 attributed multi-second reinit to
unfair-lock starvation and added short-timeout polling to mitigate it,
but this function also contains several seconds of worst-case fixed
sleeps that compound inside the three-pass retry: Sleep(500) on the
second pass, up to 700ms of HDR metadata backoff, and the per-output
delays above. Which of the two actually dominates is not something I can
determine by reading, so measure it rather than continue guessing.

Two candidates were deliberately left alone pending that measurement:
test_dxgi_duplication creates a fresh D3D11 device per output where one
per adapter would do, and the HDR metadata backoff is not gated on the
display actually being in HDR mode, so an SDR display reporting zeroed
luminance would burn the full 700ms. The new hdr_probe/retries fields
will show whether the latter fires in practice.

Not compile-verified: Windows-only translation unit, changed on macOS.
Verified structurally (braces balanced in all three edited functions,
short-circuit evaluation order preserved in the refactored condition).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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 Plus

Run ID: 56f430ed-629f-428f-a139-eed074e9b600

📥 Commits

Reviewing files that changed from the base of the PR and between 253b8df and 328ee1b.

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

Summary by CodeRabbit

  • Bug 修复
    • 改进 Windows 桌面重复捕获初始化的重试策略:改为可配置的最大尝试次数与重试间隔;在最后一次失败时不再等待,减少初始化失败后的延迟。
    • 更新失败后重试与退避条件,并同步到对应的重复捕获测试流程。
  • 性能改进
    • 新增初始化阶段耗时统计与更细粒度的日志输出,便于定位输出选择、HDR 探测及设备创建等步骤耗时。

Walkthrough

本次修改调整 Windows DXGI 重复捕获的重试与退避行为,并在显示初始化流程中增加输出选择、设备创建、HDR 探测及失败状态的阶段耗时日志。

Changes

Windows 显示捕获初始化

Layer / File(s) Summary
DXGI 重试与退避控制
src/platform/windows/display_base.cpp
DuplicateOutput1DuplicateOutput 及重复捕获能力测试统一采用最大尝试次数,并仅在后续尝试存在时休眠。
显示初始化计时与输出判定
src/platform/windows/display_base.cpp
初始化流程记录输出选择次数、设备创建时间、HDR 探测重试次数及阶段耗时;RDP 输出直接接受,非 RDP 输出仅对附着桌面的输出执行重复捕获测试,并在失败或成功路径记录结果。

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 标题简洁且准确概括了移除尾随重试等待并为 Windows DDA 初始化加上性能埋点。
Description check ✅ Passed 描述与改动高度一致,解释了重试延迟优化、初始化计时埋点及测试说明。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/dda-init-latency

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/platform/windows/display_base.cpp (1)

579-587: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

补齐所有初始化早退路径的耗时日志。

计时 lambda 只覆盖了部分失败路径;CreateDXGIFactory1adapter->QueryInterface 和后续 device->QueryInterface(IID_IDXGIDevice) 失败时仍直接返回。这样会遗漏 PR 目标中“早退失败”的延迟数据。

建议补充日志调用
 if (FAILED(status)) {
   BOOST_LOG(error) << "Failed to create DXGIFactory1 ...";
+  log_init_timing("factory create failed");
   return -1;
 }

 if (FAILED(status)) {
   BOOST_LOG(error) << "Failed to query IDXGIAdapter interface";
+  log_init_timing("adapter query failed");
   return -1;
 }

 if (FAILED(status)) {
   BOOST_LOG(warning) << "Failed to query DXGI interface ...";
+  log_init_timing("dxgi device query failed");
   return -1;
 }
🤖 Prompt for 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.

In `@src/platform/windows/display_base.cpp` around lines 579 - 587, 扩展 Display
初始化流程中的 log_init_timing 使用范围,确保 CreateDXGIFactory1、adapter->QueryInterface 以及
device->QueryInterface(IID_IDXGIDevice) 失败并早退前都调用该 lambda 记录耗时;为每条失败路径传入明确的
outcome,并保持现有错误处理与返回行为不变。
🤖 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.

Nitpick comments:
In `@src/platform/windows/display_base.cpp`:
- Around line 579-587: 扩展 Display 初始化流程中的 log_init_timing 使用范围,确保
CreateDXGIFactory1、adapter->QueryInterface 以及
device->QueryInterface(IID_IDXGIDevice) 失败并早退前都调用该 lambda 记录耗时;为每条失败路径传入明确的
outcome,并保持现有错误处理与返回行为不变。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c1b32a3-82c0-4d16-8a00-6b58d9a62f4d

📥 Commits

Reviewing files that changed from the base of the PR and between 9f74ecf and 253b8df.

📒 Files selected for processing (1)
  • src/platform/windows/display_base.cpp
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{cpp,c,h}

⚙️ CodeRabbit configuration file

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

Files:

  • src/platform/windows/display_base.cpp
src/platform/**

⚙️ CodeRabbit configuration file

src/platform/**: 平台抽象层代码(Windows/Linux/macOS)。确保各平台实现一致, 注意 Windows API 调用的错误处理和资源释放。

Files:

  • src/platform/windows/display_base.cpp
🔇 Additional comments (3)
src/platform/windows/display_base.cpp (3)

52-55: LGTM!

Also applies to: 74-84, 105-115


505-531: LGTM!


632-632: LGTM!

Also applies to: 666-672, 706-710, 743-747, 875-875, 904-917

Three failure paths in display_base_t::init returned without calling
log_init_timing(), so a reinit that died at factory creation, adapter
QueryInterface, or IDXGIDevice QueryInterface produced no timing line at
all. Those are exactly the cases where knowing how long the attempt took
before failing is useful, since display_base_t::init is retried per
capture backend.

Add the call to all three with distinct outcome strings. Error handling,
log levels, and return values are unchanged; the stamps for phases that
were never reached are still initialized to init_start, so they report 0ms
rather than garbage.

All seven return paths in the function are now covered: factory create
failed, no output found, adapter query failed, device create failed, dxgi
device query failed, no timer, ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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