/** ****************************************************************************** * 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 #include #include #include #include #include #include #include #include #include #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: // /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 inline uint64_t ReadShaderEntries(FILE* file, Callback&& callback) { uint64_t valid_bytes = sizeof(ShaderStorageFileHeader); ShaderStoredHeader shader_header; std::vector 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 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; // 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>& 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& 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> 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 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 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 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 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 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 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 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 storage_write_thread_; std::mutex storage_write_request_lock_; std::condition_variable storage_write_request_cond_; std::deque storage_write_shader_queue_; std::deque 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_