rexglue-skate3/src/codegen/config.cpp

625 lines
22 KiB
C++

/**
* @file rexcodegen/config.cpp
*
* @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 Based on XenonRecomp/UnleashedRecomp toml config
*/
// TOML config file loading
#include <algorithm>
#include <map>
#include <set>
#include <fmt/format.h>
#include <rex/codegen/config.h>
#include <rex/logging.h>
#include "codegen_logging.h"
#include <toml++/toml.hpp>
namespace rex::codegen {
namespace {
/// Maximum nesting depth for include chains.
constexpr uint32_t kMaxIncludeDepth = 32;
/// Parse a hex address string (with or without "0x"/"0X" prefix).
std::optional<uint32_t> ParseHexAddress(const std::string& keyStr) {
try {
if (keyStr.starts_with("0x") || keyStr.starts_with("0X")) {
return static_cast<uint32_t>(std::stoul(keyStr.substr(2), nullptr, 16));
} else {
return static_cast<uint32_t>(std::stoul(keyStr, nullptr, 16));
}
} catch (...) {
return std::nullopt;
}
}
// ---------------------------------------------------------------------------
// Scalar merge helpers -- log overrides at debug level
// ---------------------------------------------------------------------------
template <typename T>
void MergeScalar(T& dst, const T& src, const char* name) {
if (src != T{} && src != dst) {
if (dst != T{}) {
if constexpr (std::is_same_v<T, bool>) {
REXCODEGEN_DEBUG("[config] {}: {} -> {} (overridden)", name, dst, src);
} else if constexpr (std::is_same_v<T, std::string>) {
REXCODEGEN_DEBUG("[config] {}: \"{}\" -> \"{}\" (overridden)", name, dst, src);
} else {
REXCODEGEN_DEBUG("[config] {}: {} -> {} (overridden)", name, dst, src);
}
}
dst = src;
}
}
/// Overload for booleans where the "zero" state (false) is meaningful.
/// Only skip the merge when the overlay explicitly did not set the key.
void MergeBool(bool& dst, bool src, bool present, const char* name) {
if (!present)
return;
if (src != dst) {
REXCODEGEN_DEBUG("[config] {}: {} -> {} (overridden)", name, dst ? "true" : "false",
src ? "true" : "false");
}
dst = src;
}
// ---------------------------------------------------------------------------
// Apply a single parsed TOML table onto the config (merge semantics)
// ---------------------------------------------------------------------------
void ApplyToml(const toml::table& toml, RecompilerConfig& cfg, const std::string& filePath) {
// --- Scalars: last wins ---
// String scalars (only override if present in this file)
if (auto v = toml["project_name"].value<std::string>()) {
MergeScalar(cfg.projectName, *v, "project_name");
}
if (auto v = toml["symbol_prefix"].value<std::string>()) {
MergeScalar(cfg.symbolPrefix, *v, "symbol_prefix");
}
if (auto v = toml["file_path"].value<std::string>()) {
MergeScalar(cfg.filePath, *v, "file_path");
}
if (auto v = toml["out_directory_path"].value<std::string>()) {
MergeScalar(cfg.outDirectoryPath, *v, "out_directory_path");
}
if (auto v = toml["template_dir"].value<std::string>()) {
MergeScalar(cfg.templateDir, *v, "template_dir");
}
if (auto v = toml["patch_file_path"].value<std::string>()) {
MergeScalar(cfg.patchFilePath, *v, "patch_file_path");
}
if (auto v = toml["patched_file_path"].value<std::string>()) {
MergeScalar(cfg.patchedFilePath, *v, "patched_file_path");
}
// Bool scalars
auto hasBool = [&](const char* key) -> bool { return toml[key].is_boolean(); };
MergeBool(cfg.skipLr, toml["skip_lr"].value_or(false), hasBool("skip_lr"), "skip_lr");
MergeBool(cfg.skipMsr, toml["skip_msr"].value_or(false), hasBool("skip_msr"), "skip_msr");
MergeBool(cfg.ctrAsLocalVariable, toml["ctr_as_local"].value_or(false), hasBool("ctr_as_local"),
"ctr_as_local");
MergeBool(cfg.xerAsLocalVariable, toml["xer_as_local"].value_or(false), hasBool("xer_as_local"),
"xer_as_local");
MergeBool(cfg.reservedRegisterAsLocalVariable, toml["reserved_as_local"].value_or(false),
hasBool("reserved_as_local"), "reserved_as_local");
MergeBool(cfg.crRegistersAsLocalVariables, toml["cr_as_local"].value_or(false),
hasBool("cr_as_local"), "cr_as_local");
MergeBool(cfg.nonArgumentRegistersAsLocalVariables, toml["non_argument_as_local"].value_or(false),
hasBool("non_argument_as_local"), "non_argument_as_local");
MergeBool(cfg.nonVolatileRegistersAsLocalVariables, toml["non_volatile_as_local"].value_or(false),
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>()) {
uint32_t addr = static_cast<uint32_t>(*v);
MergeScalar(cfg.longJmpAddress, addr, "longjmp_address");
}
if (auto v = toml["setjmp_address"].value<int64_t>()) {
uint32_t addr = static_cast<uint32_t>(*v);
MergeScalar(cfg.setJmpAddress, addr, "setjmp_address");
}
// --- [analysis] section scalars ---
if (auto* analysisTable = toml["analysis"].as_table()) {
if (auto v = (*analysisTable)["max_jump_extension"].value<uint32_t>()) {
MergeScalar(cfg.maxJumpExtension, *v, "analysis.max_jump_extension");
}
if (auto v = (*analysisTable)["data_region_threshold"].value<uint32_t>()) {
MergeScalar(cfg.dataRegionThreshold, *v, "analysis.data_region_threshold");
}
if (auto v = (*analysisTable)["large_function_threshold"].value<uint32_t>()) {
MergeScalar(cfg.largeFunctionThreshold, *v, "analysis.large_function_threshold");
}
// exceptionHandlerFuncHints -- push_back then deduplicate at end
if (auto handlers = (*analysisTable)["exception_handler_funcs"].as_array()) {
for (const auto& elem : *handlers) {
if (auto val = elem.value<int64_t>()) {
cfg.exceptionHandlerFuncHints.push_back(static_cast<uint32_t>(*val));
}
}
}
}
// --- Keyed tables: additive, same key = last wins ---
// [rexcrt]
if (auto* rexcrt = toml["rexcrt"].as_table()) {
size_t added = 0;
for (const auto& [name, val] : *rexcrt) {
if (auto addr = val.value<int64_t>()) {
auto key = std::string(name);
auto it = cfg.rexcrtFunctions.find(key);
if (it != cfg.rexcrtFunctions.end()) {
REXCODEGEN_DEBUG("[config] [rexcrt] {} overridden: 0x{:08X} -> 0x{:08X}", key,
it->second, static_cast<uint32_t>(*addr));
}
cfg.rexcrtFunctions[key] = static_cast<uint32_t>(*addr);
++added;
}
}
if (added > 0) {
REXCODEGEN_DEBUG("[config] [rexcrt] added/updated {} entries from {}", added, filePath);
}
}
// [functions]
if (auto functionsTable = toml["functions"].as_table()) {
size_t added = 0;
for (auto& [key, value] : *functionsTable) {
std::string keyStr(key.str());
auto addrOpt = ParseHexAddress(keyStr);
if (!addrOpt) {
REXCODEGEN_ERROR("Invalid function address key: {}", keyStr);
continue;
}
uint32_t address = *addrOpt;
auto* table = value.as_table();
if (!table) {
REXCODEGEN_ERROR("Invalid [functions] entry at 0x{:08X}: expected table", address);
continue;
}
FunctionConfig fcfg;
fcfg.size = (*table)["size"].value_or(0u);
fcfg.end = (*table)["end"].value_or(0u);
fcfg.name = (*table)["name"].value_or(std::string{});
fcfg.parent = (*table)["parent"].value_or(0u);
if (fcfg.size && fcfg.end) {
REXCODEGEN_ERROR("Function 0x{:08X}: cannot specify both 'size' and 'end'", address);
continue;
}
if (fcfg.end && fcfg.end <= address) {
REXCODEGEN_ERROR("Function 0x{:08X}: 'end' (0x{:08X}) must be greater than address",
address, fcfg.end);
continue;
}
auto it = cfg.functions.find(address);
if (it != cfg.functions.end()) {
REXCODEGEN_DEBUG("[config] [functions] 0x{:08X} overridden from {}", address, filePath);
}
cfg.functions.insert_or_assign(address, std::move(fcfg));
++added;
}
if (added > 0) {
REXCODEGEN_DEBUG("[config] [functions] added/updated {} entries from {}", added, filePath);
}
}
// --- Arrays of tables: deduplicated by primary key (address), last wins ---
// [[invalid_instructions]] -- keyed by "data" address
if (auto invalidArray = toml["invalid_instructions"].as_array()) {
for (auto& entry : *invalidArray) {
auto* table = entry.as_table();
if (!table) {
REXCODEGEN_ERROR("Invalid [[invalid_instructions]] entry: expected table");
continue;
}
auto data_opt = (*table)["data"].value<uint32_t>();
auto size_opt = (*table)["size"].value<uint32_t>();
if (!data_opt) {
REXCODEGEN_ERROR("Missing 'data' in [[invalid_instructions]] entry");
continue;
}
if (!size_opt) {
REXCODEGEN_ERROR("Missing 'size' in [[invalid_instructions]] entry");
continue;
}
auto it = cfg.invalidInstructionHints.find(*data_opt);
if (it != cfg.invalidInstructionHints.end()) {
REXCODEGEN_DEBUG("[config] [[invalid_instructions]] 0x{:08X} overridden from {}",
*data_opt, filePath);
}
cfg.invalidInstructionHints.insert_or_assign(*data_opt, *size_opt);
}
}
// [[switch_tables]] -- keyed by "address"
if (auto switchTableArray = toml["switch_tables"].as_array()) {
for (auto& entry : *switchTableArray) {
auto* table = entry.as_table();
if (!table) {
REXCODEGEN_ERROR("Invalid [[switch_tables]] entry: expected table");
continue;
}
auto address_opt = (*table)["address"].value<uint32_t>();
auto register_opt = (*table)["register"].value<uint32_t>();
auto labels_array = (*table)["labels"].as_array();
if (!address_opt) {
REXCODEGEN_ERROR("Missing 'address' in [[switch_tables]] entry");
continue;
}
if (!register_opt) {
REXCODEGEN_ERROR("Missing 'register' in [[switch_tables]] entry");
continue;
}
if (!labels_array) {
REXCODEGEN_ERROR("Missing 'labels' in [[switch_tables]] entry");
continue;
}
JumpTable jt;
jt.bctrAddress = *address_opt;
jt.tableAddress = 0;
jt.indexRegister = static_cast<uint8_t>(*register_opt);
for (auto& label : *labels_array) {
if (auto label_val = label.value<int64_t>()) {
jt.targets.push_back(static_cast<uint32_t>(*label_val));
}
}
if (jt.targets.empty()) {
REXCODEGEN_ERROR("Empty 'labels' array in [[switch_tables]] at 0x{:08X}", *address_opt);
continue;
}
auto it = cfg.switchTables.find(*address_opt);
if (it != cfg.switchTables.end()) {
REXCODEGEN_DEBUG("[config] [[switch_tables]] 0x{:08X} overridden from {}", *address_opt,
filePath);
}
size_t label_count = jt.targets.size();
cfg.switchTables.insert_or_assign(*address_opt, std::move(jt));
REXCODEGEN_DEBUG("Loaded manual switch table at 0x{:08X} with {} labels", *address_opt,
label_count);
}
}
// [[midasm_hook]] -- keyed by "address"
if (auto midAsmHookArray = toml["midasm_hook"].as_array()) {
for (auto& entry : *midAsmHookArray) {
auto* table = entry.as_table();
if (!table) {
REXCODEGEN_ERROR("Invalid [[midasm_hook]] entry: expected table");
continue;
}
auto address_opt = (*table)["address"].value<uint32_t>();
auto name_opt = (*table)["name"].value<std::string>();
if (!address_opt) {
REXCODEGEN_ERROR("Missing 'address' in [[midasm_hook]] entry");
continue;
}
if (!name_opt) {
REXCODEGEN_ERROR("Missing 'name' in [[midasm_hook]] entry");
continue;
}
MidAsmHook midAsmHook;
midAsmHook.name = *name_opt;
if (auto registerArray = (*table)["registers"].as_array()) {
for (auto& reg : *registerArray) {
if (auto reg_str = reg.value<std::string>()) {
midAsmHook.registers.push_back(*reg_str);
}
}
}
midAsmHook.ret = (*table)["return"].value_or(false);
midAsmHook.returnOnTrue = (*table)["return_on_true"].value_or(false);
midAsmHook.returnOnFalse = (*table)["return_on_false"].value_or(false);
midAsmHook.jumpAddress = (*table)["jump_address"].value_or(0u);
midAsmHook.jumpAddressOnTrue = (*table)["jump_address_on_true"].value_or(0u);
midAsmHook.jumpAddressOnFalse = (*table)["jump_address_on_false"].value_or(0u);
if ((midAsmHook.ret && midAsmHook.jumpAddress != 0) ||
(midAsmHook.returnOnTrue && midAsmHook.jumpAddressOnTrue != 0) ||
(midAsmHook.returnOnFalse && midAsmHook.jumpAddressOnFalse != 0)) {
REXCODEGEN_ERROR("{}: can't return and jump at the same time", midAsmHook.name);
}
if ((midAsmHook.ret || midAsmHook.jumpAddress != 0) &&
(midAsmHook.returnOnFalse || midAsmHook.returnOnTrue ||
midAsmHook.jumpAddressOnFalse != 0 || midAsmHook.jumpAddressOnTrue != 0)) {
REXCODEGEN_ERROR("{}: can't mix direct and conditional return/jump", midAsmHook.name);
}
midAsmHook.afterInstruction = (*table)["after_instruction"].value_or(false);
auto it = cfg.midAsmHooks.find(*address_opt);
if (it != cfg.midAsmHooks.end()) {
REXCODEGEN_DEBUG("[config] [[midasm_hook]] 0x{:08X} overridden from {}", *address_opt,
filePath);
}
cfg.midAsmHooks.insert_or_assign(*address_opt, std::move(midAsmHook));
}
}
// --- Sets: additive ---
// indirect_calls -> knownIndirectCallHints (set)
if (auto indirectCallArray = toml["indirect_calls"].as_array()) {
for (auto& entry : *indirectCallArray) {
if (auto addr = entry.value<int64_t>()) {
cfg.knownIndirectCallHints.insert(static_cast<uint32_t>(*addr));
REXCODEGEN_DEBUG("Loaded known indirect call hint at 0x{:08X}", *addr);
}
}
}
}
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) {
if (depth > kMaxIncludeDepth) {
REXCODEGEN_ERROR("[config] include depth exceeds maximum ({}) at: {}", kMaxIncludeDepth,
filePath.string());
return false;
}
std::error_code ec;
auto canonical = std::filesystem::canonical(filePath, ec);
if (ec) {
REXCODEGEN_ERROR("[config] cannot resolve path '{}': {}", filePath.string(), ec.message());
return false;
}
std::string canonicalStr = canonical.string();
if (visited.contains(canonicalStr)) {
REXCODEGEN_ERROR("[config] circular include detected: {}", canonicalStr);
return false;
}
visited.insert(canonicalStr);
toml::table toml;
try {
toml = toml::parse_file(canonicalStr);
} catch (const toml::parse_error& e) {
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());
}
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 = base_dir / *includePath;
if (!std::filesystem::exists(resolved)) {
REXCODEGEN_ERROR("[config] included file not found: {} (resolved from {})", *includePath,
description);
return false;
}
if (!LoadRecursive(resolved, cfg, visited, depth + 1)) {
return false;
}
}
}
}
ApplyToml(tbl, cfg, description);
return true;
}
} // namespace
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
namespace {
bool FinalizeConfig(RecompilerConfig& cfg) {
if (!cfg.functions.empty()) {
size_t chunks_count = 0;
for (const auto& [addr, fc] : cfg.functions) {
if (fc.isChunk())
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());
}
bool ok = true;
if (cfg.filePath.empty()) {
REXCODEGEN_ERROR("Missing required field: file_path");
ok = false;
}
auto result = cfg.Validate();
for (const auto& warning : result.warnings) {
REXCODEGEN_WARN("[config] {}", warning);
}
for (const auto& error : result.errors) {
REXCODEGEN_ERROR("[config] {}", error);
}
return ok;
}
} // 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 {
ValidationResult result;
// Helper to check 4-byte alignment (PPC instructions are 4-byte aligned)
auto checkAlignment = [&](uint32_t addr, const char* name) {
if (addr != 0 && (addr & 0x3) != 0) {
result.errors.push_back(fmt::format("{} address 0x{:08X} is not 4-byte aligned", name, addr));
result.valid = false;
}
};
// Check special address alignment
checkAlignment(longJmpAddress, "longjmp");
checkAlignment(setJmpAddress, "setjmp");
for (const auto& [name, addr] : rexcrtFunctions) {
if (addr & 0x3) {
result.errors.push_back(
fmt::format("rexcrt function '{}' address 0x{:08X} is not 4-byte aligned", name, addr));
result.valid = false;
}
}
// Check function address alignment
for (const auto& [addr, size] : functions) {
if (addr & 0x3) {
result.errors.push_back(fmt::format("Function address 0x{:08X} is not 4-byte aligned", addr));
result.valid = false;
}
}
// Check for duplicate function boundaries
{
std::map<uint32_t, uint32_t> seen;
for (const auto& [addr, cfg] : functions) {
uint32_t funcSize = cfg.getSize(addr);
auto it = seen.find(addr);
if (it != seen.end()) {
if (it->second == funcSize) {
result.errors.push_back(
fmt::format("Duplicate function boundary: 0x{:08X} size 0x{:X}", addr, funcSize));
} else {
result.errors.push_back(fmt::format("Conflicting sizes at 0x{:08X}: 0x{:X} vs 0x{:X}",
addr, it->second, funcSize));
}
result.valid = false;
}
seen[addr] = funcSize;
}
}
// Check for overlapping function boundaries (standalone functions only)
{
std::vector<std::pair<uint32_t, uint32_t>> sorted;
for (const auto& [addr, cfg] : functions) {
if (!cfg.isChunk()) {
sorted.emplace_back(addr, cfg.getSize(addr));
}
}
std::sort(sorted.begin(), sorted.end());
for (size_t i = 1; i < sorted.size(); ++i) {
uint32_t prev_end = sorted[i - 1].first + sorted[i - 1].second;
if (sorted[i].first < prev_end) {
result.errors.push_back(fmt::format(
"Overlapping boundaries: 0x{:08X}+0x{:X} overlaps 0x{:08X}+0x{:X}", sorted[i - 1].first,
sorted[i - 1].second, sorted[i].first, sorted[i].second));
result.valid = false;
}
}
}
// Validate rexcrt all-or-nothing groups -- originals are stripped so partial
// sets would leave the game with missing CRT functions at runtime.
if (!rexcrtFunctions.empty()) {
auto checkGroup = [&](const char* groupName, std::initializer_list<const char*> required) {
size_t found = 0;
for (const char* name : required) {
if (rexcrtFunctions.contains(name))
++found;
}
if (found > 0 && found < required.size()) {
std::string missing;
for (const char* name : required) {
if (!rexcrtFunctions.contains(name)) {
if (!missing.empty())
missing += ", ";
missing += name;
}
}
result.errors.push_back(
fmt::format("[rexcrt] {} group is incomplete ({}/{} specified), missing: {}", groupName,
found, required.size(), missing));
result.valid = false;
}
};
checkGroup("heap", {"RtlAllocateHeap", "RtlFreeHeap", "RtlSizeHeap", "RtlReAllocateHeap"});
}
// Check required fields
if (filePath.empty()) {
result.warnings.push_back("file_path is empty");
}
if (outDirectoryPath.empty()) {
result.warnings.push_back("out_directory_path is empty");
}
return result;
}
} // namespace rex::codegen