[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.
This commit is contained in:
Herman S.
2025-12-31 20:23:37 +09:00
parent ccf8fb66f5
commit 5845f3437b
14 changed files with 702 additions and 99 deletions

View File

@@ -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() |

View File

@@ -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,

View File

@@ -11,6 +11,7 @@
#include <cmath>
#include <cstring>
#include <thread>
#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<xe_mutex> 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<xe_mutex> lock(creation_request_lock_);
// Assuming the creation queue is already empty (because the processor
// thread also worked on creating the leftover pipelines), so only check
// if there are threads with pipelines currently being created.
await_creation_completion_event = creation_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<reg::SQ_PROGRAM_CNTL>().vs_export_mode !=
xenos::VertexShaderExportMode::kPosition2VectorsEdgeKill);
assert_false(register_file_.Get<reg::SQ_PROGRAM_CNTL>().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<xe_mutex> 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<const DxbcShader*>(&vertex_shader->shader()),
pixel_shader ? static_cast<const DxbcShader*>(&pixel_shader->shader())
: nullptr,
(pixel_shader && !for_placeholder)
? static_cast<const DxbcShader*>(&pixel_shader->shader())
: nullptr,
tessellated);
if (runtime_description_out.root_signature == nullptr) {
return false;
@@ -2831,6 +2870,72 @@ const std::vector<uint32_t>& 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<DxbcShader&>(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<const DxbcShader*>(&desc.vertex_shader->shader()),
static_cast<const DxbcShader*>(&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");
}
}
}

View File

@@ -10,12 +10,14 @@
#ifndef XENIA_GPU_D3D12_PIPELINE_CACHE_H_
#define XENIA_GPU_D3D12_PIPELINE_CACHE_H_
#include <atomic>
#include <condition_variable>
#include <cstdio>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <queue>
#include <string>
#include <thread>
#include <unordered_map>
@@ -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<const Pipeline*>(handle)->state;
return reinterpret_cast<const Pipeline*>(handle)->state.load(
std::memory_order_acquire);
}
ID3D12RootSignature* GetRootSignatureByHandle(void* handle) const {
return reinterpret_cast<const Pipeline*>(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<DxbcShaderTranslator> shader_translator_;
// Command processor thread DXIL conversion/disassembly interfaces, if DXIL
@@ -358,10 +372,39 @@ class PipelineCache {
std::vector<uint8_t> depth_only_pixel_shader_;
struct Pipeline {
// nullptr if creation has failed.
ID3D12PipelineState* state;
// nullptr if creation has failed or still pending.
std::atomic<ID3D12PipelineState*> 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<uint64_t, Pipeline*,
xe::hash::IdentityHasher<uint64_t>>
@@ -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<Pipeline*> 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<Pipeline*, std::vector<Pipeline*>,
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_.

View File

@@ -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");

View File

@@ -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

View File

@@ -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 <cstdint>
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_

View File

@@ -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,
};

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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<std::mutex> 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<std::mutex> 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<VkShaderModule> modules_to_destroy;
std::vector<VkPipeline> pipelines_to_destroy;
uint64_t completed_submission = command_processor_.GetCompletedSubmission();
{
std::lock_guard<std::mutex> 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

View File

@@ -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<bool> 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<xe::threading::Event> creation_completion_event_ = nullptr;
std::atomic<bool> 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<VkShaderModule> 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<std::pair<VkPipeline, uint64_t>> deferred_destroy_pipelines_;
std::mutex deferred_destroy_mutex_;
};
} // namespace vulkan

View File

@@ -520,6 +520,65 @@ std::unique_ptr<ImmediateDrawer> 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<D3D12_MESSAGE*>(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<int>(message->ID), message->pDescription);
} else if (message->Severity == D3D12_MESSAGE_SEVERITY_WARNING) {
XELOGW("D3D12 {}: [ID {}] {}", severity_str,
static_cast<int>(message->ID), message->pDescription);
} else {
XELOGI("D3D12 {}: [ID {}] {}", severity_str,
static_cast<int>(message->ID), message->pDescription);
}
}
info_queue->ClearStoredMessages();
info_queue->Release();
}
} // namespace d3d12
} // namespace ui
} // namespace xe

View File

@@ -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;