mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-12 17:19:28 +01:00
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>
This commit is contained in:
1 parent
5fbd134a7d
commit
04ae3fea5b
11 files changed
+84
-16
No files matched your search
@@ -280,6 +280,7 @@ enum class SystemNotification {
|
||||
CONFIG_LOADED,
|
||||
BEFORE_CONFIG_SAVE_ON_EXIT,
|
||||
TOP_LEVEL_CHANGED, // NativeIsAtTopLevel() changed. From the render thread.
|
||||
EXTERNAL_DISPLAY_CHANGED, // g_Config.iExternalDisplay changed.
|
||||
};
|
||||
|
||||
// I guess it's not super great architecturally to centralize this, since it's not general - but same with a lot of
|
||||
|
||||
@@ -749,6 +749,7 @@ static int DefaultAnalogAltSpeedPercent() {
|
||||
static constexpr ConfigSetting graphicsSettings[] = {
|
||||
ConfigSetting("iShowStatusFlags", SETTING(g_Config, iShowStatusFlags), 0, CfgFlag::PER_GAME),
|
||||
ConfigSetting("GraphicsBackend", SETTING(g_Config, iGPUBackend), &DefaultGPUBackend, &GPUBackendTranslator::To, &GPUBackendTranslator::From, CfgFlag::DEFAULT | CfgFlag::REPORT),
|
||||
ConfigSetting("ExternalDisplay", SETTING(g_Config, iExternalDisplay), (int)ExternalDisplayMode::SHOW_GAME, CfgFlag::DEFAULT),
|
||||
#if PPSSPP_PLATFORM(ANDROID) && PPSSPP_ARCH(ARM64)
|
||||
ConfigSetting("CustomDriver", SETTING(g_Config, sCustomDriver), "", CfgFlag::DEFAULT),
|
||||
#endif
|
||||
|
||||
@@ -288,6 +288,7 @@ public:
|
||||
|
||||
// GFX
|
||||
int iGPUBackend;
|
||||
int iExternalDisplay; // ExternalDisplayMode
|
||||
std::string sCustomDriver;
|
||||
std::string sFailedGPUBackends; // NOT stored in ppsspp.ini anymore!
|
||||
std::string sDisabledGPUBackends;
|
||||
|
||||
@@ -135,6 +135,12 @@ enum class GPUBackend {
|
||||
METAL = 4,
|
||||
};
|
||||
|
||||
// What an external display (a TV) shows, where the platform lets us choose. See docs/multi-swapchain.md.
|
||||
enum class ExternalDisplayMode {
|
||||
SHOW_GAME = 0, // The game, with the controls and the menus on the device. Needs Metal or Vulkan.
|
||||
MIRROR = 1,
|
||||
};
|
||||
|
||||
enum class DepthRasterMode {
|
||||
DEFAULT = 0,
|
||||
LOW_QUALITY = 1,
|
||||
|
||||
@@ -328,6 +328,21 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if (PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(TVOS)) || PPSSPP_PLATFORM(ANDROID)
|
||||
// Showing the game there needs a second swapchain. Otherwise the system mirrors the screen.
|
||||
static const char *externalDisplayModes[] = { "Show the game", "Mirror the screen" };
|
||||
PopupMultiChoice *externalDisplayChoice = graphicsSettings->Add(new PopupMultiChoice(&g_Config.iExternalDisplay, gr->T("External display"), externalDisplayModes, 0, ARRAY_SIZE(externalDisplayModes), I18NCat::GRAPHICS, screenManager()));
|
||||
externalDisplayChoice->SetEnabled(draw->GetDeviceCaps().multipleSwapchains);
|
||||
// Picking the current value again would rebuild the TV's output for nothing (iOS starts over on a change).
|
||||
externalDisplayChoice->OnChoice.Add([previous = g_Config.iExternalDisplay](UI::EventParams &e) mutable {
|
||||
if (g_Config.iExternalDisplay == previous) {
|
||||
return;
|
||||
}
|
||||
previous = g_Config.iExternalDisplay;
|
||||
System_Notify(SystemNotification::EXTERNAL_DISPLAY_CHANGED);
|
||||
});
|
||||
#endif
|
||||
|
||||
// Backends that don't allow a device choice will only expose one device.
|
||||
if (draw->GetDeviceList().size() > 1) {
|
||||
std::string *deviceNameSetting = GPUDeviceNameSetting();
|
||||
|
||||
@@ -593,8 +593,8 @@ static std::string QueryConfig(std::string_view query) {
|
||||
} else if (query == "audioMixWithOthers") {
|
||||
return g_Config.bAudioMixWithOthers ? "1" : "0";
|
||||
} else if (query == "externalDisplayOutput") {
|
||||
// Whether the game can go to a second display, see ExternalDisplayPresentation.java.
|
||||
return g_Config.iGPUBackend == (int)GPUBackend::VULKAN ? "1" : "0";
|
||||
// Whether the game goes to a second display, see ExternalDisplayPresentation.java.
|
||||
return g_Config.iGPUBackend == (int)GPUBackend::VULKAN && g_Config.iExternalDisplay == (int)ExternalDisplayMode::SHOW_GAME ? "1" : "0";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
@@ -1032,6 +1032,9 @@ void System_Notify(SystemNotification notification) {
|
||||
case SystemNotification::SUSTAINED_PERF_CHANGE:
|
||||
PushCommand("sustainedPerfMode", "");
|
||||
break;
|
||||
case SystemNotification::EXTERNAL_DISPLAY_CHANGED:
|
||||
PushCommand("externalDisplay", "");
|
||||
break;
|
||||
case SystemNotification::TEST_JAVA_EXCEPTION:
|
||||
PushCommand("testException", "This is a test exception");
|
||||
break;
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.hardware.display.DeviceProductInfo;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.media.AudioManager;
|
||||
@@ -121,6 +122,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
|
||||
// The game on a second display, while there's one (and the backend can draw to it) and we're started.
|
||||
private ExternalDisplayPresentation externalDisplayPresentation;
|
||||
private boolean externalDisplayWatched = false;
|
||||
private final DisplayManager.DisplayListener displayListener = new DisplayManager.DisplayListener() {
|
||||
@Override
|
||||
public void onDisplayAdded(int displayId) {
|
||||
@@ -966,15 +968,17 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
updateSustainedPerformanceMode();
|
||||
}
|
||||
|
||||
// Shows the game on a presentation display (like a TV on HDMI), if there's one and the backend can draw
|
||||
// to a second surface (Vulkan). Otherwise Android mirrors the phone there.
|
||||
// Shows the game on a presentation display (like a TV on HDMI), if there's one, the backend can draw
|
||||
// to a second surface (Vulkan) and the setting says so. Otherwise Android mirrors the phone there.
|
||||
private void updateExternalDisplay() {
|
||||
Display display = null;
|
||||
if ("1".equals(NativeApp.queryConfig("externalDisplayOutput"))) {
|
||||
if (externalDisplayWatched && "1".equals(NativeApp.queryConfig("externalDisplayOutput"))) {
|
||||
DisplayManager displayManager = (DisplayManager)getSystemService(Context.DISPLAY_SERVICE);
|
||||
Display[] displays = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
|
||||
if (displays.length > 0) {
|
||||
display = displays[0];
|
||||
for (Display candidate : displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)) {
|
||||
if (!isBuiltInDisplay(candidate)) {
|
||||
display = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (externalDisplayPresentation != null && (display == null || externalDisplayPresentation.getDisplay().getDisplayId() != display.getDisplayId())) {
|
||||
@@ -993,6 +997,16 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
}
|
||||
}
|
||||
|
||||
// A second screen that's part of the device (a dual-screen handheld's, a cover screen) isn't a TV, and
|
||||
// the game shouldn't move there by itself. Only Android 12 and up can tell.
|
||||
private static boolean isBuiltInDisplay(Display display) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
return false;
|
||||
}
|
||||
DeviceProductInfo info = display.getDeviceProductInfo();
|
||||
return info != null && info.getConnectionToSinkType() == DeviceProductInfo.CONNECTION_TO_SINK_BUILT_IN;
|
||||
}
|
||||
|
||||
private void dismissExternalDisplay() {
|
||||
if (externalDisplayPresentation != null) {
|
||||
externalDisplayPresentation.dismiss();
|
||||
@@ -1142,6 +1156,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
}
|
||||
DisplayManager displayManager = (DisplayManager)getSystemService(Context.DISPLAY_SERVICE);
|
||||
displayManager.registerDisplayListener(displayListener, null);
|
||||
externalDisplayWatched = true;
|
||||
updateExternalDisplay();
|
||||
}
|
||||
|
||||
@@ -1164,6 +1179,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
// Before the render thread stops, so it can let go of the second display's surface.
|
||||
DisplayManager displayManager = (DisplayManager)getSystemService(Context.DISPLAY_SERVICE);
|
||||
displayManager.unregisterDisplayListener(displayListener);
|
||||
externalDisplayWatched = false;
|
||||
dismissExternalDisplay();
|
||||
|
||||
// Normally surfaceDestroyed has done this already, but not every device destroys the surface of a
|
||||
@@ -1876,6 +1892,9 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
} else if (command.equals("sustainedPerfMode")) {
|
||||
updateSustainedPerformanceMode();
|
||||
return true;
|
||||
} else if (command.equals("externalDisplay")) {
|
||||
updateExternalDisplay();
|
||||
return true;
|
||||
} else if (command.equals("immersive")) {
|
||||
updateSystemUiVisibility();
|
||||
return true;
|
||||
|
||||
@@ -269,8 +269,9 @@ can be tested on its own.
|
||||
the main thread waits for that (the same handshake as `requestExitVulkanRenderLoop`). Only then does
|
||||
it remove the window. Removing the window should give the TV back to system mirroring. **Verify on
|
||||
the device:** if iOS doesn't resume mirroring, we need a `Mirror` output for the menus instead.
|
||||
- **Settings:** "When a TV is connected: Show the game on the TV (default) / Mirror the screen". The
|
||||
pause menu gets a toggle for the current session.
|
||||
- **Setting:** Graphics → "External display": "Show the game" (the default) or "Mirror the screen"
|
||||
(`iExternalDisplay`, iOS and Android). It's disabled on backends without `multipleSwapchains`, and a
|
||||
change applies right away (`SystemNotification::EXTERNAL_DISPLAY_CHANGED`).
|
||||
- **Out of scope:** iPad Stage Manager's interactive extended displays (role `Application`), and
|
||||
tvOS.
|
||||
- **AirPlay latency:** this is the same video stream as mirroring, so the same lag. Input stays local,
|
||||
@@ -283,8 +284,10 @@ can be tested on its own.
|
||||
(Miracast, like Samsung's Smart View), and Developer options → "Simulate secondary displays".
|
||||
Casting to a Chromecast or an Android TV is mirroring only.
|
||||
- **Showing it:** while it's started, the activity watches `DisplayManager` and, when there's a
|
||||
`DISPLAY_CATEGORY_PRESENTATION` display and the backend is Vulkan (`queryConfig("externalDisplayOutput")`),
|
||||
shows an `ExternalDisplayPresentation` there. That's a `Presentation` with a `SurfaceView`, whose
|
||||
`DISPLAY_CATEGORY_PRESENTATION` display, the backend is Vulkan and the setting says "Show the game"
|
||||
(`queryConfig("externalDisplayOutput")`), shows an `ExternalDisplayPresentation` there. Displays that
|
||||
are part of the device (a dual-screen handheld's second screen) are skipped, where Android can tell
|
||||
(12 and up, `DeviceProductInfo.CONNECTION_TO_SINK_BUILT_IN`). That's a `Presentation` with a `SurfaceView`, whose
|
||||
surface callbacks add, resize and remove a `Game` output through JNI. Otherwise Android mirrors.
|
||||
A backend change recreates the activity, which checks again.
|
||||
- **Removing:** the surface mustn't be touched once `surfaceDestroyed` returns, so it removes the output
|
||||
|
||||
@@ -30,3 +30,5 @@
|
||||
|
||||
// Called by the app's SceneDelegate when PPSSPP has (re)started, and the config is loaded.
|
||||
void ExternalDisplayNotifyLaunched();
|
||||
// g_Config.iExternalDisplay changed. On the main thread.
|
||||
void ExternalDisplaySettingChanged();
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "Core/Config.h"
|
||||
#include "Core/ConfigValues.h"
|
||||
|
||||
static NSString *const PPSSPPDidLaunchNotification = @"PPSSPPDidLaunch";
|
||||
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;
|
||||
@@ -95,14 +95,15 @@ static bool g_launched = false;
|
||||
}
|
||||
self.windowScene = (UIWindowScene *)scene;
|
||||
INFO_LOG(Log::G3D, "External display connected");
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didLaunch:) name:PPSSPPDidLaunchNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(update:) name:PPSSPPExternalDisplayUpdateNotification object:nil];
|
||||
if (g_launched) {
|
||||
[self attach];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didLaunch:(NSNotification *)notification {
|
||||
- (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];
|
||||
}
|
||||
@@ -116,6 +117,10 @@ static bool g_launched = false;
|
||||
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)" : "");
|
||||
@@ -149,5 +154,11 @@ static bool g_launched = false;
|
||||
|
||||
void ExternalDisplayNotifyLaunched() {
|
||||
g_launched = true;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PPSSPPDidLaunchNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PPSSPPExternalDisplayUpdateNotification object:nil];
|
||||
}
|
||||
|
||||
void ExternalDisplaySettingChanged() {
|
||||
if (g_launched) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PPSSPPExternalDisplayUpdateNotification object:nil];
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "ViewController.h"
|
||||
#import "ExternalDisplaySceneDelegate.h"
|
||||
#import "iOSCoreAudio.h"
|
||||
#import "IAPManager.h"
|
||||
#import "SceneDelegate.h"
|
||||
@@ -444,6 +445,11 @@ void System_Notify(SystemNotification notification) {
|
||||
}
|
||||
});
|
||||
break;
|
||||
case SystemNotification::EXTERNAL_DISPLAY_CHANGED:
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
ExternalDisplaySettingChanged();
|
||||
});
|
||||
break;
|
||||
case SystemNotification::IMMERSIVE_MODE_CHANGE:
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (sharedViewController) {
|
||||
|
||||
Reference in new issue
Block a user