Upgraded to RexGlue v0.8 and edited a bit of the Audio code

This commit is contained in:
salh
2026-05-29 22:17:02 +03:00
parent 0fddc26760
commit ead11217b7
113 changed files with 4655 additions and 1032 deletions
+55
View File
@@ -10670,3 +10670,58 @@ registers = ["r3", "r4", "r5", "r6", "ctr"]
address = 0x821CCC5C
name = "ac6PacDecoderDumpHook"
registers = ["r4", "r10", "r11", "r31"]
# ----------------------------------------------------------------------------
# Codec internal tracing probes.
#
# Wired at the entries of every PPC function involved in mode-1 decoding so we
# can observe the runtime state at each transition. Gated by env var
# AC6_TRACE_CODEC_INTERNALS=1 and rate-limited (first 3 mode-1 invocations
# only) so log volume stays bounded. Used to confirm whether the bytes the
# codec actually consumes match the .compressed.bin files we dump.
#
# Hook pipeline (top-down):
# sub_822CF510 codec dispatcher -> ac6PacCodecDispatchProbe
# sub_822CF2F8 mode-1 entry -> ac6PacMode1EntryProbe (one-shot ROM dump
# of static-Huffman tables at 0x8243D1F8 /
# 0x8243DDF8)
# sub_822CCB50 bit fetcher -> ac6PacBitFetcherProbe
# sub_822CEAC0 dynamic header -> ac6PacDynamicHeaderProbe
# sub_822CDB38 tree builder -> ac6PacTreeBuilderProbe (dumps the 19
# u16 code-length array at r3+108)
# sub_822CD068 / sub_822CD758 -> ac6PacBlockConsumerProbe
# ----------------------------------------------------------------------------
[[midasm_hook]]
address = 0x822CF510
name = "ac6PacCodecDispatchProbe"
registers = ["r3"]
[[midasm_hook]]
address = 0x822CF2F8
name = "ac6PacMode1EntryProbe"
registers = ["r3"]
[[midasm_hook]]
address = 0x822CCB50
name = "ac6PacBitFetcherProbe"
registers = ["r3", "r4", "r5"]
[[midasm_hook]]
address = 0x822CEAC0
name = "ac6PacDynamicHeaderProbe"
registers = ["r3", "r4"]
[[midasm_hook]]
address = 0x822CDB38
name = "ac6PacTreeBuilderProbe"
registers = ["r3"]
[[midasm_hook]]
address = 0x822CD068
name = "ac6PacBlockConsumerProbe"
registers = ["r3"]
[[midasm_hook]]
address = 0x822CD758
name = "ac6PacBlockConsumerProbe"
registers = ["r3"]
+1 -1
View File
@@ -17,7 +17,7 @@ else()
if(REXSDK_VERSION)
find_package(rexglue ${REXSDK_VERSION} EXACT QUIET CONFIG)
else()
find_package(rexglue 0.7.4 QUIET CONFIG)
find_package(rexglue 0.8.0 QUIET CONFIG)
endif()
if(NOT rexglue_FOUND)
message(FATAL_ERROR
BIN
View File
Binary file not shown.
+25 -8
View File
@@ -10,19 +10,36 @@ It automates these stages:
4. `SWG` UI metadata parsing
5. `NTXR` texture export
## Important Limitation
## Offline mode-1 decompression (solved)
The pipeline can process **all PAC entries and all runtime dumps you already have**, but it **cannot yet offline-decompress every compressed PAC entry in the archive** by itself.
The pipeline can now **decompress every compressed PAC entry fully offline**, with
no need to launch the game. The AC6 "mode-1" codec has been reverse-engineered:
That means:
1. **Descramble**: stored bytes are XORed with an 8-byte repeating pad. The pad
for a DATA.TBL entry is derived from the entry's table index:
- Raw PAC entries are handled in one pass.
- Runtime-decoded assets are handled in one pass.
- If a compressed asset has never been decoded by the game and never appeared in `out/ac6_pac_runtime_dump`, this pipeline cannot currently materialize it.
```
pad(index) = pi_words[2*(index % 256) + 1] ++ pi_words[2*(index % 256) + 2]
```
So the pipeline is "all at once" for the **current corpus**, not yet "decode the entire PAC archive from scratch with no runtime help".
where `pi_words` are the big-endian base-2^32 words of the fractional part of
pi (`0x243F6A88 0x85A308D3 0x13198A2E ...`). The game generates these at runtime
via Machin's formula (`4*arctan(1/5) - arctan(1/239)`); the offline tool simply
computes pi to the needed precision.
If you want truly complete one-shot extraction of every compressed PAC asset without launching the game, the remaining missing piece is an offline implementation of AC6's mode-1 decompressor.
2. **Inflate**: the descrambled bytes are raw DEFLATE (RFC 1951, no zlib/gzip
wrapper) -- `zlib.decompress(data, wbits=-15)`.
This is implemented in `tools/ac6_mode1_codec.py` and wired into the extractor:
```powershell
python .\tools\extract_ac6_pac.py <asset_root> --decompress
```
Compressed entries are written as `files/DATA0x/compressed/<index>.decompressed.bin`
and the manifest records `decompressed_count` / `decode_failures`. Playing the game
to pre-populate `out/ac6_pac_runtime_dump` is no longer required for compressed-entry
extraction; it remains useful only for assets synthesized at runtime.
## Prerequisites
+17
View File
@@ -441,6 +441,23 @@ void Ac6DumpPacDecodedEntry(uint16_t entry_index, uint8_t codec_mode, uint32_t c
path.string());
}
std::vector<uint8_t> Ac6PeekCompressedHead(uint32_t entry_index, std::size_t max_bytes) {
if (max_bytes == 0) return {};
auto rec = ac6_pac_index::GetByIndex(entry_index);
if (!rec || rec->compressed_size == 0) return {};
const uint32_t want = static_cast<uint32_t>(
std::min<size_t>(max_bytes, static_cast<size_t>(rec->compressed_size)));
const uint32_t start = rec->offset;
if (want == 0 || start > UINT32_MAX - want) return {};
const uint32_t end = start + want;
std::scoped_lock lock(ArchiveMutex());
auto& archive = GetArchive(rec->is_data01);
if (!IsRangeCovered(archive.chunks, start, end)) return {};
return GatherRange(archive.chunks, start, end);
}
void Ac6OnPacReadCompleted(std::string_view path, uint32_t guest_buffer, uint64_t file_offset,
uint32_t bytes_read) {
if (!DumpingEnabled() || guest_buffer == 0 || bytes_read == 0) return;
+10
View File
@@ -1,7 +1,9 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <vector>
// Writes a single decoded PAC entry to the runtime dump directory.
// Filename format: entry_<index>_mode<mode>_c<csize>_u<usize>_off<hex_offset>.bin
@@ -9,6 +11,14 @@ void Ac6DumpPacDecodedEntry(uint16_t entry_index, uint8_t codec_mode, uint32_t c
uint32_t decompressed_size, uint32_t source_offset,
const uint8_t* host_data);
// Returns up to `max_bytes` of the compressed source for the given DATA.TBL
// entry, drawing from the chunks recorded by Ac6OnPacReadCompleted. Returns
// an empty vector if DATA.TBL isn't loaded yet, the entry index is unknown,
// the entry has zero compressed size, or the relevant byte range hasn't been
// streamed in yet. Safe to call from any thread; takes the same mutex used
// by the dump path so it can race with in-flight reads.
std::vector<uint8_t> Ac6PeekCompressedHead(uint32_t entry_index, std::size_t max_bytes);
// Hook called from the kernel-side NtReadFile completion path for any read
// targeting an AC6 PAC archive (DATA00.PAC, DATA01.PAC) or DATA.TBL itself.
// - For DATA.TBL reads: parses and caches the index.
+335 -2
View File
@@ -1,4 +1,5 @@
#include "ac6_pac_decoder_probe.h"
#include "ac6_pac_decode_dump.h"
#include <rex/logging.h>
#include <rex/logging/api.h>
@@ -7,11 +8,18 @@
#include <rex/system/kernel_state.h>
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <sstream>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -176,6 +184,39 @@ void ac6PacWorkerL2DispatchHook(PPCRegister& r3, PPCRegister& r4, PPCRegister& r
}
}
namespace {
bool DumpEnabledEnv() {
static const bool enabled = [] {
const char* v = std::getenv("AC6_DUMP_PAC_DECODED");
return v && v[0] && std::string_view(v) != "0";
}();
return enabled;
}
std::mutex& DecoderSeenMutex() {
static std::mutex m;
return m;
}
std::unordered_set<uint64_t>& DecoderSeenEntries() {
static std::unordered_set<uint64_t> s;
return s;
}
std::string HexPreviewBytes(const uint8_t* data, std::size_t len) {
static constexpr char kHex[] = "0123456789abcdef";
std::string out;
out.reserve(len * 2);
for (std::size_t i = 0; i < len; ++i) {
out.push_back(kHex[(data[i] >> 4) & 0xF]);
out.push_back(kHex[data[i] & 0xF]);
}
return out;
}
} // namespace
void ac6PacDecoderDumpHook(PPCRegister& r4, PPCRegister& r10, PPCRegister& r11,
PPCRegister& r31) {
auto* memory = REX_KERNEL_MEMORY();
@@ -201,6 +242,298 @@ void ac6PacDecoderDumpHook(PPCRegister& r4, PPCRegister& r10, PPCRegister& r11,
const auto* host = memory->TranslateVirtual<const uint8_t*>(r4.u32);
if (!host) return;
Ac6DumpPacDecodedEntry(static_cast<uint16_t>(r10.u32 & 0xFFFFu),
codec, csize, usize, source_offset, host);
const uint16_t entry_index = static_cast<uint16_t>(r10.u32 & 0xFFFFu);
// Per-distinct-entry RE log. Dedup on (csize,usize) because r10&0xFFFF is a
// runtime tag that collides across distinct TOC entries. Captures the
// descramble pad and its seed so we can crack the per-entry XOR key:
// codec_ctx = streamer(r31) + 344
// pad (8B) @ codec_ctx + 22380 == r31 + 22724
// seed (u32) @ codec_ctx + 22392 == r31 + 22736
// The pad is generated by sub_822CF018(seed) and applied as a repeating
// 8-byte XOR over the codec input. Logging (csize,usize,seed,pad) for every
// distinct entry in one play session gives us the seed->pad table and the
// seed=f(entry) relationship needed for a fully offline decoder.
if (DumpEnabledEnv()) {
const uint64_t dedup_key = (static_cast<uint64_t>(csize) << 32) | usize;
bool first = false;
{
std::scoped_lock lock(DecoderSeenMutex());
first = DecoderSeenEntries().insert(dedup_key).second;
}
if (first) {
constexpr std::size_t kPreviewBytes = 32;
std::string record_hex;
const uint32_t record_end_check =
r11.u32 > UINT32_MAX - kPreviewBytes ? 0 : r11.u32 + kPreviewBytes - 1;
if (record_end_check && memory->LookupHeap(r11.u32) &&
memory->LookupHeap(record_end_check)) {
const auto* rec_host = memory->TranslateVirtual<const uint8_t*>(r11.u32);
if (rec_host) record_hex = HexPreviewBytes(rec_host, kPreviewBytes);
}
const std::size_t out_preview =
usize < kPreviewBytes ? static_cast<std::size_t>(usize) : kPreviewBytes;
const std::string out_hex = HexPreviewBytes(host, out_preview);
std::vector<uint8_t> compressed_head =
Ac6PeekCompressedHead(entry_index, kPreviewBytes);
const std::string in_hex =
compressed_head.empty()
? std::string{}
: HexPreviewBytes(compressed_head.data(), compressed_head.size());
// Descramble pad + seed, read from the codec sub-struct at r31+344.
const uint32_t seed = load_u32_be(r31.u32 + 22736);
std::string pad_hex;
const uint32_t pad_addr = r31.u32 + 22724;
if (memory->LookupHeap(pad_addr) && memory->LookupHeap(pad_addr + 7)) {
const auto* pad = memory->TranslateVirtual<const uint8_t*>(pad_addr);
if (pad) pad_hex = HexPreviewBytes(pad, 8);
}
REXFS_INFO(
"[AC6 PAC DECODER] entry={} mode={} csize=0x{:x} usize=0x{:x} src_off=0x{:x} "
"seed=0x{:08x} pad={} r4=0x{:08X} r11=0x{:08X} record_hex={} in_head={} out_head={}",
entry_index, uint32_t(codec), csize, usize, source_offset, seed,
pad_hex.empty() ? "<unmapped>" : pad_hex, r4.u32, r11.u32,
record_hex.empty() ? "<unmapped>" : record_hex,
in_hex.empty() ? "<not_buffered>" : in_hex,
out_hex.empty() ? "<empty>" : out_hex);
}
}
Ac6DumpPacDecodedEntry(entry_index, codec, csize, usize, source_offset, host);
}
// ============================================================================
// Codec internal probes
// ----------------------------------------------------------------------------
// Gated by AC6_TRACE_CODEC_INTERNALS=1. Rate-limited to the first
// kMaxTracedMode1Calls invocations of the mode-1 decoder so log volume stays
// bounded.
// ============================================================================
namespace {
bool CodecTraceEnabledEnv() {
static const bool enabled = [] {
const char* v = std::getenv("AC6_TRACE_CODEC_INTERNALS");
return v && v[0] && std::string_view(v) != "0";
}();
return enabled;
}
std::atomic<int>& Mode1CallCount() {
static std::atomic<int> c{0};
return c;
}
// First-N detailed-internals trace: bit-fetcher / tree-builder / dynamic-header
// / block-consumer probes only fire while Mode1CallCount <= this. Three
// invocations is enough to capture one entry's worth of internal activity.
constexpr int kMaxInternalTracedCalls = 3;
// Mode-1 entry probe (including codec_input dump) fires up to this many times,
// regardless of the internal-trace gate. Lets us collect input buffers from
// many entries in a single play session to derive the per-entry XOR key.
constexpr int kMaxMode1EntryProbeCalls = 100;
bool CodecTraceActive() {
if (!CodecTraceEnabledEnv()) return false;
return Mode1CallCount().load(std::memory_order_relaxed) <= kMaxInternalTracedCalls;
}
uint32_t LoadGuestU32BE(rex::memory::Memory* memory, uint32_t va) {
if (!memory || va > UINT32_MAX - 3) return 0;
if (!memory->LookupHeap(va) || !memory->LookupHeap(va + 3)) return 0;
return rex::memory::load_and_swap<uint32_t>(memory->TranslateVirtual(va));
}
uint64_t LoadGuestU64BE(rex::memory::Memory* memory, uint32_t va) {
if (!memory || va > UINT32_MAX - 7) return 0;
if (!memory->LookupHeap(va) || !memory->LookupHeap(va + 7)) return 0;
return rex::memory::load_and_swap<uint64_t>(memory->TranslateVirtual(va));
}
uint16_t LoadGuestU16BE(rex::memory::Memory* memory, uint32_t va) {
if (!memory || va > UINT32_MAX - 1) return 0;
if (!memory->LookupHeap(va) || !memory->LookupHeap(va + 1)) return 0;
return rex::memory::load_and_swap<uint16_t>(memory->TranslateVirtual(va));
}
void DumpStaticHuffmanTablesOnce() {
static std::once_flag flag;
std::call_once(flag, [] {
auto* memory = REX_KERNEL_MEMORY();
if (!memory) return;
// Addresses confirmed via IDA Pro disassembly of sub_822CF2F8.
const uint32_t kAddrs[2] = {0x827BD1F8u, 0x827BDDF8u};
for (uint32_t addr : kAddrs) {
constexpr uint32_t kSize = 256;
if (!memory->LookupHeap(addr) || !memory->LookupHeap(addr + kSize - 1)) continue;
const auto* p = memory->TranslateVirtual<const uint8_t*>(addr);
if (!p) continue;
REXFS_INFO("[AC6 PAC CODEC] ROM table @ 0x{:08X} ({} bytes): {}",
addr, kSize, HexPreviewBytes(p, kSize));
}
});
}
// Dumps up to `max_bytes` from the codec's input buffer (base = *(ctx + 56))
// to disk as codec_input_<call_n>.bin. Lets us compare the bytes the codec
// actually decompresses against the kernel-recorded .compressed.bin to
// identify any pre-codec transformation (descrambling, XOR, etc.).
void DumpCodecInputBuffer(uint32_t ctx, int call_n, std::size_t max_bytes) {
auto* memory = REX_KERNEL_MEMORY();
if (!memory) return;
const uint32_t base = LoadGuestU32BE(memory, ctx + 56);
if (base == 0) return;
if (!memory->LookupHeap(base)) return;
const uint32_t avail = LoadGuestU32BE(memory, ctx + 16); // in_end
const std::size_t want =
std::min<std::size_t>(max_bytes, avail > 0 ? static_cast<std::size_t>(avail) : max_bytes);
if (want == 0) return;
if (!memory->LookupHeap(base + static_cast<uint32_t>(want) - 1)) return;
const auto* p = memory->TranslateVirtual<const uint8_t*>(base);
if (!p) return;
// Anchor output next to the existing PAC runtime dumps so analysis can
// compare byte-by-byte against entry_*.compressed.bin in the same dir.
static const std::filesystem::path kDumpRoot = [] {
std::error_code ec;
auto cwd = std::filesystem::current_path(ec);
std::filesystem::path candidate = (ec ? std::filesystem::path() : cwd) /
"out" / "ac6_pac_runtime_dump";
// Walk up to find repo root if cwd is somewhere deeper.
for (auto cur = candidate.parent_path().parent_path();
!cur.empty(); cur = cur.parent_path()) {
if (std::filesystem::exists(cur / "tools" / "run_ac6_asset_pipeline.py", ec)) {
candidate = cur / "out" / "ac6_pac_runtime_dump";
break;
}
if (cur.has_parent_path() && cur == cur.parent_path()) break;
}
return candidate;
}();
std::error_code ec;
std::filesystem::create_directories(kDumpRoot, ec);
std::ostringstream name;
name << "codec_input_" << call_n << "_base0x" << std::hex << base << "_size0x" << avail
<< ".bin";
const auto path = kDumpRoot / name.str();
std::ofstream f(path, std::ios::binary | std::ios::trunc);
if (!f) {
REXFS_ERROR("[AC6 PAC CODEC] failed to open codec input dump {}", path.string());
return;
}
f.write(reinterpret_cast<const char*>(p), static_cast<std::streamsize>(want));
REXFS_INFO("[AC6 PAC CODEC] dumped codec input #{} base=0x{:08X} size=0x{:x} "
"(first {} bytes) path={}", call_n, base, avail, want, path.string());
}
} // namespace
void ac6PacCodecDispatchProbe(PPCRegister& r3) {
if (!CodecTraceActive()) return;
auto* memory = REX_KERNEL_MEMORY();
if (!memory) return;
const uint32_t ctx = r3.u32;
uint8_t codec_mode = 0;
if (memory->LookupHeap(ctx + 68)) {
codec_mode = *memory->TranslateVirtual<const uint8_t*>(ctx + 68);
}
REXFS_INFO("[AC6 PAC CODEC] dispatch ctx=0x{:08X} codec_mode={} disp_state=0x{:x}",
ctx, uint32_t(codec_mode), LoadGuestU32BE(memory, ctx + 88));
}
void ac6PacMode1EntryProbe(PPCRegister& r3) {
if (!CodecTraceEnabledEnv()) return;
const int call_n = Mode1CallCount().fetch_add(1, std::memory_order_relaxed) + 1;
if (call_n > kMaxMode1EntryProbeCalls) return;
auto* memory = REX_KERNEL_MEMORY();
if (!memory) return;
DumpStaticHuffmanTablesOnce();
const uint32_t ctx = r3.u32;
REXFS_INFO("[AC6 PAC CODEC] mode1_entry #{} ctx=0x{:08X} state=0x{:x} "
"in_ptr=0x{:x} in_end=0x{:x} out_pos=0x{:x} bit_buf=0x{:016x} bit_count={} "
"base_ptr=0x{:08X}",
call_n, ctx,
LoadGuestU32BE(memory, ctx + 84),
LoadGuestU64BE(memory, ctx + 8),
LoadGuestU64BE(memory, ctx + 16),
LoadGuestU64BE(memory, ctx + 32),
LoadGuestU64BE(memory, ctx + 72),
LoadGuestU32BE(memory, ctx + 80),
LoadGuestU32BE(memory, ctx + 56));
// Dump the codec input buffer on state==0 (initial chunk of a new entry)
// so we can diff it against the kernel-recorded .compressed.bin to find
// any transformation between PAC archive bytes and the decoder's input.
const uint32_t state = LoadGuestU32BE(memory, ctx + 84);
if (state == 0) {
DumpCodecInputBuffer(ctx, call_n, 4096);
}
}
void ac6PacBitFetcherProbe(PPCRegister& r3, PPCRegister& r4, PPCRegister& r5) {
if (!CodecTraceActive()) return;
auto* memory = REX_KERNEL_MEMORY();
if (!memory) return;
const uint32_t ctx = r3.u32;
const uint32_t cursor_ptr = r4.u32;
REXFS_INFO("[AC6 PAC CODEC] bit_fetch bits_req={} cursor_ptr=0x{:x} "
"cursor=0x{:x} bit_buf=0x{:016x} bit_count={}",
r5.u32, cursor_ptr,
LoadGuestU64BE(memory, cursor_ptr),
LoadGuestU64BE(memory, ctx + 72),
LoadGuestU32BE(memory, ctx + 80));
}
void ac6PacDynamicHeaderProbe(PPCRegister& r3, PPCRegister& r4) {
if (!CodecTraceActive()) return;
auto* memory = REX_KERNEL_MEMORY();
if (!memory) return;
const uint32_t ctx = r3.u32;
REXFS_INFO("[AC6 PAC CODEC] dynamic_header_entry ctx=0x{:08X} cursor_arg=0x{:x} "
"bit_buf=0x{:016x} bit_count={}",
ctx, r4.u32,
LoadGuestU64BE(memory, ctx + 72),
LoadGuestU32BE(memory, ctx + 80));
}
void ac6PacTreeBuilderProbe(PPCRegister& r3) {
if (!CodecTraceActive()) return;
auto* memory = REX_KERNEL_MEMORY();
if (!memory) return;
const uint32_t ctx = r3.u32;
const uint32_t arr_base = ctx + 108;
constexpr uint32_t kEntries = 19;
std::string lens;
lens.reserve(kEntries * 4);
for (uint32_t i = 0; i < kEntries; ++i) {
if (i) lens.push_back(',');
lens += std::to_string(LoadGuestU16BE(memory, arr_base + i * 2));
}
REXFS_INFO("[AC6 PAC CODEC] tree_builder_entry ctx=0x{:08X} cl_array=[{}]", ctx, lens);
}
void ac6PacBlockConsumerProbe(PPCRegister& r3) {
if (!CodecTraceActive()) return;
auto* memory = REX_KERNEL_MEMORY();
if (!memory) return;
const uint32_t ctx = r3.u32;
REXFS_INFO("[AC6 PAC CODEC] block_consumer_entry ctx=0x{:08X} state=0x{:x} "
"out_pos=0x{:x} bit_buf=0x{:016x} bit_count={}",
ctx,
LoadGuestU32BE(memory, ctx + 84),
LoadGuestU64BE(memory, ctx + 32),
LoadGuestU64BE(memory, ctx + 72),
LoadGuestU32BE(memory, ctx + 80));
}
+26
View File
@@ -45,3 +45,29 @@ void ac6PacDecoderDumpHook(PPCRegister& r4, PPCRegister& r10, PPCRegister& r11,
void Ac6DumpPacDecodedEntry(uint16_t entry_index, uint8_t codec_mode,
uint32_t compressed_size, uint32_t decompressed_size,
uint32_t source_offset, const uint8_t* host_data);
// ---------------------------------------------------------------------------
// Codec-internals tracing probes.
//
// Each probe is a mid-asm hook on a specific PPC function entry inside the
// mode-1 decoder pipeline. All probes are gated by env var
// AC6_TRACE_CODEC_INTERNALS=1 and rate-limited to the first 3 mode-1 decoder
// invocations so log volume stays manageable.
//
// Decoder pipeline (top-down):
// sub_822CF510 codec dispatcher -> ac6PacCodecDispatchProbe
// sub_822CF2F8 mode-1 entry -> ac6PacMode1EntryProbe (one-shot ROM dump)
// sub_822CCB50 bit fetcher -> ac6PacBitFetcherProbe
// sub_822CEAC0 dynamic header -> ac6PacDynamicHeaderProbe
// sub_822CDB38 tree builder -> ac6PacTreeBuilderProbe
// sub_822CD068 / sub_822CD758 -> ac6PacBlockConsumerProbe
//
// The bit-fetcher probe is the highest-value: it logs every byte the codec
// consumes, which lets us verify whether the .compressed.bin bytes are read
// verbatim or transformed before reaching the bit buffer.
void ac6PacCodecDispatchProbe(PPCRegister& r3);
void ac6PacMode1EntryProbe(PPCRegister& r3);
void ac6PacBitFetcherProbe(PPCRegister& r3, PPCRegister& r4, PPCRegister& r5);
void ac6PacDynamicHeaderProbe(PPCRegister& r3, PPCRegister& r4);
void ac6PacTreeBuilderProbe(PPCRegister& r3);
void ac6PacBlockConsumerProbe(PPCRegister& r3);
+2
View File
@@ -0,0 +1,2 @@
# Global owners
* @tomcl7
+1 -1
View File
@@ -3,7 +3,7 @@
cmake_minimum_required(VERSION 3.25)
project(ReXGlue
VERSION 0.7.1
VERSION 0.8.0
LANGUAGES C CXX
DESCRIPTION "Xbox 360 Recompilation SDK"
)
@@ -21,9 +21,9 @@ namespace util {
using DescriptorCpuGpuHandlePair =
std::pair<D3D12_CPU_DESCRIPTOR_HANDLE, D3D12_GPU_DESCRIPTOR_HANDLE>;
extern const D3D12_HEAP_PROPERTIES kHeapPropertiesDefault;
extern const D3D12_HEAP_PROPERTIES kHeapPropertiesUpload;
extern const D3D12_HEAP_PROPERTIES kHeapPropertiesReadback;
inline constexpr D3D12_HEAP_PROPERTIES kHeapPropertiesDefault = {D3D12_HEAP_TYPE_DEFAULT};
inline constexpr D3D12_HEAP_PROPERTIES kHeapPropertiesUpload = {D3D12_HEAP_TYPE_UPLOAD};
inline constexpr D3D12_HEAP_PROPERTIES kHeapPropertiesReadback = {D3D12_HEAP_TYPE_READBACK};
template <typename T>
bool ReleaseAndNull(T& object) {
+2 -1
View File
@@ -12,6 +12,7 @@
#pragma once
#include <rex/codegen/codegen_context.h>
#include <rex/codegen/progress_reporter.h>
#include <rex/result.h>
namespace rex::codegen {
@@ -31,6 +32,6 @@ namespace rex::codegen {
* @param ctx CodegenContext with binary and config loaded
* @return Success if graph is valid, error otherwise
*/
Result<void> Analyze(CodegenContext& ctx);
Result<void> Analyze(CodegenContext& ctx, ProgressReporter* reporter = nullptr);
} // namespace rex::codegen
+1 -1
View File
@@ -19,7 +19,7 @@
namespace rex::codegen {
// Forward declarations
class RecompilerConfig;
struct RecompilerConfig;
/**
* @brief CSR (Control/Status Register) state for FPU denormal handling.
+2 -7
View File
@@ -50,15 +50,10 @@ class CodegenPipeline {
*/
static Result<CodegenPipeline> Create(const std::filesystem::path& configPath);
/**
* Run the full pipeline: Analyze -> Recompile.
*
* @param force If true, generate output even with validation errors
* @return Success or error with description
*/
Result<void> Run(bool force = false);
Result<void> RunAnalyze();
Result<void> RunWrite(bool force = false);
/// Access context for CLI needs (output path, project name, etc.)
CodegenContext& context() { return *ctx_; }
const CodegenContext& context() const { return *ctx_; }
@@ -32,6 +32,18 @@ class CodegenWriter {
/// Run the full output pipeline: validate, clean old files, generate, flush.
bool write(bool force);
/**
* Basenames of files removed by the pre-emit cleanup sweep during write().
* Populated only after write() completes. Empty otherwise.
*/
const std::vector<std::string>& deletedFiles() const { return deletedFiles_; }
/**
* Basenames of files written to disk during write() (via FlushPendingWrites).
* Populated only after write() completes. Empty otherwise.
*/
const std::vector<std::string>& writtenFiles() const { return writtenFiles_; }
private:
CodegenContext& ctx_;
Runtime* runtime_;
@@ -39,6 +51,8 @@ class CodegenWriter {
std::string out;
size_t cppFileIndex = 0;
std::vector<std::pair<std::string, std::string>> pendingWrites;
std::vector<std::string> deletedFiles_;
std::vector<std::string> writtenFiles_;
template <class... Args>
void print(fmt::format_string<Args...> fmt, Args&&... args) {
+14
View File
@@ -13,11 +13,14 @@
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <toml++/toml.hpp>
#include <rex/codegen/function_graph.h> // For JumpTable
namespace rex::codegen {
@@ -95,6 +98,10 @@ struct RecompilerConfig {
uint32_t dataRegionThreshold = 16; ///< Consecutive invalid instructions to mark as data region
uint32_t largeFunctionThreshold = 1048576; ///< 1MB - warn if function exceeds this size
// Optional override for DLL module flag. If unset, the orchestrator infers
// from the module's position in the manifest (entrypoint = false, modules = true).
std::optional<bool> isDll;
// === Manual overrides ===
std::unordered_map<uint32_t, FunctionConfig> functions; ///< Function/chunk configuration
std::unordered_map<uint32_t, JumpTable> switchTables;
@@ -127,6 +134,13 @@ struct RecompilerConfig {
*/
bool Load(const std::string_view& configFilePath);
/**
* Load configuration from an in-memory TOML table (e.g. an inline binary
* entry inside a manifest). Includes referenced from the table resolve
* relative to `base_dir`. Same merge semantics as Load().
*/
bool LoadFromTable(const toml::table& tbl, const std::filesystem::path& base_dir);
/// Validation result containing warnings and errors.
struct ValidationResult {
bool valid = true; ///< true if no errors (warnings OK)
+2 -2
View File
@@ -94,8 +94,8 @@ class FunctionNode {
/// Emit C++ code for this function
/// Requires: state() == kSealed
/// For imports: emits PPC_IMPORT macro
/// For normal functions: emits PPC_FUNC with blocks and instructions
/// For imports: emits REX_IMPORT macro
/// For normal functions: emits REX_FUNC with blocks and instructions
std::string emitCpp(const EmitContext& ctx) const;
//=========================================================================
+70
View File
@@ -0,0 +1,70 @@
/**
* @file rex/codegen/manifest.h
* @brief Manifest TOML parser for multi-binary projects
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <rex/codegen/config.h>
namespace rex::codegen {
/**
* Codegen settings for a single binary inside a manifest. The entrypoint
* uses an empty `guestPath`; module entries set it to the canonicalized
* guest-visible path the host runtime resolves against.
*/
struct BinaryConfig {
RecompilerConfig recompiler;
std::string guestPath;
};
/**
* Canonicalize a module guest path: device-stripped, slashes/case normalized,
* with `<project>/assets/` stripped when a matching project name is given.
*/
std::string CanonicalizeModuleGuestPath(std::string_view path, std::string_view project_name = {});
/**
* Parsed manifest TOML. Construct via Load(); treat as read-only after.
*/
struct ManifestConfig {
std::string projectName;
std::optional<std::string> sdkVersion; ///< Last SDK that ran codegen on this project
std::optional<std::string> gameRoot; ///< Game asset root, relative to manifestDir.
///< Set by `rexglue init` to anchor DLL guest paths.
std::filesystem::path manifestDir; ///< Directory containing the manifest
BinaryConfig entrypoint; ///< Entrypoint codegen settings (inline)
std::vector<BinaryConfig> modules; ///< DLL module codegen settings (inline)
/**
* Load a manifest TOML file. Returns nullopt on parse failure.
*/
static std::optional<ManifestConfig> Load(const std::filesystem::path& path);
/**
* True when `path` parses as a manifest (i.e. has a `[project]` section).
*/
static bool IsManifest(const std::filesystem::path& path);
/**
* Insert or overwrite [project].sdk_version in the manifest file at `path`.
* Preserves the rest of the file's content. Returns false on parse or write
* failure.
*/
static bool WriteSdkVersionStamp(const std::filesystem::path& path, std::string_view version);
};
} // namespace rex::codegen
+7 -6
View File
@@ -14,26 +14,27 @@
#include <string>
#include <rex/codegen/codegen_context.h>
#include <rex/codegen/progress_reporter.h>
#include <rex/result.h>
namespace rex::codegen::phases {
/// Register PDATA, CONFIG, helpers, imports into FunctionGraph
VoidResult Register(CodegenContext& ctx);
VoidResult Register(CodegenContext& ctx, ProgressReporter* reporter = nullptr);
/// Scan binary for bl targets, thunks, bctr locations
VoidResult Scan(CodegenContext& ctx);
VoidResult Scan(CodegenContext& ctx, ProgressReporter* reporter = nullptr);
/// Discover function blocks from candidates
VoidResult Discover(CodegenContext& ctx);
VoidResult Discover(CodegenContext& ctx, ProgressReporter* reporter = nullptr);
/// Vacancy-based function expansion and sealing
VoidResult Merge(CodegenContext& ctx);
VoidResult Merge(CodegenContext& ctx, ProgressReporter* reporter = nullptr);
/// Find uncovered code regions and register them as functions
VoidResult GapFill(CodegenContext& ctx);
VoidResult GapFill(CodegenContext& ctx, ProgressReporter* reporter = nullptr);
/// Validate all call targets resolve
VoidResult Validate(CodegenContext& ctx);
VoidResult Validate(CodegenContext& ctx, ProgressReporter* reporter = nullptr);
} // namespace rex::codegen::phases
@@ -0,0 +1,49 @@
/**
* @file rex/codegen/progress_reporter.h
* @brief Abstract callback interface for codegen pipeline progress
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#pragma once
#include <chrono>
#include <cstddef>
#include <string_view>
namespace rex::codegen {
/**
* Optional progress callback invoked by the codegen pipeline at module
* and phase boundaries. CLI consumers implement this to drive a progress
* view; library-internal callers (tests, headless callers) can pass
* nullptr to opt out.
*
* All methods are called from the thread that drove the pipeline; no
* cross-thread synchronization is implied.
*/
class ProgressReporter {
public:
virtual ~ProgressReporter() = default;
/** A new module's analysis+write cycle is starting. `index` is 0-based. */
virtual void moduleStarted(std::string_view name, std::size_t index, std::size_t total) = 0;
/** A named phase within the current module is starting. */
virtual void phaseChanged(std::string_view name) = 0;
/** The current module finished successfully. */
virtual void moduleFinished(std::chrono::milliseconds elapsed) = 0;
/** Project-level (non-module) emit phase started, e.g. "module_registry". */
virtual void projectPhaseStarted(std::string_view name) = 0;
/** Project-level emit phase finished. */
virtual void projectPhaseFinished() = 0;
};
} // namespace rex::codegen
@@ -0,0 +1,53 @@
/**
* @file rex/codegen/project_recompiler.h
* @brief Project-level recompiler driving manifest-based multi-binary codegen
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#pragma once
#include <string>
#include <vector>
#include <rex/codegen/manifest.h>
#include <rex/codegen/progress_reporter.h>
#include <rex/result.h>
namespace rex::codegen {
struct ProjectRecompilerOptions {
std::vector<std::string> targets; // empty = all
bool force = false;
bool enableExceptionHandlers = false;
ProgressReporter* reporter = nullptr;
};
class ProjectRecompiler {
public:
explicit ProjectRecompiler(ManifestConfig manifest);
Result<void> Run(const ProjectRecompilerOptions& opts);
/**
* Aggregated basenames of files removed across all modules during the most
* recent Run() call. Empty until Run() completes successfully.
*/
const std::vector<std::string>& deletedFiles() const { return deletedFiles_; }
/**
* Aggregated basenames of files written across all modules during the most
* recent Run() call. Empty until Run() completes successfully.
*/
const std::vector<std::string>& writtenFiles() const { return writtenFiles_; }
private:
ManifestConfig manifest_;
std::vector<std::string> deletedFiles_;
std::vector<std::string> writtenFiles_;
};
} // namespace rex::codegen
+262 -153
View File
@@ -12,7 +12,7 @@
* REXCVAR_DEFINE_STRING(my_string, "default", "Category", "A string setting");
* @endcode
*
* Available types: BOOL, INT32, INT64, UINT32, UINT64, DOUBLE, STRING
* Available types: BOOL, INT32, INT64, UINT32, UINT64, DOUBLE, STRING, COMMAND
*
* @section cvar_declaring Declaring CVars (for use in other files)
*
@@ -114,7 +114,7 @@ void SaveConfig(const std::filesystem::path& config_path);
// Flag Registry
//=============================================================================
enum class FlagType { Boolean, Int32, Int64, Uint32, Uint64, Double, String };
enum class FlagType { Boolean, Int32, Int64, Uint32, Uint64, Double, String, Command };
// Lifecycle: when can this flag be modified?
enum class Lifecycle {
@@ -141,6 +141,7 @@ struct FlagEntry {
std::string description;
std::function<bool(std::string_view)> setter;
std::function<std::string()> getter;
std::function<void()> command_callback;
Lifecycle lifecycle = Lifecycle::kHotReload;
Constraints constraints;
std::string default_value;
@@ -148,9 +149,45 @@ struct FlagEntry {
};
std::vector<FlagEntry>& GetRegistry();
void RegisterFlag(FlagEntry entry);
/**
* Returns the registered entry's index, or nullopt if the name was already
* registered (logged at ERROR).
*/
std::optional<size_t> RegisterFlag(FlagEntry entry);
/**
* Removes a flag from the registry. Used by `FlagRegistrar`'s destructor so
* that DLL unload tears down the lambdas captured in each FlagEntry.
*/
void UnregisterFlag(std::string_view name);
bool SetFlagByName(std::string_view name, std::string_view value);
std::string GetFlagByName(std::string_view name);
// Typed registry query. Cross-DLL access path that does not require linking
// the DLL where the cvar is defined. Slower than REXCVAR_GET (string parse +
// hash lookup), so prefer REXCVAR_GET when the defining DLL is already on the
// link line. Returns a value-initialized T when the cvar is missing or its
// stored string fails to parse.
template <typename T>
T Query(std::string_view name);
template <>
bool Query<bool>(std::string_view name);
template <>
int32_t Query<int32_t>(std::string_view name);
template <>
int64_t Query<int64_t>(std::string_view name);
template <>
uint32_t Query<uint32_t>(std::string_view name);
template <>
uint64_t Query<uint64_t>(std::string_view name);
template <>
double Query<double>(std::string_view name);
template <>
std::string Query<std::string>(std::string_view name);
std::vector<std::string> ListFlags();
std::vector<std::string> ListFlagsByCategory(std::string_view category);
std::vector<std::string> ListFlagsByLifecycle(Lifecycle lc);
@@ -175,54 +212,70 @@ void RegisterChangeCallback(std::string_view name, ChangeCallback callback);
/// Unregister all callbacks for a specific CVAR
void UnregisterChangeCallbacks(std::string_view name);
/**
* RAII handle for a registered flag. Destructor unregisters by name; on
* duplicate-name registration `owned_name_` is empty so chain methods and
* the destructor become no-ops and the original owner's entry is untouched.
*/
struct FlagRegistrar {
FlagEntry* entry_ptr = nullptr;
std::string owned_name_; // empty when registration was rejected
explicit FlagRegistrar(FlagEntry e) {
// Register immediately and store pointer to the registered entry for chaining
RegisterFlag(std::move(e));
auto& registry = GetRegistry();
entry_ptr = &registry.back();
std::string name = e.name;
if (RegisterFlag(std::move(e)).has_value()) {
owned_name_ = std::move(name);
}
}
// Move constructor for copy-initialization in macros
FlagRegistrar(FlagRegistrar&& other) noexcept : entry_ptr(other.entry_ptr) {
other.entry_ptr = nullptr;
FlagRegistrar(FlagRegistrar&& other) noexcept : owned_name_(std::move(other.owned_name_)) {
other.owned_name_.clear();
}
// Chain methods are rvalue-ref-qualified to work with temporaries
~FlagRegistrar() {
if (!owned_name_.empty()) {
UnregisterFlag(owned_name_);
}
}
// Chain methods mutate the registered entry by name lookup.
FlagRegistrar&& range(double min_val, double max_val) && {
entry_ptr->constraints.min = min_val;
entry_ptr->constraints.max = max_val;
apply_([=](FlagEntry& entry) {
entry.constraints.min = min_val;
entry.constraints.max = max_val;
});
return std::move(*this);
}
FlagRegistrar&& allowed(std::initializer_list<std::string> values) && {
entry_ptr->constraints.allowed_values = values;
std::vector<std::string> vals(values);
apply_([vals = std::move(vals)](FlagEntry& entry) { entry.constraints.allowed_values = vals; });
return std::move(*this);
}
FlagRegistrar&& lifecycle(Lifecycle lc) && {
entry_ptr->lifecycle = lc;
apply_([=](FlagEntry& entry) { entry.lifecycle = lc; });
return std::move(*this);
}
FlagRegistrar&& debug_only() && {
entry_ptr->is_debug_only = true;
apply_([](FlagEntry& entry) { entry.is_debug_only = true; });
return std::move(*this);
}
FlagRegistrar&& validator(std::function<bool(std::string_view)> fn) && {
entry_ptr->constraints.custom_validator = std::move(fn);
apply_([fn = std::move(fn)](FlagEntry& entry) mutable {
entry.constraints.custom_validator = std::move(fn);
});
return std::move(*this);
}
~FlagRegistrar() = default;
// Non-copyable (prevent double registration)
FlagRegistrar(const FlagRegistrar&) = delete;
FlagRegistrar& operator=(const FlagRegistrar&) = delete;
FlagRegistrar& operator=(FlagRegistrar&&) = delete;
private:
void apply_(std::function<void(FlagEntry&)> fn);
};
inline bool ParseDouble(std::string_view s, double& out) {
@@ -240,158 +293,214 @@ inline bool ParseDouble(std::string_view s, double& out) {
// CVar Macros
//=============================================================================
// Declare a cvar (use in files that need access to a cvar defined elsewhere)
#define REXCVAR_DECLARE(type, name) extern type FLAGS_##name
// Declare a cvar (use in headers and TUs that need to read it).
// The accessor function returns a reference to the cvar's storage. Storage
// lives as a static-local inside whichever DLL contains the matching
// REXCVAR_DEFINE_*. Cross-DLL access goes through the import lib.
#define REXCVAR_DECLARE(type, name) type& FLAGS_##name##_storage_()
// Get a cvar value
#define REXCVAR_GET(name) (FLAGS_##name)
#define REXCVAR_GET(name) (FLAGS_##name##_storage_())
// Set a cvar value
#define REXCVAR_SET(name, value) (FLAGS_##name = (value))
#define REXCVAR_SET(name, value) (FLAGS_##name##_storage_() = (value))
// Cross-module typed query that goes through the cvar registry by name.
// Use this when the defining DLL is not on the consumer's link line (e.g.,
// across one-way subsystem dependencies where adding the reverse link would
// create a cycle). Slower than REXCVAR_GET; prefer REXCVAR_GET when possible.
#define REXCVAR_QUERY(type, name) (::rex::cvar::Query<type>(#name))
// Define cvars (use in one .cpp file per cvar)
// The FlagRegistrar registers the flag in its destructor, allowing method chaining.
#define REXCVAR_DEFINE_BOOL(name, default_val, category, desc) \
bool FLAGS_##name = (default_val); \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Boolean, \
category, \
desc, \
[](std::string_view v) { \
bool val = (v == "true" || v == "1" || v == "yes"); \
FLAGS_##name = val; \
return true; \
}, \
[]() { return FLAGS_##name ? "true" : "false"; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
(default_val) ? "true" : "false", \
#define REXCVAR_DEFINE_BOOL(name, default_val, category, desc) \
bool& FLAGS_##name##_storage_() { \
static bool storage = (default_val); \
return storage; \
} \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Boolean, \
category, \
desc, \
[](std::string_view v) { \
bool val = (v == "true" || v == "1" || v == "yes"); \
FLAGS_##name##_storage_() = val; \
return true; \
}, \
[]() { return FLAGS_##name##_storage_() ? "true" : "false"; }, \
[]() { return; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
(default_val) ? "true" : "false", \
false})
#define REXCVAR_DEFINE_INT32(name, default_val, category, desc) \
int32_t FLAGS_##name = (default_val); \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Int32, \
category, \
desc, \
[](std::string_view v) { \
int32_t val = 0; \
auto [ptr, ec] = \
std::from_chars(v.data(), v.data() + v.size(), val); \
if (ec != std::errc()) \
return false; \
FLAGS_##name = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name); }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
#define REXCVAR_DEFINE_INT32(name, default_val, category, desc) \
int32_t& FLAGS_##name##_storage_() { \
static int32_t storage = (default_val); \
return storage; \
} \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Int32, \
category, \
desc, \
[](std::string_view v) { \
int32_t val = 0; \
auto [ptr, ec] = \
std::from_chars(v.data(), v.data() + v.size(), val); \
if (ec != std::errc()) \
return false; \
FLAGS_##name##_storage_() = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
[]() { return; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
false})
#define REXCVAR_DEFINE_INT64(name, default_val, category, desc) \
int64_t FLAGS_##name = (default_val); \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Int64, \
category, \
desc, \
[](std::string_view v) { \
int64_t val = 0; \
auto [ptr, ec] = \
std::from_chars(v.data(), v.data() + v.size(), val); \
if (ec != std::errc()) \
return false; \
FLAGS_##name = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name); }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
#define REXCVAR_DEFINE_INT64(name, default_val, category, desc) \
int64_t& FLAGS_##name##_storage_() { \
static int64_t storage = (default_val); \
return storage; \
} \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Int64, \
category, \
desc, \
[](std::string_view v) { \
int64_t val = 0; \
auto [ptr, ec] = \
std::from_chars(v.data(), v.data() + v.size(), val); \
if (ec != std::errc()) \
return false; \
FLAGS_##name##_storage_() = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
[]() { return; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
false})
#define REXCVAR_DEFINE_UINT32(name, default_val, category, desc) \
uint32_t FLAGS_##name = (default_val); \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Uint32, \
category, \
desc, \
[](std::string_view v) { \
uint32_t val = 0; \
auto [ptr, ec] = \
std::from_chars(v.data(), v.data() + v.size(), val); \
if (ec != std::errc()) \
return false; \
FLAGS_##name = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name); }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
#define REXCVAR_DEFINE_UINT32(name, default_val, category, desc) \
uint32_t& FLAGS_##name##_storage_() { \
static uint32_t storage = (default_val); \
return storage; \
} \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Uint32, \
category, \
desc, \
[](std::string_view v) { \
uint32_t val = 0; \
auto [ptr, ec] = \
std::from_chars(v.data(), v.data() + v.size(), val); \
if (ec != std::errc()) \
return false; \
FLAGS_##name##_storage_() = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
[]() { return; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
false})
#define REXCVAR_DEFINE_UINT64(name, default_val, category, desc) \
uint64_t FLAGS_##name = (default_val); \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Uint64, \
category, \
desc, \
[](std::string_view v) { \
uint64_t val = 0; \
auto [ptr, ec] = \
std::from_chars(v.data(), v.data() + v.size(), val); \
if (ec != std::errc()) \
return false; \
FLAGS_##name = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name); }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
#define REXCVAR_DEFINE_UINT64(name, default_val, category, desc) \
uint64_t& FLAGS_##name##_storage_() { \
static uint64_t storage = (default_val); \
return storage; \
} \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Uint64, \
category, \
desc, \
[](std::string_view v) { \
uint64_t val = 0; \
auto [ptr, ec] = \
std::from_chars(v.data(), v.data() + v.size(), val); \
if (ec != std::errc()) \
return false; \
FLAGS_##name##_storage_() = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
[]() { return; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
false})
#define REXCVAR_DEFINE_DOUBLE(name, default_val, category, desc) \
double FLAGS_##name = (default_val); \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Double, \
category, \
desc, \
[](std::string_view v) { \
double val = 0; \
if (!::rex::cvar::ParseDouble(v, val)) \
return false; \
FLAGS_##name = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name); }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
#define REXCVAR_DEFINE_DOUBLE(name, default_val, category, desc) \
double& FLAGS_##name##_storage_() { \
static double storage = (default_val); \
return storage; \
} \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Double, \
category, \
desc, \
[](std::string_view v) { \
double val = 0; \
if (!::rex::cvar::ParseDouble(v, val)) \
return false; \
FLAGS_##name##_storage_() = val; \
return true; \
}, \
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
[]() { return; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
std::to_string(default_val), \
false})
#define REXCVAR_DEFINE_STRING(name, default_val, category, desc) \
std::string FLAGS_##name = (default_val); \
static auto _cvar_reg_##name = ::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::String, \
category, \
desc, \
[](std::string_view v) { \
FLAGS_##name = std::string(v); \
return true; \
}, \
[]() { return FLAGS_##name; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
default_val, \
false})
#define REXCVAR_DEFINE_STRING(name, default_val, category, desc) \
std::string& FLAGS_##name##_storage_() { \
static std::string storage = (default_val); \
return storage; \
} \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::String, \
category, \
desc, \
[](std::string_view v) { \
FLAGS_##name##_storage_() = std::string(v); \
return true; \
}, \
[]() { return FLAGS_##name##_storage_(); }, \
[]() { return; }, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
default_val, \
false})
#define REXCVAR_DEFINE_COMMAND(name, callback, category, desc) \
std::function<void()>& FLAGS_##name##_storage_() { \
static std::function<void()> storage = (callback); \
return storage; \
} \
static auto _cvar_reg_##name = \
::rex::cvar::FlagRegistrar({#name, \
::rex::cvar::FlagType::Command, \
category, \
desc, \
[](std::string_view) { return false; }, \
[]() { return "<command>"; }, \
callback, \
::rex::cvar::Lifecycle::kHotReload, \
{}, \
"<command>", \
false})
namespace rex::cvar {
namespace testing {
@@ -27,11 +27,11 @@ class StfsContainerFile : public File {
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 {
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; }
X_STATUS SetLength(size_t /*length*/) override { return X_STATUS_ACCESS_DENIED; }
private:
StfsContainerEntry* entry_;
@@ -29,7 +29,7 @@ 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};
struct tm tm = {};
// 80 is the difference between 1980 (FAT) and 1900 (tm);
tm.tm_year = ((0xFE00 & date) >> 9) + 80;
tm.tm_mon = ((0x01E0 & date) >> 5) - 1;
@@ -28,11 +28,8 @@ class D3D12GraphicsSystem : public GraphicsSystem {
std::string name() const override;
X_STATUS Setup(runtime::FunctionDispatcher* function_dispatcher,
system::KernelState* kernel_state, ui::WindowedAppContext* app_context,
bool with_presentation) override;
protected:
void CreateProvider(bool with_presentation) override;
std::unique_ptr<CommandProcessor> CreateCommandProcessor() override;
};
+8 -8
View File
@@ -235,10 +235,10 @@ struct ControlFlowExecInstruction {
uint32_t count_ : 3;
uint32_t is_yield_ : 1;
uint32_t sequence_ : 12;
uint32_t vc_hi_ : 4; // Vertex cache?
[[maybe_unused]] uint32_t vc_hi_ : 4; // Vertex cache?
// Word 1: (16 bits)
uint32_t vc_lo_ : 2;
[[maybe_unused]] uint32_t vc_lo_ : 2;
uint32_t : 7;
// According to the description of Conditional_Execute_Predicates_No_Stall in
// the IPR2015-00325 sequencer specification, the sequencer's control flow
@@ -280,10 +280,10 @@ struct ControlFlowCondExecInstruction {
uint32_t count_ : 3;
uint32_t is_yield_ : 1;
uint32_t sequence_ : 12;
uint32_t vc_hi_ : 4; // Vertex cache?
[[maybe_unused]] uint32_t vc_hi_ : 4; // Vertex cache?
// Word 1: (16 bits)
uint32_t vc_lo_ : 2;
[[maybe_unused]] uint32_t vc_lo_ : 2;
uint32_t bool_address_ : 8;
uint32_t condition_ : 1;
AddressingMode address_mode_ : 1;
@@ -314,10 +314,10 @@ struct ControlFlowCondExecPredInstruction {
uint32_t count_ : 3;
uint32_t is_yield_ : 1;
uint32_t sequence_ : 12;
uint32_t vc_hi_ : 4; // Vertex cache?
[[maybe_unused]] uint32_t vc_hi_ : 4; // Vertex cache?
// Word 1: (16 bits)
uint32_t vc_lo_ : 2;
[[maybe_unused]] uint32_t vc_lo_ : 2;
uint32_t : 7;
uint32_t is_predicate_clean_ : 1;
uint32_t condition_ : 1;
@@ -457,7 +457,7 @@ struct ControlFlowCondJmpInstruction {
// Word 1: (16 bits)
uint32_t : 1;
uint32_t direction_ : 1;
[[maybe_unused]] uint32_t direction_ : 1;
uint32_t bool_address_ : 8;
uint32_t condition_ : 1;
AddressingMode address_mode_ : 1;
@@ -480,7 +480,7 @@ struct ControlFlowAllocInstruction {
// Word 1: (16 bits)
uint32_t : 8;
uint32_t is_unserialized_ : 1;
uint32_t : 1; // is_unserialized_
AllocType alloc_type_ : 2;
uint32_t : 1;
ControlFlowOpcode opcode_ : 4;
@@ -58,10 +58,11 @@ class GraphicsSystem : public system::IGraphicsSystem {
::rex::ui::GraphicsProvider* provider() const override { return provider_.get(); }
::rex::ui::Presenter* presenter() const override { return presenter_.get(); }
virtual X_STATUS Setup(runtime::FunctionDispatcher* function_dispatcher,
system::KernelState* kernel_state,
::rex::ui::WindowedAppContext* app_context, bool with_presentation);
virtual void Shutdown();
X_STATUS SetupPresentation(::rex::ui::WindowedAppContext* app_context) override;
X_STATUS SetupGuestGpu(runtime::FunctionDispatcher* function_dispatcher,
system::KernelState* kernel_state) override;
bool has_presentation() const override { return presenter_ != nullptr; }
void Shutdown() override;
// May be called from any thread any number of times, even during recovery
// from a device loss.
@@ -107,6 +108,10 @@ class GraphicsSystem : public system::IGraphicsSystem {
protected:
GraphicsSystem();
// Backends build their provider here. Called lazily from either setup
// entry point; with_presentation is false only on headless guest-GPU paths.
virtual void CreateProvider(bool with_presentation) = 0;
virtual std::unique_ptr<CommandProcessor> CreateCommandProcessor() = 0;
static uint32_t ReadRegisterThunk(void* ppc_context, GraphicsSystem* gs, uint32_t addr);
@@ -123,6 +128,7 @@ class GraphicsSystem : public system::IGraphicsSystem {
system::KernelState* kernel_state_ = nullptr;
::rex::ui::WindowedAppContext* app_context_ = nullptr;
std::unique_ptr<::rex::ui::GraphicsProvider> provider_;
bool provider_supports_presentation_ = false;
uint32_t interrupt_callback_ = 0;
uint32_t interrupt_callback_data_ = 0;
@@ -50,7 +50,7 @@ class DxbcShader : public Shader {
const std::vector<TextureBinding>& GetTextureBindingsAfterTranslation() const {
return texture_bindings_;
}
const uint32_t GetUsedTextureMaskAfterTranslation() const { return used_texture_mask_; }
uint32_t GetUsedTextureMaskAfterTranslation() const { return used_texture_mask_; }
static constexpr uint32_t kMaxSamplerBindingIndexBits =
DxbcShaderTranslator::kMaxSamplerBindingIndexBits;
@@ -33,9 +33,9 @@ class ShaderInterpreter {
class ExportSink {
public:
virtual ~ExportSink() = default;
virtual void AllocExport(ucode::AllocType type, uint32_t size) {}
virtual void Export(ucode::ExportRegister export_register, const float* value,
uint32_t value_mask) {}
virtual void AllocExport(ucode::AllocType /*type*/, uint32_t /*size*/) {}
virtual void Export(ucode::ExportRegister /*export_register*/, const float* /*value*/,
uint32_t /*value_mask*/) {}
};
void SetTraceWriter(TraceWriter* new_trace_writer) { trace_writer_ = new_trace_writer; }
@@ -1005,7 +1005,7 @@ class Shader {
std::string ucode_disassembly_;
std::vector<VertexBinding> vertex_bindings_;
std::vector<TextureBinding> texture_bindings_;
ConstantRegisterMap constant_register_map_ = {0};
ConstantRegisterMap constant_register_map_ = {};
std::set<uint32_t> label_addresses_;
uint32_t cf_pair_index_bound_ = 0;
uint32_t register_static_address_bound_ = 0;
@@ -29,13 +29,13 @@ class ShaderTranslator {
virtual ~ShaderTranslator();
virtual uint64_t GetDefaultVertexShaderModification(
uint32_t dynamic_addressable_register_count,
Shader::HostVertexShaderType host_vertex_shader_type =
uint32_t /*dynamic_addressable_register_count*/,
Shader::HostVertexShaderType /*host_vertex_shader_type*/ =
Shader::HostVertexShaderType::kVertex) const {
return 0;
}
virtual uint64_t GetDefaultPixelShaderModification(
uint32_t dynamic_addressable_register_count) const {
uint32_t /*dynamic_addressable_register_count*/) const {
return 0;
}
@@ -84,51 +84,52 @@ class ShaderTranslator {
// Pre-process a control-flow instruction before anything else.
virtual void PreProcessControlFlowInstructions(
std::vector<ucode::ControlFlowInstruction> instrs) {}
std::vector<ucode::ControlFlowInstruction> /*instrs*/) {}
// Handles translation for control flow label addresses.
// This is triggered once for each label required (due to control flow
// operations) before any of the instructions within the target exec.
virtual void ProcessLabel(uint32_t cf_index) {}
virtual void ProcessLabel(uint32_t /*cf_index*/) {}
// Handles translation for control flow nop instructions.
virtual void ProcessControlFlowNopInstruction(uint32_t cf_index) {}
virtual void ProcessControlFlowNopInstruction(uint32_t /*cf_index*/) {}
// Handles the start of a control flow instruction at the given address.
virtual void ProcessControlFlowInstructionBegin(uint32_t cf_index) {}
virtual void ProcessControlFlowInstructionBegin(uint32_t /*cf_index*/) {}
// Handles the end of a control flow instruction that began at the given
// address.
virtual void ProcessControlFlowInstructionEnd(uint32_t cf_index) {}
virtual void ProcessControlFlowInstructionEnd(uint32_t /*cf_index*/) {}
// Handles translation for control flow exec instructions prior to their
// contained ALU/fetch instructions.
virtual void ProcessExecInstructionBegin(const ParsedExecInstruction& instr) {}
virtual void ProcessExecInstructionBegin(const ParsedExecInstruction& /*instr*/) {}
// Handles translation for control flow exec instructions after their
// contained ALU/fetch instructions.
virtual void ProcessExecInstructionEnd(const ParsedExecInstruction& instr) {}
virtual void ProcessExecInstructionEnd(const ParsedExecInstruction& /*instr*/) {}
// Handles translation for loop start instructions.
virtual void ProcessLoopStartInstruction(const ParsedLoopStartInstruction& instr) {}
virtual void ProcessLoopStartInstruction(const ParsedLoopStartInstruction& /*instr*/) {}
// Handles translation for loop end instructions.
virtual void ProcessLoopEndInstruction(const ParsedLoopEndInstruction& instr) {}
virtual void ProcessLoopEndInstruction(const ParsedLoopEndInstruction& /*instr*/) {}
// Handles translation for function call instructions.
virtual void ProcessCallInstruction(const ParsedCallInstruction& instr) {}
virtual void ProcessCallInstruction(const ParsedCallInstruction& /*instr*/) {}
// Handles translation for function return instructions.
virtual void ProcessReturnInstruction(const ParsedReturnInstruction& instr) {}
virtual void ProcessReturnInstruction(const ParsedReturnInstruction& /*instr*/) {}
// Handles translation for jump instructions.
virtual void ProcessJumpInstruction(const ParsedJumpInstruction& instr) {}
virtual void ProcessJumpInstruction(const ParsedJumpInstruction& /*instr*/) {}
// Handles translation for alloc instructions. Memory exports for eM#
// indicated by export_eM must be performed, regardless of the alloc type.
virtual void ProcessAllocInstruction(const ParsedAllocInstruction& instr, uint8_t export_eM) {}
virtual void ProcessAllocInstruction(const ParsedAllocInstruction& /*instr*/,
uint8_t /*export_eM*/) {}
// Handles translation for vertex fetch instructions.
virtual void ProcessVertexFetchInstruction(const ParsedVertexFetchInstruction& instr) {}
virtual void ProcessVertexFetchInstruction(const ParsedVertexFetchInstruction& /*instr*/) {}
// Handles translation for texture fetch instructions.
virtual void ProcessTextureFetchInstruction(const ParsedTextureFetchInstruction& instr) {}
virtual void ProcessTextureFetchInstruction(const ParsedTextureFetchInstruction& /*instr*/) {}
// Handles translation for ALU instructions.
// memexport_eM_potentially_written_before needs to be handled by `kill`
// instruction to make sure memory exports for the eM# writes earlier in
// previous execs and the current exec are done before the invocation becomes
// inactive.
virtual void ProcessAluInstruction(const ParsedAluInstruction& instr,
uint8_t memexport_eM_potentially_written_before) {}
virtual void ProcessAluInstruction(const ParsedAluInstruction& /*instr*/,
uint8_t /*memexport_eM_potentially_written_before*/) {}
private:
void TranslateControlFlowInstruction(const ucode::ControlFlowInstruction& cf);
@@ -83,8 +83,9 @@ class TextureCache {
void MarkRangeAsResolved(uint32_t start_unscaled, uint32_t length_unscaled);
// Ensures the memory backing the range in the scaled resolve address space is
// allocated and returns whether it is.
virtual bool EnsureScaledResolveMemoryCommitted(uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2 = 0) {
virtual bool EnsureScaledResolveMemoryCommitted(uint32_t /*start_unscaled*/,
uint32_t /*length_unscaled*/,
uint32_t /*length_scaled_alignment_log2*/ = 0) {
return false;
}
@@ -484,11 +485,11 @@ class TextureCache {
// Whether the signed version of the texture has a different representation on
// the host than its unsigned version (for example, if it's a fixed-point
// texture emulated with a larger host pixel format).
virtual bool IsSignedVersionSeparateForFormat(TextureKey key) const { return false; }
virtual bool IsSignedVersionSeparateForFormat(TextureKey /*key*/) const { return false; }
// Parameters like whether the texture is tiled and its dimensions are checked
// externally, the implementation should take only format-related parameters
// such as the format itself and the signedness into account.
virtual bool IsScaledResolveSupportedForFormat(TextureKey key) const { return false; }
virtual bool IsScaledResolveSupportedForFormat(TextureKey /*key*/) const { return false; }
// For formats with less than 4 components, implementations normally should
// replicate the last component into the non-existent ones, similar to what is
// done for unused components of operands in shaders by Microsoft's Xbox 360
@@ -542,7 +543,7 @@ class TextureCache {
}
// Called when something in a texture binding is changed for the
// implementation to update the internal dependencies of the binding.
virtual void UpdateTextureBindingsImpl(uint32_t fetch_constant_mask) {}
virtual void UpdateTextureBindingsImpl(uint32_t /*fetch_constant_mask*/) {}
private:
struct PendingTextureLoad {
@@ -26,9 +26,8 @@ class VulkanGraphicsSystem : public GraphicsSystem {
std::string name() const override;
X_STATUS Setup(runtime::FunctionDispatcher* function_dispatcher,
system::KernelState* kernel_state, ui::WindowedAppContext* app_context,
bool with_presentation) override;
protected:
void CreateProvider(bool with_presentation) override;
private:
std::unique_ptr<CommandProcessor> CreateCommandProcessor() override;
+8
View File
@@ -184,6 +184,14 @@ void RemoveSink(spdlog::sink_ptr sink);
*/
void RemoveSink(LogCategoryId category, spdlog::sink_ptr sink);
/**
* Replace the global console sink on every registered logger with `sink`.
* Pass nullptr to remove the console sink without replacement.
*
* @param sink New console sink, or nullptr to remove.
*/
void ReplaceConsoleSink(spdlog::sink_ptr sink);
/**
* Update the format pattern on the stdout console sink.
*
+4
View File
@@ -45,6 +45,10 @@ struct PPCContext;
// Function signature for recompiled PPC functions
using PPCFunc = void(PPCContext& ctx, uint8_t* base);
namespace rex::runtime {
PPCFunc* ResolveIndirectFunction(uint32_t guest_address);
} // namespace rex::runtime
//=============================================================================
// Compiler-Specific Intrinsics
//=============================================================================
+10
View File
@@ -15,8 +15,17 @@
struct PPCFuncMapping;
namespace rex::system {
class KernelState;
}
namespace rex {
/**
* Callback for registering recompiled modules with KernelState (multi-binary projects).
*/
using RegisterModulesFunc = void (*)(system::KernelState*);
/// PPC image layout passed from the generated config header into ReXApp.
struct PPCImageInfo {
uint32_t code_base;
@@ -25,6 +34,7 @@ struct PPCImageInfo {
uint32_t image_size;
const PPCFuncMapping* func_mappings;
bool rexcrt_heap = false; ///< Set by codegen when [rexcrt] has heap functions
RegisterModulesFunc register_modules = nullptr; ///< Set by codegen for multi-binary projects
};
} // namespace rex
+1
View File
@@ -34,6 +34,7 @@ enum class ErrorCategory {
Validation, // Validation errors (e.g., unresolved functions)
NotFound, // Resource not found
NotImplemented, // Feature not implemented
UserAbort, // User declined an interactive prompt
};
//=============================================================================
@@ -15,7 +15,12 @@
#pragma once
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <rex/memory.h>
#include <rex/memory/mapped_memory.h>
@@ -30,8 +35,27 @@ namespace rex::runtime {
class ExportResolver;
class ThreadState;
class FunctionDispatcher {
/**
* Narrow registration interface used by generated DLLs.
*/
class IModuleRegistrar {
public:
/**
* Returns false (and logs) if guest_address is outside every module range.
*/
virtual bool SetFunction(uint32_t guest_address, ::PPCFunc* func) = 0;
protected:
~IModuleRegistrar() = default;
};
class FunctionDispatcher : public IModuleRegistrar {
public:
/**
* Callback type for module registration functions.
*/
using RegisterFn = void (*)(IModuleRegistrar*);
FunctionDispatcher(memory::Memory* memory, ExportResolver* export_resolver);
~FunctionDispatcher();
@@ -42,33 +66,86 @@ class FunctionDispatcher {
uint64_t ExecuteInterrupt(ThreadState* thread_state, uint32_t address, uint64_t args[],
size_t arg_count);
// rexglue function table management
// Shared thunk region size per module.
static constexpr uint32_t kThunkReserveSize = 0x10000; // 64KB
// rexglue function table management (per-module table at IMAGE_BASE + IMAGE_SIZE)
// Set is_entrypoint=true exactly once for the host-loaded entrypoint so
// AllocateThunk(caller_address=0) can route to its pool.
bool InitializeFunctionTable(uint32_t code_base, uint32_t code_size, uint32_t image_base,
uint32_t image_size);
void SetFunction(uint32_t guest_address, ::PPCFunc* func);
uint32_t image_size, bool is_entrypoint = false);
bool SetFunction(uint32_t guest_address, ::PPCFunc* func) override;
::PPCFunc* GetFunction(uint32_t guest_address);
bool HasFunctionTable() const { return function_table_initialized_; }
uint32_t AllocateThunk(::PPCFunc* func);
bool HasAnyFunctionTable() const { return !module_tables_.empty(); }
/**
* caller_address must be inside a registered module, or 0 to mean "host-
* initiated, route to the entrypoint pool".
*/
uint32_t AllocateThunk(::PPCFunc* func, uint32_t caller_address);
/**
* Returns the `code_base` of the module containing `guest_address`,
* or 0 if no module covers that address.
*/
uint32_t FindCallerModuleBase(uint32_t guest_address);
/**
* Register a module while recording guest addresses written via SetFunction.
* `code_base` must equal the value previously passed to InitializeFunctionTable
* for the same module.
*/
void RegisterModule(const std::string& module_id, uint32_t code_base, RegisterFn register_func);
/**
* Unregister `module_id`: clears its function-table entries, releases its
* thunk pool, and removes its per-module function table. Returns the
* cleared thunk-pool range `[lo, hi)` for external cache invalidation, or
* nullopt if the module was not registered.
*/
std::optional<std::pair<uint32_t, uint32_t>> UnregisterModule(const std::string& module_id);
private:
bool Execute(ThreadState* thread_state, uint32_t address);
struct ModuleTableInfo {
uint32_t code_base;
uint32_t code_size;
uint32_t image_base;
uint32_t image_size;
uint32_t next_thunk_address;
uint32_t thunk_limit;
};
ModuleTableInfo* FindModuleByAddress(uint32_t guest_address);
memory::Memory* memory_ = nullptr;
ExportResolver* export_resolver_ = nullptr;
rex::thread::global_critical_region global_critical_region_;
// rexglue function table
// Host-side function lookup.
std::unordered_map<uint32_t, ::PPCFunc*> function_table_;
uint32_t code_base_ = 0;
uint32_t code_size_ = 0;
uint32_t image_base_ = 0;
uint32_t image_size_ = 0;
bool function_table_initialized_ = false;
// Runtime thunk allocation (for XexGetProcedureAddress)
uint32_t next_thunk_address_ = 0;
uint32_t thunk_limit_ = 0;
// Per-module function table metadata.
std::vector<ModuleTableInfo> module_tables_;
// code_base of the entrypoint module, or 0 if not yet registered.
uint32_t entrypoint_code_base_ = 0;
// Module recording for RegisterModule/UnregisterModule.
bool recording_ = false;
std::vector<uint32_t> recording_addresses_;
struct ModuleRegistration {
uint32_t code_base;
std::vector<uint32_t> addresses;
};
// Recorded state per module, keyed by module_id.
std::unordered_map<std::string, ModuleRegistration> module_addresses_;
// Protects dispatcher metadata during module registration and callback dispatch.
mutable std::recursive_mutex dispatch_mutex_;
};
} // namespace rex::runtime
+30
View File
@@ -0,0 +1,30 @@
/**
* @file rex/system/guest_path.h
* @brief Guest path normalization for Xbox 360 VFS paths
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#pragma once
#include <string>
#include <string_view>
namespace rex::system {
/**
* Normalize a guest path to the canonical form used as a key for runtime
* lookups: device prefix stripped, separators canonicalized to forward
* slashes, redundant `..` segments resolved, leading slashes removed,
* and ASCII lowercased. Codegen-side canonicalization
* (rex::codegen::CanonicalizeModuleGuestPath) layers a project-scoped prefix
* strip on top of this; both must agree on this base form for runtime
* lookups to find the registered module.
*/
std::string NormalizeGuestPath(std::string_view path);
} // namespace rex::system
@@ -47,9 +47,34 @@ struct GraphicsSwapSubmission {
class IGraphicsSystem {
public:
virtual ~IGraphicsSystem() = default;
virtual X_STATUS Setup(runtime::FunctionDispatcher* function_dispatcher,
KernelState* kernel_state, ui::WindowedAppContext* app_context,
bool with_presentation) = 0;
// Build the provider + presenter. Safe to call standalone (without a
// Runtime) to stand up a window + ImGui for an installer. Idempotent.
// Must be called before SetupGuestGpu if presentation is desired: some
// backends (e.g. Vulkan) bake swapchain support into the provider, and
// a headless provider from SetupGuestGpu cannot be upgraded in place.
virtual X_STATUS SetupPresentation(ui::WindowedAppContext* app_context) = 0;
// Wire the GPU into the guest address space: MMIO, command processor,
// vsync worker. Needs the Runtime's dispatcher + kernel state. If
// SetupPresentation has not been called, a headless provider is built.
virtual X_STATUS SetupGuestGpu(runtime::FunctionDispatcher* function_dispatcher,
KernelState* kernel_state) = 0;
virtual bool has_presentation() const = 0;
// One-shot convenience for callers that don't care about the split.
X_STATUS Setup(runtime::FunctionDispatcher* function_dispatcher, KernelState* kernel_state,
ui::WindowedAppContext* app_context, bool with_presentation) {
if (with_presentation && !has_presentation()) {
X_STATUS status = SetupPresentation(app_context);
if (XFAILED(status)) {
return status;
}
}
return SetupGuestGpu(function_dispatcher, kernel_state);
}
virtual void Shutdown() = 0;
virtual ui::GraphicsProvider* provider() const = 0;
virtual ui::Presenter* presenter() const = 0;
+28 -5
View File
@@ -1,7 +1,13 @@
#pragma once
/**
* 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 <unordered_map>
@@ -21,9 +27,14 @@ class KernelModule : public XModule {
const std::string& path() const override { return path_; }
const std::string& name() const override { return name_; }
uint32_t GetProcAddressByOrdinal(uint16_t ordinal) override;
uint32_t GetProcAddressByOrdinal(uint16_t ordinal, uint32_t caller_address = 0) override;
uint32_t GetProcAddressByName(const std::string_view name) override;
/**
* Erase any cached thunks whose guest address falls in [lo, hi).
*/
void InvalidateThunkCacheInRange(uint32_t lo, uint32_t hi);
protected:
rex::runtime::ExportResolver* export_resolver_;
@@ -32,8 +43,20 @@ class KernelModule : public XModule {
rex::thread::global_critical_region global_critical_region_;
// Cache of ordinal -> thunk guest address (for XexGetProcedureAddress)
std::unordered_map<uint16_t, uint32_t> thunk_cache_;
// Cache of (caller_module_base, ordinal) -> thunk guest address.
struct ThunkKey {
uint32_t caller_module_base;
uint16_t ordinal;
bool operator==(const ThunkKey& other) const {
return caller_module_base == other.caller_module_base && ordinal == other.ordinal;
}
};
struct ThunkKeyHash {
std::size_t operator()(const ThunkKey& k) const noexcept {
return std::hash<uint64_t>{}((uint64_t(k.caller_module_base) << 16) | k.ordinal);
}
};
std::unordered_map<ThunkKey, uint32_t, ThunkKeyHash> thunk_cache_;
};
} // namespace rex::system
+1 -1
View File
@@ -51,7 +51,7 @@ class MMIOHandler {
const void* host_to_guest_virtual_context,
AccessViolationCallback access_violation_callback,
void* access_violation_callback_context);
static MMIOHandler* global_handler() { return global_handler_; }
static MMIOHandler* global_handler();
bool RegisterRange(uint32_t virtual_address, uint32_t mask, uint32_t size, void* context,
MMIOReadCallback read_callback, MMIOWriteCallback write_callback);
@@ -0,0 +1,37 @@
/**
* @file rex/system/shared_library.h
* @brief Platform-agnostic shared library loader
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#pragma once
#include <string>
namespace rex::system {
class SharedLibrary {
public:
SharedLibrary() = default;
~SharedLibrary();
SharedLibrary(const SharedLibrary&) = delete;
SharedLibrary& operator=(const SharedLibrary&) = delete;
SharedLibrary(SharedLibrary&& other) noexcept;
SharedLibrary& operator=(SharedLibrary&& other) noexcept;
bool Load(const std::string& name);
void* GetSymbol(const char* name);
void Close();
bool is_loaded() const { return handle_ != nullptr; }
private:
void* handle_ = nullptr;
};
} // namespace rex::system
+1 -1
View File
@@ -60,7 +60,7 @@ class UserModule : public XModule {
X_STATUS LoadFromMemory(const void* addr, const size_t length);
X_STATUS Unload();
uint32_t GetProcAddressByOrdinal(uint16_t ordinal) override;
uint32_t GetProcAddressByOrdinal(uint16_t ordinal, uint32_t caller_address = 0) override;
uint32_t GetProcAddressByName(const std::string_view name) override;
X_STATUS GetSection(const std::string_view name, uint32_t* out_section_data,
uint32_t* out_section_size) override;
+54 -27
View File
@@ -1,7 +1,13 @@
#pragma once
/**
* 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 <cstdint>
@@ -16,8 +22,35 @@
#include <rex/system/mmio_handler.h>
#include <rex/thread/mutex.h>
namespace rex::stream {
class ByteStream;
} // namespace rex::stream
namespace rex::memory::detail {
/// Compensates for Windows 64KB allocation granularity on the 0xE0 physical heap.
/// The backing file maps the 0xE0 heap at a 0x1000-byte offset, but MapViewOfFileEx
/// rounds down to 64KB boundaries. Linux mmap handles 4KB offsets natively.
constexpr u32 PhysicalHostOffset([[maybe_unused]] u32 guest_addr) noexcept {
#if REX_PLATFORM_WIN32
return (guest_addr >= 0xE0000000u) ? 0x1000u : 0u;
#else
return 0u;
#endif
}
} // namespace rex::memory::detail
namespace rex::memory {
/// Lightweight guest-to-host pointer translation using the memory base.
/// For hooks and kernel code operating with the base pointer directly.
/// Same raw arithmetic as recompiled code; no Memory* or heap lookup needed.
template <typename T = u8*>
inline T GuestPtr(u8* base, u32 guest_address) noexcept {
return reinterpret_cast<T>(base + guest_address + detail::PhysicalHostOffset(guest_address));
}
class Memory;
enum SystemHeapFlag : uint32_t {
@@ -182,6 +215,8 @@ class BaseHeap {
// range.
rex::memory::PageAccess QueryRangeAccess(uint32_t low_address, uint32_t high_address);
bool Save(stream::ByteStream* stream);
bool Restore(stream::ByteStream* stream);
void Reset();
@@ -466,32 +501,21 @@ class Memory {
// Dumps a map of all allocated memory to the log.
void DumpMap();
bool Save(stream::ByteStream* stream);
bool Restore(stream::ByteStream* stream);
//==========================================================================
// Recompiled Code Function Table API
//==========================================================================
// These methods support static recompilation by providing a function
// dispatch table stored in guest memory at IMAGE_BASE + IMAGE_SIZE.
// The table is indexed by (guest_addr - code_base) * 2, allowing 8-byte
// pointers for each 4-byte-aligned guest address.
// Initialize the function table region for recompiled code dispatch.
// Must be called before SetFunction/GetFunction.
// Returns false if memory allocation fails.
// Per-module function dispatch table at IMAGE_BASE + IMAGE_SIZE, indexed by
// (guest_addr - code_base) * 2. Internally locked: callers may invoke from
// any thread.
bool InitializeFunctionTable(uint32_t code_base, uint32_t code_size, uint32_t image_base,
uint32_t image_size);
// Register a host function for a guest address.
// The function will be called when recompiled code does an indirect call
// to this address via PPC_LOOKUP_FUNC/PPC_CALL_INDIRECT_FUNC.
void SetFunction(uint32_t guest_address, PPCFunc* host_function);
// Get the registered host function for a guest address.
// Returns nullptr if no function is registered.
PPCFunc* GetFunction(uint32_t guest_address) const;
// Check if the function table has been initialized.
bool HasFunctionTable() const { return function_table_base_ != 0; }
bool DestroyFunctionTable(uint32_t code_base);
// Returns false if guest_address is outside every registered module range.
bool SetFunction(uint32_t guest_address, PPCFunc* host_function);
bool HasAnyFunctionTable() const;
private:
int MapViews(uint8_t* mapping_base);
@@ -511,11 +535,14 @@ class Memory {
uint8_t* virtual_membase_ = nullptr;
uint8_t* physical_membase_ = nullptr;
// Recompiled code function table configuration
uint32_t function_table_base_ = 0; // Guest address of function table (IMAGE_BASE + IMAGE_SIZE)
uint32_t function_code_base_ = 0; // CODE_BASE for offset calculation
uint32_t function_code_size_ = 0; // CODE_SIZE for bounds checking
uint32_t function_thunk_reserve_ = 0; // Extra space reserved for runtime thunks
struct FunctionTableEntry {
uint32_t table_base;
uint32_t code_base;
uint32_t code_size;
uint32_t thunk_reserve;
};
std::vector<FunctionTableEntry> function_tables_;
mutable std::mutex function_tables_mutex_;
rex::memory::FileMappingHandle mapping_ = rex::memory::kFileMappingHandleInvalid;
uint8_t* mapping_base_ = nullptr;
+1 -1
View File
@@ -64,7 +64,7 @@ class XModule : public XObject {
rex::runtime::Module* processor_module() const { return processor_module_; }
uint32_t hmodule_ptr() const { return hmodule_ptr_; }
virtual uint32_t GetProcAddressByOrdinal(uint16_t ordinal) = 0;
virtual uint32_t GetProcAddressByOrdinal(uint16_t ordinal, uint32_t caller_address = 0) = 0;
virtual uint32_t GetProcAddressByName(const std::string_view name) = 0;
virtual X_STATUS GetSection(const std::string_view name, uint32_t* out_section_data,
uint32_t* out_section_size);
+5 -1
View File
@@ -14,6 +14,7 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <vector>
@@ -24,6 +25,7 @@
#include <rex/ui/window_listener.h>
struct ImDrawData;
struct ImFontAtlas;
struct ImGuiContext;
struct ImGuiIO;
enum ImGuiKey : int;
@@ -36,7 +38,8 @@ class Window;
class ImGuiDrawer : public WindowInputListener, public UIDrawer {
public:
ImGuiDrawer(Window* window, size_t z_order);
using FontSetupCallback = std::function<void(ImFontAtlas*)>;
ImGuiDrawer(Window* window, size_t z_order, FontSetupCallback font_setup = nullptr);
~ImGuiDrawer();
ImGuiIO& GetIO();
@@ -85,6 +88,7 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
Window* window_;
size_t z_order_;
FontSetupCallback font_setup_;
ImGuiContext* internal_state_ = nullptr;
@@ -0,0 +1,38 @@
//=============================================================================
// Indirect Call Dispatch
//
// REX_LOOKUP_FUNC indexes into the per-module dispatch table at
// IMAGE_BASE + IMAGE_SIZE. REX_CALL_INDIRECT_FUNC bounds-checks the target
// against the caller's [REX_CODE_BASE, REX_CODE_BASE + REX_CODE_SIZE +
// REX_THUNK_RESERVE_SIZE) range; out-of-bounds targets and in-bounds
// unregistered slots fall back to rex::runtime::ResolveIndirectFunction,
// which queries the global FunctionDispatcher across every loaded module.
//=============================================================================
#ifdef REXGLUE_ENABLE_PROFILING
#include <rex/perf/counter.h>
#define REX_PROFILE_INDIRECT_DISPATCH() PROFILE_FUNCTION_DISPATCHED()
#else
#define REX_PROFILE_INDIRECT_DISPATCH()
#endif
#define REX_LOOKUP_FUNC(x, y) \
(*(PPCFunc**)(x + REX_IMAGE_BASE + REX_IMAGE_SIZE + (u64(u32(y) - REX_CODE_BASE) * 2)))
#define REX_CALL_INDIRECT_FUNC(x) \
do { \
REX_PROFILE_INDIRECT_DISPATCH(); \
uint32_t rex_indirect_target_ = (uint32_t)(x); \
PPCFunc* rex_indirect_func_; \
if ((uint32_t)(rex_indirect_target_ - REX_CODE_BASE) < \
REX_CODE_SIZE + REX_THUNK_RESERVE_SIZE) [[likely]] { \
rex_indirect_func_ = REX_LOOKUP_FUNC(base, rex_indirect_target_); \
} else { \
rex_indirect_func_ = nullptr; \
} \
if (!rex_indirect_func_) [[unlikely]] { \
ctx.last_indirect_target = rex_indirect_target_; \
rex_indirect_func_ = rex::runtime::ResolveIndirectFunction(rex_indirect_target_); \
} \
rex_indirect_func_(ctx, base); \
} while (0)
@@ -0,0 +1,32 @@
# Auto-generated by rexglue codegen - DO NOT EDIT
# DLL module shared library targets
{% for mod in dll_modules %}
# DLL module: {{ mod.target_name }}
if(EXISTS "{{ cmake_var("CMAKE_CURRENT_SOURCE_DIR") }}/{{ mod.output_dir }}/sources.cmake")
set(_REXGLUE_RESTORE_GENERATED_SOURCES FALSE)
if(DEFINED GENERATED_SOURCES)
set(_REXGLUE_PREVIOUS_GENERATED_SOURCES {{ cmake_var("GENERATED_SOURCES") }})
set(_REXGLUE_RESTORE_GENERATED_SOURCES TRUE)
endif()
include({{ mod.output_dir }}/sources.cmake)
set(DLL_SOURCES_{{ mod.target_name }} {{ cmake_var("GENERATED_SOURCES") }})
if(_REXGLUE_RESTORE_GENERATED_SOURCES)
set(GENERATED_SOURCES {{ cmake_var("_REXGLUE_PREVIOUS_GENERATED_SOURCES") }})
else()
unset(GENERATED_SOURCES)
endif()
unset(_REXGLUE_PREVIOUS_GENERATED_SOURCES)
unset(_REXGLUE_RESTORE_GENERATED_SOURCES)
add_library({{ mod.lib_name }} SHARED {{ cmake_var("DLL_SOURCES_" + mod.target_name) }})
target_include_directories({{ mod.lib_name }} PRIVATE
{{ cmake_var("CMAKE_CURRENT_SOURCE_DIR") }}
{{ cmake_var("CMAKE_CURRENT_SOURCE_DIR") }}/{{ mod.output_dir }}
)
target_link_libraries({{ mod.lib_name }} PRIVATE rex::runtime)
set_target_properties({{ mod.lib_name }} PROPERTIES
CXX_VISIBILITY_PRESET hidden
)
rexglue_configure_module_target({{ mod.lib_name }} HOST {{ cmake_var("REXGLUE_HOST_TARGET") }})
endif()
{% endfor %}
@@ -0,0 +1,14 @@
//=============================================================================
// ReXGlue Generated - {{ project }} Module Registry
//=============================================================================
#include <rex/system/kernel_state.h>
void RegisterRecompiledModules(rex::system::KernelState* kernel_state) {
{% for mod in dll_modules %}
kernel_state->RegisterRecompiledModule(
"{{ mod.pe_name }}",
"{{ mod.guest_path }}",
"{{ mod.shared_lib_name }}");
{% endfor %}
}
@@ -0,0 +1,21 @@
//=============================================================================
// ReXGlue Generated - {{ project }} Function Registration
//=============================================================================
#include "{{ project }}_init.h"
#include <rex/system/function_dispatcher.h>
{% if is_dll %}
#ifdef _WIN32
#define REX_MODULE_EXPORT __declspec(dllexport)
#else
#define REX_MODULE_EXPORT __attribute__((visibility("default")))
#endif
extern "C" REX_MODULE_EXPORT
void ReXModule_Register(rex::runtime::IModuleRegistrar* registrar) {
{% else %}
void {{ project }}_RegisterFunctions(rex::runtime::IModuleRegistrar* registrar) {
{% endif %}
{% for fn in functions %}{% if not fn.below_code_base or fn.is_import %} registrar->SetFunction({{ fn.address }}, {{ fn.name }});
{% endif %}{% endfor %}}
@@ -7,7 +7,9 @@
set(GENERATED_SOURCES
{{ cmake_var("CMAKE_CURRENT_LIST_DIR") }}/{{ project }}_config.cpp
{{ cmake_var("CMAKE_CURRENT_LIST_DIR") }}/{{ project }}_init.cpp
{% for file in recomp_files %} {{ cmake_var("CMAKE_CURRENT_LIST_DIR") }}/{{ file }}
{{ cmake_var("CMAKE_CURRENT_LIST_DIR") }}/{{ project }}_register.cpp
{% if has_dll_modules and not is_dll %} {{ cmake_var("CMAKE_CURRENT_LIST_DIR") }}/module_registry.cpp
{% endif %}{% for file in recomp_files %} {{ cmake_var("CMAKE_CURRENT_LIST_DIR") }}/{{ file }}
{% endfor %})
{% if has_icon %}
# Windows application icon resource extracted from the Xbox 360 executable.
+1 -1
View File
@@ -24,6 +24,7 @@ set(CODEGEN_CORE_SOURCES
phase_merge.cpp
phase_validate.cpp
template_registry.cpp
manifest.cpp
)
# PPC architecture sources (merged from rexarch)
@@ -85,4 +86,3 @@ target_link_libraries(rexcodegen
xxHash::xxhash
simde
)
+24 -12
View File
@@ -23,27 +23,33 @@
namespace rex::codegen {
Result<void> Analyze(CodegenContext& ctx) {
REXCODEGEN_INFO("Analyze: starting analysis...");
Result<void> Analyze(CodegenContext& ctx, ProgressReporter* reporter) {
REXCODEGEN_TRACE("Analyze: starting analysis...");
ctx.initDecoded();
REXCODEGEN_INFO("Analyze: decoded {} instructions across {} code regions",
ctx.decoded().instructionCount(), ctx.decoded().codeRegions().size());
REXCODEGEN_TRACE("Analyze: decoded {} instructions across {} code regions",
ctx.decoded().instructionCount(), ctx.decoded().codeRegions().size());
// 1. Register entry points (imports, helpers, config, pdata)
auto regResult = phases::Register(ctx);
if (reporter)
reporter->phaseChanged("Register");
auto regResult = phases::Register(ctx, reporter);
if (!regResult) {
return regResult;
}
// 2. Scan binary into code/data regions
auto scanResult = phases::Scan(ctx);
if (reporter)
reporter->phaseChanged("Scan");
auto scanResult = phases::Scan(ctx, reporter);
if (!scanResult) {
return scanResult;
}
// 3. Discover function blocks iteratively (includes vtable scan)
auto discoverResult = phases::Discover(ctx);
if (reporter)
reporter->phaseChanged("Discover");
auto discoverResult = phases::Discover(ctx, reporter);
if (!discoverResult) {
return discoverResult;
}
@@ -53,25 +59,31 @@ Result<void> Analyze(CodegenContext& ctx) {
// functionPointerScan(ctx);
// 4. Gap fill uncovered regions + discover blocks for gap-filled functions + cleanup
auto gapFillResult = phases::GapFill(ctx);
if (reporter)
reporter->phaseChanged("GapFill");
auto gapFillResult = phases::GapFill(ctx, reporter);
if (!gapFillResult) {
return gapFillResult;
}
// 5. Merge: resolve jumps and seal functions
auto mergeResult = phases::Merge(ctx);
if (reporter)
reporter->phaseChanged("Merge");
auto mergeResult = phases::Merge(ctx, reporter);
if (!mergeResult) {
return mergeResult;
}
// 6. Validate
auto validateResult = phases::Validate(ctx);
if (reporter)
reporter->phaseChanged("Validate");
auto validateResult = phases::Validate(ctx, reporter);
if (!validateResult) {
return validateResult;
}
REXCODEGEN_INFO("Analyze: complete - {} functions ready for code generation",
ctx.graph.functionCount());
REXCODEGEN_TRACE("Analyze: complete - {} functions ready for code generation",
ctx.graph.functionCount());
return Ok();
}
+38 -7
View File
@@ -1292,15 +1292,46 @@ bool build_vpkd3d128(BuilderContext& ctx) {
case 5: // float16_4
{
// Pack 4 elements into 64 bits (4 x 16-bit floats)
// Guest element 0 goes to highest 16-bit position, element 3 to lowest
// Output u16 index = (3-i) + 2*shift for element i
if (ctx.insn.operands[3] != 2 || ctx.insn.operands[4] > 2)
REXCODEGEN_WARN("Unexpected float16_4 pack instruction at {:X}", ctx.base);
// NOTE: These combinations come from game traces (heuristic handling), not the official spec.
// The spec only defines the encoding, not the exact semantics of y (mask) and z (shift) here.
// Anyone reading this later should know it may need extending if new combos turn up in other
// games. Combinations observed so far: mask=2, shift=0 → write u16[3..0], zero u16[4..7]
// mask=2, shift=2 → write u16[7..4], zero u16[3..0] (first pass)
// mask=3, shift=0 → write u16[3..0] without zeroing (second pass, preserves the upper half)
uint32_t mask = ctx.insn.operands[3];
uint32_t shift = ctx.insn.operands[4];
// Guard fix: The shift bound was too loose (shift=3 would cause dstIdx to reach 9, an OOB
// store). We also explicitly reject shift == 1 since the clear block below only handles 0
// and 2. Furthermore, we explicitly warn on unexpected mask values (e.g., 0, 1) to avoid
// silent fallthroughs and silently generating miscompiled code.
if ((shift != 0 && shift != 2) || (mask != 2 && mask != 3)) {
REXCODEGEN_WARN("Unexpected float16_4 pack instruction at {:X} (mask={}, shift={})",
ctx.base, mask, shift);
// Emits a debug trap in the generated code to catch this at runtime.
ctx.println("\t__builtin_debugtrap();");
return true;
}
// mask=2: before writing, clear the half that will NOT be written.
// Shift is guaranteed to be 0 or 2 at this point due to the guard above.
if (mask == 2) {
// Optimization: Emit a single u64 write instead of two u32 writes.
// shift=0 → clears upper half u64[1]
// shift=2 → clears lower half u64[0]
size_t clearU64Start = (shift == 0) ? 1 : 0;
ctx.println("\t{}.u64[{}] = 0;", ctx.v(ctx.insn.operands[0]), clearU64Start);
}
// Invariant: dstIdx must stay under 8 (valid u16 lanes are 0..7).
// Capping the shift to 0 or 2 in the guard check above ensures this is safe:
// it restricts the max dstIdx to (3 - 0) + (2 * 2) = 7.
// Do not widen the shift bounds without adjusting this logic.
for (size_t i = 0; i < 4; i++) {
size_t srcIdx = 3 - i; // Guest element i is at host array index 3-i
size_t dstIdx = (3 - i) + (2 * ctx.insn.operands[4]); // Output also reversed
size_t srcIdx = 3 - i;
size_t dstIdx = (3 - i) + (2 * shift);
ctx.println("\t{}.u32 = ({}.u32[{}]&0x7FFFFFFF);", ctx.temp(), ctx.v(ctx.insn.operands[1]),
srcIdx);
ctx.println(
+10 -2
View File
@@ -77,14 +77,22 @@ Result<CodegenPipeline> CodegenPipeline::Create(const std::filesystem::path& con
}
Result<void> CodegenPipeline::Run(bool force) {
// Phase 1: Analyze (builds and validates function graph)
auto result = RunAnalyze();
if (!result)
return result;
return RunWrite(force);
}
Result<void> CodegenPipeline::RunAnalyze() {
auto analyzeResult = Analyze(*ctx_);
if (!analyzeResult) {
REXLOG_ERROR("Analysis failed: {}", analyzeResult.error().message);
return analyzeResult;
}
return Ok();
}
// Phase 2: Generate C++ output
Result<void> CodegenPipeline::RunWrite(bool force) {
CodegenWriter writer(*ctx_, runtime_.get());
if (!writer.write(force))
return Err(ErrorCategory::Validation, "Code generation failed.");
-4
View File
@@ -33,7 +33,3 @@ REXCVAR_DECLARE(uint32_t, max_seh_scope_entries);
REXCVAR_DECLARE(uint32_t, backward_scan_limit);
REXCVAR_DECLARE(uint32_t, max_jump_table_entries);
REXCVAR_DECLARE(uint32_t, max_blocks_per_function);
// Codegen (moved from main.cpp)
REXCVAR_DECLARE(bool, force);
REXCVAR_DECLARE(bool, enable_exception_handlers);
+57 -46
View File
@@ -122,6 +122,9 @@ void ApplyToml(const toml::table& toml, RecompilerConfig& cfg, const std::string
hasBool("non_volatile_as_local"), "non_volatile_as_local");
MergeBool(cfg.generateExceptionHandlers, toml["generate_exception_handlers"].value_or(false),
hasBool("generate_exception_handlers"), "generate_exception_handlers");
if (hasBool("is_dll")) {
cfg.isDll = toml["is_dll"].value_or(false);
}
// Integer scalars (only override if present)
if (auto v = toml["longjmp_address"].value<int64_t>()) {
@@ -385,20 +388,18 @@ void ApplyToml(const toml::table& toml, RecompilerConfig& cfg, const std::string
}
}
// ---------------------------------------------------------------------------
// Recursive include loader
// ---------------------------------------------------------------------------
bool ApplyTableWithIncludes(const toml::table& tbl, const std::filesystem::path& base_dir,
RecompilerConfig& cfg, std::set<std::string>& visited, uint32_t depth,
const std::string& description);
bool LoadRecursive(const std::filesystem::path& filePath, RecompilerConfig& cfg,
std::set<std::string>& visited, uint32_t depth) {
// Depth check
if (depth > kMaxIncludeDepth) {
REXCODEGEN_ERROR("[config] include depth exceeds maximum ({}) at: {}", kMaxIncludeDepth,
filePath.string());
return false;
}
// Canonical path for cycle detection
std::error_code ec;
auto canonical = std::filesystem::canonical(filePath, ec);
if (ec) {
@@ -406,15 +407,12 @@ bool LoadRecursive(const std::filesystem::path& filePath, RecompilerConfig& cfg,
return false;
}
std::string canonicalStr = canonical.string();
// Circular include detection
if (visited.contains(canonicalStr)) {
REXCODEGEN_ERROR("[config] circular include detected: {}", canonicalStr);
return false;
}
visited.insert(canonicalStr);
// Parse this file
toml::table toml;
try {
toml = toml::parse_file(canonicalStr);
@@ -422,20 +420,22 @@ bool LoadRecursive(const std::filesystem::path& filePath, RecompilerConfig& cfg,
REXCODEGEN_ERROR("Failed to parse config file '{}': {}", canonicalStr, e.what());
return false;
}
REXCODEGEN_DEBUG("[config] loaded: {}", filePath.filename().string());
return ApplyTableWithIncludes(toml, canonical.parent_path(), cfg, visited, depth,
filePath.filename().string());
}
// Process includes first (depth-first), before applying this file's values.
// This means this file's values override anything set by included files.
auto parentDir = canonical.parent_path();
if (auto* includesArray = toml["includes"].as_array()) {
bool ApplyTableWithIncludes(const toml::table& tbl, const std::filesystem::path& base_dir,
RecompilerConfig& cfg, std::set<std::string>& visited, uint32_t depth,
const std::string& description) {
// Process includes first (depth-first), so this table's own values win.
if (auto* includesArray = tbl["includes"].as_array()) {
for (const auto& elem : *includesArray) {
if (auto includePath = elem.value<std::string>()) {
auto resolved = parentDir / *includePath;
auto resolved = base_dir / *includePath;
if (!std::filesystem::exists(resolved)) {
REXCODEGEN_ERROR("[config] included file not found: {} (resolved from {})", *includePath,
canonicalStr);
description);
return false;
}
if (!LoadRecursive(resolved, cfg, visited, depth + 1)) {
@@ -444,10 +444,7 @@ bool LoadRecursive(const std::filesystem::path& filePath, RecompilerConfig& cfg,
}
}
}
// Apply this file's config (after includes, so this file wins)
ApplyToml(toml, cfg, filePath.filename().string());
ApplyToml(tbl, cfg, description);
return true;
}
@@ -457,48 +454,62 @@ bool LoadRecursive(const std::filesystem::path& filePath, RecompilerConfig& cfg,
// Public API
// ---------------------------------------------------------------------------
bool RecompilerConfig::Load(const std::string_view& configFilePath) {
std::set<std::string> visited;
std::filesystem::path path(configFilePath);
namespace {
if (!LoadRecursive(path, *this, visited, 0)) {
return false;
}
// Post-load summary logging
if (!functions.empty()) {
bool FinalizeConfig(RecompilerConfig& cfg) {
if (!cfg.functions.empty()) {
size_t chunks_count = 0;
for (const auto& [addr, cfg] : functions) {
if (cfg.isChunk())
for (const auto& [addr, fc] : cfg.functions) {
if (fc.isChunk())
chunks_count++;
}
REXCODEGEN_INFO("Loaded {} function configs ({} standalone, {} chunks)", functions.size(),
functions.size() - chunks_count, chunks_count);
REXCODEGEN_TRACE("Loaded {} function configs ({} standalone, {} chunks)", cfg.functions.size(),
cfg.functions.size() - chunks_count, chunks_count);
}
if (!cfg.exceptionHandlerFuncHints.empty()) {
std::sort(cfg.exceptionHandlerFuncHints.begin(), cfg.exceptionHandlerFuncHints.end());
cfg.exceptionHandlerFuncHints.erase(
std::unique(cfg.exceptionHandlerFuncHints.begin(), cfg.exceptionHandlerFuncHints.end()),
cfg.exceptionHandlerFuncHints.end());
}
// Deduplicate exceptionHandlerFuncHints (push_back from multiple files)
if (!exceptionHandlerFuncHints.empty()) {
std::sort(exceptionHandlerFuncHints.begin(), exceptionHandlerFuncHints.end());
exceptionHandlerFuncHints.erase(
std::unique(exceptionHandlerFuncHints.begin(), exceptionHandlerFuncHints.end()),
exceptionHandlerFuncHints.end());
}
// Required field check
if (filePath.empty()) {
bool ok = true;
if (cfg.filePath.empty()) {
REXCODEGEN_ERROR("Missing required field: file_path");
ok = false;
}
// Validate assembled config
auto result = Validate();
auto result = cfg.Validate();
for (const auto& warning : result.warnings) {
REXCODEGEN_WARN("[config] {}", warning);
}
for (const auto& error : result.errors) {
REXCODEGEN_ERROR("[config] {}", error);
}
if (!result.valid) {
ok = false;
}
return ok;
}
return true;
} // namespace
bool RecompilerConfig::Load(const std::string_view& configFilePath) {
std::set<std::string> visited;
std::filesystem::path path(configFilePath);
if (!LoadRecursive(path, *this, visited, 0)) {
return false;
}
return FinalizeConfig(*this);
}
bool RecompilerConfig::LoadFromTable(const toml::table& tbl,
const std::filesystem::path& base_dir) {
std::set<std::string> visited;
if (!ApplyTableWithIncludes(tbl, base_dir, *this, visited, 0, "<inline>")) {
return false;
}
return FinalizeConfig(*this);
}
RecompilerConfig::ValidationResult RecompilerConfig::Validate() const {
+6 -6
View File
@@ -566,8 +566,8 @@ std::string FunctionNode::emitCpp(const EmitContext& ctx) const {
for (auto label : activeJt->targets) {
labels.emplace(label);
}
REXCODEGEN_INFO("Late-detected jump table at 0x{:08X} with {} entries", blockBase,
activeJt->targets.size());
REXCODEGEN_TRACE("Late-detected jump table at 0x{:08X} with {} entries", blockBase,
activeJt->targets.size());
}
}
}
@@ -1065,10 +1065,10 @@ size_t FunctionGraph::sealAllReady() {
sealed++;
} else {
couldNotSeal++;
REXCODEGEN_WARN("FunctionGraph::sealAllReady: 0x{:08X} ({}) cannot seal ({} unresolved)",
base, node->name(), node->unresolvedJumps().size());
REXCODEGEN_DEBUG("FunctionGraph::sealAllReady: 0x{:08X} ({}) cannot seal ({} unresolved)",
base, node->name(), node->unresolvedJumps().size());
for (const auto& jump : node->unresolvedJumps()) {
REXCODEGEN_WARN(" 0x{:08X} -> 0x{:08X}", jump.site, jump.target);
REXCODEGEN_DEBUG(" 0x{:08X} -> 0x{:08X}", jump.site, jump.target);
}
}
}
@@ -1111,7 +1111,7 @@ void FunctionGraph::sealAll() {
throw std::runtime_error(msg);
}
REXCODEGEN_INFO("FunctionGraph::sealAll: all {} functions sealed", functions_.size());
REXCODEGEN_TRACE("FunctionGraph::sealAll: all {} functions sealed", functions_.size());
}
//=============================================================================
+268
View File
@@ -0,0 +1,268 @@
/**
* @file codegen/manifest.cpp
* @brief Manifest TOML parser for multi-binary projects
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <rex/codegen/manifest.h>
#include <algorithm>
#include <cctype>
#include <fstream>
#include <optional>
#include <string>
#include <vector>
#include <toml++/toml.hpp>
#include <rex/logging.h>
#include <rex/system/guest_path.h>
namespace rex::codegen {
std::string CanonicalizeModuleGuestPath(std::string_view path, std::string_view project_name) {
std::string guest_path = rex::system::NormalizeGuestPath(path);
if (!project_name.empty()) {
std::string lower_project(project_name);
std::transform(lower_project.begin(), lower_project.end(), lower_project.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
const std::string project_assets_prefix = lower_project + "/assets/";
if (guest_path.rfind(project_assets_prefix, 0) == 0) {
guest_path.erase(0, project_assets_prefix.size());
}
}
return guest_path;
}
namespace {
bool IsValidProjectName(std::string_view name) {
if (name.empty())
return false;
if (!(std::isalpha(static_cast<unsigned char>(name[0])) || name[0] == '_')) {
return false;
}
return std::all_of(name.begin(), name.end(),
[](unsigned char c) { return std::isalnum(c) || c == '_'; });
}
/**
* Pull file_path / out_directory_path / project_name onto the recompiler
* config so downstream consumers can rely on them. Other fields (codegen
* flags, sub-tables, includes) come from RecompilerConfig::LoadFromTable.
*/
bool LoadBinaryConfig(const toml::table& tbl, const std::filesystem::path& base_dir,
std::string_view project_name, BinaryConfig& out) {
out.recompiler = RecompilerConfig{};
out.recompiler.projectName = std::string(project_name);
if (!out.recompiler.LoadFromTable(tbl, base_dir)) {
return false;
}
return true;
}
} // namespace
std::optional<ManifestConfig> ManifestConfig::Load(const std::filesystem::path& path) {
toml::table tbl;
try {
tbl = toml::parse_file(path.string());
} catch (const toml::parse_error& err) {
REXLOG_ERROR("Failed to parse manifest {}: {}", path.string(), err.what());
return std::nullopt;
}
ManifestConfig manifest;
manifest.manifestDir = path.parent_path();
auto* project = tbl["project"].as_table();
if (!project) {
REXLOG_ERROR("Manifest missing [project] section: {}", path.string());
return std::nullopt;
}
manifest.projectName = (*project)["name"].value_or<std::string>("");
if (manifest.projectName.empty()) {
REXLOG_ERROR("Manifest missing [project].name: {}", path.string());
return std::nullopt;
}
if (!IsValidProjectName(manifest.projectName)) {
REXLOG_ERROR(
"Manifest [project].name '{}' is not a valid identifier "
"(letters, digits, underscore; must not start with a digit): {}",
manifest.projectName, path.string());
return std::nullopt;
}
if (auto stamp = (*project)["sdk_version"].value<std::string>(); stamp && !stamp->empty()) {
manifest.sdkVersion = *stamp;
}
if (auto root = (*project)["game_root"].value<std::string>(); root && !root->empty()) {
manifest.gameRoot = *root;
}
auto* entrypoint = tbl["entrypoint"].as_table();
if (!entrypoint) {
REXLOG_ERROR("Manifest missing [entrypoint] section: {}", path.string());
return std::nullopt;
}
if (!LoadBinaryConfig(*entrypoint, manifest.manifestDir, manifest.projectName,
manifest.entrypoint)) {
return std::nullopt;
}
if (auto modules = tbl["modules"].as_array()) {
size_t index = 0;
for (const auto& mod : *modules) {
auto* modTbl = mod.as_table();
if (!modTbl) {
REXLOG_ERROR("Manifest [[modules]] entry #{} is not a table", index);
return std::nullopt;
}
BinaryConfig binary;
if (!LoadBinaryConfig(*modTbl, manifest.manifestDir, manifest.projectName, binary)) {
return std::nullopt;
}
auto guest_path = (*modTbl)["guest_path"].value_or<std::string>("");
if (guest_path.empty()) {
REXLOG_ERROR("Manifest [[modules]] entry #{} missing guest_path", index);
return std::nullopt;
}
binary.guestPath = CanonicalizeModuleGuestPath(guest_path, manifest.projectName);
for (const auto& existing : manifest.modules) {
if (existing.guestPath == binary.guestPath) {
REXLOG_ERROR("Manifest [[modules]] duplicate guest_path '{}' (entry #{})",
binary.guestPath, index);
return std::nullopt;
}
}
manifest.modules.push_back(std::move(binary));
++index;
}
}
return manifest;
}
bool ManifestConfig::IsManifest(const std::filesystem::path& path) {
try {
auto tbl = toml::parse_file(path.string());
return tbl.contains("project");
} catch (const toml::parse_error&) {
return false;
}
}
namespace {
std::optional<std::string> ParseSectionHeader(std::string_view line) {
auto first = line.find_first_not_of(" \t");
if (first == std::string_view::npos)
return std::nullopt;
if (line[first] != '[' || (first + 1 < line.size() && line[first + 1] == '['))
return std::nullopt;
auto end = line.find(']', first + 1);
if (end == std::string_view::npos)
return std::nullopt;
return std::string(line.substr(first + 1, end - first - 1));
}
bool LineSetsKey(std::string_view line, std::string_view key) {
auto first = line.find_first_not_of(" \t");
if (first == std::string_view::npos)
return false;
if (line.compare(first, key.size(), key) != 0)
return false;
auto after = first + key.size();
while (after < line.size() && (line[after] == ' ' || line[after] == '\t'))
++after;
return after < line.size() && line[after] == '=';
}
} // namespace
bool ManifestConfig::WriteSdkVersionStamp(const std::filesystem::path& path,
std::string_view version) {
std::ifstream in(path);
if (!in) {
REXLOG_ERROR("Failed to open manifest for stamping: {}", path.string());
return false;
}
std::vector<std::string> lines;
std::string buf;
while (std::getline(in, buf)) {
lines.push_back(std::move(buf));
buf.clear();
}
in.close();
const std::string stamp_line = "sdk_version = \"" + std::string(version) + "\"";
std::optional<size_t> project_header_idx;
std::optional<size_t> stamp_idx;
bool in_project = false;
for (size_t i = 0; i < lines.size(); ++i) {
auto sec = ParseSectionHeader(lines[i]);
if (sec) {
in_project = (*sec == "project");
if (in_project) {
project_header_idx = i;
stamp_idx.reset();
}
continue;
}
if (in_project && !stamp_idx && LineSetsKey(lines[i], "sdk_version")) {
stamp_idx = i;
}
}
if (stamp_idx) {
lines[*stamp_idx] = stamp_line;
} else if (project_header_idx) {
lines.insert(lines.begin() + *project_header_idx + 1, stamp_line);
} else {
if (!lines.empty() && !lines.back().empty())
lines.emplace_back();
lines.emplace_back("[project]");
lines.push_back(stamp_line);
}
auto tmp_path = path;
tmp_path += ".tmp";
{
std::ofstream out(tmp_path, std::ios::binary);
if (!out) {
REXLOG_ERROR("Failed to open manifest tmp for writing: {}", tmp_path.string());
return false;
}
for (size_t i = 0; i < lines.size(); ++i) {
out << lines[i];
if (i + 1 < lines.size())
out << '\n';
}
out << '\n';
if (!out.good()) {
REXLOG_ERROR("Failed while writing manifest tmp: {}", tmp_path.string());
std::error_code ignore;
std::filesystem::remove(tmp_path, ignore);
return false;
}
}
std::error_code ec;
std::filesystem::rename(tmp_path, path, ec);
if (ec) {
REXLOG_ERROR("Failed to rename manifest tmp into place: {}", ec.message());
std::error_code ignore;
std::filesystem::remove(tmp_path, ignore);
return false;
}
return true;
}
} // namespace rex::codegen
+8 -7
View File
@@ -191,7 +191,7 @@ void discoverFunction(CodegenContext& ctx, uint32_t funcAddr,
}
void discoverAllFunctions(CodegenContext& ctx) {
REXCODEGEN_INFO("Analyze: starting iterative discovery...");
REXCODEGEN_TRACE("Analyze: starting iterative discovery...");
auto& graph = ctx.graph;
auto& binary = ctx.binary();
@@ -219,7 +219,7 @@ void discoverAllFunctions(CodegenContext& ctx) {
}
}
REXCODEGEN_INFO("Analyze: {} functions after call graph expansion", graph.functionCount());
REXCODEGEN_TRACE("Analyze: {} functions after call graph expansion", graph.functionCount());
// VTable scanning
{
@@ -242,8 +242,8 @@ void discoverAllFunctions(CodegenContext& ctx) {
}
}
REXCODEGEN_INFO("Analyze: VTable scan found {} vtables, {} new functions", vtables.size(),
newFunctions);
REXCODEGEN_TRACE("Analyze: VTable scan found {} vtables, {} new functions", vtables.size(),
newFunctions);
// Continue discovery for vtable functions
if (newFunctions > 0) {
@@ -264,7 +264,7 @@ void discoverAllFunctions(CodegenContext& ctx) {
}
}
REXCODEGEN_INFO("Analyze: {} total functions after vtable scan", graph.functionCount());
REXCODEGEN_TRACE("Analyze: {} total functions after vtable scan", graph.functionCount());
}
//=============================================================================
@@ -399,7 +399,7 @@ void functionPointerScan(CodegenContext& ctx) {
}
}
REXCODEGEN_INFO("functionPointerScan: found {} new function pointer targets", foundCount);
REXCODEGEN_TRACE("functionPointerScan: found {} new function pointer targets", foundCount);
}
} // anonymous namespace
@@ -421,7 +421,8 @@ size_t discoverPendingFunctions(CodegenContext& ctx,
namespace phases {
VoidResult Discover(CodegenContext& ctx) {
VoidResult Discover(CodegenContext& ctx, ProgressReporter* reporter) {
(void)reporter;
discoverAllFunctions(ctx);
return Ok();
}
+2 -1
View File
@@ -249,7 +249,8 @@ void cleanupAbsorbedGapFills(CodegenContext& ctx) {
namespace phases {
VoidResult GapFill(CodegenContext& ctx) {
VoidResult GapFill(CodegenContext& ctx, ProgressReporter* reporter) {
(void)reporter;
size_t lastCount = 0;
size_t iteration = 0;
+6 -5
View File
@@ -28,7 +28,7 @@ namespace {
// Merge to resolve jumps then seal functions
//=============================================================================
void mergeAndSeal(CodegenContext& ctx) {
REXCODEGEN_INFO("Analyze: resolving jumps and sealing functions...");
REXCODEGEN_TRACE("Analyze: resolving jumps and sealing functions...");
auto& graph = ctx.graph;
auto& binary = ctx.binary();
@@ -71,11 +71,11 @@ void mergeAndSeal(CodegenContext& ctx) {
size_t totalSealed = graph.sealAllReady();
size_t stillPending = graph.pendingCount();
REXCODEGEN_INFO("Analyze: {} iterations, resolved={}, sealed={}/{}", iteration, totalResolved,
totalSealed, graph.functionCount());
REXCODEGEN_TRACE("Analyze: {} iterations, resolved={}, sealed={}/{}", iteration, totalResolved,
totalSealed, graph.functionCount());
if (stillPending > 0) {
REXCODEGEN_WARN("Analyze: {} functions still PENDING with unresolved jumps", stillPending);
REXCODEGEN_DEBUG("Analyze: {} functions still PENDING with unresolved jumps", stillPending);
}
}
@@ -83,7 +83,8 @@ void mergeAndSeal(CodegenContext& ctx) {
namespace phases {
VoidResult Merge(CodegenContext& ctx) {
VoidResult Merge(CodegenContext& ctx, ProgressReporter* reporter) {
(void)reporter;
mergeAndSeal(ctx);
return Ok();
}
+9 -7
View File
@@ -418,7 +418,7 @@ void detectSaveRestoreHelpers(const BinaryView& binary, AnalysisState& state) {
//=============================================================================
VoidResult registerEntryPoints(CodegenContext& ctx) {
REXCODEGEN_INFO("Analyze: registering entry points...");
REXCODEGEN_TRACE("Analyze: registering entry points...");
auto& graph = ctx.graph;
auto& config = ctx.Config();
@@ -502,8 +502,9 @@ VoidResult registerEntryPoints(CodegenContext& ctx) {
importCount++;
}
REXCODEGEN_INFO("Analyze: loaded {} imports ({} resolved, {} unresolved, {} variables skipped)",
importCount, resolvedCount, unresolvedCount, variableCount);
REXCODEGEN_TRACE(
"Analyze: loaded {} imports ({} resolved, {} unresolved, {} variables skipped)",
importCount, resolvedCount, unresolvedCount, variableCount);
}
// Register save/restore helpers
@@ -579,7 +580,7 @@ VoidResult registerEntryPoints(CodegenContext& ctx) {
uint32_t offsetInSection = pdataAddr - pdataSection->baseAddress;
const uint8_t* pdataData = pdataSection->data + offsetInSection;
REXCODEGEN_INFO("Analyze: PDATA base=0x{:08X}, size={}", pdataAddr, pdataSize);
REXCODEGEN_TRACE("Analyze: PDATA base=0x{:08X}, size={}", pdataAddr, pdataSize);
size_t count = pdataSize / sizeof(IMAGE_CE_RUNTIME_FUNCTION);
auto* entries = reinterpret_cast<const IMAGE_CE_RUNTIME_FUNCTION*>(pdataData);
@@ -669,7 +670,7 @@ VoidResult registerEntryPoints(CodegenContext& ctx) {
pdataAdded++;
}
REXCODEGEN_INFO("Analyze: added {} functions from PDATA", pdataAdded);
REXCODEGEN_TRACE("Analyze: added {} functions from PDATA", pdataAdded);
// Queue EH-discovered functions
size_t ehFuncsQueued = 0;
@@ -684,7 +685,7 @@ VoidResult registerEntryPoints(CodegenContext& ctx) {
}
if (ehFuncsQueued > 0) {
REXCODEGEN_INFO("Analyze: queued {} functions from exception handling", ehFuncsQueued);
REXCODEGEN_TRACE("Analyze: queued {} functions from exception handling", ehFuncsQueued);
}
return Ok();
@@ -694,7 +695,8 @@ VoidResult registerEntryPoints(CodegenContext& ctx) {
namespace phases {
VoidResult Register(CodegenContext& ctx) {
VoidResult Register(CodegenContext& ctx, ProgressReporter* reporter) {
(void)reporter;
return registerEntryPoints(ctx);
}
+6 -5
View File
@@ -82,7 +82,7 @@ std::vector<CodeRegion> segmentSection(const SectionView& section,
}
void scanBinary(CodegenContext& ctx) {
REXCODEGEN_INFO("Analyze: scanning binary...");
REXCODEGEN_TRACE("Analyze: scanning binary...");
auto& binary = ctx.binary();
auto& config = ctx.Config();
@@ -116,7 +116,7 @@ void scanBinary(CodegenContext& ctx) {
scan.codeRegions.insert(scan.codeRegions.end(), regions.begin(), regions.end());
}
REXCODEGEN_INFO("Analyze: segmented into {} code regions", scan.codeRegions.size());
REXCODEGEN_TRACE("Analyze: segmented into {} code regions", scan.codeRegions.size());
// Detect data regions
for (const auto& section : binary.sections()) {
@@ -159,15 +159,16 @@ void scanBinary(CodegenContext& ctx) {
}
}
REXCODEGEN_INFO("Analyze: {} code regions, {} data regions", scan.codeRegions.size(),
scan.dataRegions.size());
REXCODEGEN_TRACE("Analyze: {} code regions, {} data regions", scan.codeRegions.size(),
scan.dataRegions.size());
}
} // anonymous namespace
namespace phases {
VoidResult Scan(CodegenContext& ctx) {
VoidResult Scan(CodegenContext& ctx, ProgressReporter* reporter) {
(void)reporter;
scanBinary(ctx);
return Ok();
}
+6 -5
View File
@@ -43,7 +43,7 @@ const CallEdge* findCallEdgeAt(const FunctionNode* node, uint32_t site) {
}
VoidResult validateGraph(CodegenContext& ctx) {
REXCODEGEN_INFO("Analyze: validating call graph...");
REXCODEGEN_TRACE("Analyze: validating call graph...");
auto& graph = ctx.graph;
auto& binary = ctx.binary();
@@ -140,8 +140,8 @@ VoidResult validateGraph(CodegenContext& ctx) {
}
}
REXCODEGEN_INFO("Analyze: checked {} branches in {} functions, verified {} edges", callsChecked,
functionsChecked, edgesVerified);
REXCODEGEN_TRACE("Analyze: checked {} branches in {} functions, verified {} edges", callsChecked,
functionsChecked, edgesVerified);
if (errors.HasErrors()) {
REXCODEGEN_ERROR("Analyze: found {} errors", errors.Count());
@@ -151,7 +151,7 @@ VoidResult validateGraph(CodegenContext& ctx) {
errors.Count(AnalysisErrors::Category::UnresolvedCall)));
}
REXCODEGEN_INFO("Analyze: all calls resolve");
REXCODEGEN_TRACE("Analyze: all calls resolve");
return Ok();
}
@@ -159,7 +159,8 @@ VoidResult validateGraph(CodegenContext& ctx) {
namespace phases {
VoidResult Validate(CodegenContext& ctx) {
VoidResult Validate(CodegenContext& ctx, ProgressReporter* reporter) {
(void)reporter;
return validateGraph(ctx);
}
+11 -1
View File
@@ -13,13 +13,23 @@
namespace rex::codegen::ppc {
thread_local DisassemblerEngine gBigEndianDisassembler{BFD_ENDIAN_BIG, "cell 64"};
// Binutils PPC_OPCODE_* dialect bits (see thirdparty/disasm/ppc-dis.c)
constexpr uintptr_t kXenonDialect = 0x1 // PPC
| 0x4 // 64
| 0x4000 // POWER4
| 0x8000000 // CELL
| 0x200 // ALTIVEC
| 0x1000000 // VMX_128
| 0x10000; // CLASSIC
thread_local DisassemblerEngine gBigEndianDisassembler{BFD_ENDIAN_BIG, nullptr};
DisassemblerEngine::DisassemblerEngine(bfd_endian endian, const char* options) {
INIT_DISASSEMBLE_INFO(info, stdout, fprintf);
info.arch = bfd_arch_powerpc;
info.endian = endian;
info.disassembler_options = options;
info.private_data = reinterpret_cast<void*>(kXenonDialect);
}
int DisassemblerEngine::Disassemble(const void* code, size_t size, uint64_t base, ppc_insn& out) {
@@ -0,0 +1,435 @@
/**
* @file codegen/project_recompiler.cpp
* @brief Project-level recompiler driving manifest-based multi-binary codegen
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <rex/codegen/project_recompiler.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <unordered_map>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <rex/codegen/analyze.h>
#include <rex/codegen/binary_view.h>
#include <rex/codegen/codegen.h>
#include <rex/codegen/codegen_context.h>
#include <rex/codegen/codegen_writer.h>
#include <rex/codegen/config.h>
#include <rex/codegen/template_registry.h>
#include <rex/kernel/init.h>
#include <rex/logging.h>
#include <rex/runtime.h>
#include <rex/system/user_module.h>
#include <chrono>
#include "codegen_logging.h"
#include "template_registry_internal.h"
namespace rex::codegen {
namespace {
std::string DeriveTargetNameFromFilePath(const std::string& file_path) {
std::filesystem::path p(file_path);
std::string name = p.stem().string();
std::replace(name.begin(), name.end(), '.', '_');
std::replace(name.begin(), name.end(), ' ', '_');
return name;
}
} // namespace
ProjectRecompiler::ProjectRecompiler(ManifestConfig manifest) : manifest_(std::move(manifest)) {}
Result<void> ProjectRecompiler::Run(const ProjectRecompilerOptions& opts) {
namespace fs = std::filesystem;
deletedFiles_.clear();
writtenFiles_.clear();
if (manifest_.modules.empty()) {
REXCODEGEN_TRACE("Recompiling '{}' (entrypoint)", manifest_.projectName);
} else {
REXCODEGEN_TRACE("Recompiling '{}' (entrypoint + {} DLL{})", manifest_.projectName,
manifest_.modules.size(), manifest_.modules.size() == 1 ? "" : "s");
}
struct ModuleEntry {
std::string targetName;
std::string guestPath;
bool isDll;
RecompilerConfig config;
};
std::vector<ModuleEntry> allModules;
allModules.push_back({DeriveTargetNameFromFilePath(manifest_.entrypoint.recompiler.filePath), "",
false, std::move(manifest_.entrypoint.recompiler)});
for (auto& mod : manifest_.modules) {
allModules.push_back({DeriveTargetNameFromFilePath(mod.recompiler.filePath), mod.guestPath,
true, std::move(mod.recompiler)});
}
if (!opts.targets.empty()) {
std::vector<std::string> unknown;
for (const auto& t : opts.targets) {
bool match = std::any_of(allModules.begin() + 1, allModules.end(),
[&t](const ModuleEntry& m) { return m.targetName == t; });
if (!match) {
unknown.push_back(t);
}
}
if (!unknown.empty()) {
std::string known;
for (size_t i = 1; i < allModules.size(); ++i) {
if (i > 1)
known += ", ";
known += allModules[i].targetName;
}
std::string list;
for (size_t i = 0; i < unknown.size(); ++i) {
if (i > 0)
list += ", ";
list += unknown[i];
}
return Err<void>(ErrorCategory::Config,
fmt::format("Unknown --target value(s): {}. Known DLL targets: {}", list,
known.empty() ? "(none)" : known));
}
}
std::vector<ModuleEntry> targeted;
targeted.push_back(std::move(allModules[0]));
for (size_t i = 1; i < allModules.size(); ++i) {
if (opts.targets.empty() || std::find(opts.targets.begin(), opts.targets.end(),
allModules[i].targetName) != opts.targets.end()) {
targeted.push_back(std::move(allModules[i]));
}
}
// Two binaries sharing an out_directory_path would clobber each other's
// sources.cmake on emit (the writer's cleanup sweep is unprefixed for
// that file).
std::unordered_map<std::string, std::string> outDirOwner;
for (const auto& m : targeted) {
const auto& outDir = m.config.outDirectoryPath;
if (outDir.empty())
continue;
auto [it, inserted] = outDirOwner.emplace(outDir, m.targetName);
if (!inserted) {
return Err<void>(
ErrorCategory::Validation,
fmt::format("out_directory_path '{}' is shared by '{}' and '{}'; each binary "
"needs its own output directory",
outDir, it->second, m.targetName));
}
}
const auto& entryConfig = targeted[0].config;
auto configDir = manifest_.manifestDir;
fs::path entryXexPath = configDir / entryConfig.filePath;
if (!entryConfig.patchedFilePath.empty()) {
auto patched = configDir / entryConfig.patchedFilePath;
if (fs::exists(patched))
entryXexPath = patched;
}
if (!fs::exists(entryXexPath)) {
return Err<void>(ErrorCategory::IO,
fmt::format("Entrypoint XEX not found: {}", entryXexPath.string()));
}
entryXexPath = fs::canonical(entryXexPath);
// gameRoot anchors VFS root and DLL guest_path derivation. Honor the
// manifest override if set; otherwise default to the entrypoint's parent.
fs::path gameRoot;
if (manifest_.gameRoot && !manifest_.gameRoot->empty()) {
fs::path resolved = configDir / *manifest_.gameRoot;
if (!fs::exists(resolved) || !fs::is_directory(resolved)) {
return Err<void>(ErrorCategory::Validation,
fmt::format("[project].game_root '{}' does not resolve to a directory",
resolved.string()));
}
gameRoot = fs::canonical(resolved);
} else {
gameRoot = fs::canonical(entryXexPath.parent_path());
}
fs::path entryRel = fs::relative(entryXexPath, gameRoot);
if (entryRel.empty() || *entryRel.begin() == "..") {
return Err<void>(ErrorCategory::Validation,
fmt::format("Entrypoint XEX '{}' resolves outside game root '{}'",
entryXexPath.string(), gameRoot.string()));
}
auto runtime = std::make_unique<Runtime>(gameRoot.string());
auto rtStatus = runtime->Setup(rex::RuntimeConfig{
.kernel_init = rex::kernel::InitializeKernel,
.tool_mode = true,
});
if (rtStatus != X_STATUS_SUCCESS) {
return Err<void>(ErrorCategory::IO,
fmt::format("Failed to initialize Runtime: {:#x}", rtStatus));
}
std::string entryRelStr = entryRel.string();
std::replace(entryRelStr.begin(), entryRelStr.end(), '/', '\\');
auto entryVfsPath = "game:\\" + entryRelStr;
rtStatus = runtime->LoadXexImage(entryVfsPath);
if (rtStatus != X_STATUS_SUCCESS) {
return Err<void>(ErrorCategory::IO,
fmt::format("Failed to load entrypoint XEX: {:#x}", rtStatus));
}
std::vector<rex::system::object_ref<rex::system::UserModule>> dllModules;
for (size_t i = 1; i < targeted.size(); ++i) {
const auto& dllConfig = targeted[i].config;
fs::path dllXexPath = configDir / dllConfig.filePath;
if (!dllConfig.patchedFilePath.empty()) {
auto patched = configDir / dllConfig.patchedFilePath;
if (fs::exists(patched))
dllXexPath = patched;
}
if (!fs::exists(dllXexPath)) {
return Err<void>(ErrorCategory::IO,
fmt::format("DLL XEX not found: {}", dllXexPath.string()));
}
dllXexPath = fs::canonical(dllXexPath);
auto relPath = fs::relative(dllXexPath, gameRoot);
if (relPath.empty() || *relPath.begin() == "..") {
return Err<void>(ErrorCategory::Validation,
fmt::format("DLL '{}' resolves outside game root '{}': {}",
targeted[i].targetName, gameRoot.string(), dllXexPath.string()));
}
std::string relStr = relPath.string();
std::replace(relStr.begin(), relStr.end(), '/', '\\');
auto dllVfsPath = "game:\\" + relStr;
auto userMod = runtime->kernel_state()->LoadUserModule(dllVfsPath, false);
if (!userMod) {
return Err<void>(ErrorCategory::IO,
fmt::format("Failed to load DLL module: {}", targeted[i].targetName));
}
REXCODEGEN_TRACE("Loaded DLL module '{}' at base 0x{:08X}", targeted[i].targetName,
userMod->xex_module()->base_address());
dllModules.push_back(std::move(userMod));
}
struct ContextEntry {
CodegenContext ctx;
const ModuleEntry* module;
std::string display_name;
};
std::vector<ContextEntry> contexts;
auto make_display_name = [](const std::string& filePath) {
return std::filesystem::path(filePath).filename().string();
};
auto* resolver = runtime->export_resolver();
{
auto execMod = runtime->kernel_state()->GetExecutableModule();
auto bv = BinaryView::fromModule(*execMod->xex_module());
auto entry_display = make_display_name(targeted[0].config.filePath);
RecompilerConfig cfg = std::move(targeted[0].config);
if (opts.enableExceptionHandlers)
cfg.generateExceptionHandlers = true;
auto ctx = CodegenContext::Create(std::move(bv), std::move(cfg));
ctx.setResolver(resolver);
ctx.setConfigDir(configDir);
ctx.analysisState().format = "xex";
ctx.analysisState().loadAddress = ctx.binary().baseAddress();
ctx.analysisState().entryPoint = ctx.binary().entryPoint();
ctx.analysisState().imageSize = ctx.binary().imageSize();
ctx.setHasDllModules(!manifest_.modules.empty());
if (ctx.Config().isDll.has_value())
ctx.setDllModule(*ctx.Config().isDll);
contexts.push_back({std::move(ctx), &targeted[0], std::move(entry_display)});
}
for (size_t i = 0; i < dllModules.size(); ++i) {
auto& userMod = dllModules[i];
auto bv = BinaryView::fromModule(*userMod->xex_module());
auto dll_display = make_display_name(targeted[i + 1].config.filePath);
RecompilerConfig cfg = std::move(targeted[i + 1].config);
if (opts.enableExceptionHandlers)
cfg.generateExceptionHandlers = true;
auto ctx = CodegenContext::Create(std::move(bv), std::move(cfg));
ctx.setResolver(resolver);
ctx.setConfigDir(configDir);
ctx.analysisState().format = "xex";
ctx.analysisState().loadAddress = ctx.binary().baseAddress();
ctx.analysisState().entryPoint = ctx.binary().entryPoint();
ctx.analysisState().imageSize = ctx.binary().imageSize();
ctx.setDllModule(ctx.Config().isDll.value_or(true));
ctx.setHasDllModules(true);
contexts.push_back({std::move(ctx), &targeted[i + 1], std::move(dll_display)});
}
std::vector<std::chrono::steady_clock::time_point> module_started_at(contexts.size());
for (size_t i = 0; i < contexts.size(); ++i) {
auto& entry = contexts[i];
if (opts.reporter) {
opts.reporter->moduleStarted(entry.display_name, i, contexts.size());
}
module_started_at[i] = std::chrono::steady_clock::now();
REXCODEGEN_TRACE("Analyzing '{}'...", entry.module->targetName);
auto result = Analyze(entry.ctx, opts.reporter);
if (!result) {
if (opts.force && result.error().category == ErrorCategory::Validation) {
REXLOG_WARN("Analysis errors for '{}' (continuing due to --force): {}",
entry.module->targetName, result.error().message);
} else {
REXLOG_ERROR("Analysis failed for '{}'", entry.module->targetName);
return result;
}
}
}
for (size_t i = 0; i < contexts.size(); ++i) {
uint32_t a_base = contexts[i].ctx.binary().baseAddress();
uint32_t a_end = a_base + contexts[i].ctx.binary().imageSize();
for (size_t j = i + 1; j < contexts.size(); ++j) {
uint32_t b_base = contexts[j].ctx.binary().baseAddress();
uint32_t b_end = b_base + contexts[j].ctx.binary().imageSize();
if (a_base < b_end && b_base < a_end) {
return Err<void>(ErrorCategory::Validation,
fmt::format("Module '{}' [{:08X}, {:08X}) overlaps '{}' [{:08X}, {:08X})",
contexts[i].module->targetName, a_base, a_end,
contexts[j].module->targetName, b_base, b_end));
}
}
}
deletedFiles_.clear();
writtenFiles_.clear();
for (size_t i = 0; i < contexts.size(); ++i) {
auto& entry = contexts[i];
if (opts.reporter) {
opts.reporter->moduleStarted(entry.display_name, i, contexts.size());
opts.reporter->phaseChanged("Write");
}
REXCODEGEN_TRACE("Writing output for '{}'...", entry.module->targetName);
CodegenWriter writer(entry.ctx, runtime.get());
if (!writer.write(opts.force)) {
return Err<void>(ErrorCategory::Validation,
fmt::format("Write failed for '{}'", entry.module->targetName));
}
deletedFiles_.insert(deletedFiles_.end(), writer.deletedFiles().begin(),
writer.deletedFiles().end());
writtenFiles_.insert(writtenFiles_.end(), writer.writtenFiles().begin(),
writer.writtenFiles().end());
if (opts.reporter) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - module_started_at[i]);
opts.reporter->moduleFinished(elapsed);
}
}
if (manifest_.modules.empty()) {
REXCODEGEN_TRACE("Project recompiler complete");
return Ok();
}
auto outputPath = contexts[0].ctx.configDir() / contexts[0].ctx.Config().outDirectoryPath;
std::error_code mk_ec;
fs::create_directories(outputPath, mk_ec);
if (mk_ec) {
return Err<void>(ErrorCategory::IO, fmt::format("Failed to create output dir {}: {}",
outputPath.string(), mk_ec.message()));
}
if (opts.reporter)
opts.reporter->projectPhaseStarted("module_registry");
{
nlohmann::json registryData;
registryData["project"] = manifest_.projectName;
auto& dllArray = registryData["dll_modules"];
dllArray = nlohmann::json::array();
for (size_t i = 1; i < targeted.size(); ++i) {
auto& mod = targeted[i];
auto guestPath = mod.guestPath;
std::replace(guestPath.begin(), guestPath.end(), '\\', '/');
nlohmann::json dllEntry;
dllEntry["pe_name"] = fs::path(mod.guestPath).filename().string();
dllEntry["guest_path"] = guestPath;
dllEntry["shared_lib_name"] = manifest_.projectName + "_" + mod.targetName;
dllArray.push_back(dllEntry);
}
TemplateRegistry registry;
auto registryContent = renderWithJson(registry, "codegen/module_registry_cpp", registryData);
auto registryPath = outputPath / "module_registry.cpp";
std::ofstream f(registryPath);
if (!f) {
return Err<void>(ErrorCategory::IO, fmt::format("Failed to open {}", registryPath.string()));
}
f << registryContent;
if (!f.good()) {
return Err<void>(ErrorCategory::IO,
fmt::format("Failed while writing {}", registryPath.string()));
}
writtenFiles_.push_back(registryPath.filename().string());
REXCODEGEN_TRACE("Wrote {}", registryPath.string());
}
if (opts.reporter)
opts.reporter->projectPhaseFinished();
if (opts.reporter)
opts.reporter->projectPhaseStarted("dll_targets.cmake");
{
nlohmann::json dllTargetsData;
auto& dllTargetsArray = dllTargetsData["dll_modules"];
dllTargetsArray = nlohmann::json::array();
for (size_t i = 0; i < dllModules.size(); ++i) {
auto& mod = targeted[i + 1];
auto& dllCtx = contexts[i + 1].ctx;
nlohmann::json entry;
entry["target_name"] = mod.targetName;
entry["lib_name"] = manifest_.projectName + "_" + mod.targetName;
entry["output_dir"] = dllCtx.Config().outDirectoryPath;
dllTargetsArray.push_back(entry);
}
TemplateRegistry registry;
auto dllCmakeContent = renderWithJson(registry, "codegen/dll_targets_cmake", dllTargetsData);
auto dllCmakePath = outputPath / "dll_targets.cmake";
std::ofstream cf(dllCmakePath);
if (!cf) {
return Err<void>(ErrorCategory::IO, fmt::format("Failed to open {}", dllCmakePath.string()));
}
cf << dllCmakeContent;
if (!cf.good()) {
return Err<void>(ErrorCategory::IO,
fmt::format("Failed while writing {}", dllCmakePath.string()));
}
writtenFiles_.push_back(dllCmakePath.filename().string());
REXCODEGEN_TRACE("Wrote {}", dllCmakePath.string());
}
if (opts.reporter)
opts.reporter->projectPhaseFinished();
REXCODEGEN_TRACE("Project recompiler complete");
return Ok();
}
} // namespace rex::codegen
+18 -1
View File
@@ -57,6 +57,23 @@ struct TemplateRegistry::Impl {
oss << "0x" << std::hex << std::uppercase << val;
return oss.str();
});
// Resolve {% include "<id>" %} against the embedded registry. Templates
// are embedded under canonical IDs without the .inja extension; accept
// either form so include directives can use either.
env_.set_search_included_templates_in_files(false);
env_.set_include_callback(
[this](const std::filesystem::path&, const std::string& name) -> inja::Template {
std::string id = name;
if (id.size() > 5 && id.compare(id.size() - 5, 5, ".inja") == 0) {
id.erase(id.size() - 5);
}
auto it = embedded_.find(id);
if (it == embedded_.end()) {
throw TemplateError(name, "include not found in embedded registry");
}
return env_.parse(std::string(it->second));
});
}
std::string renderImpl(const std::string& id, const nlohmann::json& data) {
@@ -130,7 +147,7 @@ void TemplateRegistry::loadOverrides(const std::filesystem::path& dir) {
}();
auto tmpl = impl_->env_.parse(content);
impl_->overrides_[id] = std::move(tmpl);
REXCODEGEN_INFO("Loaded template override: {} -> {}", id, entry.path().string());
REXCODEGEN_TRACE("Loaded template override: {} -> {}", id, entry.path().string());
} catch (const std::exception& e) {
throw TemplateError(id, e.what(), entry.path().string());
}
+2
View File
@@ -41,6 +41,7 @@ if(WIN32)
seh_win.cpp
socket_win.cpp
string_win.cpp
system_win.cpp
threading_win.cpp
)
elseif(UNIX)
@@ -58,6 +59,7 @@ elseif(UNIX)
seh_posix.cpp
socket_posix.cpp
string_posix.cpp
system_posix.cpp
threading_posix.cpp
)
endif()
+124 -6
View File
@@ -8,6 +8,7 @@
#include <algorithm>
#include <cctype>
#include <charconv>
#include <cstdlib>
#include <filesystem>
#include <fstream>
@@ -29,6 +30,13 @@ bool g_finalized = false;
bool g_lifecycle_override = false;
std::mutex g_mutex;
// Recursive: FlagRegistrar chain methods re-enter; change callbacks invoked
// from SetFlagByName must not mutate the registry.
std::recursive_mutex& GetRegistryMutex() {
static std::recursive_mutex m;
return m;
}
// Flag registry - use functions to avoid static init order issues
std::vector<FlagEntry>& GetRegistryStorage() {
static std::vector<FlagEntry> registry;
@@ -164,17 +172,60 @@ std::vector<FlagEntry>& GetRegistry() {
return GetRegistryStorage();
}
void RegisterFlag(FlagEntry entry) {
auto it = GetRegistryIndex().find(entry.name);
if (it != GetRegistryIndex().end()) {
return; // Already registered
std::optional<size_t> RegisterFlag(FlagEntry entry) {
std::lock_guard lock(GetRegistryMutex());
(void)GetCallbackStorage();
(void)GetPendingRestartStorage();
auto& index = GetRegistryIndex();
auto& storage = GetRegistryStorage();
auto it = index.find(entry.name);
if (it != index.end()) {
REXLOG_ERROR("cvar: duplicate registration of '{}'; second registration ignored", entry.name);
return std::nullopt;
}
size_t pos = storage.size();
index[entry.name] = pos;
storage.push_back(std::move(entry));
return pos;
}
GetRegistryIndex()[entry.name] = GetRegistryStorage().size();
GetRegistryStorage().push_back(std::move(entry));
void UnregisterFlag(std::string_view name) {
std::lock_guard lock(GetRegistryMutex());
auto& index = GetRegistryIndex();
auto& storage = GetRegistryStorage();
std::string key(name);
auto idx_it = index.find(key);
if (idx_it == index.end()) {
return;
}
size_t pos = idx_it->second;
index.erase(idx_it);
storage.erase(storage.begin() + pos);
for (auto& [n, i] : index) {
if (i > pos) {
--i;
}
}
GetCallbackStorage().erase(key);
auto& pending = GetPendingRestartStorage();
pending.erase(std::remove(pending.begin(), pending.end(), key), pending.end());
}
void FlagRegistrar::apply_(std::function<void(FlagEntry&)> fn) {
if (owned_name_.empty()) {
return;
}
std::lock_guard lock(GetRegistryMutex());
auto& index = GetRegistryIndex();
auto it = index.find(owned_name_);
if (it == index.end()) {
return;
}
fn(GetRegistryStorage()[it->second]);
}
bool SetFlagByName(std::string_view name, std::string_view value) {
std::lock_guard lock(GetRegistryMutex());
auto it = GetRegistryIndex().find(std::string(name));
if (it == GetRegistryIndex().end()) {
return false;
@@ -215,6 +266,7 @@ bool SetFlagByName(std::string_view name, std::string_view value) {
}
std::string GetFlagByName(std::string_view name) {
std::lock_guard lock(GetRegistryMutex());
auto it = GetRegistryIndex().find(std::string(name));
if (it == GetRegistryIndex().end()) {
return "";
@@ -224,6 +276,7 @@ std::string GetFlagByName(std::string_view name) {
}
std::vector<std::string> ListFlags() {
std::lock_guard lock(GetRegistryMutex());
std::vector<std::string> result;
result.reserve(GetRegistryStorage().size());
for (const auto& entry : GetRegistryStorage()) {
@@ -234,6 +287,7 @@ std::vector<std::string> ListFlags() {
}
std::vector<std::string> ListFlagsByCategory(std::string_view category) {
std::lock_guard lock(GetRegistryMutex());
std::vector<std::string> result;
for (const auto& entry : GetRegistryStorage()) {
if (entry.category == category) {
@@ -245,6 +299,7 @@ std::vector<std::string> ListFlagsByCategory(std::string_view category) {
}
std::vector<std::string> ListFlagsByLifecycle(Lifecycle lc) {
std::lock_guard lock(GetRegistryMutex());
std::vector<std::string> result;
for (const auto& entry : GetRegistryStorage()) {
if (entry.lifecycle == lc) {
@@ -256,6 +311,8 @@ std::vector<std::string> ListFlagsByLifecycle(Lifecycle lc) {
}
const FlagEntry* GetFlagInfo(std::string_view name) {
// Pointer is invalidated by any subsequent registry call.
std::lock_guard lock(GetRegistryMutex());
auto it = GetRegistryIndex().find(std::string(name));
if (it == GetRegistryIndex().end()) {
return nullptr;
@@ -263,15 +320,69 @@ const FlagEntry* GetFlagInfo(std::string_view name) {
return &GetRegistryStorage()[it->second];
}
template <>
bool Query<bool>(std::string_view name) {
std::string v = GetFlagByName(name);
return v == "true" || v == "1" || v == "yes";
}
template <>
int32_t Query<int32_t>(std::string_view name) {
std::string v = GetFlagByName(name);
int32_t out = 0;
std::from_chars(v.data(), v.data() + v.size(), out);
return out;
}
template <>
int64_t Query<int64_t>(std::string_view name) {
std::string v = GetFlagByName(name);
int64_t out = 0;
std::from_chars(v.data(), v.data() + v.size(), out);
return out;
}
template <>
uint32_t Query<uint32_t>(std::string_view name) {
std::string v = GetFlagByName(name);
uint32_t out = 0;
std::from_chars(v.data(), v.data() + v.size(), out);
return out;
}
template <>
uint64_t Query<uint64_t>(std::string_view name) {
std::string v = GetFlagByName(name);
uint64_t out = 0;
std::from_chars(v.data(), v.data() + v.size(), out);
return out;
}
template <>
double Query<double>(std::string_view name) {
std::string v = GetFlagByName(name);
double out = 0.0;
ParseDouble(v, out);
return out;
}
template <>
std::string Query<std::string>(std::string_view name) {
return GetFlagByName(name);
}
std::vector<std::string> GetPendingRestartFlags() {
std::lock_guard lock(GetRegistryMutex());
return GetPendingRestartStorage();
}
void ClearPendingRestartFlags() {
std::lock_guard lock(GetRegistryMutex());
GetPendingRestartStorage().clear();
}
void ResetToDefault(std::string_view name) {
std::lock_guard lock(GetRegistryMutex());
auto it = GetRegistryIndex().find(std::string(name));
if (it == GetRegistryIndex().end()) {
return;
@@ -281,12 +392,14 @@ void ResetToDefault(std::string_view name) {
}
void ResetAllToDefaults() {
std::lock_guard lock(GetRegistryMutex());
for (const auto& entry : GetRegistryStorage()) {
entry.setter(entry.default_value);
}
}
bool HasNonDefaultValue(std::string_view name) {
std::lock_guard lock(GetRegistryMutex());
auto it = GetRegistryIndex().find(std::string(name));
if (it == GetRegistryIndex().end()) {
return false;
@@ -296,6 +409,7 @@ bool HasNonDefaultValue(std::string_view name) {
}
std::vector<std::string> ListModifiedFlags() {
std::lock_guard lock(GetRegistryMutex());
std::vector<std::string> result;
for (const auto& entry : GetRegistryStorage()) {
if (entry.getter() != entry.default_value) {
@@ -306,6 +420,7 @@ std::vector<std::string> ListModifiedFlags() {
}
std::string SerializeToTOML() {
std::lock_guard lock(GetRegistryMutex());
std::string result;
for (const auto& entry : GetRegistryStorage()) {
if (entry.getter() != entry.default_value) {
@@ -320,6 +435,7 @@ std::string SerializeToTOML() {
}
std::string SerializeToTOML(std::string_view category) {
std::lock_guard lock(GetRegistryMutex());
std::string result;
for (const auto& entry : GetRegistryStorage()) {
if (entry.category == category && entry.getter() != entry.default_value) {
@@ -334,10 +450,12 @@ std::string SerializeToTOML(std::string_view category) {
}
void RegisterChangeCallback(std::string_view name, ChangeCallback callback) {
std::lock_guard lock(GetRegistryMutex());
GetCallbackStorage()[std::string(name)].push_back(std::move(callback));
}
void UnregisterChangeCallbacks(std::string_view name) {
std::lock_guard lock(GetRegistryMutex());
GetCallbackStorage().erase(std::string(name));
}
+14
View File
@@ -442,6 +442,20 @@ void RemoveSink(LogCategoryId category, spdlog::sink_ptr sink) {
}
}
void ReplaceConsoleSink(spdlog::sink_ptr sink) {
std::lock_guard lock(g_mutex);
for (auto& entry : g_registry) {
if (!entry.logger)
continue;
auto& sinks = entry.logger->sinks();
if (g_console_sink)
std::erase(sinks, g_console_sink);
if (sink)
sinks.push_back(sink);
}
g_console_sink = sink;
}
void SetConsolePattern(const std::string& pattern) {
if (g_console_sink)
g_console_sink->set_pattern(pattern);
+33
View File
@@ -0,0 +1,33 @@
/**
* @file core/system_posix.cpp
* @brief POSIX implementations of rex/system.h platform helpers.
*
* @copyright Copyright (c) 2026 Tom Clay
* @license BSD 3-Clause License
*/
#include <cstdio>
#include <rex/system.h>
namespace rex {
// TODO(tomc): add linux support for showing a native message box
void ShowSimpleMessageBox(SimpleMessageBoxType type, std::string_view message) {
const char* level = "INFO";
switch (type) {
case SimpleMessageBoxType::Help:
level = "INFO";
break;
case SimpleMessageBoxType::Warning:
level = "WARNING";
break;
case SimpleMessageBoxType::Error:
level = "ERROR";
break;
}
std::fprintf(stderr, "[%s] %.*s\n", level, static_cast<int>(message.size()), message.data());
std::fflush(stderr);
}
} // namespace rex
+37
View File
@@ -0,0 +1,37 @@
/**
* @file core/system_win.cpp
* @brief Windows implementations of rex/system.h platform helpers.
*
* @copyright Copyright (c) 2026 Tom Clay
* @license BSD 3-Clause License
*/
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <rex/string.h>
#include <rex/system.h>
namespace rex {
void ShowSimpleMessageBox(SimpleMessageBoxType type, std::string_view message) {
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
const wchar_t* title = L"ReXGlue";
switch (type) {
case SimpleMessageBoxType::Help:
flags |= MB_ICONINFORMATION;
break;
case SimpleMessageBoxType::Warning:
flags |= MB_ICONWARNING;
break;
case SimpleMessageBoxType::Error:
flags |= MB_ICONERROR;
break;
}
auto wide = rex::string::to_utf16(message);
::MessageBoxW(nullptr, reinterpret_cast<const wchar_t*>(wide.c_str()), title, flags);
}
} // namespace rex
@@ -37,11 +37,10 @@ std::string D3D12GraphicsSystem::name() const {
return "Direct3D 12";
}
X_STATUS D3D12GraphicsSystem::Setup(runtime::FunctionDispatcher* function_dispatcher,
system::KernelState* kernel_state,
ui::WindowedAppContext* app_context, bool with_presentation) {
void D3D12GraphicsSystem::CreateProvider(bool /*with_presentation*/) {
// D3D12 doesn't differentiate headless vs. swapchain-capable providers;
// swapchains are created lazily per-window by the presenter.
provider_ = rex::ui::d3d12::D3D12Provider::Create();
return GraphicsSystem::Setup(function_dispatcher, kernel_state, app_context, with_presentation);
}
std::unique_ptr<CommandProcessor> D3D12GraphicsSystem::CreateCommandProcessor() {
@@ -2686,12 +2686,12 @@ ID3D12PipelineState* PipelineCache::CreateD3D12Pipeline(
const PipelineDescription& description = runtime_description.description;
if (runtime_description.pixel_shader != nullptr) {
REXGPU_INFO("Creating graphics pipeline with VS {:016X}, PS {:016X}",
runtime_description.vertex_shader->shader().ucode_data_hash(),
runtime_description.pixel_shader->shader().ucode_data_hash());
REXGPU_DEBUG("Creating graphics pipeline with VS {:016X}, PS {:016X}",
runtime_description.vertex_shader->shader().ucode_data_hash(),
runtime_description.pixel_shader->shader().ucode_data_hash());
} else {
REXGPU_INFO("Creating graphics pipeline with VS {:016X}",
runtime_description.vertex_shader->shader().ucode_data_hash());
REXGPU_DEBUG("Creating graphics pipeline with VS {:016X}",
runtime_description.vertex_shader->shader().ucode_data_hash());
}
D3D12_GRAPHICS_PIPELINE_STATE_DESC state_desc;
+46 -22
View File
@@ -117,32 +117,56 @@ GraphicsSystem::GraphicsSystem() : vsync_worker_running_(false) {}
GraphicsSystem::~GraphicsSystem() = default;
X_STATUS GraphicsSystem::Setup(runtime::FunctionDispatcher* function_dispatcher,
system::KernelState* kernel_state,
ui::WindowedAppContext* app_context, bool with_presentation) {
X_STATUS GraphicsSystem::SetupPresentation(ui::WindowedAppContext* app_context) {
if (presenter_) {
return X_STATUS_SUCCESS;
}
if (!provider_) {
CreateProvider(true);
if (!provider_) {
REXGPU_ERROR("Unable to create graphics provider");
return X_STATUS_UNSUCCESSFUL;
}
provider_supports_presentation_ = true;
} else if (!provider_supports_presentation_) {
// A prior SetupGuestGpu built a headless provider; backends like Vulkan
// need swapchain support baked in at provider creation time.
REXGPU_ERROR("SetupPresentation called after headless SetupGuestGpu; call order is reversed");
return X_STATUS_UNSUCCESSFUL;
}
app_context_ = app_context;
auto loss_cb = [this](bool is_responsible, bool statically_from_ui_thread) {
OnHostGpuLossFromAnyThread(is_responsible);
};
if (app_context_) {
// Presenter creation must happen on the UI thread.
app_context_->CallInUIThreadSynchronous(
[this, loss_cb]() { presenter_ = provider_->CreatePresenter(loss_cb); });
} else {
// Offscreen path (e.g. capturing guest output without a window).
presenter_ = provider_->CreatePresenter(loss_cb);
}
if (!presenter_) {
REXGPU_ERROR("Unable to create presenter");
return X_STATUS_UNSUCCESSFUL;
}
return X_STATUS_SUCCESS;
}
X_STATUS GraphicsSystem::SetupGuestGpu(runtime::FunctionDispatcher* function_dispatcher,
system::KernelState* kernel_state) {
memory_ = function_dispatcher->memory();
function_dispatcher_ = function_dispatcher;
kernel_state_ = kernel_state;
app_context_ = app_context;
// Create presenter if presentation is requested and provider is available
if (with_presentation && provider_) {
// Safe if either the UI thread call or the presenter creation fails.
if (app_context_) {
app_context_->CallInUIThreadSynchronous([this]() {
presenter_ =
provider_->CreatePresenter([this](bool is_responsible, bool statically_from_ui_thread) {
OnHostGpuLossFromAnyThread(is_responsible);
});
});
} else {
// May be needed for offscreen use, such as capturing the guest output
// image.
presenter_ =
provider_->CreatePresenter([this](bool is_responsible, bool statically_from_ui_thread) {
OnHostGpuLossFromAnyThread(is_responsible);
});
}
// Headless path: no one set up presentation, so build a no-presentation
// provider just for the command processor.
if (!provider_) {
CreateProvider(false);
provider_supports_presentation_ = false;
}
// Create command processor. This will spin up a thread to process all
@@ -1193,7 +1193,7 @@ RenderTargetCache::RenderTarget* RenderTargetCache::GetOrCreateRenderTarget(Rend
uint32_t width = key.GetWidth();
uint32_t height = GetRenderTargetHeight(key.pitch_tiles_at_32bpp, key.msaa_samples);
if (render_target) {
REXGPU_INFO(
REXGPU_DEBUG(
"Created a {}x{} {}xMSAA {} render target with guest format {} at "
"EDRAM base {}",
width, height, uint32_t(1) << uint32_t(key.msaa_samples),
@@ -24,11 +24,8 @@ std::string VulkanGraphicsSystem::name() const {
return "Vulkan";
}
X_STATUS VulkanGraphicsSystem::Setup(runtime::FunctionDispatcher* function_dispatcher,
system::KernelState* kernel_state,
ui::WindowedAppContext* app_context, bool with_presentation) {
void VulkanGraphicsSystem::CreateProvider(bool with_presentation) {
provider_ = rex::ui::vulkan::VulkanProvider::Create(true, with_presentation);
return GraphicsSystem::Setup(function_dispatcher, kernel_state, app_context, with_presentation);
}
std::unique_ptr<CommandProcessor> VulkanGraphicsSystem::CreateCommandProcessor() {
@@ -1471,7 +1471,7 @@ ppc_ptr_result_t InterlockedPushEntrySList_entry(ppc_ptr_t<X_SLIST_HEADER> plist
assert_not_null(entry);
alignas(8) X_SLIST_HEADER old_hdr = *plist_ptr;
alignas(8) X_SLIST_HEADER new_hdr = {0};
alignas(8) X_SLIST_HEADER new_hdr = {};
uint32_t old_head = 0;
do {
old_hdr = *plist_ptr;
@@ -27,23 +27,6 @@
#include <rex/system/xtypes.h>
#include <rex/ui/flags.h>
REXCVAR_DEFINE_INT32(video_mode_width, 1280, "GPU", "Guest video mode width in pixels")
.range(640, 0x0FFF)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_INT32(video_mode_height, 720, "GPU", "Guest video mode height in pixels")
.range(480, 0x0FFF)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_STRING(resolution, "", "GPU",
"Common resolution preset for both guest video mode and startup window (for "
"example: 720p, 1080p, 1440p, 4k, 1280x720)")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_DOUBLE(video_mode_refresh_rate, 60.0, "GPU", "Guest video mode refresh rate in Hz")
.range(24.0, 240.0)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
namespace {
// Display gamma type: 0 - linear, 1 - sRGB (CRT), 2 - BT.709 (HDTV), 3 - power
constexpr uint32_t kDisplayGammaType = 2;
@@ -4,24 +4,20 @@
#include <native/audio/render_driver_frame_layout.h>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <native/math.h>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/types.h>
REXCVAR_DEFINE_STRING(audio_render_driver_layout, "auto", "Audio",
"Layout for XAudio render-driver frames: auto, planar, or interleaved")
REXCVAR_DEFINE_STRING(audio_render_driver_layout, "planar", "Audio",
"Layout for XAudio render-driver frames: planar, interleaved, or auto")
.allowed({"auto", "planar", "interleaved"})
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
namespace rex::audio::conversion {
namespace {
std::atomic<int> g_detected_layout{0};
float DecodeSanitizedSample(const float* input, const size_t ch_sample_count, const size_t sample,
const size_t channel, const RenderDriverFrameLayout layout) {
const size_t index = layout == RenderDriverFrameLayout::kInterleaved
@@ -41,7 +37,6 @@ struct LayoutScore {
struct LayoutDetectionResult {
RenderDriverFrameLayout layout = RenderDriverFrameLayout::kPlanar;
bool cacheable = false;
};
LayoutScore ScoreLayout(const float* input, const size_t ch_sample_count,
@@ -77,15 +72,14 @@ LayoutDetectionResult DetectFrameLayout(const float* input, const size_t ch_samp
constexpr double kDecisionRatio = 0.75;
if (planar.continuity * kDecisionRatio < interleaved.continuity) {
return {RenderDriverFrameLayout::kPlanar, true};
return {RenderDriverFrameLayout::kPlanar};
}
if (interleaved.continuity * kDecisionRatio < planar.continuity) {
return {RenderDriverFrameLayout::kInterleaved, true};
return {RenderDriverFrameLayout::kInterleaved};
}
return {planar.continuity <= interleaved.continuity ? RenderDriverFrameLayout::kPlanar
: RenderDriverFrameLayout::kInterleaved,
false};
: RenderDriverFrameLayout::kInterleaved};
}
} // namespace
@@ -100,28 +94,7 @@ RenderDriverFrameLayout ResolveRenderDriverFrameLayout(const float* input,
return RenderDriverFrameLayout::kInterleaved;
}
const int cached_layout = g_detected_layout.load(std::memory_order_acquire);
if (cached_layout == 1) {
return RenderDriverFrameLayout::kPlanar;
}
if (cached_layout == 2) {
return RenderDriverFrameLayout::kInterleaved;
}
const LayoutDetectionResult detection = DetectFrameLayout(input, ch_sample_count);
if (!detection.cacheable) {
return detection.layout;
}
const RenderDriverFrameLayout detected_layout = detection.layout;
const int detected_value =
detected_layout == RenderDriverFrameLayout::kInterleaved ? 2 : 1;
const int previous =
g_detected_layout.exchange(detected_value, std::memory_order_acq_rel);
if (previous == 0) {
REXAPU_INFO("Audio render-driver layout auto-detected: {}", ToString(detected_layout));
}
return detected_layout;
return DetectFrameLayout(input, ch_sample_count).layout;
}
const char* ToString(const RenderDriverFrameLayout layout) {
@@ -20,7 +20,7 @@ NullDevice::NullDevice(const std::string& mount_path,
NullDevice::~NullDevice() = default;
bool NullDevice::Initialize() {
auto root_entry = new NullEntry(this, nullptr, mount_path_);
auto root_entry = new NullEntry(this, nullptr, "");
root_entry->attributes_ = kFileAttributeDirectory;
root_entry_ = std::unique_ptr<Entry>(root_entry);
@@ -37,7 +37,7 @@ void NullDevice::Dump(string::StringBuffer* string_buffer) {
}
Entry* NullDevice::ResolvePath(const std::string_view path) {
REXFS_INFO("NullDevice::ResolvePath({})", path);
REXFS_DEBUG("NullDevice::ResolvePath({})", path);
auto root = root_entry_.get();
if (path.empty()) {
@@ -185,7 +185,7 @@ 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);
REXFS_DEBUG("StfsContainerDevice::ResolvePath({})", path);
return root_entry_->ResolvePath(path);
}
@@ -123,12 +123,20 @@ Entry* VirtualFileSystem::ResolvePath(const std::string_view path) {
auto* entry = device->ResolvePath(relative_path);
if (entry) {
REXFS_INFO("VFS: '{}' -> '{}' -> device '{}' -> host '{}'", path,
had_symlink ? normalized_path : "(no symlink)", device->mount_path(),
entry->absolute_path());
if (had_symlink) {
REXFS_TRACE("VFS resolved '{}' via symlink '{}' on device '{}' -> '{}'", path,
normalized_path, device->mount_path(), entry->absolute_path());
} else {
REXFS_TRACE("VFS resolved '{}' on device '{}' -> '{}'", path, device->mount_path(),
entry->absolute_path());
}
} else {
REXFS_WARN("VFS: '{}' -> '{}' -> device '{}' -> [entry not found]", path,
had_symlink ? normalized_path : "(no symlink)", device->mount_path());
if (had_symlink) {
REXFS_WARN("VFS: entry not found for '{}' (via symlink '{}') on device '{}'", path,
normalized_path, device->mount_path());
} else {
REXFS_WARN("VFS: entry not found for '{}' on device '{}'", path, device->mount_path());
}
}
return entry;
@@ -16,10 +16,6 @@
namespace rex::ui::d3d12 {
namespace util {
const D3D12_HEAP_PROPERTIES kHeapPropertiesDefault = {D3D12_HEAP_TYPE_DEFAULT};
const D3D12_HEAP_PROPERTIES kHeapPropertiesUpload = {D3D12_HEAP_TYPE_UPLOAD};
const D3D12_HEAP_PROPERTIES kHeapPropertiesReadback = {D3D12_HEAP_TYPE_READBACK};
ID3D12RootSignature* CreateRootSignature(const D3D12Provider& provider,
const D3D12_ROOT_SIGNATURE_DESC& desc) {
ID3DBlob* blob;
+17
View File
@@ -32,6 +32,23 @@ REXCVAR_DEFINE_INT32(monitor, 0, "UI/Window",
.range(0, 16)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_INT32(video_mode_width, 1280, "GPU", "Guest video mode width in pixels")
.range(640, 0x0FFF)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_INT32(video_mode_height, 720, "GPU", "Guest video mode height in pixels")
.range(480, 0x0FFF)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_STRING(resolution, "", "GPU",
"Common resolution preset for both guest video mode and startup window (for "
"example: 720p, 1080p, 1440p, 4k, 1280x720)")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_DOUBLE(video_mode_refresh_rate, 60.0, "GPU", "Guest video mode refresh rate in Hz")
.range(24.0, 240.0)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
namespace rex {
namespace ui {
+2 -2
View File
@@ -1243,7 +1243,7 @@ LRESULT Win32Window::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPar
TABLET_DISABLE_TOUCHUIFORCEON | TABLET_ENABLE_MULTITOUCHDATA;
case WM_MENUCOMMAND: {
MENUINFO menu_info = {0};
MENUINFO menu_info = {};
menu_info.cbSize = sizeof(menu_info);
menu_info.fMask = MIM_MENUDATA;
GetMenuInfo(HMENU(lParam), &menu_info);
@@ -1331,7 +1331,7 @@ Win32MenuItem::Win32MenuItem(Type type, const std::string& text, const std::stri
break;
}
if (handle_) {
MENUINFO menu_info = {0};
MENUINFO menu_info = {};
menu_info.cbSize = sizeof(menu_info);
menu_info.fMask = MIM_MENUDATA | MIM_STYLE;
menu_info.dwMenuData = ULONG_PTR(this);
+4
View File
@@ -20,6 +20,8 @@ set(REXSYSTEM_SOURCES
xsymboliclink.cpp
xobject.cpp
xmodule.cpp
shared_library.cpp
guest_path.cpp
# Utilities
util/object_table.cpp
@@ -70,6 +72,8 @@ target_link_libraries(rexsystem
if(WIN32)
target_link_libraries(rexsystem PRIVATE ws2_32)
elseif(UNIX)
target_link_libraries(rexsystem PRIVATE dl)
endif()
# Propagate graphics backend selection
+229 -42
View File
@@ -19,10 +19,40 @@
#include <rex/memory.h>
#include <rex/ppc/context.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/kernel_state.h>
#include <rex/system/thread_state.h>
namespace rex::runtime {
namespace {
FunctionDispatcher* GetBoundFunctionDispatcher() {
auto* thread_state = ThreadState::Get();
if (!thread_state || !thread_state->context() || !thread_state->context()->kernel_state) {
return nullptr;
}
return thread_state->context()->kernel_state->function_dispatcher();
}
} // namespace
static void InvalidFunctionTrap(PPCContext& ctx, uint8_t* /*base*/) {
REX_FATAL("Call to invalid or unregistered indirect function (ctr=0x{:08X})", ctx.ctr.u32);
}
PPCFunc* ResolveIndirectFunction(uint32_t guest_address) {
FunctionDispatcher* dispatcher = GetBoundFunctionDispatcher();
if (!dispatcher) {
return &InvalidFunctionTrap;
}
if (PPCFunc* func = dispatcher->GetFunction(guest_address)) {
return func;
}
return &InvalidFunctionTrap;
}
FunctionDispatcher::FunctionDispatcher(rex::memory::Memory* memory, ExportResolver* export_resolver)
: memory_(memory), export_resolver_(export_resolver) {}
@@ -31,29 +61,29 @@ FunctionDispatcher::~FunctionDispatcher() = default;
bool FunctionDispatcher::Execute(ThreadState* thread_state, uint32_t address) {
SCOPE_profile_cpu_f("cpu");
// rexglue: Look up pre-compiled function
auto fn = GetFunction(address);
PPCFunc* fn = GetFunction(address);
if (!fn) {
REXCPU_ERROR("Execute({:08X}): function not in function table", address);
return false;
}
auto* ctx = thread_state->context();
auto* previous_thread_state = ThreadState::Get();
// Pad out stack a bit, as some games seem to overwrite the caller by about
// 16 to 32b.
// Rebind the active guest thread for cross-module callbacks.
ThreadState::Bind(thread_state);
// Pad out stack a bit, as some games seem to overwrite the caller by about 16 to 32b.
ctx->r1.u64 -= 64 + 112;
// This could be set to anything to give us a unique identifier to track
// re-entrancy/etc.
uint64_t previous_lr = ctx->lr;
ctx->lr = 0xBCBCBCBC;
// NOTE(tomc): rexglue direct function call
fn(*ctx, memory_->virtual_membase());
ctx->lr = previous_lr;
ctx->r1.u64 += 64 + 112;
ThreadState::Bind(previous_thread_state);
return true;
}
@@ -64,7 +94,6 @@ uint64_t FunctionDispatcher::Execute(ThreadState* thread_state, uint32_t address
auto* ctx = thread_state->context();
// Set up arguments (rexglue uses named registers)
if (arg_count > 0)
ctx->r3.u64 = args[0];
if (arg_count > 1)
@@ -82,9 +111,8 @@ uint64_t FunctionDispatcher::Execute(ThreadState* thread_state, uint32_t address
if (arg_count > 7)
ctx->r10.u64 = args[7];
// FIXME: stack-arg path assumes 32-bit values; 64-bit and float args are wrong.
if (arg_count > 8) {
// Rest of the arguments go on the stack.
// FIXME: This assumes arguments are 32 bits!
auto stack_arg_base =
memory_->TranslateVirtual(static_cast<uint32_t>(ctx->r1.u64) + 0x54 - (64 + 112));
for (size_t i = 8; i < arg_count; i++) {
@@ -104,14 +132,11 @@ uint64_t FunctionDispatcher::ExecuteInterrupt(ThreadState* thread_state, uint32_
SCOPE_profile_cpu_f("cpu");
// Hold the global lock during interrupt dispatch.
// This will block if any code is in a critical region (has interrupts
// disabled) or if any other interrupt is executing.
auto global_lock = global_critical_region_.Acquire();
auto* ctx = thread_state->context();
assert_true(arg_count <= 5);
// Set up arguments (rexglue uses named registers)
if (arg_count > 0)
ctx->r3.u64 = args[0];
if (arg_count > 1)
@@ -123,8 +148,8 @@ uint64_t FunctionDispatcher::ExecuteInterrupt(ThreadState* thread_state, uint32_
if (arg_count > 4)
ctx->r7.u64 = args[4];
// TLS ptr must be zero during interrupts. Some games check this and
// early-exit routines when under interrupts.
// TLS ptr must be zero during interrupts. Some games check this and early-exit
// routines when under interrupts.
auto pcr_address = memory_->TranslateVirtual(static_cast<uint32_t>(ctx->r13.u64));
uint32_t old_tls_ptr = memory::load_and_swap<uint32_t>(pcr_address);
memory::store_and_swap<uint32_t>(pcr_address, 0);
@@ -133,52 +158,110 @@ uint64_t FunctionDispatcher::ExecuteInterrupt(ThreadState* thread_state, uint32_
return 0xDEADBABE;
}
// Restores TLS ptr.
// Restore TLS ptr.
memory::store_and_swap<uint32_t>(pcr_address, old_tls_ptr);
return ctx->r3.u64;
}
// rexglue function table management
bool FunctionDispatcher::InitializeFunctionTable(uint32_t code_base, uint32_t code_size,
uint32_t image_base, uint32_t image_size) {
if (function_table_initialized_) {
REXLOG_WARN("Function table already initialized");
uint32_t image_base, uint32_t image_size,
bool is_entrypoint) {
std::lock_guard<std::recursive_mutex> lock(dispatch_mutex_);
if (is_entrypoint && entrypoint_code_base_ != 0) {
REXLOG_ERROR("InitializeFunctionTable: entrypoint already registered at {:08X}",
entrypoint_code_base_);
return false;
}
// Initialize the guest memory function table (for PPC_LOOKUP_FUNC in recompiled code)
uint32_t new_table_end = image_base + image_size + (code_size + kThunkReserveSize) * 2;
uint32_t new_code_end = code_base + code_size + kThunkReserveSize;
for (const auto& existing : module_tables_) {
uint32_t existing_table_end =
existing.image_base + existing.image_size + (existing.code_size + kThunkReserveSize) * 2;
uint32_t existing_code_end = existing.code_base + existing.code_size + kThunkReserveSize;
if (image_base < existing_table_end && new_table_end > existing.image_base) {
REXLOG_ERROR("Module image range [{:08X}, {:08X}) overlaps existing [{:08X}, {:08X})",
image_base, new_table_end, existing.image_base, existing_table_end);
return false;
}
if (code_base < existing_code_end && new_code_end > existing.code_base) {
REXLOG_ERROR("Module code range [{:08X}, {:08X}) overlaps existing [{:08X}, {:08X})",
code_base, new_code_end, existing.code_base, existing_code_end);
return false;
}
}
if (!memory_->InitializeFunctionTable(code_base, code_size, image_base, image_size)) {
REXLOG_ERROR("Failed to initialize guest memory function table");
return false;
}
code_base_ = code_base;
code_size_ = code_size;
image_base_ = image_base;
image_size_ = image_size;
function_table_initialized_ = true;
module_tables_.push_back({
.code_base = code_base,
.code_size = code_size,
.image_base = image_base,
.image_size = image_size,
.next_thunk_address = code_base + code_size,
.thunk_limit = code_base + code_size + kThunkReserveSize,
});
// Initialize thunk allocation region (immediately after code section)
next_thunk_address_ = code_base + code_size;
thunk_limit_ = next_thunk_address_ + 0x10000;
REXLOG_INFO(
"FunctionDispatcher function table initialized: code={:08X}-{:08X}, image={:08X}-{:08X}",
code_base, code_base + code_size, image_base, image_base + image_size);
if (is_entrypoint) {
entrypoint_code_base_ = code_base;
}
REXLOG_INFO("Function table initialized for module: code={:08X}-{:08X}, image={:08X}-{:08X}",
code_base, code_base + code_size, image_base, image_base + image_size);
return true;
}
void FunctionDispatcher::SetFunction(uint32_t guest_address, ::PPCFunc* func) {
assert_true(function_table_initialized_);
FunctionDispatcher::ModuleTableInfo* FunctionDispatcher::FindModuleByAddress(
uint32_t guest_address) {
for (auto& mod : module_tables_) {
if (guest_address >= mod.code_base && guest_address < mod.thunk_limit) {
return &mod;
}
}
return nullptr;
}
uint32_t FunctionDispatcher::FindCallerModuleBase(uint32_t guest_address) {
std::lock_guard<std::recursive_mutex> lock(dispatch_mutex_);
if (auto* mod = FindModuleByAddress(guest_address)) {
return mod->code_base;
}
return 0;
}
bool FunctionDispatcher::SetFunction(uint32_t guest_address, ::PPCFunc* func) {
std::lock_guard<std::recursive_mutex> lock(dispatch_mutex_);
assert_true(!module_tables_.empty());
if (!FindModuleByAddress(guest_address)) {
REXLOG_ERROR("SetFunction: address {:08X} outside all registered module ranges", guest_address);
return false;
}
// Store in C++ map (for FunctionDispatcher::Execute/GetFunction)
function_table_[guest_address] = func;
// Also write to guest memory (for PPC_LOOKUP_FUNC in recompiled code)
memory_->SetFunction(guest_address, func);
if (!memory_->SetFunction(guest_address, func)) {
REXLOG_ERROR("SetFunction: dispatcher / Memory module-table state out of sync at {:08X}",
guest_address);
function_table_.erase(guest_address);
return false;
}
if (recording_) {
recording_addresses_.push_back(guest_address);
}
return true;
}
::PPCFunc* FunctionDispatcher::GetFunction(uint32_t guest_address) {
std::lock_guard<std::recursive_mutex> lock(dispatch_mutex_);
auto it = function_table_.find(guest_address);
if (it != function_table_.end()) {
return it->second;
@@ -186,15 +269,119 @@ void FunctionDispatcher::SetFunction(uint32_t guest_address, ::PPCFunc* func) {
return nullptr;
}
uint32_t FunctionDispatcher::AllocateThunk(::PPCFunc* func) {
if (next_thunk_address_ >= thunk_limit_) {
REXLOG_ERROR("Thunk address space exhausted");
uint32_t FunctionDispatcher::AllocateThunk(::PPCFunc* func, uint32_t caller_address) {
std::lock_guard<std::recursive_mutex> lock(dispatch_mutex_);
auto* mod = FindModuleByAddress(caller_address);
if (!mod) {
if (caller_address != 0) {
REXLOG_ERROR("AllocateThunk: caller_address {:08X} not in any registered module",
caller_address);
return 0;
}
if (entrypoint_code_base_ == 0) {
REXLOG_ERROR("AllocateThunk: caller_address=0 but no entrypoint registered");
return 0;
}
mod = FindModuleByAddress(entrypoint_code_base_);
if (!mod) {
REXLOG_ERROR("AllocateThunk: entrypoint code_base {:08X} not in module_tables_",
entrypoint_code_base_);
return 0;
}
}
if (mod->next_thunk_address >= mod->thunk_limit) {
REXLOG_ERROR("Thunk address space exhausted for module at {:08X}", mod->code_base);
return 0;
}
uint32_t addr = mod->next_thunk_address;
mod->next_thunk_address += 4;
if (!SetFunction(addr, func)) {
mod->next_thunk_address -= 4;
return 0;
}
uint32_t addr = next_thunk_address_;
next_thunk_address_ += 4; // 4-byte aligned
SetFunction(addr, func);
return addr;
}
void FunctionDispatcher::RegisterModule(const std::string& module_id, uint32_t code_base,
RegisterFn register_func) {
std::lock_guard<std::recursive_mutex> lock(dispatch_mutex_);
if (recording_) {
REX_FATAL("RegisterModule called while already recording (re-entrancy)");
return;
}
if (module_addresses_.find(module_id) != module_addresses_.end()) {
REXLOG_WARN("RegisterModule: '{}' is already registered; cleaning up prior batch", module_id);
UnregisterModule(module_id);
}
REXLOG_INFO("Registering module: {} (code_base={:08X})", module_id, code_base);
recording_addresses_.clear();
recording_ = true;
struct RecordingGuard {
FunctionDispatcher* self;
~RecordingGuard() {
self->recording_ = false;
self->recording_addresses_.clear();
}
} guard{this};
register_func(this);
ModuleRegistration reg;
reg.code_base = code_base;
reg.addresses = std::move(recording_addresses_);
size_t count = reg.addresses.size();
module_addresses_[module_id] = std::move(reg);
REXLOG_INFO("Module '{}' registered {} functions", module_id, count);
}
std::optional<std::pair<uint32_t, uint32_t>> FunctionDispatcher::UnregisterModule(
const std::string& module_id) {
std::lock_guard<std::recursive_mutex> lock(dispatch_mutex_);
auto it = module_addresses_.find(module_id);
if (it == module_addresses_.end()) {
REXLOG_WARN("UnregisterModule: module '{}' not found", module_id);
return std::nullopt;
}
REXLOG_INFO("Unregistering module: {} ({} functions)", module_id, it->second.addresses.size());
auto table_it = std::find_if(module_tables_.begin(), module_tables_.end(),
[code_base = it->second.code_base](const ModuleTableInfo& mti) {
return mti.code_base == code_base;
});
for (uint32_t addr : it->second.addresses) {
function_table_.erase(addr);
memory_->SetFunction(addr, nullptr);
}
std::optional<std::pair<uint32_t, uint32_t>> cleared_range;
if (table_it != module_tables_.end()) {
uint32_t pool_start = table_it->code_base + table_it->code_size;
uint32_t pool_end = table_it->next_thunk_address;
for (uint32_t addr = pool_start; addr < pool_end; addr += 4) {
function_table_.erase(addr);
memory_->SetFunction(addr, nullptr);
}
cleared_range = std::make_pair(pool_start, pool_end);
if (table_it->code_base == entrypoint_code_base_) {
entrypoint_code_base_ = 0;
}
memory_->DestroyFunctionTable(table_it->code_base);
module_tables_.erase(table_it);
}
module_addresses_.erase(it);
return cleared_range;
}
} // namespace rex::runtime
+44
View File
@@ -0,0 +1,44 @@
/**
* @file system/guest_path.cpp
* @brief Guest path normalization for Xbox 360 VFS paths
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <rex/system/guest_path.h>
#include <algorithm>
#include <cctype>
#include <rex/string/utf8.h>
namespace rex::system {
std::string NormalizeGuestPath(std::string_view path) {
// Manifest / config consumers expect POSIX-style guest paths: forward
// slashes, no device prefix, lowercase. The runtime VFS tolerates either
// separator, so converting backslashes here keeps writers (TOML) happy.
std::string result = rex::string::utf8_fix_path_separators(path, U'/');
auto colon = result.find(':');
if (colon != std::string::npos && colon + 1 < result.size() && result[colon + 1] == '/') {
result.erase(0, colon + 2);
}
result = rex::string::utf8_canonicalize_path(result, U'/');
while (!result.empty() && result.front() == '/') {
result.erase(result.begin());
}
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
} // namespace rex::system
+28 -9
View File
@@ -1,13 +1,19 @@
/**
* 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 <rex/logging.h>
#include <rex/ppc/function.h>
#include <rex/system/kernel_module.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/kernel_state.h>
#include <rex/system/function_dispatcher.h>
#include <rex/thread/mutex.h>
namespace rex::system {
@@ -27,7 +33,7 @@ KernelModule::KernelModule(KernelState* kernel_state, const std::string_view pat
KernelModule::~KernelModule() {}
uint32_t KernelModule::GetProcAddressByOrdinal(uint16_t ordinal) {
uint32_t KernelModule::GetProcAddressByOrdinal(uint16_t ordinal, uint32_t caller_address) {
// Look up the export in the resolver
auto export_entry = export_resolver_->GetExportByOrdinal(name_, ordinal);
if (!export_entry) {
@@ -42,8 +48,12 @@ uint32_t KernelModule::GetProcAddressByOrdinal(uint16_t ordinal) {
return export_entry->variable_ptr;
}
// Check thunk cache first (already allocated)
auto thunk_it = thunk_cache_.find(ordinal);
auto* dispatcher = kernel_state_->function_dispatcher();
uint32_t caller_module_base = dispatcher->FindCallerModuleBase(caller_address);
ThunkKey key{caller_module_base, ordinal};
// Check thunk cache first (already allocated for this caller's module)
auto thunk_it = thunk_cache_.find(key);
if (thunk_it != thunk_cache_.end()) {
REXSYS_DEBUG("GetProcAddressByOrdinal: {} ({:04X}) in {} -> cached thunk {:08X}",
export_entry->name, ordinal, name_, thunk_it->second);
@@ -55,10 +65,9 @@ uint32_t KernelModule::GetProcAddressByOrdinal(uint16_t ordinal) {
REXSYS_DEBUG("GetProcAddressByOrdinal: searching registry for '{}'", imp_name);
PPCFunc* func = rex::FindPPCFuncByName(imp_name.c_str());
if (func) {
auto* dispatcher = kernel_state_->function_dispatcher();
uint32_t thunk_addr = dispatcher->AllocateThunk(func);
uint32_t thunk_addr = dispatcher->AllocateThunk(func, caller_address);
if (thunk_addr) {
thunk_cache_[ordinal] = thunk_addr;
thunk_cache_[key] = thunk_addr;
REXSYS_INFO("GetProcAddressByOrdinal: {} ({:04X}) in {} -> thunk at {:08X}",
export_entry->name, ordinal, name_, thunk_addr);
return thunk_addr;
@@ -78,4 +87,14 @@ uint32_t KernelModule::GetProcAddressByName(const std::string_view name) {
return 0;
}
void KernelModule::InvalidateThunkCacheInRange(uint32_t lo, uint32_t hi) {
for (auto it = thunk_cache_.begin(); it != thunk_cache_.end();) {
if (it->second >= lo && it->second < hi) {
it = thunk_cache_.erase(it);
} else {
++it;
}
}
}
} // namespace rex::system
+5 -1
View File
@@ -21,6 +21,10 @@ namespace rex::runtime {
MMIOHandler* MMIOHandler::global_handler_ = nullptr;
MMIOHandler* MMIOHandler::global_handler() {
return global_handler_;
}
std::unique_ptr<MMIOHandler> MMIOHandler::Install(uint8_t* virtual_membase,
uint8_t* physical_membase, uint8_t* membase_end,
HostToGuestVirtual host_to_guest_virtual,
@@ -38,7 +42,7 @@ std::unique_ptr<MMIOHandler> MMIOHandler::Install(uint8_t* virtual_membase,
host_to_guest_virtual_context, access_violation_callback, access_violation_callback_context));
// Install exception handler for memory coherence (SharedMemory write tracking).
// Note: MMIO operations are handled at the recompiler level via PPC_MM_LOAD/STORE
// Note: MMIO operations are handled at the recompiler level via REX_MM_LOAD/STORE
// macros that call CheckLoad/CheckStore directly.
arch::ExceptionHandler::Install(ExceptionCallbackThunk, handler.get());
+121
View File
@@ -0,0 +1,121 @@
/**
* @file system/shared_library.cpp
* @brief Platform-agnostic shared library loader
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <rex/system/shared_library.h>
#include <fmt/format.h>
#include <rex/assert.h>
#include <rex/filesystem.h>
#include <rex/logging.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif
namespace rex::system {
namespace {
#ifdef _WIN32
std::string FormatLastError(DWORD err) {
char* buffer = nullptr;
DWORD len = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast<LPSTR>(&buffer), 0,
nullptr);
std::string msg = (len && buffer) ? std::string(buffer, len) : std::string{};
if (buffer) {
LocalFree(buffer);
}
while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r' || msg.back() == ' ')) {
msg.pop_back();
}
return msg.empty() ? fmt::format("error {}", err) : fmt::format("{} (error {})", msg, err);
}
#endif
} // namespace
SharedLibrary::~SharedLibrary() {
Close();
}
SharedLibrary::SharedLibrary(SharedLibrary&& other) noexcept : handle_(other.handle_) {
other.handle_ = nullptr;
}
SharedLibrary& SharedLibrary::operator=(SharedLibrary&& other) noexcept {
if (this != &other) {
Close();
handle_ = other.handle_;
other.handle_ = nullptr;
}
return *this;
}
bool SharedLibrary::Load(const std::string& name) {
assert_true(handle_ == nullptr);
if (handle_) {
REXSYS_ERROR("SharedLibrary::Load called over a live handle");
return false;
}
auto exe_dir = rex::filesystem::GetExecutableFolder();
#ifdef _WIN32
std::string full_name = (exe_dir / (name + ".dll")).string();
handle_ = LoadLibraryA(full_name.c_str());
if (!handle_) {
DWORD err = GetLastError();
REXSYS_ERROR("Failed to load shared library '{}': {}", full_name, FormatLastError(err));
return false;
}
#else
std::string full_name = (exe_dir / ("lib" + name + ".so")).string();
handle_ = dlopen(full_name.c_str(), RTLD_NOW);
if (!handle_) {
const char* err = dlerror();
REXSYS_ERROR("Failed to load shared library '{}': {}", full_name, err ? err : "unknown");
return false;
}
#endif
return true;
}
void* SharedLibrary::GetSymbol(const char* name) {
if (!handle_)
return nullptr;
#ifdef _WIN32
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(handle_), name));
#else
return dlsym(handle_, name);
#endif
}
void SharedLibrary::Close() {
if (!handle_) {
return;
}
#ifdef _WIN32
if (!FreeLibrary(static_cast<HMODULE>(handle_))) {
REXSYS_ERROR("FreeLibrary failed: {}", FormatLastError(GetLastError()));
}
#else
if (dlclose(handle_) != 0) {
const char* err = dlerror();
REXSYS_ERROR("dlclose failed: {}", err ? err : "unknown");
}
#endif
handle_ = nullptr;
}
} // namespace rex::system
+2 -1
View File
@@ -52,7 +52,8 @@ ThreadState* ThreadState::Get() {
}
uint32_t ThreadState::GetThreadID() {
return thread_state_ ? thread_state_->thread_id_ : 0xFFFFFFFF;
auto* thread_state = Get();
return thread_state ? thread_state->thread_id_ : 0xFFFFFFFF;
}
} // namespace rex::runtime
+15 -3
View File
@@ -15,7 +15,8 @@
#include <rex/system/xfile.h>
#include <rex/system/xthread.h>
REXCVAR_DEFINE_BOOL(xex_apply_patches, true, "Kernel", "Apply XEX patches");
REXCVAR_DEFINE_BOOL(xex_apply_patches, false, "Kernel",
"Search for and apply XEX patches (path + 'p') on module load");
namespace rex::system {
@@ -233,8 +234,19 @@ X_STATUS UserModule::Unload() {
return X_STATUS_UNSUCCESSFUL;
}
uint32_t UserModule::GetProcAddressByOrdinal(uint16_t ordinal) {
return xex_module()->GetProcAddress(ordinal);
uint32_t UserModule::GetProcAddressByOrdinal(uint16_t ordinal, uint32_t caller_address) {
uint32_t guest_addr = xex_module()->GetProcAddress(ordinal);
if (!guest_addr || !caller_address) {
return guest_addr;
}
auto* dispatcher = kernel_state_->function_dispatcher();
auto* func = dispatcher->GetFunction(guest_addr);
if (!func) {
return guest_addr;
}
return dispatcher->AllocateThunk(func, caller_address);
}
uint32_t UserModule::GetProcAddressByName(std::string_view name) {
+1 -1
View File
@@ -53,7 +53,7 @@ XdbfBlock XdbfWrapper::GetEntry(XdbfSection section, uint64_t id) const {
return block;
}
}
return {0};
return {};
}
std::string XdbfWrapper::GetStringTableEntry(XLanguage language, uint16_t string_id) const {
+16 -8
View File
@@ -21,20 +21,28 @@ void AppManager::RegisterApp(std::unique_ptr<App> app) {
X_HRESULT AppManager::DispatchMessageSync(uint32_t app_id, uint32_t message, uint32_t buffer_ptr,
uint32_t buffer_length) {
const auto& it = app_lookup_.find(app_id);
if (it == app_lookup_.end()) {
return X_E_NOTFOUND;
App* app;
{
auto it = app_lookup_.find(app_id);
if (it == app_lookup_.end()) {
return X_E_NOTFOUND;
}
app = it->second;
}
return it->second->DispatchMessageSync(message, buffer_ptr, buffer_length);
return app->DispatchMessageSync(message, buffer_ptr, buffer_length);
}
X_HRESULT AppManager::DispatchMessageAsync(uint32_t app_id, uint32_t message, uint32_t buffer_ptr,
uint32_t buffer_length) {
const auto& it = app_lookup_.find(app_id);
if (it == app_lookup_.end()) {
return X_E_NOTFOUND;
App* app;
{
auto it = app_lookup_.find(app_id);
if (it == app_lookup_.end()) {
return X_E_NOTFOUND;
}
app = it->second;
}
return it->second->DispatchMessageSync(message, buffer_ptr, buffer_length);
return app->DispatchMessageSync(message, buffer_ptr, buffer_length);
}
} // namespace xam
+157 -64
View File
@@ -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
*/
#include <algorithm>
@@ -14,6 +20,8 @@
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/stream.h>
#include <rex/system/function_dispatcher.h>
#include <rex/system/mmio_handler.h>
#include <rex/system/xmemory.h>
#include <rex/thread.h>
@@ -120,7 +128,7 @@ Memory::~Memory() {
}
bool Memory::Initialize() {
file_name_ = fmt::format("rexglue_memory_{}", chrono::Clock::QueryHostTickCount());
file_name_ = fmt::format("xenia_memory_{}", chrono::Clock::QueryHostTickCount());
// Create main page file-backed mapping. This is all reserved but
// uncommitted (so it shouldn't expand page file).
@@ -668,6 +676,27 @@ void Memory::DumpMap() {
REXSYS_ERROR("");
}
bool Memory::Save(stream::ByteStream* stream) {
REXSYS_DEBUG("Serializing memory...");
heaps_.v00000000.Save(stream);
heaps_.v40000000.Save(stream);
heaps_.v80000000.Save(stream);
heaps_.v90000000.Save(stream);
heaps_.physical.Save(stream);
return true;
}
bool Memory::Restore(stream::ByteStream* stream) {
REXSYS_DEBUG("Restoring memory...");
heaps_.v00000000.Restore(stream);
heaps_.v40000000.Restore(stream);
heaps_.v80000000.Restore(stream);
heaps_.v90000000.Restore(stream);
heaps_.physical.Restore(stream);
return true;
}
//=============================================================================
// Recompiled Code Function Table
@@ -675,91 +704,87 @@ void Memory::DumpMap() {
bool Memory::InitializeFunctionTable(uint32_t code_base, uint32_t code_size, uint32_t image_base,
uint32_t image_size) {
if (function_table_base_ != 0) {
REXSYS_ERROR("Function table already initialized");
return false;
}
// The function table lives at IMAGE_BASE + IMAGE_SIZE in guest address space.
// Each 4-byte-aligned guest address gets an 8-byte slot for a host function pointer.
// Table size = (code_size + thunk_reserve) * 2 bytes (since offset = (addr - code_base) * 2).
// The thunk reserve provides space for runtime-allocated thunks (e.g. XexGetProcedureAddress).
constexpr uint32_t kThunkReserveSize = 0x10000; // 64KB = up to 16K thunks
function_table_base_ = image_base + image_size;
function_code_base_ = code_base;
function_code_size_ = code_size;
function_thunk_reserve_ = kThunkReserveSize;
constexpr uint32_t kThunkReserveSize = runtime::FunctionDispatcher::kThunkReserveSize;
uint32_t table_base = image_base + image_size;
uint32_t table_size = (code_size + kThunkReserveSize) * 2;
REXSYS_DEBUG(
"Initializing function table at {:08X}, size {:08X} for code {:08X}-{:08X} "
"(+{:08X} thunk reserve)",
function_table_base_, table_size, code_base, code_base + code_size, kThunkReserveSize);
table_base, table_size, code_base, code_base + code_size, kThunkReserveSize);
// Allocate the function table region in guest memory.
// Use the 64k page heap (v80000000) since that's where XEX code lives.
if (!heaps_.v80000000.AllocFixed(
function_table_base_, table_size, 0x10000,
table_base, table_size, 0x10000,
memory::kMemoryAllocationReserve | memory::kMemoryAllocationCommit,
memory::kMemoryProtectRead | memory::kMemoryProtectWrite)) {
REXSYS_ERROR("Failed to allocate function table at {:08X}", function_table_base_);
function_table_base_ = 0;
REXSYS_ERROR("Failed to allocate function table at {:08X}", table_base);
return false;
}
// Zero-initialize the table (nullptr for all entries).
Zero(function_table_base_, table_size);
Zero(table_base, table_size);
std::lock_guard lock(function_tables_mutex_);
function_tables_.push_back({
.table_base = table_base,
.code_base = code_base,
.code_size = code_size,
.thunk_reserve = kThunkReserveSize,
});
return true;
}
void Memory::SetFunction(uint32_t guest_address, PPCFunc* host_function) {
if (function_table_base_ == 0) {
REXSYS_ERROR("SetFunction called before InitializeFunctionTable");
return;
bool Memory::DestroyFunctionTable(uint32_t code_base) {
uint32_t table_base = 0;
uint32_t table_size = 0;
uint32_t logged_code_base = 0;
uint32_t logged_code_size = 0;
{
std::lock_guard lock(function_tables_mutex_);
auto it = std::find_if(
function_tables_.begin(), function_tables_.end(),
[code_base](const FunctionTableEntry& entry) { return entry.code_base == code_base; });
if (it == function_tables_.end()) {
return false;
}
table_base = it->table_base;
table_size = (it->code_size + it->thunk_reserve) * 2;
logged_code_base = it->code_base;
logged_code_size = it->code_size;
function_tables_.erase(it);
}
// Bounds check - addresses outside code section + thunk reserve are unexpected.
// IAT imports are called directly via __imp__ symbols, not through function table.
// Thunk reserve extends past code section for runtime-allocated thunks.
if (guest_address < function_code_base_ ||
guest_address >= function_code_base_ + function_code_size_ + function_thunk_reserve_) {
REXSYS_DEBUG("SetFunction: skipping {:08X} (outside code+thunk range [{:08X}, {:08X}))",
guest_address, function_code_base_,
function_code_base_ + function_code_size_ + function_thunk_reserve_);
return;
}
REXSYS_DEBUG("Destroying function table at {:08X}, size {:08X} for code {:08X}-{:08X}",
table_base, table_size, logged_code_base, logged_code_base + logged_code_size);
// Calculate table offset: (guest_addr - code_base) * 2
// This gives us the byte offset into the table for this 8-byte slot.
uint64_t offset = (uint64_t(guest_address) - function_code_base_) * 2;
uint32_t table_address = function_table_base_ + uint32_t(offset);
// Write the host function pointer to the table.
// The table is in guest memory but stores host pointers.
auto* slot = TranslateVirtual<PPCFunc**>(table_address);
*slot = host_function;
heaps_.v80000000.Release(table_base);
return true;
}
PPCFunc* Memory::GetFunction(uint32_t guest_address) const {
if (function_table_base_ == 0) {
return nullptr;
bool Memory::SetFunction(uint32_t guest_address, PPCFunc* host_function) {
uint32_t table_address = 0;
{
std::lock_guard lock(function_tables_mutex_);
for (const auto& entry : function_tables_) {
uint32_t range_end = entry.code_base + entry.code_size + entry.thunk_reserve;
if (guest_address >= entry.code_base && guest_address < range_end) {
uint64_t offset = (uint64_t(guest_address) - entry.code_base) * 2;
table_address = entry.table_base + uint32_t(offset);
break;
}
}
}
// Bounds check (includes thunk reserve for runtime-allocated thunks)
if (guest_address < function_code_base_ ||
guest_address >= function_code_base_ + function_code_size_ + function_thunk_reserve_) {
return nullptr;
if (!table_address) {
return false;
}
auto* slot = TranslateVirtual<PPCFunc**>(table_address);
*slot = host_function;
return true;
}
// Calculate table offset
uint64_t offset = (uint64_t(guest_address) - function_code_base_) * 2;
uint32_t table_address = function_table_base_ + uint32_t(offset);
// Read the host function pointer from the table.
auto* slot = const_cast<memory::Memory*>(this)->TranslateVirtual<PPCFunc**>(table_address);
return *slot;
bool Memory::HasAnyFunctionTable() const {
std::lock_guard lock(function_tables_mutex_);
return !function_tables_.empty();
}
rex::memory::PageAccess ToPageAccess(uint32_t protect) {
@@ -908,6 +933,74 @@ uint32_t BaseHeap::GetUnreservedPageCount() {
return count;
}
bool BaseHeap::Save(stream::ByteStream* stream) {
REXSYS_DEBUG("Heap {:08X}-{:08X}", heap_base_, heap_base_ + (heap_size_ - 1));
for (size_t i = 0; i < page_table_.size(); i++) {
auto& page = page_table_[i];
stream->Write(page.qword);
if (!page.state) {
// Unallocated.
continue;
}
// TODO(DrChat): write compressed with snappy.
if (page.state & memory::kMemoryAllocationCommit) {
void* addr = TranslateRelative(i << page_size_shift_);
memory::PageAccess old_access;
memory::Protect(addr, page_size_, memory::PageAccess::kReadWrite, &old_access);
stream->Write(addr, page_size_);
memory::Protect(addr, page_size_, old_access, nullptr);
}
}
return true;
}
bool BaseHeap::Restore(stream::ByteStream* stream) {
REXSYS_DEBUG("Heap {:08X}-{:08X}", heap_base_, heap_base_ + (heap_size_ - 1));
for (size_t i = 0; i < page_table_.size(); i++) {
auto& page = page_table_[i];
page.qword = stream->Read<uint64_t>();
if (!page.state) {
// Unallocated.
continue;
}
memory::PageAccess page_access = memory::PageAccess::kNoAccess;
if ((page.current_protect & memory::kMemoryProtectRead) &&
(page.current_protect & memory::kMemoryProtectWrite)) {
page_access = memory::PageAccess::kReadWrite;
} else if (page.current_protect & memory::kMemoryProtectRead) {
page_access = memory::PageAccess::kReadOnly;
}
// Commit the memory if it isn't already. We do not need to reserve any
// memory, as the mapping has already taken care of that.
if (page.state & memory::kMemoryAllocationCommit) {
rex::memory::AllocFixed(TranslateRelative(i << page_size_shift_), page_size_,
memory::AllocationType::kCommit, memory::PageAccess::kReadWrite);
}
// Now read into memory. We'll set R/W protection first, then set the
// protection back to its previous state.
// TODO(DrChat): read compressed with snappy.
if (page.state & memory::kMemoryAllocationCommit) {
void* addr = TranslateRelative(i << page_size_shift_);
rex::memory::Protect(addr, page_size_, memory::PageAccess::kReadWrite, nullptr);
stream->Read(addr, page_size_);
rex::memory::Protect(addr, page_size_, page_access, nullptr);
}
}
return true;
}
void BaseHeap::Reset() {
// TODO(DrChat): protect pages.
+11 -3
View File
@@ -101,16 +101,24 @@ XThread::~XThread() {
thread_local XThread* current_xthread_tls_ = nullptr;
namespace {
XThread* GetBoundCurrentXThread() {
return current_xthread_tls_;
}
} // namespace
bool XThread::IsInThread() {
return current_xthread_tls_ != nullptr;
return GetBoundCurrentXThread() != nullptr;
}
bool XThread::IsInThread(XThread* other) {
return current_xthread_tls_ == other;
return GetBoundCurrentXThread() == other;
}
XThread* XThread::GetCurrentThread() {
XThread* thread = reinterpret_cast<XThread*>(current_xthread_tls_);
XThread* thread = GetBoundCurrentXThread();
if (!thread) {
assert_always("Attempting to use kernel stuff from a non-kernel thread");
}

Some files were not shown because too many files have changed in this diff Show More