diff --git a/src/gui/rhi/qrhid3d11.cpp b/src/gui/rhi/qrhid3d11.cpp index b9829ed263..f3ddc7aac4 100644 --- a/src/gui/rhi/qrhid3d11.cpp +++ b/src/gui/rhi/qrhid3d11.cpp @@ -1903,14 +1903,17 @@ void QRhiD3D11::updateShaderResourceBindings(QD3D11ShaderResourceBindings *srbD, const QShader::NativeResourceBindingMap *nativeResourceBindingMaps[]) { srbD->vsubufs.clear(); + srbD->vsubuforigbindings.clear(); srbD->vsubufoffsets.clear(); srbD->vsubufsizes.clear(); srbD->fsubufs.clear(); + srbD->fsubuforigbindings.clear(); srbD->fsubufoffsets.clear(); srbD->fsubufsizes.clear(); srbD->csubufs.clear(); + srbD->csubuforigbindings.clear(); srbD->csubufoffsets.clear(); srbD->csubufsizes.clear(); @@ -1927,6 +1930,7 @@ void QRhiD3D11::updateShaderResourceBindings(QD3D11ShaderResourceBindings *srbD, struct Stage { struct Buffer { + int binding; // stored and sent along in XXorigbindings just for applyDynamicOffsets() int breg; // b0, b1, ... ID3D11Buffer *buffer; uint offsetInConstants; @@ -1960,9 +1964,12 @@ void QRhiD3D11::updateShaderResourceBindings(QD3D11ShaderResourceBindings *srbD, Q_ASSERT(aligned(b->u.ubuf.offset, 256) == b->u.ubuf.offset); bd.ubuf.id = bufD->m_id; bd.ubuf.generation = bufD->generation; - // dynamic ubuf offsets are not considered here, those are baked in + // Dynamic ubuf offsets are not considered here, those are baked in // at a later stage, which is good as vsubufoffsets and friends are - // per-srb, not per-setShaderResources call + // per-srb, not per-setShaderResources call. Other backends (GL, + // Metal) are different in this respect since those do not store + // per-srb vsubufoffsets etc. data so life's a bit easier for them. + // But here we have to defer baking in the dynamic offset. const uint offsetInConstants = uint(b->u.ubuf.offset) / 16; // size must be 16 mult. (in constants, i.e. multiple of 256 bytes). // We can round up if needed since the buffers's actual size @@ -1971,17 +1978,17 @@ void QRhiD3D11::updateShaderResourceBindings(QD3D11ShaderResourceBindings *srbD, if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { QPair nativeBinding = mapBinding(b->binding, RBM_VERTEX, nativeResourceBindingMaps); if (nativeBinding.first >= 0) - res[RBM_VERTEX].buffers.append({ nativeBinding.first, bufD->buffer, offsetInConstants, sizeInConstants }); + res[RBM_VERTEX].buffers.append({ b->binding, nativeBinding.first, bufD->buffer, offsetInConstants, sizeInConstants }); } if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) { QPair nativeBinding = mapBinding(b->binding, RBM_FRAGMENT, nativeResourceBindingMaps); if (nativeBinding.first >= 0) - res[RBM_FRAGMENT].buffers.append({ nativeBinding.first, bufD->buffer, offsetInConstants, sizeInConstants }); + res[RBM_FRAGMENT].buffers.append({ b->binding, nativeBinding.first, bufD->buffer, offsetInConstants, sizeInConstants }); } if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) { QPair nativeBinding = mapBinding(b->binding, RBM_COMPUTE, nativeResourceBindingMaps); if (nativeBinding.first >= 0) - res[RBM_COMPUTE].buffers.append({ nativeBinding.first, bufD->buffer, offsetInConstants, sizeInConstants }); + res[RBM_COMPUTE].buffers.append({ b->binding, nativeBinding.first, bufD->buffer, offsetInConstants, sizeInConstants }); } } break; @@ -2088,28 +2095,34 @@ void QRhiD3D11::updateShaderResourceBindings(QD3D11ShaderResourceBindings *srbD, for (const Stage::Buffer &buf : qAsConst(res[RBM_VERTEX].buffers)) { srbD->vsubufs.feed(buf.breg, buf.buffer); + srbD->vsubuforigbindings.feed(buf.breg, UINT(buf.binding)); srbD->vsubufoffsets.feed(buf.breg, buf.offsetInConstants); srbD->vsubufsizes.feed(buf.breg, buf.sizeInConstants); } srbD->vsubufs.finish(); + srbD->vsubuforigbindings.finish(); srbD->vsubufoffsets.finish(); srbD->vsubufsizes.finish(); for (const Stage::Buffer &buf : qAsConst(res[RBM_FRAGMENT].buffers)) { srbD->fsubufs.feed(buf.breg, buf.buffer); + srbD->fsubuforigbindings.feed(buf.breg, UINT(buf.binding)); srbD->fsubufoffsets.feed(buf.breg, buf.offsetInConstants); srbD->fsubufsizes.feed(buf.breg, buf.sizeInConstants); } srbD->fsubufs.finish(); + srbD->fsubuforigbindings.finish(); srbD->fsubufoffsets.finish(); srbD->fsubufsizes.finish(); for (const Stage::Buffer &buf : qAsConst(res[RBM_COMPUTE].buffers)) { srbD->csubufs.feed(buf.breg, buf.buffer); + srbD->csubuforigbindings.feed(buf.breg, UINT(buf.binding)); srbD->csubufoffsets.feed(buf.breg, buf.offsetInConstants); srbD->csubufsizes.feed(buf.breg, buf.sizeInConstants); } srbD->csubufs.finish(); + srbD->csubuforigbindings.finish(); srbD->csubufoffsets.finish(); srbD->csubufsizes.finish(); @@ -2158,17 +2171,20 @@ void QRhiD3D11::executeBufferHostWrites(QD3D11Buffer *bufD) static void applyDynamicOffsets(QVarLengthArray *offsets, int batchIndex, - QRhiBatchedBindings *ubufs, - QRhiBatchedBindings *ubufoffsets, + QRhiBatchedBindings *originalBindings, + QRhiBatchedBindings *staticOffsets, const uint *dynOfsPairs, int dynOfsPairCount) { - const int count = ubufs->batches[batchIndex].resources.count(); - const UINT startBinding = ubufs->batches[batchIndex].startBinding; - *offsets = ubufoffsets->batches[batchIndex].resources; + const int count = staticOffsets->batches[batchIndex].resources.count(); + // Make a copy of the offset list, the entries that have no corresponding + // dynamic offset will continue to use the existing offset value. + *offsets = staticOffsets->batches[batchIndex].resources; for (int b = 0; b < count; ++b) { for (int di = 0; di < dynOfsPairCount; ++di) { const uint binding = dynOfsPairs[2 * di]; - if (binding == startBinding + UINT(b)) { + // binding is the SPIR-V style binding point here, nothing to do + // with the native one. + if (binding == originalBindings->batches[batchIndex].resources[b]) { const uint offsetInConstants = dynOfsPairs[2 * di + 1]; (*offsets)[b] = offsetInConstants; break; @@ -2258,7 +2274,8 @@ void QRhiD3D11::bindShaderResources(QD3D11ShaderResourceBindings *srbD, srbD->vsubufsizes.batches[i].resources.constData()); } else { QVarLengthArray offsets; - applyDynamicOffsets(&offsets, i, &srbD->vsubufs, &srbD->vsubufoffsets, dynOfsPairs, dynOfsPairCount); + applyDynamicOffsets(&offsets, i, &srbD->vsubuforigbindings, &srbD->vsubufoffsets, + dynOfsPairs, dynOfsPairCount); context->VSSetConstantBuffers1(srbD->vsubufs.batches[i].startBinding, count, srbD->vsubufs.batches[i].resources.constData(), @@ -2282,7 +2299,8 @@ void QRhiD3D11::bindShaderResources(QD3D11ShaderResourceBindings *srbD, srbD->fsubufsizes.batches[i].resources.constData()); } else { QVarLengthArray offsets; - applyDynamicOffsets(&offsets, i, &srbD->fsubufs, &srbD->fsubufoffsets, dynOfsPairs, dynOfsPairCount); + applyDynamicOffsets(&offsets, i, &srbD->fsubuforigbindings, &srbD->fsubufoffsets, + dynOfsPairs, dynOfsPairCount); context->PSSetConstantBuffers1(srbD->fsubufs.batches[i].startBinding, count, srbD->fsubufs.batches[i].resources.constData(), @@ -2306,7 +2324,8 @@ void QRhiD3D11::bindShaderResources(QD3D11ShaderResourceBindings *srbD, srbD->csubufsizes.batches[i].resources.constData()); } else { QVarLengthArray offsets; - applyDynamicOffsets(&offsets, i, &srbD->csubufs, &srbD->csubufoffsets, dynOfsPairs, dynOfsPairCount); + applyDynamicOffsets(&offsets, i, &srbD->csubuforigbindings, &srbD->csubufoffsets, + dynOfsPairs, dynOfsPairCount); context->CSSetConstantBuffers1(srbD->csubufs.batches[i].startBinding, count, srbD->csubufs.batches[i].resources.constData(), diff --git a/src/gui/rhi/qrhid3d11_p_p.h b/src/gui/rhi/qrhid3d11_p_p.h index c6890ca353..a4d5098084 100644 --- a/src/gui/rhi/qrhid3d11_p_p.h +++ b/src/gui/rhi/qrhid3d11_p_p.h @@ -238,14 +238,17 @@ struct QD3D11ShaderResourceBindings : public QRhiShaderResourceBindings QVarLengthArray boundResourceData; QRhiBatchedBindings vsubufs; + QRhiBatchedBindings vsubuforigbindings; QRhiBatchedBindings vsubufoffsets; QRhiBatchedBindings vsubufsizes; QRhiBatchedBindings fsubufs; + QRhiBatchedBindings fsubuforigbindings; QRhiBatchedBindings fsubufoffsets; QRhiBatchedBindings fsubufsizes; QRhiBatchedBindings csubufs; + QRhiBatchedBindings csubuforigbindings; QRhiBatchedBindings csubufoffsets; QRhiBatchedBindings csubufsizes; diff --git a/src/gui/rhi/qrhigles2_p_p.h b/src/gui/rhi/qrhigles2_p_p.h index 1f46d424a4..bee9b3249e 100644 --- a/src/gui/rhi/qrhigles2_p_p.h +++ b/src/gui/rhi/qrhigles2_p_p.h @@ -390,7 +390,7 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer QRhiComputePipeline *maybeComputePs; QRhiShaderResourceBindings *srb; int dynamicOffsetCount; - uint dynamicOffsetPairs[MAX_UBUF_BINDINGS * 2]; // binding, offsetInConstants + uint dynamicOffsetPairs[MAX_UBUF_BINDINGS * 2]; // binding, offset } bindShaderResources; struct { GLbitfield mask; diff --git a/tests/auto/gui/rhi/qrhi/data/buildshaders.bat b/tests/auto/gui/rhi/qrhi/data/buildshaders.bat index f4ebae070b..0102457b8a 100644 --- a/tests/auto/gui/rhi/qrhi/data/buildshaders.bat +++ b/tests/auto/gui/rhi/qrhi/data/buildshaders.bat @@ -44,3 +44,5 @@ qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o simpletextured.frag.qsb sim qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 20 -o simpletextured_array.frag.qsb simpletextured_array.frag qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.vert.qsb textured.vert 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 diff --git a/tests/auto/gui/rhi/qrhi/data/simple.frag.qsb b/tests/auto/gui/rhi/qrhi/data/simple.frag.qsb index 43edbdffd9..d398c81e0a 100644 Binary files a/tests/auto/gui/rhi/qrhi/data/simple.frag.qsb and b/tests/auto/gui/rhi/qrhi/data/simple.frag.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/simple.vert.qsb b/tests/auto/gui/rhi/qrhi/data/simple.vert.qsb index 06af5df492..c17286c38d 100644 Binary files a/tests/auto/gui/rhi/qrhi/data/simple.vert.qsb and b/tests/auto/gui/rhi/qrhi/data/simple.vert.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/simpletextured.frag.qsb b/tests/auto/gui/rhi/qrhi/data/simpletextured.frag.qsb index 7749f3caad..e3f57f41d4 100644 Binary files a/tests/auto/gui/rhi/qrhi/data/simpletextured.frag.qsb and b/tests/auto/gui/rhi/qrhi/data/simpletextured.frag.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/simpletextured.vert.qsb b/tests/auto/gui/rhi/qrhi/data/simpletextured.vert.qsb index c87d4b2fc1..22e2c7c3ec 100644 Binary files a/tests/auto/gui/rhi/qrhi/data/simpletextured.vert.qsb and b/tests/auto/gui/rhi/qrhi/data/simpletextured.vert.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/simpletextured_array.frag.qsb b/tests/auto/gui/rhi/qrhi/data/simpletextured_array.frag.qsb index 362e220d25..b7ae790fd4 100644 Binary files a/tests/auto/gui/rhi/qrhi/data/simpletextured_array.frag.qsb and b/tests/auto/gui/rhi/qrhi/data/simpletextured_array.frag.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/textured.frag.qsb b/tests/auto/gui/rhi/qrhi/data/textured.frag.qsb index f669152c9c..0935a1ab2c 100644 Binary files a/tests/auto/gui/rhi/qrhi/data/textured.frag.qsb and b/tests/auto/gui/rhi/qrhi/data/textured.frag.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/textured.vert.qsb b/tests/auto/gui/rhi/qrhi/data/textured.vert.qsb index d4ba474777..22f014ff91 100644 Binary files a/tests/auto/gui/rhi/qrhi/data/textured.vert.qsb and b/tests/auto/gui/rhi/qrhi/data/textured.vert.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.frag b/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.frag new file mode 100644 index 0000000000..c1b707a1bb --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.frag @@ -0,0 +1,18 @@ +#version 440 + +layout(location = 0) in vec2 uv; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 1) uniform buf { + float opacity; +} fubuf; + +layout(binding = 2) uniform sampler2D tex; + +void main() +{ + vec4 c = texture(tex, uv); + c.a *= fubuf.opacity; + c.rgb *= c.a; + fragColor = c; +} diff --git a/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.frag.qsb b/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.frag.qsb new file mode 100644 index 0000000000..728ecaf312 Binary files /dev/null and b/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.frag.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.vert b/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.vert new file mode 100644 index 0000000000..88dd4e45b6 --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.vert @@ -0,0 +1,18 @@ +#version 440 + +layout(location = 0) in vec4 position; +layout(location = 1) in vec2 texcoord; + +layout(location = 0) out vec2 uv; + +layout(std140, binding = 0) uniform buf { + mat4 matrix; +} vubuf; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() +{ + uv = texcoord; + gl_Position = vubuf.matrix * position; +} diff --git a/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.vert.qsb b/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.vert.qsb new file mode 100644 index 0000000000..3cc009d549 Binary files /dev/null and b/tests/auto/gui/rhi/qrhi/data/textured_multiubuf.vert.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp index a20bea7ace..568e77fd61 100644 --- a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp +++ b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp @@ -112,6 +112,8 @@ private slots: void renderToTextureTexturedQuadAndUniformBuffer(); void renderToTextureDeferredSrb_data(); void renderToTextureDeferredSrb(); + void renderToTextureMultipleUniformBuffersAndDynamicOffset_data(); + void renderToTextureMultipleUniformBuffersAndDynamicOffset(); void renderToWindowSimple_data(); void renderToWindowSimple(); void finishWithinSwapchainFrame_data(); @@ -2354,6 +2356,175 @@ void tst_QRhi::renderToTextureDeferredSrb() QCOMPARE(result.pixel(4, 227), empty); } +void tst_QRhi::renderToTextureMultipleUniformBuffersAndDynamicOffset_data() +{ + rhiTestData(); +} + +void tst_QRhi::renderToTextureMultipleUniformBuffersAndDynamicOffset() +{ + 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"); + + QImage inputImage; + inputImage.load(QLatin1String(":/data/qt256.png")); + QVERIFY(!inputImage.isNull()); + + QScopedPointer texture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size(), 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()); + + QRhiCommandBuffer *cb = nullptr; + QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess); + QVERIFY(cb); + + QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch(); + + static const float verticesUvs[] = { + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + -1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f + }; + QScopedPointer vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(verticesUvs))); + QVERIFY(vbuf->create()); + updates->uploadStaticBuffer(vbuf.data(), verticesUvs); + + QScopedPointer inputTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size())); + QVERIFY(inputTexture->create()); + updates->uploadTexture(inputTexture.data(), inputImage); + + QScopedPointer sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None, + QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge)); + QVERIFY(sampler->create()); + + const int MATRIX_COUNT = 4; // put 4 mat4s into the buffer, will only use one + const int ubufElemSize = rhi->ubufAligned(64); + QVERIFY(ubufElemSize >= 64); + QScopedPointer ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, MATRIX_COUNT * ubufElemSize)); + QVERIFY(ubuf->create()); + + float zeroes[16]; + memset(zeroes, 0, sizeof(zeroes)); + updates->updateDynamicBuffer(ubuf.data(), 0, 64, zeroes); + updates->updateDynamicBuffer(ubuf.data(), ubufElemSize, 64, zeroes); + // the only correct matrix is the third one + QMatrix4x4 matrix; + updates->updateDynamicBuffer(ubuf.data(), ubufElemSize * 2, 64, matrix.constData()); + updates->updateDynamicBuffer(ubuf.data(), ubufElemSize * 3, 64, zeroes); + + const int OPACITY_COUNT = 6; // put 6 floats into the buffer, will only use one + const int ubuf2ElemSize = rhi->ubufAligned(4); + QVERIFY(ubuf2ElemSize >= 4); + QScopedPointer ubuf2(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, OPACITY_COUNT * ubuf2ElemSize)); + QVERIFY(ubuf2->create()); + + updates->updateDynamicBuffer(ubuf2.data(), 0, 4, &zeroes[0]); + updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize, 4, &zeroes[0]); + updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize * 2, 4, &zeroes[0]); + // the only correct opacity value is the fourth one + float opacity = 0.5f; + updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize * 3, 4, &opacity); + updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize * 4, 4, &zeroes[0]); + updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize * 5, 4, &zeroes[0]); + + const QRhiShaderResourceBinding::StageFlags commonVisibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage; + QScopedPointer srb(rhi->newShaderResourceBindings()); + srb->setBindings({ + QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, commonVisibility, ubuf.data(), 64), + QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(1, commonVisibility, ubuf2.data(), 4), + QRhiShaderResourceBinding::sampledTexture(2, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler.data()) + }); + QVERIFY(srb->create()); + + QScopedPointer pipeline(rhi->newGraphicsPipeline()); + pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip); + QShader vs = loadShader(":/data/textured_multiubuf.vert.qsb"); + QVERIFY(vs.isValid()); + QShader fs = loadShader(":/data/textured_multiubuf.frag.qsb"); + QVERIFY(fs.isValid()); + pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } }); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ { 4 * sizeof(float) } }); + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, + { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) } + }); + pipeline->setVertexInputLayout(inputLayout); + pipeline->setShaderResourceBindings(srb.data()); + pipeline->setRenderPassDescriptor(rpDesc.data()); + + QVERIFY(pipeline->create()); + + cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, updates); + cb->setGraphicsPipeline(pipeline.data()); + + // Now the magic, expose the 3rd matrix and 4th opacity value to the shader. + // If the handling of dynamic offsets were broken, the shaders would likely + // "see" an all zero matrix and zero opacity, thus leading to different + // rendering output. This way we can verify if using dynamic offsets, and + // more than one at the same time, is functional. + QVarLengthArray, 2> dynamicOffset = { + { 0, quint32(ubufElemSize * 2) }, + { 1, quint32(ubuf2ElemSize * 3) }, + }; + cb->setShaderResources(srb.data(), 2, dynamicOffset.constData()); + + cb->setViewport({ 0, 0, float(texture->pixelSize().width()), float(texture->pixelSize().height()) }); + QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0); + cb->setVertexInput(0, 1, &vbindings); + cb->draw(4); + + QRhiReadbackResult readResult; + QImage result; + readResult.completed = [&readResult, &result] { + result = QImage(reinterpret_cast(readResult.data.constData()), + readResult.pixelSize.width(), readResult.pixelSize.height(), + QImage::Format_RGBA8888_Premultiplied); + }; + QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch(); + readbackBatch->readBackTexture({ texture.data() }, &readResult); + cb->endPass(readbackBatch); + + rhi->endOffscreenFrame(); + + QVERIFY(!result.isNull()); + + if (impl == QRhi::Null) + return; + + if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC()) + result = std::move(result).mirrored(); + + // opacity 0.5 (premultiplied) + static const auto checkSemiWhite = [](const QRgb &c) { + QRgb semiWhite127 = qPremultiply(qRgba(255, 255, 255, 127)); + QRgb semiWhite128 = qPremultiply(qRgba(255, 255, 255, 128)); + return c == semiWhite127 || c == semiWhite128; + }; + QVERIFY(checkSemiWhite(result.pixel(79, 77))); + QVERIFY(checkSemiWhite(result.pixel(124, 81))); + QVERIFY(checkSemiWhite(result.pixel(128, 149))); + QVERIFY(checkSemiWhite(result.pixel(120, 189))); + QVERIFY(checkSemiWhite(result.pixel(116, 185))); + QVERIFY(checkSemiWhite(result.pixel(191, 172))); + + QRgb empty = qRgba(0, 0, 0, 0); + QCOMPARE(result.pixel(11, 45), empty); + QCOMPARE(result.pixel(246, 202), empty); + QCOMPARE(result.pixel(130, 18), empty); + QCOMPARE(result.pixel(4, 227), empty); +} + void tst_QRhi::setWindowType(QWindow *window, QRhi::Implementation impl) { switch (impl) {