From 6129d6e463c8e1b1b485b834d800d7ff376573ad Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 18:28:13 -0400 Subject: [PATCH] [Fix] (Magma): zero gl_Layer in draws into a non-layered framebuffer --- CMakeLists.txt | 1 + .../DirectVulkan/Renderer/PipelineFactory.cpp | 1 + .../DirectVulkan/Renderer/PipelineFactory.h | 4 + .../DirectVulkan/Renderer/ProgramFactory.cpp | 26 ++++ .../DirectVulkan/Renderer/ProgramFactory.h | 9 ++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 8 +- .../DirectVulkan/Renderer/VulkanRenderer.h | 6 +- .../DirectVulkan/Renderer/WireDraw.inc | 15 ++- .../MG_Test/ShaderTranspiler/CMakeLists.txt | 1 + .../ShaderTranspiler/ZeroLayerOutputTest.cpp | 124 ++++++++++++++++++ .../ShaderTranspiler/ShaderCompiler.cpp | 12 ++ .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 6 + .../SpirvPasses/ZeroLayerOutputPass.cpp | 109 +++++++++++++++ .../SpirvPasses/ZeroLayerOutputPass.h | 43 ++++++ 14 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 MobileGL/MG_Test/ShaderTranspiler/ZeroLayerOutputTest.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d5bc10b6..77eefeb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -417,6 +417,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index 9488e751..aff62450 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -222,6 +222,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(hashState, &payload.passthroughTessControlKey, sizeof(payload.passthroughTessControlKey))); XXHASH_VERIFY(XXH64_update(hashState, &payload.viewportCount, sizeof(payload.viewportCount))); + XXHASH_VERIFY(XXH64_update(hashState, &payload.nonLayeredStages, sizeof(payload.nonLayeredStages))); XXHASH_VERIFY(XXH64_update(hashState, &payload.polygonMode, sizeof(payload.polygonMode))); XXHASH_VERIFY(XXH64_update(hashState, &payload.cullMode, sizeof(payload.cullMode))); XXHASH_VERIFY(XXH64_update(hashState, &payload.frontFace, sizeof(payload.frontFace))); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index 03dd7b23..b61b82b6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -81,6 +81,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have // exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418). Uint32 viewportCount = 1; + // `stages` is the program's nonLayeredStages: the gl_Layer-zeroed modules a draw into + // a non-layered framebuffer uses (ZeroLayerOutputPass). Hashed - the two stage sets + // are different pipelines; the program hash alone names both. + Bool nonLayeredStages = false; VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL; VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT; VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 79e0de75..31f002fd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -3998,6 +3998,32 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.stageSpirvDigests.push_back(ShaderStageSpirvDigest{ static_cast(stage.stage), static_cast(moduleSpv.size()), XXH64(moduleSpv.data(), moduleSpv.size() * sizeof(Uint), 0)}); + + // GL ignores gl_Layer when the framebuffer is not layered; Vulkan leaves a Layer at or + // past the framebuffer's layer count undefined. A stage that stores it gets a second + // module storing 0 for draws into a non-layered framebuffer (ZeroLayerOutputPass). + VkPipelineShaderStageCreateInfo nonLayeredStage = stage; + if (shaderStage == ShaderStage::Vertex || shaderStage == ShaderStage::TessEval || + shaderStage == ShaderStage::Geometry) { + Vector zeroLayerSpv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroLayerOutputForVulkan(moduleSpv, zeroLayerSpv, + enableSpirvValidation) && + !zeroLayerSpv.empty() && zeroLayerSpv != moduleSpv) { + VkShaderModuleCreateInfo zeroLayerInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + zeroLayerInfo.codeSize = zeroLayerSpv.size() * sizeof(Uint); + zeroLayerInfo.pCode = zeroLayerSpv.data(); + VkShaderModule zeroLayerModule = VK_NULL_HANDLE; + VK_VERIFY(vkCreateShaderModule(m_device, &zeroLayerInfo, nullptr, &zeroLayerModule), + "vkCreateShaderModule(zero gl_Layer)"); + entry.modules.push_back(zeroLayerModule); + nonLayeredStage.module = zeroLayerModule; + // Back-fill: every stage before this one is shared with `stages`. + if (entry.nonLayeredStages.empty()) { + entry.nonLayeredStages.assign(entry.stages.begin(), entry.stages.end() - 1); + } + } + } + if (!entry.nonLayeredStages.empty()) entry.nonLayeredStages.push_back(nonLayeredStage); } // Reflect and create layout as part of the program object diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 03871b14..55de3d6d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -128,6 +128,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { HashType hash = 0; Vector stages; + // `stages` for a draw into a NON-LAYERED framebuffer: the same list, except that every + // module storing gl_Layer is replaced by its ZeroLayerOutputPass variant (GL ignores + // gl_Layer there; Vulkan offsets the primitive out of the attachment). Empty for every + // program that never writes gl_Layer, which then uses `stages` for both. The variant + // modules are owned by `modules` like the others. + Vector nonLayeredStages; Vector modules; // Parallel to stages; identifies the exact module bytes handed to the driver when a // pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V. @@ -277,6 +283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkProgramObject(VkProgramObject&& other) noexcept { hash = other.hash; stages = std::move(other.stages); + nonLayeredStages = std::move(other.nonLayeredStages); modules = std::move(other.modules); // Must travel with `modules`: these digests name the SPIR-V those exact // shader modules were built from, and the pipeline-failure diagnostics @@ -356,6 +363,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Destroy(); hash = other.hash; stages = std::move(other.stages); + nonLayeredStages = std::move(other.nonLayeredStages); modules = std::move(other.modules); stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor descriptorSetLayout = other.descriptorSetLayout; @@ -446,6 +454,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } modules.clear(); stages.clear(); + nonLayeredStages.clear(); stageSpirvDigests.clear(); // the modules they describe are gone } }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 0b345fdc..4bfda4f6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2822,8 +2822,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipeline VulkanRenderer::GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry, - Bool primitiveRestartEnable, Uint64 wireRenderPassCompatibilityId) { + Bool primitiveRestartEnable, Bool layeredFramebuffer, Uint64 wireRenderPassCompatibilityId) { Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip; + const Bool nonLayeredStages = !layeredFramebuffer && !programObj.nonLayeredStages.empty(); if (programObj.stages.empty()) { MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages"); return VK_NULL_HANDLE; @@ -2874,6 +2875,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.pipelineStateHash == pipelineStateHash && entry.renderStateCso == renderStateCso && entry.primitiveRestartEnable == primitiveRestartEnable && + entry.nonLayeredStages == nonLayeredStages && entry.transformFlags == transformFlags) { if (MG_Util::PipeStats::Enabled()) { // Gate 5 of section 2.3.1. On a hit this whole function cost the one @@ -3161,6 +3163,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { .primitiveRestartEnable = primitiveRestartEnabled, .patchControlPoints = static_cast(MG_Pipe::gPipeInputs->GetPatchVertices()), .viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin), + .nonLayeredStages = nonLayeredStages, .polygonMode = effectivePolygonMode, .cullMode = cullFaceEnabled ? MG_Util::ConvertCullFaceModeToVkEnum(MG_Pipe::gPipeInputs->GetCullFaceMode(), false) @@ -3202,7 +3205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { .backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func), .fragmentReplacesDepth = programObj.fragmentReplacesDepth, .fragmentShadingRateDynamic = m_fragmentShadingRate.Pipeline, - .stages = &programObj.stages, + .stages = nonLayeredStages ? &programObj.nonLayeredStages : &programObj.stages, .vertexInputState = pipelineVertexInputState, .stageSpirvDigests = &programObj.stageSpirvDigests }; @@ -3445,6 +3448,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.pipelineStateHash = pipelineStateHash; entry.renderStateCso = renderStateCso; entry.primitiveRestartEnable = primitiveRestartEnable; + entry.nonLayeredStages = nonLayeredStages; entry.transformFlags = transformFlags; entry.pipeline = pipeline; m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 3c025087..d7c962fa 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -973,7 +973,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipeline GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry, - Bool primitiveRestartEnable, Uint64 wireRenderPassCompatibilityId = 0); + Bool primitiveRestartEnable, Bool layeredFramebuffer, Uint64 wireRenderPassCompatibilityId = 0); void CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); @@ -1468,6 +1468,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // indexed and a non-indexed draw over the same program and state collide on one entry // and the second one gets the first one's restart setting. Bool primitiveRestartEnable = false; + // The pipeline was built from the program's nonLayeredStages (gl_Layer stores zeroed, + // PipelineCreatePayload::nonLayeredStages). Not derivable from the render pass: a + // layered and a single-layer attachment of one format share it. + Bool nonLayeredStages = false; VkPipeline pipeline = VK_NULL_HANDLE; }; static constexpr Uint32 kPipelineMemoSize = 8; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index b96e5df9..3358f17f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -13,6 +13,19 @@ // Through the same seam as MagmaWireFatal, and for the same reason (P7 wave 0). The family // stays RoleViolation: reaching the legacy buffer arm inside a Magma server is a role // invariant the two halves must hold, not a verb the wire failed to migrate. + // GL 4.6 core 9.8: a framebuffer is layered when its attachments are (completeness makes + // them agree), and only then does gl_Layer select a layer. + static Bool WireFramebufferIsLayered(const MG_Pipe::MGPFramebufferState& fbo) { + if (fbo.IsDefault) return false; + const auto layered = [](const MG_Pipe::MGPSurface& surface) { + return surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture && surface.Layered != 0; + }; + for (const auto& color : fbo.Color) { + if (layered(color)) return true; + } + return layered(fbo.Depth) || layered(fbo.Stencil); + } + [[noreturn]] static void WireBufferLegacyFatal() { MG_Pipe::MGPipeSessionFail( MG_Pipe::MGPipeFatalFamily::RoleViolation, @@ -847,7 +860,7 @@ // Interned once per pass (above): a continued pass has the same compatibility. const Uint64 compatibilityId = m_wireDrawPassCompatibilityId; const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, - ResolvePrimitiveRestartEnable(aspects, indices), compatibilityId); + ResolvePrimitiveRestartEnable(aspects, indices), WireFramebufferIsLayered(*fbo), compatibilityId); if (pipeline == VK_NULL_HANDLE) { MGL_WIRE_DECLINE_AT(PipelineUnavailable, "no graphics pipeline could be created for this draw"); return false; diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index ee131a5b..ca3498db 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -21,6 +21,7 @@ add_executable( DemotePointSizeTest.cpp FragCoordYFlipTest.cpp PixelCenterIntegerTest.cpp + ZeroLayerOutputTest.cpp RelaxedOpaqueStructTest.cpp ShadingRateEsslTest.cpp ) diff --git a/MobileGL/MG_Test/ShaderTranspiler/ZeroLayerOutputTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/ZeroLayerOutputTest.cpp new file mode 100644 index 00000000..4b680c62 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/ZeroLayerOutputTest.cpp @@ -0,0 +1,124 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ZeroLayerOutputTest.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 + +// GL ignores gl_Layer for a non-layered framebuffer; Vulkan offsets a primitive by a Layer past the +// framebuffer's layer count (KHR-GL4x.texture_cube_map_array.color_depth_attachments lost every +// draw into layer-face 1). The non-layered variant must validate and store nothing but 0. + +#include + +#include "Includes.h" +#include "Init.h" +#include +#include + +#include "spirv-tools/libspirv.hpp" + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + constexpr const char* kVertex = R"(#version 330 core +layout(location = 0) in vec2 position; +void main() { gl_Position = vec4(position, 0.0, 1.0); } +)"; + constexpr const char* kFragment = R"(#version 330 core +out vec4 color; +void main() { color = vec4(1.0); } +)"; + + // Index of the geometry module in the program's SPIR-V (stages come back sorted). + Vector CompileGeometry(const char* geometry) { + using namespace MG_Util::ShaderTranspiler; + Vector> shaders; + for (const auto& [stage, source] : Vector>{ + {GL_VERTEX_SHADER, kVertex}, {GL_GEOMETRY_SHADER, geometry}, {GL_FRAGMENT_SHADER, kFragment}}) { + ShaderAttrib attrib{.shaderType = stage, .sourceStr = source}; + auto shader = ShaderCompiler::CompileShader(attrib); + EXPECT_TRUE(shader) << (shader ? String{} : shader.error().log); + if (!shader) return {}; + shaders.push_back(shader.value()); + } + ProgramAttrib programAttrib{.shaders = shaders}; + auto program = ShaderCompiler::LinkProgram(programAttrib); + EXPECT_TRUE(program) << (program ? String{} : program.error().log); + if (!program) return {}; + ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER}, + .program = *program.value()}; + auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + EXPECT_TRUE(binary) << (binary ? String{} : binary.error().log); + if (!binary || binary->size() != 3) return {}; + Vector module = binary.value()[1]; + EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true)); + return module; + } + + String Disassemble(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + String text; + EXPECT_TRUE(tools.Disassemble(spirv, &text)); + return text; + } + + String ValidationError(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + String error; + tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, + const char* message) { error += message != nullptr ? message : ""; }); + return tools.Validate(spirv) ? String{} : (error.empty() ? String{"invalid"} : error); + } + + SizeT CountOccurrences(const String& text, const String& needle) { + SizeT count = 0; + for (SizeT at = text.find(needle); at != String::npos; at = text.find(needle, at + needle.size())) ++count; + return count; + } + + constexpr const char* kLayeredGeometry = R"(#version 330 core +layout(points) in; +layout(triangle_strip, max_vertices = 3) out; +uniform int layer; +void main() { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); gl_Layer = layer; EmitVertex(); + gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); gl_Layer = layer; EmitVertex(); + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); gl_Layer = layer; EmitVertex(); + EndPrimitive(); +} +)"; +} // namespace + +TEST(ZeroLayerOutput, EveryLayerStoreWritesZero) { + MobileGL::Initialize(); + const Vector input = CompileGeometry(kLayeredGeometry); + ASSERT_FALSE(input.empty()); + const String original = Disassemble(input); + ASSERT_EQ(CountOccurrences(original, "OpStore %gl_Layer "), 3u) << original; + ASSERT_EQ(CountOccurrences(original, "OpStore %gl_Layer %int_0"), 0u) << original; + + Vector output; + ASSERT_TRUE(ShaderCompiler::ZeroLayerOutputForVulkan(input, output, true)); + EXPECT_EQ(ValidationError(output), String{}); + const String text = Disassemble(output); + EXPECT_EQ(CountOccurrences(text, "OpStore %gl_Layer %int_0"), 3u) << text; + EXPECT_EQ(CountOccurrences(text, "OpStore %gl_Layer "), 3u) << text; +} + +// A module that never writes gl_Layer is left alone, so its program keeps one stage list. +TEST(ZeroLayerOutput, LeavesModulesWithoutLayerUnchanged) { + MobileGL::Initialize(); + const Vector input = CompileGeometry(R"(#version 330 core +layout(points) in; +layout(points, max_vertices = 1) out; +void main() { gl_Position = vec4(0.0); EmitVertex(); EndPrimitive(); } +)"); + ASSERT_FALSE(input.empty()); + + Vector output; + ASSERT_TRUE(ShaderCompiler::ZeroLayerOutputForVulkan(input, output, true)); + EXPECT_EQ(output, input); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index e2bcf31b..6ebb5b49 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -34,6 +34,7 @@ #include "SpirvPasses/RebaseInstanceIndexPass.h" #include "SpirvPasses/ZeroBaseVertexPass.h" #include "SpirvPasses/LowerPixelCenterIntegerPass.h" +#include "SpirvPasses/ZeroLayerOutputPass.h" #include "SpirvPasses/DeriveNumSubgroupsPass.h" #include "SpirvPasses/EmulateSubgroupsPass.h" #include "SpirvPasses/FixIterationRPBarrierPass.h" @@ -1997,6 +1998,17 @@ namespace MobileGL { true, enableSpirvValidation); } + bool ShaderCompiler::ZeroLayerOutputForVulkan(const Vector& inputBinary, + Vector& outputBinary, + const bool enableSpirvValidation) { + using namespace spvtools; + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(ZeroLayerOutputPass::CreateZeroLayerOutputPass()); + + return RunOptimizerChecked("ZeroLayerOutputForVulkan", optimizer, inputBinary, outputBinary, true, + enableSpirvValidation); + } + bool ShaderCompiler::DeriveNumSubgroupsForVulkan(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 0f57450c..70c66bb7 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -353,6 +353,12 @@ namespace MobileGL { static bool LowerPixelCenterIntegerForVulkan(const Vector& inputBinary, Vector& outputBinary, bool enableSpirvValidation = false); + // Builds the variant of a pre-rasterization module that a non-layered framebuffer + // draws with: every gl_Layer store writes 0, since GL ignores gl_Layer there. A + // module that writes no Layer comes back unchanged. See ZeroLayerOutputPass. + static bool ZeroLayerOutputForVulkan(const Vector& inputBinary, + Vector& outputBinary, + bool enableSpirvValidation = false); // Replaces compute gl_NumSubgroups loads with ceil(workgroup invocations / // gl_SubgroupSize). DirectVulkan only; this repairs drivers whose builtin // disagrees with the subgroup IDs the same dispatch emits (Adreno reports 1 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp new file mode 100644 index 00000000..0f0f8c09 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp @@ -0,0 +1,109 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.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 "ZeroLayerOutputPass.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/opt/type_manager.h" +#include "source/util/make_unique.h" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + + // Returns the Output OpVariable decorated BuiltIn Layer, or nullptr if none. + Instruction* FindLayerOutputVariable(IRContext* context) { + 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)) != spv::BuiltIn::Layer) + continue; + Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0)); + if (variable != nullptr && variable->opcode() == spv::Op::OpVariable && + static_cast(variable->GetSingleWordInOperand(0)) == + spv::StorageClass::Output) { + return variable; + } + } + return nullptr; + } + + Bool IsDeclarationUse(const Instruction* user) { + switch (user->opcode()) { + case spv::Op::OpDecorate: + case spv::Op::OpDecorateId: + case spv::Op::OpDecorateString: + case spv::Op::OpName: + case spv::Op::OpEntryPoint: + return true; + default: + return false; + } + } + } // namespace + + spvtools::opt::Pass::Status ZeroLayerOutputPass::Process() { + auto* irContext = context(); + auto* defUseMgr = irContext->get_def_use_mgr(); + auto* constantMgr = irContext->get_constant_mgr(); + auto* typeMgr = irContext->get_type_mgr(); + + Instruction* layer = FindLayerOutputVariable(irContext); + if (layer == nullptr) return Status::SuccessWithoutChange; + + // Gather the stores before mutating anything, so a shape the pass refuses leaves + // the module exactly as it was. + const uint32_t layerId = layer->result_id(); + std::vector stores; + Bool refused = false; + defUseMgr->ForEachUser(layer, [&](Instruction* user) { + if (refused || IsDeclarationUse(user)) return; + if (user->opcode() == spv::Op::OpStore && user->GetSingleWordInOperand(0) == layerId) { + stores.push_back(user); + return; + } + refused = true; + }); + if (refused) return Status::Failure; + if (stores.empty()) return Status::SuccessWithoutChange; + + const auto* pointerType = typeMgr->GetType(layer->type_id()); + const auto* pointer = pointerType ? pointerType->AsPointer() : nullptr; + const auto* valueType = pointer ? pointer->pointee_type() : nullptr; + if (valueType == nullptr || valueType->AsInteger() == nullptr) return Status::Failure; + const uint32_t valueTypeId = typeMgr->GetId(valueType); + const auto* zero = constantMgr->GetConstant(valueType, {0u}); + const Instruction* zeroInst = zero ? constantMgr->GetDefiningInstruction(zero, valueTypeId) : nullptr; + if (valueTypeId == 0 || zeroInst == nullptr) return Status::Failure; + const uint32_t zeroId = zeroInst->result_id(); + + for (Instruction* store : stores) store->SetInOperand(1, {zeroId}); + + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken ZeroLayerOutputPass::CreateZeroLayerOutputPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.h new file mode 100644 index 00000000..ea861f39 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.h @@ -0,0 +1,43 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.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 "source/opt/pass.h" +#include "spirv-tools/optimizer.hpp" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // GL ignores gl_Layer when the framebuffer is not layered (GL 4.6 core 9.8): every + // primitive lands in the one attached layer whatever the shader wrote. Vulkan has no + // such rule - a Layer value at or past the framebuffer's layer count is undefined, + // and Adreno offsets the primitive by it, out of the attachment + // (KHR-GL4x.texture_cube_map_array.color_depth_attachments renders layer-face 1's + // draw into face 2 of a single-layer attachment, and nothing into face 1). This pass + // builds the module a non-layered framebuffer needs: every store to the Layer output + // stores 0 instead. + // + // The renderer keeps this as a second module beside the original rather than + // selecting with a specialization constant: a `layered ? value : 0` select on a + // boolean specialization constant did not change the result on Adreno 750, while a + // constant 0 store did. + // + // A use of the builtin other than a direct whole-variable store fails the pass, and + // the caller then keeps the original module. + class ZeroLayerOutputPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "zero-layer-output"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateZeroLayerOutputPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL