server/src/remote_display_topology.cpp
Rama main · réplica de lectura del repositorio original.
#include "remote_display_topology.h"
#include <algorithm>
#include <set>
#include <unordered_set>
#include <sstream>
// The composer is built on its own by a component test, which links no logging backend. So the
// lines are compiled out there and go to the project loggers in the product: this file stays
// dependency-free enough to be tested, and still says what it is doing on a real host.
#ifdef SUNSHINE_TESTS
namespace {
// Swallows the stream without naming a logger, so a component test can build this file
// without a logging backend.
struct discarded_log_t {
template <typename T>
discarded_log_t &operator<<(const T &) { return *this; }
};
} // namespace
#define REMOTE_MONITOR_LOG(level) discarded_log_t {}
#else
// Only in the product build. The component test links no logging backend and carries only
// Boost's core headers, so this include is a compile error there: boost/log/trivial.hpp lives
// in Boost's log module, which the test does not have. It sat above the #ifdef and broke the
// Linux build the first time the suite reached this file (2026-11-04 17:40:54).
#include <boost/log/trivial.hpp>
#include "logging.h"
#define REMOTE_MONITOR_LOG(level) BOOST_LOG(level)
#endif
namespace remote_display_topology {
std::string_view remote_monitor_suffix() { return ":remote-monitor"; }
std::string remote_monitor_stable_id(const std::string &client_uuid) {
if (client_uuid.empty()) {
return {};
}
// A role suffix, not a second uuid: the game role keeps hashing the bare client uuid, so
// every existing game display and its recovery journal entry stays valid, and only the
// monitor moves to an identity of its own.
return client_uuid + std::string {remote_monitor_suffix()};
}
namespace {
constexpr int max_gap_px = 100000;
bool contains(const std::vector<std::string> &values, const std::string &value) {
return std::find(values.begin(), values.end(), value) != values.end();
}
const char *lifecycle_name(lifecycle_e lifecycle) {
switch (lifecycle) {
case lifecycle_e::desired: return "desired";
case lifecycle_e::leased: return "leased";
case lifecycle_e::applying: return "applying";
case lifecycle_e::ready: return "ready";
case lifecycle_e::retryable: return "retryable";
case lifecycle_e::released: return "released";
}
return "retryable";
}
} // namespace
namespace {
nlohmann::json empty_layout() { return {{"version", layout_version}, {"placements", nlohmann::json::object()}}; }
}
bool layout_schema_valid(const nlohmann::json &layout, std::string &error) {
if (!layout.is_object() || !layout.contains("version") || !layout["version"].is_number_integer() || layout["version"].get<int>() != static_cast<int>(layout_version) || !layout.contains("placements") || !layout["placements"].is_object()) {
error = "Layout must be version 1 with an object of placements.";
return false;
}
const std::set<std::string> edges {"left", "right", "above", "below"};
const std::set<std::string> alignments {"start", "center", "end"};
for (const auto &[client, placement] : layout["placements"].items()) {
if (client.empty() || !placement.is_object() || !placement.contains("anchor_kind") || !placement["anchor_kind"].is_string() || !placement.contains("anchor_id") || !placement["anchor_id"].is_string() || !placement.contains("edge") || !placement["edge"].is_string() || !placement.contains("alignment") || !placement["alignment"].is_string() || !placement.contains("gap_px") || !placement["gap_px"].is_number_integer()) {
error = "Every layout placement must use the expected field types.";
return false;
}
if (placement.contains("primary") && !placement["primary"].is_boolean()) {
error = "Placement primary must be a boolean.";
return false;
}
const auto gap = placement["gap_px"].get<int>();
if ((placement["anchor_kind"].get<std::string>() != "physical" && placement["anchor_kind"].get<std::string>() != "client") || placement["anchor_id"].get<std::string>().empty() || !edges.contains(placement["edge"].get<std::string>()) || !alignments.contains(placement["alignment"].get<std::string>()) || gap < 0 || gap > max_gap_px) {
error = "Placement has an unsupported anchor, edge, alignment, or gap.";
return false;
}
}
return true;
}
nlohmann::json normalize_layout(const nlohmann::json &layout) {
std::string error;
return layout_schema_valid(layout, error) ? layout : empty_layout();
}
bool validate_layout(const nlohmann::json &layout, const std::vector<std::string> &known_clients, const std::vector<std::string> &known_physical_ids, std::string &error) {
if (!layout_schema_valid(layout, error)) return false;
unsigned int primary_count = 0;
std::unordered_map<std::string, std::string> client_anchors;
const std::set<std::string> edges {"left", "right", "above", "below"};
const std::set<std::string> alignments {"start", "center", "end"};
for (const auto &[client, placement] : layout["placements"].items()) {
if (!contains(known_clients, client) || !placement.is_object()) {
error = "Every placement must name a paired client.";
return false;
}
const auto anchor_kind = placement["anchor_kind"].get<std::string>();
const auto anchor_id = placement["anchor_id"].get<std::string>();
if (placement.value("primary", false) && ++primary_count > 1) {
error = "Only one client display may be primary.";
return false;
}
if (anchor_kind == "client") {
if (!contains(known_clients, anchor_id) || anchor_id == client) {
error = "A client placement cannot anchor to itself or an unknown client.";
return false;
}
client_anchors.emplace(client, anchor_id);
} else if (!contains(known_physical_ids, anchor_id)) {
error = "A physical placement must anchor to a currently known monitor device path.";
return false;
}
}
for (const auto &[start, ignored] : client_anchors) {
std::unordered_set<std::string> visited;
auto current = start;
for (auto it = client_anchors.find(current); it != client_anchors.end(); it = client_anchors.find(current)) {
if (!visited.insert(current).second) {
error = "Client placements must not contain an anchor cycle.";
return false;
}
current = it->second;
}
}
return true;
}
coordinator_t &instance() { static coordinator_t coordinator; return coordinator; }
void coordinator_t::set_runtime_callbacks(runtime_callbacks_t callbacks) { std::lock_guard lock(mutex_); callbacks_ = std::move(callbacks); }
void coordinator_t::set_layout(nlohmann::json layout) { std::lock_guard lock(mutex_); layout_ = normalize_layout(layout); }
void coordinator_t::set_physical_baseline(std::vector<node_t> nodes) { std::lock_guard lock(mutex_); physical_baseline_ = std::move(nodes); }
std::vector<std::string> coordinator_t::physical_node_ids() const {
std::lock_guard lock(mutex_);
std::vector<std::string> ids;
for (const auto &node : physical_baseline_) {
if (node.physical) ids.push_back(node.id);
}
return ids;
}
bool coordinator_t::normal_game_release_pending() const {
std::lock_guard lock(mutex_);
return std::any_of(clients_.begin(), clients_.end(), [](const auto &entry) {
return entry.second.normal_release_pending;
});
}
std::size_t coordinator_t::managed_client_identity_count() const {
std::lock_guard lock(mutex_);
return static_cast<std::size_t>(std::count_if(clients_.begin(), clients_.end(), [](const auto &entry) {
return entry.second.normal_game || entry.second.remote_monitor;
}));
}
std::vector<std::string> coordinator_t::managed_client_identity_ids() const {
std::lock_guard lock(mutex_);
std::vector<std::string> ids;
for (const auto &[uuid, state] : clients_) {
if (state.normal_game || state.remote_monitor) ids.push_back(uuid);
}
std::sort(ids.begin(), ids.end());
return ids;
}
std::vector<std::string> coordinator_t::protected_remote_monitor_client_ids() const {
std::lock_guard lock(mutex_);
std::vector<std::string> ids;
for (const auto &[uuid, state] : clients_) {
if (state.remote_monitor) ids.push_back(uuid);
}
std::sort(ids.begin(), ids.end());
return ids;
}
bool coordinator_t::generic_virtual_display_cleanup_allowed() const {
std::lock_guard lock(mutex_);
return std::none_of(clients_.begin(), clients_.end(), [](const auto &entry) {
return entry.second.normal_game || entry.second.remote_monitor;
});
}
void coordinator_t::set_plaintext_rtsp_warning_provider(std::function<std::string(const std::string &)> provider) { std::lock_guard lock(mutex_); plaintext_rtsp_warning_provider_ = std::move(provider); }
normal_game_reservation_t coordinator_t::reserve_normal_game_identity(const std::string &client_uuid, const std::string &label, mode_t mode) {
std::lock_guard lock(mutex_);
if (client_uuid.empty() || (!clients_.contains(client_uuid) && clients_.size() >= max_client_identities)) return {};
auto [state_it, inserted] = clients_.try_emplace(client_uuid);
auto &state = state_it->second;
if (inserted) state.placement_order = ++next_placement_order_;
state.label = label;
state.normal_requested_mode = mode;
if (!state.remote_monitor) state.effective_mode = mode;
if (state.normal_game && !state.normal_release_pending) return {true, false, state.normal_game_token};
state.normal_game = true;
state.normal_release_pending = false;
state.normal_game_token = ++next_normal_game_token_;
return {true, true, state.normal_game_token};
}
bool coordinator_t::reapply_composed_topology() {
std::lock_guard lock(mutex_);
if (!callbacks_.apply_composed_topology) {
return false;
}
if (callbacks_.resolve_mode) {
for (auto &[uuid, state] : clients_) {
if (state.normal_game || state.remote_monitor) {
resolve_effective_mode_locked(uuid, state);
}
}
} else {
for (auto &[_, state] : clients_) {
if (state.normal_game || state.remote_monitor) state.effective_mode = desired_mode(state);
}
}
if (!refresh_unverified_shared_geometry_locked()) {
// A geometry change is still landing. Replacing the topology now would move live peers
// onto coordinates that do not exist yet, and the compositor would refuse the lot.
return false;
}
std::vector<std::string> ignored;
return callbacks_.apply_composed_topology(compose_locked(ignored));
}
bool coordinator_t::rollback_normal_game_identity(const std::string &client_uuid, const std::uint64_t token) {
std::lock_guard lock(mutex_);
const auto it = clients_.find(client_uuid);
if (it == clients_.end() || !it->second.normal_game || it->second.normal_game_token != token) {
return false;
}
if (!it->second.normal_capture_references.empty()) {
it->second.normal_release_pending = true;
return false;
}
it->second.normal_game = false;
it->second.normal_requested_mode.reset();
it->second.normal_game_token = 0;
it->second.normal_release_pending = false;
if (it->second.remote_monitor) {
return false;
}
clients_.erase(it);
return true;
}
void coordinator_t::release_normal_game_identity(const std::string &client_uuid, const std::uint64_t token) {
std::lock_guard lock(mutex_);
const auto it = clients_.find(client_uuid);
if (it == clients_.end() || !it->second.normal_game || token == 0 || it->second.normal_game_token != token) return;
auto &state = it->second;
state.normal_release_pending = true;
if (!state.normal_capture_references.empty()) {
return;
}
release_normal_game_identity_locked(client_uuid, state);
}
struct coordinator_t::capture_reference_t {
coordinator_t &owner;
std::string client_uuid;
std::uint64_t token;
bool held = false;
capture_reference_t(coordinator_t &owner, const std::string &client_uuid, const std::uint64_t token):
owner(owner),
client_uuid(client_uuid),
token(token) {}
~capture_reference_t() {
if (!held) {
return;
}
std::lock_guard lock(owner.mutex_);
const auto client = owner.clients_.find(client_uuid);
if (client == owner.clients_.end()) {
return;
}
auto &references = client->second.normal_capture_references;
const auto reference = references.find(token);
if (reference != references.end() && --reference->second == 0) {
references.erase(reference);
}
}
};
std::shared_ptr<void> coordinator_t::retain_normal_game_capture(const std::string &client_uuid, const std::uint64_t token) {
auto reference = std::make_shared<capture_reference_t>(*this, client_uuid, token);
std::lock_guard lock(mutex_);
const auto client = clients_.find(client_uuid);
if (client == clients_.end() || !client->second.normal_game || token == 0 || client->second.normal_game_token != token || client->second.normal_release_pending) {
return {};
}
++client->second.normal_capture_references[token];
reference->held = true;
return reference;
}
void coordinator_t::release_drained_normal_game_identities() {
std::lock_guard lock(mutex_);
for (auto it = clients_.begin(); it != clients_.end();) {
auto current = it++;
if (current->second.normal_release_pending && current->second.normal_capture_references.empty()) {
release_normal_game_identity_locked(current->first, current->second);
}
}
}
void coordinator_t::release_normal_game_identity_locked(const std::string &client_uuid, client_state_t &state) {
const auto requested_mode = state.normal_requested_mode;
const auto token = state.normal_game_token;
state.normal_game = false;
state.normal_requested_mode.reset();
state.normal_game_token = 0;
if (state.remote_monitor) {
state.normal_release_pending = false;
return;
}
// Retire the output from KWin only after the remaining/saved topology is
// active. Disconnecting the connector first can transiently leave the
// compositor with zero outputs and force a physical-link retrain.
if (callbacks_.apply_composed_topology) {
std::vector<std::string> ignored;
if (!apply_composed_or_accept_empty_locked(ignored)) {
state.normal_game = true;
state.normal_requested_mode = requested_mode;
state.normal_game_token = token;
return;
}
} else {
state.normal_game = true;
state.normal_requested_mode = requested_mode;
state.normal_game_token = token;
return;
}
if (callbacks_.remove_owned_display && !callbacks_.remove_owned_display(client_uuid)) {
state.normal_game = true;
state.normal_requested_mode = requested_mode;
state.normal_game_token = token;
std::vector<std::string> ignored;
(void) callbacks_.apply_composed_topology(compose_locked(ignored));
return;
}
clients_.erase(client_uuid);
}
void coordinator_t::release_all_normal_game_identities() {
std::vector<std::pair<std::string, std::uint64_t>> owners;
{
std::lock_guard lock(mutex_);
for (const auto &[uuid, state] : clients_) {
if (state.normal_game) owners.emplace_back(uuid, state.normal_game_token);
}
}
for (const auto &[uuid, token] : owners) {
release_normal_game_identity(uuid, token);
}
}
activation_result_t coordinator_t::activate_remote_monitor(const std::string &client_uuid, const std::string &label, mode_t mode) {
const auto result = activate_or_resume(client_uuid, label, mode, 0);
return {result.accepted, result.ready, result.error};
}
activation_result_t coordinator_t::resume_remote_monitor(const std::string &client_uuid) {
std::lock_guard lock(mutex_);
const auto it = clients_.find(client_uuid);
if (it == clients_.end() || !it->second.remote_monitor) {
REMOTE_MONITOR_LOG(warning) << "Remote Monitor resume refused: client '" << client_uuid << "' does not hold the monitor role.";
return {false, false, "Remote Monitor is not owned by this paired client."};
}
return activate_locked(client_uuid, it->second);
}
monitor_runtime_state_t coordinator_t::activate_or_resume(const std::string &client_uuid, const std::string &label, mode_t mode, uint64_t generation) {
std::lock_guard lock(mutex_);
if (!clients_.contains(client_uuid) && clients_.size() >= max_client_identities) {
REMOTE_MONITOR_LOG(warning) << "Remote Monitor refused: capacity reached (" << clients_.size() << "/"
<< max_client_identities << " client identities).";
return {false, false, true, {}, "Remote display capacity is four paired-client identities."};
}
auto [state_it, inserted] = clients_.try_emplace(client_uuid);
auto &state = state_it->second;
if (inserted) state.placement_order = ++next_placement_order_;
if (generation < state.generation) {
return {true, state.lifecycle == lifecycle_e::ready, state.lifecycle == lifecycle_e::retryable, state.exact_output, state.warning, state.effective_mode.hdr};
}
state.generation = generation;
state.label = label;
state.monitor_requested_mode = mode;
state.effective_mode = mode;
state.remote_monitor = true;
const auto result = activate_locked(client_uuid, state);
return {result.accepted, result.capture_ready, state.lifecycle == lifecycle_e::retryable, state.exact_output, result.warning, state.effective_mode.hdr};
}
monitor_runtime_state_t coordinator_t::snapshot(const std::string &client_uuid, uint64_t generation) const {
std::lock_guard lock(mutex_);
const auto it = clients_.find(client_uuid);
if (it == clients_.end() || generation != it->second.generation) return {};
const auto &state = it->second;
return {true, state.lifecycle == lifecycle_e::ready, state.lifecycle == lifecycle_e::retryable, state.exact_output, state.warning, state.effective_mode.hdr};
}
bool coordinator_t::is_ready(const std::string &client_uuid, uint64_t generation) const { return snapshot(client_uuid, generation).ready; }
void coordinator_t::explicit_release(const std::string &client_uuid, uint64_t generation, const std::string &reason) {
std::lock_guard lock(mutex_);
const auto it = clients_.find(client_uuid);
if (it == clients_.end() || generation != it->second.generation) return;
release_locked(client_uuid, it->second, reason);
if (!it->second.normal_game && !it->second.remote_monitor) {
clients_.erase(it);
}
}
void coordinator_t::transport_lost(const std::string &client_uuid, uint64_t generation) {
std::lock_guard lock(mutex_);
const auto it = clients_.find(client_uuid);
if (it == clients_.end() || generation != it->second.generation) return;
it->second.lifecycle = lifecycle_e::retryable;
it->second.warning = "Transport was lost; Remote Monitor ownership and desired settings were retained for Resume.";
}
activation_result_t coordinator_t::activate_locked(const std::string &client_uuid, client_state_t &state) {
if (!callbacks_.create_or_reclaim || !callbacks_.apply_composed_topology || !callbacks_.exact_target_has_current_mode_and_dxgi) {
state.lifecycle = lifecycle_e::retryable;
state.warning = "Remote Monitor runtime wiring is unavailable; ownership was retained for Resume.";
return {true, false, state.warning};
}
// Another role's shared output may still be landing a mode it was asked for. Every node is
// placed from these numbers, so composing a requested one that has not taken effect moves
// live peers onto a rectangle the desktop does not have - and the compositor refuses the
// whole topology and keeps the old one, which is exactly what the refusal looks like.
if (!refresh_unverified_shared_geometry_locked(client_uuid)) {
state.lifecycle = lifecycle_e::retryable;
state.warning = "A shared display mode is awaiting verification; topology changes are deferred.";
return {true, false, state.warning};
}
// A lease taken on this call is a connector nothing else is capturing: its mode is the
// pool's default rather than a promise, and the apply below is what sets it. Verifying it
// beforehand can only ever disagree, and returning a 503 to a client that asks once per
// launch turns that disagreement into a Remote Monitor that never opens. A lease that was
// already held is the opposite case - a live peer we must not move onto unverified numbers.
bool took_lease_now = false;
if (!state.lease_held) {
state.lifecycle = lifecycle_e::leased;
if (!callbacks_.create_or_reclaim(client_uuid, state.label, desired_mode(state))) {
const auto want = desired_mode(state);
REMOTE_MONITOR_LOG(warning) << "Remote monitor activation: create_or_reclaim failed for client '" << client_uuid
<< "' (label '" << state.label << "') mode=" << want.width << "x" << want.height << "x"
<< want.refresh_hz << "; ownership retained for Resume.";
state.lifecycle = lifecycle_e::retryable;
state.warning = "The owned virtual display could not be created or reclaimed; Resume will retry the same identity.";
return {true, false, state.warning};
}
state.lease_held = true;
took_lease_now = true;
}
resolve_effective_mode_locked(client_uuid, state);
// A retained lease is a live peer: its geometry must be observed before anything is placed
// against it. A lease taken on this call is the only case that skips the check, because its
// mode is the pool default until the apply below sets it.
if (callbacks_.observe_shared_mode && !took_lease_now) {
// A successful create/reclaim does not prove the geometry took effect, and a role that
// owns a live output can be asked for more than its panel or driver granted.
const auto observed = callbacks_.observe_shared_mode(client_uuid, state.remote_monitor);
const bool disagrees = observed && (observed->width != state.effective_mode.width ||
observed->height != state.effective_mode.height ||
observed->refresh_hz != state.effective_mode.refresh_hz);
if (disagrees && state.shared_mode_deferrals < max_shared_geometry_deferrals) {
++state.shared_mode_deferrals;
REMOTE_MONITOR_LOG(warning) << "Remote monitor activation: shared geometry for client '" << client_uuid
<< "' is " << observed->width << "x" << observed->height << "x" << observed->refresh_hz
<< " but " << state.effective_mode.width << "x" << state.effective_mode.height << "x"
<< state.effective_mode.refresh_hz << " was requested; deferring the topology.";
state.effective_mode.width = observed->width;
state.effective_mode.height = observed->height;
state.effective_mode.refresh_hz = observed->refresh_hz;
state.shared_mode_unverified = true;
state.lifecycle = lifecycle_e::retryable;
state.warning = "The shared display does not yet match the requested geometry; Resume will retry.";
return {true, false, state.warning};
}
if (!disagrees) {
// Unknown geometry is not a disagreement: the platform simply could not read it, and
// refusing to stream because a query failed would be worse than the old behaviour.
state.shared_mode_unverified = false;
state.shared_mode_deferrals = 0;
}
}
state.lifecycle = lifecycle_e::applying;
std::vector<std::string> warnings;
const auto composed = compose_locked(warnings);
if (!callbacks_.apply_composed_topology(composed)) {
REMOTE_MONITOR_LOG(warning) << "Remote monitor activation: composed topology did not apply for client '" << client_uuid
<< "' (" << composed.size() << " node(s)); ownership retained for Resume.";
state.shared_mode_unverified = true;
state.lifecycle = lifecycle_e::retryable;
state.warning = "The composed display topology did not apply; existing owners and their displays were retained.";
return {true, false, state.warning};
}
if (!warnings.empty()) state.warning = warnings.front();
const auto exact_output = callbacks_.exact_target_has_current_mode_and_dxgi(client_uuid, state.effective_mode);
if (!exact_output || exact_output->empty()) {
REMOTE_MONITOR_LOG(warning) << "Remote monitor activation: no exact capture output for client '" << client_uuid
<< "' mode=" << state.effective_mode.width << "x" << state.effective_mode.height << "x"
<< state.effective_mode.refresh_hz << "; ownership retained for Resume.";
// The output is not capture-ready yet, which is the same obligation as an unverified
// geometry: do not let the next composition assume the mode landed.
state.shared_mode_unverified = true;
state.lifecycle = lifecycle_e::retryable;
state.warning = "The requested client display is not yet capture-ready. Existing streamed physical or virtual displays remain topology anchors, but capture will wait rather than mirror another screen.";
return {true, false, state.warning};
}
state.exact_output = *exact_output;
state.shared_mode_unverified = false;
state.shared_mode_deferrals = 0;
state.lifecycle = lifecycle_e::ready;
state.warning.clear();
return {true, true, {}};
}
void coordinator_t::note_lease_lost(const std::string &client_uuid) {
std::lock_guard lock(mutex_);
if (auto it = clients_.find(client_uuid); it != clients_.end()) {
it->second.lease_held = false;
it->second.lifecycle = lifecycle_e::retryable;
it->second.warning = "Remote Monitor lease was lost; desired settings and identity are retained for Resume.";
}
}
void coordinator_t::release_abandoned_remote_monitors(const std::string &reason) {
std::lock_guard lock(mutex_);
for (auto it = clients_.begin(); it != clients_.end();) {
// ready means a live transport is still capturing this role: a monitor someone is
// actually watching is never abandoned. Everything else - retryable after its transport
// went away, leased or applying with no session left - is a display kept only for a
// resume that cannot happen now that the app has ended.
if (!it->second.remote_monitor || it->second.lifecycle == lifecycle_e::ready) {
++it;
continue;
}
const auto released_uuid = it->first;
REMOTE_MONITOR_LOG(info) << "Releasing Remote Monitor role for client '" << released_uuid << "' (" << reason << ").";
release_locked(released_uuid, it->second, reason);
// The composition no longer names this client and an empty compose is accepted without
// asking Windows for anything, so nothing else will retire this output. Whoever releases a
// role on the app's last breath removes the display it owned, right here.
if (callbacks_.remove_owned_display) {
callbacks_.remove_owned_display(released_uuid);
}
if (!it->second.normal_game && !it->second.remote_monitor) {
it = clients_.erase(it);
} else {
++it;
}
}
}
void coordinator_t::disconnect_monitor(const std::string &client_uuid) {
std::lock_guard lock(mutex_);
const auto it = clients_.find(client_uuid);
if (it == clients_.end()) return;
release_locked(client_uuid, it->second, "Remote Monitor was disconnected.");
if (!it->second.normal_game && !it->second.remote_monitor) {
clients_.erase(it);
}
}
void coordinator_t::unpair_client(const std::string &client_uuid) {
// Unpair releases Remote Monitor ownership, but it cannot discard a normal
// game's still-live ownership record. The process teardown releases that
// role with its reservation token.
disconnect_monitor(client_uuid);
}
void coordinator_t::shutdown(const bool preserve_owned_displays) {
std::lock_guard lock(mutex_);
if (preserve_owned_displays) {
// A supervised host hands the still-connected machine display to its
// successor. Process teardown must not issue KScreen or hotplug work.
clients_.clear();
return;
}
std::vector<std::string> managed_ids;
for (const auto &[uuid, state] : clients_) {
if (state.normal_game || state.remote_monitor) managed_ids.push_back(uuid);
}
std::sort(managed_ids.begin(), managed_ids.end());
clients_.clear();
if (callbacks_.apply_composed_topology) {
std::vector<std::string> ignored;
(void) callbacks_.apply_composed_topology(compose_locked(ignored));
}
for (const auto &uuid : managed_ids) {
if (callbacks_.remove_owned_display) {
(void) callbacks_.remove_owned_display(uuid);
}
}
}
void coordinator_t::release_locked(const std::string &client_uuid, client_state_t &state, const std::string &reason) {
if (!state.remote_monitor) return;
const auto monitor_mode = state.monitor_requested_mode;
const auto previous_lease = state.lease_held;
const auto previous_lifecycle = state.lifecycle;
const auto previous_warning = state.warning;
/*
* Each role owns its own VDD identity - the monitor's display is created under
* "<uuid>:remote-monitor", the game's under the bare uuid - so ending the monitor role must
* retire the monitor's display whether or not the same client's game role is still streaming.
* The old rule ("ending either role cannot remove the other's display") was true while the two
* roles shared one connector. Since the split it retires nothing and leaves the monitor's
* display behind, which is the ghost that makes every later setDisplayConfig call fail.
*/
const bool remove_display = callbacks_.remove_owned_display != nullptr;
bool display_removed = false;
state.remote_monitor = false;
state.monitor_requested_mode.reset();
if (state.normal_game) {
resolve_effective_mode_locked(client_uuid, state);
}
// The normal-game role still owns the shared connector. Keep the lease
// truthfully held so a later monitor resume cannot appear to reacquire or
// disconnect it out from under the game.
state.lease_held = state.normal_game;
state.lifecycle = lifecycle_e::released;
state.warning = reason;
// Recompose only the remaining explicit owners. This intentionally does
// not restore a saved/global topology or remove any peer identity.
if (callbacks_.apply_composed_topology) {
std::vector<std::string> ignored;
bool applied = apply_composed_or_accept_empty_locked(ignored);
if (!applied && remove_display) {
/*
* A departing display whose own session has ended can still sit in Windows' database as an
* active path with no target mode. setDisplayConfig then rejects the request during
* validation with ERROR_INVALID_PARAMETER before it looks at it at all (2026-11-04
* 15:20:34: one requested node, game display on DISPLAY15, monitor display left active on
* DISPLAY16 with target_mode absent), so shrinking the topology first can never succeed
* while that ghost exists. Retire the display this role owns - the other role's display has
* a different identity and is not touched - and ask once more: with the ghost gone the
* request is valid and the handoff completes.
*/
display_removed = callbacks_.remove_owned_display(client_uuid);
applied = display_removed && apply_composed_or_accept_empty_locked(ignored);
}
if (!applied) {
state.remote_monitor = true;
state.monitor_requested_mode = monitor_mode;
state.lease_held = previous_lease;
state.lifecycle = previous_lifecycle;
state.warning = previous_warning;
resolve_effective_mode_locked(client_uuid, state);
return;
}
} else if (remove_display) {
state.remote_monitor = true;
state.monitor_requested_mode = monitor_mode;
state.lease_held = previous_lease;
state.lifecycle = previous_lifecycle;
state.warning = previous_warning;
resolve_effective_mode_locked(client_uuid, state);
return;
}
if (remove_display && !display_removed && !callbacks_.remove_owned_display(client_uuid)) {
state.remote_monitor = true;
state.monitor_requested_mode = monitor_mode;
state.lease_held = previous_lease;
state.lifecycle = previous_lifecycle;
state.warning = previous_warning;
resolve_effective_mode_locked(client_uuid, state);
std::vector<std::string> ignored;
(void) callbacks_.apply_composed_topology(compose_locked(ignored));
}
}
mode_t coordinator_t::effective_mode(const node_t &node) { return node.current_mode.value_or(node.last_requested_mode.value_or(node.configured_mode)); }
int coordinator_t::layout_width(const node_t &node) {
return node.layout_width.value_or(effective_mode(node).width);
}
int coordinator_t::layout_height(const node_t &node) {
return node.layout_height.value_or(effective_mode(node).height);
}
mode_t coordinator_t::desired_mode(const client_state_t &state) {
if (state.remote_monitor && state.monitor_requested_mode) return *state.monitor_requested_mode;
if (state.normal_game && state.normal_requested_mode) return *state.normal_requested_mode;
return {};
}
void coordinator_t::resolve_effective_mode_locked(const std::string &client_uuid, client_state_t &state) {
state.effective_mode = desired_mode(state);
if (callbacks_.resolve_mode) callbacks_.resolve_mode(client_uuid, state.effective_mode);
}
bool coordinator_t::refresh_unverified_shared_geometry_locked(const std::string &except) {
if (!callbacks_.observe_shared_mode) {
return true;
}
for (auto &[uuid, state] : clients_) {
if (uuid == except || (!state.normal_game && !state.remote_monitor)) continue;
if (!state.shared_mode_unverified) continue;
const auto observed = callbacks_.observe_shared_mode(uuid, state.remote_monitor);
if (!observed || observed->width <= 0 || observed->height <= 0 || observed->refresh_hz <= 0) {
// The output exists but its geometry is not readable yet. Composing the requested
// numbers here is what made the compositor refuse and retain the old topology.
return false;
}
state.effective_mode.width = observed->width;
state.effective_mode.height = observed->height;
state.effective_mode.refresh_hz = observed->refresh_hz;
}
return true;
}
bool coordinator_t::apply_composed_or_accept_empty_locked(std::vector<std::string> &warnings) {
const auto composed = compose_locked(warnings);
const bool nothing_to_place = composed.empty();
const bool applied = callbacks_.apply_composed_topology ? callbacks_.apply_composed_topology(composed) : false;
if (!applied && nothing_to_place) {
// No role wants a display: there is no topology to apply, and asking for one would be a
// request to place nothing rather than a request that failed. Reported as a refusal, the
// last release rolled its identity back (release_locked and release_normal_game_identity_
// locked both undo on a false return), which kept normal_game set for the life of the
// process: generic_virtual_display_cleanup_allowed() then answered false forever, every
// teardown deferred, and the physical outputs were never restored - the black KVM after a
// session ended (2026-11-04 16:24:36: three deferrals in thirty-one milliseconds, with the
// roster already reading game=false monitor=false).
REMOTE_MONITOR_LOG(info) << "Remote monitor composition: no role wants a display; accepting the refusal.";
return true;
}
return applied;
}
std::vector<node_t> coordinator_t::compose_locked(std::vector<std::string> &warnings) const {
// Name the roster every time a topology is built. A compose that silently carries one client
// fewer than the caller expected is indistinguishable from one that was correct, and the log
// of a refusal then reads like a clean run - which is exactly how a missing node stayed
// invisible for a night.
for (const auto &[uuid, state] : clients_) {
REMOTE_MONITOR_LOG(info) << "Remote monitor compose roster: client '" << uuid << "' game=" << state.normal_game
<< " monitor=" << state.remote_monitor << " lifecycle=" << static_cast<int>(state.lifecycle)
<< " mode=" << state.effective_mode.width << "x" << state.effective_mode.height << "x"
<< state.effective_mode.refresh_hz;
}
auto nodes = physical_baseline_;
int rightmost = 0;
for (const auto &node : nodes) rightmost = std::max(rightmost, node.x + layout_width(node));
std::vector<std::string> active_ids;
for (const auto &[uuid, state] : clients_) {
if (state.remote_monitor || state.normal_game) active_ids.push_back(uuid);
}
std::sort(active_ids.begin(), active_ids.end(), [&](const std::string &lhs, const std::string &rhs) {
const auto lhs_order = clients_.at(lhs).placement_order;
const auto rhs_order = clients_.at(rhs).placement_order;
return lhs_order == rhs_order ? lhs < rhs : lhs_order < rhs_order;
});
std::unordered_set<std::string> emitted;
std::unordered_set<std::string> visiting;
std::function<void(const std::string &)> emit = [&](const std::string &uuid) {
if (emitted.contains(uuid)) return;
const auto state_it = clients_.find(uuid);
if (state_it == clients_.end() || (!state_it->second.remote_monitor && !state_it->second.normal_game)) return;
/*
* Per role, not per client: effective_mode is a single field and the monitor's activation
* overwrites it, so the game node was wearing the monitor's mode. Each role's own request is
* what belongs on its node.
*/
const auto role_mode = state_it->second.remote_monitor && state_it->second.normal_game
? state_it->second.normal_requested_mode.value_or(state_it->second.effective_mode)
: state_it->second.effective_mode;
/*
* The monitor role's display is created under "<uuid>:remote-monitor", never the bare uuid,
* so a client holding only the monitor role must carry that identity on its node or the
* compose step looks the display up under an identity nothing was ever created with. Before
* the per-role identity existed the monitor borrowed the client's display, which is why a
* second device used to work and stopped once the role got its own display.
*/
const bool monitor_only = state_it->second.remote_monitor && !state_it->second.normal_game;
node_t node {
.id = monitor_only ? remote_monitor_stable_id(uuid) : uuid,
.label = state_it->second.label,
.active = true,
.configured_mode = role_mode,
.last_requested_mode = role_mode,
};
const auto append_right = [&](const std::string &warning) {
node.x = rightmost;
rightmost += layout_width(node);
if (!warning.empty()) warnings.push_back(warning);
nodes.push_back(std::move(node));
emitted.insert(uuid);
};
if (!visiting.insert(uuid).second) {
append_right("Saved client anchors contain a cycle; '" + uuid + "' was appended to the right for this activation.");
return;
}
const auto placement_it = layout_["placements"].find(uuid);
if (placement_it == layout_["placements"].end()) {
append_right({});
visiting.erase(uuid);
return;
}
const auto &placement = *placement_it;
node.primary = placement.value("primary", false);
const auto anchor_id = placement.value("anchor_id", "");
if (placement.value("anchor_kind", "") == "client") {
emit(anchor_id);
if (emitted.contains(uuid)) {
visiting.erase(uuid);
return;
}
}
// Saved anchors are client uuids, but a monitor-only node carries the role suffix, so match
// the anchor on either form or an existing placement stops resolving.
const auto anchor = std::find_if(nodes.begin(), nodes.end(), [&](const node_t &candidate) {
return candidate.id == anchor_id || candidate.id == remote_monitor_stable_id(anchor_id);
});
if (anchor == nodes.end()) {
append_right("Saved anchor '" + anchor_id + "' is unavailable; the client display was appended to the right for this activation.");
visiting.erase(uuid);
return;
}
const auto anchor_width = layout_width(*anchor);
const auto anchor_height = layout_height(*anchor);
const auto width = layout_width(node);
const auto height = layout_height(node);
const auto gap = placement.value("gap_px", 0);
const auto edge = placement.value("edge", "right");
const auto alignment = placement.value("alignment", "center");
if (edge == "left") node.x = anchor->x - width - gap;
if (edge == "right") node.x = anchor->x + anchor_width + gap;
if (edge == "above") node.y = anchor->y - height - gap;
if (edge == "below") node.y = anchor->y + anchor_height + gap;
if (edge == "left" || edge == "right") node.y = alignment == "start" ? anchor->y : alignment == "end" ? anchor->y + anchor_height - height : anchor->y + (anchor_height - height) / 2;
if (edge == "above" || edge == "below") node.x = alignment == "start" ? anchor->x : alignment == "end" ? anchor->x + anchor_width - width : anchor->x + (anchor_width - width) / 2;
rightmost = std::max(rightmost, node.x + width);
nodes.push_back(std::move(node));
emitted.insert(uuid);
visiting.erase(uuid);
};
for (const auto &uuid : active_ids) emit(uuid);
// A paired client can hold a game role and a Remote Monitor role at once, and each role owns
// a separate display. The list above carries one node per client, so the monitor's display
// had no place in the model at all - the composer kept the game's node and the capture was
// refused ("capture will wait rather than mirror another screen") instead of being aimed at
// the wrong screen. Give the monitor its own node, on its own stable identity, beside its
// owner's. Two-device setups are untouched by construction: each client there holds a single
// role, so this loop never runs and the node list is byte-identical to before.
for (const auto &uuid : active_ids) {
const auto state_it = clients_.find(uuid);
if (state_it == clients_.end()) continue;
const auto &state = state_it->second;
if (!state.remote_monitor || !state.normal_game) continue;
const auto monitor_mode = state.monitor_requested_mode.value_or(state.effective_mode);
node_t monitor {
.id = remote_monitor_stable_id(uuid),
.label = state.label,
.active = true,
.configured_mode = monitor_mode,
.last_requested_mode = monitor_mode,
};
monitor.x = rightmost;
rightmost += layout_width(monitor);
nodes.push_back(std::move(monitor));
warnings.push_back("Client '" + uuid + "' owns both a game display and a Remote Monitor display; the monitor was placed to the right for this activation.");
}
// One place that says what the topology will actually ask Windows for. A refusal is otherwise
// read against the roster and the node list guessed at, and a node that never got emitted looks
// exactly like a node that was emitted and rejected.
REMOTE_MONITOR_LOG(info) << "Remote monitor compose result: " << nodes.size() << " node(s)";
for (const auto &node : nodes) {
REMOTE_MONITOR_LOG(info) << "Remote monitor compose result: node id='" << node.id << "' label='" << node.label
<< "' device_id='" << node.device_id << "' physical=" << node.physical
<< " preexisting=" << node.preexisting << " mode=" << node.configured_mode.width << "x"
<< node.configured_mode.height << "x" << node.configured_mode.refresh_hz << " at " << node.x
<< "," << node.y << (node.primary ? " primary" : "");
}
return nodes;
}
nlohmann::json coordinator_t::snapshot(const std::vector<nlohmann::json> &paired_clients) const {
std::lock_guard lock(mutex_);
std::vector<std::string> warnings;
const auto nodes = compose_locked(warnings);
nlohmann::json result {{"status", true}, {"version", layout_version}, {"layout", layout_}, {"capacity", {{"max", max_client_identities}, {"used", clients_.size()}}}, {"warnings", warnings}, {"nodes", nlohmann::json::array()}, {"clients", paired_clients}};
for (const auto &node : nodes) {
const auto mode = effective_mode(node);
result["nodes"].push_back({{"id", node.id}, {"label", node.label}, {"kind", node.physical ? "physical" : "client"}, {"active", node.active}, {"primary", node.primary}, {"desired_position", {{"x", node.x}, {"y", node.y}}}, {"current_position", {{"x", node.x}, {"y", node.y}}}, {"mode", {{"width", mode.width}, {"height", mode.height}, {"refresh_hz", mode.refresh_hz}, {"hdr", mode.hdr}}}});
}
for (const auto &[uuid, state] : clients_) result["runtime"][uuid] = {{"lifecycle", lifecycle_name(state.lifecycle)}, {"ready", state.lifecycle == lifecycle_e::ready}, {"retryable", state.lifecycle == lifecycle_e::retryable}, {"lease_held", state.lease_held}, {"warning", state.warning}, {"plaintext_rtsp_warning", plaintext_rtsp_warning_provider_ ? plaintext_rtsp_warning_provider_(uuid) : ""}};
return result;
}
} // namespace remote_display_topology