mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[D3D12] Improve shader disk cache handling
Fixed issue with async compiled shaders not persisted to disk and changed startup pipeline creation to run async while the game goes through its own startup instead of blocking
This commit is contained in:
@@ -1831,15 +1831,16 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path,
|
||||
}
|
||||
}
|
||||
|
||||
// Initializing the shader storage in a blocking way so the user doesn't
|
||||
// miss the initial seconds - for instance, sound from an intro video may
|
||||
// start playing before the video can be seen if doing this in parallel with
|
||||
// the main thread.
|
||||
// Initialize shader storage asynchronously - pipeline compilation happens in
|
||||
// background while the game goes through its normal startup (loading screens,
|
||||
// intro videos, etc.). With async_shader_compilation enabled, draws are
|
||||
// skipped until pipelines are ready, so this is safe. By the time actual
|
||||
// gameplay starts, most cached pipelines should be compiled.
|
||||
if (graphics_system_) {
|
||||
on_shader_storage_initialization(true);
|
||||
graphics_system_->InitializeShaderStorage(cache_root_, title_id_.value(),
|
||||
true);
|
||||
on_shader_storage_initialization(false);
|
||||
graphics_system_->InitializeShaderStorage(
|
||||
cache_root_, title_id_.value(), false,
|
||||
[this]() { on_shader_storage_initialization(false); });
|
||||
}
|
||||
|
||||
auto main_thread = kernel_state_->LaunchModule(module);
|
||||
|
||||
@@ -192,7 +192,11 @@ void CommandProcessor::Shutdown() {
|
||||
}
|
||||
|
||||
void CommandProcessor::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback) {
|
||||
if (completion_callback) {
|
||||
completion_callback();
|
||||
}
|
||||
}
|
||||
|
||||
void CommandProcessor::RequestFrameTrace(
|
||||
|
||||
@@ -144,8 +144,9 @@ class CommandProcessor {
|
||||
// May be called not only from the command processor thread when the command
|
||||
// processor is paused, and the termination of this function may be explicitly
|
||||
// awaited.
|
||||
virtual void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
||||
uint32_t title_id, bool blocking);
|
||||
virtual void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr);
|
||||
|
||||
virtual void RequestFrameTrace(const std::filesystem::path& root_path);
|
||||
virtual void BeginTracing(const std::filesystem::path& root_path);
|
||||
|
||||
@@ -113,9 +113,12 @@ void D3D12CommandProcessor::ClearReadbackBuffers() {
|
||||
}
|
||||
|
||||
void D3D12CommandProcessor::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
|
||||
CommandProcessor::InitializeShaderStorage(cache_root, title_id, blocking);
|
||||
pipeline_cache_->InitializeShaderStorage(cache_root, title_id, blocking);
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback) {
|
||||
CommandProcessor::InitializeShaderStorage(cache_root, title_id, blocking,
|
||||
nullptr);
|
||||
pipeline_cache_->InitializeShaderStorage(cache_root, title_id, blocking,
|
||||
std::move(completion_callback));
|
||||
}
|
||||
|
||||
void D3D12CommandProcessor::RequestFrameTrace(
|
||||
@@ -2892,7 +2895,12 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type,
|
||||
|
||||
if (cvars::async_shader_compilation) {
|
||||
if (pipeline_cache_->GetD3D12PipelineByHandle(pipeline_handle) == nullptr) {
|
||||
XELOGI("Skipping draw - pipeline not ready");
|
||||
XELOGI(
|
||||
"Skipping draw - pipeline not ready: VS {:016X} mod {:016X}, PS "
|
||||
"{:016X} mod {:016X}",
|
||||
vertex_shader->ucode_data_hash(), vertex_shader_modification.value,
|
||||
pixel_shader ? pixel_shader->ucode_data_hash() : 0,
|
||||
pixel_shader_modification.value);
|
||||
return true;
|
||||
}
|
||||
// Re-fetch root signature now that pipeline is ready.
|
||||
|
||||
@@ -70,8 +70,9 @@ class D3D12CommandProcessor final : public CommandProcessor {
|
||||
void InvalidateGpuMemory() override;
|
||||
void ClearReadbackBuffers() override;
|
||||
|
||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
||||
uint32_t title_id, bool blocking) override;
|
||||
void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr) override;
|
||||
|
||||
void RequestFrameTrace(const std::filesystem::path& root_path) override;
|
||||
|
||||
|
||||
@@ -226,7 +226,8 @@ void PipelineCache::Shutdown() {
|
||||
}
|
||||
|
||||
void PipelineCache::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback) {
|
||||
ShutdownShaderStorage();
|
||||
|
||||
auto shader_storage_root = cache_root / "shaders";
|
||||
@@ -576,7 +577,7 @@ void PipelineCache::InitializeShaderStorage(
|
||||
std::min(pipeline_stored_descriptions.size(), logical_processor_count) -
|
||||
size_t(1),
|
||||
creation_thread_original_count);
|
||||
while (creation_threads_.size() < creation_thread_original_count) {
|
||||
while (creation_threads_.size() < creation_thread_needed_count) {
|
||||
size_t creation_thread_index = creation_threads_.size();
|
||||
std::unique_ptr<xe::threading::Thread> creation_thread =
|
||||
xe::threading::Thread::Create({}, [this, creation_thread_index]() {
|
||||
@@ -588,6 +589,12 @@ void PipelineCache::InitializeShaderStorage(
|
||||
}
|
||||
|
||||
size_t pipelines_created = 0;
|
||||
size_t pipelines_already_exist = 0;
|
||||
size_t pipelines_vs_not_found = 0;
|
||||
size_t pipelines_vs_translation_missing = 0;
|
||||
size_t pipelines_ps_not_found = 0;
|
||||
size_t pipelines_ps_translation_missing = 0;
|
||||
size_t pipelines_root_sig_failed = 0;
|
||||
for (const PipelineStoredDescription& pipeline_stored_description :
|
||||
pipeline_stored_descriptions) {
|
||||
const PipelineDescription& pipeline_description =
|
||||
@@ -607,6 +614,7 @@ void PipelineCache::InitializeShaderStorage(
|
||||
}
|
||||
}
|
||||
if (pipeline_found) {
|
||||
++pipelines_already_exist;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -614,6 +622,9 @@ void PipelineCache::InitializeShaderStorage(
|
||||
auto vertex_shader_it =
|
||||
shaders_.find(pipeline_description.vertex_shader_hash);
|
||||
if (vertex_shader_it == shaders_.end()) {
|
||||
++pipelines_vs_not_found;
|
||||
XELOGW("Pipeline cache: VS {:016X} not found in shader storage",
|
||||
pipeline_description.vertex_shader_hash);
|
||||
continue;
|
||||
}
|
||||
D3D12Shader* vertex_shader = vertex_shader_it->second;
|
||||
@@ -624,6 +635,12 @@ void PipelineCache::InitializeShaderStorage(
|
||||
if (!pipeline_runtime_description.vertex_shader ||
|
||||
!pipeline_runtime_description.vertex_shader->is_translated() ||
|
||||
!pipeline_runtime_description.vertex_shader->is_valid()) {
|
||||
++pipelines_vs_translation_missing;
|
||||
XELOGW(
|
||||
"Pipeline cache: VS {:016X} mod {:016X} translation "
|
||||
"missing/invalid",
|
||||
pipeline_description.vertex_shader_hash,
|
||||
pipeline_description.vertex_shader_modification);
|
||||
continue;
|
||||
}
|
||||
D3D12Shader* pixel_shader;
|
||||
@@ -631,6 +648,9 @@ void PipelineCache::InitializeShaderStorage(
|
||||
auto pixel_shader_it =
|
||||
shaders_.find(pipeline_description.pixel_shader_hash);
|
||||
if (pixel_shader_it == shaders_.end()) {
|
||||
++pipelines_ps_not_found;
|
||||
XELOGW("Pipeline cache: PS {:016X} not found in shader storage",
|
||||
pipeline_description.pixel_shader_hash);
|
||||
continue;
|
||||
}
|
||||
pixel_shader = pixel_shader_it->second;
|
||||
@@ -641,6 +661,12 @@ void PipelineCache::InitializeShaderStorage(
|
||||
if (!pipeline_runtime_description.pixel_shader ||
|
||||
!pipeline_runtime_description.pixel_shader->is_translated() ||
|
||||
!pipeline_runtime_description.pixel_shader->is_valid()) {
|
||||
++pipelines_ps_translation_missing;
|
||||
XELOGW(
|
||||
"Pipeline cache: PS {:016X} mod {:016X} translation "
|
||||
"missing/invalid",
|
||||
pipeline_description.pixel_shader_hash,
|
||||
pipeline_description.pixel_shader_modification);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
@@ -666,6 +692,11 @@ void PipelineCache::InitializeShaderStorage(
|
||||
pipeline_description.vertex_shader_modification)
|
||||
.vertex.host_vertex_shader_type));
|
||||
if (!pipeline_runtime_description.root_signature) {
|
||||
++pipelines_root_sig_failed;
|
||||
XELOGW(
|
||||
"Pipeline cache: Root signature failed for VS {:016X} PS {:016X}",
|
||||
pipeline_description.vertex_shader_hash,
|
||||
pipeline_description.pixel_shader_hash);
|
||||
continue;
|
||||
}
|
||||
std::memcpy(&pipeline_runtime_description.description,
|
||||
@@ -704,29 +735,36 @@ void PipelineCache::InitializeShaderStorage(
|
||||
}
|
||||
|
||||
if (!creation_threads_.empty()) {
|
||||
CreateQueuedPipelinesOnProcessorThread();
|
||||
if (creation_threads_.size() > creation_thread_original_count) {
|
||||
{
|
||||
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
||||
creation_threads_shutdown_from_ = creation_thread_original_count;
|
||||
// Assuming the queue is empty because of
|
||||
// CreateQueuedPipelinesOnProcessorThread.
|
||||
}
|
||||
creation_request_cond_.notify_all();
|
||||
while (creation_threads_.size() > creation_thread_original_count) {
|
||||
xe::threading::Wait(creation_threads_.back().get(), false);
|
||||
creation_threads_.pop_back();
|
||||
if (blocking) {
|
||||
// Blocking mode: help drain the queue on this thread, then wait for
|
||||
// background threads to finish.
|
||||
CreateQueuedPipelinesOnProcessorThread();
|
||||
if (creation_threads_.size() > creation_thread_original_count) {
|
||||
{
|
||||
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
||||
creation_threads_shutdown_from_ = creation_thread_original_count;
|
||||
// Assuming the queue is empty because of
|
||||
// CreateQueuedPipelinesOnProcessorThread.
|
||||
}
|
||||
creation_request_cond_.notify_all();
|
||||
while (creation_threads_.size() > creation_thread_original_count) {
|
||||
xe::threading::Wait(creation_threads_.back().get(), false);
|
||||
creation_threads_.pop_back();
|
||||
}
|
||||
{
|
||||
// Cleanup so additional threads can be created later again.
|
||||
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
||||
creation_threads_shutdown_from_ = SIZE_MAX;
|
||||
}
|
||||
}
|
||||
// Wait for any background threads (including original ones) to finish
|
||||
// creating pipelines they may have popped from the queue. This ensures
|
||||
// all cached pipelines are fully created before the game starts,
|
||||
// populating the driver's shader cache.
|
||||
bool await_creation_completion_event;
|
||||
{
|
||||
// Cleanup so additional threads can be created later again.
|
||||
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
||||
creation_threads_shutdown_from_ = SIZE_MAX;
|
||||
// If the invocation is blocking, all the shader storage
|
||||
// initialization is expected to be done before proceeding, to avoid
|
||||
// latency in the command processor after the invocation.
|
||||
await_creation_completion_event =
|
||||
blocking && creation_threads_busy_ != 0;
|
||||
await_creation_completion_event = creation_threads_busy_ != 0;
|
||||
if (await_creation_completion_event) {
|
||||
creation_completion_event_->Reset();
|
||||
creation_completion_set_event_ = true;
|
||||
@@ -736,15 +774,37 @@ void PipelineCache::InitializeShaderStorage(
|
||||
creation_request_cond_.notify_one();
|
||||
xe::threading::Wait(creation_completion_event_.get(), false);
|
||||
}
|
||||
} else {
|
||||
// Non-blocking mode: let background threads handle all pipeline
|
||||
// creation. Store completion callback to be invoked when done.
|
||||
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
||||
if (creation_queue_.empty() && creation_threads_busy_ == 0) {
|
||||
// No work pending - callback will be invoked at end of function.
|
||||
} else {
|
||||
creation_completion_callback_ = std::move(completion_callback);
|
||||
completion_callback =
|
||||
nullptr; // Prevent invocation at end of function
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XELOGGPU(
|
||||
"Created {} graphics pipelines (not including reading the "
|
||||
"descriptions) from the storage in {} milliseconds",
|
||||
pipelines_created,
|
||||
(xe::Clock::QueryHostTickCount() - pipeline_creation_start_) * 1000 /
|
||||
xe::Clock::QueryHostTickFrequency());
|
||||
XELOGI(
|
||||
"Pipeline cache loaded: {} created, {} already exist, {} total stored",
|
||||
pipelines_created, pipelines_already_exist,
|
||||
pipeline_stored_descriptions.size());
|
||||
if (pipelines_vs_not_found || pipelines_vs_translation_missing ||
|
||||
pipelines_ps_not_found || pipelines_ps_translation_missing ||
|
||||
pipelines_root_sig_failed) {
|
||||
XELOGI(
|
||||
"Pipeline cache skipped: {} VS not found, {} VS translation missing, "
|
||||
"{} PS not found, {} PS translation missing, {} root sig failed",
|
||||
pipelines_vs_not_found, pipelines_vs_translation_missing,
|
||||
pipelines_ps_not_found, pipelines_ps_translation_missing,
|
||||
pipelines_root_sig_failed);
|
||||
}
|
||||
XELOGI("Pipeline creation took {} milliseconds",
|
||||
(xe::Clock::QueryHostTickCount() - pipeline_creation_start_) * 1000 /
|
||||
xe::Clock::QueryHostTickFrequency());
|
||||
// If any pipeline descriptions were corrupted (or the whole file has excess
|
||||
// bytes in the end), truncate to the last valid pipeline description.
|
||||
xe::filesystem::TruncateStdioFile(
|
||||
@@ -773,6 +833,13 @@ void PipelineCache::InitializeShaderStorage(
|
||||
xe::threading::Thread::Create({}, [this]() { StorageWriteThread(); });
|
||||
assert_not_null(storage_write_thread_);
|
||||
storage_write_thread_->set_name("D3D12 Storage writer");
|
||||
|
||||
// Invoke completion callback if provided (for blocking mode or when no
|
||||
// background work was needed). For non-blocking mode with background work,
|
||||
// the callback is stored and invoked by CreationThread when done.
|
||||
if (completion_callback) {
|
||||
completion_callback();
|
||||
}
|
||||
}
|
||||
|
||||
void PipelineCache::ShutdownShaderStorage() {
|
||||
@@ -2904,6 +2971,20 @@ void PipelineCache::EnsurePipelineShadersTranslated(
|
||||
dxc_utils, dxc_compiler)) {
|
||||
XELOGE("Failed to translate {} shader {:016X}", shader_type,
|
||||
shader.ucode_data_hash());
|
||||
} else {
|
||||
// Queue shader for storage writing if not already written.
|
||||
// This ensures shaders translated by background threads are
|
||||
// persisted.
|
||||
if (shader_storage_file_ &&
|
||||
shader.ucode_storage_index() != shader_storage_index_) {
|
||||
shader.set_ucode_storage_index(shader_storage_index_);
|
||||
shader_storage_file_flush_needed_ = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_shader_queue_.push_back(&shader);
|
||||
}
|
||||
storage_write_request_cond_.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3496,10 +3577,21 @@ void PipelineCache::CreationThread(size_t thread_index) {
|
||||
std::unique_lock<xe_mutex> lock(creation_request_lock_);
|
||||
if (thread_index >= creation_threads_shutdown_from_ ||
|
||||
creation_queue_.empty()) {
|
||||
if (creation_completion_set_event_ && creation_threads_busy_ == 0) {
|
||||
// Last pipeline in the queue created - signal the event if requested.
|
||||
creation_completion_set_event_ = false;
|
||||
creation_completion_event_->Set();
|
||||
if (creation_threads_busy_ == 0) {
|
||||
// Last pipeline in the queue created.
|
||||
if (creation_completion_set_event_) {
|
||||
// Signal the event if requested (blocking mode).
|
||||
creation_completion_set_event_ = false;
|
||||
creation_completion_event_->Set();
|
||||
}
|
||||
if (creation_completion_callback_) {
|
||||
// Invoke completion callback (non-blocking mode).
|
||||
auto callback = std::move(creation_completion_callback_);
|
||||
creation_completion_callback_ = nullptr;
|
||||
lock.unlock();
|
||||
callback();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
if (thread_index >= creation_threads_shutdown_from_) {
|
||||
// Cleanup thread-local resources.
|
||||
|
||||
@@ -62,8 +62,9 @@ class PipelineCache {
|
||||
// takes a long time, and if it's not, there will be heavy stuttering for the
|
||||
// rest of the execution of the guest).
|
||||
|
||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
||||
uint32_t title_id, bool blocking);
|
||||
void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr);
|
||||
void ShutdownShaderStorage();
|
||||
|
||||
void EndSubmission();
|
||||
@@ -466,6 +467,9 @@ class PipelineCache {
|
||||
// Whether setting the event on completion is queued. Protected with
|
||||
// creation_request_lock_, notify_one creation_request_cond_ when set.
|
||||
bool creation_completion_set_event_ = false;
|
||||
// Callback to invoke when all queued pipelines are created (for non-blocking
|
||||
// initialization). Protected with creation_request_lock_.
|
||||
std::function<void()> creation_completion_callback_;
|
||||
// Creation threads with this index or above need to be shut down as soon as
|
||||
// possible. Protected with creation_request_lock_, notify_all
|
||||
// creation_request_cond_ when set.
|
||||
|
||||
@@ -364,27 +364,38 @@ void GraphicsSystem::InvalidateGpuMemory() {
|
||||
}
|
||||
|
||||
void GraphicsSystem::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback) {
|
||||
if (!cvars::store_shaders) {
|
||||
if (completion_callback) {
|
||||
completion_callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (blocking) {
|
||||
if (command_processor_->is_paused()) {
|
||||
// Safe to run on any thread while the command processor is paused, no
|
||||
// race condition.
|
||||
command_processor_->InitializeShaderStorage(cache_root, title_id, true);
|
||||
command_processor_->InitializeShaderStorage(
|
||||
cache_root, title_id, true, std::move(completion_callback));
|
||||
} else {
|
||||
xe::threading::Fence fence;
|
||||
command_processor_->CallInThread([this, cache_root, title_id, &fence]() {
|
||||
command_processor_->InitializeShaderStorage(cache_root, title_id, true);
|
||||
fence.Signal();
|
||||
});
|
||||
command_processor_->CallInThread(
|
||||
[this, cache_root, title_id, &fence,
|
||||
completion_callback = std::move(completion_callback)]() mutable {
|
||||
command_processor_->InitializeShaderStorage(
|
||||
cache_root, title_id, true, std::move(completion_callback));
|
||||
fence.Signal();
|
||||
});
|
||||
fence.Wait();
|
||||
}
|
||||
} else {
|
||||
command_processor_->CallInThread([this, cache_root, title_id]() {
|
||||
command_processor_->InitializeShaderStorage(cache_root, title_id, false);
|
||||
});
|
||||
command_processor_->CallInThread(
|
||||
[this, cache_root, title_id,
|
||||
completion_callback = std::move(completion_callback)]() mutable {
|
||||
command_processor_->InitializeShaderStorage(
|
||||
cache_root, title_id, false, std::move(completion_callback));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,8 +96,9 @@ class GraphicsSystem {
|
||||
|
||||
void InvalidateGpuMemory();
|
||||
|
||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
||||
uint32_t title_id, bool blocking);
|
||||
void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr);
|
||||
|
||||
void RequestFrameTrace();
|
||||
void BeginTracing();
|
||||
|
||||
Reference in New Issue
Block a user