[feat] add macOS and mobile resolution controls with disconnect restore

This commit is contained in:
dijunkun
2026-11-11 11:02:41 +08:00
parent a3159b198e
commit 34b59de15d
50 changed files with 1086 additions and 36 deletions
@@ -220,6 +220,7 @@ void GuiRuntime::HandleServerControllerDisconnected(
windows_rdp_handoff_connected_->store(has_connected_controller,
std::memory_order_relaxed);
#endif
if (!has_connected_controller) ++display_resolution_epoch_;
}
clipboard_.ForgetWeb(remote_id);
if (has_connected_controller) {
@@ -269,6 +270,9 @@ void GuiRuntime::HandleScreenLock() {
std::unique_lock lock(connection_status_mutex_);
if (!disconnect_lock_.TakeRequest(config_center_->IsLockOnDisconnect()))
return;
// Restore while the interactive desktop is still available. This forced
// path does not reacquire connection_status_mutex_.
RestoreHostDisplayResolutions(true);
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
if (LockScreen()) LOG_INFO("Requested screen lock after last controller disconnected");
#endif
@@ -0,0 +1,61 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-11-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DISPLAY_RESOLUTION_RESTORATION_H_
#define _DISPLAY_RESOLUTION_RESTORATION_H_
#include <functional>
#include <map>
#include <string>
#include <utility>
#include <display_resolution.h>
namespace crossdesk {
// Application-thread state. Each callback owns the original native display
// mode, so restoration does not depend on capture still running or slot order.
class DisplayResolutionRestoration {
public:
using Restore = std::function<bool()>;
template <typename Capture, typename Change>
bool Apply(const std::string &key, Capture capture, Change change) {
if (originals_.count(key))
return change();
if (originals_.size() >= kMaxResolutionDisplays)
return false;
auto restore = capture();
if (!restore)
return false;
// Save before changing anything, including a failed change whose native
// rollback might fail. Subsequent requests never replace this baseline.
originals_.emplace(key, std::move(restore));
const bool accepted = change();
if (!accepted && originals_.at(key)())
originals_.erase(key);
return accepted;
}
bool RestoreAll() {
for (auto it = originals_.begin(); it != originals_.end();) {
if (it->second())
it = originals_.erase(it);
else
++it;
}
return originals_.empty();
}
bool Empty() const { return originals_.empty(); }
private:
std::map<std::string, Restore> originals_;
};
} // namespace crossdesk
#endif
@@ -2,15 +2,19 @@
#include <SDL3/SDL_timer.h>
#include "rd_log.h"
#ifdef _WIN32
#include "interactive_desktop.h"
#include "platform/windows/screen_capturer/display_resolution_win.h"
#elif defined(__APPLE__)
#include "platform/macos/screen_capturer/display_resolution_mac.h"
#endif
namespace crossdesk {
namespace {
// EnumDisplaySettings caches its enumeration. Serialize queries on transport
// threads with mode changes on the application thread.
// Serialize native queries on transport threads with mode changes on the
// application thread (EnumDisplaySettings also caches its enumeration).
std::mutex native_display_resolution_mutex;
} // namespace
@@ -18,6 +22,9 @@ std::vector<DisplayResolutionInfo> GuiRuntime::QueryHostDisplayResolutions() {
#ifdef _WIN32
std::lock_guard lock(native_display_resolution_mutex);
return QueryWindowsDisplayResolutions(devices_.display_info_list());
#elif defined(__APPLE__)
std::lock_guard lock(native_display_resolution_mutex);
return QueryMacDisplayResolutions(devices_.display_info_list());
#else
return {};
#endif
@@ -26,24 +33,54 @@ std::vector<DisplayResolutionInfo> GuiRuntime::QueryHostDisplayResolutions() {
void GuiRuntime::QueueDisplayResolutionChange(
const std::string &id, PeerEventHandler *source,
const DisplayResolutionChange &change) {
const auto epoch = display_resolution_epoch_.load();
const auto events = ControllerEvents(id);
if (events.get() != source || !IsAuthorizedController(id))
return;
std::lock_guard lock(display_resolution_commands_mutex_);
if (display_resolution_commands_.size() >= 32)
return;
display_resolution_commands_.push_back({id, events, change});
display_resolution_commands_.push_back({id, events, change, epoch});
}
void GuiRuntime::RestoreHostDisplayResolutions(bool force) {
const auto epoch = display_resolution_epoch_.load();
const bool disconnected = epoch != handled_display_resolution_epoch_;
handled_display_resolution_epoch_ = epoch;
if (display_resolution_restoration_.Empty())
return;
const auto now = SDL_GetTicks();
// A disconnect boundary must survive a rapid reconnect. Otherwise retry
// failed restoration only while idle, without disrupting an active session.
if (!disconnected && !force &&
(now < next_display_resolution_restore_ ||
!ConnectedControllers().empty()))
return;
next_display_resolution_restore_ = now + 2000;
std::lock_guard lock(native_display_resolution_mutex);
#ifdef _WIN32
if (!IsCurrentSessionUserDesktopActive())
return;
#endif
if (display_resolution_restoration_.RestoreAll())
LOG_INFO("Restored original display modes after remote control");
else if (disconnected || force)
LOG_WARN("Could not restore every display mode; will retry while idle");
next_display_resolution_poll_ = 0;
}
void GuiRuntime::HandleDisplayResolutionChanges() {
RestoreHostDisplayResolutions();
std::deque<PendingDisplayResolution> commands;
{
std::lock_guard lock(display_resolution_commands_mutex_);
commands.swap(display_resolution_commands_);
}
for (const auto &command : commands) {
RestoreHostDisplayResolutions();
const auto source = command.source.lock();
if (!source || !source->IsActive() ||
if (stopping_ || command.epoch != display_resolution_epoch_.load() ||
!source || !source->IsActive() ||
source != ControllerEvents(command.remote_id) ||
!IsAuthorizedController(command.remote_id))
continue;
@@ -51,20 +88,57 @@ void GuiRuntime::HandleDisplayResolutionChanges() {
reply.type = ControlType::display_resolution_status;
reply.resolution_change = command.change;
reply.resolution_change.accepted = false;
const auto displays = devices_.display_info_list();
const auto available = QueryHostDisplayResolutions();
const auto selected =
std::find_if(available.begin(), available.end(), [&](const auto &info) {
return SupportsDisplayResolution(info, command.change);
});
if (selected == available.end()) {
// Unsupported or stale requests must never create a restoration record.
} else if (selected->current == command.change.resolution) {
reply.resolution_change.accepted = true;
} else {
#ifdef _WIN32
{
std::lock_guard lock(native_display_resolution_mutex);
if (IsCurrentSessionUserDesktopActive())
reply.resolution_change.accepted = ApplyWindowsDisplayResolution(
devices_.display_info_list(), command.change);
}
{
std::lock_guard lock(native_display_resolution_mutex);
if (IsCurrentSessionUserDesktopActive())
reply.resolution_change.accepted =
display_resolution_restoration_.Apply(
command.change.display_key,
[&] {
return CaptureWindowsDisplayResolution(displays,
command.change);
},
[&] {
return ApplyWindowsDisplayResolution(displays,
command.change);
});
}
#elif defined(__APPLE__)
{
std::lock_guard lock(native_display_resolution_mutex);
reply.resolution_change.accepted =
display_resolution_restoration_.Apply(
command.change.display_key,
[&] {
return CaptureMacDisplayResolution(displays, command.change);
},
[&] {
return ApplyMacDisplayResolution(displays, command.change);
});
}
#endif
}
reply.display_resolutions = QueryHostDisplayResolutions();
const auto message = reply.to_json();
SendControllerData(command.remote_id, message.data(), message.size(),
control_data_label_.c_str());
next_display_resolution_poll_ = 0;
}
// The transport can disconnect while a synchronous native mode switch is
// in progress. Restore that change on this tick as well.
RestoreHostDisplayResolutions();
const auto now = SDL_GetTicks();
if (now < next_display_resolution_poll_)
return;
@@ -82,6 +82,7 @@ GuiRuntime::~GuiRuntime() {
lan_.reset();
transfers_.Shutdown();
WaitForSessionCleanup();
RestoreHostDisplayResolutions(true);
}
void GuiRuntime::SendRemoteInput(const RemoteAction& action) {
@@ -25,6 +25,7 @@
#include "privacy_controller.h"
#include "runtime/gui_state.h"
#include "runtime/disconnect_lock_policy.h"
#include "runtime/display_resolution_restoration.h"
#include "runtime/announcement_inbox.h"
#include "runtime/peer_event_handler.h"
#include "runtime/peer_transport.h"
@@ -157,6 +158,7 @@ class GuiRuntime : protected gui_detail::GuiState {
PeerEventHandler* source,
const DisplayResolutionChange& change);
void HandleDisplayResolutionChanges();
void RestoreHostDisplayResolutions(bool force = false);
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
@@ -256,11 +258,18 @@ class GuiRuntime : protected gui_detail::GuiState {
std::string remote_id;
std::weak_ptr<PeerEventHandler> source;
DisplayResolutionChange change;
uint64_t epoch;
};
std::mutex display_resolution_commands_mutex_;
std::deque<PendingDisplayResolution> display_resolution_commands_;
std::vector<DisplayResolutionInfo> last_display_resolutions_;
uint64_t next_display_resolution_poll_ = 0;
// Incremented when the last incoming controller disconnects. Keep the
// boundary even if a new connection arrives before the application tick.
std::atomic<uint64_t> display_resolution_epoch_{0};
uint64_t handled_display_resolution_epoch_ = 0;
uint64_t next_display_resolution_restore_ = 0;
DisplayResolutionRestoration display_resolution_restoration_;
bool privacy_settings_changed_ = false;
uint32_t last_privacy_revision_ = 0;
uint64_t last_privacy_status_tick_ = 0;
@@ -218,6 +218,7 @@ void GuiRuntime::BeginShutdown() {
std::chrono::steady_clock::now() + std::chrono::seconds(5);
if (lan_) lan_->Stop();
CloseAllRemoteSessions();
RestoreHostDisplayResolutions(true);
}
bool GuiRuntime::ShutdownDrained() {
TickLan();
@@ -225,6 +226,7 @@ bool GuiRuntime::ShutdownDrained() {
const bool drained =
(!lan_ || lan_->Quiescent()) && !connection_peer_cleanup_.valid() &&
session_cleanup_tasks_.empty() && controller_cleanup_tasks_.empty();
if (drained) RestoreHostDisplayResolutions(true);
if (!drained && std::chrono::steady_clock::now() >= shutdown_report_at_) {
LOG_WARN(
"Shutdown waiting: LAN [{}], cloud peer={}, viewers={}, controllers={}",