diff --git a/Engine/gapi/abstractgraphicsapi.cpp b/Engine/gapi/abstractgraphicsapi.cpp index d92ef351..b10cc183 100644 --- a/Engine/gapi/abstractgraphicsapi.cpp +++ b/Engine/gapi/abstractgraphicsapi.cpp @@ -131,3 +131,11 @@ std::shared_ptr AbstractGraphicsApi::submit(Device* NonUniqResId AbstractGraphicsApi::Swapchain::syncId() const { return NonUniqResId(0x1); } + +AbstractGraphicsApi::PSwapchainImage AbstractGraphicsApi::Swapchain::next() { + throw std::system_error(GraphicsErrc::UnsupportedExtension); + } + +void AbstractGraphicsApi::present(Device*, SwapchainImage*) { + throw std::system_error(GraphicsErrc::UnsupportedExtension); + } diff --git a/Engine/gapi/abstractgraphicsapi.h b/Engine/gapi/abstractgraphicsapi.h index 7938dfe0..0164698d 100644 --- a/Engine/gapi/abstractgraphicsapi.h +++ b/Engine/gapi/abstractgraphicsapi.h @@ -519,6 +519,15 @@ namespace Tempest { virtual void setPayload(std::vector>&&) {} }; + struct SwapchainImage:Shared { + SwapchainImage(Device& device, uint32_t w, uint32_t h):device(device),width(w),height(h) {} + + Device& device; + const uint32_t width; + const uint32_t height; + }; + using PSwapchainImage = Detail::DSharedPtr; + struct Swapchain:NoCopy { virtual ~Swapchain()=default; virtual void reset()=0; @@ -527,6 +536,7 @@ namespace Tempest { virtual uint32_t imageCount() const=0; virtual uint32_t w() const=0; virtual uint32_t h() const=0; + virtual PSwapchainImage next(); }; struct Texture:Shared { virtual uint32_t mipCount() const = 0; @@ -663,6 +673,7 @@ namespace Tempest { virtual void readBytes (Device* d, Buffer* buf, void* out, size_t size) = 0; virtual void present(Device *d, Swapchain* sw) = 0; + virtual void present(Device *d, SwapchainImage* image); virtual auto submit (Device *d, CommandBuffer* cmd) -> std::shared_ptr = 0; virtual void getCaps(Device *d, Props& caps)=0; @@ -689,6 +700,7 @@ namespace Detail { AttachmentDesc desc [MaxFramebufferAttachments+1] = {}; AbstractGraphicsApi::Texture* att [MaxFramebufferAttachments+1] = {}; AbstractGraphicsApi::Swapchain* sw [MaxFramebufferAttachments+1] = {}; + AbstractGraphicsApi::SwapchainImage* image[MaxFramebufferAttachments+1] = {}; uint32_t imgId[MaxFramebufferAttachments+1] = {}; }; diff --git a/Engine/gapi/metal/mtcommandbuffer.cpp b/Engine/gapi/metal/mtcommandbuffer.cpp index 2997f449..d02bb884 100644 --- a/Engine/gapi/metal/mtcommandbuffer.cpp +++ b/Engine/gapi/metal/mtcommandbuffer.cpp @@ -10,6 +10,7 @@ #include "mttexture.h" #include "mtswapchain.h" #include "mtaccelerationstructure.h" +#include using namespace Tempest; using namespace Tempest::Detail; @@ -65,6 +66,7 @@ void MtCommandBuffer::end() { } void MtCommandBuffer::reset() { + swapchainImages.clear(); auto pool = NsPtr::init(); auto desc = NsPtr::init(); desc->setRetainedReferences(false); @@ -92,9 +94,19 @@ void MtCommandBuffer::beginRendering(const FrameBufferDesc& fbo, size_t fboSize, continue; } auto clr = desc->colorAttachments()->object(i); - if(fbo.sw[i]!=nullptr) { + if(fbo.image[i]!=nullptr) { + auto& image = *static_cast(fbo.image[i]); + if(&image.device!=&device) + throw std::system_error(GraphicsErrc::InvalidTexture); + auto* texture = image.drawable->texture(); + clr->setTexture(texture); + curFbo.colorFormat[curFbo.numColors] = texture->pixelFormat(); + auto found = std::find_if(swapchainImages.begin(),swapchainImages.end(),[&](const auto& i){ return i.handler==ℑ }); + if(found==swapchainImages.end()) + swapchainImages.emplace_back(&image); + } else if(fbo.sw[i]!=nullptr) { auto& s = *reinterpret_cast(fbo.sw[i]); - clr->setTexture(s.img[fbo.imgId[i]].tex.get()); + clr->setTexture(s.image(fbo.imgId[i])); curFbo.colorFormat[curFbo.numColors] = s.format(); } else { auto& t = *reinterpret_cast(fbo.att[i]); diff --git a/Engine/gapi/metal/mtcommandbuffer.h b/Engine/gapi/metal/mtcommandbuffer.h index 0d49c76c..b9074b86 100644 --- a/Engine/gapi/metal/mtcommandbuffer.h +++ b/Engine/gapi/metal/mtcommandbuffer.h @@ -113,6 +113,7 @@ class MtCommandBuffer : public AbstractGraphicsApi::CommandBuffer { NsPtr encBlit; std::vector usedResources; + std::vector swapchainImages; MtFboLayout curFbo; Push pushData; diff --git a/Engine/gapi/metal/mtswapchain.h b/Engine/gapi/metal/mtswapchain.h index 0f250bd1..86f74155 100644 --- a/Engine/gapi/metal/mtswapchain.h +++ b/Engine/gapi/metal/mtswapchain.h @@ -1,10 +1,10 @@ #pragma once #include -#include "utility/spinlock.h" #include "nsptr.h" #include +#include namespace CA { @@ -16,6 +16,13 @@ namespace Detail { class MtDevice; +struct MtSwapchainImage : AbstractGraphicsApi::SwapchainImage { + MtSwapchainImage(MtDevice& device, CA::MetalDrawable* drawable); + ~MtSwapchainImage(); + + NsPtr drawable; + }; + class MtSwapchain : public AbstractGraphicsApi::Swapchain { public: MtSwapchain(MtDevice& dev, SystemApi::Window* w); @@ -27,6 +34,9 @@ class MtSwapchain : public AbstractGraphicsApi::Swapchain { uint32_t w() const override; uint32_t h() const override; void present(); + AbstractGraphicsApi::PSwapchainImage next() override; + MTL::Texture* image(uint32_t id); + static void present(MtDevice& device, MtSwapchainImage& image); NonUniqResId syncId() const override { return NonUniqResId::I_None; } MTL::PixelFormat format() const; @@ -40,7 +50,7 @@ class MtSwapchain : public AbstractGraphicsApi::Swapchain { struct Impl; std::unique_ptr pimpl; - SpinLock sync; + std::mutex sync; MtDevice& dev; Tempest::Size sz; diff --git a/Engine/gapi/metal/mtswapchain.mm b/Engine/gapi/metal/mtswapchain.mm index e0eaf26c..4cc7e4ee 100644 --- a/Engine/gapi/metal/mtswapchain.mm +++ b/Engine/gapi/metal/mtswapchain.mm @@ -63,6 +63,15 @@ - (CALayer *)makeBackingLayer { } }; +MtSwapchainImage::MtSwapchainImage(MtDevice& device, CA::MetalDrawable* drawable) + :SwapchainImage(device,uint32_t(drawable->texture()->width()),uint32_t(drawable->texture()->height())), + drawable(drawable) { + drawable->retain(); + } + +MtSwapchainImage::~MtSwapchainImage() { + } + static float backingScaleFactor(SysWindow* w) { #if defined(__OSX__) return [w screen].backingScaleFactor; @@ -93,7 +102,7 @@ static CGRect windowRect(UIWindow* wnd) { // note : MoltenVK supports NSView, UIView, CAMetalLayer, so we should align to it MtSwapchain::MtSwapchain(MtDevice& dev, SystemApi::Window *w) - :dev(dev), pimpl(new Impl()) { + :pimpl(new Impl()), dev(dev) { NSObject* obj = reinterpret_cast(w); if([obj isKindOfClass : [SysWindow class]]) pimpl->wnd = reinterpret_cast(w); @@ -127,13 +136,14 @@ static CGRect windowRect(UIWindow* wnd) { } MtSwapchain::~MtSwapchain() { + dev.waitIdle(); if(pimpl->view!=nil) [pimpl->view release]; } void MtSwapchain::reset() { dev.waitIdle(); // pending commands - std::lock_guard guard(sync); + std::lock_guard guard(sync); // https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc CAMetalLayer* lay = pimpl->metalLayer(); @@ -146,8 +156,6 @@ static CGRect windowRect(UIWindow* wnd) { img.resize(imgCount); for(size_t i=0; i guard(sync); + if(sz.w<=0 || sz.h<=0) + throw SwapchainSuboptimal(); + auto pool = NsPtr::init(); + auto* layer = reinterpret_cast(pimpl->metalLayer()); + pimpl->metalLayer().allowsNextDrawableTimeout = YES; + auto* drawable = layer->nextDrawable(); + if(drawable==nullptr || drawable->texture()->width()!=size_t(sz.w) || drawable->texture()->height()!=size_t(sz.h)) + throw SwapchainSuboptimal(); + return AbstractGraphicsApi::PSwapchainImage(new MtSwapchainImage(dev,drawable)); + } + +MTL::Texture* MtSwapchain::image(uint32_t id) { + // Keep private textures only for callers of the indexed swapchain API. + if(img[id].tex==nullptr) + img[id].tex = mkTexture(); + return img[id].tex.get(); + } + +void MtSwapchain::present(MtDevice& device, MtSwapchainImage& image) { + auto pool = NsPtr::init(); + auto cmd = device.queue->commandBuffer(); + cmd->presentDrawable(image.drawable.get()); + auto* dev = &device; + dev->onSubmit(); + cmd->addCompletedHandler(^(MTL::CommandBuffer* c){ + if(c->status()!=MTL::CommandBufferStatusCompleted) + Log::e("swapchain fatal error"); + dev->onFinish(); + }); + cmd->commit(); + } + void MtSwapchain::present() { auto pool = NsPtr::init(); CA::MetalLayer* lay = reinterpret_cast(pimpl->metalLayer()); uint32_t i = currentImg; + pimpl->metalLayer().allowsNextDrawableTimeout = NO; auto drawable = lay->nextDrawable(); if(drawable==nullptr) throw SwapchainSuboptimal(); - std::lock_guard guard(sync); + std::lock_guard guard(sync); + auto src = image(i); auto dr = drawable->texture(); - if(dr->width()!=img[i].tex->width() || dr->height()!=img[i].tex->height()) { + if(dr->width()!=src->width() || dr->height()!=src->height()) { throw SwapchainSuboptimal(); } @@ -178,29 +222,18 @@ static CGRect windowRect(UIWindow* wnd) { auto cmd = dev.queue->commandBuffer(desc.get()); auto enc = cmd->blitCommandEncoder(); - enc->copyFromTexture(img[i].tex.get(), 0, 0, + enc->copyFromTexture(src, 0, 0, dr, 0, 0, 1, 1); enc->endEncoding(); cmd->presentDrawable(drawable); + auto* device = &dev; dev.onSubmit(); cmd->addCompletedHandler(^(MTL::CommandBuffer* c){ - MTL::CommandBufferStatus s = c->status(); - if(s==MTL::CommandBufferStatusNotEnqueued || - s==MTL::CommandBufferStatusEnqueued || - s==MTL::CommandBufferStatusCommitted || - s==MTL::CommandBufferStatusScheduled) - return; - - if(s!=MTL::CommandBufferStatusCompleted) { + if(c->status()!=MTL::CommandBufferStatusCompleted) Log::e("swapchain fatal error"); - dev.onFinish(); - dev.waitIdle(); - return; - } - - dev.onFinish(); + device->onFinish(); }); cmd->commit(); diff --git a/Engine/gapi/metalapi.cpp b/Engine/gapi/metalapi.cpp index 3b426c75..9544020b 100644 --- a/Engine/gapi/metalapi.cpp +++ b/Engine/gapi/metalapi.cpp @@ -194,6 +194,11 @@ void MetalApi::present(AbstractGraphicsApi::Device*, AbstractGraphicsApi::Swapch s.present(); } +void MetalApi::present(Device* d, SwapchainImage* image) { + auto& device = *static_cast(d); + MtSwapchain::present(device,*static_cast(image)); + } + std::shared_ptr MetalApi::submit(Device* d, CommandBuffer* c) { auto* dx = reinterpret_cast(d); auto& cx = *reinterpret_cast(c); @@ -203,10 +208,17 @@ std::shared_ptr MetalApi::submit(Device* d, CommandB throw DeviceLostException(); MTL::CommandBuffer& cmd = *cx.impl; + std::shared_ptr> images; + if(!cx.swapchainImages.empty()) + images = std::make_shared>(std::move(cx.swapchainImages)); dx->onSubmit(); cmd.addCompletedHandler(^(MTL::CommandBuffer* c){ + if(images!=nullptr) + images->clear(); const MTL::CommandBufferStatus s = c->status(); - dx->signalFence(*pfence, s, MTL::CommandBufferError(c->error()->code()), c->error()); + auto* error = c->error(); + auto code = error==nullptr ? MTL::CommandBufferError(0) : MTL::CommandBufferError(error->code()); + dx->signalFence(*pfence, s, code, error); if(s==MTL::CommandBufferStatusCompleted || s==MTL::CommandBufferStatusError) dx->onFinish(); }); diff --git a/Engine/gapi/metalapi.h b/Engine/gapi/metalapi.h index b7110401..7682c17b 100644 --- a/Engine/gapi/metalapi.h +++ b/Engine/gapi/metalapi.h @@ -40,6 +40,7 @@ class MetalApi : public AbstractGraphicsApi { CommandBuffer* createCommandBuffer(Device* d) override; void present(Device *d, Swapchain* sw) override; + void present(Device *d, SwapchainImage* image) override; auto submit (Device *d, CommandBuffer* cmd) -> std::shared_ptr override; void getCaps(Device *d, Props& caps) override; diff --git a/Engine/graphics/attachment.cpp b/Engine/graphics/attachment.cpp index 02e00f24..d4fa1cde 100644 --- a/Engine/graphics/attachment.cpp +++ b/Engine/graphics/attachment.cpp @@ -2,32 +2,53 @@ using namespace Tempest; +Attachment::Attachment(Attachment&& other) + :tImpl(std::move(other.tImpl)),sImpl(other.sImpl),pImpl(std::move(other.pImpl)) { + other.sImpl = {}; + } + +Attachment& Attachment::operator=(Attachment&& other) { + Attachment tmp(std::move(other)); + std::swap(tImpl,tmp.tImpl); + std::swap(sImpl,tmp.sImpl); + std::swap(pImpl,tmp.pImpl); + return *this; + } + Attachment::Attachment(AbstractGraphicsApi::Swapchain* sw, uint32_t id) { sImpl.swapchain = sw; sImpl.id = id; } int Attachment::w() const { + if(pImpl) + return int(pImpl.handler->width); if(sImpl.swapchain) return int(sImpl.swapchain->w()); return tImpl.w(); } int Attachment::h() const { + if(pImpl) + return int(pImpl.handler->height); if(sImpl.swapchain) return int(sImpl.swapchain->h()); return tImpl.h(); } Size Attachment::size() const { + if(pImpl) + return Size(int(pImpl.handler->width),int(pImpl.handler->height)); if(sImpl.swapchain) return Size(int(sImpl.swapchain->w()),int(sImpl.swapchain->h())); return tImpl.size(); } bool Attachment::isEmpty() const { + if(pImpl) + return false; if(sImpl.swapchain) - return int(sImpl.swapchain->w()> 0 && sImpl.swapchain->h()>0); + return sImpl.swapchain->w()==0 || sImpl.swapchain->h()==0; return tImpl.isEmpty(); } diff --git a/Engine/graphics/attachment.h b/Engine/graphics/attachment.h index c6dac8c6..00f93798 100644 --- a/Engine/graphics/attachment.h +++ b/Engine/graphics/attachment.h @@ -13,9 +13,9 @@ template T textureCast(const Attachment& s); class Attachment final { public: Attachment()=default; - Attachment(Attachment&&)=default; + Attachment(Attachment&& other); ~Attachment()=default; - Attachment& operator=(Attachment&&)=default; + Attachment& operator=(Attachment&& other); int w() const; int h() const; @@ -25,6 +25,7 @@ class Attachment final { private: Attachment(Texture2d&& t):tImpl(std::move(t)){} Attachment(AbstractGraphicsApi::Swapchain* sw, uint32_t id); + Attachment(AbstractGraphicsApi::PSwapchainImage&& image):pImpl(std::move(image)){} struct SwImage { AbstractGraphicsApi::Swapchain* swapchain = nullptr; @@ -32,6 +33,7 @@ class Attachment final { }; Texture2d tImpl; SwImage sImpl; + AbstractGraphicsApi::PSwapchainImage pImpl; friend class Tempest::Device; friend class Tempest::Swapchain; @@ -43,21 +45,21 @@ class Attachment final { template<> inline Texture2d& textureCast(Attachment& a) { - if(a.sImpl.swapchain) + if(a.sImpl.swapchain || a.pImpl) throw BadTextureCastException("Cannot cast swapchain image to Texture2d"); return a.tImpl; } template<> inline const Texture2d& textureCast(Attachment& a) { - if(a.sImpl.swapchain) + if(a.sImpl.swapchain || a.pImpl) throw BadTextureCastException("Cannot cast swapchain image to Texture2d"); return a.tImpl; } template<> inline const Texture2d& textureCast(const Attachment& a) { - if(a.sImpl.swapchain) + if(a.sImpl.swapchain || a.pImpl) throw BadTextureCastException("Cannot cast swapchain image to Texture2d"); return a.tImpl; } diff --git a/Engine/graphics/device.cpp b/Engine/graphics/device.cpp index a0d8b705..4e3b29f3 100644 --- a/Engine/graphics/device.cpp +++ b/Engine/graphics/device.cpp @@ -87,6 +87,12 @@ void Device::present(Swapchain& sw) { api.present(dev,sw.impl.handler); } +void Device::present(Attachment image) { + if(!image.pImpl || &image.pImpl.handler->device!=dev) + throw std::system_error(GraphicsErrc::InvalidTexture); + api.present(dev,image.pImpl.handler); + } + Shader Device::shader(RFile &file) { const size_t fileSize=file.size(); diff --git a/Engine/graphics/device.h b/Engine/graphics/device.h index bc06d6d4..a7a3119d 100644 --- a/Engine/graphics/device.h +++ b/Engine/graphics/device.h @@ -48,6 +48,8 @@ class Device { [[nodiscard]] Fence submit(const CommandBuffer& cmd); void present(Swapchain& sw); + // Consumes an image acquired from Swapchain::next(), including on failure. + void present(Attachment image); Swapchain swapchain(SystemApi::Window* w) const; diff --git a/Engine/graphics/encoder.cpp b/Engine/graphics/encoder.cpp index 959dac5d..6ad43962 100644 --- a/Engine/graphics/encoder.cpp +++ b/Engine/graphics/encoder.cpp @@ -278,10 +278,14 @@ void Tempest::Encoder::implSetFramebuffer(const Attachme Detail::FrameBufferDesc fbo; for(size_t i=0; iisEmpty()) + throw std::system_error(GraphicsErrc::InvalidTexture); if(ax->w()!=int(w) || ax->h()!=int(h)) throw IncompleteFboException(); fbo.desc[i] = rt[i]; - if(ax->sImpl.swapchain!=nullptr) { + if(ax->pImpl) { + fbo.image[i] = ax->pImpl.handler; + } else if(ax->sImpl.swapchain!=nullptr) { fbo.frm[i] = TextureFormat::Undefined; fbo.sw[i] = ax->sImpl.swapchain; fbo.imgId[i] = ax->sImpl.id; diff --git a/Engine/graphics/swapchain.cpp b/Engine/graphics/swapchain.cpp index 564c7996..dffe7380 100644 --- a/Engine/graphics/swapchain.cpp +++ b/Engine/graphics/swapchain.cpp @@ -44,6 +44,10 @@ void Swapchain::reset() { implReset(); } +Attachment Swapchain::next() { + return Attachment(impl.handler->next()); + } + uint32_t Swapchain::imageCount() const { return impl.handler->imageCount(); } diff --git a/Engine/graphics/swapchain.h b/Engine/graphics/swapchain.h index f10513a8..47735960 100644 --- a/Engine/graphics/swapchain.h +++ b/Engine/graphics/swapchain.h @@ -21,6 +21,10 @@ class Swapchain final { void reset(); + // Acquires an image for one frame. Currently supported by Metal. + // Present it after submitting all rendering, or drop it to discard the frame. + Attachment next(); + uint32_t currentImage() const; uint32_t imageCount() const; Attachment& operator[](size_t id); diff --git a/Tests/tests/gapi/metal_swapchain_test.cpp b/Tests/tests/gapi/metal_swapchain_test.cpp new file mode 100644 index 00000000..2a8bbeb7 --- /dev/null +++ b/Tests/tests/gapi/metal_swapchain_test.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include + +#include + +#if defined(__OSX__) +using namespace Tempest; + +namespace { +class TestWindow : public Window { + public: + using Window::hwnd; + }; +} + +TEST(MetalApi,DynamicSwapchain) { + try { + Application app; + TestWindow window; + MetalApi api; + Device device(api); + auto swapchain = device.swapchain(window.hwnd()); + CommandBuffer command; + + for(int i=0; i<12; ++i) { + auto image = swapchain.next(); + EXPECT_FALSE(image.isEmpty()); + EXPECT_EQ(image.w(),int(swapchain.w())); + EXPECT_EQ(image.h(),int(swapchain.h())); + EXPECT_THROW(textureCast(image),BadTextureCastException); + + Attachment moved(std::move(image)); + EXPECT_TRUE(image.isEmpty()); + EXPECT_EQ(image.w(),0); + image = std::move(moved); + EXPECT_TRUE(moved.isEmpty()); + + { + auto encoder = command.startEncoding(device); + encoder.setFramebuffer({{image,Vec4(0.25f,0.5f,0.75f,1.f),Preserve}}); + encoder.setFramebuffer({}); + encoder.setFramebuffer({{image,Preserve,Preserve}}); + } + const bool discard = i%3==0; + if(discard) + image = Attachment(); + auto fence = device.submit(command); + if(i==5) { + const auto size = image.size(); + swapchain.reset(); + EXPECT_EQ(image.size(),size); + } + if(!discard) + device.present(std::move(image)); + EXPECT_TRUE(image.isEmpty()); + EXPECT_THROW(device.present(std::move(image)),std::system_error); + fence.wait(); + } + + // Keep the indexed API working while other backends migrate. + { + auto encoder = command.startEncoding(device); + auto& image = swapchain[swapchain.currentImage()]; + EXPECT_FALSE(image.isEmpty()); + encoder.setFramebuffer({{image,Vec4(0),Preserve}}); + } + auto fence = device.submit(command); + device.present(swapchain); + fence.wait(); + device.waitIdle(); + + auto offscreen = device.attachment(TextureFormat::RGBA8,16,16); + EXPECT_THROW(device.present(std::move(offscreen)),std::system_error); + { + auto encoder = command.startEncoding(device); + Attachment empty; + EXPECT_THROW(encoder.setFramebuffer({{empty,Vec4(0),Preserve}}),std::system_error); + } + } + catch(const std::system_error& e) { + if(e.code()!=GraphicsErrc::NoDevice) + throw; + Log::d("Skipping graphics testcase: ",e.what()); + } + } +#endif