[Fix, Test] (Review): strip a block's member-level locations too, restore the probe's colour mask, and let the POST verdict follow the override

This commit is contained in:
2026-08-27 20:15:31 -04:00
parent eab622388f
commit ad28d2b744
5 changed files with 212 additions and 48 deletions
+33 -7
View File
@@ -8,6 +8,7 @@
#include "DriverBugProbes.h"
#include <Config.h>
#include <MG_Util/Debug/Log.h>
#include <cstring>
@@ -1828,11 +1829,19 @@ namespace MobileGL::MG_Util::SelfTest {
SavedState saved;
Save(gl, saved);
// The colour mask is not in SavedState - no other probe touches it - so this one saves
// and puts back its own. It has to be forced open: a masked channel would read back as
// zero and turn a healthy driver into a "payload lost" verdict.
GLboolean savedColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
const Bool canMaskColor = gl.glColorMask != nullptr && gl.glGetBooleanv != nullptr;
if (canMaskColor) {
gl.glGetBooleanv(GL_COLOR_WRITEMASK, savedColorMask);
gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
}
GLuint vao = 0;
gl.glGenVertexArrays(1, &vao);
gl.glBindVertexArray(vao);
PrepareForProbeDraw(gl);
if (gl.glColorMask != nullptr) gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// THE CONTROL, and it runs first: the identical three-stage program with no location on
// the blocks. If THAT cannot carry the payload, this driver's problem is not the
@@ -1863,6 +1872,9 @@ namespace MobileGL::MG_Util::SelfTest {
gl.glBindVertexArray(0);
gl.glDeleteVertexArrays(1, &vao);
}
if (canMaskColor) {
gl.glColorMask(savedColorMask[0], savedColorMask[1], savedColorMask[2], savedColorMask[3]);
}
Restore(gl, saved);
Drain(gl);
return measurement;
@@ -2015,13 +2027,27 @@ namespace MobileGL::MG_Util::SelfTest {
"vertex+fragment program is still emitted as the application wrote it"
: ". A located block between a VERTEX and a FRAGMENT stage is delivered "
"correctly on the same driver, which is what scopes the repair";
detail +=
". MobileGL emits a tessellation/geometry program's interface blocks with no "
"location qualifier at all (StripIoBlockLocationsPass) and lets ES match them by "
"block name and member sequence, which it does; the locations were invented by "
"the cross-stage IO resolver rather than written by the application";
// The repair can be switched off from the environment, and a report that said
// "Fixed" while the strip was disabled would be describing a build nobody is
// running. The verdict follows what this process will actually do, not what the
// code is capable of.
const Bool repairDisabled =
MG_Config::Features.EsprytUnlocatedIoBlocks == MG_Config::QuirkOverride::ForceOff;
if (repairDisabled) {
detail +=
". THE REPAIR IS DISABLED in this process: MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS "
"is set to force located blocks ON, so DirectGLES emits the location "
"qualifier the driver cannot honour and the payload is lost. Unset the "
"variable to get the repair back";
} else {
detail +=
". MobileGL emits a tessellation/geometry program's interface blocks with no "
"location qualifier at all (StripIoBlockLocationsPass) and lets ES match them "
"by block name and member sequence, which it does; the locations were invented "
"by the cross-stage IO resolver rather than written by the application";
}
return DriverBugFinding{"Located inter-stage interface blocks carry no payload",
measurement.alsoAffectsVertexToFragment
(repairDisabled || measurement.alsoAffectsVertexToFragment)
? DriverBugVerdict::Unfixable
: DriverBugVerdict::Fixed,
Move(detail)};
@@ -54,27 +54,24 @@ namespace MobileGL {
return blockStructIds;
}
// True when `variable` is an Input/Output interface block of the direction the
// caller armed. Tessellation and geometry interfaces are arrays of the block
// struct, so array levels are unwrapped before the struct is recognised.
Bool IsArmedInterfaceBlock(IRContext* irContext, Instruction& variable,
const std::unordered_set<uint32_t>& blockStructIds,
Bool stripInputBlocks, Bool stripOutputBlocks) {
if (variable.opcode() != spv::Op::OpVariable) return false;
// The interface-block struct an Input/Output variable declares, or 0 when the
// variable is not one. Tessellation and geometry interfaces are arrays of the
// block struct, so array levels are unwrapped before the struct is recognised.
uint32_t GetInterfaceBlockStructId(IRContext* irContext, Instruction& variable,
const std::unordered_set<uint32_t>& blockStructIds,
spv::StorageClass& outStorageClass) {
if (variable.opcode() != spv::Op::OpVariable) return 0;
const auto storageClass =
static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0));
if (storageClass == spv::StorageClass::Input) {
if (!stripInputBlocks) return false;
} else if (storageClass == spv::StorageClass::Output) {
if (!stripOutputBlocks) return false;
} else {
return false;
if (storageClass != spv::StorageClass::Input &&
storageClass != spv::StorageClass::Output) {
return 0;
}
auto* defUseMgr = irContext->get_def_use_mgr();
Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) {
return false;
return 0;
}
uint32_t pointeeId = pointerType->GetSingleWordInOperand(1);
Instruction* pointee = defUseMgr->GetDef(pointeeId);
@@ -83,8 +80,16 @@ namespace MobileGL {
pointeeId = pointee->GetSingleWordInOperand(0);
pointee = defUseMgr->GetDef(pointeeId);
}
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) return false;
return blockStructIds.find(pointeeId) != blockStructIds.end();
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) return 0;
if (blockStructIds.find(pointeeId) == blockStructIds.end()) return 0;
outStorageClass = storageClass;
return pointeeId;
}
Bool DirectionIsArmed(spv::StorageClass storageClass, Bool stripInputBlocks,
Bool stripOutputBlocks) {
return storageClass == spv::StorageClass::Input ? stripInputBlocks : stripOutputBlocks;
}
} // namespace
@@ -96,36 +101,74 @@ namespace MobileGL {
const std::unordered_set<uint32_t> blockStructIds = CollectUserBlockStructIds(irContext);
if (blockStructIds.empty()) return Status::SuccessWithoutChange;
// The variable ids to strip, resolved BEFORE anything is killed: the walk below
// deletes annotations, and deciding what to delete while deleting reads a list
// that is being mutated underneath it.
// What to strip, resolved BEFORE anything is killed: the walk below deletes
// annotations, and deciding what to delete while deleting reads a list that is
// being mutated underneath it.
//
// BOTH LEVELS, because a block carries its location at exactly one of them and
// which one is not the caller's choice. When the location came from the
// cross-stage IO resolver (or from `layout(location=) out Blk {...}`) glslang
// puts it on the VARIABLE; when the application located the members instead
// (`out Blk { layout(location = 4) vec4 v; }`) it puts one OpMemberDecorate per
// member and NOTHING on the variable - and SPIRV-Cross then suppresses the
// block-level qualifier and prints the member ones instead
// (spirv_glsl.cpp:1444 and :2037-2045). Stripping only the variable level would
// leave that second shape emitting exactly the located block this driver drops
// the payload for, and - because there was no variable decoration to remove -
// would report nothing stripped, so the caller would decline the module and
// nothing would say the repair had passed the shader by.
std::unordered_set<uint32_t> armedVariableIds;
std::unordered_set<uint32_t> armedStructIds;
// Block structs reached by an interface variable whose direction is NOT armed.
// A struct in here is left alone even if some armed variable also reaches it:
// member decorations belong to the TYPE, so stripping them would take the
// qualifier off the unarmed side too - the one whose other end is in a
// different program and is matched by exactly that number.
std::unordered_set<uint32_t> unarmedStructIds;
for (Instruction& variable : irContext->module()->types_values()) {
if (IsArmedInterfaceBlock(irContext, variable, blockStructIds, m_stripInputBlocks,
m_stripOutputBlocks)) {
spv::StorageClass storageClass = spv::StorageClass::Input;
const uint32_t structId =
GetInterfaceBlockStructId(irContext, variable, blockStructIds, storageClass);
if (structId == 0) continue;
if (DirectionIsArmed(storageClass, m_stripInputBlocks, m_stripOutputBlocks)) {
armedVariableIds.insert(variable.result_id());
armedStructIds.insert(structId);
} else {
unarmedStructIds.insert(structId);
}
}
for (const uint32_t unarmedStructId : unarmedStructIds) {
armedStructIds.erase(unarmedStructId);
}
if (armedVariableIds.empty()) return Status::SuccessWithoutChange;
// Component travels with Location and is meaningless without it. Leaving one
// behind is not merely untidy: for an ES target SPIRV-Cross THROWS on a block
// member's Component (spirv_glsl.cpp:1447-1460) rather than printing it, which
// costs the whole stage.
const auto isLocationOrComponent = [](uint32_t decoration) {
return static_cast<spv::Decoration>(decoration) == spv::Decoration::Location ||
static_cast<spv::Decoration>(decoration) == spv::Decoration::Component;
};
std::vector<Instruction*> toKill;
for (Instruction& annotation : irContext->module()->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate) continue;
const auto decoration =
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1));
// Component travels with Location and is meaningless without it; a block
// whose Location is gone and whose Component survives would be a shader
// SPIRV-Cross prints `layout(component = N)` for on its own, which ESSL has
// no spelling for at all.
if (decoration != spv::Decoration::Location &&
decoration != spv::Decoration::Component) {
continue;
if (annotation.opcode() == spv::Op::OpDecorate) {
if (!isLocationOrComponent(annotation.GetSingleWordInOperand(1))) continue;
if (armedVariableIds.find(annotation.GetSingleWordInOperand(0)) ==
armedVariableIds.end()) {
continue;
}
toKill.push_back(&annotation);
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
// OpMemberDecorate <struct> <member> <decoration> ...
if (!isLocationOrComponent(annotation.GetSingleWordInOperand(2))) continue;
if (armedStructIds.find(annotation.GetSingleWordInOperand(0)) ==
armedStructIds.end()) {
continue;
}
toKill.push_back(&annotation);
}
if (armedVariableIds.find(annotation.GetSingleWordInOperand(0)) ==
armedVariableIds.end()) {
continue;
}
toKill.push_back(&annotation);
}
for (Instruction* inst : toKill) {
@@ -46,6 +46,16 @@ namespace MobileGL {
// and so do vertex attributes and fragment outputs, which are never blocks.
// Builtin blocks (gl_PerVertex) are skipped; they carry no Location anyway.
//
// BOTH DECORATION LEVELS, because a block carries its location at exactly one of
// them: on the VARIABLE when the cross-stage IO resolver assigned it (or the
// application wrote `layout(location=) out Blk {...}`), and on the MEMBERS when the
// application located those instead - in which case glslang puts nothing on the
// variable at all and SPIRV-Cross suppresses the block-level qualifier in favour of
// the member ones. A variable-only strip would silently pass that second shape by.
// A struct reached by an interface variable whose direction is NOT armed keeps its
// member decorations: they belong to the type, and taking them off would strip the
// unarmed side too.
//
// The two directions are armed SEPARATELY by the caller, because an interface
// whose other end lives in a DIFFERENT program (a separable program pipeline)
// must keep its location: that is the only thing matching it there, and the other