mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
2d8a8d5428
The no-bios_config fallback probed exactly two paths, the second of which hardcoded the submodule directory name "psxrecomp/". Game repos do not all use that name: ApeEscapeRecomp vendors the framework as "psxrecomp-v4/", so `psxrecomp-game --config game.toml` failed outright there with "no BIOS profile found" and could not regenerate at all. Probe one directory level for any vendored <framework>/bios/SCPH1001.toml instead, sorted so the pick is deterministic. Verified by regenerating Ape Escape (SCUS-94423) with no [recompiler] bios_config set. Also split the failure message: an explicit bios_config that does not resolve now names the offending path, instead of reporting "no BIOS profile found (set [recompiler] bios_config ...)" for a key that is in fact set -- which sends you looking for the wrong problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1532 lines
75 KiB
C++
1532 lines
75 KiB
C++
#include <cstdio>
|
|
#include <cerrno>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <algorithm>
|
|
#include <set>
|
|
#include <vector>
|
|
|
|
#include "ps1_exe_parser.h"
|
|
#include "gte.h"
|
|
#include "function_analysis.h"
|
|
#include "control_flow.h"
|
|
#include "code_generator.h"
|
|
#include "annotations.hpp"
|
|
#include "config_loader.h"
|
|
#include "rabbitizer.hpp"
|
|
#include "fmt/format.h"
|
|
|
|
/* Baked emitter-source hash (recompiler/CMakeLists.txt custom command; same
|
|
* hash_codegen.cmake + canonical source list as the runtime's overlay cache
|
|
* tag). Exposed via --codegen-hash so compile_overlays.py can verify THIS
|
|
* binary was built from the same emitter sources as the cg tag it stamps —
|
|
* the stale-recompiler-binary guard. 0 when built without the bake (guard
|
|
* consumers treat that as "cannot verify" and fail loudly). */
|
|
#if defined(__has_include)
|
|
# if __has_include("psxrecomp_baked_codegen_hash.h")
|
|
# include "psxrecomp_baked_codegen_hash.h"
|
|
# endif
|
|
#endif
|
|
#ifndef PSX_OVERLAY_CODEGEN_HASH
|
|
# define PSX_OVERLAY_CODEGEN_HASH 0u
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
struct AliasEntry { uint32_t addr, host_start, host_end; };
|
|
|
|
// Materialize alias entries as overlapping functions, grouped by host. Each
|
|
// group shares one emitted body (entry switch + host-range blocks); every
|
|
// member lists its siblings so all group CFGs carry identical block leaders.
|
|
// Host functions are emitted unchanged — aliases never cap or truncate them.
|
|
void materialize_alias_groups(PSXRecomp::FunctionAnalysisResult& result,
|
|
const std::vector<AliasEntry>& alias_entries) {
|
|
std::map<uint32_t, std::vector<const AliasEntry*>> by_host;
|
|
for (const auto& ae : alias_entries) {
|
|
by_host[ae.host_start].push_back(&ae);
|
|
}
|
|
for (const auto& [host_start, group] : by_host) {
|
|
uint32_t producer_lo = 0;
|
|
uint32_t producer_hi = 0;
|
|
for (const auto& function : result.functions) {
|
|
if (function.start_addr == host_start) {
|
|
producer_lo = function.producer_lo;
|
|
producer_hi = function.producer_hi;
|
|
break;
|
|
}
|
|
}
|
|
std::vector<uint32_t> entries;
|
|
for (const AliasEntry* ae : group) entries.push_back(ae->addr);
|
|
for (const AliasEntry* ae : group) {
|
|
PSXRecomp::Function af;
|
|
af.start_addr = ae->addr;
|
|
af.end_addr = ae->host_end;
|
|
af.size = af.end_addr - af.start_addr;
|
|
af.has_prologue = false;
|
|
af.has_epilogue = false;
|
|
af.stack_frame_size = 0;
|
|
af.name = fmt::format("func_{:08X}", ae->addr);
|
|
af.is_data_section = false;
|
|
af.alias_walk_lo = ae->host_start;
|
|
af.alias_group_entries = entries;
|
|
af.producer_lo = producer_lo;
|
|
af.producer_hi = producer_hi;
|
|
result.functions.push_back(af);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
const auto print_usage = [&]() {
|
|
fmt::print("Usage: {} --config <game.toml>\n", argv[0]);
|
|
fmt::print(" {} <PS1-EXE file> [--seeds <file>] [--out-dir <dir>] [--strict] [--inspect]\n", argv[0]);
|
|
fmt::print("Example: {} SCUS_942.36 --seeds seeds/functions.txt --out-dir generated --strict\n", argv[0]);
|
|
};
|
|
for (int i = 1; i < argc; ++i) {
|
|
const std::string arg = argv[i];
|
|
if (arg == "-h" || arg == "--help") {
|
|
print_usage();
|
|
return 0;
|
|
}
|
|
}
|
|
if (argc < 2) {
|
|
print_usage();
|
|
return 0;
|
|
}
|
|
|
|
/* --codegen-hash: print the baked emitter-source hash and exit. Handled
|
|
* before the banner so the output is machine-parseable (one hex line).
|
|
* compile_overlays.py compares this to the hash from --runtime-include and
|
|
* refuses to build shards with a stale recompiler binary. */
|
|
for (int i = 1; i < argc; ++i) {
|
|
if (std::string(argv[i]) == "--codegen-hash") {
|
|
fmt::print("{:08x}\n", (unsigned)PSX_OVERLAY_CODEGEN_HASH);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
fmt::print("PSXRecomp - PlayStation 1 Static Recompiler\n");
|
|
fmt::print("============================================\n\n");
|
|
|
|
// ── --config <path> short-form ────────────────────────────────────
|
|
// If --config is provided, all paths come from the TOML and other
|
|
// CLI flags are ignored. Positional form below stays for back-compat.
|
|
std::filesystem::path config_path;
|
|
// --ws-config <path>: load ONLY the [widescreen] site lists from this TOML
|
|
// without treating it as the full game config. Used by compile_overlays.py
|
|
// so overlay compilation gets the widescreen emits (sprite-tag / cull /
|
|
// backdrop) whose addresses live in overlay code — the overlay path is
|
|
// positional (no --config), so this is the channel for those sites.
|
|
std::filesystem::path ws_config_path;
|
|
for (int i = 1; i < argc; ++i) {
|
|
std::string a = argv[i];
|
|
if (a == "--config" && i + 1 < argc) {
|
|
config_path = argv[i + 1];
|
|
break;
|
|
}
|
|
if (a.rfind("--config=", 0) == 0) {
|
|
config_path = a.substr(std::string("--config=").size());
|
|
break;
|
|
}
|
|
}
|
|
for (int i = 1; i < argc; ++i) {
|
|
std::string a = argv[i];
|
|
if (a == "--ws-config" && i + 1 < argc) { ws_config_path = argv[i + 1]; break; }
|
|
if (a.rfind("--ws-config=", 0) == 0) {
|
|
ws_config_path = a.substr(std::string("--ws-config=").size());
|
|
break;
|
|
}
|
|
}
|
|
|
|
std::filesystem::path exe_path;
|
|
std::string extra_funcs_storage; // lifetime anchor for the .c_str() below
|
|
const char* extra_funcs_path = nullptr;
|
|
bool inspect_mode = false;
|
|
bool overlay_mode = false;
|
|
bool reachable_discovery = false;
|
|
std::set<uint32_t> ws_tag_funcs; // [widescreen] sprite_tag_funcs
|
|
std::set<uint32_t> ds_funcs; // [data_shards] funcs
|
|
std::set<uint32_t> hot_funcs; // [recompiler] hot_funcs
|
|
std::map<uint32_t, std::array<uint32_t, 4>> vsync_query_hle_funcs;
|
|
std::set<uint32_t> ws_cull_bias, ws_cull_range, ws_cull_a1; // [widescreen.cull]
|
|
std::set<uint32_t> ws_cull_screen_x; // [widescreen.cull] screen_x_sites
|
|
std::set<uint32_t> ws_cull_slti; // [widescreen.cull] slti_sites
|
|
std::set<uint32_t> ws_cull_negsub; // [widescreen.cull] negsub_sites
|
|
std::set<uint32_t> ws_cull_vxrange; // [widescreen.cull] vxrange_sites
|
|
std::set<uint32_t> ws_cull_depth; // [widescreen.cull] depth_sites
|
|
std::set<uint32_t> ws_cull_plane_nx; // [widescreen.cull] plane_nx_sites
|
|
std::set<uint32_t> ws_cull_xclip_load; // [widescreen.cull] xclip_load_sites
|
|
std::vector<uint32_t> ws_cull_w_imms = { 0x140, 0x141 }; // [widescreen.cull] screen_w_imms
|
|
std::vector<uint32_t> ws_cull_h_imms = { 0xE0, 0xF1 }; // [widescreen.cull] screen_h_imms
|
|
std::set<uint32_t> ws_backdrop_x; // [widescreen.backdrop] x_sites
|
|
std::set<uint32_t> ws_backdrop_unsquash; // [widescreen.backdrop] unsquash_funcs
|
|
bool ws_auto_screen_x_cull = false; // [widescreen.cull] auto_screen_x
|
|
std::set<uint32_t> persist_init_sites; // [persist_options] init-store hooks (game_options.toml)
|
|
std::vector<PSXRecompV4::RecompilerPatch> instruction_patches;
|
|
std::vector<PSXRecompV4::WidescreenSignedBoundSite> ws_signed_x_bound_sites;
|
|
bool ws_auto_backdrop_preload = false; // [widescreen.cull] auto_backdrop
|
|
uint32_t ws_bg2d_count_site = 0, ws_bg2d_startcol_site = 0,
|
|
ws_bg2d_startx_site = 0,
|
|
ws_bg2d_stream_left_site = 0,
|
|
ws_bg2d_stream_right_site = 0,
|
|
ws_bg2d_bufbase_site = 0,
|
|
ws_bg2d_cap_site = 0,
|
|
ws_bg2d_init_func = 0; // [widescreen.bg2d]
|
|
std::filesystem::path out_dir = "generated";
|
|
uint32_t configured_text_size = 0;
|
|
std::filesystem::path bios_profile_path; // [recompiler] bios_config
|
|
|
|
if (!config_path.empty()) {
|
|
const auto cfg = PSXRecompV4::load_game_config(config_path);
|
|
exe_path = cfg.exe_path;
|
|
configured_text_size = cfg.text_size;
|
|
reachable_discovery = cfg.discovery == "reachable";
|
|
extra_funcs_storage = cfg.seeds_path.string();
|
|
extra_funcs_path = extra_funcs_storage.c_str();
|
|
bios_profile_path = cfg.bios_config_path;
|
|
out_dir = cfg.out_dir;
|
|
instruction_patches = cfg.recompiler_patches;
|
|
ws_tag_funcs.insert(cfg.ws_sprite_tag_funcs.begin(),
|
|
cfg.ws_sprite_tag_funcs.end());
|
|
ds_funcs.insert(cfg.data_shard_funcs.begin(), cfg.data_shard_funcs.end());
|
|
hot_funcs.insert(cfg.hot_funcs.begin(), cfg.hot_funcs.end());
|
|
if (cfg.vsync_query_func)
|
|
vsync_query_hle_funcs[cfg.vsync_query_func] = {
|
|
cfg.vsync_counter_addr, cfg.vsync_gpustat_ptr_addr,
|
|
cfg.vsync_timer1_ptr_addr, cfg.vsync_timer1_cache_addr };
|
|
ws_cull_bias.insert(cfg.ws_cull_bias_sites.begin(), cfg.ws_cull_bias_sites.end());
|
|
ws_cull_range.insert(cfg.ws_cull_range_sites.begin(), cfg.ws_cull_range_sites.end());
|
|
ws_cull_a1.insert(cfg.ws_cull_a1_sites.begin(), cfg.ws_cull_a1_sites.end());
|
|
ws_cull_screen_x.insert(cfg.ws_cull_screen_x_sites.begin(), cfg.ws_cull_screen_x_sites.end());
|
|
ws_cull_slti.insert(cfg.ws_cull_slti_sites.begin(), cfg.ws_cull_slti_sites.end());
|
|
ws_cull_negsub.insert(cfg.ws_cull_negsub_sites.begin(), cfg.ws_cull_negsub_sites.end());
|
|
ws_cull_vxrange.insert(cfg.ws_cull_vxrange_sites.begin(), cfg.ws_cull_vxrange_sites.end());
|
|
ws_cull_depth.insert(cfg.ws_cull_depth_sites.begin(), cfg.ws_cull_depth_sites.end());
|
|
ws_cull_plane_nx.insert(cfg.ws_cull_plane_nx_sites.begin(), cfg.ws_cull_plane_nx_sites.end());
|
|
ws_cull_xclip_load.insert(cfg.ws_cull_xclip_load_sites.begin(), cfg.ws_cull_xclip_load_sites.end());
|
|
ws_cull_w_imms = cfg.ws_cull_w_imms;
|
|
ws_cull_h_imms = cfg.ws_cull_h_imms;
|
|
ws_backdrop_x.insert(cfg.ws_backdrop_x_sites.begin(), cfg.ws_backdrop_x_sites.end());
|
|
ws_backdrop_unsquash.insert(cfg.ws_backdrop_unsquash_funcs.begin(), cfg.ws_backdrop_unsquash_funcs.end());
|
|
ws_auto_screen_x_cull = ws_auto_screen_x_cull || cfg.ws_auto_screen_x_cull;
|
|
ws_auto_backdrop_preload = ws_auto_backdrop_preload || cfg.ws_auto_backdrop_preload;
|
|
if (cfg.ws_bg2d_count_site) ws_bg2d_count_site = cfg.ws_bg2d_count_site;
|
|
if (cfg.ws_bg2d_startcol_site) ws_bg2d_startcol_site = cfg.ws_bg2d_startcol_site;
|
|
if (cfg.ws_bg2d_startx_site) ws_bg2d_startx_site = cfg.ws_bg2d_startx_site;
|
|
if (cfg.ws_bg2d_stream_left_site) ws_bg2d_stream_left_site = cfg.ws_bg2d_stream_left_site;
|
|
if (cfg.ws_bg2d_stream_right_site) ws_bg2d_stream_right_site = cfg.ws_bg2d_stream_right_site;
|
|
if (cfg.ws_bg2d_bufbase_site) ws_bg2d_bufbase_site = cfg.ws_bg2d_bufbase_site;
|
|
if (cfg.ws_bg2d_cap_site) ws_bg2d_cap_site = cfg.ws_bg2d_cap_site;
|
|
if (cfg.ws_bg2d_init_func) ws_bg2d_init_func = cfg.ws_bg2d_init_func;
|
|
ws_signed_x_bound_sites = cfg.ws_signed_x_bound_sites;
|
|
// [persist_options] init-store hook sites live in a dedicated
|
|
// game_options.toml next to game.toml (the game's own native OPTION
|
|
// settings, kept separate from game.toml/settings.toml). Best-effort:
|
|
// a missing/bad file just means no persistence hooks this build.
|
|
try {
|
|
std::filesystem::path go_path =
|
|
std::filesystem::path(config_path).parent_path() / "game_options.toml";
|
|
const auto goc = PSXRecompV4::load_game_options(go_path);
|
|
for (const auto& o : goc.options)
|
|
if (o.init_store_pc) persist_init_sites.insert(o.init_store_pc);
|
|
} catch (const std::exception& e) {
|
|
fmt::print(stderr, " game_options.toml ignored: {}\n", e.what());
|
|
}
|
|
fmt::print("config: {}\n", config_path.string());
|
|
fmt::print(" exe = {}\n", exe_path.string());
|
|
fmt::print(" seeds = {}\n", extra_funcs_storage);
|
|
fmt::print(" out_dir = {}\n", out_dir.string());
|
|
if (!ws_tag_funcs.empty())
|
|
fmt::print(" ws_tag_funcs= {}\n", ws_tag_funcs.size());
|
|
fmt::print("\n");
|
|
} else {
|
|
exe_path = argv[1];
|
|
for (int i = 2; i < argc; i++) {
|
|
std::string arg = argv[i];
|
|
if ((arg == "--extra-funcs" || arg == "--seeds") && i + 1 < argc) {
|
|
extra_funcs_path = argv[++i];
|
|
} else if (arg == "--out-dir" && i + 1 < argc) {
|
|
out_dir = argv[++i];
|
|
} else if (arg == "--strict") {
|
|
/* The PS-X EXE path is fail-loud by default; accepted for parity
|
|
* with psxrecomp-bios and project scripts. */
|
|
} else if (arg == "--inspect") {
|
|
inspect_mode = true;
|
|
} else if (arg == "--overlay") {
|
|
/* Overlay-compilation contract: this input is a runtime-captured
|
|
* overlay with execution evidence, so discovery is evidence-scoped
|
|
* (no whole-byte sweep) and branch/jump-table targets stay as
|
|
* in-parent labels. Set unconditionally by compile_overlays.py for
|
|
* every overlay input — never a human toggle. */
|
|
overlay_mode = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --ws-config: merge the [widescreen] site lists from a side config WITHOUT
|
|
// adopting its exe/paths. The overlay path is positional (no --config), so
|
|
// this is how overlay compilation receives the widescreen emits whose
|
|
// addresses live in overlay code (backdrop screenX, and any sprite-tag /
|
|
// cull sites that resolve there). No-op when --config already provided the
|
|
// sites (same TOML) — std::set insert dedupes.
|
|
if (!ws_config_path.empty()) {
|
|
const auto wscfg = PSXRecompV4::load_game_config(ws_config_path);
|
|
ws_tag_funcs.insert(wscfg.ws_sprite_tag_funcs.begin(), wscfg.ws_sprite_tag_funcs.end());
|
|
ws_cull_bias.insert(wscfg.ws_cull_bias_sites.begin(), wscfg.ws_cull_bias_sites.end());
|
|
ws_cull_range.insert(wscfg.ws_cull_range_sites.begin(), wscfg.ws_cull_range_sites.end());
|
|
ws_cull_a1.insert(wscfg.ws_cull_a1_sites.begin(), wscfg.ws_cull_a1_sites.end());
|
|
ws_cull_screen_x.insert(wscfg.ws_cull_screen_x_sites.begin(), wscfg.ws_cull_screen_x_sites.end());
|
|
ws_cull_slti.insert(wscfg.ws_cull_slti_sites.begin(), wscfg.ws_cull_slti_sites.end());
|
|
ws_cull_negsub.insert(wscfg.ws_cull_negsub_sites.begin(), wscfg.ws_cull_negsub_sites.end());
|
|
ws_cull_vxrange.insert(wscfg.ws_cull_vxrange_sites.begin(), wscfg.ws_cull_vxrange_sites.end());
|
|
ws_cull_depth.insert(wscfg.ws_cull_depth_sites.begin(), wscfg.ws_cull_depth_sites.end());
|
|
ws_cull_plane_nx.insert(wscfg.ws_cull_plane_nx_sites.begin(), wscfg.ws_cull_plane_nx_sites.end());
|
|
ws_cull_xclip_load.insert(wscfg.ws_cull_xclip_load_sites.begin(), wscfg.ws_cull_xclip_load_sites.end());
|
|
ws_cull_w_imms = wscfg.ws_cull_w_imms;
|
|
ws_cull_h_imms = wscfg.ws_cull_h_imms;
|
|
ws_backdrop_x.insert(wscfg.ws_backdrop_x_sites.begin(), wscfg.ws_backdrop_x_sites.end());
|
|
ws_backdrop_unsquash.insert(wscfg.ws_backdrop_unsquash_funcs.begin(), wscfg.ws_backdrop_unsquash_funcs.end());
|
|
ws_auto_screen_x_cull = ws_auto_screen_x_cull || wscfg.ws_auto_screen_x_cull;
|
|
ws_auto_backdrop_preload = ws_auto_backdrop_preload || wscfg.ws_auto_backdrop_preload;
|
|
PSXRecompV4::merge_recompiler_patches(
|
|
instruction_patches, wscfg.recompiler_patches);
|
|
ws_signed_x_bound_sites.insert(ws_signed_x_bound_sites.end(),
|
|
wscfg.ws_signed_x_bound_sites.begin(),
|
|
wscfg.ws_signed_x_bound_sites.end());
|
|
if (wscfg.ws_bg2d_init_func) ws_bg2d_init_func = wscfg.ws_bg2d_init_func;
|
|
fmt::print("ws-config: {} (backdrop_x sites={}, unsquash funcs={})\n",
|
|
ws_config_path.string(), ws_backdrop_x.size(), ws_backdrop_unsquash.size());
|
|
}
|
|
|
|
/* Resolve the BIOS profile this game builds against and activate its
|
|
* address model for codegen (jump tables inside relocated BIOS code
|
|
* windows fold through it — see code_generator.cpp ram_to_rom). Order:
|
|
* explicit [recompiler] bios_config; else the SCPH1001 profile in-repo
|
|
* (framework checkout) or under the psxrecomp/ submodule (game repo).
|
|
* No profile -> fatal: there are no built-in windows. */
|
|
static PSXRecompV4::BiosAddressModel s_bios_model;
|
|
{
|
|
std::filesystem::path prof = bios_profile_path;
|
|
/* An explicit bios_config that does not resolve is a different failure
|
|
* from having none at all — say which, or the message sends you looking
|
|
* for an unset key that is in fact set. */
|
|
if (!prof.empty() && !std::filesystem::exists(prof)) {
|
|
fmt::print(stderr,
|
|
"psxrecomp-game: FATAL: [recompiler] bios_config = '{}' does "
|
|
"not exist (path is relative to the game project root)\n",
|
|
prof.string());
|
|
return 1;
|
|
}
|
|
if (prof.empty()) {
|
|
if (std::filesystem::exists("bios/SCPH1001.toml")) {
|
|
prof = "bios/SCPH1001.toml"; /* framework checkout */
|
|
} else {
|
|
/* Game repo vendoring the framework. Do NOT assume the
|
|
* directory is named "psxrecomp": ApeEscapeRecomp vendors it as
|
|
* "psxrecomp-v4", and hardcoding the name made regeneration
|
|
* fail outright there. Probe one level for any vendored
|
|
* framework, sorted so the pick is deterministic. */
|
|
std::vector<std::filesystem::path> found;
|
|
std::error_code ec;
|
|
for (const auto& de :
|
|
std::filesystem::directory_iterator(".", ec)) {
|
|
if (ec) break;
|
|
if (!de.is_directory(ec) || ec) continue;
|
|
auto cand = de.path() / "bios" / "SCPH1001.toml";
|
|
if (std::filesystem::exists(cand)) found.push_back(cand);
|
|
}
|
|
std::sort(found.begin(), found.end());
|
|
if (!found.empty()) prof = found.front();
|
|
}
|
|
}
|
|
if (prof.empty()) {
|
|
fmt::print(stderr,
|
|
"psxrecomp-game: FATAL: no BIOS profile found (set "
|
|
"[recompiler] bios_config in game.toml, or run from a root "
|
|
"containing bios/SCPH1001.toml or <framework>/bios/"
|
|
"SCPH1001.toml)\n");
|
|
return 1;
|
|
}
|
|
const auto bios_cfg = PSXRecompV4::load_bios_config(prof);
|
|
s_bios_model = PSXRecompV4::BiosAddressModel::from_config(bios_cfg);
|
|
PSXRecomp::CodeGenerator::set_bios_address_model(&s_bios_model);
|
|
}
|
|
|
|
// Parse the PS1-EXE file
|
|
std::string error_msg;
|
|
fmt::print("Parsing PS1-EXE: {}\n", exe_path.string());
|
|
|
|
auto exe = PSXRecomp::PS1ExeParser::parse_file(exe_path, error_msg);
|
|
|
|
if (!exe.has_value()) {
|
|
fmt::print(stderr, "Failed to parse PS1-EXE: {}\n", error_msg);
|
|
return 1;
|
|
}
|
|
|
|
// [game].text_size is the title-owned static-analysis bound. It may trim a
|
|
// verified data/padding tail, but it may never widen the parsed payload or
|
|
// exclude the executable entry. Runtime disc loading remains unchanged.
|
|
if (!config_path.empty()) {
|
|
const uint32_t parsed_size = exe->header.file_size;
|
|
std::string bound_error;
|
|
if (!PSXRecomp::apply_static_analysis_bound(
|
|
*exe, configured_text_size, bound_error)) {
|
|
fmt::print(stderr, "Invalid static analysis bound: {}\n", bound_error);
|
|
return 1;
|
|
}
|
|
if (exe->header.file_size < parsed_size) {
|
|
fmt::print("Static analysis bound: 0x{:X} bytes from game.text_size "
|
|
"(PS-X EXE header: 0x{:X})\n",
|
|
exe->header.file_size, parsed_size);
|
|
}
|
|
}
|
|
|
|
// Patch the bounded image before discovery/CFG analysis so replacements of
|
|
// control-flow instructions cannot disagree with the generated graph.
|
|
PSXRecompV4::apply_recompiler_patches_to_executable(
|
|
*exe, instruction_patches, overlay_mode);
|
|
|
|
fmt::print("✓ Successfully parsed PS1-EXE!\n\n");
|
|
|
|
// Print header information
|
|
fmt::print("PS1-EXE Header Info:\n");
|
|
fmt::print(" Entry Point: 0x{:08X}\n", exe->header.initial_pc);
|
|
fmt::print(" Load Address: 0x{:08X}\n", exe->header.load_address);
|
|
fmt::print(" Code Size: {} bytes ({} KB)\n",
|
|
exe->header.file_size, exe->header.file_size / 1024);
|
|
fmt::print(" End Address: 0x{:08X}\n", exe->end_address());
|
|
fmt::print(" Global Pointer: 0x{:08X}\n", exe->header.initial_gp);
|
|
fmt::print(" Stack Pointer: 0x{:08X}\n\n", exe->header.initial_sp);
|
|
|
|
// Validate entry point
|
|
if (!exe->header.entry_in_range()) {
|
|
fmt::print(stderr, "⚠ Warning: Entry point 0x{:08X} is outside loaded code range!\n\n",
|
|
exe->header.initial_pc);
|
|
}
|
|
|
|
if (inspect_mode) {
|
|
// Disassemble first 20 instructions from entry point
|
|
fmt::print("Disassembly at Entry Point (0x{:08X}):\n", exe->header.initial_pc);
|
|
fmt::print("---------------------------------------\n");
|
|
|
|
uint32_t current_addr = exe->header.initial_pc;
|
|
const int num_instructions = 20;
|
|
|
|
for (int i = 0; i < num_instructions; i++) {
|
|
auto word_opt = exe->read_word(current_addr);
|
|
|
|
if (!word_opt.has_value()) {
|
|
fmt::print(" [End of code or read error]\n");
|
|
break;
|
|
}
|
|
|
|
uint32_t instr_word = *word_opt;
|
|
|
|
// Use Rabbitizer to disassemble the instruction
|
|
// Note: Rabbitizer expects big-endian, PS1 is little-endian, so the word is already in correct format
|
|
rabbitizer::InstructionCpu instr(instr_word, current_addr);
|
|
|
|
// Get disassembly (parameter is flags, 0 = default)
|
|
std::string disasm = instr.disassemble(0);
|
|
|
|
// Print address, hex, and disassembly
|
|
fmt::print(" {:08X}: {:08X} {}\n", current_addr, instr_word, disasm);
|
|
|
|
current_addr += 4;
|
|
}
|
|
|
|
fmt::print("\n");
|
|
fmt::print("✓ Disassembly complete!\n");
|
|
|
|
// Instruction frequency analysis
|
|
fmt::print("\n");
|
|
fmt::print("Analyzing instruction frequency...\n");
|
|
|
|
std::map<std::string, int> instr_freq;
|
|
std::map<uint32_t, int> opcode_freq;
|
|
std::map<uint32_t, int> special_funct_freq;
|
|
|
|
uint32_t total_instructions = 0;
|
|
uint32_t addr = exe->header.load_address;
|
|
while (addr < exe->end_address()) {
|
|
auto word_opt = exe->read_word(addr);
|
|
if (!word_opt.has_value()) break;
|
|
|
|
uint32_t instr_word = *word_opt;
|
|
rabbitizer::InstructionCpu instr(instr_word, addr);
|
|
std::string mnemonic = instr.getOpcodeName();
|
|
|
|
instr_freq[mnemonic]++;
|
|
total_instructions++;
|
|
|
|
// Track opcode distribution
|
|
uint32_t opcode = (instr_word >> 26) & 0x3F;
|
|
opcode_freq[opcode]++;
|
|
|
|
// Track SPECIAL function codes
|
|
if (opcode == 0x00) {
|
|
uint32_t funct = instr_word & 0x3F;
|
|
special_funct_freq[funct]++;
|
|
}
|
|
|
|
addr += 4;
|
|
}
|
|
|
|
fmt::print("Total instructions: {}\n\n", total_instructions);
|
|
|
|
// Sort by frequency and print top 20
|
|
std::vector<std::pair<std::string, int>> sorted_freq(instr_freq.begin(), instr_freq.end());
|
|
std::sort(sorted_freq.begin(), sorted_freq.end(),
|
|
[](const auto& a, const auto& b) { return a.second > b.second; });
|
|
|
|
fmt::print("=== Top 20 Most Common Instructions ===\n\n");
|
|
fmt::print("{:4s} {:12s} {:10s} {:s}\n", "Rank", "Instruction", "Count", "Percentage");
|
|
fmt::print("-----------------------------------------------------\n");
|
|
|
|
for (size_t i = 0; i < std::min(size_t(20), sorted_freq.size()); i++) {
|
|
const auto& [instr_name, count] = sorted_freq[i];
|
|
double pct = 100.0 * count / total_instructions;
|
|
fmt::print("{:4d} {:12s} {:10d} {:5.2f}%\n", i + 1, instr_name, count, pct);
|
|
}
|
|
|
|
fmt::print("\n");
|
|
}
|
|
|
|
// Function boundary detection
|
|
fmt::print("Performing function analysis...\n");
|
|
|
|
std::vector<uint32_t> exact_entries;
|
|
exact_entries.push_back(exe->header.initial_pc);
|
|
|
|
// Explicit entry points: functions called through pointers but not
|
|
// automatically detected by the heuristic-based function analysis.
|
|
// Pass game-specific entry points via --extra-funcs file.
|
|
|
|
/* Load extra function addresses from a file (one hex address per line).
|
|
* Lines of the form `interior 0xXXXXXXXX` mark dispatch-proven interior
|
|
* addresses: alias candidates, never walk roots. `hosted_interior TARGET
|
|
* HOST` is a narrow cross-variant identity assertion: TARGET may alias only
|
|
* the explicitly rooted HOST, and only when TARGET is a syntactic block
|
|
* leader in HOST's current-byte range. The shard publisher additionally
|
|
* requires both emitted identities to match a previously CRC-validated host
|
|
* exactly before the result can reach the runtime. Lines of the form
|
|
* `dispatch_root 0xXXXXXXXX` mark classifier-proven dispatch roots
|
|
* WITHOUT a callable boundary (the kernel install-slot class, e.g. RAM
|
|
* 0xCF0): the static recompiler's install-slot hooks tail-dispatch into
|
|
* exactly these PCs, so they are real execution roots even though no
|
|
* prologue / preceding jr $ra exists. They are trusted walk roots and
|
|
* exempt from the overlay-mode boundary re-check below. Lines of the
|
|
* form `call_root 0xXXXXXXXX` carry the same mechanical trust for a
|
|
* statically proven direct or constant-register call/tail-call target.
|
|
*
|
|
* Seeds are accepted within the loaded image's own bounds — overlay mode
|
|
* wraps arbitrary regions (kernel RAM at 0x80000000, overlays at
|
|
* 0x800E7000, ...), so a hardcoded game-RAM window would silently drop
|
|
* valid seeds. */
|
|
std::vector<uint32_t> file_seeds;
|
|
std::vector<uint32_t> interior_seeds;
|
|
std::map<uint32_t, uint32_t> hosted_interior_seeds;
|
|
std::set<uint32_t> hosted_interior_hosts;
|
|
constexpr size_t kHostedInteriorTargetCap = 4096;
|
|
constexpr size_t kHostedInteriorHostCap = 512;
|
|
std::set<uint32_t> trusted_root_seeds;
|
|
std::set<uint32_t> trusted_call_root_seeds;
|
|
std::vector<std::pair<uint32_t, uint32_t>> producer_ranges;
|
|
std::set<uint32_t> cross_call_allow;
|
|
if (extra_funcs_path) {
|
|
std::ifstream ef(extra_funcs_path);
|
|
if (ef.is_open()) {
|
|
const uint32_t seed_lo = exe->header.load_address;
|
|
const uint32_t seed_hi = exe->end_address();
|
|
std::string line;
|
|
while (std::getline(ef, line)) {
|
|
if (line.empty() || line[0] == '#') continue;
|
|
if (line.rfind("producer_range", 0) == 0) {
|
|
std::istringstream in(line);
|
|
std::string tag, lo_text, hi_text;
|
|
in >> tag >> lo_text >> hi_text;
|
|
uint32_t lo = static_cast<uint32_t>(
|
|
std::strtoul(lo_text.c_str(), nullptr, 16));
|
|
uint32_t hi = static_cast<uint32_t>(
|
|
std::strtoul(hi_text.c_str(), nullptr, 16));
|
|
if (lo_text.empty() || hi_text.empty() ||
|
|
lo < seed_lo || lo >= hi || hi > seed_hi) {
|
|
fmt::print(stderr, "ERROR: invalid producer_range: {}\n", line);
|
|
return 1;
|
|
}
|
|
producer_ranges.emplace_back(lo, hi);
|
|
continue;
|
|
}
|
|
if (line.rfind("cross_call_allow", 0) == 0) {
|
|
const char* p = line.c_str() + 16;
|
|
uint32_t addr = static_cast<uint32_t>(
|
|
std::strtoul(p, nullptr, 16));
|
|
if (addr < seed_lo || addr >= seed_hi) {
|
|
fmt::print(stderr, "ERROR: invalid cross_call_allow: {}\n", line);
|
|
return 1;
|
|
}
|
|
cross_call_allow.insert(addr);
|
|
continue;
|
|
}
|
|
if (line.rfind("retained_alias", 0) == 0) {
|
|
fmt::print(" ignoring legacy retained_alias; old host "
|
|
"ranges are never reused: {}\n", line);
|
|
continue;
|
|
}
|
|
if (line.rfind("hosted_interior", 0) == 0) {
|
|
std::istringstream in(line);
|
|
std::string tag, target_text, host_text, trailing;
|
|
in >> tag >> target_text >> host_text >> trailing;
|
|
auto parse_hex_u32 = [](const std::string& text,
|
|
uint32_t& value) -> bool {
|
|
if (text.empty()) return false;
|
|
char* end = nullptr;
|
|
errno = 0;
|
|
unsigned long long parsed =
|
|
std::strtoull(text.c_str(), &end, 16);
|
|
if (errno != 0 || end == text.c_str() || *end != '\0' ||
|
|
parsed > 0xFFFFFFFFull) return false;
|
|
value = static_cast<uint32_t>(parsed);
|
|
return true;
|
|
};
|
|
uint32_t target = 0, host = 0;
|
|
if (tag != "hosted_interior" || !trailing.empty() ||
|
|
!parse_hex_u32(target_text, target) ||
|
|
!parse_hex_u32(host_text, host) ||
|
|
(target & 3u) != 0u || (host & 3u) != 0u ||
|
|
target < seed_lo || target >= seed_hi ||
|
|
host < seed_lo || host >= seed_hi || target == host) {
|
|
fmt::print(stderr, "ERROR: invalid hosted_interior: {}\n", line);
|
|
return 1;
|
|
}
|
|
auto prior = hosted_interior_seeds.find(target);
|
|
if (prior != hosted_interior_seeds.end() &&
|
|
prior->second != host) {
|
|
fmt::print(stderr, "ERROR: conflicting hosted_interior "
|
|
"hosts for 0x{:08X}\n", target);
|
|
return 1;
|
|
}
|
|
hosted_interior_seeds.emplace(target, host);
|
|
hosted_interior_hosts.insert(host);
|
|
if (hosted_interior_seeds.size() >
|
|
kHostedInteriorTargetCap ||
|
|
hosted_interior_hosts.size() >
|
|
kHostedInteriorHostCap) {
|
|
fmt::print(stderr, "ERROR: hosted_interior capacity "
|
|
"exceeded (targets {} / {}, hosts {} / {})\n",
|
|
hosted_interior_seeds.size(),
|
|
kHostedInteriorTargetCap,
|
|
hosted_interior_hosts.size(),
|
|
kHostedInteriorHostCap);
|
|
return 1;
|
|
}
|
|
continue;
|
|
}
|
|
bool interior = false;
|
|
bool trusted_root = false;
|
|
bool trusted_call_root = false;
|
|
const char* p = line.c_str();
|
|
if (line.rfind("interior", 0) == 0) {
|
|
interior = true;
|
|
p += 8;
|
|
} else if (line.rfind("dispatch_root", 0) == 0) {
|
|
trusted_root = true;
|
|
p += 13;
|
|
} else if (line.rfind("call_root", 0) == 0) {
|
|
trusted_call_root = true;
|
|
p += 9;
|
|
}
|
|
uint32_t addr = (uint32_t)std::strtoul(p, nullptr, 16);
|
|
if (addr >= seed_lo && addr < seed_hi) {
|
|
if (interior) {
|
|
interior_seeds.push_back(addr);
|
|
} else {
|
|
if (trusted_root) trusted_root_seeds.insert(addr);
|
|
if (trusted_call_root) trusted_call_root_seeds.insert(addr);
|
|
file_seeds.push_back(addr);
|
|
exact_entries.push_back(addr);
|
|
}
|
|
}
|
|
}
|
|
std::sort(producer_ranges.begin(), producer_ranges.end());
|
|
for (size_t i = 1; i < producer_ranges.size(); i++) {
|
|
if (producer_ranges[i - 1].second > producer_ranges[i].first) {
|
|
fmt::print(stderr, "ERROR: overlapping producer_range entries\n");
|
|
return 1;
|
|
}
|
|
}
|
|
for (uint32_t addr : cross_call_allow) {
|
|
bool contained = false;
|
|
for (const auto& range : producer_ranges) {
|
|
if (addr >= range.first && addr < range.second) {
|
|
contained = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!contained) {
|
|
fmt::print(stderr,
|
|
"ERROR: cross_call_allow 0x{:08X} is outside "
|
|
"producer ranges\n", addr);
|
|
return 1;
|
|
}
|
|
}
|
|
fmt::print("Loaded {} extra function addresses ({} interior, "
|
|
"{} hosted-interior, {} dispatch-root, {} call-root, {} producer ranges, "
|
|
"{} cross-call allows) from {}\n",
|
|
file_seeds.size() + interior_seeds.size() +
|
|
hosted_interior_seeds.size(),
|
|
interior_seeds.size(), hosted_interior_seeds.size(),
|
|
trusted_root_seeds.size(),
|
|
trusted_call_root_seeds.size(),
|
|
producer_ranges.size(), cross_call_allow.size(),
|
|
extra_funcs_path);
|
|
} else {
|
|
fmt::print("WARNING: Cannot open extra-funcs file: {}\n", extra_funcs_path);
|
|
}
|
|
}
|
|
|
|
PSXRecomp::FunctionAnalysisResult analysis_result;
|
|
|
|
if (overlay_mode || reachable_discovery) {
|
|
// ── Exact-entry mode: partition seeds into walk roots and interior
|
|
// alias candidates. `interior`-marked seeds (classifier-proven
|
|
// dispatch targets without a callable boundary) are NEVER walk roots —
|
|
// a root inside a host function hard-caps (truncates) it, the
|
|
// mid-function-seed softlock class. Unmarked seeds are re-verified
|
|
// against the same boundary rule as defense in depth.
|
|
const uint32_t exe_lo = exe->header.load_address;
|
|
auto read_w = [&](uint32_t a) -> uint32_t {
|
|
auto w = exe->read_word(a);
|
|
return w.has_value() ? *w : 0u;
|
|
};
|
|
auto dense_local_pointer_table = [&](uint32_t a) -> bool {
|
|
for (uint32_t i = 0; i < 3u; i++) {
|
|
auto value = exe->read_word(a + i * 4u);
|
|
if (!value.has_value() || (*value & 3u) != 0u ||
|
|
*value < exe_lo || *value >= exe->end_address()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
auto callable_boundary = [&](uint32_t a) -> bool {
|
|
uint32_t w = read_w(a);
|
|
if (!PSXRecomp::FunctionAnalyzer::is_valid_mips_word(w)) return false;
|
|
if (a == exe_lo) return true;
|
|
if (a >= exe_lo + 8 && read_w(a - 8) == 0x03E00008u) return true;
|
|
// Normal-mode discovery already skips alignment padding after a
|
|
// return when it infers a frameless function start. Preserve that
|
|
// evidence here. Requiring the next entry to be exactly jr-ra+8
|
|
// demoted legitimate Psy-Q leaf routines separated by one to six
|
|
// padding NOPs to orphan interiors, so exact-entry analysis emitted
|
|
// no function at all. The delay slot (jr+4) may be non-NOP; only
|
|
// words after it and before the proposed entry must be padding.
|
|
bool padding_only = true;
|
|
for (uint32_t back = 12; back <= 32 && a >= exe_lo + back;
|
|
back += 4) {
|
|
if (read_w(a - back + 8) != 0u) padding_only = false;
|
|
if (!padding_only) break;
|
|
if (read_w(a - back) == 0x03E00008u) return true;
|
|
}
|
|
int32_t frame = 0;
|
|
return PSXRecomp::FunctionAnalyzer::is_prologue(w, frame) &&
|
|
!(a >= exe_lo + 4 &&
|
|
PSXRecomp::FunctionAnalyzer::is_branch_or_jump(read_w(a - 4)));
|
|
};
|
|
|
|
std::set<uint32_t> roots;
|
|
std::set<uint32_t> interior;
|
|
std::set<uint32_t> hosted_targets;
|
|
for (const auto& seed : hosted_interior_seeds)
|
|
hosted_targets.insert(seed.first);
|
|
for (uint32_t a : exact_entries) {
|
|
if (hosted_targets.count(a)) {
|
|
// A fragment PS-X header needs one initial PC, but a trusted
|
|
// hosted interior must never become a walk root through it.
|
|
continue;
|
|
} else if (reachable_discovery && a == exe->header.initial_pc) {
|
|
// The PS-X EXE header is direct execution evidence for the
|
|
// main entry even when it uses a nonstandard prologue.
|
|
roots.insert(a);
|
|
} else if (trusted_root_seeds.count(a)) {
|
|
/* Classifier-proven dispatch root (install-slot class): the
|
|
* static code tail-dispatches into this PC, so it is a real
|
|
* execution root despite having no callable boundary. */
|
|
fmt::print(" seed 0x{:08X} accepted as dispatch root "
|
|
"(install-slot class, no boundary evidence)\n", a);
|
|
roots.insert(a);
|
|
} else if (trusted_call_root_seeds.count(a) &&
|
|
!dense_local_pointer_table(a)) {
|
|
fmt::print(" seed 0x{:08X} accepted as static call root "
|
|
"(direct/constant-register target)\n", a);
|
|
roots.insert(a);
|
|
} else if (callable_boundary(a)) {
|
|
roots.insert(a);
|
|
} else {
|
|
fmt::print(" seed 0x{:08X} fails the boundary check — "
|
|
"treating as interior alias candidate\n", a);
|
|
interior.insert(a);
|
|
}
|
|
}
|
|
for (uint32_t a : interior_seeds) interior.insert(a);
|
|
|
|
auto containing = [&](uint32_t a) -> const PSXRecomp::Function* {
|
|
const PSXRecomp::Function* best = nullptr;
|
|
for (const auto& f : analysis_result.functions) {
|
|
if (a >= f.start_addr && a < f.end_addr) { best = &f; break; }
|
|
}
|
|
return best;
|
|
};
|
|
|
|
// Interior candidates attach only to functions the EVIDENCE-walked
|
|
// root set already owns. No backward-boundary guessing: a false
|
|
// prologue/jr-ra in embedded data mints a root that decodes data as
|
|
// code (audit-fatal). Orphans stay with the interpreter — it
|
|
// self-heals via recapture once real call evidence appears.
|
|
{
|
|
PSXRecomp::FunctionAnalyzer analyzer(*exe);
|
|
std::vector<uint32_t> roots_vec(roots.begin(), roots.end());
|
|
analysis_result = analyzer.analyze_exact_entries(
|
|
roots_vec, producer_ranges, cross_call_allow);
|
|
}
|
|
|
|
std::vector<AliasEntry> alias_entries;
|
|
std::set<uint32_t> alias_seen;
|
|
for (uint32_t a : interior) {
|
|
const PSXRecomp::Function* host = containing(a);
|
|
if (host && a == host->start_addr) continue; // became a real entry
|
|
if (host && analysis_result.exact_reachable_pcs.count(a)) {
|
|
if (alias_seen.insert(a).second)
|
|
alias_entries.push_back({a, host->start_addr, host->end_addr});
|
|
} else {
|
|
fmt::print(" WARNING: interior seed 0x{:08X} has no host "
|
|
"function — left to the interpreter\n", a);
|
|
}
|
|
}
|
|
|
|
// Cross-variant propagation never guesses a new walk. The caller names
|
|
// a current-byte host that was already CRC-valid in the cache, and the
|
|
// recompiler requires that exact root plus a syntactic block boundary
|
|
// in the host's current-byte range. This block test alone is NOT proof
|
|
// of reachability: the publisher must compare the emitted host+alias
|
|
// CRC/range identities with the preselected cached owner before use.
|
|
PSXRecomp::ControlFlowAnalyzer hosted_cfg_analyzer(*exe);
|
|
std::map<uint32_t, PSXRecomp::ControlFlowGraph> hosted_cfgs;
|
|
for (const auto& seed : hosted_interior_seeds) {
|
|
uint32_t target = seed.first;
|
|
uint32_t requested_host = seed.second;
|
|
const PSXRecomp::Function* host = nullptr;
|
|
for (const auto& function : analysis_result.functions) {
|
|
if (function.start_addr == requested_host) {
|
|
host = &function;
|
|
break;
|
|
}
|
|
}
|
|
bool explicitly_rooted = roots.count(requested_host) != 0;
|
|
bool producer_ok = host &&
|
|
(host->producer_lo == 0 ||
|
|
(target >= host->producer_lo && target < host->producer_hi));
|
|
bool block_ok = false;
|
|
if (host && producer_ok && target > host->start_addr &&
|
|
target < host->end_addr) {
|
|
auto inserted = hosted_cfgs.emplace(
|
|
host->start_addr, PSXRecomp::ControlFlowGraph{});
|
|
if (inserted.second)
|
|
inserted.first->second = hosted_cfg_analyzer.analyze_function(*host);
|
|
block_ok = inserted.first->second.blocks.count(target) != 0;
|
|
}
|
|
if (host && explicitly_rooted && producer_ok && block_ok) {
|
|
if (alias_seen.insert(target).second)
|
|
alias_entries.push_back(
|
|
{target, host->start_addr, host->end_addr});
|
|
} else {
|
|
fmt::print(" WARNING: hosted interior 0x{:08X} rejected for "
|
|
"host 0x{:08X} (host={}, rooted={}, producer={}, block={})\n",
|
|
target, requested_host, host ? "yes" : "no",
|
|
explicitly_rooted ? "yes" : "no",
|
|
producer_ok ? "yes" : "no", block_ok ? "yes" : "no");
|
|
}
|
|
}
|
|
|
|
// Range-ownership completeness uses only analyzer-proven transfers from
|
|
// reachable instructions. The analyzer also revalidates each target's
|
|
// host against the final root partition and producer boundaries.
|
|
size_t proven_alias_added = 0;
|
|
for (const auto& absorbed : analysis_result.absorbed_entries) {
|
|
if (!alias_seen.insert(absorbed.addr).second) continue;
|
|
alias_entries.push_back(
|
|
{absorbed.addr, absorbed.host_start, absorbed.host_end});
|
|
proven_alias_added++;
|
|
}
|
|
if (proven_alias_added)
|
|
fmt::print(" +{} analyzer-proven absorbed alias entries "
|
|
"(final range ownership)\n", proven_alias_added);
|
|
|
|
materialize_alias_groups(analysis_result, alias_entries);
|
|
fmt::print("Exact-entry alias entries emitted: {}\n\n", alias_entries.size());
|
|
} else {
|
|
// ── Iterative discovery: seed classification + static data-table scan ──
|
|
//
|
|
// Every candidate entry (explicit seed or scanned data pointer) is
|
|
// classified against the current function set:
|
|
// * matches a function start → already covered
|
|
// * INSIDE a function's range → ALIAS entry (overlapping
|
|
// emission; the host is never capped/truncated — forcing such an
|
|
// entry is the mid-function-seed softlock class)
|
|
// * in a gap between functions → forced entry (explicit
|
|
// seeds are trusted; scanned pointers additionally need boundary
|
|
// evidence: a jr $ra two slots earlier or a non-delay-slot
|
|
// prologue)
|
|
//
|
|
// The data scan walks every word of the image OUTSIDE code-function
|
|
// ranges (pointer tables live in data regions) and collects words that
|
|
// point at valid instructions inside code-function ranges. Promotions
|
|
// change function boundaries, so iterate to a fixed point.
|
|
std::vector<AliasEntry> alias_entries;
|
|
|
|
std::set<uint32_t> forced;
|
|
forced.insert(exe->header.initial_pc);
|
|
const uint32_t exe_lo = exe->header.load_address;
|
|
const uint32_t exe_hi = exe->end_address();
|
|
auto read_w = [&](uint32_t a) -> uint32_t {
|
|
auto w = exe->read_word(a);
|
|
return w.has_value() ? *w : 0u;
|
|
};
|
|
|
|
const int kMaxDiscoveryIters = 6;
|
|
for (int iter = 0; iter < kMaxDiscoveryIters; ++iter) {
|
|
PSXRecomp::FunctionAnalyzer analyzer(*exe);
|
|
for (uint32_t a : forced) analyzer.add_forced_entry(a);
|
|
analysis_result = analyzer.analyze();
|
|
|
|
std::vector<const PSXRecomp::Function*> code_funcs;
|
|
for (const auto& f : analysis_result.functions) {
|
|
if (!f.is_data_section) code_funcs.push_back(&f);
|
|
}
|
|
auto containing = [&](uint32_t a) -> const PSXRecomp::Function* {
|
|
auto it = std::upper_bound(code_funcs.begin(), code_funcs.end(), a,
|
|
[](uint32_t v, const PSXRecomp::Function* f) { return v < f->start_addr; });
|
|
if (it == code_funcs.begin()) return nullptr;
|
|
--it;
|
|
return (a >= (*it)->start_addr && a < (*it)->end_addr) ? *it : nullptr;
|
|
};
|
|
|
|
struct Candidate { uint32_t addr; bool trusted; bool table_evidence; };
|
|
std::vector<Candidate> candidates;
|
|
for (uint32_t s : file_seeds) candidates.push_back({s, true, true});
|
|
for (uint32_t s : interior_seeds) candidates.push_back({s, true, true});
|
|
uint32_t scanned_words = 0;
|
|
auto is_text_ptr = [&](uint32_t v) {
|
|
return (v & 3u) == 0 && v >= exe_lo && v < exe_hi && containing(v) != nullptr;
|
|
};
|
|
for (uint32_t p = exe_lo; p + 4 <= exe_hi; p += 4) {
|
|
if (containing(p)) continue; // instruction word, not a data word
|
|
uint32_t w = read_w(p);
|
|
if ((w & 3u) || w < exe_lo || w >= exe_hi) continue;
|
|
// Table evidence: an adjacent data word that is also a text
|
|
// pointer. Interior handler entries live in dense pointer
|
|
// tables; a lone pointer-shaped data word is too weak to mint
|
|
// an alias from (random data aliases into loose code ranges).
|
|
bool table_evidence =
|
|
(p >= exe_lo + 4 && is_text_ptr(read_w(p - 4))) ||
|
|
(p + 8 <= exe_hi && is_text_ptr(read_w(p + 4)));
|
|
candidates.push_back({w, false, table_evidence});
|
|
scanned_words++;
|
|
}
|
|
|
|
alias_entries.clear();
|
|
std::set<uint32_t> alias_seen;
|
|
bool promoted_new = false;
|
|
for (const auto& [a, trusted, table_evidence] : candidates) {
|
|
if ((a & 3u) || a < exe_lo || a >= exe_hi) continue;
|
|
const PSXRecomp::Function* host = containing(a);
|
|
if (host && a == host->start_addr) continue; // already an entry
|
|
uint32_t w = read_w(a);
|
|
if (!PSXRecomp::FunctionAnalyzer::is_valid_mips_word(w)) continue;
|
|
if (host) {
|
|
if ((trusted || table_evidence) && alias_seen.insert(a).second) {
|
|
alias_entries.push_back({a, host->start_addr, host->end_addr});
|
|
}
|
|
continue;
|
|
}
|
|
bool promote = trusted;
|
|
if (!promote) {
|
|
bool after_ret = (a >= exe_lo + 8) && (read_w(a - 8) == 0x03E00008u);
|
|
int32_t frame = 0;
|
|
bool prologue =
|
|
PSXRecomp::FunctionAnalyzer::is_prologue(w, frame) &&
|
|
!(a >= exe_lo + 4 &&
|
|
PSXRecomp::FunctionAnalyzer::is_branch_or_jump(read_w(a - 4)));
|
|
promote = after_ret || prologue;
|
|
}
|
|
if (promote && forced.insert(a).second) promoted_new = true;
|
|
}
|
|
|
|
fmt::print("Discovery iteration {}: {} data pointers scanned, "
|
|
"{} forced entries, {} alias entries\n",
|
|
iter + 1, scanned_words, forced.size(), alias_entries.size());
|
|
if (!promoted_new) break;
|
|
}
|
|
|
|
materialize_alias_groups(analysis_result, alias_entries);
|
|
fmt::print("Alias entries emitted: {}\n\n", alias_entries.size());
|
|
}
|
|
|
|
// Print summary statistics
|
|
fmt::print("\n=== Function Analysis Summary ===\n\n");
|
|
fmt::print("Total Functions: {}\n", analysis_result.functions.size());
|
|
fmt::print("Code Coverage: {} KB analyzed\n",
|
|
(exe->end_address() - exe->header.load_address) / 1024);
|
|
|
|
// Count functions with prologues/epilogues
|
|
int with_prologue = 0;
|
|
int with_epilogue = 0;
|
|
int with_both = 0;
|
|
|
|
for (const auto& func : analysis_result.functions) {
|
|
if (func.has_prologue) with_prologue++;
|
|
if (func.has_epilogue) with_epilogue++;
|
|
if (func.has_prologue && func.has_epilogue) with_both++;
|
|
}
|
|
|
|
fmt::print("With Prologue: {} ({:.1f}%)\n", with_prologue,
|
|
100.0 * with_prologue / analysis_result.functions.size());
|
|
fmt::print("With Epilogue: {} ({:.1f}%)\n", with_epilogue,
|
|
100.0 * with_epilogue / analysis_result.functions.size());
|
|
fmt::print("Complete: {} ({:.1f}%)\n\n", with_both,
|
|
100.0 * with_both / analysis_result.functions.size());
|
|
|
|
// Print first 20 functions
|
|
fmt::print("First 20 Functions:\n");
|
|
fmt::print("---------------------------------------\n");
|
|
|
|
for (size_t i = 0; i < std::min(size_t(20), analysis_result.functions.size()); i++) {
|
|
const auto& func = analysis_result.functions[i];
|
|
fmt::print(" {:08X}-{:08X} {:5} bytes {}{}\n",
|
|
func.start_addr,
|
|
func.end_addr,
|
|
func.size,
|
|
func.name,
|
|
func.has_prologue ? fmt::format(" (frame={})", func.stack_frame_size) : "");
|
|
}
|
|
|
|
|
|
fmt::print("\n... ({} more functions)\n\n",
|
|
analysis_result.functions.size() - std::min(size_t(20), analysis_result.functions.size()));
|
|
|
|
|
|
// Control flow analysis
|
|
PSXRecomp::ControlFlowAnalyzer cfg_analyzer(*exe);
|
|
auto all_cfgs = cfg_analyzer.analyze_all_functions(analysis_result.functions);
|
|
|
|
// Print CFG summary statistics
|
|
fmt::print("=== Control Flow Summary ===\n\n");
|
|
|
|
int total_basic_blocks = 0;
|
|
int total_loops = 0;
|
|
int total_branches = 0;
|
|
|
|
for (const auto& [func_addr, cfg] : all_cfgs) {
|
|
total_basic_blocks += static_cast<int>(cfg.blocks.size());
|
|
total_loops += cfg.loop_count;
|
|
|
|
// Count branch instructions across all blocks
|
|
for (const auto& [block_addr, block] : cfg.blocks) {
|
|
if (block.exit_instr.type == PSXRecomp::ControlFlowType::Branch) {
|
|
total_branches++;
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt::print("Basic Blocks: {}\n", total_basic_blocks);
|
|
fmt::print("Loops Detected: {}\n", total_loops);
|
|
fmt::print("Branch Instructions: {}\n", total_branches);
|
|
fmt::print("Avg Blocks/Function: {:.1f}\n\n",
|
|
static_cast<double>(total_basic_blocks) / all_cfgs.size());
|
|
|
|
// Show detailed CFG for first function (as an example)
|
|
if (!analysis_result.functions.empty() && !all_cfgs.empty()) {
|
|
const auto& first_func = analysis_result.functions[0];
|
|
const auto& first_cfg = all_cfgs.at(first_func.start_addr);
|
|
|
|
fmt::print("Example: CFG for {} (0x{:08X}):\n", first_func.name, first_func.start_addr);
|
|
fmt::print("---------------------------------------\n");
|
|
fmt::print(" Blocks: {}\n", first_cfg.blocks.size());
|
|
fmt::print(" Loops: {}\n", first_cfg.loop_count);
|
|
|
|
if (first_cfg.blocks.size() > 1) {
|
|
fmt::print("\n Block Details:\n");
|
|
int count = 0;
|
|
for (const auto& block_addr : first_cfg.block_order) {
|
|
const auto& block = first_cfg.blocks.at(block_addr);
|
|
fmt::print(" Block 0x{:08X}: {} instructions", block.start_addr, block.instruction_count);
|
|
|
|
if (block.is_entry) fmt::print(" [ENTRY]");
|
|
if (block.is_exit) fmt::print(" [EXIT]");
|
|
if (block.is_loop_header) fmt::print(" [LOOP]");
|
|
|
|
fmt::print("\n");
|
|
fmt::print(" Successors: {}", block.successors.size());
|
|
if (!block.successors.empty()) {
|
|
fmt::print(" (");
|
|
for (size_t i = 0; i < block.successors.size(); i++) {
|
|
fmt::print("0x{:08X}", block.successors[i]);
|
|
if (i + 1 < block.successors.size()) fmt::print(", ");
|
|
}
|
|
fmt::print(")");
|
|
}
|
|
fmt::print("\n");
|
|
|
|
// Only show first 5 blocks for brevity
|
|
if (++count >= 5 && first_cfg.blocks.size() > 5) {
|
|
fmt::print(" ... ({} more blocks)\n", first_cfg.blocks.size() - 5);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
fmt::print("\n");
|
|
}
|
|
|
|
// C code generation
|
|
PSXRecomp::CodeGenConfig codegen_config;
|
|
codegen_config.emit_comments = true;
|
|
codegen_config.emit_line_numbers = true;
|
|
codegen_config.split_mid_function_targets = !overlay_mode;
|
|
codegen_config.overlay_mode = overlay_mode;
|
|
codegen_config.ws_signed_x_bound_sites = ws_signed_x_bound_sites;
|
|
codegen_config.ws_sprite_tag_funcs = ws_tag_funcs;
|
|
codegen_config.data_shard_funcs = ds_funcs;
|
|
codegen_config.hot_funcs = hot_funcs;
|
|
codegen_config.vsync_query_hle_funcs = vsync_query_hle_funcs;
|
|
codegen_config.ws_bg2d_init_func = ws_bg2d_init_func;
|
|
codegen_config.ws_cull_bias_sites = ws_cull_bias;
|
|
codegen_config.ws_cull_range_sites = ws_cull_range;
|
|
codegen_config.ws_cull_a1_sites = ws_cull_a1;
|
|
codegen_config.ws_cull_screen_x_sites = ws_cull_screen_x;
|
|
codegen_config.ws_cull_slti_sites = ws_cull_slti;
|
|
codegen_config.ws_cull_negsub_sites = ws_cull_negsub;
|
|
codegen_config.ws_cull_vxrange_sites = ws_cull_vxrange;
|
|
codegen_config.ws_cull_depth_sites = ws_cull_depth;
|
|
codegen_config.ws_cull_plane_nx_sites = ws_cull_plane_nx;
|
|
codegen_config.ws_cull_xclip_load_sites = ws_cull_xclip_load;
|
|
codegen_config.ws_cull_w_imms = ws_cull_w_imms;
|
|
codegen_config.ws_cull_h_imms = ws_cull_h_imms;
|
|
codegen_config.ws_backdrop_x_sites = ws_backdrop_x;
|
|
codegen_config.ws_backdrop_unsquash_funcs = ws_backdrop_unsquash;
|
|
codegen_config.ws_auto_screen_x_cull = ws_auto_screen_x_cull;
|
|
codegen_config.persist_init_store_sites = persist_init_sites;
|
|
codegen_config.ws_auto_backdrop_preload = ws_auto_backdrop_preload;
|
|
codegen_config.ws_bg2d_count_site = ws_bg2d_count_site;
|
|
codegen_config.ws_bg2d_startcol_site = ws_bg2d_startcol_site;
|
|
codegen_config.ws_bg2d_startx_site = ws_bg2d_startx_site;
|
|
codegen_config.ws_bg2d_stream_left_site = ws_bg2d_stream_left_site;
|
|
codegen_config.ws_bg2d_stream_right_site = ws_bg2d_stream_right_site;
|
|
codegen_config.ws_bg2d_bufbase_site = ws_bg2d_bufbase_site;
|
|
codegen_config.ws_bg2d_cap_site = ws_bg2d_cap_site;
|
|
if (ws_bg2d_count_site || ws_bg2d_startcol_site || ws_bg2d_startx_site)
|
|
fmt::print(" ws_bg2d 2D-background widen = ON (count=0x{:08X} startcol=0x{:08X} startx=0x{:08X})\n",
|
|
ws_bg2d_count_site, ws_bg2d_startcol_site, ws_bg2d_startx_site);
|
|
if (ws_auto_screen_x_cull)
|
|
fmt::print(" ws_auto_screen_x_cull = ON (render-funnel FOV widening)\n");
|
|
if (!persist_init_sites.empty())
|
|
fmt::print(" persist_options = {} init-store hook(s)\n", persist_init_sites.size());
|
|
if (ws_auto_backdrop_preload)
|
|
fmt::print(" ws_auto_backdrop_preload = ON (far-backdrop column preload)\n");
|
|
|
|
// Load per-game annotations: annotations/<exe_stem>_annotations.csv
|
|
// Silently skipped if the file doesn't exist.
|
|
PSXRecomp::AnnotationTable annotations;
|
|
{
|
|
std::string stem = exe_path.filename().string();
|
|
std::string ann_path = "annotations/" + stem + "_annotations.csv";
|
|
if (annotations.load(ann_path.c_str()))
|
|
fmt::print("✓ Loaded {} annotations from {}\n\n", annotations.count(), ann_path);
|
|
else
|
|
fmt::print(" (No annotations file found at {})\n\n", ann_path);
|
|
}
|
|
|
|
PSXRecomp::CodeGenerator codegen(*exe, codegen_config);
|
|
codegen.set_annotations(&annotations);
|
|
|
|
// Generate code for first 5 functions (as examples)
|
|
fmt::print("=== C Code Generation Examples ===\n\n");
|
|
fmt::print("Generating code for first 5 functions...\n\n");
|
|
|
|
std::vector<PSXRecomp::Function> sample_funcs;
|
|
for (size_t i = 0; i < std::min(size_t(5), analysis_result.functions.size()); i++) {
|
|
sample_funcs.push_back(analysis_result.functions[i]);
|
|
}
|
|
|
|
auto generated = codegen.generate_all_functions(sample_funcs, all_cfgs);
|
|
|
|
// Print first generated function as example
|
|
if (!generated.empty()) {
|
|
fmt::print("Example Generated Function:\n");
|
|
fmt::print("---------------------------------------\n");
|
|
fmt::print("{}\n", generated[0].full_code);
|
|
fmt::print("---------------------------------------\n\n");
|
|
}
|
|
|
|
// Generate full C file and save to the generated directory
|
|
// Use argv[2] as output path if provided, otherwise derive from input filename
|
|
// Use filename() not stem() because ".36" in SCUS_942.36 is part of the serial, not an extension
|
|
std::string exe_stem = exe_path.filename().string();
|
|
std::filesystem::create_directories(out_dir);
|
|
std::filesystem::path output_filename = out_dir / (exe_stem + "_full.c");
|
|
fmt::print("Generating complete C file: {}\n", output_filename.string());
|
|
|
|
std::string full_c_code = codegen.generate_file(analysis_result.functions, all_cfgs);
|
|
std::set<uint32_t> dispatch_addrs;
|
|
{
|
|
const std::string marker = "void func_";
|
|
size_t pos = 0;
|
|
while ((pos = full_c_code.find(marker, pos)) != std::string::npos) {
|
|
size_t hex_pos = pos + marker.size();
|
|
if (hex_pos + 8 <= full_c_code.size() &&
|
|
full_c_code.compare(hex_pos + 8, 14, "(CPUState* cpu") == 0) {
|
|
std::string hex = full_c_code.substr(hex_pos, 8);
|
|
dispatch_addrs.insert((uint32_t)std::strtoul(hex.c_str(), nullptr, 16));
|
|
}
|
|
pos = hex_pos + 8;
|
|
}
|
|
}
|
|
if (dispatch_addrs.empty()) {
|
|
for (const auto& func : analysis_result.functions) {
|
|
dispatch_addrs.insert(func.start_addr);
|
|
}
|
|
}
|
|
|
|
// Split-TU output: instead of one monolithic <exe_stem>_full.c, write a
|
|
// shared declarations header plus N shards bucketed by cumulative line
|
|
// count, so the game build can compile the shards in parallel. The
|
|
// in-memory full_c_code / dispatch_addrs scan above is unaffected — only
|
|
// the on-disk representation changes. Function bodies are byte-identical
|
|
// to what full_c_code would have contained; see
|
|
// CodeGenerator::last_gen_funcs() / build_shared_decls_header().
|
|
if (overlay_mode) {
|
|
// Overlays are single small TUs consumed by compile_overlays.py, which
|
|
// reads <stem>_full.c directly. Emit the monolith; the split path below
|
|
// would delete _full.c and leave only shards, breaking overlay compile
|
|
// (no_output). Splitting a small overlay TU has no parallel-compile
|
|
// benefit anyway.
|
|
std::ofstream full_file(output_filename);
|
|
if (full_file.is_open()) {
|
|
full_file << full_c_code;
|
|
full_file.close();
|
|
fmt::print("✓ Saved overlay monolith to {}\n", output_filename.string());
|
|
} else {
|
|
fmt::print(stderr, "⚠ Failed to write overlay monolith {}\n\n",
|
|
output_filename.string());
|
|
}
|
|
} else {
|
|
// 1. Remove stale outputs: the old monolith and any previously
|
|
// written shards (a shard count shrink must not leave orphans).
|
|
std::error_code rm_ec;
|
|
std::filesystem::remove(output_filename, rm_ec);
|
|
for (int stale = 0; stale < 256; stale++) {
|
|
std::filesystem::path stale_shard =
|
|
out_dir / fmt::format("{}_full_{:02d}.c", exe_stem, stale);
|
|
std::filesystem::remove(stale_shard, rm_ec);
|
|
}
|
|
|
|
// 2. Shared declarations header.
|
|
std::filesystem::path decls_filename = out_dir / (exe_stem + "_decls.h");
|
|
{
|
|
std::ofstream decls_file(decls_filename);
|
|
if (decls_file.is_open()) {
|
|
decls_file << codegen.build_shared_decls_header(codegen.last_gen_funcs());
|
|
decls_file.close();
|
|
fmt::print("✓ Saved shared decls header to {}\n", decls_filename.string());
|
|
} else {
|
|
fmt::print(stderr, "⚠ Failed to write shared decls header\n\n");
|
|
}
|
|
}
|
|
|
|
// 3. Bucket generated functions into shards by cumulative line count.
|
|
const int SHARD_LINE_BUDGET = 40000;
|
|
const std::vector<PSXRecomp::GeneratedFunction>& gen_funcs_for_shards = codegen.last_gen_funcs();
|
|
std::string decls_basename = exe_stem + "_decls.h";
|
|
|
|
int shard_index = 0;
|
|
int shard_lines = 0;
|
|
int total_shard_lines = 0;
|
|
std::stringstream shard_buf;
|
|
auto flush_shard = [&]() {
|
|
if (shard_buf.tellp() == std::streampos(0)) return;
|
|
std::filesystem::path shard_filename =
|
|
out_dir / fmt::format("{}_full_{:02d}.c", exe_stem, shard_index);
|
|
std::ofstream shard_file(shard_filename);
|
|
if (shard_file.is_open()) {
|
|
shard_file << fmt::format(
|
|
"/* Generated by PSXRecomp - full.c shard {}. DO NOT EDIT. */\n",
|
|
shard_index);
|
|
shard_file << fmt::format("#include \"{}\"\n\n", decls_basename);
|
|
shard_file << shard_buf.str();
|
|
shard_file.close();
|
|
fmt::print("✓ Saved shard {} ({} lines) to {}\n",
|
|
shard_index, shard_lines, shard_filename.string());
|
|
} else {
|
|
fmt::print(stderr, "⚠ Failed to write shard {}\n\n", shard_index);
|
|
}
|
|
total_shard_lines += shard_lines;
|
|
shard_index++;
|
|
shard_lines = 0;
|
|
shard_buf.str(std::string());
|
|
shard_buf.clear();
|
|
};
|
|
|
|
for (const auto& gf : gen_funcs_for_shards) {
|
|
if (shard_lines > 0 && shard_lines + gf.line_count > SHARD_LINE_BUDGET) {
|
|
flush_shard();
|
|
}
|
|
shard_buf << gf.full_code << "\n";
|
|
shard_lines += gf.line_count;
|
|
}
|
|
flush_shard();
|
|
|
|
fmt::print("✓ Wrote {} shards ({} total lines) for {}\n\n",
|
|
shard_index, total_shard_lines, exe_stem);
|
|
}
|
|
|
|
// Per-function code-range manifest (design §8): consumed by the overlay
|
|
// loader's per-entry validity hash. Emitted alongside _full.c for every
|
|
// build; only the overlay path actually loads it.
|
|
const std::string ranges_manifest = codegen.last_ranges_manifest().empty()
|
|
? codegen.generate_ranges_manifest(analysis_result.functions, all_cfgs)
|
|
: codegen.last_ranges_manifest();
|
|
{
|
|
std::filesystem::path ranges_filename = out_dir / (exe_stem + "_full.ranges");
|
|
std::ofstream rf(ranges_filename);
|
|
if (rf.is_open()) {
|
|
rf << ranges_manifest;
|
|
rf.close();
|
|
fmt::print("✓ Saved code-range manifest to {}\n\n",
|
|
ranges_filename.string());
|
|
}
|
|
}
|
|
|
|
// Generate dispatch table (tomba_dispatch.c)
|
|
// This maps PS1 addresses to compiled C functions so call_by_address() can
|
|
// dispatch dynamic jalr/jr calls to the right compiled function.
|
|
{
|
|
std::filesystem::path dispatch_filename = out_dir / (exe_stem + "_dispatch.c");
|
|
fmt::print("Generating dispatch table: {}\n", dispatch_filename.string());
|
|
|
|
std::ostringstream ds;
|
|
ds << "/* Generated by PSXRecomp - dynamic dispatch table */\n";
|
|
ds << "#include \"psx_runtime.h\"\n\n";
|
|
ds << "extern void psx_check_interrupts_dispatch_entry(CPUState* cpu, uint32_t resume_pc);\n\n";
|
|
ds << "extern int dirty_ram_text_native_ok_ranges_from(const uint32_t* lo_len_pairs, uint32_t count, uint32_t exec_pc);\n\n";
|
|
|
|
// Forward declarations
|
|
ds << "/* Forward declarations for all recompiled functions */\n";
|
|
for (uint32_t addr : dispatch_addrs) {
|
|
ds << fmt::format("extern void func_{:08X}(CPUState* cpu);\n", addr);
|
|
}
|
|
ds << "\n";
|
|
|
|
// Dispatch function
|
|
uint32_t game_text_start = exe->load_address() & 0x1FFFFFFFu;
|
|
uint32_t game_text_end = game_text_start + exe->code_size();
|
|
ds << "int psx_game_address_in_text(uint32_t addr) {\n";
|
|
ds << " uint32_t phys = addr & 0x1FFFFFFFu;\n";
|
|
ds << fmt::format(" return phys >= 0x{:08X}u && phys < 0x{:08X}u;\n",
|
|
game_text_start, game_text_end);
|
|
ds << "}\n\n";
|
|
|
|
// Materialize one sorted data table for dispatch and entry probes. A
|
|
// giant sparse switch is catastrophically slow in Debug/-O0 builds:
|
|
// GCC lowers X4's roughly 60K cases to a multi-megabyte linear compare
|
|
// chain. Binary search stays O(log N) at every optimizer level and
|
|
// avoids emitting the same case set twice.
|
|
struct DispatchRecord {
|
|
uint32_t addr;
|
|
uint32_t resume;
|
|
uint32_t owner;
|
|
uint32_t range_index;
|
|
uint32_t range_count;
|
|
};
|
|
std::vector<DispatchRecord> records;
|
|
records.reserve(dispatch_addrs.size() +
|
|
(codegen.cps_enabled() ? codegen.cps_continuations().size() : 0));
|
|
for (uint32_t addr : dispatch_addrs)
|
|
records.push_back({addr, 0, addr, 0, 0});
|
|
if (codegen.cps_enabled()) {
|
|
const auto& conts = codegen.cps_continuations();
|
|
for (const auto& [cont, owner] : conts) {
|
|
if (dispatch_addrs.count(cont)) continue;
|
|
records.push_back({cont, cont, owner, 0, 0});
|
|
}
|
|
}
|
|
std::sort(records.begin(), records.end(),
|
|
[](const DispatchRecord& a, const DispatchRecord& b) {
|
|
return a.addr < b.addr;
|
|
});
|
|
|
|
// Attach the exact CFG instruction ranges from the manifest to every
|
|
// dispatch owner. Mid-function alias wrappers have no separate F record,
|
|
// so resolve them to the containing host range. Mutable data and
|
|
// jump-table gaps sharing a page remain deliberately excluded.
|
|
using CodeRange = std::pair<uint32_t, uint32_t>; // virtual lo, byte len
|
|
std::map<uint32_t, std::vector<CodeRange>> function_ranges;
|
|
{
|
|
std::istringstream input(ranges_manifest);
|
|
std::string line;
|
|
uint32_t current_owner = 0;
|
|
while (std::getline(input, line)) {
|
|
std::istringstream fields(line);
|
|
char kind = 0;
|
|
fields >> kind;
|
|
if (kind == 'F') {
|
|
fields >> std::hex >> current_owner;
|
|
function_ranges[current_owner];
|
|
} else if (kind == 'R' && current_owner != 0) {
|
|
uint32_t lo = 0, len = 0;
|
|
fields >> std::hex >> lo >> len;
|
|
if (len != 0)
|
|
function_ranges[current_owner].push_back({lo, len});
|
|
}
|
|
}
|
|
}
|
|
std::vector<CodeRange> flat_ranges;
|
|
std::map<uint32_t, std::pair<uint32_t, uint32_t>> owner_spans;
|
|
auto resolve_owner_ranges = [&](uint32_t owner)
|
|
-> const std::vector<CodeRange>* {
|
|
auto exact = function_ranges.find(owner);
|
|
if (exact != function_ranges.end() && !exact->second.empty())
|
|
return &exact->second;
|
|
for (const auto& [entry, ranges] : function_ranges) {
|
|
(void)entry;
|
|
for (const auto& [lo, len] : ranges) {
|
|
const uint64_t hi = (uint64_t)lo + len;
|
|
if (owner >= lo && (uint64_t)owner < hi) return &ranges;
|
|
}
|
|
}
|
|
return nullptr;
|
|
};
|
|
for (auto& rec : records) {
|
|
auto cached = owner_spans.find(rec.owner);
|
|
if (cached == owner_spans.end()) {
|
|
const uint32_t first = (uint32_t)flat_ranges.size();
|
|
const auto* ranges = resolve_owner_ranges(rec.owner);
|
|
if (ranges)
|
|
flat_ranges.insert(flat_ranges.end(), ranges->begin(), ranges->end());
|
|
const uint32_t count = (uint32_t)flat_ranges.size() - first;
|
|
cached = owner_spans.emplace(rec.owner,
|
|
std::make_pair(first, count)).first;
|
|
}
|
|
rec.range_index = cached->second.first;
|
|
rec.range_count = cached->second.second;
|
|
}
|
|
|
|
ds << "typedef struct { uint32_t lo; uint32_t len; } PsxGameCodeRange;\n";
|
|
ds << "static const PsxGameCodeRange k_psx_game_code_ranges[] = {\n";
|
|
if (flat_ranges.empty()) ds << " {0u, 0u},\n";
|
|
for (const auto& [lo, len] : flat_ranges)
|
|
ds << fmt::format(" {{0x{:08X}u, 0x{:X}u}},\n", lo, len);
|
|
ds << "};\n\n";
|
|
|
|
ds << "typedef void (*PsxGameDispatchFn)(CPUState* cpu);\n";
|
|
ds << "typedef struct {\n";
|
|
ds << " uint32_t addr;\n";
|
|
ds << " uint32_t resume_pc;\n";
|
|
ds << " uint32_t range_index;\n";
|
|
ds << " uint32_t range_count;\n";
|
|
ds << " PsxGameDispatchFn fn;\n";
|
|
ds << "} PsxGameDispatchEntry;\n\n";
|
|
ds << "static const PsxGameDispatchEntry k_psx_game_dispatch[] = {\n";
|
|
for (const auto& rec : records) {
|
|
ds << fmt::format(" {{0x{:08X}u, 0x{:08X}u, {}u, {}u, func_{:08X}}},\n",
|
|
rec.addr, rec.resume, rec.range_index,
|
|
rec.range_count, rec.owner);
|
|
}
|
|
ds << "};\n";
|
|
ds << fmt::format("#define PSX_GAME_DISPATCH_COUNT {}u\n\n", records.size());
|
|
ds << "static const PsxGameDispatchEntry* psx_game_find_entry(uint32_t addr) {\n";
|
|
ds << " uint32_t lo = 0, hi = PSX_GAME_DISPATCH_COUNT;\n";
|
|
ds << " while (lo < hi) {\n";
|
|
ds << " uint32_t mid = lo + (hi - lo) / 2;\n";
|
|
ds << " uint32_t key = k_psx_game_dispatch[mid].addr;\n";
|
|
ds << " if (addr < key) hi = mid;\n";
|
|
ds << " else if (addr > key) lo = mid + 1;\n";
|
|
ds << " else return &k_psx_game_dispatch[mid];\n";
|
|
ds << " }\n";
|
|
ds << " return 0;\n";
|
|
ds << "}\n\n";
|
|
|
|
ds << "/* Exact static-code validity for this entry's emitted CFG ranges. */\n";
|
|
ds << "int psx_game_text_native_ok(uint32_t addr) {\n";
|
|
ds << " const PsxGameDispatchEntry* entry = psx_game_find_entry(addr);\n";
|
|
ds << " if (!entry || entry->range_count == 0) return 0;\n";
|
|
ds << " return dirty_ram_text_native_ok_ranges_from(\n";
|
|
ds << " &k_psx_game_code_ranges[entry->range_index].lo, entry->range_count, addr);\n";
|
|
ds << "}\n\n";
|
|
|
|
ds << "/* Maps PS1 address to compiled game code. Returns 1 if dispatched, 0 if unknown. */\n";
|
|
ds << "int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr) {\n";
|
|
ds << " const PsxGameDispatchEntry* entry = psx_game_find_entry(addr);\n";
|
|
ds << " if (!entry) return 0;\n";
|
|
ds << " if (entry->range_count == 0 || !dirty_ram_text_native_ok_ranges_from(\n";
|
|
ds << " &k_psx_game_code_ranges[entry->range_index].lo, entry->range_count, addr)) return 0;\n";
|
|
ds << " psx_check_interrupts_dispatch_entry(cpu, addr);\n";
|
|
if (codegen.cps_enabled())
|
|
ds << " cpu->pc = entry->resume_pc;\n";
|
|
ds << " /* load_accel: cycle-faithful VSync(-1) query (config-gated). */\n";
|
|
ds << " {\n";
|
|
ds << " extern int psx_vsync_query_hle_try(CPUState* cpu, uint32_t addr);\n";
|
|
ds << " if (psx_vsync_query_hle_try(cpu, addr)) return 1;\n";
|
|
ds << " }\n";
|
|
ds << " entry->fn(cpu);\n";
|
|
ds << " return 1;\n";
|
|
ds << "}\n";
|
|
|
|
// Non-destructive companion to psx_dispatch_game_compiled: is `addr` a PC
|
|
// the top-level dispatch can re-enter (a compiled function entry, or a CPS
|
|
// continuation resume-point)? The precise event slicer (dirty_ram_interp.c)
|
|
// needs this to know when it may hand a resume PC back to the dispatcher:
|
|
// a MID-function clean-text PC is NOT re-enterable (the switch falls to
|
|
// `default: return 0` and the dirty path returns 0 -> top-level PC=0 exit),
|
|
// so the slicer must keep interpreting until cpu->pc lands on a true entry.
|
|
// Same precise address set as dispatch, without executing the entry.
|
|
ds << "\n/* 1 iff addr is a re-enterable compiled entry/continuation (no exec). */\n";
|
|
ds << "int psx_game_is_function_entry(uint32_t addr) {\n";
|
|
ds << " return psx_game_find_entry(addr) != 0;\n";
|
|
ds << "}\n";
|
|
|
|
if (codegen.cps_enabled()) {
|
|
// RECURSION_BUG.md §25 — mark CPS mode at startup so runtime code that
|
|
// routes CPS continuations (overlay_loader.c) sees the contract.
|
|
// Static ctor: no clash with the BIOS dispatch's marker.
|
|
ds << "\n/* CPS runtime-mode marker (the overlay loader reads g_psx_cps_mode). */\n";
|
|
ds << "static void psx_cps_mark_game(void) {\n";
|
|
ds << " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n";
|
|
ds << "}\n";
|
|
// Run psx_cps_mark_game before main(). __attribute__((constructor)) is
|
|
// GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU.
|
|
ds << "#if defined(_MSC_VER)\n";
|
|
ds << "#pragma section(\".CRT$XCU\", read)\n";
|
|
ds << "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_game_ctor)(void) = psx_cps_mark_game;\n";
|
|
ds << "#else\n";
|
|
ds << "__attribute__((constructor)) static void psx_cps_mark_game_ctor(void) { psx_cps_mark_game(); }\n";
|
|
ds << "#endif\n";
|
|
}
|
|
|
|
std::ofstream dispatch_file(dispatch_filename);
|
|
if (dispatch_file.is_open()) {
|
|
dispatch_file << ds.str();
|
|
dispatch_file.close();
|
|
fmt::print("✓ Dispatch table written ({} entries)\n\n",
|
|
dispatch_addrs.size());
|
|
} else {
|
|
fmt::print(stderr, "⚠ Failed to write dispatch file\n\n");
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|