Add support for per render target blending for OpenGL

Per render target blending is required for WeightedBlended order
independent transparency method.

Also add an autotest for the feature.

Task-number: QTBUG-130334
Change-Id: Iae1bd75c917f0f1f8314e57091d3f71dc4ffd92a
Reviewed-by: Laszlo Agocs <laszlo.agocs@qt.io>
This commit is contained in:
Antti Määttä 2024-08-27 08:50:12 +03:00
parent c06c8eba50
commit 5921ca95bd
9 changed files with 230 additions and 46 deletions

View File

@ -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.
*/

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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

View File

@ -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);
}

Binary file not shown.

View File

@ -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.

View File

@ -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<QRhiGraphicsPipeline> 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<QRhi> 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<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QScopedPointer<QRhiTexture> texture2(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture2->create());
QRhiTextureRenderTargetDescription desc;
desc.setColorAttachments({{texture.data()}, {texture2.data()}});
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(desc));
QScopedPointer<QRhiRenderPassDescriptor> 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<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
QScopedPointer<QRhiShaderResourceBindings> 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<QRhiGraphicsPipeline> 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<const uchar *>(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<const uchar *>(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<const quint32 *>(result1.constScanLine(y));
const quint32 *p2 = reinterpret_cast<const quint32 *>(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();