mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (Espryt): probe empty image units and keep invalid units empty on drivers that read them as zero
This commit is contained in:
@@ -214,6 +214,15 @@ namespace MobileGL::MG_Config {
|
||||
// right everywhere. ForceOn exercises the atomic reads on a healthy driver; ForceOff is
|
||||
// the negative control.
|
||||
QuirkOverride EsprytAtomicSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER: DirectGLES binds an image unit GL 4.6 8.26
|
||||
// calls invalid (a level the texture does not have) to a zeroed 1x1 texture of the bind's
|
||||
// shape instead of leaving it empty. The Adreno ES driver answers a load through an EMPTY
|
||||
// unit with alpha 1 where GL says 0; the driver POST measures that and arms this by
|
||||
// itself, so Auto is right everywhere. The placeholder's one texel is real, so a store
|
||||
// through such a unit can be read back through the same unit in the same pass - which is
|
||||
// why a healthy driver keeps the empty unit. ForceOn exercises the placeholder on a
|
||||
// healthy driver; ForceOff is the negative control.
|
||||
QuirkOverride EsprytInvalidImagePlaceholder = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
|
||||
@@ -400,6 +400,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.EsprytDeferGeometryEmits = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS");
|
||||
features.EsprytAtomicSubgroupScratch = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH");
|
||||
features.EsprytInvalidImagePlaceholder = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
|
||||
@@ -2870,8 +2870,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
|
||||
// What such a unit is bound to instead. An EMPTY ES unit is not enough: ES 3.2 8.22 leaves
|
||||
// an invalid load's alpha undefined, and Adreno answers 1 where GL says 0. A texel outside
|
||||
// What such a unit is bound to instead, on a driver whose EMPTY unit is not enough
|
||||
// (GLESCapabilities::EmptyImageUnitsReadNonzero): ES 3.2 8.22 leaves an invalid load's
|
||||
// alpha undefined, and Adreno answers 1 where GL says 0. Elsewhere the unit stays empty,
|
||||
// which is exact. A texel outside
|
||||
// a level is invalid too, and that Adreno answers (0, 0, 0, 0) with the store dropped - so
|
||||
// the unit gets a 1x1 texture of the shape and format the bind names, cleared to zero each
|
||||
// time it is bound. Only texel (0, 0[, 0]) of it exists; it is private to the unit, so a
|
||||
@@ -2988,6 +2990,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
if (ImageViewNamesNoTexel(view)) {
|
||||
if (!g_GLESCapabilities.EmptyImageUnitsReadNonzero) {
|
||||
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
return;
|
||||
}
|
||||
// The format the shader was widened from is the one the application bound, so
|
||||
// the placeholder takes that format's carrier, as the storage would have.
|
||||
GLenum format = static_cast<GLenum>(view.InternalFormat);
|
||||
|
||||
@@ -2125,6 +2125,23 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
? "true (the subgroup emulation reads its scratch through atomic loads)"
|
||||
: "false");
|
||||
|
||||
switch (MG_Config::Features.EsprytInvalidImagePlaceholder) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
caps.EmptyImageUnitsReadNonzero = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
caps.EmptyImageUnitsReadNonzero = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
default:
|
||||
caps.EmptyImageUnitsReadNonzero = SelfTest::EmptyImageUnitsReadNonzero(glesFuncs);
|
||||
break;
|
||||
}
|
||||
MGLOG_I(" Empty image units read nonzero: %s",
|
||||
caps.EmptyImageUnitsReadNonzero
|
||||
? "true (an invalid image unit is bound to a zeroed placeholder)"
|
||||
: "false");
|
||||
|
||||
caps.IsAngleRenderer = caps.GLESRendererString.find("ANGLE") != String::npos;
|
||||
caps.IsAngleLlvmpipeRenderer =
|
||||
caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos;
|
||||
|
||||
@@ -1329,6 +1329,11 @@ namespace MobileGL {
|
||||
// Adreno). The subgroup emulation then reads its scratch through atomic loads.
|
||||
// Defaults FALSE: never assumed broken on an unmeasured driver.
|
||||
Bool SharedMemoryLoadsGoStale = false;
|
||||
// True when a load through an EMPTY image unit returns a nonzero texel
|
||||
// (SelfTest::EmptyImageUnitsReadNonzero - measured on Adreno: alpha 1). DirectGLES
|
||||
// then binds an invalid unit to a zeroed placeholder instead of leaving it empty.
|
||||
// Defaults FALSE: never assumed broken on an unmeasured driver.
|
||||
Bool EmptyImageUnitsReadNonzero = false;
|
||||
Int UniformBufferOffsetAlignment = 256;
|
||||
// Its storage-buffer counterpart, queried separately because it is a separate limit:
|
||||
// Adreno 830 answers 32 for GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and 64 for
|
||||
|
||||
@@ -530,6 +530,104 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
return stale;
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr const char* kEmptyImageProbeName = "empty image unit load";
|
||||
constexpr GLuint kEmptyImageSentinel = 0xDEADBEEFu;
|
||||
|
||||
// Loads texel (0, 0) of the rgba8 image on kProbeImageUnit and writes it packed.
|
||||
String EmptyImageComputeSource() {
|
||||
return "#version 310 es\n"
|
||||
"layout(local_size_x = 1) in;\n"
|
||||
"layout(rgba8, binding = " + std::to_string(kProbeImageUnit) +
|
||||
") readonly uniform highp image2D u_image;\n"
|
||||
"layout(std430, binding = 0) buffer Result { highp uint data[4]; } g_result;\n"
|
||||
"void main() { g_result.data[0] = packUnorm4x8(imageLoad(u_image, ivec2(0))); }\n";
|
||||
}
|
||||
|
||||
// Runs the program with `texture` (0 = empty) on the probe unit; nullopt when the
|
||||
// dispatch failed or the shader never wrote its word.
|
||||
Optional<GLuint> RunEmptyImageVariant(const GLESFunctionsTable& gl, GLuint program, GLuint buffer,
|
||||
GLuint texture) {
|
||||
const GLuint init[4] = {kEmptyImageSentinel, 0u, 0u, 0u};
|
||||
gl.glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
gl.glBufferData(GL_SHADER_STORAGE_BUFFER, kProbeSize, init, GL_DYNAMIC_DRAW);
|
||||
gl.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffer);
|
||||
gl.glBindImageTexture(kProbeImageUnit, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
gl.glUseProgram(program);
|
||||
Drain(gl);
|
||||
gl.glDispatchCompute(1, 1, 1);
|
||||
if (gl.glGetError() != GL_NO_ERROR) return std::nullopt;
|
||||
gl.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
gl.glFinish();
|
||||
const GLuint value = ReadFirstWord(gl, buffer);
|
||||
if (value == kEmptyImageSentinel) return std::nullopt;
|
||||
return value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ProbeEmptyImageUnitsReadNonzero(const GLESFunctionsTable& gl) {
|
||||
if (!HasEveryEntryPoint(gl) || gl.glDispatchCompute == nullptr || gl.glBindImageTexture == nullptr ||
|
||||
gl.glTexStorage2D == nullptr || gl.glTexSubImage2D == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SavedState saved;
|
||||
Save(gl, saved);
|
||||
Drain(gl);
|
||||
|
||||
const String source = EmptyImageComputeSource();
|
||||
const GLuint shader = CompileStage(gl, GL_COMPUTE_SHADER, source.c_str(), "compute", kEmptyImageProbeName);
|
||||
GLuint program = 0;
|
||||
GLuint buffer = 0;
|
||||
GLuint texture = 0;
|
||||
Optional<GLuint> control;
|
||||
Optional<GLuint> empty;
|
||||
if (shader != 0 && (program = gl.glCreateProgram()) != 0) {
|
||||
gl.glAttachShader(program, shader);
|
||||
gl.glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
gl.glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_FALSE) {
|
||||
gl.glGenBuffers(1, &buffer);
|
||||
gl.glGenTextures(1, &texture);
|
||||
gl.glActiveTexture(GL_TEXTURE0);
|
||||
gl.glBindTexture(GL_TEXTURE_2D, texture);
|
||||
gl.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
|
||||
gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
const GLubyte zero[4] = {0, 0, 0, 0};
|
||||
gl.glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, zero);
|
||||
if (gl.glGetError() == GL_NO_ERROR) {
|
||||
control = RunEmptyImageVariant(gl, program, buffer, texture);
|
||||
empty = RunEmptyImageVariant(gl, program, buffer, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texture != 0) gl.glDeleteTextures(1, &texture);
|
||||
if (buffer != 0) gl.glDeleteBuffers(1, &buffer);
|
||||
if (program != 0) gl.glDeleteProgram(program);
|
||||
if (shader != 0) gl.glDeleteShader(shader);
|
||||
Restore(gl, saved);
|
||||
|
||||
if (!control || !empty) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (a variant did not build or run)", kEmptyImageProbeName);
|
||||
return false;
|
||||
}
|
||||
if (*control != 0u) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict: the zeroed control read 0x%08x", kEmptyImageProbeName,
|
||||
*control);
|
||||
return false;
|
||||
}
|
||||
const Bool nonzero = *empty != 0u;
|
||||
MGLOG_I("[driver-bug] %s probe: control reads 0x%08x, empty unit reads 0x%08x (expected 0)%s",
|
||||
kEmptyImageProbeName, *control, *empty, nonzero ? " - EMPTY IMAGE UNITS READ NONZERO" : "");
|
||||
return nonzero;
|
||||
}
|
||||
|
||||
Bool EmptyImageUnitsReadNonzero(const GLESFunctionsTable& gl) {
|
||||
static const Bool nonzero = ProbeEmptyImageUnitsReadNonzero(gl);
|
||||
return nonzero;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ===================== SHARED PROGRAM BUILDING =====================
|
||||
// The four probes below all decide their verdict from whether a program LINKS or from
|
||||
@@ -2421,6 +2519,22 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
Move(detail)};
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> ProbeEmptyImageUnitBug(const GLESFunctionsTable& gl) {
|
||||
if (!EmptyImageUnitsReadNonzero(gl)) return std::nullopt;
|
||||
String detail = "a load through an image unit with no texture bound returns alpha 1 where GL 4.6 8.26 "
|
||||
"says every channel is 0. ";
|
||||
if (MG_Config::Features.EsprytInvalidImagePlaceholder == MG_Config::QuirkOverride::ForceOff) {
|
||||
detail += "THE REPAIR IS DISABLED in this process (MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER), so "
|
||||
"a load through an invalid image unit reads alpha 1";
|
||||
return DriverBugFinding{"Empty image units read nonzero", DriverBugVerdict::Unfixable, Move(detail)};
|
||||
}
|
||||
detail += "DirectGLES binds an image unit whose texture lacks the bound level to a zeroed 1x1 texture "
|
||||
"of the bind's shape, so its loads read 0 (a store through that unit can still be read back "
|
||||
"through the same unit at texel 0 within one pass); a unit the application left empty stays "
|
||||
"empty; override with MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER";
|
||||
return DriverBugFinding{"Empty image units read nonzero", DriverBugVerdict::Fixed, Move(detail)};
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> ProbeR32FMultisampleSwizzleBug(const GLESFunctionsTable& gl) {
|
||||
if (!R32FMultisampleSwizzleCorrupted(gl)) return std::nullopt;
|
||||
return DriverBugFinding{
|
||||
@@ -2594,6 +2708,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
|
||||
&ProbeGeometryWriteAfterEmitBug,
|
||||
&ProbeStaleSharedMemoryLoadBug,
|
||||
&ProbeEmptyImageUnitBug,
|
||||
&ProbeR32FMultisampleSwizzleBug,
|
||||
&ProbeImageLocationPerNameBug,
|
||||
&ProbeCrossStageImageQualifierMergeBug,
|
||||
|
||||
@@ -198,6 +198,17 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
// ProbeSharedMemoryLoadsGoStale(), evaluated at most once per process.
|
||||
Bool SharedMemoryLoadsGoStale(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// A compute shader loads texel (0, 0) of an rgba8 image unit twice: once with a zeroed 1x1
|
||||
// texture bound (the control, which must read 0) and once with the unit EMPTY (the subject).
|
||||
// GL 4.6 8.26 makes the empty load return zero; Adreno returns alpha 1 (0xff000000). True
|
||||
// only when the subject reads nonzero while the control reads zero. Deterministic, so
|
||||
// latched; false when compute shaders or an entry point are missing or anything fails to
|
||||
// set up. Restores the GL state it touches.
|
||||
Bool ProbeEmptyImageUnitsReadNonzero(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeEmptyImageUnitsReadNonzero(), evaluated at most once per process.
|
||||
Bool EmptyImageUnitsReadNonzero(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Samples one R32F GL_TEXTURE_2D_MULTISAMPLE texel through a swizzled alpha channel, twice,
|
||||
// with a separately linked program each time. Returns true only when the swizzled read goes
|
||||
// wrong while every control read stays right.
|
||||
|
||||
Reference in New Issue
Block a user