mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-03 09:03:03 -04:00
388 lines
12 KiB
C++
388 lines
12 KiB
C++
/**
|
|
* @file system/function_dispatcher.cpp
|
|
* @brief Guest function dispatch coordinator for recompiled code
|
|
*
|
|
* @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.
|
|
*
|
|
* @remarks Derived from Xenia's runtime::Processor (Ben Vanik, 2020).
|
|
* Stripped of emulation-era dead code and renamed to reflect its
|
|
* role as a function dispatch table rather than a CPU emulator.
|
|
*/
|
|
|
|
#include <rex/assert.h>
|
|
#include <rex/dbg.h>
|
|
#include <rex/logging.h>
|
|
#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) {}
|
|
|
|
FunctionDispatcher::~FunctionDispatcher() = default;
|
|
|
|
bool FunctionDispatcher::Execute(ThreadState* thread_state, uint32_t address) {
|
|
SCOPE_profile_cpu_f("cpu");
|
|
|
|
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();
|
|
|
|
// 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;
|
|
|
|
uint64_t previous_lr = ctx->lr;
|
|
ctx->lr = 0xBCBCBCBC;
|
|
|
|
fn(*ctx, memory_->virtual_membase());
|
|
|
|
ctx->lr = previous_lr;
|
|
ctx->r1.u64 += 64 + 112;
|
|
ThreadState::Bind(previous_thread_state);
|
|
|
|
return true;
|
|
}
|
|
|
|
uint64_t FunctionDispatcher::Execute(ThreadState* thread_state, uint32_t address, uint64_t args[],
|
|
size_t arg_count) {
|
|
SCOPE_profile_cpu_f("cpu");
|
|
|
|
auto* ctx = thread_state->context();
|
|
|
|
if (arg_count > 0)
|
|
ctx->r3.u64 = args[0];
|
|
if (arg_count > 1)
|
|
ctx->r4.u64 = args[1];
|
|
if (arg_count > 2)
|
|
ctx->r5.u64 = args[2];
|
|
if (arg_count > 3)
|
|
ctx->r6.u64 = args[3];
|
|
if (arg_count > 4)
|
|
ctx->r7.u64 = args[4];
|
|
if (arg_count > 5)
|
|
ctx->r8.u64 = args[5];
|
|
if (arg_count > 6)
|
|
ctx->r9.u64 = args[6];
|
|
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) {
|
|
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++) {
|
|
memory::store_and_swap<uint32_t>(stack_arg_base + ((i - 8) * 8),
|
|
static_cast<uint32_t>(args[i]));
|
|
}
|
|
}
|
|
|
|
if (!Execute(thread_state, address)) {
|
|
return 0xDEADBABE;
|
|
}
|
|
return ctx->r3.u64;
|
|
}
|
|
|
|
uint64_t FunctionDispatcher::ExecuteInterrupt(ThreadState* thread_state, uint32_t address,
|
|
uint64_t args[], size_t arg_count) {
|
|
SCOPE_profile_cpu_f("cpu");
|
|
|
|
// Hold the global lock during interrupt dispatch.
|
|
auto global_lock = global_critical_region_.Acquire();
|
|
|
|
auto* ctx = thread_state->context();
|
|
assert_true(arg_count <= 5);
|
|
|
|
if (arg_count > 0)
|
|
ctx->r3.u64 = args[0];
|
|
if (arg_count > 1)
|
|
ctx->r4.u64 = args[1];
|
|
if (arg_count > 2)
|
|
ctx->r5.u64 = args[2];
|
|
if (arg_count > 3)
|
|
ctx->r6.u64 = args[3];
|
|
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.
|
|
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);
|
|
|
|
if (!Execute(thread_state, address)) {
|
|
return 0xDEADBABE;
|
|
}
|
|
|
|
// 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,
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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,
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function_table_[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;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
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;
|
|
}
|
|
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
|