Integrate rexglue 0.7.5 and 0.7.6 improvements

This commit is contained in:
salh
2026-04-22 14:30:40 +03:00
parent db811bd0ba
commit d3bb76be61
28 changed files with 2344 additions and 395 deletions
+8
View File
@@ -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()
@@ -0,0 +1,3 @@
#pragma once
#include <native/filesystem/device.h>
@@ -0,0 +1,3 @@
#pragma once
#include <native/filesystem/devices/host_path_device.h>
@@ -0,0 +1,3 @@
#pragma once
#include <native/filesystem/devices/null_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 <rex/filesystem/devices/stfs_xbox.h>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <rex/filesystem/device.h>
#include <rex/math.h>
#include <rex/string/util.h>
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<StfsHeader> 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<size_t, FILE*> files_;
size_t files_total_size_;
size_t svod_base_offset_;
std::unique_ptr<Entry> root_entry_;
StfsHeader header_;
SvodLayoutType svod_layout_;
uint32_t blocks_per_hash_table_;
uint32_t block_step[2];
std::unordered_map<size_t, StfsHashTable> cached_hash_tables_;
};
} // namespace rex::filesystem
@@ -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 <map>
#include <string>
#include <vector>
#include <rex/filesystem/entry.h>
#include <rex/filesystem/file.h>
namespace rex::filesystem {
typedef std::map<size_t, FILE*> 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<StfsContainerEntry> 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<BlockRecord>& 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<BlockRecord> block_list_;
};
} // namespace rex::filesystem
@@ -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 <span>
#include <rex/filesystem/file.h>
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<uint8_t> buffer, size_t byte_offset, size_t* out_bytes_read) override;
X_STATUS WriteSync(std::span<const uint8_t> 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
@@ -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 <time.h>
#include <rex/assert.h>
#include <rex/memory.h>
#include <rex/platform.h>
#include <rex/string/util.h>
#include <rex/system/util/xex2_info.h>
#include <rex/system/xcontent.h>
#include <rex/types.h>
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<uint32_t> total_block_count;
be<uint32_t> 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<uint32_t> 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<uint32_t> 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<uint16_t> directory_index;
be<uint32_t> length;
be<uint16_t> create_date;
be<uint16_t> create_time;
be<uint16_t> modified_date;
be<uint16_t> 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<uint64_t> licensee_id;
be<uint32_t> license_bits;
be<uint32_t> license_flags;
};
static_assert_size(XContentLicense, 0x10);
struct XContentMediaData {
uint8_t series_id[0x10];
uint8_t season_id[0x10];
be<uint16_t> season_number;
be<uint16_t> episode_number;
};
static_assert_size(XContentMediaData, 0x24);
struct XContentAvatarAssetData {
be<uint32_t> sub_category;
be<uint32_t> 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<XContentType> content_type;
be<uint32_t> metadata_version;
be<uint64_t> content_size;
xex2_opt_execution_info execution_info;
uint8_t console_id[5];
be<uint64_t> profile_id;
union {
StfsVolumeDescriptor stfs;
SvodDeviceDescriptor svod;
} volume_descriptor;
be<uint32_t> data_file_count;
be<uint64_t> data_file_size;
be<XContentVolumeType> volume_type;
be<uint64_t> online_creator;
be<uint32_t> category;
uint8_t reserved2[0x20];
union {
XContentMediaData media_data;
XContentAvatarAssetData avatar_asset_data;
} metadata_v2;
uint8_t device_id[0x14];
union {
be<uint16_t> uint[kNumLanguagesV1][128];
char16_t chars[kNumLanguagesV1][128];
} display_name_raw;
union {
be<uint16_t> uint[kNumLanguagesV1][128];
char16_t chars[kNumLanguagesV1][128];
} description_raw;
union {
be<uint16_t> uint[64];
char16_t chars[64];
} publisher_raw;
union {
be<uint16_t> uint[64];
char16_t chars[64];
} title_name_raw;
union {
uint8_t as_byte;
XContentAttributes bits;
} flags;
be<uint32_t> thumbnail_size;
be<uint32_t> title_thumbnail_size;
uint8_t thumbnail[kThumbLengthV2];
union {
be<uint16_t> uint[kNumLanguagesV2 - kNumLanguagesV1][128];
char16_t chars[kNumLanguagesV2 - kNumLanguagesV1][128];
} display_name_ex_raw;
uint8_t title_thumbnail[kThumbLengthV2];
union {
be<uint16_t> 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<uint16_t>* 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<std::u16string>(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<uint16_t>* 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<std::u16string>(str);
}
std::u16string publisher() const {
return memory::load_and_swap<std::u16string>(publisher_raw.uint);
}
std::u16string title_name() const {
return memory::load_and_swap<std::u16string>(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<XContentPackageType> magic;
uint8_t signature[0x228];
XContentLicense licenses[0x10];
uint8_t content_id[0x14];
be<uint32_t> 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
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#include <native/filesystem/entry.h>
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#include <native/filesystem/file.h>
@@ -0,0 +1,3 @@
#pragma once
#include <native/filesystem/wildcard.h>
+1 -1
View File
@@ -17,7 +17,7 @@
// Function declarations, CVAR declarations
#include <rex/logging/api.h>
// Logging macros (parameterized, legacy aliases, FN, TID)
// Logging macros (parameterized, per-subsystem aliases, category definition)
#include <rex/logging/macros.h>
// Fatal error and assertion macros
+19 -12
View File
@@ -13,6 +13,7 @@
#include <rex/logging/types.h>
#include <filesystem>
#include <span>
#include <rex/cvar.h>
@@ -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<std::string, std::string>& 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<std::string, std::string> ParseCategoryLevelsFromConfig(
const std::filesystem::path& config_path);
} // namespace rex
+102 -113
View File
@@ -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 <tomc@tctechstuff.com>
* 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();
}
+13 -55
View File
@@ -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<spdlog::logger> logger; /**< Per-category spdlog logger instance */
std::optional<LogCategoryId> 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<LogCategory> CategoryFromName(const std::string& name);
/** @deprecated Use GetLogger(LogCategoryId) instead. */
std::shared_ptr<spdlog::logger> GetLogger(LogCategory category);
/** @deprecated Use SetCategoryLevel(LogCategoryId, level) instead. */
void SetCategoryLevel(LogCategory category, spdlog::level::level_enum level);
} // namespace rex
@@ -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<uint8_t> 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<string::string_key_case, ContentPackage*,
string::string_key_case::Hash>::iterator
FindOpenPackageByData(const XCONTENT_AGGREGATE_DATA& data);
ContentPackage* DetachPackage(std::unordered_map<string::string_key_case, ContentPackage*,
string::string_key_case::Hash>::iterator it);
KernelState* kernel_state_;
std::filesystem::path root_path_;
@@ -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;
}
+208 -124
View File
@@ -11,50 +11,91 @@
#include <algorithm>
#include <cctype>
#include <charconv>
#include <filesystem>
#include <mutex>
#include <unordered_map>
#include <vector>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <toml++/toml.hpp>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/platform.h>
#if REX_PLATFORM_WIN32
#include <spdlog/sinks/msvc_sink.h>
#else
#include <spdlog/sinks/stdout_sinks.h>
#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<LogCategoryEntry> 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<spdlog::sink_ptr> 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<spdlog::sink_ptr> BuildDefaultSinks() {
std::vector<spdlog::sink_ptr> 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<spdlog::logger> 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<spdlog::sinks::msvc_sink_mt>();
#else
auto sink = std::make_shared<spdlog::sinks::stdout_sink_mt>();
#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<spdlog::sinks::stdout_color_sink_mt>();
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<spdlog::sinks::basic_file_sink_mt>(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<spdlog::sinks::stdout_color_sink_mt>();
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<spdlog::sinks::basic_file_sink_mt>(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<spdlog::sinks::rotating_file_sink_mt>(
resolved_path, static_cast<size_t>(REXCVAR_GET(log_max_file_size_mb)) * 1024 * 1024,
static_cast<size_t>(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<uint16_t>(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<uint16_t>(i)};
}
uint16_t id = static_cast<uint16_t>(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<LogCategoryId> 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<const LogCategoryEntry> 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<spdlog::logger> 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<spdlog::logger> GetLogger(LogCategoryId category) {
}
std::shared_ptr<spdlog::logger> 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<uint16_t>(i)}, *level);
}
}
});
}
@@ -406,53 +515,28 @@ LogConfig BuildLogConfig(const char* log_file, const std::string& cli_level,
return config;
}
// ==========================================================================
// Guest Thread ID
// ==========================================================================
std::map<std::string, std::string> ParseCategoryLevelsFromConfig(
const std::filesystem::path& config_path) {
std::map<std::string, std::string> 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<uint16_t>(std::to_underlying(category));
return LogCategoryId{idx};
}
std::optional<LogCategory> 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<std::string, LogCategory> 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<spdlog::logger> GetLogger(LogCategory category) {
return GetLogger(CategoryToId(category));
}
void SetCategoryLevel(LogCategory category, spdlog::level::level_enum level) {
SetCategoryLevel(CategoryToId(category), level);
return result;
}
} // namespace rex
+1 -1
View File
@@ -183,7 +183,7 @@ std::unique_ptr<FileHandle> 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,
@@ -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)
@@ -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 <rex/filesystem/devices/stfs_container_device.h>
#include <rex/filesystem/devices/stfs_container_entry.h>
#include <algorithm>
#include <queue>
#include <vector>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/system/xio.h>
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<StfsHeader> 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<StfsHeader>();
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<int>(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<int>(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<Entry>(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<char[]>(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<Entry>(root_entry);
std::vector<StfsContainerEntry*> 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<const char*>(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<size_t>(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<uint32_t>(map->data()));
}
bool StfsContainerDevice::ResolveFromFolder(const std::filesystem::path& path) {
// Scan through folders until a file with magic is found
std::queue<filesystem::FileInfo> 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
@@ -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 <rex/filesystem/devices/stfs_container_entry.h>
#include <rex/filesystem/devices/stfs_container_file.h>
#include <map>
#include <rex/math.h>
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> 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<StfsContainerEntry>(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
@@ -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 <rex/filesystem/devices/stfs_container_entry.h>
#include <rex/filesystem/devices/stfs_container_file.h>
#include <algorithm>
#include <cmath>
#include <rex/math.h>
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<uint8_t> 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
+14 -6
View File
@@ -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::LogCaptureSink>();
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<ui::DebugOverlayDialog>(imgui_drawer_.get());
console_overlay_ = std::make_unique<ui::ConsoleDialog>(imgui_drawer_.get(), log_sink_);
settings_overlay_ = std::make_unique<ui::SettingsDialog>(
imgui_drawer_.get(), exe_dir / (std::string(GetName()) + ".toml"));
imgui_drawer_.get(), config_path);
// Allow subclass to add custom dialogs
OnCreateDialogs(imgui_drawer_.get());
@@ -3,16 +3,12 @@
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <spdlog/common.h>
#include <rex/cvar.h>
#include <rex/filesystem.h>
#include <rex/logging.h>
#include <native/ui/windowed_app.h>
#include <native/ui/windowed_app_context_gtk.h>
@@ -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;
@@ -1,7 +1,6 @@
// Native UI runtime - Win32 windowed app entry point
// Part of the AC6 Recompilation native presenter/window layer
#include <cstdio>
#include <cstdlib>
#include <map>
#include <memory>
@@ -14,8 +13,6 @@
#include <native/ui/windowed_app.h>
#include <native/ui/windowed_app_context_win.h>
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<int>(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<rex::ui::WindowedApp> 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();
}
+2
View File
@@ -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<std::string, std::string> 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
+241 -15
View File
@@ -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 <array>
#include <cstring>
#include <queue>
#include <string>
#include <fmt/format.h>
#include <rex/filesystem.h>
#include <native/filesystem/devices/host_path_device.h>
#include <rex/filesystem/devices/host_path_device.h>
#include <rex/filesystem/devices/stfs_container_device.h>
#include <rex/string.h>
#include <rex/system/kernel_state.h>
#include <rex/system/xam/content_device.h>
#include <rex/system/xam/content_manager.h>
#include <rex/system/xfile.h>
#include <rex/system/xobject.h>
@@ -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<string::string_key_case, ContentPackage*,
string::string_key_case::Hash>::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<string::string_key_case, ContentPackage*,
string::string_key_case::Hash>::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<string::string_key_case, ContentPackage*> 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<uint8_t[]>(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<uint8_t>(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<rex::filesystem::StfsContainerDevice>("", 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<uint32_t>(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<rex::filesystem::Entry*> 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