mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[Fix, Test] (DirectVulkan, ShaderTranspiler, TraceReplay): repair iterationRP's missing reduction barrier
Program 203 reuses prefixSumCache for a second subgroup reduction before every workgroup invocation has consumed the first result. Add a fingerprint-gated SPIR-V pass that inserts the missing Workgroup acquire-release barrier while preserving native subgroup operations. Keep the repair opt-in behind MOBILEGL_ITERATIONRP_FIX_BARRIER, cover insertion, pass-through, and idempotence, and enable it together with the existing iterationRP subgroup repairs for the matching Linux and Android CI retraces.
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
|
||||
#include "SpirvPasses/EmulateSubgroupsPass.h"
|
||||
#include "SpirvPasses/FixIterationRPBarrierPass.h"
|
||||
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
@@ -924,6 +925,17 @@ namespace MobileGL {
|
||||
inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FixIterationRPBarrierForVulkan(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass());
|
||||
|
||||
return RunOptimizerChecked("FixIterationRPBarrierForVulkan", optimizer,
|
||||
inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -179,6 +179,12 @@ namespace MobileGL {
|
||||
Uint32 nativeSubgroupSize,
|
||||
Uint32 maxWorkgroupScratchBytes,
|
||||
bool enableSpirvValidation = false);
|
||||
// Inserts the missing workgroup rendezvous between Program 203's two
|
||||
// prefixSumCache reductions. Fingerprint-gated to the iterationRP shape;
|
||||
// unrelated and already-repaired modules pass through byte-identical.
|
||||
static bool FixIterationRPBarrierForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
|
||||
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
|
||||
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
||||
// Copyright (c) 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 "FixIterationRPBarrierPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
const Instruction* RootVariable(IRContext* context, uint32_t pointerId) {
|
||||
const Instruction* def = context->get_def_use_mgr()->GetDef(pointerId);
|
||||
while (def != nullptr) {
|
||||
switch (def->opcode()) {
|
||||
case spv::Op::OpVariable:
|
||||
return def;
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
case spv::Op::OpCopyObject:
|
||||
def = context->get_def_use_mgr()->GetDef(def->GetSingleWordInOperand(0));
|
||||
break;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsUintConstant(IRContext* context, uint32_t id, uint32_t wanted) {
|
||||
const Instruction* def = context->get_def_use_mgr()->GetDef(id);
|
||||
return def != nullptr && def->opcode() == spv::Op::OpConstant && def->NumInOperands() == 1u &&
|
||||
def->GetSingleWordInOperand(0) == wanted;
|
||||
}
|
||||
|
||||
bool IsZeroElementPointer(IRContext* context, uint32_t pointerId, const Instruction** root) {
|
||||
const Instruction* pointer = context->get_def_use_mgr()->GetDef(pointerId);
|
||||
if (pointer == nullptr ||
|
||||
(pointer->opcode() != spv::Op::OpAccessChain && pointer->opcode() != spv::Op::OpInBoundsAccessChain) ||
|
||||
pointer->NumInOperands() < 2u) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 1u; i < pointer->NumInOperands(); ++i) {
|
||||
if (!IsUintConstant(context, pointer->GetSingleWordInOperand(i), 0u)) return false;
|
||||
}
|
||||
*root = RootVariable(context, pointerId);
|
||||
return *root != nullptr;
|
||||
}
|
||||
|
||||
bool IsWorkgroupVec2Array(IRContext* context, const Instruction* variable) {
|
||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable || variable->NumInOperands() < 1u ||
|
||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) != spv::StorageClass::Workgroup) {
|
||||
return false;
|
||||
}
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
const Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
|
||||
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer ||
|
||||
pointerType->NumInOperands() < 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* arrayType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
|
||||
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray ||
|
||||
arrayType->NumInOperands() < 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* length = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
|
||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant || length->NumInOperands() != 1u) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t arrayLength = length->GetSingleWordInOperand(0);
|
||||
if (arrayLength < 32u || arrayLength > 512u) return false;
|
||||
|
||||
const Instruction* vectorType = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(0));
|
||||
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
|
||||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* scalarType = defUseMgr->GetDef(vectorType->GetSingleWordInOperand(0));
|
||||
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
|
||||
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
|
||||
}
|
||||
|
||||
bool IsVec2FloatInclusiveAdd(IRContext* context, const Instruction* inst) {
|
||||
if (inst->opcode() != spv::Op::OpGroupNonUniformFAdd || inst->NumInOperands() < 3u ||
|
||||
static_cast<spv::GroupOperation>(inst->GetSingleWordInOperand(1)) !=
|
||||
spv::GroupOperation::InclusiveScan) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* vectorType = context->get_def_use_mgr()->GetDef(inst->type_id());
|
||||
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
|
||||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* scalarType = context->get_def_use_mgr()->GetDef(vectorType->GetSingleWordInOperand(0));
|
||||
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
|
||||
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
|
||||
}
|
||||
|
||||
bool HasProgram203LocalSize(IRContext* context) {
|
||||
for (const Instruction& entryPoint : context->module()->entry_points()) {
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::GLCompute) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const Instruction& mode : context->module()->execution_modes()) {
|
||||
if (mode.opcode() == spv::Op::OpExecutionMode && mode.NumInOperands() >= 5u &&
|
||||
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) == spv::ExecutionMode::LocalSize) {
|
||||
return mode.GetSingleWordInOperand(2) == 32u && mode.GetSingleWordInOperand(3) == 16u &&
|
||||
mode.GetSingleWordInOperand(4) == 1u;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsStoreToRoot(IRContext* context, const Instruction* inst, const Instruction* root) {
|
||||
return inst->opcode() == spv::Op::OpStore && inst->NumInOperands() >= 2u &&
|
||||
RootVariable(context, inst->GetSingleWordInOperand(0)) == root;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status FixIterationRPBarrierPass::Process() {
|
||||
auto* irContext = context();
|
||||
if (!HasProgram203LocalSize(irContext)) return Status::SuccessWithoutChange;
|
||||
|
||||
for (auto& function : *irContext->module()) {
|
||||
std::vector<Instruction*> instructions;
|
||||
std::vector<size_t> scans;
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
if (IsVec2FloatInclusiveAdd(irContext, &inst)) scans.push_back(instructions.size());
|
||||
instructions.push_back(&inst);
|
||||
}
|
||||
}
|
||||
// Program 203 has exactly two vec2 inclusive adds: the luminance reduction
|
||||
// and the weighted-exposure reduction. More or fewer is not our fingerprint.
|
||||
if (scans.size() != 2u) continue;
|
||||
|
||||
const size_t firstScan = scans[0];
|
||||
const size_t secondScan = scans[1];
|
||||
const Instruction* scratch = nullptr;
|
||||
size_t averageLoad = instructions.size();
|
||||
|
||||
for (size_t i = firstScan + 1u; i < secondScan; ++i) {
|
||||
Instruction* inst = instructions[i];
|
||||
if (inst->opcode() != spv::Op::OpLoad || inst->NumInOperands() < 1u) continue;
|
||||
const Instruction* root = nullptr;
|
||||
if (!IsZeroElementPointer(irContext, inst->GetSingleWordInOperand(0), &root) ||
|
||||
!IsWorkgroupVec2Array(irContext, root)) {
|
||||
continue;
|
||||
}
|
||||
// The broadcast is read as prefixSumCache[0].x, hence a scalar load.
|
||||
const Instruction* type = irContext->get_def_use_mgr()->GetDef(inst->type_id());
|
||||
if (type == nullptr || type->opcode() != spv::Op::OpTypeFloat || type->NumInOperands() != 1u ||
|
||||
type->GetSingleWordInOperand(0) != 32u) {
|
||||
continue;
|
||||
}
|
||||
scratch = root;
|
||||
averageLoad = i;
|
||||
break;
|
||||
}
|
||||
if (scratch == nullptr) continue;
|
||||
|
||||
bool sawZeroBroadcastStore = false;
|
||||
bool sawPublishBarrier = false;
|
||||
for (size_t i = firstScan + 1u; i < averageLoad; ++i) {
|
||||
const Instruction* root = nullptr;
|
||||
if (instructions[i]->opcode() == spv::Op::OpStore &&
|
||||
IsZeroElementPointer(irContext, instructions[i]->GetSingleWordInOperand(0), &root) &&
|
||||
root == scratch) {
|
||||
sawZeroBroadcastStore = true;
|
||||
} else if (sawZeroBroadcastStore && instructions[i]->opcode() == spv::Op::OpControlBarrier) {
|
||||
sawPublishBarrier = true;
|
||||
}
|
||||
}
|
||||
if (!sawZeroBroadcastStore || !sawPublishBarrier) continue;
|
||||
|
||||
bool alreadySynchronized = false;
|
||||
for (size_t i = averageLoad + 1u; i < secondScan; ++i) {
|
||||
if (instructions[i]->opcode() == spv::Op::OpControlBarrier) {
|
||||
alreadySynchronized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alreadySynchronized) return Status::SuccessWithoutChange;
|
||||
|
||||
bool secondPhaseReusesScratch = false;
|
||||
for (size_t i = secondScan + 1u; i < instructions.size(); ++i) {
|
||||
if (IsStoreToRoot(irContext, instructions[i], scratch)) {
|
||||
secondPhaseReusesScratch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!secondPhaseReusesScratch) continue;
|
||||
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
const uint32_t scopeId = constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::Scope::Workgroup));
|
||||
const uint32_t semanticsId =
|
||||
constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
|
||||
static_cast<uint32_t>(spv::MemorySemanticsMask::WorkgroupMemory));
|
||||
if (scopeId == 0u || semanticsId == 0u) return Status::Failure;
|
||||
|
||||
instructions[secondScan]->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpControlBarrier, 0u, 0u,
|
||||
Instruction::OperandList{Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
|
||||
Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
|
||||
Operand{SPV_OPERAND_TYPE_ID, {semanticsId}}}));
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<FixIterationRPBarrierPass>());
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::ShaderTranspiler
|
||||
@@ -0,0 +1,28 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.h
|
||||
// Copyright (c) 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 "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// Repairs iterationRP Program 203's missing workgroup rendezvous between two
|
||||
// reductions that reuse prefixSumCache. The first phase broadcasts its result
|
||||
// through prefixSumCache[0], but the second phase may overwrite that element before
|
||||
// every invocation has read it. The pass fingerprints that exact two-scan,
|
||||
// 512-invocation shape and inserts one Workgroup control barrier immediately before
|
||||
// the second scan. Unrelated modules and already-repaired modules are byte-identical.
|
||||
class FixIterationRPBarrierPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "fix-iterationrp-barrier"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateFixIterationRPBarrierPass();
|
||||
};
|
||||
} // namespace MobileGL::MG_Util::ShaderTranspiler
|
||||
Reference in New Issue
Block a user