[GPU] Add vulkan shader disk storage / startup loading
Big refactor of the shader storage to allow both backends to share code
This commit is contained in:
@@ -1648,14 +1648,17 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path,
|
||||
}
|
||||
}
|
||||
|
||||
// Initializing the shader storage in a blocking way so the user doesn't
|
||||
// miss the initial seconds - for instance, sound from an intro video may
|
||||
// start playing before the video can be seen if doing this in parallel with
|
||||
// the main thread.
|
||||
on_shader_storage_initialization(true);
|
||||
graphics_system_->InitializeShaderStorage(cache_root_, title_id_.value(),
|
||||
true);
|
||||
on_shader_storage_initialization(false);
|
||||
// Initialize shader storage asynchronously - pipeline compilation happens in
|
||||
// background while the game goes through its normal startup (loading screens,
|
||||
// intro videos, etc.). With async_shader_compilation enabled, draws are
|
||||
// skipped until pipelines are ready, so this is safe. By the time actual
|
||||
// gameplay starts, most cached pipelines should be compiled.
|
||||
if (graphics_system_) {
|
||||
on_shader_storage_initialization(true);
|
||||
graphics_system_->InitializeShaderStorage(
|
||||
cache_root_, title_id_.value(), false,
|
||||
[this]() { on_shader_storage_initialization(false); });
|
||||
}
|
||||
|
||||
auto main_thread = kernel_state_->LaunchModule(module);
|
||||
if (!main_thread) {
|
||||
|
||||
@@ -172,7 +172,11 @@ void CommandProcessor::Shutdown() {
|
||||
}
|
||||
|
||||
void CommandProcessor::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback) {
|
||||
if (completion_callback) {
|
||||
completion_callback();
|
||||
}
|
||||
}
|
||||
|
||||
void CommandProcessor::RequestFrameTrace(
|
||||
|
||||
@@ -121,8 +121,9 @@ class CommandProcessor {
|
||||
// May be called not only from the command processor thread when the command
|
||||
// processor is paused, and the termination of this function may be explicitly
|
||||
// awaited.
|
||||
virtual void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
||||
uint32_t title_id, bool blocking);
|
||||
virtual void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr);
|
||||
|
||||
virtual void RequestFrameTrace(const std::filesystem::path& root_path);
|
||||
virtual void BeginTracing(const std::filesystem::path& root_path);
|
||||
|
||||
@@ -66,9 +66,12 @@ void D3D12CommandProcessor::ClearCaches() {
|
||||
}
|
||||
|
||||
void D3D12CommandProcessor::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
|
||||
CommandProcessor::InitializeShaderStorage(cache_root, title_id, blocking);
|
||||
pipeline_cache_->InitializeShaderStorage(cache_root, title_id, blocking);
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<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(
|
||||
@@ -2585,7 +2588,12 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type,
|
||||
|
||||
if (cvars::async_shader_compilation) {
|
||||
if (pipeline_cache_->GetD3D12PipelineByHandle(pipeline_handle) == nullptr) {
|
||||
XELOGI("Skipping draw - pipeline not ready");
|
||||
XELOGI(
|
||||
"Skipping draw - pipeline not ready: VS {:016X} mod {:016X}, PS "
|
||||
"{:016X} mod {:016X}",
|
||||
vertex_shader->ucode_data_hash(), vertex_shader_modification.value,
|
||||
pixel_shader ? pixel_shader->ucode_data_hash() : 0,
|
||||
pixel_shader_modification.value);
|
||||
return true;
|
||||
}
|
||||
// Re-fetch root signature now that pipeline is ready.
|
||||
|
||||
@@ -68,8 +68,9 @@ class D3D12CommandProcessor final : public CommandProcessor {
|
||||
|
||||
void ClearCaches() override;
|
||||
|
||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
||||
uint32_t title_id, bool blocking) override;
|
||||
void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr) override;
|
||||
|
||||
void RequestFrameTrace(const std::filesystem::path& root_path) override;
|
||||
|
||||
|
||||
@@ -218,7 +218,6 @@ void PipelineCache::Shutdown() {
|
||||
delete it.second;
|
||||
}
|
||||
shaders_.clear();
|
||||
shader_storage_index_ = 0;
|
||||
|
||||
// Shut down shader translation.
|
||||
ui::d3d12::util::ReleaseAndNull(dxc_compiler_);
|
||||
@@ -227,346 +226,48 @@ void PipelineCache::Shutdown() {
|
||||
}
|
||||
|
||||
void PipelineCache::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback) {
|
||||
ShutdownShaderStorage();
|
||||
|
||||
auto shader_storage_root = cache_root / "shaders";
|
||||
// For files that can be moved between different hosts.
|
||||
// Host PSO blobs - if ever added - should be stored in shaders/local/ (they
|
||||
// currently aren't used because because they may be not very practical -
|
||||
// would need to invalidate them every commit likely, and additional I/O
|
||||
// cost - though D3D's internal validation would possibly be enough to ensure
|
||||
// they are up to date).
|
||||
auto shader_storage_shareable_root = shader_storage_root / "shareable";
|
||||
if (!std::filesystem::exists(shader_storage_shareable_root)) {
|
||||
if (!std::filesystem::create_directories(shader_storage_shareable_root)) {
|
||||
XELOGE(
|
||||
"Failed to create the shareable shader storage directory, persistent "
|
||||
"shader storage will be disabled: {}",
|
||||
shader_storage_shareable_root);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool edram_rov_used = render_target_cache_.GetPath() ==
|
||||
RenderTargetCache::Path::kPixelShaderInterlock;
|
||||
|
||||
// Initialize the pipeline storage stream - read pipeline descriptions and
|
||||
// collect used shader modifications to translate.
|
||||
ShaderStorageWriter<PipelineStoredDescription>::PipelineStorageConfig
|
||||
pipeline_config;
|
||||
pipeline_config.file_suffix =
|
||||
fmt::format(".{}.d3d12.xpso", edram_rov_used ? "rov" : "rtv");
|
||||
pipeline_config.api_magic = edram_rov_used ? 0x4F525844 : 0x54525844;
|
||||
pipeline_config.version =
|
||||
std::max(PipelineDescription::kVersion,
|
||||
DxbcShaderTranslator::Modification::kVersion);
|
||||
|
||||
uint32_t storage_index = storage_writer_.storage_index() + 1;
|
||||
|
||||
std::vector<PipelineStoredDescription> pipeline_stored_descriptions;
|
||||
// <Shader hash, modification bits>.
|
||||
std::set<std::pair<uint64_t, uint64_t>> shader_translations_needed;
|
||||
auto pipeline_storage_file_path =
|
||||
shader_storage_shareable_root /
|
||||
fmt::format("{:08X}.{}.d3d12.xpso", title_id,
|
||||
edram_rov_used ? "rov" : "rtv");
|
||||
pipeline_storage_file_ =
|
||||
xe::filesystem::OpenFile(pipeline_storage_file_path, "a+b");
|
||||
if (!pipeline_storage_file_) {
|
||||
XELOGE(
|
||||
"Failed to open the Direct3D 12 pipeline description storage file for "
|
||||
"writing, persistent shader storage will be disabled: {}",
|
||||
pipeline_storage_file_path);
|
||||
return;
|
||||
}
|
||||
pipeline_storage_file_flush_needed_ = false;
|
||||
// 'XEPS'.
|
||||
constexpr uint32_t pipeline_storage_magic = 0x53504558;
|
||||
// 'DXRO' or 'DXRT'.
|
||||
const uint32_t pipeline_storage_magic_api =
|
||||
edram_rov_used ? 0x4F525844 : 0x54525844;
|
||||
const uint32_t pipeline_storage_version_swapped =
|
||||
xe::byte_swap(std::max(PipelineDescription::kVersion,
|
||||
DxbcShaderTranslator::Modification::kVersion));
|
||||
struct {
|
||||
uint32_t magic;
|
||||
uint32_t magic_api;
|
||||
uint32_t version_swapped;
|
||||
} pipeline_storage_file_header;
|
||||
if (fread(&pipeline_storage_file_header, sizeof(pipeline_storage_file_header),
|
||||
1, pipeline_storage_file_) &&
|
||||
pipeline_storage_file_header.magic == pipeline_storage_magic &&
|
||||
pipeline_storage_file_header.magic_api == pipeline_storage_magic_api &&
|
||||
pipeline_storage_file_header.version_swapped ==
|
||||
pipeline_storage_version_swapped) {
|
||||
xe::filesystem::Seek(pipeline_storage_file_, 0, SEEK_END);
|
||||
int64_t pipeline_storage_told_end =
|
||||
xe::filesystem::Tell(pipeline_storage_file_);
|
||||
size_t pipeline_storage_told_count =
|
||||
size_t(pipeline_storage_told_end >=
|
||||
int64_t(sizeof(pipeline_storage_file_header))
|
||||
? (uint64_t(pipeline_storage_told_end) -
|
||||
sizeof(pipeline_storage_file_header)) /
|
||||
sizeof(PipelineStoredDescription)
|
||||
: 0);
|
||||
if (pipeline_storage_told_count &&
|
||||
xe::filesystem::Seek(pipeline_storage_file_,
|
||||
int64_t(sizeof(pipeline_storage_file_header)),
|
||||
SEEK_SET)) {
|
||||
pipeline_stored_descriptions.resize(pipeline_storage_told_count);
|
||||
pipeline_stored_descriptions.resize(
|
||||
fread(pipeline_stored_descriptions.data(),
|
||||
sizeof(PipelineStoredDescription), pipeline_storage_told_count,
|
||||
pipeline_storage_file_));
|
||||
size_t pipeline_storage_read_count = pipeline_stored_descriptions.size();
|
||||
for (size_t i = 0; i < pipeline_storage_read_count; ++i) {
|
||||
const PipelineStoredDescription& pipeline_stored_description =
|
||||
pipeline_stored_descriptions[i];
|
||||
// Validate file integrity, stop and truncate the stream if data is
|
||||
// corrupted.
|
||||
if (XXH3_64bits(&pipeline_stored_description.description,
|
||||
sizeof(pipeline_stored_description.description)) !=
|
||||
pipeline_stored_description.description_hash) {
|
||||
pipeline_stored_descriptions.resize(i);
|
||||
break;
|
||||
}
|
||||
// TODO(Triang3l): On Vulkan, skip pipelines requiring unsupported
|
||||
// device features (to keep the cache files mostly shareable across
|
||||
// devices).
|
||||
// Mark the shader modifications as needed for translation.
|
||||
shader_translations_needed.emplace(
|
||||
pipeline_stored_description.description.vertex_shader_hash,
|
||||
pipeline_stored_description.description.vertex_shader_modification);
|
||||
if (pipeline_stored_description.description.pixel_shader_hash) {
|
||||
shader_translations_needed.emplace(
|
||||
pipeline_stored_description.description.pixel_shader_hash,
|
||||
pipeline_stored_description.description
|
||||
.pixel_shader_modification);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t logical_processor_count = xe::threading::logical_processor_count();
|
||||
if (!logical_processor_count) {
|
||||
// Pick some reasonable amount if couldn't determine the number of cores.
|
||||
logical_processor_count = 6;
|
||||
}
|
||||
|
||||
// Initialize the Xenos shader storage stream.
|
||||
uint64_t shader_storage_initialization_start =
|
||||
xe::Clock::QueryHostTickCount();
|
||||
auto shader_storage_file_path =
|
||||
shader_storage_shareable_root / fmt::format("{:08X}.xsh", title_id);
|
||||
shader_storage_file_ =
|
||||
xe::filesystem::OpenFile(shader_storage_file_path, "a+b");
|
||||
if (!shader_storage_file_) {
|
||||
XELOGE(
|
||||
"Failed to open the guest shader storage file for writing, persistent "
|
||||
"shader storage will be disabled: {}",
|
||||
shader_storage_file_path);
|
||||
fclose(pipeline_storage_file_);
|
||||
pipeline_storage_file_ = nullptr;
|
||||
return;
|
||||
}
|
||||
++shader_storage_index_;
|
||||
shader_storage_file_flush_needed_ = false;
|
||||
struct {
|
||||
uint32_t magic;
|
||||
uint32_t version_swapped;
|
||||
} shader_storage_file_header;
|
||||
// 'XESH'.
|
||||
constexpr uint32_t shader_storage_magic = 0x48534558;
|
||||
if (fread(&shader_storage_file_header, sizeof(shader_storage_file_header), 1,
|
||||
shader_storage_file_) &&
|
||||
shader_storage_file_header.magic == shader_storage_magic &&
|
||||
xe::byte_swap(shader_storage_file_header.version_swapped) ==
|
||||
ShaderStoredHeader::kVersion) {
|
||||
uint64_t shader_storage_valid_bytes = sizeof(shader_storage_file_header);
|
||||
// Load and translate shaders written by previous Xenia executions until the
|
||||
// end of the file or until a corrupted one is detected.
|
||||
ShaderStoredHeader shader_header;
|
||||
std::vector<uint32_t> ucode_dwords;
|
||||
ucode_dwords.reserve(0xFFFF);
|
||||
size_t shaders_translated = 0;
|
||||
|
||||
// Threads overlapping file reading.
|
||||
std::mutex shaders_translation_thread_mutex;
|
||||
std::condition_variable shaders_translation_thread_cond;
|
||||
std::deque<D3D12Shader*> shaders_to_translate;
|
||||
size_t shader_translation_threads_busy = 0;
|
||||
bool shader_translation_threads_shutdown = false;
|
||||
std::mutex shaders_failed_to_translate_mutex;
|
||||
std::vector<D3D12Shader::D3D12Translation*> shaders_failed_to_translate;
|
||||
auto shader_translation_thread_function = [&]() {
|
||||
const ui::d3d12::D3D12Provider& provider =
|
||||
command_processor_.GetD3D12Provider();
|
||||
StringBuffer ucode_disasm_buffer;
|
||||
DxbcShaderTranslator translator(
|
||||
provider.GetAdapterVendorID(), bindless_resources_used_,
|
||||
edram_rov_used,
|
||||
!(edram_rov_used ||
|
||||
render_target_cache_.gamma_render_target_as_unorm16()),
|
||||
render_target_cache_.msaa_2x_supported(),
|
||||
render_target_cache_.draw_resolution_scale_x(),
|
||||
render_target_cache_.draw_resolution_scale_y(),
|
||||
provider.GetGraphicsAnalysis() != nullptr);
|
||||
// If needed and possible, create objects needed for DXIL conversion and
|
||||
// disassembly on this thread.
|
||||
IDxbcConverter* dxbc_converter = nullptr;
|
||||
IDxcUtils* dxc_utils = nullptr;
|
||||
IDxcCompiler* dxc_compiler = nullptr;
|
||||
if (cvars::d3d12_dxbc_disasm_dxilconv && dxbc_converter_ && dxc_utils_ &&
|
||||
dxc_compiler_) {
|
||||
provider.DxbcConverterCreateInstance(CLSID_DxbcConverter,
|
||||
IID_PPV_ARGS(&dxbc_converter));
|
||||
provider.DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&dxc_utils));
|
||||
provider.DxcCreateInstance(CLSID_DxcCompiler,
|
||||
IID_PPV_ARGS(&dxc_compiler));
|
||||
}
|
||||
for (;;) {
|
||||
D3D12Shader* shader_to_translate;
|
||||
for (;;) {
|
||||
std::unique_lock<std::mutex> lock(shaders_translation_thread_mutex);
|
||||
if (shaders_to_translate.empty()) {
|
||||
if (shader_translation_threads_shutdown) {
|
||||
return;
|
||||
if (!storage_writer_.InitializeShaderStorage(
|
||||
cache_root, title_id, pipeline_config,
|
||||
// Shader load callback.
|
||||
[&](xenos::ShaderType type, const uint32_t* ucode_dwords,
|
||||
uint32_t ucode_dword_count, uint64_t ucode_data_hash) {
|
||||
D3D12Shader* shader = LoadShader(
|
||||
type, ucode_dwords, ucode_dword_count, ucode_data_hash);
|
||||
if (shader->ucode_storage_index() == storage_index) {
|
||||
return true; // Already loaded.
|
||||
}
|
||||
shaders_translation_thread_cond.wait(lock);
|
||||
continue;
|
||||
}
|
||||
shader_to_translate = shaders_to_translate.front();
|
||||
shaders_to_translate.pop_front();
|
||||
++shader_translation_threads_busy;
|
||||
break;
|
||||
}
|
||||
if (!shader_to_translate->is_ucode_analyzed()) {
|
||||
shader_to_translate->AnalyzeUcode(ucode_disasm_buffer);
|
||||
}
|
||||
// Translate each needed modification on this thread after performing
|
||||
// modification-independent analysis of the whole shader.
|
||||
uint64_t ucode_data_hash = shader_to_translate->ucode_data_hash();
|
||||
for (auto modification_it = shader_translations_needed.lower_bound(
|
||||
std::make_pair(ucode_data_hash, uint64_t(0)));
|
||||
modification_it != shader_translations_needed.end() &&
|
||||
modification_it->first == ucode_data_hash;
|
||||
++modification_it) {
|
||||
D3D12Shader::D3D12Translation* translation =
|
||||
static_cast<D3D12Shader::D3D12Translation*>(
|
||||
shader_to_translate->GetOrCreateTranslation(
|
||||
modification_it->second));
|
||||
// Only try (and delete in case of failure) if it's a new translation.
|
||||
// If it's a shader previously encountered in the game, translation of
|
||||
// which has failed, and the shader storage is loaded later, keep it
|
||||
// this way not to try to translate it again.
|
||||
if (!translation->is_translated() &&
|
||||
!TranslateAnalyzedShader(translator, *translation, dxbc_converter,
|
||||
dxc_utils, dxc_compiler)) {
|
||||
std::lock_guard<std::mutex> lock(shaders_failed_to_translate_mutex);
|
||||
shaders_failed_to_translate.push_back(translation);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shaders_translation_thread_mutex);
|
||||
--shader_translation_threads_busy;
|
||||
}
|
||||
}
|
||||
if (dxc_compiler) {
|
||||
dxc_compiler->Release();
|
||||
}
|
||||
if (dxc_utils) {
|
||||
dxc_utils->Release();
|
||||
}
|
||||
if (dxbc_converter) {
|
||||
dxbc_converter->Release();
|
||||
}
|
||||
};
|
||||
std::vector<std::unique_ptr<xe::threading::Thread>>
|
||||
shader_translation_threads;
|
||||
|
||||
while (true) {
|
||||
if (!fread(&shader_header, sizeof(shader_header), 1,
|
||||
shader_storage_file_)) {
|
||||
break;
|
||||
}
|
||||
size_t ucode_byte_count =
|
||||
shader_header.ucode_dword_count * sizeof(uint32_t);
|
||||
ucode_dwords.resize(shader_header.ucode_dword_count);
|
||||
if (shader_header.ucode_dword_count &&
|
||||
!fread(ucode_dwords.data(), ucode_byte_count, 1,
|
||||
shader_storage_file_)) {
|
||||
break;
|
||||
}
|
||||
uint64_t ucode_data_hash =
|
||||
XXH3_64bits(ucode_dwords.data(), ucode_byte_count);
|
||||
if (shader_header.ucode_data_hash != ucode_data_hash) {
|
||||
// Validation failed.
|
||||
break;
|
||||
}
|
||||
shader_storage_valid_bytes += sizeof(shader_header) + ucode_byte_count;
|
||||
D3D12Shader* shader =
|
||||
LoadShader(shader_header.type, ucode_dwords.data(),
|
||||
shader_header.ucode_dword_count, ucode_data_hash);
|
||||
if (shader->ucode_storage_index() == shader_storage_index_) {
|
||||
// Appeared twice in this file for some reason - skip, otherwise race
|
||||
// condition will be caused by translating twice in parallel.
|
||||
continue;
|
||||
}
|
||||
// Loaded from the current storage - don't write again.
|
||||
shader->set_ucode_storage_index(shader_storage_index_);
|
||||
// Create new threads if the currently existing threads can't keep up
|
||||
// with file reading, but not more than the number of logical processors
|
||||
// minus one.
|
||||
size_t shader_translation_threads_needed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shaders_translation_thread_mutex);
|
||||
shader_translation_threads_needed =
|
||||
std::min(shader_translation_threads_busy +
|
||||
shaders_to_translate.size() + size_t(1),
|
||||
logical_processor_count - size_t(1));
|
||||
}
|
||||
while (shader_translation_threads.size() <
|
||||
shader_translation_threads_needed) {
|
||||
auto thread = xe::threading::Thread::Create(
|
||||
{}, shader_translation_thread_function);
|
||||
assert_not_null(thread);
|
||||
thread->set_name("Shader Translation");
|
||||
shader_translation_threads.push_back(std::move(thread));
|
||||
}
|
||||
// Request ucode information gathering and translation of all the needed
|
||||
// shaders.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shaders_translation_thread_mutex);
|
||||
shaders_to_translate.push_back(shader);
|
||||
}
|
||||
shaders_translation_thread_cond.notify_one();
|
||||
++shaders_translated;
|
||||
}
|
||||
if (!shader_translation_threads.empty()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shaders_translation_thread_mutex);
|
||||
shader_translation_threads_shutdown = true;
|
||||
}
|
||||
shaders_translation_thread_cond.notify_all();
|
||||
for (auto& shader_translation_thread : shader_translation_threads) {
|
||||
xe::threading::Wait(shader_translation_thread.get(), false);
|
||||
}
|
||||
shader_translation_threads.clear();
|
||||
for (D3D12Shader::D3D12Translation* translation :
|
||||
shaders_failed_to_translate) {
|
||||
D3D12Shader* shader = static_cast<D3D12Shader*>(&translation->shader());
|
||||
shader->DestroyTranslation(translation->modification());
|
||||
if (shader->translations().empty()) {
|
||||
shaders_.erase(shader->ucode_data_hash());
|
||||
delete shader;
|
||||
}
|
||||
}
|
||||
}
|
||||
XELOGGPU("Translated {} shaders from the storage in {} milliseconds",
|
||||
shaders_translated,
|
||||
(xe::Clock::QueryHostTickCount() -
|
||||
shader_storage_initialization_start) *
|
||||
1000 / xe::Clock::QueryHostTickFrequency());
|
||||
xe::filesystem::TruncateStdioFile(shader_storage_file_,
|
||||
shader_storage_valid_bytes);
|
||||
} else {
|
||||
xe::filesystem::TruncateStdioFile(shader_storage_file_, 0);
|
||||
shader_storage_file_header.magic = shader_storage_magic;
|
||||
shader_storage_file_header.version_swapped =
|
||||
xe::byte_swap(ShaderStoredHeader::kVersion);
|
||||
fwrite(&shader_storage_file_header, sizeof(shader_storage_file_header), 1,
|
||||
shader_storage_file_);
|
||||
shader->set_ucode_storage_index(storage_index);
|
||||
return true;
|
||||
},
|
||||
// Shader translate callback.
|
||||
[this, edram_rov_used](const std::set<std::pair<uint64_t, uint64_t>>
|
||||
& translations_needed) {
|
||||
TranslateShadersForStorage(translations_needed, edram_rov_used);
|
||||
},
|
||||
pipeline_stored_descriptions)) {
|
||||
return;
|
||||
}
|
||||
shader_storage_file_flush_needed_ = false;
|
||||
pipeline_storage_file_flush_needed_ = false;
|
||||
|
||||
// Create the pipelines.
|
||||
if (!pipeline_stored_descriptions.empty()) {
|
||||
@@ -574,12 +275,16 @@ void PipelineCache::InitializeShaderStorage(
|
||||
|
||||
// Launch additional creation threads to use all cores to create
|
||||
// pipelines faster. Will also be using the main thread, so minus 1.
|
||||
size_t logical_processor_count = xe::threading::logical_processor_count();
|
||||
if (!logical_processor_count) {
|
||||
logical_processor_count = 6;
|
||||
}
|
||||
size_t creation_thread_original_count = creation_threads_.size();
|
||||
size_t creation_thread_needed_count = std::max(
|
||||
std::min(pipeline_stored_descriptions.size(), logical_processor_count) -
|
||||
size_t(1),
|
||||
creation_thread_original_count);
|
||||
while (creation_threads_.size() < creation_thread_original_count) {
|
||||
while (creation_threads_.size() < creation_thread_needed_count) {
|
||||
size_t creation_thread_index = creation_threads_.size();
|
||||
std::unique_ptr<xe::threading::Thread> creation_thread =
|
||||
xe::threading::Thread::Create({}, [this, creation_thread_index]() {
|
||||
@@ -591,6 +296,12 @@ void PipelineCache::InitializeShaderStorage(
|
||||
}
|
||||
|
||||
size_t pipelines_created = 0;
|
||||
size_t pipelines_already_exist = 0;
|
||||
size_t pipelines_vs_not_found = 0;
|
||||
size_t pipelines_vs_translation_missing = 0;
|
||||
size_t pipelines_ps_not_found = 0;
|
||||
size_t pipelines_ps_translation_missing = 0;
|
||||
size_t pipelines_root_sig_failed = 0;
|
||||
for (const PipelineStoredDescription& pipeline_stored_description :
|
||||
pipeline_stored_descriptions) {
|
||||
const PipelineDescription& pipeline_description =
|
||||
@@ -610,6 +321,7 @@ void PipelineCache::InitializeShaderStorage(
|
||||
}
|
||||
}
|
||||
if (pipeline_found) {
|
||||
++pipelines_already_exist;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -617,6 +329,9 @@ void PipelineCache::InitializeShaderStorage(
|
||||
auto vertex_shader_it =
|
||||
shaders_.find(pipeline_description.vertex_shader_hash);
|
||||
if (vertex_shader_it == shaders_.end()) {
|
||||
++pipelines_vs_not_found;
|
||||
XELOGW("Pipeline cache: VS {:016X} not found in shader storage",
|
||||
pipeline_description.vertex_shader_hash);
|
||||
continue;
|
||||
}
|
||||
D3D12Shader* vertex_shader = vertex_shader_it->second;
|
||||
@@ -627,6 +342,12 @@ void PipelineCache::InitializeShaderStorage(
|
||||
if (!pipeline_runtime_description.vertex_shader ||
|
||||
!pipeline_runtime_description.vertex_shader->is_translated() ||
|
||||
!pipeline_runtime_description.vertex_shader->is_valid()) {
|
||||
++pipelines_vs_translation_missing;
|
||||
XELOGW(
|
||||
"Pipeline cache: VS {:016X} mod {:016X} translation "
|
||||
"missing/invalid",
|
||||
pipeline_description.vertex_shader_hash,
|
||||
pipeline_description.vertex_shader_modification);
|
||||
continue;
|
||||
}
|
||||
D3D12Shader* pixel_shader;
|
||||
@@ -634,6 +355,9 @@ void PipelineCache::InitializeShaderStorage(
|
||||
auto pixel_shader_it =
|
||||
shaders_.find(pipeline_description.pixel_shader_hash);
|
||||
if (pixel_shader_it == shaders_.end()) {
|
||||
++pipelines_ps_not_found;
|
||||
XELOGW("Pipeline cache: PS {:016X} not found in shader storage",
|
||||
pipeline_description.pixel_shader_hash);
|
||||
continue;
|
||||
}
|
||||
pixel_shader = pixel_shader_it->second;
|
||||
@@ -644,6 +368,12 @@ void PipelineCache::InitializeShaderStorage(
|
||||
if (!pipeline_runtime_description.pixel_shader ||
|
||||
!pipeline_runtime_description.pixel_shader->is_translated() ||
|
||||
!pipeline_runtime_description.pixel_shader->is_valid()) {
|
||||
++pipelines_ps_translation_missing;
|
||||
XELOGW(
|
||||
"Pipeline cache: PS {:016X} mod {:016X} translation "
|
||||
"missing/invalid",
|
||||
pipeline_description.pixel_shader_hash,
|
||||
pipeline_description.pixel_shader_modification);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
@@ -669,6 +399,11 @@ void PipelineCache::InitializeShaderStorage(
|
||||
pipeline_description.vertex_shader_modification)
|
||||
.vertex.host_vertex_shader_type));
|
||||
if (!pipeline_runtime_description.root_signature) {
|
||||
++pipelines_root_sig_failed;
|
||||
XELOGW(
|
||||
"Pipeline cache: Root signature failed for VS {:016X} PS {:016X}",
|
||||
pipeline_description.vertex_shader_hash,
|
||||
pipeline_description.pixel_shader_hash);
|
||||
continue;
|
||||
}
|
||||
std::memcpy(&pipeline_runtime_description.description,
|
||||
@@ -707,29 +442,36 @@ void PipelineCache::InitializeShaderStorage(
|
||||
}
|
||||
|
||||
if (!creation_threads_.empty()) {
|
||||
CreateQueuedPipelinesOnProcessorThread();
|
||||
if (creation_threads_.size() > creation_thread_original_count) {
|
||||
{
|
||||
std::lock_guard<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;
|
||||
@@ -739,87 +481,62 @@ void PipelineCache::InitializeShaderStorage(
|
||||
creation_request_cond_.notify_one();
|
||||
xe::threading::Wait(creation_completion_event_.get(), false);
|
||||
}
|
||||
} else {
|
||||
// Non-blocking mode: let background threads handle all pipeline
|
||||
// creation. Store completion callback to be invoked when done.
|
||||
std::lock_guard<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());
|
||||
// If any pipeline descriptions were corrupted (or the whole file has excess
|
||||
// bytes in the end), truncate to the last valid pipeline description.
|
||||
xe::filesystem::TruncateStdioFile(
|
||||
pipeline_storage_file_,
|
||||
uint64_t(sizeof(pipeline_storage_file_header) +
|
||||
sizeof(PipelineStoredDescription) *
|
||||
pipeline_stored_descriptions.size()));
|
||||
} else {
|
||||
xe::filesystem::TruncateStdioFile(pipeline_storage_file_, 0);
|
||||
pipeline_storage_file_header.magic = pipeline_storage_magic;
|
||||
pipeline_storage_file_header.magic_api = pipeline_storage_magic_api;
|
||||
pipeline_storage_file_header.version_swapped =
|
||||
pipeline_storage_version_swapped;
|
||||
fwrite(&pipeline_storage_file_header, sizeof(pipeline_storage_file_header),
|
||||
1, pipeline_storage_file_);
|
||||
XELOGI(
|
||||
"Pipeline cache loaded: {} created, {} already exist, {} total stored",
|
||||
pipelines_created, pipelines_already_exist,
|
||||
pipeline_stored_descriptions.size());
|
||||
if (pipelines_vs_not_found || pipelines_vs_translation_missing ||
|
||||
pipelines_ps_not_found || pipelines_ps_translation_missing ||
|
||||
pipelines_root_sig_failed) {
|
||||
XELOGI(
|
||||
"Pipeline cache skipped: {} VS not found, {} VS translation missing, "
|
||||
"{} PS not found, {} PS translation missing, {} root sig failed",
|
||||
pipelines_vs_not_found, pipelines_vs_translation_missing,
|
||||
pipelines_ps_not_found, pipelines_ps_translation_missing,
|
||||
pipelines_root_sig_failed);
|
||||
}
|
||||
XELOGI("Pipeline creation took {} milliseconds",
|
||||
(xe::Clock::QueryHostTickCount() - pipeline_creation_start_) * 1000 /
|
||||
xe::Clock::QueryHostTickFrequency());
|
||||
}
|
||||
|
||||
shader_storage_cache_root_ = cache_root;
|
||||
shader_storage_title_id_ = title_id;
|
||||
|
||||
// Start the storage writing thread.
|
||||
storage_write_flush_shaders_ = false;
|
||||
storage_write_flush_pipelines_ = false;
|
||||
storage_write_thread_shutdown_ = false;
|
||||
storage_write_thread_ =
|
||||
xe::threading::Thread::Create({}, [this]() { StorageWriteThread(); });
|
||||
assert_not_null(storage_write_thread_);
|
||||
storage_write_thread_->set_name("D3D12 Storage writer");
|
||||
// Invoke completion callback if provided (for blocking mode or when no
|
||||
// background work was needed). For non-blocking mode with background work,
|
||||
// the callback is stored and invoked by CreationThread when done.
|
||||
if (completion_callback) {
|
||||
completion_callback();
|
||||
}
|
||||
}
|
||||
|
||||
void PipelineCache::ShutdownShaderStorage() {
|
||||
if (storage_write_thread_) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_thread_shutdown_ = true;
|
||||
}
|
||||
storage_write_request_cond_.notify_all();
|
||||
xe::threading::Wait(storage_write_thread_.get(), false);
|
||||
storage_write_thread_.reset();
|
||||
}
|
||||
storage_write_shader_queue_.clear();
|
||||
storage_write_pipeline_queue_.clear();
|
||||
|
||||
if (pipeline_storage_file_) {
|
||||
fclose(pipeline_storage_file_);
|
||||
pipeline_storage_file_ = nullptr;
|
||||
pipeline_storage_file_flush_needed_ = false;
|
||||
}
|
||||
|
||||
if (shader_storage_file_) {
|
||||
fclose(shader_storage_file_);
|
||||
shader_storage_file_ = nullptr;
|
||||
shader_storage_file_flush_needed_ = false;
|
||||
}
|
||||
|
||||
shader_storage_cache_root_.clear();
|
||||
// Shut down the storage writer (closes files, stops write thread).
|
||||
storage_writer_.ShutdownShaderStorage();
|
||||
shader_storage_file_flush_needed_ = false;
|
||||
pipeline_storage_file_flush_needed_ = false;
|
||||
shader_storage_title_id_ = 0;
|
||||
}
|
||||
|
||||
void PipelineCache::EndSubmission() {
|
||||
if (shader_storage_file_flush_needed_ ||
|
||||
pipeline_storage_file_flush_needed_) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
if (shader_storage_file_flush_needed_) {
|
||||
storage_write_flush_shaders_ = true;
|
||||
}
|
||||
if (pipeline_storage_file_flush_needed_) {
|
||||
storage_write_flush_pipelines_ = true;
|
||||
}
|
||||
}
|
||||
storage_write_request_cond_.notify_one();
|
||||
storage_writer_.RequestFlush(shader_storage_file_flush_needed_,
|
||||
pipeline_storage_file_flush_needed_);
|
||||
shader_storage_file_flush_needed_ = false;
|
||||
pipeline_storage_file_flush_needed_ = false;
|
||||
}
|
||||
@@ -1035,16 +752,11 @@ bool PipelineCache::ConfigurePipeline(
|
||||
XELOGE("Failed to translate the vertex shader!");
|
||||
return false;
|
||||
}
|
||||
if (shader_storage_file_ && vertex_shader->shader().ucode_storage_index() !=
|
||||
shader_storage_index_) {
|
||||
vertex_shader->shader().set_ucode_storage_index(shader_storage_index_);
|
||||
assert_not_null(storage_write_thread_);
|
||||
if (storage_writer_.is_active() &&
|
||||
vertex_shader->shader().try_set_ucode_storage_index(
|
||||
storage_writer_.storage_index())) {
|
||||
shader_storage_file_flush_needed_ = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_shader_queue_.push_back(&vertex_shader->shader());
|
||||
}
|
||||
storage_write_request_cond_.notify_all();
|
||||
storage_writer_.QueueShaderWrite(&vertex_shader->shader());
|
||||
}
|
||||
}
|
||||
if (!use_async && !vertex_shader->is_valid()) {
|
||||
@@ -1066,17 +778,11 @@ bool PipelineCache::ConfigurePipeline(
|
||||
XELOGE("Failed to translate the pixel shader!");
|
||||
return false;
|
||||
}
|
||||
if (shader_storage_file_ &&
|
||||
pixel_shader->shader().ucode_storage_index() !=
|
||||
shader_storage_index_) {
|
||||
pixel_shader->shader().set_ucode_storage_index(shader_storage_index_);
|
||||
assert_not_null(storage_write_thread_);
|
||||
if (storage_writer_.is_active() &&
|
||||
pixel_shader->shader().try_set_ucode_storage_index(
|
||||
storage_writer_.storage_index())) {
|
||||
shader_storage_file_flush_needed_ = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_shader_queue_.push_back(&pixel_shader->shader());
|
||||
}
|
||||
storage_write_request_cond_.notify_all();
|
||||
storage_writer_.QueueShaderWrite(&pixel_shader->shader());
|
||||
}
|
||||
}
|
||||
if (!pixel_shader->is_valid()) {
|
||||
@@ -1145,19 +851,13 @@ bool PipelineCache::ConfigurePipeline(
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
if (pipeline_storage_file_) {
|
||||
assert_not_null(storage_write_thread_);
|
||||
if (storage_writer_.is_active()) {
|
||||
pipeline_storage_file_flush_needed_ = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_pipeline_queue_.emplace_back();
|
||||
PipelineStoredDescription& stored_description =
|
||||
storage_write_pipeline_queue_.back();
|
||||
stored_description.description_hash = hash;
|
||||
std::memcpy(&stored_description.description, &description,
|
||||
sizeof(description));
|
||||
}
|
||||
storage_write_request_cond_.notify_all();
|
||||
PipelineStoredDescription stored_description;
|
||||
stored_description.description_hash = hash;
|
||||
std::memcpy(&stored_description.description, &description,
|
||||
sizeof(description));
|
||||
storage_writer_.QueuePipelineWrite(stored_description);
|
||||
}
|
||||
|
||||
current_pipeline_ = new_pipeline;
|
||||
@@ -1363,6 +1063,127 @@ bool PipelineCache::TranslateAnalyzedShader(
|
||||
return translation.is_valid();
|
||||
}
|
||||
|
||||
void PipelineCache::TranslateShadersForStorage(
|
||||
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed,
|
||||
bool edram_rov_used) {
|
||||
uint64_t translation_start = xe::Clock::QueryHostTickCount();
|
||||
|
||||
std::vector<D3D12Shader*> shaders_to_translate_list;
|
||||
shaders_to_translate_list.reserve(shaders_.size());
|
||||
for (auto& shader_pair : shaders_) {
|
||||
D3D12Shader* shader = shader_pair.second;
|
||||
uint64_t ucode_data_hash = shader->ucode_data_hash();
|
||||
if (translations_needed.lower_bound(
|
||||
std::make_pair(ucode_data_hash, uint64_t(0))) !=
|
||||
translations_needed.upper_bound(
|
||||
std::make_pair(ucode_data_hash, UINT64_MAX))) {
|
||||
shaders_to_translate_list.push_back(shader);
|
||||
}
|
||||
}
|
||||
|
||||
if (shaders_to_translate_list.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::atomic<size_t> translation_index{0};
|
||||
std::mutex shaders_failed_to_translate_mutex;
|
||||
std::vector<D3D12Shader::D3D12Translation*> shaders_failed_to_translate;
|
||||
|
||||
auto translate_function = [&, this]() {
|
||||
const ui::d3d12::D3D12Provider& provider =
|
||||
command_processor_.GetD3D12Provider();
|
||||
StringBuffer ucode_disasm_buffer;
|
||||
DxbcShaderTranslator translator(
|
||||
provider.GetAdapterVendorID(), bindless_resources_used_, edram_rov_used,
|
||||
!(edram_rov_used ||
|
||||
render_target_cache_.gamma_render_target_as_unorm16()),
|
||||
render_target_cache_.msaa_2x_supported(),
|
||||
render_target_cache_.draw_resolution_scale_x(),
|
||||
render_target_cache_.draw_resolution_scale_y(),
|
||||
provider.GetGraphicsAnalysis() != nullptr);
|
||||
// DXIL conversion objects.
|
||||
IDxbcConverter* dxbc_converter = nullptr;
|
||||
IDxcUtils* dxc_utils = nullptr;
|
||||
IDxcCompiler* dxc_compiler = nullptr;
|
||||
if (cvars::d3d12_dxbc_disasm_dxilconv && dxbc_converter_ && dxc_utils_ &&
|
||||
dxc_compiler_) {
|
||||
provider.DxbcConverterCreateInstance(CLSID_DxbcConverter,
|
||||
IID_PPV_ARGS(&dxbc_converter));
|
||||
provider.DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&dxc_utils));
|
||||
provider.DxcCreateInstance(CLSID_DxcCompiler,
|
||||
IID_PPV_ARGS(&dxc_compiler));
|
||||
}
|
||||
|
||||
while (true) {
|
||||
size_t index = translation_index.fetch_add(1);
|
||||
if (index >= shaders_to_translate_list.size()) {
|
||||
break;
|
||||
}
|
||||
D3D12Shader* shader = shaders_to_translate_list[index];
|
||||
if (!shader->is_ucode_analyzed()) {
|
||||
shader->AnalyzeUcode(ucode_disasm_buffer);
|
||||
}
|
||||
uint64_t ucode_data_hash = shader->ucode_data_hash();
|
||||
for (auto modification_it = translations_needed.lower_bound(
|
||||
std::make_pair(ucode_data_hash, uint64_t(0)));
|
||||
modification_it != translations_needed.end() &&
|
||||
modification_it->first == ucode_data_hash;
|
||||
++modification_it) {
|
||||
D3D12Shader::D3D12Translation* translation =
|
||||
static_cast<D3D12Shader::D3D12Translation*>(
|
||||
shader->GetOrCreateTranslation(modification_it->second));
|
||||
if (!translation->is_translated() &&
|
||||
!TranslateAnalyzedShader(translator, *translation, dxbc_converter,
|
||||
dxc_utils, dxc_compiler)) {
|
||||
std::lock_guard<std::mutex> lock(shaders_failed_to_translate_mutex);
|
||||
shaders_failed_to_translate.push_back(translation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dxc_compiler) dxc_compiler->Release();
|
||||
if (dxc_utils) dxc_utils->Release();
|
||||
if (dxbc_converter) dxbc_converter->Release();
|
||||
};
|
||||
|
||||
size_t logical_processor_count = xe::threading::logical_processor_count();
|
||||
if (!logical_processor_count) {
|
||||
logical_processor_count = 6;
|
||||
}
|
||||
size_t thread_count = std::min(logical_processor_count - size_t(1),
|
||||
shaders_to_translate_list.size());
|
||||
std::vector<std::unique_ptr<xe::threading::Thread>> translation_threads;
|
||||
for (size_t i = 0; i < thread_count; ++i) {
|
||||
auto thread = xe::threading::Thread::Create({}, translate_function);
|
||||
if (thread) {
|
||||
thread->set_name("Shader Translation");
|
||||
translation_threads.push_back(std::move(thread));
|
||||
}
|
||||
}
|
||||
|
||||
// Main thread also participates.
|
||||
translate_function();
|
||||
|
||||
for (auto& thread : translation_threads) {
|
||||
xe::threading::Wait(thread.get(), false);
|
||||
}
|
||||
|
||||
for (D3D12Shader::D3D12Translation* translation :
|
||||
shaders_failed_to_translate) {
|
||||
D3D12Shader* shader = static_cast<D3D12Shader*>(&translation->shader());
|
||||
shader->DestroyTranslation(translation->modification());
|
||||
if (shader->translations().empty()) {
|
||||
shaders_.erase(shader->ucode_data_hash());
|
||||
delete shader;
|
||||
}
|
||||
}
|
||||
|
||||
XELOGI("Translated {} shaders in {} ms",
|
||||
shaders_to_translate_list.size() - shaders_failed_to_translate.size(),
|
||||
(xe::Clock::QueryHostTickCount() - translation_start) * 1000 /
|
||||
xe::Clock::QueryHostTickFrequency());
|
||||
}
|
||||
|
||||
bool PipelineCache::GetCurrentStateDescription(
|
||||
D3D12Shader::D3D12Translation* vertex_shader,
|
||||
D3D12Shader::D3D12Translation* pixel_shader,
|
||||
@@ -2901,6 +2722,13 @@ void PipelineCache::EnsurePipelineShadersTranslated(
|
||||
dxc_utils, dxc_compiler)) {
|
||||
XELOGE("Failed to translate {} shader {:016X}", shader_type,
|
||||
shader.ucode_data_hash());
|
||||
} else {
|
||||
if (storage_writer_.is_active() &&
|
||||
shader.try_set_ucode_storage_index(
|
||||
storage_writer_.storage_index())) {
|
||||
shader_storage_file_flush_needed_ = true;
|
||||
storage_writer_.QueueShaderWrite(&shader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3377,86 +3205,6 @@ ID3D12PipelineState* PipelineCache::CreateD3D12Pipeline(
|
||||
return state;
|
||||
}
|
||||
|
||||
void PipelineCache::StorageWriteThread() {
|
||||
ShaderStoredHeader shader_header;
|
||||
// Don't leak anything in unused bits.
|
||||
std::memset(&shader_header, 0, sizeof(shader_header));
|
||||
|
||||
std::vector<uint32_t> ucode_guest_endian;
|
||||
ucode_guest_endian.reserve(0xFFFF);
|
||||
|
||||
bool flush_shaders = false;
|
||||
bool flush_pipelines = false;
|
||||
|
||||
while (true) {
|
||||
if (flush_shaders) {
|
||||
flush_shaders = false;
|
||||
assert_not_null(shader_storage_file_);
|
||||
fflush(shader_storage_file_);
|
||||
}
|
||||
if (flush_pipelines) {
|
||||
flush_pipelines = false;
|
||||
assert_not_null(pipeline_storage_file_);
|
||||
fflush(pipeline_storage_file_);
|
||||
}
|
||||
|
||||
const Shader* shader = nullptr;
|
||||
PipelineStoredDescription pipeline_description;
|
||||
bool write_pipeline = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(storage_write_request_lock_);
|
||||
if (storage_write_thread_shutdown_) {
|
||||
return;
|
||||
}
|
||||
if (!storage_write_shader_queue_.empty()) {
|
||||
shader = storage_write_shader_queue_.front();
|
||||
storage_write_shader_queue_.pop_front();
|
||||
} else if (storage_write_flush_shaders_) {
|
||||
storage_write_flush_shaders_ = false;
|
||||
flush_shaders = true;
|
||||
}
|
||||
if (!storage_write_pipeline_queue_.empty()) {
|
||||
std::memcpy(&pipeline_description,
|
||||
&storage_write_pipeline_queue_.front(),
|
||||
sizeof(pipeline_description));
|
||||
storage_write_pipeline_queue_.pop_front();
|
||||
write_pipeline = true;
|
||||
} else if (storage_write_flush_pipelines_) {
|
||||
storage_write_flush_pipelines_ = false;
|
||||
flush_pipelines = true;
|
||||
}
|
||||
if (!shader && !write_pipeline) {
|
||||
storage_write_request_cond_.wait(lock);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (shader) {
|
||||
shader_header.ucode_data_hash = shader->ucode_data_hash();
|
||||
shader_header.ucode_dword_count = shader->ucode_dword_count();
|
||||
shader_header.type = shader->type();
|
||||
assert_not_null(shader_storage_file_);
|
||||
fwrite(&shader_header, sizeof(shader_header), 1, shader_storage_file_);
|
||||
if (shader_header.ucode_dword_count) {
|
||||
ucode_guest_endian.resize(shader_header.ucode_dword_count);
|
||||
// Need to swap because the hash is calculated for the shader with guest
|
||||
// endianness.
|
||||
xe::copy_and_swap(ucode_guest_endian.data(), shader->ucode_dwords(),
|
||||
shader_header.ucode_dword_count);
|
||||
fwrite(ucode_guest_endian.data(),
|
||||
shader_header.ucode_dword_count * sizeof(uint32_t), 1,
|
||||
shader_storage_file_);
|
||||
}
|
||||
}
|
||||
|
||||
if (write_pipeline) {
|
||||
assert_not_null(pipeline_storage_file_);
|
||||
fwrite(&pipeline_description, sizeof(pipeline_description), 1,
|
||||
pipeline_storage_file_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PipelineCache::CreationThread(size_t thread_index) {
|
||||
// Create thread-local translator to avoid contention with main thread.
|
||||
// This mirrors what the shader storage loading threads do.
|
||||
@@ -3494,10 +3242,21 @@ void PipelineCache::CreationThread(size_t thread_index) {
|
||||
std::unique_lock<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.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
@@ -36,6 +37,7 @@
|
||||
#include "xenia/gpu/primitive_processor.h"
|
||||
#include "xenia/gpu/register_file.h"
|
||||
#include "xenia/gpu/registers.h"
|
||||
#include "xenia/gpu/shader_storage.h"
|
||||
#include "xenia/gpu/xenos.h"
|
||||
#include "xenia/ui/d3d12/d3d12_api.h"
|
||||
|
||||
@@ -62,8 +64,9 @@ class PipelineCache {
|
||||
// takes a long time, and if it's not, there will be heavy stuttering for the
|
||||
// rest of the execution of the guest).
|
||||
|
||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
||||
uint32_t title_id, bool blocking);
|
||||
void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr);
|
||||
void ShutdownShaderStorage();
|
||||
|
||||
void EndSubmission();
|
||||
@@ -113,15 +116,6 @@ class PipelineCache {
|
||||
}
|
||||
|
||||
private:
|
||||
XEPACKEDSTRUCT(ShaderStoredHeader, {
|
||||
uint64_t ucode_data_hash;
|
||||
|
||||
uint32_t ucode_dword_count : 31;
|
||||
xenos::ShaderType type : 1;
|
||||
|
||||
static constexpr uint32_t kVersion = 0x20201219;
|
||||
});
|
||||
|
||||
// Update PipelineDescription::kVersion if any of the Pipeline* enums are
|
||||
// changed!
|
||||
|
||||
@@ -289,6 +283,11 @@ class PipelineCache {
|
||||
IDxcUtils* dxc_utils = nullptr,
|
||||
IDxcCompiler* dxc_compiler = nullptr);
|
||||
|
||||
// Translates shaders in parallel for storage loading.
|
||||
void TranslateShadersForStorage(
|
||||
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed,
|
||||
bool edram_rov_used);
|
||||
|
||||
// If draw_util::IsRasterizationPotentiallyDone is false, the pixel shader
|
||||
// MUST be made nullptr BEFORE calling this! The shaders must be translated
|
||||
// and valid, unless for_placeholder is true.
|
||||
@@ -415,33 +414,14 @@ class PipelineCache {
|
||||
// changed.
|
||||
Pipeline* current_pipeline_ = nullptr;
|
||||
|
||||
// Currently open shader storage path.
|
||||
std::filesystem::path shader_storage_cache_root_;
|
||||
// Currently open shader storage state.
|
||||
uint32_t shader_storage_title_id_ = 0;
|
||||
std::atomic<bool> shader_storage_file_flush_needed_{false};
|
||||
std::atomic<bool> pipeline_storage_file_flush_needed_{false};
|
||||
|
||||
// Shader storage output stream, for preload in the next emulator runs.
|
||||
FILE* shader_storage_file_ = nullptr;
|
||||
// For only writing shaders to the currently open storage once, incremented
|
||||
// when switching the storage.
|
||||
uint32_t shader_storage_index_ = 0;
|
||||
bool shader_storage_file_flush_needed_ = false;
|
||||
|
||||
// Pipeline storage output stream, for preload in the next emulator runs.
|
||||
FILE* pipeline_storage_file_ = nullptr;
|
||||
bool pipeline_storage_file_flush_needed_ = false;
|
||||
|
||||
// Thread for asynchronous writing to the storage streams.
|
||||
void StorageWriteThread();
|
||||
std::mutex storage_write_request_lock_;
|
||||
std::condition_variable storage_write_request_cond_;
|
||||
// Storage thread input is protected with storage_write_request_lock_, and the
|
||||
// thread is notified about its change via storage_write_request_cond_.
|
||||
std::deque<const Shader*> storage_write_shader_queue_;
|
||||
std::deque<PipelineStoredDescription> storage_write_pipeline_queue_;
|
||||
bool storage_write_flush_shaders_ = false;
|
||||
bool storage_write_flush_pipelines_ = false;
|
||||
bool storage_write_thread_shutdown_ = false;
|
||||
std::unique_ptr<xe::threading::Thread> storage_write_thread_;
|
||||
// Storage writer for shaders and pipelines (owns file handles and storage
|
||||
// index).
|
||||
ShaderStorageWriter<PipelineStoredDescription> storage_writer_;
|
||||
|
||||
// Pipeline creation threads.
|
||||
void CreationThread(size_t thread_index);
|
||||
@@ -466,6 +446,9 @@ class PipelineCache {
|
||||
// Whether setting the event on completion is queued. Protected with
|
||||
// creation_request_lock_, notify_one creation_request_cond_ when set.
|
||||
bool creation_completion_set_event_ = false;
|
||||
// Callback to invoke when all queued pipelines are created (for non-blocking
|
||||
// initialization). Protected with creation_request_lock_.
|
||||
std::function<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.
|
||||
|
||||
@@ -379,27 +379,38 @@ void GraphicsSystem::ClearCaches() {
|
||||
}
|
||||
|
||||
void GraphicsSystem::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) {
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,8 +94,9 @@ class GraphicsSystem {
|
||||
|
||||
virtual void ClearCaches();
|
||||
|
||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
||||
uint32_t title_id, bool blocking);
|
||||
void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr);
|
||||
|
||||
void RequestFrameTrace();
|
||||
void BeginTracing();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#define XENIA_GPU_SHADER_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
@@ -1006,9 +1007,20 @@ class Shader {
|
||||
// An externally managed identifier of the shader storage the microcode of the
|
||||
// shader was last written to, or was loaded from, to only write the shader
|
||||
// microcode to the storage once. UINT32_MAX by default.
|
||||
uint32_t ucode_storage_index() const { return ucode_storage_index_; }
|
||||
uint32_t ucode_storage_index() const {
|
||||
return ucode_storage_index_.load(std::memory_order_relaxed);
|
||||
}
|
||||
void set_ucode_storage_index(uint32_t storage_index) {
|
||||
ucode_storage_index_ = storage_index;
|
||||
ucode_storage_index_.store(storage_index, std::memory_order_relaxed);
|
||||
}
|
||||
// Atomically set storage index if changed. Returns true if updated.
|
||||
bool try_set_ucode_storage_index(uint32_t new_index) {
|
||||
uint32_t expected = ucode_storage_index_.load(std::memory_order_relaxed);
|
||||
if (expected == new_index) {
|
||||
return false;
|
||||
}
|
||||
return ucode_storage_index_.compare_exchange_strong(
|
||||
expected, new_index, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Dumps the shader's microcode binary and, if analyzed, disassembly, to files
|
||||
@@ -1073,7 +1085,7 @@ class Shader {
|
||||
// Modification bits -> translation.
|
||||
std::unordered_map<uint64_t, Translation*> translations_;
|
||||
|
||||
uint32_t ucode_storage_index_ = UINT32_MAX;
|
||||
std::atomic<uint32_t> ucode_storage_index_{UINT32_MAX};
|
||||
|
||||
private:
|
||||
void GatherExecInformation(
|
||||
|
||||
562
src/xenia/gpu/shader_storage.h
Normal file
562
src/xenia/gpu/shader_storage.h
Normal file
@@ -0,0 +1,562 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2026 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_GPU_SHADER_STORAGE_H_
|
||||
#define XENIA_GPU_SHADER_STORAGE_H_
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/assert.h"
|
||||
#include "xenia/base/byte_order.h"
|
||||
#include "xenia/base/filesystem.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/platform.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/base/xxhash.h"
|
||||
#include "xenia/gpu/shader.h"
|
||||
#include "xenia/gpu/xenos.h"
|
||||
|
||||
namespace xe {
|
||||
namespace gpu {
|
||||
|
||||
// Shader storage file format (.xsh) - shared between D3D12 and Vulkan backends.
|
||||
// Stores guest shader microcode (Xbox 360 bytecode) for persistent caching.
|
||||
|
||||
// File magic: 'XESH'
|
||||
constexpr uint32_t kShaderStorageMagic = 0x48534558;
|
||||
|
||||
// Header for each stored shader entry.
|
||||
XEPACKEDSTRUCT(ShaderStoredHeader, {
|
||||
uint64_t ucode_data_hash;
|
||||
|
||||
uint32_t ucode_dword_count : 31;
|
||||
xenos::ShaderType type : 1;
|
||||
|
||||
// Increment when the format changes in an incompatible way.
|
||||
static constexpr uint32_t kVersion = 0x20201219;
|
||||
});
|
||||
|
||||
// Pipeline storage file format (.xpso) - API-specific but with shared header.
|
||||
// File magic: 'XEPS'
|
||||
constexpr uint32_t kPipelineStorageMagic = 0x53504558;
|
||||
|
||||
// Storage directory structure:
|
||||
// <cache_root>/shaders/
|
||||
// shareable/ - Files shared between different machines/drivers
|
||||
// {TITLE_ID}.xsh - Shader microcode (shared between D3D12/Vulkan)
|
||||
// {TITLE_ID}.*.xpso - Pipeline descriptions (API-specific)
|
||||
// local/ - Files specific to this machine/driver
|
||||
// {TITLE_ID}.*.bin - Driver-specific pipeline cache
|
||||
|
||||
inline std::filesystem::path GetShaderStorageRoot(
|
||||
const std::filesystem::path& cache_root) {
|
||||
return cache_root / "shaders";
|
||||
}
|
||||
|
||||
inline std::filesystem::path GetShaderStorageShareableRoot(
|
||||
const std::filesystem::path& cache_root) {
|
||||
return GetShaderStorageRoot(cache_root) / "shareable";
|
||||
}
|
||||
|
||||
inline std::filesystem::path GetShaderStorageLocalRoot(
|
||||
const std::filesystem::path& cache_root) {
|
||||
return GetShaderStorageRoot(cache_root) / "local";
|
||||
}
|
||||
|
||||
inline std::filesystem::path GetShaderStorageFilePath(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id) {
|
||||
return GetShaderStorageShareableRoot(cache_root) /
|
||||
fmt::format("{:08X}.xsh", title_id);
|
||||
}
|
||||
|
||||
// Ensures shader storage directories exist. Returns true on success.
|
||||
inline bool EnsureShaderStorageDirectoriesExist(
|
||||
const std::filesystem::path& cache_root) {
|
||||
std::error_code error_code;
|
||||
auto shareable_root = GetShaderStorageShareableRoot(cache_root);
|
||||
if (!std::filesystem::exists(shareable_root)) {
|
||||
if (!std::filesystem::create_directories(shareable_root, error_code)) {
|
||||
XELOGE(
|
||||
"Failed to create shader storage directory, persistent shader "
|
||||
"storage will be disabled: {}",
|
||||
xe::path_to_utf8(shareable_root));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// File header for shader storage (.xsh).
|
||||
XEPACKEDSTRUCT(ShaderStorageFileHeader, {
|
||||
uint32_t magic;
|
||||
uint32_t version_swapped;
|
||||
});
|
||||
|
||||
// Validates the shader storage file header. Returns true if valid.
|
||||
inline bool ValidateShaderStorageHeader(FILE* file,
|
||||
ShaderStorageFileHeader& header_out) {
|
||||
if (!fread(&header_out, sizeof(header_out), 1, file)) {
|
||||
return false;
|
||||
}
|
||||
return header_out.magic == kShaderStorageMagic &&
|
||||
xe::byte_swap(header_out.version_swapped) ==
|
||||
ShaderStoredHeader::kVersion;
|
||||
}
|
||||
|
||||
// Writes a new shader storage file header.
|
||||
inline bool WriteShaderStorageHeader(FILE* file) {
|
||||
ShaderStorageFileHeader header;
|
||||
header.magic = kShaderStorageMagic;
|
||||
header.version_swapped = xe::byte_swap(ShaderStoredHeader::kVersion);
|
||||
return fwrite(&header, sizeof(header), 1, file) == 1;
|
||||
}
|
||||
|
||||
// Reads shader entries from storage file and calls the callback for each.
|
||||
// Returns the number of valid bytes read (for truncation on corruption).
|
||||
// The callback signature is: bool(xenos::ShaderType type,
|
||||
// const uint32_t* ucode_dwords,
|
||||
// uint32_t ucode_dword_count,
|
||||
// uint64_t ucode_data_hash)
|
||||
// Callback should return true to continue reading, false to stop.
|
||||
template <typename Callback>
|
||||
inline uint64_t ReadShaderEntries(FILE* file, Callback&& callback) {
|
||||
uint64_t valid_bytes = sizeof(ShaderStorageFileHeader);
|
||||
|
||||
ShaderStoredHeader shader_header;
|
||||
std::vector<uint32_t> ucode_dwords;
|
||||
ucode_dwords.reserve(0xFFFF);
|
||||
|
||||
while (true) {
|
||||
if (!fread(&shader_header, sizeof(shader_header), 1, file)) {
|
||||
break;
|
||||
}
|
||||
size_t ucode_byte_count =
|
||||
shader_header.ucode_dword_count * sizeof(uint32_t);
|
||||
ucode_dwords.resize(shader_header.ucode_dword_count);
|
||||
if (shader_header.ucode_dword_count &&
|
||||
!fread(ucode_dwords.data(), ucode_byte_count, 1, file)) {
|
||||
break;
|
||||
}
|
||||
uint64_t ucode_data_hash =
|
||||
XXH3_64bits(ucode_dwords.data(), ucode_byte_count);
|
||||
if (shader_header.ucode_data_hash != ucode_data_hash) {
|
||||
// Validation failed - corrupted entry.
|
||||
break;
|
||||
}
|
||||
valid_bytes += sizeof(shader_header) + ucode_byte_count;
|
||||
|
||||
if (!callback(shader_header.type, ucode_dwords.data(),
|
||||
shader_header.ucode_dword_count, ucode_data_hash)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return valid_bytes;
|
||||
}
|
||||
|
||||
// File header for pipeline storage (.xpso).
|
||||
XEPACKEDSTRUCT(PipelineStorageFileHeader, {
|
||||
uint32_t magic;
|
||||
uint32_t magic_api;
|
||||
uint32_t version_swapped;
|
||||
});
|
||||
|
||||
// Template class for shader and pipeline storage management.
|
||||
// TPipelineStoredDescription is the API-specific pipeline description type.
|
||||
template <typename TPipelineStoredDescription>
|
||||
class ShaderStorageWriter {
|
||||
public:
|
||||
// Configuration for pipeline storage files.
|
||||
struct PipelineStorageConfig {
|
||||
std::string file_suffix; // e.g., ".fsi.vk.xpso" or ".rov.d3d12.xpso"
|
||||
uint32_t api_magic; // e.g., 'VKPS' or 'DXRO'
|
||||
uint32_t version; // Pipeline description version
|
||||
};
|
||||
|
||||
// Callback for loading a shader from ucode data.
|
||||
// Return true to continue reading, false to stop.
|
||||
using ShaderLoadCallback =
|
||||
std::function<bool(xenos::ShaderType type, const uint32_t* ucode_dwords,
|
||||
uint32_t ucode_dword_count, uint64_t ucode_data_hash)>;
|
||||
|
||||
// Callback for translating shaders. Called with the set of
|
||||
// (ucode_hash, modification) pairs that need translation.
|
||||
// Implementation should handle parallel translation internally.
|
||||
using TranslateCallback = std::function<void(
|
||||
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed)>;
|
||||
|
||||
ShaderStorageWriter() = default;
|
||||
~ShaderStorageWriter() { ShutdownShaderStorage(); }
|
||||
|
||||
// Non-copyable.
|
||||
ShaderStorageWriter(const ShaderStorageWriter&) = delete;
|
||||
ShaderStorageWriter& operator=(const ShaderStorageWriter&) = delete;
|
||||
|
||||
// Opens files, loads shaders, triggers translation, starts write thread.
|
||||
// Caller must call ShutdownShaderStorage() first if re-initializing.
|
||||
bool InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id,
|
||||
const PipelineStorageConfig& pipeline_config,
|
||||
ShaderLoadCallback load_shader, TranslateCallback translate_shaders,
|
||||
std::vector<TPipelineStoredDescription>& pipeline_descriptions_out) {
|
||||
cache_root_ = cache_root;
|
||||
title_id_ = title_id;
|
||||
|
||||
if (!EnsureShaderStorageDirectoriesExist(cache_root)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto shader_storage_shareable_root =
|
||||
GetShaderStorageShareableRoot(cache_root);
|
||||
++storage_index_;
|
||||
|
||||
// Open pipeline storage file.
|
||||
auto pipeline_storage_file_path =
|
||||
shader_storage_shareable_root /
|
||||
fmt::format("{:08X}{}", title_id, pipeline_config.file_suffix);
|
||||
pipeline_storage_file_ =
|
||||
xe::filesystem::OpenFile(pipeline_storage_file_path, "a+b");
|
||||
if (!pipeline_storage_file_) {
|
||||
XELOGE(
|
||||
"Failed to open the pipeline storage file for writing, persistent "
|
||||
"shader storage will be disabled: {}",
|
||||
xe::path_to_utf8(pipeline_storage_file_path));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read pipeline descriptions.
|
||||
const uint32_t pipeline_storage_version_swapped =
|
||||
xe::byte_swap(pipeline_config.version);
|
||||
PipelineStorageFileHeader pipeline_header;
|
||||
if (fread(&pipeline_header, sizeof(pipeline_header), 1,
|
||||
pipeline_storage_file_) &&
|
||||
pipeline_header.magic == kPipelineStorageMagic &&
|
||||
pipeline_header.magic_api == pipeline_config.api_magic &&
|
||||
pipeline_header.version_swapped == pipeline_storage_version_swapped) {
|
||||
// Valid header, read pipeline descriptions.
|
||||
xe::filesystem::Seek(pipeline_storage_file_, 0, SEEK_END);
|
||||
int64_t pipeline_storage_told_end =
|
||||
xe::filesystem::Tell(pipeline_storage_file_);
|
||||
size_t pipeline_storage_told_count =
|
||||
size_t(pipeline_storage_told_end >= int64_t(sizeof(pipeline_header))
|
||||
? (uint64_t(pipeline_storage_told_end) -
|
||||
sizeof(pipeline_header)) /
|
||||
sizeof(TPipelineStoredDescription)
|
||||
: 0);
|
||||
if (pipeline_storage_told_count) {
|
||||
xe::filesystem::Seek(pipeline_storage_file_,
|
||||
int64_t(sizeof(pipeline_header)), SEEK_SET);
|
||||
pipeline_descriptions_out.resize(pipeline_storage_told_count);
|
||||
pipeline_descriptions_out.resize(
|
||||
fread(pipeline_descriptions_out.data(),
|
||||
sizeof(TPipelineStoredDescription),
|
||||
pipeline_storage_told_count, pipeline_storage_file_));
|
||||
// Validate each description's hash.
|
||||
size_t valid_count = 0;
|
||||
for (size_t i = 0; i < pipeline_descriptions_out.size(); ++i) {
|
||||
const TPipelineStoredDescription& desc = pipeline_descriptions_out[i];
|
||||
if (XXH3_64bits(&desc.description, sizeof(desc.description)) !=
|
||||
desc.description_hash) {
|
||||
break;
|
||||
}
|
||||
++valid_count;
|
||||
}
|
||||
pipeline_descriptions_out.resize(valid_count);
|
||||
}
|
||||
// Truncate to last valid description.
|
||||
xe::filesystem::TruncateStdioFile(
|
||||
pipeline_storage_file_,
|
||||
uint64_t(sizeof(pipeline_header) +
|
||||
sizeof(TPipelineStoredDescription) *
|
||||
pipeline_descriptions_out.size()));
|
||||
} else {
|
||||
// Write new header.
|
||||
xe::filesystem::TruncateStdioFile(pipeline_storage_file_, 0);
|
||||
pipeline_header.magic = kPipelineStorageMagic;
|
||||
pipeline_header.magic_api = pipeline_config.api_magic;
|
||||
pipeline_header.version_swapped = pipeline_storage_version_swapped;
|
||||
if (fwrite(&pipeline_header, sizeof(pipeline_header), 1,
|
||||
pipeline_storage_file_) != 1) {
|
||||
XELOGE("Failed to write pipeline storage header");
|
||||
fclose(pipeline_storage_file_);
|
||||
pipeline_storage_file_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Open shader storage file.
|
||||
auto shader_storage_file_path =
|
||||
GetShaderStorageFilePath(cache_root, title_id);
|
||||
shader_storage_file_ =
|
||||
xe::filesystem::OpenFile(shader_storage_file_path, "a+b");
|
||||
if (!shader_storage_file_) {
|
||||
XELOGE(
|
||||
"Failed to open the guest shader storage file for writing, "
|
||||
"persistent shader storage will be disabled: {}",
|
||||
xe::path_to_utf8(shader_storage_file_path));
|
||||
fclose(pipeline_storage_file_);
|
||||
pipeline_storage_file_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load shaders from storage.
|
||||
size_t shaders_loaded = 0;
|
||||
ShaderStorageFileHeader shader_header;
|
||||
if (ValidateShaderStorageHeader(shader_storage_file_, shader_header)) {
|
||||
uint64_t shader_storage_valid_bytes = ReadShaderEntries(
|
||||
shader_storage_file_,
|
||||
[&](xenos::ShaderType type, const uint32_t* ucode_dwords,
|
||||
uint32_t ucode_dword_count, uint64_t ucode_data_hash) {
|
||||
if (load_shader(type, ucode_dwords, ucode_dword_count,
|
||||
ucode_data_hash)) {
|
||||
++shaders_loaded;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
xe::filesystem::TruncateStdioFile(shader_storage_file_,
|
||||
shader_storage_valid_bytes);
|
||||
} else {
|
||||
// Write new header.
|
||||
xe::filesystem::TruncateStdioFile(shader_storage_file_, 0);
|
||||
if (!WriteShaderStorageHeader(shader_storage_file_)) {
|
||||
XELOGE("Failed to write shader storage header");
|
||||
fclose(shader_storage_file_);
|
||||
shader_storage_file_ = nullptr;
|
||||
fclose(pipeline_storage_file_);
|
||||
pipeline_storage_file_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
XELOGI("Loaded {} shaders from storage", shaders_loaded);
|
||||
|
||||
// Collect shader translations needed from pipeline descriptions.
|
||||
std::set<std::pair<uint64_t, uint64_t>> translations_needed;
|
||||
for (const TPipelineStoredDescription& desc : pipeline_descriptions_out) {
|
||||
translations_needed.emplace(desc.description.vertex_shader_hash,
|
||||
desc.description.vertex_shader_modification);
|
||||
if (desc.description.pixel_shader_hash) {
|
||||
translations_needed.emplace(desc.description.pixel_shader_hash,
|
||||
desc.description.pixel_shader_modification);
|
||||
}
|
||||
}
|
||||
XELOGI("Loaded {} pipeline descriptions, {} shader translations needed",
|
||||
pipeline_descriptions_out.size(), translations_needed.size());
|
||||
|
||||
// Translate shaders (callback handles parallel translation).
|
||||
if (!translations_needed.empty() && translate_shaders) {
|
||||
translate_shaders(translations_needed);
|
||||
}
|
||||
|
||||
// Start the write thread.
|
||||
storage_write_thread_shutdown_ = false;
|
||||
storage_write_thread_ =
|
||||
xe::threading::Thread::Create({}, [this]() { WriteThread(); });
|
||||
storage_write_thread_->set_name("Shader Storage Writer");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Shutdown storage: stops write thread, closes files.
|
||||
void ShutdownShaderStorage() {
|
||||
if (storage_write_thread_) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_thread_shutdown_ = true;
|
||||
}
|
||||
storage_write_request_cond_.notify_one();
|
||||
xe::threading::Wait(storage_write_thread_.get(), false);
|
||||
storage_write_thread_.reset();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_shader_queue_.clear();
|
||||
storage_write_pipeline_queue_.clear();
|
||||
storage_write_flush_shaders_ = false;
|
||||
storage_write_flush_pipelines_ = false;
|
||||
}
|
||||
|
||||
if (pipeline_storage_file_) {
|
||||
fclose(pipeline_storage_file_);
|
||||
pipeline_storage_file_ = nullptr;
|
||||
}
|
||||
if (shader_storage_file_) {
|
||||
fclose(shader_storage_file_);
|
||||
shader_storage_file_ = nullptr;
|
||||
}
|
||||
|
||||
cache_root_.clear();
|
||||
title_id_ = 0;
|
||||
}
|
||||
|
||||
uint32_t storage_index() const { return storage_index_; }
|
||||
FILE* shader_storage_file() const { return shader_storage_file_; }
|
||||
FILE* pipeline_storage_file() const { return pipeline_storage_file_; }
|
||||
bool is_active() const { return shader_storage_file_ != nullptr; }
|
||||
const std::filesystem::path& cache_root() const { return cache_root_; }
|
||||
uint32_t title_id() const { return title_id_; }
|
||||
|
||||
void QueueShaderWrite(const Shader* shader) {
|
||||
if (!shader_storage_file_) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_shader_queue_.push_back(shader);
|
||||
}
|
||||
storage_write_request_cond_.notify_one();
|
||||
}
|
||||
|
||||
void QueuePipelineWrite(const TPipelineStoredDescription& description) {
|
||||
if (!pipeline_storage_file_) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
storage_write_pipeline_queue_.push_back(description);
|
||||
}
|
||||
storage_write_request_cond_.notify_one();
|
||||
}
|
||||
|
||||
void RequestFlush(bool flush_shaders, bool flush_pipelines) {
|
||||
bool need_notify = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
||||
if (flush_shaders) {
|
||||
storage_write_flush_shaders_ = true;
|
||||
need_notify = true;
|
||||
}
|
||||
if (flush_pipelines) {
|
||||
storage_write_flush_pipelines_ = true;
|
||||
need_notify = true;
|
||||
}
|
||||
}
|
||||
if (need_notify) {
|
||||
storage_write_request_cond_.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void WriteThread() {
|
||||
ShaderStoredHeader shader_header;
|
||||
std::memset(&shader_header, 0, sizeof(shader_header));
|
||||
|
||||
std::vector<uint32_t> ucode_guest_endian;
|
||||
ucode_guest_endian.reserve(0xFFFF);
|
||||
|
||||
bool flush_shaders = false;
|
||||
bool flush_pipelines = false;
|
||||
|
||||
while (true) {
|
||||
if (flush_shaders) {
|
||||
flush_shaders = false;
|
||||
assert_not_null(shader_storage_file_);
|
||||
fflush(shader_storage_file_);
|
||||
}
|
||||
if (flush_pipelines) {
|
||||
flush_pipelines = false;
|
||||
assert_not_null(pipeline_storage_file_);
|
||||
fflush(pipeline_storage_file_);
|
||||
}
|
||||
|
||||
const Shader* shader = nullptr;
|
||||
TPipelineStoredDescription pipeline_description;
|
||||
bool write_pipeline = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(storage_write_request_lock_);
|
||||
if (storage_write_thread_shutdown_) {
|
||||
return;
|
||||
}
|
||||
if (!storage_write_shader_queue_.empty()) {
|
||||
shader = storage_write_shader_queue_.front();
|
||||
storage_write_shader_queue_.pop_front();
|
||||
} else if (storage_write_flush_shaders_) {
|
||||
storage_write_flush_shaders_ = false;
|
||||
flush_shaders = true;
|
||||
}
|
||||
if (!storage_write_pipeline_queue_.empty()) {
|
||||
std::memcpy(&pipeline_description,
|
||||
&storage_write_pipeline_queue_.front(),
|
||||
sizeof(pipeline_description));
|
||||
storage_write_pipeline_queue_.pop_front();
|
||||
write_pipeline = true;
|
||||
} else if (storage_write_flush_pipelines_) {
|
||||
storage_write_flush_pipelines_ = false;
|
||||
flush_pipelines = true;
|
||||
}
|
||||
if (!shader && !write_pipeline && !flush_shaders && !flush_pipelines) {
|
||||
storage_write_request_cond_.wait(lock);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (shader) {
|
||||
shader_header.ucode_data_hash = shader->ucode_data_hash();
|
||||
shader_header.ucode_dword_count = shader->ucode_dword_count();
|
||||
shader_header.type = shader->type();
|
||||
assert_not_null(shader_storage_file_);
|
||||
if (fwrite(&shader_header, sizeof(shader_header), 1,
|
||||
shader_storage_file_) != 1) {
|
||||
XELOGE("Failed to write shader header to storage");
|
||||
} else if (shader_header.ucode_dword_count) {
|
||||
ucode_guest_endian.resize(shader_header.ucode_dword_count);
|
||||
// Need to swap because the hash is calculated for the shader with
|
||||
// guest endianness.
|
||||
xe::copy_and_swap(ucode_guest_endian.data(), shader->ucode_dwords(),
|
||||
shader_header.ucode_dword_count);
|
||||
if (fwrite(ucode_guest_endian.data(),
|
||||
shader_header.ucode_dword_count * sizeof(uint32_t), 1,
|
||||
shader_storage_file_) != 1) {
|
||||
XELOGE("Failed to write shader ucode to storage");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (write_pipeline) {
|
||||
assert_not_null(pipeline_storage_file_);
|
||||
if (fwrite(&pipeline_description, sizeof(pipeline_description), 1,
|
||||
pipeline_storage_file_) != 1) {
|
||||
XELOGE("Failed to write pipeline description to storage");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path cache_root_;
|
||||
uint32_t title_id_ = 0;
|
||||
uint32_t storage_index_ = 0;
|
||||
|
||||
FILE* shader_storage_file_ = nullptr;
|
||||
FILE* pipeline_storage_file_ = nullptr;
|
||||
|
||||
std::unique_ptr<xe::threading::Thread> storage_write_thread_;
|
||||
std::mutex storage_write_request_lock_;
|
||||
std::condition_variable storage_write_request_cond_;
|
||||
std::deque<const Shader*> storage_write_shader_queue_;
|
||||
std::deque<TPipelineStoredDescription> storage_write_pipeline_queue_;
|
||||
bool storage_write_flush_shaders_ = false;
|
||||
bool storage_write_flush_pipelines_ = false;
|
||||
bool storage_write_thread_shutdown_ = false;
|
||||
};
|
||||
|
||||
} // namespace gpu
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_GPU_SHADER_STORAGE_H_
|
||||
@@ -108,6 +108,15 @@ void VulkanCommandProcessor::TracePlaybackWroteMemory(uint32_t base_ptr,
|
||||
primitive_processor_->MemoryInvalidationCallback(base_ptr, length, true);
|
||||
}
|
||||
|
||||
void VulkanCommandProcessor::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback) {
|
||||
CommandProcessor::InitializeShaderStorage(cache_root, title_id, blocking,
|
||||
nullptr);
|
||||
pipeline_cache_->InitializeShaderStorage(cache_root, title_id, blocking,
|
||||
std::move(completion_callback));
|
||||
}
|
||||
|
||||
void VulkanCommandProcessor::RestoreEdramSnapshot(const void* snapshot) {}
|
||||
|
||||
std::string VulkanCommandProcessor::GetWindowTitleText() const {
|
||||
|
||||
@@ -145,6 +145,10 @@ class VulkanCommandProcessor final : public CommandProcessor {
|
||||
|
||||
void TracePlaybackWroteMemory(uint32_t base_ptr, uint32_t length) override;
|
||||
|
||||
void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr) override;
|
||||
|
||||
void RestoreEdramSnapshot(const void* snapshot) override;
|
||||
|
||||
ui::vulkan::VulkanDevice* GetVulkanDevice() const {
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/assert.h"
|
||||
#include "xenia/base/byte_order.h"
|
||||
#include "xenia/base/filesystem.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/math.h"
|
||||
#include "xenia/base/profiling.h"
|
||||
@@ -218,6 +221,9 @@ bool VulkanPipelineCache::Initialize() {
|
||||
}
|
||||
|
||||
void VulkanPipelineCache::Shutdown() {
|
||||
// Shut down shader storage first.
|
||||
ShutdownShaderStorage();
|
||||
|
||||
// Shut down all threads, before destroying the pipelines since they may be
|
||||
// creating them.
|
||||
if (!creation_threads_.empty()) {
|
||||
@@ -231,6 +237,13 @@ void VulkanPipelineCache::Shutdown() {
|
||||
}
|
||||
creation_threads_.clear();
|
||||
}
|
||||
// Clear any pending completion callback (may capture 'this') and reset
|
||||
// startup state.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(creation_request_lock_);
|
||||
creation_completion_callback_ = nullptr;
|
||||
}
|
||||
startup_loading_ = false;
|
||||
creation_completion_event_.reset();
|
||||
|
||||
const ui::vulkan::VulkanDevice* const vulkan_device =
|
||||
@@ -579,6 +592,28 @@ bool VulkanPipelineCache::ConfigurePipeline(
|
||||
auto& pipeline_pair =
|
||||
*pipelines_.emplace(description, Pipeline(pipeline_layout)).first;
|
||||
|
||||
if (storage_writer_.is_active()) {
|
||||
VulkanShader& vs = static_cast<VulkanShader&>(vertex_shader->shader());
|
||||
if (vs.try_set_ucode_storage_index(storage_writer_.storage_index())) {
|
||||
shader_storage_file_flush_needed_ = true;
|
||||
storage_writer_.QueueShaderWrite(&vs);
|
||||
}
|
||||
if (pixel_shader) {
|
||||
VulkanShader& ps = static_cast<VulkanShader&>(pixel_shader->shader());
|
||||
if (ps.try_set_ucode_storage_index(storage_writer_.storage_index())) {
|
||||
shader_storage_file_flush_needed_ = true;
|
||||
storage_writer_.QueueShaderWrite(&ps);
|
||||
}
|
||||
}
|
||||
|
||||
pipeline_storage_file_flush_needed_ = true;
|
||||
PipelineStoredDescription stored_description;
|
||||
stored_description.description_hash = description.GetHash();
|
||||
std::memcpy(&stored_description.description, &description,
|
||||
sizeof(description));
|
||||
storage_writer_.QueuePipelineWrite(stored_description);
|
||||
}
|
||||
|
||||
// Get tessellation shaders if needed.
|
||||
VkShaderModule tessellation_vertex_shader = VK_NULL_HANDLE;
|
||||
VkShaderModule tessellation_control_shader = VK_NULL_HANDLE;
|
||||
@@ -670,7 +705,8 @@ bool VulkanPipelineCache::ConfigurePipeline(
|
||||
creation_arguments.pixel_shader = pixel_shader;
|
||||
creation_arguments.geometry_shader = geometry_shader;
|
||||
creation_arguments.tessellation_vertex_shader = tessellation_vertex_shader;
|
||||
creation_arguments.tessellation_control_shader = tessellation_control_shader;
|
||||
creation_arguments.tessellation_control_shader =
|
||||
tessellation_control_shader;
|
||||
creation_arguments.render_pass = render_pass;
|
||||
if (!EnsurePipelineCreated(creation_arguments)) {
|
||||
return false;
|
||||
@@ -683,31 +719,42 @@ bool VulkanPipelineCache::ConfigurePipeline(
|
||||
}
|
||||
|
||||
void VulkanPipelineCache::EndSubmission() {
|
||||
if (shader_storage_file_flush_needed_ ||
|
||||
pipeline_storage_file_flush_needed_) {
|
||||
storage_writer_.RequestFlush(shader_storage_file_flush_needed_,
|
||||
pipeline_storage_file_flush_needed_);
|
||||
shader_storage_file_flush_needed_ = false;
|
||||
pipeline_storage_file_flush_needed_ = false;
|
||||
}
|
||||
|
||||
if (creation_threads_.empty()) {
|
||||
// Process deferred destructions when GPU is idle
|
||||
ProcessDeferredDestructions();
|
||||
return;
|
||||
}
|
||||
// Await creation of all queued pipelines.
|
||||
bool await_creation_completion_event;
|
||||
{
|
||||
std::lock_guard<std::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_queue_.empty() || creation_threads_busy_ != 0;
|
||||
|
||||
if (startup_loading_) {
|
||||
// Non-blocking: let background threads work asynchronously.
|
||||
creation_request_cond_.notify_one();
|
||||
} else {
|
||||
// Blocking: wait for all queued pipelines.
|
||||
bool await_creation_completion_event;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(creation_request_lock_);
|
||||
await_creation_completion_event =
|
||||
!creation_queue_.empty() || creation_threads_busy_ != 0;
|
||||
if (await_creation_completion_event) {
|
||||
creation_completion_event_->Reset();
|
||||
creation_completion_set_event_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
if (await_creation_completion_event) {
|
||||
creation_completion_event_->Reset();
|
||||
creation_completion_set_event_.store(true, std::memory_order_release);
|
||||
creation_request_cond_.notify_one();
|
||||
xe::threading::Wait(creation_completion_event_.get(), false);
|
||||
}
|
||||
}
|
||||
if (await_creation_completion_event) {
|
||||
creation_request_cond_.notify_one();
|
||||
xe::threading::Wait(creation_completion_event_.get(), false);
|
||||
}
|
||||
|
||||
// Process deferred destructions after waiting for pipelines
|
||||
// Process deferred destructions
|
||||
ProcessDeferredDestructions();
|
||||
}
|
||||
|
||||
@@ -753,12 +800,24 @@ void VulkanPipelineCache::CreationThread() {
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(creation_request_lock_);
|
||||
std::unique_lock<std::mutex> lock(creation_request_lock_);
|
||||
--creation_threads_busy_;
|
||||
if (creation_completion_set_event_.load(std::memory_order_acquire) &&
|
||||
creation_threads_busy_ == 0 && creation_queue_.empty()) {
|
||||
creation_completion_set_event_.store(false, std::memory_order_release);
|
||||
creation_completion_event_->Set();
|
||||
if (creation_threads_busy_ == 0 && creation_queue_.empty()) {
|
||||
// All pipelines created.
|
||||
if (creation_completion_set_event_.load(std::memory_order_acquire)) {
|
||||
// Signal the event (blocking mode).
|
||||
creation_completion_set_event_.store(false,
|
||||
std::memory_order_release);
|
||||
creation_completion_event_->Set();
|
||||
}
|
||||
if (creation_completion_callback_) {
|
||||
// Invoke completion callback (non-blocking mode).
|
||||
auto callback = std::move(creation_completion_callback_);
|
||||
creation_completion_callback_ = nullptr;
|
||||
lock.unlock();
|
||||
callback();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -843,6 +902,104 @@ bool VulkanPipelineCache::TranslateAnalyzedShader(
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanPipelineCache::TranslateShadersForStorage(
|
||||
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed,
|
||||
bool edram_fsi_used) {
|
||||
uint64_t translation_start = xe::Clock::QueryHostTickCount();
|
||||
|
||||
std::vector<std::pair<VulkanShader*, uint64_t>> translations_to_do;
|
||||
translations_to_do.reserve(translations_needed.size());
|
||||
for (const auto& needed : translations_needed) {
|
||||
auto shader_it = shaders_.find(needed.first);
|
||||
if (shader_it == shaders_.end()) {
|
||||
continue;
|
||||
}
|
||||
VulkanShader* shader = shader_it->second;
|
||||
VulkanShader::VulkanTranslation* translation =
|
||||
static_cast<VulkanShader::VulkanTranslation*>(
|
||||
shader->GetOrCreateTranslation(needed.second));
|
||||
if (translation && !translation->is_translated()) {
|
||||
translations_to_do.emplace_back(shader, needed.second);
|
||||
}
|
||||
}
|
||||
|
||||
if (translations_to_do.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::atomic<size_t> translation_index{0};
|
||||
std::atomic<size_t> translations_completed{0};
|
||||
|
||||
const ui::vulkan::VulkanDevice* vulkan_device =
|
||||
command_processor_.GetVulkanDevice();
|
||||
bool msaa_2x_attachments =
|
||||
render_target_cache_.msaa_2x_attachments_supported();
|
||||
bool msaa_2x_no_attachments =
|
||||
render_target_cache_.msaa_2x_no_attachments_supported();
|
||||
uint32_t draw_res_x = render_target_cache_.draw_resolution_scale_x();
|
||||
uint32_t draw_res_y = render_target_cache_.draw_resolution_scale_y();
|
||||
|
||||
auto translate_function = [this, &translations_to_do, &translation_index,
|
||||
&translations_completed, vulkan_device,
|
||||
msaa_2x_attachments, msaa_2x_no_attachments,
|
||||
edram_fsi_used, draw_res_x, draw_res_y]() {
|
||||
// Each thread needs its own translator.
|
||||
SpirvShaderTranslator translator(
|
||||
SpirvShaderTranslator::Features(vulkan_device), msaa_2x_attachments,
|
||||
msaa_2x_no_attachments, edram_fsi_used, draw_res_x, draw_res_y);
|
||||
|
||||
while (true) {
|
||||
size_t index = translation_index.fetch_add(1);
|
||||
if (index >= translations_to_do.size()) {
|
||||
break;
|
||||
}
|
||||
VulkanShader* shader = translations_to_do[index].first;
|
||||
uint64_t modification = translations_to_do[index].second;
|
||||
VulkanShader::VulkanTranslation* translation =
|
||||
static_cast<VulkanShader::VulkanTranslation*>(
|
||||
shader->GetTranslation(modification));
|
||||
if (translation && !translation->is_translated()) {
|
||||
if (TranslateAnalyzedShader(translator, *translation)) {
|
||||
translations_completed.fetch_add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
size_t thread_count = 0;
|
||||
if (cvars::vulkan_pipeline_creation_threads != 0) {
|
||||
uint32_t logical_processor_count =
|
||||
std::max(uint32_t(1), xe::threading::logical_processor_count());
|
||||
if (cvars::vulkan_pipeline_creation_threads < 0) {
|
||||
thread_count = std::max(logical_processor_count * 3 / 4, uint32_t(1));
|
||||
} else {
|
||||
thread_count = std::min(uint32_t(cvars::vulkan_pipeline_creation_threads),
|
||||
logical_processor_count);
|
||||
}
|
||||
thread_count = std::min(thread_count, translations_to_do.size());
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<xe::threading::Thread>> translation_threads;
|
||||
for (size_t i = 0; i < thread_count; ++i) {
|
||||
auto thread = xe::threading::Thread::Create({}, translate_function);
|
||||
if (thread) {
|
||||
thread->set_name("Shader Translation");
|
||||
translation_threads.push_back(std::move(thread));
|
||||
}
|
||||
}
|
||||
|
||||
// Main thread also participates.
|
||||
translate_function();
|
||||
|
||||
for (auto& thread : translation_threads) {
|
||||
xe::threading::Wait(thread.get(), false);
|
||||
}
|
||||
|
||||
XELOGI("Translated {} shaders in {} ms", translations_completed.load(),
|
||||
(xe::Clock::QueryHostTickCount() - translation_start) * 1000 /
|
||||
xe::Clock::QueryHostTickFrequency());
|
||||
}
|
||||
|
||||
void VulkanPipelineCache::WritePipelineRenderTargetDescription(
|
||||
reg::RB_BLENDCONTROL blend_control, uint32_t write_mask,
|
||||
PipelineRenderTarget& render_target_out) const {
|
||||
@@ -2883,6 +3040,407 @@ void VulkanPipelineCache::ProcessDeferredDestructions() {
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanPipelineCache::InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback) {
|
||||
ShutdownShaderStorage();
|
||||
|
||||
shader_storage_title_id_ = title_id;
|
||||
|
||||
if (!blocking) {
|
||||
startup_loading_ = true;
|
||||
if (completion_callback) {
|
||||
completion_callback = [this, orig = std::move(completion_callback)]() {
|
||||
startup_loading_ = false;
|
||||
orig();
|
||||
};
|
||||
} else {
|
||||
completion_callback = [this]() { startup_loading_ = false; };
|
||||
}
|
||||
}
|
||||
|
||||
bool edram_fsi_used = render_target_cache_.GetPath() ==
|
||||
RenderTargetCache::Path::kPixelShaderInterlock;
|
||||
|
||||
ShaderStorageWriter<PipelineStoredDescription>::PipelineStorageConfig
|
||||
pipeline_config;
|
||||
pipeline_config.file_suffix =
|
||||
fmt::format(".{}.vk.xpso", edram_fsi_used ? "fsi" : "fbo");
|
||||
pipeline_config.api_magic = kPipelineStorageAPIMagicVulkan;
|
||||
pipeline_config.version =
|
||||
std::max(PipelineDescription::kVersion,
|
||||
SpirvShaderTranslator::Modification::kVersion);
|
||||
|
||||
uint32_t storage_index = storage_writer_.storage_index() + 1;
|
||||
|
||||
std::vector<PipelineStoredDescription> pipeline_stored_descriptions;
|
||||
if (!storage_writer_.InitializeShaderStorage(
|
||||
cache_root, title_id, pipeline_config,
|
||||
// Shader load callback.
|
||||
[&](xenos::ShaderType type, const uint32_t* ucode_dwords,
|
||||
uint32_t ucode_dword_count, uint64_t ucode_data_hash) {
|
||||
VulkanShader* shader =
|
||||
LoadShader(type, ucode_dwords, ucode_dword_count);
|
||||
if (!shader || shader->ucode_storage_index() == storage_index) {
|
||||
return true; // Continue reading.
|
||||
}
|
||||
shader->set_ucode_storage_index(storage_index);
|
||||
if (!shader->is_ucode_analyzed()) {
|
||||
shader->AnalyzeUcode(ucode_disasm_buffer_);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// Shader translate callback - handles parallel translation.
|
||||
[this, edram_fsi_used](const std::set<std::pair<uint64_t, uint64_t>>
|
||||
& translations_needed) {
|
||||
TranslateShadersForStorage(translations_needed, edram_fsi_used);
|
||||
},
|
||||
pipeline_stored_descriptions)) {
|
||||
if (completion_callback) {
|
||||
completion_callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
shader_storage_file_flush_needed_ = false;
|
||||
pipeline_storage_file_flush_needed_ = false;
|
||||
|
||||
// Load VkPipelineCache from disk if available.
|
||||
auto shader_storage_local_root = GetShaderStorageLocalRoot(cache_root);
|
||||
if (!std::filesystem::exists(shader_storage_local_root)) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(shader_storage_local_root, ec);
|
||||
}
|
||||
vk_pipeline_cache_path_ =
|
||||
shader_storage_local_root /
|
||||
fmt::format("{:08X}.vk.bin", shader_storage_title_id_);
|
||||
|
||||
const ui::vulkan::VulkanDevice* const vulkan_device =
|
||||
command_processor_.GetVulkanDevice();
|
||||
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
|
||||
const VkDevice device = vulkan_device->device();
|
||||
|
||||
// Try to load existing VkPipelineCache data.
|
||||
std::vector<uint8_t> pipeline_cache_data;
|
||||
if (FILE* cache_file =
|
||||
xe::filesystem::OpenFile(vk_pipeline_cache_path_, "rb")) {
|
||||
xe::filesystem::Seek(cache_file, 0, SEEK_END);
|
||||
int64_t cache_size = xe::filesystem::Tell(cache_file);
|
||||
if (cache_size > 0) {
|
||||
pipeline_cache_data.resize(size_t(cache_size));
|
||||
xe::filesystem::Seek(cache_file, 0, SEEK_SET);
|
||||
pipeline_cache_data.resize(fread(pipeline_cache_data.data(), 1,
|
||||
pipeline_cache_data.size(), cache_file));
|
||||
}
|
||||
fclose(cache_file);
|
||||
XELOGI("Loaded {} bytes of VkPipelineCache data",
|
||||
pipeline_cache_data.size());
|
||||
}
|
||||
|
||||
// Recreate the VkPipelineCache with the loaded data.
|
||||
if (vk_pipeline_cache_ != VK_NULL_HANDLE) {
|
||||
dfn.vkDestroyPipelineCache(device, vk_pipeline_cache_, nullptr);
|
||||
vk_pipeline_cache_ = VK_NULL_HANDLE;
|
||||
}
|
||||
VkPipelineCacheCreateInfo pipeline_cache_create_info = {};
|
||||
pipeline_cache_create_info.sType =
|
||||
VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
|
||||
pipeline_cache_create_info.initialDataSize = pipeline_cache_data.size();
|
||||
pipeline_cache_create_info.pInitialData =
|
||||
pipeline_cache_data.empty() ? nullptr : pipeline_cache_data.data();
|
||||
if (dfn.vkCreatePipelineCache(device, &pipeline_cache_create_info, nullptr,
|
||||
&vk_pipeline_cache_) != VK_SUCCESS) {
|
||||
XELOGW(
|
||||
"VulkanPipelineCache: Failed to create pipeline cache with "
|
||||
"initial data, creating empty cache");
|
||||
vk_pipeline_cache_ = VK_NULL_HANDLE;
|
||||
pipeline_cache_create_info.initialDataSize = 0;
|
||||
pipeline_cache_create_info.pInitialData = nullptr;
|
||||
dfn.vkCreatePipelineCache(device, &pipeline_cache_create_info, nullptr,
|
||||
&vk_pipeline_cache_);
|
||||
}
|
||||
|
||||
// Create pipelines from stored descriptions.
|
||||
if (!pipeline_stored_descriptions.empty()) {
|
||||
uint64_t pipeline_creation_start = xe::Clock::QueryHostTickCount();
|
||||
|
||||
size_t pipelines_created = 0;
|
||||
size_t pipelines_already_exist = 0;
|
||||
size_t pipelines_vs_not_found = 0;
|
||||
size_t pipelines_vs_translation_missing = 0;
|
||||
size_t pipelines_ps_not_found = 0;
|
||||
size_t pipelines_ps_translation_missing = 0;
|
||||
size_t pipelines_render_pass_failed = 0;
|
||||
size_t pipelines_layout_failed = 0;
|
||||
size_t pipelines_requirements_not_met = 0;
|
||||
|
||||
for (const PipelineStoredDescription& pipeline_stored_description :
|
||||
pipeline_stored_descriptions) {
|
||||
const PipelineDescription& pipeline_description =
|
||||
pipeline_stored_description.description;
|
||||
|
||||
// Skip pipelines not supported by this device.
|
||||
if (!ArePipelineRequirementsMet(pipeline_description)) {
|
||||
++pipelines_requirements_not_met;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip already known pipelines.
|
||||
auto it = pipelines_.find(pipeline_description);
|
||||
if (it != pipelines_.end()) {
|
||||
++pipelines_already_exist;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look up vertex shader.
|
||||
auto vertex_shader_it =
|
||||
shaders_.find(pipeline_description.vertex_shader_hash);
|
||||
if (vertex_shader_it == shaders_.end()) {
|
||||
++pipelines_vs_not_found;
|
||||
continue;
|
||||
}
|
||||
VulkanShader* vertex_shader = vertex_shader_it->second;
|
||||
VulkanShader::VulkanTranslation* vertex_translation =
|
||||
static_cast<VulkanShader::VulkanTranslation*>(
|
||||
vertex_shader->GetTranslation(
|
||||
pipeline_description.vertex_shader_modification));
|
||||
if (!vertex_translation || !vertex_translation->is_translated() ||
|
||||
!vertex_translation->is_valid()) {
|
||||
++pipelines_vs_translation_missing;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look up pixel shader if present.
|
||||
VulkanShader* pixel_shader = nullptr;
|
||||
VulkanShader::VulkanTranslation* pixel_translation = nullptr;
|
||||
if (pipeline_description.pixel_shader_hash) {
|
||||
auto pixel_shader_it =
|
||||
shaders_.find(pipeline_description.pixel_shader_hash);
|
||||
if (pixel_shader_it == shaders_.end()) {
|
||||
++pipelines_ps_not_found;
|
||||
continue;
|
||||
}
|
||||
pixel_shader = pixel_shader_it->second;
|
||||
pixel_translation = static_cast<VulkanShader::VulkanTranslation*>(
|
||||
pixel_shader->GetTranslation(
|
||||
pipeline_description.pixel_shader_modification));
|
||||
if (!pixel_translation || !pixel_translation->is_translated() ||
|
||||
!pixel_translation->is_valid()) {
|
||||
++pipelines_ps_translation_missing;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get render pass.
|
||||
VkRenderPass render_pass =
|
||||
render_target_cache_.GetPath() ==
|
||||
RenderTargetCache::Path::kPixelShaderInterlock
|
||||
? render_target_cache_.GetFragmentShaderInterlockRenderPass()
|
||||
: render_target_cache_.GetHostRenderTargetsRenderPass(
|
||||
pipeline_description.render_pass_key);
|
||||
if (render_pass == VK_NULL_HANDLE) {
|
||||
++pipelines_render_pass_failed;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get pipeline layout.
|
||||
const PipelineLayoutProvider* pipeline_layout =
|
||||
command_processor_.GetPipelineLayout(
|
||||
pixel_shader
|
||||
? pixel_shader->GetTextureBindingsAfterTranslation().size()
|
||||
: 0,
|
||||
pixel_shader
|
||||
? pixel_shader->GetSamplerBindingsAfterTranslation().size()
|
||||
: 0,
|
||||
vertex_shader->GetTextureBindingsAfterTranslation().size(),
|
||||
vertex_shader->GetSamplerBindingsAfterTranslation().size());
|
||||
if (!pipeline_layout) {
|
||||
++pipelines_layout_failed;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get geometry shader if needed.
|
||||
VkShaderModule geometry_shader = VK_NULL_HANDLE;
|
||||
if (pipeline_description.geometry_shader !=
|
||||
PipelineGeometryShader::kNone) {
|
||||
GeometryShaderKey geometry_shader_key;
|
||||
GetGeometryShaderKey(
|
||||
pipeline_description.geometry_shader,
|
||||
SpirvShaderTranslator::Modification(
|
||||
vertex_translation->modification()),
|
||||
SpirvShaderTranslator::Modification(
|
||||
pixel_translation ? pixel_translation->modification() : 0),
|
||||
geometry_shader_key);
|
||||
geometry_shader = GetGeometryShader(geometry_shader_key);
|
||||
if (geometry_shader == VK_NULL_HANDLE) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get tessellation shaders if needed.
|
||||
VkShaderModule tessellation_vertex_shader = VK_NULL_HANDLE;
|
||||
VkShaderModule tessellation_control_shader = VK_NULL_HANDLE;
|
||||
if (pipeline_description.tessellation_mode !=
|
||||
PipelineTessellationMode::kNone) {
|
||||
tessellation_vertex_shader =
|
||||
GetTessellationVertexShader(pipeline_description.tessellation_mode);
|
||||
bool use_control_point_count =
|
||||
(pipeline_description.tessellation_mode ==
|
||||
PipelineTessellationMode::kAdaptive);
|
||||
tessellation_control_shader = GetTessellationControlShader(
|
||||
pipeline_description.tessellation_mode,
|
||||
pipeline_description.tessellation_patch, use_control_point_count);
|
||||
if (tessellation_vertex_shader == VK_NULL_HANDLE ||
|
||||
tessellation_control_shader == VK_NULL_HANDLE) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the pipeline entry.
|
||||
auto& pipeline_pair =
|
||||
*pipelines_.emplace(pipeline_description, Pipeline(pipeline_layout))
|
||||
.first;
|
||||
|
||||
// Queue for creation.
|
||||
if (!creation_threads_.empty()) {
|
||||
// Calculate priority based on whether shader writes to visible RTs.
|
||||
uint8_t priority = 0;
|
||||
if (pixel_shader) {
|
||||
uint32_t bound_rts =
|
||||
(pipeline_description.render_targets[0].color_write_mask ? 1
|
||||
: 0) |
|
||||
(pipeline_description.render_targets[1].color_write_mask ? 2
|
||||
: 0) |
|
||||
(pipeline_description.render_targets[2].color_write_mask ? 4
|
||||
: 0) |
|
||||
(pipeline_description.render_targets[3].color_write_mask ? 8 : 0);
|
||||
priority = pipeline_util::CalculatePipelinePriority(
|
||||
bound_rts, pixel_shader->writes_color_targets(),
|
||||
pixel_shader->writes_depth());
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(creation_request_lock_);
|
||||
PipelineCreationArguments creation_arguments;
|
||||
creation_arguments.pipeline = &pipeline_pair;
|
||||
creation_arguments.vertex_shader = vertex_translation;
|
||||
creation_arguments.pixel_shader = pixel_translation;
|
||||
creation_arguments.geometry_shader = geometry_shader;
|
||||
creation_arguments.tessellation_vertex_shader =
|
||||
tessellation_vertex_shader;
|
||||
creation_arguments.tessellation_control_shader =
|
||||
tessellation_control_shader;
|
||||
creation_arguments.render_pass = render_pass;
|
||||
creation_arguments.priority = priority;
|
||||
creation_queue_.push(creation_arguments);
|
||||
creation_request_cond_.notify_one();
|
||||
} else {
|
||||
// No creation threads - create synchronously.
|
||||
PipelineCreationArguments creation_arguments;
|
||||
creation_arguments.pipeline = &pipeline_pair;
|
||||
creation_arguments.vertex_shader = vertex_translation;
|
||||
creation_arguments.pixel_shader = pixel_translation;
|
||||
creation_arguments.geometry_shader = geometry_shader;
|
||||
creation_arguments.tessellation_vertex_shader =
|
||||
tessellation_vertex_shader;
|
||||
creation_arguments.tessellation_control_shader =
|
||||
tessellation_control_shader;
|
||||
creation_arguments.render_pass = render_pass;
|
||||
EnsurePipelineCreated(creation_arguments);
|
||||
}
|
||||
|
||||
++pipelines_created;
|
||||
}
|
||||
|
||||
if (!creation_threads_.empty()) {
|
||||
if (blocking) {
|
||||
// Blocking mode: wait for all pipelines to be created.
|
||||
bool await_creation_completion_event;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(creation_request_lock_);
|
||||
await_creation_completion_event =
|
||||
!creation_queue_.empty() || creation_threads_busy_ != 0;
|
||||
if (await_creation_completion_event) {
|
||||
creation_completion_event_->Reset();
|
||||
creation_completion_set_event_.store(true,
|
||||
std::memory_order_release);
|
||||
}
|
||||
}
|
||||
if (await_creation_completion_event) {
|
||||
creation_request_cond_.notify_one();
|
||||
xe::threading::Wait(creation_completion_event_.get(), false);
|
||||
}
|
||||
} else {
|
||||
// Non-blocking mode: store callback for later invocation.
|
||||
std::lock_guard<std::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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XELOGI("Pipeline cache: {} created, {} already exist, {} total in {} ms",
|
||||
pipelines_created, pipelines_already_exist,
|
||||
pipeline_stored_descriptions.size(),
|
||||
(xe::Clock::QueryHostTickCount() - pipeline_creation_start) * 1000 /
|
||||
xe::Clock::QueryHostTickFrequency());
|
||||
|
||||
if (pipelines_vs_not_found || pipelines_vs_translation_missing ||
|
||||
pipelines_ps_not_found || pipelines_ps_translation_missing ||
|
||||
pipelines_render_pass_failed || pipelines_layout_failed ||
|
||||
pipelines_requirements_not_met) {
|
||||
XELOGI(
|
||||
"Pipeline cache skipped: {} VS not found, {} VS translation missing, "
|
||||
"{} PS not found, {} PS translation missing, {} render pass failed, "
|
||||
"{} layout failed, {} requirements not met",
|
||||
pipelines_vs_not_found, pipelines_vs_translation_missing,
|
||||
pipelines_ps_not_found, pipelines_ps_translation_missing,
|
||||
pipelines_render_pass_failed, pipelines_layout_failed,
|
||||
pipelines_requirements_not_met);
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke completion callback if no async work was queued.
|
||||
if (completion_callback) {
|
||||
completion_callback();
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanPipelineCache::ShutdownShaderStorage() {
|
||||
// Save VkPipelineCache to disk before shutting down storage.
|
||||
if (vk_pipeline_cache_ != VK_NULL_HANDLE &&
|
||||
!vk_pipeline_cache_path_.empty()) {
|
||||
const ui::vulkan::VulkanDevice* const vulkan_device =
|
||||
command_processor_.GetVulkanDevice();
|
||||
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
|
||||
const VkDevice device = vulkan_device->device();
|
||||
|
||||
size_t cache_size = 0;
|
||||
if (dfn.vkGetPipelineCacheData(device, vk_pipeline_cache_, &cache_size,
|
||||
nullptr) == VK_SUCCESS &&
|
||||
cache_size > 0) {
|
||||
std::vector<uint8_t> cache_data(cache_size);
|
||||
if (dfn.vkGetPipelineCacheData(device, vk_pipeline_cache_, &cache_size,
|
||||
cache_data.data()) == VK_SUCCESS) {
|
||||
if (FILE* cache_file =
|
||||
xe::filesystem::OpenFile(vk_pipeline_cache_path_, "wb")) {
|
||||
fwrite(cache_data.data(), 1, cache_size, cache_file);
|
||||
fclose(cache_file);
|
||||
XELOGI("Saved {} bytes of VkPipelineCache data", cache_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vk_pipeline_cache_path_.clear();
|
||||
|
||||
// Shut down the storage writer (closes files, stops write thread).
|
||||
storage_writer_.ShutdownShaderStorage();
|
||||
shader_storage_file_flush_needed_ = false;
|
||||
pipeline_storage_file_flush_needed_ = false;
|
||||
shader_storage_title_id_ = 0;
|
||||
}
|
||||
|
||||
} // namespace vulkan
|
||||
} // namespace gpu
|
||||
} // namespace xe
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -30,6 +33,7 @@
|
||||
#include "xenia/gpu/primitive_processor.h"
|
||||
#include "xenia/gpu/register_file.h"
|
||||
#include "xenia/gpu/registers.h"
|
||||
#include "xenia/gpu/shader_storage.h"
|
||||
#include "xenia/gpu/spirv_shader_translator.h"
|
||||
#include "xenia/gpu/vulkan/vulkan_render_target_cache.h"
|
||||
#include "xenia/gpu/vulkan/vulkan_shader.h"
|
||||
@@ -101,6 +105,12 @@ class VulkanPipelineCache {
|
||||
bool Initialize();
|
||||
void Shutdown();
|
||||
|
||||
// Shader and pipeline storage.
|
||||
void InitializeShaderStorage(
|
||||
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||
std::function<void()> completion_callback = nullptr);
|
||||
void ShutdownShaderStorage();
|
||||
|
||||
void EndSubmission();
|
||||
bool IsCreatingPipelines();
|
||||
|
||||
@@ -259,6 +269,18 @@ class VulkanPipelineCache {
|
||||
return size_t(description.GetHash());
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr uint32_t kVersion = 0x20250118;
|
||||
});
|
||||
|
||||
// Pipeline storage constants.
|
||||
static constexpr uint32_t kPipelineStorageVersionWithoutAPI = 0x20201219;
|
||||
static constexpr uint32_t kPipelineStorageAPIMagicVulkan = 'VLKN';
|
||||
|
||||
// Pipeline storage description.
|
||||
XEPACKEDSTRUCT(PipelineStoredDescription, {
|
||||
uint64_t description_hash;
|
||||
PipelineDescription description;
|
||||
});
|
||||
|
||||
// creation threads, with everything needed from caches pre-looked-up.
|
||||
@@ -314,6 +336,11 @@ class VulkanPipelineCache {
|
||||
bool TranslateAnalyzedShader(SpirvShaderTranslator& translator,
|
||||
VulkanShader::VulkanTranslation& translation);
|
||||
|
||||
// Translates shaders in parallel for storage loading.
|
||||
void TranslateShadersForStorage(
|
||||
const std::set<std::pair<uint64_t, uint64_t>>& translations_needed,
|
||||
bool edram_fsi_used);
|
||||
|
||||
void WritePipelineRenderTargetDescription(
|
||||
reg::RB_BLENDCONTROL blend_control, uint32_t write_mask,
|
||||
PipelineRenderTarget& render_target_out) const;
|
||||
@@ -452,6 +479,10 @@ class VulkanPipelineCache {
|
||||
std::condition_variable creation_request_cond_;
|
||||
std::unique_ptr<xe::threading::Event> creation_completion_event_ = nullptr;
|
||||
std::atomic<bool> creation_completion_set_event_{false};
|
||||
std::function<void()> creation_completion_callback_;
|
||||
// During startup loading, don't block on pipeline creation to allow game
|
||||
// boot.
|
||||
bool startup_loading_ = false;
|
||||
|
||||
// Deferred destruction of replaced shader modules and pipelines.
|
||||
// Pipelines are only destroyed after the GPU submission that might reference
|
||||
@@ -462,6 +493,18 @@ class VulkanPipelineCache {
|
||||
// last potentially used in. Only destroyed when that submission completes.
|
||||
std::vector<std::pair<VkPipeline, uint64_t>> deferred_destroy_pipelines_;
|
||||
std::mutex deferred_destroy_mutex_;
|
||||
|
||||
// Shader and pipeline storage.
|
||||
uint32_t shader_storage_title_id_ = 0;
|
||||
std::atomic<bool> shader_storage_file_flush_needed_{false};
|
||||
std::atomic<bool> pipeline_storage_file_flush_needed_{false};
|
||||
|
||||
// Storage writer for shaders and pipelines (owns file handles and storage
|
||||
// index).
|
||||
ShaderStorageWriter<PipelineStoredDescription> storage_writer_;
|
||||
|
||||
// VkPipelineCache persistence path.
|
||||
std::filesystem::path vk_pipeline_cache_path_;
|
||||
};
|
||||
|
||||
} // namespace vulkan
|
||||
|
||||
@@ -69,6 +69,7 @@ XE_UI_VULKAN_FUNCTION(vkGetBufferMemoryRequirements)
|
||||
XE_UI_VULKAN_FUNCTION(vkGetDeviceQueue)
|
||||
XE_UI_VULKAN_FUNCTION(vkGetFenceStatus)
|
||||
XE_UI_VULKAN_FUNCTION(vkGetImageMemoryRequirements)
|
||||
XE_UI_VULKAN_FUNCTION(vkGetPipelineCacheData)
|
||||
XE_UI_VULKAN_FUNCTION(vkInvalidateMappedMemoryRanges)
|
||||
XE_UI_VULKAN_FUNCTION(vkMapMemory)
|
||||
XE_UI_VULKAN_FUNCTION(vkResetCommandPool)
|
||||
|
||||
Reference in New Issue
Block a user