#include "ShaderDiskCache.hpp" #include "CacheKey.hpp" #include "ShaderCachePack.hpp" #include "ShaderCacheDirectory.hpp" #include "ThreadOwned.hpp" #include "Optimization/ResourceMaterializer.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #elif defined(__APPLE__) #include #endif #ifndef _WIN32 #include #include #include #include #endif #include "ShaderCacheSourceVersion.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ShaderRecompiler { std::filesystem::path ShaderCacheDirectory() { const char* disabled = std::getenv("ANYPS5_NO_SHADER_CACHE"); if (disabled != nullptr && *disabled != '\0' && std::strcmp(disabled, "0") != 0) return {}; #ifdef _WIN32 const wchar_t* directory = _wgetenv(L"ANYPS5_SHADER_CACHE_DIR"); if (directory != nullptr && *directory != L'\0') return std::filesystem::path(directory); std::wstring executable(MAX_PATH, L'\0'); for (;;) { const auto length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); if (length == 0) return {}; if (length < executable.size()) { executable.resize(length); break; } executable.resize(executable.size() * 2); } return std::filesystem::path(executable).parent_path() / "shader_cache"; #else const char* directory = std::getenv("ANYPS5_SHADER_CACHE_DIR"); if (directory != nullptr && *directory != '\0') return std::filesystem::path(directory); std::error_code error; #ifdef __APPLE__ std::uint32_t size = 0; _NSGetExecutablePath(nullptr, &size); std::vector buffer(size); if (_NSGetExecutablePath(buffer.data(), &size) != 0) return {}; const auto executable = std::filesystem::canonical(buffer.data(), error); #else const auto executable = std::filesystem::read_symlink("/proc/self/exe", error); #endif if (error) return {}; return executable.parent_path() / "shader_cache"; #endif } } namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); static_assert(sizeof(FragmentParameter) == 16, "FragmentParameter changed: update the parameter encoder"); static_assert(sizeof(CompiledShaderInfo) == 352, "CompiledShaderInfo changed: update the info encoder"); static_assert(sizeof(ShaderInfo) == 232, "ShaderInfo changed: update the info encoder"); static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder"); static_assert(sizeof(ImageResource) == 92, "ImageResource changed: update the info encoder"); static_assert(sizeof(SamplerResource) == 24, "SamplerResource changed: update the info encoder"); static_assert(sizeof(SampledResourcePair) == 12, "SampledResourcePair changed: update the info encoder"); static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info encoder"); static_assert(sizeof(StageOutput) == 48, "StageOutput changed: update the info encoder"); static_assert(sizeof(IrBindingLayout) == 80, "IrBindingLayout changed: update the layout encoder"); static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: update the layout encoder"); static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 712, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); static_assert(sizeof(DescriptorSource) == 160, "DescriptorSource changed: update the plan encoder"); static_assert(sizeof(TableColumn) == 80, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); static_assert(sizeof(UniformFill) == 28, "UniformFill changed: update the plan encoder"); static_assert(sizeof(UniformFillPlan) == 64, "UniformFillPlan changed: update the plan encoder"); #endif namespace { struct AnyField { template operator TField() const; }; template constexpr bool bracedWith(std::index_sequence) { return requires { TStruct{(static_cast(Index), AnyField{})...}; }; } template constexpr std::size_t fieldCount() { if constexpr (bracedWith(std::make_index_sequence{})) { return fieldCount(); } else { return Count; } } static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); static_assert(fieldCount() == 38, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); static_assert(fieldCount() == 23, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); static_assert(fieldCount() == 15, "TableColumn changed: update the plan encoder"); static_assert(fieldCount() == 4, "KeyDomain changed: update the plan encoder"); static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); static_assert(fieldCount() == 5, "UniformFill changed: update the plan encoder"); static_assert(fieldCount() == 2, "UniformFillPlan changed: update the plan encoder"); constexpr std::uint32_t FileMagic = 0x43535041u; struct FileHeader { std::uint32_t magic; std::uint32_t format; std::uint64_t sourceVersion; std::uint64_t keyBytes; std::uint64_t payloadBytes; std::uint64_t keyHash; std::uint64_t payloadHash; }; std::size_t PayloadOffset(std::size_t keyBytes) { constexpr std::size_t alignment = 8; return (sizeof(FileHeader) + keyBytes + alignment - 1) / alignment * alignment; } class MappedFile { public: MappedFile(const void* data, std::size_t size) : data(data), size(size) {} MappedFile(const MappedFile&) = delete; MappedFile& operator=(const MappedFile&) = delete; ~MappedFile() { #ifdef _WIN32 UnmapViewOfFile(data); #else munmap(const_cast(data), size); #endif } [[nodiscard]] std::span Bytes() const { return {static_cast(data), size}; } private: const void* data; std::size_t size; }; std::shared_ptr MapFile(const std::filesystem::path& path) { #ifdef _WIN32 const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return nullptr; LARGE_INTEGER length{}; if (!GetFileSizeEx(file, &length) || length.QuadPart <= 0 || static_cast(length.QuadPart) > std::numeric_limits::max()) { CloseHandle(file); return nullptr; } const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); CloseHandle(file); if (mapping == nullptr) return nullptr; const void* data = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); CloseHandle(mapping); if (data == nullptr) return nullptr; try { return std::make_shared(data, static_cast(length.QuadPart)); } catch (...) { UnmapViewOfFile(data); throw; } #else const int descriptor = open(path.c_str(), O_RDONLY | O_CLOEXEC); if (descriptor < 0) return nullptr; struct stat status{}; void* data = MAP_FAILED; if (fstat(descriptor, &status) == 0 && status.st_size > 0) data = mmap(nullptr, static_cast(status.st_size), PROT_READ, MAP_PRIVATE, descriptor, 0); close(descriptor); if (data == MAP_FAILED) return nullptr; try { return std::make_shared(data, static_cast(status.st_size)); } catch (...) { munmap(data, static_cast(status.st_size)); throw; } #endif } static_assert(sizeof(FileHeader) == 48 && std::is_trivially_copyable_v); class Writer { public: explicit Writer(std::vector& out) : out(out) {} template void Value(TValue value) requires (std::is_integral_v || std::is_enum_v) { if constexpr (std::is_same_v) { Raw(static_cast(value ? 1u : 0u)); } else if constexpr (std::is_enum_v) { Raw(static_cast(value)); } else { Raw(value); } } template void Values(std::span values) requires std::is_integral_v { Value(values.size()); const auto bytes = std::as_bytes(values); out.insert(out.end(), bytes.begin(), bytes.end()); } void Flags(const std::vector& flags) { Value(flags.size()); for (const bool flag : flags) Value(flag); } void Compact(std::uint64_t value) { while (value >= 0x80u) { Raw(static_cast(value | 0x80u)); value >>= 7u; } Raw(static_cast(value)); } void Text(const std::string& text) { Value(text.size()); const auto bytes = std::as_bytes(std::span(text.data(), text.size())); out.insert(out.end(), bytes.begin(), bytes.end()); } template void List(const std::vector& values, TEncode&& encode) { Value(values.size()); for (const auto& value : values) encode(*this, value); } private: template void Raw(TValue value) { const auto offset = out.size(); out.resize(offset + sizeof(value)); std::memcpy(out.data() + offset, &value, sizeof(value)); } std::vector& out; }; class Reader { public: explicit Reader(std::span bytes, std::shared_ptr owner = nullptr) : bytes(bytes), owner(std::move(owner)) {} [[nodiscard]] bool Ok() const { return ok; } [[nodiscard]] bool Done() const { return ok && position == bytes.size(); } [[nodiscard]] std::size_t Remaining() const { return ok ? bytes.size() - position : 0; } void Fail() { ok = false; } [[nodiscard]] std::uint64_t Compact() { std::uint64_t value = 0; for (std::uint32_t shift = 0; shift < 64u; shift += 7u) { const auto byte = Raw(); if (!ok) return 0; value |= static_cast(byte & 0x7fu) << shift; if ((byte & 0x80u) != 0u) continue; if ((byte == 0u && shift != 0u) || (shift == 63u && byte > 1u)) ok = false; return ok ? value : 0; } ok = false; return 0; } template void Value(TValue& value) requires (std::is_integral_v || std::is_enum_v) { if constexpr (std::is_same_v) { const auto raw = Raw(); if (raw > 1u) ok = false; value = raw == 1u; } else if constexpr (std::is_enum_v) { value = static_cast(Raw()); } else { value = Raw(); } } template [[nodiscard]] TValue Get() { TValue value{}; Value(value); return value; } template void Values(std::vector& values) requires std::is_integral_v { const auto count = Count(sizeof(TValue)); values.resize(count); if (count != 0) { std::memcpy(values.data(), bytes.data() + position, count * sizeof(TValue)); position += count * sizeof(TValue); } } SharedSpirv Spirv() { const auto count = Count(sizeof(std::uint32_t)); const auto* first = bytes.data() + position; position += count * sizeof(std::uint32_t); if (owner != nullptr && reinterpret_cast(first) % alignof(std::uint32_t) == 0) { return SharedSpirv(owner, std::span(reinterpret_cast(first), count)); } std::vector words(count); if (count != 0) std::memcpy(words.data(), first, count * sizeof(std::uint32_t)); return SharedSpirv(std::move(words)); } void Flags(std::vector& flags) { const auto count = Count(1); flags.assign(count, false); for (std::size_t i = 0; i < count; ++i) flags[i] = Get(); } void Text(std::string& text) { const auto count = Count(1); text.assign(reinterpret_cast(bytes.data() + position), count); position += count; } template void List(std::vector& values, std::size_t minimum, TDecode&& decode) { const auto count = Count(minimum); values.assign(count, TValue{}); for (auto& value : values) { if (!ok) break; decode(*this, value); } } private: template TValue Raw() { TValue value{}; if (!ok || bytes.size() - position < sizeof(value)) { ok = false; return value; } std::memcpy(&value, bytes.data() + position, sizeof(value)); position += sizeof(value); return value; } std::size_t Count(std::size_t elementBytes) { const auto count = Raw(); if (!ok || elementBytes == 0 || count > (bytes.size() - position) / elementBytes) { ok = false; return 0; } return static_cast(count); } std::span bytes; std::shared_ptr owner; std::size_t position = 0; bool ok = true; }; void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Value(binding.kind); writer.Value(binding.role); writer.Value(binding.descriptorSet); writer.Value(binding.binding); writer.Value(binding.count); writer.Values(std::span(binding.guestDescriptor)); writer.Value(binding.readOnly); writer.Value(binding.imageShape.has_value()); writer.Value(binding.imageShape.value_or(DescriptorImageShape::Image1D)); writer.Flags(binding.samplerDepthCompare); writer.Flags(binding.imageWritten); writer.Flags(binding.imageDepthCompare); writer.Flags(binding.imageAtomic); writer.Flags(binding.imageAtomic64); writer.List(binding.imageShapes, [](Writer& out, DescriptorImageShape shape) { out.Value(shape); }); writer.Flags(binding.bufferAtomic); writer.Flags(binding.bufferWritten); writer.Flags(binding.samplerUnnormalized); writer.Flags(binding.imageUnnormalized); writer.Flags(binding.imageUnnormalizedBaseLevel); writer.Flags(binding.samplerIntegerBorder); writer.Values(std::span(binding.imageSamplers)); writer.Flags(binding.bufferRead); } void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Value(binding.kind); reader.Value(binding.role); reader.Value(binding.descriptorSet); reader.Value(binding.binding); reader.Value(binding.count); reader.Values(binding.guestDescriptor); reader.Value(binding.readOnly); const bool hasShape = reader.Get(); const auto shape = reader.Get(); binding.imageShape = hasShape ? std::optional(shape) : std::nullopt; reader.Flags(binding.samplerDepthCompare); reader.Flags(binding.imageWritten); reader.Flags(binding.imageDepthCompare); reader.Flags(binding.imageAtomic); reader.Flags(binding.imageAtomic64); reader.List(binding.imageShapes, 4, [](Reader& in, DescriptorImageShape& shape) { in.Value(shape); }); reader.Flags(binding.bufferAtomic); reader.Flags(binding.bufferWritten); reader.Flags(binding.samplerUnnormalized); reader.Flags(binding.imageUnnormalized); reader.Flags(binding.imageUnnormalizedBaseLevel); reader.Flags(binding.samplerIntegerBorder); reader.Values(binding.imageSamplers); reader.Flags(binding.bufferRead); } void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.runtimeAbiVersion); writer.Values(std::span(result.spirv.Words())); writer.Value(result.bdaAbiVersion); writer.Value(result.memoryOffsetDword); writer.Value(result.shaderDataDwords); writer.Value(result.imageMetadataDword); writer.Value(result.runtimeImageCount); writer.Values(std::span(result.runtimeImageResources)); writer.Value(result.registrationModule); writer.List(result.vertexInputPatches, [](Writer& out, const VertexInputPatch& patch) { out.Value(patch.location); out.Value(patch.word); for (const auto value : patch.values) out.Value(value); }); writer.List(result.vertexInputs, [](Writer& out, const VertexInput& attribute) { out.Value(attribute.location); out.Value(attribute.components); out.Value(attribute.fetchIndex); out.Value(attribute.outputMask); }); writer.Value(result.vertexOffsetSgpr); writer.Value(result.instanceOffsetSgpr); writer.Value(result.vertexOffsetShared); writer.Value(result.instanceOffsetShared); writer.Value(result.vertexOffsetConflict); writer.Value(result.instanceOffsetConflict); writer.Value(result.hostSubgroupSize); writer.Value(result.fullSubgroups); writer.Values(std::span(result.parameterExports)); writer.List(result.fragmentParameters, [](Writer& out, const FragmentParameter& parameter) { out.Value(parameter.location); out.Value(parameter.sourceLocation); out.Value(parameter.flat); out.Value(parameter.perVertex); out.Value(parameter.custom); out.Value(parameter.flatP10); out.Value(parameter.vertexOrder); }); writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); writer.Value(result.barycentricEmulation.linear); writer.Values(std::span(result.stencilReferences)); writer.Value(result.stencilReplaySafe); writer.Value(result.stencilReferenceDword); } void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.runtimeAbiVersion); result.spirv = reader.Spirv(); reader.Value(result.bdaAbiVersion); reader.Value(result.memoryOffsetDword); reader.Value(result.shaderDataDwords); reader.Value(result.imageMetadataDword); reader.Value(result.runtimeImageCount); reader.Values(result.runtimeImageResources); reader.Value(result.registrationModule); reader.List(result.vertexInputPatches, 20, [](Reader& in, VertexInputPatch& patch) { in.Value(patch.location); in.Value(patch.word); for (auto& value : patch.values) in.Value(value); }); reader.List(result.vertexInputs, 16, [](Reader& in, VertexInput& attribute) { in.Value(attribute.location); in.Value(attribute.components); in.Value(attribute.fetchIndex); in.Value(attribute.outputMask); }); reader.Value(result.vertexOffsetSgpr); reader.Value(result.instanceOffsetSgpr); reader.Value(result.vertexOffsetShared); reader.Value(result.instanceOffsetShared); reader.Value(result.vertexOffsetConflict); reader.Value(result.instanceOffsetConflict); reader.Value(result.hostSubgroupSize); reader.Value(result.fullSubgroups); reader.Values(result.parameterExports); reader.List(result.fragmentParameters, 10, [](Reader& in, FragmentParameter& parameter) { in.Value(parameter.location); in.Value(parameter.sourceLocation); in.Value(parameter.flat); in.Value(parameter.perVertex); in.Value(parameter.custom); in.Value(parameter.flatP10); in.Value(parameter.vertexOrder); }); reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); reader.Value(result.barycentricEmulation.linear); reader.Values(result.stencilReferences); reader.Value(result.stencilReplaySafe); reader.Value(result.stencilReferenceDword); result.variantId = 0; } void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) { writer.List(invocation.specialization, [](Writer& out, const PipelineSpecializationConstant& constant) { out.Value(constant.id); out.Value(constant.value); }); writer.List(invocation.bindings, encodeBinding); writer.Value(invocation.pushConstants.size()); for (const auto byte : invocation.pushConstants) writer.Value(static_cast(byte)); writer.List(invocation.vertexAttributes, [](Writer& out, const VertexAttribute& attribute) { out.Value(attribute.location); out.Value(attribute.components); for (const auto field : attribute.resource.fields) out.Value(field); out.Value(attribute.fetchIndex); out.Value(attribute.formatComponents); }); writer.Value(invocation.poisonedSrtReads); writer.List(invocation.imageTablePoison, [](Writer& out, const ImageTableEntryPoison& poison) { for (const auto word : poison.words) out.Value(word); out.Value(poison.dwordCount); out.Value(poison.resource); out.Value(poison.reason); }); writer.List(invocation.imageTableRanges, [](Writer& out, const std::pair& range) { out.Value(range.first); out.Value(range.second); }); writer.List(invocation.imageTableReadRanges, [](Writer& out, const auto& ranges) { out.List(ranges, [](Writer& rangeWriter, const auto& range) { rangeWriter.Value(range.first); rangeWriter.Value(range.second); }); }); writer.Value(invocation.imageTableShader); writer.Value(invocation.imageTableFaults); } void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { invocation.specializationId = 0; reader.List(invocation.specialization, 8, [](Reader& in, PipelineSpecializationConstant& constant) { in.Value(constant.id); in.Value(constant.value); }); reader.List(invocation.bindings, 8, decodeBinding); std::vector pushConstants; reader.Values(pushConstants); invocation.pushConstants.resize(pushConstants.size()); if (!pushConstants.empty()) std::memcpy(invocation.pushConstants.data(), pushConstants.data(), pushConstants.size()); reader.List(invocation.vertexAttributes, 32, [](Reader& in, VertexAttribute& attribute) { in.Value(attribute.location); in.Value(attribute.components); for (auto& field : attribute.resource.fields) in.Value(field); in.Value(attribute.fetchIndex); in.Value(attribute.formatComponents); }); reader.Value(invocation.poisonedSrtReads); reader.List(invocation.imageTablePoison, 44, [](Reader& in, ImageTableEntryPoison& poison) { for (auto& word : poison.words) in.Value(word); in.Value(poison.dwordCount); in.Value(poison.resource); in.Value(poison.reason); }); reader.List(invocation.imageTableRanges, 16, [](Reader& in, std::pair& range) { in.Value(range.first); in.Value(range.second); }); reader.List(invocation.imageTableReadRanges, 8, [](Reader& in, auto& ranges) { in.List(ranges, 16, [](Reader& rangeReader, auto& range) { rangeReader.Value(range.first); rangeReader.Value(range.second); }); }); reader.Value(invocation.imageTableShader); reader.Value(invocation.imageTableFaults); } void encodeLayout(Writer& writer, const IrBindingLayout& layout) { writer.Value(layout.pushDataStartDword); writer.Value(layout.stencilReferenceDword); writer.Value(layout.memoryOffsetDword); writer.Value(layout.memoryOffsetCount); writer.Value(layout.dispatchThreadLimit); writer.Value(layout.runtimeImageCount); writer.Value(layout.runtimeStateDwords); writer.Values(std::span(layout.userDataRegisters)); writer.List(layout.descriptors, [](Writer& out, const IrDescriptorBinding& descriptor) { out.Value(descriptor.kind); out.Values(std::span(descriptor.resources)); }); } void decodeLayout(Reader& reader, IrBindingLayout& layout) { reader.Value(layout.pushDataStartDword); reader.Value(layout.stencilReferenceDword); reader.Value(layout.memoryOffsetDword); reader.Value(layout.memoryOffsetCount); reader.Value(layout.dispatchThreadLimit); reader.Value(layout.runtimeImageCount); reader.Value(layout.runtimeStateDwords); reader.Values(layout.userDataRegisters); reader.List(layout.descriptors, 12, [](Reader& in, IrDescriptorBinding& descriptor) { in.Value(descriptor.kind); in.Values(descriptor.resources); }); } void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { writer.Value(info.scratchDwords); writer.Value(info.sharedMemoryBytes); writer.List(info.buffers, [](Writer& out, const BufferResource& buffer) { out.Value(buffer.source); out.Value(buffer.firstUsePc); out.Value(buffer.maxByteExtent); out.Value(buffer.imageAlias); out.Value(buffer.read); out.Value(buffer.written); out.Value(buffer.atomic); out.Value(buffer.formatted); out.Value(buffer.descriptorFormatted); out.Value(buffer.formattedReadMask); out.Value(buffer.scalar); out.Value(buffer.typedAlignment); }); writer.List(info.images, [](Writer& out, const ImageResource& image) { out.Value(image.source); out.Value(image.firstUsePc); out.Value(image.resourceClass); out.Value(image.numericClass); out.Value(image.dimension); out.Value(image.mipMode); out.Value(image.mipCount); out.Value(image.conversionFormat); out.Value(image.shaderSwizzle); out.Value(image.read); out.Value(image.written); out.Value(image.atomic); out.Value(image.atomic64); out.Value(image.depthCompare); out.Value(image.cube); out.Value(image.r128); out.Value(image.srgbDecode); out.Value(image.srgbDecodeCompatible); out.Value(image.srgbDecodeFormats); out.Value(image.depthBits); out.Value(image.depthUnorm16); out.Value(image.packed); out.Value(image.fmaskCompatible); out.Value(image.depthBitsCompatible); out.Value(image.constantSwizzle); out.Value(image.constantSwizzleCompatible); out.Value(image.cubeFaceCompatible); out.Value(image.flatVolumeCompatible); out.Value(image.singleMipPlaneBiasCompatible); out.Value(image.flatLineCompatible); out.Value(image.flatVolumeLineCompatible); out.Value(image.foldTexelOffsetsOnly); out.Value(image.byElements); out.Value(image.byComponents); out.Value(image.packedFormat); out.Value(image.emulatedCompare); out.Value(image.table); out.Value(image.tableOperation); }); writer.Value(!info.runtimeImageModes.empty()); writer.List(info.samplers, [](Writer& out, const SamplerResource& sampler) { out.Value(sampler.source); out.Value(sampler.firstUsePc); out.Value(sampler.copyOf); out.Value(sampler.forcePointFiltering); out.Value(sampler.depthCompare); out.Value(sampler.uses); out.Value(sampler.foldTexelOffsetsOnly); out.Value(sampler.table); }); writer.List(info.sampledPairs, [](Writer& out, const SampledResourcePair& pair) { out.Value(pair.image); out.Value(pair.sampler); out.Value(pair.firstUsePc); }); writer.List(info.inputs, [](Writer& out, const StageInput& input) { out.Value(input.kind); out.Value(input.location); out.Value(input.componentCount); out.Text(input.debugName); out.Value(input.perVertex); }); writer.List(info.outputs, [](Writer& out, const StageOutput& output) { out.Value(output.kind); out.Value(output.index); out.Value(output.location); out.Text(output.debugName); }); for (const auto components : info.vertexFetchComponents) writer.Value(components); writer.Value(info.vertexOffsetSgpr); writer.Value(info.instanceOffsetSgpr); writer.Value(info.vertexOffsetShared); writer.Value(info.instanceOffsetShared); writer.Value(info.vertexOffsetConflict); writer.Value(info.instanceOffsetConflict); writer.Value(info.hasBitwiseXor); writer.Value(info.usesDma); writer.Value(info.bdaWrites); writer.Value(info.usesFaultBuffer); writer.Value(info.dispatchThreadLimit); writer.Value(info.rasterPrimitive); writer.Value(info.layerExported); } void decodeShaderInfo(Reader& reader, ShaderInfo& info) { reader.Value(info.scratchDwords); reader.Value(info.sharedMemoryBytes); reader.List(info.buffers, 27, [](Reader& in, BufferResource& buffer) { in.Value(buffer.source); in.Value(buffer.firstUsePc); in.Value(buffer.maxByteExtent); in.Value(buffer.imageAlias); in.Value(buffer.read); in.Value(buffer.written); in.Value(buffer.atomic); in.Value(buffer.formatted); in.Value(buffer.descriptorFormatted); in.Value(buffer.formattedReadMask); in.Value(buffer.scalar); in.Value(buffer.typedAlignment); }); reader.List(info.images, 77, [](Reader& in, ImageResource& image) { in.Value(image.source); in.Value(image.firstUsePc); in.Value(image.resourceClass); in.Value(image.numericClass); in.Value(image.dimension); in.Value(image.mipMode); in.Value(image.mipCount); in.Value(image.conversionFormat); in.Value(image.shaderSwizzle); in.Value(image.read); in.Value(image.written); in.Value(image.atomic); in.Value(image.atomic64); in.Value(image.depthCompare); in.Value(image.cube); in.Value(image.r128); in.Value(image.srgbDecode); in.Value(image.srgbDecodeCompatible); in.Value(image.srgbDecodeFormats); in.Value(image.depthBits); in.Value(image.depthUnorm16); in.Value(image.packed); in.Value(image.fmaskCompatible); in.Value(image.depthBitsCompatible); in.Value(image.constantSwizzle); in.Value(image.constantSwizzleCompatible); in.Value(image.cubeFaceCompatible); in.Value(image.flatVolumeCompatible); in.Value(image.singleMipPlaneBiasCompatible); in.Value(image.flatLineCompatible); in.Value(image.flatVolumeLineCompatible); in.Value(image.foldTexelOffsetsOnly); in.Value(image.byElements); in.Value(image.byComponents); in.Value(image.packedFormat); in.Value(image.emulatedCompare); in.Value(image.table); in.Value(image.tableOperation); }); bool preparedImageModes = false; reader.Value(preparedImageModes); if (preparedImageModes) ResourceMaterializer::PrepareImageModes(info); reader.List(info.samplers, 21, [](Reader& in, SamplerResource& sampler) { in.Value(sampler.source); in.Value(sampler.firstUsePc); in.Value(sampler.copyOf); in.Value(sampler.forcePointFiltering); in.Value(sampler.depthCompare); in.Value(sampler.uses); in.Value(sampler.foldTexelOffsetsOnly); in.Value(sampler.table); }); reader.List(info.sampledPairs, 12, [](Reader& in, SampledResourcePair& pair) { in.Value(pair.image); in.Value(pair.sampler); in.Value(pair.firstUsePc); }); reader.List(info.inputs, 21, [](Reader& in, StageInput& input) { in.Value(input.kind); in.Value(input.location); in.Value(input.componentCount); in.Text(input.debugName); in.Value(input.perVertex); }); reader.List(info.outputs, 20, [](Reader& in, StageOutput& output) { in.Value(output.kind); in.Value(output.index); in.Value(output.location); in.Text(output.debugName); }); for (auto& components : info.vertexFetchComponents) reader.Value(components); reader.Value(info.vertexOffsetSgpr); reader.Value(info.instanceOffsetSgpr); reader.Value(info.vertexOffsetShared); reader.Value(info.instanceOffsetShared); reader.Value(info.vertexOffsetConflict); reader.Value(info.instanceOffsetConflict); reader.Value(info.hasBitwiseXor); reader.Value(info.usesDma); reader.Value(info.bdaWrites); reader.Value(info.usesFaultBuffer); reader.Value(info.dispatchThreadLimit); reader.Value(info.rasterPrimitive); reader.Value(info.layerExported); } void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { writer.Value(compiled.stage); writer.Value(compiled.shaderHash); writer.Value(compiled.waveSize); writer.Value(compiled.userDataBase); writer.Value(compiled.userDataCount); writer.Value(compiled.scratchDwords); writer.Value(compiled.paramExportMask); encodeShaderInfo(writer, compiled.info); encodeLayout(writer, compiled.bindings); } void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { reader.Value(compiled.stage); reader.Value(compiled.shaderHash); reader.Value(compiled.waveSize); reader.Value(compiled.userDataBase); reader.Value(compiled.userDataCount); reader.Value(compiled.scratchDwords); reader.Value(compiled.paramExportMask); decodeShaderInfo(reader, compiled.info); decodeLayout(reader, compiled.bindings); } void encodeAllocation(Writer& writer, const CompiledBindingLayout& allocation) { encodeLayout(writer, allocation.layout); writer.Value(allocation.pushConstantOffsetBytes); writer.Value(allocation.pushConstantSizeBytes); } void decodeAllocation(Reader& reader, CompiledBindingLayout& allocation) { decodeLayout(reader, allocation.layout); reader.Value(allocation.pushConstantOffsetBytes); reader.Value(allocation.pushConstantSizeBytes); } constexpr std::uint32_t PlanEntryKind = 0x4e414c50u; constexpr std::uint32_t NoPlanIndex = std::numeric_limits::max(); void encodeMemoryInfo(Writer& out, const MemoryInfo& memory) { out.Value(memory.kind); out.Value(memory.resource); out.Value(memory.sampler); out.Value(memory.offset); out.Value(memory.secondaryOffset); out.Value(memory.dmask); out.Value(memory.dataDwords); out.Value(memory.dataBits); out.Value(memory.componentIndex); out.Value(memory.componentCount); out.Value(memory.dataFormat); out.Value(memory.numberFormat); out.Value(memory.imageSampleFlags); out.Value(memory.imageDimension); out.Value(memory.imageAddressComponents); out.Value(memory.addressIsFull); out.Value(memory.dataSigned); out.Value(memory.typed); out.Value(memory.formatted); out.Value(memory.d16); out.Value(memory.imageHasMip); out.Value(memory.imageR128); out.Value(memory.imagePacked); out.Value(memory.imageByElements); out.Value(memory.idxen); out.Value(memory.offen); out.Value(memory.planningOnly); out.Value(memory.coherent); out.Value(memory.gpuDescriptor); out.Value(memory.flushDenormals); out.Value(memory.flushDenormalResults); } void decodeMemoryInfo(Reader& in, MemoryInfo& memory) { in.Value(memory.kind); in.Value(memory.resource); in.Value(memory.sampler); in.Value(memory.offset); in.Value(memory.secondaryOffset); in.Value(memory.dmask); in.Value(memory.dataDwords); in.Value(memory.dataBits); in.Value(memory.componentIndex); in.Value(memory.componentCount); in.Value(memory.dataFormat); in.Value(memory.numberFormat); in.Value(memory.imageSampleFlags); in.Value(memory.imageDimension); in.Value(memory.imageAddressComponents); in.Value(memory.addressIsFull); in.Value(memory.dataSigned); in.Value(memory.typed); in.Value(memory.formatted); in.Value(memory.d16); in.Value(memory.imageHasMip); in.Value(memory.imageR128); in.Value(memory.imagePacked); in.Value(memory.imageByElements); in.Value(memory.idxen); in.Value(memory.offen); in.Value(memory.planningOnly); in.Value(memory.coherent); in.Value(memory.gpuDescriptor); in.Value(memory.flushDenormals); in.Value(memory.flushDenormalResults); } bool encodePlan(Writer& writer, const IrResourcePlan& plan) { if (std::ranges::any_of(plan.blockStorage, [](const auto& block) { return block == nullptr; })) return false; std::unordered_map values; std::unordered_map blocks; values.reserve(plan.valueStorage.Size()); blocks.reserve(plan.blockStorage.size()); for (std::size_t index = 0; index < plan.valueStorage.Size(); ++index) values.emplace(&plan.valueStorage[index], static_cast(index)); for (std::size_t index = 0; index < plan.blockStorage.size(); ++index) blocks.emplace(plan.blockStorage[index].get(), static_cast(index)); bool owned = values.size() == plan.valueStorage.Size() && blocks.size() == plan.blockStorage.size(); std::uint32_t current = 0; const auto valueIndex = [&](const IrValue* value) { if (value == nullptr) return NoPlanIndex; const auto found = values.find(value); if (found == values.end()) { owned = false; return NoPlanIndex; } return found->second; }; writer.Value(plan.stage); writer.Value(plan.shaderHash); writer.Value(plan.userDataBase); writer.Value(plan.userDataCount); writer.Value(plan.srgbDecodeFormats); writer.List(plan.blockStorage, [](Writer& out, const std::unique_ptr& block) { out.Value(block->Id()); }); writer.Value(plan.valueStorage.Size()); for (const auto& stored : plan.valueStorage) { const auto* value = &stored; const auto bank = static_cast(value->Register().bank); if (bank > 0xfu) owned = false; writer.Compact(static_cast(value->Opcode())); writer.Compact(static_cast(value->Type())); writer.Compact(value->Id()); writer.Compact(value->Flags()); writer.Compact((static_cast(value->Register().index) << 4u) | (bank & 0xfu)); writer.Compact((static_cast(value->ArgumentCount()) << 1u) | (value->HasImmediate() ? 1u : 0u)); if (value->HasImmediate()) writer.Compact(value->ImmediateU64()); for (const auto* argument : value->Arguments()) { const auto index = valueIndex(argument); const auto delta = static_cast(index) - static_cast(current); writer.Compact(argument == nullptr ? 0u : ((static_cast(delta) << 1u) ^ static_cast(delta >> 63)) + 1u); } if (value->IsPhi() && value->PhiBlockCount() != value->ArgumentCount()) owned = false; for (std::size_t operand = 0; value->IsPhi() && operand < value->ArgumentCount(); ++operand) { const auto found = operand < value->PhiBlockCount() ? blocks.find(value->PhiBlock(operand)) : blocks.end(); if (found == blocks.end()) owned = false; writer.Compact(found != blocks.end() ? found->second : 0u); } ++current; } writer.List(plan.memoryInfo, encodeMemoryInfo); writer.List(plan.descriptorSources, [&](Writer& out, const DescriptorSource& source) { for (const auto* dword : source.dwords) out.Value(valueIndex(dword)); out.Value(source.dwordCount); const auto column = source.tableColumn.value_or(TableColumn{}); out.Value(source.tableColumn.has_value()); out.Value(column.heapSource); out.Value(column.stride); out.Value(column.addend); out.Value(column.offset); out.Value(column.dwordCount); out.Value(column.maxKey); out.Value(column.sampler); out.Value(column.address); out.List(column.keyDomains, [](Writer& domains, const KeyDomain& domain) { domains.Value(domain.source); domains.Value(domain.offset); domains.Value(domain.stride); domains.Value(domain.value); }); out.Value(column.unkeyed); out.Value(column.indirect); out.Value(column.indirectOffset); out.Value(column.indirectImmediate); out.Value(column.foldedAddend); out.Value(column.foldedCarry); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { out.Value(valueIndex(block.condition)); out.Values(std::span(block.successors)); out.Values(std::span(block.sources)); out.Values(std::span(block.images)); }); writer.Values(std::span(plan.materializationSources)); writer.List(plan.srtReads, [&](Writer& out, const SrtRead& read) { out.Value(valueIndex(read.value)); out.Value(read.flatOffset); }); writer.Values(std::span(plan.guardedSrtSlots)); writer.Value(plan.srtGuardOffset); writer.Values(std::span(plan.cleanFlatSlots)); writer.Values(std::span(plan.pureFlatSlots)); writer.Value(plan.denseValueIds); writer.Value(plan.requiresSpecializationMemory); writer.Value(plan.srtPlanComplete); writer.Value(plan.resourceTrackingComplete); encodeShaderInfo(writer, plan.info); const auto& fill = plan.uniformFill.fill; writer.Value(fill.kind); writer.Value(fill.resource); for (const auto stride : fill.groupStride) writer.Value(stride); writer.Value(fill.words); writer.Value(fill.value); for (const auto* value : plan.uniformFill.values) writer.Value(valueIndex(value)); return owned; } struct PlanValueRecord { IrOpcode opcode = IrOpcode::Identity; IrType type = IrType::Void; std::uint32_t id = 0; std::uint64_t flags = 0; bool hasImmediate = false; std::uint64_t immediate = 0; GuestRegister registerValue{}; std::vector arguments; std::vector phiBlocks; }; bool decodePlan(Reader& reader, IrResourcePlan& plan) { reader.Value(plan.stage); reader.Value(plan.shaderHash); reader.Value(plan.userDataBase); reader.Value(plan.userDataCount); reader.Value(plan.srgbDecodeFormats); std::vector blockIds; reader.Values(blockIds); std::vector records; std::uint32_t current = 0; reader.List(records, 6, [&](Reader& in, PlanValueRecord& record) { constexpr std::uint64_t Word = std::numeric_limits::max(); const auto opcode = in.Compact(); const auto type = in.Compact(); const auto id = in.Compact(); record.flags = in.Compact(); const auto registerValue = in.Compact(); const auto shape = in.Compact(); const auto arguments = shape >> 1u; if (opcode >= static_cast(IrOpcode::Count) || type > Word || id > Word || (registerValue >> 4u) > Word || arguments > in.Remaining()) { in.Fail(); return; } record.opcode = static_cast(opcode); record.type = static_cast(type); record.id = static_cast(id); record.registerValue = {static_cast(registerValue & 0xfu), static_cast(registerValue >> 4u)}; record.hasImmediate = (shape & 1u) != 0u; if (record.hasImmediate) record.immediate = in.Compact(); record.arguments.resize(static_cast(arguments)); for (auto& argument : record.arguments) { const auto code = in.Compact(); if (code == 0u) { argument = NoPlanIndex; continue; } const auto zigzag = code - 1u; const auto delta = static_cast(zigzag >> 1u) ^ -static_cast(zigzag & 1u); const auto index = static_cast(current) + delta; if (index < 0 || index >= static_cast(records.size())) { in.Fail(); return; } argument = static_cast(index); } if (record.opcode == IrOpcode::Phi) { record.phiBlocks.resize(record.arguments.size()); for (auto& block : record.phiBlocks) { const auto index = in.Compact(); block = index < blockIds.size() ? static_cast(index) : NoPlanIndex; if (block == NoPlanIndex) in.Fail(); } } ++current; }); if (!reader.Ok()) return false; plan.blockStorage.reserve(blockIds.size()); for (const auto id : blockIds) plan.blockStorage.push_back(std::make_unique(id)); plan.valueStorage.Reserve(records.size()); for (const auto& record : records) { auto* value = plan.valueStorage.Create(record.opcode, record.type, record.id); value->SetFlags(record.flags); if (record.hasImmediate) value->SetImmediateU64(record.immediate); value->SetRegister(record.registerValue); } bool valid = true; const auto valueAt = [&](std::uint32_t index) -> IrValue* { if (index == NoPlanIndex) return nullptr; if (index >= plan.valueStorage.Size()) { valid = false; return nullptr; } return &plan.valueStorage[index]; }; for (std::size_t index = 0; index < records.size(); ++index) { const auto& record = records[index]; auto& value = plan.valueStorage[index]; std::vector arguments; std::vector phiBlocks; arguments.reserve(record.arguments.size()); for (std::size_t operand = 0; operand < record.arguments.size(); ++operand) { arguments.push_back(valueAt(record.arguments[operand])); if (value.IsPhi()) phiBlocks.push_back(plan.blockStorage[record.phiBlocks[operand]].get()); } value.SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); } reader.List(plan.memoryInfo, 79, decodeMemoryInfo); reader.List(plan.descriptorSources, 72, [&](Reader& in, DescriptorSource& source) { for (auto& dword : source.dwords) dword = valueAt(in.Get()); in.Value(source.dwordCount); const bool table = in.Get(); TableColumn column; in.Value(column.heapSource); in.Value(column.stride); in.Value(column.addend); in.Value(column.offset); in.Value(column.dwordCount); in.Value(column.maxKey); in.Value(column.sampler); in.Value(column.address); in.List(column.keyDomains, 13, [](Reader& domains, KeyDomain& domain) { domains.Value(domain.source); domains.Value(domain.offset); domains.Value(domain.stride); domains.Value(domain.value); }); in.Value(column.unkeyed); in.Value(column.indirect); in.Value(column.indirectOffset); in.Value(column.indirectImmediate); in.Value(column.foldedAddend); in.Value(column.foldedCarry); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { block.condition = valueAt(in.Get()); in.Values(block.successors); in.Values(block.sources); in.Values(block.images); }); reader.Values(plan.materializationSources); reader.List(plan.srtReads, 8, [&](Reader& in, SrtRead& read) { read.value = valueAt(in.Get()); in.Value(read.flatOffset); }); reader.Values(plan.guardedSrtSlots); reader.Value(plan.srtGuardOffset); reader.Values(plan.cleanFlatSlots); reader.Values(plan.pureFlatSlots); reader.Value(plan.denseValueIds); reader.Value(plan.requiresSpecializationMemory); reader.Value(plan.srtPlanComplete); reader.Value(plan.resourceTrackingComplete); decodeShaderInfo(reader, plan.info); auto& fill = plan.uniformFill.fill; reader.Value(fill.kind); reader.Value(fill.resource); for (auto& stride : fill.groupStride) reader.Value(stride); reader.Value(fill.words); reader.Value(fill.value); for (auto& value : plan.uniformFill.values) value = valueAt(reader.Get()); if (!valid || !reader.Done() || fill.words > plan.uniformFill.values.size()) return false; const auto knownSource = [&](std::uint32_t source) { return source < plan.descriptorSources.size(); }; if (!std::ranges::all_of(plan.materializationSources, knownSource)) return false; if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.tableColumn.has_value() || (knownSource(source.tableColumn->heapSource) && std::ranges::all_of(source.tableColumn->keyDomains, [&](const KeyDomain& domain) { return knownSource(domain.source); }))); })) return false; if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; if (!std::ranges::all_of(plan.info.samplers, [&](const SamplerResource& sampler) { return knownSource(sampler.source); })) return false; const bool blocksValid = std::ranges::all_of(plan.controlFlow, [&](const ResourceBlock& block) { return (block.condition == nullptr || block.successors.size() == 2u) && std::ranges::all_of(block.successors, [&](std::uint32_t successor) { return successor < plan.controlFlow.size(); }) && std::ranges::all_of(block.sources, knownSource) && std::ranges::all_of(block.images, [&](std::uint32_t image) { return image < plan.info.images.size(); }); }); if (!blocksValid) return false; ResourceMaterializer::CompactPlan(plan); return true; } constexpr std::string_view NeutralSwitches[] = { "APS5_PROFILE_DRAW", "APS5_DUMP_IR", "APS5_NO_CODE_HASH_KEY", "APS5_NO_FAILURE_MEMO", "APS5_NO_PERF_FRONTEND_PAIR", "APS5_NO_PERF_DISK_PLANS", "APS5_NO_PERF_DOM_INTERSECTION", "APS5_NO_PERF_ITERATION_SELECTION", "APS5_NO_PERF_LAYOUT_PAIR", "APS5_NO_RESULT_MEMO", }; const std::vector& switchKey() { static const std::vector key = [] { std::vector bytes; Writer writer(bytes); for (const auto name : Generated::RecompilerSwitches) { if (std::find(std::begin(NeutralSwitches), std::end(NeutralSwitches), name) != std::end(NeutralSwitches)) continue; const char* value = std::getenv(std::string(name).c_str()); if (value == nullptr) continue; writer.Text(std::string(name)); writer.Text(value); } return bytes; }(); return key; } std::string hex(std::uint64_t value) { char text[17]; std::snprintf(text, sizeof(text), "%016llx", static_cast(value)); return text; } std::vector wrapEntry(std::span key, std::span payload) { const FileHeader header{FileMagic, FormatVersion, SourceVersion(), key.size(), payload.size(), HashBytes(key), HashBytes(payload)}; const auto payloadOffset = PayloadOffset(key.size()); std::vector file(payloadOffset + payload.size()); std::memcpy(file.data(), &header, sizeof(header)); std::memcpy(file.data() + sizeof(header), key.data(), key.size()); if (!payload.empty()) std::memcpy(file.data() + payloadOffset, payload.data(), payload.size()); return file; } LoadStatus unwrapEntry(std::span file, std::span key, std::span& payload) { if (file.size() < sizeof(FileHeader)) return LoadStatus::Rejected; FileHeader header; std::memcpy(&header, file.data(), sizeof(header)); if (header.magic != FileMagic || header.format != FormatVersion || header.sourceVersion != SourceVersion()) return LoadStatus::Rejected; const auto body = file.size() - sizeof(header); if (header.keyBytes > body) return LoadStatus::Rejected; const auto payloadOffset = PayloadOffset(static_cast(header.keyBytes)); if (payloadOffset > file.size() || header.payloadBytes != file.size() - payloadOffset) return LoadStatus::Rejected; const auto storedKey = file.subspan(sizeof(header), static_cast(header.keyBytes)); const auto padding = file.subspan(sizeof(header) + storedKey.size(), payloadOffset - sizeof(header) - storedKey.size()); if (std::ranges::any_of(padding, [](std::byte value) { return value != std::byte{0}; })) return LoadStatus::Rejected; payload = file.subspan(payloadOffset); if (HashBytes(storedKey) != header.keyHash) return LoadStatus::Rejected; if (storedKey.size() != key.size() || !std::equal(storedKey.begin(), storedKey.end(), key.begin())) return LoadStatus::KeyMismatch; if (HashBytes(payload) != header.payloadHash) return LoadStatus::Rejected; return LoadStatus::Loaded; } class DiskStore { public: DiskStore() { const auto root = ShaderCacheDirectory(); if (root.empty()) return; this->root = root; directory = root / hex(SourceVersion()); discoverPrebuilt(); std::thread([this] { run(); }).detach(); } [[nodiscard]] bool Enabled() const { return !directory.empty(); } [[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); } bool LoadPlan(std::span key, IrResourcePlan& plan) { return load(key, [&](std::span file, std::shared_ptr) { return DecodePlanEntry(file, key, plan); }, planHits, planMisses, planLoadFailures); } void Store(std::vector key, std::shared_ptr variant) { enqueue({std::move(key), std::move(variant), nullptr}); } void StorePlan(std::vector key, std::shared_ptr plan) { enqueue({std::move(key), nullptr, std::move(plan)}); } std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { const auto path = directory / EntryName(key); const auto file = EncodeEntry(key, variant); if (!WriteFileAtomically(path, file)) return nullptr; writes.fetch_add(1, std::memory_order_relaxed); bytesWritten.fetch_add(file.size(), std::memory_order_relaxed); auto mapped = MapFile(path); if (mapped == nullptr) return nullptr; auto persisted = std::make_shared(); const auto bytes = mapped->Bytes(); if (DecodeEntry(bytes, key, *persisted, std::move(mapped)) != LoadStatus::Loaded) return nullptr; persisted->layout = variant.layout; persisted->artifact.variantId = variant.artifact.variantId; return persisted; } void Flush() { std::unique_lock lock(mutex); idle.wait(lock, [&] { return jobs.empty() && !writing; }); } Counters Totals() const { Counters counters; counters.hits = hits.load(std::memory_order_relaxed); counters.misses = misses.load(std::memory_order_relaxed); counters.writes = writes.load(std::memory_order_relaxed); counters.loadFailures = loadFailures.load(std::memory_order_relaxed); counters.writeFailures = writeFailures.load(std::memory_order_relaxed); counters.bytesRead = bytesRead.load(std::memory_order_relaxed); counters.bytesWritten = bytesWritten.load(std::memory_order_relaxed); counters.planHits = planHits.load(std::memory_order_relaxed); 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(); const auto name = EntryName(key); thread_local std::vector* fileSlot = nullptr; auto& file = ThreadOwned(fileSlot); bool loaded = false; std::span bytes; std::shared_ptr owner; if (auto mapped = MapFile(directory / name)) { bytes = mapped->Bytes(); owner = std::move(mapped); } const bool present = owner != nullptr || ReadWholeFile(directory / name, file); if (owner == nullptr) bytes = file; if (!present) { missCount.fetch_add(1, std::memory_order_relaxed); } else { switch (decode(bytes, std::move(owner))) { case LoadStatus::Loaded: hitCount.fetch_add(1, std::memory_order_relaxed); bytesRead.fetch_add(bytes.size(), std::memory_order_relaxed); loaded = true; break; case LoadStatus::Absent: case LoadStatus::KeyMismatch: missCount.fetch_add(1, std::memory_order_relaxed); break; case LoadStatus::Rejected: { const auto failures = failureCount.fetch_add(1, std::memory_order_relaxed); if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), bytes.size()); break; } } } if (file.capacity() > (4u << 20u)) std::vector().swap(file); loadNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); report(false); return loaded; } void enqueue(Job job) { { std::lock_guard lock(mutex); jobs.push_back(std::move(job)); } wake.notify_one(); } void run() { std::fprintf(stderr, "[shader-disk-cache] %s (source version %s, format %u)\n", directory.string().c_str(), hex(SourceVersion()).c_str(), FormatVersion); housekeeping(); std::unique_lock lock(mutex); while (true) { wake.wait_for(lock, std::chrono::seconds(10), [&] { return !jobs.empty(); }); while (!jobs.empty()) { auto job = std::move(jobs.front()); jobs.pop_front(); writing = true; lock.unlock(); write(job); job = {}; lock.lock(); writing = false; } idle.notify_all(); lock.unlock(); report(false); lock.lock(); } } void write(const Job& job) { const auto file = job.plan != nullptr ? EncodePlanEntry(job.key, *job.plan) : EncodeEntry(job.key, *job.variant); if (!file.empty() && WriteFileAtomically(directory / EntryName(job.key), file)) { (job.plan != nullptr ? planWrites : writes).fetch_add(1, std::memory_order_relaxed); bytesWritten.fetch_add(file.size(), std::memory_order_relaxed); } else { const auto failures = writeFailures.fetch_add(1, std::memory_order_relaxed); if (failures < 4) std::fprintf(stderr, "[shader-disk-cache] cannot write %s\n", (directory / EntryName(job.key)).string().c_str()); } } void housekeeping() { std::error_code error; std::filesystem::create_directories(directory, error); const auto stamp = directory / "last-used"; std::ofstream(stamp, std::ios::binary | std::ios::trunc).close(); std::filesystem::last_write_time(stamp, std::filesystem::file_time_type::clock::now(), error); const auto now = std::filesystem::file_time_type::clock::now(); for (std::filesystem::directory_iterator it(root, error), end; !error && it != end; it.increment(error)) { const auto name = it->path().filename().string(); if (it->path() == directory || name.size() != 16 || name.find_first_not_of("0123456789abcdef") != std::string::npos || !it->is_directory(error)) continue; std::error_code timeError; auto used = std::filesystem::last_write_time(it->path() / "last-used", timeError); if (timeError) used = std::filesystem::last_write_time(it->path(), timeError); if (!timeError && now - used > std::chrono::hours(24 * 14)) { std::error_code removeError; std::filesystem::remove_all(it->path(), removeError); } } error.clear(); for (std::filesystem::directory_iterator it(directory, error), end; !error && it != end; it.increment(error)) { if (it->path().filename().string().find(".tmp.") == std::string::npos) continue; std::error_code timeError; const auto written = std::filesystem::last_write_time(it->path(), timeError); if (!timeError && now - written > std::chrono::hours(1)) { std::error_code removeError; std::filesystem::remove(it->path(), removeError); } } } void report(bool force) { const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); auto last = lastReport.load(std::memory_order_relaxed); if (last == 0) { lastReport.compare_exchange_strong(last, now, std::memory_order_relaxed); return; } if (!force && (now - last < std::chrono::duration_cast(std::chrono::seconds(10)).count() || !lastReport.compare_exchange_strong(last, now, std::memory_order_relaxed))) return; std::lock_guard lock(reportMutex); 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); }; 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; std::deque jobs; 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; std::uint64_t reportedLoadNs = 0; }; DiskStore& store() { static DiskStore* instance = new DiskStore(); return *instance; } } std::uint64_t SourceVersion() { return Generated::SourceVersion; } void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key) { key.clear(); Writer writer(key); writer.Value(FileMagic); writer.Value(FormatVersion); writer.Value(SourceVersion()); thread_local std::vector* memoryKeySlot = nullptr; auto& memoryKey = ThreadOwned(memoryKeySlot); RecompileCacheKey::Build(request, memoryKey); writer.Values(std::span(memoryKey)); writer.Values(request.shader.code); writer.Value(hostSubgroupSize); writer.Value(request.layout.descriptorSet); writer.Value(request.layout.firstBinding); writer.Value(request.layout.pushConstantOffsetBytes); writer.Value(request.layout.pushConstantSizeBytes); const auto& switches = switchKey(); key.insert(key.end(), switches.begin(), switches.end()); } void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key) { key.clear(); Writer writer(key); writer.Value(FileMagic); writer.Value(FormatVersion); writer.Value(SourceVersion()); writer.Value(PlanEntryKind); thread_local std::vector* memoryKeySlot = nullptr; auto& memoryKey = ThreadOwned(memoryKeySlot); RecompileCacheKey::Build(request, memoryKey); writer.Values(std::span(memoryKey)); writer.Values(request.shader.code); writer.Value(hostSubgroupSize); const auto& switches = switchKey(); key.insert(key.end(), switches.begin(), switches.end()); } std::string EntryName(std::span key) { return hex(HashBytes(key, 0x5eed0001ull)) + hex(HashBytes(key, 0x5eed0002ull)) + ".bin"; } void EncodeResult(const RecompileResult& result, std::vector& out) { Writer writer(out); encodeArtifact(writer, result); encodeInvocation(writer, result); writer.Value(result.workgroupMemoryDwords); writer.Values(std::span(result.shaderDataUserWords)); } bool DecodeResult(std::span bytes, RecompileResult& result, std::shared_ptr owner) { Reader reader(bytes, std::move(owner)); decodeArtifact(reader, result); decodeInvocation(reader, result); reader.Value(result.workgroupMemoryDwords); reader.Values(result.shaderDataUserWords); result.cacheHit = false; return reader.Done() && result.runtimeAbiVersion == RuntimeAbi::Version; } std::vector EncodeEntry(std::span key, const CompiledVariant& variant) { std::vector payload; payload.reserve(variant.artifact.spirv.size() * sizeof(std::uint32_t) + 4096); Writer writer(payload); encodeArtifact(writer, variant.artifact); encodeInfo(writer, variant.info); encodeAllocation(writer, variant.bindings); return wrapEntry(key, payload); } LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner) { std::span payload; const auto status = unwrapEntry(file, key, payload); if (status != LoadStatus::Loaded) return status; Reader reader(payload, std::move(owner)); CompiledVariant decoded; decodeArtifact(reader, decoded.artifact); decodeInfo(reader, decoded.info); decodeAllocation(reader, decoded.bindings); if (!reader.Done() || decoded.artifact.runtimeAbiVersion != RuntimeAbi::Version) return LoadStatus::Rejected; variant.info = std::move(decoded.info); variant.bindings = std::move(decoded.bindings); variant.artifact = std::move(decoded.artifact); return LoadStatus::Loaded; } bool EncodePlan(const IrResourcePlan& plan, std::vector& out) { Writer writer(out); return encodePlan(writer, plan); } bool DecodePlan(std::span bytes, IrResourcePlan& plan) { Reader reader(bytes); IrResourcePlan decoded; if (!decodePlan(reader, decoded)) return false; plan = std::move(decoded); return true; } std::vector EncodePlanEntry(std::span key, const IrResourcePlan& plan) { std::vector payload; if (!EncodePlan(plan, payload)) return {}; return wrapEntry(key, payload); } LoadStatus DecodePlanEntry(std::span file, std::span key, IrResourcePlan& plan) { std::span payload; const auto status = unwrapEntry(file, key, payload); if (status != LoadStatus::Loaded) return status; return DecodePlan(payload, plan) ? LoadStatus::Loaded : LoadStatus::Rejected; } bool Enabled() { return store().Enabled(); } std::filesystem::path EntryDirectory() { return store().Directory(); } bool Load(std::span key, CompiledVariant& variant) { auto& instance = store(); return instance.Enabled() && instance.Load(key, variant); } void Store(std::vector key, std::shared_ptr variant) { auto& instance = store(); if (instance.Enabled()) instance.Store(std::move(key), std::move(variant)); } bool LoadPlan(std::span key, IrResourcePlan& plan) { auto& instance = store(); return instance.Enabled() && instance.LoadPlan(key, plan); } void StorePlan(std::vector key, std::shared_ptr plan) { auto& instance = store(); if (instance.Enabled() && plan != nullptr) instance.StorePlan(std::move(key), std::move(plan)); } std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { auto& instance = store(); return instance.Enabled() ? instance.Persist(key, variant) : nullptr; } void Flush() { auto& instance = store(); if (instance.Enabled()) instance.Flush(); } Counters Totals() { return store().Totals(); } }