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 <paul.tvete@qt.io>bb10
parent
9baf69c765
commit
dd105fab8d
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ class QRhiCommandBuffer;
|
|||
class QRhiResourceUpdateBatch;
|
||||
class QRhiResourceUpdateBatchPrivate;
|
||||
class QRhiProfiler;
|
||||
class QRhiShaderResourceBindingPrivate;
|
||||
|
||||
class Q_GUI_EXPORT QRhiDepthStencilClearValue
|
||||
{
|
||||
|
|
|
|||
|
|
@ -205,6 +205,8 @@ public:
|
|||
cleanupCallbacks.append(callback);
|
||||
}
|
||||
|
||||
bool sanityCheckGraphicsPipeline(QRhiGraphicsPipeline *ps);
|
||||
|
||||
QRhi *q;
|
||||
|
||||
static const int MAX_SHADER_CACHE_ENTRIES = 128;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -805,6 +805,10 @@ void QNullGraphicsPipeline::release()
|
|||
|
||||
bool QNullGraphicsPipeline::build()
|
||||
{
|
||||
QRHI_RES_RHI(QRhiNull);
|
||||
if (!rhiD->sanityCheckGraphicsPipeline(this))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5886,6 +5886,9 @@ bool QVkGraphicsPipeline::build()
|
|||
release();
|
||||
|
||||
QRHI_RES_RHI(QRhiVulkan);
|
||||
if (!rhiD->sanityCheckGraphicsPipeline(this))
|
||||
return false;
|
||||
|
||||
if (!rhiD->ensurePipelineCache())
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
#version 440
|
||||
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,10 @@
|
|||
#version 440
|
||||
|
||||
layout(location = 0) in vec4 position;
|
||||
|
||||
out gl_PerVertex { vec4 gl_Position; };
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = position;
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,12 +0,0 @@
|
|||
#version 440
|
||||
|
||||
layout(location = 0) in vec2 v_texcoord;
|
||||
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(binding = 1) uniform sampler2D tex;
|
||||
|
||||
void main()
|
||||
{
|
||||
fragColor = texture(tex, v_texcoord);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#version 440
|
||||
|
||||
layout(location = 0) in vec4 position;
|
||||
layout(location = 1) in vec2 texcoord;
|
||||
|
||||
layout(location = 0) out vec2 v_texcoord;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 mvp;
|
||||
} ubuf;
|
||||
|
||||
out gl_PerVertex { vec4 gl_Position; };
|
||||
|
||||
void main()
|
||||
{
|
||||
v_texcoord = texcoord;
|
||||
gl_Position = ubuf.mvp * position;
|
||||
}
|
||||
|
|
@ -81,6 +81,10 @@ private slots:
|
|||
void resourceUpdateBatchRGBATextureCopy();
|
||||
void resourceUpdateBatchRGBATextureMip_data();
|
||||
void resourceUpdateBatchRGBATextureMip();
|
||||
void invalidPipeline_data();
|
||||
void invalidPipeline();
|
||||
void renderToTextureSimple_data();
|
||||
void renderToTextureSimple();
|
||||
|
||||
private:
|
||||
struct {
|
||||
|
|
@ -980,5 +984,222 @@ void tst_QRhi::resourceUpdateBatchRGBATextureMip()
|
|||
}
|
||||
}
|
||||
|
||||
static QShader loadShader(const char *name)
|
||||
{
|
||||
QFile f(QString::fromUtf8(name));
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
const QByteArray contents = f.readAll();
|
||||
return QShader::fromSerialized(contents);
|
||||
}
|
||||
return QShader();
|
||||
}
|
||||
|
||||
void tst_QRhi::invalidPipeline_data()
|
||||
{
|
||||
rhiTestData();
|
||||
}
|
||||
|
||||
void tst_QRhi::invalidPipeline()
|
||||
{
|
||||
QFETCH(QRhi::Implementation, impl);
|
||||
QFETCH(QRhiInitParams *, initParams);
|
||||
|
||||
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
|
||||
if (!rhi)
|
||||
QSKIP("QRhi could not be created, skipping testing empty shader");
|
||||
|
||||
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget));
|
||||
QVERIFY(texture->build());
|
||||
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
|
||||
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
|
||||
rt->setRenderPassDescriptor(rpDesc.data());
|
||||
QVERIFY(rt->build());
|
||||
|
||||
QRhiCommandBuffer *cb = nullptr;
|
||||
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
|
||||
QVERIFY(cb);
|
||||
|
||||
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
|
||||
QVERIFY(srb->build());
|
||||
|
||||
QRhiVertexInputLayout inputLayout;
|
||||
inputLayout.setBindings({ { 2 * sizeof(float) } });
|
||||
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
|
||||
|
||||
// no stages
|
||||
QScopedPointer<QRhiGraphicsPipeline> 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<QRhi> 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<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
|
||||
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
|
||||
QVERIFY(texture->build());
|
||||
|
||||
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
|
||||
QScopedPointer<QRhiRenderPassDescriptor> 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<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
|
||||
QVERIFY(vbuf->build());
|
||||
updates->uploadStaticBuffer(vbuf.data(), vertices);
|
||||
|
||||
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
|
||||
QVERIFY(srb->build());
|
||||
|
||||
QScopedPointer<QRhiGraphicsPipeline> 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<const uchar *>(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<const quint32 *>(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 <tst_qrhi.moc>
|
||||
QTEST_MAIN(tst_QRhi)
|
||||
|
|
|
|||
Loading…
Reference in New Issue