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
2 changes: 2 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ namespace config {
"auto"s, // capture_compute_shader (automatic capability and benefit detection)
false, // wgc_disable_secure_desktop (disabled by default for security)
true, // dynamic_resolution_follow_display (default: on; matches existing behavior. Set false for legacy clients like PSVita Moonlight.)
true, // allow_client_resolution_change (default: on, so clients that already send 0x5506 RESOLUTION keep working; set false to ignore them)
};

audio_t audio {
Expand Down Expand Up @@ -1403,6 +1404,7 @@ namespace config {
}
bool_f(vars, "wgc_disable_secure_desktop", video.wgc_disable_secure_desktop);
bool_f(vars, "dynamic_resolution_follow_display", video.dynamic_resolution_follow_display);
bool_f(vars, "allow_client_resolution_change", video.allow_client_resolution_change);
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);
Expand Down
1 change: 1 addition & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ namespace config {
std::string capture_compute_shader; // GPU frame conversion: "auto", "on", "off"
bool wgc_disable_secure_desktop; // Auto-disable UAC secure desktop when using WGC capture
bool dynamic_resolution_follow_display; // If true, follow mid-stream host display resolution changes and notify client via extension; if false, keep initial stream resolution and let scaler handle changes (compatible with legacy clients like PSVita Moonlight that don't implement the extension)
bool allow_client_resolution_change; // If true, apply client-requested mid-stream resolution changes (0x5506 RESOLUTION); also requires dynamic_resolution_follow_display and automatic resolution_change
};

struct audio_t {
Expand Down
1 change: 1 addition & 0 deletions src/platform/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ namespace platf {
constexpr caps_t ds5_haptics_pcm = 0x80; // Native DualSense authored haptics PCM
constexpr caps_t dynamic_sdr_white = 0x100; // Reserved legacy SDR white updates; never advertised
constexpr caps_t remote_text_context = 0x200; // InputPane/UIA text context updates
constexpr caps_t client_resolution_change = 0x400; // Client-requested stream resolution changes (0x5506 RESOLUTION)
constexpr caps_t endpoint_auto_refresh = 0x8000; // Authenticated UDP endpoint migration v1
constexpr caps_t host_session_state = 0x800; // Session lock state updates (0x550E)
}; // namespace platform_caps
Expand Down
3 changes: 3 additions & 0 deletions src/rtsp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,9 @@ namespace rtsp_stream {
if (platf::query_session_locked().has_value()) {
caps |= platf::platform_caps::host_session_state;
}
if (stream::client_resolution_change_enabled(config::video, session.enable_sops)) {
caps |= platf::platform_caps::client_resolution_change;
}
ss << "a=x-ss-general.featureFlags:" << caps << std::endl;
#ifdef _WIN32
if (config::input.mouse && (caps & platf::platform_caps::pen_touch)) {
Expand Down
186 changes: 148 additions & 38 deletions src/stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ extern "C" {
#include "config.h"
#include "streaming/fec.h"
#include "display_device/display_device.h"
#include "display_device/parsed_config.h"
#include "display_device/session.h"
#include "globals.h"
#include "haptics/authored_ir.h"
Expand Down Expand Up @@ -155,6 +156,42 @@ namespace stream {
std::chrono::round<signed_rtp_tick>(presentation_time - epoch).count());
}

bool
client_resolution_change_enabled([[maybe_unused]] const config::video_t &video_config, [[maybe_unused]] bool session_enable_sops) {
#ifdef _WIN32
return video_config.allow_client_resolution_change &&
video_config.dynamic_resolution_follow_display &&
video_config.resolution_change == static_cast<int>(display_device::parsed_config_t::resolution_change_e::automatic) &&
session_enable_sops;
#else
// Live display mode switching is only implemented on Windows.
return false;
#endif
}

std::optional<std::pair<int, int>>
normalize_client_resolution_request(int width, int height) {
// The floor matches the Android client's and keeps tiny windows away from display
// modes that Windows or the VDD may reject mid-stream.
constexpr int min_dimension = 256;
constexpr int max_dimension = 16384;

// Encoders require even dimensions.
width &= ~1;
height &= ~1;
if (width < min_dimension || width > max_dimension || height < min_dimension || height > max_dimension) {
return std::nullopt;
}
return std::make_pair(width, height);
}

bool
client_resolution_request_exact(bool encodes_display_1to1, const std::optional<std::pair<int, int>> &shown_size, int stream_width, int stream_height) {
return encodes_display_1to1 && shown_size &&
((shown_size->first == stream_width && shown_size->second == stream_height) ||
(shown_size->first == stream_height && shown_size->second == stream_width));
}

namespace {
boost::atomic_flag global_cancel_pending = BOOST_ATOMIC_FLAG_INIT;

Expand Down Expand Up @@ -722,6 +759,9 @@ namespace stream {
hdr::client_display_capabilities_t reported_hdr_capabilities;
hdr::target_source_e hdr_target_source { hdr::target_source_e::safe_defaults };

// Last accepted client-driven resolution change; only touched by the control thread.
std::chrono::steady_clock::time_point client_resolution_last_request {};

safe::mail_raw_t::event_t<bool> shutdown_event;
safe::signal_t controlEnd;

Expand Down Expand Up @@ -2007,43 +2047,60 @@ namespace stream {
session->video.idr_events->raise(true);
});

// 辅助函数:处理分辨率变更
auto handle_resolution_change = [](session_t *session, int new_width, int new_height) {
int old_width = session->config.monitor.width;
int old_height = session->config.monitor.height;

BOOST_LOG(info) << "Dynamic resolution change requested: " << old_width << "x" << old_height
<< " -> " << new_width << "x" << new_height;
// 辅助函数:处理客户端请求的分辨率变更
// Only the display mode is changed here. The capture loop then follows the new display size
// (dynamic_resolution_follow_display), re-initializes the encoder and notifies the client (0x5507).
auto handle_resolution_change = [](session_t *session, int requested_width, int requested_height) {
// Re-validated here: the feature flag was computed at DESCRIBE and the config may have changed since.
if (!client_resolution_change_enabled(config::video, session->enable_sops)) {
BOOST_LOG(info) << "Ignoring client resolution change request to " << requested_width << "x" << requested_height
<< ": requires allow_client_resolution_change, dynamic_resolution_follow_display, automatic resolution_change"
<< " and a session started with \"Optimize game settings\"";
return;
}

// 验证分辨率范围
constexpr int MAX_RESOLUTION = 16384;
if (new_width <= 0 || new_width > MAX_RESOLUTION || new_height <= 0 || new_height > MAX_RESOLUTION) {
BOOST_LOG(warning) << "Invalid resolution value: " << new_width << "x" << new_height;
// A control-only session has no stream of its own and must not resize another client's display.
if (session->control_only) {
BOOST_LOG(info) << "Ignoring client resolution change request from a control-only session";
return;
}

const auto requested = normalize_client_resolution_request(requested_width, requested_height);
if (!requested) {
BOOST_LOG(warning) << "Invalid resolution value: " << requested_width << "x" << requested_height;
return;
}
const int new_width = requested->first;
const int new_height = requested->second;

int old_width = session->config.monitor.width;
int old_height = session->config.monitor.height;

BOOST_LOG(info) << "Dynamic resolution change requested: " << old_width << "x" << old_height
<< " -> " << new_width << "x" << new_height;

// 检查分辨率是否真的改变了
if (old_width == new_width && old_height == new_height) {
BOOST_LOG(debug) << "Resolution unchanged, ignoring request";
return;
}

// Clients debounce resizes; this only sheds request storms. It is measured from the start of the
// previous attempt, so a request that queued up behind a slow mode switch is still applied.
const auto now = std::chrono::steady_clock::now();
if (now - session->client_resolution_last_request < std::chrono::milliseconds(250)) {
BOOST_LOG(info) << "Dropping client resolution change request to " << new_width << "x" << new_height
<< ": less than 250 ms since the previous request";
return;
}
session->client_resolution_last_request = now;

// 检测是否是旋转导致的宽高互换(例如:1920x1080 -> 1080x1920)
bool is_rotation = (old_width == new_height && old_height == new_width);
if (is_rotation) {
BOOST_LOG(info) << "Detected display rotation: width and height swapped";
}

// 更新会话配置
session->config.monitor.width = new_width;
session->config.monitor.height = new_height;
perf::update_session_display(
session->launch_session_id,
session->config.monitor.width,
session->config.monitor.height,
session->config.monitor.framerate
);

// 创建临时的 launch_session_t 来更新显示设备配置
// 注意:必须按照结构体声明顺序初始化字段
rtsp_stream::launch_session_t temp_launch_session {};
Expand All @@ -2068,6 +2125,7 @@ namespace stream {
temp_launch_session.hdr_target_source = session->hdr_target_source;

bool active_display_resolved = true;
std::string target_device_id;
const auto active_display_event = mail::man->event<std::string>(mail::active_display);
const auto active_display = active_display_event->view(std::chrono::milliseconds {0});
const bool has_active_display = active_display && !active_display->empty();
Expand All @@ -2082,6 +2140,7 @@ namespace stream {
entry.second.friendly_name == target_display;
});
if (device_it != devices.end()) {
target_device_id = device_it->first;
temp_launch_session.env["SUNSHINE_CLIENT_DISPLAY_NAME"] = device_it->first;
temp_launch_session.use_vdd = device_it->second.friendly_name == ZAKO_NAME;
}
Expand All @@ -2095,8 +2154,34 @@ namespace stream {
}
}

if (!active_display_resolved) {
return;
}

// Current mode of the capture display, if it can be read.
const auto capture_display_size = [&]() -> std::optional<std::pair<int, int>> {
if (target_device_id.empty()) {
return std::nullopt;
}
const auto modes = display_device::get_current_display_modes({target_device_id});
const auto mode = modes.find(target_device_id);
if (mode == modes.end()) {
return std::nullopt;
}
return std::make_pair(static_cast<int>(mode->second.resolution.width), static_cast<int>(mode->second.resolution.height));
};

// The capture loop keeps the client/display scale ratio it chose when capture started, so a client-driven size
// is only exact while that ratio is 1:1 and the capture display still shows this stream's size: the launch mode
// (or the last client-driven change) was applied and the host display has not changed underneath.
const auto &encodes_1to1 = session->config.monitor.stream_encodes_display_1to1;
if (!client_resolution_request_exact(encodes_1to1 && encodes_1to1->load(), capture_display_size(), old_width, old_height)) {
BOOST_LOG(info) << "Ignoring client resolution change request: the capture display does not show the stream resolution "
<< old_width << "x" << old_height << " at a 1:1 scale";
return;
}

// 更新显示设备配置(重新配置模式)
// 注意:这也会触发捕获端和编码器的重新初始化,以适配新的分辨率
if (is_rotation) {
BOOST_LOG(info) << "Reconfiguring display device for rotation: " << old_width << "x" << old_height
<< " -> " << new_width << "x" << new_height;
Expand All @@ -2105,30 +2190,53 @@ namespace stream {
BOOST_LOG(info) << "Reconfiguring display device for new resolution: " << old_width << "x" << old_height
<< " -> " << new_width << "x" << new_height;
}

if (active_display_resolved) {
const bool display_reconfigured = stream::session::run_display_reconfiguration_if_single_video_session([&]() {
const auto result = display_device::session_t::get().configure_display(config::video, temp_launch_session, true);
if (!result) {
BOOST_LOG(warning) << "Dynamic display reconfiguration failed: " << result.message;
}
});
if (!display_reconfigured) {
BOOST_LOG(info) << "Skipping display device reconfiguration for the dynamic resolution request because other streaming sessions are active";

// Until the display reaches the requested size, the capture loop skips intermediate modes.
const auto &resolution_target = session->config.monitor.client_resolution_target;
std::optional<display_device::session_t::configure_result_t> result;
const bool display_reconfigured = stream::session::run_display_reconfiguration_if_single_video_session([&]() {
if (resolution_target) {
resolution_target->set(new_width, new_height);
}
result = display_device::session_t::get().configure_display(config::video, temp_launch_session, true);
});
if (!display_reconfigured) {
BOOST_LOG(info) << "Skipping display device reconfiguration for the dynamic resolution request because other streaming sessions are active";
return;
}
else {
BOOST_LOG(info) << "Dynamic stream resolution updated without changing the unavailable capture display";

// deferred_retry is not a success here: the retry yields to active sessions, so the mode never
// applies mid-stream. Keep the current state; the client may request the size again.
if (result->result != display_device::session_t::configure_result_t::result_e::success) {
if (resolution_target) {
resolution_target->clear();
}
BOOST_LOG(warning) << "Dynamic display reconfiguration to " << new_width << "x" << new_height
<< " was not applied (result " << static_cast<int>(result->result) << "): " << result->message;
return;
}

// The driver may have applied a slightly different mode; the capture loop and the session follow that one.
// If it cannot be read, assume the requested size; the target's deadline still bounds the capture loop's wait.
const auto applied_size = capture_display_size().value_or(std::make_pair(new_width, new_height));
if (resolution_target) {
resolution_target->settle(applied_size.first, applied_size.second);
}

session->config.monitor.width = applied_size.first;
session->config.monitor.height = applied_size.second;
perf::update_session_display(
session->launch_session_id,
session->config.monitor.width,
session->config.monitor.height,
session->config.monitor.framerate
);

// 请求 IDR 帧以确保客户端能正确显示新分辨率
// 这对于旋转场景特别重要,因为宽高互换需要新的关键帧
session->video.idr_events->raise(true);

// 注意:编码器和触摸端口的更新会在捕获端重新初始化时自动处理
// - 编码器会在重新初始化时使用新的宽高(通过 config.monitor.width/height)
// - 触摸端口会在视频捕获循环中通过 make_port() 自动更新
BOOST_LOG(info) << "Dynamic stream resolution updated: " << new_width << "x" << new_height
BOOST_LOG(info) << "Dynamic stream resolution updated: " << applied_size.first << "x" << applied_size.second
<< (is_rotation ? " (rotation detected)" : "");
};

Expand Down Expand Up @@ -4840,6 +4948,8 @@ namespace stream {

session->config = config;
session->config.monitor.capture_target = std::make_shared<streaming::capture_target_t>();
session->config.monitor.client_resolution_target = std::make_shared<video::pending_display_size_t>();
session->config.monitor.stream_encodes_display_1to1 = std::make_shared<std::atomic<bool>>(false);

session->control.connect_data = launch_session.control_connect_data;
session->control.feedback_queue = mail->queue<platf::gamepad_feedback_msg_t>(mail::gamepad_feedback);
Expand Down
34 changes: 34 additions & 0 deletions src/stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
Expand All @@ -19,6 +20,10 @@
#include "crypto.h"
#include "video.h"

namespace config {
struct video_t;
}

namespace rtsp_stream {
struct launch_session_t;
}
Expand All @@ -44,6 +49,35 @@ namespace stream {
std::chrono::steady_clock::time_point presentation_time,
std::chrono::steady_clock::time_point epoch);

/**
* @brief Whether a session may request stream resolution changes (0x5506 RESOLUTION).
*
* Shared by the RTSP feature flag (platform_caps::client_resolution_change) and the
* control-stream handler so the two cannot drift. Besides the opt-in, the capture loop only
* resizes the encoder and notifies the client (0x5507) when it follows the host display, and
* client-driven sizes are only exact when the launch mode was applied 1:1 (automatic resolution
* change with SOPS); otherwise its fixed scale ratio distorts them.
* Always false off Windows, where live display mode switching is not implemented.
*/
bool
client_resolution_change_enabled(const config::video_t &video_config, bool session_enable_sops);

/**
* @brief Validate a client-requested stream resolution.
* @return The size rounded down to even dimensions, or nullopt when a dimension is out of range.
*/
std::optional<std::pair<int, int>>
normalize_client_resolution_request(int width, int height);

/**
* @brief Whether a client-requested display size would become the stream size unchanged.
* @param encodes_display_1to1 Latched by the capture loop when capture starts (false until then); see
* video::encodes_display_1to1(). A scaled session stays scaled whatever its display shows later.
* @param shown_size Current mode of the capture display, or nullopt if it could not be read.
*/
bool
client_resolution_request_exact(bool encodes_display_1to1, const std::optional<std::pair<int, int>> &shown_size, int stream_width, int stream_height);

struct session_t;
struct config_t {
audio::config_t audio;
Expand Down
Loading
Loading