mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (Blend): decline an unsupported dual-source blend instead of throwing through the GL ABI
This commit is contained in:
@@ -1772,6 +1772,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits
|
||||
// the clear's undoctored mask.
|
||||
static Uint32 g_syncedColorMaskAlphaWidenMask = 0;
|
||||
// Scratch for the dual-source-blend decline path in the blend block below. File-scope
|
||||
// rather than a local so the ordinary draw pays nothing for it: it is written only on a
|
||||
// driver with no GL_EXT_blend_func_extended that is also handed a GL_SRC1_* factor, and
|
||||
// SyncRenderState runs on the GL thread only.
|
||||
static Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS>
|
||||
g_dualSourceDeclinedBlendStates;
|
||||
void InvalidateSyncedRenderState() {
|
||||
g_forceFullRenderStateResync = true;
|
||||
g_hasSyncedRenderState = false;
|
||||
@@ -1931,31 +1937,66 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; };
|
||||
|
||||
// Which draw buffers the blend block below DECLINED (see it for why). Needed again at
|
||||
// the shadow write-back at the end of this function: the span memcpy there clones the
|
||||
// FRONTEND block, which for a declined draw buffer is not what the driver was handed.
|
||||
Uint32 dualSourceDeclinedMask = 0;
|
||||
|
||||
if (blendSpanDirty) { // Blend State
|
||||
using FBO = MG_State::GLState::FramebufferObject;
|
||||
const auto& targetStates = parameters.BlendStates;
|
||||
auto& syncedStates = g_syncedRenderStateParameters.BlendStates;
|
||||
|
||||
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
|
||||
// glBindFragDataLocationIndexed) needs GL_EXT_blend_func_extended; GLES core has none.
|
||||
// Detected at load and surfaced in the POST. There is no fallback, so if a draw actually
|
||||
// enables blending with a SRC1 factor on a driver that lacks it, hard-fail here at use
|
||||
// time rather than let the driver reject glBlendFuncSeparate and silently mis-blend.
|
||||
// Detected at load and surfaced in the POST. There is no fallback that BLENDS
|
||||
// correctly, so a draw that asks for a SRC1 factor on a driver without the extension
|
||||
// gets the blend DECLINED: that draw buffer is pushed with blending off and neutral
|
||||
// One/Zero factors, and the loss is logged once. The two rejected alternatives are
|
||||
// both worse - pushing GL_SRC1_* at glBlendFuncSeparate leaves the driver to raise
|
||||
// GL_INVALID_ENUM and keep whatever factors were there before (a silent mis-blend
|
||||
// against stale state), and throwing, which is what this did until now, takes the
|
||||
// whole process down over one unsupported blend factor. Declining is defined,
|
||||
// survivable and visible in the log.
|
||||
const auto* effectiveBlendStates = ¶meters.BlendStates;
|
||||
if (!g_GLESCapabilities.SupportsDualSourceBlend) {
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
const auto& s = targetStates[i];
|
||||
const auto& s = parameters.BlendStates[i];
|
||||
if (s.Enabled &&
|
||||
(IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) ||
|
||||
IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha))) {
|
||||
THROW_EXCEPTION(
|
||||
"Dual-source blending (GL_SRC1_* blend factor) was used on draw buffer " +
|
||||
std::to_string(i) +
|
||||
", but the GLES driver does not expose GL_EXT_blend_func_extended (see the "
|
||||
"dual-source blend row in the driver POST). No fallback exists; the draw "
|
||||
"cannot proceed.");
|
||||
dualSourceDeclinedMask |= 1u << i;
|
||||
}
|
||||
}
|
||||
if (dualSourceDeclinedMask != 0) {
|
||||
MGLOG_E_ONCE(
|
||||
"SyncRenderState: dual-source blending (GL_SRC1_* blend factor) was requested on "
|
||||
"draw buffer mask 0x%x, but the GLES driver does not expose "
|
||||
"GL_EXT_blend_func_extended (see the dual-source blend row in the driver POST). "
|
||||
"Blending is DECLINED on those draw buffers - the fragment's first output is "
|
||||
"written unblended and the second source is dropped.",
|
||||
dualSourceDeclinedMask);
|
||||
g_dualSourceDeclinedBlendStates = parameters.BlendStates;
|
||||
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
|
||||
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
|
||||
auto& s = g_dualSourceDeclinedBlendStates[i];
|
||||
s.Enabled = false;
|
||||
// Neutral factors as well as the disable: the factor push below is not
|
||||
// gated on Enabled (one glBlendFuncSeparate serves every draw buffer when
|
||||
// they agree), so leaving Src1Color here would still hand the driver a
|
||||
// GL_SRC1_* enum it cannot parse.
|
||||
s.SrcFactorRGB = BlendFactor::One;
|
||||
s.DstFactorRGB = BlendFactor::Zero;
|
||||
s.SrcFactorAlpha = BlendFactor::One;
|
||||
s.DstFactorAlpha = BlendFactor::Zero;
|
||||
}
|
||||
effectiveBlendStates = &g_dualSourceDeclinedBlendStates;
|
||||
}
|
||||
}
|
||||
// The rest of the block reads the EFFECTIVE state. The per-field writes it makes
|
||||
// into `syncedStates` are provisional - the span memcpy at the end of this function
|
||||
// overwrites the whole blend span with the frontend's own bytes - so the declined
|
||||
// draw buffers are put back there, see the write-back below.
|
||||
const auto& targetStates = *effectiveBlendStates;
|
||||
|
||||
Bool allEnabled = true;
|
||||
Bool allDisabled = true;
|
||||
@@ -2355,6 +2396,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (blendSpanDirty) {
|
||||
std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin,
|
||||
kBlendSpanEnd - kBlendSpanBegin);
|
||||
// ...except for a draw buffer whose dual-source blend was DECLINED, where the
|
||||
// frontend block is precisely what did NOT reach the driver. The shadow has to hold
|
||||
// what was pushed or the next diff compares against state the ES context never got:
|
||||
// going from a SRC1 factor to an ordinary one leaves Enabled equal on both sides,
|
||||
// the enable block finds nothing to do, and blending stays off from the decline.
|
||||
// The span stays permanently "dirty" against the frontend as a result, which costs
|
||||
// one memcmp plus this block per render-state VERSION change - the top-of-function
|
||||
// version early-out still skips repeat draws entirely.
|
||||
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
||||
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
|
||||
g_syncedRenderStateParameters.BlendStates[i] = g_dualSourceDeclinedBlendStates[i];
|
||||
}
|
||||
}
|
||||
if (tailSpanDirty) {
|
||||
std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd,
|
||||
|
||||
@@ -5533,18 +5533,32 @@ void main() {
|
||||
}
|
||||
}
|
||||
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
|
||||
// glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected at
|
||||
// device creation and surfaced in the POST; if a shader actually issues a draw with a SRC1
|
||||
// factor on a device that lacks it, there is no fallback, so hard-fail here at use time
|
||||
// rather than silently mistranslating the blend equation.
|
||||
if (effectiveBlendEnabled && !m_dualSrcBlendFeatureEnabled &&
|
||||
// glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected
|
||||
// at device creation and surfaced in the POST; there is no fallback that BLENDS correctly,
|
||||
// so a draw that asks for a SRC1 factor on a device without the feature gets the blend
|
||||
// DECLINED - this attachment is baked with blending off and neutral One/Zero factors, and
|
||||
// the loss is logged once. Both the factors AND the enable have to be neutralised:
|
||||
// VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three
|
||||
// siblings forbid a VK_BLEND_FACTOR_SRC1_* in the struct without the feature whatever
|
||||
// blendEnable says, so clearing only the enable would still be invalid pipeline state.
|
||||
// The previous behaviour, throwing, took the whole process down over one unsupported
|
||||
// blend factor; this is defined, survivable and visible in the log, and it matches what
|
||||
// the non-blendable-format arm above already does.
|
||||
if (!m_dualSrcBlendFeatureEnabled &&
|
||||
(IsDualSourceBlendFactor(srcRGB) || IsDualSourceBlendFactor(dstRGB) ||
|
||||
IsDualSourceBlendFactor(srcAlpha) || IsDualSourceBlendFactor(dstAlpha))) {
|
||||
THROW_EXCEPTION(
|
||||
"Dual-source blending (GL_SRC1_* blend factor) was used on color attachment " +
|
||||
std::to_string(i) +
|
||||
", but the Vulkan device does not support the dualSrcBlend feature (see the "
|
||||
"dualSrcBlend row in the driver POST). No fallback exists; the draw cannot proceed.");
|
||||
MGLOG_E_ONCE(
|
||||
"GetOrCreatePipeline: dual-source blending (GL_SRC1_* blend factor) was requested on "
|
||||
"color attachment %u, but the Vulkan device does not support the dualSrcBlend feature "
|
||||
"(see the dualSrcBlend row in the driver POST). Blending is DECLINED on that "
|
||||
"attachment - the fragment's first output is written unblended and the second source "
|
||||
"is dropped (program=%u)",
|
||||
i, program.GetExternalIndex());
|
||||
effectiveBlendEnabled = false;
|
||||
srcRGB = BlendFactor::One;
|
||||
dstRGB = BlendFactor::Zero;
|
||||
srcAlpha = BlendFactor::One;
|
||||
dstAlpha = BlendFactor::Zero;
|
||||
}
|
||||
payload.colorBlendAttachments[i] = MakeColorBlendAttachmentState(
|
||||
effectiveBlendEnabled,
|
||||
|
||||
@@ -115,6 +115,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/IntegerBorderColorScenario.cpp
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DualSourceBlendScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A DUAL-SOURCE BLEND DRAW HAS TO SURVIVE ON EVERY DRIVER.
|
||||
//
|
||||
// GL_SRC1_COLOR / GL_ONE_MINUS_SRC1_COLOR / GL_SRC1_ALPHA / GL_ONE_MINUS_SRC1_ALPHA
|
||||
// (ARB_blend_func_extended, core since 3.3) need a backend capability that not every device has:
|
||||
// GL_EXT_blend_func_extended on the ES driver, or the dualSrcBlend device feature on Vulkan. When
|
||||
// the capability IS there both backends translate the factors properly, and that has always
|
||||
// worked. When it is NOT, both backends used to THROW_EXCEPTION at draw time - and
|
||||
// MG_Util/Types.h's THROW_EXCEPTION is a plain `throw`, with no catch anywhere in MG_Impl or
|
||||
// MG_Backend, so the exception unwound out through the C GL ABI and killed the process. An
|
||||
// application asking for a blend factor the device cannot do is a picture problem, never a reason
|
||||
// to take the process down.
|
||||
//
|
||||
// Both are now a DECLINE: the attachment is drawn with blending off and neutral One/Zero factors,
|
||||
// and the loss is logged once. So a dual-source draw has exactly two defined outcomes, and this
|
||||
// scenario pins that it lands on one of them and never on a crash:
|
||||
//
|
||||
// capability present - src0 * src1 + dst * (1 - src1)
|
||||
// capability absent - src0, written straight through
|
||||
//
|
||||
// On the CI runners (llvmpipe / lavapipe) both capabilities are normally present, so what CI
|
||||
// exercises here is the working path plus the fact that the whole sequence is crash-free; the
|
||||
// decline arm is what the same code does on a device without the capability, and it is asserted
|
||||
// by value rather than assumed.
|
||||
//
|
||||
// The Vulkan half has a second edge the last case covers: the dual-source VUIDs
|
||||
// (VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three siblings)
|
||||
// forbid a VK_BLEND_FACTOR_SRC1_* anywhere in VkPipelineColorBlendAttachmentState without the
|
||||
// feature, whatever blendEnable says - so leaving the factors in place while clearing the enable
|
||||
// would still be invalid pipeline state.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 16;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Two outputs on the SAME location, indices 0 and 1: the shader-side spelling of
|
||||
// dual-source output (GLSL 3.30 4.4.2, the `index` layout qualifier). No
|
||||
// glBindFragDataLocationIndexed needed, which keeps the program buildable through the
|
||||
// harness's compile-and-link helper.
|
||||
constexpr const char* kDualSourceFragmentSource = R"(#version 330 core
|
||||
uniform vec4 uSrc0;
|
||||
uniform vec4 uSrc1;
|
||||
layout(location = 0, index = 0) out vec4 fragColor0;
|
||||
layout(location = 0, index = 1) out vec4 fragColor1;
|
||||
void main()
|
||||
{
|
||||
fragColor0 = uSrc0;
|
||||
fragColor1 = uSrc1;
|
||||
}
|
||||
)";
|
||||
|
||||
class DualSourceBlendScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kExtent, kExtent);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kDualSourceFragmentSource, &error);
|
||||
m_programError = error;
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
}
|
||||
|
||||
void Draw(float src0, float src1) {
|
||||
glUseProgram(m_program);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uSrc0"), src0, src0, src0, 1.0f);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uSrc1"), src1, src1, src1, 1.0f);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
std::string m_programError;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The whole point of the scenario: this sequence used to be a process kill on any device
|
||||
// without the capability, and it has to be a picture either way.
|
||||
//
|
||||
// dst is black, src0 is white and src1 is mid-grey, with SRC1_COLOR / ONE_MINUS_SRC1_COLOR.
|
||||
// blended = 1.0 * 0.5 + 0.0 * 0.5 = 0.5 -> ~128
|
||||
// declined = 1.0 -> 255
|
||||
// Anything else means the factors were mistranslated rather than either honoured or declined.
|
||||
TEST_F(DualSourceBlendScenario, DualSourceBlendDrawProducesOneOfTheTwoDefinedResults) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (m_program == 0) {
|
||||
GTEST_SKIP() << "this driver cannot build a dual-source fragment shader: " << m_programError;
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glBlendFunc must accept the GL_SRC1_* factors - they are core since 3.3";
|
||||
Draw(/*src0=*/1.0f, /*src1=*/0.5f);
|
||||
glFinish();
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dual-source draw left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
const int red = static_cast<int>(centre.r);
|
||||
const bool blended = red > 100 && red < 160;
|
||||
const bool declined = red > 245;
|
||||
EXPECT_TRUE(blended || declined)
|
||||
<< "got " << centre << ", which is neither the dual-source blend (~128) nor the declined "
|
||||
<< "straight-through source (255) - the SRC1 factors were mistranslated";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same factors with blending DISABLED. Nothing may blend, and on the Vulkan side nothing
|
||||
// may reach VkPipelineColorBlendAttachmentState carrying a VK_BLEND_FACTOR_SRC1_* on a device
|
||||
// without dualSrcBlend - the VUIDs bind to the struct, not to blendEnable. The picture is the
|
||||
// source either way, so this case is really "no crash, no error, no surprise".
|
||||
TEST_F(DualSourceBlendScenario, DualSourceFactorsWithBlendingDisabledJustWriteTheSource) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (m_program == 0) {
|
||||
GTEST_SKIP() << "this driver cannot build a dual-source fragment shader: " << m_programError;
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
Draw(/*src0=*/0.0f, /*src1=*/0.0f);
|
||||
|
||||
glBlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
Draw(/*src0=*/1.0f, /*src1=*/0.25f);
|
||||
glFinish();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "a draw with SRC1 factors and blending off left a GL error behind";
|
||||
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty());
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
EXPECT_GT(static_cast<int>(centre.r), 245)
|
||||
<< "got " << centre << ": blending is disabled, so the source has to be written straight through";
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -1034,14 +1034,69 @@ namespace {
|
||||
if (index < kRecordedDrawBuffers) g_driverIndexedColorMasks[index] = {true, r, g, b, a};
|
||||
}
|
||||
|
||||
// What the blend block of SyncRenderState pushed. Enough to answer the two questions the
|
||||
// dual-source cases ask: is blending on for a draw buffer, and which factor enums reached
|
||||
// the driver.
|
||||
struct RecordedBlend {
|
||||
Bool enabled = false;
|
||||
Bool enableSeen = false;
|
||||
Bool factorsSeen = false;
|
||||
GLenum srcRGB = 0, dstRGB = 0, srcAlpha = 0, dstAlpha = 0;
|
||||
};
|
||||
RecordedBlend g_driverBlend[kRecordedDrawBuffers];
|
||||
|
||||
void ResetRecordedBlend() {
|
||||
for (auto& recorded : g_driverBlend) recorded = {};
|
||||
}
|
||||
|
||||
void RecordBlendEnable(Bool enabled) {
|
||||
for (auto& recorded : g_driverBlend) {
|
||||
recorded.enabled = enabled;
|
||||
recorded.enableSeen = true;
|
||||
}
|
||||
}
|
||||
|
||||
void RecordBlendFactors(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
|
||||
for (auto& recorded : g_driverBlend) {
|
||||
recorded.factorsSeen = true;
|
||||
recorded.srcRGB = srcRGB;
|
||||
recorded.dstRGB = dstRGB;
|
||||
recorded.srcAlpha = srcAlpha;
|
||||
recorded.dstAlpha = dstAlpha;
|
||||
}
|
||||
}
|
||||
|
||||
void StubViewport(GLint, GLint, GLsizei, GLsizei) {}
|
||||
void StubScissor(GLint, GLint, GLsizei, GLsizei) {}
|
||||
void StubEnable(GLenum) {}
|
||||
void StubDisable(GLenum) {}
|
||||
void StubEnablei(GLenum, GLuint) {}
|
||||
void StubDisablei(GLenum, GLuint) {}
|
||||
void StubBlendFuncSeparate(GLenum, GLenum, GLenum, GLenum) {}
|
||||
void StubBlendFuncSeparatei(GLuint, GLenum, GLenum, GLenum, GLenum) {}
|
||||
void StubEnable(GLenum cap) {
|
||||
if (cap == GL_BLEND) RecordBlendEnable(true);
|
||||
}
|
||||
void StubDisable(GLenum cap) {
|
||||
if (cap == GL_BLEND) RecordBlendEnable(false);
|
||||
}
|
||||
void StubEnablei(GLenum cap, GLuint index) {
|
||||
if (cap == GL_BLEND && index < kRecordedDrawBuffers) {
|
||||
g_driverBlend[index].enabled = true;
|
||||
g_driverBlend[index].enableSeen = true;
|
||||
}
|
||||
}
|
||||
void StubDisablei(GLenum cap, GLuint index) {
|
||||
if (cap == GL_BLEND && index < kRecordedDrawBuffers) {
|
||||
g_driverBlend[index].enabled = false;
|
||||
g_driverBlend[index].enableSeen = true;
|
||||
}
|
||||
}
|
||||
void StubBlendFuncSeparate(GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
|
||||
RecordBlendFactors(srcRGB, dstRGB, srcAlpha, dstAlpha);
|
||||
}
|
||||
void StubBlendFuncSeparatei(GLuint index, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha) {
|
||||
if (index >= kRecordedDrawBuffers) return;
|
||||
g_driverBlend[index].factorsSeen = true;
|
||||
g_driverBlend[index].srcRGB = srcRGB;
|
||||
g_driverBlend[index].dstRGB = dstRGB;
|
||||
g_driverBlend[index].srcAlpha = srcAlpha;
|
||||
g_driverBlend[index].dstAlpha = dstAlpha;
|
||||
}
|
||||
void StubBlendEquationSeparate(GLenum, GLenum) {}
|
||||
void StubBlendEquationSeparatei(GLuint, GLenum, GLenum) {}
|
||||
void StubBlendColor(GLfloat, GLfloat, GLfloat, GLfloat) {}
|
||||
@@ -1066,7 +1121,9 @@ namespace {
|
||||
// into a driver that this process never made current.
|
||||
class ScopedRenderStateDriverStubs {
|
||||
public:
|
||||
ScopedRenderStateDriverStubs():
|
||||
// dualSourceBlendSupported models GL_EXT_blend_func_extended on the ES driver, which is
|
||||
// the one capability in here that a real device is commonly WITHOUT.
|
||||
explicit ScopedRenderStateDriverStubs(Bool dualSourceBlendSupported = true):
|
||||
m_funcs(MG_Backend::DirectGLES::g_GLESFuncs), m_caps(MG_Backend::DirectGLES::g_GLESCapabilities) {
|
||||
auto& gl = MG_Backend::DirectGLES::g_GLESFuncs;
|
||||
gl = MG_External::GLESFunctionsTable{};
|
||||
@@ -1102,9 +1159,10 @@ namespace {
|
||||
caps.SupportsIndexedColorMask = true;
|
||||
caps.SupportsSrgbWriteControl = false;
|
||||
caps.SupportsPolygonMode = false;
|
||||
caps.SupportsDualSourceBlend = true;
|
||||
caps.SupportsDualSourceBlend = dualSourceBlendSupported;
|
||||
|
||||
ResetRecordedColorMasks();
|
||||
ResetRecordedBlend();
|
||||
// The viewport and scissor blocks fall back to querying the surface size when the
|
||||
// frontend's rectangle is degenerate, and there is no surface in this process.
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 4, 4);
|
||||
@@ -1119,6 +1177,11 @@ namespace {
|
||||
// The shadow now describes pushes that went to the stubs, not to any driver.
|
||||
MG_Backend::DirectGLES::RenderStateImpl::InvalidateSyncedRenderState();
|
||||
MG_Impl::GLImpl::ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
// Blend state is per-CONTEXT and the context outlives the fixture, so a case that
|
||||
// enabled blending or asked for an exotic factor has to put it back or every later
|
||||
// case in this binary inherits it.
|
||||
MG_Impl::GLImpl::Disable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_ONE, GL_ZERO);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -1253,6 +1316,72 @@ TEST_F(FramebufferTest, ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffe
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// --- Dual-source blending without GL_EXT_blend_func_extended ------------------------------------
|
||||
//
|
||||
// GL_SRC1_* blend factors are core GL since 3.3, GLES core has nothing equivalent, and the ES
|
||||
// driver may or may not carry GL_EXT_blend_func_extended. When it does, the factors translate and
|
||||
// blend properly - the positive case below. When it does not, the blend block used to
|
||||
// THROW_EXCEPTION, which is a plain `throw` (MG_Util/Types.h) with no catch anywhere in MG_Impl or
|
||||
// MG_Backend, so it unwound out through the C GL ABI and killed the process over one unsupported
|
||||
// blend factor. It now DECLINES: the draw buffer is pushed with blending off and neutral One/Zero
|
||||
// factors, and the loss is logged once.
|
||||
//
|
||||
// Both halves are asserted at the seam that matters - what the ES driver is actually handed -
|
||||
// because a GL_SRC1_* enum reaching a driver without the extension is the other failure mode: the
|
||||
// driver answers GL_INVALID_ENUM, keeps whatever factors were set before, and mis-blends silently.
|
||||
|
||||
TEST_F(FramebufferTest, DualSourceBlendFactorsReachTheDriverWhenTheExtensionIsThere) {
|
||||
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/true);
|
||||
|
||||
MG_Impl::GLImpl::Enable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_COLOR, GL_ONE_MINUS_SRC1_COLOR);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_SRC1_* is core since 3.3; glBlendFunc must take it";
|
||||
ResetRecordedBlend();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
EXPECT_TRUE(g_driverBlend[0].enabled) << "nothing may decline a blend the driver can do";
|
||||
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC1_COLOR));
|
||||
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR));
|
||||
EXPECT_EQ(g_driverBlend[0].srcAlpha, static_cast<GLenum>(GL_SRC1_COLOR));
|
||||
EXPECT_EQ(g_driverBlend[0].dstAlpha, static_cast<GLenum>(GL_ONE_MINUS_SRC1_COLOR));
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing) {
|
||||
ScopedRenderStateDriverStubs driver(/*dualSourceBlendSupported=*/false);
|
||||
|
||||
MG_Impl::GLImpl::Enable(GL_BLEND);
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC1_ALPHA, GL_ONE_MINUS_SRC1_ALPHA);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
|
||||
<< "the FRONTEND accepts the factor whatever the driver can do - the decline is a backend decision";
|
||||
ResetRecordedBlend();
|
||||
|
||||
// The whole point: this used to be `throw std::runtime_error` straight through the GL ABI.
|
||||
ASSERT_NO_THROW(MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false));
|
||||
|
||||
ASSERT_TRUE(g_driverBlend[0].enableSeen) << "the blend enable still has to be pushed";
|
||||
EXPECT_FALSE(g_driverBlend[0].enabled) << "a blend the driver cannot do is declined, not attempted";
|
||||
for (Uint i = 0; i < kRecordedDrawBuffers; ++i) {
|
||||
EXPECT_NE(g_driverBlend[i].srcRGB, static_cast<GLenum>(GL_SRC1_ALPHA))
|
||||
<< "draw buffer " << i << ": no GL_SRC1_* enum may reach a driver without the extension";
|
||||
EXPECT_NE(g_driverBlend[i].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
EXPECT_NE(g_driverBlend[i].srcAlpha, static_cast<GLenum>(GL_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
EXPECT_NE(g_driverBlend[i].dstAlpha, static_cast<GLenum>(GL_ONE_MINUS_SRC1_ALPHA)) << "draw buffer " << i;
|
||||
}
|
||||
|
||||
// The decline is scoped to the offending factor, not to blending as a whole: an ordinary
|
||||
// blend on the same driver still goes through, and the SAME sync that declined the first one
|
||||
// is what has to push it.
|
||||
MG_Impl::GLImpl::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
ResetRecordedBlend();
|
||||
MG_Backend::DirectGLES::RenderStateImpl::SyncRenderState(/*forColorClear=*/false);
|
||||
ASSERT_TRUE(g_driverBlend[0].factorsSeen);
|
||||
EXPECT_TRUE(g_driverBlend[0].enabled);
|
||||
EXPECT_EQ(g_driverBlend[0].srcRGB, static_cast<GLenum>(GL_SRC_ALPHA));
|
||||
EXPECT_EQ(g_driverBlend[0].dstRGB, static_cast<GLenum>(GL_ONE_MINUS_SRC_ALPHA));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// --- glFramebufferTexture error conditions (GL 4.6 core 9.2.8) ---------------------------------
|
||||
//
|
||||
// Four of them were missing from the bound-target path while its DSA sibling
|
||||
|
||||
Reference in New Issue
Block a user