diff options
author | Alexandru Croitor <alexandru.croitor@qt.io> | 2019-07-11 17:17:13 +0200 |
---|---|---|
committer | Alexandru Croitor <alexandru.croitor@qt.io> | 2019-07-11 17:17:51 +0200 |
commit | 4dac45c9ee59ff6586d90d423654da91523ab679 (patch) | |
tree | cd4a4adf2cbc9e77bf86d2d11e71ec66afdf3be4 /src/gui/rhi/qrhimetal.mm | |
parent | 078cd61751aeaa310d35a3d596a21a36004a1a0f (diff) | |
parent | f44850b5c3464cdda0ee9b1ee858d95f3ffaa3e2 (diff) |
Merge remote-tracking branch 'origin/wip/qt6' into wip/cmake
Change-Id: I715b1d743d5f11560e7b3fbeb8fd64a5e5ddb277
Diffstat (limited to 'src/gui/rhi/qrhimetal.mm')
-rw-r--r-- | src/gui/rhi/qrhimetal.mm | 3566 |
1 files changed, 3566 insertions, 0 deletions
diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm new file mode 100644 index 0000000000..fa537a504b --- /dev/null +++ b/src/gui/rhi/qrhimetal.mm @@ -0,0 +1,3566 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Gui module +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qrhimetal_p_p.h" +#include "qshader_p.h" +#include "qshaderdescription_p.h" +#include <QGuiApplication> +#include <QWindow> +#include <qmath.h> + +#ifdef Q_OS_MACOS +#include <AppKit/AppKit.h> +#endif + +#include <Metal/Metal.h> +#include <QuartzCore/CAMetalLayer.h> + +QT_BEGIN_NAMESPACE + +/* + Metal backend. Double buffers and throttles to vsync. "Dynamic" buffers are + Shared (host visible) and duplicated (to help having 2 frames in flight), + "static" and "immutable" are Managed on macOS and Shared on iOS/tvOS. + Textures are Private (device local) and a host visible staging buffer is + used to upload data to them. Does not rely on strong objects refs from + command buffers but does rely on the automatic resource tracking of the + command encoders. +*/ + +#if __has_feature(objc_arc) +#error ARC not supported +#endif + +// Note: we expect everything here pass the Metal API validation when running +// in Debug mode in XCode. Some of the issues that break validation are not +// obvious and not visible when running outside XCode. +// +// An exception is the nextDrawable Called Early blah blah warning, which is +// plain and simply false. + +/*! + \class QRhiMetalInitParams + \inmodule QtRhi + \brief Metal specific initialization parameters. + + A Metal-based QRhi needs no special parameters for initialization. + + \badcode + QRhiMetalInitParams params; + rhi = QRhi::create(QRhi::Metal, ¶ms); + \endcode + + \note Metal API validation cannot be enabled by the application. Instead, + run the debug build of the application in XCode. Generating a + \c{.xcodeproj} file via \c{qmake -spec macx-xcode} provides a convenient + way to enable this. + + \note QRhiSwapChain can only target QWindow instances that have their + surface type set to QSurface::MetalSurface. + + \section2 Working with existing Metal devices + + When interoperating with another graphics engine, it may be necessary to + get a QRhi instance that uses the same Metal device. This can be achieved + by passing a pointer to a QRhiMetalNativeHandles to QRhi::create(). The + device must be set to a non-null value then. Optionally, a command queue + object can be specified as well. + + The QRhi does not take ownership of any of the external objects. + */ + +/*! + \class QRhiMetalNativeHandles + \inmodule QtRhi + \brief Holds the Metal device used by the QRhi. + + \note The class uses \c{void *} as the type since including the Objective C + headers is not acceptable here. The actual types are \c{id<MTLDevice>} and + \c{id<MTLCommandQueue>}. + */ + +/*! + \class QRhiMetalTextureNativeHandles + \inmodule QtRhi + \brief Holds the Metal texture object that is backing a QRhiTexture instance. + + \note The class uses \c{void *} as the type since including the Objective C + headers is not acceptable here. The actual type is \c{id<MTLTexture>}. + */ + +/*! + \class QRhiMetalCommandBufferNativeHandles + \inmodule QtRhi + \brief Holds the MTLCommandBuffer and MTLRenderCommandEncoder objects that are backing a QRhiCommandBuffer. + + \note The command buffer object is only guaranteed to be valid while + recording a frame, that is, between a \l{QRhi::beginFrame()}{beginFrame()} + - \l{QRhi::endFrame()}{endFrame()} or + \l{QRhi::beginOffscreenFrame()}{beginOffscreenFrame()} - + \l{QRhi::endOffsrceenFrame()}{endOffscreenFrame()} pair. + + \note The command encoder is only valid while recording a pass, that is, + between \l{QRhiCommandBuffer::beginPass()} - + \l{QRhiCommandBuffer::endPass()}. + */ + +struct QRhiMetalData +{ + QRhiMetalData(QRhiImplementation *rhi) : ofr(rhi) { } + + id<MTLDevice> dev = nil; + id<MTLCommandQueue> cmdQueue = nil; + + MTLRenderPassDescriptor *createDefaultRenderPass(bool hasDepthStencil, + const QColor &colorClearValue, + const QRhiDepthStencilClearValue &depthStencilClearValue, + int colorAttCount); + id<MTLLibrary> createMetalLib(const QShader &shader, QShader::Variant shaderVariant, + QString *error, QByteArray *entryPoint); + id<MTLFunction> createMSLShaderFunction(id<MTLLibrary> lib, const QByteArray &entryPoint); + + struct DeferredReleaseEntry { + enum Type { + Buffer, + RenderBuffer, + Texture, + Sampler, + StagingBuffer + }; + Type type; + int lastActiveFrameSlot; // -1 if not used otherwise 0..FRAMES_IN_FLIGHT-1 + union { + struct { + id<MTLBuffer> buffers[QMTL_FRAMES_IN_FLIGHT]; + } buffer; + struct { + id<MTLTexture> texture; + } renderbuffer; + struct { + id<MTLTexture> texture; + id<MTLBuffer> stagingBuffers[QMTL_FRAMES_IN_FLIGHT]; + id<MTLTexture> views[QRhi::MAX_LEVELS]; + } texture; + struct { + id<MTLSamplerState> samplerState; + } sampler; + struct { + id<MTLBuffer> buffer; + } stagingBuffer; + }; + }; + QVector<DeferredReleaseEntry> releaseQueue; + + struct OffscreenFrame { + OffscreenFrame(QRhiImplementation *rhi) : cbWrapper(rhi) { } + bool active = false; + QMetalCommandBuffer cbWrapper; + } ofr; + + struct ActiveReadback { + int activeFrameSlot = -1; + QRhiReadbackDescription desc; + QRhiReadbackResult *result; + id<MTLBuffer> buf; + quint32 bufSize; + QSize pixelSize; + QRhiTexture::Format format; + }; + QVector<ActiveReadback> activeReadbacks; + + API_AVAILABLE(macos(10.13), ios(11.0)) MTLCaptureManager *captureMgr; + API_AVAILABLE(macos(10.13), ios(11.0)) id<MTLCaptureScope> captureScope = nil; + + static const int TEXBUF_ALIGN = 256; // probably not accurate +}; + +Q_DECLARE_TYPEINFO(QRhiMetalData::DeferredReleaseEntry, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(QRhiMetalData::ActiveReadback, Q_MOVABLE_TYPE); + +struct QMetalBufferData +{ + bool managed; + bool slotted; + id<MTLBuffer> buf[QMTL_FRAMES_IN_FLIGHT]; + QVector<QRhiResourceUpdateBatchPrivate::DynamicBufferUpdate> pendingUpdates[QMTL_FRAMES_IN_FLIGHT]; +}; + +struct QMetalRenderBufferData +{ + MTLPixelFormat format; + id<MTLTexture> tex = nil; +}; + +struct QMetalTextureData +{ + QMetalTextureData(QMetalTexture *t) : q(t) { } + + QMetalTexture *q; + MTLPixelFormat format; + id<MTLTexture> tex = nil; + id<MTLBuffer> stagingBuf[QMTL_FRAMES_IN_FLIGHT]; + bool owns = true; + id<MTLTexture> perLevelViews[QRhi::MAX_LEVELS]; + + id<MTLTexture> viewForLevel(int level); +}; + +struct QMetalSamplerData +{ + id<MTLSamplerState> samplerState = nil; +}; + +struct QMetalCommandBufferData +{ + id<MTLCommandBuffer> cb; + id<MTLRenderCommandEncoder> currentRenderPassEncoder; + id<MTLComputeCommandEncoder> currentComputePassEncoder; + MTLRenderPassDescriptor *currentPassRpDesc; + int currentFirstVertexBinding; + QRhiBatchedBindings<id<MTLBuffer> > currentVertexInputsBuffers; + QRhiBatchedBindings<NSUInteger> currentVertexInputOffsets; +}; + +struct QMetalRenderTargetData +{ + QSize pixelSize; + float dpr = 1; + int sampleCount = 1; + int colorAttCount = 0; + int dsAttCount = 0; + + struct ColorAtt { + bool needsDrawableForTex = false; + id<MTLTexture> tex = nil; + int layer = 0; + int level = 0; + bool needsDrawableForResolveTex = false; + id<MTLTexture> resolveTex = nil; + int resolveLayer = 0; + int resolveLevel = 0; + }; + + struct { + ColorAtt colorAtt[QMetalRenderPassDescriptor::MAX_COLOR_ATTACHMENTS]; + id<MTLTexture> dsTex = nil; + bool hasStencil = false; + bool depthNeedsStore = false; + } fb; +}; + +struct QMetalGraphicsPipelineData +{ + id<MTLRenderPipelineState> ps = nil; + id<MTLDepthStencilState> ds = nil; + MTLPrimitiveType primitiveType; + MTLWinding winding; + MTLCullMode cullMode; + id<MTLLibrary> vsLib = nil; + id<MTLFunction> vsFunc = nil; + id<MTLLibrary> fsLib = nil; + id<MTLFunction> fsFunc = nil; +}; + +struct QMetalComputePipelineData +{ + id<MTLComputePipelineState> ps = nil; + id<MTLLibrary> csLib = nil; + id<MTLFunction> csFunc = nil; + MTLSize localSize; +}; + +struct QMetalSwapChainData +{ + CAMetalLayer *layer = nullptr; + id<CAMetalDrawable> curDrawable; + dispatch_semaphore_t sem[QMTL_FRAMES_IN_FLIGHT]; + MTLRenderPassDescriptor *rp = nullptr; + id<MTLTexture> msaaTex[QMTL_FRAMES_IN_FLIGHT]; + QRhiTexture::Format rhiColorFormat; + MTLPixelFormat colorFormat; +}; + +QRhiMetal::QRhiMetal(QRhiMetalInitParams *params, QRhiMetalNativeHandles *importDevice) +{ + Q_UNUSED(params); + + d = new QRhiMetalData(this); + + importedDevice = importDevice != nullptr; + if (importedDevice) { + if (d->dev) { + d->dev = (id<MTLDevice>) importDevice->dev; + importedCmdQueue = importDevice->cmdQueue != nullptr; + if (importedCmdQueue) + d->cmdQueue = (id<MTLCommandQueue>) importDevice->cmdQueue; + } else { + qWarning("No MTLDevice given, cannot import"); + importedDevice = false; + } + } +} + +QRhiMetal::~QRhiMetal() +{ + delete d; +} + +static inline uint aligned(uint v, uint byteAlign) +{ + return (v + byteAlign - 1) & ~(byteAlign - 1); +} + +bool QRhiMetal::create(QRhi::Flags flags) +{ + Q_UNUSED(flags); + + if (importedDevice) + [d->dev retain]; + else + d->dev = MTLCreateSystemDefaultDevice(); + + qDebug("Metal device: %s", qPrintable(QString::fromNSString([d->dev name]))); + + if (importedCmdQueue) + [d->cmdQueue retain]; + else + d->cmdQueue = [d->dev newCommandQueue]; + + if (@available(macOS 10.13, iOS 11.0, *)) { + d->captureMgr = [MTLCaptureManager sharedCaptureManager]; + // Have a custom capture scope as well which then shows up in XCode as + // an option when capturing, and becomes especially useful when having + // multiple windows with multiple QRhis. + d->captureScope = [d->captureMgr newCaptureScopeWithCommandQueue: d->cmdQueue]; + const QString label = QString::asprintf("Qt capture scope for QRhi %p", this); + d->captureScope.label = label.toNSString(); + } + +#if defined(Q_OS_MACOS) + caps.maxTextureSize = 16384; +#elif defined(Q_OS_TVOS) + if ([d->dev supportsFeatureSet: MTLFeatureSet(30003)]) // MTLFeatureSet_tvOS_GPUFamily2_v1 + caps.maxTextureSize = 16384; + else + caps.maxTextureSize = 8192; +#elif defined(Q_OS_IOS) + // welcome to feature set hell + if ([d->dev supportsFeatureSet: MTLFeatureSet(16)] // MTLFeatureSet_iOS_GPUFamily5_v1 + || [d->dev supportsFeatureSet: MTLFeatureSet(11)] // MTLFeatureSet_iOS_GPUFamily4_v1 + || [d->dev supportsFeatureSet: MTLFeatureSet(4)]) // MTLFeatureSet_iOS_GPUFamily3_v1 + { + caps.maxTextureSize = 16384; + } else if ([d->dev supportsFeatureSet: MTLFeatureSet(3)] // MTLFeatureSet_iOS_GPUFamily2_v2 + || [d->dev supportsFeatureSet: MTLFeatureSet(2)]) // MTLFeatureSet_iOS_GPUFamily1_v2 + { + caps.maxTextureSize = 8192; + } else { + caps.maxTextureSize = 4096; + } +#endif + + nativeHandlesStruct.dev = d->dev; + nativeHandlesStruct.cmdQueue = d->cmdQueue; + + return true; +} + +void QRhiMetal::destroy() +{ + executeDeferredReleases(true); + finishActiveReadbacks(true); + + if (@available(macOS 10.13, iOS 11.0, *)) { + [d->captureScope release]; + d->captureScope = nil; + } + + [d->cmdQueue release]; + if (!importedCmdQueue) + d->cmdQueue = nil; + + [d->dev release]; + if (!importedDevice) + d->dev = nil; +} + +QVector<int> QRhiMetal::supportedSampleCounts() const +{ + return { 1, 2, 4, 8 }; +} + +int QRhiMetal::effectiveSampleCount(int sampleCount) const +{ + // Stay compatible with QSurfaceFormat and friends where samples == 0 means the same as 1. + const int s = qBound(1, sampleCount, 64); + if (!supportedSampleCounts().contains(s)) { + qWarning("Attempted to set unsupported sample count %d", sampleCount); + return 1; + } + return s; +} + +QRhiSwapChain *QRhiMetal::createSwapChain() +{ + return new QMetalSwapChain(this); +} + +QRhiBuffer *QRhiMetal::createBuffer(QRhiBuffer::Type type, QRhiBuffer::UsageFlags usage, int size) +{ + return new QMetalBuffer(this, type, usage, size); +} + +int QRhiMetal::ubufAlignment() const +{ + return 256; +} + +bool QRhiMetal::isYUpInFramebuffer() const +{ + return false; +} + +bool QRhiMetal::isYUpInNDC() const +{ + return true; +} + +bool QRhiMetal::isClipDepthZeroToOne() const +{ + return true; +} + +QMatrix4x4 QRhiMetal::clipSpaceCorrMatrix() const +{ + // depth range 0..1 + static QMatrix4x4 m; + if (m.isIdentity()) { + // NB the ctor takes row-major + m = QMatrix4x4(1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.5f, 0.5f, + 0.0f, 0.0f, 0.0f, 1.0f); + } + return m; +} + +bool QRhiMetal::isTextureFormatSupported(QRhiTexture::Format format, QRhiTexture::Flags flags) const +{ + Q_UNUSED(flags); + +#ifdef Q_OS_MACOS + if (format >= QRhiTexture::ETC2_RGB8 && format <= QRhiTexture::ETC2_RGBA8) + return false; + if (format >= QRhiTexture::ASTC_4x4 && format <= QRhiTexture::ASTC_12x12) + return false; +#else + if (format >= QRhiTexture::BC1 && format <= QRhiTexture::BC7) + return false; +#endif + + return true; +} + +bool QRhiMetal::isFeatureSupported(QRhi::Feature feature) const +{ + switch (feature) { + case QRhi::MultisampleTexture: + return true; + case QRhi::MultisampleRenderBuffer: + return true; + case QRhi::DebugMarkers: + return true; + case QRhi::Timestamps: + return false; + case QRhi::Instancing: + return true; + case QRhi::CustomInstanceStepRate: + return true; + case QRhi::PrimitiveRestart: + return true; + case QRhi::NonDynamicUniformBuffers: + return true; + case QRhi::NonFourAlignedEffectiveIndexBufferOffset: + return false; + case QRhi::NPOTTextureRepeat: + return true; + case QRhi::RedOrAlpha8IsRed: + return true; + case QRhi::ElementIndexUint: + return true; + case QRhi::Compute: + return true; + case QRhi::WideLines: + return false; + case QRhi::VertexShaderPointSize: + return true; + case QRhi::BaseVertex: + return true; + case QRhi::BaseInstance: + return true; + default: + Q_UNREACHABLE(); + return false; + } +} + +int QRhiMetal::resourceLimit(QRhi::ResourceLimit limit) const +{ + switch (limit) { + case QRhi::TextureSizeMin: + return 1; + case QRhi::TextureSizeMax: + return caps.maxTextureSize; + case QRhi::MaxColorAttachments: + return 8; + case QRhi::FramesInFlight: + return QMTL_FRAMES_IN_FLIGHT; + default: + Q_UNREACHABLE(); + return 0; + } +} + +const QRhiNativeHandles *QRhiMetal::nativeHandles() +{ + return &nativeHandlesStruct; +} + +void QRhiMetal::sendVMemStatsToProfiler() +{ + // nothing to do here +} + +void QRhiMetal::makeThreadLocalNativeContextCurrent() +{ + // nothing to do here +} + +QRhiRenderBuffer *QRhiMetal::createRenderBuffer(QRhiRenderBuffer::Type type, const QSize &pixelSize, + int sampleCount, QRhiRenderBuffer::Flags flags) +{ + return new QMetalRenderBuffer(this, type, pixelSize, sampleCount, flags); +} + +QRhiTexture *QRhiMetal::createTexture(QRhiTexture::Format format, const QSize &pixelSize, + int sampleCount, QRhiTexture::Flags flags) +{ + return new QMetalTexture(this, format, pixelSize, sampleCount, flags); +} + +QRhiSampler *QRhiMetal::createSampler(QRhiSampler::Filter magFilter, QRhiSampler::Filter minFilter, + QRhiSampler::Filter mipmapMode, + QRhiSampler::AddressMode u, QRhiSampler::AddressMode v) +{ + return new QMetalSampler(this, magFilter, minFilter, mipmapMode, u, v); +} + +QRhiTextureRenderTarget *QRhiMetal::createTextureRenderTarget(const QRhiTextureRenderTargetDescription &desc, + QRhiTextureRenderTarget::Flags flags) +{ + return new QMetalTextureRenderTarget(this, desc, flags); +} + +QRhiGraphicsPipeline *QRhiMetal::createGraphicsPipeline() +{ + return new QMetalGraphicsPipeline(this); +} + +QRhiComputePipeline *QRhiMetal::createComputePipeline() +{ + return new QMetalComputePipeline(this); +} + +QRhiShaderResourceBindings *QRhiMetal::createShaderResourceBindings() +{ + return new QMetalShaderResourceBindings(this); +} + +void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD, QMetalCommandBuffer *cbD, + int dynamicOffsetCount, + const QRhiCommandBuffer::DynamicOffset *dynamicOffsets, + bool offsetOnlyChange) +{ + static const int KNOWN_STAGES = 3; + struct { + QRhiBatchedBindings<id<MTLBuffer> > buffers; + QRhiBatchedBindings<NSUInteger> bufferOffsets; + QRhiBatchedBindings<id<MTLTexture> > textures; + QRhiBatchedBindings<id<MTLSamplerState> > samplers; + } res[KNOWN_STAGES]; + + for (const QRhiShaderResourceBinding &binding : qAsConst(srbD->sortedBindings)) { + const QRhiShaderResourceBindingPrivate *b = QRhiShaderResourceBindingPrivate::get(&binding); + switch (b->type) { + case QRhiShaderResourceBinding::UniformBuffer: + { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.ubuf.buf); + id<MTLBuffer> mtlbuf = bufD->d->buf[bufD->d->slotted ? currentFrameSlot : 0]; + uint offset = b->u.ubuf.offset; + for (int i = 0; i < dynamicOffsetCount; ++i) { + const QRhiCommandBuffer::DynamicOffset &dynOfs(dynamicOffsets[i]); + if (dynOfs.first == b->binding) { + offset = dynOfs.second; + break; + } + } + if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { + res[0].buffers.feed(b->binding, mtlbuf); + res[0].bufferOffsets.feed(b->binding, offset); + } + if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) { + res[1].buffers.feed(b->binding, mtlbuf); + res[1].bufferOffsets.feed(b->binding, offset); + } + if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) { + res[2].buffers.feed(b->binding, mtlbuf); + res[2].bufferOffsets.feed(b->binding, offset); + } + } + break; + case QRhiShaderResourceBinding::SampledTexture: + { + QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.tex); + QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.sampler); + if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { + res[0].textures.feed(b->binding, texD->d->tex); + res[0].samplers.feed(b->binding, samplerD->d->samplerState); + } + if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) { + res[1].textures.feed(b->binding, texD->d->tex); + res[1].samplers.feed(b->binding, samplerD->d->samplerState); + } + if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) { + res[2].textures.feed(b->binding, texD->d->tex); + res[2].samplers.feed(b->binding, samplerD->d->samplerState); + } + } + break; + case QRhiShaderResourceBinding::ImageLoad: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::ImageStore: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::ImageLoadStore: + { + QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.simage.tex); + id<MTLTexture> t = texD->d->viewForLevel(b->u.simage.level); + if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) + res[0].textures.feed(b->binding, t); + if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) + res[1].textures.feed(b->binding, t); + if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) + res[2].textures.feed(b->binding, t); + } + break; + case QRhiShaderResourceBinding::BufferLoad: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::BufferStore: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::BufferLoadStore: + { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.sbuf.buf); + id<MTLBuffer> mtlbuf = bufD->d->buf[0]; + uint offset = b->u.sbuf.offset; + if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { + res[0].buffers.feed(b->binding, mtlbuf); + res[0].bufferOffsets.feed(b->binding, offset); + } + if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) { + res[1].buffers.feed(b->binding, mtlbuf); + res[1].bufferOffsets.feed(b->binding, offset); + } + if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) { + res[2].buffers.feed(b->binding, mtlbuf); + res[2].bufferOffsets.feed(b->binding, offset); + } + } + break; + default: + Q_UNREACHABLE(); + break; + } + } + + for (int idx = 0; idx < KNOWN_STAGES; ++idx) { + res[idx].buffers.finish(); + res[idx].bufferOffsets.finish(); + + for (int i = 0, ie = res[idx].buffers.batches.count(); i != ie; ++i) { + const auto &bufferBatch(res[idx].buffers.batches[i]); + const auto &offsetBatch(res[idx].bufferOffsets.batches[i]); + switch (idx) { + case 0: + [cbD->d->currentRenderPassEncoder setVertexBuffers: bufferBatch.resources.constData() + offsets: offsetBatch.resources.constData() + withRange: NSMakeRange(bufferBatch.startBinding, bufferBatch.resources.count())]; + break; + case 1: + [cbD->d->currentRenderPassEncoder setFragmentBuffers: bufferBatch.resources.constData() + offsets: offsetBatch.resources.constData() + withRange: NSMakeRange(bufferBatch.startBinding, bufferBatch.resources.count())]; + break; + case 2: + [cbD->d->currentComputePassEncoder setBuffers: bufferBatch.resources.constData() + offsets: offsetBatch.resources.constData() + withRange: NSMakeRange(bufferBatch.startBinding, bufferBatch.resources.count())]; + break; + default: + Q_UNREACHABLE(); + break; + } + } + + if (offsetOnlyChange) + continue; + + res[idx].textures.finish(); + res[idx].samplers.finish(); + + for (int i = 0, ie = res[idx].textures.batches.count(); i != ie; ++i) { + const auto &batch(res[idx].textures.batches[i]); + switch (idx) { + case 0: + [cbD->d->currentRenderPassEncoder setVertexTextures: batch.resources.constData() + withRange: NSMakeRange(batch.startBinding, batch.resources.count())]; + break; + case 1: + [cbD->d->currentRenderPassEncoder setFragmentTextures: batch.resources.constData() + withRange: NSMakeRange(batch.startBinding, batch.resources.count())]; + break; + case 2: + [cbD->d->currentComputePassEncoder setTextures: batch.resources.constData() + withRange: NSMakeRange(batch.startBinding, batch.resources.count())]; + break; + default: + Q_UNREACHABLE(); + break; + } + } + for (int i = 0, ie = res[idx].samplers.batches.count(); i != ie; ++i) { + const auto &batch(res[idx].samplers.batches[i]); + switch (idx) { + case 0: + [cbD->d->currentRenderPassEncoder setVertexSamplerStates: batch.resources.constData() + withRange: NSMakeRange(batch.startBinding, batch.resources.count())]; + break; + case 1: + [cbD->d->currentRenderPassEncoder setFragmentSamplerStates: batch.resources.constData() + withRange: NSMakeRange(batch.startBinding, batch.resources.count())]; + break; + case 2: + [cbD->d->currentComputePassEncoder setSamplerStates: batch.resources.constData() + withRange: NSMakeRange(batch.startBinding, batch.resources.count())]; + break; + default: + Q_UNREACHABLE(); + break; + } + } + } +} + +void QRhiMetal::setGraphicsPipeline(QRhiCommandBuffer *cb, QRhiGraphicsPipeline *ps) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + QMetalGraphicsPipeline *psD = QRHI_RES(QMetalGraphicsPipeline, ps); + + if (cbD->currentGraphicsPipeline != ps || cbD->currentPipelineGeneration != psD->generation) { + cbD->currentGraphicsPipeline = ps; + cbD->currentComputePipeline = nullptr; + cbD->currentPipelineGeneration = psD->generation; + + [cbD->d->currentRenderPassEncoder setRenderPipelineState: psD->d->ps]; + [cbD->d->currentRenderPassEncoder setDepthStencilState: psD->d->ds]; + [cbD->d->currentRenderPassEncoder setCullMode: psD->d->cullMode]; + [cbD->d->currentRenderPassEncoder setFrontFacingWinding: psD->d->winding]; + } + + psD->lastActiveFrameSlot = currentFrameSlot; +} + +void QRhiMetal::setShaderResources(QRhiCommandBuffer *cb, QRhiShaderResourceBindings *srb, + int dynamicOffsetCount, + const QRhiCommandBuffer::DynamicOffset *dynamicOffsets) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass != QMetalCommandBuffer::NoPass); + QMetalGraphicsPipeline *gfxPsD = QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline); + QMetalComputePipeline *compPsD = QRHI_RES(QMetalComputePipeline, cbD->currentComputePipeline); + + if (!srb) { + if (gfxPsD) + srb = gfxPsD->m_shaderResourceBindings; + else + srb = compPsD->m_shaderResourceBindings; + } + + QMetalShaderResourceBindings *srbD = QRHI_RES(QMetalShaderResourceBindings, srb); + bool hasSlottedResourceInSrb = false; + bool hasDynamicOffsetInSrb = false; + bool resNeedsRebind = false; + + // do buffer writes, figure out if we need to rebind, and mark as in-use + for (int i = 0, ie = srbD->sortedBindings.count(); i != ie; ++i) { + const QRhiShaderResourceBindingPrivate *b = QRhiShaderResourceBindingPrivate::get(&srbD->sortedBindings[i]); + QMetalShaderResourceBindings::BoundResourceData &bd(srbD->boundResourceData[i]); + switch (b->type) { + case QRhiShaderResourceBinding::UniformBuffer: + { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.ubuf.buf); + Q_ASSERT(bufD->m_usage.testFlag(QRhiBuffer::UniformBuffer)); + executeBufferHostWritesForCurrentFrame(bufD); + if (bufD->d->slotted) + hasSlottedResourceInSrb = true; + if (b->u.ubuf.hasDynamicOffset) + hasDynamicOffsetInSrb = true; + if (bufD->generation != bd.ubuf.generation || bufD->m_id != bd.ubuf.id) { + resNeedsRebind = true; + bd.ubuf.id = bufD->m_id; + bd.ubuf.generation = bufD->generation; + } + bufD->lastActiveFrameSlot = currentFrameSlot; + } + break; + case QRhiShaderResourceBinding::SampledTexture: + { + QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.tex); + QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.sampler); + if (texD->generation != bd.stex.texGeneration + || texD->m_id != bd.stex.texId + || samplerD->generation != bd.stex.samplerGeneration + || samplerD->m_id != bd.stex.samplerId) + { + resNeedsRebind = true; + bd.stex.texId = texD->m_id; + bd.stex.texGeneration = texD->generation; + bd.stex.samplerId = samplerD->m_id; + bd.stex.samplerGeneration = samplerD->generation; + } + texD->lastActiveFrameSlot = currentFrameSlot; + samplerD->lastActiveFrameSlot = currentFrameSlot; + } + break; + case QRhiShaderResourceBinding::ImageLoad: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::ImageStore: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::ImageLoadStore: + { + QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.simage.tex); + if (texD->generation != bd.simage.generation || texD->m_id != bd.simage.id) { + resNeedsRebind = true; + bd.simage.id = texD->m_id; + bd.simage.generation = texD->generation; + } + texD->lastActiveFrameSlot = currentFrameSlot; + } + break; + case QRhiShaderResourceBinding::BufferLoad: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::BufferStore: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::BufferLoadStore: + { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.sbuf.buf); + Q_ASSERT(bufD->m_usage.testFlag(QRhiBuffer::StorageBuffer)); + executeBufferHostWritesForCurrentFrame(bufD); + if (bufD->generation != bd.sbuf.generation || bufD->m_id != bd.sbuf.id) { + resNeedsRebind = true; + bd.sbuf.id = bufD->m_id; + bd.sbuf.generation = bufD->generation; + } + bufD->lastActiveFrameSlot = currentFrameSlot; + } + break; + default: + Q_UNREACHABLE(); + break; + } + } + + // make sure the resources for the correct slot get bound + const int resSlot = hasSlottedResourceInSrb ? currentFrameSlot : 0; + if (hasSlottedResourceInSrb && cbD->currentResSlot != resSlot) + resNeedsRebind = true; + + const bool srbChanged = gfxPsD ? (cbD->currentGraphicsSrb != srb) : (cbD->currentComputeSrb != srb); + const bool srbRebuilt = cbD->currentSrbGeneration != srbD->generation; + + // dynamic uniform buffer offsets always trigger a rebind + if (hasDynamicOffsetInSrb || resNeedsRebind || srbChanged || srbRebuilt) { + if (gfxPsD) { + cbD->currentGraphicsSrb = srb; + cbD->currentComputeSrb = nullptr; + } else { + cbD->currentGraphicsSrb = nullptr; + cbD->currentComputeSrb = srb; + } + cbD->currentSrbGeneration = srbD->generation; + cbD->currentResSlot = resSlot; + + const bool offsetOnlyChange = hasDynamicOffsetInSrb && !resNeedsRebind && !srbChanged && !srbRebuilt; + enqueueShaderResourceBindings(srbD, cbD, dynamicOffsetCount, dynamicOffsets, offsetOnlyChange); + } +} + +void QRhiMetal::setVertexInput(QRhiCommandBuffer *cb, + int startBinding, int bindingCount, const QRhiCommandBuffer::VertexInput *bindings, + QRhiBuffer *indexBuf, quint32 indexOffset, QRhiCommandBuffer::IndexFormat indexFormat) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + + QRhiBatchedBindings<id<MTLBuffer> > buffers; + QRhiBatchedBindings<NSUInteger> offsets; + for (int i = 0; i < bindingCount; ++i) { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, bindings[i].first); + executeBufferHostWritesForCurrentFrame(bufD); + bufD->lastActiveFrameSlot = currentFrameSlot; + id<MTLBuffer> mtlbuf = bufD->d->buf[bufD->d->slotted ? currentFrameSlot : 0]; + buffers.feed(startBinding + i, mtlbuf); + offsets.feed(startBinding + i, bindings[i].second); + } + buffers.finish(); + offsets.finish(); + + // same binding space for vertex and constant buffers - work it around + QRhiShaderResourceBindings *srb = cbD->currentGraphicsSrb; + // There's nothing guaranteeing setShaderResources() was called before + // setVertexInput()... but whatever srb will get bound will have to be + // layout-compatible anyways so maxBinding is the same. + if (!srb) + srb = cbD->currentGraphicsPipeline->shaderResourceBindings(); + const int firstVertexBinding = QRHI_RES(QMetalShaderResourceBindings, srb)->maxBinding + 1; + + if (firstVertexBinding != cbD->d->currentFirstVertexBinding + || buffers != cbD->d->currentVertexInputsBuffers + || offsets != cbD->d->currentVertexInputOffsets) + { + cbD->d->currentFirstVertexBinding = firstVertexBinding; + cbD->d->currentVertexInputsBuffers = buffers; + cbD->d->currentVertexInputOffsets = offsets; + + for (int i = 0, ie = buffers.batches.count(); i != ie; ++i) { + const auto &bufferBatch(buffers.batches[i]); + const auto &offsetBatch(offsets.batches[i]); + [cbD->d->currentRenderPassEncoder setVertexBuffers: + bufferBatch.resources.constData() + offsets: offsetBatch.resources.constData() + withRange: NSMakeRange(firstVertexBinding + bufferBatch.startBinding, bufferBatch.resources.count())]; + } + } + + if (indexBuf) { + QMetalBuffer *ibufD = QRHI_RES(QMetalBuffer, indexBuf); + executeBufferHostWritesForCurrentFrame(ibufD); + ibufD->lastActiveFrameSlot = currentFrameSlot; + cbD->currentIndexBuffer = indexBuf; + cbD->currentIndexOffset = indexOffset; + cbD->currentIndexFormat = indexFormat; + } else { + cbD->currentIndexBuffer = nullptr; + } +} + +void QRhiMetal::setViewport(QRhiCommandBuffer *cb, const QRhiViewport &viewport) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + const QSize outputSize = cbD->currentTarget->pixelSize(); + + // x,y is top-left in MTLViewportRect but bottom-left in QRhiViewport + float x, y, w, h; + if (!qrhi_toTopLeftRenderTargetRect(outputSize, viewport.viewport(), &x, &y, &w, &h)) + return; + + MTLViewport vp; + vp.originX = x; + vp.originY = y; + vp.width = w; + vp.height = h; + vp.znear = viewport.minDepth(); + vp.zfar = viewport.maxDepth(); + + [cbD->d->currentRenderPassEncoder setViewport: vp]; + + if (!QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->m_flags.testFlag(QRhiGraphicsPipeline::UsesScissor)) { + MTLScissorRect s; + s.x = x; + s.y = y; + s.width = w; + s.height = h; + [cbD->d->currentRenderPassEncoder setScissorRect: s]; + } +} + +void QRhiMetal::setScissor(QRhiCommandBuffer *cb, const QRhiScissor &scissor) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + Q_ASSERT(QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->m_flags.testFlag(QRhiGraphicsPipeline::UsesScissor)); + const QSize outputSize = cbD->currentTarget->pixelSize(); + + // x,y is top-left in MTLScissorRect but bottom-left in QRhiScissor + int x, y, w, h; + if (!qrhi_toTopLeftRenderTargetRect(outputSize, scissor.scissor(), &x, &y, &w, &h)) + return; + + MTLScissorRect s; + s.x = x; + s.y = y; + s.width = w; + s.height = h; + + [cbD->d->currentRenderPassEncoder setScissorRect: s]; +} + +void QRhiMetal::setBlendConstants(QRhiCommandBuffer *cb, const QColor &c) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + + [cbD->d->currentRenderPassEncoder setBlendColorRed: c.redF() green: c.greenF() blue: c.blueF() alpha: c.alphaF()]; +} + +void QRhiMetal::setStencilRef(QRhiCommandBuffer *cb, quint32 refValue) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + + [cbD->d->currentRenderPassEncoder setStencilReferenceValue: refValue]; +} + +void QRhiMetal::draw(QRhiCommandBuffer *cb, quint32 vertexCount, + quint32 instanceCount, quint32 firstVertex, quint32 firstInstance) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + + [cbD->d->currentRenderPassEncoder drawPrimitives: + QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->d->primitiveType + vertexStart: firstVertex vertexCount: vertexCount instanceCount: instanceCount baseInstance: firstInstance]; +} + +void QRhiMetal::drawIndexed(QRhiCommandBuffer *cb, quint32 indexCount, + quint32 instanceCount, quint32 firstIndex, qint32 vertexOffset, quint32 firstInstance) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + + if (!cbD->currentIndexBuffer) + return; + + const quint32 indexOffset = cbD->currentIndexOffset + firstIndex * (cbD->currentIndexFormat == QRhiCommandBuffer::IndexUInt16 ? 2 : 4); + Q_ASSERT(indexOffset == aligned(indexOffset, 4)); + + QMetalBuffer *ibufD = QRHI_RES(QMetalBuffer, cbD->currentIndexBuffer); + id<MTLBuffer> mtlbuf = ibufD->d->buf[ibufD->d->slotted ? currentFrameSlot : 0]; + + [cbD->d->currentRenderPassEncoder drawIndexedPrimitives: QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->d->primitiveType + indexCount: indexCount + indexType: cbD->currentIndexFormat == QRhiCommandBuffer::IndexUInt16 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32 + indexBuffer: mtlbuf + indexBufferOffset: indexOffset + instanceCount: instanceCount + baseVertex: vertexOffset + baseInstance: firstInstance]; +} + +void QRhiMetal::debugMarkBegin(QRhiCommandBuffer *cb, const QByteArray &name) +{ + if (!debugMarkers) + return; + + NSString *str = [NSString stringWithUTF8String: name.constData()]; + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + if (cbD->recordingPass != QMetalCommandBuffer::NoPass) { + [cbD->d->currentRenderPassEncoder pushDebugGroup: str]; + } else { + if (@available(macOS 10.13, iOS 11.0, *)) + [cbD->d->cb pushDebugGroup: str]; + } +} + +void QRhiMetal::debugMarkEnd(QRhiCommandBuffer *cb) +{ + if (!debugMarkers) + return; + + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + if (cbD->recordingPass != QMetalCommandBuffer::NoPass) { + [cbD->d->currentRenderPassEncoder popDebugGroup]; + } else { + if (@available(macOS 10.13, iOS 11.0, *)) + [cbD->d->cb popDebugGroup]; + } +} + +void QRhiMetal::debugMarkMsg(QRhiCommandBuffer *cb, const QByteArray &msg) +{ + if (!debugMarkers) + return; + + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + if (cbD->recordingPass != QMetalCommandBuffer::NoPass) + [cbD->d->currentRenderPassEncoder insertDebugSignpost: [NSString stringWithUTF8String: msg.constData()]]; +} + +const QRhiNativeHandles *QRhiMetal::nativeHandles(QRhiCommandBuffer *cb) +{ + return QRHI_RES(QMetalCommandBuffer, cb)->nativeHandles(); +} + +void QRhiMetal::beginExternal(QRhiCommandBuffer *cb) +{ + Q_UNUSED(cb); +} + +void QRhiMetal::endExternal(QRhiCommandBuffer *cb) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + cbD->resetPerPassCachedState(); +} + +QRhi::FrameOpResult QRhiMetal::beginFrame(QRhiSwapChain *swapChain, QRhi::BeginFrameFlags flags) +{ + Q_UNUSED(flags); + + QMetalSwapChain *swapChainD = QRHI_RES(QMetalSwapChain, swapChain); + + // This is a bit messed up since for this swapchain we want to wait for the + // commands+present to complete, while for others just for the commands + // (for this same frame slot) but not sure how to do that in a sane way so + // wait for full cb completion for now. + for (QMetalSwapChain *sc : qAsConst(swapchains)) { + dispatch_semaphore_t sem = sc->d->sem[swapChainD->currentFrameSlot]; + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + if (sc != swapChainD) + dispatch_semaphore_signal(sem); + } + + currentSwapChain = swapChainD; + currentFrameSlot = swapChainD->currentFrameSlot; + if (swapChainD->ds) + swapChainD->ds->lastActiveFrameSlot = currentFrameSlot; + + if (@available(macOS 10.13, iOS 11.0, *)) + [d->captureScope beginScope]; + + // Do not let the command buffer mess with the refcount of objects. We do + // have a proper render loop and will manage lifetimes similarly to other + // backends (Vulkan). + swapChainD->cbWrapper.d->cb = [d->cmdQueue commandBufferWithUnretainedReferences]; + + QMetalRenderTargetData::ColorAtt colorAtt; + if (swapChainD->samples > 1) { + colorAtt.tex = swapChainD->d->msaaTex[currentFrameSlot]; + colorAtt.needsDrawableForResolveTex = true; + } else { + colorAtt.needsDrawableForTex = true; + } + + swapChainD->rtWrapper.d->fb.colorAtt[0] = colorAtt; + swapChainD->rtWrapper.d->fb.dsTex = swapChainD->ds ? swapChainD->ds->d->tex : nil; + swapChainD->rtWrapper.d->fb.hasStencil = swapChainD->ds ? true : false; + swapChainD->rtWrapper.d->fb.depthNeedsStore = false; + + QRhiProfilerPrivate *rhiP = profilerPrivateOrNull(); + QRHI_PROF_F(beginSwapChainFrame(swapChain)); + + executeDeferredReleases(); + swapChainD->cbWrapper.resetState(); + finishActiveReadbacks(); + + return QRhi::FrameOpSuccess; +} + +QRhi::FrameOpResult QRhiMetal::endFrame(QRhiSwapChain *swapChain, QRhi::EndFrameFlags flags) +{ + QMetalSwapChain *swapChainD = QRHI_RES(QMetalSwapChain, swapChain); + Q_ASSERT(currentSwapChain == swapChainD); + + const bool needsPresent = !flags.testFlag(QRhi::SkipPresent); + if (needsPresent) { + [swapChainD->cbWrapper.d->cb presentDrawable: swapChainD->d->curDrawable]; + swapChainD->d->curDrawable = nil; + } + + __block int thisFrameSlot = currentFrameSlot; + [swapChainD->cbWrapper.d->cb addCompletedHandler: ^(id<MTLCommandBuffer>) { + dispatch_semaphore_signal(swapChainD->d->sem[thisFrameSlot]); + }]; + + [swapChainD->cbWrapper.d->cb commit]; + + QRhiProfilerPrivate *rhiP = profilerPrivateOrNull(); + QRHI_PROF_F(endSwapChainFrame(swapChain, swapChainD->frameCount + 1)); + + if (@available(macOS 10.13, iOS 11.0, *)) + [d->captureScope endScope]; + + if (needsPresent) + swapChainD->currentFrameSlot = (swapChainD->currentFrameSlot + 1) % QMTL_FRAMES_IN_FLIGHT; + + swapChainD->frameCount += 1; + currentSwapChain = nullptr; + return QRhi::FrameOpSuccess; +} + +QRhi::FrameOpResult QRhiMetal::beginOffscreenFrame(QRhiCommandBuffer **cb) +{ + currentFrameSlot = (currentFrameSlot + 1) % QMTL_FRAMES_IN_FLIGHT; + if (swapchains.count() > 1) { + for (QMetalSwapChain *sc : qAsConst(swapchains)) { + // wait+signal is the general pattern to ensure the commands for a + // given frame slot have completed (if sem is 1, we go 0 then 1; if + // sem is 0 we go -1, block, completion increments to 0, then us to 1) + dispatch_semaphore_t sem = sc->d->sem[currentFrameSlot]; + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + dispatch_semaphore_signal(sem); + } + } + + d->ofr.active = true; + *cb = &d->ofr.cbWrapper; + d->ofr.cbWrapper.d->cb = [d->cmdQueue commandBufferWithUnretainedReferences]; + + executeDeferredReleases(); + d->ofr.cbWrapper.resetState(); + finishActiveReadbacks(); + + return QRhi::FrameOpSuccess; +} + +QRhi::FrameOpResult QRhiMetal::endOffscreenFrame() +{ + Q_ASSERT(d->ofr.active); + d->ofr.active = false; + + [d->ofr.cbWrapper.d->cb commit]; + + // offscreen frames wait for completion, unlike swapchain ones + [d->ofr.cbWrapper.d->cb waitUntilCompleted]; + + finishActiveReadbacks(true); + + return QRhi::FrameOpSuccess; +} + +QRhi::FrameOpResult QRhiMetal::finish() +{ + id<MTLCommandBuffer> cb = nil; + QMetalSwapChain *swapChainD = nullptr; + if (inFrame) { + if (d->ofr.active) { + Q_ASSERT(!currentSwapChain); + Q_ASSERT(d->ofr.cbWrapper.recordingPass == QMetalCommandBuffer::NoPass); + cb = d->ofr.cbWrapper.d->cb; + } else { + Q_ASSERT(currentSwapChain); + swapChainD = currentSwapChain; + Q_ASSERT(swapChainD->cbWrapper.recordingPass == QMetalCommandBuffer::NoPass); + cb = swapChainD->cbWrapper.d->cb; + } + } + + for (QMetalSwapChain *sc : qAsConst(swapchains)) { + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + if (currentSwapChain && sc == currentSwapChain && i == currentFrameSlot) { + // no wait as this is the thing we're going to be commit below and + // beginFrame decremented sem already and going to be signaled by endFrame + continue; + } + dispatch_semaphore_t sem = sc->d->sem[i]; + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + dispatch_semaphore_signal(sem); + } + } + + if (cb) { + [cb commit]; + [cb waitUntilCompleted]; + } + + if (inFrame) { + if (d->ofr.active) + d->ofr.cbWrapper.d->cb = [d->cmdQueue commandBufferWithUnretainedReferences]; + else + swapChainD->cbWrapper.d->cb = [d->cmdQueue commandBufferWithUnretainedReferences]; + } + + executeDeferredReleases(true); + + finishActiveReadbacks(true); + + return QRhi::FrameOpSuccess; +} + +MTLRenderPassDescriptor *QRhiMetalData::createDefaultRenderPass(bool hasDepthStencil, + const QColor &colorClearValue, + const QRhiDepthStencilClearValue &depthStencilClearValue, + int colorAttCount) +{ + MTLRenderPassDescriptor *rp = [MTLRenderPassDescriptor renderPassDescriptor]; + MTLClearColor c = MTLClearColorMake(colorClearValue.redF(), colorClearValue.greenF(), colorClearValue.blueF(), + colorClearValue.alphaF()); + + for (int i = 0; i < colorAttCount; ++i) { + rp.colorAttachments[i].loadAction = MTLLoadActionClear; + rp.colorAttachments[i].storeAction = MTLStoreActionStore; + rp.colorAttachments[i].clearColor = c; + } + + if (hasDepthStencil) { + rp.depthAttachment.loadAction = MTLLoadActionClear; + rp.depthAttachment.storeAction = MTLStoreActionDontCare; + rp.stencilAttachment.loadAction = MTLLoadActionClear; + rp.stencilAttachment.storeAction = MTLStoreActionDontCare; + rp.depthAttachment.clearDepth = depthStencilClearValue.depthClearValue(); + rp.stencilAttachment.clearStencil = depthStencilClearValue.stencilClearValue(); + } + + return rp; +} + +qsizetype QRhiMetal::subresUploadByteSize(const QRhiTextureSubresourceUploadDescription &subresDesc) const +{ + qsizetype size = 0; + const qsizetype imageSizeBytes = subresDesc.image().isNull() ? + subresDesc.data().size() : subresDesc.image().sizeInBytes(); + if (imageSizeBytes > 0) + size += aligned(imageSizeBytes, QRhiMetalData::TEXBUF_ALIGN); + return size; +} + +void QRhiMetal::enqueueSubresUpload(QMetalTexture *texD, void *mp, void *blitEncPtr, + int layer, int level, const QRhiTextureSubresourceUploadDescription &subresDesc, + qsizetype *curOfs) +{ + const QPoint dp = subresDesc.destinationTopLeft(); + const QByteArray rawData = subresDesc.data(); + QImage img = subresDesc.image(); + id<MTLBlitCommandEncoder> blitEnc = (id<MTLBlitCommandEncoder>) blitEncPtr; + + if (!img.isNull()) { + const qsizetype fullImageSizeBytes = img.sizeInBytes(); + int w = img.width(); + int h = img.height(); + int bpl = img.bytesPerLine(); + int srcOffset = 0; + + if (!subresDesc.sourceSize().isEmpty() || !subresDesc.sourceTopLeft().isNull()) { + const int sx = subresDesc.sourceTopLeft().x(); + const int sy = subresDesc.sourceTopLeft().y(); + if (!subresDesc.sourceSize().isEmpty()) { + w = subresDesc.sourceSize().width(); + h = subresDesc.sourceSize().height(); + } + if (img.depth() == 32) { + memcpy(reinterpret_cast<char *>(mp) + *curOfs, img.constBits(), fullImageSizeBytes); + srcOffset = sy * bpl + sx * 4; + // bpl remains set to the original image's row stride + } else { + img = img.copy(sx, sy, w, h); + bpl = img.bytesPerLine(); + Q_ASSERT(img.sizeInBytes() <= fullImageSizeBytes); + memcpy(reinterpret_cast<char *>(mp) + *curOfs, img.constBits(), img.sizeInBytes()); + } + } else { + memcpy(reinterpret_cast<char *>(mp) + *curOfs, img.constBits(), fullImageSizeBytes); + } + + [blitEnc copyFromBuffer: texD->d->stagingBuf[currentFrameSlot] + sourceOffset: *curOfs + srcOffset + sourceBytesPerRow: bpl + sourceBytesPerImage: 0 + sourceSize: MTLSizeMake(w, h, 1) + toTexture: texD->d->tex + destinationSlice: layer + destinationLevel: level + destinationOrigin: MTLOriginMake(dp.x(), dp.y(), 0) + options: MTLBlitOptionNone]; + + *curOfs += aligned(fullImageSizeBytes, QRhiMetalData::TEXBUF_ALIGN); + } else if (!rawData.isEmpty() && isCompressedFormat(texD->m_format)) { + const QSize subresSize = q->sizeForMipLevel(level, texD->m_pixelSize); + const int subresw = subresSize.width(); + const int subresh = subresSize.height(); + int w, h; + if (subresDesc.sourceSize().isEmpty()) { + w = subresw; + h = subresh; + } else { + w = subresDesc.sourceSize().width(); + h = subresDesc.sourceSize().height(); + } + + quint32 bpl = 0; + QSize blockDim; + compressedFormatInfo(texD->m_format, QSize(w, h), &bpl, nullptr, &blockDim); + + const int dx = aligned(dp.x(), blockDim.width()); + const int dy = aligned(dp.y(), blockDim.height()); + if (dx + w != subresw) + w = aligned(w, blockDim.width()); + if (dy + h != subresh) + h = aligned(h, blockDim.height()); + + memcpy(reinterpret_cast<char *>(mp) + *curOfs, rawData.constData(), rawData.size()); + + [blitEnc copyFromBuffer: texD->d->stagingBuf[currentFrameSlot] + sourceOffset: *curOfs + sourceBytesPerRow: bpl + sourceBytesPerImage: 0 + sourceSize: MTLSizeMake(w, h, 1) + toTexture: texD->d->tex + destinationSlice: layer + destinationLevel: level + destinationOrigin: MTLOriginMake(dx, dy, 0) + options: MTLBlitOptionNone]; + + *curOfs += aligned(rawData.size(), QRhiMetalData::TEXBUF_ALIGN); + } else if (!rawData.isEmpty()) { + const QSize subresSize = q->sizeForMipLevel(level, texD->m_pixelSize); + const int subresw = subresSize.width(); + const int subresh = subresSize.height(); + int w, h; + if (subresDesc.sourceSize().isEmpty()) { + w = subresw; + h = subresh; + } else { + w = subresDesc.sourceSize().width(); + h = subresDesc.sourceSize().height(); + } + + quint32 bpl = 0; + textureFormatInfo(texD->m_format, QSize(w, h), &bpl, nullptr); + memcpy(reinterpret_cast<char *>(mp) + *curOfs, rawData.constData(), rawData.size()); + + [blitEnc copyFromBuffer: texD->d->stagingBuf[currentFrameSlot] + sourceOffset: *curOfs + sourceBytesPerRow: bpl + sourceBytesPerImage: 0 + sourceSize: MTLSizeMake(w, h, 1) + toTexture: texD->d->tex + destinationSlice: layer + destinationLevel: level + destinationOrigin: MTLOriginMake(dp.x(), dp.y(), 0) + options: MTLBlitOptionNone]; + + *curOfs += aligned(rawData.size(), QRhiMetalData::TEXBUF_ALIGN); + } else { + qWarning("Invalid texture upload for %p layer=%d mip=%d", texD, layer, level); + } +} + +void QRhiMetal::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + QRhiResourceUpdateBatchPrivate *ud = QRhiResourceUpdateBatchPrivate::get(resourceUpdates); + QRhiProfilerPrivate *rhiP = profilerPrivateOrNull(); + + for (const QRhiResourceUpdateBatchPrivate::DynamicBufferUpdate &u : ud->dynamicBufferUpdates) { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, u.buf); + Q_ASSERT(bufD->m_type == QRhiBuffer::Dynamic); + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) + bufD->d->pendingUpdates[i].append(u); + } + + // Due to the Metal API the handling of static and dynamic buffers is + // basically the same. So go through the same pendingUpdates machinery. + for (const QRhiResourceUpdateBatchPrivate::StaticBufferUpload &u : ud->staticBufferUploads) { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, u.buf); + Q_ASSERT(bufD->m_type != QRhiBuffer::Dynamic); + Q_ASSERT(u.offset + u.data.size() <= bufD->m_size); + for (int i = 0, ie = bufD->d->slotted ? QMTL_FRAMES_IN_FLIGHT : 1; i != ie; ++i) + bufD->d->pendingUpdates[i].append({ u.buf, u.offset, u.data.size(), u.data.constData() }); + } + + id<MTLBlitCommandEncoder> blitEnc = nil; + auto ensureBlit = [&blitEnc, cbD, this] { + if (!blitEnc) { + blitEnc = [cbD->d->cb blitCommandEncoder]; + if (debugMarkers) + [blitEnc pushDebugGroup: @"Texture upload/copy"]; + } + }; + + for (const QRhiResourceUpdateBatchPrivate::TextureOp &u : ud->textureOps) { + if (u.type == QRhiResourceUpdateBatchPrivate::TextureOp::Upload) { + QMetalTexture *utexD = QRHI_RES(QMetalTexture, u.upload.tex); + qsizetype stagingSize = 0; + for (int layer = 0; layer < QRhi::MAX_LAYERS; ++layer) { + for (int level = 0; level < QRhi::MAX_LEVELS; ++level) { + for (const QRhiTextureSubresourceUploadDescription &subresDesc : qAsConst(u.upload.subresDesc[layer][level])) + stagingSize += subresUploadByteSize(subresDesc); + } + } + + ensureBlit(); + Q_ASSERT(!utexD->d->stagingBuf[currentFrameSlot]); + utexD->d->stagingBuf[currentFrameSlot] = [d->dev newBufferWithLength: stagingSize + options: MTLResourceStorageModeShared]; + QRHI_PROF_F(newTextureStagingArea(utexD, currentFrameSlot, stagingSize)); + + void *mp = [utexD->d->stagingBuf[currentFrameSlot] contents]; + qsizetype curOfs = 0; + for (int layer = 0; layer < QRhi::MAX_LAYERS; ++layer) { + for (int level = 0; level < QRhi::MAX_LEVELS; ++level) { + for (const QRhiTextureSubresourceUploadDescription &subresDesc : qAsConst(u.upload.subresDesc[layer][level])) + enqueueSubresUpload(utexD, mp, blitEnc, layer, level, subresDesc, &curOfs); + } + } + + utexD->lastActiveFrameSlot = currentFrameSlot; + + QRhiMetalData::DeferredReleaseEntry e; + e.type = QRhiMetalData::DeferredReleaseEntry::StagingBuffer; + e.lastActiveFrameSlot = currentFrameSlot; + e.stagingBuffer.buffer = utexD->d->stagingBuf[currentFrameSlot]; + utexD->d->stagingBuf[currentFrameSlot] = nil; + d->releaseQueue.append(e); + QRHI_PROF_F(releaseTextureStagingArea(utexD, currentFrameSlot)); + } else if (u.type == QRhiResourceUpdateBatchPrivate::TextureOp::Copy) { + Q_ASSERT(u.copy.src && u.copy.dst); + QMetalTexture *srcD = QRHI_RES(QMetalTexture, u.copy.src); + QMetalTexture *dstD = QRHI_RES(QMetalTexture, u.copy.dst); + const QPoint dp = u.copy.desc.destinationTopLeft(); + const QSize size = u.copy.desc.pixelSize().isEmpty() ? srcD->m_pixelSize : u.copy.desc.pixelSize(); + const QPoint sp = u.copy.desc.sourceTopLeft(); + + ensureBlit(); + [blitEnc copyFromTexture: srcD->d->tex + sourceSlice: u.copy.desc.sourceLayer() + sourceLevel: u.copy.desc.sourceLevel() + sourceOrigin: MTLOriginMake(sp.x(), sp.y(), 0) + sourceSize: MTLSizeMake(size.width(), size.height(), 1) + toTexture: dstD->d->tex + destinationSlice: u.copy.desc.destinationLayer() + destinationLevel: u.copy.desc.destinationLevel() + destinationOrigin: MTLOriginMake(dp.x(), dp.y(), 0)]; + + srcD->lastActiveFrameSlot = dstD->lastActiveFrameSlot = currentFrameSlot; + } else if (u.type == QRhiResourceUpdateBatchPrivate::TextureOp::Read) { + QRhiMetalData::ActiveReadback aRb; + aRb.activeFrameSlot = currentFrameSlot; + aRb.desc = u.read.rb; + aRb.result = u.read.result; + + QMetalTexture *texD = QRHI_RES(QMetalTexture, u.read.rb.texture()); + QMetalSwapChain *swapChainD = nullptr; + id<MTLTexture> src; + QSize srcSize; + if (texD) { + if (texD->samples > 1) { + qWarning("Multisample texture cannot be read back"); + continue; + } + aRb.pixelSize = u.read.rb.level() > 0 ? q->sizeForMipLevel(u.read.rb.level(), texD->m_pixelSize) + : texD->m_pixelSize; + aRb.format = texD->m_format; + src = texD->d->tex; + srcSize = texD->m_pixelSize; + texD->lastActiveFrameSlot = currentFrameSlot; + } else { + Q_ASSERT(currentSwapChain); + swapChainD = QRHI_RES(QMetalSwapChain, currentSwapChain); + aRb.pixelSize = swapChainD->pixelSize; + aRb.format = swapChainD->d->rhiColorFormat; + // Multisample swapchains need nothing special since resolving + // happens when ending a renderpass. + const QMetalRenderTargetData::ColorAtt &colorAtt(swapChainD->rtWrapper.d->fb.colorAtt[0]); + src = colorAtt.resolveTex ? colorAtt.resolveTex : colorAtt.tex; + srcSize = swapChainD->rtWrapper.d->pixelSize; + } + + quint32 bpl = 0; + textureFormatInfo(aRb.format, aRb.pixelSize, &bpl, &aRb.bufSize); + aRb.buf = [d->dev newBufferWithLength: aRb.bufSize options: MTLResourceStorageModeShared]; + + QRHI_PROF_F(newReadbackBuffer(quint64(quintptr(aRb.buf)), + texD ? static_cast<QRhiResource *>(texD) : static_cast<QRhiResource *>(swapChainD), + aRb.bufSize)); + + ensureBlit(); + [blitEnc copyFromTexture: src + sourceSlice: u.read.rb.layer() + sourceLevel: u.read.rb.level() + sourceOrigin: MTLOriginMake(0, 0, 0) + sourceSize: MTLSizeMake(srcSize.width(), srcSize.height(), 1) + toBuffer: aRb.buf + destinationOffset: 0 + destinationBytesPerRow: bpl + destinationBytesPerImage: 0 + options: MTLBlitOptionNone]; + + d->activeReadbacks.append(aRb); + } else if (u.type == QRhiResourceUpdateBatchPrivate::TextureOp::MipGen) { + QMetalTexture *utexD = QRHI_RES(QMetalTexture, u.mipgen.tex); + ensureBlit(); + [blitEnc generateMipmapsForTexture: utexD->d->tex]; + utexD->lastActiveFrameSlot = currentFrameSlot; + } + } + + if (blitEnc) { + if (debugMarkers) + [blitEnc popDebugGroup]; + [blitEnc endEncoding]; + } + + ud->free(); +} + +// this handles all types of buffers, not just Dynamic +void QRhiMetal::executeBufferHostWritesForCurrentFrame(QMetalBuffer *bufD) +{ + const int idx = bufD->d->slotted ? currentFrameSlot : 0; + QVector<QRhiResourceUpdateBatchPrivate::DynamicBufferUpdate> &updates(bufD->d->pendingUpdates[idx]); + if (updates.isEmpty()) + return; + + void *p = [bufD->d->buf[idx] contents]; + int changeBegin = -1; + int changeEnd = -1; + for (const QRhiResourceUpdateBatchPrivate::DynamicBufferUpdate &u : updates) { + Q_ASSERT(bufD == QRHI_RES(QMetalBuffer, u.buf)); + memcpy(static_cast<char *>(p) + u.offset, u.data.constData(), u.data.size()); + if (changeBegin == -1 || u.offset < changeBegin) + changeBegin = u.offset; + if (changeEnd == -1 || u.offset + u.data.size() > changeEnd) + changeEnd = u.offset + u.data.size(); + } + if (changeBegin >= 0 && bufD->d->managed) + [bufD->d->buf[idx] didModifyRange: NSMakeRange(changeBegin, changeEnd - changeBegin)]; + + updates.clear(); +} + +void QRhiMetal::resourceUpdate(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates) +{ + Q_ASSERT(QRHI_RES(QMetalCommandBuffer, cb)->recordingPass == QMetalCommandBuffer::NoPass); + + enqueueResourceUpdates(cb, resourceUpdates); +} + +void QRhiMetal::beginPass(QRhiCommandBuffer *cb, + QRhiRenderTarget *rt, + const QColor &colorClearValue, + const QRhiDepthStencilClearValue &depthStencilClearValue, + QRhiResourceUpdateBatch *resourceUpdates) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::NoPass); + + if (resourceUpdates) + enqueueResourceUpdates(cb, resourceUpdates); + + QMetalRenderTargetData *rtD = nullptr; + switch (rt->resourceType()) { + case QRhiResource::RenderTarget: + rtD = QRHI_RES(QMetalReferenceRenderTarget, rt)->d; + cbD->d->currentPassRpDesc = d->createDefaultRenderPass(rtD->dsAttCount, colorClearValue, depthStencilClearValue, rtD->colorAttCount); + if (rtD->colorAttCount) { + QMetalRenderTargetData::ColorAtt &color0(rtD->fb.colorAtt[0]); + if (color0.needsDrawableForTex || color0.needsDrawableForResolveTex) { + Q_ASSERT(currentSwapChain); + QMetalSwapChain *swapChainD = QRHI_RES(QMetalSwapChain, currentSwapChain); + if (!swapChainD->d->curDrawable) + swapChainD->d->curDrawable = [swapChainD->d->layer nextDrawable]; + if (!swapChainD->d->curDrawable) { + qWarning("No drawable"); + return; + } + id<MTLTexture> scTex = swapChainD->d->curDrawable.texture; + if (color0.needsDrawableForTex) { + color0.tex = scTex; + color0.needsDrawableForTex = false; + } else { + color0.resolveTex = scTex; + color0.needsDrawableForResolveTex = false; + } + } + } + break; + case QRhiResource::TextureRenderTarget: + { + QMetalTextureRenderTarget *rtTex = QRHI_RES(QMetalTextureRenderTarget, rt); + rtD = rtTex->d; + cbD->d->currentPassRpDesc = d->createDefaultRenderPass(rtD->dsAttCount, colorClearValue, depthStencilClearValue, rtD->colorAttCount); + if (rtTex->m_flags.testFlag(QRhiTextureRenderTarget::PreserveColorContents)) { + for (int i = 0; i < rtD->colorAttCount; ++i) + cbD->d->currentPassRpDesc.colorAttachments[i].loadAction = MTLLoadActionLoad; + } + if (rtD->dsAttCount && rtTex->m_flags.testFlag(QRhiTextureRenderTarget::PreserveDepthStencilContents)) { + cbD->d->currentPassRpDesc.depthAttachment.loadAction = MTLLoadActionLoad; + cbD->d->currentPassRpDesc.stencilAttachment.loadAction = MTLLoadActionLoad; + } + const QVector<QRhiColorAttachment> colorAttachments = rtTex->m_desc.colorAttachments(); + for (const QRhiColorAttachment &colorAttachment : colorAttachments) { + if (colorAttachment.texture()) + QRHI_RES(QMetalTexture, colorAttachment.texture())->lastActiveFrameSlot = currentFrameSlot; + else if (colorAttachment.renderBuffer()) + QRHI_RES(QMetalRenderBuffer, colorAttachment.renderBuffer())->lastActiveFrameSlot = currentFrameSlot; + if (colorAttachment.resolveTexture()) + QRHI_RES(QMetalTexture, colorAttachment.resolveTexture())->lastActiveFrameSlot = currentFrameSlot; + } + if (rtTex->m_desc.depthStencilBuffer()) + QRHI_RES(QMetalRenderBuffer, rtTex->m_desc.depthStencilBuffer())->lastActiveFrameSlot = currentFrameSlot; + if (rtTex->m_desc.depthTexture()) + QRHI_RES(QMetalTexture, rtTex->m_desc.depthTexture())->lastActiveFrameSlot = currentFrameSlot; + } + break; + default: + Q_UNREACHABLE(); + break; + } + + for (int i = 0; i < rtD->colorAttCount; ++i) { + cbD->d->currentPassRpDesc.colorAttachments[i].texture = rtD->fb.colorAtt[i].tex; + cbD->d->currentPassRpDesc.colorAttachments[i].slice = rtD->fb.colorAtt[i].layer; + cbD->d->currentPassRpDesc.colorAttachments[i].level = rtD->fb.colorAtt[i].level; + if (rtD->fb.colorAtt[i].resolveTex) { + cbD->d->currentPassRpDesc.colorAttachments[i].storeAction = MTLStoreActionMultisampleResolve; + cbD->d->currentPassRpDesc.colorAttachments[i].resolveTexture = rtD->fb.colorAtt[i].resolveTex; + cbD->d->currentPassRpDesc.colorAttachments[i].resolveSlice = rtD->fb.colorAtt[i].resolveLayer; + cbD->d->currentPassRpDesc.colorAttachments[i].resolveLevel = rtD->fb.colorAtt[i].resolveLevel; + } + } + + if (rtD->dsAttCount) { + Q_ASSERT(rtD->fb.dsTex); + cbD->d->currentPassRpDesc.depthAttachment.texture = rtD->fb.dsTex; + cbD->d->currentPassRpDesc.stencilAttachment.texture = rtD->fb.hasStencil ? rtD->fb.dsTex : nil; + if (rtD->fb.depthNeedsStore) // Depth/Stencil is set to DontCare by default, override if needed + cbD->d->currentPassRpDesc.depthAttachment.storeAction = MTLStoreActionStore; + } + + cbD->d->currentRenderPassEncoder = [cbD->d->cb renderCommandEncoderWithDescriptor: cbD->d->currentPassRpDesc]; + + cbD->resetPerPassState(); + + cbD->recordingPass = QMetalCommandBuffer::RenderPass; + cbD->currentTarget = rt; +} + +void QRhiMetal::endPass(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::RenderPass); + + [cbD->d->currentRenderPassEncoder endEncoding]; + + cbD->recordingPass = QMetalCommandBuffer::NoPass; + cbD->currentTarget = nullptr; + + if (resourceUpdates) + enqueueResourceUpdates(cb, resourceUpdates); +} + +void QRhiMetal::beginComputePass(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::NoPass); + + if (resourceUpdates) + enqueueResourceUpdates(cb, resourceUpdates); + + cbD->d->currentComputePassEncoder = [cbD->d->cb computeCommandEncoder]; + cbD->resetPerPassState(); + cbD->recordingPass = QMetalCommandBuffer::ComputePass; +} + +void QRhiMetal::endComputePass(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::ComputePass); + + [cbD->d->currentComputePassEncoder endEncoding]; + cbD->recordingPass = QMetalCommandBuffer::NoPass; + + if (resourceUpdates) + enqueueResourceUpdates(cb, resourceUpdates); +} + +void QRhiMetal::setComputePipeline(QRhiCommandBuffer *cb, QRhiComputePipeline *ps) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::ComputePass); + QMetalComputePipeline *psD = QRHI_RES(QMetalComputePipeline, ps); + + if (cbD->currentComputePipeline != ps || cbD->currentPipelineGeneration != psD->generation) { + cbD->currentGraphicsPipeline = nullptr; + cbD->currentComputePipeline = ps; + cbD->currentPipelineGeneration = psD->generation; + + [cbD->d->currentComputePassEncoder setComputePipelineState: psD->d->ps]; + } + + psD->lastActiveFrameSlot = currentFrameSlot; +} + +void QRhiMetal::dispatch(QRhiCommandBuffer *cb, int x, int y, int z) +{ + QMetalCommandBuffer *cbD = QRHI_RES(QMetalCommandBuffer, cb); + Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::ComputePass); + QMetalComputePipeline *psD = QRHI_RES(QMetalComputePipeline, cbD->currentComputePipeline); + + [cbD->d->currentComputePassEncoder dispatchThreadgroups: MTLSizeMake(x, y, z) + threadsPerThreadgroup: psD->d->localSize]; +} + +static void qrhimtl_releaseBuffer(const QRhiMetalData::DeferredReleaseEntry &e) +{ + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) + [e.buffer.buffers[i] release]; +} + +static void qrhimtl_releaseRenderBuffer(const QRhiMetalData::DeferredReleaseEntry &e) +{ + [e.renderbuffer.texture release]; +} + +static void qrhimtl_releaseTexture(const QRhiMetalData::DeferredReleaseEntry &e) +{ + [e.texture.texture release]; + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) + [e.texture.stagingBuffers[i] release]; + for (int i = 0; i < QRhi::MAX_LEVELS; ++i) + [e.texture.views[i] release]; +} + +static void qrhimtl_releaseSampler(const QRhiMetalData::DeferredReleaseEntry &e) +{ + [e.sampler.samplerState release]; +} + +void QRhiMetal::executeDeferredReleases(bool forced) +{ + for (int i = d->releaseQueue.count() - 1; i >= 0; --i) { + const QRhiMetalData::DeferredReleaseEntry &e(d->releaseQueue[i]); + if (forced || currentFrameSlot == e.lastActiveFrameSlot || e.lastActiveFrameSlot < 0) { + switch (e.type) { + case QRhiMetalData::DeferredReleaseEntry::Buffer: + qrhimtl_releaseBuffer(e); + break; + case QRhiMetalData::DeferredReleaseEntry::RenderBuffer: + qrhimtl_releaseRenderBuffer(e); + break; + case QRhiMetalData::DeferredReleaseEntry::Texture: + qrhimtl_releaseTexture(e); + break; + case QRhiMetalData::DeferredReleaseEntry::Sampler: + qrhimtl_releaseSampler(e); + break; + case QRhiMetalData::DeferredReleaseEntry::StagingBuffer: + [e.stagingBuffer.buffer release]; + break; + default: + break; + } + d->releaseQueue.removeAt(i); + } + } +} + +void QRhiMetal::finishActiveReadbacks(bool forced) +{ + QVarLengthArray<std::function<void()>, 4> completedCallbacks; + QRhiProfilerPrivate *rhiP = profilerPrivateOrNull(); + + for (int i = d->activeReadbacks.count() - 1; i >= 0; --i) { + const QRhiMetalData::ActiveReadback &aRb(d->activeReadbacks[i]); + if (forced || currentFrameSlot == aRb.activeFrameSlot || aRb.activeFrameSlot < 0) { + aRb.result->format = aRb.format; + aRb.result->pixelSize = aRb.pixelSize; + aRb.result->data.resize(aRb.bufSize); + void *p = [aRb.buf contents]; + memcpy(aRb.result->data.data(), p, aRb.bufSize); + [aRb.buf release]; + + QRHI_PROF_F(releaseReadbackBuffer(quint64(quintptr(aRb.buf)))); + + if (aRb.result->completed) + completedCallbacks.append(aRb.result->completed); + + d->activeReadbacks.removeAt(i); + } + } + + for (auto f : completedCallbacks) + f(); +} + +QMetalBuffer::QMetalBuffer(QRhiImplementation *rhi, Type type, UsageFlags usage, int size) + : QRhiBuffer(rhi, type, usage, size), + d(new QMetalBufferData) +{ + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) + d->buf[i] = nil; +} + +QMetalBuffer::~QMetalBuffer() +{ + release(); + delete d; +} + +void QMetalBuffer::release() +{ + if (!d->buf[0]) + return; + + QRhiMetalData::DeferredReleaseEntry e; + e.type = QRhiMetalData::DeferredReleaseEntry::Buffer; + e.lastActiveFrameSlot = lastActiveFrameSlot; + + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + e.buffer.buffers[i] = d->buf[i]; + d->buf[i] = nil; + d->pendingUpdates[i].clear(); + } + + QRHI_RES_RHI(QRhiMetal); + rhiD->d->releaseQueue.append(e); + QRHI_PROF; + QRHI_PROF_F(releaseBuffer(this)); + rhiD->unregisterResource(this); +} + +bool QMetalBuffer::build() +{ + if (d->buf[0]) + release(); + + if (m_usage.testFlag(QRhiBuffer::StorageBuffer) && m_type == Dynamic) { + qWarning("StorageBuffer cannot be combined with Dynamic"); + return false; + } + + const int nonZeroSize = m_size <= 0 ? 256 : m_size; + const int roundedSize = m_usage.testFlag(QRhiBuffer::UniformBuffer) ? aligned(nonZeroSize, 256) : nonZeroSize; + + d->managed = false; + MTLResourceOptions opts = MTLResourceStorageModeShared; +#ifdef Q_OS_MACOS + if (m_type != Dynamic) { + opts = MTLResourceStorageModeManaged; + d->managed = true; + } +#endif + + // Immutable and Static only has buf[0] and pendingUpdates[0] in use. + // Dynamic uses all. + d->slotted = m_type == Dynamic; + + QRHI_RES_RHI(QRhiMetal); + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + if (i == 0 || d->slotted) { + d->buf[i] = [rhiD->d->dev newBufferWithLength: roundedSize options: opts]; + d->pendingUpdates[i].reserve(16); + if (!m_objectName.isEmpty()) { + if (!d->slotted) { + d->buf[i].label = [NSString stringWithUTF8String: m_objectName.constData()]; + } else { + const QByteArray name = m_objectName + '/' + QByteArray::number(i); + d->buf[i].label = [NSString stringWithUTF8String: name.constData()]; + } + } + } + } + + QRHI_PROF; + QRHI_PROF_F(newBuffer(this, roundedSize, d->slotted ? QMTL_FRAMES_IN_FLIGHT : 1, 0)); + + lastActiveFrameSlot = -1; + generation += 1; + rhiD->registerResource(this); + return true; +} + +QMetalRenderBuffer::QMetalRenderBuffer(QRhiImplementation *rhi, Type type, const QSize &pixelSize, + int sampleCount, QRhiRenderBuffer::Flags flags) + : QRhiRenderBuffer(rhi, type, pixelSize, sampleCount, flags), + d(new QMetalRenderBufferData) +{ +} + +QMetalRenderBuffer::~QMetalRenderBuffer() +{ + release(); + delete d; +} + +void QMetalRenderBuffer::release() +{ + if (!d->tex) + return; + + QRhiMetalData::DeferredReleaseEntry e; + e.type = QRhiMetalData::DeferredReleaseEntry::RenderBuffer; + e.lastActiveFrameSlot = lastActiveFrameSlot; + + e.renderbuffer.texture = d->tex; + d->tex = nil; + + QRHI_RES_RHI(QRhiMetal); + rhiD->d->releaseQueue.append(e); + QRHI_PROF; + QRHI_PROF_F(releaseRenderBuffer(this)); + rhiD->unregisterResource(this); +} + +bool QMetalRenderBuffer::build() +{ + if (d->tex) + release(); + + if (m_pixelSize.isEmpty()) + return false; + + QRHI_RES_RHI(QRhiMetal); + samples = rhiD->effectiveSampleCount(m_sampleCount); + + MTLTextureDescriptor *desc = [[MTLTextureDescriptor alloc] init]; + desc.textureType = samples > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; + desc.width = m_pixelSize.width(); + desc.height = m_pixelSize.height(); + if (samples > 1) + desc.sampleCount = samples; + desc.resourceOptions = MTLResourceStorageModePrivate; + desc.usage = MTLTextureUsageRenderTarget; + + bool transientBacking = false; + switch (m_type) { + case DepthStencil: +#ifdef Q_OS_MACOS + desc.storageMode = MTLStorageModePrivate; +#else + desc.storageMode = MTLResourceStorageModeMemoryless; + transientBacking = true; +#endif + d->format = rhiD->d->dev.depth24Stencil8PixelFormatSupported + ? MTLPixelFormatDepth24Unorm_Stencil8 : MTLPixelFormatDepth32Float_Stencil8; + desc.pixelFormat = d->format; + break; + case Color: + desc.storageMode = MTLStorageModePrivate; + d->format = MTLPixelFormatRGBA8Unorm; + desc.pixelFormat = d->format; + break; + default: + Q_UNREACHABLE(); + break; + } + + d->tex = [rhiD->d->dev newTextureWithDescriptor: desc]; + [desc release]; + + if (!m_objectName.isEmpty()) + d->tex.label = [NSString stringWithUTF8String: m_objectName.constData()]; + + QRHI_PROF; + QRHI_PROF_F(newRenderBuffer(this, transientBacking, false, samples)); + + lastActiveFrameSlot = -1; + generation += 1; + rhiD->registerResource(this); + return true; +} + +QRhiTexture::Format QMetalRenderBuffer::backingFormat() const +{ + return m_type == Color ? QRhiTexture::RGBA8 : QRhiTexture::UnknownFormat; +} + +QMetalTexture::QMetalTexture(QRhiImplementation *rhi, Format format, const QSize &pixelSize, + int sampleCount, Flags flags) + : QRhiTexture(rhi, format, pixelSize, sampleCount, flags), + d(new QMetalTextureData(this)) +{ + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) + d->stagingBuf[i] = nil; + + for (int i = 0; i < QRhi::MAX_LEVELS; ++i) + d->perLevelViews[i] = nil; +} + +QMetalTexture::~QMetalTexture() +{ + release(); + delete d; +} + +void QMetalTexture::release() +{ + if (!d->tex) + return; + + QRhiMetalData::DeferredReleaseEntry e; + e.type = QRhiMetalData::DeferredReleaseEntry::Texture; + e.lastActiveFrameSlot = lastActiveFrameSlot; + + e.texture.texture = d->owns ? d->tex : nil; + d->tex = nil; + nativeHandlesStruct.texture = nullptr; + + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + e.texture.stagingBuffers[i] = d->stagingBuf[i]; + d->stagingBuf[i] = nil; + } + + for (int i = 0; i < QRhi::MAX_LEVELS; ++i) { + e.texture.views[i] = d->perLevelViews[i]; + d->perLevelViews[i] = nil; + } + + QRHI_RES_RHI(QRhiMetal); + rhiD->d->releaseQueue.append(e); + QRHI_PROF; + QRHI_PROF_F(releaseTexture(this)); + rhiD->unregisterResource(this); +} + +static inline MTLPixelFormat toMetalTextureFormat(QRhiTexture::Format format, QRhiTexture::Flags flags) +{ + const bool srgb = flags.testFlag(QRhiTexture::sRGB); + switch (format) { + case QRhiTexture::RGBA8: + return srgb ? MTLPixelFormatRGBA8Unorm_sRGB : MTLPixelFormatRGBA8Unorm; + case QRhiTexture::BGRA8: + return srgb ? MTLPixelFormatBGRA8Unorm_sRGB : MTLPixelFormatBGRA8Unorm; + case QRhiTexture::R8: +#ifdef Q_OS_MACOS + return MTLPixelFormatR8Unorm; +#else + return srgb ? MTLPixelFormatR8Unorm_sRGB : MTLPixelFormatR8Unorm; +#endif + case QRhiTexture::R16: + return MTLPixelFormatR16Unorm; + case QRhiTexture::RED_OR_ALPHA8: + return MTLPixelFormatR8Unorm; + + case QRhiTexture::RGBA16F: + return MTLPixelFormatRGBA16Float; + case QRhiTexture::RGBA32F: + return MTLPixelFormatRGBA32Float; + + case QRhiTexture::D16: +#ifdef Q_OS_MACOS + return MTLPixelFormatDepth16Unorm; +#else + return MTLPixelFormatDepth32Float; +#endif + case QRhiTexture::D32F: + return MTLPixelFormatDepth32Float; + +#ifdef Q_OS_MACOS + case QRhiTexture::BC1: + return srgb ? MTLPixelFormatBC1_RGBA_sRGB : MTLPixelFormatBC1_RGBA; + case QRhiTexture::BC2: + return srgb ? MTLPixelFormatBC2_RGBA_sRGB : MTLPixelFormatBC2_RGBA; + case QRhiTexture::BC3: + return srgb ? MTLPixelFormatBC3_RGBA_sRGB : MTLPixelFormatBC3_RGBA; + case QRhiTexture::BC4: + return MTLPixelFormatBC4_RUnorm; + case QRhiTexture::BC5: + qWarning("QRhiMetal does not support BC5"); + return MTLPixelFormatRGBA8Unorm; + case QRhiTexture::BC6H: + return MTLPixelFormatBC6H_RGBUfloat; + case QRhiTexture::BC7: + return srgb ? MTLPixelFormatBC7_RGBAUnorm_sRGB : MTLPixelFormatBC7_RGBAUnorm; +#else + case QRhiTexture::BC1: + case QRhiTexture::BC2: + case QRhiTexture::BC3: + case QRhiTexture::BC4: + case QRhiTexture::BC5: + case QRhiTexture::BC6H: + case QRhiTexture::BC7: + qWarning("QRhiMetal: BCx compression not supported on this platform"); + return MTLPixelFormatRGBA8Unorm; +#endif + +#ifndef Q_OS_MACOS + case QRhiTexture::ETC2_RGB8: + return srgb ? MTLPixelFormatETC2_RGB8_sRGB : MTLPixelFormatETC2_RGB8; + case QRhiTexture::ETC2_RGB8A1: + return srgb ? MTLPixelFormatETC2_RGB8A1_sRGB : MTLPixelFormatETC2_RGB8A1; + case QRhiTexture::ETC2_RGBA8: + return srgb ? MTLPixelFormatEAC_RGBA8_sRGB : MTLPixelFormatEAC_RGBA8; + + case QRhiTexture::ASTC_4x4: + return srgb ? MTLPixelFormatASTC_4x4_sRGB : MTLPixelFormatASTC_4x4_LDR; + case QRhiTexture::ASTC_5x4: + return srgb ? MTLPixelFormatASTC_5x4_sRGB : MTLPixelFormatASTC_5x4_LDR; + case QRhiTexture::ASTC_5x5: + return srgb ? MTLPixelFormatASTC_5x5_sRGB : MTLPixelFormatASTC_5x5_LDR; + case QRhiTexture::ASTC_6x5: + return srgb ? MTLPixelFormatASTC_6x5_sRGB : MTLPixelFormatASTC_6x5_LDR; + case QRhiTexture::ASTC_6x6: + return srgb ? MTLPixelFormatASTC_6x6_sRGB : MTLPixelFormatASTC_6x6_LDR; + case QRhiTexture::ASTC_8x5: + return srgb ? MTLPixelFormatASTC_8x5_sRGB : MTLPixelFormatASTC_8x5_LDR; + case QRhiTexture::ASTC_8x6: + return srgb ? MTLPixelFormatASTC_8x6_sRGB : MTLPixelFormatASTC_8x6_LDR; + case QRhiTexture::ASTC_8x8: + return srgb ? MTLPixelFormatASTC_8x8_sRGB : MTLPixelFormatASTC_8x8_LDR; + case QRhiTexture::ASTC_10x5: + return srgb ? MTLPixelFormatASTC_10x5_sRGB : MTLPixelFormatASTC_10x5_LDR; + case QRhiTexture::ASTC_10x6: + return srgb ? MTLPixelFormatASTC_10x6_sRGB : MTLPixelFormatASTC_10x6_LDR; + case QRhiTexture::ASTC_10x8: + return srgb ? MTLPixelFormatASTC_10x8_sRGB : MTLPixelFormatASTC_10x8_LDR; + case QRhiTexture::ASTC_10x10: + return srgb ? MTLPixelFormatASTC_10x10_sRGB : MTLPixelFormatASTC_10x10_LDR; + case QRhiTexture::ASTC_12x10: + return srgb ? MTLPixelFormatASTC_12x10_sRGB : MTLPixelFormatASTC_12x10_LDR; + case QRhiTexture::ASTC_12x12: + return srgb ? MTLPixelFormatASTC_12x12_sRGB : MTLPixelFormatASTC_12x12_LDR; +#else + case QRhiTexture::ETC2_RGB8: + case QRhiTexture::ETC2_RGB8A1: + case QRhiTexture::ETC2_RGBA8: + qWarning("QRhiMetal: ETC2 compression not supported on this platform"); + return MTLPixelFormatRGBA8Unorm; + + case QRhiTexture::ASTC_4x4: + case QRhiTexture::ASTC_5x4: + case QRhiTexture::ASTC_5x5: + case QRhiTexture::ASTC_6x5: + case QRhiTexture::ASTC_6x6: + case QRhiTexture::ASTC_8x5: + case QRhiTexture::ASTC_8x6: + case QRhiTexture::ASTC_8x8: + case QRhiTexture::ASTC_10x5: + case QRhiTexture::ASTC_10x6: + case QRhiTexture::ASTC_10x8: + case QRhiTexture::ASTC_10x10: + case QRhiTexture::ASTC_12x10: + case QRhiTexture::ASTC_12x12: + qWarning("QRhiMetal: ASTC compression not supported on this platform"); + return MTLPixelFormatRGBA8Unorm; +#endif + + default: + Q_UNREACHABLE(); + return MTLPixelFormatRGBA8Unorm; + } +} + +bool QMetalTexture::prepareBuild(QSize *adjustedSize) +{ + if (d->tex) + release(); + + const QSize size = m_pixelSize.isEmpty() ? QSize(1, 1) : m_pixelSize; + const bool isCube = m_flags.testFlag(CubeMap); + const bool hasMipMaps = m_flags.testFlag(MipMapped); + + QRHI_RES_RHI(QRhiMetal); + d->format = toMetalTextureFormat(m_format, m_flags); + mipLevelCount = hasMipMaps ? rhiD->q->mipLevelsForSize(size) : 1; + samples = rhiD->effectiveSampleCount(m_sampleCount); + if (samples > 1) { + if (isCube) { + qWarning("Cubemap texture cannot be multisample"); + return false; + } + if (hasMipMaps) { + qWarning("Multisample texture cannot have mipmaps"); + return false; + } + } + + if (adjustedSize) + *adjustedSize = size; + + return true; +} + +bool QMetalTexture::build() +{ + QSize size; + if (!prepareBuild(&size)) + return false; + + MTLTextureDescriptor *desc = [[MTLTextureDescriptor alloc] init]; + + const bool isCube = m_flags.testFlag(CubeMap); + if (isCube) + desc.textureType = MTLTextureTypeCube; + else + desc.textureType = samples > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; + desc.pixelFormat = d->format; + desc.width = size.width(); + desc.height = size.height(); + desc.mipmapLevelCount = mipLevelCount; + if (samples > 1) + desc.sampleCount = samples; + desc.resourceOptions = MTLResourceStorageModePrivate; + desc.storageMode = MTLStorageModePrivate; + desc.usage = MTLTextureUsageShaderRead; + if (m_flags.testFlag(RenderTarget)) + desc.usage |= MTLTextureUsageRenderTarget; + if (m_flags.testFlag(UsedWithLoadStore)) + desc.usage |= MTLTextureUsageShaderWrite; + + QRHI_RES_RHI(QRhiMetal); + d->tex = [rhiD->d->dev newTextureWithDescriptor: desc]; + [desc release]; + + if (!m_objectName.isEmpty()) + d->tex.label = [NSString stringWithUTF8String: m_objectName.constData()]; + + d->owns = true; + nativeHandlesStruct.texture = d->tex; + + QRHI_PROF; + QRHI_PROF_F(newTexture(this, true, mipLevelCount, isCube ? 6 : 1, samples)); + + lastActiveFrameSlot = -1; + generation += 1; + rhiD->registerResource(this); + return true; +} + +bool QMetalTexture::buildFrom(const QRhiNativeHandles *src) +{ + const QRhiMetalTextureNativeHandles *h = static_cast<const QRhiMetalTextureNativeHandles *>(src); + if (!h || !h->texture) + return false; + + if (!prepareBuild()) + return false; + + d->tex = (id<MTLTexture>) h->texture; + + d->owns = false; + nativeHandlesStruct.texture = d->tex; + + QRHI_PROF; + QRHI_PROF_F(newTexture(this, false, mipLevelCount, m_flags.testFlag(CubeMap) ? 6 : 1, samples)); + + lastActiveFrameSlot = -1; + generation += 1; + QRHI_RES_RHI(QRhiMetal); + rhiD->registerResource(this); + return true; +} + +const QRhiNativeHandles *QMetalTexture::nativeHandles() +{ + return &nativeHandlesStruct; +} + +id<MTLTexture> QMetalTextureData::viewForLevel(int level) +{ + Q_ASSERT(level >= 0 && level < int(q->mipLevelCount)); + if (perLevelViews[level]) + return perLevelViews[level]; + + const MTLTextureType type = [tex textureType]; + const bool isCube = q->m_flags.testFlag(QRhiTexture::CubeMap); + id<MTLTexture> view = [tex newTextureViewWithPixelFormat: format textureType: type + levels: NSMakeRange(level, 1) slices: NSMakeRange(0, isCube ? 6 : 1)]; + + perLevelViews[level] = view; + return view; +} + +QMetalSampler::QMetalSampler(QRhiImplementation *rhi, Filter magFilter, Filter minFilter, Filter mipmapMode, + AddressMode u, AddressMode v) + : QRhiSampler(rhi, magFilter, minFilter, mipmapMode, u, v), + d(new QMetalSamplerData) +{ +} + +QMetalSampler::~QMetalSampler() +{ + release(); + delete d; +} + +void QMetalSampler::release() +{ + if (!d->samplerState) + return; + + QRhiMetalData::DeferredReleaseEntry e; + e.type = QRhiMetalData::DeferredReleaseEntry::Sampler; + e.lastActiveFrameSlot = lastActiveFrameSlot; + + e.sampler.samplerState = d->samplerState; + d->samplerState = nil; + + QRHI_RES_RHI(QRhiMetal); + rhiD->d->releaseQueue.append(e); + rhiD->unregisterResource(this); +} + +static inline MTLSamplerMinMagFilter toMetalFilter(QRhiSampler::Filter f) +{ + switch (f) { + case QRhiSampler::Nearest: + return MTLSamplerMinMagFilterNearest; + case QRhiSampler::Linear: + return MTLSamplerMinMagFilterLinear; + default: + Q_UNREACHABLE(); + return MTLSamplerMinMagFilterNearest; + } +} + +static inline MTLSamplerMipFilter toMetalMipmapMode(QRhiSampler::Filter f) +{ + switch (f) { + case QRhiSampler::None: + return MTLSamplerMipFilterNotMipmapped; + case QRhiSampler::Nearest: + return MTLSamplerMipFilterNearest; + case QRhiSampler::Linear: + return MTLSamplerMipFilterLinear; + default: + Q_UNREACHABLE(); + return MTLSamplerMipFilterNotMipmapped; + } +} + +static inline MTLSamplerAddressMode toMetalAddressMode(QRhiSampler::AddressMode m) +{ + switch (m) { + case QRhiSampler::Repeat: + return MTLSamplerAddressModeRepeat; + case QRhiSampler::ClampToEdge: + return MTLSamplerAddressModeClampToEdge; + case QRhiSampler::Border: + return MTLSamplerAddressModeClampToBorderColor; + case QRhiSampler::Mirror: + return MTLSamplerAddressModeMirrorRepeat; + case QRhiSampler::MirrorOnce: + return MTLSamplerAddressModeMirrorClampToEdge; + default: + Q_UNREACHABLE(); + return MTLSamplerAddressModeClampToEdge; + } +} + +static inline MTLCompareFunction toMetalTextureCompareFunction(QRhiSampler::CompareOp op) +{ + switch (op) { + case QRhiSampler::Never: + return MTLCompareFunctionNever; + case QRhiSampler::Less: + return MTLCompareFunctionLess; + case QRhiSampler::Equal: + return MTLCompareFunctionEqual; + case QRhiSampler::LessOrEqual: + return MTLCompareFunctionLessEqual; + case QRhiSampler::Greater: + return MTLCompareFunctionGreater; + case QRhiSampler::NotEqual: + return MTLCompareFunctionNotEqual; + case QRhiSampler::GreaterOrEqual: + return MTLCompareFunctionGreaterEqual; + case QRhiSampler::Always: + return MTLCompareFunctionAlways; + default: + Q_UNREACHABLE(); + return MTLCompareFunctionNever; + } +} + +bool QMetalSampler::build() +{ + if (d->samplerState) + release(); + + MTLSamplerDescriptor *desc = [[MTLSamplerDescriptor alloc] init]; + desc.minFilter = toMetalFilter(m_minFilter); + desc.magFilter = toMetalFilter(m_magFilter); + desc.mipFilter = toMetalMipmapMode(m_mipmapMode); + desc.sAddressMode = toMetalAddressMode(m_addressU); + desc.tAddressMode = toMetalAddressMode(m_addressV); + desc.rAddressMode = toMetalAddressMode(m_addressW); + desc.compareFunction = toMetalTextureCompareFunction(m_compareOp); + + QRHI_RES_RHI(QRhiMetal); + d->samplerState = [rhiD->d->dev newSamplerStateWithDescriptor: desc]; + [desc release]; + + lastActiveFrameSlot = -1; + generation += 1; + rhiD->registerResource(this); + return true; +} + +// dummy, no Vulkan-style RenderPass+Framebuffer concept here. +// We do have MTLRenderPassDescriptor of course, but it will be created on the fly for each pass. +QMetalRenderPassDescriptor::QMetalRenderPassDescriptor(QRhiImplementation *rhi) + : QRhiRenderPassDescriptor(rhi) +{ +} + +QMetalRenderPassDescriptor::~QMetalRenderPassDescriptor() +{ + release(); +} + +void QMetalRenderPassDescriptor::release() +{ + // nothing to do here +} + +QMetalReferenceRenderTarget::QMetalReferenceRenderTarget(QRhiImplementation *rhi) + : QRhiRenderTarget(rhi), + d(new QMetalRenderTargetData) +{ +} + +QMetalReferenceRenderTarget::~QMetalReferenceRenderTarget() +{ + release(); + delete d; +} + +void QMetalReferenceRenderTarget::release() +{ + // nothing to do here +} + +QSize QMetalReferenceRenderTarget::pixelSize() const +{ + return d->pixelSize; +} + +float QMetalReferenceRenderTarget::devicePixelRatio() const +{ + return d->dpr; +} + +int QMetalReferenceRenderTarget::sampleCount() const +{ + return d->sampleCount; +} + +QMetalTextureRenderTarget::QMetalTextureRenderTarget(QRhiImplementation *rhi, + const QRhiTextureRenderTargetDescription &desc, + Flags flags) + : QRhiTextureRenderTarget(rhi, desc, flags), + d(new QMetalRenderTargetData) +{ +} + +QMetalTextureRenderTarget::~QMetalTextureRenderTarget() +{ + release(); + delete d; +} + +void QMetalTextureRenderTarget::release() +{ + // nothing to do here +} + +QRhiRenderPassDescriptor *QMetalTextureRenderTarget::newCompatibleRenderPassDescriptor() +{ + const QVector<QRhiColorAttachment> colorAttachments = m_desc.colorAttachments(); + QMetalRenderPassDescriptor *rpD = new QMetalRenderPassDescriptor(m_rhi); + rpD->colorAttachmentCount = colorAttachments.count(); + rpD->hasDepthStencil = m_desc.depthStencilBuffer() || m_desc.depthTexture(); + + for (int i = 0, ie = colorAttachments.count(); i != ie; ++i) { + QMetalTexture *texD = QRHI_RES(QMetalTexture, colorAttachments[i].texture()); + QMetalRenderBuffer *rbD = QRHI_RES(QMetalRenderBuffer, colorAttachments[i].renderBuffer()); + rpD->colorFormat[i] = texD ? texD->d->format : rbD->d->format; + } + + if (m_desc.depthTexture()) + rpD->dsFormat = QRHI_RES(QMetalTexture, m_desc.depthTexture())->d->format; + else if (m_desc.depthStencilBuffer()) + rpD->dsFormat = QRHI_RES(QMetalRenderBuffer, m_desc.depthStencilBuffer())->d->format; + + return rpD; +} + +bool QMetalTextureRenderTarget::build() +{ + const QVector<QRhiColorAttachment> colorAttachments = m_desc.colorAttachments(); + Q_ASSERT(!colorAttachments.isEmpty() || m_desc.depthTexture()); + Q_ASSERT(!m_desc.depthStencilBuffer() || !m_desc.depthTexture()); + const bool hasDepthStencil = m_desc.depthStencilBuffer() || m_desc.depthTexture(); + + d->colorAttCount = colorAttachments.count(); + for (int i = 0; i < d->colorAttCount; ++i) { + QMetalTexture *texD = QRHI_RES(QMetalTexture, colorAttachments[i].texture()); + QMetalRenderBuffer *rbD = QRHI_RES(QMetalRenderBuffer, colorAttachments[i].renderBuffer()); + Q_ASSERT(texD || rbD); + id<MTLTexture> dst = nil; + if (texD) { + dst = texD->d->tex; + if (i == 0) { + d->pixelSize = texD->pixelSize(); + d->sampleCount = texD->samples; + } + } else if (rbD) { + dst = rbD->d->tex; + if (i == 0) { + d->pixelSize = rbD->pixelSize(); + d->sampleCount = rbD->samples; + } + } + QMetalRenderTargetData::ColorAtt colorAtt; + colorAtt.tex = dst; + colorAtt.layer = colorAttachments[i].layer(); + colorAtt.level = colorAttachments[i].level(); + QMetalTexture *resTexD = QRHI_RES(QMetalTexture, colorAttachments[i].resolveTexture()); + colorAtt.resolveTex = resTexD ? resTexD->d->tex : nil; + colorAtt.resolveLayer = colorAttachments[i].resolveLayer(); + colorAtt.resolveLevel = colorAttachments[i].resolveLevel(); + d->fb.colorAtt[i] = colorAtt; + } + d->dpr = 1; + + if (hasDepthStencil) { + if (m_desc.depthTexture()) { + QMetalTexture *depthTexD = QRHI_RES(QMetalTexture, m_desc.depthTexture()); + d->fb.dsTex = depthTexD->d->tex; + d->fb.hasStencil = false; + d->fb.depthNeedsStore = true; + if (d->colorAttCount == 0) { + d->pixelSize = depthTexD->pixelSize(); + d->sampleCount = depthTexD->samples; + } + } else { + QMetalRenderBuffer *depthRbD = QRHI_RES(QMetalRenderBuffer, m_desc.depthStencilBuffer()); + d->fb.dsTex = depthRbD->d->tex; + d->fb.hasStencil = true; + d->fb.depthNeedsStore = false; + if (d->colorAttCount == 0) { + d->pixelSize = depthRbD->pixelSize(); + d->sampleCount = depthRbD->samples; + } + } + d->dsAttCount = 1; + } else { + d->dsAttCount = 0; + } + + return true; +} + +QSize QMetalTextureRenderTarget::pixelSize() const +{ + return d->pixelSize; +} + +float QMetalTextureRenderTarget::devicePixelRatio() const +{ + return d->dpr; +} + +int QMetalTextureRenderTarget::sampleCount() const +{ + return d->sampleCount; +} + +QMetalShaderResourceBindings::QMetalShaderResourceBindings(QRhiImplementation *rhi) + : QRhiShaderResourceBindings(rhi) +{ +} + +QMetalShaderResourceBindings::~QMetalShaderResourceBindings() +{ + release(); +} + +void QMetalShaderResourceBindings::release() +{ + sortedBindings.clear(); + maxBinding = -1; +} + +bool QMetalShaderResourceBindings::build() +{ + if (!sortedBindings.isEmpty()) + release(); + + sortedBindings = m_bindings; + std::sort(sortedBindings.begin(), sortedBindings.end(), + [](const QRhiShaderResourceBinding &a, const QRhiShaderResourceBinding &b) + { + return QRhiShaderResourceBindingPrivate::get(&a)->binding < QRhiShaderResourceBindingPrivate::get(&b)->binding; + }); + if (!sortedBindings.isEmpty()) + maxBinding = QRhiShaderResourceBindingPrivate::get(&sortedBindings.last())->binding; + else + maxBinding = -1; + + boundResourceData.resize(sortedBindings.count()); + + for (int i = 0, ie = sortedBindings.count(); i != ie; ++i) { + const QRhiShaderResourceBindingPrivate *b = QRhiShaderResourceBindingPrivate::get(&sortedBindings[i]); + QMetalShaderResourceBindings::BoundResourceData &bd(boundResourceData[i]); + switch (b->type) { + case QRhiShaderResourceBinding::UniformBuffer: + { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.ubuf.buf); + bd.ubuf.id = bufD->m_id; + bd.ubuf.generation = bufD->generation; + } + break; + case QRhiShaderResourceBinding::SampledTexture: + { + QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.tex); + QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.sampler); + bd.stex.texId = texD->m_id; + bd.stex.texGeneration = texD->generation; + bd.stex.samplerId = samplerD->m_id; + bd.stex.samplerGeneration = samplerD->generation; + } + break; + case QRhiShaderResourceBinding::ImageLoad: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::ImageStore: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::ImageLoadStore: + { + QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.simage.tex); + bd.simage.id = texD->m_id; + bd.simage.generation = texD->generation; + } + break; + case QRhiShaderResourceBinding::BufferLoad: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::BufferStore: + Q_FALLTHROUGH(); + case QRhiShaderResourceBinding::BufferLoadStore: + { + QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.sbuf.buf); + bd.sbuf.id = bufD->m_id; + bd.sbuf.generation = bufD->generation; + } + break; + default: + Q_UNREACHABLE(); + break; + } + } + + generation += 1; + return true; +} + +QMetalGraphicsPipeline::QMetalGraphicsPipeline(QRhiImplementation *rhi) + : QRhiGraphicsPipeline(rhi), + d(new QMetalGraphicsPipelineData) +{ +} + +QMetalGraphicsPipeline::~QMetalGraphicsPipeline() +{ + release(); + delete d; +} + +void QMetalGraphicsPipeline::release() +{ + QRHI_RES_RHI(QRhiMetal); + + if (!d->ps) + return; + + if (d->ps) { + [d->ps release]; + d->ps = nil; + } + + if (d->ds) { + [d->ds release]; + d->ds = nil; + } + + if (d->vsFunc) { + [d->vsFunc release]; + d->vsFunc = nil; + } + if (d->vsLib) { + [d->vsLib release]; + d->vsLib = nil; + } + + if (d->fsFunc) { + [d->fsFunc release]; + d->fsFunc = nil; + } + if (d->fsLib) { + [d->fsLib release]; + d->fsLib = nil; + } + + rhiD->unregisterResource(this); +} + +static inline MTLVertexFormat toMetalAttributeFormat(QRhiVertexInputAttribute::Format format) +{ + switch (format) { + case QRhiVertexInputAttribute::Float4: + return MTLVertexFormatFloat4; + case QRhiVertexInputAttribute::Float3: + return MTLVertexFormatFloat3; + case QRhiVertexInputAttribute::Float2: + return MTLVertexFormatFloat2; + case QRhiVertexInputAttribute::Float: + return MTLVertexFormatFloat; + case QRhiVertexInputAttribute::UNormByte4: + return MTLVertexFormatUChar4Normalized; + case QRhiVertexInputAttribute::UNormByte2: + return MTLVertexFormatUChar2Normalized; + case QRhiVertexInputAttribute::UNormByte: + if (@available(macOS 10.13, iOS 11.0, *)) + return MTLVertexFormatUCharNormalized; + else + Q_UNREACHABLE(); + default: + Q_UNREACHABLE(); + return MTLVertexFormatFloat4; + } +} + +static inline MTLBlendFactor toMetalBlendFactor(QRhiGraphicsPipeline::BlendFactor f) +{ + switch (f) { + case QRhiGraphicsPipeline::Zero: + return MTLBlendFactorZero; + case QRhiGraphicsPipeline::One: + return MTLBlendFactorOne; + case QRhiGraphicsPipeline::SrcColor: + return MTLBlendFactorSourceColor; + case QRhiGraphicsPipeline::OneMinusSrcColor: + return MTLBlendFactorOneMinusSourceColor; + case QRhiGraphicsPipeline::DstColor: + return MTLBlendFactorDestinationColor; + case QRhiGraphicsPipeline::OneMinusDstColor: + return MTLBlendFactorOneMinusDestinationColor; + case QRhiGraphicsPipeline::SrcAlpha: + return MTLBlendFactorSourceAlpha; + case QRhiGraphicsPipeline::OneMinusSrcAlpha: + return MTLBlendFactorOneMinusSourceAlpha; + case QRhiGraphicsPipeline::DstAlpha: + return MTLBlendFactorDestinationAlpha; + case QRhiGraphicsPipeline::OneMinusDstAlpha: + return MTLBlendFactorOneMinusDestinationAlpha; + case QRhiGraphicsPipeline::ConstantColor: + return MTLBlendFactorBlendColor; + case QRhiGraphicsPipeline::ConstantAlpha: + return MTLBlendFactorBlendAlpha; + case QRhiGraphicsPipeline::OneMinusConstantColor: + return MTLBlendFactorOneMinusBlendColor; + case QRhiGraphicsPipeline::OneMinusConstantAlpha: + return MTLBlendFactorOneMinusBlendAlpha; + case QRhiGraphicsPipeline::SrcAlphaSaturate: + return MTLBlendFactorSourceAlphaSaturated; + case QRhiGraphicsPipeline::Src1Color: + return MTLBlendFactorSource1Color; + case QRhiGraphicsPipeline::OneMinusSrc1Color: + return MTLBlendFactorOneMinusSource1Color; + case QRhiGraphicsPipeline::Src1Alpha: + return MTLBlendFactorSource1Alpha; + case QRhiGraphicsPipeline::OneMinusSrc1Alpha: + return MTLBlendFactorOneMinusSource1Alpha; + default: + Q_UNREACHABLE(); + return MTLBlendFactorZero; + } +} + +static inline MTLBlendOperation toMetalBlendOp(QRhiGraphicsPipeline::BlendOp op) +{ + switch (op) { + case QRhiGraphicsPipeline::Add: + return MTLBlendOperationAdd; + case QRhiGraphicsPipeline::Subtract: + return MTLBlendOperationSubtract; + case QRhiGraphicsPipeline::ReverseSubtract: + return MTLBlendOperationReverseSubtract; + case QRhiGraphicsPipeline::Min: + return MTLBlendOperationMin; + case QRhiGraphicsPipeline::Max: + return MTLBlendOperationMax; + default: + Q_UNREACHABLE(); + return MTLBlendOperationAdd; + } +} + +static inline uint toMetalColorWriteMask(QRhiGraphicsPipeline::ColorMask c) +{ + uint f = 0; + if (c.testFlag(QRhiGraphicsPipeline::R)) + f |= MTLColorWriteMaskRed; + if (c.testFlag(QRhiGraphicsPipeline::G)) + f |= MTLColorWriteMaskGreen; + if (c.testFlag(QRhiGraphicsPipeline::B)) + f |= MTLColorWriteMaskBlue; + if (c.testFlag(QRhiGraphicsPipeline::A)) + f |= MTLColorWriteMaskAlpha; + return f; +} + +static inline MTLCompareFunction toMetalCompareOp(QRhiGraphicsPipeline::CompareOp op) +{ + switch (op) { + case QRhiGraphicsPipeline::Never: + return MTLCompareFunctionNever; + case QRhiGraphicsPipeline::Less: + return MTLCompareFunctionLess; + case QRhiGraphicsPipeline::Equal: + return MTLCompareFunctionEqual; + case QRhiGraphicsPipeline::LessOrEqual: + return MTLCompareFunctionLessEqual; + case QRhiGraphicsPipeline::Greater: + return MTLCompareFunctionGreater; + case QRhiGraphicsPipeline::NotEqual: + return MTLCompareFunctionNotEqual; + case QRhiGraphicsPipeline::GreaterOrEqual: + return MTLCompareFunctionGreaterEqual; + case QRhiGraphicsPipeline::Always: + return MTLCompareFunctionAlways; + default: + Q_UNREACHABLE(); + return MTLCompareFunctionAlways; + } +} + +static inline MTLStencilOperation toMetalStencilOp(QRhiGraphicsPipeline::StencilOp op) +{ + switch (op) { + case QRhiGraphicsPipeline::StencilZero: + return MTLStencilOperationZero; + case QRhiGraphicsPipeline::Keep: + return MTLStencilOperationKeep; + case QRhiGraphicsPipeline::Replace: + return MTLStencilOperationReplace; + case QRhiGraphicsPipeline::IncrementAndClamp: + return MTLStencilOperationIncrementClamp; + case QRhiGraphicsPipeline::DecrementAndClamp: + return MTLStencilOperationDecrementClamp; + case QRhiGraphicsPipeline::Invert: + return MTLStencilOperationInvert; + case QRhiGraphicsPipeline::IncrementAndWrap: + return MTLStencilOperationIncrementWrap; + case QRhiGraphicsPipeline::DecrementAndWrap: + return MTLStencilOperationDecrementWrap; + default: + Q_UNREACHABLE(); + return MTLStencilOperationKeep; + } +} + +static inline MTLPrimitiveType toMetalPrimitiveType(QRhiGraphicsPipeline::Topology t) +{ + switch (t) { + case QRhiGraphicsPipeline::Triangles: + return MTLPrimitiveTypeTriangle; + case QRhiGraphicsPipeline::TriangleStrip: + return MTLPrimitiveTypeTriangleStrip; + case QRhiGraphicsPipeline::Lines: + return MTLPrimitiveTypeLine; + case QRhiGraphicsPipeline::LineStrip: + return MTLPrimitiveTypeLineStrip; + case QRhiGraphicsPipeline::Points: + return MTLPrimitiveTypePoint; + default: + Q_UNREACHABLE(); + return MTLPrimitiveTypeTriangle; + } +} + +static inline MTLCullMode toMetalCullMode(QRhiGraphicsPipeline::CullMode c) +{ + switch (c) { + case QRhiGraphicsPipeline::None: + return MTLCullModeNone; + case QRhiGraphicsPipeline::Front: + return MTLCullModeFront; + case QRhiGraphicsPipeline::Back: + return MTLCullModeBack; + default: + Q_UNREACHABLE(); + return MTLCullModeNone; + } +} + +id<MTLLibrary> QRhiMetalData::createMetalLib(const QShader &shader, QShader::Variant shaderVariant, + QString *error, QByteArray *entryPoint) +{ + QShaderCode mtllib = shader.shader({ QShader::MetalLibShader, 12, shaderVariant }); + if (!mtllib.shader().isEmpty()) { + dispatch_data_t data = dispatch_data_create(mtllib.shader().constData(), + mtllib.shader().size(), + dispatch_get_global_queue(0, 0), + DISPATCH_DATA_DESTRUCTOR_DEFAULT); + NSError *err = nil; + id<MTLLibrary> lib = [dev newLibraryWithData: data error: &err]; + dispatch_release(data); + if (!err) { + *entryPoint = mtllib.entryPoint(); + return lib; + } else { + const QString msg = QString::fromNSString(err.localizedDescription); + qWarning("Failed to load metallib from baked shader: %s", qPrintable(msg)); + } + } + + QShaderCode mslSource = shader.shader({ QShader::MslShader, 12, shaderVariant }); + if (mslSource.shader().isEmpty()) { + qWarning() << "No MSL 1.2 code found in baked shader" << shader; + return nil; + } + + NSString *src = [NSString stringWithUTF8String: mslSource.shader().constData()]; + MTLCompileOptions *opts = [[MTLCompileOptions alloc] init]; + opts.languageVersion = MTLLanguageVersion1_2; + NSError *err = nil; + id<MTLLibrary> lib = [dev newLibraryWithSource: src options: opts error: &err]; + [opts release]; + // src is autoreleased + + if (err) { + const QString msg = QString::fromNSString(err.localizedDescription); + *error = msg; + return nil; + } + + *entryPoint = mslSource.entryPoint(); + return lib; +} + +id<MTLFunction> QRhiMetalData::createMSLShaderFunction(id<MTLLibrary> lib, const QByteArray &entryPoint) +{ + NSString *name = [NSString stringWithUTF8String: entryPoint.constData()]; + id<MTLFunction> f = [lib newFunctionWithName: name]; + [name release]; + return f; +} + +bool QMetalGraphicsPipeline::build() +{ + if (d->ps) + release(); + + QRHI_RES_RHI(QRhiMetal); + + // same binding space for vertex and constant buffers - work it around + const int firstVertexBinding = QRHI_RES(QMetalShaderResourceBindings, m_shaderResourceBindings)->maxBinding + 1; + + MTLVertexDescriptor *inputLayout = [MTLVertexDescriptor vertexDescriptor]; + const QVector<QRhiVertexInputAttribute> attributes = m_vertexInputLayout.attributes(); + for (const QRhiVertexInputAttribute &attribute : attributes) { + const int loc = attribute.location(); + inputLayout.attributes[loc].format = toMetalAttributeFormat(attribute.format()); + inputLayout.attributes[loc].offset = attribute.offset(); + inputLayout.attributes[loc].bufferIndex = firstVertexBinding + attribute.binding(); + } + const QVector<QRhiVertexInputBinding> bindings = m_vertexInputLayout.bindings(); + for (int i = 0, ie = bindings.count(); i != ie; ++i) { + const QRhiVertexInputBinding &binding(bindings[i]); + const int layoutIdx = firstVertexBinding + i; + inputLayout.layouts[layoutIdx].stepFunction = + binding.classification() == QRhiVertexInputBinding::PerInstance + ? MTLVertexStepFunctionPerInstance : MTLVertexStepFunctionPerVertex; + inputLayout.layouts[layoutIdx].stepRate = binding.instanceStepRate(); + inputLayout.layouts[layoutIdx].stride = binding.stride(); + } + + MTLRenderPipelineDescriptor *rpDesc = [[MTLRenderPipelineDescriptor alloc] init]; + + rpDesc.vertexDescriptor = inputLayout; + + // mutability cannot be determined (slotted buffers could be set as + // MTLMutabilityImmutable, but then we potentially need a different + // descriptor for each buffer combination as this depends on the actual + // buffers not just the resource binding layout) so leave it at the default + + for (const QRhiShaderStage &shaderStage : qAsConst(m_shaderStages)) { + QString error; + QByteArray entryPoint; + id<MTLLibrary> lib = rhiD->d->createMetalLib(shaderStage.shader(), shaderStage.shaderVariant(), &error, &entryPoint); + if (!lib) { + qWarning("MSL shader compilation failed: %s", qPrintable(error)); + return false; + } + id<MTLFunction> func = rhiD->d->createMSLShaderFunction(lib, entryPoint); + if (!func) { + qWarning("MSL function for entry point %s not found", entryPoint.constData()); + [lib release]; + return false; + } + switch (shaderStage.type()) { + case QRhiShaderStage::Vertex: + rpDesc.vertexFunction = func; + d->vsLib = lib; + d->vsFunc = func; + break; + case QRhiShaderStage::Fragment: + rpDesc.fragmentFunction = func; + d->fsLib = lib; + d->fsFunc = func; + break; + default: + [func release]; + [lib release]; + break; + } + } + + QMetalRenderPassDescriptor *rpD = QRHI_RES(QMetalRenderPassDescriptor, m_renderPassDesc); + + if (rpD->colorAttachmentCount) { + // defaults when no targetBlends are provided + rpDesc.colorAttachments[0].pixelFormat = MTLPixelFormat(rpD->colorFormat[0]); + rpDesc.colorAttachments[0].writeMask = MTLColorWriteMaskAll; + rpDesc.colorAttachments[0].blendingEnabled = false; + + Q_ASSERT(m_targetBlends.count() == rpD->colorAttachmentCount + || (m_targetBlends.isEmpty() && rpD->colorAttachmentCount == 1)); + + for (int i = 0, ie = m_targetBlends.count(); i != ie; ++i) { + const QRhiGraphicsPipeline::TargetBlend &b(m_targetBlends[i]); + rpDesc.colorAttachments[i].pixelFormat = MTLPixelFormat(rpD->colorFormat[i]); + rpDesc.colorAttachments[i].blendingEnabled = b.enable; + rpDesc.colorAttachments[i].sourceRGBBlendFactor = toMetalBlendFactor(b.srcColor); + rpDesc.colorAttachments[i].destinationRGBBlendFactor = toMetalBlendFactor(b.dstColor); + rpDesc.colorAttachments[i].rgbBlendOperation = toMetalBlendOp(b.opColor); + rpDesc.colorAttachments[i].sourceAlphaBlendFactor = toMetalBlendFactor(b.srcAlpha); + rpDesc.colorAttachments[i].destinationAlphaBlendFactor = toMetalBlendFactor(b.dstAlpha); + rpDesc.colorAttachments[i].alphaBlendOperation = toMetalBlendOp(b.opAlpha); + rpDesc.colorAttachments[i].writeMask = toMetalColorWriteMask(b.colorWrite); + } + } + + if (rpD->hasDepthStencil) { + // Must only be set when a depth-stencil buffer will actually be bound, + // validation blows up otherwise. + MTLPixelFormat fmt = MTLPixelFormat(rpD->dsFormat); + rpDesc.depthAttachmentPixelFormat = fmt; + if (fmt != MTLPixelFormatDepth16Unorm && fmt != MTLPixelFormatDepth32Float) + rpDesc.stencilAttachmentPixelFormat = fmt; + } + + rpDesc.sampleCount = rhiD->effectiveSampleCount(m_sampleCount); + + NSError *err = nil; + d->ps = [rhiD->d->dev newRenderPipelineStateWithDescriptor: rpDesc error: &err]; + if (!d->ps) { + const QString msg = QString::fromNSString(err.localizedDescription); + qWarning("Failed to create render pipeline state: %s", qPrintable(msg)); + [rpDesc release]; + return false; + } + [rpDesc release]; + + MTLDepthStencilDescriptor *dsDesc = [[MTLDepthStencilDescriptor alloc] init]; + dsDesc.depthCompareFunction = m_depthTest ? toMetalCompareOp(m_depthOp) : MTLCompareFunctionAlways; + dsDesc.depthWriteEnabled = m_depthWrite; + if (m_stencilTest) { + dsDesc.frontFaceStencil = [[MTLStencilDescriptor alloc] init]; + dsDesc.frontFaceStencil.stencilFailureOperation = toMetalStencilOp(m_stencilFront.failOp); + dsDesc.frontFaceStencil.depthFailureOperation = toMetalStencilOp(m_stencilFront.depthFailOp); + dsDesc.frontFaceStencil.depthStencilPassOperation = toMetalStencilOp(m_stencilFront.passOp); + dsDesc.frontFaceStencil.stencilCompareFunction = toMetalCompareOp(m_stencilFront.compareOp); + dsDesc.frontFaceStencil.readMask = m_stencilReadMask; + dsDesc.frontFaceStencil.writeMask = m_stencilWriteMask; + + dsDesc.backFaceStencil = [[MTLStencilDescriptor alloc] init]; + dsDesc.backFaceStencil.stencilFailureOperation = toMetalStencilOp(m_stencilBack.failOp); + dsDesc.backFaceStencil.depthFailureOperation = toMetalStencilOp(m_stencilBack.depthFailOp); + dsDesc.backFaceStencil.depthStencilPassOperation = toMetalStencilOp(m_stencilBack.passOp); + dsDesc.backFaceStencil.stencilCompareFunction = toMetalCompareOp(m_stencilBack.compareOp); + dsDesc.backFaceStencil.readMask = m_stencilReadMask; + dsDesc.backFaceStencil.writeMask = m_stencilWriteMask; + } + + d->ds = [rhiD->d->dev newDepthStencilStateWithDescriptor: dsDesc]; + [dsDesc release]; + + d->primitiveType = toMetalPrimitiveType(m_topology); + d->winding = m_frontFace == CCW ? MTLWindingCounterClockwise : MTLWindingClockwise; + d->cullMode = toMetalCullMode(m_cullMode); + + lastActiveFrameSlot = -1; + generation += 1; + rhiD->registerResource(this); + return true; +} + +QMetalComputePipeline::QMetalComputePipeline(QRhiImplementation *rhi) + : QRhiComputePipeline(rhi), + d(new QMetalComputePipelineData) +{ +} + +QMetalComputePipeline::~QMetalComputePipeline() +{ + release(); + delete d; +} + +void QMetalComputePipeline::release() +{ + QRHI_RES_RHI(QRhiMetal); + + if (d->csFunc) { + [d->csFunc release]; + d->csFunc = nil; + } + if (d->csLib) { + [d->csLib release]; + d->csLib = nil; + } + + if (!d->ps) + return; + + if (d->ps) { + [d->ps release]; + d->ps = nil; + } + + rhiD->unregisterResource(this); +} + +bool QMetalComputePipeline::build() +{ + if (d->ps) + release(); + + QRHI_RES_RHI(QRhiMetal); + + const QShader shader = m_shaderStage.shader(); + QString error; + QByteArray entryPoint; + id<MTLLibrary> lib = rhiD->d->createMetalLib(shader, m_shaderStage.shaderVariant(), + &error, &entryPoint); + if (!lib) { + qWarning("MSL shader compilation failed: %s", qPrintable(error)); + return false; + } + id<MTLFunction> func = rhiD->d->createMSLShaderFunction(lib, entryPoint); + if (!func) { + qWarning("MSL function for entry point %s not found", entryPoint.constData()); + [lib release]; + return false; + } + d->csLib = lib; + d->csFunc = func; + std::array<uint, 3> localSize = shader.description().computeShaderLocalSize(); + d->localSize = MTLSizeMake(localSize[0], localSize[1], localSize[2]); + + NSError *err = nil; + d->ps = [rhiD->d->dev newComputePipelineStateWithFunction: d->csFunc error: &err]; + if (!d->ps) { + const QString msg = QString::fromNSString(err.localizedDescription); + qWarning("Failed to create render pipeline state: %s", qPrintable(msg)); + return false; + } + + lastActiveFrameSlot = -1; + generation += 1; + rhiD->registerResource(this); + return true; +} + +QMetalCommandBuffer::QMetalCommandBuffer(QRhiImplementation *rhi) + : QRhiCommandBuffer(rhi), + d(new QMetalCommandBufferData) +{ + resetState(); +} + +QMetalCommandBuffer::~QMetalCommandBuffer() +{ + release(); + delete d; +} + +void QMetalCommandBuffer::release() +{ + // nothing to do here, we do not own the MTL cb object +} + +const QRhiNativeHandles *QMetalCommandBuffer::nativeHandles() +{ + nativeHandlesStruct.commandBuffer = d->cb; + nativeHandlesStruct.encoder = d->currentRenderPassEncoder; + return &nativeHandlesStruct; +} + +void QMetalCommandBuffer::resetState() +{ + d->currentRenderPassEncoder = nil; + d->currentComputePassEncoder = nil; + d->currentPassRpDesc = nil; + resetPerPassState(); +} + +void QMetalCommandBuffer::resetPerPassState() +{ + recordingPass = NoPass; + currentTarget = nullptr; + resetPerPassCachedState(); +} + +void QMetalCommandBuffer::resetPerPassCachedState() +{ + currentGraphicsPipeline = nullptr; + currentComputePipeline = nullptr; + currentPipelineGeneration = 0; + currentGraphicsSrb = nullptr; + currentComputeSrb = nullptr; + currentSrbGeneration = 0; + currentResSlot = -1; + currentIndexBuffer = nullptr; + + d->currentFirstVertexBinding = -1; + d->currentVertexInputsBuffers.clear(); + d->currentVertexInputOffsets.clear(); +} + +QMetalSwapChain::QMetalSwapChain(QRhiImplementation *rhi) + : QRhiSwapChain(rhi), + rtWrapper(rhi), + cbWrapper(rhi), + d(new QMetalSwapChainData) +{ + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + d->sem[i] = nullptr; + d->msaaTex[i] = nil; + } +} + +QMetalSwapChain::~QMetalSwapChain() +{ + release(); + delete d; +} + +void QMetalSwapChain::release() +{ + if (!d->layer) + return; + + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + if (d->sem[i]) { + // the semaphores cannot be released if they do not have the initial value + dispatch_semaphore_wait(d->sem[i], DISPATCH_TIME_FOREVER); + dispatch_semaphore_signal(d->sem[i]); + + dispatch_release(d->sem[i]); + d->sem[i] = nullptr; + } + } + + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + [d->msaaTex[i] release]; + d->msaaTex[i] = nil; + } + + d->layer = nullptr; + + QRHI_RES_RHI(QRhiMetal); + rhiD->swapchains.remove(this); + + QRHI_PROF; + QRHI_PROF_F(releaseSwapChain(this)); + + rhiD->unregisterResource(this); +} + +QRhiCommandBuffer *QMetalSwapChain::currentFrameCommandBuffer() +{ + return &cbWrapper; +} + +QRhiRenderTarget *QMetalSwapChain::currentFrameRenderTarget() +{ + return &rtWrapper; +} + +QSize QMetalSwapChain::surfacePixelSize() +{ + // may be called before build, must not access other than m_* + + NSView *v = (NSView *) m_window->winId(); + if (v) { + CAMetalLayer *layer = (CAMetalLayer *) [v layer]; + if (layer) { + CGSize size = [layer drawableSize]; + return QSize(size.width, size.height); + } + } + return QSize(); +} + +QRhiRenderPassDescriptor *QMetalSwapChain::newCompatibleRenderPassDescriptor() +{ + chooseFormats(); // ensure colorFormat and similar are filled out + + QRHI_RES_RHI(QRhiMetal); + QMetalRenderPassDescriptor *rpD = new QMetalRenderPassDescriptor(m_rhi); + rpD->colorAttachmentCount = 1; + rpD->hasDepthStencil = m_depthStencil != nullptr; + + rpD->colorFormat[0] = d->colorFormat; + + // m_depthStencil may not be built yet so cannot rely on computed fields in it + rpD->dsFormat = rhiD->d->dev.depth24Stencil8PixelFormatSupported + ? MTLPixelFormatDepth24Unorm_Stencil8 : MTLPixelFormatDepth32Float_Stencil8; + + return rpD; +} + +void QMetalSwapChain::chooseFormats() +{ + QRHI_RES_RHI(QRhiMetal); + samples = rhiD->effectiveSampleCount(m_sampleCount); + // pick a format that is allowed for CAMetalLayer.pixelFormat + d->colorFormat = m_flags.testFlag(sRGB) ? MTLPixelFormatBGRA8Unorm_sRGB : MTLPixelFormatBGRA8Unorm; + d->rhiColorFormat = QRhiTexture::BGRA8; +} + +bool QMetalSwapChain::buildOrResize() +{ + Q_ASSERT(m_window); + + const bool needsRegistration = !window || window != m_window; + + if (window && window != m_window) + release(); + // else no release(), this is intentional + + QRHI_RES_RHI(QRhiMetal); + if (needsRegistration) + rhiD->swapchains.insert(this); + + window = m_window; + + if (window->surfaceType() != QSurface::MetalSurface) { + qWarning("QMetalSwapChain only supports MetalSurface windows"); + return false; + } + + NSView *v = (NSView *) window->winId(); + d->layer = (CAMetalLayer *) [v layer]; + Q_ASSERT(d->layer); + + chooseFormats(); + if (d->colorFormat != d->layer.pixelFormat) + d->layer.pixelFormat = d->colorFormat; + + if (m_flags.testFlag(UsedAsTransferSource)) + d->layer.framebufferOnly = NO; + +#ifdef Q_OS_MACOS + if (m_flags.testFlag(NoVSync)) { + if (@available(macOS 10.13, *)) + d->layer.displaySyncEnabled = NO; + } +#endif + + m_currentPixelSize = surfacePixelSize(); + pixelSize = m_currentPixelSize; + + [d->layer setDevice: rhiD->d->dev]; + + d->curDrawable = nil; + + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + if (!d->sem[i]) + d->sem[i] = dispatch_semaphore_create(QMTL_FRAMES_IN_FLIGHT - 1); + } + + currentFrameSlot = 0; + frameCount = 0; + + ds = m_depthStencil ? QRHI_RES(QMetalRenderBuffer, m_depthStencil) : nullptr; + if (m_depthStencil && m_depthStencil->sampleCount() != m_sampleCount) { + qWarning("Depth-stencil buffer's sampleCount (%d) does not match color buffers' sample count (%d). Expect problems.", + m_depthStencil->sampleCount(), m_sampleCount); + } + if (m_depthStencil && m_depthStencil->pixelSize() != pixelSize) { + qWarning("Depth-stencil buffer's size (%dx%d) does not match the layer size (%dx%d). Expect problems.", + m_depthStencil->pixelSize().width(), m_depthStencil->pixelSize().height(), + pixelSize.width(), pixelSize.height()); + } + + rtWrapper.d->pixelSize = pixelSize; + rtWrapper.d->dpr = window->devicePixelRatio(); + rtWrapper.d->sampleCount = samples; + rtWrapper.d->colorAttCount = 1; + rtWrapper.d->dsAttCount = ds ? 1 : 0; + + qDebug("got CAMetalLayer, size %dx%d", pixelSize.width(), pixelSize.height()); + + if (samples > 1) { + MTLTextureDescriptor *desc = [[MTLTextureDescriptor alloc] init]; + desc.textureType = MTLTextureType2DMultisample; + desc.pixelFormat = d->colorFormat; + desc.width = pixelSize.width(); + desc.height = pixelSize.height(); + desc.sampleCount = samples; + desc.resourceOptions = MTLResourceStorageModePrivate; + desc.storageMode = MTLStorageModePrivate; + desc.usage = MTLTextureUsageRenderTarget; + for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { + [d->msaaTex[i] release]; + d->msaaTex[i] = [rhiD->d->dev newTextureWithDescriptor: desc]; + } + [desc release]; + } + + QRHI_PROF; + QRHI_PROF_F(resizeSwapChain(this, QMTL_FRAMES_IN_FLIGHT, samples > 1 ? QMTL_FRAMES_IN_FLIGHT : 0, samples)); + + if (needsRegistration) + rhiD->registerResource(this); + + return true; +} + +QT_END_NAMESPACE |