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

No files matched your search

+4
View File
@@ -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();
+48 -34
View File
@@ -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<MTLTexture> offscreen;
id<MTLTexture> 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<CAMetalDrawable> 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<std::pair<uint32_t, uint32_t>> 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<void *> reads; // Textures sampled by the pass, or the copy's source.
@@ -305,8 +322,12 @@ struct MetalCommandChunk {
std::vector<MetalRecordedDispatch> 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<DrawableResize> 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<MTLTexture> 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<MTLTexture> src, int srcLevel, MTLOrigin srcOrigin, MTLSize size, __unsafe_unretained id<MTLTexture> dst, int dstLevel, MTLOrigin dstOrigin);
// src nil means the backbuffer.
void CopyTextureToBuffer(__unsafe_unretained id<MTLTexture> src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id<MTLBuffer> 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<MTLTexture> src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id<MTLBuffer> dst, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLBlitOption options, MetalSwapchain *srcSwapchain = nullptr);
void CopyBufferToTexture(__unsafe_unretained id<MTLBuffer> src, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLSize size, __unsafe_unretained id<MTLTexture> dst, int slice, int level, MTLOrigin origin, MTLBlitOption options, bool upload);
void GenerateMips(__unsafe_unretained id<MTLTexture> 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 &copy, id<MTLCommandBuffer> cmd, bool upload);
void EndReplayEncoders();
void ReplayDispatch(const MetalRecordedDispatch &dispatch, id<MTLCommandBuffer> cmd);
id<MTLTexture> ReplayBackbuffer();
id<MTLTexture> ReplaySwapchainTexture(MetalSwapchain *swapchain);
id<MTLDevice> device_;
id<MTLCommandQueue> queue_;
CAMetalLayer *layer_ = nil;
bool unifiedMemory_ = false;
bool appleGPU_ = false;
@@ -480,16 +499,11 @@ private:
// Recording state (emu thread).
MetalCommandChunk *chunk_ = nullptr;
id<MTLTexture> offscreenBackbuffer_;
bool backbufferDrawn_ = false;
id<MTLTexture> bbDepth_;
int bbWidth_ = 0;
int bbHeight_ = 0;
int pendingDrawableWidth_ = 0;
int pendingDrawableHeight_ = 0;
std::vector<MetalSwapchain *> 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<MTLBlitCommandEncoder> rtBlit_;
id<MTLBlitCommandEncoder> rtUploadBlit_;
id<MTLComputeCommandEncoder> rtUploadCompute_;
id<CAMetalDrawable> rtDrawable_;
std::vector<MetalSwapchain *> rtAcquired_; // Swapchains with a drawable to present.
// Replay scratch, reused between chunks.
std::vector<MetalStep> rtSteps_;
size_t rtStepCount_ = 0;
+118 -87
View File
@@ -301,7 +301,7 @@ void RunOnCompileThread(std::function<void()> work) {
}
@end
MetalRenderManager::MetalRenderManager(id<MTLDevice> device, CAMetalLayer *layer, int offscreenWidth, int offscreenHeight, bool useThread) : device_(device), layer_(layer) {
MetalRenderManager::MetalRenderManager(id<MTLDevice> 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<MTLDevice> 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<MTLTexture> 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<MTLTexture> src, int
copy.size = size;
}
void MetalRenderManager::CopyTextureToBuffer(__unsafe_unretained id<MTLTexture> src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id<MTLBuffer> dst, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLBlitOption options) {
void MetalRenderManager::CopyTextureToBuffer(__unsafe_unretained id<MTLTexture> src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id<MTLBuffer> dst, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLBlitOption options, MetalSwapchain *srcSwapchain) {
MetalRecordedCopy &copy = 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<MTLTexture> MetalRenderManager::ReplayBackbuffer() {
if (!layer_) {
return offscreenBackbuffer_;
id<MTLTexture> 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<CAMetalDrawable> drawable = chunk->end == MetalChunkEnd::PRESENT ? rtDrawable_ : nil;
if (frameSemaphore || completed) {
[cmd addCompletedHandler:^(id<MTLCommandBuffer> 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<void *> 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<MTLTexture> color = pass.backbuffer ? ReplayBackbuffer() : (__bridge id<MTLTexture>)pass.color;
if (pass.backbuffer && !color) {
id<MTLTexture> color = pass.swapchain ? ReplaySwapchainTexture(pass.swapchain) : (__bridge id<MTLTexture>)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<MTLTexture> src = copy.srcIsBackbuffer ? ReplayBackbuffer() : (__bridge id<MTLTexture>)copy.src;
id<MTLTexture> src = copy.srcSwapchain ? ReplaySwapchainTexture(copy.srcSwapchain) : (__bridge id<MTLTexture>)copy.src;
if (!src) {
break;
}
+88 -19
View File
@@ -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<MTLDepthStencilState> 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<MTLDevice> 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<MTLTexture> 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<MTLTexture> 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;
+4
View File
@@ -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();
+5
View File
@@ -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);
+54 -2
View File
@@ -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<FrameTimeData, FRAME_TIME_HISTORY_LENGTH> frameTimeHistory_;
ShaderModule *vsPresets_[VS_MAX_PRESET]{};
+2 -2
View File
@@ -212,8 +212,8 @@ ScreenRenderFlags ScreenManager::Render(std::function<void()> 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};
+3 -3
View File
@@ -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) {
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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");
}
}
}
+5 -4
View File
@@ -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
+1 -1
View File
@@ -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));
}
+1 -1
View File
@@ -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) {