diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-11-01 13:27:36 -0500 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-11-01 13:27:36 -0500 |
| commit | dfda645f6d4de27f7cab340ed66a79c2033c0c6b (patch) | |
| tree | 6ce720fdad485e4ad898027aa214ea84fc5bbea0 /src/video_core | |
| parent | 745fab0f0a6902de4808daf86d0628904e123791 (diff) | |
| parent | a999d34f393a74a51132b041821b3bd693ce5233 (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/video_core')
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_graphics_pipeline.h | 16 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 163 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_pipeline_cache.h | 21 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_rasterizer.cpp | 1 |
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(); |
