[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
|
// Initialize shader storage asynchronously - pipeline compilation happens in
|
||||||
// miss the initial seconds - for instance, sound from an intro video may
|
// background while the game goes through its normal startup (loading screens,
|
||||||
// start playing before the video can be seen if doing this in parallel with
|
// intro videos, etc.). With async_shader_compilation enabled, draws are
|
||||||
// the main thread.
|
// skipped until pipelines are ready, so this is safe. By the time actual
|
||||||
on_shader_storage_initialization(true);
|
// gameplay starts, most cached pipelines should be compiled.
|
||||||
graphics_system_->InitializeShaderStorage(cache_root_, title_id_.value(),
|
if (graphics_system_) {
|
||||||
true);
|
on_shader_storage_initialization(true);
|
||||||
on_shader_storage_initialization(false);
|
graphics_system_->InitializeShaderStorage(
|
||||||
|
cache_root_, title_id_.value(), false,
|
||||||
|
[this]() { on_shader_storage_initialization(false); });
|
||||||
|
}
|
||||||
|
|
||||||
auto main_thread = kernel_state_->LaunchModule(module);
|
auto main_thread = kernel_state_->LaunchModule(module);
|
||||||
if (!main_thread) {
|
if (!main_thread) {
|
||||||
|
|||||||
@@ -172,7 +172,11 @@ void CommandProcessor::Shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CommandProcessor::InitializeShaderStorage(
|
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(
|
void CommandProcessor::RequestFrameTrace(
|
||||||
|
|||||||
@@ -121,8 +121,9 @@ class CommandProcessor {
|
|||||||
// May be called not only from the command processor thread when the command
|
// 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
|
// processor is paused, and the termination of this function may be explicitly
|
||||||
// awaited.
|
// awaited.
|
||||||
virtual void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
virtual void InitializeShaderStorage(
|
||||||
uint32_t title_id, bool blocking);
|
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 RequestFrameTrace(const std::filesystem::path& root_path);
|
||||||
virtual void BeginTracing(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(
|
void D3D12CommandProcessor::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,
|
||||||
CommandProcessor::InitializeShaderStorage(cache_root, title_id, blocking);
|
std::function<void()> completion_callback) {
|
||||||
pipeline_cache_->InitializeShaderStorage(cache_root, title_id, blocking);
|
CommandProcessor::InitializeShaderStorage(cache_root, title_id, blocking,
|
||||||
|
nullptr);
|
||||||
|
pipeline_cache_->InitializeShaderStorage(cache_root, title_id, blocking,
|
||||||
|
std::move(completion_callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void D3D12CommandProcessor::RequestFrameTrace(
|
void D3D12CommandProcessor::RequestFrameTrace(
|
||||||
@@ -2585,7 +2588,12 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type,
|
|||||||
|
|
||||||
if (cvars::async_shader_compilation) {
|
if (cvars::async_shader_compilation) {
|
||||||
if (pipeline_cache_->GetD3D12PipelineByHandle(pipeline_handle) == nullptr) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
// Re-fetch root signature now that pipeline is ready.
|
// Re-fetch root signature now that pipeline is ready.
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ class D3D12CommandProcessor final : public CommandProcessor {
|
|||||||
|
|
||||||
void ClearCaches() override;
|
void ClearCaches() override;
|
||||||
|
|
||||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
void InitializeShaderStorage(
|
||||||
uint32_t title_id, bool blocking) override;
|
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;
|
void RequestFrameTrace(const std::filesystem::path& root_path) override;
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ void PipelineCache::Shutdown() {
|
|||||||
delete it.second;
|
delete it.second;
|
||||||
}
|
}
|
||||||
shaders_.clear();
|
shaders_.clear();
|
||||||
shader_storage_index_ = 0;
|
|
||||||
|
|
||||||
// Shut down shader translation.
|
// Shut down shader translation.
|
||||||
ui::d3d12::util::ReleaseAndNull(dxc_compiler_);
|
ui::d3d12::util::ReleaseAndNull(dxc_compiler_);
|
||||||
@@ -227,346 +226,48 @@ void PipelineCache::Shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PipelineCache::InitializeShaderStorage(
|
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();
|
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() ==
|
bool edram_rov_used = render_target_cache_.GetPath() ==
|
||||||
RenderTargetCache::Path::kPixelShaderInterlock;
|
RenderTargetCache::Path::kPixelShaderInterlock;
|
||||||
|
|
||||||
// Initialize the pipeline storage stream - read pipeline descriptions and
|
ShaderStorageWriter<PipelineStoredDescription>::PipelineStorageConfig
|
||||||
// collect used shader modifications to translate.
|
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;
|
std::vector<PipelineStoredDescription> pipeline_stored_descriptions;
|
||||||
// <Shader hash, modification bits>.
|
if (!storage_writer_.InitializeShaderStorage(
|
||||||
std::set<std::pair<uint64_t, uint64_t>> shader_translations_needed;
|
cache_root, title_id, pipeline_config,
|
||||||
auto pipeline_storage_file_path =
|
// Shader load callback.
|
||||||
shader_storage_shareable_root /
|
[&](xenos::ShaderType type, const uint32_t* ucode_dwords,
|
||||||
fmt::format("{:08X}.{}.d3d12.xpso", title_id,
|
uint32_t ucode_dword_count, uint64_t ucode_data_hash) {
|
||||||
edram_rov_used ? "rov" : "rtv");
|
D3D12Shader* shader = LoadShader(
|
||||||
pipeline_storage_file_ =
|
type, ucode_dwords, ucode_dword_count, ucode_data_hash);
|
||||||
xe::filesystem::OpenFile(pipeline_storage_file_path, "a+b");
|
if (shader->ucode_storage_index() == storage_index) {
|
||||||
if (!pipeline_storage_file_) {
|
return true; // Already loaded.
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
shaders_translation_thread_cond.wait(lock);
|
shader->set_ucode_storage_index(storage_index);
|
||||||
continue;
|
return true;
|
||||||
}
|
},
|
||||||
shader_to_translate = shaders_to_translate.front();
|
// Shader translate callback.
|
||||||
shaders_to_translate.pop_front();
|
[this, edram_rov_used](const std::set<std::pair<uint64_t, uint64_t>>
|
||||||
++shader_translation_threads_busy;
|
& translations_needed) {
|
||||||
break;
|
TranslateShadersForStorage(translations_needed, edram_rov_used);
|
||||||
}
|
},
|
||||||
if (!shader_to_translate->is_ucode_analyzed()) {
|
pipeline_stored_descriptions)) {
|
||||||
shader_to_translate->AnalyzeUcode(ucode_disasm_buffer);
|
return;
|
||||||
}
|
|
||||||
// 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_storage_file_flush_needed_ = false;
|
||||||
|
pipeline_storage_file_flush_needed_ = false;
|
||||||
|
|
||||||
// Create the pipelines.
|
// Create the pipelines.
|
||||||
if (!pipeline_stored_descriptions.empty()) {
|
if (!pipeline_stored_descriptions.empty()) {
|
||||||
@@ -574,12 +275,16 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
|
|
||||||
// Launch additional creation threads to use all cores to create
|
// Launch additional creation threads to use all cores to create
|
||||||
// pipelines faster. Will also be using the main thread, so minus 1.
|
// 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_original_count = creation_threads_.size();
|
||||||
size_t creation_thread_needed_count = std::max(
|
size_t creation_thread_needed_count = std::max(
|
||||||
std::min(pipeline_stored_descriptions.size(), logical_processor_count) -
|
std::min(pipeline_stored_descriptions.size(), logical_processor_count) -
|
||||||
size_t(1),
|
size_t(1),
|
||||||
creation_thread_original_count);
|
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();
|
size_t creation_thread_index = creation_threads_.size();
|
||||||
std::unique_ptr<xe::threading::Thread> creation_thread =
|
std::unique_ptr<xe::threading::Thread> creation_thread =
|
||||||
xe::threading::Thread::Create({}, [this, creation_thread_index]() {
|
xe::threading::Thread::Create({}, [this, creation_thread_index]() {
|
||||||
@@ -591,6 +296,12 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t pipelines_created = 0;
|
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 :
|
for (const PipelineStoredDescription& pipeline_stored_description :
|
||||||
pipeline_stored_descriptions) {
|
pipeline_stored_descriptions) {
|
||||||
const PipelineDescription& pipeline_description =
|
const PipelineDescription& pipeline_description =
|
||||||
@@ -610,6 +321,7 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pipeline_found) {
|
if (pipeline_found) {
|
||||||
|
++pipelines_already_exist;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,6 +329,9 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
auto vertex_shader_it =
|
auto vertex_shader_it =
|
||||||
shaders_.find(pipeline_description.vertex_shader_hash);
|
shaders_.find(pipeline_description.vertex_shader_hash);
|
||||||
if (vertex_shader_it == shaders_.end()) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
D3D12Shader* vertex_shader = vertex_shader_it->second;
|
D3D12Shader* vertex_shader = vertex_shader_it->second;
|
||||||
@@ -627,6 +342,12 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
if (!pipeline_runtime_description.vertex_shader ||
|
if (!pipeline_runtime_description.vertex_shader ||
|
||||||
!pipeline_runtime_description.vertex_shader->is_translated() ||
|
!pipeline_runtime_description.vertex_shader->is_translated() ||
|
||||||
!pipeline_runtime_description.vertex_shader->is_valid()) {
|
!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;
|
continue;
|
||||||
}
|
}
|
||||||
D3D12Shader* pixel_shader;
|
D3D12Shader* pixel_shader;
|
||||||
@@ -634,6 +355,9 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
auto pixel_shader_it =
|
auto pixel_shader_it =
|
||||||
shaders_.find(pipeline_description.pixel_shader_hash);
|
shaders_.find(pipeline_description.pixel_shader_hash);
|
||||||
if (pixel_shader_it == shaders_.end()) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
pixel_shader = pixel_shader_it->second;
|
pixel_shader = pixel_shader_it->second;
|
||||||
@@ -644,6 +368,12 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
if (!pipeline_runtime_description.pixel_shader ||
|
if (!pipeline_runtime_description.pixel_shader ||
|
||||||
!pipeline_runtime_description.pixel_shader->is_translated() ||
|
!pipeline_runtime_description.pixel_shader->is_translated() ||
|
||||||
!pipeline_runtime_description.pixel_shader->is_valid()) {
|
!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;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -669,6 +399,11 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
pipeline_description.vertex_shader_modification)
|
pipeline_description.vertex_shader_modification)
|
||||||
.vertex.host_vertex_shader_type));
|
.vertex.host_vertex_shader_type));
|
||||||
if (!pipeline_runtime_description.root_signature) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
std::memcpy(&pipeline_runtime_description.description,
|
std::memcpy(&pipeline_runtime_description.description,
|
||||||
@@ -707,29 +442,36 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!creation_threads_.empty()) {
|
if (!creation_threads_.empty()) {
|
||||||
CreateQueuedPipelinesOnProcessorThread();
|
if (blocking) {
|
||||||
if (creation_threads_.size() > creation_thread_original_count) {
|
// Blocking mode: help drain the queue on this thread, then wait for
|
||||||
{
|
// background threads to finish.
|
||||||
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
CreateQueuedPipelinesOnProcessorThread();
|
||||||
creation_threads_shutdown_from_ = creation_thread_original_count;
|
if (creation_threads_.size() > creation_thread_original_count) {
|
||||||
// Assuming the queue is empty because of
|
{
|
||||||
// CreateQueuedPipelinesOnProcessorThread.
|
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
||||||
}
|
creation_threads_shutdown_from_ = creation_thread_original_count;
|
||||||
creation_request_cond_.notify_all();
|
// Assuming the queue is empty because of
|
||||||
while (creation_threads_.size() > creation_thread_original_count) {
|
// CreateQueuedPipelinesOnProcessorThread.
|
||||||
xe::threading::Wait(creation_threads_.back().get(), false);
|
}
|
||||||
creation_threads_.pop_back();
|
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;
|
bool await_creation_completion_event;
|
||||||
{
|
{
|
||||||
// Cleanup so additional threads can be created later again.
|
|
||||||
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
std::lock_guard<xe_mutex> lock(creation_request_lock_);
|
||||||
creation_threads_shutdown_from_ = SIZE_MAX;
|
await_creation_completion_event = creation_threads_busy_ != 0;
|
||||||
// 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;
|
|
||||||
if (await_creation_completion_event) {
|
if (await_creation_completion_event) {
|
||||||
creation_completion_event_->Reset();
|
creation_completion_event_->Reset();
|
||||||
creation_completion_set_event_ = true;
|
creation_completion_set_event_ = true;
|
||||||
@@ -739,87 +481,62 @@ void PipelineCache::InitializeShaderStorage(
|
|||||||
creation_request_cond_.notify_one();
|
creation_request_cond_.notify_one();
|
||||||
xe::threading::Wait(creation_completion_event_.get(), false);
|
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(
|
XELOGI(
|
||||||
"Created {} graphics pipelines (not including reading the "
|
"Pipeline cache loaded: {} created, {} already exist, {} total stored",
|
||||||
"descriptions) from the storage in {} milliseconds",
|
pipelines_created, pipelines_already_exist,
|
||||||
pipelines_created,
|
pipeline_stored_descriptions.size());
|
||||||
(xe::Clock::QueryHostTickCount() - pipeline_creation_start_) * 1000 /
|
if (pipelines_vs_not_found || pipelines_vs_translation_missing ||
|
||||||
xe::Clock::QueryHostTickFrequency());
|
pipelines_ps_not_found || pipelines_ps_translation_missing ||
|
||||||
// If any pipeline descriptions were corrupted (or the whole file has excess
|
pipelines_root_sig_failed) {
|
||||||
// bytes in the end), truncate to the last valid pipeline description.
|
XELOGI(
|
||||||
xe::filesystem::TruncateStdioFile(
|
"Pipeline cache skipped: {} VS not found, {} VS translation missing, "
|
||||||
pipeline_storage_file_,
|
"{} PS not found, {} PS translation missing, {} root sig failed",
|
||||||
uint64_t(sizeof(pipeline_storage_file_header) +
|
pipelines_vs_not_found, pipelines_vs_translation_missing,
|
||||||
sizeof(PipelineStoredDescription) *
|
pipelines_ps_not_found, pipelines_ps_translation_missing,
|
||||||
pipeline_stored_descriptions.size()));
|
pipelines_root_sig_failed);
|
||||||
} else {
|
}
|
||||||
xe::filesystem::TruncateStdioFile(pipeline_storage_file_, 0);
|
XELOGI("Pipeline creation took {} milliseconds",
|
||||||
pipeline_storage_file_header.magic = pipeline_storage_magic;
|
(xe::Clock::QueryHostTickCount() - pipeline_creation_start_) * 1000 /
|
||||||
pipeline_storage_file_header.magic_api = pipeline_storage_magic_api;
|
xe::Clock::QueryHostTickFrequency());
|
||||||
pipeline_storage_file_header.version_swapped =
|
|
||||||
pipeline_storage_version_swapped;
|
|
||||||
fwrite(&pipeline_storage_file_header, sizeof(pipeline_storage_file_header),
|
|
||||||
1, pipeline_storage_file_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shader_storage_cache_root_ = cache_root;
|
|
||||||
shader_storage_title_id_ = title_id;
|
shader_storage_title_id_ = title_id;
|
||||||
|
|
||||||
// Start the storage writing thread.
|
// Invoke completion callback if provided (for blocking mode or when no
|
||||||
storage_write_flush_shaders_ = false;
|
// background work was needed). For non-blocking mode with background work,
|
||||||
storage_write_flush_pipelines_ = false;
|
// the callback is stored and invoked by CreationThread when done.
|
||||||
storage_write_thread_shutdown_ = false;
|
if (completion_callback) {
|
||||||
storage_write_thread_ =
|
completion_callback();
|
||||||
xe::threading::Thread::Create({}, [this]() { StorageWriteThread(); });
|
}
|
||||||
assert_not_null(storage_write_thread_);
|
|
||||||
storage_write_thread_->set_name("D3D12 Storage writer");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PipelineCache::ShutdownShaderStorage() {
|
void PipelineCache::ShutdownShaderStorage() {
|
||||||
if (storage_write_thread_) {
|
// Shut down the storage writer (closes files, stops write thread).
|
||||||
{
|
storage_writer_.ShutdownShaderStorage();
|
||||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
shader_storage_file_flush_needed_ = false;
|
||||||
storage_write_thread_shutdown_ = true;
|
pipeline_storage_file_flush_needed_ = false;
|
||||||
}
|
|
||||||
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();
|
|
||||||
shader_storage_title_id_ = 0;
|
shader_storage_title_id_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PipelineCache::EndSubmission() {
|
void PipelineCache::EndSubmission() {
|
||||||
if (shader_storage_file_flush_needed_ ||
|
if (shader_storage_file_flush_needed_ ||
|
||||||
pipeline_storage_file_flush_needed_) {
|
pipeline_storage_file_flush_needed_) {
|
||||||
{
|
storage_writer_.RequestFlush(shader_storage_file_flush_needed_,
|
||||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
pipeline_storage_file_flush_needed_);
|
||||||
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();
|
|
||||||
shader_storage_file_flush_needed_ = false;
|
shader_storage_file_flush_needed_ = false;
|
||||||
pipeline_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!");
|
XELOGE("Failed to translate the vertex shader!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (shader_storage_file_ && vertex_shader->shader().ucode_storage_index() !=
|
if (storage_writer_.is_active() &&
|
||||||
shader_storage_index_) {
|
vertex_shader->shader().try_set_ucode_storage_index(
|
||||||
vertex_shader->shader().set_ucode_storage_index(shader_storage_index_);
|
storage_writer_.storage_index())) {
|
||||||
assert_not_null(storage_write_thread_);
|
|
||||||
shader_storage_file_flush_needed_ = true;
|
shader_storage_file_flush_needed_ = true;
|
||||||
{
|
storage_writer_.QueueShaderWrite(&vertex_shader->shader());
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!use_async && !vertex_shader->is_valid()) {
|
if (!use_async && !vertex_shader->is_valid()) {
|
||||||
@@ -1066,17 +778,11 @@ bool PipelineCache::ConfigurePipeline(
|
|||||||
XELOGE("Failed to translate the pixel shader!");
|
XELOGE("Failed to translate the pixel shader!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (shader_storage_file_ &&
|
if (storage_writer_.is_active() &&
|
||||||
pixel_shader->shader().ucode_storage_index() !=
|
pixel_shader->shader().try_set_ucode_storage_index(
|
||||||
shader_storage_index_) {
|
storage_writer_.storage_index())) {
|
||||||
pixel_shader->shader().set_ucode_storage_index(shader_storage_index_);
|
|
||||||
assert_not_null(storage_write_thread_);
|
|
||||||
shader_storage_file_flush_needed_ = true;
|
shader_storage_file_flush_needed_ = true;
|
||||||
{
|
storage_writer_.QueueShaderWrite(&pixel_shader->shader());
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!pixel_shader->is_valid()) {
|
if (!pixel_shader->is_valid()) {
|
||||||
@@ -1145,19 +851,13 @@ bool PipelineCache::ConfigurePipeline(
|
|||||||
std::memory_order_release);
|
std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pipeline_storage_file_) {
|
if (storage_writer_.is_active()) {
|
||||||
assert_not_null(storage_write_thread_);
|
|
||||||
pipeline_storage_file_flush_needed_ = true;
|
pipeline_storage_file_flush_needed_ = true;
|
||||||
{
|
PipelineStoredDescription stored_description;
|
||||||
std::lock_guard<std::mutex> lock(storage_write_request_lock_);
|
stored_description.description_hash = hash;
|
||||||
storage_write_pipeline_queue_.emplace_back();
|
std::memcpy(&stored_description.description, &description,
|
||||||
PipelineStoredDescription& stored_description =
|
sizeof(description));
|
||||||
storage_write_pipeline_queue_.back();
|
storage_writer_.QueuePipelineWrite(stored_description);
|
||||||
stored_description.description_hash = hash;
|
|
||||||
std::memcpy(&stored_description.description, &description,
|
|
||||||
sizeof(description));
|
|
||||||
}
|
|
||||||
storage_write_request_cond_.notify_all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current_pipeline_ = new_pipeline;
|
current_pipeline_ = new_pipeline;
|
||||||
@@ -1363,6 +1063,127 @@ bool PipelineCache::TranslateAnalyzedShader(
|
|||||||
return translation.is_valid();
|
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(
|
bool PipelineCache::GetCurrentStateDescription(
|
||||||
D3D12Shader::D3D12Translation* vertex_shader,
|
D3D12Shader::D3D12Translation* vertex_shader,
|
||||||
D3D12Shader::D3D12Translation* pixel_shader,
|
D3D12Shader::D3D12Translation* pixel_shader,
|
||||||
@@ -2901,6 +2722,13 @@ void PipelineCache::EnsurePipelineShadersTranslated(
|
|||||||
dxc_utils, dxc_compiler)) {
|
dxc_utils, dxc_compiler)) {
|
||||||
XELOGE("Failed to translate {} shader {:016X}", shader_type,
|
XELOGE("Failed to translate {} shader {:016X}", shader_type,
|
||||||
shader.ucode_data_hash());
|
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;
|
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) {
|
void PipelineCache::CreationThread(size_t thread_index) {
|
||||||
// Create thread-local translator to avoid contention with main thread.
|
// Create thread-local translator to avoid contention with main thread.
|
||||||
// This mirrors what the shader storage loading threads do.
|
// 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_);
|
std::unique_lock<xe_mutex> lock(creation_request_lock_);
|
||||||
if (thread_index >= creation_threads_shutdown_from_ ||
|
if (thread_index >= creation_threads_shutdown_from_ ||
|
||||||
creation_queue_.empty()) {
|
creation_queue_.empty()) {
|
||||||
if (creation_completion_set_event_ && creation_threads_busy_ == 0) {
|
if (creation_threads_busy_ == 0) {
|
||||||
// Last pipeline in the queue created - signal the event if requested.
|
// Last pipeline in the queue created.
|
||||||
creation_completion_set_event_ = false;
|
if (creation_completion_set_event_) {
|
||||||
creation_completion_event_->Set();
|
// 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_) {
|
if (thread_index >= creation_threads_shutdown_from_) {
|
||||||
// Cleanup thread-local resources.
|
// Cleanup thread-local resources.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
#include "xenia/gpu/primitive_processor.h"
|
#include "xenia/gpu/primitive_processor.h"
|
||||||
#include "xenia/gpu/register_file.h"
|
#include "xenia/gpu/register_file.h"
|
||||||
#include "xenia/gpu/registers.h"
|
#include "xenia/gpu/registers.h"
|
||||||
|
#include "xenia/gpu/shader_storage.h"
|
||||||
#include "xenia/gpu/xenos.h"
|
#include "xenia/gpu/xenos.h"
|
||||||
#include "xenia/ui/d3d12/d3d12_api.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
|
// takes a long time, and if it's not, there will be heavy stuttering for the
|
||||||
// rest of the execution of the guest).
|
// rest of the execution of the guest).
|
||||||
|
|
||||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
void InitializeShaderStorage(
|
||||||
uint32_t title_id, bool blocking);
|
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||||
|
std::function<void()> completion_callback = nullptr);
|
||||||
void ShutdownShaderStorage();
|
void ShutdownShaderStorage();
|
||||||
|
|
||||||
void EndSubmission();
|
void EndSubmission();
|
||||||
@@ -113,15 +116,6 @@ class PipelineCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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
|
// Update PipelineDescription::kVersion if any of the Pipeline* enums are
|
||||||
// changed!
|
// changed!
|
||||||
|
|
||||||
@@ -289,6 +283,11 @@ class PipelineCache {
|
|||||||
IDxcUtils* dxc_utils = nullptr,
|
IDxcUtils* dxc_utils = nullptr,
|
||||||
IDxcCompiler* dxc_compiler = 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
|
// If draw_util::IsRasterizationPotentiallyDone is false, the pixel shader
|
||||||
// MUST be made nullptr BEFORE calling this! The shaders must be translated
|
// MUST be made nullptr BEFORE calling this! The shaders must be translated
|
||||||
// and valid, unless for_placeholder is true.
|
// and valid, unless for_placeholder is true.
|
||||||
@@ -415,33 +414,14 @@ class PipelineCache {
|
|||||||
// changed.
|
// changed.
|
||||||
Pipeline* current_pipeline_ = nullptr;
|
Pipeline* current_pipeline_ = nullptr;
|
||||||
|
|
||||||
// Currently open shader storage path.
|
// Currently open shader storage state.
|
||||||
std::filesystem::path shader_storage_cache_root_;
|
|
||||||
uint32_t shader_storage_title_id_ = 0;
|
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.
|
// Storage writer for shaders and pipelines (owns file handles and storage
|
||||||
FILE* shader_storage_file_ = nullptr;
|
// index).
|
||||||
// For only writing shaders to the currently open storage once, incremented
|
ShaderStorageWriter<PipelineStoredDescription> storage_writer_;
|
||||||
// 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_;
|
|
||||||
|
|
||||||
// Pipeline creation threads.
|
// Pipeline creation threads.
|
||||||
void CreationThread(size_t thread_index);
|
void CreationThread(size_t thread_index);
|
||||||
@@ -466,6 +446,9 @@ class PipelineCache {
|
|||||||
// Whether setting the event on completion is queued. Protected with
|
// Whether setting the event on completion is queued. Protected with
|
||||||
// creation_request_lock_, notify_one creation_request_cond_ when set.
|
// creation_request_lock_, notify_one creation_request_cond_ when set.
|
||||||
bool creation_completion_set_event_ = false;
|
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
|
// Creation threads with this index or above need to be shut down as soon as
|
||||||
// possible. Protected with creation_request_lock_, notify_all
|
// possible. Protected with creation_request_lock_, notify_all
|
||||||
// creation_request_cond_ when set.
|
// creation_request_cond_ when set.
|
||||||
|
|||||||
@@ -379,27 +379,38 @@ void GraphicsSystem::ClearCaches() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GraphicsSystem::InitializeShaderStorage(
|
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 (!cvars::store_shaders) {
|
||||||
|
if (completion_callback) {
|
||||||
|
completion_callback();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (blocking) {
|
if (blocking) {
|
||||||
if (command_processor_->is_paused()) {
|
if (command_processor_->is_paused()) {
|
||||||
// Safe to run on any thread while the command processor is paused, no
|
// Safe to run on any thread while the command processor is paused, no
|
||||||
// race condition.
|
// race condition.
|
||||||
command_processor_->InitializeShaderStorage(cache_root, title_id, true);
|
command_processor_->InitializeShaderStorage(
|
||||||
|
cache_root, title_id, true, std::move(completion_callback));
|
||||||
} else {
|
} else {
|
||||||
xe::threading::Fence fence;
|
xe::threading::Fence fence;
|
||||||
command_processor_->CallInThread([this, cache_root, title_id, &fence]() {
|
command_processor_->CallInThread(
|
||||||
command_processor_->InitializeShaderStorage(cache_root, title_id, true);
|
[this, cache_root, title_id, &fence,
|
||||||
fence.Signal();
|
completion_callback = std::move(completion_callback)]() mutable {
|
||||||
});
|
command_processor_->InitializeShaderStorage(
|
||||||
|
cache_root, title_id, true, std::move(completion_callback));
|
||||||
|
fence.Signal();
|
||||||
|
});
|
||||||
fence.Wait();
|
fence.Wait();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
command_processor_->CallInThread([this, cache_root, title_id]() {
|
command_processor_->CallInThread(
|
||||||
command_processor_->InitializeShaderStorage(cache_root, title_id, false);
|
[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();
|
virtual void ClearCaches();
|
||||||
|
|
||||||
void InitializeShaderStorage(const std::filesystem::path& cache_root,
|
void InitializeShaderStorage(
|
||||||
uint32_t title_id, bool blocking);
|
const std::filesystem::path& cache_root, uint32_t title_id, bool blocking,
|
||||||
|
std::function<void()> completion_callback = nullptr);
|
||||||
|
|
||||||
void RequestFrameTrace();
|
void RequestFrameTrace();
|
||||||
void BeginTracing();
|
void BeginTracing();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#define XENIA_GPU_SHADER_H_
|
#define XENIA_GPU_SHADER_H_
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -1006,9 +1007,20 @@ class Shader {
|
|||||||
// An externally managed identifier of the shader storage the microcode of the
|
// 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
|
// shader was last written to, or was loaded from, to only write the shader
|
||||||
// microcode to the storage once. UINT32_MAX by default.
|
// 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) {
|
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
|
// Dumps the shader's microcode binary and, if analyzed, disassembly, to files
|
||||||
@@ -1073,7 +1085,7 @@ class Shader {
|
|||||||
// Modification bits -> translation.
|
// Modification bits -> translation.
|
||||||
std::unordered_map<uint64_t, Translation*> translations_;
|
std::unordered_map<uint64_t, Translation*> translations_;
|
||||||
|
|
||||||
uint32_t ucode_storage_index_ = UINT32_MAX;
|
std::atomic<uint32_t> ucode_storage_index_{UINT32_MAX};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void GatherExecInformation(
|
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);
|
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) {}
|
void VulkanCommandProcessor::RestoreEdramSnapshot(const void* snapshot) {}
|
||||||
|
|
||||||
std::string VulkanCommandProcessor::GetWindowTitleText() const {
|
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 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;
|
void RestoreEdramSnapshot(const void* snapshot) override;
|
||||||
|
|
||||||
ui::vulkan::VulkanDevice* GetVulkanDevice() const {
|
ui::vulkan::VulkanDevice* GetVulkanDevice() const {
|
||||||
|
|||||||
@@ -11,9 +11,12 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
#include "third_party/fmt/include/fmt/format.h"
|
#include "third_party/fmt/include/fmt/format.h"
|
||||||
#include "xenia/base/assert.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/logging.h"
|
||||||
#include "xenia/base/math.h"
|
#include "xenia/base/math.h"
|
||||||
#include "xenia/base/profiling.h"
|
#include "xenia/base/profiling.h"
|
||||||
@@ -218,6 +221,9 @@ bool VulkanPipelineCache::Initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VulkanPipelineCache::Shutdown() {
|
void VulkanPipelineCache::Shutdown() {
|
||||||
|
// Shut down shader storage first.
|
||||||
|
ShutdownShaderStorage();
|
||||||
|
|
||||||
// Shut down all threads, before destroying the pipelines since they may be
|
// Shut down all threads, before destroying the pipelines since they may be
|
||||||
// creating them.
|
// creating them.
|
||||||
if (!creation_threads_.empty()) {
|
if (!creation_threads_.empty()) {
|
||||||
@@ -231,6 +237,13 @@ void VulkanPipelineCache::Shutdown() {
|
|||||||
}
|
}
|
||||||
creation_threads_.clear();
|
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();
|
creation_completion_event_.reset();
|
||||||
|
|
||||||
const ui::vulkan::VulkanDevice* const vulkan_device =
|
const ui::vulkan::VulkanDevice* const vulkan_device =
|
||||||
@@ -579,6 +592,28 @@ bool VulkanPipelineCache::ConfigurePipeline(
|
|||||||
auto& pipeline_pair =
|
auto& pipeline_pair =
|
||||||
*pipelines_.emplace(description, Pipeline(pipeline_layout)).first;
|
*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.
|
// Get tessellation shaders if needed.
|
||||||
VkShaderModule tessellation_vertex_shader = VK_NULL_HANDLE;
|
VkShaderModule tessellation_vertex_shader = VK_NULL_HANDLE;
|
||||||
VkShaderModule tessellation_control_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.pixel_shader = pixel_shader;
|
||||||
creation_arguments.geometry_shader = geometry_shader;
|
creation_arguments.geometry_shader = geometry_shader;
|
||||||
creation_arguments.tessellation_vertex_shader = tessellation_vertex_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;
|
creation_arguments.render_pass = render_pass;
|
||||||
if (!EnsurePipelineCreated(creation_arguments)) {
|
if (!EnsurePipelineCreated(creation_arguments)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -683,31 +719,42 @@ bool VulkanPipelineCache::ConfigurePipeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VulkanPipelineCache::EndSubmission() {
|
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()) {
|
if (creation_threads_.empty()) {
|
||||||
// Process deferred destructions when GPU is idle
|
// Process deferred destructions when GPU is idle
|
||||||
ProcessDeferredDestructions();
|
ProcessDeferredDestructions();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Await creation of all queued pipelines.
|
|
||||||
bool await_creation_completion_event;
|
if (startup_loading_) {
|
||||||
{
|
// Non-blocking: let background threads work asynchronously.
|
||||||
std::lock_guard<std::mutex> lock(creation_request_lock_);
|
creation_request_cond_.notify_one();
|
||||||
// Assuming the creation queue is already empty (because the processor
|
} else {
|
||||||
// thread also worked on creating the leftover pipelines), so only check
|
// Blocking: wait for all queued pipelines.
|
||||||
// if there are threads with pipelines currently being created.
|
bool await_creation_completion_event;
|
||||||
await_creation_completion_event =
|
{
|
||||||
!creation_queue_.empty() || creation_threads_busy_ != 0;
|
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) {
|
if (await_creation_completion_event) {
|
||||||
creation_completion_event_->Reset();
|
creation_request_cond_.notify_one();
|
||||||
creation_completion_set_event_.store(true, std::memory_order_release);
|
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();
|
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_;
|
--creation_threads_busy_;
|
||||||
if (creation_completion_set_event_.load(std::memory_order_acquire) &&
|
if (creation_threads_busy_ == 0 && creation_queue_.empty()) {
|
||||||
creation_threads_busy_ == 0 && creation_queue_.empty()) {
|
// All pipelines created.
|
||||||
creation_completion_set_event_.store(false, std::memory_order_release);
|
if (creation_completion_set_event_.load(std::memory_order_acquire)) {
|
||||||
creation_completion_event_->Set();
|
// 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;
|
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(
|
void VulkanPipelineCache::WritePipelineRenderTargetDescription(
|
||||||
reg::RB_BLENDCONTROL blend_control, uint32_t write_mask,
|
reg::RB_BLENDCONTROL blend_control, uint32_t write_mask,
|
||||||
PipelineRenderTarget& render_target_out) const {
|
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 vulkan
|
||||||
} // namespace gpu
|
} // namespace gpu
|
||||||
} // namespace xe
|
} // namespace xe
|
||||||
|
|||||||
@@ -13,12 +13,15 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
#include <set>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
#include "xenia/gpu/primitive_processor.h"
|
#include "xenia/gpu/primitive_processor.h"
|
||||||
#include "xenia/gpu/register_file.h"
|
#include "xenia/gpu/register_file.h"
|
||||||
#include "xenia/gpu/registers.h"
|
#include "xenia/gpu/registers.h"
|
||||||
|
#include "xenia/gpu/shader_storage.h"
|
||||||
#include "xenia/gpu/spirv_shader_translator.h"
|
#include "xenia/gpu/spirv_shader_translator.h"
|
||||||
#include "xenia/gpu/vulkan/vulkan_render_target_cache.h"
|
#include "xenia/gpu/vulkan/vulkan_render_target_cache.h"
|
||||||
#include "xenia/gpu/vulkan/vulkan_shader.h"
|
#include "xenia/gpu/vulkan/vulkan_shader.h"
|
||||||
@@ -101,6 +105,12 @@ class VulkanPipelineCache {
|
|||||||
bool Initialize();
|
bool Initialize();
|
||||||
void Shutdown();
|
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();
|
void EndSubmission();
|
||||||
bool IsCreatingPipelines();
|
bool IsCreatingPipelines();
|
||||||
|
|
||||||
@@ -259,6 +269,18 @@ class VulkanPipelineCache {
|
|||||||
return size_t(description.GetHash());
|
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.
|
// creation threads, with everything needed from caches pre-looked-up.
|
||||||
@@ -314,6 +336,11 @@ class VulkanPipelineCache {
|
|||||||
bool TranslateAnalyzedShader(SpirvShaderTranslator& translator,
|
bool TranslateAnalyzedShader(SpirvShaderTranslator& translator,
|
||||||
VulkanShader::VulkanTranslation& translation);
|
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(
|
void WritePipelineRenderTargetDescription(
|
||||||
reg::RB_BLENDCONTROL blend_control, uint32_t write_mask,
|
reg::RB_BLENDCONTROL blend_control, uint32_t write_mask,
|
||||||
PipelineRenderTarget& render_target_out) const;
|
PipelineRenderTarget& render_target_out) const;
|
||||||
@@ -452,6 +479,10 @@ class VulkanPipelineCache {
|
|||||||
std::condition_variable creation_request_cond_;
|
std::condition_variable creation_request_cond_;
|
||||||
std::unique_ptr<xe::threading::Event> creation_completion_event_ = nullptr;
|
std::unique_ptr<xe::threading::Event> creation_completion_event_ = nullptr;
|
||||||
std::atomic<bool> creation_completion_set_event_{false};
|
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.
|
// Deferred destruction of replaced shader modules and pipelines.
|
||||||
// Pipelines are only destroyed after the GPU submission that might reference
|
// 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.
|
// last potentially used in. Only destroyed when that submission completes.
|
||||||
std::vector<std::pair<VkPipeline, uint64_t>> deferred_destroy_pipelines_;
|
std::vector<std::pair<VkPipeline, uint64_t>> deferred_destroy_pipelines_;
|
||||||
std::mutex deferred_destroy_mutex_;
|
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
|
} // namespace vulkan
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ XE_UI_VULKAN_FUNCTION(vkGetBufferMemoryRequirements)
|
|||||||
XE_UI_VULKAN_FUNCTION(vkGetDeviceQueue)
|
XE_UI_VULKAN_FUNCTION(vkGetDeviceQueue)
|
||||||
XE_UI_VULKAN_FUNCTION(vkGetFenceStatus)
|
XE_UI_VULKAN_FUNCTION(vkGetFenceStatus)
|
||||||
XE_UI_VULKAN_FUNCTION(vkGetImageMemoryRequirements)
|
XE_UI_VULKAN_FUNCTION(vkGetImageMemoryRequirements)
|
||||||
|
XE_UI_VULKAN_FUNCTION(vkGetPipelineCacheData)
|
||||||
XE_UI_VULKAN_FUNCTION(vkInvalidateMappedMemoryRanges)
|
XE_UI_VULKAN_FUNCTION(vkInvalidateMappedMemoryRanges)
|
||||||
XE_UI_VULKAN_FUNCTION(vkMapMemory)
|
XE_UI_VULKAN_FUNCTION(vkMapMemory)
|
||||||
XE_UI_VULKAN_FUNCTION(vkResetCommandPool)
|
XE_UI_VULKAN_FUNCTION(vkResetCommandPool)
|
||||||
|
|||||||
Reference in New Issue
Block a user