From 5845f3437b8c22f95d4b9daee008c7b6e58c1fd8 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:23:37 +0900 Subject: [PATCH] [GPU] Async shader compilation for D3D12 and Vulkan Adds async_shader_compilation cvar (default true) that forces new pipelines to be created async from the main thread, skips draws entirely on D3D12 and on vulkan replaces pixel shader with placeholder until the real one is ready. Causes some visual artifacts on first load but greatly reduces load times and stutter. --- .../gpu/d3d12/d3d12_command_processor.cc | 9 + src/xenia/gpu/d3d12/d3d12_shader.h | 10 + src/xenia/gpu/d3d12/pipeline_cache.cc | 289 ++++++++++++++---- src/xenia/gpu/d3d12/pipeline_cache.h | 65 +++- src/xenia/gpu/gpu_flags.cc | 9 + src/xenia/gpu/gpu_flags.h | 2 + src/xenia/gpu/pipeline_util.h | 61 ++++ .../bytecode/vulkan_spirv/placeholder_ps.h | 47 +++ src/xenia/gpu/shaders/placeholder.ps.glsl | 20 ++ .../gpu/vulkan/vulkan_command_processor.cc | 4 + src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc | 183 +++++++++-- src/xenia/gpu/vulkan/vulkan_pipeline_cache.h | 39 ++- src/xenia/ui/d3d12/d3d12_provider.cc | 59 ++++ src/xenia/ui/d3d12/d3d12_provider.h | 4 + 14 files changed, 702 insertions(+), 99 deletions(-) create mode 100644 src/xenia/gpu/pipeline_util.h create mode 100644 src/xenia/gpu/shaders/bytecode/vulkan_spirv/placeholder_ps.h create mode 100644 src/xenia/gpu/shaders/placeholder.ps.glsl diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index 0698ff00f..6a74ab54a 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -2583,6 +2583,15 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, return false; } + if (cvars::async_shader_compilation) { + if (pipeline_cache_->GetD3D12PipelineByHandle(pipeline_handle) == nullptr) { + XELOGI("Skipping draw - pipeline not ready"); + return true; + } + // Re-fetch root signature now that pipeline is ready. + root_signature = pipeline_cache_->GetRootSignatureByHandle(pipeline_handle); + } + // Update the textures - this may bind pipelines. uint32_t used_texture_mask = vertex_shader->GetUsedTextureMaskAfterTranslation() | diff --git a/src/xenia/gpu/d3d12/d3d12_shader.h b/src/xenia/gpu/d3d12/d3d12_shader.h index c33c486a1..7a71b21e6 100644 --- a/src/xenia/gpu/d3d12/d3d12_shader.h +++ b/src/xenia/gpu/d3d12/d3d12_shader.h @@ -31,6 +31,16 @@ class D3D12Shader : public DxbcShader { IDxbcConverter* dxbc_converter = nullptr, IDxcUtils* dxc_utils = nullptr, IDxcCompiler* dxc_compiler = nullptr); + + // For background thread translation: atomically claim the right to + // translate. Returns true if caller should translate, false if another + // thread claimed it. + bool TryClaimTranslation() { + return !translation_claimed_.test_and_set(std::memory_order_acq_rel); + } + + private: + std::atomic_flag translation_claimed_ = ATOMIC_FLAG_INIT; }; D3D12Shader(xenos::ShaderType shader_type, uint64_t ucode_data_hash, diff --git a/src/xenia/gpu/d3d12/pipeline_cache.cc b/src/xenia/gpu/d3d12/pipeline_cache.cc index 43d483991..f360fbc0e 100644 --- a/src/xenia/gpu/d3d12/pipeline_cache.cc +++ b/src/xenia/gpu/d3d12/pipeline_cache.cc @@ -11,6 +11,7 @@ #include #include +#include #include "third_party/dxbc/DXBCChecksum.h" #include "third_party/fmt/include/fmt/format.h" @@ -31,6 +32,7 @@ #include "xenia/gpu/dxbc.h" #include "xenia/gpu/dxbc_shader_translator.h" #include "xenia/gpu/gpu_flags.h" +#include "xenia/gpu/pipeline_util.h" #include "xenia/gpu/registers.h" #include "xenia/gpu/xenos.h" #include "xenia/ui/d3d12/d3d12_util.h" @@ -195,7 +197,11 @@ void PipelineCache::Shutdown() { // Destroy all pipelines. current_pipeline_ = nullptr; for (auto it : pipelines_) { - it.second->state->Release(); + ID3D12PipelineState* state = + it.second->state.load(std::memory_order_acquire); + if (state) { + state->Release(); + } delete it.second; } pipelines_.clear(); @@ -669,9 +675,19 @@ void PipelineCache::InitializeShaderStorage( &pipeline_description, sizeof(pipeline_description)); Pipeline* new_pipeline = new Pipeline; - new_pipeline->state = nullptr; std::memcpy(&new_pipeline->description, &pipeline_runtime_description, sizeof(pipeline_runtime_description)); + // Calculate priority based on whether shader writes to visible RTs. + if (pixel_shader) { + uint32_t bound_rts = + (pipeline_description.render_targets[0].used ? 1 : 0) | + (pipeline_description.render_targets[1].used ? 2 : 0) | + (pipeline_description.render_targets[2].used ? 4 : 0) | + (pipeline_description.render_targets[3].used ? 8 : 0); + new_pipeline->priority = pipeline_util::CalculatePipelinePriority( + bound_rts, pixel_shader->writes_color_targets(), + pixel_shader->writes_depth()); + } pipelines_.emplace(pipeline_stored_description.description_hash, new_pipeline); COUNT_profile_set("gpu/pipeline_cache/pipelines", pipelines_.size()); @@ -679,11 +695,13 @@ void PipelineCache::InitializeShaderStorage( // Submit the pipeline for creation to any available thread. { std::lock_guard lock(creation_request_lock_); - creation_queue_.push_back(new_pipeline); + creation_queue_.push(new_pipeline); } creation_request_cond_.notify_one(); } else { - new_pipeline->state = CreateD3D12Pipeline(pipeline_runtime_description); + new_pipeline->state.store( + CreateD3D12Pipeline(pipeline_runtime_description), + std::memory_order_release); } ++pipelines_created; } @@ -806,24 +824,10 @@ void PipelineCache::EndSubmission() { pipeline_storage_file_flush_needed_ = false; } if (!creation_threads_.empty()) { - CreateQueuedPipelinesOnProcessorThread(); - // 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_threads_busy_ != 0; - if (await_creation_completion_event) { - creation_completion_event_->Reset(); - creation_completion_set_event_ = true; - } - } - if (await_creation_completion_event) { - creation_request_cond_.notify_one(); - xe::threading::Wait(creation_completion_event_.get(), false); - } + // Don't wait for pipeline creation - let background threads work + // asynchronously. Draws will be skipped until pipelines are ready. + // This avoids frame-time spikes from blocking on pipeline creation. + creation_request_cond_.notify_one(); } } @@ -1010,10 +1014,22 @@ bool PipelineCache::ConfigurePipeline( register_file_.Get().vs_export_mode != xenos::VertexShaderExportMode::kPosition2VectorsEdgeKill); assert_false(register_file_.Get().gen_index_vtx); - if (!vertex_shader->is_translated()) { - if (!vertex_shader->shader().is_ucode_analyzed()) { - vertex_shader->shader().AnalyzeUcode(ucode_disasm_buffer_); - } + + // Check if we should use async pipeline creation. + // When enabled, defer shader translation and pipeline creation to background. + // Only use async when there's a pixel shader - VS-only pipelines are fast + // to compile and don't benefit from async (vertex shaders are small). + bool use_async = cvars::async_shader_compilation && + !creation_threads_.empty() && pixel_shader != nullptr; + + // Ensure VS ucode is analyzed (needed for description hash). + if (!vertex_shader->shader().is_ucode_analyzed()) { + vertex_shader->shader().AnalyzeUcode(ucode_disasm_buffer_); + } + + // For async mode, defer VS translation to background thread. + // For sync mode, translate VS now on main thread. + if (!vertex_shader->is_translated() && !use_async) { if (!TranslateAnalyzedShader(*shader_translator_, *vertex_shader, dxbc_converter_, dxc_utils_, dxc_compiler_)) { XELOGE("Failed to translate the vertex shader!"); @@ -1031,11 +1047,15 @@ bool PipelineCache::ConfigurePipeline( storage_write_request_cond_.notify_all(); } } - if (!vertex_shader->is_valid()) { - // Translation attempted previously, but not valid. + if (!use_async && !vertex_shader->is_valid()) { + // Translation attempted previously, but not valid (sync mode only). return false; } - if (pixel_shader != nullptr) { + + if (pixel_shader != nullptr && !use_async) { + // Sync mode - must translate PS now on main thread. + // No mutex needed - main thread translator is not shared with background + // threads (they have their own translators). if (!pixel_shader->is_translated()) { if (!pixel_shader->shader().is_ucode_analyzed()) { pixel_shader->shader().AnalyzeUcode(ucode_disasm_buffer_); @@ -1070,7 +1090,8 @@ bool PipelineCache::ConfigurePipeline( vertex_shader, pixel_shader, primitive_processing_result, normalized_depth_control, normalized_color_mask, bound_depth_and_color_render_target_bits, - bound_depth_and_color_render_target_formats, runtime_description)) { + bound_depth_and_color_render_target_formats, runtime_description, + use_async)) { return false; } PipelineDescription& description = runtime_description.description; @@ -1078,7 +1099,7 @@ bool PipelineCache::ConfigurePipeline( if (current_pipeline_ != nullptr && current_pipeline_->description.description == description) { *pipeline_handle_out = current_pipeline_; - *root_signature_out = runtime_description.root_signature; + *root_signature_out = current_pipeline_->description.root_signature; return true; } @@ -1096,21 +1117,32 @@ bool PipelineCache::ConfigurePipeline( } Pipeline* new_pipeline = new Pipeline; - new_pipeline->state = nullptr; std::memcpy(&new_pipeline->description, &runtime_description, sizeof(runtime_description)); pipelines_.emplace(hash, new_pipeline); COUNT_profile_set("gpu/pipeline_cache/pipelines", pipelines_.size()); - if (!creation_threads_.empty()) { - // Submit the pipeline for creation to any available thread. + if (use_async) { + // Queue for background thread. + new_pipeline->pending_vertex_shader = vertex_shader; + new_pipeline->pending_pixel_shader = pixel_shader; + // Calculate priority based on whether shader writes to visible RTs. + if (pixel_shader) { + uint32_t bound_rts = pipeline_util::GetBoundRTMaskFromNormalizedColorMask( + normalized_color_mask); + new_pipeline->priority = pipeline_util::CalculatePipelinePriority( + bound_rts, pixel_shader->shader().writes_color_targets(), + pixel_shader->shader().writes_depth()); + } { std::lock_guard lock(creation_request_lock_); - creation_queue_.push_back(new_pipeline); + creation_queue_.push(new_pipeline); } creation_request_cond_.notify_one(); } else { - new_pipeline->state = CreateD3D12Pipeline(runtime_description); + // Sync mode or no creation threads: create synchronously. + new_pipeline->state.store(CreateD3D12Pipeline(runtime_description), + std::memory_order_release); } if (pipeline_storage_file_) { @@ -1339,10 +1371,14 @@ bool PipelineCache::GetCurrentStateDescription( uint32_t normalized_color_mask, uint32_t bound_depth_and_color_render_target_bits, const uint32_t* bound_depth_and_color_render_target_formats, - PipelineRuntimeDescription& runtime_description_out) { + PipelineRuntimeDescription& runtime_description_out, bool for_placeholder) { // Translated shaders needed at least for the root signature. - assert_true(vertex_shader->is_translated() && vertex_shader->is_valid()); - assert_true(!pixel_shader || + // Exception: for_placeholder mode (async pipeline creation) allows + // untranslated shaders - root signature uses VS bindings only initially, + // updated after background translation. + assert_true(for_placeholder || + (vertex_shader->is_translated() && vertex_shader->is_valid())); + assert_true(!pixel_shader || for_placeholder || (pixel_shader->is_translated() && pixel_shader->is_valid())); PipelineDescription& description_out = runtime_description_out.description; @@ -1376,10 +1412,13 @@ bool PipelineCache::GetCurrentStateDescription( RenderTargetCache::Path::kPixelShaderInterlock; // Root signature. + // For placeholder mode, pass nullptr for pixel_shader since placeholder PS + // has no texture/sampler bindings - root signature only needs VS bindings. runtime_description_out.root_signature = command_processor_.GetRootSignature( static_cast(&vertex_shader->shader()), - pixel_shader ? static_cast(&pixel_shader->shader()) - : nullptr, + (pixel_shader && !for_placeholder) + ? static_cast(&pixel_shader->shader()) + : nullptr, tessellated); if (runtime_description_out.root_signature == nullptr) { return false; @@ -2831,6 +2870,72 @@ const std::vector& PipelineCache::GetGeometryShader( return geometry_shaders_.emplace(key, std::move(shader)).first->second; } +void PipelineCache::EnsurePipelineShadersTranslated( + Pipeline* pipeline, DxbcShaderTranslator& translator, + StringBuffer& ucode_disasm_buffer, IDxbcConverter* dxbc_converter, + IDxcUtils* dxc_utils, IDxcCompiler* dxc_compiler, bool use_try_claim, + bool handle_non_placeholder) { + D3D12Shader::D3D12Translation* pending_vs = pipeline->pending_vertex_shader; + D3D12Shader::D3D12Translation* pending_ps = pipeline->pending_pixel_shader; + + // Helper lambda to translate a shader, optionally using TryClaimTranslation. + auto translate_shader = [&](D3D12Shader::D3D12Translation* translation, + const char* shader_type) { + if (!translation->is_translated()) { + bool should_translate = true; + if (use_try_claim) { + should_translate = translation->TryClaimTranslation(); + if (!should_translate) { + // Another thread is translating - wait for it. + while (!translation->is_translated()) { + std::this_thread::yield(); + } + } + } + if (should_translate) { + DxbcShader& shader = static_cast(translation->shader()); + if (!shader.is_ucode_analyzed()) { + shader.AnalyzeUcode(ucode_disasm_buffer); + } + if (!TranslateAnalyzedShader(translator, *translation, dxbc_converter, + dxc_utils, dxc_compiler)) { + XELOGE("Failed to translate {} shader {:016X}", shader_type, + shader.ucode_data_hash()); + } + } + } + }; + + // Translate pending VS if present. + if (pending_vs != nullptr) { + translate_shader(pending_vs, "vertex"); + pipeline->pending_vertex_shader = nullptr; + } + + // Translate pending PS if present and update root signature. + if (pending_ps != nullptr) { + translate_shader(pending_ps, "pixel"); + // Update root signature now that PS is translated. + if (pending_ps->is_valid()) { + PipelineRuntimeDescription& desc = pipeline->description; + bool tessellated = Shader::IsHostVertexShaderTypeDomain( + DxbcShaderTranslator::Modification(desc.vertex_shader->modification()) + .vertex.host_vertex_shader_type); + desc.root_signature = command_processor_.GetRootSignature( + static_cast(&desc.vertex_shader->shader()), + static_cast(&pending_ps->shader()), tessellated); + } + pipeline->pending_pixel_shader = nullptr; + } else if (handle_non_placeholder && pending_vs == nullptr) { + // Non-placeholder mode: translate desc.pixel_shader if needed (for + // pipelines loaded from cache). + D3D12Shader::D3D12Translation* ps = pipeline->description.pixel_shader; + if (ps != nullptr) { + translate_shader(ps, "pixel"); + } + } +} + ID3D12PipelineState* PipelineCache::CreateD3D12Pipeline( const PipelineRuntimeDescription& runtime_description) { const PipelineDescription& description = runtime_description.description; @@ -3238,16 +3343,23 @@ ID3D12PipelineState* PipelineCache::CreateD3D12Pipeline( // Create the D3D12 pipeline state object. ID3D12Device* device = command_processor_.GetD3D12Provider().GetDevice(); ID3D12PipelineState* state; - if (FAILED(device->CreateGraphicsPipelineState(&state_desc, - IID_PPV_ARGS(&state)))) { + HRESULT hr = + device->CreateGraphicsPipelineState(&state_desc, IID_PPV_ARGS(&state)); + if (FAILED(hr)) { if (runtime_description.pixel_shader != nullptr) { - XELOGE("Failed to create graphics pipeline with VS {:016X}, PS {:016X}", - runtime_description.vertex_shader->shader().ucode_data_hash(), - runtime_description.pixel_shader->shader().ucode_data_hash()); + XELOGE( + "Failed to create graphics pipeline with VS {:016X}, PS {:016X}: " + "HRESULT 0x{:08X}", + runtime_description.vertex_shader->shader().ucode_data_hash(), + runtime_description.pixel_shader->shader().ucode_data_hash(), hr); } else { - XELOGE("Failed to create graphics pipeline with VS {:016X}", - runtime_description.vertex_shader->shader().ucode_data_hash()); + XELOGE( + "Failed to create graphics pipeline with VS {:016X}: HRESULT " + "0x{:08X}", + runtime_description.vertex_shader->shader().ucode_data_hash(), hr); } + // Log D3D12 debug messages for all pipeline failures. + command_processor_.GetD3D12Provider().LogD3D12DebugMessages(); return nullptr; } std::wstring name; @@ -3346,6 +3458,33 @@ void PipelineCache::StorageWriteThread() { } 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. + const ui::d3d12::D3D12Provider& provider = + command_processor_.GetD3D12Provider(); + bool edram_rov_used = render_target_cache_.GetPath() == + RenderTargetCache::Path::kPixelShaderInterlock; + 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); + // Create thread-local DXIL conversion objects if needed. + 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) { Pipeline* pipeline_to_create = nullptr; @@ -3361,6 +3500,10 @@ void PipelineCache::CreationThread(size_t thread_index) { creation_completion_event_->Set(); } if (thread_index >= creation_threads_shutdown_from_) { + // Cleanup thread-local resources. + if (dxc_compiler) dxc_compiler->Release(); + if (dxc_utils) dxc_utils->Release(); + if (dxbc_converter) dxbc_converter->Release(); return; } creation_request_cond_.wait(lock); @@ -3370,15 +3513,38 @@ void PipelineCache::CreationThread(size_t thread_index) { // until the pipeline is created - other threads must be able to dequeue // requests, but can't set the completion event until the pipelines are // fully created (rather than just started creating). - pipeline_to_create = creation_queue_.front(); - creation_queue_.pop_front(); + pipeline_to_create = creation_queue_.top(); + creation_queue_.pop(); ++creation_threads_busy_; } + // Translate pending shaders and update root signature. + EnsurePipelineShadersTranslated(pipeline_to_create, translator, + ucode_disasm_buffer, dxbc_converter, + dxc_utils, dxc_compiler, + /*use_try_claim=*/true, + /*handle_non_placeholder=*/true); + // Create the D3D12 pipeline state object. - pipeline_to_create->state = + ID3D12PipelineState* new_state = CreateD3D12Pipeline(pipeline_to_create->description); + // Store the pipeline. If creation failed, state stays nullptr and draws + // will be skipped. + if (new_state != nullptr) { + pipeline_to_create->state.store(new_state, std::memory_order_release); + } else { + XELOGE("Pipeline creation failed (VS {:016X}, PS {:016X})", + pipeline_to_create->description.vertex_shader + ? pipeline_to_create->description.vertex_shader->shader() + .ucode_data_hash() + : 0, + pipeline_to_create->description.pixel_shader + ? pipeline_to_create->description.pixel_shader->shader() + .ucode_data_hash() + : 0); + } + // Pipeline created - the thread is not busy anymore, safe to set the // completion event if needed (at the next iteration, or in some other // thread). @@ -3398,11 +3564,26 @@ void PipelineCache::CreateQueuedPipelinesOnProcessorThread() { if (creation_queue_.empty()) { break; } - pipeline_to_create = creation_queue_.front(); - creation_queue_.pop_front(); + pipeline_to_create = creation_queue_.top(); + creation_queue_.pop(); } - pipeline_to_create->state = + + // Translate pending shaders and update root signature. + EnsurePipelineShadersTranslated(pipeline_to_create, *shader_translator_, + ucode_disasm_buffer_, dxbc_converter_, + dxc_utils_, dxc_compiler_, + /*use_try_claim=*/true, + /*handle_non_placeholder=*/true); + + ID3D12PipelineState* new_state = CreateD3D12Pipeline(pipeline_to_create->description); + + // Store the pipeline. If creation failed, state stays nullptr. + if (new_state != nullptr) { + pipeline_to_create->state.store(new_state, std::memory_order_release); + } else { + XELOGW("ProcessorThread: Pipeline creation failed"); + } } } diff --git a/src/xenia/gpu/d3d12/pipeline_cache.h b/src/xenia/gpu/d3d12/pipeline_cache.h index 43e528d35..ed4017d9d 100644 --- a/src/xenia/gpu/d3d12/pipeline_cache.h +++ b/src/xenia/gpu/d3d12/pipeline_cache.h @@ -10,12 +10,14 @@ #ifndef XENIA_GPU_D3D12_PIPELINE_CACHE_H_ #define XENIA_GPU_D3D12_PIPELINE_CACHE_H_ +#include #include #include #include #include #include #include +#include #include #include #include @@ -99,9 +101,15 @@ class PipelineCache { void** pipeline_handle_out, ID3D12RootSignature** root_signature_out); // Returns a pipeline with deferred creation by its handle. May return nullptr - // if failed to create the pipeline. + // if failed to create the pipeline or still being created asynchronously. ID3D12PipelineState* GetD3D12PipelineByHandle(void* handle) const { - return reinterpret_cast(handle)->state; + return reinterpret_cast(handle)->state.load( + std::memory_order_acquire); + } + + ID3D12RootSignature* GetRootSignatureByHandle(void* handle) const { + return reinterpret_cast(handle) + ->description.root_signature; } private: @@ -283,7 +291,11 @@ class PipelineCache { // If draw_util::IsRasterizationPotentiallyDone is false, the pixel shader // MUST be made nullptr BEFORE calling this! The shaders must be translated - // and valid. + // and valid, unless for_placeholder is true. + // When for_placeholder is true (async pipeline creation): + // - Shaders don't need to be translated yet (only hash/modification used) + // - Root signature uses VS bindings only, updated after background + // translation bool GetCurrentStateDescription( D3D12Shader::D3D12Translation* vertex_shader, D3D12Shader::D3D12Translation* pixel_shader, @@ -292,7 +304,8 @@ class PipelineCache { uint32_t normalized_color_mask, uint32_t bound_depth_and_color_render_target_bits, const uint32_t* bound_depth_and_color_render_target_formats, - PipelineRuntimeDescription& runtime_description_out); + PipelineRuntimeDescription& runtime_description_out, + bool for_placeholder = false); static bool GetGeometryShaderKey( PipelineGeometryShader geometry_shader_type, @@ -314,6 +327,7 @@ class PipelineCache { // Temporary storage for AnalyzeUcode calls on the processor thread. StringBuffer ucode_disasm_buffer_; // Reusable shader translator for the processor thread. + // Background creation threads have their own translators to avoid contention. std::unique_ptr shader_translator_; // Command processor thread DXIL conversion/disassembly interfaces, if DXIL @@ -358,10 +372,39 @@ class PipelineCache { std::vector depth_only_pixel_shader_; struct Pipeline { - // nullptr if creation has failed. - ID3D12PipelineState* state; + // nullptr if creation has failed or still pending. + std::atomic state{nullptr}; PipelineRuntimeDescription description; + // For background creation: stores the untranslated shaders. + // Background thread translates both VS and PS together, then creates the + // pipeline. Set to nullptr after translation is done. + D3D12Shader::D3D12Translation* pending_vertex_shader{nullptr}; + D3D12Shader::D3D12Translation* pending_pixel_shader{nullptr}; + // Priority for async compilation (higher = compiled sooner). + // Pipelines that write to visible render targets get higher priority. + uint8_t priority{0}; }; + + // Comparator for priority queue - higher priority first. + struct PipelineCreationPriorityCompare { + bool operator()(const Pipeline* a, const Pipeline* b) const { + return a->priority < b->priority; // max-heap: lower priority at bottom + } + }; + + // Helper to translate pending shaders for a pipeline and update root + // signature. Used by CreationThread and + // CreateQueuedPipelinesOnProcessorThread. If use_try_claim is true + // (background threads), uses TryClaimTranslation to prevent multiple threads + // translating the same shader. If handle_non_placeholder is true, also + // translates desc.pixel_shader when pending shaders are null (for pipelines + // loaded from cache). + void EnsurePipelineShadersTranslated( + Pipeline* pipeline, DxbcShaderTranslator& translator, + StringBuffer& ucode_disasm_buffer, IDxbcConverter* dxbc_converter, + IDxcUtils* dxc_utils, IDxcCompiler* dxc_compiler, bool use_try_claim, + bool handle_non_placeholder); + // All previously generated pipelines identified by hash and the description. std::unordered_multimap> @@ -405,9 +448,13 @@ class PipelineCache { void CreateQueuedPipelinesOnProcessorThread(); xe_mutex creation_request_lock_; std::condition_variable_any creation_request_cond_; - // Protected with creation_request_lock_, notify_one creation_request_cond_ - // when set. - std::deque creation_queue_; + // Priority queue contains pointers to map entries. Pipelines are never + // evicted as games have a finite set that should all remain cached for + // performance. Higher priority pipelines (those writing to visible RTs) + // are compiled first. + std::priority_queue, + PipelineCreationPriorityCompare> + creation_queue_; // Number of threads that are currently creating a pipeline - incremented when // a pipeline is dequeued (the completion event can't be triggered before this // is zero). Protected with creation_request_lock_. diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index 6d1e063c8..8f99e4e06 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -94,3 +94,12 @@ DEFINE_bool(gpu_3d_to_2d_texture, true, "Handle shaders that sample 3D textures as 2D by creating a 2D " "texture from slice 0 of the guest memory.", "GPU"); + +DEFINE_bool( + async_shader_compilation, true, + "Compile shaders and create pipelines asynchronously in background " + "threads. " + "Eliminates shader compilation stutter but may cause brief rendering " + "artifacts while pipelines are being created. When disabled, pipelines are " + "created synchronously which causes stutter but no visual artifacts.", + "GPU"); diff --git a/src/xenia/gpu/gpu_flags.h b/src/xenia/gpu/gpu_flags.h index 2e865abf5..5dbc34f09 100644 --- a/src/xenia/gpu/gpu_flags.h +++ b/src/xenia/gpu/gpu_flags.h @@ -36,6 +36,8 @@ DECLARE_bool(disassemble_pm4); DECLARE_bool(no_discard_stencil_in_transfer_pipelines); +DECLARE_bool(async_shader_compilation); + DECLARE_bool(gpu_3d_to_2d_texture); #define XE_GPU_FINE_GRAINED_DRAW_SCOPES 1 diff --git a/src/xenia/gpu/pipeline_util.h b/src/xenia/gpu/pipeline_util.h new file mode 100644 index 000000000..bad5e0d26 --- /dev/null +++ b/src/xenia/gpu/pipeline_util.h @@ -0,0 +1,61 @@ +/** + ****************************************************************************** + * 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_PIPELINE_UTIL_H_ +#define XENIA_GPU_PIPELINE_UTIL_H_ + +#include + +namespace xe { +namespace gpu { +namespace pipeline_util { + +// Priority levels for async pipeline compilation. +// Higher values = compiled sooner. +constexpr uint8_t kPriorityLowest = 0; // Writes to unbound RTs only +constexpr uint8_t kPriorityDepthOnly = 1; // Depth-only writes +constexpr uint8_t kPriorityVisibleRT = 2; // Writes to any visible RT +constexpr uint8_t kPriorityRT0 = 3; // Writes to RT0 (main color buffer) + +// Converts normalized_color_mask to a 4-bit bitmask of bound render targets. +// normalized_color_mask uses 4 bits per RT (for RGBA components). +inline uint32_t GetBoundRTMaskFromNormalizedColorMask( + uint32_t normalized_color_mask) { + return (((normalized_color_mask >> 0) & 0xF) ? 1 : 0) | + (((normalized_color_mask >> 4) & 0xF) ? 2 : 0) | + (((normalized_color_mask >> 8) & 0xF) ? 4 : 0) | + (((normalized_color_mask >> 12) & 0xF) ? 8 : 0); +} + +// Calculates pipeline compilation priority based on shader output. +// bound_rts: 4-bit mask of render targets that are bound +// shader_writes_color_targets: 4-bit mask of RTs the shader writes to +// shader_writes_depth: whether the shader writes depth +inline uint8_t CalculatePipelinePriority(uint32_t bound_rts, + uint32_t shader_writes_color_targets, + bool shader_writes_depth) { + uint32_t visible_writes = bound_rts & shader_writes_color_targets; + if (visible_writes) { + // Writes to at least one visible RT - high priority. + // Extra priority if writing to RT0 (usually main color buffer). + return (visible_writes & 1) ? kPriorityRT0 : kPriorityVisibleRT; + } + if (shader_writes_depth) { + // Depth-only - medium priority. + return kPriorityDepthOnly; + } + // Writes to unbound RTs only - lowest priority. + return kPriorityLowest; +} + +} // namespace pipeline_util +} // namespace gpu +} // namespace xe + +#endif // XENIA_GPU_PIPELINE_UTIL_H_ diff --git a/src/xenia/gpu/shaders/bytecode/vulkan_spirv/placeholder_ps.h b/src/xenia/gpu/shaders/bytecode/vulkan_spirv/placeholder_ps.h new file mode 100644 index 000000000..852bab45e --- /dev/null +++ b/src/xenia/gpu/shaders/bytecode/vulkan_spirv/placeholder_ps.h @@ -0,0 +1,47 @@ +// Generated with `xb buildshaders`. +#if 0 +; SPIR-V +; Version: 1.0 +; Generator: Khronos Glslang Reference Front End; 11 +; Bound: 9012 +; Schema: 0 + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %oC0 + OpExecutionMode %main OriginUpperLeft + OpSource GLSL 460 + OpName %main "main" + OpName %oC0 "oC0" + OpDecorate %oC0 Location 0 + %void = OpTypeVoid + %1282 = OpTypeFunction %void + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 +%_ptr_Output_v4float = OpTypePointer Output %v4float + %oC0 = OpVariable %_ptr_Output_v4float Output + %float_0 = OpConstant %float 0 + %2938 = OpConstantComposite %v4float %float_0 %float_0 %float_0 %float_0 + %main = OpFunction %void None %1282 + %9011 = OpLabel + OpStore %oC0 %2938 + OpReturn + OpFunctionEnd +#endif + +const uint32_t placeholder_ps[] = { + 0x07230203, 0x00010000, 0x0008000B, 0x00002334, 0x00000000, 0x00020011, + 0x00000001, 0x0006000B, 0x00000001, 0x4C534C47, 0x6474732E, 0x3035342E, + 0x00000000, 0x0003000E, 0x00000000, 0x00000001, 0x0006000F, 0x00000004, + 0x0000161F, 0x6E69616D, 0x00000000, 0x00001238, 0x00030010, 0x0000161F, + 0x00000007, 0x00030003, 0x00000002, 0x000001CC, 0x00040005, 0x0000161F, + 0x6E69616D, 0x00000000, 0x00030005, 0x00001238, 0x0030436F, 0x00040047, + 0x00001238, 0x0000001E, 0x00000000, 0x00020013, 0x00000008, 0x00030021, + 0x00000502, 0x00000008, 0x00030016, 0x0000000D, 0x00000020, 0x00040017, + 0x0000001D, 0x0000000D, 0x00000004, 0x00040020, 0x0000029A, 0x00000003, + 0x0000001D, 0x0004003B, 0x0000029A, 0x00001238, 0x00000003, 0x0004002B, + 0x0000000D, 0x00000A0C, 0x00000000, 0x0007002C, 0x0000001D, 0x00000B7A, + 0x00000A0C, 0x00000A0C, 0x00000A0C, 0x00000A0C, 0x00050036, 0x00000008, + 0x0000161F, 0x00000000, 0x00000502, 0x000200F8, 0x00002333, 0x0003003E, + 0x00001238, 0x00000B7A, 0x000100FD, 0x00010038, +}; diff --git a/src/xenia/gpu/shaders/placeholder.ps.glsl b/src/xenia/gpu/shaders/placeholder.ps.glsl new file mode 100644 index 000000000..d47ca76ba --- /dev/null +++ b/src/xenia/gpu/shaders/placeholder.ps.glsl @@ -0,0 +1,20 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#version 460 + +// Minimal placeholder pixel shader for pipeline hot-swap. +// Used temporarily while the real shader compiles in the background. +// Outputs transparent black to minimize visual disruption. + +layout(location = 0) out vec4 oC0; + +void main() { + oC0 = vec4(0.0, 0.0, 0.0, 0.0); +} diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index 875fc8ed1..6523e94e8 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -2466,6 +2466,10 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, return false; } } + // If async mode is active, this may be a placeholder pipeline. The real + // pipeline will be swapped in by the creation thread when ready. + // We re-load the handle to pick up any swap that may have happened. + current_pipeline = pipeline->pipeline.load(std::memory_order_acquire); // Update the textures before most other work in the submission because // samplers depend on this (and in case of sampler overflow in a submission, diff --git a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc index 78dcf3f05..0e15df7e3 100644 --- a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc @@ -20,6 +20,7 @@ #include "xenia/base/xxhash.h" #include "xenia/gpu/draw_util.h" #include "xenia/gpu/gpu_flags.h" +#include "xenia/gpu/pipeline_util.h" #include "xenia/gpu/register_file.h" #include "xenia/gpu/registers.h" #include "xenia/gpu/spirv_builder.h" @@ -30,7 +31,7 @@ #include "xenia/gpu/xenos.h" #include "xenia/ui/vulkan/vulkan_util.h" -// Tessellation shader bytecode. +// Shader bytecode. namespace shaders { #include "xenia/gpu/shaders/bytecode/vulkan_spirv/adaptive_quad_hs.h" #include "xenia/gpu/shaders/bytecode/vulkan_spirv/adaptive_triangle_hs.h" @@ -44,6 +45,8 @@ namespace shaders { #include "xenia/gpu/shaders/bytecode/vulkan_spirv/discrete_triangle_3cp_hs.h" #include "xenia/gpu/shaders/bytecode/vulkan_spirv/tessellation_adaptive_vs.h" #include "xenia/gpu/shaders/bytecode/vulkan_spirv/tessellation_indexed_vs.h" +// Placeholder pixel shader for pipeline hot-swap. +#include "xenia/gpu/shaders/bytecode/vulkan_spirv/placeholder_ps.h" } // namespace shaders DEFINE_int32( @@ -164,6 +167,15 @@ bool VulkanPipelineCache::Initialize() { } } + // Create placeholder pixel shader for pipeline hot-swap (stutter reduction). + placeholder_pixel_shader_ = ui::vulkan::util::CreateShaderModule( + vulkan_device, shaders::placeholder_ps, sizeof(shaders::placeholder_ps)); + if (placeholder_pixel_shader_ == VK_NULL_HANDLE) { + XELOGW( + "VulkanPipelineCache: Failed to create placeholder pixel shader - " + "pipeline hot-swap will not be available"); + } + // Create Vulkan pipeline cache for faster pipeline creation. VkPipelineCacheCreateInfo pipeline_cache_create_info = {}; pipeline_cache_create_info.sType = @@ -226,6 +238,24 @@ void VulkanPipelineCache::Shutdown() { const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); const VkDevice device = vulkan_device->device(); + // Process any remaining deferred destructions (force destroy all since + // device should be idle at shutdown). + { + std::lock_guard lock(deferred_destroy_mutex_); + for (VkShaderModule module : deferred_destroy_shader_modules_) { + if (module != VK_NULL_HANDLE) { + dfn.vkDestroyShaderModule(device, module, nullptr); + } + } + deferred_destroy_shader_modules_.clear(); + for (const auto& pipeline_pair : deferred_destroy_pipelines_) { + if (pipeline_pair.first != VK_NULL_HANDLE) { + dfn.vkDestroyPipeline(device, pipeline_pair.first, nullptr); + } + } + deferred_destroy_pipelines_.clear(); + } + // Destroy all pipelines. last_pipeline_ = nullptr; for (const auto& pipeline_pair : pipelines_) { @@ -244,6 +274,8 @@ void VulkanPipelineCache::Shutdown() { // Destroy all internal shaders. ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyShaderModule, device, depth_only_fragment_shader_); + ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyShaderModule, device, + placeholder_pixel_shader_); // Destroy tessellation shaders. ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyShaderModule, device, tessellation_indexed_vs_); @@ -597,24 +629,12 @@ bool VulkanPipelineCache::ConfigurePipeline( // Calculate priority based on whether shader writes to visible RTs. uint8_t priority = 0; if (pixel_shader) { - // Get bound RT mask from normalized_color_mask (4 bits per RT). - uint32_t bound_rts = (((normalized_color_mask >> 0) & 0xF) ? 1 : 0) | - (((normalized_color_mask >> 4) & 0xF) ? 2 : 0) | - (((normalized_color_mask >> 8) & 0xF) ? 4 : 0) | - (((normalized_color_mask >> 12) & 0xF) ? 8 : 0); - uint32_t shader_writes = pixel_shader->shader().writes_color_targets(); - if (bound_rts & shader_writes) { - // Writes to at least one visible RT - high priority. - priority = 2; - // Extra priority if writing to RT0 (usually main color buffer). - if ((bound_rts & shader_writes) & 1) { - priority = 3; - } - } else if (pixel_shader->shader().writes_depth()) { - // Depth-only - medium priority. - priority = 1; - } - // else: writes to unbound RTs only - lowest priority (0). + uint32_t bound_rts = + pipeline_util::GetBoundRTMaskFromNormalizedColorMask( + normalized_color_mask); + priority = pipeline_util::CalculatePipelinePriority( + bound_rts, pixel_shader->shader().writes_color_targets(), + pixel_shader->shader().writes_depth()); } { @@ -664,6 +684,8 @@ bool VulkanPipelineCache::ConfigurePipeline( void VulkanPipelineCache::EndSubmission() { if (creation_threads_.empty()) { + // Process deferred destructions when GPU is idle + ProcessDeferredDestructions(); return; } // Await creation of all queued pipelines. @@ -684,6 +706,9 @@ void VulkanPipelineCache::EndSubmission() { creation_request_cond_.notify_one(); xe::threading::Wait(creation_completion_event_.get(), false); } + + // Process deferred destructions after waiting for pipelines + ProcessDeferredDestructions(); } bool VulkanPipelineCache::IsCreatingPipelines() { @@ -712,14 +737,19 @@ void VulkanPipelineCache::CreationThread() { if (!EnsureShadersTranslated(creation_arguments.vertex_shader, creation_arguments.pixel_shader)) { - // Mark pipeline as failed by keeping it as VK_NULL_HANDLE. - // The pipeline will remain VK_NULL_HANDLE, indicating failure. XELOGE("Failed to translate shaders for pipeline creation"); - } else { - if (!EnsurePipelineCreated(creation_arguments)) { - // Pipeline creation failed - it will remain VK_NULL_HANDLE. - XELOGE("Failed to create Vulkan pipeline"); - } + } else if (!EnsurePipelineCreated(creation_arguments)) { + XELOGE("Failed to create Vulkan pipeline"); + } + // On failure: if a placeholder exists it will remain in use permanently. + // Clear the flag so we're not in a misleading "waiting for real" state. + if (creation_arguments.pipeline->second.is_placeholder.load( + std::memory_order_acquire)) { + XELOGW( + "Real pipeline creation failed - placeholder will remain in use " + "(may cause visual artifacts)"); + creation_arguments.pipeline->second.is_placeholder.store( + false, std::memory_order_release); } { @@ -2252,10 +2282,24 @@ VkShaderModule VulkanPipelineCache::GetTessellationVertexShader( } bool VulkanPipelineCache::EnsurePipelineCreated( - const PipelineCreationArguments& creation_arguments) { - if (creation_arguments.pipeline->second.pipeline.load( - std::memory_order_acquire) != VK_NULL_HANDLE) { - return true; + const PipelineCreationArguments& creation_arguments, + VkShaderModule fragment_shader_override) { + // Check if we already have a pipeline. + // If it's a placeholder and we're not creating another placeholder, + // we need to replace it with the real pipeline. + VkPipeline existing_pipeline = + creation_arguments.pipeline->second.pipeline.load( + std::memory_order_acquire); + bool is_placeholder = creation_arguments.pipeline->second.is_placeholder.load( + std::memory_order_acquire); + bool creating_placeholder = fragment_shader_override != VK_NULL_HANDLE; + + if (existing_pipeline != VK_NULL_HANDLE) { + if (!is_placeholder || creating_placeholder) { + // Already have a real pipeline, or trying to create another placeholder. + return true; + } + // Have a placeholder, and we're creating the real pipeline to replace it. } // This function preferably should validate the description to prevent @@ -2382,7 +2426,10 @@ bool VulkanPipelineCache::EnsurePipelineCreated( shader_stage_fragment.module = VK_NULL_HANDLE; shader_stage_fragment.pName = "main"; shader_stage_fragment.pSpecializationInfo = nullptr; - if (creation_arguments.pixel_shader) { + if (fragment_shader_override != VK_NULL_HANDLE) { + // Use the override shader (for placeholder pipelines). + shader_stage_fragment.module = fragment_shader_override; + } else if (creation_arguments.pixel_shader) { assert_true(creation_arguments.pixel_shader->is_translated()); if (!creation_arguments.pixel_shader->is_valid()) { return false; @@ -2761,11 +2808,81 @@ bool VulkanPipelineCache::EnsurePipelineCreated( } return false; } - creation_arguments.pipeline->second.pipeline.store(pipeline, - std::memory_order_release); + + // Store the new pipeline, handling placeholder hot-swap. + VkPipeline old_pipeline = + creation_arguments.pipeline->second.pipeline.exchange( + pipeline, std::memory_order_acq_rel); + + if (old_pipeline != VK_NULL_HANDLE) { + // We're replacing a placeholder pipeline with the real one. + // Queue the old placeholder for deferred destruction, recording the + // current submission number so we only destroy after the GPU is done. + uint64_t current_submission = command_processor_.GetCurrentSubmission(); + { + std::lock_guard lock(deferred_destroy_mutex_); + deferred_destroy_pipelines_.emplace_back(old_pipeline, + current_submission); + } + } + + // Mark as no longer a placeholder (for the case where we just created real). + if (!creating_placeholder) { + creation_arguments.pipeline->second.is_placeholder.store( + false, std::memory_order_release); + } + return true; } +void VulkanPipelineCache::ProcessDeferredDestructions() { + std::vector modules_to_destroy; + std::vector pipelines_to_destroy; + + uint64_t completed_submission = command_processor_.GetCompletedSubmission(); + + { + std::lock_guard lock(deferred_destroy_mutex_); + if (deferred_destroy_shader_modules_.empty() && + deferred_destroy_pipelines_.empty()) { + return; + } + modules_to_destroy = std::move(deferred_destroy_shader_modules_); + deferred_destroy_shader_modules_.clear(); + + // Only destroy pipelines whose submission has completed on the GPU. + // Keep pipelines that are still potentially in-flight. + auto it = deferred_destroy_pipelines_.begin(); + while (it != deferred_destroy_pipelines_.end()) { + if (it->second <= completed_submission) { + // This submission has completed, safe to destroy. + pipelines_to_destroy.push_back(it->first); + it = deferred_destroy_pipelines_.erase(it); + } else { + ++it; + } + } + } + + // Destroy the modules and pipelines now that we know GPU is done with them. + const ui::vulkan::VulkanDevice* vulkan_device = + command_processor_.GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + VkDevice device = vulkan_device->device(); + + for (VkShaderModule module : modules_to_destroy) { + if (module != VK_NULL_HANDLE) { + dfn.vkDestroyShaderModule(device, module, nullptr); + } + } + + for (VkPipeline pipeline : pipelines_to_destroy) { + if (pipeline != VK_NULL_HANDLE) { + dfn.vkDestroyPipeline(device, pipeline, nullptr); + } + } +} + } // 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 ebbea7425..3724e008a 100644 --- a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h +++ b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h @@ -61,18 +61,27 @@ class VulkanPipelineCache { // destroyed by it while the pipeline cache is active. const PipelineLayoutProvider* pipeline_layout; + // Placeholder pipeline support for reduced stutter. + // When true, the current pipeline uses a placeholder pixel shader and + // the real pipeline is being compiled in the background. + std::atomic is_placeholder{false}; + Pipeline(const PipelineLayoutProvider* pipeline_layout_provider) : pipeline_layout(pipeline_layout_provider) {} // Copy constructor needed for unordered_map Pipeline(const Pipeline& other) : pipeline(other.pipeline.load(std::memory_order_acquire)), - pipeline_layout(other.pipeline_layout) {} + pipeline_layout(other.pipeline_layout), + is_placeholder(other.is_placeholder.load(std::memory_order_acquire)) { + } // Move constructor Pipeline(Pipeline&& other) noexcept : pipeline(other.pipeline.load(std::memory_order_acquire)), - pipeline_layout(other.pipeline_layout) {} + pipeline_layout(other.pipeline_layout), + is_placeholder(other.is_placeholder.load(std::memory_order_acquire)) { + } // Deleted copy assignment to prevent accidental copying Pipeline& operator=(const Pipeline&) = delete; @@ -339,8 +348,18 @@ class VulkanPipelineCache { // Can be called from creation threads - all needed data must be fully set up // at the point of the call: shaders must be translated, pipeline layout and // render pass objects must be available. + // If fragment_shader_override is not VK_NULL_HANDLE, it is used instead of + // the pixel shader from creation_arguments (for placeholder pipelines). bool EnsurePipelineCreated( - const PipelineCreationArguments& creation_arguments); + const PipelineCreationArguments& creation_arguments, + VkShaderModule fragment_shader_override = VK_NULL_HANDLE); + + // Creates a placeholder pipeline using the placeholder pixel shader. + // Used for pipeline hot-swap to reduce stutter. + bool EnsurePipelineCreatedWithPlaceholder( + const PipelineCreationArguments& creation_arguments) { + return EnsurePipelineCreated(creation_arguments, placeholder_pixel_shader_); + } VulkanCommandProcessor& command_processor_; const RegisterFile& register_file_; @@ -382,6 +401,10 @@ class VulkanPipelineCache { // shader interlock when no Xenos pixel shader provided. VkShaderModule depth_only_fragment_shader_ = VK_NULL_HANDLE; + // Placeholder pixel shader for pipeline hot-swap to reduce stutter. + // Outputs transparent black while the real shader compiles in background. + VkShaderModule placeholder_pixel_shader_ = VK_NULL_HANDLE; + // Tessellation shaders. // Vertex shaders for tessellation - pass indices/factors to TCS. VkShaderModule tessellation_indexed_vs_ = VK_NULL_HANDLE; @@ -429,6 +452,16 @@ class VulkanPipelineCache { std::condition_variable creation_request_cond_; std::unique_ptr creation_completion_event_ = nullptr; std::atomic creation_completion_set_event_{false}; + + // Deferred destruction of replaced shader modules and pipelines. + // Pipelines are only destroyed after the GPU submission that might reference + // them has completed (tracked via submission numbers from command processor). + void ProcessDeferredDestructions(); + std::vector deferred_destroy_shader_modules_; + // Pipelines pending destruction, paired with the submission number they were + // last potentially used in. Only destroyed when that submission completes. + std::vector> deferred_destroy_pipelines_; + std::mutex deferred_destroy_mutex_; }; } // namespace vulkan diff --git a/src/xenia/ui/d3d12/d3d12_provider.cc b/src/xenia/ui/d3d12/d3d12_provider.cc index 541f3428e..069ea1aeb 100644 --- a/src/xenia/ui/d3d12/d3d12_provider.cc +++ b/src/xenia/ui/d3d12/d3d12_provider.cc @@ -520,6 +520,65 @@ std::unique_ptr D3D12Provider::CreateImmediateDrawer() { return D3D12ImmediateDrawer::Create(*this); } +void D3D12Provider::LogD3D12DebugMessages() const { + if (!device_) { + return; + } + + ID3D12InfoQueue* info_queue = nullptr; + if (FAILED(device_->QueryInterface(IID_PPV_ARGS(&info_queue)))) { + return; + } + + UINT64 message_count = info_queue->GetNumStoredMessages(); + for (UINT64 i = 0; i < message_count; ++i) { + SIZE_T message_size = 0; + if (FAILED(info_queue->GetMessage(i, nullptr, &message_size))) { + continue; + } + + D3D12_MESSAGE* message = + reinterpret_cast(alloca(message_size)); + if (FAILED(info_queue->GetMessage(i, message, &message_size))) { + continue; + } + + const char* severity_str = "INFO"; + switch (message->Severity) { + case D3D12_MESSAGE_SEVERITY_CORRUPTION: + severity_str = "CORRUPTION"; + break; + case D3D12_MESSAGE_SEVERITY_ERROR: + severity_str = "ERROR"; + break; + case D3D12_MESSAGE_SEVERITY_WARNING: + severity_str = "WARNING"; + break; + case D3D12_MESSAGE_SEVERITY_INFO: + severity_str = "INFO"; + break; + case D3D12_MESSAGE_SEVERITY_MESSAGE: + severity_str = "MESSAGE"; + break; + } + + if (message->Severity == D3D12_MESSAGE_SEVERITY_ERROR || + message->Severity == D3D12_MESSAGE_SEVERITY_CORRUPTION) { + XELOGE("D3D12 {}: [ID {}] {}", severity_str, + static_cast(message->ID), message->pDescription); + } else if (message->Severity == D3D12_MESSAGE_SEVERITY_WARNING) { + XELOGW("D3D12 {}: [ID {}] {}", severity_str, + static_cast(message->ID), message->pDescription); + } else { + XELOGI("D3D12 {}: [ID {}] {}", severity_str, + static_cast(message->ID), message->pDescription); + } + } + + info_queue->ClearStoredMessages(); + info_queue->Release(); +} + } // namespace d3d12 } // namespace ui } // namespace xe diff --git a/src/xenia/ui/d3d12/d3d12_provider.h b/src/xenia/ui/d3d12/d3d12_provider.h index e40db61aa..32d5606b8 100644 --- a/src/xenia/ui/d3d12/d3d12_provider.h +++ b/src/xenia/ui/d3d12/d3d12_provider.h @@ -158,6 +158,10 @@ class D3D12Provider : public GraphicsProvider { return pfn_dxcompiler_dxc_create_instance_(rclsid, riid, ppv); } + // Logs all pending D3D12 debug messages to xenia's log. + // Call this after operations that fail to get detailed error info. + void LogD3D12DebugMessages() const; + private: D3D12Provider() = default;