mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
323 lines
16 KiB
C++
323 lines
16 KiB
C++
// MobileGL - MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
|
// Copyright (c) 2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#include "DriverBugProbes.h"
|
|
|
|
#include <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
|