mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Feature, Test] (SelfTest): add a Known Driver Bugs POST section and probe the geometry write-after-emit drop
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user