mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 05:38:31 +09:00
[Merge] (DirectGLES, GLState, ShaderTranspiler): land GL43 wave5 with its two new passes inside the L2 boundary
This commit is contained in:
@@ -519,13 +519,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"narrowed members, so an application that hard-codes std140 offsets "
|
||||
"computed for doubles must query them instead"));
|
||||
builder.Warn("64-bit vertex attributes",
|
||||
"not supported (ES has no GL_DOUBLE vertex format, and after the fp64 demotion "
|
||||
"above there is no 64-bit shader input left to feed either); "
|
||||
"glVertexAttribLFormat / glVertexArrayAttribLFormat succeed and their state is "
|
||||
"queryable, but an ENABLED 64-bit array is DROPPED at draw and the attribute "
|
||||
"reads its generic current value - feed the attribute with "
|
||||
"glVertexAttribPointer(GL_FLOAT) instead, which a demoted dvec input reads "
|
||||
"correctly");
|
||||
"narrowed to float32 (ES has no GL_DOUBLE vertex format, and after the fp64 "
|
||||
"demotion above there is no 64-bit shader input left to feed either); "
|
||||
"glVertexAttribLFormat / glVertexArrayAttribLFormat succeed, their state is "
|
||||
"queryable, and an ENABLED 64-bit array IS fetched - the source doubles are "
|
||||
"deinterleaved into a float32 stream at draw, so values outside float32's "
|
||||
"range or precision are rounded rather than exact");
|
||||
if (glesFuncs.glPatchParameteri != nullptr) {
|
||||
builder.Pass("Tessellation patch parameters",
|
||||
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
||||
@@ -2337,12 +2336,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"for doubles must query them instead",
|
||||
features.shaderFloat64 == VK_TRUE ? "supported" : "unsupported")));
|
||||
builder.Warn("64-bit vertex attributes",
|
||||
"not supported; there is no 64-bit shader input left to feed after the fp64 demotion "
|
||||
"above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most devices "
|
||||
"anyway. glVertexAttribLFormat succeeds and its state is queryable, but an ENABLED "
|
||||
"64-bit array is DROPPED at pipeline build and the attribute reads its generic "
|
||||
"current value - feed the attribute with glVertexAttribPointer(GL_FLOAT) instead, "
|
||||
"which a demoted dvec input reads correctly");
|
||||
"narrowed to float32; there is no 64-bit shader input left to feed after the fp64 "
|
||||
"demotion above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most "
|
||||
"devices anyway. glVertexAttribLFormat succeeds, its state is queryable, and an "
|
||||
"ENABLED 64-bit array IS fetched - the source doubles are deinterleaved into a "
|
||||
"float32 stream at draw, so values outside float32's range or precision are "
|
||||
"rounded rather than exact");
|
||||
|
||||
Bool shaderDrawParameters = false;
|
||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "SpirvPasses/LegalizeFragmentOutputIndexPass.h"
|
||||
#include "SpirvPasses/LegalizeStorageBlockArrayIndexPass.h"
|
||||
#include "SpirvPasses/FlattenAtomicCounterBlockPass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
@@ -941,6 +943,100 @@ namespace MobileGL {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
// Detection gates everything: a module that declares no array of storage
|
||||
// blocks, or indexes one only with constants - every shader but a handful -
|
||||
// pays one BuildModule and is handed back byte for byte, so the folding chain
|
||||
// can never perturb a shader that did not need it.
|
||||
if (!LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
inputBinary)) {
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stock passes do the real work, exactly as in the fragment-output
|
||||
// legalization. The only bespoke member of the chain is the loop-control hint
|
||||
// the stock unroller demands (see the pass header); with it set, the
|
||||
// `for (i = 0; i < 4; ++i) arr[i]...` shape folds to literals here and the
|
||||
// fallback below never runs.
|
||||
Optimizer folder(SPV_ENV_VULKAN_1_1);
|
||||
// First, because both the unroller and the marking pass below read the
|
||||
// induction variable as an OpPhi, and glslang emits it as loads and stores of
|
||||
// a Function variable.
|
||||
folder.RegisterPass(CreateLocalMultiStoreElimPass());
|
||||
folder.RegisterPass(LegalizeStorageBlockArrayIndexPass::CreateMarkLoopsForUnrollPass());
|
||||
folder.RegisterPass(CreateLoopUnrollPass(true));
|
||||
// Fold the unrolled induction values into the access chains, then clear out
|
||||
// what constant conditions leave behind.
|
||||
folder.RegisterPass(CreateCCPPass());
|
||||
folder.RegisterPass(CreateSimplificationPass());
|
||||
folder.RegisterPass(CreateDeadBranchElimPass());
|
||||
folder.RegisterPass(CreateBlockMergePass());
|
||||
|
||||
Vector<uint32_t> folded;
|
||||
if (!RunOptimizerChecked("LegalizeStorageBlockArrayIndexingForEssl.fold", folder,
|
||||
inputBinary, folded, true, enableSpirvValidation) ||
|
||||
folded.empty()) {
|
||||
// Fail open onto the fallback rather than onto the illegal module.
|
||||
folded = inputBinary;
|
||||
}
|
||||
|
||||
if (!LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
folded)) {
|
||||
outputBinary = folded;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Genuinely dynamic (uniform-derived, non-constant trip count, ...): lower it.
|
||||
Optimizer lowerer(SPV_ENV_VULKAN_1_1);
|
||||
lowerer.RegisterPass(
|
||||
LegalizeStorageBlockArrayIndexPass::CreateLowerToConstantSwitchPass());
|
||||
// The chains the lowering replaced are dead now; remove_outputs must stay
|
||||
// false here for the same reason it does in SanitizeAndOptimizeBinary.
|
||||
lowerer.RegisterPass(CreateAggressiveDCEPass(false));
|
||||
|
||||
if (!RunOptimizerChecked("LegalizeStorageBlockArrayIndexingForEssl.lower", lowerer, folded,
|
||||
outputBinary, true, enableSpirvValidation) ||
|
||||
outputBinary.empty()) {
|
||||
outputBinary = folded;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
outputBinary)) {
|
||||
// MGLOG_W, latched, for the same reason the fragment-output one is: this
|
||||
// runs per shader compile and shader packs compile lazily mid-session.
|
||||
MGLOG_W_ONCE("[spirv] LegalizeStorageBlockArrayIndexingForEssl: an array of storage "
|
||||
"blocks is still indexed dynamically; a strict ES driver will reject "
|
||||
"this shader");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
// Detection gates everything: a module with no atomic counter, or one whose
|
||||
// counters sit at their natural std430 offsets - which is every shader that omits
|
||||
// the offset qualifier - pays one BuildModule and is handed back byte for byte.
|
||||
if (!FlattenAtomicCounterBlockPass::BinaryHasOffsetAtomicCounterBlock(inputBinary)) {
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
}
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(FlattenAtomicCounterBlockPass::CreateFlattenAtomicCounterBlockPass());
|
||||
|
||||
return RunOptimizerChecked("FlattenAtomicCounterBlockOffsetsForEssl", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
@@ -956,26 +1052,27 @@ namespace MobileGL {
|
||||
using namespace spvtools;
|
||||
|
||||
// Declined rather than half-translated: after the rewrite the image is a 2D
|
||||
// array, so a size query on it yields three components where the shader consumes
|
||||
// two. Handing back a differently-shaped size silently is worse than leaving the
|
||||
// module alone and letting the driver say what it does not like - and unlike the
|
||||
// access path there is no correct answer to substitute, because the ES texture
|
||||
// (array) one, so a size query on it yields a component more than the shader
|
||||
// consumes. Handing back a differently-shaped size silently is worse than leaving
|
||||
// the module alone and letting the driver say what it does not like - and unlike
|
||||
// the access path there is no correct answer to substitute, because the ES texture
|
||||
// genuinely has a height the GL one does not.
|
||||
//
|
||||
// MGLOG_W, latched: per shader compile, and shader packs compile lazily
|
||||
// mid-session. (Parked at MGLOG_I until the Log.h ordering fix made W live.)
|
||||
const auto traits = Lower1DArrayImagesPass::InspectBinary(inputBinary);
|
||||
// The overwhelmingly common answer, and the reason the inspection exists: no
|
||||
// 1D-array storage image, so the module is handed back byte for byte without an
|
||||
// Optimizer ever being built. Every ESSL shader in the process passes through
|
||||
// here, so the cost of the case with nothing to do is the cost of this pass.
|
||||
// 1D storage image this pass owns, so the module is handed back byte for byte
|
||||
// without an Optimizer ever being built. Every ESSL shader in the process passes
|
||||
// through here, so the cost of the case with nothing to do is the cost of this
|
||||
// pass.
|
||||
if (!traits.declaresImage) {
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
}
|
||||
if (traits.queriesImageSize) {
|
||||
MGLOG_W_ONCE("[spirv] Lower1DArrayImagesForEssl: the module queries the size of a 1D-array "
|
||||
"storage image, which cannot be answered in the 2D-array shape ES stores it in; "
|
||||
MGLOG_W_ONCE("[spirv] Lower1DArrayImagesForEssl: the module queries the size of a 1D "
|
||||
"storage image, which cannot be answered in the 2D shape ES stores it in; "
|
||||
"leaving the module alone, and a strict ES driver will reject it");
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
@@ -983,8 +1080,8 @@ namespace MobileGL {
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(Lower1DArrayImagesPass::CreateLower1DArrayImagesPass());
|
||||
// Mandatory, not tidying. Rewriting a 1D-array image type to the 2D-array one
|
||||
// makes it structurally IDENTICAL to any real 2D-array image of the same sampled
|
||||
// Mandatory, not tidying. Rewriting a 1D(-array) image type to the 2D(-array) one
|
||||
// makes it structurally IDENTICAL to any real 2D(-array) image of the same sampled
|
||||
// type and format that the module already declared - and SPIR-V forbids duplicate
|
||||
// non-aggregate type declarations, so the result fails validation. That collision
|
||||
// is not exotic: it is the shape of this whole change's headline case, where one
|
||||
|
||||
@@ -156,6 +156,32 @@ namespace MobileGL {
|
||||
static bool LegalizeFragmentOutputIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Makes every index into an ARRAY OF SHADER STORAGE BLOCKS a constant integral
|
||||
// expression. GL 4.3 allows any dynamically-uniform index there; the Qualcomm
|
||||
// ES compiler enforces the ES 3.1 constant-expression rule and refuses the whole
|
||||
// stage ("indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted"), which loses the program while the frontend still reports
|
||||
// GL_LINK_STATUS = TRUE. Same two halves as the fragment-output legalization:
|
||||
// fold the loop-derived indices, then lower whatever is genuinely dynamic to a
|
||||
// switch over the array's range. DirectGLES transpile path only - Vulkan has no
|
||||
// such restriction and must keep seeing one descriptor array. Copies the input
|
||||
// through untouched when no block array is indexed dynamically, which is every
|
||||
// shader but a handful. See LegalizeStorageBlockArrayIndexPass.
|
||||
static bool LegalizeStorageBlockArrayIndexingForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Collapses each synthesized gl_AtomicCounterBlock_<N> into one uint array at
|
||||
// offset 0, re-indexing every counter access to the element that used to sit at
|
||||
// its byte offset. glslang preserves the application's layout(offset = N) as the
|
||||
// member's Offset decoration, no std140/std430 layout can express a first member
|
||||
// at a non-zero offset, and GLSL ES has no member layout(offset=) - so SPIRV-Cross
|
||||
// throws and takes the whole stage with it. DirectGLES transpile path only.
|
||||
// Copies the input through untouched when every counter block is already packed
|
||||
// naturally, which is every shader that omits the offset qualifier. See
|
||||
// FlattenAtomicCounterBlockPass.
|
||||
static bool FlattenAtomicCounterBlockOffsetsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||
@@ -168,10 +194,13 @@ namespace MobileGL {
|
||||
bool enableSpirvValidation = false);
|
||||
// GL_TEXTURE_1D_ARRAY storage images rewritten to the 2D-array shape the texture
|
||||
// is actually stored in on ES, with the layer moved from the coordinate's second
|
||||
// component to its third. DirectGLES transpile path only - Vulkan binds a real
|
||||
// VK_IMAGE_VIEW_TYPE_1D_ARRAY and must see the module unchanged. Copies the input
|
||||
// through untouched when the module declares no such image, which is every shader
|
||||
// but a handful. See Lower1DArrayImagesPass for what it declines and why.
|
||||
// component to its third - and, when the module performs an image ATOMIC on one,
|
||||
// the non-arrayed GL_TEXTURE_1D storage image to the 2D shape with its coordinate
|
||||
// widened to (u, 0), which is the one 1D shape SPIRV-Cross does not widen itself.
|
||||
// DirectGLES transpile path only - Vulkan binds a real VK_IMAGE_VIEW_TYPE_1D(_ARRAY)
|
||||
// and must see the module unchanged. Copies the input through untouched when the
|
||||
// module declares no such image, which is every shader but a handful. See
|
||||
// Lower1DArrayImagesPass for what it declines and why.
|
||||
static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <Config.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include "EsslBuiltinFunctionNames.h"
|
||||
|
||||
@@ -1603,6 +1604,92 @@ namespace MobileGL {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<String> FindAtomicCounterOffsetViolation(const String& source) {
|
||||
// Fast path: both keywords are required for a violation to exist, and the pair is
|
||||
// absent from every shader-pack source.
|
||||
if (source.find("atomic_uint") == String::npos || source.find("offset") == String::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
constexpr long long kAtomicCounterSize = 4; // one 32-bit word per counter
|
||||
const long long maxBufferSize = static_cast<long long>(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||
const SizeT count = tokens.size();
|
||||
// The offset the qualifier run currently being scanned declared, -1 for none.
|
||||
// Same accumulate-then-consume shape as the storage-binding scan above.
|
||||
long long offset = -1;
|
||||
long long literal = 0;
|
||||
for (SizeT pos = 0; pos < count; ++pos) {
|
||||
const String& text = tokens[pos].text;
|
||||
if (text == "layout" && pos + 1 < count && tokens[pos + 1].text == "(") {
|
||||
SizeT j = pos + 2;
|
||||
Int parenDepth = 1;
|
||||
while (j < count && parenDepth > 0) {
|
||||
const String& layoutToken = tokens[j].text;
|
||||
if (layoutToken == "(") {
|
||||
++parenDepth;
|
||||
} else if (layoutToken == ")") {
|
||||
--parenDepth;
|
||||
} else if (parenDepth == 1 && layoutToken == "offset" && j + 2 < count &&
|
||||
tokens[j + 1].text == "=" &&
|
||||
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
|
||||
offset = literal;
|
||||
j += 2;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
pos = j - 1;
|
||||
continue;
|
||||
}
|
||||
if (text == "atomic_uint") {
|
||||
// How far the declaration reaches: `atomic_uint c[N]` occupies N words
|
||||
// from the offset. An unparsable or absent declarator (an expression-sized
|
||||
// array, or the "layout(...) uniform atomic_uint;" default-qualifier form,
|
||||
// which declares no counter at all) is left alone rather than guessed at -
|
||||
// over-rejection here would be a compile failure the application cannot
|
||||
// work around.
|
||||
long long elements = 1;
|
||||
SizeT k = pos + 1;
|
||||
if (k < count && IsIdentifierToken(tokens[k])) {
|
||||
++k;
|
||||
if (k < count && tokens[k].text == "[") {
|
||||
elements = (k + 2 < count && tokens[k + 2].text == "]" &&
|
||||
ParseGlslIntegerLiteral(tokens[k + 1].text, literal))
|
||||
? std::max<long long>(1, literal)
|
||||
: -1;
|
||||
}
|
||||
} else {
|
||||
elements = -1;
|
||||
}
|
||||
// Clamped so the byte arithmetic below cannot overflow on an absurd
|
||||
// literal; any element count at or past the ceiling already fails.
|
||||
elements = std::min(elements, maxBufferSize);
|
||||
|
||||
if (offset >= 0 && elements > 0) {
|
||||
if (offset % kAtomicCounterSize != 0) {
|
||||
return "ERROR: invalid value " + std::to_string(offset) +
|
||||
" for layout specifier 'offset': an atomic counter offset must be a "
|
||||
"multiple of 4.";
|
||||
}
|
||||
if (offset > maxBufferSize - elements * kAtomicCounterSize) {
|
||||
return "ERROR: invalid value " + std::to_string(offset) +
|
||||
" for layout specifier 'offset': an atomic counter ending at byte " +
|
||||
std::to_string(offset + elements * kAtomicCounterSize) +
|
||||
" passes GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE (" +
|
||||
std::to_string(maxBufferSize) + ").";
|
||||
}
|
||||
}
|
||||
offset = -1;
|
||||
continue;
|
||||
}
|
||||
// `uniform` and the precision/auxiliary qualifiers may sit between the layout
|
||||
// list and the type keyword; anything else ends the run, so an offset never
|
||||
// leaks onto an unrelated declaration.
|
||||
if (text != "uniform" && !IsNonLayoutQualifierKeyword(text)) offset = -1;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
|
||||
UnorderedMap<String, Int> locations;
|
||||
// Fast path: without the qualifier keyword there is nothing to extract.
|
||||
|
||||
@@ -75,6 +75,18 @@ namespace MobileGL {
|
||||
// `maxBindings` is what glGetIntegerv answers for that pname; a non-positive value
|
||||
// means "nothing to check against" and every declaration passes.
|
||||
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings);
|
||||
|
||||
// GL 4.6 core 7.7 / ARB_shader_atomic_counters makes it a COMPILE-time error to
|
||||
// declare an atomic counter at an offset that is not a multiple of 4, or whose last
|
||||
// byte passes GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces both in fixOffset(),
|
||||
// which the Vulkan-relaxed parse never reaches (vkRelaxedRemapUniformVariable folds
|
||||
// the atomic_uint into a synthesized storage block and returns from declareVariable()
|
||||
// first), so MobileGL only caught them at LINK - and
|
||||
// KHR-GL43.shader_atomic_counters.negative-offset-1 never links at all. The
|
||||
// cross-stage rule (two counters sharing a binding must not overlap) stays at link:
|
||||
// a single-stage source cannot see it. Returns the compile-error text for the first
|
||||
// violation, or nullopt for a clean source.
|
||||
std::optional<String> FindAtomicCounterOffsetViolation(const String& source);
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -50,6 +50,33 @@ namespace MobileGL {
|
||||
// for the same reason - writes exactly where the demoted shader reads. Blocks with no
|
||||
// 64-bit member anywhere are never touched.
|
||||
//
|
||||
// THE MEASURED COST, so the next wave does not re-diagnose it. Four GL 4.3 conformance
|
||||
// cases fail on BOTH backends and on both an Adreno 830 and a Mali G925 - i.e. on every
|
||||
// device, because no device has shaderFloat64 and the demotion therefore always runs:
|
||||
//
|
||||
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3
|
||||
// KHR-GL43.compute_shader.fp64-case1
|
||||
// KHR-GL43.compute_shader.fp64-case3
|
||||
// ...and the std430 half of the same stdLayout case.
|
||||
//
|
||||
// They fail in the two ways this comment predicts and in no other. stdLayout-case3
|
||||
// copies a block byte for byte: the output matches the input for bytes [0, 76) and is
|
||||
// zero from there on, which is exactly the block's size once every double became a
|
||||
// float and the layout repacked tightly. fp64-case1 reports ceil(2.2) as 2: the
|
||||
// uniform's double 2.0 is 0x4000000000000000, the demoted read takes its low 32 bits
|
||||
// (0.0), ceil(0.0 + 0.2) = 1.0f = 0x3F800000 lands in the low half of the 8-byte
|
||||
// output slot and the whole thing prints as 2.
|
||||
//
|
||||
// Fixing them means NOT demoting a double that lives in a buffer block, and carrying
|
||||
// it as a uvec2 word pair instead - preserving the application's byte layout exactly,
|
||||
// unpacking to fp32 for arithmetic and repacking on store. That is a large pass with
|
||||
// the same dmat problem the paragraph above describes (a uvec2 representation cannot
|
||||
// express a matrix stride either, so it would have to decline dmat types), and the
|
||||
// default-uniform routing above reflects the demoted module, so a representation
|
||||
// change there ripples into every glUniform*d. Four of 16085 cases; deliberately not
|
||||
// attempted. compute_shader.fp64-case2 passes today and any attempt has to keep it
|
||||
// green.
|
||||
//
|
||||
// Declines (leaves the module byte-identical, so the caller's existing "this module
|
||||
// still declares Float64" failure path reports it) when the module contains an
|
||||
// operation whose validity depends on the operand really being 64 bits wide:
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.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
|
||||
|
||||
#include "FlattenAtomicCounterBlockPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/decoration_manager.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/function.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_builder.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::MakeUnique;
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Module;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// Kept in step with MG_Util/ShaderTranspiler/Types.h's
|
||||
// MAX_ATOMIC_COUNTER_BUFFER_SIZE (16384 bytes), expressed in uint elements. A
|
||||
// block whose declared byte window is wider than GL will ever let an application
|
||||
// bind is refused rather than expanded into a huge array.
|
||||
constexpr uint32_t kMaxCounterElements = 16384u / 4u;
|
||||
// The lowered block's name always starts with this; the spelling lives in
|
||||
// Types.h as ATOMIC_COUNTER_BLOCK_PREFIX, which is what the rest of MobileGL
|
||||
// matches on. Repeated rather than included because that header pulls the whole
|
||||
// backend-parameter surface into a pass that needs one string.
|
||||
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
|
||||
// The stride the flattened array is laid out with, and the size of one counter.
|
||||
constexpr uint32_t kCounterBytes = 4;
|
||||
|
||||
struct MemberPlan {
|
||||
// Where this member starts, in uint elements from the block's byte 0.
|
||||
uint32_t elementOffset = 0;
|
||||
// How many uints it occupies: 1 for a scalar counter, N for `atomic_uint c[N]`.
|
||||
uint32_t elementCount = 1;
|
||||
bool isArray = false;
|
||||
};
|
||||
|
||||
struct BlockPlan {
|
||||
Instruction* structType = nullptr;
|
||||
uint32_t uintTypeId = 0;
|
||||
std::vector<MemberPlan> members;
|
||||
// Access chains rooted at a variable of this block, in the order found.
|
||||
std::vector<Instruction*> chains;
|
||||
uint32_t totalElements = 0;
|
||||
};
|
||||
|
||||
bool NameStartsWithAtomicCounterBlockPrefix(IRContext* context, uint32_t id) {
|
||||
for (const Instruction& debug : context->module()->debugs2()) {
|
||||
if (debug.opcode() != spv::Op::OpName || debug.NumInOperands() < 2) continue;
|
||||
if (debug.GetSingleWordInOperand(0) != id) continue;
|
||||
const std::string name = debug.GetInOperand(1).AsString();
|
||||
return name.compare(0, std::strlen(kAtomicCounterBlockPrefix),
|
||||
kAtomicCounterBlockPrefix) == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The literal of the first OpMemberDecorate <structId> <member> <kind>, or none.
|
||||
bool TryGetMemberDecorationLiteral(IRContext* context, uint32_t structId, uint32_t member,
|
||||
spv::Decoration kind, uint32_t* literal) {
|
||||
for (Instruction* decoration :
|
||||
context->get_decoration_mgr()->GetDecorationsFor(structId, false)) {
|
||||
if (decoration->opcode() != spv::Op::OpMemberDecorate ||
|
||||
decoration->NumInOperands() < 4 ||
|
||||
decoration->GetSingleWordInOperand(1) != member ||
|
||||
static_cast<spv::Decoration>(decoration->GetSingleWordInOperand(2)) != kind) {
|
||||
continue;
|
||||
}
|
||||
*literal = decoration->GetSingleWordInOperand(3);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryGetDecorationLiteral(IRContext* context, uint32_t id, spv::Decoration kind,
|
||||
uint32_t* literal) {
|
||||
for (Instruction* decoration : context->get_decoration_mgr()->GetDecorationsFor(id, false)) {
|
||||
if (decoration->opcode() != spv::Op::OpDecorate || decoration->NumInOperands() < 3 ||
|
||||
static_cast<spv::Decoration>(decoration->GetSingleWordInOperand(1)) != kind) {
|
||||
continue;
|
||||
}
|
||||
*literal = decoration->GetSingleWordInOperand(2);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsUint32Type(const Instruction* type) {
|
||||
return type != nullptr && type->opcode() == spv::Op::OpTypeInt &&
|
||||
type->NumInOperands() >= 2 && type->GetSingleWordInOperand(0) == 32u &&
|
||||
type->GetSingleWordInOperand(1) == 0u;
|
||||
}
|
||||
|
||||
// The member's shape as this pass needs it, or false when it is one the pass
|
||||
// cannot re-index.
|
||||
bool DescribeMember(IRContext* context, uint32_t memberTypeId, uint32_t* uintTypeId,
|
||||
MemberPlan* plan) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
Instruction* memberType = defUseMgr->GetDef(memberTypeId);
|
||||
if (memberType == nullptr) return false;
|
||||
|
||||
if (IsUint32Type(memberType)) {
|
||||
plan->isArray = false;
|
||||
plan->elementCount = 1;
|
||||
*uintTypeId = memberTypeId;
|
||||
return true;
|
||||
}
|
||||
if (memberType->opcode() != spv::Op::OpTypeArray || memberType->NumInOperands() < 2) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t elementTypeId = memberType->GetSingleWordInOperand(0);
|
||||
if (!IsUint32Type(defUseMgr->GetDef(elementTypeId))) return false;
|
||||
// The array's stride must be the tight 4 for the flattening to keep every
|
||||
// counter on the byte it was declared at.
|
||||
uint32_t stride = 0;
|
||||
if (!TryGetDecorationLiteral(context, memberTypeId, spv::Decoration::ArrayStride, &stride) ||
|
||||
stride != kCounterBytes) {
|
||||
return false;
|
||||
}
|
||||
const spvtools::opt::analysis::Constant* length =
|
||||
context->get_constant_mgr()->FindDeclaredConstant(memberType->GetSingleWordInOperand(1));
|
||||
if (length == nullptr || length->AsIntConstant() == nullptr) return false;
|
||||
const uint32_t count = length->AsIntConstant()->GetU32BitValue();
|
||||
if (count == 0u) return false;
|
||||
plan->isArray = true;
|
||||
plan->elementCount = count;
|
||||
*uintTypeId = elementTypeId;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Whether the members' offsets already ARE the natural std430 packing, i.e.
|
||||
// whether the block transpiles as it stands and this pass must leave it alone.
|
||||
bool IsNaturallyPacked(const std::vector<MemberPlan>& members) {
|
||||
uint32_t natural = 0;
|
||||
for (const MemberPlan& member : members) {
|
||||
if (member.elementOffset != natural) return false;
|
||||
natural += member.elementCount;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Plans every atomic-counter block the module declares that is NOT already
|
||||
// naturally packed and that this pass can re-index exactly. Reads the module;
|
||||
// never rewrites it, so the same walk serves both the detection probe and phase 1
|
||||
// of the rewrite.
|
||||
std::vector<BlockPlan> BuildPlans(IRContext* context) {
|
||||
std::vector<BlockPlan> plans;
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
|
||||
std::unordered_map<uint32_t, Instruction*> candidateStructs;
|
||||
for (Instruction& inst : context->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpTypeStruct || inst.NumInOperands() == 0) continue;
|
||||
if (!NameStartsWithAtomicCounterBlockPrefix(context, inst.result_id())) continue;
|
||||
candidateStructs.emplace(inst.result_id(), &inst);
|
||||
}
|
||||
if (candidateStructs.empty()) return plans;
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> variableToStruct;
|
||||
for (Instruction& inst : context->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||
Instruction* pointerType = defUseMgr->GetDef(inst.type_id());
|
||||
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) continue;
|
||||
if (candidateStructs.count(pointerType->GetSingleWordInOperand(1)) == 0) continue;
|
||||
variableToStruct.emplace(inst.result_id(), pointerType->GetSingleWordInOperand(1));
|
||||
}
|
||||
if (variableToStruct.empty()) return plans;
|
||||
|
||||
// A block whose variable is used as anything but an access-chain base (loaded
|
||||
// whole, handed to a function) cannot be re-indexed; a partially re-indexed
|
||||
// block would address the wrong counters, so the whole block is refused.
|
||||
std::unordered_map<uint32_t, std::vector<Instruction*>> chainsByStruct;
|
||||
std::unordered_set<uint32_t> undoableStructs;
|
||||
for (const auto& [variableId, structId] : variableToStruct) {
|
||||
defUseMgr->ForEachUser(defUseMgr->GetDef(variableId), [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
case spv::Op::OpEntryPoint:
|
||||
return;
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
if (user->NumInOperands() >= 2 &&
|
||||
user->GetSingleWordInOperand(0) == variableId) {
|
||||
chainsByStruct[structId].push_back(user);
|
||||
return;
|
||||
}
|
||||
undoableStructs.insert(structId);
|
||||
return;
|
||||
default:
|
||||
undoableStructs.insert(structId);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const auto& [structId, structType] : candidateStructs) {
|
||||
if (undoableStructs.count(structId) != 0) continue;
|
||||
|
||||
BlockPlan plan;
|
||||
plan.structType = structType;
|
||||
const uint32_t memberCount = structType->NumInOperands();
|
||||
bool expressible = true;
|
||||
for (uint32_t member = 0; member < memberCount; ++member) {
|
||||
uint32_t byteOffset = 0;
|
||||
if (!TryGetMemberDecorationLiteral(context, structId, member,
|
||||
spv::Decoration::Offset, &byteOffset) ||
|
||||
byteOffset % kCounterBytes != 0u) {
|
||||
expressible = false;
|
||||
break;
|
||||
}
|
||||
MemberPlan memberPlan;
|
||||
uint32_t uintTypeId = 0;
|
||||
if (!DescribeMember(context, structType->GetSingleWordInOperand(member), &uintTypeId,
|
||||
&memberPlan)) {
|
||||
expressible = false;
|
||||
break;
|
||||
}
|
||||
if (plan.uintTypeId != 0 && plan.uintTypeId != uintTypeId) {
|
||||
expressible = false;
|
||||
break;
|
||||
}
|
||||
plan.uintTypeId = uintTypeId;
|
||||
memberPlan.elementOffset = byteOffset / kCounterBytes;
|
||||
const uint64_t end = static_cast<uint64_t>(memberPlan.elementOffset) +
|
||||
static_cast<uint64_t>(memberPlan.elementCount);
|
||||
if (end > kMaxCounterElements) {
|
||||
expressible = false;
|
||||
break;
|
||||
}
|
||||
if (end > plan.totalElements) plan.totalElements = static_cast<uint32_t>(end);
|
||||
plan.members.push_back(memberPlan);
|
||||
}
|
||||
if (!expressible || plan.members.empty() || plan.totalElements == 0) continue;
|
||||
// Already std430: leave it exactly as it is. This is the overwhelmingly
|
||||
// common answer and the reason the pass can be gated on a cheap probe.
|
||||
if (IsNaturallyPacked(plan.members)) continue;
|
||||
|
||||
// Every chain must be one of the two shapes the re-index understands: a
|
||||
// scalar counter reached by (variable, member) or an array element
|
||||
// reached by (variable, member, index). One that stops at the member, or
|
||||
// reaches deeper, is not a counter access this pass can move.
|
||||
const auto chains = chainsByStruct.find(structId);
|
||||
if (chains != chainsByStruct.end()) {
|
||||
for (Instruction* chain : chains->second) {
|
||||
const spvtools::opt::analysis::Constant* memberIndex =
|
||||
context->get_constant_mgr()->FindDeclaredConstant(
|
||||
chain->GetSingleWordInOperand(1));
|
||||
if (memberIndex == nullptr || memberIndex->AsIntConstant() == nullptr) {
|
||||
expressible = false;
|
||||
break;
|
||||
}
|
||||
const uint32_t member = memberIndex->AsIntConstant()->GetU32BitValue();
|
||||
if (member >= plan.members.size() ||
|
||||
chain->NumInOperands() != (plan.members[member].isArray ? 3u : 2u)) {
|
||||
expressible = false;
|
||||
break;
|
||||
}
|
||||
plan.chains.push_back(chain);
|
||||
}
|
||||
}
|
||||
if (!expressible) continue;
|
||||
|
||||
plans.push_back(std::move(plan));
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
|
||||
// The id of |value| as a constant of the same integer type as |likeId|.
|
||||
uint32_t ConstantLike(IRContext* context, uint32_t likeId, uint32_t value) {
|
||||
Instruction* likeDef = context->get_def_use_mgr()->GetDef(likeId);
|
||||
const spvtools::opt::analysis::Type* type =
|
||||
context->get_type_mgr()->GetType(likeDef->type_id());
|
||||
const spvtools::opt::analysis::Constant* constant =
|
||||
context->get_constant_mgr()->GetConstant(type, {value});
|
||||
return context->get_constant_mgr()->GetDefiningInstruction(constant)->result_id();
|
||||
}
|
||||
|
||||
Module::inst_iterator PositionOf(IRContext* context, const Instruction* target) {
|
||||
for (auto it = context->types_values_begin(); it != context->types_values_end(); ++it) {
|
||||
if (&*it == target) return it;
|
||||
}
|
||||
return context->types_values_end();
|
||||
}
|
||||
|
||||
// Whether |firstId| is declared before |secondId| in the types/constants section.
|
||||
bool DeclaredBefore(IRContext* context, uint32_t firstId, uint32_t secondId) {
|
||||
for (const Instruction& inst : context->module()->types_values()) {
|
||||
if (inst.result_id() == firstId) return true;
|
||||
if (inst.result_id() == secondId) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A fresh `uint[length]` with ArrayStride 4, spliced in immediately BEFORE the
|
||||
// block that will name it - SPIR-V has no forward references between types, so
|
||||
// appending it at the end of the section would make the module invalid. A
|
||||
// duplicate OpTypeArray is legal (SPIR-V 2.8 exempts aggregates from the
|
||||
// uniqueness rule, and so does spirv-val), so no search for an existing one is
|
||||
// needed; the LENGTH CONSTANT is not exempt, and if the module already declares
|
||||
// it after the block there is nowhere legal to put the array - the block is then
|
||||
// declined and keeps today's behaviour. Returns 0 for that.
|
||||
uint32_t CreateCounterArrayTypeBefore(IRContext* context, Instruction* structType,
|
||||
uint32_t uintTypeId, uint32_t length) {
|
||||
auto* constantMgr = context->get_constant_mgr();
|
||||
const spvtools::opt::analysis::Type* uintType = context->get_type_mgr()->GetType(uintTypeId);
|
||||
if (uintType == nullptr) return 0;
|
||||
const spvtools::opt::analysis::Constant* lengthConstant =
|
||||
constantMgr->GetConstant(uintType, {length});
|
||||
if (lengthConstant == nullptr) return 0;
|
||||
|
||||
Module::inst_iterator position = PositionOf(context, structType);
|
||||
if (position == context->types_values_end()) return 0;
|
||||
Instruction* lengthInst = constantMgr->GetDefiningInstruction(lengthConstant, 0, &position);
|
||||
if (lengthInst == nullptr) return 0;
|
||||
if (!DeclaredBefore(context, lengthInst->result_id(), structType->result_id())) return 0;
|
||||
|
||||
const uint32_t arrayTypeId = context->TakeNextId();
|
||||
if (arrayTypeId == 0) return 0;
|
||||
auto arrayType = MakeUnique<Instruction>(
|
||||
context, spv::Op::OpTypeArray, 0, arrayTypeId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {uintTypeId}},
|
||||
{SPV_OPERAND_TYPE_ID, {lengthInst->result_id()}}});
|
||||
Instruction* inserted = structType->InsertBefore(std::move(arrayType));
|
||||
context->AnalyzeDefUse(inserted);
|
||||
context->get_decoration_mgr()->AddDecorationVal(
|
||||
arrayTypeId, static_cast<uint32_t>(spv::Decoration::ArrayStride), kCounterBytes);
|
||||
return arrayTypeId;
|
||||
}
|
||||
|
||||
// Drops the annotations the collapsed struct no longer has a member for: every
|
||||
// OpMemberDecorate and OpMemberName past member 0, plus member 0's own Offset
|
||||
// (the caller re-adds it as 0). Member 0's OTHER decorations - Coherent,
|
||||
// Volatile, Restrict and the like, which describe how the counters are accessed
|
||||
// rather than where they sit - are deliberately kept.
|
||||
void StripMemberAnnotations(IRContext* context, uint32_t structId) {
|
||||
std::vector<Instruction*> doomed;
|
||||
for (Instruction* decoration :
|
||||
context->get_decoration_mgr()->GetDecorationsFor(structId, false)) {
|
||||
if (decoration->opcode() != spv::Op::OpMemberDecorate ||
|
||||
decoration->NumInOperands() < 3) {
|
||||
continue;
|
||||
}
|
||||
const bool pastMemberZero = decoration->GetSingleWordInOperand(1) != 0u;
|
||||
const bool isOffset =
|
||||
static_cast<spv::Decoration>(decoration->GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::Offset;
|
||||
if (pastMemberZero || isOffset) doomed.push_back(decoration);
|
||||
}
|
||||
for (Instruction& debug : context->module()->debugs2()) {
|
||||
if (debug.opcode() != spv::Op::OpMemberName || debug.NumInOperands() < 2) continue;
|
||||
if (debug.GetSingleWordInOperand(0) != structId) continue;
|
||||
if (debug.GetSingleWordInOperand(1) == 0u) continue; // member 0 keeps its name
|
||||
doomed.push_back(&debug);
|
||||
}
|
||||
for (Instruction* inst : doomed) context->KillInst(inst);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool FlattenAtomicCounterBlockPass::BinaryHasOffsetAtomicCounterBlock(const Vector<Uint32>& binary) {
|
||||
if (binary.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1,
|
||||
[](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
||||
binary.data(), binary.size());
|
||||
if (!context) {
|
||||
// Unparseable here means unusable downstream too; let the ordinary transpile
|
||||
// path produce the error rather than inventing a verdict from it.
|
||||
return false;
|
||||
}
|
||||
return !BuildPlans(context.get()).empty();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status FlattenAtomicCounterBlockPass::Process() {
|
||||
auto* irContext = context();
|
||||
const std::vector<BlockPlan> plans = BuildPlans(irContext);
|
||||
if (plans.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
bool modified = false;
|
||||
for (const BlockPlan& plan : plans) {
|
||||
const uint32_t structId = plan.structType->result_id();
|
||||
const uint32_t arrayTypeId = CreateCounterArrayTypeBefore(
|
||||
irContext, plan.structType, plan.uintTypeId, plan.totalElements);
|
||||
if (arrayTypeId == 0) {
|
||||
MGLOG_D("[spirv] atomic-counter block %%%u: no legal place for the flattened array "
|
||||
"type; leaving the block alone",
|
||||
structId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Re-index BEFORE the struct is collapsed, so the member index each chain
|
||||
// carries still names the member the plan was built from.
|
||||
for (Instruction* chain : plan.chains) {
|
||||
const uint32_t memberIndexId = chain->GetSingleWordInOperand(1);
|
||||
const uint32_t member = irContext->get_constant_mgr()
|
||||
->FindDeclaredConstant(memberIndexId)
|
||||
->AsIntConstant()
|
||||
->GetU32BitValue();
|
||||
const MemberPlan& memberPlan = plan.members[member];
|
||||
|
||||
std::vector<Operand> operands;
|
||||
operands.push_back(chain->GetInOperand(0));
|
||||
operands.push_back({SPV_OPERAND_TYPE_ID, {ConstantLike(irContext, memberIndexId, 0u)}});
|
||||
if (!memberPlan.isArray) {
|
||||
operands.push_back(
|
||||
{SPV_OPERAND_TYPE_ID,
|
||||
{ConstantLike(irContext, memberIndexId, memberPlan.elementOffset)}});
|
||||
} else {
|
||||
const uint32_t elementId = chain->GetSingleWordInOperand(2);
|
||||
uint32_t shiftedId = elementId;
|
||||
if (memberPlan.elementOffset != 0u) {
|
||||
const spvtools::opt::analysis::Constant* elementConstant =
|
||||
irContext->get_constant_mgr()->FindDeclaredConstant(elementId);
|
||||
if (elementConstant != nullptr && elementConstant->AsIntConstant() != nullptr) {
|
||||
shiftedId = ConstantLike(irContext, elementId,
|
||||
elementConstant->AsIntConstant()->GetU32BitValue() +
|
||||
memberPlan.elementOffset);
|
||||
} else {
|
||||
InstructionBuilder builder(
|
||||
irContext, chain,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
Instruction* elementDef = irContext->get_def_use_mgr()->GetDef(elementId);
|
||||
shiftedId = builder
|
||||
.AddBinaryOp(elementDef->type_id(), spv::Op::OpIAdd,
|
||||
elementId,
|
||||
ConstantLike(irContext, elementId,
|
||||
memberPlan.elementOffset))
|
||||
->result_id();
|
||||
}
|
||||
}
|
||||
operands.push_back({SPV_OPERAND_TYPE_ID, {shiftedId}});
|
||||
}
|
||||
chain->SetInOperands(std::move(operands));
|
||||
irContext->UpdateDefUse(chain);
|
||||
}
|
||||
|
||||
StripMemberAnnotations(irContext, structId);
|
||||
plan.structType->SetInOperands({{SPV_OPERAND_TYPE_ID, {arrayTypeId}}});
|
||||
irContext->UpdateDefUse(plan.structType);
|
||||
irContext->get_decoration_mgr()->AddMemberDecoration(
|
||||
structId, 0u, static_cast<uint32_t>(spv::Decoration::Offset), 0u);
|
||||
modified = true;
|
||||
MGLOG_D("[spirv] atomic-counter block %%%u: collapsed %zu offset member(s) into one "
|
||||
"%u-element array so std430 can express it",
|
||||
structId, plan.members.size(), plan.totalElements);
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken FlattenAtomicCounterBlockPass::CreateFlattenAtomicCounterBlockPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<FlattenAtomicCounterBlockPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,81 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// glslang's relaxed parse lowers every atomic_uint onto a synthesized storage block
|
||||
// named gl_AtomicCounterBlock_<GL binding>, and it PRESERVES the application's
|
||||
// `layout(offset = N)` as the member's SPIR-V Offset decoration. A block whose first
|
||||
// member sits at offset 8 is not expressible in std140 or std430 - both put member 0
|
||||
// at offset 0 - and GLSL ES has no member layout(offset=), so SPIRV-Cross refuses the
|
||||
// whole stage rather than emit something wrong:
|
||||
//
|
||||
// Push constant block cannot be expressed as neither std430 nor std140.
|
||||
// ES-targets do not support GL_ARB_enhanced_layouts.
|
||||
//
|
||||
// (The message says "push constant"; the variable is StorageClass Uniform. Do not
|
||||
// chase push constants.) The stage never reaches the driver, the program links short
|
||||
// of it with an EMPTY driver info log, and the dispatch no-ops while the frontend
|
||||
// still reports the link status glslang published - KHR-GL43.compute_shader.resources
|
||||
// -atomic-counter's non-zero-offset sibling, and the shape every conformance case
|
||||
// that declares `layout(binding = B, offset = N)` takes.
|
||||
//
|
||||
// The repair is to make the offsets DISAPPEAR rather than to move the buffer. Each
|
||||
// atomic-counter block is collapsed into ONE `uint` array covering the same byte
|
||||
// window, member 0 at offset 0 with ArrayStride 4 - a layout std430 expresses
|
||||
// exactly - and every access is re-indexed to the element that used to be at its
|
||||
// byte offset. `counters[k]` declared at offset 8 becomes element (2 + k) of the
|
||||
// array, i.e. byte 8 + 4k, which is the byte the application's counter buffer really
|
||||
// holds.
|
||||
//
|
||||
// Why not simply rebase the offsets to zero and bind the buffer 8 bytes in: because
|
||||
// glBindBufferRange's offset must be a multiple of
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which the target device reports as 32.
|
||||
// A byte offset of 8 cannot be expressed as a binding at all, so the correction has
|
||||
// to live in the shader's indexing, where it costs nothing.
|
||||
//
|
||||
// A block that is ALREADY laid out naturally - which is every shader that omits the
|
||||
// offset qualifier, and so very nearly all of them - is left byte-identical: the
|
||||
// detection below is the gate, and KHR-GL43.compute_shader.resource-atomic-counter
|
||||
// (offset 0) is the latch that the no-op case stays a no-op.
|
||||
//
|
||||
// Declines the whole block, leaving it untouched, on any shape it cannot re-index
|
||||
// exactly: a member that is not `uint` or an array of `uint` with stride 4, an offset
|
||||
// that is not a multiple of 4, a byte window past what GL_MAX_ATOMIC_COUNTER_BUFFER
|
||||
// _SIZE allows, a member with no Offset decoration at all, or an access chain that
|
||||
// stops at the member (a pointer handed to a function) rather than reaching a
|
||||
// counter.
|
||||
//
|
||||
// DirectGLES transpile path only. DirectVulkan takes the block's declared offsets
|
||||
// natively through an explicitly-laid-out descriptor and must see them unchanged.
|
||||
class FlattenAtomicCounterBlockPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-flatten-atomic-counter-block"; }
|
||||
Status Process() override;
|
||||
|
||||
// The detection half, on a serialized module: true when the module declares an
|
||||
// atomic-counter block whose member offsets are not already the natural std430
|
||||
// packing, i.e. whether this pass could change anything. One BuildModule, no
|
||||
// serialization, so the ~every shader that declares no counter (or declares one
|
||||
// at offset 0) pays no optimizer round trip.
|
||||
static bool BinaryHasOffsetAtomicCounterBlock(const Vector<Uint32>& binary);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateFlattenAtomicCounterBlockPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,610 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.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 "LegalizeStorageBlockArrayIndexPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/decoration_manager.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/function.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_builder.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/loop_descriptor.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::MakeUnique;
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// GL_MAX_*_SHADER_STORAGE_BLOCKS is 16 on the devices MobileGL targets, and
|
||||
// each lowered element costs one basic block per write, so a module claiming
|
||||
// more than this is refused rather than exploded. The largest array in the
|
||||
// conformance suite is 8.
|
||||
constexpr uint32_t kMaxLoweredArrayLength = 32;
|
||||
// One CFG-changing rewrite per round (analyses are dropped after each), so
|
||||
// the round budget bounds the work on a pathological module.
|
||||
constexpr int kMaxLoweringRounds = 256;
|
||||
// Full unrolling copies the body once per iteration, and nothing in the stock
|
||||
// unroller bounds that. Past this count the loop is left alone and the switch
|
||||
// lowering, whose cost is the array length rather than the trip count, takes
|
||||
// it instead. A loop over an array of storage blocks iterates at most
|
||||
// GL_MAX_*_SHADER_STORAGE_BLOCKS times in any shader that is not already
|
||||
// broken.
|
||||
constexpr size_t kMaxUnrolledIterations = 64;
|
||||
|
||||
struct DynamicIndexUse {
|
||||
Instruction* accessChain = nullptr;
|
||||
uint32_t arrayLength = 0;
|
||||
};
|
||||
|
||||
bool HasDecoration(IRContext* context, uint32_t id, spv::Decoration kind) {
|
||||
for (Instruction* decoration : context->get_decoration_mgr()->GetDecorationsFor(id, false)) {
|
||||
if (decoration->opcode() != spv::Op::OpDecorate ||
|
||||
decoration->NumInOperands() < 2) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::Decoration>(decoration->GetSingleWordInOperand(1)) == kind) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every variable that is an ARRAY OF STORAGE BLOCKS, mapped to that array's
|
||||
// length. Two spellings are accepted because both reach here depending on the
|
||||
// SPIR-V version glslang targets: StorageBuffer + Block (1.3, what MobileGL
|
||||
// asks for) and Uniform + BufferBlock (the pre-1.3 encoding). A UNIFORM block
|
||||
// array - Uniform + Block - is deliberately NOT collected; see the header.
|
||||
//
|
||||
// A length that is not a plain OpConstant (a spec constant) maps to 0: still
|
||||
// detected as illegal ESSL, never lowered.
|
||||
std::unordered_map<uint32_t, uint32_t> CollectStorageBlockArrays(IRContext* context) {
|
||||
std::unordered_map<uint32_t, uint32_t> blockArrays;
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
auto* constantMgr = context->get_constant_mgr();
|
||||
|
||||
for (Instruction& inst : context->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) {
|
||||
continue;
|
||||
}
|
||||
const auto storageClass =
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0));
|
||||
if (storageClass != spv::StorageClass::StorageBuffer &&
|
||||
storageClass != spv::StorageClass::Uniform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* pointerType = defUseMgr->GetDef(inst.type_id());
|
||||
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) {
|
||||
continue;
|
||||
}
|
||||
Instruction* pointeeType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
|
||||
if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray) {
|
||||
continue;
|
||||
}
|
||||
Instruction* elementType = defUseMgr->GetDef(pointeeType->GetSingleWordInOperand(0));
|
||||
if (elementType == nullptr || elementType->opcode() != spv::Op::OpTypeStruct) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool isStorageBlock =
|
||||
storageClass == spv::StorageClass::StorageBuffer
|
||||
? HasDecoration(context, elementType->result_id(), spv::Decoration::Block)
|
||||
: HasDecoration(context, elementType->result_id(),
|
||||
spv::Decoration::BufferBlock);
|
||||
if (!isStorageBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t arrayLength = 0;
|
||||
const spvtools::opt::analysis::Constant* lengthConstant =
|
||||
constantMgr->FindDeclaredConstant(pointeeType->GetSingleWordInOperand(1));
|
||||
if (lengthConstant != nullptr && lengthConstant->AsIntConstant() != nullptr) {
|
||||
arrayLength = lengthConstant->AsIntConstant()->GetU32BitValue();
|
||||
}
|
||||
blockArrays.emplace(inst.result_id(), arrayLength);
|
||||
}
|
||||
return blockArrays;
|
||||
}
|
||||
|
||||
// "Constant integral expression" in the ESSL sense: an OpConstant (or the
|
||||
// zero an OpConstantNull stands for). A spec constant is deliberately NOT
|
||||
// one - SPIRV-Cross prints it as an identifier, which is exactly what the
|
||||
// driver rejects.
|
||||
bool IsConstantIndex(IRContext* context, uint32_t indexId) {
|
||||
Instruction* def = context->get_def_use_mgr()->GetDef(indexId);
|
||||
return def != nullptr && (def->opcode() == spv::Op::OpConstant ||
|
||||
def->opcode() == spv::Op::OpConstantNull);
|
||||
}
|
||||
|
||||
// Access chains that index an array of storage blocks with a non-constant.
|
||||
// Only the FIRST index is considered: it is the one that selects the block,
|
||||
// and it is the only one ESSL constrains here. Indices inside the block - the
|
||||
// member selector and any array subscript below it - are legal however they
|
||||
// are computed, and chains rooted at another access chain are already inside
|
||||
// one element.
|
||||
std::vector<DynamicIndexUse> CollectDynamicIndexUses(IRContext* context) {
|
||||
std::vector<DynamicIndexUse> uses;
|
||||
const std::unordered_map<uint32_t, uint32_t> blockArrays =
|
||||
CollectStorageBlockArrays(context);
|
||||
if (blockArrays.empty()) {
|
||||
return uses;
|
||||
}
|
||||
|
||||
for (Function& function : *context->module()) {
|
||||
for (BasicBlock& block : function) {
|
||||
for (Instruction& inst : block) {
|
||||
if (inst.opcode() != spv::Op::OpAccessChain &&
|
||||
inst.opcode() != spv::Op::OpInBoundsAccessChain) {
|
||||
continue;
|
||||
}
|
||||
if (inst.NumInOperands() < 2) {
|
||||
continue;
|
||||
}
|
||||
const auto arrayIt = blockArrays.find(inst.GetSingleWordInOperand(0));
|
||||
if (arrayIt == blockArrays.end()) {
|
||||
continue;
|
||||
}
|
||||
if (IsConstantIndex(context, inst.GetSingleWordInOperand(1))) {
|
||||
continue;
|
||||
}
|
||||
uses.push_back({&inst, arrayIt->second});
|
||||
}
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
// The block index operand of |accessChain| replaced by the constant |element|,
|
||||
// built at the builder's insertion point. Every later index is copied through
|
||||
// unchanged: `arr[idx].data[j]` keeps its (legal) dynamic member subscript.
|
||||
Instruction* CloneChainWithConstantIndex(InstructionBuilder& builder, IRContext* context,
|
||||
Instruction* accessChain, uint32_t constantIndexId) {
|
||||
std::vector<Operand> operands;
|
||||
operands.reserve(accessChain->NumInOperands());
|
||||
for (uint32_t i = 0; i < accessChain->NumInOperands(); ++i) {
|
||||
if (i == 1) {
|
||||
operands.push_back({SPV_OPERAND_TYPE_ID, {constantIndexId}});
|
||||
} else {
|
||||
operands.push_back(accessChain->GetInOperand(i));
|
||||
}
|
||||
}
|
||||
return builder.AddInstruction(MakeUnique<Instruction>(context, accessChain->opcode(),
|
||||
accessChain->type_id(),
|
||||
context->TakeNextId(), operands));
|
||||
}
|
||||
|
||||
// The id of |element| as a constant of the same integer type as |indexId|.
|
||||
uint32_t ConstantLikeIndex(IRContext* context, uint32_t indexId, uint32_t element) {
|
||||
Instruction* indexDef = context->get_def_use_mgr()->GetDef(indexId);
|
||||
const spvtools::opt::analysis::Type* indexType =
|
||||
context->get_type_mgr()->GetType(indexDef->type_id());
|
||||
const spvtools::opt::analysis::Constant* constant =
|
||||
context->get_constant_mgr()->GetConstant(indexType, {element});
|
||||
return context->get_constant_mgr()->GetDefiningInstruction(constant)->result_id();
|
||||
}
|
||||
|
||||
// A 32-bit integer is the only index this pass lowers: OpSwitch matches its
|
||||
// literals against the selector's width, and every ESSL block-array index is
|
||||
// an int or uint.
|
||||
bool IsLowerableIndexType(IRContext* context, uint32_t indexId) {
|
||||
Instruction* indexDef = context->get_def_use_mgr()->GetDef(indexId);
|
||||
if (indexDef == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const spvtools::opt::analysis::Type* type =
|
||||
context->get_type_mgr()->GetType(indexDef->type_id());
|
||||
const spvtools::opt::analysis::Integer* integer =
|
||||
type != nullptr ? type->AsInteger() : nullptr;
|
||||
return integer != nullptr && integer->width() == 32;
|
||||
}
|
||||
|
||||
// The condition type OpSelect needs for |resultTypeId|. Before SPIR-V 1.4 a
|
||||
// scalar bool may not select between vectors, so a vector result needs a bool
|
||||
// vector of the same width - built by broadcasting the scalar comparison.
|
||||
// Anything that is neither scalar nor vector (a matrix or struct element) is
|
||||
// refused: pre-1.4 OpSelect cannot express it either.
|
||||
bool TryGetSelectConditionType(IRContext* context, uint32_t resultTypeId,
|
||||
uint32_t* conditionTypeId, uint32_t* dimension) {
|
||||
auto* typeMgr = context->get_type_mgr();
|
||||
const spvtools::opt::analysis::Type* resultType = typeMgr->GetType(resultTypeId);
|
||||
if (resultType == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
spvtools::opt::analysis::Bool boolType;
|
||||
if (resultType->AsVector() != nullptr) {
|
||||
const uint32_t count = resultType->AsVector()->element_count();
|
||||
spvtools::opt::analysis::Vector boolVector(&boolType, count);
|
||||
*conditionTypeId = typeMgr->GetTypeInstruction(&boolVector);
|
||||
*dimension = count;
|
||||
return *conditionTypeId != 0;
|
||||
}
|
||||
if (resultType->AsInteger() != nullptr || resultType->AsFloat() != nullptr ||
|
||||
resultType->AsBool() != nullptr) {
|
||||
*conditionTypeId = typeMgr->GetTypeInstruction(&boolType);
|
||||
*dimension = 1;
|
||||
return *conditionTypeId != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether fully unrolling |loop| is bounded work. The trip count is read the
|
||||
// same way the stock unroller reads it, so a loop this declines to measure is
|
||||
// one CanPerformUnroll would refuse anyway - the hint would be inert on it,
|
||||
// and the fallback lowering is what handles it. Requires the induction
|
||||
// variable to already be an OpPhi, which is why this runs after ssa-rewrite.
|
||||
bool IsBoundedUnrollCandidate(spvtools::opt::Loop* loop) {
|
||||
const spvtools::opt::BasicBlock* condition = loop->FindConditionBlock();
|
||||
if (condition == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* induction = loop->FindConditionVariable(condition);
|
||||
if (induction == nullptr || induction->opcode() != spv::Op::OpPhi) {
|
||||
return false;
|
||||
}
|
||||
size_t iterations = 0;
|
||||
if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations)) {
|
||||
return false;
|
||||
}
|
||||
return iterations <= kMaxUnrolledIterations;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing(
|
||||
const std::vector<uint32_t>& binary) {
|
||||
if (binary.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1,
|
||||
[](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
||||
binary.data(), binary.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
return !CollectDynamicIndexUses(context.get()).empty();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::Process() {
|
||||
return m_mode == Mode::MarkLoopsForUnroll ? MarkLoopsForUnroll() : LowerToConstantSwitch();
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::MarkLoopsForUnroll() {
|
||||
auto* irContext = context();
|
||||
const std::vector<DynamicIndexUse> uses = CollectDynamicIndexUses(irContext);
|
||||
if (uses.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
bool modified = false;
|
||||
for (const DynamicIndexUse& use : uses) {
|
||||
BasicBlock* block = irContext->get_instr_block(use.accessChain);
|
||||
if (block == nullptr) {
|
||||
continue;
|
||||
}
|
||||
Function* function = block->GetParent();
|
||||
if (function == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
spvtools::opt::LoopDescriptor* loops = irContext->GetLoopDescriptor(function);
|
||||
for (spvtools::opt::Loop* loop = (*loops)[block->id()]; loop != nullptr;
|
||||
loop = loop->GetParent()) {
|
||||
if (!IsBoundedUnrollCandidate(loop)) {
|
||||
continue;
|
||||
}
|
||||
Instruction* mergeInst = loop->GetHeaderBlock()->GetLoopMergeInst();
|
||||
// Only a bare `None` control is promoted, and only when no extra
|
||||
// literal (PartialCount, PeelCount, ...) follows it: the unroller
|
||||
// tests the control word for equality with Unroll, so ORing the bit
|
||||
// into a control that already carries something - DontUnroll above
|
||||
// all - would neither unroll nor mean what it says.
|
||||
if (mergeInst == nullptr || mergeInst->NumOperands() != 3 ||
|
||||
mergeInst->GetSingleWordOperand(2) !=
|
||||
static_cast<uint32_t>(spv::LoopControlMask::MaskNone)) {
|
||||
continue;
|
||||
}
|
||||
mergeInst->SetOperand(
|
||||
2, {static_cast<uint32_t>(spv::LoopControlMask::Unroll)});
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
MGLOG_D("[spirv] storage-block array index: marked enclosing loops for full unrolling");
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::LowerToConstantSwitch() {
|
||||
auto* irContext = context();
|
||||
|
||||
bool modified = false;
|
||||
// Access chains this pass has already refused, so a shape it cannot rewrite
|
||||
// exactly cannot spin the round loop.
|
||||
std::unordered_set<uint32_t> declined;
|
||||
|
||||
for (int round = 0; round < kMaxLoweringRounds; ++round) {
|
||||
const std::vector<DynamicIndexUse> uses = CollectDynamicIndexUses(irContext);
|
||||
bool progressed = false;
|
||||
|
||||
for (const DynamicIndexUse& use : uses) {
|
||||
if (declined.count(use.accessChain->result_id()) != 0) {
|
||||
continue;
|
||||
}
|
||||
const LoweringOutcome outcome = LowerOneChain(use.accessChain, use.arrayLength);
|
||||
if (outcome == LoweringOutcome::Declined) {
|
||||
declined.insert(use.accessChain->result_id());
|
||||
continue;
|
||||
}
|
||||
if (outcome == LoweringOutcome::Changed) {
|
||||
modified = true;
|
||||
progressed = true;
|
||||
// A store rewrite splits the block it sat in; every cached
|
||||
// analysis (and the instruction list this loop is walking) is
|
||||
// stale from here on. Recollect from scratch.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!progressed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerOneChain(Instruction* accessChain, uint32_t arrayLength) {
|
||||
auto* irContext = context();
|
||||
if (arrayLength == 0 || arrayLength > kMaxLoweredArrayLength) {
|
||||
MGLOG_D("[spirv] storage-block array index: array length %u is not lowerable",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
if (!IsLowerableIndexType(irContext, accessChain->GetSingleWordInOperand(1))) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
|
||||
std::vector<Instruction*> stores;
|
||||
std::vector<Instruction*> loads;
|
||||
bool unsupportedUse = false;
|
||||
irContext->get_def_use_mgr()->ForEachUser(accessChain, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
return;
|
||||
case spv::Op::OpStore:
|
||||
// Only as the pointer. A pointer stored as a *value* is not a storage
|
||||
// block write and cannot be redirected element-wise.
|
||||
if (user->GetSingleWordInOperand(0) == accessChain->result_id()) {
|
||||
stores.push_back(user);
|
||||
} else {
|
||||
unsupportedUse = true;
|
||||
}
|
||||
return;
|
||||
case spv::Op::OpLoad:
|
||||
// Memory operands (Volatile, Aligned, ...) would be dropped by the
|
||||
// per-element rebuild, so a load carrying any is refused instead.
|
||||
if (user->NumInOperands() == 1) {
|
||||
loads.push_back(user);
|
||||
} else {
|
||||
unsupportedUse = true;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
// A pointer passed to a function, copied, chained further, used by an
|
||||
// atomic, or measured by OpArrayLength cannot be resolved to one
|
||||
// element here.
|
||||
unsupportedUse = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (unsupportedUse) {
|
||||
MGLOG_D("[spirv] storage-block array index: chain %%%u has a use this pass cannot "
|
||||
"rewrite",
|
||||
accessChain->result_id());
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
|
||||
if (!loads.empty()) {
|
||||
return LowerLoad(accessChain, arrayLength, loads.front());
|
||||
}
|
||||
if (!stores.empty()) {
|
||||
return LowerStore(accessChain, arrayLength, stores.front());
|
||||
}
|
||||
|
||||
// No uses left: the chain itself is what detection is still seeing.
|
||||
irContext->KillInst(accessChain);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
// switch (idx) { case 0: arr[0]... = v; break; case 1: arr[1]... = v; break; ... }
|
||||
//
|
||||
// The block holding the store is split at the store, and the tail becomes the
|
||||
// switch's merge block, so whatever followed the store still runs exactly once on
|
||||
// every path. An index outside [0, length) reaches the default target, which is
|
||||
// the merge block: nothing is stored, which is what indexing a block array out of
|
||||
// range already meant.
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerStore(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* store) {
|
||||
auto* irContext = context();
|
||||
BasicBlock* block = irContext->get_instr_block(store);
|
||||
if (block == nullptr) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
// Splitting a loop header keeps the label - and so the back edge's target -
|
||||
// on the first half while the OpLoopMerge moves to the second, which is not
|
||||
// a loop any more. Refuse instead of producing that.
|
||||
if (block->GetLoopMergeInst() != nullptr) {
|
||||
MGLOG_D("[spirv] storage-block array index: store sits in a loop header, declining");
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
Function* function = block->GetParent();
|
||||
if (function == nullptr) {
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
|
||||
const uint32_t indexId = accessChain->GetSingleWordInOperand(1);
|
||||
const uint32_t valueId = store->GetSingleWordInOperand(1);
|
||||
std::vector<Operand> memoryOperands;
|
||||
for (uint32_t i = 2; i < store->NumInOperands(); ++i) {
|
||||
memoryOperands.push_back(store->GetInOperand(i));
|
||||
}
|
||||
|
||||
const uint32_t mergeLabelId = irContext->TakeNextId();
|
||||
block->SplitBasicBlock(irContext, mergeLabelId, BasicBlock::iterator(store));
|
||||
// |store| now heads the merge block; the per-element stores replace it.
|
||||
irContext->KillInst(store);
|
||||
|
||||
std::vector<std::pair<Operand::OperandData, uint32_t>> targets;
|
||||
targets.reserve(arrayLength);
|
||||
BasicBlock* insertAfter = block;
|
||||
for (uint32_t element = 0; element < arrayLength; ++element) {
|
||||
const uint32_t caseLabelId = irContext->TakeNextId();
|
||||
auto caseBlock = MakeUnique<BasicBlock>(MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpLabel, 0, caseLabelId, std::initializer_list<Operand>{}));
|
||||
caseBlock->SetParent(function);
|
||||
BasicBlock* casePtr = function->InsertBasicBlockAfter(std::move(caseBlock), insertAfter);
|
||||
// The builders below register what they add, but this label was built by
|
||||
// hand: without this the OpSwitch would name a target the def-use manager
|
||||
// has never seen, which a consistency-checking build calls out.
|
||||
irContext->AnalyzeDefUse(casePtr->GetLabelInst());
|
||||
irContext->set_instr_block(casePtr->GetLabelInst(), casePtr);
|
||||
|
||||
InstructionBuilder caseBuilder(
|
||||
irContext, casePtr,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
const uint32_t constantId = ConstantLikeIndex(irContext, indexId, element);
|
||||
Instruction* elementChain =
|
||||
CloneChainWithConstantIndex(caseBuilder, irContext, accessChain, constantId);
|
||||
|
||||
std::vector<Operand> storeOperands;
|
||||
storeOperands.push_back({SPV_OPERAND_TYPE_ID, {elementChain->result_id()}});
|
||||
storeOperands.push_back({SPV_OPERAND_TYPE_ID, {valueId}});
|
||||
for (const Operand& memoryOperand : memoryOperands) {
|
||||
storeOperands.push_back(memoryOperand);
|
||||
}
|
||||
caseBuilder.AddInstruction(
|
||||
MakeUnique<Instruction>(irContext, spv::Op::OpStore, 0, 0, storeOperands));
|
||||
caseBuilder.AddBranch(mergeLabelId);
|
||||
|
||||
targets.push_back({Operand::OperandData{element}, caseLabelId});
|
||||
insertAfter = casePtr;
|
||||
}
|
||||
|
||||
InstructionBuilder switchBuilder(
|
||||
irContext, block, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
switchBuilder.AddSwitch(indexId, mergeLabelId, targets, mergeLabelId);
|
||||
|
||||
if (irContext->get_def_use_mgr()->NumUsers(accessChain) == 0) {
|
||||
irContext->KillInst(accessChain);
|
||||
}
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
MGLOG_D("[spirv] storage-block array index: lowered a dynamic write to a %u-way switch",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
// A read needs no control flow: load every element through a constant index and
|
||||
// pick with OpSelect. Reading the elements the shader did not ask for is safe -
|
||||
// every one of them is a storage block this stage already declares, and an ES
|
||||
// driver bounds-checks a storage buffer read that lands outside what is bound.
|
||||
LegalizeStorageBlockArrayIndexPass::LoweringOutcome
|
||||
LegalizeStorageBlockArrayIndexPass::LowerLoad(Instruction* accessChain, uint32_t arrayLength,
|
||||
Instruction* load) {
|
||||
auto* irContext = context();
|
||||
uint32_t conditionTypeId = 0;
|
||||
uint32_t dimension = 0;
|
||||
if (!TryGetSelectConditionType(irContext, load->type_id(), &conditionTypeId, &dimension)) {
|
||||
MGLOG_D("[spirv] storage-block array index: element type is not selectable, declining");
|
||||
return LoweringOutcome::Declined;
|
||||
}
|
||||
const uint32_t boolTypeId = irContext->get_type_mgr()->GetBoolTypeId();
|
||||
const uint32_t indexId = accessChain->GetSingleWordInOperand(1);
|
||||
|
||||
InstructionBuilder builder(
|
||||
irContext, load, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
|
||||
uint32_t selectedId = 0;
|
||||
for (uint32_t element = 0; element < arrayLength; ++element) {
|
||||
const uint32_t constantId = ConstantLikeIndex(irContext, indexId, element);
|
||||
Instruction* elementChain =
|
||||
CloneChainWithConstantIndex(builder, irContext, accessChain, constantId);
|
||||
Instruction* elementLoad = builder.AddLoad(load->type_id(), elementChain->result_id());
|
||||
if (element == 0) {
|
||||
// Element 0 is the else-arm of the whole ladder, so an out-of-range
|
||||
// index reads it - an undefined element for an undefined index.
|
||||
selectedId = elementLoad->result_id();
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* isElement =
|
||||
builder.AddBinaryOp(boolTypeId, spv::Op::OpIEqual, indexId, constantId);
|
||||
uint32_t conditionId = isElement->result_id();
|
||||
if (dimension > 1) {
|
||||
std::vector<uint32_t> components(dimension, conditionId);
|
||||
conditionId = builder.AddCompositeConstruct(conditionTypeId, components)->result_id();
|
||||
}
|
||||
selectedId = builder
|
||||
.AddSelect(load->type_id(), conditionId, elementLoad->result_id(),
|
||||
selectedId)
|
||||
->result_id();
|
||||
}
|
||||
|
||||
irContext->ReplaceAllUsesWith(load->result_id(), selectedId);
|
||||
irContext->KillInst(load);
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
MGLOG_D("[spirv] storage-block array index: lowered a dynamic read to %u constant-indexed "
|
||||
"loads",
|
||||
arrayLength);
|
||||
return LoweringOutcome::Changed;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LegalizeStorageBlockArrayIndexPass::CreateMarkLoopsForUnrollPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<LegalizeStorageBlockArrayIndexPass>(Mode::MarkLoopsForUnroll));
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LegalizeStorageBlockArrayIndexPass::CreateLowerToConstantSwitchPass() {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<LegalizeStorageBlockArrayIndexPass>(Mode::LowerToConstantSwitch));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,120 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// GL 4.3 lets an ARRAY OF SHADER STORAGE BLOCKS be indexed with any
|
||||
// dynamically-uniform expression (GL 4.6 core / GLSL 4.30 4.1.9). GLSL ES keeps
|
||||
// the stricter ES 3.1 rule - the index must be a *constant integral expression* -
|
||||
// and the Qualcomm ES compiler enforces it to the letter:
|
||||
//
|
||||
// '[' : indexing into an SSBO array using a non-constant expression is not
|
||||
// permitted
|
||||
//
|
||||
// glslang keeps the whole array as ONE SPIR-V variable, so SPIRV-Cross prints
|
||||
// `layout(binding = N, std430) buffer Blk { ... } arr[4];` plus `arr[i]` verbatim
|
||||
// and the stage never compiles. The backend program then links nothing and every
|
||||
// draw or dispatch that uses it is a silent no-op, which reads back as "the buffer
|
||||
// was never written" rather than as an error - the frontend has already published
|
||||
// GL_LINK_STATUS = TRUE from glslang's own link.
|
||||
//
|
||||
// Verified on the device: an Adreno 830 ES probe with no MobileGL in the loop
|
||||
// rejects the non-constant subscript with AND without GL_EXT_gpu_shader5 (which
|
||||
// the driver does advertise), and accepts a constant one. So the ES 3.2
|
||||
// "dynamically uniform" relaxation is not a way out - every index really has to
|
||||
// become a compile-time constant.
|
||||
//
|
||||
// Two modes, used as two halves of one legalization in
|
||||
// ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl - the same shape, for
|
||||
// the same reasons, as LegalizeFragmentOutputIndexPass:
|
||||
//
|
||||
// MarkLoopsForUnroll - `for (int i = 0; i < 4; ++i) arr[i].x = ...` is the
|
||||
// common shape, and full unrolling turns its index into a literal at no cost
|
||||
// in emitted code. spirv-opt's CreateLoopUnrollPass only touches loops whose
|
||||
// OpLoopMerge carries the Unroll control, so this mode sets that hint on
|
||||
// exactly the loops that enclose an offending access chain, and only when
|
||||
// their trip count is known and small. Must run AFTER ssa-rewrite: both the
|
||||
// trip-count check and the unroller need the induction variable as an OpPhi.
|
||||
//
|
||||
// LowerToConstantSwitch - the fallback for a genuinely dynamic index
|
||||
// (uniform-sourced, which is what the CTS indirect-addressing and resource-max
|
||||
// cases use). A write through such a chain becomes an OpSwitch over the
|
||||
// array's range with one constant-indexed store per case; a read becomes one
|
||||
// constant-indexed load per element combined with OpSelect. This is what ANGLE
|
||||
// does for the same ES 3.1 rule.
|
||||
//
|
||||
// Storage blocks only. A UNIFORM block array is a different namespace with its own
|
||||
// (less strictly enforced) rule and no observed failure, so it is deliberately left
|
||||
// alone rather than lowered on speculation.
|
||||
//
|
||||
// DirectGLES transpile path only: the original module is legal for Vulkan, which
|
||||
// has no such restriction, and DirectVulkan must keep seeing the array as one
|
||||
// descriptor array.
|
||||
//
|
||||
// The pass DECLINES - leaving the module untouched rather than half-transforming
|
||||
// it - whenever it meets a shape it cannot rewrite exactly: a pointer handed to a
|
||||
// function or chained further, an atomic or an OpArrayLength through the chain, a
|
||||
// load carrying memory operands, a spec-constant array length, an index that is
|
||||
// not a 32-bit integer, or a store sitting in a loop header block (splitting there
|
||||
// would move the OpLoopMerge away from the back edge's target).
|
||||
class LegalizeStorageBlockArrayIndexPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
enum class Mode {
|
||||
MarkLoopsForUnroll,
|
||||
LowerToConstantSwitch,
|
||||
};
|
||||
|
||||
explicit LegalizeStorageBlockArrayIndexPass(Mode mode) : m_mode(mode) {}
|
||||
|
||||
const char* name() const override {
|
||||
return m_mode == Mode::MarkLoopsForUnroll
|
||||
? "mobilegl-mark-storage-block-array-index-loops"
|
||||
: "mobilegl-lower-storage-block-array-index";
|
||||
}
|
||||
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateMarkLoopsForUnrollPass();
|
||||
static spvtools::Optimizer::PassToken CreateLowerToConstantSwitchPass();
|
||||
|
||||
// The detection half, on a serialized module: true when an array of storage
|
||||
// blocks is indexed with anything but an OpConstant. Cheap enough to gate the
|
||||
// whole legalization on (one BuildModule, no serialization) and used again
|
||||
// after the folding chain to decide whether the fallback has to run at all.
|
||||
static bool BinaryHasDynamicStorageBlockArrayIndexing(const std::vector<uint32_t>& binary);
|
||||
|
||||
private:
|
||||
enum class LoweringOutcome {
|
||||
// The shape is not one this pass can rewrite exactly; the module keeps
|
||||
// the illegal chain rather than a half-transform of it.
|
||||
Declined,
|
||||
Changed,
|
||||
};
|
||||
|
||||
Status MarkLoopsForUnroll();
|
||||
Status LowerToConstantSwitch();
|
||||
|
||||
LoweringOutcome LowerOneChain(spvtools::opt::Instruction* accessChain, uint32_t arrayLength);
|
||||
LoweringOutcome LowerStore(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* store);
|
||||
LoweringOutcome LowerLoad(spvtools::opt::Instruction* accessChain, uint32_t arrayLength,
|
||||
spvtools::opt::Instruction* load);
|
||||
|
||||
Mode m_mode;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -49,10 +49,22 @@ namespace MobileGL {
|
||||
imageType->GetSingleWordInOperand(kSampledOperand) == 2u;
|
||||
}
|
||||
|
||||
// The other half of the 1D storage-image family: not arrayed. SPIRV-Cross emits
|
||||
// read and write through one of these correctly, and an ATOMIC through one
|
||||
// incorrectly (see the header), so this predicate only ever decides anything
|
||||
// together with the atomic probe below.
|
||||
bool Is1DNonArrayedStorageImageType(const Instruction* imageType) {
|
||||
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
|
||||
imageType->NumInOperands() > kSampledOperand &&
|
||||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(kDimOperand)) == spv::Dim::Dim1D &&
|
||||
imageType->GetSingleWordInOperand(kArrayedOperand) == 0u &&
|
||||
imageType->GetSingleWordInOperand(kSampledOperand) == 2u;
|
||||
}
|
||||
|
||||
// Any Dim1D image, sampled or storage. Used only to decide whether the Image1D
|
||||
// capability is still needed - deliberately wider than the rewrite's own
|
||||
// predicate, so a module that also holds a non-arrayed 1D image (which this pass
|
||||
// leaves to SPIRV-Cross) keeps the capability it still requires.
|
||||
// predicate, so a module that also holds a 1D image this pass left alone keeps the
|
||||
// capability it still requires.
|
||||
bool IsDim1DImageType(const Instruction* imageType) {
|
||||
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
|
||||
imageType->NumInOperands() > kSampledOperand &&
|
||||
@@ -110,6 +122,60 @@ namespace MobileGL {
|
||||
return opcode == spv::Op::OpImageQuerySize || opcode == spv::Op::OpImageQuerySizeLod ||
|
||||
opcode == spv::Op::OpImageQueryLevels || opcode == spv::Op::OpImageQuerySamples;
|
||||
}
|
||||
|
||||
// Which 1D storage images this module is to be rewritten for. Arrayed ones always;
|
||||
// non-arrayed ones only when an atomic reaches one, because that is the only shape
|
||||
// SPIRV-Cross gets wrong for them and taking over a path it gets right would be a
|
||||
// regression looking for somewhere to happen.
|
||||
struct LoweringScope {
|
||||
bool arrayed = false;
|
||||
bool nonArrayed = false;
|
||||
|
||||
bool Any() const { return arrayed || nonArrayed; }
|
||||
bool Covers(const Instruction* imageType) const {
|
||||
return (arrayed && Is1DArrayStorageImageType(imageType)) ||
|
||||
(nonArrayed && Is1DNonArrayedStorageImageType(imageType));
|
||||
}
|
||||
};
|
||||
|
||||
// OpImageTexelPointer is the operand path of every imageAtomic*; nothing else in a
|
||||
// GLSL-derived module produces one.
|
||||
bool PerformsAtomicOnNonArrayed1DImage(IRContext* context) {
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
if (instruction.opcode() != spv::Op::OpImageTexelPointer ||
|
||||
instruction.NumInOperands() < 1) {
|
||||
continue;
|
||||
}
|
||||
if (Is1DNonArrayedStorageImageType(
|
||||
ResolveImageType(context, instruction.GetSingleWordInOperand(0)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// One walk of the type table, then - and only when the module declares a
|
||||
// non-arrayed 1D storage image at all - one walk of the code. Every other shader
|
||||
// pays the type walk and nothing else.
|
||||
LoweringScope ResolveLoweringScope(IRContext* context) {
|
||||
LoweringScope scope;
|
||||
bool hasNonArrayed = false;
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (Is1DArrayStorageImageType(&type)) {
|
||||
scope.arrayed = true;
|
||||
} else if (Is1DNonArrayedStorageImageType(&type)) {
|
||||
hasNonArrayed = true;
|
||||
}
|
||||
}
|
||||
if (hasNonArrayed) {
|
||||
scope.nonArrayed = PerformsAtomicOnNonArrayed1DImage(context);
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Lower1DArrayImagesPass::ModuleTraits Lower1DArrayImagesPass::InspectBinary(const Vector<Uint32>& binary) {
|
||||
@@ -126,15 +192,11 @@ namespace MobileGL {
|
||||
|
||||
// The type table settles it for the cheap half, and it is the half almost every
|
||||
// shader takes: no such type declared, nothing to inspect further.
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (Is1DArrayStorageImageType(&type)) {
|
||||
traits.declaresImage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!traits.declaresImage) {
|
||||
const LoweringScope scope = ResolveLoweringScope(context.get());
|
||||
if (!scope.Any()) {
|
||||
return traits;
|
||||
}
|
||||
traits.declaresImage = true;
|
||||
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
@@ -142,7 +204,7 @@ namespace MobileGL {
|
||||
if (!QueriesImageSize(instruction.opcode()) || instruction.NumInOperands() < 1) {
|
||||
continue;
|
||||
}
|
||||
if (Is1DArrayStorageImageType(
|
||||
if (scope.Covers(
|
||||
ResolveImageType(context.get(), instruction.GetSingleWordInOperand(0)))) {
|
||||
traits.queriesImageSize = true;
|
||||
return traits;
|
||||
@@ -160,27 +222,21 @@ namespace MobileGL {
|
||||
|
||||
// Nothing to do unless the module actually declares one. Every other shader pays
|
||||
// one walk of the type table and is handed back unchanged.
|
||||
bool hasType = false;
|
||||
for (const Instruction& type : irContext->types_values()) {
|
||||
if (Is1DArrayStorageImageType(&type)) {
|
||||
hasType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasType) {
|
||||
const LoweringScope scope = ResolveLoweringScope(irContext);
|
||||
if (!scope.Any()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// The same refusal the caller makes, restated here so the pass is safe wherever
|
||||
// it is registered. Rewriting the type while leaving an OpImageQuerySize on it
|
||||
// produces a query whose result type has one component too few - an invalid
|
||||
// module - and there is no correct two-component size to substitute, because the
|
||||
// ES texture genuinely has a height the GL one does not.
|
||||
// module - and there is no correct narrower size to substitute, because the ES
|
||||
// texture genuinely has a height the GL one does not.
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
if (QueriesImageSize(instruction.opcode()) && instruction.NumInOperands() >= 1 &&
|
||||
Is1DArrayStorageImageType(
|
||||
scope.Covers(
|
||||
ResolveImageType(irContext, instruction.GetSingleWordInOperand(0)))) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
@@ -188,9 +244,12 @@ namespace MobileGL {
|
||||
}
|
||||
}
|
||||
|
||||
// (u, layer) -> (u, 0, layer). The height the ES 2D array carries is 1, so Y is
|
||||
// always 0 and the layer has to move from the second component to the third; a
|
||||
// plain widening that appended the 0 would read layer 0 of every access instead.
|
||||
// Arrayed: (u, layer) -> (u, 0, layer). The height the ES 2D array carries is 1,
|
||||
// so Y is always 0 and the layer has to move from the second component to the
|
||||
// third; a plain widening that appended the 0 would read layer 0 of every access
|
||||
// instead. Non-arrayed: u -> (u, 0), which is exactly what SPIRV-Cross itself
|
||||
// writes for the operations it does widen - reproduced here so read, write and
|
||||
// atomic all come out of one place.
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
@@ -199,38 +258,44 @@ namespace MobileGL {
|
||||
instruction.NumInOperands() <= coordinateOperand) {
|
||||
continue;
|
||||
}
|
||||
if (!Is1DArrayStorageImageType(
|
||||
ResolveImageType(irContext, instruction.GetSingleWordInOperand(0)))) {
|
||||
const Instruction* imageType =
|
||||
ResolveImageType(irContext, instruction.GetSingleWordInOperand(0));
|
||||
if (!scope.Covers(imageType)) {
|
||||
continue;
|
||||
}
|
||||
const bool arrayed = Is1DArrayStorageImageType(imageType);
|
||||
|
||||
const uint32_t coordinateId = instruction.GetSingleWordInOperand(coordinateOperand);
|
||||
|
||||
// Built from the COORDINATE's own component type rather than a
|
||||
// hardcoded signed int. GLSL only ever spells these ivec2, but SPIR-V
|
||||
// permits an unsigned coordinate, and extracting a uint component
|
||||
// into an int result is an invalid module rather than a wrong answer -
|
||||
// the kind of defect that reaches a driver as "compiles here, not
|
||||
// there".
|
||||
// hardcoded signed int. GLSL only ever spells these int/ivec2, but
|
||||
// SPIR-V permits an unsigned coordinate, and extracting a uint
|
||||
// component into an int result is an invalid module rather than a
|
||||
// wrong answer - the kind of defect that reaches a driver as "compiles
|
||||
// here, not there".
|
||||
Instruction* coordinateDef = irContext->get_def_use_mgr()->GetDef(coordinateId);
|
||||
if (coordinateDef == nullptr) return Status::Failure;
|
||||
const auto* coordinateType = typeMgr->GetType(coordinateDef->type_id());
|
||||
const auto* coordinateVector = coordinateType != nullptr ? coordinateType->AsVector()
|
||||
: nullptr;
|
||||
if (coordinateVector == nullptr || coordinateVector->element_count() != 2) {
|
||||
if (coordinateType == nullptr) return Status::Failure;
|
||||
// Arrayed coordinates are the two-component (u, layer); non-arrayed
|
||||
// ones are the bare scalar u. Anything else is a shape this pass does
|
||||
// not translate, and declining leaves the module byte for byte.
|
||||
const auto* coordinateVector = arrayed ? coordinateType->AsVector() : nullptr;
|
||||
if (arrayed && (coordinateVector == nullptr || coordinateVector->element_count() != 2)) {
|
||||
return Status::Failure;
|
||||
}
|
||||
const auto* component = coordinateVector->element_type();
|
||||
const auto* component =
|
||||
arrayed ? coordinateVector->element_type() : coordinateType;
|
||||
const auto* componentInteger = component != nullptr ? component->AsInteger() : nullptr;
|
||||
if (componentInteger == nullptr) return Status::Failure;
|
||||
|
||||
spvtools::opt::analysis::Vector widenedVector(component, 3);
|
||||
const uint32_t int3TypeId = typeMgr->GetTypeInstruction(&widenedVector);
|
||||
spvtools::opt::analysis::Vector widenedVector(component, arrayed ? 3 : 2);
|
||||
const uint32_t widenedTypeId = typeMgr->GetTypeInstruction(&widenedVector);
|
||||
const uint32_t intTypeId = typeMgr->GetTypeInstruction(component);
|
||||
const uint32_t zeroId = componentInteger->IsSigned()
|
||||
? constantMgr->GetSIntConstId(0)
|
||||
: constantMgr->GetUIntConstId(0);
|
||||
if (int3TypeId == 0 || intTypeId == 0 || zeroId == 0) {
|
||||
if (widenedTypeId == 0 || intTypeId == 0 || zeroId == 0) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
@@ -238,15 +303,20 @@ namespace MobileGL {
|
||||
irContext, &instruction,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
|
||||
Instruction* u =
|
||||
builder.AddCompositeExtract(intTypeId, coordinateId, {0});
|
||||
Instruction* layer =
|
||||
builder.AddCompositeExtract(intTypeId, coordinateId, {1});
|
||||
if (u == nullptr || layer == nullptr) {
|
||||
return Status::Failure;
|
||||
Instruction* widened = nullptr;
|
||||
if (arrayed) {
|
||||
Instruction* u =
|
||||
builder.AddCompositeExtract(intTypeId, coordinateId, {0});
|
||||
Instruction* layer =
|
||||
builder.AddCompositeExtract(intTypeId, coordinateId, {1});
|
||||
if (u == nullptr || layer == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
widened = builder.AddCompositeConstruct(
|
||||
widenedTypeId, {u->result_id(), zeroId, layer->result_id()});
|
||||
} else {
|
||||
widened = builder.AddCompositeConstruct(widenedTypeId, {coordinateId, zeroId});
|
||||
}
|
||||
Instruction* widened = builder.AddCompositeConstruct(
|
||||
int3TypeId, {u->result_id(), zeroId, layer->result_id()});
|
||||
if (widened == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
@@ -256,21 +326,22 @@ namespace MobileGL {
|
||||
}
|
||||
}
|
||||
|
||||
// Only now, with no access still spelling the 1D-array coordinate, does the type
|
||||
// become the 2D array one. Arrayed stays 1: this is a 2D ARRAY image, which is
|
||||
// what the texture was stored as.
|
||||
// Only now, with no access still spelling a 1D coordinate, does the type become
|
||||
// the 2D one. Arrayed is left exactly as it was - a 1D array becomes a 2D ARRAY
|
||||
// image, which is what the texture was stored as, and a non-arrayed 1D becomes the
|
||||
// plain 2D image MobileGL stores a GL_TEXTURE_1D in (height 1).
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (Is1DArrayStorageImageType(&type)) {
|
||||
if (scope.Covers(&type)) {
|
||||
type.SetInOperand(kDimOperand, {static_cast<uint32_t>(spv::Dim::Dim2D)});
|
||||
}
|
||||
}
|
||||
|
||||
// Image1D describes the types just rewritten - but only drop it if no 1D image
|
||||
// type is left at all. A module may hold a non-arrayed 1D storage image, which
|
||||
// this pass deliberately leaves to SPIRV-Cross, and that one still needs the
|
||||
// capability. Shader is always declared by any module reaching here, so restating
|
||||
// it keeps the instruction valid without leaving a capability a consumer could
|
||||
// key off.
|
||||
// type is left at all. A module may hold a 1D image this pass left alone (a
|
||||
// SAMPLED one always, and a non-arrayed storage one whenever no atomic reaches
|
||||
// it), and that one still needs the capability. Shader is always declared by any
|
||||
// module reaching here, so restating it keeps the instruction valid without
|
||||
// leaving a capability a consumer could key off.
|
||||
bool anyDim1DLeft = false;
|
||||
for (const Instruction& type : irContext->types_values()) {
|
||||
if (IsDim1DImageType(&type)) {
|
||||
|
||||
@@ -46,13 +46,30 @@ namespace MobileGL {
|
||||
// through it has its coordinate widened from (u, layer) to (u, 0, layer). SPIRV-Cross
|
||||
// is then looking at an ordinary 2D array image and its 1D path never fires.
|
||||
//
|
||||
// The NON-arrayed 1D storage image is handled too, but only in one shape. SPIRV-Cross
|
||||
// applies the widening above in OpImageRead (spirv_glsl.cpp) and in OpImageWrite - and
|
||||
// NOT in OpImageTexelPointer, which is the operand path every imageAtomic* goes
|
||||
// through. So `imageAtomicAdd(g_image_1d, coord.x, 2)` comes out with a SCALAR
|
||||
// coordinate against a variable it declared `iimage2D`, and the ES compiler answers
|
||||
// "'imageAtomicAdd' : no matching overloaded function found" - losing the whole stage
|
||||
// and with it every other image in it, which is how
|
||||
// KHR-GL4x.shader_image_load_store.basic-allTargets-atomic lost a seven-image fragment
|
||||
// shader over one of them.
|
||||
//
|
||||
// That case is lowered here for the same reason as the arrayed one: the type becomes
|
||||
// Dim2D and every coordinate is widened from u to (u, 0) in the module, so SPIRV-Cross
|
||||
// has no 1D image left to emulate and read, write and atomic are all spelled by one
|
||||
// piece of code. It is gated on the module ACTUALLY performing an image atomic on such
|
||||
// an image, so a shader that only loads and stores through a 1D image keeps taking
|
||||
// SPIRV-Cross's own (correct) emission byte for byte and this pass cannot regress it.
|
||||
//
|
||||
// Deliberately narrow, on three axes:
|
||||
//
|
||||
// * STORAGE images only (Sampled == 2). Sampled images reach SPIRV-Cross's sampler
|
||||
// path, which is correct today; rewriting them would replace working emission
|
||||
// with our own for no reason.
|
||||
// * ARRAYED only. A non-arrayed 1D storage image is emitted correctly by the same
|
||||
// SPIRV-Cross code, and is left to it.
|
||||
// * ARRAYED always; NON-arrayed only when the module holds an OpImageTexelPointer
|
||||
// into one, i.e. only when SPIRV-Cross's own emission is already broken for it.
|
||||
// * ESSL only. Vulkan has VK_IMAGE_VIEW_TYPE_1D_ARRAY natively and Magma binds it
|
||||
// directly, so the module must reach that backend unchanged.
|
||||
//
|
||||
@@ -81,13 +98,15 @@ namespace MobileGL {
|
||||
// cost one module parse and no optimizer run at all - not one parse to ask about
|
||||
// size queries and a second inside an Optimizer that then early-outs.
|
||||
struct ModuleTraits {
|
||||
// The module declares a 1D-array storage image, i.e. there is anything to do.
|
||||
// The module declares an image this pass would rewrite - a 1D-array storage
|
||||
// image, or a non-arrayed 1D storage image the module performs an atomic on -
|
||||
// i.e. there is anything to do.
|
||||
bool declaresImage = false;
|
||||
// ...and queries its size, which is the shape this pass refuses to translate:
|
||||
// afterwards the image is a 2D array, so the query yields three components
|
||||
// where the shader consumes two, and there is no correct two-component answer
|
||||
// to substitute. The caller leaves such a module alone rather than half
|
||||
// rewriting it.
|
||||
// afterwards the image is a 2D (array) one, so the query yields a component
|
||||
// more than the shader consumes, and there is no correct narrower answer to
|
||||
// substitute. The caller leaves such a module alone rather than half rewriting
|
||||
// it.
|
||||
bool queriesImageSize = false;
|
||||
};
|
||||
static ModuleTraits InspectBinary(const Vector<Uint32>& binary);
|
||||
|
||||
@@ -48,13 +48,16 @@ namespace MobileGL {
|
||||
return SPVC_BASETYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
// Record one flattened leaf uniform of the global UBO into the metadata maps.
|
||||
static void RecordGlobalUboLeaf(const SpvReflectBlockVariable& member, const String& name,
|
||||
Uint32 offsetInUBO, SpvcMetadata& metadata) {
|
||||
// Write one metadata entry. `name` is already the glslang-reflection spelling the
|
||||
// GL uniform locations are keyed on, and `arrayStride`/`sizeInBytes` describe the
|
||||
// entry rather than the whole declaration (they differ for a sub-array of an
|
||||
// array-of-arrays - see RecordGlobalUboLeaf).
|
||||
static void RecordGlobalUboLeafEntry(const SpvReflectBlockVariable& member, const String& name,
|
||||
Uint32 offsetInUBO, Uint32 arrayStride, SizeT sizeInBytes,
|
||||
SpvcMetadata& metadata) {
|
||||
metadata.plainUniformOffsetsInUBO[name] = offsetInUBO;
|
||||
metadata.plainUniformMemberSizesInBytes[name] = member.size;
|
||||
metadata.plainUniformArrayStridesInUBO[name] =
|
||||
member.array.dims_count > 0 ? member.array.stride : 0;
|
||||
metadata.plainUniformMemberSizesInBytes[name] = sizeInBytes;
|
||||
metadata.plainUniformArrayStridesInUBO[name] = arrayStride;
|
||||
|
||||
Uint32 vectorSize = member.numeric.vector.component_count;
|
||||
if (vectorSize == 0) vectorSize = 1;
|
||||
@@ -67,6 +70,70 @@ namespace MobileGL {
|
||||
};
|
||||
}
|
||||
|
||||
// Record one flattened leaf uniform of the global UBO into the metadata maps.
|
||||
//
|
||||
// SPIRV-Reflect keeps `float u[2][3]` as ONE leaf carrying every dimension in
|
||||
// array.dims[] and the INNERMOST element stride in array.stride (the outer
|
||||
// ArrayStride decoration is overwritten as ParseType recurses into the element
|
||||
// type, which is also why array.size == product(dims) * stride). glslang's
|
||||
// reflection - which owns the names GL uniform locations are keyed on - stops at
|
||||
// "reflection granularity" instead (reflection.cpp: !type.isArrayOfArrays()), so
|
||||
// the same declaration arrives on the GL side as "u[0]" and "u[1]", each a
|
||||
// `float[3]` holding its own three locations.
|
||||
//
|
||||
// Emitting a single "u" leaf here therefore only ever routes the FIRST sub-array:
|
||||
// the routing loop stops as soon as a location belongs to a different uniform, and
|
||||
// every element from "u[1][0]" on finds no offset and falls through to the fallback
|
||||
// scratch storage at the tail of the shadow - bytes the GPU never reads, so those
|
||||
// glUniform writes are silently lost
|
||||
// (KHR-GLES31.explicit_uniform_location.uniform-loc-arrays-of-arrays). Expand every
|
||||
// dimension but the last, exactly as glslang does, and give each sub-array its own
|
||||
// byte offset.
|
||||
static void RecordGlobalUboLeaf(const SpvReflectBlockVariable& member, const String& name,
|
||||
Uint32 offsetInUBO, SpvcMetadata& metadata) {
|
||||
const Uint32 arrayStride = member.array.dims_count > 0 ? member.array.stride : 0;
|
||||
const Bool isArrayOfArrays = member.array.dims_count > 1 && arrayStride > 0;
|
||||
if (!isArrayOfArrays) {
|
||||
RecordGlobalUboLeafEntry(member, name, offsetInUBO, arrayStride, member.size, metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extent 0 is SPIRV-Reflect's OpTypeRuntimeArray marker, which a plain uniform
|
||||
// cannot be - but it must not be expanded (or divided by) if it ever appears.
|
||||
Uint32 subArrayCount = 1;
|
||||
for (Uint32 dim = 0; dim + 1 < member.array.dims_count; ++dim) {
|
||||
const Uint32 extent = member.array.dims[dim];
|
||||
if (extent == 0) {
|
||||
MGLOG_W_ONCE("RecordGlobalUboLeaf: multi-dimensional uniform '%s' has a non-constant "
|
||||
"dimension, recording the base entry only",
|
||||
name.c_str());
|
||||
RecordGlobalUboLeafEntry(member, name, offsetInUBO, arrayStride, member.size, metadata);
|
||||
return;
|
||||
}
|
||||
subArrayCount *= extent;
|
||||
}
|
||||
const Uint32 innerExtent = member.array.dims[member.array.dims_count - 1] > 0
|
||||
? member.array.dims[member.array.dims_count - 1]
|
||||
: 1;
|
||||
const Uint32 subArrayStride = innerExtent * arrayStride;
|
||||
|
||||
// Row-major odometer over dims[0 .. dims_count-2]: the last dimension varies
|
||||
// fastest, so `subArray` counts sub-arrays in exactly memory order.
|
||||
Vector<Uint32> indices(member.array.dims_count - 1, 0);
|
||||
for (Uint32 subArray = 0; subArray < subArrayCount; ++subArray) {
|
||||
String elementName = name;
|
||||
for (const Uint32 index : indices) {
|
||||
elementName += "[" + std::to_string(index) + "]";
|
||||
}
|
||||
RecordGlobalUboLeafEntry(member, elementName, offsetInUBO + subArray * subArrayStride,
|
||||
arrayStride, subArrayStride, metadata);
|
||||
for (SizeT dim = indices.size(); dim-- > 0;) {
|
||||
if (++indices[dim] < member.array.dims[dim]) break;
|
||||
indices[dim] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten a (possibly nested struct / struct array) member of the global UBO
|
||||
// into leaf entries named the way glslang reflection names plain uniforms:
|
||||
// "s[0].b[1].b" for `uniform S s[2]` with `struct T { vec2 b[2]; }` members.
|
||||
|
||||
@@ -564,8 +564,20 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// compile-time constants (GLSL_ES true, VULKAN_SEMANTICS false);
|
||||
// * the SPIR-V validation switch, as in L1.
|
||||
//
|
||||
// Unconditional passes (StripUboMemberRelaxedPrecision, LowerRectImages,
|
||||
// Lower1DArrayImages) take no input but the module and so need no key material.
|
||||
// Unconditional passes take no input but the module and so need no key material:
|
||||
// StripUboMemberRelaxedPrecision, LowerRectImages, Lower1DArrayImages,
|
||||
// LegalizeStorageBlockArrayIndexing and FlattenAtomicCounterBlockOffsets. Each self-gates
|
||||
// on the module's own content and is armed by nothing, so the SPIR-V already in this key
|
||||
// covers them completely.
|
||||
//
|
||||
// THE TEST FOR THAT CLAIM IS NOT THE SIGNATURE. LowerViewportIndexForEssl is equally
|
||||
// module-only to look at, yet SupportsViewportArray is in this key because that bit ARMS
|
||||
// it at the call site. So a new pass needs BOTH checks - what it takes, and what decides
|
||||
// whether it runs - before "no key material" is a conclusion rather than an assumption.
|
||||
// Note also where an application-authored value can hide: the atomic-counter
|
||||
// layout(offset = N) qualifiers that FlattenAtomicCounterBlockOffsets rewrites are not a
|
||||
// separate input at all, because glslang already baked them into the module as member
|
||||
// Offset decorations - i.e. into the key's biggest field.
|
||||
struct EsslTranslationResult {
|
||||
String essl;
|
||||
// Which interface blocks FlattenXfbInterfaceBlocksForEssl actually rewrote
|
||||
|
||||
Reference in New Issue
Block a user