[Fix] (Magma): decline programs whose final SPIR-V fails validation instead of handing the module to the driver

This commit is contained in:
2026-10-02 23:09:58 -04:00
parent b64c2de3c8
commit bfa7987ca6
11 changed files with 380 additions and 19 deletions
+5
View File
@@ -130,6 +130,11 @@ namespace MobileGL::MG_Config {
Bool AdvertiseFp64 = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV: hand a shader module that fails SPIR-V validation to
// the driver anyway. Off by default: Magma declines such a program (no shader module, its
// draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU
// into device loss. For diagnosing a driver that copes with a specific module only.
Bool MagmaAllowInvalidSpirv = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
+1
View File
@@ -348,6 +348,7 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
@@ -391,6 +391,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
// The assert compiles out of shipping builds, and a program declined for an invalid
// module (VkProgramObject::invalidSpirvDeclined) has no stages at all: a caller that
// forgot to refuse it must get a skipped draw, not a stageCount=0 pipeline.
if (payload.stages == nullptr || payload.stages->empty()) {
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with no shader stages "
"(programHash=0x%llx); its draws are skipped",
static_cast<unsigned long long>(payload.programHash));
return VK_NULL_HANDLE;
}
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
@@ -12,7 +12,9 @@
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <Config.h>
#include <algorithm>
#include <atomic>
#include <bit>
#include <cmath>
#include <cstring>
@@ -2273,8 +2275,84 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
}
// ScreenModuleForDriver's memory. Keyed by content, never by program: the same bytes get
// the same answer whichever program, variant or session carries them.
struct DriverModuleVerdictCache {
struct Entry {
Uint64 wordCount = 0;
Bool valid = true;
String reason;
};
// Generous for any one application's distinct stage modules; past it the whole table
// is dropped rather than tracked per entry - a re-validation is the only cost.
static constexpr SizeT kMaxEntries = 8192;
std::mutex mutex;
UnorderedMap<Uint64, Entry> entries;
std::atomic<Uint64> validatorRuns{0};
};
DriverModuleVerdictCache& GetDriverModuleVerdictCache() {
// Leaked on purpose: screened from session threads that may outlive static destruction.
static auto* cache = new DriverModuleVerdictCache();
return *cache;
}
} // namespace
ProgramFactory::DriverModuleVerdict ProgramFactory::ScreenModuleForDriver(const Vector<Uint>& spirv,
Bool allowInvalid, String* outReason) {
if (outReason != nullptr) outReason->clear();
if (spirv.empty()) return DriverModuleVerdict::Valid;
auto& cache = GetDriverModuleVerdictCache();
const Uint64 key = XXH64(spirv.data(), spirv.size() * sizeof(Uint), 0);
Bool valid = true;
String reason;
Bool known = false;
{
const std::lock_guard<std::mutex> lock(cache.mutex);
const auto it = cache.entries.find(key);
if (it != cache.entries.end() && it->second.wordCount == spirv.size()) {
known = true;
valid = it->second.valid;
reason = it->second.reason;
}
}
if (!known) {
// Outside the lock: two threads screening the same new module both validate, and
// agree; nothing waits on another session's spirv-val.
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
cache.validatorRuns.fetch_add(1, std::memory_order_relaxed);
spv_context context = spvContextCreate(GetSpirvTargetEnv(spirv));
spv_validator_options options = spvValidatorOptionsCreate();
spvValidatorOptionsSetFriendlyNames(options, true);
spv_const_binary_t binary = {spirv.data(), spirv.size()};
spv_diagnostic diagnostic = nullptr;
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
valid = result == SPV_SUCCESS;
if (!valid) {
reason = diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<no diagnostic>";
// The process-wide counter the validating test lanes assert on.
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
}
spvDiagnosticDestroy(diagnostic);
spvValidatorOptionsDestroy(options);
spvContextDestroy(context);
const std::lock_guard<std::mutex> lock(cache.mutex);
if (cache.entries.size() >= DriverModuleVerdictCache::kMaxEntries) cache.entries.clear();
cache.entries[key] = DriverModuleVerdictCache::Entry{spirv.size(), valid, reason};
}
if (valid) return DriverModuleVerdict::Valid;
if (outReason != nullptr) *outReason = Move(reason);
return allowInvalid ? DriverModuleVerdict::AdmittedInvalid : DriverModuleVerdict::DeclinedInvalid;
}
Uint64 ProgramFactory::DriverModuleValidatorRuns() {
return GetDriverModuleVerdictCache().validatorRuns.load(std::memory_order_relaxed);
}
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
// position math between pipelines cannot desynchronize it; the blended depth-write
@@ -3814,27 +3892,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
// Last look at the exact bytes the driver receives, in EVERY build rather than only in
// DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed, and for every stage BEFORE any
// module is created. By here the descriptor bindings have been remapped and the layout
// about to be reflected describes the remapped module, so there is no module left that
// is both valid and consistent with it to fall back to (the recovery that exists lives
// one step earlier, at the clip/XFB fixups). What is left is to keep the bytes away from
// the driver: a driver is not a validating entry point, and an invalid module it accepts
// can fault the GPU into device loss (a frontend translation once put a sampler inside
// the default uniform block, and Adreno page-faulted reading its "descriptor" out of
// uniform-buffer bytes). One invalid stage declines the whole program: no shader module
// of it is created, and every draw or dispatch of it is refused by name
// (VkProgramObject::invalidSpirvDeclined). MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV hands the
// bytes over anyway, for diagnosing a driver that copes.
for (SizeT i = 0; i < moduleCount; ++i) {
String reason;
const DriverModuleVerdict verdict = ScreenModuleForDriver(
moduleSpirvs[i], MG_Config::Features.MagmaAllowInvalidSpirv, &reason);
if (verdict == DriverModuleVerdict::DeclinedInvalid) {
MGLOG_E_ONCE("ProgramFactory: declining program %u - its stage %d module fails SPIR-V validation (%s); "
"no shader module is created and its draws are refused. "
"MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV=1 hands such modules to the driver anyway.",
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
entry.invalidSpirvDeclined = true;
break;
}
if (verdict == DriverModuleVerdict::AdmittedInvalid) {
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d "
"because MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV is set (%s)",
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
}
}
for (SizeT i = 0; i < moduleCount && !entry.invalidSpirvDeclined; ++i) {
auto& moduleSpv = moduleSpirvs[i];
if (moduleSpv.empty()) continue;
// Last look at the exact bytes the driver receives, in EVERY build rather than only
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
// by here the descriptor bindings have been remapped and the layout about to be
// reflected describes the remapped module, so there is no module left that is both
// valid and consistent with it to fall back to. The recovery lives one step earlier,
// at the clip/XFB fixups (see the revert there) - which is where a transform can
// introduce a reference to a delisted interface variable, the failure this whole
// guard exists for. Anything that reaches this line names itself in the log of a
// shipping build instead of dying anonymously inside the driver.
SpirvValidationFailure finalFailure{};
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
"a backend transform after the clip/XFB fixups broke it (%s)",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
finalFailure.message.c_str());
}
// Does the stage the driver will treat as the last pre-rasterization one actually
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
@@ -229,6 +229,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the cache entry, so every later draw of the same program variant reads the same
// answer instead of re-deciding it.
Bool pointSizeCapabilityUnsupported = false;
// One of the program's final modules fails SPIR-V validation, so none of its modules
// was created and every draw or dispatch of it is refused by name (the ProgramInvalidSpirv /
// ComputeProgramInvalidSpirv wire declines) - see ProgramFactory::ScreenModuleForDriver.
// Sticky like the flag above: it lives on the cache entry, so the one log line is honest.
Bool invalidSpirvDeclined = false;
Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are
@@ -289,6 +294,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
// The three refusal flags travel like every other field: a robin-hood insert swaps
// entries, and a flag left behind would refuse (or admit) the wrong program.
invalidSpirvDeclined = other.invalidSpirvDeclined;
xfbCaptureDeclined = other.xfbCaptureDeclined;
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -310,6 +320,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.declinedDescriptors = false;
other.invalidSpirvDeclined = false;
other.xfbCaptureDeclined = false;
other.pointSizeCapabilityUnsupported = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
@@ -353,6 +366,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
// The three refusal flags travel like every other field: a robin-hood insert swaps
// entries, and a flag left behind would refuse (or admit) the wrong program.
invalidSpirvDeclined = other.invalidSpirvDeclined;
xfbCaptureDeclined = other.xfbCaptureDeclined;
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -374,6 +392,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.declinedDescriptors = false;
other.invalidSpirvDeclined = false;
other.xfbCaptureDeclined = false;
other.pointSizeCapabilityUnsupported = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
@@ -504,6 +525,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// VkRenderPassManager::OnPresent's sweep.
void OnFrameBoundary();
// What the last look at a module's final bytes - the exact words vkCreateShaderModule
// would receive - decided.
enum class DriverModuleVerdict : Uint8 {
Valid, // validates: handed to the driver
DeclinedInvalid, // fails validation: never handed over, the program's work is declined
AdmittedInvalid, // fails validation, handed over anyway (MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV)
};
// Validates `spirv` for the Vulkan environment its version word names and turns the
// result into a verdict. A driver is not a validating entry point: Adreno page-faulted
// into device loss on a module that carried a sampler inside a uniform block, Mali
// SIGSEGVs inside pipeline creation. So an invalid module is declined unless
// `allowInvalid` (the escape hatch) says to hand it over regardless.
//
// The validator's answer is remembered per module content (64-bit hash plus word count),
// process-wide: a program variant, an evicted-then-rebuilt entry and two programs that
// share a stage all re-screen the same bytes, and only the first pays for spirv-val.
// `outReason` receives the validator's message for an invalid module.
static DriverModuleVerdict ScreenModuleForDriver(const Vector<Uint>& spirv, Bool allowInvalid,
String* outReason = nullptr);
// How many times ScreenModuleForDriver actually ran the validator (cache misses), for
// the tests that pin the cache.
static Uint64 DriverModuleValidatorRuns();
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
@@ -6835,7 +6835,7 @@ void main() {
// A refused program cannot reach here today - the full path refuses before it ever
// records a snapshot - but declining the fast path costs one compare and means the
// refusal does not depend on that ordering staying true.
if (programObj.pointSizeCapabilityUnsupported) {
if (programObj.pointSizeCapabilityUnsupported || programObj.invalidSpirvDeclined) {
return false;
}
@@ -7318,7 +7318,7 @@ void main() {
// The build already said why, once, naming the program and the stage. Refusing here -
// before any pipeline is built from it - is what makes that message a decline rather
// than a note attached to invalid usage the driver still receives.
if (programObj.pointSizeCapabilityUnsupported) {
if (programObj.pointSizeCapabilityUnsupported || programObj.invalidSpirvDeclined) {
return false;
}
// For the snapshot's memoised entry pointer: if anything below inserts into the
@@ -7778,6 +7778,8 @@ void main() {
}
ProgramFactory::CompileOptionFlags transformFlags = 0;
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
// No shader module of a program that failed validation exists; the build said why, once.
if (programObj.invalidSpirvDeclined) return;
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
@@ -7840,6 +7842,8 @@ void main() {
}
ProgramFactory::CompileOptionFlags transformFlags = 0;
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
// No shader module of a program that failed validation exists; the build said why, once.
if (programObj.invalidSpirvDeclined) return;
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
@@ -126,3 +126,11 @@ MGL_WIRE_DECLINE(MipmapDepthStencilAspect)
// is missing, or the three group-count words lie outside it or are misaligned.
MGL_WIRE_DECLINE(DispatchIndirectBufferUnbound)
MGL_WIRE_DECLINE(DispatchIndirectRange)
// ---- ProgramFactory::ScreenModuleForDriver ------------------------------------------------
// One of the bound program's final modules fails SPIR-V validation, so no shader module of it was
// created (VkProgramObject::invalidSpirvDeclined): SetupWireDraw refuses the draw, and the two
// dispatch arms the dispatch. A driver handed the module could fault the GPU into device loss.
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV=1 builds the module anyway, and then neither row moves.
MGL_WIRE_DECLINE(ProgramInvalidSpirv)
MGL_WIRE_DECLINE(ComputeProgramInvalidSpirv)
@@ -230,6 +230,10 @@
flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
programObject = &m_programFactory->GetOrCreateProgram(program, flags);
}
if (programObject->invalidSpirvDeclined) {
MGL_WIRE_DECLINE_AT(ProgramInvalidSpirv, "a module of the program fails SPIR-V validation and was never built");
return false;
}
if (programObject->pointSizeCapabilityUnsupported) {
MGL_WIRE_DECLINE_AT(PointSizeCapability, "the program writes gl_PointSize and the device cannot express it");
return false;
@@ -775,6 +779,10 @@
return;
}
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
if (programObj.invalidSpirvDeclined) {
MGL_WIRE_DECLINE_AT(ComputeProgramInvalidSpirv, "a module of the compute program fails SPIR-V validation and was never built");
return;
}
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
return;
@@ -847,6 +855,10 @@
return;
}
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
if (programObj.invalidSpirvDeclined) {
MGL_WIRE_DECLINE_AT(ComputeProgramInvalidSpirv, "a module of the compute program fails SPIR-V validation and was never built");
return;
}
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
return;
+1
View File
@@ -5,6 +5,7 @@ add_executable(
PipelineQuirkTest.cpp
PassthroughTessControlTest.cpp
ViewportIndexReflectionTest.cpp
DriverModuleScreenTest.cpp
)
target_include_directories(PipelineQuirkTest PRIVATE
@@ -0,0 +1,183 @@
// MobileGL - MobileGL/MG_Test/Pipeline/DriverModuleScreenTest.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
//
// ProgramFactory::ScreenModuleForDriver is the last look Magma takes at a module's bytes before
// vkCreateShaderModule. A driver is not a validating entry point: a module that carried a sampler
// inside the default uniform block (a frontend translation defect) was accepted by Adreno, which
// then page-faulted on a descriptor read out of uniform-buffer bytes and lost the device. The
// screen used to report such a module and hand it over anyway; it must DECLINE it, so the GPU
// never sees it, unless MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV asks otherwise.
//
// The invalid module here is that exact shape, assembled by hand so it stays invalid whatever
// the frontend does: a Block-decorated uniform struct with a sampled-image member.
#include <gtest/gtest.h>
#include <chrono>
#include <cstdio>
#include "Includes.h"
#include "Init.h"
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include "spirv-tools/libspirv.hpp"
using namespace MobileGL;
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
using Verdict = ProgramFactory::DriverModuleVerdict;
namespace {
// Shared by both modules: a fragment shader whose default uniform block holds a vec4 and,
// in the invalid one, a sampler2D next to it.
String UniformBlockFragment(Bool withSampler) {
String text =
"OpCapability Shader\n"
"OpMemoryModel Logical GLSL450\n"
"OpEntryPoint Fragment %main \"main\" %out\n"
"OpExecutionMode %main OriginUpperLeft\n"
"OpDecorate %out Location 0\n"
"OpDecorate %Block Block\n"
"OpMemberDecorate %Block 0 Offset 0\n"
"OpDecorate %ubo DescriptorSet 0\n"
"OpDecorate %ubo Binding 0\n"
"%void = OpTypeVoid\n"
"%fn = OpTypeFunction %void\n"
"%float = OpTypeFloat 32\n"
"%v4 = OpTypeVector %float 4\n"
"%int = OpTypeInt 32 1\n"
"%zero = OpConstant %int 0\n"
"%img = OpTypeImage %float 2D 0 0 0 1 Unknown\n"
"%simg = OpTypeSampledImage %img\n";
text += withSampler ? "%Block = OpTypeStruct %v4 %simg\n" : "%Block = OpTypeStruct %v4\n";
text +=
"%pBlock = OpTypePointer Uniform %Block\n"
"%ubo = OpVariable %pBlock Uniform\n"
"%pv4u = OpTypePointer Uniform %v4\n"
"%pv4o = OpTypePointer Output %v4\n"
"%out = OpVariable %pv4o Output\n"
"%main = OpFunction %void None %fn\n"
"%entry = OpLabel\n"
"%ptr = OpAccessChain %pv4u %ubo %zero\n"
"%color = OpLoad %v4 %ptr\n"
"OpStore %out %color\n"
"OpReturn\n"
"OpFunctionEnd\n";
return text;
}
Vector<Uint> Assemble(const String& text) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
std::vector<uint32_t> words;
EXPECT_TRUE(tools.Assemble(text, &words)) << text;
return Vector<Uint>(words.begin(), words.end());
}
// A realistic module out of the real frontend, for the cost measurement.
Vector<Uint> CompileFragment(const String& source) {
using namespace MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_SHADER, .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 = {GL_FRAGMENT_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!binaryResult || binaryResult->empty()) return {};
const auto& words = binaryResult->front();
return Vector<Uint>(words.begin(), words.end());
}
class DriverModuleScreenTest : public ::testing::Test {
protected:
void SetUp() override { MobileGL::Initialize(); }
};
} // namespace
// The headline: the sampler-in-uniform-block module never reaches the driver. Red before the
// screen acted on its verdict - it answered AdmittedInvalid, i.e. "logged, handed over anyway".
TEST_F(DriverModuleScreenTest, ASamplerInsideTheDefaultUniformBlockIsDeclined) {
const Vector<Uint> invalid = Assemble(UniformBlockFragment(/*withSampler=*/true));
ASSERT_FALSE(invalid.empty());
String reason;
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(invalid, /*allowInvalid=*/false, &reason),
Verdict::DeclinedInvalid);
EXPECT_FALSE(reason.empty()) << "a decline must carry the validator's reason for its log line";
}
// The positive control: the same block without the sampler validates and is handed over. A screen
// that declined everything would pass the case above and fail this one.
TEST_F(DriverModuleScreenTest, TheSameBlockWithoutTheSamplerPasses) {
const Vector<Uint> valid = Assemble(UniformBlockFragment(/*withSampler=*/false));
ASSERT_FALSE(valid.empty());
String reason = "stale";
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(valid, /*allowInvalid=*/false, &reason), Verdict::Valid);
EXPECT_TRUE(reason.empty());
}
// The escape hatch: with MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV the invalid module is handed over,
// and the verdict says it was invalid rather than pretending it validated.
TEST_F(DriverModuleScreenTest, TheEscapeHatchAdmitsAnInvalidModuleAndSaysSo) {
const Vector<Uint> invalid = Assemble(UniformBlockFragment(/*withSampler=*/true));
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(invalid, /*allowInvalid=*/true), Verdict::AdmittedInvalid);
const Vector<Uint> valid = Assemble(UniformBlockFragment(/*withSampler=*/false));
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(valid, /*allowInvalid=*/true), Verdict::Valid);
}
// The verdict is remembered by content: re-screening the same bytes does not run spirv-val again,
// and a different module does. Also reports what one validation costs next to a cached answer.
TEST_F(DriverModuleScreenTest, TheVerdictIsCachedPerModuleContent) {
const Vector<Uint> module = CompileFragment(R"(#version 330 core
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform vec4 tint;
uniform mat4 colorMatrix;
in vec2 uv;
in vec4 vertexColor;
out vec4 fragColor;
vec3 tonemap(vec3 c) { return c / (c + vec3(1.0)); }
void main() {
vec4 base = texture(tex0, uv) * vertexColor;
vec4 detail = texture(tex1, uv * 4.0);
vec4 c = colorMatrix * mix(base, base * detail, 0.5) * tint;
for (int i = 0; i < 4; ++i) c.rgb += 0.01 * sin(c.gbr * float(i + 1));
fragColor = vec4(tonemap(c.rgb), c.a);
}
)");
ASSERT_FALSE(module.empty());
// A copy with one changed word in the bound field of the header: different bytes, same
// validity, so it must miss the cache.
Vector<Uint> sibling = module;
sibling[3] += 1;
const Uint64 runsBefore = ProgramFactory::DriverModuleValidatorRuns();
const auto t0 = std::chrono::steady_clock::now();
const Verdict first = ProgramFactory::ScreenModuleForDriver(module, false);
const auto t1 = std::chrono::steady_clock::now();
const Verdict second = ProgramFactory::ScreenModuleForDriver(module, false);
const auto t2 = std::chrono::steady_clock::now();
EXPECT_EQ(first, Verdict::Valid);
EXPECT_EQ(second, first);
EXPECT_EQ(ProgramFactory::DriverModuleValidatorRuns(), runsBefore + 1) << "the second screen re-validated";
EXPECT_EQ(ProgramFactory::ScreenModuleForDriver(sibling, false), Verdict::Valid);
EXPECT_EQ(ProgramFactory::DriverModuleValidatorRuns(), runsBefore + 2) << "different bytes shared a verdict";
const auto us = [](auto a, auto b) {
return static_cast<long long>(std::chrono::duration_cast<std::chrono::microseconds>(b - a).count());
};
std::printf("[ screen ] %zu-word module: validate %lld us, cached %lld us\n", module.size(), us(t0, t1),
us(t1, t2));
}
+1
View File
@@ -166,6 +166,7 @@ MobileGL supports runtime configuration via environment variables.
| `MOBILEGL_MAGMA_DISABLE_SUBGROUP` | Disable Vulkan shader subgroup support. | `0`, `1` | `0` |
| `MOBILEGL_ADVERTISE_FP64` | Advertise `GL_ARB_gpu_shader_fp64`. GLSL `double`/`dvec`/`dmat` compile and run either way - they are narrowed to 32 bits - so this only changes whether an application is told it has 64-bit precision, which it does not. | `0`, `1` | `0` |
| `MOBILEGL_MAGMA_R11G11B10F_FALLBACK` | Use Magma's R11G11B10F format fallback. | `0`, `1` | `0` |
| `MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV` | Hand a shader module that fails SPIR-V validation to the driver anyway. By default Magma declines such a program (no shader module is created, its draws and dispatches are refused and logged by name), because a driver that accepts invalid SPIR-V can fault the GPU into device loss. Diagnostic use only. | `0`, `1` | `0` |
| `MOBILEGL_MAGMA_FRAMESINFLIGHT` | Set Magma frames in flight. | Integer `1`–`64` | `3` |
| `MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER` | Draws and dispatches Magma records into one command buffer before it submits the buffer mid-frame and continues on a fresh one. `0` never splits. | Integer `0`–`16777216` | `16384` |
| `MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES` | Frames a frame slot's descriptor pools must stay under a quarter full before Magma frees the slot's cached descriptor sets and grown pools. `0` never trims. | Integer `0`–`1048576` | `120` |