diff --git a/CMakeLists.txt b/CMakeLists.txt index aab1cf97..ae52f6bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,6 +310,7 @@ set(SOURCE_FILES MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp + MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp MobileGL/MG_Util/SelfTest/DriverPost.cpp MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp diff --git a/MobileGL/MG_Test/SelfTest/CMakeLists.txt b/MobileGL/MG_Test/SelfTest/CMakeLists.txt index 3dd1e5c0..47c7714b 100644 --- a/MobileGL/MG_Test/SelfTest/CMakeLists.txt +++ b/MobileGL/MG_Test/SelfTest/CMakeLists.txt @@ -15,5 +15,21 @@ target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE ${LINK_LIBRARIES} ) +add_executable( + DriverBugProbesTest + DriverBugProbesTest.cpp +) + +target_include_directories(DriverBugProbesTest PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL +) + +target_link_libraries(DriverBugProbesTest PRIVATE + GTest::gtest_main + ${LINK_LIBRARIES} +) + include(GoogleTest) gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) +gtest_discover_tests(DriverBugProbesTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) diff --git a/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp new file mode 100644 index 00000000..91ca697d --- /dev/null +++ b/MobileGL/MG_Test/SelfTest/DriverBugProbesTest.cpp @@ -0,0 +1,54 @@ +// MobileGL - MobileGL/MG_Test/SelfTest/DriverBugProbesTest.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 + +#include + +using namespace MobileGL; +using MobileGL::MG_Util::SelfTest::CollectGlesKnownDriverBugs; +using MobileGL::MG_Util::SelfTest::DriverBugVerdict; +using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped; + +namespace { + // A driver table with nothing resolved. Every probe has to treat this as "cannot tell", + // never as "affected". + MG_External::GLESFunctionsTable EmptyFunctionTable() { + return MG_External::GLESFunctionsTable{}; + } +} // namespace + +// The rule the whole section depends on: a probe that cannot run reports NO bug. If an +// unrunnable probe answered "affected", every device without the entry points - every desktop +// build, every unit-test process - would grow a driver-bug row it has no evidence for, and the +// section would stop meaning "this device has these bugs". +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"; +} + +// The section lists only bugs the device HAS, so a driver nothing could be probed on renders +// nothing at all rather than a list of reassurances. +TEST(DriverBugProbes, CollectsNoFindingsWhenNothingCanBeProbed) { + const MG_External::GLESFunctionsTable gl = EmptyFunctionTable(); + EXPECT_TRUE(CollectGlesKnownDriverBugs(gl).empty()); +} + +// Every finding the table can produce is a bug that is PRESENT, which is why the vocabulary is +// FIXED/UNFIXABLE and not PASS/FAIL. This latches that no probe can smuggle in a "not affected" +// row by returning a finding with an empty name or detail - the screen renders both. +TEST(DriverBugProbes, EveryFindingCarriesANameAndAnExplanation) { + const MG_External::GLESFunctionsTable gl = EmptyFunctionTable(); + for (const auto& finding : CollectGlesKnownDriverBugs(gl)) { + EXPECT_FALSE(finding.name.empty()); + EXPECT_FALSE(finding.detail.empty()) << finding.name << " must say what MobileGL does about it"; + EXPECT_TRUE(finding.verdict == DriverBugVerdict::Fixed || + finding.verdict == DriverBugVerdict::Unfixable); + } +} diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp new file mode 100644 index 00000000..e9ca0d29 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -0,0 +1,322 @@ +// 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 + +#include +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + namespace { + using MG_External::GLESFunctionsTable; + + 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 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(sizeof(log) - 1), nullptr, log); + MGLOG_I("[driver-bug] geometry write-after-emit probe: %s stage did not compile: %s", + 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. + 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; + }; + + 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); + } + + 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])); + } + 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 (state.rasterizerDiscard) gl.glEnable(GL_RASTERIZER_DISCARD); + if (state.scissorTest) gl.glEnable(GL_SCISSOR_TEST); + if (state.cullFace) gl.glEnable(GL_CULL_FACE); + 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"); + geometryShader = CompileStage(gl, GL_GEOMETRY_SHADER, kProbeGeometrySource, "geometry"); + fragmentShader = CompileStage(gl, GL_FRAGMENT_SHADER, kProbeFragmentSource, "fragment"); + 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 { + Optional 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"}; + } + + // 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, + }; + } // namespace + + Vector CollectGlesKnownDriverBugs(const GLESFunctionsTable& gl) { + Vector findings; + for (const DriverBugProbeFn probe : kGlesDriverBugProbes) { + if (Optional finding = probe(gl)) { + findings.push_back(Move(*finding)); + } + } + return findings; + } +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h new file mode 100644 index 00000000..16315bee --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -0,0 +1,84 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/DriverBugProbes.h +// 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 + +#pragma once +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + // ===================== KNOWN DRIVER BUGS ===================== + // + // THIS IS THE DESIGNATED HOME FOR DRIVER-CAPABILITY LIES. + // + // The rest of the POST suite answers a different question: does the extension exist, and + // does a simple probe show it working. The entries here are not extension questions at + // all - they are CORE functionality that a driver advertises, accepts without error, and + // then does not perform. Nothing in an extension string or a limit query says so, which + // is exactly why each one needs its own executable probe. + // + // The inventory comes from CAMPAIGN FINDINGS, not from anything the driver reports. + // + // EVERY PROBE MUST CARRY A CONTROL. The geometry entry below is why the rule is written + // down: the same defect was first characterised as "this driver drops all geometry-stage + // storage-buffer writes", which would have justified withdrawing + // GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS entirely. A control showed geometry-stage writes + // land perfectly well when they precede EmitVertex(), so the limit is not a lie and + // withdrawing it would have broken shaders that work today. A probe without a control + // measures a symptom and invites exactly that over-correction. + // + // 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. + + // 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 + // report only ever lists bugs actually present on this device. + enum class DriverBugVerdict : Uint8 { + // A MobileGL quirk repairs or substitutes for the defect and the application sees + // correct behaviour. + Fixed, + // There is no substitute. `detail` says what MobileGL does defensively instead, and + // what an application can still rely on. + Unfixable, + }; + + struct DriverBugFinding { + // Short name of the bug, not of the feature. + String name; + DriverBugVerdict verdict = DriverBugVerdict::Unfixable; + // One line: what the driver does wrong, and what MobileGL does about it. + String detail; + }; + + // Draws one point through VS+GS+FS whose geometry stage writes two storage buffers: one + // BEFORE its EmitVertex()/EndPrimitive() and one AFTER. Returns true only when the + // before-emit write lands and the after-emit write does not. + // + // The before-emit write is the control, and it is the whole point of the probe. Adreno 830 + // discards geometry-stage storage writes issued after the last emit while performing the + // identical write issued before it (measured both ways, and for both point and triangle + // geometry shaders, so the primitive shape is not the variable). Reading only the + // after-emit half would say "geometry storage writes do not work on this driver", which is + // false and would justify withdrawing a limit applications legitimately use. + // + // Deterministic by construction - the write either reaches memory or the driver + // structurally discards it - so the answer is latched, not sampled. Returns false when the + // driver advertises no geometry storage blocks, when an entry point is missing, or when + // anything about the probe fails to set up: an inconclusive probe must never be reported + // as a bug. Restores every piece of GL state it touches. + Bool ProbeGeometryStageSsboWriteAfterEmitDropped(const MG_External::GLESFunctionsTable& gl); + + // ProbeGeometryStageSsboWriteAfterEmitDropped(), evaluated at most once per process. + Bool GeometryStageSsboWriteAfterEmitDropped(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); +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 5e1942d6..3dc44e16 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -1157,6 +1157,11 @@ namespace MobileGL::MG_Util::SelfTest { MG_Backend::DirectGLES::PopulateFormatCapabilities( glesFuncs, caps, builder.report.formatCapabilities.value()); ReportThreeChannelColorAttachments(builder, caps, builder.report.formatCapabilities.value()); + // The "Known Driver Bugs" section. Deliberately last, and deliberately not a + // builder.Pass/Warn/Fail row: these are not capability checks and they must not move + // the backend verdict, which is about whether the backend can RUN on this driver. + // Only bugs the device actually has come back, so a clean driver adds nothing here. + builder.report.knownDriverBugs = CollectGlesKnownDriverBugs(glesFuncs); } while (false); } diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.h b/MobileGL/MG_Util/SelfTest/DriverPost.h index e698acf9..2f417484 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.h +++ b/MobileGL/MG_Util/SelfTest/DriverPost.h @@ -7,6 +7,8 @@ // End of Source File Header #pragma once +#include "DriverBugProbes.h" + #include #include @@ -34,6 +36,17 @@ namespace MobileGL::MG_Util::SelfTest { String verdict = "UNSUPPORTED"; // "OK" | "DEGRADED" | "UNSUPPORTED" String rendererInfo; Vector checks; + // The "Known Driver Bugs" section, kept apart from `checks` on purpose. `checks` asks + // whether a feature is there and roughly works; these are core features the driver + // claims, accepts, and then does not perform - a separate question, from a separate + // inventory (campaign findings, not the extension string). See DriverBugProbes.h. + // + // Only bugs this device ACTUALLY HAS appear here: a probe that comes back clean + // contributes no entry, so an unaffected driver renders the section empty rather than + // as a list of reassurances. That is also why the verdict vocabulary is FIXED / + // UNFIXABLE rather than PASS / FAIL - every row is a bug that is present, and the + // verdict says whether MobileGL can do anything about it. + Vector knownDriverBugs; Optional formatCapabilities; }; diff --git a/MobileGL/MG_Util/SelfTest/DriverPostJni.cpp b/MobileGL/MG_Util/SelfTest/DriverPostJni.cpp index c42f57b0..79a1b439 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPostJni.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPostJni.cpp @@ -144,6 +144,27 @@ namespace { out << '}'; } out << ']'; + // The "Known Driver Bugs" section, separate from "checks" because it answers a + // different question and uses a different verdict vocabulary (FIXED | UNFIXABLE). + // Every entry is a bug the device HAS - a clean probe contributes nothing - so an + // unaffected driver serializes an empty array and the screen renders no section. + out << ",\"knownDriverBugs\":["; + for (SizeT i = 0; i < report.knownDriverBugs.size(); ++i) { + const MobileGL::MG_Util::SelfTest::DriverBugFinding& bug = report.knownDriverBugs[i]; + if (i != 0) { + out << ','; + } + out << "{\"name\":"; + AppendJsonString(out, bug.name); + out << ",\"verdict\":"; + AppendJsonString(out, bug.verdict == MobileGL::MG_Util::SelfTest::DriverBugVerdict::Fixed + ? "FIXED" + : "UNFIXABLE"); + out << ",\"detail\":"; + AppendJsonString(out, bug.detail); + out << '}'; + } + out << ']'; if (report.formatCapabilities.has_value()) { AppendFormatCapabilitiesJson(out, report.formatCapabilities.value()); } diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java index 0e69f4cb..010c2cd6 100644 --- a/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java @@ -385,9 +385,54 @@ public final class PostActivity extends Activity { } } + renderKnownDriverBugs(backend.optJSONArray("knownDriverBugs")); renderFormatCapabilities(backend.optJSONObject("formatCapabilities")); } + /** + * The "Known Driver Bugs" section: core functionality this driver advertises, accepts, + * and then does not perform. Separate from the capability checks above because it answers + * a different question and uses its own vocabulary. + * + * Only bugs the device actually HAS are reported, so a clean driver renders no section at + * all rather than a list of reassurances - which is why the verdicts are FIXED (a MobileGL + * quirk makes application behaviour correct anyway) and UNFIXABLE (no substitute; the + * one-liner says what MobileGL does defensively), never PASS/FAIL. + */ + private void renderKnownDriverBugs(JSONArray bugs) { + if (bugs == null || bugs.length() == 0) { + return; + } + addText("Known driver bugs", 14, COLOR_TEXT, true, dp(16)); + LinearLayout table = new LinearLayout(this); + table.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams tableParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + tableParams.topMargin = dp(6); + contentLayout.addView(table, tableParams); + + int rowIndex = 0; + for (int i = 0; i < bugs.length(); ++i) { + JSONObject bug = bugs.optJSONObject(i); + if (bug == null) { + continue; + } + // addCheckRow renders name + chip + collapsible detail, which is exactly this + // section's shape; the chip text is the verdict rather than a status. + JSONObject row = new JSONObject(); + try { + row.put("name", bug.optString("name", "unnamed bug")); + row.put("status", bug.optString("verdict", "UNFIXABLE")); + row.put("detail", bug.optString("detail", "")); + } catch (JSONException ignored) { + continue; + } + addCheckRow(table, row, rowIndex++); + } + } + /** The MOBILEGL_BACKEND_TYPE value a POST section name stands for, or null. */ private static String backendTypeForSection(String sectionName) { switch (sectionName.toLowerCase(Locale.ROOT)) { @@ -731,6 +776,13 @@ public final class PostActivity extends Activity { return COLOR_FAIL; case "INFO": return COLOR_INFO; + // The "Known driver bugs" section's own vocabulary. Every row there is a defect + // this device HAS, so neither verdict is reassuring: FIXED means MobileGL papers + // over it and applications still behave correctly, UNFIXABLE means they do not. + case "FIXED": + return COLOR_WARN; + case "UNFIXABLE": + return COLOR_FAIL; default: return COLOR_TEXT; }