diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ec27beb..36c17b87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index 6b9ff2b2..c73228bb 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -36,6 +36,7 @@ add_executable( ZeroLayerOutputTest.cpp FullCoverageSampleMaskTest.cpp ExtendNarrowIntegerVertexInputsTest.cpp + ExtendNarrowIntegerVertexInputsRuntimeTest.cpp RelaxedOpaqueStructTest.cpp ShadingRateEsslTest.cpp ) diff --git a/MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsRuntimeTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsRuntimeTest.cpp new file mode 100644 index 00000000..cab1ad74 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/ExtendNarrowIntegerVertexInputsRuntimeTest.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 + +#include +#include + +#include "Includes.h" +#include "Init.h" +#include +#include + +#include +#include + +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> CompileProgram(const char* vertex, const char* fragment) { + using namespace MG_Util::ShaderTranspiler; + Vector> shaders; + for (const auto& [stage, source] : + Vector>{{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> modules = binary.value(); + for (auto& module : modules) + EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true)); + return modules; + } + + String Disassemble(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + String text; + EXPECT_TRUE(tools.Disassemble(spirv, &text)); + return text; + } + + String ValidationError(const Vector& spirv, spv_target_env env = SPV_ENV_VULKAN_1_1) { + spvtools::SpirvTools tools(env); + String error; + tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) { + error += message != nullptr ? message : ""; + }); + return tools.Validate(spirv) ? String{} : (error.empty() ? String{"invalid"} : error); + } + + SizeT CountOccurrences(const String& text, const String& needle) { + SizeT count = 0; + for (SizeT at = text.find(needle); at != String::npos; at = text.find(needle, at + needle.size())) + ++count; + return count; + } + + Vector LinesWith(const String& text, const String& needle) { + Vector lines; + SizeT begin = 0; + while (begin < text.size()) { + SizeT end = text.find('\n', begin); + if (end == String::npos) end = text.size(); + String line = text.substr(begin, end - begin); + if (line.find(needle) != String::npos) lines.push_back(line); + begin = end + 1; + } + return lines; + } + + Vector> ReflectInputs(const Vector& spirv) { + SpvReflectShaderModule module{}; + EXPECT_EQ(spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &module), + SPV_REFLECT_RESULT_SUCCESS); + Uint32 count = 0; + EXPECT_EQ(spvReflectEnumerateInputVariables(&module, &count, nullptr), SPV_REFLECT_RESULT_SUCCESS); + Vector inputs(count); + EXPECT_EQ(spvReflectEnumerateInputVariables(&module, &count, inputs.data()), SPV_REFLECT_RESULT_SUCCESS); + Vector> result; + for (const auto* input : inputs) { + if (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) continue; + result.emplace_back(input->location, input->numeric.vector.component_count, + static_cast(input->type_description->type_flags)); + } + spvReflectDestroyShaderModule(&module); + std::sort(result.begin(), result.end()); + return result; + } + + // Moves every `OpDecorate %id Location from` to `to`, in the binary itself. + void MoveLocation(Vector& 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 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 input = modules[0]; + MoveLocation(input, 1, 40); // lightAndData + MoveLocation(input, 3, 33); // single + + Vector 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 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 input = modules[0]; + input[1] = 0x00010400u; + ASSERT_EQ(ValidationError(input, SPV_ENV_VULKAN_1_2), String{}); + + Vector 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]; +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h b/MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h index aa736fb7..322cc8c4 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h +++ b/MobileGL/MG_Util/ShaderTranspiler/NarrowIntegerVertexInputs.h @@ -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 { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 465e90fa..cdd32f01 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -1998,6 +1998,19 @@ namespace MobileGL { outputBinary, true, enableSpirvValidation); } + bool ShaderCompiler::ExtendNarrowIntegerVertexInputsForEssl(const Vector& inputBinary, + Vector& 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& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 5b4cddec..554127bf 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -351,6 +351,12 @@ namespace MobileGL { Vector& 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& inputBinary, + Vector& 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. diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.cpp index 5a3e43f8..ac4cc052 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.cpp @@ -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 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> 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(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(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(inputs)); } + + spvtools::Optimizer::PassToken ExtendNarrowIntegerVertexInputsPass::CreateRuntimeWidthsPass( + Bool* outChanged) { + return spvtools::Optimizer::PassToken(MakeUnique(outChanged)); + } } // namespace ShaderTranspiler } // namespace MG_Util } // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h index 144a3d3f..f1f52317 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ExtendNarrowIntegerVertexInputsPass.h @@ -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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.cpp new file mode 100644 index 00000000..364a7e59 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.cpp @@ -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 + +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( + context, spv::Op::OpVariable, ids.privatePointerTypeId, id, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast(spv::StorageClass::Private)}}, + {SPV_OPERAND_TYPE_ID, {ids.zeroId}}})); + context->AddDebug2Inst(spvtools::MakeUnique( + context, spv::Op::OpName, 0, 0, + std::initializer_list{{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& 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& 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(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(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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.h new file mode 100644 index 00000000..9eb74a06 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NarrowIntegerRuntimeWidths.h @@ -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 + +#include + +// 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& 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 nextId; + + Uint32 Emit(spv::Op opcode, Uint32 typeId, const Vector& 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