mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-19 10:43:33 +00:00
vulkan: Add memory pressure handling and pipeline eviction
Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -343,6 +344,12 @@ private:
|
|||||||
|
|
||||||
void RunGarbageCollector();
|
void RunGarbageCollector();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// Public interface to trigger garbage collection
|
||||||
|
void TriggerGarbageCollection() {
|
||||||
|
RunGarbageCollector();
|
||||||
|
}
|
||||||
|
|
||||||
void BindHostIndexBuffer();
|
void BindHostIndexBuffer();
|
||||||
|
|
||||||
void BindHostVertexBuffers();
|
void BindHostVertexBuffers();
|
||||||
|
|||||||
@@ -426,6 +426,59 @@ PipelineCache::~PipelineCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PipelineCache::EvictOldPipelines() {
|
||||||
|
constexpr u64 FRAMES_TO_KEEP = 2000;
|
||||||
|
|
||||||
|
const u64 current_frame = scheduler.CurrentTick();
|
||||||
|
|
||||||
|
if (current_frame - last_memory_pressure_frame < MEMORY_PRESSURE_COOLDOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last_memory_pressure_frame = current_frame;
|
||||||
|
|
||||||
|
const u64 evict_before_frame = current_frame > FRAMES_TO_KEEP ? current_frame - FRAMES_TO_KEEP : 0;
|
||||||
|
|
||||||
|
size_t evicted_graphics = 0;
|
||||||
|
size_t evicted_compute = 0;
|
||||||
|
|
||||||
|
for (auto it = graphics_cache.begin(); it != graphics_cache.end();) {
|
||||||
|
const GraphicsPipeline* pipeline = it->second.get();
|
||||||
|
if (pipeline && pipeline != current_pipeline) {
|
||||||
|
auto use_it = graphics_pipeline_last_use.find(pipeline);
|
||||||
|
if (use_it == graphics_pipeline_last_use.end() || use_it->second < evict_before_frame) {
|
||||||
|
graphics_pipeline_last_use.erase(pipeline);
|
||||||
|
it = graphics_cache.erase(it);
|
||||||
|
evicted_graphics++;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = compute_cache.begin(); it != compute_cache.end();) {
|
||||||
|
const ComputePipeline* pipeline = it->second.get();
|
||||||
|
if (pipeline) {
|
||||||
|
auto use_it = compute_pipeline_last_use.find(pipeline);
|
||||||
|
if (use_it == compute_pipeline_last_use.end() || use_it->second < evict_before_frame) {
|
||||||
|
compute_pipeline_last_use.erase(pipeline);
|
||||||
|
it = compute_cache.erase(it);
|
||||||
|
evicted_compute++;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evicted_graphics > 0 || evicted_compute > 0) {
|
||||||
|
LOG_INFO(Render_Vulkan, "Evicted {} graphics and {} compute pipelines to free memory",
|
||||||
|
evicted_graphics, evicted_compute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() {
|
GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() {
|
||||||
MICROPROFILE_SCOPE(Vulkan_PipelineCache);
|
MICROPROFILE_SCOPE(Vulkan_PipelineCache);
|
||||||
|
|
||||||
@@ -439,10 +492,16 @@ GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() {
|
|||||||
GraphicsPipeline* const next{current_pipeline->Next(graphics_key)};
|
GraphicsPipeline* const next{current_pipeline->Next(graphics_key)};
|
||||||
if (next) {
|
if (next) {
|
||||||
current_pipeline = next;
|
current_pipeline = next;
|
||||||
|
// Update last use frame
|
||||||
|
graphics_pipeline_last_use[current_pipeline] = scheduler.CurrentTick();
|
||||||
return BuiltPipeline(current_pipeline);
|
return BuiltPipeline(current_pipeline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return CurrentGraphicsPipelineSlowPath();
|
GraphicsPipeline* result = CurrentGraphicsPipelineSlowPath();
|
||||||
|
if (result) {
|
||||||
|
graphics_pipeline_last_use[result] = scheduler.CurrentTick();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputePipeline* PipelineCache::CurrentComputePipeline() {
|
ComputePipeline* PipelineCache::CurrentComputePipeline() {
|
||||||
@@ -460,10 +519,14 @@ ComputePipeline* PipelineCache::CurrentComputePipeline() {
|
|||||||
};
|
};
|
||||||
const auto [pair, is_new]{compute_cache.try_emplace(key)};
|
const auto [pair, is_new]{compute_cache.try_emplace(key)};
|
||||||
auto& pipeline{pair->second};
|
auto& pipeline{pair->second};
|
||||||
if (!is_new) {
|
if (!is_new && pipeline) {
|
||||||
|
compute_pipeline_last_use[pipeline.get()] = scheduler.CurrentTick();
|
||||||
return pipeline.get();
|
return pipeline.get();
|
||||||
}
|
}
|
||||||
pipeline = CreateComputePipeline(key, shader);
|
pipeline = CreateComputePipeline(key, shader);
|
||||||
|
if (pipeline) {
|
||||||
|
compute_pipeline_last_use[pipeline.get()] = scheduler.CurrentTick();
|
||||||
|
}
|
||||||
return pipeline.get();
|
return pipeline.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,6 +768,13 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
|
|||||||
descriptor_pool, guest_descriptor_queue, thread_worker, statistics, render_pass_cache, key,
|
descriptor_pool, guest_descriptor_queue, thread_worker, statistics, render_pass_cache, key,
|
||||||
std::move(modules), infos);
|
std::move(modules), infos);
|
||||||
|
|
||||||
|
} catch (const vk::Exception& exception) {
|
||||||
|
if (exception.GetResult() == VK_ERROR_OUT_OF_DEVICE_MEMORY) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Out of device memory during graphics pipeline creation, attempting recovery");
|
||||||
|
EvictOldPipelines();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
throw;
|
||||||
} catch (const Shader::Exception& exception) {
|
} catch (const Shader::Exception& exception) {
|
||||||
auto hash = key.Hash();
|
auto hash = key.Hash();
|
||||||
size_t env_index{0};
|
size_t env_index{0};
|
||||||
@@ -801,6 +871,13 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
|
|||||||
guest_descriptor_queue, thread_worker, statistics,
|
guest_descriptor_queue, thread_worker, statistics,
|
||||||
&shader_notify, program.info, std::move(spv_module));
|
&shader_notify, program.info, std::move(spv_module));
|
||||||
|
|
||||||
|
} catch (const vk::Exception& exception) {
|
||||||
|
if (exception.GetResult() == VK_ERROR_OUT_OF_DEVICE_MEMORY) {
|
||||||
|
LOG_ERROR(Render_Vulkan, "Out of device memory during compute pipeline creation, attempting recovery");
|
||||||
|
EvictOldPipelines();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
throw;
|
||||||
} catch (const Shader::Exception& exception) {
|
} catch (const Shader::Exception& exception) {
|
||||||
LOG_ERROR(Render_Vulkan, "{}", exception.what());
|
LOG_ERROR(Render_Vulkan, "{}", exception.what());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -140,6 +141,15 @@ private:
|
|||||||
vk::PipelineCache LoadVulkanPipelineCache(const std::filesystem::path& filename,
|
vk::PipelineCache LoadVulkanPipelineCache(const std::filesystem::path& filename,
|
||||||
u32 expected_cache_version);
|
u32 expected_cache_version);
|
||||||
|
|
||||||
|
/// Evicts old unused pipelines to free memory when under pressure
|
||||||
|
void EvictOldPipelines();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// Public interface to evict old pipelines (for memory pressure handling)
|
||||||
|
void TriggerPipelineEviction() {
|
||||||
|
EvictOldPipelines();
|
||||||
|
}
|
||||||
|
|
||||||
const Device& device;
|
const Device& device;
|
||||||
Scheduler& scheduler;
|
Scheduler& scheduler;
|
||||||
DescriptorPool& descriptor_pool;
|
DescriptorPool& descriptor_pool;
|
||||||
@@ -157,6 +167,12 @@ private:
|
|||||||
std::unordered_map<ComputePipelineCacheKey, std::unique_ptr<ComputePipeline>> compute_cache;
|
std::unordered_map<ComputePipelineCacheKey, std::unique_ptr<ComputePipeline>> compute_cache;
|
||||||
std::unordered_map<GraphicsPipelineCacheKey, std::unique_ptr<GraphicsPipeline>> graphics_cache;
|
std::unordered_map<GraphicsPipelineCacheKey, std::unique_ptr<GraphicsPipeline>> graphics_cache;
|
||||||
|
|
||||||
|
std::unordered_map<const GraphicsPipeline*, u64> graphics_pipeline_last_use;
|
||||||
|
std::unordered_map<const ComputePipeline*, u64> compute_pipeline_last_use;
|
||||||
|
|
||||||
|
u64 last_memory_pressure_frame{0};
|
||||||
|
static constexpr u64 MEMORY_PRESSURE_COOLDOWN = 300;
|
||||||
|
|
||||||
ShaderPools main_pools;
|
ShaderPools main_pools;
|
||||||
|
|
||||||
Shader::Profile profile;
|
Shader::Profile profile;
|
||||||
@@ -170,6 +186,7 @@ private:
|
|||||||
Common::ThreadWorker workers;
|
Common::ThreadWorker workers;
|
||||||
Common::ThreadWorker serialization_thread;
|
Common::ThreadWorker serialization_thread;
|
||||||
DynamicFeatures dynamic_features;
|
DynamicFeatures dynamic_features;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|||||||
@@ -202,6 +202,14 @@ RasterizerVulkan::RasterizerVulkan(Core::Frontend::EmuWindow& emu_window_, Tegra
|
|||||||
fence_manager(*this, gpu, texture_cache, buffer_cache, query_cache, device, scheduler),
|
fence_manager(*this, gpu, texture_cache, buffer_cache, query_cache, device, scheduler),
|
||||||
wfi_event(device.GetLogical().CreateEvent()) {
|
wfi_event(device.GetLogical().CreateEvent()) {
|
||||||
scheduler.SetQueryCache(query_cache);
|
scheduler.SetQueryCache(query_cache);
|
||||||
|
|
||||||
|
memory_allocator.SetMemoryPressureCallback([this]() {
|
||||||
|
pipeline_cache.TriggerPipelineEviction();
|
||||||
|
texture_cache.TriggerGarbageCollection();
|
||||||
|
buffer_cache.TriggerGarbageCollection();
|
||||||
|
staging_pool.TriggerCacheRelease(MemoryUsage::Upload);
|
||||||
|
staging_pool.TriggerCacheRelease(MemoryUsage::Download);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
RasterizerVulkan::~RasterizerVulkan() = default;
|
RasterizerVulkan::~RasterizerVulkan() = default;
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ private:
|
|||||||
void ReleaseCache(MemoryUsage usage);
|
void ReleaseCache(MemoryUsage usage);
|
||||||
|
|
||||||
void ReleaseLevel(StagingBuffersCache& cache, size_t log2);
|
void ReleaseLevel(StagingBuffersCache& cache, size_t log2);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// Public interface to release staging buffer cache
|
||||||
|
void TriggerCacheRelease(MemoryUsage usage) {
|
||||||
|
ReleaseCache(usage);
|
||||||
|
}
|
||||||
size_t Region(size_t iter) const noexcept {
|
size_t Region(size_t iter) const noexcept {
|
||||||
return iter / region_size;
|
return iter / region_size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -289,6 +290,12 @@ private:
|
|||||||
/// Runs the Garbage Collector.
|
/// Runs the Garbage Collector.
|
||||||
void RunGarbageCollector();
|
void RunGarbageCollector();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// Public interface to trigger garbage collection
|
||||||
|
void TriggerGarbageCollection() {
|
||||||
|
RunGarbageCollector();
|
||||||
|
}
|
||||||
|
|
||||||
/// Fills image_view_ids in the image views in indices
|
/// Fills image_view_ids in the image views in indices
|
||||||
template <bool has_blacklists>
|
template <bool has_blacklists>
|
||||||
void FillImageViews(DescriptorTable<TICEntry>& table,
|
void FillImageViews(DescriptorTable<TICEntry>& table,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -290,6 +291,15 @@ MemoryCommit MemoryAllocator::Commit(const VkMemoryRequirements& requirements, M
|
|||||||
// Commit has failed, allocate more memory.
|
// Commit has failed, allocate more memory.
|
||||||
const u64 chunk_size = AllocationChunkSize(requirements.size);
|
const u64 chunk_size = AllocationChunkSize(requirements.size);
|
||||||
if (!TryAllocMemory(flags, type_mask, chunk_size)) {
|
if (!TryAllocMemory(flags, type_mask, chunk_size)) {
|
||||||
|
if (memory_pressure_callback) {
|
||||||
|
LOG_WARNING(Render_Vulkan, "Memory allocation failed, attempting to free resources...");
|
||||||
|
memory_pressure_callback();
|
||||||
|
if (TryAllocMemory(flags, type_mask, chunk_size)) {
|
||||||
|
if (auto commit = TryCommit(requirements, flags)) {
|
||||||
|
return std::move(*commit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// TODO(Rodrigo): Handle out of memory situations in some way like flushing to guest memory.
|
// TODO(Rodrigo): Handle out of memory situations in some way like flushing to guest memory.
|
||||||
throw vk::Exception(VK_ERROR_OUT_OF_DEVICE_MEMORY);
|
throw vk::Exception(VK_ERROR_OUT_OF_DEVICE_MEMORY);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -111,6 +113,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
MemoryCommit Commit(const VkMemoryRequirements& requirements, MemoryUsage usage);
|
MemoryCommit Commit(const VkMemoryRequirements& requirements, MemoryUsage usage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a callback to be called when memory pressure is detected.
|
||||||
|
* This allows external systems (like caches) to free resources.
|
||||||
|
*/
|
||||||
|
void SetMemoryPressureCallback(std::function<void()> callback) {
|
||||||
|
memory_pressure_callback = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
/// Commits memory required by the buffer and binds it.
|
/// Commits memory required by the buffer and binds it.
|
||||||
MemoryCommit Commit(const vk::Buffer& buffer, MemoryUsage usage);
|
MemoryCommit Commit(const vk::Buffer& buffer, MemoryUsage usage);
|
||||||
|
|
||||||
@@ -138,6 +148,7 @@ private:
|
|||||||
VkDeviceSize buffer_image_granularity; // The granularity for adjacent offsets between buffers
|
VkDeviceSize buffer_image_granularity; // The granularity for adjacent offsets between buffers
|
||||||
// and optimal images
|
// and optimal images
|
||||||
u32 valid_memory_types{~0u};
|
u32 valid_memory_types{~0u};
|
||||||
|
std::function<void()> memory_pressure_callback; ///< Callback to free resources under memory pressure
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|||||||
Reference in New Issue
Block a user