Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
39 changes: 39 additions & 0 deletions src/display_device/parsed_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,19 @@ namespace display_device {
return static_cast<int>(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<int>(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<int>(parsed_config_t::vdd_prep_e::no_operation);
}

boost::optional<parsed_config_t>
make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session, bool is_reconfigure) {
parsed_config_t parsed_config;
Expand All @@ -553,6 +566,7 @@ namespace display_device {

parsed_config.device_id = device_id_to_use;
parsed_config.device_prep = static_cast<parsed_config_t::device_prep_e>(config.display_device_prep);
parsed_config.vdd_prep = static_cast<parsed_config_t::vdd_prep_e>(config.vdd_prep);
parsed_config.change_hdr_state = parse_hdr_option(config, session);

const int custom_screen_mode = session.custom_screen_mode;
Expand All @@ -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<int>(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<int>(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<int>(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<int>(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) ||
Expand All @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions src/display_device/parsed_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_t> resolution; /**< Parsed resolution value we need to switch to. Empty optional if no action is required. */
boost::optional<refresh_rate_t> refresh_rate; /**< Parsed refresh rate value we need to switch to. Empty optional if no action is required. */
boost::optional<bool> change_hdr_state; /**< Parsed HDR state value we need to switch to (true == ON, false == OFF). Empty optional if no action is required. */
Expand Down
106 changes: 71 additions & 35 deletions src/display_device/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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]() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<parsed_config_t::device_prep_e>(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();
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<parsed_config_t::vdd_prep_e>(config::video.vdd_prep)
);
const auto device_prep = current_device_prep.value_or(
static_cast<parsed_config_t::device_prep_e>(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;
Expand All @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions src/display_device/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<parsed_config_t::device_prep_e> current_device_prep; /**< Current device preparation mode, respecting client overrides. */
boost::optional<parsed_config_t::vdd_prep_e> current_vdd_prep; /**< Current VDD preparation mode for VDD mode sessions. */
boost::optional<bool> 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. */
Expand Down
Loading