[Feature, Test] (SelfTest): add a Known Driver Bugs POST section and probe the geometry write-after-emit drop

This commit is contained in:
2026-08-22 07:49:17 -04:00
parent 1ebe9d11c5
commit a5f36c8f8d
9 changed files with 568 additions and 0 deletions
+1
View File
@@ -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
+16
View File
@@ -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)
@@ -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 <gtest/gtest.h>
#include <MG_Util/SelfTest/DriverBugProbes.h>
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);
}
}
@@ -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 <MG_Util/Debug/Log.h>
#include <cstring>
#include <optional>
#include <string>
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<GLsizei>(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<GLuint>(state.indexedStorageBuffer[i]));
}
gl.glBindBuffer(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(state.storageBuffer));
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(state.renderbuffer));
gl.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(state.drawFramebuffer));
gl.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(state.readFramebuffer));
gl.glBindVertexArray(static_cast<GLuint>(state.vertexArray));
gl.glUseProgram(static_cast<GLuint>(state.program));
gl.glViewport(state.viewport[0], state.viewport[1], state.viewport[2], state.viewport[3]);
if (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<DriverBugFinding> ProbeGeometryWriteAfterEmitBug(const GLESFunctionsTable& gl) {
if (!GeometryStageSsboWriteAfterEmitDropped(gl)) return std::nullopt;
return DriverBugFinding{
"Geometry-stage storage writes after EmitVertex",
DriverBugVerdict::Unfixable,
"the driver silently discards shader storage buffer writes a geometry shader "
"issues after its last EmitVertex()/EndPrimitive(); the identical write issued "
"BEFORE the emit lands, for both point and triangle geometry shaders. "
"GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS is therefore NOT withdrawn - doing so "
"would break the shaders that write before emitting, which work correctly. "
"A shader that must write after emitting has no substitute on this driver"};
}
// The table. One row per known driver bug; see the header for how to add a sibling.
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
&ProbeGeometryWriteAfterEmitBug,
};
} // namespace
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const GLESFunctionsTable& gl) {
Vector<DriverBugFinding> findings;
for (const DriverBugProbeFn probe : kGlesDriverBugProbes) {
if (Optional<DriverBugFinding> finding = probe(gl)) {
findings.push_back(Move(*finding));
}
}
return findings;
}
} // namespace MobileGL::MG_Util::SelfTest
@@ -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 <Includes.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
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<DriverBugFinding> 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<DriverBugFinding> CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);
} // namespace MobileGL::MG_Util::SelfTest
+5
View File
@@ -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);
}
+13
View File
@@ -7,6 +7,8 @@
// End of Source File Header
#pragma once
#include "DriverBugProbes.h"
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
@@ -34,6 +36,17 @@ namespace MobileGL::MG_Util::SelfTest {
String verdict = "UNSUPPORTED"; // "OK" | "DEGRADED" | "UNSUPPORTED"
String rendererInfo;
Vector<PostCheck> 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<DriverBugFinding> knownDriverBugs;
Optional<MG_Backend::FormatCapabilityCache> formatCapabilities;
};
@@ -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());
}
@@ -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;
}