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;