mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-01 03:57:17 +01:00
An external display (like an Apple TV over AirPlay) becomes a Game output, when the backend is Metal. The game is shown there full screen, and the phone shows the touch controls with "Playing on TV" in place of the game. The menus stay on the phone. With other backends, iOS keeps mirroring. Up to iOS 26 the external display scene comes from the Info.plist role. iOS 27 ignores that, and only offers the scene to apps that register a scene accessory for it, so we do that too. The registration is withdrawn when the view controller shuts down, so it follows a backend change, and a restart now goes to the app's scene rather than the first one. An Apple TV 4K only offers 3840x2160 over AirPlay, so the stream stays 4K, but we render at 1920x1080 and let the layer scale it up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
162 lines
5.2 KiB
Plaintext
162 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 <algorithm>
|
|
|
|
#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 PPSSPPDidLaunchNotification = @"PPSSPPDidLaunch";
|
|
|
|
// 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;
|
|
CGFloat scale = screen ? screen.nativeScale : 1.0;
|
|
// Render no more than 1080p (a 4K Apple TV comes in at 3840x2160). The layer is scaled up to fill.
|
|
const CGFloat longSide = std::max(size.width, size.height);
|
|
const CGFloat shortSide = std::min(size.width, size.height);
|
|
if (longSide > 0 && shortSide > 0) {
|
|
scale = std::min(scale, std::min(1920.0 / longSide, 1080.0 / shortSide));
|
|
}
|
|
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(didLaunch:) name:PPSSPPDidLaunchNotification object:nil];
|
|
if (g_launched) {
|
|
[self attach];
|
|
}
|
|
}
|
|
|
|
- (void)didLaunch:(NSNotification *)notification {
|
|
// After a restart, the outputs are gone along with the old screen manager, and the backend may have changed.
|
|
[self detach];
|
|
[self attach];
|
|
}
|
|
|
|
- (void)attach {
|
|
if (!self.windowScene || self.window) {
|
|
return;
|
|
}
|
|
// Only Metal can present to a second display so far. Without a window, iOS mirrors the phone.
|
|
if (g_Config.iGPUBackend != (int)GPUBackend::METAL) {
|
|
INFO_LOG(Log::G3D, "External display: mirroring, as the backend isn't Metal");
|
|
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:PPSSPPDidLaunchNotification object:nil];
|
|
}
|