From 2b6c2b561ca90dc53ca43f78278b82ec928deb51 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 18 Aug 2026 22:31:57 -0400 Subject: [PATCH] [Fix, Test] (DirectVulkan, ShaderTranspiler, TraceReplay): derive NumSubgroups behind opt-in quirk --- .github/workflows/apk.yml | 3 + CMakeLists.txt | 1 + MobileGL/Config.h | 5 + MobileGL/ConfigLoader.cpp | 1 + .../DirectVulkan/Renderer/ProgramFactory.cpp | 18 ++ .../MG_Test/ShaderTranspiler/CMakeLists.txt | 1 + .../DeriveNumSubgroupsTest.cpp | 147 ++++++++++ .../ShaderTranspiler/ShaderCompiler.cpp | 12 + .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 7 + .../SpirvPasses/DeriveNumSubgroupsPass.cpp | 251 ++++++++++++++++++ .../SpirvPasses/DeriveNumSubgroupsPass.h | 37 +++ .../app/src/trace/cpp/trace_replay_core.cpp | 6 + .../app/src/trace/cpp/trace_replay_core.hpp | 1 + .../app/src/trace/cpp/trace_replay_jni.cpp | 2 + .../plugin/trace/TraceReplayActivity.java | 6 + android-plugin/trace-replay-ci.sh | 10 +- tools/trace_replay/trace_cases.json | 3 +- 17 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 MobileGL/MG_Test/ShaderTranspiler/DeriveNumSubgroupsTest.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.h diff --git a/.github/workflows/apk.yml b/.github/workflows/apk.yml index d8cb80e8..e8801850 100644 --- a/.github/workflows/apk.yml +++ b/.github/workflows/apk.yml @@ -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 \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 483c6fed..5ffc7bca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/Config.h b/MobileGL/Config.h index beefdba9..b0540a60 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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 diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 0b0df12a..4e20e8e3 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 4a5fe2fc..151ffc1e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -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 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. diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index 25dacdd2..91db71dd 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.14) add_executable( SpirvPassTest SpirvPassTest.cpp + DeriveNumSubgroupsTest.cpp DemoteFloat64Test.cpp FlattenXfbInterfaceBlocksTest.cpp ) diff --git a/MobileGL/MG_Test/ShaderTranspiler/DeriveNumSubgroupsTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/DeriveNumSubgroupsTest.cpp new file mode 100644 index 00000000..b3392549 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/DeriveNumSubgroupsTest.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 + +#define SPV_ENABLE_UTILITY_CODE +#include "glslang/SPIRV/spirv.hpp11" +#undef SPV_ENABLE_UTILITY_CODE + +#include "Includes.h" +#include +#include + +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + constexpr SizeT kSpirvHeaderWordCount = 5u; + + template + void ForEachInstruction(const Vector& 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(spirv[offset] & 0xffffu), &spirv[offset], wordCount); + offset += wordCount; + } + } + + Vector 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& 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(words[2]) == spv::Decoration::BuiltIn && + static_cast(words[3]) == builtin) { + target = words[1]; + } + }); + return target; + } + + Uint32 CountLoadsFrom(const Vector& 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& 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& 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 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 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 once; + ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(CompileCompute(kNumSubgroupsOnlySource), once, true)); + Vector twice; + ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(once, twice, true)); + EXPECT_EQ(twice, once); +} + +TEST(DeriveNumSubgroupsPass, LeavesUnrelatedComputeShaderUntouched) { + const Vector input = CompileCompute(kNoNumSubgroupsSource); + ASSERT_FALSE(input.empty()); + Vector output; + ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true)); + EXPECT_EQ(output, input); + EXPECT_EQ(FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize), 0u); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 661d373a..28367501 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -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& inputBinary, + Vector& 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& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { using namespace spvtools; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 1864dc3a..5073de2b 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -145,6 +145,13 @@ namespace MobileGL { static bool ZeroBaseVertexForVulkan(const Vector& inputBinary, Vector& 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& inputBinary, + Vector& 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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp new file mode 100644 index 00000000..f3fbeacf --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp @@ -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 + +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(annotation.GetSingleWordInOperand(1)) != + spv::Decoration::BuiltIn) { + continue; + } + if (static_cast(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(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(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( + context, spv::Op::OpVariable, pointerTypeId, variableId, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, + {static_cast(spv::StorageClass::Input)}}})); + context->AddAnnotationInst(spvtools::MakeUnique( + context, spv::Op::OpDecorate, 0, 0, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {variableId}}, + {SPV_OPERAND_TYPE_DECORATION, + {static_cast(spv::Decoration::BuiltIn)}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, + {static_cast(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 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( + irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeXId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {workgroupSizeId}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}}})); + load->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeYId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {workgroupSizeId}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {1u}}})); + load->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeZId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {workgroupSizeId}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {2u}}})); + load->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpIMul, valueTypeId, localSizeXYId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {localSizeXId}}, + {SPV_OPERAND_TYPE_ID, {localSizeYId}}})); + load->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpIMul, valueTypeId, invocationCountId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {localSizeXYId}}, + {SPV_OPERAND_TYPE_ID, {localSizeZId}}})); + load->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpISub, valueTypeId, adjustedCountId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {invocationCountId}}, + {SPV_OPERAND_TYPE_ID, {oneId}}})); + load->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpLoad, valueTypeId, subgroupSizeId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {subgroupSizeVarId}}})); + load->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpUDiv, valueTypeId, quotientId, + std::initializer_list{ + {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()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.h new file mode 100644 index 00000000..330c17a4 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.h @@ -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 + +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 diff --git a/android-plugin/app/src/trace/cpp/trace_replay_core.cpp b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp index 151fa98c..7a700921 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_core.cpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp @@ -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"; diff --git a/android-plugin/app/src/trace/cpp/trace_replay_core.hpp b/android-plugin/app/src/trace/cpp/trace_replay_core.hpp index cc550378..aab23e71 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_core.hpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.hpp @@ -44,6 +44,7 @@ struct Request { bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false; bool avoidAngleLlvmpipeExplicitLodBias = false; bool coherentAsFlush = false; + bool numSubgroupsQuirk = false; int holdMs = 0; }; diff --git a/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp b/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp index 8c6e1a6e..590d15df 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp @@ -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); diff --git a/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java b/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java index 33e81d28..b2bacd3c 100644 --- a/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java +++ b/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java @@ -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", "") ); } diff --git a/android-plugin/trace-replay-ci.sh b/android-plugin/trace-replay-ci.sh index 93a9c923..8f461a70 100644 --- a/android-plugin/trace-replay-ci.sh +++ b/android-plugin/trace-replay-ci.sh @@ -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 diff --git a/tools/trace_replay/trace_cases.json b/tools/trace_replay/trace_cases.json index b8f4dc85..7cee7bc7 100644 --- a/tools/trace_replay/trace_cases.json +++ b/tools/trace_replay/trace_cases.json @@ -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",