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) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-22 18:42:53 +02:00
1 parent 56c76ace35
commit d491fc90cf
17 files changed
+323 -8

No files matched your search

+15
View File
@@ -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 {
+12
View File
@@ -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) {}
+84
View File
@@ -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<void()> 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();
+16
View File
@@ -9,6 +9,7 @@
#include <vector>
#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<int64_t, int> lastAxis_;
struct Output {
OutputDisplay display;
Draw::SwapchainDesc desc;
};
std::vector<Output> outputs_;
bool deviceLost_ = false;
std::atomic<InputMode> passInputToMapper_{InputMode::None};
std::atomic<bool> topIsTopLevel_{false};
};
+1
View File
@@ -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:/."},
+1
View File
@@ -45,6 +45,7 @@ struct CommandLineOptions {
std::optional<std::string> startScreen;
std::optional<bool> escapeExit;
std::optional<bool> gameWindow;
// Run as if no memory stick were inserted.
std::optional<bool> memStickInserted;
// Quit the app instead of going to the main menu when the game ends, for external launchers.
+4
View File
@@ -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_))
+2
View File
@@ -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<VirtualFramebuffer *> &GetVFBs() const {
return vfbs_;
+4
View File
@@ -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.
+4
View File
@@ -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);
+1
View File
@@ -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;
+6
View File
@@ -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);
+1
View File
@@ -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;
+62
View File
@@ -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);
+38 -8
View File
@@ -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(&params);
}
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();
+7
View File
@@ -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 &params);
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;
};
+65
View File
@@ -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<bool> 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<OutputRequest> g_outputRequests;
static std::atomic<int> g_nextOutputId{1};
static void ProcessOutputRequests() {
std::vector<OutputRequest> requests;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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);