[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
+28 -192
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");
caps.SupportsLocatedInterStageIoBlocks =
ProbeLocatedInterStageIoBlocksTransportPayload(caps, glesFuncs);
// 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 =
!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.