diff --git a/src/config.cpp b/src/config.cpp index 4c54a736c40..028a2be62fc 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -395,6 +395,7 @@ namespace config { true, // nv_sunshine_high_power_mode false, // vdd_keep_enabled false, // vdd_headless_create_enabled + false, // vdd_reuse (default: recreate VDD for each client) {}, // nv_legacy { @@ -437,6 +438,7 @@ namespace config { {}, // capture_target (default: empty, will be set to "display" in apply_config) {}, // window_title (int) display_device::parsed_config_t::device_prep_e::no_operation, // display_device_prep + (int) display_device::parsed_config_t::vdd_prep_e::no_operation, // vdd_prep (int) display_device::parsed_config_t::resolution_change_e::automatic, // resolution_change {}, // manual_resolution (int) display_device::parsed_config_t::refresh_rate_change_e::automatic, // refresh_rate_change @@ -1191,6 +1193,7 @@ namespace config { } #endif int_f(vars, "display_device_prep", video.display_device_prep, display_device::parsed_config_t::device_prep_from_view); + int_f(vars, "vdd_prep", video.vdd_prep, display_device::parsed_config_t::vdd_prep_from_view); int_f(vars, "resolution_change", video.resolution_change, display_device::parsed_config_t::resolution_change_from_view); string_f(vars, "manual_resolution", video.manual_resolution); list_display_mode_remapping_f(vars, "display_mode_remapping", video.display_mode_remapping); @@ -1202,6 +1205,7 @@ namespace config { int_between_f(vars, "minimum_fps_target", video.minimum_fps_target, { 0, 1000 }); bool_f(vars, "vdd_keep_enabled", video.vdd_keep_enabled); bool_f(vars, "vdd_headless_create", video.vdd_headless_create_enabled); + bool_f(vars, "vdd_reuse", video.vdd_reuse); // Downscaling quality: "fast" (bilinear+8pt average), "balanced" (bicubic), "high_quality" (future: lanczos) string_f(vars, "downscaling_quality", video.downscaling_quality); diff --git a/src/config.h b/src/config.h index 6c65a3486b3..312de1d0e8c 100644 --- a/src/config.h +++ b/src/config.h @@ -41,6 +41,8 @@ namespace config { bool vdd_keep_enabled; /** When true, after stream end if no display is found (headless), create Zako VDD automatically. Default false. */ bool vdd_headless_create_enabled; + /** When true, reuse existing VDD on client switch instead of destroying and recreating. Default true. */ + bool vdd_reuse; struct { int preset; @@ -99,6 +101,7 @@ namespace config { std::string capture_target; // "display" or "window" - determines whether to capture display or window std::string window_title; // Window title to capture when capture_target="window" int display_device_prep; + int vdd_prep; // How to handle physical displays when using VDD (Virtual Display Device) int resolution_change; std::string manual_resolution; int refresh_rate_change; diff --git a/src/display_device/parsed_config.cpp b/src/display_device/parsed_config.cpp index 3bdf73ffdbb..0e5625cfe17 100644 --- a/src/display_device/parsed_config.cpp +++ b/src/display_device/parsed_config.cpp @@ -537,6 +537,19 @@ namespace display_device { return static_cast(parsed_config_t::hdr_prep_e::no_operation); } + int + parsed_config_t::vdd_prep_from_view(std::string_view value) { + using namespace std::string_view_literals; +#define _CONVERT_(x) \ + if (value == #x##sv) return static_cast(parsed_config_t::vdd_prep_e::x); + _CONVERT_(no_operation); + _CONVERT_(vdd_as_primary); + _CONVERT_(vdd_as_secondary); + _CONVERT_(display_off); +#undef _CONVERT_ + return static_cast(parsed_config_t::vdd_prep_e::no_operation); + } + boost::optional make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session, bool is_reconfigure) { parsed_config_t parsed_config; @@ -553,6 +566,7 @@ namespace display_device { parsed_config.device_id = device_id_to_use; parsed_config.device_prep = static_cast(config.display_device_prep); + parsed_config.vdd_prep = static_cast(config.vdd_prep); parsed_config.change_hdr_state = parse_hdr_option(config, session); const int custom_screen_mode = session.custom_screen_mode; @@ -574,6 +588,24 @@ namespace display_device { } } + // 客户端自定义VDD屏幕模式 + const int custom_vdd_screen_mode = session.custom_vdd_screen_mode; + if (custom_vdd_screen_mode != -1) { + BOOST_LOG(debug) << "客户端自定义VDD屏幕模式: "sv << custom_vdd_screen_mode; + if (custom_vdd_screen_mode == static_cast(parsed_config_t::vdd_prep_e::no_operation)) { + parsed_config.vdd_prep = parsed_config_t::vdd_prep_e::no_operation; + } + else if (custom_vdd_screen_mode == static_cast(parsed_config_t::vdd_prep_e::vdd_as_primary)) { + parsed_config.vdd_prep = parsed_config_t::vdd_prep_e::vdd_as_primary; + } + else if (custom_vdd_screen_mode == static_cast(parsed_config_t::vdd_prep_e::vdd_as_secondary)) { + parsed_config.vdd_prep = parsed_config_t::vdd_prep_e::vdd_as_secondary; + } + else if (custom_vdd_screen_mode == static_cast(parsed_config_t::vdd_prep_e::display_off)) { + parsed_config.vdd_prep = parsed_config_t::vdd_prep_e::display_off; + } + } + // 解析分辨率和刷新率配置 if (!parse_resolution_option(config, session, parsed_config) || !parse_refresh_rate_option(config, session, parsed_config) || @@ -599,9 +631,16 @@ namespace display_device { // 不需要VDD时直接返回 if (!needs_vdd) { BOOST_LOG(debug) << "输出设备已存在,跳过VDD准备"sv; + parsed_config.use_vdd = false; return parsed_config; } + // 标记为VDD模式 + // VDD模式下,vdd_prep 控制屏幕布局,apply_config 会根据 use_vdd 分别处理 + // device_prep 保留原值但不会被使用(由 handle_topology_for_vdd_mode 处理) + parsed_config.use_vdd = true; + BOOST_LOG(debug) << "VDD模式:使用 vdd_prep 控制屏幕布局"sv; + // 不是SYSTEM权限且处于RDP中,强制使用RDP虚拟显示器,不创建VDD if (!is_running_as_system_user && display_device::w_utils::is_any_rdp_session_active()) { BOOST_LOG(info) << "[Display] RDP环境:强制使用RDP虚拟显示器,跳过VDD准备"sv; diff --git a/src/display_device/parsed_config.h b/src/display_device/parsed_config.h index d22e5b93170..866be4e8a4b 100644 --- a/src/display_device/parsed_config.h +++ b/src/display_device/parsed_config.h @@ -114,8 +114,36 @@ namespace display_device { static int hdr_prep_from_view(std::string_view value); + /** + * @brief Enum detailing how to prepare physical displays when using VDD (Virtual Display Device). + * @note In VDD mode, topology changes are handled by Windows automatically when displays are added/removed, + * so we don't save/restore topology state - just modify it as requested. + */ + enum class vdd_prep_e : int { + no_operation, /**< Do nothing to physical displays. */ + vdd_as_primary, /**< VDD as primary display, physical displays as secondary (extend mode). */ + vdd_as_secondary, /**< Physical displays as primary, VDD as secondary (extend mode). */ + display_off /**< Turn off physical displays, only VDD remains active. */ + }; + + /** + * @brief Convert the string to the matching value of vdd_prep_e. + * @param value String value to map to vdd_prep_e. + * @returns A vdd_prep_e value (converted to int) that matches the string + * or the default value if string does not match anything. + * @see vdd_prep_e + * + * EXAMPLES: + * ```cpp + * const int vdd_prep = vdd_prep_from_view("display_off"); + * ``` + */ + static int + vdd_prep_from_view(std::string_view value); + std::string device_id; /**< Device id manually provided by the user via config. */ device_prep_e device_prep; /**< The device_prep_e value taken from config. */ + vdd_prep_e vdd_prep; /**< The vdd_prep_e value taken from config (only used in VDD mode). */ boost::optional resolution; /**< Parsed resolution value we need to switch to. Empty optional if no action is required. */ boost::optional refresh_rate; /**< Parsed refresh rate value we need to switch to. Empty optional if no action is required. */ boost::optional change_hdr_state; /**< Parsed HDR state value we need to switch to (true == ON, false == OFF). Empty optional if no action is required. */ diff --git a/src/display_device/session.cpp b/src/display_device/session.cpp index 3e6ae8aadfd..b87f1485f79 100644 --- a/src/display_device/session.cpp +++ b/src/display_device/session.cpp @@ -159,6 +159,8 @@ namespace display_device { current_vdd_client_id.clear(); last_vdd_setting.clear(); current_device_prep.reset(); + current_vdd_prep.reset(); + current_use_vdd.reset(); // 恢复原始的 output_name,避免下一个会话使用已销毁的 VDD 设备 ID if (!original_output_name.empty()) { config::video.output_name = original_output_name; @@ -240,10 +242,15 @@ namespace display_device { constexpr int max_retries = 3; const vdd_utils::physical_size_t physical_size = vdd_utils::get_client_physical_size(client_name); + // 复用模式使用固定标识符,否则使用客户端ID + const std::string vdd_identifier = config::video.vdd_reuse + ? "shared_vdd" + : client_id; + for (int retry = 1; retry <= max_retries; ++retry) { BOOST_LOG(info) << "正在执行第" << retry << "次VDD恢复尝试..."; - if (!vdd_utils::create_vdd_monitor(client_id, hdr_brightness, physical_size)) { + if (!vdd_utils::create_vdd_monitor(vdd_identifier, hdr_brightness, physical_size)) { BOOST_LOG(error) << "创建虚拟显示器失败,尝试" << retry << "/" << max_retries; if (retry < max_retries) { std::this_thread::sleep_for(std::chrono::seconds(1 << retry)); @@ -337,8 +344,10 @@ namespace display_device { return; } - // 保存当前会话的device_prep模式(可能包含客户端的override) + // 保存当前会话的配置模式(可能包含客户端的override) current_device_prep = parsed_config->device_prep; + current_vdd_prep = parsed_config->vdd_prep; + current_use_vdd = parsed_config->use_vdd; if (settings.is_changing_settings_going_to_fail()) { timer->setup_timer([this, config_copy = *parsed_config, &session, pre_saved_initial_topology]() { @@ -371,7 +380,11 @@ namespace display_device { bool session_t::create_vdd_monitor(const std::string &client_name) { const vdd_utils::physical_size_t physical_size = vdd_utils::get_client_physical_size(client_name); - return vdd_utils::create_vdd_monitor(client_name, vdd_utils::hdr_brightness_t { 1000.0f, 0.001f, 1000.0f }, physical_size); + // 复用模式使用固定标识符,否则使用客户端名称 + const std::string vdd_identifier = config::video.vdd_reuse + ? "shared_vdd" + : client_name; + return vdd_utils::create_vdd_monitor(vdd_identifier, vdd_utils::hdr_brightness_t { 1000.0f, 0.001f, 1000.0f }, physical_size); } bool @@ -425,20 +438,17 @@ namespace display_device { if (!device_zako.empty() && !current_vdd_client_id.empty() && !current_client_id.empty() && current_vdd_client_id != current_client_id) { - // 获取设备准备模式 - const auto device_prep = current_device_prep.value_or( - static_cast(config::video.display_device_prep) - ); - - // 无操作模式:复用VDD,只更新客户端ID - if (device_prep == parsed_config_t::device_prep_e::no_operation) { - BOOST_LOG(info) << "无操作模式,客户端切换时复用现有VDD"; + // 是否复用VDD(由独立配置项控制) + const bool reuse_vdd = config::video.vdd_reuse; + + if (reuse_vdd) { + // 复用VDD:所有客户端共享同一VDD,只更新客户端ID + BOOST_LOG(info) << "共享VDD模式,复用现有VDD(客户端: " << current_vdd_client_id << " -> " << current_client_id << ")"; current_vdd_client_id = current_client_id; - // 不销毁VDD,不重建,直接继续使用 } else { - // 常驻模式和非常驻模式:销毁并重建VDD - BOOST_LOG(info) << "客户端切换,重建VDD设备"; + // 不复用:销毁并重建VDD(每个客户端独立VDD) + BOOST_LOG(info) << "独立VDD模式,重建VDD设备(客户端: " << current_vdd_client_id << " -> " << current_client_id << ")"; const auto old_vdd_id = device_zako; vdd_utils::destroy_vdd_monitor(); @@ -471,7 +481,11 @@ namespace display_device { // Create VDD device if not present if (device_zako.empty()) { BOOST_LOG(info) << "创建虚拟显示器..."; - vdd_utils::create_vdd_monitor(current_client_id, hdr_brightness, physical_size); + // 复用模式使用固定标识符,否则使用客户端ID生成唯一GUID + const std::string vdd_identifier = config::video.vdd_reuse + ? "shared_vdd" // 固定标识符,所有客户端共用同一GUID + : current_client_id; // 为每个客户端生成不同GUID + vdd_utils::create_vdd_monitor(vdd_identifier, hdr_brightness, physical_size); std::this_thread::sleep_for(500ms); } @@ -511,10 +525,22 @@ namespace display_device { current_vdd_client_id = current_client_id; BOOST_LOG(info) << "成功配置VDD设备: " << device_zako; - // Ensure VDD is in extended mode - if (vdd_utils::ensure_vdd_extended_mode(device_zako)) { - BOOST_LOG(info) << "已将VDD切换到扩展模式"; - std::this_thread::sleep_for(500ms); + // Apply VDD prep settings to handle display topology + // This determines how VDD interacts with physical displays + // VDD模式下的拓扑控制与普通模式分开处理 + 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)) { + BOOST_LOG(info) << "已应用VDD屏幕布局设置"; + std::this_thread::sleep_for(500ms); + } + } + else { + // No specific configuration, ensure VDD is in extended mode (default behavior) + if (vdd_utils::ensure_vdd_extended_mode(device_zako)) { + BOOST_LOG(info) << "已将VDD切换到扩展模式"; + std::this_thread::sleep_for(500ms); + } } // Set HDR state with retry @@ -542,31 +568,42 @@ namespace display_device { session_t::restore_state_impl(revert_reason_e reason) { // 统一的VDD清理逻辑(在恢复拓扑之前执行,不需要CCD API,锁屏时也可以执行) const auto vdd_id = display_device::find_device_by_friendlyname(ZAKO_NAME); + + // 判断是否为 VDD 模式 + const bool is_vdd_mode = current_use_vdd.value_or(false); + + // 获取当前有效的配置模式 + // VDD模式:使用 vdd_prep + // 普通模式:使用 device_prep + const auto vdd_prep = current_vdd_prep.value_or( + static_cast(config::video.vdd_prep) + ); const auto device_prep = current_device_prep.value_or( static_cast(config::video.display_device_prep) ); - // 判断是否是跳过拓扑恢复的模式 - const bool is_no_operation = (device_prep == parsed_config_t::device_prep_e::no_operation); + // 判断是否是无操作模式(拓扑从未被修改过) + // VDD模式看 vdd_prep,普通模式看 device_prep + const bool is_no_operation = is_vdd_mode + ? (vdd_prep == parsed_config_t::vdd_prep_e::no_operation) + : (device_prep == parsed_config_t::device_prep_e::no_operation); + + // 常驻模式:只影响 VDD 是否销毁,不影响拓扑恢复 const bool is_keep_enabled = config::video.vdd_keep_enabled; if (!vdd_id.empty()) { bool should_destroy = false; - // 判断1:无操作模式 - 保留VDD - if (is_no_operation) { - BOOST_LOG(debug) << "无操作模式,保留VDD"; - } - // 判断2:常驻模式 - 保留VDD - else if (is_keep_enabled) { + // 判断1:常驻模式 - 保留VDD + if (is_keep_enabled) { BOOST_LOG(debug) << "常驻模式,保留VDD"; } - // 判断3:有persistent_data - 非常驻模式销毁VDD(无论是否在初始拓扑) + // 判断2:非常驻模式 - 销毁VDD(无论是否是无操作模式) else if (settings.has_persistent_data()) { - BOOST_LOG(info) << "非常驻/无操作模式,销毁VDD"; + BOOST_LOG(info) << "非常驻模式,销毁VDD"; should_destroy = true; } - // 判断4:无persistent_data(异常残留)- 非常驻模式清理 + // 判断3:无persistent_data(异常残留)- 清理VDD else { BOOST_LOG(info) << "检测到异常残留的VDD(无persistent_data),清理VDD"; should_destroy = true; @@ -578,11 +615,10 @@ namespace display_device { } } - // 常驻模式或无操作模式:跳过拓扑恢复,只清理VDD状态 - if (is_keep_enabled || is_no_operation) { - BOOST_LOG(info) << (is_keep_enabled ? "常驻模式" : "无操作模式") << ",跳过拓扑恢复"; - // 不调用 revert_settings,避免恢复屏幕记忆 - // 但仍然清理VDD状态(current_vdd_client_id等) + // 无操作模式:跳过拓扑恢复(因为拓扑从未被修改过) + // 注意:即使是无操作模式,VDD 在非常驻模式下也会被销毁(上面已处理) + if (is_no_operation) { + BOOST_LOG(info) << "无操作模式,跳过拓扑恢复"; stop_timer_and_clear_vdd_state(); return; } diff --git a/src/display_device/session.h b/src/display_device/session.h index 93609043324..9374a6e30f5 100644 --- a/src/display_device/session.h +++ b/src/display_device/session.h @@ -240,6 +240,8 @@ namespace display_device { std::string current_vdd_client_id; /**< Current client ID associated with VDD monitor. */ std::string original_output_name; /**< Original output_name value before VDD device ID was set. */ boost::optional current_device_prep; /**< Current device preparation mode, respecting client overrides. */ + boost::optional current_vdd_prep; /**< Current VDD preparation mode for VDD mode sessions. */ + boost::optional current_use_vdd; /**< Whether current session is using VDD mode. */ bool pending_restore_ = false; /**< Flag indicating if there is a pending restore settings operation waiting for unlock. */ bool should_replace_vdd_id_ = false; /**< Flag indicating if VDD ID needs to be replaced after client switch. */ std::string old_vdd_id_; /**< Old VDD ID that needs to be replaced. */ diff --git a/src/display_device/vdd_utils.cpp b/src/display_device/vdd_utils.cpp index 377bc8b5172..67baef49657 100644 --- a/src/display_device/vdd_utils.cpp +++ b/src/display_device/vdd_utils.cpp @@ -607,5 +607,91 @@ namespace display_device { BOOST_LOG(warning) << action << "虚拟显示器HDR失败"; return false; } + + bool + apply_vdd_prep(const std::string &vdd_device_id, parsed_config_t::vdd_prep_e vdd_prep) { + if (vdd_device_id.empty()) { + BOOST_LOG(debug) << "VDD设备ID为空,跳过vdd_prep处理"; + return true; + } + + if (vdd_prep == parsed_config_t::vdd_prep_e::no_operation) { + BOOST_LOG(debug) << "vdd_prep设置为无操作,跳过物理显示器处理"; + return true; + } + + auto current_topology = get_current_topology(); + if (current_topology.empty()) { + BOOST_LOG(warning) << "无法获取当前显示器拓扑"; + return false; + } + + // 找出所有物理显示器(非VDD设备) + std::vector physical_devices; + for (const auto &group : current_topology) { + for (const auto &id : group) { + if (id != vdd_device_id) { + physical_devices.push_back(id); + } + } + } + + if (physical_devices.empty()) { + BOOST_LOG(debug) << "没有物理显示器需要处理"; + return true; + } + + active_topology_t new_topology; + + switch (vdd_prep) { + case parsed_config_t::vdd_prep_e::vdd_as_primary: { + // VDD为主屏模式:VDD放在第一位(主屏),物理显示器作为扩展显示器 + BOOST_LOG(info) << "应用vdd_prep: VDD为主屏,物理显示器为副屏"; + // VDD单独一组(放在第一位作为主显示器) + new_topology.push_back({ vdd_device_id }); + // 每个物理显示器单独一组(扩展模式) + for (const auto &physical_id : physical_devices) { + new_topology.push_back({ physical_id }); + } + break; + } + + case parsed_config_t::vdd_prep_e::vdd_as_secondary: { + // VDD为副屏模式:物理显示器为主屏,VDD作为扩展显示器 + BOOST_LOG(info) << "应用vdd_prep: 物理显示器为主屏,VDD为副屏"; + // 物理显示器放在前面(第一个为主显示器) + for (const auto &physical_id : physical_devices) { + new_topology.push_back({ physical_id }); + } + // VDD单独一组(作为副显示器) + new_topology.push_back({ vdd_device_id }); + break; + } + + case parsed_config_t::vdd_prep_e::display_off: { + // 熄屏模式:只保留VDD,关闭所有物理显示器 + BOOST_LOG(info) << "应用vdd_prep: 关闭物理显示器"; + new_topology.push_back({ vdd_device_id }); + // 不添加物理显示器,它们将被禁用 + break; + } + + default: + return true; + } + + if (!is_topology_valid(new_topology)) { + BOOST_LOG(error) << "新拓扑无效"; + return false; + } + + if (!set_topology(new_topology)) { + BOOST_LOG(error) << "设置拓扑失败"; + return false; + } + + BOOST_LOG(info) << "成功应用vdd_prep设置"; + return true; + } } // namespace vdd_utils } // namespace display_device \ No newline at end of file diff --git a/src/display_device/vdd_utils.h b/src/display_device/vdd_utils.h index 72f8384b8f5..26a46706b5d 100644 --- a/src/display_device/vdd_utils.h +++ b/src/display_device/vdd_utils.h @@ -123,6 +123,17 @@ namespace display_device::vdd_utils { bool ensure_vdd_extended_mode(const std::string &device_id, const std::unordered_set &physical_devices_to_preserve = {}); + /** + * @brief Apply VDD prep settings to handle physical displays. + * @param vdd_device_id The VDD device ID. + * @param vdd_prep The vdd_prep_e value specifying how to handle physical displays. + * @returns True if the operation succeeded. + * @note This operation modifies topology without saving/restoring state, + * as Windows automatically handles topology memory when displays change. + */ + bool + apply_vdd_prep(const std::string &vdd_device_id, parsed_config_t::vdd_prep_e vdd_prep); + VddSettings prepare_vdd_settings(const parsed_config_t &config); diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 76e482b3bfd..3a9c4f92752 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -400,6 +400,7 @@ namespace nvhttp { launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0")); launch_session->use_vdd = util::from_view(get_arg(args, "useVdd", "0")); launch_session->custom_screen_mode = util::from_view(get_arg(args, "customScreenMode", "-1")); + launch_session->custom_vdd_screen_mode = util::from_view(get_arg(args, "customVddScreenMode", "-1")); launch_session->max_nits = std::stof(get_arg(args, "maxBrightness", "1000")); launch_session->min_nits = std::stof(get_arg(args, "minBrightness", "0.001")); launch_session->max_full_nits = std::stof(get_arg(args, "maxAverageBrightness", "1000")); @@ -448,6 +449,7 @@ namespace nvhttp { launch_session->env["SUNSHINE_CLIENT_ENABLE_MIC"] = launch_session->enable_mic ? "true" : "false"; launch_session->env["SUNSHINE_CLIENT_USE_VDD"] = launch_session->use_vdd ? "true" : "false"; launch_session->env["SUNSHINE_CLIENT_CUSTOM_SCREEN_MODE"] = std::to_string(launch_session->custom_screen_mode); + launch_session->env["SUNSHINE_CLIENT_CUSTOM_VDD_SCREEN_MODE"] = std::to_string(launch_session->custom_vdd_screen_mode); int channelCount = launch_session->surround_info & (65535); switch (channelCount) { case 2: diff --git a/src/platform/windows/display_device/settings.cpp b/src/platform/windows/display_device/settings.cpp index 7fbadb73e01..625bfbc8fba 100644 --- a/src/platform/windows/display_device/settings.cpp +++ b/src/platform/windows/display_device/settings.cpp @@ -749,36 +749,46 @@ namespace display_device { const rtsp_stream::launch_session_t &session, const boost::optional &pre_saved_initial_topology) { const auto do_apply_config { [this, &pre_saved_initial_topology](const parsed_config_t &config) -> settings_t::apply_result_t { - // no_operation模式:完全不做任何事情 - if (config.device_prep == parsed_config_t::device_prep_e::no_operation) { - BOOST_LOG(info) << "Display device preparation mode is set to no_operation, skipping all display settings changes"; - - // 注意:这里不要清除persistent_data! - // 1. 如果一开始就是no_operation,persistent_data本来就是空的,保持为空即可。 - // 2. 如果之前是active模式(已有persistent_data),中途切换成no_operation,我们需要保留persistent_data, - // 以便在最终结束串流时能恢复到最初的状态。如果清除,会导致无法恢复之前的修改。 - - return { apply_result_t::result_e::success }; - } - + // 检测是否为VDD模式 + const bool is_vdd_mode = config.use_vdd && *config.use_vdd; + + // 根据模式选择不同的拓扑处理方式 + boost::optional topology_result; bool failed_while_reverting_settings { false }; - const boost::optional previously_configured_topology { persistent_data ? boost::make_optional(persistent_data->topology) : boost::none }; - - // On Windows the display settings are kept per an active topology list - each topology - // has separate configuration saved in the database. Therefore, we must always switch - // to the topology we want to modify before we actually start applying settings. - const auto topology_result { handle_device_topology_configuration(config, previously_configured_topology, [&]() { - const bool audio_sink_was_captured { audio_data != nullptr }; - if (!revert_settings(revert_reason_e::topology_switch)) { - failed_while_reverting_settings = true; - return false; - } - if (audio_sink_was_captured && !audio_data) { - audio_data = std::make_unique(); + if (is_vdd_mode) { + // VDD模式:拓扑由 vdd_prep 控制(在 prepare_vdd 中已处理),这里只获取 metadata + // 这里不修改拓扑,分辨率、刷新率、HDR 等设置仍然会应用 + BOOST_LOG(info) << "VDD mode: topology controlled by vdd_prep in prepare_vdd, only getting current topology metadata"; + topology_result = get_current_topology_metadata(config.device_id); + } + else { + // 普通模式:device_prep 控制拓扑 + if (config.device_prep == parsed_config_t::device_prep_e::no_operation) { + BOOST_LOG(info) << "Display device preparation mode is set to no_operation, topology will not be changed"; } - return true; - }, pre_saved_initial_topology) }; + + const boost::optional previously_configured_topology { + persistent_data ? boost::make_optional(persistent_data->topology) : boost::none + }; + + // On Windows the display settings are kept per an active topology list - each topology + // has separate configuration saved in the database. Therefore, we must always switch + // to the topology we want to modify before we actually start applying settings. + topology_result = handle_device_topology_configuration(config, previously_configured_topology, [&]() { + const bool audio_sink_was_captured { audio_data != nullptr }; + if (!revert_settings(revert_reason_e::topology_switch)) { + failed_while_reverting_settings = true; + return false; + } + + if (audio_sink_was_captured && !audio_data) { + audio_data = std::make_unique(); + } + return true; + }, pre_saved_initial_topology); + } + if (!topology_result) { // Error already logged return { failed_while_reverting_settings ? apply_result_t::result_e::revert_fail : apply_result_t::result_e::topology_fail }; diff --git a/src/platform/windows/display_device/settings_topology.cpp b/src/platform/windows/display_device/settings_topology.cpp index cb5fde0ae3e..0383bdbf2fd 100644 --- a/src/platform/windows/display_device/settings_topology.cpp +++ b/src/platform/windows/display_device/settings_topology.cpp @@ -434,4 +434,39 @@ namespace display_device { }; } + boost::optional + get_current_topology_metadata(const std::string &device_id) { + const std::string requested_device_id { find_one_of_the_available_devices(device_id) }; + if (requested_device_id.empty()) { + BOOST_LOG(error) << "Device not found: " << device_id; + return boost::none; + } + + const auto current_topology { get_current_topology() }; + if (!is_topology_valid(current_topology)) { + BOOST_LOG(error) << "Display topology is invalid!"; + return boost::none; + } + + if (!is_device_found_in_active_topology(requested_device_id, current_topology)) { + BOOST_LOG(error) << "Device " << requested_device_id << " is not active!"; + return boost::none; + } + + const bool primary_device_requested { device_id.empty() }; + const auto duplicated_devices { get_duplicate_devices(requested_device_id, current_topology) }; + + // VDD模式:不修改拓扑,使用当前拓扑作为initial和modified + return handled_topology_result_t { + topology_pair_t { + current_topology, + current_topology }, + topology_metadata_t { + current_topology, + {}, // 没有新启用的设备 + primary_device_requested, + duplicated_devices } + }; + } + } // namespace display_device diff --git a/src/platform/windows/display_device/settings_topology.h b/src/platform/windows/display_device/settings_topology.h index a2402b85968..52065ca4e97 100644 --- a/src/platform/windows/display_device/settings_topology.h +++ b/src/platform/windows/display_device/settings_topology.h @@ -108,4 +108,21 @@ namespace display_device { std::unordered_set remove_vdd_from_topology(active_topology_t &topology); + /** + * @brief Get current topology metadata without modifying anything. + * @param device_id The device ID to use for metadata. + * @return A topology metadata object with current state. + * + * This is a simplified version of handle_device_topology_configuration + * for VDD mode where we don't want to modify topology but still need + * the metadata for resolution/refresh rate settings. + * + * EXAMPLES: + * ```cpp + * const auto metadata = get_current_topology_metadata("DEVICE_ID"); + * ``` + */ + boost::optional + get_current_topology_metadata(const std::string &device_id); + } // namespace display_device diff --git a/src/process.cpp b/src/process.cpp index a8656a01e0c..f5ac594db21 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -186,6 +186,7 @@ namespace proc { _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; _env["SUNSHINE_CLIENT_ENABLE_MIC"] = launch_session->enable_mic ? "true" : "false"; _env["SUNSHINE_CLIENT_CUSTOM_SCREEN_MODE"] = std::to_string(launch_session->custom_screen_mode); + _env["SUNSHINE_CLIENT_CUSTOM_VDD_SCREEN_MODE"] = std::to_string(launch_session->custom_vdd_screen_mode); int channelCount = launch_session->surround_info & (65535); switch (channelCount) { case 2: diff --git a/src/rtsp.h b/src/rtsp.h index 730b2e9cd68..00a82645c5e 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -40,6 +40,7 @@ namespace rtsp_stream { bool enable_mic; bool use_vdd; int custom_screen_mode; + int custom_vdd_screen_mode; float max_nits; float min_nits; float max_full_nits; diff --git a/src_assets/common/assets/web/components/SetupWizard.vue b/src_assets/common/assets/web/components/SetupWizard.vue index 38c99c0414a..683fa2f7c5a 100644 --- a/src_assets/common/assets/web/components/SetupWizard.vue +++ b/src_assets/common/assets/web/components/SetupWizard.vue @@ -139,45 +139,91 @@

{{ $t('setup.step3_description') }}

-
-
- + + + + +
@@ -312,7 +358,8 @@ export default { selectedLocale: 'zh', // 默认中文 selectedDisplay: null, // 选择的显示器(虚拟或物理) selectedAdapter: '', - displayDevicePrep: 'ensure_only_display', // 默认选择:确保唯一显示器 + displayDevicePrep: 'ensure_only_display', // 默认选择:确保唯一显示器(普通模式) + vddPrep: 'no_operation', // VDD模式下的屏幕布局选项,默认:保持当前布局 saveSuccess: false, saveError: null, saving: false, @@ -355,6 +402,10 @@ export default { } else if (this.currentStep === 3) { return this.selectedAdapter !== null } else if (this.currentStep === 4) { + // VDD模式检查 vddPrep,普通模式检查 displayDevicePrep + if (this.isVirtualDisplay) { + return this.vddPrep !== null + } return this.displayDevicePrep !== null } return false @@ -436,8 +487,16 @@ export default { // 设置选择的显示器 config.output_name = this.selectedDisplay - // 添加显示器组合策略 - config.display_device_prep = this.displayDevicePrep + // 根据模式添加显示器组合策略 + if (this.isVirtualDisplay) { + // VDD模式:保存 vdd_prep + config.vdd_prep = this.vddPrep + // device_prep 在 VDD 模式下不使用,但保留默认值 + config.display_device_prep = 'no_operation' + } else { + // 普通模式:保存 device_prep + config.display_device_prep = this.displayDevicePrep + } console.log('保存配置:', config) @@ -457,7 +516,8 @@ export default { trackEvents.userAction('setup_wizard_completed', { selected_display: this.selectedDisplay, adapter: this.selectedAdapter, - display_device_prep: this.displayDevicePrep, + display_device_prep: this.isVirtualDisplay ? null : this.displayDevicePrep, + vdd_prep: this.isVirtualDisplay ? this.vddPrep : null, is_virtual_display: this.isVirtualDisplay }) diff --git a/src_assets/common/assets/web/composables/useConfig.js b/src_assets/common/assets/web/composables/useConfig.js index 5e7d6544d9a..22b1a51e662 100644 --- a/src_assets/common/assets/web/composables/useConfig.js +++ b/src_assets/common/assets/web/composables/useConfig.js @@ -57,6 +57,8 @@ const DEFAULT_TABS = [ capture_target: 'display', window_title: '', display_device_prep: 'no_operation', + vdd_prep: 'no_operation', + vdd_reuse: 'disabled', resolution_change: 'automatic', manual_resolution: '', refresh_rate_change: 'automatic', diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue index 72858232d28..a3af3194f13 100644 --- a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue +++ b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue @@ -1,5 +1,5 @@