mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[Fix] (Magma): zero gl_Layer in draws into a non-layered framebuffer
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user