diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp index ad5d10819d4..642f1cf9376 100644 --- a/src/gui/rhi/qrhi.cpp +++ b/src/gui/rhi/qrhi.cpp @@ -1072,7 +1072,8 @@ Q_STATIC_LOGGING_CATEGORY(QRHI_LOG_RUB, "qt.rhi.rub") \value PerRenderTargetBlending Indicates that per rendertarget blending is supported i.e. different render targets in MRT framebuffer can have different - blending modes. Currently always returns false for OpenGL. + blending modes. In practice this can be expected to be supported everywhere + except OpenGL ES, where it is only available with GLES 3.2 implementations. This enum value has been introduced in Qt 6.9. */ diff --git a/src/gui/rhi/qrhigles2.cpp b/src/gui/rhi/qrhigles2.cpp index 445f0edb329..50a446fe588 100644 --- a/src/gui/rhi/qrhigles2.cpp +++ b/src/gui/rhi/qrhigles2.cpp @@ -1117,7 +1117,11 @@ bool QRhiGles2::create(QRhi::Flags flags) } caps.unpackRowLength = !caps.gles || caps.ctxMajor >= 3; - caps.perRenderTargetBlending = false; + + if (caps.gles) + caps.perRenderTargetBlending = caps.ctxMajor > 3 || (caps.ctxMajor == 3 && caps.ctxMinor >= 2); + else + caps.perRenderTargetBlending = caps.ctxMajor >= 4; nativeHandlesStruct.context = ctx; @@ -3884,54 +3888,63 @@ void QRhiGles2::executeBindGraphicsPipeline(QGles2CommandBuffer *cbD, QGles2Grap } if (!psD->m_targetBlends.isEmpty()) { - // We do not have MRT support here, meaning all targets use the blend - // params from the first one. This is technically incorrect, even if - // nothing in Qt relies on it. However, considering that - // glBlendFuncSeparatei is only available in GL 4.0+ and GLES 3.2+, we - // may just live with this for now because no point in bothering if it - // won't be usable on many GLES (3.1 or 3.0) systems. - const QRhiGraphicsPipeline::TargetBlend &targetBlend(psD->m_targetBlends.first()); - - const QGles2CommandBuffer::GraphicsPassState::ColorMask colorMask = { - targetBlend.colorWrite.testFlag(QRhiGraphicsPipeline::R), - targetBlend.colorWrite.testFlag(QRhiGraphicsPipeline::G), - targetBlend.colorWrite.testFlag(QRhiGraphicsPipeline::B), - targetBlend.colorWrite.testFlag(QRhiGraphicsPipeline::A) - }; - if (forceUpdate || colorMask != state.colorMask) { - state.colorMask = colorMask; - f->glColorMask(colorMask.r, colorMask.g, colorMask.b, colorMask.a); - } - - const bool blendEnabled = targetBlend.enable; - const QGles2CommandBuffer::GraphicsPassState::Blend blend = { - toGlBlendFactor(targetBlend.srcColor), - toGlBlendFactor(targetBlend.dstColor), - toGlBlendFactor(targetBlend.srcAlpha), - toGlBlendFactor(targetBlend.dstAlpha), - toGlBlendOp(targetBlend.opColor), - toGlBlendOp(targetBlend.opAlpha) - }; - if (forceUpdate || blendEnabled != state.blendEnabled || (blendEnabled && blend != state.blend)) { - state.blendEnabled = blendEnabled; - if (blendEnabled) { - state.blend = blend; - f->glEnable(GL_BLEND); - f->glBlendFuncSeparate(blend.srcColor, blend.dstColor, blend.srcAlpha, blend.dstAlpha); - f->glBlendEquationSeparate(blend.opColor, blend.opAlpha); - } else { - f->glDisable(GL_BLEND); + GLint buffer = 0; + bool anyBlendEnabled = false; + for (const auto targetBlend : psD->m_targetBlends) { + const QGles2CommandBuffer::GraphicsPassState::ColorMask colorMask = { + targetBlend.colorWrite.testFlag(QRhiGraphicsPipeline::R), + targetBlend.colorWrite.testFlag(QRhiGraphicsPipeline::G), + targetBlend.colorWrite.testFlag(QRhiGraphicsPipeline::B), + targetBlend.colorWrite.testFlag(QRhiGraphicsPipeline::A) + }; + if (forceUpdate || colorMask != state.colorMask[buffer]) { + state.colorMask[buffer] = colorMask; + if (caps.perRenderTargetBlending) + f->glColorMaski(buffer, colorMask.r, colorMask.g, colorMask.b, colorMask.a); + else + f->glColorMask(colorMask.r, colorMask.g, colorMask.b, colorMask.a); } + + const bool blendEnabled = targetBlend.enable; + const QGles2CommandBuffer::GraphicsPassState::Blend blend = { + toGlBlendFactor(targetBlend.srcColor), + toGlBlendFactor(targetBlend.dstColor), + toGlBlendFactor(targetBlend.srcAlpha), + toGlBlendFactor(targetBlend.dstAlpha), + toGlBlendOp(targetBlend.opColor), + toGlBlendOp(targetBlend.opAlpha) + }; + anyBlendEnabled |= blendEnabled; + if (forceUpdate || blendEnabled != state.blendEnabled[buffer] || (blendEnabled && blend != state.blend[buffer])) { + state.blendEnabled[buffer] = blendEnabled; + if (blendEnabled) { + state.blend[buffer] = blend; + if (caps.perRenderTargetBlending) { + f->glBlendFuncSeparatei(buffer, blend.srcColor, blend.dstColor, blend.srcAlpha, blend.dstAlpha); + f->glBlendEquationSeparatei(buffer, blend.opColor, blend.opAlpha); + } else { + f->glBlendFuncSeparate(blend.srcColor, blend.dstColor, blend.srcAlpha, blend.dstAlpha); + f->glBlendEquationSeparate(blend.opColor, blend.opAlpha); + } + } + } + buffer++; + if (!caps.perRenderTargetBlending) + break; } + if (anyBlendEnabled) + f->glEnable(GL_BLEND); + else + f->glDisable(GL_BLEND); } else { const QGles2CommandBuffer::GraphicsPassState::ColorMask colorMask = { true, true, true, true }; - if (forceUpdate || colorMask != state.colorMask) { - state.colorMask = colorMask; + if (forceUpdate || colorMask != state.colorMask[0]) { + state.colorMask[0] = colorMask; f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); } const bool blendEnabled = false; - if (forceUpdate || blendEnabled != state.blendEnabled) { - state.blendEnabled = blendEnabled; + if (forceUpdate || blendEnabled != state.blendEnabled[0]) { + state.blendEnabled[0] = blendEnabled; f->glDisable(GL_BLEND); } } diff --git a/src/gui/rhi/qrhigles2_p.h b/src/gui/rhi/qrhigles2_p.h index 9458ae92bd5..f183bc5ae60 100644 --- a/src/gui/rhi/qrhigles2_p.h +++ b/src/gui/rhi/qrhigles2_p.h @@ -575,8 +575,8 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer bool cullFace; GLenum cullMode; GLenum frontFace; - bool blendEnabled; - struct ColorMask { bool r, g, b, a; } colorMask; + bool blendEnabled[16]; + struct ColorMask { bool r, g, b, a; } colorMask[16]; struct Blend { GLenum srcColor; GLenum dstColor; @@ -584,7 +584,7 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer GLenum dstAlpha; GLenum opColor; GLenum opAlpha; - } blend; + } blend[16]; bool depthTest; bool depthWrite; GLenum depthFunc; diff --git a/tests/auto/gui/rhi/qrhi/data/buildshaders.bat b/tests/auto/gui/rhi/qrhi/data/buildshaders.bat index fe40459719a..284ca8ba25e 100644 --- a/tests/auto/gui/rhi/qrhi/data/buildshaders.bat +++ b/tests/auto/gui/rhi/qrhi/data/buildshaders.bat @@ -11,6 +11,8 @@ qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.vert.qsb textured. qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.frag.qsb textured.frag qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured_multiubuf.vert.qsb textured_multiubuf.vert qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured_multiubuf.frag.qsb textured_multiubuf.frag +qsb --glsl 320es,400 --hlsl 50 -c --msl 12 -o mrtbl.vert.qsb mrtbl.vert +qsb --glsl 320es,400 --hlsl 50 -c --msl 12 -o mrtbl.frag.qsb mrtbl.frag qsb --glsl 320es,410 --msl 12 --msltess simpletess.vert -o simpletess.vert.qsb qsb --glsl 320es,410 --msl 12 --tess-mode triangles simpletess.tesc -o simpletess.tesc.qsb qsb --glsl 320es,410 --msl 12 --tess-vertex-count 3 simpletess.tese -o simpletess.tese.qsb diff --git a/tests/auto/gui/rhi/qrhi/data/mrtbl.frag b/tests/auto/gui/rhi/qrhi/data/mrtbl.frag new file mode 100644 index 00000000000..bfce2c81eaa --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/mrtbl.frag @@ -0,0 +1,10 @@ +#version 440 + +layout(location = 0) out vec4 fragColor0; +layout(location = 1) out vec4 fragColor1; + +void main() +{ + fragColor0 = vec4(1.0, 0.0, 0.0, 0.5); + fragColor1 = vec4(1.0, 0.0, 0.0, 0.5); +} diff --git a/tests/auto/gui/rhi/qrhi/data/mrtbl.frag.qsb b/tests/auto/gui/rhi/qrhi/data/mrtbl.frag.qsb new file mode 100644 index 00000000000..78180b80781 Binary files /dev/null and b/tests/auto/gui/rhi/qrhi/data/mrtbl.frag.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/data/mrtbl.vert b/tests/auto/gui/rhi/qrhi/data/mrtbl.vert new file mode 100644 index 00000000000..16ee61beca3 --- /dev/null +++ b/tests/auto/gui/rhi/qrhi/data/mrtbl.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/mrtbl.vert.qsb b/tests/auto/gui/rhi/qrhi/data/mrtbl.vert.qsb new file mode 100644 index 00000000000..92d16de42b1 Binary files /dev/null and b/tests/auto/gui/rhi/qrhi/data/mrtbl.vert.qsb differ diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp index 8929b69cec5..9129bbdbf9d 100644 --- a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp +++ b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp @@ -124,6 +124,8 @@ private slots: void resourceUpdateBatchBufferTextureWithSwapchainFrames(); void textureRenderTargetAutoRebuild_data(); void textureRenderTargetAutoRebuild(); + void renderToMRTPerRenderTargetBlending(); + void renderToMRTPerRenderTargetBlending_data(); void pipelineCache_data(); void pipelineCache(); @@ -4372,6 +4374,152 @@ void tst_QRhi::textureRenderTargetAutoRebuild() } } + +void tst_QRhi::renderToMRTPerRenderTargetBlending_data() +{ + rhiTestData(); +} + +static QRhiGraphicsPipeline *createMRTBLPipeline(QRhi *rhi, QRhiShaderResourceBindings *srb, QRhiRenderPassDescriptor *rpDesc, QRhiGraphicsPipeline::TargetBlend blend[2]) +{ + std::unique_ptr pipeline(rhi->newGraphicsPipeline()); + QShader vs = loadShader(":/data/mrtbl.vert.qsb"); + if (!vs.isValid()) + return nullptr; + QShader fs = loadShader(":/data/mrtbl.frag.qsb"); + if (!fs.isValid()) + return nullptr; + 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); + pipeline->setRenderPassDescriptor(rpDesc); + pipeline->setTargetBlends({blend[0], blend[1]}); + return pipeline->create() ? pipeline.release() : nullptr; +} + +void tst_QRhi::renderToMRTPerRenderTargetBlending() +{ + QFETCH(QRhi::Implementation, impl); + QFETCH(QRhiInitParams *, initParams); + + QScopedPointer rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr)); + if (!rhi) + QSKIP("QRhi could not be created, skipping testing rendering"); + if (!rhi->isFeatureSupported(QRhi::PerRenderTargetBlending)) + QSKIP("QRhi::PerRenderTargetBlending not supported, skipping testing rendering"); + + const QSize outputSize(1920, 1080); + QScopedPointer texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1, + QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); + QVERIFY(texture->create()); + + QScopedPointer texture2(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1, + QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); + QVERIFY(texture2->create()); + + QRhiTextureRenderTargetDescription desc; + desc.setColorAttachments({{texture.data()}, {texture2.data()}}); + QScopedPointer rt(rhi->newTextureRenderTarget(desc)); + 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(); + + QScopedPointer vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices))); + QVERIFY(vbuf->create()); + updates->uploadStaticBuffer(vbuf.data(), triangleVertices); + + QScopedPointer srb(rhi->newShaderResourceBindings()); + QVERIFY(srb->create()); + + QRhiGraphicsPipeline::TargetBlend blend[2]; + blend[0].enable = true; // Use src-over for first rt + blend[0].srcColor = QRhiGraphicsPipeline::One; + blend[0].dstColor = QRhiGraphicsPipeline::Zero; + blend[0].srcAlpha = QRhiGraphicsPipeline::One; + blend[0].dstAlpha = QRhiGraphicsPipeline::Zero; + blend[1].enable = true; // Use custom blending second rt + blend[1].srcColor = QRhiGraphicsPipeline::Zero; + blend[1].dstColor = QRhiGraphicsPipeline::OneMinusSrcColor; + blend[1].srcAlpha = QRhiGraphicsPipeline::One; + blend[1].dstAlpha = QRhiGraphicsPipeline::Zero; + + QScopedPointer pipeline(createMRTBLPipeline(rhi.data(), srb.data(), rpDesc.data(), blend)); + QVERIFY(pipeline); + + cb->beginPass(rt.data(), Qt::white, { 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 readResult1; + QImage result1; + readResult1.completed = [&readResult1, &result1] { + result1 = QImage(reinterpret_cast(readResult1.data.constData()), + readResult1.pixelSize.width(), readResult1.pixelSize.height(), + QImage::Format_RGBA8888_Premultiplied); // non-owning, no copy needed because readResult outlives result + }; + QRhiReadbackResult readResult2; + QImage result2; + readResult2.completed = [&readResult2, &result2] { + result2 = QImage(reinterpret_cast(readResult2.data.constData()), + readResult2.pixelSize.width(), readResult2.pixelSize.height(), + QImage::Format_RGBA8888_Premultiplied); // non-owning, no copy needed because readResult outlives result + }; + QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch(); + readbackBatch->readBackTexture({ texture.data() }, &readResult1); + readbackBatch->readBackTexture({ texture2.data() }, &readResult2); + cb->endPass(readbackBatch); + + rhi->endOffscreenFrame(); + + QCOMPARE(result1.size(), texture->pixelSize()); + QCOMPARE(result2.size(), texture2->pixelSize()); + + if (impl == QRhi::Null) + return; + + // Now we have a red triangle in result1 and an 'electric blue' triangle in the result2 + // which are complementary colors. If we sum them up they should return all white. + + int y = result1.size().height() / 2; + const quint32 *p1 = reinterpret_cast(result1.constScanLine(y)); + const quint32 *p2 = reinterpret_cast(result2.constScanLine(y)); + + const int width = result1.size().width(); + for (int i = 0; i < width; i++) { + QRgb c1(*p1++); + QRgb c2(*p2++); + const bool c1white = (qAbs(qRed(c1) - 255) <= 1 && qAbs(qGreen(c1) - 255) <= 1 && qAbs(qBlue(c1) - 255) <= 1); + const bool c2white = (qAbs(qRed(c2) - 255) <= 1 && qAbs(qGreen(c2) - 255) <= 1 && qAbs(qBlue(c2) - 255) <= 1); + + // skip if both pixels are white + if (c1white && c2white) + continue; + + // remove the chance that either color accounts + // for all the value in the color sum + if (c1white || c2white) + QFAIL("If both colors are not white then neither can be white."); + + if (qAbs((qRed(c1) + qRed(c2) - 255)) > 1 + || qAbs((qGreen(c1) + qGreen(c2) - 255)) > 1 + || qAbs((qBlue(c1) + qBlue(c2) - 255)) > 1) { + QFAIL("Colors in resulting images are not complementary"); + } + } +} + void tst_QRhi::srbLayoutCompatibility_data() { rhiTestData();