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

+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();