From d491fc90cf3367f4d93298fdfc73fc0ff386ff23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 22 Oct 2026 13:06:50 +0200 Subject: [PATCH] Secondary outputs, and --game-window on Mac to show the game in a second window The ScreenManager keeps a list of secondary outputs, each with its own swapchain, recreated after a lost device. Platform code adds, resizes and removes them from any thread through Native_AddOutput() and friends, which take effect at the start of the next frame. Each frame, every output is drawn by the topmost screen that claims it (Screen::renderOutput). EmuScreen claims Game outputs: it presents the game there, laid out for that output, and leaves it out of the backbuffer, where the touch controls and the menus stay. Not in non-buffered mode, which draws straight into the backbuffer. --game-window opens such an output on Mac, with Metal. It's a smoke test for multiple swapchains, and a stand-in for an AirPlay display. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/System/NativeApp.h | 15 +++++ Common/UI/Screen.h | 12 ++++ Common/UI/ScreenManager.cpp | 84 +++++++++++++++++++++++++ Common/UI/ScreenManager.h | 16 +++++ Core/CmdLine.cpp | 1 + Core/CmdLine.h | 1 + GPU/Common/FramebufferManagerCommon.cpp | 4 ++ GPU/Common/FramebufferManagerCommon.h | 2 + GPU/GPUCommon.h | 4 ++ GPU/GPUCommonHW.cpp | 4 ++ GPU/GPUCommonHW.h | 1 + GPU/Software/SoftGpu.cpp | 6 ++ GPU/Software/SoftGpu.h | 1 + SDL/SDLMain.cpp | 62 ++++++++++++++++++ UI/EmuScreen.cpp | 46 +++++++++++--- UI/EmuScreen.h | 7 +++ UI/NativeApp.cpp | 65 +++++++++++++++++++ 17 files changed, 323 insertions(+), 8 deletions(-) diff --git a/Common/System/NativeApp.h b/Common/System/NativeApp.h index 9af36836d3..6f733bdb14 100644 --- a/Common/System/NativeApp.h +++ b/Common/System/NativeApp.h @@ -110,6 +110,21 @@ bool Native_IsWindowHidden(); // TODO: Feels like this belongs elsewhere. bool Native_UpdateScreenScale(int width, int height, float customScale); +namespace Draw { +struct SwapchainDesc; +} + +enum class OutputRole { + Game, // The emulated display, with no UI. +}; + +// Secondary outputs, like a second window or a TV (see ScreenManager::AddOutput). Safe to call from any +// thread. They take effect at the start of the next frame. The surface in desc has to stay valid until +// the output is removed, and that frame has started. +int Native_AddOutput(OutputRole role, const Draw::SwapchainDesc &desc); +void Native_ResizeOutput(int id, int width, int height); +void Native_RemoveOutput(int id); + AudioBackend *System_CreateAudioBackend(); class NativeApplication : public Application { diff --git a/Common/UI/Screen.h b/Common/UI/Screen.h index 9848db8f67..c1fc7d2e2c 100644 --- a/Common/UI/Screen.h +++ b/Common/UI/Screen.h @@ -18,6 +18,7 @@ #include "Common/Common.h" #include "Common/Input/InputState.h" +#include "Common/System/NativeApp.h" #include "Common/System/System.h" namespace UI { @@ -26,8 +27,16 @@ namespace UI { namespace Draw { class DrawContext; +class Swapchain; } +// A display other than the primary one, like a second window or a TV. See docs/multi-swapchain.md. +struct OutputDisplay { + int id; + OutputRole role; + Draw::Swapchain *swapchain; // nullptr while there's no draw context. +}; + enum DialogResult { DR_NONE, DR_OK, @@ -107,6 +116,9 @@ public: virtual void update() {} virtual ScreenRenderFlags PreRender(ScreenRenderMode mode) { return ScreenRenderFlags::NONE; } virtual ScreenRenderFlags render(ScreenRenderMode mode) = 0; + // Draws this screen's content for a secondary output, whose swapchain is bound. Returns false if the + // screen has nothing for it. The topmost screen that returns true is the one shown. + virtual bool renderOutput(const OutputDisplay &output) { return false; } virtual void resized() {} virtual void dialogFinished(const Screen *dialog, DialogResult result) {} virtual void sendMessage(UIMessage message, const char *value) {} diff --git a/Common/UI/ScreenManager.cpp b/Common/UI/ScreenManager.cpp index dbcb111048..38246c348a 100644 --- a/Common/UI/ScreenManager.cpp +++ b/Common/UI/ScreenManager.cpp @@ -177,12 +177,78 @@ void ScreenManager::switchToNext() { void ScreenManager::deviceLost() { for (auto &iter : stack_) iter.screen->deviceLost(); + for (Output &output : outputs_) { + if (output.display.swapchain) { + draw_->DestroySwapchain(output.display.swapchain); + output.display.swapchain = nullptr; + } + } + // Outputs added before the device is restored get their swapchains then. + deviceLost_ = true; } void ScreenManager::deviceRestored(Draw::DrawContext *draw) { draw_ = draw; + deviceLost_ = false; for (auto &iter : stack_) iter.screen->deviceRestored(draw); + for (Output &output : outputs_) { + std::string error; + output.display.swapchain = draw_->CreateSwapchain(output.desc, &error); + if (!output.display.swapchain) { + ERROR_LOG(Log::G3D, "Failed to recreate the swapchain for output %d: %s", output.display.id, error.c_str()); + } + } +} + +void ScreenManager::AddOutput(int id, OutputRole role, const Draw::SwapchainDesc &desc) { + Output output{}; + output.display.id = id; + output.display.role = role; + output.desc = desc; + if (draw_ && !deviceLost_) { + std::string error; + output.display.swapchain = draw_->CreateSwapchain(desc, &error); + if (!output.display.swapchain) { + ERROR_LOG(Log::G3D, "Failed to create the swapchain for output %d: %s", id, error.c_str()); + } + } + INFO_LOG(Log::G3D, "Added output %d (%dx%d)", id, desc.width, desc.height); + outputs_.push_back(output); +} + +void ScreenManager::ResizeOutput(int id, int width, int height) { + for (Output &output : outputs_) { + if (output.display.id == id) { + output.desc.width = width; + output.desc.height = height; + if (output.display.swapchain) { + draw_->ResizeSwapchain(output.display.swapchain, width, height); + } + } + } +} + +void ScreenManager::RemoveOutput(int id) { + for (auto iter = outputs_.begin(); iter != outputs_.end(); ++iter) { + if (iter->display.id == id) { + if (iter->display.swapchain) { + draw_->DestroySwapchain(iter->display.swapchain); + } + outputs_.erase(iter); + INFO_LOG(Log::G3D, "Removed output %d", id); + return; + } + } +} + +const OutputDisplay *ScreenManager::FindOutput(OutputRole role) const { + for (const Output &output : outputs_) { + if (output.display.role == role && output.display.swapchain) { + return &output.display; + } + } + return nullptr; } void ScreenManager::resized() { @@ -212,6 +278,24 @@ ScreenRenderFlags ScreenManager::Render(std::function afterPreRender) { afterPreRender(); + // Secondary outputs. The topmost screen with something for one draws it, otherwise it's left cleared. + for (const Output &output : outputs_) { + Draw::Swapchain *swapchain = output.display.swapchain; + if (!swapchain) { + continue; + } + draw_->BindFramebufferAsRenderTarget(swapchain, {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "Output"); + int w, h; + draw_->GetFramebufferDimensions(swapchain, &w, &h); + draw_->SetViewport(Draw::Viewport{0.0f, 0.0f, (float)w, (float)h, 0.0f, 1.0f}); + draw_->SetScissorRect(0, 0, w, h); + for (auto iter = stack_.rbegin(); iter != stack_.rend(); ++iter) { + if (iter->screen->renderOutput(output.display)) { + break; + } + } + } + // Now, start the final render pass. This is now the ONLY place where binding the backbuffer is allowed. draw_->BindFramebufferAsRenderTarget(draw_->GetPrimarySwapchain(), {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer"); getUIContext()->BeginFrame(); diff --git a/Common/UI/ScreenManager.h b/Common/UI/ScreenManager.h index 3881d2bf5a..9b8e46af69 100644 --- a/Common/UI/ScreenManager.h +++ b/Common/UI/ScreenManager.h @@ -9,6 +9,7 @@ #include #include "Common/Common.h" +#include "Common/GPU/thin3d.h" #include "Common/Input/InputState.h" #include "Common/System/System.h" #include "Common/UI/Screen.h" @@ -93,6 +94,14 @@ public: return topIsTopLevel_; } + // Secondary outputs. Call these on the render thread, between frames. The swapchains are recreated + // after a lost device, so the surface in desc has to stay valid until RemoveOutput. + void AddOutput(int id, OutputRole role, const Draw::SwapchainDesc &desc); + void ResizeOutput(int id, int width, int height); + void RemoveOutput(int id); + // nullptr if there's no output with the role, or it has no swapchain right now. + const OutputDisplay *FindOutput(OutputRole role) const; + private: void pop(); void switchToNext(); @@ -125,6 +134,13 @@ private: std::unordered_map lastAxis_; + struct Output { + OutputDisplay display; + Draw::SwapchainDesc desc; + }; + std::vector outputs_; + bool deviceLost_ = false; + std::atomic passInputToMapper_{InputMode::None}; std::atomic topIsTopLevel_{false}; }; diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index ad1ae1a746..34651fb05e 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -189,6 +189,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(fullscreen), CmdParamType::BoolInverse, "windowed", '\0', "Force windowed mode", CmdLineMode::Application}, {POFF(startScreen), CmdParamType::String, "start-screen", '\0', "Start on a specific screen (e.g. 'gamesettings', 'touchscreentest')", CmdLineMode::Application}, {POFF(escapeExit), CmdParamType::Bool, "escape-exit", '\0', "Escape key exits the application", CmdLineMode::Application}, + {POFF(gameWindow), CmdParamType::Bool, "game-window", '\0', "Show the game in a second window, for testing multiple outputs (SDL, Metal only)", CmdLineMode::Application}, {POFF(pauseMenuExit), CmdParamType::Bool, "pause-menu-exit", '\0', "Exit the application when the game ends, instead of returning to the menu", CmdLineMode::Application}, {POFF(appendConfig), CmdParamType::String, "appendconfig", '\0', "Merge config FILE into the current configuration"}, {POFF(root), CmdParamType::String, "root", 'r', "Mount directory as the root of host0:/."}, diff --git a/Core/CmdLine.h b/Core/CmdLine.h index e5dfa88fa7..76792e516f 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -45,6 +45,7 @@ struct CommandLineOptions { std::optional startScreen; std::optional escapeExit; + std::optional gameWindow; // Run as if no memory stick were inserted. std::optional memStickInserted; // Quit the app instead of going to the main menu when the game ends, for external launchers. diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index 956e7f71c0..ae684a1028 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -128,6 +128,10 @@ bool FramebufferManagerCommon::PresentedThisFrame() const { return presentation_->PresentedThisFrame(); } +void FramebufferManagerCommon::SetPresentationOutput(const OutputParams *output) { + presentation_->SetOutputOverride(output); +} + void FramebufferManagerCommon::SetDisplayFramebuffer(u32 framebuf, u32 stride, GEBufferFormat format) { displayFramebufPtr_ = framebuf & 0x3FFFFFFF; if (Memory::IsVRAMAddress(displayFramebufPtr_)) diff --git a/GPU/Common/FramebufferManagerCommon.h b/GPU/Common/FramebufferManagerCommon.h index 95e2bb1e0d..61e2d4b250 100644 --- a/GPU/Common/FramebufferManagerCommon.h +++ b/GPU/Common/FramebufferManagerCommon.h @@ -295,6 +295,7 @@ struct DrawPixelsEntry { struct GPUDebugBuffer; class DrawEngineCommon; class PresentationCommon; +struct OutputParams; class ShaderManagerCommon; class TextureCacheCommon; struct DisplayLayoutConfig; @@ -523,6 +524,7 @@ public: } bool PresentedThisFrame() const; + void SetPresentationOutput(const OutputParams *output); const std::vector &GetVFBs() const { return vfbs_; diff --git a/GPU/GPUCommon.h b/GPU/GPUCommon.h index 0cb177a1b2..c63a415b7c 100644 --- a/GPU/GPUCommon.h +++ b/GPU/GPUCommon.h @@ -27,6 +27,7 @@ class FramebufferManagerCommon; class TextureCacheCommon; +struct OutputParams; class DrawEngineCommon; class VertexDecoder; class GraphicsContext; @@ -152,6 +153,9 @@ public: virtual void PrepareCopyDisplayToOutput(const DisplayLayoutConfig &config) = 0; virtual void CopyDisplayToOutput(const DisplayLayoutConfig &config) = 0; virtual bool PresentedThisFrame() const = 0; + // Presents the display to another output than the primary one, until called with nullptr. Set it + // before PrepareCopyDisplayToOutput, which lays out for it. + virtual void SetPresentationOutput(const OutputParams *output) {} // Invalidate any cached content sourced from the specified range. // If size = -1, invalidate everything. diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index bf6d14a854..6a00b9291b 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -521,6 +521,10 @@ bool GPUCommonHW::PresentedThisFrame() const { return framebufferManager_->PresentedThisFrame(); } +void GPUCommonHW::SetPresentationOutput(const OutputParams *output) { + framebufferManager_->SetPresentationOutput(output); +} + void GPUCommonHW::DoState(PointerWrap &p) { GPUCommon::DoState(p); diff --git a/GPU/GPUCommonHW.h b/GPU/GPUCommonHW.h index 95d4f56936..e4f3f0d9a8 100644 --- a/GPU/GPUCommonHW.h +++ b/GPU/GPUCommonHW.h @@ -71,6 +71,7 @@ public: void ExecuteOp(u32 op, u32 diff) override; bool PresentedThisFrame() const override; + void SetPresentationOutput(const OutputParams *output) override; private: void CheckDepthUsage(VirtualFramebuffer *vfb) override; diff --git a/GPU/Software/SoftGpu.cpp b/GPU/Software/SoftGpu.cpp index aa26ae31be..8293e46dca 100644 --- a/GPU/Software/SoftGpu.cpp +++ b/GPU/Software/SoftGpu.cpp @@ -720,6 +720,12 @@ bool SoftGPU::PresentedThisFrame() const { return presentation_ ? presentation_->PresentedThisFrame() : false; } +void SoftGPU::SetPresentationOutput(const OutputParams *output) { + if (presentation_) { + presentation_->SetOutputOverride(output); + } +} + void SoftGPU::MarkDirty(uint32_t addr, uint32_t stride, uint32_t height, GEBufferFormat fmt, SoftGPUVRAMDirty value) { uint32_t bytes = height * stride * (fmt == GE_FORMAT_8888 ? 4 : 2); MarkDirty(addr, bytes, value); diff --git a/GPU/Software/SoftGpu.h b/GPU/Software/SoftGpu.h index ffe2f36796..79e1fba66c 100644 --- a/GPU/Software/SoftGpu.h +++ b/GPU/Software/SoftGpu.h @@ -256,6 +256,7 @@ public: void BeginHostFrame(const DisplayLayoutConfig &config) override; bool PresentedThisFrame() const override; + void SetPresentationOutput(const OutputParams *output) override; protected: void FastRunLoop(DisplayList &list) override; diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index bb484cd52f..f07fe6af58 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -28,6 +28,7 @@ SDLJoystick *joystick = NULL; #include "Common/System/System.h" #include "Common/System/Request.h" #include "Common/System/NativeApp.h" +#include "Common/GPU/thin3d.h" #include "Common/Audio/AudioBackend.h" #include "Core/CmdLine.h" #include "Core/EmuThread.h" @@ -1437,7 +1438,64 @@ void UpdateCursor() { SDL_SetCursor(g_builtinCursors[(int)curCursor]); } +// --game-window: a second window showing just the game, for testing multiple outputs. +static SDL_Window *g_gameWindow = nullptr; +static int g_gameOutputId = 0; + +static void CreateGameWindow() { +#if PPSSPP_PLATFORM(MAC) + if (g_Config.iGPUBackend != (int)GPUBackend::METAL) { + fprintf(stderr, "--game-window needs --graphics=metal\n"); + return; + } + g_gameWindow = SDL_CreateWindow("PPSSPP game", 960, 544, SDL_WINDOW_METAL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY); + if (!g_gameWindow) { + fprintf(stderr, "Error creating the game window: %s\n", SDL_GetError()); + return; + } + SDL_MetalView metalView = SDL_Metal_CreateView(g_gameWindow); + int width = 0, height = 0; + SDL_GetWindowSizeInPixels(g_gameWindow, &width, &height); + Draw::SwapchainDesc desc{}; + desc.winsys = WINDOWSYSTEM_METAL_EXT; + desc.data1 = SDL_Metal_GetLayer(metalView); + desc.width = width; + desc.height = height; + desc.tag = "GameWindow"; + g_gameOutputId = Native_AddOutput(OutputRole::Game, desc); +#else + fprintf(stderr, "--game-window is only implemented on Mac (Metal)\n"); +#endif +} + +// Returns true if the event was for the game window, and has been handled. Keys still go to the emulator. +static bool ProcessGameWindowEvent(const SDL_Event &event) { + if (!g_gameWindow || SDL_GetWindowFromEvent(&event) != g_gameWindow) { + return false; + } + switch (event.type) { + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + Native_ResizeOutput(g_gameOutputId, event.window.data1, event.window.data2); + return true; + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + // The layer has to outlive the swapchain, so the window stays until we quit. + Native_RemoveOutput(g_gameOutputId); + SDL_HideWindow(g_gameWindow); + return true; + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + case SDL_EVENT_TEXT_INPUT: + return false; + default: + // Window events, and the mouse, which the UI in the main window would get the coordinates wrong for. + return true; + } +} + static void ProcessSDLEvent(SDL_Window *window, const SDL_Event &event, InputStateTracker *inputTracker) { + if (ProcessGameWindowEvent(event)) { + return; + } // SDL input uses window coordinates; convert to pixels before applying the UI scale. float inputPixelDensity = SDL_GetWindowPixelDensity(window); if (inputPixelDensity <= 0.0f) { @@ -2289,6 +2347,10 @@ int main(int argc, char *argv[]) { SDL_SetWindowTitle(window, (app_name_nice + " " + PPSSPP_GIT_VERSION).c_str()); + if (cmdLineOptions.gameWindow.value_or(false)) { + CreateGameWindow(); + } + char iconPath[PATH_MAX]; #if defined(ASSETS_DIR) snprintf(iconPath, PATH_MAX, "%sui_images/icon.png", ASSETS_DIR); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 6e18dd1282..875907f4cc 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1643,14 +1643,14 @@ ScreenRenderFlags EmuScreen::PreRender(ScreenRenderMode mode) { using namespace Draw; skipBufferEffects_ = g_Config.bSkipBufferEffects; + UpdateGameOutput(); if (!skipBufferEffects_ && PSP_IsInited()) { if (ShouldRunEmulation(mode)) { // We need to run emulation here, and perform all the normal render passes. return RunEmulation(false); } else if (gpu && GPUIsBuffered()) { // Need to check for gpu here, we might be in reset. - const DeviceOrientation orientation = GetDeviceOrientation(); - const DisplayLayoutConfig &displayLayoutConfig = g_Config.GetDisplayLayoutConfig(orientation); + const DisplayLayoutConfig &displayLayoutConfig = GameLayoutConfig(); // We run just the post shaders. gpu->PrepareCopyDisplayToOutput(displayLayoutConfig); // Screens on top like reporting might want to take screenshots of existing framebuffers. @@ -1660,6 +1660,38 @@ ScreenRenderFlags EmuScreen::PreRender(ScreenRenderMode mode) { return ScreenRenderFlags::NONE; } +// Sends the game to a Game output, if there is one. Not in non-buffered mode, which draws straight +// into the backbuffer during emulation. +void EmuScreen::UpdateGameOutput() { + const OutputDisplay *output = screenManager()->FindOutput(OutputRole::Game); + gameOnOutput_ = output && gpu && !skipBufferEffects_ && GPUIsBuffered(); + if (!gpu) { + return; + } + if (!gameOnOutput_) { + gpu->SetPresentationOutput(nullptr); + return; + } + OutputParams params; + params.pixelWidth = output->swapchain->Width(); + params.pixelHeight = output->swapchain->Height(); + params.orientation = params.pixelHeight > params.pixelWidth * 1.1f ? DeviceOrientation::Portrait : DeviceOrientation::Landscape; + gameOutputOrientation_ = params.orientation; + gpu->SetPresentationOutput(¶ms); +} + +const DisplayLayoutConfig &EmuScreen::GameLayoutConfig() const { + return g_Config.GetDisplayLayoutConfig(gameOnOutput_ ? gameOutputOrientation_ : GetDeviceOrientation()); +} + +bool EmuScreen::renderOutput(const OutputDisplay &output) { + if (output.role != OutputRole::Game || !gameOnOutput_ || !PSP_IsInited()) { + return false; + } + gpu->CopyDisplayToOutput(GameLayoutConfig()); + return true; +} + ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { // Moved from update, because we want it to be possible for booting to happen even when the screen // is in the background, like when choosing Reset from the pause menu. @@ -1677,13 +1709,12 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { System_Notify(SystemNotification::KEEP_SCREEN_AWAKE); } - const DeviceOrientation orientation = GetDeviceOrientation(); - const DisplayLayoutConfig &displayLayoutConfig = g_Config.GetDisplayLayoutConfig(orientation); + const DisplayLayoutConfig &displayLayoutConfig = GameLayoutConfig(); // We might have a bad viewport after RunEmulation, reset. Viewport viewport{0.0f, 0.0f, (float)g_display.pixel_xres, (float)g_display.pixel_yres, 0.0f, 1.0f}; if (!skipBufferEffects_ && !ShouldRunEmulation(mode)) { - if (gpu) { + if (gpu && !gameOnOutput_) { gpu->CopyDisplayToOutput(displayLayoutConfig); } draw->SetViewport(viewport); @@ -1716,7 +1747,7 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { const bool skipBufferEffects = skipBufferEffects_; // Gotta copy the output at some point. Also this is where we take the screenshot if needed. - if (gpu) { + if (gpu && !gameOnOutput_) { gpu->CopyDisplayToOutput(displayLayoutConfig); } @@ -1771,8 +1802,7 @@ ScreenRenderFlags EmuScreen::RunEmulation(bool skipBufferEffects) { g_OSD.NudgeIngameNotifications(); - const DeviceOrientation orientation = GetDeviceOrientation(); - const DisplayLayoutConfig &displayLayoutConfig = g_Config.GetDisplayLayoutConfig(orientation); + const DisplayLayoutConfig &displayLayoutConfig = GameLayoutConfig(); __DisplaySetDisplayLayoutConfig(displayLayoutConfig); DrawContext *draw = screenManager()->getDrawContext(); diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index 87905d024f..51065f308d 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -31,6 +31,7 @@ #include "UI/ImDebugger/ImCommand.h" struct AxisInput; +struct DisplayLayoutConfig; class AsyncImageFileView; class ChatMenu; @@ -66,6 +67,7 @@ protected: void darken(); void focusChanged(ScreenFocusChange focusChange) override; ScreenRenderFlags PreRender(ScreenRenderMode mode) override; + bool renderOutput(const OutputDisplay &output) override; // ControlListener implementations void OnVKey(VirtKey virtualKeyCode, bool down) override; @@ -79,6 +81,8 @@ protected: private: void CreateViews() override; ScreenRenderFlags RunEmulation(bool skipBufferEffects); + void UpdateGameOutput(); + const DisplayLayoutConfig &GameLayoutConfig() const; void OnDevTools(UI::EventParams ¶ms); void OpenChat(bool focus); @@ -153,6 +157,9 @@ private: bool autoLoadFailed_ = false; // to prevent repeat reloads bool readyToFinishBoot_ = false; bool skipBufferEffects_ = false; // cached state, fetched once per frame. + // The game goes to a secondary output (OutputRole::Game) instead of the backbuffer. Per frame too. + bool gameOnOutput_ = false; + DeviceOrientation gameOutputOrientation_ = DeviceOrientation::Landscape; uint32_t clearColor_ = 0; }; diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index b1cfcfa1b7..15d7a0662f 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -190,6 +190,38 @@ std::string config_filename; // that consumes it also sees those writes - with a plain bool it could see the flag first and keep stale bounds. static std::atomic resized{false}; +struct OutputRequest { + enum class Type { + ADD, + RESIZE, + REMOVE, + }; + Type type; + int id; + OutputRole role; + Draw::SwapchainDesc desc; + int width; + int height; +}; +static std::mutex g_outputRequestLock; +static std::vector g_outputRequests; +static std::atomic g_nextOutputId{1}; + +static void ProcessOutputRequests() { + std::vector requests; + { + std::lock_guard guard(g_outputRequestLock); + requests.swap(g_outputRequests); + } + for (const OutputRequest &request : requests) { + switch (request.type) { + case OutputRequest::Type::ADD: g_screenManager->AddOutput(request.id, request.role, request.desc); break; + case OutputRequest::Type::RESIZE: g_screenManager->ResizeOutput(request.id, request.width, request.height); break; + case OutputRequest::Type::REMOVE: g_screenManager->RemoveOutput(request.id); break; + } + } +} + static int renderCounter = 0; struct PendingMessage { @@ -940,6 +972,8 @@ bool NativeInitGraphics(GraphicsContext *graphicsContext) { g_screenManager->setUIContext(uiContext); g_screenManager->setPostRenderCallback(&CallbackPostRender, nullptr); + // An output removed while there was no device can't be recreated, its surface may be gone. + ProcessOutputRequests(); g_screenManager->deviceRestored(g_draw); g_audioBackend = System_CreateAudioBackend(); @@ -1193,6 +1227,8 @@ void NativeFrame(GraphicsContext *graphicsContext) { #endif } + ProcessOutputRequests(); + // NOTE: We must begin the frame before update, so we can do texture size queries and stuff in Measure etc. Draw::DebugFlags debugFlags = Draw::DebugFlags::NONE; if ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::GPU_PROFILE) @@ -2000,6 +2036,35 @@ static bool IsWindowSmall(int pixelWidth, int pixelHeight) { return config.InternalRotationIsPortrait() ? (h < 480 + 80) : (w < 480 + 80); } +int Native_AddOutput(OutputRole role, const Draw::SwapchainDesc &desc) { + OutputRequest request{}; + request.type = OutputRequest::Type::ADD; + request.id = g_nextOutputId++; + request.role = role; + request.desc = desc; + std::lock_guard guard(g_outputRequestLock); + g_outputRequests.push_back(request); + return request.id; +} + +void Native_ResizeOutput(int id, int width, int height) { + OutputRequest request{}; + request.type = OutputRequest::Type::RESIZE; + request.id = id; + request.width = width; + request.height = height; + std::lock_guard guard(g_outputRequestLock); + g_outputRequests.push_back(request); +} + +void Native_RemoveOutput(int id) { + OutputRequest request{}; + request.type = OutputRequest::Type::REMOVE; + request.id = id; + std::lock_guard guard(g_outputRequestLock); + g_outputRequests.push_back(request); +} + bool Native_UpdateScreenScale(int pixel_width, int pixel_height, float customScale) { INFO_LOG(Log::System, "Native_UpdateScreenScale: %dx%d, customScale=%f", pixel_width, pixel_height, customScale);