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:
parent
c06c8eba50
commit
5921ca95bd
@ -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.
|
||||
*/
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
10
tests/auto/gui/rhi/qrhi/data/mrtbl.frag
Normal file
10
tests/auto/gui/rhi/qrhi/data/mrtbl.frag
Normal 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);
|
||||
}
|
BIN
tests/auto/gui/rhi/qrhi/data/mrtbl.frag.qsb
Normal file
BIN
tests/auto/gui/rhi/qrhi/data/mrtbl.frag.qsb
Normal file
Binary file not shown.
10
tests/auto/gui/rhi/qrhi/data/mrtbl.vert
Normal file
10
tests/auto/gui/rhi/qrhi/data/mrtbl.vert
Normal file
@ -0,0 +1,10 @@
|
||||
#version 440
|
||||
|
||||
layout(location = 0) in vec4 position;
|
||||
|
||||
out gl_PerVertex { vec4 gl_Position; };
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = position;
|
||||
}
|
BIN
tests/auto/gui/rhi/qrhi/data/mrtbl.vert.qsb
Normal file
BIN
tests/auto/gui/rhi/qrhi/data/mrtbl.vert.qsb
Normal file
Binary file not shown.
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user