mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
3790 lines
181 KiB
C++
3790 lines
181 KiB
C++
// config_loader.cpp — see config_loader.h for the contract.
|
|
|
|
#include "config_loader.h"
|
|
|
|
#include <unordered_set>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <set>
|
|
#include <cstdint>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<uint32_t> 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_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<WidescreenSignedBoundSite> 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<WidescreenCullKeepSite> 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);
|
|
}
|
|
|
|
std::vector<WidescreenCullScaleSite> 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<WidescreenAngleSite> 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<WidescreenAspectConeSite> 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<RecompilerPatch> 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<uint32_t> 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<double>
|
|
* 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<double>(v.as_integer()) : v.as_floating();
|
|
}
|
|
|
|
static uint32_t parse_hex(const std::string& s, const std::string& field) {
|
|
try {
|
|
return static_cast<uint32_t>(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()));
|
|
}
|
|
}
|
|
|
|
// 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<unsigned char>(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<std::string>(loc, "language");
|
|
// `default` is the launcher-facing alias for the pre-selected language.
|
|
if (loc.contains("default"))
|
|
rt.language = toml::find<std::string>(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<std::string>(e, "code");
|
|
lo.label = e.contains("label") ? toml::find<std::string>(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<std::string>(runtime, "language");
|
|
|
|
if (runtime.contains("debug_port")) {
|
|
const auto port = toml::find<int64_t>(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<uint16_t>(port);
|
|
rt.has_debug_port = true;
|
|
}
|
|
if (runtime.contains("window_title")) {
|
|
rt.window_title = toml::find<std::string>(runtime, "window_title");
|
|
rt.has_window_title = true;
|
|
}
|
|
if (runtime.contains("controller")) {
|
|
rt.controller = toml::find<std::string>(runtime, "controller");
|
|
for (char& c : rt.controller) {
|
|
c = static_cast<char>(std::tolower(static_cast<unsigned char>(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<std::string>(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<std::string>(runtime, "disc_speed");
|
|
rt.has_disc_speed = true;
|
|
}
|
|
if (runtime.contains("instant_max_per_frame")) {
|
|
const auto n = toml::find<int64_t>(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<int>(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<int64_t>(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<int>(arm);
|
|
const auto& lbas = toml::find<std::vector<int64_t>>(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<int>(lba));
|
|
}
|
|
if (route.contains("instant_max_per_frame")) {
|
|
const auto n = toml::find<int64_t>(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<int>(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<bool>(runtime, "fast_boot");
|
|
}
|
|
if (runtime.contains("openbios")) {
|
|
rt.openbios = toml::find<bool>(runtime, "openbios");
|
|
}
|
|
if (runtime.contains("bios_hle")) {
|
|
rt.bios_hle = toml::find<bool>(runtime, "bios_hle");
|
|
}
|
|
if (runtime.contains("bios_hle_keep_intro")) {
|
|
rt.bios_hle_keep_intro = toml::find<bool>(runtime, "bios_hle_keep_intro");
|
|
}
|
|
if (runtime.contains("hide_hidden_mod_features")) {
|
|
rt.hide_hidden_mod_features = toml::find<bool>(runtime, "hide_hidden_mod_features");
|
|
}
|
|
if (runtime.contains("hle_scheduler")) {
|
|
rt.hle_scheduler = toml::find<bool>(runtime, "hle_scheduler");
|
|
}
|
|
if (runtime.contains("overlay_cache")) {
|
|
rt.overlay_cache = toml::find<bool>(runtime, "overlay_cache");
|
|
}
|
|
if (runtime.contains("overlay_capture_history")) {
|
|
rt.overlay_capture_history =
|
|
toml::find<bool>(runtime, "overlay_capture_history");
|
|
}
|
|
if (runtime.contains("overlay_capture_persist_dir")) {
|
|
rt.overlay_capture_persist_dir =
|
|
toml::find<std::string>(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<bool>(runtime, "turbo_loads");
|
|
rt.has_turbo_loads = true;
|
|
}
|
|
if (runtime.contains("offer_turbo_loads")) {
|
|
rt.offer_turbo_loads =
|
|
toml::find<bool>(runtime, "offer_turbo_loads");
|
|
rt.has_offer_turbo_loads = true;
|
|
}
|
|
if (runtime.contains("turbo_audio_sink")) {
|
|
rt.turbo_audio_sink = toml::find<bool>(runtime, "turbo_audio_sink");
|
|
}
|
|
if (runtime.contains("idle_skip_store_counters")) {
|
|
rt.idle_skip_store_counters =
|
|
toml::find<bool>(runtime, "idle_skip_store_counters");
|
|
}
|
|
if (runtime.contains("idle_skip_store_counter_sites")) {
|
|
for (const auto& a : toml::find<std::vector<std::string>>(
|
|
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<bool>(runtime, "idle_skip");
|
|
}
|
|
if (runtime.contains("overlay_autocompile_cmd")) {
|
|
rt.overlay_autocompile_cmd =
|
|
toml::find<std::string>(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<std::string>(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<std::string>(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<uint32_t>(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<std::vector<std::string>>(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<std::string>(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<int64_t>(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<long long>(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<int>(n);
|
|
}
|
|
if (tm.contains("guest_cycle_scale_gated"))
|
|
rt.guest_cycle_scale_gated = toml::find<bool>(tm, "guest_cycle_scale_gated");
|
|
if (tm.contains("guest_cycle_scale_batch"))
|
|
rt.guest_cycle_scale_batch = toml::find<bool>(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<toml::value> 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<long long>(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<uint32_t>(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<int64_t>(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<int>(n);
|
|
}
|
|
if (video.contains("depth24_trailing_margin")) {
|
|
const auto n = toml::find<int64_t>(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<int>(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<int>(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<bool>(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<int>(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<int64_t>(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<int>(n);
|
|
}
|
|
if (video.contains("match_display_max_lines")) {
|
|
const auto n = toml::find<int64_t>(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<int>(n);
|
|
}
|
|
if (video.contains("supersample_disable_at_lines")) {
|
|
const auto n = toml::find<int64_t>(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<int>(n);
|
|
}
|
|
if (video.contains("window_width")) {
|
|
const auto n = toml::find<int64_t>(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<int>(n);
|
|
}
|
|
if (video.contains("antialiasing")) {
|
|
rt.video_antialiasing = toml::find<bool>(video, "antialiasing");
|
|
}
|
|
if (video.contains("antialiasing_mode")) {
|
|
const auto mode = toml::find<std::string>(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<double>(v.as_integer())
|
|
: toml::get<double>(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<float>(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<std::string>(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<double>(v.as_integer())
|
|
: toml::get<double>(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<int>(f * 1000.0 + 0.5);
|
|
}
|
|
if (video.contains("texture_filtering")) {
|
|
const auto mode = toml::find<std::string>(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<std::string>(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<std::string>(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<bool>(video, "offer_vulkan");
|
|
}
|
|
if (video.contains("geometry_correction")) {
|
|
rt.video_geometry_correction =
|
|
toml::find<bool>(video, "geometry_correction");
|
|
}
|
|
if (video.contains("perspective_texturing")) {
|
|
rt.video_perspective_texturing =
|
|
toml::find<bool>(video, "perspective_texturing");
|
|
}
|
|
if (video.contains("pgxp_cpu_mode")) {
|
|
rt.video_pgxp_cpu_mode = toml::find<bool>(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<bool>(video, "pgxp_position_fallback");
|
|
rt.video_pgxp_position_fallback_set = true;
|
|
}
|
|
if (video.contains("pgxp_preserve_projection")) {
|
|
rt.video_pgxp_preserve_projection =
|
|
toml::find<bool>(video, "pgxp_preserve_projection");
|
|
}
|
|
if (video.contains("fmv_chroma_smoothing"))
|
|
rt.video_fmv_chroma_smoothing = toml::find<bool>(video, "fmv_chroma_smoothing");
|
|
if (video.contains("texture_lod")) {
|
|
const auto mode = toml::find<std::string>(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<int>(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<bool>(video, "accurate_blending");
|
|
if (video.contains("dithering")) {
|
|
const auto mode = toml::find<std::string>(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<bool>(video, "pgxp_depth_buffer");
|
|
if (video.contains("pgxp_color_correction"))
|
|
rt.video_pgxp_color_correction = toml::find<bool>(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<std::string>(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<bool>(video, "pgxp_mod_only");
|
|
}
|
|
if (video.contains("crt_filter")) {
|
|
const auto mode = toml::find<std::string>(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<bool>(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<bool>(video, "auto_skip_fmv");
|
|
}
|
|
if (video.contains("offer_skip_fmv")) {
|
|
rt.video_offer_skip_fmv = toml::find<bool>(video, "offer_skip_fmv");
|
|
}
|
|
if (video.contains("fmv_skip_total_table")) {
|
|
rt.video_fmv_skip_total_table =
|
|
(uint32_t)toml::find<int64_t>(video, "fmv_skip_total_table");
|
|
}
|
|
if (video.contains("fmv_skip_movie_id")) {
|
|
rt.video_fmv_skip_movie_id =
|
|
(uint32_t)toml::find<int64_t>(video, "fmv_skip_movie_id");
|
|
}
|
|
if (video.contains("fmv_skip_end_total")) {
|
|
rt.video_fmv_skip_end_total =
|
|
(int)toml::find<int64_t>(video, "fmv_skip_end_total");
|
|
}
|
|
if (video.contains("fmv_skip_no_xa")) {
|
|
rt.video_fmv_skip_no_xa = toml::find<bool>(video, "fmv_skip_no_xa");
|
|
}
|
|
if (video.contains("fmv_skip_no_xa_hold")) {
|
|
rt.video_fmv_skip_no_xa_hold =
|
|
(int)toml::find<int64_t>(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<bool>(video, "low_latency_input");
|
|
}
|
|
if (video.contains("texture_window_batching")) {
|
|
rt.video_texture_window_batching =
|
|
toml::find<bool>(video, "texture_window_batching");
|
|
}
|
|
if (video.contains("render_thread")) {
|
|
rt.video_render_thread = toml::find<bool>(video, "render_thread");
|
|
}
|
|
if (video.contains("present_thread")) {
|
|
rt.video_present_thread = toml::find<bool>(video, "present_thread");
|
|
}
|
|
if (video.contains("frame_generation")) {
|
|
rt.video_frame_generation = toml::find<bool>(video, "frame_generation");
|
|
}
|
|
if (video.contains("frame_generation_method")) {
|
|
const auto m = toml::find<std::string>(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<std::string>(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<bool>(video, "frame_interpolation");
|
|
}
|
|
if (video.contains("frame_interpolation_fps")) {
|
|
rt.video_frame_interpolation_fps =
|
|
toml::find<int>(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<bool>(video, "offer_frame_interpolation");
|
|
}
|
|
if (video.contains("aspect_ratio")) {
|
|
const auto mode = toml::find<std::string>(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<int64_t>(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<int>(n);
|
|
}
|
|
if (audio.contains("spu_hq")) {
|
|
rt.audio_spu_hq = toml::find<bool>(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<bool>(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<bool>(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<bool>(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<std::string>(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<std::string>(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<std::string>(ct, "p2_mode"), PAD_MODE_ANALOG);
|
|
rt.has_default_mode = true;
|
|
}
|
|
if (ct.contains("p1_device")) {
|
|
rt.default_p1_device = toml::find<std::string>(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<std::string>(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<bool>(ct, "lock_mode");
|
|
}
|
|
if (ct.contains("lock_device")) {
|
|
rt.controller_lock_device = toml::find<bool>(ct, "lock_device");
|
|
}
|
|
if (ct.contains("deadzone")) {
|
|
const auto n = toml::find<int64_t>(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<int>(n);
|
|
rt.has_deadzone = true;
|
|
}
|
|
if (ct.contains("multitap_port")) {
|
|
const auto n = toml::find<int64_t>(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<int>(n);
|
|
rt.has_multitap_port = true;
|
|
}
|
|
if (ct.contains("multitap")) {
|
|
rt.multitap_default = toml::find<bool>(ct, "multitap");
|
|
rt.has_multitap_default = true;
|
|
}
|
|
if (ct.contains("multitap_analog")) {
|
|
rt.multitap_analog = toml::find<bool>(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<bool>(ct, "legacy_pad_config");
|
|
}
|
|
if (ct.contains("direct_shortcut")) {
|
|
rt.direct_shortcut = toml::find<std::string>(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<std::string>(ct, "direct_shortcut_button");
|
|
}
|
|
if (ct.contains("experimental_jogcon_force_feedback"))
|
|
rt.experimental_jogcon_force_feedback =
|
|
toml::find<bool>(ct, "experimental_jogcon_force_feedback");
|
|
if (ct.contains("experimental_jogcon_force_feedback_invert"))
|
|
rt.experimental_jogcon_force_feedback_invert =
|
|
toml::find<bool>(ct, "experimental_jogcon_force_feedback_invert");
|
|
if (ct.contains("anti_deadzone")) {
|
|
const auto n = toml::find<int64_t>(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<int>(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<ModImmediateSite> parse_mod_immediate_sites(
|
|
const toml::value& cfg, const fs::path& config_path) {
|
|
std::vector<ModImmediateSite> 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<std::pair<uint32_t, uint32_t>> seen;
|
|
for (const auto& item : toml::find<toml::array>(mi, "sites")) {
|
|
ModImmediateSite site;
|
|
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
|
"mod_immediate.sites.address");
|
|
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
|
"mod_immediate.sites.expected");
|
|
site.group = (uint32_t)toml::find<int64_t>(item, "group");
|
|
for (const auto& v : toml::find<toml::array>(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<DrawDistanceClampSite> parse_draw_distance_clamps(
|
|
const toml::value& cfg, const fs::path& config_path) {
|
|
std::vector<DrawDistanceClampSite> 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<toml::array>(dd, "clamp");
|
|
std::set<uint32_t> seen;
|
|
for (const auto& item : items) {
|
|
DrawDistanceClampSite site;
|
|
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
|
"draw_distance.clamp.address");
|
|
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
|
"draw_distance.clamp.expected");
|
|
const int64_t reg = toml::find<int64_t>(item, "reg");
|
|
const int64_t max = toml::find<int64_t>(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.<name>] (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<QualityPreset> parse_quality_presets(const toml::value& cfg,
|
|
const fs::path& root,
|
|
const fs::path& config_path) {
|
|
std::vector<QualityPreset> 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<unsigned char>(a)) !=
|
|
std::tolower(static_cast<unsigned char>(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<std::string>(prog, "name");
|
|
const std::string id = prog.contains("id")
|
|
? toml::find<std::string>(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<std::string>(prog, "rom");
|
|
} else if (prog.contains("exe")) {
|
|
rom_field = toml::find<std::string>(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<std::string>(prog, "load_address"), "program.load_address");
|
|
const uint32_t entry_pc =
|
|
prog.contains("entry_pc")
|
|
? parse_hex(toml::find<std::string>(prog, "entry_pc"), "program.entry_pc")
|
|
: load_address;
|
|
const uint32_t text_size =
|
|
parse_hex(toml::find<std::string>(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<std::string>(img, "sha256");
|
|
if (img.contains("redistributable"))
|
|
image_redistributable = toml::find<bool>(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<std::string>(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<std::string>(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<bool>(recomp, "strict")
|
|
: true;
|
|
|
|
std::string out_stem;
|
|
if (recomp.contains("out_stem")) {
|
|
out_stem = toml::find<std::string>(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<BiosVectorTable> 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<std::string>(v, "ram_addr"), "bios_vectors.ram_addr");
|
|
bvt.index_reg = static_cast<int>(
|
|
toml::find<int64_t>(v, "index_reg"));
|
|
bvt.table_rom_addr = parse_hex(
|
|
toml::find<std::string>(v, "table_rom_addr"), "bios_vectors.table_rom_addr");
|
|
bvt.table_count = static_cast<uint32_t>(
|
|
toml::find<int64_t>(v, "table_count"));
|
|
bvt.table_ram_addr = v.contains("table_ram_addr")
|
|
? parse_hex(toml::find<std::string>(v, "table_ram_addr"),
|
|
"bios_vectors.table_ram_addr")
|
|
: 0u;
|
|
bios_vectors.push_back(bvt);
|
|
}
|
|
}
|
|
|
|
// [[recompiler.bios_aliases]] — fixed-target RAM trampolines
|
|
std::vector<BiosAlias> 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<std::string>(v, "ram_addr"),
|
|
"bios_aliases.ram_addr");
|
|
ba.target_key = parse_hex(toml::find<std::string>(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<BiosAddrCopy> 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<std::string>(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<std::string>(v, "name");
|
|
c.rom_lo = parse_hex(toml::find<std::string>(v, "rom_lo"),
|
|
"address_model.copy.rom_lo");
|
|
c.rom_hi = parse_hex(toml::find<std::string>(v, "rom_hi"),
|
|
"address_model.copy.rom_hi");
|
|
c.ram_lo = parse_hex(toml::find<std::string>(v, "ram_lo"),
|
|
"address_model.copy.ram_lo");
|
|
c.runtime_base = parse_hex(
|
|
toml::find<std::string>(v, "runtime_base"),
|
|
"address_model.copy.runtime_base");
|
|
const std::string key =
|
|
toml::find<std::string>(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<bool>(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<BiosInstallSlot> 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<std::string>(v, "ram_addr"),
|
|
"install_slots.ram_addr");
|
|
if (v.contains("len"))
|
|
slot.len = parse_hex(toml::find<std::string>(v, "len"),
|
|
"install_slots.len");
|
|
if (v.contains("resume")) {
|
|
const std::string r = toml::find<std::string>(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<std::string>(rx, "shell_entry_phys"),
|
|
"runtime_exports.shell_entry_phys");
|
|
if (rx.contains("deliver_event_ret"))
|
|
deliver_event_ret = parse_hex(
|
|
toml::find<std::string>(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<std::string>(game, "name");
|
|
const std::string id = game.contains("id")
|
|
? toml::find<std::string>(game, "id")
|
|
: std::string{};
|
|
const std::string region = game.contains("region")
|
|
? toml::find<std::string>(game, "region")
|
|
: std::string{};
|
|
const int players = game.contains("players")
|
|
? (int)toml::find<int64_t>(game, "players")
|
|
: 1;
|
|
|
|
std::string exe_field;
|
|
if (game.contains("exe")) {
|
|
exe_field = toml::find<std::string>(game, "exe");
|
|
} else if (game.contains("rom")) {
|
|
exe_field = toml::find<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<fs::path> discs;
|
|
if (game.contains("discs")) {
|
|
const auto& arr = toml::find<std::vector<std::string>>(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<std::string>(game, "disc");
|
|
discs.push_back(PSXRecompV4::host_resolve(root, d));
|
|
}
|
|
/* Per-disc serials, parallel to `discs`. Absent => no per-disc gate. */
|
|
std::vector<std::string> disc_serials;
|
|
if (game.contains("disc_serials"))
|
|
disc_serials = toml::find<std::vector<std::string>>(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<std::string>(game, "disc_crc"), "game.disc_crc");
|
|
has_disc_crc = true;
|
|
}
|
|
if (game.contains("disc_sha1")) {
|
|
disc_sha1 = toml::find<std::string>(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<std::string> netplay_required_disc_fps;
|
|
std::string netplay_local_viewport;
|
|
std::string netplay_local_viewport_aspect;
|
|
std::string netplay_local_viewport_renderer;
|
|
std::vector<uint32_t> netplay_local_viewport_width_sites;
|
|
uint32_t netplay_local_viewport_state_addr = 0;
|
|
std::vector<uint32_t> 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<bool>(np, "content_negotiation");
|
|
if (np.contains("require_cue"))
|
|
netplay_require_cue = toml::find<bool>(np, "require_cue");
|
|
if (np.contains("required_tracks"))
|
|
netplay_required_tracks = toml::find<int>(np, "required_tracks");
|
|
if (np.contains("required_leadout_lba")) {
|
|
netplay_required_leadout_lba =
|
|
(uint32_t)toml::find<int64_t>(np, "required_leadout_lba");
|
|
has_netplay_required_leadout = true;
|
|
}
|
|
if (np.contains("required_disc_fp")) {
|
|
netplay_required_disc_fp = toml::find<std::string>(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<std::vector<std::string>>(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<std::string>(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<std::string>(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<std::vector<std::string>>(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<std::string>(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<uint32_t> 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<std::string>(np, "local_viewport_state_addr"),
|
|
"netplay.local_viewport_state_addr");
|
|
for (const auto& value : toml::find<std::vector<std::string>>(
|
|
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<std::string>(recomp, "seeds"));
|
|
|
|
fs::path bios_thunks_path;
|
|
if (recomp.contains("bios_thunks")) {
|
|
bios_thunks_path =
|
|
PSXRecompV4::host_resolve(root, toml::find<std::string>(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<std::string>(recomp, "bios_config"));
|
|
}
|
|
|
|
const std::string out_dir_field =
|
|
recomp.contains("out_dir")
|
|
? toml::find<std::string>(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<bool>(recomp, "strict")
|
|
: true;
|
|
|
|
const std::string discovery =
|
|
recomp.contains("discovery")
|
|
? toml::find<std::string>(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<std::string>(recomp, "out_stem");
|
|
} else {
|
|
out_stem = derive_out_stem(fs::path(exe_field).filename().string());
|
|
}
|
|
|
|
std::vector<RecompilerPatch> recompiler_patches;
|
|
if (recomp.contains("patch")) {
|
|
const auto& patches = toml::find<toml::array>(recomp, "patch");
|
|
std::set<std::string> seen_ids;
|
|
std::set<uint32_t> seen_addresses;
|
|
for (const auto& item : patches) {
|
|
RecompilerPatch patch;
|
|
patch.id = toml::find<std::string>(item, "id");
|
|
patch.address = parse_hex(toml::find<std::string>(item, "address"),
|
|
"recompiler.patch.address");
|
|
patch.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
|
"recompiler.patch.expected");
|
|
patch.replacement = parse_hex(
|
|
toml::find<std::string>(item, "replacement"),
|
|
"recompiler.patch.replacement");
|
|
if (item.contains("note")) {
|
|
patch.note = toml::find<std::string>(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<uint32_t> 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<uint32_t> 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<WidescreenSignedBoundSite> 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<bool>(video, "offer_vulkan");
|
|
}
|
|
// Optional [data_shards] block — memoized pure-function replay hooks.
|
|
std::vector<uint32_t> 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<std::vector<std::string>>(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<uint32_t> mod_instruction_sites;
|
|
if (recomp.contains("mod_instruction_sites"))
|
|
for (const auto& a : toml::find<std::vector<std::string>>(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<uint32_t> mod_function_entry_funcs;
|
|
if (recomp.contains("mod_function_entry_funcs")) {
|
|
const auto& arr = toml::find<std::vector<std::string>>(
|
|
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<uint32_t> hot_funcs;
|
|
if (recomp.contains("hot_funcs")) {
|
|
const auto& arr = toml::find<std::vector<std::string>>(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<uint32_t> load_charge_batch_funcs;
|
|
if (recomp.contains("load_charge_batch"))
|
|
load_charge_batch = toml::find<bool>(recomp, "load_charge_batch");
|
|
if (recomp.contains("load_charge_batch_funcs")) {
|
|
const auto& arr =
|
|
toml::find<std::vector<std::string>>(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<uint32_t> vsync_event_horizon_sites;
|
|
std::vector<uint32_t> 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<std::string>(vq, "func"),
|
|
"load_accel.vsync_query.func");
|
|
vsync_counter_addr = parse_hex(
|
|
toml::find<std::string>(vq, "counter_addr"),
|
|
"load_accel.vsync_query.counter_addr");
|
|
vsync_gpustat_ptr_addr = parse_hex(
|
|
toml::find<std::string>(vq, "gpustat_ptr_addr"),
|
|
"load_accel.vsync_query.gpustat_ptr_addr");
|
|
vsync_timer1_ptr_addr = parse_hex(
|
|
toml::find<std::string>(vq, "timer1_ptr_addr"),
|
|
"load_accel.vsync_query.timer1_ptr_addr");
|
|
vsync_timer1_cache_addr = parse_hex(
|
|
toml::find<std::string>(vq, "timer1_cache_addr"),
|
|
"load_accel.vsync_query.timer1_cache_addr");
|
|
if (vq.contains("event_horizon_sites")) {
|
|
const auto& arr = toml::find<std::vector<std::string>>(
|
|
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<std::vector<std::string>>(
|
|
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<bool>(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<std::vector<std::string>>(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<std::string>(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<bool>(ws, "hud_sprt_squash");
|
|
if (ws.contains("auto_ui_squash"))
|
|
ws_auto_ui_squash = toml::find<bool>(ws, "auto_ui_squash");
|
|
if (ws.contains("auto_ui_anchor")) {
|
|
const auto anchor = toml::find<std::string>(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<std::string>(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<bool>(ws, "full_2d");
|
|
if (ws.contains("gte_game_mode"))
|
|
ws_gte_game_mode = toml::find<bool>(ws, "gte_game_mode");
|
|
if (ws.contains("precise_nclip"))
|
|
ws_precise_nclip = toml::find<bool>(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<std::string>(ws, "gameplay_state_addr"),
|
|
"widescreen.gameplay_state_addr");
|
|
const auto& arr = toml::find<std::vector<std::string>>(
|
|
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<bool>(ws, "native_wide");
|
|
if (ws.contains("nw_hud_corners"))
|
|
ws_nw_hud_corners = toml::find<bool>(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<std::string>(ws, "nw_left_hud_packet_lo"),
|
|
"widescreen.nw_left_hud_packet_lo");
|
|
ws_nw_left_hud_packet_hi = parse_hex(
|
|
toml::find<std::string>(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<bool>(ws, "nw_backdrop");
|
|
if (ws.contains("clear_reveal"))
|
|
ws_clear_reveal = toml::find<bool>(ws, "clear_reveal");
|
|
if (ws.contains("nw_flat_backdrop"))
|
|
ws_nw_flat_backdrop = toml::find<bool>(ws, "nw_flat_backdrop");
|
|
if (ws.contains("nw_phase_backdrop"))
|
|
ws_nw_phase_backdrop = toml::find<bool>(ws, "nw_phase_backdrop");
|
|
if (ws.contains("nw_textured_edges"))
|
|
ws_nw_textured_edges = toml::find<bool>(ws, "nw_textured_edges");
|
|
if (ws.contains("nw_textured_edge_scale")) {
|
|
ws_nw_textured_edge_scale = toml::find<int>(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<bool>(ws, "nw_full_mirror");
|
|
if (ws.contains("signed_x_bound")) {
|
|
std::set<uint32_t> seen;
|
|
for (const auto& item : toml::find<toml::array>(ws, "signed_x_bound")) {
|
|
WidescreenSignedBoundSite site;
|
|
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
|
"widescreen.signed_x_bound.address");
|
|
site.expected = parse_hex(toml::find<std::string>(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<bool>(ws, "offer");
|
|
if (ws.contains("offer_ultrawide"))
|
|
ws_ultrawide_offered = toml::find<bool>(ws, "offer_ultrawide");
|
|
if (ws.contains("adaptive_view"))
|
|
ws_adaptive_view = toml::find<bool>(ws, "adaptive_view");
|
|
}
|
|
|
|
// Optional [widescreen.cull] block — world-space draw-cull widening.
|
|
std::vector<uint32_t> ws_cull_bias_sites, ws_cull_range_sites, ws_cull_a1_sites;
|
|
std::vector<uint32_t> ws_cull_bias_lower_sites;
|
|
std::vector<uint32_t> ws_cull_screen_x_sites;
|
|
std::vector<uint32_t> ws_cull_slti_sites;
|
|
std::vector<uint32_t> ws_cull_slti_lower_sites;
|
|
std::vector<uint32_t> ws_cull_bltz_sites;
|
|
std::vector<uint32_t> ws_cull_negsub_sites;
|
|
std::vector<uint32_t> ws_cull_vxrange_sites;
|
|
std::vector<uint32_t> ws_cull_depth_sites;
|
|
std::vector<uint32_t> ws_cull_plane_nx_sites;
|
|
std::vector<uint32_t> ws_cull_xclip_load_sites;
|
|
std::vector<uint32_t> ws_cull_nclip_keep_sites;
|
|
std::vector<uint32_t> ws_cull_nclip_exact_sites;
|
|
std::vector<uint32_t> ws_cull_branch_keep_sites;
|
|
std::vector<uint32_t> ws_cull_bgez_sites;
|
|
std::vector<uint32_t> ws_cull_clip_edge_x_load_sites;
|
|
uint32_t ws_cull_clip_edge_width = 0;
|
|
std::vector<WidescreenCullKeepSite> ws_cull_keep_sites;
|
|
std::vector<WidescreenMaskedRejectSite> ws_cull_masked_reject_sites;
|
|
std::vector<WidescreenCullScaleSite> ws_cull_scale_sites;
|
|
std::vector<WidescreenAngleSite> 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<uint32_t> ws_cull_w_imms = { 0x140, 0x141 };
|
|
std::vector<uint32_t> 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<uint32_t>& out) {
|
|
if (!cull.contains(key)) return;
|
|
for (const auto& a : toml::find<std::vector<std::string>>(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<int64_t>(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<uint32_t> seen;
|
|
for (const auto& item : toml::find<toml::array>(cull, "masked_reject")) {
|
|
WidescreenMaskedRejectSite site;
|
|
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
|
"widescreen.cull.masked_reject.address");
|
|
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
|
"widescreen.cull.masked_reject.expected");
|
|
site.reject_mask = parse_hex(toml::find<std::string>(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("keep")) {
|
|
std::set<uint32_t> seen;
|
|
for (const auto& item : toml::find<toml::array>(cull, "keep")) {
|
|
WidescreenCullKeepSite site;
|
|
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
|
"widescreen.cull.keep.address");
|
|
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
|
"widescreen.cull.keep.expected");
|
|
site.result = (uint32_t)toml::find<int>(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("scale")) {
|
|
std::set<uint32_t> seen;
|
|
for (const auto& item : toml::find<toml::array>(cull, "scale")) {
|
|
WidescreenCullScaleSite site;
|
|
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
|
"widescreen.cull.scale.address");
|
|
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
|
"widescreen.cull.scale.expected");
|
|
const std::string operand = toml::find<std::string>(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<int>(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<uint32_t> seen;
|
|
for (const auto& item :
|
|
toml::find<toml::array>(cull, "angle")) {
|
|
WidescreenAngleSite site;
|
|
site.address = parse_hex(
|
|
toml::find<std::string>(item, "address"),
|
|
"widescreen.cull.angle.address");
|
|
site.expected = parse_hex(
|
|
toml::find<std::string>(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<std::string>(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<int>(cone, "object_type_offset");
|
|
const int object_reg = toml::find<int>(cone, "object_reg");
|
|
const int x_reg = toml::find<int>(cone, "x_reg");
|
|
const int z_reg = toml::find<int>(cone, "z_reg");
|
|
const int y_reg = toml::find<int>(cone, "y_reg");
|
|
const int hysteresis_pixels =
|
|
toml::find_or<int>(cone, "hysteresis_pixels", 0);
|
|
const int queue_reserve =
|
|
toml::find_or<int>(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<uint32_t, 3>& out) {
|
|
const auto values =
|
|
toml::find<std::vector<std::string>>(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<uint32_t, 3>& out) {
|
|
const auto values =
|
|
toml::find<std::vector<int>>(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<uint32_t> seen;
|
|
for (const auto& item :
|
|
toml::find<toml::array>(cone, "sites")) {
|
|
WidescreenAspectConeSite site;
|
|
site.address = parse_hex(
|
|
toml::find<std::string>(item, "address"),
|
|
"widescreen.cull.aspect_cone.sites.address");
|
|
site.expected = parse_hex(
|
|
toml::find<std::string>(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<int>(
|
|
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<int>(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<bool>(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<int>(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<int>(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<bool>(cull, "auto_screen_x");
|
|
if (cull.contains("auto_backdrop"))
|
|
ws_auto_backdrop_preload = toml::find<bool>(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<std::string>(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<int64_t>(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<uint32_t>(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<uint32_t> ws_backdrop_x_sites;
|
|
std::vector<uint32_t> ws_backdrop_unsquash_funcs;
|
|
std::vector<uint32_t> 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<std::vector<std::string>>(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<std::vector<std::string>>(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<std::vector<std::string>>(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_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.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<toml::array>(doc, "option");
|
|
for (const auto& item : arr) {
|
|
GameOption o;
|
|
o.name = toml::find<std::string>(item, "name");
|
|
o.addr = parse_hex(toml::find<std::string>(item, "addr"),
|
|
fmt::format("game_options [[option]] '{}' addr", o.name));
|
|
o.size = static_cast<int>(toml::find<int64_t>(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<std::string>(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<int64_t>(item, "max");
|
|
o.vmin = item.contains("min") ? toml::find<int64_t>(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<std::string>(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<int64_t>(v, "supersampling");
|
|
if (n >= 1 && n <= 32) { s.supersampling = (int)n; s.has_supersampling = 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<bool>(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<int64_t>(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<bool>(v, "antialiasing"); s.has_antialiasing = true;
|
|
});
|
|
if (v.contains("texture_filtering")) try_get([&]{
|
|
const auto m = toml::find<std::string>(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<std::string>(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<bool>(v, "geometry_correction");
|
|
s.has_geometry_correction = true;
|
|
});
|
|
if (v.contains("perspective_texturing")) try_get([&]{
|
|
s.perspective_texturing = toml::find<bool>(v, "perspective_texturing");
|
|
s.has_perspective_texturing = true;
|
|
});
|
|
if (v.contains("crt_filter")) try_get([&]{
|
|
const auto m = toml::find<std::string>(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<bool>(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<double>(n.as_integer());
|
|
else return;
|
|
s.has_fov_scale = true;
|
|
});
|
|
if (v.contains("auto_skip_fmv")) try_get([&]{
|
|
s.auto_skip_fmv = toml::find<bool>(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<bool>(v, "turbo_loads"); s.has_turbo_loads = true;
|
|
});
|
|
if (v.contains("fast_boot")) try_get([&]{
|
|
s.fast_boot = toml::find<bool>(v, "fast_boot"); s.has_fast_boot = true;
|
|
});
|
|
if (v.contains("bios_hle")) try_get([&]{
|
|
s.bios_hle = toml::find<bool>(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<int>(v, "fullscreen");
|
|
} catch (const std::exception&) {
|
|
s.fullscreen = toml::find<bool>(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<bool>(v, "low_latency_input");
|
|
s.has_low_latency_input = true;
|
|
});
|
|
if (v.contains("render_thread")) try_get([&]{
|
|
s.render_thread = toml::find<bool>(v, "render_thread");
|
|
s.has_render_thread = true;
|
|
});
|
|
if (v.contains("present_thread")) try_get([&]{
|
|
s.present_thread = toml::find<bool>(v, "present_thread");
|
|
s.has_present_thread = true;
|
|
});
|
|
if (v.contains("frame_generation")) try_get([&]{
|
|
s.frame_generation = toml::find<bool>(v, "frame_generation");
|
|
s.has_frame_generation = true;
|
|
});
|
|
if (v.contains("vsync")) try_get([&]{
|
|
const auto m = toml::find<std::string>(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<bool>(v, "frame_interpolation");
|
|
s.has_frame_interpolation = true;
|
|
});
|
|
if (v.contains("frame_interpolation_fps")) try_get([&]{
|
|
s.frame_interpolation_fps = toml::find<int>(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<std::string>(v, "quality_preset");
|
|
s.has_quality_preset = !s.quality_preset.empty();
|
|
});
|
|
if (v.contains("quality_base")) try_get([&]{
|
|
s.quality_base = toml::find<std::string>(v, "quality_base");
|
|
s.has_quality_base = !s.quality_base.empty();
|
|
});
|
|
if (v.contains("quality_hardware")) try_get([&]{
|
|
s.quality_hardware = toml::find<std::string>(v, "quality_hardware");
|
|
s.has_quality_hardware = !s.quality_hardware.empty();
|
|
});
|
|
if (v.contains("quality_detected")) try_get([&]{
|
|
s.quality_detected = toml::find<std::string>(v, "quality_detected");
|
|
s.has_quality_detected = !s.quality_detected.empty();
|
|
});
|
|
if (v.contains("aspect_ratio")) try_get([&]{
|
|
const auto m = toml::find<std::string>(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<bool>(v, "adaptive_view");
|
|
s.has_adaptive_view = true;
|
|
});
|
|
if (v.contains("rewind")) try_get([&]{
|
|
s.rewind = toml::find<bool>(v, "rewind");
|
|
s.has_rewind = true;
|
|
});
|
|
if (v.contains("rewind_depth")) try_get([&]{
|
|
int d = toml::find<int>(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<int>(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<int64_t>(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<bool>(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<int64_t>(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<int64_t>(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<int64_t>(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<int64_t>(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<bool>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<int>(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<std::string>(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<std::string>(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<std::string>(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<bool>(m, "enable1"); s.has_memcard1_enabled = true;
|
|
});
|
|
if (m.contains("enable2")) try_get([&]{
|
|
s.memcard2_enabled = toml::find<bool>(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<std::string>(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<std::string>(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<int64_t>(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<int64_t>(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<std::string>(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<bool>(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<std::string>(ct, kModeKeys[i]), PAD_MODE_ANALOG);
|
|
s.has_p_mode[i] = true;
|
|
});
|
|
if (ct.contains(kDzKeys[i])) try_get([&]{
|
|
const auto n = toml::find<int64_t>(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<bool>(ct, "multitap");
|
|
s.has_multitap_enabled = true;
|
|
});
|
|
if (ct.contains("multitap_analog")) try_get([&]{
|
|
s.multitap_analog = toml::find<bool>(ct, "multitap_analog");
|
|
s.has_multitap_analog = true;
|
|
});
|
|
if (ct.contains("jogcon_force_feedback")) try_get([&]{
|
|
s.jogcon_force_feedback = toml::find<bool>(ct, "jogcon_force_feedback");
|
|
s.has_jogcon_force_feedback = true;
|
|
});
|
|
if (ct.contains("deadzone")) try_get([&]{
|
|
const auto n = toml::find<int64_t>(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_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<std::string> 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
|