mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-03 09:03:03 -04:00
Upgraded to RexGlue v0.8 and edited a bit of the Audio code
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
# Global owners
|
||||
* @tomcl7
|
||||
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
//=========================================================================
|
||||
|
||||
@@ -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
@@ -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
@@ -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 = ®istry.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 {
|
||||
|
||||
+3
-3
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
//=============================================================================
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
|
||||
+92
-15
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.");
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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()) {
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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());
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user