mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-23 00:53:02 -05:00
SpecializeSpirv kept its per-id data (folded values, result types, scalar types, removed ids, blocks, edges, copies, definitions and the live sets) in std::map and std::set, and repeats fold, prune and copy propagation over the whole module until nothing changes. Ids are dense below the header's bound, so the tables become vectors indexed by id. Lookups of operands that are literals rather than ids stay bounds-checked, iteration keeps ascending id order, and an id at or beyond the bound now throws.
321 lines
16 KiB
C++
321 lines
16 KiB
C++
#include "SpirvBackend/SpirvModule.hpp"
|
|
#include "SpirvBackend/SpirvSpecialization.hpp"
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <exception>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <spirv/unified1/spirv.hpp>
|
|
#include <vector>
|
|
|
|
using namespace ShaderRecompiler;
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
|
|
void check(bool condition, const char* what) {
|
|
if (!condition) {
|
|
std::fprintf(stderr, "%s\n", what);
|
|
++failures;
|
|
}
|
|
}
|
|
|
|
std::map<std::uint32_t, std::size_t> opcodeCounts(const std::vector<std::uint32_t>& words) {
|
|
std::map<std::uint32_t, std::size_t> counts;
|
|
std::size_t offset = 5;
|
|
while (offset < words.size()) {
|
|
const auto header = words[offset];
|
|
const auto wordCount = header >> spv::WordCountShift;
|
|
if (wordCount == 0 || offset + wordCount > words.size()) {
|
|
return counts;
|
|
}
|
|
++counts[header & 0xffffu];
|
|
offset += wordCount;
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
std::size_t declared(const SpirvModule& module, std::uint32_t opcode) {
|
|
const auto counts = opcodeCounts(module.Finalize());
|
|
const auto found = counts.find(opcode);
|
|
return found == counts.end() ? 0 : found->second;
|
|
}
|
|
|
|
void testRepeatedTypesShareOneId() {
|
|
SpirvModule module;
|
|
const auto first = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
const auto second = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
check(first == second, "a repeated OpTypeInt 32 0 returned a different id");
|
|
check(declared(module, spv::OpTypeInt) == 1, "a repeated OpTypeInt 32 0 was declared more than once");
|
|
check(module.Type(spv::OpTypeVoid) == module.Type(spv::OpTypeVoid), "a repeated OpTypeVoid returned a different id");
|
|
check(declared(module, spv::OpTypeVoid) == 1, "OpTypeVoid was not declared exactly once");
|
|
check(module.Type(spv::OpTypeBool) == module.Type(spv::OpTypeBool), "a repeated OpTypeBool returned a different id");
|
|
check(module.Type(spv::OpTypeFloat, 32u) == module.Type(spv::OpTypeFloat, 32u), "a repeated OpTypeFloat 32 returned a different id");
|
|
check(declared(module, spv::OpTypeFloat) == 1, "OpTypeFloat was not declared exactly once");
|
|
}
|
|
|
|
void testOperandShapesDoNotCollide() {
|
|
SpirvModule module;
|
|
const auto oneWord = module.Type(spv::OpTypeInt, 32u);
|
|
const auto twoWords = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
check(oneWord != twoWords, "OpTypeInt with one operand collided with OpTypeInt with two");
|
|
check(declared(module, spv::OpTypeInt) == 2, "the two OpTypeInt shapes were not both declared");
|
|
const auto voidType = module.Type(spv::OpTypeVoid);
|
|
const auto boolType = module.Type(spv::OpTypeBool);
|
|
check(voidType != boolType, "OpTypeVoid collided with OpTypeBool");
|
|
check(module.Type(spv::OpTypeInt, 32u, 0u) != module.Type(spv::OpTypeInt, 32u, 1u), "unsigned and signed OpTypeInt 32 collided");
|
|
check(module.Type(spv::OpTypeInt, 32u, 0u) != module.Type(spv::OpTypeInt, 64u, 0u), "OpTypeInt 32 collided with OpTypeInt 64");
|
|
check(module.Type(spv::OpTypeFloat, 32u) != module.Type(spv::OpTypeFloat, 64u), "OpTypeFloat 32 collided with OpTypeFloat 64");
|
|
}
|
|
|
|
void testOperandKindsAgree() {
|
|
SpirvModule module;
|
|
const auto fromUnsigned = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
const auto fromSigned = module.Type(spv::OpTypeInt, static_cast<std::int32_t>(32), static_cast<std::int32_t>(0));
|
|
check(fromUnsigned == fromSigned, "OpTypeInt disagreed between unsigned and signed operands");
|
|
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
const auto fromEnum = module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32);
|
|
const auto fromWord = module.Type(spv::OpTypePointer, static_cast<std::uint32_t>(spv::StorageClassFunction), typeU32);
|
|
check(fromEnum == fromWord, "OpTypePointer disagreed between enum and integer operands");
|
|
check(fromEnum == module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32), "a repeated enum-operand OpTypePointer returned a different id");
|
|
check(declared(module, spv::OpTypePointer) == 1, "OpTypePointer was not declared exactly once");
|
|
}
|
|
|
|
void testDecoratedTypesStayDistinct() {
|
|
SpirvModule module;
|
|
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
const auto plain = module.Type(spv::OpTypeRuntimeArray, typeU32);
|
|
const auto decorated = module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, typeU32);
|
|
check(plain != decorated, "a plain OpTypeRuntimeArray collided with the decorated one");
|
|
check(decorated == module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, typeU32), "a repeated decorated type returned a different id");
|
|
check(declared(module, spv::OpTypeRuntimeArray) == 2, "the plain and decorated OpTypeRuntimeArray were not both declared");
|
|
}
|
|
|
|
void testConstantsShareOneId() {
|
|
SpirvModule module;
|
|
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
const auto typeBool = module.Type(spv::OpTypeBool);
|
|
check(module.Constant(spv::OpConstant, typeU32, 0u) == module.Constant(spv::OpConstant, typeU32, 0u), "a repeated OpConstant 0 returned a different id");
|
|
check(module.Constant(spv::OpConstant, typeU32, 0u) != module.Constant(spv::OpConstant, typeU32, 1u), "OpConstant 0 collided with OpConstant 1");
|
|
check(module.Constant(spv::OpConstant, typeU32, 1000u) != module.Constant(spv::OpConstant, typeU32, 0u), "OpConstant 1000 collided with OpConstant 0");
|
|
check(module.Constant(spv::OpConstantTrue, typeBool) == module.Constant(spv::OpConstantTrue, typeBool), "a repeated OpConstantTrue returned a different id");
|
|
check(module.Constant(spv::OpConstantTrue, typeBool) != module.Constant(spv::OpConstantFalse, typeBool), "OpConstantTrue collided with OpConstantFalse");
|
|
check(declared(module, spv::OpConstant) == 3, "OpConstant was not declared once per distinct value");
|
|
check(declared(module, spv::OpConstantTrue) == 1, "OpConstantTrue was not declared exactly once");
|
|
const auto signedId = module.Constant(spv::OpConstant, module.Type(spv::OpTypeInt, 32u, 1u), static_cast<std::uint32_t>(-1));
|
|
check(signedId != 0, "a signed OpConstant produced a zero id");
|
|
}
|
|
|
|
void testOperandFreeConstantsShareOneId() {
|
|
SpirvModule module;
|
|
const auto typeBool = module.Type(spv::OpTypeBool);
|
|
const auto typeVoid = module.Type(spv::OpTypeVoid);
|
|
check(module.Constant(spv::OpConstantTrue, typeBool) == module.Constant(spv::OpConstantTrue, typeBool), "a repeated OpConstantTrue returned a different id");
|
|
check(module.Constant(spv::OpConstantTrue, typeBool) != module.Constant(spv::OpConstantFalse, typeBool), "OpConstantTrue collided with OpConstantFalse");
|
|
check(module.Constant(spv::OpConstantTrue, typeVoid) != module.Constant(spv::OpConstantTrue, typeBool), "OpConstantTrue of different types collided");
|
|
check(declared(module, spv::OpConstantTrue) == 2, "OpConstantTrue was not declared once per type");
|
|
check(declared(module, spv::OpConstantFalse) == 1, "OpConstantFalse was not declared exactly once");
|
|
check(module.Type(spv::OpTypeVoid) == typeVoid, "a repeated operand-free type returned a different id");
|
|
}
|
|
|
|
void testIdsSurviveInterleavedAllocation() {
|
|
SpirvModule module;
|
|
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
const auto constant = module.Constant(spv::OpConstant, typeU32, 4u);
|
|
for (int i = 0; i < 8; ++i) {
|
|
check(module.AllocateId() != 0, "AllocateId returned zero");
|
|
check(module.Type(spv::OpTypeInt, 32u, 0u) == typeU32, "a memoized type changed id after AllocateId");
|
|
check(module.Constant(spv::OpConstant, typeU32, 4u) == constant, "a memoized constant changed id after AllocateId");
|
|
}
|
|
}
|
|
|
|
void testModulesAreIndependent() {
|
|
SpirvModule first;
|
|
SpirvModule second;
|
|
const auto firstId = first.Type(spv::OpTypeInt, 32u, 0u);
|
|
const auto secondId = second.Type(spv::OpTypeInt, 32u, 0u);
|
|
check(firstId == 1, "the first id of a fresh module was not 1");
|
|
check(secondId == 1, "a second module did not start its ids from 1");
|
|
first.Type(spv::OpTypeVoid);
|
|
check(second.Type(spv::OpTypeInt, 32u, 0u) == secondId, "a second module's ids moved when the first module was used");
|
|
}
|
|
|
|
void testIdsStayUniqueBeyondCacheCapacity() {
|
|
SpirvModule module;
|
|
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
std::vector<std::uint32_t> ids;
|
|
ids.reserve(4096);
|
|
for (std::uint32_t value = 0; value < 4096; ++value) {
|
|
ids.push_back(module.Constant(spv::OpConstant, typeU32, value));
|
|
}
|
|
const std::set<std::uint32_t> distinct(ids.begin(), ids.end());
|
|
check(distinct.size() == ids.size(), "distinct constants shared an id once the cache was overrun");
|
|
for (std::uint32_t value = 0; value < 4096; ++value) {
|
|
if (module.Constant(spv::OpConstant, typeU32, value) != ids[value]) {
|
|
std::fprintf(stderr, "constant %u changed id after the cache was overrun\n", value);
|
|
++failures;
|
|
break;
|
|
}
|
|
}
|
|
std::uint32_t largest = 0;
|
|
for (const auto id : distinct) {
|
|
largest = largest < id ? id : largest;
|
|
}
|
|
const auto first = ids.front();
|
|
check(largest - first + 1 == distinct.size(), "the constant ids were not contiguous, so some were never declared");
|
|
check(first > typeU32, "a constant was numbered before the type it uses");
|
|
}
|
|
|
|
void testEmittedWordsAreStable() {
|
|
SpirvModule module;
|
|
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
|
for (int i = 0; i < 32; ++i) {
|
|
module.Type(spv::OpTypeVector, typeU32, 4u);
|
|
module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32);
|
|
module.Constant(spv::OpConstant, typeU32, static_cast<std::uint32_t>(i % 8));
|
|
module.AddFunction(spv::OpNop);
|
|
}
|
|
const auto words = module.Finalize();
|
|
check(!words.empty(), "Finalize produced no words");
|
|
const auto counts = opcodeCounts(words);
|
|
check(counts.at(spv::OpTypeVector) == 1, "OpTypeVector was declared more than once");
|
|
check(counts.at(spv::OpTypePointer) == 1, "OpTypePointer was declared more than once");
|
|
check(counts.at(spv::OpConstant) == 8, "OpConstant was not declared once per distinct value");
|
|
check(counts.at(spv::OpNop) == 32, "the function body lost instructions");
|
|
}
|
|
|
|
}
|
|
|
|
void testStructExtractsAreNotShuffled() {
|
|
const auto op = [](std::uint32_t count, spv::Op code) { return (count << 16u) | static_cast<std::uint32_t>(code); };
|
|
for (const bool vectorSource : {false, true}) {
|
|
const std::vector<std::uint32_t> words{
|
|
spv::MagicNumber, 0x00010300u, 0u, 20u, 0u,
|
|
op(2, spv::OpCapability), spv::CapabilityShader,
|
|
op(3, spv::OpMemoryModel), spv::AddressingModelLogical, spv::MemoryModelGLSL450,
|
|
op(6, spv::OpEntryPoint), spv::ExecutionModelGLCompute, 10u, 0x6e69616du, 0u, 9u,
|
|
op(6, spv::OpExecutionMode), 10u, spv::ExecutionModeLocalSize, 1u, 1u, 1u,
|
|
op(2, spv::OpTypeVoid), 1u,
|
|
op(4, spv::OpTypeInt), 2u, 32u, 0u,
|
|
vectorSource ? op(4, spv::OpTypeVector) : op(4, spv::OpTypeStruct), 3u, 2u, 2u,
|
|
op(4, spv::OpTypeVector), 4u, 2u, 4u,
|
|
op(4, spv::OpTypePointer), 5u, spv::StorageClassPrivate, 4u,
|
|
op(3, spv::OpTypeFunction), 6u, 1u,
|
|
op(4, spv::OpConstant), 2u, 7u, 1u,
|
|
op(4, spv::OpConstant), 2u, 8u, 2u,
|
|
op(4, spv::OpVariable), 5u, 9u, spv::StorageClassPrivate,
|
|
op(5, spv::OpFunction), 1u, 10u, 0u, 6u,
|
|
op(2, spv::OpLabel), 11u,
|
|
op(5, spv::OpCompositeConstruct), 3u, 12u, 7u, 8u,
|
|
op(5, spv::OpCompositeExtract), 2u, 13u, 12u, 0u,
|
|
op(5, spv::OpCompositeExtract), 2u, 14u, 12u, 1u,
|
|
op(7, spv::OpCompositeConstruct), 4u, 15u, 13u, 14u, 13u, 13u,
|
|
op(3, spv::OpStore), 9u, 15u,
|
|
op(1, spv::OpReturn),
|
|
op(1, spv::OpFunctionEnd),
|
|
};
|
|
const auto specialized = ShaderRecompiler::SpecializeSpirv(words);
|
|
bool shuffled = false;
|
|
for (std::size_t cursor = 5; cursor < specialized.size(); cursor += specialized[cursor] >> 16u) {
|
|
if ((specialized[cursor] >> 16u) == 0u) break;
|
|
if ((specialized[cursor] & 0xffffu) == spv::OpVectorShuffle) shuffled = true;
|
|
}
|
|
check(shuffled == vectorSource, vectorSource ? "extracts of one vector were not folded into a shuffle" : "extracts of a struct were folded into a vector shuffle");
|
|
}
|
|
}
|
|
|
|
std::vector<std::uint32_t> foldedSwitchModule(std::uint32_t deadSum, std::array<std::uint32_t, 3> localSize) {
|
|
const auto op = [](std::uint32_t count, spv::Op code) { return (count << 16u) | static_cast<std::uint32_t>(code); };
|
|
return {
|
|
spv::MagicNumber, 0x00010300u, 0u, 40u, 0u,
|
|
op(2, spv::OpCapability), spv::CapabilityShader,
|
|
op(3, spv::OpMemoryModel), spv::AddressingModelLogical, spv::MemoryModelGLSL450,
|
|
op(6, spv::OpEntryPoint), spv::ExecutionModelGLCompute, 11u, 0x6e69616du, 0u, 10u,
|
|
op(6, spv::OpExecutionMode), 11u, spv::ExecutionModeLocalSize, localSize[0], localSize[1], localSize[2],
|
|
op(2, spv::OpTypeVoid), 1u,
|
|
op(4, spv::OpTypeInt), 2u, 32u, 0u,
|
|
op(4, spv::OpTypePointer), 4u, spv::StorageClassPrivate, 2u,
|
|
op(3, spv::OpTypeFunction), 5u, 1u,
|
|
op(4, spv::OpConstant), 2u, 6u, 2u,
|
|
op(4, spv::OpConstant), 2u, 7u, 10u,
|
|
op(4, spv::OpConstant), 2u, 8u, 20u,
|
|
op(4, spv::OpVariable), 4u, 10u, spv::StorageClassPrivate,
|
|
op(5, spv::OpFunction), 1u, 11u, 0u, 5u,
|
|
op(2, spv::OpLabel), 12u,
|
|
op(5, spv::OpIAdd), 2u, deadSum, 6u, 7u,
|
|
op(5, spv::OpIMul), 2u, 14u, deadSum, 7u,
|
|
op(3, spv::OpSelectionMerge), 20u, spv::SelectionControlMaskNone,
|
|
op(7, spv::OpSwitch), 6u, 15u, 1u, 16u, 2u, 17u,
|
|
op(2, spv::OpLabel), 15u,
|
|
op(3, spv::OpStore), 10u, 7u,
|
|
op(2, spv::OpBranch), 20u,
|
|
op(2, spv::OpLabel), 16u,
|
|
op(3, spv::OpStore), 10u, 8u,
|
|
op(2, spv::OpBranch), 20u,
|
|
op(2, spv::OpLabel), 17u,
|
|
op(3, spv::OpStore), 10u, 14u,
|
|
op(2, spv::OpBranch), 20u,
|
|
op(2, spv::OpLabel), 20u,
|
|
op(1, spv::OpReturn),
|
|
op(1, spv::OpFunctionEnd),
|
|
};
|
|
}
|
|
|
|
void checkFoldedSwitch(const std::vector<std::uint32_t>& words, std::uint32_t deadSum, const char* what) {
|
|
const auto specialized = ShaderRecompiler::SpecializeSpirv(words);
|
|
const auto counts = opcodeCounts(specialized);
|
|
std::set<std::uint32_t> constants;
|
|
bool storesFolded = false;
|
|
for (std::size_t cursor = 5; cursor < specialized.size(); cursor += specialized[cursor] >> 16u) {
|
|
if ((specialized[cursor] >> 16u) == 0u) break;
|
|
const auto opcode = specialized[cursor] & 0xffffu;
|
|
if (opcode == spv::OpConstant) constants.insert(specialized[cursor + 2u]);
|
|
if (opcode == spv::OpStore && specialized[cursor + 2u] == 14u) storesFolded = true;
|
|
}
|
|
const std::string context = std::string(" (") + what + ")";
|
|
check(!counts.contains(spv::OpUnreachable), ("blocks the folded switch left unreachable were kept" + context).c_str());
|
|
check(counts.at(spv::OpLabel) == 3u, ("the specialized function does not have exactly its three live blocks" + context).c_str());
|
|
check(storesFolded && constants.contains(14u), ("the folded product the live case stores is missing" + context).c_str());
|
|
check(!constants.contains(deadSum), ("a folded constant without uses was kept" + context).c_str());
|
|
}
|
|
|
|
void testFoldingLeavesNoDeadBlocksOrConstants() {
|
|
checkFoldedSwitch(foldedSwitchModule(13u, {1u, 1u, 1u}), 13u, "no literal names a dead id");
|
|
checkFoldedSwitch(foldedSwitchModule(32u, {15u, 16u, 1u}), 32u, "the OpTypeInt width and the LocalSize literals equal the dead ids");
|
|
}
|
|
|
|
void testIdsBeyondTheBoundAreRejected() {
|
|
auto words = foldedSwitchModule(13u, {1u, 1u, 1u});
|
|
words[3] = 12u;
|
|
bool rejected = false;
|
|
try {
|
|
static_cast<void>(ShaderRecompiler::SpecializeSpirv(words));
|
|
} catch (const std::exception&) {
|
|
rejected = true;
|
|
}
|
|
check(rejected, "a module whose ids reach its bound was specialized");
|
|
}
|
|
|
|
int main() {
|
|
testFoldingLeavesNoDeadBlocksOrConstants();
|
|
testIdsBeyondTheBoundAreRejected();
|
|
testStructExtractsAreNotShuffled();
|
|
testRepeatedTypesShareOneId();
|
|
testOperandShapesDoNotCollide();
|
|
testOperandKindsAgree();
|
|
testDecoratedTypesStayDistinct();
|
|
testConstantsShareOneId();
|
|
testOperandFreeConstantsShareOneId();
|
|
testIdsSurviveInterleavedAllocation();
|
|
testModulesAreIndependent();
|
|
testIdsStayUniqueBeyondCacheCapacity();
|
|
testEmittedWordsAreStable();
|
|
if (failures != 0) {
|
|
std::fprintf(stderr, "%d check(s) failed\n", failures);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|