From dd105fab8d8b4bd39654e7e268e6782e53cce2de Mon Sep 17 00:00:00 2001 From: Laszlo Agocs Date: Sun, 6 Oct 2019 17:25:06 +0200 Subject: [PATCH] rhi: Autotest rendering a triangle Also improve (docs and runtime checks) and test the minimum set of required data to create a graphics pipeline. Task-number: QTBUG-78971 Change-Id: If5c14f1ab1ff3cf70f168fde585f05fc9d28ec91 Reviewed-by: Paul Olav Tvete --- src/gui/rhi/qrhi.cpp | 48 +++- src/gui/rhi/qrhi_p.h | 1 - src/gui/rhi/qrhi_p_p.h | 2 + src/gui/rhi/qrhid3d11.cpp | 2 + src/gui/rhi/qrhigles2.cpp | 3 + src/gui/rhi/qrhimetal.mm | 2 + src/gui/rhi/qrhinull.cpp | 4 + src/gui/rhi/qrhivulkan.cpp | 3 + tests/auto/gui/rhi/qrhi/data/simple.frag | 8 + tests/auto/gui/rhi/qrhi/data/simple.frag.qsb | Bin 0 -> 773 bytes tests/auto/gui/rhi/qrhi/data/simple.vert | 10 + tests/auto/gui/rhi/qrhi/data/simple.vert.qsb | Bin 0 -> 851 bytes tests/auto/gui/rhi/qrhi/data/texture.frag | 12 - tests/auto/gui/rhi/qrhi/data/texture.vert | 18 -- tests/auto/gui/rhi/qrhi/tst_qrhi.cpp | 221 +++++++++++++++++++ 15 files changed, 300 insertions(+), 34 deletions(-) create mode 100644 tests/auto/gui/rhi/qrhi/data/simple.frag create mode 100644 tests/auto/gui/rhi/qrhi/data/simple.frag.qsb create mode 100644 tests/auto/gui/rhi/qrhi/data/simple.vert create mode 100644 tests/auto/gui/rhi/qrhi/data/simple.vert.qsb delete mode 100644 tests/auto/gui/rhi/qrhi/data/texture.frag delete mode 100644 tests/auto/gui/rhi/qrhi/data/texture.vert diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp index 57c466678e..dabad35688 100644 --- a/src/gui/rhi/qrhi.cpp +++ b/src/gui/rhi/qrhi.cpp @@ -3084,9 +3084,13 @@ QDebug operator<<(QDebug dbg, const QRhiShaderResourceBindings &srb) \inmodule QtGui \brief Graphics pipeline state resource. + \note Setting the shader stages is mandatory. There must be at least one + stage, and there must be a vertex stage. + \note Setting the shader resource bindings is mandatory. The referenced QRhiShaderResourceBindings must already be built by the time build() is - called. + called. Associating with a QRhiShaderResourceBindings that has no bindings + is also valid, as long as no shader in any stage expects any resources. \note Setting the render pass descriptor is mandatory. To obtain a QRhiRenderPassDescriptor that can be passed to setRenderPassDescriptor(), @@ -3095,8 +3099,6 @@ QDebug operator<<(QDebug dbg, const QRhiShaderResourceBindings &srb) \note Setting the vertex input layout is mandatory. - \note Setting the shader stages is mandatory. - \note sampleCount() defaults to 1 and must match the sample count of the render target's color and depth stencil attachments. @@ -3912,6 +3914,46 @@ quint32 QRhiImplementation::approxByteSizeForTexture(QRhiTexture::Format format, return approxSize; } +bool QRhiImplementation::sanityCheckGraphicsPipeline(QRhiGraphicsPipeline *ps) +{ + if (ps->cbeginShaderStages() == ps->cendShaderStages()) { + qWarning("Cannot build a graphics pipeline without any stages"); + return false; + } + + bool hasVertexStage = false; + for (auto it = ps->cbeginShaderStages(), itEnd = ps->cendShaderStages(); it != itEnd; ++it) { + if (!it->shader().isValid()) { + qWarning("Empty shader passed to graphics pipeline"); + return false; + } + if (it->type() == QRhiShaderStage::Vertex) { + hasVertexStage = true; + const QRhiVertexInputLayout inputLayout = ps->vertexInputLayout(); + if (inputLayout.cbeginAttributes() == inputLayout.cendAttributes()) { + qWarning("Vertex stage present without any vertex inputs"); + return false; + } + } + } + if (!hasVertexStage) { + qWarning("Cannot build a graphics pipeline without a vertex stage"); + return false; + } + + if (!ps->renderPassDescriptor()) { + qWarning("Cannot build a graphics pipeline without a QRhiRenderPassDescriptor"); + return false; + } + + if (!ps->shaderResourceBindings()) { + qWarning("Cannot build a graphics pipeline without QRhiShaderResourceBindings"); + return false; + } + + return true; +} + /*! \internal */ diff --git a/src/gui/rhi/qrhi_p.h b/src/gui/rhi/qrhi_p.h index bfcc1d39f5..907924c788 100644 --- a/src/gui/rhi/qrhi_p.h +++ b/src/gui/rhi/qrhi_p.h @@ -72,7 +72,6 @@ class QRhiCommandBuffer; class QRhiResourceUpdateBatch; class QRhiResourceUpdateBatchPrivate; class QRhiProfiler; -class QRhiShaderResourceBindingPrivate; class Q_GUI_EXPORT QRhiDepthStencilClearValue { diff --git a/src/gui/rhi/qrhi_p_p.h b/src/gui/rhi/qrhi_p_p.h index cc14293580..baffe28202 100644 --- a/src/gui/rhi/qrhi_p_p.h +++ b/src/gui/rhi/qrhi_p_p.h @@ -205,6 +205,8 @@ public: cleanupCallbacks.append(callback); } + bool sanityCheckGraphicsPipeline(QRhiGraphicsPipeline *ps); + QRhi *q; static const int MAX_SHADER_CACHE_ENTRIES = 128; diff --git a/src/gui/rhi/qrhid3d11.cpp b/src/gui/rhi/qrhid3d11.cpp index 096a8c2756..57ebb2909f 100644 --- a/src/gui/rhi/qrhid3d11.cpp +++ b/src/gui/rhi/qrhid3d11.cpp @@ -3491,6 +3491,8 @@ bool QD3D11GraphicsPipeline::build() release(); QRHI_RES_RHI(QRhiD3D11); + if (!rhiD->sanityCheckGraphicsPipeline(this)) + return false; D3D11_RASTERIZER_DESC rastDesc; memset(&rastDesc, 0, sizeof(rastDesc)); diff --git a/src/gui/rhi/qrhigles2.cpp b/src/gui/rhi/qrhigles2.cpp index ca1c91499d..4163ab1e79 100644 --- a/src/gui/rhi/qrhigles2.cpp +++ b/src/gui/rhi/qrhigles2.cpp @@ -3690,6 +3690,9 @@ bool QGles2GraphicsPipeline::build() if (!rhiD->ensureContext()) return false; + if (!rhiD->sanityCheckGraphicsPipeline(this)) + return false; + drawMode = toGlTopology(m_topology); program = rhiD->f->glCreateProgram(); diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm index 694882ab93..ef5ab696d7 100644 --- a/src/gui/rhi/qrhimetal.mm +++ b/src/gui/rhi/qrhimetal.mm @@ -3139,6 +3139,8 @@ bool QMetalGraphicsPipeline::build() release(); QRHI_RES_RHI(QRhiMetal); + if (!rhiD->sanityCheckGraphicsPipeline(this)) + return false; // same binding space for vertex and constant buffers - work it around const int firstVertexBinding = QRHI_RES(QMetalShaderResourceBindings, m_shaderResourceBindings)->maxBinding + 1; diff --git a/src/gui/rhi/qrhinull.cpp b/src/gui/rhi/qrhinull.cpp index d78b5e20c5..2eb55e705b 100644 --- a/src/gui/rhi/qrhinull.cpp +++ b/src/gui/rhi/qrhinull.cpp @@ -805,6 +805,10 @@ void QNullGraphicsPipeline::release() bool QNullGraphicsPipeline::build() { + QRHI_RES_RHI(QRhiNull); + if (!rhiD->sanityCheckGraphicsPipeline(this)) + return false; + return true; } diff --git a/src/gui/rhi/qrhivulkan.cpp b/src/gui/rhi/qrhivulkan.cpp index 0d364dd9fc..fca2125f10 100644 --- a/src/gui/rhi/qrhivulkan.cpp +++ b/src/gui/rhi/qrhivulkan.cpp @@ -5886,6 +5886,9 @@ bool QVkGraphicsPipeline::build() release(); QRHI_RES_RHI(QRhiVulkan); + if (!rhiD->sanityCheckGraphicsPipeline(this)) + return false; + if (!rhiD->ensurePipelineCache()) return false; diff --git a/tests/auto/gui/rhi/qrhi/data/simple.frag b/tests/auto/gui/rhi/qrhi/data/simple.frag new file mode 100644 index 0000000000..2aa500e09a --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/simple.frag @@ -0,0 +1,8 @@ +#version 440 + +layout(location = 0) out vec4 fragColor; + +void main() +{ + fragColor = vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/tests/auto/gui/rhi/qrhi/data/simple.frag.qsb b/tests/auto/gui/rhi/qrhi/data/simple.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..bf0c2692af4802768958aab97f7ae106d47c34d9 GIT binary patch literal 773 zcmV+g1N!^`00+@{oZVF2Y7;>eo;2IWOzJ=AML|a6MS~@7ODe^*6|Go=fTa;{YFVed zlg-x6Zn`^LYD+;1zJL^;#YgbcOCLo9!Ek7FYE>()4H!^ zkPn4aZ>4FbbUG)?TzFHQsFo0Z2`XRI)~nDRa!`Q~+}B&}Ry~$Zy|IYU$t6-uqd_i% zkDMlO97Pp7lhK zi%s8VYdS|}M_FM<5QTQo>RI9yU2Ar}Zq6}t@Bp=z`bR=0QGj7km?DvrDz(1Nh1^Ik z#S-S<9%(-V+TM7+`N~?0!Z@~GsDIUYh##`{E9)hXTp`UA@qXwz%E7|=1DxZrbQF%k z6D>8>|7ZtnB%Lmh;vW+LFkNA#Uxm~uZy9Dc;~+=oaDro+8#SD}PdQHSRQE*`!$*b9 zFPbo49Y=K7ddqse`NWF5+=02Wd>*(7W^62&FkWwIRUAkWsHi`<0?(1yOT0)xP~do- z-%E%-kGK>nfrzYdXII!V9#>(!=V$?n^N63kp#f31d+x=e`}h$}M>5Zzqjy|~8GEKl z`;o9crFA{G+fT=(Ib!w#Ikq9!x1K>yPr2k<=JsE9b&$K$Ygtzd|H+;HE4F?C+KSGn DK&5-n literal 0 HcmV?d00001 diff --git a/tests/auto/gui/rhi/qrhi/data/simple.vert b/tests/auto/gui/rhi/qrhi/data/simple.vert new file mode 100644 index 0000000000..16ee61beca --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/simple.vert @@ -0,0 +1,10 @@ +#version 440 + +layout(location = 0) in vec4 position; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() +{ + gl_Position = position; +} diff --git a/tests/auto/gui/rhi/qrhi/data/simple.vert.qsb b/tests/auto/gui/rhi/qrhi/data/simple.vert.qsb new file mode 100644 index 0000000000000000000000000000000000000000..1e010ebac3cdc0c7b9d87a53fe9784c87e121159 GIT binary patch literal 851 zcmV-Z1FZZ200|9voZVI3P7^^Cp3;`W0P^Es42fA&FG@^mCB+yDB#5SxkU*#s6Kpoy z?z9YUcgyZ9K?yfRU%(ij#Ro9)#!DZD8xzl&nPs;>L@&H@$?0~^Ip25Ad}oFdLQ+IM zKaU!R0WN-kWx>9I-AEHMO1Rq%Mc}}C1ndCp4Ok9r2kaB%j1k9cT7r8nxqRkF3CR%G z>M)FVy@0Fu{5bl>yDr1`F>96ynS}iNU@KrBp^49Pgp5m0r^eqiQX<%g%DL(}2i!R{ zc%D{#AC!AkhwV5sb&(#)=Wy#e=xk1VXJL1^{|SKLySi1|Dh9$XmY1+Rxkl0o4K^)z zkNp$UvAEndl{o~m(o(0dDbz93lamGRF?hEfbBFnR%oprLHL3hW$0D(^z(ut`3xCKT zD@n);@I|-@{jLILOwXzc)e&=7$F74H{r6;-D(4n_Ly-)y+mN5rqGcpbaphz@12Z8K zhrWtlQ|-G6>j}++8a>eKNnIQ9Q#zK@_$jSfO2=n37semM-bQpEMEftmgJ*M9_c^Wc z>)@S{GgSRx%{4&G>YQIt;fXrx89j?kqBZPbJ4UYHQ1?3&7Q8t`g$nHr#U&y&YCD^|d*+7k1%bJV z|B9}VW!ZT7{Po86_U^M%kD5vRb|?l2lDk|sYI}*+q^=&f4HVbJ%@rcudbNqW1|bFu zN|g%(8!@7B!1su^8?v-+gKZ2bK-D#dIvU9U^0Su>_aqEzPqN!CZ;@Ln@5MXGbCT z8Gu}yxB5%4uH{>TNdo4ZUgMB8MKG+w@XWyi6s`>ZJ|)kjJPm()5c<@zN&IvBDpsph dqV)1Y|I>meiSgWt3oiaA rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr)); + if (!rhi) + QSKIP("QRhi could not be created, skipping testing empty shader"); + + QScopedPointer texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget)); + QVERIFY(texture->build()); + QScopedPointer rt(rhi->newTextureRenderTarget({ texture.data() })); + QScopedPointer rpDesc(rt->newCompatibleRenderPassDescriptor()); + rt->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(rt->build()); + + QRhiCommandBuffer *cb = nullptr; + QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess); + QVERIFY(cb); + + QScopedPointer srb(rhi->newShaderResourceBindings()); + QVERIFY(srb->build()); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ { 2 * sizeof(float) } }); + inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } }); + + // no stages + QScopedPointer pipeline(rhi->newGraphicsPipeline()); + pipeline->setVertexInputLayout(inputLayout); + pipeline->setShaderResourceBindings(srb.data()); + pipeline->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(!pipeline->build()); + + QShader vs; + QShader fs; + + // no shaders in the stages + pipeline.reset(rhi->newGraphicsPipeline()); + pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } }); + pipeline->setVertexInputLayout(inputLayout); + pipeline->setShaderResourceBindings(srb.data()); + pipeline->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(!pipeline->build()); + + vs = loadShader(":/data/simple.vert.qsb"); + QVERIFY(vs.isValid()); + fs = loadShader(":/data/simple.frag.qsb"); + QVERIFY(fs.isValid()); + + // no vertex stage + pipeline.reset(rhi->newGraphicsPipeline()); + pipeline->setShaderStages({ { QRhiShaderStage::Fragment, fs } }); + pipeline->setVertexInputLayout(inputLayout); + pipeline->setShaderResourceBindings(srb.data()); + pipeline->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(!pipeline->build()); + + // no vertex inputs + pipeline.reset(rhi->newGraphicsPipeline()); + pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } }); + pipeline->setRenderPassDescriptor(rpDesc.data()); + pipeline->setShaderResourceBindings(srb.data()); + QVERIFY(!pipeline->build()); + + // no renderpass descriptor + pipeline.reset(rhi->newGraphicsPipeline()); + pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } }); + pipeline->setVertexInputLayout(inputLayout); + pipeline->setShaderResourceBindings(srb.data()); + QVERIFY(!pipeline->build()); + + // no shader resource bindings + pipeline.reset(rhi->newGraphicsPipeline()); + pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } }); + pipeline->setVertexInputLayout(inputLayout); + pipeline->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(!pipeline->build()); + + // correct + pipeline.reset(rhi->newGraphicsPipeline()); + pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } }); + pipeline->setVertexInputLayout(inputLayout); + pipeline->setRenderPassDescriptor(rpDesc.data()); + pipeline->setShaderResourceBindings(srb.data()); + QVERIFY(pipeline->build()); +} + +void tst_QRhi::renderToTextureSimple_data() +{ + rhiTestData(); +} + +void tst_QRhi::renderToTextureSimple() +{ + 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"); + + const QSize outputSize(1920, 1080); + QScopedPointer texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1, + QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); + QVERIFY(texture->build()); + + QScopedPointer rt(rhi->newTextureRenderTarget({ texture.data() })); + QScopedPointer rpDesc(rt->newCompatibleRenderPassDescriptor()); + rt->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(rt->build()); + + QRhiCommandBuffer *cb = nullptr; + QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess); + QVERIFY(cb); + + QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch(); + + static const float vertices[] = { + -1.0f, -1.0f, + 1.0f, -1.0f, + 0.0f, 1.0f + }; + QScopedPointer vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices))); + QVERIFY(vbuf->build()); + updates->uploadStaticBuffer(vbuf.data(), vertices); + + QScopedPointer srb(rhi->newShaderResourceBindings()); + QVERIFY(srb->build()); + + QScopedPointer pipeline(rhi->newGraphicsPipeline()); + QShader vs = loadShader(":/data/simple.vert.qsb"); + QVERIFY(vs.isValid()); + QShader fs = loadShader(":/data/simple.frag.qsb"); + QVERIFY(fs.isValid()); + pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } }); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ { 2 * sizeof(float) } }); + inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } }); + pipeline->setVertexInputLayout(inputLayout); + pipeline->setShaderResourceBindings(srb.data()); + pipeline->setRenderPassDescriptor(rpDesc.data()); + + QVERIFY(pipeline->build()); + + cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates); + cb->setGraphicsPipeline(pipeline.data()); + cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) }); + QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0); + cb->setVertexInput(0, 1, &vbindings); + 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_Premultiplied); // non-owning, no copy needed because readResult outlives result + }; + QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch(); + readbackBatch->readBackTexture({ texture.data() }, &readResult); + cb->endPass(readbackBatch); + + rhi->endOffscreenFrame(); + // Offscreen frames are synchronous, so the readback is guaranteed to + // complete at this point. This would not be the case with swapchain-based + // frames. + QCOMPARE(result.size(), texture->pixelSize()); + + if (impl == QRhi::Null) + return; + + // Now we have a red rectangle on blue background. + const int y = 100; + const quint32 *p = reinterpret_cast(result.constScanLine(y)); + int x = result.width() - 1; + int redCount = 0; + int blueCount = 0; + const int maxFuzz = 1; + while (x-- >= 0) { + const QRgb c(*p++); + if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0) + ++redCount; + else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz)) + ++blueCount; + else + QFAIL("Encountered a pixel that is neither red or blue"); + } + + QCOMPARE(redCount + blueCount, texture->pixelSize().width()); + + // The triangle is "pointing up" in the resulting image with OpenGL + // (because Y is up both in normalized device coordinates and in images) + // and Vulkan (because Y is down in both and the vertex data was specified + // with Y up in mind), but "pointing down" with D3D (because Y is up in NDC + // but down in images). + if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC()) + QVERIFY(redCount < blueCount); + else + QVERIFY(redCount > blueCount); +} + #include QTEST_MAIN(tst_QRhi)