diff --git a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp index 91ca697d..a6e1d37b 100644 --- a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp +++ b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp @@ -10,10 +10,21 @@ #include +#include +#include +#include +#include +#include +#include + using namespace MobileGL; using MobileGL::MG_Util::SelfTest::CollectGlesKnownDriverBugs; using MobileGL::MG_Util::SelfTest::DriverBugVerdict; +using MobileGL::MG_Util::SelfTest::ProbeCrossStageImageQualifierMergeDropsWrites; using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped; +using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget; +using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual; +using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption; namespace { // A driver table with nothing resolved. Every probe has to treat this as "cannot tell", @@ -21,6 +32,418 @@ namespace { MG_External::GLESFunctionsTable EmptyFunctionTable() { return MG_External::GLESFunctionsTable{}; } + + // ===================== THE FAKE DRIVER ===================== + // + // Same idea as the fake GLES table BackendLoaderTest drives the gl_InstanceID probe with: + // captureless lambdas over one file-scope state, with per-test knobs that turn each defect + // on and off. It is deliberately a MODEL of the defect rather than a canned answer - the + // fake reads the shader text the probe actually submitted and reproduces what the affected + // driver does with it, so a probe that stopped building the triggering shape would stop + // detecting, which is exactly what these tests are for. + // + // These tests call the Probe* functions directly rather than through + // CollectGlesKnownDriverBugs(): the collector goes through the once-per-process memos, and a + // memo latched by one test would decide the answer for every later one. + + // The exact text an affected Adreno driver puts in the info log for this refusal. + const char* const kImageLocationLinkLog = + "Error: Image Image location or component exceeds max allowed.\nError: Linking failed."; + + struct FakeDriver { + // ---- limits the probes gate on ------------------------------------- + GLint maxColorTextureSamples = 4; + GLint maxImageUnits = 8; + GLint maxVertexImageUniforms = 8; + GLint maxFragmentImageUniforms = 8; + GLint maxGeometryImageUniforms = 3; + // The landed geometry probe reads this; zero keeps it inert so it cannot interfere. + GLint maxGeometrySsboBlocks = 0; + bool geometryImageLimitQueryRaisesError = false; + bool colorTextureSamplesQueryRaisesError = false; + + // ---- defect knobs --------------------------------------------------- + // Probe 1: a swizzled-alpha, non-zero-sample .w fetch reads garbage from the second + // sampling program onward. + bool msaaSwizzledAlphaCorrupted = false; + // Probe 1's inconclusive path: EVERY sampled read is wrong, including the controls. + bool msaaEveryReadWrong = false; + // Probe 2: the link fails once the program declares more distinct image uniform NAMES + // than this. + int distinctImageNameBudget = 1000; + // Probe 3: a same-name coherent writeonly/readonly pair loses the writing stage's store. + bool sameNameImagePairDropsWrites = false; + // Probe 3's inconclusive path: the renamed control loses it too. + bool everyVertexImageWriteDropped = false; + // Probe 4: how many texels the in-invocation dependent read misses under the STRONGEST + // shape, how many it misses under the shape MobileGL emits today, and whether the + // two-draw control misses them too. + int coherencyStrongestShapeFailedTexels = 0; + int coherencyEmittedShapeFailedTexels = 0; + int coherencyControlFailedTexels = 0; + + // ---- object bookkeeping --------------------------------------------- + GLenum pendingError = GL_NO_ERROR; + GLuint nextShaderId = 1; + GLuint nextProgramId = 1; + GLuint nextTextureId = 1; + GLuint nextFramebufferId = 1; + GLuint nextVertexArrayId = 1; + int aliveShaders = 0; + int alivePrograms = 0; + int aliveTextures = 0; + int aliveFramebuffers = 0; + int aliveVertexArrays = 0; + + std::map shaderSources; + std::map> programShaders; + std::map programLinked; + std::map programInfoLogs; + // texture id -> GL_TEXTURE_SWIZZLE_A + std::map multisampleAlphaSwizzle; + + GLuint boundMultisampleTexture = 0; + GLuint currentProgram = 0; + // How many programs that sample a multisample texture have been linked so far. The + // corruption starts at the second. + int sampledMultisampleProgramCount = 0; + // Set by glDrawArrays, consumed by glReadPixels. + GLfloat lastSampledValue = 1.0f; + int lastFailedTexelCount = 0; + }; + + FakeDriver g_fake; + + void ResetFakeDriver() { g_fake = FakeDriver{}; } + + const std::string& SourceOf(GLuint shader) { + static const std::string empty; + const auto it = g_fake.shaderSources.find(shader); + return it == g_fake.shaderSources.end() ? empty : it->second; + } + + bool Contains(const std::string& haystack, const char* needle) { + return haystack.find(needle) != std::string::npos; + } + + // Every `image2D ` the program declares, across all its stages. + std::vector DeclaredImageNames(GLuint program) { + std::vector names; + const auto attached = g_fake.programShaders.find(program); + if (attached == g_fake.programShaders.end()) return names; + for (const GLuint shader : attached->second) { + const std::string& source = SourceOf(shader); + std::size_t at = 0; + while ((at = source.find("image2D ", at)) != std::string::npos) { + at += std::strlen("image2D "); + const std::size_t end = source.find_first_of(";,)", at); + if (end == std::string::npos) break; + std::string name = source.substr(at, end - at); + while (!name.empty() && (name.back() == ' ' || name.back() == '\t')) name.pop_back(); + if (std::find(names.begin(), names.end(), name) == names.end()) { + names.push_back(name); + } + at = end; + } + } + return names; + } + + std::string StageSourceContaining(GLuint program, const char* needle) { + const auto attached = g_fake.programShaders.find(program); + if (attached == g_fake.programShaders.end()) return {}; + for (const GLuint shader : attached->second) { + const std::string& source = SourceOf(shader); + if (Contains(source, needle)) return source; + } + return {}; + } + + // The uniform name in `... image2D ;` of the first declaration in `source`. + std::string FirstImageNameIn(const std::string& source) { + const std::size_t at = source.find("image2D "); + if (at == std::string::npos) return {}; + const std::size_t start = at + std::strlen("image2D "); + const std::size_t end = source.find(';', start); + if (end == std::string::npos) return {}; + return source.substr(start, end - start); + } + + // Whatever the sampling vertex shader asked for: `texelFetch(mg_probeSampler, ivec2(0), N).C`. + void ParseSampledFetch(const std::string& source, int& sampleIndex, char& component) { + sampleIndex = -1; + component = '?'; + const std::size_t at = source.find("texelFetch(mg_probeSampler, ivec2(0), "); + if (at == std::string::npos) return; + const std::size_t start = at + std::strlen("texelFetch(mg_probeSampler, ivec2(0), "); + sampleIndex = std::atoi(source.c_str() + start); + const std::size_t dot = source.find(").", start); + if (dot != std::string::npos && dot + 2 < source.size()) component = source[dot + 2]; + } + + MG_External::GLESFunctionsTable MakeFakeGLESFunctions() { + MG_External::GLESFunctionsTable funcs{}; + + funcs.glGetError = []() -> GLenum { + const GLenum error = g_fake.pendingError; + g_fake.pendingError = GL_NO_ERROR; + return error; + }; + funcs.glGetIntegerv = [](GLenum pname, GLint* data) { + switch (pname) { + case GL_MAX_COLOR_TEXTURE_SAMPLES: + if (g_fake.colorTextureSamplesQueryRaisesError) { + g_fake.pendingError = GL_INVALID_ENUM; + } else { + *data = g_fake.maxColorTextureSamples; + } + break; + case GL_MAX_IMAGE_UNITS: + *data = g_fake.maxImageUnits; + break; + case GL_MAX_VERTEX_IMAGE_UNIFORMS: + *data = g_fake.maxVertexImageUniforms; + break; + case GL_MAX_FRAGMENT_IMAGE_UNIFORMS: + *data = g_fake.maxFragmentImageUniforms; + break; + case GL_MAX_GEOMETRY_IMAGE_UNIFORMS: + if (g_fake.geometryImageLimitQueryRaisesError) { + g_fake.pendingError = GL_INVALID_ENUM; + } else { + *data = g_fake.maxGeometryImageUniforms; + } + break; + case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: + *data = g_fake.maxGeometrySsboBlocks; + break; + default: + break; + } + }; + funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) { *data = 0; }; + funcs.glGetFloatv = [](GLenum, GLfloat* data) { + data[0] = 0.0f; + data[1] = 0.0f; + data[2] = 0.0f; + data[3] = 0.0f; + }; + funcs.glIsEnabled = [](GLenum) -> GLboolean { return GL_FALSE; }; + funcs.glEnable = [](GLenum) {}; + funcs.glDisable = [](GLenum) {}; + funcs.glFinish = []() {}; + funcs.glMemoryBarrier = [](GLbitfield) {}; + funcs.glPixelStorei = [](GLenum, GLint) {}; + funcs.glViewport = [](GLint, GLint, GLsizei, GLsizei) {}; + funcs.glClear = [](GLbitfield) {}; + funcs.glClearColor = [](GLfloat, GLfloat, GLfloat, GLfloat) {}; + funcs.glActiveTexture = [](GLenum) {}; + + // ---- shaders and programs ------------------------------------------- + funcs.glCreateShader = [](GLenum) -> GLuint { + ++g_fake.aliveShaders; + return g_fake.nextShaderId++; + }; + funcs.glShaderSource = [](GLuint shader, GLsizei count, const GLchar* const* strings, + const GLint*) { + std::string source; + for (GLsizei i = 0; i < count; ++i) { + if (strings[i] != nullptr) source += strings[i]; + } + g_fake.shaderSources[shader] = std::move(source); + }; + funcs.glCompileShader = [](GLuint) {}; + funcs.glGetShaderiv = [](GLuint, GLenum pname, GLint* params) { + if (pname == GL_COMPILE_STATUS) *params = GL_TRUE; + }; + funcs.glGetShaderInfoLog = [](GLuint, GLsizei bufSize, GLsizei*, GLchar* infoLog) { + if (bufSize > 0) infoLog[0] = '\0'; + }; + funcs.glDeleteShader = [](GLuint shader) { + if (shader != 0) --g_fake.aliveShaders; + }; + funcs.glCreateProgram = []() -> GLuint { + ++g_fake.alivePrograms; + return g_fake.nextProgramId++; + }; + funcs.glAttachShader = [](GLuint program, GLuint shader) { + g_fake.programShaders[program].push_back(shader); + }; + funcs.glLinkProgram = [](GLuint program) { + const std::vector names = DeclaredImageNames(program); + const bool overBudget = static_cast(names.size()) > g_fake.distinctImageNameBudget; + g_fake.programLinked[program] = !overBudget; + g_fake.programInfoLogs[program] = overBudget ? kImageLocationLinkLog : ""; + if (!overBudget && !StageSourceContaining(program, "texelFetch(mg_probeSampler").empty()) { + ++g_fake.sampledMultisampleProgramCount; + } + }; + funcs.glGetProgramiv = [](GLuint program, GLenum pname, GLint* params) { + if (pname != GL_LINK_STATUS) return; + const auto it = g_fake.programLinked.find(program); + *params = (it == g_fake.programLinked.end() || it->second) ? GL_TRUE : GL_FALSE; + }; + funcs.glGetProgramInfoLog = [](GLuint program, GLsizei bufSize, GLsizei*, GLchar* infoLog) { + if (bufSize <= 0) return; + const auto it = g_fake.programInfoLogs.find(program); + const std::string& log = it == g_fake.programInfoLogs.end() ? std::string() : it->second; + const GLsizei copied = static_cast( + std::min(log.size(), static_cast(bufSize - 1))); + std::memcpy(infoLog, log.data(), static_cast(copied)); + infoLog[copied] = '\0'; + }; + funcs.glDeleteProgram = [](GLuint program) { + if (program != 0) --g_fake.alivePrograms; + }; + funcs.glUseProgram = [](GLuint program) { g_fake.currentProgram = program; }; + funcs.glGetUniformLocation = [](GLuint, const GLchar*) -> GLint { return 0; }; + funcs.glUniform1i = [](GLint, GLint) {}; + + // ---- textures, framebuffers, vertex arrays --------------------------- + funcs.glGenTextures = [](GLsizei n, GLuint* textures) { + for (GLsizei i = 0; i < n; ++i) { + textures[i] = g_fake.nextTextureId++; + ++g_fake.aliveTextures; + } + }; + funcs.glBindTexture = [](GLenum target, GLuint texture) { + if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = texture; + }; + funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) { + for (GLsizei i = 0; i < n; ++i) { + if (textures[i] != 0) --g_fake.aliveTextures; + } + }; + funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) { + if (target == GL_TEXTURE_2D_MULTISAMPLE && pname == GL_TEXTURE_SWIZZLE_A) { + g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture] = + static_cast(param); + } + }; + funcs.glTexImage2D = [](GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, + const void*) {}; + funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, + const void*) {}; + funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {}; + funcs.glTexStorage2DMultisample = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei, GLboolean) {}; + funcs.glGenFramebuffers = [](GLsizei n, GLuint* framebuffers) { + for (GLsizei i = 0; i < n; ++i) { + framebuffers[i] = g_fake.nextFramebufferId++; + ++g_fake.aliveFramebuffers; + } + }; + funcs.glBindFramebuffer = [](GLenum, GLuint) {}; + funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {}; + funcs.glCheckFramebufferStatus = [](GLenum) -> GLenum { return GL_FRAMEBUFFER_COMPLETE; }; + funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) { + for (GLsizei i = 0; i < n; ++i) { + if (framebuffers[i] != 0) --g_fake.aliveFramebuffers; + } + }; + funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) { + for (GLsizei i = 0; i < n; ++i) { + arrays[i] = g_fake.nextVertexArrayId++; + ++g_fake.aliveVertexArrays; + } + }; + funcs.glBindVertexArray = [](GLuint) {}; + funcs.glDeleteVertexArrays = [](GLsizei n, const GLuint* arrays) { + for (GLsizei i = 0; i < n; ++i) { + if (arrays[i] != 0) --g_fake.aliveVertexArrays; + } + }; + funcs.glBindImageTexture = [](GLuint, GLuint, GLint, GLboolean, GLint, GLenum, GLenum) {}; + + // ---- the draw, where the defects live -------------------------------- + funcs.glDrawArrays = [](GLenum, GLint, GLsizei) { + const GLuint program = g_fake.currentProgram; + const std::string sampling = StageSourceContaining(program, "texelFetch(mg_probeSampler"); + if (!sampling.empty()) { + int sampleIndex = -1; + char component = '?'; + ParseSampledFetch(sampling, sampleIndex, component); + const GLenum swizzle = g_fake.multisampleAlphaSwizzle.count( + g_fake.boundMultisampleTexture) != 0 + ? g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture] + : GL_ALPHA; + // An R32F texel filled with (1, 0, 0, -) reads 1.0 through both the ALPHA and the + // RED swizzle sources, which is why one expected constant covers every shape. + g_fake.lastSampledValue = 1.0f; + if (g_fake.msaaEveryReadWrong) { + g_fake.lastSampledValue = 0.0f; + } else if (g_fake.msaaSwizzledAlphaCorrupted && swizzle == GL_RED && component == 'w' && + sampleIndex != 0 && g_fake.sampledMultisampleProgramCount >= 2) { + // Uninitialised memory: a value that is neither the answer nor the clear. + g_fake.lastSampledValue = -1.34954e-17f; + } + return; + } + + // Matched on the access qualifier alone, not on "coherent writeonly": the strongest + // coherency shape spells it "coherent volatile writeonly". + const std::string writeStage = StageSourceContaining(program, "writeonly"); + const std::string readStage = StageSourceContaining(program, "readonly"); + if (!writeStage.empty() && !readStage.empty() && Contains(readStage, "memoryBarrierImage")) { + // The coherency probe: one invocation stores and then reads back. `volatile` is + // what tells the strongest shape apart from the one MobileGL emits today, and + // giving them separate knobs is what lets a test pin the case where only the + // emitted shape is wrong - a fixable defect that must not be reported here. + g_fake.lastFailedTexelCount = Contains(readStage, "coherent volatile") + ? g_fake.coherencyStrongestShapeFailedTexels + : g_fake.coherencyEmittedShapeFailedTexels; + return; + } + if (!writeStage.empty() && readStage.empty()) { + // The coherency control's store half; the load half decides the result. + g_fake.lastFailedTexelCount = 0; + return; + } + if (writeStage.empty() && !readStage.empty()) { + g_fake.lastFailedTexelCount = g_fake.coherencyControlFailedTexels; + return; + } + if (!writeStage.empty() && !readStage.empty()) { + // The qualifier-merge pair: the stores are lost when the two halves share a name. + const bool sharedName = + FirstImageNameIn(writeStage) == FirstImageNameIn(readStage) && + !FirstImageNameIn(writeStage).empty(); + const bool lost = g_fake.everyVertexImageWriteDropped || + (g_fake.sameNameImagePairDropsWrites && sharedName); + g_fake.lastFailedTexelCount = lost ? 1 << 20 : 0; + return; + } + g_fake.lastFailedTexelCount = 0; + }; + funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type, + void* pixels) { + const std::size_t texels = static_cast(width) * static_cast(height); + if (format == GL_RED && type == GL_FLOAT) { + GLfloat* out = static_cast(pixels); + for (std::size_t i = 0; i < texels; ++i) out[i] = g_fake.lastSampledValue; + return; + } + GLubyte* out = static_cast(pixels); + const std::size_t failed = + std::min(texels, static_cast(g_fake.lastFailedTexelCount)); + for (std::size_t i = 0; i < texels; ++i) { + const bool ok = i >= failed; + out[i * 4 + 0] = ok ? 0 : 255; + out[i * 4 + 1] = ok ? 255 : 0; + out[i * 4 + 2] = 0; + out[i * 4 + 3] = 255; + } + }; + + return funcs; + } + + void ExpectProbeReleasedEverything() { + EXPECT_EQ(g_fake.aliveShaders, 0); + EXPECT_EQ(g_fake.alivePrograms, 0); + EXPECT_EQ(g_fake.aliveTextures, 0); + EXPECT_EQ(g_fake.aliveFramebuffers, 0); + EXPECT_EQ(g_fake.aliveVertexArrays, 0); + } } // namespace // The rule the whole section depends on: a probe that cannot run reports NO bug. If an @@ -31,6 +454,10 @@ TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) { const MG_External::GLESFunctionsTable gl = EmptyFunctionTable(); EXPECT_FALSE(ProbeGeometryStageSsboWriteAfterEmitDropped(gl)) << "a probe with no entry points to call must not claim the driver is affected"; + EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl)); + EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected); + EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl)); + EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected); } // The section lists only bugs the device HAS, so a driver nothing could be probed on renders @@ -52,3 +479,182 @@ TEST(DriverBugProbes, EveryFindingCarriesANameAndAnExplanation) { finding.verdict == DriverBugVerdict::Unfixable); } } + +// ===================== R32F MULTISAMPLE SWIZZLE ===================== + +TEST(DriverBugProbes, R32FMultisampleSwizzleIsCleanOnAConformingDriver) { + ResetFakeDriver(); + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl)); + ExpectProbeReleasedEverything(); +} + +TEST(DriverBugProbes, R32FMultisampleSwizzleIsDetectedFromTheSecondProgramOnward) { + ResetFakeDriver(); + g_fake.msaaSwizzledAlphaCorrupted = true; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_TRUE(ProbeR32FMultisampleSwizzleCorruption(gl)); + ExpectProbeReleasedEverything(); +} + +// The control rule, made executable: a driver on which even the default-swizzle, sample-zero and +// .x reads are wrong is broken in some larger way, and the probe may not name the alpha swizzle +// as the cause. +TEST(DriverBugProbes, R32FMultisampleSwizzleReportsNothingWhenTheControlsAreWrongToo) { + ResetFakeDriver(); + g_fake.msaaSwizzledAlphaCorrupted = true; + g_fake.msaaEveryReadWrong = true; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl)) + << "with every read wrong the probe has no evidence that the alpha swizzle is the variable"; +} + +TEST(DriverBugProbes, R32FMultisampleSwizzleNeedsMoreThanOneSample) { + ResetFakeDriver(); + g_fake.msaaSwizzledAlphaCorrupted = true; + g_fake.maxColorTextureSamples = 1; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl)); +} + +// ===================== IMAGE LOCATION PER NAME ===================== + +TEST(DriverBugProbes, ImageLocationBudgetIsCleanWhenNamesDoNotCost) { + ResetFakeDriver(); + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + const auto measurement = ProbeImageLocationPerNameBudget(gl); + EXPECT_FALSE(measurement.detected); + ExpectProbeReleasedEverything(); +} + +TEST(DriverBugProbes, ImageLocationBudgetIsDetectedWhenOnlyTheSharedNamesLink) { + ResetFakeDriver(); + // Four image uniforms per stage: twelve distinct names in the subject, four in the control. + g_fake.distinctImageNameBudget = 5; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + const auto measurement = ProbeImageLocationPerNameBudget(gl); + EXPECT_TRUE(measurement.detected); + EXPECT_EQ(measurement.perStageImageUniforms, g_fake.maxGeometryImageUniforms + 1); + EXPECT_EQ(measurement.subjectDistinctNames, measurement.perStageImageUniforms * 3); + EXPECT_EQ(measurement.controlDistinctNames, measurement.perStageImageUniforms); + EXPECT_NE(measurement.driverMessage.find("exceeds max allowed"), String::npos) + << "the report quotes the driver rather than paraphrasing it"; + ExpectProbeReleasedEverything(); +} + +// The control rule again: when the shared-name program is refused too, the shape is simply too +// big for this driver and the refusal is honest. +TEST(DriverBugProbes, ImageLocationBudgetReportsNothingWhenTheControlAlsoFails) { + ResetFakeDriver(); + g_fake.distinctImageNameBudget = 2; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected); +} + +TEST(DriverBugProbes, ImageLocationBudgetNeedsAGeometryStageThatCanHoldImages) { + ResetFakeDriver(); + g_fake.distinctImageNameBudget = 5; + g_fake.maxGeometryImageUniforms = 0; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected); +} + +TEST(DriverBugProbes, ImageLocationBudgetStaysSilentOnAContextWithoutTheGeometryLimit) { + ResetFakeDriver(); + g_fake.distinctImageNameBudget = 5; + g_fake.geometryImageLimitQueryRaisesError = true; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected) + << "a pre-ES-3.2 context has no geometry stage to build the shape out of"; +} + +// ===================== CROSS-STAGE QUALIFIER MERGE ===================== + +TEST(DriverBugProbes, QualifierMergeIsCleanWhenTheDriverKeepsTheStore) { + ResetFakeDriver(); + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl)); + ExpectProbeReleasedEverything(); +} + +TEST(DriverBugProbes, QualifierMergeIsDetectedWhenOnlyTheSharedNameLosesTheStore) { + ResetFakeDriver(); + g_fake.sameNameImagePairDropsWrites = true; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_TRUE(ProbeCrossStageImageQualifierMergeDropsWrites(gl)); + ExpectProbeReleasedEverything(); +} + +// A driver that loses the RENAMED store too cannot write images from the vertex stage at all - +// a different and much larger claim, which this probe may not make. +TEST(DriverBugProbes, QualifierMergeReportsNothingWhenTheRenamedControlAlsoFails) { + ResetFakeDriver(); + g_fake.sameNameImagePairDropsWrites = true; + g_fake.everyVertexImageWriteDropped = true; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl)); +} + +TEST(DriverBugProbes, QualifierMergeNeedsVertexStageImageUniforms) { + ResetFakeDriver(); + g_fake.sameNameImagePairDropsWrites = true; + g_fake.maxVertexImageUniforms = 0; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl)); +} + +// ===================== IMAGE COHERENCY RESIDUAL ===================== + +TEST(DriverBugProbes, ImageCoherencyIsCleanWhenTheDependentReadObservesTheStore) { + ResetFakeDriver(); + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + const auto measurement = ProbeImageWriteReadCoherencyResidual(gl); + EXPECT_FALSE(measurement.detected); + ExpectProbeReleasedEverything(); +} + +TEST(DriverBugProbes, ImageCoherencyResidualIsDetectedAndQuantified) { + ResetFakeDriver(); + g_fake.coherencyStrongestShapeFailedTexels = 376; + g_fake.coherencyEmittedShapeFailedTexels = 418; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + const auto measurement = ProbeImageWriteReadCoherencyResidual(gl); + EXPECT_TRUE(measurement.detected); + EXPECT_EQ(measurement.mismatchedTexels, 376); + EXPECT_EQ(measurement.emittedShapeMismatchedTexels, 418) + << "the row reports what applications get, not only what is theoretically reachable"; + EXPECT_GT(measurement.totalTexels, 418) << "the report needs a denominator to quote a rate"; + ExpectProbeReleasedEverything(); +} + +// The reason the subject is the STRONGEST shape and not the one MobileGL emits. Mesa llvmpipe +// misses every texel with `coherent` + memoryBarrierImage() and none once the pair is also +// `volatile` - a defect MobileGL could fix by emitting a different shape, which is not what +// UNFIXABLE means and does not belong in this section. +TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenAStrongerShapeWouldFixIt) { + ResetFakeDriver(); + g_fake.coherencyStrongestShapeFailedTexels = 0; + g_fake.coherencyEmittedShapeFailedTexels = 4096; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected) + << "a driver the volatile shape satisfies has a fixable defect, not an unfixable one"; +} + +// The control rule once more: a driver whose glFinish-separated two-draw dependency is ALSO +// dirty has a bigger defect than an in-invocation ordering residual, and this probe must not +// dress that up as one. +TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenTheFinishSeparatedControlIsDirtyToo) { + ResetFakeDriver(); + g_fake.coherencyStrongestShapeFailedTexels = 376; + g_fake.coherencyControlFailedTexels = 4096; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected); +} + +TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) { + ResetFakeDriver(); + g_fake.coherencyStrongestShapeFailedTexels = 376; + g_fake.maxFragmentImageUniforms = 1; + const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions(); + EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected); +} diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index e9ca0d29..c0b051f7 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -18,6 +18,8 @@ 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 @@ -74,7 +76,7 @@ namespace MobileGL::MG_Util::SelfTest { } GLuint CompileStage(const GLESFunctionsTable& gl, GLenum stage, const char* source, - const char* stageName) { + const char* stageName, const char* probeName) { const GLuint shader = gl.glCreateShader(stage); if (shader == 0) return 0; gl.glShaderSource(shader, 1, &source, nullptr); @@ -87,8 +89,7 @@ namespace MobileGL::MG_Util::SelfTest { // reason that is indistinguishable from a clean driver. char log[512] = {0}; gl.glGetShaderInfoLog(shader, static_cast(sizeof(log) - 1), nullptr, log); - MGLOG_I("[driver-bug] geometry write-after-emit probe: %s stage did not compile: %s", - stageName, log); + MGLOG_I("[driver-bug] %s probe: %s stage did not compile: %s", probeName, stageName, log); gl.glDeleteShader(shader); return 0; } @@ -97,6 +98,10 @@ namespace MobileGL::MG_Util::SelfTest { // 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; @@ -109,8 +114,28 @@ namespace MobileGL::MG_Util::SelfTest { 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; + GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + GLint packAlignment = 4; + GLint packRowLength = 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); @@ -125,23 +150,77 @@ namespace MobileGL::MG_Util::SelfTest { 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); + gl.glGetIntegerv(GL_TEXTURE_BINDING_2D, &state.texture2D); + gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_MULTISAMPLE, &state.texture2DMultisample); + gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment); + gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength); + 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) { - for (GLuint i = 0; i < 2; ++i) { - gl.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, i, - static_cast(state.indexedStorageBuffer[i])); + if (gl.glBindBufferBase != nullptr) { + for (GLuint i = 0; i < 2; ++i) { + gl.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, i, + static_cast(state.indexedStorageBuffer[i])); + } + } + if (gl.glBindBuffer != nullptr) { + gl.glBindBuffer(GL_SHADER_STORAGE_BUFFER, static_cast(state.storageBuffer)); + } + if (gl.glBindRenderbuffer != nullptr) { + gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(state.renderbuffer)); } - gl.glBindBuffer(GL_SHADER_STORAGE_BUFFER, static_cast(state.storageBuffer)); - gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(state.renderbuffer)); gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(state.drawFramebuffer)); gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(state.readFramebuffer)); gl.glBindVertexArray(static_cast(state.vertexArray)); gl.glUseProgram(static_cast(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(state.imageName), + state.imageLevel, state.imageLayered != 0 ? GL_TRUE : GL_FALSE, + state.imageLayer, static_cast(state.imageAccess), + static_cast(state.imageFormat)); + } + if (gl.glActiveTexture != nullptr) { + gl.glActiveTexture(GL_TEXTURE0); + if (gl.glBindTexture != nullptr) { + gl.glBindTexture(GL_TEXTURE_2D, static_cast(state.texture2D)); + gl.glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, + static_cast(state.texture2DMultisample)); + } + gl.glActiveTexture(static_cast(state.activeTexture)); + } + if (gl.glPixelStorei != nullptr) { + gl.glPixelStorei(GL_PACK_ALIGNMENT, state.packAlignment); + gl.glPixelStorei(GL_PACK_ROW_LENGTH, state.packRowLength); + } + 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); } @@ -180,9 +259,12 @@ namespace MobileGL::MG_Util::SelfTest { GLuint vertexArray = 0, framebuffer = 0, renderbuffer = 0; do { - vertexShader = CompileStage(gl, GL_VERTEX_SHADER, kProbeVertexSource, "vertex"); - geometryShader = CompileStage(gl, GL_GEOMETRY_SHADER, kProbeGeometrySource, "geometry"); - fragmentShader = CompileStage(gl, GL_FRAGMENT_SHADER, kProbeFragmentSource, "fragment"); + 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; @@ -289,6 +371,982 @@ namespace MobileGL::MG_Util::SelfTest { 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& stages, + const char* probeName) { + ProgramBuild build; + Vector 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(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& 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 pixels(static_cast(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& values) { + if (values.empty()) return false; + for (const GLfloat value : values) { + if (value != kMsaaExpected) return false; + } + return true; + } + + String DescribeRounds(const Vector& 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(targets.size) * targets.size * 4; + const Vector 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 pixels(static_cast(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(maxColorTextureSamples); + const Int lastSample = static_cast(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 subject; + Vector controlDefaultSwizzle; + Vector controlSampleZero; + Vector 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(samples), static_cast(lastSample), + DescribeRounds(subject).c_str(), DescribeRounds(controlDefaultSwizzle).c_str(), + DescribeRounds(controlSampleZero).c_str(), DescribeRounds(controlRedComponent).c_str(), + static_cast(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(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(maxGeometryImages), static_cast(maxVertexImages), + static_cast(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(maxImageUnits), "mg_probeVsImage", + "mg_probeGsImage", "mg_probeFsImage"); + ProgramBuild control = + BuildThreeStageImageProgram(gl, perStage, static_cast(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(perStage), static_cast(maxImageUnits), + static_cast(measurement.subjectDistinctNames), + subject.linked ? "link" : "FAIL", static_cast(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(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(subjectFailed), + static_cast(kQualifierMergeSize) * static_cast(kQualifierMergeSize), + static_cast(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(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(kCoherencySize) * static_cast(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(subjectFailed), static_cast(measurement.totalTexels), + static_cast(emittedFailed), static_cast(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 { Optional ProbeGeometryWriteAfterEmitBug(const GLESFunctionsTable& gl) { if (!GeometryStageSsboWriteAfterEmitDropped(gl)) return std::nullopt; @@ -303,10 +1361,93 @@ namespace MobileGL::MG_Util::SelfTest { "A shader that must write after emitting has no substitute on this driver"}; } + Optional 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 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 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 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))}; + } + // The table. One row per known driver bug; see the header for how to add a sibling. using DriverBugProbeFn = Optional (*)(const GLESFunctionsTable&); constexpr DriverBugProbeFn kGlesDriverBugProbes[] = { &ProbeGeometryWriteAfterEmitBug, + &ProbeR32FMultisampleSwizzleBug, + &ProbeImageLocationPerNameBug, + &ProbeCrossStageImageQualifierMergeBug, + &ProbeImageCoherencyResidualBug, }; } // namespace diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index 16315bee..f3cf4333 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -33,9 +33,7 @@ namespace MobileGL::MG_Util::SelfTest { // // ADDING A SIBLING IS ONE FUNCTION: write an `Optional ProbeXxx(gl)` // that returns nullopt when the driver is not affected, and add it to the table in - // CollectGlesKnownDriverBugs(). Known siblings still to be probed: the R32F-MSAA swizzle - // corruption, the image-location-per-name link limit, the cross-stage qualifier merge, and - // the coherency residual. + // CollectGlesKnownDriverBugs(). // What MobileGL can do about a bug this device HAS. There is deliberately no "not // affected" member: a driver that passes the probe produces no finding at all, so the @@ -78,6 +76,123 @@ namespace MobileGL::MG_Util::SelfTest { // ProbeGeometryStageSsboWriteAfterEmitDropped(), evaluated at most once per process. Bool GeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl); + // Samples one R32F GL_TEXTURE_2D_MULTISAMPLE texel through a swizzled alpha channel, twice, + // with a separately linked program each time. Returns true only when the swizzled read goes + // wrong while every control read stays right. + // + // Adreno 830 returns uninitialised memory - a different value every run - for + // texelFetch(sampler2DMS, ..., sampleIndex != 0).w on an R32F multisample texture whose + // GL_TEXTURE_SWIZZLE_A is not the default, from the SECOND such program in the context + // onward. The first program reads correctly, which is why the probe links two. + // + // THREE CONTROLS, each identical to the subject but for one variable, and all three must + // read correctly for a wrong subject to count: (1) the same fetch with + // GL_TEXTURE_SWIZZLE_A left at its default, (2) the same fetch at sample index 0, and + // (3) the same swizzled texture read through .x instead of .w. Without them a driver that + // simply cannot render R32F, or cannot sample multisample textures at all, would be + // reported as having this very specific corruption. + // + // Returns false when the driver cannot host the shape (no multisample R32F colour target, + // fewer than two samples, a missing entry point, an incomplete framebuffer): an + // inconclusive probe must never be reported as a bug. Restores every piece of GL state it + // touches. + Bool ProbeR32FMultisampleSwizzleCorruption(const MG_External::GLESFunctionsTable& gl); + + // ProbeR32FMultisampleSwizzleCorruption(), evaluated at most once per process. + Bool R32FMultisampleSwizzleCorrupted(const MG_External::GLESFunctionsTable& gl); + + // What the image-location budget probe measured. `detected` is the only field the verdict + // depends on; the rest exist so the report can say what the shape was instead of asserting + // a number that was true on one device in one campaign. + struct ImageLocationBudgetMeasurement { + Bool detected = false; + // Image uniforms declared per stage in both the subject and the control - one more than + // GL_MAX_GEOMETRY_IMAGE_UNIFORMS, which is the smallest of the three stages' budgets. + Int perStageImageUniforms = 0; + // Distinct uniform NAMES in the subject (per-stage-unique) and in the control (shared). + Int subjectDistinctNames = 0; + Int controlDistinctNames = 0; + // The first line of the driver's info log for the failing link, so the report quotes the + // driver rather than paraphrasing it. + String driverMessage; + }; + + // Links the same three-stage (vertex, geometry, fragment) program twice: once with every + // stage naming its image uniforms uniquely, once with all three stages sharing one set of + // names. Both declare the same number of image uniforms per stage, on the same bindings, + // with the same qualifier and the same stores - the names are the only difference. + // + // Adreno 830 charges its image-location budget per distinct NAME, so the shared-name program + // links while the per-stage-named one is rejected with "Image location or component exceeds + // max allowed", even though nothing about the image USAGE changed. That is what makes the + // shared-name link the control: it proves the driver can host this exact amount of image + // work and that only the naming moved the answer. + // + // `detected` is false unless the subject fails AND the control links. Both failing means the + // shape is simply too large for the driver (an honest refusal); both linking means the + // driver does not have this bug. + ImageLocationBudgetMeasurement ProbeImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl); + + // ProbeImageLocationPerNameBudget(), evaluated at most once per process. + const ImageLocationBudgetMeasurement& ImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl); + + // Draws one quad whose vertex stage stores to a `coherent writeonly` image and whose + // fragment stage reads the same image declared `coherent readonly` under the SAME name, then + // checks every fragment saw the store. Returns true only when the same-name program loses + // the store while the different-name control keeps it. + // + // Adreno 830 merges the two declarations into one uniform and silently discards the writing + // stage's stores. The control is the identical pair of shaders with the two halves renamed - + // exactly what MobileGL's image-uniform repair emits - which keeps every store. Without it + // the probe would be indistinguishable from "this driver cannot store to images from the + // vertex stage", which is a different and much larger claim. + // + // Returns false when the driver advertises no vertex-stage image uniforms, when an entry + // point is missing, or when the setup fails. + Bool ProbeCrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl); + + // ProbeCrossStageImageQualifierMergeDropsWrites(), evaluated at most once per process. + Bool CrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl); + + // What the image coherency probe measured. The residual is reported rather than hard-coded: + // it is a rate, it differs between devices, and a report that quotes a number measured + // somewhere else is worse than no number at all. + struct ImageCoherencyResidualMeasurement { + Bool detected = false; + // Texels the STRONGEST in-shader shape missed - that is what makes the defect unfixable. + Int mismatchedTexels = 0; + // Texels the shape MobileGL emits today missed, on the same driver in the same run. It + // is what applications actually get, and it is not always the same number. + Int emittedShapeMismatchedTexels = 0; + Int totalTexels = 0; + }; + + // Counts the texels whose dependent imageLoad() did not observe the imageStore() that + // precedes it in the same fragment invocation. + // + // THE SUBJECT IS THE STRONGEST SHAPE THE LANGUAGE OFFERS - a `coherent volatile` + // readonly/writeonly pair on one binding with BOTH memoryBarrierImage() and memoryBarrier() + // between the store and the read - and that choice is the whole reason the row can say + // "unfixable". Probing only the shape MobileGL emits today (`coherent` plus + // memoryBarrierImage()) reports a bug on drivers where simply adding `volatile` makes the + // read correct, which is a defect MobileGL could fix rather than one it cannot: measured on + // Mesa llvmpipe, the emitted shape misses every texel while the `volatile` shape misses + // none. Only a driver that fails even the strongest shape has no in-shader substitute left. + // + // The control is the same dependency split across TWO draws with a glMemoryBarrier and a + // glFinish between them. It separates "this driver cannot make image writes visible at all" + // (control also dirty - a far worse defect, and the probe declines to call it this one) from + // the finding, which is about ordering inside one invocation. + // + // `detected` is false unless the strongest shape is dirty AND the control is clean. The + // shape MobileGL emits is measured either way, so the report can say what applications get. + ImageCoherencyResidualMeasurement ProbeImageWriteReadCoherencyResidual( + const MG_External::GLESFunctionsTable& gl); + + // ProbeImageWriteReadCoherencyResidual(), evaluated at most once per process. + const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual( + const MG_External::GLESFunctionsTable& gl); + // Every known driver bug this GLES driver actually has. Bugs it does not have are absent, // so an unaffected device renders an empty section rather than a wall of "not affected". Vector CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);