[Fix] (DirectGLES): broadcast legacy gl_FragColor to every draw buffer

Legacy GLSL's gl_FragColor goes to every enabled draw buffer (GL 4.6 15.2.3),
but ShaderSourceProcessor lowers it to a single mg_FragColor output, which only
ever reaches draw buffer 0. Everything past the first attachment kept its
pre-draw contents.

Replicated across the enabled draw buffers with copies at the end of main.
Gated on the count so the ordinary single-target shader is byte-for-byte what it
was: the pass is a no-op below two draw buffers, and the count comes from the
frontend draw framebuffer at program-sync time (not from the backend framebuffer
sync, which only runs later in PrepareForDraw - a program compiled against a
stale count would not be relinked until the draw after the one that needed it).
It joins the snorm/unorm clamp masks as framebuffer state the shader is compiled
against, with the same relink-on-change check.

Also advertises GL_ARB_explicit_attrib_location and GL_ARB_texture_multisample,
which DirectGLES implements for every version it advertises but only listed for
DirectVulkan. Both are core from GL 3.2/3.3 on, so an app targeting 3.0/3.1
reaches them only through the extension string - without the former the CTS
picks an entirely different draw_buffers shader, and without the latter
KHR-GL31.texture_size_promotion.functional crashed outright.

KHR-GL3{0,1,2,3}.draw_buffers.draw_buffers_1 now passes on all four versions,
and texture_size_promotion.functional on GL31 downgrades from a crash to a
(still open) comparison failure.
This commit is contained in:
BZLZHH
2026-08-01 13:59:11 -04:00
parent 1c5f6c0986
commit 4c7332d5e6
6 changed files with 92 additions and 1 deletions
@@ -832,6 +832,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
// Both are core from GL 3.2/3.3 on and implemented here for
// every advertised version, but an app targeting 3.0/3.1
// only reaches them through the extension string - the CTS
// picks a whole different shader for draw_buffers without
// explicit_attrib_location. DirectVulkan advertises both.
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample,
E_GL_ARB_shader_image_size};
// Only advertised when the device driver actually has usable timer queries
// (GL_EXT_disjoint_timer_query plus its entry points) and the
+19 -1
View File
@@ -1120,6 +1120,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_lastUsedBackendProgramId = 0;
return;
}
// Read from the frontend rather than from the backend framebuffer sync, which
// only runs later in PrepareForDraw: a program compiled against a stale count
// would not be relinked until the draw after the one that needed it.
{
Uint enabledDrawBuffers = 0;
if (const auto& drawFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject()) {
const auto& drawBuffers = drawFBO->GetDrawBuffers();
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
if (drawBuffers[i] != FramebufferAttachmentType::None) {
enabledDrawBuffers = i + 1;
}
}
}
g_fragColorBroadcastCount = std::max<Uint>(enabledDrawBuffers, 1);
}
const auto& backendProgramIt = g_backendProgramObjects.find(currentProgram.get());
Bool exist = (backendProgramIt != g_backendProgramObjects.end());
auto& backendObj = exist ? backendProgramIt->second : g_backendProgramObjects.GetOrCreate(currentProgram);
@@ -1133,7 +1150,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!backendObj->GetBackendProgramId() ||
backendObj->GetSyncedLinkVersion() != currentProgram->GetLinkVersion() ||
backendObj->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
backendObj->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask) {
backendObj->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
backendObj->GetFragColorBroadcastCount() != g_fragColorBroadcastCount) {
backendObj->SyncToBackend(currentProgram);
}
}
@@ -3181,6 +3181,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace PrgramImpl {
Uint32 g_snormFallbackClampOutputMask = 0;
Uint g_fragColorBroadcastCount = 1;
Uint32 g_unormFallbackClampOutputMask = 0;
Uint g_lastUsedBackendProgramId = 0;
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
@@ -3234,6 +3235,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
stateProgramObject->GetExternalIndex(), m_backendProgramId);
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
// Detach all existing shaders
GLint attachedCount = 0;
@@ -3347,6 +3349,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = RemoveLayoutBinding(source);
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
source = EmulateTextureLodBias(source);
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
@@ -596,6 +596,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -622,6 +623,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int m_indirectParamsBinding = -1;
Uint32 m_snormFallbackClampOutputMask = 0;
Uint32 m_unormFallbackClampOutputMask = 0;
// Draw buffers a legacy gl_FragColor write has to reach (see
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
Uint m_fragColorBroadcastCount = 1;
Bool m_isInitialized = false;
Int m_globalUboBackendBlockIndex = -1;
@@ -635,6 +639,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern Uint32 g_snormFallbackClampOutputMask;
extern Uint32 g_unormFallbackClampOutputMask;
// Draw buffers the current draw framebuffer enables. Like the clamp masks above it
// is framebuffer state that the shader has to be compiled against, so a program
// whose snapshot no longer matches is relinked.
extern Uint g_fragColorBroadcastCount;
// Backend id of the last glUseProgram issued through this backend; lets Use()
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
// ES context is recreated.
+49
View File
@@ -279,6 +279,55 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode;
}
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The name is the marker: ShaderSourceProcessor only emits it when the source
// wrote gl_FragColor, and such a shader can have no other output.
static const char* const kLoweredName = "mg_FragColor";
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
return glslCode;
}
static const std::regex declRegex(
R"(layout\s*\(\s*location\s*=\s*0\s*\)\s*out\s+((?:lowp|mediump|highp)\s+)?vec4\s+mg_FragColor\s*;)");
std::smatch declMatch;
if (!std::regex_search(glslCode, declMatch, declRegex)) {
return glslCode;
}
const String precision = declMatch[1].matched ? declMatch[1].str() : String();
String replicaDecls;
String replicaCopies;
for (Uint location = 1; location < drawBufferCount; ++location) {
const String name = String(kLoweredName) + "_" + std::to_string(location);
replicaDecls += "\nlayout(location = " + std::to_string(location) + ") out " + precision + "vec4 " +
name + ";";
replicaCopies += "\n " + name + " = " + kLoweredName + ";";
}
static const std::regex mainRegex(R"(void\s+main\s*\([^)]*\)\s*\{)");
std::smatch mainMatch;
if (!std::regex_search(glslCode, mainMatch, mainRegex)) {
return glslCode;
}
SizeT bracePos = static_cast<SizeT>(mainMatch.position(0) + mainMatch.length(0) - 1);
Int depth = 0;
for (SizeT pos = bracePos; pos < glslCode.size(); ++pos) {
if (glslCode[pos] == '{') {
++depth;
} else if (glslCode[pos] == '}') {
--depth;
if (depth == 0) {
glslCode.insert(pos, replicaCopies + "\n");
break;
}
}
}
glslCode.insert(static_cast<SizeT>(declMatch.position(0)) + declMatch[0].str().size(), replicaDecls);
return glslCode;
}
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
+7
View File
@@ -104,6 +104,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
// outputs and copies the value into them at the end of main. A no-op for
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
// enables several draw buffers, so the ordinary single-target shader is untouched.
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
String RemoveLayoutBinding(const String& glslCode);
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.