mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feat] (CMakeLists.txt, MG_Test/ShaderTranspiler, MG_Util/ShaderTranspiler): add a pass extending narrow integer vertex inputs GL's way
- listed locations are masked to 8/16 bits or sign-extended from them at the top of main - each input becomes a Private copy seeded once; access chains are retyped, arrays go per element
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -35,6 +35,7 @@ add_executable(
|
||||
PixelCenterIntegerTest.cpp
|
||||
ZeroLayerOutputTest.cpp
|
||||
FullCoverageSampleMaskTest.cpp
|
||||
ExtendNarrowIntegerVertexInputsTest.cpp
|
||||
RelaxedOpaqueStructTest.cpp
|
||||
ShadingRateEsslTest.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 <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <tuple>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
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<Vector<Uint32>> CompileProgram() {
|
||||
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_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<Vector<Uint32>> modules = binary.value();
|
||||
for (auto& module : modules)
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
|
||||
return modules;
|
||||
}
|
||||
|
||||
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, 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<String> LinesWith(const String& text, const String& opcode) {
|
||||
Vector<String> 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<std::tuple<Uint32, Uint32, Uint32>> ReflectInputs(const Vector<Uint32>& 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<SpvReflectInterfaceVariable*> inputs(count);
|
||||
EXPECT_EQ(spvReflectEnumerateInputVariables(&module, &count, inputs.data()), SPV_REFLECT_RESULT_SUCCESS);
|
||||
Vector<std::tuple<Uint32, Uint32, Uint32>> 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<Uint32>(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<Uint32>& input = modules[0];
|
||||
const String original = Disassemble(input);
|
||||
ASSERT_EQ(CountOccurrences(original, "OpBitwiseAnd"), 0u) << original;
|
||||
ASSERT_EQ(CountOccurrences(original, "OpBitFieldSExtract"), 0u) << original;
|
||||
|
||||
Vector<Uint32> 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<Uint32> 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<Uint32> input = modules[0];
|
||||
ASSERT_GT(input.size(), 1u);
|
||||
input[1] = 0x00010400u;
|
||||
ASSERT_EQ(ValidationError(input, SPV_ENV_VULKAN_1_2), String{});
|
||||
|
||||
Vector<Uint32> 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];
|
||||
}
|
||||
@@ -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 <Includes.h>
|
||||
|
||||
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
|
||||
@@ -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<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& 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<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "SpvcSession.h"
|
||||
#include "glslang/TVarEntryInfo.h"
|
||||
#include "glslang/TMglGlslIoResolver.h"
|
||||
#include "NarrowIntegerVertexInputs.h"
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
@@ -342,6 +343,14 @@ namespace MobileGL {
|
||||
// See ZeroBaseVertexPass.
|
||||
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary, Vector<uint32_t>& 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<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& 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.
|
||||
|
||||
+360
@@ -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 <memory>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<Extension> 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<Uint32> 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<std::pair<Instruction*, Uint32>> 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<spv::ExecutionModel>(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<Uint32, Uint32> locationById;
|
||||
for (const Instruction& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) continue;
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) == spv::Decoration::Location)
|
||||
locationById[annotation.GetSingleWordInOperand(0)] = annotation.GetSingleWordInOperand(2);
|
||||
}
|
||||
|
||||
std::vector<Target> targets;
|
||||
for (Instruction& inst : irContext->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||
if (static_cast<spv::StorageClass>(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<Uint32>(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<Uint32>(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<Instruction*> 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<Instruction*> 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<Uint32> 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<Instruction>(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<Instruction>(
|
||||
irContext, spv::Op::OpVariable, variable->type_id(), inputId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(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<Uint32>(spv::StorageClass::Private)});
|
||||
variable->RemoveFromList();
|
||||
irContext->AddGlobalValue(std::unique_ptr<Instruction>(variable));
|
||||
for (const auto& [chain, privateChainTypeId] : target.accessChains)
|
||||
chain->SetResultType(privateChainTypeId);
|
||||
|
||||
std::vector<Operand> 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<Uint32> elementIds;
|
||||
for (Uint32 element = 0; element < target.arrayLength; ++element) {
|
||||
const Uint32 elementId = nextId();
|
||||
insertPoint = insertPoint.InsertBefore(MakeUnique<Instruction>(
|
||||
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<Instruction>(
|
||||
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<ExtendNarrowIntegerVertexInputsPass>(inputs));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -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 <Includes.h>
|
||||
#include <MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user