From 24c934a770e53789e685b45398fb618db25566d1 Mon Sep 17 00:00:00 2001 From: SaloQT <123214229+SaloQT@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:56:22 +1100 Subject: [PATCH 1/2] feat(recompiler): load prebuilt shader packs before the writable cache Read-only packs of encoded cache entries are mapped once at startup from the shader cache prebuilt directory (plus ANYPS5_PREBUILT_SHADER_CACHE, opt out with ANYPS5_NO_PREBUILT_SHADERS). Disk loads try packs and loose prebuilt folders first and fall through to the writable cache and the compiler on a miss. Keys, source version, format and payload checksums are validated exactly as for writable entries, and interval logs report prebuilt hits, misses, rejections and load timings. Depends on the uncached compile entry point only through the offline tool that produces these packs; the loader itself is independent. --- core/shader/recompiler/CMakeLists.txt | 1 + core/shader/recompiler/ShaderCachePack.cpp | 115 +++++++++++++++++++++ core/shader/recompiler/ShaderCachePack.hpp | 51 +++++++++ core/shader/recompiler/ShaderDiskCache.cpp | 107 ++++++++++++++++++- core/shader/recompiler/ShaderDiskCache.hpp | 10 ++ 5 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 core/shader/recompiler/ShaderCachePack.cpp create mode 100644 core/shader/recompiler/ShaderCachePack.hpp diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 3461dd0d214..763a665ef37 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -36,6 +36,7 @@ function(add_shader_recompiler target runtimeTarget) add_library(${target} STATIC EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Recompiler.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderDiskCache.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCachePack.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/RectListShaders.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/BarycentricGeometryShader.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaOpcode.cpp diff --git a/core/shader/recompiler/ShaderCachePack.cpp b/core/shader/recompiler/ShaderCachePack.cpp new file mode 100644 index 00000000000..a9550483f54 --- /dev/null +++ b/core/shader/recompiler/ShaderCachePack.cpp @@ -0,0 +1,115 @@ +#include "ShaderCachePack.hpp" +#include "ShaderCacheDirectory.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#include +#include +#endif + +namespace ShaderRecompiler::ShaderDiskCache { +namespace { +using Clock = std::chrono::steady_clock; +std::uint64_t elapsed(Clock::time_point start) { + return std::chrono::duration_cast(Clock::now() - start).count(); +} +} + +std::array PackKeyHash(std::span key) { + return {HashBytes(key, 0x5eed0001ull), HashBytes(key, 0x5eed0002ull)}; +} + +std::vector PrebuiltCachePaths(const std::filesystem::path& cacheRoot) { + if (cacheRoot.empty()) return {}; + return {cacheRoot / "prebuilt"}; +} + +CachePack::CachePack(const std::filesystem::path& path) { + static_assert(std::endian::native == std::endian::little); +#ifdef _WIN32 + const auto handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) { error = "cannot open"; return; } + LARGE_INTEGER size{}; + if (!GetFileSizeEx(handle, &size) || size.QuadPart < static_cast(sizeof(PackHeader)) || static_cast(size.QuadPart) > std::numeric_limits::max()) { + CloseHandle(handle); error = "invalid size"; return; + } + mapping = CreateFileMappingW(handle, nullptr, PAGE_READONLY, 0, 0, nullptr); + fileHandle = handle; + if (!mapping) { error = "cannot map"; return; } + const auto* data = static_cast(MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0)); + if (!data) { error = "cannot map view"; return; } + file = {data, static_cast(size.QuadPart)}; +#else + const auto fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { error = "cannot open"; return; } + struct stat size{}; + if (fstat(fd, &size) != 0 || size.st_size < static_cast(sizeof(PackHeader)) || static_cast(size.st_size) > std::numeric_limits::max()) { + close(fd); error = "invalid size"; return; + } + auto* data = mmap(nullptr, size.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (data == MAP_FAILED) { error = "cannot map"; return; } + file = {static_cast(data), static_cast(size.st_size)}; +#endif + Validate(); +} + +CachePack::~CachePack() { +#ifdef _WIN32 + if (!file.empty()) UnmapViewOfFile(file.data()); + if (mapping) CloseHandle(mapping); + if (fileHandle) CloseHandle(fileHandle); +#else + if (!file.empty()) munmap(const_cast(file.data()), file.size()); +#endif +} + +void CachePack::Validate() { + PackHeader header; + std::memcpy(&header, file.data(), sizeof(header)); + if (header.magic != PackMagic) { error = "unknown pack format"; return; } + if (header.sourceVersion != SourceVersion()) { error = "different compiler source version"; return; } + if (header.fileBytes != file.size() || header.entryCount > (file.size() - sizeof(header)) / sizeof(PackRecord)) { error = "truncated index"; return; } + const auto indexBytes = static_cast(header.entryCount) * sizeof(PackRecord); + const auto index = file.subspan(sizeof(header), indexBytes); + if (HashBytes(index) != header.indexHash) { error = "index checksum mismatch"; return; } + const std::span checked(reinterpret_cast(index.data()), header.entryCount); + std::array previous{}; + for (const auto& record : checked) { + if (record.hash < previous || record.offset < sizeof(header) + indexBytes || record.offset > file.size() || record.size > file.size() - record.offset || record.size < 48) { error = "invalid index record"; return; } + previous = record.hash; + } + records = checked; +} + +LoadStatus CachePack::Load(std::span key, CompiledVariant& variant, PackLoadTimings& timings) const { + const auto start = Clock::now(); + const auto hash = PackKeyHash(key); + const auto first = std::lower_bound(records.begin(), records.end(), hash, [](const auto& record, const auto& value) { return record.hash < value; }); + timings.lookupNanoseconds += elapsed(start); + auto status = LoadStatus::Absent; + for (auto it = first; it != records.end() && it->hash == hash; ++it) { + const auto data = file.subspan(it->offset, it->size); + const auto decode = Clock::now(); + const auto result = DecodeEntry(data, key, variant); + timings.decodeNanoseconds += elapsed(decode); + timings.bytes += data.size(); + if (result == LoadStatus::Loaded) return result; + if (result == LoadStatus::Rejected) status = result; + } + return status; +} +} diff --git a/core/shader/recompiler/ShaderCachePack.hpp b/core/shader/recompiler/ShaderCachePack.hpp new file mode 100644 index 00000000000..24aff336100 --- /dev/null +++ b/core/shader/recompiler/ShaderCachePack.hpp @@ -0,0 +1,51 @@ +#pragma once +#include "ShaderDiskCache.hpp" +#include + +namespace ShaderRecompiler::ShaderDiskCache { + +inline constexpr std::uint64_t PackMagic = 0x314b434150535041ull; +struct PackHeader { + std::uint64_t magic = PackMagic; + std::uint64_t sourceVersion = 0; + std::uint64_t fileBytes = 0; + std::uint64_t entryCount = 0; + std::uint64_t indexHash = 0; +}; +struct PackRecord { + std::array hash{}; + std::uint64_t offset = 0; + std::uint64_t size = 0; +}; +static_assert(sizeof(PackHeader) == 40 && sizeof(PackRecord) == 32); + +struct PackLoadTimings { + std::uint64_t lookupNanoseconds = 0; + std::uint64_t decodeNanoseconds = 0; + std::uint64_t bytes = 0; +}; + +[[nodiscard]] std::array PackKeyHash(std::span key); +[[nodiscard]] std::vector PrebuiltCachePaths(const std::filesystem::path& cacheRoot); + +class CachePack { +public: + explicit CachePack(const std::filesystem::path& path); + ~CachePack(); + CachePack(const CachePack&) = delete; + CachePack& operator=(const CachePack&) = delete; + [[nodiscard]] bool Valid() const { return error.empty(); } + [[nodiscard]] const std::string& Error() const { return error; } + [[nodiscard]] std::size_t Count() const { return records.size(); } + [[nodiscard]] LoadStatus Load(std::span key, CompiledVariant& variant, PackLoadTimings& timings) const; + +private: + void Validate(); + std::span file; + std::span records; + std::string error; + void* mapping = nullptr; + void* fileHandle = nullptr; +}; + +} diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ede17b76e5e..acc7ccfb167 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "CacheKey.hpp" +#include "ShaderCachePack.hpp" #include "ShaderCacheDirectory.hpp" #include "ThreadOwned.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -1283,6 +1284,7 @@ class DiskStore { if (root.empty()) return; this->root = root; directory = root / hex(SourceVersion()); + discoverPrebuilt(); std::thread([this] { run(); }).detach(); } @@ -1290,6 +1292,7 @@ class DiskStore { [[nodiscard]] const std::filesystem::path& Directory() const { return directory; } bool Load(std::span key, CompiledVariant& variant) { + if (loadPrebuilt(key, variant)) return true; return load(key, [&](std::span file, std::shared_ptr owner) { return DecodeEntry(file, key, variant, std::move(owner)); }, hits, misses, loadFailures); } @@ -1339,16 +1342,107 @@ class DiskStore { counters.planMisses = planMisses.load(std::memory_order_relaxed); counters.planWrites = planWrites.load(std::memory_order_relaxed); counters.planLoadFailures = planLoadFailures.load(std::memory_order_relaxed); + counters.prebuiltHits = prebuiltHits.load(std::memory_order_relaxed); + counters.prebuiltMisses = prebuiltMisses.load(std::memory_order_relaxed); + counters.prebuiltRejected = prebuiltRejected.load(std::memory_order_relaxed); + counters.lookupNanoseconds = lookupNanoseconds.load(std::memory_order_relaxed); + counters.readNanoseconds = readNanoseconds.load(std::memory_order_relaxed); + counters.decodeNanoseconds = decodeNanoseconds.load(std::memory_order_relaxed); + counters.loadNanoseconds = loadNanoseconds.load(std::memory_order_relaxed); + counters.maxLoadNanoseconds = maxLoadNanoseconds.load(std::memory_order_relaxed); + counters.prebuiltBytesRead = prebuiltBytesRead.load(std::memory_order_relaxed); + counters.prebuiltLoadNanoseconds = prebuiltLoadNanoseconds.load(std::memory_order_relaxed); return counters; } private: + void discoverPrebuilt() { + const auto start = std::chrono::steady_clock::now(); + std::size_t entries = 0; + const auto addPack = [&](const std::filesystem::path& path) { + auto pack = std::make_unique(path); + if (!pack->Valid()) { + std::fprintf(stderr, "[shader-prebuilt] ignored %s: %s\n", path.string().c_str(), pack->Error().c_str()); + return; + } + entries += pack->Count(); + packs.push_back(std::move(pack)); + }; + for (const auto& path : PrebuiltCachePaths(root)) { + std::error_code error; + if (std::filesystem::is_regular_file(path, error)) { addPack(path); continue; } + if (!std::filesystem::is_directory(path, error)) continue; + bool loose = false; + std::vector packPaths; + for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) { + const auto extension = it->path().extension(); + if (extension == ".pack") packPaths.push_back(it->path()); + if (extension == ".bin" && it->path().stem().string().size() == 32) loose = true; + } + std::sort(packPaths.begin(), packPaths.end()); + for (const auto& item : packPaths) addPack(item); + if (loose) looseDirectories.push_back(path); + const auto versioned = path / hex(SourceVersion()); + if (std::filesystem::is_directory(versioned, error)) looseDirectories.push_back(versioned); + } + if (!packs.empty() || !looseDirectories.empty()) std::fprintf(stderr, "[shader-prebuilt] mapped %zu packs, %zu entries, %zu loose folders in %.3f ms; exact source/ABI/request keys required\n", packs.size(), entries, looseDirectories.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + } + struct Job { std::vector key; std::shared_ptr variant; std::shared_ptr plan; }; + bool loadPrebuilt(std::span key, CompiledVariant& variant) { + if (packs.empty() && looseDirectories.empty()) return false; + const auto started = std::chrono::steady_clock::now(); + const auto elapsed = [](auto start) { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); }; + bool loaded = false; + PackLoadTimings timing; + for (const auto& pack : packs) { + const auto status = pack->Load(key, variant, timing); + if (status == LoadStatus::Loaded) { loaded = true; break; } + if (status == LoadStatus::Rejected) prebuiltRejected.fetch_add(1, std::memory_order_relaxed); + } + lookupNanoseconds.fetch_add(timing.lookupNanoseconds, std::memory_order_relaxed); + decodeNanoseconds.fetch_add(timing.decodeNanoseconds, std::memory_order_relaxed); + prebuiltBytesRead.fetch_add(timing.bytes, std::memory_order_relaxed); + if (!loaded) { + const auto name = EntryName(key); + thread_local std::vector* fileSlot = nullptr; + auto& file = ThreadOwned(fileSlot); + for (const auto& path : looseDirectories) { + const auto start = std::chrono::steady_clock::now(); + const auto read = ReadWholeFile(path / name, file); + readNanoseconds.fetch_add(elapsed(start), std::memory_order_relaxed); + if (!read) continue; + const auto decode = std::chrono::steady_clock::now(); + const auto status = DecodeEntry(file, key, variant); + decodeNanoseconds.fetch_add(elapsed(decode), std::memory_order_relaxed); + prebuiltBytesRead.fetch_add(file.size(), std::memory_order_relaxed); + if (status == LoadStatus::Rejected) { + const auto failures = prebuiltRejected.fetch_add(1, std::memory_order_relaxed); + if (failures < 8) std::fprintf(stderr, "[shader-prebuilt] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), file.size()); + } + if (status == LoadStatus::Loaded) { loaded = true; break; } + } + if (file.capacity() > (4u << 20u)) std::vector().swap(file); + } + if (loaded) { + const auto previous = prebuiltHits.fetch_add(1, std::memory_order_relaxed); + if (previous == 0) std::fprintf(stderr, "[shader-prebuilt] first exact-key hit loaded in %.3f ms\n", elapsed(started) / 1e6); + } else prebuiltMisses.fetch_add(1, std::memory_order_relaxed); + if (loaded) { + const auto duration = elapsed(started); + prebuiltLoadNanoseconds.fetch_add(duration, std::memory_order_relaxed); + auto maximum = maxLoadNanoseconds.load(std::memory_order_relaxed); + while (maximum < duration && !maxLoadNanoseconds.compare_exchange_weak(maximum, duration, std::memory_order_relaxed)) {} + report(false); + } + return loaded; + } + template bool load(std::span key, TDecode&& decode, std::atomic& hitCount, std::atomic& missCount, std::atomic& failureCount) { const auto started = std::chrono::steady_clock::now(); @@ -1474,19 +1568,27 @@ class DiskStore { const auto totals = Totals(); const auto loadNs = loadNanoseconds.load(std::memory_order_relaxed); const auto delta = [](std::uint64_t after, std::uint64_t before) { return static_cast(after - before); }; - if (totals.hits == reported.hits && totals.misses == reported.misses && totals.writes == reported.writes && totals.loadFailures == reported.loadFailures && totals.writeFailures == reported.writeFailures && totals.planHits == reported.planHits && totals.planMisses == reported.planMisses && totals.planWrites == reported.planWrites && totals.planLoadFailures == reported.planLoadFailures) return; - std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; resource plans: %llu hits, %llu misses, %llu writes, %llu load failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes; plans %llu hits, %llu misses)\n", + const auto baseChanged = totals.hits != reported.hits || totals.misses != reported.misses || totals.writes != reported.writes || totals.loadFailures != reported.loadFailures || totals.writeFailures != reported.writeFailures || totals.planHits != reported.planHits || totals.planMisses != reported.planMisses || totals.planWrites != reported.planWrites || totals.planLoadFailures != reported.planLoadFailures; + const auto prebuiltChanged = totals.prebuiltHits != reported.prebuiltHits || totals.prebuiltMisses != reported.prebuiltMisses || totals.prebuiltRejected != reported.prebuiltRejected; + if (!baseChanged && !prebuiltChanged) return; + if (baseChanged) std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; resource plans: %llu hits, %llu misses, %llu writes, %llu load failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes; plans %llu hits, %llu misses)\n", delta(totals.hits, reported.hits), delta(totals.misses, reported.misses), delta(totals.writes, reported.writes), static_cast(totals.bytesWritten - reported.bytesWritten) / (1024.0 * 1024.0), delta(totals.loadFailures, reported.loadFailures), delta(totals.writeFailures, reported.writeFailures), delta(totals.planHits, reported.planHits), delta(totals.planMisses, reported.planMisses), delta(totals.planWrites, reported.planWrites), delta(totals.planLoadFailures, reported.planLoadFailures), static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes), static_cast(totals.planHits), static_cast(totals.planMisses)); + if (prebuiltChanged && (!packs.empty() || !looseDirectories.empty())) std::fprintf(stderr, "[shader-prebuilt] interval: %llu hits, %llu misses, %llu rejected; lookup %.3f ms, file read %.3f ms, decode %.3f ms; total load %.3f ms, max %.3f ms (lifetime hits %llu)\n", + delta(totals.prebuiltHits, reported.prebuiltHits), delta(totals.prebuiltMisses, reported.prebuiltMisses), delta(totals.prebuiltRejected, reported.prebuiltRejected), + (totals.lookupNanoseconds - reported.lookupNanoseconds) / 1e6, (totals.readNanoseconds - reported.readNanoseconds) / 1e6, (totals.decodeNanoseconds - reported.decodeNanoseconds) / 1e6, + (totals.prebuiltLoadNanoseconds - reported.prebuiltLoadNanoseconds) / 1e6, totals.maxLoadNanoseconds / 1e6, static_cast(totals.prebuiltHits)); reported = totals; reportedLoadNs = loadNs; } std::filesystem::path root; std::filesystem::path directory; + std::vector> packs; + std::vector looseDirectories; std::mutex mutex; std::condition_variable wake; std::condition_variable idle; @@ -1494,6 +1596,7 @@ class DiskStore { bool writing = false; std::atomic hits{0}, misses{0}, writes{0}, loadFailures{0}, writeFailures{0}, bytesRead{0}, bytesWritten{0}, loadNanoseconds{0}; std::atomic planHits{0}, planMisses{0}, planWrites{0}, planLoadFailures{0}; + std::atomic prebuiltHits{0}, prebuiltMisses{0}, prebuiltRejected{0}, lookupNanoseconds{0}, readNanoseconds{0}, decodeNanoseconds{0}, maxLoadNanoseconds{0}, prebuiltBytesRead{0}, prebuiltLoadNanoseconds{0}; std::atomic lastReport{0}; std::mutex reportMutex; Counters reported; diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 09feddad59f..d29d23ceb96 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -34,6 +34,16 @@ struct Counters { std::uint64_t planMisses = 0; std::uint64_t planWrites = 0; std::uint64_t planLoadFailures = 0; + std::uint64_t prebuiltHits = 0; + std::uint64_t prebuiltMisses = 0; + std::uint64_t prebuiltRejected = 0; + std::uint64_t lookupNanoseconds = 0; + std::uint64_t readNanoseconds = 0; + std::uint64_t decodeNanoseconds = 0; + std::uint64_t loadNanoseconds = 0; + std::uint64_t maxLoadNanoseconds = 0; + std::uint64_t prebuiltBytesRead = 0; + std::uint64_t prebuiltLoadNanoseconds = 0; }; [[nodiscard]] std::uint64_t SourceVersion(); From 93ab137194fa45d98220db6bf826cd23c5d258dc Mon Sep 17 00:00:00 2001 From: SaloQT <123214229+SaloQT@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:41:11 +1100 Subject: [PATCH 2/2] test(recompiler): cover prebuilt shader pack loading Build packs from EncodeEntry output and check the hit and miss paths, an entry stored under another key, a truncated entry, a damaged index, an out-of-range record, a foreign compiler source version and an unknown format. A child process covers pack discovery on a real disk load, the ANYPS5_NO_PREBUILT_SHADERS opt out and fall-through to the writable cache. ARCHITECTURE.md describes where the prebuilt lookup sits. --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 169 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index aad1cc12cd3..d308d3b4e47 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "CacheKey.hpp" +#include "ShaderCachePack.hpp" #include "VertexInputSpecialization.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -377,6 +378,171 @@ void verifyEntryRoundTrip() { require(ShaderDiskCache::DecodeEntry(incompatibleFile, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an entry with an incompatible runtime ABI loads"); } +std::vector packBytes(const std::vector, std::vector>>& entries) { + using namespace ShaderRecompiler::ShaderDiskCache; + std::vector records; + std::uint64_t offset = sizeof(PackHeader) + entries.size() * sizeof(PackRecord); + for (const auto& [key, entry] : entries) { + records.push_back({PackKeyHash(key), offset, entry.size()}); + offset += entry.size(); + } + std::sort(records.begin(), records.end(), [](const PackRecord& left, const PackRecord& right) { return left.hash < right.hash; }); + PackHeader header; + header.sourceVersion = SourceVersion(); + header.fileBytes = offset; + header.entryCount = entries.size(); + header.indexHash = ShaderRecompiler::HashBytes(std::as_bytes(std::span(records))); + std::vector bytes(offset); + std::memcpy(bytes.data(), &header, sizeof(header)); + std::memcpy(bytes.data() + sizeof(header), records.data(), records.size() * sizeof(PackRecord)); + std::size_t at = sizeof(header) + records.size() * sizeof(PackRecord); + for (const auto& [key, entry] : entries) { + std::memcpy(bytes.data() + at, entry.data(), entry.size()); + at += entry.size(); + } + return bytes; +} + +void refreshIndexHash(std::vector& pack, std::size_t entries) { + using namespace ShaderRecompiler::ShaderDiskCache; + const auto index = std::span(pack).subspan(sizeof(PackHeader), entries * sizeof(PackRecord)); + const auto hash = ShaderRecompiler::HashBytes(index); + std::memcpy(pack.data() + offsetof(PackHeader, indexHash), &hash, sizeof(hash)); +} + +void verifyPrebuiltPack(const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + const auto file = EncodeEntry(key, variant); + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + const auto otherFile = EncodeEntry(otherKey, variant); + std::filesystem::create_directories(directory); + const auto writePack = [&](const char* name, const std::vector& bytes) { + const auto path = directory / name; + require(WriteFileAtomically(path, bytes), "cannot write the test pack"); + return path; + }; + PackLoadTimings timings; + CompiledVariant decoded; + { + CachePack pack(writePack("valid.pack", packBytes({{key, file}, {otherKey, otherFile}}))); + require(pack.Valid(), "a valid pack is rejected: " + pack.Error()); + require(pack.Count() == 2, "the pack does not hold both entries"); + require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a pack entry does not load"); + requireSameVariant(variant, decoded, "pack round trip"); + auto missingKey = key; + missingKey.front() ^= std::byte{1}; + require(pack.Load(missingKey, decoded, timings) == LoadStatus::Absent, "an absent key loads from the pack"); + } + { + std::vector, std::vector>> mismatched; + mismatched.emplace_back(otherKey, file); + CachePack pack(writePack("mismatch.pack", packBytes(mismatched))); + require(pack.Valid(), "the key mismatch pack is rejected: " + pack.Error()); + require(pack.Load(otherKey, decoded, timings) != LoadStatus::Loaded, "an entry stored under another key loads"); + } + { + auto bytes = packBytes({{key, file}}); + const auto truncated = static_cast(file.size() / 2); + std::memcpy(bytes.data() + sizeof(PackHeader) + offsetof(PackRecord, size), &truncated, sizeof(truncated)); + refreshIndexHash(bytes, 1); + CachePack pack(writePack("truncated.pack", bytes)); + require(pack.Valid(), "the truncated-entry pack is rejected: " + pack.Error()); + require(pack.Load(key, decoded, timings) == LoadStatus::Rejected, "a truncated pack entry loads"); + } + { + auto bytes = packBytes({{key, file}}); + bytes[sizeof(PackHeader)] ^= std::byte{0x10}; + CachePack pack(writePack("corrupt-index.pack", bytes)); + require(!pack.Valid(), "a pack with a damaged index is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + const auto outside = static_cast(bytes.size()) + 16; + std::memcpy(bytes.data() + sizeof(PackHeader) + offsetof(PackRecord, offset), &outside, sizeof(outside)); + refreshIndexHash(bytes, 1); + CachePack pack(writePack("corrupt-record.pack", bytes)); + require(!pack.Valid(), "a pack with an out-of-range record is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + const auto wrong = SourceVersion() + 1; + std::memcpy(bytes.data() + offsetof(PackHeader, sourceVersion), &wrong, sizeof(wrong)); + CachePack pack(writePack("wrong-version.pack", bytes)); + require(!pack.Valid(), "a pack from another compiler source version is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + bytes.front() ^= std::byte{0x10}; + CachePack pack(writePack("unknown-format.pack", bytes)); + require(!pack.Valid(), "a pack with an unknown format is accepted"); + } +} + +SampleRequest prebuiltSample() { + SampleRequest sample; + sample.code[3] ^= 0x5a5a5a5au; + return sample; +} + +int runPrebuiltLoadingProcess() { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample = prebuiltSample(); + const auto key = sample.Key(); + const auto expected = sampleVariant(); + CompiledVariant loaded; + require(Load(key, loaded), "the prebuilt pack entry does not load"); + requireSameVariant(expected, loaded, "prebuilt load"); + require(Totals().prebuiltHits == 1, "the prebuilt load was not counted"); + const auto afterPack = Totals(); + require(afterPack.prebuiltBytesRead > 0 && afterPack.prebuiltLoadNanoseconds > 0, "the prebuilt load was not timed"); + require(afterPack.hits == 0 && afterPack.loadFailures == 0 && afterPack.bytesRead == 0 && afterPack.loadNanoseconds == 0, "the prebuilt load reached the writable totals"); + auto looseKey = key; + looseKey.back() ^= std::byte{1}; + CompiledVariant loose; + require(Load(looseKey, loose), "the loose prebuilt entry does not load"); + requireSameVariant(expected, loose, "loose prebuilt load"); + require(Totals().prebuiltHits == 2, "the loose prebuilt load was not counted"); + require(Totals().hits == 0 && Totals().loadFailures == 0 && Totals().bytesRead == 0, "the loose prebuilt load reached the writable totals"); + auto corruptKey = looseKey; + corruptKey.front() ^= std::byte{1}; + CompiledVariant corrupt; + require(!Load(corruptKey, corrupt), "a corrupt loose prebuilt entry loads"); + require(Totals().prebuiltRejected >= 1, "the corrupt loose prebuilt entry was not counted as rejected"); + require(Totals().loadFailures == 0, "the corrupt loose prebuilt entry reached the writable failures"); + auto otherKey = looseKey; + otherKey.front() ^= std::byte{1}; + CompiledVariant other; + require(!Load(otherKey, other), "a key absent from prebuilt storage loads"); + Store(otherKey, std::make_shared(sampleVariant())); + Flush(); + require(Load(otherKey, other), "the writable cache fall-through does not load"); + std::cout << "prebuilt process loaded from the pack and fell through to the writable cache\n"; + return 0; +} + +void verifyPrebuiltAcrossProcesses(const char* self, const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample = prebuiltSample(); + const auto prebuilt = directory / "prebuilt"; + std::filesystem::create_directories(prebuilt); + const auto bytes = packBytes({{sample.Key(), EncodeEntry(sample.Key(), sampleVariant())}}); + require(WriteFileAtomically(prebuilt / "test.pack", bytes), "cannot write the prebuilt pack"); + auto looseKey = sample.Key(); + looseKey.back() ^= std::byte{1}; + require(WriteFileAtomically(prebuilt / EntryName(looseKey), EncodeEntry(looseKey, sampleVariant())), "cannot write the loose prebuilt entry"); + auto corruptKey = looseKey; + corruptKey.front() ^= std::byte{1}; + auto corruptEntry = EncodeEntry(corruptKey, sampleVariant()); + corruptEntry.resize(corruptEntry.size() / 2); + require(WriteFileAtomically(prebuilt / EntryName(corruptKey), corruptEntry), "cannot write the corrupt loose prebuilt entry"); + const std::string command = "\"" + std::string(self) + "\" --load-prebuilt"; + require(std::system(command.c_str()) == 0, "the prebuilt loading process failed"); +} + void verifyArtifactStorageIsolation() { SampleRequest sample; const auto key = sample.Key(); @@ -1906,6 +2072,7 @@ void verifyDefaultDirectory(const char* self) { int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); + if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); if (argc == 2 && std::string_view(argv[1]).starts_with("--load-plan")) return runPlanLoadingProcess(argv[1]); const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}())); @@ -1916,6 +2083,7 @@ int main(int argc, char** argv) { verifyAtomicReplacement(directory); verifyResultRoundTrip(); verifyEntryRoundTrip(); + verifyPrebuiltPack(directory / "packs"); verifyArtifactStorageIsolation(); verifyKeySensitivity(); verifyFloatModeSerialization(); @@ -1924,6 +2092,7 @@ int main(int argc, char** argv) { verifyStore(); verifyPersist(); verifyAcrossProcesses(argv[0]); + verifyPrebuiltAcrossProcesses(argv[0], directory); verifyPlanAcrossProcesses(argv[0]); verifyEmissionFailureMemo(); verifyFailureMemoSwitch(argv[0]); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index e73a415d40a..235bf72314e 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -94,7 +94,7 @@ flowchart LR ``` - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. -- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. +- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers.