mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 12:18:30 +09:00
[Feat, Perf, Test] (MG_Util, MG_Backend, MG_Test): test-lane SPIR-V validation latch; entry-local Private rewrite, in-chain rect lowering and uniform-Location strip
This commit is contained in:
@@ -205,6 +205,8 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
|
||||
@@ -66,6 +66,11 @@ namespace MobileGL::MG_Config {
|
||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||
// (see MG_Util/Debug/Log.cpp).
|
||||
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
|
||||
// ShaderCompiler without ever running MobileGL::Initialize(), and every
|
||||
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
|
||||
// programmatic override stored here (see ShaderCompiler.cpp,
|
||||
// SpirvValidationEnabled).
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
|
||||
@@ -372,6 +372,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
if (result != SPV_SUCCESS) {
|
||||
// MGLOG_I, not E: at the INFO compile level of the CI/test lanes that arm
|
||||
// the validation switch, MGLOG_E is compiled out (Log.h orders
|
||||
// DEBUG < WARN < ERROR < INFO) and the VUID would never reach a log. The
|
||||
// latch is what a test harness asserts on.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
MGLOG_I(
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||
static_cast<Int>(shaderStage),
|
||||
programExternalIndex,
|
||||
static_cast<Int>(result),
|
||||
diagnostic != nullptr ? diagnostic->position.index : 0,
|
||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>");
|
||||
}
|
||||
MOBILEGL_ASSERT(
|
||||
result == SPV_SUCCESS,
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d line=%zu column=%zu index=%zu msg=%s",
|
||||
@@ -1263,8 +1277,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
// Matches the position-fix pass: this build of spirv-tools asserts rather than
|
||||
// reporting, so validation stays off in the shipping path.
|
||||
// Always off: the optimizer's input validator conflates "input invalid" with
|
||||
// "transform failed", and this call site fails open. Validating lanes check the
|
||||
// FINAL module via ValidateTransformedSpirv, which latches instead of rerouting
|
||||
// control flow.
|
||||
options.set_run_validator(false);
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
@@ -1304,7 +1320,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
|
||||
@@ -1334,7 +1350,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: position fix pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(CreateGlToVulkanPositionFixPass(transformFlags));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
@@ -2525,6 +2545,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||
#else
|
||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||
// lanes, where the DEBUG gate above is compiled out.
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||
}
|
||||
#endif
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
|
||||
@@ -2759,3 +2759,323 @@ void main() {
|
||||
}
|
||||
EXPECT_GE(checked, 7u) << "expected all seven declared inputs to be present in the raw module";
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Storage-class census of module-scope OpVariables plus an OpFunctionCall count -
|
||||
// everything the dead-interface-elimination tests need to see, nothing more.
|
||||
struct SpirvVariableCensus {
|
||||
SizeT inputCount = 0;
|
||||
SizeT outputCount = 0;
|
||||
SizeT privateCount = 0;
|
||||
SizeT functionCallCount = 0;
|
||||
};
|
||||
|
||||
SpirvVariableCensus TakeVariableCensus(const Vector<Uint32>& spirv) {
|
||||
constexpr unsigned kOpVariable = 59, kOpFunctionCall = 57;
|
||||
constexpr unsigned kStorageClassInput = 1, kStorageClassPrivate = 6, kStorageClassOutput = 3;
|
||||
SpirvVariableCensus census;
|
||||
for (SizeT i = 5; i < spirv.size();) { // 5-word header
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpVariable && wordCount >= 4) {
|
||||
switch (spirv[i + 3]) {
|
||||
case kStorageClassInput: ++census.inputCount; break;
|
||||
case kStorageClassOutput: ++census.outputCount; break;
|
||||
case kStorageClassPrivate: ++census.privateCount; break;
|
||||
default: break;
|
||||
}
|
||||
} else if (opcode == kOpFunctionCall) {
|
||||
++census.functionCallCount;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return census;
|
||||
}
|
||||
|
||||
// The exact Iris shim shape that shipped an invalid module for a month: a declared
|
||||
// vertex input whose only use is the initializer of a file-scope global nothing ever
|
||||
// reads, in a shader whose main() still contains calls (which is what used to make
|
||||
// ADCE keep the whole chain alive).
|
||||
constexpr const char* kDeadPrivateChainVertexSource = R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
in vec2 mc_midTexCoord;
|
||||
out vec4 v_Color;
|
||||
vec4 iris_MidTex = vec4(mc_midTexCoord * (1.0 / 32768.0), 0.0, 1.0);
|
||||
vec4 helperTint();
|
||||
void main() {
|
||||
v_Color = helperTint();
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
vec4 helperTint() { return vec4(1.0); }
|
||||
)";
|
||||
|
||||
Vector<Uint32> CompileVertexToRawSpirv(const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
if (!shaderResult) {
|
||||
ADD_FAILURE() << shaderResult.error().log;
|
||||
return {};
|
||||
}
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!programResult) {
|
||||
ADD_FAILURE() << programResult.error().log;
|
||||
return {};
|
||||
}
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER},
|
||||
.program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
if (!binaryResult || binaryResult->size() != 1u) {
|
||||
ADD_FAILURE() << (binaryResult ? "unexpected module count"
|
||||
: binaryResult.error().log);
|
||||
return {};
|
||||
}
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
struct SpirvValidationScope {
|
||||
bool previous;
|
||||
explicit SpirvValidationScope(bool enabled)
|
||||
: previous(MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(enabled);
|
||||
}
|
||||
~SpirvValidationScope() {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(previous);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(ProgramUtilTest, DeadPrivateChainVertexInputIsEliminatedFromOptimizedBinary) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(kDeadPrivateChainVertexSource);
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
const SpirvVariableCensus before = TakeVariableCensus(raw);
|
||||
// Preconditions that make this module exercise the ADCE conservatism gate: the dead
|
||||
// input is present, its Private sink is present, and main() still contains a call.
|
||||
// Four Inputs, not two: the frontend always emits gl_VertexIndex/gl_InstanceIndex
|
||||
// built-ins alongside a_Position and mc_midTexCoord.
|
||||
ASSERT_EQ(before.inputCount, 4u)
|
||||
<< "expected a_Position, mc_midTexCoord, gl_VertexIndex and gl_InstanceIndex in the raw module";
|
||||
ASSERT_GE(before.privateCount, 1u);
|
||||
ASSERT_GE(before.functionCallCount, 1u)
|
||||
<< "helperTint() was inlined by the frontend; this test no longer covers the "
|
||||
<< "entry-point-with-calls shape it exists for";
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
|
||||
const SpirvVariableCensus after = TakeVariableCensus(optimized);
|
||||
EXPECT_EQ(after.inputCount, 1u)
|
||||
<< "mc_midTexCoord feeds only a never-read Private global and must not reach the driver";
|
||||
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String validatorMessages;
|
||||
tools.SetMessageConsumer([&validatorMessages](spv_message_level_t, const char*,
|
||||
const spv_position_t&, const char* message) {
|
||||
if (message != nullptr) validatorMessages += String(message) + "\n";
|
||||
});
|
||||
EXPECT_TRUE(tools.Validate(optimized)) << validatorMessages;
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, DeclaredButUnwrittenOutputSurvivesOptimization) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Chocapic-class packs declare varyings some variants never write while the paired
|
||||
// fragment shader still reads them. The OpVariable (and its Location) must survive the
|
||||
// chain on both backends: Espryt's ESSL link would otherwise fail with "varying not
|
||||
// declared in vertex shader", and Magma's stage-interface contract breaks the same way.
|
||||
// ADCE guarantees this only while remove_outputs stays false - this test freezes that.
|
||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
out vec4 v_Written;
|
||||
out vec4 v_NeverWritten;
|
||||
void main() {
|
||||
v_Written = vec4(1.0);
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
// v_Written, v_NeverWritten, and the gl_PerVertex block are all Output-storage variables.
|
||||
const SpirvVariableCensus before = TakeVariableCensus(raw);
|
||||
ASSERT_GE(before.outputCount, 3u);
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_EQ(TakeVariableCensus(optimized).outputCount, before.outputCount)
|
||||
<< "a declared-but-unwritten output was deleted; a fragment stage reading it now "
|
||||
<< "fails to link (ES) or breaks the Vulkan stage interface";
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, ValidationLatchFlagsInvalidModuleWithoutChangingResults) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Only LIVE inputs, so the chain cannot heal the module by deleting them: both
|
||||
// survive to the output, undecorated, and the output is invalid SPIR-V.
|
||||
Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
in vec4 a_Color;
|
||||
out vec4 v_Color;
|
||||
void main() {
|
||||
v_Color = a_Color;
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
|
||||
// Strip every Input Location decoration - the exact defect class the
|
||||
// TMglGlslIoResolver used to ship ([VUID-StandaloneSpirv-Location-04916]).
|
||||
constexpr unsigned kOpDecorate = 71, kOpVariable = 59;
|
||||
constexpr unsigned kDecorationLocation = 30, kStorageClassInput = 1;
|
||||
std::set<unsigned> inputIds;
|
||||
for (SizeT i = 5; i < raw.size();) {
|
||||
const unsigned wordCount = raw[i] >> 16;
|
||||
const unsigned opcode = raw[i] & 0xFFFFu;
|
||||
ASSERT_GT(wordCount, 0u);
|
||||
if (i + wordCount > raw.size()) break;
|
||||
if (opcode == kOpVariable && wordCount >= 4 && raw[i + 3] == kStorageClassInput) {
|
||||
inputIds.insert(raw[i + 2]);
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
SizeT strippedCount = 0;
|
||||
for (SizeT i = 5; i < raw.size();) {
|
||||
const unsigned wordCount = raw[i] >> 16;
|
||||
const unsigned opcode = raw[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > raw.size()) break;
|
||||
if (opcode == kOpDecorate && wordCount >= 4 && raw[i + 2] == kDecorationLocation &&
|
||||
inputIds.count(raw[i + 1]) != 0) {
|
||||
raw.erase(raw.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
raw.begin() + static_cast<std::ptrdiff_t>(i + wordCount));
|
||||
++strippedCount;
|
||||
continue; // do not advance: the next instruction moved into place
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
ASSERT_GE(strippedCount, 2u) << "expected to strip both live inputs' Location decorations";
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
{
|
||||
// The armed lane: control flow is IDENTICAL to shipping (the wrapper still
|
||||
// succeeds - fail-open call sites downstream must not see a different world),
|
||||
// and the failure latch is the signal. This is the catch that took a device
|
||||
// bisect to find when the validator was off everywhere.
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_GT(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "an invalid optimized module must bump the validation-failure latch";
|
||||
}
|
||||
{
|
||||
// The shipping configuration: same result, no validation, latch untouched.
|
||||
SpirvValidationScope validationOff(false);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// OpTypeImage: result id (+1), sampled type (+2), dim (+3). Dim::Rect == 4.
|
||||
SizeT CountRectImageTypes(const Vector<Uint32>& spirv) {
|
||||
constexpr unsigned kOpTypeImage = 25, kDimRect = 4;
|
||||
SizeT count = 0;
|
||||
for (SizeT i = 5; i < spirv.size();) {
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpTypeImage && wordCount >= 4 && spirv[i + 3] == kDimRect) {
|
||||
++count;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// True when any OpDecorate Location targets a UniformConstant/Uniform-storage
|
||||
// variable ([VUID-StandaloneSpirv-Location-06672]).
|
||||
bool AnyLocationOnUniformStorage(const Vector<Uint32>& spirv) {
|
||||
constexpr unsigned kOpDecorate = 71, kOpVariable = 59, kDecorationLocation = 30;
|
||||
constexpr unsigned kStorageUniformConstant = 0, kStorageUniform = 2;
|
||||
std::set<unsigned> locatedIds;
|
||||
for (SizeT i = 5; i < spirv.size();) {
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpDecorate && wordCount >= 4 && spirv[i + 2] == kDecorationLocation) {
|
||||
locatedIds.insert(spirv[i + 1]);
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
for (SizeT i = 5; i < spirv.size();) {
|
||||
const unsigned wordCount = spirv[i] >> 16;
|
||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
if (opcode == kOpVariable && wordCount >= 4 &&
|
||||
(spirv[i + 3] == kStorageUniformConstant || spirv[i + 3] == kStorageUniform) &&
|
||||
locatedIds.count(spirv[i + 2]) != 0) {
|
||||
return true;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(ProgramUtilTest, RectangleSamplerModuleLeavesTheChainVulkanLegal) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Dim::Rect is invalid under every Vulkan environment; the lowering used to run
|
||||
// only in the backends, i.e. AFTER the chain whose output the validating lanes
|
||||
// check. It now runs inside the chain, so the driver-bound bytes are rect-free.
|
||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
uniform sampler2DRect uRect;
|
||||
out vec4 v_Color;
|
||||
void main() {
|
||||
v_Color = texture(uRect, a_Position.xy);
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_GE(CountRectImageTypes(raw), 1u) << "glslang no longer emits Dim::Rect for sampler2DRect";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_EQ(CountRectImageTypes(optimized), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "a rectangle module must leave the chain valid, not latched as a failure";
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, ExplicitSamplerLocationIsStrippedFromTheOptimizedBinary) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// glslang's relaxed GL path keeps layout(location=N) on the UniformConstant
|
||||
// variable, which Vulkan forbids; nothing downstream reads it (GL locations come
|
||||
// from phase-A reflection, Vulkan bindings go by name).
|
||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
||||
in vec3 a_Position;
|
||||
layout(location = 5) uniform sampler2D uTex;
|
||||
out vec4 v_Color;
|
||||
void main() {
|
||||
v_Color = texture(uTex, a_Position.xy);
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
)");
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_TRUE(AnyLocationOnUniformStorage(raw))
|
||||
<< "glslang no longer keeps the explicit uniform location; the strip pass may be obsolete";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
EXPECT_FALSE(AnyLocationOnUniformStorage(optimized));
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the stripped module must validate clean";
|
||||
}
|
||||
|
||||
@@ -54,9 +54,7 @@ namespace MobileGL::MG_Util::Async {
|
||||
// calling eglTerminate, which is the norm for a test binary and legal
|
||||
// for an application. Registered here, during main, so it runs before
|
||||
// the destructors of statics constructed at load time.
|
||||
if (ShaderCompilePool* pool = g_processPool.load(std::memory_order_acquire)) {
|
||||
pool->StopAndDrain();
|
||||
}
|
||||
ShaderCompilePool::StopAndDrainProcessPoolAtExit();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -345,4 +343,10 @@ namespace MobileGL::MG_Util::Async {
|
||||
// eglInitialize to get its worker threads back, the re-arm belongs in
|
||||
// MobileGL::Initialize(), next to glslang::InitializeProcess().
|
||||
}
|
||||
|
||||
void ShaderCompilePool::StopAndDrainProcessPoolAtExit() {
|
||||
if (ShaderCompilePool* pool = g_processPool.load(std::memory_order_acquire)) {
|
||||
pool->StopAndDrain();
|
||||
}
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::Async
|
||||
|
||||
@@ -109,6 +109,13 @@ namespace MobileGL::MG_Util::Async {
|
||||
// but they share glslang's process globals, which teardown is about to free.
|
||||
void StopAndDrain();
|
||||
|
||||
// StopAndDrain() on the process-wide pool if one was ever created; never creates
|
||||
// one. For extra atexit sentinels owned by other subsystems (the SPIR-V validation
|
||||
// switch registers one after forcing spirv-tools' lazy function-local tables into
|
||||
// existence, so the drain is sequenced before those tables' destructors - a worker
|
||||
// mid-Validate would otherwise touch freed memory during process exit).
|
||||
static void StopAndDrainProcessPoolAtExit();
|
||||
|
||||
Uint GetThreadCount() const;
|
||||
Uint GetMaxConcurrency() const;
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
@@ -30,9 +32,13 @@
|
||||
|
||||
#include "ShaderSourceProcessor.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
@@ -354,16 +360,215 @@ namespace MobileGL {
|
||||
return allSpirv;
|
||||
}
|
||||
|
||||
// -1 unresolved, 0 off, 1 on. Resolved once from MOBILEGL_VALIDATE_SPIRV on first
|
||||
// use. A live getenv rather than an MG_Config::Features field, for the same reason
|
||||
// Config.h already exempts MOBILEGL_LOG_FILE_PATH: suites like SpirvPassTest never
|
||||
// run MobileGL::Initialize(), and every Initialize() re-runs MG_ConfigLoader::Init,
|
||||
// which would clobber a programmatic override stored in the feature table.
|
||||
static std::atomic<int> g_validateSpirv{-1};
|
||||
// Total validation failures observed this process. This latch - not the wrappers'
|
||||
// return values - is the test-lane signal: validation must never change what a
|
||||
// wrapper returns, or the validating lanes would render differently from the
|
||||
// shipping configuration (fail-open call sites would silently substitute an
|
||||
// earlier-stage module).
|
||||
static std::atomic<Uint64> g_spirvValidationFailures{0};
|
||||
|
||||
namespace {
|
||||
// Test lanes (desktop/CI/WSL) validate by default; device builds do not -
|
||||
// validation costs real time per module, and on device the driver is the
|
||||
// final validator anyway. MOBILEGL_VALIDATE_SPIRV overrides in either
|
||||
// direction, using the ConfigLoader truthy rule.
|
||||
constexpr bool kValidateSpirvDefault =
|
||||
#if defined(__ANDROID__)
|
||||
false;
|
||||
#else
|
||||
true;
|
||||
#endif
|
||||
|
||||
bool IsTruthySpirvEnvValue(const char* value) {
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
String lowered(value);
|
||||
for (auto& c : lowered) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return lowered != "0" && lowered != "false";
|
||||
}
|
||||
|
||||
// spirv-tools' validator lazily constructs function-local static tables on
|
||||
// its first run, which on this codebase happens on a ShaderCompilePool
|
||||
// worker. Function-local statics are destroyed in reverse construction
|
||||
// order, so those tables would die BEFORE the pool's own atexit sentinel
|
||||
// (registered at first pool use) gets to drain the workers - and a worker
|
||||
// mid-Validate would then read freed memory during process exit. Pin the
|
||||
// order instead: force the tables into existence now, then register a
|
||||
// second drain handler; being registered after the tables' destructors, it
|
||||
// runs before them.
|
||||
void PinValidatorTablesForProcessExit() {
|
||||
static std::once_flag pinnedOnce;
|
||||
std::call_once(pinnedOnce, [] {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
Vector<Uint32> warmup;
|
||||
// The module is shaped to reach BOTH lazily-constructed tables in
|
||||
// the vendored validate_id.cpp: a type-generating operand pins
|
||||
// InstructionCanHaveTypeOperand's allow-set, and the OpExtInst use
|
||||
// of the TYPELESS %glsl import is the one path into
|
||||
// InstructionRequiresTypeOperand's deny-set (its call site is
|
||||
// guarded on a referenced def with no result type). A straight-line
|
||||
// module without it leaves the deny-set to be built later on a pool
|
||||
// worker, re-creating the exit-order hazard for that one table.
|
||||
if (tools.Assemble("OpCapability Shader\n"
|
||||
"%glsl = OpExtInstImport \"GLSL.std.450\"\n"
|
||||
"OpMemoryModel Logical GLSL450\n"
|
||||
"OpEntryPoint GLCompute %main \"main\"\n"
|
||||
"OpExecutionMode %main LocalSize 1 1 1\n"
|
||||
"%void = OpTypeVoid\n"
|
||||
"%fn = OpTypeFunction %void\n"
|
||||
"%float = OpTypeFloat 32\n"
|
||||
"%c = OpConstant %float 1\n"
|
||||
"%main = OpFunction %void None %fn\n"
|
||||
"%entry = OpLabel\n"
|
||||
"%abs = OpExtInst %float %glsl FAbs %c\n"
|
||||
"OpReturn\n"
|
||||
"OpFunctionEnd\n",
|
||||
&warmup)) {
|
||||
tools.Validate(warmup);
|
||||
}
|
||||
std::atexit(+[] {
|
||||
// Flip validation off first: a validator table this warmup does
|
||||
// not know about (a future spirv-tools bump) would still be
|
||||
// destroyed before this handler, and workers must stop entering
|
||||
// Validate before the drain waits for them.
|
||||
g_validateSpirv.store(0, std::memory_order_release);
|
||||
Async::ShaderCompilePool::StopAndDrainProcessPoolAtExit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
spvtools::MessageConsumer MakeSpirvMessageConsumer(const char* site) {
|
||||
return [site](spv_message_level_t level, const char* /*source*/,
|
||||
const spv_position_t& position, const char* message) {
|
||||
const char* text = message ? message : "";
|
||||
switch (level) {
|
||||
case SPV_MSG_FATAL:
|
||||
case SPV_MSG_INTERNAL_ERROR:
|
||||
case SPV_MSG_ERROR:
|
||||
// MGLOG_I, deliberately: at the INFO compile level of every
|
||||
// CI/WSL/retrace build, MGLOG_E and MGLOG_W are compiled out
|
||||
// (Log.h orders DEBUG < WARN < ERROR < INFO) and the VUID
|
||||
// would never reach a log.
|
||||
MGLOG_I("[spirv] %s: %s (word index %zu)", site, text, position.index);
|
||||
break;
|
||||
default:
|
||||
MGLOG_D("[spirv] %s: %s", site, text);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Validation is decoupled from control flow on purpose: a failure logs and
|
||||
// bumps the latch, and the caller proceeds exactly as the shipping (non-
|
||||
// validating) configuration would. Tests assert on the latch delta.
|
||||
void ValidateOrLatch(const char* site, const Vector<Uint32>& binary) {
|
||||
if (!ShaderCompiler::SpirvValidationEnabled()) {
|
||||
return;
|
||||
}
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer(MakeSpirvMessageConsumer(site));
|
||||
if (!tools.Validate(binary)) {
|
||||
MGLOG_I("[spirv] %s: produced a module that fails validation (failure #%llu)",
|
||||
site,
|
||||
static_cast<unsigned long long>(
|
||||
ShaderCompiler::NoteSpirvValidationFailure()));
|
||||
}
|
||||
}
|
||||
|
||||
// Shared tail for every Optimizer wrapper in this file. The optimizer's own
|
||||
// input validator stays off even in validating lanes, for two reasons: its
|
||||
// failure is indistinguishable from a transform failure (Optimizer::Run
|
||||
// returns false before BuildModule), and the FIRST wrapper's input is
|
||||
// glslang output that is legitimately not Vulkan-clean yet. What gets
|
||||
// validated is each wrapper's OUTPUT - the only bytes a driver can ever
|
||||
// receive. The message consumer is installed unconditionally: without one,
|
||||
// spirv-tools drops pass diagnostics on the floor.
|
||||
bool RunOptimizerChecked(const char* site, spvtools::Optimizer& optimizer,
|
||||
const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
optimizer.SetMessageConsumer(MakeSpirvMessageConsumer(site));
|
||||
if (!optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options)) {
|
||||
return false;
|
||||
}
|
||||
ValidateOrLatch(site, outputBinary);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ShaderCompiler::SpirvValidationEnabled() {
|
||||
int state = g_validateSpirv.load(std::memory_order_acquire);
|
||||
if (state < 0) {
|
||||
const char* env = std::getenv("MOBILEGL_VALIDATE_SPIRV");
|
||||
const bool resolved = env != nullptr ? IsTruthySpirvEnvValue(env) : kValidateSpirvDefault;
|
||||
int expected = -1;
|
||||
g_validateSpirv.compare_exchange_strong(expected, resolved ? 1 : 0,
|
||||
std::memory_order_acq_rel);
|
||||
state = g_validateSpirv.load(std::memory_order_acquire);
|
||||
if (state == 1) {
|
||||
PinValidatorTablesForProcessExit();
|
||||
}
|
||||
}
|
||||
return state == 1;
|
||||
}
|
||||
|
||||
void ShaderCompiler::SetSpirvValidationEnabled(bool enabled) {
|
||||
g_validateSpirv.store(enabled ? 1 : 0, std::memory_order_release);
|
||||
if (enabled) {
|
||||
PinValidatorTablesForProcessExit();
|
||||
}
|
||||
}
|
||||
|
||||
Uint64 ShaderCompiler::NoteSpirvValidationFailure() {
|
||||
return g_spirvValidationFailures.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
}
|
||||
|
||||
Uint64 ShaderCompiler::SpirvValidationFailureCount() {
|
||||
return g_spirvValidationFailures.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
|
||||
// ADCE refuses to treat a Private global as deletable while the entry point
|
||||
// still contains any OpFunctionCall (IsLocalVar -> IsEntryPointWithNoCalls), so
|
||||
// a dead vertex input feeding a never-read Private shim used to survive the
|
||||
// whole chain (the Chocapic13 shadow.vsh mc_midTexCoord/iris_MidTex case).
|
||||
// Rewriting entry-point-owned Private variables to Function storage first
|
||||
// satisfies ADCE without inlining: over 521 real Iris modules the rewrite
|
||||
// captured 17 of the 21 extra dead interface variables exhaustive inlining
|
||||
// would, while shrinking the corpus 8% - inlining grew it 20% with a 5.3x
|
||||
// worst-case module and no additional GPU-side benefit.
|
||||
optimizer.RegisterPass(PrivateToEntryLocalPass::CreatePrivateToEntryLocalPass());
|
||||
// Keep the one-arg overload: remove_outputs must stay false, forever. Output
|
||||
// variables on the entry-point interface are ADCE's only unconditional live
|
||||
// roots; XFB capture resolves varyings by OpName after this chain, and the
|
||||
// VS-out/FS-in interface contract on both backends depends on declared outputs
|
||||
// surviving even when never stored.
|
||||
optimizer.RegisterPass(CreateAggressiveDCEPass(false));
|
||||
// Complementary to ADCE, not redundant: ADCE can never delete or delist an
|
||||
// Output (see above), so never-written outputs are trimmed from the
|
||||
// OpEntryPoint operand list here.
|
||||
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
||||
// The two module-legality repairs, so the chain's output - the bytes every
|
||||
// consumer downstream sees - is valid Vulkan SPIR-V. Rect lowering used to
|
||||
// live only in the backends; a validating lane would flag every rectangle
|
||||
// module long before the backend got the chance to fix it, and the backend
|
||||
// calls remain as no-ops on the now rect-free modules.
|
||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
||||
optimizer.RegisterPass(StripUniformLocationsPass::CreateStripUniformLocationsPass());
|
||||
optimizer.RegisterPass(FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass());
|
||||
optimizer.RegisterPass(RenameSamplerFunctionParameterPass::CreateRenameSamplerFunctionParameterPass());
|
||||
optimizer.RegisterPass(
|
||||
@@ -371,104 +576,88 @@ namespace MobileGL {
|
||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("LowerDrawParametersForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("PackDoubleVertexInputsForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("StripUboMemberRelaxedPrecisionForEssl", optimizer,
|
||||
inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("StripNoPerspectiveForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("EmulateNoPerspectiveForEssl", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("RebaseInstanceIndexForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
return RunOptimizerChecked("DecoratePositionInvariantForVulkan", optimizer, inputBinary,
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||
@@ -598,6 +787,9 @@ namespace MobileGL {
|
||||
}
|
||||
outputBinary.insert(outputBinary.begin() + static_cast<std::ptrdiff_t>(capabilityInsertOffset),
|
||||
addedCapabilities.begin(), addedCapabilities.end());
|
||||
// Hand-rolled word walk, so no Optimizer wrapper ever sees this rewrite;
|
||||
// check the modified module explicitly in validating lanes.
|
||||
ValidateOrLatch("UseUnformattedFloatStorageImagesForVulkan", outputBinary);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,27 @@ namespace MobileGL {
|
||||
// it, the second eglInitialize of a process comes back up unwarmed and with
|
||||
// no way left to warm it.
|
||||
static void ResetPrewarmLatch();
|
||||
|
||||
// Test-environment SPIR-V validation. When enabled, every Optimizer wrapper
|
||||
// in this file validates its OUTPUT binary - the bytes a driver can actually
|
||||
// receive - and a failure logs the VUID (via MGLOG_I; see the consumer for
|
||||
// why not MGLOG_E) and bumps the failure latch below WITHOUT changing the
|
||||
// wrapper's return value: control flow must stay identical between the
|
||||
// validating and shipping configurations, or fail-open call sites would make
|
||||
// the two render differently. Resolved lazily from MOBILEGL_VALIDATE_SPIRV;
|
||||
// defaults on for desktop/CI/WSL builds and off for device (__ANDROID__)
|
||||
// builds. The setter wins over the environment and is safe to call from test
|
||||
// fixtures at any time.
|
||||
static bool SpirvValidationEnabled();
|
||||
static void SetSpirvValidationEnabled(bool enabled);
|
||||
|
||||
// The test-lane enforcement signal: total validation failures observed this
|
||||
// process. Tests snapshot it, run the operation under scrutiny, and assert
|
||||
// on the delta. NoteSpirvValidationFailure is for validation done outside
|
||||
// this file (ProgramFactory::ValidateTransformedSpirv); it returns the new
|
||||
// total.
|
||||
static Uint64 SpirvValidationFailureCount();
|
||||
static Uint64 NoteSpirvValidationFailure();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.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
|
||||
//
|
||||
// Derived from SPIRV-Tools' PrivateToLocalPass (source/opt/private_to_local_pass.cpp,
|
||||
// Copyright (c) 2017 Google Inc., Apache License 2.0). The one behavioral difference is
|
||||
// the entry-point restriction in FindEntryLocalFunction; see the header for why.
|
||||
|
||||
#include "PrivateToEntryLocalPass.h"
|
||||
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/spirv_constant.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
constexpr uint32_t kVariableStorageClassInIdx = 0;
|
||||
constexpr uint32_t kSpvTypePointerTypeIdInIdx = 1;
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status PrivateToEntryLocalPass::Process() {
|
||||
// Private variables require the Shader capability; with Addresses the
|
||||
// rewrite below is not guaranteed sound (variable pointers may escape).
|
||||
if (context()->get_feature_mgr()->HasCapability(spv::Capability::Addresses)) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
std::vector<std::pair<Instruction*, Function*>> variablesToMove;
|
||||
std::unordered_set<uint32_t> localizedVariables;
|
||||
for (auto& inst : context()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) {
|
||||
continue;
|
||||
}
|
||||
if (spv::StorageClass(inst.GetSingleWordInOperand(kVariableStorageClassInIdx)) !=
|
||||
spv::StorageClass::Private) {
|
||||
continue;
|
||||
}
|
||||
Function* targetFunction = FindEntryLocalFunction(inst);
|
||||
if (targetFunction != nullptr) {
|
||||
variablesToMove.push_back({&inst, targetFunction});
|
||||
}
|
||||
}
|
||||
|
||||
const bool modified = !variablesToMove.empty();
|
||||
for (auto& p : variablesToMove) {
|
||||
if (!MoveVariable(p.first, p.second)) {
|
||||
return Status::Failure;
|
||||
}
|
||||
localizedVariables.insert(p.first->result_id());
|
||||
}
|
||||
|
||||
if (get_module()->version() >= SPV_SPIRV_VERSION_WORD(1, 4)) {
|
||||
// SPIR-V 1.4+ lists statically-used Private variables on OpEntryPoint;
|
||||
// drop the ones that just stopped being Private. Dead code for the 1.3
|
||||
// modules MobileGL emits, kept for robustness.
|
||||
for (auto& entry : get_module()->entry_points()) {
|
||||
std::vector<Operand> newOperands;
|
||||
for (uint32_t i = 0; i < entry.NumInOperands(); ++i) {
|
||||
// Execution model, function id and name are always kept.
|
||||
if (i < 3 || !localizedVariables.count(entry.GetSingleWordInOperand(i))) {
|
||||
newOperands.push_back(entry.GetInOperand(i));
|
||||
}
|
||||
}
|
||||
if (newOperands.size() != entry.NumInOperands()) {
|
||||
entry.SetInOperands(std::move(newOperands));
|
||||
context()->AnalyzeUses(&entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
Function* PrivateToEntryLocalPass::FindEntryLocalFunction(const Instruction& inst) const {
|
||||
bool foundFirstUse = false;
|
||||
Function* targetFunction = nullptr;
|
||||
const uint32_t variableId = inst.result_id();
|
||||
context()->get_def_use_mgr()->ForEachUser(
|
||||
variableId, [&targetFunction, &foundFirstUse, variableId, this](Instruction* use) {
|
||||
BasicBlock* currentBlock = context()->get_instr_block(use);
|
||||
if (currentBlock == nullptr) {
|
||||
// Module-scope users: OpName, decorations, the OpEntryPoint
|
||||
// interface list. None of them pins the variable to a function,
|
||||
// but a debug-info extended instruction would go stale after the
|
||||
// move, so treat it as disqualifying.
|
||||
if (use->opcode() == spv::Op::OpExtInst) {
|
||||
foundFirstUse = true;
|
||||
targetFunction = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!IsValidUse(use, variableId)) {
|
||||
foundFirstUse = true;
|
||||
targetFunction = nullptr;
|
||||
return;
|
||||
}
|
||||
Function* currentFunction = currentBlock->GetParent();
|
||||
if (!foundFirstUse) {
|
||||
foundFirstUse = true;
|
||||
targetFunction = currentFunction;
|
||||
} else if (targetFunction != currentFunction) {
|
||||
targetFunction = nullptr;
|
||||
}
|
||||
});
|
||||
if (targetFunction != nullptr && !IsEntryPointFunction(targetFunction)) {
|
||||
// The whole point of this derivative: a helper can be called more than
|
||||
// once per invocation, and Function storage would reset the variable at
|
||||
// every call.
|
||||
return nullptr;
|
||||
}
|
||||
return targetFunction;
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::IsEntryPointFunction(Function* function) const {
|
||||
for (auto& entry : get_module()->entry_points()) {
|
||||
if (entry.GetSingleWordInOperand(1) == function->result_id()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::IsValidUse(const Instruction* inst, uint32_t variableId) const {
|
||||
// The cases here have to match the cases in UpdateUse: a use the rewrite
|
||||
// does not know how to update disqualifies the variable.
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpLoad:
|
||||
case spv::Op::OpImageTexelPointer: // treat like a load
|
||||
return true;
|
||||
case spv::Op::OpStore:
|
||||
// Storing the variable's ADDRESS somewhere else escapes it.
|
||||
return inst->GetOperand(1).AsId() != variableId;
|
||||
case spv::Op::OpAccessChain:
|
||||
return context()->get_def_use_mgr()->WhileEachUser(
|
||||
inst, [this, inst](const Instruction* user) {
|
||||
return IsValidUse(user, inst->result_id());
|
||||
});
|
||||
case spv::Op::OpName:
|
||||
return true;
|
||||
default:
|
||||
return spvOpcodeIsDecoration(inst->opcode());
|
||||
}
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::MoveVariable(Instruction* variable, Function* function) {
|
||||
// Remove from the global section and re-insert at the head of the entry
|
||||
// function's first block, Function-storage variables' one legal position.
|
||||
variable->RemoveFromList();
|
||||
std::unique_ptr<Instruction> var(variable); // take ownership
|
||||
context()->ForgetUses(variable);
|
||||
|
||||
variable->SetInOperand(kVariableStorageClassInIdx,
|
||||
{uint32_t(spv::StorageClass::Function)});
|
||||
|
||||
const uint32_t newTypeId = GetNewType(variable->type_id());
|
||||
if (newTypeId == 0) {
|
||||
return false;
|
||||
}
|
||||
variable->SetResultType(newTypeId);
|
||||
|
||||
context()->AnalyzeUses(variable);
|
||||
context()->set_instr_block(variable, &*function->begin());
|
||||
function->begin()->begin()->InsertBefore(std::move(var));
|
||||
|
||||
return UpdateUses(variable);
|
||||
}
|
||||
|
||||
uint32_t PrivateToEntryLocalPass::GetNewType(uint32_t oldTypeId) {
|
||||
auto* typeMgr = context()->get_type_mgr();
|
||||
Instruction* oldTypeInst = get_def_use_mgr()->GetDef(oldTypeId);
|
||||
const uint32_t pointeeTypeId =
|
||||
oldTypeInst->GetSingleWordInOperand(kSpvTypePointerTypeIdInIdx);
|
||||
const uint32_t newTypeId =
|
||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Function);
|
||||
if (newTypeId != 0) {
|
||||
context()->UpdateDefUse(context()->get_def_use_mgr()->GetDef(newTypeId));
|
||||
}
|
||||
return newTypeId;
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::UpdateUse(Instruction* inst, Instruction* user) {
|
||||
// The cases here have to match the cases in IsValidUse.
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpLoad:
|
||||
case spv::Op::OpStore:
|
||||
case spv::Op::OpImageTexelPointer: // treat like a load
|
||||
// Fine as-is: their type is the pointed-to type, which is unchanged.
|
||||
break;
|
||||
case spv::Op::OpAccessChain: {
|
||||
context()->ForgetUses(inst);
|
||||
const uint32_t newTypeId = GetNewType(inst->type_id());
|
||||
if (newTypeId == 0) {
|
||||
return false;
|
||||
}
|
||||
inst->SetResultType(newTypeId);
|
||||
context()->AnalyzeUses(inst);
|
||||
if (!UpdateUses(inst)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpEntryPoint: // handled separately in Process()
|
||||
break;
|
||||
default:
|
||||
assert(spvOpcodeIsDecoration(inst->opcode()) &&
|
||||
"PrivateToEntryLocalPass: unexpected use opcode");
|
||||
break;
|
||||
}
|
||||
(void)user;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrivateToEntryLocalPass::UpdateUses(Instruction* inst) {
|
||||
const uint32_t id = inst->result_id();
|
||||
std::vector<Instruction*> uses;
|
||||
context()->get_def_use_mgr()->ForEachUser(id,
|
||||
[&uses](Instruction* use) { uses.push_back(use); });
|
||||
for (Instruction* use : uses) {
|
||||
if (!UpdateUse(use, inst)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken PrivateToEntryLocalPass::CreatePrivateToEntryLocalPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
spvtools::MakeUnique<PrivateToEntryLocalPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,55 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// AggressiveDCE treats every store to a Private global as an observable side
|
||||
// effect while the entry point still contains any OpFunctionCall, so a dead
|
||||
// vertex-input -> Private-shim chain (Iris rewrites unused legacy attributes
|
||||
// into exactly this shape) survives the whole optimizer chain. Rewriting such
|
||||
// a variable to Function storage unlocks ADCE without inlining anything.
|
||||
//
|
||||
// Upstream's PrivateToLocalPass does that rewrite for a Private variable used
|
||||
// in ANY single function - which is unsound here: a Function-storage variable
|
||||
// is recreated on every call, so a Private global that carries state across
|
||||
// repeated calls of one helper (a memoized init flag, LCG rand state) would
|
||||
// silently lose it. This derivative applies the same rewrite restricted to
|
||||
// variables whose only using function is an entry point: an entry point runs
|
||||
// once per invocation, so the two lifetimes are indistinguishable there.
|
||||
//
|
||||
// Derived from SPIRV-Tools' PrivateToLocalPass
|
||||
// (source/opt/private_to_local_pass.cpp, Copyright (c) 2017 Google Inc.,
|
||||
// Apache License 2.0).
|
||||
class PrivateToEntryLocalPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-private-to-entry-local"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreatePrivateToEntryLocalPass();
|
||||
|
||||
private:
|
||||
// The single entry-point function every block-level use of the variable
|
||||
// lives in, or nullptr when the uses span functions, include an opcode the
|
||||
// rewrite cannot update, or belong to a non-entry function.
|
||||
spvtools::opt::Function* FindEntryLocalFunction(const spvtools::opt::Instruction& inst) const;
|
||||
bool IsEntryPointFunction(spvtools::opt::Function* function) const;
|
||||
bool IsValidUse(const spvtools::opt::Instruction* inst, uint32_t variableId) const;
|
||||
bool MoveVariable(spvtools::opt::Instruction* variable, spvtools::opt::Function* function);
|
||||
uint32_t GetNewType(uint32_t oldTypeId);
|
||||
bool UpdateUse(spvtools::opt::Instruction* inst, spvtools::opt::Instruction* user);
|
||||
bool UpdateUses(spvtools::opt::Instruction* inst);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,59 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.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 "StripUniformLocationsPass.h"
|
||||
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
spvtools::opt::Pass::Status StripUniformLocationsPass::Process() {
|
||||
using spvtools::opt::Instruction;
|
||||
|
||||
std::vector<Instruction*> toKill;
|
||||
for (auto& annotation : get_module()->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate) {
|
||||
continue;
|
||||
}
|
||||
if (annotation.GetSingleWordInOperand(1) !=
|
||||
static_cast<uint32_t>(spv::Decoration::Location)) {
|
||||
continue;
|
||||
}
|
||||
Instruction* target =
|
||||
get_def_use_mgr()->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
if (target == nullptr || target->opcode() != spv::Op::OpVariable) {
|
||||
continue;
|
||||
}
|
||||
switch (spv::StorageClass(target->GetSingleWordInOperand(0))) {
|
||||
case spv::StorageClass::UniformConstant:
|
||||
case spv::StorageClass::Uniform:
|
||||
case spv::StorageClass::StorageBuffer:
|
||||
toKill.push_back(&annotation);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (Instruction* inst : toKill) {
|
||||
context()->KillInst(inst);
|
||||
}
|
||||
return toKill.empty() ? Status::SuccessWithoutChange : Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken StripUniformLocationsPass::CreateStripUniformLocationsPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
spvtools::MakeUnique<StripUniformLocationsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,32 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// glslang's relaxed GL path keeps `layout(location = N) uniform ...` as a
|
||||
// Location decoration on the UniformConstant/Uniform variable, which Vulkan
|
||||
// forbids ([VUID-StandaloneSpirv-Location-06672]). Nothing downstream reads
|
||||
// it: GL-side uniform locations come from the phase-A glslang reflection,
|
||||
// Vulkan binding assignment goes by (kind, name), and SPIRV-Cross's ESSL
|
||||
// resolves uniforms by name. Strip it so the driver-bound module is valid.
|
||||
class StripUniformLocationsPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-strip-uniform-locations"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateStripUniformLocationsPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -131,6 +131,11 @@ bool LoadMobileGL(const Request& request, std::string& error) {
|
||||
setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1);
|
||||
setenv("MOBILEGL_TRACE_LIBRARY", request.mobileGlLibrary.c_str(), 1);
|
||||
setenv("MOBILEGL_TRACE_SKIP_AUTODESTROY", "1", 1);
|
||||
// Retrace is a test lane on every platform, including the Android AVD one where
|
||||
// MobileGL's __ANDROID__ default would leave validation off. No overwrite: an outer
|
||||
// MOBILEGL_VALIDATE_SPIRV=0 must keep working as the escape hatch, and retracing the
|
||||
// exact shipping pipeline must stay possible.
|
||||
setenv("MOBILEGL_VALIDATE_SPIRV", "1", 0);
|
||||
setenv("MOBILEGL_TRACE_SURFACE", request.usePbuffer ? "pbuffer" : "window", 1);
|
||||
if (request.backend == "DirectVulkan") {
|
||||
setenv("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", 1);
|
||||
|
||||
Reference in New Issue
Block a user