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) {