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): seed narrow integer inputs from runtime widths
- ExtendNarrowIntegerVertexInputsPass gains a RuntimeWidths mode: every 32-bit integer input below location 32 is widened from two Private uint globals by shifts only - ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl reports whether it rewrote the module - the constant-mask mode is unchanged byte for byte
This commit is contained in:
@@ -420,6 +420,7 @@ set(SOURCE_FILES
|
||||
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/NarrowIntegerRuntimeWidths.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.cpp
|
||||
|
||||
@@ -36,6 +36,7 @@ add_executable(
|
||||
ZeroLayerOutputTest.cpp
|
||||
FullCoverageSampleMaskTest.cpp
|
||||
ExtendNarrowIntegerVertexInputsTest.cpp
|
||||
ExtendNarrowIntegerVertexInputsRuntimeTest.cpp
|
||||
RelaxedOpaqueStructTest.cpp
|
||||
ShadingRateEsslTest.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsRuntimeTest.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
|
||||
//
|
||||
// ExtendNarrowIntegerVertexInputsPass's RuntimeWidths mode, the one a backend uses when it cannot
|
||||
// vary the program per draw. Every 32-bit integer input below location 32 is seeded from the two
|
||||
// width globals, with shifts only - no bitfield instruction, which ESSL 3.00 cannot spell - and the
|
||||
// module must stay valid, keep its reflected inputs and leave a module without integer inputs 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::kNarrowIntegerWidths16GlobalName;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::kNarrowIntegerWidths8GlobalName;
|
||||
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 int single;
|
||||
layout(location = 4) in uint other;
|
||||
layout(location = 5) in int packed[2];
|
||||
flat out ivec4 a;
|
||||
flat out uvec4 b;
|
||||
void main() {
|
||||
a = lightAndData + ivec4(single, packed[0], packed[1], 0);
|
||||
b = uvec4(texc, other, 0u);
|
||||
gl_Position = vec4(pos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
constexpr const char* kFloatOnlyVertex = R"(#version 330 core
|
||||
layout(location = 0) in vec2 pos;
|
||||
void main() { 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); }
|
||||
)";
|
||||
constexpr const char* kPlainFragment = R"(#version 330 core
|
||||
out vec4 color;
|
||||
void main() { color = vec4(1.0); }
|
||||
)";
|
||||
|
||||
// Index 0 is the vertex module, 1 the fragment module.
|
||||
Vector<Vector<Uint32>> CompileProgram(const char* vertex, const char* fragment) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
for (const auto& [stage, source] :
|
||||
Vector<Pair<GLenum, const char*>>{{GL_VERTEX_SHADER, vertex}, {GL_FRAGMENT_SHADER, fragment}}) {
|
||||
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;
|
||||
}
|
||||
|
||||
Vector<String> LinesWith(const String& text, const String& needle) {
|
||||
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(needle) != String::npos) lines.push_back(line);
|
||||
begin = end + 1;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Moves every `OpDecorate %id Location from` to `to`, in the binary itself.
|
||||
void MoveLocation(Vector<Uint32>& spirv, Uint32 from, Uint32 to) {
|
||||
for (SizeT word = 5; word < spirv.size();) {
|
||||
const Uint32 count = spirv[word] >> 16;
|
||||
const Uint32 opcode = spirv[word] & 0xFFFFu;
|
||||
if (count == 0) break;
|
||||
if (opcode == 71u /* OpDecorate */ && count == 4 && spirv[word + 2] == 30u /* Location */ &&
|
||||
spirv[word + 3] == from)
|
||||
spirv[word + 3] = to;
|
||||
word += count;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(ExtendNarrowIntegerVertexInputsRuntimeWidths, SeedsEveryIntegerInputFromTheTwoWidths) {
|
||||
MobileGL::Initialize();
|
||||
const auto modules = CompileProgram(kVertex, kFragment);
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
const String original = Disassemble(modules[0]);
|
||||
|
||||
Vector<Uint32> output;
|
||||
Bool changed = false;
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(modules[0], output, changed, true));
|
||||
EXPECT_TRUE(changed);
|
||||
EXPECT_EQ(ValidationError(output), String{});
|
||||
const String text = Disassemble(output);
|
||||
|
||||
// The two globals: named, Private, initialised to 0 and never stored.
|
||||
for (const char* name : {kNarrowIntegerWidths8GlobalName, kNarrowIntegerWidths16GlobalName}) {
|
||||
const String id = String("%") + name;
|
||||
EXPECT_NE(text.find(id + " = OpVariable %_ptr_Private_uint Private %uint_0"), String::npos) << name << "\n"
|
||||
<< text;
|
||||
EXPECT_EQ(CountOccurrences(text, String("OpStore ") + id + " "), 0u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, String("OpLoad %uint ") + id), 1u) << text;
|
||||
}
|
||||
|
||||
// Every integer input is a seeded Private copy now; the float input is untouched.
|
||||
for (const char* name : {"%lightAndData", "%texc", "%single", "%other", "%packed"}) {
|
||||
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;
|
||||
|
||||
// Six seeded values (the ivec4, the uvec2, the int, the uint and two array elements): a left
|
||||
// shift each, an arithmetic right shift for the two uint ones, a logical one for the four int
|
||||
// ones, and the shift amount splatted for the two vectors. No bitfield instruction at all.
|
||||
EXPECT_EQ(CountOccurrences(text, "OpShiftLeftLogical") - CountOccurrences(original, "OpShiftLeftLogical"), 6u)
|
||||
<< text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpShiftRightArithmetic %v2uint") +
|
||||
CountOccurrences(text, "OpShiftRightArithmetic %uint"),
|
||||
2u)
|
||||
<< text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpShiftLeftLogical %v4int"), 1u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpShiftRightLogical %v4int"), 1u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpShiftRightLogical %int"), 3u) << text;
|
||||
for (const char* splat : {"OpCompositeConstruct %v4uint", "OpCompositeConstruct %v2uint"})
|
||||
EXPECT_EQ(CountOccurrences(text, splat) - CountOccurrences(original, splat), 1u) << splat << "\n" << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpBitFieldSExtract"), 0u) << text;
|
||||
EXPECT_EQ(CountOccurrences(text, "OpBitFieldUExtract"), 0u) << text;
|
||||
EXPECT_NE(text.find("%uint_24"), String::npos) << text;
|
||||
EXPECT_NE(text.find("%uint_16"), String::npos) << text;
|
||||
|
||||
EXPECT_EQ(ReflectInputs(output), ReflectInputs(modules[0]));
|
||||
}
|
||||
|
||||
// A width bit exists only for locations 0-31, and shifting a 32-bit value by 32 or more is
|
||||
// undefined, so an input from location 32 up keeps its fetched value and its Input variable.
|
||||
TEST(ExtendNarrowIntegerVertexInputsRuntimeWidths, LeavesLocationsFrom32UpAlone) {
|
||||
MobileGL::Initialize();
|
||||
auto modules = CompileProgram(kVertex, kFragment);
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
Vector<Uint32> input = modules[0];
|
||||
MoveLocation(input, 1, 40); // lightAndData
|
||||
MoveLocation(input, 3, 33); // single
|
||||
|
||||
Vector<Uint32> output;
|
||||
Bool changed = false;
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(input, output, changed, true));
|
||||
EXPECT_TRUE(changed);
|
||||
const String text = Disassemble(output);
|
||||
EXPECT_NE(text.find("%lightAndData = OpVariable %_ptr_Input_v4int Input"), String::npos) << text;
|
||||
EXPECT_NE(text.find("%single = OpVariable %_ptr_Input_int Input"), String::npos) << text;
|
||||
EXPECT_NE(text.find("%texc = OpVariable %_ptr_Private_"), String::npos) << text;
|
||||
|
||||
// Every integer input above the bits: nothing to seed, so nothing changes.
|
||||
for (const Uint32 location : {2u, 4u, 5u, 6u})
|
||||
MoveLocation(input, location, 50 + location);
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(input, output, changed, true));
|
||||
EXPECT_FALSE(changed);
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(ExtendNarrowIntegerVertexInputsRuntimeWidths, LeavesAModuleWithoutIntegerInputsAlone) {
|
||||
MobileGL::Initialize();
|
||||
const auto plain = CompileProgram(kFloatOnlyVertex, kPlainFragment);
|
||||
ASSERT_EQ(plain.size(), 2u);
|
||||
Vector<Uint32> output;
|
||||
Bool changed = true;
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(plain[0], output, changed, true));
|
||||
EXPECT_FALSE(changed);
|
||||
EXPECT_EQ(output, plain[0]);
|
||||
|
||||
const auto modules = CompileProgram(kVertex, kFragment);
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
changed = true;
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(modules[1], output, changed, true));
|
||||
EXPECT_FALSE(changed) << "a fragment module has no vertex inputs";
|
||||
EXPECT_EQ(output, modules[1]);
|
||||
}
|
||||
|
||||
// From SPIR-V 1.4 an entry point lists every global it uses, so the two width globals have to
|
||||
// join the interface beside the new Inputs and the Private copies.
|
||||
TEST(ExtendNarrowIntegerVertexInputsRuntimeWidths, ListsTheWidthGlobalsInASpirv14Interface) {
|
||||
MobileGL::Initialize();
|
||||
const auto modules = CompileProgram(kVertex, kFragment);
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
Vector<Uint32> input = modules[0];
|
||||
input[1] = 0x00010400u;
|
||||
ASSERT_EQ(ValidationError(input, SPV_ENV_VULKAN_1_2), String{});
|
||||
|
||||
Vector<Uint32> output;
|
||||
Bool changed = false;
|
||||
ASSERT_TRUE(ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(input, output, changed, false));
|
||||
EXPECT_TRUE(changed);
|
||||
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 : {kNarrowIntegerWidths8GlobalName, kNarrowIntegerWidths16GlobalName, "lightAndData", "texc"})
|
||||
EXPECT_NE(entryPoint[0].find(String("%") + name), String::npos) << name << "\n" << entryPoint[0];
|
||||
}
|
||||
@@ -34,6 +34,13 @@ namespace MobileGL {
|
||||
// Hashed as raw bytes by the program cache, so it must stay free of padding.
|
||||
static_assert(sizeof(NarrowIntegerVertexInputs) == 4 * sizeof(Uint32));
|
||||
|
||||
// The two Private uint globals ExtendNarrowIntegerVertexInputsPass's RuntimeWidths mode
|
||||
// reads each location's data width from: bit L of the first is set for 8-bit data at
|
||||
// location L, of the second for 16-bit data. A backend running that mode turns them into
|
||||
// uniforms and sets them per draw; 0, the value after a link, leaves every input as fetched.
|
||||
inline constexpr const char* kNarrowIntegerWidths8GlobalName = "mg_NarrowIntegerWidths8";
|
||||
inline constexpr const char* kNarrowIntegerWidths16GlobalName = "mg_NarrowIntegerWidths16";
|
||||
|
||||
// The vertex-input half: the locations whose attribute DATA is narrow integer, by width
|
||||
// and signedness. Each backend fills it from its own description of the data.
|
||||
struct NarrowIntegerAttributeMasks {
|
||||
|
||||
@@ -1998,6 +1998,19 @@ namespace MobileGL {
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, Bool& changed,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
changed = false;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(ExtendNarrowIntegerVertexInputsPass::CreateRuntimeWidthsPass(&changed));
|
||||
const bool ok = RunOptimizerChecked("ExtendNarrowIntegerVertexInputsForEssl", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
changed = changed && ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerPixelCenterIntegerForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
|
||||
@@ -351,6 +351,12 @@ namespace MobileGL {
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const NarrowIntegerVertexInputs& inputs,
|
||||
bool enableSpirvValidation = false);
|
||||
// The same correction with each location's data width read at run time from the two
|
||||
// kNarrowIntegerWidths*GlobalName globals, for a backend that cannot vary the program
|
||||
// per draw. `changed` reports whether the module had an integer input to seed.
|
||||
static bool ExtendNarrowIntegerVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, Bool& changed,
|
||||
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.
|
||||
|
||||
+60
-5
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "ExtendNarrowIntegerVertexInputsPass.h"
|
||||
#include "NarrowIntegerRuntimeWidths.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
@@ -38,6 +39,8 @@ namespace MobileGL {
|
||||
struct Extension {
|
||||
Uint32 bits = 0; // 0: leave the value alone
|
||||
Bool sign = false;
|
||||
// The width is read at run time (RuntimeWidths); `bits` is then only non-zero.
|
||||
Bool runtime = false;
|
||||
};
|
||||
|
||||
Extension ExtensionAt(const NarrowIntegerVertexInputs& inputs, Uint32 location) {
|
||||
@@ -50,6 +53,17 @@ namespace MobileGL {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Every input below location 32 (a width bit exists for it), extended by its own rule.
|
||||
Extension RuntimeExtensionAt(Uint32 location, Bool unsignedInput) {
|
||||
if (location >= kMaxLocations) return {};
|
||||
return {32, unsignedInput, true};
|
||||
}
|
||||
|
||||
Bool IsUnsignedInteger(const analysis::Type* type) {
|
||||
if (const auto* vector = type->AsVector()) type = vector->element_type();
|
||||
return !type->AsInteger()->IsSigned();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -72,6 +86,8 @@ namespace MobileGL {
|
||||
// of a sign-extension; 0 where the element is left alone.
|
||||
std::vector<Uint32> operandIds;
|
||||
Uint32 privatePointerTypeId = 0;
|
||||
Uint32 baseLocation = 0;
|
||||
NarrowIntegerRuntimeWidths::ShiftType shiftType;
|
||||
// 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;
|
||||
@@ -92,7 +108,8 @@ namespace MobileGL {
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status ExtendNarrowIntegerVertexInputsPass::Process() {
|
||||
if (!m_inputs.Any()) return Status::SuccessWithoutChange;
|
||||
const Bool runtime = m_mode == Mode::RuntimeWidths;
|
||||
if (!runtime && !m_inputs.Any()) return Status::SuccessWithoutChange;
|
||||
|
||||
auto* irContext = context();
|
||||
auto entryPoints = irContext->module()->entry_points();
|
||||
@@ -141,11 +158,15 @@ namespace MobileGL {
|
||||
target.arrayLength = static_cast<Uint32>(length->GetZeroExtendedValue());
|
||||
elementCount = target.arrayLength;
|
||||
}
|
||||
if (!IsInteger32ScalarOrVector(typeMgr->GetType(target.elementTypeId))) continue;
|
||||
const analysis::Type* elementType = typeMgr->GetType(target.elementTypeId);
|
||||
if (!IsInteger32ScalarOrVector(elementType)) continue;
|
||||
target.baseLocation = location->second;
|
||||
|
||||
Bool any = false;
|
||||
for (Uint32 element = 0; element < elementCount; ++element) {
|
||||
target.extensions.push_back(ExtensionAt(m_inputs, location->second + element));
|
||||
target.extensions.push_back(
|
||||
runtime ? RuntimeExtensionAt(location->second + element, IsUnsignedInteger(elementType))
|
||||
: ExtensionAt(m_inputs, location->second + element));
|
||||
any = any || target.extensions.back().bits != 0;
|
||||
}
|
||||
if (any) targets.push_back(std::move(target));
|
||||
@@ -186,11 +207,19 @@ namespace MobileGL {
|
||||
constant ? constantMgr->GetDefiningInstruction(constant, typeId) : nullptr;
|
||||
return inst ? inst->result_id() : 0;
|
||||
};
|
||||
NarrowIntegerRuntimeWidths::Ids runtimeIds;
|
||||
if (runtime && !NarrowIntegerRuntimeWidths::MaterializeTypes(irContext, runtimeIds))
|
||||
return Status::Failure;
|
||||
Uint32 zeroOffsetId = 0;
|
||||
for (auto& target : targets) {
|
||||
for (const Extension& extension : target.extensions) {
|
||||
for (SizeT element = 0; element < target.extensions.size(); ++element) {
|
||||
const Extension& extension = target.extensions[element];
|
||||
Uint32 operandId = 0;
|
||||
if (extension.bits != 0) {
|
||||
if (extension.runtime) {
|
||||
// The location, which selects the element's bit in the two widths.
|
||||
operandId = constantMgr->GetUIntConstId(target.baseLocation + static_cast<Uint32>(element));
|
||||
if (operandId == 0) return Status::Failure;
|
||||
} else if (extension.bits != 0) {
|
||||
operandId = extension.sign ? constantMgr->GetUIntConstId(extension.bits)
|
||||
: constantId(target.elementTypeId, (1u << extension.bits) - 1u);
|
||||
if (operandId == 0) return Status::Failure;
|
||||
@@ -201,6 +230,11 @@ namespace MobileGL {
|
||||
}
|
||||
target.operandIds.push_back(operandId);
|
||||
}
|
||||
if (runtime) {
|
||||
target.shiftType = NarrowIntegerRuntimeWidths::MaterializeShiftType(irContext, runtimeIds,
|
||||
target.elementTypeId);
|
||||
if (target.shiftType.typeId == 0) return Status::Failure;
|
||||
}
|
||||
target.privatePointerTypeId =
|
||||
typeMgr->FindPointerToType(target.pointeeTypeId, spv::StorageClass::Private);
|
||||
if (target.privatePointerTypeId == 0) return Status::Failure;
|
||||
@@ -269,9 +303,20 @@ namespace MobileGL {
|
||||
++insertPoint;
|
||||
return resultId;
|
||||
};
|
||||
const NarrowIntegerRuntimeWidths::Emitter emitter{irContext, &insertPoint, nextId};
|
||||
if (runtime) {
|
||||
if (!NarrowIntegerRuntimeWidths::DeclareGlobals(irContext, runtimeIds, nextId))
|
||||
return Status::Failure;
|
||||
NarrowIntegerRuntimeWidths::LoadWidths(emitter, runtimeIds);
|
||||
}
|
||||
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.runtime) {
|
||||
return NarrowIntegerRuntimeWidths::Extend(emitter, runtimeIds, target.elementTypeId,
|
||||
target.shiftType, target.operandIds[element],
|
||||
extension.sign, valueId);
|
||||
}
|
||||
if (!extension.sign) {
|
||||
return insert(spv::Op::OpBitwiseAnd, target.elementTypeId,
|
||||
{valueId, target.operandIds[element]});
|
||||
@@ -345,9 +390,14 @@ namespace MobileGL {
|
||||
}
|
||||
insert(spv::Op::OpStore, 0, {privateId, correctedId});
|
||||
}
|
||||
if (runtime && interfaceListsPrivate) {
|
||||
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {runtimeIds.widths8Id}});
|
||||
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {runtimeIds.widths16Id}});
|
||||
}
|
||||
|
||||
if (outOfIds) return Status::Failure;
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
if (m_outChanged != nullptr) *m_outChanged = true;
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
@@ -355,6 +405,11 @@ namespace MobileGL {
|
||||
CreateExtendNarrowIntegerVertexInputsPass(const NarrowIntegerVertexInputs& inputs) {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<ExtendNarrowIntegerVertexInputsPass>(inputs));
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken ExtendNarrowIntegerVertexInputsPass::CreateRuntimeWidthsPass(
|
||||
Bool* outChanged) {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<ExtendNarrowIntegerVertexInputsPass>(outChanged));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
+15
-2
@@ -26,20 +26,33 @@ namespace MobileGL {
|
||||
// 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.
|
||||
// ConstantMasks bakes the listed locations into the module. RuntimeWidths seeds every
|
||||
// 32-bit integer input below location 32 from two Private uint globals instead (see
|
||||
// kNarrowIntegerWidths8GlobalName), so one module serves every data width; a width bit of
|
||||
// 0 leaves the value as fetched.
|
||||
class ExtendNarrowIntegerVertexInputsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
enum class Mode : Uint8 {
|
||||
ConstantMasks,
|
||||
RuntimeWidths,
|
||||
};
|
||||
|
||||
explicit ExtendNarrowIntegerVertexInputsPass(const NarrowIntegerVertexInputs& inputs)
|
||||
: m_inputs(inputs) {}
|
||||
// RuntimeWidths; `outChanged` (optional) reports whether the module was rewritten.
|
||||
explicit ExtendNarrowIntegerVertexInputsPass(Bool* outChanged)
|
||||
: m_mode(Mode::RuntimeWidths), m_outChanged(outChanged) {}
|
||||
const char* name() const override { return "mobilegl-extend-narrow-integer-vertex-inputs"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateExtendNarrowIntegerVertexInputsPass(
|
||||
const NarrowIntegerVertexInputs& inputs);
|
||||
static spvtools::Optimizer::PassToken CreateRuntimeWidthsPass(Bool* outChanged);
|
||||
|
||||
private:
|
||||
NarrowIntegerVertexInputs m_inputs;
|
||||
Mode m_mode = Mode::ConstantMasks;
|
||||
Bool* m_outChanged = nullptr;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.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 "NarrowIntegerRuntimeWidths.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h>
|
||||
|
||||
namespace MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerRuntimeWidths {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
Bool AddNamedGlobal(IRContext* context, const Ids& ids, Uint32 id, const char* name) {
|
||||
if (id == 0) return false;
|
||||
context->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
context, spv::Op::OpVariable, ids.privatePointerTypeId, id,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Private)}},
|
||||
{SPV_OPERAND_TYPE_ID, {ids.zeroId}}}));
|
||||
context->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
|
||||
context, spv::Op::OpName, 0, 0,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {id}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MaterializeTypes(IRContext* context, Ids& ids) {
|
||||
auto* typeMgr = context->get_type_mgr();
|
||||
auto* constantMgr = context->get_constant_mgr();
|
||||
ids.uintTypeId = typeMgr->GetUIntTypeId();
|
||||
ids.zeroId = constantMgr->GetUIntConstId(0);
|
||||
ids.oneId = constantMgr->GetUIntConstId(1);
|
||||
ids.byteShiftId = constantMgr->GetUIntConstId(24);
|
||||
ids.shortShiftId = constantMgr->GetUIntConstId(16);
|
||||
ids.privatePointerTypeId = typeMgr->FindPointerToType(ids.uintTypeId, spv::StorageClass::Private);
|
||||
return ids.uintTypeId != 0 && ids.zeroId != 0 && ids.oneId != 0 && ids.byteShiftId != 0 &&
|
||||
ids.shortShiftId != 0 && ids.privatePointerTypeId != 0;
|
||||
}
|
||||
|
||||
ShiftType MaterializeShiftType(IRContext* context, const Ids& ids, Uint32 elementTypeId) {
|
||||
auto* typeMgr = context->get_type_mgr();
|
||||
const auto* type = typeMgr->GetType(elementTypeId);
|
||||
if (type == nullptr) return {};
|
||||
if (const auto* vector = type->AsVector()) {
|
||||
const Uint32 components = vector->element_count();
|
||||
return {typeMgr->GetUIntVectorTypeId(components), components};
|
||||
}
|
||||
return {ids.uintTypeId, 1};
|
||||
}
|
||||
|
||||
Bool DeclareGlobals(IRContext* context, Ids& ids, const std::function<Uint32()>& nextId) {
|
||||
ids.widths8Id = nextId();
|
||||
ids.widths16Id = nextId();
|
||||
return AddNamedGlobal(context, ids, ids.widths8Id, kNarrowIntegerWidths8GlobalName) &&
|
||||
AddNamedGlobal(context, ids, ids.widths16Id, kNarrowIntegerWidths16GlobalName);
|
||||
}
|
||||
|
||||
Uint32 Emitter::Emit(spv::Op opcode, Uint32 typeId, const Vector<Uint32>& operands) const {
|
||||
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(
|
||||
spvtools::MakeUnique<Instruction>(context, opcode, typeId, resultId, operandList));
|
||||
++*insertPoint;
|
||||
return resultId;
|
||||
}
|
||||
|
||||
void LoadWidths(const Emitter& emitter, Ids& ids) {
|
||||
ids.widths8ValueId = emitter.Emit(spv::Op::OpLoad, ids.uintTypeId, {ids.widths8Id});
|
||||
ids.widths16ValueId = emitter.Emit(spv::Op::OpLoad, ids.uintTypeId, {ids.widths16Id});
|
||||
}
|
||||
|
||||
Uint32 Extend(const Emitter& emitter, const Ids& ids, Uint32 elementTypeId, const ShiftType& shiftType,
|
||||
Uint32 locationId, Bool signExtend, Uint32 valueId) {
|
||||
// s = ((w8 >> L) & 1) * 24 + ((w16 >> L) & 1) * 16; one location has one data type, so at
|
||||
// most one of the two bits is set and s is 0, 16 or 24.
|
||||
const auto widthShift = [&](Uint32 widthsValueId, Uint32 shiftId) {
|
||||
const Uint32 shifted =
|
||||
emitter.Emit(spv::Op::OpShiftRightLogical, ids.uintTypeId, {widthsValueId, locationId});
|
||||
const Uint32 bit = emitter.Emit(spv::Op::OpBitwiseAnd, ids.uintTypeId, {shifted, ids.oneId});
|
||||
return emitter.Emit(spv::Op::OpIMul, ids.uintTypeId, {bit, shiftId});
|
||||
};
|
||||
const Uint32 byteShift = widthShift(ids.widths8ValueId, ids.byteShiftId);
|
||||
const Uint32 shortShift = widthShift(ids.widths16ValueId, ids.shortShiftId);
|
||||
Uint32 shift = emitter.Emit(spv::Op::OpIAdd, ids.uintTypeId, {byteShift, shortShift});
|
||||
if (shiftType.components > 1)
|
||||
shift = emitter.Emit(spv::Op::OpCompositeConstruct, shiftType.typeId,
|
||||
Vector<Uint32>(shiftType.components, shift));
|
||||
const Uint32 high = emitter.Emit(spv::Op::OpShiftLeftLogical, elementTypeId, {valueId, shift});
|
||||
return emitter.Emit(signExtend ? spv::Op::OpShiftRightArithmetic : spv::Op::OpShiftRightLogical, elementTypeId,
|
||||
{high, shift});
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerRuntimeWidths
|
||||
@@ -0,0 +1,66 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.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/instruction_list.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
// The RuntimeWidths half of ExtendNarrowIntegerVertexInputsPass. The data width of each location
|
||||
// is draw state, so the seed reads it from two uint globals - bit L of the first set for 8-bit
|
||||
// data at location L, of the second for 16-bit data - and widens the fetched value by the input's
|
||||
// own rule from that many low bits: s = 24 or 16 (0 where neither bit is set) and
|
||||
// int input: value = (value << s) >>> s (zero-extension)
|
||||
// uint input: value = (value << s) >> s (sign-extension)
|
||||
// Only shifts, AND, multiply and add, which ESSL 3.00 can spell; no bitfield instruction.
|
||||
namespace MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerRuntimeWidths {
|
||||
struct Ids {
|
||||
Uint32 uintTypeId = 0;
|
||||
Uint32 zeroId = 0;
|
||||
Uint32 oneId = 0;
|
||||
Uint32 byteShiftId = 0; // 24
|
||||
Uint32 shortShiftId = 0; // 16
|
||||
Uint32 privatePointerTypeId = 0;
|
||||
Uint32 widths8Id = 0;
|
||||
Uint32 widths16Id = 0;
|
||||
Uint32 widths8ValueId = 0;
|
||||
Uint32 widths16ValueId = 0;
|
||||
};
|
||||
|
||||
// The shift-amount type of one input: its uint vector, or uint for a scalar input.
|
||||
struct ShiftType {
|
||||
Uint32 typeId = 0;
|
||||
Uint32 components = 1;
|
||||
};
|
||||
|
||||
// Types and constants first: they have to precede every variable the pass adds or moves.
|
||||
Bool MaterializeTypes(spvtools::opt::IRContext* context, Ids& ids);
|
||||
ShiftType MaterializeShiftType(spvtools::opt::IRContext* context, const Ids& ids, Uint32 elementTypeId);
|
||||
|
||||
// The two named Private globals, initialised to 0 so an unset width reads as "as fetched".
|
||||
Bool DeclareGlobals(spvtools::opt::IRContext* context, Ids& ids, const std::function<Uint32()>& nextId);
|
||||
|
||||
// Instructions go in before `insertPoint`, which then points past them.
|
||||
struct Emitter {
|
||||
spvtools::opt::IRContext* context = nullptr;
|
||||
spvtools::opt::InstructionList::iterator* insertPoint = nullptr;
|
||||
std::function<Uint32()> nextId;
|
||||
|
||||
Uint32 Emit(spv::Op opcode, Uint32 typeId, const Vector<Uint32>& operands) const;
|
||||
};
|
||||
|
||||
// Loads both widths once, at the top of the entry point.
|
||||
void LoadWidths(const Emitter& emitter, Ids& ids);
|
||||
|
||||
// `valueId` (of `elementTypeId`, the input's type) widened per the bits at `locationId`.
|
||||
Uint32 Extend(const Emitter& emitter, const Ids& ids, Uint32 elementTypeId, const ShiftType& shiftType,
|
||||
Uint32 locationId, Bool signExtend, Uint32 valueId);
|
||||
} // namespace MobileGL::MG_Util::ShaderTranspiler::NarrowIntegerRuntimeWidths
|
||||
Reference in New Issue
Block a user