From d65cc07a1b609b836be77af25916861e2fc66f1f Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:09:34 +0200 Subject: [PATCH] perf(agc): load the pipeline cache file only on the windowed device and save without re-reading an unchanged file The headless device is replaced by the windowed one at window attach and then only lingers for in-flight workers, but it parsed its own full copy of the cache file. It now starts empty (it still merges what it compiles into the file). Load reuses the file buffer as the initial data instead of copying it, and save() writes the live data directly when the file still carries the header this object loaded or wrote, skipping the read-back and the merge cache. --- .../Execution/src/VulkanDevice.cpp | 2 +- .../Graphics/include/PipelineCache.hpp | 2 +- .../Graphics/src/PipelineCache.cpp | 20 +++++++++-------- .../prx/libSceAgcDriver/tests/Recorder.cpp | 22 +++++++++++++++++++ 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bb39fa7beb..0044028bfb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1839,7 +1839,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : Graphics::PrepareImportWatch(graphicsContext()); state->bufferPool = std::make_shared(graphicsContext()); state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - state->pipelineCache = std::make_unique(graphicsContext(), state->properties); + state->pipelineCache = std::make_unique(graphicsContext(), state->properties, window != nullptr || compileOnly); state->detiler = std::make_unique(graphicsContext()); state->textureCache = std::make_unique(graphicsContext()); state->colorTransfer = std::make_unique(graphicsContext()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 92506617e4..73b999b4d6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -18,7 +18,7 @@ class PipelineCache { public: static constexpr int SaveIntervalSeconds = 10; - PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties); + PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties, bool loadFromFile = true); ~PipelineCache(); PipelineCache(const PipelineCache&) = delete; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index 8b5d426e59..ebfa65095e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -35,7 +35,7 @@ std::mutex& pathMutex(const std::filesystem::path& path) { } -PipelineCache::PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties) : context(context), properties(properties) { +PipelineCache::PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties, bool loadFromFile) : context(context), properties(properties) { static_assert(sizeof(FileHeader) == 24); const auto directory = ShaderRecompiler::ShaderCacheDirectory(); if (!directory.empty()) { @@ -52,7 +52,7 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::vector initialData; if (!path.empty()) { std::lock_guard pathLock(pathMutex(path)); - load(initialData); + if (loadFromFile) load(initialData); } VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; info.initialDataSize = initialData.size(); @@ -145,7 +145,7 @@ std::optional PipelineCache::readHeader() const { } void PipelineCache::load(std::vector& initialData) { - const auto file = readFile(); + auto file = readFile(); if (file.state == FileState::Missing) return; if (file.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; starting empty\n", path.string().c_str()); @@ -155,8 +155,8 @@ void PipelineCache::load(std::vector& initialData) { std::fprintf(stderr, "[pipeline-cache] ignoring %s: %s\n", path.string().c_str(), file.invalidReason); return; } - const auto data = file.Payload(); - initialData.assign(data.begin(), data.end()); + initialData = std::move(file.contents); + initialData.erase(initialData.begin(), initialData.begin() + sizeof(FileHeader)); loadedBytes = initialData.size(); settledBytes = initialData.size(); settledFile = file.header; @@ -171,20 +171,22 @@ void PipelineCache::save(bool final) { auto synchronizationLock = synchronization.LockForSnapshot(); if (getData(context.device, cache, &liveBytes, nullptr) != VK_SUCCESS || liveBytes == 0) return; } - if (liveBytes == settledBytes && readHeader() == settledFile) return; + const auto onDisk = readHeader(); + if (liveBytes == settledBytes && onDisk == settledFile) return; + const bool fileUnchanged = settledFile.has_value() && onDisk == settledFile; const auto started = std::chrono::steady_clock::now(); const auto settle = [&](const std::optional& file) { settledBytes = liveBytes; settledFile = file; }; - const auto disk = readFile(); + const auto disk = fileUnchanged ? File{} : readFile(); if (disk.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; not saving over it\n", path.string().c_str()); settle(readHeader()); return; } if (disk.state == FileState::Invalid) std::fprintf(stderr, "[pipeline-cache] replacing %s: %s\n", path.string().c_str(), disk.invalidReason); - std::size_t floor = 0; + std::size_t floor = fileUnchanged ? std::max(loadedBytes, settledFile->dataBytes) : 0; VkPipelineCache merged = VK_NULL_HANDLE; struct DestroyMerged { const Context& context; @@ -231,7 +233,7 @@ void PipelineCache::save(bool final) { } if (size < floor) { std::fprintf(stderr, "[pipeline-cache] keeping %s: the merged cache has %.1f KiB, less than the %.1f KiB on disk or loaded at start\n", path.string().c_str(), static_cast(size) / 1024.0, static_cast(floor) / 1024.0); - settle(disk.header); + settle(fileUnchanged ? *settledFile : disk.header); return; } const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))}; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 040c2dd195..662ce7d21c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -7429,6 +7429,28 @@ int pipelineCacheFileTests(const Device& device) { PipelineCache idle(context, properties); } Require(contents(path) == stored, "an object that added no pipelines rewrote the cache file"); + { + PipelineCache unseeded(context, properties, false); + Require(!holds(unseeded.Handle(), 0), "a cache created without the file already held a pipeline of the file"); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 0) && holds(reloaded.Handle(), 1), "an unseeded cache dropped pipelines from the cache file when it saved"); + } + std::filesystem::remove_all(directory); + { + PipelineCache writer(context, properties); + build(writer.Handle(), 0); + } + { + PipelineCache extender(context, properties); + Require(holds(extender.Handle(), 0), "the cache file lost a pipeline"); + build(extender.Handle(), 1); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 0) && holds(reloaded.Handle(), 1), "a cache that saved over an unchanged file lost pipelines"); + } { std::ofstream corrupt(path, std::ios::binary | std::ios::trunc); corrupt << "not a pipeline cache";