[Fix, Test] (MG_Util, MG_Backend/DirectVulkan, MG_IntegrationTest): adversarial review - the rewritten 1D-array image collided with a module's own 2D-array one and left an invalid duplicate type; pin the component order, keep the subject kinds in a truncated matrix

This commit is contained in:
2026-08-12 16:49:59 -04:00
parent 257fcbfd0b
commit 44805bfa07
6 changed files with 142 additions and 16 deletions
@@ -858,6 +858,16 @@ namespace MobileGL {
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(Lower1DArrayImagesPass::CreateLower1DArrayImagesPass());
// Mandatory, not tidying. Rewriting a 1D-array image type to the 2D-array one
// makes it structurally IDENTICAL to any real 2D-array image of the same sampled
// type and format that the module already declared - and SPIR-V forbids duplicate
// non-aggregate type declarations, so the result fails validation. That collision
// is not exotic: it is the shape of this whole change's headline case, where one
// compute shader declares uimage1DArray and uimage2DArray side by side, both
// r32ui. The same applies one level up, to the OpTypePointer instructions that
// named the two types, and to the Image1D capability the rewrite turns into a
// second Shader. Deduplicating afterwards collapses all three at once.
optimizer.RegisterPass(CreateRemoveDuplicatesPass());
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary);
}
@@ -171,13 +171,21 @@ namespace MobileGL {
return Status::SuccessWithoutChange;
}
spvtools::opt::analysis::Integer signedInt(32, true);
spvtools::opt::analysis::Vector int3(&signedInt, 3);
const uint32_t int3TypeId = typeMgr->GetTypeInstruction(&int3);
const uint32_t intTypeId = typeMgr->GetTypeInstruction(&signedInt);
const uint32_t zeroId = constantMgr->GetSIntConstId(0);
if (int3TypeId == 0 || intTypeId == 0 || zeroId == 0) {
return Status::Failure;
// The same refusal the caller makes, restated here so the pass is safe wherever
// it is registered. Rewriting the type while leaving an OpImageQuerySize on it
// produces a query whose result type has one component too few - an invalid
// module - and there is no correct two-component size to substitute, because the
// ES texture genuinely has a height the GL one does not.
for (auto& function : *irContext->module()) {
for (auto& block : function) {
for (auto& instruction : block) {
if (QueriesImageSize(instruction.opcode()) && instruction.NumInOperands() >= 1 &&
Is1DArrayStorageImageType(
ResolveImageType(irContext, instruction.GetSingleWordInOperand(0)))) {
return Status::SuccessWithoutChange;
}
}
}
}
// (u, layer) -> (u, 0, layer). The height the ES 2D array carries is 1, so Y is
@@ -197,6 +205,35 @@ namespace MobileGL {
}
const uint32_t coordinateId = instruction.GetSingleWordInOperand(coordinateOperand);
// Built from the COORDINATE's own component type rather than a
// hardcoded signed int. GLSL only ever spells these ivec2, but SPIR-V
// permits an unsigned coordinate, and extracting a uint component
// into an int result is an invalid module rather than a wrong answer -
// the kind of defect that reaches a driver as "compiles here, not
// there".
Instruction* coordinateDef = irContext->get_def_use_mgr()->GetDef(coordinateId);
if (coordinateDef == nullptr) return Status::Failure;
const auto* coordinateType = typeMgr->GetType(coordinateDef->type_id());
const auto* coordinateVector = coordinateType != nullptr ? coordinateType->AsVector()
: nullptr;
if (coordinateVector == nullptr || coordinateVector->element_count() != 2) {
return Status::Failure;
}
const auto* component = coordinateVector->element_type();
const auto* componentInteger = component != nullptr ? component->AsInteger() : nullptr;
if (componentInteger == nullptr) return Status::Failure;
spvtools::opt::analysis::Vector widenedVector(component, 3);
const uint32_t int3TypeId = typeMgr->GetTypeInstruction(&widenedVector);
const uint32_t intTypeId = typeMgr->GetTypeInstruction(component);
const uint32_t zeroId = componentInteger->IsSigned()
? constantMgr->GetSIntConstId(0)
: constantMgr->GetUIntConstId(0);
if (int3TypeId == 0 || intTypeId == 0 || zeroId == 0) {
return Status::Failure;
}
InstructionBuilder builder(
irContext, &instruction,
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
@@ -60,6 +60,16 @@ namespace MobileGL {
// the rewrite OpImageQuerySize yields three components where the shader consumes two,
// and silently handing back a differently-shaped size is worse than refusing. The
// caller logs it and leaves the module alone.
//
// KNOWN LIMITATION - the decline is per MODULE, and a program is several of them. A
// program whose vertex and fragment stages share a uimage1DArray uniform, where only
// one stage calls imageSize() on it, gets that stage declined and the other rewritten:
// the two then declare the same uniform with different types and the ES LINK fails on
// a type mismatch, rather than the single compile error a reader of the comment above
// would expect. Correlating the decision across a program's stages needs the decision
// to be made where the program is known, which is above this pass; it is left undone
// deliberately rather than papered over, because both outcomes are a refusal and the
// shape has never been observed outside a deliberately constructed shader.
class Lower1DArrayImagesPass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "mobilegl-lower-1d-array-images"; }