mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-23 00:53:02 -05:00
referencedIds() took every operand word of every instruction as a possible id, so a literal that happened to equal the id of a dead folded constant or an unreachable block kept it: the OpTypeInt width 32 kept a dead constant %32, LocalSize literals kept dead labels. Visit only the <id> operands of each instruction. Literal operands are skipped for the instructions that carry them: type widths, counts and image parameters, constant values, storage classes, memory access and image operand masks (their trailing ids still count), composite and shuffle indices, OpArrayLength members, extended instruction numbers, entry point names, execution modes, merge and function controls, branch weights, OpSwitch case values (two words for a 64-bit selector) and group operations. An instruction without a listed layout still counts every word, so an unknown instruction can only keep debris, never drop something in use. The SpirvModule case now also folds a switch whose dead constant has id 32 and whose dead blocks' ids appear as LocalSize literals; it fails without this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
307 lines
16 KiB
C++
307 lines
16 KiB
C++
#include "SpirvBackend/SpirvModule.hpp"
|
|
#include "SpirvBackend/SpirvSpecialization.hpp"
|
|
#include <array>
|
|
#include <cstdio>
|
|
#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");
|
|
}
|
|
|
|
int main() {
|
|
testFoldingLeavesNoDeadBlocksOrConstants();
|
|
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;
|
|
}
|