Files
ppsspp/ios/ExternalDisplaySceneDelegate.mm
T
Henrik RydgårdandClaude Opus 5.5 04ae3fea5b External display setting: Show the game, or mirror the screen
Graphics > "External display" on iOS and Android. "Show the game" (the
default) is the TV mode, "Mirror the screen" leaves the TV to the system.
A change applies right away. It's disabled on backends that can't draw to
a second swapchain.

On Android 12 and up, a second screen that's part of the device (like a
dual-screen handheld's) isn't used, so the game doesn't move there by
itself.

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

165 lines
5.2 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"
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;
@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;
desc.data1 = (__bridge void *)self.view.layer;
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.
if (g_Config.iGPUBackend != (int)GPUBackend::METAL && g_Config.iGPUBackend != (int)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 swapchain keeps the layer alive until the removal has gone through.
Native_RemoveOutput(viewController.outputId);
}
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];
}
}