UTM/Platform/iOS/Display/VMDisplayMetalViewController.m

313 lines
11 KiB
Objective-C

//
// Copyright © 2019 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#import "VMDisplayMetalViewController.h"
#import "VMDisplayMetalViewController+Private.h"
#import "VMDisplayMetalViewController+Keyboard.h"
#import "VMDisplayMetalViewController+Touch.h"
#import "VMDisplayMetalViewController+Pointer.h"
#if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION
#import "VMDisplayMetalViewController+Pencil.h"
#endif
#import "VMDisplayMetalViewController+Gamepad.h"
#import "VMKeyboardView.h"
#import "UTMLogging.h"
#import "CSDisplay.h"
#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, nullable) id debounceResize;
@property (nonatomic, nullable) id cancelResize;
@property (nonatomic) BOOL ignoreNextResize;
@end
@implementation VMDisplayMetalViewController
- (instancetype)initWithDisplay:(CSDisplay *)display input:(CSInput *)input {
if (self = [super initWithNibName:nil bundle:nil]) {
self.vmDisplay = display;
self.vmInput = input;
}
return self;
}
- (void)loadView {
[super loadView];
self.keyboardView = [[VMKeyboardView alloc] initWithFrame:CGRectZero];
self.mtkView = [[CSMTKView alloc] initWithFrame:CGRectZero];
self.keyboardView.delegate = self;
[self.view insertSubview:self.keyboardView atIndex:0];
[self.view insertSubview:self.mtkView atIndex:1];
[self.mtkView bindFrameToSuperviewBounds];
[self loadInputAccessory];
}
- (void)loadInputAccessory {
UINib *nib = [UINib nibWithNibName:@"VMDisplayMetalViewInputAccessory" bundle:nil];
[nib instantiateWithOwner:self options:nil];
}
- (BOOL)serverModeCursor {
return self.vmInput.serverModeCursor;
}
- (void)viewDidLoad {
[super viewDidLoad];
// set up software keyboard
self.keyboardView.inputAccessoryView = self.inputAccessoryView;
// Set the view to use the default device
self.mtkView.frame = self.view.bounds;
self.mtkView.device = MTLCreateSystemDefaultDevice();
if (!self.mtkView.device) {
UTMLog(@"Metal is not supported on this device");
return;
}
self.renderer = [[CSMetalRenderer alloc] initWithMetalKitView:self.mtkView];
if (!self.renderer) {
UTMLog(@"Renderer failed initialization");
return;
}
// Initialize our renderer with the view size
if ([self integerForSetting:@"QEMURendererFPSLimit"] > 0) {
self.mtkView.preferredFramesPerSecond = [self integerForSetting:@"QEMURendererFPSLimit"];
}
[self.renderer changeUpscaler:self.delegate.qemuDisplayUpscaler
downscaler:self.delegate.qemuDisplayDownscaler];
self.mtkView.delegate = self.renderer;
[self initTouch];
[self initGamepad];
// Pointing device support on iPadOS 13.4 GM or later
if (@available(iOS 13.4, *)) {
// Betas of iPadOS 13.4 did not include this API, that's why I check if the class exists
if (NSClassFromString(@"UIPointerInteraction") != nil) {
[self initPointerInteraction];
}
}
#if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION
// Apple Pencil 2 double tap support on iOS 12.1+
if (@available(iOS 12.1, *)) {
[self initPencilInteraction];
}
#endif
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.prefersHomeIndicatorAutoHidden = YES;
#if !TARGET_OS_VISION
[self startGCMouse];
#endif
[self.vmDisplay addRenderer:self.renderer];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
#if !TARGET_OS_VISION
[self stopGCMouse];
#endif
[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 {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[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];
}
}
}];
}
- (void)enterSuspendedWithIsBusy:(BOOL)busy {
[super enterSuspendedWithIsBusy:busy];
self.prefersPointerLocked = NO;
self.view.window.isIndirectPointerTouchIgnored = NO;
if (!busy) {
if (self.delegate.qemuHasClipboardSharing) {
[[UTMPasteboard generalPasteboard] releasePollingModeForObject:self];
}
}
}
- (void)enterLive {
[super enterLive];
self.prefersPointerLocked = YES;
self.view.window.isIndirectPointerTouchIgnored = YES;
if (self.delegate.qemuDisplayIsDynamicResolution && self.isDynamicResolutionSupported) {
[self requestResolutionChangeToSize:self.view.bounds.size];
}
if (self.delegate.qemuHasClipboardSharing) {
[[UTMPasteboard generalPasteboard] requestPollingModeForObject:self];
}
}
#pragma mark - Key handling
- (void)showKeyboard {
[super showKeyboard];
[self.keyboardView becomeFirstResponder];
}
- (void)hideKeyboard {
[super hideKeyboard];
[self.keyboardView resignFirstResponder];
}
- (void)sendExtendedKey:(CSInputKey)type code:(int)code {
if ((code & 0xFF00) == 0xE000) {
code = 0x100 | (code & 0xFF);
} else if (code >= 0x100) {
UTMLog(@"warning: ignored invalid keycode 0x%x", code);
}
[self.vmInput sendKey:type code:code];
}
#pragma mark - Resizing
- (CGSize)convertSizeToNative:(CGSize)size {
if (self.delegate.qemuDisplayIsNativeResolution) {
size.width = CGPointToPixel(size.width);
size.height = CGPointToPixel(size.height);
}
return size;
}
- (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 {
if (self.renderer) {
[_vmDisplay removeRenderer:self.renderer];
_vmDisplay = display;
[display addRenderer:self.renderer];
}
}
- (void)setDisplayScaling:(CGFloat)scaling origin:(CGPoint)origin {
self.vmDisplay.viewportOrigin = origin;
if (!self.delegate.qemuDisplayIsNativeResolution) {
scaling = CGPointToPixel(scaling);
}
if (scaling) { // cannot be zero
self.vmDisplay.viewportScale = scaling;
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"vmDisplay.displaySize"]) {
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