diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-30 21:17:05 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-30 21:17:05 -0400 |
| commit | be76402ab51deb40c8dea57f66078a19e4a561b2 (patch) | |
| tree | 47086d481507a520e8326bc936f73d33b52c68ae /src/video_core | |
| parent | b40ad217ab829226841cb71f6e4c5cb05977a225 (diff) | |
vulkan: build at boot only the cached pipelines recent sessions used
Every graphics pipeline in the disk cache was built at boot, and each one
holds driver memory: Mario Kart World builds 6.5k (the NVIDIA driver keeps
~1.6 GB for them) and a session uses about a quarter. Record the pipelines
each session uses in <title>/vulkan_used.bin (key hash + sessions since the
last use, dropped after 8), build only those at boot and the rest when a
game first needs them, without storing them in the disk cache again. The
first session without the file builds everything as before.
EDEN_NX2_PRELOAD_ALL_PIPELINES=1 keeps building all of them.
Diffstat (limited to 'src/video_core')
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 92 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_pipeline_cache.h | 12 |
2 files changed, 104 insertions, 0 deletions
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 2c64f3375b..e369caa58f 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -559,13 +559,80 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, device.SupportsTransformFeedbackProvokingVertexPreservation(); } +namespace { +// <title>/vulkan_used.bin: the graphics pipelines recent sessions used, as key hash + sessions +// since the last use. +constexpr u32 UsedPipelinesMagic = 0x31505545; // "EUP1" +constexpr u8 UsedPipelinesMaxAge = 8; + +std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path) { + std::unordered_map<u64, u8> used; + std::ifstream file{path, std::ios::binary}; + u32 magic{}, count{}; + if (!file.read(reinterpret_cast<char*>(&magic), sizeof(magic)) || magic != UsedPipelinesMagic || + !file.read(reinterpret_cast<char*>(&count), sizeof(count))) { + return used; + } + used.reserve(count); + for (u32 i = 0; i < count; ++i) { + u64 hash{}; + u8 age{}; + if (!file.read(reinterpret_cast<char*>(&hash), sizeof(hash)) || + !file.read(reinterpret_cast<char*>(&age), sizeof(age))) { + break; + } + used.emplace(hash, age); + } + return used; +} + +// EDEN_NX2_PRELOAD_ALL_PIPELINES=1 builds every cached pipeline at boot, as before. +bool PreloadAllPipelines() { + static const bool all = [] { + const char* value = std::getenv("EDEN_NX2_PRELOAD_ALL_PIPELINES"); + return value != nullptr && value[0] == '1'; + }(); + return all; +} +} // Anonymous namespace + PipelineCache::~PipelineCache() { + SaveUsedPipelines(); if (use_vulkan_pipeline_cache && !vulkan_pipeline_cache_filename.empty()) { SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache, CACHE_VERSION); } } +void PipelineCache::SaveUsedPipelines() { + used_pipelines_saved = std::chrono::steady_clock::now(); + // A session too short to say what the game uses (a boot to the menu) keeps the old list. + constexpr size_t MinimumUsed = 64; + if (used_pipelines_filename.empty() || used_this_session.size() < MinimumUsed) { + return; + } + std::unordered_map<u64, u8> next; + next.reserve(used_pipelines.size() + used_this_session.size()); + for (const auto& [key, pipeline] : graphics_cache) { + if (used_this_session.contains(pipeline.get())) { + next.emplace(key.Hash(), u8{0}); + } + } + for (const auto& [hash, age] : used_pipelines) { + if (age + 1 < UsedPipelinesMaxAge) { + next.try_emplace(hash, static_cast<u8>(age + 1)); + } + } + std::ofstream file{used_pipelines_filename, std::ios::binary | std::ios::trunc}; + const u32 count = static_cast<u32>(next.size()); + file.write(reinterpret_cast<const char*>(&UsedPipelinesMagic), sizeof(UsedPipelinesMagic)); + file.write(reinterpret_cast<const char*>(&count), sizeof(count)); + for (const auto& [hash, age] : next) { + file.write(reinterpret_cast<const char*>(&hash), sizeof(hash)); + file.write(reinterpret_cast<const char*>(&age), sizeof(age)); + } +} + GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() { if (!RefreshStages(graphics_key.unique_hashes)) { @@ -625,6 +692,10 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading return; } pipeline_cache_filename = base_dir / "vulkan.bin"; + used_pipelines_filename = base_dir / "vulkan_used.bin"; + used_pipelines = LoadUsedPipelines(used_pipelines_filename); + used_pipelines_saved = std::chrono::steady_clock::now(); + size_t skipped_pipelines = 0; if (use_vulkan_pipeline_cache) { vulkan_pipeline_cache_filename = base_dir / "vulkan_pipelines.bin"; @@ -693,6 +764,13 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading return; } + const u64 hash = key.Hash(); + disk_pipelines.insert(hash); + if (!PreloadAllPipelines() && !used_pipelines.empty() && !used_pipelines.contains(hash)) { + ++skipped_pipelines; + return; + } + workers.QueueWork([this, key, envs_ = std::move(envs), &state, &callback]() mutable { ShaderPools pools; boost::container::static_vector<Shader::Environment*, 5> env_ptrs; @@ -717,6 +795,11 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading load_graphics); LOG_INFO(Render_Vulkan, "Total Pipeline Count: {}", state.total); + if (skipped_pipelines != 0) { + LOG_INFO(Render_Vulkan, + "{} cached graphics pipelines no recent session used are built when first needed", + skipped_pipelines); + } std::unique_lock lock{state.mutex}; callback(VideoCore::LoadCallbackStage::Build, 0, state.total); @@ -833,6 +916,10 @@ GraphicsPipeline* PipelineCache::CurrentGraphicsPipelineSlowPath() { if (!pipeline) { return nullptr; } + if (used_this_session.insert(pipeline.get()).second && + std::chrono::steady_clock::now() - used_pipelines_saved > std::chrono::minutes{2}) { + SaveUsedPipelines(); + } if (current_pipeline) { current_pipeline->AddTransition(pipeline.get()); } @@ -1029,6 +1116,11 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline() { if (!pipeline || pipeline_cache_filename.empty()) { return pipeline; } + if (disk_pipelines.contains(graphics_key.Hash())) { + // In the disk cache already, only not built at boot. + QueueVulkanPipelineCacheFlush(); + return pipeline; + } serialization_thread.QueueWork([this, key = graphics_key, envs = std::move(environments.envs)] { boost::container::static_vector<const GenericEnvironment*, Maxwell::MaxShaderProgram> env_ptrs; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h index 71dfb46e3d..4d1abf83d5 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h @@ -11,6 +11,8 @@ #include <chrono> #include <cstddef> #include <filesystem> +#include <unordered_map> +#include <unordered_set> #include <memory> #include <type_traits> #include "common/container/unordered_map.h" @@ -173,6 +175,16 @@ private: std::filesystem::path pipeline_cache_filename; + /// Key hashes of the graphics pipelines in the disk cache. Only those a recent session used + /// (used_pipelines) are built at boot; the rest are built when first needed, and not stored + /// again. Each built pipeline holds driver memory (title 1: 6.5k cached, a quarter used). + std::unordered_set<u64> disk_pipelines; + std::unordered_map<u64, u8> used_pipelines; ///< Key hash -> sessions since it was last used + std::unordered_set<const GraphicsPipeline*> used_this_session; + std::filesystem::path used_pipelines_filename; + std::chrono::steady_clock::time_point used_pipelines_saved{}; + void SaveUsedPipelines(); + std::filesystem::path vulkan_pipeline_cache_filename; vk::PipelineCache vulkan_pipeline_cache; size_t pipelines_since_flush{}; |
