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) BOOL isDynamicResolutionSupported;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
- (instancetype)initWithDisplay:(CSDisplay *)display input:(nullable CSInput *)input NS_DESIGNATED_INITIALIZER;

View File

@ -29,11 +29,15 @@
#import "UTM-Swift.h"
@import CocoaSpiceRenderer;
static const NSInteger kResizeDebounceSecs = 1;
static const NSInteger kResizeTimeoutSecs = 5;
@interface VMDisplayMetalViewController ()
@property (nonatomic, nullable) CSMetalRenderer *renderer;
@property (nonatomic) CGFloat windowScaling;
@property (nonatomic) CGPoint windowOrigin;
@property (nonatomic, nullable) id debounceResize;
@property (nonatomic, nullable) id cancelResize;
@property (nonatomic) BOOL ignoreNextResize;
@end
@ -43,9 +47,6 @@
if (self = [super initWithNibName:nil bundle:nil]) {
self.vmDisplay = display;
self.vmInput = input;
self.windowScaling = 1.0;
self.windowOrigin = CGPointZero;
[self addObserver:self forKeyPath:@"vmDisplay.displaySize" options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
@ -128,11 +129,13 @@
[super viewWillDisappear:animated];
[self stopGCMouse];
[self.vmDisplay removeRenderer:self.renderer];
[self removeObserver:self forKeyPath:@"vmDisplay.displaySize"];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
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 {
@ -140,10 +143,12 @@
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
self.delegate.displayViewSize = [self convertSizeToNative:size];
[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 {
@ -161,8 +166,8 @@
[super enterLive];
self.prefersPointerLocked = YES;
self.view.window.isIndirectPointerTouchIgnored = YES;
if (self.delegate.qemuDisplayIsDynamicResolution) {
[self displayResize:self.view.bounds.size];
if (self.delegate.qemuDisplayIsDynamicResolution && self.isDynamicResolutionSupported) {
[self requestResolutionChangeToSize:self.view.bounds.size];
}
if (self.delegate.qemuHasClipboardSharing) {
[[UTMPasteboard generalPasteboard] requestPollingModeForObject:self];
@ -200,11 +205,21 @@
return size;
}
- (void)displayResize:(CGSize)size {
UTMLog(@"resizing to (%f, %f)", size.width, size.height);
size = [self convertSizeToNative:size];
CGRect bounds = CGRectMake(0, 0, size.width, size.height);
[self.vmDisplay requestResolution:bounds];
- (void)requestResolutionChangeToSize:(CGSize)size {
self.debounceResize = [self debounce:kResizeDebounceSecs context:self.debounceResize action:^{
UTMLog(@"DISPLAY: requesting resolution (%f, %f)", size.width, size.height);
CGSize newSize = [self convertSizeToNative:size];
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 {
@ -217,8 +232,6 @@
- (void)setDisplayScaling:(CGFloat)scaling origin:(CGPoint)origin {
self.vmDisplay.viewportOrigin = origin;
self.windowScaling = scaling;
self.windowOrigin = origin;
if (!self.delegate.qemuDisplayIsNativeResolution) {
scaling = CGPointToPixel(scaling);
}
@ -229,25 +242,67 @@
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"vmDisplay.displaySize"]) {
#if defined(TARGET_OS_VISION) && TARGET_OS_VISION
dispatch_async(dispatch_get_main_queue(), ^{
CGSize minSize = self.vmDisplay.displaySize;
if (self.delegate.qemuDisplayIsNativeResolution) {
minSize.width = CGPixelToPoint(minSize.width);
minSize.height = CGPixelToPoint(minSize.height);
}
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
UTMLog(@"DISPLAY: vmDisplay.displaySize changed");
if (self.cancelResize) {
[self debounce:0 context:self.cancelResize action:^{}];
self.cancelResize = nil;
}
self.debounceResize = [self debounce:kResizeDebounceSecs context:self.debounceResize action:^{
[self resizeWindowToDisplaySize];
}];
}
}
- (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

View File

@ -134,4 +134,15 @@ public extension VMDisplayViewController {
func integerForSetting(_ key: String) -> Int {
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
vc.setDisplayScaling(state.displayScale, origin: state.displayOrigin)
vc.isDynamicResolutionSupported = state.isDynamicResolutionSupported
}
case .serial(let serial, _):
if let vc = uiViewController as? VMDisplayTerminalViewController {

View File

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

View File

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

View File

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