Files
MobileGL/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
T
swung0x48 9eae98581f [Test] (SelfTest, POST): probe upload/draw ordering in persistently mapped vertex arenas - native GLES controls distinguish mapped destination corruption from staging and synchronization failures
Add the persistent buffer ordering probe to the GLES POST's known-driver-bug inventory. Queue updates and draws into independent FBOs before reading them back, covering SubData and copies from both coherent persistent and ordinary staging buffers. Retry fresh mapped allocations for intermittent corruption.

Require passing never-mapped and fully serialized controls before reporting a finding. Include finish-before, map-then-unmap and barrier diagnostics, preserve caller GL state, and treat setup, allocation and GL errors as inconclusive. This adds detection and reporting only; no rendering workaround is enabled.

Validation: all 46 DriverBugProbesTest tests pass, including 11 new ordering, control, collector and cleanup cases. The Android API 26 / NDK 27 native probe detects all three upload paths on Mali-G1-Ultra r54p1 with clean controls and no GL errors. llvmpipe reports no finding after 240 mapped FBO checks per upload path.
2026-09-07 00:44:08 -04:00

2479 lines
137 KiB
C++

// MobileGL - MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
// Copyright (c) 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 "DriverBugProbes.h"
#include "PersistentBufferOrderingProbe.h"
#include <Config.h>
#include <MG_Util/Debug/Log.h>
#include <algorithm>
#include <cstring>
#include <optional>
#include <string>
namespace MobileGL::MG_Util::SelfTest {
namespace {
using MG_External::GLESFunctionsTable;
constexpr const char* kGeometryProbeName = "geometry write-after-emit";
constexpr GLuint kProbeMagic = 7u;
constexpr GLsizei kProbeSize = 16;
// Binding 0 carries the write issued BEFORE EmitVertex (the control), binding 1 the
// write issued AFTER it (the subject). Same shader, same draw, same buffer shape - the
// only difference between them is where the store sits.
constexpr GLuint kBeforeEmitBinding = 0;
constexpr GLuint kAfterEmitBinding = 1;
const char* const kProbeVertexSource =
"#version 320 es\n"
"void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }\n";
// No gl_PointSize anywhere: writing it from a geometry shader needs
// EXT/OES_geometry_point_size, which not every ES 3.2 driver exposes (Mesa's does not),
// and a probe that fails to COMPILE reaches no verdict at all.
const char* const kProbeGeometrySource =
"#version 320 es\n"
"layout(points) in;\n"
"layout(points, max_vertices = 1) out;\n"
"layout(std430, binding = 0) coherent buffer BeforeEmit { uint data[4]; } g_before;\n"
"layout(std430, binding = 1) coherent buffer AfterEmit { uint data[4]; } g_after;\n"
"void main() {\n"
" g_before.data[0] = 7u;\n"
" gl_Position = gl_in[0].gl_Position;\n"
" EmitVertex();\n"
" EndPrimitive();\n"
" g_after.data[0] = 7u;\n"
"}\n";
const char* const kProbeFragmentSource =
"#version 320 es\n"
"precision highp float;\n"
"layout(location = 0) out vec4 o_color;\n"
"void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }\n";
Bool HasEveryEntryPoint(const GLESFunctionsTable& gl) {
return gl.glCreateShader && gl.glShaderSource && gl.glCompileShader && gl.glGetShaderiv &&
gl.glGetShaderInfoLog && gl.glCreateProgram && gl.glAttachShader && gl.glLinkProgram &&
gl.glGetProgramiv && gl.glDeleteShader && gl.glDeleteProgram && gl.glUseProgram &&
gl.glGenBuffers && gl.glBindBuffer && gl.glBufferData && gl.glBindBufferBase &&
gl.glDeleteBuffers && gl.glGenVertexArrays && gl.glBindVertexArray &&
gl.glDeleteVertexArrays && gl.glGenFramebuffers && gl.glBindFramebuffer &&
gl.glFramebufferRenderbuffer && gl.glCheckFramebufferStatus && gl.glDeleteFramebuffers &&
gl.glGenRenderbuffers && gl.glBindRenderbuffer && gl.glRenderbufferStorage &&
gl.glDeleteRenderbuffers && gl.glViewport && gl.glDrawArrays && gl.glMemoryBarrier &&
gl.glMapBufferRange && gl.glUnmapBuffer && gl.glGetIntegerv && gl.glGetIntegeri_v &&
gl.glGetError && gl.glFinish && gl.glEnable && gl.glDisable && gl.glIsEnabled;
}
void Drain(const GLESFunctionsTable& gl) {
// Bounded: a driver that returns an error forever must not hang the probe.
for (Int i = 0; i < 32 && gl.glGetError() != GL_NO_ERROR; ++i) {
}
}
GLuint CompileStage(const GLESFunctionsTable& gl, GLenum stage, const char* source,
const char* stageName, const char* probeName) {
const GLuint shader = gl.glCreateShader(stage);
if (shader == 0) return 0;
gl.glShaderSource(shader, 1, &source, nullptr);
gl.glCompileShader(shader);
GLint compiled = 0;
gl.glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
// Bounded (at most three lines, once per process) and worth every one: a probe
// that cannot build its own subject reaches no verdict, and without the driver's
// reason that is indistinguishable from a clean driver.
char log[512] = {0};
gl.glGetShaderInfoLog(shader, static_cast<GLsizei>(sizeof(log) - 1), nullptr, log);
MGLOG_I("[driver-bug] %s probe: %s stage did not compile: %s", probeName, stageName, log);
gl.glDeleteShader(shader);
return 0;
}
return shader;
}
// Every piece of GL state the probe disturbs, captured on the way in and put back on
// the way out. It runs inside the POST context, which is not allowed to notice.
// The image unit every image-using probe binds to. One unit for all of them keeps the
// saved/restored set small, and nothing in the POST context is using it.
constexpr GLuint kProbeImageUnit = 1;
struct SavedState {
GLint program = 0;
GLint vertexArray = 0;
GLint drawFramebuffer = 0;
GLint readFramebuffer = 0;
GLint renderbuffer = 0;
GLint storageBuffer = 0;
GLint viewport[4] = {0, 0, 0, 0};
GLint indexedStorageBuffer[2] = {0, 0};
GLboolean rasterizerDiscard = GL_FALSE;
GLboolean scissorTest = GL_FALSE;
GLboolean cullFace = GL_FALSE;
// Everything the texture- and image-based probes disturb. Saved unconditionally
// (a query is cheaper than deciding which probe ran) and restored in the same call,
// so a probe cannot leave a binding behind for the next one to trip over.
GLboolean depthTest = GL_FALSE;
GLboolean blend = GL_FALSE;
GLint activeTexture = GL_TEXTURE0;
GLint texture2D = 0;
GLint texture2DMultisample = 0;
GLint texture2DArray = 0;
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
GLint packAlignment = 4;
GLint packRowLength = 0;
// The rest of the pixel-transfer scope. The probes that upload or read back texels
// run under whatever scope their caller left - the lazy ones run from live paths,
// not just the POST screen - and a caller's skip/row-length/PBO would silently
// shear a probe's own data. Saved so a probe can zero them and the caller gets
// them back.
GLint packSkipPixels = 0;
GLint packSkipRows = 0;
GLint unpackAlignment = 4;
GLint unpackRowLength = 0;
GLint unpackImageHeight = 0;
GLint unpackSkipPixels = 0;
GLint unpackSkipRows = 0;
GLint unpackSkipImages = 0;
GLint pixelPackBuffer = 0;
GLint pixelUnpackBuffer = 0;
GLint imageName = 0;
GLint imageLevel = 0;
GLint imageLayered = 0;
GLint imageLayer = 0;
GLint imageAccess = GL_READ_ONLY;
GLint imageFormat = GL_R32UI;
};
// The texture and image parts of the state are only queryable when the driver resolved
// the entry points that read them; a probe that never touches them still restores the
// rest.
void Save(const GLESFunctionsTable& gl, SavedState& state) {
gl.glGetIntegerv(GL_CURRENT_PROGRAM, &state.program);
gl.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &state.vertexArray);
gl.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &state.drawFramebuffer);
gl.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &state.readFramebuffer);
gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &state.renderbuffer);
gl.glGetIntegerv(GL_SHADER_STORAGE_BUFFER_BINDING, &state.storageBuffer);
gl.glGetIntegerv(GL_VIEWPORT, state.viewport);
for (GLuint i = 0; i < 2; ++i) {
gl.glGetIntegeri_v(GL_SHADER_STORAGE_BUFFER_BINDING, i, &state.indexedStorageBuffer[i]);
}
state.rasterizerDiscard = gl.glIsEnabled(GL_RASTERIZER_DISCARD);
state.scissorTest = gl.glIsEnabled(GL_SCISSOR_TEST);
state.cullFace = gl.glIsEnabled(GL_CULL_FACE);
state.depthTest = gl.glIsEnabled(GL_DEPTH_TEST);
state.blend = gl.glIsEnabled(GL_BLEND);
gl.glGetIntegerv(GL_ACTIVE_TEXTURE, &state.activeTexture);
// Unit 0 is selected BEFORE the per-unit bindings are read, because Restore puts them
// back on unit 0 unconditionally. Reading them off whatever unit happened to be
// active and writing them to unit 0 would corrupt unit 0's binding for whoever runs
// next - harmless while every probe ran from the POST screen with nothing else using
// the context, and not harmless now that one of them runs from a live draw path.
if (gl.glActiveTexture != nullptr) gl.glActiveTexture(GL_TEXTURE0);
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &state.texture2D);
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &state.texture2DMultisample);
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &state.texture2DArray);
gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment);
gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength);
gl.glGetIntegerv(GL_PACK_SKIP_PIXELS, &state.packSkipPixels);
gl.glGetIntegerv(GL_PACK_SKIP_ROWS, &state.packSkipRows);
gl.glGetIntegerv(GL_UNPACK_ALIGNMENT, &state.unpackAlignment);
gl.glGetIntegerv(GL_UNPACK_ROW_LENGTH, &state.unpackRowLength);
gl.glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, &state.unpackImageHeight);
gl.glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &state.unpackSkipPixels);
gl.glGetIntegerv(GL_UNPACK_SKIP_ROWS, &state.unpackSkipRows);
gl.glGetIntegerv(GL_UNPACK_SKIP_IMAGES, &state.unpackSkipImages);
gl.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &state.pixelPackBuffer);
gl.glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &state.pixelUnpackBuffer);
if (gl.glGetFloatv != nullptr) {
gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, state.clearColor);
}
gl.glGetIntegeri_v(GL_IMAGE_BINDING_NAME, kProbeImageUnit, &state.imageName);
gl.glGetIntegeri_v(GL_IMAGE_BINDING_LEVEL, kProbeImageUnit, &state.imageLevel);
gl.glGetIntegeri_v(GL_IMAGE_BINDING_LAYERED, kProbeImageUnit, &state.imageLayered);
gl.glGetIntegeri_v(GL_IMAGE_BINDING_LAYER, kProbeImageUnit, &state.imageLayer);
gl.glGetIntegeri_v(GL_IMAGE_BINDING_ACCESS, kProbeImageUnit, &state.imageAccess);
gl.glGetIntegeri_v(GL_IMAGE_BINDING_FORMAT, kProbeImageUnit, &state.imageFormat);
// Any of the above may be rejected by a driver that does not know the pname; the
// fields keep their defaults and the restore below puts those back, which is the
// right answer for a context nobody else is sharing.
Drain(gl);
}
// Null-safe throughout: the state it puts back is the superset of what any probe
// disturbs, and a probe that never needed (say) storage buffers is allowed to run on a
// driver table where they were never resolved.
void Restore(const GLESFunctionsTable& gl, const SavedState& state) {
if (gl.glBindBufferBase != nullptr) {
for (GLuint i = 0; i < 2; ++i) {
gl.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, i,
static_cast<GLuint>(state.indexedStorageBuffer[i]));
}
}
if (gl.glBindBuffer != nullptr) {
gl.glBindBuffer(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(state.storageBuffer));
}
if (gl.glBindRenderbuffer != nullptr) {
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(state.renderbuffer));
}
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(state.drawFramebuffer));
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(state.readFramebuffer));
gl.glBindVertexArray(static_cast<GLuint>(state.vertexArray));
gl.glUseProgram(static_cast<GLuint>(state.program));
gl.glViewport(state.viewport[0], state.viewport[1], state.viewport[2], state.viewport[3]);
if (gl.glBindImageTexture != nullptr) {
gl.glBindImageTexture(kProbeImageUnit, static_cast<GLuint>(state.imageName),
state.imageLevel, state.imageLayered != 0 ? GL_TRUE : GL_FALSE,
state.imageLayer, static_cast<GLenum>(state.imageAccess),
static_cast<GLenum>(state.imageFormat));
}
if (gl.glActiveTexture != nullptr) {
gl.glActiveTexture(GL_TEXTURE0);
if (gl.glBindTexture != nullptr) {
gl.glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(state.texture2D));
gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE,
static_cast<GLuint>(state.texture2DMultisample));
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, static_cast<GLuint>(state.texture2DArray));
}
gl.glActiveTexture(static_cast<GLenum>(state.activeTexture));
}
if (gl.glPixelStorei != nullptr) {
gl.glPixelStorei(GL_PACK_ALIGNMENT, state.packAlignment);
gl.glPixelStorei(GL_PACK_ROW_LENGTH, state.packRowLength);
gl.glPixelStorei(GL_PACK_SKIP_PIXELS, state.packSkipPixels);
gl.glPixelStorei(GL_PACK_SKIP_ROWS, state.packSkipRows);
gl.glPixelStorei(GL_UNPACK_ALIGNMENT, state.unpackAlignment);
gl.glPixelStorei(GL_UNPACK_ROW_LENGTH, state.unpackRowLength);
gl.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, state.unpackImageHeight);
gl.glPixelStorei(GL_UNPACK_SKIP_PIXELS, state.unpackSkipPixels);
gl.glPixelStorei(GL_UNPACK_SKIP_ROWS, state.unpackSkipRows);
gl.glPixelStorei(GL_UNPACK_SKIP_IMAGES, state.unpackSkipImages);
}
if (gl.glBindBuffer != nullptr) {
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(state.pixelPackBuffer));
gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(state.pixelUnpackBuffer));
}
if (gl.glClearColor != nullptr) {
gl.glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
state.clearColor[3]);
}
if (state.rasterizerDiscard) gl.glEnable(GL_RASTERIZER_DISCARD);
if (state.scissorTest) gl.glEnable(GL_SCISSOR_TEST);
if (state.cullFace) gl.glEnable(GL_CULL_FACE);
if (state.depthTest) gl.glEnable(GL_DEPTH_TEST);
if (state.blend) gl.glEnable(GL_BLEND);
Drain(gl);
}
GLuint ReadFirstWord(const GLESFunctionsTable& gl, GLuint buffer) {
gl.glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
const void* mapped =
gl.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, kProbeSize, GL_MAP_READ_BIT);
if (mapped == nullptr) return 0u;
GLuint value = 0;
std::memcpy(&value, mapped, sizeof(value));
gl.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
return value;
}
} // namespace
Bool ProbeGeometryStageSsboWriteAfterEmitDropped(const GLESFunctionsTable& gl) {
if (!HasEveryEntryPoint(gl)) return false;
// Nothing to measure if the driver serves no geometry storage blocks at all.
GLint advertisedGeometryBlocks = 0;
Drain(gl);
gl.glGetIntegerv(GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS, &advertisedGeometryBlocks);
if (gl.glGetError() != GL_NO_ERROR || advertisedGeometryBlocks < 2) {
Drain(gl);
return false;
}
SavedState saved;
Save(gl, saved);
Drain(gl);
Bool dropped = false;
const char* inconclusive = nullptr;
GLuint vertexShader = 0, geometryShader = 0, fragmentShader = 0, program = 0;
GLuint buffers[2] = {0, 0};
GLuint vertexArray = 0, framebuffer = 0, renderbuffer = 0;
do {
vertexShader = CompileStage(gl, GL_VERTEX_SHADER, kProbeVertexSource, "vertex",
kGeometryProbeName);
geometryShader = CompileStage(gl, GL_GEOMETRY_SHADER, kProbeGeometrySource, "geometry",
kGeometryProbeName);
fragmentShader = CompileStage(gl, GL_FRAGMENT_SHADER, kProbeFragmentSource, "fragment",
kGeometryProbeName);
if (vertexShader == 0 || geometryShader == 0 || fragmentShader == 0) {
inconclusive = "one of the probe stages did not compile";
break;
}
program = gl.glCreateProgram();
if (program == 0) {
inconclusive = "glCreateProgram returned 0";
break;
}
gl.glAttachShader(program, vertexShader);
gl.glAttachShader(program, geometryShader);
gl.glAttachShader(program, fragmentShader);
gl.glLinkProgram(program);
GLint linked = 0;
gl.glGetProgramiv(program, GL_LINK_STATUS, &linked);
// A driver that REFUSES the program is being honest about not supporting it; that
// is not the silent drop this looks for.
if (linked == GL_FALSE) {
inconclusive = "the driver refused to link the probe program, which is an honest "
"refusal rather than a silent drop";
break;
}
gl.glGenBuffers(2, buffers);
const GLuint zero[4] = {0u, 0u, 0u, 0u};
for (GLuint i = 0; i < 2; ++i) {
gl.glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[i]);
gl.glBufferData(GL_SHADER_STORAGE_BUFFER, kProbeSize, zero, GL_DYNAMIC_DRAW);
gl.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, i, buffers[i]);
}
gl.glGenRenderbuffers(1, &renderbuffer);
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
gl.glGenFramebuffers(1, &framebuffer);
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
gl.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
renderbuffer);
if (gl.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
inconclusive = "the probe's own 4x4 RGBA8 framebuffer came back incomplete";
break;
}
gl.glViewport(0, 0, 4, 4);
gl.glGenVertexArrays(1, &vertexArray);
gl.glBindVertexArray(vertexArray);
gl.glDisable(GL_RASTERIZER_DISCARD);
gl.glDisable(GL_SCISSOR_TEST);
gl.glDisable(GL_CULL_FACE);
gl.glUseProgram(program);
Drain(gl);
gl.glDrawArrays(GL_POINTS, 0, 1);
if (const GLenum drawError = gl.glGetError(); drawError != GL_NO_ERROR) {
inconclusive = "the probe draw itself raised a GL error";
MGLOG_I("[driver-bug] geometry write-after-emit probe: draw error 0x%x", drawError);
break;
}
gl.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
gl.glFinish();
const GLuint beforeEmit = ReadFirstWord(gl, buffers[kBeforeEmitBinding]);
const GLuint afterEmit = ReadFirstWord(gl, buffers[kAfterEmitBinding]);
// The control decides whether the subject means anything. If the BEFORE-emit write
// did not land either, this driver's geometry stage cannot write storage buffers at
// all - a different (and much larger) claim, which this probe may not make.
if (beforeEmit != kProbeMagic) {
inconclusive = "the before-emit control write did not land either, so the "
"after-emit result says nothing about emit ordering";
} else {
dropped = afterEmit != kProbeMagic;
}
MGLOG_I("[driver-bug] geometry write-after-emit probe: advertised %d block(s); "
"before-emit write=%u after-emit write=%u (expected %u each)%s",
advertisedGeometryBlocks, beforeEmit, afterEmit, kProbeMagic,
dropped ? " - WRITES AFTER EmitVertex ARE DISCARDED" : "");
} while (false);
if (inconclusive != nullptr) {
MGLOG_I("[driver-bug] geometry write-after-emit probe reached no verdict (%s)",
inconclusive);
}
if (vertexArray != 0) gl.glDeleteVertexArrays(1, &vertexArray);
if (framebuffer != 0) gl.glDeleteFramebuffers(1, &framebuffer);
if (renderbuffer != 0) gl.glDeleteRenderbuffers(1, &renderbuffer);
if (buffers[0] != 0) gl.glDeleteBuffers(2, buffers);
if (program != 0) gl.glDeleteProgram(program);
if (vertexShader != 0) gl.glDeleteShader(vertexShader);
if (geometryShader != 0) gl.glDeleteShader(geometryShader);
if (fragmentShader != 0) gl.glDeleteShader(fragmentShader);
Restore(gl, saved);
return dropped;
}
Bool GeometryStageSsboWriteAfterEmitDropped(const GLESFunctionsTable& gl) {
// One driver per process, and the answer is structural rather than sampled.
static const Bool dropped = ProbeGeometryStageSsboWriteAfterEmitDropped(gl);
return dropped;
}
namespace {
// ===================== SHARED PROGRAM BUILDING =====================
// The four probes below all decide their verdict from whether a program LINKS or from
// what a draw wrote, so they share one builder that keeps the two apart: a stage that
// does not COMPILE means the probe could not build its own subject (inconclusive),
// while a program that compiles and does not LINK is a result.
struct StageSource {
GLenum stage = GL_VERTEX_SHADER;
String source;
const char* name = "";
};
struct ProgramBuild {
GLuint program = 0;
Bool compiled = false;
Bool linked = false;
String infoLog;
};
ProgramBuild BuildProgram(const GLESFunctionsTable& gl, const Vector<StageSource>& stages,
const char* probeName) {
ProgramBuild build;
Vector<GLuint> shaders;
const auto releaseShaders = [&]() {
for (const GLuint shader : shaders) {
gl.glDeleteShader(shader);
}
};
for (const StageSource& stage : stages) {
const GLuint shader =
CompileStage(gl, stage.stage, stage.source.c_str(), stage.name, probeName);
if (shader == 0) {
releaseShaders();
return build;
}
shaders.push_back(shader);
}
build.compiled = true;
build.program = gl.glCreateProgram();
if (build.program == 0) {
build.compiled = false;
releaseShaders();
return build;
}
for (const GLuint shader : shaders) {
gl.glAttachShader(build.program, shader);
}
gl.glLinkProgram(build.program);
GLint linked = 0;
gl.glGetProgramiv(build.program, GL_LINK_STATUS, &linked);
build.linked = linked != GL_FALSE;
char log[512] = {0};
gl.glGetProgramInfoLog(build.program, static_cast<GLsizei>(sizeof(log) - 1), nullptr, log);
build.infoLog = log;
releaseShaders();
return build;
}
// The driver's own words, trimmed to the first line: a report that quotes the linker is
// far more actionable than one that paraphrases it, but the whole log is multi-line and
// this is a one-liner.
String FirstLine(const String& text) {
const SizeT end = text.find_first_of("\r\n");
String line = end == String::npos ? text : text.substr(0, end);
while (!line.empty() && (line.back() == ' ' || line.back() == '\t')) {
line.pop_back();
}
return line;
}
// Every probe below rasterizes the same full-viewport quad from a vertex ID, so none of
// them needs a vertex buffer or an attribute.
const char* const kFullscreenQuadBody =
" vec2 mg_p = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,\n"
" (gl_VertexID & 2) == 0 ? -1.0 : 1.0);\n"
" gl_Position = vec4(mg_p, 0.0, 1.0);\n";
const char* const kProbeQuadVertexSource =
"#version 320 es\n"
"void main()\n{\n"
" vec2 mg_p = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,\n"
" (gl_VertexID & 2) == 0 ? -1.0 : 1.0);\n"
" gl_Position = vec4(mg_p, 0.0, 1.0);\n}\n";
// Puts the context into the shape every probe draw wants. The caller has already saved
// the state this disturbs.
void PrepareForProbeDraw(const GLESFunctionsTable& gl) {
gl.glDisable(GL_RASTERIZER_DISCARD);
gl.glDisable(GL_SCISSOR_TEST);
gl.glDisable(GL_CULL_FACE);
gl.glDisable(GL_DEPTH_TEST);
gl.glDisable(GL_BLEND);
gl.glPixelStorei(GL_PACK_ALIGNMENT, 4);
gl.glPixelStorei(GL_PACK_ROW_LENGTH, 0);
}
// ===================== R32F MULTISAMPLE SWIZZLE =====================
constexpr const char* kMsaaProbeName = "R32F multisample swizzle";
constexpr GLsizei kMsaaOutputSize = 8;
// The fill draw writes 1.0 to red and nothing else, so an R32F texel reads back as
// (1, 0, 0, 1) - the missing green/blue default to 0 and the missing alpha to 1. With
// GL_TEXTURE_SWIZZLE_A pointed at RED the .w a shader sees is that same 1.0, which is
// what makes a single expected constant enough for both the subject and the controls.
constexpr GLfloat kMsaaExpected = 1.0f;
// Neither 0 nor the expected value, so "the clear never landed" and "the draw never
// landed" are distinguishable from "the driver returned the wrong number".
constexpr GLfloat kMsaaClearSentinel = 7.0f;
Bool HasMsaaEntryPoints(const GLESFunctionsTable& gl) {
return gl.glCreateShader && gl.glShaderSource && gl.glCompileShader && gl.glGetShaderiv &&
gl.glGetShaderInfoLog && gl.glCreateProgram && gl.glAttachShader && gl.glLinkProgram &&
gl.glGetProgramiv && gl.glGetProgramInfoLog && gl.glDeleteShader &&
gl.glDeleteProgram && gl.glUseProgram && gl.glGenTextures && gl.glBindTexture &&
gl.glDeleteTextures && gl.glTexParameteri && gl.glTexImage2D &&
gl.glTexStorage2DMultisample && gl.glGenFramebuffers && gl.glBindFramebuffer &&
gl.glFramebufferTexture2D && gl.glCheckFramebufferStatus && gl.glDeleteFramebuffers &&
gl.glGenVertexArrays && gl.glBindVertexArray && gl.glDeleteVertexArrays &&
gl.glActiveTexture && gl.glGetUniformLocation && gl.glUniform1i && gl.glViewport &&
gl.glClear && gl.glClearColor && gl.glReadPixels && gl.glDrawArrays &&
gl.glPixelStorei && gl.glGetIntegerv && gl.glGetIntegeri_v && gl.glGetError &&
gl.glFinish && gl.glEnable && gl.glDisable && gl.glIsEnabled;
}
const char* const kMsaaFillFragmentSource =
"#version 320 es\n"
"precision highp float;\n"
"layout(location = 0) out highp vec4 o_color;\n"
"void main() { o_color = vec4(1.0, 0.0, 0.0, 0.0); }\n";
const char* const kMsaaSampleFragmentSource =
"#version 320 es\n"
"precision highp float;\n"
"layout(location = 0) out highp float o_color;\n"
"layout(location = 0) flat in highp float v_result;\n"
"void main() { o_color = v_result; }\n";
// One vertex shader per round. The `round` term is multiplied by zero, so it changes
// nothing about the result and everything about the source text - which is the point:
// the corruption only appears from the SECOND separately compiled sampling program
// onward, and a driver that recognised an identical source could hand back the first
// program's binary and hide it.
String BuildMsaaSampleVertexSource(Int sampleIndex, char component, Int round) {
return format("#version 320 es\n"
"precision highp float;\n"
"precision highp int;\n"
"uniform highp sampler2DMS mg_probeSampler;\n"
"layout(location = 0) flat out float v_result;\n"
"void main()\n{{\n"
" v_result = texelFetch(mg_probeSampler, ivec2(0), {}).{} + float({}) * 0.0;\n"
"{}}}\n",
sampleIndex, component, round, kFullscreenQuadBody);
}
// Samples one texel of `source` `rounds` times, each round through its own freshly
// linked program and its own freshly created R32F output texture, and stores what came
// back. Returns false when any round could not be set up at all.
Bool RunMsaaSampledRead(const GLESFunctionsTable& gl, GLuint source, Int sampleIndex,
char component, Int rounds, Vector<GLfloat>& values) {
values.clear();
for (Int round = 0; round < rounds; ++round) {
GLuint output = 0;
GLuint framebuffer = 0;
Bool ok = false;
ProgramBuild build;
do {
gl.glGenTextures(1, &output);
gl.glBindTexture(GL_TEXTURE_2D, output);
gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, kMsaaOutputSize, kMsaaOutputSize, 0,
GL_RED, GL_FLOAT, nullptr);
gl.glGenFramebuffers(1, &framebuffer);
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
output, 0);
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) break;
build = BuildProgram(gl,
{{GL_VERTEX_SHADER,
BuildMsaaSampleVertexSource(sampleIndex, component, round),
"vertex"},
{GL_FRAGMENT_SHADER, kMsaaSampleFragmentSource, "fragment"}},
kMsaaProbeName);
if (!build.linked) break;
gl.glUseProgram(build.program);
gl.glActiveTexture(GL_TEXTURE0);
gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, source);
gl.glUniform1i(gl.glGetUniformLocation(build.program, "mg_probeSampler"), 0);
gl.glViewport(0, 0, kMsaaOutputSize, kMsaaOutputSize);
gl.glClearColor(kMsaaClearSentinel, kMsaaClearSentinel, kMsaaClearSentinel,
kMsaaClearSentinel);
gl.glClear(GL_COLOR_BUFFER_BIT);
Drain(gl);
gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if (gl.glGetError() != GL_NO_ERROR) break;
gl.glFinish();
Vector<GLfloat> pixels(static_cast<SizeT>(kMsaaOutputSize) * kMsaaOutputSize, 0.0f);
gl.glReadPixels(0, 0, kMsaaOutputSize, kMsaaOutputSize, GL_RED, GL_FLOAT,
pixels.data());
if (gl.glGetError() != GL_NO_ERROR) break;
values.push_back(pixels[0]);
ok = true;
} while (false);
if (build.program != 0) gl.glDeleteProgram(build.program);
if (framebuffer != 0) gl.glDeleteFramebuffers(1, &framebuffer);
if (output != 0) gl.glDeleteTextures(1, &output);
if (!ok) return false;
}
return true;
}
// Creates a `samples`-sample R32F multisample texture, fills it with (1, 0, 0, 0), and
// points GL_TEXTURE_SWIZZLE_R/G/B at ALPHA/BLUE/GREEN with GL_TEXTURE_SWIZZLE_A at
// `alphaSwizzle`. Every source the probe builds is identical except for that one enum.
GLuint MakeSwizzledMultisampleSource(const GLESFunctionsTable& gl, GLsizei samples,
GLenum alphaSwizzle) {
GLuint texture = 0;
gl.glGenTextures(1, &texture);
gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
gl.glTexStorage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_R32F, 1, 1, GL_FALSE);
if (gl.glGetError() != GL_NO_ERROR) {
gl.glDeleteTextures(1, &texture);
return 0;
}
GLuint framebuffer = 0;
gl.glGenFramebuffers(1, &framebuffer);
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE,
texture, 0);
Bool filled = false;
ProgramBuild fill;
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
fill = BuildProgram(gl,
{{GL_VERTEX_SHADER, kProbeQuadVertexSource, "vertex"},
{GL_FRAGMENT_SHADER, kMsaaFillFragmentSource, "fragment"}},
kMsaaProbeName);
if (fill.linked) {
gl.glUseProgram(fill.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_TRIANGLE_STRIP, 0, 4);
filled = gl.glGetError() == GL_NO_ERROR;
gl.glFinish();
}
}
if (fill.program != 0) gl.glDeleteProgram(fill.program);
gl.glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (framebuffer != 0) gl.glDeleteFramebuffers(1, &framebuffer);
if (!filled) {
gl.glDeleteTextures(1, &texture);
return 0;
}
gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
gl.glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_R, GL_ALPHA);
gl.glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_G, GL_BLUE);
gl.glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_B, GL_GREEN);
gl.glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_A, alphaSwizzle);
Drain(gl);
return texture;
}
Bool EveryRoundIsExpected(const Vector<GLfloat>& values) {
if (values.empty()) return false;
for (const GLfloat value : values) {
if (value != kMsaaExpected) return false;
}
return true;
}
String DescribeRounds(const Vector<GLfloat>& values) {
String description;
for (SizeT i = 0; i < values.size(); ++i) {
if (i != 0) description += ", ";
description += format("round{}={}", i, values[i]);
}
return description;
}
// ===================== IMAGE LOCATION PER NAME =====================
constexpr const char* kImageLocationProbeName = "image-location-per-name";
Bool HasImageProgramEntryPoints(const GLESFunctionsTable& gl) {
return gl.glCreateShader && gl.glShaderSource && gl.glCompileShader && gl.glGetShaderiv &&
gl.glGetShaderInfoLog && gl.glCreateProgram && gl.glAttachShader && gl.glLinkProgram &&
gl.glGetProgramiv && gl.glGetProgramInfoLog && gl.glDeleteShader &&
gl.glDeleteProgram && gl.glGetIntegerv && gl.glGetIntegeri_v && gl.glGetError &&
gl.glIsEnabled;
}
// One stage declaring `count` image uniforms named `namePrefix`0..count-1, each stored to
// so the compiler cannot eliminate it. The bindings cycle through the driver's image
// units, so the subject and the control occupy exactly the same units - the names are
// the only thing that differs between them.
String BuildImageNameStageSource(GLenum stage, Int count, Int units, const char* namePrefix) {
String source = "#version 320 es\n";
if (stage == GL_GEOMETRY_SHADER) {
source += "layout(points) in;\nlayout(points, max_vertices = 1) out;\n";
}
source += "precision highp float;\nprecision highp int;\n";
for (Int i = 0; i < count; ++i) {
source += format("layout(binding = {}, rgba32f) uniform writeonly highp image2D {}{};\n",
i % units, namePrefix, i);
}
if (stage == GL_FRAGMENT_SHADER) {
source += "layout(location = 0) out highp vec4 o_color;\n";
}
source += "void main()\n{\n";
for (Int i = 0; i < count; ++i) {
source += format(" imageStore({}{}, ivec2(0), vec4({}.0));\n", namePrefix, i, i);
}
switch (stage) {
case GL_VERTEX_SHADER:
source += " gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n";
break;
case GL_GEOMETRY_SHADER:
source += " gl_Position = gl_in[0].gl_Position;\n EmitVertex();\n EndPrimitive();\n";
break;
default:
source += " o_color = vec4(0.0);\n";
break;
}
source += "}\n";
return source;
}
ProgramBuild BuildThreeStageImageProgram(const GLESFunctionsTable& gl, Int count, Int units,
const char* vertexPrefix, const char* geometryPrefix,
const char* fragmentPrefix) {
return BuildProgram(
gl,
{{GL_VERTEX_SHADER, BuildImageNameStageSource(GL_VERTEX_SHADER, count, units, vertexPrefix),
"vertex"},
{GL_GEOMETRY_SHADER,
BuildImageNameStageSource(GL_GEOMETRY_SHADER, count, units, geometryPrefix), "geometry"},
{GL_FRAGMENT_SHADER,
BuildImageNameStageSource(GL_FRAGMENT_SHADER, count, units, fragmentPrefix), "fragment"}},
kImageLocationProbeName);
}
// ===================== SHARED IMAGE-DRAW SCAFFOLDING =====================
// Both remaining probes bind one rgba32f image, draw a quad into a small RGBA8 target,
// and count the texels the shader painted red. Sharing the scaffolding keeps what
// differs between them - the shader text - the only thing either probe has to explain.
constexpr GLubyte kProbePassColor[4] = {0, 255, 0, 255};
struct ImageDrawTargets {
GLuint image = 0;
GLuint color = 0;
GLuint framebuffer = 0;
GLsizei size = 0;
Bool valid = false;
};
Bool HasImageDrawEntryPoints(const GLESFunctionsTable& gl) {
return HasImageProgramEntryPoints(gl) && gl.glUseProgram && gl.glGenTextures &&
gl.glBindTexture && gl.glDeleteTextures && gl.glTexStorage2D && gl.glTexSubImage2D &&
gl.glGenFramebuffers && gl.glBindFramebuffer && gl.glFramebufferTexture2D &&
gl.glCheckFramebufferStatus && gl.glDeleteFramebuffers && gl.glGenVertexArrays &&
gl.glBindVertexArray && gl.glDeleteVertexArrays && gl.glBindImageTexture &&
gl.glViewport && gl.glClear && gl.glClearColor && gl.glReadPixels && gl.glDrawArrays &&
gl.glMemoryBarrier && gl.glPixelStorei && gl.glFinish && gl.glEnable && gl.glDisable;
}
ImageDrawTargets MakeImageDrawTargets(const GLESFunctionsTable& gl, GLsizei size) {
ImageDrawTargets targets;
targets.size = size;
gl.glGenTextures(1, &targets.image);
gl.glBindTexture(GL_TEXTURE_2D, targets.image);
gl.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, size, size);
gl.glGenTextures(1, &targets.color);
gl.glBindTexture(GL_TEXTURE_2D, targets.color);
gl.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, size, size);
gl.glGenFramebuffers(1, &targets.framebuffer);
gl.glBindFramebuffer(GL_FRAMEBUFFER, targets.framebuffer);
gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, targets.color,
0);
targets.valid = gl.glGetError() == GL_NO_ERROR &&
gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
Drain(gl);
return targets;
}
void ReleaseImageDrawTargets(const GLESFunctionsTable& gl, ImageDrawTargets& targets) {
if (targets.framebuffer != 0) gl.glDeleteFramebuffers(1, &targets.framebuffer);
if (targets.color != 0) gl.glDeleteTextures(1, &targets.color);
if (targets.image != 0) gl.glDeleteTextures(1, &targets.image);
targets = ImageDrawTargets{};
}
// Zeroes the image so a stale value from an earlier draw can never stand in for a store
// that did not happen.
void ZeroProbeImage(const GLESFunctionsTable& gl, const ImageDrawTargets& targets) {
const SizeT texelCount = static_cast<SizeT>(targets.size) * targets.size * 4;
const Vector<GLfloat> zeroes(texelCount, 0.0f);
gl.glBindTexture(GL_TEXTURE_2D, targets.image);
gl.glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, targets.size, targets.size, GL_RGBA, GL_FLOAT,
zeroes.data());
gl.glMemoryBarrier(GL_ALL_BARRIER_BITS);
gl.glFinish();
Drain(gl);
}
// Reads the colour target back and counts the texels the shader did NOT paint with
// kProbePassColor. A negative result means the readback itself failed.
Int CountFailedTexels(const GLESFunctionsTable& gl, const ImageDrawTargets& targets) {
Vector<GLubyte> pixels(static_cast<SizeT>(targets.size) * targets.size * 4, 0);
Drain(gl);
gl.glReadPixels(0, 0, targets.size, targets.size, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
if (gl.glGetError() != GL_NO_ERROR) return -1;
Int failed = 0;
for (SizeT texel = 0; texel < pixels.size(); texel += 4) {
if (pixels[texel + 0] != kProbePassColor[0] || pixels[texel + 1] != kProbePassColor[1] ||
pixels[texel + 2] != kProbePassColor[2] || pixels[texel + 3] != kProbePassColor[3]) {
++failed;
}
}
return failed;
}
void BeginProbeDraw(const GLESFunctionsTable& gl, const ImageDrawTargets& targets) {
gl.glBindImageTexture(kProbeImageUnit, targets.image, 0, GL_FALSE, 0, GL_READ_WRITE,
GL_RGBA32F);
gl.glBindFramebuffer(GL_FRAMEBUFFER, targets.framebuffer);
gl.glViewport(0, 0, targets.size, targets.size);
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
gl.glClear(GL_COLOR_BUFFER_BIT);
}
// ===================== CROSS-STAGE QUALIFIER MERGE =====================
constexpr const char* kQualifierMergeProbeName = "cross-stage image qualifier merge";
constexpr GLsizei kQualifierMergeSize = 16;
// The value the vertex stage stores and the fragment stage expects to read back.
constexpr const char* kQualifierMergeValue = "2.0";
String BuildQualifierMergeVertexSource(const char* writeName) {
return format("#version 320 es\n"
"precision highp float;\n"
"precision highp int;\n"
"layout(binding = {}, rgba32f) uniform coherent writeonly highp image2D {};\n"
"void main()\n{{\n"
"{}"
" imageStore({}, ivec2(0), vec4({}));\n"
" memoryBarrier();\n}}\n",
kProbeImageUnit, writeName, kFullscreenQuadBody, writeName,
kQualifierMergeValue);
}
String BuildQualifierMergeFragmentSource(const char* readName) {
return format("#version 320 es\n"
"precision highp float;\n"
"precision highp int;\n"
"layout(binding = {}, rgba32f) uniform coherent readonly highp image2D {};\n"
"layout(location = 0) out highp vec4 o_color;\n"
"void main()\n{{\n"
" o_color = vec4(0.0, 1.0, 0.0, 1.0);\n"
" if (any(notEqual(imageLoad({}, ivec2(0)), vec4({}))))\n"
" {{\n o_color = vec4(1.0, 0.0, 0.0, 1.0);\n }}\n}}\n",
kProbeImageUnit, readName, readName, kQualifierMergeValue);
}
// Draws the pair once and reports how many fragments failed to see the vertex stage's
// store. -1 means the case could not be built or drawn at all.
Int RunQualifierMergeCase(const GLESFunctionsTable& gl, const ImageDrawTargets& targets,
const char* writeName, const char* readName) {
ProgramBuild build =
BuildProgram(gl,
{{GL_VERTEX_SHADER, BuildQualifierMergeVertexSource(writeName), "vertex"},
{GL_FRAGMENT_SHADER, BuildQualifierMergeFragmentSource(readName),
"fragment"}},
kQualifierMergeProbeName);
Int failed = -1;
if (build.linked) {
ZeroProbeImage(gl, targets);
BeginProbeDraw(gl, targets);
gl.glUseProgram(build.program);
Drain(gl);
gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if (gl.glGetError() == GL_NO_ERROR) {
gl.glMemoryBarrier(GL_ALL_BARRIER_BITS);
gl.glFinish();
failed = CountFailedTexels(gl, targets);
}
}
if (build.program != 0) gl.glDeleteProgram(build.program);
return failed;
}
// ===================== IMAGE COHERENCY RESIDUAL =====================
constexpr const char* kCoherencyProbeName = "image write-read coherency";
constexpr GLsizei kCoherencySize = 64;
// A split read-write image pair on ONE binding, storing and then reading the same texel
// back inside one invocation. `qualifiers` and `barriers` are what separate the SUBJECT
// (the strongest shape ESSL offers) from the shape MobileGL emits today; everything else
// about the two is identical, so the two numbers are comparable.
String BuildCoherencyFragmentSource(const char* qualifiers, const char* barriers) {
return format(
"#version 320 es\n"
"precision highp float;\n"
"precision highp int;\n"
"layout(binding = {}, rgba32f) uniform {} readonly highp image2D mg_probeImageRead;\n"
"layout(binding = {}, rgba32f) uniform {} writeonly highp image2D mg_probeImageWrite;\n"
"layout(location = 0) out highp vec4 o_color;\n"
"void main()\n{{\n"
" ivec2 mg_c = ivec2(gl_FragCoord.xy);\n"
" o_color = vec4(0.0, 1.0, 0.0, 1.0);\n"
" for (int mg_i = 0; mg_i < 3; ++mg_i)\n {{\n"
" imageStore(mg_probeImageWrite, mg_c, vec4(float(mg_i)));\n"
" {}\n"
" highp vec4 mg_v = imageLoad(mg_probeImageRead, mg_c);\n"
" if (any(notEqual(mg_v, vec4(float(mg_i)))))\n {{\n"
" o_color = vec4(1.0, 0.0, 0.0, 1.0);\n break;\n }}\n"
" }}\n}}\n",
kProbeImageUnit, qualifiers, kProbeImageUnit, qualifiers, barriers);
}
// Every qualifier and every barrier ESSL has for this. A driver that still misses the
// store here has nothing left to be told.
constexpr const char* kStrongestCoherencyQualifiers = "coherent volatile";
constexpr const char* kStrongestCoherencyBarriers = "memoryBarrierImage(); memoryBarrier();";
// What SplitReadWriteImageUniforms emits today, measured alongside so the row can report
// what applications actually get rather than only what is theoretically reachable.
constexpr const char* kEmittedCoherencyQualifiers = "coherent";
constexpr const char* kEmittedCoherencyBarriers = "memoryBarrierImage();";
// The control's two halves. Same image, same binding, same qualifiers, same dependency -
// the store and the read are just in different draws, with a glMemoryBarrier and a
// glFinish between them.
String BuildCoherencyControlStoreSource() {
return format(
"#version 320 es\n"
"precision highp float;\n"
"precision highp int;\n"
"layout(binding = {}, rgba32f) uniform coherent writeonly highp image2D mg_probeImageWrite;\n"
"layout(location = 0) out highp vec4 o_color;\n"
"void main()\n{{\n"
" ivec2 mg_c = ivec2(gl_FragCoord.xy);\n"
" imageStore(mg_probeImageWrite, mg_c, vec4(float(mg_c.x * {} + mg_c.y)));\n"
" memoryBarrierImage();\n"
" o_color = vec4(0.0, 1.0, 0.0, 1.0);\n}}\n",
kProbeImageUnit, kCoherencySize);
}
String BuildCoherencyControlLoadSource() {
return format(
"#version 320 es\n"
"precision highp float;\n"
"precision highp int;\n"
"layout(binding = {}, rgba32f) uniform coherent readonly highp image2D mg_probeImageRead;\n"
"layout(location = 0) out highp vec4 o_color;\n"
"void main()\n{{\n"
" ivec2 mg_c = ivec2(gl_FragCoord.xy);\n"
" highp vec4 mg_v = imageLoad(mg_probeImageRead, mg_c);\n"
" o_color = all(equal(mg_v, vec4(float(mg_c.x * {} + mg_c.y))))\n"
" ? vec4(0.0, 1.0, 0.0, 1.0)\n"
" : vec4(1.0, 0.0, 0.0, 1.0);\n}}\n",
kProbeImageUnit, kCoherencySize);
}
// One draw, store and dependent read inside the same invocation, in whichever
// qualifier/barrier shape the caller asked for.
Int RunCoherencyShape(const GLESFunctionsTable& gl, const ImageDrawTargets& targets,
const char* qualifiers, const char* barriers) {
ProgramBuild build = BuildProgram(
gl,
{{GL_VERTEX_SHADER, kProbeQuadVertexSource, "vertex"},
{GL_FRAGMENT_SHADER, BuildCoherencyFragmentSource(qualifiers, barriers), "fragment"}},
kCoherencyProbeName);
Int failed = -1;
if (build.linked) {
ZeroProbeImage(gl, targets);
BeginProbeDraw(gl, targets);
gl.glUseProgram(build.program);
Drain(gl);
gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if (gl.glGetError() == GL_NO_ERROR) {
gl.glMemoryBarrier(GL_ALL_BARRIER_BITS);
gl.glFinish();
failed = CountFailedTexels(gl, targets);
}
}
if (build.program != 0) gl.glDeleteProgram(build.program);
return failed;
}
// The control: the same store and the same dependent read, split across two draws with a
// glMemoryBarrier and a glFinish between them.
Int RunCoherencyControl(const GLESFunctionsTable& gl, const ImageDrawTargets& targets) {
ProgramBuild store =
BuildProgram(gl,
{{GL_VERTEX_SHADER, kProbeQuadVertexSource, "vertex"},
{GL_FRAGMENT_SHADER, BuildCoherencyControlStoreSource(), "fragment"}},
kCoherencyProbeName);
ProgramBuild load =
BuildProgram(gl,
{{GL_VERTEX_SHADER, kProbeQuadVertexSource, "vertex"},
{GL_FRAGMENT_SHADER, BuildCoherencyControlLoadSource(), "fragment"}},
kCoherencyProbeName);
Int failed = -1;
if (store.linked && load.linked) {
ZeroProbeImage(gl, targets);
BeginProbeDraw(gl, targets);
gl.glUseProgram(store.program);
Drain(gl);
gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
gl.glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
gl.glFinish();
gl.glUseProgram(load.program);
gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if (gl.glGetError() == GL_NO_ERROR) {
gl.glMemoryBarrier(GL_ALL_BARRIER_BITS);
gl.glFinish();
failed = CountFailedTexels(gl, targets);
}
}
if (store.program != 0) gl.glDeleteProgram(store.program);
if (load.program != 0) gl.glDeleteProgram(load.program);
return failed;
}
} // namespace
Bool ProbeR32FMultisampleSwizzleCorruption(const GLESFunctionsTable& gl) {
if (!HasMsaaEntryPoints(gl)) return false;
// A sample index other than 0 is the whole subject, so a driver that cannot host a
// multisample colour target at all has nothing to say here.
Drain(gl);
GLint maxColorTextureSamples = 0;
gl.glGetIntegerv(GL_MAX_COLOR_TEXTURE_SAMPLES, &maxColorTextureSamples);
const Bool sampleQueryAccepted = gl.glGetError() == GL_NO_ERROR;
Drain(gl);
if (!sampleQueryAccepted || maxColorTextureSamples < 2) return false;
const GLsizei samples =
maxColorTextureSamples >= 4 ? 4 : static_cast<GLsizei>(maxColorTextureSamples);
const Int lastSample = static_cast<Int>(samples) - 1;
SavedState saved;
Save(gl, saved);
Drain(gl);
Bool corrupted = false;
const char* inconclusive = nullptr;
GLuint vertexArray = 0;
GLuint swizzledSource = 0;
GLuint defaultSwizzleSource = 0;
Vector<GLfloat> subject;
Vector<GLfloat> controlDefaultSwizzle;
Vector<GLfloat> controlSampleZero;
Vector<GLfloat> controlRedComponent;
do {
gl.glGenVertexArrays(1, &vertexArray);
gl.glBindVertexArray(vertexArray);
PrepareForProbeDraw(gl);
// Two sources, identical but for GL_TEXTURE_SWIZZLE_A. Separate textures rather than
// one re-swizzled between reads: re-pushing the swizzle is itself a variable, and it
// was measured not to be the one that matters.
swizzledSource = MakeSwizzledMultisampleSource(gl, samples, GL_RED);
defaultSwizzleSource = MakeSwizzledMultisampleSource(gl, samples, GL_ALPHA);
if (swizzledSource == 0 || defaultSwizzleSource == 0) {
inconclusive = "the driver would not host a filled R32F multisample colour target";
break;
}
// Two rounds each: the corruption only appears from the second separately linked
// sampling program onward, so a single read would call an affected driver clean.
constexpr Int kRounds = 2;
if (!RunMsaaSampledRead(gl, swizzledSource, lastSample, 'w', kRounds, subject) ||
!RunMsaaSampledRead(gl, defaultSwizzleSource, lastSample, 'w', kRounds,
controlDefaultSwizzle) ||
!RunMsaaSampledRead(gl, swizzledSource, 0, 'w', kRounds, controlSampleZero) ||
!RunMsaaSampledRead(gl, swizzledSource, lastSample, 'x', kRounds, controlRedComponent)) {
inconclusive = "one of the sampling programs could not be built or drawn";
break;
}
// All three controls have to be right for the subject to mean anything. A driver
// that simply cannot render R32F, or cannot fetch multisample texels, would fail the
// subject too - and calling that "the alpha swizzle is corrupted" would be a claim
// the probe has no evidence for.
const Bool controlsClean = EveryRoundIsExpected(controlDefaultSwizzle) &&
EveryRoundIsExpected(controlSampleZero) &&
EveryRoundIsExpected(controlRedComponent);
if (!controlsClean) {
inconclusive = "a control read came back wrong too, so the subject says nothing "
"about the alpha swizzle in particular";
break;
}
corrupted = !EveryRoundIsExpected(subject);
MGLOG_I("[driver-bug] R32F multisample swizzle probe: %d samples; subject(swizzle_a=RED, "
"sample=%d, .w) %s; controls default-swizzle [%s] sample-0 [%s] .x [%s]; expected "
"%g everywhere%s",
static_cast<int>(samples), static_cast<int>(lastSample),
DescribeRounds(subject).c_str(), DescribeRounds(controlDefaultSwizzle).c_str(),
DescribeRounds(controlSampleZero).c_str(), DescribeRounds(controlRedComponent).c_str(),
static_cast<double>(kMsaaExpected),
corrupted ? " - THE SWIZZLED ALPHA READ IS CORRUPTED" : "");
} while (false);
if (inconclusive != nullptr) {
MGLOG_I("[driver-bug] R32F multisample swizzle probe reached no verdict (%s)", inconclusive);
}
if (vertexArray != 0) gl.glDeleteVertexArrays(1, &vertexArray);
if (swizzledSource != 0) gl.glDeleteTextures(1, &swizzledSource);
if (defaultSwizzleSource != 0) gl.glDeleteTextures(1, &defaultSwizzleSource);
Restore(gl, saved);
return corrupted;
}
Bool R32FMultisampleSwizzleCorrupted(const GLESFunctionsTable& gl) {
static const Bool corrupted = ProbeR32FMultisampleSwizzleCorruption(gl);
return corrupted;
}
ImageLocationBudgetMeasurement ProbeImageLocationPerNameBudget(const GLESFunctionsTable& gl) {
ImageLocationBudgetMeasurement measurement;
if (!HasImageProgramEntryPoints(gl)) return measurement;
Drain(gl);
GLint maxImageUnits = 0;
GLint maxVertexImages = 0;
GLint maxFragmentImages = 0;
GLint maxGeometryImages = 0;
gl.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
gl.glGetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &maxVertexImages);
gl.glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages);
// ES 3.2 only; an older context raises GL_INVALID_ENUM and has no geometry stage to
// build the shape out of anyway.
gl.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImages);
const Bool limitsAccepted = gl.glGetError() == GL_NO_ERROR;
Drain(gl);
if (!limitsAccepted || maxImageUnits < 1 || maxGeometryImages < 1) {
MGLOG_I("[driver-bug] image-location-per-name probe reached no verdict (this context "
"has no geometry-stage image uniforms to build the shape out of)");
return measurement;
}
// One more than the smallest of the three stages' budgets. Below that, a driver with
// per-name accounting has no reason to refuse anything; above it, the CONTROL stops
// linking too and the pair stops being a comparison.
const Int perStage = static_cast<Int>(maxGeometryImages) + 1;
if (perStage > maxVertexImages || perStage > maxFragmentImages) {
MGLOG_I("[driver-bug] image-location-per-name probe reached no verdict (the geometry "
"stage's own image budget, %d, is not smaller than the vertex/fragment budgets "
"%d/%d, so no shape exceeds one stage without exceeding them all)",
static_cast<int>(maxGeometryImages), static_cast<int>(maxVertexImages),
static_cast<int>(maxFragmentImages));
return measurement;
}
measurement.perStageImageUniforms = perStage;
measurement.subjectDistinctNames = perStage * 3;
measurement.controlDistinctNames = perStage;
// Same stage count, same per-stage image count, same bindings, same stores. The names
// are the only difference between these two programs.
ProgramBuild subject =
BuildThreeStageImageProgram(gl, perStage, static_cast<Int>(maxImageUnits), "mg_probeVsImage",
"mg_probeGsImage", "mg_probeFsImage");
ProgramBuild control =
BuildThreeStageImageProgram(gl, perStage, static_cast<Int>(maxImageUnits),
"mg_probeSharedImage", "mg_probeSharedImage",
"mg_probeSharedImage");
if (subject.compiled && control.compiled) {
measurement.driverMessage = FirstLine(subject.infoLog);
// Both failing is an honest refusal of a shape that is simply too big; both linking
// is a driver that does not have this bug.
measurement.detected = !subject.linked && control.linked;
MGLOG_I("[driver-bug] image-location-per-name probe: %d image uniform(s) per stage over "
"%d image unit(s); %d distinct names %s, %d shared names %s%s%s",
static_cast<int>(perStage), static_cast<int>(maxImageUnits),
static_cast<int>(measurement.subjectDistinctNames),
subject.linked ? "link" : "FAIL", static_cast<int>(measurement.controlDistinctNames),
control.linked ? "link" : "FAIL",
measurement.driverMessage.empty() ? "" : "; driver says: ",
measurement.driverMessage.c_str());
} else {
MGLOG_I("[driver-bug] image-location-per-name probe reached no verdict (a probe stage "
"did not compile)");
}
if (subject.program != 0) gl.glDeleteProgram(subject.program);
if (control.program != 0) gl.glDeleteProgram(control.program);
Drain(gl);
return measurement;
}
const ImageLocationBudgetMeasurement& ImageLocationPerNameBudget(const GLESFunctionsTable& gl) {
static const ImageLocationBudgetMeasurement measurement = ProbeImageLocationPerNameBudget(gl);
return measurement;
}
Bool ProbeCrossStageImageQualifierMergeDropsWrites(const GLESFunctionsTable& gl) {
if (!HasImageDrawEntryPoints(gl)) return false;
Drain(gl);
GLint maxImageUnits = 0;
GLint maxVertexImages = 0;
GLint maxFragmentImages = 0;
gl.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
gl.glGetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &maxVertexImages);
gl.glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages);
const Bool limitsAccepted = gl.glGetError() == GL_NO_ERROR;
Drain(gl);
if (!limitsAccepted || maxVertexImages < 1 || maxFragmentImages < 1 ||
maxImageUnits <= static_cast<GLint>(kProbeImageUnit)) {
return false;
}
SavedState saved;
Save(gl, saved);
Drain(gl);
Bool dropped = false;
const char* inconclusive = nullptr;
GLuint vertexArray = 0;
ImageDrawTargets targets;
Int subjectFailed = -1;
Int controlFailed = -1;
do {
gl.glGenVertexArrays(1, &vertexArray);
gl.glBindVertexArray(vertexArray);
PrepareForProbeDraw(gl);
targets = MakeImageDrawTargets(gl, kQualifierMergeSize);
if (!targets.valid) {
inconclusive = "the probe's own rgba32f image and RGBA8 target would not come up";
break;
}
subjectFailed = RunQualifierMergeCase(gl, targets, "mg_probeImage", "mg_probeImage");
// The control names the two halves the way MobileGL's image-uniform repair does.
controlFailed = RunQualifierMergeCase(gl, targets, "mg_imageWo_mg_probeImage",
"mg_imageRo_mg_probeImage");
if (subjectFailed < 0 || controlFailed < 0) {
inconclusive = "one of the two programs could not be built or drawn";
break;
}
if (controlFailed > 0) {
inconclusive = "the RENAMED control lost the store too, so this driver does not "
"perform vertex-stage image writes at all - a different and much "
"larger claim than a same-name merge";
break;
}
dropped = subjectFailed > 0;
MGLOG_I("[driver-bug] cross-stage image qualifier merge probe: same-name pair lost the "
"store for %d/%d texels, renamed control lost %d%s",
static_cast<int>(subjectFailed),
static_cast<int>(kQualifierMergeSize) * static_cast<int>(kQualifierMergeSize),
static_cast<int>(controlFailed),
dropped ? " - THE MERGED WRITE IS DISCARDED" : "");
} while (false);
if (inconclusive != nullptr) {
MGLOG_I("[driver-bug] cross-stage image qualifier merge probe reached no verdict (%s)",
inconclusive);
}
ReleaseImageDrawTargets(gl, targets);
if (vertexArray != 0) gl.glDeleteVertexArrays(1, &vertexArray);
Restore(gl, saved);
return dropped;
}
Bool CrossStageImageQualifierMergeDropsWrites(const GLESFunctionsTable& gl) {
static const Bool dropped = ProbeCrossStageImageQualifierMergeDropsWrites(gl);
return dropped;
}
ImageCoherencyResidualMeasurement ProbeImageWriteReadCoherencyResidual(
const GLESFunctionsTable& gl) {
ImageCoherencyResidualMeasurement measurement;
if (!HasImageDrawEntryPoints(gl)) return measurement;
Drain(gl);
GLint maxImageUnits = 0;
GLint maxFragmentImages = 0;
gl.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
gl.glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages);
const Bool limitsAccepted = gl.glGetError() == GL_NO_ERROR;
Drain(gl);
// The subject declares BOTH halves of the split pair in the fragment stage.
if (!limitsAccepted || maxFragmentImages < 2 ||
maxImageUnits <= static_cast<GLint>(kProbeImageUnit)) {
return measurement;
}
SavedState saved;
Save(gl, saved);
Drain(gl);
const char* inconclusive = nullptr;
GLuint vertexArray = 0;
ImageDrawTargets targets;
Int subjectFailed = -1;
Int emittedFailed = -1;
Int controlFailed = -1;
do {
gl.glGenVertexArrays(1, &vertexArray);
gl.glBindVertexArray(vertexArray);
PrepareForProbeDraw(gl);
targets = MakeImageDrawTargets(gl, kCoherencySize);
if (!targets.valid) {
inconclusive = "the probe's own rgba32f image and RGBA8 target would not come up";
break;
}
subjectFailed = RunCoherencyShape(gl, targets, kStrongestCoherencyQualifiers,
kStrongestCoherencyBarriers);
emittedFailed =
RunCoherencyShape(gl, targets, kEmittedCoherencyQualifiers, kEmittedCoherencyBarriers);
controlFailed = RunCoherencyControl(gl, targets);
if (subjectFailed < 0 || emittedFailed < 0 || controlFailed < 0) {
inconclusive = "one of the three programs could not be built or drawn";
break;
}
if (controlFailed > 0) {
inconclusive = "the glFinish-separated control was dirty too, so this driver does "
"not make image writes visible across draws either - a different "
"and much larger claim than an in-invocation ordering residual";
break;
}
measurement.totalTexels = static_cast<Int>(kCoherencySize) * static_cast<Int>(kCoherencySize);
measurement.mismatchedTexels = subjectFailed;
measurement.emittedShapeMismatchedTexels = emittedFailed;
// Only the STRONGEST shape decides. A driver that gets it right there but not in the
// shape MobileGL emits has a fixable defect, not an unfixable one, and this section
// is not where that belongs.
measurement.detected = subjectFailed > 0;
MGLOG_I("[driver-bug] image write-read coherency probe: the strongest in-shader shape "
"(coherent volatile + image and global barriers) missed its own store for "
"%d/%d texels; the shape MobileGL emits (coherent + memoryBarrierImage) missed "
"%d; the glFinish-separated two-draw control missed %d%s",
static_cast<int>(subjectFailed), static_cast<int>(measurement.totalTexels),
static_cast<int>(emittedFailed), static_cast<int>(controlFailed),
measurement.detected ? " - THE IN-INVOCATION ORDERING IS NOT HONOURED" : "");
} while (false);
if (inconclusive != nullptr) {
MGLOG_I("[driver-bug] image write-read coherency probe reached no verdict (%s)",
inconclusive);
}
ReleaseImageDrawTargets(gl, targets);
if (vertexArray != 0) gl.glDeleteVertexArrays(1, &vertexArray);
Restore(gl, saved);
return measurement;
}
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
const GLESFunctionsTable& gl) {
static const ImageCoherencyResidualMeasurement measurement =
ProbeImageWriteReadCoherencyResidual(gl);
return measurement;
}
namespace {
// ===================== LAYERED BLIT DESTINATION =====================
constexpr const char* kLayeredBlitProbeName = "layered blit destination";
// Four texels wide: a 1x1 blit is a shape drivers special-case, and a rectangle keeps
// the probe on the ordinary path. Two layers is all the question needs.
constexpr GLsizei kLayeredBlitSize = 4;
constexpr GLsizei kLayeredBlitLayers = 2;
// One RGBA8 2D array whose every layer is filled with a distinguishable byte.
GLuint MakeLayeredBlitTexture(const GLESFunctionsTable& gl, GLubyte layer0, GLubyte layer1) {
GLuint texture = 0;
gl.glGenTextures(1, &texture);
if (texture == 0) return 0;
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
gl.glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kLayeredBlitSize, kLayeredBlitSize,
kLayeredBlitLayers);
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
const GLubyte fills[kLayeredBlitLayers] = {layer0, layer1};
for (GLint layer = 0; layer < kLayeredBlitLayers; ++layer) {
GLubyte texels[kLayeredBlitSize * kLayeredBlitSize * 4];
for (SizeT i = 0; i < sizeof(texels); i += 4) {
texels[i + 0] = fills[layer];
texels[i + 1] = fills[layer];
texels[i + 2] = fills[layer];
texels[i + 3] = 255;
}
gl.glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kLayeredBlitSize, kLayeredBlitSize, 1,
GL_RGBA, GL_UNSIGNED_BYTE, texels);
}
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
return texture;
}
// A framebuffer naming exactly one layer of one array texture.
GLuint MakeLayeredBlitFramebuffer(const GLESFunctionsTable& gl, GLuint texture, GLint layer) {
GLuint framebuffer = 0;
gl.glGenFramebuffers(1, &framebuffer);
if (framebuffer == 0) return 0;
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl.glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, layer);
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
gl.glDeleteFramebuffers(1, &framebuffer);
return 0;
}
return framebuffer;
}
// The red byte of texel (0, 0) of one layer, read through a framebuffer that names it.
// 256 is "could not read", which no fill value can be.
Int ReadLayeredBlitTexel(const GLESFunctionsTable& gl, GLuint texture, GLint layer) {
const GLuint framebuffer = MakeLayeredBlitFramebuffer(gl, texture, layer);
if (framebuffer == 0) return 256;
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
gl.glReadBuffer(GL_COLOR_ATTACHMENT0);
gl.glPixelStorei(GL_PACK_ALIGNMENT, 1);
GLubyte pixel[4] = {0, 0, 0, 0};
gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
gl.glDeleteFramebuffers(1, &framebuffer);
Drain(gl);
return static_cast<Int>(pixel[0]);
}
// Blits source layer 1 onto `destinationLayer` of a freshly filled destination and
// reports which layer actually received it, or -1 when the blit could not be issued.
Int LayeredBlitLandsOnLayer(const GLESFunctionsTable& gl, GLint destinationLayer, GLubyte magic) {
const GLuint source = MakeLayeredBlitTexture(gl, 0x11, magic);
const GLuint destination = MakeLayeredBlitTexture(gl, 0x33, 0x44);
const GLuint sourceFramebuffer = MakeLayeredBlitFramebuffer(gl, source, 1);
const GLuint destinationFramebuffer = MakeLayeredBlitFramebuffer(gl, destination, destinationLayer);
Int landedOn = -1;
if (source != 0 && destination != 0 && sourceFramebuffer != 0 && destinationFramebuffer != 0) {
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
gl.glReadBuffer(GL_COLOR_ATTACHMENT0);
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFramebuffer);
Drain(gl);
gl.glBlitFramebuffer(0, 0, kLayeredBlitSize, kLayeredBlitSize, 0, 0, kLayeredBlitSize,
kLayeredBlitSize, GL_COLOR_BUFFER_BIT, GL_NEAREST);
if (gl.glGetError() == GL_NO_ERROR) {
landedOn = -2; // issued, but seen on no layer yet
for (GLint layer = 0; layer < kLayeredBlitLayers; ++layer) {
if (ReadLayeredBlitTexel(gl, destination, layer) == static_cast<Int>(magic)) {
landedOn = layer;
break;
}
}
}
}
if (sourceFramebuffer != 0) gl.glDeleteFramebuffers(1, &sourceFramebuffer);
if (destinationFramebuffer != 0) gl.glDeleteFramebuffers(1, &destinationFramebuffer);
if (source != 0) gl.glDeleteTextures(1, &source);
if (destination != 0) gl.glDeleteTextures(1, &destination);
Drain(gl);
return landedOn;
}
} // namespace
Bool ProbeBlitIgnoresDestinationArrayLayer(const GLESFunctionsTable& gl) {
if (!gl.glGenTextures || !gl.glBindTexture || !gl.glTexStorage3D || !gl.glTexSubImage3D ||
!gl.glTexParameteri || !gl.glDeleteTextures || !gl.glGenFramebuffers || !gl.glBindFramebuffer ||
!gl.glFramebufferTextureLayer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers ||
!gl.glBlitFramebuffer || !gl.glReadBuffer || !gl.glReadPixels || !gl.glPixelStorei || !gl.glGetError ||
!gl.glIsEnabled || !gl.glDisable) {
return false;
}
SavedState saved;
Save(gl, saved);
// A scissor left on by whoever ran before would clip the probe's own blit and make a
// working driver look broken.
gl.glDisable(GL_SCISSOR_TEST);
Drain(gl);
// THE CONTROL: the same blit onto destination layer 0, which is the case no
// implementation gets wrong. It also proves the SOURCE layer is honoured, since the
// magic byte it looks for only exists on source layer 1 - so a driver that cannot blit
// between array layers at all, or that has no working glFramebufferTextureLayer, fails
// here and reaches no verdict rather than being reported as having this bug.
const Int controlLanded = LayeredBlitLandsOnLayer(gl, 0, 0x5Au);
Bool detected = false;
if (controlLanded != 0) {
MGLOG_I("[driver-bug] %s probe reached no verdict (the destination-layer-0 control "
"landed on layer %d instead of 0)",
kLayeredBlitProbeName, controlLanded);
} else {
// THE SUBJECT: the identical blit asking for layer 1. Only the destination layer moved.
const Int subjectLanded = LayeredBlitLandsOnLayer(gl, 1, 0x5Au);
detected = subjectLanded == 0;
if (subjectLanded != 0 && subjectLanded != 1) {
MGLOG_I("[driver-bug] %s probe reached no verdict (the subject blit landed on "
"no layer at all: %d)",
kLayeredBlitProbeName, subjectLanded);
} else {
MGLOG_I("[driver-bug] %s probe: a blit asking for destination layer 1 landed on "
"layer %d%s",
kLayeredBlitProbeName, subjectLanded,
detected ? " - THE DESTINATION LAYER IS IGNORED" : "");
}
}
Restore(gl, saved);
return detected;
}
Bool BlitIgnoresDestinationArrayLayer(const GLESFunctionsTable& gl) {
// One driver per process, and the answer is structural rather than sampled.
static const Bool ignored = ProbeBlitIgnoresDestinationArrayLayer(gl);
return ignored;
}
namespace {
Optional<DriverBugFinding> ProbeLayeredBlitDestinationBug(const GLESFunctionsTable& gl) {
if (!BlitIgnoresDestinationArrayLayer(gl)) return std::nullopt;
return DriverBugFinding{
"glBlitFramebuffer ignores the destination array layer",
DriverBugVerdict::Fixed,
"a glBlitFramebuffer whose DRAW framebuffer attaches a non-zero array layer with "
"glFramebufferTextureLayer writes to layer 0 instead, and raises no error doing "
"it. Measured here on the colour aspect; the depth aspect behaves the same way on "
"the device this was characterised on. The layer is honoured everywhere else on "
"the same driver - the blit's own SOURCE layer is read correctly, which is this "
"probe's control - so neither layered attachments nor blitting is withdrawn. "
"MobileGL performs such a blit with glCopyImageSubData instead, which takes the "
"destination layer explicitly and honours it here, and applies that substitute to "
"the depth and stencil aspects as well; a blit that scales, flips, changes format, "
"resolves samples or is clipped by the scissor cannot be expressed as a copy and "
"is still handed to the driver"};
}
// ===================== EXPLICIT VERTEX INPUT LOCATION CEILING =====================
constexpr const char* kAttributeLocationProbeName = "explicit vertex input location";
// COMPILES ONE VERTEX STAGE and reports nothing else. A link would drag in every other
// reason a program can be refused (varying budgets, the fragment stage, the linker's own
// location rules), and the defect this measures is in the driver's ESSL COMPILER: it
// rejects the declaration itself, before any of that can matter.
Bool ExplicitVertexInputLocationCompiles(const GLESFunctionsTable& gl, Int location,
String* firstRejectionMessage) {
const String source = format("#version 320 es\n"
"layout(location = {}) in vec4 a_probe;\n"
"void main() {{ gl_Position = a_probe; }}\n",
location);
Drain(gl);
const GLuint shader = gl.glCreateShader(GL_VERTEX_SHADER);
if (shader == 0) return false;
const char* text = source.c_str();
gl.glShaderSource(shader, 1, &text, nullptr);
gl.glCompileShader(shader);
GLint compiled = GL_FALSE;
gl.glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE && firstRejectionMessage != nullptr && firstRejectionMessage->empty()) {
char log[256] = {0};
gl.glGetShaderInfoLog(shader, static_cast<GLsizei>(sizeof(log) - 1), nullptr, log);
// One line: the driver's own wording is the report's whole evidential value, and
// the rest of the log is the same sentence repeated per declaration.
String message = log;
if (const SizeT newline = message.find('\n'); newline != String::npos) {
message.resize(newline);
}
while (!message.empty() && (message.back() == ' ' || message.back() == '\r')) message.pop_back();
*firstRejectionMessage = Move(message);
}
gl.glDeleteShader(shader);
Drain(gl);
return compiled != GL_FALSE;
}
// THE SECOND CONTROL, and the one that decides whether the cap is about the LAYOUT
// QUALIFIER or about the attribute itself. The same input, declared with no qualifier at
// all and placed by glBindAttribLocation instead. If this links and glGetAttribLocation
// answers with the location asked for, the driver can address that attribute perfectly
// well and only the qualifier path is capped - which is what makes clamping the
// advertised count the right response rather than a shrug. If it fails too, the driver
// genuinely has fewer attributes than it advertises; the clamp is still correct, but the
// report must not claim the attribute is reachable another way.
Bool BindAttribLocationReaches(const GLESFunctionsTable& gl, Int location) {
if (!gl.glCreateProgram || !gl.glAttachShader || !gl.glBindAttribLocation || !gl.glLinkProgram ||
!gl.glGetProgramiv || !gl.glGetAttribLocation || !gl.glDeleteProgram) {
return false;
}
constexpr const char* kVertexSource = "#version 320 es\n"
"in vec4 a_probe;\n"
"void main() { gl_Position = a_probe; }\n";
constexpr const char* kFragmentSource = "#version 320 es\n"
"precision highp float;\n"
"out vec4 o_color;\n"
"void main() { o_color = vec4(1.0); }\n";
Drain(gl);
const GLuint vertexShader =
CompileStage(gl, GL_VERTEX_SHADER, kVertexSource, "vertex", kAttributeLocationProbeName);
if (vertexShader == 0) return false;
const GLuint fragmentShader =
CompileStage(gl, GL_FRAGMENT_SHADER, kFragmentSource, "fragment", kAttributeLocationProbeName);
if (fragmentShader == 0) {
gl.glDeleteShader(vertexShader);
return false;
}
const GLuint program = gl.glCreateProgram();
gl.glAttachShader(program, vertexShader);
gl.glAttachShader(program, fragmentShader);
gl.glBindAttribLocation(program, static_cast<GLuint>(location), "a_probe");
gl.glLinkProgram(program);
GLint linked = GL_FALSE;
gl.glGetProgramiv(program, GL_LINK_STATUS, &linked);
const Bool reached = linked != GL_FALSE && gl.glGetAttribLocation(program, "a_probe") == location;
gl.glDeleteShader(vertexShader);
gl.glDeleteShader(fragmentShader);
gl.glDeleteProgram(program);
Drain(gl);
return reached;
}
} // namespace
VertexInputLocationCeilingMeasurement ProbeExplicitVertexInputLocationCeiling(const GLESFunctionsTable& gl) {
VertexInputLocationCeilingMeasurement measurement;
// `usableLocations` is the number a caller clamps to, so it carries the driver's own
// answer from the first line onward and every early return below leaves it there. A
// probe that cannot run has to withdraw nothing at all, and a zero here would withdraw
// every attribute the device has.
if (gl.glGetIntegerv != nullptr) {
GLint advertisedEarly = 0;
gl.glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &advertisedEarly);
if (gl.glGetError != nullptr) Drain(gl);
measurement.advertisedMaxVertexAttribs = advertisedEarly;
measurement.usableLocations = advertisedEarly;
}
if (!gl.glCreateShader || !gl.glShaderSource || !gl.glCompileShader || !gl.glGetShaderiv ||
!gl.glGetShaderInfoLog || !gl.glDeleteShader || !gl.glGetIntegerv || !gl.glGetError) {
return measurement;
}
const GLint advertised = measurement.advertisedMaxVertexAttribs;
// Nothing to bisect, and nothing a clamp could usefully say.
if (advertised < 2) return measurement;
// THE CONTROL, and the reason a compiler that is simply unavailable cannot be reported as
// this bug: location 0 is the one every ES driver in existence accepts, so a probe that
// cannot compile even that has measured its own failure, not the driver's.
if (!ExplicitVertexInputLocationCompiles(gl, 0, nullptr)) {
MGLOG_I("[driver-bug] %s probe reached no verdict (the location-0 control did not "
"compile, so nothing higher says anything)",
kAttributeLocationProbeName);
return measurement;
}
// The common case is one compile: a conforming driver takes the highest location it
// advertises and the probe stops there.
if (ExplicitVertexInputLocationCompiles(gl, advertised - 1, nullptr)) return measurement;
// Bisect for the highest location that still compiles. `low` always compiles (the control
// proved location 0 does) and `high` never does, so the loop closes on the boundary in
// ceil(log2(advertised)) compiles - five for the 32 attributes Adreno advertises.
String rejectionMessage;
ExplicitVertexInputLocationCompiles(gl, advertised - 1, &rejectionMessage);
Int low = 0;
Int high = advertised - 1;
while (high - low > 1) {
const Int middle = low + (high - low) / 2;
if (ExplicitVertexInputLocationCompiles(gl, middle, &rejectionMessage)) {
low = middle;
} else {
high = middle;
}
}
measurement.detected = true;
measurement.usableLocations = low + 1;
measurement.driverMessage = Move(rejectionMessage);
measurement.bindAttribLocationReachesAdvertisedMax = BindAttribLocationReaches(gl, advertised - 1);
MGLOG_I("[driver-bug] %s probe: GL_MAX_VERTEX_ATTRIBS is %d but layout(location = N) on a "
"vertex input is refused from N = %d upward - only %d location(s) are usable; "
"glBindAttribLocation(%d) %s%s%s",
kAttributeLocationProbeName, advertised, measurement.usableLocations,
measurement.usableLocations, advertised - 1,
measurement.bindAttribLocationReachesAdvertisedMax ? "still resolves correctly"
: "does not resolve either",
measurement.driverMessage.empty() ? "" : "; the driver says: ",
measurement.driverMessage.c_str());
return measurement;
}
const VertexInputLocationCeilingMeasurement& ExplicitVertexInputLocationCeiling(const GLESFunctionsTable& gl) {
static const VertexInputLocationCeilingMeasurement measurement =
ProbeExplicitVertexInputLocationCeiling(gl);
return measurement;
}
namespace {
// ===================== LOCATED INTER-STAGE INTERFACE BLOCKS =====================
constexpr const char* kIoBlockProbeName = "located interface block";
// This probe's OWN requirements, not HasEveryEntryPoint's. That one is the geometry
// storage probe's list and asks for storage buffers and buffer mapping, which nothing
// here touches - borrowing it would let one unresolved SSBO pointer leave a driver that
// HAS this defect unrepaired, which is the opposite of what a gate is for. Covers what
// BuildProgram, Save/Restore, PrepareForProbeDraw and the draw below actually call.
Bool HasIoBlockProbeEntryPoints(const GLESFunctionsTable& gl) {
return gl.glCreateShader && gl.glShaderSource && gl.glCompileShader && gl.glGetShaderiv &&
gl.glGetShaderInfoLog && gl.glCreateProgram && gl.glAttachShader &&
gl.glLinkProgram && gl.glGetProgramiv && gl.glGetProgramInfoLog &&
gl.glDeleteShader && gl.glDeleteProgram && gl.glUseProgram && gl.glGenVertexArrays &&
gl.glBindVertexArray && gl.glDeleteVertexArrays && gl.glGenRenderbuffers &&
gl.glBindRenderbuffer && gl.glRenderbufferStorage && gl.glDeleteRenderbuffers &&
gl.glGenFramebuffers && gl.glBindFramebuffer && gl.glFramebufferRenderbuffer &&
gl.glCheckFramebufferStatus && gl.glDeleteFramebuffers && gl.glViewport &&
gl.glClearColor && gl.glClear && gl.glDrawArrays && gl.glReadPixels &&
gl.glPixelStorei && gl.glGetIntegerv && gl.glGetIntegeri_v && gl.glGetError &&
gl.glEnable && gl.glDisable && gl.glIsEnabled;
}
// 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 (!HasIoBlockProbeEntryPoints(gl)) return measurement;
SavedState saved;
Save(gl, saved);
// The colour mask is not in SavedState - no other probe touches it - so this one saves
// and puts back its own. It has to be forced open: a masked channel would read back as
// zero and turn a healthy driver into a "payload lost" verdict.
GLboolean savedColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
const Bool canMaskColor = gl.glColorMask != nullptr && gl.glGetBooleanv != nullptr;
if (canMaskColor) {
gl.glGetBooleanv(GL_COLOR_WRITEMASK, savedColorMask);
gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
}
GLuint vao = 0;
gl.glGenVertexArrays(1, &vao);
gl.glBindVertexArray(vao);
PrepareForProbeDraw(gl);
// 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);
}
if (canMaskColor) {
gl.glColorMask(savedColorMask[0], savedColorMask[1], savedColorMask[2], savedColorMask[3]);
}
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 {
// ===================== PACKED16 COPY-IMAGE FIELD ORDER =====================
constexpr const char* kPacked16CopyProbeName = "packed16 copy-image field order";
// The shape the KHR-GL4x.copy_image failures pin: a 30x30x12 GL_RGB5_A1 2D array with
// the CTS's three-level chain (FUNCTIONAL_TEST_N_LEVELS = 3, makeTextureComplete(0, 2):
// 30/15/7 x12; the plain endpoints are 7/3/1), against plain-2D endpoints.
//
// WHAT THE DEVICE MEASUREMENTS ACTUALLY SHOWED (round 2): the mirrored field order is
// a property of the WHOLE ALLOCATION, not of a mip level - a 30x30x12 packed16 array
// is born in the mirrored layout at every level, while the small arrays the CTS's
// passing iterations used (7- and 15-texel bases; its src/dst dim loop is {7, 15}, so
// a base-30 array only ever appears at level 1) are born plain, which is why the
// failures looked per-mip-level from the QPA alone. AND the layout is not fixed for
// the allocation's lifetime: FBO-ATTACHING the array transitions it to the plain
// (renderable) layout, content preserved. That transition is what produced every
// seemingly contradictory measurement of this campaign - a probe that direct-read its
// array before copying relayouted its own subject and reported the device clean in
// the very process whose CTS copies kept mirroring, and the raw matrix's one
// "clean" 30x30x12 array was exactly the one that had been direct-read first. It is
// also why the CTS's own "source image was not modified" checks always passed: they
// read through an FBO attach, after the copy already went wrong. So: subject copies
// FIRST, every control that attaches the array AFTER, and because the driver's
// allocation heuristic beyond the size threshold is not fully mapped, the probe tries
// several allocation recipes of the same client-visible texture and a mirror from ANY
// level of ANY recipe is the finding.
constexpr GLsizei kPacked16BaseSize = 30;
constexpr GLsizei kPacked16Layers = 12;
constexpr GLsizei kPacked16DstSize = 7;
constexpr GLint kPacked16Levels = 3;
// One GL_RGB5_A1 texel, as the client word the probe uploads everywhere:
// (R, G, B, A) = (0, 1, 3, 1) under GL_UNSIGNED_SHORT_5_5_5_1. Chosen because 5551 is
// the one 16-bit packed layout whose field widths are not a palindrome - its mirror
// fixes the DIRECTION of the swap - and because this word's mirror differs in every
// channel including alpha, so no expansion rounding can confuse the two predictions.
constexpr Uint16 kPacked16Word = 0x0047;
// What an FBO readback answers for the word, as UNorm8: (0, 1, 3) / 31 and alpha 1.
constexpr GLubyte kPacked16Expected[4] = {0, 8, 25, 255};
// The same readback when the stored bits are the mirrored re-encoding: 0x0047 decoded
// as 5_5_5_1 and re-encoded as 1_5_5_5_REV is 0x8C20, which the destination's non-REV
// layout then decodes as (17, 16, 16) / 31 with alpha 0. This is byte-for-byte the
// arithmetic behind every failing CTS body (src 0x0047 -> got 0x8C20).
constexpr GLubyte kPacked16Mirrored[4] = {140, 132, 132, 0};
// A 5-bit step is 255/31 ~ 8.2 UNorm8 codes; half a step accepts every 5-bit-to-8-bit
// expansion a driver uses (floor, round, bit replication) while still telling two
// adjacent 5-bit values apart.
constexpr Int kPacked16Tolerance = 4;
// The allocation recipes the probe tries. Same client-visible texture, same data -
// only the order and the filter state during the uploads move, because those are the
// knobs the driver's layout heuristic was measured keying on (differently in
// different contexts).
enum class Packed16Recipe : Uint8 {
// glTexImage3D per level on a fresh texture at driver defaults, parameters after:
// the order a freshly minted MobileGL backend texture performs (the storage sync
// runs before the parameter re-push, see SyncTextureObjectToBackend).
UploadsFirst,
// NEAREST and MAX_LEVEL set before the uploads: the shape an application that
// configures its sampler state ahead of its data gets.
ParamsFirst,
// MAX_LEVEL bounded but MIN_FILTER left at its mipmapped default: the CTS
// copy-test texture verbatim - copy tests never touch filters, and the chain is
// complete because all three levels exist under MAX_LEVEL = 2.
CtsShape,
};
constexpr Packed16Recipe kPacked16Recipes[] = {Packed16Recipe::UploadsFirst,
Packed16Recipe::ParamsFirst,
Packed16Recipe::CtsShape};
const char* Packed16RecipeName(Packed16Recipe recipe) {
switch (recipe) {
case Packed16Recipe::UploadsFirst: return "uploads-first";
case Packed16Recipe::ParamsFirst: return "params-first";
case Packed16Recipe::CtsShape: return "cts-shape";
}
return "?";
}
// A three-level GL_RGB5_A1 2D array (30/15/7, twelve layers each, every texel holding
// kPacked16Word) allocated per `recipe`. Every recipe ends mipmap-complete - some
// drivers refuse glCopyImageSubData on an incomplete texture.
GLuint MakePacked16ArrayTexture(const GLESFunctionsTable& gl, Packed16Recipe recipe) {
GLuint texture = 0;
gl.glGenTextures(1, &texture);
if (texture == 0) return 0;
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
if (recipe == Packed16Recipe::ParamsFirst) {
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
if (recipe != Packed16Recipe::UploadsFirst) {
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, kPacked16Levels - 1);
}
for (GLint level = 0; level < kPacked16Levels; ++level) {
const GLsizei size = kPacked16BaseSize >> level;
const Vector<Uint16> words(
static_cast<SizeT>(size) * static_cast<SizeT>(size) * kPacked16Layers, kPacked16Word);
gl.glTexImage3D(GL_TEXTURE_2D_ARRAY, level, GL_RGB5_A1, size, size, kPacked16Layers, 0,
GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, words.data());
}
if (recipe == Packed16Recipe::UploadsFirst) {
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, kPacked16Levels - 1);
}
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
return texture;
}
// The plain-2D destination, three levels (7/3/1) like the CTS's, every level filled
// with 0xFFFF - the CTS's own (1,1,1,1) destination fill - so a copy that silently
// did nothing reads as "no verdict" rather than as either prediction.
// A plain-2D endpoint with the CTS's three-level 7/3/1 chain, every texel of every
// level holding `fill`: 0xFFFF (the CTS's own (1,1,1,1) destination fill, so a copy
// that silently did nothing reads as "no verdict" rather than as either prediction),
// or kPacked16Word for the machinery control's source. Uploads first, parameters
// after, for the same in-situ fidelity as the array above - this is the allocation
// discipline every MobileGL-minted texture gets, and the shape the failing bodies'
// clean plain endpoints had.
GLuint MakePacked16FlatTexture(const GLESFunctionsTable& gl, Uint16 fill) {
GLuint texture = 0;
gl.glGenTextures(1, &texture);
if (texture == 0) return 0;
gl.glBindTexture(GL_TEXTURE_2D, texture);
for (GLint level = 0; level < kPacked16Levels; ++level) {
const GLsizei size = std::max<GLsizei>(kPacked16DstSize >> level, 1);
const Vector<Uint16> texels(static_cast<SizeT>(size) * size, fill);
gl.glTexImage2D(GL_TEXTURE_2D, level, GL_RGB5_A1, size, size, 0, GL_RGBA,
GL_UNSIGNED_SHORT_5_5_5_1, texels.data());
}
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, kPacked16Levels - 1);
gl.glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
// Texel (0, 0) of a 2D level 0, or of layer 0 of an array's `level`, through a
// framebuffer of its own. False when the attachment is incomplete or the read errors -
// both are declines, not verdicts.
Bool ReadPacked16Texel(const GLESFunctionsTable& gl, GLuint texture, Bool isArray, GLint level,
GLubyte out[4]) {
GLuint framebuffer = 0;
gl.glGenFramebuffers(1, &framebuffer);
if (framebuffer == 0) return false;
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
if (isArray) {
gl.glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, 0);
} else {
gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level);
}
Bool read = false;
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
gl.glReadBuffer(GL_COLOR_ATTACHMENT0);
Drain(gl);
gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
read = gl.glGetError() == GL_NO_ERROR;
}
gl.glBindFramebuffer(GL_FRAMEBUFFER, 0);
gl.glDeleteFramebuffers(1, &framebuffer);
Drain(gl);
return read;
}
// Copies a kPacked16DstSize-square region out of (source, sourceTarget, sourceLevel)
// layer 0 onto a freshly 0xFFFF-filled 2D destination and hands back the destination's
// texel (0, 0). False when the copy raised an error or the readback could not run.
Bool Packed16CopyLandsTexel(const GLESFunctionsTable& gl, GLuint source, GLenum sourceTarget,
GLint sourceLevel, GLubyte out[4]) {
const GLuint destination = MakePacked16FlatTexture(gl, Uint16{0xFFFF});
if (destination == 0) return false;
Drain(gl);
gl.glCopyImageSubData(source, sourceTarget, sourceLevel, 0, 0, 0, destination,
GL_TEXTURE_2D, 0, 0, 0, 0, kPacked16DstSize, kPacked16DstSize, 1);
const Bool copied = gl.glGetError() == GL_NO_ERROR;
const Bool read = copied && ReadPacked16Texel(gl, destination, false, 0, out);
gl.glDeleteTextures(1, &destination);
Drain(gl);
return read;
}
Bool Packed16TexelNear(const GLubyte got[4], const GLubyte want[4]) {
for (Int i = 0; i < 4; ++i) {
const Int delta = static_cast<Int>(got[i]) - static_cast<Int>(want[i]);
if (delta > kPacked16Tolerance || delta < -kPacked16Tolerance) return false;
}
return true;
}
// One recipe's whole measurement: allocate, both subject copies, THEN the round-trip
// control. The order is load-bearing: FBO-ATTACHING THE ARRAY TRANSITIONS IT to the
// plain (renderable) layout on the affected driver, so a round-trip read taken before
// the copies RELAYOUTS the subject and measures a texture the application's copy
// never sees - round two's first deployment did exactly that and reported the device
// clean while the CTS bodies kept failing in the same process. Copies first, the
// control after: the attach-driven transition preserves content, so the read still
// answers "the upload was intact" without disturbing what the copies measured. Only a
// mirror that matches the PREDICTION while that control holds counts; everything else
// is that recipe's no-verdict (logged as such).
Bool RunPacked16Recipe(const GLESFunctionsTable& gl, Packed16Recipe recipe) {
Bool mirrored = false;
const GLuint array = MakePacked16ArrayTexture(gl, recipe);
GLubyte direct[4] = {0, 0, 0, 0};
GLubyte level0[4] = {0, 0, 0, 0};
GLubyte level1[4] = {0, 0, 0, 0};
if (array == 0 || !Packed16CopyLandsTexel(gl, array, GL_TEXTURE_2D_ARRAY, 0, level0) ||
!Packed16CopyLandsTexel(gl, array, GL_TEXTURE_2D_ARRAY, 1, level1)) {
MGLOG_I("[driver-bug] %s probe [%s]: no verdict (a subject copy could not run)",
kPacked16CopyProbeName, Packed16RecipeName(recipe));
} else if (!ReadPacked16Texel(gl, array, true, 1, direct) ||
!Packed16TexelNear(direct, kPacked16Expected)) {
// The recipe's own round trip: reading the level directly decodes the driver's
// own storage and must deliver the word whatever layout it picked. A wrong
// answer means the UPLOAD is what corrupts - a different defect, and one the
// widening's raw-copy reasoning says nothing about.
MGLOG_I("[driver-bug] %s probe [%s]: no verdict (the array's own level-1 readback "
"answered (%d, %d, %d, %d) instead of the word - the upload, not the "
"copy, is what diverges)",
kPacked16CopyProbeName, Packed16RecipeName(recipe), direct[0], direct[1],
direct[2], direct[3]);
} else if (Packed16TexelNear(level0, kPacked16Mirrored) ||
Packed16TexelNear(level1, kPacked16Mirrored)) {
mirrored = true;
MGLOG_I("[driver-bug] %s probe [%s]: copies delivered level 0 (%d, %d, %d, %d) / "
"level 1 (%d, %d, %d, %d) - the 1_5_5_5_REV re-encoding of the word - "
"THIS ALLOCATION'S FIELD ORDER IS MIRRORED",
kPacked16CopyProbeName, Packed16RecipeName(recipe), level0[0], level0[1],
level0[2], level0[3], level1[0], level1[1], level1[2], level1[3]);
} else if (Packed16TexelNear(level0, kPacked16Expected) &&
Packed16TexelNear(level1, kPacked16Expected)) {
MGLOG_I("[driver-bug] %s probe [%s]: both levels copied the word intact",
kPacked16CopyProbeName, Packed16RecipeName(recipe));
} else {
MGLOG_I("[driver-bug] %s probe [%s]: no verdict (copies read back level 0 "
"(%d, %d, %d, %d) / level 1 (%d, %d, %d, %d), neither the word nor its "
"mirror)",
kPacked16CopyProbeName, Packed16RecipeName(recipe), level0[0], level0[1],
level0[2], level0[3], level1[0], level1[1], level1[2], level1[3]);
}
if (array != 0) gl.glDeleteTextures(1, &array);
return mirrored;
}
} // namespace
Bool ProbeCopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) {
if (!gl.glGenTextures || !gl.glBindTexture || !gl.glTexParameteri || !gl.glTexImage2D ||
!gl.glTexImage3D || !gl.glDeleteTextures || !gl.glCopyImageSubData || !gl.glGenFramebuffers ||
!gl.glBindFramebuffer || !gl.glFramebufferTexture2D || !gl.glFramebufferTextureLayer ||
!gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glReadBuffer ||
!gl.glReadPixels || !gl.glPixelStorei || !gl.glGetError) {
return false;
}
SavedState saved;
Save(gl, saved);
// The uploads and readbacks below run under the probe's own tight pixel-transfer
// scope - a caller's skip/row-length/PBO would shear the probe's data into a false
// verdict either way. Restore puts the caller's scope back with the rest.
gl.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
gl.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
gl.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
gl.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
gl.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
gl.glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
gl.glPixelStorei(GL_PACK_ALIGNMENT, 1);
gl.glPixelStorei(GL_PACK_ROW_LENGTH, 0);
gl.glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
gl.glPixelStorei(GL_PACK_SKIP_ROWS, 0);
if (gl.glBindBuffer != nullptr) {
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
}
Drain(gl);
Bool detected = false;
const GLuint flatSource = MakePacked16FlatTexture(gl, kPacked16Word);
GLubyte machinery[4] = {0, 0, 0, 0};
// THE MACHINERY CONTROL: a copy between two 2D images of the same three-level shape
// and allocation discipline. Two identical allocations share the driver's layout
// whatever it is, so this must deliver the word on ANY driver that can run copy_image
// on these formats at all - a driver that cannot reaches no verdict instead of being
// reported as this.
if (flatSource == 0 || !Packed16CopyLandsTexel(gl, flatSource, GL_TEXTURE_2D, 0, machinery)) {
MGLOG_I("[driver-bug] %s probe reached no verdict (the 2D-to-2D machinery control "
"could not run)",
kPacked16CopyProbeName);
} else if (!Packed16TexelNear(machinery, kPacked16Expected)) {
MGLOG_I("[driver-bug] %s probe reached no verdict (the 2D-to-2D machinery control "
"read back (%d, %d, %d, %d) instead of the word's (%d, %d, %d, %d))",
kPacked16CopyProbeName, machinery[0], machinery[1], machinery[2], machinery[3],
kPacked16Expected[0], kPacked16Expected[1], kPacked16Expected[2], kPacked16Expected[3]);
} else {
// THE SUBJECTS: every allocation recipe of the same array, each with its own
// round-trip control; a mirror from any level of any recipe is the finding. Every
// recipe logs its own verdict either way, so a device run always shows whether
// this probe executed and what each allocation delivered - a silent clean path
// would be indistinguishable from a probe that never ran.
for (const Packed16Recipe recipe : kPacked16Recipes) {
detected = RunPacked16Recipe(gl, recipe) || detected;
}
}
if (flatSource != 0) gl.glDeleteTextures(1, &flatSource);
Restore(gl, saved);
return detected;
}
Bool CopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) {
// One driver per process, and the answer is structural (the driver's storage layout
// for a shape), not sampled.
static const Bool mirrored = ProbeCopyImageMirrorsPacked16FieldOrder(gl);
return mirrored;
}
namespace {
Optional<DriverBugFinding> ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) {
const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl);
if (!measurement.detected) return std::nullopt;
String detail =
format("GL_MAX_VERTEX_ATTRIBS is {} but the ESSL compiler refuses "
"layout(location = N) on a vertex input for every N at or above {} - so {} of "
"the {} attributes advertised cannot be declared at all",
measurement.advertisedMaxVertexAttribs, measurement.usableLocations,
measurement.advertisedMaxVertexAttribs - measurement.usableLocations,
measurement.advertisedMaxVertexAttribs);
if (!measurement.driverMessage.empty()) {
detail += format(" - the driver says \"{}\"", measurement.driverMessage);
}
detail += measurement.bindAttribLocationReachesAdvertisedMax
? format(". The same driver ACCEPTS glBindAttribLocation({}) on an unqualified "
"input and resolves it correctly, so the attributes are there and only "
"the layout qualifier is capped",
measurement.advertisedMaxVertexAttribs - 1)
: ". glBindAttribLocation does not reach those locations either, so the "
"attributes appear genuinely absent rather than merely unspellable";
detail += format(". MobileGL emits its vertex inputs as layout qualifiers, so it advertises the "
"{} locations it can actually deliver rather than the {} the driver claims. An "
"application asking for more used to be handed a count it could not build a "
"shader against, which failed at the stage compile with no way back",
measurement.usableLocations, measurement.advertisedMaxVertexAttribs);
return DriverBugFinding{"Vertex input layout(location) capped below GL_MAX_VERTEX_ATTRIBS",
DriverBugVerdict::Fixed, Move(detail)};
}
Optional<DriverBugFinding> ProbeGeometryWriteAfterEmitBug(const GLESFunctionsTable& gl) {
if (!GeometryStageSsboWriteAfterEmitDropped(gl)) return std::nullopt;
return DriverBugFinding{
"Geometry-stage storage writes after EmitVertex",
DriverBugVerdict::Unfixable,
"the driver silently discards shader storage buffer writes a geometry shader "
"issues after its last EmitVertex()/EndPrimitive(); the identical write issued "
"BEFORE the emit lands, for both point and triangle geometry shaders. "
"GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS is therefore NOT withdrawn - doing so "
"would break the shaders that write before emitting, which work correctly. "
"A shader that must write after emitting has no substitute on this driver"};
}
Optional<DriverBugFinding> ProbeR32FMultisampleSwizzleBug(const GLESFunctionsTable& gl) {
if (!R32FMultisampleSwizzleCorrupted(gl)) return std::nullopt;
return DriverBugFinding{
"R32F multisample fetch through a swizzled alpha",
DriverBugVerdict::Unfixable,
"texelFetch() on an R32F GL_TEXTURE_2D_MULTISAMPLE texture whose "
"GL_TEXTURE_SWIZZLE_A is not the default returns uninitialised memory - a "
"different value every run - for any sample index other than 0, from the SECOND "
"such program in the context onward. The identical fetch with the default alpha "
"swizzle, at sample index 0, or through .x instead of .w is correct, so neither "
"R32F multisample targets nor texture swizzles are withdrawn. No substitute was "
"adopted: widening every R32F multisample target to RG32F would sidestep it and "
"doubles multisample memory, which was declined. A shader that fetches a non-zero "
"sample through a swizzled alpha cannot be relied on here"};
}
Optional<DriverBugFinding> ProbeImageLocationPerNameBug(const GLESFunctionsTable& gl) {
const ImageLocationBudgetMeasurement& measurement = ImageLocationPerNameBudget(gl);
if (!measurement.detected) return std::nullopt;
String detail = format(
"this driver's image-location budget is charged per distinct uniform NAME, not per "
"image unit: a vertex+geometry+fragment program declaring {} image uniform(s) per "
"stage links when all three stages share one set of names ({} distinct) and is "
"rejected when each stage names its own copies ({} distinct), with the same "
"bindings, the same qualifiers and the same stores either way",
measurement.perStageImageUniforms, measurement.controlDistinctNames,
measurement.subjectDistinctNames);
if (!measurement.driverMessage.empty()) {
detail += format(" - the driver says \"{}\"", measurement.driverMessage);
}
detail +=
". MobileGL names a repaired image uniform after the REPAIR rather than after the "
"stage, so stages that use an image alike keep one shared name and stay merged; "
"applications never see the link failure";
return DriverBugFinding{"Image locations charged per uniform name",
DriverBugVerdict::Fixed, Move(detail)};
}
Optional<DriverBugFinding> ProbeCrossStageImageQualifierMergeBug(const GLESFunctionsTable& gl) {
if (!CrossStageImageQualifierMergeDropsWrites(gl)) return std::nullopt;
return DriverBugFinding{
"Cross-stage image qualifier merge discards writes",
DriverBugVerdict::Fixed,
"an image declared `coherent writeonly` in one stage and `coherent readonly` in "
"another under the SAME name is merged into one uniform whose writing stage's "
"imageStore()s are then silently discarded - every fragment reads the "
"pre-store value. The identical shader pair with the two halves renamed keeps "
"every store, which is what proves the stage's image writes work and only the "
"merge is at fault. MobileGL renames the two halves (mg_imageWo_ / mg_imageRo_) so "
"the driver cannot merge them, and application behaviour is correct"};
}
Optional<DriverBugFinding> ProbeImageCoherencyResidualBug(const GLESFunctionsTable& gl) {
const ImageCoherencyResidualMeasurement& measurement = ImageWriteReadCoherencyResidual(gl);
if (!measurement.detected) return std::nullopt;
const auto percentOf = [&](Int texels) {
return measurement.totalTexels > 0 ? 100.0 * texels / measurement.totalTexels : 0.0;
};
return DriverBugFinding{
"Image write-to-read ordering within one invocation",
DriverBugVerdict::Unfixable,
format(
"an imageLoad() does not observe the imageStore() that precedes it in the same "
"fragment invocation for {} of {} texels ({:.2f}%) under the STRONGEST shape "
"ESSL offers - a `coherent volatile` readonly/writeonly pair on one binding "
"with both memoryBarrierImage() and memoryBarrier() between the store and the "
"read - which is what leaves nothing to substitute. The shape MobileGL emits "
"today (`coherent` plus memoryBarrierImage()) misses {} ({:.2f}%) on this "
"driver. The same dependency split across two draws with a glMemoryBarrier and "
"a glFinish between them is clean, so writes do become visible; it is the "
"ordering inside one invocation that is not honoured. MobileGL keeps the "
"coherent pair and the barrier - without them every texel is wrong - and a "
"shader that reads back its own image write within one invocation cannot be "
"relied on here",
measurement.mismatchedTexels, measurement.totalTexels,
percentOf(measurement.mismatchedTexels), measurement.emittedShapeMismatchedTexels,
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";
// The repair can be switched off from the environment, and a report that said
// "Fixed" while the strip was disabled would be describing a build nobody is
// running. The verdict follows what this process will actually do, not what the
// code is capable of.
const Bool repairDisabled =
MG_Config::Features.EsprytUnlocatedIoBlocks == MG_Config::QuirkOverride::ForceOff;
if (repairDisabled) {
detail +=
". THE REPAIR IS DISABLED in this process: MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS "
"is set to force located blocks ON, so DirectGLES emits the location "
"qualifier the driver cannot honour and the payload is lost. Unset the "
"variable to get the repair back";
} else {
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",
(repairDisabled || measurement.alsoAffectsVertexToFragment)
? DriverBugVerdict::Unfixable
: DriverBugVerdict::Fixed,
Move(detail)};
}
Optional<DriverBugFinding> ProbeCopyImagePacked16FieldOrderBug(const GLESFunctionsTable& gl) {
if (!CopyImageMirrorsPacked16FieldOrder(gl)) return std::nullopt;
// The mitigation is a knob (MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE), so the row consults
// it: under ForceOff - the documented negative control - the corruption is
// replayed verbatim, and a hardcoded "Fixed" would be exactly the kind of
// reassurance this file exists to refuse. Auto and ForceOn both widen once this
// probe has fired. Should POST ever run before env parsing, the field still holds
// its Auto default - which is also what the widening itself consults, so the row
// and the behaviour cannot disagree.
const Bool widened = MG_Config::Features.EsprytWidenPacked16Storage !=
MG_Config::QuirkOverride::ForceOff;
String detail =
"the driver stores SOME 16-bit packed images (RGB565 / RGB5_A1 / RGBA4) with the "
"R/G/B/A fields packed from the other end of the word - which allocations get the "
"*_REV layout depends on shape and context history (measured here on a 30x30x12 "
"three-level 2D array, every level of it) - so a glCopyImageSubData, a raw "
"texel-block move, between a mirrored allocation and a plain one lands the fields "
"reversed (a 5551 word 0x0047 arrives as 0x8C20). Uploads and readbacks of the "
"same image are clean - the driver decodes its own layout consistently, which is "
"this probe's second control - so only the raw-copy path ever crosses the two "
"layouts. ";
if (widened) {
detail +=
"MobileGL stores these three formats as 8-bit-per-channel ES storage on this "
"driver instead (GL_RGB8 / GL_RGBA8, the storage their canonical shadow "
"already holds and the client word round-trips through exactly), so no "
"16-bit packed image is left for a copy to disagree about, at twice the "
"memory for images of those formats; override with "
"MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE";
return DriverBugFinding{
"glCopyImageSubData mirrors 16-bit packed texels between differently-laid-out images",
DriverBugVerdict::Fixed, detail};
}
detail += "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=0 keeps the native narrow storage, so such "
"copies are left exactly as the driver delivers them, mirrored words included";
return DriverBugFinding{
"glCopyImageSubData mirrors 16-bit packed texels between differently-laid-out images",
DriverBugVerdict::Unfixable, detail};
}
Optional<DriverBugFinding> ProbePersistentBufferOrderingBug(const GLESFunctionsTable& gl) {
return DescribePersistentBufferOrderingBug(ProbePersistentBufferUpdateOrdering(gl));
}
// 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[] = {
&ProbeGeometryWriteAfterEmitBug,
&ProbeR32FMultisampleSwizzleBug,
&ProbeImageLocationPerNameBug,
&ProbeCrossStageImageQualifierMergeBug,
&ProbeImageCoherencyResidualBug,
&ProbeExplicitVertexInputLocationCeilingBug,
&ProbeLayeredBlitDestinationBug,
&ProbeLocatedIoBlockPayloadBug,
&ProbeCopyImagePacked16FieldOrderBug,
&ProbePersistentBufferOrderingBug,
};
} // namespace
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const GLESFunctionsTable& gl) {
Vector<DriverBugFinding> findings;
for (const DriverBugProbeFn probe : kGlesDriverBugProbes) {
if (Optional<DriverBugFinding> finding = probe(gl)) {
findings.push_back(Move(*finding));
}
}
return findings;
}
} // namespace MobileGL::MG_Util::SelfTest