From 23565fcacd5509b08bac209eeda516042082b4f1 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 27 Aug 2026 19:38:39 -0400 Subject: [PATCH] [Fix, Test] (DirectGLES, SelfTest, MG_Test): probe the located-interface-block defect with its controls and cover the strip in both gates --- MobileGL/Config.h | 10 + MobileGL/ConfigLoader.cpp | 1 + MobileGL/MG_IntegrationTest/CMakeLists.txt | 20 + .../Scenarios/UnlocatedIoBlockScenario.cpp | 421 ++++++++++++++++++ .../MG_Test/ShaderTranspiler/CMakeLists.txt | 1 + .../StripIoBlockLocationsTest.cpp | 219 +++++++++ .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 220 ++------- .../MG_Util/BackendLoaders/OpenGL/Loader.h | 6 - MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp | 205 +++++++++ MobileGL/MG_Util/SelfTest/DriverBugProbes.h | 38 ++ 10 files changed, 943 insertions(+), 198 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp create mode 100644 MobileGL/MG_Test/ShaderTranspiler/StripIoBlockLocationsTest.cpp diff --git a/MobileGL/Config.h b/MobileGL/Config.h index b67181d7..014d4c56 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -142,6 +142,16 @@ namespace MobileGL::MG_Config { // descriptor and kills the process. Deviates from spec (Vulkan adds the bias to // OpImageSampleExplicitLod), so it is an avoidance for that stack only. Bool AvoidExplicitLodBias = false; + // MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's + // inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES + // match them by block name and member sequence instead. The Mali ES driver delivers + // nothing at all through a located block once a tessellation or geometry stage is in + // the pipeline; the driver POST measures that and turns this on by itself, so Auto is + // the right setting everywhere. ForceOn exists so the emulation can be exercised on a + // healthy driver - which is what the integration lane does, since llvmpipe and + // lavapipe carry a located block correctly and would otherwise never run this code - + // and ForceOff is the negative control. See StripIoBlockLocationsPass. + QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto; // MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write // GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never // flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 7283791f..084f491a 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -180,6 +180,7 @@ namespace MobileGL::MG_ConfigLoader { features.AvoidSamplerMipmapMinFilter = QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER"); features.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS"); + features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS"); features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 20fceb0b..ef204087 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -91,6 +91,7 @@ add_executable(MobileGLIntegrationTest Scenarios/SsboDeclarationFormScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp Scenarios/IoBlockNameCollisionScenario.cpp + Scenarios/UnlocatedIoBlockScenario.cpp Scenarios/TessellationDrawModeScenario.cpp Scenarios/GeometryDrawModeScenario.cpp Scenarios/PostLinkAttachScenario.cpp @@ -352,6 +353,8 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT "MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV}) mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV}) +mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1" ${MGL_ITEST_COMMON_ENV}) # TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it. set(MGL_ITEST_TIMEOUT 120) @@ -418,6 +421,23 @@ gtest_discover_tests(MobileGLIntegrationTest ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}" ) +# UnlocatedIoBlockScenario with the interface-block location strip PINNED ON, for the same +# reason the depth/stencil entry above pins its emulation: without it this scenario is +# UNFALSIFIABLE on the machines this suite runs on. llvmpipe carries a located interface block +# correctly, so the driver POST that arms the strip on Mali answers "healthy" here and the +# emulation never runs - the ambient registration would be exercising the un-stripped path +# twice and calling it coverage. With the variable set, the blocks really are emitted with no +# location and the assertion is about the spelling the device gets. +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.UnlocatedIoBlocks." + TEST_FILTER "UnlocatedIoBlockScenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}" +) + # AsyncCompileScenario, with asynchronous compilation PINNED ON per backend. # # Not a duplicate of what the two ambient registrations already run: they run whatever diff --git a/MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp new file mode 100644 index 00000000..c3cc5804 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp @@ -0,0 +1,421 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnlocatedIoBlockScenario.cpp +// Copyright (c) 2025-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 +// +// Scenario - AN INTER-STAGE INTERFACE BLOCK STILL FINDS ITS OTHER END WITH ITS LOCATION +// QUALIFIER REMOVED. +// +// The Mali-G1-Ultra ES driver delivers NOTHING through an interface block that carries an +// explicit layout(location=) once a tessellation or geometry stage is in the pipeline: the +// stages compile, the program links with an empty info log, the draw runs, and the consuming +// stage reads zeroes. Measured with no MobileGL in the process - a bare EGL/GLES 3.2 program +// built from the five ESSL stages MobileGL emits reproduces it, and removing the qualifier +// from the blocks (and changing nothing else) makes the same program carry its payload. The +// locations are not the application's in the first place: these shaders declare none, and +// glslang's cross-stage IO resolver invents them. +// +// DirectGLES answers by dropping the decoration for those programs (StripIoBlockLocationsPass), +// leaving ES to match the blocks by block name and member sequence. THAT is what this scenario +// guards: with the strip forced on, a five-stage pipeline whose four block boundaries carry no +// location must still deliver its payload end to end. It is the assertion the affected device +// cannot make about itself in CI, and the one the healthy machines here CAN make - which is +// the opposite of IoBlockNameCollisionScenario's position, where the machines that run it +// cannot reproduce the defect at all. +// +// The strip is armed for this suite by MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1 on the ctest +// entry, because llvmpipe carries a located block correctly and the driver POST would +// therefore never turn the emulation on here. The SAME cases also run under the ambient +// registrations with the emulation off, so both spellings of the interface are covered and a +// regression in either shows up. +// +// Colour code, so a failure names its own cause: +// green - the payload crossed all four stage boundaries, which is the pass. +// blue - the clear colour: nothing was drawn at all (the program did not link, or the +// backend program was rejected and every draw became a no-op). +// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the +// failure is not about interface blocks. +// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back +// zeroed. That is what an interface whose two ends stopped matching looks like. + +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + // NOTHING in these five stages declares a location. Every location the emitted ESSL + // carries is invented by the cross-stage resolver, which is exactly the shape the + // affected driver mishandles and exactly what the strip removes. + // + // Two members per block, of different types, because an interface that is matched by + // name and member sequence rather than by location has to agree on the sequence too - + // a repair that silently reordered or dropped a member would still light up green with + // one member in the block. + const char* const kVertexSource = R"(#version 420 core +out VsData { + vec4 payload; + vec2 tint; +} vs_out; +out float vs_tcs_alive; +void main() +{ + vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0); + vs_out.tint = vec2(0.25, 0.5); + vs_tcs_alive = 1.0; + gl_Position = vec4(0.0, 0.0, 0.0, 1.0); +} +)"; + + const char* const kTessControlSource = R"(#version 420 core +layout(vertices = 1) out; +in VsData { + vec4 payload; + vec2 tint; +} tcs_in[]; +in float vs_tcs_alive[]; +out TcsData { + vec4 payload; + vec2 tint; +} tcs_out[]; +out float tcs_tes_alive[]; +void main() +{ + tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload; + tcs_out[gl_InvocationID].tint = tcs_in[gl_InvocationID].tint; + tcs_tes_alive[gl_InvocationID] = vs_tcs_alive[gl_InvocationID]; + gl_TessLevelOuter[0] = 1.0; + gl_TessLevelOuter[1] = 1.0; + gl_TessLevelOuter[2] = 1.0; + gl_TessLevelOuter[3] = 1.0; + gl_TessLevelInner[0] = 1.0; + gl_TessLevelInner[1] = 1.0; +} +)"; + + // Distinct block names, so this case is about the LOCATION and nothing else; the + // one-name-in-both-directions shape is the case below. + const char* const kDistinctTessEvalSource = R"(#version 420 core +layout(isolines, point_mode) in; +in TcsData { + vec4 payload; + vec2 tint; +} tes_in[]; +in float tcs_tes_alive[]; +out TesData { + vec4 payload; + vec2 tint; +} tes_out; +out float tes_gs_alive; +void main() +{ + tes_out.payload = tes_in[0].payload; + tes_out.tint = tes_in[0].tint; + tes_gs_alive = tcs_tes_alive[0]; +} +)"; + + // The 420pack shape: ONE name for the block this stage consumes and the block it + // produces. Legal desktop GLSL, and the case where the two repairs have to compose - + // the rename gives the two blocks one spelling per producing stage, the strip takes + // their locations off, and the interfaces still have to meet. + const char* const kCollidingTessEvalSource = R"(#version 420 core +layout(isolines, point_mode) in; +in TcsData { + vec4 payload; + vec2 tint; +} tes_in[]; +in float tcs_tes_alive[]; +out TcsData { + vec4 payload; + vec2 tint; +} tes_out; +out float tes_gs_alive; +void main() +{ + tes_out.payload = tes_in[0].payload; + tes_out.tint = tes_in[0].tint; + tes_gs_alive = tcs_tes_alive[0]; +} +)"; + + // One geometry source per evaluation stage, because the block it consumes is named + // after the block the evaluation stage produced. + const char* const kDistinctGeometrySource = R"(#version 420 core +layout(points) in; +layout(triangle_strip, max_vertices = 4) out; +in TesData { + vec4 payload; + vec2 tint; +} gs_in[]; +in float tes_gs_alive[]; +out GsData { + vec4 payload; + vec2 tint; +} gs_out; +out float gs_fs_alive; +void EmitCorner(vec2 corner) +{ + gs_out.payload = gs_in[0].payload; + gs_out.tint = gs_in[0].tint; + gs_fs_alive = tes_gs_alive[0]; + gl_Position = vec4(corner, 0.0, 1.0); + EmitVertex(); +} +void main() +{ + EmitCorner(vec2(-1.0, -1.0)); + EmitCorner(vec2(-1.0, 1.0)); + EmitCorner(vec2( 1.0, -1.0)); + EmitCorner(vec2( 1.0, 1.0)); +} +)"; + + const char* const kCollidingGeometrySource = R"(#version 420 core +layout(points) in; +layout(triangle_strip, max_vertices = 4) out; +in TcsData { + vec4 payload; + vec2 tint; +} gs_in[]; +in float tes_gs_alive[]; +out GsData { + vec4 payload; + vec2 tint; +} gs_out; +out float gs_fs_alive; +void EmitCorner(vec2 corner) +{ + gs_out.payload = gs_in[0].payload; + gs_out.tint = gs_in[0].tint; + gs_fs_alive = tes_gs_alive[0]; + gl_Position = vec4(corner, 0.0, 1.0); + EmitVertex(); +} +void main() +{ + EmitCorner(vec2(-1.0, -1.0)); + EmitCorner(vec2(-1.0, 1.0)); + EmitCorner(vec2( 1.0, -1.0)); + EmitCorner(vec2( 1.0, 1.0)); +} +)"; + + // Green ONLY when both block members arrived: a repair that kept the first member and + // lost the second would otherwise pass. Red when the plain varying is missing too, so + // "the pipeline is broken" and "the block is broken" cannot be confused. + const char* const kFragmentSource = R"(#version 420 core +in GsData { + vec4 payload; + vec2 tint; +} fs_in; +in float gs_fs_alive; +out vec4 fragColor; +void main() +{ + if (gs_fs_alive <= 0.5) { + fragColor = vec4(1.0, 0.0, 0.0, 1.0); + } else if (abs(fs_in.tint.x - 0.25) > 0.01 || abs(fs_in.tint.y - 0.5) > 0.01) { + fragColor = vec4(0.0, 0.0, 0.0, 1.0); + } else { + fragColor = fs_in.payload; + } +} +)"; + + class UnlocatedIoBlockScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + if (!BackendHostsTessellationAndGeometry()) { + GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " (" + << Gl().RendererString() << "); there is no five-stage pipeline to " + << "carry a block through"; + } + } + + void TearDown() override { + if (!Ready()) return; + glUseProgram(0); + for (const GLuint program : m_programs) { + glDeleteProgram(program); + } + m_programs.clear(); + glBindVertexArray(0); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + m_vao = 0; + } + + // Same calibration IoBlockNameCollisionScenario uses, and for the same reason: + // GL_MAX_TESS_GEN_LEVEL is a real backend answer while GL_MAX_GEOMETRY_* are + // frontend constants, so a stack with no five-stage pipeline is recognised by + // trying to build one, not by asking. + static bool BackendHostsTessellationAndGeometry() { + GLint maxTessGenLevel = 0; + glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel); + GLint maxGeometryOutputVertices = 0; + glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices); + while (glGetError() != GL_NO_ERROR) { + } + return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4; + } + + GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) { + const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, + GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, + GL_FRAGMENT_SHADER}; + const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource, + geometrySource, kFragmentSource}; + + GLuint shaders[5] = {0, 0, 0, 0, 0}; + bool ok = true; + for (int i = 0; i < 5; ++i) { + shaders[i] = glCreateShader(stages[i]); + glShaderSource(shaders[i], 1, &sources[i], nullptr); + glCompileShader(shaders[i]); + GLint compiled = 0; + glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled); + if (!compiled) { + m_buildLog = InfoLog(shaders[i], true); + ok = false; + break; + } + } + if (!ok) { + for (const GLuint shader : shaders) { + if (shader != 0) glDeleteShader(shader); + } + return 0; + } + + const GLuint program = glCreateProgram(); + for (const GLuint shader : shaders) { + glAttachShader(program, shader); + } + glLinkProgram(program); + GLint linked = 0; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + for (const GLuint shader : shaders) { + glDeleteShader(shader); + } + if (!linked) { + m_buildLog = InfoLog(program, false); + glDeleteProgram(program); + return 0; + } + m_programs.push_back(program); + return program; + } + + // Clears to BLUE, so "the draw painted nothing" is a colour of its own rather + // than something that could be mistaken for a zeroed payload. + Rgba8 DrawAndReadCentre(GLuint program) const { + glViewport(0, 0, Gl().Width(), Gl().Height()); + glClearColor(0.0f, 0.0f, 1.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glUseProgram(program); + glPatchParameteri(GL_PATCH_VERTICES, 1); + glDrawArrays(GL_PATCHES, 0, 1); + + Rgba8 pixel{}; + glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel); + return pixel; + } + + static bool IsGreen(const Rgba8& pixel) { + return pixel.r < 64 && pixel.g > 192 && pixel.b < 64; + } + + const std::string& BuildLog() const { return m_buildLog; } + + static GLenum FirstGLError() { + const GLenum first = glGetError(); + while (glGetError() != GL_NO_ERROR) { + } + return first; + } + + private: + static std::string InfoLog(GLuint object, bool isShader) { + GLint length = 0; + if (isShader) { + glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length); + } else { + glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length); + } + std::vector log(static_cast(length > 1 ? length : 1), '\0'); + if (isShader) { + glGetShaderInfoLog(object, static_cast(log.size()), nullptr, log.data()); + } else { + glGetProgramInfoLog(object, static_cast(log.size()), nullptr, log.data()); + } + return std::string(log.data()); + } + + GLuint m_vao = 0; + std::vector m_programs; + std::string m_buildLog; + }; + + TEST_F(UnlocatedIoBlockScenario, BlocksCarryTheirPayloadThroughFiveStages) { + if (!Ready()) return; + + const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource); + if (program == 0) { + GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on " + << Gl().BackendName() << ", so there is no block to carry through: " + << BuildLog(); + } + + const Rgba8 centre = DrawAndReadCentre(program); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(IsGreen(centre)) + << "a four-boundary interface-block chain did not deliver its payload: " << centre + << " (blue: nothing drew; red: the plain varying was lost too; black: a block " + "member arrived wrong, i.e. the interface stopped matching)"; + } + + // The two repairs together. The rename is what makes the evaluation stage's two + // TcsData blocks one spelling per producing stage; the strip then takes the locations + // off the names the rename just settled. Either one alone leaves a working program on + // these machines, so this case is here to catch the two of them disagreeing. + TEST_F(UnlocatedIoBlockScenario, BlocksNamedInBothDirectionsStillMeetWithoutLocations) { + if (!Ready()) return; + + if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) { + GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on " + << Gl().BackendName() << ", so there is no block to carry through: " + << BuildLog(); + } + + const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource); + ASSERT_NE(program, 0u) + << "an interface block name reused across the two directions of one stage is legal " + "desktop GLSL, but the program did not build: " + << BuildLog(); + + const Rgba8 centre = DrawAndReadCentre(program); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(IsGreen(centre)) + << "the renamed-and-unlocated interface chain lost its payload: " << centre; + } + + } // namespace +} // namespace MGITest diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index 920b4608..4e184c3f 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable( FlattenFloat64StorageBlockTest.cpp FlattenXfbInterfaceBlocksTest.cpp UniquifyIoBlockNamesTest.cpp + StripIoBlockLocationsTest.cpp LowerViewportIndexTest.cpp ClampMultisampleFetchTest.cpp LegalizeResourceArrayIndexTest.cpp diff --git a/MobileGL/MG_Test/ShaderTranspiler/StripIoBlockLocationsTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/StripIoBlockLocationsTest.cpp new file mode 100644 index 00000000..b9353cc0 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/StripIoBlockLocationsTest.cpp @@ -0,0 +1,219 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/StripIoBlockLocationsTest.cpp +// Copyright (c) 2025-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 + +#include +#include + +#include "Includes.h" +#include "Init.h" +#include +#include +#include + +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; +using MobileGL::MG_Util::ShaderTranspiler::SpvcSession; + +namespace { + Vector CompileToSpirv(GLenum stage, const String& source) { + using namespace MG_Util::ShaderTranspiler; + ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source}; + auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib); + EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log); + if (!shaderResult) return {}; + + ProgramAttrib programAttrib{.shaders = {shaderResult.value()}}; + auto programResult = ShaderCompiler::LinkProgram(programAttrib); + EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log); + if (!programResult) return {}; + + ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()}; + auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log); + if (!binaryResult || binaryResult->empty()) return {}; + return binaryResult->front(); + } + + String Transpile(const Vector& spirv) { + SpvcSession session(spirv, SessionUsageBit::Transpile); + auto essl = ShaderCompiler::DecompileShader(session); + EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log); + return essl ? essl.value() : String{}; + } + + // How many times `needle` occurs in `haystack`. + SizeT CountOf(const String& haystack, const String& needle) { + SizeT count = 0; + for (SizeT at = haystack.find(needle); at != String::npos; at = haystack.find(needle, at + 1)) { + ++count; + } + return count; + } + + // The tessellation evaluation stage of + // KHR-GLxx.shading_language_420pack.length_of_vector_and_matrix_*, reduced to what this + // pass is about: one block consumed, one block produced, a plain varying in each + // direction, and NO location written anywhere in the source. Every location in the + // emitted ESSL is invented by glslang's cross-stage IO resolver. + const char* kTessEvalSource = R"(#version 420 core +layout(isolines, point_mode) in; + +in vec4 tcs_tes_result[]; +out vec4 tes_gs_result; + +in TCSOutputBlock { + vec4 tcs_tes_variable; +} input_block[]; +out TESOutputBlock { + vec4 tes_gs_variable; +} output_block; + +void main() +{ + tes_gs_result = tcs_tes_result[0]; + output_block.tes_gs_variable = input_block[0].tcs_tes_variable; +} +)"; + + // A stage with no interface block at all: the pass must leave its located varyings alone + // and report that it changed nothing, so the caller declines the re-serialised module. + const char* kNoBlockTessEvalSource = R"(#version 420 core +layout(isolines, point_mode) in; + +in vec4 tcs_tes_result[]; +out vec4 tes_gs_result; + +void main() +{ + tes_gs_result = tcs_tes_result[0]; +} +)"; +} // namespace + +class StripIoBlockLocationsTest : public ::testing::Test { +protected: + void SetUp() override { + MobileGL::Initialize(); + m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount(); + } + + void TearDown() override { + EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart) + << "the stripped module did not survive spirv-val"; + } + + Uint64 m_validationFailuresAtStart = 0; +}; + +TEST_F(StripIoBlockLocationsTest, DropsTheQualifierFromBothBlocksAndLeavesVaryingsAlone) { + const Vector input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource); + ASSERT_FALSE(input.empty()); + + // The defect this exists for, pinned before the repair: SPIRV-Cross really does print a + // location on the blocks, and on this driver that is what loses their payload. + const String before = Transpile(input); + EXPECT_NE(before.find(") in TCSOutputBlock"), String::npos) << before; + EXPECT_NE(before.find(") out TESOutputBlock"), String::npos) << before; + + bool strippedAny = false; + Vector output; + ASSERT_TRUE(ShaderCompiler::StripIoBlockLocationsForEssl(input, true, true, strippedAny, output, true)); + ASSERT_FALSE(output.empty()); + EXPECT_TRUE(strippedAny); + + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + ASSERT_TRUE(tools.Validate(output)); + + const String after = Transpile(output); + // The blocks come out bare... + EXPECT_NE(after.find("in TCSOutputBlock"), String::npos) << after; + EXPECT_NE(after.find("out TESOutputBlock"), String::npos) << after; + EXPECT_EQ(after.find(") in TCSOutputBlock"), String::npos) + << "the consumed block still carries a layout qualifier:\n" + << after; + EXPECT_EQ(after.find(") out TESOutputBlock"), String::npos) + << "the produced block still carries a layout qualifier:\n" + << after; + // ...and everything ES matches them by is untouched, which is what makes the unlocated + // interface still find its other end. + EXPECT_NE(after.find("input_block"), String::npos) << after; + EXPECT_NE(after.find("output_block"), String::npos) << after; + EXPECT_NE(after.find("tcs_tes_variable"), String::npos) << after; + EXPECT_NE(after.find("tes_gs_variable"), String::npos) << after; + // The PLAIN varyings keep their locations. They work on the affected driver, and a + // fragment stage's inputs and a vertex stage's attributes are matched by them. + EXPECT_NE(after.find("in vec4 tcs_tes_result"), String::npos) << after; + EXPECT_NE(after.find("out vec4 tes_gs_result"), String::npos) << after; + EXPECT_EQ(CountOf(after, "layout(location"), 2u) + << "exactly the two plain varyings should still be located:\n" + << after; +} + +TEST_F(StripIoBlockLocationsTest, StripsOnlyTheDirectionTheCallerArmed) { + const Vector input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource); + ASSERT_FALSE(input.empty()); + + // A separate-shader-objects program that ENDS at this stage: the block it produces is + // matched, in another program that never saw this decision, by the location alone. Only + // the consumed side may lose its qualifier. + bool strippedAny = false; + Vector output; + ASSERT_TRUE(ShaderCompiler::StripIoBlockLocationsForEssl(input, true, false, strippedAny, output, true)); + ASSERT_FALSE(output.empty()); + EXPECT_TRUE(strippedAny); + + const String after = Transpile(output); + EXPECT_EQ(after.find(") in TCSOutputBlock"), String::npos) << after; + EXPECT_NE(after.find(") out TESOutputBlock"), String::npos) + << "the produced block's location was dropped even though its consumer is elsewhere:\n" + << after; + + // And the mirror image, for a program that BEGINS at this stage. + bool strippedOutputOnly = false; + Vector outputOnly; + ASSERT_TRUE( + ShaderCompiler::StripIoBlockLocationsForEssl(input, false, true, strippedOutputOnly, outputOnly, true)); + ASSERT_FALSE(outputOnly.empty()); + EXPECT_TRUE(strippedOutputOnly); + const String afterOutputOnly = Transpile(outputOnly); + EXPECT_NE(afterOutputOnly.find(") in TCSOutputBlock"), String::npos) << afterOutputOnly; + EXPECT_EQ(afterOutputOnly.find(") out TESOutputBlock"), String::npos) << afterOutputOnly; +} + +TEST_F(StripIoBlockLocationsTest, ReportsNoChangeForAStageWithoutInterfaceBlocks) { + const Vector input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kNoBlockTessEvalSource); + ASSERT_FALSE(input.empty()); + const String before = Transpile(input); + + bool strippedAny = true; // deliberately wrong going in; the pass must clear it + Vector output; + ShaderCompiler::StripIoBlockLocationsForEssl(input, true, true, strippedAny, output, true); + EXPECT_FALSE(strippedAny) << "a stage with no interface block must report nothing stripped, or " + "the caller adopts a re-serialised module for nothing"; + // gl_PerVertex is an Input AND an Output block in this stage and must not be touched; the + // located plain varyings must not be either. Either way the emitted ESSL is unchanged. + if (!output.empty()) { + EXPECT_EQ(Transpile(output), before); + } +} + +TEST_F(StripIoBlockLocationsTest, DeclinesWhenNeitherDirectionIsArmed) { + const Vector input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kTessEvalSource); + ASSERT_FALSE(input.empty()); + + bool strippedAny = true; + Vector output; + EXPECT_FALSE(ShaderCompiler::StripIoBlockLocationsForEssl(input, false, false, strippedAny, output, true)); + EXPECT_FALSE(strippedAny); + EXPECT_TRUE(output.empty()) << "an unarmed call must not even re-serialise the module"; +} diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 42c88547..4e1f228d 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -836,196 +836,6 @@ namespace MobileGL::MG_Util::BackendLoader { return includesBase; } - // Whether an inter-stage interface BLOCK that carries an explicit layout(location=) - // actually transports its payload across a geometry (or tessellation) boundary. - // - // The Mali-G1-Ultra ES driver (r54p1) links such a program with an empty info log, runs - // the draw, and delivers ZEROES to the consuming stage; the identical program with the - // qualifier removed from the blocks carries the payload correctly. That is not a MobileGL - // artefact - a bare EGL/GLES 3.2 program with no MobileGL in the process reproduces it - - // and it is the whole of the KHR-GLxx.shading_language_420pack interface-block group's - // failures on this device. A located block between a VERTEX and a FRAGMENT stage is fine - // on the same driver, so the probe deliberately spans a geometry stage: that is the - // shape that breaks, and answering the narrower question would report a healthy driver. - // - // Probed rather than matched on the renderer string, because "which drivers do this" is - // not knowable and a quirk list is wrong the moment a driver is fixed. A driver that - // cannot run the probe at all (pre-ES 3.2, no geometry stage, missing entry point) is - // reported as HEALTHY: that leaves its ESSL exactly as it is today, which is the answer - // with no behaviour change in it. - Bool ProbeLocatedInterStageIoBlocksTransportPayload(const MG_External::GLESCapabilities& caps, - const MG_External::GLESFunctionsTable& f) { - const Bool esVersionOk = - caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2); - if (!esVersionOk || !f.glCreateShader || !f.glCreateProgram || !f.glGenVertexArrays || - !f.glGenRenderbuffers || !f.glGenFramebuffers || !f.glReadPixels || !f.glDrawArrays) { - MGLOG_I("located-IO-block probe skipped: needs an ES 3.2 context with a geometry stage"); - return true; - } - while (f.glGetError() != GL_NO_ERROR) { - } - - // Position comes from gl_VertexID so the probe needs no vertex buffer, and the block - // payload is two values that survive an 8-bit target exactly (0.25 -> 64, 0.5 -> 128). - // TWO different block names, because the two boundaries this crosses (VS->GS and - // GS->FS) are separate interfaces; a single name would also trip the in/out collision - // UniquifyIoBlockNamesPass exists for and confuse one defect with the other. - const char* vsSource = "#version 320 es\n" - "precision highp float;\n" - "layout(location = 0) out MgProbeBlock { vec2 mg_probeValue; } mg_probeOut;\n" - "void main() {\n" - " vec2 p = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);\n" - " gl_Position = vec4(p, 0.0, 1.0);\n" - " mg_probeOut.mg_probeValue = vec2(0.25, 0.5);\n" - "}\n"; - const char* gsSource = "#version 320 es\n" - "precision highp float;\n" - "layout(triangles) in;\n" - "layout(triangle_strip, max_vertices = 3) out;\n" - "layout(location = 0) in MgProbeBlock { vec2 mg_probeValue; } mg_probeIn[];\n" - "layout(location = 0) out MgProbeBlock2 { vec2 mg_probeValue; } mg_probeOut;\n" - "void main() {\n" - " for (int i = 0; i < 3; ++i) {\n" - " gl_Position = gl_in[i].gl_Position;\n" - " mg_probeOut.mg_probeValue = mg_probeIn[i].mg_probeValue;\n" - " EmitVertex();\n" - " }\n" - "}\n"; - const char* fsSource = "#version 320 es\n" - "precision highp float;\n" - "layout(location = 0) in MgProbeBlock2 { vec2 mg_probeValue; } mg_probeIn;\n" - "layout(location = 0) out vec4 mg_probeColor;\n" - "void main() { mg_probeColor = vec4(mg_probeIn.mg_probeValue, 0.0, 1.0); }\n"; - - const auto compileShader = [&f](GLenum type, const char* src) -> GLuint { - const GLuint shader = f.glCreateShader(type); - if (shader == 0) return 0; - f.glShaderSource(shader, 1, &src, nullptr); - f.glCompileShader(shader); - GLint status = GL_FALSE; - f.glGetShaderiv(shader, GL_COMPILE_STATUS, &status); - if (status != GL_TRUE) { - f.glDeleteShader(shader); - return 0; - } - return shader; - }; - const GLuint vs = compileShader(GL_VERTEX_SHADER, vsSource); - const GLuint gs = compileShader(GL_GEOMETRY_SHADER, gsSource); - const GLuint fs = compileShader(GL_FRAGMENT_SHADER, fsSource); - GLuint program = 0; - if (vs != 0 && gs != 0 && fs != 0) { - program = f.glCreateProgram(); - if (program != 0) { - f.glAttachShader(program, vs); - f.glAttachShader(program, gs); - f.glAttachShader(program, fs); - f.glLinkProgram(program); - GLint status = GL_FALSE; - f.glGetProgramiv(program, GL_LINK_STATUS, &status); - if (status != GL_TRUE) { - f.glDeleteProgram(program); - program = 0; - } - } - } - if (vs != 0) f.glDeleteShader(vs); - if (gs != 0) f.glDeleteShader(gs); - if (fs != 0) f.glDeleteShader(fs); - if (program == 0) { - MGLOG_I("located-IO-block probe skipped: probe program failed to build (vs=%u gs=%u fs=%u)", vs, - gs, fs); - while (f.glGetError() != GL_NO_ERROR) { - } - return true; - } - - // Everything this probe changes is read back first and put back afterwards. The - // capability run owns a context of its own, but a probe that leaves a 1x1 viewport or - // a bound scratch framebuffer behind would be found by whatever draws next rather - // than here, and that is not a bug anyone should have to chase twice. - GLint savedProgram = 0, savedVao = 0, savedDrawFbo = 0, savedReadFbo = 0, savedRenderbuffer = 0; - GLint savedViewport[4] = {0, 0, 0, 0}; - GLfloat savedClearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; - GLboolean savedColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE}; - f.glGetIntegerv(GL_CURRENT_PROGRAM, &savedProgram); - f.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &savedVao); - f.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &savedDrawFbo); - f.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &savedReadFbo); - f.glGetIntegerv(GL_RENDERBUFFER_BINDING, &savedRenderbuffer); - f.glGetIntegerv(GL_VIEWPORT, savedViewport); - f.glGetFloatv(GL_COLOR_CLEAR_VALUE, savedClearColor); - f.glGetBooleanv(GL_COLOR_WRITEMASK, savedColorMask); - // The five raster states that can void the draw and turn a healthy driver into a - // "broken" verdict. Saved, forced off, and put back. - constexpr GLenum kQuietedStates[] = {GL_SCISSOR_TEST, GL_RASTERIZER_DISCARD, GL_CULL_FACE, - GL_DEPTH_TEST, GL_BLEND}; - GLboolean savedStates[5] = {GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE}; - for (SizeT i = 0; i < std::size(kQuietedStates); ++i) { - savedStates[i] = f.glIsEnabled(kQuietedStates[i]); - if (savedStates[i] == GL_TRUE) f.glDisable(kQuietedStates[i]); - } - f.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - - // ES makes a draw on the default vertex array object invalid in a core-profile sense, - // and the default framebuffer may be incomplete (surfaceless contexts), so the probe - // brings both of its own. - GLuint vao = 0, framebuffer = 0, renderbuffer = 0; - f.glGenVertexArrays(1, &vao); - f.glBindVertexArray(vao); - f.glGenRenderbuffers(1, &renderbuffer); - f.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - f.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1); - f.glGenFramebuffers(1, &framebuffer); - f.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); - f.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); - f.glViewport(0, 0, 1, 1); - f.glUseProgram(program); - f.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - f.glClear(GL_COLOR_BUFFER_BIT); - f.glDrawArrays(GL_TRIANGLES, 0, 3); - - Bool transports = true; - const GLenum drawError = f.glGetError(); - if (drawError == GL_NO_ERROR) { - GLubyte pixel[4] = {0, 0, 0, 0}; - f.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); - if (f.glGetError() == GL_NO_ERROR) { - // Exactly the two values the vertex stage wrote, with one bit of slack for a - // driver that rounds the 8-bit conversion the other way. A stage that received - // nothing reads 0/0, which is nowhere near either. - transports = pixel[0] >= 0x3f && pixel[0] <= 0x41 && pixel[1] >= 0x7f && pixel[1] <= 0x81; - MGLOG_I("located-IO-block probe: fragment stage received (%u, %u), expected (64, 128)", - pixel[0], pixel[1]); - } else { - MGLOG_I("located-IO-block probe inconclusive: readback failed"); - } - } else { - MGLOG_I("located-IO-block probe inconclusive: draw raised GL error 0x%x", drawError); - } - - f.glUseProgram(static_cast(savedProgram)); - f.glBindFramebuffer(GL_FRAMEBUFFER, 0); - f.glDeleteFramebuffers(1, &framebuffer); - f.glDeleteRenderbuffers(1, &renderbuffer); - f.glBindVertexArray(0); - f.glDeleteVertexArrays(1, &vao); - f.glBindVertexArray(static_cast(savedVao)); - f.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(savedRenderbuffer)); - f.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(savedDrawFbo)); - f.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(savedReadFbo)); - f.glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]); - f.glClearColor(savedClearColor[0], savedClearColor[1], savedClearColor[2], savedClearColor[3]); - f.glColorMask(savedColorMask[0], savedColorMask[1], savedColorMask[2], savedColorMask[3]); - for (SizeT i = 0; i < std::size(kQuietedStates); ++i) { - if (savedStates[i] == GL_TRUE) f.glEnable(kQuietedStates[i]); - } - f.glDeleteProgram(program); - while (f.glGetError() != GL_NO_ERROR) { - } - return transports; - } - // GL 4.6 table 23.65 admits exactly four answers for GL_LAYER_PROVOKING_VERTEX and // GL_VIEWPORT_INDEX_PROVOKING_VERTEX. Anything else means the driver wrote something MobileGL // cannot forward as a convention, and GL_UNDEFINED_VERTEX - a legal answer, not a placeholder @@ -1914,8 +1724,34 @@ namespace MobileGL::MG_Util::BackendLoader { MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s", caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false"); - caps.SupportsLocatedInterStageIoBlocks = - ProbeLocatedInterStageIoBlocksTransportPayload(caps, glesFuncs); + // ForceOn means "emit the blocks unlocated", i.e. treat the driver as NOT supporting + // located blocks - which is why the override reads inverted here. Auto is the probe's + // own answer and is what every real run uses; the two forced settings exist so the + // emulation can be exercised on a healthy driver (the integration lane) and turned + // off again as a negative control. + switch (MG_Config::Features.EsprytUnlocatedIoBlocks) { + case MG_Config::QuirkOverride::ForceOn: + caps.SupportsLocatedInterStageIoBlocks = false; + MGLOG_I(" Located inter-stage interface blocks: forced OFF by " + "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS; the driver was not probed"); + break; + case MG_Config::QuirkOverride::ForceOff: + caps.SupportsLocatedInterStageIoBlocks = true; + MGLOG_I(" Located inter-stage interface blocks: forced ON by " + "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS; the driver was not probed"); + break; + case MG_Config::QuirkOverride::Auto: + default: + // SelfTest::ProbeLocatedIoBlocksLosePayload - the Mali-G1-Ultra ES driver + // delivers nothing through an interface block that carries an explicit + // layout(location=) once a tessellation or geometry stage is in the pipeline. + // Probed with its own controls rather than matched on a renderer string; see + // DriverBugProbes.h for the shape and for why the two controls decide what the + // finding is allowed to claim. + caps.SupportsLocatedInterStageIoBlocks = + !SelfTest::LocatedIoBlocksLosePayload(glesFuncs).detected; + break; + } MGLOG_I(" Located inter-stage interface blocks transport their payload: %s", caps.SupportsLocatedInterStageIoBlocks ? "true" diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index defd5564..d899c32d 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1360,12 +1360,6 @@ namespace MobileGL { // so MG_Test can drive it against a fake GLES functions table. Bool ProbeIndirectInstanceIdIncludesBaseInstance(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs); - // Detects whether an inter-stage interface block that carries an explicit - // layout(location=) actually transports its payload across a geometry boundary (see - // Loader.cpp). Called by FillInGLESCapabilities; exposed so MG_Test can drive it - // against a fake GLES functions table. - Bool ProbeLocatedInterStageIoBlocksTransportPayload(const MG_External::GLESCapabilities& caps, - const MG_External::GLESFunctionsTable& glesFuncs); } // namespace MG_Util::BackendLoader } // namespace MobileGL #undef MOBILEGL_EXTERNAL_GLES diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index 5020c776..3085b67f 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -1686,6 +1686,179 @@ namespace MobileGL::MG_Util::SelfTest { return measurement; } + namespace { + // ===================== LOCATED INTER-STAGE INTERFACE BLOCKS ===================== + + constexpr const char* kIoBlockProbeName = "located interface block"; + // Two values that survive an 8-bit target exactly, so the read is a comparison and not + // a tolerance: 0.25 -> 64, 0.5 -> 128. A stage that received nothing reads 0/0, which is + // nowhere near either. + constexpr GLubyte kIoBlockExpectedR = 0x40; + constexpr GLubyte kIoBlockExpectedG = 0x80; + + // `@BL@` becomes the layout qualifier under test, or nothing at all for the control. + // Position comes from gl_VertexID, so no probe here needs a vertex buffer. + String BuildIoBlockVertexSource(const char* blockQualifier) { + return format("#version 320 es\n" + "precision highp float;\n" + "{}out MgProbeBlock {{ vec2 mg_probeValue; }} mg_probeOut;\n" + "void main() {{\n" + " vec2 mg_p = vec2((gl_VertexID == 1) ? 3.0 : -1.0,\n" + " (gl_VertexID == 2) ? 3.0 : -1.0);\n" + " gl_Position = vec4(mg_p, 0.0, 1.0);\n" + " mg_probeOut.mg_probeValue = vec2(0.25, 0.5);\n" + "}}\n", + blockQualifier); + } + + // The block name changes across the geometry stage, because the two boundaries are two + // separate interfaces; one name would also be the in-and-out-under-one-name shape + // UniquifyIoBlockNamesPass exists for, and confusing one defect with the other is + // exactly what this file's control rule is against. + String BuildIoBlockGeometrySource(const char* blockQualifier) { + return format("#version 320 es\n" + "precision highp float;\n" + "layout(triangles) in;\n" + "layout(triangle_strip, max_vertices = 3) out;\n" + "{0}in MgProbeBlock {{ vec2 mg_probeValue; }} mg_probeIn[];\n" + "{0}out MgProbeBlock2 {{ vec2 mg_probeValue; }} mg_probeOut;\n" + "void main() {{\n" + " for (int i = 0; i < 3; ++i) {{\n" + " gl_Position = gl_in[i].gl_Position;\n" + " mg_probeOut.mg_probeValue = mg_probeIn[i].mg_probeValue;\n" + " EmitVertex();\n" + " }}\n" + "}}\n", + blockQualifier); + } + + String BuildIoBlockFragmentSource(const char* blockQualifier, const char* blockName) { + return format("#version 320 es\n" + "precision highp float;\n" + "{}in {} {{ vec2 mg_probeValue; }} mg_probeIn;\n" + "layout(location = 0) out vec4 mg_probeColor;\n" + "void main() {{ mg_probeColor = vec4(mg_probeIn.mg_probeValue, 0.0, 1.0); }}\n", + blockQualifier, blockName); + } + + // Builds and draws one of the four programs this probe compares and reports whether the + // fragment stage received the payload. `outRan` distinguishes "the payload did not + // arrive" from "this program could not be built or drawn at all" - the second is + // inconclusive and must never become a finding. + Bool IoBlockPayloadArrives(const GLESFunctionsTable& gl, const char* blockQualifier, + Bool withGeometryStage, Bool& outRan) { + outRan = false; + Vector stages; + stages.push_back({GL_VERTEX_SHADER, BuildIoBlockVertexSource(blockQualifier), "vertex"}); + if (withGeometryStage) { + stages.push_back( + {GL_GEOMETRY_SHADER, BuildIoBlockGeometrySource(blockQualifier), "geometry"}); + } + stages.push_back({GL_FRAGMENT_SHADER, + BuildIoBlockFragmentSource(blockQualifier, + withGeometryStage ? "MgProbeBlock2" + : "MgProbeBlock"), + "fragment"}); + + const ProgramBuild build = BuildProgram(gl, stages, kIoBlockProbeName); + if (!build.linked) { + if (build.program != 0) gl.glDeleteProgram(build.program); + return false; + } + + GLuint renderbuffer = 0; + GLuint framebuffer = 0; + Bool arrives = false; + gl.glGenRenderbuffers(1, &renderbuffer); + gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1); + gl.glGenFramebuffers(1, &framebuffer); + gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + renderbuffer); + if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + gl.glUseProgram(build.program); + gl.glViewport(0, 0, 1, 1); + gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + gl.glClear(GL_COLOR_BUFFER_BIT); + Drain(gl); + gl.glDrawArrays(GL_TRIANGLES, 0, 3); + if (gl.glGetError() == GL_NO_ERROR) { + GLubyte pixel[4] = {0, 0, 0, 0}; + gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + if (gl.glGetError() == GL_NO_ERROR) { + outRan = true; + // One bit of slack each way, for a driver that rounds the 8-bit + // conversion the other direction. + arrives = pixel[0] + 1 >= kIoBlockExpectedR && pixel[0] <= kIoBlockExpectedR + 1 && + pixel[1] + 1 >= kIoBlockExpectedG && pixel[1] <= kIoBlockExpectedG + 1; + } + } + } + + if (framebuffer != 0) gl.glDeleteFramebuffers(1, &framebuffer); + if (renderbuffer != 0) gl.glDeleteRenderbuffers(1, &renderbuffer); + gl.glDeleteProgram(build.program); + return arrives; + } + } // namespace + + LocatedIoBlockMeasurement ProbeLocatedIoBlocksLosePayload(const GLESFunctionsTable& gl) { + LocatedIoBlockMeasurement measurement; + if (!HasEveryEntryPoint(gl) || !gl.glRenderbufferStorage || !gl.glFramebufferRenderbuffer || + !gl.glCheckFramebufferStatus || !gl.glReadPixels || !gl.glClearColor || !gl.glClear || + !gl.glDrawArrays || !gl.glViewport) { + return measurement; + } + + SavedState saved; + Save(gl, saved); + GLuint vao = 0; + gl.glGenVertexArrays(1, &vao); + gl.glBindVertexArray(vao); + PrepareForProbeDraw(gl); + if (gl.glColorMask != nullptr) gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + // THE CONTROL, and it runs first: the identical three-stage program with no location on + // the blocks. If THAT cannot carry the payload, this driver's problem is not the + // qualifier and the probe has no finding to make - reporting one would justify dropping + // a qualifier that was never the cause. + Bool controlRan = false; + const Bool controlArrives = IoBlockPayloadArrives(gl, "", true, controlRan); + if (controlRan && controlArrives) { + Bool subjectRan = false; + const Bool subjectArrives = + IoBlockPayloadArrives(gl, "layout(location = 0) ", true, subjectRan); + if (subjectRan && !subjectArrives) { + measurement.detected = true; + // The second control, and the one that scopes the repair: the same located + // block between a vertex and a fragment stage. It arrives on the driver this + // was characterised on, which is why DirectGLES only drops the qualifier for + // programs that have a tessellation or geometry stage. A driver where this one + // ALSO fails is losing payloads the repair does not reach, and the report says + // so rather than implying the fix is complete. + Bool vsFsRan = false; + const Bool vsFsArrives = + IoBlockPayloadArrives(gl, "layout(location = 0) ", false, vsFsRan); + measurement.alsoAffectsVertexToFragment = vsFsRan && !vsFsArrives; + } + } + + if (vao != 0) { + gl.glBindVertexArray(0); + gl.glDeleteVertexArrays(1, &vao); + } + Restore(gl, saved); + Drain(gl); + return measurement; + } + + const LocatedIoBlockMeasurement& LocatedIoBlocksLosePayload(const GLESFunctionsTable& gl) { + // One driver per process, and the answer is structural rather than sampled. + static const LocatedIoBlockMeasurement measurement = ProbeLocatedIoBlocksLosePayload(gl); + return measurement; + } + namespace { Optional ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) { const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl); @@ -1808,6 +1981,37 @@ namespace MobileGL::MG_Util::SelfTest { percentOf(measurement.emittedShapeMismatchedTexels))}; } + Optional ProbeLocatedIoBlockPayloadBug(const GLESFunctionsTable& gl) { + const LocatedIoBlockMeasurement& measurement = LocatedIoBlocksLosePayload(gl); + if (!measurement.detected) return std::nullopt; + String detail = + "an inter-stage interface block that carries an explicit layout(location = N) " + "delivers NOTHING once a geometry (or tessellation) stage is in the pipeline: the " + "stages compile, the program links with an empty info log, the draw raises no " + "error, and the consuming stage reads zeroes. The byte-identical program with the " + "qualifier removed from the blocks carries its payload correctly, which is what " + "makes this a LOCATION defect rather than an interface-block one - blocks " + "themselves work here"; + detail += measurement.alsoAffectsVertexToFragment + ? ". A located block between a VERTEX and a FRAGMENT stage is lost on " + "this driver too, so the defect is wider than the repair below " + "reaches: MobileGL only drops the qualifier for programs that have a " + "tessellation or geometry stage, and a located block in a plain " + "vertex+fragment program is still emitted as the application wrote it" + : ". A located block between a VERTEX and a FRAGMENT stage is delivered " + "correctly on the same driver, which is what scopes the repair"; + detail += + ". MobileGL emits a tessellation/geometry program's interface blocks with no " + "location qualifier at all (StripIoBlockLocationsPass) and lets ES match them by " + "block name and member sequence, which it does; the locations were invented by " + "the cross-stage IO resolver rather than written by the application"; + return DriverBugFinding{"Located inter-stage interface blocks carry no payload", + measurement.alsoAffectsVertexToFragment + ? DriverBugVerdict::Unfixable + : DriverBugVerdict::Fixed, + Move(detail)}; + } + // The table. One row per known driver bug; see the header for how to add a sibling. using DriverBugProbeFn = Optional (*)(const GLESFunctionsTable&); constexpr DriverBugProbeFn kGlesDriverBugProbes[] = { @@ -1818,6 +2022,7 @@ namespace MobileGL::MG_Util::SelfTest { &ProbeImageCoherencyResidualBug, &ProbeExplicitVertexInputLocationCeilingBug, &ProbeLayeredBlitDestinationBug, + &ProbeLocatedIoBlockPayloadBug, }; } // namespace diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index a77b21b0..c7671bd1 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -55,6 +55,44 @@ namespace MobileGL::MG_Util::SelfTest { String detail; }; + // What the located-interface-block probe measured. + struct LocatedIoBlockMeasurement { + // The driver delivers nothing through an inter-stage interface block that carries an + // explicit layout(location=) once a geometry stage is in the pipeline. The only field + // any caller's behaviour depends on. + Bool detected = false; + // ...and it does the same WITHOUT a geometry stage, i.e. between a vertex and a + // fragment stage. False on the device this was characterised on, and reported because + // DirectGLES's repair is scoped to tessellation/geometry programs: a driver that + // answered true here would be losing block payloads the repair does not reach. + Bool alsoAffectsVertexToFragment = false; + }; + + // Draws one full-viewport triangle through VS+GS+FS whose two interface blocks carry an + // explicit layout(location = 0), and reports whether the payload the vertex stage wrote + // reached the fragment stage. + // + // The Mali-G1-Ultra ES driver (r54p1) delivers ZEROES: the stages compile, the program + // links with an empty info log, the draw runs without error, and the block is empty. It is + // the whole of the KHR-GLxx.shading_language_420pack interface-block group's failures on + // that device, and of a further 21 tessellation and geometry bodies beside it. + // + // TWO CONTROLS, and the first is why this is a LOCATION finding rather than a block one: + // (1) the identical three-stage program with the qualifier removed from both blocks must + // deliver its payload - without that, "this driver cannot carry an interface block through + // a geometry stage" would be the claim, which is false and would justify flattening every + // block on the device; and (2) a two-stage vertex-to-fragment program with a LOCATED block + // is measured separately, because that one works on the affected driver and is what scopes + // the repair to programs with a tessellation or geometry stage. + // + // Returns `detected` false when an entry point is missing, when the driver has no geometry + // stage, or when the unlocated control fails - an inconclusive probe must never be reported + // as a bug, and must never arm the repair. Restores every piece of GL state it touches. + LocatedIoBlockMeasurement ProbeLocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl); + + // ProbeLocatedIoBlocksLosePayload(), evaluated at most once per process. + const LocatedIoBlockMeasurement& LocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl); + // Blits one layer of an RGBA8 2D array onto another array's layer 1 and reports whether the // copy landed where it was asked to. Returns true only when the destination layer is ignored // while the control lands correctly.