#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h" #include "common/logging/log.h" #include "common/stringUtils.h" #include "graphics/host_gpu/vulkanCommon.h" #include "graphics/shader/recompiler/ShaderRecompiler.h" #include "graphics/shader/recompiler/ir/Serialize.h" #include #include #include #include #include #include #include #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS #define WIN32_LEAN_AND_MEAN #include #elif defined(__APPLE__) #include #endif // The shader store: a header, then zstd-compressed records of everything a run created first. // Records are only appended, so a store cut short by a crash keeps every complete record. namespace Libs::Graphics { namespace { using ShaderRecompiler::IR::IoAll; using ShaderRecompiler::IR::Reader; using ShaderRecompiler::IR::Writer; enum class Record : uint8_t { Module, GraphicsPipeline, TessellationLayout }; // Precedes each record's zstd frame. struct Frame { uint32_t compressed_size = 0; uint32_t raw_size = 0; uint64_t checksum = 0; // XXH3 of the compressed bytes. }; // Changes with every build, so records never meet code that encodes them differently. Zero when // the executable cannot be found. uint64_t BuildStamp() { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS std::array name {}; GetModuleFileNameW(nullptr, name.data(), static_cast(name.size())); const std::filesystem::path path = name.data(); #elif defined(__APPLE__) std::array name {}; auto size = static_cast(name.size()); _NSGetExecutablePath(name.data(), &size); const std::filesystem::path path = name.data(); #else const std::filesystem::path path = "/proc/self/exe"; #endif std::error_code error; const auto time = std::filesystem::last_write_time(path, error).time_since_epoch().count(); return error ? 0 : static_cast(time); } // What every record depends on besides its own fields; empty when the build is unknown. std::vector StoreHeader(const GraphicContext& graphics) { const auto stamp = BuildStamp(); if (stamp == 0) { return {}; } Writer header; IoAll(header, stamp, SpirvProfile(graphics), graphics.GetPhysicalDeviceProperties().limits.maxComputeSharedMemorySize); return std::move(header.bytes); } // Record fields, shared by writing and reading. template void IoKey(S& io, Key& key) { auto& [stage, hash, user_data_count, static_state] = key; IoAll(io, stage, hash, user_data_count, static_state); } template void IoModule(S& io, Module& stage) { // The handle comes from the SPIR-V; stored programs have no function code. auto& [id, handle, info, specialization, function_code, vertex_formats, rect_parameters] = stage; IoAll(io, id, info, specialization, vertex_formats, rect_parameters); } } // namespace void ShaderStoreWriter::Open(const std::filesystem::path& path, std::span header, uint64_t kept) { Common::File::CreateDirectories(path.parent_path()); if (kept == 0) { if (m_file.Create(path)) { m_file.Write(header.data(), static_cast(header.size())); } } else if (m_file.Open(path, Common::File::Mode::ReadWrite)) { m_file.Truncate(kept); m_file.Seek(kept); } if (m_file.IsInvalid()) { return; } m_thread = std::jthread([this](std::stop_token stop) { std::unique_lock lock(m_mutex); // Once stop is requested the wait returns false only when the queue is empty. while (m_ready.wait(lock, stop, [this] { return !m_jobs.empty(); })) { const auto job = std::move(m_jobs.front()); m_jobs.pop(); lock.unlock(); job(); lock.lock(); } }); } void ShaderStoreWriter::Append(std::vector&& record) { Post([this, record = std::move(record)] { std::vector bytes(sizeof(Frame) + ZSTD_compressBound(record.size())); const auto size = ZSTD_compress(bytes.data() + sizeof(Frame), bytes.size() - sizeof(Frame), record.data(), record.size(), 3); EXIT_IF(ZSTD_isError(size)); const Frame frame {.compressed_size = static_cast(size), .raw_size = static_cast(record.size()), .checksum = XXH3_64bits(bytes.data() + sizeof(Frame), size)}; std::memcpy(bytes.data(), &frame, sizeof(frame)); // A failed write leaves a partial record; the next run drops it and everything after it. m_file.Write(bytes.data(), static_cast(sizeof(Frame) + size)); }); } void ShaderStoreWriter::Post(Common::UniqueFunction&& job) { { std::scoped_lock lock(m_mutex); m_jobs.push(std::move(job)); } m_ready.notify_one(); } void PipelineCache::WarmUp(const std::filesystem::path& path) { if (path.empty()) { return; } const auto header = StoreHeader(m_graphics); if (header.empty()) { return; } const auto start = std::chrono::steady_clock::now(); std::vector bytes; if (Common::File file; Common::File::IsFileExisting(path) && file.Open(path, Common::File::Mode::Read)) { bytes.resize(file.Size()); uint32_t read = 0; file.Read(bytes.data(), static_cast(bytes.size()), &read); bytes.resize(read); } Log::WriteToConsoleAndLog( fmt::format("Shader cache: compiling shaders and pipelines from {} ({:.1f} MB)\n", Common::PathToString(path), static_cast(bytes.size()) / 1'000'000.0)); std::vector pipelines; auto records = std::span(bytes); const bool reusable = records.size() >= header.size() && std::ranges::equal(records.first(header.size()), header); if (reusable) { records = records.subspan(header.size()); while (LoadRecord(records, pipelines)) { } } // A damaged record ends the store; the records before it stay. m_store.Open(path, header, reusable ? bytes.size() - records.size() : 0); const auto modules = MapModules(); for (const auto& key: pipelines) { Pipeline pipeline; if (CreateGraphicsPipeline(pipeline, key, modules)) { m_graphics_pipelines.emplace(key, pipeline); } } // A compute pipeline depends only on its module. for (const auto* module: modules | std::views::values) { if (module != nullptr && module->info.stage == ShaderType::Compute) { CreatePipelineInternal(m_graphics, m_compute_pipelines[module->id], *module, m_driver_cache); } } if (!m_graphics_pipelines.empty() || !m_compute_pipelines.empty()) { CheckpointDriverCache(); } Log::WriteToConsoleAndLog(fmt::format( "Shader cache: loaded {} programs, {} modules, {} pipelines in {} ms\n", m_program_cache.size(), modules.size() - 1, m_graphics_pipelines.size() + m_compute_pipelines.size(), std::chrono::duration_cast(std::chrono::steady_clock::now() - start) .count())); } bool PipelineCache::LoadRecord(std::span& records, std::vector& pipelines) { Frame frame; if (records.size() < sizeof(frame)) { return false; } std::memcpy(&frame, records.data(), sizeof(frame)); if (records.size() - sizeof(frame) < frame.compressed_size) { return false; } const auto compressed = records.subspan(sizeof(frame), frame.compressed_size); if (XXH3_64bits(compressed.data(), compressed.size()) != frame.checksum || ZSTD_getFrameContentSize(compressed.data(), compressed.size()) != frame.raw_size) { return false; } std::vector raw(frame.raw_size); if (ZSTD_decompress(raw.data(), raw.size(), compressed.data(), compressed.size()) != raw.size()) { return false; } Reader reader {raw}; auto type = Record::Module; const auto complete = [&] { return reader.ok && reader.bytes.empty(); }; IoAll(reader, type); switch (type) { case Record::Module: { ProgramKey key; std::vector plan; ShaderModule module; std::vector spirv; IoKey(reader, key); IoAll(reader, plan); IoModule(reader, module); IoAll(reader, spirv); if (!complete()) { return false; } const auto [program, inserted] = m_program_cache.try_emplace(std::move(key)); if (inserted && !ShaderRecompiler::IR::LoadResourcePlan(plan, program->second.resource_plan)) { m_program_cache.erase(program); return false; } module.module = CompileSPV(spirv, m_graphics.device); if (module.module == nullptr) { return false; } program->second.modules.push_back(std::move(module)); break; } case Record::GraphicsPipeline: { GraphicsPipelineKey key; reader.Raw(&key, sizeof(key)); if (!complete()) { return false; } pipelines.push_back(key); break; } case Record::TessellationLayout: { uint64_t key = 0; ShaderTessellationInputInfo layout; IoAll(reader, key, layout); if (!complete()) { return false; } m_tess_layouts.try_emplace(key, layout); break; } default: return false; } records = records.subspan(sizeof(frame) + compressed.size()); return true; } // Records are encoded here, since programs and modules change later. void PipelineCache::StoreModule(const ProgramKey& key, const Program& program, const ShaderModule& module, const std::vector& spirv) { if (!m_store) { return; } Writer record; IoAll(record, Record::Module); IoKey(record, key); IoAll(record, ShaderRecompiler::IR::SaveResourcePlan(program.resource_plan)); IoModule(record, module); IoAll(record, spirv); m_store.Append(std::move(record.bytes)); } void PipelineCache::StorePipeline(const GraphicsPipelineKey& key) { if (!m_store) { return; } Writer record; IoAll(record, Record::GraphicsPipeline); record.Raw(&key, sizeof(key)); m_store.Append(std::move(record.bytes)); } void PipelineCache::StoreLayout(uint64_t key, const ShaderTessellationInputInfo& layout) { if (!m_store) { return; } Writer record; IoAll(record, Record::TessellationLayout, key, layout); m_store.Append(std::move(record.bytes)); } PipelineCache::ModuleMap PipelineCache::MapModules() const { ModuleMap modules {{0, nullptr}}; for (const auto& program: m_program_cache | std::views::values) { for (const auto& module: program.modules) { modules.emplace(module.id, &module); } } return modules; } bool PipelineCache::CreateGraphicsPipeline(Pipeline& pipeline, const GraphicsPipelineKey& key, const ModuleMap& modules) { std::array stages {}; for (size_t i = 0; i < stages.size(); i++) { const auto found = modules.find(key.stage_ids[i]); if (found == modules.end()) { return false; } stages[i] = found->second; } CreatePipelineInternal(m_graphics, pipeline, key, stages, m_driver_cache); return true; } } // namespace Libs::Graphics