diff --git a/CMakeLists.txt b/CMakeLists.txt index ec52c5bb..1329f9f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -372,6 +372,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 0dddfb1f..af038dc1 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -124,12 +124,16 @@ layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; layout(rgba16, binding = 0) uniform image2D floatImage; layout(r32ui, binding = 1) uniform uimage2D atomicImage; layout(rgba32f, binding = 2) uniform image2D otherFloatImages[2]; +layout(std430, binding = 3) buffer Output0 { uint value0; }; +layout(std430, binding = 4) buffer Output1 { uint value1; }; void main() { ivec2 coordinate = ivec2(gl_GlobalInvocationID.xy); imageStore(floatImage, coordinate, imageLoad(floatImage, coordinate)); imageStore(otherFloatImages[1], coordinate, imageLoad(otherFloatImages[0], coordinate)); imageAtomicAdd(atomicImage, coordinate, 1u); + value0 = 11u; + value1 = 22u; } )"; @@ -180,6 +184,8 @@ void main() { EXPECT_NE(outputText.find("Binding 0"), String::npos) << outputText; EXPECT_NE(outputText.find("Binding 1"), String::npos) << outputText; EXPECT_NE(outputText.find("Binding 2"), String::npos) << outputText; + EXPECT_NE(outputText.find("\"Output0\""), String::npos) << outputText; + EXPECT_NE(outputText.find("\"Output1\""), String::npos) << outputText; EXPECT_TRUE(tools.Validate(outputBinary)); Vector secondOutputBinary; @@ -195,12 +201,28 @@ TEST_F(ProgramUtilTest, LowerRectImagesMergesWithExisting2DImageTypes) { OpMemoryModel Logical GLSL450 OpEntryPoint GLCompute %main "main" OpExecutionMode %main LocalSize 1 1 1 + OpName %Output0 "Output0" + OpName %Output1 "Output1" + OpDecorate %Output0 Block + OpDecorate %Output1 Block + OpMemberDecorate %Output0 0 Offset 0 + OpMemberDecorate %Output1 0 Offset 0 + OpDecorate %buffer0 DescriptorSet 0 + OpDecorate %buffer0 Binding 2 + OpDecorate %buffer1 DescriptorSet 0 + OpDecorate %buffer1 Binding 3 OpDecorate %rect DescriptorSet 0 OpDecorate %rect Binding 0 OpDecorate %flat DescriptorSet 0 OpDecorate %flat Binding 1 %void = OpTypeVoid %uint = OpTypeInt 32 0 + %Output0 = OpTypeStruct %uint + %Output1 = OpTypeStruct %uint + %outputPtr0 = OpTypePointer StorageBuffer %Output0 + %outputPtr1 = OpTypePointer StorageBuffer %Output1 + %buffer0 = OpVariable %outputPtr0 StorageBuffer + %buffer1 = OpVariable %outputPtr1 StorageBuffer %rectType = OpTypeImage %uint Rect 0 0 0 2 R32ui %flatType = OpTypeImage %uint 2D 0 0 0 2 R32ui %rectPtr = OpTypePointer UniformConstant %rectType @@ -228,6 +250,8 @@ TEST_F(ProgramUtilTest, LowerRectImagesMergesWithExisting2DImageTypes) { const auto firstImage = outputText.find("OpTypeImage"); ASSERT_NE(firstImage, String::npos); EXPECT_EQ(outputText.find("OpTypeImage", firstImage + 1), String::npos); + EXPECT_NE(outputText.find("\"Output0\""), String::npos) << outputText; + EXPECT_NE(outputText.find("\"Output1\""), String::npos) << outputText; } TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepFloatAtomicImageTypesTyped) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index c9e28dc8..a6e94efd 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -38,6 +38,7 @@ #include "SpirvPasses/FixIterationRPBarrierPass.h" #include "SpirvPasses/FixIterationRPSubgroupScratchPass.h" #include "SpirvPasses/NormalizeRectCoordinatesPass.h" +#include "SpirvPasses/DeduplicateImageTypesPass.h" #include "SpirvPasses/Lower1DArrayImagesPass.h" #include "SpirvPasses/Lower1DSampledImagesPass.h" #include "SpirvPasses/BakeImageFormatsPass.h" @@ -1814,7 +1815,7 @@ namespace MobileGL { Optimizer optimizer(SPV_ENV_VULKAN_1_1); optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass()); // Rect and 2D images of the same format become the same type after lowering. - optimizer.RegisterPass(CreateRemoveDuplicatesPass()); + optimizer.RegisterPass(DeduplicateImageTypesPass::Create()); return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); } @@ -2106,10 +2107,9 @@ namespace MobileGL { outputBinary.insert(outputBinary.begin() + static_cast(capabilityInsertOffset), addedCapabilities.begin(), addedCapabilities.end()); // Erasing the format can make distinct image types identical. Use the same - // cleanup as the image-widening/lowering paths to merge their dependent types - // and update every use before validating the rewritten module. + // image-only cleanup as rectangle lowering; named blocks retain their identity. spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1); - optimizer.RegisterPass(spvtools::CreateRemoveDuplicatesPass()); + optimizer.RegisterPass(DeduplicateImageTypesPass::Create()); Vector rewrittenBinary = std::move(outputBinary); return RunOptimizerChecked("UseUnformattedFloatStorageImagesForVulkan", optimizer, rewrittenBinary, outputBinary, true, enableSpirvValidation); diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp new file mode 100644 index 00000000..131d6656 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp @@ -0,0 +1,75 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp +// Copyright (c) 2025-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 "DeduplicateImageTypesPass.h" +#include "source/opt/decoration_manager.h" +#include "source/opt/ir_context.h" +#include "source/util/make_unique.h" +#include +#include +#include + +namespace MobileGL::MG_Util::ShaderTranspiler { + spvtools::opt::Pass::Status DeduplicateImageTypesPass::Process() { + using spvtools::opt::Instruction; + std::unordered_set imageTypes; + std::map, std::vector> canonical; + std::vector duplicates; + for (auto& type : context()->types_values()) { + bool imageRelated = false; + switch (type.opcode()) { + case spv::Op::OpTypeImage: + imageRelated = true; + break; + case spv::Op::OpTypeSampledImage: + case spv::Op::OpTypeArray: + case spv::Op::OpTypeRuntimeArray: + case spv::Op::OpTypePointer: + case spv::Op::OpTypeFunction: + type.ForEachInId([&](const uint32_t* id) { + imageRelated |= imageTypes.count(*id) != 0; + }); + break; + default: + break; + } + if (!imageRelated) continue; + imageTypes.insert(type.result_id()); + + // Compare operand IDs, not structural equivalence. Distinct struct IDs + // must never collapse just because their member layouts happen to match. + std::vector key{static_cast(type.opcode())}; + for (uint32_t i = 0; i < type.NumInOperands(); ++i) { + const auto& words = type.GetInOperand(i).words; + key.insert(key.end(), words.begin(), words.end()); + } + auto& candidates = canonical[key]; + Instruction* match = nullptr; + for (auto* candidate : candidates) { + if (context()->get_decoration_mgr()->HaveTheSameDecorations( + type.result_id(), candidate->result_id())) { + match = candidate; + break; + } + } + if (!match) { + candidates.push_back(&type); + continue; + } + context()->KillNamesAndDecorates(type.result_id()); + context()->ReplaceAllUsesWith(type.result_id(), match->result_id()); + duplicates.push_back(&type); + } + for (auto* type : duplicates) context()->KillInst(type); + return duplicates.empty() ? Status::SuccessWithoutChange : Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken DeduplicateImageTypesPass::Create() { + return spvtools::Optimizer::PassToken(spvtools::MakeUnique()); + } +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.h new file mode 100644 index 00000000..c37db93f --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.h @@ -0,0 +1,21 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.h +// Copyright (c) 2025-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 "spirv-tools/optimizer.hpp" +#include "source/opt/pass.h" + +namespace MobileGL::MG_Util::ShaderTranspiler { + // Preserve named structs: GL reflection and cross-stage interfaces use their identity. + class DeduplicateImageTypesPass final : public spvtools::opt::Pass { + public: + const char* name() const override { return "mobilegl-deduplicate-image-types"; } + Status Process() override; + static spvtools::Optimizer::PassToken Create(); + }; +}