Files
ppsspp/ios/ExternalDisplaySceneDelegate.mm
Henrik RydgårdandClaude Opus 5.5 b642984755 Outputs: Decide on the TV mode by the running backend
iOS and Android checked the backend setting, which a change sets right
away while the restart prompt is still up. A TV connected then got an
output OpenGL can't draw to, and showed black instead of mirroring.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:04:15 +02:00

178 lines
5.9 KiB
Plaintext

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#import "ExternalDisplaySceneDelegate.h"
#import <QuartzCore/CAMetalLayer.h>
#include "Common/GPU/thin3d.h"
#include "Common/Log.h"
#include "Common/System/NativeApp.h"
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "Core/System.h"
static NSString *const PPSSPPExternalDisplayUpdateNotification = @"PPSSPPExternalDisplayUpdate";
// Set by the SceneDelegate. Before that, the config (and so the graphics backend) isn't known.
static bool g_launched = false;
@interface ExternalDisplayView : UIView
@end
@implementation ExternalDisplayView
+ (Class)layerClass {
return [CAMetalLayer class];
}
@end
// Adds the view's layer as a Game output once it has a size, and keeps the output's size up to date.
@interface ExternalDisplayViewController : UIViewController
@property (nonatomic) int outputId;
// A reference to the layer for the output's requests, which hold it unretained. Released once it's removed.
@property (nonatomic) void *retainedLayer;
@end
@implementation ExternalDisplayViewController {
int width_;
int height_;
}
- (void)loadView {
self.view = [[ExternalDisplayView alloc] init];
self.view.backgroundColor = UIColor.blackColor;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
UIScreen *screen = self.view.window.windowScene.screen;
const CGSize size = self.view.bounds.size;
const CGFloat scale = screen ? screen.nativeScale : 1.0;
self.view.contentScaleFactor = scale;
const int width = (int)(size.width * scale);
const int height = (int)(size.height * scale);
if (width <= 0 || height <= 0 || (width == width_ && height == height_)) {
return;
}
width_ = width;
height_ = height;
if (self.outputId) {
Native_ResizeOutput(self.outputId, width, height);
return;
}
Draw::SwapchainDesc desc{};
desc.winsys = WINDOWSYSTEM_METAL_EXT;
self.retainedLayer = (void *)CFBridgingRetain(self.view.layer);
desc.data1 = self.retainedLayer;
desc.width = width;
desc.height = height;
desc.tag = "ExternalDisplay";
self.outputId = Native_AddOutput(OutputRole::Game, desc);
INFO_LOG(Log::G3D, "External display: %dx%d, output %d", width, height, self.outputId);
}
@end
@interface ExternalDisplaySceneDelegate ()
@property (weak, nonatomic) UIWindowScene *windowScene;
@end
@implementation ExternalDisplaySceneDelegate
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
if (![scene isKindOfClass:[UIWindowScene class]]) {
return;
}
self.windowScene = (UIWindowScene *)scene;
INFO_LOG(Log::G3D, "External display connected");
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(update:) name:PPSSPPExternalDisplayUpdateNotification object:nil];
if (g_launched) {
[self attach];
}
}
- (void)update:(NSNotification *)notification {
// After a restart, the outputs are gone along with the old screen manager, and the backend may have changed.
// Or the setting did.
[self detach];
[self attach];
}
- (void)attach {
if (!self.windowScene || self.window) {
return;
}
// Without a window, iOS mirrors the phone.
// The running backend: the setting is already the new one while the restart prompt for a change is up.
if (GetGPUBackend() != GPUBackend::METAL && GetGPUBackend() != GPUBackend::VULKAN) {
INFO_LOG(Log::G3D, "External display: mirroring, as the backend can't present to it");
return;
}
if (g_Config.iExternalDisplay != (int)ExternalDisplayMode::SHOW_GAME) {
INFO_LOG(Log::G3D, "External display: mirroring, as set");
return;
}
#if !TARGET_OS_TV
for (UIScreenMode *mode in self.windowScene.screen.availableModes) {
INFO_LOG(Log::G3D, "External display mode: %dx%d%s", (int)mode.size.width, (int)mode.size.height, mode == self.windowScene.screen.currentMode ? " (current)" : "");
}
#endif
self.window = [[UIWindow alloc] initWithWindowScene:self.windowScene];
self.window.rootViewController = [[ExternalDisplayViewController alloc] init];
self.window.hidden = NO;
}
- (void)detach {
if (!self.window) {
return;
}
ExternalDisplayViewController *viewController = (ExternalDisplayViewController *)self.window.rootViewController;
if (viewController.outputId) {
// The window and its layer go now, but the add may still be queued, so our reference to the layer has to
// stay until the removal has been handled. Waited for off the main thread, which the render thread may
// be waiting on. If that times out, the layer is kept, as the add may still come.
const int outputId = viewController.outputId;
void *layer = viewController.retainedLayer;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
if (Native_RemoveOutputAndWait(outputId)) {
CFRelease(layer);
}
});
}
self.window = nil;
}
- (void)sceneDidDisconnect:(UIScene *)scene {
INFO_LOG(Log::G3D, "External display disconnected");
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self detach];
self.windowScene = nil;
}
@end
void ExternalDisplayNotifyLaunched() {
g_launched = true;
[[NSNotificationCenter defaultCenter] postNotificationName:PPSSPPExternalDisplayUpdateNotification object:nil];
}
void ExternalDisplaySettingChanged() {
if (g_launched) {
[[NSNotificationCenter defaultCenter] postNotificationName:PPSSPPExternalDisplayUpdateNotification object:nil];
}
}