[Vulkan] Add shader disk storage / startup loading

Big refactor of the shader storage to allow both backends to share code
This commit is contained in:
Herman S.
2026-01-19 00:58:28 +09:00
parent d05273d6b7
commit f1aa4ea244
9 changed files with 1413 additions and 570 deletions
File diff suppressed because it is too large Load Diff
+13 -34
View File
@@ -18,6 +18,7 @@
#include <memory>
#include <mutex>
#include <queue>
#include <set>
#include <string>
#include <thread>
#include <unordered_map>
@@ -36,6 +37,7 @@
#include "xenia/gpu/primitive_processor.h"
#include "xenia/gpu/register_file.h"
#include "xenia/gpu/registers.h"
#include "xenia/gpu/shader_storage.h"
#include "xenia/gpu/xenos.h"
#include "xenia/ui/d3d12/d3d12_api.h"
@@ -114,15 +116,6 @@ class PipelineCache {
}
private:
XEPACKEDSTRUCT(ShaderStoredHeader, {
uint64_t ucode_data_hash;
uint32_t ucode_dword_count : 31;
xenos::ShaderType type : 1;
static constexpr uint32_t kVersion = 0x20201219;
});
// Update PipelineDescription::kVersion if any of the Pipeline* enums are
// changed!
@@ -290,6 +283,11 @@ class PipelineCache {
IDxcUtils* dxc_utils = nullptr,
IDxcCompiler* dxc_compiler = nullptr);
// Translates shaders in parallel for storage loading.
void TranslateShadersForStorage(
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed,
bool edram_rov_used);
// If draw_util::IsRasterizationPotentiallyDone is false, the pixel shader
// MUST be made nullptr BEFORE calling this! The shaders must be translated
// and valid, unless for_placeholder is true.
@@ -416,33 +414,14 @@ class PipelineCache {
// changed.
Pipeline* current_pipeline_ = nullptr;
// Currently open shader storage path.
std::filesystem::path shader_storage_cache_root_;
// Currently open shader storage state.
uint32_t shader_storage_title_id_ = 0;
std::atomic<bool> shader_storage_file_flush_needed_{false};
std::atomic<bool> pipeline_storage_file_flush_needed_{false};
// Shader storage output stream, for preload in the next emulator runs.
FILE* shader_storage_file_ = nullptr;
// For only writing shaders to the currently open storage once, incremented
// when switching the storage.
uint32_t shader_storage_index_ = 0;
bool shader_storage_file_flush_needed_ = false;
// Pipeline storage output stream, for preload in the next emulator runs.
FILE* pipeline_storage_file_ = nullptr;
bool pipeline_storage_file_flush_needed_ = false;
// Thread for asynchronous writing to the storage streams.
void StorageWriteThread();
std::mutex storage_write_request_lock_;
std::condition_variable storage_write_request_cond_;
// Storage thread input is protected with storage_write_request_lock_, and the
// thread is notified about its change via storage_write_request_cond_.
std::deque<const Shader*> storage_write_shader_queue_;
std::deque<PipelineStoredDescription> storage_write_pipeline_queue_;
bool storage_write_flush_shaders_ = false;
bool storage_write_flush_pipelines_ = false;
bool storage_write_thread_shutdown_ = false;
std::unique_ptr<xe::threading::Thread> storage_write_thread_;
// Storage writer for shaders and pipelines (owns file handles and storage
// index).
ShaderStorageWriter<PipelineStoredDescription> storage_writer_;
// Pipeline creation threads.
void CreationThread(size_t thread_index);
+15 -3
View File
@@ -11,6 +11,7 @@
#define XENIA_GPU_SHADER_H_
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <set>
@@ -1007,9 +1008,20 @@ class Shader {
// An externally managed identifier of the shader storage the microcode of the
// shader was last written to, or was loaded from, to only write the shader
// microcode to the storage once. UINT32_MAX by default.
uint32_t ucode_storage_index() const { return ucode_storage_index_; }
uint32_t ucode_storage_index() const {
return ucode_storage_index_.load(std::memory_order_relaxed);
}
void set_ucode_storage_index(uint32_t storage_index) {
ucode_storage_index_ = storage_index;
ucode_storage_index_.store(storage_index, std::memory_order_relaxed);
}
// Atomically set storage index if changed. Returns true if updated.
bool try_set_ucode_storage_index(uint32_t new_index) {
uint32_t expected = ucode_storage_index_.load(std::memory_order_relaxed);
if (expected == new_index) {
return false;
}
return ucode_storage_index_.compare_exchange_strong(
expected, new_index, std::memory_order_relaxed);
}
// Dumps the shader's microcode binary and, if analyzed, disassembly, to files
@@ -1074,7 +1086,7 @@ class Shader {
// Modification bits -> translation.
std::unordered_map<uint64_t, Translation*> translations_;
uint32_t ucode_storage_index_ = UINT32_MAX;
std::atomic<uint32_t> ucode_storage_index_{UINT32_MAX};
private:
void GatherExecInformation(
File diff suppressed because it is too large Load Diff
@@ -167,6 +167,15 @@ void VulkanCommandProcessor::TracePlaybackWroteMemory(uint32_t base_ptr,
primitive_processor_->MemoryInvalidationCallback(base_ptr, length, true);
}
void VulkanCommandProcessor::InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback) {
CommandProcessor::InitializeShaderStorage(cache_root, title_id, blocking,
nullptr);
pipeline_cache_->InitializeShaderStorage(cache_root, title_id, blocking,
std::move(completion_callback));
}
void VulkanCommandProcessor::RestoreEdramSnapshot(const void* snapshot) {}
void VulkanCommandProcessor::PrepareForWait() {
@@ -148,6 +148,10 @@ class VulkanCommandProcessor final : public CommandProcessor {
void TracePlaybackWroteMemory(uint32_t base_ptr, uint32_t length) override;
void InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback = nullptr) override;
void RestoreEdramSnapshot(const void* snapshot) override;
void PrepareForWait() override;
File diff suppressed because it is too large Load Diff
@@ -13,11 +13,15 @@
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <deque>
#include <filesystem>
#include <functional>
#include <memory>
#include <mutex>
#include <queue>
#include <set>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -29,6 +33,7 @@
#include "xenia/gpu/primitive_processor.h"
#include "xenia/gpu/register_file.h"
#include "xenia/gpu/registers.h"
#include "xenia/gpu/shader_storage.h"
#include "xenia/gpu/spirv_shader_translator.h"
#include "xenia/gpu/vulkan/vulkan_render_target_cache.h"
#include "xenia/gpu/vulkan/vulkan_shader.h"
@@ -105,6 +110,12 @@ class VulkanPipelineCache {
bool Initialize();
void Shutdown();
// Shader and pipeline storage.
void InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback = nullptr);
void ShutdownShaderStorage();
void EndSubmission();
bool IsCreatingPipelines();
@@ -263,6 +274,18 @@ class VulkanPipelineCache {
return size_t(description.GetHash());
}
};
static constexpr uint32_t kVersion = 0x20250118;
});
// Pipeline storage constants.
static constexpr uint32_t kPipelineStorageVersionWithoutAPI = 0x20201219;
static constexpr uint32_t kPipelineStorageAPIMagicVulkan = 'VLKN';
// Pipeline storage description.
XEPACKEDSTRUCT(PipelineStoredDescription, {
uint64_t description_hash;
PipelineDescription description;
});
// creation threads, with everything needed from caches pre-looked-up.
@@ -320,6 +343,11 @@ class VulkanPipelineCache {
bool TranslateAnalyzedShader(SpirvShaderTranslator& translator,
VulkanShader::VulkanTranslation& translation);
// Translates shaders in parallel for storage loading.
void TranslateShadersForStorage(
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed,
bool edram_fsi_used);
void WritePipelineRenderTargetDescription(
reg::RB_BLENDCONTROL blend_control, uint32_t write_mask,
PipelineRenderTarget& render_target_out) const;
@@ -468,6 +496,10 @@ class VulkanPipelineCache {
std::condition_variable creation_request_cond_;
std::unique_ptr<xe::threading::Event> creation_completion_event_ = nullptr;
std::atomic<bool> creation_completion_set_event_{false};
std::function<void()> creation_completion_callback_;
// During startup loading, don't block on pipeline creation to allow game
// boot.
bool startup_loading_ = false;
// Deferred destruction of pipelines.
// Pipelines are only destroyed after the GPU submission that might reference
@@ -475,6 +507,18 @@ class VulkanPipelineCache {
void ProcessDeferredDestructions();
std::vector<std::pair<VkPipeline, uint64_t>> deferred_destroy_pipelines_;
std::mutex deferred_destroy_mutex_;
// Shader and pipeline storage.
uint32_t shader_storage_title_id_ = 0;
std::atomic<bool> shader_storage_file_flush_needed_{false};
std::atomic<bool> pipeline_storage_file_flush_needed_{false};
// Storage writer for shaders and pipelines (owns file handles and storage
// index).
ShaderStorageWriter<PipelineStoredDescription> storage_writer_;
// VkPipelineCache persistence path.
std::filesystem::path vk_pipeline_cache_path_;
};
} // namespace vulkan
@@ -75,6 +75,7 @@ XE_UI_VULKAN_FUNCTION(vkGetBufferMemoryRequirements)
XE_UI_VULKAN_FUNCTION(vkGetDeviceQueue)
XE_UI_VULKAN_FUNCTION(vkGetFenceStatus)
XE_UI_VULKAN_FUNCTION(vkGetImageMemoryRequirements)
XE_UI_VULKAN_FUNCTION(vkGetPipelineCacheData)
XE_UI_VULKAN_FUNCTION(vkInvalidateMappedMemoryRanges)
XE_UI_VULKAN_FUNCTION(vkMapMemory)
XE_UI_VULKAN_FUNCTION(vkResetCommandPool)