[Fix, Test] (DirectGLES, SelfTest, MG_Test): probe the located-interface-block defect with its controls and cover the strip in both gates

This commit is contained in:
2026-08-27 19:38:39 -04:00
parent 5dc26e3e2c
commit 23565fcacd
10 changed files with 943 additions and 198 deletions
+10
View File
@@ -142,6 +142,16 @@ namespace MobileGL::MG_Config {
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
Bool AvoidExplicitLodBias = false;
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
// match them by block name and member sequence instead. The Mali ES driver delivers
// nothing at all through a located block once a tessellation or geometry stage is in
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
// healthy driver - which is what the integration lane does, since llvmpipe and
// lavapipe carry a located block correctly and would otherwise never run this code -
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
+1
View File
@@ -180,6 +180,7 @@ namespace MobileGL::MG_ConfigLoader {
features.AvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
@@ -91,6 +91,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/SsboDeclarationFormScenario.cpp
Scenarios/Glsl420DeclarationScenario.cpp
Scenarios/IoBlockNameCollisionScenario.cpp
Scenarios/UnlocatedIoBlockScenario.cpp
Scenarios/TessellationDrawModeScenario.cpp
Scenarios/GeometryDrawModeScenario.cpp
Scenarios/PostLinkAttachScenario.cpp
@@ -352,6 +353,8 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT
"MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1" ${MGL_ITEST_COMMON_ENV})
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
set(MGL_ITEST_TIMEOUT 120)
@@ -418,6 +421,23 @@ gtest_discover_tests(MobileGLIntegrationTest
ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}"
)
# UnlocatedIoBlockScenario with the interface-block location strip PINNED ON, for the same
# reason the depth/stencil entry above pins its emulation: without it this scenario is
# UNFALSIFIABLE on the machines this suite runs on. llvmpipe carries a located interface block
# correctly, so the driver POST that arms the strip on Mali answers "healthy" here and the
# emulation never runs - the ambient registration would be exercising the un-stripped path
# twice and calling it coverage. With the variable set, the blocks really are emitted with no
# location and the assertion is about the spelling the device gets.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.UnlocatedIoBlocks."
TEST_FILTER "UnlocatedIoBlockScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
)
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
#
# Not a duplicate of what the two ambient registrations already run: they run whatever
@@ -0,0 +1,421 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.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 - AN INTER-STAGE INTERFACE BLOCK STILL FINDS ITS OTHER END WITH ITS LOCATION
// QUALIFIER REMOVED.
//
// The Mali-G1-Ultra ES driver delivers NOTHING through an interface block that carries an
// explicit layout(location=) once a tessellation or geometry stage is in the pipeline: the
// stages compile, the program links with an empty info log, the draw runs, and the consuming
// stage reads zeroes. Measured with no MobileGL in the process - a bare EGL/GLES 3.2 program
// built from the five ESSL stages MobileGL emits reproduces it, and removing the qualifier
// from the blocks (and changing nothing else) makes the same program carry its payload. The
// locations are not the application's in the first place: these shaders declare none, and
// glslang's cross-stage IO resolver invents them.
//
// DirectGLES answers by dropping the decoration for those programs (StripIoBlockLocationsPass),
// leaving ES to match the blocks by block name and member sequence. THAT is what this scenario
// guards: with the strip forced on, a five-stage pipeline whose four block boundaries carry no
// location must still deliver its payload end to end. It is the assertion the affected device
// cannot make about itself in CI, and the one the healthy machines here CAN make - which is
// the opposite of IoBlockNameCollisionScenario's position, where the machines that run it
// cannot reproduce the defect at all.
//
// The strip is armed for this suite by MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1 on the ctest
// entry, because llvmpipe carries a located block correctly and the driver POST would
// therefore never turn the emulation on here. The SAME cases also run under the ambient
// registrations with the emulation off, so both spellings of the interface are covered and a
// regression in either shows up.
//
// Colour code, so a failure names its own cause:
// green - the payload crossed all four stage boundaries, which is the pass.
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
// backend program was rejected and every draw became a no-op).
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
// failure is not about interface blocks.
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
// zeroed. That is what an interface whose two ends stopped matching looks like.
#include <string>
#include <vector>
#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 {
// NOTHING in these five stages declares a location. Every location the emitted ESSL
// carries is invented by the cross-stage resolver, which is exactly the shape the
// affected driver mishandles and exactly what the strip removes.
//
// Two members per block, of different types, because an interface that is matched by
// name and member sequence rather than by location has to agree on the sequence too -
// a repair that silently reordered or dropped a member would still light up green with
// one member in the block.
const char* const kVertexSource = R"(#version 420 core
out VsData {
vec4 payload;
vec2 tint;
} vs_out;
out float vs_tcs_alive;
void main()
{
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
vs_out.tint = vec2(0.25, 0.5);
vs_tcs_alive = 1.0;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
const char* const kTessControlSource = R"(#version 420 core
layout(vertices = 1) out;
in VsData {
vec4 payload;
vec2 tint;
} tcs_in[];
in float vs_tcs_alive[];
out TcsData {
vec4 payload;
vec2 tint;
} tcs_out[];
out float tcs_tes_alive[];
void main()
{
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
tcs_out[gl_InvocationID].tint = tcs_in[gl_InvocationID].tint;
tcs_tes_alive[gl_InvocationID] = vs_tcs_alive[gl_InvocationID];
gl_TessLevelOuter[0] = 1.0;
gl_TessLevelOuter[1] = 1.0;
gl_TessLevelOuter[2] = 1.0;
gl_TessLevelOuter[3] = 1.0;
gl_TessLevelInner[0] = 1.0;
gl_TessLevelInner[1] = 1.0;
}
)";
// Distinct block names, so this case is about the LOCATION and nothing else; the
// one-name-in-both-directions shape is the case below.
const char* const kDistinctTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in TcsData {
vec4 payload;
vec2 tint;
} tes_in[];
in float tcs_tes_alive[];
out TesData {
vec4 payload;
vec2 tint;
} tes_out;
out float tes_gs_alive;
void main()
{
tes_out.payload = tes_in[0].payload;
tes_out.tint = tes_in[0].tint;
tes_gs_alive = tcs_tes_alive[0];
}
)";
// The 420pack shape: ONE name for the block this stage consumes and the block it
// produces. Legal desktop GLSL, and the case where the two repairs have to compose -
// the rename gives the two blocks one spelling per producing stage, the strip takes
// their locations off, and the interfaces still have to meet.
const char* const kCollidingTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in TcsData {
vec4 payload;
vec2 tint;
} tes_in[];
in float tcs_tes_alive[];
out TcsData {
vec4 payload;
vec2 tint;
} tes_out;
out float tes_gs_alive;
void main()
{
tes_out.payload = tes_in[0].payload;
tes_out.tint = tes_in[0].tint;
tes_gs_alive = tcs_tes_alive[0];
}
)";
// One geometry source per evaluation stage, because the block it consumes is named
// after the block the evaluation stage produced.
const char* const kDistinctGeometrySource = R"(#version 420 core
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in TesData {
vec4 payload;
vec2 tint;
} gs_in[];
in float tes_gs_alive[];
out GsData {
vec4 payload;
vec2 tint;
} gs_out;
out float gs_fs_alive;
void EmitCorner(vec2 corner)
{
gs_out.payload = gs_in[0].payload;
gs_out.tint = gs_in[0].tint;
gs_fs_alive = tes_gs_alive[0];
gl_Position = vec4(corner, 0.0, 1.0);
EmitVertex();
}
void main()
{
EmitCorner(vec2(-1.0, -1.0));
EmitCorner(vec2(-1.0, 1.0));
EmitCorner(vec2( 1.0, -1.0));
EmitCorner(vec2( 1.0, 1.0));
}
)";
const char* const kCollidingGeometrySource = R"(#version 420 core
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in TcsData {
vec4 payload;
vec2 tint;
} gs_in[];
in float tes_gs_alive[];
out GsData {
vec4 payload;
vec2 tint;
} gs_out;
out float gs_fs_alive;
void EmitCorner(vec2 corner)
{
gs_out.payload = gs_in[0].payload;
gs_out.tint = gs_in[0].tint;
gs_fs_alive = tes_gs_alive[0];
gl_Position = vec4(corner, 0.0, 1.0);
EmitVertex();
}
void main()
{
EmitCorner(vec2(-1.0, -1.0));
EmitCorner(vec2(-1.0, 1.0));
EmitCorner(vec2( 1.0, -1.0));
EmitCorner(vec2( 1.0, 1.0));
}
)";
// Green ONLY when both block members arrived: a repair that kept the first member and
// lost the second would otherwise pass. Red when the plain varying is missing too, so
// "the pipeline is broken" and "the block is broken" cannot be confused.
const char* const kFragmentSource = R"(#version 420 core
in GsData {
vec4 payload;
vec2 tint;
} fs_in;
in float gs_fs_alive;
out vec4 fragColor;
void main()
{
if (gs_fs_alive <= 0.5) {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
} else if (abs(fs_in.tint.x - 0.25) > 0.01 || abs(fs_in.tint.y - 0.5) > 0.01) {
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
} else {
fragColor = fs_in.payload;
}
}
)";
class UnlocatedIoBlockScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
if (!BackendHostsTessellationAndGeometry()) {
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
<< Gl().RendererString() << "); there is no five-stage pipeline to "
<< "carry a block through";
}
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
}
// Same calibration IoBlockNameCollisionScenario uses, and for the same reason:
// GL_MAX_TESS_GEN_LEVEL is a real backend answer while GL_MAX_GEOMETRY_* are
// frontend constants, so a stack with no five-stage pipeline is recognised by
// trying to build one, not by asking.
static bool BackendHostsTessellationAndGeometry() {
GLint maxTessGenLevel = 0;
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
GLint maxGeometryOutputVertices = 0;
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
while (glGetError() != GL_NO_ERROR) {
}
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
}
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
GL_FRAGMENT_SHADER};
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
geometrySource, kFragmentSource};
GLuint shaders[5] = {0, 0, 0, 0, 0};
bool ok = true;
for (int i = 0; i < 5; ++i) {
shaders[i] = glCreateShader(stages[i]);
glShaderSource(shaders[i], 1, &sources[i], nullptr);
glCompileShader(shaders[i]);
GLint compiled = 0;
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
if (!compiled) {
m_buildLog = InfoLog(shaders[i], true);
ok = false;
break;
}
}
if (!ok) {
for (const GLuint shader : shaders) {
if (shader != 0) glDeleteShader(shader);
}
return 0;
}
const GLuint program = glCreateProgram();
for (const GLuint shader : shaders) {
glAttachShader(program, shader);
}
glLinkProgram(program);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
for (const GLuint shader : shaders) {
glDeleteShader(shader);
}
if (!linked) {
m_buildLog = InfoLog(program, false);
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
}
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
// than something that could be mistaken for a zeroed payload.
Rgba8 DrawAndReadCentre(GLuint program) const {
glViewport(0, 0, Gl().Width(), Gl().Height());
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glPatchParameteri(GL_PATCH_VERTICES, 1);
glDrawArrays(GL_PATCHES, 0, 1);
Rgba8 pixel{};
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
return pixel;
}
static bool IsGreen(const Rgba8& pixel) {
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
}
const std::string& BuildLog() const { return m_buildLog; }
static GLenum FirstGLError() {
const GLenum first = glGetError();
while (glGetError() != GL_NO_ERROR) {
}
return first;
}
private:
static std::string InfoLog(GLuint object, bool isShader) {
GLint length = 0;
if (isShader) {
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
} else {
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
}
std::vector<char> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
if (isShader) {
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
} else {
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
}
return std::string(log.data());
}
GLuint m_vao = 0;
std::vector<GLuint> m_programs;
std::string m_buildLog;
};
TEST_F(UnlocatedIoBlockScenario, BlocksCarryTheirPayloadThroughFiveStages) {
if (!Ready()) return;
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
if (program == 0) {
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
<< Gl().BackendName() << ", so there is no block to carry through: "
<< BuildLog();
}
const Rgba8 centre = DrawAndReadCentre(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(centre))
<< "a four-boundary interface-block chain did not deliver its payload: " << centre
<< " (blue: nothing drew; red: the plain varying was lost too; black: a block "
"member arrived wrong, i.e. the interface stopped matching)";
}
// The two repairs together. The rename is what makes the evaluation stage's two
// TcsData blocks one spelling per producing stage; the strip then takes the locations
// off the names the rename just settled. Either one alone leaves a working program on
// these machines, so this case is here to catch the two of them disagreeing.
TEST_F(UnlocatedIoBlockScenario, BlocksNamedInBothDirectionsStillMeetWithoutLocations) {
if (!Ready()) return;
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
<< Gl().BackendName() << ", so there is no block to carry through: "
<< BuildLog();
}
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
ASSERT_NE(program, 0u)
<< "an interface block name reused across the two directions of one stage is legal "
"desktop GLSL, but the program did not build: "
<< BuildLog();
const Rgba8 centre = DrawAndReadCentre(program);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(centre))
<< "the renamed-and-unlocated interface chain lost its payload: " << centre;
}
} // namespace
} // namespace MGITest
@@ -11,6 +11,7 @@ add_executable(
FlattenFloat64StorageBlockTest.cpp
FlattenXfbInterfaceBlocksTest.cpp
UniquifyIoBlockNamesTest.cpp
StripIoBlockLocationsTest.cpp
LowerViewportIndexTest.cpp
ClampMultisampleFetchTest.cpp
LegalizeResourceArrayIndexTest.cpp
@@ -0,0 +1,219 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/StripIoBlockLocationsTest.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
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
namespace {
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
String Transpile(const Vector<Uint32>& spirv) {
SpvcSession session(spirv, SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
return essl ? essl.value() : String{};
}
// How many times `needle` occurs in `haystack`.
SizeT CountOf(const String& haystack, const String& needle) {
SizeT count = 0;
for (SizeT at = haystack.find(needle); at != String::npos; at = haystack.find(needle, at + 1)) {
++count;
}
return count;
}
// The tessellation evaluation stage of
// KHR-GLxx.shading_language_420pack.length_of_vector_and_matrix_*, reduced to what this
// pass is about: one block consumed, one block produced, a plain varying in each
// direction, and NO location written anywhere in the source. Every location in the
// emitted ESSL is invented by glslang's cross-stage IO resolver.
const char* kTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in vec4 tcs_tes_result[];
out vec4 tes_gs_result;
in TCSOutputBlock {
vec4 tcs_tes_variable;
} input_block[];
out TESOutputBlock {
vec4 tes_gs_variable;
} output_block;
void main()
{
tes_gs_result = tcs_tes_result[0];
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
}
)";
// A stage with no interface block at all: the pass must leave its located varyings alone
// and report that it changed nothing, so the caller declines the re-serialised module.
const char* kNoBlockTessEvalSource = R"(#version 420 core
layout(isolines, point_mode) in;
in vec4 tcs_tes_result[];
out vec4 tes_gs_result;
void main()
{
tes_gs_result = tcs_tes_result[0];
}
)";
} // namespace
class StripIoBlockLocationsTest : public ::testing::Test {
protected:
void SetUp() override {
MobileGL::Initialize();
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
}
void TearDown() override {
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
<< "the stripped module did not survive spirv-val";
}
Uint64 m_validationFailuresAtStart = 0;
};
TEST_F(StripIoBlockLocationsTest, DropsTheQualifierFromBothBlocksAndLeavesVaryingsAlone) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource);
ASSERT_FALSE(input.empty());
// The defect this exists for, pinned before the repair: SPIRV-Cross really does print a
// location on the blocks, and on this driver that is what loses their payload.
const String before = Transpile(input);
EXPECT_NE(before.find(") in TCSOutputBlock"), String::npos) << before;
EXPECT_NE(before.find(") out TESOutputBlock"), String::npos) << before;
bool strippedAny = false;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::StripIoBlockLocationsForEssl(input, true, true, strippedAny, output, true));
ASSERT_FALSE(output.empty());
EXPECT_TRUE(strippedAny);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
ASSERT_TRUE(tools.Validate(output));
const String after = Transpile(output);
// The blocks come out bare...
EXPECT_NE(after.find("in TCSOutputBlock"), String::npos) << after;
EXPECT_NE(after.find("out TESOutputBlock"), String::npos) << after;
EXPECT_EQ(after.find(") in TCSOutputBlock"), String::npos)
<< "the consumed block still carries a layout qualifier:\n"
<< after;
EXPECT_EQ(after.find(") out TESOutputBlock"), String::npos)
<< "the produced block still carries a layout qualifier:\n"
<< after;
// ...and everything ES matches them by is untouched, which is what makes the unlocated
// interface still find its other end.
EXPECT_NE(after.find("input_block"), String::npos) << after;
EXPECT_NE(after.find("output_block"), String::npos) << after;
EXPECT_NE(after.find("tcs_tes_variable"), String::npos) << after;
EXPECT_NE(after.find("tes_gs_variable"), String::npos) << after;
// The PLAIN varyings keep their locations. They work on the affected driver, and a
// fragment stage's inputs and a vertex stage's attributes are matched by them.
EXPECT_NE(after.find("in vec4 tcs_tes_result"), String::npos) << after;
EXPECT_NE(after.find("out vec4 tes_gs_result"), String::npos) << after;
EXPECT_EQ(CountOf(after, "layout(location"), 2u)
<< "exactly the two plain varyings should still be located:\n"
<< after;
}
TEST_F(StripIoBlockLocationsTest, StripsOnlyTheDirectionTheCallerArmed) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource);
ASSERT_FALSE(input.empty());
// A separate-shader-objects program that ENDS at this stage: the block it produces is
// matched, in another program that never saw this decision, by the location alone. Only
// the consumed side may lose its qualifier.
bool strippedAny = false;
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::StripIoBlockLocationsForEssl(input, true, false, strippedAny, output, true));
ASSERT_FALSE(output.empty());
EXPECT_TRUE(strippedAny);
const String after = Transpile(output);
EXPECT_EQ(after.find(") in TCSOutputBlock"), String::npos) << after;
EXPECT_NE(after.find(") out TESOutputBlock"), String::npos)
<< "the produced block's location was dropped even though its consumer is elsewhere:\n"
<< after;
// And the mirror image, for a program that BEGINS at this stage.
bool strippedOutputOnly = false;
Vector<Uint32> outputOnly;
ASSERT_TRUE(
ShaderCompiler::StripIoBlockLocationsForEssl(input, false, true, strippedOutputOnly, outputOnly, true));
ASSERT_FALSE(outputOnly.empty());
EXPECT_TRUE(strippedOutputOnly);
const String afterOutputOnly = Transpile(outputOnly);
EXPECT_NE(afterOutputOnly.find(") in TCSOutputBlock"), String::npos) << afterOutputOnly;
EXPECT_EQ(afterOutputOnly.find(") out TESOutputBlock"), String::npos) << afterOutputOnly;
}
TEST_F(StripIoBlockLocationsTest, ReportsNoChangeForAStageWithoutInterfaceBlocks) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kNoBlockTessEvalSource);
ASSERT_FALSE(input.empty());
const String before = Transpile(input);
bool strippedAny = true; // deliberately wrong going in; the pass must clear it
Vector<Uint32> output;
ShaderCompiler::StripIoBlockLocationsForEssl(input, true, true, strippedAny, output, true);
EXPECT_FALSE(strippedAny) << "a stage with no interface block must report nothing stripped, or "
"the caller adopts a re-serialised module for nothing";
// gl_PerVertex is an Input AND an Output block in this stage and must not be touched; the
// located plain varyings must not be either. Either way the emitted ESSL is unchanged.
if (!output.empty()) {
EXPECT_EQ(Transpile(output), before);
}
}
TEST_F(StripIoBlockLocationsTest, DeclinesWhenNeitherDirectionIsArmed) {
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource);
ASSERT_FALSE(input.empty());
bool strippedAny = true;
Vector<Uint32> output;
EXPECT_FALSE(ShaderCompiler::StripIoBlockLocationsForEssl(input, false, false, strippedAny, output, true));
EXPECT_FALSE(strippedAny);
EXPECT_TRUE(output.empty()) << "an unarmed call must not even re-serialise the module";
}
+27 -191
View File
@@ -836,196 +836,6 @@ namespace MobileGL::MG_Util::BackendLoader {
return includesBase;
}
// Whether an inter-stage interface BLOCK that carries an explicit layout(location=)
// actually transports its payload across a geometry (or tessellation) boundary.
//
// The Mali-G1-Ultra ES driver (r54p1) links such a program with an empty info log, runs
// the draw, and delivers ZEROES to the consuming stage; the identical program with the
// qualifier removed from the blocks carries the payload correctly. That is not a MobileGL
// artefact - a bare EGL/GLES 3.2 program with no MobileGL in the process reproduces it -
// and it is the whole of the KHR-GLxx.shading_language_420pack interface-block group's
// failures on this device. A located block between a VERTEX and a FRAGMENT stage is fine
// on the same driver, so the probe deliberately spans a geometry stage: that is the
// shape that breaks, and answering the narrower question would report a healthy driver.
//
// Probed rather than matched on the renderer string, because "which drivers do this" is
// not knowable and a quirk list is wrong the moment a driver is fixed. A driver that
// cannot run the probe at all (pre-ES 3.2, no geometry stage, missing entry point) is
// reported as HEALTHY: that leaves its ESSL exactly as it is today, which is the answer
// with no behaviour change in it.
Bool ProbeLocatedInterStageIoBlocksTransportPayload(const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& f) {
const Bool esVersionOk =
caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
if (!esVersionOk || !f.glCreateShader || !f.glCreateProgram || !f.glGenVertexArrays ||
!f.glGenRenderbuffers || !f.glGenFramebuffers || !f.glReadPixels || !f.glDrawArrays) {
MGLOG_I("located-IO-block probe skipped: needs an ES 3.2 context with a geometry stage");
return true;
}
while (f.glGetError() != GL_NO_ERROR) {
}
// Position comes from gl_VertexID so the probe needs no vertex buffer, and the block
// payload is two values that survive an 8-bit target exactly (0.25 -> 64, 0.5 -> 128).
// TWO different block names, because the two boundaries this crosses (VS->GS and
// GS->FS) are separate interfaces; a single name would also trip the in/out collision
// UniquifyIoBlockNamesPass exists for and confuse one defect with the other.
const char* vsSource = "#version 320 es\n"
"precision highp float;\n"
"layout(location = 0) out MgProbeBlock { vec2 mg_probeValue; } mg_probeOut;\n"
"void main() {\n"
" vec2 p = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);\n"
" gl_Position = vec4(p, 0.0, 1.0);\n"
" mg_probeOut.mg_probeValue = vec2(0.25, 0.5);\n"
"}\n";
const char* gsSource = "#version 320 es\n"
"precision highp float;\n"
"layout(triangles) in;\n"
"layout(triangle_strip, max_vertices = 3) out;\n"
"layout(location = 0) in MgProbeBlock { vec2 mg_probeValue; } mg_probeIn[];\n"
"layout(location = 0) out MgProbeBlock2 { vec2 mg_probeValue; } mg_probeOut;\n"
"void main() {\n"
" for (int i = 0; i < 3; ++i) {\n"
" gl_Position = gl_in[i].gl_Position;\n"
" mg_probeOut.mg_probeValue = mg_probeIn[i].mg_probeValue;\n"
" EmitVertex();\n"
" }\n"
"}\n";
const char* fsSource = "#version 320 es\n"
"precision highp float;\n"
"layout(location = 0) in MgProbeBlock2 { vec2 mg_probeValue; } mg_probeIn;\n"
"layout(location = 0) out vec4 mg_probeColor;\n"
"void main() { mg_probeColor = vec4(mg_probeIn.mg_probeValue, 0.0, 1.0); }\n";
const auto compileShader = [&f](GLenum type, const char* src) -> GLuint {
const GLuint shader = f.glCreateShader(type);
if (shader == 0) return 0;
f.glShaderSource(shader, 1, &src, nullptr);
f.glCompileShader(shader);
GLint status = GL_FALSE;
f.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_TRUE) {
f.glDeleteShader(shader);
return 0;
}
return shader;
};
const GLuint vs = compileShader(GL_VERTEX_SHADER, vsSource);
const GLuint gs = compileShader(GL_GEOMETRY_SHADER, gsSource);
const GLuint fs = compileShader(GL_FRAGMENT_SHADER, fsSource);
GLuint program = 0;
if (vs != 0 && gs != 0 && fs != 0) {
program = f.glCreateProgram();
if (program != 0) {
f.glAttachShader(program, vs);
f.glAttachShader(program, gs);
f.glAttachShader(program, fs);
f.glLinkProgram(program);
GLint status = GL_FALSE;
f.glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status != GL_TRUE) {
f.glDeleteProgram(program);
program = 0;
}
}
}
if (vs != 0) f.glDeleteShader(vs);
if (gs != 0) f.glDeleteShader(gs);
if (fs != 0) f.glDeleteShader(fs);
if (program == 0) {
MGLOG_I("located-IO-block probe skipped: probe program failed to build (vs=%u gs=%u fs=%u)", vs,
gs, fs);
while (f.glGetError() != GL_NO_ERROR) {
}
return true;
}
// Everything this probe changes is read back first and put back afterwards. The
// capability run owns a context of its own, but a probe that leaves a 1x1 viewport or
// a bound scratch framebuffer behind would be found by whatever draws next rather
// than here, and that is not a bug anyone should have to chase twice.
GLint savedProgram = 0, savedVao = 0, savedDrawFbo = 0, savedReadFbo = 0, savedRenderbuffer = 0;
GLint savedViewport[4] = {0, 0, 0, 0};
GLfloat savedClearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
GLboolean savedColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
f.glGetIntegerv(GL_CURRENT_PROGRAM, &savedProgram);
f.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &savedVao);
f.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFbo);
f.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &savedReadFbo);
f.glGetIntegerv(GL_RENDERBUFFER_BINDING, &savedRenderbuffer);
f.glGetIntegerv(GL_VIEWPORT, savedViewport);
f.glGetFloatv(GL_COLOR_CLEAR_VALUE, savedClearColor);
f.glGetBooleanv(GL_COLOR_WRITEMASK, savedColorMask);
// The five raster states that can void the draw and turn a healthy driver into a
// "broken" verdict. Saved, forced off, and put back.
constexpr GLenum kQuietedStates[] = {GL_SCISSOR_TEST, GL_RASTERIZER_DISCARD, GL_CULL_FACE,
GL_DEPTH_TEST, GL_BLEND};
GLboolean savedStates[5] = {GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE};
for (SizeT i = 0; i < std::size(kQuietedStates); ++i) {
savedStates[i] = f.glIsEnabled(kQuietedStates[i]);
if (savedStates[i] == GL_TRUE) f.glDisable(kQuietedStates[i]);
}
f.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// ES makes a draw on the default vertex array object invalid in a core-profile sense,
// and the default framebuffer may be incomplete (surfaceless contexts), so the probe
// brings both of its own.
GLuint vao = 0, framebuffer = 0, renderbuffer = 0;
f.glGenVertexArrays(1, &vao);
f.glBindVertexArray(vao);
f.glGenRenderbuffers(1, &renderbuffer);
f.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
f.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
f.glGenFramebuffers(1, &framebuffer);
f.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
f.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
f.glViewport(0, 0, 1, 1);
f.glUseProgram(program);
f.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
f.glClear(GL_COLOR_BUFFER_BIT);
f.glDrawArrays(GL_TRIANGLES, 0, 3);
Bool transports = true;
const GLenum drawError = f.glGetError();
if (drawError == GL_NO_ERROR) {
GLubyte pixel[4] = {0, 0, 0, 0};
f.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
if (f.glGetError() == GL_NO_ERROR) {
// Exactly the two values the vertex stage wrote, with one bit of slack for a
// driver that rounds the 8-bit conversion the other way. A stage that received
// nothing reads 0/0, which is nowhere near either.
transports = pixel[0] >= 0x3f && pixel[0] <= 0x41 && pixel[1] >= 0x7f && pixel[1] <= 0x81;
MGLOG_I("located-IO-block probe: fragment stage received (%u, %u), expected (64, 128)",
pixel[0], pixel[1]);
} else {
MGLOG_I("located-IO-block probe inconclusive: readback failed");
}
} else {
MGLOG_I("located-IO-block probe inconclusive: draw raised GL error 0x%x", drawError);
}
f.glUseProgram(static_cast<GLuint>(savedProgram));
f.glBindFramebuffer(GL_FRAMEBUFFER, 0);
f.glDeleteFramebuffers(1, &framebuffer);
f.glDeleteRenderbuffers(1, &renderbuffer);
f.glBindVertexArray(0);
f.glDeleteVertexArrays(1, &vao);
f.glBindVertexArray(static_cast<GLuint>(savedVao));
f.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(savedRenderbuffer));
f.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(savedDrawFbo));
f.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(savedReadFbo));
f.glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]);
f.glClearColor(savedClearColor[0], savedClearColor[1], savedClearColor[2], savedClearColor[3]);
f.glColorMask(savedColorMask[0], savedColorMask[1], savedColorMask[2], savedColorMask[3]);
for (SizeT i = 0; i < std::size(kQuietedStates); ++i) {
if (savedStates[i] == GL_TRUE) f.glEnable(kQuietedStates[i]);
}
f.glDeleteProgram(program);
while (f.glGetError() != GL_NO_ERROR) {
}
return transports;
}
// GL 4.6 table 23.65 admits exactly four answers for GL_LAYER_PROVOKING_VERTEX and
// GL_VIEWPORT_INDEX_PROVOKING_VERTEX. Anything else means the driver wrote something MobileGL
// cannot forward as a convention, and GL_UNDEFINED_VERTEX - a legal answer, not a placeholder
@@ -1914,8 +1724,34 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s",
caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false");
// ForceOn means "emit the blocks unlocated", i.e. treat the driver as NOT supporting
// located blocks - which is why the override reads inverted here. Auto is the probe's
// own answer and is what every real run uses; the two forced settings exist so the
// emulation can be exercised on a healthy driver (the integration lane) and turned
// off again as a negative control.
switch (MG_Config::Features.EsprytUnlocatedIoBlocks) {
case MG_Config::QuirkOverride::ForceOn:
caps.SupportsLocatedInterStageIoBlocks = false;
MGLOG_I(" Located inter-stage interface blocks: forced OFF by "
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS; the driver was not probed");
break;
case MG_Config::QuirkOverride::ForceOff:
caps.SupportsLocatedInterStageIoBlocks = true;
MGLOG_I(" Located inter-stage interface blocks: forced ON by "
"MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS; the driver was not probed");
break;
case MG_Config::QuirkOverride::Auto:
default:
// SelfTest::ProbeLocatedIoBlocksLosePayload - the Mali-G1-Ultra ES driver
// delivers nothing through an interface block that carries an explicit
// layout(location=) once a tessellation or geometry stage is in the pipeline.
// Probed with its own controls rather than matched on a renderer string; see
// DriverBugProbes.h for the shape and for why the two controls decide what the
// finding is allowed to claim.
caps.SupportsLocatedInterStageIoBlocks =
ProbeLocatedInterStageIoBlocksTransportPayload(caps, glesFuncs);
!SelfTest::LocatedIoBlocksLosePayload(glesFuncs).detected;
break;
}
MGLOG_I(" Located inter-stage interface blocks transport their payload: %s",
caps.SupportsLocatedInterStageIoBlocks
? "true"
@@ -1360,12 +1360,6 @@ namespace MobileGL {
// so MG_Test can drive it against a fake GLES functions table.
Bool ProbeIndirectInstanceIdIncludesBaseInstance(const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& glesFuncs);
// Detects whether an inter-stage interface block that carries an explicit
// layout(location=) actually transports its payload across a geometry boundary (see
// Loader.cpp). Called by FillInGLESCapabilities; exposed so MG_Test can drive it
// against a fake GLES functions table.
Bool ProbeLocatedInterStageIoBlocksTransportPayload(const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& glesFuncs);
} // namespace MG_Util::BackendLoader
} // namespace MobileGL
#undef MOBILEGL_EXTERNAL_GLES
@@ -1686,6 +1686,179 @@ namespace MobileGL::MG_Util::SelfTest {
return measurement;
}
namespace {
// ===================== LOCATED INTER-STAGE INTERFACE BLOCKS =====================
constexpr const char* kIoBlockProbeName = "located interface block";
// Two values that survive an 8-bit target exactly, so the read is a comparison and not
// a tolerance: 0.25 -> 64, 0.5 -> 128. A stage that received nothing reads 0/0, which is
// nowhere near either.
constexpr GLubyte kIoBlockExpectedR = 0x40;
constexpr GLubyte kIoBlockExpectedG = 0x80;
// `@BL@` becomes the layout qualifier under test, or nothing at all for the control.
// Position comes from gl_VertexID, so no probe here needs a vertex buffer.
String BuildIoBlockVertexSource(const char* blockQualifier) {
return format("#version 320 es\n"
"precision highp float;\n"
"{}out MgProbeBlock {{ vec2 mg_probeValue; }} mg_probeOut;\n"
"void main() {{\n"
" vec2 mg_p = vec2((gl_VertexID == 1) ? 3.0 : -1.0,\n"
" (gl_VertexID == 2) ? 3.0 : -1.0);\n"
" gl_Position = vec4(mg_p, 0.0, 1.0);\n"
" mg_probeOut.mg_probeValue = vec2(0.25, 0.5);\n"
"}}\n",
blockQualifier);
}
// The block name changes across the geometry stage, because the two boundaries are two
// separate interfaces; one name would also be the in-and-out-under-one-name shape
// UniquifyIoBlockNamesPass exists for, and confusing one defect with the other is
// exactly what this file's control rule is against.
String BuildIoBlockGeometrySource(const char* blockQualifier) {
return format("#version 320 es\n"
"precision highp float;\n"
"layout(triangles) in;\n"
"layout(triangle_strip, max_vertices = 3) out;\n"
"{0}in MgProbeBlock {{ vec2 mg_probeValue; }} mg_probeIn[];\n"
"{0}out MgProbeBlock2 {{ vec2 mg_probeValue; }} mg_probeOut;\n"
"void main() {{\n"
" for (int i = 0; i < 3; ++i) {{\n"
" gl_Position = gl_in[i].gl_Position;\n"
" mg_probeOut.mg_probeValue = mg_probeIn[i].mg_probeValue;\n"
" EmitVertex();\n"
" }}\n"
"}}\n",
blockQualifier);
}
String BuildIoBlockFragmentSource(const char* blockQualifier, const char* blockName) {
return format("#version 320 es\n"
"precision highp float;\n"
"{}in {} {{ vec2 mg_probeValue; }} mg_probeIn;\n"
"layout(location = 0) out vec4 mg_probeColor;\n"
"void main() {{ mg_probeColor = vec4(mg_probeIn.mg_probeValue, 0.0, 1.0); }}\n",
blockQualifier, blockName);
}
// Builds and draws one of the four programs this probe compares and reports whether the
// fragment stage received the payload. `outRan` distinguishes "the payload did not
// arrive" from "this program could not be built or drawn at all" - the second is
// inconclusive and must never become a finding.
Bool IoBlockPayloadArrives(const GLESFunctionsTable& gl, const char* blockQualifier,
Bool withGeometryStage, Bool& outRan) {
outRan = false;
Vector<StageSource> stages;
stages.push_back({GL_VERTEX_SHADER, BuildIoBlockVertexSource(blockQualifier), "vertex"});
if (withGeometryStage) {
stages.push_back(
{GL_GEOMETRY_SHADER, BuildIoBlockGeometrySource(blockQualifier), "geometry"});
}
stages.push_back({GL_FRAGMENT_SHADER,
BuildIoBlockFragmentSource(blockQualifier,
withGeometryStage ? "MgProbeBlock2"
: "MgProbeBlock"),
"fragment"});
const ProgramBuild build = BuildProgram(gl, stages, kIoBlockProbeName);
if (!build.linked) {
if (build.program != 0) gl.glDeleteProgram(build.program);
return false;
}
GLuint renderbuffer = 0;
GLuint framebuffer = 0;
Bool arrives = false;
gl.glGenRenderbuffers(1, &renderbuffer);
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
gl.glGenFramebuffers(1, &framebuffer);
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
renderbuffer);
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
gl.glUseProgram(build.program);
gl.glViewport(0, 0, 1, 1);
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
gl.glClear(GL_COLOR_BUFFER_BIT);
Drain(gl);
gl.glDrawArrays(GL_TRIANGLES, 0, 3);
if (gl.glGetError() == GL_NO_ERROR) {
GLubyte pixel[4] = {0, 0, 0, 0};
gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
if (gl.glGetError() == GL_NO_ERROR) {
outRan = true;
// One bit of slack each way, for a driver that rounds the 8-bit
// conversion the other direction.
arrives = pixel[0] + 1 >= kIoBlockExpectedR && pixel[0] <= kIoBlockExpectedR + 1 &&
pixel[1] + 1 >= kIoBlockExpectedG && pixel[1] <= kIoBlockExpectedG + 1;
}
}
}
if (framebuffer != 0) gl.glDeleteFramebuffers(1, &framebuffer);
if (renderbuffer != 0) gl.glDeleteRenderbuffers(1, &renderbuffer);
gl.glDeleteProgram(build.program);
return arrives;
}
} // namespace
LocatedIoBlockMeasurement ProbeLocatedIoBlocksLosePayload(const GLESFunctionsTable& gl) {
LocatedIoBlockMeasurement measurement;
if (!HasEveryEntryPoint(gl) || !gl.glRenderbufferStorage || !gl.glFramebufferRenderbuffer ||
!gl.glCheckFramebufferStatus || !gl.glReadPixels || !gl.glClearColor || !gl.glClear ||
!gl.glDrawArrays || !gl.glViewport) {
return measurement;
}
SavedState saved;
Save(gl, saved);
GLuint vao = 0;
gl.glGenVertexArrays(1, &vao);
gl.glBindVertexArray(vao);
PrepareForProbeDraw(gl);
if (gl.glColorMask != nullptr) gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// THE CONTROL, and it runs first: the identical three-stage program with no location on
// the blocks. If THAT cannot carry the payload, this driver's problem is not the
// qualifier and the probe has no finding to make - reporting one would justify dropping
// a qualifier that was never the cause.
Bool controlRan = false;
const Bool controlArrives = IoBlockPayloadArrives(gl, "", true, controlRan);
if (controlRan && controlArrives) {
Bool subjectRan = false;
const Bool subjectArrives =
IoBlockPayloadArrives(gl, "layout(location = 0) ", true, subjectRan);
if (subjectRan && !subjectArrives) {
measurement.detected = true;
// The second control, and the one that scopes the repair: the same located
// block between a vertex and a fragment stage. It arrives on the driver this
// was characterised on, which is why DirectGLES only drops the qualifier for
// programs that have a tessellation or geometry stage. A driver where this one
// ALSO fails is losing payloads the repair does not reach, and the report says
// so rather than implying the fix is complete.
Bool vsFsRan = false;
const Bool vsFsArrives =
IoBlockPayloadArrives(gl, "layout(location = 0) ", false, vsFsRan);
measurement.alsoAffectsVertexToFragment = vsFsRan && !vsFsArrives;
}
}
if (vao != 0) {
gl.glBindVertexArray(0);
gl.glDeleteVertexArrays(1, &vao);
}
Restore(gl, saved);
Drain(gl);
return measurement;
}
const LocatedIoBlockMeasurement& LocatedIoBlocksLosePayload(const GLESFunctionsTable& gl) {
// One driver per process, and the answer is structural rather than sampled.
static const LocatedIoBlockMeasurement measurement = ProbeLocatedIoBlocksLosePayload(gl);
return measurement;
}
namespace {
Optional<DriverBugFinding> ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) {
const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl);
@@ -1808,6 +1981,37 @@ namespace MobileGL::MG_Util::SelfTest {
percentOf(measurement.emittedShapeMismatchedTexels))};
}
Optional<DriverBugFinding> ProbeLocatedIoBlockPayloadBug(const GLESFunctionsTable& gl) {
const LocatedIoBlockMeasurement& measurement = LocatedIoBlocksLosePayload(gl);
if (!measurement.detected) return std::nullopt;
String detail =
"an inter-stage interface block that carries an explicit layout(location = N) "
"delivers NOTHING once a geometry (or tessellation) stage is in the pipeline: the "
"stages compile, the program links with an empty info log, the draw raises no "
"error, and the consuming stage reads zeroes. The byte-identical program with the "
"qualifier removed from the blocks carries its payload correctly, which is what "
"makes this a LOCATION defect rather than an interface-block one - blocks "
"themselves work here";
detail += measurement.alsoAffectsVertexToFragment
? ". A located block between a VERTEX and a FRAGMENT stage is lost on "
"this driver too, so the defect is wider than the repair below "
"reaches: MobileGL only drops the qualifier for programs that have a "
"tessellation or geometry stage, and a located block in a plain "
"vertex+fragment program is still emitted as the application wrote it"
: ". A located block between a VERTEX and a FRAGMENT stage is delivered "
"correctly on the same driver, which is what scopes the repair";
detail +=
". MobileGL emits a tessellation/geometry program's interface blocks with no "
"location qualifier at all (StripIoBlockLocationsPass) and lets ES match them by "
"block name and member sequence, which it does; the locations were invented by "
"the cross-stage IO resolver rather than written by the application";
return DriverBugFinding{"Located inter-stage interface blocks carry no payload",
measurement.alsoAffectsVertexToFragment
? DriverBugVerdict::Unfixable
: DriverBugVerdict::Fixed,
Move(detail)};
}
// The table. One row per known driver bug; see the header for how to add a sibling.
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
@@ -1818,6 +2022,7 @@ namespace MobileGL::MG_Util::SelfTest {
&ProbeImageCoherencyResidualBug,
&ProbeExplicitVertexInputLocationCeilingBug,
&ProbeLayeredBlitDestinationBug,
&ProbeLocatedIoBlockPayloadBug,
};
} // namespace
@@ -55,6 +55,44 @@ namespace MobileGL::MG_Util::SelfTest {
String detail;
};
// What the located-interface-block probe measured.
struct LocatedIoBlockMeasurement {
// The driver delivers nothing through an inter-stage interface block that carries an
// explicit layout(location=) once a geometry stage is in the pipeline. The only field
// any caller's behaviour depends on.
Bool detected = false;
// ...and it does the same WITHOUT a geometry stage, i.e. between a vertex and a
// fragment stage. False on the device this was characterised on, and reported because
// DirectGLES's repair is scoped to tessellation/geometry programs: a driver that
// answered true here would be losing block payloads the repair does not reach.
Bool alsoAffectsVertexToFragment = false;
};
// Draws one full-viewport triangle through VS+GS+FS whose two interface blocks carry an
// explicit layout(location = 0), and reports whether the payload the vertex stage wrote
// reached the fragment stage.
//
// The Mali-G1-Ultra ES driver (r54p1) delivers ZEROES: the stages compile, the program
// links with an empty info log, the draw runs without error, and the block is empty. It is
// the whole of the KHR-GLxx.shading_language_420pack interface-block group's failures on
// that device, and of a further 21 tessellation and geometry bodies beside it.
//
// TWO CONTROLS, and the first is why this is a LOCATION finding rather than a block one:
// (1) the identical three-stage program with the qualifier removed from both blocks must
// deliver its payload - without that, "this driver cannot carry an interface block through
// a geometry stage" would be the claim, which is false and would justify flattening every
// block on the device; and (2) a two-stage vertex-to-fragment program with a LOCATED block
// is measured separately, because that one works on the affected driver and is what scopes
// the repair to programs with a tessellation or geometry stage.
//
// Returns `detected` false when an entry point is missing, when the driver has no geometry
// stage, or when the unlocated control fails - an inconclusive probe must never be reported
// as a bug, and must never arm the repair. Restores every piece of GL state it touches.
LocatedIoBlockMeasurement ProbeLocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl);
// ProbeLocatedIoBlocksLosePayload(), evaluated at most once per process.
const LocatedIoBlockMeasurement& LocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl);
// Blits one layer of an RGBA8 2D array onto another array's layer 1 and reports whether the
// copy landed where it was asked to. Returns true only when the destination layer is ignored
// while the control lands correctly.