diff --git a/CMakeLists.txt b/CMakeLists.txt index 75b64a94..6ec27beb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -419,6 +419,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/ExtendNarrowIntegerVertexInputsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.cpp diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index 541a0e50..6b9ff2b2 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable( PixelCenterIntegerTest.cpp ZeroLayerOutputTest.cpp FullCoverageSampleMaskTest.cpp + ExtendNarrowIntegerVertexInputsTest.cpp RelaxedOpaqueStructTest.cpp ShadingRateEsslTest.cpp ) diff --git a/MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsTest.cpp new file mode 100644 index 00000000..b13f05b4 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsTest.cpp @@ -0,0 +1,248 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsTest.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 +// +// Magma fetches an integer attribute with the format of the shader input's signedness, so 8/16-bit +// data reaches an input of the other signedness extended by the wrong rule (Sodium's UNSIGNED_BYTE +// a_LightAndData read as `ivec4` turned 240 into -16). ExtendNarrowIntegerVertexInputsPass puts +// GL's value back. What has to hold: the module stays valid, exactly the listed locations get a +// zero-extension mask or a sign-extension, an array input is corrected element by element, the +// reflected vertex inputs do not move, and everything else is left byte-for-byte alone. + +#include + +#include +#include + +#include "Includes.h" +#include "Init.h" +#include +#include + +#include +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerVertexInputs; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + constexpr const char* kVertex = R"(#version 330 core +layout(location = 0) in vec2 pos; +layout(location = 1) in ivec4 lightAndData; +layout(location = 2) in uvec2 texc; +layout(location = 3) in uvec4 sbyte; +layout(location = 4) in int shortAsInt; +layout(location = 5) in int packed[3]; +layout(location = 8) in uint sshort; +flat out ivec4 a; +flat out uvec4 b; +void main() { + a = lightAndData + ivec4(shortAsInt) + ivec4(packed[0], packed[1], packed[2], 0); + b = uvec4(texc, sshort, 0u) + sbyte; + gl_Position = vec4(pos, 0.0, 1.0); +} +)"; + constexpr const char* kFragment = R"(#version 330 core +flat in ivec4 a; +flat in uvec4 b; +out vec4 color; +void main() { color = vec4(a) + vec4(b); } +)"; + + // Index 0 is the vertex module, 1 the fragment module (stages come back sorted). + Vector> CompileProgram() { + using namespace MG_Util::ShaderTranspiler; + Vector> shaders; + for (const auto& [stage, source] : + Vector>{{GL_VERTEX_SHADER, kVertex}, {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_FRAGMENT_SHADER}, + .program = *program.value()}; + auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + EXPECT_TRUE(binary) << (binary ? String{} : binary.error().log); + if (!binary || binary->size() != 2) return {}; + Vector> modules = binary.value(); + for (auto& module : modules) + EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true)); + return modules; + } + + 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, spv_target_env env = SPV_ENV_VULKAN_1_1) { + spvtools::SpirvTools tools(env); + 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; + } + + // The lines of `text` holding `opcode`, for checking an instruction's operands. + Vector LinesWith(const String& text, const String& opcode) { + Vector lines; + SizeT begin = 0; + while (begin < text.size()) { + SizeT end = text.find('\n', begin); + if (end == String::npos) end = text.size(); + String line = text.substr(begin, end - begin); + if (line.find(opcode) != String::npos) lines.push_back(line); + begin = end + 1; + } + return lines; + } + + // (location, component count, numeric flags) of every non-builtin input, sorted: what the + // program factory reflects the vertex inputs from. + Vector> ReflectInputs(const Vector& spirv) { + SpvReflectShaderModule module{}; + EXPECT_EQ(spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &module), + SPV_REFLECT_RESULT_SUCCESS); + Uint32 count = 0; + EXPECT_EQ(spvReflectEnumerateInputVariables(&module, &count, nullptr), SPV_REFLECT_RESULT_SUCCESS); + Vector inputs(count); + EXPECT_EQ(spvReflectEnumerateInputVariables(&module, &count, inputs.data()), SPV_REFLECT_RESULT_SUCCESS); + Vector> result; + for (const auto* input : inputs) { + if (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) continue; + result.emplace_back(input->location, input->numeric.vector.component_count, + static_cast(input->type_description->type_flags)); + } + spvReflectDestroyShaderModule(&module); + std::sort(result.begin(), result.end()); + return result; + } + + // Sodium's shape plus one of each other kind: UNSIGNED_BYTE into ivec4 (1), BYTE into uvec4 (3), + // UNSIGNED_SHORT into int (4), SHORT into uint (8), and UNSIGNED_BYTE into the middle element + // of an int array (location 6 = packed[1]). + NarrowIntegerVertexInputs SodiumLikeInputs() { + NarrowIntegerVertexInputs inputs; + inputs.zeroExtend8 = (1u << 1) | (1u << 6); + inputs.signExtend8 = 1u << 3; + inputs.zeroExtend16 = 1u << 4; + inputs.signExtend16 = 1u << 8; + return inputs; + } +} // namespace + +TEST(ExtendNarrowIntegerVertexInputs, ExtendsExactlyTheListedLocations) { + MobileGL::Initialize(); + const auto modules = CompileProgram(); + ASSERT_EQ(modules.size(), 2u); + const Vector& input = modules[0]; + const String original = Disassemble(input); + ASSERT_EQ(CountOccurrences(original, "OpBitwiseAnd"), 0u) << original; + ASSERT_EQ(CountOccurrences(original, "OpBitFieldSExtract"), 0u) << original; + + Vector output; + ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(input, output, SodiumLikeInputs(), true)); + EXPECT_EQ(ValidationError(output), String{}); + const String text = Disassemble(output); + + // Zero-extensions: the ivec4 by 0xff, the int by 0xffff, and packed[1] (one element) by 0xff. + const auto ands = LinesWith(text, "OpBitwiseAnd"); + ASSERT_EQ(ands.size(), 3u) << text; + EXPECT_EQ(CountOccurrences(text, "OpBitwiseAnd %v4int"), 1u) << text; + EXPECT_EQ(CountOccurrences(text, "OpBitwiseAnd %int"), 2u) << text; + EXPECT_EQ(CountOccurrences(text, "%int_65535"), 2u) << text; // declaration + the one use + EXPECT_NE(text.find("%int_255"), String::npos) << text; + + // Sign-extensions: the uvec4 from 8 bits, the uint from 16, both at bit offset 0. + const auto extracts = LinesWith(text, "OpBitFieldSExtract"); + ASSERT_EQ(extracts.size(), 2u) << text; + EXPECT_EQ(CountOccurrences(text, "OpBitFieldSExtract %v4uint"), 1u) << text; + EXPECT_EQ(CountOccurrences(text, "OpBitFieldSExtract %uint"), 1u) << text; + for (const auto& line : extracts) + EXPECT_NE(line.find(" %uint_0 %uint_"), String::npos) << line; + EXPECT_EQ(CountOccurrences(text, " %uint_0 %uint_8"), 1u) << text; + EXPECT_EQ(CountOccurrences(text, " %uint_0 %uint_16"), 1u) << text; + + // The corrected inputs are now Private copies seeded once; the untouched ones stay Inputs. + for (const char* name : {"%lightAndData", "%sbyte", "%shortAsInt", "%packed", "%sshort"}) { + EXPECT_NE(text.find(String(name) + " = OpVariable %_ptr_Private_"), String::npos) << name << "\n" << text; + EXPECT_EQ(CountOccurrences(text, String("OpStore ") + name + " "), 1u) << name << "\n" << text; + } + EXPECT_NE(text.find("%pos = OpVariable %_ptr_Input_v2float Input"), String::npos) << text; + EXPECT_NE(text.find("%texc = OpVariable %_ptr_Input_v2uint Input"), String::npos) << text; + + // The array is split into its three elements and rebuilt, one element corrected. + EXPECT_EQ(CountOccurrences(text, "OpCompositeExtract %int") - CountOccurrences(original, "OpCompositeExtract %int"), + 3u) + << text; + EXPECT_EQ(CountOccurrences(text, "OpCompositeConstruct %_arr_int_uint_3") - + CountOccurrences(original, "OpCompositeConstruct %_arr_int_uint_3"), + 1u) + << text; + + // Same interface: every location, width and type the program factory reflects is unchanged. + EXPECT_EQ(ReflectInputs(output), ReflectInputs(input)); +} + +// A list that names no integer input of the module - a float input, an empty location, a fragment +// module - leaves the bytes alone, so a mis-keyed variant cannot change what the shader computes. +TEST(ExtendNarrowIntegerVertexInputs, LeavesEverythingElseUnchanged) { + MobileGL::Initialize(); + const auto modules = CompileProgram(); + ASSERT_EQ(modules.size(), 2u); + + NarrowIntegerVertexInputs floatOrEmpty; + floatOrEmpty.zeroExtend8 = (1u << 0) | (1u << 12); + floatOrEmpty.signExtend16 = 1u << 31; + Vector output; + ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(modules[0], output, floatOrEmpty, true)); + EXPECT_EQ(output, modules[0]); + + ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(modules[1], output, SodiumLikeInputs(), true)); + EXPECT_EQ(output, modules[1]); + + ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(modules[0], output, {}, true)); + EXPECT_EQ(output, modules[0]); +} + +// From SPIR-V 1.4 an entry point lists every global it uses, so the Private copies have to join +// the new Inputs in the interface. Checked by re-labelling the module 1.4 and validating it for an +// environment that accepts 1.4. +TEST(ExtendNarrowIntegerVertexInputs, ListsPrivateCopiesInASpirv14Interface) { + MobileGL::Initialize(); + const auto modules = CompileProgram(); + ASSERT_EQ(modules.size(), 2u); + Vector input = modules[0]; + ASSERT_GT(input.size(), 1u); + input[1] = 0x00010400u; + ASSERT_EQ(ValidationError(input, SPV_ENV_VULKAN_1_2), String{}); + + Vector output; + ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(input, output, SodiumLikeInputs(), false)); + EXPECT_EQ(ValidationError(output, SPV_ENV_VULKAN_1_2), String{}); + const auto entryPoint = LinesWith(Disassemble(output), "OpEntryPoint"); + ASSERT_EQ(entryPoint.size(), 1u); + for (const char* name : {"%lightAndData", "%sbyte", "%shortAsInt", "%packed", "%sshort", "%texc"}) + EXPECT_NE(entryPoint[0].find(name), String::npos) << name << "\n" << entryPoint[0]; +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h b/MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h new file mode 100644 index 00000000..2ddc8db9 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h @@ -0,0 +1,38 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.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 + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // The vertex input locations whose integer value a vertex module has to widen itself, + // one bit per location in each mask, and how. Every desktop GL driver hands an integer + // input the source integer converted to 32 bits by the SOURCE type's signedness and + // then reinterpreted as the declared int/uint type. A Vulkan fetch whose format has to + // carry the input's signedness instead extends by the wrong rule for 8/16-bit data: + // - zeroExtend*: an int input fed unsigned bytes/shorts, fetched sign-extended, is + // masked back to its low 8/16 bits; + // - signExtend*: a uint input fed signed bytes/shorts, fetched zero-extended, has its + // low 8/16 bits sign-extended. + // Plain data so the shader pass, the program cache key and the draw path share it. + struct NarrowIntegerVertexInputs { + Uint32 zeroExtend8 = 0; + Uint32 zeroExtend16 = 0; + Uint32 signExtend8 = 0; + Uint32 signExtend16 = 0; + + constexpr Bool Any() const { return (zeroExtend8 | zeroExtend16 | signExtend8 | signExtend16) != 0; } + constexpr Bool operator==(const NarrowIntegerVertexInputs&) const = default; + }; + // Hashed as raw bytes by the program cache, so it must stay free of padding. + static_assert(sizeof(NarrowIntegerVertexInputs) == 4 * sizeof(Uint32)); + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index a40d82ca..465e90fa 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -34,6 +34,7 @@ #include "SpirvPasses/SplitArrayVertexInputsPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h" #include "SpirvPasses/ZeroBaseVertexPass.h" +#include "SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h" #include "SpirvPasses/LowerPixelCenterIntegerPass.h" #include "SpirvPasses/ZeroLayerOutputPass.h" #include "SpirvPasses/FullCoverageSampleMaskPass.h" @@ -1984,6 +1985,19 @@ namespace MobileGL { enableSpirvValidation); } + bool ShaderCompiler::ExtendNarrowIntegerVertexInputsForVulkan(const Vector& inputBinary, + Vector& outputBinary, + const NarrowIntegerVertexInputs& inputs, + const bool enableSpirvValidation) { + using namespace spvtools; + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass( + ExtendNarrowIntegerVertexInputsPass::CreateExtendNarrowIntegerVertexInputsPass(inputs)); + + return RunOptimizerChecked("ExtendNarrowIntegerVertexInputsForVulkan", optimizer, inputBinary, + outputBinary, true, enableSpirvValidation); + } + bool ShaderCompiler::LowerPixelCenterIntegerForVulkan(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 b730b959..5b4cddec 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -11,6 +11,7 @@ #include "SpvcSession.h" #include "glslang/TVarEntryInfo.h" #include "glslang/TMglGlslIoResolver.h" +#include "NarrowIntegerVertexInputs.h" #include #include @@ -342,6 +343,14 @@ namespace MobileGL { // See ZeroBaseVertexPass. static bool ZeroBaseVertexForVulkan(const Vector& inputBinary, Vector& outputBinary, bool enableSpirvValidation = false); + // Builds the vertex-shader variant that widens narrow integer attribute data the way + // GL does: the inputs at the listed locations are zero- or sign-extended from 8/16 + // bits right after they are read. A module with no listed integer input comes back + // unchanged. See ExtendNarrowIntegerVertexInputsPass. + static bool ExtendNarrowIntegerVertexInputsForVulkan(const Vector& inputBinary, + Vector& outputBinary, + const NarrowIntegerVertexInputs& inputs, + bool enableSpirvValidation = false); // Drops `layout(pixel_center_integer)` from a fragment module, which the Vulkan // environment forbids, and shifts every gl_FragCoord.xy read by -0.5 to keep its // values. See LowerPixelCenterIntegerPass. diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.cpp new file mode 100644 index 00000000..5a3e43f8 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.cpp @@ -0,0 +1,360 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.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 "ExtendNarrowIntegerVertexInputsPass.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/types.h" +#include "source/util/make_unique.h" + +#include +#include +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + using spvtools::opt::Operand; + namespace analysis = spvtools::opt::analysis; + + constexpr Uint32 kMaxLocations = 32; + + // What one location's value needs: the width of the source data, and whether the + // fetched value is sign-extended (true) or zero-extended (false) from it. + struct Extension { + Uint32 bits = 0; // 0: leave the value alone + Bool sign = false; + }; + + Extension ExtensionAt(const NarrowIntegerVertexInputs& inputs, Uint32 location) { + if (location >= kMaxLocations) return {}; + const Uint32 bit = 1u << location; + if (inputs.zeroExtend8 & bit) return {8, false}; + if (inputs.zeroExtend16 & bit) return {16, false}; + if (inputs.signExtend8 & bit) return {8, true}; + if (inputs.signExtend16 & bit) return {16, true}; + return {}; + } + + // True for a 32-bit integer scalar or vector: the only shapes a 32-bit integer + // vertex attribute can feed, and the only ones the bit operations below accept. + Bool IsInteger32ScalarOrVector(const analysis::Type* type) { + if (type == nullptr) return false; + if (const auto* vector = type->AsVector()) type = vector->element_type(); + const auto* integer = type->AsInteger(); + return integer != nullptr && integer->width() == 32; + } + + // One input variable to correct, with every id the rewrite needs resolved up front. + struct Target { + Instruction* variable = nullptr; + Uint32 pointeeTypeId = 0; + Uint32 elementTypeId = 0; + // 0 for a scalar or vector input; the element count of an array input. + Uint32 arrayLength = 0; + // One entry per element (one for a non-array input). + std::vector extensions; + // Per element: the mask constant of a zero-extension, or the bit-count constant + // of a sign-extension; 0 where the element is left alone. + std::vector operandIds; + Uint32 privatePointerTypeId = 0; + // Access chains rooted at the variable (chains of chains included), each with + // the Private pointer type its result takes once the variable is Private. + std::vector> accessChains; + }; + + 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 ExtendNarrowIntegerVertexInputsPass::Process() { + if (!m_inputs.Any()) return Status::SuccessWithoutChange; + + auto* irContext = context(); + auto entryPoints = irContext->module()->entry_points(); + if (entryPoints.begin() == entryPoints.end()) return Status::SuccessWithoutChange; + Instruction* entryPoint = &*entryPoints.begin(); + if (static_cast(entryPoint->GetSingleWordInOperand(0)) != + spv::ExecutionModel::Vertex) { + return Status::SuccessWithoutChange; + } + // The Private copy is seeded in ONE entry point; a second one reading the same input + // would see it unwritten. glslang never emits two, so refuse rather than half-fix. + auto secondEntryPoint = entryPoints.begin(); + if (++secondEntryPoint != entryPoints.end()) return Status::Failure; + + auto* defUseMgr = irContext->get_def_use_mgr(); + auto* typeMgr = irContext->get_type_mgr(); + auto* constantMgr = irContext->get_constant_mgr(); + + std::unordered_map locationById; + for (const Instruction& annotation : irContext->annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) continue; + if (static_cast(annotation.GetSingleWordInOperand(1)) == spv::Decoration::Location) + locationById[annotation.GetSingleWordInOperand(0)] = annotation.GetSingleWordInOperand(2); + } + + std::vector targets; + for (Instruction& inst : irContext->types_values()) { + if (inst.opcode() != spv::Op::OpVariable) continue; + if (static_cast(inst.GetSingleWordInOperand(0)) != spv::StorageClass::Input) + continue; + const auto location = locationById.find(inst.result_id()); + if (location == locationById.end()) continue; // a built-in + const Instruction* pointerType = defUseMgr->GetDef(inst.type_id()); + if (pointerType == nullptr) continue; + + Target target; + target.variable = &inst; + target.pointeeTypeId = pointerType->GetSingleWordInOperand(1); + target.elementTypeId = target.pointeeTypeId; + Uint32 elementCount = 1; + const Instruction* pointee = defUseMgr->GetDef(target.pointeeTypeId); + if (pointee != nullptr && pointee->opcode() == spv::Op::OpTypeArray) { + const auto* length = constantMgr->FindDeclaredConstant(pointee->GetSingleWordInOperand(1)); + if (length == nullptr || length->AsScalarConstant() == nullptr) continue; + target.elementTypeId = pointee->GetSingleWordInOperand(0); + target.arrayLength = static_cast(length->GetZeroExtendedValue()); + elementCount = target.arrayLength; + } + if (!IsInteger32ScalarOrVector(typeMgr->GetType(target.elementTypeId))) continue; + + Bool any = false; + for (Uint32 element = 0; element < elementCount; ++element) { + target.extensions.push_back(ExtensionAt(m_inputs, location->second + element)); + any = any || target.extensions.back().bits != 0; + } + if (any) targets.push_back(std::move(target)); + } + if (targets.empty()) return Status::SuccessWithoutChange; + + const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1); + spvtools::opt::Function* entryFunction = nullptr; + for (auto& function : *irContext->module()) { + if (function.result_id() == entryFunctionId) { + entryFunction = &function; + break; + } + } + if (entryFunction == nullptr || entryFunction->begin() == entryFunction->end()) { + return Status::Failure; + } + + // Every type and constant the rewrite names is materialized BEFORE any variable is + // added or moved: the types-and-values section is ordered, these lookups append to + // it, and a variable may not forward-reference its type. + const auto constantId = [&](Uint32 typeId, Uint32 scalarValue) -> Uint32 { + const analysis::Type* type = typeMgr->GetType(typeId); + if (type == nullptr) return 0; + const analysis::Constant* constant = nullptr; + if (const auto* vector = type->AsVector()) { + const analysis::Constant* component = + constantMgr->GetConstant(vector->element_type(), {scalarValue}); + const Instruction* componentInst = + component ? constantMgr->GetDefiningInstruction(component) : nullptr; + if (componentInst == nullptr) return 0; + constant = constantMgr->GetConstant( + type, std::vector(vector->element_count(), componentInst->result_id())); + } else { + constant = constantMgr->GetConstant(type, {scalarValue}); + } + const Instruction* inst = + constant ? constantMgr->GetDefiningInstruction(constant, typeId) : nullptr; + return inst ? inst->result_id() : 0; + }; + Uint32 zeroOffsetId = 0; + for (auto& target : targets) { + for (const Extension& extension : target.extensions) { + Uint32 operandId = 0; + if (extension.bits != 0) { + operandId = extension.sign ? constantMgr->GetUIntConstId(extension.bits) + : constantId(target.elementTypeId, (1u << extension.bits) - 1u); + if (operandId == 0) return Status::Failure; + if (extension.sign && zeroOffsetId == 0) { + zeroOffsetId = constantMgr->GetUIntConstId(0); + if (zeroOffsetId == 0) return Status::Failure; + } + } + target.operandIds.push_back(operandId); + } + target.privatePointerTypeId = + typeMgr->FindPointerToType(target.pointeeTypeId, spv::StorageClass::Private); + if (target.privatePointerTypeId == 0) return Status::Failure; + + // An access chain names its result's storage class, so every chain into the + // variable has to move to Private with it. Loads need nothing. Any other use - + // a pointer passed on, copied or stored - is refused before anything changes, + // rather than half-rewritten; glslang emits none of them for an input. + std::vector pending{target.variable}; + Bool unexpectedUse = false; + while (!pending.empty() && !unexpectedUse) { + Instruction* base = pending.back(); + pending.pop_back(); + // Collected first: the pointer-type lookups below may add instructions. + std::vector users; + defUseMgr->ForEachUser(base, [&](Instruction* user) { users.push_back(user); }); + for (Instruction* user : users) { + if (IsDeclarationUse(user) || user->opcode() == spv::Op::OpLoad) continue; + const Bool isChain = user->opcode() == spv::Op::OpAccessChain || + user->opcode() == spv::Op::OpInBoundsAccessChain; + const Instruction* chainType = + isChain && user->GetSingleWordInOperand(0) == base->result_id() + ? defUseMgr->GetDef(user->type_id()) + : nullptr; + const Uint32 privateChainTypeId = + chainType ? typeMgr->FindPointerToType(chainType->GetSingleWordInOperand(1), + spv::StorageClass::Private) + : 0; + if (privateChainTypeId == 0) { + unexpectedUse = true; + break; + } + target.accessChains.emplace_back(user, privateChainTypeId); + pending.push_back(user); + } + } + if (unexpectedUse) return Status::Failure; + } + + // From SPIR-V 1.4 the interface lists every global the entry point uses, Private + // included; before it, Input and Output only. + const Bool interfaceListsPrivate = irContext->module()->version() >= 0x00010400u; + + auto& entryBlock = *entryFunction->begin(); + auto insertPoint = entryBlock.begin(); + while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) { + ++insertPoint; + } + if (insertPoint == entryBlock.end()) return Status::Failure; + + // A TakeNextId failure (the id bound is exhausted) is reported once the rewrite is + // over; the optimizer then discards the half-rewritten module. + Bool outOfIds = false; + const auto nextId = [&]() { + const Uint32 id = TakeNextId(); + outOfIds = outOfIds || id == 0; + return id; + }; + const auto insert = [&](spv::Op opcode, Uint32 typeId, std::initializer_list operands) { + const Uint32 resultId = typeId != 0 ? nextId() : 0; + Instruction::OperandList operandList; + for (const Uint32 id : operands) + operandList.push_back({SPV_OPERAND_TYPE_ID, {id}}); + insertPoint = insertPoint.InsertBefore( + MakeUnique(irContext, opcode, typeId, resultId, operandList)); + ++insertPoint; + return resultId; + }; + const auto extend = [&](const Target& target, SizeT element, Uint32 valueId) -> Uint32 { + const Extension& extension = target.extensions[element]; + if (extension.bits == 0) return valueId; + if (!extension.sign) { + return insert(spv::Op::OpBitwiseAnd, target.elementTypeId, + {valueId, target.operandIds[element]}); + } + return insert(spv::Op::OpBitFieldSExtract, target.elementTypeId, + {valueId, zeroOffsetId, target.operandIds[element]}); + }; + + for (const auto& target : targets) { + Instruction* variable = target.variable; + const Uint32 privateId = variable->result_id(); + const Uint32 inputId = nextId(); + irContext->AddGlobalValue(MakeUnique( + irContext, spv::Op::OpVariable, variable->type_id(), inputId, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast(spv::StorageClass::Input)}}})); + + // Every decoration describes the interface, so all of them follow the Input. + for (auto& annotation : irContext->annotations()) { + const spv::Op op = annotation.opcode(); + if ((op == spv::Op::OpDecorate || op == spv::Op::OpDecorateId || + op == spv::Op::OpDecorateString) && + annotation.GetSingleWordInOperand(0) == privateId) { + annotation.SetInOperand(0, {inputId}); + } + } + + // The original becomes the Private copy every existing access keeps reading. It + // moves to the end of the section because its new pointer type may be newer. + variable->SetResultType(target.privatePointerTypeId); + variable->SetInOperand(0, {static_cast(spv::StorageClass::Private)}); + variable->RemoveFromList(); + irContext->AddGlobalValue(std::unique_ptr(variable)); + for (const auto& [chain, privateChainTypeId] : target.accessChains) + chain->SetResultType(privateChainTypeId); + + std::vector interfaceOperands; + for (Uint32 i = 0; i < entryPoint->NumInOperands(); ++i) { + const Operand& operand = entryPoint->GetInOperand(i); + if (i >= 3 && !interfaceListsPrivate && entryPoint->GetSingleWordInOperand(i) == privateId) { + interfaceOperands.push_back({SPV_OPERAND_TYPE_ID, {inputId}}); + continue; + } + interfaceOperands.push_back(operand); + } + if (interfaceListsPrivate) interfaceOperands.push_back({SPV_OPERAND_TYPE_ID, {inputId}}); + entryPoint->SetInOperands(std::move(interfaceOperands)); + + const Uint32 loadedId = insert(spv::Op::OpLoad, target.pointeeTypeId, {inputId}); + Uint32 correctedId = 0; + if (target.arrayLength == 0) { + correctedId = extend(target, 0, loadedId); + } else { + std::vector elementIds; + for (Uint32 element = 0; element < target.arrayLength; ++element) { + const Uint32 elementId = nextId(); + insertPoint = insertPoint.InsertBefore(MakeUnique( + irContext, spv::Op::OpCompositeExtract, target.elementTypeId, elementId, + Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {loadedId}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {element}}})); + ++insertPoint; + elementIds.push_back(extend(target, element, elementId)); + } + correctedId = nextId(); + Instruction::OperandList constituents; + for (const Uint32 id : elementIds) + constituents.push_back({SPV_OPERAND_TYPE_ID, {id}}); + insertPoint = insertPoint.InsertBefore(MakeUnique( + irContext, spv::Op::OpCompositeConstruct, target.pointeeTypeId, correctedId, constituents)); + ++insertPoint; + } + insert(spv::Op::OpStore, 0, {privateId, correctedId}); + } + + if (outOfIds) return Status::Failure; + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken ExtendNarrowIntegerVertexInputsPass:: + CreateExtendNarrowIntegerVertexInputsPass(const NarrowIntegerVertexInputs& inputs) { + return spvtools::Optimizer::PassToken(MakeUnique(inputs)); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h new file mode 100644 index 00000000..144a3d3f --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h @@ -0,0 +1,46 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.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 +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // Gives the integer vertex inputs at the listed locations the value GL defines for + // narrow integer attribute data (see NarrowIntegerVertexInputs). Each such input is + // re-declared as a fresh Input variable carrying the original's interface decorations, + // the original is demoted to a Private global, and the entry point seeds it once, at its + // top, with the corrected value: the low 8/16 bits masked off for a zero-extension, or + // sign-extended for a sign-extension. Every existing load and access chain keeps its id + // and type, so no other instruction is rewritten, and an array input is corrected per + // element (element i sits at location base + i). Inputs at other locations, and modules + // that are not vertex shaders, are left untouched. + // + // DirectVulkan only: the Vulkan fetch format must match the input's signedness, which is + // what moves the extension rule away from the source data's own. + class ExtendNarrowIntegerVertexInputsPass : public spvtools::opt::Pass { + public: + explicit ExtendNarrowIntegerVertexInputsPass(const NarrowIntegerVertexInputs& inputs) + : m_inputs(inputs) {} + const char* name() const override { return "mobilegl-extend-narrow-integer-vertex-inputs"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateExtendNarrowIntegerVertexInputsPass( + const NarrowIntegerVertexInputs& inputs); + + private: + NarrowIntegerVertexInputs m_inputs; + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL