mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Fix, Test] (DirectVulkan, ShaderTranspiler, TraceReplay): derive NumSubgroups behind opt-in quirk
This commit is contained in:
@@ -435,6 +435,9 @@ jobs:
|
||||
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
|
||||
extra_retrace_args+=(--coherent-as-flush)
|
||||
fi
|
||||
if [ "${{ matrix.case.num_subgroups_quirk || false }}" = "true" ]; then
|
||||
extra_retrace_args+=(--num-subgroups-quirk)
|
||||
fi
|
||||
|
||||
run_retrace() {
|
||||
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
|
||||
|
||||
@@ -284,6 +284,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
|
||||
@@ -80,6 +80,11 @@ namespace MobileGL::MG_Config {
|
||||
#endif
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||
Bool DisableSubgroup = false;
|
||||
// MOBILEGL_NUM_SUBGROUPS_QUIRK: derive compute gl_NumSubgroups from the local
|
||||
// workgroup dimensions and gl_SubgroupSize instead of reading Vulkan's
|
||||
// NumSubgroups builtin. Off by default; enable only for drivers whose builtin
|
||||
// disagrees with the SubgroupId topology emitted by the same dispatch.
|
||||
Bool NumSubgroupsQuirk = false;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
|
||||
@@ -168,6 +168,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.NumSubgroupsQuirk = QueryEnvFlag("MOBILEGL_NUM_SUBGROUPS_QUIRK");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
|
||||
#include "Config.h"
|
||||
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||
@@ -3163,6 +3164,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// NumSubgroups is defined by the local workgroup dimensions and SubgroupSize. Derive
|
||||
// it in SPIR-V instead of trusting a driver builtin that can disagree with the
|
||||
// SubgroupId topology produced by the same compute dispatch (Adreno reports 1 while
|
||||
// emitting IDs 0..7 for a 512-invocation, 64-wide workgroup).
|
||||
if (MG_Config::Features.NumSubgroupsQuirk && shaders[i] &&
|
||||
shaders[i]->GetShaderStage() == ShaderStage::Compute) {
|
||||
Vector<Uint> derivedNumSubgroupsSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DeriveNumSubgroupsForVulkan(
|
||||
moduleSpirvs[i], derivedNumSubgroupsSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(derivedNumSubgroupsSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to derive gl_NumSubgroups for program %u; "
|
||||
"compute shaders may observe a driver-inconsistent subgroup count",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
||||
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
||||
// stored as - which addresses [0,1] where the application addressed texels.
|
||||
|
||||
@@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.14)
|
||||
add_executable(
|
||||
SpirvPassTest
|
||||
SpirvPassTest.cpp
|
||||
DeriveNumSubgroupsTest.cpp
|
||||
DemoteFloat64Test.cpp
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DeriveNumSubgroupsTest.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 <gtest/gtest.h>
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
||||
offset += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileCompute(const String& source) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_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_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
Uint32 FindBuiltinTarget(const Vector<Uint32>& spirv, spv::BuiltIn builtin) {
|
||||
Uint32 target = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == spv::Op::OpDecorate && wordCount >= 4u &&
|
||||
static_cast<spv::Decoration>(words[2]) == spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(words[3]) == builtin) {
|
||||
target = words[1];
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
Uint32 CountLoadsFrom(const Vector<Uint32>& spirv, Uint32 pointerId) {
|
||||
Uint32 count = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == spv::Op::OpLoad && wordCount >= 4u && words[3] == pointerId) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
|
||||
Uint32 count = 0u;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == wanted) ++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
constexpr const char* kNumSubgroupsOnlySource = R"(#version 450 core
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint value; } outputData;
|
||||
void main() {
|
||||
if (gl_LocalInvocationIndex == 0u)
|
||||
outputData.value = gl_NumSubgroups;
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kNoNumSubgroupsSource = R"(#version 450 core
|
||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||
layout(std430, binding = 0) buffer Output { uint value; } outputData;
|
||||
void main() {
|
||||
if (gl_LocalInvocationIndex == 0u)
|
||||
outputData.value = gl_WorkGroupSize.x;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(DeriveNumSubgroupsPass, ReplacesBuiltinLoadAndSynthesizesSubgroupSize) {
|
||||
const Vector<Uint32> input = CompileCompute(kNumSubgroupsOnlySource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Uint32 inputNumSubgroups = FindBuiltinTarget(input, spv::BuiltIn::NumSubgroups);
|
||||
ASSERT_NE(inputNumSubgroups, 0u);
|
||||
EXPECT_EQ(CountLoadsFrom(input, inputNumSubgroups), 1u);
|
||||
EXPECT_EQ(FindBuiltinTarget(input, spv::BuiltIn::SubgroupSize), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true));
|
||||
ASSERT_TRUE(Validates(output));
|
||||
|
||||
const Uint32 outputNumSubgroups = FindBuiltinTarget(output, spv::BuiltIn::NumSubgroups);
|
||||
const Uint32 outputSubgroupSize = FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize);
|
||||
ASSERT_NE(outputNumSubgroups, 0u);
|
||||
ASSERT_NE(outputSubgroupSize, 0u);
|
||||
EXPECT_EQ(CountLoadsFrom(output, outputNumSubgroups), 0u);
|
||||
EXPECT_EQ(CountLoadsFrom(output, outputSubgroupSize), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpCompositeExtract), 3u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpIMul), 2u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpUDiv), 1u);
|
||||
}
|
||||
|
||||
TEST(DeriveNumSubgroupsPass, IsIdempotent) {
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(CompileCompute(kNumSubgroupsOnlySource), once, true));
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
|
||||
TEST(DeriveNumSubgroupsPass, LeavesUnrelatedComputeShaderUntouched) {
|
||||
const Vector<Uint32> input = CompileCompute(kNoNumSubgroupsSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
EXPECT_EQ(FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize), 0u);
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||
@@ -883,6 +884,17 @@ namespace MobileGL {
|
||||
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(DeriveNumSubgroupsPass::CreateDeriveNumSubgroupsPass());
|
||||
|
||||
return RunOptimizerChecked("DeriveNumSubgroupsForVulkan", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -145,6 +145,13 @@ namespace MobileGL {
|
||||
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Replaces compute gl_NumSubgroups loads with the value derived from the local
|
||||
// workgroup dimensions and gl_SubgroupSize. DirectVulkan only; this avoids a
|
||||
// driver builtin that can disagree with the subgroup IDs the same dispatch emits.
|
||||
// See DeriveNumSubgroupsPass.
|
||||
static bool DeriveNumSubgroupsForVulkan(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,251 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.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 "DeriveNumSubgroupsPass.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 {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
for (auto& annotation : context->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::BuiltIn) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != builtin) {
|
||||
continue;
|
||||
}
|
||||
return defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsInputPointerTo(IRContext* context, const Instruction* variable, uint32_t pointeeTypeId) {
|
||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
|
||||
variable->NumInOperands() < 1 ||
|
||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* pointerType = context->get_def_use_mgr()->GetDef(variable->type_id());
|
||||
return pointerType != nullptr && pointerType->opcode() == spv::Op::OpTypePointer &&
|
||||
pointerType->NumInOperands() >= 2 &&
|
||||
static_cast<spv::StorageClass>(pointerType->GetSingleWordInOperand(0)) ==
|
||||
spv::StorageClass::Input &&
|
||||
pointerType->GetSingleWordInOperand(1) == pointeeTypeId;
|
||||
}
|
||||
|
||||
bool IsUnsignedInt32(IRContext* context, uint32_t typeId) {
|
||||
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
||||
return type != nullptr && type->opcode() == spv::Op::OpTypeInt &&
|
||||
type->NumInOperands() >= 2 && type->GetSingleWordInOperand(0) == 32u &&
|
||||
type->GetSingleWordInOperand(1) == 0u;
|
||||
}
|
||||
|
||||
uint32_t SynthesizeSubgroupSizeVariable(IRContext* context, uint32_t pointerTypeId) {
|
||||
const uint32_t variableId = context->TakeNextId();
|
||||
context->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
context, spv::Op::OpVariable, pointerTypeId, variableId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Input)}}}));
|
||||
context->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||
context, spv::Op::OpDecorate, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {variableId}},
|
||||
{SPV_OPERAND_TYPE_DECORATION,
|
||||
{static_cast<uint32_t>(spv::Decoration::BuiltIn)}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
||||
{static_cast<uint32_t>(spv::BuiltIn::SubgroupSize)}}}));
|
||||
|
||||
for (Instruction& entryPoint : context->module()->entry_points()) {
|
||||
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
|
||||
}
|
||||
return variableId;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status DeriveNumSubgroupsPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
Instruction* numSubgroupsVar = FindBuiltinDefinition(irContext, spv::BuiltIn::NumSubgroups);
|
||||
if (numSubgroupsVar == nullptr) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
std::vector<Instruction*> numSubgroupsLoads;
|
||||
bool sawUnexpectedUser = false;
|
||||
const uint32_t numSubgroupsVarId = numSubgroupsVar->result_id();
|
||||
defUseMgr->ForEachUser(numSubgroupsVar, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpLoad:
|
||||
if (user->NumInOperands() >= 1 &&
|
||||
user->GetSingleWordInOperand(0) == numSubgroupsVarId) {
|
||||
numSubgroupsLoads.push_back(user);
|
||||
} else {
|
||||
sawUnexpectedUser = true;
|
||||
}
|
||||
return;
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
case spv::Op::OpDecorateString:
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpEntryPoint:
|
||||
return;
|
||||
default:
|
||||
sawUnexpectedUser = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (sawUnexpectedUser) {
|
||||
return Status::Failure;
|
||||
}
|
||||
if (numSubgroupsLoads.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
const uint32_t valueTypeId = numSubgroupsLoads.front()->type_id();
|
||||
if (!IsUnsignedInt32(irContext, valueTypeId) ||
|
||||
!IsInputPointerTo(irContext, numSubgroupsVar, valueTypeId)) {
|
||||
return Status::Failure;
|
||||
}
|
||||
for (const Instruction* load : numSubgroupsLoads) {
|
||||
if (load->type_id() != valueTypeId) {
|
||||
return Status::Failure;
|
||||
}
|
||||
}
|
||||
|
||||
Instruction* workgroupSize = FindBuiltinDefinition(irContext, spv::BuiltIn::WorkgroupSize);
|
||||
if (workgroupSize == nullptr ||
|
||||
(workgroupSize->opcode() != spv::Op::OpConstantComposite &&
|
||||
workgroupSize->opcode() != spv::Op::OpSpecConstantComposite)) {
|
||||
return Status::Failure;
|
||||
}
|
||||
const Instruction* workgroupSizeType = defUseMgr->GetDef(workgroupSize->type_id());
|
||||
if (workgroupSizeType == nullptr || workgroupSizeType->opcode() != spv::Op::OpTypeVector ||
|
||||
workgroupSizeType->NumInOperands() < 2 ||
|
||||
workgroupSizeType->GetSingleWordInOperand(0) != valueTypeId ||
|
||||
workgroupSizeType->GetSingleWordInOperand(1) != 3u) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
Instruction* subgroupSizeVar = FindBuiltinDefinition(irContext, spv::BuiltIn::SubgroupSize);
|
||||
if (subgroupSizeVar != nullptr &&
|
||||
!IsInputPointerTo(irContext, subgroupSizeVar, valueTypeId)) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
const auto* valueType = typeMgr->GetType(valueTypeId);
|
||||
if (valueType == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
const auto* one = constantMgr->GetConstant(valueType, {1u});
|
||||
const Instruction* oneInst =
|
||||
one != nullptr ? constantMgr->GetDefiningInstruction(one, valueTypeId) : nullptr;
|
||||
if (oneInst == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
const uint32_t oneId = oneInst->result_id();
|
||||
|
||||
const uint32_t subgroupSizeVarId = subgroupSizeVar != nullptr
|
||||
? subgroupSizeVar->result_id()
|
||||
: SynthesizeSubgroupSizeVariable(irContext, numSubgroupsVar->type_id());
|
||||
const uint32_t workgroupSizeId = workgroupSize->result_id();
|
||||
|
||||
// The pipeline never enables ALLOW_VARYING_SUBGROUP_SIZE, so Vulkan's fixed
|
||||
// subgroup partition is exactly ceil(local invocation count / SubgroupSize).
|
||||
// `(count - 1) / size + 1` avoids an addition overflow at count + size - 1.
|
||||
for (Instruction* load : numSubgroupsLoads) {
|
||||
const uint32_t localSizeXId = irContext->TakeNextId();
|
||||
const uint32_t localSizeYId = irContext->TakeNextId();
|
||||
const uint32_t localSizeZId = irContext->TakeNextId();
|
||||
const uint32_t localSizeXYId = irContext->TakeNextId();
|
||||
const uint32_t invocationCountId = irContext->TakeNextId();
|
||||
const uint32_t adjustedCountId = irContext->TakeNextId();
|
||||
const uint32_t subgroupSizeId = irContext->TakeNextId();
|
||||
const uint32_t quotientId = irContext->TakeNextId();
|
||||
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeXId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}}}));
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeYId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {1u}}}));
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeZId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {2u}}}));
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpIMul, valueTypeId, localSizeXYId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {localSizeXId}},
|
||||
{SPV_OPERAND_TYPE_ID, {localSizeYId}}}));
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpIMul, valueTypeId, invocationCountId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {localSizeXYId}},
|
||||
{SPV_OPERAND_TYPE_ID, {localSizeZId}}}));
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpISub, valueTypeId, adjustedCountId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {invocationCountId}},
|
||||
{SPV_OPERAND_TYPE_ID, {oneId}}}));
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLoad, valueTypeId, subgroupSizeId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {subgroupSizeVarId}}}));
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpUDiv, valueTypeId, quotientId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {adjustedCountId}},
|
||||
{SPV_OPERAND_TYPE_ID, {subgroupSizeId}}}));
|
||||
|
||||
// Preserve the original result id so every downstream use automatically sees
|
||||
// the derived value instead of the driver's NumSubgroups builtin.
|
||||
load->SetOpcode(spv::Op::OpIAdd);
|
||||
load->SetInOperands(Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_ID, {quotientId}},
|
||||
{SPV_OPERAND_TYPE_ID, {oneId}}});
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken DeriveNumSubgroupsPass::CreateDeriveNumSubgroupsPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<DeriveNumSubgroupsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,37 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.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"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Replaces compute-stage NumSubgroups builtin loads with
|
||||
// ceil(WorkgroupSize.x * WorkgroupSize.y * WorkgroupSize.z / SubgroupSize).
|
||||
//
|
||||
// That is the value Vulkan defines for NumSubgroups when the pipeline does not
|
||||
// enable varying subgroup sizes, which MobileGL never does. Deriving it avoids
|
||||
// drivers that expose the real SubgroupId topology but return an inconsistent
|
||||
// NumSubgroups value. This is a DirectVulkan semantic repair, not a source-shader
|
||||
// rewrite; the application's subgroup arithmetic and shared-memory logic remain
|
||||
// unchanged.
|
||||
class DeriveNumSubgroupsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "derive-num-subgroups"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateDeriveNumSubgroupsPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -164,6 +164,11 @@ bool LoadMobileGL(const Request& request, std::string& error) {
|
||||
} else {
|
||||
unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
}
|
||||
if (request.numSubgroupsQuirk) {
|
||||
setenv("MOBILEGL_NUM_SUBGROUPS_QUIRK", "1", 1);
|
||||
} else {
|
||||
unsetenv("MOBILEGL_NUM_SUBGROUPS_QUIRK");
|
||||
}
|
||||
if (request.fboAttachmentDumps.empty()) {
|
||||
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
|
||||
} else {
|
||||
@@ -818,6 +823,7 @@ bool WriteResultJson(const Request& request, const Result& result) {
|
||||
<< (request.avoidAngleLlvmpipeSamplerMipmapMinFilter ? "true" : "false") << ",\n";
|
||||
file << " \"avoidAngleLlvmpipeExplicitLodBias\": "
|
||||
<< (request.avoidAngleLlvmpipeExplicitLodBias ? "true" : "false") << ",\n";
|
||||
file << " \"numSubgroupsQuirk\": " << (request.numSubgroupsQuirk ? "true" : "false") << ",\n";
|
||||
file << " \"holdMs\": " << request.holdMs << ",\n";
|
||||
file << " \"mismatchPixels\": " << result.mismatchPixels << "\n";
|
||||
file << "}\n";
|
||||
|
||||
@@ -44,6 +44,7 @@ struct Request {
|
||||
bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false;
|
||||
bool avoidAngleLlvmpipeExplicitLodBias = false;
|
||||
bool coherentAsFlush = false;
|
||||
bool numSubgroupsQuirk = false;
|
||||
int holdMs = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
|
||||
jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||
jboolean avoidAngleLlvmpipeExplicitLodBias,
|
||||
jboolean coherentAsFlush,
|
||||
jboolean numSubgroupsQuirk,
|
||||
jstring texture2dDumps) {
|
||||
mobilegl_trace::Request request;
|
||||
request.tracePath = ToString(env, tracePath);
|
||||
@@ -150,6 +151,7 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
|
||||
avoidAngleLlvmpipeSamplerMipmapMinFilter == JNI_TRUE;
|
||||
request.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias == JNI_TRUE;
|
||||
request.coherentAsFlush = coherentAsFlush == JNI_TRUE;
|
||||
request.numSubgroupsQuirk = numSubgroupsQuirk == JNI_TRUE;
|
||||
|
||||
ScopedTraceReplayState replayState;
|
||||
mobilegl_trace_set_requested_size(request.width, request.height);
|
||||
|
||||
@@ -116,6 +116,7 @@ public final class TraceReplayActivity extends Activity {
|
||||
request.avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||
request.avoidAngleLlvmpipeExplicitLodBias,
|
||||
request.coherentAsFlush,
|
||||
request.numSubgroupsQuirk,
|
||||
request.texture2dDumps
|
||||
);
|
||||
Log.i(TAG, result.toString());
|
||||
@@ -149,6 +150,7 @@ public final class TraceReplayActivity extends Activity {
|
||||
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||
boolean avoidAngleLlvmpipeExplicitLodBias,
|
||||
boolean coherentAsFlush,
|
||||
boolean numSubgroupsQuirk,
|
||||
String texture2dDumps
|
||||
);
|
||||
|
||||
@@ -174,6 +176,7 @@ public final class TraceReplayActivity extends Activity {
|
||||
final boolean avoidAngleLlvmpipeSamplerMipmapMinFilter;
|
||||
final boolean avoidAngleLlvmpipeExplicitLodBias;
|
||||
final boolean coherentAsFlush;
|
||||
final boolean numSubgroupsQuirk;
|
||||
final String texture2dDumps;
|
||||
|
||||
private TraceReplayRequest(
|
||||
@@ -198,6 +201,7 @@ public final class TraceReplayActivity extends Activity {
|
||||
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||
boolean avoidAngleLlvmpipeExplicitLodBias,
|
||||
boolean coherentAsFlush,
|
||||
boolean numSubgroupsQuirk,
|
||||
String texture2dDumps
|
||||
) {
|
||||
this.tracePath = tracePath;
|
||||
@@ -221,6 +225,7 @@ public final class TraceReplayActivity extends Activity {
|
||||
this.avoidAngleLlvmpipeSamplerMipmapMinFilter = avoidAngleLlvmpipeSamplerMipmapMinFilter;
|
||||
this.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias;
|
||||
this.coherentAsFlush = coherentAsFlush;
|
||||
this.numSubgroupsQuirk = numSubgroupsQuirk;
|
||||
this.texture2dDumps = texture2dDumps;
|
||||
}
|
||||
|
||||
@@ -249,6 +254,7 @@ public final class TraceReplayActivity extends Activity {
|
||||
intent.getBooleanExtra("avoid_angle_llvmpipe_sampler_mipmap_min_filter", false),
|
||||
intent.getBooleanExtra("avoid_angle_llvmpipe_explicit_lod_bias", false),
|
||||
intent.getBooleanExtra("coherent_as_flush", false),
|
||||
intent.getBooleanExtra("num_subgroups_quirk", false),
|
||||
readString(intent, "texture_2d_dumps", "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ Usage:
|
||||
[--avoid-angle-llvmpipe-sampler-mipmap-min-filter] \
|
||||
[--avoid-angle-llvmpipe-explicit-lod-bias] \
|
||||
[--coherent-as-flush] \
|
||||
[--num-subgroups-quirk] \
|
||||
[--dump-texture-2d CALL,TEXTURE,LEVEL,DIR] \
|
||||
--timeout-seconds N
|
||||
|
||||
@@ -47,6 +48,8 @@ sample with an explicit LOD that ANGLE llvmpipe cannot take a LOD bias on
|
||||
(MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1).
|
||||
Pass --coherent-as-flush for traces whose engine writes persistent
|
||||
GL_MAP_FLUSH_EXPLICIT_BIT maps it never flushes (MOBILEGL_COHERENT_AS_FLUSH=1).
|
||||
Pass --num-subgroups-quirk to derive compute gl_NumSubgroups instead of reading
|
||||
the Vulkan builtin (MOBILEGL_NUM_SUBGROUPS_QUIRK=1).
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -105,6 +108,7 @@ use_pbuffer=0
|
||||
avoid_angle_llvmpipe_sampler_mipmap_min_filter=0
|
||||
avoid_angle_llvmpipe_explicit_lod_bias=0
|
||||
coherent_as_flush=0
|
||||
num_subgroups_quirk=0
|
||||
texture_2d_dumps=""
|
||||
timeout_seconds=""
|
||||
|
||||
@@ -146,6 +150,7 @@ while [ "$#" -gt 0 ]; do
|
||||
shift 1
|
||||
;;
|
||||
--coherent-as-flush) coherent_as_flush=1; shift 1 ;;
|
||||
--num-subgroups-quirk) num_subgroups_quirk=1; shift 1 ;;
|
||||
--dump-texture-2d) texture_2d_dumps="$(next_arg "$@")"; shift 2 ;;
|
||||
--timeout-seconds) timeout_seconds="$(next_arg "$@")"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
@@ -266,7 +271,7 @@ copy_app_artifact() {
|
||||
}
|
||||
|
||||
copy_texture_2d_dumps() {
|
||||
[ -n "${texture_2d_dumps}" ] || return
|
||||
[ -n "${texture_2d_dumps}" ] || return 0
|
||||
saved_ifs="${IFS}"
|
||||
IFS=';'
|
||||
set -- ${texture_2d_dumps}
|
||||
@@ -363,6 +368,9 @@ run_retrace() {
|
||||
if [ "${coherent_as_flush}" -eq 1 ]; then
|
||||
set -- "$@" --ez coherent_as_flush true
|
||||
fi
|
||||
if [ "${num_subgroups_quirk}" -eq 1 ]; then
|
||||
set -- "$@" --ez num_subgroups_quirk true
|
||||
fi
|
||||
if [ -n "${texture_2d_dumps}" ]; then
|
||||
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
|
||||
fi
|
||||
|
||||
@@ -284,7 +284,8 @@
|
||||
"golden": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.0000202020.png",
|
||||
"target_call": 202020,
|
||||
"timeout_seconds": 1800,
|
||||
"ssim_threshold": 0.98
|
||||
"ssim_threshold": 0.98,
|
||||
"num_subgroups_quirk": true
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.4-fabric-iris-bsl-esc-menu-854",
|
||||
|
||||
Reference in New Issue
Block a user