From c681c7c23f79e2f0d6dbb2ce8961edd216cefd91 Mon Sep 17 00:00:00 2001 From: Laszlo Agocs Date: Thu, 18 Aug 2022 12:43:23 +0200 Subject: rhi: metal: Add support for tessellation Change-Id: Ie8d226a6a959aa5e78284ea72505fd26aec1e671 Reviewed-by: Andy Nichols --- src/gui/rhi/qrhi.cpp | 106 +- src/gui/rhi/qrhi_p_p.h | 3 + src/gui/rhi/qrhimetal.mm | 1382 +++++++++++++++++--- src/gui/rhi/qrhimetal_p_p.h | 48 +- src/gui/rhi/qshader.cpp | 118 +- src/gui/rhi/qshader_p.h | 16 +- src/gui/rhi/qshader_p_p.h | 16 +- src/gui/rhi/qshaderdescription.cpp | 364 +++++- src/gui/rhi/qshaderdescription_p.h | 76 ++ src/gui/rhi/qshaderdescription_p_p.h | 17 +- tests/auto/gui/rhi/qrhi/data/buildshaders.bat | 4 + tests/auto/gui/rhi/qrhi/data/simpletess.frag | 10 + tests/auto/gui/rhi/qrhi/data/simpletess.frag.qsb | Bin 0 -> 591 bytes tests/auto/gui/rhi/qrhi/data/simpletess.tesc | 22 + tests/auto/gui/rhi/qrhi/data/simpletess.tesc.qsb | Bin 0 -> 1402 bytes tests/auto/gui/rhi/qrhi/data/simpletess.tese | 17 + tests/auto/gui/rhi/qrhi/data/simpletess.tese.qsb | Bin 0 -> 1401 bytes tests/auto/gui/rhi/qrhi/data/simpletess.vert | 12 + tests/auto/gui/rhi/qrhi/data/simpletess.vert.qsb | Bin 0 -> 936 bytes tests/auto/gui/rhi/qrhi/tst_qrhi.cpp | 163 +++ tests/auto/gui/rhi/qshader/data/color.vert | 18 - .../data/metal_enabled_tessellation_v7.frag.qsb | Bin 0 -> 729 bytes .../data/metal_enabled_tessellation_v7.tesc.qsb | Bin 0 -> 1749 bytes .../data/metal_enabled_tessellation_v7.tese.qsb | Bin 0 -> 2390 bytes .../data/metal_enabled_tessellation_v7.vert.qsb | Bin 0 -> 1106 bytes tests/auto/gui/rhi/qshader/data/texture.frag | 16 - tests/auto/gui/rhi/qshader/data/texture_sep.frag | 17 - tests/auto/gui/rhi/qshader/data_src/color.vert | 18 + tests/auto/gui/rhi/qshader/data_src/texture.frag | 16 + .../auto/gui/rhi/qshader/data_src/texture_sep.frag | 17 + tests/auto/gui/rhi/qshader/tst_qshader.cpp | 83 ++ tests/manual/rhi/tessellation/buildshaders.bat | 8 +- tests/manual/rhi/tessellation/tessellation.cpp | 26 +- tests/manual/rhi/tessellation/test.frag.qsb | Bin 581 -> 729 bytes tests/manual/rhi/tessellation/test.tesc | 6 + tests/manual/rhi/tessellation/test.tesc.qsb | Bin 1202 -> 1765 bytes tests/manual/rhi/tessellation/test.tese | 13 +- tests/manual/rhi/tessellation/test.tese.qsb | Bin 1522 -> 2436 bytes tests/manual/rhi/tessellation/test.vert.qsb | Bin 714 -> 1106 bytes tests/manual/rhi/tessellation/test_domain.hlsl | 2 +- 40 files changed, 2312 insertions(+), 302 deletions(-) create mode 100644 tests/auto/gui/rhi/qrhi/data/simpletess.frag create mode 100644 tests/auto/gui/rhi/qrhi/data/simpletess.frag.qsb create mode 100644 tests/auto/gui/rhi/qrhi/data/simpletess.tesc create mode 100644 tests/auto/gui/rhi/qrhi/data/simpletess.tesc.qsb create mode 100644 tests/auto/gui/rhi/qrhi/data/simpletess.tese create mode 100644 tests/auto/gui/rhi/qrhi/data/simpletess.tese.qsb create mode 100644 tests/auto/gui/rhi/qrhi/data/simpletess.vert create mode 100644 tests/auto/gui/rhi/qrhi/data/simpletess.vert.qsb delete mode 100644 tests/auto/gui/rhi/qshader/data/color.vert create mode 100644 tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.frag.qsb create mode 100644 tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.tesc.qsb create mode 100644 tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.tese.qsb create mode 100644 tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.vert.qsb delete mode 100644 tests/auto/gui/rhi/qshader/data/texture.frag delete mode 100644 tests/auto/gui/rhi/qshader/data/texture_sep.frag create mode 100644 tests/auto/gui/rhi/qshader/data_src/color.vert create mode 100644 tests/auto/gui/rhi/qshader/data_src/texture.frag create mode 100644 tests/auto/gui/rhi/qshader/data_src/texture_sep.frag diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp index e822568c4f..0ebcd7ffeb 100644 --- a/src/gui/rhi/qrhi.cpp +++ b/src/gui/rhi/qrhi.cpp @@ -677,16 +677,17 @@ Q_LOGGING_CATEGORY(QRHI_LOG_INFO, "qt.rhi.general") can be set via \l{QRhiGraphicsPipeline::setPatchControlPointCount()}{setPatchControlPointCount()}, and shaders for tessellation control and evaluation can be specified in the - QRhiShaderStage list. Tessellation is considered an experimental feature in - QRhi and can only be expected to be supported with Vulkan, OpenGL (ES), and - Direct 3D, assuming the implementation reports it as supported at run time. - Tessellation shaders have portability issues between APIs (for example, - translating GLSL/SPIR-V to HLSL is problematic due to the way hull shaders - are structured, whereas Metal uses a somewhat different tessellation - pipeline than others), and therefore no guarantees can be given for a - universal solution for now. (for Direct 3D in particular, handwritten HLSL - hull and domain shaders must be injected into each QShader since qsb cannot - generate these from SPIR-V) + QRhiShaderStage list. Tessellation shaders have portability issues between + APIs (for example, translating GLSL/SPIR-V to HLSL is problematic due to + the way hull shaders are structured, whereas Metal uses a somewhat + different tessellation pipeline than others), and therefore unexpected + issues may still arise, even though basic functionality is implemented + across all the underlying APIs. For Direct 3D in particular, handwritten + HLSL hull and domain shaders must be injected into each QShader for the + tessellation control and evaluation stages, respectively, since qsb cannot + generate these from SPIR-V. Note that isoline tessellation should be + avoided as it will not be supported by all backends. The maximum patch + control point count portable between backends is 32. \value GeometryShader Indicates that the geometry shader stage is supported. When supported, a geometry shader can be specified in the @@ -695,9 +696,9 @@ Q_LOGGING_CATEGORY(QRHI_LOG_INFO, "qt.rhi.general") Direct 3D, OpenGL (3.2+) and OpenGL ES (3.2+), assuming the implementation reports it as supported at run time. Geometry shaders have portability issues between APIs, and therefore no guarantees can be given for a - universal solution for now. (for Direct 3D in particular, a handwritten - HLSL geometry shader must be injected into each QShader since qsb cannot - generate this from SPIR-V) + universal solution. They will never be supported with Metal. Whereas with + Direct 3D a handwritten HLSL geometry shader must be injected into each + QShader for the geometry stage since qsb cannot generate this from SPIR-V. \value TextureArrayRange Indicates that for \l{QRhi::newTextureArray()}{texture arrays} it is possible to specify a @@ -1403,6 +1404,85 @@ QDebug operator<<(QDebug dbg, const QRhiVertexInputAttribute &a) } #endif +QRhiVertexInputAttribute::Format QRhiImplementation::shaderDescVariableFormatToVertexInputFormat(QShaderDescription::VariableType type) const +{ + switch (type) { + case QShaderDescription::Vec4: + return QRhiVertexInputAttribute::Float4; + case QShaderDescription::Vec3: + return QRhiVertexInputAttribute::Float3; + case QShaderDescription::Vec2: + return QRhiVertexInputAttribute::Float2; + case QShaderDescription::Float: + return QRhiVertexInputAttribute::Float; + + case QShaderDescription::Int4: + return QRhiVertexInputAttribute::SInt4; + case QShaderDescription::Int3: + return QRhiVertexInputAttribute::SInt3; + case QShaderDescription::Int2: + return QRhiVertexInputAttribute::SInt2; + case QShaderDescription::Int: + return QRhiVertexInputAttribute::SInt; + + case QShaderDescription::Uint4: + return QRhiVertexInputAttribute::UInt4; + case QShaderDescription::Uint3: + return QRhiVertexInputAttribute::UInt3; + case QShaderDescription::Uint2: + return QRhiVertexInputAttribute::UInt2; + case QShaderDescription::Uint: + return QRhiVertexInputAttribute::UInt; + + default: + Q_UNREACHABLE(); + return QRhiVertexInputAttribute::Float; + } +} + +quint32 QRhiImplementation::byteSizePerVertexForVertexInputFormat(QRhiVertexInputAttribute::Format format) const +{ + switch (format) { + case QRhiVertexInputAttribute::Float4: + return 4 * sizeof(float); + case QRhiVertexInputAttribute::Float3: + return 4 * sizeof(float); // vec3 still takes 16 bytes + case QRhiVertexInputAttribute::Float2: + return 2 * sizeof(float); + case QRhiVertexInputAttribute::Float: + return sizeof(float); + + case QRhiVertexInputAttribute::UNormByte4: + return 4 * sizeof(quint8); + case QRhiVertexInputAttribute::UNormByte2: + return 2 * sizeof(quint8); + case QRhiVertexInputAttribute::UNormByte: + return sizeof(quint8); + + case QRhiVertexInputAttribute::UInt4: + return 4 * sizeof(quint32); + case QRhiVertexInputAttribute::UInt3: + return 4 * sizeof(quint32); // ivec3 still takes 16 bytes + case QRhiVertexInputAttribute::UInt2: + return 2 * sizeof(quint32); + case QRhiVertexInputAttribute::UInt: + return sizeof(quint32); + + case QRhiVertexInputAttribute::SInt4: + return 4 * sizeof(qint32); + case QRhiVertexInputAttribute::SInt3: + return 4 * sizeof(qint32); // uvec3 still takes 16 bytes + case QRhiVertexInputAttribute::SInt2: + return 2 * sizeof(qint32); + case QRhiVertexInputAttribute::SInt: + return sizeof(qint32); + + default: + Q_UNREACHABLE(); + return 1; + } +} + /*! \class QRhiVertexInputLayout \internal diff --git a/src/gui/rhi/qrhi_p_p.h b/src/gui/rhi/qrhi_p_p.h index d7490f07c0..bf596abef1 100644 --- a/src/gui/rhi/qrhi_p_p.h +++ b/src/gui/rhi/qrhi_p_p.h @@ -217,6 +217,9 @@ public: return accumulatedPipelineCreationTime; } + QRhiVertexInputAttribute::Format shaderDescVariableFormatToVertexInputFormat(QShaderDescription::VariableType type) const; + quint32 byteSizePerVertexForVertexInputFormat(QRhiVertexInputAttribute::Format format) const; + QRhi *q; static const int MAX_SHADER_CACHE_ENTRIES = 128; diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm index a969b4d332..7eb15a2079 100644 --- a/src/gui/rhi/qrhimetal.mm +++ b/src/gui/rhi/qrhimetal.mm @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qrhimetal_p_p.h" +#include #include #include #include @@ -104,8 +105,11 @@ struct QMetalShader { id lib = nil; id func = nil; - std::array localSize; + std::array localSize = {}; + uint outputVertexCount = 0; + QShaderDescription desc; QShader::NativeResourceBindingMap nativeResourceBindingMap; + QShader::NativeShaderInfo nativeShaderInfo; void destroy() { nativeResourceBindingMap.clear(); @@ -164,6 +168,8 @@ struct QRhiMetalData struct { id pipelineState; id depthStencilState; + std::array, 3> tessVertexComputeState; + id tessTessControlComputeState; } graphicsPipeline; struct { id pipelineState; @@ -268,6 +274,7 @@ struct QMetalCommandBufferData id cb; id currentRenderPassEncoder; id currentComputePassEncoder; + id tessellationComputeEncoder; MTLRenderPassDescriptor *currentPassRpDesc; int currentFirstVertexBinding; QRhiBatchedBindings > currentVertexInputsBuffers; @@ -308,6 +315,7 @@ struct QMetalRenderTargetData struct QMetalGraphicsPipelineData { + QMetalGraphicsPipeline *q = nullptr; id ps = nil; id ds = nil; MTLPrimitiveType primitiveType; @@ -318,6 +326,48 @@ struct QMetalGraphicsPipelineData float slopeScaledDepthBias; QMetalShader vs; QMetalShader fs; + struct Tessellation { + QMetalGraphicsPipelineData *q = nullptr; + bool enabled = false; + bool failed = false; + uint inControlPointCount; + uint outControlPointCount; + QMetalShader compVs[3]; + std::array, 3> vertexComputeState = {}; + id tessControlComputeState = nil; + QMetalShader compTesc; + QMetalShader vertTese; + quint32 vsCompOutputBufferSize(quint32 vertexOrIndexCount, quint32 instanceCount) const + { + // max vertex output components = resourceLimit(MaxVertexOutputs) * 4 = 60 + return vertexOrIndexCount * instanceCount * sizeof(float) * 60; + } + quint32 tescCompOutputBufferSize(quint32 patchCount) const + { + return outControlPointCount * patchCount * sizeof(float) * 60; + } + quint32 tescCompPatchOutputBufferSize(quint32 patchCount) const + { + // assume maxTessellationControlPerPatchOutputComponents is 128 + return patchCount * sizeof(float) * 128; + } + quint32 patchCountForDrawCall(quint32 vertexOrIndexCount, quint32 instanceCount) const + { + return ((vertexOrIndexCount + inControlPointCount - 1) / inControlPointCount) * instanceCount; + } + static int vsCompVariantToIndex(QShader::Variant vertexCompVariant); + id vsCompPipeline(QRhiMetal *rhiD, QShader::Variant vertexCompVariant); + id tescCompPipeline(QRhiMetal *rhiD); + id teseFragRenderPipeline(QRhiMetal *rhiD, QMetalGraphicsPipeline *pipeline); + enum class WorkBufType { + DeviceLocal, + HostVisible + }; + QMetalBuffer *acquireWorkBuffer(QRhiMetal *rhiD, quint32 size, WorkBufType type = WorkBufType::DeviceLocal); + QVector deviceLocalWorkBuffers; + QVector hostVisibleWorkBuffers; + } tess; + template void setupVertexOrStageInputDescriptor(T *desc); }; struct QMetalComputePipelineData @@ -661,7 +711,7 @@ bool QRhiMetal::isFeatureSupported(QRhi::Feature feature) const case QRhi::TextureArrays: return true; case QRhi::Tessellation: - return false; + return true; case QRhi::GeometryShader: return false; case QRhi::TextureArrayRange: @@ -824,6 +874,119 @@ static inline int mapBinding(int binding, return -1; } +static inline void bindStageBuffers(QMetalCommandBuffer *cbD, + int stage, + const QRhiBatchedBindings>::Batch &bufferBatch, + const QRhiBatchedBindings::Batch &offsetBatch) +{ + switch (stage) { + case QMetalShaderResourceBindingsData::VERTEX: + [cbD->d->currentRenderPassEncoder setVertexBuffers: bufferBatch.resources.constData() + offsets: offsetBatch.resources.constData() + withRange: NSMakeRange(bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))]; + break; + case QMetalShaderResourceBindingsData::FRAGMENT: + [cbD->d->currentRenderPassEncoder setFragmentBuffers: bufferBatch.resources.constData() + offsets: offsetBatch.resources.constData() + withRange: NSMakeRange(bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))]; + break; + case QMetalShaderResourceBindingsData::COMPUTE: + [cbD->d->currentComputePassEncoder setBuffers: bufferBatch.resources.constData() + offsets: offsetBatch.resources.constData() + withRange: NSMakeRange(bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))]; + break; + default: + Q_UNREACHABLE(); + break; + } +} + +static inline void bindStageTextures(QMetalCommandBuffer *cbD, + int stage, + const QRhiBatchedBindings>::Batch &textureBatch) +{ + switch (stage) { + case QMetalShaderResourceBindingsData::VERTEX: + [cbD->d->currentRenderPassEncoder setVertexTextures: textureBatch.resources.constData() + withRange: NSMakeRange(textureBatch.startBinding, NSUInteger(textureBatch.resources.count()))]; + break; + case QMetalShaderResourceBindingsData::FRAGMENT: + [cbD->d->currentRenderPassEncoder setFragmentTextures: textureBatch.resources.constData() + withRange: NSMakeRange(textureBatch.startBinding, NSUInteger(textureBatch.resources.count()))]; + break; + case QMetalShaderResourceBindingsData::COMPUTE: + [cbD->d->currentComputePassEncoder setTextures: textureBatch.resources.constData() + withRange: NSMakeRange(textureBatch.startBinding, NSUInteger(textureBatch.resources.count()))]; + break; + default: + Q_UNREACHABLE(); + break; + } +} + +static inline void bindStageSamplers(QMetalCommandBuffer *cbD, + int encoderStage, + const QRhiBatchedBindings>::Batch &samplerBatch) +{ + switch (encoderStage) { + case QMetalShaderResourceBindingsData::VERTEX: + [cbD->d->currentRenderPassEncoder setVertexSamplerStates: samplerBatch.resources.constData() + withRange: NSMakeRange(samplerBatch.startBinding, NSUInteger(samplerBatch.resources.count()))]; + break; + case QMetalShaderResourceBindingsData::FRAGMENT: + [cbD->d->currentRenderPassEncoder setFragmentSamplerStates: samplerBatch.resources.constData() + withRange: NSMakeRange(samplerBatch.startBinding, NSUInteger(samplerBatch.resources.count()))]; + break; + case QMetalShaderResourceBindingsData::COMPUTE: + [cbD->d->currentComputePassEncoder setSamplerStates: samplerBatch.resources.constData() + withRange: NSMakeRange(samplerBatch.startBinding, NSUInteger(samplerBatch.resources.count()))]; + break; + default: + Q_UNREACHABLE(); + break; + } +} + +// Helper that is not used during the common vertex+fragment and compute +// pipelines, but is necessary when tessellation is involved and so the +// graphics pipeline is under the hood a combination of multiple compute and +// render pipelines. We need to be able to set the buffers, textures, samplers +// when a switching between render and compute encoders. +static inline void rebindShaderResources(QMetalCommandBuffer *cbD, int resourceStage, int encoderStage, + const QMetalShaderResourceBindingsData *customBindingState = nullptr) +{ + const QMetalShaderResourceBindingsData *bindingData = customBindingState ? customBindingState : &cbD->d->currentShaderResourceBindingState; + + for (int i = 0, ie = bindingData->res[resourceStage].bufferBatches.batches.count(); i != ie; ++i) { + const auto &bufferBatch(bindingData->res[resourceStage].bufferBatches.batches[i]); + const auto &offsetBatch(bindingData->res[resourceStage].bufferOffsetBatches.batches[i]); + bindStageBuffers(cbD, encoderStage, bufferBatch, offsetBatch); + } + + for (int i = 0, ie = bindingData->res[resourceStage].textureBatches.batches.count(); i != ie; ++i) { + const auto &batch(bindingData->res[resourceStage].textureBatches.batches[i]); + bindStageTextures(cbD, encoderStage, batch); + } + + for (int i = 0, ie = bindingData->res[resourceStage].samplerBatches.batches.count(); i != ie; ++i) { + const auto &batch(bindingData->res[resourceStage].samplerBatches.batches[i]); + bindStageSamplers(cbD, encoderStage, batch); + } +} + +// Resources marked for the tess.control and/or eval. stages are treated as if +// they were for the vertex stage. For tess.eval. this is trivial because +// that's translated to a Metal a vertex function, but tess.control (and the +// GLSL vertex) shader becomes compute. Yet dumping them under the vertex +// category still works, because rebindShaderResources(VERTEX, COMPUTE) can +// then be used to set them active on the compute encoder. +static inline bool isVertexishResource(QRhiShaderResourceBinding::StageFlags stages) +{ + return stages.testAnyFlags(QRhiShaderResourceBinding::VertexStage + | QRhiShaderResourceBinding::TessellationControlStage + | QRhiShaderResourceBinding::TessellationEvaluationStage); +} + void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD, QMetalCommandBuffer *cbD, int dynamicOffsetCount, @@ -848,7 +1011,7 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD break; } } - if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { + if (isVertexishResource(b->stage)) { const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Buffer); if (nativeBinding >= 0) bindingData.res[QMetalShaderResourceBindingsData::VERTEX].buffers.append({ nativeBinding, mtlbuf, offset }); @@ -873,7 +1036,7 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD for (int elem = 0; elem < data->count; ++elem) { QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.texSamplers[elem].tex); QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.texSamplers[elem].sampler); - if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { + if (isVertexishResource(b->stage)) { // Must handle all three cases (combined, separate, separate): // first = texture binding, second = sampler binding // first = texture binding @@ -913,7 +1076,7 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD { QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.simage.tex); id t = texD->d->viewForLevel(b->u.simage.level); - if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { + if (isVertexishResource(b->stage)) { const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Texture); if (nativeBinding >= 0) bindingData.res[QMetalShaderResourceBindingsData::VERTEX].textures.append({ nativeBinding, t }); @@ -937,7 +1100,7 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.sbuf.buf); id mtlbuf = bufD->d->buf[0]; quint32 offset = b->u.sbuf.offset; - if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { + if (isVertexishResource(b->stage)) { const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Buffer); if (nativeBinding >= 0) bindingData.res[QMetalShaderResourceBindingsData::VERTEX].buffers.append({ nativeBinding, mtlbuf, offset }); @@ -994,26 +1157,7 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD { continue; } - switch (stage) { - case QMetalShaderResourceBindingsData::VERTEX: - [cbD->d->currentRenderPassEncoder setVertexBuffers: bufferBatch.resources.constData() - offsets: offsetBatch.resources.constData() - withRange: NSMakeRange(bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))]; - break; - case QMetalShaderResourceBindingsData::FRAGMENT: - [cbD->d->currentRenderPassEncoder setFragmentBuffers: bufferBatch.resources.constData() - offsets: offsetBatch.resources.constData() - withRange: NSMakeRange(bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))]; - break; - case QMetalShaderResourceBindingsData::COMPUTE: - [cbD->d->currentComputePassEncoder setBuffers: bufferBatch.resources.constData() - offsets: offsetBatch.resources.constData() - withRange: NSMakeRange(bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))]; - break; - default: - Q_UNREACHABLE(); - break; - } + bindStageBuffers(cbD, stage, bufferBatch, offsetBatch); } if (offsetOnlyChange) @@ -1044,23 +1188,7 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD { continue; } - switch (stage) { - case QMetalShaderResourceBindingsData::VERTEX: - [cbD->d->currentRenderPassEncoder setVertexTextures: batch.resources.constData() - withRange: NSMakeRange(batch.startBinding, NSUInteger(batch.resources.count()))]; - break; - case QMetalShaderResourceBindingsData::FRAGMENT: - [cbD->d->currentRenderPassEncoder setFragmentTextures: batch.resources.constData() - withRange: NSMakeRange(batch.startBinding, NSUInteger(batch.resources.count()))]; - break; - case QMetalShaderResourceBindingsData::COMPUTE: - [cbD->d->currentComputePassEncoder setTextures: batch.resources.constData() - withRange: NSMakeRange(batch.startBinding, NSUInteger(batch.resources.count()))]; - break; - default: - Q_UNREACHABLE(); - break; - } + bindStageTextures(cbD, stage, batch); } for (int i = 0, ie = bindingData.res[stage].samplerBatches.batches.count(); i != ie; ++i) { @@ -1071,65 +1199,68 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD { continue; } - switch (stage) { - case QMetalShaderResourceBindingsData::VERTEX: - [cbD->d->currentRenderPassEncoder setVertexSamplerStates: batch.resources.constData() - withRange: NSMakeRange(batch.startBinding, NSUInteger(batch.resources.count()))]; - break; - case QMetalShaderResourceBindingsData::FRAGMENT: - [cbD->d->currentRenderPassEncoder setFragmentSamplerStates: batch.resources.constData() - withRange: NSMakeRange(batch.startBinding, NSUInteger(batch.resources.count()))]; - break; - case QMetalShaderResourceBindingsData::COMPUTE: - [cbD->d->currentComputePassEncoder setSamplerStates: batch.resources.constData() - withRange: NSMakeRange(batch.startBinding, NSUInteger(batch.resources.count()))]; - break; - default: - Q_UNREACHABLE(); - break; - } + bindStageSamplers(cbD, stage, batch); } } + cbD->d->currentShaderResourceBindingState = bindingData; } +void QMetalGraphicsPipeline::makeActiveForCurrentRenderPassEncoder(QMetalCommandBuffer *cbD) +{ + [cbD->d->currentRenderPassEncoder setRenderPipelineState: d->ps]; + + if (cbD->d->currentDepthStencilState != d->ds) { + [cbD->d->currentRenderPassEncoder setDepthStencilState: d->ds]; + cbD->d->currentDepthStencilState = d->ds; + } + + if (cbD->currentCullMode == -1 || d->cullMode != uint(cbD->currentCullMode)) { + [cbD->d->currentRenderPassEncoder setCullMode: d->cullMode]; + cbD->currentCullMode = int(d->cullMode); + } + if (cbD->currentTriangleFillMode == -1 || d->triangleFillMode != uint(cbD->currentTriangleFillMode)) { + [cbD->d->currentRenderPassEncoder setTriangleFillMode: d->triangleFillMode]; + cbD->currentTriangleFillMode = int(d->triangleFillMode); + } + if (cbD->currentFrontFaceWinding == -1 || d->winding != uint(cbD->currentFrontFaceWinding)) { + [cbD->d->currentRenderPassEncoder setFrontFacingWinding: d->winding]; + cbD->currentFrontFaceWinding = int(d->winding); + } + if (!qFuzzyCompare(d->depthBias, cbD->currentDepthBiasValues.first) + || !qFuzzyCompare(d->slopeScaledDepthBias, cbD->currentDepthBiasValues.second)) + { + [cbD->d->currentRenderPassEncoder setDepthBias: d->depthBias + slopeScale: d->slopeScaledDepthBias + clamp: 0.0f]; + cbD->currentDepthBiasValues = { d->depthBias, d->slopeScaledDepthBias }; + } +} + 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; + if (cbD->currentGraphicsPipeline == psD && cbD->currentPipelineGeneration == psD->generation) + return; - [cbD->d->currentRenderPassEncoder setRenderPipelineState: psD->d->ps]; + cbD->currentGraphicsPipeline = psD; + cbD->currentComputePipeline = nullptr; + cbD->currentPipelineGeneration = psD->generation; - if (cbD->d->currentDepthStencilState != psD->d->ds) { - [cbD->d->currentRenderPassEncoder setDepthStencilState: psD->d->ds]; - cbD->d->currentDepthStencilState = psD->d->ds; - } - - if (cbD->currentCullMode == -1 || psD->d->cullMode != uint(cbD->currentCullMode)) { - [cbD->d->currentRenderPassEncoder setCullMode: psD->d->cullMode]; - cbD->currentCullMode = int(psD->d->cullMode); - } - if (cbD->currentTriangleFillMode == -1 || psD->d->triangleFillMode != uint(cbD->currentTriangleFillMode)) { - [cbD->d->currentRenderPassEncoder setTriangleFillMode: psD->d->triangleFillMode]; - cbD->currentTriangleFillMode = int(psD->d->triangleFillMode); - } - if (cbD->currentFrontFaceWinding == -1 || psD->d->winding != uint(cbD->currentFrontFaceWinding)) { - [cbD->d->currentRenderPassEncoder setFrontFacingWinding: psD->d->winding]; - cbD->currentFrontFaceWinding = int(psD->d->winding); + if (!psD->d->tess.enabled && !psD->d->tess.failed) { + psD->makeActiveForCurrentRenderPassEncoder(cbD); + } else { + // mark work buffers that can now be safely reused as reusable + for (QMetalBuffer *workBuf : psD->d->tess.deviceLocalWorkBuffers) { + if (workBuf && workBuf->lastActiveFrameSlot == currentFrameSlot) + workBuf->lastActiveFrameSlot = -1; } - if (!qFuzzyCompare(psD->d->depthBias, cbD->currentDepthBiasValues.first) - || !qFuzzyCompare(psD->d->slopeScaledDepthBias, cbD->currentDepthBiasValues.second)) - { - [cbD->d->currentRenderPassEncoder setDepthBias: psD->d->depthBias - slopeScale: psD->d->slopeScaledDepthBias - clamp: 0.0f]; - cbD->currentDepthBiasValues = { psD->d->depthBias, psD->d->slopeScaledDepthBias }; + for (QMetalBuffer *workBuf : psD->d->tess.hostVisibleWorkBuffers) { + if (workBuf && workBuf->lastActiveFrameSlot == currentFrameSlot) + workBuf->lastActiveFrameSlot = -1; } } @@ -1142,8 +1273,8 @@ void QRhiMetal::setShaderResources(QRhiCommandBuffer *cb, QRhiShaderResourceBind { 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); + QMetalGraphicsPipeline *gfxPsD = cbD->currentGraphicsPipeline; + QMetalComputePipeline *compPsD = cbD->currentComputePipeline; if (!srb) { if (gfxPsD) @@ -1253,20 +1384,20 @@ void QRhiMetal::setShaderResources(QRhiCommandBuffer *cb, QRhiShaderResourceBind if (hasSlottedResourceInSrb && cbD->currentResSlot != resSlot) resNeedsRebind = true; - const bool srbChanged = gfxPsD ? (cbD->currentGraphicsSrb != srb) : (cbD->currentComputeSrb != srb); + const bool srbChanged = gfxPsD ? (cbD->currentGraphicsSrb != srbD) : (cbD->currentComputeSrb != srbD); const bool srbRebuilt = cbD->currentSrbGeneration != srbD->generation; // dynamic uniform buffer offsets always trigger a rebind if (hasDynamicOffsetInSrb || resNeedsRebind || srbChanged || srbRebuilt) { const QShader::NativeResourceBindingMap *resBindMaps[SUPPORTED_STAGES] = { nullptr, nullptr, nullptr }; if (gfxPsD) { - cbD->currentGraphicsSrb = srb; + cbD->currentGraphicsSrb = srbD; cbD->currentComputeSrb = nullptr; resBindMaps[0] = &gfxPsD->d->vs.nativeResourceBindingMap; resBindMaps[1] = &gfxPsD->d->fs.nativeResourceBindingMap; } else { cbD->currentGraphicsSrb = nullptr; - cbD->currentComputeSrb = srb; + cbD->currentComputeSrb = srbD; resBindMaps[2] = &compPsD->d->cs.nativeResourceBindingMap; } cbD->currentSrbGeneration = srbD->generation; @@ -1298,13 +1429,13 @@ void QRhiMetal::setVertexInput(QRhiCommandBuffer *cb, offsets.finish(); // same binding space for vertex and constant buffers - work it around - QRhiShaderResourceBindings *srb = cbD->currentGraphicsSrb; + QMetalShaderResourceBindings *srbD = 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 (!srbD) + srbD = QRHI_RES(QMetalShaderResourceBindings, cbD->currentGraphicsPipeline->shaderResourceBindings()); + const int firstVertexBinding = srbD->maxBinding + 1; if (firstVertexBinding != cbD->d->currentFirstVertexBinding || buffers != cbD->d->currentVertexInputsBuffers @@ -1328,7 +1459,7 @@ void QRhiMetal::setVertexInput(QRhiCommandBuffer *cb, QMetalBuffer *ibufD = QRHI_RES(QMetalBuffer, indexBuf); executeBufferHostWritesForCurrentFrame(ibufD); ibufD->lastActiveFrameSlot = currentFrameSlot; - cbD->currentIndexBuffer = indexBuf; + cbD->currentIndexBuffer = ibufD; cbD->currentIndexOffset = indexOffset; cbD->currentIndexFormat = indexFormat; } else { @@ -1357,7 +1488,7 @@ void QRhiMetal::setViewport(QRhiCommandBuffer *cb, const QRhiViewport &viewport) [cbD->d->currentRenderPassEncoder setViewport: vp]; - if (!QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->m_flags.testFlag(QRhiGraphicsPipeline::UsesScissor)) { + if (!cbD->currentGraphicsPipeline->m_flags.testFlag(QRhiGraphicsPipeline::UsesScissor)) { MTLScissorRect s; s.x = NSUInteger(x); s.y = NSUInteger(y); @@ -1371,7 +1502,7 @@ 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)); + Q_ASSERT(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 @@ -1405,20 +1536,240 @@ void QRhiMetal::setStencilRef(QRhiCommandBuffer *cb, quint32 refValue) [cbD->d->currentRenderPassEncoder setStencilReferenceValue: refValue]; } +static id tessellationComputeEncoder(QMetalCommandBuffer *cbD) +{ + if (cbD->d->currentRenderPassEncoder) { + [cbD->d->currentRenderPassEncoder endEncoding]; + cbD->d->currentRenderPassEncoder = nil; + } + + if (!cbD->d->tessellationComputeEncoder) + cbD->d->tessellationComputeEncoder = [cbD->d->cb computeCommandEncoder]; + + return cbD->d->tessellationComputeEncoder; +} + +static void endTessellationComputeEncoding(QMetalCommandBuffer *cbD) +{ + if (cbD->d->tessellationComputeEncoder) { + [cbD->d->tessellationComputeEncoder endEncoding]; + cbD->d->tessellationComputeEncoder = nil; + } + + cbD->d->currentRenderPassEncoder = [cbD->d->cb renderCommandEncoderWithDescriptor: cbD->d->currentPassRpDesc]; + cbD->resetPerPassCachedState(); +} + +void QRhiMetal::tessellatedDraw(const TessDrawArgs &args) +{ + QMetalCommandBuffer *cbD = args.cbD; + QMetalGraphicsPipeline *graphicsPipeline = cbD->currentGraphicsPipeline; + if (graphicsPipeline->d->tess.failed) + return; + + const bool indexed = args.type != TessDrawArgs::NonIndexed; + const quint32 instanceCount = indexed ? args.drawIndexed.instanceCount : args.draw.instanceCount; + const quint32 vertexOrIndexCount = indexed ? args.drawIndexed.indexCount : args.draw.vertexCount; + + QMetalGraphicsPipelineData::Tessellation &tess(graphicsPipeline->d->tess); + const quint32 patchCount = tess.patchCountForDrawCall(vertexOrIndexCount, instanceCount); + QMetalBuffer *vertOutBuf = nullptr; + QMetalBuffer *tescOutBuf = nullptr; + QMetalBuffer *tescPatchOutBuf = nullptr; + QMetalBuffer *tescFactorBuf = nullptr; + QMetalBuffer *tescParamsBuf = nullptr; + id vertTescComputeEncoder = tessellationComputeEncoder(cbD); + + // Step 1: vertex shader (as compute) + { + id computeEncoder = vertTescComputeEncoder; + QShader::Variant shaderVariant = QShader::NonIndexedVertexAsComputeShader; + if (args.type == TessDrawArgs::U16Indexed) + shaderVariant = QShader::UInt16IndexedVertexAsComputeShader; + else if (args.type == TessDrawArgs::U32Indexed) + shaderVariant = QShader::UInt32IndexedVertexAsComputeShader; + const int varIndex = QMetalGraphicsPipelineData::Tessellation::vsCompVariantToIndex(shaderVariant); + id computePipelineState = tess.vsCompPipeline(this, shaderVariant); + [computeEncoder setComputePipelineState: computePipelineState]; + + // Make uniform buffers, textures, and samplers (meant for the + // vertex stage from the client's point of view) visible in the + // compute shaders (both vertex and tess.control). + cbD->d->currentComputePassEncoder = computeEncoder; + rebindShaderResources(cbD, QMetalShaderResourceBindingsData::VERTEX, QMetalShaderResourceBindingsData::COMPUTE); + cbD->d->currentComputePassEncoder = nil; + + const QMap &ebb(tess.compVs[varIndex].nativeShaderInfo.extraBufferBindings); + const int outputBufferBinding = ebb.value(QShaderPrivate::MslTessVertTescOutputBufferBinding, -1); + const int indexBufferBinding = ebb.value(QShaderPrivate::MslTessVertIndicesBufferBinding, -1); + + if (outputBufferBinding >= 0) { + const quint32 workBufSize = tess.vsCompOutputBufferSize(vertexOrIndexCount, instanceCount); + vertOutBuf = tess.acquireWorkBuffer(this, workBufSize); + if (!vertOutBuf) + return; + [computeEncoder setBuffer: vertOutBuf->d->buf[0] offset: 0 atIndex: outputBufferBinding]; + } + + if (indexBufferBinding >= 0) + [computeEncoder setBuffer: (id) args.drawIndexed.indexBuffer offset: 0 atIndex: indexBufferBinding]; + + for (int i = 0, ie = cbD->d->currentVertexInputsBuffers.batches.count(); i != ie; ++i) { + const auto &bufferBatch(cbD->d->currentVertexInputsBuffers.batches[i]); + const auto &offsetBatch(cbD->d->currentVertexInputOffsets.batches[i]); + [computeEncoder setBuffers: bufferBatch.resources.constData() + offsets: offsetBatch.resources.constData() + withRange: NSMakeRange(uint(cbD->d->currentFirstVertexBinding) + bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))]; + } + + if (indexed) { + [computeEncoder setStageInRegion: MTLRegionMake2D(args.drawIndexed.vertexOffset, args.drawIndexed.firstInstance, + args.drawIndexed.indexCount, args.drawIndexed.instanceCount)]; + } else { + [computeEncoder setStageInRegion: MTLRegionMake2D(args.draw.firstVertex, args.draw.firstInstance, + args.draw.vertexCount, args.draw.instanceCount)]; + } + + [computeEncoder dispatchThreads: MTLSizeMake(vertexOrIndexCount, instanceCount, 1) + threadsPerThreadgroup: MTLSizeMake(computePipelineState.threadExecutionWidth, 1, 1)]; + } + + // Step 2: tessellation control shader (as compute) + { + id computeEncoder = vertTescComputeEncoder; + id computePipelineState = tess.tescCompPipeline(this); + [computeEncoder setComputePipelineState: computePipelineState]; + + // Shader resources are set already in step 1. (because srb stage + // flags for tesc and tese visibility are treated as if they were + // specified as vertex visibility -> QMSRBD::VERTEX includes those too) + + const QMap &ebb(tess.compTesc.nativeShaderInfo.extraBufferBindings); + const int outputBufferBinding = ebb.value(QShaderPrivate::MslTessVertTescOutputBufferBinding, -1); + const int patchOutputBufferBinding = ebb.value(QShaderPrivate::MslTessTescPatchOutputBufferBinding, -1); + const int tessFactorBufferBinding = ebb.value(QShaderPrivate::MslTessTescTessLevelBufferBinding, -1); + const int paramsBufferBinding = ebb.value(QShaderPrivate::MslTessTescParamsBufferBinding, -1); + const int inputBufferBinding = ebb.value(QShaderPrivate::MslTessTescInputBufferBinding, -1); + + if (outputBufferBinding >= 0) { + const quint32 workBufSize = tess.tescCompOutputBufferSize(patchCount); + tescOutBuf = tess.acquireWorkBuffer(this, workBufSize); + if (!tescOutBuf) + return; + [computeEncoder setBuffer: tescOutBuf->d->buf[0] offset: 0 atIndex: outputBufferBinding]; + } + + if (patchOutputBufferBinding >= 0) { + const quint32 workBufSize = tess.tescCompPatchOutputBufferSize(patchCount); + tescPatchOutBuf = tess.acquireWorkBuffer(this, workBufSize); + if (!tescPatchOutBuf) + return; + [computeEncoder setBuffer: tescPatchOutBuf->d->buf[0] offset: 0 atIndex: patchOutputBufferBinding]; + } + + if (tessFactorBufferBinding >= 0) { + tescFactorBuf = tess.acquireWorkBuffer(this, patchCount * sizeof(MTLQuadTessellationFactorsHalf)); + [computeEncoder setBuffer: tescFactorBuf->d->buf[0] offset: 0 atIndex: tessFactorBufferBinding]; + } + + if (paramsBufferBinding >= 0) { + struct { + quint32 inControlPointCount; + quint32 patchCount; + } params; + tescParamsBuf = tess.acquireWorkBuffer(this, sizeof(params), QMetalGraphicsPipelineData::Tessellation::WorkBufType::HostVisible); + if (!tescParamsBuf) + return; + params.inControlPointCount = tess.inControlPointCount; + params.patchCount = patchCount; + id paramsBuf = tescParamsBuf->d->buf[0]; + char *p = reinterpret_cast([paramsBuf contents]); + memcpy(p, ¶ms, sizeof(params)); + [computeEncoder setBuffer: paramsBuf offset: 0 atIndex: paramsBufferBinding]; + } + + if (vertOutBuf && inputBufferBinding >= 0) + [computeEncoder setBuffer: vertOutBuf->d->buf[0] offset: 0 atIndex: inputBufferBinding]; + + int sgSize = int(computePipelineState.threadExecutionWidth); + int wgSize = std::lcm(tess.outControlPointCount, sgSize); + while (wgSize > caps.maxThreadGroupSize) { + sgSize /= 2; + wgSize = std::lcm(tess.outControlPointCount, sgSize); + } + [computeEncoder dispatchThreads: MTLSizeMake(patchCount * tess.outControlPointCount, 1, 1) + threadsPerThreadgroup: MTLSizeMake(wgSize, 1, 1)]; + } + + // Much of the state in the QMetalCommandBuffer is going to be reset + // when we get a new render encoder. Save what we need. (cheaper than + // starting to walk over the srb again) + const QMetalShaderResourceBindingsData resourceBindings = cbD->d->currentShaderResourceBindingState; + + endTessellationComputeEncoding(cbD); + + // Step 3: tessellation evaluation (as vertex) + fragment shader + { + // No need to call tess.teseFragRenderPipeline because it was done + // once and we know the result is stored in the standard place + // (graphicsPipeline->d->ps). + + graphicsPipeline->makeActiveForCurrentRenderPassEncoder(cbD); + id renderEncoder = cbD->d->currentRenderPassEncoder; + + rebindShaderResources(cbD, QMetalShaderResourceBindingsData::VERTEX, QMetalShaderResourceBindingsData::VERTEX, &resourceBindings); + rebindShaderResources(cbD, QMetalShaderResourceBindingsData::FRAGMENT, QMetalShaderResourceBindingsData::FRAGMENT, &resourceBindings); + + const QMap &ebb(tess.compTesc.nativeShaderInfo.extraBufferBindings); + const int outputBufferBinding = ebb.value(QShaderPrivate::MslTessVertTescOutputBufferBinding, -1); + const int patchOutputBufferBinding = ebb.value(QShaderPrivate::MslTessTescPatchOutputBufferBinding, -1); + const int tessFactorBufferBinding = ebb.value(QShaderPrivate::MslTessTescTessLevelBufferBinding, -1); + + if (outputBufferBinding >= 0 && tescOutBuf) + [renderEncoder setVertexBuffer: tescOutBuf->d->buf[0] offset: 0 atIndex: outputBufferBinding]; + + if (patchOutputBufferBinding >= 0 && tescPatchOutBuf) + [renderEncoder setVertexBuffer: tescPatchOutBuf->d->buf[0] offset: 0 atIndex: patchOutputBufferBinding]; + + if (tessFactorBufferBinding >= 0 && tescFactorBuf) { + [renderEncoder setTessellationFactorBuffer: tescFactorBuf->d->buf[0] offset: 0 instanceStride: 0]; + [renderEncoder setVertexBuffer: tescFactorBuf->d->buf[0] offset: 0 atIndex: tessFactorBufferBinding]; + } + + [cbD->d->currentRenderPassEncoder drawPatches: tess.outControlPointCount + patchStart: 0 + patchCount: patchCount + patchIndexBuffer: nil + patchIndexBufferOffset: 0 + instanceCount: 1 + baseInstance: 0]; + } +} + 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); + if (cbD->currentGraphicsPipeline->d->tess.enabled) { + TessDrawArgs a; + a.cbD = cbD; + a.type = TessDrawArgs::NonIndexed; + a.draw.vertexCount = vertexCount; + a.draw.instanceCount = instanceCount; + a.draw.firstVertex = firstVertex; + a.draw.firstInstance = firstInstance; + tessellatedDraw(a); + return; + } + if (caps.baseVertexAndInstance) { - [cbD->d->currentRenderPassEncoder drawPrimitives: - QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->d->primitiveType - vertexStart: firstVertex vertexCount: vertexCount instanceCount: instanceCount baseInstance: firstInstance]; + [cbD->d->currentRenderPassEncoder drawPrimitives: cbD->currentGraphicsPipeline->d->primitiveType + vertexStart: firstVertex vertexCount: vertexCount instanceCount: instanceCount baseInstance: firstInstance]; } else { - [cbD->d->currentRenderPassEncoder drawPrimitives: - QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->d->primitiveType - vertexStart: firstVertex vertexCount: vertexCount instanceCount: instanceCount]; + [cbD->d->currentRenderPassEncoder drawPrimitives: cbD->currentGraphicsPipeline->d->primitiveType + vertexStart: firstVertex vertexCount: vertexCount instanceCount: instanceCount]; } } @@ -1434,23 +1785,37 @@ void QRhiMetal::drawIndexed(QRhiCommandBuffer *cb, quint32 indexCount, const quint32 indexOffset = cbD->currentIndexOffset + firstIndex * (cbD->currentIndexFormat == QRhiCommandBuffer::IndexUInt16 ? 2 : 4); Q_ASSERT(indexOffset == aligned(indexOffset, 4u)); - QMetalBuffer *ibufD = QRHI_RES(QMetalBuffer, cbD->currentIndexBuffer); - id mtlbuf = ibufD->d->buf[ibufD->d->slotted ? currentFrameSlot : 0]; + QMetalBuffer *ibufD = cbD->currentIndexBuffer; + id mtlibuf = ibufD->d->buf[ibufD->d->slotted ? currentFrameSlot : 0]; + + if (cbD->currentGraphicsPipeline->d->tess.enabled) { + TessDrawArgs a; + a.cbD = cbD; + a.type = cbD->currentIndexFormat == QRhiCommandBuffer::IndexUInt16 ? TessDrawArgs::U16Indexed : TessDrawArgs::U32Indexed; + a.drawIndexed.indexCount = indexCount; + a.drawIndexed.instanceCount = instanceCount; + a.drawIndexed.firstIndex = firstIndex; + a.drawIndexed.vertexOffset = vertexOffset; + a.drawIndexed.firstInstance = firstInstance; + a.drawIndexed.indexBuffer = mtlibuf; + tessellatedDraw(a); + return; + } if (caps.baseVertexAndInstance) { - [cbD->d->currentRenderPassEncoder drawIndexedPrimitives: QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->d->primitiveType + [cbD->d->currentRenderPassEncoder drawIndexedPrimitives: cbD->currentGraphicsPipeline->d->primitiveType indexCount: indexCount indexType: cbD->currentIndexFormat == QRhiCommandBuffer::IndexUInt16 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32 - indexBuffer: mtlbuf + indexBuffer: mtlibuf indexBufferOffset: indexOffset instanceCount: instanceCount baseVertex: vertexOffset baseInstance: firstInstance]; } else { - [cbD->d->currentRenderPassEncoder drawIndexedPrimitives: QRHI_RES(QMetalGraphicsPipeline, cbD->currentGraphicsPipeline)->d->primitiveType + [cbD->d->currentRenderPassEncoder drawIndexedPrimitives: cbD->currentGraphicsPipeline->d->primitiveType indexCount: indexCount indexType: cbD->currentIndexFormat == QRhiCommandBuffer::IndexUInt16 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32 - indexBuffer: mtlbuf + indexBuffer: mtlibuf indexBufferOffset: indexOffset instanceCount: instanceCount]; } @@ -2221,9 +2586,9 @@ void QRhiMetal::setComputePipeline(QRhiCommandBuffer *cb, QRhiComputePipeline *p Q_ASSERT(cbD->recordingPass == QMetalCommandBuffer::ComputePass); QMetalComputePipeline *psD = QRHI_RES(QMetalComputePipeline, ps); - if (cbD->currentComputePipeline != ps || cbD->currentPipelineGeneration != psD->generation) { + if (cbD->currentComputePipeline != psD || cbD->currentPipelineGeneration != psD->generation) { cbD->currentGraphicsPipeline = nullptr; - cbD->currentComputePipeline = ps; + cbD->currentComputePipeline = psD; cbD->currentPipelineGeneration = psD->generation; [cbD->d->currentComputePassEncoder setComputePipelineState: psD->d->ps]; @@ -2289,8 +2654,12 @@ void QRhiMetal::executeDeferredReleases(bool forced) [e.stagingBuffer.buffer release]; break; case QRhiMetalData::DeferredReleaseEntry::GraphicsPipeline: - [e.graphicsPipeline.depthStencilState release]; [e.graphicsPipeline.pipelineState release]; + [e.graphicsPipeline.depthStencilState release]; + [e.graphicsPipeline.tessVertexComputeState[0] release]; + [e.graphicsPipeline.tessVertexComputeState[1] release]; + [e.graphicsPipeline.tessVertexComputeState[2] release]; + [e.graphicsPipeline.tessTessControlComputeState release]; break; case QRhiMetalData::DeferredReleaseEntry::ComputePipeline: [e.computePipeline.pipelineState release]; @@ -2393,6 +2762,9 @@ bool QMetalBuffer::create() // Static maps to on macOS) is not safe when another frame reading from the // same buffer is still in flight. d->slotted = !m_usage.testFlag(QRhiBuffer::StorageBuffer); // except for SSBOs written in the shader + // and a special case for internal work buffers + if (int(m_usage) == WorkBufPoolUsage) + d->slotted = false; for (int i = 0; i < QMTL_FRAMES_IN_FLIGHT; ++i) { if (i == 0 || d->slotted) { @@ -3453,6 +3825,8 @@ QMetalGraphicsPipeline::QMetalGraphicsPipeline(QRhiImplementation *rhi) : QRhiGraphicsPipeline(rhi), d(new QMetalGraphicsPipelineData) { + d->q = this; + d->tess.q = d; } QMetalGraphicsPipeline::~QMetalGraphicsPipeline() @@ -3466,16 +3840,36 @@ void QMetalGraphicsPipeline::destroy() d->vs.destroy(); d->fs.destroy(); - if (!d->ps) + d->tess.compVs[0].destroy(); + d->tess.compVs[1].destroy(); + d->tess.compVs[2].destroy(); + + d->tess.compTesc.destroy(); + d->tess.vertTese.destroy(); + + qDeleteAll(d->tess.deviceLocalWorkBuffers); + d->tess.deviceLocalWorkBuffers.clear(); + qDeleteAll(d->tess.hostVisibleWorkBuffers); + d->tess.hostVisibleWorkBuffers.clear(); + + if (!d->ps && !d->ds + && !d->tess.vertexComputeState[0] && !d->tess.vertexComputeState[1] && !d->tess.vertexComputeState[2] + && !d->tess.tessControlComputeState) + { return; + } QRhiMetalData::DeferredReleaseEntry e; e.type = QRhiMetalData::DeferredReleaseEntry::GraphicsPipeline; e.lastActiveFrameSlot = lastActiveFrameSlot; - e.graphicsPipeline.depthStencilState = d->ds; e.graphicsPipeline.pipelineState = d->ps; - d->ds = nil; + e.graphicsPipeline.depthStencilState = d->ds; + e.graphicsPipeline.tessVertexComputeState = d->tess.vertexComputeState; + e.graphicsPipeline.tessTessControlComputeState = d->tess.tessControlComputeState; d->ps = nil; + d->ds = nil; + d->tess.vertexComputeState = {}; + d->tess.tessControlComputeState = nil; QRHI_RES_RHI(QRhiMetal); if (rhiD) { @@ -3700,6 +4094,34 @@ static inline MTLTriangleFillMode toMetalTriangleFillMode(QRhiGraphicsPipeline:: } } +static inline MTLWinding toMetalTessellationWindingOrder(QShaderDescription::TessellationWindingOrder w) +{ + switch (w) { + case QShaderDescription::CwTessellationWindingOrder: + return MTLWindingClockwise; + case QShaderDescription::CcwTessellationWindingOrder: + return MTLWindingCounterClockwise; + default: + // this is reachable, consider a tess.eval. shader not declaring it, the value is then Unknown + return MTLWindingCounterClockwise; + } +} + +static inline MTLTessellationPartitionMode toMetalTessellationPartitionMode(QShaderDescription::TessellationPartitioning p) +{ + switch (p) { + case QShaderDescription::EqualTessellationPartitioning: + return MTLTessellationPartitionModePow2; + case QShaderDescription::FractionalEvenTessellationPartitioning: + return MTLTessellationPartitionModeFractionalEven; + case QShaderDescription::FractionalOddTessellationPartitioning: + return MTLTessellationPartitionModeFractionalOdd; + default: + // this is reachable, consider a tess.eval. shader not declaring it, the value is then Unknown + return MTLTessellationPartitionModePow2; + } +} + id QRhiMetalData::createMetalLib(const QShader &shader, QShader::Variant shaderVariant, QString *error, QByteArray *entryPoint, QShaderKey *activeKey) { @@ -3768,48 +4190,135 @@ id QRhiMetalData::createMSLShaderFunction(id lib, const return f; } -bool QMetalGraphicsPipeline::create() +void QMetalGraphicsPipeline::setupAttachmentsInMetalRenderPassDescriptor(void *metalRpDesc, QMetalRenderPassDescriptor *rpD) { - if (d->ps) - destroy(); + MTLRenderPipelineDescriptor *rpDesc = reinterpret_cast(metalRpDesc); + + 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 (uint i = 0, ie = uint(m_targetBlends.count()); i != ie; ++i) { + const QRhiGraphicsPipeline::TargetBlend &b(m_targetBlends[int(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 defined(Q_OS_MACOS) + if (fmt != MTLPixelFormatDepth16Unorm && fmt != MTLPixelFormatDepth32Float) +#else + if (fmt != MTLPixelFormatDepth32Float) +#endif + rpDesc.stencilAttachmentPixelFormat = fmt; + } QRHI_RES_RHI(QRhiMetal); - rhiD->pipelineCreationStart(); - if (!rhiD->sanityCheckGraphicsPipeline(this)) - return false; + rpDesc.sampleCount = NSUInteger(rhiD->effectiveSampleCount(m_sampleCount)); +} + +void QMetalGraphicsPipeline::setupMetalDepthStencilDescriptor(void *metalDsDesc) +{ + MTLDepthStencilDescriptor *dsDesc = reinterpret_cast(metalDsDesc); + + 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; + } +} + +void QMetalGraphicsPipeline::mapStates() +{ + d->winding = m_frontFace == CCW ? MTLWindingCounterClockwise : MTLWindingClockwise; + d->cullMode = toMetalCullMode(m_cullMode); + d->triangleFillMode = toMetalTriangleFillMode(m_polygonMode); + d->depthBias = float(m_depthBias); + d->slopeScaledDepthBias = m_slopeScaledDepthBias; +} + +template +void QMetalGraphicsPipelineData::setupVertexOrStageInputDescriptor(T *desc) +{ // same binding space for vertex and constant buffers - work it around - const int firstVertexBinding = QRHI_RES(QMetalShaderResourceBindings, m_shaderResourceBindings)->maxBinding + 1; + const int firstVertexBinding = QRHI_RES(QMetalShaderResourceBindings, q->shaderResourceBindings())->maxBinding + 1; - MTLVertexDescriptor *inputLayout = [MTLVertexDescriptor vertexDescriptor]; - for (auto it = m_vertexInputLayout.cbeginAttributes(), itEnd = m_vertexInputLayout.cendAttributes(); + QRhiVertexInputLayout vertexInputLayout = q->vertexInputLayout(); + for (auto it = vertexInputLayout.cbeginAttributes(), itEnd = vertexInputLayout.cendAttributes(); it != itEnd; ++it) { const uint loc = uint(it->location()); - inputLayout.attributes[loc].format = toMetalAttributeFormat(it->format()); - inputLayout.attributes[loc].offset = NSUInteger(it->offset()); - inputLayout.attributes[loc].bufferIndex = NSUInteger(firstVertexBinding + it->binding()); + // either MTLVertexFormat or MTLAttributeFormat, the values are the same + desc.attributes[loc].format = decltype(desc.attributes[loc].format)(toMetalAttributeFormat(it->format())); + desc.attributes[loc].offset = NSUInteger(it->offset()); + desc.attributes[loc].bufferIndex = NSUInteger(firstVertexBinding + it->binding()); } int bindingIndex = 0; - for (auto it = m_vertexInputLayout.cbeginBindings(), itEnd = m_vertexInputLayout.cendBindings(); + for (auto it = vertexInputLayout.cbeginBindings(), itEnd = vertexInputLayout.cendBindings(); it != itEnd; ++it, ++bindingIndex) { const uint layoutIdx = uint(firstVertexBinding + bindingIndex); - inputLayout.layouts[layoutIdx].stepFunction = - it->classification() == QRhiVertexInputBinding::PerInstance - ? MTLVertexStepFunctionPerInstance : MTLVertexStepFunctionPerVertex; - inputLayout.layouts[layoutIdx].stepRate = NSUInteger(it->instanceStepRate()); - inputLayout.layouts[layoutIdx].stride = it->stride(); + using StepT = decltype(desc.layouts[layoutIdx].stepFunction); + if (std::is_same_v) { + desc.layouts[layoutIdx].stepFunction = StepT( + it->classification() == QRhiVertexInputBinding::PerInstance + ? MTLStepFunctionThreadPositionInGridY : MTLStepFunctionThreadPositionInGridX); + } else { + desc.layouts[layoutIdx].stepFunction = StepT( + it->classification() == QRhiVertexInputBinding::PerInstance + ? MTLVertexStepFunctionPerInstance : MTLVertexStepFunctionPerVertex); + } + desc.layouts[layoutIdx].stepRate = NSUInteger(it->instanceStepRate()); + desc.layouts[layoutIdx].stride = it->stride(); } +} - MTLRenderPipelineDescriptor *rpDesc = [[MTLRenderPipelineDescriptor alloc] init]; +bool QMetalGraphicsPipeline::createVertexFragmentPipeline() +{ + QRHI_RES_RHI(QRhiMetal); - rpDesc.vertexDescriptor = inputLayout; + MTLVertexDescriptor *vertexDesc = [MTLVertexDescriptor vertexDescriptor]; + d->setupVertexOrStageInputDescriptor(vertexDesc); + + MTLRenderPipelineDescriptor *rpDesc = [[MTLRenderPipelineDescriptor alloc] init]; + rpDesc.vertexDescriptor = vertexDesc; - // mutability cannot be determined (slotted buffers could be set as + // 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 + // buffers not just the resource binding layout), so leave + // rpDesc.vertex/fragmentBuffers at the defaults. for (const QRhiShaderStage &shaderStage : qAsConst(m_shaderStages)) { auto cacheIt = rhiD->d->shaderCache.constFind(shaderStage); @@ -3881,85 +4390,557 @@ bool QMetalGraphicsPipeline::create() } QMetalRenderPassDescriptor *rpD = QRHI_RES(QMetalRenderPassDescriptor, m_renderPassDesc); + setupAttachmentsInMetalRenderPassDescriptor(rpDesc, rpD); + NSError *err = nil; + d->ps = [rhiD->d->dev newRenderPipelineStateWithDescriptor: rpDesc error: &err]; + [rpDesc release]; + if (!d->ps) { + const QString msg = QString::fromNSString(err.localizedDescription); + qWarning("Failed to create render pipeline state: %s", qPrintable(msg)); + return false; + } - 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; + MTLDepthStencilDescriptor *dsDesc = [[MTLDepthStencilDescriptor alloc] init]; + setupMetalDepthStencilDescriptor(dsDesc); + d->ds = [rhiD->d->dev newDepthStencilStateWithDescriptor: dsDesc]; + [dsDesc release]; - Q_ASSERT(m_targetBlends.count() == rpD->colorAttachmentCount - || (m_targetBlends.isEmpty() && rpD->colorAttachmentCount == 1)); + d->primitiveType = toMetalPrimitiveType(m_topology); + mapStates(); - for (uint i = 0, ie = uint(m_targetBlends.count()); i != ie; ++i) { - const QRhiGraphicsPipeline::TargetBlend &b(m_targetBlends[int(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); + return true; +} + +int QMetalGraphicsPipelineData::Tessellation::vsCompVariantToIndex(QShader::Variant vertexCompVariant) +{ + switch (vertexCompVariant) { + case QShader::NonIndexedVertexAsComputeShader: + return 0; + case QShader::UInt32IndexedVertexAsComputeShader: + return 1; + case QShader::UInt16IndexedVertexAsComputeShader: + return 2; + default: + break; + } + return -1; +} + +id QMetalGraphicsPipelineData::Tessellation::vsCompPipeline(QRhiMetal *rhiD, QShader::Variant vertexCompVariant) +{ + const int varIndex = vsCompVariantToIndex(vertexCompVariant); + if (varIndex >= 0 && vertexComputeState[varIndex]) + return vertexComputeState[varIndex]; + + id func = nil; + if (varIndex >= 0) + func = compVs[varIndex].func; + + if (!func) { + qWarning("No compute function found for vertex shader translated for tessellation, this should not happen"); + return nil; + } + + const QMap &ebb(compVs[varIndex].nativeShaderInfo.extraBufferBindings); + const int indexBufferBinding = ebb.value(QShaderPrivate::MslTessVertIndicesBufferBinding, -1); + + MTLComputePipelineDescriptor *cpDesc = [MTLComputePipelineDescriptor new]; + cpDesc.computeFunction = func; + cpDesc.threadGroupSizeIsMultipleOfThreadExecutionWidth = YES; + cpDesc.stageInputDescriptor = [MTLStageInputOutputDescriptor stageInputOutputDescriptor]; + if (indexBufferBinding >= 0) { + if (vertexCompVariant == QShader::UInt32IndexedVertexAsComputeShader) { + cpDesc.stageInputDescriptor.indexType = MTLIndexTypeUInt32; + cpDesc.stageInputDescriptor.indexBufferIndex = indexBufferBinding; + } else if (vertexCompVariant == QShader::UInt16IndexedVertexAsComputeShader) { + cpDesc.stageInputDescriptor.indexType = MTLIndexTypeUInt16; + cpDesc.stageInputDescriptor.indexBufferIndex = indexBufferBinding; } } + q->setupVertexOrStageInputDescriptor(cpDesc.stageInputDescriptor); - 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 defined(Q_OS_MACOS) - if (fmt != MTLPixelFormatDepth16Unorm && fmt != MTLPixelFormatDepth32Float) -#else - if (fmt != MTLPixelFormatDepth32Float) -#endif - rpDesc.stencilAttachmentPixelFormat = fmt; + NSError *err = nil; + id ps = [rhiD->d->dev newComputePipelineStateWithDescriptor: cpDesc + options: MTLPipelineOptionNone + reflection: nil + error: &err]; + if (!ps) { + const QString msg = QString::fromNSString(err.localizedDescription); + qWarning("Failed to create compute pipeline state: %s", qPrintable(msg)); + } else { + vertexComputeState[varIndex] = ps; } + // not retained, the only owner is vertexComputeState and so the QRhiGraphicsPipeline + return ps; +} - rpDesc.sampleCount = NSUInteger(rhiD->effectiveSampleCount(m_sampleCount)); +id QMetalGraphicsPipelineData::Tessellation::tescCompPipeline(QRhiMetal *rhiD) +{ + if (tessControlComputeState) + return tessControlComputeState; + + MTLComputePipelineDescriptor *cpDesc = [MTLComputePipelineDescriptor new]; + cpDesc.computeFunction = compTesc.func; NSError *err = nil; - d->ps = [rhiD->d->dev newRenderPipelineStateWithDescriptor: rpDesc error: &err]; - if (!d->ps) { + id ps = [rhiD->d->dev newComputePipelineStateWithDescriptor: cpDesc + options: MTLPipelineOptionNone + reflection: nil + error: &err]; + if (!ps) { const QString msg = QString::fromNSString(err.localizedDescription); - qWarning("Failed to create render pipeline state: %s", qPrintable(msg)); - [rpDesc release]; - return false; + qWarning("Failed to create compute pipeline state: %s", qPrintable(msg)); + } else { + tessControlComputeState = ps; + } + // not retained, the only owner is tessControlComputeState and so the QRhiGraphicsPipeline + return ps; +} + +static inline bool hasBuiltin(const QVector &builtinList, QShaderDescription::BuiltinType builtin) +{ + return std::find_if(builtinList.cbegin(), builtinList.cend(), + [builtin](const QShaderDescription::BuiltinVariable &b) { return b.type == builtin; }) != builtinList.cend(); +} + +id QMetalGraphicsPipelineData::Tessellation::teseFragRenderPipeline(QRhiMetal *rhiD, QMetalGraphicsPipeline *pipeline) +{ + if (pipeline->d->ps) + return pipeline->d->ps; + + MTLRenderPipelineDescriptor *rpDesc = [[MTLRenderPipelineDescriptor alloc] init]; + MTLVertexDescriptor *vertexDesc = [MTLVertexDescriptor vertexDescriptor]; + + // Going to use the same buffer indices for the extra buffers as the tess.control compute shader did. + const QMap &ebb(compTesc.nativeShaderInfo.extraBufferBindings); + const int tescOutputBufferBinding = ebb.value(QShaderPrivate::MslTessVertTescOutputBufferBinding, -1); + const int tescPatchOutputBufferBinding = ebb.value(QShaderPrivate::MslTessTescPatchOutputBufferBinding, -1); + const int tessFactorBufferBinding = ebb.value(QShaderPrivate::MslTessTescTessLevelBufferBinding, -1); + + QVarLengthArray teseInputLocations; + for (const QShaderDescription::InOutVariable &v : vertTese.desc.inputVariables()) + teseInputLocations.append(v.location); + + quint32 offsetInTescOutput = 0; + quint32 offsetInTescPatchOutput = 0; + int lastLocation = -1; + + for (const QShaderDescription::InOutVariable &tescOutVar : compTesc.desc.outputVariables()) { + const int location = tescOutVar.location; + lastLocation = location; + const QRhiVertexInputAttribute::Format format = rhiD->shaderDescVariableFormatToVertexInputFormat(tescOutVar.type); + if (teseInputLocations.contains(location)) { + if (tescOutVar.perPatch) { + if (tescPatchOutputBufferBinding >= 0) { + vertexDesc.attributes[location].bufferIndex = tescPatchOutputBufferBinding; + vertexDesc.attributes[location].format = toMetalAttributeFormat(format); + vertexDesc.attributes[location].offset = offsetInTescPatchOutput; + } + } else { + if (tescOutputBufferBinding >= 0) { + vertexDesc.attributes[location].bufferIndex = tescOutputBufferBinding; + vertexDesc.attributes[location].format = toMetalAttributeFormat(format); + vertexDesc.attributes[location].offset = offsetInTescOutput; + } + } + } + if (tescOutVar.perPatch) + offsetInTescPatchOutput += rhiD->byteSizePerVertexForVertexInputFormat(format); + else + offsetInTescOutput += rhiD->byteSizePerVertexForVertexInputFormat(format); + } + + const QVector tescOutBuiltins = compTesc.desc.outputBuiltinVariables(); + const QVector teseInBuiltins = vertTese.desc.inputBuiltinVariables(); + + // Take a tess.control shader with an output variable layout(location = 0) out vec3 outColor[]. + // Assume it also writes to glPosition, e.g. gl_out[gl_InvocationID].gl_Position = ... + // The tess.eval. shader translated to a Metal vertex function will then contain: + // + // struct main0_in { + // float3 inColor [[attribute(0)]]; + // float4 gl_Position [[attribute(1)]]; } + // + // The vertex description has to be set up accordingly. The color is + // simple because that will be in the input/output variable list with + // location 0. The position is a builtin however. So for now just + // assume that builtins such as that come after the other variables, + // with increasing location values. + + if (hasBuiltin(tescOutBuiltins, QShaderDescription::PositionBuiltin) + && hasBuiltin(teseInBuiltins, QShaderDescription::PositionBuiltin) + && tescOutputBufferBinding >= 0) + { + const int location = ++lastLocation; + vertexDesc.attributes[location].bufferIndex = tescOutputBufferBinding; + vertexDesc.attributes[location].format = toMetalAttributeFormat(QRhiVertexInputAttribute::Float4); + vertexDesc.attributes[location].offset = offsetInTescOutput; + offsetInTescOutput += 4 * sizeof(float); + } + + // Per-patch outputs from the tess.control stage. are mostly handled above. + // Consider: + // layout(location = 1) patch in vec3 stuff; + // layout(location = 2) patch in float more_stuff; + // + // This maps to: + // + // struct main0_patchIn { + // float3 stuff [[attribute(1)]]; + // float more_stuff [[attribute(2)]]; + // patch_control_point gl_in; }; + // + // These are already in place (location 1 and 2, referencing the per-patch + // output buffer of tesc) at this point. But now if the tess.eval.shader + // reads gl_TessLevelInner and gl_TessLevelOuter, which are also per-patch, + // that adds, if the mode is triangles: + // (assuming gl_Position got location 3, sorted based on the builtin type + // (Position < Outer < Inner)) + // + // float4 gl_TessLevel [[attribute(4)]]; + // + // or if the mode is quads: + // + // float4 gl_TessLevelOuter [[attribute(4)]]; + // float2 gl_TessLevelInner [[attribute(5)]]; + // + // Like gl_Position, these built-ins needs to be handled specially. + // Note that the data is in a dedicated buffer, not in the patch buffer. + + const bool hasTessLevelOuter = hasBuiltin(tescOutBuiltins, QShaderDescription::TessLevelOuterBuiltin) + && hasBuiltin(teseInBuiltins, QShaderDescription::TessLevelOuterBuiltin); + const bool hasTessLevelInner = hasBuiltin(tescOutBuiltins, QShaderDescription::TessLevelInnerBuiltin) + && hasBuiltin(teseInBuiltins, QShaderDescription::TessLevelInnerBuiltin); + if (vertTese.desc.tessellationMode() != QShaderDescription::TrianglesTessellationMode + && vertTese.desc.tessellationMode() != QShaderDescription::QuadTessellationMode) + { + qWarning("Tessellation evaluation stage mode is neither 'triangles' nor 'quads', this should not happen"); + } + const bool trianglesMode = vertTese.desc.tessellationMode() == QShaderDescription::TrianglesTessellationMode; + if ((hasTessLevelOuter || hasTessLevelInner) && tessFactorBufferBinding >= 0) { + int loc0 = -1; + int loc1 = -1; + if (trianglesMode) { + loc0 = ++lastLocation; // float4 gl_TessLevel + } else { + loc0 = ++lastLocation; // float4 gl_TessLevelOuter + loc1 = ++lastLocation; // float2 gl_TessLevelInner + } + if (loc0 >= 0) { + vertexDesc.attributes[loc0].bufferIndex = tessFactorBufferBinding; + vertexDesc.attributes[loc0].format = MTLVertexFormatHalf4; + vertexDesc.attributes[loc0].offset = 0; + } + if (loc1 >= 0) { + vertexDesc.attributes[loc1].bufferIndex = tessFactorBufferBinding; + vertexDesc.attributes[loc1].format = MTLVertexFormatHalf2; + vertexDesc.attributes[loc1].offset = 8; + } + vertexDesc.layouts[tessFactorBufferBinding].stepFunction = MTLVertexStepFunctionPerPatch; + vertexDesc.layouts[tessFactorBufferBinding].stride = trianglesMode ? 8 : 12; + } + + if (offsetInTescOutput > 0) { + vertexDesc.layouts[tescOutputBufferBinding].stepFunction = MTLVertexStepFunctionPerPatchControlPoint; + vertexDesc.layouts[tescOutputBufferBinding].stride = offsetInTescOutput; + } + + if (offsetInTescPatchOutput > 0) { + vertexDesc.layouts[tescPatchOutputBufferBinding].stepFunction = MTLVertexStepFunctionPerPatch; + vertexDesc.layouts[tescPatchOutputBufferBinding].stride = offsetInTescPatchOutput; } + + rpDesc.vertexDescriptor = vertexDesc; + rpDesc.vertexFunction = vertTese.func; + rpDesc.fragmentFunction = pipeline->d->fs.func; + + // The portable, cross-API approach is to use CCW, the results are then + // identical (assuming the applied clipSpaceCorrMatrix) for all the 3D + // APIs. The tess.eval. GLSL shader is thus expected to specify ccw. If it + // doesn't, things may not work as expected. + rpDesc.tessellationOutputWindingOrder = toMetalTessellationWindingOrder(vertTese.desc.tessellationWindingOrder()); + + rpDesc.tessellationPartitionMode = toMetalTessellationPartitionMode(vertTese.desc.tessellationPartitioning()); + + QMetalRenderPassDescriptor *rpD = QRHI_RES(QMetalRenderPassDescriptor, pipeline->renderPassDescriptor()); + pipeline->setupAttachmentsInMetalRenderPassDescriptor(rpDesc, rpD); + + NSError *err = nil; + id ps = [rhiD->d->dev newRenderPipelineStateWithDescriptor: rpDesc error: &err]; [rpDesc release]; + if (!ps) { + const QString msg = QString::fromNSString(err.localizedDescription); + qWarning("Failed to create render pipeline state for tessellation: %s", qPrintable(msg)); + } else { + // ps is stored in the QMetalGraphicsPipelineData so the end result in this + // regard is no different from what createVertexFragmentPipeline does + pipeline->d->ps = ps; + } + return ps; +} - 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; +QMetalBuffer *QMetalGraphicsPipelineData::Tessellation::acquireWorkBuffer(QRhiMetal *rhiD, quint32 size, WorkBufType type) +{ + QVector *workBuffers = type == WorkBufType::DeviceLocal ? &deviceLocalWorkBuffers : &hostVisibleWorkBuffers; - 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; + // Check if something is reusable as-is. + for (QMetalBuffer *workBuf : *workBuffers) { + if (workBuf && workBuf->lastActiveFrameSlot == -1 && workBuf->size() >= size) { + workBuf->lastActiveFrameSlot = rhiD->currentFrameSlot; + return workBuf; + } } + // Once the pool is above a certain threshold, see if there is something + // unused (but too small) and recreate that our size. + if (workBuffers->count() > QMTL_FRAMES_IN_FLIGHT * 8) { + for (QMetalBuffer *workBuf : *workBuffers) { + if (workBuf && workBuf->lastActiveFrameSlot == -1) { + workBuf->setSize(size); + if (workBuf->create()) { + workBuf->lastActiveFrameSlot = rhiD->currentFrameSlot; + return workBuf; + } + } + } + } + + // Add a new buffer to the pool. + QMetalBuffer *buf; + if (type == WorkBufType::DeviceLocal) { + // for GPU->GPU data (non-slotted, not necessarily host writable) + buf = new QMetalBuffer(rhiD, QRhiBuffer::Static, QRhiBuffer::UsageFlags(QMetalBuffer::WorkBufPoolUsage), size); + } else { + // for CPU->GPU (non-slotted, host writable/coherent) + buf = new QMetalBuffer(rhiD, QRhiBuffer::Dynamic, QRhiBuffer::UsageFlags(QMetalBuffer::WorkBufPoolUsage), size); + } + if (buf->create()) { + buf->lastActiveFrameSlot = rhiD->currentFrameSlot; + workBuffers->append(buf); + return buf; + } + + qWarning("Failed to acquire work buffer of size %u", size); + return nullptr; +} + +bool QMetalGraphicsPipeline::createTessellationPipelines(const QShader &tessVert, const QShader &tesc, const QShader &tese, const QShader &tessFrag) +{ + QRHI_RES_RHI(QRhiMetal); + QString error; + QByteArray entryPoint; + QShaderKey activeKey; + + const QShaderDescription tescDesc = tesc.description(); + const QShaderDescription teseDesc = tese.description(); + d->tess.inControlPointCount = uint(m_patchControlPointCount); + d->tess.outControlPointCount = tescDesc.tessellationOutputVertexCount(); + if (!d->tess.outControlPointCount) + d->tess.outControlPointCount = teseDesc.tessellationOutputVertexCount(); + + if (!d->tess.outControlPointCount) { + qWarning("Failed to determine output vertex count from the tessellation control or evaluation shader, cannot tessellate"); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + + // Now the vertex shader is a compute shader. + // It should have three dedicated *VertexAsComputeShader variants. + // What the requested variant was (Standard or Batchable) plays no role here. + // (the Qt Quick scenegraph does not use tessellation with its materials) + // Create all three versions. + + bool variantsPresent[3] = {}; + const QVector tessVertKeys = tessVert.availableShaders(); + for (const QShaderKey &k : tessVertKeys) { + switch (k.sourceVariant()) { + case QShader::NonIndexedVertexAsComputeShader: + variantsPresent[0] = true; + break; + case QShader::UInt32IndexedVertexAsComputeShader: + variantsPresent[1] = true; + break; + case QShader::UInt16IndexedVertexAsComputeShader: + variantsPresent[2] = true; + break; + default: + break; + } + } + if (!(variantsPresent[0] && variantsPresent[1] && variantsPresent[2])) { + qWarning("Vertex shader is not prepared for Metal tessellation. Cannot tessellate. " + "Perhaps the relevant variants (UInt32IndexedVertexAsComputeShader et al) were not generated? " + "Try passing --msltess to qsb."); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + + int varIndex = 0; // Will map NonIndexed as 0, UInt32 as 1, UInt16 as 2. Do not change this ordering. + for (QShader::Variant variant : { + QShader::NonIndexedVertexAsComputeShader, + QShader::UInt32IndexedVertexAsComputeShader, + QShader::UInt16IndexedVertexAsComputeShader }) + { + id lib = rhiD->d->createMetalLib(tessVert, variant, &error, &entryPoint, &activeKey); + if (!lib) { + qWarning("MSL shader compilation failed for vertex-as-compute shader %d: %s", int(variant), qPrintable(error)); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + id func = rhiD->d->createMSLShaderFunction(lib, entryPoint); + if (!func) { + qWarning("MSL function for entry point %s not found", entryPoint.constData()); + [lib release]; + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + QMetalShader &compVs(d->tess.compVs[varIndex]); + compVs.lib = lib; + compVs.func = func; + compVs.desc = tessVert.description(); + compVs.nativeResourceBindingMap = tessVert.nativeResourceBindingMap(activeKey); + compVs.nativeShaderInfo = tessVert.nativeShaderInfo(activeKey); + + // pre-create all three MTLComputePipelineStates + if (!d->tess.vsCompPipeline(rhiD, variant)) { + qWarning("Failed to pre-generate compute pipeline for vertex compute shader (tessellation variant %d)", int(variant)); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + + ++varIndex; + } + + // Pipeline #2 is a compute that runs the tessellation control (compute) shader + id tessControlLib = rhiD->d->createMetalLib(tesc, QShader::StandardShader, &error, &entryPoint, &activeKey); + if (!tessControlLib) { + qWarning("MSL shader compilation failed for tessellation control compute shader: %s", qPrintable(error)); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + id tessControlFunc = rhiD->d->createMSLShaderFunction(tessControlLib, entryPoint); + if (!tessControlFunc) { + qWarning("MSL function for entry point %s not found", entryPoint.constData()); + [tessControlLib release]; + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + d->tess.compTesc.lib = tessControlLib; + d->tess.compTesc.func = tessControlFunc; + d->tess.compTesc.desc = tesc.description(); + d->tess.compTesc.nativeResourceBindingMap = tesc.nativeResourceBindingMap(activeKey); + d->tess.compTesc.nativeShaderInfo = tesc.nativeShaderInfo(activeKey); + if (!d->tess.tescCompPipeline(rhiD)) { + qWarning("Failed to pre-generate compute pipeline for tessellation control shader"); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + + // Pipeline #3 is a render pipeline with the tessellation evaluation (vertex) + the fragment shader + id tessEvalLib = rhiD->d->createMetalLib(tese, QShader::StandardShader, &error, &entryPoint, &activeKey); + if (!tessEvalLib) { + qWarning("MSL shader compilation failed for tessellation evaluation vertex shader: %s", qPrintable(error)); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + id tessEvalFunc = rhiD->d->createMSLShaderFunction(tessEvalLib, entryPoint); + if (!tessEvalFunc) { + qWarning("MSL function for entry point %s not found", entryPoint.constData()); + [tessEvalLib release]; + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + d->tess.vertTese.lib = tessEvalLib; + d->tess.vertTese.func = tessEvalFunc; + d->tess.vertTese.desc = tese.description(); + d->tess.vertTese.nativeResourceBindingMap = tese.nativeResourceBindingMap(activeKey); + d->tess.vertTese.nativeShaderInfo = tese.nativeShaderInfo(activeKey); + + id fragLib = rhiD->d->createMetalLib(tessFrag, QShader::StandardShader, &error, &entryPoint, &activeKey); + if (!fragLib) { + qWarning("MSL shader compilation failed for fragment shader: %s", qPrintable(error)); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + id fragFunc = rhiD->d->createMSLShaderFunction(fragLib, entryPoint); + if (!fragFunc) { + qWarning("MSL function for entry point %s not found", entryPoint.constData()); + [fragLib release]; + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + d->fs.lib = fragLib; + d->fs.func = fragFunc; + d->fs.nativeResourceBindingMap = tese.nativeResourceBindingMap(activeKey); + + if (!d->tess.teseFragRenderPipeline(rhiD, this)) { + qWarning("Failed to pre-generate render pipeline for tessellation evaluation + fragment shader"); + d->tess.enabled = false; + d->tess.failed = true; + return false; + } + + MTLDepthStencilDescriptor *dsDesc = [[MTLDepthStencilDescriptor alloc] init]; + setupMetalDepthStencilDescriptor(dsDesc); 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); - d->triangleFillMode = toMetalTriangleFillMode(m_polygonMode); - d->depthBias = float(m_depthBias); - d->slopeScaledDepthBias = m_slopeScaledDepthBias; + // no primitiveType + mapStates(); + + return true; +} + +bool QMetalGraphicsPipeline::create() +{ + destroy(); // no early test, always invoke and leave it to destroy to decide what to clean up + + QRHI_RES_RHI(QRhiMetal); + rhiD->pipelineCreationStart(); + if (!rhiD->sanityCheckGraphicsPipeline(this)) + return false; + + // See if tessellation is involved. Things will be very different, if so. + QShader tessVert; + QShader tesc; + QShader tese; + QShader tessFrag; + for (const QRhiShaderStage &shaderStage : qAsConst(m_shaderStages)) { + switch (shaderStage.type()) { + case QRhiShaderStage::Vertex: + tessVert = shaderStage.shader(); + break; + case QRhiShaderStage::TessellationControl: + tesc = shaderStage.shader(); + break; + case QRhiShaderStage::TessellationEvaluation: + tese = shaderStage.shader(); + break; + case QRhiShaderStage::Fragment: + tessFrag = shaderStage.shader(); + break; + default: + break; + } + } + d->tess.enabled = tesc.isValid() && tese.isValid() && m_topology == Patches && m_patchControlPointCount > 0; + d->tess.failed = false; + + bool ok = d->tess.enabled ? createTessellationPipelines(tessVert, tesc, tese, tessFrag) : createVertexFragmentPipeline(); + if (!ok) + return false; rhiD->pipelineCreationEnd(); lastActiveFrameSlot = -1; @@ -4050,7 +5031,7 @@ bool QMetalComputePipeline::create() d->ps = [rhiD->d->dev newComputePipelineStateWithFunction: d->cs.func error: &err]; if (!d->ps) { const QString msg = QString::fromNSString(err.localizedDescription); - qWarning("Failed to create render pipeline state: %s", qPrintable(msg)); + qWarning("Failed to create compute pipeline state: %s", qPrintable(msg)); return false; } @@ -4090,6 +5071,7 @@ void QMetalCommandBuffer::resetState() { d->currentRenderPassEncoder = nil; d->currentComputePassEncoder = nil; + d->tessellationComputeEncoder = nil; d->currentPassRpDesc = nil; resetPerPassState(); } diff --git a/src/gui/rhi/qrhimetal_p_p.h b/src/gui/rhi/qrhimetal_p_p.h index bba6134c0e..fe43e46f41 100644 --- a/src/gui/rhi/qrhimetal_p_p.h +++ b/src/gui/rhi/qrhimetal_p_p.h @@ -41,6 +41,9 @@ struct QMetalBuffer : public QRhiBuffer int lastActiveFrameSlot = -1; friend class QRhiMetal; friend struct QMetalShaderResourceBindings; + + static constexpr int WorkBufPoolUsage = 1 << 8; + static_assert(WorkBufPoolUsage > QRhiBuffer::StorageBuffer); }; struct QMetalRenderBufferData; @@ -204,6 +207,7 @@ struct QMetalShaderResourceBindings : public QRhiShaderResourceBindings }; struct QMetalGraphicsPipelineData; +struct QMetalCommandBuffer; struct QMetalGraphicsPipeline : public QRhiGraphicsPipeline { @@ -212,6 +216,13 @@ struct QMetalGraphicsPipeline : public QRhiGraphicsPipeline void destroy() override; bool create() override; + void makeActiveForCurrentRenderPassEncoder(QMetalCommandBuffer *cbD); + void setupAttachmentsInMetalRenderPassDescriptor(void *metalRpDesc, QMetalRenderPassDescriptor *rpD); + void setupMetalDepthStencilDescriptor(void *metalDsDesc); + void mapStates(); + bool createVertexFragmentPipeline(); + bool createTessellationPipelines(const QShader &tessVert, const QShader &tesc, const QShader &tese, const QShader &tessFrag); + QMetalGraphicsPipelineData *d; uint generation = 0; int lastActiveFrameSlot = -1; @@ -256,14 +267,14 @@ struct QMetalCommandBuffer : public QRhiCommandBuffer QRhiRenderTarget *currentTarget; // per-pass (render or compute command encoder) volatile (cached) state - QRhiGraphicsPipeline *currentGraphicsPipeline; - QRhiComputePipeline *currentComputePipeline; + QMetalGraphicsPipeline *currentGraphicsPipeline; + QMetalComputePipeline *currentComputePipeline; uint currentPipelineGeneration; - QRhiShaderResourceBindings *currentGraphicsSrb; - QRhiShaderResourceBindings *currentComputeSrb; + QMetalShaderResourceBindings *currentGraphicsSrb; + QMetalShaderResourceBindings *currentComputeSrb; uint currentSrbGeneration; int currentResSlot; - QRhiBuffer *currentIndexBuffer; + QMetalBuffer *currentIndexBuffer; quint32 currentIndexOffset; QRhiCommandBuffer::IndexFormat currentIndexFormat; int currentCullMode; @@ -442,6 +453,33 @@ public: bool offsetOnlyChange, const QShader::NativeResourceBindingMap *nativeResourceBindingMaps[SUPPORTED_STAGES]); int effectiveSampleCount(int sampleCount) const; + struct TessDrawArgs { + QMetalCommandBuffer *cbD; + enum { + NonIndexed, + U16Indexed, + U32Indexed + } type; + struct NonIndexedArgs { + quint32 vertexCount; + quint32 instanceCount; + quint32 firstVertex; + quint32 firstInstance; + }; + struct IndexedArgs { + quint32 indexCount; + quint32 instanceCount; + quint32 firstIndex; + qint32 vertexOffset; + quint32 firstInstance; + void *indexBuffer; + }; + union { + NonIndexedArgs draw; + IndexedArgs drawIndexed; + }; + }; + void tessellatedDraw(const TessDrawArgs &args); bool importedDevice = false; bool importedCmdQueue = false; diff --git a/src/gui/rhi/qshader.cpp b/src/gui/rhi/qshader.cpp index 8a0e841132..6802be36ef 100644 --- a/src/gui/rhi/qshader.cpp +++ b/src/gui/rhi/qshader.cpp @@ -167,7 +167,31 @@ QT_BEGIN_NAMESPACE Describes what kind of shader code an entry contains. \value StandardShader The normal, unmodified version of the shader code. + \value BatchableVertexShader Vertex shader rewritten to be suitable for Qt Quick scenegraph batching. + + \value UInt16IndexedVertexAsComputeShader A vertex shader meant to be used + in a Metal pipeline with tessellation in combination with indexed draw + calls sourcing index data from a uint16 index buffer. To support the Metal + tessellation pipeline, the vertex shader is translated to a compute shader + that may be dependent on the index buffer usage in the draw calls (e.g. if + the shader is using gl_VertexIndex), hence the need for three dedicated + variants. + + \value UInt32IndexedVertexAsComputeShader A vertex shader meant to be used + in a Metal pipeline with tessellation in combination with indexed draw + calls sourcing index data from a uint32 index buffer. To support the Metal + tessellation pipeline, the vertex shader is translated to a compute shader + that may be dependent on the index buffer usage in the draw calls (e.g. if + the shader is using gl_VertexIndex), hence the need for three dedicated + variants. + + \value NonIndexedVertexAsComputeShader A vertex shader meant to be used in + a Metal pipeline with tessellation in combination with non-indexed draw + calls. To support the Metal tessellation pipeline, the vertex shader is + translated to a compute shader that may be dependent on the index buffer + usage in the draw calls (e.g. if the shader is using gl_VertexIndex), hence + the need for three dedicated variants. */ /*! @@ -367,6 +391,19 @@ QByteArray QShader::serialized() const ds << listIt->samplerBinding; } } + ds << int(d->nativeShaderInfoMap.count()); + for (auto it = d->nativeShaderInfoMap.cbegin(), itEnd = d->nativeShaderInfoMap.cend(); it != itEnd; ++it) { + const QShaderKey &k(it.key()); + writeShaderKey(&ds, k); + ds << it->flags; + ds << int(it->extraBufferBindings.count()); + for (auto mapIt = it->extraBufferBindings.cbegin(), mapItEnd = it->extraBufferBindings.cend(); + mapIt != mapItEnd; ++mapIt) + { + ds << mapIt.key(); + ds << mapIt.value(); + } + } return qCompress(buf.buffer()); } @@ -407,6 +444,7 @@ QShader QShader::fromSerialized(const QByteArray &data) ds >> intVal; d->qsbVersion = intVal; if (d->qsbVersion != QShaderPrivate::QSB_VERSION + && d->qsbVersion != QShaderPrivate::QSB_VERSION_WITHOUT_NATIVE_SHADER_INFO && d->qsbVersion != QShaderPrivate::QSB_VERSION_WITHOUT_SEPARATE_IMAGES_AND_SAMPLERS && d->qsbVersion != QShaderPrivate::QSB_VERSION_WITHOUT_VAR_ARRAYDIMS && d->qsbVersion != QShaderPrivate::QSB_VERSION_WITH_CBOR @@ -484,6 +522,26 @@ QShader QShader::fromSerialized(const QByteArray &data) } } + if (d->qsbVersion > QShaderPrivate::QSB_VERSION_WITHOUT_NATIVE_SHADER_INFO) { + ds >> count; + for (int i = 0; i < count; ++i) { + QShaderKey k; + readShaderKey(&ds, &k); + int flags; + ds >> flags; + QMap extraBufferBindings; + int mapSize; + ds >> mapSize; + for (int b = 0; b < mapSize; ++b) { + int k, v; + ds >> k; + ds >> v; + extraBufferBindings.insert(k, v); + } + d->nativeShaderInfoMap.insert(k, { flags, extraBufferBindings }); + } + } + return bs; } @@ -711,7 +769,7 @@ QDebug operator<<(QDebug dbg, const QShaderVersion &v) /*! \typedef QShader::NativeResourceBindingMap - Synonym for QHash>. + Synonym for QMap>. The resource binding model QRhi assumes is based on SPIR-V. This means that uniform buffers, storage buffers, combined image samplers, and storage @@ -839,4 +897,62 @@ void QShader::removeSeparateToCombinedImageSamplerMappingList(const QShaderKey & d->combinedImageMap.erase(it); } +/*! + \struct QShader::NativeShaderInfo + + Describes information about the native shader code, if applicable. This + becomes relevant with certain shader languages for certain shader stages, + in case the translation from SPIR-V involves the introduction of + additional, "magic" inputs, outputs, or resources in the generated shader. + Such additions may be dependent on the original source code (i.e. the usage + of various GLSL language constructs or built-ins), and therefore it needs + to be indicated in a dynamic manner if certain features got added to the + generated shader code. + + As an example, consider a tessellation control shader with a per-patch (not + per-vertex) output variable. This is translated to a Metal compute shader + outputting (among others) into an spvPatchOut buffer. But this buffer would + not be present at all if per-patch output variables were not used. The fact + that the shader code relies on such a buffer present can be indicated by + the data in this struct. + */ + +/*! + \return the native shader info struct for \a key, or an empty object if + there is no data available for \a key, for example because such a mapping + is not applicable for the shading language or the shader stage. + */ +QShader::NativeShaderInfo QShader::nativeShaderInfo(const QShaderKey &key) const +{ + auto it = d->nativeShaderInfoMap.constFind(key); + if (it == d->nativeShaderInfoMap.cend()) + return {}; + + return it.value(); +} + +/*! + Stores the given native shader \a info associated with \a key. + + \sa nativeShaderInfo() + */ +void QShader::setNativeShaderInfo(const QShaderKey &key, const NativeShaderInfo &info) +{ + detach(); + d->nativeShaderInfoMap[key] = info; +} + +/*! + Removes the native shader information for \a key. + */ +void QShader::removeNativeShaderInfo(const QShaderKey &key) +{ + auto it = d->nativeShaderInfoMap.find(key); + if (it == d->nativeShaderInfoMap.end()) + return; + + detach(); + d->nativeShaderInfoMap.erase(it); +} + QT_END_NAMESPACE diff --git a/src/gui/rhi/qshader_p.h b/src/gui/rhi/qshader_p.h index c6ef338bfa..335712b3e1 100644 --- a/src/gui/rhi/qshader_p.h +++ b/src/gui/rhi/qshader_p.h @@ -17,6 +17,7 @@ #include #include +#include #include QT_BEGIN_NAMESPACE @@ -102,7 +103,10 @@ public: enum Variant { StandardShader = 0, - BatchableVertexShader + BatchableVertexShader, + UInt16IndexedVertexAsComputeShader, + UInt32IndexedVertexAsComputeShader, + NonIndexedVertexAsComputeShader }; QShader(); @@ -127,7 +131,7 @@ public: QByteArray serialized() const; static QShader fromSerialized(const QByteArray &data); - using NativeResourceBindingMap = QHash >; // binding -> native_binding[, native_binding] + using NativeResourceBindingMap = QMap >; // binding -> native_binding[, native_binding] NativeResourceBindingMap nativeResourceBindingMap(const QShaderKey &key) const; void setResourceBindingMap(const QShaderKey &key, const NativeResourceBindingMap &map); void removeResourceBindingMap(const QShaderKey &key); @@ -143,6 +147,14 @@ public: const SeparateToCombinedImageSamplerMappingList &list); void removeSeparateToCombinedImageSamplerMappingList(const QShaderKey &key); + struct NativeShaderInfo { + int flags = 0; + QMap extraBufferBindings; + }; + NativeShaderInfo nativeShaderInfo(const QShaderKey &key) const; + void setNativeShaderInfo(const QShaderKey &key, const NativeShaderInfo &info); + void removeNativeShaderInfo(const QShaderKey &key); + private: QShaderPrivate *d; friend struct QShaderPrivate; diff --git a/src/gui/rhi/qshader_p_p.h b/src/gui/rhi/qshader_p_p.h index e9d1e31aaf..88406b1ea2 100644 --- a/src/gui/rhi/qshader_p_p.h +++ b/src/gui/rhi/qshader_p_p.h @@ -24,13 +24,23 @@ QT_BEGIN_NAMESPACE struct Q_GUI_EXPORT QShaderPrivate { - static const int QSB_VERSION = 6; + static const int QSB_VERSION = 7; + static const int QSB_VERSION_WITHOUT_NATIVE_SHADER_INFO = 6; static const int QSB_VERSION_WITHOUT_SEPARATE_IMAGES_AND_SAMPLERS = 5; static const int QSB_VERSION_WITHOUT_VAR_ARRAYDIMS = 4; static const int QSB_VERSION_WITH_CBOR = 3; static const int QSB_VERSION_WITH_BINARY_JSON = 2; static const int QSB_VERSION_WITHOUT_BINDINGS = 1; + enum MslNativeShaderInfoExtraBufferBindings { + MslTessVertIndicesBufferBinding = 0, + MslTessVertTescOutputBufferBinding, + MslTessTescTessLevelBufferBinding, + MslTessTescPatchOutputBufferBinding, + MslTessTescParamsBufferBinding, + MslTessTescInputBufferBinding + }; + QShaderPrivate() : ref(1) { @@ -43,7 +53,8 @@ struct Q_GUI_EXPORT QShaderPrivate desc(other.desc), shaders(other.shaders), bindings(other.bindings), - combinedImageMap(other.combinedImageMap) + combinedImageMap(other.combinedImageMap), + nativeShaderInfoMap(other.nativeShaderInfoMap) { } @@ -58,6 +69,7 @@ struct Q_GUI_EXPORT QShaderPrivate QMap shaders; QMap bindings; QMap combinedImageMap; + QMap nativeShaderInfoMap; }; QT_END_NAMESPACE diff --git a/src/gui/rhi/qshaderdescription.cpp b/src/gui/rhi/qshaderdescription.cpp index d55caed210..2018a3bc13 100644 --- a/src/gui/rhi/qshaderdescription.cpp +++ b/src/gui/rhi/qshaderdescription.cpp @@ -302,7 +302,8 @@ bool QShaderDescription::isValid() const return !d->inVars.isEmpty() || !d->outVars.isEmpty() || !d->uniformBlocks.isEmpty() || !d->pushConstantBlocks.isEmpty() || !d->storageBlocks.isEmpty() || !d->combinedImageSamplers.isEmpty() || !d->storageImages.isEmpty() - || !d->separateImages.isEmpty() || !d->separateSamplers.isEmpty(); + || !d->separateImages.isEmpty() || !d->separateSamplers.isEmpty() + || !d->inBuiltins.isEmpty() || !d->outBuiltins.isEmpty(); } /*! @@ -514,7 +515,26 @@ QList QShaderDescription::storageImages() con } /*! - Returns the local size of a compute shader. + \return the list of active builtins used as input. For example, a + tessellation evaluation shader reading the value of gl_TessCoord and + gl_Position will have TessCoordBuiltin and PositionBuiltin listed here. + */ +QVector QShaderDescription::inputBuiltinVariables() const +{ + return d->inBuiltins; +} + +/*! + \return the list of active built-in variables used as input. For example, a + vertex shader will very often have PositionBuiltin as an output built-in. + */ +QVector QShaderDescription::outputBuiltinVariables() const +{ + return d->outBuiltins; +} + +/*! + \return the local size of a compute shader. For example, for a compute shader with the following declaration the function returns { 256, 16, 1}. @@ -528,6 +548,101 @@ std::array QShaderDescription::computeShaderLocalSize() const return d->localSize; } +/*! + \return the number of output vertices. + + For example, for a tessellation control shader with the following + declaration the function returns 3. + + \badcode + layout(vertices = 3) out; + \endcode + */ +uint QShaderDescription::tessellationOutputVertexCount() const +{ + return d->tessOutVertCount; +} + +/*! + \enum QShaderDescription::TessellationMode + + \value UnknownTessellationMode + \value TrianglesTessellationMode + \value QuadTessellationMode + \value IsolinesTessellationMode + */ + +/*! + \return the tessellation execution mode for a tessellation control or + evaluation shader. + + When not set, the returned value is UnknownTessellationMode. + + For example, for a tessellation evaluation shader with the following + declaration the function returns TrianglesTessellationMode. + + \badcode + layout(triangles) in; + \endcode + */ +QShaderDescription::TessellationMode QShaderDescription::tessellationMode() const +{ + return d->tessMode; +} + +/*! + \enum QShaderDescription::TessellationWindingOrder + + \value UnknownTessellationWindingOrder + \value CwTessellationWindingOrder + \value CcwTessellationWindingOrder + */ + +/*! + \return the tessellation winding order for a tessellation control or + evaluation shader. + + When not set, the returned value is UnknownTessellationWindingOrder. + + For example, for a tessellation evaluation shader with the following + declaration the function returns CcwTessellationWindingOrder. + + \badcode + layout(triangles, fractional_odd_spacing, ccw) in; + \endcode + */ +QShaderDescription::TessellationWindingOrder QShaderDescription::tessellationWindingOrder() const +{ + return d->tessWind; +} + +/*! + \enum QShaderDescription::TessellationPartitioning + + \value UnknownTessellationPartitioning + \value EqualTessellationPartitioning + \value FractionalEvenTessellationPartitioning + \value FractionalOddTessellationPartitioning + */ + +/*! + \return the tessellation partitioning mode for a tessellation control or + evaluation shader. + + When not set, the returned value is UnknownTessellationPartitioning. + + For example, for a tessellation evaluation shader with the following + declaration the function returns FractionalOddTessellationPartitioning. + + \badcode + layout(triangles, fractional_odd_spacing, ccw) in; + \endcode + */ +QShaderDescription::TessellationPartitioning QShaderDescription::tessellationPartitioning() const +{ + return d->tessPart; +} + static const struct TypeTab { const char k[20]; QShaderDescription::VariableType v; @@ -607,7 +722,7 @@ static const struct TypeTab { { "imageBuffer", QShaderDescription::ImageBuffer } }; -static QLatin1StringView typeStr(const QShaderDescription::VariableType &t) +static QLatin1StringView typeStr(QShaderDescription::VariableType t) { for (size_t i = 0; i < sizeof(typeTab) / sizeof(TypeTab); ++i) { if (typeTab[i].v == t) @@ -662,7 +777,7 @@ static const struct ImageFormatTab { { "r8ui", QShaderDescription::ImageFormatR8ui } }; -static QLatin1StringView imageFormatStr(const QShaderDescription::ImageFormat &f) +static QLatin1StringView imageFormatStr(QShaderDescription::ImageFormat f) { for (size_t i = 0; i < sizeof(imageFormatTab) / sizeof(ImageFormatTab); ++i) { if (imageFormatTab[i].v == f) @@ -671,6 +786,106 @@ static QLatin1StringView imageFormatStr(const QShaderDescription::ImageFormat &f return {}; } +static const struct BuiltinTypeTab { + const char k[21]; + QShaderDescription::BuiltinType v; +} builtinTypeTab[] = { + { "Position", QShaderDescription::PositionBuiltin }, + { "PointSize", QShaderDescription::PointSizeBuiltin }, + { "ClipDistance", QShaderDescription::ClipDistanceBuiltin }, + { "CullDistance", QShaderDescription::CullDistanceBuiltin }, + { "VertexId", QShaderDescription::VertexIdBuiltin }, + { "InstanceId", QShaderDescription::InstanceIdBuiltin }, + { "PrimitiveId", QShaderDescription::PrimitiveIdBuiltin }, + { "InvocationId", QShaderDescription::InvocationIdBuiltin }, + { "Layer", QShaderDescription::LayerBuiltin }, + { "ViewportIndex", QShaderDescription::ViewportIndexBuiltin }, + { "TessLevelOuter", QShaderDescription::TessLevelOuterBuiltin }, + { "TessLevelInner", QShaderDescription::TessLevelInnerBuiltin }, + { "TessCoord", QShaderDescription::TessCoordBuiltin }, + { "PatchVertices", QShaderDescription::PatchVerticesBuiltin }, + { "FragCoord", QShaderDescription::FragCoordBuiltin }, + { "PointCoord", QShaderDescription::PointCoordBuiltin }, + { "FrontFacing", QShaderDescription::FrontFacingBuiltin }, + { "SampleId", QShaderDescription::SampleIdBuiltin }, + { "SamplePosition", QShaderDescription::SamplePositionBuiltin }, + { "SampleMask", QShaderDescription::SampleMaskBuiltin }, + { "FragDepth", QShaderDescription::FragDepthBuiltin }, + { "NumWorkGroups", QShaderDescription::NumWorkGroupsBuiltin }, + { "WorkgroupSize", QShaderDescription::WorkgroupSizeBuiltin }, + { "WorkgroupId", QShaderDescription::WorkgroupIdBuiltin }, + { "LocalInvocationId", QShaderDescription::LocalInvocationIdBuiltin }, + { "GlobalInvocationId", QShaderDescription::GlobalInvocationIdBuiltin }, + { "LocalInvocationIndex", QShaderDescription::LocalInvocationIndexBuiltin }, + { "VertexIndex", QShaderDescription::VertexIndexBuiltin }, + { "InstanceIndex", QShaderDescription::InstanceIndexBuiltin } +}; + +static QLatin1StringView builtinTypeStr(QShaderDescription::BuiltinType t) +{ + for (size_t i = 0; i < sizeof(builtinTypeTab) / sizeof(BuiltinTypeTab); ++i) { + if (builtinTypeTab[i].v == t) + return QLatin1StringView(builtinTypeTab[i].k); + } + return {}; +} + +static const struct TessellationModeTab { + const char k[10]; + QShaderDescription::TessellationMode v; +} tessellationModeTab[] { + { "unknown", QShaderDescription::UnknownTessellationMode }, + { "triangles", QShaderDescription::TrianglesTessellationMode }, + { "quad", QShaderDescription::QuadTessellationMode }, + { "isoline", QShaderDescription::IsolineTessellationMode } +}; + +static QLatin1StringView tessModeStr(QShaderDescription::TessellationMode mode) +{ + for (size_t i = 0; i < sizeof(tessellationModeTab) / sizeof(TessellationModeTab); ++i) { + if (tessellationModeTab[i].v == mode) + return QLatin1StringView(tessellationModeTab[i].k); + } + return {}; +} + +static const struct TessellationWindingOrderTab { + const char k[8]; + QShaderDescription::TessellationWindingOrder v; +} tessellationWindingOrderTab[] { + { "unknown", QShaderDescription::UnknownTessellationWindingOrder }, + { "cw", QShaderDescription::CwTessellationWindingOrder }, + { "ccw", QShaderDescription::CcwTessellationWindingOrder } +}; + +static QLatin1StringView tessWindStr(QShaderDescription::TessellationWindingOrder w) +{ + for (size_t i = 0; i < sizeof(tessellationWindingOrderTab) / sizeof(TessellationWindingOrderTab); ++i) { + if (tessellationWindingOrderTab[i].v == w) + return QLatin1StringView(tessellationWindingOrderTab[i].k); + } + return {}; +} + +static const struct TessellationPartitioningTab { + const char k[24]; + QShaderDescription::TessellationPartitioning v; +} tessellationPartitioningTab[] { + { "unknown", QShaderDescription::UnknownTessellationPartitioning }, + { "equal_spacing", QShaderDescription::EqualTessellationPartitioning }, + { "fractional_even_spacing", QShaderDescription::FractionalEvenTessellationPartitioning }, + { "fractional_odd_spacing", QShaderDescription::FractionalOddTessellationPartitioning } +}; + +static QLatin1StringView tessPartStr(QShaderDescription::TessellationPartitioning p) +{ + for (size_t i = 0; i < sizeof(tessellationPartitioningTab) / sizeof(TessellationPartitioningTab); ++i) { + if (tessellationPartitioningTab[i].v == p) + return QLatin1StringView(tessellationPartitioningTab[i].k); + } + return {}; +} + #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug dbg, const QShaderDescription &sd) { @@ -688,6 +903,8 @@ QDebug operator<<(QDebug dbg, const QShaderDescription &sd) << " storageImages " << d->storageImages << " separateImages " << d->separateImages << " separateSamplers " << d->separateSamplers + << " inBuiltins " << d->inBuiltins + << " outBuiltins " << d->outBuiltins << ')'; } else { dbg.nospace() << "QShaderDescription(null)"; @@ -700,6 +917,8 @@ QDebug operator<<(QDebug dbg, const QShaderDescription::InOutVariable &var) { QDebugStateSaver saver(dbg); dbg.nospace() << "InOutVariable(" << typeStr(var.type) << ' ' << var.name; + if (var.perPatch) + dbg.nospace() << " per-patch"; if (var.location >= 0) dbg.nospace() << " location=" << var.location; if (var.binding >= 0) @@ -768,6 +987,13 @@ QDebug operator<<(QDebug dbg, const QShaderDescription::StorageBlock &blk) dbg.nospace() << ' ' << blk.members << ')'; return dbg; } + +QDebug operator<<(QDebug dbg, const QShaderDescription::BuiltinVariable &builtin) +{ + QDebugStateSaver saver(dbg); + dbg.nospace() << "BuiltinVariable(type=" << builtinTypeStr(builtin.type) << ")"; + return dbg; +} #endif #define JSON_KEY(key) static constexpr QLatin1StringView key ## Key() noexcept { return QLatin1StringView( #key ); } @@ -776,6 +1002,7 @@ JSON_KEY(type) JSON_KEY(location) JSON_KEY(binding) JSON_KEY(set) +JSON_KEY(perPatch) JSON_KEY(imageFormat) JSON_KEY(imageFlags) JSON_KEY(offset) @@ -797,7 +1024,13 @@ JSON_KEY(pushConstantBlocks) JSON_KEY(storageBlocks) JSON_KEY(combinedImageSamplers) JSON_KEY(storageImages) -JSON_KEY(localSize) +JSON_KEY(inBuiltins) +JSON_KEY(outBuiltins) +JSON_KEY(computeLocalSize) +JSON_KEY(tessellationOutputVertexCount) +JSON_KEY(tessellationMode) +JSON_KEY(tessellationWindingOrder) +JSON_KEY(tessellationPartitioning) JSON_KEY(separateImages) JSON_KEY(separateSamplers) #undef JSON_KEY @@ -810,6 +1043,8 @@ static void addDeco(QJsonObject *obj, const QShaderDescription::InOutVariable &v (*obj)[bindingKey()] = v.binding; if (v.descriptorSet >= 0) (*obj)[setKey()] = v.descriptorSet; + if (v.perPatch) + (*obj)[perPatchKey()] = v.perPatch; if (v.imageFormat != QShaderDescription::ImageFormatUnknown) (*obj)[imageFormatKey()] = imageFormatStr(v.imageFormat); if (v.imageFlags) @@ -832,6 +1067,7 @@ static void serializeDecorations(QDataStream *stream, const QShaderDescription:: (*stream) << int(v.arrayDims.count()); for (int dim : v.arrayDims) (*stream) << dim; + (*stream) << quint8(v.perPatch); } static QJsonObject inOutObject(const QShaderDescription::InOutVariable &v) @@ -985,10 +1221,42 @@ QJsonDocument QShaderDescriptionPrivate::makeDoc() if (!jstorageImages.isEmpty()) root[storageImagesKey()] = jstorageImages; - QJsonArray jlocalSize; - for (int i = 0; i < 3; ++i) - jlocalSize.append(QJsonValue(int(localSize[i]))); - root[localSizeKey()] = jlocalSize; + QJsonArray jinBuiltins; + for (const QShaderDescription::BuiltinVariable &v : qAsConst(inBuiltins)) { + QJsonObject builtin; + builtin[typeKey()] = builtinTypeStr(v.type); + jinBuiltins.append(builtin); + } + if (!jinBuiltins.isEmpty()) + root[inBuiltinsKey()] = jinBuiltins; + + QJsonArray joutBuiltins; + for (const QShaderDescription::BuiltinVariable &v : qAsConst(outBuiltins)) { + QJsonObject builtin; + builtin[typeKey()] = builtinTypeStr(v.type); + joutBuiltins.append(builtin); + } + if (!joutBuiltins.isEmpty()) + root[outBuiltinsKey()] = joutBuiltins; + + if (localSize[0] || localSize[1] || localSize[2]) { + QJsonArray jlocalSize; + for (size_t i = 0; i < 3; ++i) + jlocalSize.append(QJsonValue(int(localSize[i]))); + root[computeLocalSizeKey()] = jlocalSize; + } + + if (tessOutVertCount) + root[tessellationOutputVertexCountKey()] = int(tessOutVertCount); + + if (tessMode != QShaderDescription::UnknownTessellationMode) + root[tessellationModeKey()] = tessModeStr(tessMode); + + if (tessWind != QShaderDescription::UnknownTessellationWindingOrder) + root[tessellationWindingOrderKey()] = tessWindStr(tessWind); + + if (tessPart != QShaderDescription::UnknownTessellationPartitioning) + root[tessellationPartitioningKey()] = tessPartStr(tessPart); QJsonArray jseparateImages; for (const QShaderDescription::InOutVariable &v : qAsConst(separateImages)) { @@ -1073,7 +1341,7 @@ void QShaderDescriptionPrivate::writeToStream(QDataStream *stream) } for (size_t i = 0; i < 3; ++i) - (*stream) << localSize[i]; + (*stream) << quint32(localSize[i]); (*stream) << int(separateImages.count()); for (const QShaderDescription::InOutVariable &v : qAsConst(separateImages)) { @@ -1088,6 +1356,19 @@ void QShaderDescriptionPrivate::writeToStream(QDataStream *stream) (*stream) << int(v.type); serializeDecorations(stream, v); } + + (*stream) << quint32(tessOutVertCount); + (*stream) << quint32(tessMode); + (*stream) << quint32(tessWind); + (*stream) << quint32(tessPart); + + (*stream) << int(inBuiltins.count()); + for (const QShaderDescription::BuiltinVariable &v : qAsConst(inBuiltins)) + (*stream) << int(v.type); + + (*stream) << int(outBuiltins.count()); + for (const QShaderDescription::BuiltinVariable &v : qAsConst(outBuiltins)) + (*stream) << int(v.type); } static void deserializeDecorations(QDataStream *stream, int version, QShaderDescription::InOutVariable *v) @@ -1107,6 +1388,12 @@ static void deserializeDecorations(QDataStream *stream, int version, QShaderDesc for (int i = 0; i < f; ++i) (*stream) >> v->arrayDims[i]; } + + if (version > QShaderPrivate::QSB_VERSION_WITHOUT_NATIVE_SHADER_INFO) { + quint8 b; + (*stream) >> b; + v->perPatch = b; + } } static QShaderDescription::InOutVariable deserializeInOutVar(QDataStream *stream, int version) @@ -1237,8 +1524,11 @@ void QShaderDescriptionPrivate::loadFromStream(QDataStream *stream, int version) deserializeDecorations(stream, version, &storageImages[i]); } - for (size_t i = 0; i < 3; ++i) - (*stream) >> localSize[i]; + for (size_t i = 0; i < 3; ++i) { + quint32 v; + (*stream) >> v; + localSize[i] = v; + } if (version > QShaderPrivate::QSB_VERSION_WITHOUT_SEPARATE_IMAGES_AND_SAMPLERS) { (*stream) >> count; @@ -1265,6 +1555,34 @@ void QShaderDescriptionPrivate::loadFromStream(QDataStream *stream, int version) deserializeDecorations(stream, version, &separateSamplers[i]); } } + + if (version > QShaderPrivate::QSB_VERSION_WITHOUT_NATIVE_SHADER_INFO) { + quint32 v; + (*stream) >> v; + tessOutVertCount = v; + (*stream) >> v; + tessMode = QShaderDescription::TessellationMode(v); + (*stream) >> v; + tessWind = QShaderDescription::TessellationWindingOrder(v); + (*stream) >> v; + tessPart = QShaderDescription::TessellationPartitioning(v); + + (*stream) >> count; + inBuiltins.resize(count); + for (int i = 0; i < count; ++i) { + int t; + (*stream) >> t; + inBuiltins[i].type = QShaderDescription::BuiltinType(t); + } + + (*stream) >> count; + outBuiltins.resize(count); + for (int i = 0; i < count; ++i) { + int t; + (*stream) >> t; + outBuiltins[i].type = QShaderDescription::BuiltinType(t); + } + } } /*! @@ -1287,7 +1605,13 @@ bool operator==(const QShaderDescription &lhs, const QShaderDescription &rhs) no && lhs.d->separateImages == rhs.d->separateImages && lhs.d->separateSamplers == rhs.d->separateSamplers && lhs.d->storageImages == rhs.d->storageImages - && lhs.d->localSize == rhs.d->localSize; + && lhs.d->inBuiltins == rhs.d->inBuiltins + && lhs.d->outBuiltins == rhs.d->outBuiltins + && lhs.d->localSize == rhs.d->localSize + && lhs.d->tessOutVertCount == rhs.d->tessOutVertCount + && lhs.d->tessMode == rhs.d->tessMode + && lhs.d->tessWind == rhs.d->tessWind + && lhs.d->tessPart == rhs.d->tessPart; } /*! @@ -1305,7 +1629,8 @@ bool operator==(const QShaderDescription::InOutVariable &lhs, const QShaderDescr && lhs.descriptorSet == rhs.descriptorSet && lhs.imageFormat == rhs.imageFormat && lhs.imageFlags == rhs.imageFlags - && lhs.arrayDims == rhs.arrayDims; + && lhs.arrayDims == rhs.arrayDims + && lhs.perPatch == rhs.perPatch; } /*! @@ -1372,4 +1697,15 @@ bool operator==(const QShaderDescription::StorageBlock &lhs, const QShaderDescri && lhs.members == rhs.members; } +/*! + Returns \c true if the two BuiltinVariable objects \a lhs and \a rhs are + equal. + + \relates QShaderDescription::BuiltinVariable + */ +bool operator==(const QShaderDescription::BuiltinVariable &lhs, const QShaderDescription::BuiltinVariable &rhs) noexcept +{ + return lhs.type == rhs.type; +} + QT_END_NAMESPACE diff --git a/src/gui/rhi/qshaderdescription_p.h b/src/gui/rhi/qshaderdescription_p.h index a64496ead7..df4e8fd873 100644 --- a/src/gui/rhi/qshaderdescription_p.h +++ b/src/gui/rhi/qshaderdescription_p.h @@ -181,6 +181,7 @@ public: ImageFormat imageFormat = ImageFormatUnknown; ImageFlags imageFlags; QList arrayDims; + bool perPatch = false; }; struct BlockVariable { @@ -229,8 +230,76 @@ public: QList separateSamplers() const; QList storageImages() const; + enum BuiltinType { + // must match SpvBuiltIn + PositionBuiltin = 0, + PointSizeBuiltin = 1, + ClipDistanceBuiltin = 3, + CullDistanceBuiltin = 4, + VertexIdBuiltin = 5, + InstanceIdBuiltin = 6, + PrimitiveIdBuiltin = 7, + InvocationIdBuiltin = 8, + LayerBuiltin = 9, + ViewportIndexBuiltin = 10, + TessLevelOuterBuiltin = 11, + TessLevelInnerBuiltin = 12, + TessCoordBuiltin = 13, + PatchVerticesBuiltin = 14, + FragCoordBuiltin = 15, + PointCoordBuiltin = 16, + FrontFacingBuiltin = 17, + SampleIdBuiltin = 18, + SamplePositionBuiltin = 19, + SampleMaskBuiltin = 20, + FragDepthBuiltin = 22, + NumWorkGroupsBuiltin = 24, + WorkgroupSizeBuiltin = 25, + WorkgroupIdBuiltin = 26, + LocalInvocationIdBuiltin = 27, + GlobalInvocationIdBuiltin = 28, + LocalInvocationIndexBuiltin = 29, + VertexIndexBuiltin = 42, + InstanceIndexBuiltin = 43 + }; + + struct BuiltinVariable { + BuiltinType type; + }; + + QList inputBuiltinVariables() const; + QList outputBuiltinVariables() const; + std::array computeShaderLocalSize() const; + uint tessellationOutputVertexCount() const; + + enum TessellationMode { + UnknownTessellationMode, + TrianglesTessellationMode, + QuadTessellationMode, + IsolineTessellationMode + }; + + TessellationMode tessellationMode() const; + + enum TessellationWindingOrder { + UnknownTessellationWindingOrder, + CwTessellationWindingOrder, + CcwTessellationWindingOrder + }; + + TessellationWindingOrder tessellationWindingOrder() const; + + enum TessellationPartitioning { + UnknownTessellationPartitioning, + EqualTessellationPartitioning, + FractionalEvenTessellationPartitioning, + FractionalOddTessellationPartitioning + }; + + TessellationPartitioning tessellationPartitioning() const; + private: QShaderDescriptionPrivate *d; friend struct QShaderDescriptionPrivate; @@ -249,6 +318,7 @@ Q_GUI_EXPORT QDebug operator<<(QDebug, const QShaderDescription::BlockVariable & Q_GUI_EXPORT QDebug operator<<(QDebug, const QShaderDescription::UniformBlock &); Q_GUI_EXPORT QDebug operator<<(QDebug, const QShaderDescription::PushConstantBlock &); Q_GUI_EXPORT QDebug operator<<(QDebug, const QShaderDescription::StorageBlock &); +Q_GUI_EXPORT QDebug operator<<(QDebug, const QShaderDescription::BuiltinVariable &); #endif Q_GUI_EXPORT bool operator==(const QShaderDescription &lhs, const QShaderDescription &rhs) noexcept; @@ -257,6 +327,7 @@ Q_GUI_EXPORT bool operator==(const QShaderDescription::BlockVariable &lhs, const Q_GUI_EXPORT bool operator==(const QShaderDescription::UniformBlock &lhs, const QShaderDescription::UniformBlock &rhs) noexcept; Q_GUI_EXPORT bool operator==(const QShaderDescription::PushConstantBlock &lhs, const QShaderDescription::PushConstantBlock &rhs) noexcept; Q_GUI_EXPORT bool operator==(const QShaderDescription::StorageBlock &lhs, const QShaderDescription::StorageBlock &rhs) noexcept; +Q_GUI_EXPORT bool operator==(const QShaderDescription::BuiltinVariable &lhs, const QShaderDescription::BuiltinVariable &rhs) noexcept; inline bool operator!=(const QShaderDescription &lhs, const QShaderDescription &rhs) noexcept { @@ -288,6 +359,11 @@ inline bool operator!=(const QShaderDescription::StorageBlock &lhs, const QShade return !(lhs == rhs); } +inline bool operator!=(const QShaderDescription::BuiltinVariable &lhs, const QShaderDescription::BuiltinVariable &rhs) noexcept +{ + return !(lhs == rhs); +} + QT_END_NAMESPACE #endif diff --git a/src/gui/rhi/qshaderdescription_p_p.h b/src/gui/rhi/qshaderdescription_p_p.h index 6a981213b1..1ccb7a14ed 100644 --- a/src/gui/rhi/qshaderdescription_p_p.h +++ b/src/gui/rhi/qshaderdescription_p_p.h @@ -27,7 +27,6 @@ struct Q_GUI_EXPORT QShaderDescriptionPrivate QShaderDescriptionPrivate() : ref(1) { - localSize[0] = localSize[1] = localSize[2] = 0; } QShaderDescriptionPrivate(const QShaderDescriptionPrivate &other) @@ -41,7 +40,13 @@ struct Q_GUI_EXPORT QShaderDescriptionPrivate separateImages(other.separateImages), separateSamplers(other.separateSamplers), storageImages(other.storageImages), - localSize(other.localSize) + inBuiltins(other.inBuiltins), + outBuiltins(other.outBuiltins), + localSize(other.localSize), + tessOutVertCount(other.tessOutVertCount), + tessMode(other.tessMode), + tessWind(other.tessWind), + tessPart(other.tessPart) { } @@ -62,7 +67,13 @@ struct Q_GUI_EXPORT QShaderDescriptionPrivate QList separateImages; QList separateSamplers; QList storageImages; - std::array localSize; + QList inBuiltins; + QList outBuiltins; + std::array localSize = {}; + uint tessOutVertCount = 0; + QShaderDescription::TessellationMode tessMode = QShaderDescription::UnknownTessellationMode; + QShaderDescription::TessellationWindingOrder tessWind = QShaderDescription::UnknownTessellationWindingOrder; + QShaderDescription::TessellationPartitioning tessPart = QShaderDescription::UnknownTessellationPartitioning; }; QT_END_NAMESPACE diff --git a/tests/auto/gui/rhi/qrhi/data/buildshaders.bat b/tests/auto/gui/rhi/qrhi/data/buildshaders.bat index 68d9bb8ae7..8518db8afc 100644 --- a/tests/auto/gui/rhi/qrhi/data/buildshaders.bat +++ b/tests/auto/gui/rhi/qrhi/data/buildshaders.bat @@ -11,3 +11,7 @@ qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.vert.qsb textured. qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.frag.qsb textured.frag qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured_multiubuf.vert.qsb textured_multiubuf.vert qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured_multiubuf.frag.qsb textured_multiubuf.frag +qsb --glsl 320es,410 --msl 12 --msltess simpletess.vert -o simpletess.vert.qsb +qsb --glsl 320es,410 --msl 12 --tess-mode triangles simpletess.tesc -o simpletess.tesc.qsb +qsb --glsl 320es,410 --msl 12 --tess-vertex-count 3 simpletess.tese -o simpletess.tese.qsb +qsb --glsl 320es,410 --msl 12 simpletess.frag -o simpletess.frag.qsb diff --git a/tests/auto/gui/rhi/qrhi/data/simpletess.frag b/tests/auto/gui/rhi/qrhi/data/simpletess.frag new file mode 100644 index 0000000000..375587662f --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/simpletess.frag @@ -0,0 +1,10 @@ +#version 440 + +layout(location = 0) in vec3 v_color; + +layout(location = 0) out vec4 fragColor; + +void main() +{ + fragColor = vec4(v_color, 1.0); +} diff --git a/tests/auto/gui/rhi/qrhi/data/simpletess.frag.qsb b/tests/auto/gui/rhi/qrhi/data/simpletess.frag.qsb new file mode 100644 index 0000000000..0f42103ac5 Binary files /dev/null and b/tests/auto/gui/rhi/qrhi/data/simpletess.frag.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/simpletess.tesc b/tests/auto/gui/rhi/qrhi/data/simpletess.tesc new file mode 100644 index 0000000000..e192fc77c7 --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/simpletess.tesc @@ -0,0 +1,22 @@ +#version 440 + +layout(vertices = 3) out; + +layout(location = 0) in vec3 inColor[]; +layout(location = 0) out vec3 outColor[]; +layout(location = 1) patch out float a_per_patch_output_variable; + +void main() +{ + if (gl_InvocationID == 0) { + gl_TessLevelOuter[0] = 4.0; + gl_TessLevelOuter[1] = 4.0; + gl_TessLevelOuter[2] = 4.0; + + gl_TessLevelInner[0] = 4.0; + } + + gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; + outColor[gl_InvocationID] = inColor[gl_InvocationID]; + a_per_patch_output_variable = 1.0; +} diff --git a/tests/auto/gui/rhi/qrhi/data/simpletess.tesc.qsb b/tests/auto/gui/rhi/qrhi/data/simpletess.tesc.qsb new file mode 100644 index 0000000000..8c98d92c46 Binary files /dev/null and b/tests/auto/gui/rhi/qrhi/data/simpletess.tesc.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/simpletess.tese b/tests/auto/gui/rhi/qrhi/data/simpletess.tese new file mode 100644 index 0000000000..17b348635a --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/simpletess.tese @@ -0,0 +1,17 @@ +#version 440 + +layout(triangles, fractional_odd_spacing, ccw) in; + +layout(location = 0) in vec3 inColor[]; +layout(location = 0) out vec3 outColor; +layout(location = 1) patch in float a_per_patch_output_variable; + +layout(std140, binding = 0) uniform buf { + mat4 mvp; +}; + +void main() +{ + gl_Position = mvp * ((gl_TessCoord.x * gl_in[0].gl_Position) + (gl_TessCoord.y * gl_in[1].gl_Position) + (gl_TessCoord.z * gl_in[2].gl_Position)); + outColor = gl_TessCoord.x * inColor[0] + gl_TessCoord.y * inColor[1] + gl_TessCoord.z * inColor[2] * a_per_patch_output_variable; +} diff --git a/tests/auto/gui/rhi/qrhi/data/simpletess.tese.qsb b/tests/auto/gui/rhi/qrhi/data/simpletess.tese.qsb new file mode 100644 index 0000000000..8aa7632717 Binary files /dev/null and b/tests/auto/gui/rhi/qrhi/data/simpletess.tese.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/simpletess.vert b/tests/auto/gui/rhi/qrhi/data/simpletess.vert new file mode 100644 index 0000000000..3838d2f3bb --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/simpletess.vert @@ -0,0 +1,12 @@ +#version 440 + +layout(location = 0) in vec3 position; +layout(location = 1) in vec3 color; + +layout(location = 0) out vec3 v_color; + +void main() +{ + gl_Position = vec4(position, 1.0); + v_color = color; +} diff --git a/tests/auto/gui/rhi/qrhi/data/simpletess.vert.qsb b/tests/auto/gui/rhi/qrhi/data/simpletess.vert.qsb new file mode 100644 index 0000000000..ee90983e0b Binary files /dev/null and b/tests/auto/gui/rhi/qrhi/data/simpletess.vert.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp index d1252f8abf..45aa8799f9 100644 --- a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp +++ b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp @@ -136,6 +136,9 @@ private slots: void renderToRgb10Texture_data(); void renderToRgb10Texture(); + void tessellation_data(); + void tessellation(); + private: void setWindowType(QWindow *window, QRhi::Implementation impl); @@ -4899,5 +4902,165 @@ void tst_QRhi::renderToRgb10Texture() QVERIFY(redCount > blueCount); // 1742 > 178 } +void tst_QRhi::tessellation_data() +{ + rhiTestData(); +} + +void tst_QRhi::tessellation() +{ + QFETCH(QRhi::Implementation, impl); + QFETCH(QRhiInitParams *, initParams); + + QScopedPointer rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr)); + if (!rhi) + QSKIP("QRhi could not be created, skipping testing rendering"); + + if (!rhi->isFeatureSupported(QRhi::Tessellation)) { + // From a Vulkan or Metal implementation we expect tessellation to work, + // even though it is optional (as per spec) for Vulkan. + QVERIFY(rhi->backend() != QRhi::Vulkan); + QVERIFY(rhi->backend() != QRhi::Metal); + QSKIP("Tessellation is not supported with this graphics API, skipping test"); + } + + if (rhi->backend() == QRhi::D3D11) + QSKIP("Skipping tessellation test on D3D for now, test assets not prepared for HLSL yet"); + + QScopedPointer texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(1280, 720), 1, + QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); + QVERIFY(texture->create()); + + QScopedPointer rt(rhi->newTextureRenderTarget({ texture.data() })); + QScopedPointer rpDesc(rt->newCompatibleRenderPassDescriptor()); + rt->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(rt->create()); + + static const float triangleVertices[] = { + 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, + -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, + }; + + QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch(); + QScopedPointer vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices))); + QVERIFY(vbuf->create()); + u->uploadStaticBuffer(vbuf.data(), triangleVertices); + + QScopedPointer ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64)); + QVERIFY(ubuf->create()); + + // Use the 3D API specific correction matrix that flips Y, so we can use + // the OpenGL-targeted vertex data and the tessellation winding order of + // counter-clockwise to get uniform results. + QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix(); + u->updateDynamicBuffer(ubuf.data(), 0, 64, mvp.constData()); + + QScopedPointer srb(rhi->newShaderResourceBindings()); + srb->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::TessellationEvaluationStage, ubuf.data()), + }); + QVERIFY(srb->create()); + + QScopedPointer pipeline(rhi->newGraphicsPipeline()); + + pipeline->setTopology(QRhiGraphicsPipeline::Patches); + pipeline->setPatchControlPointCount(3); + + pipeline->setShaderStages({ + { QRhiShaderStage::Vertex, loadShader(":/data/simpletess.vert.qsb") }, + { QRhiShaderStage::TessellationControl, loadShader(":/data/simpletess.tesc.qsb") }, + { QRhiShaderStage::TessellationEvaluation, loadShader(":/data/simpletess.tese.qsb") }, + { QRhiShaderStage::Fragment, loadShader(":/data/simpletess.frag.qsb") } + }); + + pipeline->setCullMode(QRhiGraphicsPipeline::Back); // to ensure the winding order is correct + + // won't get the wireframe with OpenGL ES + if (rhi->isFeatureSupported(QRhi::NonFillPolygonMode)) + pipeline->setPolygonMode(QRhiGraphicsPipeline::Line); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 6 * sizeof(float) } + }); + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, + { 0, 1, QRhiVertexInputAttribute::Float3, 3 * sizeof(float) } + }); + + pipeline->setVertexInputLayout(inputLayout); + pipeline->setShaderResourceBindings(srb.data()); + pipeline->setRenderPassDescriptor(rpDesc.data()); + + QVERIFY(pipeline->create()); + + QRhiCommandBuffer *cb = nullptr; + QCOMPARE(rhi->beginOffscreenFrame(&cb), QRhi::FrameOpSuccess); + + cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, u); + cb->setGraphicsPipeline(pipeline.data()); + cb->setViewport({ 0, 0, float(rt->pixelSize().width()), float(rt->pixelSize().height()) }); + cb->setShaderResources(); + QRhiCommandBuffer::VertexInput vbufBinding(vbuf.data(), 0); + cb->setVertexInput(0, 1, &vbufBinding); + cb->draw(3); + + QRhiReadbackResult readResult; + QImage result; + readResult.completed = [&readResult, &result] { + result = QImage(reinterpret_cast(readResult.data.constData()), + readResult.pixelSize.width(), readResult.pixelSize.height(), + QImage::Format_RGBA8888); + }; + QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch(); + readbackBatch->readBackTexture({ texture.data() }, &readResult); + cb->endPass(readbackBatch); + + rhi->endOffscreenFrame(); + + if (rhi->isYUpInFramebuffer()) // we used clipSpaceCorrMatrix so this is different from many other tests + result = std::move(result).mirrored(); + + QCOMPARE(result.size(), rt->pixelSize()); + + // cannot check rendering results with Null, because there is no rendering there + if (impl == QRhi::Null) + return; + + int redCount = 0, greenCount = 0, blueCount = 0; + for (int y = 0; y < result.height(); ++y) { + const quint32 *p = reinterpret_cast(result.constScanLine(y)); + int x = result.width() - 1; + while (x-- >= 0) { + const QRgb c(*p++); + const int red = qRed(c); + const int green = qGreen(c); + const int blue = qBlue(c); + // just count the color components that are above a certain threshold + if (red > 240) + ++redCount; + if (green > 240) + ++greenCount; + if (blue > 240) + ++blueCount; + } + } + + // Line drawing can be different between the 3D APIs. What we will check if + // the number of strong-enough r/g/b components above a certain threshold. + // That is good enough to ensure that something got rendered, i.e. that + // tessellation is not completely broken. + // + // For the record the actual values are something like: + // OpenGL (NVIDIA, Windows) 59 82 82 + // Metal (Intel, macOS 12.5) 59 79 79 + // Vulkan (NVIDIA, Windows) 71 85 85 + + QVERIFY(redCount > 50); + QVERIFY(blueCount > 50); + QVERIFY(greenCount > 50); +} + #include QTEST_MAIN(tst_QRhi) diff --git a/tests/auto/gui/rhi/qshader/data/color.vert b/tests/auto/gui/rhi/qshader/data/color.vert deleted file mode 100644 index c92f71b9e1..0000000000 --- a/tests/auto/gui/rhi/qshader/data/color.vert +++ /dev/null @@ -1,18 +0,0 @@ -#version 440 - -layout(location = 0) in vec4 position; -layout(location = 1) in vec3 color; -layout(location = 0) out vec3 v_color; - -layout(std140, binding = 0) uniform buf { - mat4 mvp; - float opacity; -} ubuf; - -out gl_PerVertex { vec4 gl_Position; }; - -void main() -{ - v_color = color; - gl_Position = ubuf.mvp * position; -} diff --git a/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.frag.qsb b/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.frag.qsb new file mode 100644 index 0000000000..4d49ede3ff Binary files /dev/null and b/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.frag.qsb differ diff --git a/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.tesc.qsb b/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.tesc.qsb new file mode 100644 index 0000000000..ea68da7eb4 Binary files /dev/null and b/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.tesc.qsb differ diff --git a/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.tese.qsb b/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.tese.qsb new file mode 100644 index 0000000000..41005f76bc Binary files /dev/null and b/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.tese.qsb differ diff --git a/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.vert.qsb b/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.vert.qsb new file mode 100644 index 0000000000..39734b6d5d Binary files /dev/null and b/tests/auto/gui/rhi/qshader/data/metal_enabled_tessellation_v7.vert.qsb differ diff --git a/tests/auto/gui/rhi/qshader/data/texture.frag b/tests/auto/gui/rhi/qshader/data/texture.frag deleted file mode 100644 index bd22f817e0..0000000000 --- a/tests/auto/gui/rhi/qshader/data/texture.frag +++ /dev/null @@ -1,16 +0,0 @@ -#version 440 - -layout(location = 0) in vec2 qt_TexCoord; -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - float opacity; -} ubuf; - -layout(binding = 1) uniform sampler2D qt_Texture; - -void main() -{ - fragColor = texture(qt_Texture, qt_TexCoord) * ubuf.opacity; -} diff --git a/tests/auto/gui/rhi/qshader/data/texture_sep.frag b/tests/auto/gui/rhi/qshader/data/texture_sep.frag deleted file mode 100644 index 368e851bb4..0000000000 --- a/tests/auto/gui/rhi/qshader/data/texture_sep.frag +++ /dev/null @@ -1,17 +0,0 @@ -#version 440 - -layout(location = 0) in vec2 v_texcoord; - -layout(location = 0) out vec4 fragColor; - -layout(binding = 1) uniform sampler2D combinedTexSampler; -layout(binding = 2) uniform texture2D sepTex; -layout(binding = 3) uniform sampler sepSampler; -layout(binding = 4) uniform sampler sepSampler2; - -void main() -{ - fragColor = texture(sampler2D(sepTex, sepSampler), v_texcoord); - fragColor *= texture(sampler2D(sepTex, sepSampler2), v_texcoord); - fragColor *= texture(combinedTexSampler, v_texcoord); -} diff --git a/tests/auto/gui/rhi/qshader/data_src/color.vert b/tests/auto/gui/rhi/qshader/data_src/color.vert new file mode 100644 index 0000000000..c92f71b9e1 --- /dev/null +++ b/tests/auto/gui/rhi/qshader/data_src/color.vert @@ -0,0 +1,18 @@ +#version 440 + +layout(location = 0) in vec4 position; +layout(location = 1) in vec3 color; +layout(location = 0) out vec3 v_color; + +layout(std140, binding = 0) uniform buf { + mat4 mvp; + float opacity; +} ubuf; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() +{ + v_color = color; + gl_Position = ubuf.mvp * position; +} diff --git a/tests/auto/gui/rhi/qshader/data_src/texture.frag b/tests/auto/gui/rhi/qshader/data_src/texture.frag new file mode 100644 index 0000000000..bd22f817e0 --- /dev/null +++ b/tests/auto/gui/rhi/qshader/data_src/texture.frag @@ -0,0 +1,16 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float opacity; +} ubuf; + +layout(binding = 1) uniform sampler2D qt_Texture; + +void main() +{ + fragColor = texture(qt_Texture, qt_TexCoord) * ubuf.opacity; +} diff --git a/tests/auto/gui/rhi/qshader/data_src/texture_sep.frag b/tests/auto/gui/rhi/qshader/data_src/texture_sep.frag new file mode 100644 index 0000000000..368e851bb4 --- /dev/null +++ b/tests/auto/gui/rhi/qshader/data_src/texture_sep.frag @@ -0,0 +1,17 @@ +#version 440 + +layout(location = 0) in vec2 v_texcoord; + +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D combinedTexSampler; +layout(binding = 2) uniform texture2D sepTex; +layout(binding = 3) uniform sampler sepSampler; +layout(binding = 4) uniform sampler sepSampler2; + +void main() +{ + fragColor = texture(sampler2D(sepTex, sepSampler), v_texcoord); + fragColor *= texture(sampler2D(sepTex, sepSampler2), v_texcoord); + fragColor *= texture(combinedTexSampler, v_texcoord); +} diff --git a/tests/auto/gui/rhi/qshader/tst_qshader.cpp b/tests/auto/gui/rhi/qshader/tst_qshader.cpp index 40aa9d9a87..3065386ea9 100644 --- a/tests/auto/gui/rhi/qshader/tst_qshader.cpp +++ b/tests/auto/gui/rhi/qshader/tst_qshader.cpp @@ -25,6 +25,7 @@ private slots: void loadV4(); void manualShaderPackCreation(); void loadV6WithSeparateImagesAndSamplers(); + void loadV7(); }; static QShader getShader(const QString &name) @@ -590,5 +591,87 @@ void tst_QShader::loadV6WithSeparateImagesAndSamplers() } } +void tst_QShader::loadV7() +{ + QShader vert = getShader(QLatin1String(":/data/metal_enabled_tessellation_v7.vert.qsb")); + QVERIFY(vert.isValid()); + QCOMPARE(QShaderPrivate::get(&vert)->qsbVersion, 7); + QCOMPARE(vert.availableShaders().count(), 8); + + QCOMPARE(vert.description().inputVariables().count(), 2); + QCOMPARE(vert.description().outputBuiltinVariables().count(), 1); + QCOMPARE(vert.description().outputBuiltinVariables()[0].type, QShaderDescription::PositionBuiltin); + QCOMPARE(vert.description().outputVariables().count(), 1); + QCOMPARE(vert.description().outputVariables()[0].name, QByteArrayLiteral("v_color")); + + QVERIFY(vert.availableShaders().contains(QShaderKey(QShader::MslShader, QShaderVersion(12)))); + QVERIFY(!vert.shader(QShaderKey(QShader::MslShader, QShaderVersion(12), QShader::NonIndexedVertexAsComputeShader)).shader().isEmpty()); + QVERIFY(!vert.shader(QShaderKey(QShader::MslShader, QShaderVersion(12), QShader::UInt16IndexedVertexAsComputeShader)).shader().isEmpty()); + QVERIFY(!vert.shader(QShaderKey(QShader::MslShader, QShaderVersion(12), QShader::UInt32IndexedVertexAsComputeShader)).shader().isEmpty()); + + QShader tesc = getShader(QLatin1String(":/data/metal_enabled_tessellation_v7.tesc.qsb")); + QVERIFY(tesc.isValid()); + QCOMPARE(QShaderPrivate::get(&tesc)->qsbVersion, 7); + QCOMPARE(tesc.availableShaders().count(), 5); + QCOMPARE(tesc.description().tessellationOutputVertexCount(), 3); + + QCOMPARE(tesc.description().inputBuiltinVariables().count(), 2); + QCOMPARE(tesc.description().outputBuiltinVariables().count(), 3); + // builtins must be sorted based on the type + QCOMPARE(tesc.description().inputBuiltinVariables()[0].type, QShaderDescription::PositionBuiltin); + QCOMPARE(tesc.description().inputBuiltinVariables()[1].type, QShaderDescription::InvocationIdBuiltin); + QCOMPARE(tesc.description().outputBuiltinVariables()[0].type, QShaderDescription::PositionBuiltin); + QCOMPARE(tesc.description().outputBuiltinVariables()[1].type, QShaderDescription::TessLevelOuterBuiltin); + QCOMPARE(tesc.description().outputBuiltinVariables()[2].type, QShaderDescription::TessLevelInnerBuiltin); + + QCOMPARE(tesc.description().outputVariables().count(), 3); + for (const QShaderDescription::InOutVariable &v : tesc.description().outputVariables()) { + switch (v.location) { + case 0: + QCOMPARE(v.name, QByteArrayLiteral("outColor")); + QCOMPARE(v.type, QShaderDescription::Vec3); + QCOMPARE(v.perPatch, false); + break; + case 1: + QCOMPARE(v.name, QByteArrayLiteral("stuff")); + QCOMPARE(v.type, QShaderDescription::Vec3); + QCOMPARE(v.perPatch, true); + break; + case 2: + QCOMPARE(v.name, QByteArrayLiteral("more_stuff")); + QCOMPARE(v.type, QShaderDescription::Float); + QCOMPARE(v.perPatch, true); + break; + default: + QFAIL(qPrintable(QStringLiteral("Bad location: %1").arg(v.location))); + break; + } + } + + QVERIFY(!tesc.shader(QShaderKey(QShader::MslShader, QShaderVersion(12))).shader().isEmpty()); + QCOMPARE(tesc.nativeShaderInfo(QShaderKey(QShader::SpirvShader, QShaderVersion(100))).extraBufferBindings.count(), 0); + QCOMPARE(tesc.nativeShaderInfo(QShaderKey(QShader::MslShader, QShaderVersion(12))).extraBufferBindings.count(), 5); + + QShader tese = getShader(QLatin1String(":/data/metal_enabled_tessellation_v7.tese.qsb")); + QVERIFY(tese.isValid()); + QCOMPARE(QShaderPrivate::get(&tese)->qsbVersion, 7); + QCOMPARE(tese.availableShaders().count(), 5); + QCOMPARE(tese.description().tessellationMode(), QShaderDescription::TrianglesTessellationMode); + QCOMPARE(tese.description().tessellationWindingOrder(), QShaderDescription::CcwTessellationWindingOrder); + QCOMPARE(tese.description().tessellationPartitioning(), QShaderDescription::FractionalOddTessellationPartitioning); + + QCOMPARE(tese.description().inputBuiltinVariables()[0].type, QShaderDescription::PositionBuiltin); + QCOMPARE(tese.description().inputBuiltinVariables()[1].type, QShaderDescription::TessLevelOuterBuiltin); + QCOMPARE(tese.description().inputBuiltinVariables()[2].type, QShaderDescription::TessLevelInnerBuiltin); + QCOMPARE(tese.description().inputBuiltinVariables()[3].type, QShaderDescription::TessCoordBuiltin); + + QCOMPARE(tese.nativeResourceBindingMap(QShaderKey(QShader::MslShader, QShaderVersion(12))).count(), 1); + QCOMPARE(tese.nativeResourceBindingMap(QShaderKey(QShader::MslShader, QShaderVersion(12))).value(0), qMakePair(0, -1)); + + QShader frag = getShader(QLatin1String(":/data/metal_enabled_tessellation_v7.frag.qsb")); + QVERIFY(frag.isValid()); + QCOMPARE(QShaderPrivate::get(&frag)->qsbVersion, 7); +} + #include QTEST_MAIN(tst_QShader) diff --git a/tests/manual/rhi/tessellation/buildshaders.bat b/tests/manual/rhi/tessellation/buildshaders.bat index c9afe1b178..bc992dc28c 100644 --- a/tests/manual/rhi/tessellation/buildshaders.bat +++ b/tests/manual/rhi/tessellation/buildshaders.bat @@ -1,6 +1,6 @@ -qsb --glsl 320es,410 --hlsl 50 test.vert -o test.vert.qsb -qsb --glsl 320es,410 test.tesc -o test.tesc.qsb +qsb --glsl 320es,410 --hlsl 50 --msl 12 --msltess test.vert -o test.vert.qsb +qsb --glsl 320es,410 --msl 12 --tess-mode triangles test.tesc -o test.tesc.qsb qsb -r hlsl,50,test_hull.hlsl test.tesc.qsb -qsb --glsl 320es,410 test.tese -o test.tese.qsb +qsb --glsl 320es,410 --msl 12 --tess-vertex-count 3 test.tese -o test.tese.qsb qsb -r hlsl,50,test_domain.hlsl test.tese.qsb -qsb --glsl 320es,410 --hlsl 50 test.frag -o test.frag.qsb +qsb --glsl 320es,410 --hlsl 50 --msl 12 test.frag -o test.frag.qsb diff --git a/tests/manual/rhi/tessellation/tessellation.cpp b/tests/manual/rhi/tessellation/tessellation.cpp index 56e9503afc..c746e6f83b 100644 --- a/tests/manual/rhi/tessellation/tessellation.cpp +++ b/tests/manual/rhi/tessellation/tessellation.cpp @@ -9,9 +9,13 @@ static const float tri[] = { 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, }; +static const bool INDEXED = false; +static const quint32 indices[] = { 0, 1, 2 }; + struct { QVector releasePool; QRhiBuffer *vbuf = nullptr; + QRhiBuffer *ibuf = nullptr; QRhiBuffer *ubuf = nullptr; QRhiShaderResourceBindings *srb = nullptr; QRhiGraphicsPipeline *ps = nullptr; @@ -29,6 +33,12 @@ void Window::customInit() d.vbuf->create(); d.releasePool << d.vbuf; + if (INDEXED) { + d.ibuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(indices)); + d.ibuf->create(); + d.releasePool << d.ibuf; + } + d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4 + 4); d.ubuf->create(); d.releasePool << d.ubuf; @@ -71,8 +81,12 @@ void Window::customInit() d.initialUpdates = m_r->nextResourceUpdateBatch(); d.initialUpdates->uploadStaticBuffer(d.vbuf, tri); + const float amplitude = 0.5f; d.initialUpdates->updateDynamicBuffer(d.ubuf, 68, 4, &litude); + + if (INDEXED) + d.initialUpdates->uploadStaticBuffer(d.ibuf, indices); } void Window::customRelease() @@ -96,14 +110,20 @@ void Window::customRender() u->updateDynamicBuffer(d.ubuf, 0, 64, d.winProj.constData()); } u->updateDynamicBuffer(d.ubuf, 64, 4, &d.time); - d.time += 0.1f; + d.time += 0.01f; cb->beginPass(m_sc->currentFrameRenderTarget(), m_clearColor, { 1.0f, 0 }, u); cb->setGraphicsPipeline(d.ps); cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) }); cb->setShaderResources(); QRhiCommandBuffer::VertexInput vbufBinding(d.vbuf, 0); - cb->setVertexInput(0, 1, &vbufBinding); - cb->draw(3); + if (INDEXED) { + cb->setVertexInput(0, 1, &vbufBinding, d.ibuf, 0, QRhiCommandBuffer::IndexUInt32); + cb->drawIndexed(3); + } else { + cb->setVertexInput(0, 1, &vbufBinding); + cb->draw(3); + } + cb->endPass(); } diff --git a/tests/manual/rhi/tessellation/test.frag.qsb b/tests/manual/rhi/tessellation/test.frag.qsb index 4ec03e5700..4d49ede3ff 100644 Binary files a/tests/manual/rhi/tessellation/test.frag.qsb and b/tests/manual/rhi/tessellation/test.frag.qsb differ diff --git a/tests/manual/rhi/tessellation/test.tesc b/tests/manual/rhi/tessellation/test.tesc index 9cbf9c12c7..54937967fa 100644 --- a/tests/manual/rhi/tessellation/test.tesc +++ b/tests/manual/rhi/tessellation/test.tesc @@ -6,6 +6,10 @@ layout(location = 0) in vec3 inColor[]; layout(location = 0) out vec3 outColor[]; +// these serve no purpose, just exist to test per-patch outputs +layout(location = 1) patch out vec3 stuff; +layout(location = 2) patch out float more_stuff; + void main() { if (gl_InvocationID == 0) { @@ -18,4 +22,6 @@ void main() gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; outColor[gl_InvocationID] = inColor[gl_InvocationID]; + stuff = vec3(1.0); + more_stuff = 1.0; } diff --git a/tests/manual/rhi/tessellation/test.tesc.qsb b/tests/manual/rhi/tessellation/test.tesc.qsb index 57451d7c08..064e26040a 100644 Binary files a/tests/manual/rhi/tessellation/test.tesc.qsb and b/tests/manual/rhi/tessellation/test.tesc.qsb differ diff --git a/tests/manual/rhi/tessellation/test.tese b/tests/manual/rhi/tessellation/test.tese index c82344af8e..c50230f852 100644 --- a/tests/manual/rhi/tessellation/test.tese +++ b/tests/manual/rhi/tessellation/test.tese @@ -6,6 +6,10 @@ layout(location = 0) in vec3 inColor[]; layout(location = 0) out vec3 outColor; +// these serve no purpose, just exist to test per-patch outputs +layout(location = 1) patch in vec3 stuff; +layout(location = 2) patch in float more_stuff; + layout(std140, binding = 0) uniform buf { mat4 mvp; float time; @@ -14,7 +18,10 @@ layout(std140, binding = 0) uniform buf { void main() { - gl_Position = mvp * ((gl_TessCoord.x * gl_in[0].gl_Position) + (gl_TessCoord.y * gl_in[1].gl_Position) + (gl_TessCoord.z * gl_in[2].gl_Position)); - gl_Position.x += sin(time + gl_Position.y) * amplitude; - outColor = gl_TessCoord.x * inColor[0] + gl_TessCoord.y * inColor[1] + gl_TessCoord.z * inColor[2]; + vec4 pos = (gl_TessCoord.x * gl_in[0].gl_Position) + (gl_TessCoord.y * gl_in[1].gl_Position) + (gl_TessCoord.z * gl_in[2].gl_Position); + gl_Position = mvp * pos; + gl_Position.x += sin(time + pos.y) * amplitude; + outColor = gl_TessCoord.x * inColor[0] + gl_TessCoord.y * inColor[1] + gl_TessCoord.z * inColor[2] + // these are all 1.0, just here to exercise the shader generation and the runtime pipeline setup + * stuff.x * more_stuff * (gl_TessLevelOuter[0] / 4.0) * (gl_TessLevelInner[0] / 4.0); } diff --git a/tests/manual/rhi/tessellation/test.tese.qsb b/tests/manual/rhi/tessellation/test.tese.qsb index 4ca3c35e92..a6caab67c3 100644 Binary files a/tests/manual/rhi/tessellation/test.tese.qsb and b/tests/manual/rhi/tessellation/test.tese.qsb differ diff --git a/tests/manual/rhi/tessellation/test.vert.qsb b/tests/manual/rhi/tessellation/test.vert.qsb index ac261d2b41..39734b6d5d 100644 Binary files a/tests/manual/rhi/tessellation/test.vert.qsb and b/tests/manual/rhi/tessellation/test.vert.qsb differ diff --git a/tests/manual/rhi/tessellation/test_domain.hlsl b/tests/manual/rhi/tessellation/test_domain.hlsl index a3de658c0e..a9697d32cf 100644 --- a/tests/manual/rhi/tessellation/test_domain.hlsl +++ b/tests/manual/rhi/tessellation/test_domain.hlsl @@ -30,7 +30,7 @@ PixelInput main(Input input, float3 uvwCoord : SV_DomainLocation, const OutputPa float3 vertexPosition = uvwCoord.x * patch[0].position + uvwCoord.y * patch[1].position + uvwCoord.z * patch[2].position; output.position = mul(float4(vertexPosition, 1.0f), mvp); - output.position.x += sin(time + output.position.y) * amplitude; + output.position.x += sin(time + vertexPosition.y) * amplitude; output.color = uvwCoord.x * patch[0].color + uvwCoord.y * patch[1].color + uvwCoord.z * patch[2].color; -- cgit v1.2.3