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
@@ -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);