[Feat] (MobileGL): full dual-source blending across state, transpiler, and both backends

Wire GL_SRC1_* dual-source blend factors (glBlendFunc) end to end with the
glBindFragDataLocationIndexed color index, so a fragment shader can drive both
dual-source blend inputs.

State + converters:
  - RenderState BlendFactor gains Src1Color/OneMinusSrc1Color/Src1Alpha/
    OneMinusSrc1Alpha; GLToMG/MGToGL/MGToVk/MGToStr converters map them to
    GL_SRC1_*, VK_BLEND_FACTOR_SRC1_*, and readable names.

Transpiler layout(index = N):
  - ProgramAttrib carries explicitFragmentOutIndices; ProgramObject threads
    m_explicitFragDataIndex into it at both link sites.
  - TMglGlslIoResolver applies the color index as TQualifier.layoutIndex on the
    fragment output, emitting layout(index = 1) via the glslang Index decoration
    -> SPIRV-Cross path. Only the non-zero (dual-source) index is emitted: index 0
    is the GL default and an explicit "index = 0" would demand
    GL_EXT_blend_func_extended on GLES for ordinary single-source outputs.

Feature detection, POST, and hard-fail at use time (no silent fallback):
  - Vulkan: dualSrcBlend is detected at device creation and cached; a draw whose
    enabled blend state uses a SRC1 factor without the feature throws at pipeline
    build with the reason and a pointer to the POST row.
  - GLES: GL_EXT_blend_func_extended detected at load into
    GLESCapabilities.SupportsDualSourceBlend; a draw enabling blend with a SRC1
    factor without it throws in the blend-state sync with the same guidance.
  - DriverPost adds a dual-source-blend row for both backends (Pass/Warn).

Tests:
  - ProgramTest.CompileAndLinkWithExplicitFragmentOut now asserts the transpiled
    fragment shader carries layout(location = 0, index = 1) after a re-link with
    glBindFragDataLocationIndexed(index 1), and still omits any index qualifier
    for the plain index-0 output.
This commit is contained in:
2026-07-11 01:32:07 -04:00
parent e9fa99e16b
commit 0f99d93300
16 changed files with 154 additions and 4 deletions
@@ -742,6 +742,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_disjoint_timer_query") == 0) {
caps.SupportsDisjointTimerQuery = true;
}
if (std::strcmp(extension, "GL_EXT_blend_func_extended") == 0) {
caps.SupportsDualSourceBlend = true;
}
}
}
@@ -754,6 +757,8 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glColorMaskiOES != nullptr;
MGLOG_I(" glPolygonMode (NV/ANGLE): %s", caps.SupportsPolygonMode ? "yes" : "no");
MGLOG_I(" indexed glColorMaski: %s", caps.SupportsIndexedColorMask ? "yes" : "no");
MGLOG_I(" dual-source blend (EXT_blend_func_extended): %s",
caps.SupportsDualSourceBlend ? "yes" : "no");
MGLOG_I("OpenGL ES capabilities:");
glesFuncs.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &caps.UniformBufferOffsetAlignment);
@@ -1040,6 +1040,10 @@ namespace MobileGL {
// An indexed glColorMaski entry point loaded (GLES 3.2 core, or GL_OES/EXT_draw_buffers_indexed).
// Without it, only the non-indexed glColorMask (draw buffer 0 broadcast) is available.
Bool SupportsIndexedColorMask = false;
// GL_EXT_blend_func_extended is present: the driver accepts GL_SRC1_* dual-source blend
// factors and layout(index = 1) fragment outputs. GLES core has no dual-source blending,
// so without this a draw using a SRC1 factor cannot proceed.
Bool SupportsDualSourceBlend = false;
// GL_RENDERER contains "ANGLE".
Bool IsAngleRenderer = false;
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
@@ -40,6 +40,14 @@ namespace MobileGL {
return BlendFactor::ConstantAlpha;
case GL_ONE_MINUS_CONSTANT_ALPHA:
return BlendFactor::OneMinusConstantAlpha;
case GL_SRC1_COLOR:
return BlendFactor::Src1Color;
case GL_ONE_MINUS_SRC1_COLOR:
return BlendFactor::OneMinusSrc1Color;
case GL_SRC1_ALPHA:
return BlendFactor::Src1Alpha;
case GL_ONE_MINUS_SRC1_ALPHA:
return BlendFactor::OneMinusSrc1Alpha;
default:
return BlendFactor::Unknown;
}
@@ -41,6 +41,14 @@ namespace MobileGL {
return GL_CONSTANT_ALPHA;
case BlendFactor::OneMinusConstantAlpha:
return GL_ONE_MINUS_CONSTANT_ALPHA;
case BlendFactor::Src1Color:
return GL_SRC1_COLOR;
case BlendFactor::OneMinusSrc1Color:
return GL_ONE_MINUS_SRC1_COLOR;
case BlendFactor::Src1Alpha:
return GL_SRC1_ALPHA;
case BlendFactor::OneMinusSrc1Alpha:
return GL_ONE_MINUS_SRC1_ALPHA;
default:
return GL_UNKNOWN_MGL;
}
@@ -40,6 +40,14 @@ namespace MobileGL {
return "ConstantAlpha";
case BlendFactor::OneMinusConstantAlpha:
return "OneMinusConstantAlpha";
case BlendFactor::Src1Color:
return "Src1Color";
case BlendFactor::OneMinusSrc1Color:
return "OneMinusSrc1Color";
case BlendFactor::Src1Alpha:
return "Src1Alpha";
case BlendFactor::OneMinusSrc1Alpha:
return "OneMinusSrc1Alpha";
default:
return "Unknown";
}
@@ -175,6 +175,14 @@ namespace MobileGL {
return VK_BLEND_FACTOR_CONSTANT_ALPHA;
case BlendFactor::OneMinusConstantAlpha:
return VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA;
case BlendFactor::Src1Color:
return VK_BLEND_FACTOR_SRC1_COLOR;
case BlendFactor::OneMinusSrc1Color:
return VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR;
case BlendFactor::Src1Alpha:
return VK_BLEND_FACTOR_SRC1_ALPHA;
case BlendFactor::OneMinusSrc1Alpha:
return VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA;
default:
return VK_BLEND_FACTOR_ONE;
}
+7
View File
@@ -248,6 +248,13 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("Indexed color mask",
"no indexed glColorMaski; per-draw-buffer color masks fall back to draw buffer 0");
}
if (caps.SupportsDualSourceBlend) {
builder.Pass("Dual-source blend",
"GL_SRC1_* dual-source blend factors available via GL_EXT_blend_func_extended");
} else {
builder.Warn("Dual-source blend",
"no GL_EXT_blend_func_extended; GL_SRC1_* dual-source blend factors hard-fail at draw");
}
if (es31) {
GLint maxVertexSsboBlocks = 0;
@@ -205,6 +205,7 @@ namespace MobileGL {
resolver =
MakeUnique<TMglGlslIoResolver>(*program, (EShLanguage)stage, attrib.explicitVertexInLocations,
attrib.explicitFragmentOutLocations,
attrib.explicitFragmentOutIndices,
attrib.explicitOpaqueUniformBindings);
break;
}
@@ -31,6 +31,9 @@ namespace MobileGL {
Vector<SharedPtr<glslang::TShader>> shaders;
UnorderedMap<String, Uint> explicitVertexInLocations;
UnorderedMap<String, Uint> explicitFragmentOutLocations;
// Dual-source blend color index per fragment output (glBindFragDataLocationIndexed) ->
// emitted as layout(index = N).
UnorderedMap<String, Uint> explicitFragmentOutIndices;
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
};
@@ -76,6 +76,15 @@ namespace MobileGL {
auto& writableType = ent.symbol->getWritableType();
writableType.getQualifier().layoutLocation = it->second;
}
// Dual-source blend color index (glBindFragDataLocationIndexed) -> layout(index = N).
// Only the non-zero (dual-source) index is emitted: index 0 is the GL default, and
// emitting an explicit "index = 0" qualifier would demand GL_EXT_blend_func_extended on
// GLES even for ordinary single-source fragment outputs.
auto idxIt = m_explicitFragOutIndices.find(name.c_str());
if (idxIt != m_explicitFragOutIndices.end() && idxIt->second != 0) {
auto& writableType = ent.symbol->getWritableType();
writableType.getQualifier().layoutIndex = idxIt->second;
}
}
if (ShouldAssignPlainUniformLocation(type)) {
const int size = glslang::TIntermediate::computeTypeUniformLocationSize(type);
@@ -26,13 +26,15 @@ namespace MobileGL {
public:
using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
const ExplicitVarSlotMap& fragOuts, ExplicitVarSlotMap* opaqueUniformBindings)
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
ExplicitVarSlotMap* opaqueUniformBindings)
: TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts),
m_explicitOpaqueUniformBindings(opaqueUniformBindings) {}
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings) {}
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
ExplicitVarSlotMap* opaqueUniformBindings)
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, opaqueUniformBindings) {}
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings)
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices,
opaqueUniformBindings) {}
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
int resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
@@ -43,6 +45,7 @@ namespace MobileGL {
const ExplicitVarSlotMap& m_explicitVertexIns;
const ExplicitVarSlotMap& m_explicitFragOuts;
const ExplicitVarSlotMap& m_explicitFragOutIndices;
ExplicitVarSlotMap* m_explicitOpaqueUniformBindings = nullptr;
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
std::map<glslang::TString, int> m_plainUniformLocationByName;