From 69d33ef8e93d80da98d551a0ed44b849b5997969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 10 Oct 2026 15:01:44 +0200 Subject: [PATCH] iOS: Resize in place on rotation instead of restarting graphics Both view controllers stopped the emu/render thread on every rotation and started it again, tearing down all of graphics. Since d0e45b94d9 NativeFrame resizes the graphics context and posts GPU_DISPLAY_RESIZED on iOS too, so a NativeResized() is all a size change needs, like Android now does. GLKView resizes its drawable by itself, and the Vulkan context recreates the swapchain, which MoltenVK sizes from the layer. The resize happens in viewDidLayoutSubviews, which UIKit runs at the final size as a rotation animation starts. Resizing when the rotation completed instead left half the screen black while it animated. This also covers window resizes, which matter more now that UIRequiresFullScreen is deprecated (iPadOS 26). NativeFrame sets the PSP_CoreParameter() size, so the main thread doesn't have to. Co-Authored-By: Claude Opus 5.5 --- ios/ViewController.mm | 18 ------------------ ios/ViewControllerCommon.mm | 23 +++++++++++++++++++---- ios/ViewControllerMetal.mm | 30 ------------------------------ 3 files changed, 19 insertions(+), 52 deletions(-) diff --git a/ios/ViewController.mm b/ios/ViewController.mm index 25ffc72be7..7eb06142eb 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -259,24 +259,6 @@ PPSSPPBaseViewController *sharedViewController; [(GLKView*)self.glView bindDrawable]; } -// Can't consolidate this yet. -- (void)viewWillTransitionToSize:(CGSize)size - withTransitionCoordinator:(id)coordinator { - [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; - - [self.view endEditing:YES]; // clears any input focus - - [coordinator animateAlongsideTransition:^(id context) { - NSLog(@"Rotating to size: %@", NSStringFromCGSize(size)); - } completion:^(id context) { - NSLog(@"Rotation finished"); - // Reinitialize graphics context to match new size - [self requestExitGLRenderLoop]; - [self updateResolutionWithView:self.view]; - [self runGLRenderLoop]; - }]; -} - @end void bindDefaultFBO() diff --git a/ios/ViewControllerCommon.mm b/ios/ViewControllerCommon.mm index 46e96cf891..783d0f7ffa 100644 --- a/ios/ViewControllerCommon.mm +++ b/ios/ViewControllerCommon.mm @@ -29,6 +29,7 @@ @implementation PPSSPPBaseViewController { UIScreenEdgePanGestureRecognizer *mBackGestureRecognizer; + CGSize lastLayoutSize_; } // Strange idiom for generating unique IDs (within the process, at least). @@ -523,6 +524,23 @@ extern float g_safeInsetBottom; [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; // The immersive mode setting is per-orientation, so the status bar may need to change along with the rotation. [self setNeedsStatusBarAppearanceUpdate]; + + [self.view endEditing:YES]; // clears any input focus +} + +// Rotations and window resizes land here. UIKit lays out at the final size at the start of a +// rotation animation, so resizing here (rather than when the rotation completes) keeps the +// backbuffer matching the view while it animates. +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + CGSize size = self.view.bounds.size; + if (size.width <= 0.0 || size.height <= 0.0 || CGSizeEqualToSize(size, lastLayoutSize_)) { + return; + } + lastLayoutSize_ = size; + INFO_LOG(Log::G3D, "Laid out at %dx%d", (int)size.width, (int)size.height); + // Resized in place: NativeFrame picks up the new size and lets the graphics context resize. + [self updateResolutionWithView:self.view]; } - (void)updateResolutionWithView:(UIView *)view { @@ -548,10 +566,7 @@ extern float g_safeInsetBottom; [view setContentScaleFactor:scale]; - // PSP native resize - PSP_CoreParameter().pixelWidth = g_display.pixel_xres; - PSP_CoreParameter().pixelHeight = g_display.pixel_yres; - + // NativeFrame updates the PSP_CoreParameter() size along with the rest. NativeResized(); NSLog(@"Updated display resolution: (%d, %d) @%.1fx", diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm index 6510cbe1d3..7caa1fecef 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -220,36 +220,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met INFO_LOG(Log::G3D, "viewDidDisappear"); } -- (void)viewWillLayoutSubviews { - [super viewWillLayoutSubviews]; - - // This is the first reliable place where self.view.bounds - // matches the forced orientation. - CGRect bounds = self.view.bounds; - - INFO_LOG(Log::G3D, "Correcting metal view layout: %dx%d", - (int)bounds.size.width, (int)bounds.size.height); - - // Update your Metal layer/viewport here if necessary -} - -- (void)viewWillTransitionToSize:(CGSize)size - withTransitionCoordinator:(id)coordinator { - [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; - - [self.view endEditing:YES]; // clears any input focus - - [coordinator animateAlongsideTransition:^(id context) { - NSLog(@"Rotating to size: %@", NSStringFromCGSize(size)); - } completion:^(id context) { - NSLog(@"Rotation finished"); - // Reinitialize graphics context to match new size - [self requestExitVulkanRenderLoop]; - [self updateResolutionWithView:self.view]; - [self runVulkanRenderLoop]; - }]; -} - @end @implementation PPSSPPMetalView