From e1818d497a0b61f665f6d3d9222331449f37e7ee Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 28 Aug 2026 04:23:36 -0400 Subject: [PATCH] [Test] (ShaderTranspiler): pin the point-size demotion's shapes - capability stripped, carriers named and located past the program's varyings, byte-identical no-ops, whole-struct-copy decline, and the two new L1 key bits --- .../MG_Test/ShaderTranspiler/CMakeLists.txt | 1 + .../ShaderTranspiler/DemotePointSizeTest.cpp | 465 ++++++++++++++++++ .../ShaderTranspiler/TranslationCacheTest.cpp | 55 +++ 3 files changed, 521 insertions(+) create mode 100644 MobileGL/MG_Test/ShaderTranspiler/DemotePointSizeTest.cpp diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index 4e184c3f..6ee4482e 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable( FlattenAtomicCounterBlockTest.cpp WidenImageFormatsTest.cpp GlslangCaptureTest.cpp + DemotePointSizeTest.cpp ) target_include_directories(SpirvPassTest PRIVATE diff --git a/MobileGL/MG_Test/ShaderTranspiler/DemotePointSizeTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/DemotePointSizeTest.cpp new file mode 100644 index 00000000..b7080c84 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/DemotePointSizeTest.cpp @@ -0,0 +1,465 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DemotePointSizeTest.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 "spirv-tools/libspirv.hpp" + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; +using MobileGL::MG_Util::ShaderTranspiler::SpvcSession; + +namespace { + // Compiles and LINKS a whole program, then returns one sanitized module per stage - the + // exact bytes ProgramSpirvTask hands the demotion in production, so every shape assertion + // below is made against what the backends would really receive. + Vector> CompileProgramToSpirv(const Vector>& stages) { + using namespace MG_Util::ShaderTranspiler; + Vector> shaders; + Vector types; + for (const auto& [stage, source] : stages) { + // sourceStr is a StringView; the literals handed in are static, so the view + // stays valid for the whole compile. + ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source}; + auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib); + EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log); + if (!shaderResult) return {}; + shaders.push_back(shaderResult.value()); + types.push_back(stage); + } + ProgramAttrib programAttrib{.shaders = shaders}; + auto programResult = ShaderCompiler::LinkProgram(programAttrib); + EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log); + if (!programResult) return {}; + ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *programResult.value()}; + auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log); + if (!binaryResult) return {}; + Vector> modules = Move(binaryResult.value()); + for (auto& module : modules) { + EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true)); + } + return modules; + } + + String Disassemble(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + String text; + EXPECT_TRUE(tools.Disassemble(spirv, &text)); + return text; + } + + 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{}; + } + + Bool Validates(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + return tools.Validate(spirv); + } + + // The five-stage shape of the KHR-GL4x transform-feedback / tessellation capture bodies: + // the value is WRITTEN in the vertex stage, READ from gl_in and re-written in every stage + // after it, and the rasterized size never matters (the captures run under rasterizer + // discard). This is exactly the class the demotion exists to rescue. + const char* kVertexSource = R"(#version 460 core +void main() { + gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0); + gl_PointSize = 2.0; +} +)"; + + const char* kTessControlSource = R"(#version 460 core +layout(vertices = 3) out; +void main() { + gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; + gl_out[gl_InvocationID].gl_PointSize = gl_in[gl_InvocationID].gl_PointSize + 1.0; + gl_TessLevelOuter[0] = 1.0; + gl_TessLevelOuter[1] = 1.0; + gl_TessLevelOuter[2] = 1.0; + gl_TessLevelInner[0] = 1.0; +} +)"; + + const char* kTessEvalSource = R"(#version 460 core +layout(triangles, point_mode) in; +void main() { + gl_Position = gl_TessCoord.x * gl_in[0].gl_Position + gl_TessCoord.y * gl_in[1].gl_Position + + gl_TessCoord.z * gl_in[2].gl_Position; + gl_PointSize = gl_in[0].gl_PointSize + gl_in[1].gl_PointSize + gl_in[2].gl_PointSize; +} +)"; + + const char* kGeometrySource = R"(#version 460 core +layout(points) in; +layout(points, max_vertices = 1) out; +void main() { + gl_Position = gl_in[0].gl_Position; + gl_PointSize = gl_in[0].gl_PointSize * 2.0; + EmitVertex(); + EndPrimitive(); +} +)"; + + const char* kFragmentSource = R"(#version 460 core +layout(location = 0) out vec4 fragColor; +void main() { fragColor = vec4(1.0); } +)"; + + // A control chain that never touches point size: the demotion must prove it changed + // NOTHING here, byte for byte, because this is the overwhelming majority of programs on + // an affected device. + const char* kPlainTessControlSource = R"(#version 460 core +layout(vertices = 3) out; +void main() { + gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; + gl_TessLevelOuter[0] = 1.0; + gl_TessLevelOuter[1] = 1.0; + gl_TessLevelOuter[2] = 1.0; + gl_TessLevelInner[0] = 1.0; +} +)"; + + const char* kPlainTessEvalSource = R"(#version 460 core +layout(triangles, point_mode) in; +void main() { + gl_Position = gl_in[0].gl_Position; +} +)"; + + const char* kPlainVertexSource = R"(#version 460 core +void main() { + gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0); +} +)"; + + // A tessellation evaluation module reaching PointSize through a WHOLE-STRUCT load - the + // one shape the pass must refuse rather than half-rewrite. glslang never emits it, so it + // is assembled by hand. + const char* kWholeStructCopyTessEvalAsm = R"( + OpCapability Tessellation + OpCapability TessellationPointSize + OpMemoryModel Logical GLSL450 + OpEntryPoint TessellationEvaluation %main "main" %gl_in %out_block + OpExecutionMode %main Triangles + OpExecutionMode %main SpacingEqual + OpExecutionMode %main VertexOrderCcw + OpMemberDecorate %gl_PerVertex 0 BuiltIn Position + OpMemberDecorate %gl_PerVertex 1 BuiltIn PointSize + OpDecorate %gl_PerVertex Block + %void = OpTypeVoid + %fn_ty = OpTypeFunction %void + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 +%gl_PerVertex = OpTypeStruct %v4float %float + %uint = OpTypeInt 32 0 + %uint_32 = OpConstant %uint 32 + %arr = OpTypeArray %gl_PerVertex %uint_32 + %ptr_in_arr = OpTypePointer Input %arr + %gl_in = OpVariable %ptr_in_arr Input + %ptr_out_s = OpTypePointer Output %gl_PerVertex + %out_block = OpVariable %ptr_out_s Output + %ptr_in_s = OpTypePointer Input %gl_PerVertex + %int = OpTypeInt 32 1 + %int_0 = OpConstant %int 0 + %main = OpFunction %void None %fn_ty + %entry = OpLabel + %p = OpAccessChain %ptr_in_s %gl_in %int_0 + %v = OpLoad %gl_PerVertex %p + OpStore %out_block %v + OpReturn + OpFunctionEnd +)"; +} // namespace + +class DemotePointSizeTest : public ::testing::Test { +protected: + void SetUp() override { + MobileGL::Initialize(); + m_validationFailuresBefore = ShaderCompiler::SpirvValidationFailureCount(); + } + void TearDown() override { + EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresBefore) + << "a demoted module did not survive spirv-val"; + } + +private: + Uint64 m_validationFailuresBefore = 0; +}; + +TEST_F(DemotePointSizeTest, DemotesAFiveStageProgramWholesale) { + Vector> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource}, + {GL_TESS_CONTROL_SHADER, kTessControlSource}, + {GL_TESS_EVALUATION_SHADER, kTessEvalSource}, + {GL_GEOMETRY_SHADER, kGeometrySource}, + {GL_FRAGMENT_SHADER, kFragmentSource}}); + ASSERT_EQ(modules.size(), 5u); + const Vector types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, + GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER}; + + // The defect, pinned first: every tessellation/geometry stage really does declare the + // capability the device lacks - the same probe production's declines use. + EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1])); + EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[2])); + EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[3])); + + ShaderCompiler::PointSizeDemotionOutcome outcome; + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + modules, types, true, true, /*captureRequestsPointSize=*/true, outcome, true, true)); + EXPECT_TRUE(outcome.demoted) << outcome.declineDetail; + + // THE PRODUCTION GATE, as the arming guard: after demotion neither decline can arm. + // Magma's refusal and Espryt's missing-extension failure both key off exactly these. + EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1])); + EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[2])); + EXPECT_FALSE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[3])); + for (const auto& module : modules) { + EXPECT_TRUE(Validates(module)); + } + + // The carrier chain, boundary by boundary. No user varyings, so the shared location is 0. + const String vs = Disassemble(modules[0]); + EXPECT_NE(vs.find("OpName %mg_PointSizeIo0"), String::npos) << vs; + EXPECT_NE(vs.find("OpStore %mg_PointSizeIo0"), String::npos) + << "the vertex stage must mirror its built-in into the carrier:\n" + << vs; + EXPECT_NE(vs.find("BuiltIn PointSize"), String::npos) + << "the vertex stage KEEPS its core built-in - only tess/geometry stages demote:\n" + << vs; + + const String tcs = Disassemble(modules[1]); + EXPECT_EQ(tcs.find("OpCapability TessellationPointSize"), String::npos) << tcs; + EXPECT_NE(tcs.find("OpName %mg_PointSizeIo0"), String::npos) << tcs; + EXPECT_NE(tcs.find("OpName %mg_PointSizeIo1"), String::npos) << tcs; + + const String tes = Disassemble(modules[2]); + EXPECT_EQ(tes.find("OpCapability TessellationPointSize"), String::npos) << tes; + EXPECT_NE(tes.find("OpName %mg_PointSizeIo1"), String::npos) << tes; + EXPECT_NE(tes.find("OpName %mg_PointSizeIo2"), String::npos) + << "with a geometry stage present the evaluation stage feeds the Io2 boundary, not the " + "capture carrier:\n" + << tes; + + const String gs = Disassemble(modules[3]); + EXPECT_EQ(gs.find("OpCapability GeometryPointSize"), String::npos) << gs; + EXPECT_NE(gs.find("OpName %mg_PointSizeIo2"), String::npos) << gs; + EXPECT_NE(gs.find("OpName %mg_PointSizeCapture"), String::npos) << gs; + EXPECT_NE(gs.find("OpDecorate %mg_PointSizeCapture Location 0"), String::npos) << gs; + EXPECT_NE(gs.find("OpStore %mg_PointSizeCapture"), String::npos) << gs; + + // The struct keeps its member - declared, decorated, unaccessed - which is the shape a + // point-size-free glslang module already has on every extension-less driver. + EXPECT_NE(tes.find("BuiltIn PointSize"), String::npos) << tes; + + // What SPIRV-Cross then prints: no gl_PointSize anywhere in a demoted stage's ESSL (the + // token DirectGLES's extension gate greps for), the carriers in its place. + const String tesEssl = Transpile(modules[2]); + EXPECT_EQ(tesEssl.find("gl_PointSize"), String::npos) << tesEssl; + EXPECT_NE(tesEssl.find("mg_PointSizeIo1"), String::npos) << tesEssl; + const String gsEssl = Transpile(modules[3]); + EXPECT_EQ(gsEssl.find("gl_PointSize"), String::npos) << gsEssl; + EXPECT_NE(gsEssl.find("mg_PointSizeCapture"), String::npos) << gsEssl; + + // Demotion is idempotent by construction: with the capability gone, a second pass over + // the same modules finds nothing to arm on and must not touch a byte. + Vector> again = modules; + ShaderCompiler::PointSizeDemotionOutcome secondOutcome; + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + again, types, true, true, true, secondOutcome, true, true)); + EXPECT_FALSE(secondOutcome.demoted); + EXPECT_TRUE(secondOutcome.declineDetail.empty()) << secondOutcome.declineDetail; + EXPECT_EQ(again, modules); +} + +TEST_F(DemotePointSizeTest, WithoutAGeometryStageTheEvaluationStageOwnsTheCaptureCarrier) { + Vector> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource}, + {GL_TESS_CONTROL_SHADER, kTessControlSource}, + {GL_TESS_EVALUATION_SHADER, kTessEvalSource}, + {GL_FRAGMENT_SHADER, kFragmentSource}}); + ASSERT_EQ(modules.size(), 4u); + const Vector types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, + GL_FRAGMENT_SHADER}; + ShaderCompiler::PointSizeDemotionOutcome outcome; + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + modules, types, true, true, true, outcome, true, true)); + EXPECT_TRUE(outcome.demoted) << outcome.declineDetail; + + const String tes = Disassemble(modules[2]); + EXPECT_NE(tes.find("OpName %mg_PointSizeCapture"), String::npos) << tes; + EXPECT_NE(tes.find("OpStore %mg_PointSizeCapture"), String::npos) << tes; + EXPECT_EQ(tes.find("OpName %mg_PointSizeIo2"), String::npos) + << "no geometry stage, no Io2 boundary:\n" + << tes; +} + +TEST_F(DemotePointSizeTest, AGeometryOnlyProgramReadsTheVertexBoundary) { + const char* geometryReadingVs = R"(#version 460 core +layout(points) in; +layout(points, max_vertices = 1) out; +void main() { + gl_Position = gl_in[0].gl_Position; + gl_PointSize = gl_in[0].gl_PointSize * 2.0; + EmitVertex(); + EndPrimitive(); +} +)"; + Vector> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource}, + {GL_GEOMETRY_SHADER, geometryReadingVs}, + {GL_FRAGMENT_SHADER, kFragmentSource}}); + ASSERT_EQ(modules.size(), 3u); + const Vector types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER}; + ShaderCompiler::PointSizeDemotionOutcome outcome; + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + modules, types, /*demoteTessellation=*/false, /*demoteGeometry=*/true, false, outcome, true, + true)); + EXPECT_TRUE(outcome.demoted) << outcome.declineDetail; + + const String gs = Disassemble(modules[1]); + EXPECT_NE(gs.find("OpName %mg_PointSizeIo0"), String::npos) + << "the geometry stage's input boundary is fed by the vertex stage:\n" + << gs; + const String vs = Disassemble(modules[0]); + EXPECT_NE(vs.find("OpStore %mg_PointSizeIo0"), String::npos) << vs; +} + +TEST_F(DemotePointSizeTest, TheCarrierLandsPastTheProgramsOwnVaryings) { + const char* vsWithVarying = R"(#version 460 core +out vec4 v_color; +void main() { + gl_Position = vec4(1.0); + gl_PointSize = 3.0; + v_color = vec4(0.5); +} +)"; + const char* gsWithVarying = R"(#version 460 core +layout(points) in; +layout(points, max_vertices = 1) out; +in vec4 v_color[]; +out vec4 g_color; +void main() { + gl_Position = gl_in[0].gl_Position; + gl_PointSize = gl_in[0].gl_PointSize; + g_color = v_color[0]; + EmitVertex(); + EndPrimitive(); +} +)"; + const char* fsWithVarying = R"(#version 460 core +in vec4 g_color; +layout(location = 0) out vec4 fragColor; +void main() { fragColor = g_color; } +)"; + Vector> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, vsWithVarying}, + {GL_GEOMETRY_SHADER, gsWithVarying}, + {GL_FRAGMENT_SHADER, fsWithVarying}}); + ASSERT_EQ(modules.size(), 3u); + const Vector types{GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER}; + ShaderCompiler::PointSizeDemotionOutcome outcome; + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + modules, types, false, true, true, outcome, true, true)); + EXPECT_TRUE(outcome.demoted) << outcome.declineDetail; + + // v_color / g_color occupy location 0, so every carrier must sit at 1 - in every stage, + // because producer and consumer match by location. + const String vs = Disassemble(modules[0]); + EXPECT_NE(vs.find("OpDecorate %mg_PointSizeIo0 Location 1"), String::npos) << vs; + const String gs = Disassemble(modules[1]); + EXPECT_NE(gs.find("OpDecorate %mg_PointSizeIo0 Location 1"), String::npos) << gs; + EXPECT_NE(gs.find("OpDecorate %mg_PointSizeCapture Location 1"), String::npos) << gs; +} + +TEST_F(DemotePointSizeTest, APointSizeFreeProgramStaysByteIdentical) { + Vector> modules = + CompileProgramToSpirv({{GL_VERTEX_SHADER, kPlainVertexSource}, + {GL_TESS_CONTROL_SHADER, kPlainTessControlSource}, + {GL_TESS_EVALUATION_SHADER, kPlainTessEvalSource}, + {GL_FRAGMENT_SHADER, kFragmentSource}}); + ASSERT_EQ(modules.size(), 4u); + const Vector> before = modules; + const Vector types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, + GL_FRAGMENT_SHADER}; + ShaderCompiler::PointSizeDemotionOutcome outcome; + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + modules, types, true, true, false, outcome, true, true)); + EXPECT_FALSE(outcome.demoted); + EXPECT_TRUE(outcome.declineDetail.empty()) << outcome.declineDetail; + EXPECT_EQ(modules, before); +} + +TEST_F(DemotePointSizeTest, AHostingDeviceStaysByteIdentical) { + Vector> modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource}, + {GL_TESS_CONTROL_SHADER, kTessControlSource}, + {GL_TESS_EVALUATION_SHADER, kTessEvalSource}, + {GL_FRAGMENT_SHADER, kFragmentSource}}); + ASSERT_EQ(modules.size(), 4u); + const Vector> before = modules; + const Vector types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, + GL_FRAGMENT_SHADER}; + ShaderCompiler::PointSizeDemotionOutcome outcome; + // Both verdicts say the device hosts the built-in: the un-forced lane's contract. + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + modules, types, false, false, true, outcome, true, true)); + EXPECT_FALSE(outcome.demoted); + EXPECT_EQ(modules, before); + EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[1])) + << "the un-demoted module must still arm the existing declines"; +} + +TEST_F(DemotePointSizeTest, ACaptureRequestForcesTheCarrierOnANonWritingCaptureStage) { + // The control stage writes point size (arming the demotion); the evaluation stage never + // does - but a by-name capture must still find the carrier declared there, holding + // whatever an unwritten varying holds, exactly as the unwritten built-in would have. + Vector> modules = + CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertexSource}, + {GL_TESS_CONTROL_SHADER, kTessControlSource}, + {GL_TESS_EVALUATION_SHADER, kPlainTessEvalSource}, + {GL_FRAGMENT_SHADER, kFragmentSource}}); + ASSERT_EQ(modules.size(), 4u); + const Vector types{GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, + GL_FRAGMENT_SHADER}; + ShaderCompiler::PointSizeDemotionOutcome outcome; + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + modules, types, true, true, /*captureRequestsPointSize=*/true, outcome, true, true)); + EXPECT_TRUE(outcome.demoted) << outcome.declineDetail; + + const String tes = Disassemble(modules[2]); + EXPECT_NE(tes.find("OpName %mg_PointSizeCapture"), String::npos) << tes; + EXPECT_TRUE(Validates(modules[2])); +} + +TEST_F(DemotePointSizeTest, AWholeStructCopyDeclinesTheProgramByteIdentically) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + Vector module; + ASSERT_TRUE(tools.Assemble(kWholeStructCopyTessEvalAsm, &module)); + ASSERT_TRUE(tools.Validate(module)); + + Vector> modules{module}; + const Vector types{GL_TESS_EVALUATION_SHADER}; + ShaderCompiler::PointSizeDemotionOutcome outcome; + ASSERT_TRUE(ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram( + modules, types, true, true, false, outcome, true, true)); + EXPECT_FALSE(outcome.demoted); + EXPECT_FALSE(outcome.declineDetail.empty()) + << "a shape the pass cannot express must say so, not silently no-op"; + EXPECT_EQ(modules[0], module) << "a decline must not leave a half-demoted module behind"; + EXPECT_TRUE(ShaderCompiler::ModuleDeclaresTessellationOrGeometryPointSize(modules[0])) + << "the declined module must still arm the existing honest refusals"; +} diff --git a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp index a8863da3..e0859101 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp @@ -499,6 +499,19 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) { v.nativeFloat64 = true; variants.emplace_back("nativeFloat64", BuildSpirvTranslationKey(v)); } + { // CompileEnv::DemotesTessellationPointSize(): phase B rewrites the cached modules + // under it (the point-size demotion), so one key shape would describe two module + // sets - built-in kept vs carried as a varying with the capability stripped. + SpirvTranslationKeyInputs v = base; + v.demoteTessellationPointSize = true; + variants.emplace_back("demoteTessellationPointSize", BuildSpirvTranslationKey(v)); + } + { // ... and its geometry twin, keyed separately because the ES loader really does + // probe the two extension families independently. + SpirvTranslationKeyInputs v = base; + v.demoteGeometryPointSize = true; + variants.emplace_back("demoteGeometryPointSize", BuildSpirvTranslationKey(v)); + } // ---- inputs the WIDENED payload pulled into the key ---- // They cannot move a word of the generated SPIR-V, but they do shape the reflection the // payload now carries, so they have to split the key. This is the group that would go @@ -651,6 +664,48 @@ TEST_F(TranslationCacheTest, NativeFloat64IsOutOfTheFrontendFingerprintAndInside << "one L1 entry would then describe two different module sets"; } +// The second and third capability bits under the same placement rule as nativeFloat64: +// out of the front-end fingerprint (glslang produces the same thing either way), inside +// the L1 key (phase B's point-size demotion rewrites the cached modules under them). The +// accessor direction is pinned too, because it is INVERTED relative to the params field +// and a swap of the arms would disable the device repair with every rendering test green. +TEST_F(TranslationCacheTest, PointSizeDemotionBitsAreOutOfTheFrontendFingerprintAndInsideTheL1Key) { + CompileEnv none; // no backend at all: never demote, standalone compiles stay standard + CompileEnv hosting; // a backend that hosts the built-in + CompileEnv demoting; // a backend that cannot + hosting.backend = BackendType::DirectVulkan; + demoting.backend = BackendType::DirectVulkan; + demoting.params.SupportsTessellationPointSize = false; + demoting.params.SupportsGeometryPointSize = false; + + EXPECT_FALSE(none.DemotesTessellationPointSize()); + EXPECT_FALSE(none.DemotesGeometryPointSize()); + EXPECT_FALSE(hosting.DemotesTessellationPointSize()); + EXPECT_FALSE(hosting.DemotesGeometryPointSize()); + EXPECT_TRUE(demoting.DemotesTessellationPointSize()); + EXPECT_TRUE(demoting.DemotesGeometryPointSize()); + + EXPECT_EQ(ComputeFrontendCompileEnvFingerprint(hosting), ComputeFrontendCompileEnvFingerprint(demoting)) + << "the point-size capability leaked into the front-end fingerprint"; + EXPECT_NE(ComputeCompileEnvFingerprint(hosting), ComputeCompileEnvFingerprint(demoting)) + << "the whole-environment fingerprint has to notice it - it is a DynamicBackendParameters " + "field, hashed by object representation"; + + const Vector stages{{GL_VERTEX_SHADER, kVertexSource}, + {GL_FRAGMENT_SHADER, kFragmentSource}}; + SpirvTranslationKeyInputs demotedKey = BaselineSpirvInputs(stages); + demotedKey.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(demoting); + demotedKey.demoteTessellationPointSize = demoting.DemotesTessellationPointSize(); + demotedKey.demoteGeometryPointSize = demoting.DemotesGeometryPointSize(); + SpirvTranslationKeyInputs keptKey = BaselineSpirvInputs(stages); + keptKey.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(hosting); + keptKey.demoteTessellationPointSize = hosting.DemotesTessellationPointSize(); + keptKey.demoteGeometryPointSize = hosting.DemotesGeometryPointSize(); + + EXPECT_FALSE(BuildSpirvTranslationKey(demotedKey) == BuildSpirvTranslationKey(keptKey)) + << "one L1 entry would then describe two different module sets"; +} + // The other direction, one case per input that was KEPT. Each is a limit the front end // really consumes - everything BuildTBuiltInResource copies into TBuiltInResource, plus the // two inputs to the reflection vertex-attrib limit - so each must still split the key.