mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-16 06:23:23 +09:00
815 lines
46 KiB
C++
815 lines
46 KiB
C++
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.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 "EmulateClipDistancePass.h"
|
|
|
|
#include "spirv.hpp"
|
|
#include "source/opt/basic_block.h"
|
|
#include "source/opt/build_module.h"
|
|
#include "source/opt/constants.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/opt/types.h"
|
|
#include "source/util/make_unique.h"
|
|
#include "source/util/string_utils.h"
|
|
|
|
#include <format>
|
|
#include <unordered_set>
|
|
#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::InstructionBuilder;
|
|
using spvtools::opt::IRContext;
|
|
using spvtools::opt::Operand;
|
|
namespace analysis = spvtools::opt::analysis;
|
|
|
|
const IRContext::Analysis kBuilderAnalyses = static_cast<IRContext::Analysis>(
|
|
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
|
|
|
Instruction* EntryPoint(IRContext* ctx) {
|
|
for (Instruction& ep : ctx->module()->entry_points()) {
|
|
return &ep;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Function* EntryFunction(IRContext* ctx, Instruction* entryPoint) {
|
|
const uint32_t id = entryPoint->GetSingleWordInOperand(1);
|
|
for (auto& function : *ctx->module()) {
|
|
if (function.result_id() == id) return &function;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
uint32_t PointeeOf(IRContext* ctx, uint32_t pointerTypeId) {
|
|
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(pointerTypeId);
|
|
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return 0;
|
|
return ptrType->GetSingleWordInOperand(1);
|
|
}
|
|
|
|
bool PlainConstantValue(IRContext* ctx, uint32_t id, uint32_t& outValue) {
|
|
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
|
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
|
|
if (def->NumInOperands() != 1) return false;
|
|
outValue = def->GetSingleWordInOperand(0);
|
|
return true;
|
|
}
|
|
|
|
// Length of an OpTypeArray of 32-bit float with a plain constant length, else 0.
|
|
uint32_t FloatArrayLength(IRContext* ctx, uint32_t typeId) {
|
|
Instruction* t = ctx->get_def_use_mgr()->GetDef(typeId);
|
|
if (t == nullptr || t->opcode() != spv::Op::OpTypeArray) return 0;
|
|
Instruction* element = ctx->get_def_use_mgr()->GetDef(t->GetSingleWordInOperand(0));
|
|
if (element == nullptr || element->opcode() != spv::Op::OpTypeFloat ||
|
|
element->GetSingleWordInOperand(0) != 32) {
|
|
return 0;
|
|
}
|
|
uint32_t length = 0;
|
|
if (!PlainConstantValue(ctx, t->GetSingleWordInOperand(1), length)) return 0;
|
|
return length;
|
|
}
|
|
|
|
void AddNameFor(IRContext* ctx, uint32_t id, const String& name) {
|
|
std::vector<Operand> operands;
|
|
operands.push_back({SPV_OPERAND_TYPE_ID, {id}});
|
|
operands.push_back({SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)});
|
|
ctx->AddDebug2Inst(spvtools::MakeUnique<Instruction>(ctx, spv::Op::OpName, 0, 0, operands));
|
|
}
|
|
|
|
uint32_t AddGlobalVariable(IRContext* ctx, uint32_t pointeeTypeId, spv::StorageClass storage,
|
|
const String& name) {
|
|
const uint32_t ptrTypeId = ctx->get_type_mgr()->FindPointerToType(pointeeTypeId, storage);
|
|
if (ptrTypeId == 0) return 0;
|
|
const uint32_t varId = ctx->TakeNextId();
|
|
auto var = spvtools::MakeUnique<Instruction>(
|
|
ctx, spv::Op::OpVariable, ptrTypeId, varId,
|
|
std::initializer_list<Operand>{
|
|
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<uint32_t>(storage)}}});
|
|
Instruction* varInst = var.get();
|
|
ctx->AddGlobalValue(std::move(var));
|
|
ctx->get_def_use_mgr()->AnalyzeInstDefUse(varInst);
|
|
AddNameFor(ctx, varId, name);
|
|
return varId;
|
|
}
|
|
|
|
void AddLocationDecoration(IRContext* ctx, uint32_t id, uint32_t location) {
|
|
ctx->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
|
ctx, spv::Op::OpDecorate, 0, 0,
|
|
std::initializer_list<Operand>{
|
|
{SPV_OPERAND_TYPE_ID, {id}},
|
|
{SPV_OPERAND_TYPE_DECORATION, {static_cast<uint32_t>(spv::Decoration::Location)}},
|
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {location}}}));
|
|
}
|
|
|
|
void RemoveFromInterface(Instruction* entryPoint, uint32_t id) {
|
|
Instruction::OperandList kept;
|
|
for (uint32_t i = 0; i < entryPoint->NumInOperands(); ++i) {
|
|
const Operand& operand = entryPoint->GetInOperand(i);
|
|
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID && operand.words.size() == 1 &&
|
|
operand.words[0] == id) {
|
|
continue;
|
|
}
|
|
kept.push_back(operand);
|
|
}
|
|
entryPoint->SetInOperands(Move(kept));
|
|
}
|
|
|
|
// Whether every use of a pointer into the built-in is one the retype below can
|
|
// follow: loads, stores (as the POINTER), copies, and further access chains.
|
|
bool PointerUsesAreRetypable(IRContext* ctx, Instruction* pointer, String& reason) {
|
|
bool ok = true;
|
|
ctx->get_def_use_mgr()->ForEachUse(pointer, [&](Instruction* user, uint32_t operandIndex) {
|
|
if (!ok) return;
|
|
switch (user->opcode()) {
|
|
case spv::Op::OpName:
|
|
case spv::Op::OpDecorate:
|
|
case spv::Op::OpEntryPoint:
|
|
return;
|
|
case spv::Op::OpLoad:
|
|
case spv::Op::OpCopyMemory:
|
|
return;
|
|
case spv::Op::OpStore:
|
|
// Operand 0 of OpStore is the pointer; anything else would be
|
|
// storing the pointer itself, which logical addressing forbids.
|
|
if (operandIndex != 0) {
|
|
ok = false;
|
|
reason = "a pointer into gl_ClipDistance stored as a value";
|
|
}
|
|
return;
|
|
case spv::Op::OpAccessChain:
|
|
case spv::Op::OpInBoundsAccessChain:
|
|
// Base operand: the instruction's first id after result type/id.
|
|
if (operandIndex != 2) {
|
|
ok = false;
|
|
reason = "a pointer into gl_ClipDistance used as an access-chain index";
|
|
return;
|
|
}
|
|
if (!PointerUsesAreRetypable(ctx, user, reason)) ok = false;
|
|
return;
|
|
default:
|
|
ok = false;
|
|
reason = std::format("a pointer into gl_ClipDistance reaching SPIR-V opcode {}",
|
|
static_cast<uint32_t>(user->opcode()));
|
|
return;
|
|
}
|
|
});
|
|
return ok;
|
|
}
|
|
|
|
// Re-points every access chain hanging off `pointer` (which now lives in Private
|
|
// storage) at a Private pointer type of the same pointee.
|
|
void RetypeChainUsers(IRContext* ctx, Instruction* pointer) {
|
|
std::vector<Instruction*> chains;
|
|
ctx->get_def_use_mgr()->ForEachUser(pointer, [&](Instruction* user) {
|
|
if ((user->opcode() == spv::Op::OpAccessChain ||
|
|
user->opcode() == spv::Op::OpInBoundsAccessChain) &&
|
|
user->GetSingleWordInOperand(0) == pointer->result_id()) {
|
|
chains.push_back(user);
|
|
}
|
|
});
|
|
for (Instruction* chain : chains) {
|
|
const uint32_t pointee = PointeeOf(ctx, chain->type_id());
|
|
chain->SetResultType(
|
|
ctx->get_type_mgr()->FindPointerToType(pointee, spv::StorageClass::Private));
|
|
ctx->get_def_use_mgr()->AnalyzeInstUse(chain);
|
|
RetypeChainUsers(ctx, chain);
|
|
}
|
|
}
|
|
|
|
// A scalar constant of the given 32-bit integer type.
|
|
uint32_t IntConstantOfType(IRContext* ctx, uint32_t intTypeId, uint32_t value) {
|
|
const analysis::Type* type = ctx->get_type_mgr()->GetType(intTypeId);
|
|
const analysis::Integer* integer = type != nullptr ? type->AsInteger() : nullptr;
|
|
if (integer != nullptr && integer->IsSigned()) {
|
|
return ctx->get_constant_mgr()->GetSIntConstId(static_cast<int32_t>(value));
|
|
}
|
|
return ctx->get_constant_mgr()->GetUIntConstId(value);
|
|
}
|
|
|
|
bool IsInt32Type(IRContext* ctx, uint32_t typeId) {
|
|
const analysis::Type* type = ctx->get_type_mgr()->GetType(typeId);
|
|
const analysis::Integer* integer = type != nullptr ? type->AsInteger() : nullptr;
|
|
return integer != nullptr && integer->width() == 32;
|
|
}
|
|
|
|
struct ClipSites {
|
|
// struct type id -> member index of its BuiltIn ClipDistance member
|
|
std::vector<std::pair<uint32_t, uint32_t>> members;
|
|
std::unordered_set<uint32_t> standaloneVars;
|
|
Instruction* capability = nullptr;
|
|
};
|
|
|
|
ClipSites FindClipSites(IRContext* ctx) {
|
|
ClipSites sites;
|
|
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) {
|
|
sites.members.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) {
|
|
Instruction* var = ctx->get_def_use_mgr()->GetDef(ann.GetSingleWordInOperand(0));
|
|
if (var != nullptr && var->opcode() == spv::Op::OpVariable) {
|
|
sites.standaloneVars.insert(var->result_id());
|
|
}
|
|
}
|
|
}
|
|
for (Instruction& capability : ctx->module()->capabilities()) {
|
|
if (capability.NumInOperands() >= 1 &&
|
|
static_cast<spv::Capability>(capability.GetSingleWordInOperand(0)) ==
|
|
spv::Capability::ClipDistance) {
|
|
sites.capability = &capability;
|
|
}
|
|
}
|
|
return sites;
|
|
}
|
|
|
|
bool ClipMemberOf(const ClipSites& sites, uint32_t structId, uint32_t& member) {
|
|
for (const auto& [id, index] : sites.members) {
|
|
if (id == structId) {
|
|
member = index;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Every redirected access must agree on one gl_ClipDistance array type.
|
|
bool NoteShadowType(uint32_t& shadowTypeId, uint32_t typeId) {
|
|
if (shadowTypeId == 0) {
|
|
shadowTypeId = typeId;
|
|
return true;
|
|
}
|
|
return shadowTypeId == typeId;
|
|
}
|
|
|
|
// A chain that stops short of the gl_PerVertex member (a whole block or gl_in[i]
|
|
// pointer) is harmless unless something then reaches the clip member through it or
|
|
// copies the whole struct.
|
|
bool ShortChainTouchesClipMember(IRContext* ctx, Instruction* chain, uint32_t clipMember) {
|
|
bool touches = false;
|
|
ctx->get_def_use_mgr()->ForEachUser(chain, [&](Instruction* next) {
|
|
if (next->opcode() == spv::Op::OpAccessChain ||
|
|
next->opcode() == spv::Op::OpInBoundsAccessChain) {
|
|
uint32_t member = 0;
|
|
if (next->NumInOperands() < 2 ||
|
|
!PlainConstantValue(ctx, next->GetSingleWordInOperand(1), member) ||
|
|
member == clipMember) {
|
|
touches = true;
|
|
}
|
|
} else if (next->opcode() != spv::Op::OpName) {
|
|
touches = true; // whole-struct load/store/copy
|
|
}
|
|
});
|
|
return touches;
|
|
}
|
|
|
|
struct BlockVariable {
|
|
Instruction* var = nullptr;
|
|
spv::StorageClass storage = spv::StorageClass::Output;
|
|
bool arrayed = false;
|
|
uint32_t clipMember = 0;
|
|
uint32_t memberTypeId = 0;
|
|
};
|
|
|
|
// Classifies every use of a gl_PerVertex block variable: chains onto the clip member
|
|
// of a non-arrayed block are collected, anything else that reaches it declines.
|
|
bool CollectBlockClipChains(IRContext* ctx, const BlockVariable& block, uint32_t& shadowTypeId,
|
|
std::vector<Instruction*>& memberChains, String& reason) {
|
|
const uint32_t memberOperand = block.arrayed ? 2u : 1u; // [vertex,] member, ...
|
|
bool declined = false;
|
|
ctx->get_def_use_mgr()->ForEachUser(block.var, [&](Instruction* user) {
|
|
if (declined) return;
|
|
switch (user->opcode()) {
|
|
case spv::Op::OpEntryPoint:
|
|
case spv::Op::OpName:
|
|
case spv::Op::OpDecorate:
|
|
return;
|
|
case spv::Op::OpAccessChain:
|
|
case spv::Op::OpInBoundsAccessChain: {
|
|
if (user->NumInOperands() <= memberOperand) {
|
|
if (ShortChainTouchesClipMember(ctx, user, block.clipMember)) {
|
|
declined = true;
|
|
reason = "a split access chain or whole-gl_PerVertex copy reaching gl_ClipDistance";
|
|
}
|
|
return;
|
|
}
|
|
uint32_t member = 0;
|
|
if (!PlainConstantValue(ctx, user->GetSingleWordInOperand(memberOperand), member)) {
|
|
declined = true;
|
|
reason = "a non-constant gl_PerVertex member index";
|
|
return;
|
|
}
|
|
if (member != block.clipMember) return; // another member
|
|
if (block.arrayed) {
|
|
declined = true;
|
|
reason = block.storage == spv::StorageClass::Input ? "a gl_in[].gl_ClipDistance read"
|
|
: "a gl_out[].gl_ClipDistance write";
|
|
return;
|
|
}
|
|
if (!NoteShadowType(shadowTypeId, block.memberTypeId)) {
|
|
declined = true;
|
|
reason = "two differently sized gl_ClipDistance arrays";
|
|
return;
|
|
}
|
|
if (!PointerUsesAreRetypable(ctx, user, reason)) {
|
|
declined = true;
|
|
return;
|
|
}
|
|
memberChains.push_back(user);
|
|
return;
|
|
}
|
|
case spv::Op::OpLoad:
|
|
case spv::Op::OpStore:
|
|
case spv::Op::OpCopyMemory:
|
|
declined = true;
|
|
reason = "a whole-aggregate load/store/copy of the gl_PerVertex interface";
|
|
return;
|
|
default:
|
|
declined = true;
|
|
reason = std::format("SPIR-V opcode {} reaching the gl_PerVertex interface",
|
|
static_cast<uint32_t>(user->opcode()));
|
|
return;
|
|
}
|
|
});
|
|
return !declined;
|
|
}
|
|
|
|
// The clip member type of the stage's gl_PerVertex OUTPUT block, or 0. A producer with
|
|
// nothing redirected may still have declared the length the driver planned with.
|
|
uint32_t DeclaredOutputClipType(IRContext* ctx, const ClipSites& sites) {
|
|
for (Instruction& inst : ctx->module()->types_values()) {
|
|
if (inst.opcode() != spv::Op::OpVariable ||
|
|
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
|
spv::StorageClass::Output) {
|
|
continue;
|
|
}
|
|
Instruction* type = ctx->get_def_use_mgr()->GetDef(PointeeOf(ctx, inst.type_id()));
|
|
uint32_t member = 0;
|
|
if (type != nullptr && type->opcode() == spv::Op::OpTypeStruct &&
|
|
ClipMemberOf(sites, type->result_id(), member)) {
|
|
return type->GetSingleWordInOperand(member);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
struct EnablesUniform {
|
|
uint32_t varId = 0;
|
|
uint32_t memberIndex = 0;
|
|
uint32_t typeId = 0;
|
|
};
|
|
|
|
// The producer's enables uniform: a 32-bit integer member, by name, of a
|
|
// Uniform-storage struct.
|
|
EnablesUniform FindEnablesUniform(IRContext* ctx, const String& memberName) {
|
|
EnablesUniform found;
|
|
for (Instruction& debug : ctx->module()->debugs2()) {
|
|
if (debug.opcode() != spv::Op::OpMemberName || debug.NumInOperands() < 3) continue;
|
|
if (debug.GetOperand(2).AsString() != memberName) continue;
|
|
const uint32_t structId = debug.GetSingleWordInOperand(0);
|
|
const uint32_t member = debug.GetSingleWordInOperand(1);
|
|
Instruction* structType = ctx->get_def_use_mgr()->GetDef(structId);
|
|
if (structType == nullptr || structType->opcode() != spv::Op::OpTypeStruct ||
|
|
member >= structType->NumInOperands()) {
|
|
continue;
|
|
}
|
|
const uint32_t memberType = structType->GetSingleWordInOperand(member);
|
|
if (!IsInt32Type(ctx, memberType)) continue;
|
|
for (Instruction& var : ctx->module()->types_values()) {
|
|
if (var.opcode() == spv::Op::OpVariable &&
|
|
static_cast<spv::StorageClass>(var.GetSingleWordInOperand(0)) ==
|
|
spv::StorageClass::Uniform &&
|
|
PointeeOf(ctx, var.type_id()) == structId) {
|
|
return {var.result_id(), member, memberType};
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// Where the producer writes its distances out: before each vertex a geometry stage
|
|
// emits, else at the entry function's returns.
|
|
std::vector<Instruction*> FlushSites(IRContext* ctx, Instruction* entryPoint, bool isGeometry) {
|
|
std::vector<Instruction*> sites;
|
|
if (isGeometry) {
|
|
for (auto& function : *ctx->module()) {
|
|
function.ForEachInst([&](Instruction* inst) {
|
|
if (inst->opcode() == spv::Op::OpEmitVertex ||
|
|
inst->opcode() == spv::Op::OpEmitStreamVertex) {
|
|
sites.push_back(inst);
|
|
}
|
|
});
|
|
}
|
|
} else if (Function* entry = EntryFunction(ctx, entryPoint)) {
|
|
entry->ForEachInst([&](Instruction* inst) {
|
|
if (inst->opcode() == spv::Op::OpReturn || inst->opcode() == spv::Op::OpReturnValue) {
|
|
sites.push_back(inst);
|
|
}
|
|
});
|
|
}
|
|
return sites;
|
|
}
|
|
|
|
struct ScalarIds {
|
|
uint32_t floatType = 0;
|
|
uint32_t vec4Type = 0;
|
|
uint32_t boolType = 0;
|
|
uint32_t one = 0;
|
|
uint32_t zero = 0;
|
|
uint32_t ptrPrivateFloat = 0;
|
|
};
|
|
|
|
ScalarIds MakeScalarIds(IRContext* ctx) {
|
|
auto* typeMgr = ctx->get_type_mgr();
|
|
ScalarIds ids;
|
|
analysis::Float floatTy(32);
|
|
ids.floatType = typeMgr->GetTypeInstruction(&floatTy);
|
|
analysis::Vector vec4Ty(typeMgr->GetRegisteredType(&floatTy), 4);
|
|
ids.vec4Type = typeMgr->GetTypeInstruction(&vec4Ty);
|
|
analysis::Bool boolTy;
|
|
ids.boolType = typeMgr->GetTypeInstruction(&boolTy);
|
|
ids.one = ctx->get_constant_mgr()->GetFloatConstId(1.0f);
|
|
ids.zero = ctx->get_constant_mgr()->GetFloatConstId(0.0f);
|
|
ids.ptrPrivateFloat = typeMgr->FindPointerToType(ids.floatType, spv::StorageClass::Private);
|
|
return ids;
|
|
}
|
|
|
|
// Fragment stage: load the carriers at the top of the entry function, serve the
|
|
// shadow's gl_ClipDistance reads from them, and kill on any negative distance.
|
|
void EmitCarrierDiscard(IRContext* ctx, Instruction* entryPoint, const ScalarIds& ids,
|
|
Uint32 consumedCount, Uint32 location, uint32_t shadowId,
|
|
uint32_t shadowLength) {
|
|
Function* entry = EntryFunction(ctx, entryPoint);
|
|
if (entry == nullptr || entry->begin() == entry->end()) return;
|
|
auto* constMgr = ctx->get_constant_mgr();
|
|
const Uint32 carrierCount = (consumedCount + 3) / 4;
|
|
uint32_t carriers[EmulateClipDistancePass::kMaxCarriers] = {0, 0};
|
|
for (Uint32 k = 0; k < carrierCount; ++k) {
|
|
carriers[k] = AddGlobalVariable(ctx, ids.vec4Type, spv::StorageClass::Input,
|
|
EmulateClipDistancePass::CarrierName(k));
|
|
AddLocationDecoration(ctx, carriers[k], location + k);
|
|
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {carriers[k]}});
|
|
}
|
|
BasicBlock* head = &*entry->begin();
|
|
auto split = head->begin();
|
|
while (split != head->end() && split->opcode() == spv::Op::OpVariable)
|
|
++split;
|
|
BasicBlock* rest = head->SplitBasicBlock(ctx, ctx->TakeNextId(), split);
|
|
|
|
InstructionBuilder b(ctx, head, kBuilderAnalyses);
|
|
uint32_t loaded[EmulateClipDistancePass::kMaxCarriers] = {0, 0};
|
|
for (Uint32 k = 0; k < carrierCount; ++k) {
|
|
loaded[k] = b.AddLoad(ids.vec4Type, carriers[k])->result_id();
|
|
}
|
|
uint32_t anyNegative = 0;
|
|
for (Uint32 i = 0; i < consumedCount; ++i) {
|
|
const uint32_t component =
|
|
b.AddCompositeExtract(ids.floatType, loaded[i / 4], {i % 4})->result_id();
|
|
if (shadowId != 0 && i < shadowLength) {
|
|
Instruction* ptr =
|
|
b.AddAccessChain(ids.ptrPrivateFloat, shadowId, {constMgr->GetUIntConstId(i)});
|
|
b.AddStore(ptr->result_id(), component);
|
|
}
|
|
const uint32_t negative =
|
|
b.AddBinaryOp(ids.boolType, spv::Op::OpFOrdLessThan, component, ids.zero)->result_id();
|
|
anyNegative =
|
|
anyNegative == 0
|
|
? negative
|
|
: b.AddBinaryOp(ids.boolType, spv::Op::OpLogicalOr, anyNegative, negative)->result_id();
|
|
}
|
|
const uint32_t killLabel = ctx->TakeNextId();
|
|
b.AddConditionalBranch(anyNegative, killLabel, rest->id(), rest->id());
|
|
|
|
auto killBlock = spvtools::MakeUnique<BasicBlock>(spvtools::MakeUnique<Instruction>(
|
|
ctx, spv::Op::OpLabel, 0, killLabel, std::initializer_list<Operand>{}));
|
|
killBlock->AddInstruction(spvtools::MakeUnique<Instruction>(ctx, spv::Op::OpKill, 0, 0,
|
|
std::initializer_list<Operand>{}));
|
|
entry->InsertBasicBlockAfter(std::move(killBlock), head);
|
|
}
|
|
} // namespace
|
|
|
|
ClipDistanceModuleProbe EmulateClipDistancePass::ProbeModule(const Vector<Uint32>& spirv) {
|
|
ClipDistanceModuleProbe probe;
|
|
if (spirv.empty()) {
|
|
probe.parsed = true;
|
|
return probe;
|
|
}
|
|
std::unique_ptr<IRContext> ctx = spvtools::BuildModule(
|
|
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
|
spirv.data(), spirv.size());
|
|
if (!ctx) return probe;
|
|
probe.parsed = true;
|
|
const ClipSites sites = FindClipSites(ctx.get());
|
|
probe.declaresClipDistanceCapability = sites.capability != nullptr;
|
|
for (Instruction& inst : ctx->module()->types_values()) {
|
|
if (inst.opcode() != spv::Op::OpVariable) continue;
|
|
if (static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) != spv::StorageClass::Output) {
|
|
continue;
|
|
}
|
|
const uint32_t pointee = PointeeOf(ctx.get(), inst.type_id());
|
|
if (sites.standaloneVars.count(inst.result_id()) != 0) {
|
|
probe.outputDistanceCount = FloatArrayLength(ctx.get(), pointee);
|
|
break;
|
|
}
|
|
Instruction* type = ctx->get_def_use_mgr()->GetDef(pointee);
|
|
uint32_t member = 0;
|
|
if (type != nullptr && type->opcode() == spv::Op::OpTypeStruct &&
|
|
ClipMemberOf(sites, type->result_id(), member)) {
|
|
probe.outputDistanceCount = FloatArrayLength(ctx.get(), type->GetSingleWordInOperand(member));
|
|
break;
|
|
}
|
|
}
|
|
return probe;
|
|
}
|
|
|
|
spvtools::opt::Pass::Status EmulateClipDistancePass::Process() {
|
|
IRContext* ctx = context();
|
|
auto* defUse = ctx->get_def_use_mgr();
|
|
auto* typeMgr = ctx->get_type_mgr();
|
|
auto* constMgr = ctx->get_constant_mgr();
|
|
using Role = EmulateClipDistanceOptions::Role;
|
|
|
|
Instruction* entryPoint = EntryPoint(ctx);
|
|
if (entryPoint == nullptr) return Status::SuccessWithoutChange;
|
|
const auto model = static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0));
|
|
const bool isFragment = model == spv::ExecutionModel::Fragment;
|
|
const bool isGeometry = model == spv::ExecutionModel::Geometry;
|
|
const bool isPreRaster = model == spv::ExecutionModel::Vertex ||
|
|
model == spv::ExecutionModel::TessellationControl ||
|
|
model == spv::ExecutionModel::TessellationEvaluation || isGeometry;
|
|
|
|
const auto decline = [&](String reason) {
|
|
if (m_report != nullptr) {
|
|
m_report->declined = true;
|
|
m_report->declineReason = Move(reason);
|
|
}
|
|
return Status::SuccessWithoutChange;
|
|
};
|
|
if (m_options.role == Role::Consumer && !isFragment)
|
|
return decline("consumer role on a non-fragment stage");
|
|
if (m_options.role != Role::Consumer && !isPreRaster) {
|
|
return decline("producer/redirect role on a non-pre-rasterization stage");
|
|
}
|
|
if (m_options.role == Role::Producer && model == spv::ExecutionModel::TessellationControl) {
|
|
return decline("a control stage cannot be the last pre-rasterization stage");
|
|
}
|
|
const bool native = m_options.nativeBuiltin;
|
|
if (native && m_options.role != Role::Producer) return decline("native masking outside the producer");
|
|
|
|
const ClipSites sites = FindClipSites(ctx);
|
|
|
|
// ---- Phase 1: ANALYSIS ONLY. A decline leaves the module byte-identical. ----
|
|
std::vector<Instruction*> memberChains; // [clipMember, rest...] on a non-arrayed block
|
|
std::vector<Instruction*> standaloneVars; // standalone ClipDistance variables
|
|
// Native masking: where the masked values are written back to.
|
|
Instruction* nativeBlockVar = nullptr;
|
|
uint32_t nativeBlockMember = 0;
|
|
Instruction* nativeStandaloneVar = nullptr;
|
|
uint32_t shadowTypeId = 0;
|
|
String reason;
|
|
|
|
for (Instruction& inst : ctx->module()->types_values()) {
|
|
if (inst.opcode() != spv::Op::OpVariable) continue;
|
|
const auto storage = static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0));
|
|
if (storage != spv::StorageClass::Input && storage != spv::StorageClass::Output) continue;
|
|
// Native masking leaves every INPUT alone: gl_in[] reads are hosted natively.
|
|
if (native && storage == spv::StorageClass::Input) continue;
|
|
const uint32_t pointeeId = PointeeOf(ctx, inst.type_id());
|
|
|
|
if (sites.standaloneVars.count(inst.result_id()) != 0) {
|
|
if (native) nativeStandaloneVar = &inst;
|
|
if (FloatArrayLength(ctx, pointeeId) == 0) {
|
|
return decline("a standalone gl_ClipDistance that is not a sized float array");
|
|
}
|
|
// A pre-rasterization stage's standalone INPUT would be gl_in data,
|
|
// which a non-arrayed variable cannot be; only the fragment stage reads
|
|
// gl_ClipDistance this way.
|
|
if (storage == spv::StorageClass::Input && !isFragment) {
|
|
return decline("a standalone gl_ClipDistance input outside the fragment stage");
|
|
}
|
|
if (!NoteShadowType(shadowTypeId, pointeeId)) {
|
|
return decline("two differently sized gl_ClipDistance arrays");
|
|
}
|
|
if (!PointerUsesAreRetypable(ctx, &inst, reason)) return decline(Move(reason));
|
|
standaloneVars.push_back(&inst);
|
|
continue;
|
|
}
|
|
|
|
Instruction* pointee = defUse->GetDef(pointeeId);
|
|
if (pointee == nullptr) continue;
|
|
bool arrayed = false;
|
|
if (pointee->opcode() == spv::Op::OpTypeArray) {
|
|
arrayed = true;
|
|
pointee = defUse->GetDef(pointee->GetSingleWordInOperand(0));
|
|
if (pointee == nullptr) continue;
|
|
}
|
|
BlockVariable block{&inst, storage, arrayed, 0, 0};
|
|
if (pointee->opcode() != spv::Op::OpTypeStruct ||
|
|
!ClipMemberOf(sites, pointee->result_id(), block.clipMember)) {
|
|
continue;
|
|
}
|
|
block.memberTypeId = pointee->GetSingleWordInOperand(block.clipMember);
|
|
if (native && !arrayed && storage == spv::StorageClass::Output) {
|
|
nativeBlockVar = &inst;
|
|
nativeBlockMember = block.clipMember;
|
|
}
|
|
if (!CollectBlockClipChains(ctx, block, shadowTypeId, memberChains, reason)) {
|
|
return decline(Move(reason));
|
|
}
|
|
}
|
|
|
|
const bool hasAccesses = !memberChains.empty() || !standaloneVars.empty();
|
|
if (shadowTypeId == 0 && m_options.role == Role::Producer) {
|
|
shadowTypeId = DeclaredOutputClipType(ctx, sites);
|
|
}
|
|
const uint32_t shadowLength = shadowTypeId != 0 ? FloatArrayLength(ctx, shadowTypeId) : 0;
|
|
if (shadowTypeId != 0 && shadowLength == 0)
|
|
return decline("gl_ClipDistance is not a sized float array");
|
|
|
|
Uint32 producedCount = 0;
|
|
if (m_options.role == Role::Producer) {
|
|
producedCount = std::min<Uint32>(shadowLength, kMaxCarriers * 4);
|
|
if (shadowLength > kMaxCarriers * 4) return decline("more than eight clip distances");
|
|
}
|
|
const Uint32 consumedCount =
|
|
m_options.role == Role::Consumer ? std::min<Uint32>(m_options.distanceCount, kMaxCarriers * 4) : 0;
|
|
|
|
const EnablesUniform enables = producedCount > 0 && !m_options.enablesMemberName.empty()
|
|
? FindEnablesUniform(ctx, m_options.enablesMemberName)
|
|
: EnablesUniform{};
|
|
const uint32_t enablesVarId = enables.varId;
|
|
const uint32_t enablesTypeId = enables.typeId;
|
|
|
|
if (!hasAccesses && sites.capability == nullptr && producedCount == 0 && consumedCount == 0) {
|
|
return Status::SuccessWithoutChange;
|
|
}
|
|
if (native && producedCount > 0 && nativeBlockVar == nullptr && nativeStandaloneVar == nullptr) {
|
|
return decline("no gl_ClipDistance output to write the masked distances back into");
|
|
}
|
|
|
|
// ---- Phase 2: MUTATION. Nothing below may decline. ----
|
|
const bool interfaceListsAllGlobals = ctx->module()->version() >= 0x00010400u;
|
|
uint32_t shadowId = 0;
|
|
if (shadowTypeId != 0 && (hasAccesses || producedCount > 0)) {
|
|
shadowId = AddGlobalVariable(ctx, shadowTypeId, spv::StorageClass::Private, kShadowName);
|
|
if (interfaceListsAllGlobals) entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {shadowId}});
|
|
}
|
|
|
|
for (Instruction* chain : memberChains) {
|
|
if (chain->NumInOperands() == 2) {
|
|
// The chain IS the whole array: every use now addresses the shadow.
|
|
ctx->ReplaceAllUsesWith(chain->result_id(), shadowId);
|
|
ctx->KillInst(chain);
|
|
continue;
|
|
}
|
|
Instruction::OperandList operands;
|
|
operands.push_back({SPV_OPERAND_TYPE_ID, {shadowId}});
|
|
for (uint32_t i = 2; i < chain->NumInOperands(); ++i) {
|
|
operands.push_back(chain->GetInOperand(i));
|
|
}
|
|
chain->SetInOperands(Move(operands));
|
|
chain->SetResultType(
|
|
typeMgr->FindPointerToType(PointeeOf(ctx, chain->type_id()), spv::StorageClass::Private));
|
|
defUse->AnalyzeInstUse(chain);
|
|
RetypeChainUsers(ctx, chain);
|
|
}
|
|
for (Instruction* var : standaloneVars) {
|
|
const uint32_t varId = var->result_id();
|
|
if (native) {
|
|
// The variable stays the built-in the flush writes; only the shader's
|
|
// own accesses move to the shadow.
|
|
ctx->ReplaceAllUsesWithPredicate(varId, shadowId, [](Instruction* user) {
|
|
return user->opcode() != spv::Op::OpDecorate && user->opcode() != spv::Op::OpName &&
|
|
user->opcode() != spv::Op::OpEntryPoint;
|
|
});
|
|
continue;
|
|
}
|
|
ctx->KillNamesAndDecorates(varId);
|
|
RemoveFromInterface(entryPoint, varId);
|
|
ctx->ReplaceAllUsesWith(varId, shadowId);
|
|
ctx->KillInst(var);
|
|
}
|
|
if (shadowId != 0) RetypeChainUsers(ctx, defUse->GetDef(shadowId));
|
|
if (sites.capability != nullptr && !native) ctx->KillInst(sites.capability);
|
|
|
|
const ScalarIds ids = MakeScalarIds(ctx);
|
|
const uint32_t floatTypeId = ids.floatType;
|
|
const uint32_t vec4TypeId = ids.vec4Type;
|
|
const uint32_t boolTypeId = ids.boolType;
|
|
const uint32_t oneId = ids.one;
|
|
|
|
// ---- producer: write the carriers ----
|
|
if (producedCount > 0) {
|
|
const Uint32 carrierCount = native ? 0 : (producedCount + 3) / 4;
|
|
uint32_t carriers[kMaxCarriers] = {0, 0};
|
|
const uint32_t ptrOutputFloat = typeMgr->FindPointerToType(floatTypeId, spv::StorageClass::Output);
|
|
const uint32_t nativeMemberConst =
|
|
nativeBlockVar != nullptr ? constMgr->GetSIntConstId(static_cast<int32_t>(nativeBlockMember))
|
|
: 0;
|
|
for (Uint32 k = 0; k < carrierCount; ++k) {
|
|
carriers[k] = AddGlobalVariable(ctx, vec4TypeId, spv::StorageClass::Output, CarrierName(k));
|
|
AddLocationDecoration(ctx, carriers[k], m_options.location + k);
|
|
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {carriers[k]}});
|
|
}
|
|
uint32_t ptrUniformEnables = 0;
|
|
uint32_t enablesMemberConst = 0;
|
|
if (enablesVarId != 0) {
|
|
ptrUniformEnables = typeMgr->FindPointerToType(enablesTypeId, spv::StorageClass::Uniform);
|
|
enablesMemberConst = constMgr->GetSIntConstId(static_cast<int32_t>(enables.memberIndex));
|
|
if (interfaceListsAllGlobals) {
|
|
bool listed = false;
|
|
for (uint32_t i = 3; i < entryPoint->NumInOperands(); ++i) {
|
|
if (entryPoint->GetSingleWordInOperand(i) == enablesVarId) listed = true;
|
|
}
|
|
if (!listed) entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {enablesVarId}});
|
|
}
|
|
}
|
|
|
|
for (Instruction* site : FlushSites(ctx, entryPoint, isGeometry)) {
|
|
InstructionBuilder b(ctx, site, kBuilderAnalyses);
|
|
uint32_t maskId = 0;
|
|
if (enablesVarId != 0) {
|
|
Instruction* ptr = b.AddAccessChain(ptrUniformEnables, enablesVarId, {enablesMemberConst});
|
|
maskId = b.AddLoad(enablesTypeId, ptr->result_id())->result_id();
|
|
}
|
|
std::vector<uint32_t> values;
|
|
for (Uint32 i = 0; i < producedCount; ++i) {
|
|
Instruction* ptr =
|
|
b.AddAccessChain(ids.ptrPrivateFloat, shadowId, {constMgr->GetUIntConstId(i)});
|
|
uint32_t value = b.AddLoad(floatTypeId, ptr->result_id())->result_id();
|
|
if (maskId != 0) {
|
|
const uint32_t bit = b.AddBinaryOp(enablesTypeId, spv::Op::OpBitwiseAnd, maskId,
|
|
IntConstantOfType(ctx, enablesTypeId, 1u << i))
|
|
->result_id();
|
|
const uint32_t on = b.AddBinaryOp(boolTypeId, spv::Op::OpINotEqual, bit,
|
|
IntConstantOfType(ctx, enablesTypeId, 0u))
|
|
->result_id();
|
|
value = b.AddSelect(floatTypeId, on, value, oneId)->result_id();
|
|
}
|
|
values.push_back(value);
|
|
}
|
|
if (native) {
|
|
for (Uint32 i = 0; i < producedCount; ++i) {
|
|
const uint32_t index = constMgr->GetUIntConstId(i);
|
|
Instruction* ptr =
|
|
nativeBlockVar != nullptr
|
|
? b.AddAccessChain(ptrOutputFloat, nativeBlockVar->result_id(),
|
|
{nativeMemberConst, index})
|
|
: b.AddAccessChain(ptrOutputFloat, nativeStandaloneVar->result_id(), {index});
|
|
b.AddStore(ptr->result_id(), values[i]);
|
|
}
|
|
}
|
|
while (values.size() % 4 != 0)
|
|
values.push_back(oneId);
|
|
for (Uint32 k = 0; k < carrierCount; ++k) {
|
|
const uint32_t composite =
|
|
b.AddCompositeConstruct(vec4TypeId, {values[k * 4], values[k * 4 + 1],
|
|
values[k * 4 + 2], values[k * 4 + 3]})
|
|
->result_id();
|
|
b.AddStore(carriers[k], composite);
|
|
}
|
|
}
|
|
if (m_report != nullptr) {
|
|
m_report->distanceCount = producedCount;
|
|
m_report->enablesFound = enablesVarId != 0;
|
|
}
|
|
}
|
|
|
|
// ---- consumer: read the carriers, discard, serve gl_ClipDistance reads ----
|
|
if (m_options.role == Role::Consumer && consumedCount > 0) {
|
|
EmitCarrierDiscard(ctx, entryPoint, ids, consumedCount, m_options.location, shadowId, shadowLength);
|
|
}
|
|
|
|
if (m_report != nullptr) m_report->rewrote = true;
|
|
ctx->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
|
return Status::SuccessWithChange;
|
|
}
|
|
|
|
spvtools::Optimizer::PassToken EmulateClipDistancePass::CreateEmulateClipDistancePass(
|
|
EmulateClipDistanceOptions options, EmulateClipDistanceReport* report) {
|
|
return spvtools::Optimizer::PassToken(MakeUnique<EmulateClipDistancePass>(Move(options), report));
|
|
}
|
|
} // namespace ShaderTranspiler
|
|
} // namespace MG_Util
|
|
} // namespace MobileGL
|