Files
Xenia-Canary/src/xenia/gpu/shader_storage.h
Herman S. 64e51c544e [GPU] Add vulkan shader disk storage / startup loading
Big refactor of the shader storage to allow both backends to share code
2026-03-10 00:49:56 +09:00

563 lines
20 KiB
C++

/**
******************************************************************************
* 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_