// config_loader.cpp — see config_loader.h for the contract. #include "config_loader.h" #include #include #include #include #include #include #include #include #include #include #include #include "bios_rom_alias.h" #include "../../runtime/include/internal_resolution.h" #include "host_path.h" #include "fmt/format.h" #include "ps1_exe_parser.h" // toml11 is header-only. #define TOML11_USE_UNRELEASED_TOML_FEATURES #include "toml.hpp" namespace PSXRecompV4 { namespace fs = std::filesystem; namespace { struct ConfigHash { uint32_t value = 2166136261u; // FNV-1a 32 void byte(uint8_t v) { value ^= v; value *= 16777619u; } void u32(uint32_t v) { byte((uint8_t)(v >> 0)); byte((uint8_t)(v >> 8)); byte((uint8_t)(v >> 16)); byte((uint8_t)(v >> 24)); } void tag(const char *s) { while (*s) byte((uint8_t)*s++); byte(0); } void words(const char *name, std::vector values) { tag(name); std::sort(values.begin(), values.end()); values.erase(std::unique(values.begin(), values.end()), values.end()); u32((uint32_t)values.size()); for (uint32_t value : values) u32(value); } }; } // namespace uint32_t overlay_codegen_config_hash(const GameConfig& c) { ConfigHash h; h.tag("psxrecomp-overlay-config-v1"); h.words("sprite_tag_funcs", c.ws_sprite_tag_funcs); h.words("mod_function_entry_funcs", c.mod_function_entry_funcs); if (!c.ws_cull_packed_x_sites.empty()) { h.tag("cull_packed_x"); auto sites = c.ws_cull_packed_x_sites; std::sort(sites.begin(), sites.end(), [](const auto& a, const auto& b) { return (a.address & 0x1FFFFFFFu) < (b.address & 0x1FFFFFFFu); }); h.u32((uint32_t)sites.size()); for (const auto& site : sites) { h.u32(site.address & 0x1FFFFFFFu); h.u32(site.expected); } } if (!c.ws_cull_masked_reject_sites.empty()) { h.tag("cull_masked_reject"); auto sites = c.ws_cull_masked_reject_sites; std::sort(sites.begin(), sites.end(), [](const auto& a, const auto& b) { return (a.address & 0x1FFFFFFFu) < (b.address & 0x1FFFFFFFu); }); h.u32((uint32_t)sites.size()); for (const auto& site : sites) { h.u32(site.address & 0x1FFFFFFFu); h.u32(site.expected); h.u32(site.reject_mask); } } // Appended only when used, like the other late additions: a config that // does not use the feature keeps the hash (and every overlay cache) it had. if (!c.mod_instruction_sites.empty()) h.words("mod_instruction_sites", c.mod_instruction_sites); if (!c.mod_immediate_sites.empty()) { // Appended only when used (same rule as above). Order-independent. auto sites = c.mod_immediate_sites; std::sort(sites.begin(), sites.end(), [](const auto& x, const auto& y) { const uint32_t ax = x.address & 0x1FFFFFFFu, ay = y.address & 0x1FFFFFFFu; return ax != ay ? ax < ay : x.expected < y.expected; }); h.tag("mod_immediate_sites"); h.u32((uint32_t)sites.size()); for (const auto& site : sites) { h.u32(site.address & 0x1FFFFFFFu); h.u32(site.expected); h.u32(site.group); h.u32((uint32_t)site.values.size()); for (const uint16_t v : site.values) h.u32(v); } } h.words("cull_bias", c.ws_cull_bias_sites); if (!c.ws_cull_bias_lower_sites.empty()) h.words("cull_bias_lower", c.ws_cull_bias_lower_sites); h.words("cull_range", c.ws_cull_range_sites); h.words("cull_a1", c.ws_cull_a1_sites); h.words("cull_screen_x", c.ws_cull_screen_x_sites); h.words("cull_slti", c.ws_cull_slti_sites); h.words("cull_slti_lower", c.ws_cull_slti_lower_sites); h.words("cull_bltz", c.ws_cull_bltz_sites); h.words("cull_negsub", c.ws_cull_negsub_sites); h.words("cull_vxrange", c.ws_cull_vxrange_sites); h.words("cull_depth", c.ws_cull_depth_sites); h.words("cull_plane_nx", c.ws_cull_plane_nx_sites); h.words("cull_xclip_load", c.ws_cull_xclip_load_sites); h.words("cull_nclip_keep", c.ws_cull_nclip_keep_sites); h.words("cull_nclip_exact", c.ws_cull_nclip_exact_sites); h.words("cull_branch_keep", c.ws_cull_branch_keep_sites); h.words("cull_w_imms", c.ws_cull_w_imms); h.words("cull_h_imms", c.ws_cull_h_imms); h.words("backdrop_x", c.ws_backdrop_x_sites); h.words("backdrop_unsquash", c.ws_backdrop_unsquash_funcs); // Kinds added after the v1 hash layout enter the identity only when a // title uses them, so every existing overlay cache stays valid. if (!c.ws_cull_bgez_sites.empty()) h.words("cull_bgez", c.ws_cull_bgez_sites); if (!c.ws_cull_clip_edge_x_load_sites.empty()) { h.words("cull_clip_edge_x_load", c.ws_cull_clip_edge_x_load_sites); h.u32(ws_cull_clip_edge_width(c)); } if (!c.netplay_local_viewport_width_sites.empty()) h.words("local_viewport_width", c.netplay_local_viewport_width_sites); h.tag("flags"); h.u32(c.ws_auto_screen_x_cull ? 1u : 0u); h.u32(c.ws_auto_backdrop_preload ? 1u : 0u); h.u32(c.ws_bg2d_init_func); h.u32((uint32_t)c.ws_cull_guard_pixels); h.u32((uint32_t)c.ws_cull_activation_guard_pixels); std::vector signed_sites = c.ws_signed_x_bound_sites; std::sort(signed_sites.begin(), signed_sites.end(), [](const auto& a, const auto& b) { if (a.address != b.address) return a.address < b.address; return a.expected < b.expected; }); h.tag("signed_x_bounds"); h.u32((uint32_t)signed_sites.size()); for (const auto& site : signed_sites) { h.u32(site.address); h.u32(site.expected); } std::vector keep_sites = c.ws_cull_keep_sites; std::sort(keep_sites.begin(), keep_sites.end(), [](const auto& a, const auto& b) { if (a.address != b.address) return a.address < b.address; if (a.expected != b.expected) return a.expected < b.expected; return a.result < b.result; }); h.tag("cull_keep"); h.u32((uint32_t)keep_sites.size()); for (const auto& site : keep_sites) { h.u32(site.address); h.u32(site.expected); h.u32(site.result); } // Appended only when used, so every other title's hash is unchanged. if (!c.ws_cull_sxy_sites.empty()) { std::vector sxy_sites = c.ws_cull_sxy_sites; std::sort(sxy_sites.begin(), sxy_sites.end(), [](const auto& a, const auto& b) { return a.fold_addresses.front() < b.fold_addresses.front(); }); h.tag("cull_sxy"); h.u32((uint32_t)sxy_sites.size()); for (const auto& site : sxy_sites) { h.u32(site.vertices); for (size_t i = 0; i < site.vertices; i++) { h.u32(site.fold_addresses[i]); h.u32(site.fold_expected[i]); h.u32(site.vertex_regs[i]); } h.u32(site.consumer_address); h.u32(site.consumer_expected); } } std::vector scale_sites = c.ws_cull_scale_sites; std::sort(scale_sites.begin(), scale_sites.end(), [](const auto& a, const auto& b) { return a.address < b.address; }); if (!scale_sites.empty()) { // appended only when used (see above) h.tag("cull_scale"); h.u32((uint32_t)scale_sites.size()); for (const auto& site : scale_sites) { h.u32(site.address); h.u32(site.expected); h.u32(site.operand); h.u32(site.half_extent); } } std::vector angle_sites = c.ws_cull_angle_sites; std::sort(angle_sites.begin(), angle_sites.end(), [](const auto& a, const auto& b) { if (a.address != b.address) return a.address < b.address; return a.expected < b.expected; }); h.tag("cull_angle"); h.u32((uint32_t)angle_sites.size()); for (const auto& site : angle_sites) { h.u32(site.address); h.u32(site.expected); } std::vector cone_sites = c.ws_aspect_cone.sites; std::sort(cone_sites.begin(), cone_sites.end(), [](const auto& a, const auto& b) { if (a.address != b.address) return a.address < b.address; return a.expected < b.expected; }); h.tag("aspect_cone"); h.u32((uint32_t)cone_sites.size()); for (const auto& site : cone_sites) { h.u32(site.address); h.u32(site.expected); h.u32(site.cosine_threshold); h.u32(site.object_reg); h.u32(site.x_reg); h.u32(site.z_reg); h.u32(site.y_reg); h.u32(site.queue_guard ? 1u : 0u); } h.u32(c.ws_aspect_cone.forward_addr); h.u32(c.ws_aspect_cone.object_type_offset); h.u32(c.ws_aspect_cone.object_reg); h.u32(c.ws_aspect_cone.x_reg); h.u32(c.ws_aspect_cone.z_reg); h.u32(c.ws_aspect_cone.y_reg); h.u32(c.ws_aspect_cone.hysteresis_pixels); h.u32(c.ws_aspect_cone.queue_reserve); for (uint32_t value : c.ws_aspect_cone.queue_count_addrs) h.u32(value); for (uint32_t value : c.ws_aspect_cone.queue_capacities) h.u32(value); for (uint32_t value : c.ws_aspect_cone.queue_type_masks) h.u32(value); std::vector patches = c.recompiler_patches; std::sort(patches.begin(), patches.end(), [](const auto& a, const auto& b) { const uint32_t aa = recompiler_patch_address_key(a.address); const uint32_t ba = recompiler_patch_address_key(b.address); if (aa != ba) return aa < ba; if (a.expected != b.expected) return a.expected < b.expected; return a.replacement < b.replacement; }); h.tag("instruction_patches"); h.u32((uint32_t)patches.size()); for (const auto& patch : patches) { h.u32(recompiler_patch_address_key(patch.address)); h.u32(patch.expected); h.u32(patch.replacement); } h.tag("load_charge_batch"); h.u32(c.load_charge_batch ? 1u : 0u); { std::vector lcb = c.load_charge_batch_funcs; std::sort(lcb.begin(), lcb.end()); h.u32((uint32_t)lcb.size()); for (uint32_t pc : lcb) h.u32(pc); } return h.value; } // Pad mode <-> string. Accepts "hybrid"/"analog"/"digital" (case-insensitive); // returns `fallback` for anything unrecognised so a typo never silently flips // the pad type. int pad_mode_from_string(const std::string& s, int fallback) { std::string l; l.reserve(s.size()); for (char c : s) l.push_back((char)std::tolower((unsigned char)c)); /* "hybrid" used to be a framework mode. A game.toml that declares it is * now a configuration error: game-specific auto-switching belongs in an * enabled trusted plugin policy, not in the global launcher/config surface. */ if (l == "hybrid") throw std::runtime_error( "[controller] pad mode \"hybrid\" is not selectable. Hybrid is a " "game-owned mod policy, not a game.toml value. Use \"analog\" or " "\"digital\" here."); if (l == "analog") return PAD_MODE_ANALOG; if (l == "digital") return PAD_MODE_DIGITAL; return fallback; } /* Settings-file variant: a user's settings.toml may still carry a persisted * "hybrid" from before the mode was removed from the selector. Migrate it to * analog (what the old allow_hybrid clamp did) rather than refusing to * launch over a value the user never typed. */ int pad_mode_from_settings_string(const std::string& s, int fallback) { std::string l; l.reserve(s.size()); for (char c : s) l.push_back((char)std::tolower((unsigned char)c)); if (l == "hybrid") return PAD_MODE_ANALOG; if (l == "analog") return PAD_MODE_ANALOG; if (l == "digital") return PAD_MODE_DIGITAL; return fallback; } const char* pad_mode_to_string(int mode) { /* Never writes "hybrid": the mod requests it at runtime and it is not * persisted as a user preference. */ switch (mode) { case PAD_MODE_DIGITAL: return "digital"; default: return "analog"; } } // Parse a hex string ("0x...") to uint32_t. Throws on malformed input. /* A TOML number key: `0` and `0.0` both mean zero. toml::find * rejects an integer literal and failed the whole load on `key = 0`. */ static double toml_number(const toml::value& table, const char* key) { const toml::value& v = toml::find(table, key); return v.is_integer() ? static_cast(v.as_integer()) : v.as_floating(); } static uint32_t parse_hex(const std::string& s, const std::string& field) { try { return static_cast(std::stoul(s, nullptr, 16)); } catch (const std::exception& ex) { throw std::runtime_error( fmt::format("config field '{}': expected hex string, got '{}' ({})", field, s, ex.what())); } } // One [[widescreen.cull.sxy]] entry. The fold and its consumer are identified // by their exact instruction words; everything the runtime relies on is // derived from those words and must agree with the declared registers: // fold[0] = and r, v0, mask (operands in either order) // fold[k] = and r, vk, r (k >= 1, operands in either order) // consumer = beq/bne r, zero (within 4 instructions after the last AND) // `seen` holds the physical addresses claimed by earlier entries. static WidescreenSxyCullSite parse_sxy_cull_site(const toml::value& item, const std::string& path, std::set& seen) { auto fail = [&](const std::string& why) { return std::runtime_error(fmt::format( "{}: [[widescreen.cull.sxy]] {}", path, why)); }; WidescreenSxyCullSite site; const std::string kind = toml::find(item, "kind"); if (kind == "tri") site.vertices = 3; else if (kind == "quad") site.vertices = 4; else throw fail("kind must be \"tri\" or \"quad\""); for (int reg : toml::find>(item, "vertex_regs")) { if (reg < 1 || reg > 31) throw fail(fmt::format("vertex_regs: {} is not a GPR 1..31", reg)); site.vertex_regs.push_back((uint32_t)reg); } for (const auto& a : toml::find>(item, "fold_addresses")) site.fold_addresses.push_back(parse_hex(a, "widescreen.cull.sxy.fold_addresses")); for (const auto& e : toml::find>(item, "fold_expected")) site.fold_expected.push_back(parse_hex(e, "widescreen.cull.sxy.fold_expected")); site.consumer_address = parse_hex(toml::find(item, "consumer_address"), "widescreen.cull.sxy.consumer_address"); site.consumer_expected = parse_hex(toml::find(item, "consumer_expected"), "widescreen.cull.sxy.consumer_expected"); const size_t n = site.vertices; if (site.vertex_regs.size() != n || site.fold_addresses.size() != n || site.fold_expected.size() != n) throw fail(fmt::format("{} needs exactly {} vertex_regs, fold_addresses " "and fold_expected", kind, n)); for (size_t i = 0; i < n; i++) for (size_t j = i + 1; j < n; j++) if (site.vertex_regs[i] == site.vertex_regs[j]) throw fail("vertex_regs must be distinct"); // Addresses: aligned, one segment, strictly increasing, and short. const uint32_t segment = site.fold_addresses[0] & 0xE0000000u; uint32_t prev = 0; for (size_t i = 0; i <= n; i++) { const uint32_t a = i < n ? site.fold_addresses[i] : site.consumer_address; if ((a & 3u) != 0 || (a & 0xE0000000u) != segment) throw fail(fmt::format("0x{:08X} must be word-aligned and in the " "segment of the first AND", a)); if (i > 0 && a <= prev) throw fail(fmt::format("0x{:08X} must follow 0x{:08X}", a, prev)); if (!seen.insert(a & 0x1FFFFFFFu).second) throw fail(fmt::format("address 0x{:08X} is already used by " "another sxy site", a)); prev = a; } if (site.fold_addresses[n - 1] - site.fold_addresses[0] > 4u * 15u) throw fail("the AND fold spans more than 16 instructions"); if (site.consumer_address - site.fold_addresses[n - 1] > 16u) throw fail("consumer_address must be within 4 instructions of the last AND"); // The words: one AND chain into a single register r. auto rs_of = [](uint32_t w) { return (w >> 21) & 31u; }; auto rt_of = [](uint32_t w) { return (w >> 16) & 31u; }; const uint32_t r = (site.fold_expected[0] >> 11) & 31u; if (r == 0) throw fail("the AND fold must not write $zero"); for (size_t i = 0; i < n; i++) { const uint32_t w = site.fold_expected[i]; const uint32_t v = site.vertex_regs[i]; if ((w >> 26) != 0u || (w & 0x7FFu) != 0x24u || ((w >> 11) & 31u) != r) throw fail(fmt::format("fold_expected 0x{:08X} must be `and r, ...` " "with the same destination as the first AND", w)); if (v == r) throw fail(fmt::format("vertex register {} is the fold register", v)); const uint32_t other = rs_of(w) == v ? rt_of(w) : rt_of(w) == v ? rs_of(w) : 32u; if (other == 32u) throw fail(fmt::format("fold_expected 0x{:08X} does not read " "vertex register {}", w, v)); if (i == 0) { // The seed operand is the sign mask: a register of its own. if (other == 0 || other == r || std::find(site.vertex_regs.begin(), site.vertex_regs.end(), other) != site.vertex_regs.end()) throw fail(fmt::format("first AND 0x{:08X} must combine vertex " "register {} with a separate mask " "register", w, v)); } else if (other != r) { throw fail(fmt::format("fold_expected 0x{:08X} must combine vertex " "register {} with the fold register {}", w, v, r)); } } const uint32_t c = site.consumer_expected; const uint32_t c_op = c >> 26; if ((c_op != 0x04u && c_op != 0x05u) || !((rs_of(c) == r && rt_of(c) == 0) || (rs_of(c) == 0 && rt_of(c) == r))) throw fail(fmt::format("consumer_expected 0x{:08X} must be beq/bne of the " "fold register {} against $zero", c, r)); return site; } // Parse a display aspect "W:H" string (e.g. "4:3", "16:9", "21:9"). Accepts // only aspects between native 4:3 and 32:9 — narrower than 4:3 has no // widescreen meaning and ultra-extreme values would squash the GTE X axis // into unusability. Returns false on malformed input or out-of-range aspect. static bool parse_aspect_ratio(const std::string& s, int* num, int* den) { const size_t colon = s.find(':'); if (colon == std::string::npos || colon == 0 || colon + 1 >= s.size()) return false; int n = 0, d = 0; try { size_t used = 0; n = std::stoi(s.substr(0, colon), &used); if (used != colon) return false; const std::string rest = s.substr(colon + 1); d = std::stoi(rest, &used); if (used != rest.size()) return false; } catch (const std::exception&) { return false; } if (n <= 0 || d <= 0 || n > 99 || d > 99) return false; if (3 * n < 4 * d) return false; // narrower than 4:3 if (9 * n > 32 * d) return false; // wider than 32:9 *num = n; *den = d; return true; } // Reject a config path that is not confined to the project directory. // // A game.toml is checked in and read on every host, so a path in it must mean // the same thing everywhere; std::filesystem::path answers by the *host* // grammar, so a Windows-authored escape passes on POSIX. Scan the raw bytes // under both separator conventions instead. NUL is rejected outright because // the runtime hands this value on as a C string — anything past a NUL would // silently vanish between what is validated and what is used. static void validate_project_relative(const std::string& value, const char* field) { if (value.find('\0') != std::string::npos) { throw std::runtime_error( fmt::format("{} must not contain a NUL byte", field)); } // Rooted ("/x", "\x"), UNC ("\\server\share"), drive-qualified ("C:/x", // "C:\x") and drive-relative ("C:x") alike. const bool rooted = !value.empty() && (value[0] == '/' || value[0] == '\\'); const bool drive = value.size() >= 2 && value[1] == ':' && std::isalpha(static_cast(value[0])); if (rooted || drive) { throw std::runtime_error( fmt::format("{} must be project-relative", field)); } for (size_t start = 0;;) { const size_t sep = value.find_first_of("/\\", start); const size_t end = (sep == std::string::npos) ? value.size() : sep; if (value.compare(start, end - start, "..") == 0) { throw std::runtime_error( fmt::format("{} must stay inside the project", field)); } if (sep == std::string::npos) break; start = sep + 1; } } // Parse the optional [runtime] block. All fields optional; absent fields // leave has_* = false on the returned struct. Paths are resolved relative // to `root` (project root). static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& root) { RuntimeConfig rt; // [localization] language = "en" (top-level; independent of [runtime]). if (cfg.contains("localization")) { const toml::value& loc = toml::find(cfg, "localization"); if (loc.contains("language")) rt.language = toml::find(loc, "language"); // `default` is the launcher-facing alias for the pre-selected language. if (loc.contains("default")) rt.language = toml::find(loc, "default"); // Optional launcher dropdown options: languages = [ {code, label}, ... ]. if (loc.contains("languages") && toml::find(loc, "languages").is_array()) { for (const auto& e : toml::find(loc, "languages").as_array()) { if (!e.contains("code")) continue; RuntimeConfig::LanguageOption lo; lo.code = toml::find(e, "code"); lo.label = e.contains("label") ? toml::find(e, "label") : lo.code; if (!lo.code.empty()) rt.languages.push_back(std::move(lo)); } } } if (cfg.contains("runtime")) { const toml::value& runtime = toml::find(cfg, "runtime"); if (runtime.contains("language")) // [runtime].language convenience alias rt.language = toml::find(runtime, "language"); if (runtime.contains("debug_port")) { const auto port = toml::find(runtime, "debug_port"); if (port < 0 || port > 65535) { throw std::runtime_error( fmt::format("[runtime] debug_port out of range (0..65535): {}", port)); } rt.debug_port = static_cast(port); rt.has_debug_port = true; } if (runtime.contains("window_title")) { rt.window_title = toml::find(runtime, "window_title"); rt.has_window_title = true; } if (runtime.contains("controller")) { rt.controller = toml::find(runtime, "controller"); for (char& c : rt.controller) { c = static_cast(std::tolower(static_cast(c))); } if (rt.controller != "digital" && rt.controller != "dualshock") { throw std::runtime_error( fmt::format("[runtime] controller must be 'digital' or 'dualshock', got '{}'", rt.controller)); } rt.has_controller = true; } if (runtime.contains("memcard_dir")) { const auto rel = toml::find(runtime, "memcard_dir"); rt.memcard_dir = PSXRecompV4::host_resolve(root, rel); rt.has_memcard_dir = true; } if (runtime.contains("disc_speed")) { rt.disc_speed = toml::find(runtime, "disc_speed"); rt.has_disc_speed = true; } if (runtime.contains("instant_max_per_frame")) { const auto n = toml::find(runtime, "instant_max_per_frame"); if (n < 1 || n > 4096) { throw std::runtime_error(fmt::format( "[runtime] instant_max_per_frame out of range (1..4096): {}", n)); } rt.instant_max_per_frame = static_cast(n); rt.has_instant_max_per_frame = true; } auto parse_warm_cd_route = [&](const toml::value& route, const std::string& label) { if (!route.contains("arm_lba") || !route.contains("lbas")) { throw std::runtime_error( label + " arm_lba and lbas are required"); } const auto arm = toml::find(route, "arm_lba"); if (arm < 0 || arm > 0x7FFFFFFFll) { throw std::runtime_error(fmt::format( "{} arm_lba out of range: {}", label, arm)); } RuntimeConfig::WarmCdRoute parsed; parsed.arm_lba = static_cast(arm); const auto& lbas = toml::find>(route, "lbas"); if (lbas.empty() || lbas.size() > 64) { throw std::runtime_error( label + " lbas must contain 1..64 entries"); } for (const auto lba : lbas) { if (lba < 0 || lba > 0x7FFFFFFFll) { throw std::runtime_error(fmt::format( "{} LBA out of range: {}", label, lba)); } parsed.lbas.push_back(static_cast(lba)); } if (route.contains("instant_max_per_frame")) { const auto n = toml::find(route, "instant_max_per_frame"); if (n < 1 || n > 4096) { throw std::runtime_error(fmt::format( "{} instant_max_per_frame out of range (1..4096): {}", label, n)); } parsed.instant_max_per_frame = static_cast(n); } for (const auto& existing : rt.warm_cd_routes) { if (existing.arm_lba == parsed.arm_lba && existing.lbas.front() == parsed.lbas.front()) { throw std::runtime_error(fmt::format( "{} duplicates route arm {} / first LBA {}", label, parsed.arm_lba, parsed.lbas.front())); } } rt.warm_cd_routes.push_back(std::move(parsed)); }; if (runtime.contains("warm_cd_route")) { std::fprintf(stderr, "psxrecomp: warning: [runtime.warm_cd_route] is deprecated; " "use [[runtime.warm_cd_routes]]\n"); parse_warm_cd_route(toml::find(runtime, "warm_cd_route"), "[runtime.warm_cd_route]"); } if (runtime.contains("warm_cd_routes")) { const auto& routes = toml::find(runtime, "warm_cd_routes"); if (!routes.is_array() || routes.as_array().empty() || routes.as_array().size() > 16) { throw std::runtime_error( "[[runtime.warm_cd_routes]] must contain 1..16 routes"); } size_t route_index = 0; for (const auto& route : routes.as_array()) { if (!route.is_table()) { throw std::runtime_error( "[[runtime.warm_cd_routes]] entries must be tables"); } parse_warm_cd_route(route, fmt::format( "[[runtime.warm_cd_routes]] entry {}", route_index++)); } } if (runtime.contains("fast_boot")) { rt.fast_boot = toml::find(runtime, "fast_boot"); } if (runtime.contains("openbios")) { rt.openbios = toml::find(runtime, "openbios"); } if (runtime.contains("bios_hle")) { rt.bios_hle = toml::find(runtime, "bios_hle"); } if (runtime.contains("bios_hle_keep_intro")) { rt.bios_hle_keep_intro = toml::find(runtime, "bios_hle_keep_intro"); } if (runtime.contains("hide_hidden_mod_features")) { rt.hide_hidden_mod_features = toml::find(runtime, "hide_hidden_mod_features"); } if (runtime.contains("hle_scheduler")) { rt.hle_scheduler = toml::find(runtime, "hle_scheduler"); } if (runtime.contains("overlay_cache")) { rt.overlay_cache = toml::find(runtime, "overlay_cache"); } if (runtime.contains("overlay_capture_history")) { rt.overlay_capture_history = toml::find(runtime, "overlay_capture_history"); } if (runtime.contains("overlay_capture_persist_dir")) { rt.overlay_capture_persist_dir = toml::find(runtime, "overlay_capture_persist_dir"); validate_project_relative(rt.overlay_capture_persist_dir, "runtime.overlay_capture_persist_dir"); } // turbo_loads / offer_turbo_loads are deprecated and ignored (see // config_loader.h). Still parsed so old configs load, and the presence // flags let the runtime name the dead key in its deprecation notice. if (runtime.contains("turbo_loads")) { rt.turbo_loads = toml::find(runtime, "turbo_loads"); rt.has_turbo_loads = true; } if (runtime.contains("offer_turbo_loads")) { rt.offer_turbo_loads = toml::find(runtime, "offer_turbo_loads"); rt.has_offer_turbo_loads = true; } if (runtime.contains("turbo_audio_sink")) { rt.turbo_audio_sink = toml::find(runtime, "turbo_audio_sink"); } if (runtime.contains("idle_skip_store_counters")) { rt.idle_skip_store_counters = toml::find(runtime, "idle_skip_store_counters"); } if (runtime.contains("idle_skip_store_counter_sites")) { for (const auto& a : toml::find>( runtime, "idle_skip_store_counter_sites")) rt.idle_skip_store_counter_sites.push_back( parse_hex(a, "runtime.idle_skip_store_counter_sites")); } if (runtime.contains("idle_skip")) { rt.idle_skip = toml::find(runtime, "idle_skip"); } if (runtime.contains("overlay_autocompile_cmd")) { rt.overlay_autocompile_cmd = toml::find(runtime, "overlay_autocompile_cmd"); rt.has_overlay_autocompile_cmd = !rt.overlay_autocompile_cmd.empty(); } if (runtime.contains("overlay_autocompile_cmd_tcc")) { rt.overlay_autocompile_cmd_tcc = toml::find(runtime, "overlay_autocompile_cmd_tcc"); rt.has_overlay_autocompile_cmd_tcc = !rt.overlay_autocompile_cmd_tcc.empty(); } if (runtime.contains("overlay_backend")) { rt.overlay_backend = toml::find(runtime, "overlay_backend"); } if (runtime.contains("overlay_region_floor")) { const auto& v = toml::find(runtime, "overlay_region_floor"); uint32_t floor = 0; if (v.is_string()) { floor = parse_hex(v.as_string(), "runtime.overlay_region_floor"); } else { // Raw TOML integers are 64-bit signed: reject anything that would // wrap or be masked into a different physical address downstream. const std::int64_t raw = v.as_integer(); if (raw < 0 || raw > 0xFFFFFFFFll) { throw std::runtime_error(fmt::format( "[runtime] overlay_region_floor out of range " "(0x10000..0x7FFFFF physical, KSEG0/KSEG1 prefix allowed): {}", raw)); } floor = static_cast(raw); } // The floor names a main-RAM physical address above the kernel window, // inside the 8 MiB decode window (the opt-in 8 MB map's text may end // above 2 MiB; on retail RAM the runtime clamps it to the live end). // Fail loud here instead of relying on the runtime clamp/mask. const uint32_t phys = floor & 0x1FFFFFFFu; if (phys < 0x00010000u || phys >= 0x00800000u) { throw std::runtime_error(fmt::format( "[runtime] overlay_region_floor out of range " "(0x10000..0x7FFFFF physical, KSEG0/KSEG1 prefix allowed): 0x{:08X}", floor)); } rt.overlay_region_floor = floor; rt.has_overlay_region_floor = true; } if (runtime.contains("overlay_native_block")) { for (const auto& a : toml::find>(runtime, "overlay_native_block")) { rt.overlay_native_block.push_back(parse_hex(a, "runtime.overlay_native_block")); } } } if (cfg.contains("parappa_timing")) { const toml::value& timing = toml::find(cfg, "parappa_timing"); rt.has_parappa_timing = true; if (timing.contains("mode")) { rt.parappa_timing_mode = toml::find(timing, "mode"); for (char& c : rt.parappa_timing_mode) c = (char)std::tolower((unsigned char)c); } auto parse_window = [&](const char *key) -> int { const auto n = toml::find(timing, key); if (n < 0 || n > 60) { throw std::runtime_error(fmt::format( "[parappa_timing] {} out of range (0..60): {}", key, n)); } return (int)n; }; if (timing.contains("extra_early")) rt.parappa_timing_extra_early = parse_window("extra_early"); if (timing.contains("extra_late")) rt.parappa_timing_extra_late = parse_window("extra_late"); } // Optional [timing] block — title constants, never player settings. // guest_cycle_scale: no CPU is emulated; recompiled code charges the // guest clock a fixed cost per instruction, and scale N charges 1/N of // it, so N times as much game code fits between two VBlanks while // VBlank, timers, CD, SPU and DMA keep hardware time. 1 = faithful; // values outside 1..64 are rejected. guest_cycle_scale_gated: the scale applies only // while the mod gate is open (psx_mod_set_guest_cycle_scale_gate); see // guest_cycle_scale_gate below for the declarative RAM gate. if (cfg.contains("timing")) { const toml::value& tm = toml::find(cfg, "timing"); if (tm.contains("guest_cycle_scale")) { const long long n = toml::find(tm, "guest_cycle_scale"); if (n < 1 || n > 64) throw std::runtime_error("[timing] guest_cycle_scale must be 1..64"); rt.guest_cycle_scale = static_cast(n); } if (tm.contains("guest_cycle_scale_gated")) rt.guest_cycle_scale_gated = toml::find(tm, "guest_cycle_scale_gated"); if (tm.contains("guest_cycle_scale_batch")) rt.guest_cycle_scale_batch = toml::find(tm, "guest_cycle_scale_batch"); // guest_cycle_scale_gate: one inline table or an array of them, // { addr, value, size = 4, mask = 0xFFFFFFFF }; all must hold. if (tm.contains("guest_cycle_scale_gate")) { const toml::value& g = toml::find(tm, "guest_cycle_scale_gate"); std::vector preds; if (g.is_array()) preds = g.as_array(); else preds.push_back(g); if (preds.size() > 8) throw std::runtime_error("[timing] guest_cycle_scale_gate: at most 8 predicates"); // Range-check the signed TOML integer before narrowing, so an // oversized or negative value cannot wrap into a valid one. auto u32_field = [](const toml::value& pv, const char* key) -> uint32_t { const long long v = toml::find(pv, key); if (v < 0 || v > 0xFFFFFFFFLL) throw std::runtime_error( std::string("[timing] guest_cycle_scale_gate: ") + key + " must be 0..0xFFFFFFFF"); return static_cast(v); }; for (const toml::value& pv : preds) { RuntimeConfig::GuestCycleScaleGatePred p; p.addr = u32_field(pv, "addr"); p.value = u32_field(pv, "value"); if (pv.contains("size")) p.size = u32_field(pv, "size"); if (pv.contains("mask")) p.mask = u32_field(pv, "mask"); const uint32_t phys = p.addr & 0x1FFFFFFFu; if ((p.size != 1 && p.size != 2 && p.size != 4) || (phys & (p.size - 1u)) != 0 || phys >= 0x00800000u) throw std::runtime_error( "[timing] guest_cycle_scale_gate: addr must be an aligned " "main-RAM address and size 1, 2 or 4"); rt.guest_cycle_scale_gate.push_back(p); } } } // Optional [video] block — visual enhancement options. Kept on the same // RuntimeConfig so main.cpp consumes them alongside the other knobs. if (cfg.contains("video")) { const toml::value& video = toml::find(cfg, "video"); if (video.contains("supersampling")) { const auto n = toml::find(video, "supersampling"); // 1..32: the runtime clamps per backend (software/Vulkan 4, OpenGL // to the driver's texture limits and a memory budget). if (n < 1 || n > 32) { throw std::runtime_error(fmt::format( "[video] supersampling out of range (1..32): {}", n)); } rt.video_supersampling = static_cast(n); } if (video.contains("depth24_trailing_margin")) { const auto n = toml::find(video, "depth24_trailing_margin"); if (n < 0 || n > 64) { throw std::runtime_error(fmt::format( "[video] depth24_trailing_margin out of range (0..64): {}", n)); } rt.video_depth24_trailing_margin = static_cast(n); } if (video.contains("internal_resolution")) { const toml::value& ir = toml::find(video, "internal_resolution"); int value = 0; bool ok = false; if (ir.is_string()) { ok = psx_ir_parse(ir.as_string().str.c_str(), &value) != 0; } else if (ir.is_integer()) { const auto n = ir.as_integer(); ok = n >= PSX_IR_MIN_LINES && n <= PSX_IR_MAX_LINES; value = static_cast(n); } if (!ok) { throw std::runtime_error( "[video] internal_resolution must be native, 720p, 1080p, " "1440p, 4k, 5k, 8k, display, or a number of lines"); } rt.video_internal_resolution = value; } if (video.contains("dynamic_resolution")) { rt.video_dynamic_resolution = toml::find(video, "dynamic_resolution"); } if (video.contains("dynamic_resolution_min")) { const toml::value& ir = toml::find(video, "dynamic_resolution_min"); int value = 0; bool ok = false; if (ir.is_string()) { ok = psx_ir_parse(ir.as_string().str.c_str(), &value) != 0; } else if (ir.is_integer()) { const auto n = ir.as_integer(); ok = n >= PSX_IR_MIN_LINES && n <= PSX_IR_MAX_LINES; value = static_cast(n); } if (!ok) { throw std::runtime_error( "[video] dynamic_resolution_min must be native, 720p, 1080p, " "1440p, 4k, 5k, 8k, display, or a number of lines"); } rt.video_dynamic_resolution_min = value; } if (video.contains("resolution_reference_lines")) { const auto n = toml::find(video, "resolution_reference_lines"); if (n < 120 || n > 1024) { throw std::runtime_error(fmt::format( "[video] resolution_reference_lines out of range (120..1024): {}", n)); } rt.video_resolution_reference_lines = static_cast(n); } if (video.contains("match_display_max_lines")) { const auto n = toml::find(video, "match_display_max_lines"); if (n != 0 && (n < 240 || n > 8640)) { throw std::runtime_error(fmt::format( "[video] match_display_max_lines must be 0 (no cap) or 240..8640: {}", n)); } rt.video_match_display_max_lines = static_cast(n); } if (video.contains("supersample_disable_at_lines")) { const auto n = toml::find(video, "supersample_disable_at_lines"); if (n != 0 && (n < 240 || n > 8640)) throw std::runtime_error(fmt::format( "[video] supersample_disable_at_lines must be 0 or 240..8640: {}", n)); rt.video_supersample_disable_at_lines = static_cast(n); } if (video.contains("window_width")) { const auto n = toml::find(video, "window_width"); if (n < 640 || n > 7680) { throw std::runtime_error(fmt::format( "[video] window_width out of range (640..7680): {}", n)); } rt.video_window_width = static_cast(n); } if (video.contains("antialiasing")) { rt.video_antialiasing = toml::find(video, "antialiasing"); } if (video.contains("antialiasing_mode")) { const auto mode = toml::find(video, "antialiasing_mode"); if (mode == "off") rt.video_antialiasing_mode = 0; else if (mode == "fxaa") rt.video_antialiasing_mode = 1; else if (mode == "fxaa_hq") rt.video_antialiasing_mode = 2; else throw std::runtime_error(fmt::format( "[video] antialiasing_mode must be \"off\", \"fxaa\" or \"fxaa_hq\": {}", mode)); } // bloom: opt-in glow (OpenGL), 0.0 (off, default) .. 2.0. if (video.contains("bloom")) { const auto& v = toml::find(video, "bloom"); const double f = v.is_integer() ? static_cast(v.as_integer()) : toml::get(v); if (!(f >= 0.0 && f <= 2.0)) throw std::runtime_error(fmt::format("[video] bloom out of range (0.0..2.0): {}", f)); rt.video_bloom = static_cast(f); } // post_shader: an elective post-effect chain (.pfx), relative to the // game root. The runtime parses it and logs (and skips) a bad one. if (video.contains("post_shader")) { const std::string ps = toml::find(video, "post_shader"); if (!ps.empty()) rt.video_post_shader = PSXRecompV4::host_resolve(root, ps); } if (video.contains("supersample")) { const auto& v = toml::find(video, "supersample"); const double f = v.is_integer() ? static_cast(v.as_integer()) : toml::get(v); if (!(f >= 1.0 && f <= 4.0)) { throw std::runtime_error(fmt::format( "[video] supersample out of range (1.0..4.0): {}", f)); } rt.video_supersample_milli = static_cast(f * 1000.0 + 0.5); } if (video.contains("texture_filtering")) { const auto mode = toml::find(video, "texture_filtering"); if (mode == "nearest") rt.video_texture_filter = 0; else if (mode == "bilinear") rt.video_texture_filter = 1; else if (mode == "xbr") rt.video_texture_filter = 3; else throw std::runtime_error(fmt::format( "[video] texture_filtering must be \"nearest\", \"bilinear\" or \"xbr\": {}", mode)); } if (video.contains("fmv_filter")) { const auto mode = toml::find(video, "fmv_filter"); if (!video_fmv_filter_parse(mode, &rt.video_fmv_filter)) throw std::runtime_error(fmt::format( "[video] fmv_filter must be \"nearest\", \"bilinear\", " "\"sharp\" or \"bicubic\": {}", mode)); } if (video.contains("renderer")) { const auto mode = toml::find(video, "renderer"); if (mode == "software") rt.video_renderer = 0; else if (mode == "opengl") rt.video_renderer = 1; else if (mode == "vulkan") rt.video_renderer = 2; else throw std::runtime_error(fmt::format( "[video] renderer must be \"software\", \"opengl\" or \"vulkan\": {}", mode)); } if (video.contains("offer_vulkan")) { rt.video_offer_vulkan = toml::find(video, "offer_vulkan"); } if (video.contains("geometry_correction")) { rt.video_geometry_correction = toml::find(video, "geometry_correction"); } if (video.contains("perspective_texturing")) { rt.video_perspective_texturing = toml::find(video, "perspective_texturing"); } if (video.contains("pgxp_cpu_mode")) { rt.video_pgxp_cpu_mode = toml::find(video, "pgxp_cpu_mode"); } if (video.contains("pgxp_tolerance")) { rt.video_pgxp_tolerance = toml_number(video, "pgxp_tolerance"); rt.video_pgxp_tolerance_set = true; } if (video.contains("pgxp_position_fallback")) { rt.video_pgxp_position_fallback = toml::find(video, "pgxp_position_fallback"); rt.video_pgxp_position_fallback_set = true; } if (video.contains("pgxp_preserve_projection")) { rt.video_pgxp_preserve_projection = toml::find(video, "pgxp_preserve_projection"); } if (video.contains("fmv_chroma_smoothing")) rt.video_fmv_chroma_smoothing = toml::find(video, "fmv_chroma_smoothing"); if (video.contains("texture_lod")) { const auto mode = toml::find(video, "texture_lod"); if (mode == "off") rt.video_texture_lod = 0; else if (mode == "mipmap") rt.video_texture_lod = 1; else throw std::runtime_error(fmt::format( "[video] texture_lod must be \"off\" or \"mipmap\": {}", mode)); } if (video.contains("anisotropic_filtering")) { const auto n = toml::find(video, "anisotropic_filtering"); if (n < 1 || n > 16) throw std::runtime_error(fmt::format( "[video] anisotropic_filtering must be 1..16: {}", n)); rt.video_anisotropic_filtering = n; } if (video.contains("accurate_blending")) rt.video_accurate_blending = toml::find(video, "accurate_blending"); if (video.contains("dithering")) { const auto mode = toml::find(video, "dithering"); if (mode == "off") rt.video_dithering = 0; else if (mode == "on" || mode == "scaled") rt.video_dithering = 1; else if (mode == "fine") rt.video_dithering = 2; else throw std::runtime_error(fmt::format( "[video] dithering must be \"on\", \"fine\" or \"off\": {}", mode)); } if (video.contains("pgxp_depth_buffer")) rt.video_pgxp_depth_buffer = toml::find(video, "pgxp_depth_buffer"); if (video.contains("pgxp_color_correction")) rt.video_pgxp_color_correction = toml::find(video, "pgxp_color_correction"); if (video.contains("pgxp_depth_threshold")) rt.video_pgxp_depth_threshold = toml_number(video, "pgxp_depth_threshold"); if (video.contains("pgxp_seam")) { const auto m = toml::find(video, "pgxp_seam"); if (m == "off") rt.video_pgxp_seam = 0; else if (m == "fine") rt.video_pgxp_seam = 1; else if (m == "wide") rt.video_pgxp_seam = 2; else throw std::runtime_error("[video] pgxp_seam must be \"off\", \"fine\" or \"wide\""); } if (video.contains("pgxp_mod_only")) { rt.video_pgxp_mod_only = toml::find(video, "pgxp_mod_only"); } if (video.contains("crt_filter")) { const auto mode = toml::find(video, "crt_filter"); if (mode == "raw") rt.video_screen_kind = 0; else if (mode == "crt") rt.video_screen_kind = 1; else if (mode == "composite") rt.video_screen_kind = 2; else if (mode == "trinitron") rt.video_screen_kind = 3; else throw std::runtime_error(fmt::format( "[video] crt_filter must be \"raw\"|\"crt\"|\"composite\"|\"trinitron\": {}", mode)); } if (video.contains("scanlines")) { rt.video_scanlines = toml::find(video, "scanlines"); } if (video.contains("scanline_strength")) { double s = toml_number(video, "scanline_strength"); if (s < 0.0) s = 0.0; if (s > 1.0) s = 1.0; rt.video_scanline_strength = s; } if (video.contains("auto_skip_fmv")) { rt.video_auto_skip_fmv = toml::find(video, "auto_skip_fmv"); } if (video.contains("offer_skip_fmv")) { rt.video_offer_skip_fmv = toml::find(video, "offer_skip_fmv"); } if (video.contains("fmv_skip_total_table")) { rt.video_fmv_skip_total_table = (uint32_t)toml::find(video, "fmv_skip_total_table"); } if (video.contains("fmv_skip_movie_id")) { rt.video_fmv_skip_movie_id = (uint32_t)toml::find(video, "fmv_skip_movie_id"); } if (video.contains("fmv_skip_end_total")) { rt.video_fmv_skip_end_total = (int)toml::find(video, "fmv_skip_end_total"); } if (video.contains("fmv_skip_no_xa")) { rt.video_fmv_skip_no_xa = toml::find(video, "fmv_skip_no_xa"); } if (video.contains("fmv_skip_no_xa_hold")) { rt.video_fmv_skip_no_xa_hold = (int)toml::find(video, "fmv_skip_no_xa_hold"); if (rt.video_fmv_skip_no_xa_hold < 4 || rt.video_fmv_skip_no_xa_hold > 3600) throw std::runtime_error( "[video] fmv_skip_no_xa_hold must be between 4 and 3600 vblanks"); } if (video.contains("low_latency_input")) { rt.video_low_latency_input = toml::find(video, "low_latency_input"); } if (video.contains("texture_window_batching")) { rt.video_texture_window_batching = toml::find(video, "texture_window_batching"); } if (video.contains("render_thread")) { rt.video_render_thread = toml::find(video, "render_thread"); } if (video.contains("present_thread")) { rt.video_present_thread = toml::find(video, "present_thread"); } if (video.contains("frame_generation")) { rt.video_frame_generation = toml::find(video, "frame_generation"); } if (video.contains("frame_generation_method")) { const auto m = toml::find(video, "frame_generation_method"); if (m == "redraw") rt.video_frame_generation_method = 0; else if (m == "reprojection") rt.video_frame_generation_method = 1; else throw std::runtime_error(fmt::format( "[video] frame_generation_method must be \"redraw\"|\"reprojection\": {}", m)); } if (video.contains("vsync")) { const auto mode = toml::find(video, "vsync"); if (mode == "on" || mode == "vsync") rt.video_vsync = 1; else if (mode == "off" || mode == "immediate") rt.video_vsync = 0; else if (mode == "adaptive") rt.video_vsync = -1; // vrr: variable refresh (G-Sync/FreeSync, ProMotion): present // each frame when it is ready (swap interval 0), Smooth motion // targets the panel's maximum refresh and never exceeds it. else if (mode == "vrr") rt.video_vsync = 2; else throw std::runtime_error(fmt::format( "[video] vsync must be \"on\"|\"off\"|\"immediate\"|\"adaptive\"|\"vrr\": {}", mode)); } if (video.contains("frame_interpolation")) { rt.video_frame_interpolation = toml::find(video, "frame_interpolation"); } if (video.contains("frame_interpolation_fps")) { rt.video_frame_interpolation_fps = toml::find(video, "frame_interpolation_fps"); if (rt.video_frame_interpolation_fps != 0 && rt.video_frame_interpolation_fps < 90) throw std::runtime_error( "[video] frame_interpolation_fps must be 0 (display) or >= 90"); } if (video.contains("offer_frame_interpolation")) { rt.video_offer_frame_interpolation = toml::find(video, "offer_frame_interpolation"); } if (video.contains("aspect_ratio")) { const auto mode = toml::find(video, "aspect_ratio"); int n = 0, d = 0; if (!parse_aspect_ratio(mode, &n, &d)) throw std::runtime_error(fmt::format( "[video] aspect_ratio must be \"W:H\", no narrower than 4:3 " "and no wider than 32:9 (e.g. \"4:3\", \"16:9\"): {}", mode)); rt.video_aspect_num = n; rt.video_aspect_den = d; } } // Optional [audio] block. if (cfg.contains("audio")) { const toml::value& audio = toml::find(cfg, "audio"); if (audio.contains("buffer_ms")) { const auto n = toml::find(audio, "buffer_ms"); if (n < 30 || n > 500) { throw std::runtime_error(fmt::format( "[audio] buffer_ms out of range (30..500): {}", n)); } rt.audio_buffer_ms = static_cast(n); } if (audio.contains("spu_hq")) { rt.audio_spu_hq = toml::find(audio, "spu_hq"); } } // Optional [controller] block — game-declared input defaults. if (cfg.contains("controller")) { const toml::value& ct = toml::find(cfg, "controller"); // Legacy boolean form (true->analog, false->digital), read first so the // new string `*_mode` keys win when both are present. if (ct.contains("default_analog")) { const int m = toml::find(ct, "default_analog") ? PAD_MODE_ANALOG : PAD_MODE_DIGITAL; rt.default_p1_mode = rt.default_p2_mode = m; rt.has_default_mode = true; } if (ct.contains("p1_analog")) { rt.default_p1_mode = toml::find(ct, "p1_analog") ? PAD_MODE_ANALOG : PAD_MODE_DIGITAL; rt.has_default_mode = true; } if (ct.contains("p2_analog")) { rt.default_p2_mode = toml::find(ct, "p2_analog") ? PAD_MODE_ANALOG : PAD_MODE_DIGITAL; rt.has_default_mode = true; } // Preferred string form. if (ct.contains("default_mode")) { const int m = pad_mode_from_string( toml::find(ct, "default_mode"), PAD_MODE_ANALOG); rt.default_p1_mode = rt.default_p2_mode = m; rt.has_default_mode = true; } if (ct.contains("p1_mode")) { rt.default_p1_mode = pad_mode_from_string( toml::find(ct, "p1_mode"), PAD_MODE_ANALOG); rt.has_default_mode = true; } if (ct.contains("p2_mode")) { rt.default_p2_mode = pad_mode_from_string( toml::find(ct, "p2_mode"), PAD_MODE_ANALOG); rt.has_default_mode = true; } if (ct.contains("p1_device")) { rt.default_p1_device = toml::find(ct, "p1_device"); if (!rt.default_p1_device.empty()) rt.has_default_p1_device = true; } if (ct.contains("p2_device")) { rt.default_p2_device = toml::find(ct, "p2_device"); if (!rt.default_p2_device.empty()) rt.has_default_p2_device = true; } if (ct.contains("lock_mode")) { rt.controller_lock_mode = toml::find(ct, "lock_mode"); } if (ct.contains("lock_device")) { rt.controller_lock_device = toml::find(ct, "lock_device"); } if (ct.contains("deadzone")) { const auto n = toml::find(ct, "deadzone"); if (n < 0 || n > 32767) throw std::runtime_error(fmt::format( "[controller] deadzone out of range (0..32767): {}", n)); rt.deadzone = static_cast(n); rt.has_deadzone = true; } if (ct.contains("multitap_port")) { const auto n = toml::find(ct, "multitap_port"); if (n != 1 && n != 2) throw std::runtime_error(fmt::format( "[controller] multitap_port must be 1 or 2, got {}", n)); rt.multitap_port = static_cast(n); rt.has_multitap_port = true; } if (ct.contains("multitap")) { rt.multitap_default = toml::find(ct, "multitap"); rt.has_multitap_default = true; } if (ct.contains("multitap_analog")) { rt.multitap_analog = toml::find(ct, "multitap_analog"); rt.has_multitap_analog = true; } // Prefer string key; accept legacy bool alias. if (ct.contains("legacy_pad_config")) { rt.legacy_pad_config = toml::find(ct, "legacy_pad_config"); } if (ct.contains("direct_shortcut")) { rt.direct_shortcut = toml::find(ct, "direct_shortcut"); if (rt.direct_shortcut != "rewind" && rt.direct_shortcut != "save_state_menu" && rt.direct_shortcut != "fast_forward" && rt.direct_shortcut != "fast_forward_toggle") throw std::runtime_error(fmt::format( "[controller] direct_shortcut must be rewind, " "save_state_menu, fast_forward or fast_forward_toggle, " "got '{}'", rt.direct_shortcut)); if (!ct.contains("direct_shortcut_button")) throw std::runtime_error( "[controller] direct_shortcut needs direct_shortcut_button"); rt.direct_shortcut_button = toml::find(ct, "direct_shortcut_button"); } if (ct.contains("experimental_jogcon_force_feedback")) rt.experimental_jogcon_force_feedback = toml::find(ct, "experimental_jogcon_force_feedback"); if (ct.contains("experimental_jogcon_force_feedback_invert")) rt.experimental_jogcon_force_feedback_invert = toml::find(ct, "experimental_jogcon_force_feedback_invert"); if (ct.contains("anti_deadzone")) { const auto n = toml::find(ct, "anti_deadzone"); if (n < 0 || n > 32767) throw std::runtime_error(fmt::format( "[controller] anti_deadzone out of range (0..32767): {}", n)); rt.anti_deadzone = static_cast(n); rt.has_anti_deadzone = true; } } return rt; } fs::path find_project_root(const fs::path& config_path) { fs::path cur = PSXRecompV4::host_absolute(config_path).parent_path(); const fs::path fallback = cur; // A packaged game (tools/package_game_release.sh stages its licenses/ // beside game.toml) is its own root. Walking up from it would stop at // the first ancestor with a marker -- e.g. a global ~/.gitignore -- and // anchor the game's relative paths (memcard_dir = "saves") there. if (fs::exists(cur / "licenses" / "psxrecomp-LICENSE")) return cur; for (int i = 0; i < 8; ++i) { for (const char* marker : { ".gitignore", ".git", "CMakeLists.txt" }) { if (fs::exists(cur / marker)) { return cur; } } const fs::path parent = cur.parent_path(); if (parent == cur) break; cur = parent; } return fallback; } bool draw_distance_clamp_reads(uint32_t instr, uint32_t reg) { if (reg == 0u || reg > 31u) return false; const uint32_t op = instr >> 26; const uint32_t rs = (instr >> 21) & 31u; const uint32_t rt = (instr >> 16) & 31u; // ADDI, ADDIU, SLTI, SLTIU, ANDI, ORI, XORI read rs (LUI reads nothing). if (op >= 0x08u && op <= 0x0Eu) return rs == reg; if (op != 0x00u) return false; const uint32_t funct = instr & 0x3Fu; // SLL, SRL, SRA: rt. SLLV, SRLV, SRAV: rt and rs. if (funct == 0x00u || funct == 0x02u || funct == 0x03u) return rt == reg; if (funct == 0x04u || funct == 0x06u || funct == 0x07u) return rs == reg || rt == reg; // ADD, ADDU, SUB, SUBU, AND, OR, XOR, NOR, SLT, SLTU. if (funct >= 0x20u && funct <= 0x2Bu && funct != 0x28u && funct != 0x29u) return rs == reg || rt == reg; return false; } std::string mod_immediate_site_error(const ModImmediateSite& site) { if ((site.address & 3u) != 0u) return "address is not instruction-aligned"; if ((site.address & 0x1FFFFFFFu) >= 0x00800000u || site.address >= 0xC0000000u) return "address is not main RAM"; const uint32_t op = site.expected >> 26; const bool alu_imm = op >= 0x08u && op <= 0x0Fu; const bool load_store = op >= 0x20u && op <= 0x2Bu && op != 0x27u; if (!alu_imm && !load_store) return "expected is not an I-type ALU/load/store instruction " "(ADDI..LUI or LB..SW)"; if (site.group >= 64u) return "group must be 0..63"; if (site.values.empty()) return "values must list at least one immediate"; if (site.values.size() > 15u) return "at most 15 values"; return {}; } // [[mod_immediate.sites]] (docs/config_schema.md "Mod immediates"). static std::vector parse_mod_immediate_sites( const toml::value& cfg, const fs::path& config_path) { std::vector sites; if (!cfg.contains("mod_immediate")) return sites; const toml::value& mi = toml::find(cfg, "mod_immediate"); if (!mi.contains("sites")) return sites; std::set> seen; for (const auto& item : toml::find(mi, "sites")) { ModImmediateSite site; site.address = parse_hex(toml::find(item, "address"), "mod_immediate.sites.address"); site.expected = parse_hex(toml::find(item, "expected"), "mod_immediate.sites.expected"); site.group = (uint32_t)toml::find(item, "group"); for (const auto& v : toml::find(item, "values")) { const int64_t n = v.as_integer(); if (n < -32768 || n > 65535) throw std::runtime_error(fmt::format( "{}: mod_immediate site 0x{:08X}: value {} does not fit " "16 bits", config_path.string(), site.address, n)); site.values.push_back((uint16_t)n); } if (const std::string why = mod_immediate_site_error(site); !why.empty()) throw std::runtime_error(fmt::format( "{}: mod_immediate site 0x{:08X}: {}", config_path.string(), site.address, why)); // Overlays share load addresses, so one address may carry several // sites; only the (address, word) pair must be unique. if (!seen.insert({site.address & 0x1FFFFFFFu, site.expected}).second) throw std::runtime_error(fmt::format( "{}: duplicate mod_immediate site 0x{:08X}", config_path.string(), site.address)); sites.push_back(std::move(site)); } return sites; } // [[draw_distance.clamp]] (docs/config_schema.md "Draw-distance clamps"). static std::vector parse_draw_distance_clamps( const toml::value& cfg, const fs::path& config_path) { std::vector sites; if (!cfg.contains("draw_distance")) return sites; const toml::value& dd = toml::find(cfg, "draw_distance"); if (!dd.contains("clamp")) return sites; const auto& items = toml::find(dd, "clamp"); std::set seen; for (const auto& item : items) { DrawDistanceClampSite site; site.address = parse_hex(toml::find(item, "address"), "draw_distance.clamp.address"); site.expected = parse_hex(toml::find(item, "expected"), "draw_distance.clamp.expected"); const int64_t reg = toml::find(item, "reg"); const int64_t max = toml::find(item, "max"); if ((site.address & 3u) != 0u) throw std::runtime_error(fmt::format( "{}: draw-distance clamp address 0x{:08X} is not " "instruction-aligned", config_path.string(), site.address)); if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error(fmt::format( "{}: duplicate draw-distance clamp address 0x{:08X}", config_path.string(), site.address)); if (reg < 1 || reg > 31) throw std::runtime_error(fmt::format( "{}: draw-distance clamp at 0x{:08X}: reg must be 1..31", config_path.string(), site.address)); if (max < INT32_MIN || max > INT32_MAX) throw std::runtime_error(fmt::format( "{}: draw-distance clamp at 0x{:08X}: max is not a signed " "32-bit value", config_path.string(), site.address)); site.reg = (uint32_t)reg; site.max = (int32_t)max; if (!draw_distance_clamp_reads(site.expected, site.reg)) throw std::runtime_error(fmt::format( "{}: draw-distance clamp at 0x{:08X}: expected 0x{:08X} must " "be an ALU instruction that reads reg {}", config_path.string(), site.address, site.expected, site.reg)); sites.push_back(site); } return sites; } // Derive the output filename stem from a rom basename. Mirrors the Python // audit_config.py logic: strip a trailing .BIN/.EXE (case-insensitive) but // preserve dotted names like "SCUS_942.36" unchanged. // [quality.] (docs/QUALITY_PRESETS.md). Each preset is the whole // game.toml re-parsed with its keys laid over [video], so each key gets the // same validation as in [video]. Only the supported quality keys // (quality_key_supported) are accepted. Lowest preset first. static const char* const kQualityPlayerKeys[] = { "texture_filtering", "fmv_filter", "antialiasing", "dynamic_resolution", "dynamic_resolution_min", "frame_generation", "render_thread", "present_thread", "geometry_correction", "perspective_texturing", "frame_interpolation", "frame_interpolation_fps"}; static const char* const kQualityPresetOnlyKeys[] = { "supersample", "texture_lod", "anisotropic_filtering", "fmv_chroma_smoothing", "bloom", "dithering", "pgxp_depth_buffer", "pgxp_color_correction", "pgxp_seam"}; bool quality_key_player_editable(const std::string& key) { for (const char* k : kQualityPlayerKeys) if (key == k) return true; return false; } bool quality_key_supported(const std::string& key) { if (quality_key_player_editable(key)) return true; for (const char* k : kQualityPresetOnlyKeys) if (key == k) return true; return false; } static std::vector parse_quality_presets(const toml::value& cfg, const fs::path& root, const fs::path& config_path) { std::vector out; if (!cfg.contains("quality")) return out; const toml::value& q = toml::find(cfg, "quality"); if (!q.is_table()) throw std::runtime_error(fmt::format("{}: [quality] must be a table", config_path.string())); static const char* const kNames[] = {"low", "medium", "high", "ultra"}; for (const auto& kv : q.as_table()) { bool known = false; for (const char* n : kNames) known = known || kv.first == n; if (!known) throw std::runtime_error(fmt::format( "{}: [quality.{}] is not a preset (low, medium, high, ultra)", config_path.string(), kv.first)); if (!kv.second.is_table()) throw std::runtime_error(fmt::format( "{}: [quality.{}] must be a table of [video] keys", config_path.string(), kv.first)); } for (const char* n : kNames) { if (!q.contains(n)) continue; QualityPreset p; p.name = n; toml::value merged = cfg; toml::table& top = merged.as_table(); if (!top.count("video") || !top["video"].is_table()) top["video"] = toml::table{}; toml::table& video = top["video"].as_table(); for (const auto& kv : toml::find(q, n).as_table()) { if (kv.second.is_table()) throw std::runtime_error(fmt::format( "{}: [quality.{}] {} must be a [video] value, not a table", config_path.string(), n, kv.first)); if (!quality_key_supported(kv.first)) throw std::runtime_error(fmt::format( "{}: [quality.{}] {} is not a preset key (docs/QUALITY_PRESETS.md " "lists the supported set)", config_path.string(), n, kv.first)); video[kv.first] = kv.second; p.keys.push_back(kv.first); } try { p.runtime = parse_runtime_block(merged, root); } catch (const std::exception& e) { throw std::runtime_error(fmt::format("[quality.{}]: {}", n, e.what())); } out.push_back(std::move(p)); } return out; } static std::string derive_out_stem(const std::string& rom_basename) { auto ends_with_ci = [](const std::string& s, const std::string& suffix) { if (s.size() < suffix.size()) return false; for (size_t i = 0; i < suffix.size(); ++i) { char a = s[s.size() - suffix.size() + i]; char b = suffix[i]; if (std::tolower(static_cast(a)) != std::tolower(static_cast(b))) return false; } return true; }; if (ends_with_ci(rom_basename, ".bin") || ends_with_ci(rom_basename, ".exe")) { return rom_basename.substr(0, rom_basename.size() - 4); } return rom_basename; } BiosConfig load_bios_config(const fs::path& config_path_in) { const fs::path config_path = PSXRecompV4::host_absolute(config_path_in); if (!fs::exists(config_path)) { throw std::runtime_error( fmt::format("config file not found: {}", config_path.string())); } const fs::path root = find_project_root(config_path); toml::value cfg; try { cfg = toml::parse(config_path); } catch (const toml::syntax_error& ex) { throw std::runtime_error( fmt::format("TOML syntax error in {}: {}", config_path.string(), ex.what())); } // A BIOS profile describes an IMAGE — facts about bytes — never a runtime // preference. This block used to be parsed into BiosConfig::runtime and // read by nothing, which let bios/OpenBIOS.toml carry a `bios_hle = false` // that looked authoritative and was inert. Fail loud instead of ignoring: // a preference put here would never take effect, and believing it had is // how the OpenBIOS boot-skip regression got explained away. if (cfg.contains("runtime")) { throw std::runtime_error(fmt::format( "{}: a BIOS profile has no [runtime] block — it describes an image, " "not a preference. Runtime options belong in game.toml / " "settings.toml. (The BIOS-dependent HLE gating is expressed by " "[recompiler.runtime_exports]: shell_entry_phys enables the boot-" "skip, deliver_event_ret enables the kernel-call HLE tier.)", config_path.string())); } // [program] — bios.toml uses this; some legacy files use [game] const toml::value* prog_ptr = nullptr; if (cfg.contains("program")) { prog_ptr = &toml::find(cfg, "program"); } else if (cfg.contains("game")) { prog_ptr = &toml::find(cfg, "game"); } else { throw std::runtime_error( fmt::format("{}: missing [program] (or [game]) block", config_path.string())); } const toml::value& prog = *prog_ptr; const std::string name = toml::find(prog, "name"); const std::string id = prog.contains("id") ? toml::find(prog, "id") : std::string{}; // BIOS uses `rom`; game files use `exe`. Either is accepted here, but // BIOS callers should pass a bios config that has `rom`. std::string rom_field; if (prog.contains("rom")) { rom_field = toml::find(prog, "rom"); } else if (prog.contains("exe")) { rom_field = toml::find(prog, "exe"); } else { throw std::runtime_error( fmt::format("{}: [program] missing 'rom' or 'exe' field", config_path.string())); } // Tolerate either BIOS filename convention (bare "SCPH1001.BIN" vs // region-qualified "US-PSX-SCPH1001.BIN") — see bios_rom_alias.h. A no-op // for game [program].exe fields, which never match a BIOS model token. const fs::path rom_path = resolve_bios_rom(PSXRecompV4::host_resolve(root, rom_field)); const uint32_t load_address = parse_hex(toml::find(prog, "load_address"), "program.load_address"); const uint32_t entry_pc = prog.contains("entry_pc") ? parse_hex(toml::find(prog, "entry_pc"), "program.entry_pc") : load_address; const uint32_t text_size = parse_hex(toml::find(prog, "text_size"), "program.text_size"); // [program.image] — declared identity (optional) std::string image_sha256; bool image_redistributable = false; if (prog.contains("image")) { const toml::value& img = toml::find(prog, "image"); if (img.contains("sha256")) image_sha256 = toml::find(img, "sha256"); if (img.contains("redistributable")) image_redistributable = toml::find(img, "redistributable"); } // [recompiler] if (!cfg.contains("recompiler")) { throw std::runtime_error( fmt::format("{}: missing [recompiler] block", config_path.string())); } const toml::value& recomp = toml::find(cfg, "recompiler"); if (!recomp.contains("seeds")) { throw std::runtime_error( fmt::format("{}: [recompiler] missing 'seeds' field", config_path.string())); } const std::string seeds_field = toml::find(recomp, "seeds"); const fs::path seeds_path = PSXRecompV4::host_resolve(root, seeds_field); const std::string out_dir_field = recomp.contains("out_dir") ? toml::find(recomp, "out_dir") : std::string{"generated"}; const fs::path out_dir = PSXRecompV4::host_resolve(root, out_dir_field); const bool strict = recomp.contains("strict") ? toml::find(recomp, "strict") : true; std::string out_stem; if (recomp.contains("out_stem")) { out_stem = toml::find(recomp, "out_stem"); } else { out_stem = derive_out_stem(fs::path(rom_field).filename().string()); } // [[recompiler.bios_vectors]] — optional array of vector dispatch tables std::vector bios_vectors; if (recomp.contains("bios_vectors")) { const auto& arr = recomp.at("bios_vectors").as_array(); for (const auto& v : arr) { BiosVectorTable bvt; bvt.ram_addr = parse_hex( toml::find(v, "ram_addr"), "bios_vectors.ram_addr"); bvt.index_reg = static_cast( toml::find(v, "index_reg")); bvt.table_rom_addr = parse_hex( toml::find(v, "table_rom_addr"), "bios_vectors.table_rom_addr"); bvt.table_count = static_cast( toml::find(v, "table_count")); bvt.table_ram_addr = v.contains("table_ram_addr") ? parse_hex(toml::find(v, "table_ram_addr"), "bios_vectors.table_ram_addr") : 0u; bios_vectors.push_back(bvt); } } // [[recompiler.bios_aliases]] — fixed-target RAM trampolines std::vector bios_aliases; if (recomp.contains("bios_aliases")) { const auto& arr = recomp.at("bios_aliases").as_array(); for (const auto& v : arr) { BiosAlias ba; ba.ram_addr = parse_hex(toml::find(v, "ram_addr"), "bios_aliases.ram_addr"); ba.target_key = parse_hex(toml::find(v, "target_key"), "bios_aliases.target_key"); bios_aliases.push_back(ba); } } // [recompiler.address_model] — the BIOS's boot-time ROM->RAM code copies. // Optional: absent (or an empty copy list) means the BIOS runs entirely // from ROM and normalization degenerates to the KSEG mask. Semantic // invariants (window overlap, bless uniqueness, alignment) are enforced // in BiosAddressModel::from_config, not here. std::vector address_copies; if (recomp.contains("address_model")) { const toml::value& am = toml::find(recomp, "address_model"); if (am.contains("normalize_mask")) { const uint32_t mask = parse_hex( toml::find(am, "normalize_mask"), "address_model.normalize_mask"); // Only the KSEG strip is implemented; refuse anything else rather // than silently mis-normalizing (RELOCATION_MANIFEST_FORMAT.md // echo-and-check). if (mask != 0x1FFFFFFFu) { throw std::runtime_error(fmt::format( "{}: address_model.normalize_mask must be 0x1FFFFFFF", config_path.string())); } } if (am.contains("copy")) { for (const auto& v : am.at("copy").as_array()) { BiosAddrCopy c; c.name = toml::find(v, "name"); c.rom_lo = parse_hex(toml::find(v, "rom_lo"), "address_model.copy.rom_lo"); c.rom_hi = parse_hex(toml::find(v, "rom_hi"), "address_model.copy.rom_hi"); c.ram_lo = parse_hex(toml::find(v, "ram_lo"), "address_model.copy.ram_lo"); c.runtime_base = parse_hex( toml::find(v, "runtime_base"), "address_model.copy.runtime_base"); const std::string key = toml::find(v, "dispatch_key"); if (key == "ram") c.key_is_ram = true; else if (key == "rom") c.key_is_ram = false; else { throw std::runtime_error(fmt::format( "{}: address_model.copy '{}': dispatch_key must be " "\"ram\" or \"rom\", got '{}'", config_path.string(), c.name, key)); } if (v.contains("kernel_bless")) c.kernel_bless = toml::find(v, "kernel_bless"); address_copies.push_back(std::move(c)); } } } // [[recompiler.install_slots]] — kernel-RAM ranges the BIOS or the game's // Psy-Q libapi patchers overwrite at runtime. `ram_addr` alone keeps the // original meaning (a 4-word jalr stub); `len` and `resume` describe the // other patch shapes (BiosInstallSlot, bios_address_model.h). std::vector install_slots; if (recomp.contains("install_slots")) { for (const auto& v : recomp.at("install_slots").as_array()) { BiosInstallSlot slot; slot.ram_addr = parse_hex( toml::find(v, "ram_addr"), "install_slots.ram_addr"); if (v.contains("len")) slot.len = parse_hex(toml::find(v, "len"), "install_slots.len"); if (v.contains("resume")) { const std::string r = toml::find(v, "resume"); if (r == "jalr") slot.resume = BiosInstallSlot::Resume::Jalr; else if (r == "fallthrough") slot.resume = BiosInstallSlot::Resume::Fallthrough; else if (r == "none") slot.resume = BiosInstallSlot::Resume::None; else throw std::runtime_error(fmt::format( "{}: install_slots 0x{:08X}: resume must be \"jalr\", " "\"fallthrough\" or \"none\", got '{}'", config_path.string(), slot.ram_addr, r)); } install_slots.push_back(slot); } } // [recompiler.runtime_exports] — per-image HLE anchors couriered into the // generated C. Absent keys stay 0 = structurally unavailable. uint32_t shell_entry_phys = 0, deliver_event_ret = 0; if (recomp.contains("runtime_exports")) { const toml::value& rx = toml::find(recomp, "runtime_exports"); if (rx.contains("shell_entry_phys")) shell_entry_phys = parse_hex( toml::find(rx, "shell_entry_phys"), "runtime_exports.shell_entry_phys"); if (rx.contains("deliver_event_ret")) deliver_event_ret = parse_hex( toml::find(rx, "deliver_event_ret"), "runtime_exports.deliver_event_ret"); } return BiosConfig{ /*config_path*/ config_path, /*project_root*/ root, /*name*/ name, /*id*/ id, /*rom_path*/ rom_path, /*load_address*/ load_address, /*entry_pc*/ entry_pc, /*text_size*/ text_size, /*image_sha256*/ image_sha256, /*image_redistributable*/ image_redistributable, /*seeds_path*/ seeds_path, /*out_dir*/ out_dir, /*strict*/ strict, /*out_stem*/ out_stem, /*bios_vectors*/ std::move(bios_vectors), /*bios_aliases*/ std::move(bios_aliases), /*address_copies*/ std::move(address_copies), /*install_slots*/ std::move(install_slots), /*shell_entry_phys*/ shell_entry_phys, /*deliver_event_ret*/ deliver_event_ret, }; } GameConfig load_game_config(const fs::path& config_path_in) { const fs::path config_path = PSXRecompV4::host_absolute(config_path_in); if (!fs::exists(config_path)) { throw std::runtime_error( fmt::format("game config not found: {}", config_path.string())); } const fs::path root = find_project_root(config_path); toml::value cfg; try { cfg = toml::parse(config_path); } catch (const toml::syntax_error& ex) { throw std::runtime_error( fmt::format("TOML syntax error in {}: {}", config_path.string(), ex.what())); } // [game] (preferred for game configs) or [program] const toml::value* prog_ptr = nullptr; if (cfg.contains("game")) { prog_ptr = &toml::find(cfg, "game"); } else if (cfg.contains("program")) { prog_ptr = &toml::find(cfg, "program"); } else { throw std::runtime_error( fmt::format("{}: missing [game] (or [program]) block", config_path.string())); } const toml::value& game = *prog_ptr; const std::string name = toml::find(game, "name"); const std::string id = game.contains("id") ? toml::find(game, "id") : std::string{}; const std::string region = game.contains("region") ? toml::find(game, "region") : std::string{}; const int players = game.contains("players") ? (int)toml::find(game, "players") : 1; std::string exe_field; if (game.contains("exe")) { exe_field = toml::find(game, "exe"); } else if (game.contains("rom")) { exe_field = toml::find(game, "rom"); } else { throw std::runtime_error( fmt::format("{}: [game] missing 'exe' or 'rom' field", config_path.string())); } const fs::path exe_path = PSXRecompV4::host_resolve(root, exe_field); // Auto-detect EXE header values for any field not explicitly set in TOML. // Parses the PS-X EXE header once and fills in load_address, entry_pc, // text_size, and stack_base from the binary. If the EXE cannot be read // (malformed header, file not found) and a required field is missing // from TOML, the error surfaces when the EXE parse itself fails. struct ExeHeaderAuto { bool ok = false; uint32_t load_address = 0; uint32_t entry_pc = 0; uint32_t text_size = 0; uint32_t stack_base = 0; }; ExeHeaderAuto auto_hdr; // We only parse the EXE header if at least one field is missing from TOML. const bool need_auto = !game.contains("load_address") || !game.contains("entry_pc") || !game.contains("text_size") || !game.contains("stack_base"); if (need_auto) { std::string err; if (auto exe = PSXRecomp::PS1ExeParser::parse_file(exe_path, err)) { auto_hdr.ok = true; auto_hdr.load_address = exe->load_address(); auto_hdr.entry_pc = exe->entry_point(); auto_hdr.text_size = exe->code_size(); auto_hdr.stack_base = exe->header.stack_base; // Round text_size up to next 4K page if it's not already // page-aligned (the runtime's overlay region floor uses this // as a guard, and the text segment in RAM is always fully // page-reserved even if the EXE doesn't fill the last page). if (auto_hdr.text_size & 0xFFF) { auto_hdr.text_size = (auto_hdr.text_size + 0xFFF) & ~0xFFFu; } } } const uint32_t load_address = game.contains("load_address") ? parse_hex(toml::find(game, "load_address"), "game.load_address") : (auto_hdr.ok ? auto_hdr.load_address : []() -> uint32_t { throw std::runtime_error( "game.toml: missing 'load_address' in [game] and could not " "auto-detect from EXE header"); }()); const uint32_t entry_pc = game.contains("entry_pc") ? parse_hex(toml::find(game, "entry_pc"), "game.entry_pc") : (auto_hdr.ok ? auto_hdr.entry_pc : load_address); const uint32_t text_size = game.contains("text_size") ? parse_hex(toml::find(game, "text_size"), "game.text_size") : (auto_hdr.ok ? auto_hdr.text_size : []() -> uint32_t { throw std::runtime_error( "game.toml: missing 'text_size' in [game] and could not " "auto-detect from EXE header"); }()); const uint32_t stack_base = game.contains("stack_base") ? parse_hex(toml::find(game, "stack_base"), "game.stack_base") : (auto_hdr.ok && auto_hdr.stack_base != 0 ? auto_hdr.stack_base : 0x801FFFF0u); // Disc paths: accept either single `disc` or array `discs`. std::vector discs; if (game.contains("discs")) { const auto& arr = toml::find>(game, "discs"); for (const auto& d : arr) discs.push_back(PSXRecompV4::host_resolve(root, d)); } else if (game.contains("disc")) { const auto& d = toml::find(game, "disc"); discs.push_back(PSXRecompV4::host_resolve(root, d)); } /* Per-disc serials, parallel to `discs`. Absent => no per-disc gate. */ std::vector disc_serials; if (game.contains("disc_serials")) disc_serials = toml::find>(game, "disc_serials"); // Optional expected disc identity (launcher verification badge). bool has_disc_crc = false; uint32_t disc_crc = 0; std::string disc_sha1; if (game.contains("disc_crc")) { disc_crc = parse_hex(toml::find(game, "disc_crc"), "game.disc_crc"); has_disc_crc = true; } if (game.contains("disc_sha1")) { disc_sha1 = toml::find(game, "disc_sha1"); // normalize to lowercase hex for (char& c : disc_sha1) c = (char)std::tolower((unsigned char)c); } // [netplay] — mount / TOC policy for online (see DiscIdentity::disc_fp). bool netplay_require_cue = false; int netplay_required_tracks = 0; bool has_netplay_required_leadout = false; uint32_t netplay_required_leadout_lba = 0; std::string netplay_required_disc_fp; std::vector netplay_required_disc_fps; std::string netplay_local_viewport; std::string netplay_local_viewport_aspect; std::string netplay_local_viewport_renderer; std::vector netplay_local_viewport_width_sites; uint32_t netplay_local_viewport_state_addr = 0; std::vector netplay_local_viewport_state_values; bool netplay_content_negotiation = false; if (cfg.contains("netplay")) { const toml::value& np = toml::find(cfg, "netplay"); if (np.contains("content_negotiation")) netplay_content_negotiation = toml::find(np, "content_negotiation"); if (np.contains("require_cue")) netplay_require_cue = toml::find(np, "require_cue"); if (np.contains("required_tracks")) netplay_required_tracks = toml::find(np, "required_tracks"); if (np.contains("required_leadout_lba")) { netplay_required_leadout_lba = (uint32_t)toml::find(np, "required_leadout_lba"); has_netplay_required_leadout = true; } if (np.contains("required_disc_fp")) { netplay_required_disc_fp = toml::find(np, "required_disc_fp"); for (char& c : netplay_required_disc_fp) c = (char)std::tolower((unsigned char)c); } /* Per-disc fingerprints for a multi-disc set, parallel to [game] * discs. Lowercased here so the runtime compares like for like. */ if (np.contains("required_disc_fps")) { netplay_required_disc_fps = toml::find>(np, "required_disc_fps"); for (std::string& fp : netplay_required_disc_fps) for (char& c : fp) c = (char)std::tolower((unsigned char)c); } if (np.contains("local_viewport")) { netplay_local_viewport = toml::find(np, "local_viewport"); for (char& c : netplay_local_viewport) c = (char)std::tolower((unsigned char)c); if (!netplay_local_viewport.empty() && netplay_local_viewport != "vertical_split") { throw std::runtime_error(fmt::format( "[netplay] local_viewport must be \"vertical_split\", got '{}'", netplay_local_viewport)); } } if (np.contains("local_viewport_aspect")) { netplay_local_viewport_aspect = toml::find(np, "local_viewport_aspect"); for (char& c : netplay_local_viewport_aspect) c = (char)std::tolower((unsigned char)c); if (!netplay_local_viewport_aspect.empty() && netplay_local_viewport_aspect != "16:9" && netplay_local_viewport_aspect != "21:9" && netplay_local_viewport_aspect != "fixed" && netplay_local_viewport_aspect != "adaptive") { throw std::runtime_error(fmt::format( "[netplay] local_viewport_aspect must be \"16:9\", " "\"21:9\", \"fixed\", or \"adaptive\", got '{}'", netplay_local_viewport_aspect)); } if (!netplay_local_viewport_aspect.empty() && netplay_local_viewport.empty()) { throw std::runtime_error( "[netplay] local_viewport_aspect requires local_viewport"); } } if (np.contains("local_viewport_width_sites")) { for (const auto& value : toml::find>(np, "local_viewport_width_sites")) netplay_local_viewport_width_sites.push_back(parse_hex(value, "netplay.local_viewport_width_sites")); } if (np.contains("local_viewport_renderer")) { netplay_local_viewport_renderer = toml::find(np, "local_viewport_renderer"); for (char& c : netplay_local_viewport_renderer) c = (char)std::tolower((unsigned char)c); if (netplay_local_viewport_renderer != "native_wide" && netplay_local_viewport_renderer != "projection") { throw std::runtime_error(fmt::format( "[netplay] local_viewport_renderer must be \"native_wide\" " "or \"projection\", got '{}'", netplay_local_viewport_renderer)); } if (netplay_local_viewport.empty()) { throw std::runtime_error( "[netplay] local_viewport_renderer requires local_viewport"); } } if (!netplay_local_viewport_width_sites.empty()) { if (netplay_local_viewport_renderer != "projection") throw std::runtime_error("[netplay] local_viewport_width_sites requires projection renderer"); if (netplay_local_viewport_width_sites.size() > 16) throw std::runtime_error("[netplay] local_viewport_width_sites supports at most 16 sites"); std::unordered_set unique; for (uint32_t pc : netplay_local_viewport_width_sites) { if ((pc & 3) || (pc & 0x1fffffff) >= 0x800000) throw std::runtime_error("[netplay] local_viewport_width_sites requires aligned main RAM addresses"); if (!unique.insert(pc & 0x1fffffff).second) throw std::runtime_error("[netplay] duplicate local_viewport_width_sites address"); } } { const bool has_addr = np.contains("local_viewport_state_addr"); const bool has_values = np.contains("local_viewport_state_values"); if (has_addr != has_values) throw std::runtime_error( "[netplay] local_viewport_state_addr and " "local_viewport_state_values must be set together"); if (has_addr) { if (netplay_local_viewport.empty()) throw std::runtime_error( "[netplay] local_viewport_state_addr requires local_viewport"); netplay_local_viewport_state_addr = parse_hex( toml::find(np, "local_viewport_state_addr"), "netplay.local_viewport_state_addr"); for (const auto& value : toml::find>( np, "local_viewport_state_values")) netplay_local_viewport_state_values.push_back(parse_hex( value, "netplay.local_viewport_state_values")); if (netplay_local_viewport_state_values.empty()) throw std::runtime_error( "[netplay] local_viewport_state_values must not be empty"); } } } // [recompiler] if (!cfg.contains("recompiler")) { throw std::runtime_error( fmt::format("{}: missing [recompiler] block", config_path.string())); } const toml::value& recomp = toml::find(cfg, "recompiler"); if (!recomp.contains("seeds")) { throw std::runtime_error( fmt::format("{}: [recompiler] missing 'seeds' field", config_path.string())); } const fs::path seeds_path = PSXRecompV4::host_resolve(root, toml::find(recomp, "seeds")); fs::path bios_thunks_path; if (recomp.contains("bios_thunks")) { bios_thunks_path = PSXRecompV4::host_resolve(root, toml::find(recomp, "bios_thunks")); } // [recompiler] bios_config — the BIOS profile this game is built // against. Optional: main_psx falls back to the SCPH1001 profile // (bios/SCPH1001.toml, in-repo or under the psxrecomp/ submodule). fs::path bios_config_path; if (recomp.contains("bios_config")) { bios_config_path = PSXRecompV4::host_resolve(root, toml::find(recomp, "bios_config")); } const std::string out_dir_field = recomp.contains("out_dir") ? toml::find(recomp, "out_dir") : std::string{"generated"}; const fs::path out_dir = PSXRecompV4::host_resolve(root, out_dir_field); const bool strict = recomp.contains("strict") ? toml::find(recomp, "strict") : true; const std::string discovery = recomp.contains("discovery") ? toml::find(recomp, "discovery") : std::string{"whole-image"}; if (discovery != "whole-image" && discovery != "reachable") { throw std::runtime_error(fmt::format( "{}: [recompiler].discovery must be 'whole-image' or 'reachable'", config_path.string())); } std::string out_stem; if (recomp.contains("out_stem")) { out_stem = toml::find(recomp, "out_stem"); } else { out_stem = derive_out_stem(fs::path(exe_field).filename().string()); } std::vector recompiler_patches; if (recomp.contains("patch")) { const auto& patches = toml::find(recomp, "patch"); std::set seen_ids; std::set seen_addresses; for (const auto& item : patches) { RecompilerPatch patch; patch.id = toml::find(item, "id"); patch.address = parse_hex(toml::find(item, "address"), "recompiler.patch.address"); patch.expected = parse_hex(toml::find(item, "expected"), "recompiler.patch.expected"); patch.replacement = parse_hex( toml::find(item, "replacement"), "recompiler.patch.replacement"); if (item.contains("note")) { patch.note = toml::find(item, "note"); } if (patch.id.empty()) { throw std::runtime_error( "recompiler.patch.id must not be empty"); } if ((patch.address & 3u) != 0u) { throw std::runtime_error(fmt::format( "{}: recompiler patch '{}' address 0x{:08X} is not " "instruction-aligned", config_path.string(), patch.id, patch.address)); } if (!seen_ids.insert(patch.id).second) { throw std::runtime_error(fmt::format( "{}: duplicate [[recompiler.patch]] id '{}'", config_path.string(), patch.id)); } const uint32_t address_key = recompiler_patch_address_key(patch.address); if (!seen_addresses.insert(address_key).second) { throw std::runtime_error(fmt::format( "{}: duplicate [[recompiler.patch]] physical address " "0x{:08X} (KUSEG/KSEG aliases are one site)", config_path.string(), address_key)); } recompiler_patches.push_back(std::move(patch)); } } // Optional [widescreen] block — per-game hooks for the widescreen hack. std::vector ws_sprite_tag_funcs; uint32_t ws_sprite_anchor_addr = 0; bool ws_hud_sprt_squash = false; bool ws_auto_ui_squash = false; bool ws_auto_ui_in_place = false; bool ws_auto_ui_proportional = false; bool ws_full_2d = false; bool ws_gte_game_mode = false; bool ws_precise_nclip = false; uint32_t ws_gameplay_state_addr = 0; std::vector ws_gameplay_state_values; bool ws_native_wide = true; bool ws_nw_hud_corners = false; uint32_t ws_nw_left_hud_packet_lo = 0; uint32_t ws_nw_left_hud_packet_hi = 0; bool ws_nw_backdrop = false; bool ws_clear_reveal = false; bool ws_nw_flat_backdrop = false; bool ws_nw_phase_backdrop = false; bool ws_nw_textured_edges = false; int ws_nw_textured_edge_scale = 0; bool ws_nw_full_mirror = false; std::vector ws_signed_x_bound_sites; bool ws_offered = true; bool vulkan_offered = false; bool ws_adaptive_view = false; bool ws_ultrawide_offered = false; if (cfg.contains("video")) { const toml::value& video = toml::find(cfg, "video"); if (video.contains("offer_vulkan")) vulkan_offered = toml::find(video, "offer_vulkan"); } // Optional [data_shards] block — memoized pure-function replay hooks. std::vector data_shard_funcs; if (cfg.contains("data_shards")) { const toml::value& dsv = toml::find(cfg, "data_shards"); if (dsv.contains("funcs")) { const auto& arr = toml::find>(dsv, "funcs"); for (const auto& a : arr) data_shard_funcs.push_back(parse_hex(a, "data_shards.funcs")); } } // Optional [recompiler] hot_funcs — __attribute__((hot)) on emitted C. // Optional trusted, statically linked game-mod entry hooks. std::vector mod_instruction_sites; if (recomp.contains("mod_instruction_sites")) for (const auto& a : toml::find>(recomp, "mod_instruction_sites")) { const auto pc = parse_hex(a, "recompiler.mod_instruction_sites"); if ((pc & 3u) || pc >= 0xC0000000u || (pc & 0x1FFFFFFFu) >= 0x00800000u) throw std::runtime_error("mod instruction site must be aligned main RAM"); mod_instruction_sites.push_back(pc); } std::vector mod_function_entry_funcs; if (recomp.contains("mod_function_entry_funcs")) { const auto& arr = toml::find>( recomp, "mod_function_entry_funcs"); for (const auto& a : arr) mod_function_entry_funcs.push_back( parse_hex(a, "recompiler.mod_function_entry_funcs")); } std::vector hot_funcs; if (recomp.contains("hot_funcs")) { const auto& arr = toml::find>(recomp, "hot_funcs"); for (const auto& a : arr) hot_funcs.push_back(parse_hex(a, "recompiler.hot_funcs")); } // Optional emitter-level load-charge batching for VLC leaves. bool load_charge_batch = false; std::vector load_charge_batch_funcs; if (recomp.contains("load_charge_batch")) load_charge_batch = toml::find(recomp, "load_charge_batch"); if (recomp.contains("load_charge_batch_funcs")) { const auto& arr = toml::find>(recomp, "load_charge_batch_funcs"); for (const auto& a : arr) load_charge_batch_funcs.push_back( parse_hex(a, "recompiler.load_charge_batch_funcs")); } if (load_charge_batch && load_charge_batch_funcs.empty()) load_charge_batch_funcs = hot_funcs; uint32_t vsync_query_func = 0; uint32_t vsync_counter_addr = 0; uint32_t vsync_gpustat_ptr_addr = 0; uint32_t vsync_timer1_ptr_addr = 0; uint32_t vsync_timer1_cache_addr = 0; std::vector vsync_event_horizon_sites; std::vector vsync_event_horizon_extra_sites; bool vsync_event_horizon_any = false; if (cfg.contains("load_accel")) { const toml::value& lav = toml::find(cfg, "load_accel"); if (lav.contains("vsync_query")) { const toml::value& vq = toml::find(lav, "vsync_query"); const bool has_func = vq.contains("func"); const bool has_counter = vq.contains("counter_addr"); const bool has_gpu_ptr = vq.contains("gpustat_ptr_addr"); const bool has_timer_ptr = vq.contains("timer1_ptr_addr"); const bool has_timer_cache = vq.contains("timer1_cache_addr"); const int present = (int)has_func + (int)has_counter + (int)has_gpu_ptr + (int)has_timer_ptr + (int)has_timer_cache; if (present != 0 && present != 5) throw std::runtime_error(fmt::format( "{}: [load_accel.vsync_query] func, counter_addr, gpustat_ptr_addr, " "timer1_ptr_addr, and timer1_cache_addr must be set together", config_path.string())); if (has_func) { vsync_query_func = parse_hex( toml::find(vq, "func"), "load_accel.vsync_query.func"); vsync_counter_addr = parse_hex( toml::find(vq, "counter_addr"), "load_accel.vsync_query.counter_addr"); vsync_gpustat_ptr_addr = parse_hex( toml::find(vq, "gpustat_ptr_addr"), "load_accel.vsync_query.gpustat_ptr_addr"); vsync_timer1_ptr_addr = parse_hex( toml::find(vq, "timer1_ptr_addr"), "load_accel.vsync_query.timer1_ptr_addr"); vsync_timer1_cache_addr = parse_hex( toml::find(vq, "timer1_cache_addr"), "load_accel.vsync_query.timer1_cache_addr"); if (vq.contains("event_horizon_sites")) { const auto& arr = toml::find>( vq, "event_horizon_sites"); for (const auto& a : arr) vsync_event_horizon_sites.push_back(parse_hex( a, "load_accel.vsync_query.event_horizon_sites")); } if (vq.contains("event_horizon_extra_sites")) { const auto& arr = toml::find>( vq, "event_horizon_extra_sites"); for (const auto& a : arr) vsync_event_horizon_extra_sites.push_back(parse_hex( a, "load_accel.vsync_query.event_horizon_extra_sites")); } if (vq.contains("event_horizon_any")) vsync_event_horizon_any = toml::find(vq, "event_horizon_any"); } } } if (cfg.contains("widescreen")) { const toml::value& ws = toml::find(cfg, "widescreen"); if (ws.contains("sprite_tag_funcs")) { const auto& arr = toml::find>(ws, "sprite_tag_funcs"); for (const auto& a : arr) ws_sprite_tag_funcs.push_back( parse_hex(a, "widescreen.sprite_tag_funcs")); } if (ws.contains("sprite_anchor_addr")) { ws_sprite_anchor_addr = parse_hex( toml::find(ws, "sprite_anchor_addr"), "widescreen.sprite_anchor_addr"); } if (!ws_sprite_tag_funcs.empty() && ws_sprite_anchor_addr == 0) throw std::runtime_error(fmt::format( "{}: [widescreen] sprite_tag_funcs requires sprite_anchor_addr", config_path.string())); if (ws.contains("hud_sprt_squash")) ws_hud_sprt_squash = toml::find(ws, "hud_sprt_squash"); if (ws.contains("auto_ui_squash")) ws_auto_ui_squash = toml::find(ws, "auto_ui_squash"); if (ws.contains("auto_ui_anchor")) { const auto anchor = toml::find(ws, "auto_ui_anchor"); if (anchor == "in_place") ws_auto_ui_in_place = true; else if (anchor != "edges") throw std::runtime_error(fmt::format( "{}: [widescreen] auto_ui_anchor must be \"edges\" or " "\"in_place\", got \"{}\"", config_path.string(), anchor)); } if (ws.contains("auto_ui_size")) { const auto size = toml::find(ws, "auto_ui_size"); if (size == "proportional") ws_auto_ui_proportional = true; else if (size != "original") throw std::runtime_error(fmt::format( "{}: [widescreen] auto_ui_size must be \"original\" or " "\"proportional\", got \"{}\"", config_path.string(), size)); } if (ws.contains("full_2d")) ws_full_2d = toml::find(ws, "full_2d"); if (ws.contains("gte_game_mode")) ws_gte_game_mode = toml::find(ws, "gte_game_mode"); if (ws.contains("precise_nclip")) ws_precise_nclip = toml::find(ws, "precise_nclip"); const bool has_gameplay_state_addr = ws.contains("gameplay_state_addr"); const bool has_gameplay_state_values = ws.contains("gameplay_state_values"); if (has_gameplay_state_addr != has_gameplay_state_values) throw std::runtime_error(fmt::format( "{}: [widescreen] gameplay_state_addr and " "gameplay_state_values must be set together", config_path.string())); if (has_gameplay_state_addr) { ws_gameplay_state_addr = parse_hex( toml::find(ws, "gameplay_state_addr"), "widescreen.gameplay_state_addr"); const auto& arr = toml::find>( ws, "gameplay_state_values"); for (const auto& value : arr) ws_gameplay_state_values.push_back(parse_hex( value, "widescreen.gameplay_state_values")); if (ws_gameplay_state_values.empty()) throw std::runtime_error(fmt::format( "{}: [widescreen] gameplay_state_values must not be empty", config_path.string())); } if (ws.contains("native_wide")) ws_native_wide = toml::find(ws, "native_wide"); if (ws.contains("nw_hud_corners")) ws_nw_hud_corners = toml::find(ws, "nw_hud_corners"); const bool has_nw_left_hud_lo = ws.contains("nw_left_hud_packet_lo"); const bool has_nw_left_hud_hi = ws.contains("nw_left_hud_packet_hi"); if (has_nw_left_hud_lo != has_nw_left_hud_hi) throw std::runtime_error(fmt::format( "{}: [widescreen] nw_left_hud_packet_lo and " "nw_left_hud_packet_hi must be set together", config_path.string())); if (has_nw_left_hud_lo) { ws_nw_left_hud_packet_lo = parse_hex( toml::find(ws, "nw_left_hud_packet_lo"), "widescreen.nw_left_hud_packet_lo"); ws_nw_left_hud_packet_hi = parse_hex( toml::find(ws, "nw_left_hud_packet_hi"), "widescreen.nw_left_hud_packet_hi"); if (ws_nw_left_hud_packet_lo >= ws_nw_left_hud_packet_hi) throw std::runtime_error(fmt::format( "{}: [widescreen] nw_left_hud_packet range is empty or reversed", config_path.string())); } if (ws.contains("nw_backdrop")) ws_nw_backdrop = toml::find(ws, "nw_backdrop"); if (ws.contains("clear_reveal")) ws_clear_reveal = toml::find(ws, "clear_reveal"); if (ws.contains("nw_flat_backdrop")) ws_nw_flat_backdrop = toml::find(ws, "nw_flat_backdrop"); if (ws.contains("nw_phase_backdrop")) ws_nw_phase_backdrop = toml::find(ws, "nw_phase_backdrop"); if (ws.contains("nw_textured_edges")) ws_nw_textured_edges = toml::find(ws, "nw_textured_edges"); if (ws.contains("nw_textured_edge_scale")) { ws_nw_textured_edge_scale = toml::find(ws, "nw_textured_edge_scale"); if (ws_nw_textured_edge_scale != 0 && (ws_nw_textured_edge_scale < 100 || ws_nw_textured_edge_scale > 400)) throw std::runtime_error(fmt::format( "{}: [widescreen] nw_textured_edge_scale must be 0 or in [100, 400]", config_path.string())); } if (ws.contains("nw_full_mirror")) ws_nw_full_mirror = toml::find(ws, "nw_full_mirror"); if (ws.contains("signed_x_bound")) { std::set seen; for (const auto& item : toml::find(ws, "signed_x_bound")) { WidescreenSignedBoundSite site; site.address = parse_hex(toml::find(item, "address"), "widescreen.signed_x_bound.address"); site.expected = parse_hex(toml::find(item, "expected"), "widescreen.signed_x_bound.expected"); const uint32_t opcode = site.expected >> 26; if (opcode != 0x0Fu && opcode != 0x09u && opcode != 0x0Du) throw std::runtime_error(fmt::format( "{}: [[widescreen.signed_x_bound]] expected must be LUI or ADDIU/ORI", config_path.string())); if (opcode == 0x09u || opcode == 0x0Du) { const uint32_t rs = (site.expected >> 21) & 0x1Fu; const uint32_t rt = (site.expected >> 16) & 0x1Fu; const uint32_t imm = site.expected & 0xFFFFu; if (rs != 0u) throw std::runtime_error(fmt::format( "{}: [[widescreen.signed_x_bound]] ADDIU/ORI expected must use rs=$zero", config_path.string())); if (rt == 0u) throw std::runtime_error(fmt::format( "{}: [[widescreen.signed_x_bound]] ADDIU/ORI expected must not write $zero", config_path.string())); if (imm == 0u) throw std::runtime_error(fmt::format( "{}: [[widescreen.signed_x_bound]] ADDIU/ORI expected must have a non-zero signed screen edge", config_path.string())); } if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error(fmt::format( "{}: duplicate [[widescreen.signed_x_bound]] address 0x{:08X}", config_path.string(), site.address)); ws_signed_x_bound_sites.push_back(site); } } if (ws.contains("offer")) ws_offered = toml::find(ws, "offer"); if (ws.contains("offer_ultrawide")) ws_ultrawide_offered = toml::find(ws, "offer_ultrawide"); if (ws.contains("adaptive_view")) ws_adaptive_view = toml::find(ws, "adaptive_view"); } // Optional [widescreen.cull] block — world-space draw-cull widening. std::vector ws_cull_bias_sites, ws_cull_range_sites, ws_cull_a1_sites; std::vector ws_cull_bias_lower_sites; std::vector ws_cull_screen_x_sites; std::vector ws_cull_slti_sites; std::vector ws_cull_slti_lower_sites; std::vector ws_cull_bltz_sites; std::vector ws_cull_negsub_sites; std::vector ws_cull_vxrange_sites; std::vector ws_cull_depth_sites; std::vector ws_cull_plane_nx_sites; std::vector ws_cull_xclip_load_sites; std::vector ws_cull_nclip_keep_sites; std::vector ws_cull_nclip_exact_sites; std::vector ws_cull_branch_keep_sites; std::vector ws_cull_bgez_sites; std::vector ws_cull_clip_edge_x_load_sites; uint32_t ws_cull_clip_edge_width = 0; std::vector ws_cull_keep_sites; std::vector ws_cull_sxy_sites; std::vector ws_cull_masked_reject_sites; std::vector ws_cull_packed_x_sites; std::vector ws_cull_scale_sites; std::vector ws_cull_angle_sites; WidescreenAspectConeConfig ws_aspect_cone; int ws_cull_guard_pixels = 0; int ws_cull_activation_guard_pixels = 0; // Cull-signature immediates (screen_w_imms / screen_h_imms). Defaults are // the original Tomba signature (320-display: 0x140/0x141 + 0xE0/0xF1); a // game with a different display width overrides them (Ape Escape: 0x181). std::vector ws_cull_w_imms = { 0x140, 0x141 }; std::vector ws_cull_h_imms = { 0xE0, 0xF1 }; bool ws_auto_screen_x_cull = false; bool ws_auto_backdrop_preload = false; if (cfg.contains("widescreen")) { const toml::value& ws = toml::find(cfg, "widescreen"); if (ws.contains("cull")) { const toml::value& cull = toml::find(ws, "cull"); auto load_sites = [&](const char* key, std::vector& out) { if (!cull.contains(key)) return; for (const auto& a : toml::find>(cull, key)) out.push_back(parse_hex(a, fmt::format("widescreen.cull.{}", key))); }; load_sites("bias_sites", ws_cull_bias_sites); load_sites("bias_lower_sites", ws_cull_bias_lower_sites); load_sites("range_sites", ws_cull_range_sites); load_sites("a1_sites", ws_cull_a1_sites); load_sites("screen_x_sites", ws_cull_screen_x_sites); load_sites("slti_sites", ws_cull_slti_sites); load_sites("slti_lower_sites", ws_cull_slti_lower_sites); load_sites("bltz_sites", ws_cull_bltz_sites); load_sites("negsub_sites", ws_cull_negsub_sites); load_sites("vxrange_sites", ws_cull_vxrange_sites); load_sites("depth_sites", ws_cull_depth_sites); load_sites("plane_nx_sites", ws_cull_plane_nx_sites); load_sites("xclip_load_sites", ws_cull_xclip_load_sites); load_sites("nclip_keep_sites", ws_cull_nclip_keep_sites); load_sites("nclip_exact_sites", ws_cull_nclip_exact_sites); load_sites("branch_keep_sites", ws_cull_branch_keep_sites); load_sites("bgez_sites", ws_cull_bgez_sites); load_sites("clip_edge_x_load_sites", ws_cull_clip_edge_x_load_sites); if (cull.contains("clip_edge_width")) { const int64_t width = toml::find(cull, "clip_edge_width"); if (width <= 0 || width > 1024) throw std::runtime_error(fmt::format( "{}: [widescreen.cull] clip_edge_width must be 1..1024", config_path.string())); ws_cull_clip_edge_width = (uint32_t)width; } if (cull.contains("masked_reject")) { std::set seen; for (const auto& item : toml::find(cull, "masked_reject")) { WidescreenMaskedRejectSite site; site.address = parse_hex(toml::find(item, "address"), "widescreen.cull.masked_reject.address"); site.expected = parse_hex(toml::find(item, "expected"), "widescreen.cull.masked_reject.expected"); site.reject_mask = parse_hex(toml::find(item, "reject_mask"), "widescreen.cull.masked_reject.reject_mask"); if ((site.address & 3u) || (site.expected >> 26) != 5u || ((site.expected >> 16) & 31u) != 0u || ((site.expected >> 21) & 31u) == 0u || !site.reject_mask) throw std::runtime_error("masked_reject needs aligned BNE reg,zero and nonzero reject_mask"); if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error("duplicate masked_reject address"); ws_cull_masked_reject_sites.push_back(site); if (ws_cull_masked_reject_sites.size() > 256) throw std::runtime_error("masked_reject supports at most 256 sites"); } } if (cull.contains("packed_x")) { std::set seen; for (const auto& item : toml::find(cull, "packed_x")) { WidescreenPackedXSite site; site.address = parse_hex(toml::find(item, "address"), "widescreen.cull.packed_x.address"); site.expected = parse_hex(toml::find(item, "expected"), "widescreen.cull.packed_x.expected"); if ((site.address & 3u) || (site.expected >> 26) != 0u || (site.expected & 0x7FFu) != 0x2Bu || ((site.expected >> 21) & 31u) == 0u || ((site.expected >> 16) & 31u) == 0u || ((site.expected >> 11) & 31u) == 0u) throw std::runtime_error("packed_x needs aligned SLTU rd,rs,rt with nonzero registers"); if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error("duplicate packed_x address"); ws_cull_packed_x_sites.push_back(site); if (ws_cull_packed_x_sites.size() > 256) throw std::runtime_error("packed_x supports at most 256 sites"); } } if (cull.contains("keep")) { std::set seen; for (const auto& item : toml::find(cull, "keep")) { WidescreenCullKeepSite site; site.address = parse_hex(toml::find(item, "address"), "widescreen.cull.keep.address"); site.expected = parse_hex(toml::find(item, "expected"), "widescreen.cull.keep.expected"); site.result = (uint32_t)toml::find(item, "result"); const uint32_t op = site.expected >> 26; const uint32_t fn = site.expected & 0x3Fu; if (!((op == 0x0Au) || (op == 0x0Bu) || (op == 0u && (fn == 0x2Au || fn == 0x2Bu)))) { throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.keep]] expected must be " "SLT/SLTU/SLTI/SLTIU", config_path.string())); } if (site.result > 1u) { throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.keep]] result must be 0 or 1", config_path.string())); } if (!seen.insert(site.address & 0x1FFFFFFFu).second) { throw std::runtime_error(fmt::format( "{}: duplicate [[widescreen.cull.keep]] address 0x{:08X}", config_path.string(), site.address)); } ws_cull_keep_sites.push_back(site); } } if (cull.contains("sxy")) { std::set seen; for (const auto& item : toml::find(cull, "sxy")) { ws_cull_sxy_sites.push_back(parse_sxy_cull_site( item, config_path.string(), seen)); if (ws_cull_sxy_sites.size() > 64) throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.sxy]] supports at most 64 sites", config_path.string())); } } if (cull.contains("scale")) { std::set seen; for (const auto& item : toml::find(cull, "scale")) { WidescreenCullScaleSite site; site.address = parse_hex(toml::find(item, "address"), "widescreen.cull.scale.address"); site.expected = parse_hex(toml::find(item, "expected"), "widescreen.cull.scale.expected"); const std::string operand = toml::find(item, "operand"); if (operand == "rs") site.operand = 0; else if (operand == "rt") site.operand = 1; else throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.scale]] operand must be rs or rt", config_path.string())); const int half = toml::find(item, "half_extent"); if (half <= 0 || half > 1024) throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.scale]] half_extent must be 1..1024", config_path.string())); site.half_extent = (uint32_t)half; const uint32_t op = site.expected >> 26; const uint32_t fn = site.expected & 0x3Fu; if (!(op == 0u && (fn == 0x2Au || fn == 0x2Bu))) throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.scale]] expected must be SLT/SLTU", config_path.string())); if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error(fmt::format( "{}: duplicate [[widescreen.cull.scale]] address 0x{:08X}", config_path.string(), site.address)); ws_cull_scale_sites.push_back(site); } } if (cull.contains("angle")) { std::set seen; for (const auto& item : toml::find(cull, "angle")) { WidescreenAngleSite site; site.address = parse_hex( toml::find(item, "address"), "widescreen.cull.angle.address"); site.expected = parse_hex( toml::find(item, "expected"), "widescreen.cull.angle.expected"); const uint32_t op = site.expected >> 26; const uint32_t rs = (site.expected >> 21) & 31u; const int32_t imm = (int16_t)(site.expected & 0xFFFFu); if ((op != 0x08u && op != 0x09u) || rs != 0u || imm <= 0 || imm >= 1024) { throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.angle]] expected must be " "ADDI/ADDIU rt,zero,imm with imm in [1, 1023]", config_path.string())); } if ((site.address & 3u) != 0) throw std::runtime_error(fmt::format( "{}: cull-angle address 0x{:08X} is not " "instruction-aligned", config_path.string(), site.address)); if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error(fmt::format( "{}: duplicate cull-angle address 0x{:08X}", config_path.string(), site.address)); ws_cull_angle_sites.push_back(site); } } if (cull.contains("aspect_cone")) { const toml::value& cone = toml::find(cull, "aspect_cone"); auto cone_hex = [&](const char *key) { return parse_hex(toml::find(cone, key), fmt::format("widescreen.cull.aspect_cone.{}", key)); }; ws_aspect_cone.forward_addr = cone_hex("forward_addr"); const int object_type_offset = toml::find(cone, "object_type_offset"); const int object_reg = toml::find(cone, "object_reg"); const int x_reg = toml::find(cone, "x_reg"); const int z_reg = toml::find(cone, "z_reg"); const int y_reg = toml::find(cone, "y_reg"); const int hysteresis_pixels = toml::find_or(cone, "hysteresis_pixels", 0); const int queue_reserve = toml::find_or(cone, "queue_reserve", 0); if (object_type_offset < 0 || object_type_offset > 0xFFFF) { throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] " "object_type_offset must be in [0, 65535]", config_path.string())); } if (object_reg < 0 || object_reg > 31 || x_reg < 0 || x_reg > 31 || z_reg < 0 || z_reg > 31 || y_reg < 0 || y_reg > 31) { throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] register indices " "must be in [0, 31]", config_path.string())); } if (hysteresis_pixels < 0 || hysteresis_pixels > 256 || queue_reserve < 0 || queue_reserve > 256) { throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] hysteresis_pixels " "and queue_reserve must be in [0, 256]", config_path.string())); } if ((ws_aspect_cone.forward_addr & 1u) != 0) { throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] forward_addr " "must be halfword-aligned", config_path.string())); } ws_aspect_cone.object_type_offset = (uint32_t)object_type_offset; ws_aspect_cone.object_reg = (uint32_t)object_reg; ws_aspect_cone.x_reg = (uint32_t)x_reg; ws_aspect_cone.z_reg = (uint32_t)z_reg; ws_aspect_cone.y_reg = (uint32_t)y_reg; ws_aspect_cone.hysteresis_pixels = (uint32_t)hysteresis_pixels; ws_aspect_cone.queue_reserve = (uint32_t)queue_reserve; auto load_cone_hex_array = [&](const char *key, std::array& out) { const auto values = toml::find>(cone, key); if (values.size() != out.size()) throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] {} must " "contain exactly three values", config_path.string(), key)); for (size_t i = 0; i < out.size(); i++) out[i] = parse_hex( values[i], fmt::format("widescreen.cull.aspect_cone.{}", key)); }; auto load_cone_int_array = [&](const char *key, std::array& out) { const auto values = toml::find>(cone, key); if (values.size() != out.size()) throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] {} must " "contain exactly three values", config_path.string(), key)); for (size_t i = 0; i < out.size(); i++) { if (values[i] < 0) throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] {} " "values must be non-negative", config_path.string(), key)); out[i] = (uint32_t)values[i]; } }; load_cone_hex_array("queue_count_addrs", ws_aspect_cone.queue_count_addrs); load_cone_int_array("queue_capacities", ws_aspect_cone.queue_capacities); load_cone_hex_array("queue_type_masks", ws_aspect_cone.queue_type_masks); uint32_t used_type_mask = 0; for (size_t i = 0; i < 3; i++) { if ((ws_aspect_cone.queue_count_addrs[i] & 1u) != 0) throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] " "queue_count_addrs values must be " "halfword-aligned", config_path.string())); if (ws_aspect_cone.queue_capacities[i] == 0 || ws_aspect_cone.queue_capacities[i] > 0x7FFFu) throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] " "queue_capacities values must be in [1, 32767]", config_path.string())); if (ws_aspect_cone.queue_reserve >= ws_aspect_cone.queue_capacities[i]) throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] " "queue_reserve must be smaller than every " "queue capacity", config_path.string())); if ((used_type_mask & ws_aspect_cone.queue_type_masks[i]) != 0) throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] " "queue_type_masks must not overlap", config_path.string())); used_type_mask |= ws_aspect_cone.queue_type_masks[i]; } if (!cone.contains("sites")) throw std::runtime_error(fmt::format( "{}: [widescreen.cull.aspect_cone] requires at least " "one [[...sites]] entry", config_path.string())); std::set seen; for (const auto& item : toml::find(cone, "sites")) { WidescreenAspectConeSite site; site.address = parse_hex( toml::find(item, "address"), "widescreen.cull.aspect_cone.sites.address"); site.expected = parse_hex( toml::find(item, "expected"), "widescreen.cull.aspect_cone.sites.expected"); const uint32_t opcode = site.expected >> 26; const bool is_slti = opcode == 0x0Au; const bool is_slt = opcode == 0u && (site.expected & 0x3Fu) == 0x2Au && ((site.expected >> 6) & 0x1Fu) == 0u; if (!is_slti && !is_slt) throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.aspect_cone.sites]] " "expected must be signed SLTI or SLT", config_path.string())); const int threshold = toml::find_or( item, "cosine_threshold", is_slti ? (int)(site.expected & 0xFFFFu) : -1); if (threshold < 1 || threshold > 1023) throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.aspect_cone.sites]] " "cosine_threshold must be in [1, 1023] " "(and is required for SLT)", config_path.string())); site.cosine_threshold = (uint32_t)threshold; auto site_reg = [&](const char *key, int fallback) { const int value = item.contains(key) ? toml::find(item, key) : fallback; if (value < 0 || value > 31) throw std::runtime_error(fmt::format( "{}: [[widescreen.cull.aspect_cone.sites]] " "{} must be in [0, 31]", config_path.string(), key)); return (uint32_t)value; }; site.object_reg = site_reg("object_reg", object_reg); site.x_reg = site_reg("x_reg", x_reg); site.z_reg = site_reg("z_reg", z_reg); site.y_reg = site_reg("y_reg", y_reg); site.queue_guard = toml::find_or(item, "queue_guard", true); if ((site.address & 3u) != 0) throw std::runtime_error(fmt::format( "{}: aspect-cone address 0x{:08X} is not " "instruction-aligned", config_path.string(), site.address)); if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error(fmt::format( "{}: duplicate aspect-cone address 0x{:08X}", config_path.string(), site.address)); ws_aspect_cone.sites.push_back(site); } } if (cull.contains("guard_pixels")) { ws_cull_guard_pixels = toml::find(cull, "guard_pixels"); if (ws_cull_guard_pixels < 0 || ws_cull_guard_pixels > 256) throw std::runtime_error(fmt::format( "{}: [widescreen.cull] guard_pixels must be in [0, 256]", config_path.string())); } if (cull.contains("activation_guard_pixels")) { ws_cull_activation_guard_pixels = toml::find(cull, "activation_guard_pixels"); if (ws_cull_activation_guard_pixels < 0 || ws_cull_activation_guard_pixels > 256) throw std::runtime_error(fmt::format( "{}: [widescreen.cull] activation_guard_pixels must " "be in [0, 256]", config_path.string())); } if (cull.contains("screen_w_imms")) { ws_cull_w_imms.clear(); load_sites("screen_w_imms", ws_cull_w_imms); } if (cull.contains("screen_h_imms")) { ws_cull_h_imms.clear(); load_sites("screen_h_imms", ws_cull_h_imms); } if (cull.contains("auto_screen_x")) ws_auto_screen_x_cull = toml::find(cull, "auto_screen_x"); if (cull.contains("auto_backdrop")) ws_auto_backdrop_preload = toml::find(cull, "auto_backdrop"); } } // Optional [widescreen.bg2d] block — pure-2D background tile-loop widen. uint32_t ws_bg2d_count_site = 0, ws_bg2d_startcol_site = 0, ws_bg2d_startx_site = 0; uint32_t ws_bg2d_stream_left_site = 0, ws_bg2d_stream_right_site = 0; uint32_t ws_bg2d_bufbase_site = 0, ws_bg2d_cap_site = 0; uint32_t ws_bg2d_layer_base = 0x800971F8u, ws_bg2d_ring_base = 0x800A21B8u; uint32_t ws_bg2d_map_size_addr = 0x800CD338u, ws_bg2d_layer_stride_addr = 0x8008EC10u; uint32_t ws_bg2d_ring_cols = 64, ws_bg2d_layer_count = 3; uint32_t ws_bg2d_layer_struct_stride = 0x54; uint32_t ws_bg2d_init_func = 0; uint32_t ws_bg2d_packet_cap = 1000; if (cfg.contains("widescreen")) { const toml::value& ws = toml::find(cfg, "widescreen"); if (ws.contains("bg2d")) { const toml::value& bg = toml::find(ws, "bg2d"); auto load1 = [&](const char* key) -> uint32_t { return bg.contains(key) ? parse_hex(toml::find(bg, key), fmt::format("widescreen.bg2d.{}", key)) : 0u; }; ws_bg2d_count_site = load1("count_site"); ws_bg2d_startcol_site = load1("startcol_site"); ws_bg2d_startx_site = load1("startx_site"); ws_bg2d_stream_left_site = load1("stream_left_site"); ws_bg2d_stream_right_site = load1("stream_right_site"); ws_bg2d_bufbase_site = load1("bufbase_site"); ws_bg2d_cap_site = load1("cap_site"); if (bg.contains("layer_base")) ws_bg2d_layer_base = load1("layer_base"); if (bg.contains("ring_base")) ws_bg2d_ring_base = load1("ring_base"); if (bg.contains("map_size_addr")) ws_bg2d_map_size_addr = load1("map_size_addr"); if (bg.contains("layer_stride_addr")) ws_bg2d_layer_stride_addr = load1("layer_stride_addr"); auto load_positive = [&](const char* key, uint32_t current) -> uint32_t { if (!bg.contains(key)) return current; const int64_t value = toml::find(bg, key); if (value <= 0 || value > UINT32_MAX) throw std::runtime_error(fmt::format( "widescreen.bg2d.{} must be a positive 32-bit integer", key)); return static_cast(value); }; ws_bg2d_ring_cols = load_positive("ring_cols", ws_bg2d_ring_cols); ws_bg2d_layer_count = load_positive("layer_count", ws_bg2d_layer_count); ws_bg2d_layer_struct_stride = load_positive( "layer_struct_stride", ws_bg2d_layer_struct_stride); ws_bg2d_packet_cap = load_positive("packet_cap", ws_bg2d_packet_cap); if ((ws_bg2d_ring_cols & (ws_bg2d_ring_cols - 1u)) != 0u) throw std::runtime_error( "widescreen.bg2d.ring_cols must be a power of two"); ws_bg2d_init_func = load1("init_func"); } } // Optional [widescreen.backdrop] block — parallax 2D backdrop screenX squash // (x_sites) + far-backdrop GTE un-squash (unsquash_funcs). std::vector ws_backdrop_x_sites; std::vector ws_backdrop_unsquash_funcs; std::vector ws_dome_call_sites; if (cfg.contains("widescreen")) { const toml::value& ws = toml::find(cfg, "widescreen"); if (ws.contains("backdrop")) { const toml::value& bd = toml::find(ws, "backdrop"); if (bd.contains("x_sites")) for (const auto& a : toml::find>(bd, "x_sites")) ws_backdrop_x_sites.push_back( parse_hex(a, "widescreen.backdrop.x_sites")); if (bd.contains("unsquash_funcs")) for (const auto& a : toml::find>(bd, "unsquash_funcs")) ws_backdrop_unsquash_funcs.push_back( parse_hex(a, "widescreen.backdrop.unsquash_funcs")); } } // Optional [widescreen.dome] block: exact projection calls belonging to // finite curved background meshes that retain authored 4:3 coverage. if (cfg.contains("widescreen")) { const toml::value& ws = toml::find(cfg, "widescreen"); if (ws.contains("dome")) { const toml::value& dome = toml::find(ws, "dome"); if (dome.contains("call_sites")) for (const auto& a : toml::find>(dome, "call_sites")) ws_dome_call_sites.push_back( parse_hex(a, "widescreen.dome.call_sites")); } } GameConfig loaded{ /*config_path*/ config_path, /*project_root*/ root, /*name*/ name, /*id*/ id, /*region*/ region, /*players*/ players, /*exe_path*/ exe_path, /*load_address*/ load_address, /*entry_pc*/ entry_pc, /*text_size*/ text_size, /*stack_base*/ stack_base, /*discs*/ discs, /*disc_serials*/ disc_serials, /*has_disc_crc*/ has_disc_crc, /*disc_crc*/ disc_crc, /*disc_sha1*/ disc_sha1, /*netplay_require_cue*/ netplay_require_cue, /*netplay_required_tracks*/ netplay_required_tracks, /*has_netplay_required_leadout*/ has_netplay_required_leadout, /*netplay_required_leadout_lba*/ netplay_required_leadout_lba, /*netplay_required_disc_fp*/ netplay_required_disc_fp, /*netplay_required_disc_fps*/ netplay_required_disc_fps, /*netplay_local_viewport*/ netplay_local_viewport, /*netplay_local_viewport_aspect*/ netplay_local_viewport_aspect, /*netplay_local_viewport_renderer*/ netplay_local_viewport_renderer, /*netplay_local_viewport_width_sites*/ netplay_local_viewport_width_sites, /*netplay_local_viewport_state_addr*/ netplay_local_viewport_state_addr, /*netplay_local_viewport_state_values*/ netplay_local_viewport_state_values, /*netplay_content_negotiation*/ netplay_content_negotiation, /*seeds_path*/ seeds_path, /*bios_thunks_path*/ bios_thunks_path, /*bios_config_path*/ bios_config_path, /*out_dir*/ out_dir, /*strict*/ strict, /*discovery*/ discovery, /*out_stem*/ out_stem, /*recompiler_patches*/ recompiler_patches, /*runtime*/ parse_runtime_block(cfg, root), /*ws_sprite_tag_funcs*/ ws_sprite_tag_funcs, /*ws_sprite_anchor_addr*/ ws_sprite_anchor_addr, /*ws_hud_sprt_squash*/ ws_hud_sprt_squash, /*ws_auto_ui_squash*/ ws_auto_ui_squash, /*ws_auto_ui_in_place*/ ws_auto_ui_in_place, /*ws_auto_ui_proportional*/ ws_auto_ui_proportional, /*data_shard_funcs*/ data_shard_funcs, /*mod_function_entry_funcs*/ mod_function_entry_funcs, /*mod_instruction_sites*/ mod_instruction_sites, /*hot_funcs*/ hot_funcs, /*load_charge_batch*/ load_charge_batch, /*load_charge_batch_funcs*/ load_charge_batch_funcs, /*vsync_query_func*/ vsync_query_func, /*vsync_counter_addr*/ vsync_counter_addr, /*vsync_gpustat_ptr_addr*/ vsync_gpustat_ptr_addr, /*vsync_timer1_ptr_addr*/ vsync_timer1_ptr_addr, /*vsync_timer1_cache_addr*/ vsync_timer1_cache_addr, /*vsync_event_horizon_sites*/ vsync_event_horizon_sites, /*vsync_event_horizon_extra_sites*/ vsync_event_horizon_extra_sites, /*vsync_event_horizon_any*/ vsync_event_horizon_any, /*ws_cull_bias_sites*/ ws_cull_bias_sites, /*ws_cull_bias_lower_sites*/ ws_cull_bias_lower_sites, /*ws_cull_range_sites*/ ws_cull_range_sites, /*ws_cull_a1_sites*/ ws_cull_a1_sites, /*ws_cull_screen_x_sites*/ ws_cull_screen_x_sites, /*ws_cull_slti_sites*/ ws_cull_slti_sites, /*ws_cull_slti_lower_sites*/ ws_cull_slti_lower_sites, /*ws_cull_bltz_sites*/ ws_cull_bltz_sites, /*ws_cull_negsub_sites*/ ws_cull_negsub_sites, /*ws_cull_vxrange_sites*/ ws_cull_vxrange_sites, /*ws_cull_depth_sites*/ ws_cull_depth_sites, /*ws_cull_plane_nx_sites*/ ws_cull_plane_nx_sites, /*ws_cull_xclip_load_sites*/ ws_cull_xclip_load_sites, /*ws_cull_nclip_keep_sites*/ ws_cull_nclip_keep_sites, /*ws_cull_nclip_exact_sites*/ ws_cull_nclip_exact_sites, /*ws_cull_branch_keep_sites*/ ws_cull_branch_keep_sites, /*ws_cull_keep_sites*/ ws_cull_keep_sites, /*ws_cull_sxy_sites*/ ws_cull_sxy_sites, /*ws_cull_scale_sites*/ ws_cull_scale_sites, /*ws_cull_angle_sites*/ ws_cull_angle_sites, /*ws_aspect_cone*/ ws_aspect_cone, /*ws_cull_guard_pixels*/ ws_cull_guard_pixels, /*ws_cull_activation_guard_pixels*/ ws_cull_activation_guard_pixels, /*ws_cull_w_imms*/ ws_cull_w_imms, /*ws_cull_h_imms*/ ws_cull_h_imms, /*ws_backdrop_x_sites*/ ws_backdrop_x_sites, /*ws_backdrop_unsquash_funcs*/ ws_backdrop_unsquash_funcs, /*ws_dome_call_sites*/ ws_dome_call_sites, /*ws_auto_screen_x_cull*/ ws_auto_screen_x_cull, /*ws_auto_backdrop_preload*/ ws_auto_backdrop_preload, /*ws_full_2d*/ ws_full_2d, /*ws_gte_game_mode*/ ws_gte_game_mode, /*ws_precise_nclip*/ ws_precise_nclip, /*ws_gameplay_state_addr*/ ws_gameplay_state_addr, /*ws_gameplay_state_values*/ ws_gameplay_state_values, /*ws_native_wide*/ ws_native_wide, /*ws_nw_hud_corners*/ ws_nw_hud_corners, /*ws_nw_left_hud_packet_lo*/ ws_nw_left_hud_packet_lo, /*ws_nw_left_hud_packet_hi*/ ws_nw_left_hud_packet_hi, /*ws_nw_backdrop*/ ws_nw_backdrop, /*ws_clear_reveal*/ ws_clear_reveal, /*ws_nw_flat_backdrop*/ ws_nw_flat_backdrop, /*ws_nw_phase_backdrop*/ ws_nw_phase_backdrop, /*ws_nw_textured_edges*/ ws_nw_textured_edges, /*ws_nw_textured_edge_scale*/ ws_nw_textured_edge_scale, /*ws_nw_full_mirror*/ ws_nw_full_mirror, /*ws_signed_x_bound_sites*/ ws_signed_x_bound_sites, /*ws_offered*/ ws_offered, /*vulkan_offered*/ vulkan_offered, /*ws_adaptive_view*/ ws_adaptive_view, /*ws_ultrawide_offered*/ ws_ultrawide_offered, /*ws_bg2d_count_site*/ ws_bg2d_count_site, /*ws_bg2d_startcol_site*/ ws_bg2d_startcol_site, /*ws_bg2d_startx_site*/ ws_bg2d_startx_site, /*ws_bg2d_stream_left_site*/ ws_bg2d_stream_left_site, /*ws_bg2d_stream_right_site*/ ws_bg2d_stream_right_site, /*ws_bg2d_bufbase_site*/ ws_bg2d_bufbase_site, /*ws_bg2d_cap_site*/ ws_bg2d_cap_site, /*ws_bg2d_layer_base*/ ws_bg2d_layer_base, /*ws_bg2d_ring_base*/ ws_bg2d_ring_base, /*ws_bg2d_map_size_addr*/ ws_bg2d_map_size_addr, /*ws_bg2d_layer_stride_addr*/ ws_bg2d_layer_stride_addr, /*ws_bg2d_ring_cols*/ ws_bg2d_ring_cols, /*ws_bg2d_layer_count*/ ws_bg2d_layer_count, /*ws_bg2d_layer_struct_stride*/ ws_bg2d_layer_struct_stride, /*ws_bg2d_init_func*/ ws_bg2d_init_func, /*ws_bg2d_packet_cap*/ ws_bg2d_packet_cap, }; loaded.ws_cull_bgez_sites = std::move(ws_cull_bgez_sites); loaded.ws_cull_clip_edge_x_load_sites = std::move(ws_cull_clip_edge_x_load_sites); loaded.ws_cull_clip_edge_width = ws_cull_clip_edge_width; loaded.ws_cull_masked_reject_sites = std::move(ws_cull_masked_reject_sites); loaded.ws_cull_packed_x_sites = std::move(ws_cull_packed_x_sites); loaded.draw_distance_clamp_sites = parse_draw_distance_clamps(cfg, config_path); loaded.quality_presets = parse_quality_presets(cfg, root, config_path); loaded.mod_immediate_sites = parse_mod_immediate_sites(cfg, config_path); return loaded; } // ---- GameOptions (game_options.toml) — the game's own native settings ---- GameOptions load_game_options(const fs::path& path) { GameOptions go; std::error_code ec; if (path.empty() || !fs::exists(path, ec)) return go; const toml::value doc = toml::parse(path.string()); if (!doc.contains("option")) return go; const auto& arr = toml::find(doc, "option"); for (const auto& item : arr) { GameOption o; o.name = toml::find(item, "name"); o.addr = parse_hex(toml::find(item, "addr"), fmt::format("game_options [[option]] '{}' addr", o.name)); o.size = static_cast(toml::find(item, "size")); if (o.size != 1 && o.size != 2) throw std::runtime_error(fmt::format( "game_options [[option]] '{}' size must be 1 or 2: {}", o.name, o.size)); if (item.contains("init_store_pc")) o.init_store_pc = parse_hex(toml::find(item, "init_store_pc"), fmt::format("game_options '{}' init_store_pc", o.name)); // Optional restore-time value range. `max` (with optional `min`, default 0) // marks the field validated; a saved value outside [min,max] is dropped. if (item.contains("max")) { o.vmax = toml::find(item, "max"); o.vmin = item.contains("min") ? toml::find(item, "min") : 0; if (o.vmax < o.vmin) throw std::runtime_error(fmt::format( "game_options '{}' max ({}) < min ({})", o.name, o.vmax, o.vmin)); o.has_range = true; } go.options.push_back(o); } return go; } // ---- UserSettings (settings.toml) — launcher-written override layer ---- static bool valid_user_audio_freq(int n) { return n == 32040 || n == 32000 || n == 44100 || n == 48000; } UserSettings load_user_settings(const fs::path& path) { UserSettings s; std::error_code ec; if (path.empty() || !fs::exists(path, ec)) return s; toml::value doc; try { doc = toml::parse(path.string()); } catch (const std::exception&) { // Malformed file: fall back to all-defaults rather than refuse to boot, // but tell the caller so the user hears about it (and so the file can // be preserved before any launcher save overwrites it with defaults). s.parse_error = true; return s; } // Each field guarded independently so one bad value can't blank the rest. auto try_get = [](auto&& fn) { try { fn(); } catch (const std::exception&) {} }; if (doc.contains("video")) { const toml::value& v = toml::find(doc, "video"); if (v.contains("renderer")) try_get([&]{ const auto m = toml::find(v, "renderer"); if (m == "software") { s.renderer = 0; s.has_renderer = true; } else if (m == "opengl") { s.renderer = 1; s.has_renderer = true; } else if (m == "vulkan") { s.renderer = 2; s.has_renderer = true; } }); if (v.contains("supersampling")) try_get([&]{ const auto n = toml::find(v, "supersampling"); if (n >= 1 && n <= 32) { s.supersampling = (int)n; s.has_supersampling = true; } }); if (v.contains("supersample")) try_get([&]{ const toml::value& ss = toml::find(v, "supersample"); const double f = ss.is_integer() ? static_cast(ss.as_integer()) : toml::get(ss); if (f >= 1.0 && f <= 4.0) { s.supersample_milli = static_cast(f * 1000.0 + 0.5); s.has_supersample = true; } }); if (v.contains("internal_resolution")) try_get([&]{ const toml::value& ir = toml::find(v, "internal_resolution"); int value = 0; if (ir.is_string()) { if (psx_ir_parse(ir.as_string().str.c_str(), &value)) { s.internal_resolution = value; s.has_internal_resolution = true; } } else if (ir.is_integer()) { const auto n = ir.as_integer(); if (n >= PSX_IR_MIN_LINES && n <= PSX_IR_MAX_LINES) { s.internal_resolution = (int)n; s.has_internal_resolution = true; } } }); if (v.contains("dynamic_resolution")) try_get([&]{ s.dynamic_resolution = toml::find(v, "dynamic_resolution"); s.has_dynamic_resolution = true; }); if (v.contains("dynamic_resolution_min")) try_get([&]{ const toml::value& ir = toml::find(v, "dynamic_resolution_min"); int value = 0; if (ir.is_string()) { if (psx_ir_parse(ir.as_string().str.c_str(), &value)) { s.dynamic_resolution_min = value; s.has_dynamic_resolution_min = true; } } else if (ir.is_integer()) { const auto n = ir.as_integer(); if (n >= PSX_IR_MIN_LINES && n <= PSX_IR_MAX_LINES) { s.dynamic_resolution_min = (int)n; s.has_dynamic_resolution_min = true; } } }); if (v.contains("window_width")) try_get([&]{ const auto n = toml::find(v, "window_width"); if (n >= 640 && n <= 7680) { s.window_width = (int)n; s.has_window_width = true; } }); if (v.contains("antialiasing")) try_get([&]{ s.antialiasing = toml::find(v, "antialiasing"); s.has_antialiasing = true; }); if (v.contains("texture_filtering")) try_get([&]{ const auto m = toml::find(v, "texture_filtering"); if (m == "nearest") { s.texture_filter = 0; s.has_texture_filter = true; } else if (m == "bilinear") { s.texture_filter = 1; s.has_texture_filter = true; } else if (m == "xbr") { s.texture_filter = 3; s.has_texture_filter = true; } }); if (v.contains("fmv_filter")) try_get([&]{ const auto m = toml::find(v, "fmv_filter"); if (video_fmv_filter_parse(m, &s.fmv_filter)) s.has_fmv_filter = true; }); if (v.contains("geometry_correction")) try_get([&]{ s.geometry_correction = toml::find(v, "geometry_correction"); s.has_geometry_correction = true; }); if (v.contains("perspective_texturing")) try_get([&]{ s.perspective_texturing = toml::find(v, "perspective_texturing"); s.has_perspective_texturing = true; }); if (v.contains("crt_filter")) try_get([&]{ const auto m = toml::find(v, "crt_filter"); if (m == "raw") { s.screen_kind = 0; s.has_screen_kind = true; } else if (m == "crt") { s.screen_kind = 1; s.has_screen_kind = true; } else if (m == "composite") { s.screen_kind = 2; s.has_screen_kind = true; } else if (m == "trinitron") { s.screen_kind = 3; s.has_screen_kind = true; } }); if (v.contains("scanlines")) try_get([&]{ s.scanlines = toml::find(v, "scanlines"); s.has_scanlines = true; }); if (v.contains("scanline_strength")) try_get([&]{ double d = toml_number(v, "scanline_strength"); if (d < 0.0) d = 0.0; if (d > 1.0) d = 1.0; s.scanline_strength = d; s.has_scanline_strength = true; }); if (v.contains("fov_scale")) try_get([&]{ const auto& n = toml::find(v, "fov_scale"); if (n.is_floating()) s.fov_scale = n.as_floating(); else if (n.is_integer()) s.fov_scale = static_cast(n.as_integer()); else return; s.has_fov_scale = true; }); if (v.contains("auto_skip_fmv")) try_get([&]{ s.auto_skip_fmv = toml::find(v, "auto_skip_fmv"); s.has_auto_skip_fmv = true; }); // Deprecated and ignored: read only so the runtime can report that a // stale value was found (and so the next save drops it). Never applied // — see UserSettings::turbo_loads in config_loader.h. if (v.contains("turbo_loads")) try_get([&]{ s.turbo_loads = toml::find(v, "turbo_loads"); s.has_turbo_loads = true; }); if (v.contains("fast_boot")) try_get([&]{ s.fast_boot = toml::find(v, "fast_boot"); s.has_fast_boot = true; }); if (v.contains("bios_hle")) try_get([&]{ s.bios_hle = toml::find(v, "bios_hle"); s.has_bios_hle = true; }); if (v.contains("fullscreen")) try_get([&]{ // Tri-state (0 off / 1 borderless / 2 exclusive). Back-compat: a // settings.toml written before the tri-state migration stores this // as a bool (true meant borderless desktop fullscreen). try { s.fullscreen = toml::find(v, "fullscreen"); } catch (const std::exception&) { s.fullscreen = toml::find(v, "fullscreen") ? 1 : 0; } if (s.fullscreen < 0 || s.fullscreen > 2) s.fullscreen = 0; s.has_fullscreen = true; }); if (v.contains("low_latency_input")) try_get([&]{ s.low_latency_input = toml::find(v, "low_latency_input"); s.has_low_latency_input = true; }); if (v.contains("render_thread")) try_get([&]{ s.render_thread = toml::find(v, "render_thread"); s.has_render_thread = true; }); if (v.contains("present_thread")) try_get([&]{ s.present_thread = toml::find(v, "present_thread"); s.has_present_thread = true; }); if (v.contains("frame_generation")) try_get([&]{ s.frame_generation = toml::find(v, "frame_generation"); s.has_frame_generation = true; }); if (v.contains("vsync")) try_get([&]{ const auto m = toml::find(v, "vsync"); if (m == "on" || m == "vsync") { s.vsync = 1; s.has_vsync = true; } else if (m == "off" || m == "immediate"){ s.vsync = 0; s.has_vsync = true; } else if (m == "adaptive") { s.vsync = -1; s.has_vsync = true; } else if (m == "vrr") { s.vsync = 2; s.has_vsync = true; } }); if (v.contains("frame_interpolation")) try_get([&]{ s.frame_interpolation = toml::find(v, "frame_interpolation"); s.has_frame_interpolation = true; }); if (v.contains("frame_interpolation_fps")) try_get([&]{ s.frame_interpolation_fps = toml::find(v, "frame_interpolation_fps"); if (s.frame_interpolation_fps == 0 || s.frame_interpolation_fps >= 90) s.has_frame_interpolation_fps = true; }); // Graphics preset state (docs/QUALITY_PRESETS.md). Values are checked // by the runtime, which knows the title's presets. if (v.contains("quality_preset")) try_get([&]{ s.quality_preset = toml::find(v, "quality_preset"); s.has_quality_preset = !s.quality_preset.empty(); }); if (v.contains("quality_base")) try_get([&]{ s.quality_base = toml::find(v, "quality_base"); s.has_quality_base = !s.quality_base.empty(); }); if (v.contains("quality_hardware")) try_get([&]{ s.quality_hardware = toml::find(v, "quality_hardware"); s.has_quality_hardware = !s.quality_hardware.empty(); }); if (v.contains("quality_detected")) try_get([&]{ s.quality_detected = toml::find(v, "quality_detected"); s.has_quality_detected = !s.quality_detected.empty(); }); if (v.contains("aspect_ratio")) try_get([&]{ const auto m = toml::find(v, "aspect_ratio"); int n = 0, d = 0; if (parse_aspect_ratio(m, &n, &d)) { s.aspect_num = n; s.aspect_den = d; s.has_aspect_ratio = true; } }); if (v.contains("adaptive_view")) try_get([&]{ s.adaptive_view = toml::find(v, "adaptive_view"); s.has_adaptive_view = true; }); if (v.contains("rewind")) try_get([&]{ s.rewind = toml::find(v, "rewind"); s.has_rewind = true; }); if (v.contains("rewind_depth")) try_get([&]{ int d = toml::find(v, "rewind_depth"); static const int opts[4] = {50, 100, 150, 200}; int best = opts[0]; int best_d = d > best ? d - best : best - d; for (int i = 1; i < 4; ++i) { int dd = d > opts[i] ? d - opts[i] : opts[i] - d; if (dd < best_d) { best_d = dd; best = opts[i]; } } s.rewind_depth = best; s.has_rewind_depth = true; }); if (v.contains("rewind_interval")) try_get([&]{ int d = toml::find(v, "rewind_interval"); static const int opts[5] = {1, 4, 8, 12, 15}; int best = opts[0]; int best_d = d > best ? d - best : best - d; for (int i = 1; i < 5; ++i) { int dd = d > opts[i] ? d - opts[i] : opts[i] - d; if (dd < best_d) { best_d = dd; best = opts[i]; } } s.rewind_interval = best; s.has_rewind_interval = true; }); } if (doc.contains("audio")) { const toml::value& a = toml::find(doc, "audio"); if (a.contains("frequency")) try_get([&]{ const auto n = toml::find(a, "frequency"); if (valid_user_audio_freq((int)n)) { s.audio_freq = (int)n; s.has_audio_freq = true; } }); if (a.contains("spu_hq")) try_get([&]{ s.spu_hq = toml::find(a, "spu_hq"); s.has_spu_hq = true; }); } if (doc.contains("hotkeys")) { const toml::value& h = toml::find(doc, "hotkeys"); if (h.contains("rewind_pad")) try_get([&]{ const auto n = toml::find(h, "rewind_pad"); if (pad_bind_value_ok(n)) { s.hotkey_pad_rewind = (int)n; s.has_hotkey_pad_rewind = true; } }); if (h.contains("save_state_menu_pad")) try_get([&]{ const auto n = toml::find(h, "save_state_menu_pad"); if (pad_bind_value_ok(n)) { s.hotkey_pad_save_state_menu = (int)n; s.has_hotkey_pad_save_state_menu = true; } }); if (h.contains("fast_forward_pad")) try_get([&]{ const auto n = toml::find(h, "fast_forward_pad"); if (pad_bind_value_ok(n)) { s.hotkey_pad_fast_forward = (int)n; s.has_hotkey_pad_fast_forward = true; } }); if (h.contains("fast_forward_toggle_pad")) try_get([&]{ const auto n = toml::find(h, "fast_forward_toggle_pad"); if (pad_bind_value_ok(n)) { s.hotkey_pad_fast_forward_toggle = (int)n; s.has_hotkey_pad_fast_forward_toggle = true; } }); } if (doc.contains("launcher")) { const toml::value& l = toml::find(doc, "launcher"); if (l.contains("skip_launcher")) try_get([&]{ s.skip_launcher = toml::find(l, "skip_launcher"); s.has_skip_launcher = true; }); } if (doc.contains("netplay")) { const toml::value& n = toml::find(doc, "netplay"); if (n.contains("player_name")) try_get([&]{ const auto v = toml::find(n, "player_name"); if (!v.empty()) { s.netplay_player_name = v; s.has_netplay_player_name = true; } }); if (n.contains("lobby_url")) try_get([&]{ const auto v = toml::find(n, "lobby_url"); if (!v.empty()) { s.netplay_lobby_url = v; s.has_netplay_lobby_url = true; } }); } if (doc.contains("bios")) { const toml::value& b = toml::find(doc, "bios"); if (b.contains("path")) try_get([&]{ const auto p = toml::find(b, "path"); if (!p.empty()) { s.bios_path = fs::path(p); s.has_bios_path = true; } }); } if (doc.contains("disc")) { const toml::value& d = toml::find(doc, "disc"); if (d.contains("path")) try_get([&]{ const auto p = toml::find(d, "path"); if (!p.empty()) { s.disc_path = fs::path(p); s.has_disc_path = true; } }); /* Multi-disc selection, 1-based. Anything below 1 is a malformed * hand-edit, not a request to boot disc 0 -- clamp up and keep going * rather than silently mounting nothing. The upper bound is the disc * roster's size, which only the runtime knows, so it clamps there. */ if (d.contains("selected")) try_get([&]{ const auto n = toml::find(d, "selected"); s.disc_index = n < 1 ? 1 : n; s.has_disc_index = true; }); } if (doc.contains("memcard")) { const toml::value& m = toml::find(doc, "memcard"); if (m.contains("dir")) try_get([&]{ const auto p = toml::find(m, "dir"); if (!p.empty()) { s.memcard_dir = fs::path(p); s.has_memcard_dir = true; } }); if (m.contains("card1")) try_get([&]{ const auto p = toml::find(m, "card1"); if (!p.empty()) { s.memcard1_path = fs::path(p); s.has_memcard1_path = true; } }); if (m.contains("card2")) try_get([&]{ const auto p = toml::find(m, "card2"); if (!p.empty()) { s.memcard2_path = fs::path(p); s.has_memcard2_path = true; } }); if (m.contains("enable1")) try_get([&]{ s.memcard1_enabled = toml::find(m, "enable1"); s.has_memcard1_enabled = true; }); if (m.contains("enable2")) try_get([&]{ s.memcard2_enabled = toml::find(m, "enable2"); s.has_memcard2_enabled = true; }); } if (doc.contains("localization")) { const toml::value& lc = toml::find(doc, "localization"); if (lc.contains("language")) try_get([&]{ const auto v = toml::find(lc, "language"); if (!v.empty()) { s.language = v; s.has_language = true; } }); } if (doc.contains("parappa_timing")) { const toml::value& timing = toml::find(doc, "parappa_timing"); if (timing.contains("mode")) try_get([&]{ auto mode = toml::find(timing, "mode"); for (char& c : mode) c = (char)std::tolower((unsigned char)c); if (mode == "stock" || mode == "off" || mode == "medium" || mode == "permissive" || mode == "easy" || mode == "custom") { s.parappa_timing_mode = mode; s.has_parappa_timing_mode = true; } }); if (timing.contains("extra_early")) try_get([&]{ const auto n = toml::find(timing, "extra_early"); if (n >= 0 && n <= 60) { s.parappa_timing_extra_early = (int)n; s.has_parappa_timing_extra_early = true; } }); if (timing.contains("extra_late")) try_get([&]{ const auto n = toml::find(timing, "extra_late"); if (n >= 0 && n <= 60) { s.parappa_timing_extra_late = (int)n; s.has_parappa_timing_extra_late = true; } }); } if (doc.contains("controller")) { const toml::value& ct = toml::find(doc, "controller"); static const char* kDevKeys[] = { "p1_device", "p2_device", "p3_device", "p4_device", "p5_device"}; static const char* kModeKeys[] = { "p1_mode", "p2_mode", "p3_mode", "p4_mode", "p5_mode"}; static const char* kDzKeys[] = { "p1_deadzone", "p2_deadzone", "p3_deadzone", "p4_deadzone", "p5_deadzone"}; static const char* kAnalogKeys[] = { "p1_analog", "p2_analog", "p3_analog", "p4_analog", "p5_analog"}; for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) { if (ct.contains(kDevKeys[i])) try_get([&]{ const auto v = toml::find(ct, kDevKeys[i]); if (!v.empty()) { s.p_device[i] = v; s.has_p_device[i] = true; } }); // Legacy boolean form first (true->analog, false->digital); the // string `*_mode` keys override when present. if (ct.contains(kAnalogKeys[i])) try_get([&]{ s.p_mode[i] = toml::find(ct, kAnalogKeys[i]) ? PAD_MODE_ANALOG : PAD_MODE_DIGITAL; s.has_p_mode[i] = true; }); if (ct.contains(kModeKeys[i])) try_get([&]{ s.p_mode[i] = pad_mode_from_settings_string( toml::find(ct, kModeKeys[i]), PAD_MODE_ANALOG); s.has_p_mode[i] = true; }); if (ct.contains(kDzKeys[i])) try_get([&]{ const auto n = toml::find(ct, kDzKeys[i]); if (n >= 0 && n <= 32767) { s.p_deadzone[i] = (int)n; s.has_p_deadzone[i] = true; } }); } if (ct.contains("multitap")) try_get([&]{ s.multitap_enabled = toml::find(ct, "multitap"); s.has_multitap_enabled = true; }); if (ct.contains("multitap_analog")) try_get([&]{ s.multitap_analog = toml::find(ct, "multitap_analog"); s.has_multitap_analog = true; }); if (ct.contains("jogcon_force_feedback")) try_get([&]{ s.jogcon_force_feedback = toml::find(ct, "jogcon_force_feedback"); s.has_jogcon_force_feedback = true; }); if (ct.contains("deadzone")) try_get([&]{ const auto n = toml::find(ct, "deadzone"); if (n >= 0 && n <= 32767) { s.deadzone = (int)n; s.has_deadzone = true; /* Legacy global: fill any slot that was not given pN_deadzone. */ for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) { if (!s.has_p_deadzone[i]) { s.p_deadzone[i] = s.deadzone; s.has_p_deadzone[i] = true; } } } }); } return s; } fs::path relative_to_folder(const fs::path& p, const fs::path& folder) { if (p.empty() || folder.empty() || !p.is_absolute()) return p; std::error_code ec; const fs::path base = fs::weakly_canonical(folder, ec); if (ec) return p; const fs::path full = fs::weakly_canonical(p, ec); if (ec) return p; const fs::path r = full.lexically_relative(base); // Outside the folder (other drive, or a "../" climb) stays absolute. if (r.empty() || r.is_absolute() || *r.begin() == "..") return p; return r; } bool save_user_settings(const fs::path& path, const UserSettings& s) { std::error_code ec; if (!path.parent_path().empty()) fs::create_directories(path.parent_path(), ec); // best-effort std::ofstream f(path, std::ios::trunc); if (!f.is_open()) return false; // Windows paths use forward slashes so separators need no TOML escaping. auto fwd = [](const fs::path& p) { return host_path_forward_slashes(p); }; // Paths inside the game folder are stored relative to it, so a portable // folder still finds its disc, BIOS and memory cards after it is moved or // copied to another PC. Readers anchor relative paths on the exe // directory, which is where settings.toml lives. auto rel = [&](const fs::path& p) { return fwd(relative_to_folder(p, path.parent_path())); }; f << "# psxrecomp user settings - written by the launcher. Safe to hand-edit.\n"; f << "# Overrides the bundled game.toml; the command line overrides this file.\n\n"; f << "[video]\n"; if (s.has_renderer) f << "renderer = \"" << (s.renderer == 2 ? "vulkan" : s.renderer == 1 ? "opengl" : "software") << "\"\n"; if (s.has_supersampling) f << "supersampling = " << s.supersampling << "\n"; if (s.has_internal_resolution && psx_ir_value_valid(s.internal_resolution)) { const char* id = psx_ir_id_for(s.internal_resolution); if (id) f << "internal_resolution = \"" << id << "\"\n"; else f << "internal_resolution = " << s.internal_resolution << "\n"; } if (s.has_supersample) f << "supersample = " << (s.supersample_milli / 1000.0) << "\n"; if (s.has_dynamic_resolution) f << "dynamic_resolution = " << (s.dynamic_resolution ? "true" : "false") << "\n"; if (s.has_dynamic_resolution_min && psx_ir_value_valid(s.dynamic_resolution_min)) { const char* id = psx_ir_id_for(s.dynamic_resolution_min); if (id) f << "dynamic_resolution_min = \"" << id << "\"\n"; else f << "dynamic_resolution_min = " << s.dynamic_resolution_min << "\n"; } if (s.has_window_width) f << "window_width = " << s.window_width << "\n"; if (s.has_antialiasing) f << "antialiasing = " << (s.antialiasing ? "true" : "false") << "\n"; if (s.has_texture_filter) f << "texture_filtering = \"" << (s.texture_filter == 3 ? "xbr" : s.texture_filter ? "bilinear" : "nearest") << "\"\n"; if (s.has_fmv_filter) f << "fmv_filter = \"" << video_fmv_filter_name(s.fmv_filter) << "\"\n"; if (s.has_geometry_correction) f << "geometry_correction = " << (s.geometry_correction ? "true" : "false") << "\n"; if (s.has_perspective_texturing) f << "perspective_texturing = " << (s.perspective_texturing ? "true" : "false") << "\n"; if (s.has_screen_kind) { const char* k = s.screen_kind == 1 ? "crt" : s.screen_kind == 2 ? "composite" : s.screen_kind == 3 ? "trinitron" : "raw"; f << "crt_filter = \"" << k << "\"\n"; } if (s.has_scanlines) f << "scanlines = " << (s.scanlines ? "true" : "false") << "\n"; if (s.has_scanline_strength) f << "scanline_strength = " << s.scanline_strength << "\n"; if (s.has_fov_scale) f << "fov_scale = " << s.fov_scale << "\n"; if (s.has_auto_skip_fmv) f << "auto_skip_fmv = " << (s.auto_skip_fmv ? "true" : "false") << "\n"; /* turbo_loads is deliberately NOT written back: it is deprecated and no * longer restored, so re-emitting it would preserve a dead row that looks * authoritative. Omitting it lets an existing settings.toml self-clean on * the first save after the update. */ if (s.has_fast_boot) f << "fast_boot = " << (s.fast_boot ? "true" : "false") << "\n"; if (s.has_bios_hle) f << "bios_hle = " << (s.bios_hle ? "true" : "false") << "\n"; if (s.has_fullscreen) f << "fullscreen = " << s.fullscreen << "\n"; if (s.has_low_latency_input) f << "low_latency_input = " << (s.low_latency_input ? "true" : "false") << "\n"; if (s.has_render_thread) f << "render_thread = " << (s.render_thread ? "true" : "false") << "\n"; if (s.has_present_thread) f << "present_thread = " << (s.present_thread ? "true" : "false") << "\n"; if (s.has_frame_generation) f << "frame_generation = " << (s.frame_generation ? "true" : "false") << "\n"; if (s.has_vsync) f << "vsync = \"" << (s.vsync == 0 ? "immediate" : s.vsync < 0 ? "adaptive" : s.vsync == 2 ? "vrr" : "on") << "\"\n"; if (s.has_frame_interpolation) f << "frame_interpolation = " << (s.frame_interpolation ? "true" : "false") << "\n"; if (s.has_frame_interpolation_fps) f << "frame_interpolation_fps = " << s.frame_interpolation_fps << "\n"; if (s.has_quality_preset) f << "quality_preset = \"" << s.quality_preset << "\"\n"; if (s.has_quality_base) f << "quality_base = \"" << s.quality_base << "\"\n"; if (s.has_quality_hardware) f << "quality_hardware = \"" << s.quality_hardware << "\"\n"; if (s.has_quality_detected) f << "quality_detected = \"" << s.quality_detected << "\"\n"; if (s.has_aspect_ratio) f << "aspect_ratio = \"" << s.aspect_num << ":" << s.aspect_den << "\"\n"; if (s.has_adaptive_view) f << "adaptive_view = " << (s.adaptive_view ? "true" : "false") << "\n"; if (s.has_rewind) f << "rewind = " << (s.rewind ? "true" : "false") << "\n"; if (s.has_rewind_depth) f << "rewind_depth = " << s.rewind_depth << "\n"; if (s.has_rewind_interval) f << "rewind_interval = " << s.rewind_interval << "\n"; f << "\n[audio]\n"; if (s.has_audio_freq) f << "frequency = " << s.audio_freq << "\n"; if (s.has_spu_hq) f << "spu_hq = " << (s.spu_hq ? "true" : "false") << "\n"; if (s.has_hotkey_pad_rewind || s.has_hotkey_pad_save_state_menu || s.has_hotkey_pad_fast_forward || s.has_hotkey_pad_fast_forward_toggle) { f << "\n[hotkeys]\n"; if (s.has_hotkey_pad_rewind) f << "rewind_pad = " << s.hotkey_pad_rewind << "\n"; if (s.has_hotkey_pad_save_state_menu) f << "save_state_menu_pad = " << s.hotkey_pad_save_state_menu << "\n"; if (s.has_hotkey_pad_fast_forward) f << "fast_forward_pad = " << s.hotkey_pad_fast_forward << "\n"; if (s.has_hotkey_pad_fast_forward_toggle) f << "fast_forward_toggle_pad = " << s.hotkey_pad_fast_forward_toggle << "\n"; } if (s.has_skip_launcher) f << "\n[launcher]\nskip_launcher = " << (s.skip_launcher ? "true" : "false") << "\n"; if ((s.has_netplay_player_name && !s.netplay_player_name.empty()) || (s.has_netplay_lobby_url && !s.netplay_lobby_url.empty())) { f << "\n[netplay]\n"; if (s.has_netplay_player_name && !s.netplay_player_name.empty()) f << "player_name = \"" << s.netplay_player_name << "\"\n"; if (s.has_netplay_lobby_url && !s.netplay_lobby_url.empty()) f << "lobby_url = \"" << s.netplay_lobby_url << "\"\n"; } if (s.has_bios_path) f << "\n[bios]\npath = \"" << rel(s.bios_path) << "\"\n"; if (s.has_disc_path || s.has_disc_index) { f << "\n[disc]\n"; if (s.has_disc_path) f << "path = \"" << rel(s.disc_path) << "\"\n"; /* Only meaningful for a multi-disc title; harmless (and informative) * for a single-disc one, where it is always 1. */ if (s.has_disc_index) f << "selected = " << s.disc_index << "\n"; } if (s.has_memcard_dir || s.has_memcard1_path || s.has_memcard2_path || s.has_memcard1_enabled || s.has_memcard2_enabled) { f << "\n[memcard]\n"; if (s.has_memcard_dir) f << "dir = \"" << rel(s.memcard_dir) << "\"\n"; if (s.has_memcard1_path) f << "card1 = \"" << rel(s.memcard1_path) << "\"\n"; if (s.has_memcard2_path) f << "card2 = \"" << rel(s.memcard2_path) << "\"\n"; if (s.has_memcard1_enabled) f << "enable1 = " << (s.memcard1_enabled ? "true" : "false") << "\n"; if (s.has_memcard2_enabled) f << "enable2 = " << (s.memcard2_enabled ? "true" : "false") << "\n"; } { bool any_ctrl = s.has_deadzone || s.has_multitap_enabled || s.has_multitap_analog || s.has_jogcon_force_feedback; for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) { if (s.has_p_device[i] || s.has_p_mode[i] || s.has_p_deadzone[i]) any_ctrl = true; } if (any_ctrl) { static const char* kDevKeys[] = { "p1_device", "p2_device", "p3_device", "p4_device", "p5_device"}; static const char* kModeKeys[] = { "p1_mode", "p2_mode", "p3_mode", "p4_mode", "p5_mode"}; static const char* kDzKeys[] = { "p1_deadzone", "p2_deadzone", "p3_deadzone", "p4_deadzone", "p5_deadzone"}; f << "\n[controller]\n"; for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) { if (s.has_p_device[i]) f << kDevKeys[i] << " = \"" << s.p_device[i] << "\"\n"; if (s.has_p_mode[i]) f << kModeKeys[i] << " = \"" << pad_mode_to_string(s.p_mode[i]) << "\"\n"; if (s.has_p_deadzone[i]) f << kDzKeys[i] << " = " << s.p_deadzone[i] << "\n"; } if (s.has_multitap_enabled) f << "multitap = " << (s.multitap_enabled ? "true" : "false") << "\n"; if (s.has_multitap_analog) f << "multitap_analog = " << (s.multitap_analog ? "true" : "false") << "\n"; if (s.has_jogcon_force_feedback) f << "jogcon_force_feedback = " << (s.jogcon_force_feedback ? "true" : "false") << "\n"; /* Keep a global deadzone= for older readers (mirrors P1). */ if (s.has_deadzone || s.has_p_deadzone[0]) f << "deadzone = " << (s.has_p_deadzone[0] ? s.p_deadzone[0] : s.deadzone) << "\n"; } } if (s.has_language) { f << "\n[localization]\n"; f << "language = \"" << s.language << "\"\n"; } if (s.has_parappa_timing_mode || s.has_parappa_timing_extra_early || s.has_parappa_timing_extra_late) { f << "\n[parappa_timing]\n"; if (s.has_parappa_timing_mode) f << "mode = \"" << s.parappa_timing_mode << "\"\n"; if (s.has_parappa_timing_extra_early) f << "extra_early = " << s.parappa_timing_extra_early << "\n"; if (s.has_parappa_timing_extra_late) f << "extra_late = " << s.parappa_timing_extra_late << "\n"; } return f.good(); } bool upsert_game_toml_controller_bool(const std::filesystem::path& path, const char* key, bool value) { if (!key || !key[0]) return false; std::string text; { std::ifstream in(path); if (in) { std::ostringstream ss; ss << in.rdbuf(); text = ss.str(); } } const std::string val = value ? "true" : "false"; const std::string assign = std::string(key) + " = " + val; auto is_section = [](const std::string& line) { size_t i = 0; while (i < line.size() && (line[i] == ' ' || line[i] == '\t')) ++i; return i < line.size() && line[i] == '['; }; auto section_name = [](const std::string& line) -> std::string { size_t a = line.find('['); size_t b = line.find(']'); if (a == std::string::npos || b == std::string::npos || b <= a + 1) return {}; return line.substr(a + 1, b - a - 1); }; auto key_prefix = [&](const std::string& line) { size_t i = 0; while (i < line.size() && (line[i] == ' ' || line[i] == '\t')) ++i; if (i < line.size() && line[i] == '#') return false; const std::string rest = line.substr(i); return rest.rfind(std::string(key) + " =", 0) == 0 || rest.rfind(std::string(key) + "=", 0) == 0; }; std::istringstream ls(text); std::vector lines; std::string line; while (std::getline(ls, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); lines.push_back(line); } int ctrl_start = -1, ctrl_end = -1; for (int i = 0; i < (int)lines.size(); ++i) { if (is_section(lines[i]) && section_name(lines[i]) == "controller") { ctrl_start = i; ctrl_end = (int)lines.size(); for (int j = i + 1; j < (int)lines.size(); ++j) { if (is_section(lines[j])) { ctrl_end = j; break; } } break; } } if (ctrl_start < 0) { if (!text.empty() && text.back() != '\n') text.push_back('\n'); text += "\n[controller]\n"; text += assign; text += "\n"; } else { int replace_at = -1; for (int i = ctrl_start + 1; i < ctrl_end; ++i) { if (key_prefix(lines[i])) { replace_at = i; break; } } if (replace_at >= 0) { lines[replace_at] = assign; } else { int insert_at = ctrl_end; while (insert_at > ctrl_start + 1 && lines[insert_at - 1].find_first_not_of(" \t") == std::string::npos) --insert_at; lines.insert(lines.begin() + insert_at, assign); } std::ostringstream out; for (size_t i = 0; i < lines.size(); ++i) { out << lines[i]; if (i + 1 < lines.size() || (!text.empty() && text.back() == '\n')) out << '\n'; } text = out.str(); } std::ofstream f(path, std::ios::binary | std::ios::trunc); if (!f) return false; f << text; return f.good(); } } // namespace PSXRecompV4