mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
1339 lines
83 KiB
C++
1339 lines
83 KiB
C++
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.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 "FlattenFloat64StorageBlockPass.h"
|
|
|
|
#include "spirv.hpp"
|
|
#include "source/opt/basic_block.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 <algorithm>
|
|
#include <memory>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace MobileGL {
|
|
namespace MG_Util {
|
|
namespace ShaderTranspiler {
|
|
namespace {
|
|
using spvtools::MakeUnique;
|
|
using spvtools::opt::Instruction;
|
|
using spvtools::opt::InstructionBuilder;
|
|
using spvtools::opt::IRContext;
|
|
using spvtools::opt::Module;
|
|
using spvtools::opt::Operand;
|
|
|
|
// The flattened array's element, and the granularity every offset and stride in
|
|
// the block has to land on.
|
|
constexpr uint32_t kWordBytes = 4u;
|
|
// A block wider than any GL implementation lets one binding cover is refused
|
|
// rather than expanded into an array nothing could address. GL 4.6 core table
|
|
// 23.64 puts the minimum GL_MAX_SHADER_STORAGE_BLOCK_SIZE at 2^24 bytes; this is
|
|
// a generous multiple of that and exists only to bound the rewrite.
|
|
constexpr uint64_t kMaxBlockBytes = 1ull << 27;
|
|
// The most scalars one load or store may decompose into. A whole-aggregate copy
|
|
// becomes one word access per scalar, so without a cap a `dvec4 data[4096]`
|
|
// member would turn a two-instruction copy into a 32k-instruction one.
|
|
constexpr uint32_t kMaxLeavesPerAccess = 1024u;
|
|
// The analyses every builder in this pass keeps current as it inserts.
|
|
constexpr IRContext::Analysis kPreservedAnalyses = static_cast<IRContext::Analysis>(
|
|
static_cast<uint32_t>(IRContext::kAnalysisDefUse) |
|
|
static_cast<uint32_t>(IRContext::kAnalysisInstrToBlockMapping));
|
|
|
|
// How a type sits in memory, as the ENCLOSING struct member described it.
|
|
// MatrixStride and RowMajor are member decorations rather than type decorations,
|
|
// so a matrix type carries no layout of its own and the walk has to hand it down -
|
|
// through arrays of matrices too, which is why this rides alongside the type id
|
|
// instead of being looked up from it.
|
|
struct TypeCursor {
|
|
uint32_t typeId = 0;
|
|
uint32_t matrixStride = 0;
|
|
bool rowMajor = false;
|
|
};
|
|
|
|
// One access chain rooted at a flattened block's variable, and everything the
|
|
// rewrite needs so it does not have to walk the type tree a second time.
|
|
struct ChainPlan {
|
|
Instruction* chain = nullptr;
|
|
uint32_t variableId = 0;
|
|
// What the chain's CONSTANT indices contribute, in words.
|
|
uint32_t constantWords = 0;
|
|
// Its non-constant indices, as (index value id, words per step).
|
|
std::vector<std::pair<uint32_t, uint32_t>> dynamicTerms;
|
|
TypeCursor pointee;
|
|
std::vector<Instruction*> loads;
|
|
std::vector<Instruction*> stores;
|
|
};
|
|
|
|
struct BlockPlan {
|
|
Instruction* structType = nullptr;
|
|
uint32_t storageClass = 0;
|
|
// A bounded block's length in words. For an open-ended block - one whose
|
|
// last member is a runtime array - the FIXED PREFIX in words, i.e. the
|
|
// runtime array's own offset, which is where its element 0 starts.
|
|
uint32_t wordCount = 0;
|
|
bool openEnded = false;
|
|
// The original runtime array's stride in words; what one element of it
|
|
// steps by, and what its word count divides by to become a length.
|
|
uint32_t tailStrideWords = 0;
|
|
std::vector<ChainPlan> chains;
|
|
// The OpArrayLength users of an open-ended block's variables, which count
|
|
// WORDS once the member is a `uint[]` and so have to be rewritten too.
|
|
std::vector<Instruction*> arrayLengths;
|
|
};
|
|
|
|
bool IsDoubleType(const Instruction* type) {
|
|
return type != nullptr && type->opcode() == spv::Op::OpTypeFloat &&
|
|
type->NumInOperands() >= 1 && type->GetSingleWordInOperand(0) == 64u;
|
|
}
|
|
|
|
// Byte size of a scalar this pass can carry, or 0 for one it cannot.
|
|
uint32_t ScalarByteSize(const Instruction* type) {
|
|
if (type == nullptr) return 0;
|
|
if (type->opcode() != spv::Op::OpTypeFloat && type->opcode() != spv::Op::OpTypeInt) {
|
|
return 0;
|
|
}
|
|
const uint32_t width = type->GetSingleWordInOperand(0);
|
|
if (width == 32u) return 4u;
|
|
if (width == 64u && type->opcode() == spv::Op::OpTypeFloat) return 8u;
|
|
return 0;
|
|
}
|
|
|
|
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 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 HasMemberDecoration(IRContext* context, uint32_t structId, uint32_t member,
|
|
spv::Decoration kind) {
|
|
for (Instruction* decoration :
|
|
context->get_decoration_mgr()->GetDecorationsFor(structId, false)) {
|
|
if (decoration->opcode() != spv::Op::OpMemberDecorate ||
|
|
decoration->NumInOperands() < 3 ||
|
|
decoration->GetSingleWordInOperand(1) != member) {
|
|
continue;
|
|
}
|
|
if (static_cast<spv::Decoration>(decoration->GetSingleWordInOperand(2)) == kind) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// The cursor for member `member` of a struct: its type, plus the matrix layout
|
|
// that member's own decorations describe.
|
|
TypeCursor MemberCursor(IRContext* context, const Instruction* structType, uint32_t member) {
|
|
TypeCursor cursor;
|
|
cursor.typeId = structType->GetSingleWordInOperand(member);
|
|
uint32_t stride = 0;
|
|
if (TryGetMemberDecorationLiteral(context, structType->result_id(), member,
|
|
spv::Decoration::MatrixStride, &stride)) {
|
|
cursor.matrixStride = stride;
|
|
}
|
|
cursor.rowMajor = HasMemberDecoration(context, structType->result_id(), member,
|
|
spv::Decoration::RowMajor);
|
|
return cursor;
|
|
}
|
|
|
|
// Byte size of a type as it is laid out INSIDE a block, or 0 when this pass
|
|
// cannot describe it (a runtime array - the one place a block may have one is
|
|
// its last member, which MeasureBlock handles above this - a width it does not
|
|
// carry, a matrix with no stride or a row-major one).
|
|
uint32_t LaidOutByteSize(IRContext* context, const TypeCursor& cursor) {
|
|
const Instruction* type = context->get_def_use_mgr()->GetDef(cursor.typeId);
|
|
if (type == nullptr) return 0;
|
|
switch (type->opcode()) {
|
|
case spv::Op::OpTypeInt:
|
|
case spv::Op::OpTypeFloat:
|
|
return ScalarByteSize(type);
|
|
case spv::Op::OpTypeVector: {
|
|
const uint32_t component = ScalarByteSize(
|
|
context->get_def_use_mgr()->GetDef(type->GetSingleWordInOperand(0)));
|
|
if (component == 0) return 0;
|
|
return component * type->GetSingleWordInOperand(1);
|
|
}
|
|
case spv::Op::OpTypeMatrix: {
|
|
if (cursor.matrixStride == 0 || cursor.rowMajor) return 0;
|
|
return cursor.matrixStride * type->GetSingleWordInOperand(1);
|
|
}
|
|
case spv::Op::OpTypeArray: {
|
|
uint32_t stride = 0;
|
|
if (!TryGetDecorationLiteral(context, cursor.typeId, spv::Decoration::ArrayStride,
|
|
&stride) ||
|
|
stride == 0) {
|
|
return 0;
|
|
}
|
|
const spvtools::opt::analysis::Constant* length =
|
|
context->get_constant_mgr()->FindDeclaredConstant(type->GetSingleWordInOperand(1));
|
|
if (length == nullptr || length->AsIntConstant() == nullptr) return 0;
|
|
const uint64_t total = static_cast<uint64_t>(stride) *
|
|
static_cast<uint64_t>(length->AsIntConstant()->GetU32BitValue());
|
|
return total > kMaxBlockBytes ? 0u : static_cast<uint32_t>(total);
|
|
}
|
|
case spv::Op::OpTypeStruct: {
|
|
uint64_t end = 0;
|
|
for (uint32_t member = 0; member < type->NumInOperands(); ++member) {
|
|
uint32_t offset = 0;
|
|
if (!TryGetMemberDecorationLiteral(context, cursor.typeId, member,
|
|
spv::Decoration::Offset, &offset)) {
|
|
return 0;
|
|
}
|
|
const uint32_t size = LaidOutByteSize(context, MemberCursor(context, type, member));
|
|
if (size == 0) return 0;
|
|
end = std::max<uint64_t>(end, static_cast<uint64_t>(offset) + size);
|
|
}
|
|
return end > kMaxBlockBytes ? 0u : static_cast<uint32_t>(end);
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Whether this type decomposes into scalars the rewrite can move one word at a
|
|
// time. |leafCount| counts them, so a whole-aggregate access can be refused
|
|
// before it is expanded; passing NULL asks the SHAPE question alone - is this
|
|
// type addressable at all - and then identical array elements and vector
|
|
// components are walked once instead of once each, because the answer cannot
|
|
// differ between them and the walk of a big one would not be free.
|
|
//
|
|
// The two questions are separate because only a LOAD or a STORE expands into
|
|
// leaves, and the cap bounds one of those. How large a runtime array's element
|
|
// is says nothing about how many scalars a single access to it moves, so
|
|
// MeasureBlock asks for the shape and BuildPlans applies the cap where it
|
|
// belongs - per chain, to the type that chain actually names.
|
|
bool CanDecompose(IRContext* context, const TypeCursor& cursor, uint32_t* leafCount) {
|
|
const Instruction* type = context->get_def_use_mgr()->GetDef(cursor.typeId);
|
|
if (type == nullptr) return false;
|
|
switch (type->opcode()) {
|
|
case spv::Op::OpTypeInt:
|
|
case spv::Op::OpTypeFloat:
|
|
if (ScalarByteSize(type) == 0) return false;
|
|
if (leafCount == nullptr) return true;
|
|
++*leafCount;
|
|
return *leafCount <= kMaxLeavesPerAccess;
|
|
case spv::Op::OpTypeVector: {
|
|
TypeCursor component;
|
|
component.typeId = type->GetSingleWordInOperand(0);
|
|
const uint32_t repeats =
|
|
leafCount == nullptr ? 1u : type->GetSingleWordInOperand(1);
|
|
for (uint32_t i = 0; i < repeats; ++i) {
|
|
if (!CanDecompose(context, component, leafCount)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
case spv::Op::OpTypeMatrix: {
|
|
if (cursor.matrixStride == 0 || cursor.rowMajor ||
|
|
cursor.matrixStride % kWordBytes != 0) {
|
|
return false;
|
|
}
|
|
TypeCursor column;
|
|
column.typeId = type->GetSingleWordInOperand(0);
|
|
const uint32_t repeats =
|
|
leafCount == nullptr ? 1u : type->GetSingleWordInOperand(1);
|
|
for (uint32_t i = 0; i < repeats; ++i) {
|
|
if (!CanDecompose(context, column, leafCount)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
case spv::Op::OpTypeArray: {
|
|
uint32_t stride = 0;
|
|
if (!TryGetDecorationLiteral(context, cursor.typeId, spv::Decoration::ArrayStride,
|
|
&stride) ||
|
|
stride == 0 || stride % kWordBytes != 0) {
|
|
return false;
|
|
}
|
|
const spvtools::opt::analysis::Constant* length =
|
|
context->get_constant_mgr()->FindDeclaredConstant(type->GetSingleWordInOperand(1));
|
|
if (length == nullptr || length->AsIntConstant() == nullptr) return false;
|
|
const uint32_t count = length->AsIntConstant()->GetU32BitValue();
|
|
if (count == 0) return false;
|
|
if (leafCount != nullptr && count > kMaxLeavesPerAccess) return false;
|
|
TypeCursor element = cursor;
|
|
element.typeId = type->GetSingleWordInOperand(0);
|
|
const uint32_t repeats = leafCount == nullptr ? 1u : count;
|
|
for (uint32_t i = 0; i < repeats; ++i) {
|
|
if (!CanDecompose(context, element, leafCount)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
case spv::Op::OpTypeStruct: {
|
|
for (uint32_t member = 0; member < type->NumInOperands(); ++member) {
|
|
uint32_t offset = 0;
|
|
if (!TryGetMemberDecorationLiteral(context, cursor.typeId, member,
|
|
spv::Decoration::Offset, &offset) ||
|
|
offset % kWordBytes != 0) {
|
|
return false;
|
|
}
|
|
if (!CanDecompose(context, MemberCursor(context, type, member), leafCount)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Measures the block struct itself. A bounded block reports its laid-out byte
|
|
// size; a block whose LAST member is a runtime array - the only place GLSL lets
|
|
// one stand, and the only place SPIR-V lets a Block have one - reports the byte
|
|
// offset that array starts at and says so through |openEnded|, with the array's
|
|
// stride alongside. A runtime array anywhere else, one without a stride the
|
|
// words can step by, or one whose element the rewrite could not take apart is a
|
|
// shape this pass does not describe, and so is a bounded block it cannot size.
|
|
bool MeasureBlock(IRContext* context, const Instruction* structType, uint32_t* bytes,
|
|
bool* openEnded, uint32_t* tailStrideBytes) {
|
|
*bytes = 0;
|
|
*openEnded = false;
|
|
*tailStrideBytes = 0;
|
|
const uint32_t structId = structType->result_id();
|
|
const uint32_t memberCount = structType->NumInOperands();
|
|
uint64_t end = 0;
|
|
for (uint32_t member = 0; member < memberCount; ++member) {
|
|
uint32_t offset = 0;
|
|
if (!TryGetMemberDecorationLiteral(context, structId, member, spv::Decoration::Offset,
|
|
&offset)) {
|
|
return false;
|
|
}
|
|
const TypeCursor cursor = MemberCursor(context, structType, member);
|
|
const Instruction* type = context->get_def_use_mgr()->GetDef(cursor.typeId);
|
|
if (type == nullptr) return false;
|
|
if (type->opcode() == spv::Op::OpTypeRuntimeArray) {
|
|
if (member + 1 != memberCount) return false;
|
|
uint32_t stride = 0;
|
|
if (!TryGetDecorationLiteral(context, cursor.typeId, spv::Decoration::ArrayStride,
|
|
&stride) ||
|
|
stride == 0 || stride % kWordBytes != 0) {
|
|
return false;
|
|
}
|
|
// The member's own matrix decorations describe the array's ELEMENTS,
|
|
// exactly as they do for a bounded array of matrices. Only the shape
|
|
// is asked for: how big one element is decides nothing about how
|
|
// many scalars one access moves, and a leaf cap here would decline a
|
|
// block over a member the shader may never read whole.
|
|
TypeCursor element = cursor;
|
|
element.typeId = type->GetSingleWordInOperand(0);
|
|
if (!CanDecompose(context, element, nullptr)) return false;
|
|
// Element 0 has to start past every fixed member, or the words the
|
|
// prefix owns and the words the array owns would overlap.
|
|
if (offset < end) return false;
|
|
end = offset;
|
|
*openEnded = true;
|
|
*tailStrideBytes = stride;
|
|
break;
|
|
}
|
|
const uint32_t size = LaidOutByteSize(context, cursor);
|
|
if (size == 0) return false;
|
|
end = std::max<uint64_t>(end, static_cast<uint64_t>(offset) + size);
|
|
}
|
|
if (end > kMaxBlockBytes) return false;
|
|
*bytes = static_cast<uint32_t>(end);
|
|
return true;
|
|
}
|
|
|
|
bool TypeContainsFloat64(IRContext* context, uint32_t typeId,
|
|
std::unordered_set<uint32_t>& visiting) {
|
|
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
|
if (type == nullptr || !visiting.insert(typeId).second) return false;
|
|
switch (type->opcode()) {
|
|
case spv::Op::OpTypeFloat:
|
|
return type->GetSingleWordInOperand(0) == 64u;
|
|
case spv::Op::OpTypeVector:
|
|
case spv::Op::OpTypeMatrix:
|
|
case spv::Op::OpTypeArray:
|
|
case spv::Op::OpTypeRuntimeArray:
|
|
return TypeContainsFloat64(context, type->GetSingleWordInOperand(0), visiting);
|
|
case spv::Op::OpTypeStruct:
|
|
for (uint32_t member = 0; member < type->NumInOperands(); ++member) {
|
|
if (TypeContainsFloat64(context, type->GetSingleWordInOperand(member), visiting)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The constant an index operand names, or false when it is not one.
|
|
bool TryGetConstantIndex(IRContext* context, uint32_t id, uint32_t* value) {
|
|
const spvtools::opt::analysis::Constant* constant =
|
|
context->get_constant_mgr()->FindDeclaredConstant(id);
|
|
if (constant == nullptr || constant->AsIntConstant() == nullptr) return false;
|
|
*value = constant->AsIntConstant()->GetU32BitValue();
|
|
return true;
|
|
}
|
|
|
|
// Walks one access chain against the block's type tree, recording the byte offset
|
|
// it names as a constant part plus a list of (index, stride) terms. False for any
|
|
// shape the rewrite cannot address exactly.
|
|
bool PlanChain(IRContext* context, Instruction* chain, const TypeCursor& blockCursor,
|
|
ChainPlan* plan) {
|
|
TypeCursor cursor = blockCursor;
|
|
uint64_t constantBytes = 0;
|
|
for (uint32_t operand = 1; operand < chain->NumInOperands(); ++operand) {
|
|
const uint32_t indexId = chain->GetSingleWordInOperand(operand);
|
|
const Instruction* type = context->get_def_use_mgr()->GetDef(cursor.typeId);
|
|
if (type == nullptr) return false;
|
|
|
|
uint32_t stride = 0;
|
|
if (type->opcode() == spv::Op::OpTypeStruct) {
|
|
uint32_t member = 0;
|
|
if (!TryGetConstantIndex(context, indexId, &member) ||
|
|
member >= type->NumInOperands()) {
|
|
return false;
|
|
}
|
|
uint32_t offset = 0;
|
|
if (!TryGetMemberDecorationLiteral(context, cursor.typeId, member,
|
|
spv::Decoration::Offset, &offset) ||
|
|
offset % kWordBytes != 0) {
|
|
return false;
|
|
}
|
|
constantBytes += offset;
|
|
cursor = MemberCursor(context, type, member);
|
|
if (constantBytes > kMaxBlockBytes) return false;
|
|
continue;
|
|
}
|
|
|
|
switch (type->opcode()) {
|
|
case spv::Op::OpTypeArray:
|
|
// A runtime array steps exactly like a bounded one; only its end is
|
|
// unknown, and a chain never needs that.
|
|
case spv::Op::OpTypeRuntimeArray:
|
|
if (!TryGetDecorationLiteral(context, cursor.typeId, spv::Decoration::ArrayStride,
|
|
&stride)) {
|
|
return false;
|
|
}
|
|
cursor.typeId = type->GetSingleWordInOperand(0);
|
|
break;
|
|
case spv::Op::OpTypeMatrix:
|
|
if (cursor.rowMajor || cursor.matrixStride == 0) return false;
|
|
stride = cursor.matrixStride;
|
|
cursor.typeId = type->GetSingleWordInOperand(0);
|
|
cursor.matrixStride = 0;
|
|
break;
|
|
case spv::Op::OpTypeVector:
|
|
stride = ScalarByteSize(
|
|
context->get_def_use_mgr()->GetDef(type->GetSingleWordInOperand(0)));
|
|
cursor.typeId = type->GetSingleWordInOperand(0);
|
|
cursor.matrixStride = 0;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (stride == 0 || stride % kWordBytes != 0) return false;
|
|
uint32_t index = 0;
|
|
if (TryGetConstantIndex(context, indexId, &index)) {
|
|
constantBytes += static_cast<uint64_t>(index) * stride;
|
|
if (constantBytes > kMaxBlockBytes) return false;
|
|
} else {
|
|
plan->dynamicTerms.emplace_back(indexId, stride / kWordBytes);
|
|
}
|
|
}
|
|
|
|
if (constantBytes % kWordBytes != 0) return false;
|
|
plan->constantWords = static_cast<uint32_t>(constantBytes / kWordBytes);
|
|
plan->pointee = cursor;
|
|
return true;
|
|
}
|
|
|
|
// Everything the rewrite emits, over one module's shared scalar types.
|
|
class Emitter {
|
|
public:
|
|
// Where one block's words live: the variable holding them, the pointer type
|
|
// that reaches one, and the base index the chain resolved to.
|
|
struct Access {
|
|
uint32_t variableId = 0;
|
|
uint32_t wordPointerTypeId = 0;
|
|
uint32_t baseWordId = 0;
|
|
uint32_t memberZeroId = 0;
|
|
};
|
|
|
|
Emitter(IRContext* context, uint32_t uintTypeId, uint32_t boolTypeId, uint32_t floatTypeId)
|
|
: m_context(context), m_uintTypeId(uintTypeId), m_boolTypeId(boolTypeId),
|
|
m_floatTypeId(floatTypeId) {}
|
|
|
|
uint32_t UintConstant(uint32_t value) {
|
|
const spvtools::opt::analysis::Type* type =
|
|
m_context->get_type_mgr()->GetType(m_uintTypeId);
|
|
const spvtools::opt::analysis::Constant* constant =
|
|
m_context->get_constant_mgr()->GetConstant(type, {value});
|
|
return m_context->get_constant_mgr()->GetDefiningInstruction(constant)->result_id();
|
|
}
|
|
|
|
// The word index the chain names, materialised at the chain's own position so
|
|
// every load and store that uses it is dominated by it.
|
|
uint32_t WordIndexOf(const ChainPlan& plan) {
|
|
InstructionBuilder builder(m_context, plan.chain, kPreservedAnalyses);
|
|
uint32_t total = 0;
|
|
for (const auto& [indexId, wordsPerStep] : plan.dynamicTerms) {
|
|
uint32_t term = AsUint(builder, indexId);
|
|
if (wordsPerStep != 1u) {
|
|
term = Binary(builder, spv::Op::OpIMul, m_uintTypeId, term,
|
|
UintConstant(wordsPerStep));
|
|
}
|
|
total = total == 0 ? term
|
|
: Binary(builder, spv::Op::OpIAdd, m_uintTypeId, total, term);
|
|
}
|
|
if (total == 0) return UintConstant(plan.constantWords);
|
|
if (plan.constantWords == 0) return total;
|
|
return Binary(builder, spv::Op::OpIAdd, m_uintTypeId, total,
|
|
UintConstant(plan.constantWords));
|
|
}
|
|
|
|
// Rebuilds the value an OpLoad of `cursor` would have produced, out of the
|
|
// words that live at `baseWordId + relWords`.
|
|
uint32_t BuildValue(InstructionBuilder& builder, const Access& access,
|
|
const TypeCursor& cursor, uint32_t relWords) {
|
|
const Instruction* type = m_context->get_def_use_mgr()->GetDef(cursor.typeId);
|
|
switch (type->opcode()) {
|
|
case spv::Op::OpTypeInt:
|
|
case spv::Op::OpTypeFloat: {
|
|
if (IsDoubleType(type)) {
|
|
const uint32_t lo = LoadWord(builder, access, relWords);
|
|
const uint32_t hi = LoadWord(builder, access, relWords + 1);
|
|
const uint32_t narrowed = builder
|
|
.AddUnaryOp(m_floatTypeId, spv::Op::OpBitcast,
|
|
NarrowDoubleBits(builder, lo, hi))
|
|
->result_id();
|
|
return builder
|
|
.AddUnaryOp(cursor.typeId, spv::Op::OpFConvert, narrowed)
|
|
->result_id();
|
|
}
|
|
const uint32_t word = LoadWord(builder, access, relWords);
|
|
if (cursor.typeId == m_uintTypeId) return word;
|
|
return builder.AddUnaryOp(cursor.typeId, spv::Op::OpBitcast, word)->result_id();
|
|
}
|
|
case spv::Op::OpTypeVector: {
|
|
TypeCursor component;
|
|
component.typeId = type->GetSingleWordInOperand(0);
|
|
const uint32_t step = ComponentWords(component.typeId);
|
|
std::vector<uint32_t> parts;
|
|
for (uint32_t i = 0; i < type->GetSingleWordInOperand(1); ++i) {
|
|
parts.push_back(BuildValue(builder, access, component, relWords + i * step));
|
|
}
|
|
return builder.AddCompositeConstruct(cursor.typeId, parts)->result_id();
|
|
}
|
|
case spv::Op::OpTypeMatrix: {
|
|
TypeCursor column;
|
|
column.typeId = type->GetSingleWordInOperand(0);
|
|
const uint32_t step = cursor.matrixStride / kWordBytes;
|
|
std::vector<uint32_t> parts;
|
|
for (uint32_t i = 0; i < type->GetSingleWordInOperand(1); ++i) {
|
|
parts.push_back(BuildValue(builder, access, column, relWords + i * step));
|
|
}
|
|
return builder.AddCompositeConstruct(cursor.typeId, parts)->result_id();
|
|
}
|
|
case spv::Op::OpTypeArray: {
|
|
TypeCursor element = cursor;
|
|
element.typeId = type->GetSingleWordInOperand(0);
|
|
uint32_t stride = 0;
|
|
TryGetDecorationLiteral(m_context, cursor.typeId, spv::Decoration::ArrayStride,
|
|
&stride);
|
|
const uint32_t count = m_context->get_constant_mgr()
|
|
->FindDeclaredConstant(type->GetSingleWordInOperand(1))
|
|
->AsIntConstant()
|
|
->GetU32BitValue();
|
|
std::vector<uint32_t> parts;
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
parts.push_back(
|
|
BuildValue(builder, access, element, relWords + i * (stride / kWordBytes)));
|
|
}
|
|
return builder.AddCompositeConstruct(cursor.typeId, parts)->result_id();
|
|
}
|
|
case spv::Op::OpTypeStruct: {
|
|
std::vector<uint32_t> parts;
|
|
for (uint32_t member = 0; member < type->NumInOperands(); ++member) {
|
|
uint32_t offset = 0;
|
|
TryGetMemberDecorationLiteral(m_context, cursor.typeId, member,
|
|
spv::Decoration::Offset, &offset);
|
|
parts.push_back(BuildValue(builder, access, MemberCursor(m_context, type, member),
|
|
relWords + offset / kWordBytes));
|
|
}
|
|
return builder.AddCompositeConstruct(cursor.typeId, parts)->result_id();
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// The mirror image: writes `valueId` into the words at
|
|
// `baseWordId + relWords`. `path` is the composite-extract index list that
|
|
// reaches the part being written, empty at the root.
|
|
void StoreValue(InstructionBuilder& builder, const Access& access,
|
|
const TypeCursor& cursor, uint32_t relWords, uint32_t rootValueId,
|
|
std::vector<uint32_t>& path) {
|
|
const Instruction* type = m_context->get_def_use_mgr()->GetDef(cursor.typeId);
|
|
switch (type->opcode()) {
|
|
case spv::Op::OpTypeInt:
|
|
case spv::Op::OpTypeFloat: {
|
|
const uint32_t leaf = Extract(builder, cursor.typeId, rootValueId, path);
|
|
if (IsDoubleType(type)) {
|
|
const uint32_t narrowed =
|
|
builder.AddUnaryOp(m_floatTypeId, spv::Op::OpFConvert, leaf)->result_id();
|
|
const uint32_t bits =
|
|
builder.AddUnaryOp(m_uintTypeId, spv::Op::OpBitcast, narrowed)->result_id();
|
|
uint32_t lo = 0;
|
|
uint32_t hi = 0;
|
|
WidenFloatBits(builder, bits, &lo, &hi);
|
|
StoreWord(builder, access, relWords, lo);
|
|
StoreWord(builder, access, relWords + 1, hi);
|
|
return;
|
|
}
|
|
const uint32_t word =
|
|
cursor.typeId == m_uintTypeId
|
|
? leaf
|
|
: builder.AddUnaryOp(m_uintTypeId, spv::Op::OpBitcast, leaf)->result_id();
|
|
StoreWord(builder, access, relWords, word);
|
|
return;
|
|
}
|
|
case spv::Op::OpTypeVector: {
|
|
TypeCursor component;
|
|
component.typeId = type->GetSingleWordInOperand(0);
|
|
const uint32_t step = ComponentWords(component.typeId);
|
|
for (uint32_t i = 0; i < type->GetSingleWordInOperand(1); ++i) {
|
|
path.push_back(i);
|
|
StoreValue(builder, access, component, relWords + i * step, rootValueId, path);
|
|
path.pop_back();
|
|
}
|
|
return;
|
|
}
|
|
case spv::Op::OpTypeMatrix: {
|
|
TypeCursor column;
|
|
column.typeId = type->GetSingleWordInOperand(0);
|
|
const uint32_t step = cursor.matrixStride / kWordBytes;
|
|
for (uint32_t i = 0; i < type->GetSingleWordInOperand(1); ++i) {
|
|
path.push_back(i);
|
|
StoreValue(builder, access, column, relWords + i * step, rootValueId, path);
|
|
path.pop_back();
|
|
}
|
|
return;
|
|
}
|
|
case spv::Op::OpTypeArray: {
|
|
TypeCursor element = cursor;
|
|
element.typeId = type->GetSingleWordInOperand(0);
|
|
uint32_t stride = 0;
|
|
TryGetDecorationLiteral(m_context, cursor.typeId, spv::Decoration::ArrayStride,
|
|
&stride);
|
|
const uint32_t count = m_context->get_constant_mgr()
|
|
->FindDeclaredConstant(type->GetSingleWordInOperand(1))
|
|
->AsIntConstant()
|
|
->GetU32BitValue();
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
path.push_back(i);
|
|
StoreValue(builder, access, element, relWords + i * (stride / kWordBytes),
|
|
rootValueId, path);
|
|
path.pop_back();
|
|
}
|
|
return;
|
|
}
|
|
case spv::Op::OpTypeStruct: {
|
|
for (uint32_t member = 0; member < type->NumInOperands(); ++member) {
|
|
uint32_t offset = 0;
|
|
TryGetMemberDecorationLiteral(m_context, cursor.typeId, member,
|
|
spv::Decoration::Offset, &offset);
|
|
path.push_back(member);
|
|
StoreValue(builder, access, MemberCursor(m_context, type, member),
|
|
relWords + offset / kWordBytes, rootValueId, path);
|
|
path.pop_back();
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Replaces an OpArrayLength of an open-ended block with the element count
|
|
// of the ORIGINAL runtime array. The instruction now counts the words of
|
|
// the flattened `uint[]`, so the length is `(words - prefix) / stride`, in
|
|
// unsigned arithmetic and clamped at zero when the bound range does not
|
|
// even reach the array's offset - a wrapped subtraction would otherwise
|
|
// report a few billion elements. The division floors, which is what GL
|
|
// defines `.length()` as for a range that is not a whole number of
|
|
// elements. A fresh OpArrayLength is issued rather than the old one re-aimed,
|
|
// so the uses being redirected are never the ones the arithmetic just made.
|
|
void RewriteArrayLength(Instruction* arrayLength, uint32_t prefixWords, uint32_t strideWords) {
|
|
InstructionBuilder builder(m_context, arrayLength, kPreservedAnalyses);
|
|
const uint32_t variableId = arrayLength->GetSingleWordInOperand(0);
|
|
const uint32_t wordsId = m_context->TakeNextId();
|
|
builder.AddInstruction(MakeUnique<Instruction>(
|
|
m_context, spv::Op::OpArrayLength, m_uintTypeId, wordsId,
|
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {variableId}},
|
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}}}));
|
|
uint32_t count = wordsId;
|
|
if (prefixWords != 0) {
|
|
const uint32_t prefixId = UintConstant(prefixWords);
|
|
const uint32_t past = Binary(builder, spv::Op::OpISub, m_uintTypeId, count, prefixId);
|
|
const uint32_t tooShort =
|
|
Binary(builder, spv::Op::OpULessThan, m_boolTypeId, count, prefixId);
|
|
count = Select(builder, tooShort, UintConstant(0), past);
|
|
}
|
|
if (strideWords != 1) {
|
|
count = Binary(builder, spv::Op::OpUDiv, m_uintTypeId, count,
|
|
UintConstant(strideWords));
|
|
}
|
|
m_context->ReplaceAllUsesWith(arrayLength->result_id(), count);
|
|
m_context->KillInst(arrayLength);
|
|
}
|
|
|
|
private:
|
|
uint32_t ComponentWords(uint32_t componentTypeId) {
|
|
return ScalarByteSize(m_context->get_def_use_mgr()->GetDef(componentTypeId)) /
|
|
kWordBytes;
|
|
}
|
|
|
|
uint32_t Binary(InstructionBuilder& builder, spv::Op opcode, uint32_t typeId, uint32_t a,
|
|
uint32_t b) {
|
|
return builder.AddBinaryOp(typeId, opcode, a, b)->result_id();
|
|
}
|
|
|
|
uint32_t Select(InstructionBuilder& builder, uint32_t condition, uint32_t whenTrue,
|
|
uint32_t whenFalse) {
|
|
return builder
|
|
.AddTernaryOp(m_uintTypeId, spv::Op::OpSelect, condition, whenTrue, whenFalse)
|
|
->result_id();
|
|
}
|
|
|
|
uint32_t AsUint(InstructionBuilder& builder, uint32_t valueId) {
|
|
const Instruction* def = m_context->get_def_use_mgr()->GetDef(valueId);
|
|
if (def != nullptr && def->type_id() == m_uintTypeId) return valueId;
|
|
return builder.AddUnaryOp(m_uintTypeId, spv::Op::OpBitcast, valueId)->result_id();
|
|
}
|
|
|
|
// `base + words`, folding away the add when there is nothing to add.
|
|
uint32_t Offset(InstructionBuilder& builder, uint32_t baseWordId, uint32_t words) {
|
|
if (words == 0) return baseWordId;
|
|
return Binary(builder, spv::Op::OpIAdd, m_uintTypeId, baseWordId, UintConstant(words));
|
|
}
|
|
|
|
uint32_t LoadWord(InstructionBuilder& builder, const Access& access, uint32_t words) {
|
|
const uint32_t indexId = Offset(builder, access.baseWordId, words);
|
|
Instruction* pointer = builder.AddAccessChain(
|
|
access.wordPointerTypeId, access.variableId, {access.memberZeroId, indexId});
|
|
return builder.AddLoad(m_uintTypeId, pointer->result_id())->result_id();
|
|
}
|
|
|
|
void StoreWord(InstructionBuilder& builder, const Access& access, uint32_t words,
|
|
uint32_t valueId) {
|
|
const uint32_t indexId = Offset(builder, access.baseWordId, words);
|
|
Instruction* pointer = builder.AddAccessChain(
|
|
access.wordPointerTypeId, access.variableId, {access.memberZeroId, indexId});
|
|
builder.AddStore(pointer->result_id(), valueId);
|
|
}
|
|
|
|
// One scalar of the value being stored. An empty path IS the value.
|
|
uint32_t Extract(InstructionBuilder& builder, uint32_t typeId, uint32_t rootValueId,
|
|
const std::vector<uint32_t>& path) {
|
|
if (path.empty()) return rootValueId;
|
|
return builder.AddCompositeExtract(typeId, rootValueId, path)->result_id();
|
|
}
|
|
|
|
// binary64 word pair -> the binary32 bit pattern nearest it, truncating the
|
|
// mantissa bits binary32 cannot hold. Straight-line by construction: every
|
|
// case is an OpSelect, so this needs no control flow and never splits a block.
|
|
uint32_t NarrowDoubleBits(InstructionBuilder& builder, uint32_t lo, uint32_t hi) {
|
|
const uint32_t sign = Binary(builder, spv::Op::OpBitwiseAnd, m_uintTypeId, hi,
|
|
UintConstant(0x80000000u));
|
|
const uint32_t exponent = Binary(
|
|
builder, spv::Op::OpBitwiseAnd, m_uintTypeId,
|
|
Binary(builder, spv::Op::OpShiftRightLogical, m_uintTypeId, hi, UintConstant(20)),
|
|
UintConstant(0x7FFu));
|
|
const uint32_t significandHigh = Binary(builder, spv::Op::OpBitwiseAnd, m_uintTypeId,
|
|
hi, UintConstant(0xFFFFFu));
|
|
// The 23 bits binary32 keeps: 20 from the high word, 3 from the low one.
|
|
const uint32_t significand = Binary(
|
|
builder, spv::Op::OpBitwiseOr, m_uintTypeId,
|
|
Binary(builder, spv::Op::OpShiftLeftLogical, m_uintTypeId, significandHigh,
|
|
UintConstant(3)),
|
|
Binary(builder, spv::Op::OpShiftRightLogical, m_uintTypeId, lo, UintConstant(29)));
|
|
|
|
// 1023 - 127 = 896, so the binary32 exponent field is `exponent - 896` and
|
|
// every bound on it is an unsigned comparison against that bias.
|
|
const uint32_t normalBits = Binary(
|
|
builder, spv::Op::OpBitwiseOr, m_uintTypeId, sign,
|
|
Binary(builder, spv::Op::OpBitwiseOr, m_uintTypeId,
|
|
Binary(builder, spv::Op::OpShiftLeftLogical, m_uintTypeId,
|
|
Binary(builder, spv::Op::OpISub, m_uintTypeId, exponent,
|
|
UintConstant(896)),
|
|
UintConstant(23)),
|
|
significand));
|
|
const uint32_t infinityBits = Binary(builder, spv::Op::OpBitwiseOr, m_uintTypeId, sign,
|
|
UintConstant(0x7F800000u));
|
|
// A quiet NaN that stays one even when every significant bit sat in the 29
|
|
// low bits binary32 discards.
|
|
const uint32_t nanBits =
|
|
Binary(builder, spv::Op::OpBitwiseOr, m_uintTypeId, infinityBits,
|
|
Binary(builder, spv::Op::OpBitwiseOr, m_uintTypeId,
|
|
UintConstant(0x400000u), significand));
|
|
|
|
const uint32_t significandIsZero =
|
|
Binary(builder, spv::Op::OpIEqual, m_boolTypeId,
|
|
Binary(builder, spv::Op::OpBitwiseOr, m_uintTypeId, significandHigh, lo),
|
|
UintConstant(0));
|
|
const uint32_t maxExponentBits =
|
|
Select(builder, significandIsZero, infinityBits, nanBits);
|
|
|
|
const uint32_t isZeroExponent =
|
|
Binary(builder, spv::Op::OpIEqual, m_boolTypeId, exponent, UintConstant(0));
|
|
const uint32_t isMaxExponent =
|
|
Binary(builder, spv::Op::OpIEqual, m_boolTypeId, exponent, UintConstant(0x7FFu));
|
|
// <= 896 is every magnitude binary32 could hold only as a subnormal;
|
|
// >= 1151 is every one it cannot hold at all.
|
|
const uint32_t underflows = Binary(builder, spv::Op::OpULessThanEqual, m_boolTypeId,
|
|
exponent, UintConstant(896));
|
|
const uint32_t overflows = Binary(builder, spv::Op::OpUGreaterThanEqual, m_boolTypeId,
|
|
exponent, UintConstant(1151));
|
|
|
|
uint32_t bits = Select(builder, overflows, infinityBits, normalBits);
|
|
bits = Select(builder, underflows, sign, bits);
|
|
bits = Select(builder, isMaxExponent, maxExponentBits, bits);
|
|
return Select(builder, isZeroExponent, sign, bits);
|
|
}
|
|
|
|
// The inverse: a binary32 bit pattern -> the binary64 word pair for it.
|
|
void WidenFloatBits(InstructionBuilder& builder, uint32_t bits, uint32_t* lo,
|
|
uint32_t* hi) {
|
|
const uint32_t sign = Binary(builder, spv::Op::OpBitwiseAnd, m_uintTypeId, bits,
|
|
UintConstant(0x80000000u));
|
|
const uint32_t exponent = Binary(
|
|
builder, spv::Op::OpBitwiseAnd, m_uintTypeId,
|
|
Binary(builder, spv::Op::OpShiftRightLogical, m_uintTypeId, bits, UintConstant(23)),
|
|
UintConstant(0xFFu));
|
|
const uint32_t significand = Binary(builder, spv::Op::OpBitwiseAnd, m_uintTypeId, bits,
|
|
UintConstant(0x7FFFFFu));
|
|
const uint32_t significandHigh = Binary(builder, spv::Op::OpShiftRightLogical,
|
|
m_uintTypeId, significand, UintConstant(3));
|
|
const uint32_t significandLow = Binary(builder, spv::Op::OpShiftLeftLogical,
|
|
m_uintTypeId, significand, UintConstant(29));
|
|
|
|
const uint32_t normalHigh = Binary(
|
|
builder, spv::Op::OpBitwiseOr, m_uintTypeId, sign,
|
|
Binary(builder, spv::Op::OpBitwiseOr, m_uintTypeId,
|
|
Binary(builder, spv::Op::OpShiftLeftLogical, m_uintTypeId,
|
|
Binary(builder, spv::Op::OpIAdd, m_uintTypeId, exponent,
|
|
UintConstant(896)),
|
|
UintConstant(20)),
|
|
significandHigh));
|
|
const uint32_t maxHigh = Binary(builder, spv::Op::OpBitwiseOr, m_uintTypeId, sign,
|
|
Binary(builder, spv::Op::OpBitwiseOr, m_uintTypeId,
|
|
UintConstant(0x7FF00000u), significandHigh));
|
|
|
|
const uint32_t isZeroExponent =
|
|
Binary(builder, spv::Op::OpIEqual, m_boolTypeId, exponent, UintConstant(0));
|
|
const uint32_t isMaxExponent =
|
|
Binary(builder, spv::Op::OpIEqual, m_boolTypeId, exponent, UintConstant(0xFFu));
|
|
|
|
// A binary32 subnormal is below every binary64 this can name without a
|
|
// normalising loop, so it becomes the signed zero it is nearest to.
|
|
uint32_t high = Select(builder, isMaxExponent, maxHigh, normalHigh);
|
|
*hi = Select(builder, isZeroExponent, sign, high);
|
|
*lo = Select(builder, isZeroExponent, UintConstant(0), significandLow);
|
|
}
|
|
|
|
IRContext* m_context;
|
|
uint32_t m_uintTypeId;
|
|
uint32_t m_boolTypeId;
|
|
uint32_t m_floatTypeId;
|
|
};
|
|
// --- the module-level rewrite ------------------------------------------------
|
|
|
|
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 - or, for an open-ended block, a
|
|
// `uint[]` runtime array with the same stride and no length at all - 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 or OpTypeRuntimeArray 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; an open-ended block has no length constant to
|
|
// place, so that reason cannot reach it.
|
|
//
|
|
// The `uint` element type is a different matter, and only for an OPEN-ENDED
|
|
// block. The front end declares types in first-use order, so a block that is the
|
|
// first thing a shader touches sits BEFORE the module's `uint` (or the module has
|
|
// none, and the one the pass asked for was appended at the end). Declining there
|
|
// would send exactly the buffers this rewrite exists for back to the demotion on
|
|
// nothing but where they stand in the source. OpTypeInt has no operands, and
|
|
// nothing that names it can precede where it was, so moving it up in front of the
|
|
// block is always legal. A BOUNDED block keeps declining instead: that is what it
|
|
// has always done, and widening it is a change to a path this one does not need.
|
|
//
|
|
// NOTHING IS WRITTEN until every reason to decline has been ruled out, so a block
|
|
// this returns 0 for leaves the module as it found it - which is what lets
|
|
// Process() truthfully report SuccessWithoutChange for a module of only those.
|
|
uint32_t CreateWordArrayTypeBefore(IRContext* context, Instruction* structType,
|
|
uint32_t uintTypeId, uint32_t length, bool openEnded) {
|
|
Instruction* uintType = context->get_def_use_mgr()->GetDef(uintTypeId);
|
|
if (uintType == nullptr || uintType->opcode() != spv::Op::OpTypeInt) return 0;
|
|
const bool hoistUint = !DeclaredBefore(context, uintTypeId, structType->result_id());
|
|
if (hoistUint && !openEnded) return 0;
|
|
|
|
uint32_t lengthConstantId = 0;
|
|
if (!openEnded) {
|
|
auto* constantMgr = context->get_constant_mgr();
|
|
const spvtools::opt::analysis::Type* uintDescriptor =
|
|
context->get_type_mgr()->GetType(uintTypeId);
|
|
if (uintDescriptor == nullptr) return 0;
|
|
const spvtools::opt::analysis::Constant* lengthConstant =
|
|
constantMgr->GetConstant(uintDescriptor, {length});
|
|
if (lengthConstant == nullptr) return 0;
|
|
|
|
Module::inst_iterator position = PositionOf(context, structType);
|
|
if (position == context->types_values_end()) return 0;
|
|
// Created in front of the block when it is not there yet, so the only way
|
|
// this declines is a constant the module already declares after it.
|
|
Instruction* lengthInst =
|
|
constantMgr->GetDefiningInstruction(lengthConstant, 0, &position);
|
|
if (lengthInst == nullptr) return 0;
|
|
if (!DeclaredBefore(context, lengthInst->result_id(), structType->result_id())) return 0;
|
|
lengthConstantId = lengthInst->result_id();
|
|
}
|
|
|
|
const uint32_t arrayTypeId = context->TakeNextId();
|
|
if (arrayTypeId == 0) return 0;
|
|
|
|
if (hoistUint) uintType->InsertBefore(structType);
|
|
std::unique_ptr<Instruction> arrayType =
|
|
openEnded
|
|
? MakeUnique<Instruction>(
|
|
context, spv::Op::OpTypeRuntimeArray, 0, arrayTypeId,
|
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {uintTypeId}}})
|
|
: MakeUnique<Instruction>(
|
|
context, spv::Op::OpTypeArray, 0, arrayTypeId,
|
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {uintTypeId}},
|
|
{SPV_OPERAND_TYPE_ID, {lengthConstantId}}});
|
|
Instruction* inserted = structType->InsertBefore(std::move(arrayType));
|
|
context->AnalyzeDefUse(inserted);
|
|
context->get_decoration_mgr()->AddDecorationVal(
|
|
arrayTypeId, static_cast<uint32_t>(spv::Decoration::ArrayStride), kWordBytes);
|
|
return arrayTypeId;
|
|
}
|
|
|
|
// The access qualifiers the collapsed member has to keep. Coherent and Volatile
|
|
// are taken from ANY member that had them - dropping one could lose a write
|
|
// another invocation has to see - while Restrict, NonWritable and NonReadable are
|
|
// taken only from ALL of them, because each is a promise the shader would break
|
|
// if one member never made it.
|
|
std::vector<spv::Decoration> SurvivingAccessQualifiers(IRContext* context, uint32_t structId,
|
|
uint32_t memberCount) {
|
|
static constexpr spv::Decoration kAny[] = {spv::Decoration::Coherent,
|
|
spv::Decoration::Volatile};
|
|
static constexpr spv::Decoration kAll[] = {spv::Decoration::Restrict,
|
|
spv::Decoration::NonWritable,
|
|
spv::Decoration::NonReadable};
|
|
std::vector<spv::Decoration> surviving;
|
|
for (const spv::Decoration kind : kAny) {
|
|
for (uint32_t member = 0; member < memberCount; ++member) {
|
|
if (HasMemberDecoration(context, structId, member, kind)) {
|
|
surviving.push_back(kind);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
for (const spv::Decoration kind : kAll) {
|
|
bool all = memberCount > 0;
|
|
for (uint32_t member = 0; member < memberCount && all; ++member) {
|
|
all = HasMemberDecoration(context, structId, member, kind);
|
|
}
|
|
if (all) surviving.push_back(kind);
|
|
}
|
|
return surviving;
|
|
}
|
|
|
|
// Drops every OpMemberDecorate and OpMemberName the collapsed struct no longer has
|
|
// a member for - which is all of them, since the one member it keeps is a
|
|
// different thing entirely from the one that used to be member 0.
|
|
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) 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;
|
|
doomed.push_back(&debug);
|
|
}
|
|
for (Instruction* inst : doomed) context->KillInst(inst);
|
|
}
|
|
|
|
// Plans every shader storage block in the module that holds a 64-bit float and
|
|
// that this pass can rewrite exactly. Reads the module and never writes it.
|
|
std::vector<BlockPlan> BuildPlans(IRContext* context) {
|
|
std::vector<BlockPlan> plans;
|
|
auto* defUseMgr = context->get_def_use_mgr();
|
|
|
|
// Declaration order, so a module with two candidate blocks is rewritten the
|
|
// same way every time it is compiled.
|
|
std::vector<uint32_t> structOrder;
|
|
std::unordered_map<uint32_t, std::vector<Instruction*>> variablesByStruct;
|
|
std::unordered_map<uint32_t, uint32_t> storageClassByStruct;
|
|
for (Instruction& inst : context->module()->types_values()) {
|
|
if (inst.opcode() != spv::Op::OpVariable || inst.NumInOperands() < 1) continue;
|
|
const uint32_t storageClass = inst.GetSingleWordInOperand(0);
|
|
const bool isStorageBufferClass =
|
|
storageClass == static_cast<uint32_t>(spv::StorageClass::StorageBuffer);
|
|
const bool isUniformClass =
|
|
storageClass == static_cast<uint32_t>(spv::StorageClass::Uniform);
|
|
if (!isStorageBufferClass && !isUniformClass) continue;
|
|
|
|
Instruction* pointerType = defUseMgr->GetDef(inst.type_id());
|
|
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) continue;
|
|
const uint32_t structId = pointerType->GetSingleWordInOperand(1);
|
|
Instruction* structType = defUseMgr->GetDef(structId);
|
|
if (structType == nullptr || structType->opcode() != spv::Op::OpTypeStruct ||
|
|
structType->NumInOperands() == 0) {
|
|
continue;
|
|
}
|
|
// A shader storage block is spelled Block + StorageBuffer from SPIR-V 1.3
|
|
// and BufferBlock + Uniform before it; a plain UNIFORM block is neither,
|
|
// and is deliberately left to the demotion - the frontend's own uniform
|
|
// routing reflects the module that pass produces.
|
|
const bool isStorageBlock =
|
|
(isStorageBufferClass && HasDecoration(context, structId, spv::Decoration::Block)) ||
|
|
(isUniformClass && HasDecoration(context, structId, spv::Decoration::BufferBlock));
|
|
if (!isStorageBlock) continue;
|
|
|
|
std::unordered_set<uint32_t> visiting;
|
|
if (!TypeContainsFloat64(context, structId, visiting)) continue;
|
|
|
|
if (variablesByStruct.find(structId) == variablesByStruct.end()) {
|
|
structOrder.push_back(structId);
|
|
storageClassByStruct[structId] = storageClass;
|
|
}
|
|
variablesByStruct[structId].push_back(&inst);
|
|
}
|
|
|
|
// A struct type is not owned by the variables that happen to be storage blocks:
|
|
// if ANY other variable points at the same one, collapsing it would rewrite a
|
|
// declaration this pass never looked at. Count every variable of every struct
|
|
// and require the two counts to agree.
|
|
std::unordered_map<uint32_t, uint32_t> variableCountByStruct;
|
|
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;
|
|
++variableCountByStruct[pointerType->GetSingleWordInOperand(1)];
|
|
}
|
|
for (auto it = structOrder.begin(); it != structOrder.end();) {
|
|
if (variableCountByStruct[*it] == variablesByStruct[*it].size()) {
|
|
++it;
|
|
continue;
|
|
}
|
|
MGLOG_D("[spirv] storage block %%%u holds a double but its struct type is shared "
|
|
"with a declaration that is not one; left to the fp64 demotion",
|
|
*it);
|
|
it = structOrder.erase(it);
|
|
}
|
|
|
|
for (const uint32_t structId : structOrder) {
|
|
Instruction* structType = defUseMgr->GetDef(structId);
|
|
TypeCursor blockCursor;
|
|
blockCursor.typeId = structId;
|
|
uint32_t blockBytes = 0;
|
|
bool openEnded = false;
|
|
uint32_t tailStrideBytes = 0;
|
|
// An open-ended block whose runtime array is its only member measures a
|
|
// prefix of 0 bytes and is perfectly describable; only a BOUNDED block of
|
|
// no bytes is not, and MeasureBlock already refuses to size one of those.
|
|
if (!MeasureBlock(context, structType, &blockBytes, &openEnded, &tailStrideBytes) ||
|
|
blockBytes % kWordBytes != 0 || (!openEnded && blockBytes == 0)) {
|
|
MGLOG_D("[spirv] storage block %%%u holds a double but its byte layout cannot be "
|
|
"described exactly; left to the fp64 demotion",
|
|
structId);
|
|
continue;
|
|
}
|
|
|
|
BlockPlan plan;
|
|
plan.structType = structType;
|
|
plan.storageClass = storageClassByStruct[structId];
|
|
plan.wordCount = blockBytes / kWordBytes;
|
|
plan.openEnded = openEnded;
|
|
plan.tailStrideWords = tailStrideBytes / kWordBytes;
|
|
const uint32_t lastMember = structType->NumInOperands() - 1;
|
|
|
|
bool expressible = true;
|
|
for (Instruction* variable : variablesByStruct[structId]) {
|
|
std::vector<Instruction*> chains;
|
|
std::unordered_set<uint32_t> seenChains;
|
|
std::unordered_set<uint32_t> seenLengths;
|
|
defUseMgr->ForEachUser(variable, [&](Instruction* user) {
|
|
if (!expressible) return;
|
|
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() >= 1 &&
|
|
user->GetSingleWordInOperand(0) == variable->result_id()) {
|
|
if (seenChains.insert(user->result_id()).second) chains.push_back(user);
|
|
return;
|
|
}
|
|
expressible = false;
|
|
return;
|
|
case spv::Op::OpArrayLength: {
|
|
// Only an open-ended block has a length to ask for, and
|
|
// only of its last member; the result has to be the 32-bit
|
|
// uint the rewrite's arithmetic is typed in, which is the
|
|
// only result type the instruction allows anyway.
|
|
const Instruction* resultType = defUseMgr->GetDef(user->type_id());
|
|
const bool isUint = resultType != nullptr &&
|
|
resultType->opcode() == spv::Op::OpTypeInt &&
|
|
resultType->GetSingleWordInOperand(0) == 32u &&
|
|
resultType->GetSingleWordInOperand(1) == 0u;
|
|
if (openEnded && isUint && user->NumInOperands() >= 2 &&
|
|
user->GetSingleWordInOperand(0) == variable->result_id() &&
|
|
user->GetSingleWordInOperand(1) == lastMember) {
|
|
if (seenLengths.insert(user->result_id()).second) {
|
|
plan.arrayLengths.push_back(user);
|
|
}
|
|
return;
|
|
}
|
|
expressible = false;
|
|
return;
|
|
}
|
|
default:
|
|
expressible = false;
|
|
return;
|
|
}
|
|
});
|
|
if (!expressible) break;
|
|
|
|
for (Instruction* chain : chains) {
|
|
ChainPlan chainPlan;
|
|
chainPlan.chain = chain;
|
|
chainPlan.variableId = variable->result_id();
|
|
if (!PlanChain(context, chain, blockCursor, &chainPlan)) {
|
|
expressible = false;
|
|
break;
|
|
}
|
|
uint32_t leafCount = 0;
|
|
if (!CanDecompose(context, chainPlan.pointee, &leafCount)) {
|
|
expressible = false;
|
|
break;
|
|
}
|
|
std::unordered_set<uint32_t> seenUses;
|
|
defUseMgr->ForEachUser(chain, [&](Instruction* user) {
|
|
if (!expressible) return;
|
|
if (user->opcode() == spv::Op::OpLoad &&
|
|
user->GetSingleWordInOperand(0) == chain->result_id()) {
|
|
if (seenUses.insert(user->unique_id()).second) {
|
|
chainPlan.loads.push_back(user);
|
|
}
|
|
return;
|
|
}
|
|
if (user->opcode() == spv::Op::OpStore && user->NumInOperands() >= 2 &&
|
|
user->GetSingleWordInOperand(0) == chain->result_id() &&
|
|
user->GetSingleWordInOperand(1) != chain->result_id()) {
|
|
if (seenUses.insert(user->unique_id()).second) {
|
|
chainPlan.stores.push_back(user);
|
|
}
|
|
return;
|
|
}
|
|
expressible = false;
|
|
});
|
|
if (!expressible) break;
|
|
plan.chains.push_back(std::move(chainPlan));
|
|
}
|
|
if (!expressible) break;
|
|
}
|
|
if (!expressible) {
|
|
MGLOG_D("[spirv] storage block %%%u holds a double but is reached in a way this "
|
|
"pass cannot re-address; left to the fp64 demotion",
|
|
structId);
|
|
continue;
|
|
}
|
|
plans.push_back(std::move(plan));
|
|
}
|
|
return plans;
|
|
}
|
|
} // namespace
|
|
|
|
spvtools::opt::Pass::Status FlattenFloat64StorageBlockPass::Process() {
|
|
auto* irContext = context();
|
|
std::vector<BlockPlan> plans = BuildPlans(irContext);
|
|
if (plans.empty()) {
|
|
return Status::SuccessWithoutChange;
|
|
}
|
|
|
|
spvtools::opt::analysis::Integer uintDescriptor(32, false);
|
|
spvtools::opt::analysis::Bool boolDescriptor;
|
|
spvtools::opt::analysis::Float floatDescriptor(32);
|
|
const uint32_t uintTypeId = irContext->get_type_mgr()->GetTypeInstruction(&uintDescriptor);
|
|
const uint32_t boolTypeId = irContext->get_type_mgr()->GetTypeInstruction(&boolDescriptor);
|
|
const uint32_t floatTypeId = irContext->get_type_mgr()->GetTypeInstruction(&floatDescriptor);
|
|
if (uintTypeId == 0 || boolTypeId == 0 || floatTypeId == 0) {
|
|
return Status::SuccessWithoutChange;
|
|
}
|
|
|
|
Emitter emitter(irContext, uintTypeId, boolTypeId, floatTypeId);
|
|
bool modified = false;
|
|
// Every block declines before anything is written for it, so |touched| only ever
|
|
// parts company with |modified| on a shape that cannot happen without the module
|
|
// running out of ids - and even then the status must not claim the bytes are
|
|
// untouched, because the caller relies on that to skip invalidating its analyses.
|
|
bool touched = false;
|
|
for (BlockPlan& plan : plans) {
|
|
const uint32_t structId = plan.structType->result_id();
|
|
const uint32_t arrayTypeId = CreateWordArrayTypeBefore(
|
|
irContext, plan.structType, uintTypeId, plan.wordCount, plan.openEnded);
|
|
if (arrayTypeId == 0) {
|
|
// Nothing was written for it, so the module is still the one that came in.
|
|
MGLOG_D("[spirv] storage block %%%u: no legal place for the flattened word array; "
|
|
"left to the fp64 demotion",
|
|
structId);
|
|
continue;
|
|
}
|
|
// Past this point the module HAS been written to, so an abandoned block would
|
|
// leave a dead type behind - the status has to say so even then.
|
|
touched = true;
|
|
const uint32_t wordPointerTypeId = irContext->get_type_mgr()->FindPointerToType(
|
|
uintTypeId, static_cast<spv::StorageClass>(plan.storageClass));
|
|
if (wordPointerTypeId == 0) continue;
|
|
const uint32_t memberZeroId = emitter.UintConstant(0);
|
|
|
|
for (ChainPlan& chainPlan : plan.chains) {
|
|
Emitter::Access access;
|
|
access.variableId = chainPlan.variableId;
|
|
access.wordPointerTypeId = wordPointerTypeId;
|
|
access.memberZeroId = memberZeroId;
|
|
access.baseWordId = emitter.WordIndexOf(chainPlan);
|
|
|
|
for (Instruction* load : chainPlan.loads) {
|
|
InstructionBuilder builder(irContext, load, kPreservedAnalyses);
|
|
const uint32_t rebuilt = emitter.BuildValue(builder, access, chainPlan.pointee, 0);
|
|
irContext->ReplaceAllUsesWith(load->result_id(), rebuilt);
|
|
irContext->KillInst(load);
|
|
}
|
|
for (Instruction* store : chainPlan.stores) {
|
|
InstructionBuilder builder(irContext, store, kPreservedAnalyses);
|
|
std::vector<uint32_t> path;
|
|
emitter.StoreValue(builder, access, chainPlan.pointee, 0,
|
|
store->GetSingleWordInOperand(1), path);
|
|
irContext->KillInst(store);
|
|
}
|
|
irContext->KillInst(chainPlan.chain);
|
|
}
|
|
for (Instruction* arrayLength : plan.arrayLengths) {
|
|
emitter.RewriteArrayLength(arrayLength, plan.wordCount, plan.tailStrideWords);
|
|
}
|
|
|
|
const std::vector<spv::Decoration> surviving = SurvivingAccessQualifiers(
|
|
irContext, structId, plan.structType->NumInOperands());
|
|
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);
|
|
for (const spv::Decoration kind : surviving) {
|
|
// AddMemberDecoration always carries a literal value; these have none, so
|
|
// the instruction has to be spelled out.
|
|
irContext->get_decoration_mgr()->AddDecoration(
|
|
spv::Op::OpMemberDecorate,
|
|
{{SPV_OPERAND_TYPE_ID, {structId}},
|
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}},
|
|
{SPV_OPERAND_TYPE_DECORATION, {static_cast<uint32_t>(kind)}}});
|
|
}
|
|
modified = true;
|
|
if (plan.openEnded) {
|
|
MGLOG_D("[spirv] storage block %%%u: flattened into an open-ended word array (%u-word "
|
|
"prefix, %u-word elements) so its 64-bit members keep the byte layout the "
|
|
"application bound",
|
|
structId, plan.wordCount, plan.tailStrideWords);
|
|
} else {
|
|
MGLOG_D("[spirv] storage block %%%u: flattened into %u words so its 64-bit members "
|
|
"keep the byte layout the application bound",
|
|
structId, plan.wordCount);
|
|
}
|
|
}
|
|
|
|
if (!modified && !touched) {
|
|
return Status::SuccessWithoutChange;
|
|
}
|
|
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
|
return Status::SuccessWithChange;
|
|
}
|
|
|
|
spvtools::Optimizer::PassToken
|
|
FlattenFloat64StorageBlockPass::CreateFlattenFloat64StorageBlockPass() {
|
|
return spvtools::Optimizer::PassToken(MakeUnique<FlattenFloat64StorageBlockPass>());
|
|
}
|
|
} // namespace ShaderTranspiler
|
|
} // namespace MG_Util
|
|
} // namespace MobileGL
|