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) {