diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 012b81797..e550eb4b3 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -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) { diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 4c9a6c91b..deffb2049 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -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 completion_callback) { + if (completion_callback) { + completion_callback(); + } } void CommandProcessor::RequestFrameTrace( diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index f2bf000d4..1fa5bcaeb 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -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 completion_callback = nullptr); virtual void RequestFrameTrace(const std::filesystem::path& root_path); virtual void BeginTracing(const std::filesystem::path& root_path); diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index 6a74ab54a..923da2999 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -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 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. diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.h b/src/xenia/gpu/d3d12/d3d12_command_processor.h index bd70565fe..557d42479 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.h +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.h @@ -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 completion_callback = nullptr) override; void RequestFrameTrace(const std::filesystem::path& root_path) override; diff --git a/src/xenia/gpu/d3d12/pipeline_cache.cc b/src/xenia/gpu/d3d12/pipeline_cache.cc index f360fbc0e..d82658c98 100644 --- a/src/xenia/gpu/d3d12/pipeline_cache.cc +++ b/src/xenia/gpu/d3d12/pipeline_cache.cc @@ -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 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::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 pipeline_stored_descriptions; - // . - std::set> 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 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 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 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 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( - 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 lock(shaders_failed_to_translate_mutex); - shaders_failed_to_translate.push_back(translation); - } - } - { - std::lock_guard 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> - 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 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 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 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(&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> + & 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 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 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 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 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 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 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 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 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 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 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 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>& translations_needed, + bool edram_rov_used) { + uint64_t translation_start = xe::Clock::QueryHostTickCount(); + + std::vector 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 translation_index{0}; + std::mutex shaders_failed_to_translate_mutex; + std::vector 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( + shader->GetOrCreateTranslation(modification_it->second)); + if (!translation->is_translated() && + !TranslateAnalyzedShader(translator, *translation, dxbc_converter, + dxc_utils, dxc_compiler)) { + std::lock_guard 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> 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(&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 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 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 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. diff --git a/src/xenia/gpu/d3d12/pipeline_cache.h b/src/xenia/gpu/d3d12/pipeline_cache.h index ed4017d9d..6771fb771 100644 --- a/src/xenia/gpu/d3d12/pipeline_cache.h +++ b/src/xenia/gpu/d3d12/pipeline_cache.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -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 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>& 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 shader_storage_file_flush_needed_{false}; + std::atomic 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 storage_write_shader_queue_; - std::deque 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 storage_write_thread_; + // Storage writer for shaders and pipelines (owns file handles and storage + // index). + ShaderStorageWriter 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 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. diff --git a/src/xenia/gpu/graphics_system.cc b/src/xenia/gpu/graphics_system.cc index 6be717f52..610e5a005 100644 --- a/src/xenia/gpu/graphics_system.cc +++ b/src/xenia/gpu/graphics_system.cc @@ -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 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)); + }); } } diff --git a/src/xenia/gpu/graphics_system.h b/src/xenia/gpu/graphics_system.h index 6862903bb..3f8d5d7a9 100644 --- a/src/xenia/gpu/graphics_system.h +++ b/src/xenia/gpu/graphics_system.h @@ -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 completion_callback = nullptr); void RequestFrameTrace(); void BeginTracing(); diff --git a/src/xenia/gpu/shader.h b/src/xenia/gpu/shader.h index 0466b0043..fa3141d90 100644 --- a/src/xenia/gpu/shader.h +++ b/src/xenia/gpu/shader.h @@ -11,6 +11,7 @@ #define XENIA_GPU_SHADER_H_ #include +#include #include #include #include @@ -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 translations_; - uint32_t ucode_storage_index_ = UINT32_MAX; + std::atomic ucode_storage_index_{UINT32_MAX}; private: void GatherExecInformation( diff --git a/src/xenia/gpu/shader_storage.h b/src/xenia/gpu/shader_storage.h new file mode 100644 index 000000000..70c2c91d6 --- /dev/null +++ b/src/xenia/gpu/shader_storage.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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: +// /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 +inline uint64_t ReadShaderEntries(FILE* file, Callback&& callback) { + uint64_t valid_bytes = sizeof(ShaderStorageFileHeader); + + ShaderStoredHeader shader_header; + std::vector 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 +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; + + // 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>& 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& 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> 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 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 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 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 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 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 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 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 storage_write_thread_; + std::mutex storage_write_request_lock_; + std::condition_variable storage_write_request_cond_; + std::deque storage_write_shader_queue_; + std::deque 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_ diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index 6523e94e8..1ada25576 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -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 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 { diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.h b/src/xenia/gpu/vulkan/vulkan_command_processor.h index 77ac2f63a..424fbabb3 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.h +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.h @@ -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 completion_callback = nullptr) override; + void RestoreEdramSnapshot(const void* snapshot) override; ui::vulkan::VulkanDevice* GetVulkanDevice() const { diff --git a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc index 0e15df7e3..784d369ad 100644 --- a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc @@ -11,9 +11,12 @@ #include #include +#include #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 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(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(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 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 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 lock(creation_request_lock_); + std::unique_lock 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>& translations_needed, + bool edram_fsi_used) { + uint64_t translation_start = xe::Clock::QueryHostTickCount(); + + std::vector> 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( + 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 translation_index{0}; + std::atomic 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( + 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> 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 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::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 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> + & 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 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( + 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( + 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 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 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 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 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 diff --git a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h index 3724e008a..fdab3c061 100644 --- a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h +++ b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h @@ -13,12 +13,15 @@ #include #include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include @@ -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 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>& 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 creation_completion_event_ = nullptr; std::atomic creation_completion_set_event_{false}; + std::function 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> deferred_destroy_pipelines_; std::mutex deferred_destroy_mutex_; + + // Shader and pipeline storage. + uint32_t shader_storage_title_id_ = 0; + std::atomic shader_storage_file_flush_needed_{false}; + std::atomic pipeline_storage_file_flush_needed_{false}; + + // Storage writer for shaders and pipelines (owns file handles and storage + // index). + ShaderStorageWriter storage_writer_; + + // VkPipelineCache persistence path. + std::filesystem::path vk_pipeline_cache_path_; }; } // namespace vulkan diff --git a/src/xenia/ui/vulkan/functions/device_1_0.inc b/src/xenia/ui/vulkan/functions/device_1_0.inc index c2c251131..5646e9d1d 100644 --- a/src/xenia/ui/vulkan/functions/device_1_0.inc +++ b/src/xenia/ui/vulkan/functions/device_1_0.inc @@ -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)