mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 14:48:32 +09:00
[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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user