#include "remote_display_topology.h" #include #include #include #include // 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 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 #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 &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() != static_cast(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 edges {"left", "right", "above", "below"}; const std::set 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(); if ((placement["anchor_kind"].get() != "physical" && placement["anchor_kind"].get() != "client") || placement["anchor_id"].get().empty() || !edges.contains(placement["edge"].get()) || !alignments.contains(placement["alignment"].get()) || 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 &known_clients, const std::vector &known_physical_ids, std::string &error) { if (!layout_schema_valid(layout, error)) return false; unsigned int primary_count = 0; std::unordered_map client_anchors; const std::set edges {"left", "right", "above", "below"}; const std::set 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(); const auto anchor_id = placement["anchor_id"].get(); 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 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 nodes) { std::lock_guard lock(mutex_); physical_baseline_ = std::move(nodes); } std::vector coordinator_t::physical_node_ids() const { std::lock_guard lock(mutex_); std::vector 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::count_if(clients_.begin(), clients_.end(), [](const auto &entry) { return entry.second.normal_game || entry.second.remote_monitor; })); } std::vector coordinator_t::managed_client_identity_ids() const { std::lock_guard lock(mutex_); std::vector 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 coordinator_t::protected_remote_monitor_client_ids() const { std::lock_guard lock(mutex_); std::vector 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 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 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 coordinator_t::retain_normal_game_capture(const std::string &client_uuid, const std::uint64_t token) { auto reference = std::make_shared(*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 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 ignored; (void) callbacks_.apply_composed_topology(compose_locked(ignored)); return; } clients_.erase(client_uuid); } void coordinator_t::release_all_normal_game_identities() { std::vector> 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 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 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 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 * ":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 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 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 &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 coordinator_t::compose_locked(std::vector &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(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 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 emitted; std::unordered_set visiting; std::function 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 ":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 &paired_clients) const { std::lock_guard lock(mutex_); std::vector 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