aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/renderer_vulkan
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-30 21:17:05 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-30 21:17:05 -0400
commitbe76402ab51deb40c8dea57f66078a19e4a561b2 (patch)
tree47086d481507a520e8326bc936f73d33b52c68ae /src/video_core/renderer_vulkan
parentb40ad217ab829226841cb71f6e4c5cb05977a225 (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/renderer_vulkan')
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp92
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.h12
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{};