diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e96e867..ed2c4aa0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,14 @@ project(ac6recomp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Keep reconfigure passes pinned to the vendored SDK when the cache is empty. +if(NOT DEFINED REXSDK_DIR OR REXSDK_DIR STREQUAL "") + set(_ac6_vendored_rexsdk "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/rexglue-sdk") + if(EXISTS "${_ac6_vendored_rexsdk}/CMakeLists.txt") + set(REXSDK_DIR "${_ac6_vendored_rexsdk}" CACHE PATH "Path to rexglue-sdk source tree" FORCE) + endif() +endif() + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/generated/rexglue.cmake") include(generated/rexglue.cmake) else() diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/device.h b/thirdparty/rexglue-sdk/include/rex/filesystem/device.h new file mode 100644 index 00000000..023cc8d2 --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/device.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/devices/host_path_device.h b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/host_path_device.h new file mode 100644 index 00000000..e2d05944 --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/host_path_device.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/devices/null_device.h b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/null_device.h new file mode 100644 index 00000000..d685ebcf --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/null_device.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_device.h b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_device.h new file mode 100644 index 00000000..a30bcec2 --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_device.h @@ -0,0 +1,142 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2020 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace rex::filesystem { + +// https://free60project.github.io/wiki/STFS.html + +class StfsContainerEntry; + +class StfsContainerDevice : public Device { + public: + const static uint32_t kBlockSize = 0x1000; + + StfsContainerDevice(const std::string_view mount_path, const std::filesystem::path& host_path); + ~StfsContainerDevice() override; + + bool Initialize() override; + + bool is_read_only() const override { + return header_.metadata.volume_type != XContentVolumeType::kStfs || + header_.metadata.volume_descriptor.stfs.flags.bits.read_only_format; + } + + void Dump(string::StringBuffer* string_buffer) override; + Entry* ResolvePath(const std::string_view path) override; + + const std::string& name() const override { return name_; } + uint32_t attributes() const override { return 0; } + uint32_t component_name_max_length() const override { return 40; } + const StfsHeader& header() const { return header_; } + + // Reads and validates the StfsHeader from an STFS package file without + // mounting the device. Returns nullptr if the file is missing, too small, + // or has an invalid magic. + static std::unique_ptr ReadPackageHeader(const std::filesystem::path& file_path); + + uint32_t total_allocation_units() const override { + if (header_.metadata.volume_type == XContentVolumeType::kStfs) { + return header_.metadata.volume_descriptor.stfs.total_block_count; + } + + return uint32_t(data_size() / sectors_per_allocation_unit() / bytes_per_sector()); + } + uint32_t available_allocation_units() const override { + if (!is_read_only()) { + auto& descriptor = header_.metadata.volume_descriptor.stfs; + return kBlocksPerHashLevel[2] - (descriptor.total_block_count - descriptor.free_block_count); + } + return 0; + } + uint32_t sectors_per_allocation_unit() const override { return 8; } + uint32_t bytes_per_sector() const override { return 0x200; } + + size_t data_size() const { + if (header_.header.header_size) { + if (header_.metadata.volume_type == XContentVolumeType::kStfs) { + return header_.metadata.volume_descriptor.stfs.total_block_count * kBlockSize; + } + return files_total_size_ - rex::round_up(header_.header.header_size, kBlockSize); + } + return files_total_size_ - sizeof(StfsHeader); + } + + private: + const uint32_t kBlocksPerHashLevel[3] = {170, 28900, 4913000}; + const uint32_t kEndOfChain = 0xFFFFFF; + const uint32_t kEntriesPerDirectoryBlock = kBlockSize / sizeof(StfsDirectoryEntry); + + enum class Error { + kSuccess = 0, + kErrorOutOfMemory = -1, + kErrorReadError = -10, + kErrorFileMismatch = -30, + kErrorDamagedFile = -31, + kErrorTooSmall = -32, + }; + + enum class SvodLayoutType { + kUnknown = 0x0, + kEnhancedGDF = 0x1, + kXSF = 0x2, + kSingleFile = 0x4, + }; + + XContentPackageType ReadMagic(const std::filesystem::path& path); + bool ResolveFromFolder(const std::filesystem::path& path); + + Error OpenFiles(); + void CloseFiles(); + + Error ReadHeaderAndVerify(FILE* header_file); + + Error ReadSVOD(); + Error ReadEntrySVOD(uint32_t sector, uint32_t ordinal, StfsContainerEntry* parent); + void BlockToOffsetSVOD(size_t sector, size_t* address, size_t* file_index); + + Error ReadSTFS(); + size_t BlockToOffsetSTFS(uint64_t block_index) const; + uint32_t BlockToHashBlockNumberSTFS(uint32_t block_index, uint32_t hash_level) const; + size_t BlockToHashBlockOffsetSTFS(uint32_t block_index, uint32_t hash_level) const; + + const StfsHashEntry* GetBlockHash(uint32_t block_index); + + std::string name_; + std::filesystem::path host_path_; + + std::map files_; + size_t files_total_size_; + + size_t svod_base_offset_; + + std::unique_ptr root_entry_; + StfsHeader header_; + SvodLayoutType svod_layout_; + uint32_t blocks_per_hash_table_; + uint32_t block_step[2]; + + std::unordered_map cached_hash_tables_; +}; + +} // namespace rex::filesystem diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_entry.h b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_entry.h new file mode 100644 index 00000000..45d7254b --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_entry.h @@ -0,0 +1,60 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2020 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace rex::filesystem { +typedef std::map MultiFileHandles; + +class StfsContainerDevice; + +class StfsContainerEntry : public Entry { + public: + StfsContainerEntry(Device* device, Entry* parent, const std::string_view path, + MultiFileHandles* files); + ~StfsContainerEntry() override; + + static std::unique_ptr Create(Device* device, Entry* parent, + const std::string_view name, + MultiFileHandles* files); + + MultiFileHandles* files() const { return files_; } + size_t data_offset() const { return data_offset_; } + size_t data_size() const { return data_size_; } + size_t block() const { return block_; } + + X_STATUS Open(uint32_t desired_access, File** out_file) override; + + struct BlockRecord { + size_t file; + size_t offset; + size_t length; + }; + const std::vector& block_list() const { return block_list_; } + + private: + friend class StfsContainerDevice; + + MultiFileHandles* files_; + size_t data_offset_; + size_t data_size_; + size_t block_; + std::vector block_list_; +}; + +} // namespace rex::filesystem diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_file.h b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_file.h new file mode 100644 index 00000000..c583e243 --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_container_file.h @@ -0,0 +1,40 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2014 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime + */ + +#pragma once + +#include + +#include + +namespace rex::filesystem { + +class StfsContainerEntry; + +class StfsContainerFile : public File { + public: + StfsContainerFile(uint32_t file_access, StfsContainerEntry* entry); + ~StfsContainerFile() override; + + void Destroy() override; + + X_STATUS ReadSync(std::span buffer, size_t byte_offset, size_t* out_bytes_read) override; + X_STATUS WriteSync(std::span buffer, size_t byte_offset, + size_t* out_bytes_written) override { + return X_STATUS_ACCESS_DENIED; + } + X_STATUS SetLength(size_t length) override { return X_STATUS_ACCESS_DENIED; } + + private: + StfsContainerEntry* entry_; +}; + +} // namespace rex::filesystem diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_xbox.h b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_xbox.h new file mode 100644 index 00000000..a2c77098 --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/devices/stfs_xbox.h @@ -0,0 +1,479 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace rex::filesystem { + +// Import kernel content types for STFS structures +using rex::system::XContentType; +using rex::system::XLanguage; + +// Convert FAT timestamp to 100-nanosecond intervals since January 1, 1601 (UTC) +inline uint64_t decode_fat_timestamp(const uint32_t date, const uint32_t time) { + struct tm tm = {0}; + // 80 is the difference between 1980 (FAT) and 1900 (tm); + tm.tm_year = ((0xFE00 & date) >> 9) + 80; + tm.tm_mon = ((0x01E0 & date) >> 5) - 1; + tm.tm_mday = (0x001F & date) >> 0; + tm.tm_hour = (0xF800 & time) >> 11; + tm.tm_min = (0x07E0 & time) >> 5; + tm.tm_sec = (0x001F & time) << 1; // the value stored in 2-seconds intervals + tm.tm_isdst = 0; + +#if REX_PLATFORM_WIN32 + time_t timet = _mkgmtime(&tm); +#else + time_t timet = timegm(&tm); +#endif + + if (timet == -1) { + return 0; + } + // 11644473600LL is a difference between 1970 and 1601 + return (timet + 11644473600LL) * 10000000; +} + +// Structs used for interchange between Xenia and actual Xbox360 kernel/XAM + +inline uint32_t load_uint24_be(const uint8_t* p) { + return (uint32_t(p[0]) << 16) | (uint32_t(p[1]) << 8) | uint32_t(p[2]); +} +inline uint32_t load_uint24_le(const uint8_t* p) { + return (uint32_t(p[2]) << 16) | (uint32_t(p[1]) << 8) | uint32_t(p[0]); +} +inline void store_uint24_le(uint8_t* p, uint32_t value) { + p[2] = uint8_t((value >> 16) & 0xFF); + p[1] = uint8_t((value >> 8) & 0xFF); + p[0] = uint8_t(value & 0xFF); +} + +enum class XContentPackageType : uint32_t { + kCon = 0x434F4E20, + kPirs = 0x50495253, + kLive = 0x4C495645, +}; + +enum class XContentVolumeType : uint32_t { + kStfs = 0, + kSvod = 1, +}; + +/* STFS structures */ +#pragma pack(push, 1) +struct StfsVolumeDescriptor { + uint8_t descriptor_length; + uint8_t version; + union { + uint8_t as_byte; + struct { + uint8_t read_only_format : 1; // if set, only uses a single backing-block + // per hash table (no resiliency), + // otherwise uses two + uint8_t root_active_index : 1; // if set, uses secondary backing-block + // for the highest-level hash table + + uint8_t directory_overallocated : 1; + uint8_t directory_index_bounds_valid : 1; + } bits; + } flags; + uint16_t file_table_block_count; + uint8_t file_table_block_number_raw[3]; + uint8_t top_hash_table_hash[0x14]; + be total_block_count; + be free_block_count; + + uint32_t file_table_block_number() const { return load_uint24_le(file_table_block_number_raw); } + + void set_file_table_block_number(uint32_t value) { + store_uint24_le(file_table_block_number_raw, value); + } + + bool is_valid() const { return descriptor_length == sizeof(StfsVolumeDescriptor); } +}; +static_assert_size(StfsVolumeDescriptor, 0x24); +#pragma pack(pop) + +enum class StfsHashState : uint8_t { + kFree = 0, // unallocated but doesn't exist in package (needs to expand)? + kFree2 = 1, // unallocated but exists in package? + kInUse = 2, +}; + +struct StfsHashEntry { + uint8_t sha1[0x14]; + + rex::be info_raw; + + uint32_t level0_next_block() const { return info_raw & 0xFFFFFF; } + void set_level0_next_block(uint32_t value) { + info_raw = (info_raw & ~0xFFFFFF) | (value & 0xFFFFFF); + } + + StfsHashState level0_allocation_state() const { + return StfsHashState(uint8_t(((info_raw & 0xC0000000) >> 30) & 0xFF)); + } + void set_level0_allocation_state(StfsHashState value) { + info_raw = (info_raw & ~0xC0000000) | (uint32_t(value) << 30); + } + + uint32_t levelN_num_blocks_free() const { return info_raw & 0x7FFF; } + void set_levelN_num_blocks_free(uint32_t value) { + info_raw = (info_raw & ~0x7FFF) | (value & 0x7FFF); + } + + uint32_t levelN_num_blocks_unk() const { return ((info_raw & 0x3FFF8000) >> 15) & 0x7FFF; } + void set_levelN_num_blocks_unk(uint32_t value) { + info_raw = (info_raw & ~0x3FFF8000) | ((value & 0x7FFF) << 15); + } + + bool levelN_active_index() const { return (info_raw & 0x40000000) != 0; } + void set_levelN_active_index(bool value) { + info_raw = (info_raw & ~0x40000000) | (value ? 0x40000000 : 0); + } + + bool levelN_writeable() const { return (info_raw & 0x80000000) != 0; } + void set_levelN_writeable(bool value) { + info_raw = (info_raw & ~0x80000000) | (value ? 0x80000000 : 0); + } +}; +static_assert_size(StfsHashEntry, 0x18); + +struct StfsHashTable { + StfsHashEntry entries[170]; + rex::be num_blocks; // num L0 blocks covered by this table? + uint8_t padding[12]; +}; +static_assert_size(StfsHashTable, 0x1000); + +struct StfsDirectoryEntry { + char name[40]; + + struct { + uint8_t name_length : 6; + uint8_t contiguous : 1; + uint8_t directory : 1; + } flags; + + uint8_t valid_data_blocks_raw[3]; + uint8_t allocated_data_blocks_raw[3]; + uint8_t start_block_number_raw[3]; + + be directory_index; + + be length; + + be create_date; + be create_time; + be modified_date; + be modified_time; + + uint32_t valid_data_blocks() const { return load_uint24_le(valid_data_blocks_raw); } + + void set_valid_data_blocks(uint32_t value) { store_uint24_le(valid_data_blocks_raw, value); } + + uint32_t allocated_data_blocks() const { return load_uint24_le(allocated_data_blocks_raw); } + + void set_allocated_data_blocks(uint32_t value) { + store_uint24_le(allocated_data_blocks_raw, value); + } + + uint32_t start_block_number() const { return load_uint24_le(start_block_number_raw); } + + void set_start_block_number(uint32_t value) { store_uint24_le(start_block_number_raw, value); } +}; +static_assert_size(StfsDirectoryEntry, 0x40); + +struct StfsDirectoryBlock { + StfsDirectoryEntry entries[0x40]; +}; +static_assert_size(StfsDirectoryBlock, 0x1000); + +/* SVOD structures */ +struct SvodDeviceDescriptor { + uint8_t descriptor_length; + uint8_t block_cache_element_count; + uint8_t worker_thread_processor; + uint8_t worker_thread_priority; + uint8_t first_fragment_hash_entry[0x14]; + union { + uint8_t as_byte; + struct { + uint8_t must_be_zero_for_future_usage : 6; + uint8_t enhanced_gdf_layout : 1; + uint8_t zero_for_downlevel_clients : 1; + } bits; + } features; + uint8_t num_data_blocks_raw[3]; + uint8_t start_data_block_raw[3]; + uint8_t reserved[5]; + + uint32_t num_data_blocks() { return load_uint24_le(num_data_blocks_raw); } + + uint32_t start_data_block() { return load_uint24_le(start_data_block_raw); } +}; +static_assert_size(SvodDeviceDescriptor, 0x24); + +/* XContent structures */ +struct XContentLicense { + be licensee_id; + be license_bits; + be license_flags; +}; +static_assert_size(XContentLicense, 0x10); + +struct XContentMediaData { + uint8_t series_id[0x10]; + uint8_t season_id[0x10]; + be season_number; + be episode_number; +}; +static_assert_size(XContentMediaData, 0x24); + +struct XContentAvatarAssetData { + be sub_category; + be colorizable; + uint8_t asset_id[0x10]; + uint8_t skeleton_version_mask; + uint8_t reserved[0xB]; +}; +static_assert_size(XContentAvatarAssetData, 0x24); + +struct XContentAttributes { + uint8_t profile_transfer : 1; + uint8_t device_transfer : 1; + uint8_t move_only_transfer : 1; + uint8_t kinect_enabled : 1; + uint8_t disable_network_storage : 1; + uint8_t deep_link_supported : 1; + uint8_t reserved : 2; +}; +static_assert_size(XContentAttributes, 1); + +#pragma pack(push, 1) +struct XContentMetadata { + static const uint32_t kThumbLengthV1 = 0x4000; + static const uint32_t kThumbLengthV2 = 0x3D00; + + static const uint32_t kNumLanguagesV1 = 9; + // metadata_version 2 adds 3 languages inside thumbnail/title_thumbnail space + static const uint32_t kNumLanguagesV2 = 12; + + be content_type; + be metadata_version; + be content_size; + xex2_opt_execution_info execution_info; + uint8_t console_id[5]; + be profile_id; + union { + StfsVolumeDescriptor stfs; + SvodDeviceDescriptor svod; + } volume_descriptor; + be data_file_count; + be data_file_size; + be volume_type; + be online_creator; + be category; + uint8_t reserved2[0x20]; + union { + XContentMediaData media_data; + XContentAvatarAssetData avatar_asset_data; + } metadata_v2; + uint8_t device_id[0x14]; + union { + be uint[kNumLanguagesV1][128]; + char16_t chars[kNumLanguagesV1][128]; + } display_name_raw; + union { + be uint[kNumLanguagesV1][128]; + char16_t chars[kNumLanguagesV1][128]; + } description_raw; + union { + be uint[64]; + char16_t chars[64]; + } publisher_raw; + union { + be uint[64]; + char16_t chars[64]; + } title_name_raw; + union { + uint8_t as_byte; + XContentAttributes bits; + } flags; + be thumbnail_size; + be title_thumbnail_size; + uint8_t thumbnail[kThumbLengthV2]; + union { + be uint[kNumLanguagesV2 - kNumLanguagesV1][128]; + char16_t chars[kNumLanguagesV2 - kNumLanguagesV1][128]; + } display_name_ex_raw; + uint8_t title_thumbnail[kThumbLengthV2]; + union { + be uint[kNumLanguagesV2 - kNumLanguagesV1][128]; + char16_t chars[kNumLanguagesV2 - kNumLanguagesV1][128]; + } description_ex_raw; + + std::u16string display_name(XLanguage language) const { + uint32_t lang_id = uint32_t(language) - 1; + + if (lang_id >= kNumLanguagesV2) { + assert_always(); + // no room for this lang, read from english slot.. + lang_id = uint32_t(XLanguage::kEnglish) - 1; + } + + const be* str = 0; + if (lang_id >= 0 && lang_id < kNumLanguagesV1) { + str = display_name_raw.uint[lang_id]; + } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 && metadata_version >= 2) { + str = display_name_ex_raw.uint[lang_id - kNumLanguagesV1]; + } + + if (!str) { + // Invalid language ID? + assert_always(); + return u""; + } + + return memory::load_and_swap(str); + } + + std::u16string description(XLanguage language) const { + uint32_t lang_id = uint32_t(language) - 1; + + if (lang_id >= kNumLanguagesV2) { + assert_always(); + // no room for this lang, read from english slot.. + lang_id = uint32_t(XLanguage::kEnglish) - 1; + } + + const be* str = 0; + if (lang_id >= 0 && lang_id < kNumLanguagesV1) { + str = description_raw.uint[lang_id]; + } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 && metadata_version >= 2) { + str = description_ex_raw.uint[lang_id - kNumLanguagesV1]; + } + + if (!str) { + // Invalid language ID? + assert_always(); + return u""; + } + + return memory::load_and_swap(str); + } + + std::u16string publisher() const { + return memory::load_and_swap(publisher_raw.uint); + } + + std::u16string title_name() const { + return memory::load_and_swap(title_name_raw.uint); + } + + bool set_display_name(XLanguage language, const std::u16string_view value) { + uint32_t lang_id = uint32_t(language) - 1; + + if (lang_id >= kNumLanguagesV2) { + assert_always(); + // no room for this lang, store in english slot.. + lang_id = uint32_t(XLanguage::kEnglish) - 1; + } + + char16_t* str = 0; + if (lang_id >= 0 && lang_id < kNumLanguagesV1) { + str = display_name_raw.chars[lang_id]; + } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 && metadata_version >= 2) { + str = display_name_ex_raw.chars[lang_id - kNumLanguagesV1]; + } + + if (!str) { + // Invalid language ID? + assert_always(); + return false; + } + + rex::string::util_copy_and_swap_truncating(str, value, countof(display_name_raw.chars[0])); + return true; + } + + bool set_description(XLanguage language, const std::u16string_view value) { + uint32_t lang_id = uint32_t(language) - 1; + + if (lang_id >= kNumLanguagesV2) { + assert_always(); + // no room for this lang, store in english slot.. + lang_id = uint32_t(XLanguage::kEnglish) - 1; + } + + char16_t* str = 0; + if (lang_id >= 0 && lang_id < kNumLanguagesV1) { + str = description_raw.chars[lang_id]; + } else if (lang_id >= kNumLanguagesV1 && lang_id < kNumLanguagesV2 && metadata_version >= 2) { + str = description_ex_raw.chars[lang_id - kNumLanguagesV1]; + } + + if (!str) { + // Invalid language ID? + assert_always(); + return false; + } + + rex::string::util_copy_and_swap_truncating(str, value, countof(description_raw.chars[0])); + return true; + } + + void set_publisher(const std::u16string_view value) { + rex::string::util_copy_and_swap_truncating(publisher_raw.chars, value, + countof(publisher_raw.chars)); + } + + void set_title_name(const std::u16string_view value) { + rex::string::util_copy_and_swap_truncating(title_name_raw.chars, value, + countof(title_name_raw.chars)); + } +}; +static_assert_size(XContentMetadata, 0x93D6); + +struct XContentHeader { + be magic; + uint8_t signature[0x228]; + XContentLicense licenses[0x10]; + uint8_t content_id[0x14]; + be header_size; + + bool is_magic_valid() const { + return magic == XContentPackageType::kCon || magic == XContentPackageType::kLive || + magic == XContentPackageType::kPirs; + } +}; +static_assert_size(XContentHeader, 0x344); +#pragma pack(pop) + +struct StfsHeader { + XContentHeader header; + XContentMetadata metadata; + // TODO: title/system updates contain more data after XContentMetadata, seems + // to affect header.header_size +}; +static_assert_size(StfsHeader, 0x971A); + +} // namespace rex::filesystem diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/entry.h b/thirdparty/rexglue-sdk/include/rex/filesystem/entry.h new file mode 100644 index 00000000..9786abe7 --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/entry.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/file.h b/thirdparty/rexglue-sdk/include/rex/filesystem/file.h new file mode 100644 index 00000000..9bddb207 --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/file.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/thirdparty/rexglue-sdk/include/rex/filesystem/wildcard.h b/thirdparty/rexglue-sdk/include/rex/filesystem/wildcard.h new file mode 100644 index 00000000..6e3c6aff --- /dev/null +++ b/thirdparty/rexglue-sdk/include/rex/filesystem/wildcard.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/thirdparty/rexglue-sdk/include/rex/logging.h b/thirdparty/rexglue-sdk/include/rex/logging.h index 5aac27f2..d1b98357 100644 --- a/thirdparty/rexglue-sdk/include/rex/logging.h +++ b/thirdparty/rexglue-sdk/include/rex/logging.h @@ -17,7 +17,7 @@ // Function declarations, CVAR declarations #include -// Logging macros (parameterized, legacy aliases, FN, TID) +// Logging macros (parameterized, per-subsystem aliases, category definition) #include // Fatal error and assertion macros diff --git a/thirdparty/rexglue-sdk/include/rex/logging/api.h b/thirdparty/rexglue-sdk/include/rex/logging/api.h index 1bbf6a07..1e85ab7b 100644 --- a/thirdparty/rexglue-sdk/include/rex/logging/api.h +++ b/thirdparty/rexglue-sdk/include/rex/logging/api.h @@ -13,6 +13,7 @@ #include +#include #include #include @@ -21,7 +22,10 @@ REXCVAR_DECLARE(std::string, log_level); REXCVAR_DECLARE(std::string, log_file); REXCVAR_DECLARE(bool, log_verbose); -REXCVAR_DECLARE(bool, enable_console); +REXCVAR_DECLARE(bool, log_noisy); +REXCVAR_DECLARE(int32_t, log_flush_interval); +REXCVAR_DECLARE(int32_t, log_max_file_size_mb); +REXCVAR_DECLARE(int32_t, log_max_files); namespace rex { @@ -49,12 +53,19 @@ void InitLogging(const LogConfig& config); /** * Initialize logging with simple parameters (convenience overload). * - * @param log_file Path to log file, or nullptr for console-only. + * @param log_file Path to log file, or nullptr for no file logging. * @param level Default log level for all categories. */ void InitLogging(const char* log_file = nullptr, spdlog::level::level_enum level = spdlog::level::info); +/** + * Early-phase logging initialization (before config is loaded). + * Creates a platform debug sink (OutputDebugString on Windows, stdout elsewhere) + * so log lines emitted before InitLogging() is called are captured. + */ +void InitLoggingEarly(); + /** * Flush all loggers and shut down the logging system. */ @@ -75,6 +86,9 @@ void ShutdownLogging(); */ LogCategoryId RegisterLogCategory(const char* name); +LogCategoryId RegisterLogSubcategory(const char* name, LogCategoryId parent); +void SetRootLevel(LogCategoryId root, spdlog::level::level_enum level); + /** * Look up a category by name. * @@ -171,7 +185,7 @@ void RemoveSink(spdlog::sink_ptr sink); void RemoveSink(LogCategoryId category, spdlog::sink_ptr sink); /** - * Update the format pattern on the console sink. + * Update the format pattern on the stdout console sink. * * @param pattern spdlog pattern string. */ @@ -218,14 +232,7 @@ spdlog::level::level_enum ParseLogLevelOr(const std::string& level_str, LogConfig BuildLogConfig(const char* log_file, const std::string& cli_level, const std::map& category_levels); -/** - * Get the current guest thread ID for logging. - * - * This is a weak symbol that returns 0 by default. Override in the - * runtime library to return the actual guest thread ID. - * - * @return Guest thread ID, or 0 if not in guest context. - */ -uint32_t GetLogGuestThreadId(); +std::map ParseCategoryLevelsFromConfig( + const std::filesystem::path& config_path); } // namespace rex diff --git a/thirdparty/rexglue-sdk/include/rex/logging/macros.h b/thirdparty/rexglue-sdk/include/rex/logging/macros.h index 72e49305..6ca295ef 100644 --- a/thirdparty/rexglue-sdk/include/rex/logging/macros.h +++ b/thirdparty/rexglue-sdk/include/rex/logging/macros.h @@ -1,6 +1,6 @@ /** * @file rex/logging/macros.h - * @brief Logging macros - parameterized, legacy aliases, function-prefixed, thread-ID + * @brief Logging macros - parameterized, per-subsystem aliases, category definition * * @copyright Copyright (c) 2026 Tom Clay * All rights reserved. @@ -22,6 +22,13 @@ rex_log_ptr_->log(spdlog::source_loc{__FILE__, __LINE__, __FUNCTION__}, lvl, __VA_ARGS__); \ } while (0) +#define REX_LOG_NOISY_IMPL(cat, lvl, ...) \ + do { \ + if (!REXCVAR_GET(log_noisy)) \ + break; \ + REX_LOG_IMPL(cat, lvl, __VA_ARGS__); \ + } while (0) + /* --- Parameterized Macros (Primary API) --------------------------------- */ /** @{ */ @@ -33,127 +40,94 @@ #define REXLOG_CAT_CRITICAL(cat, ...) REX_LOG_IMPL(cat, spdlog::level::critical, __VA_ARGS__) /** @} */ -/* --- Function-Prefixed Parameterized Macros ----------------------------- */ +/* --- Noisy Parameterized Macros (cvar-gated) ------------------------------ */ + +#define REXLOG_CAT_NOISY_TRACE(cat, ...) REX_LOG_NOISY_IMPL(cat, spdlog::level::trace, __VA_ARGS__) +#define REXLOG_CAT_NOISY_DEBUG(cat, ...) REX_LOG_NOISY_IMPL(cat, spdlog::level::debug, __VA_ARGS__) + +/* --- Per-Subsystem Alias Macros - Core Category -------------------------- */ /** @{ */ -#define REXLOG_CAT_FN_TRACE(cat, fmt, ...) \ - REXLOG_CAT_TRACE(cat, "{}: " fmt, __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_FN_DEBUG(cat, fmt, ...) \ - REXLOG_CAT_DEBUG(cat, "{}: " fmt, __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_FN_INFO(cat, fmt, ...) \ - REXLOG_CAT_INFO(cat, "{}: " fmt, __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_FN_WARN(cat, fmt, ...) \ - REXLOG_CAT_WARN(cat, "{}: " fmt, __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_FN_ERROR(cat, fmt, ...) \ - REXLOG_CAT_ERROR(cat, "{}: " fmt, __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_FN_CRITICAL(cat, fmt, ...) \ - REXLOG_CAT_CRITICAL(cat, "{}: " fmt, __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) +#define REXLOG_TRACE(...) REXLOG_CAT_TRACE(::rex::log::core(), __VA_ARGS__) +#define REXLOG_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::core(), __VA_ARGS__) +#define REXLOG_INFO(...) REXLOG_CAT_INFO(::rex::log::core(), __VA_ARGS__) +#define REXLOG_WARN(...) REXLOG_CAT_WARN(::rex::log::core(), __VA_ARGS__) +#define REXLOG_ERROR(...) REXLOG_CAT_ERROR(::rex::log::core(), __VA_ARGS__) +#define REXLOG_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::core(), __VA_ARGS__) /** @} */ -/* --- Thread-ID Parameterized Macros ------------------------------------- */ - -/** @{ */ -#define REXLOG_CAT_TID_TRACE(cat, fmt, ...) \ - REXLOG_CAT_TRACE(cat, "[T:{:08X}] {}: " fmt, ::rex::GetLogGuestThreadId(), \ - __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_TID_DEBUG(cat, fmt, ...) \ - REXLOG_CAT_DEBUG(cat, "[T:{:08X}] {}: " fmt, ::rex::GetLogGuestThreadId(), \ - __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_TID_INFO(cat, fmt, ...) \ - REXLOG_CAT_INFO(cat, "[T:{:08X}] {}: " fmt, ::rex::GetLogGuestThreadId(), \ - __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_TID_WARN(cat, fmt, ...) \ - REXLOG_CAT_WARN(cat, "[T:{:08X}] {}: " fmt, ::rex::GetLogGuestThreadId(), \ - __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_TID_ERROR(cat, fmt, ...) \ - REXLOG_CAT_ERROR(cat, "[T:{:08X}] {}: " fmt, ::rex::GetLogGuestThreadId(), \ - __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#define REXLOG_CAT_TID_CRITICAL(cat, fmt, ...) \ - REXLOG_CAT_CRITICAL(cat, "[T:{:08X}] {}: " fmt, ::rex::GetLogGuestThreadId(), \ - __FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -/** @} */ - -/* --- Legacy Alias Macros - Core Category -------------------------------- */ - -/** @{ */ -#define REXLOG_TRACE(...) REXLOG_CAT_TRACE(::rex::log::Core, __VA_ARGS__) -#define REXLOG_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::Core, __VA_ARGS__) -#define REXLOG_INFO(...) REXLOG_CAT_INFO(::rex::log::Core, __VA_ARGS__) -#define REXLOG_WARN(...) REXLOG_CAT_WARN(::rex::log::Core, __VA_ARGS__) -#define REXLOG_ERROR(...) REXLOG_CAT_ERROR(::rex::log::Core, __VA_ARGS__) -#define REXLOG_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::Core, __VA_ARGS__) -/** @} */ - -/* --- Legacy Alias Macros - Per-Subsystem -------------------------------- */ +/* --- Per-Subsystem Alias Macros ------------------------------------------ */ /** @{ CPU */ -#define REXCPU_TRACE(...) REXLOG_CAT_TRACE(::rex::log::CPU, __VA_ARGS__) -#define REXCPU_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::CPU, __VA_ARGS__) -#define REXCPU_INFO(...) REXLOG_CAT_INFO(::rex::log::CPU, __VA_ARGS__) -#define REXCPU_WARN(...) REXLOG_CAT_WARN(::rex::log::CPU, __VA_ARGS__) -#define REXCPU_ERROR(...) REXLOG_CAT_ERROR(::rex::log::CPU, __VA_ARGS__) -#define REXCPU_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::CPU, __VA_ARGS__) +#define REXCPU_TRACE(...) REXLOG_CAT_TRACE(::rex::log::cpu(), __VA_ARGS__) +#define REXCPU_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::cpu(), __VA_ARGS__) +#define REXCPU_INFO(...) REXLOG_CAT_INFO(::rex::log::cpu(), __VA_ARGS__) +#define REXCPU_WARN(...) REXLOG_CAT_WARN(::rex::log::cpu(), __VA_ARGS__) +#define REXCPU_ERROR(...) REXLOG_CAT_ERROR(::rex::log::cpu(), __VA_ARGS__) +#define REXCPU_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::cpu(), __VA_ARGS__) /** @} */ /** @{ APU */ -#define REXAPU_TRACE(...) REXLOG_CAT_TRACE(::rex::log::APU, __VA_ARGS__) -#define REXAPU_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::APU, __VA_ARGS__) -#define REXAPU_INFO(...) REXLOG_CAT_INFO(::rex::log::APU, __VA_ARGS__) -#define REXAPU_WARN(...) REXLOG_CAT_WARN(::rex::log::APU, __VA_ARGS__) -#define REXAPU_ERROR(...) REXLOG_CAT_ERROR(::rex::log::APU, __VA_ARGS__) -#define REXAPU_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::APU, __VA_ARGS__) +#define REXAPU_TRACE(...) REXLOG_CAT_TRACE(::rex::log::apu(), __VA_ARGS__) +#define REXAPU_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::apu(), __VA_ARGS__) +#define REXAPU_INFO(...) REXLOG_CAT_INFO(::rex::log::apu(), __VA_ARGS__) +#define REXAPU_WARN(...) REXLOG_CAT_WARN(::rex::log::apu(), __VA_ARGS__) +#define REXAPU_ERROR(...) REXLOG_CAT_ERROR(::rex::log::apu(), __VA_ARGS__) +#define REXAPU_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::apu(), __VA_ARGS__) /** @} */ /** @{ GPU */ -#define REXGPU_TRACE(...) REXLOG_CAT_TRACE(::rex::log::GPU, __VA_ARGS__) -#define REXGPU_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::GPU, __VA_ARGS__) -#define REXGPU_INFO(...) REXLOG_CAT_INFO(::rex::log::GPU, __VA_ARGS__) -#define REXGPU_WARN(...) REXLOG_CAT_WARN(::rex::log::GPU, __VA_ARGS__) -#define REXGPU_ERROR(...) REXLOG_CAT_ERROR(::rex::log::GPU, __VA_ARGS__) -#define REXGPU_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::GPU, __VA_ARGS__) +#define REXGPU_TRACE(...) REXLOG_CAT_TRACE(::rex::log::gpu(), __VA_ARGS__) +#define REXGPU_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::gpu(), __VA_ARGS__) +#define REXGPU_INFO(...) REXLOG_CAT_INFO(::rex::log::gpu(), __VA_ARGS__) +#define REXGPU_WARN(...) REXLOG_CAT_WARN(::rex::log::gpu(), __VA_ARGS__) +#define REXGPU_ERROR(...) REXLOG_CAT_ERROR(::rex::log::gpu(), __VA_ARGS__) +#define REXGPU_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::gpu(), __VA_ARGS__) /** @} */ /** @{ Kernel */ -#define REXKRNL_TRACE(...) REXLOG_CAT_TRACE(::rex::log::Kernel, __VA_ARGS__) -#define REXKRNL_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::Kernel, __VA_ARGS__) -#define REXKRNL_INFO(...) REXLOG_CAT_INFO(::rex::log::Kernel, __VA_ARGS__) -#define REXKRNL_WARN(...) REXLOG_CAT_WARN(::rex::log::Kernel, __VA_ARGS__) -#define REXKRNL_ERROR(...) REXLOG_CAT_ERROR(::rex::log::Kernel, __VA_ARGS__) -#define REXKRNL_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::Kernel, __VA_ARGS__) +#define REXKRNL_TRACE(...) REXLOG_CAT_TRACE(::rex::log::krnl(), __VA_ARGS__) +#define REXKRNL_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::krnl(), __VA_ARGS__) +#define REXKRNL_INFO(...) REXLOG_CAT_INFO(::rex::log::krnl(), __VA_ARGS__) +#define REXKRNL_WARN(...) REXLOG_CAT_WARN(::rex::log::krnl(), __VA_ARGS__) +#define REXKRNL_ERROR(...) REXLOG_CAT_ERROR(::rex::log::krnl(), __VA_ARGS__) +#define REXKRNL_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::krnl(), __VA_ARGS__) /** @} */ /** @{ System */ -#define REXSYS_TRACE(...) REXLOG_CAT_TRACE(::rex::log::System, __VA_ARGS__) -#define REXSYS_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::System, __VA_ARGS__) -#define REXSYS_INFO(...) REXLOG_CAT_INFO(::rex::log::System, __VA_ARGS__) -#define REXSYS_WARN(...) REXLOG_CAT_WARN(::rex::log::System, __VA_ARGS__) -#define REXSYS_ERROR(...) REXLOG_CAT_ERROR(::rex::log::System, __VA_ARGS__) -#define REXSYS_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::System, __VA_ARGS__) +#define REXSYS_TRACE(...) REXLOG_CAT_TRACE(::rex::log::sys(), __VA_ARGS__) +#define REXSYS_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::sys(), __VA_ARGS__) +#define REXSYS_INFO(...) REXLOG_CAT_INFO(::rex::log::sys(), __VA_ARGS__) +#define REXSYS_WARN(...) REXLOG_CAT_WARN(::rex::log::sys(), __VA_ARGS__) +#define REXSYS_ERROR(...) REXLOG_CAT_ERROR(::rex::log::sys(), __VA_ARGS__) +#define REXSYS_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::sys(), __VA_ARGS__) /** @} */ /** @{ Filesystem */ -#define REXFS_TRACE(...) REXLOG_CAT_TRACE(::rex::log::FS, __VA_ARGS__) -#define REXFS_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::FS, __VA_ARGS__) -#define REXFS_INFO(...) REXLOG_CAT_INFO(::rex::log::FS, __VA_ARGS__) -#define REXFS_WARN(...) REXLOG_CAT_WARN(::rex::log::FS, __VA_ARGS__) -#define REXFS_ERROR(...) REXLOG_CAT_ERROR(::rex::log::FS, __VA_ARGS__) -#define REXFS_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::FS, __VA_ARGS__) +#define REXFS_TRACE(...) REXLOG_CAT_TRACE(::rex::log::fs(), __VA_ARGS__) +#define REXFS_DEBUG(...) REXLOG_CAT_DEBUG(::rex::log::fs(), __VA_ARGS__) +#define REXFS_INFO(...) REXLOG_CAT_INFO(::rex::log::fs(), __VA_ARGS__) +#define REXFS_WARN(...) REXLOG_CAT_WARN(::rex::log::fs(), __VA_ARGS__) +#define REXFS_ERROR(...) REXLOG_CAT_ERROR(::rex::log::fs(), __VA_ARGS__) +#define REXFS_CRITICAL(...) REXLOG_CAT_CRITICAL(::rex::log::fs(), __VA_ARGS__) /** @} */ -/* --- Legacy Function-Prefixed (Core) ------------------------------------ */ +/* --- Noisy Aliases - Per-Subsystem ---------------------------------------- */ -/** @{ */ -#define REXLOGFN_TRACE(fmt, ...) \ - REXLOG_CAT_FN_TRACE(::rex::log::Core, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXLOGFN_DEBUG(fmt, ...) \ - REXLOG_CAT_FN_DEBUG(::rex::log::Core, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXLOGFN_INFO(fmt, ...) REXLOG_CAT_FN_INFO(::rex::log::Core, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXLOGFN_WARN(fmt, ...) REXLOG_CAT_FN_WARN(::rex::log::Core, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXLOGFN_ERROR(fmt, ...) \ - REXLOG_CAT_FN_ERROR(::rex::log::Core, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXLOGFN_CRITICAL(fmt, ...) \ - REXLOG_CAT_FN_CRITICAL(::rex::log::Core, fmt __VA_OPT__(, ) __VA_ARGS__) -/** @} */ +#define REXLOG_NOISY_TRACE(...) REXLOG_CAT_NOISY_TRACE(::rex::log::core(), __VA_ARGS__) +#define REXLOG_NOISY_DEBUG(...) REXLOG_CAT_NOISY_DEBUG(::rex::log::core(), __VA_ARGS__) +#define REXCPU_NOISY_TRACE(...) REXLOG_CAT_NOISY_TRACE(::rex::log::cpu(), __VA_ARGS__) +#define REXCPU_NOISY_DEBUG(...) REXLOG_CAT_NOISY_DEBUG(::rex::log::cpu(), __VA_ARGS__) +#define REXAPU_NOISY_TRACE(...) REXLOG_CAT_NOISY_TRACE(::rex::log::apu(), __VA_ARGS__) +#define REXAPU_NOISY_DEBUG(...) REXLOG_CAT_NOISY_DEBUG(::rex::log::apu(), __VA_ARGS__) +#define REXGPU_NOISY_TRACE(...) REXLOG_CAT_NOISY_TRACE(::rex::log::gpu(), __VA_ARGS__) +#define REXGPU_NOISY_DEBUG(...) REXLOG_CAT_NOISY_DEBUG(::rex::log::gpu(), __VA_ARGS__) +#define REXKRNL_NOISY_TRACE(...) REXLOG_CAT_NOISY_TRACE(::rex::log::krnl(), __VA_ARGS__) +#define REXKRNL_NOISY_DEBUG(...) REXLOG_CAT_NOISY_DEBUG(::rex::log::krnl(), __VA_ARGS__) +#define REXSYS_NOISY_TRACE(...) REXLOG_CAT_NOISY_TRACE(::rex::log::sys(), __VA_ARGS__) +#define REXSYS_NOISY_DEBUG(...) REXLOG_CAT_NOISY_DEBUG(::rex::log::sys(), __VA_ARGS__) +#define REXFS_NOISY_TRACE(...) REXLOG_CAT_NOISY_TRACE(::rex::log::fs(), __VA_ARGS__) +#define REXFS_NOISY_DEBUG(...) REXLOG_CAT_NOISY_DEBUG(::rex::log::fs(), __VA_ARGS__) /* --- Custom Category Definition ----------------------------------------- */ @@ -176,19 +150,34 @@ } \ } -/* --- Legacy Kernel Thread-ID -------------------------------------------- */ +// For dynamic parents (defined via REXLOG_DEFINE_CATEGORY) +#define REXLOG_DEFINE_SUBCATEGORY(child, parent) \ + namespace rex::log { \ + inline ::rex::LogCategoryId parent##_##child() { \ + static const ::rex::LogCategoryId id = \ + ::rex::RegisterLogSubcategory(#child, ::rex::log::parent()); \ + return id; \ + } \ + } -/** @{ */ -#define REXKRNLFN_TRACE(fmt, ...) \ - REXLOG_CAT_TID_TRACE(::rex::log::Kernel, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXKRNLFN_DEBUG(fmt, ...) \ - REXLOG_CAT_TID_DEBUG(::rex::log::Kernel, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXKRNLFN_INFO(fmt, ...) \ - REXLOG_CAT_TID_INFO(::rex::log::Kernel, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXKRNLFN_WARN(fmt, ...) \ - REXLOG_CAT_TID_WARN(::rex::log::Kernel, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXKRNLFN_ERROR(fmt, ...) \ - REXLOG_CAT_TID_ERROR(::rex::log::Kernel, fmt __VA_OPT__(, ) __VA_ARGS__) -#define REXKRNLFN_CRITICAL(fmt, ...) \ - REXLOG_CAT_TID_CRITICAL(::rex::log::Kernel, fmt __VA_OPT__(, ) __VA_ARGS__) -/** @} */ +/* --- Built-in SDK Categories ---------------------------------------------- */ + +REXLOG_DEFINE_CATEGORY(core) +REXLOG_DEFINE_CATEGORY(cpu) +REXLOG_DEFINE_CATEGORY(apu) +REXLOG_DEFINE_CATEGORY(gpu) +REXLOG_DEFINE_CATEGORY(krnl) +REXLOG_DEFINE_CATEGORY(sys) +REXLOG_DEFINE_CATEGORY(fs) + +// Backward-compatible aliases for older call sites that referenced the +// built-in categories as constants rather than accessor functions. +namespace rex::log { +inline const ::rex::LogCategoryId CORE = core(); +inline const ::rex::LogCategoryId CPU = cpu(); +inline const ::rex::LogCategoryId APU = apu(); +inline const ::rex::LogCategoryId GPU = gpu(); +inline const ::rex::LogCategoryId KRNL = krnl(); +inline const ::rex::LogCategoryId SYS = sys(); +inline const ::rex::LogCategoryId FS = fs(); +} diff --git a/thirdparty/rexglue-sdk/include/rex/logging/types.h b/thirdparty/rexglue-sdk/include/rex/logging/types.h index 201db061..984e28a4 100644 --- a/thirdparty/rexglue-sdk/include/rex/logging/types.h +++ b/thirdparty/rexglue-sdk/include/rex/logging/types.h @@ -26,8 +26,8 @@ namespace rex { * Lightweight handle identifying a log category. * * Internally a uint16_t index into the global category registry. - * SDK built-in categories are constexpr globals; consumer categories - * are obtained at runtime via RegisterLogCategory(). + * SDK built-in categories are registered via REXLOG_DEFINE_CATEGORY; + * consumer categories are obtained at runtime via RegisterLogCategory(). */ struct LogCategoryId { uint16_t id; @@ -35,23 +35,6 @@ struct LogCategoryId { constexpr bool operator==(const LogCategoryId&) const = default; }; -/** - * Pre-allocated category IDs for SDK subsystems. - * These are constexpr and resolve to array indices at zero cost. - */ -namespace log { -inline constexpr LogCategoryId Core{0}; /**< General/default messages */ -inline constexpr LogCategoryId CPU{1}; /**< CPU emulation, PPC code */ -inline constexpr LogCategoryId APU{2}; /**< Audio processing unit */ -inline constexpr LogCategoryId GPU{3}; /**< Graphics processing unit */ -inline constexpr LogCategoryId Kernel{4}; /**< Kernel/OS emulation */ -inline constexpr LogCategoryId System{5}; /**< System emulation layer */ -inline constexpr LogCategoryId FS{6}; /**< Filesystem operations */ - -/** Number of built-in SDK categories (consumer categories start after this). */ -inline constexpr uint16_t kBuiltinCount = 7; -} // namespace log - /** * Entry in the global category registry. * Each registered category has a human-readable name and its own spdlog logger. @@ -59,6 +42,8 @@ inline constexpr uint16_t kBuiltinCount = 7; struct LogCategoryEntry { std::string name; /**< Category name (e.g. "core", "app.network") */ std::shared_ptr logger; /**< Per-category spdlog logger instance */ + std::optional parent; + bool has_explicit_level = false; }; #if defined(NDEBUG) @@ -80,23 +65,22 @@ struct LogConfig { /** Global default log level applied to all categories unless overridden. */ spdlog::level::level_enum default_level = spdlog::level::info; - /** Whether to create a console (stdout) sink. */ - bool log_to_console = true; - - /** Whether the console sink uses ANSI color codes. */ - bool use_colors = true; + /** Whether to create a colored stdout sink. Intended for console-subsystem + * processes (CLI tools). Windowed apps should leave this false and rely on + * the platform debug sink created by InitLoggingEarly(). */ + bool log_to_console = false; /** Path to a log file, or nullptr for no file logging. */ const char* log_file = nullptr; - /** spdlog pattern string for the console sink. */ + /** spdlog pattern string for the stdout console sink. */ std::string console_pattern = "[%^%l%$] [%n] [t%t] %v"; /** spdlog pattern string for the file sink. */ std::string file_pattern = "[%Y-%m-%d %H:%M:%S.%e] [%l] [%n] [t%t] %v"; /** Messages at or above this level trigger an immediate flush. */ - spdlog::level::level_enum flush_level = spdlog::level::warn; + spdlog::level::level_enum flush_level = spdlog::level::info; /** * Per-category log level overrides. @@ -123,35 +107,9 @@ struct LogConfig { * Default is false (additive). */ bool category_sinks_exclusive = false; + + std::string app_name; + std::string log_dir; }; -/* ========================================================================= - Deprecated - LogCategory Enum Compatibility Layer - ========================================================================= - These exist solely for backwards compatibility with code that uses - the old LogCategory enum. Prefer LogCategoryId and the new API. - ========================================================================= */ - -/** @deprecated Use LogCategoryId and rex::log:: constants instead. */ -enum class LogCategory : size_t { - Core, - CPU, - APU, - GPU, - Kernel, - System, - FS, - Codegen, // Kept in enum for ABI compat; not initialized by SDK - Count -}; - -/** @deprecated Use FindCategory() or rex::log:: constants instead. */ -std::optional CategoryFromName(const std::string& name); - -/** @deprecated Use GetLogger(LogCategoryId) instead. */ -std::shared_ptr GetLogger(LogCategory category); - -/** @deprecated Use SetCategoryLevel(LogCategoryId, level) instead. */ -void SetCategoryLevel(LogCategory category, spdlog::level::level_enum level); - } // namespace rex diff --git a/thirdparty/rexglue-sdk/include/rex/system/xam/content_manager.h b/thirdparty/rexglue-sdk/include/rex/system/xam/content_manager.h index e17ae076..0890be5c 100644 --- a/thirdparty/rexglue-sdk/include/rex/system/xam/content_manager.h +++ b/thirdparty/rexglue-sdk/include/rex/system/xam/content_manager.h @@ -1,6 +1,12 @@ /** - * ReXGlue runtime - AC6 Recompilation project - * Copyright (c) 2026 Tom Clay. All rights reserved. + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2020 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ #pragma once @@ -157,8 +163,11 @@ class ContentManager { X_RESULT SetContentThumbnail(uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data, std::vector buffer); X_RESULT DeleteContent(uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data); + X_RESULT UnmountContent(uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data); + X_RESULT UnmountAndDeleteContent(uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data); - X_RESULT WriteContentHeaderFile(uint64_t xuid, XCONTENT_AGGREGATE_DATA data); + X_RESULT WriteContentHeaderFile(uint64_t xuid, XCONTENT_AGGREGATE_DATA data, + uint32_t license_mask = 0); X_RESULT ReadContentHeaderFile(const std::string_view file_name, uint64_t xuid, uint32_t title_id, XContentType content_type, XCONTENT_AGGREGATE_DATA& data) const; @@ -169,6 +178,11 @@ class ContentManager { // Returns the host filesystem path for an open content package, or empty. std::filesystem::path GetOpenPackagePath(const std::string_view root_name) const; + // Installs an STFS content package from an arbitrary host path. + // Extracts the package into root_path_/0000000000000000/{title_id}/00000002/{filename}/ + // and writes a .header file for XAM enumeration. + X_RESULT InstallContent(const std::filesystem::path& package_path); + private: std::filesystem::path ResolvePackageRoot(uint64_t xuid, XContentType content_type, uint32_t title_id = -1); @@ -177,6 +191,12 @@ class ContentManager { uint32_t title_id, XContentType content_type) const; + std::unordered_map::iterator + FindOpenPackageByData(const XCONTENT_AGGREGATE_DATA& data); + ContentPackage* DetachPackage(std::unordered_map::iterator it); + KernelState* kernel_state_; std::filesystem::path root_path_; diff --git a/thirdparty/rexglue-sdk/src/codegen/builders/floating_point.cpp b/thirdparty/rexglue-sdk/src/codegen/builders/floating_point.cpp index 922d35a4..8f446d2d 100644 --- a/thirdparty/rexglue-sdk/src/codegen/builders/floating_point.cpp +++ b/thirdparty/rexglue-sdk/src/codegen/builders/floating_point.cpp @@ -76,6 +76,16 @@ bool build_fctidz(BuilderContext& ctx) { return true; } +bool build_fctiw(BuilderContext& ctx) { + ctx.emit_set_flush_mode(false); + ctx.println( + "\t{0}.s64 = std::isnan({1}.f64) ? int64_t(0x80000000U) : " + "({1}.f64 > double(INT_MAX)) ? INT_MAX : " + "simde_mm_cvtsd_si32(simde_mm_load_sd(&{1}.f64));", + ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1])); + return true; +} + bool build_fctiwz(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); ctx.println( @@ -189,7 +199,7 @@ bool build_fdivs(BuilderContext& ctx) { bool build_fmadd(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = {}.f64 * {}.f64 + {}.f64;", ctx.f(ctx.insn.operands[0]), + ctx.println("\t{}.f64 = std::fma({}.f64, {}.f64, {}.f64);", ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), ctx.f(ctx.insn.operands[3])); return true; @@ -197,15 +207,15 @@ bool build_fmadd(BuilderContext& ctx) { bool build_fmadds(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = double(float({}.f64 * {}.f64 + {}.f64));", ctx.f(ctx.insn.operands[0]), - ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), + ctx.println("\t{}.f64 = double(float(std::fma({}.f64, {}.f64, {}.f64)));", + ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), ctx.f(ctx.insn.operands[3])); return true; } bool build_fmsub(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = {}.f64 * {}.f64 - {}.f64;", ctx.f(ctx.insn.operands[0]), + ctx.println("\t{}.f64 = std::fma({}.f64, {}.f64, -{}.f64);", ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), ctx.f(ctx.insn.operands[3])); return true; @@ -213,15 +223,15 @@ bool build_fmsub(BuilderContext& ctx) { bool build_fmsubs(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = double(float({}.f64 * {}.f64 - {}.f64));", ctx.f(ctx.insn.operands[0]), - ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), + ctx.println("\t{}.f64 = double(float(std::fma({}.f64, {}.f64, -{}.f64)));", + ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), ctx.f(ctx.insn.operands[3])); return true; } bool build_fnmadd(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = -({}.f64 * {}.f64 + {}.f64);", ctx.f(ctx.insn.operands[0]), + ctx.println("\t{}.f64 = -std::fma({}.f64, {}.f64, {}.f64);", ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), ctx.f(ctx.insn.operands[3])); return true; @@ -229,15 +239,15 @@ bool build_fnmadd(BuilderContext& ctx) { bool build_fnmadds(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = double(float(-({}.f64 * {}.f64 + {}.f64)));", ctx.f(ctx.insn.operands[0]), - ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), + ctx.println("\t{}.f64 = double(float(-std::fma({}.f64, {}.f64, {}.f64)));", + ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), ctx.f(ctx.insn.operands[3])); return true; } bool build_fnmsub(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = -({}.f64 * {}.f64 - {}.f64);", ctx.f(ctx.insn.operands[0]), + ctx.println("\t{}.f64 = -std::fma({}.f64, {}.f64, -{}.f64);", ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), ctx.f(ctx.insn.operands[3])); return true; @@ -245,8 +255,8 @@ bool build_fnmsub(BuilderContext& ctx) { bool build_fnmsubs(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = double(float(-({}.f64 * {}.f64 - {}.f64)));", ctx.f(ctx.insn.operands[0]), - ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), + ctx.println("\t{}.f64 = double(float(-std::fma({}.f64, {}.f64, -{}.f64)));", + ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1]), ctx.f(ctx.insn.operands[2]), ctx.f(ctx.insn.operands[3])); return true; } @@ -264,7 +274,7 @@ bool build_fres(BuilderContext& ctx) { bool build_frsqrte(BuilderContext& ctx) { ctx.emit_set_flush_mode(false); - ctx.println("\t{}.f64 = 1.0 / sqrt({}.f64);", ctx.f(ctx.insn.operands[0]), + ctx.println("\t{}.f64 = double(float(1.0 / sqrt({}.f64)));", ctx.f(ctx.insn.operands[0]), ctx.f(ctx.insn.operands[1])); return true; } diff --git a/thirdparty/rexglue-sdk/src/core/logging.cpp b/thirdparty/rexglue-sdk/src/core/logging.cpp index 5b20c8c6..0b97fab7 100644 --- a/thirdparty/rexglue-sdk/src/core/logging.cpp +++ b/thirdparty/rexglue-sdk/src/core/logging.cpp @@ -11,50 +11,91 @@ #include #include +#include +#include #include #include #include -#include +#include #include +#include + #include #include +#include + +#if REX_PLATFORM_WIN32 +#include +#else +#include +#endif REXCVAR_DEFINE_STRING(log_level, "info", "Log", "Global log level: trace, debug, info, warn, error, critical, off") .allowed({"trace", "debug", "info", "warn", "error", "critical", "off"}); -REXCVAR_DEFINE_STRING(log_file, "", "Log", "Log file path (empty = no file logging)") - .lifecycle(rex::cvar::Lifecycle::kInitOnly); +REXCVAR_DEFINE_STRING(log_file, "", "Log", "Log file path (empty = auto sequential naming)"); REXCVAR_DEFINE_BOOL(log_verbose, false, "Log", "Enable verbose logging (sets level to trace)") .debug_only(); +REXCVAR_DEFINE_BOOL(log_noisy, false, "Log", "Enable noisy/high-frequency log macros"); + +REXCVAR_DEFINE_INT32(log_flush_interval, 0, "Log", "Periodic flush interval in seconds (0 = off)") + .range(0, 60) + .lifecycle(rex::cvar::Lifecycle::kInitOnly); + +REXCVAR_DEFINE_INT32(log_max_file_size_mb, 5, "Log", "Max log file size in MB before rotation") + .range(1, 100) + .lifecycle(rex::cvar::Lifecycle::kInitOnly); + +REXCVAR_DEFINE_INT32(log_max_files, 20, "Log", "Max number of rotated log files to keep") + .range(1, 100) + .lifecycle(rex::cvar::Lifecycle::kInitOnly); + namespace rex { namespace { -// Category registry — indices 0..kBuiltinCount-1 are SDK built-ins. std::vector g_registry; - -// Shared default sinks (stored for pattern changes) spdlog::sink_ptr g_console_sink; spdlog::sink_ptr g_file_sink; - -// Extra global sinks added via AddSink() +spdlog::sink_ptr g_early_sink; std::vector g_extra_sinks; - -// Initialization state +bool g_early_initialized = false; bool g_initialized = false; std::mutex g_mutex; - -// Stored config (for resolving category_levels/category_sinks on late registration) LogConfig g_config; -// Collect the default sinks into a vector for logger construction +std::filesystem::path NextSequentialLogPath(const std::filesystem::path& logs_dir, + std::string_view app_name) { + std::filesystem::create_directories(logs_dir); + + int max_seq = 0; + std::string prefix = std::string(app_name) + "_"; + std::error_code ec; + for (const auto& entry : std::filesystem::directory_iterator(logs_dir, ec)) { + if (!entry.is_regular_file()) + continue; + auto stem = entry.path().stem().string(); + if (stem.starts_with(prefix)) { + auto num_str = stem.substr(prefix.size()); + int num = 0; + auto [ptr, parse_ec] = std::from_chars(num_str.data(), num_str.data() + num_str.size(), num); + if (parse_ec == std::errc() && ptr == num_str.data() + num_str.size()) + max_seq = std::max(max_seq, num); + } + } + + return logs_dir / fmt::format("{}_{:03d}.log", app_name, max_seq + 1); +} + std::vector BuildDefaultSinks() { std::vector sinks; + if (g_early_sink) + sinks.push_back(g_early_sink); if (g_console_sink) sinks.push_back(g_console_sink); if (g_file_sink) @@ -101,46 +142,72 @@ std::shared_ptr CreateCategoryLogger(const std::string& name) { } // namespace -// ---- Built-in category short names (must match log:: constant IDs) ---- -static constexpr const char* kBuiltinNames[] = {"core", "cpu", "apu", "gpu", "krnl", "sys", "fs"}; +void InitLoggingEarly() { + std::lock_guard lock(g_mutex); + if (g_early_initialized || g_initialized) + return; + +#if REX_PLATFORM_WIN32 + auto sink = std::make_shared(); +#else + auto sink = std::make_shared(); +#endif + sink->set_level(spdlog::level::trace); + sink->set_pattern("[%l] [%n] %v"); + g_early_sink = sink; + + g_config.default_level = kDefaultLogLevel; + + // Create loggers for any categories already registered during static init + for (auto& entry : g_registry) { + if (!entry.name.empty() && !entry.logger) + entry.logger = CreateCategoryLogger(entry.name); + } + + // Set default logger to "core" if registered + for (auto& entry : g_registry) { + if (entry.name == "core") { + spdlog::set_default_logger(entry.logger); + break; + } + } + g_early_initialized = true; +} void InitLogging(const LogConfig& config) { std::lock_guard lock(g_mutex); if (g_initialized) { - // Re-initialization: rebuild default sinks so early default init can be - // replaced by config-driven logging once runtime config has been loaded. g_config = config; - g_console_sink.reset(); - g_file_sink.reset(); - - if (config.log_to_console) { - auto sink = std::make_shared(); - sink->set_level(spdlog::level::trace); - sink->set_pattern(config.console_pattern); - g_console_sink = sink; - } - - if (config.log_file) { - auto sink = std::make_shared(config.log_file, true); - sink->set_level(spdlog::level::trace); - sink->set_pattern(config.file_pattern); - g_file_sink = sink; - } - for (auto& entry : g_registry) { - if (entry.logger) { - entry.logger->sinks() = BuildCategorySinks(entry.name); - entry.logger->set_level(ResolveCategoryLevel(entry.name)); - entry.logger->flush_on(config.flush_level); - } + if (!entry.logger) + continue; + entry.logger->set_level(ResolveCategoryLevel(entry.name)); + entry.logger->flush_on(config.flush_level); + if (g_config.category_levels.count(entry.name)) + entry.has_explicit_level = true; } return; } g_config = config; - // Create console sink + // Early sink handling: + // Windows: the early msvc_sink is the persistent debug channel for GUI + // apps and does not conflict with the stdout console sink, so keep it. + // Non-Windows: drop the early stdout sink unconditionally so file-only + // configs don't leak to stdout and console configs don't duplicate. +#if !REX_PLATFORM_WIN32 + if (g_early_sink) { + for (auto& entry : g_registry) { + if (entry.logger) + std::erase(entry.logger->sinks(), g_early_sink); + } + g_early_sink.reset(); + } +#endif + + // Console sink (stdout, colored). Intended for console-subsystem processes. if (config.log_to_console) { auto sink = std::make_shared(); sink->set_level(spdlog::level::trace); @@ -148,39 +215,50 @@ void InitLogging(const LogConfig& config) { g_console_sink = sink; } - // Create file sink + // File sink (rotating) with sequential naming fallback + std::string resolved_path; if (config.log_file) { - auto sink = std::make_shared(config.log_file, true); + resolved_path = config.log_file; + } else if (!config.app_name.empty()) { + auto log_dir = config.log_dir.empty() ? std::filesystem::current_path() / "logs" + : std::filesystem::path(config.log_dir); + resolved_path = NextSequentialLogPath(log_dir, config.app_name).string(); + } + if (!resolved_path.empty()) { + auto sink = std::make_shared( + resolved_path, static_cast(REXCVAR_GET(log_max_file_size_mb)) * 1024 * 1024, + static_cast(REXCVAR_GET(log_max_files)), false); sink->set_level(spdlog::level::trace); sink->set_pattern(config.file_pattern); g_file_sink = sink; } - // Populate extra global sinks from config g_extra_sinks = config.extra_sinks; - // Ensure registry has room for built-in categories (don't truncate - // any consumer categories that were registered during static init). - if (g_registry.size() < log::kBuiltinCount) { - g_registry.resize(log::kBuiltinCount); - } - for (uint16_t i = 0; i < log::kBuiltinCount; ++i) { - g_registry[i].name = kBuiltinNames[i]; - g_registry[i].logger = CreateCategoryLogger(kBuiltinNames[i]); + // Rebuild all loggers with new sinks + for (auto& entry : g_registry) { + if (entry.name.empty()) + continue; + if (entry.logger) + spdlog::drop(entry.name); + entry.logger = CreateCategoryLogger(entry.name); + if (g_config.category_levels.count(entry.name)) + entry.has_explicit_level = true; } - // Set core as default logger for spdlog - spdlog::set_default_logger(g_registry[0].logger); - - g_initialized = true; - - // Create loggers for any categories registered during static init - // (they have a name but no logger since sinks didn't exist yet). - for (size_t i = log::kBuiltinCount; i < g_registry.size(); ++i) { - if (!g_registry[i].name.empty() && !g_registry[i].logger) { - g_registry[i].logger = CreateCategoryLogger(g_registry[i].name); + // Set default logger to "core" + for (auto& entry : g_registry) { + if (entry.name == "core") { + spdlog::set_default_logger(entry.logger); + break; } } + g_initialized = true; + + // Periodic flush + int flush_interval = REXCVAR_GET(log_flush_interval); + if (flush_interval > 0) + spdlog::flush_every(std::chrono::seconds(flush_interval)); } void InitLogging(const char* log_file, spdlog::level::level_enum level) { @@ -192,33 +270,26 @@ void InitLogging(const char* log_file, spdlog::level::level_enum level) { void ShutdownLogging() { std::lock_guard lock(g_mutex); - if (!g_initialized) + if (!g_initialized && !g_early_initialized) return; - for (auto& entry : g_registry) { + for (auto& entry : g_registry) if (entry.logger) entry.logger->flush(); - } spdlog::shutdown(); - g_registry.clear(); g_console_sink.reset(); g_file_sink.reset(); + g_early_sink.reset(); g_extra_sinks.clear(); g_initialized = false; + g_early_initialized = false; } LogCategoryId RegisterLogCategory(const char* name) { std::lock_guard lock(g_mutex); - // Reserve indices 0..kBuiltinCount-1 for SDK built-in categories so - // consumer categories never collide with built-in IDs even when - // registered during static initialization (before InitLogging). - if (g_registry.size() < log::kBuiltinCount) { - g_registry.resize(log::kBuiltinCount); - } - // Check for duplicates for (size_t i = 0; i < g_registry.size(); ++i) { if (g_registry[i].name == name) { @@ -229,13 +300,39 @@ LogCategoryId RegisterLogCategory(const char* name) { uint16_t id = static_cast(g_registry.size()); LogCategoryEntry entry; entry.name = name; - if (g_initialized) { + if (g_initialized || g_early_initialized) { entry.logger = CreateCategoryLogger(name); } g_registry.push_back(std::move(entry)); return LogCategoryId{id}; } +LogCategoryId RegisterLogSubcategory(const char* name, LogCategoryId parent) { + std::lock_guard lock(g_mutex); + + std::string parent_name; + if (parent.id < g_registry.size()) + parent_name = g_registry[parent.id].name; + std::string full_name = parent_name + "." + name; + + for (size_t i = 0; i < g_registry.size(); ++i) { + if (g_registry[i].name == full_name) + return LogCategoryId{static_cast(i)}; + } + + uint16_t id = static_cast(g_registry.size()); + LogCategoryEntry entry; + entry.name = full_name; + entry.parent = parent; + if (g_initialized || g_early_initialized) { + entry.logger = CreateCategoryLogger(full_name); + if (g_config.category_levels.count(full_name)) + entry.has_explicit_level = true; + } + g_registry.push_back(std::move(entry)); + return LogCategoryId{id}; +} + std::optional FindCategory(const std::string& name) { std::lock_guard lock(g_mutex); for (size_t i = 0; i < g_registry.size(); ++i) { @@ -252,8 +349,8 @@ std::span GetAllCategories() { } spdlog::logger* GetLoggerRaw(LogCategoryId category) { - if (!g_initialized) - InitLogging(); + if (!g_initialized && !g_early_initialized) + InitLoggingEarly(); if (category.id < g_registry.size()) { return g_registry[category.id].logger.get(); } @@ -261,8 +358,8 @@ spdlog::logger* GetLoggerRaw(LogCategoryId category) { } std::shared_ptr GetLogger(LogCategoryId category) { - if (!g_initialized) - InitLogging(); + if (!g_initialized && !g_early_initialized) + InitLoggingEarly(); if (category.id < g_registry.size()) { return g_registry[category.id].logger; } @@ -270,7 +367,7 @@ std::shared_ptr GetLogger(LogCategoryId category) { } std::shared_ptr GetLogger() { - return GetLogger(log::Core); + return GetLogger(rex::log::core()); } void SetCategoryLevel(LogCategoryId category, spdlog::level::level_enum level) { @@ -288,11 +385,23 @@ void SetAllLevels(spdlog::level::level_enum level) { } } +void SetRootLevel(LogCategoryId root, spdlog::level::level_enum level) { + SetCategoryLevel(root, level); + for (auto& entry : g_registry) { + if (entry.parent.has_value() && entry.parent->id == root.id && !entry.has_explicit_level && + entry.logger) + entry.logger->set_level(level); + } +} + void RegisterLogLevelCallback() { rex::cvar::RegisterChangeCallback("log_level", [](std::string_view, std::string_view value) { if (auto level = ParseLogLevel(std::string(value))) { - SetAllLevels(*level); - REXLOG_DEBUG("Log level changed to {}", value); + for (size_t i = 0; i < g_registry.size(); ++i) { + auto& entry = g_registry[i]; + if (!entry.parent.has_value() && entry.logger && !entry.has_explicit_level) + SetRootLevel(LogCategoryId{static_cast(i)}, *level); + } } }); } @@ -406,53 +515,28 @@ LogConfig BuildLogConfig(const char* log_file, const std::string& cli_level, return config; } -// ========================================================================== -// Guest Thread ID -// ========================================================================== +std::map ParseCategoryLevelsFromConfig( + const std::filesystem::path& config_path) { + std::map result; + if (!std::filesystem::exists(config_path)) + return result; -uint32_t GetLogGuestThreadId() { - // TODO: Link to actual guest context when available - return 0; -} + try { + auto config = toml::parse_file(config_path.string()); + auto* log_table = config["log"].as_table(); + if (!log_table) + return result; + auto* levels_table = (*log_table)["levels"].as_table(); + if (!levels_table) + return result; -// ========================================================================== -// Deprecated Compatibility Layer -// ========================================================================== + for (const auto& [key, value] : *levels_table) { + if (value.is_string()) + result[std::string(key)] = value.as_string()->get(); + } + } catch (const toml::parse_error&) {} -// Map old LogCategory enum to new LogCategoryId -static LogCategoryId CategoryToId(LogCategory category) { - auto idx = static_cast(std::to_underlying(category)); - return LogCategoryId{idx}; -} - -std::optional CategoryFromName(const std::string& name) { - std::string lower = name; - std::transform(lower.begin(), lower.end(), lower.begin(), - [](unsigned char c) { return std::tolower(c); }); - - static const std::unordered_map name_map = { - {"core", LogCategory::Core}, {"cpu", LogCategory::CPU}, - {"ppc", LogCategory::CPU}, {"apu", LogCategory::APU}, - {"audio", LogCategory::APU}, {"gpu", LogCategory::GPU}, - {"graphics", LogCategory::GPU}, {"kernel", LogCategory::Kernel}, - {"krnl", LogCategory::Kernel}, {"runtime", LogCategory::Kernel}, - {"system", LogCategory::System}, {"sys", LogCategory::System}, - {"fs", LogCategory::FS}, {"filesystem", LogCategory::FS}, - {"vfs", LogCategory::FS}, - }; - - auto it = name_map.find(lower); - if (it != name_map.end()) - return it->second; - return std::nullopt; -} - -std::shared_ptr GetLogger(LogCategory category) { - return GetLogger(CategoryToId(category)); -} - -void SetCategoryLevel(LogCategory category, spdlog::level::level_enum level) { - SetCategoryLevel(CategoryToId(category), level); + return result; } } // namespace rex diff --git a/thirdparty/rexglue-sdk/src/native/core/filesystem_win.cpp b/thirdparty/rexglue-sdk/src/native/core/filesystem_win.cpp index efb9a501..1d9c5070 100644 --- a/thirdparty/rexglue-sdk/src/native/core/filesystem_win.cpp +++ b/thirdparty/rexglue-sdk/src/native/core/filesystem_win.cpp @@ -183,7 +183,7 @@ std::unique_ptr FileHandle::OpenExisting(const std::filesystem::path if (desired_access & FileAccess::kFileAppendData) { open_access |= FILE_APPEND_DATA; } - DWORD share_mode = FILE_SHARE_READ | FILE_SHARE_WRITE; + DWORD share_mode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; // We assume we've already created the file in the caller. DWORD creation_disposition = OPEN_EXISTING; HANDLE handle = CreateFileW(path.c_str(), open_access, share_mode, nullptr, creation_disposition, diff --git a/thirdparty/rexglue-sdk/src/native/filesystem/CMakeLists.txt b/thirdparty/rexglue-sdk/src/native/filesystem/CMakeLists.txt index 3270ccb1..6055dea6 100644 --- a/thirdparty/rexglue-sdk/src/native/filesystem/CMakeLists.txt +++ b/thirdparty/rexglue-sdk/src/native/filesystem/CMakeLists.txt @@ -11,6 +11,9 @@ add_library(rexfilesystem STATIC devices/null_device.cpp devices/null_entry.cpp devices/null_file.cpp + devices/stfs_container_device.cpp + devices/stfs_container_entry.cpp + devices/stfs_container_file.cpp ) add_library(rex::filesystem ALIAS rexfilesystem) diff --git a/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_device.cpp b/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_device.cpp new file mode 100644 index 00000000..16420896 --- /dev/null +++ b/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_device.cpp @@ -0,0 +1,834 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2020 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime + */ + +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace rex::filesystem { + +StfsContainerDevice::StfsContainerDevice(const std::string_view mount_path, + const std::filesystem::path& host_path) + : Device(mount_path), + name_("STFS"), + host_path_(host_path), + files_total_size_(), + svod_base_offset_(), + header_(), + svod_layout_(), + blocks_per_hash_table_(1), + block_step{0, 0} {} + +StfsContainerDevice::~StfsContainerDevice() { + CloseFiles(); +} + +std::unique_ptr StfsContainerDevice::ReadPackageHeader( + const std::filesystem::path& file_path) { + if (!std::filesystem::exists(file_path)) { + return nullptr; + } + + if (std::filesystem::file_size(file_path) < sizeof(StfsHeader)) { + return nullptr; + } + + auto file = rex::filesystem::OpenFile(file_path, "rb"); + if (!file) { + return nullptr; + } + + auto header = std::make_unique(); + if (fread(header.get(), sizeof(StfsHeader), 1, file) != 1) { + fclose(file); + return nullptr; + } + fclose(file); + + if (!header->header.is_magic_valid()) { + return nullptr; + } + + return header; +} + +bool StfsContainerDevice::Initialize() { + // Resolve a valid STFS file if a directory is given. + if (std::filesystem::is_directory(host_path_) && !ResolveFromFolder(host_path_)) { + REXFS_ERROR("Could not resolve an STFS container given path {}", rex::path_to_utf8(host_path_)); + return false; + } + + if (!std::filesystem::exists(host_path_)) { + REXFS_ERROR("Path to STFS container does not exist: {}", rex::path_to_utf8(host_path_)); + return false; + } + + // Open the data file(s) + auto open_result = OpenFiles(); + if (open_result != Error::kSuccess) { + REXFS_ERROR("Failed to open STFS container: {}", static_cast(open_result)); + return false; + } + + switch (header_.metadata.volume_type) { + case XContentVolumeType::kStfs: + return ReadSTFS() == Error::kSuccess; + break; + case XContentVolumeType::kSvod: + return ReadSVOD() == Error::kSuccess; + default: + REXFS_ERROR("Unknown XContent volume type: {}", + rex::byte_swap(uint32_t(header_.metadata.volume_type.value))); + return false; + } +} + +StfsContainerDevice::Error StfsContainerDevice::OpenFiles() { + // Map the file containing the STFS Header and read it. + REXFS_INFO("Loading STFS header file: {}", rex::path_to_utf8(host_path_)); + + auto header_file = rex::filesystem::OpenFile(host_path_, "rb"); + if (!header_file) { + REXFS_ERROR("Error opening STFS header file."); + return Error::kErrorReadError; + } + + auto header_result = ReadHeaderAndVerify(header_file); + if (header_result != Error::kSuccess) { + REXFS_ERROR("Error reading STFS header: {}", static_cast(header_result)); + fclose(header_file); + files_total_size_ = 0; + return header_result; + } + + // If the STFS package is a single file, the header is self contained and + // we don't need to map any extra files. + // NOTE: data_file_count is 0 for STFS and 1 for SVOD + if (header_.metadata.data_file_count <= 1) { + REXFS_INFO("STFS container is a single file."); + files_.emplace(std::make_pair(0, header_file)); + return Error::kSuccess; + } + + // If the STFS package is multi-file, it is an SVOD system. We need to map + // the files in the .data folder and can discard the header. + auto data_fragment_path = host_path_; + data_fragment_path += ".data"; + if (!std::filesystem::exists(data_fragment_path)) { + REXFS_ERROR("STFS container is multi-file, but path {} does not exist.", + rex::path_to_utf8(data_fragment_path)); + return Error::kErrorFileMismatch; + } + + // Ensure data fragment files are sorted + auto fragment_files = filesystem::ListFiles(data_fragment_path); + std::sort(fragment_files.begin(), fragment_files.end(), + [](filesystem::FileInfo& left, filesystem::FileInfo& right) { + return left.name < right.name; + }); + + if (fragment_files.size() != header_.metadata.data_file_count) { + REXFS_ERROR("SVOD expecting {} data fragments, but {} are present.", + header_.metadata.data_file_count.get(), fragment_files.size()); + return Error::kErrorFileMismatch; + } + + for (size_t i = 0; i < fragment_files.size(); i++) { + auto& fragment = fragment_files.at(i); + auto path = fragment.path / fragment.name; + auto file = rex::filesystem::OpenFile(path, "rb"); + if (!file) { + REXFS_INFO("Failed to map SVOD file {}.", rex::path_to_utf8(path)); + CloseFiles(); + return Error::kErrorReadError; + } + + rex::filesystem::Seek(file, 0L, SEEK_END); + files_total_size_ += rex::filesystem::Tell(file); + // no need to seek back, any reads from this file will seek first anyway + files_.emplace(std::make_pair(i, file)); + } + REXFS_INFO("SVOD successfully mapped {} files.", fragment_files.size()); + return Error::kSuccess; +} + +void StfsContainerDevice::CloseFiles() { + for (auto& file : files_) { + fclose(file.second); + } + files_.clear(); + files_total_size_ = 0; +} + +void StfsContainerDevice::Dump(string::StringBuffer* string_buffer) { + auto global_lock = global_critical_region_.Acquire(); + root_entry_->Dump(string_buffer, 0); +} + +Entry* StfsContainerDevice::ResolvePath(const std::string_view path) { + // The filesystem will have stripped our prefix off already, so the path will + // be in the form: + // some\PATH.foo + REXFS_INFO("StfsContainerDevice::ResolvePath({})", path); + return root_entry_->ResolvePath(path); +} + +StfsContainerDevice::Error StfsContainerDevice::ReadHeaderAndVerify(FILE* header_file) { + // Check size of the file is enough to store an STFS header + rex::filesystem::Seek(header_file, 0L, SEEK_END); + files_total_size_ = rex::filesystem::Tell(header_file); + rex::filesystem::Seek(header_file, 0L, SEEK_SET); + + if (sizeof(StfsHeader) > files_total_size_) { + return Error::kErrorTooSmall; + } + + // Read header & check signature + if (fread(&header_, sizeof(StfsHeader), 1, header_file) != 1) { + return Error::kErrorReadError; + } + + if (!header_.header.is_magic_valid()) { + // Unexpected format. + return Error::kErrorFileMismatch; + } + + // Pre-calculate some values used in block number calculations + if (header_.metadata.volume_type == XContentVolumeType::kStfs) { + blocks_per_hash_table_ = + header_.metadata.volume_descriptor.stfs.flags.bits.read_only_format ? 1 : 2; + + block_step[0] = kBlocksPerHashLevel[0] + blocks_per_hash_table_; + block_step[1] = + kBlocksPerHashLevel[1] + ((kBlocksPerHashLevel[0] + 1) * blocks_per_hash_table_); + } + + return Error::kSuccess; +} + +StfsContainerDevice::Error StfsContainerDevice::ReadSVOD() { + // SVOD Systems can have different layouts. The root block is + // denoted by the magic "MICROSOFT*XBOX*MEDIA" and is always in + // the first "actual" data fragment of the system. + auto& svod_header = files_.at(0); + const char* MEDIA_MAGIC = "MICROSOFT*XBOX*MEDIA"; + + uint8_t magic_buf[20]; + size_t magic_offset; + + // Check for EDGF layout + if (header_.metadata.volume_descriptor.svod.features.bits.enhanced_gdf_layout) { + // The STFS header has specified that this SVOD system uses the EGDF layout. + // We can expect the magic block to be located immediately after the hash + // blocks. We also offset block address calculation by 0x1000 by shifting + // block indices by +0x2. + rex::filesystem::Seek(svod_header, 0x2000, SEEK_SET); + if (fread(magic_buf, 1, countof(magic_buf), svod_header) != countof(magic_buf)) { + REXFS_ERROR("ReadSVOD failed to read SVOD magic at 0x2000"); + return Error::kErrorReadError; + } + + if (std::memcmp(magic_buf, MEDIA_MAGIC, 20) == 0) { + svod_base_offset_ = 0x0000; + magic_offset = 0x2000; + svod_layout_ = SvodLayoutType::kEnhancedGDF; + REXFS_INFO("SVOD uses an EGDF layout. Magic block present at 0x2000."); + } else { + REXFS_ERROR("SVOD uses an EGDF layout, but the magic block was not found."); + return Error::kErrorFileMismatch; + } + } else { + rex::filesystem::Seek(svod_header, 0x12000, SEEK_SET); + if (fread(magic_buf, 1, countof(magic_buf), svod_header) != countof(magic_buf)) { + REXFS_ERROR("ReadSVOD failed to read SVOD magic at 0x12000"); + return Error::kErrorReadError; + } + if (std::memcmp(magic_buf, MEDIA_MAGIC, 20) == 0) { + // If the SVOD's magic block is at 0x12000, it is likely using an XSF + // layout. This is usually due to converting the game using a third-party + // tool, as most of them use a nulled XSF as a template. + + svod_base_offset_ = 0x10000; + magic_offset = 0x12000; + + // Check for XSF Header + const char* XSF_MAGIC = "XSF"; + rex::filesystem::Seek(svod_header, 0x2000, SEEK_SET); + if (fread(magic_buf, 1, 3, svod_header) != 3) { + REXFS_ERROR("ReadSVOD failed to read SVOD XSF magic at 0x2000"); + return Error::kErrorReadError; + } + if (std::memcmp(magic_buf, XSF_MAGIC, 3) == 0) { + svod_layout_ = SvodLayoutType::kXSF; + REXFS_INFO("SVOD uses an XSF layout. Magic block present at 0x12000."); + REXFS_INFO("Game was likely converted using a third-party tool."); + } else { + svod_layout_ = SvodLayoutType::kUnknown; + REXFS_INFO("SVOD appears to use an XSF layout, but no header is present."); + REXFS_INFO("SVOD magic block found at 0x12000"); + } + } else { + rex::filesystem::Seek(svod_header, 0xD000, SEEK_SET); + if (fread(magic_buf, 1, countof(magic_buf), svod_header) != countof(magic_buf)) { + REXFS_ERROR("ReadSVOD failed to read SVOD magic at 0xD000"); + return Error::kErrorReadError; + } + if (std::memcmp(magic_buf, MEDIA_MAGIC, 20) == 0) { + // If the SVOD's magic block is at 0xD000, it most likely means that it + // is a single-file system. The STFS Header is 0xB000 bytes , and the + // remaining 0x2000 is from hash tables. In most cases, these will be + // STFS, not SVOD. + + svod_base_offset_ = 0xB000; + magic_offset = 0xD000; + + // Check for single file system + if (header_.metadata.data_file_count == 1) { + svod_layout_ = SvodLayoutType::kSingleFile; + REXFS_INFO("SVOD is a single file. Magic block present at 0xD000."); + } else { + svod_layout_ = SvodLayoutType::kUnknown; + REXFS_ERROR( + "SVOD is not a single file, but the magic block was found at " + "0xD000."); + } + } else { + REXFS_ERROR("Could not locate SVOD magic block."); + return Error::kErrorReadError; + } + } + } + + // Parse the root directory + rex::filesystem::Seek(svod_header, magic_offset + 0x14, SEEK_SET); + + struct { + uint32_t block; + uint32_t size; + uint32_t creation_date; + uint32_t creation_time; + } root_data; + static_assert_size(root_data, 0x10); + + if (fread(&root_data, sizeof(root_data), 1, svod_header) != 1) { + REXFS_ERROR("ReadSVOD failed to read root block data at 0x{:X}", magic_offset + 0x14); + return Error::kErrorReadError; + } + + uint64_t root_creation_timestamp = + decode_fat_timestamp(root_data.creation_date, root_data.creation_time); + + auto root_entry = new StfsContainerEntry(this, nullptr, "", &files_); + root_entry->attributes_ = kFileAttributeDirectory; + root_entry->access_timestamp_ = root_creation_timestamp; + root_entry->create_timestamp_ = root_creation_timestamp; + root_entry->write_timestamp_ = root_creation_timestamp; + root_entry_ = std::unique_ptr(root_entry); + + // Traverse all child entries + return ReadEntrySVOD(root_data.block, 0, root_entry); +} + +StfsContainerDevice::Error StfsContainerDevice::ReadEntrySVOD(uint32_t block, uint32_t ordinal, + StfsContainerEntry* parent) { + // For games with a large amount of files, the ordinal offset can overrun + // the current block and potentially hit a hash block. + size_t ordinal_offset = ordinal * 0x4; + size_t block_offset = ordinal_offset / 0x800; + size_t true_ordinal_offset = ordinal_offset % 0x800; + + // Calculate the file & address of the block + size_t entry_address, entry_file; + BlockToOffsetSVOD(block + block_offset, &entry_address, &entry_file); + entry_address += true_ordinal_offset; + + // Read directory entry + auto& file = files_.at(entry_file); + rex::filesystem::Seek(file, entry_address, SEEK_SET); + +#pragma pack(push, 1) + struct { + uint16_t node_l; + uint16_t node_r; + uint32_t data_block; + uint32_t length; + uint8_t attributes; + uint8_t name_length; + } dir_entry; + static_assert_size(dir_entry, 0xE); +#pragma pack(pop) + + if (fread(&dir_entry, sizeof(dir_entry), 1, file) != 1) { + REXFS_ERROR("ReadEntrySVOD failed to read directory entry at 0x{:X}", entry_address); + return Error::kErrorReadError; + } + + auto name_buffer = std::make_unique(dir_entry.name_length); + if (fread(name_buffer.get(), 1, dir_entry.name_length, file) != dir_entry.name_length) { + REXFS_ERROR("ReadEntrySVOD failed to read directory entry name at 0x{:X}", entry_address); + return Error::kErrorReadError; + } + + auto name = std::string(name_buffer.get(), dir_entry.name_length); + + // Read the left node + if (dir_entry.node_l) { + auto node_result = ReadEntrySVOD(block, dir_entry.node_l, parent); + if (node_result != Error::kSuccess) { + return node_result; + } + } + + // Read file & address of block's data + size_t data_address, data_file; + BlockToOffsetSVOD(dir_entry.data_block, &data_address, &data_file); + + // Create the entry + // NOTE: SVOD entries don't have timestamps for individual files, which can + // cause issues when decrypting games. Using the root entry's timestamp + // solves this issues. + auto entry = StfsContainerEntry::Create(this, parent, name, &files_); + if (dir_entry.attributes & kFileAttributeDirectory) { + // Entry is a directory + entry->attributes_ = kFileAttributeDirectory | kFileAttributeReadOnly; + entry->data_offset_ = 0; + entry->data_size_ = 0; + entry->block_ = block; + entry->access_timestamp_ = root_entry_->create_timestamp(); + entry->create_timestamp_ = root_entry_->create_timestamp(); + entry->write_timestamp_ = root_entry_->create_timestamp(); + + if (dir_entry.length) { + // If length is greater than 0, traverse the directory's children + auto directory_result = ReadEntrySVOD(dir_entry.data_block, 0, entry.get()); + if (directory_result != Error::kSuccess) { + return directory_result; + } + } + } else { + // Entry is a file + entry->attributes_ = kFileAttributeNormal | kFileAttributeReadOnly; + entry->size_ = dir_entry.length; + entry->allocation_size_ = rex::round_up(dir_entry.length, kBlockSize); + entry->data_offset_ = data_address; + entry->data_size_ = dir_entry.length; + entry->block_ = dir_entry.data_block; + entry->access_timestamp_ = root_entry_->create_timestamp(); + entry->create_timestamp_ = root_entry_->create_timestamp(); + entry->write_timestamp_ = root_entry_->create_timestamp(); + + // Fill in all block records, sector by sector. + if (entry->attributes() & system::X_FILE_ATTRIBUTE_NORMAL) { + uint32_t block_index = dir_entry.data_block; + size_t remaining_size = rex::round_up(dir_entry.length, 0x800); + + size_t last_record = -1; + size_t last_offset = -1; + while (remaining_size) { + const size_t BLOCK_SIZE = 0x800; + + size_t offset, file_index; + BlockToOffsetSVOD(block_index, &offset, &file_index); + + block_index++; + remaining_size -= BLOCK_SIZE; + + if (offset - last_offset == 0x800) { + // Consecutive, so append to last entry. + entry->block_list_[last_record].length += BLOCK_SIZE; + last_offset = offset; + continue; + } + + entry->block_list_.push_back({file_index, offset, BLOCK_SIZE}); + last_record = entry->block_list_.size() - 1; + last_offset = offset; + } + } + } + + parent->children_.emplace_back(std::move(entry)); + + // Read the right node. + if (dir_entry.node_r) { + auto node_result = ReadEntrySVOD(block, dir_entry.node_r, parent); + if (node_result != Error::kSuccess) { + return node_result; + } + } + + return Error::kSuccess; +} + +void StfsContainerDevice::BlockToOffsetSVOD(size_t block, size_t* out_address, + size_t* out_file_index) { + // SVOD Systems use hash blocks for integrity checks. These hash blocks + // cause blocks to be discontinuous in memory, and must be accounted for. + // - Each data block is 0x800 bytes in length + // - Every group of 0x198 data blocks is preceded a Level0 hash table. + // Level0 tables contain 0xCC hashes, each representing two data blocks. + // The total size of each Level0 hash table is 0x1000 bytes in length. + // - Every 0xA1C4 Level0 hash tables is preceded by a Level1 hash table. + // Level1 tables contain 0xCB hashes, each representing two Level0 hashes. + // The total size of each Level1 hash table is 0x1000 bytes in length. + // - Files are split into fragments of 0xA290000 bytes in length, + // consisting of 0x14388 data blocks, 0xCB Level0 hash tables, and 0x1 + // Level1 hash table. + + const size_t BLOCK_SIZE = 0x800; + const size_t HASH_BLOCK_SIZE = 0x1000; + const size_t BLOCKS_PER_L0_HASH = 0x198; + const size_t HASHES_PER_L1_HASH = 0xA1C4; + const size_t BLOCKS_PER_FILE = 0x14388; + const size_t MAX_FILE_SIZE = 0xA290000; + const size_t BLOCK_OFFSET = header_.metadata.volume_descriptor.svod.start_data_block(); + + // Resolve the true block address and file index + size_t true_block = block - (BLOCK_OFFSET * 2); + if (svod_layout_ == SvodLayoutType::kEnhancedGDF) { + // EGDF has an 0x1000 byte offset, which is two blocks + true_block += 0x2; + } + + size_t file_block = true_block % BLOCKS_PER_FILE; + size_t file_index = true_block / BLOCKS_PER_FILE; + size_t offset = 0; + + // Calculate offset caused by Level0 Hash Tables + size_t level0_table_count = (file_block / BLOCKS_PER_L0_HASH) + 1; + offset += level0_table_count * HASH_BLOCK_SIZE; + + // Calculate offset caused by Level1 Hash Tables + size_t level1_table_count = (level0_table_count / HASHES_PER_L1_HASH) + 1; + offset += level1_table_count * HASH_BLOCK_SIZE; + + // For single-file SVOD layouts, include the size of the header in the offset. + if (svod_layout_ == SvodLayoutType::kSingleFile) { + offset += svod_base_offset_; + } + + size_t block_address = (file_block * BLOCK_SIZE) + offset; + + // If the offset causes the block address to overrun the file, round it. + if (block_address >= MAX_FILE_SIZE) { + file_index += 1; + block_address %= MAX_FILE_SIZE; + block_address += 0x2000; + } + + *out_address = block_address; + *out_file_index = file_index; +} + +StfsContainerDevice::Error StfsContainerDevice::ReadSTFS() { + auto& file = files_.at(0); + + auto root_entry = new StfsContainerEntry(this, nullptr, "", &files_); + root_entry->attributes_ = kFileAttributeDirectory; + root_entry_ = std::unique_ptr(root_entry); + + std::vector all_entries; + + // Load all listings. + StfsDirectoryBlock directory; + + auto& descriptor = header_.metadata.volume_descriptor.stfs; + uint32_t table_block_index = descriptor.file_table_block_number(); + size_t n = 0; + for (n = 0; n < descriptor.file_table_block_count; n++) { + auto offset = BlockToOffsetSTFS(table_block_index); + rex::filesystem::Seek(file, offset, SEEK_SET); + + if (fread(&directory, sizeof(StfsDirectoryBlock), 1, file) != 1) { + REXFS_ERROR("ReadSTFS failed to read directory block at 0x{:X}", offset); + return Error::kErrorReadError; + } + + for (size_t m = 0; m < kEntriesPerDirectoryBlock; m++) { + auto& dir_entry = directory.entries[m]; + + if (dir_entry.name[0] == 0) { + // Done. + break; + } + + StfsContainerEntry* parent_entry = nullptr; + if (dir_entry.directory_index == 0xFFFF) { + parent_entry = root_entry; + } else { + parent_entry = all_entries[dir_entry.directory_index]; + } + + std::string name(reinterpret_cast(dir_entry.name), + dir_entry.flags.name_length & 0x3F); + auto entry = StfsContainerEntry::Create(this, parent_entry, name, &files_); + + if (dir_entry.flags.directory) { + entry->attributes_ = kFileAttributeDirectory; + } else { + entry->attributes_ = kFileAttributeNormal | kFileAttributeReadOnly; + entry->data_offset_ = BlockToOffsetSTFS(dir_entry.start_block_number()); + entry->data_size_ = dir_entry.length; + } + entry->size_ = dir_entry.length; + entry->allocation_size_ = rex::round_up(dir_entry.length, kBlockSize); + + entry->create_timestamp_ = decode_fat_timestamp(dir_entry.create_date, dir_entry.create_time); + entry->write_timestamp_ = + decode_fat_timestamp(dir_entry.modified_date, dir_entry.modified_time); + entry->access_timestamp_ = entry->write_timestamp_; + + all_entries.push_back(entry.get()); + + // Fill in all block records. + // It's easier to do this now and just look them up later, at the cost + // of some memory. Nasty chain walk. + // TODO(benvanik): optimize if flags.contiguous is set. + if (entry->attributes() & system::X_FILE_ATTRIBUTE_NORMAL) { + uint32_t block_index = dir_entry.start_block_number(); + size_t remaining_size = dir_entry.length; + while (remaining_size && block_index != kEndOfChain) { + size_t block_size = std::min(static_cast(kBlockSize), remaining_size); + size_t offset = BlockToOffsetSTFS(block_index); + entry->block_list_.push_back({0, offset, block_size}); + remaining_size -= block_size; + auto block_hash = GetBlockHash(block_index); + block_index = block_hash->level0_next_block(); + } + + if (remaining_size) { + // Loop above must have exited prematurely, bad hash tables? + REXFS_WARN( + "STFS file {} only found {} bytes for file, expected {} ({} " + "bytes missing)", + name, dir_entry.length.get() - remaining_size, dir_entry.length.get(), + remaining_size); + assert_always(); + } + + // Check that the number of blocks retrieved from hash entries matches + // the block count read from the file entry + if (entry->block_list_.size() != dir_entry.allocated_data_blocks()) { + REXFS_WARN( + "STFS failed to read correct block-chain for entry {}, read {} " + "blocks, expected {}", + entry->name_, entry->block_list_.size(), dir_entry.allocated_data_blocks()); + assert_always(); + } + } + + parent_entry->children_.emplace_back(std::move(entry)); + } + + auto block_hash = GetBlockHash(table_block_index); + table_block_index = block_hash->level0_next_block(); + if (table_block_index == kEndOfChain) { + break; + } + } + + if (n + 1 != descriptor.file_table_block_count) { + REXFS_WARN("STFS read {} file table blocks, but STFS headers expected {}!", n + 1, + descriptor.file_table_block_count); + assert_always(); + } + + return Error::kSuccess; +} + +size_t StfsContainerDevice::BlockToOffsetSTFS(uint64_t block_index) const { + // For every level there is a hash table + // Level 0: hash table of next 170 blocks + // Level 1: hash table of next 170 hash tables + // Level 2: hash table of next 170 level 1 hash tables + // And so on... + uint64_t base = kBlocksPerHashLevel[0]; + uint64_t block = block_index; + for (uint32_t i = 0; i < 3; i++) { + block += ((block_index + base) / base) * blocks_per_hash_table_; + if (block_index < base) { + break; + } + + base *= kBlocksPerHashLevel[0]; + } + + return rex::round_up(header_.header.header_size, kBlockSize) + (block << 12); +} + +uint32_t StfsContainerDevice::BlockToHashBlockNumberSTFS(uint32_t block_index, + uint32_t hash_level) const { + uint32_t block = 0; + if (hash_level == 0) { + if (block_index < kBlocksPerHashLevel[0]) { + return 0; + } + + block = (block_index / kBlocksPerHashLevel[0]) * block_step[0]; + block += ((block_index / kBlocksPerHashLevel[1]) + 1) * blocks_per_hash_table_; + + if (block_index < kBlocksPerHashLevel[1]) { + return block; + } + + return block + blocks_per_hash_table_; + } + + if (hash_level == 1) { + if (block_index < kBlocksPerHashLevel[1]) { + return block_step[0]; + } + + block = (block_index / kBlocksPerHashLevel[1]) * block_step[1]; + return block + blocks_per_hash_table_; + } + + // Level 2 is always at blockStep1 + return block_step[1]; +} + +size_t StfsContainerDevice::BlockToHashBlockOffsetSTFS(uint32_t block_index, + uint32_t hash_level) const { + uint64_t block = BlockToHashBlockNumberSTFS(block_index, hash_level); + return rex::round_up(header_.header.header_size, kBlockSize) + (block << 12); +} + +const StfsHashEntry* StfsContainerDevice::GetBlockHash(uint32_t block_index) { + auto& file = files_.at(0); + + auto& descriptor = header_.metadata.volume_descriptor.stfs; + + // Offset for selecting the secondary hash block, in packages that have them + uint32_t secondary_table_offset = descriptor.flags.bits.root_active_index ? kBlockSize : 0; + + auto hash_offset_lv0 = BlockToHashBlockOffsetSTFS(block_index, 0); + if (!cached_hash_tables_.count(hash_offset_lv0)) { + // If this is read_only_format then it doesn't contain secondary blocks, no + // need to check upper hash levels + if (descriptor.flags.bits.read_only_format) { + secondary_table_offset = 0; + } else { + // Not a read-only package, need to check each levels active index flag to + // see if we need to use secondary block or not + + // Check level1 table if package has it + if (descriptor.total_block_count > kBlocksPerHashLevel[0]) { + auto hash_offset_lv1 = BlockToHashBlockOffsetSTFS(block_index, 1); + + if (!cached_hash_tables_.count(hash_offset_lv1)) { + // Check level2 table if package has it + if (descriptor.total_block_count > kBlocksPerHashLevel[1]) { + auto hash_offset_lv2 = BlockToHashBlockOffsetSTFS(block_index, 2); + + if (!cached_hash_tables_.count(hash_offset_lv2)) { + rex::filesystem::Seek(file, hash_offset_lv2 + secondary_table_offset, SEEK_SET); + + StfsHashTable table_lv2; + if (fread(&table_lv2, sizeof(StfsHashTable), 1, file) != 1) { + REXFS_ERROR("GetBlockHash failed to read level2 hash table at 0x{:X}", + hash_offset_lv2 + secondary_table_offset); + return nullptr; + } + cached_hash_tables_[hash_offset_lv2] = table_lv2; + } + + auto record = (block_index / kBlocksPerHashLevel[1]) % kBlocksPerHashLevel[0]; + auto record_data = &cached_hash_tables_[hash_offset_lv2].entries[record]; + secondary_table_offset = record_data->levelN_active_index() ? kBlockSize : 0; + } + + rex::filesystem::Seek(file, hash_offset_lv1 + secondary_table_offset, SEEK_SET); + + StfsHashTable table_lv1; + if (fread(&table_lv1, sizeof(StfsHashTable), 1, file) != 1) { + REXFS_ERROR("GetBlockHash failed to read level1 hash table at 0x{:X}", + hash_offset_lv1 + secondary_table_offset); + return nullptr; + } + cached_hash_tables_[hash_offset_lv1] = table_lv1; + } + + auto record = (block_index / kBlocksPerHashLevel[0]) % kBlocksPerHashLevel[0]; + auto record_data = &cached_hash_tables_[hash_offset_lv1].entries[record]; + secondary_table_offset = record_data->levelN_active_index() ? kBlockSize : 0; + } + } + + rex::filesystem::Seek(file, hash_offset_lv0 + secondary_table_offset, SEEK_SET); + + StfsHashTable table_lv0; + if (fread(&table_lv0, sizeof(StfsHashTable), 1, file) != 1) { + REXFS_ERROR("GetBlockHash failed to read level0 hash table at 0x{:X}", + (hash_offset_lv0 + secondary_table_offset)); + return nullptr; + } + cached_hash_tables_[hash_offset_lv0] = table_lv0; + } + + auto record = block_index % kBlocksPerHashLevel[0]; + auto record_data = &cached_hash_tables_[hash_offset_lv0].entries[record]; + + return record_data; +} + +XContentPackageType StfsContainerDevice::ReadMagic(const std::filesystem::path& path) { + auto map = memory::MappedMemory::Open(path, memory::MappedMemory::Mode::kRead, 0, 4); + return XContentPackageType(memory::load_and_swap(map->data())); +} + +bool StfsContainerDevice::ResolveFromFolder(const std::filesystem::path& path) { + // Scan through folders until a file with magic is found + std::queue queue; + + filesystem::FileInfo folder; + filesystem::GetInfo(host_path_, &folder); + queue.push(folder); + + while (!queue.empty()) { + auto current_file = queue.front(); + queue.pop(); + + if (current_file.type == filesystem::FileInfo::Type::kDirectory) { + auto path = current_file.path / current_file.name; + auto child_files = filesystem::ListFiles(path); + for (auto file : child_files) { + queue.push(file); + } + } else { + // Try to read the file's magic + auto path = current_file.path / current_file.name; + auto magic = ReadMagic(path); + + if (magic == XContentPackageType::kCon || magic == XContentPackageType::kLive || + magic == XContentPackageType::kPirs) { + host_path_ = current_file.path / current_file.name; + REXFS_INFO("STFS Package found: {}", rex::path_to_utf8(host_path_)); + return true; + } + } + } + + if (host_path_ == path) { + // Could not find a suitable container file + return false; + } + return true; +} + +} // namespace rex::filesystem diff --git a/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_entry.cpp b/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_entry.cpp new file mode 100644 index 00000000..85359446 --- /dev/null +++ b/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_entry.cpp @@ -0,0 +1,41 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2020 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime + */ + +#include +#include + +#include + +#include + +namespace rex::filesystem { + +StfsContainerEntry::StfsContainerEntry(Device* device, Entry* parent, const std::string_view path, + MultiFileHandles* files) + : Entry(device, parent, path), files_(files), data_offset_(0), data_size_(0), block_(0) {} + +StfsContainerEntry::~StfsContainerEntry() = default; + +std::unique_ptr StfsContainerEntry::Create(Device* device, Entry* parent, + const std::string_view name, + MultiFileHandles* files) { + auto path = rex::string::utf8_join_guest_paths(parent->path(), name); + auto entry = std::make_unique(device, parent, path, files); + + return std::move(entry); +} + +X_STATUS StfsContainerEntry::Open(uint32_t desired_access, File** out_file) { + *out_file = new StfsContainerFile(desired_access, this); + return X_STATUS_SUCCESS; +} + +} // namespace rex::filesystem diff --git a/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_file.cpp b/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_file.cpp new file mode 100644 index 00000000..0dd13f30 --- /dev/null +++ b/thirdparty/rexglue-sdk/src/native/filesystem/devices/stfs_container_file.cpp @@ -0,0 +1,69 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2014 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime + */ + +#include +#include + +#include +#include + +#include + +namespace rex::filesystem { + +StfsContainerFile::StfsContainerFile(uint32_t file_access, StfsContainerEntry* entry) + : File(file_access, entry), entry_(entry) {} + +StfsContainerFile::~StfsContainerFile() = default; + +void StfsContainerFile::Destroy() { + delete this; +} + +X_STATUS StfsContainerFile::ReadSync(std::span buffer, size_t byte_offset, + size_t* out_bytes_read) { + if (byte_offset >= entry_->size()) { + return X_STATUS_END_OF_FILE; + } + + size_t src_offset = 0; + uint8_t* p = buffer.data(); + size_t remaining_length = std::min(buffer.size(), entry_->size() - byte_offset); + + *out_bytes_read = 0; + for (size_t i = 0; i < entry_->block_list().size(); i++) { + auto& record = entry_->block_list()[i]; + if (src_offset + record.length <= byte_offset) { + // Doesn't begin in this region. Skip it. + src_offset += record.length; + continue; + } + + size_t read_offset = (byte_offset > src_offset) ? byte_offset - src_offset : 0; + size_t read_length = std::min(record.length - read_offset, remaining_length); + + auto& file = entry_->files()->at(record.file); + rex::filesystem::Seek(file, record.offset + read_offset, SEEK_SET); + auto num_read = fread(p, 1, read_length, file); + + *out_bytes_read += num_read; + p += num_read; + src_offset += record.length; + remaining_length -= read_length; + if (remaining_length == 0) { + break; + } + } + + return X_STATUS_SUCCESS; +} + +} // namespace rex::filesystem diff --git a/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp b/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp index 85c83213..cab8a82c 100644 --- a/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp +++ b/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp @@ -89,25 +89,33 @@ bool ReXApp::OnInitialize() { user_data_root_ = std::move(path_config.user_data_root); update_data_root_ = std::move(path_config.update_data_root); + auto config_path = exe_dir / (std::string(GetName()) + ".toml"); + + // Load saved config (CVARs) before anything reads them + if (std::filesystem::exists(config_path)) { + rex::cvar::LoadConfig(config_path); + } + // Logging setup from CVARs std::string log_file_cvar = REXCVAR_GET(log_file); std::string log_level_str = REXCVAR_GET(log_level); if (REXCVAR_GET(log_verbose) && log_level_str == "info") { log_level_str = "trace"; } + auto category_levels = rex::ParseCategoryLevelsFromConfig(config_path); auto log_config = rex::BuildLogConfig(log_file_cvar.empty() ? nullptr : log_file_cvar.c_str(), - log_level_str, {}); + log_level_str, category_levels); + if (log_file_cvar.empty()) { + log_config.app_name = std::string(GetName()); + log_config.log_dir = (exe_dir / "logs").string(); + } rex::InitLogging(log_config); rex::RegisterLogLevelCallback(); // Attach log capture sink to all loggers for the console overlay log_sink_ = std::make_shared(); rex::AddSink(log_sink_); - - // Load saved config (CVARs) before anything reads them - auto config_path = exe_dir / (std::string(GetName()) + ".toml"); if (std::filesystem::exists(config_path)) { - rex::cvar::LoadConfig(config_path); REXLOG_INFO("Loaded config: {}", config_path.filename().string()); } @@ -210,7 +218,7 @@ bool ReXApp::OnInitialize() { debug_overlay_ = std::make_unique(imgui_drawer_.get()); console_overlay_ = std::make_unique(imgui_drawer_.get(), log_sink_); settings_overlay_ = std::make_unique( - imgui_drawer_.get(), exe_dir / (std::string(GetName()) + ".toml")); + imgui_drawer_.get(), config_path); // Allow subclass to add custom dialogs OnCreateDialogs(imgui_drawer_.get()); diff --git a/thirdparty/rexglue-sdk/src/native/ui/windowed_app_main_posix.cpp b/thirdparty/rexglue-sdk/src/native/ui/windowed_app_main_posix.cpp index 1df6d246..14490b63 100644 --- a/thirdparty/rexglue-sdk/src/native/ui/windowed_app_main_posix.cpp +++ b/thirdparty/rexglue-sdk/src/native/ui/windowed_app_main_posix.cpp @@ -3,16 +3,12 @@ #include #include -#include #include #include #include #include -#include - #include -#include #include #include #include @@ -40,6 +36,7 @@ extern "C" int main(int argc_pre_gtk, char** argv_pre_gtk) { auto remaining = rex::cvar::Init(argc_post_gtk, argv_post_gtk); rex::cvar::ApplyEnvironment(); + rex::InitLoggingEarly(); int result; @@ -57,20 +54,6 @@ extern "C" int main(int argc_pre_gtk, char** argv_pre_gtk) { } app->SetParsedArguments(std::move(parsed)); - // Initialize logging. - // Never use the bare app name as a file path (can collide with the executable). - std::filesystem::path exe_dir = rex::filesystem::GetExecutableFolder(); - std::filesystem::path log_path = exe_dir / (app->GetName() + ".log"); - - try { - rex::InitLogging(log_path.string().c_str()); - } catch (const spdlog::spdlog_ex& e) { - // If file logging fails (permissions, ETXTBSY, etc), fall back to console-only. - std::fprintf(stderr, "Logging init failed for '%s': %s\n", log_path.string().c_str(), - e.what()); - rex::InitLogging(nullptr); - } - if (app->OnInitialize()) { app_context.RunMainGTKLoop(); result = EXIT_SUCCESS; diff --git a/thirdparty/rexglue-sdk/src/native/ui/windowed_app_main_win.cpp b/thirdparty/rexglue-sdk/src/native/ui/windowed_app_main_win.cpp index 5786df44..8af02841 100644 --- a/thirdparty/rexglue-sdk/src/native/ui/windowed_app_main_win.cpp +++ b/thirdparty/rexglue-sdk/src/native/ui/windowed_app_main_win.cpp @@ -1,7 +1,6 @@ // Native UI runtime - Win32 windowed app entry point // Part of the AC6 Recompilation native presenter/window layer -#include #include #include #include @@ -14,8 +13,6 @@ #include #include -REXCVAR_DEFINE_BOOL(enable_console, true, "UI/Window", "Enable console window on Windows"); - namespace { // Convert wide argv from CommandLineToArgvW to UTF-8 argc/argv for cvar::Init @@ -59,38 +56,19 @@ int WINAPI wWinMain(HINSTANCE hinstance, HINSTANCE hinstance_prev, LPWSTR comman } auto remaining = rex::cvar::Init(static_cast(argv_ptrs.size()), argv_ptrs.data()); rex::cvar::ApplyEnvironment(); - - // Force logging to a file immediately - auto log_config = rex::BuildLogConfig("ac6_boot.log", "info", {}); - rex::InitLogging(log_config); - REXLOG_INFO("wWinMain started"); - - // Allocate a console for debugging if enabled - if (REXCVAR_GET(enable_console)) { - AllocConsole(); - FILE* fp; - freopen_s(&fp, "CONOUT$", "w", stdout); - freopen_s(&fp, "CONOUT$", "w", stderr); - freopen_s(&fp, "CONIN$", "r", stdin); - printf("Console attached for debugging\n"); - } + rex::InitLoggingEarly(); int result; { - REXLOG_INFO("wWinMain: Creating Win32WindowedAppContext..."); rex::ui::Win32WindowedAppContext app_context(hinstance, show_cmd); // TODO(Triang3l): Initialize creates a window. Set DPI awareness via the // manifest. - REXLOG_INFO("wWinMain: Initializing app context..."); if (!app_context.Initialize()) { - REXLOG_ERROR("wWinMain: app_context.Initialize failed"); return EXIT_FAILURE; } - REXLOG_INFO("wWinMain: Getting app creator..."); std::unique_ptr app = rex::ui::GetWindowedAppCreator()(app_context); - REXLOG_INFO("wWinMain: App instance created"); // Match remaining positional args to app's expected options const auto& option_names = app->GetPositionalOptions(); @@ -103,24 +81,14 @@ int WINAPI wWinMain(HINSTANCE hinstance, HINSTANCE hinstance_prev, LPWSTR comman // Initialize COM on the UI thread with the apartment-threaded concurrency // model, so dialogs can be used. - REXLOG_INFO("wWinMain: Initializing COM..."); if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) { - REXLOG_ERROR("wWinMain: CoInitializeEx failed"); return EXIT_FAILURE; } // TODO: Port InitializeWin32App from Xenia // rex::InitializeWin32App(app->GetName()); - REXLOG_INFO("wWinMain: Calling app->OnInitialize()..."); - if (!app->OnInitialize()) { - REXLOG_ERROR("wWinMain: app->OnInitialize failed"); - return EXIT_FAILURE; - } - - REXLOG_INFO("wWinMain: Entering main message loop..."); - result = app_context.RunMainMessageLoop(); - REXLOG_INFO("wWinMain: Main message loop exited with result {}", result); + result = app->OnInitialize() ? app_context.RunMainMessageLoop() : EXIT_FAILURE; app->InvokeOnDestroy(); } diff --git a/thirdparty/rexglue-sdk/src/rexglue/main.cpp b/thirdparty/rexglue-sdk/src/rexglue/main.cpp index 23720e6a..f63205d8 100644 --- a/thirdparty/rexglue-sdk/src/rexglue/main.cpp +++ b/thirdparty/rexglue-sdk/src/rexglue/main.cpp @@ -61,6 +61,7 @@ void PrintUsage() { int main(int argc, char** argv) { auto remaining = rex::cvar::Init(argc, argv); rex::cvar::ApplyEnvironment(); + rex::InitLoggingEarly(); std::string command; @@ -87,6 +88,7 @@ int main(int argc, char** argv) { std::map category_levels; auto log_config = rex::BuildLogConfig(log_file_path.empty() ? nullptr : log_file_path.c_str(), level_str, category_levels); + log_config.log_to_console = true; // CLI always logs to console rex::InitLogging(log_config); // Register callback for runtime level changes diff --git a/thirdparty/rexglue-sdk/src/system/xam/content_manager.cpp b/thirdparty/rexglue-sdk/src/system/xam/content_manager.cpp index fa0e5e10..1ca9457d 100644 --- a/thirdparty/rexglue-sdk/src/system/xam/content_manager.cpp +++ b/thirdparty/rexglue-sdk/src/system/xam/content_manager.cpp @@ -1,18 +1,27 @@ /** - * ReXGlue runtime - AC6 Recompilation project - * Copyright (c) 2026 Tom Clay. All rights reserved. + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2020 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + * + * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ #include #include +#include #include #include #include -#include +#include +#include #include #include +#include #include #include #include @@ -179,7 +188,8 @@ bool ContentManager::ContentExists(uint64_t xuid, const XCONTENT_AGGREGATE_DATA& return std::filesystem::exists(path); } -X_RESULT ContentManager::WriteContentHeaderFile(uint64_t xuid, XCONTENT_AGGREGATE_DATA data) { +X_RESULT ContentManager::WriteContentHeaderFile(uint64_t xuid, XCONTENT_AGGREGATE_DATA data, + uint32_t license_mask) { if (data.title_id == uint32_t(-1)) { data.title_id = kernel_state_->title_id(); } @@ -205,6 +215,9 @@ X_RESULT ContentManager::WriteContentHeaderFile(uint64_t xuid, XCONTENT_AGGREGAT return X_ERROR_FILE_NOT_FOUND; } fwrite(&data, 1, sizeof(XCONTENT_AGGREGATE_DATA), file); + if (license_mask != 0) { + fwrite(&license_mask, 1, sizeof(license_mask), file); + } fclose(file); return X_ERROR_SUCCESS; } @@ -310,9 +323,7 @@ X_RESULT ContentManager::CloseContent(const std::string_view root_name) { if (it == open_packages_.end()) { return X_ERROR_FILE_NOT_FOUND; } - CloseOpenedFilesFromContent(root_name); - package = it->second; - open_packages_.erase(it); + package = DetachPackage(it); } delete package; return X_ERROR_SUCCESS; @@ -363,15 +374,70 @@ X_RESULT ContentManager::DeleteContent(uint64_t xuid, const XCONTENT_AGGREGATE_D auto package_path = ResolvePackagePath(xuid, data); std::error_code ec; - auto removed = std::filesystem::remove_all(package_path, ec); + auto dir_removed = std::filesystem::remove_all(package_path, ec); if (ec) { return X_ERROR_ACCESS_DENIED; } - if (removed > 0) { + + uint64_t used_xuid = (data.xuid != uint64_t(-1) && data.xuid != 0) ? uint64_t(data.xuid) : xuid; + auto header_path = + ResolvePackageHeaderPath(data.file_name(), used_xuid, data.title_id, data.content_type); + std::error_code ec2; + bool header_removed = std::filesystem::remove(header_path, ec2); + + if (dir_removed > 0 || header_removed) { return X_ERROR_SUCCESS; - } else { - return X_ERROR_FILE_NOT_FOUND; } + return X_ERROR_FILE_NOT_FOUND; +} + +X_RESULT ContentManager::UnmountContent(uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data) { + ContentPackage* package = nullptr; + { + auto global_lock = global_critical_region_.Acquire(); + auto it = FindOpenPackageByData(data); + if (it == open_packages_.end()) { + return X_ERROR_FILE_NOT_FOUND; + } + package = DetachPackage(it); + } + delete package; + return X_ERROR_SUCCESS; +} + +X_RESULT ContentManager::UnmountAndDeleteContent(uint64_t xuid, + const XCONTENT_AGGREGATE_DATA& data) { + // Unmount phase: tolerant of not-mounted state + ContentPackage* package = nullptr; + { + auto global_lock = global_critical_region_.Acquire(); + auto it = FindOpenPackageByData(data); + if (it != open_packages_.end()) { + package = DetachPackage(it); + } + } + delete package; + + // Delete phase: remove package directory and .header file + auto package_path = ResolvePackagePath(xuid, data); + + uint64_t used_xuid = (data.xuid != uint64_t(-1) && data.xuid != 0) ? uint64_t(data.xuid) : xuid; + auto header_path = + ResolvePackageHeaderPath(data.file_name(), used_xuid, data.title_id, data.content_type); + + std::error_code ec; + auto dir_removed = std::filesystem::remove_all(package_path, ec); + if (ec) { + return X_ERROR_ACCESS_DENIED; + } + + std::error_code ec2; + bool header_removed = std::filesystem::remove(header_path, ec2); + + if (dir_removed > 0 || header_removed) { + return X_ERROR_SUCCESS; + } + return X_ERROR_FILE_NOT_FOUND; } std::filesystem::path ContentManager::ResolveGameUserContentPath() { @@ -383,11 +449,46 @@ std::filesystem::path ContentManager::ResolveGameUserContentPath() { return root_path_ / title_id / kGameUserContentDirName / user_name; } +std::unordered_map::iterator +ContentManager::FindOpenPackageByData(const XCONTENT_AGGREGATE_DATA& data) { + // Resolve kCurrentlyRunningTitleId so both sides compare actual title IDs. + uint32_t query_title = data.title_id; + if (query_title == kCurrentlyRunningTitleId) { + query_title = kernel_state_->title_id(); + } + + for (auto it = open_packages_.begin(); it != open_packages_.end(); ++it) { + const auto& pkg = it->second->GetPackageContentData(); + + uint32_t pkg_title = pkg.title_id; + if (pkg_title == kCurrentlyRunningTitleId) { + pkg_title = kernel_state_->title_id(); + } + + // Match on content_type + file_name + resolved title_id. + // device_id is a virtual storage selector, not a content identifier. + if (data.content_type == pkg.content_type && data.file_name() == pkg.file_name() && + query_title == pkg_title) { + return it; + } + } + return open_packages_.end(); +} + +ContentPackage* ContentManager::DetachPackage( + std::unordered_map::iterator it) { + CloseOpenedFilesFromContent(it->first.view()); + ContentPackage* package = it->second; + open_packages_.erase(it); + return package; +} + bool ContentManager::IsContentOpen(const XCONTENT_AGGREGATE_DATA& data) const { - return std::any_of(open_packages_.cbegin(), open_packages_.cend(), - [data](std::pair content) { - return data == content.second->GetPackageContentData(); - }); + return std::any_of(open_packages_.cbegin(), open_packages_.cend(), [&data](const auto& content) { + return data == content.second->GetPackageContentData(); + }); } std::filesystem::path ContentManager::GetOpenPackagePath(const std::string_view root_name) const { @@ -417,6 +518,131 @@ void ContentManager::CloseOpenedFilesFromContent(const std::string_view root_nam } } +static X_RESULT ExtractEntry(rex::filesystem::Entry* entry, + const std::filesystem::path& base_path) { + auto dest_path = base_path / rex::to_path(rex::string::utf8_fix_path_separators(entry->path())); + + if (entry->attributes() & rex::filesystem::kFileAttributeDirectory) { + std::error_code ec; + std::filesystem::create_directories(dest_path, ec); + if (ec) { + return X_ERROR_ACCESS_DENIED; + } + return X_ERROR_SUCCESS; + } + + // Ensure parent directory exists + std::error_code ec; + std::filesystem::create_directories(dest_path.parent_path(), ec); + + rex::filesystem::File* in_file = nullptr; + X_STATUS status = entry->Open(rex::filesystem::FileAccess::kFileReadData, &in_file); + if (status != X_STATUS_SUCCESS) { + return X_ERROR_ACCESS_DENIED; + } + + auto out_file = rex::filesystem::OpenFile(dest_path, "wb"); + if (!out_file) { + in_file->Destroy(); + return X_ERROR_ACCESS_DENIED; + } + + constexpr size_t kBufferSize = 4 * 1024 * 1024; // 4 MiB + auto buffer = std::make_unique(kBufferSize); + size_t remaining = entry->size(); + size_t offset = 0; + + while (remaining > 0) { + size_t bytes_read = 0; + size_t to_read = std::min(remaining, kBufferSize); + in_file->ReadSync(std::span(buffer.get(), to_read), offset, &bytes_read); + if (bytes_read == 0) { + break; + } + fwrite(buffer.get(), 1, bytes_read, out_file); + offset += bytes_read; + remaining -= bytes_read; + } + + fclose(out_file); + in_file->Destroy(); + return X_ERROR_SUCCESS; +} + +X_RESULT ContentManager::InstallContent(const std::filesystem::path& package_path) { + if (!std::filesystem::exists(package_path)) { + return X_ERROR_FILE_NOT_FOUND; + } + + // Mount the STFS package as a virtual filesystem device + auto device = std::make_unique("", package_path); + if (!device->Initialize()) { + return X_ERROR_ACCESS_DENIED; + } + + // Derive install destination: + // root_path_/0000000000000000/{title_id}/00000002/{filename}/ + auto file_name = rex::path_to_utf8(package_path.filename()); + + XCONTENT_AGGREGATE_DATA content_data; + content_data.device_id = static_cast(DummyDeviceId::HDD); + content_data.content_type = XContentType::kMarketplaceContent; + content_data.title_id = kernel_state_->title_id(); + content_data.xuid = 0; + content_data.set_file_name(file_name); + + // Read display name from STFS metadata + auto display_name = device->header().metadata.display_name(rex::system::XLanguage::kEnglish); + if (!display_name.empty()) { + content_data.set_display_name(display_name); + } else { + content_data.set_display_name(rex::path_to_utf16(package_path.filename())); + } + + auto install_path = ResolvePackagePath(0, content_data); + + // Create destination directory + std::error_code ec; + std::filesystem::create_directories(install_path, ec); + if (ec) { + return X_ERROR_ACCESS_DENIED; + } + + // Extract all files breadth-first + auto* root = device->ResolvePath(""); + if (!root) { + return X_ERROR_ACCESS_DENIED; + } + + std::queue queue; + queue.push(root); + + while (!queue.empty()) { + auto* entry = queue.front(); + queue.pop(); + + for (auto& child : entry->children()) { + queue.push(child.get()); + } + + auto result = ExtractEntry(entry, install_path); + if (result != X_ERROR_SUCCESS) { + return result; + } + } + + // Compute license mask from STFS header licenses + uint32_t license_mask = 0; + for (size_t i = 0; i < 0x10; i++) { + if (device->header().header.licenses[i].license_flags) { + license_mask |= device->header().header.licenses[i].license_bits; + } + } + + // Write .header file + return WriteContentHeaderFile(0, content_data, license_mask); +} + } // namespace xam } // namespace system } // namespace rex