display(visionOS): dynamic resolution from window resize

This commit is contained in:
osy 2024-02-21 10:21:34 -08:00
parent 2538c20845
commit 5f7e11e161
7 changed files with 118 additions and 36 deletions

View File

@ -42,6 +42,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) NSMutableArray<UIKeyCommand *> *mutableKeyCommands; @property (nonatomic, strong) NSMutableArray<UIKeyCommand *> *mutableKeyCommands;
@property (nonatomic) BOOL isDynamicResolutionSupported;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE; - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
- (instancetype)initWithDisplay:(CSDisplay *)display input:(nullable CSInput *)input NS_DESIGNATED_INITIALIZER; - (instancetype)initWithDisplay:(CSDisplay *)display input:(nullable CSInput *)input NS_DESIGNATED_INITIALIZER;

View File

@ -29,11 +29,15 @@
#import "UTM-Swift.h" #import "UTM-Swift.h"
@import CocoaSpiceRenderer; @import CocoaSpiceRenderer;
static const NSInteger kResizeDebounceSecs = 1;
static const NSInteger kResizeTimeoutSecs = 5;
@interface VMDisplayMetalViewController () @interface VMDisplayMetalViewController ()
@property (nonatomic, nullable) CSMetalRenderer *renderer; @property (nonatomic, nullable) CSMetalRenderer *renderer;
@property (nonatomic) CGFloat windowScaling; @property (nonatomic, nullable) id debounceResize;
@property (nonatomic) CGPoint windowOrigin; @property (nonatomic, nullable) id cancelResize;
@property (nonatomic) BOOL ignoreNextResize;
@end @end
@ -43,9 +47,6 @@
if (self = [super initWithNibName:nil bundle:nil]) { if (self = [super initWithNibName:nil bundle:nil]) {
self.vmDisplay = display; self.vmDisplay = display;
self.vmInput = input; self.vmInput = input;
self.windowScaling = 1.0;
self.windowOrigin = CGPointZero;
[self addObserver:self forKeyPath:@"vmDisplay.displaySize" options:NSKeyValueObservingOptionNew context:nil];
} }
return self; return self;
} }
@ -128,11 +129,13 @@
[super viewWillDisappear:animated]; [super viewWillDisappear:animated];
[self stopGCMouse]; [self stopGCMouse];
[self.vmDisplay removeRenderer:self.renderer]; [self.vmDisplay removeRenderer:self.renderer];
[self removeObserver:self forKeyPath:@"vmDisplay.displaySize"];
} }
- (void)viewDidAppear:(BOOL)animated { - (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated]; [super viewDidAppear:animated];
self.delegate.displayViewSize = [self convertSizeToNative:self.view.bounds.size]; self.delegate.displayViewSize = [self convertSizeToNative:self.view.bounds.size];
[self addObserver:self forKeyPath:@"vmDisplay.displaySize" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:nil];
} }
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator { - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
@ -140,10 +143,12 @@
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
self.delegate.displayViewSize = [self convertSizeToNative:size]; self.delegate.displayViewSize = [self convertSizeToNative:size];
[self.delegate display:self.vmDisplay didResizeTo:self.vmDisplay.displaySize]; [self.delegate display:self.vmDisplay didResizeTo:self.vmDisplay.displaySize];
if (self.delegate.qemuDisplayIsDynamicResolution && self.isDynamicResolutionSupported) {
if (!CGSizeEqualToSize(size, self.vmDisplay.displaySize)) {
[self requestResolutionChangeToSize:size];
}
}
}]; }];
if (self.delegate.qemuDisplayIsDynamicResolution) {
[self displayResize:size];
}
} }
- (void)enterSuspendedWithIsBusy:(BOOL)busy { - (void)enterSuspendedWithIsBusy:(BOOL)busy {
@ -161,8 +166,8 @@
[super enterLive]; [super enterLive];
self.prefersPointerLocked = YES; self.prefersPointerLocked = YES;
self.view.window.isIndirectPointerTouchIgnored = YES; self.view.window.isIndirectPointerTouchIgnored = YES;
if (self.delegate.qemuDisplayIsDynamicResolution) { if (self.delegate.qemuDisplayIsDynamicResolution && self.isDynamicResolutionSupported) {
[self displayResize:self.view.bounds.size]; [self requestResolutionChangeToSize:self.view.bounds.size];
} }
if (self.delegate.qemuHasClipboardSharing) { if (self.delegate.qemuHasClipboardSharing) {
[[UTMPasteboard generalPasteboard] requestPollingModeForObject:self]; [[UTMPasteboard generalPasteboard] requestPollingModeForObject:self];
@ -200,11 +205,21 @@
return size; return size;
} }
- (void)displayResize:(CGSize)size { - (void)requestResolutionChangeToSize:(CGSize)size {
UTMLog(@"resizing to (%f, %f)", size.width, size.height); self.debounceResize = [self debounce:kResizeDebounceSecs context:self.debounceResize action:^{
size = [self convertSizeToNative:size]; UTMLog(@"DISPLAY: requesting resolution (%f, %f)", size.width, size.height);
CGRect bounds = CGRectMake(0, 0, size.width, size.height); CGSize newSize = [self convertSizeToNative:size];
[self.vmDisplay requestResolution:bounds]; CGRect bounds = CGRectMake(0, 0, newSize.width, newSize.height);
self.debounceResize = nil;
#if defined(TARGET_OS_VISION) && TARGET_OS_VISION
self.cancelResize = [self debounce:kResizeTimeoutSecs context:self.cancelResize action:^{
self.cancelResize = nil;
UTMLog(@"DISPLAY: requesting resolution cancelled");
[self resizeWindowToDisplaySize];
}];
#endif
[self.vmDisplay requestResolution:bounds];
}];
} }
- (void)setVmDisplay:(CSDisplay *)display { - (void)setVmDisplay:(CSDisplay *)display {
@ -217,8 +232,6 @@
- (void)setDisplayScaling:(CGFloat)scaling origin:(CGPoint)origin { - (void)setDisplayScaling:(CGFloat)scaling origin:(CGPoint)origin {
self.vmDisplay.viewportOrigin = origin; self.vmDisplay.viewportOrigin = origin;
self.windowScaling = scaling;
self.windowOrigin = origin;
if (!self.delegate.qemuDisplayIsNativeResolution) { if (!self.delegate.qemuDisplayIsNativeResolution) {
scaling = CGPointToPixel(scaling); scaling = CGPointToPixel(scaling);
} }
@ -229,25 +242,67 @@
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"vmDisplay.displaySize"]) { if ([keyPath isEqualToString:@"vmDisplay.displaySize"]) {
#if defined(TARGET_OS_VISION) && TARGET_OS_VISION UTMLog(@"DISPLAY: vmDisplay.displaySize changed");
dispatch_async(dispatch_get_main_queue(), ^{ if (self.cancelResize) {
CGSize minSize = self.vmDisplay.displaySize; [self debounce:0 context:self.cancelResize action:^{}];
if (self.delegate.qemuDisplayIsNativeResolution) { self.cancelResize = nil;
minSize.width = CGPixelToPoint(minSize.width); }
minSize.height = CGPixelToPoint(minSize.height); self.debounceResize = [self debounce:kResizeDebounceSecs context:self.debounceResize action:^{
} [self resizeWindowToDisplaySize];
CGSize displaySize = CGSizeMake(minSize.width * self.windowScaling, minSize.height * self.windowScaling); }];
CGSize maxSize = CGSizeMake(UIProposedSceneSizeNoPreference, UIProposedSceneSizeNoPreference);
UIWindowSceneGeometryPreferencesVision *geoPref = [[UIWindowSceneGeometryPreferencesVision alloc] initWithSize:displaySize];
geoPref.minimumSize = minSize;
geoPref.maximumSize = maxSize;
geoPref.resizingRestrictions = UIWindowSceneResizingRestrictionsUniform;
[self.view.window.windowScene requestGeometryUpdateWithPreferences:geoPref errorHandler:nil];
});
#else
[self.delegate display:self.vmDisplay didResizeTo:self.vmDisplay.displaySize];
#endif
} }
} }
- (void)setIsDynamicResolutionSupported:(BOOL)isDynamicResolutionSupported {
if (_isDynamicResolutionSupported != isDynamicResolutionSupported) {
_isDynamicResolutionSupported = isDynamicResolutionSupported;
UTMLog(@"DISPLAY: isDynamicResolutionSupported = %d", isDynamicResolutionSupported);
if (self.delegate.qemuDisplayIsDynamicResolution) {
if (isDynamicResolutionSupported) {
[self requestResolutionChangeToSize:self.view.bounds.size];
} else {
[self resizeWindowToDisplaySize];
}
}
}
}
- (void)resizeWindowToDisplaySize {
CGSize displaySize = self.vmDisplay.displaySize;
UTMLog(@"DISPLAY: request window resize to (%f, %f)", displaySize.width, displaySize.height);
#if defined(TARGET_OS_VISION) && TARGET_OS_VISION
CGSize minSize = displaySize;
if (self.delegate.qemuDisplayIsNativeResolution) {
minSize.width = CGPixelToPoint(minSize.width);
minSize.height = CGPixelToPoint(minSize.height);
}
CGSize maxSize = CGSizeMake(UIProposedSceneSizeNoPreference, UIProposedSceneSizeNoPreference);
UIWindowSceneGeometryPreferencesVision *geoPref = [[UIWindowSceneGeometryPreferencesVision alloc] initWithSize:minSize];
if (self.delegate.qemuDisplayIsDynamicResolution && self.isDynamicResolutionSupported) {
geoPref.minimumSize = CGSizeMake(800, 600);
geoPref.maximumSize = maxSize;
geoPref.resizingRestrictions = UIWindowSceneResizingRestrictionsFreeform;
} else {
geoPref.minimumSize = minSize;
geoPref.maximumSize = maxSize;
geoPref.resizingRestrictions = UIWindowSceneResizingRestrictionsUniform;
}
dispatch_async(dispatch_get_main_queue(), ^{
CGSize currentViewSize = self.view.bounds.size;
UTMLog(@"DISPLAY: old view size = (%f, %f)", currentViewSize.width, currentViewSize.height);
if (CGSizeEqualToSize(minSize, currentViewSize)) {
// since `-viewWillTransitionToSize:withTransitionCoordinator:` is not called
self.delegate.displayViewSize = [self convertSizeToNative:currentViewSize];
[self.delegate display:self.vmDisplay didResizeTo:displaySize];
}
[self.view.window.windowScene requestGeometryUpdateWithPreferences:geoPref errorHandler:nil];
});
#else
if (CGSizeEqualToSize(displaySize, CGSizeZero)) {
return;
}
[self.delegate display:self.vmDisplay didResizeTo:displaySize];
#endif
}
@end @end

View File

@ -134,4 +134,15 @@ public extension VMDisplayViewController {
func integerForSetting(_ key: String) -> Int { func integerForSetting(_ key: String) -> Int {
return UserDefaults.standard.integer(forKey: key) return UserDefaults.standard.integer(forKey: key)
} }
@discardableResult
func debounce(_ delaySeconds: Int, context: Any? = nil, action: @escaping () -> Void) -> Any {
if context != nil {
let previous = context as! DispatchWorkItem
previous.cancel()
}
let item = DispatchWorkItem(block: action)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(delaySeconds), execute: item)
return item
}
} }

View File

@ -190,6 +190,7 @@ struct VMDisplayHostedView: UIViewControllerRepresentable {
} }
// some obscure SwiftUI error means we cannot refer to Coordinator's state binding // some obscure SwiftUI error means we cannot refer to Coordinator's state binding
vc.setDisplayScaling(state.displayScale, origin: state.displayOrigin) vc.setDisplayScaling(state.displayScale, origin: state.displayOrigin)
vc.isDynamicResolutionSupported = state.isDynamicResolutionSupported
} }
case .serial(let serial, _): case .serial(let serial, _):
if let vc = uiViewController as? VMDisplayTerminalViewController { if let vc = uiViewController as? VMDisplayTerminalViewController {

View File

@ -78,7 +78,9 @@ import SwiftUI
@Published var externalWindowBinding: Binding<VMWindowState>? @Published var externalWindowBinding: Binding<VMWindowState>?
@Published var hasShownMemoryWarning: Bool = false @Published var hasShownMemoryWarning: Bool = false
@Published var isDynamicResolutionSupported: Bool = false
private var hasAutosave: Bool = false private var hasAutosave: Bool = false
init(for vm: any UTMSpiceVirtualMachine) { init(for vm: any UTMSpiceVirtualMachine) {
@ -291,6 +293,12 @@ extension VMSessionState: UTMSpiceIODelegate {
} }
} }
#endif #endif
nonisolated func spiceDynamicResolutionSupportDidChange(_ supported: Bool) {
Task { @MainActor in
isDynamicResolutionSupported = supported
}
}
} }
#if WITH_USB #if WITH_USB

View File

@ -71,6 +71,8 @@ struct VMWindowState: Identifiable {
var isRunning: Bool = false var isRunning: Bool = false
var alert: Alert? var alert: Alert?
var isDynamicResolutionSupported: Bool = false
} }
// MARK: - VM action alerts // MARK: - VM action alerts

View File

@ -171,6 +171,9 @@ struct VMWindowView: View {
.onChange(of: session.vmState) { [oldValue = session.vmState] newValue in .onChange(of: session.vmState) { [oldValue = session.vmState] newValue in
vmStateUpdated(from: oldValue, to: newValue) vmStateUpdated(from: oldValue, to: newValue)
} }
.onChange(of: session.isDynamicResolutionSupported) { newValue in
state.isDynamicResolutionSupported = newValue
}
.onReceive(keyboardDidShowNotification) { _ in .onReceive(keyboardDidShowNotification) { _ in
state.isKeyboardShown = true state.isKeyboardShown = true
state.isKeyboardRequested = true state.isKeyboardRequested = true