[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:
2026-10-19 02:32:21 -04:00
parent fd48928260
commit 381124a0b6
10 changed files with 551 additions and 7 deletions
+1
View File
@@ -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.
@@ -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
@@ -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