display(visionOS): dynamic resolution from window resize
This commit is contained in:
parent
2538c20845
commit
5f7e11e161
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue