mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
666f150202 | ||
|
|
2f62970dd5 | ||
|
|
dcb568d445 | ||
|
|
a5f36c8f8d | ||
|
|
1ebe9d11c5 | ||
|
|
a8bb63950d |
@@ -310,6 +310,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/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/DriverPost.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||||
|
|
||||||
|
|||||||
@@ -434,8 +434,28 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
|
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
|
||||||
const Bool mipmapped =
|
// A multisample texture is fetched, never filtered. GL 4.6 core 8.17 gives it exactly
|
||||||
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
|
// one level and says its sampler state is not used at all - texelFetch is the only way
|
||||||
|
// a shader can read it - so 8.14's filter-completeness rules, which is what the
|
||||||
|
// `mipmapped` branch below asks about, never apply to it.
|
||||||
|
//
|
||||||
|
// Deriving `mipmapped` from that unused sampler is what made EVERY multisample texture
|
||||||
|
// look incomplete: MIN_FILTER's initial value is NEAREST_MIPMAP_LINEAR, and a texture
|
||||||
|
// that can only ever have one level never satisfies the mip-chain check. Both backends
|
||||||
|
// treat "samples as incomplete" as "do not bind it" (DirectGLES's per-unit walk in
|
||||||
|
// ResolveAndBindUnitTextures, DirectVulkan's UniformManager), so the sampler2DMS the
|
||||||
|
// shader declared was left pointing at nothing and every texelFetch read zero. That is
|
||||||
|
// the sampler2DMS/sampler2DMSArray half of KHR-GL43.compute_shader.resource-texture,
|
||||||
|
// which fails at the first data7 element with the multisample texture correctly
|
||||||
|
// cleared and simply never bound.
|
||||||
|
//
|
||||||
|
// IsCopyImageEndpointComplete already spells the same guard as
|
||||||
|
// CopyImageTargetHasMipmapChain; this was the one place that asked without it.
|
||||||
|
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||||
|
const Bool filtered = target != TextureTarget::Texture2DMultisample &&
|
||||||
|
target != TextureTarget::Texture2DMultisampleArray;
|
||||||
|
const Bool mipmapped = filtered && effectiveSampler != nullptr &&
|
||||||
|
effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
|
||||||
return !IsMipmapCompleteForFilter(texture, mipmapped);
|
return !IsMipmapCompleteForFilter(texture, mipmapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -263,6 +263,63 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
|
|||||||
extensions.end());
|
extensions.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A multisample texture is fetched, never filtered, so the mip-chain completeness rules never
|
||||||
|
// apply to it (GL 4.6 core 8.17). It has exactly one level and MIN_FILTER's initial value is
|
||||||
|
// NEAREST_MIPMAP_LINEAR, so asking those rules anyway calls EVERY multisample texture incomplete
|
||||||
|
// - and both backends express "incomplete" as "leave the native target unbound", which makes the
|
||||||
|
// shader's sampler2DMS read zero from a texture that was written correctly.
|
||||||
|
//
|
||||||
|
// That is KHR-GL43.compute_shader.resource-texture: it clears its 2DMS texture to 123.0 through
|
||||||
|
// an FBO (which succeeds - the ES clear is issued on a COMPLETE 4-sample framebuffer with no
|
||||||
|
// error) and then fails at the first sampler2DMS element because the texture was never bound.
|
||||||
|
TEST(DirectGLESSanity, BindsAMultisampleTextureDespiteTheDefaultMipmapFilter) {
|
||||||
|
using namespace MobileGL;
|
||||||
|
namespace DirectGLES = MG_Backend::DirectGLES;
|
||||||
|
|
||||||
|
ScopedDirectGLESTextureBindings state;
|
||||||
|
|
||||||
|
GLuint frontendTexture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &frontendTexture);
|
||||||
|
ASSERT_NE(frontendTexture, 0u);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, frontendTexture);
|
||||||
|
const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(0)
|
||||||
|
.GetBindingSlot(TextureTarget::Texture2DMultisample)
|
||||||
|
.GetBoundObject();
|
||||||
|
ASSERT_NE(textureObject, nullptr);
|
||||||
|
|
||||||
|
textureObject->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||||
|
textureObject->SetSamples(4);
|
||||||
|
textureObject->SetFixedSampleLocations(false);
|
||||||
|
// One level, 4x4 - the shape glTexImage2DMultisample produces, and a size whose mip chain
|
||||||
|
// would need three levels if the filter rules were (wrongly) applied.
|
||||||
|
MG_State::GLState::AsMipmapTexture(textureObject.get())
|
||||||
|
->AllocateStorage(TextureUploadTarget::Texture2DMultisample, 0, {{4, 4, 1}, 4});
|
||||||
|
|
||||||
|
// The precondition that used to poison it, asserted rather than assumed: the texture's own
|
||||||
|
// sampler still reports a mipmapping filter, because GL's initial MIN_FILTER is
|
||||||
|
// NEAREST_MIPMAP_LINEAR and a multisample texture has no way (and no reason) to change it.
|
||||||
|
// If a future default made this None the test would pass without covering anything.
|
||||||
|
const auto& sampler = textureObject->GetSamplerObject();
|
||||||
|
ASSERT_NE(sampler, nullptr);
|
||||||
|
ASSERT_NE(sampler->GetMipmapMode(), SamplerMipmapMode::None)
|
||||||
|
<< "fixture is stale: the default sampler no longer asks for mipmapping, so this test "
|
||||||
|
"would not exercise the multisample guard";
|
||||||
|
|
||||||
|
EXPECT_FALSE(MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(), sampler.get()))
|
||||||
|
<< "a multisample texture is never filter-incomplete";
|
||||||
|
|
||||||
|
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(textureObject);
|
||||||
|
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
|
||||||
|
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
|
||||||
|
|
||||||
|
// The symptom itself: the per-unit walk has to actually bind it.
|
||||||
|
DirectGLES::BindCurrentTextures();
|
||||||
|
ASSERT_EQ(state.bindCalls.size(), 1u)
|
||||||
|
<< "the multisample texture was not bound; every texelFetch against it reads zero";
|
||||||
|
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D_MULTISAMPLE);
|
||||||
|
EXPECT_EQ(state.bindCalls[0].texture, backendTextureId);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
namespace DirectGLES = MG_Backend::DirectGLES;
|
namespace DirectGLES = MG_Backend::DirectGLES;
|
||||||
|
|||||||
@@ -15,5 +15,21 @@ target_link_libraries(DriverPostIterationRPWitnessTest PRIVATE
|
|||||||
${LINK_LIBRARIES}
|
${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)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(DriverPostIterationRPWitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
gtest_discover_tests(DriverBugProbesTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -0,0 +1,660 @@
|
|||||||
|
// 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>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using MobileGL::MG_Util::SelfTest::CollectGlesKnownDriverBugs;
|
||||||
|
using MobileGL::MG_Util::SelfTest::DriverBugVerdict;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ProbeCrossStageImageQualifierMergeDropsWrites;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
||||||
|
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
||||||
|
|
||||||
|
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{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== THE FAKE DRIVER =====================
|
||||||
|
//
|
||||||
|
// Same idea as the fake GLES table BackendLoaderTest drives the gl_InstanceID probe with:
|
||||||
|
// captureless lambdas over one file-scope state, with per-test knobs that turn each defect
|
||||||
|
// on and off. It is deliberately a MODEL of the defect rather than a canned answer - the
|
||||||
|
// fake reads the shader text the probe actually submitted and reproduces what the affected
|
||||||
|
// driver does with it, so a probe that stopped building the triggering shape would stop
|
||||||
|
// detecting, which is exactly what these tests are for.
|
||||||
|
//
|
||||||
|
// These tests call the Probe* functions directly rather than through
|
||||||
|
// CollectGlesKnownDriverBugs(): the collector goes through the once-per-process memos, and a
|
||||||
|
// memo latched by one test would decide the answer for every later one.
|
||||||
|
|
||||||
|
// The exact text an affected Adreno driver puts in the info log for this refusal.
|
||||||
|
const char* const kImageLocationLinkLog =
|
||||||
|
"Error: Image Image location or component exceeds max allowed.\nError: Linking failed.";
|
||||||
|
|
||||||
|
struct FakeDriver {
|
||||||
|
// ---- limits the probes gate on -------------------------------------
|
||||||
|
GLint maxColorTextureSamples = 4;
|
||||||
|
GLint maxImageUnits = 8;
|
||||||
|
GLint maxVertexImageUniforms = 8;
|
||||||
|
GLint maxFragmentImageUniforms = 8;
|
||||||
|
GLint maxGeometryImageUniforms = 3;
|
||||||
|
// The landed geometry probe reads this; zero keeps it inert so it cannot interfere.
|
||||||
|
GLint maxGeometrySsboBlocks = 0;
|
||||||
|
bool geometryImageLimitQueryRaisesError = false;
|
||||||
|
bool colorTextureSamplesQueryRaisesError = false;
|
||||||
|
|
||||||
|
// ---- defect knobs ---------------------------------------------------
|
||||||
|
// Probe 1: a swizzled-alpha, non-zero-sample .w fetch reads garbage from the second
|
||||||
|
// sampling program onward.
|
||||||
|
bool msaaSwizzledAlphaCorrupted = false;
|
||||||
|
// Probe 1's inconclusive path: EVERY sampled read is wrong, including the controls.
|
||||||
|
bool msaaEveryReadWrong = false;
|
||||||
|
// Probe 2: the link fails once the program declares more distinct image uniform NAMES
|
||||||
|
// than this.
|
||||||
|
int distinctImageNameBudget = 1000;
|
||||||
|
// Probe 3: a same-name coherent writeonly/readonly pair loses the writing stage's store.
|
||||||
|
bool sameNameImagePairDropsWrites = false;
|
||||||
|
// Probe 3's inconclusive path: the renamed control loses it too.
|
||||||
|
bool everyVertexImageWriteDropped = false;
|
||||||
|
// Probe 4: how many texels the in-invocation dependent read misses under the STRONGEST
|
||||||
|
// shape, how many it misses under the shape MobileGL emits today, and whether the
|
||||||
|
// two-draw control misses them too.
|
||||||
|
int coherencyStrongestShapeFailedTexels = 0;
|
||||||
|
int coherencyEmittedShapeFailedTexels = 0;
|
||||||
|
int coherencyControlFailedTexels = 0;
|
||||||
|
|
||||||
|
// ---- object bookkeeping ---------------------------------------------
|
||||||
|
GLenum pendingError = GL_NO_ERROR;
|
||||||
|
GLuint nextShaderId = 1;
|
||||||
|
GLuint nextProgramId = 1;
|
||||||
|
GLuint nextTextureId = 1;
|
||||||
|
GLuint nextFramebufferId = 1;
|
||||||
|
GLuint nextVertexArrayId = 1;
|
||||||
|
int aliveShaders = 0;
|
||||||
|
int alivePrograms = 0;
|
||||||
|
int aliveTextures = 0;
|
||||||
|
int aliveFramebuffers = 0;
|
||||||
|
int aliveVertexArrays = 0;
|
||||||
|
|
||||||
|
std::map<GLuint, std::string> shaderSources;
|
||||||
|
std::map<GLuint, std::vector<GLuint>> programShaders;
|
||||||
|
std::map<GLuint, bool> programLinked;
|
||||||
|
std::map<GLuint, std::string> programInfoLogs;
|
||||||
|
// texture id -> GL_TEXTURE_SWIZZLE_A
|
||||||
|
std::map<GLuint, GLenum> multisampleAlphaSwizzle;
|
||||||
|
|
||||||
|
GLuint boundMultisampleTexture = 0;
|
||||||
|
GLuint currentProgram = 0;
|
||||||
|
// How many programs that sample a multisample texture have been linked so far. The
|
||||||
|
// corruption starts at the second.
|
||||||
|
int sampledMultisampleProgramCount = 0;
|
||||||
|
// Set by glDrawArrays, consumed by glReadPixels.
|
||||||
|
GLfloat lastSampledValue = 1.0f;
|
||||||
|
int lastFailedTexelCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
FakeDriver g_fake;
|
||||||
|
|
||||||
|
void ResetFakeDriver() { g_fake = FakeDriver{}; }
|
||||||
|
|
||||||
|
const std::string& SourceOf(GLuint shader) {
|
||||||
|
static const std::string empty;
|
||||||
|
const auto it = g_fake.shaderSources.find(shader);
|
||||||
|
return it == g_fake.shaderSources.end() ? empty : it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Contains(const std::string& haystack, const char* needle) {
|
||||||
|
return haystack.find(needle) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every `image2D <name>` the program declares, across all its stages.
|
||||||
|
std::vector<std::string> DeclaredImageNames(GLuint program) {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
const auto attached = g_fake.programShaders.find(program);
|
||||||
|
if (attached == g_fake.programShaders.end()) return names;
|
||||||
|
for (const GLuint shader : attached->second) {
|
||||||
|
const std::string& source = SourceOf(shader);
|
||||||
|
std::size_t at = 0;
|
||||||
|
while ((at = source.find("image2D ", at)) != std::string::npos) {
|
||||||
|
at += std::strlen("image2D ");
|
||||||
|
const std::size_t end = source.find_first_of(";,)", at);
|
||||||
|
if (end == std::string::npos) break;
|
||||||
|
std::string name = source.substr(at, end - at);
|
||||||
|
while (!name.empty() && (name.back() == ' ' || name.back() == '\t')) name.pop_back();
|
||||||
|
if (std::find(names.begin(), names.end(), name) == names.end()) {
|
||||||
|
names.push_back(name);
|
||||||
|
}
|
||||||
|
at = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string StageSourceContaining(GLuint program, const char* needle) {
|
||||||
|
const auto attached = g_fake.programShaders.find(program);
|
||||||
|
if (attached == g_fake.programShaders.end()) return {};
|
||||||
|
for (const GLuint shader : attached->second) {
|
||||||
|
const std::string& source = SourceOf(shader);
|
||||||
|
if (Contains(source, needle)) return source;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The uniform name in `... image2D <name>;` of the first declaration in `source`.
|
||||||
|
std::string FirstImageNameIn(const std::string& source) {
|
||||||
|
const std::size_t at = source.find("image2D ");
|
||||||
|
if (at == std::string::npos) return {};
|
||||||
|
const std::size_t start = at + std::strlen("image2D ");
|
||||||
|
const std::size_t end = source.find(';', start);
|
||||||
|
if (end == std::string::npos) return {};
|
||||||
|
return source.substr(start, end - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whatever the sampling vertex shader asked for: `texelFetch(mg_probeSampler, ivec2(0), N).C`.
|
||||||
|
void ParseSampledFetch(const std::string& source, int& sampleIndex, char& component) {
|
||||||
|
sampleIndex = -1;
|
||||||
|
component = '?';
|
||||||
|
const std::size_t at = source.find("texelFetch(mg_probeSampler, ivec2(0), ");
|
||||||
|
if (at == std::string::npos) return;
|
||||||
|
const std::size_t start = at + std::strlen("texelFetch(mg_probeSampler, ivec2(0), ");
|
||||||
|
sampleIndex = std::atoi(source.c_str() + start);
|
||||||
|
const std::size_t dot = source.find(").", start);
|
||||||
|
if (dot != std::string::npos && dot + 2 < source.size()) component = source[dot + 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_External::GLESFunctionsTable MakeFakeGLESFunctions() {
|
||||||
|
MG_External::GLESFunctionsTable funcs{};
|
||||||
|
|
||||||
|
funcs.glGetError = []() -> GLenum {
|
||||||
|
const GLenum error = g_fake.pendingError;
|
||||||
|
g_fake.pendingError = GL_NO_ERROR;
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
|
||||||
|
switch (pname) {
|
||||||
|
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||||
|
if (g_fake.colorTextureSamplesQueryRaisesError) {
|
||||||
|
g_fake.pendingError = GL_INVALID_ENUM;
|
||||||
|
} else {
|
||||||
|
*data = g_fake.maxColorTextureSamples;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GL_MAX_IMAGE_UNITS:
|
||||||
|
*data = g_fake.maxImageUnits;
|
||||||
|
break;
|
||||||
|
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||||
|
*data = g_fake.maxVertexImageUniforms;
|
||||||
|
break;
|
||||||
|
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
|
||||||
|
*data = g_fake.maxFragmentImageUniforms;
|
||||||
|
break;
|
||||||
|
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
|
||||||
|
if (g_fake.geometryImageLimitQueryRaisesError) {
|
||||||
|
g_fake.pendingError = GL_INVALID_ENUM;
|
||||||
|
} else {
|
||||||
|
*data = g_fake.maxGeometryImageUniforms;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||||
|
*data = g_fake.maxGeometrySsboBlocks;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glGetIntegeri_v = [](GLenum, GLuint, GLint* data) { *data = 0; };
|
||||||
|
funcs.glGetFloatv = [](GLenum, GLfloat* data) {
|
||||||
|
data[0] = 0.0f;
|
||||||
|
data[1] = 0.0f;
|
||||||
|
data[2] = 0.0f;
|
||||||
|
data[3] = 0.0f;
|
||||||
|
};
|
||||||
|
funcs.glIsEnabled = [](GLenum) -> GLboolean { return GL_FALSE; };
|
||||||
|
funcs.glEnable = [](GLenum) {};
|
||||||
|
funcs.glDisable = [](GLenum) {};
|
||||||
|
funcs.glFinish = []() {};
|
||||||
|
funcs.glMemoryBarrier = [](GLbitfield) {};
|
||||||
|
funcs.glPixelStorei = [](GLenum, GLint) {};
|
||||||
|
funcs.glViewport = [](GLint, GLint, GLsizei, GLsizei) {};
|
||||||
|
funcs.glClear = [](GLbitfield) {};
|
||||||
|
funcs.glClearColor = [](GLfloat, GLfloat, GLfloat, GLfloat) {};
|
||||||
|
funcs.glActiveTexture = [](GLenum) {};
|
||||||
|
|
||||||
|
// ---- shaders and programs -------------------------------------------
|
||||||
|
funcs.glCreateShader = [](GLenum) -> GLuint {
|
||||||
|
++g_fake.aliveShaders;
|
||||||
|
return g_fake.nextShaderId++;
|
||||||
|
};
|
||||||
|
funcs.glShaderSource = [](GLuint shader, GLsizei count, const GLchar* const* strings,
|
||||||
|
const GLint*) {
|
||||||
|
std::string source;
|
||||||
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
|
if (strings[i] != nullptr) source += strings[i];
|
||||||
|
}
|
||||||
|
g_fake.shaderSources[shader] = std::move(source);
|
||||||
|
};
|
||||||
|
funcs.glCompileShader = [](GLuint) {};
|
||||||
|
funcs.glGetShaderiv = [](GLuint, GLenum pname, GLint* params) {
|
||||||
|
if (pname == GL_COMPILE_STATUS) *params = GL_TRUE;
|
||||||
|
};
|
||||||
|
funcs.glGetShaderInfoLog = [](GLuint, GLsizei bufSize, GLsizei*, GLchar* infoLog) {
|
||||||
|
if (bufSize > 0) infoLog[0] = '\0';
|
||||||
|
};
|
||||||
|
funcs.glDeleteShader = [](GLuint shader) {
|
||||||
|
if (shader != 0) --g_fake.aliveShaders;
|
||||||
|
};
|
||||||
|
funcs.glCreateProgram = []() -> GLuint {
|
||||||
|
++g_fake.alivePrograms;
|
||||||
|
return g_fake.nextProgramId++;
|
||||||
|
};
|
||||||
|
funcs.glAttachShader = [](GLuint program, GLuint shader) {
|
||||||
|
g_fake.programShaders[program].push_back(shader);
|
||||||
|
};
|
||||||
|
funcs.glLinkProgram = [](GLuint program) {
|
||||||
|
const std::vector<std::string> names = DeclaredImageNames(program);
|
||||||
|
const bool overBudget = static_cast<int>(names.size()) > g_fake.distinctImageNameBudget;
|
||||||
|
g_fake.programLinked[program] = !overBudget;
|
||||||
|
g_fake.programInfoLogs[program] = overBudget ? kImageLocationLinkLog : "";
|
||||||
|
if (!overBudget && !StageSourceContaining(program, "texelFetch(mg_probeSampler").empty()) {
|
||||||
|
++g_fake.sampledMultisampleProgramCount;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glGetProgramiv = [](GLuint program, GLenum pname, GLint* params) {
|
||||||
|
if (pname != GL_LINK_STATUS) return;
|
||||||
|
const auto it = g_fake.programLinked.find(program);
|
||||||
|
*params = (it == g_fake.programLinked.end() || it->second) ? GL_TRUE : GL_FALSE;
|
||||||
|
};
|
||||||
|
funcs.glGetProgramInfoLog = [](GLuint program, GLsizei bufSize, GLsizei*, GLchar* infoLog) {
|
||||||
|
if (bufSize <= 0) return;
|
||||||
|
const auto it = g_fake.programInfoLogs.find(program);
|
||||||
|
const std::string& log = it == g_fake.programInfoLogs.end() ? std::string() : it->second;
|
||||||
|
const GLsizei copied = static_cast<GLsizei>(
|
||||||
|
std::min<std::size_t>(log.size(), static_cast<std::size_t>(bufSize - 1)));
|
||||||
|
std::memcpy(infoLog, log.data(), static_cast<std::size_t>(copied));
|
||||||
|
infoLog[copied] = '\0';
|
||||||
|
};
|
||||||
|
funcs.glDeleteProgram = [](GLuint program) {
|
||||||
|
if (program != 0) --g_fake.alivePrograms;
|
||||||
|
};
|
||||||
|
funcs.glUseProgram = [](GLuint program) { g_fake.currentProgram = program; };
|
||||||
|
funcs.glGetUniformLocation = [](GLuint, const GLchar*) -> GLint { return 0; };
|
||||||
|
funcs.glUniform1i = [](GLint, GLint) {};
|
||||||
|
|
||||||
|
// ---- textures, framebuffers, vertex arrays ---------------------------
|
||||||
|
funcs.glGenTextures = [](GLsizei n, GLuint* textures) {
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
textures[i] = g_fake.nextTextureId++;
|
||||||
|
++g_fake.aliveTextures;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
||||||
|
if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = texture;
|
||||||
|
};
|
||||||
|
funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) {
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
if (textures[i] != 0) --g_fake.aliveTextures;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) {
|
||||||
|
if (target == GL_TEXTURE_2D_MULTISAMPLE && pname == GL_TEXTURE_SWIZZLE_A) {
|
||||||
|
g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture] =
|
||||||
|
static_cast<GLenum>(param);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glTexImage2D = [](GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum,
|
||||||
|
const void*) {};
|
||||||
|
funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum,
|
||||||
|
const void*) {};
|
||||||
|
funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
|
||||||
|
funcs.glTexStorage2DMultisample = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei, GLboolean) {};
|
||||||
|
funcs.glGenFramebuffers = [](GLsizei n, GLuint* framebuffers) {
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
framebuffers[i] = g_fake.nextFramebufferId++;
|
||||||
|
++g_fake.aliveFramebuffers;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glBindFramebuffer = [](GLenum, GLuint) {};
|
||||||
|
funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
|
||||||
|
funcs.glCheckFramebufferStatus = [](GLenum) -> GLenum { return GL_FRAMEBUFFER_COMPLETE; };
|
||||||
|
funcs.glDeleteFramebuffers = [](GLsizei n, const GLuint* framebuffers) {
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
arrays[i] = g_fake.nextVertexArrayId++;
|
||||||
|
++g_fake.aliveVertexArrays;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glBindVertexArray = [](GLuint) {};
|
||||||
|
funcs.glDeleteVertexArrays = [](GLsizei n, const GLuint* arrays) {
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
if (arrays[i] != 0) --g_fake.aliveVertexArrays;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
funcs.glBindImageTexture = [](GLuint, GLuint, GLint, GLboolean, GLint, GLenum, GLenum) {};
|
||||||
|
|
||||||
|
// ---- the draw, where the defects live --------------------------------
|
||||||
|
funcs.glDrawArrays = [](GLenum, GLint, GLsizei) {
|
||||||
|
const GLuint program = g_fake.currentProgram;
|
||||||
|
const std::string sampling = StageSourceContaining(program, "texelFetch(mg_probeSampler");
|
||||||
|
if (!sampling.empty()) {
|
||||||
|
int sampleIndex = -1;
|
||||||
|
char component = '?';
|
||||||
|
ParseSampledFetch(sampling, sampleIndex, component);
|
||||||
|
const GLenum swizzle = g_fake.multisampleAlphaSwizzle.count(
|
||||||
|
g_fake.boundMultisampleTexture) != 0
|
||||||
|
? g_fake.multisampleAlphaSwizzle[g_fake.boundMultisampleTexture]
|
||||||
|
: GL_ALPHA;
|
||||||
|
// An R32F texel filled with (1, 0, 0, -) reads 1.0 through both the ALPHA and the
|
||||||
|
// RED swizzle sources, which is why one expected constant covers every shape.
|
||||||
|
g_fake.lastSampledValue = 1.0f;
|
||||||
|
if (g_fake.msaaEveryReadWrong) {
|
||||||
|
g_fake.lastSampledValue = 0.0f;
|
||||||
|
} else if (g_fake.msaaSwizzledAlphaCorrupted && swizzle == GL_RED && component == 'w' &&
|
||||||
|
sampleIndex != 0 && g_fake.sampledMultisampleProgramCount >= 2) {
|
||||||
|
// Uninitialised memory: a value that is neither the answer nor the clear.
|
||||||
|
g_fake.lastSampledValue = -1.34954e-17f;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matched on the access qualifier alone, not on "coherent writeonly": the strongest
|
||||||
|
// coherency shape spells it "coherent volatile writeonly".
|
||||||
|
const std::string writeStage = StageSourceContaining(program, "writeonly");
|
||||||
|
const std::string readStage = StageSourceContaining(program, "readonly");
|
||||||
|
if (!writeStage.empty() && !readStage.empty() && Contains(readStage, "memoryBarrierImage")) {
|
||||||
|
// The coherency probe: one invocation stores and then reads back. `volatile` is
|
||||||
|
// what tells the strongest shape apart from the one MobileGL emits today, and
|
||||||
|
// giving them separate knobs is what lets a test pin the case where only the
|
||||||
|
// emitted shape is wrong - a fixable defect that must not be reported here.
|
||||||
|
g_fake.lastFailedTexelCount = Contains(readStage, "coherent volatile")
|
||||||
|
? g_fake.coherencyStrongestShapeFailedTexels
|
||||||
|
: g_fake.coherencyEmittedShapeFailedTexels;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!writeStage.empty() && readStage.empty()) {
|
||||||
|
// The coherency control's store half; the load half decides the result.
|
||||||
|
g_fake.lastFailedTexelCount = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (writeStage.empty() && !readStage.empty()) {
|
||||||
|
g_fake.lastFailedTexelCount = g_fake.coherencyControlFailedTexels;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!writeStage.empty() && !readStage.empty()) {
|
||||||
|
// The qualifier-merge pair: the stores are lost when the two halves share a name.
|
||||||
|
const bool sharedName =
|
||||||
|
FirstImageNameIn(writeStage) == FirstImageNameIn(readStage) &&
|
||||||
|
!FirstImageNameIn(writeStage).empty();
|
||||||
|
const bool lost = g_fake.everyVertexImageWriteDropped ||
|
||||||
|
(g_fake.sameNameImagePairDropsWrites && sharedName);
|
||||||
|
g_fake.lastFailedTexelCount = lost ? 1 << 20 : 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_fake.lastFailedTexelCount = 0;
|
||||||
|
};
|
||||||
|
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||||
|
void* pixels) {
|
||||||
|
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||||
|
if (format == GL_RED && type == GL_FLOAT) {
|
||||||
|
GLfloat* out = static_cast<GLfloat*>(pixels);
|
||||||
|
for (std::size_t i = 0; i < texels; ++i) out[i] = g_fake.lastSampledValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLubyte* out = static_cast<GLubyte*>(pixels);
|
||||||
|
const std::size_t failed =
|
||||||
|
std::min<std::size_t>(texels, static_cast<std::size_t>(g_fake.lastFailedTexelCount));
|
||||||
|
for (std::size_t i = 0; i < texels; ++i) {
|
||||||
|
const bool ok = i >= failed;
|
||||||
|
out[i * 4 + 0] = ok ? 0 : 255;
|
||||||
|
out[i * 4 + 1] = ok ? 255 : 0;
|
||||||
|
out[i * 4 + 2] = 0;
|
||||||
|
out[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return funcs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectProbeReleasedEverything() {
|
||||||
|
EXPECT_EQ(g_fake.aliveShaders, 0);
|
||||||
|
EXPECT_EQ(g_fake.alivePrograms, 0);
|
||||||
|
EXPECT_EQ(g_fake.aliveTextures, 0);
|
||||||
|
EXPECT_EQ(g_fake.aliveFramebuffers, 0);
|
||||||
|
EXPECT_EQ(g_fake.aliveVertexArrays, 0);
|
||||||
|
}
|
||||||
|
} // 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";
|
||||||
|
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||||
|
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||||
|
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||||
|
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== R32F MULTISAMPLE SWIZZLE =====================
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, R32FMultisampleSwizzleIsCleanOnAConformingDriver) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||||
|
ExpectProbeReleasedEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, R32FMultisampleSwizzleIsDetectedFromTheSecondProgramOnward) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.msaaSwizzledAlphaCorrupted = true;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_TRUE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||||
|
ExpectProbeReleasedEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The control rule, made executable: a driver on which even the default-swizzle, sample-zero and
|
||||||
|
// .x reads are wrong is broken in some larger way, and the probe may not name the alpha swizzle
|
||||||
|
// as the cause.
|
||||||
|
TEST(DriverBugProbes, R32FMultisampleSwizzleReportsNothingWhenTheControlsAreWrongToo) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.msaaSwizzledAlphaCorrupted = true;
|
||||||
|
g_fake.msaaEveryReadWrong = true;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl))
|
||||||
|
<< "with every read wrong the probe has no evidence that the alpha swizzle is the variable";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, R32FMultisampleSwizzleNeedsMoreThanOneSample) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.msaaSwizzledAlphaCorrupted = true;
|
||||||
|
g_fake.maxColorTextureSamples = 1;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeR32FMultisampleSwizzleCorruption(gl));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== IMAGE LOCATION PER NAME =====================
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, ImageLocationBudgetIsCleanWhenNamesDoNotCost) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
const auto measurement = ProbeImageLocationPerNameBudget(gl);
|
||||||
|
EXPECT_FALSE(measurement.detected);
|
||||||
|
ExpectProbeReleasedEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, ImageLocationBudgetIsDetectedWhenOnlyTheSharedNamesLink) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
// Four image uniforms per stage: twelve distinct names in the subject, four in the control.
|
||||||
|
g_fake.distinctImageNameBudget = 5;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
const auto measurement = ProbeImageLocationPerNameBudget(gl);
|
||||||
|
EXPECT_TRUE(measurement.detected);
|
||||||
|
EXPECT_EQ(measurement.perStageImageUniforms, g_fake.maxGeometryImageUniforms + 1);
|
||||||
|
EXPECT_EQ(measurement.subjectDistinctNames, measurement.perStageImageUniforms * 3);
|
||||||
|
EXPECT_EQ(measurement.controlDistinctNames, measurement.perStageImageUniforms);
|
||||||
|
EXPECT_NE(measurement.driverMessage.find("exceeds max allowed"), String::npos)
|
||||||
|
<< "the report quotes the driver rather than paraphrasing it";
|
||||||
|
ExpectProbeReleasedEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The control rule again: when the shared-name program is refused too, the shape is simply too
|
||||||
|
// big for this driver and the refusal is honest.
|
||||||
|
TEST(DriverBugProbes, ImageLocationBudgetReportsNothingWhenTheControlAlsoFails) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.distinctImageNameBudget = 2;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, ImageLocationBudgetNeedsAGeometryStageThatCanHoldImages) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.distinctImageNameBudget = 5;
|
||||||
|
g_fake.maxGeometryImageUniforms = 0;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, ImageLocationBudgetStaysSilentOnAContextWithoutTheGeometryLimit) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.distinctImageNameBudget = 5;
|
||||||
|
g_fake.geometryImageLimitQueryRaisesError = true;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected)
|
||||||
|
<< "a pre-ES-3.2 context has no geometry stage to build the shape out of";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== CROSS-STAGE QUALIFIER MERGE =====================
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, QualifierMergeIsCleanWhenTheDriverKeepsTheStore) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||||
|
ExpectProbeReleasedEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, QualifierMergeIsDetectedWhenOnlyTheSharedNameLosesTheStore) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.sameNameImagePairDropsWrites = true;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_TRUE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||||
|
ExpectProbeReleasedEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A driver that loses the RENAMED store too cannot write images from the vertex stage at all -
|
||||||
|
// a different and much larger claim, which this probe may not make.
|
||||||
|
TEST(DriverBugProbes, QualifierMergeReportsNothingWhenTheRenamedControlAlsoFails) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.sameNameImagePairDropsWrites = true;
|
||||||
|
g_fake.everyVertexImageWriteDropped = true;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, QualifierMergeNeedsVertexStageImageUniforms) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.sameNameImagePairDropsWrites = true;
|
||||||
|
g_fake.maxVertexImageUniforms = 0;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== IMAGE COHERENCY RESIDUAL =====================
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, ImageCoherencyIsCleanWhenTheDependentReadObservesTheStore) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
const auto measurement = ProbeImageWriteReadCoherencyResidual(gl);
|
||||||
|
EXPECT_FALSE(measurement.detected);
|
||||||
|
ExpectProbeReleasedEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, ImageCoherencyResidualIsDetectedAndQuantified) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
||||||
|
g_fake.coherencyEmittedShapeFailedTexels = 418;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
const auto measurement = ProbeImageWriteReadCoherencyResidual(gl);
|
||||||
|
EXPECT_TRUE(measurement.detected);
|
||||||
|
EXPECT_EQ(measurement.mismatchedTexels, 376);
|
||||||
|
EXPECT_EQ(measurement.emittedShapeMismatchedTexels, 418)
|
||||||
|
<< "the row reports what applications get, not only what is theoretically reachable";
|
||||||
|
EXPECT_GT(measurement.totalTexels, 418) << "the report needs a denominator to quote a rate";
|
||||||
|
ExpectProbeReleasedEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The reason the subject is the STRONGEST shape and not the one MobileGL emits. Mesa llvmpipe
|
||||||
|
// misses every texel with `coherent` + memoryBarrierImage() and none once the pair is also
|
||||||
|
// `volatile` - a defect MobileGL could fix by emitting a different shape, which is not what
|
||||||
|
// UNFIXABLE means and does not belong in this section.
|
||||||
|
TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenAStrongerShapeWouldFixIt) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.coherencyStrongestShapeFailedTexels = 0;
|
||||||
|
g_fake.coherencyEmittedShapeFailedTexels = 4096;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected)
|
||||||
|
<< "a driver the volatile shape satisfies has a fixable defect, not an unfixable one";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The control rule once more: a driver whose glFinish-separated two-draw dependency is ALSO
|
||||||
|
// dirty has a bigger defect than an in-invocation ordering residual, and this probe must not
|
||||||
|
// dress that up as one.
|
||||||
|
TEST(DriverBugProbes, ImageCoherencyReportsNothingWhenTheFinishSeparatedControlIsDirtyToo) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
||||||
|
g_fake.coherencyControlFailedTexels = 4096;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.coherencyStrongestShapeFailedTexels = 376;
|
||||||
|
g_fake.maxFragmentImageUniforms = 1;
|
||||||
|
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||||
|
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
@@ -156,6 +157,53 @@ void main() {
|
|||||||
g_out.value[0] = 1u;
|
g_out.value[0] = 1u;
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
// glslang emits constants in FIRST-USE order, so a shader that does not use the flattened
|
||||||
|
// array's length until after it has declared the counter block leaves that constant BELOW the
|
||||||
|
// block. The pass needs the length to build `uint[length]` immediately before the block (SPIR-V
|
||||||
|
// forbids forward type references), and it used to decline the whole block in that case - which
|
||||||
|
// left the offsets in place and made SPIRV-Cross refuse the stage outright:
|
||||||
|
//
|
||||||
|
// Push constant block cannot be expressed as neither std430 nor std140.
|
||||||
|
//
|
||||||
|
// That is KHR-GL43.compute_shader.pipeline-compute-chain: its first kernel declares two counters
|
||||||
|
// at offset 8 (so the flattened array is 4 elements) and first uses the value 4 after the block,
|
||||||
|
// so the kernel never reached the driver and every buffer, image and counter it writes kept its
|
||||||
|
// initial value. Here `i < 4u` is what puts `uint 4` below the block; the ordering assertion
|
||||||
|
// below is the fixture's own latch, so a future glslang that emits constants differently reports
|
||||||
|
// a stale fixture rather than silently testing nothing.
|
||||||
|
constexpr const char* kLateLengthConstantCounters = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(binding = 1, offset = 8) uniform atomic_uint g_counter[2];
|
||||||
|
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
|
||||||
|
void main() {
|
||||||
|
uint i = atomicCounterIncrement(g_counter[1]);
|
||||||
|
if (i < 4u) { g_out.value[0] = i; }
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Index of the first OpConstant of type uint with value |value|, and of struct |structId|, in
|
||||||
|
// the module's instruction order. -1 when absent.
|
||||||
|
std::pair<Int64, Int64> UintConstantAndStructOrder(const Vector<Uint32>& spirv, Uint32 structId,
|
||||||
|
Uint32 value) {
|
||||||
|
Int64 index = 0, constantIndex = -1, structIndex = -1;
|
||||||
|
Uint32 uintTypeId = 0;
|
||||||
|
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||||
|
if (opcode == spv::Op::OpTypeInt && wordCount >= 4u && words[2] == 32u && words[3] == 0u) {
|
||||||
|
uintTypeId = words[1];
|
||||||
|
}
|
||||||
|
if (opcode == spv::Op::OpConstant && wordCount >= 4u && words[1] == uintTypeId &&
|
||||||
|
words[3] == value && constantIndex < 0) {
|
||||||
|
constantIndex = index;
|
||||||
|
}
|
||||||
|
if (opcode == spv::Op::OpTypeStruct && wordCount >= 2u && words[1] == structId) {
|
||||||
|
structIndex = index;
|
||||||
|
}
|
||||||
|
++index;
|
||||||
|
});
|
||||||
|
return {constantIndex, structIndex};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(FlattenAtomicCounterBlockPass, MovesTheBlockToOffsetZeroAndGrowsTheArray) {
|
TEST(FlattenAtomicCounterBlockPass, MovesTheBlockToOffsetZeroAndGrowsTheArray) {
|
||||||
@@ -212,3 +260,44 @@ TEST(FlattenAtomicCounterBlockPass, IsIdempotent) {
|
|||||||
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(once, twice, true));
|
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(once, twice, true));
|
||||||
EXPECT_EQ(twice, once);
|
EXPECT_EQ(twice, once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The block must still flatten when the module already declares the flattened array's length
|
||||||
|
// constant BELOW the block. The pass relocates that constant instead of declining; declining
|
||||||
|
// left the offsets in place and cost the whole stage its transpile.
|
||||||
|
TEST(FlattenAtomicCounterBlockPass, FlattensWhenTheLengthConstantIsDeclaredAfterTheBlock) {
|
||||||
|
const Vector<Uint32> input = CompileCompute(kLateLengthConstantCounters);
|
||||||
|
ASSERT_FALSE(input.empty());
|
||||||
|
|
||||||
|
const Uint32 structId = FindAtomicCounterBlockStructId(input);
|
||||||
|
ASSERT_NE(structId, 0u);
|
||||||
|
ASSERT_EQ(MemberOffsetOf(input, structId, 0u), 8);
|
||||||
|
|
||||||
|
// The fixture's precondition, asserted rather than assumed: two counters at offset 8 need a
|
||||||
|
// 4-element array, and this shader's `uint 4` really does sit below the block.
|
||||||
|
const auto [constantIndex, structIndex] = UintConstantAndStructOrder(input, structId, 4u);
|
||||||
|
ASSERT_GE(constantIndex, 0) << "fixture is stale: the module no longer declares a uint 4";
|
||||||
|
ASSERT_GE(structIndex, 0);
|
||||||
|
ASSERT_GT(constantIndex, structIndex)
|
||||||
|
<< "fixture is stale: `uint 4` is no longer declared after the counter block, so this "
|
||||||
|
"test would pass without exercising the relocation at all";
|
||||||
|
|
||||||
|
Vector<Uint32> output;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
|
||||||
|
ASSERT_FALSE(output.empty());
|
||||||
|
ASSERT_NE(output, input) << "the block was declined; the offsets are still in the module and "
|
||||||
|
"SPIRV-Cross will refuse the stage";
|
||||||
|
|
||||||
|
const Uint32 outStructId = FindAtomicCounterBlockStructId(output);
|
||||||
|
ASSERT_EQ(outStructId, structId);
|
||||||
|
EXPECT_EQ(MemberCountOf(output, outStructId), 1u);
|
||||||
|
EXPECT_EQ(MemberOffsetOf(output, outStructId, 0u), 0);
|
||||||
|
EXPECT_EQ(ArrayLengthOf(output, MemberTypeOf(output, outStructId, 0u)), 4);
|
||||||
|
// The relocation moved a definition; the module has to still be well-ordered.
|
||||||
|
EXPECT_TRUE(Validates(output));
|
||||||
|
|
||||||
|
// The symptom the CTS case actually failed on: with the block declined this throws.
|
||||||
|
MG_Util::ShaderTranspiler::SpvcSession session(
|
||||||
|
output, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
EXPECT_TRUE(essl) << "ESSL transpile failed: " << (essl ? String{} : essl.error().log);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
|||||||
|
// 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().
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Adreno 830 returns uninitialised memory - a different value every run - for
|
||||||
|
// texelFetch(sampler2DMS, ..., sampleIndex != 0).w on an R32F multisample texture whose
|
||||||
|
// GL_TEXTURE_SWIZZLE_A is not the default, from the SECOND such program in the context
|
||||||
|
// onward. The first program reads correctly, which is why the probe links two.
|
||||||
|
//
|
||||||
|
// THREE CONTROLS, each identical to the subject but for one variable, and all three must
|
||||||
|
// read correctly for a wrong subject to count: (1) the same fetch with
|
||||||
|
// GL_TEXTURE_SWIZZLE_A left at its default, (2) the same fetch at sample index 0, and
|
||||||
|
// (3) the same swizzled texture read through .x instead of .w. Without them a driver that
|
||||||
|
// simply cannot render R32F, or cannot sample multisample textures at all, would be
|
||||||
|
// reported as having this very specific corruption.
|
||||||
|
//
|
||||||
|
// Returns false when the driver cannot host the shape (no multisample R32F colour target,
|
||||||
|
// fewer than two samples, a missing entry point, an incomplete framebuffer): an
|
||||||
|
// inconclusive probe must never be reported as a bug. Restores every piece of GL state it
|
||||||
|
// touches.
|
||||||
|
Bool ProbeR32FMultisampleSwizzleCorruption(const MG_External::GLESFunctionsTable& gl);
|
||||||
|
|
||||||
|
// ProbeR32FMultisampleSwizzleCorruption(), evaluated at most once per process.
|
||||||
|
Bool R32FMultisampleSwizzleCorrupted(const MG_External::GLESFunctionsTable& gl);
|
||||||
|
|
||||||
|
// What the image-location budget probe measured. `detected` is the only field the verdict
|
||||||
|
// depends on; the rest exist so the report can say what the shape was instead of asserting
|
||||||
|
// a number that was true on one device in one campaign.
|
||||||
|
struct ImageLocationBudgetMeasurement {
|
||||||
|
Bool detected = false;
|
||||||
|
// Image uniforms declared per stage in both the subject and the control - one more than
|
||||||
|
// GL_MAX_GEOMETRY_IMAGE_UNIFORMS, which is the smallest of the three stages' budgets.
|
||||||
|
Int perStageImageUniforms = 0;
|
||||||
|
// Distinct uniform NAMES in the subject (per-stage-unique) and in the control (shared).
|
||||||
|
Int subjectDistinctNames = 0;
|
||||||
|
Int controlDistinctNames = 0;
|
||||||
|
// The first line of the driver's info log for the failing link, so the report quotes the
|
||||||
|
// driver rather than paraphrasing it.
|
||||||
|
String driverMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Links the same three-stage (vertex, geometry, fragment) program twice: once with every
|
||||||
|
// stage naming its image uniforms uniquely, once with all three stages sharing one set of
|
||||||
|
// names. Both declare the same number of image uniforms per stage, on the same bindings,
|
||||||
|
// with the same qualifier and the same stores - the names are the only difference.
|
||||||
|
//
|
||||||
|
// Adreno 830 charges its image-location budget per distinct NAME, so the shared-name program
|
||||||
|
// links while the per-stage-named one is rejected with "Image location or component exceeds
|
||||||
|
// max allowed", even though nothing about the image USAGE changed. That is what makes the
|
||||||
|
// shared-name link the control: it proves the driver can host this exact amount of image
|
||||||
|
// work and that only the naming moved the answer.
|
||||||
|
//
|
||||||
|
// `detected` is false unless the subject fails AND the control links. Both failing means the
|
||||||
|
// shape is simply too large for the driver (an honest refusal); both linking means the
|
||||||
|
// driver does not have this bug.
|
||||||
|
ImageLocationBudgetMeasurement ProbeImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl);
|
||||||
|
|
||||||
|
// ProbeImageLocationPerNameBudget(), evaluated at most once per process.
|
||||||
|
const ImageLocationBudgetMeasurement& ImageLocationPerNameBudget(const MG_External::GLESFunctionsTable& gl);
|
||||||
|
|
||||||
|
// Draws one quad whose vertex stage stores to a `coherent writeonly` image and whose
|
||||||
|
// fragment stage reads the same image declared `coherent readonly` under the SAME name, then
|
||||||
|
// checks every fragment saw the store. Returns true only when the same-name program loses
|
||||||
|
// the store while the different-name control keeps it.
|
||||||
|
//
|
||||||
|
// Adreno 830 merges the two declarations into one uniform and silently discards the writing
|
||||||
|
// stage's stores. The control is the identical pair of shaders with the two halves renamed -
|
||||||
|
// exactly what MobileGL's image-uniform repair emits - which keeps every store. Without it
|
||||||
|
// the probe would be indistinguishable from "this driver cannot store to images from the
|
||||||
|
// vertex stage", which is a different and much larger claim.
|
||||||
|
//
|
||||||
|
// Returns false when the driver advertises no vertex-stage image uniforms, when an entry
|
||||||
|
// point is missing, or when the setup fails.
|
||||||
|
Bool ProbeCrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl);
|
||||||
|
|
||||||
|
// ProbeCrossStageImageQualifierMergeDropsWrites(), evaluated at most once per process.
|
||||||
|
Bool CrossStageImageQualifierMergeDropsWrites(const MG_External::GLESFunctionsTable& gl);
|
||||||
|
|
||||||
|
// What the image coherency probe measured. The residual is reported rather than hard-coded:
|
||||||
|
// it is a rate, it differs between devices, and a report that quotes a number measured
|
||||||
|
// somewhere else is worse than no number at all.
|
||||||
|
struct ImageCoherencyResidualMeasurement {
|
||||||
|
Bool detected = false;
|
||||||
|
// Texels the STRONGEST in-shader shape missed - that is what makes the defect unfixable.
|
||||||
|
Int mismatchedTexels = 0;
|
||||||
|
// Texels the shape MobileGL emits today missed, on the same driver in the same run. It
|
||||||
|
// is what applications actually get, and it is not always the same number.
|
||||||
|
Int emittedShapeMismatchedTexels = 0;
|
||||||
|
Int totalTexels = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Counts the texels whose dependent imageLoad() did not observe the imageStore() that
|
||||||
|
// precedes it in the same fragment invocation.
|
||||||
|
//
|
||||||
|
// THE SUBJECT IS THE STRONGEST SHAPE THE LANGUAGE OFFERS - a `coherent volatile`
|
||||||
|
// readonly/writeonly pair on one binding with BOTH memoryBarrierImage() and memoryBarrier()
|
||||||
|
// between the store and the read - and that choice is the whole reason the row can say
|
||||||
|
// "unfixable". Probing only the shape MobileGL emits today (`coherent` plus
|
||||||
|
// memoryBarrierImage()) reports a bug on drivers where simply adding `volatile` makes the
|
||||||
|
// read correct, which is a defect MobileGL could fix rather than one it cannot: measured on
|
||||||
|
// Mesa llvmpipe, the emitted shape misses every texel while the `volatile` shape misses
|
||||||
|
// none. Only a driver that fails even the strongest shape has no in-shader substitute left.
|
||||||
|
//
|
||||||
|
// The control is the same dependency split across TWO draws with a glMemoryBarrier and a
|
||||||
|
// glFinish between them. It separates "this driver cannot make image writes visible at all"
|
||||||
|
// (control also dirty - a far worse defect, and the probe declines to call it this one) from
|
||||||
|
// the finding, which is about ordering inside one invocation.
|
||||||
|
//
|
||||||
|
// `detected` is false unless the strongest shape is dirty AND the control is clean. The
|
||||||
|
// shape MobileGL emits is measured either way, so the report can say what applications get.
|
||||||
|
ImageCoherencyResidualMeasurement ProbeImageWriteReadCoherencyResidual(
|
||||||
|
const MG_External::GLESFunctionsTable& gl);
|
||||||
|
|
||||||
|
// ProbeImageWriteReadCoherencyResidual(), evaluated at most once per process.
|
||||||
|
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
|
||||||
|
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
|
||||||
@@ -37,17 +37,19 @@
|
|||||||
namespace MobileGL::MG_Util::SelfTest {
|
namespace MobileGL::MG_Util::SelfTest {
|
||||||
namespace {
|
namespace {
|
||||||
// Display ranks for PostCheck::displayRank: within one backend section, FAIL
|
// Display ranks for PostCheck::displayRank: within one backend section, FAIL
|
||||||
// rows render first, then WARN, PASS, INFO, then the device-driver identity
|
// rows render first, then WARN, then PASS, then the device-driver identity
|
||||||
// strings, and always last (regardless of status) the strings MobileGL itself
|
// strings, and always last (regardless of status) the strings MobileGL itself
|
||||||
// reports to applications. Rows are stable-sorted, so relative order within a
|
// reports to applications. Rows are stable-sorted, so relative order within a
|
||||||
// rank is preserved. Purely cosmetic: the verdict computation is unaffected.
|
// rank is preserved. Purely cosmetic: the verdict computation is unaffected.
|
||||||
|
//
|
||||||
|
// There is no rank between PASS and the identity blocks because there are no INFO
|
||||||
|
// capability rows any more - see the taxonomy on ReportBuilder below.
|
||||||
enum DisplayRank : Int {
|
enum DisplayRank : Int {
|
||||||
RankFail = 0,
|
RankFail = 0,
|
||||||
RankWarn = 1,
|
RankWarn = 1,
|
||||||
RankPass = 2,
|
RankPass = 2,
|
||||||
RankInfo = 3,
|
RankDriverReported = 3,
|
||||||
RankDriverReported = 4,
|
RankMobileGLReported = 4,
|
||||||
RankMobileGLReported = 5,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Both backends' fp64 rows end the same way, and the sentence they end with depends on
|
// Both backends' fp64 rows end the same way, and the sentence they end with depends on
|
||||||
@@ -68,6 +70,28 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
"to advertise it anyway";
|
"to advertise it anyway";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== THE ROW VERDICT TAXONOMY =====================
|
||||||
|
//
|
||||||
|
// EVERY CAPABILITY ROW IS PASS, WARN OR FAIL. INFO IS FOR IDENTITY ONLY - renderer
|
||||||
|
// names, version strings, driver strings - and there is deliberately no way to emit an
|
||||||
|
// INFO capability row from here: the only INFO emitters are the two identity helpers at
|
||||||
|
// the bottom of this struct. A row that says "not supported; no impact today" tells a
|
||||||
|
// reader nothing about whether their application will work, which is the one question
|
||||||
|
// the screen exists to answer.
|
||||||
|
//
|
||||||
|
// PASS - the backend supports the capability directly.
|
||||||
|
// WARN - the backend does NOT support it directly, but a MobileGL quirk substitutes
|
||||||
|
// and the application still sees correct behaviour. The detail names the
|
||||||
|
// substitute and whatever it costs.
|
||||||
|
// FAIL - unsupported, with no substitute: an application that uses it gets wrong
|
||||||
|
// output, a failed draw, or nothing at all. The detail says what breaks.
|
||||||
|
//
|
||||||
|
// FAIL comes in two flavours, and the difference is about the BACKEND, not the row.
|
||||||
|
// Fail() is for a capability the backend cannot start without, and it drives the
|
||||||
|
// backend summary to UNSUPPORTED. FailOptional() is for a capability that is just as
|
||||||
|
// unusable but that the backend runs fine without, so the summary stays DEGRADED - a
|
||||||
|
// device with no dual-source blend still plays Minecraft, and reporting the whole
|
||||||
|
// backend as unusable because of it would be a lie in the other direction.
|
||||||
struct ReportBuilder {
|
struct ReportBuilder {
|
||||||
BackendPostReport report;
|
BackendPostReport report;
|
||||||
Bool fatalFailed = false;
|
Bool fatalFailed = false;
|
||||||
@@ -77,20 +101,27 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
report.checks.push_back({Move(name), "PASS", Move(detail), RankPass});
|
report.checks.push_back({Move(name), "PASS", Move(detail), RankPass});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FAIL on a capability the backend cannot run without: the backend summary becomes
|
||||||
|
// UNSUPPORTED.
|
||||||
void Fail(String name, String detail) {
|
void Fail(String name, String detail) {
|
||||||
fatalFailed = true;
|
fatalFailed = true;
|
||||||
report.checks.push_back({Move(name), "FAIL", Move(detail), RankFail});
|
report.checks.push_back({Move(name), "FAIL", Move(detail), RankFail});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FAIL on a capability with no substitute that the backend can nonetheless run
|
||||||
|
// without. The row is as red as any other FAIL - an application using it does not
|
||||||
|
// work - but the backend summary degrades rather than declaring the whole backend
|
||||||
|
// unusable.
|
||||||
|
void FailOptional(String name, String detail) {
|
||||||
|
warnUnmet = true;
|
||||||
|
report.checks.push_back({Move(name), "FAIL", Move(detail), RankFail});
|
||||||
|
}
|
||||||
|
|
||||||
void Warn(String name, String detail) {
|
void Warn(String name, String detail) {
|
||||||
warnUnmet = true;
|
warnUnmet = true;
|
||||||
report.checks.push_back({Move(name), "WARN", Move(detail), RankWarn});
|
report.checks.push_back({Move(name), "WARN", Move(detail), RankWarn});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Info(String name, String detail) {
|
|
||||||
report.checks.push_back({Move(name), "INFO", Move(detail), RankInfo});
|
|
||||||
}
|
|
||||||
|
|
||||||
// A "Backend driver reported ..." identity string straight from the device
|
// A "Backend driver reported ..." identity string straight from the device
|
||||||
// driver; rendered after the regular rows.
|
// driver; rendered after the regular rows.
|
||||||
void DriverReported(String name, String detail) {
|
void DriverReported(String name, String detail) {
|
||||||
@@ -158,17 +189,19 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
// applications DO, not just what they can do: with the extension advertised, Iris
|
// applications DO, not just what they can do: with the extension advertised, Iris
|
||||||
// and Sodium batch their pipeline compiles and poll GL_COMPLETION_STATUS_KHR.
|
// and Sodium batch their pipeline compiles and poll GL_COMPLETION_STATUS_KHR.
|
||||||
//
|
//
|
||||||
// PASS when it is on (the intended configuration once the default flips), INFO when
|
// PASS when it is on (the intended configuration once the default flips), WARN when it
|
||||||
// it is off - "off" is a supported configuration, not a degradation, so it must not
|
// is off: the capability is not advertised, and what stands in for it - compiling on
|
||||||
// colour the verdict. Either way the row names MOBILEGL_ASYNC_SHADER_COMPILE, so a
|
// the calling thread - produces exactly the same programs, just without the overlap.
|
||||||
// user reading a POST page can tell which side of the switch they are on and how to
|
// Either way the row names MOBILEGL_ASYNC_SHADER_COMPILE, so a user reading a POST page
|
||||||
// change it.
|
// can tell which side of the switch they are on and how to change it.
|
||||||
void AppendAsyncShaderCompileRow(ReportBuilder& builder) {
|
void AppendAsyncShaderCompileRow(ReportBuilder& builder) {
|
||||||
constexpr const char* rowName = "Asynchronous shader compilation";
|
constexpr const char* rowName = "Asynchronous shader compilation";
|
||||||
if (!MG_Util::Async::AsyncShaderCompileEnabled()) {
|
if (!MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
builder.Info(rowName,
|
builder.Warn(rowName,
|
||||||
"off; glCompileShader and glLinkProgram run on the calling thread and "
|
"off; GL_KHR_parallel_shader_compile is not advertised and "
|
||||||
"GL_KHR_parallel_shader_compile is not advertised (set environment variable "
|
"glCompileShader/glLinkProgram run on the calling thread instead. The "
|
||||||
|
"programs are identical - only the overlap is lost, so a shaderpack load "
|
||||||
|
"takes as long as its compiles do (set environment variable "
|
||||||
"MOBILEGL_ASYNC_SHADER_COMPILE=1 to enable it)");
|
"MOBILEGL_ASYNC_SHADER_COMPILE=1 to enable it)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -301,22 +334,30 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Pass("Polygon mode",
|
builder.Pass("Polygon mode",
|
||||||
"glPolygonMode GL_LINE/GL_POINT available via GL_NV/ANGLE_polygon_mode");
|
"glPolygonMode GL_LINE/GL_POINT available via GL_NV/ANGLE_polygon_mode");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Polygon mode",
|
builder.FailOptional("Polygon mode",
|
||||||
"no GL_NV/ANGLE_polygon_mode; glPolygonMode GL_LINE/GL_POINT falls back to GL_FILL");
|
"no GL_NV/ANGLE_polygon_mode; glPolygonMode GL_LINE/GL_POINT silently "
|
||||||
|
"falls back to GL_FILL. There is no substitute - wireframe and point "
|
||||||
|
"rasterization would have to be rebuilt out of line/point primitives - "
|
||||||
|
"so an application asking for either gets solid triangles instead");
|
||||||
}
|
}
|
||||||
if (caps.SupportsIndexedColorMask) {
|
if (caps.SupportsIndexedColorMask) {
|
||||||
builder.Pass("Indexed color mask",
|
builder.Pass("Indexed color mask",
|
||||||
"per-draw-buffer glColorMaski available (ES 3.2 core or draw_buffers_indexed)");
|
"per-draw-buffer glColorMaski available (ES 3.2 core or draw_buffers_indexed)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Indexed color mask",
|
builder.FailOptional("Indexed color mask",
|
||||||
"no indexed glColorMaski; per-draw-buffer color masks fall back to draw buffer 0");
|
"no indexed glColorMaski; every per-draw-buffer colour mask collapses "
|
||||||
|
"onto draw buffer 0's, so an MRT pass that masks its attachments "
|
||||||
|
"differently writes the wrong channels to all but one of them, with "
|
||||||
|
"nothing to substitute");
|
||||||
}
|
}
|
||||||
if (caps.SupportsDualSourceBlend) {
|
if (caps.SupportsDualSourceBlend) {
|
||||||
builder.Pass("Dual-source blend",
|
builder.Pass("Dual-source blend",
|
||||||
"GL_SRC1_* dual-source blend factors available via GL_EXT_blend_func_extended");
|
"GL_SRC1_* dual-source blend factors available via GL_EXT_blend_func_extended");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Dual-source blend",
|
builder.FailOptional("Dual-source blend",
|
||||||
"no GL_EXT_blend_func_extended; GL_SRC1_* dual-source blend factors hard-fail at draw");
|
"no GL_EXT_blend_func_extended; a draw using a GL_SRC1_* blend factor "
|
||||||
|
"hard-fails, and a second fragment output cannot be produced any other "
|
||||||
|
"way");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (es31) {
|
if (es31) {
|
||||||
@@ -328,10 +369,13 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Pass("Vertex shader storage blocks",
|
builder.Pass("Vertex shader storage blocks",
|
||||||
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}", maxVertexSsboBlocks));
|
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}", maxVertexSsboBlocks));
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Vertex shader storage blocks",
|
builder.FailOptional(
|
||||||
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}; the Flywheel/Create indirect draw "
|
"Vertex shader storage blocks",
|
||||||
"machinery cannot read indirect command buffers from the vertex stage",
|
format("GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = {}; the vertex stage cannot read a "
|
||||||
maxVertexSsboBlocks));
|
"storage buffer at all, and there is nothing to read one with instead - the "
|
||||||
|
"Flywheel/Create indirect draw machinery, which fetches its per-instance data "
|
||||||
|
"from a vertex-stage SSBO, cannot run",
|
||||||
|
maxVertexSsboBlocks));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caps.MaxShaderStorageBufferBindings >= 8) {
|
if (caps.MaxShaderStorageBufferBindings >= 8) {
|
||||||
@@ -350,14 +394,15 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
if (caps.SupportsPersistentMapping) {
|
if (caps.SupportsPersistentMapping) {
|
||||||
builder.Pass("GL_EXT_buffer_storage", "supported (persistent buffer mapping)");
|
builder.Pass("GL_EXT_buffer_storage", "supported (persistent buffer mapping)");
|
||||||
} else {
|
} else {
|
||||||
builder.Info("GL_EXT_buffer_storage",
|
builder.Warn("GL_EXT_buffer_storage",
|
||||||
"not supported; no impact today: the frontend fully emulates persistent "
|
"not supported; the frontend emulates persistent mapping with its own "
|
||||||
"mapping regardless of this extension");
|
"shadow storage instead, so glBufferStorage and a GL_MAP_PERSISTENT_BIT "
|
||||||
|
"mapping behave correctly - at the cost of the shadow copy");
|
||||||
}
|
}
|
||||||
if (caps.SupportsBaseInstance) {
|
if (caps.SupportsBaseInstance) {
|
||||||
builder.Pass("GL_EXT_base_instance", "supported (native baseInstance draws)");
|
builder.Pass("GL_EXT_base_instance", "supported (native baseInstance draws)");
|
||||||
} else {
|
} else {
|
||||||
builder.Info("GL_EXT_base_instance",
|
builder.Warn("GL_EXT_base_instance",
|
||||||
"not supported; direct baseInstance draws are emulated by shifting the "
|
"not supported; direct baseInstance draws are emulated by shifting the "
|
||||||
"instanced arrays' attribute offsets, and gl_BaseInstance by a uniform. "
|
"instanced arrays' attribute offsets, and gl_BaseInstance by a uniform. "
|
||||||
"The one gap is an INDIRECT draw whose command carries a non-zero "
|
"The one gap is an INDIRECT draw whose command carries a non-zero "
|
||||||
@@ -366,15 +411,17 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
// Both multi-draw rows gate on the capability flags, not the entry-point pointers:
|
// Both multi-draw rows gate on the capability flags, not the entry-point pointers:
|
||||||
// eglGetProcAddress may hand back a non-NULL stub for these on drivers without the
|
// eglGetProcAddress may hand back a non-NULL stub for these on drivers without the
|
||||||
// extension (NVIDIA ES does, and its glMultiDrawElementsBaseVertexEXT stub silently
|
// extension (NVIDIA ES does, and its glMultiDrawElementsBaseVertexEXT stub silently
|
||||||
// drops every draw), so the pointers prove nothing. Absence is INFO in both cases
|
// drops every draw), so the pointers prove nothing. Absence is WARN in both cases:
|
||||||
// because MobileGL falls back to an equivalent per-draw loop.
|
// MobileGL falls back to an equivalent per-draw loop, so the output is identical and
|
||||||
|
// only the command count changes.
|
||||||
if (caps.SupportsMultiDrawIndirect) {
|
if (caps.SupportsMultiDrawIndirect) {
|
||||||
builder.Pass("Multi-draw indirect",
|
builder.Pass("Multi-draw indirect",
|
||||||
"glMultiDrawArrays/ElementsIndirectEXT available via GL_EXT_multi_draw_indirect");
|
"glMultiDrawArrays/ElementsIndirectEXT available via GL_EXT_multi_draw_indirect");
|
||||||
} else {
|
} else {
|
||||||
builder.Info("Multi-draw indirect",
|
builder.Warn("Multi-draw indirect",
|
||||||
"GL_EXT_multi_draw_indirect not supported; no impact today: multi-draw "
|
"GL_EXT_multi_draw_indirect not supported; MobileGL decomposes a multi-draw "
|
||||||
"indirect is decomposed into per-command indirect draws regardless");
|
"indirect batch into per-command indirect draws, which renders the same "
|
||||||
|
"thing for one driver call per command instead of one per batch");
|
||||||
}
|
}
|
||||||
if (caps.SupportsMultiDrawElementsBaseVertex) {
|
if (caps.SupportsMultiDrawElementsBaseVertex) {
|
||||||
builder.Pass("Multi-draw base vertex",
|
builder.Pass("Multi-draw base vertex",
|
||||||
@@ -382,7 +429,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
"with GL_EXT_multi_draw_arrays); glMultiDrawElementsBaseVertex batches into one "
|
"with GL_EXT_multi_draw_arrays); glMultiDrawElementsBaseVertex batches into one "
|
||||||
"driver call");
|
"driver call");
|
||||||
} else {
|
} else {
|
||||||
builder.Info("Multi-draw base vertex",
|
builder.Warn("Multi-draw base vertex",
|
||||||
"glMultiDrawElementsBaseVertexEXT not supported (needs EXT/OES_"
|
"glMultiDrawElementsBaseVertexEXT not supported (needs EXT/OES_"
|
||||||
"draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); the batch "
|
"draw_elements_base_vertex plus GL_EXT_multi_draw_arrays); the batch "
|
||||||
"takes the next emulation tier instead, with identical output - see "
|
"takes the next emulation tier instead, with identical output - see "
|
||||||
@@ -407,9 +454,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
"available (ES 3.1 core); the opt-in \"compute\" multi-draw tier can flatten a "
|
"available (ES 3.1 core); the opt-in \"compute\" multi-draw tier can flatten a "
|
||||||
"whole batch into one draw");
|
"whole batch into one draw");
|
||||||
} else {
|
} else {
|
||||||
builder.Info("Compute shaders",
|
builder.FailOptional("Compute shaders",
|
||||||
"not available (pre-ES 3.1); no impact on the default multi-draw tiers, which "
|
"not available (pre-ES 3.1); MobileGL advertises "
|
||||||
"never use compute");
|
"GL_ARB_compute_shader on an OpenGL 4.x context and there is no way to "
|
||||||
|
"run a glDispatchCompute without the ES counterpart, so a program with "
|
||||||
|
"a compute shader cannot be built at all. The default multi-draw tiers "
|
||||||
|
"never use compute, so nothing else is lost");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// The same resolution the backend runs, over the capabilities probed here.
|
// The same resolution the backend runs, over the capabilities probed here.
|
||||||
@@ -420,45 +470,58 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
// was consulted.
|
// was consulted.
|
||||||
using MG_Backend::DirectGLES::MultiDrawImpl::ResolveTier;
|
using MG_Backend::DirectGLES::MultiDrawImpl::ResolveTier;
|
||||||
String resolution;
|
String resolution;
|
||||||
ResolveTier(caps, glesFuncs, MG_Config::Features.EsprytMultiDrawMode, &resolution);
|
const MG_Config::GLESMultiDrawMode tier =
|
||||||
builder.Info("Multi-draw elements tier",
|
ResolveTier(caps, glesFuncs, MG_Config::Features.EsprytMultiDrawMode, &resolution);
|
||||||
"glMultiDrawElements(BaseVertex) emulation: " + resolution +
|
const String detail = "glMultiDrawElements(BaseVertex) emulation: " + resolution +
|
||||||
"; override with MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
"; override with MOBILEGL_ESPRYT_MULTIDRAW_MODE";
|
||||||
|
// PASS only on the tier that hands the whole batch to the driver in one call.
|
||||||
|
// Every other tier is a MobileGL substitute: the output is identical, the
|
||||||
|
// command count is not.
|
||||||
|
if (tier == MG_Config::GLESMultiDrawMode::Ext) {
|
||||||
|
builder.Pass("Multi-draw elements tier", detail);
|
||||||
|
} else {
|
||||||
|
builder.Warn("Multi-draw elements tier",
|
||||||
|
detail + " - the batch is replayed rather than handed over whole, "
|
||||||
|
"which renders the same thing for more driver calls");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (caps.SupportsTextureBorderClamp) {
|
if (caps.SupportsTextureBorderClamp) {
|
||||||
builder.Pass("Texture border clamp",
|
builder.Pass("Texture border clamp",
|
||||||
"supported (GL_TEXTURE_BORDER_COLOR reaches the driver, so "
|
"supported (GL_TEXTURE_BORDER_COLOR reaches the driver, so "
|
||||||
"GL_CLAMP_TO_BORDER samples the colour the application set)");
|
"GL_CLAMP_TO_BORDER samples the colour the application set)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Texture border clamp",
|
builder.FailOptional(
|
||||||
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_border_clamp); "
|
"Texture border clamp",
|
||||||
"GL_TEXTURE_BORDER_COLOR is not synced to the driver at all, so anything "
|
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_border_clamp); "
|
||||||
"sampling outside a GL_CLAMP_TO_BORDER texture reads the driver's default "
|
"GL_TEXTURE_BORDER_COLOR is not synced to the driver at all, so anything "
|
||||||
"border instead of the requested colour");
|
"sampling outside a GL_CLAMP_TO_BORDER texture reads the driver's default "
|
||||||
|
"border instead of the requested colour, and no wrap mode substitutes for it");
|
||||||
}
|
}
|
||||||
if (caps.SupportsTextureCubeMapArray) {
|
if (caps.SupportsTextureCubeMapArray) {
|
||||||
builder.Pass("Texture cube map array",
|
builder.Pass("Texture cube map array",
|
||||||
"supported (GL_TEXTURE_CUBE_MAP_ARRAY textures get real storage and can be "
|
"supported (GL_TEXTURE_CUBE_MAP_ARRAY textures get real storage and can be "
|
||||||
"attached to a framebuffer)");
|
"attached to a framebuffer)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Texture cube map array",
|
builder.FailOptional(
|
||||||
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_cube_map_array); a cube "
|
"Texture cube map array",
|
||||||
"map array texture gets no driver storage at all, so sampling one reads nothing "
|
"not supported (pre-ES 3.2 without GL_EXT/OES_texture_cube_map_array); a cube "
|
||||||
"and rendering to one does not reach the screen");
|
"map array texture gets no driver storage at all, so sampling one reads nothing "
|
||||||
|
"and rendering to one does not reach the screen. Nothing substitutes: the "
|
||||||
|
"shaders that declare a samplerCubeArray do not compile either");
|
||||||
}
|
}
|
||||||
// WARN, not FAIL, and the choice is deliberate. The consequence is severe - buffer
|
// FAIL, and specifically FailOptional. The consequence is severe - buffer textures
|
||||||
// textures are CORE in OpenGL 3.1 and MobileGL advertises a 4.x context, so an
|
// are CORE in OpenGL 3.1 and MobileGL advertises a 4.x context, so an application
|
||||||
// application may use one without asking, and nothing degrades gracefully: the
|
// may use one without asking, and nothing degrades gracefully: the texture gets no
|
||||||
// texture gets no driver storage, and every shader declaring a samplerBuffer fails
|
// driver storage, and every shader declaring a samplerBuffer fails to compile
|
||||||
// to compile outright, because SPIRV-Cross emits `#extension GL_EXT_texture_buffer :
|
// outright, because SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require`
|
||||||
// require` for it below ESSL 320, so the program never links and every draw using it
|
// for it below ESSL 320, so the program never links and every draw using it silently
|
||||||
// silently draws nothing. That is how Minecraft 26.3, whose cloud layer is built
|
// draws nothing. That is how Minecraft 26.3, whose cloud layer is built entirely
|
||||||
// entirely from gl_VertexID plus texelFetch on a GL_R8I buffer texture, loses its
|
// from gl_VertexID plus texelFetch on a GL_R8I buffer texture, loses its clouds.
|
||||||
// clouds. But FAIL means "this backend cannot run on this driver", and that is not
|
// There is no substitute, which is what makes the row FAIL; the backend still RUNS
|
||||||
// true: such a device runs everything that does not touch a buffer texture. It is
|
// everything that does not touch a buffer texture, which is what keeps the failure
|
||||||
// also exactly the shape of the "Texture cube map array" row above, which loses its
|
// out of the backend summary. It is exactly the shape of the "Texture cube map
|
||||||
// shaders to the same SPIRV-Cross `: require` mechanism and is a WARN - two adjacent
|
// array" row above, which loses its shaders to the same SPIRV-Cross `: require`
|
||||||
// rows with one consequence must not carry two severities.
|
// mechanism - two adjacent rows with one consequence must carry one severity.
|
||||||
// The limit is stated on every tier because it is the one number an application can
|
// The limit is stated on every tier because it is the one number an application can
|
||||||
// read, and on the None tier it is knowingly a fiction (see below).
|
// read, and on the None tier it is knowingly a fiction (see below).
|
||||||
{
|
{
|
||||||
@@ -495,16 +558,17 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
break;
|
break;
|
||||||
case Tier::None:
|
case Tier::None:
|
||||||
default:
|
default:
|
||||||
builder.Warn("Buffer textures",
|
builder.FailOptional(
|
||||||
format("not supported (pre-ES 3.2 without GL_EXT/OES_texture_buffer); "
|
"Buffer textures",
|
||||||
"glTexBuffer does not exist, so a buffer texture gets no storage, "
|
format("not supported (pre-ES 3.2 without GL_EXT/OES_texture_buffer); "
|
||||||
"and any shader declaring a samplerBuffer fails to compile and "
|
"glTexBuffer does not exist, so a buffer texture gets no storage, "
|
||||||
"leaves its program unlinked - every draw using it is a silent "
|
"and any shader declaring a samplerBuffer fails to compile and "
|
||||||
"no-op. MobileGL still reports GL_MAX_TEXTURE_BUFFER_SIZE = {}: "
|
"leaves its program unlinked - every draw using it is a silent "
|
||||||
"the value is a floor it cannot honour, kept because an OpenGL "
|
"no-op. MobileGL still reports GL_MAX_TEXTURE_BUFFER_SIZE = {}: "
|
||||||
"4.x context may not answer 0 and GL has no way to say that a "
|
"the value is a floor it cannot honour, kept because an OpenGL "
|
||||||
"core feature is missing",
|
"4.x context may not answer 0 and GL has no way to say that a "
|
||||||
advertisedLimit));
|
"core feature is missing",
|
||||||
|
advertisedLimit));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,7 +577,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
// either answer, and the rows exist so the two halves of the loss are named at
|
// either answer, and the rows exist so the two halves of the loss are named at
|
||||||
// startup instead of discovered as a shader that will not compile or an
|
// startup instead of discovered as a shader that will not compile or an
|
||||||
// unexplained GL_INVALID_OPERATION at draw setup.
|
// unexplained GL_INVALID_OPERATION at draw setup.
|
||||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
// WARN, not PASS: ESSL has no 64-bit float type, so this backend does not support
|
||||||
|
// fp64 directly at all. What it has is a complete substitute - the shaders build and
|
||||||
|
// run - which is exactly what WARN means.
|
||||||
|
builder.Warn("fp64", AppendFp64AdvertisementNote(
|
||||||
"demoted to fp32 - ESSL has no 64-bit float type, so every double / "
|
"demoted to fp32 - ESSL has no 64-bit float type, so every double / "
|
||||||
"dvec / dmat in a shader is narrowed to 32 bits before transpilation "
|
"dvec / dmat in a shader is narrowed to 32 bits before transpilation "
|
||||||
"(DemoteFloat64Pass). Such shaders COMPILE AND RUN, at single "
|
"(DemoteFloat64Pass). Such shaders COMPILE AND RUN, at single "
|
||||||
@@ -531,10 +598,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Pass("Tessellation patch parameters",
|
builder.Pass("Tessellation patch parameters",
|
||||||
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Tessellation patch parameters",
|
builder.FailOptional("Tessellation patch parameters",
|
||||||
"glPatchParameteri missing (pre-ES 3.2 without GL_EXT_tessellation_shader); "
|
"glPatchParameteri missing (pre-ES 3.2 without "
|
||||||
"GL_PATCH_VERTICES stays at the driver default of 3 and a patch draw of any "
|
"GL_EXT_tessellation_shader); GL_PATCH_VERTICES stays at the driver "
|
||||||
"other size renders nothing");
|
"default of 3 and a patch draw of any other size renders nothing - "
|
||||||
|
"the patch size cannot be communicated any other way");
|
||||||
}
|
}
|
||||||
if (glesFuncs.glGenTransformFeedbacks != nullptr && glesFuncs.glBindTransformFeedback != nullptr &&
|
if (glesFuncs.glGenTransformFeedbacks != nullptr && glesFuncs.glBindTransformFeedback != nullptr &&
|
||||||
glesFuncs.glPauseTransformFeedback != nullptr && glesFuncs.glResumeTransformFeedback != nullptr) {
|
glesFuncs.glPauseTransformFeedback != nullptr && glesFuncs.glResumeTransformFeedback != nullptr) {
|
||||||
@@ -550,7 +618,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Pass("GL_EXT_texture_norm16", "supported");
|
builder.Pass("GL_EXT_texture_norm16", "supported");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("GL_EXT_texture_norm16",
|
builder.Warn("GL_EXT_texture_norm16",
|
||||||
"not supported; 16-bit normalized texture formats need emulation");
|
"not supported; MobileGL substitutes a wider format for every 16-bit "
|
||||||
|
"normalized texture, so the texels are still readable at their declared "
|
||||||
|
"precision at the cost of the extra storage");
|
||||||
}
|
}
|
||||||
if (caps.SupportsRenderSnorm) {
|
if (caps.SupportsRenderSnorm) {
|
||||||
builder.Pass("GL_EXT_render_snorm",
|
builder.Pass("GL_EXT_render_snorm",
|
||||||
@@ -583,23 +653,31 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
"render targets (Iris reports GL_FRAMEBUFFER_UNSUPPORTED and refuses to load)");
|
"render targets (Iris reports GL_FRAMEBUFFER_UNSUPPORTED and refuses to load)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// INFO, never WARN: this is the HOST driver's ability to compile its own ESSL on
|
// WARN and never FAIL when it is absent: this is the HOST driver's ability to
|
||||||
// its own threads, and MobileGL's asynchronous compilation does not depend on it
|
// compile its own ESSL on its own threads, and MobileGL's own compile pool stands in
|
||||||
// in the slightest - the pool parallelises GLSL -> SPIR-V -> ESSL translation,
|
// for all of it that matters - the pool parallelises GLSL -> SPIR-V -> ESSL
|
||||||
// which is where a shaderpack load actually spends its time, and it does that on
|
// translation, which is where a shaderpack load actually spends its time, and it
|
||||||
// a driver that has never heard of the extension. The row exists so that the day
|
// does that on a driver that has never heard of the extension. The row exists so
|
||||||
// the driver-side half is overlapped too, the POST already says which devices can.
|
// that the day the driver-side half is overlapped too, the POST already says which
|
||||||
builder.Info("Driver GL_KHR_parallel_shader_compile",
|
// devices can.
|
||||||
caps.SupportsParallelShaderCompile
|
if (caps.SupportsParallelShaderCompile) {
|
||||||
? "supported; the device driver can also compile the translated ESSL off-thread"
|
builder.Pass("Driver GL_KHR_parallel_shader_compile",
|
||||||
: "not supported; the device driver compiles the translated ESSL on the calling "
|
"supported; the device driver can also compile the translated ESSL off-thread");
|
||||||
"thread (MobileGL's own compile pool is unaffected)");
|
} else {
|
||||||
|
builder.Warn("Driver GL_KHR_parallel_shader_compile",
|
||||||
|
"not supported; the device driver compiles the translated ESSL on the calling "
|
||||||
|
"thread. MobileGL's own compile pool substitutes for the expensive half of the "
|
||||||
|
"work (GLSL -> SPIR-V -> ESSL) and is unaffected, so loads still overlap");
|
||||||
|
}
|
||||||
|
|
||||||
builder.Info("Indirect gl_InstanceID semantics",
|
if (caps.IndirectDrawInstanceIdIncludesBaseInstance) {
|
||||||
caps.IndirectDrawInstanceIdIncludesBaseInstance
|
builder.Warn("Indirect gl_InstanceID semantics",
|
||||||
? "includes baseInstance (ANGLE-style; MobileGL's shader rewrite keeps gl_InstanceID "
|
"includes baseInstance (ANGLE-style), which is not what GL promises; "
|
||||||
"zero-based)"
|
"MobileGL's shader rewrite subtracts it back out so gl_InstanceID stays "
|
||||||
: "conforming (zero-based)");
|
"zero-based and instanced indirect draws index their arrays correctly");
|
||||||
|
} else {
|
||||||
|
builder.Pass("Indirect gl_InstanceID semantics", "conforming (zero-based)");
|
||||||
|
}
|
||||||
|
|
||||||
builder.DriverReported("Backend driver reported GL_VENDOR", caps.GLESVendorString);
|
builder.DriverReported("Backend driver reported GL_VENDOR", caps.GLESVendorString);
|
||||||
builder.DriverReported("Backend driver reported GL_RENDERER", caps.GLESRendererString);
|
builder.DriverReported("Backend driver reported GL_RENDERER", caps.GLESRendererString);
|
||||||
@@ -614,17 +692,21 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
const MG_External::GLESFunctionsTable& glesFuncs) {
|
const MG_External::GLESFunctionsTable& glesFuncs) {
|
||||||
const String disabledNote = TimerQueryDisabledNote();
|
const String disabledNote = TimerQueryDisabledNote();
|
||||||
if (!caps.SupportsDisjointTimerQuery) {
|
if (!caps.SupportsDisjointTimerQuery) {
|
||||||
builder.Warn("Timer queries",
|
builder.FailOptional("Timer queries",
|
||||||
"GL_EXT_disjoint_timer_query not supported; timer queries unavailable; "
|
"GL_EXT_disjoint_timer_query not supported; there is no way to time "
|
||||||
"Minecraft F3 GPU% will not show" +
|
"GPU work from the client, so glBeginQuery(GL_TIME_ELAPSED) has "
|
||||||
disabledNote);
|
"nothing to stand in for it and Minecraft's F3 GPU% will not show" +
|
||||||
|
disabledNote);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Every emit carries the extension-presence fact the old standalone
|
// Every emit carries the extension-presence fact the old standalone
|
||||||
// GL_EXT_disjoint_timer_query row showed, plus the probe outcome.
|
// GL_EXT_disjoint_timer_query row showed, plus the probe outcome.
|
||||||
const String extensionPresent = "GL_EXT_disjoint_timer_query extension present";
|
const String extensionPresent = "GL_EXT_disjoint_timer_query extension present";
|
||||||
|
// FailOptional: a driver that advertises the extension and then cannot serve a
|
||||||
|
// query is broken in a way nothing substitutes for, but timing GPU work is not
|
||||||
|
// something the backend needs in order to run.
|
||||||
const auto fail = [&](const String& detail) {
|
const auto fail = [&](const String& detail) {
|
||||||
builder.Fail("Timer queries", extensionPresent + "; but " + detail + disabledNote);
|
builder.FailOptional("Timer queries", extensionPresent + "; but " + detail + disabledNote);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!glesFuncs.glGenQueries || !glesFuncs.glDeleteQueries || !glesFuncs.glBeginQuery ||
|
if (!glesFuncs.glGenQueries || !glesFuncs.glDeleteQueries || !glesFuncs.glBeginQuery ||
|
||||||
@@ -758,8 +840,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
const String pathNote = native ? "GL_NV_shader_noperspective_interpolation present (native path)"
|
const String pathNote = native ? "GL_NV_shader_noperspective_interpolation present (native path)"
|
||||||
: "GL_NV_shader_noperspective_interpolation absent (gl_Position.w / "
|
: "GL_NV_shader_noperspective_interpolation absent (gl_Position.w / "
|
||||||
"gl_FragCoord.w emulation path)";
|
"gl_FragCoord.w emulation path)";
|
||||||
|
// FailOptional: a shaderpack that declares a noperspective varying renders it wrong
|
||||||
|
// and nothing stands in for the interpolation, but everything that does not use one
|
||||||
|
// is unaffected, so the backend still runs.
|
||||||
const auto fail = [&](const String& detail) {
|
const auto fail = [&](const String& detail) {
|
||||||
builder.Fail("noperspective interpolation", pathNote + "; " + detail);
|
builder.FailOptional("noperspective interpolation", pathNote + "; " + detail);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!g.glCreateShader || !g.glShaderSource || !g.glCompileShader || !g.glGetShaderiv ||
|
if (!g.glCreateShader || !g.glShaderSource || !g.glCompileShader || !g.glGetShaderiv ||
|
||||||
@@ -1157,6 +1242,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
MG_Backend::DirectGLES::PopulateFormatCapabilities(
|
MG_Backend::DirectGLES::PopulateFormatCapabilities(
|
||||||
glesFuncs, caps, builder.report.formatCapabilities.value());
|
glesFuncs, caps, builder.report.formatCapabilities.value());
|
||||||
ReportThreeChannelColorAttachments(builder, 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);
|
} while (false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1252,8 +1342,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
const String timestampFacts =
|
const String timestampFacts =
|
||||||
format("timestampValidBits = {} on the graphics queue family; timestampPeriod = {} ns per tick",
|
format("timestampValidBits = {} on the graphics queue family; timestampPeriod = {} ns per tick",
|
||||||
timestampValidBits, timestampPeriod);
|
timestampValidBits, timestampPeriod);
|
||||||
|
// FailOptional, for the same reason as the GLES row: the backend does not need to
|
||||||
|
// time GPU work in order to run.
|
||||||
const auto fail = [&](const String& detail) {
|
const auto fail = [&](const String& detail) {
|
||||||
builder.Fail("Timer queries", timestampFacts + "; but " + detail + disabledNote);
|
builder.FailOptional("Timer queries", timestampFacts + "; but " + detail + disabledNote);
|
||||||
};
|
};
|
||||||
const auto vkCreateDeviceFn =
|
const auto vkCreateDeviceFn =
|
||||||
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
|
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
|
||||||
@@ -1473,7 +1565,13 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
Bool subgroupPropertiesAvailable,
|
Bool subgroupPropertiesAvailable,
|
||||||
const VkPhysicalDeviceSubgroupProperties& subgroupProperties) {
|
const VkPhysicalDeviceSubgroupProperties& subgroupProperties) {
|
||||||
constexpr const char* RowName = "Subgroup first-reduction witness";
|
constexpr const char* RowName = "Subgroup first-reduction witness";
|
||||||
const auto fail = [&](String detail) { builder.Fail(RowName, Move(detail)); };
|
// FailOptional, not Fail. The witness reports whether the NATIVE subgroup
|
||||||
|
// first-reduction works; when it does not, the renderer takes its non-subgroup
|
||||||
|
// iteration path and draws the same image. Both an Adreno 830 and Mesa lavapipe
|
||||||
|
// fail this row's topology check today while running the DirectVulkan backend
|
||||||
|
// perfectly well, so a fatal verdict here would have the screen announce that a
|
||||||
|
// backend the user is looking at through that very backend cannot run.
|
||||||
|
const auto fail = [&](String detail) { builder.FailOptional(RowName, Move(detail)); };
|
||||||
|
|
||||||
if (!subgroupPropertiesAvailable) {
|
if (!subgroupPropertiesAvailable) {
|
||||||
fail("vkGetPhysicalDeviceProperties2 could not provide raw Vulkan subgroup properties");
|
fail("vkGetPhysicalDeviceProperties2 could not provide raw Vulkan subgroup properties");
|
||||||
@@ -1500,7 +1598,13 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
|
|
||||||
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
|
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
|
||||||
if (eligibility.eligibility == IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet) {
|
if (eligibility.eligibility == IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet) {
|
||||||
builder.Info(RowName, eligibility.detail);
|
// WARN, not FAIL: there is nothing to witness on a device with no native
|
||||||
|
// subgroup contract, and the renderer takes its non-subgroup iteration path,
|
||||||
|
// which produces the same image.
|
||||||
|
builder.Warn(RowName,
|
||||||
|
eligibility.detail +
|
||||||
|
"; the renderer takes its non-subgroup iteration path instead, which "
|
||||||
|
"renders the same thing without the first-reduction shortcut");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (eligibility.eligibility == IterationRPWitnessEligibility::FailInadequateLimits) {
|
if (eligibility.eligibility == IterationRPWitnessEligibility::FailInadequateLimits) {
|
||||||
@@ -2199,20 +2303,23 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
if (features.multiDrawIndirect == VK_TRUE) {
|
if (features.multiDrawIndirect == VK_TRUE) {
|
||||||
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
|
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
|
||||||
} else {
|
} else {
|
||||||
builder.Info("multiDrawIndirect",
|
builder.Warn("multiDrawIndirect",
|
||||||
"unsupported; multi-draw batches fall back to one draw per command (tier "
|
"unsupported; MobileGL unrolls a multi-draw batch into one draw per command "
|
||||||
"\"indirect\" of the multi-draw dispatch is unavailable)");
|
"(tier \"indirect\" of the multi-draw dispatch is unavailable), which renders "
|
||||||
|
"the same thing for more commands");
|
||||||
}
|
}
|
||||||
if (features.drawIndirectFirstInstance == VK_TRUE) {
|
if (features.drawIndirectFirstInstance == VK_TRUE) {
|
||||||
builder.Pass("drawIndirectFirstInstance", "indirect commands may carry a non-zero firstInstance");
|
builder.Pass("drawIndirectFirstInstance", "indirect commands may carry a non-zero firstInstance");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("drawIndirectFirstInstance",
|
builder.FailOptional("drawIndirectFirstInstance",
|
||||||
"unsupported; indirect commands with a non-zero baseInstance cannot run natively");
|
"unsupported; an indirect command carrying a non-zero baseInstance "
|
||||||
|
"cannot run, and the offset cannot be folded into the command from the "
|
||||||
|
"CPU because the command is on the GPU");
|
||||||
}
|
}
|
||||||
// Multi-draw dispatch tiers (ext -> indirect -> unroll). INFO on the missing
|
// Multi-draw dispatch tiers (ext -> indirect -> unroll). WARN on the missing
|
||||||
// pieces: every tier has a fallback, nothing is lost, only batched into more
|
// pieces: every tier has a fallback that renders the same thing, only batched
|
||||||
// commands. The renderer resolves the same chain at device creation, clamped
|
// into more commands. The renderer resolves the same chain at device creation,
|
||||||
// by MOBILEGL_MAGMA_MULTIDRAW_MODE.
|
// clamped by MOBILEGL_MAGMA_MULTIDRAW_MODE.
|
||||||
{
|
{
|
||||||
Bool multiDrawExtUsable = false;
|
Bool multiDrawExtUsable = false;
|
||||||
if (HasVkExtension(deviceExtensions, VK_EXT_MULTI_DRAW_EXTENSION_NAME) &&
|
if (HasVkExtension(deviceExtensions, VK_EXT_MULTI_DRAW_EXTENSION_NAME) &&
|
||||||
@@ -2229,8 +2336,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Pass("VK_EXT_multi_draw",
|
builder.Pass("VK_EXT_multi_draw",
|
||||||
"supported; a glMultiDraw* batch runs as one vkCmdDrawMulti(Indexed)EXT");
|
"supported; a glMultiDraw* batch runs as one vkCmdDrawMulti(Indexed)EXT");
|
||||||
} else {
|
} else {
|
||||||
builder.Info("VK_EXT_multi_draw",
|
builder.Warn("VK_EXT_multi_draw",
|
||||||
"unsupported; glMultiDraw* batches use the indirect or unrolled tier");
|
"unsupported; glMultiDraw* batches take the indirect or unrolled tier "
|
||||||
|
"instead, with identical output");
|
||||||
}
|
}
|
||||||
const char* resolvedTier = multiDrawExtUsable ? "ext"
|
const char* resolvedTier = multiDrawExtUsable ? "ext"
|
||||||
: features.multiDrawIndirect == VK_TRUE ? "indirect"
|
: features.multiDrawIndirect == VK_TRUE ? "indirect"
|
||||||
@@ -2243,32 +2351,46 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
: multiDrawMode == MG_Config::MultiDrawMode::Indirect ? "indirect"
|
: multiDrawMode == MG_Config::MultiDrawMode::Indirect ? "indirect"
|
||||||
: "unroll");
|
: "unroll");
|
||||||
}
|
}
|
||||||
builder.Info("Multi-draw dispatch tier", tierDetail);
|
// PASS only on the tier that hands the whole batch to the driver in one command.
|
||||||
|
if (multiDrawExtUsable) {
|
||||||
|
builder.Pass("Multi-draw dispatch tier", tierDetail);
|
||||||
|
} else {
|
||||||
|
builder.Warn("Multi-draw dispatch tier",
|
||||||
|
tierDetail + "; the batch is replayed rather than handed over whole, which "
|
||||||
|
"renders the same thing for more commands");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (features.vertexPipelineStoresAndAtomics == VK_TRUE) {
|
if (features.vertexPipelineStoresAndAtomics == VK_TRUE) {
|
||||||
builder.Pass("vertexPipelineStoresAndAtomics",
|
builder.Pass("vertexPipelineStoresAndAtomics",
|
||||||
"supported by driver (not currently enabled by the DirectVulkan backend)");
|
"supported by driver (not currently enabled by the DirectVulkan backend)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("vertexPipelineStoresAndAtomics",
|
builder.FailOptional("vertexPipelineStoresAndAtomics",
|
||||||
"unsupported; shaders that write storage buffers from the vertex stage will not work");
|
"unsupported; a shader that writes a storage buffer or runs an atomic "
|
||||||
|
"from the vertex stage cannot build a pipeline, and the write cannot be "
|
||||||
|
"moved to another stage without changing what the shader does");
|
||||||
}
|
}
|
||||||
if (features.fillModeNonSolid == VK_TRUE) {
|
if (features.fillModeNonSolid == VK_TRUE) {
|
||||||
builder.Pass("fillModeNonSolid", "glPolygonMode GL_LINE/GL_POINT rasterization supported");
|
builder.Pass("fillModeNonSolid", "glPolygonMode GL_LINE/GL_POINT rasterization supported");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("fillModeNonSolid",
|
builder.FailOptional("fillModeNonSolid",
|
||||||
"unsupported; glPolygonMode GL_LINE/GL_POINT falls back to GL_FILL (no wireframe/point "
|
"unsupported; glPolygonMode GL_LINE/GL_POINT silently falls back to "
|
||||||
"rasterization)");
|
"GL_FILL, and wireframe/point rasterization cannot be rebuilt out of "
|
||||||
|
"the triangle pipeline");
|
||||||
}
|
}
|
||||||
if (features.independentBlend == VK_TRUE) {
|
if (features.independentBlend == VK_TRUE) {
|
||||||
builder.Pass("independentBlend", "per-draw-buffer glColorMaski and indexed blend state supported");
|
builder.Pass("independentBlend", "per-draw-buffer glColorMaski and indexed blend state supported");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("independentBlend",
|
builder.FailOptional("independentBlend",
|
||||||
"unsupported; per-draw-buffer glColorMaski falls back to draw buffer 0 for all attachments");
|
"unsupported; every attachment takes draw buffer 0's colour mask and "
|
||||||
|
"blend state, so an MRT pass that configures them separately writes the "
|
||||||
|
"wrong channels to all but one attachment");
|
||||||
}
|
}
|
||||||
if (features.dualSrcBlend == VK_TRUE) {
|
if (features.dualSrcBlend == VK_TRUE) {
|
||||||
builder.Pass("dualSrcBlend", "GL_SRC1_* dual-source blend factors supported");
|
builder.Pass("dualSrcBlend", "GL_SRC1_* dual-source blend factors supported");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw");
|
builder.FailOptional("dualSrcBlend",
|
||||||
|
"unsupported; a draw using a GL_SRC1_* blend factor hard-fails, and a "
|
||||||
|
"second fragment output cannot be produced any other way");
|
||||||
}
|
}
|
||||||
// The Magma counterpart of the GLES "Buffer textures" row, so the two sections can be
|
// The Magma counterpart of the GLES "Buffer textures" row, so the two sections can be
|
||||||
// read side by side. Vulkan has no optional-feature bit here: a uniform texel buffer is
|
// read side by side. Vulkan has no optional-feature bit here: a uniform texel buffer is
|
||||||
@@ -2308,10 +2430,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
"back on its own; a format that refuses the flag is detected at image "
|
"back on its own; a format that refuses the flag is detected at image "
|
||||||
"creation and declines per-slice attachment)");
|
"creation and declines per-slice attachment)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("2D-array-compatible 3D images",
|
builder.FailOptional("2D-array-compatible 3D images",
|
||||||
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour attachments; "
|
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour "
|
||||||
"glFramebufferTextureLayer on a GL_TEXTURE_3D texture is declined for every "
|
"attachments; glFramebufferTextureLayer on a GL_TEXTURE_3D texture "
|
||||||
"slice past the first");
|
"is declined for every slice past the first, and a 3D slice cannot "
|
||||||
|
"be rendered into any other way");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (features.imageCubeArray == VK_TRUE) {
|
if (features.imageCubeArray == VK_TRUE) {
|
||||||
@@ -2319,9 +2442,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
"GL_TEXTURE_CUBE_MAP_ARRAY textures get a Vulkan image and can be sampled and "
|
"GL_TEXTURE_CUBE_MAP_ARRAY textures get a Vulkan image and can be sampled and "
|
||||||
"attached to a framebuffer per layer");
|
"attached to a framebuffer per layer");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("imageCubeArray",
|
builder.FailOptional("imageCubeArray",
|
||||||
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
|
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, "
|
||||||
"one reads nothing and glFramebufferTextureLayer on one is declined");
|
"so sampling one reads nothing and glFramebufferTextureLayer on one is "
|
||||||
|
"declined - there is no substitute image type");
|
||||||
}
|
}
|
||||||
// MobileGL follows the device here: shaderFloat64 decides whether a module keeps its
|
// MobileGL follows the device here: shaderFloat64 decides whether a module keeps its
|
||||||
// 64-bit floats or has them narrowed before pipeline creation (DemoteFloat64Pass). Adreno
|
// 64-bit floats or has them narrowed before pipeline creation (DemoteFloat64Pass). Adreno
|
||||||
@@ -2336,7 +2460,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
"FETCH here, so such a program is narrowed whole exactly as it would be on a "
|
"FETCH here, so such a program is narrowed whole exactly as it would be on a "
|
||||||
"device without the feature"));
|
"device without the feature"));
|
||||||
} else {
|
} else {
|
||||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
// WARN rather than PASS: the device does not support fp64 at all here, and what
|
||||||
|
// stands in for it is a MobileGL pass that narrows the shader. It runs, at single
|
||||||
|
// precision - the definition of a substitute.
|
||||||
|
builder.Warn("fp64", AppendFp64AdvertisementNote(
|
||||||
"demoted to fp32 (device shaderFloat64 = unsupported) - every double / dvec "
|
"demoted to fp32 (device shaderFloat64 = unsupported) - every double / dvec "
|
||||||
"/ dmat in a shader is narrowed to 32 bits before pipeline creation, so such "
|
"/ dmat in a shader is narrowed to 32 bits before pipeline creation, so such "
|
||||||
"shaders BUILD AND RUN at single precision instead of failing to create a "
|
"shaders BUILD AND RUN at single precision instead of failing to create a "
|
||||||
@@ -2369,8 +2496,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
if (shaderDrawParameters) {
|
if (shaderDrawParameters) {
|
||||||
builder.Pass("shaderDrawParameters", "gl_DrawID/gl_BaseVertex/gl_BaseInstance shaders supported");
|
builder.Pass("shaderDrawParameters", "gl_DrawID/gl_BaseVertex/gl_BaseInstance shaders supported");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("shaderDrawParameters",
|
builder.FailOptional("shaderDrawParameters",
|
||||||
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
|
"unavailable; a shader reading gl_DrawID, gl_BaseVertex or "
|
||||||
|
"gl_BaseInstance has no SPIR-V builtin to read them from, so such "
|
||||||
|
"shaders do not work and nothing supplies the values instead");
|
||||||
}
|
}
|
||||||
summary.shaderDrawParametersSupported = shaderDrawParameters;
|
summary.shaderDrawParametersSupported = shaderDrawParameters;
|
||||||
|
|
||||||
@@ -2407,10 +2536,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
"supported; flat varyings take GL's last vertex and transform feedback records "
|
"supported; flat varyings take GL's last vertex and transform feedback records "
|
||||||
"strip/fan triangles in GL's vertex order");
|
"strip/fan triangles in GL's vertex order");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("provokingVertexLast",
|
builder.FailOptional("provokingVertexLast",
|
||||||
"unsupported; flat-shaded varyings take a primitive's first vertex instead of GL's "
|
"unsupported; flat-shaded varyings take a primitive's first vertex "
|
||||||
"last, and transform feedback records TRIANGLE_STRIP/TRIANGLE_FAN triangles rotated "
|
"instead of GL's last, and transform feedback records "
|
||||||
"(e.g. 0,1,2 / 1,3,2 instead of 0,1,2 / 2,1,3)");
|
"TRIANGLE_STRIP/TRIANGLE_FAN triangles rotated (e.g. 0,1,2 / 1,3,2 "
|
||||||
|
"instead of 0,1,2 / 2,1,3). Rewriting the convention would mean "
|
||||||
|
"reordering every index buffer, which MobileGL does not do");
|
||||||
}
|
}
|
||||||
if (provokingVertexLast && !transformFeedbackPreservesProvokingVertex) {
|
if (provokingVertexLast && !transformFeedbackPreservesProvokingVertex) {
|
||||||
builder.Warn("transformFeedbackPreservesProvokingVertex",
|
builder.Warn("transformFeedbackPreservesProvokingVertex",
|
||||||
@@ -2439,9 +2570,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Pass("primitiveTopologyListRestart",
|
builder.Pass("primitiveTopologyListRestart",
|
||||||
"primitive restart supported on list topologies (GL_PRIMITIVE_RESTART)");
|
"primitive restart supported on list topologies (GL_PRIMITIVE_RESTART)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("primitiveTopologyListRestart",
|
builder.FailOptional("primitiveTopologyListRestart",
|
||||||
"unsupported; primitive restart works on strip/fan topologies only, list-topology restart "
|
"unsupported; primitive restart works on strip/fan topologies only, and "
|
||||||
"hard-fails at draw");
|
"a list-topology draw with GL_PRIMITIVE_RESTART enabled hard-fails - "
|
||||||
|
"splitting the index stream on the CPU is not done");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core 1.0 features the backend turns GL stages into pipeline stages with.
|
// Core 1.0 features the backend turns GL stages into pipeline stages with.
|
||||||
@@ -2451,9 +2583,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Pass("tessellationShader",
|
builder.Pass("tessellationShader",
|
||||||
"supported (GL_PATCHES draws run the tessellation control/evaluation stages)");
|
"supported (GL_PATCHES draws run the tessellation control/evaluation stages)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("tessellationShader",
|
builder.FailOptional("tessellationShader",
|
||||||
"unsupported; a program with a tessellation control/evaluation shader cannot build a "
|
"unsupported; a program with a tessellation control/evaluation shader "
|
||||||
"pipeline, so GL_PATCHES draws render nothing");
|
"cannot build a pipeline, so GL_PATCHES draws render nothing and there "
|
||||||
|
"is no stage to run the tessellation on instead");
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool vertexAttributeInstanceRateDivisor = false;
|
Bool vertexAttributeInstanceRateDivisor = false;
|
||||||
@@ -2471,10 +2604,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Pass("vertexAttributeInstanceRateDivisor",
|
builder.Pass("vertexAttributeInstanceRateDivisor",
|
||||||
"supported (glVertexAttribDivisor advances an attribute every N instances)");
|
"supported (glVertexAttribDivisor advances an attribute every N instances)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("vertexAttributeInstanceRateDivisor",
|
builder.FailOptional("vertexAttributeInstanceRateDivisor",
|
||||||
"unsupported; Vulkan's instance input rate can only advance once per instance, so "
|
"unsupported; Vulkan's instance input rate can only advance once per "
|
||||||
"every non-zero glVertexAttribDivisor behaves as 1 and instanced attributes meant to "
|
"instance, so every non-zero glVertexAttribDivisor behaves as 1 and "
|
||||||
"change every N instances change every one");
|
"instanced attributes meant to change every N instances change every "
|
||||||
|
"one - silently wrong geometry, with no substitute fetch rate");
|
||||||
}
|
}
|
||||||
|
|
||||||
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
|
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
|
||||||
@@ -2498,11 +2632,18 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
format("basic subgroup operations in compute, subgroup size {}",
|
format("basic subgroup operations in compute, subgroup size {}",
|
||||||
subgroupProperties.subgroupSize));
|
subgroupProperties.subgroupSize));
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Compute shader subgroup",
|
builder.FailOptional("Compute shader subgroup",
|
||||||
"basic subgroup operations are not usable from compute shaders");
|
"basic subgroup operations are not usable from compute shaders, so "
|
||||||
|
"MobileGL withholds GL_KHR_shader_subgroup and the subgroup "
|
||||||
|
"iteration-render-pass path cannot run; there is no scalar rewrite "
|
||||||
|
"that stands in for a subgroup reduction");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
|
builder.FailOptional("Compute shader subgroup",
|
||||||
|
"subgroup properties could not be queried (no "
|
||||||
|
"vkGetPhysicalDeviceProperties2, or a pre-1.1 device), so MobileGL "
|
||||||
|
"withholds GL_KHR_shader_subgroup and the subgroup paths are "
|
||||||
|
"unavailable whatever the hardware can actually do");
|
||||||
}
|
}
|
||||||
|
|
||||||
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
||||||
@@ -2522,9 +2663,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
if (indexTypeUint8) {
|
if (indexTypeUint8) {
|
||||||
builder.Pass("Index type uint8", "supported (native GL_UNSIGNED_BYTE index buffers)");
|
builder.Pass("Index type uint8", "supported (native GL_UNSIGNED_BYTE index buffers)");
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Index type uint8",
|
builder.FailOptional("Index type uint8",
|
||||||
"not supported; GL_UNSIGNED_BYTE index buffers cannot be drawn (the backend "
|
"not supported; a GL_UNSIGNED_BYTE index buffer cannot be drawn - the "
|
||||||
"has no conversion fallback and asserts on uint8 index draws)");
|
"backend has no widening conversion and asserts on uint8 index draws");
|
||||||
}
|
}
|
||||||
builder.DriverReported("Backend driver reported device", String(properties.deviceName));
|
builder.DriverReported("Backend driver reported device", String(properties.deviceName));
|
||||||
builder.DriverReported("Backend driver reported driver version", driverVersionString + " (vendor-encoded)");
|
builder.DriverReported("Backend driver reported driver version", driverVersionString + " (vendor-encoded)");
|
||||||
@@ -2542,12 +2683,14 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
ProbeVulkanTimerQuery(builder, getInstanceProcAddr, instance, physicalDevice,
|
ProbeVulkanTimerQuery(builder, getInstanceProcAddr, instance, physicalDevice,
|
||||||
graphicsQueueFamilyIndex, graphicsQueueTimestampValidBits, timestampPeriod);
|
graphicsQueueFamilyIndex, graphicsQueueTimestampValidBits, timestampPeriod);
|
||||||
} else {
|
} else {
|
||||||
builder.Warn("Timer queries",
|
builder.FailOptional(
|
||||||
format("timestampValidBits = 0 on the graphics queue family; timestampPeriod = {} ns "
|
"Timer queries",
|
||||||
"per tick; timestamps unsupported on the graphics queue; timer queries "
|
format("timestampValidBits = 0 on the graphics queue family; timestampPeriod = {} ns "
|
||||||
"unavailable",
|
"per tick; the graphics queue cannot write a timestamp at all, so there is "
|
||||||
timestampPeriod) +
|
"nothing to time GPU work with and glBeginQuery(GL_TIME_ELAPSED) has no "
|
||||||
TimerQueryDisabledNote());
|
"substitute",
|
||||||
|
timestampPeriod) +
|
||||||
|
TimerQueryDisabledNote());
|
||||||
}
|
}
|
||||||
if (vkGetPhysicalDeviceFormatPropertiesFn != nullptr) {
|
if (vkGetPhysicalDeviceFormatPropertiesFn != nullptr) {
|
||||||
MG_External::VulkanCapabilities formatProbeCapabilities{};
|
MG_External::VulkanCapabilities formatProbeCapabilities{};
|
||||||
|
|||||||
@@ -7,26 +7,43 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "DriverBugProbes.h"
|
||||||
|
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_Backend/BackendObject.h>
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::SelfTest {
|
namespace MobileGL::MG_Util::SelfTest {
|
||||||
// One row of a backend power-on self-test (POST) report.
|
// One row of a backend power-on self-test (POST) report.
|
||||||
|
//
|
||||||
|
// EVERY CAPABILITY ROW IS PASS, WARN OR FAIL; INFO IS FOR IDENTITY ONLY.
|
||||||
|
// PASS - the backend supports the capability directly.
|
||||||
|
// WARN - not directly, but a MobileGL quirk substitutes and the application still sees
|
||||||
|
// correct behaviour; the detail names the substitute and what it costs.
|
||||||
|
// FAIL - unsupported with no substitute; an application that uses it gets wrong output, a
|
||||||
|
// failed draw, or nothing.
|
||||||
|
// INFO - identity only: renderer name, API version, driver strings, and the strings
|
||||||
|
// MobileGL itself reports to applications. Never a capability answer.
|
||||||
|
// A FAIL row does not by itself mean the backend cannot run - see BackendPostReport::verdict.
|
||||||
struct PostCheck {
|
struct PostCheck {
|
||||||
String name;
|
String name;
|
||||||
String status; // "PASS" | "WARN" | "FAIL" | "INFO"
|
String status; // "PASS" | "WARN" | "FAIL" | "INFO"
|
||||||
String detail;
|
String detail;
|
||||||
// Display ordering rank within a backend section (lower renders first): FAIL,
|
// Display ordering rank within a backend section (lower renders first): FAIL,
|
||||||
// WARN, PASS, INFO, then the device-driver identity strings, then the strings
|
// WARN, PASS, then the device-driver identity strings, then the strings
|
||||||
// MobileGL itself reports to applications. Rows are stable-sorted by this rank
|
// MobileGL itself reports to applications. Rows are stable-sorted by this rank
|
||||||
// before the report is returned; it is not serialized to JSON.
|
// before the report is returned; it is not serialized to JSON.
|
||||||
Int displayRank = 0;
|
Int displayRank = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verdict for one backend's device driver.
|
// Verdict for one backend's device driver, derived from the rows.
|
||||||
// - UNSUPPORTED: a fatal check failed; the backend cannot run on this driver.
|
// - UNSUPPORTED: a REQUIRED capability failed; the backend cannot run on this driver.
|
||||||
// - DEGRADED: every fatal check passed but at least one soft expectation is unmet.
|
// - DEGRADED: every required capability is present, but at least one row is WARN or is a
|
||||||
// - OK: all expectations met.
|
// FAIL on an optional capability - the backend runs, and something an application might
|
||||||
|
// ask for is substituted or missing.
|
||||||
|
// - OK: every row passed.
|
||||||
|
// So a section can carry FAIL rows and still be DEGRADED rather than UNSUPPORTED: a device
|
||||||
|
// with no dual-source blend still runs. Which capabilities are required is decided at the
|
||||||
|
// row (ReportBuilder::Fail vs ReportBuilder::FailOptional in DriverPost.cpp).
|
||||||
// available is false when no probeable driver exists at all (library missing, display
|
// available is false when no probeable driver exists at all (library missing, display
|
||||||
// uninitializable, zero Vulkan physical devices, ...).
|
// uninitializable, zero Vulkan physical devices, ...).
|
||||||
struct BackendPostReport {
|
struct BackendPostReport {
|
||||||
@@ -34,6 +51,17 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
String verdict = "UNSUPPORTED"; // "OK" | "DEGRADED" | "UNSUPPORTED"
|
String verdict = "UNSUPPORTED"; // "OK" | "DEGRADED" | "UNSUPPORTED"
|
||||||
String rendererInfo;
|
String rendererInfo;
|
||||||
Vector<PostCheck> checks;
|
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;
|
Optional<MG_Backend::FormatCapabilityCache> formatCapabilities;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,27 @@ namespace {
|
|||||||
out << '}';
|
out << '}';
|
||||||
}
|
}
|
||||||
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()) {
|
if (report.formatCapabilities.has_value()) {
|
||||||
AppendFormatCapabilitiesJson(out, report.formatCapabilities.value());
|
AppendFormatCapabilitiesJson(out, report.formatCapabilities.value());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,9 +325,31 @@ namespace MobileGL {
|
|||||||
// appending it at the end of the section would make the module invalid. A
|
// appending it at the end of the section would make the module invalid. A
|
||||||
// duplicate OpTypeArray is legal (SPIR-V 2.8 exempts aggregates from the
|
// duplicate OpTypeArray is legal (SPIR-V 2.8 exempts aggregates from the
|
||||||
// uniqueness rule, and so does spirv-val), so no search for an existing one is
|
// uniqueness rule, and so does spirv-val), so no search for an existing one is
|
||||||
// needed; the LENGTH CONSTANT is not exempt, and if the module already declares
|
// needed; the LENGTH CONSTANT is not exempt, so when the module already declares
|
||||||
// it after the block there is nowhere legal to put the array - the block is then
|
// it the pass has to work with the one instruction that exists.
|
||||||
// declined and keeps today's behaviour. Returns 0 for that.
|
//
|
||||||
|
// That instruction is not always in a usable place. GetDefiningInstruction only
|
||||||
|
// honours `position` when it MINTS the constant; when the module already has one
|
||||||
|
// it hands back the existing instruction wherever it happens to sit, and glslang
|
||||||
|
// emits constants in first-use order, so a shader whose first use of the value is
|
||||||
|
// below the counter block declares it below the block. The flattened array would
|
||||||
|
// then forward-reference its own length.
|
||||||
|
//
|
||||||
|
// KHR-GL43.compute_shader.pipeline-compute-chain is exactly that shader: two
|
||||||
|
// counters at offset 8 need a 4-element array, and its `%uint_4` is first used by
|
||||||
|
// a later declaration, so it lands AFTER gl_AtomicCounterBlock_1. Declining there
|
||||||
|
// - which is what this used to do - left the offsets in place, and SPIRV-Cross
|
||||||
|
// then refused the whole stage with "Push constant block cannot be expressed as
|
||||||
|
// neither std430 nor std140", so the chain's first kernel never reached the
|
||||||
|
// driver and every resource it writes stayed at its initial value.
|
||||||
|
//
|
||||||
|
// Moving the constant UP to just before the block is always legal, which is why
|
||||||
|
// this is a relocation and not a second declaration: an OpConstant's only operand
|
||||||
|
// is its result TYPE, and that type already precedes the block (it is the element
|
||||||
|
// type of the counter array the block declares). Every existing use sits after
|
||||||
|
// the constant's old position and therefore after its new one too, so no use is
|
||||||
|
// left dangling - moving a definition earlier in the types/constants section
|
||||||
|
// cannot invalidate anything. Ordering is all that changes; def-use is untouched.
|
||||||
uint32_t CreateCounterArrayTypeBefore(IRContext* context, Instruction* structType,
|
uint32_t CreateCounterArrayTypeBefore(IRContext* context, Instruction* structType,
|
||||||
uint32_t uintTypeId, uint32_t length) {
|
uint32_t uintTypeId, uint32_t length) {
|
||||||
auto* constantMgr = context->get_constant_mgr();
|
auto* constantMgr = context->get_constant_mgr();
|
||||||
@@ -341,7 +363,12 @@ namespace MobileGL {
|
|||||||
if (position == context->types_values_end()) return 0;
|
if (position == context->types_values_end()) return 0;
|
||||||
Instruction* lengthInst = constantMgr->GetDefiningInstruction(lengthConstant, 0, &position);
|
Instruction* lengthInst = constantMgr->GetDefiningInstruction(lengthConstant, 0, &position);
|
||||||
if (lengthInst == nullptr) return 0;
|
if (lengthInst == nullptr) return 0;
|
||||||
if (!DeclaredBefore(context, lengthInst->result_id(), structType->result_id())) return 0;
|
if (!DeclaredBefore(context, lengthInst->result_id(), structType->result_id())) {
|
||||||
|
// Pre-existing constant, declared below the block. Relocate it; see above
|
||||||
|
// for why that is sound. InsertBefore unlinks it from its current spot
|
||||||
|
// first, so this is a move rather than an aliasing second entry.
|
||||||
|
lengthInst->InsertBefore(structType);
|
||||||
|
}
|
||||||
|
|
||||||
const uint32_t arrayTypeId = context->TakeNextId();
|
const uint32_t arrayTypeId = context->TakeNextId();
|
||||||
if (arrayTypeId == 0) return 0;
|
if (arrayTypeId == 0) return 0;
|
||||||
|
|||||||
@@ -239,13 +239,34 @@ public final class PostActivity extends Activity {
|
|||||||
nativeLoaded = true;
|
nativeLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the whole report to logcat. The report is the only machine-readable form of the
|
||||||
|
* POST, and logcat drops everything past roughly 4000 bytes of a single entry - which is
|
||||||
|
* less than one backend section, so a one-call log silently truncated the report to about
|
||||||
|
* the first dozen rows. Each chunk is prefixed with its index so a reader can reassemble
|
||||||
|
* them in order (concatenate the payloads after the "] " separator).
|
||||||
|
*/
|
||||||
|
private static void logReport(String json) {
|
||||||
|
if (json == null) {
|
||||||
|
Log.i(TAG, "<null report>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int chunkSize = 3000;
|
||||||
|
final int chunks = (json.length() + chunkSize - 1) / chunkSize;
|
||||||
|
for (int index = 0; index < chunks; ++index) {
|
||||||
|
final int start = index * chunkSize;
|
||||||
|
final int end = Math.min(start + chunkSize, json.length());
|
||||||
|
Log.i(TAG, "[" + (index + 1) + "/" + chunks + "] " + json.substring(start, end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void runDriverPost() {
|
private static void runDriverPost() {
|
||||||
String json = null;
|
String json = null;
|
||||||
Throwable failure = null;
|
Throwable failure = null;
|
||||||
try {
|
try {
|
||||||
ensureNativeLoaded();
|
ensureNativeLoaded();
|
||||||
json = nativeRunDriverPost();
|
json = nativeRunDriverPost();
|
||||||
Log.i(TAG, json == null ? "<null report>" : json);
|
logReport(json);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
Log.e(TAG, "Driver POST failed", error);
|
Log.e(TAG, "Driver POST failed", error);
|
||||||
failure = error;
|
failure = error;
|
||||||
@@ -385,9 +406,54 @@ public final class PostActivity extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderKnownDriverBugs(backend.optJSONArray("knownDriverBugs"));
|
||||||
renderFormatCapabilities(backend.optJSONObject("formatCapabilities"));
|
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. */
|
/** The MOBILEGL_BACKEND_TYPE value a POST section name stands for, or null. */
|
||||||
private static String backendTypeForSection(String sectionName) {
|
private static String backendTypeForSection(String sectionName) {
|
||||||
switch (sectionName.toLowerCase(Locale.ROOT)) {
|
switch (sectionName.toLowerCase(Locale.ROOT)) {
|
||||||
@@ -731,6 +797,13 @@ public final class PostActivity extends Activity {
|
|||||||
return COLOR_FAIL;
|
return COLOR_FAIL;
|
||||||
case "INFO":
|
case "INFO":
|
||||||
return COLOR_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:
|
default:
|
||||||
return COLOR_TEXT;
|
return COLOR_TEXT;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user