mirror of
https://github.com/sal063/AC6_recomp
synced 2026-07-11 23:30:18 -04:00
Integrate rexglue 0.7.5 and 0.7.6 improvements
This commit is contained in:
@@ -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>
|
||||
+142
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <native/filesystem/entry.h>
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <native/filesystem/file.h>
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <native/filesystem/wildcard.h>
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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_;
|
||||
|
||||
|
||||
+23
-13
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+834
@@ -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
|
||||
+41
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user