aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 13:27:36 -0500
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 13:27:36 -0500
commitdfda645f6d4de27f7cab340ed66a79c2033c0c6b (patch)
tree6ce720fdad485e4ad898027aa214ea84fc5bbea0 /src
parent745fab0f0a6902de4808daf86d0628904e123791 (diff)
parenta999d34f393a74a51132b041821b3bd693ce5233 (diff)
Merge remote-tracking branch 'origin/fix/vk-evict-unused-pipelines' into test/linux-all-prs
# Conflicts: # src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
Diffstat (limited to 'src')
-rw-r--r--src/video_core/renderer_vulkan/vk_graphics_pipeline.h16
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp163
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.h21
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp1
4 files changed, 201 insertions, 0 deletions
diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
index daee7910c1..6d6e2ab12d 100644
--- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
+++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
@@ -124,6 +124,21 @@ public:
: nullptr;
}
+ /// The frame the pipeline cache last handed this pipeline out (evicts long unused ones).
+ void MarkUsed(u64 frame) noexcept {
+ last_use_frame = frame;
+ }
+
+ [[nodiscard]] u64 LastUse() const noexcept {
+ return last_use_frame;
+ }
+
+ /// Forgets the transitions, which may point to pipelines the cache evicted.
+ void ClearTransitions() noexcept {
+ transition_keys.clear();
+ transitions.clear();
+ }
+
[[nodiscard]] bool IsBuilt() const noexcept {
return is_built.load(std::memory_order::relaxed);
}
@@ -164,6 +179,7 @@ private:
std::vector<GraphicsPipelineCacheKey> transition_keys;
std::vector<GraphicsPipeline*> transitions;
+ u64 last_use_frame{};
std::array<vk::ShaderModule, NUM_STAGES> spv_modules;
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 80bff9b654..942be5351b 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -593,13 +593,96 @@ 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"
+// Pipelines the last two sessions used are built at boot. Each holds ~0.45 MB of driver memory:
+// title 1 built 4,800 from eight sessions, 1.5 GB more than building them on first use (but that
+// stalled frames 4x as often), while one session of its title screen uses ~1,500.
+constexpr u8 UsedPipelinesMaxAge = 2;
+
+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;
+ }
+ if (age < UsedPipelinesMaxAge) { // lists saved with a longer window
+ used.emplace(hash, age);
+ }
+ }
+ return used;
+}
+
+// Without a list of the pipelines recent sessions used (the first boot with this cache, or a session
+// that ended before saving it), this many cached graphics pipelines at most are built at boot and the
+// rest when first needed. A cache that grew over many builds held 48k pipelines, ~21 GB of driver
+// memory built before the game started; the list is only saved at exit or after two minutes, so a
+// boot that ran out of memory never saved one and the next boot did the same.
+constexpr size_t MaxPipelinesWithoutUsedList = 3000;
+
+// 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() + used_and_evicted.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 u64 hash : used_and_evicted) {
+ next.emplace(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)) {
@@ -612,6 +695,7 @@ GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() {
GraphicsPipeline* const next{current_pipeline->Next(graphics_key)};
if (next) {
current_pipeline = next;
+ current_pipeline->MarkUsed(frame_count);
return BuiltPipeline(current_pipeline);
}
}
@@ -659,6 +743,11 @@ 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;
+ size_t queued_graphics = 0;
if (use_vulkan_pipeline_cache) {
vulkan_pipeline_cache_filename = base_dir / "vulkan_pipelines.bin";
@@ -736,6 +825,16 @@ 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() ? queued_graphics >= MaxPipelinesWithoutUsedList
+ : !used_pipelines.contains(hash))) {
+ ++skipped_pipelines;
+ return;
+ }
+ ++queued_graphics;
+
workers.QueueWork([this, key, envs_ = std::move(envs), &state, &callback]() mutable {
ShaderPools pools;
boost::container::static_vector<Shader::Environment*, 5> env_ptrs;
@@ -760,6 +859,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 are built when first needed",
+ skipped_pipelines);
+ }
std::unique_lock lock{state.mutex};
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
@@ -876,13 +980,66 @@ 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());
}
current_pipeline = pipeline.get();
+ current_pipeline->MarkUsed(frame_count);
return BuiltPipeline(current_pipeline);
}
+void PipelineCache::TickFrame() {
+ ++frame_count;
+ while (!evicted_pipelines.empty() && scheduler.IsFree(evicted_pipelines.front().first)) {
+ evicted_pipelines.pop_front();
+ }
+ constexpr u64 EvictCheckFrames = 600;
+ if (frame_count % EvictCheckFrames == 0) {
+ EvictUnusedPipelines();
+ }
+}
+
+void PipelineCache::EvictUnusedPipelines() {
+ // Each built pipeline holds ~0.45 MB of driver memory and none was ever destroyed, so it grew
+ // with every place a session went (title 1: 1,500 on its title screen alone). One unused for
+ // this long is destroyed; it takes 2-3 ms to build again from the driver's pipeline cache.
+ constexpr u64 EvictAfterFrames = 9000;
+ if (frame_count <= EvictAfterFrames) {
+ return;
+ }
+ const u64 cutoff = frame_count - EvictAfterFrames;
+ const u64 tick = scheduler.CurrentTick();
+ size_t evicted = 0;
+ for (auto it = graphics_cache.begin(); it != graphics_cache.end();) {
+ GraphicsPipeline* const pipeline = it->second.get();
+ if (pipeline == nullptr || pipeline == current_pipeline || !pipeline->IsBuilt() ||
+ pipeline->LastUse() >= cutoff) {
+ ++it;
+ continue;
+ }
+ if (used_this_session.erase(pipeline) != 0) {
+ used_and_evicted.insert(it->first.Hash());
+ }
+ evicted_pipelines.emplace_back(tick, std::move(it->second));
+ it = graphics_cache.erase(it);
+ ++evicted;
+ }
+ if (evicted == 0) {
+ return;
+ }
+ for (auto& [key, pipeline] : graphics_cache) {
+ if (pipeline) {
+ pipeline->ClearTransitions();
+ }
+ }
+ LOG_INFO(Render_Vulkan, "Evicted {} graphics pipelines unused for {} frames, {} left", evicted,
+ EvictAfterFrames, graphics_cache.size());
+}
+
GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const noexcept {
if (pipeline->IsBuilt()) {
return pipeline;
@@ -1083,6 +1240,12 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline() {
QueueVulkanPipelineCacheFlush();
return pipeline;
}
+ if (disk_pipelines.contains(graphics_key.Hash())) {
+ // In the disk cache already, only not built at boot.
+ QueueVulkanPipelineCacheFlush();
+ return pipeline;
+ }
+ disk_pipelines.insert(graphics_key.Hash());
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..66fa615ea5 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h
@@ -10,7 +10,10 @@
#include <atomic>
#include <chrono>
#include <cstddef>
+#include <deque>
#include <filesystem>
+#include <unordered_map>
+#include <unordered_set>
#include <memory>
#include <type_traits>
#include "common/container/unordered_map.h"
@@ -114,6 +117,8 @@ public:
[[nodiscard]] GraphicsPipeline* CurrentGraphicsPipeline();
+ void TickFrame();
+
[[nodiscard]] ComputePipeline* CurrentComputePipeline();
void LoadDiskResources(u64 title_id, std::stop_token stop_loading,
@@ -163,6 +168,12 @@ private:
GraphicsPipelineCacheKey graphics_key{};
GraphicsPipeline* current_pipeline{};
+ /// Destroys graphics pipelines unused for a while, once the GPU is done with them.
+ void EvictUnusedPipelines();
+ u64 frame_count{};
+ std::deque<std::pair<u64, std::unique_ptr<GraphicsPipeline>>> evicted_pipelines; ///< (tick, pipeline)
+ std::unordered_set<u64> used_and_evicted; ///< Key hashes of pipelines used this session, evicted since
+
::Common::unordered_map<ComputePipelineCacheKey, std::unique_ptr<ComputePipeline>> compute_cache;
::Common::unordered_map<GraphicsPipelineCacheKey, std::unique_ptr<GraphicsPipeline>> graphics_cache;
@@ -173,6 +184,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{};
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index 91baee40e4..2f638026c9 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -1231,6 +1231,7 @@ void RasterizerVulkan::FlushCommands() {
void RasterizerVulkan::TickFrame() {
draw_counter = 0;
+ pipeline_cache.TickFrame();
guest_descriptor_queue.TickFrame();
compute_pass_descriptor_queue.TickFrame();
descriptor_buffer_ring.TickFrame();