mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (DirectGLES): lower gl_ClipDistance for Adreno's ESSL compiler - shadow the builtin in a Private array with constant-index flushes before EmitVertex/return, loop-copy gl_in clip distances through dynamic indices (whole-array reads segfault the Qualcomm compiler, constant-index element reads miscompile), strip the SPIRV-Cross redeclaration Adreno rejects, and split const struct-array LUT initializers so they stay dynamically indexable; quirk-gated to Qualcomm with MOBILEGL_QUIRK_CLIP_DISTANCE override
This commit is contained in:
@@ -195,6 +195,8 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
@@ -80,6 +80,11 @@ namespace MobileGL::MG_Config {
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
|
||||
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
|
||||
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
|
||||
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
|
||||
QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||
|
||||
@@ -135,6 +135,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
|
||||
@@ -3291,6 +3291,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
||||
|
||||
// Adreno's ESSL compiler mishandles gl_ClipDistance (rejects redeclarations,
|
||||
// miscompiles non-constant-index writes and constant-index gl_in element reads,
|
||||
// crashes on whole-array gl_in reads) and cannot dynamically index the global
|
||||
// const struct[] LUTs SPIRV-Cross likes to emit. Gate the workarounds to
|
||||
// Qualcomm; MOBILEGL_QUIRK_CLIP_DISTANCE overrides the device detection.
|
||||
const MG_Config::QuirkOverride clipDistanceQuirkOverride =
|
||||
MG_Config::Features.ClipDistanceQuirk;
|
||||
const Bool applyClipDistanceQuirk =
|
||||
clipDistanceQuirkOverride == MG_Config::QuirkOverride::ForceOn ||
|
||||
(clipDistanceQuirkOverride == MG_Config::QuirkOverride::Auto &&
|
||||
pActiveBackendObject &&
|
||||
pActiveBackendObject->GetDynamicParameters().GpuVendor == GpuVendorKind::Qualcomm);
|
||||
|
||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||
auto& shader = attachedShaders[index];
|
||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
||||
@@ -3339,6 +3352,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_W("FoldConstOffsetFor1DFetchForEssl failed, continuing with unfolded SPIR-V.");
|
||||
}
|
||||
|
||||
// Adreno quirk: shadow gl_ClipDistance in Private arrays so the transpiled
|
||||
// ESSL only writes the builtin with literal constant indices (flushed before
|
||||
// EmitVertex/return) and only reads gl_in clip distances through dynamic loop
|
||||
// indices - the shapes this driver compiles correctly. Must run after the
|
||||
// access-chain clamp above so the flush indices stay literal constants.
|
||||
Vector<unsigned int> clipDistanceSpirv;
|
||||
if (applyClipDistanceQuirk &&
|
||||
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_GEOMETRY_SHADER)) {
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerClipDistanceForEssl(
|
||||
*effectiveSpirv, clipDistanceSpirv) &&
|
||||
!clipDistanceSpirv.empty()) {
|
||||
effectiveSpirv = &clipDistanceSpirv;
|
||||
} else {
|
||||
MGLOG_W("LowerClipDistanceForEssl failed, continuing with unlowered SPIR-V.");
|
||||
}
|
||||
}
|
||||
|
||||
// Adreno quirk: split single constant-composite stores of struct arrays so
|
||||
// SPIRV-Cross does not promote them to global const struct[] LUTs, which this
|
||||
// driver cannot dynamically index ("Cannot offset into the structure").
|
||||
Vector<unsigned int> structLutSpirv;
|
||||
if (applyClipDistanceQuirk) {
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DefeatConstStructArrayLutForEssl(
|
||||
*effectiveSpirv, structLutSpirv) &&
|
||||
!structLutSpirv.empty()) {
|
||||
effectiveSpirv = &structLutSpirv;
|
||||
} else {
|
||||
MGLOG_W("DefeatConstStructArrayLutForEssl failed, continuing with unsplit SPIR-V.");
|
||||
}
|
||||
}
|
||||
|
||||
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
|
||||
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
|
||||
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
|
||||
@@ -3397,6 +3441,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||
source = RemoveLayoutBinding(source);
|
||||
if (applyClipDistanceQuirk) {
|
||||
// Adreno rejects the gl_ClipDistance redeclaration SPIRV-Cross still emits
|
||||
// ("reserved built-in name") but accepts plain usage with
|
||||
// GL_EXT_clip_cull_distance required; drop the line, keep the #extension.
|
||||
source = RemoveClipDistanceRedeclaration(source);
|
||||
}
|
||||
source = ProcessOutColorLocations(source);
|
||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
||||
|
||||
@@ -342,6 +342,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved
|
||||
// built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain
|
||||
// usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross
|
||||
// prints; the "#extension GL_EXT_clip_cull_distance : require" line stays.
|
||||
static const std::regex redeclarationRegex(
|
||||
R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)");
|
||||
|
||||
String result;
|
||||
result.reserve(glslCode.size());
|
||||
SizeT lineStart = 0;
|
||||
Bool firstLine = true;
|
||||
while (lineStart <= glslCode.size()) {
|
||||
SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
const Bool lastLine = lineEnd == String::npos;
|
||||
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
|
||||
|
||||
if (!std::regex_match(line, redeclarationRegex)) {
|
||||
if (!firstLine) {
|
||||
result += '\n';
|
||||
}
|
||||
result += line;
|
||||
firstLine = false;
|
||||
}
|
||||
if (lastLine) {
|
||||
break;
|
||||
}
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -105,6 +105,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 unormOutputMask);
|
||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "SpirvPasses/FoldConstOffsetFor1DFetchPass.h"
|
||||
#include "SpirvPasses/LowerClipDistanceForEsslPass.h"
|
||||
#include "SpirvPasses/DefeatConstStructArrayLutPass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
@@ -348,6 +350,31 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -43,6 +43,20 @@ namespace MobileGL {
|
||||
// transpile path.
|
||||
static bool FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Shadows gl_ClipDistance in Private mg_ClipDistance/mg_ClipDistanceIn arrays so
|
||||
// the decompiled ESSL only writes the builtin with literal constant indices
|
||||
// (flush before EmitVertex/return) and only reads gl_in clip distances with
|
||||
// dynamic loop indices (copy loop): the other shapes miscompile or crash
|
||||
// Adreno's ESSL compiler. Vertex/geometry stages; DirectGLES transpile path on
|
||||
// Qualcomm only (quirk-gated). See LowerClipDistanceForEsslPass.
|
||||
static bool LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Splits a Function-storage array-of-structs variable's single constant-composite
|
||||
// store into per-element stores so SPIRV-Cross does not hoist it into a global
|
||||
// const struct[] LUT, which Adreno cannot dynamically index. DirectGLES
|
||||
// transpile path on Qualcomm only (quirk-gated). See DefeatConstStructArrayLutPass.
|
||||
static bool DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.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 "DefeatConstStructArrayLutPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||
analysis::Pointer ptr(pointee, sc);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||
}
|
||||
|
||||
uint32_t SignedIntConstant(IRContext* ctx, uint32_t value) {
|
||||
analysis::Integer i(32, true);
|
||||
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
|
||||
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||
}
|
||||
|
||||
// True when |var| (a Function-storage OpVariable) points to an array of structs.
|
||||
// Reports the struct type id on success.
|
||||
bool IsArrayOfStructsVariable(IRContext* ctx, Instruction* var, uint32_t& structTypeId) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
Instruction* ptrType = defUse->GetDef(var->type_id());
|
||||
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return false;
|
||||
Instruction* pointee = defUse->GetDef(ptrType->GetSingleWordInOperand(1));
|
||||
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeArray) return false;
|
||||
Instruction* element = defUse->GetDef(pointee->GetSingleWordInOperand(0));
|
||||
if (element == nullptr || element->opcode() != spv::Op::OpTypeStruct) return false;
|
||||
structTypeId = element->result_id();
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status DefeatConstStructArrayLutPass::Process() {
|
||||
auto* ctx = context();
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
bool modified = false;
|
||||
|
||||
for (Function& function : *get_module()) {
|
||||
if (function.begin() == function.end()) continue;
|
||||
BasicBlock* entryBlock = &*function.begin();
|
||||
|
||||
// Candidate variables: Function-storage arrays of structs declared in this
|
||||
// function's entry block (where OpVariables must live).
|
||||
struct Candidate {
|
||||
Instruction* var;
|
||||
uint32_t structTypeId;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
for (Instruction& inst : *entryBlock) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) break;
|
||||
// Variables with initializers keep SPIRV-Cross's initializer path; the
|
||||
// glslang pattern under attack is initializer-free with one OpStore.
|
||||
if (inst.NumInOperands() > 1) continue;
|
||||
uint32_t structTypeId = 0;
|
||||
if (IsArrayOfStructsVariable(ctx, &inst, structTypeId)) {
|
||||
candidates.push_back({&inst, structTypeId});
|
||||
}
|
||||
}
|
||||
|
||||
for (const Candidate& candidate : candidates) {
|
||||
Instruction* var = candidate.var;
|
||||
|
||||
// The variable qualifies only when its single write is one direct
|
||||
// OpStore of an OpConstantComposite; any other write shape already
|
||||
// defeats SPIRV-Cross's LUT promotion, so it is left untouched.
|
||||
Instruction* singleStore = nullptr;
|
||||
bool disqualified = false;
|
||||
defUse->ForEachUser(var, [&](Instruction* user) {
|
||||
if (user->opcode() == spv::Op::OpStore &&
|
||||
user->GetSingleWordInOperand(0) == var->result_id()) {
|
||||
if (singleStore != nullptr) {
|
||||
disqualified = true;
|
||||
} else {
|
||||
singleStore = user;
|
||||
}
|
||||
} else if (user->opcode() == spv::Op::OpCopyMemory) {
|
||||
disqualified = true;
|
||||
} else if (user->opcode() == spv::Op::OpAccessChain ||
|
||||
user->opcode() == spv::Op::OpInBoundsAccessChain) {
|
||||
defUse->ForEachUser(user, [&](Instruction* chainUser) {
|
||||
if (chainUser->opcode() == spv::Op::OpStore ||
|
||||
chainUser->opcode() == spv::Op::OpCopyMemory) {
|
||||
disqualified = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (disqualified || singleStore == nullptr) continue;
|
||||
|
||||
Instruction* composite = defUse->GetDef(singleStore->GetSingleWordInOperand(1));
|
||||
if (composite == nullptr ||
|
||||
composite->opcode() != spv::Op::OpConstantComposite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The store must sit in the entry block: that is the only placement
|
||||
// SPIRV-Cross treats as a LUT initializer.
|
||||
bool storeInEntryBlock = false;
|
||||
for (Instruction& inst : *entryBlock) {
|
||||
if (&inst == singleStore) {
|
||||
storeInEntryBlock = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!storeInEntryBlock) continue;
|
||||
|
||||
// Split the composite store into one constant-index store per element.
|
||||
const uint32_t ptrFnStruct =
|
||||
PointerTypeTo(ctx, candidate.structTypeId, spv::StorageClass::Function);
|
||||
for (uint32_t element = 0; element < composite->NumInOperands(); ++element) {
|
||||
const uint32_t elementConstId = composite->GetSingleWordInOperand(element);
|
||||
const uint32_t chainId = ctx->TakeNextId();
|
||||
Instruction* chain =
|
||||
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrFnStruct, chainId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {var->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {SignedIntConstant(ctx, element)}}}));
|
||||
ctx->AnalyzeDefUse(chain);
|
||||
Instruction* store =
|
||||
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {chainId}},
|
||||
{SPV_OPERAND_TYPE_ID, {elementConstId}}}));
|
||||
ctx->AnalyzeDefUse(store);
|
||||
}
|
||||
ctx->KillInst(singleStore);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<DefeatConstStructArrayLutPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,36 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.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>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// SPIRV-Cross hoists a Function-storage array variable whose only write is a single
|
||||
// constant-composite store into a global `const struct[]` LUT (variable_is_lut).
|
||||
// Adreno's ESSL compiler cannot dynamically index such a global const struct array
|
||||
// ("Cannot offset into the structure" - device-verified on Adreno 750). Splitting
|
||||
// the one composite store into per-element constant-index stores makes
|
||||
// variable_is_lut fail, so SPIRV-Cross keeps the array as an ordinary local that
|
||||
// Adreno indexes fine. Scalar/vector const arrays are unaffected on Adreno and are
|
||||
// left alone - only arrays OF STRUCTS are rewritten. Only meant for the DirectGLES
|
||||
// transpile path on Qualcomm devices.
|
||||
class DefeatConstStructArrayLutPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "defeat-const-struct-array-lut"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateDefeatConstStructArrayLutPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,613 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.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 "LowerClipDistanceForEsslPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
|
||||
}
|
||||
return spv::ExecutionModel::Max;
|
||||
}
|
||||
|
||||
uint32_t EntryFunctionId(IRContext* ctx) {
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
// OpEntryPoint <model> <function> "name" <interface...>
|
||||
return ep.GetSingleWordInOperand(1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
|
||||
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
|
||||
// OpTypePointer <storage-class> <pointee>
|
||||
return ptrType->GetSingleWordInOperand(1);
|
||||
}
|
||||
|
||||
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||
analysis::Pointer ptr(pointee, sc);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||
}
|
||||
|
||||
uint32_t IntConstant(IRContext* ctx, bool isSigned, uint32_t value) {
|
||||
analysis::Integer i(32, isSigned);
|
||||
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
|
||||
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||
}
|
||||
|
||||
uint32_t UintType(IRContext* ctx) {
|
||||
analysis::Integer i(32, false);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&i);
|
||||
}
|
||||
|
||||
uint32_t BoolType(IRContext* ctx) {
|
||||
analysis::Bool b;
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&b);
|
||||
}
|
||||
|
||||
// Constant length of OpTypeArray |arrayTypeId| (0 when not a sized constant).
|
||||
uint32_t ArrayLength(IRContext* ctx, uint32_t arrayTypeId) {
|
||||
Instruction* arrayType = ctx->get_def_use_mgr()->GetDef(arrayTypeId);
|
||||
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) {
|
||||
return 0;
|
||||
}
|
||||
Instruction* length = ctx->get_def_use_mgr()->GetDef(arrayType->GetSingleWordInOperand(1));
|
||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
|
||||
return 0;
|
||||
}
|
||||
return length->GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
bool IsConstantWithValue(IRContext* ctx, uint32_t id, uint32_t value) {
|
||||
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
||||
return def != nullptr && def->opcode() == spv::Op::OpConstant &&
|
||||
def->GetSingleWordInOperand(0) == value;
|
||||
}
|
||||
|
||||
bool IsAccessChain(const Instruction* inst) {
|
||||
return inst->opcode() == spv::Op::OpAccessChain ||
|
||||
inst->opcode() == spv::Op::OpInBoundsAccessChain;
|
||||
}
|
||||
|
||||
Instruction* AddPrivateVariable(IRContext* ctx, uint32_t pointeeTypeId, const char* name) {
|
||||
const uint32_t ptrType = PointerTypeTo(ctx, pointeeTypeId, spv::StorageClass::Private);
|
||||
const uint32_t varId = ctx->TakeNextId();
|
||||
ctx->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpVariable, ptrType, varId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Private)}}}));
|
||||
ctx->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpName, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {varId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
|
||||
return ctx->get_def_use_mgr()->GetDef(varId);
|
||||
}
|
||||
|
||||
// Retargets |chain| onto |newBaseId|, dropping the first |dropIndexCount| index
|
||||
// operands and switching the result pointer's storage class to Private.
|
||||
void RetargetChainToPrivate(IRContext* ctx, Instruction* chain, uint32_t newBaseId,
|
||||
uint32_t dropIndexCount) {
|
||||
Instruction* chainPtrType = ctx->get_def_use_mgr()->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType = PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newBaseId}});
|
||||
for (uint32_t i = 1 + dropIndexCount; i < chain->NumInOperands(); ++i) {
|
||||
newOperands.push_back(chain->GetInOperand(i));
|
||||
}
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
}
|
||||
|
||||
// ---- Output side --------------------------------------------------------------
|
||||
|
||||
struct OutputTarget {
|
||||
Instruction* var = nullptr; // Output gl_PerVertex block or standalone builtin
|
||||
bool isBlockMember = false;
|
||||
uint32_t memberIndex = 0; // valid when isBlockMember
|
||||
uint32_t arrayTypeId = 0; // float[N]
|
||||
uint32_t elemTypeId = 0; // float
|
||||
uint32_t arrayLen = 0; // N
|
||||
};
|
||||
|
||||
// Inserts "gl_ClipDistance[k] = mg_ClipDistance[k]" for every literal k before
|
||||
// |before|. Constant-index writes are the only write shape Adreno links correctly.
|
||||
void InsertFlushBefore(IRContext* ctx, Instruction* before, const OutputTarget& target,
|
||||
uint32_t mgVarId) {
|
||||
const uint32_t ptrPrivElem =
|
||||
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Private);
|
||||
const uint32_t ptrOutElem =
|
||||
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Output);
|
||||
for (uint32_t k = 0; k < target.arrayLen; ++k) {
|
||||
const uint32_t kConst = IntConstant(ctx, true, k);
|
||||
const uint32_t srcChainId = ctx->TakeNextId();
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrPrivElem, srcChainId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {mgVarId}},
|
||||
{SPV_OPERAND_TYPE_ID, {kConst}}}));
|
||||
const uint32_t valId = ctx->TakeNextId();
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, target.elemTypeId, valId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {srcChainId}}}));
|
||||
const uint32_t dstChainId = ctx->TakeNextId();
|
||||
std::vector<Operand> dstOperands;
|
||||
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {target.var->result_id()}});
|
||||
if (target.isBlockMember) {
|
||||
dstOperands.push_back(
|
||||
{SPV_OPERAND_TYPE_ID, {IntConstant(ctx, true, target.memberIndex)}});
|
||||
}
|
||||
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {kConst}});
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrOutElem, dstChainId, dstOperands));
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {dstChainId}},
|
||||
{SPV_OPERAND_TYPE_ID, {valId}}}));
|
||||
}
|
||||
}
|
||||
|
||||
bool LowerOutputClipDistance(IRContext* ctx, bool isGeometry) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
|
||||
// Collect (struct type, member) pairs decorated BuiltIn ClipDistance and
|
||||
// standalone variables decorated BuiltIn ClipDistance.
|
||||
std::vector<std::pair<uint32_t, uint32_t>> memberTargets; // (structId, member)
|
||||
std::vector<uint32_t> plainTargets; // variable ids
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
memberTargets.emplace_back(ann.GetSingleWordInOperand(0),
|
||||
ann.GetSingleWordInOperand(1));
|
||||
} else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
plainTargets.push_back(ann.GetSingleWordInOperand(0));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<OutputTarget> targets;
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Output) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t pointee = VariablePointeeType(ctx, &inst);
|
||||
for (const auto& [structId, member] : memberTargets) {
|
||||
if (pointee != structId) continue;
|
||||
Instruction* structType = defUse->GetDef(structId);
|
||||
if (structType == nullptr || member >= structType->NumInOperands()) continue;
|
||||
OutputTarget target;
|
||||
target.var = &inst;
|
||||
target.isBlockMember = true;
|
||||
target.memberIndex = member;
|
||||
target.arrayTypeId = structType->GetSingleWordInOperand(member);
|
||||
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
|
||||
targets.push_back(target);
|
||||
}
|
||||
for (const uint32_t varId : plainTargets) {
|
||||
if (inst.result_id() != varId) continue;
|
||||
OutputTarget target;
|
||||
target.var = &inst;
|
||||
target.isBlockMember = false;
|
||||
target.arrayTypeId = pointee;
|
||||
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
|
||||
targets.push_back(target);
|
||||
}
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (OutputTarget& target : targets) {
|
||||
if (target.arrayLen == 0) continue;
|
||||
Instruction* arrayType = defUse->GetDef(target.arrayTypeId);
|
||||
target.elemTypeId = arrayType->GetSingleWordInOperand(0);
|
||||
|
||||
// Collect the accesses to redirect. For the block form only chains whose
|
||||
// leading index selects the ClipDistance member count; for the standalone
|
||||
// form every chain plus whole-variable loads/stores.
|
||||
std::vector<Instruction*> chains;
|
||||
std::vector<Instruction*> directAccesses;
|
||||
bool unsupportedUse = false;
|
||||
defUse->ForEachUser(target.var, [&](Instruction* user) {
|
||||
if (IsAccessChain(user) &&
|
||||
user->GetSingleWordInOperand(0) == target.var->result_id()) {
|
||||
if (target.isBlockMember) {
|
||||
if (user->NumInOperands() >= 2 &&
|
||||
IsConstantWithValue(ctx, user->GetSingleWordInOperand(1),
|
||||
target.memberIndex)) {
|
||||
chains.push_back(user);
|
||||
}
|
||||
} else {
|
||||
chains.push_back(user);
|
||||
}
|
||||
} else if (!target.isBlockMember) {
|
||||
if (user->opcode() == spv::Op::OpLoad ||
|
||||
(user->opcode() == spv::Op::OpStore &&
|
||||
user->GetSingleWordInOperand(0) == target.var->result_id())) {
|
||||
directAccesses.push_back(user);
|
||||
} else if (user->opcode() == spv::Op::OpCopyMemory) {
|
||||
unsupportedUse = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (unsupportedUse || (chains.empty() && directAccesses.empty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* mgVar = AddPrivateVariable(ctx, target.arrayTypeId, "mg_ClipDistance");
|
||||
const uint32_t mgVarId = mgVar->result_id();
|
||||
|
||||
for (Instruction* chain : chains) {
|
||||
const uint32_t dropCount = target.isBlockMember ? 1u : 0u;
|
||||
if (chain->NumInOperands() == 1 + dropCount) {
|
||||
// Pointer to the whole float[N]: reuse the private variable itself.
|
||||
ctx->ReplaceAllUsesWith(chain->result_id(), mgVarId);
|
||||
ctx->KillInst(chain);
|
||||
} else {
|
||||
RetargetChainToPrivate(ctx, chain, mgVarId, dropCount);
|
||||
}
|
||||
}
|
||||
for (Instruction* access : directAccesses) {
|
||||
ctx->ForgetUses(access);
|
||||
access->SetInOperand(0, {mgVarId});
|
||||
ctx->AnalyzeUses(access);
|
||||
}
|
||||
|
||||
// Flush the shadow into the real builtin: geometry right before every
|
||||
// EmitVertex, vertex before every return of the entry point. The flush is
|
||||
// also what keeps the builtin statically used for cross-stage IO matching.
|
||||
std::vector<Instruction*> flushSites;
|
||||
if (isGeometry) {
|
||||
for (Function& function : *ctx->module()) {
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() == spv::Op::OpEmitVertex) {
|
||||
flushSites.push_back(inst);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const uint32_t entryFuncId = EntryFunctionId(ctx);
|
||||
for (Function& function : *ctx->module()) {
|
||||
if (function.result_id() != entryFuncId) continue;
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() == spv::Op::OpReturn ||
|
||||
inst->opcode() == spv::Op::OpReturnValue) {
|
||||
flushSites.push_back(inst);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
for (Instruction* site : flushSites) {
|
||||
InsertFlushBefore(ctx, site, target, mgVarId);
|
||||
}
|
||||
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ---- Input side (geometry gl_in) ----------------------------------------------
|
||||
|
||||
bool LowerInputClipDistance(IRContext* ctx) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
auto* typeMgr = ctx->get_type_mgr();
|
||||
|
||||
// Locate the gl_in block member decorated ClipDistance.
|
||||
Instruction* glInVar = nullptr;
|
||||
uint32_t memberIndex = 0;
|
||||
uint32_t arrayTypeId = 0; // float[N]
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() != spv::Op::OpMemberDecorate || ann.NumInOperands() < 4 ||
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) !=
|
||||
spv::Decoration::BuiltIn ||
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) !=
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t structId = ann.GetSingleWordInOperand(0);
|
||||
const uint32_t member = ann.GetSingleWordInOperand(1);
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t pointee = VariablePointeeType(ctx, &inst);
|
||||
Instruction* pointeeType = defUse->GetDef(pointee);
|
||||
if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray ||
|
||||
pointeeType->GetSingleWordInOperand(0) != structId) {
|
||||
continue;
|
||||
}
|
||||
Instruction* structType = defUse->GetDef(structId);
|
||||
if (structType == nullptr || member >= structType->NumInOperands()) continue;
|
||||
glInVar = &inst;
|
||||
memberIndex = member;
|
||||
arrayTypeId = structType->GetSingleWordInOperand(member);
|
||||
break;
|
||||
}
|
||||
if (glInVar != nullptr) break;
|
||||
}
|
||||
if (glInVar == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t clipCount = ArrayLength(ctx, arrayTypeId);
|
||||
const uint32_t vertexCount = ArrayLength(ctx, VariablePointeeType(ctx, glInVar));
|
||||
if (clipCount == 0 || vertexCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every gl_in chain that selects the ClipDistance member:
|
||||
// (vertex, member) yields a whole float[N], (vertex, member, k) an element.
|
||||
std::vector<Instruction*> chains;
|
||||
defUse->ForEachUser(glInVar, [&](Instruction* user) {
|
||||
if (IsAccessChain(user) && user->GetSingleWordInOperand(0) == glInVar->result_id() &&
|
||||
user->NumInOperands() >= 3 &&
|
||||
IsConstantWithValue(ctx, user->GetSingleWordInOperand(2), memberIndex)) {
|
||||
chains.push_back(user);
|
||||
}
|
||||
});
|
||||
if (chains.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Instruction* arrayTypeInst = defUse->GetDef(arrayTypeId);
|
||||
const uint32_t elemTypeId = arrayTypeInst->GetSingleWordInOperand(0);
|
||||
|
||||
// Private mg_ClipDistanceIn = float[vertexCount][clipCount].
|
||||
const uint32_t vertexCountConst = IntConstant(ctx, false, vertexCount);
|
||||
analysis::Type* innerType = typeMgr->GetType(arrayTypeId);
|
||||
analysis::Array outerArray(
|
||||
innerType, analysis::Array::LengthInfo{
|
||||
vertexCountConst,
|
||||
{analysis::Array::LengthInfo::kConstant, vertexCount}});
|
||||
const uint32_t outerArrayTypeId = typeMgr->GetTypeInstruction(&outerArray);
|
||||
Instruction* mgInVar = AddPrivateVariable(ctx, outerArrayTypeId, "mg_ClipDistanceIn");
|
||||
const uint32_t mgInVarId = mgInVar->result_id();
|
||||
|
||||
// Copy loop at the top of the entry point:
|
||||
// for (uint t = 0; t < vertexCount * clipCount; ++t)
|
||||
// mg_ClipDistanceIn[t / clipCount][t % clipCount] =
|
||||
// gl_in[t / clipCount].gl_ClipDistance[t % clipCount];
|
||||
// Both gl_in indices are loop-derived (dynamic): constant-index element reads
|
||||
// miscompile and whole-array reads crash the Adreno compiler.
|
||||
const uint32_t entryFuncId = EntryFunctionId(ctx);
|
||||
Function* entryFn = nullptr;
|
||||
for (Function& function : *ctx->module()) {
|
||||
if (function.result_id() == entryFuncId) {
|
||||
entryFn = &function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entryFn == nullptr || entryFn->begin() == entryFn->end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t uintTypeId = UintType(ctx);
|
||||
const uint32_t boolTypeId = BoolType(ctx);
|
||||
const uint32_t ptrFnUint = PointerTypeTo(ctx, uintTypeId, spv::StorageClass::Function);
|
||||
const uint32_t ptrInElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Input);
|
||||
const uint32_t ptrPrivElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Private);
|
||||
const uint32_t uint0 = IntConstant(ctx, false, 0);
|
||||
const uint32_t uint1 = IntConstant(ctx, false, 1);
|
||||
const uint32_t uintN = IntConstant(ctx, false, clipCount);
|
||||
const uint32_t uintTotal = IntConstant(ctx, false, vertexCount * clipCount);
|
||||
const uint32_t memberConst = IntConstant(ctx, true, memberIndex);
|
||||
|
||||
BasicBlock* entryBlock = &*entryFn->begin();
|
||||
auto splitPoint = entryBlock->begin();
|
||||
while (splitPoint != entryBlock->end() &&
|
||||
splitPoint->opcode() == spv::Op::OpVariable) {
|
||||
++splitPoint;
|
||||
}
|
||||
|
||||
// Loop counter lives with the other function-local variables.
|
||||
const uint32_t counterVarId = ctx->TakeNextId();
|
||||
splitPoint->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpVariable, ptrFnUint, counterVarId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Function)}}}));
|
||||
|
||||
const uint32_t restLabelId = ctx->TakeNextId();
|
||||
BasicBlock* restBlock = entryBlock->SplitBasicBlock(ctx, restLabelId, splitPoint);
|
||||
|
||||
const uint32_t headerLabelId = ctx->TakeNextId();
|
||||
const uint32_t checkLabelId = ctx->TakeNextId();
|
||||
const uint32_t bodyLabelId = ctx->TakeNextId();
|
||||
const uint32_t continueLabelId = ctx->TakeNextId();
|
||||
|
||||
auto makeBlock = [&](uint32_t labelId) {
|
||||
return spvtools::MakeUnique<BasicBlock>(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLabel, 0, labelId, std::initializer_list<Operand>{}));
|
||||
};
|
||||
auto addInst = [&](BasicBlock* block, spv::Op opcode, uint32_t typeId,
|
||||
uint32_t resultId, std::vector<Operand> operands) {
|
||||
block->AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||
ctx, opcode, typeId, resultId, std::move(operands)));
|
||||
};
|
||||
|
||||
// entry: t = 0; branch header
|
||||
addInst(entryBlock, spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {uint0}}});
|
||||
addInst(entryBlock, spv::Op::OpBranch, 0, 0, {{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
|
||||
|
||||
// header: structured loop header
|
||||
auto headerBlock = makeBlock(headerLabelId);
|
||||
addInst(headerBlock.get(), spv::Op::OpLoopMerge, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {restLabelId}},
|
||||
{SPV_OPERAND_TYPE_ID, {continueLabelId}},
|
||||
{SPV_OPERAND_TYPE_LOOP_CONTROL,
|
||||
{static_cast<uint32_t>(spv::LoopControlMask::MaskNone)}}});
|
||||
addInst(headerBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {checkLabelId}}});
|
||||
|
||||
// check: t < vertexCount * clipCount ?
|
||||
auto checkBlock = makeBlock(checkLabelId);
|
||||
const uint32_t tCheckId = ctx->TakeNextId();
|
||||
addInst(checkBlock.get(), spv::Op::OpLoad, uintTypeId, tCheckId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t condId = ctx->TakeNextId();
|
||||
addInst(checkBlock.get(), spv::Op::OpULessThan, boolTypeId, condId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tCheckId}}, {SPV_OPERAND_TYPE_ID, {uintTotal}}});
|
||||
addInst(checkBlock.get(), spv::Op::OpBranchConditional, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {condId}},
|
||||
{SPV_OPERAND_TYPE_ID, {bodyLabelId}},
|
||||
{SPV_OPERAND_TYPE_ID, {restLabelId}}});
|
||||
|
||||
// body: mg_ClipDistanceIn[t / N][t % N] = gl_in[t / N].gl_ClipDistance[t % N]
|
||||
auto bodyBlock = makeBlock(bodyLabelId);
|
||||
const uint32_t tBodyId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpLoad, uintTypeId, tBodyId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t vertexIdxId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpUDiv, uintTypeId, vertexIdxId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
|
||||
const uint32_t clipIdxId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpUMod, uintTypeId, clipIdxId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
|
||||
const uint32_t srcChainId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrInElem, srcChainId,
|
||||
{{SPV_OPERAND_TYPE_ID, {glInVar->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
|
||||
{SPV_OPERAND_TYPE_ID, {memberConst}},
|
||||
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
|
||||
const uint32_t valId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpLoad, elemTypeId, valId,
|
||||
{{SPV_OPERAND_TYPE_ID, {srcChainId}}});
|
||||
const uint32_t dstChainId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrPrivElem, dstChainId,
|
||||
{{SPV_OPERAND_TYPE_ID, {mgInVarId}},
|
||||
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
|
||||
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
|
||||
addInst(bodyBlock.get(), spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {dstChainId}}, {SPV_OPERAND_TYPE_ID, {valId}}});
|
||||
addInst(bodyBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {continueLabelId}}});
|
||||
|
||||
// continue: ++t
|
||||
auto continueBlock = makeBlock(continueLabelId);
|
||||
const uint32_t tContinueId = ctx->TakeNextId();
|
||||
addInst(continueBlock.get(), spv::Op::OpLoad, uintTypeId, tContinueId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t tIncId = ctx->TakeNextId();
|
||||
addInst(continueBlock.get(), spv::Op::OpIAdd, uintTypeId, tIncId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tContinueId}}, {SPV_OPERAND_TYPE_ID, {uint1}}});
|
||||
addInst(continueBlock.get(), spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {tIncId}}});
|
||||
addInst(continueBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
|
||||
|
||||
BasicBlock* headerPtr = entryFn->InsertBasicBlockBefore(std::move(headerBlock), restBlock);
|
||||
BasicBlock* checkPtr = entryFn->InsertBasicBlockAfter(std::move(checkBlock), headerPtr);
|
||||
BasicBlock* bodyPtr = entryFn->InsertBasicBlockAfter(std::move(bodyBlock), checkPtr);
|
||||
entryFn->InsertBasicBlockAfter(std::move(continueBlock), bodyPtr);
|
||||
|
||||
// Redirect the pre-existing accesses to the shadow copy.
|
||||
for (Instruction* chain : chains) {
|
||||
if (chain->NumInOperands() == 3) {
|
||||
// (vertex, member): whole float[N] of one vertex.
|
||||
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType =
|
||||
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
|
||||
newOperands.push_back(chain->GetInOperand(1));
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
} else {
|
||||
// (vertex, member, k, ...): drop the member index.
|
||||
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType =
|
||||
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
|
||||
newOperands.push_back(chain->GetInOperand(1));
|
||||
for (uint32_t i = 3; i < chain->NumInOperands(); ++i) {
|
||||
newOperands.push_back(chain->GetInOperand(i));
|
||||
}
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status LowerClipDistanceForEsslPass::Process() {
|
||||
auto* ctx = context();
|
||||
const spv::ExecutionModel model = EntryExecutionModel(ctx);
|
||||
const bool isVertex = model == spv::ExecutionModel::Vertex;
|
||||
const bool isGeometry = model == spv::ExecutionModel::Geometry;
|
||||
if (!isVertex && !isGeometry) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
bool changed = LowerOutputClipDistance(ctx, isGeometry);
|
||||
if (isGeometry) {
|
||||
changed |= LowerInputClipDistance(ctx);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<LowerClipDistanceForEsslPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,44 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.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>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Adreno's ESSL compiler mishandles gl_ClipDistance (device-verified on Adreno 750):
|
||||
// - writes through non-constant indices silently fail to link,
|
||||
// - reads of gl_in[i].gl_ClipDistance[k] with a CONSTANT k >= 1 fail to compile
|
||||
// ("array indexing out of boundary") while dynamic-index reads work,
|
||||
// - compiling a whole-array read of gl_in[i].gl_ClipDistance segfaults the
|
||||
// compiler backend (libllvm-qgl.so).
|
||||
// This pass shadows the builtin so the decompiled ESSL only ever touches it in the
|
||||
// shapes Adreno accepts. Output side (vertex + geometry): all accesses to the
|
||||
// Output ClipDistance (gl_PerVertex member or standalone variable) are redirected
|
||||
// to a Private mg_ClipDistance array, and a flush writing the real builtin with
|
||||
// literal constant indices is inserted before every OpEmitVertex (geometry) or
|
||||
// every return of the entry point (vertex). Input side (geometry): accesses to
|
||||
// gl_in[...].gl_ClipDistance are redirected to a Private mg_ClipDistanceIn
|
||||
// array-of-arrays filled once at the top of the entry point by a structured loop
|
||||
// whose gl_in reads use dynamic (loop-variable) indices. The builtin members stay
|
||||
// statically referenced by the flush/copy so cross-stage IO matching is intact.
|
||||
// Only meant for the DirectGLES transpile path on Qualcomm devices.
|
||||
class LowerClipDistanceForEsslPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "lower-clip-distance-for-essl"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateLowerClipDistanceForEsslPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user