diff --git a/_dev_notes/DISPLAY_MANAGEMENT_ARCHITECTURE.md b/_dev_notes/DISPLAY_MANAGEMENT_ARCHITECTURE.md new file mode 100644 index 00000000000..32a508e2c0f --- /dev/null +++ b/_dev_notes/DISPLAY_MANAGEMENT_ARCHITECTURE.md @@ -0,0 +1,575 @@ +# 显示器管理架构设计文档 + +## 目录 + +- [现状分析](#现状分析) +- [当前架构](#当前架构) +- [设计缺陷](#设计缺陷) +- [重构目标架构](#重构目标架构) +- [模块设计](#模块设计) +- [会话生命周期](#会话生命周期) +- [实施计划](#实施计划) + +--- + +## 现状分析 + +### 当前数据流 + +```mermaid +graph LR + A[Moonlight Client] -->|HTTP /launch| B[nvhttp.cpp] + B -->|env vars| C[display_device::session_t] + C -->|pipe/DevManView| D[VDD Driver] + C -->|DXGI enum| E[display_base.cpp] + E -->|select output| F[DXGI Duplication] + F -->|frames| G[Encoder] +``` + +### 当前文件分布 + +| 文件 | 职责 | +|------|------| +| `src/nvhttp.cpp` | 解析客户端请求、构建 launch_session | +| `src/display_device/vdd_utils.cpp` | VDD 驱动控制(pipe IPC + DevManView) | +| `src/platform/windows/display_base.cpp` | DXGI 枚举、显示器选择、分辨率检测 | +| `src/platform/windows/display.h` | 显示器元数据(旋转/色彩空间/格式) | +| `src/config.cpp` | `config::video.output_name` 服务端配置 | + +--- + +## 当前架构 + +```mermaid +graph TB + subgraph CurrentArch["当前架构 (散乱)"] + direction TB + + subgraph HTTP["HTTP Layer"] + NV["nvhttp.cpp
• 解析 useVdd/customScreenMode/display_name
• 构建 env vars
• 无冲突检测"] + end + + subgraph Config["配置 (散布)"] + CF["config.cpp
video.output_name"] + LS["launch_session
CLIENT_DISPLAY_NAME
CLIENT_USE_VDD
CLIENT_CUSTOM_SCREEN_MODE"] + end + + subgraph VDD["VDD 控制 (脆弱)"] + VU["vdd_utils.cpp
• 硬编码驱动名
• 全局 static 状态
• 3s 固定超时
• 进程不等待"] + end + + subgraph Display["显示器选择 (不确定)"] + DB["display_base.cpp
• DXGI 枚举竞态
• 字符串匹配
• 静默 fallback
• 无客户端通知"] + end + + NV --> CF + NV --> LS + LS --> VU + LS --> DB + CF --> DB + end + + style CurrentArch fill:#ffebee,stroke:#c62828 +``` + +--- + +## 设计缺陷 + +### 缺陷列表 + +```mermaid +graph LR + subgraph Issues["7 个设计缺陷"] + P1["P1: 显示器选择不确定
⚠️ 高"] + P2["P2: VDD 集成脆弱
⚠️ 高"] + P3["P3: 配置优先级混乱
🔶 中"] + P4["P4: 线程安全隐患
🔶 中"] + P5["P5: 格式不匹配
🔵 低"] + P6["P6: HDR 色彩空间
🔵 低"] + P7["P7: 孤立显示状态
⚠️ 高"] + end + + style P1 fill:#ffcdd2 + style P2 fill:#ffcdd2 + style P3 fill:#fff9c4 + style P4 fill:#fff9c4 + style P5 fill:#bbdefb + style P6 fill:#bbdefb + style P7 fill:#ffcdd2 +``` + +### 缺陷详情 + +#### P1: 显示器选择不确定性 + +```mermaid +sequenceDiagram + participant C as Client + participant S as Sunshine + participant D as DXGI + + C->>S: /launch?display_name=HDMI-1 + S->>D: EnumAdapters1() + EnumOutputs() + + Note over D: 枚举顺序不稳定
可能因系统状态变化 + + D-->>S: [DISPLAY1, DISPLAY3, DISPLAY2] + + alt 匹配到 HDMI-1 + S->>S: 选择对应 output ✅ + else 未匹配 + S->>S: 静默选择第一个 ⚠️ + Note over S: 用户不知道
实际捕获的是哪个 + end + + S-->>C: 开始串流 (可能是错误的显示器) +``` + +#### P2: VDD 控制流程问题 + +```mermaid +sequenceDiagram + participant S as Sunshine + participant P as Named Pipe + participant DM as DevManView + participant V as VDD Driver + + S->>P: ConnectNamedPipe (3s timeout) + + alt Pipe 连接成功 + S->>P: WriteFile(command, 4KB max) + P->>V: 转发命令 + V-->>P: 响应 + P-->>S: ReadFile + else Pipe 超时 + S->>DM: CreateProcess("DevManView /enable ...") + Note over DM: ⚠️ child.detach()
进程不等待完成
可能还在运行时函数已返回 + DM->>V: 操作驱动 + end + + Note over S: 全局 static 状态
last_toggle_time
无锁保护 ⚠️ +``` + +#### P7: 崩溃后孤立状态 + +```mermaid +stateDiagram-v2 + [*] --> Normal: Sunshine 启动 + Normal --> VddConfigured: 客户端请求 VDD + VddConfigured --> SessionActive: 串流中 + + SessionActive --> Normal: 正常断开
restore_state() ✅ + SessionActive --> Orphaned: 进程崩溃 ⚠️ + + Orphaned --> [*]: VDD 保持错误模式
物理显示器可能被禁用
vdd_settings.xml 不一致 + + note right of Orphaned + 下次启动时 + 无法恢复到正确状态 + end note +``` + +--- + +## 重构目标架构 + +```mermaid +graph TB + subgraph Client["Moonlight Client"] + CR[Launch Request
useVdd / screenMode / displayName
width / height / fps] + end + + subgraph SessionLayer["Session Layer (绿)"] + NH["nvhttp.cpp
Launch Handler"] + SC["DisplaySessionConfig
• selection priority
• vdd_mode
• resolution/fps
• validate()"] + SL["SessionLifecycle
• start / stop / crash recovery
• state persistence"] + end + + subgraph DisplayManager["IDisplayManager (蓝)"] + direction TB + DM["DisplayManager
• enum_displays()
• select_display()
• configure()
• on_change listener"] + DI["DisplayInfo 列表
• device_name
• friendly_name
• adapter_index
• supports_dup
• is_vdd"] + DV["DisplayValidator
• pre-flight 检查
• 冲突检测
• 格式兼容性"] + end + + subgraph VddDriver["VddDriver (橙)"] + direction TB + VD["VddDriver
mutex 保护状态
• is_available()
• enable() / disable()
• set_mode()"] + VP["PipeClient
• 版本化协议
• 可配置超时
• 消息校验"] + VS["VddState
• current_mode
• crash_recovery_file
• last_known_good"] + end + + subgraph CapturePipeline["Capture Pipeline (紫)"] + direction TB + CS["CaptureSelector
DDx / WGC / AMD DC"] + DD["DXGI Duplication
• format negotiation
• rotation handling
• HDR color space"] + ENC["Encoder
NVENC / AMF / QSV / SW"] + end + + subgraph OS["Windows 显示子系统"] + DXGI["DXGI Factory
Adapters & Outputs"] + PHY["物理显示器"] + VDDDRV["ZakoVDD Driver
IddCx Kernel"] + NEFCON["nefconw.exe
驱动安装"] + end + + CR -->|HTTP /launch| NH + NH -->|构建配置| SC + SC -->|预检| DV + DV -->|通过| DM + DM -->|枚举| DXGI + DXGI -.->|outputs| DI + DI -->|选择| DM + + SC -->|VDD 请求?| VD + VD -->|pipe IPC| VP + VP -->|命令| VDDDRV + VD -->|持久化| VS + + DM -->|选中的显示器| CS + CS -->|创建 dup| DD + DD -->|帧数据| ENC + + SL -->|崩溃恢复| VS + SL -->|恢复状态| DM + + DXGI --- PHY + DXGI --- VDDDRV + NEFCON -.->|安装/卸载| VDDDRV + + style DisplayManager fill:#e1f5fe,stroke:#0288d1 + style VddDriver fill:#fff3e0,stroke:#f57c00 + style SessionLayer fill:#e8f5e9,stroke:#388e3c + style CapturePipeline fill:#f3e5f5,stroke:#7b1fa2 +``` + +--- + +## 模块设计 + +### DisplaySessionConfig — 配置合并 + +```mermaid +graph TB + subgraph Inputs["配置来源"] + I1["客户端请求
display_name, useVdd, screenMode"] + I2["服务端配置
config::video.output_name"] + I3["系统状态
DXGI 枚举结果"] + end + + subgraph Resolution["优先级解析"] + R1{"客户端指定了
display_name?"} + R2{"服务端配置了
output_name?"} + R3["使用第一个
可用显示器"] + end + + subgraph Validation["预检"] + V1["显示器存在?"] + V2["支持 Duplication?"] + V3["VDD 可用?"] + V4["分辨率/帧率
编码器支持?"] + V5["冲突检测"] + end + + I1 --> R1 + I2 --> R1 + R1 -->|是| V1 + R1 -->|否| R2 + R2 -->|是| V1 + R2 -->|否| R3 + R3 --> V1 + + V1 --> V2 --> V3 --> V4 --> V5 + + V5 -->|全部通过| OK["DisplaySessionConfig ✅"] + V5 -->|失败| ERR["Result::Error ❌
明确的错误类型"] +``` + +### VddDriver — 状态机 + +```mermaid +stateDiagram-v2 + [*] --> Unknown: 进程启动 + + Unknown --> Checking: check_availability() + Checking --> NotInstalled: 驱动未安装 + Checking --> Disabled: 驱动已禁用 + Checking --> Enabled: 驱动已启用 + + NotInstalled --> [*]: 不可用 + + Disabled --> Enabling: enable() + Enabling --> Enabled: pipe/nefcon 成功 + Enabling --> Error: 失败 + 重试耗尽 + + Enabled --> Configuring: set_mode(mode) + Configuring --> Enabled: 配置完成 + Configuring --> Error: 配置失败 + + Enabled --> Disabling: disable() + Disabling --> Disabled: 成功 + + Error --> Disabled: recover() + + note right of Enabled + 状态持久化到 + crash_recovery.json: + {mode, timestamp, session_id} + end note + + note right of Unknown + 启动时读取 + crash_recovery.json + 恢复到 last_known_good + end note +``` + +### DisplayManager — 变更检测 + +```mermaid +sequenceDiagram + participant DM as DisplayManager + participant DXGI as DXGI Factory + participant L as ChangeListener + participant S as SessionManager + + loop 每 2 秒 + DM->>DXGI: factory->IsCurrent() + alt 显示器布局变化 + DXGI-->>DM: false (not current) + DM->>DM: re-enumerate() + DM->>L: on_change(DisplayChangeEvent) + L->>S: handle_display_change() + + alt 当前捕获的显示器断开 + S->>S: 暂停串流 + S->>DM: select_display(fallback) + S->>S: 恢复串流 (新显示器) + else 新显示器接入 + S->>S: 通知客户端 (可选) + end + else 无变化 + DXGI-->>DM: true (current) + end + end +``` + +--- + +## 会话生命周期 + +### 正常流程 + +```mermaid +sequenceDiagram + participant C as Client + participant N as nvhttp + participant SC as SessionConfig + participant V as VddDriver + participant DM as DisplayManager + participant Cap as Capture + + C->>N: POST /launch + N->>SC: build_config(request) + SC->>SC: validate() + + alt 需要 VDD + SC->>V: enable() + set_mode() + V->>V: persist_state(crash_recovery.json) + V-->>SC: ok + end + + SC->>DM: select_display(config) + DM->>DM: enum_displays() + DM-->>SC: DisplayInfo + + SC->>Cap: start_capture(display_info) + Cap-->>C: streaming... + + Note over C,Cap: 串流中... + + C->>N: POST /cancel 或 断开 + N->>Cap: stop_capture() + + alt 使用了 VDD + N->>V: restore_state() + V->>V: delete crash_recovery.json + end + + N-->>C: ok +``` + +### 崩溃恢复流程 + +```mermaid +sequenceDiagram + participant OS as Windows + participant S as Sunshine (重启) + participant V as VddDriver + participant F as crash_recovery.json + + Note over OS: 上次进程崩溃
VDD 停留在 "VDD Only" 模式
物理显示器被禁用 + + OS->>S: Sunshine 启动 + S->>F: 读取 crash_recovery.json + + alt 文件存在 (异常退出) + F-->>S: {mode: "vdd_only", prev: "all_enabled"} + S->>V: restore_to(prev_state) + V->>V: enable physical displays + V->>V: disable VDD (或恢复 extend) + S->>F: 删除 crash_recovery.json + Note over S: 恢复完成 ✅ + else 文件不存在 (正常退出) + Note over S: 无需恢复 + end +``` + +--- + +## 实施计划 + +### 分阶段重构 + +```mermaid +gantt + title 显示器管理重构计划 + dateFormat YYYY-MM-DD + + section Phase 1: 基础设施 + DisplayInfo 结构体定义 :p1a, 2026-04-10, 2d + DisplaySessionConfig + validate :p1b, after p1a, 3d + Result 类型替代返回码 :p1c, after p1a, 2d + + section Phase 2: VDD 加固 + VddDriver 类封装 :p2a, after p1b, 3d + crash_recovery.json 持久化 :p2b, after p2a, 2d + 启动时自动恢复 :p2c, after p2b, 1d + pipe 协议版本化 :p2d, after p2a, 2d + + section Phase 3: DisplayManager + IDisplayManager 接口 :p3a, after p2c, 2d + DxgiDisplayManager 实现 :p3b, after p3a, 3d + 变更检测 + 监听器 :p3c, after p3b, 2d + + section Phase 4: 集成 + nvhttp 使用新接口 :p4a, after p3c, 2d + 配置优先级文档化 :p4b, after p4a, 1d + 端到端测试 :p4c, after p4a, 3d +``` + +### 优先级 + +| 优先级 | 改动 | 影响 | 风险 | +|--------|------|------|------| +| P0 | VDD 崩溃恢复 (P7) | 用户体验直接影响 | 低 | +| P1 | VDD 驱动封装 (P2) | 消除全局状态/进程泄漏 | 中 | +| P2 | 配置优先级统一 (P3) | 消除配置歧义 | 低 | +| P3 | 线程安全 (P4) | 并发会话 | 中 | +| P4 | DisplayManager 抽象 (P1) | 架构改善 | 高 (改动面大) | + +--- + +## restore_state_impl 完整场景覆盖 + +### 决策流程图 + +```mermaid +flowchart TD + START["restore_state_impl(reason)"] --> VDD_CHECK{"VDD 存在?
find_device_by_friendlyname(ZAKO_NAME)"} + + VDD_CHECK -->|不存在| KEEP_NO["vdd_destroyed = false"] + VDD_CHECK -->|存在| KEEP_CHECK{"keep_enabled?"} + + KEEP_CHECK -->|true| KEEP_YES["保留 VDD
vdd_destroyed = false"] + KEEP_CHECK -->|false| DESTROY["销毁 VDD
destroy_vdd_monitor()
sleep(1000ms)
vdd_destroyed = true"] + + KEEP_NO --> BRANCH + KEEP_YES --> BRANCH + DESTROY --> BRANCH + + BRANCH{"current_use_vdd
has_value?"} + + BRANCH -->|"nullopt (启动)" | STARTUP_PATH + BRANCH -->|"has_value (会话结束)"| SESSION_PATH + + subgraph STARTUP_PATH["启动恢复路径"] + S1{"vdd_destroyed ||
vdd_id.empty()?"} + S1 -->|true| S2{"CCD API 可用?"} + S1 -->|false| S3["跳过 revert_settings
(常驻模式VDD仍存活)"] + S2 -->|可用| S4["revert_settings(reason, true)
从 persistent_data 恢复拓扑"] + S2 -->|不可用| S3 + S4 --> S5 + S3 --> S5 + S5{"无头模式 &&
devices.empty()?"} + S5 -->|true| S6["create_vdd_monitor('')
创建基地显示器"] + S5 -->|false| S7["stop_timer_and_clear_vdd_state()
RETURN"] + S6 --> S7 + end + + subgraph SESSION_PATH["会话结束恢复路径"] + E1{"has_persistent?"} + E1 -->|false| E2["跳过拓扑恢复
(apply_config从未成功)
RETURN"] + E1 -->|true| E3{"无头模式 &&
devices.empty()?"} + E3 -->|true| E4["create_vdd_monitor('')"] + E3 -->|false| E5 + E4 --> E5 + E5{"is_no_operation?"} + E5 -->|true| E6["跳过拓扑恢复
RETURN"] + E5 -->|false| E7{"CCD API 可用?"} + E7 -->|可用| E8["revert_settings(reason, true)
恢复拓扑"] + E7 -->|不可用| E9["add_unlock_task()
延迟恢复"] + E8 --> E10["RETURN"] + E9 --> E10 + end + + style DESTROY fill:#ffcdd2 + style KEEP_YES fill:#c8e6c9 + style S4 fill:#bbdefb + style E8 fill:#bbdefb + style E9 fill:#fff9c4 +``` + +### 场景矩阵 + +```mermaid +graph LR + subgraph Scenarios["11 个场景完整覆盖"] + S1["#1 启动/无VDD
revert no-op → return"] + S2["#2 启动/VDD/!keep
销毁 → revert → return"] + S3["#3 启动/VDD/keep
保留 → skip revert → return"] + S4["#4 结束/无VDD/persistent
→ revert"] + S5["#5 结束/VDD/!keep/persistent
销毁 → revert"] + S6["#6 结束/VDD/keep/persistent
保留 → revert"] + S7["#7 结束/VDD/!keep/!persistent
销毁 → 跳过拓扑"] + S8["#8 结束/无VDD/!persistent
→ 跳过拓扑"] + S9["#9 结束/no_operation
→ 跳过拓扑"] + S10["#10 结束/CCD锁定
→ 延迟重试"] + S11["#11 启动/VDD/CCD锁定
销毁 → skip revert"] + end + + style S2 fill:#e3f2fd + style S5 fill:#e3f2fd + style S10 fill:#fff9c4 + style S11 fill:#fff9c4 +``` + +### 配置优先级 + +```mermaid +flowchart LR + subgraph Priority["配置优先级 (高→低)"] + direction TB + P1["显示器ID"] --> P1A["CLIENT_DISPLAY_NAME (客户端)"] + P1 --> P1B["config.output_name (服务端)"] + P2["屏幕模式"] --> P2A["custom_screen_mode (客户端)"] + P2 --> P2B["config.display_device_prep (服务端)"] + P3["VDD决策"] --> P3A["session.use_vdd (客户端请求)"] + P3 --> P3B["设备不可用 (自动检测)"] + P3 --> P3C["VDD设备检测"] + end + + P1A -.->|覆盖| P1B + P2A -.->|覆盖| P2B +``` diff --git a/src/display_device/parsed_config.cpp b/src/display_device/parsed_config.cpp index ab42f11c7a6..bc415cd427e 100644 --- a/src/display_device/parsed_config.cpp +++ b/src/display_device/parsed_config.cpp @@ -601,6 +601,10 @@ namespace display_device { boost::optional make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session, bool is_reconfigure) { + // 配置优先级(高→低): + // 显示器ID: SUNSHINE_CLIENT_DISPLAY_NAME (客户端) > config.output_name (服务端) + // 屏幕模式: custom_screen_mode (客户端) > config.display_device_prep (服务端) + // VDD决策: session.use_vdd (客户端) > 设备不可用 > VDD设备检测 parsed_config_t parsed_config; // 优先使用客户端指定的显示器名称,如果没有则使用全局配置 @@ -684,7 +688,10 @@ namespace display_device { } // 准备VDD设备 - display_device::session_t::get().prepare_vdd(parsed_config, session); + if (!display_device::session_t::get().prepare_vdd(parsed_config, session)) { + BOOST_LOG(error) << "VDD准备失败,无法继续配置显示设备"; + return boost::none; + } return parsed_config; } diff --git a/src/display_device/session.cpp b/src/display_device/session.cpp index af66232a7ff..aa164afcc56 100644 --- a/src/display_device/session.cpp +++ b/src/display_device/session.cpp @@ -441,7 +441,7 @@ namespace display_device { std::this_thread::sleep_for(1200ms); } - void + bool session_t::prepare_vdd(parsed_config_t &config, const rtsp_stream::launch_session_t &session) { const std::string current_client_id = get_client_id_from_session(session); const vdd_utils::hdr_brightness_t hdr_brightness { session.max_nits, session.min_nits, session.max_full_nits }; @@ -522,12 +522,13 @@ namespace display_device { if (!try_recover_vdd_device(current_client_id, session.client_name, hdr_brightness, device_zako)) { BOOST_LOG(error) << "VDD设备最终初始化失败"; vdd_utils::disable_enable_vdd(); - return; + return false; } } if (device_zako.empty()) { - return; + BOOST_LOG(error) << "VDD设备ID为空,准备失败"; + return false; } if (original_output_name.empty()) { @@ -552,6 +553,11 @@ namespace display_device { // Apply VDD prep settings to handle display topology // This determines how VDD interacts with physical displays // VDD模式下的拓扑控制与普通模式分开处理 + + // Save topology checkpoint BEFORE any topology modification, + // so crash recovery can restore the original state on next startup. + settings.save_topology_checkpoint(); + if (config.vdd_prep != parsed_config_t::vdd_prep_e::no_operation) { // User has specified a display configuration, apply it if (vdd_utils::apply_vdd_prep(device_zako, config.vdd_prep, pre_vdd_devices)) { @@ -573,6 +579,8 @@ namespace display_device { std::this_thread::sleep_for(500ms); vdd_utils::set_hdr_state(false); } + + return true; } void @@ -593,42 +601,45 @@ namespace display_device { void session_t::restore_state_impl(revert_reason_e reason) { - // 统一的VDD清理逻辑(在恢复拓扑之前执行,不需要CCD API,锁屏时也可以执行) + // === VDD 生命周期决策(统一入口) === const auto vdd_id = display_device::find_device_by_friendlyname(ZAKO_NAME); - - // 常驻模式:只影响 VDD 是否销毁,不影响拓扑恢复 const bool is_keep_enabled = config::video.vdd_keep_enabled; + bool vdd_destroyed = false; - // 如果没有会话配置过(current_use_vdd 为 nullopt),说明: - // 1. 程序刚启动进行崩溃恢复(init() 调用) - // 2. 或者上一次会话已经正常结束且清理了状态 - // 此时不需要恢复拓扑(没有拓扑被修改过),只需要清理可能残留的 VDD - if (!current_use_vdd.has_value()) { - BOOST_LOG(debug) << " 无会话配置(current_use_vdd=nullopt),仅执行 VDD 清理"; - - if (!vdd_id.empty() && !is_keep_enabled) { - if (settings.has_persistent_data()) { - BOOST_LOG(info) << "非常驻模式,销毁残留 VDD"; + if (!vdd_id.empty()) { + if (is_keep_enabled) { + BOOST_LOG(debug) << "常驻模式,保留 VDD"; + } + else { + // 无头主机保护:如果销毁后会变成无头(VDD 是唯一显示设备),跳过销毁 + // 这避免了无意义的销毁+重建循环(device ID 变化导致 persistent_data 失效) + bool skip_destroy = false; + if (config::video.vdd_headless_create_enabled) { + auto devices = display_device::enum_available_devices(); + bool only_vdd = (devices.size() == 1 && devices.count(vdd_id)); + if (only_vdd || devices.empty()) { + BOOST_LOG(info) << "无头主机检测:VDD 是唯一显示设备,跳过销毁"; + skip_destroy = true; + } } - else { - BOOST_LOG(info) << "检测到异常残留的 VDD(无 persistent_data),清理 VDD"; + + if (!skip_destroy) { + BOOST_LOG(info) << "非常驻模式,销毁 VDD"; + destroy_vdd_monitor(); + std::this_thread::sleep_for(1000ms); + vdd_destroyed = true; } - destroy_vdd_monitor(); - std::this_thread::sleep_for(1000ms); } + } - // 无头主机自动创建检查 - if (reason == revert_reason_e::stream_ended && config::video.vdd_headless_create_enabled) { - auto devices = display_device::enum_available_devices(); - if (devices.empty()) { - BOOST_LOG(info) << "无头主机检测:未找到显示设备,自动创建基地显示器"; - create_vdd_monitor(""); - constexpr int max_attempts = 5; - constexpr auto wait_time = std::chrono::milliseconds(233); - for (int i = 0; i < max_attempts && !is_display_on(); ++i) { - std::this_thread::sleep_for(wait_time); - } - } + // === 拓扑恢复 === + if (!current_use_vdd.has_value()) { + // 启动恢复路径:程序刚启动或上次会话已正常清理 + // 通过 persistent_data(original_display_settings.json)恢复上次 apply_config 保存的拓扑 + BOOST_LOG(debug) << "无会话配置(current_use_vdd=nullopt),执行启动恢复"; + + if ((vdd_destroyed || vdd_id.empty()) && !settings.is_changing_settings_going_to_fail()) { + settings.revert_settings(reason, true /* VDD 已处理 */); } stop_timer_and_clear_vdd_state(); @@ -666,42 +677,7 @@ namespace display_device { // 检查 apply_config 是否曾成功执行(persistent_data 是否存在) const bool has_persistent = settings.has_persistent_data(); - // 立即执行完整 restore - // VDD 销毁逻辑 - if (!vdd_id.empty()) { - bool should_destroy = false; - - // 判断1:常驻模式 - 保留VDD - if (is_keep_enabled) { - BOOST_LOG(debug) << "常驻模式,保留VDD"; - } - // 判断2:非常驻模式 - 销毁VDD(无论是否是无操作模式) - else if (has_persistent) { - BOOST_LOG(info) << "非常驻模式,销毁VDD"; - should_destroy = true; - } - // 判断3:无persistent_data - apply_config 从未执行成功(如锁屏中退出串流) - else { - BOOST_LOG(info) << "apply_config 未执行(无persistent_data),销毁VDD并跳过拓扑恢复"; - should_destroy = true; - } - - // 无头主机保护:如果销毁后会变成无头(VDD 是唯一显示设备),跳过销毁 - // 这避免了无意义的销毁+重建循环(device ID 变化导致 persistent_data 失效) - if (should_destroy && config::video.vdd_headless_create_enabled) { - auto devices = display_device::enum_available_devices(); - bool only_vdd = (devices.size() == 1 && devices.count(vdd_id)); - if (only_vdd || devices.empty()) { - BOOST_LOG(info) << "无头主机检测:VDD 是唯一显示设备,跳过销毁"; - should_destroy = false; - } - } - - if (should_destroy) { - destroy_vdd_monitor(); - std::this_thread::sleep_for(1000ms); - } - } + // VDD 已在函数顶部统一处理,此处无需重复 // 如果 apply_config 从未执行成功,拓扑从未被修改过,不需要恢复 if (!has_persistent) { diff --git a/src/display_device/session.h b/src/display_device/session.h index f7befe39b48..e94d7753f1d 100644 --- a/src/display_device/session.h +++ b/src/display_device/session.h @@ -153,24 +153,6 @@ namespace display_device { bool destroy_vdd_monitor(); - /** - * @brief Enable VDD driver - */ - void - enable_vdd(); - - /** - * @brief Disable VDD driver - */ - void - disable_vdd(); - - /** - * @brief Disable and enable VDD driver - */ - void - disable_enable_vdd(); - /** * @brief Toggle display power */ @@ -184,9 +166,10 @@ namespace display_device { is_display_on(); /** - * @brief Prepares VDD for use + * @brief Prepares VDD for use. + * @returns True if VDD is ready (created or already present), false if preparation failed. */ - void + bool prepare_vdd(parsed_config_t &config, const rtsp_stream::launch_session_t &session); /** diff --git a/src/display_device/settings.h b/src/display_device/settings.h index e51fa509007..c71a4633d5a 100644 --- a/src/display_device/settings.h +++ b/src/display_device/settings.h @@ -235,6 +235,21 @@ namespace display_device { void replace_vdd_id(const std::string& old_id, const std::string& new_id); + /** + * @brief Save current topology as checkpoint to prevent data loss on crash. + * + * Called BEFORE topology-modifying operations (e.g., apply_vdd_prep) to ensure + * that if Sunshine crashes mid-operation, the original topology can be restored + * on next startup. + * + * If persistent_data already exists (from a previous apply_config), this is a no-op + * since the recovery data is already saved. + * + * @returns True if checkpoint was saved or already exists. + */ + bool + save_topology_checkpoint(); + private: std::unique_ptr persistent_data; /**< Platform specific persistent data. */ std::unique_ptr audio_data; /**< Platform specific temporary audio data. */ diff --git a/src/display_device/vdd_utils.cpp b/src/display_device/vdd_utils.cpp index 0b0f79b3de8..e9917bb33e2 100644 --- a/src/display_device/vdd_utils.cpp +++ b/src/display_device/vdd_utils.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,8 @@ namespace display_device { const DWORD kPipeBufferSize = 4096; const std::chrono::milliseconds kDefaultDebounceInterval { 2000 }; + // 全局状态保护锁 + static std::mutex vdd_state_mutex; // 上次切换显示器的时间点 static std::chrono::steady_clock::time_point last_toggle_time { std::chrono::steady_clock::now() }; // 防抖间隔 @@ -93,7 +96,17 @@ namespace display_device { auto child = platf::run_command(true, true, cmd, working_dir, _env, nullptr, ec, nullptr); if (!ec) { BOOST_LOG(info) << "成功执行VDD " << action_str << " 命令"; - child.detach(); + // 等待进程完成,避免资源泄漏(带超时保护) + std::error_code wait_ec; + if (!child.wait_for(std::chrono::seconds(30), wait_ec)) { + BOOST_LOG(error) << "VDD命令进程超时(30s),强制终止"; + child.terminate(wait_ec); + child.wait(wait_ec); + return false; + } + if (wait_ec) { + BOOST_LOG(warning) << "等待VDD命令进程完成时出错: " << wait_ec.message(); + } return true; } @@ -163,7 +176,7 @@ namespace display_device { HandleGuard event_guard { overlapped.hEvent }; // 发送命令(使用宽字符版本) - DWORD bytesWritten; + DWORD bytesWritten = 0; size_t cmd_len = (wcslen(command) + 1) * sizeof(wchar_t); // 包含终止符 if (!WriteFile(hPipe, command, (DWORD) cmd_len, &bytesWritten, &overlapped)) { if (GetLastError() != ERROR_IO_PENDING) { @@ -175,13 +188,27 @@ namespace display_device { DWORD waitResult = WaitForSingleObject(overlapped.hEvent, kPipeTimeoutMs); if (waitResult != WAIT_OBJECT_0) { BOOST_LOG(error) << L"发送" << command << L"命令超时"; + CancelIo(hPipe); return false; } + + if (!GetOverlappedResult(hPipe, &overlapped, &bytesWritten, FALSE)) { + BOOST_LOG(error) << "获取写入结果失败,错误代码: " << GetLastError(); + return false; + } + } + + if (bytesWritten != (DWORD) cmd_len) { + BOOST_LOG(error) << "写入字节数不完整: " << bytesWritten << "/" << cmd_len; + return false; } // 读取响应 bool read_timed_out = false; if (response) { + // 重置 event 用于读操作 + ResetEvent(overlapped.hEvent); + char buffer[kPipeBufferSize]; DWORD bytesRead = 0; if (!ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, &overlapped)) { @@ -278,7 +305,11 @@ namespace display_device { std::wstring command = L"CREATEMONITOR"; // 如果没有提供UUID,使用上一次的UUID - std::string identifier_to_use = client_identifier.empty() && !last_used_client_uuid.empty() ? last_used_client_uuid : client_identifier; + std::string identifier_to_use; + { + std::lock_guard lock(vdd_state_mutex); + identifier_to_use = client_identifier.empty() && !last_used_client_uuid.empty() ? last_used_client_uuid : client_identifier; + } if (identifier_to_use != client_identifier && !identifier_to_use.empty()) { BOOST_LOG(info) << "未提供客户端标识符,使用上一次的UUID: " << identifier_to_use; @@ -318,6 +349,7 @@ namespace display_device { // 如果使用了有效的UUID,更新上一次使用的UUID if (!identifier_to_use.empty()) { + std::lock_guard lock(vdd_state_mutex); last_used_client_uuid = identifier_to_use; } @@ -372,6 +404,11 @@ namespace display_device { void destroy_vdd_monitor_nolog() { + // 先检查管道是否可用(超时1秒),避免在析构时无限等待 + if (!WaitNamedPipeW(kVddPipeName, 1000)) { + return; + } + HANDLE hPipe = CreateFileW( kVddPipeName, GENERIC_READ | GENERIC_WRITE, @@ -386,16 +423,6 @@ namespace display_device { } } - void - enable_vdd() { - execute_vdd_command(vdd_action_e::enable); - } - - void - disable_vdd() { - execute_vdd_command(vdd_action_e::disable); - } - void disable_enable_vdd() { execute_vdd_command(vdd_action_e::disable_enable); @@ -410,16 +437,19 @@ namespace display_device { toggle_display_power() { auto now = std::chrono::steady_clock::now(); - if (now - last_toggle_time < debounce_interval) { - BOOST_LOG(debug) << "忽略快速重复的显示器开关请求,请等待" - << std::chrono::duration_cast( - debounce_interval - (now - last_toggle_time)) - .count() - << "秒"; - return false; - } + { + std::lock_guard lock(vdd_state_mutex); + if (now - last_toggle_time < debounce_interval) { + BOOST_LOG(debug) << "忽略快速重复的显示器开关请求,请等待" + << std::chrono::duration_cast( + debounce_interval - (now - last_toggle_time)) + .count() + << "秒"; + return false; + } - last_toggle_time = now; + last_toggle_time = now; + } if (is_display_on()) { destroy_vdd_monitor(); @@ -465,12 +495,21 @@ namespace display_device { } // 后台线程确保VDD处于扩展模式,并进行二次确认 - std::thread([vdd_device_id = find_device_by_friendlyname(ZAKO_NAME), physical_devices_before]() mutable { + // 使用 static jthread 保证: + // 1. 新调用前旧线程会被停止(jthread 析构时通过 stop_token 请求停止) + // 2. Sunshine 关闭时线程能正确终止 + static std::jthread toggle_thread; + toggle_thread = std::jthread([vdd_device_id = find_device_by_friendlyname(ZAKO_NAME), physical_devices_before](std::stop_token stoken) mutable { if (vdd_device_id.empty()) { - std::this_thread::sleep_for(std::chrono::seconds(2)); + for (int i = 0; i < 10 && !stoken.stop_requested(); ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + } + if (stoken.stop_requested()) return; vdd_device_id = find_device_by_friendlyname(ZAKO_NAME); } + if (stoken.stop_requested()) return; + if (vdd_device_id.empty()) { BOOST_LOG(warning) << "无法找到基地显示器设备,跳过配置"; } @@ -482,24 +521,42 @@ namespace display_device { } } + if (stoken.stop_requested()) return; + // 创建后二次确认,20秒超时 - constexpr auto timeout = std::chrono::seconds(20); + // 注意:dialog strings 作为值拷贝(不捕获引用),避免生命周期问题 std::wstring dialog_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_KEEP_TITLE)); - std::wstring confirm_message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_KEEP_MSG)); + std::wstring confirm_msg = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_KEEP_MSG)); - auto future = std::async(std::launch::async, [&]() { - return MessageBoxW(nullptr, confirm_message.c_str(), dialog_title.c_str(), MB_YESNO | MB_ICONQUESTION) == IDYES; + // 使用副本传入 async,避免引用捕获 + auto future = std::async(std::launch::async, [title = dialog_title, msg = confirm_msg]() { + return MessageBoxW(nullptr, msg.c_str(), title.c_str(), MB_YESNO | MB_ICONQUESTION) == IDYES; }); - if (future.wait_for(timeout) == std::future_status::ready && future.get()) { + constexpr auto timeout = std::chrono::seconds(20); + // 轮询等待,响应 stop_token + auto deadline = std::chrono::steady_clock::now() + timeout; + bool got_response = false; + bool user_confirmed = false; + while (std::chrono::steady_clock::now() < deadline && !stoken.stop_requested()) { + if (future.wait_for(std::chrono::milliseconds(200)) == std::future_status::ready) { + got_response = true; + user_confirmed = future.get(); + break; + } + } + + if (stoken.stop_requested()) return; + + if (got_response && user_confirmed) { BOOST_LOG(info) << "用户确认保留基地显示器"; return; } BOOST_LOG(info) << "用户未确认或超时,自动销毁基地显示器"; - std::wstring w_dialog_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_KEEP_TITLE)); - if (HWND hwnd = FindWindowW(L"#32770", w_dialog_title.c_str()); hwnd && IsWindow(hwnd)) { + // 关闭可能还在显示的对话框 + if (HWND hwnd = FindWindowW(L"#32770", dialog_title.c_str()); hwnd && IsWindow(hwnd)) { PostMessage(hwnd, WM_COMMAND, MAKEWPARAM(IDNO, BN_CLICKED), 0); PostMessage(hwnd, WM_CLOSE, 0, 0); @@ -513,8 +570,10 @@ namespace display_device { } } - destroy_vdd_monitor(); - }).detach(); + if (!stoken.stop_requested()) { + destroy_vdd_monitor(); + } + }); return true; } diff --git a/src/display_device/vdd_utils.h b/src/display_device/vdd_utils.h index a91939cf3d2..d0ace2890d8 100644 --- a/src/display_device/vdd_utils.h +++ b/src/display_device/vdd_utils.h @@ -58,10 +58,6 @@ namespace display_device::vdd_utils { std::chrono::milliseconds calculate_exponential_backoff(int attempt); - // VDD命令执行 - bool - execute_vdd_command(const std::string &action); - // 管道相关函数 HANDLE connect_to_pipe_with_retry(const wchar_t *pipe_name, int max_retries = 3); @@ -109,12 +105,6 @@ namespace display_device::vdd_utils { void destroy_vdd_monitor_nolog(); - void - enable_vdd(); - - void - disable_vdd(); - void disable_enable_vdd(); diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 476d01e5bcf..6034a08cd48 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -567,6 +567,7 @@ namespace platf::dxgi { adapter_t::pointer adapter_p; for (int tries = 0; tries < 2 && !output; ++tries) { if (tries == 1) { + BOOST_LOG(warning) << "[Display Init] 首次未找到匹配的显示器,唤醒显示器后重试..."; SetThreadExecutionState(ES_DISPLAY_REQUIRED); Sleep(500); } @@ -629,6 +630,28 @@ namespace platf::dxgi { if (!output) { BOOST_LOG(error) << "Failed to locate an output device"sv; + + // 诊断:记录配置的显示器名和所有可用显示器 + if (!output_name.empty()) { + BOOST_LOG(error) << " 配置的显示器: " << to_utf8(output_name); + } + BOOST_LOG(error) << " 可用显示器列表:"; + adapter_t::pointer diag_adapter_p; + for (int x = 0; factory->EnumAdapters1(x, &diag_adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { + dxgi::adapter_t diag_adapter { diag_adapter_p }; + DXGI_ADAPTER_DESC1 diag_desc; + diag_adapter->GetDesc1(&diag_desc); + dxgi::output_t::pointer diag_output_p; + for (int y = 0; diag_adapter->EnumOutputs(y, &diag_output_p) != DXGI_ERROR_NOT_FOUND; ++y) { + dxgi::output_t diag_output { diag_output_p }; + DXGI_OUTPUT_DESC diag_out_desc; + diag_output->GetDesc(&diag_out_desc); + BOOST_LOG(error) << " - " << to_utf8(diag_out_desc.DeviceName) + << " (adapter: " << to_utf8(diag_desc.Description) << ")" + << (diag_out_desc.AttachedToDesktop ? " [attached]" : " [detached]"); + } + } + return -1; } diff --git a/src/platform/windows/display_device/settings.cpp b/src/platform/windows/display_device/settings.cpp index b3bbe30bbd6..5056ceb697c 100644 --- a/src/platform/windows/display_device/settings.cpp +++ b/src/platform/windows/display_device/settings.cpp @@ -689,13 +689,32 @@ namespace display_device { } try { - std::ofstream file(filepath, std::ios::out | std::ios::trunc); - nlohmann::json json_data = data; + // Write to a temporary file first, then atomically rename to the target. + // This prevents data loss if Sunshine crashes mid-write. + auto tmp_filepath = filepath; + tmp_filepath += ".tmp"; + + { + std::ofstream file(tmp_filepath, std::ios::out | std::ios::trunc); + if (!file.is_open()) { + BOOST_LOG(error) << "Failed to open temp file for saving: " << tmp_filepath; + return false; + } - // Write json with indentation - file << std::setw(4) << json_data << std::endl; - BOOST_LOG(debug) << "Saved persistent display settings:\n" - << json_data.dump(4); + nlohmann::json json_data = data; + file << std::setw(4) << json_data << std::endl; + file.flush(); + + if (file.fail()) { + BOOST_LOG(error) << "Failed to write display settings to temp file"; + return false; + } + } + + // Atomic rename (on NTFS, rename is atomic for same-volume operations) + std::filesystem::rename(tmp_filepath, filepath); + + BOOST_LOG(debug) << "Saved persistent display settings to: " << filepath.string(); return true; } catch (const std::exception &err) { @@ -718,6 +737,15 @@ namespace display_device { std::unique_ptr load_settings(const std::filesystem::path &filepath) { try { + // Clean up stale .tmp file from a previous interrupted save + auto tmp_filepath = filepath; + tmp_filepath += ".tmp"; + std::error_code ec; + if (std::filesystem::exists(tmp_filepath, ec)) { + BOOST_LOG(warning) << "发现上次保存残留的临时文件,清理: " << tmp_filepath; + std::filesystem::remove(tmp_filepath, ec); + } + if (!filepath.empty() && std::filesystem::exists(filepath)) { std::ifstream file(filepath); return std::make_unique(nlohmann::json::parse(file)); @@ -1081,6 +1109,33 @@ namespace display_device { save_settings(filepath, *persistent_data); } + bool + settings_t::save_topology_checkpoint() { + if (persistent_data) { + // Recovery data already exists, nothing to do + return true; + } + + auto current_topology = get_current_topology(); + if (current_topology.empty()) { + BOOST_LOG(warning) << "save_topology_checkpoint: 无法获取当前拓扑"; + return false; + } + + persistent_data = std::make_unique(); + persistent_data->topology.initial = current_topology; + persistent_data->topology.modified = current_topology; + + if (!save_settings(filepath, *persistent_data)) { + BOOST_LOG(error) << "save_topology_checkpoint: 保存拓扑检查点失败"; + persistent_data = nullptr; + return false; + } + + BOOST_LOG(info) << "已保存拓扑检查点,用于崩溃恢复"; + return true; + } + void settings_t::replace_vdd_id(const std::string& old_id, const std::string& new_id) { if (!persistent_data) {