/** * @file rexcodegen/config.cpp * * @copyright Copyright (c) 2026 Tom Clay * 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 #include #include #include #include #include #include "codegen_logging.h" #include 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 ParseHexAddress(const std::string& keyStr) { try { if (keyStr.starts_with("0x") || keyStr.starts_with("0X")) { return static_cast(std::stoul(keyStr.substr(2), nullptr, 16)); } else { return static_cast(std::stoul(keyStr, nullptr, 16)); } } catch (...) { return std::nullopt; } } // --------------------------------------------------------------------------- // Scalar merge helpers -- log overrides at debug level // --------------------------------------------------------------------------- template void MergeScalar(T& dst, const T& src, const char* name) { if (src != T{} && src != dst) { if (dst != T{}) { if constexpr (std::is_same_v) { REXCODEGEN_DEBUG("[config] {}: {} -> {} (overridden)", name, dst, src); } else if constexpr (std::is_same_v) { 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()) { MergeScalar(cfg.projectName, *v, "project_name"); } if (auto v = toml["symbol_prefix"].value()) { MergeScalar(cfg.symbolPrefix, *v, "symbol_prefix"); } if (auto v = toml["file_path"].value()) { MergeScalar(cfg.filePath, *v, "file_path"); } if (auto v = toml["out_directory_path"].value()) { MergeScalar(cfg.outDirectoryPath, *v, "out_directory_path"); } if (auto v = toml["template_dir"].value()) { MergeScalar(cfg.templateDir, *v, "template_dir"); } if (auto v = toml["patch_file_path"].value()) { MergeScalar(cfg.patchFilePath, *v, "patch_file_path"); } if (auto v = toml["patched_file_path"].value()) { 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()) { uint32_t addr = static_cast(*v); MergeScalar(cfg.longJmpAddress, addr, "longjmp_address"); } if (auto v = toml["setjmp_address"].value()) { uint32_t addr = static_cast(*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()) { MergeScalar(cfg.maxJumpExtension, *v, "analysis.max_jump_extension"); } if (auto v = (*analysisTable)["data_region_threshold"].value()) { MergeScalar(cfg.dataRegionThreshold, *v, "analysis.data_region_threshold"); } if (auto v = (*analysisTable)["large_function_threshold"].value()) { 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()) { cfg.exceptionHandlerFuncHints.push_back(static_cast(*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()) { 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(*addr)); } cfg.rexcrtFunctions[key] = static_cast(*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(); auto size_opt = (*table)["size"].value(); 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(); auto register_opt = (*table)["register"].value(); 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(*register_opt); for (auto& label : *labels_array) { if (auto label_val = label.value()) { jt.targets.push_back(static_cast(*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(); auto name_opt = (*table)["name"].value(); 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()) { 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()) { cfg.knownIndirectCallHints.insert(static_cast(*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& visited, uint32_t depth, const std::string& description); bool LoadRecursive(const std::filesystem::path& filePath, RecompilerConfig& cfg, std::set& 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& 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()) { 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 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 visited; if (!ApplyTableWithIncludes(tbl, base_dir, *this, visited, 0, "")) { 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 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> 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 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