Add per-title vulkan pipeline cache (#1118)

This commit is contained in:
jbm11208 2025-06-23 14:51:25 -04:00 committed by GitHub
parent 680cbb559d
commit e24f8da113
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 145 additions and 21 deletions

View File

@ -10,6 +10,9 @@
#include "common/microprofile.h"
#include "common/scope_exit.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/loader/loader.h"
#include "video_core/pica/shader_setup.h"
#include "video_core/renderer_vulkan/pica_to_vk.h"
#include "video_core/renderer_vulkan/vk_descriptor_update_queue.h"
#include "video_core/renderer_vulkan/vk_instance.h"
@ -125,58 +128,102 @@ PipelineCache::~PipelineCache() {
SaveDiskCache();
}
void PipelineCache::LoadDiskCache() {
void PipelineCache::LoadDiskCache(const std::atomic_bool& stop_loading,
const VideoCore::DiskResourceLoadCallback& callback) {
vk::PipelineCacheCreateInfo cache_info{};
if (callback) {
callback(VideoCore::LoadCallbackStage::Prepare, 0, 0);
}
if (callback) {
callback(VideoCore::LoadCallbackStage::Build, 0, 1);
}
auto load_cache = [this, &cache_info, &callback](bool allow_fallback) {
const vk::Device device = instance.GetDevice();
try {
pipeline_cache = device.createPipelineCacheUnique(cache_info);
} catch (const vk::SystemError& err) {
LOG_ERROR(Render_Vulkan, "Failed to create pipeline cache: {}", err.what());
if (allow_fallback) {
// Fall back to empty cache
cache_info.initialDataSize = 0;
cache_info.pInitialData = nullptr;
try {
pipeline_cache = device.createPipelineCacheUnique(cache_info);
} catch (const vk::SystemError& err) {
LOG_ERROR(Render_Vulkan, "Failed to create fallback pipeline cache: {}",
err.what());
}
}
}
if (callback) {
callback(VideoCore::LoadCallbackStage::Complete, 0, 0);
}
};
// Try to load existing pipeline cache if disk cache is enabled and directories exist
if (!Settings::values.use_disk_shader_cache || !EnsureDirectories()) {
load_cache(false);
return;
}
// Try to load existing pipeline cache for this game/device combination
const auto cache_dir = GetPipelineCacheDir();
const u32 vendor_id = instance.GetVendorID();
const u32 device_id = instance.GetDeviceID();
const auto cache_file_path = fmt::format("{}{:x}{:x}.bin", cache_dir, vendor_id, device_id);
const u64 program_id = GetProgramID();
const auto cache_file_path =
fmt::format("{}{:016X}-{:X}{:X}.bin", cache_dir, program_id, vendor_id, device_id);
vk::PipelineCacheCreateInfo cache_info{};
std::vector<u8> cache_data;
SCOPE_EXIT({
const vk::Device device = instance.GetDevice();
pipeline_cache = device.createPipelineCacheUnique(cache_info);
});
FileUtil::IOFile cache_file{cache_file_path, "rb"};
if (!cache_file.IsOpen()) {
LOG_INFO(Render_Vulkan, "No pipeline cache found for device");
LOG_INFO(Render_Vulkan, "No pipeline cache found for title_id={:016X}", program_id);
load_cache(false);
return;
}
const u64 cache_file_size = cache_file.GetSize();
cache_data.resize(cache_file_size);
if (cache_file.ReadBytes(cache_data.data(), cache_file_size) != cache_file_size) {
LOG_ERROR(Render_Vulkan, "Error during pipeline cache read");
LOG_ERROR(Render_Vulkan, "Error reading pipeline cache");
load_cache(false);
return;
}
if (!IsCacheValid(cache_data)) {
LOG_WARNING(Render_Vulkan, "Pipeline cache provided invalid, removing");
LOG_WARNING(Render_Vulkan, "Pipeline cache invalid, removing");
cache_file.Close();
FileUtil::Delete(cache_file_path);
load_cache(false);
return;
}
LOG_INFO(Render_Vulkan, "Loading pipeline cache with size {} KB", cache_file_size / 1024);
LOG_INFO(Render_Vulkan, "Loading pipeline cache for title_id={:016X} with size {} KB",
program_id, cache_file_size / 1024);
cache_info.initialDataSize = cache_file_size;
cache_info.pInitialData = cache_data.data();
load_cache(true);
}
void PipelineCache::SaveDiskCache() {
if (!Settings::values.use_disk_shader_cache || !EnsureDirectories() || !pipeline_cache) {
// Save Vulkan pipeline cache
if (!Settings::values.use_disk_shader_cache || !pipeline_cache) {
return;
}
const auto cache_dir = GetPipelineCacheDir();
const u32 vendor_id = instance.GetVendorID();
const u32 device_id = instance.GetDeviceID();
const auto cache_file_path = fmt::format("{}{:x}{:x}.bin", cache_dir, vendor_id, device_id);
const u64 program_id = GetProgramID();
// Include both device info and program id in cache path to handle both GPU changes and
// different games
const auto cache_file_path =
fmt::format("{}{:016X}-{:X}{:X}.bin", cache_dir, program_id, vendor_id, device_id);
FileUtil::IOFile cache_file{cache_file_path, "wb"};
if (!cache_file.IsOpen()) {
@ -513,7 +560,49 @@ bool PipelineCache::EnsureDirectories() const {
}
std::string PipelineCache::GetPipelineCacheDir() const {
return FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir) + "vulkan" + DIR_SEP;
return FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir) + "vulkan" + DIR_SEP + "pipeline" +
DIR_SEP;
}
void PipelineCache::SwitchPipelineCache(u64 title_id, const std::atomic_bool& stop_loading,
const VideoCore::DiskResourceLoadCallback& callback) {
if (!Settings::values.use_disk_shader_cache || GetProgramID() == title_id) {
LOG_DEBUG(Render_Vulkan,
"Skipping pipeline cache switch - already using cache for title_id={:016X}",
title_id);
return;
}
if (callback) {
callback(VideoCore::LoadCallbackStage::Prepare, 0, 0);
}
if (callback) {
callback(VideoCore::LoadCallbackStage::Build, 0, 1);
}
// Make sure we have a valid pipeline cache before switching
if (!pipeline_cache) {
vk::PipelineCacheCreateInfo cache_info{};
try {
pipeline_cache = instance.GetDevice().createPipelineCacheUnique(cache_info);
} catch (const vk::SystemError& err) {
LOG_ERROR(Render_Vulkan, "Failed to create pipeline cache: {}", err.what());
return;
}
}
LOG_INFO(Render_Vulkan, "Switching pipeline cache to title_id={:016X}", title_id);
// Save current cache before switching
SaveDiskCache();
// Update program ID and load the new pipeline cache
SetProgramID(title_id);
LoadDiskCache(stop_loading, nullptr);
if (callback) {
callback(VideoCore::LoadCallbackStage::Complete, 0, 0);
}
}
} // namespace Vulkan

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -7,6 +7,7 @@
#include <bitset>
#include <tsl/robin_map.h>
#include "video_core/rasterizer_interface.h"
#include "video_core/renderer_vulkan/vk_graphics_pipeline.h"
#include "video_core/renderer_vulkan/vk_resource_pool.h"
#include "video_core/shader/generator/pica_fs_config.h"
@ -58,7 +59,8 @@ public:
}
/// Loads the pipeline cache stored to disk
void LoadDiskCache();
void LoadDiskCache(const std::atomic_bool& stop_loading = std::atomic_bool{false},
const VideoCore::DiskResourceLoadCallback& callback = {});
/// Stores the generated pipeline cache to disk
void SaveDiskCache();
@ -82,6 +84,20 @@ public:
/// Binds a fragment shader generated from PICA state
void UseFragmentShader(const Pica::RegsInternal& regs, const Pica::Shader::UserConfig& user);
/// Switches the shader disk cache to the specified title
void SwitchPipelineCache(u64 title_id,
const std::atomic_bool& stop_loading = std::atomic_bool{false},
const VideoCore::DiskResourceLoadCallback& callback = {});
/// Gets the current program ID
u64 GetProgramID() const {
return current_program_id;
}
void SetProgramID(u64 program_id) {
current_program_id = program_id;
}
private:
/// Builds the rasterizer pipeline layout
void BuildLayout();
@ -89,7 +105,7 @@ private:
/// Returns true when the disk data can be used by the current driver
bool IsCacheValid(std::span<const u8> cache_data) const;
/// Create shader disk cache directories. Returns true on success.
/// Create pipeline cache directories. Returns true on success.
bool EnsureDirectories() const;
/// Returns the pipeline cache storage dir
@ -110,7 +126,6 @@ private:
GraphicsPipeline* current_pipeline{};
tsl::robin_map<u64, std::unique_ptr<GraphicsPipeline>, Common::IdentityHash<u64>>
graphics_pipelines;
std::array<DescriptorHeap, NumDescriptorHeaps> descriptor_heaps;
std::array<vk::DescriptorSet, NumRasterizerSets> bound_descriptor_sets{};
std::array<u32, NumDynamicOffsets> offsets{};
@ -122,6 +137,8 @@ private:
std::unordered_map<Pica::Shader::Generator::PicaFixedGSConfig, Shader> fixed_geometry_shaders;
std::unordered_map<Pica::Shader::FSConfig, Shader> fragment_shaders;
Shader trivial_vertex_shader;
u64 current_program_id{0};
};
} // namespace Vulkan

View File

@ -8,6 +8,8 @@
#include "common/math_util.h"
#include "common/microprofile.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/loader/loader.h"
#include "core/memory.h"
#include "video_core/pica/pica_core.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
@ -143,7 +145,15 @@ void RasterizerVulkan::TickFrame() {
void RasterizerVulkan::LoadDefaultDiskResources(
const std::atomic_bool& stop_loading, const VideoCore::DiskResourceLoadCallback& callback) {
pipeline_cache.LoadDiskCache();
u64 program_id;
if (Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id) !=
Loader::ResultStatus::Success) {
program_id = 0;
}
pipeline_cache.SetProgramID(program_id);
pipeline_cache.LoadDiskCache(stop_loading, callback);
}
void RasterizerVulkan::SyncDrawState() {
@ -973,4 +983,9 @@ void RasterizerVulkan::UploadUniforms(bool accelerate_draw) {
uniform_buffer.Commit(used_bytes);
}
void RasterizerVulkan::SwitchDiskResources(u64 title_id) {
std::atomic_bool stop_loading = false;
pipeline_cache.SwitchPipelineCache(title_id, stop_loading, switch_disk_resources_callback);
}
} // namespace Vulkan

View File

@ -61,6 +61,9 @@ public:
u32 pixel_stride, ScreenInfo& screen_info);
bool AccelerateDrawBatch(bool is_indexed) override;
/// Switches the disk resources to the specified title
void SwitchDiskResources(u64 title_id) override;
private:
/// Syncs pipeline state from PICA registers
void SyncDrawState();