diff --git a/Engine/gapi/abstractgraphicsapi.cpp b/Engine/gapi/abstractgraphicsapi.cpp index d92ef351..d06104f8 100644 --- a/Engine/gapi/abstractgraphicsapi.cpp +++ b/Engine/gapi/abstractgraphicsapi.cpp @@ -131,3 +131,8 @@ std::shared_ptr AbstractGraphicsApi::submit(Device* NonUniqResId AbstractGraphicsApi::Swapchain::syncId() const { return NonUniqResId(0x1); } + +AbstractGraphicsApi::Swapchain* AbstractGraphicsApi::createSwapchain(SystemApi::Window* w, Device* d, + const Swapchain::Options&) { + return createSwapchain(w,d); + } diff --git a/Engine/gapi/abstractgraphicsapi.h b/Engine/gapi/abstractgraphicsapi.h index 7938dfe0..11c0e942 100644 --- a/Engine/gapi/abstractgraphicsapi.h +++ b/Engine/gapi/abstractgraphicsapi.h @@ -520,6 +520,16 @@ namespace Tempest { virtual void setPayload(std::vector>&&) {} }; struct Swapchain:NoCopy { + enum class RenderMode:uint8_t { + Copy, + Direct, + }; + + struct Options { + uint32_t bufferCount = 0; + RenderMode renderMode = RenderMode::Copy; + }; + virtual ~Swapchain()=default; virtual void reset()=0; virtual uint32_t currentBackBufferIndex()=0; @@ -636,6 +646,7 @@ namespace Tempest { virtual Device* createDevice(std::string_view gpuName) = 0; virtual Swapchain* createSwapchain(SystemApi::Window* w,AbstractGraphicsApi::Device *d) = 0; + virtual Swapchain* createSwapchain(SystemApi::Window* w,Device* d,const Swapchain::Options& options); virtual PPipeline createPipeline(Device* d, const RenderState &st, Topology tp, const Shader* const* sh, size_t cnt)=0; diff --git a/Engine/gapi/metal/mtcommandbuffer.cpp b/Engine/gapi/metal/mtcommandbuffer.cpp index 2997f449..9405c0cb 100644 --- a/Engine/gapi/metal/mtcommandbuffer.cpp +++ b/Engine/gapi/metal/mtcommandbuffer.cpp @@ -11,6 +11,8 @@ #include "mtswapchain.h" #include "mtaccelerationstructure.h" +#include + using namespace Tempest; using namespace Tempest::Detail; @@ -65,6 +67,7 @@ void MtCommandBuffer::end() { } void MtCommandBuffer::reset() { + swapchainFrames.clear(); auto pool = NsPtr::init(); auto desc = NsPtr::init(); desc->setRetainedReferences(false); @@ -94,7 +97,10 @@ void MtCommandBuffer::beginRendering(const FrameBufferDesc& fbo, size_t fboSize, auto clr = desc->colorAttachments()->object(i); if(fbo.sw[i]!=nullptr) { auto& s = *reinterpret_cast(fbo.sw[i]); - clr->setTexture(s.img[fbo.imgId[i]].tex.get()); + auto frame = s.acquireFrame(fbo.imgId[i]); + clr->setTexture(frame==nullptr ? s.img[fbo.imgId[i]].tex.get() : frame->texture.get()); + if(frame!=nullptr && std::find(swapchainFrames.begin(),swapchainFrames.end(),frame)==swapchainFrames.end()) + swapchainFrames.push_back(std::move(frame)); 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..b5098d34 100644 --- a/Engine/gapi/metal/mtcommandbuffer.h +++ b/Engine/gapi/metal/mtcommandbuffer.h @@ -20,6 +20,7 @@ class MtPipeline; class MtCompPipeline; class MtDescriptorArray; class MtTopAccelerationStructure; +struct MtSwapchainFrame; class MtCommandBuffer : public AbstractGraphicsApi::CommandBuffer { public: @@ -113,6 +114,7 @@ class MtCommandBuffer : public AbstractGraphicsApi::CommandBuffer { NsPtr encBlit; std::vector usedResources; + std::vector> swapchainFrames; MtFboLayout curFbo; Push pushData; diff --git a/Engine/gapi/metal/mtswapchain.h b/Engine/gapi/metal/mtswapchain.h index 0f250bd1..44272005 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,9 +16,19 @@ namespace Detail { class MtDevice; +struct MtSwapchainFrame { + MtSwapchainFrame(MTL::Texture* texture, CA::MetalDrawable* drawable); + ~MtSwapchainFrame(); + + NsPtr texture; + NsPtr drawable; + }; + class MtSwapchain : public AbstractGraphicsApi::Swapchain { public: - MtSwapchain(MtDevice& dev, SystemApi::Window* w); + using Frame = std::shared_ptr; + + MtSwapchain(MtDevice& dev, SystemApi::Window* w, const Options& options); ~MtSwapchain(); void reset() override; @@ -30,6 +40,7 @@ class MtSwapchain : public AbstractGraphicsApi::Swapchain { NonUniqResId syncId() const override { return NonUniqResId::I_None; } MTL::PixelFormat format() const; + Frame acquireFrame(uint32_t image); struct Image { NsPtr tex; @@ -40,12 +51,14 @@ class MtSwapchain : public AbstractGraphicsApi::Swapchain { struct Impl; std::unique_ptr pimpl; - SpinLock sync; + std::mutex sync; MtDevice& dev; Tempest::Size sz; uint32_t imgCount = 0; uint32_t currentImg = 0; + bool direct = false; + Frame activeFrame; NsPtr mkTexture(); void nextDrawable(); diff --git a/Engine/gapi/metal/mtswapchain.mm b/Engine/gapi/metal/mtswapchain.mm index e0eaf26c..e6c4e9cb 100644 --- a/Engine/gapi/metal/mtswapchain.mm +++ b/Engine/gapi/metal/mtswapchain.mm @@ -63,6 +63,17 @@ - (CALayer *)makeBackingLayer { } }; +MtSwapchainFrame::MtSwapchainFrame(MTL::Texture* tex, CA::MetalDrawable* dr) + :texture(tex), drawable(dr) { + if(tex!=nullptr) + tex->retain(); + if(dr!=nullptr) + dr->retain(); + } + +MtSwapchainFrame::~MtSwapchainFrame() { + } + static float backingScaleFactor(SysWindow* w) { #if defined(__OSX__) return [w screen].backingScaleFactor; @@ -92,8 +103,8 @@ static CGRect windowRect(UIWindow* wnd) { #endif // note : MoltenVK supports NSView, UIView, CAMetalLayer, so we should align to it -MtSwapchain::MtSwapchain(MtDevice& dev, SystemApi::Window *w) - :dev(dev), pimpl(new Impl()) { +MtSwapchain::MtSwapchain(MtDevice& dev, SystemApi::Window *w, const Options& options) + :pimpl(new Impl()), dev(dev), direct(options.renderMode==RenderMode::Direct) { NSObject* obj = reinterpret_cast(w); if([obj isKindOfClass : [SysWindow class]]) pimpl->wnd = reinterpret_cast(w); @@ -117,23 +128,28 @@ static CGRect windowRect(UIWindow* wnd) { [lay setContentsScale:dpi]; #if defined(__IOS__) // Swapchain takes too much memory on 2GB iPhone - lay.maximumDrawableCount = 2; + lay.maximumDrawableCount = options.bufferCount==0 ? 2 : options.bufferCount; +#elif defined(__OSX__) + if(options.bufferCount!=0) + lay.maximumDrawableCount = options.bufferCount; #endif lay.pixelFormat = MTLPixelFormatBGRA8Unorm; - lay.allowsNextDrawableTimeout = NO; + lay.allowsNextDrawableTimeout = direct ? YES : NO; lay.framebufferOnly = NO; reset(); } MtSwapchain::~MtSwapchain() { + dev.waitIdle(); if(pimpl->view!=nil) [pimpl->view release]; } void MtSwapchain::reset() { + std::lock_guard guard(sync); dev.waitIdle(); // pending commands - std::lock_guard guard(sync); + activeFrame.reset(); // https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc CAMetalLayer* lay = pimpl->metalLayer(); @@ -146,8 +162,10 @@ static CGRect windowRect(UIWindow* wnd) { img.resize(imgCount); for(size_t i=0; i guard(sync); + if(image!=currentImg) + throw SwapchainSuboptimal(); + if(activeFrame!=nullptr) + return activeFrame; + + auto pool = NsPtr::init(); + auto* lay = reinterpret_cast(pimpl->metalLayer()); + auto* dr = lay->nextDrawable(); + if(dr!=nullptr && dr->texture()->width()==size_t(sz.w) && dr->texture()->height()==size_t(sz.h)) { + activeFrame = std::make_shared(dr->texture(),dr); + } else { + // Allocate the private Copy fallback only when drawable acquisition fails. + if(img[image].tex==nullptr) + img[image].tex = mkTexture(); + activeFrame = std::make_shared(img[image].tex.get(),nullptr); + } + return activeFrame; + } + void MtSwapchain::present() { + std::lock_guard guard(sync); auto pool = NsPtr::init(); + auto frame = activeFrame; + if(direct && frame==nullptr) + throw SwapchainSuboptimal(); CA::MetalLayer* lay = reinterpret_cast(pimpl->metalLayer()); uint32_t i = currentImg; - auto drawable = lay->nextDrawable(); + auto drawable = frame==nullptr ? nullptr : frame->drawable.get(); + if(drawable==nullptr) + drawable = lay->nextDrawable(); if(drawable==nullptr) throw SwapchainSuboptimal(); - std::lock_guard guard(sync); + auto src = frame==nullptr ? img[i].tex.get() : frame->texture.get(); 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(); } auto desc = NsPtr::init(); - //desc->setRetainedReferences(true); + desc->setRetainedReferences(true); desc->setErrorOptions(MTL::CommandBufferErrorOptionEncoderExecutionStatus); auto cmd = dev.queue->commandBuffer(desc.get()); - auto enc = cmd->blitCommandEncoder(); - - enc->copyFromTexture(img[i].tex.get(), 0, 0, - dr, 0, 0, - 1, 1); - enc->endEncoding(); + if(src!=dr) { + auto enc = cmd->blitCommandEncoder(); + 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(); + activeFrame.reset(); nextDrawable(); } diff --git a/Engine/gapi/metalapi.cpp b/Engine/gapi/metalapi.cpp index 3b426c75..1a9e143f 100644 --- a/Engine/gapi/metalapi.cpp +++ b/Engine/gapi/metalapi.cpp @@ -23,6 +23,8 @@ #include +#include + using namespace Tempest; using namespace Tempest::Detail; @@ -68,8 +70,17 @@ AbstractGraphicsApi::Device* MetalApi::createDevice(std::string_view gpuName) { AbstractGraphicsApi::Swapchain *MetalApi::createSwapchain(SystemApi::Window *w, AbstractGraphicsApi::Device* d) { + return createSwapchain(w,d,Swapchain::Options{}); + } + +AbstractGraphicsApi::Swapchain* MetalApi::createSwapchain(SystemApi::Window* w, Device* d, + const Swapchain::Options& options) { + if(options.bufferCount!=0 && options.bufferCount!=2 && options.bufferCount!=3) + throw std::invalid_argument("Metal swapchain buffer count must be 0, 2, or 3"); + if(options.renderMode!=Swapchain::RenderMode::Copy && options.renderMode!=Swapchain::RenderMode::Direct) + throw std::invalid_argument("Unknown Metal swapchain render mode"); auto& dev = *reinterpret_cast(d); - return new MtSwapchain(dev,w); + return new MtSwapchain(dev,w,options); } AbstractGraphicsApi::PPipeline MetalApi::createPipeline(AbstractGraphicsApi::Device *d, @@ -203,8 +214,15 @@ std::shared_ptr MetalApi::submit(Device* d, CommandB throw DeviceLostException(); MTL::CommandBuffer& cmd = *cx.impl; + std::shared_ptr> frames; + if(!cx.swapchainFrames.empty()) { + frames = std::make_shared>(std::move(cx.swapchainFrames)); + } dx->onSubmit(); cmd.addCompletedHandler(^(MTL::CommandBuffer* c){ + // A completed command buffer may itself remain alive until the next frame. + if(frames!=nullptr) + frames->clear(); const MTL::CommandBufferStatus s = c->status(); dx->signalFence(*pfence, s, MTL::CommandBufferError(c->error()->code()), c->error()); if(s==MTL::CommandBufferStatusCompleted || s==MTL::CommandBufferStatusError) diff --git a/Engine/gapi/metalapi.h b/Engine/gapi/metalapi.h index b7110401..b789f658 100644 --- a/Engine/gapi/metalapi.h +++ b/Engine/gapi/metalapi.h @@ -14,6 +14,7 @@ class MetalApi : public AbstractGraphicsApi { protected: Device* createDevice(std::string_view gpuName) override; Swapchain* createSwapchain(SystemApi::Window* w, Device *d) override; + Swapchain* createSwapchain(SystemApi::Window* w, Device* d, const Swapchain::Options& options) override; PPipeline createPipeline(Device* d, const RenderState &st, Topology tp, const Shader*const* sh, size_t cnt) override; diff --git a/Engine/graphics/device.cpp b/Engine/graphics/device.cpp index a0d8b705..1d191715 100644 --- a/Engine/graphics/device.cpp +++ b/Engine/graphics/device.cpp @@ -160,6 +160,10 @@ Swapchain Device::swapchain(SystemApi::Window* w) const { return Swapchain(api.createSwapchain(w,impl.dev)); } +Swapchain Device::swapchain(SystemApi::Window* w, const Swapchain::Options& options) const { + return Swapchain(api.createSwapchain(w,impl.dev,options)); + } + const Device::Props& Device::properties() const { return devProps; } diff --git a/Engine/graphics/device.h b/Engine/graphics/device.h index bc06d6d4..f9e9f1b4 100644 --- a/Engine/graphics/device.h +++ b/Engine/graphics/device.h @@ -50,6 +50,7 @@ class Device { void present(Swapchain& sw); Swapchain swapchain(SystemApi::Window* w) const; + Swapchain swapchain(SystemApi::Window* w, const Swapchain::Options& options) const; Shader shader(RFile& file); Shader shader(const char* filename); diff --git a/Engine/graphics/swapchain.cpp b/Engine/graphics/swapchain.cpp index 564c7996..050ec854 100644 --- a/Engine/graphics/swapchain.cpp +++ b/Engine/graphics/swapchain.cpp @@ -21,6 +21,10 @@ Swapchain::Swapchain(Device& dev, SystemApi::Window* w) { *this = dev.swapchain(w); } +Swapchain::Swapchain(Device& dev, SystemApi::Window* w, const Options& options) { + *this = dev.swapchain(w,options); + } + Swapchain::~Swapchain() { delete impl.handler; } diff --git a/Engine/graphics/swapchain.h b/Engine/graphics/swapchain.h index f10513a8..634da4be 100644 --- a/Engine/graphics/swapchain.h +++ b/Engine/graphics/swapchain.h @@ -10,7 +10,13 @@ class Attachment; class Swapchain final { public: + using RenderMode = AbstractGraphicsApi::Swapchain::RenderMode; + using Options = AbstractGraphicsApi::Swapchain::Options; + Swapchain(Device& dev, SystemApi::Window* w); + // Metal: bufferCount 0 preserves the default, 2/3 selects the drawable pool. + // Direct prefers rendering without a copy. Other backends ignore these hints. + Swapchain(Device& dev, SystemApi::Window* w, const Options& options); Swapchain(Swapchain&&)=default; ~Swapchain(); diff --git a/Tests/tests/gapi/metal_swapchain_test.cpp b/Tests/tests/gapi/metal_swapchain_test.cpp new file mode 100644 index 00000000..e6823a86 --- /dev/null +++ b/Tests/tests/gapi/metal_swapchain_test.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#if defined(__OSX__) +using namespace Tempest; + +namespace { +class TestWindow : public Window { + public: + using Window::hwnd; + }; +} + +TEST(MetalApi,SwapchainOptions) { + try { + Application app; + TestWindow window; + MetalApi api; + Device device(api); + + for(auto count:{0u,2u,3u}) { + for(auto mode:{Swapchain::RenderMode::Copy,Swapchain::RenderMode::Direct}) { + Swapchain::Options options; + options.bufferCount = count; + options.renderMode = mode; + auto swapchain = device.swapchain(window.hwnd(),options); + EXPECT_EQ(swapchain.imageCount(),count==0 ? 3u : count); + CommandBuffer command; + for(int i=0; i<12; ++i) { + if(i==6) + swapchain.reset(); + { + auto encoder = command.startEncoding(device); + auto& image = swapchain[swapchain.currentImage()]; + encoder.setFramebuffer({{image,Vec4(0.25f,0.5f,0.75f,1.f),Preserve}}); + encoder.setFramebuffer({}); + encoder.setFramebuffer({{image,Preserve,Preserve}}); + } + auto fence = device.submit(command); + device.present(swapchain); + fence.wait(); + } + device.waitIdle(); + } + } + + for(auto count:{1u,4u,uint32_t(-1)}) { + Swapchain::Options options; + options.bufferCount = count; + EXPECT_THROW(device.swapchain(window.hwnd(),options),std::invalid_argument); + } + } + catch(const std::system_error& e) { + if(e.code()!=GraphicsErrc::NoDevice) + throw; + Log::d("Skipping graphics testcase: ",e.what()); + } + } +#endif