[GPU] Add vulkan 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-16 12:54:38 +09:00
parent 5845f3437b
commit 64e51c544e
16 changed files with 1582 additions and 622 deletions

View File

@@ -1648,14 +1648,17 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path,
}
}
// Initializing the shader storage in a blocking way so the user doesn't
// miss the initial seconds - for instance, sound from an intro video may
// start playing before the video can be seen if doing this in parallel with
// the main thread.
on_shader_storage_initialization(true);
graphics_system_->InitializeShaderStorage(cache_root_, title_id_.value(),
true);
on_shader_storage_initialization(false);
// Initialize shader storage asynchronously - pipeline compilation happens in
// background while the game goes through its normal startup (loading screens,
// intro videos, etc.). With async_shader_compilation enabled, draws are
// skipped until pipelines are ready, so this is safe. By the time actual
// gameplay starts, most cached pipelines should be compiled.
if (graphics_system_) {
on_shader_storage_initialization(true);
graphics_system_->InitializeShaderStorage(
cache_root_, title_id_.value(), false,
[this]() { on_shader_storage_initialization(false); });
}
auto main_thread = kernel_state_->LaunchModule(module);
if (!main_thread) {

View File

@@ -172,7 +172,11 @@ void CommandProcessor::Shutdown() {
}
void CommandProcessor::InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback) {
if (completion_callback) {
completion_callback();
}
}
void CommandProcessor::RequestFrameTrace(

View File

@@ -121,8 +121,9 @@ class CommandProcessor {
// May be called not only from the command processor thread when the command
// processor is paused, and the termination of this function may be explicitly
// awaited.
virtual void InitializeShaderStorage(const std::filesystem::path& cache_root,
uint32_t title_id, bool blocking);
virtual void InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback = nullptr);
virtual void RequestFrameTrace(const std::filesystem::path& root_path);
virtual void BeginTracing(const std::filesystem::path& root_path);

View File

@@ -66,9 +66,12 @@ void D3D12CommandProcessor::ClearCaches() {
}
void D3D12CommandProcessor::InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
CommandProcessor::InitializeShaderStorage(cache_root, title_id, blocking);
pipeline_cache_->InitializeShaderStorage(cache_root, title_id, blocking);
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 D3D12CommandProcessor::RequestFrameTrace(
@@ -2585,7 +2588,12 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type,
if (cvars::async_shader_compilation) {
if (pipeline_cache_->GetD3D12PipelineByHandle(pipeline_handle) == nullptr) {
XELOGI("Skipping draw - pipeline not ready");
XELOGI(
"Skipping draw - pipeline not ready: VS {:016X} mod {:016X}, PS "
"{:016X} mod {:016X}",
vertex_shader->ucode_data_hash(), vertex_shader_modification.value,
pixel_shader ? pixel_shader->ucode_data_hash() : 0,
pixel_shader_modification.value);
return true;
}
// Re-fetch root signature now that pipeline is ready.

View File

@@ -68,8 +68,9 @@ class D3D12CommandProcessor final : public CommandProcessor {
void ClearCaches() override;
void InitializeShaderStorage(const std::filesystem::path& cache_root,
uint32_t title_id, bool blocking) override;
void InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback = nullptr) override;
void RequestFrameTrace(const std::filesystem::path& root_path) override;

View File

@@ -218,7 +218,6 @@ void PipelineCache::Shutdown() {
delete it.second;
}
shaders_.clear();
shader_storage_index_ = 0;
// Shut down shader translation.
ui::d3d12::util::ReleaseAndNull(dxc_compiler_);
@@ -227,346 +226,48 @@ void PipelineCache::Shutdown() {
}
void PipelineCache::InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback) {
ShutdownShaderStorage();
auto shader_storage_root = cache_root / "shaders";
// For files that can be moved between different hosts.
// Host PSO blobs - if ever added - should be stored in shaders/local/ (they
// currently aren't used because because they may be not very practical -
// would need to invalidate them every commit likely, and additional I/O
// cost - though D3D's internal validation would possibly be enough to ensure
// they are up to date).
auto shader_storage_shareable_root = shader_storage_root / "shareable";
if (!std::filesystem::exists(shader_storage_shareable_root)) {
if (!std::filesystem::create_directories(shader_storage_shareable_root)) {
XELOGE(
"Failed to create the shareable shader storage directory, persistent "
"shader storage will be disabled: {}",
shader_storage_shareable_root);
return;
}
}
bool edram_rov_used = render_target_cache_.GetPath() ==
RenderTargetCache::Path::kPixelShaderInterlock;
// Initialize the pipeline storage stream - read pipeline descriptions and
// collect used shader modifications to translate.
ShaderStorageWriter<PipelineStoredDescription>::PipelineStorageConfig
pipeline_config;
pipeline_config.file_suffix =
fmt::format(".{}.d3d12.xpso", edram_rov_used ? "rov" : "rtv");
pipeline_config.api_magic = edram_rov_used ? 0x4F525844 : 0x54525844;
pipeline_config.version =
std::max(PipelineDescription::kVersion,
DxbcShaderTranslator::Modification::kVersion);
uint32_t storage_index = storage_writer_.storage_index() + 1;
std::vector<PipelineStoredDescription> pipeline_stored_descriptions;
// <Shader hash, modification bits>.
std::set<std::pair<uint64_t, uint64_t>> shader_translations_needed;
auto pipeline_storage_file_path =
shader_storage_shareable_root /
fmt::format("{:08X}.{}.d3d12.xpso", title_id,
edram_rov_used ? "rov" : "rtv");
pipeline_storage_file_ =
xe::filesystem::OpenFile(pipeline_storage_file_path, "a+b");
if (!pipeline_storage_file_) {
XELOGE(
"Failed to open the Direct3D 12 pipeline description storage file for "
"writing, persistent shader storage will be disabled: {}",
pipeline_storage_file_path);
return;
}
pipeline_storage_file_flush_needed_ = false;
// 'XEPS'.
constexpr uint32_t pipeline_storage_magic = 0x53504558;
// 'DXRO' or 'DXRT'.
const uint32_t pipeline_storage_magic_api =
edram_rov_used ? 0x4F525844 : 0x54525844;
const uint32_t pipeline_storage_version_swapped =
xe::byte_swap(std::max(PipelineDescription::kVersion,
DxbcShaderTranslator::Modification::kVersion));
struct {
uint32_t magic;
uint32_t magic_api;
uint32_t version_swapped;
} pipeline_storage_file_header;
if (fread(&pipeline_storage_file_header, sizeof(pipeline_storage_file_header),
1, pipeline_storage_file_) &&
pipeline_storage_file_header.magic == pipeline_storage_magic &&
pipeline_storage_file_header.magic_api == pipeline_storage_magic_api &&
pipeline_storage_file_header.version_swapped ==
pipeline_storage_version_swapped) {
xe::filesystem::Seek(pipeline_storage_file_, 0, SEEK_END);
int64_t pipeline_storage_told_end =
xe::filesystem::Tell(pipeline_storage_file_);
size_t pipeline_storage_told_count =
size_t(pipeline_storage_told_end >=
int64_t(sizeof(pipeline_storage_file_header))
? (uint64_t(pipeline_storage_told_end) -
sizeof(pipeline_storage_file_header)) /
sizeof(PipelineStoredDescription)
: 0);
if (pipeline_storage_told_count &&
xe::filesystem::Seek(pipeline_storage_file_,
int64_t(sizeof(pipeline_storage_file_header)),
SEEK_SET)) {
pipeline_stored_descriptions.resize(pipeline_storage_told_count);
pipeline_stored_descriptions.resize(
fread(pipeline_stored_descriptions.data(),
sizeof(PipelineStoredDescription), pipeline_storage_told_count,
pipeline_storage_file_));
size_t pipeline_storage_read_count = pipeline_stored_descriptions.size();
for (size_t i = 0; i < pipeline_storage_read_count; ++i) {
const PipelineStoredDescription& pipeline_stored_description =
pipeline_stored_descriptions[i];
// Validate file integrity, stop and truncate the stream if data is
// corrupted.
if (XXH3_64bits(&pipeline_stored_description.description,
sizeof(pipeline_stored_description.description)) !=
pipeline_stored_description.description_hash) {
pipeline_stored_descriptions.resize(i);
break;
}
// TODO(Triang3l): On Vulkan, skip pipelines requiring unsupported
// device features (to keep the cache files mostly shareable across
// devices).
// Mark the shader modifications as needed for translation.
shader_translations_needed.emplace(
pipeline_stored_description.description.vertex_shader_hash,
pipeline_stored_description.description.vertex_shader_modification);
if (pipeline_stored_description.description.pixel_shader_hash) {
shader_translations_needed.emplace(
pipeline_stored_description.description.pixel_shader_hash,
pipeline_stored_description.description
.pixel_shader_modification);
}
}
}
}
size_t logical_processor_count = xe::threading::logical_processor_count();
if (!logical_processor_count) {
// Pick some reasonable amount if couldn't determine the number of cores.
logical_processor_count = 6;
}
// Initialize the Xenos shader storage stream.
uint64_t shader_storage_initialization_start =
xe::Clock::QueryHostTickCount();
auto shader_storage_file_path =
shader_storage_shareable_root / fmt::format("{:08X}.xsh", title_id);
shader_storage_file_ =
xe::filesystem::OpenFile(shader_storage_file_path, "a+b");
if (!shader_storage_file_) {
XELOGE(
"Failed to open the guest shader storage file for writing, persistent "
"shader storage will be disabled: {}",
shader_storage_file_path);
fclose(pipeline_storage_file_);
pipeline_storage_file_ = nullptr;
return;
}
++shader_storage_index_;
shader_storage_file_flush_needed_ = false;
struct {
uint32_t magic;
uint32_t version_swapped;
} shader_storage_file_header;
// 'XESH'.
constexpr uint32_t shader_storage_magic = 0x48534558;
if (fread(&shader_storage_file_header, sizeof(shader_storage_file_header), 1,
shader_storage_file_) &&
shader_storage_file_header.magic == shader_storage_magic &&
xe::byte_swap(shader_storage_file_header.version_swapped) ==
ShaderStoredHeader::kVersion) {
uint64_t shader_storage_valid_bytes = sizeof(shader_storage_file_header);
// Load and translate shaders written by previous Xenia executions until the
// end of the file or until a corrupted one is detected.
ShaderStoredHeader shader_header;
std::vector<uint32_t> ucode_dwords;
ucode_dwords.reserve(0xFFFF);
size_t shaders_translated = 0;
// Threads overlapping file reading.
std::mutex shaders_translation_thread_mutex;
std::condition_variable shaders_translation_thread_cond;
std::deque<D3D12Shader*> shaders_to_translate;
size_t shader_translation_threads_busy = 0;
bool shader_translation_threads_shutdown = false;
std::mutex shaders_failed_to_translate_mutex;
std::vector<D3D12Shader::D3D12Translation*> shaders_failed_to_translate;
auto shader_translation_thread_function = [&]() {
const ui::d3d12::D3D12Provider& provider =
command_processor_.GetD3D12Provider();
StringBuffer ucode_disasm_buffer;
DxbcShaderTranslator translator(
provider.GetAdapterVendorID(), bindless_resources_used_,
edram_rov_used,
!(edram_rov_used ||
render_target_cache_.gamma_render_target_as_unorm16()),
render_target_cache_.msaa_2x_supported(),
render_target_cache_.draw_resolution_scale_x(),
render_target_cache_.draw_resolution_scale_y(),
provider.GetGraphicsAnalysis() != nullptr);
// If needed and possible, create objects needed for DXIL conversion and
// disassembly on this thread.
IDxbcConverter* dxbc_converter = nullptr;
IDxcUtils* dxc_utils = nullptr;
IDxcCompiler* dxc_compiler = nullptr;
if (cvars::d3d12_dxbc_disasm_dxilconv && dxbc_converter_ && dxc_utils_ &&
dxc_compiler_) {
provider.DxbcConverterCreateInstance(CLSID_DxbcConverter,
IID_PPV_ARGS(&dxbc_converter));
provider.DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&dxc_utils));
provider.DxcCreateInstance(CLSID_DxcCompiler,
IID_PPV_ARGS(&dxc_compiler));
}
for (;;) {
D3D12Shader* shader_to_translate;
for (;;) {
std::unique_lock<std::mutex> lock(shaders_translation_thread_mutex);
if (shaders_to_translate.empty()) {
if (shader_translation_threads_shutdown) {
return;
if (!storage_writer_.InitializeShaderStorage(
cache_root, title_id, pipeline_config,
// Shader load callback.
[&](xenos::ShaderType type, const uint32_t* ucode_dwords,
uint32_t ucode_dword_count, uint64_t ucode_data_hash) {
D3D12Shader* shader = LoadShader(
type, ucode_dwords, ucode_dword_count, ucode_data_hash);
if (shader->ucode_storage_index() == storage_index) {
return true; // Already loaded.
}
shaders_translation_thread_cond.wait(lock);
continue;
}
shader_to_translate = shaders_to_translate.front();
shaders_to_translate.pop_front();
++shader_translation_threads_busy;
break;
}
if (!shader_to_translate->is_ucode_analyzed()) {
shader_to_translate->AnalyzeUcode(ucode_disasm_buffer);
}
// Translate each needed modification on this thread after performing
// modification-independent analysis of the whole shader.
uint64_t ucode_data_hash = shader_to_translate->ucode_data_hash();
for (auto modification_it = shader_translations_needed.lower_bound(
std::make_pair(ucode_data_hash, uint64_t(0)));
modification_it != shader_translations_needed.end() &&
modification_it->first == ucode_data_hash;
++modification_it) {
D3D12Shader::D3D12Translation* translation =
static_cast<D3D12Shader::D3D12Translation*>(
shader_to_translate->GetOrCreateTranslation(
modification_it->second));
// Only try (and delete in case of failure) if it's a new translation.
// If it's a shader previously encountered in the game, translation of
// which has failed, and the shader storage is loaded later, keep it
// this way not to try to translate it again.
if (!translation->is_translated() &&
!TranslateAnalyzedShader(translator, *translation, dxbc_converter,
dxc_utils, dxc_compiler)) {
std::lock_guard<std::mutex> lock(shaders_failed_to_translate_mutex);
shaders_failed_to_translate.push_back(translation);
}
}
{
std::lock_guard<std::mutex> lock(shaders_translation_thread_mutex);
--shader_translation_threads_busy;
}
}
if (dxc_compiler) {
dxc_compiler->Release();
}
if (dxc_utils) {
dxc_utils->Release();
}
if (dxbc_converter) {
dxbc_converter->Release();
}
};
std::vector<std::unique_ptr<xe::threading::Thread>>
shader_translation_threads;
while (true) {
if (!fread(&shader_header, sizeof(shader_header), 1,
shader_storage_file_)) {
break;
}
size_t ucode_byte_count =
shader_header.ucode_dword_count * sizeof(uint32_t);
ucode_dwords.resize(shader_header.ucode_dword_count);
if (shader_header.ucode_dword_count &&
!fread(ucode_dwords.data(), ucode_byte_count, 1,
shader_storage_file_)) {
break;
}
uint64_t ucode_data_hash =
XXH3_64bits(ucode_dwords.data(), ucode_byte_count);
if (shader_header.ucode_data_hash != ucode_data_hash) {
// Validation failed.
break;
}
shader_storage_valid_bytes += sizeof(shader_header) + ucode_byte_count;
D3D12Shader* shader =
LoadShader(shader_header.type, ucode_dwords.data(),
shader_header.ucode_dword_count, ucode_data_hash);
if (shader->ucode_storage_index() == shader_storage_index_) {
// Appeared twice in this file for some reason - skip, otherwise race
// condition will be caused by translating twice in parallel.
continue;
}
// Loaded from the current storage - don't write again.
shader->set_ucode_storage_index(shader_storage_index_);
// Create new threads if the currently existing threads can't keep up
// with file reading, but not more than the number of logical processors
// minus one.
size_t shader_translation_threads_needed;
{
std::lock_guard<std::mutex> lock(shaders_translation_thread_mutex);
shader_translation_threads_needed =
std::min(shader_translation_threads_busy +
shaders_to_translate.size() + size_t(1),
logical_processor_count - size_t(1));
}
while (shader_translation_threads.size() <
shader_translation_threads_needed) {
auto thread = xe::threading::Thread::Create(
{}, shader_translation_thread_function);
assert_not_null(thread);
thread->set_name("Shader Translation");
shader_translation_threads.push_back(std::move(thread));
}
// Request ucode information gathering and translation of all the needed
// shaders.
{
std::lock_guard<std::mutex> lock(shaders_translation_thread_mutex);
shaders_to_translate.push_back(shader);
}
shaders_translation_thread_cond.notify_one();
++shaders_translated;
}
if (!shader_translation_threads.empty()) {
{
std::lock_guard<std::mutex> lock(shaders_translation_thread_mutex);
shader_translation_threads_shutdown = true;
}
shaders_translation_thread_cond.notify_all();
for (auto& shader_translation_thread : shader_translation_threads) {
xe::threading::Wait(shader_translation_thread.get(), false);
}
shader_translation_threads.clear();
for (D3D12Shader::D3D12Translation* translation :
shaders_failed_to_translate) {
D3D12Shader* shader = static_cast<D3D12Shader*>(&translation->shader());
shader->DestroyTranslation(translation->modification());
if (shader->translations().empty()) {
shaders_.erase(shader->ucode_data_hash());
delete shader;
}
}
}
XELOGGPU("Translated {} shaders from the storage in {} milliseconds",
shaders_translated,
(xe::Clock::QueryHostTickCount() -
shader_storage_initialization_start) *
1000 / xe::Clock::QueryHostTickFrequency());
xe::filesystem::TruncateStdioFile(shader_storage_file_,
shader_storage_valid_bytes);
} else {
xe::filesystem::TruncateStdioFile(shader_storage_file_, 0);
shader_storage_file_header.magic = shader_storage_magic;
shader_storage_file_header.version_swapped =
xe::byte_swap(ShaderStoredHeader::kVersion);
fwrite(&shader_storage_file_header, sizeof(shader_storage_file_header), 1,
shader_storage_file_);
shader->set_ucode_storage_index(storage_index);
return true;
},
// Shader translate callback.
[this, edram_rov_used](const std::set<std::pair<uint64_t, uint64_t>>
& translations_needed) {
TranslateShadersForStorage(translations_needed, edram_rov_used);
},
pipeline_stored_descriptions)) {
return;
}
shader_storage_file_flush_needed_ = false;
pipeline_storage_file_flush_needed_ = false;
// Create the pipelines.
if (!pipeline_stored_descriptions.empty()) {
@@ -574,12 +275,16 @@ void PipelineCache::InitializeShaderStorage(
// Launch additional creation threads to use all cores to create
// pipelines faster. Will also be using the main thread, so minus 1.
size_t logical_processor_count = xe::threading::logical_processor_count();
if (!logical_processor_count) {
logical_processor_count = 6;
}
size_t creation_thread_original_count = creation_threads_.size();
size_t creation_thread_needed_count = std::max(
std::min(pipeline_stored_descriptions.size(), logical_processor_count) -
size_t(1),
creation_thread_original_count);
while (creation_threads_.size() < creation_thread_original_count) {
while (creation_threads_.size() < creation_thread_needed_count) {
size_t creation_thread_index = creation_threads_.size();
std::unique_ptr<xe::threading::Thread> creation_thread =
xe::threading::Thread::Create({}, [this, creation_thread_index]() {
@@ -591,6 +296,12 @@ void PipelineCache::InitializeShaderStorage(
}
size_t pipelines_created = 0;
size_t pipelines_already_exist = 0;
size_t pipelines_vs_not_found = 0;
size_t pipelines_vs_translation_missing = 0;
size_t pipelines_ps_not_found = 0;
size_t pipelines_ps_translation_missing = 0;
size_t pipelines_root_sig_failed = 0;
for (const PipelineStoredDescription& pipeline_stored_description :
pipeline_stored_descriptions) {
const PipelineDescription& pipeline_description =
@@ -610,6 +321,7 @@ void PipelineCache::InitializeShaderStorage(
}
}
if (pipeline_found) {
++pipelines_already_exist;
continue;
}
@@ -617,6 +329,9 @@ void PipelineCache::InitializeShaderStorage(
auto vertex_shader_it =
shaders_.find(pipeline_description.vertex_shader_hash);
if (vertex_shader_it == shaders_.end()) {
++pipelines_vs_not_found;
XELOGW("Pipeline cache: VS {:016X} not found in shader storage",
pipeline_description.vertex_shader_hash);
continue;
}
D3D12Shader* vertex_shader = vertex_shader_it->second;
@@ -627,6 +342,12 @@ void PipelineCache::InitializeShaderStorage(
if (!pipeline_runtime_description.vertex_shader ||
!pipeline_runtime_description.vertex_shader->is_translated() ||
!pipeline_runtime_description.vertex_shader->is_valid()) {
++pipelines_vs_translation_missing;
XELOGW(
"Pipeline cache: VS {:016X} mod {:016X} translation "
"missing/invalid",
pipeline_description.vertex_shader_hash,
pipeline_description.vertex_shader_modification);
continue;
}
D3D12Shader* pixel_shader;
@@ -634,6 +355,9 @@ void PipelineCache::InitializeShaderStorage(
auto pixel_shader_it =
shaders_.find(pipeline_description.pixel_shader_hash);
if (pixel_shader_it == shaders_.end()) {
++pipelines_ps_not_found;
XELOGW("Pipeline cache: PS {:016X} not found in shader storage",
pipeline_description.pixel_shader_hash);
continue;
}
pixel_shader = pixel_shader_it->second;
@@ -644,6 +368,12 @@ void PipelineCache::InitializeShaderStorage(
if (!pipeline_runtime_description.pixel_shader ||
!pipeline_runtime_description.pixel_shader->is_translated() ||
!pipeline_runtime_description.pixel_shader->is_valid()) {
++pipelines_ps_translation_missing;
XELOGW(
"Pipeline cache: PS {:016X} mod {:016X} translation "
"missing/invalid",
pipeline_description.pixel_shader_hash,
pipeline_description.pixel_shader_modification);
continue;
}
} else {
@@ -669,6 +399,11 @@ void PipelineCache::InitializeShaderStorage(
pipeline_description.vertex_shader_modification)
.vertex.host_vertex_shader_type));
if (!pipeline_runtime_description.root_signature) {
++pipelines_root_sig_failed;
XELOGW(
"Pipeline cache: Root signature failed for VS {:016X} PS {:016X}",
pipeline_description.vertex_shader_hash,
pipeline_description.pixel_shader_hash);
continue;
}
std::memcpy(&pipeline_runtime_description.description,
@@ -707,29 +442,36 @@ void PipelineCache::InitializeShaderStorage(
}
if (!creation_threads_.empty()) {
CreateQueuedPipelinesOnProcessorThread();
if (creation_threads_.size() > creation_thread_original_count) {
{
std::lock_guard<xe_mutex> lock(creation_request_lock_);
creation_threads_shutdown_from_ = creation_thread_original_count;
// Assuming the queue is empty because of
// CreateQueuedPipelinesOnProcessorThread.
}
creation_request_cond_.notify_all();
while (creation_threads_.size() > creation_thread_original_count) {
xe::threading::Wait(creation_threads_.back().get(), false);
creation_threads_.pop_back();
if (blocking) {
// Blocking mode: help drain the queue on this thread, then wait for
// background threads to finish.
CreateQueuedPipelinesOnProcessorThread();
if (creation_threads_.size() > creation_thread_original_count) {
{
std::lock_guard<xe_mutex> lock(creation_request_lock_);
creation_threads_shutdown_from_ = creation_thread_original_count;
// Assuming the queue is empty because of
// CreateQueuedPipelinesOnProcessorThread.
}
creation_request_cond_.notify_all();
while (creation_threads_.size() > creation_thread_original_count) {
xe::threading::Wait(creation_threads_.back().get(), false);
creation_threads_.pop_back();
}
{
// Cleanup so additional threads can be created later again.
std::lock_guard<xe_mutex> lock(creation_request_lock_);
creation_threads_shutdown_from_ = SIZE_MAX;
}
}
// Wait for any background threads (including original ones) to finish
// creating pipelines they may have popped from the queue. This ensures
// all cached pipelines are fully created before the game starts,
// populating the driver's shader cache.
bool await_creation_completion_event;
{
// Cleanup so additional threads can be created later again.
std::lock_guard<xe_mutex> lock(creation_request_lock_);
creation_threads_shutdown_from_ = SIZE_MAX;
// If the invocation is blocking, all the shader storage
// initialization is expected to be done before proceeding, to avoid
// latency in the command processor after the invocation.
await_creation_completion_event =
blocking && creation_threads_busy_ != 0;
await_creation_completion_event = creation_threads_busy_ != 0;
if (await_creation_completion_event) {
creation_completion_event_->Reset();
creation_completion_set_event_ = true;
@@ -739,87 +481,62 @@ void PipelineCache::InitializeShaderStorage(
creation_request_cond_.notify_one();
xe::threading::Wait(creation_completion_event_.get(), false);
}
} else {
// Non-blocking mode: let background threads handle all pipeline
// creation. Store completion callback to be invoked when done.
std::lock_guard<xe_mutex> lock(creation_request_lock_);
if (creation_queue_.empty() && creation_threads_busy_ == 0) {
// No work pending - callback will be invoked at end of function.
} else {
creation_completion_callback_ = std::move(completion_callback);
completion_callback =
nullptr; // Prevent invocation at end of function
}
}
}
XELOGGPU(
"Created {} graphics pipelines (not including reading the "
"descriptions) from the storage in {} milliseconds",
pipelines_created,
(xe::Clock::QueryHostTickCount() - pipeline_creation_start_) * 1000 /
xe::Clock::QueryHostTickFrequency());
// If any pipeline descriptions were corrupted (or the whole file has excess
// bytes in the end), truncate to the last valid pipeline description.
xe::filesystem::TruncateStdioFile(
pipeline_storage_file_,
uint64_t(sizeof(pipeline_storage_file_header) +
sizeof(PipelineStoredDescription) *
pipeline_stored_descriptions.size()));
} else {
xe::filesystem::TruncateStdioFile(pipeline_storage_file_, 0);
pipeline_storage_file_header.magic = pipeline_storage_magic;
pipeline_storage_file_header.magic_api = pipeline_storage_magic_api;
pipeline_storage_file_header.version_swapped =
pipeline_storage_version_swapped;
fwrite(&pipeline_storage_file_header, sizeof(pipeline_storage_file_header),
1, pipeline_storage_file_);
XELOGI(
"Pipeline cache loaded: {} created, {} already exist, {} total stored",
pipelines_created, pipelines_already_exist,
pipeline_stored_descriptions.size());
if (pipelines_vs_not_found || pipelines_vs_translation_missing ||
pipelines_ps_not_found || pipelines_ps_translation_missing ||
pipelines_root_sig_failed) {
XELOGI(
"Pipeline cache skipped: {} VS not found, {} VS translation missing, "
"{} PS not found, {} PS translation missing, {} root sig failed",
pipelines_vs_not_found, pipelines_vs_translation_missing,
pipelines_ps_not_found, pipelines_ps_translation_missing,
pipelines_root_sig_failed);
}
XELOGI("Pipeline creation took {} milliseconds",
(xe::Clock::QueryHostTickCount() - pipeline_creation_start_) * 1000 /
xe::Clock::QueryHostTickFrequency());
}
shader_storage_cache_root_ = cache_root;
shader_storage_title_id_ = title_id;
// Start the storage writing thread.
storage_write_flush_shaders_ = false;
storage_write_flush_pipelines_ = false;
storage_write_thread_shutdown_ = false;
storage_write_thread_ =
xe::threading::Thread::Create({}, [this]() { StorageWriteThread(); });
assert_not_null(storage_write_thread_);
storage_write_thread_->set_name("D3D12 Storage writer");
// Invoke completion callback if provided (for blocking mode or when no
// background work was needed). For non-blocking mode with background work,
// the callback is stored and invoked by CreationThread when done.
if (completion_callback) {
completion_callback();
}
}
void PipelineCache::ShutdownShaderStorage() {
if (storage_write_thread_) {
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
storage_write_thread_shutdown_ = true;
}
storage_write_request_cond_.notify_all();
xe::threading::Wait(storage_write_thread_.get(), false);
storage_write_thread_.reset();
}
storage_write_shader_queue_.clear();
storage_write_pipeline_queue_.clear();
if (pipeline_storage_file_) {
fclose(pipeline_storage_file_);
pipeline_storage_file_ = nullptr;
pipeline_storage_file_flush_needed_ = false;
}
if (shader_storage_file_) {
fclose(shader_storage_file_);
shader_storage_file_ = nullptr;
shader_storage_file_flush_needed_ = false;
}
shader_storage_cache_root_.clear();
// Shut down the storage writer (closes files, stops write thread).
storage_writer_.ShutdownShaderStorage();
shader_storage_file_flush_needed_ = false;
pipeline_storage_file_flush_needed_ = false;
shader_storage_title_id_ = 0;
}
void PipelineCache::EndSubmission() {
if (shader_storage_file_flush_needed_ ||
pipeline_storage_file_flush_needed_) {
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
if (shader_storage_file_flush_needed_) {
storage_write_flush_shaders_ = true;
}
if (pipeline_storage_file_flush_needed_) {
storage_write_flush_pipelines_ = true;
}
}
storage_write_request_cond_.notify_one();
storage_writer_.RequestFlush(shader_storage_file_flush_needed_,
pipeline_storage_file_flush_needed_);
shader_storage_file_flush_needed_ = false;
pipeline_storage_file_flush_needed_ = false;
}
@@ -1035,16 +752,11 @@ bool PipelineCache::ConfigurePipeline(
XELOGE("Failed to translate the vertex shader!");
return false;
}
if (shader_storage_file_ && vertex_shader->shader().ucode_storage_index() !=
shader_storage_index_) {
vertex_shader->shader().set_ucode_storage_index(shader_storage_index_);
assert_not_null(storage_write_thread_);
if (storage_writer_.is_active() &&
vertex_shader->shader().try_set_ucode_storage_index(
storage_writer_.storage_index())) {
shader_storage_file_flush_needed_ = true;
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
storage_write_shader_queue_.push_back(&vertex_shader->shader());
}
storage_write_request_cond_.notify_all();
storage_writer_.QueueShaderWrite(&vertex_shader->shader());
}
}
if (!use_async && !vertex_shader->is_valid()) {
@@ -1066,17 +778,11 @@ bool PipelineCache::ConfigurePipeline(
XELOGE("Failed to translate the pixel shader!");
return false;
}
if (shader_storage_file_ &&
pixel_shader->shader().ucode_storage_index() !=
shader_storage_index_) {
pixel_shader->shader().set_ucode_storage_index(shader_storage_index_);
assert_not_null(storage_write_thread_);
if (storage_writer_.is_active() &&
pixel_shader->shader().try_set_ucode_storage_index(
storage_writer_.storage_index())) {
shader_storage_file_flush_needed_ = true;
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
storage_write_shader_queue_.push_back(&pixel_shader->shader());
}
storage_write_request_cond_.notify_all();
storage_writer_.QueueShaderWrite(&pixel_shader->shader());
}
}
if (!pixel_shader->is_valid()) {
@@ -1145,19 +851,13 @@ bool PipelineCache::ConfigurePipeline(
std::memory_order_release);
}
if (pipeline_storage_file_) {
assert_not_null(storage_write_thread_);
if (storage_writer_.is_active()) {
pipeline_storage_file_flush_needed_ = true;
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
storage_write_pipeline_queue_.emplace_back();
PipelineStoredDescription& stored_description =
storage_write_pipeline_queue_.back();
stored_description.description_hash = hash;
std::memcpy(&stored_description.description, &description,
sizeof(description));
}
storage_write_request_cond_.notify_all();
PipelineStoredDescription stored_description;
stored_description.description_hash = hash;
std::memcpy(&stored_description.description, &description,
sizeof(description));
storage_writer_.QueuePipelineWrite(stored_description);
}
current_pipeline_ = new_pipeline;
@@ -1363,6 +1063,127 @@ bool PipelineCache::TranslateAnalyzedShader(
return translation.is_valid();
}
void PipelineCache::TranslateShadersForStorage(
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed,
bool edram_rov_used) {
uint64_t translation_start = xe::Clock::QueryHostTickCount();
std::vector<D3D12Shader*> shaders_to_translate_list;
shaders_to_translate_list.reserve(shaders_.size());
for (auto& shader_pair : shaders_) {
D3D12Shader* shader = shader_pair.second;
uint64_t ucode_data_hash = shader->ucode_data_hash();
if (translations_needed.lower_bound(
std::make_pair(ucode_data_hash, uint64_t(0))) !=
translations_needed.upper_bound(
std::make_pair(ucode_data_hash, UINT64_MAX))) {
shaders_to_translate_list.push_back(shader);
}
}
if (shaders_to_translate_list.empty()) {
return;
}
std::atomic<size_t> translation_index{0};
std::mutex shaders_failed_to_translate_mutex;
std::vector<D3D12Shader::D3D12Translation*> shaders_failed_to_translate;
auto translate_function = [&, this]() {
const ui::d3d12::D3D12Provider& provider =
command_processor_.GetD3D12Provider();
StringBuffer ucode_disasm_buffer;
DxbcShaderTranslator translator(
provider.GetAdapterVendorID(), bindless_resources_used_, edram_rov_used,
!(edram_rov_used ||
render_target_cache_.gamma_render_target_as_unorm16()),
render_target_cache_.msaa_2x_supported(),
render_target_cache_.draw_resolution_scale_x(),
render_target_cache_.draw_resolution_scale_y(),
provider.GetGraphicsAnalysis() != nullptr);
// DXIL conversion objects.
IDxbcConverter* dxbc_converter = nullptr;
IDxcUtils* dxc_utils = nullptr;
IDxcCompiler* dxc_compiler = nullptr;
if (cvars::d3d12_dxbc_disasm_dxilconv && dxbc_converter_ && dxc_utils_ &&
dxc_compiler_) {
provider.DxbcConverterCreateInstance(CLSID_DxbcConverter,
IID_PPV_ARGS(&dxbc_converter));
provider.DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&dxc_utils));
provider.DxcCreateInstance(CLSID_DxcCompiler,
IID_PPV_ARGS(&dxc_compiler));
}
while (true) {
size_t index = translation_index.fetch_add(1);
if (index >= shaders_to_translate_list.size()) {
break;
}
D3D12Shader* shader = shaders_to_translate_list[index];
if (!shader->is_ucode_analyzed()) {
shader->AnalyzeUcode(ucode_disasm_buffer);
}
uint64_t ucode_data_hash = shader->ucode_data_hash();
for (auto modification_it = translations_needed.lower_bound(
std::make_pair(ucode_data_hash, uint64_t(0)));
modification_it != translations_needed.end() &&
modification_it->first == ucode_data_hash;
++modification_it) {
D3D12Shader::D3D12Translation* translation =
static_cast<D3D12Shader::D3D12Translation*>(
shader->GetOrCreateTranslation(modification_it->second));
if (!translation->is_translated() &&
!TranslateAnalyzedShader(translator, *translation, dxbc_converter,
dxc_utils, dxc_compiler)) {
std::lock_guard<std::mutex> lock(shaders_failed_to_translate_mutex);
shaders_failed_to_translate.push_back(translation);
}
}
}
if (dxc_compiler) dxc_compiler->Release();
if (dxc_utils) dxc_utils->Release();
if (dxbc_converter) dxbc_converter->Release();
};
size_t logical_processor_count = xe::threading::logical_processor_count();
if (!logical_processor_count) {
logical_processor_count = 6;
}
size_t thread_count = std::min(logical_processor_count - size_t(1),
shaders_to_translate_list.size());
std::vector<std::unique_ptr<xe::threading::Thread>> translation_threads;
for (size_t i = 0; i < thread_count; ++i) {
auto thread = xe::threading::Thread::Create({}, translate_function);
if (thread) {
thread->set_name("Shader Translation");
translation_threads.push_back(std::move(thread));
}
}
// Main thread also participates.
translate_function();
for (auto& thread : translation_threads) {
xe::threading::Wait(thread.get(), false);
}
for (D3D12Shader::D3D12Translation* translation :
shaders_failed_to_translate) {
D3D12Shader* shader = static_cast<D3D12Shader*>(&translation->shader());
shader->DestroyTranslation(translation->modification());
if (shader->translations().empty()) {
shaders_.erase(shader->ucode_data_hash());
delete shader;
}
}
XELOGI("Translated {} shaders in {} ms",
shaders_to_translate_list.size() - shaders_failed_to_translate.size(),
(xe::Clock::QueryHostTickCount() - translation_start) * 1000 /
xe::Clock::QueryHostTickFrequency());
}
bool PipelineCache::GetCurrentStateDescription(
D3D12Shader::D3D12Translation* vertex_shader,
D3D12Shader::D3D12Translation* pixel_shader,
@@ -2901,6 +2722,13 @@ void PipelineCache::EnsurePipelineShadersTranslated(
dxc_utils, dxc_compiler)) {
XELOGE("Failed to translate {} shader {:016X}", shader_type,
shader.ucode_data_hash());
} else {
if (storage_writer_.is_active() &&
shader.try_set_ucode_storage_index(
storage_writer_.storage_index())) {
shader_storage_file_flush_needed_ = true;
storage_writer_.QueueShaderWrite(&shader);
}
}
}
}
@@ -3377,86 +3205,6 @@ ID3D12PipelineState* PipelineCache::CreateD3D12Pipeline(
return state;
}
void PipelineCache::StorageWriteThread() {
ShaderStoredHeader shader_header;
// Don't leak anything in unused bits.
std::memset(&shader_header, 0, sizeof(shader_header));
std::vector<uint32_t> ucode_guest_endian;
ucode_guest_endian.reserve(0xFFFF);
bool flush_shaders = false;
bool flush_pipelines = false;
while (true) {
if (flush_shaders) {
flush_shaders = false;
assert_not_null(shader_storage_file_);
fflush(shader_storage_file_);
}
if (flush_pipelines) {
flush_pipelines = false;
assert_not_null(pipeline_storage_file_);
fflush(pipeline_storage_file_);
}
const Shader* shader = nullptr;
PipelineStoredDescription pipeline_description;
bool write_pipeline = false;
{
std::unique_lock<std::mutex> lock(storage_write_request_lock_);
if (storage_write_thread_shutdown_) {
return;
}
if (!storage_write_shader_queue_.empty()) {
shader = storage_write_shader_queue_.front();
storage_write_shader_queue_.pop_front();
} else if (storage_write_flush_shaders_) {
storage_write_flush_shaders_ = false;
flush_shaders = true;
}
if (!storage_write_pipeline_queue_.empty()) {
std::memcpy(&pipeline_description,
&storage_write_pipeline_queue_.front(),
sizeof(pipeline_description));
storage_write_pipeline_queue_.pop_front();
write_pipeline = true;
} else if (storage_write_flush_pipelines_) {
storage_write_flush_pipelines_ = false;
flush_pipelines = true;
}
if (!shader && !write_pipeline) {
storage_write_request_cond_.wait(lock);
continue;
}
}
if (shader) {
shader_header.ucode_data_hash = shader->ucode_data_hash();
shader_header.ucode_dword_count = shader->ucode_dword_count();
shader_header.type = shader->type();
assert_not_null(shader_storage_file_);
fwrite(&shader_header, sizeof(shader_header), 1, shader_storage_file_);
if (shader_header.ucode_dword_count) {
ucode_guest_endian.resize(shader_header.ucode_dword_count);
// Need to swap because the hash is calculated for the shader with guest
// endianness.
xe::copy_and_swap(ucode_guest_endian.data(), shader->ucode_dwords(),
shader_header.ucode_dword_count);
fwrite(ucode_guest_endian.data(),
shader_header.ucode_dword_count * sizeof(uint32_t), 1,
shader_storage_file_);
}
}
if (write_pipeline) {
assert_not_null(pipeline_storage_file_);
fwrite(&pipeline_description, sizeof(pipeline_description), 1,
pipeline_storage_file_);
}
}
}
void PipelineCache::CreationThread(size_t thread_index) {
// Create thread-local translator to avoid contention with main thread.
// This mirrors what the shader storage loading threads do.
@@ -3494,10 +3242,21 @@ void PipelineCache::CreationThread(size_t thread_index) {
std::unique_lock<xe_mutex> lock(creation_request_lock_);
if (thread_index >= creation_threads_shutdown_from_ ||
creation_queue_.empty()) {
if (creation_completion_set_event_ && creation_threads_busy_ == 0) {
// Last pipeline in the queue created - signal the event if requested.
creation_completion_set_event_ = false;
creation_completion_event_->Set();
if (creation_threads_busy_ == 0) {
// Last pipeline in the queue created.
if (creation_completion_set_event_) {
// Signal the event if requested (blocking mode).
creation_completion_set_event_ = false;
creation_completion_event_->Set();
}
if (creation_completion_callback_) {
// Invoke completion callback (non-blocking mode).
auto callback = std::move(creation_completion_callback_);
creation_completion_callback_ = nullptr;
lock.unlock();
callback();
lock.lock();
}
}
if (thread_index >= creation_threads_shutdown_from_) {
// Cleanup thread-local resources.

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"
@@ -62,8 +64,9 @@ class PipelineCache {
// takes a long time, and if it's not, there will be heavy stuttering for the
// rest of the execution of the guest).
void InitializeShaderStorage(const std::filesystem::path& cache_root,
uint32_t title_id, bool blocking);
void InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback = nullptr);
void ShutdownShaderStorage();
void EndSubmission();
@@ -113,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!
@@ -289,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.
@@ -415,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);
@@ -466,6 +446,9 @@ class PipelineCache {
// Whether setting the event on completion is queued. Protected with
// creation_request_lock_, notify_one creation_request_cond_ when set.
bool creation_completion_set_event_ = false;
// Callback to invoke when all queued pipelines are created (for non-blocking
// initialization). Protected with creation_request_lock_.
std::function<void()> creation_completion_callback_;
// Creation threads with this index or above need to be shut down as soon as
// possible. Protected with creation_request_lock_, notify_all
// creation_request_cond_ when set.

View File

@@ -379,27 +379,38 @@ void GraphicsSystem::ClearCaches() {
}
void GraphicsSystem::InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback) {
if (!cvars::store_shaders) {
if (completion_callback) {
completion_callback();
}
return;
}
if (blocking) {
if (command_processor_->is_paused()) {
// Safe to run on any thread while the command processor is paused, no
// race condition.
command_processor_->InitializeShaderStorage(cache_root, title_id, true);
command_processor_->InitializeShaderStorage(
cache_root, title_id, true, std::move(completion_callback));
} else {
xe::threading::Fence fence;
command_processor_->CallInThread([this, cache_root, title_id, &fence]() {
command_processor_->InitializeShaderStorage(cache_root, title_id, true);
fence.Signal();
});
command_processor_->CallInThread(
[this, cache_root, title_id, &fence,
completion_callback = std::move(completion_callback)]() mutable {
command_processor_->InitializeShaderStorage(
cache_root, title_id, true, std::move(completion_callback));
fence.Signal();
});
fence.Wait();
}
} else {
command_processor_->CallInThread([this, cache_root, title_id]() {
command_processor_->InitializeShaderStorage(cache_root, title_id, false);
});
command_processor_->CallInThread(
[this, cache_root, title_id,
completion_callback = std::move(completion_callback)]() mutable {
command_processor_->InitializeShaderStorage(
cache_root, title_id, false, std::move(completion_callback));
});
}
}

View File

@@ -94,8 +94,9 @@ class GraphicsSystem {
virtual void ClearCaches();
void InitializeShaderStorage(const std::filesystem::path& cache_root,
uint32_t title_id, bool blocking);
void InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback = nullptr);
void RequestFrameTrace();
void BeginTracing();

View File

@@ -11,6 +11,7 @@
#define XENIA_GPU_SHADER_H_
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <set>
@@ -1006,9 +1007,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
@@ -1073,7 +1085,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(

View File

@@ -0,0 +1,562 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_GPU_SHADER_STORAGE_H_
#define XENIA_GPU_SHADER_STORAGE_H_
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <deque>
#include <filesystem>
#include <functional>
#include <mutex>
#include <set>
#include <vector>
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/assert.h"
#include "xenia/base/byte_order.h"
#include "xenia/base/filesystem.h"
#include "xenia/base/logging.h"
#include "xenia/base/platform.h"
#include "xenia/base/threading.h"
#include "xenia/base/xxhash.h"
#include "xenia/gpu/shader.h"
#include "xenia/gpu/xenos.h"
namespace xe {
namespace gpu {
// Shader storage file format (.xsh) - shared between D3D12 and Vulkan backends.
// Stores guest shader microcode (Xbox 360 bytecode) for persistent caching.
// File magic: 'XESH'
constexpr uint32_t kShaderStorageMagic = 0x48534558;
// Header for each stored shader entry.
XEPACKEDSTRUCT(ShaderStoredHeader, {
uint64_t ucode_data_hash;
uint32_t ucode_dword_count : 31;
xenos::ShaderType type : 1;
// Increment when the format changes in an incompatible way.
static constexpr uint32_t kVersion = 0x20201219;
});
// Pipeline storage file format (.xpso) - API-specific but with shared header.
// File magic: 'XEPS'
constexpr uint32_t kPipelineStorageMagic = 0x53504558;
// Storage directory structure:
// <cache_root>/shaders/
// shareable/ - Files shared between different machines/drivers
// {TITLE_ID}.xsh - Shader microcode (shared between D3D12/Vulkan)
// {TITLE_ID}.*.xpso - Pipeline descriptions (API-specific)
// local/ - Files specific to this machine/driver
// {TITLE_ID}.*.bin - Driver-specific pipeline cache
inline std::filesystem::path GetShaderStorageRoot(
const std::filesystem::path& cache_root) {
return cache_root / "shaders";
}
inline std::filesystem::path GetShaderStorageShareableRoot(
const std::filesystem::path& cache_root) {
return GetShaderStorageRoot(cache_root) / "shareable";
}
inline std::filesystem::path GetShaderStorageLocalRoot(
const std::filesystem::path& cache_root) {
return GetShaderStorageRoot(cache_root) / "local";
}
inline std::filesystem::path GetShaderStorageFilePath(
const std::filesystem::path& cache_root, uint32_t title_id) {
return GetShaderStorageShareableRoot(cache_root) /
fmt::format("{:08X}.xsh", title_id);
}
// Ensures shader storage directories exist. Returns true on success.
inline bool EnsureShaderStorageDirectoriesExist(
const std::filesystem::path& cache_root) {
std::error_code error_code;
auto shareable_root = GetShaderStorageShareableRoot(cache_root);
if (!std::filesystem::exists(shareable_root)) {
if (!std::filesystem::create_directories(shareable_root, error_code)) {
XELOGE(
"Failed to create shader storage directory, persistent shader "
"storage will be disabled: {}",
xe::path_to_utf8(shareable_root));
return false;
}
}
return true;
}
// File header for shader storage (.xsh).
XEPACKEDSTRUCT(ShaderStorageFileHeader, {
uint32_t magic;
uint32_t version_swapped;
});
// Validates the shader storage file header. Returns true if valid.
inline bool ValidateShaderStorageHeader(FILE* file,
ShaderStorageFileHeader& header_out) {
if (!fread(&header_out, sizeof(header_out), 1, file)) {
return false;
}
return header_out.magic == kShaderStorageMagic &&
xe::byte_swap(header_out.version_swapped) ==
ShaderStoredHeader::kVersion;
}
// Writes a new shader storage file header.
inline bool WriteShaderStorageHeader(FILE* file) {
ShaderStorageFileHeader header;
header.magic = kShaderStorageMagic;
header.version_swapped = xe::byte_swap(ShaderStoredHeader::kVersion);
return fwrite(&header, sizeof(header), 1, file) == 1;
}
// Reads shader entries from storage file and calls the callback for each.
// Returns the number of valid bytes read (for truncation on corruption).
// The callback signature is: bool(xenos::ShaderType type,
// const uint32_t* ucode_dwords,
// uint32_t ucode_dword_count,
// uint64_t ucode_data_hash)
// Callback should return true to continue reading, false to stop.
template <typename Callback>
inline uint64_t ReadShaderEntries(FILE* file, Callback&& callback) {
uint64_t valid_bytes = sizeof(ShaderStorageFileHeader);
ShaderStoredHeader shader_header;
std::vector<uint32_t> ucode_dwords;
ucode_dwords.reserve(0xFFFF);
while (true) {
if (!fread(&shader_header, sizeof(shader_header), 1, file)) {
break;
}
size_t ucode_byte_count =
shader_header.ucode_dword_count * sizeof(uint32_t);
ucode_dwords.resize(shader_header.ucode_dword_count);
if (shader_header.ucode_dword_count &&
!fread(ucode_dwords.data(), ucode_byte_count, 1, file)) {
break;
}
uint64_t ucode_data_hash =
XXH3_64bits(ucode_dwords.data(), ucode_byte_count);
if (shader_header.ucode_data_hash != ucode_data_hash) {
// Validation failed - corrupted entry.
break;
}
valid_bytes += sizeof(shader_header) + ucode_byte_count;
if (!callback(shader_header.type, ucode_dwords.data(),
shader_header.ucode_dword_count, ucode_data_hash)) {
break;
}
}
return valid_bytes;
}
// File header for pipeline storage (.xpso).
XEPACKEDSTRUCT(PipelineStorageFileHeader, {
uint32_t magic;
uint32_t magic_api;
uint32_t version_swapped;
});
// Template class for shader and pipeline storage management.
// TPipelineStoredDescription is the API-specific pipeline description type.
template <typename TPipelineStoredDescription>
class ShaderStorageWriter {
public:
// Configuration for pipeline storage files.
struct PipelineStorageConfig {
std::string file_suffix; // e.g., ".fsi.vk.xpso" or ".rov.d3d12.xpso"
uint32_t api_magic; // e.g., 'VKPS' or 'DXRO'
uint32_t version; // Pipeline description version
};
// Callback for loading a shader from ucode data.
// Return true to continue reading, false to stop.
using ShaderLoadCallback =
std::function<bool(xenos::ShaderType type, const uint32_t* ucode_dwords,
uint32_t ucode_dword_count, uint64_t ucode_data_hash)>;
// Callback for translating shaders. Called with the set of
// (ucode_hash, modification) pairs that need translation.
// Implementation should handle parallel translation internally.
using TranslateCallback = std::function<void(
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed)>;
ShaderStorageWriter() = default;
~ShaderStorageWriter() { ShutdownShaderStorage(); }
// Non-copyable.
ShaderStorageWriter(const ShaderStorageWriter&) = delete;
ShaderStorageWriter& operator=(const ShaderStorageWriter&) = delete;
// Opens files, loads shaders, triggers translation, starts write thread.
// Caller must call ShutdownShaderStorage() first if re-initializing.
bool InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id,
const PipelineStorageConfig& pipeline_config,
ShaderLoadCallback load_shader, TranslateCallback translate_shaders,
std::vector<TPipelineStoredDescription>& pipeline_descriptions_out) {
cache_root_ = cache_root;
title_id_ = title_id;
if (!EnsureShaderStorageDirectoriesExist(cache_root)) {
return false;
}
auto shader_storage_shareable_root =
GetShaderStorageShareableRoot(cache_root);
++storage_index_;
// Open pipeline storage file.
auto pipeline_storage_file_path =
shader_storage_shareable_root /
fmt::format("{:08X}{}", title_id, pipeline_config.file_suffix);
pipeline_storage_file_ =
xe::filesystem::OpenFile(pipeline_storage_file_path, "a+b");
if (!pipeline_storage_file_) {
XELOGE(
"Failed to open the pipeline storage file for writing, persistent "
"shader storage will be disabled: {}",
xe::path_to_utf8(pipeline_storage_file_path));
return false;
}
// Read pipeline descriptions.
const uint32_t pipeline_storage_version_swapped =
xe::byte_swap(pipeline_config.version);
PipelineStorageFileHeader pipeline_header;
if (fread(&pipeline_header, sizeof(pipeline_header), 1,
pipeline_storage_file_) &&
pipeline_header.magic == kPipelineStorageMagic &&
pipeline_header.magic_api == pipeline_config.api_magic &&
pipeline_header.version_swapped == pipeline_storage_version_swapped) {
// Valid header, read pipeline descriptions.
xe::filesystem::Seek(pipeline_storage_file_, 0, SEEK_END);
int64_t pipeline_storage_told_end =
xe::filesystem::Tell(pipeline_storage_file_);
size_t pipeline_storage_told_count =
size_t(pipeline_storage_told_end >= int64_t(sizeof(pipeline_header))
? (uint64_t(pipeline_storage_told_end) -
sizeof(pipeline_header)) /
sizeof(TPipelineStoredDescription)
: 0);
if (pipeline_storage_told_count) {
xe::filesystem::Seek(pipeline_storage_file_,
int64_t(sizeof(pipeline_header)), SEEK_SET);
pipeline_descriptions_out.resize(pipeline_storage_told_count);
pipeline_descriptions_out.resize(
fread(pipeline_descriptions_out.data(),
sizeof(TPipelineStoredDescription),
pipeline_storage_told_count, pipeline_storage_file_));
// Validate each description's hash.
size_t valid_count = 0;
for (size_t i = 0; i < pipeline_descriptions_out.size(); ++i) {
const TPipelineStoredDescription& desc = pipeline_descriptions_out[i];
if (XXH3_64bits(&desc.description, sizeof(desc.description)) !=
desc.description_hash) {
break;
}
++valid_count;
}
pipeline_descriptions_out.resize(valid_count);
}
// Truncate to last valid description.
xe::filesystem::TruncateStdioFile(
pipeline_storage_file_,
uint64_t(sizeof(pipeline_header) +
sizeof(TPipelineStoredDescription) *
pipeline_descriptions_out.size()));
} else {
// Write new header.
xe::filesystem::TruncateStdioFile(pipeline_storage_file_, 0);
pipeline_header.magic = kPipelineStorageMagic;
pipeline_header.magic_api = pipeline_config.api_magic;
pipeline_header.version_swapped = pipeline_storage_version_swapped;
if (fwrite(&pipeline_header, sizeof(pipeline_header), 1,
pipeline_storage_file_) != 1) {
XELOGE("Failed to write pipeline storage header");
fclose(pipeline_storage_file_);
pipeline_storage_file_ = nullptr;
return false;
}
}
// Open shader storage file.
auto shader_storage_file_path =
GetShaderStorageFilePath(cache_root, title_id);
shader_storage_file_ =
xe::filesystem::OpenFile(shader_storage_file_path, "a+b");
if (!shader_storage_file_) {
XELOGE(
"Failed to open the guest shader storage file for writing, "
"persistent shader storage will be disabled: {}",
xe::path_to_utf8(shader_storage_file_path));
fclose(pipeline_storage_file_);
pipeline_storage_file_ = nullptr;
return false;
}
// Load shaders from storage.
size_t shaders_loaded = 0;
ShaderStorageFileHeader shader_header;
if (ValidateShaderStorageHeader(shader_storage_file_, shader_header)) {
uint64_t shader_storage_valid_bytes = ReadShaderEntries(
shader_storage_file_,
[&](xenos::ShaderType type, const uint32_t* ucode_dwords,
uint32_t ucode_dword_count, uint64_t ucode_data_hash) {
if (load_shader(type, ucode_dwords, ucode_dword_count,
ucode_data_hash)) {
++shaders_loaded;
return true;
}
return false;
});
xe::filesystem::TruncateStdioFile(shader_storage_file_,
shader_storage_valid_bytes);
} else {
// Write new header.
xe::filesystem::TruncateStdioFile(shader_storage_file_, 0);
if (!WriteShaderStorageHeader(shader_storage_file_)) {
XELOGE("Failed to write shader storage header");
fclose(shader_storage_file_);
shader_storage_file_ = nullptr;
fclose(pipeline_storage_file_);
pipeline_storage_file_ = nullptr;
return false;
}
}
XELOGI("Loaded {} shaders from storage", shaders_loaded);
// Collect shader translations needed from pipeline descriptions.
std::set<std::pair<uint64_t, uint64_t>> translations_needed;
for (const TPipelineStoredDescription& desc : pipeline_descriptions_out) {
translations_needed.emplace(desc.description.vertex_shader_hash,
desc.description.vertex_shader_modification);
if (desc.description.pixel_shader_hash) {
translations_needed.emplace(desc.description.pixel_shader_hash,
desc.description.pixel_shader_modification);
}
}
XELOGI("Loaded {} pipeline descriptions, {} shader translations needed",
pipeline_descriptions_out.size(), translations_needed.size());
// Translate shaders (callback handles parallel translation).
if (!translations_needed.empty() && translate_shaders) {
translate_shaders(translations_needed);
}
// Start the write thread.
storage_write_thread_shutdown_ = false;
storage_write_thread_ =
xe::threading::Thread::Create({}, [this]() { WriteThread(); });
storage_write_thread_->set_name("Shader Storage Writer");
return true;
}
// Shutdown storage: stops write thread, closes files.
void ShutdownShaderStorage() {
if (storage_write_thread_) {
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
storage_write_thread_shutdown_ = true;
}
storage_write_request_cond_.notify_one();
xe::threading::Wait(storage_write_thread_.get(), false);
storage_write_thread_.reset();
}
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
storage_write_shader_queue_.clear();
storage_write_pipeline_queue_.clear();
storage_write_flush_shaders_ = false;
storage_write_flush_pipelines_ = false;
}
if (pipeline_storage_file_) {
fclose(pipeline_storage_file_);
pipeline_storage_file_ = nullptr;
}
if (shader_storage_file_) {
fclose(shader_storage_file_);
shader_storage_file_ = nullptr;
}
cache_root_.clear();
title_id_ = 0;
}
uint32_t storage_index() const { return storage_index_; }
FILE* shader_storage_file() const { return shader_storage_file_; }
FILE* pipeline_storage_file() const { return pipeline_storage_file_; }
bool is_active() const { return shader_storage_file_ != nullptr; }
const std::filesystem::path& cache_root() const { return cache_root_; }
uint32_t title_id() const { return title_id_; }
void QueueShaderWrite(const Shader* shader) {
if (!shader_storage_file_) {
return;
}
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
storage_write_shader_queue_.push_back(shader);
}
storage_write_request_cond_.notify_one();
}
void QueuePipelineWrite(const TPipelineStoredDescription& description) {
if (!pipeline_storage_file_) {
return;
}
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
storage_write_pipeline_queue_.push_back(description);
}
storage_write_request_cond_.notify_one();
}
void RequestFlush(bool flush_shaders, bool flush_pipelines) {
bool need_notify = false;
{
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
if (flush_shaders) {
storage_write_flush_shaders_ = true;
need_notify = true;
}
if (flush_pipelines) {
storage_write_flush_pipelines_ = true;
need_notify = true;
}
}
if (need_notify) {
storage_write_request_cond_.notify_one();
}
}
private:
void WriteThread() {
ShaderStoredHeader shader_header;
std::memset(&shader_header, 0, sizeof(shader_header));
std::vector<uint32_t> ucode_guest_endian;
ucode_guest_endian.reserve(0xFFFF);
bool flush_shaders = false;
bool flush_pipelines = false;
while (true) {
if (flush_shaders) {
flush_shaders = false;
assert_not_null(shader_storage_file_);
fflush(shader_storage_file_);
}
if (flush_pipelines) {
flush_pipelines = false;
assert_not_null(pipeline_storage_file_);
fflush(pipeline_storage_file_);
}
const Shader* shader = nullptr;
TPipelineStoredDescription pipeline_description;
bool write_pipeline = false;
{
std::unique_lock<std::mutex> lock(storage_write_request_lock_);
if (storage_write_thread_shutdown_) {
return;
}
if (!storage_write_shader_queue_.empty()) {
shader = storage_write_shader_queue_.front();
storage_write_shader_queue_.pop_front();
} else if (storage_write_flush_shaders_) {
storage_write_flush_shaders_ = false;
flush_shaders = true;
}
if (!storage_write_pipeline_queue_.empty()) {
std::memcpy(&pipeline_description,
&storage_write_pipeline_queue_.front(),
sizeof(pipeline_description));
storage_write_pipeline_queue_.pop_front();
write_pipeline = true;
} else if (storage_write_flush_pipelines_) {
storage_write_flush_pipelines_ = false;
flush_pipelines = true;
}
if (!shader && !write_pipeline && !flush_shaders && !flush_pipelines) {
storage_write_request_cond_.wait(lock);
continue;
}
}
if (shader) {
shader_header.ucode_data_hash = shader->ucode_data_hash();
shader_header.ucode_dword_count = shader->ucode_dword_count();
shader_header.type = shader->type();
assert_not_null(shader_storage_file_);
if (fwrite(&shader_header, sizeof(shader_header), 1,
shader_storage_file_) != 1) {
XELOGE("Failed to write shader header to storage");
} else if (shader_header.ucode_dword_count) {
ucode_guest_endian.resize(shader_header.ucode_dword_count);
// Need to swap because the hash is calculated for the shader with
// guest endianness.
xe::copy_and_swap(ucode_guest_endian.data(), shader->ucode_dwords(),
shader_header.ucode_dword_count);
if (fwrite(ucode_guest_endian.data(),
shader_header.ucode_dword_count * sizeof(uint32_t), 1,
shader_storage_file_) != 1) {
XELOGE("Failed to write shader ucode to storage");
}
}
}
if (write_pipeline) {
assert_not_null(pipeline_storage_file_);
if (fwrite(&pipeline_description, sizeof(pipeline_description), 1,
pipeline_storage_file_) != 1) {
XELOGE("Failed to write pipeline description to storage");
}
}
}
}
std::filesystem::path cache_root_;
uint32_t title_id_ = 0;
uint32_t storage_index_ = 0;
FILE* shader_storage_file_ = nullptr;
FILE* pipeline_storage_file_ = nullptr;
std::unique_ptr<xe::threading::Thread> storage_write_thread_;
std::mutex storage_write_request_lock_;
std::condition_variable storage_write_request_cond_;
std::deque<const Shader*> storage_write_shader_queue_;
std::deque<TPipelineStoredDescription> storage_write_pipeline_queue_;
bool storage_write_flush_shaders_ = false;
bool storage_write_flush_pipelines_ = false;
bool storage_write_thread_shutdown_ = false;
};
} // namespace gpu
} // namespace xe
#endif // XENIA_GPU_SHADER_STORAGE_H_

View File

@@ -108,6 +108,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) {}
std::string VulkanCommandProcessor::GetWindowTitleText() const {

View File

@@ -145,6 +145,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;
ui::vulkan::VulkanDevice* GetVulkanDevice() const {

View File

@@ -11,9 +11,12 @@
#include <cstdint>
#include <cstring>
#include <set>
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/assert.h"
#include "xenia/base/byte_order.h"
#include "xenia/base/filesystem.h"
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/base/profiling.h"
@@ -218,6 +221,9 @@ bool VulkanPipelineCache::Initialize() {
}
void VulkanPipelineCache::Shutdown() {
// Shut down shader storage first.
ShutdownShaderStorage();
// Shut down all threads, before destroying the pipelines since they may be
// creating them.
if (!creation_threads_.empty()) {
@@ -231,6 +237,13 @@ void VulkanPipelineCache::Shutdown() {
}
creation_threads_.clear();
}
// Clear any pending completion callback (may capture 'this') and reset
// startup state.
{
std::lock_guard<std::mutex> lock(creation_request_lock_);
creation_completion_callback_ = nullptr;
}
startup_loading_ = false;
creation_completion_event_.reset();
const ui::vulkan::VulkanDevice* const vulkan_device =
@@ -579,6 +592,28 @@ bool VulkanPipelineCache::ConfigurePipeline(
auto& pipeline_pair =
*pipelines_.emplace(description, Pipeline(pipeline_layout)).first;
if (storage_writer_.is_active()) {
VulkanShader& vs = static_cast<VulkanShader&>(vertex_shader->shader());
if (vs.try_set_ucode_storage_index(storage_writer_.storage_index())) {
shader_storage_file_flush_needed_ = true;
storage_writer_.QueueShaderWrite(&vs);
}
if (pixel_shader) {
VulkanShader& ps = static_cast<VulkanShader&>(pixel_shader->shader());
if (ps.try_set_ucode_storage_index(storage_writer_.storage_index())) {
shader_storage_file_flush_needed_ = true;
storage_writer_.QueueShaderWrite(&ps);
}
}
pipeline_storage_file_flush_needed_ = true;
PipelineStoredDescription stored_description;
stored_description.description_hash = description.GetHash();
std::memcpy(&stored_description.description, &description,
sizeof(description));
storage_writer_.QueuePipelineWrite(stored_description);
}
// Get tessellation shaders if needed.
VkShaderModule tessellation_vertex_shader = VK_NULL_HANDLE;
VkShaderModule tessellation_control_shader = VK_NULL_HANDLE;
@@ -670,7 +705,8 @@ bool VulkanPipelineCache::ConfigurePipeline(
creation_arguments.pixel_shader = pixel_shader;
creation_arguments.geometry_shader = geometry_shader;
creation_arguments.tessellation_vertex_shader = tessellation_vertex_shader;
creation_arguments.tessellation_control_shader = tessellation_control_shader;
creation_arguments.tessellation_control_shader =
tessellation_control_shader;
creation_arguments.render_pass = render_pass;
if (!EnsurePipelineCreated(creation_arguments)) {
return false;
@@ -683,31 +719,42 @@ bool VulkanPipelineCache::ConfigurePipeline(
}
void VulkanPipelineCache::EndSubmission() {
if (shader_storage_file_flush_needed_ ||
pipeline_storage_file_flush_needed_) {
storage_writer_.RequestFlush(shader_storage_file_flush_needed_,
pipeline_storage_file_flush_needed_);
shader_storage_file_flush_needed_ = false;
pipeline_storage_file_flush_needed_ = false;
}
if (creation_threads_.empty()) {
// Process deferred destructions when GPU is idle
ProcessDeferredDestructions();
return;
}
// Await creation of all queued pipelines.
bool await_creation_completion_event;
{
std::lock_guard<std::mutex> lock(creation_request_lock_);
// Assuming the creation queue is already empty (because the processor
// thread also worked on creating the leftover pipelines), so only check
// if there are threads with pipelines currently being created.
await_creation_completion_event =
!creation_queue_.empty() || creation_threads_busy_ != 0;
if (startup_loading_) {
// Non-blocking: let background threads work asynchronously.
creation_request_cond_.notify_one();
} else {
// Blocking: wait for all queued pipelines.
bool await_creation_completion_event;
{
std::lock_guard<std::mutex> lock(creation_request_lock_);
await_creation_completion_event =
!creation_queue_.empty() || creation_threads_busy_ != 0;
if (await_creation_completion_event) {
creation_completion_event_->Reset();
creation_completion_set_event_.store(true, std::memory_order_release);
}
}
if (await_creation_completion_event) {
creation_completion_event_->Reset();
creation_completion_set_event_.store(true, std::memory_order_release);
creation_request_cond_.notify_one();
xe::threading::Wait(creation_completion_event_.get(), false);
}
}
if (await_creation_completion_event) {
creation_request_cond_.notify_one();
xe::threading::Wait(creation_completion_event_.get(), false);
}
// Process deferred destructions after waiting for pipelines
// Process deferred destructions
ProcessDeferredDestructions();
}
@@ -753,12 +800,24 @@ void VulkanPipelineCache::CreationThread() {
}
{
std::lock_guard<std::mutex> lock(creation_request_lock_);
std::unique_lock<std::mutex> lock(creation_request_lock_);
--creation_threads_busy_;
if (creation_completion_set_event_.load(std::memory_order_acquire) &&
creation_threads_busy_ == 0 && creation_queue_.empty()) {
creation_completion_set_event_.store(false, std::memory_order_release);
creation_completion_event_->Set();
if (creation_threads_busy_ == 0 && creation_queue_.empty()) {
// All pipelines created.
if (creation_completion_set_event_.load(std::memory_order_acquire)) {
// Signal the event (blocking mode).
creation_completion_set_event_.store(false,
std::memory_order_release);
creation_completion_event_->Set();
}
if (creation_completion_callback_) {
// Invoke completion callback (non-blocking mode).
auto callback = std::move(creation_completion_callback_);
creation_completion_callback_ = nullptr;
lock.unlock();
callback();
lock.lock();
}
}
}
}
@@ -843,6 +902,104 @@ bool VulkanPipelineCache::TranslateAnalyzedShader(
return true;
}
void VulkanPipelineCache::TranslateShadersForStorage(
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed,
bool edram_fsi_used) {
uint64_t translation_start = xe::Clock::QueryHostTickCount();
std::vector<std::pair<VulkanShader*, uint64_t>> translations_to_do;
translations_to_do.reserve(translations_needed.size());
for (const auto& needed : translations_needed) {
auto shader_it = shaders_.find(needed.first);
if (shader_it == shaders_.end()) {
continue;
}
VulkanShader* shader = shader_it->second;
VulkanShader::VulkanTranslation* translation =
static_cast<VulkanShader::VulkanTranslation*>(
shader->GetOrCreateTranslation(needed.second));
if (translation && !translation->is_translated()) {
translations_to_do.emplace_back(shader, needed.second);
}
}
if (translations_to_do.empty()) {
return;
}
std::atomic<size_t> translation_index{0};
std::atomic<size_t> translations_completed{0};
const ui::vulkan::VulkanDevice* vulkan_device =
command_processor_.GetVulkanDevice();
bool msaa_2x_attachments =
render_target_cache_.msaa_2x_attachments_supported();
bool msaa_2x_no_attachments =
render_target_cache_.msaa_2x_no_attachments_supported();
uint32_t draw_res_x = render_target_cache_.draw_resolution_scale_x();
uint32_t draw_res_y = render_target_cache_.draw_resolution_scale_y();
auto translate_function = [this, &translations_to_do, &translation_index,
&translations_completed, vulkan_device,
msaa_2x_attachments, msaa_2x_no_attachments,
edram_fsi_used, draw_res_x, draw_res_y]() {
// Each thread needs its own translator.
SpirvShaderTranslator translator(
SpirvShaderTranslator::Features(vulkan_device), msaa_2x_attachments,
msaa_2x_no_attachments, edram_fsi_used, draw_res_x, draw_res_y);
while (true) {
size_t index = translation_index.fetch_add(1);
if (index >= translations_to_do.size()) {
break;
}
VulkanShader* shader = translations_to_do[index].first;
uint64_t modification = translations_to_do[index].second;
VulkanShader::VulkanTranslation* translation =
static_cast<VulkanShader::VulkanTranslation*>(
shader->GetTranslation(modification));
if (translation && !translation->is_translated()) {
if (TranslateAnalyzedShader(translator, *translation)) {
translations_completed.fetch_add(1);
}
}
}
};
size_t thread_count = 0;
if (cvars::vulkan_pipeline_creation_threads != 0) {
uint32_t logical_processor_count =
std::max(uint32_t(1), xe::threading::logical_processor_count());
if (cvars::vulkan_pipeline_creation_threads < 0) {
thread_count = std::max(logical_processor_count * 3 / 4, uint32_t(1));
} else {
thread_count = std::min(uint32_t(cvars::vulkan_pipeline_creation_threads),
logical_processor_count);
}
thread_count = std::min(thread_count, translations_to_do.size());
}
std::vector<std::unique_ptr<xe::threading::Thread>> translation_threads;
for (size_t i = 0; i < thread_count; ++i) {
auto thread = xe::threading::Thread::Create({}, translate_function);
if (thread) {
thread->set_name("Shader Translation");
translation_threads.push_back(std::move(thread));
}
}
// Main thread also participates.
translate_function();
for (auto& thread : translation_threads) {
xe::threading::Wait(thread.get(), false);
}
XELOGI("Translated {} shaders in {} ms", translations_completed.load(),
(xe::Clock::QueryHostTickCount() - translation_start) * 1000 /
xe::Clock::QueryHostTickFrequency());
}
void VulkanPipelineCache::WritePipelineRenderTargetDescription(
reg::RB_BLENDCONTROL blend_control, uint32_t write_mask,
PipelineRenderTarget& render_target_out) const {
@@ -2883,6 +3040,407 @@ void VulkanPipelineCache::ProcessDeferredDestructions() {
}
}
void VulkanPipelineCache::InitializeShaderStorage(
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
std::function<void()> completion_callback) {
ShutdownShaderStorage();
shader_storage_title_id_ = title_id;
if (!blocking) {
startup_loading_ = true;
if (completion_callback) {
completion_callback = [this, orig = std::move(completion_callback)]() {
startup_loading_ = false;
orig();
};
} else {
completion_callback = [this]() { startup_loading_ = false; };
}
}
bool edram_fsi_used = render_target_cache_.GetPath() ==
RenderTargetCache::Path::kPixelShaderInterlock;
ShaderStorageWriter<PipelineStoredDescription>::PipelineStorageConfig
pipeline_config;
pipeline_config.file_suffix =
fmt::format(".{}.vk.xpso", edram_fsi_used ? "fsi" : "fbo");
pipeline_config.api_magic = kPipelineStorageAPIMagicVulkan;
pipeline_config.version =
std::max(PipelineDescription::kVersion,
SpirvShaderTranslator::Modification::kVersion);
uint32_t storage_index = storage_writer_.storage_index() + 1;
std::vector<PipelineStoredDescription> pipeline_stored_descriptions;
if (!storage_writer_.InitializeShaderStorage(
cache_root, title_id, pipeline_config,
// Shader load callback.
[&](xenos::ShaderType type, const uint32_t* ucode_dwords,
uint32_t ucode_dword_count, uint64_t ucode_data_hash) {
VulkanShader* shader =
LoadShader(type, ucode_dwords, ucode_dword_count);
if (!shader || shader->ucode_storage_index() == storage_index) {
return true; // Continue reading.
}
shader->set_ucode_storage_index(storage_index);
if (!shader->is_ucode_analyzed()) {
shader->AnalyzeUcode(ucode_disasm_buffer_);
}
return true;
},
// Shader translate callback - handles parallel translation.
[this, edram_fsi_used](const std::set<std::pair<uint64_t, uint64_t>>
& translations_needed) {
TranslateShadersForStorage(translations_needed, edram_fsi_used);
},
pipeline_stored_descriptions)) {
if (completion_callback) {
completion_callback();
}
return;
}
shader_storage_file_flush_needed_ = false;
pipeline_storage_file_flush_needed_ = false;
// Load VkPipelineCache from disk if available.
auto shader_storage_local_root = GetShaderStorageLocalRoot(cache_root);
if (!std::filesystem::exists(shader_storage_local_root)) {
std::error_code ec;
std::filesystem::create_directories(shader_storage_local_root, ec);
}
vk_pipeline_cache_path_ =
shader_storage_local_root /
fmt::format("{:08X}.vk.bin", shader_storage_title_id_);
const ui::vulkan::VulkanDevice* const vulkan_device =
command_processor_.GetVulkanDevice();
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device();
// Try to load existing VkPipelineCache data.
std::vector<uint8_t> pipeline_cache_data;
if (FILE* cache_file =
xe::filesystem::OpenFile(vk_pipeline_cache_path_, "rb")) {
xe::filesystem::Seek(cache_file, 0, SEEK_END);
int64_t cache_size = xe::filesystem::Tell(cache_file);
if (cache_size > 0) {
pipeline_cache_data.resize(size_t(cache_size));
xe::filesystem::Seek(cache_file, 0, SEEK_SET);
pipeline_cache_data.resize(fread(pipeline_cache_data.data(), 1,
pipeline_cache_data.size(), cache_file));
}
fclose(cache_file);
XELOGI("Loaded {} bytes of VkPipelineCache data",
pipeline_cache_data.size());
}
// Recreate the VkPipelineCache with the loaded data.
if (vk_pipeline_cache_ != VK_NULL_HANDLE) {
dfn.vkDestroyPipelineCache(device, vk_pipeline_cache_, nullptr);
vk_pipeline_cache_ = VK_NULL_HANDLE;
}
VkPipelineCacheCreateInfo pipeline_cache_create_info = {};
pipeline_cache_create_info.sType =
VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
pipeline_cache_create_info.initialDataSize = pipeline_cache_data.size();
pipeline_cache_create_info.pInitialData =
pipeline_cache_data.empty() ? nullptr : pipeline_cache_data.data();
if (dfn.vkCreatePipelineCache(device, &pipeline_cache_create_info, nullptr,
&vk_pipeline_cache_) != VK_SUCCESS) {
XELOGW(
"VulkanPipelineCache: Failed to create pipeline cache with "
"initial data, creating empty cache");
vk_pipeline_cache_ = VK_NULL_HANDLE;
pipeline_cache_create_info.initialDataSize = 0;
pipeline_cache_create_info.pInitialData = nullptr;
dfn.vkCreatePipelineCache(device, &pipeline_cache_create_info, nullptr,
&vk_pipeline_cache_);
}
// Create pipelines from stored descriptions.
if (!pipeline_stored_descriptions.empty()) {
uint64_t pipeline_creation_start = xe::Clock::QueryHostTickCount();
size_t pipelines_created = 0;
size_t pipelines_already_exist = 0;
size_t pipelines_vs_not_found = 0;
size_t pipelines_vs_translation_missing = 0;
size_t pipelines_ps_not_found = 0;
size_t pipelines_ps_translation_missing = 0;
size_t pipelines_render_pass_failed = 0;
size_t pipelines_layout_failed = 0;
size_t pipelines_requirements_not_met = 0;
for (const PipelineStoredDescription& pipeline_stored_description :
pipeline_stored_descriptions) {
const PipelineDescription& pipeline_description =
pipeline_stored_description.description;
// Skip pipelines not supported by this device.
if (!ArePipelineRequirementsMet(pipeline_description)) {
++pipelines_requirements_not_met;
continue;
}
// Skip already known pipelines.
auto it = pipelines_.find(pipeline_description);
if (it != pipelines_.end()) {
++pipelines_already_exist;
continue;
}
// Look up vertex shader.
auto vertex_shader_it =
shaders_.find(pipeline_description.vertex_shader_hash);
if (vertex_shader_it == shaders_.end()) {
++pipelines_vs_not_found;
continue;
}
VulkanShader* vertex_shader = vertex_shader_it->second;
VulkanShader::VulkanTranslation* vertex_translation =
static_cast<VulkanShader::VulkanTranslation*>(
vertex_shader->GetTranslation(
pipeline_description.vertex_shader_modification));
if (!vertex_translation || !vertex_translation->is_translated() ||
!vertex_translation->is_valid()) {
++pipelines_vs_translation_missing;
continue;
}
// Look up pixel shader if present.
VulkanShader* pixel_shader = nullptr;
VulkanShader::VulkanTranslation* pixel_translation = nullptr;
if (pipeline_description.pixel_shader_hash) {
auto pixel_shader_it =
shaders_.find(pipeline_description.pixel_shader_hash);
if (pixel_shader_it == shaders_.end()) {
++pipelines_ps_not_found;
continue;
}
pixel_shader = pixel_shader_it->second;
pixel_translation = static_cast<VulkanShader::VulkanTranslation*>(
pixel_shader->GetTranslation(
pipeline_description.pixel_shader_modification));
if (!pixel_translation || !pixel_translation->is_translated() ||
!pixel_translation->is_valid()) {
++pipelines_ps_translation_missing;
continue;
}
}
// Get render pass.
VkRenderPass render_pass =
render_target_cache_.GetPath() ==
RenderTargetCache::Path::kPixelShaderInterlock
? render_target_cache_.GetFragmentShaderInterlockRenderPass()
: render_target_cache_.GetHostRenderTargetsRenderPass(
pipeline_description.render_pass_key);
if (render_pass == VK_NULL_HANDLE) {
++pipelines_render_pass_failed;
continue;
}
// Get pipeline layout.
const PipelineLayoutProvider* pipeline_layout =
command_processor_.GetPipelineLayout(
pixel_shader
? pixel_shader->GetTextureBindingsAfterTranslation().size()
: 0,
pixel_shader
? pixel_shader->GetSamplerBindingsAfterTranslation().size()
: 0,
vertex_shader->GetTextureBindingsAfterTranslation().size(),
vertex_shader->GetSamplerBindingsAfterTranslation().size());
if (!pipeline_layout) {
++pipelines_layout_failed;
continue;
}
// Get geometry shader if needed.
VkShaderModule geometry_shader = VK_NULL_HANDLE;
if (pipeline_description.geometry_shader !=
PipelineGeometryShader::kNone) {
GeometryShaderKey geometry_shader_key;
GetGeometryShaderKey(
pipeline_description.geometry_shader,
SpirvShaderTranslator::Modification(
vertex_translation->modification()),
SpirvShaderTranslator::Modification(
pixel_translation ? pixel_translation->modification() : 0),
geometry_shader_key);
geometry_shader = GetGeometryShader(geometry_shader_key);
if (geometry_shader == VK_NULL_HANDLE) {
continue;
}
}
// Get tessellation shaders if needed.
VkShaderModule tessellation_vertex_shader = VK_NULL_HANDLE;
VkShaderModule tessellation_control_shader = VK_NULL_HANDLE;
if (pipeline_description.tessellation_mode !=
PipelineTessellationMode::kNone) {
tessellation_vertex_shader =
GetTessellationVertexShader(pipeline_description.tessellation_mode);
bool use_control_point_count =
(pipeline_description.tessellation_mode ==
PipelineTessellationMode::kAdaptive);
tessellation_control_shader = GetTessellationControlShader(
pipeline_description.tessellation_mode,
pipeline_description.tessellation_patch, use_control_point_count);
if (tessellation_vertex_shader == VK_NULL_HANDLE ||
tessellation_control_shader == VK_NULL_HANDLE) {
continue;
}
}
// Create the pipeline entry.
auto& pipeline_pair =
*pipelines_.emplace(pipeline_description, Pipeline(pipeline_layout))
.first;
// Queue for creation.
if (!creation_threads_.empty()) {
// Calculate priority based on whether shader writes to visible RTs.
uint8_t priority = 0;
if (pixel_shader) {
uint32_t bound_rts =
(pipeline_description.render_targets[0].color_write_mask ? 1
: 0) |
(pipeline_description.render_targets[1].color_write_mask ? 2
: 0) |
(pipeline_description.render_targets[2].color_write_mask ? 4
: 0) |
(pipeline_description.render_targets[3].color_write_mask ? 8 : 0);
priority = pipeline_util::CalculatePipelinePriority(
bound_rts, pixel_shader->writes_color_targets(),
pixel_shader->writes_depth());
}
std::lock_guard<std::mutex> lock(creation_request_lock_);
PipelineCreationArguments creation_arguments;
creation_arguments.pipeline = &pipeline_pair;
creation_arguments.vertex_shader = vertex_translation;
creation_arguments.pixel_shader = pixel_translation;
creation_arguments.geometry_shader = geometry_shader;
creation_arguments.tessellation_vertex_shader =
tessellation_vertex_shader;
creation_arguments.tessellation_control_shader =
tessellation_control_shader;
creation_arguments.render_pass = render_pass;
creation_arguments.priority = priority;
creation_queue_.push(creation_arguments);
creation_request_cond_.notify_one();
} else {
// No creation threads - create synchronously.
PipelineCreationArguments creation_arguments;
creation_arguments.pipeline = &pipeline_pair;
creation_arguments.vertex_shader = vertex_translation;
creation_arguments.pixel_shader = pixel_translation;
creation_arguments.geometry_shader = geometry_shader;
creation_arguments.tessellation_vertex_shader =
tessellation_vertex_shader;
creation_arguments.tessellation_control_shader =
tessellation_control_shader;
creation_arguments.render_pass = render_pass;
EnsurePipelineCreated(creation_arguments);
}
++pipelines_created;
}
if (!creation_threads_.empty()) {
if (blocking) {
// Blocking mode: wait for all pipelines to be created.
bool await_creation_completion_event;
{
std::lock_guard<std::mutex> lock(creation_request_lock_);
await_creation_completion_event =
!creation_queue_.empty() || creation_threads_busy_ != 0;
if (await_creation_completion_event) {
creation_completion_event_->Reset();
creation_completion_set_event_.store(true,
std::memory_order_release);
}
}
if (await_creation_completion_event) {
creation_request_cond_.notify_one();
xe::threading::Wait(creation_completion_event_.get(), false);
}
} else {
// Non-blocking mode: store callback for later invocation.
std::lock_guard<std::mutex> lock(creation_request_lock_);
if (creation_queue_.empty() && creation_threads_busy_ == 0) {
// No work pending - callback will be invoked at end of function.
} else {
creation_completion_callback_ = std::move(completion_callback);
completion_callback = nullptr; // Prevent invocation at end
}
}
}
XELOGI("Pipeline cache: {} created, {} already exist, {} total in {} ms",
pipelines_created, pipelines_already_exist,
pipeline_stored_descriptions.size(),
(xe::Clock::QueryHostTickCount() - pipeline_creation_start) * 1000 /
xe::Clock::QueryHostTickFrequency());
if (pipelines_vs_not_found || pipelines_vs_translation_missing ||
pipelines_ps_not_found || pipelines_ps_translation_missing ||
pipelines_render_pass_failed || pipelines_layout_failed ||
pipelines_requirements_not_met) {
XELOGI(
"Pipeline cache skipped: {} VS not found, {} VS translation missing, "
"{} PS not found, {} PS translation missing, {} render pass failed, "
"{} layout failed, {} requirements not met",
pipelines_vs_not_found, pipelines_vs_translation_missing,
pipelines_ps_not_found, pipelines_ps_translation_missing,
pipelines_render_pass_failed, pipelines_layout_failed,
pipelines_requirements_not_met);
}
}
// Invoke completion callback if no async work was queued.
if (completion_callback) {
completion_callback();
}
}
void VulkanPipelineCache::ShutdownShaderStorage() {
// Save VkPipelineCache to disk before shutting down storage.
if (vk_pipeline_cache_ != VK_NULL_HANDLE &&
!vk_pipeline_cache_path_.empty()) {
const ui::vulkan::VulkanDevice* const vulkan_device =
command_processor_.GetVulkanDevice();
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device();
size_t cache_size = 0;
if (dfn.vkGetPipelineCacheData(device, vk_pipeline_cache_, &cache_size,
nullptr) == VK_SUCCESS &&
cache_size > 0) {
std::vector<uint8_t> cache_data(cache_size);
if (dfn.vkGetPipelineCacheData(device, vk_pipeline_cache_, &cache_size,
cache_data.data()) == VK_SUCCESS) {
if (FILE* cache_file =
xe::filesystem::OpenFile(vk_pipeline_cache_path_, "wb")) {
fwrite(cache_data.data(), 1, cache_size, cache_file);
fclose(cache_file);
XELOGI("Saved {} bytes of VkPipelineCache data", cache_size);
}
}
}
}
vk_pipeline_cache_path_.clear();
// Shut down the storage writer (closes files, stops write thread).
storage_writer_.ShutdownShaderStorage();
shader_storage_file_flush_needed_ = false;
pipeline_storage_file_flush_needed_ = false;
shader_storage_title_id_ = 0;
}
} // namespace vulkan
} // namespace gpu
} // namespace xe

View File

@@ -13,12 +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>
@@ -30,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"
@@ -101,6 +105,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();
@@ -259,6 +269,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.
@@ -314,6 +336,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;
@@ -452,6 +479,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 replaced shader modules and pipelines.
// Pipelines are only destroyed after the GPU submission that might reference
@@ -462,6 +493,18 @@ class VulkanPipelineCache {
// last potentially used in. Only destroyed when that submission completes.
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

View File

@@ -69,6 +69,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)