shader: serialize resource plans for the shader cache

This commit is contained in:
nmzik
2026-10-24 23:01:59 +02:00
parent bfc377627a
commit 1816004357
7 changed files with 378 additions and 70 deletions
@@ -17,6 +17,7 @@
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
#include "graphics/shader/recompiler/ir/Serialize.h"
#include "graphics/shader/shaderCompiler.h"
#include "kernel/memory.h"
#include "loader/systemContent.h"
@@ -168,37 +169,6 @@ ShaderRecompiler::Spirv::Profile SpirvProfile(const GraphicContext& graphics) {
.device_clock_period_ps = graphics.device_clock_period_ps};
}
static_assert(sizeof(ShaderRecompiler::IR::ResourceSpecialization::Buffer) == 28 &&
sizeof(ShaderRecompiler::IR::ResourceSpecialization::Image) == 36 &&
sizeof(ShaderRecompiler::IR::ResourceSpecialization::Sampler) == 12,
"PackSpecialization must list every field");
// Appends every field as one word, each list preceded by its size.
void PackSpecialization(const ShaderRecompiler::IR::ResourceSpecialization& specialization,
std::vector<uint32_t>& words) {
const auto word = [](auto value) { return static_cast<uint32_t>(value); };
words.push_back(word(specialization.buffers.size()));
for (const auto& buffer: specialization.buffers) {
words.insert(words.end(), {buffer.packed_stride, word(buffer.descriptor_format),
buffer.descriptor_swizzle, word(buffer.zero_stride_oob),
word(buffer.scalar_empty), buffer.indirect_root,
buffer.indirect_mapping_offset, buffer.indirect_search_iterations});
}
words.push_back(word(specialization.images.size()));
for (const auto& image: specialization.images) {
words.insert(words.end(), {word(image.numeric_class), word(image.dimension), image.mip_count,
word(image.conversion_format), image.shader_swizzle,
image.indirect_root, image.indirect_mapping_offset,
image.indirect_search_iterations, word(image.cube),
word(image.fmask), word(image.heap)});
}
words.push_back(word(specialization.samplers.size()));
for (const auto& sampler: specialization.samplers) {
words.insert(words.end(), {sampler.indirect_root, sampler.indirect_mapping_offset,
sampler.indirect_search_iterations});
}
}
// Formats with a narrower host variant drop the components the shader does not read.
vk::Format NarrowInputFormat(vk::Format format, uint32_t used_components) {
switch (format) {
@@ -326,15 +296,11 @@ ShaderModule& PipelineCache::CompileModule(Program& program, const ProgramKey& k
// The same inputs give the same module id across runs.
const auto function_code = program.FunctionCode();
std::vector<uint32_t> words {static_cast<uint32_t>(key.stage), static_cast<uint32_t>(key.hash),
static_cast<uint32_t>(key.hash >> 32u), key.user_data_count,
static_cast<uint32_t>(key.static_state.size())};
words.insert(words.end(), key.static_state.begin(), key.static_state.end());
words.push_back(info.bindings.push_data_start_dword);
words.push_back(static_cast<uint32_t>(function_code.size()));
words.insert(words.end(), function_code.begin(), function_code.end());
PackSpecialization(program.specialization, words);
const auto id = XXH3_64bits(words.data(), words.size() * sizeof(uint32_t));
ShaderRecompiler::IR::Writer inputs;
ShaderRecompiler::IR::IoAll(inputs, key.stage, key.hash, key.user_data_count, key.static_state,
info.bindings.push_data_start_dword, program.specialization);
inputs.Raw(function_code.data(), function_code.size_bytes()); // Last, so it needs no size.
const auto id = XXH3_64bits(inputs.bytes.data(), inputs.bytes.size());
const auto* stage = ShaderRecompiler::StageName(key.stage);
DumpShader(result.spirv, fmt::format("{}_{:016x}_{:016x}.spv", stage, key.hash, id));
@@ -0,0 +1,174 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_
#define EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_
#include "common/assert.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include <algorithm>
#include <bit>
#include <optional>
#include <span>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <vector>
// Shader cache records. Io(io, value) appends value to a Writer or reads it back from a Reader
// field by field, so padding never reaches the bytes; instructions travel as plan-local indices.
namespace Libs::Graphics::ShaderRecompiler::IR {
struct Writer {
std::vector<uint8_t> bytes;
std::unordered_map<const Inst*, uint32_t> index; // Instructions that values may refer to.
void Raw(const void* data, size_t size) {
const auto* begin = static_cast<const uint8_t*>(data);
bytes.insert(bytes.end(), begin, begin + size);
}
};
struct Reader {
std::span<const uint8_t> bytes;
std::vector<Inst*> insts; // Instructions by index.
bool ok = true; // False once a read ran past the end or was invalid.
void Raw(void* data, size_t size) {
ok = ok && size <= bytes.size();
if (!ok) return;
std::copy_n(bytes.data(), size, static_cast<uint8_t*>(data));
bytes = bytes.subspan(size);
}
// Every element takes at least one byte, which bounds counts read from damaged records.
uint32_t Count(uint32_t count) {
ok = ok && count <= bytes.size();
return ok ? count : 0u;
}
};
template <class S> constexpr bool Reading = std::is_same_v<S, Reader>;
template <class T, template <class...> class Of> constexpr bool IsA = false;
template <template <class...> class Of, class... A> constexpr bool IsA<Of<A...>, Of> = true;
template <class T, class U> concept Is = std::is_same_v<std::remove_const_t<T>, U>;
template <class S, class T> void Io(S& io, T& value);
template <class S, class... T> void IoAll(S& io, T&&... values) {
(Io(io, values), ...);
}
// The value's Type, Opaque for an instruction, then the instruction index or the zero-extended
// immediate.
inline void IoValue(Writer& writer, const Value& value) {
auto type = Type::Opaque;
uint64_t bits = 0;
if (const auto* inst = value.TryInstruction()) {
bits = writer.index.at(inst);
} else {
type = value.GetType();
switch (type) {
case Type::Void: break;
case Type::ScalarReg: bits = RegIndex(value.ScalarRegister()); break;
case Type::VectorReg: bits = RegIndex(value.VectorRegister()); break;
case Type::U1: bits = value.U1(); break;
case Type::U8: bits = value.U8(); break;
case Type::U16: bits = value.U16(); break;
case Type::U32: bits = value.U32(); break;
case Type::U64: bits = value.U64(); break;
case Type::F16: bits = value.F16Bits(); break;
case Type::F32: bits = std::bit_cast<uint32_t>(value.F32Value()); break;
default: EXIT("value type %u has no cache encoding\n", static_cast<uint32_t>(type));
}
}
IoAll(writer, type, bits);
}
inline void IoValue(Reader& reader, Value& value) {
auto type = Type::Void;
uint64_t bits = 0;
IoAll(reader, type, bits);
switch (type) {
case Type::Void: value = {}; break;
case Type::Opaque:
reader.ok = reader.ok && bits < reader.insts.size();
if (reader.ok) value = Value(reader.insts[bits]);
break;
case Type::ScalarReg: value = Value(static_cast<ScalarReg>(bits)); break;
case Type::VectorReg: value = Value(static_cast<VectorReg>(bits)); break;
case Type::U1: value = Value(bits != 0u); break;
case Type::U8: value = Value(static_cast<uint8_t>(bits)); break;
case Type::U16: value = Value(static_cast<uint16_t>(bits)); break;
case Type::U32: value = Value(static_cast<uint32_t>(bits)); break;
case Type::U64: value = Value(bits); break;
case Type::F16: value = Value::F16(static_cast<uint16_t>(bits)); break;
case Type::F32: value = Value::F32(std::bit_cast<float>(static_cast<uint32_t>(bits))); break;
default: reader.ok = false; break;
}
}
// Scalars and standard containers; structs have their own overloads below.
template <class S, class T> void Io(S& io, T& value) {
using U = std::remove_const_t<T>;
static_assert(!std::is_pointer_v<U>, "pointers do not persist");
if constexpr (std::is_arithmetic_v<U> || std::is_enum_v<U>) {
io.Raw(&value, sizeof(U));
} else if constexpr (std::is_same_v<U, Value>) {
IoValue(io, value);
} else if constexpr (IsA<U, std::vector> || IsA<U, std::basic_string>) {
auto count = static_cast<uint32_t>(value.size());
Io(io, count);
if constexpr (Reading<S>) value.resize(io.Count(count));
using E = typename U::value_type;
if constexpr (std::is_arithmetic_v<E> || std::is_enum_v<E>) {
io.Raw(value.data(), value.size() * sizeof(E));
} else {
for (auto& element: value) Io(io, element);
}
} else if constexpr (IsA<U, std::optional>) {
bool has = value.has_value();
Io(io, has);
if constexpr (Reading<S>) {
if (has) value = typename U::value_type {}; // Not emplace(): clang rejects nested aggregates.
else value.reset();
}
if (has) Io(io, *value);
} else {
static_assert(requires { std::tuple_size<U>::value; }, "struct without an Io overload");
std::apply([&](auto&... elements) { IoAll(io, elements...); }, value); // std::array, std::pair
}
}
// Binds every field, so that a field added later fails to compile until it is listed.
#define KYTY_IO_FIELDS(Struct, ...) \
template <class S, Is<Struct> T> void Io(S& io, T& object) { \
auto& [__VA_ARGS__] = object; \
IoAll(io, __VA_ARGS__); \
}
KYTY_IO_FIELDS(MemoryInfo, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y)
KYTY_IO_FIELDS(BufferResource, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
KYTY_IO_FIELDS(ImageResource, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w)
KYTY_IO_FIELDS(SamplerResource, a, b, c, d, e, f, g, h, i, j, k)
KYTY_IO_FIELDS(SampledResourcePair, a, b, c)
KYTY_IO_FIELDS(StageInput, a, b, c, d, e)
KYTY_IO_FIELDS(StageOutput, a, b, c, d)
KYTY_IO_FIELDS(ShaderInfo, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z,
A, B, C, D, E, F, G, H, I, J)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead::CountedRows, a, b)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead, a, b, c, d, e, f, g, h, i, j)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::PointerTable, a, b, c, d)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)
KYTY_IO_FIELDS(DescriptorSource, a, b, c)
KYTY_IO_FIELDS(SrtRead, a, b)
KYTY_IO_FIELDS(ResourceBlock, a, b, c, d)
KYTY_IO_FIELDS(UniformFill, a, b, c, d, e, f)
KYTY_IO_FIELDS(UniformFillPlan, a, b, c, d)
KYTY_IO_FIELDS(ResourceSpecialization::Buffer, a, b, c, d, e, f, g, h)
KYTY_IO_FIELDS(ResourceSpecialization::Image, a, b, c, d, e, f, g, h, i, j, k)
KYTY_IO_FIELDS(ResourceSpecialization::Sampler, a, b, c)
KYTY_IO_FIELDS(ResourceSpecialization, a, b, c)
#undef KYTY_IO_FIELDS
} // namespace Libs::Graphics::ShaderRecompiler::IR
#endif /* EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_ */
@@ -655,7 +655,6 @@ struct UniformFillPlan {
// condition and fill values without translated blocks, plus reusable evaluation scratch.
struct ResourcePlan {
struct SelectorKeys {
uint32_t source = UINT32_MAX;
std::vector<uint32_t> values;
bool resolved = false;
};
@@ -3,6 +3,7 @@
#include "common/assert.h"
#include "graphics/guest_gpu/gpu_format.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ir/Serialize.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/shaderBindings.h"
@@ -1311,6 +1312,17 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) {
return result;
}
// State an extracted or loaded plan derives from its fields: one key set per selector group, which
// extraction numbers densely in order of first use (false when they are not), and the walker.
static bool DerivePlanState(ResourcePlan& plan) {
for (const auto group: plan.selector_groups) {
if (group == plan.selector_keys.size()) plan.selector_keys.emplace_back();
if (group != UINT32_MAX && group >= plan.selector_keys.size()) return false;
}
if (plan.srt_plan_complete && plan.resource_tracking_complete) CompileSrtWalker(plan);
return true;
}
ResourcePlan ExtractResourcePlan(const Program& program) {
ResourcePlan plan;
plan.stage = program.stage;
@@ -1391,17 +1403,18 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
// Destination tables share keys when their complete selector domains agree.
// Resolve those domains lazily at dispatch time, without repeated guest reads.
std::vector<uint32_t> group_sources; // The first source of each group.
plan.selector_groups.resize(plan.descriptor_sources.size(), UINT32_MAX);
for (uint32_t source = 0; source < plan.descriptor_sources.size(); ++source) {
const auto& descriptor = program.descriptor_sources[source].indirect_descriptor;
if (!descriptor || !descriptor->selector) continue;
const auto& a = *descriptor;
uint32_t group = 0;
for (; group < plan.selector_keys.size(); ++group) {
const auto& b = *program.descriptor_sources[plan.selector_keys[group].source].indirect_descriptor;
for (; group < group_sources.size(); ++group) {
const auto& b = *program.descriptor_sources[group_sources[group]].indirect_descriptor;
if (EquivalentSelectorDomain(program, a, b)) break;
}
if (group == plan.selector_keys.size()) plan.selector_keys.push_back({.source = source});
if (group == group_sources.size()) group_sources.push_back(source);
plan.selector_groups[source] = group;
}
plan.srt_reads.reserve(program.srt_reads.size());
@@ -1491,7 +1504,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (capture_indirect_reads || !plan.source_reads.empty())
plan.resource_tracking_complete &= !program.has_address_writes;
if (plan.srt_plan_complete && plan.resource_tracking_complete) CompileSrtWalker(plan);
DerivePlanState(plan);
return plan;
}
@@ -1986,4 +1999,86 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
image_remap.Apply(images);
}
namespace {
using ControlPhis = decltype(ResourcePlan::control_phi_predecessors);
// Instructions come first, so that any value can refer to them by index, then their arguments.
// Plan Phis have no blocks; a control Phi lists the plan blocks of its arguments after them.
void IoValues(Writer& writer, const std::list<Inst>& values, const ControlPhis& phis) {
IoAll(writer, static_cast<uint32_t>(values.size()));
for (const auto& inst: values) {
writer.index.emplace(&inst, static_cast<uint32_t>(writer.index.size()));
IoAll(writer, inst.GetOpcode(), inst.Flags<uint64_t>());
}
for (const auto& inst: values) {
if (inst.GetOpcode() != ValueOpcode::Phi) {
for (size_t index = 0; index < inst.NumArgs(); ++index) IoAll(writer, inst.Arg(index));
continue;
}
std::vector<Value> args;
for (size_t index = 0; index < inst.NumArgs(); ++index) {
EXIT_IF(inst.PhiBlock(index) != nullptr);
args.push_back(inst.Arg(index));
}
const auto found = phis.find(&inst);
IoAll(writer, args, found != phis.end() ? found->second : std::vector<uint32_t> {});
}
}
void IoValues(Reader& reader, std::list<Inst>& values, ControlPhis& phis) {
uint32_t count = 0;
Io(reader, count);
count = reader.Count(count);
for (uint32_t index = 0; index < count && reader.ok; ++index) {
ValueOpcode opcode {};
uint64_t flags = 0;
IoAll(reader, opcode, flags);
reader.ok = reader.ok && opcode < ValueOpcode::Count;
if (reader.ok) reader.insts.push_back(&values.emplace_back(opcode, flags));
}
for (auto* inst: reader.insts) {
if (inst->GetOpcode() != ValueOpcode::Phi) {
for (size_t index = 0; index < inst->NumArgs(); ++index) {
Value arg;
Io(reader, arg);
inst->SetArg(index, arg);
}
continue;
}
std::vector<Value> args;
std::vector<uint32_t> predecessors;
IoAll(reader, args, predecessors);
for (const auto arg: args) inst->AddPhiOperand(nullptr, arg);
if (!predecessors.empty()) phis.emplace(inst, std::move(predecessors));
}
}
// Binds every plan field: one added later fails to compile here until it is stored or derived.
template <class S, class P> void IoPlan(S& io, P& plan) {
auto& [stage, shader_hash, user_data_base, user_data_count, values, memory_info, descriptor_sources,
selector_groups, control_flow, control_phis, srt_reads, source_reads, capture_specialization_reads,
srt_plan_complete, resource_tracking_complete, info, uniform_fill,
// Derived on load.
srt_code, srt_user_data, srt_captures, srt_scratch, material_keys, selector_keys] = plan;
IoValues(io, values, control_phis);
IoAll(io, stage, shader_hash, user_data_base, user_data_count, memory_info, descriptor_sources,
selector_groups, control_flow, srt_reads, source_reads, capture_specialization_reads,
srt_plan_complete, resource_tracking_complete, info, uniform_fill);
}
} // namespace
std::vector<uint8_t> SaveResourcePlan(const ResourcePlan& plan) {
Writer writer;
IoPlan(writer, plan);
return std::move(writer.bytes);
}
bool LoadResourcePlan(std::span<const uint8_t> bytes, ResourcePlan& plan) {
Reader reader {bytes};
IoPlan(reader, plan);
return reader.ok && reader.bytes.empty() && DerivePlanState(plan);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -53,6 +53,11 @@ struct ResourceSpecialization {
// its values and is independent of the translated shader CFG.
ResourcePlan ExtractResourcePlan(const Program& program);
// Shader cache record of a plan; loading it into a default-constructed plan rebuilds the walker
// and scratch state like extraction. A failed load leaves a plan that must not be used.
std::vector<uint8_t> SaveResourcePlan(const ResourcePlan& plan);
bool LoadResourcePlan(std::span<const uint8_t> bytes, ResourcePlan& plan);
// Refreshes cached resources and specialization in place. A failed refresh must not be used.
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
ResourceSnapshot& snapshot, ResourceSpecialization& specialization);
+81 -21
View File
@@ -1,3 +1,4 @@
#include "graphics/shader/recompiler/ir/Serialize.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "SrtWalkerTestMemory.h"
@@ -17,6 +18,22 @@ void Check(bool value, const char *text) {
}
}
// Every extracted plan below is the one a restarted game loads from the shader cache: exactly
// its bytes load, and the loaded plan saves back to them.
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan
Extract(const Libs::Graphics::ShaderRecompiler::IR::Program &program) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const auto bytes = SaveResourcePlan(ExtractResourcePlan(program));
auto extended = bytes;
extended.push_back(0u);
ResourcePlan loaded, truncated, padded;
Check(LoadResourcePlan(bytes, loaded) && SaveResourcePlan(loaded) == bytes &&
!LoadResourcePlan(std::span(bytes).first(bytes.size() - 1u), truncated) &&
!LoadResourcePlan(extended, padded),
"resource plan did not round-trip through its exact cache bytes");
return loaded;
}
bool RejectSpecializationRead(void *userdata, uint64_t, std::span<uint32_t>) {
++*static_cast<uint32_t *>(userdata);
return false;
@@ -92,7 +109,7 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UnbasedFlatPlan() {
program.resource_tracking_complete = true;
AddValueBlock(program);
program.info.uses_dma = true;
return ExtractResourcePlan(program);
return Extract(program);
}
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UserDataBufferPlan() {
@@ -113,7 +130,7 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UserDataBufferPlan() {
source.dword_count = 4;
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0});
return ExtractResourcePlan(program);
return Extract(program);
}
Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() {
@@ -171,7 +188,7 @@ Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() {
void TestWalkerLoadsBypassSpecializationReader() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const uint32_t dword = 0x12345678;
auto plan = ExtractResourcePlan(SrtProgram(reinterpret_cast<uint64_t>(&dword)));
auto plan = Extract(SrtProgram(reinterpret_cast<uint64_t>(&dword)));
uint32_t specialization_reads = 0;
const SrtRuntime runtime{.userdata = &specialization_reads,
.read_specialization_memory =
@@ -223,7 +240,7 @@ void TestNativeNestedSrtRefresh() {
leaf->Arg(0).Instruction()->SetArg(1, pointer_high);
leaf->SetArg(1, Value(7u));
read(leaf->Arg(0), 11u);
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.srt_code != nullptr, "nested user-data tree did not compile an x86 walker");
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
@@ -268,7 +285,7 @@ void TestNativeNestedSrtRefresh() {
program.srt_reads.resize(1);
for (const int32_t offset : {-1, 7}) {
program.memory_info[0].offset = static_cast<uint32_t>(offset);
auto bounded = ExtractResourcePlan(program);
auto bounded = Extract(program);
auto base = pointer_words(second.data() + 1);
Check(bounded.srt_code != nullptr &&
MaterializeResources(bounded, {.user_data = base}, snapshot, specialization) &&
@@ -358,7 +375,7 @@ void TestSrtAliasesShareReads() {
DescriptorSource indirect;
indirect.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{}).sources = {0, 1};
program.descriptor_sources.push_back(indirect);
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
for (const auto &inst : plan.value_storage) {
Check(inst.GetOpcode() != ValueOpcode::ReadConst &&
inst.GetOpcode() != ValueOpcode::GetSrtResource,
@@ -431,7 +448,7 @@ void TestUniformVectorDescriptorRead() {
ResourceSpecialization specialization;
for (const bool written : {false, true}) {
program.info.buffers[0].written = written;
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.control_flow.empty() && plan.capture_specialization_reads,
"vector descriptor read depended on incidental control-flow capture");
reads = {};
@@ -453,7 +470,7 @@ void TestUniformVectorDescriptorRead() {
auto &guarded_lane = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&guarded), Value(&active)});
program.descriptor_sources.push_back({.dwords = {Value(&guarded_lane)}, .dword_count = 1});
{
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
uint32_t value = 0;
reads = {};
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], value) &&
@@ -466,7 +483,7 @@ void TestUniformVectorDescriptorRead() {
reads.count = 0;
uint32_t result = 99;
{
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.count == 0,
"literal false EXEC read vector memory");
@@ -475,7 +492,7 @@ void TestUniformVectorDescriptorRead() {
handle.SetArg(3, Value(0x204u));
program.descriptor_sources.back().dwords[0] = Value(&count);
{
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.count == 0,
"invalid vector buffer format read memory");
@@ -564,7 +581,7 @@ void TestWrittenDescriptorPredicateReads(bool memory_condition) {
store_block.AppendNewInst(ValueOpcode::StoreBufferU32,
{Value(&output), Value(0u), Value(0u), Value(0u), Value(1u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.capture_specialization_reads == memory_condition && !plan.control_flow.empty(),
"conditional writable descriptor used the wrong memory dependency policy");
struct Reads { uint32_t count = 0; bool readable = false; } reads;
@@ -634,7 +651,7 @@ void TestSharedControlPredicates() {
program.blocks[i]->terminator.true_block = program.blocks[i == 0 ? 1 : 3];
program.blocks[i]->terminator.false_block = program.blocks[2];
}
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.value_storage.size() <= 2u * depth + 12u &&
plan.control_flow.size() == 4 &&
plan.control_flow[0].condition == plan.control_flow[1].condition &&
@@ -706,7 +723,7 @@ void TestNestedExecPredicatesReuseReads() {
use.AppendNewInst(ValueOpcode::LoadBufferU32,
{buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
struct Reads { uint32_t value = 0; bool readable = true; uint32_t count = 0; } reads;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
@@ -769,7 +786,7 @@ void TestFiniteImageRefreshReusesScalarReads() {
.resource_class = ImageResourceClass::Sampled,
.numeric_class = Libs::Graphics::Prospero::TextureNumericClass::Float,
.dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
struct Reads {
std::array<uint32_t, 3> words{0x100u, 0x200u, 0x200u};
std::array<uint32_t, 3> counts{};
@@ -912,7 +929,7 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool count
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]});
emit(use, ValueOpcode::LoadBufferU32, {buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.Instruction()->SetFlags(MemoryFlags{.index = buffer_index});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
struct Reads { uint32_t count = 0; bool readable = true; uint32_t descriptors = 0; } reads;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
constexpr std::array<uint32_t, 4> descriptor{0x8000u, 1u << 16u, 16u,
@@ -994,7 +1011,7 @@ void TestCyclicControlPredicatesRemainUnknown() {
branch(entry, u, left, right);
branch(left, a, right, exit);
branch(right, b, left, exit);
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
uint32_t value = 99;
Check(!EvaluateSrtValue(plan, {}, plan.control_flow[1].condition, value) &&
!EvaluateSrtValue(plan, {}, plan.control_flow[2].condition, value),
@@ -1047,7 +1064,7 @@ void TestBackEdgeReachesPhiPredecessor() {
program.srt_reads.push_back({Value(&load(exit, 0u)), 0});
exit.AppendNewInst(ValueOpcode::ReadConst,
{Value(&exit.AppendNewInst(ValueOpcode::GetSrtResource)), Value(0u)});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.control_flow.size() == 4 && plan.control_phi_predecessors.size() == 1,
"back-edge plan lost its control flow or Phi");
plan.capture_specialization_reads = true;
@@ -1134,7 +1151,7 @@ void TestEmptyScalarBufferSpecialization() {
return fixture;
};
auto fixture = make_fixture();
auto plan = ExtractResourcePlan(fixture.program);
auto plan = Extract(fixture.program);
std::array<uint32_t, 4> descriptor{0x1000u, 8u << 16u, 0u, 3u << 28u};
uint32_t reads = 0u;
const SrtRuntime runtime{.user_data = descriptor, .userdata = &reads,
@@ -1191,7 +1208,7 @@ void TestEmptyScalarBufferSpecialization() {
void TestMixedSamplerVariantsShareRuntimeDescriptor() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto program = MixedSamplerProgram();
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
std::array<uint32_t, 2> user_data{0x11111111u, 0x22222222u};
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
@@ -1239,7 +1256,7 @@ void TestSignedStorageSpecialization() {
program.descriptor_sources.push_back(source);
program.info.images.push_back({.source = 0u, .resource_class = ImageResourceClass::Storage,
.dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D, .written = true});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
std::array<uint32_t, 1> data{};
const SrtRuntime runtime{.user_data = data};
ResourceSnapshot snapshot;
@@ -1317,7 +1334,7 @@ void TestIndirectImageSamplerClasses(bool mixed) {
program.descriptor_sources.push_back(root);
program.info.samplers.resize(1);
program.info.sampled_pairs.resize(1);
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
std::array<uint32_t, 1> user_data{0x11111111u};
uint32_t reads = 0;
const SrtRuntime runtime{.user_data = user_data, .userdata = &reads,
@@ -1354,6 +1371,48 @@ void TestIndirectImageSamplerClasses(bool mixed) {
}
}
void TestShaderCacheRecords() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
// Equivalent selector domains share one key group, which a load rebuilds.
Program selectors;
AddValueBlock(selectors);
for (const uint32_t stride : {4u, 8u, 4u}) {
DescriptorSource source;
source.indirect_descriptor = DescriptorSource::IndirectDescriptor{
.selector = DescriptorSource::IndirectDescriptor::SelectorRead{.stride = stride}};
selectors.descriptor_sources.push_back(source);
}
selectors.descriptor_sources.emplace_back();
const auto plan = Extract(selectors);
Check(plan.selector_groups == std::vector<uint32_t>{0u, 1u, 0u, UINT32_MAX} &&
plan.selector_keys.size() == 2u,
"loaded plan lost its selector key groups");
const std::array values{Value(), Value(ScalarReg(3)), Value(VectorReg(5)), Value(true),
Value(uint8_t{7}), Value(uint16_t{9}), Value(11u), Value(uint64_t{1} << 40u),
Value::F16(0x3c00u), Value::F32(-2.5f)};
Writer writer;
Io(writer, values);
Reader reader{writer.bytes};
std::array<Value, values.size()> loaded;
Io(reader, loaded);
Check(reader.ok && reader.bytes.empty() && loaded == values,
"immediate values changed through the shader cache");
// Damaged records: a missing instruction, a type that is no value type, an oversized count and
// an unknown opcode.
for (const uint8_t type : {1u, 3u}) {
const std::array<uint8_t, 12> bytes{type};
Reader damaged{bytes};
Value value;
Io(damaged, value);
Check(!damaged.ok, "damaged value was accepted");
}
ResourcePlan oversized, unknown;
Check(!LoadResourcePlan(std::array<uint8_t, 4>{0xffu, 0xffu, 0xffu, 0xffu}, oversized) &&
!LoadResourcePlan(std::array<uint8_t, 14>{1u, 0u, 0u, 0u, 0xffu, 0xffu}, unknown),
"damaged resource plan was accepted");
}
} // namespace
namespace Common {
@@ -1393,6 +1452,7 @@ int main() {
TestMixedSamplerVariantsShareRuntimeDescriptor();
TestSignedStorageSpecialization();
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
TestShaderCacheRecords();
std::puts("ResourceMaterializationTests: all cases passed");
return 0;
}
+13 -4
View File
@@ -1683,6 +1683,17 @@ void CheckSpirvEmission(const TestCase &test, const std::vector<u32> &spirv) {
}
}
// The plan a restarted game loads from the shader cache, which saves back to the same bytes.
ShaderRecompiler::IR::ResourcePlan LoadedResourcePlan(const char *name,
const ShaderRecompiler::IR::Program &program) {
using namespace ShaderRecompiler::IR;
const auto bytes = SaveResourcePlan(ExtractResourcePlan(program));
ResourcePlan plan;
Require(name, "resource plan cache", LoadResourcePlan(bytes, plan) && SaveResourcePlan(plan) == bytes,
"resource plan did not round-trip through its cache bytes");
return plan;
}
CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) {
auto user_data =
MakeNativeUserData(test.has_user_data ? &test.user_data : nullptr);
@@ -1715,8 +1726,7 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) {
}
auto translated = ShaderRecompiler::TranslateProgram(test.code, options);
auto resource_plan =
ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
auto resource_plan = LoadedResourcePlan(test.name, translated.program);
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
const std::array workgroup_counts{test.dispatch_x, test.dispatch_y, test.dispatch_z};
@@ -1903,8 +1913,7 @@ CompiledShader CompileFragmentCase(const GraphicsCase &test) {
auto translated =
ShaderRecompiler::TranslateProgram(test.fragment_code, options);
auto resource_plan =
ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
auto resource_plan = LoadedResourcePlan(test.name, translated.program);
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
const ShaderRecompiler::IR::SrtRuntime runtime{