From 56c76ace35979645870509e95e7e6d08c0e2d87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 22 Oct 2026 12:59:08 +0200 Subject: [PATCH] thin3d: Swapchain objects, and multiple swapchains on Metal The backbuffer is now a Swapchain, from GetPrimarySwapchain(), and callers bind, measure and read it back through that instead of nullptr. nullptr still means it too. Backends with one swapchain map the primary to their old backbuffer path. On Metal, CreateSwapchain() adds more, each with its own layer, depth buffer and drawable. Passes merge per swapchain, and Present() presents every drawable acquired that frame. The backbuffer is just the first of them. Nothing creates a second one yet. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/GPU/D3D11/thin3d_d3d11.cpp | 4 + Common/GPU/Metal/MetalRenderManager.h | 82 ++++++---- Common/GPU/Metal/MetalRenderManager.mm | 205 ++++++++++++++---------- Common/GPU/Metal/thin3d_metal.mm | 107 ++++++++++--- Common/GPU/OpenGL/thin3d_gl.cpp | 4 + Common/GPU/Vulkan/thin3d_vulkan.cpp | 5 + Common/GPU/thin3d.h | 56 ++++++- Common/UI/ScreenManager.cpp | 4 +- GPU/Common/FramebufferManagerCommon.cpp | 6 +- GPU/GLES/FramebufferManagerGLES.cpp | 4 +- UI/EmuScreen.cpp | 2 +- docs/multi-swapchain.md | 9 +- headless/Headless.cpp | 2 +- libretro/libretro.cpp | 2 +- 14 files changed, 336 insertions(+), 156 deletions(-) diff --git a/Common/GPU/D3D11/thin3d_d3d11.cpp b/Common/GPU/D3D11/thin3d_d3d11.cpp index edc4b397ab..c7a3ec225b 100644 --- a/Common/GPU/D3D11/thin3d_d3d11.cpp +++ b/Common/GPU/D3D11/thin3d_d3d11.cpp @@ -460,6 +460,7 @@ void D3D11DrawContext::HandleEvent(Event ev, int width, int height, void *param1 bbRenderTargetTex_ = (ID3D11Texture2D *)param2; bbWidth_ = width; bbHeight_ = height; + primarySwapchain_->SetSize(width, height); // Create matching depth stencil texture. This is not really needed for PPSSPP though, // and probably not for most other renderers either as you're usually rendering to other render targets and @@ -1725,6 +1726,7 @@ bool D3D11DrawContext::BlitFramebuffer(Framebuffer *srcfb, int srcX1, int srcY1, } bool D3D11DrawContext::CopyFramebufferToMemory(Framebuffer *src, Aspect channelBits, int bx, int by, int bw, int bh, Draw::DataFormat destFormat, void *pixels, int pixelStride, ReadbackMode mode, const char *tag) { + src = PrimaryToNull(src); D3D11Framebuffer *fb = (D3D11Framebuffer *)src; if (fb) { @@ -1879,6 +1881,7 @@ bool D3D11DrawContext::CopyFramebufferToMemory(Framebuffer *src, Aspect channelB } void D3D11DrawContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp, const char *tag) { + fbo = PrimaryToNull(fbo); // TODO: deviceContext1 can actually discard. if (fbo) { D3D11Framebuffer *fb = (D3D11Framebuffer *)fbo; @@ -1976,6 +1979,7 @@ uint64_t D3D11DrawContext::GetNativeObject(NativeObject obj, void *srcObject) { } void D3D11DrawContext::GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) { + fbo = PrimaryToNull(fbo); D3D11Framebuffer *fb = (D3D11Framebuffer *)fbo; if (fb) { *w = fb->Width(); diff --git a/Common/GPU/Metal/MetalRenderManager.h b/Common/GPU/Metal/MetalRenderManager.h index 63b8bcafd5..b18d3e1c7f 100644 --- a/Common/GPU/Metal/MetalRenderManager.h +++ b/Common/GPU/Metal/MetalRenderManager.h @@ -221,8 +221,24 @@ struct MetalCommand { }; }; +// A layer we present to, or an offscreen backbuffer of a fixed size. The render manager owns them, and +// the first one is the backbuffer. +struct MetalSwapchain { + CAMetalLayer *layer = nil; // nil for an offscreen backbuffer. + id offscreen; + id depth; + int width = 0; + int height = 0; + // Non-zero to resize the layer's drawables, which the replay does as it's what acquires them. + int pendingWidth = 0; + int pendingHeight = 0; + bool drawn = false; // Something has rendered to it. + // Replay side. Acquired by the first pass that renders to it, presented at the end of the frame. + id drawable; +}; + struct MetalRecordedPass { - void *color; // Unused for the backbuffer, it's the drawable's. + void *color; // Unused for a swapchain, it's the drawable's. void *depth; void *msaaColor; void *msaaDepth; @@ -235,7 +251,7 @@ struct MetalRecordedPass { MTLStoreAction depthStore; bool usesDepth; // Some draw tested or wrote depth or stencil. bool depthMemoryless; - bool backbuffer; + MetalSwapchain *swapchain; // Renders to this instead of color. std::string tag; }; @@ -248,7 +264,7 @@ enum class MetalCopyType : uint8_t { struct MetalRecordedCopy { MetalCopyType type; - bool srcIsBackbuffer; // The source texture is the current drawable. + MetalSwapchain *srcSwapchain; // Copies from the swapchain's current drawable instead of src. void *src; void *dst; uint32_t srcOffset; // For buffers. @@ -289,7 +305,8 @@ struct MetalStep { uint32_t index; // Into the chunk's passes or copies. // Command ranges, [first, last), not including the pass begin and end. Merging appends more. std::vector> ranges; - void *target; // Color (or depth, if there's no color) of a pass. Destination of a copy. + void *target; // Color (or depth, if there's no color) of a pass, or its swapchain. Destination of a copy. + bool targetIsSwapchain; void *targetDepth; void *targetMsaa; // A pass's multisampled color (or depth), which the shared code can read samples from. std::vector reads; // Textures sampled by the pass, or the copy's source. @@ -305,8 +322,12 @@ struct MetalCommandChunk { std::vector dispatches; MetalChunkEnd end = MetalChunkEnd::FLUSH; bool vsync = true; - int newDrawableWidth = 0; // Non-zero to resize the layer's drawables first. - int newDrawableHeight = 0; + struct DrawableResize { + MetalSwapchain *swapchain; + int width; + int height; + }; + std::vector drawableResizes; // Applied before the commands. dispatch_semaphore_t completed = nullptr; // Signalled when the GPU is done with the chunk, if set. bool exitThread = false; @@ -318,8 +339,7 @@ struct MetalCommandChunk { dispatches.clear(); end = MetalChunkEnd::FLUSH; vsync = true; - newDrawableWidth = 0; - newDrawableHeight = 0; + drawableResizes.clear(); completed = nullptr; exitThread = false; } @@ -356,29 +376,30 @@ public: } } - // Backbuffer - void Resize(int width, int height); - int BackbufferWidth() const { return bbWidth_; } - int BackbufferHeight() const { return bbHeight_; } + // Swapchains. The backbuffer is the first, and lives as long as the render manager. Others are created + // and destroyed between frames. Destroying one waits for the GPU. + MetalSwapchain *Backbuffer() const { return backbuffer_; } + MetalSwapchain *CreateSwapchain(CAMetalLayer *layer, int width, int height); + void DestroySwapchain(MetalSwapchain *swapchain); + void Resize(MetalSwapchain *swapchain, int width, int height); + void Resize(int width, int height) { Resize(backbuffer_, width, height); } + int BackbufferWidth() const { return backbuffer_->width; } + int BackbufferHeight() const { return backbuffer_->height; } MTLPixelFormat BackbufferFormat() const { return MTLPixelFormatBGRA8Unorm; } - // The offscreen backbuffer, or nil when rendering to the layer. - id OffscreenBackbuffer() const { return offscreenBackbuffer_; } - // False until something renders to the backbuffer (headless never does). - bool BackbufferDrawn() const { return backbufferDrawn_; } // Render passes. The pass is started lazily, see above. void BindRenderPass(const MetalRenderPassDesc &desc); - void BindBackbufferPass(MTLLoadAction colorLoad, MTLClearColor clearColor, const char *tag); + void BindBackbufferPass(MetalSwapchain *swapchain, MTLLoadAction colorLoad, MTLClearColor clearColor, const char *tag); // Clears parts of the current render target, by restarting the pass with clear load actions if needed. void Clear(bool color, bool depth, bool stencil, MTLClearColor clearColor, double clearDepth, uint32_t clearStencil); // Forget the current render target, ending its pass. void UnbindRenderPass(); - bool HasRenderPass() const { return pass_.color != nil || pass_.depth != nil || passIsBackbuffer_; } + bool HasRenderPass() const { return pass_.color != nil || pass_.depth != nil || passSwapchain_; } const MetalRenderPassDesc &CurPass() const { return pass_; } const MetalRenderTargetFormat &CurTargetFormat() const { return curFormat_; } int CurPassWidth() const { return passWidth_; } int CurPassHeight() const { return passHeight_; } - bool IsBackbufferPass() const { return passIsBackbuffer_; } + bool IsBackbufferPass() const { return passSwapchain_ != nullptr; } // Starts the render pass if needed. Returns false if there's no render target, in which case nothing // should be drawn. @@ -421,8 +442,8 @@ public: // goes in the upload command buffer, which runs before everything else in the current submit. Use that // only for resources that nothing earlier in the frame has used (like new textures). void CopyTexture(__unsafe_unretained id src, int srcLevel, MTLOrigin srcOrigin, MTLSize size, __unsafe_unretained id dst, int dstLevel, MTLOrigin dstOrigin); - // src nil means the backbuffer. - void CopyTextureToBuffer(__unsafe_unretained id src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id dst, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLBlitOption options); + // With srcSwapchain set, copies from its drawable (or offscreen texture) instead of src. + void CopyTextureToBuffer(__unsafe_unretained id src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id dst, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLBlitOption options, MetalSwapchain *srcSwapchain = nullptr); void CopyBufferToTexture(__unsafe_unretained id src, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLSize size, __unsafe_unretained id dst, int slice, int level, MTLOrigin origin, MTLBlitOption options, bool upload); void GenerateMips(__unsafe_unretained id texture, bool upload); // Runs in the upload command buffer, in order with the upload copies. Fill in the returned dispatch. @@ -438,8 +459,7 @@ private: void EndRenderPass(); void RecordEndPass(); void ResetEncoderState(); - void CreateOffscreenBackbuffer(int width, int height); - void CreateBackbufferDepth(); + void CreateSwapchainDepth(MetalSwapchain *swapchain); MetalCommand &Record(MetalCmd cmd) { chunk_->commands.emplace_back(); MetalCommand &c = chunk_->commands.back(); @@ -458,11 +478,10 @@ private: void ReplayCopy(const MetalCommandChunk &chunk, const MetalRecordedCopy ©, id cmd, bool upload); void EndReplayEncoders(); void ReplayDispatch(const MetalRecordedDispatch &dispatch, id cmd); - id ReplayBackbuffer(); + id ReplaySwapchainTexture(MetalSwapchain *swapchain); id device_; id queue_; - CAMetalLayer *layer_ = nil; bool unifiedMemory_ = false; bool appleGPU_ = false; @@ -480,16 +499,11 @@ private: // Recording state (emu thread). MetalCommandChunk *chunk_ = nullptr; - id offscreenBackbuffer_; - bool backbufferDrawn_ = false; - id bbDepth_; - int bbWidth_ = 0; - int bbHeight_ = 0; - int pendingDrawableWidth_ = 0; - int pendingDrawableHeight_ = 0; + std::vector swapchains_; + MetalSwapchain *backbuffer_ = nullptr; MetalRenderPassDesc pass_; - bool passIsBackbuffer_ = false; + MetalSwapchain *passSwapchain_ = nullptr; bool inPass_ = false; bool passUsesDepth_ = false; size_t recordedPassIndex_ = 0; @@ -541,7 +555,7 @@ private: id rtBlit_; id rtUploadBlit_; id rtUploadCompute_; - id rtDrawable_; + std::vector rtAcquired_; // Swapchains with a drawable to present. // Replay scratch, reused between chunks. std::vector rtSteps_; size_t rtStepCount_ = 0; diff --git a/Common/GPU/Metal/MetalRenderManager.mm b/Common/GPU/Metal/MetalRenderManager.mm index 596beae597..ef936d9071 100644 --- a/Common/GPU/Metal/MetalRenderManager.mm +++ b/Common/GPU/Metal/MetalRenderManager.mm @@ -301,7 +301,7 @@ void RunOnCompileThread(std::function work) { } @end -MetalRenderManager::MetalRenderManager(id device, CAMetalLayer *layer, int offscreenWidth, int offscreenHeight, bool useThread) : device_(device), layer_(layer) { +MetalRenderManager::MetalRenderManager(id device, CAMetalLayer *layer, int offscreenWidth, int offscreenHeight, bool useThread) : device_(device) { queue_ = [device_ newCommandQueue]; queue_.label = @"PPSSPP"; unifiedMemory_ = device_.hasUnifiedMemory; @@ -315,21 +315,8 @@ MetalRenderManager::MetalRenderManager(id device, CAMetalLayer *layer dispatch_semaphore_wait(frameSemaphore_, DISPATCH_TIME_FOREVER); flushSemaphore_ = dispatch_semaphore_create(0); - if (layer_) { - layer_.device = device_; - layer_.pixelFormat = MTLPixelFormatBGRA8Unorm; - // So we can read back screenshots, and copy from it. - layer_.framebufferOnly = NO; - layer_.maximumDrawableCount = 3; - CGSize size = layer_.drawableSize; - bbWidth_ = (int)size.width; - bbHeight_ = (int)size.height; - } else { - CreateOffscreenBackbuffer(offscreenWidth, offscreenHeight); - } - CreateBackbufferDepth(); - chunk_ = new MetalCommandChunk(); + backbuffer_ = CreateSwapchain(layer, offscreenWidth, offscreenHeight); if (useThread) { renderThread_ = std::thread([this] { RenderThreadFunc(); }); } @@ -347,6 +334,10 @@ MetalRenderManager::~MetalRenderManager() { queueCond_.notify_one(); renderThread_.join(); } + for (MetalSwapchain *swapchain : swapchains_) { + delete swapchain; + } + swapchains_.clear(); delete chunk_; for (MetalCommandChunk *chunk : freeChunks_) { delete chunk; @@ -372,24 +363,56 @@ void MetalRenderManager::SetInflightFrames(int count) { inflightFrames_ = std::clamp(count, 1, METAL_MAX_INFLIGHT_FRAMES); } -void MetalRenderManager::CreateOffscreenBackbuffer(int width, int height) { - MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:width height:height mipmapped:NO]; - desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; - desc.storageMode = MTLStorageModePrivate; - DeferRelease(offscreenBackbuffer_); - offscreenBackbuffer_ = [device_ newTextureWithDescriptor:desc]; - offscreenBackbuffer_.label = @"offscreen_backbuffer"; - bbWidth_ = width; - bbHeight_ = height; +MetalSwapchain *MetalRenderManager::CreateSwapchain(CAMetalLayer *layer, int width, int height) { + MetalSwapchain *swapchain = new MetalSwapchain(); + if (layer) { + swapchain->layer = layer; + layer.device = device_; + layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + // So we can read back screenshots, and copy from it. + layer.framebufferOnly = NO; + layer.maximumDrawableCount = 3; + if (width > 0 && height > 0) { + swapchain->pendingWidth = width; + swapchain->pendingHeight = height; + } else { + CGSize size = layer.drawableSize; + width = (int)size.width; + height = (int)size.height; + } + } else { + MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:width height:height mipmapped:NO]; + desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + desc.storageMode = MTLStorageModePrivate; + swapchain->offscreen = [device_ newTextureWithDescriptor:desc]; + swapchain->offscreen.label = @"offscreen_backbuffer"; + } + swapchain->width = width; + swapchain->height = height; + CreateSwapchainDepth(swapchain); + swapchains_.push_back(swapchain); + return swapchain; } -void MetalRenderManager::CreateBackbufferDepth() { - DeferRelease(bbDepth_); - if (bbWidth_ <= 0 || bbHeight_ <= 0) { - bbDepth_ = nil; +void MetalRenderManager::DestroySwapchain(MetalSwapchain *swapchain) { + _dbg_assert_(swapchain != backbuffer_); + if (passSwapchain_ == swapchain) { + UnbindRenderPass(); + } + // After this, no chunk that refers to it is left to replay. + WaitIdle(); + swapchains_.erase(std::remove(swapchains_.begin(), swapchains_.end(), swapchain), swapchains_.end()); + rtAcquired_.erase(std::remove(rtAcquired_.begin(), rtAcquired_.end(), swapchain), rtAcquired_.end()); + delete swapchain; +} + +void MetalRenderManager::CreateSwapchainDepth(MetalSwapchain *swapchain) { + DeferRelease(swapchain->depth); + if (swapchain->width <= 0 || swapchain->height <= 0) { + swapchain->depth = nil; return; } - MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 width:bbWidth_ height:bbHeight_ mipmapped:NO]; + MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 width:swapchain->width height:swapchain->height mipmapped:NO]; desc.usage = MTLTextureUsageRenderTarget; if (appleGPU_) { // The backbuffer's depth never needs to survive a render pass. @@ -397,23 +420,22 @@ void MetalRenderManager::CreateBackbufferDepth() { } else { desc.storageMode = MTLStorageModePrivate; } - bbDepth_ = [device_ newTextureWithDescriptor:desc]; - bbDepth_.label = @"backbuffer_depth"; + swapchain->depth = [device_ newTextureWithDescriptor:desc]; + swapchain->depth.label = @"backbuffer_depth"; } -void MetalRenderManager::Resize(int width, int height) { - if (!layer_ || width <= 0 || height <= 0) { +void MetalRenderManager::Resize(MetalSwapchain *swapchain, int width, int height) { + if (!swapchain->layer || width <= 0 || height <= 0) { return; } - if (passIsBackbuffer_) { + if (passSwapchain_ == swapchain) { UnbindRenderPass(); } - // The drawables are resized by the replay, as it's what acquires them. - pendingDrawableWidth_ = width; - pendingDrawableHeight_ = height; - bbWidth_ = width; - bbHeight_ = height; - CreateBackbufferDepth(); + swapchain->pendingWidth = width; + swapchain->pendingHeight = height; + swapchain->width = width; + swapchain->height = height; + CreateSwapchainDepth(swapchain); } void MetalRenderManager::BeginFrame() { @@ -433,10 +455,13 @@ void MetalRenderManager::Submit(MetalChunkEnd end, bool vsync, dispatch_semaphor chunk->end = end; chunk->vsync = vsync; chunk->completed = completed; - chunk->newDrawableWidth = pendingDrawableWidth_; - chunk->newDrawableHeight = pendingDrawableHeight_; - pendingDrawableWidth_ = 0; - pendingDrawableHeight_ = 0; + for (MetalSwapchain *swapchain : swapchains_) { + if (swapchain->pendingWidth > 0) { + chunk->drawableResizes.push_back({ swapchain, swapchain->pendingWidth, swapchain->pendingHeight }); + swapchain->pendingWidth = 0; + swapchain->pendingHeight = 0; + } + } if (renderThread_.joinable()) { { @@ -489,12 +514,12 @@ void MetalRenderManager::WaitIdle() { Flush(true); } -void MetalRenderManager::BindBackbufferPass(MTLLoadAction colorLoad, MTLClearColor clearColor, const char *tag) { - if (passIsBackbuffer_ && inPass_ && colorLoad == MTLLoadActionLoad) { +void MetalRenderManager::BindBackbufferPass(MetalSwapchain *swapchain, MTLLoadAction colorLoad, MTLClearColor clearColor, const char *tag) { + if (passSwapchain_ == swapchain && inPass_ && colorLoad == MTLLoadActionLoad) { // Already rendering to it. return; } - backbufferDrawn_ = true; + swapchain->drawn = true; EndRenderPass(); MetalRenderPassDesc desc{}; desc.colorLoad = colorLoad; @@ -503,19 +528,19 @@ void MetalRenderManager::BindBackbufferPass(MTLLoadAction colorLoad, MTLClearCol desc.stencilLoad = MTLLoadActionClear; desc.depthMemoryless = appleGPU_; desc.tag = tag; - desc.depth = bbDepth_; + desc.depth = swapchain->depth; pass_ = desc; - passIsBackbuffer_ = true; + passSwapchain_ = swapchain; curFormat_.color = MTLPixelFormatBGRA8Unorm; - curFormat_.depth = bbDepth_ ? MTLPixelFormatDepth32Float_Stencil8 : MTLPixelFormatInvalid; + curFormat_.depth = swapchain->depth ? MTLPixelFormatDepth32Float_Stencil8 : MTLPixelFormatInvalid; curFormat_.stencil = curFormat_.depth; curFormat_.sampleCount = 1; - passWidth_ = bbWidth_; - passHeight_ = bbHeight_; + passWidth_ = swapchain->width; + passHeight_ = swapchain->height; } void MetalRenderManager::BindRenderPass(const MetalRenderPassDesc &desc) { - if (!passIsBackbuffer_ && HasRenderPass() && desc.color == pass_.color && desc.depth == pass_.depth) { + if (!passSwapchain_ && HasRenderPass() && desc.color == pass_.color && desc.depth == pass_.depth) { // Binding the target we're already on continues the pass, like in the Vulkan backend: clears are applied, // and DontCare is ignored since there's content to keep. bool clearColor = desc.colorLoad == MTLLoadActionClear; @@ -528,7 +553,7 @@ void MetalRenderManager::BindRenderPass(const MetalRenderPassDesc &desc) { } EndRenderPass(); pass_ = desc; - passIsBackbuffer_ = false; + passSwapchain_ = nullptr; id sizeTex = desc.color ? desc.color : desc.depth; passWidth_ = sizeTex ? (int)sizeTex.width : 0; passHeight_ = sizeTex ? (int)sizeTex.height : 0; @@ -542,7 +567,7 @@ void MetalRenderManager::BindRenderPass(const MetalRenderPassDesc &desc) { void MetalRenderManager::UnbindRenderPass() { EndRenderPass(); pass_ = MetalRenderPassDesc{}; - passIsBackbuffer_ = false; + passSwapchain_ = nullptr; curFormat_ = MetalRenderTargetFormat{}; } @@ -573,8 +598,8 @@ void MetalRenderManager::StartRenderPass() { return; } MetalRecordedPass rp; - rp.backbuffer = passIsBackbuffer_; - rp.color = passIsBackbuffer_ ? nullptr : (__bridge void *)pass_.color; + rp.swapchain = passSwapchain_; + rp.color = passSwapchain_ ? nullptr : (__bridge void *)pass_.color; rp.depth = (__bridge void *)pass_.depth; rp.msaaColor = (__bridge void *)pass_.msaaColor; rp.msaaDepth = (__bridge void *)pass_.msaaDepth; @@ -664,10 +689,10 @@ void MetalRenderManager::CopyTexture(__unsafe_unretained id src, int copy.size = size; } -void MetalRenderManager::CopyTextureToBuffer(__unsafe_unretained id src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id dst, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLBlitOption options) { +void MetalRenderManager::CopyTextureToBuffer(__unsafe_unretained id src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id dst, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLBlitOption options, MetalSwapchain *srcSwapchain) { MetalRecordedCopy © = RecordCopy(false); copy.type = MetalCopyType::TEXTURE_TO_BUFFER; - copy.srcIsBackbuffer = src == nil; + copy.srcSwapchain = srcSwapchain; copy.src = (__bridge void *)src; copy.dst = (__bridge void *)dst; copy.srcLevel = level; @@ -897,16 +922,19 @@ void MetalRenderManager::RenderThreadFunc() { } } -id MetalRenderManager::ReplayBackbuffer() { - if (!layer_) { - return offscreenBackbuffer_; +id MetalRenderManager::ReplaySwapchainTexture(MetalSwapchain *swapchain) { + if (!swapchain->layer) { + return swapchain->offscreen; } - if (!rtDrawable_) { + if (!swapchain->drawable) { @autoreleasepool { - rtDrawable_ = [layer_ nextDrawable]; + swapchain->drawable = [swapchain->layer nextDrawable]; + } + if (swapchain->drawable) { + rtAcquired_.push_back(swapchain); } } - return rtDrawable_ ? rtDrawable_.texture : nil; + return swapchain->drawable ? swapchain->drawable.texture : nil; } void MetalRenderManager::EndReplayEncoders() { @@ -953,8 +981,8 @@ void MetalRenderManager::ReplayDispatch(const MetalRecordedDispatch &dispatch, i } void MetalRenderManager::Replay(MetalCommandChunk *chunk) { - if (layer_ && chunk->newDrawableWidth > 0) { - layer_.drawableSize = CGSizeMake(chunk->newDrawableWidth, chunk->newDrawableHeight); + for (const auto &resize : chunk->drawableResizes) { + resize.swapchain->layer.drawableSize = CGSizeMake(resize.width, resize.height); } // The upload buffer is enqueued first, so it runs first even though they're committed together. @@ -994,33 +1022,36 @@ void MetalRenderManager::Replay(MetalCommandChunk *chunk) { } EndReplayEncoders(); + // The block keeps the drawables alive until the GPU is done with them. + NSMutableArray *drawables = nil; if (chunk->end == MetalChunkEnd::PRESENT) { + for (MetalSwapchain *swapchain : rtAcquired_) { #if PPSSPP_PLATFORM(MAC) - if (layer_ && layer_.displaySyncEnabled != chunk->vsync) { - layer_.displaySyncEnabled = chunk->vsync; - } + if (swapchain->layer.displaySyncEnabled != chunk->vsync) { + swapchain->layer.displaySyncEnabled = chunk->vsync; + } #endif - if (rtDrawable_) { - [cmd presentDrawable:rtDrawable_]; + [cmd presentDrawable:swapchain->drawable]; + if (!drawables) { + drawables = [NSMutableArray arrayWithCapacity:rtAcquired_.size()]; + } + [drawables addObject:swapchain->drawable]; + swapchain->drawable = nil; } + rtAcquired_.clear(); } dispatch_semaphore_t frameSemaphore = chunk->end == MetalChunkEnd::PRESENT ? frameSemaphore_ : nullptr; dispatch_semaphore_t completed = chunk->completed; - // The block keeps the drawable alive until the GPU is done with it. - id drawable = chunk->end == MetalChunkEnd::PRESENT ? rtDrawable_ : nil; if (frameSemaphore || completed) { [cmd addCompletedHandler:^(id buffer) { - (void)drawable; + (void)drawables; if (frameSemaphore) dispatch_semaphore_signal(frameSemaphore); if (completed) dispatch_semaphore_signal(completed); }]; } - if (chunk->end == MetalChunkEnd::PRESENT) { - rtDrawable_ = nil; - } [uploadCmd commit]; // A no-op if it's nil. [cmd commit]; } @@ -1031,8 +1062,6 @@ static inline void *BaseTexture(void *obj) { return parent ? (__bridge void *)parent : obj; } -static void *const BACKBUFFER_TARGET = (void *)1; - void MetalRenderManager::BuildSteps(const MetalCommandChunk &chunk) { rtStepCount_ = 0; auto newStep = [&]() -> MetalStep & { @@ -1058,7 +1087,8 @@ void MetalRenderManager::BuildSteps(const MetalCommandChunk &chunk) { pass->isPass = true; pass->index = c.pass; pass->ranges.push_back({ i + 1, i + 1 }); - pass->target = rp.backbuffer ? BACKBUFFER_TARGET : (rp.color ? rp.color : rp.depth); + pass->target = rp.swapchain ? (void *)rp.swapchain : (rp.color ? rp.color : rp.depth); + pass->targetIsSwapchain = rp.swapchain != nullptr; pass->targetDepth = rp.depth; pass->targetMsaa = rp.msaaColor ? rp.msaaColor : rp.msaaDepth; pass->usesDepth = rp.usesDepth; @@ -1080,8 +1110,9 @@ void MetalRenderManager::BuildSteps(const MetalCommandChunk &chunk) { step.target = copy.type == MetalCopyType::TEXTURE_TO_BUFFER ? copy.dst : BaseTexture(copy.dst); step.targetMsaa = nullptr; step.targetDepth = nullptr; - if (copy.srcIsBackbuffer) { - step.reads.push_back(BACKBUFFER_TARGET); + step.targetIsSwapchain = false; + if (copy.srcSwapchain) { + step.reads.push_back(copy.srcSwapchain); } else if (copy.src && copy.type != MetalCopyType::BUFFER_TO_TEXTURE) { step.reads.push_back(BaseTexture(copy.src)); } @@ -1109,7 +1140,7 @@ void MetalRenderManager::MergeSteps(MetalCommandChunk &chunk) { std::vector touched; for (size_t i = 0; i < rtStepCount_; i++) { MetalStep &si = rtSteps_[i]; - if (!si.isPass || si.merged || si.target == BACKBUFFER_TARGET) { + if (!si.isPass || si.merged || si.targetIsSwapchain) { continue; } touched.clear(); @@ -1163,8 +1194,8 @@ void MetalRenderManager::ReplayPass(const MetalCommandChunk &chunk, MetalStep &s rtBlit_ = nil; } const MetalRecordedPass &pass = chunk.passes[step.index]; - id color = pass.backbuffer ? ReplayBackbuffer() : (__bridge id)pass.color; - if (pass.backbuffer && !color) { + id color = pass.swapchain ? ReplaySwapchainTexture(pass.swapchain) : (__bridge id)pass.color; + if (pass.swapchain && !color) { // No drawable (the window isn't visible?). Drop the pass. return; } @@ -1296,7 +1327,7 @@ void MetalRenderManager::ReplayCopy(const MetalCommandChunk &chunk, const MetalR break; case MetalCopyType::TEXTURE_TO_BUFFER: { - id src = copy.srcIsBackbuffer ? ReplayBackbuffer() : (__bridge id)copy.src; + id src = copy.srcSwapchain ? ReplaySwapchainTexture(copy.srcSwapchain) : (__bridge id)copy.src; if (!src) { break; } diff --git a/Common/GPU/Metal/thin3d_metal.mm b/Common/GPU/Metal/thin3d_metal.mm index d45e5d0b77..f64fbc8b4c 100644 --- a/Common/GPU/Metal/thin3d_metal.mm +++ b/Common/GPU/Metal/thin3d_metal.mm @@ -404,6 +404,23 @@ private: std::string tag_; }; +class MetalSwapchainFB : public Swapchain { +public: + MetalSwapchainFB(MetalSwapchain *swapchain, const char *tag) : swapchain(swapchain), tag_(tag ? tag : "") { + UpdateSize(); + } + void UpdateSize() { + width_ = swapchain->width; + height_ = swapchain->height; + } + const char *Tag() const override { return tag_.c_str(); } + + MetalSwapchain *swapchain; + +private: + std::string tag_; +}; + // Bytes only, so there's no padding for memcmp to compare. struct MetalDepthStencilKey { uint8_t depthTest; @@ -453,9 +470,13 @@ public: bool BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dst, int dstX1, int dstY1, int dstX2, int dstY2, Aspect aspects, FBBlitFilter filter, const char *tag) override; bool CopyFramebufferToMemory(Framebuffer *src, Aspect aspect, int x, int y, int w, int h, Draw::DataFormat format, void *pixels, int pixelStride, ReadbackMode mode, const char *tag) override; DataFormat PreferredFramebufferReadbackFormat(Framebuffer *src) override { - return src ? DataFormat::R8G8B8A8_UNORM : DataFormat::B8G8R8A8_UNORM; + return ToSwapchain(src) ? DataFormat::B8G8R8A8_UNORM : DataFormat::R8G8B8A8_UNORM; } + Swapchain *CreateSwapchain(const SwapchainDesc &desc, std::string *error) override; + void DestroySwapchain(Swapchain *swapchain) override; + void ResizeSwapchain(Swapchain *swapchain, int width, int height) override; + void BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp, const char *tag) override; void BindFramebufferAsTexture(Framebuffer *fbo, int binding, Aspect aspect, int layer) override; void BindFramebufferSamplesAsTexture(Framebuffer *fbo, int binding, int layer) override; @@ -520,6 +541,13 @@ private: id GetDepthStencilState(const DepthStencilStateDesc &desc); void UploadTextureData(MetalTexture *tex, int level, int x, int y, int w, int h, const uint8_t *data, int srcStride, TextureCallback callback, bool useUploadBuffer); void ResetViewportScissor(); + // nullptr and the primary swapchain mean the backbuffer. Returns nullptr for an ordinary framebuffer. + MetalSwapchain *ToSwapchain(Framebuffer *fb) const { + if (!fb || fb == primarySwapchain_) { + return rm_->Backbuffer(); + } + return fb->IsSwapchain() ? ((MetalSwapchainFB *)fb)->swapchain : nullptr; + } MetalRenderManager *rm_; id device_; @@ -621,6 +649,7 @@ MetalDrawContext::MetalDrawContext(MetalRenderManager *renderManager) : rm_(rend #else caps_.presentModesSupported = PresentMode::FIFO; #endif + caps_.multipleSwapchains = true; caps_.multiSampleLevelsMask = 1; for (int level = 1; level <= 3; level++) { if ([device_ supportsTextureSampleCount:1 << level]) { @@ -1014,6 +1043,7 @@ Framebuffer *MetalDrawContext::CreateFramebuffer(const FramebufferDesc &desc) { } void MetalDrawContext::CopyFramebufferImage(Framebuffer *srcfb, int level, int x, int y, int z, Framebuffer *dstfb, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, Aspect aspects, const char *tag) { + _dbg_assert_(srcfb && !srcfb->IsSwapchain() && dstfb && !dstfb->IsSwapchain()); MetalFramebuffer *src = (MetalFramebuffer *)srcfb; MetalFramebuffer *dst = (MetalFramebuffer *)dstfb; @@ -1083,13 +1113,21 @@ bool MetalDrawContext::BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, i } bool MetalDrawContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspect, int x, int y, int w, int h, Draw::DataFormat destFormat, void *pixels, int pixelStride, ReadbackMode mode, const char *tag) { - MetalFramebuffer *fb = (MetalFramebuffer *)srcfb; - // nil with !fb means the backbuffer (the drawable is only known when the commands are encoded). + MetalSwapchain *swapchain = ToSwapchain(srcfb); + // A swapchain's texture is only known when the commands are encoded. id srcTex; - int texW = rm_->BackbufferWidth(); - int texH = rm_->BackbufferHeight(); + int texW; + int texH; MTLPixelFormat srcFormat = rm_->BackbufferFormat(); - if (fb) { + if (swapchain) { + if (aspect != Aspect::COLOR_BIT || !swapchain->drawn) { + // Like headless Vulkan, which has no backbuffer. The GE debugger shows the PSP's framebuffer instead. + return false; + } + texW = swapchain->width; + texH = swapchain->height; + } else { + MetalFramebuffer *fb = (MetalFramebuffer *)srcfb; srcTex = aspect == Aspect::COLOR_BIT ? fb->color : fb->depth; if (!srcTex) { return false; @@ -1097,11 +1135,6 @@ bool MetalDrawContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspect texW = (int)srcTex.width; texH = (int)srcTex.height; srcFormat = srcTex.pixelFormat; - } else if (aspect != Aspect::COLOR_BIT || !rm_->BackbufferDrawn()) { - // Like headless Vulkan, which has no backbuffer. The GE debugger shows the PSP's framebuffer instead. - return false; - } else if (rm_->OffscreenBackbuffer()) { - srcTex = rm_->OffscreenBackbuffer(); } if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } @@ -1127,7 +1160,7 @@ bool MetalDrawContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspect readbackBuffer_.label = @"readback"; } - rm_->CopyTextureToBuffer(srcTex, 0, MTLOriginMake(x, y, 0), MTLSizeMake(w, h, 1), readbackBuffer_, 0, srcStride, (uint32_t)size, options); + rm_->CopyTextureToBuffer(srcTex, 0, MTLOriginMake(x, y, 0), MTLSizeMake(w, h, 1), readbackBuffer_, 0, srcStride, (uint32_t)size, options, swapchain); rm_->Flush(true); const uint8_t *src = (const uint8_t *)readbackBuffer_.contents; @@ -1172,8 +1205,11 @@ void MetalDrawContext::ResetViewportScissor() { } void MetalDrawContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp, const char *tag) { - MetalFramebuffer *fb = (MetalFramebuffer *)fbo; - if (fb) { + MetalSwapchain *swapchain = ToSwapchain(fbo); + if (swapchain) { + rm_->BindBackbufferPass(swapchain, RPActionToMetal(rp.color), ClearColorFromRGBA(rp.clearColor), tag); + } else { + MetalFramebuffer *fb = (MetalFramebuffer *)fbo; MetalRenderPassDesc desc{}; desc.color = fb->color; desc.depth = fb->depth; @@ -1187,8 +1223,6 @@ void MetalDrawContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const Ren desc.clearStencil = rp.clearStencil; desc.tag = tag; rm_->BindRenderPass(desc); - } else { - rm_->BindBackbufferPass(RPActionToMetal(rp.color), ClearColorFromRGBA(rp.clearColor), tag); } ResetViewportScissor(); if (invalidationCallback_) { @@ -1198,6 +1232,7 @@ void MetalDrawContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const Ren void MetalDrawContext::BindFramebufferAsTexture(Framebuffer *fbo, int binding, Aspect aspect, int layer) { _dbg_assert_(binding < (int)MAX_TEXTURE_SLOTS); + _dbg_assert_(fbo && !fbo->IsSwapchain()); MetalFramebuffer *fb = (MetalFramebuffer *)fbo; id tex; switch (aspect) { @@ -1218,12 +1253,45 @@ void MetalDrawContext::BindFramebufferSamplesAsTexture(Framebuffer *fbo, int bin } void MetalDrawContext::GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) { - if (fbo) { + if (MetalSwapchain *swapchain = ToSwapchain(fbo)) { + *w = swapchain->width; + *h = swapchain->height; + } else { *w = fbo->Width(); *h = fbo->Height(); + } +} + +Swapchain *MetalDrawContext::CreateSwapchain(const SwapchainDesc &desc, std::string *error) { + if (desc.winsys != WINDOWSYSTEM_METAL_EXT) { + *error = "Metal swapchains need a CAMetalLayer"; + return nullptr; + } + // A null layer gives an offscreen swapchain of the given size, like headless. + CAMetalLayer *layer = (__bridge CAMetalLayer *)desc.data1; + if (!layer && (desc.width <= 0 || desc.height <= 0)) { + *error = "No size for an offscreen swapchain"; + return nullptr; + } + return new MetalSwapchainFB(rm_->CreateSwapchain(layer, desc.width, desc.height), desc.tag); +} + +void MetalDrawContext::DestroySwapchain(Swapchain *swapchain) { + _dbg_assert_(swapchain && swapchain != primarySwapchain_); + MetalSwapchainFB *fb = (MetalSwapchainFB *)swapchain; + rm_->DestroySwapchain(fb->swapchain); + fb->swapchain = nullptr; + fb->Release(); +} + +void MetalDrawContext::ResizeSwapchain(Swapchain *swapchain, int width, int height) { + MetalSwapchain *metalSwapchain = ToSwapchain(swapchain); + _dbg_assert_(metalSwapchain); + rm_->Resize(metalSwapchain, width, height); + if (swapchain == primarySwapchain_) { + primarySwapchain_->SetSize(metalSwapchain->width, metalSwapchain->height); } else { - *w = rm_->BackbufferWidth(); - *h = rm_->BackbufferHeight(); + ((MetalSwapchainFB *)swapchain)->UpdateSize(); } } @@ -1535,6 +1603,7 @@ void MetalDrawContext::HandleEvent(Event ev, int width, int height, void *param1 break; case Event::GOT_BACKBUFFER: rm_->Resize(width, height); + primarySwapchain_->SetSize(rm_->BackbufferWidth(), rm_->BackbufferHeight()); break; default: break; diff --git a/Common/GPU/OpenGL/thin3d_gl.cpp b/Common/GPU/OpenGL/thin3d_gl.cpp index 497970a3ae..2fc87b164f 100644 --- a/Common/GPU/OpenGL/thin3d_gl.cpp +++ b/Common/GPU/OpenGL/thin3d_gl.cpp @@ -333,6 +333,7 @@ public: void SetTargetSize(int w, int h) override { DrawContext::SetTargetSize(w, h); renderManager_.Resize(w, h); + primarySwapchain_->SetSize(w, h); } const DeviceCaps &GetDeviceCaps() const override { @@ -1047,6 +1048,7 @@ static void LogReadPixelsError(GLenum error) { #endif bool OpenGLContext::CopyFramebufferToMemory(Framebuffer *src, Aspect channelBits, int x, int y, int w, int h, Draw::DataFormat dataFormat, void *pixels, int pixelStride, ReadbackMode mode, const char *tag) { + src = PrimaryToNull(src); if (gl_extensions.IsGLES && (channelBits & Aspect::COLOR_BIT) == 0) { // Can't readback depth or stencil on GLES. return false; @@ -1584,6 +1586,7 @@ Framebuffer *OpenGLContext::CreateFramebuffer(const FramebufferDesc &desc) { } void OpenGLContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp, const char *tag) { + fbo = PrimaryToNull(fbo); OpenGLFramebuffer *fb = (OpenGLFramebuffer *)fbo; GLRRenderPassAction color = (GLRRenderPassAction)rp.color; GLRRenderPassAction depth = (GLRRenderPassAction)rp.depth; @@ -1663,6 +1666,7 @@ void OpenGLContext::BindFramebufferAsTexture(Framebuffer *fbo, int binding, Aspe } void OpenGLContext::GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) { + fbo = PrimaryToNull(fbo); OpenGLFramebuffer *fb = (OpenGLFramebuffer *)fbo; if (fb) { *w = fb->Width(); diff --git a/Common/GPU/Vulkan/thin3d_vulkan.cpp b/Common/GPU/Vulkan/thin3d_vulkan.cpp index c98b7611df..7af0d6814f 100644 --- a/Common/GPU/Vulkan/thin3d_vulkan.cpp +++ b/Common/GPU/Vulkan/thin3d_vulkan.cpp @@ -1894,6 +1894,7 @@ bool VKContext::BlitFramebuffer(Framebuffer *srcfb, int srcX1, int srcY1, int sr } bool VKContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspects, int x, int y, int w, int h, Draw::DataFormat format, void *pixels, int pixelStride, ReadbackMode mode, const char *tag) { + srcfb = PrimaryToNull(srcfb); VKFramebuffer *src = (VKFramebuffer *)srcfb; if (!src && vulkan_->GetSwapchain() == VK_NULL_HANDLE) { // No backbuffer to read, as in headless. @@ -1909,6 +1910,7 @@ bool VKContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspects, int } DataFormat VKContext::PreferredFramebufferReadbackFormat(Framebuffer *src) { + src = PrimaryToNull(src); if (src) { return DrawContext::PreferredFramebufferReadbackFormat(src); } @@ -1920,6 +1922,7 @@ DataFormat VKContext::PreferredFramebufferReadbackFormat(Framebuffer *src) { } void VKContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp, const char *tag) { + fbo = PrimaryToNull(fbo); _dbg_assert_(fbo != nullptr || equals(tag, "BackBuffer")) VKFramebuffer *fb = (VKFramebuffer *)fbo; @@ -1966,6 +1969,7 @@ void VKContext::BindFramebufferSamplesAsTexture(Framebuffer *fbo, int binding, i } void VKContext::GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) { + fbo = PrimaryToNull(fbo); VKFramebuffer *fb = (VKFramebuffer *)fbo; if (fb) { *w = fb->GetFB()->width; @@ -1983,6 +1987,7 @@ void VKContext::HandleEvent(Event ev, int width, int height, void *param1, void break; case Event::GOT_BACKBUFFER: renderManager_.CreateBackbuffers(); + primarySwapchain_->SetSize(vulkan_->GetBackbufferWidth(), vulkan_->GetBackbufferHeight()); break; default: _assert_(false); diff --git a/Common/GPU/thin3d.h b/Common/GPU/thin3d.h index 7445fe2317..b45e23ca0c 100644 --- a/Common/GPU/thin3d.h +++ b/Common/GPU/thin3d.h @@ -454,12 +454,40 @@ public: virtual void UpdateTag(const char *tag) {} virtual const char *Tag() const { return "(no name)"; } + virtual bool IsSwapchain() const { return false; } protected: int width_ = -1, height_ = -1, layers_ = 1, multiSampleLevel_ = 0; bool hasDepthStencil_ = false; }; +// Something we present to other than the primary backbuffer, like a second window or an external display. +// Render to it and read it back like a Framebuffer, but don't texture or blit from it. Its size follows +// the surface (see ResizeSwapchain). Created and destroyed through the DrawContext. +class Swapchain : public Framebuffer { +public: + bool IsSwapchain() const override { return true; } +}; + +// The backbuffer, which every DrawContext has. The backends keep its size. +class PrimarySwapchain : public Swapchain { +public: + void SetSize(int width, int height) { + width_ = width; + height_ = height; + } + const char *Tag() const override { return "BackBuffer"; } +}; + +struct SwapchainDesc { + WindowSystem winsys; + void *data1; // Like GraphicsContext::InitSurface. For WINDOWSYSTEM_METAL_EXT, the CAMetalLayer. + void *data2; + int width; // In pixels. + int height; + const char *tag; +}; + class Buffer : public RefCountedObject { public: Buffer() : RefCountedObject("Buffer") {} @@ -639,6 +667,7 @@ struct DeviceCaps { int presentMaxInterval; // 1 on many backends bool presentInstantModeChange; PresentMode presentModesSupported; + bool multipleSwapchains = false; // CreateSwapchain works. u32 multiSampleLevelsMask; // Bit n is set if (1 << n) is a valid multisample level. Bit 0 is always set. std::string deviceName; // The device name to use when creating the thin3d context, to get the same one. @@ -737,7 +766,10 @@ struct ClippedDraw { class DrawContext { public: - virtual ~DrawContext() = default; + DrawContext() : primarySwapchain_(new PrimarySwapchain()) {} + virtual ~DrawContext() { + primarySwapchain_->Release(); + } bool CreatePresets(); void DestroyPresets(); @@ -820,8 +852,22 @@ public: return DataFormat::R8G8B8A8_UNORM; } + // The backbuffer. Bind it, measure it and read it back like any other swapchain. Where a Framebuffer + // is taken, nullptr also means the backbuffer. + Swapchain *GetPrimarySwapchain() const { return primarySwapchain_; } + + // Additional swapchains, presented along with the backbuffer by Present(). Create and destroy them + // between frames, on the thread that renders. Check caps.multipleSwapchains. + virtual Swapchain *CreateSwapchain(const SwapchainDesc &desc, std::string *error) { + *error = "Not supported"; + return nullptr; + } + virtual void DestroySwapchain(Swapchain *swapchain) {} + virtual void ResizeSwapchain(Swapchain *swapchain, int width, int height) {} + // These functions should be self explanatory. - // Binding a zero render target means binding the backbuffer. + // A Swapchain (the backbuffer included) can be bound like a framebuffer, once per frame, and is presented + // by Present(). // If an fbo has two layers, we bind for stereo rendering ALWAYS. There's no rendering to one layer anymore. virtual void BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp, const char *tag) = 0; @@ -933,6 +979,12 @@ public: } protected: + // The backends' backbuffer code goes by nullptr. + Framebuffer *PrimaryToNull(Framebuffer *fb) const { + return fb == primarySwapchain_ ? nullptr : fb; + } + + PrimarySwapchain *primarySwapchain_; HistoryBuffer frameTimeHistory_; ShaderModule *vsPresets_[VS_MAX_PRESET]{}; diff --git a/Common/UI/ScreenManager.cpp b/Common/UI/ScreenManager.cpp index bf741db430..dbcb111048 100644 --- a/Common/UI/ScreenManager.cpp +++ b/Common/UI/ScreenManager.cpp @@ -212,8 +212,8 @@ ScreenRenderFlags ScreenManager::Render(std::function afterPreRender) { afterPreRender(); - // Now, start the final render pass. This is now the ONLY place where binding the null fb is allowed. - draw_->BindFramebufferAsRenderTarget(nullptr, {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer"); + // 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(); const Draw::Viewport viewport{0.0f, 0.0f, (float)g_display.pixel_xres, (float)g_display.pixel_yres, 0.0f, 1.0f}; diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index 9013b70816..956e7f71c0 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -3448,8 +3448,8 @@ bool FramebufferManagerCommon::GetStencilbuffer(u32 fb_address, int fb_stride, G bool GetOutputFramebuffer(Draw::DrawContext *draw, GPUDebugBuffer &buffer) { int w, h; - draw->GetFramebufferDimensions(nullptr, &w, &h); - Draw::DataFormat fmt = draw->PreferredFramebufferReadbackFormat(nullptr); + draw->GetFramebufferDimensions(draw->GetPrimarySwapchain(), &w, &h); + Draw::DataFormat fmt = draw->PreferredFramebufferReadbackFormat(draw->GetPrimarySwapchain()); // Ignore preferred formats other than BGRA. _dbg_assert_(fmt == Draw::DataFormat::B8G8R8A8_UNORM || fmt == Draw::DataFormat::R8G8B8A8_UNORM); if (fmt != Draw::DataFormat::B8G8R8A8_UNORM) @@ -3459,7 +3459,7 @@ bool GetOutputFramebuffer(Draw::DrawContext *draw, GPUDebugBuffer &buffer) { buffer.Allocate(w, h, fmt == Draw::DataFormat::R8G8B8A8_UNORM ? GPU_DBG_FORMAT_8888 : GPU_DBG_FORMAT_8888_BGRA, flipY); buffer.SetIsBackbuffer(true); - return draw->CopyFramebufferToMemory(nullptr, Draw::Aspect::COLOR_BIT, 0, 0, w, h, fmt, buffer.GetData(), w, Draw::ReadbackMode::BLOCK, "GetOutputFramebuffer"); + return draw->CopyFramebufferToMemory(draw->GetPrimarySwapchain(), Draw::Aspect::COLOR_BIT, 0, 0, w, h, fmt, buffer.GetData(), w, Draw::ReadbackMode::BLOCK, "GetOutputFramebuffer"); } bool FramebufferManagerCommon::GetOutputFramebuffer(GPUDebugBuffer &buffer) { diff --git a/GPU/GLES/FramebufferManagerGLES.cpp b/GPU/GLES/FramebufferManagerGLES.cpp index 7e9d040924..da8612d5a9 100644 --- a/GPU/GLES/FramebufferManagerGLES.cpp +++ b/GPU/GLES/FramebufferManagerGLES.cpp @@ -51,8 +51,8 @@ void FramebufferManagerGLES::NotifyDisplayResized() { bool FramebufferManagerGLES::GetOutputFramebuffer(GPUDebugBuffer &buffer) { int w, h; - draw_->GetFramebufferDimensions(nullptr, &w, &h); + draw_->GetFramebufferDimensions(draw_->GetPrimarySwapchain(), &w, &h); buffer.Allocate(w, h, GPU_DBG_FORMAT_888_RGB, true); - draw_->CopyFramebufferToMemory(nullptr, Draw::Aspect::COLOR_BIT, 0, 0, w, h, Draw::DataFormat::R8G8B8_UNORM, buffer.GetData(), w, Draw::ReadbackMode::BLOCK, "GetOutputFramebuffer"); + draw_->CopyFramebufferToMemory(draw_->GetPrimarySwapchain(), Draw::Aspect::COLOR_BIT, 0, 0, w, h, Draw::DataFormat::R8G8B8_UNORM, buffer.GetData(), w, Draw::ReadbackMode::BLOCK, "GetOutputFramebuffer"); return true; } diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 8b47928e1e..6e18dd1282 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1878,7 +1878,7 @@ ScreenRenderFlags EmuScreen::RunEmulation(bool skipBufferEffects) { // The right time to run this if (ScreenshotNotifyPostGameRender(draw) && skipBufferEffects) { // Need to restore the backbuffer render pass, it probably got destroyed. - draw->BindFramebufferAsRenderTarget(nullptr, {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer"); + draw->BindFramebufferAsRenderTarget(draw->GetPrimarySwapchain(), {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer"); } } } diff --git a/docs/multi-swapchain.md b/docs/multi-swapchain.md index 00a61c9dd7..28d575f64e 100644 --- a/docs/multi-swapchain.md +++ b/docs/multi-swapchain.md @@ -82,10 +82,11 @@ class DrawContext { } ``` -- **`nullptr` goes away in two steps.** First it's an alias for `GetPrimarySwapchain()`, so nothing - breaks. Then the callers move over (`ScreenManager::Render` is the one bind, plus the readback and - dimension calls) and `nullptr` becomes an assert. Backends check `IsSwapchain()` instead of - comparing with null. +- **`nullptr` is an alias for the primary,** so nothing breaks. The direct callers use + `GetPrimarySwapchain()`. Some code still passes a `Framebuffer *` that may be null (non-buffered + rendering, for one), so `nullptr` can only become an assert after that's audited. Backends that + support one swapchain map the primary to their old `nullptr` path (`PrimaryToNull`). Metal treats + all its swapchains the same way. - **Size changes on resize.** Unlike other framebuffers, a swapchain's `Width()`/`Height()` change when its surface does, and only at a frame boundary. - **Platform code keeps the primary out of its hands.** `GraphicsContext::InitSurface` still creates diff --git a/headless/Headless.cpp b/headless/Headless.cpp index f8336af38a..b2bbca4c73 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -692,7 +692,7 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar if (gpu) { gpu->SetCurFramebufferDirty(true); gpu->PrepareCopyDisplayToOutput(g_Config.GetDisplayLayoutConfig(DeviceOrientation::Landscape)); - draw->BindFramebufferAsRenderTarget(nullptr, {RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE}, "BackBuffer"); + draw->BindFramebufferAsRenderTarget(draw->GetPrimarySwapchain(), {RPAction::CLEAR, RPAction::DONT_CARE, RPAction::DONT_CARE}, "BackBuffer"); gpu->CopyDisplayToOutput(g_Config.GetDisplayLayoutConfig(DeviceOrientation::Landscape)); } diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index e24ffd82f0..a75003c70b 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -1396,7 +1396,7 @@ namespace Libretro { // gotta do the backbuffer bind somewhere. using namespace Draw; if (draw) { - draw->BindFramebufferAsRenderTarget(nullptr, {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer"); + draw->BindFramebufferAsRenderTarget(draw->GetPrimarySwapchain(), {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer"); } if (gpu && draw) {