[Fix] (Magma): zero gl_Layer in draws into a non-layered framebuffer

This commit is contained in:
2026-10-12 18:28:13 -04:00
parent 53f3b7a9af
commit 6129d6e463
14 changed files with 361 additions and 4 deletions
+1
View File
@@ -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
@@ -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)));
@@ -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;
@@ -3998,6 +3998,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.stageSpirvDigests.push_back(ShaderStageSpirvDigest{
static_cast<Uint32>(stage.stage), static_cast<Uint32>(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<Uint> 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
@@ -128,6 +128,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
HashType hash = 0;
Vector<VkPipelineShaderStageCreateInfo> 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<VkPipelineShaderStageCreateInfo> nonLayeredStages;
Vector<VkShaderModule> 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
}
};
@@ -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<Uint32>(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;
@@ -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;
@@ -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;
@@ -21,6 +21,7 @@ add_executable(
DemotePointSizeTest.cpp
FragCoordYFlipTest.cpp
PixelCenterIntegerTest.cpp
ZeroLayerOutputTest.cpp
RelaxedOpaqueStructTest.cpp
ShadingRateEsslTest.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 <gtest/gtest.h>
#include "Includes.h"
#include "Init.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 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<Uint32> CompileGeometry(const char* geometry) {
using namespace MG_Util::ShaderTranspiler;
Vector<SharedPtr<glslang::TShader>> shaders;
for (const auto& [stage, source] : Vector<Pair<GLenum, const char*>>{
{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<Uint32> module = binary.value()[1];
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
return module;
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
EXPECT_TRUE(tools.Disassemble(spirv, &text));
return text;
}
String ValidationError(const Vector<Uint32>& 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<Uint32> 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<Uint32> 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<Uint32> 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<Uint32> output;
ASSERT_TRUE(ShaderCompiler::ZeroLayerOutputForVulkan(input, output, true));
EXPECT_EQ(output, input);
}
@@ -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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
@@ -353,6 +353,12 @@ namespace MobileGL {
static bool LowerPixelCenterIntegerForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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
@@ -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 <vector>
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<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn)
continue;
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != spv::BuiltIn::Layer)
continue;
Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
if (variable != nullptr && variable->opcode() == spv::Op::OpVariable &&
static_cast<spv::StorageClass>(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<Instruction*> 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<ZeroLayerOutputPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -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 <Includes.h>
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