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

This commit is contained in:
dijunkun committed 2026-11-11 11:02:41 +08:00
1 parent a3159b198e
commit 34b59de15d
50 files changed
+1086 -36

No files matched your search

@@ -0,0 +1,231 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-11-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DISPLAY_RESOLUTION_MAC_H_
#define _DISPLAY_RESOLUTION_MAC_H_
#include <ColorSync/ColorSync.h>
#include <CoreGraphics/CoreGraphics.h>
#include <IOKit/graphics/IOGraphicsTypes.h>
#include <cmath>
#include <functional>
#include <map>
#include <memory>
#include <tuple>
#include "display_info.h"
#include <display_resolution.h>
namespace crossdesk {
inline DisplayResolution MacDisplayResolution(CGDisplayModeRef mode) {
if (!mode)
return {};
// Desktop dimensions, matching macOS scaled resolutions. Pixel dimensions
// can be twice these values on Retina displays and belong to capture only.
const auto width = CGDisplayModeGetWidth(mode);
const auto height = CGDisplayModeGetHeight(mode);
if (width > 32768 || height > 32768)
return {};
return {int(width), int(height)};
}
inline bool ResolveMacResolutionDisplay(const DisplayInfo &display,
CGDirectDisplayID &id,
std::string &key) {
id = static_cast<CGDirectDisplayID>(
reinterpret_cast<uintptr_t>(display.handle));
if (!id || !CGDisplayIsOnline(id) || !CGDisplayIsActive(id))
return false;
CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(id);
if (!uuid)
return false;
CFStringRef uuid_string = CFUUIDCreateString(nullptr, uuid);
CFRelease(uuid);
if (!uuid_string)
return false;
char value[128]{};
const bool valid = CFStringGetCString(uuid_string, value, sizeof(value),
kCFStringEncodingUTF8);
CFRelease(uuid_string);
if (!valid)
return false;
key = std::to_string(id) + "|" + value;
return true;
}
// The array owns the borrowed mode references returned by Choices().
class MacResolutionModes {
public:
explicit MacResolutionModes(CGDirectDisplayID display)
: current(CGDisplayCopyDisplayMode(display)),
modes_(CGDisplayCopyAllDisplayModes(display, nullptr)) {}
~MacResolutionModes() {
if (modes_)
CFRelease(modes_);
if (current)
CGDisplayModeRelease(current);
}
MacResolutionModes(const MacResolutionModes &) = delete;
MacResolutionModes &operator=(const MacResolutionModes &) = delete;
std::vector<CGDisplayModeRef> Choices() const {
std::vector<CGDisplayModeRef> result;
if (!current || !modes_ ||
!ValidDisplayResolution(MacDisplayResolution(current)))
return result;
const double scale = double(CGDisplayModeGetPixelWidth(current)) /
CGDisplayModeGetWidth(current);
const double refresh = CGDisplayModeGetRefreshRate(current);
const auto rank = [&](CGDisplayModeRef mode) {
return std::make_tuple(
std::abs(double(CGDisplayModeGetPixelWidth(mode)) /
CGDisplayModeGetWidth(mode) -
scale),
std::abs(CGDisplayModeGetRefreshRate(mode) - refresh),
CGDisplayModeGetIODisplayModeID(mode) !=
CGDisplayModeGetIODisplayModeID(current));
};
std::map<std::pair<int, int>, CGDisplayModeRef> unique;
for (CFIndex index = 0; index < CFArrayGetCount(modes_); ++index) {
auto mode = static_cast<CGDisplayModeRef>(
const_cast<void *>(CFArrayGetValueAtIndex(modes_, index)));
const auto size = MacDisplayResolution(mode);
const auto flags = CGDisplayModeGetIOFlags(mode);
if (!ValidDisplayResolution(size) ||
!CGDisplayModeIsUsableForDesktopGUI(mode) ||
(flags & (kDisplayModeInterlacedFlag | kDisplayModeStretchedFlag)))
continue;
const auto key = std::make_pair(size.width, size.height);
auto found = unique.find(key);
// Prefer the current Retina density and then the closest refresh rate.
if (found == unique.end() || rank(mode) < rank(found->second))
unique[key] = mode;
}
for (auto it = unique.rbegin(); it != unique.rend(); ++it) {
result.push_back(it->second);
if (result.size() == kMaxDisplayResolutionModes)
break;
}
return result;
}
CGDisplayModeRef current;
private:
CFArrayRef modes_;
};
inline std::vector<DisplayResolutionInfo>
QueryMacDisplayResolutions(const std::vector<DisplayInfo> &displays) {
std::vector<DisplayResolutionInfo> result;
for (int index = 0;
index < int(displays.size()) && index < kMaxResolutionDisplays;
++index) {
DisplayResolutionInfo info;
info.display_id = index;
CGDirectDisplayID id = 0;
if (!ResolveMacResolutionDisplay(displays[index], id, info.display_key))
continue;
MacResolutionModes modes(id);
info.current = MacDisplayResolution(modes.current);
if (!ValidDisplayResolution(info.current))
continue;
for (auto mode : modes.Choices())
info.modes.push_back(MacDisplayResolution(mode));
result.push_back(std::move(info));
}
return result;
}
inline bool ApplyMacDisplayResolution(const std::vector<DisplayInfo> &displays,
const DisplayResolutionChange &change) {
if (change.display_id < 0 || change.display_id >= int(displays.size()) ||
!ValidDisplayResolution(change.resolution))
return false;
CGDirectDisplayID id = 0;
std::string key;
if (!ResolveMacResolutionDisplay(displays[change.display_id], id, key) ||
key != change.display_key)
return false;
MacResolutionModes modes(id);
for (auto mode : modes.Choices()) {
if (!(MacDisplayResolution(mode) == change.resolution))
continue;
if (MacDisplayResolution(modes.current) == change.resolution)
return true;
// A synchronous, application-lifetime change; never persist remote choices
// in the user's permanent display configuration.
if (CGDisplaySetDisplayMode(id, mode, nullptr) != kCGErrorSuccess)
return false;
CGDisplayModeRef applied = CGDisplayCopyDisplayMode(id);
const bool accepted = MacDisplayResolution(applied) == change.resolution;
if (applied)
CGDisplayModeRelease(applied);
if (!accepted)
CGDisplaySetDisplayMode(id, modes.current, nullptr);
return accepted;
}
return false;
}
inline bool SameMacResolutionMode(CGDisplayModeRef mode,
CGDisplayModeRef original) {
return mode && original &&
MacDisplayResolution(mode) == MacDisplayResolution(original) &&
CGDisplayModeGetPixelWidth(mode) ==
CGDisplayModeGetPixelWidth(original) &&
CGDisplayModeGetPixelHeight(mode) ==
CGDisplayModeGetPixelHeight(original) &&
CGDisplayModeGetRefreshRate(mode) ==
CGDisplayModeGetRefreshRate(original);
}
inline std::function<bool()>
CaptureMacDisplayResolution(const std::vector<DisplayInfo> &displays,
const DisplayResolutionChange &change) {
if (change.display_id < 0 || change.display_id >= int(displays.size()))
return {};
const auto display = displays[change.display_id];
CGDirectDisplayID id = 0;
std::string key;
if (!ResolveMacResolutionDisplay(display, id, key) ||
key != change.display_key)
return {};
// Retain the exact mode, including Retina pixel density and refresh rate.
std::shared_ptr<CGDisplayMode> original(CGDisplayCopyDisplayMode(id),
[](CGDisplayModeRef mode) {
if (mode)
CGDisplayModeRelease(mode);
});
if (!original)
return {};
return [display, key, original] {
CGDirectDisplayID id = 0;
std::string current_key;
if (!ResolveMacResolutionDisplay(display, id, current_key) ||
current_key != key)
return false;
CGDisplayModeRef current = CGDisplayCopyDisplayMode(id);
const bool unchanged = SameMacResolutionMode(current, original.get());
if (current)
CGDisplayModeRelease(current);
if (unchanged)
return true;
if (CGDisplaySetDisplayMode(id, original.get(), nullptr) != kCGErrorSuccess)
return false;
current = CGDisplayCopyDisplayMode(id);
const bool restored = SameMacResolutionMode(current, original.get());
if (current)
CGDisplayModeRelease(current);
return restored;
};
}
} // namespace crossdesk
#endif
@@ -180,7 +180,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
bool complete, SCStreamConfiguration* configuration,
NSError* error);
void RetryPrivacyFilter();
void PrivacyCoversChanged() { StartOrReconfigureCapturer(); }
void CaptureConfigurationChanged() { StartOrReconfigureCapturer(); }
void CaptureStopped(SCStream* source, NSError* error) {
{
std::lock_guard lock(lock_);
@@ -914,13 +914,23 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
DISPATCH_QUEUE_SERIAL);
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(privacyCoversChanged:)
name:@"CrossDeskPrivacyCoversChanged" object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(screenParametersChanged:)
name:NSApplicationDidChangeScreenParametersNotification object:nil];
}
return self;
}
- (void)privacyCoversChanged:(NSNotification*)notification {
std::lock_guard<std::mutex> lock(_capturer_lock);
if (_capturer) _capturer->PrivacyCoversChanged();
if (_capturer) _capturer->CaptureConfigurationChanged();
}
- (void)screenParametersChanged:(NSNotification*)notification {
// Defer until the mode switch has completed, then fetch fresh SCDisplay
// geometry and update the stream's pixel dimensions and input bounds.
dispatch_async(_completion_queue, ^{
std::lock_guard<std::mutex> lock(self->_capturer_lock);
if (self->_capturer) self->_capturer->CaptureConfigurationChanged();
});
}
- (void)configurationFinished:(SCStream*)stream generation:(uint64_t)generation
revision:(uint64_t)revision complete:(BOOL)complete
@@ -11,6 +11,7 @@
#include <algorithm>
#include <cmath>
#include <functional>
#include <map>
#include <string>
#include <vector>
@@ -105,6 +106,18 @@ inline bool ApplyWindowsResolutionMode(const wchar_t *device,
return false;
}
inline bool WindowsResolutionDeviceKey(const std::wstring &device,
std::string &key) {
DISPLAY_DEVICEW target{};
target.cb = sizeof(target);
if (!EnumDisplayDevicesW(device.c_str(), 0, &target,
EDD_GET_DEVICE_INTERFACE_NAME) ||
!target.DeviceID[0])
return false;
key = WideToUtf8(device) + "|" + WideToUtf8(target.DeviceID);
return true;
}
inline bool ResolveWindowsResolutionDisplay(const DisplayInfo &display,
std::wstring &device,
std::string &key) {
@@ -135,14 +148,55 @@ inline bool ResolveWindowsResolutionDisplay(const DisplayInfo &display,
if (device.empty())
return false;
}
DISPLAY_DEVICEW target{};
target.cb = sizeof(target);
if (!EnumDisplayDevicesW(device.c_str(), 0, &target,
EDD_GET_DEVICE_INTERFACE_NAME) ||
!target.DeviceID[0])
return false;
key = WideToUtf8(device) + "|" + WideToUtf8(target.DeviceID);
return true;
return WindowsResolutionDeviceKey(device, key);
}
inline bool SameWindowsResolutionMode(const DEVMODEW &mode,
const DEVMODEW &original) {
return mode.dmPelsWidth == original.dmPelsWidth &&
mode.dmPelsHeight == original.dmPelsHeight &&
mode.dmBitsPerPel == original.dmBitsPerPel &&
mode.dmDisplayFrequency == original.dmDisplayFrequency &&
mode.dmDisplayFlags == original.dmDisplayFlags &&
(!(original.dmFields & DM_DISPLAYORIENTATION) ||
mode.dmDisplayOrientation == original.dmDisplayOrientation) &&
(!(original.dmFields & DM_DISPLAYFIXEDOUTPUT) ||
mode.dmDisplayFixedOutput == original.dmDisplayFixedOutput);
}
inline std::function<bool()> CaptureWindowsDisplayResolution(
const std::vector<DisplayInfo> &displays,
const DisplayResolutionChange &change,
const WindowsResolutionApi &api = {}) {
if (change.display_id < 0 || change.display_id >= int(displays.size()))
return {};
std::wstring device;
std::string key;
if (!ResolveWindowsResolutionDisplay(displays[change.display_id], device, key) ||
key != change.display_key)
return {};
DEVMODEW original{};
original.dmSize = sizeof(original);
if (!api.enumerate(device.c_str(), ENUM_CURRENT_SETTINGS, &original)) return {};
return [device, key, original, api] {
std::string current_key;
// Resolve the native output, not a capture slot or stale HMONITOR. Never
// apply a saved mode to a different monitor plugged into the same output.
if (!WindowsResolutionDeviceKey(device, current_key) || current_key != key)
return false;
DEVMODEW current{};
current.dmSize = sizeof(current);
if (!api.enumerate(device.c_str(), ENUM_CURRENT_SETTINGS, &current)) return false;
if (SameWindowsResolutionMode(current, original)) return true;
auto mode = original;
if (api.change(device.c_str(), &mode, nullptr, CDS_TEST, nullptr) !=
DISP_CHANGE_SUCCESSFUL ||
api.change(device.c_str(), &mode, nullptr, 0, nullptr) !=
DISP_CHANGE_SUCCESSFUL)
return false;
return api.enumerate(device.c_str(), ENUM_CURRENT_SETTINGS, &current) &&
SameWindowsResolutionMode(current, original);
};
}
inline std::vector<DisplayResolutionInfo>
@@ -187,4 +241,4 @@ ApplyWindowsDisplayResolution(const std::vector<DisplayInfo> &displays,
} // namespace crossdesk
#endif
#endif