[Fix, Test] (DirectVulkan, ShaderTranspiler, TraceReplay): derive NumSubgroups behind opt-in quirk

This commit is contained in:
2026-08-18 22:31:57 -04:00
parent 12c94111b5
commit 2b6c2b561c
17 changed files with 509 additions and 2 deletions
+3
View File
@@ -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 \
+1
View File
@@ -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
+5
View File
@@ -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
+1
View File
@@ -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", "")
);
}
+9 -1
View File
@@ -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
+2 -1
View File
@@ -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",