RHI: Metal tessellation fixes
This patch fixes four issues with Metal tessellation. a) Shader resource binding The Metal tessellation implementation assumes identical SPIRV-Cross native Metal resource binding mapping for vertex, tessellation control, and tessellation evaluation shaders. These mappings are independently generated by SPIRV-Cross for each shader stage, and may not always be identical. This patch allows for different resource bindings for each of the vert/tesc/tese stages. b) Tessellation evaluation vertex descriptors The Metal tessellation evaluation render pipeline vertex descriptor generation code contains a bug where attribute offsets and built in variable locations could be calculated incorrectly if the tessellation control shader output variables are not provided in ascending location order. This patch fixes this by sorting the variables by location before processing. c) Render pass descriptor Metal tessellation draw ends the current render pass encoder to perform tessellation compute tasks on a compute pass encoder. When the compute pass is completed, a new render pass encoder is created to continue rendering. A bug exists where the new render pass encoder uses a render pass descriptor that clears the color, depth and stencil attachements. This patch fixes this bug by changing the render pass descriptor color, depth and stencil attachment load actions to MTLLoadActionLoad when appropriate. d) drawIndexed A bug exists where when drawIndexed is called, the Metal tessellation vertex as compute stage input descriptor buffer layout step function gets set to MTLStepFunctionThreadPositionInGridX rather than the indexed version MTLStepFunctionThreadPositionInGridXIndexed. This patch fixes this by selecting the appropriate step function. Change-Id: I122c67394719ad6b4801cd7643043839fd186bf2 Reviewed-by: Laszlo Agocs <laszlo.agocs@qt.io> (cherry picked from commit 0d7401d51bedecb1b84b78aedb50839928a0cc7b) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
parent
4897ea9c6b
commit
086de2f4b4
@ -279,7 +279,7 @@ struct QMetalShaderResourceBindingsData {
|
|||||||
QRhiBatchedBindings<id<MTLTexture> > textureBatches;
|
QRhiBatchedBindings<id<MTLTexture> > textureBatches;
|
||||||
QRhiBatchedBindings<id<MTLSamplerState> > samplerBatches;
|
QRhiBatchedBindings<id<MTLSamplerState> > samplerBatches;
|
||||||
} res[QRhiMetal::SUPPORTED_STAGES];
|
} res[QRhiMetal::SUPPORTED_STAGES];
|
||||||
enum { VERTEX = 0, FRAGMENT = 1, COMPUTE = 2 };
|
enum { VERTEX = 0, FRAGMENT = 1, COMPUTE = 2, TESSCTRL = 3, TESSEVAL = 4 };
|
||||||
};
|
};
|
||||||
|
|
||||||
struct QMetalCommandBufferData
|
struct QMetalCommandBufferData
|
||||||
@ -380,7 +380,8 @@ struct QMetalGraphicsPipelineData
|
|||||||
QVector<QMetalBuffer *> deviceLocalWorkBuffers;
|
QVector<QMetalBuffer *> deviceLocalWorkBuffers;
|
||||||
QVector<QMetalBuffer *> hostVisibleWorkBuffers;
|
QVector<QMetalBuffer *> hostVisibleWorkBuffers;
|
||||||
} tess;
|
} tess;
|
||||||
template<typename T> void setupVertexOrStageInputDescriptor(T *desc);
|
void setupVertexInputDescriptor(MTLVertexDescriptor *desc);
|
||||||
|
void setupStageInputDescriptor(MTLStageInputOutputDescriptor *desc);
|
||||||
};
|
};
|
||||||
|
|
||||||
struct QMetalComputePipelineData
|
struct QMetalComputePipelineData
|
||||||
@ -1071,6 +1072,10 @@ static inline void bindStageBuffers(QMetalCommandBuffer *cbD,
|
|||||||
offsets: offsetBatch.resources.constData()
|
offsets: offsetBatch.resources.constData()
|
||||||
withRange: NSMakeRange(bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))];
|
withRange: NSMakeRange(bufferBatch.startBinding, NSUInteger(bufferBatch.resources.count()))];
|
||||||
break;
|
break;
|
||||||
|
case QMetalShaderResourceBindingsData::TESSCTRL:
|
||||||
|
case QMetalShaderResourceBindingsData::TESSEVAL:
|
||||||
|
// do nothing. These are used later for tessellation
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Q_UNREACHABLE();
|
Q_UNREACHABLE();
|
||||||
break;
|
break;
|
||||||
@ -1094,6 +1099,10 @@ static inline void bindStageTextures(QMetalCommandBuffer *cbD,
|
|||||||
[cbD->d->currentComputePassEncoder setTextures: textureBatch.resources.constData()
|
[cbD->d->currentComputePassEncoder setTextures: textureBatch.resources.constData()
|
||||||
withRange: NSMakeRange(textureBatch.startBinding, NSUInteger(textureBatch.resources.count()))];
|
withRange: NSMakeRange(textureBatch.startBinding, NSUInteger(textureBatch.resources.count()))];
|
||||||
break;
|
break;
|
||||||
|
case QMetalShaderResourceBindingsData::TESSCTRL:
|
||||||
|
case QMetalShaderResourceBindingsData::TESSEVAL:
|
||||||
|
// do nothing. These are used later for tessellation
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Q_UNREACHABLE();
|
Q_UNREACHABLE();
|
||||||
break;
|
break;
|
||||||
@ -1117,6 +1126,10 @@ static inline void bindStageSamplers(QMetalCommandBuffer *cbD,
|
|||||||
[cbD->d->currentComputePassEncoder setSamplerStates: samplerBatch.resources.constData()
|
[cbD->d->currentComputePassEncoder setSamplerStates: samplerBatch.resources.constData()
|
||||||
withRange: NSMakeRange(samplerBatch.startBinding, NSUInteger(samplerBatch.resources.count()))];
|
withRange: NSMakeRange(samplerBatch.startBinding, NSUInteger(samplerBatch.resources.count()))];
|
||||||
break;
|
break;
|
||||||
|
case QMetalShaderResourceBindingsData::TESSCTRL:
|
||||||
|
case QMetalShaderResourceBindingsData::TESSEVAL:
|
||||||
|
// do nothing. These are used later for tessellation
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Q_UNREACHABLE();
|
Q_UNREACHABLE();
|
||||||
break;
|
break;
|
||||||
@ -1150,17 +1163,22 @@ static inline void rebindShaderResources(QMetalCommandBuffer *cbD, int resourceS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resources marked for the tess.control and/or eval. stages are treated as if
|
static inline QRhiShaderResourceBinding::StageFlag toRhiSrbStage(int stage)
|
||||||
// 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
|
switch (stage) {
|
||||||
| QRhiShaderResourceBinding::TessellationControlStage
|
case QMetalShaderResourceBindingsData::VERTEX:
|
||||||
| QRhiShaderResourceBinding::TessellationEvaluationStage);
|
return QRhiShaderResourceBinding::StageFlag::VertexStage;
|
||||||
|
case QMetalShaderResourceBindingsData::TESSCTRL:
|
||||||
|
return QRhiShaderResourceBinding::StageFlag::TessellationControlStage;
|
||||||
|
case QMetalShaderResourceBindingsData::TESSEVAL:
|
||||||
|
return QRhiShaderResourceBinding::StageFlag::TessellationEvaluationStage;
|
||||||
|
case QMetalShaderResourceBindingsData::FRAGMENT:
|
||||||
|
return QRhiShaderResourceBinding::StageFlag::FragmentStage;
|
||||||
|
case QMetalShaderResourceBindingsData::COMPUTE:
|
||||||
|
return QRhiShaderResourceBinding::StageFlag::ComputeStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_UNREACHABLE_RETURN(QRhiShaderResourceBinding::StageFlag::VertexStage);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD,
|
void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD,
|
||||||
@ -1187,20 +1205,13 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isVertexishResource(b->stage)) {
|
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Buffer);
|
for (int stage = 0; stage < SUPPORTED_STAGES; ++stage) {
|
||||||
if (nativeBinding >= 0)
|
if (b->stage.testFlag(toRhiSrbStage(stage))) {
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::VERTEX].buffers.append({ nativeBinding, mtlbuf, offset });
|
const int nativeBinding = mapBinding(b->binding, stage, nativeResourceBindingMaps, BindingType::Buffer);
|
||||||
}
|
if (nativeBinding >= 0)
|
||||||
if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) {
|
bindingData.res[stage].buffers.append({ nativeBinding, mtlbuf, offset });
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::FRAGMENT, nativeResourceBindingMaps, BindingType::Buffer);
|
}
|
||||||
if (nativeBinding >= 0)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::FRAGMENT].buffers.append({ nativeBinding, mtlbuf, offset });
|
|
||||||
}
|
|
||||||
if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) {
|
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::COMPUTE, nativeResourceBindingMaps, BindingType::Buffer);
|
|
||||||
if (nativeBinding >= 0)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::COMPUTE].buffers.append({ nativeBinding, mtlbuf, offset });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -1212,36 +1223,21 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD
|
|||||||
for (int elem = 0; elem < data->count; ++elem) {
|
for (int elem = 0; elem < data->count; ++elem) {
|
||||||
QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.texSamplers[elem].tex);
|
QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.texSamplers[elem].tex);
|
||||||
QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.texSamplers[elem].sampler);
|
QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.texSamplers[elem].sampler);
|
||||||
if (isVertexishResource(b->stage)) {
|
|
||||||
// Must handle all three cases (combined, separate, separate):
|
for (int stage = 0; stage < SUPPORTED_STAGES; ++stage) {
|
||||||
// first = texture binding, second = sampler binding
|
if (b->stage.testFlag(toRhiSrbStage(stage))) {
|
||||||
// first = texture binding
|
// Must handle all three cases (combined, separate, separate):
|
||||||
// first = sampler binding (i.e. BindingType::Texture...)
|
// first = texture binding, second = sampler binding
|
||||||
const int textureBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Texture);
|
// first = texture binding
|
||||||
const int samplerBinding = texD && samplerD ? mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Sampler)
|
// first = sampler binding (i.e. BindingType::Texture...)
|
||||||
: (samplerD ? mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Texture) : -1);
|
const int textureBinding = mapBinding(b->binding, stage, nativeResourceBindingMaps, BindingType::Texture);
|
||||||
if (textureBinding >= 0 && texD)
|
const int samplerBinding = texD && samplerD ? mapBinding(b->binding, stage, nativeResourceBindingMaps, BindingType::Sampler)
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::VERTEX].textures.append({ textureBinding + elem, texD->d->tex });
|
: (samplerD ? mapBinding(b->binding, stage, nativeResourceBindingMaps, BindingType::Texture) : -1);
|
||||||
if (samplerBinding >= 0)
|
if (textureBinding >= 0 && texD)
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::VERTEX].samplers.append({ samplerBinding + elem, samplerD->d->samplerState });
|
bindingData.res[stage].textures.append({ textureBinding + elem, texD->d->tex });
|
||||||
}
|
if (samplerBinding >= 0)
|
||||||
if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) {
|
bindingData.res[stage].samplers.append({ samplerBinding + elem, samplerD->d->samplerState });
|
||||||
const int textureBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::FRAGMENT, nativeResourceBindingMaps, BindingType::Texture);
|
}
|
||||||
const int samplerBinding = texD && samplerD ? mapBinding(b->binding, QMetalShaderResourceBindingsData::FRAGMENT, nativeResourceBindingMaps, BindingType::Sampler)
|
|
||||||
: (samplerD ? mapBinding(b->binding, QMetalShaderResourceBindingsData::FRAGMENT, nativeResourceBindingMaps, BindingType::Texture) : -1);
|
|
||||||
if (textureBinding >= 0 && texD)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::FRAGMENT].textures.append({ textureBinding + elem, texD->d->tex });
|
|
||||||
if (samplerBinding >= 0)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::FRAGMENT].samplers.append({ samplerBinding + elem, samplerD->d->samplerState });
|
|
||||||
}
|
|
||||||
if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) {
|
|
||||||
const int textureBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::COMPUTE, nativeResourceBindingMaps, BindingType::Texture);
|
|
||||||
const int samplerBinding = texD && samplerD ? mapBinding(b->binding, QMetalShaderResourceBindingsData::COMPUTE, nativeResourceBindingMaps, BindingType::Sampler)
|
|
||||||
: (samplerD ? mapBinding(b->binding, QMetalShaderResourceBindingsData::COMPUTE, nativeResourceBindingMaps, BindingType::Texture) : -1);
|
|
||||||
if (textureBinding >= 0 && texD)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::COMPUTE].textures.append({ textureBinding + elem, texD->d->tex });
|
|
||||||
if (samplerBinding >= 0)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::COMPUTE].samplers.append({ samplerBinding + elem, samplerD->d->samplerState });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1252,20 +1248,13 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD
|
|||||||
{
|
{
|
||||||
QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.simage.tex);
|
QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.simage.tex);
|
||||||
id<MTLTexture> t = texD->d->viewForLevel(b->u.simage.level);
|
id<MTLTexture> t = texD->d->viewForLevel(b->u.simage.level);
|
||||||
if (isVertexishResource(b->stage)) {
|
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Texture);
|
for (int stage = 0; stage < SUPPORTED_STAGES; ++stage) {
|
||||||
if (nativeBinding >= 0)
|
if (b->stage.testFlag(toRhiSrbStage(stage))) {
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::VERTEX].textures.append({ nativeBinding, t });
|
const int nativeBinding = mapBinding(b->binding, stage, nativeResourceBindingMaps, BindingType::Texture);
|
||||||
}
|
if (nativeBinding >= 0)
|
||||||
if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) {
|
bindingData.res[stage].textures.append({ nativeBinding, t });
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::FRAGMENT, nativeResourceBindingMaps, BindingType::Texture);
|
}
|
||||||
if (nativeBinding >= 0)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::FRAGMENT].textures.append({ nativeBinding, t });
|
|
||||||
}
|
|
||||||
if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) {
|
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::COMPUTE, nativeResourceBindingMaps, BindingType::Texture);
|
|
||||||
if (nativeBinding >= 0)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::COMPUTE].textures.append({ nativeBinding, t });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -1276,20 +1265,12 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD
|
|||||||
QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.sbuf.buf);
|
QMetalBuffer *bufD = QRHI_RES(QMetalBuffer, b->u.sbuf.buf);
|
||||||
id<MTLBuffer> mtlbuf = bufD->d->buf[0];
|
id<MTLBuffer> mtlbuf = bufD->d->buf[0];
|
||||||
quint32 offset = b->u.sbuf.offset;
|
quint32 offset = b->u.sbuf.offset;
|
||||||
if (isVertexishResource(b->stage)) {
|
for (int stage = 0; stage < SUPPORTED_STAGES; ++stage) {
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::VERTEX, nativeResourceBindingMaps, BindingType::Buffer);
|
if (b->stage.testFlag(toRhiSrbStage(stage))) {
|
||||||
if (nativeBinding >= 0)
|
const int nativeBinding = mapBinding(b->binding, stage, nativeResourceBindingMaps, BindingType::Buffer);
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::VERTEX].buffers.append({ nativeBinding, mtlbuf, offset });
|
if (nativeBinding >= 0)
|
||||||
}
|
bindingData.res[stage].buffers.append({ nativeBinding, mtlbuf, offset });
|
||||||
if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) {
|
}
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::FRAGMENT, nativeResourceBindingMaps, BindingType::Buffer);
|
|
||||||
if (nativeBinding >= 0)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::FRAGMENT].buffers.append({ nativeBinding, mtlbuf, offset });
|
|
||||||
}
|
|
||||||
if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) {
|
|
||||||
const int nativeBinding = mapBinding(b->binding, QMetalShaderResourceBindingsData::COMPUTE, nativeResourceBindingMaps, BindingType::Buffer);
|
|
||||||
if (nativeBinding >= 0)
|
|
||||||
bindingData.res[QMetalShaderResourceBindingsData::COMPUTE].buffers.append({ nativeBinding, mtlbuf, offset });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -1300,9 +1281,10 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int stage = 0; stage < SUPPORTED_STAGES; ++stage) {
|
for (int stage = 0; stage < SUPPORTED_STAGES; ++stage) {
|
||||||
if (cbD->recordingPass != QMetalCommandBuffer::RenderPass && (stage == QMetalShaderResourceBindingsData::VERTEX || stage == QMetalShaderResourceBindingsData::FRAGMENT))
|
if (cbD->recordingPass != QMetalCommandBuffer::RenderPass && (stage == QMetalShaderResourceBindingsData::VERTEX || stage == QMetalShaderResourceBindingsData::FRAGMENT
|
||||||
|
|| stage == QMetalShaderResourceBindingsData::TESSCTRL || stage == QMetalShaderResourceBindingsData::TESSEVAL))
|
||||||
continue;
|
continue;
|
||||||
if (cbD->recordingPass != QMetalCommandBuffer::ComputePass && stage == QMetalShaderResourceBindingsData::COMPUTE)
|
if (cbD->recordingPass != QMetalCommandBuffer::ComputePass && (stage == QMetalShaderResourceBindingsData::COMPUTE))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// QRhiBatchedBindings works with the native bindings and expects
|
// QRhiBatchedBindings works with the native bindings and expects
|
||||||
@ -1565,16 +1547,26 @@ void QRhiMetal::setShaderResources(QRhiCommandBuffer *cb, QRhiShaderResourceBind
|
|||||||
|
|
||||||
// dynamic uniform buffer offsets always trigger a rebind
|
// dynamic uniform buffer offsets always trigger a rebind
|
||||||
if (hasDynamicOffsetInSrb || resNeedsRebind || srbChanged || srbRebuilt) {
|
if (hasDynamicOffsetInSrb || resNeedsRebind || srbChanged || srbRebuilt) {
|
||||||
const QShader::NativeResourceBindingMap *resBindMaps[SUPPORTED_STAGES] = { nullptr, nullptr, nullptr };
|
const QShader::NativeResourceBindingMap *resBindMaps[SUPPORTED_STAGES] = { nullptr, nullptr, nullptr, nullptr, nullptr };
|
||||||
if (gfxPsD) {
|
if (gfxPsD) {
|
||||||
cbD->currentGraphicsSrb = srbD;
|
cbD->currentGraphicsSrb = srbD;
|
||||||
cbD->currentComputeSrb = nullptr;
|
cbD->currentComputeSrb = nullptr;
|
||||||
resBindMaps[0] = &gfxPsD->d->vs.nativeResourceBindingMap;
|
if (gfxPsD->d->tess.enabled) {
|
||||||
resBindMaps[1] = &gfxPsD->d->fs.nativeResourceBindingMap;
|
// If tessellating, we don't know which compVs shader to use until the draw call is
|
||||||
|
// made. They should all have the same native resource binding map, so pick one.
|
||||||
|
Q_ASSERT(gfxPsD->d->tess.compVs[0].nativeResourceBindingMap == gfxPsD->d->tess.compVs[1].nativeResourceBindingMap);
|
||||||
|
Q_ASSERT(gfxPsD->d->tess.compVs[0].nativeResourceBindingMap == gfxPsD->d->tess.compVs[2].nativeResourceBindingMap);
|
||||||
|
resBindMaps[QMetalShaderResourceBindingsData::VERTEX] = &gfxPsD->d->tess.compVs[0].nativeResourceBindingMap;
|
||||||
|
resBindMaps[QMetalShaderResourceBindingsData::TESSCTRL] = &gfxPsD->d->tess.compTesc.nativeResourceBindingMap;
|
||||||
|
resBindMaps[QMetalShaderResourceBindingsData::TESSEVAL] = &gfxPsD->d->tess.vertTese.nativeResourceBindingMap;
|
||||||
|
} else {
|
||||||
|
resBindMaps[QMetalShaderResourceBindingsData::VERTEX] = &gfxPsD->d->vs.nativeResourceBindingMap;
|
||||||
|
}
|
||||||
|
resBindMaps[QMetalShaderResourceBindingsData::FRAGMENT] = &gfxPsD->d->fs.nativeResourceBindingMap;
|
||||||
} else {
|
} else {
|
||||||
cbD->currentGraphicsSrb = nullptr;
|
cbD->currentGraphicsSrb = nullptr;
|
||||||
cbD->currentComputeSrb = srbD;
|
cbD->currentComputeSrb = srbD;
|
||||||
resBindMaps[2] = &compPsD->d->cs.nativeResourceBindingMap;
|
resBindMaps[QMetalShaderResourceBindingsData::COMPUTE] = &compPsD->d->cs.nativeResourceBindingMap;
|
||||||
}
|
}
|
||||||
cbD->currentSrbGeneration = srbD->generation;
|
cbD->currentSrbGeneration = srbD->generation;
|
||||||
cbD->currentResSlot = resSlot;
|
cbD->currentResSlot = resSlot;
|
||||||
@ -1734,8 +1726,52 @@ static void endTessellationComputeEncoding(QMetalCommandBuffer *cbD)
|
|||||||
cbD->d->tessellationComputeEncoder = nil;
|
cbD->d->tessellationComputeEncoder = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QMetalRenderTargetData * rtD = nullptr;
|
||||||
|
|
||||||
|
switch (cbD->currentTarget->resourceType()) {
|
||||||
|
case QRhiResource::SwapChainRenderTarget:
|
||||||
|
rtD = QRHI_RES(QMetalSwapChainRenderTarget, cbD->currentTarget)->d;
|
||||||
|
break;
|
||||||
|
case QRhiResource::TextureRenderTarget:
|
||||||
|
rtD = QRHI_RES(QMetalTextureRenderTarget, cbD->currentTarget)->d;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(rtD);
|
||||||
|
|
||||||
|
QVarLengthArray<MTLLoadAction, 4> oldColorLoad;
|
||||||
|
for (uint i = 0; i < uint(rtD->colorAttCount); ++i) {
|
||||||
|
oldColorLoad.append(cbD->d->currentPassRpDesc.colorAttachments[i].loadAction);
|
||||||
|
if (cbD->d->currentPassRpDesc.colorAttachments[i].storeAction != MTLStoreActionDontCare)
|
||||||
|
cbD->d->currentPassRpDesc.colorAttachments[i].loadAction = MTLLoadActionLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
MTLLoadAction oldDepthLoad;
|
||||||
|
MTLLoadAction oldStencilLoad;
|
||||||
|
if (rtD->dsAttCount) {
|
||||||
|
oldDepthLoad = cbD->d->currentPassRpDesc.depthAttachment.loadAction;
|
||||||
|
if (cbD->d->currentPassRpDesc.depthAttachment.storeAction != MTLStoreActionDontCare)
|
||||||
|
cbD->d->currentPassRpDesc.depthAttachment.loadAction = MTLLoadActionLoad;
|
||||||
|
|
||||||
|
oldStencilLoad = cbD->d->currentPassRpDesc.stencilAttachment.loadAction;
|
||||||
|
if (cbD->d->currentPassRpDesc.stencilAttachment.storeAction != MTLStoreActionDontCare)
|
||||||
|
cbD->d->currentPassRpDesc.stencilAttachment.loadAction = MTLLoadActionLoad;
|
||||||
|
}
|
||||||
|
|
||||||
cbD->d->currentRenderPassEncoder = [cbD->d->cb renderCommandEncoderWithDescriptor: cbD->d->currentPassRpDesc];
|
cbD->d->currentRenderPassEncoder = [cbD->d->cb renderCommandEncoderWithDescriptor: cbD->d->currentPassRpDesc];
|
||||||
cbD->resetPerPassCachedState();
|
cbD->resetPerPassCachedState();
|
||||||
|
|
||||||
|
for (uint i = 0; i < uint(rtD->colorAttCount); ++i) {
|
||||||
|
cbD->d->currentPassRpDesc.colorAttachments[i].loadAction = oldColorLoad[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rtD->dsAttCount) {
|
||||||
|
cbD->d->currentPassRpDesc.depthAttachment.loadAction = oldDepthLoad;
|
||||||
|
cbD->d->currentPassRpDesc.stencilAttachment.loadAction = oldStencilLoad;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QRhiMetal::tessellatedDraw(const TessDrawArgs &args)
|
void QRhiMetal::tessellatedDraw(const TessDrawArgs &args)
|
||||||
@ -1772,7 +1808,7 @@ void QRhiMetal::tessellatedDraw(const TessDrawArgs &args)
|
|||||||
|
|
||||||
// Make uniform buffers, textures, and samplers (meant for the
|
// Make uniform buffers, textures, and samplers (meant for the
|
||||||
// vertex stage from the client's point of view) visible in the
|
// vertex stage from the client's point of view) visible in the
|
||||||
// compute shaders (both vertex and tess.control).
|
// "vertex as compute" shader
|
||||||
cbD->d->currentComputePassEncoder = computeEncoder;
|
cbD->d->currentComputePassEncoder = computeEncoder;
|
||||||
rebindShaderResources(cbD, QMetalShaderResourceBindingsData::VERTEX, QMetalShaderResourceBindingsData::COMPUTE);
|
rebindShaderResources(cbD, QMetalShaderResourceBindingsData::VERTEX, QMetalShaderResourceBindingsData::COMPUTE);
|
||||||
cbD->d->currentComputePassEncoder = nil;
|
cbD->d->currentComputePassEncoder = nil;
|
||||||
@ -1818,9 +1854,9 @@ void QRhiMetal::tessellatedDraw(const TessDrawArgs &args)
|
|||||||
id<MTLComputePipelineState> computePipelineState = tess.tescCompPipeline(this);
|
id<MTLComputePipelineState> computePipelineState = tess.tescCompPipeline(this);
|
||||||
[computeEncoder setComputePipelineState: computePipelineState];
|
[computeEncoder setComputePipelineState: computePipelineState];
|
||||||
|
|
||||||
// Shader resources are set already in step 1. (because srb stage
|
cbD->d->currentComputePassEncoder = computeEncoder;
|
||||||
// flags for tesc and tese visibility are treated as if they were
|
rebindShaderResources(cbD, QMetalShaderResourceBindingsData::TESSCTRL, QMetalShaderResourceBindingsData::COMPUTE);
|
||||||
// specified as vertex visibility -> QMSRBD::VERTEX includes those too)
|
cbD->d->currentComputePassEncoder = nil;
|
||||||
|
|
||||||
const QMap<int, int> &ebb(tess.compTesc.nativeShaderInfo.extraBufferBindings);
|
const QMap<int, int> &ebb(tess.compTesc.nativeShaderInfo.extraBufferBindings);
|
||||||
const int outputBufferBinding = ebb.value(QShaderPrivate::MslTessVertTescOutputBufferBinding, -1);
|
const int outputBufferBinding = ebb.value(QShaderPrivate::MslTessVertTescOutputBufferBinding, -1);
|
||||||
@ -1895,7 +1931,7 @@ void QRhiMetal::tessellatedDraw(const TessDrawArgs &args)
|
|||||||
graphicsPipeline->makeActiveForCurrentRenderPassEncoder(cbD);
|
graphicsPipeline->makeActiveForCurrentRenderPassEncoder(cbD);
|
||||||
id<MTLRenderCommandEncoder> renderEncoder = cbD->d->currentRenderPassEncoder;
|
id<MTLRenderCommandEncoder> renderEncoder = cbD->d->currentRenderPassEncoder;
|
||||||
|
|
||||||
rebindShaderResources(cbD, QMetalShaderResourceBindingsData::VERTEX, QMetalShaderResourceBindingsData::VERTEX, &resourceBindings);
|
rebindShaderResources(cbD, QMetalShaderResourceBindingsData::TESSEVAL, QMetalShaderResourceBindingsData::VERTEX, &resourceBindings);
|
||||||
rebindShaderResources(cbD, QMetalShaderResourceBindingsData::FRAGMENT, QMetalShaderResourceBindingsData::FRAGMENT, &resourceBindings);
|
rebindShaderResources(cbD, QMetalShaderResourceBindingsData::FRAGMENT, QMetalShaderResourceBindingsData::FRAGMENT, &resourceBindings);
|
||||||
|
|
||||||
const QMap<int, int> &ebb(tess.compTesc.nativeShaderInfo.extraBufferBindings);
|
const QMap<int, int> &ebb(tess.compTesc.nativeShaderInfo.extraBufferBindings);
|
||||||
@ -4460,10 +4496,10 @@ void QMetalGraphicsPipeline::mapStates()
|
|||||||
d->slopeScaledDepthBias = m_slopeScaledDepthBias;
|
d->slopeScaledDepthBias = m_slopeScaledDepthBias;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
void QMetalGraphicsPipelineData::setupVertexInputDescriptor(MTLVertexDescriptor *desc)
|
||||||
void QMetalGraphicsPipelineData::setupVertexOrStageInputDescriptor(T *desc)
|
|
||||||
{
|
{
|
||||||
// same binding space for vertex and constant buffers - work it around
|
// same binding space for vertex and constant buffers - work it around
|
||||||
|
// should be in native resource binding not SPIR-V, but this will work anyway
|
||||||
const int firstVertexBinding = QRHI_RES(QMetalShaderResourceBindings, q->shaderResourceBindings())->maxBinding + 1;
|
const int firstVertexBinding = QRHI_RES(QMetalShaderResourceBindings, q->shaderResourceBindings())->maxBinding + 1;
|
||||||
|
|
||||||
QRhiVertexInputLayout vertexInputLayout = q->vertexInputLayout();
|
QRhiVertexInputLayout vertexInputLayout = q->vertexInputLayout();
|
||||||
@ -4471,7 +4507,6 @@ void QMetalGraphicsPipelineData::setupVertexOrStageInputDescriptor(T *desc)
|
|||||||
it != itEnd; ++it)
|
it != itEnd; ++it)
|
||||||
{
|
{
|
||||||
const uint loc = uint(it->location());
|
const uint loc = uint(it->location());
|
||||||
// either MTLVertexFormat or MTLAttributeFormat, the values are the same
|
|
||||||
desc.attributes[loc].format = decltype(desc.attributes[loc].format)(toMetalAttributeFormat(it->format()));
|
desc.attributes[loc].format = decltype(desc.attributes[loc].format)(toMetalAttributeFormat(it->format()));
|
||||||
desc.attributes[loc].offset = NSUInteger(it->offset());
|
desc.attributes[loc].offset = NSUInteger(it->offset());
|
||||||
desc.attributes[loc].bufferIndex = NSUInteger(firstVertexBinding + it->binding());
|
desc.attributes[loc].bufferIndex = NSUInteger(firstVertexBinding + it->binding());
|
||||||
@ -4481,15 +4516,42 @@ void QMetalGraphicsPipelineData::setupVertexOrStageInputDescriptor(T *desc)
|
|||||||
it != itEnd; ++it, ++bindingIndex)
|
it != itEnd; ++it, ++bindingIndex)
|
||||||
{
|
{
|
||||||
const uint layoutIdx = uint(firstVertexBinding + bindingIndex);
|
const uint layoutIdx = uint(firstVertexBinding + bindingIndex);
|
||||||
using StepT = decltype(desc.layouts[layoutIdx].stepFunction);
|
desc.layouts[layoutIdx].stepFunction =
|
||||||
if (std::is_same_v<StepT, MTLStepFunction>) {
|
|
||||||
desc.layouts[layoutIdx].stepFunction = StepT(
|
|
||||||
it->classification() == QRhiVertexInputBinding::PerInstance
|
it->classification() == QRhiVertexInputBinding::PerInstance
|
||||||
? MTLStepFunctionThreadPositionInGridY : MTLStepFunctionThreadPositionInGridX);
|
? MTLVertexStepFunctionPerInstance : MTLVertexStepFunctionPerVertex;
|
||||||
|
desc.layouts[layoutIdx].stepRate = NSUInteger(it->instanceStepRate());
|
||||||
|
desc.layouts[layoutIdx].stride = it->stride();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void QMetalGraphicsPipelineData::setupStageInputDescriptor(MTLStageInputOutputDescriptor *desc)
|
||||||
|
{
|
||||||
|
// same binding space for vertex and constant buffers - work it around
|
||||||
|
// should be in native resource binding not SPIR-V, but this will work anyway
|
||||||
|
const int firstVertexBinding = QRHI_RES(QMetalShaderResourceBindings, q->shaderResourceBindings())->maxBinding + 1;
|
||||||
|
|
||||||
|
QRhiVertexInputLayout vertexInputLayout = q->vertexInputLayout();
|
||||||
|
for (auto it = vertexInputLayout.cbeginAttributes(), itEnd = vertexInputLayout.cendAttributes();
|
||||||
|
it != itEnd; ++it)
|
||||||
|
{
|
||||||
|
const uint loc = uint(it->location());
|
||||||
|
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 = vertexInputLayout.cbeginBindings(), itEnd = vertexInputLayout.cendBindings();
|
||||||
|
it != itEnd; ++it, ++bindingIndex)
|
||||||
|
{
|
||||||
|
const uint layoutIdx = uint(firstVertexBinding + bindingIndex);
|
||||||
|
if (desc.indexBufferIndex) {
|
||||||
|
desc.layouts[layoutIdx].stepFunction =
|
||||||
|
it->classification() == QRhiVertexInputBinding::PerInstance
|
||||||
|
? MTLStepFunctionThreadPositionInGridY : MTLStepFunctionThreadPositionInGridXIndexed;
|
||||||
} else {
|
} else {
|
||||||
desc.layouts[layoutIdx].stepFunction = StepT(
|
desc.layouts[layoutIdx].stepFunction =
|
||||||
it->classification() == QRhiVertexInputBinding::PerInstance
|
it->classification() == QRhiVertexInputBinding::PerInstance
|
||||||
? MTLVertexStepFunctionPerInstance : MTLVertexStepFunctionPerVertex);
|
? MTLStepFunctionThreadPositionInGridY : MTLStepFunctionThreadPositionInGridX;
|
||||||
}
|
}
|
||||||
desc.layouts[layoutIdx].stepRate = NSUInteger(it->instanceStepRate());
|
desc.layouts[layoutIdx].stepRate = NSUInteger(it->instanceStepRate());
|
||||||
desc.layouts[layoutIdx].stride = it->stride();
|
desc.layouts[layoutIdx].stride = it->stride();
|
||||||
@ -4547,7 +4609,7 @@ bool QMetalGraphicsPipeline::createVertexFragmentPipeline()
|
|||||||
QRHI_RES_RHI(QRhiMetal);
|
QRHI_RES_RHI(QRhiMetal);
|
||||||
|
|
||||||
MTLVertexDescriptor *vertexDesc = [MTLVertexDescriptor vertexDescriptor];
|
MTLVertexDescriptor *vertexDesc = [MTLVertexDescriptor vertexDescriptor];
|
||||||
d->setupVertexOrStageInputDescriptor(vertexDesc);
|
d->setupVertexInputDescriptor(vertexDesc);
|
||||||
|
|
||||||
MTLRenderPipelineDescriptor *rpDesc = [[MTLRenderPipelineDescriptor alloc] init];
|
MTLRenderPipelineDescriptor *rpDesc = [[MTLRenderPipelineDescriptor alloc] init];
|
||||||
rpDesc.vertexDescriptor = vertexDesc;
|
rpDesc.vertexDescriptor = vertexDesc;
|
||||||
@ -4701,7 +4763,7 @@ id<MTLComputePipelineState> QMetalGraphicsPipelineData::Tessellation::vsCompPipe
|
|||||||
cpDesc.stageInputDescriptor.indexBufferIndex = indexBufferBinding;
|
cpDesc.stageInputDescriptor.indexBufferIndex = indexBufferBinding;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
q->setupVertexOrStageInputDescriptor(cpDesc.stageInputDescriptor);
|
q->setupStageInputDescriptor(cpDesc.stageInputDescriptor);
|
||||||
|
|
||||||
rhiD->d->trySeedingComputePipelineFromBinaryArchive(cpDesc);
|
rhiD->d->trySeedingComputePipelineFromBinaryArchive(cpDesc);
|
||||||
|
|
||||||
@ -4759,6 +4821,13 @@ static inline bool hasBuiltin(const QVector<QShaderDescription::BuiltinVariable>
|
|||||||
[builtin](const QShaderDescription::BuiltinVariable &b) { return b.type == builtin; }) != builtinList.cend();
|
[builtin](const QShaderDescription::BuiltinVariable &b) { return b.type == builtin; }) != builtinList.cend();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline bool matches(const QShaderDescription::InOutVariable &a, const QShaderDescription::InOutVariable &b)
|
||||||
|
{
|
||||||
|
return a.location == b.location
|
||||||
|
&& a.type == b.type
|
||||||
|
&& a.perPatch == b.perPatch;
|
||||||
|
}
|
||||||
|
|
||||||
id<MTLRenderPipelineState> QMetalGraphicsPipelineData::Tessellation::teseFragRenderPipeline(QRhiMetal *rhiD, QMetalGraphicsPipeline *pipeline)
|
id<MTLRenderPipelineState> QMetalGraphicsPipelineData::Tessellation::teseFragRenderPipeline(QRhiMetal *rhiD, QMetalGraphicsPipeline *pipeline)
|
||||||
{
|
{
|
||||||
if (pipeline->d->ps)
|
if (pipeline->d->ps)
|
||||||
@ -4773,19 +4842,28 @@ id<MTLRenderPipelineState> QMetalGraphicsPipelineData::Tessellation::teseFragRen
|
|||||||
const int tescPatchOutputBufferBinding = ebb.value(QShaderPrivate::MslTessTescPatchOutputBufferBinding, -1);
|
const int tescPatchOutputBufferBinding = ebb.value(QShaderPrivate::MslTessTescPatchOutputBufferBinding, -1);
|
||||||
const int tessFactorBufferBinding = ebb.value(QShaderPrivate::MslTessTescTessLevelBufferBinding, -1);
|
const int tessFactorBufferBinding = ebb.value(QShaderPrivate::MslTessTescTessLevelBufferBinding, -1);
|
||||||
|
|
||||||
QVarLengthArray<int, 16> teseInputLocations;
|
QMap<int, QShaderDescription::InOutVariable> teseInVars;
|
||||||
for (const QShaderDescription::InOutVariable &v : vertTese.desc.inputVariables())
|
for (const QShaderDescription::InOutVariable &teseInVar : vertTese.desc.inputVariables())
|
||||||
teseInputLocations.append(v.location);
|
teseInVars[teseInVar.location] = teseInVar;
|
||||||
|
|
||||||
quint32 offsetInTescOutput = 0;
|
quint32 offsetInTescOutput = 0;
|
||||||
quint32 offsetInTescPatchOutput = 0;
|
quint32 offsetInTescPatchOutput = 0;
|
||||||
int lastLocation = -1;
|
int lastLocation = -1;
|
||||||
|
|
||||||
for (const QShaderDescription::InOutVariable &tescOutVar : compTesc.desc.outputVariables()) {
|
// these need to be sorted in location order so that lastLocation is calculated correctly - use QMap.
|
||||||
|
QMap<int, QShaderDescription::InOutVariable> tescOutVars;
|
||||||
|
for (const QShaderDescription::InOutVariable &tescOutVar : compTesc.desc.outputVariables())
|
||||||
|
tescOutVars[tescOutVar.location] = tescOutVar;
|
||||||
|
|
||||||
|
for (const QShaderDescription::InOutVariable &tescOutVar : tescOutVars) {
|
||||||
const int location = tescOutVar.location;
|
const int location = tescOutVar.location;
|
||||||
lastLocation = location;
|
lastLocation = location;
|
||||||
const QRhiVertexInputAttribute::Format format = rhiD->shaderDescVariableFormatToVertexInputFormat(tescOutVar.type);
|
const QRhiVertexInputAttribute::Format format = rhiD->shaderDescVariableFormatToVertexInputFormat(tescOutVar.type);
|
||||||
if (teseInputLocations.contains(location)) {
|
if (teseInVars.contains(location)) {
|
||||||
|
if (!matches(teseInVars[location], tescOutVar)) {
|
||||||
|
qWarning() << "mismatched tessellation control output -> tesssellation evaluation input at location" << location;
|
||||||
|
qWarning() << "tesc out:" << tescOutVar << "tese in:" << teseInVars[location];
|
||||||
|
}
|
||||||
if (tescOutVar.perPatch) {
|
if (tescOutVar.perPatch) {
|
||||||
if (tescPatchOutputBufferBinding >= 0) {
|
if (tescPatchOutputBufferBinding >= 0) {
|
||||||
vertexDesc.attributes[location].bufferIndex = tescPatchOutputBufferBinding;
|
vertexDesc.attributes[location].bufferIndex = tescPatchOutputBufferBinding;
|
||||||
@ -4799,6 +4877,8 @@ id<MTLRenderPipelineState> QMetalGraphicsPipelineData::Tessellation::teseFragRen
|
|||||||
vertexDesc.attributes[location].offset = offsetInTescOutput;
|
vertexDesc.attributes[location].offset = offsetInTescOutput;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
qWarning() << "missing tessellation evaluation input for tessellation control output:" << tescOutVar;
|
||||||
}
|
}
|
||||||
if (tescOutVar.perPatch)
|
if (tescOutVar.perPatch)
|
||||||
offsetInTescPatchOutput += rhiD->byteSizePerVertexForVertexInputFormat(format);
|
offsetInTescPatchOutput += rhiD->byteSizePerVertexForVertexInputFormat(format);
|
||||||
|
@ -446,7 +446,7 @@ public:
|
|||||||
void enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates);
|
void enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates);
|
||||||
void executeBufferHostWritesForSlot(QMetalBuffer *bufD, int slot);
|
void executeBufferHostWritesForSlot(QMetalBuffer *bufD, int slot);
|
||||||
void executeBufferHostWritesForCurrentFrame(QMetalBuffer *bufD);
|
void executeBufferHostWritesForCurrentFrame(QMetalBuffer *bufD);
|
||||||
static const int SUPPORTED_STAGES = 3;
|
static const int SUPPORTED_STAGES = 5;
|
||||||
void enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD,
|
void enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD,
|
||||||
QMetalCommandBuffer *cbD,
|
QMetalCommandBuffer *cbD,
|
||||||
int dynamicOffsetCount,
|
int dynamicOffsetCount,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user