mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
6f37a0edee
(cherry picked from commit b0f7b2be8e)
1898 lines
99 KiB
C++
1898 lines
99 KiB
C++
#include <cstdio>
|
|
#include <exception>
|
|
#include <cerrno>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <initializer_list>
|
|
#include <map>
|
|
#include <memory>
|
|
#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 "game_dispatch_emitter.h"
|
|
#include "segment_variants.h"
|
|
#include "annotations.hpp"
|
|
#include "config_loader.h"
|
|
#include "rabbitizer.hpp"
|
|
#include "fmt/format.h"
|
|
#include "write_if_changed.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
|
|
|
|
static int psxrecomp_game_main(int argc, char** argv);
|
|
|
|
// Configuration and generation errors are reported by throwing: a malformed
|
|
// game.toml (a bad hex address, an out-of-range value, a wrong TOML type),
|
|
// or an input the emitter must refuse (an overlay whose mandatory delay slot
|
|
// lies outside the image). Left uncaught, the exception terminates through
|
|
// abort(): the process dies by SIGABRT and macOS files a crash report instead
|
|
// of the caller seeing a diagnostic. Every caller already treats any non-zero
|
|
// status as failure, so print the message and exit with status 1.
|
|
int main(int argc, char** argv) {
|
|
try {
|
|
return psxrecomp_game_main(argc, argv);
|
|
} catch (const std::exception& e) {
|
|
std::fflush(stdout);
|
|
fmt::print(stderr, "ERROR: {}\n", e.what());
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
static int psxrecomp_game_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]);
|
|
fmt::print("\n");
|
|
fmt::print(" --project-root <dir> Resolve the BIOS profile (bios/SCPH1001.toml, or\n");
|
|
fmt::print(" <framework>/bios/SCPH1001.toml one level down) against\n");
|
|
fmt::print(" <dir> instead of the CWD. Required whenever the caller\n");
|
|
fmt::print(" cannot choose its own CWD — e.g. compile_overlays.py,\n");
|
|
fmt::print(" which runs us with cwd = dirname(game.toml).\n");
|
|
};
|
|
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;
|
|
}
|
|
if (std::string(argv[i]) == "--overlay-config-hash" && i + 1 < argc) {
|
|
try {
|
|
const auto cfg = PSXRecompV4::load_game_config(argv[i + 1]);
|
|
fmt::print("{:08x}\n",
|
|
(unsigned)PSXRecompV4::overlay_codegen_config_hash(cfg));
|
|
return 0;
|
|
} catch (const std::exception& e) {
|
|
fmt::print(stderr, "overlay config hash failed: {}\n", e.what());
|
|
return 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
// --project-root <dir>: the directory to resolve the BIOS profile against
|
|
// when no explicit [recompiler] bios_config is in play. Without it the
|
|
// probe below uses the process CWD, which is wrong for every caller that
|
|
// cannot choose its own CWD — notably compile_overlays.py, which spawns us
|
|
// with cwd = dirname(game.toml). For a PACKAGED config (packaging/release/
|
|
// game.toml) that directory holds neither bios/SCPH1001.toml nor a vendored
|
|
// framework, so every shard died with "no BIOS profile found" and the whole
|
|
// overlay cache silently failed to build (issue #72).
|
|
std::filesystem::path project_root;
|
|
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;
|
|
}
|
|
}
|
|
for (int i = 1; i < argc; ++i) {
|
|
std::string a = argv[i];
|
|
if (a == "--project-root" && i + 1 < argc) { project_root = argv[i + 1]; break; }
|
|
if (a.rfind("--project-root=", 0) == 0) {
|
|
project_root = a.substr(std::string("--project-root=").size());
|
|
break;
|
|
}
|
|
}
|
|
if (!project_root.empty()) {
|
|
std::error_code prec;
|
|
if (!std::filesystem::is_directory(project_root, prec)) {
|
|
fmt::print(stderr,
|
|
"psxrecomp-game: FATAL: --project-root '{}' is not a directory\n",
|
|
project_root.string());
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
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> mod_instruction_sites;
|
|
std::vector<PSXRecompV4::ModImmediateSite> mod_immediate_sites; // [[mod_immediate.sites]]
|
|
std::set<uint32_t> mod_entry_funcs; // trusted game-mod entry hooks
|
|
std::vector<PSXRecompV4::WidescreenMaskedRejectSite> ws_cull_masked_reject;
|
|
std::set<uint32_t> hot_funcs; // [recompiler] hot_funcs
|
|
std::set<uint32_t> load_charge_batch_funcs; // [recompiler] load_charge_batch*
|
|
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_bias_lower;
|
|
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_slti_lower; // [widescreen.cull] slti_lower_sites
|
|
std::set<uint32_t> ws_cull_bltz; // [widescreen.cull] bltz_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> netplay_local_viewport_width_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::set<uint32_t> ws_cull_nclip_keep; // [widescreen.cull] nclip_keep_sites
|
|
std::set<uint32_t> ws_cull_nclip_exact; // [widescreen.cull] nclip_exact_sites
|
|
std::set<uint32_t> ws_cull_branch_keep; // [widescreen.cull] branch_keep_sites
|
|
std::set<uint32_t> ws_cull_bgez; // [widescreen.cull] bgez_sites
|
|
std::set<uint32_t> ws_cull_clip_edge_x_load; // [widescreen.cull] clip_edge_x_load_sites
|
|
uint32_t ws_cull_clip_edge_width = 0; // [widescreen.cull] clip_edge_width
|
|
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
|
|
std::vector<PSXRecompV4::WidescreenSxyCullSite> ws_cull_sxy;
|
|
std::vector<PSXRecompV4::WidescreenPackedXSite> ws_cull_packed_x;
|
|
std::vector<PSXRecompV4::WidescreenCullScaleSite> ws_cull_scale;
|
|
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle;
|
|
std::vector<PSXRecompV4::DrawDistanceClampSite> draw_distance_clamps; // [[draw_distance.clamp]]
|
|
PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone;
|
|
int ws_cull_activation_guard_pixels = 0;
|
|
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)
|
|
// [load_accel.vsync_query] event_horizon[_extra]_sites: return PCs the
|
|
// runtime compares exactly with $ra (load_accel.c). Not emitted; read here
|
|
// only for the segment check below.
|
|
std::vector<uint32_t> vsync_horizon_sites, vsync_horizon_extra_sites;
|
|
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());
|
|
mod_entry_funcs.insert(cfg.mod_function_entry_funcs.begin(),
|
|
cfg.mod_function_entry_funcs.end());
|
|
mod_instruction_sites.insert(cfg.mod_instruction_sites.begin(), cfg.mod_instruction_sites.end());
|
|
mod_immediate_sites = cfg.mod_immediate_sites;
|
|
hot_funcs.insert(cfg.hot_funcs.begin(), cfg.hot_funcs.end());
|
|
if (cfg.load_charge_batch) {
|
|
load_charge_batch_funcs.insert(cfg.load_charge_batch_funcs.begin(),
|
|
cfg.load_charge_batch_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_bias_lower.insert(cfg.ws_cull_bias_lower_sites.begin(), cfg.ws_cull_bias_lower_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_slti_lower.insert(cfg.ws_cull_slti_lower_sites.begin(),
|
|
cfg.ws_cull_slti_lower_sites.end());
|
|
ws_cull_bltz.insert(cfg.ws_cull_bltz_sites.begin(), cfg.ws_cull_bltz_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());
|
|
netplay_local_viewport_width_sites.insert(cfg.netplay_local_viewport_width_sites.begin(), cfg.netplay_local_viewport_width_sites.end());
|
|
ws_cull_xclip_load.insert(cfg.ws_cull_xclip_load_sites.begin(), cfg.ws_cull_xclip_load_sites.end());
|
|
ws_cull_nclip_keep.insert(cfg.ws_cull_nclip_keep_sites.begin(), cfg.ws_cull_nclip_keep_sites.end());
|
|
ws_cull_nclip_exact.insert(cfg.ws_cull_nclip_exact_sites.begin(), cfg.ws_cull_nclip_exact_sites.end());
|
|
ws_cull_branch_keep.insert(cfg.ws_cull_branch_keep_sites.begin(), cfg.ws_cull_branch_keep_sites.end());
|
|
ws_cull_bgez.insert(cfg.ws_cull_bgez_sites.begin(), cfg.ws_cull_bgez_sites.end());
|
|
ws_cull_clip_edge_x_load.insert(cfg.ws_cull_clip_edge_x_load_sites.begin(),
|
|
cfg.ws_cull_clip_edge_x_load_sites.end());
|
|
if (!cfg.ws_cull_clip_edge_x_load_sites.empty())
|
|
ws_cull_clip_edge_width = PSXRecompV4::ws_cull_clip_edge_width(cfg);
|
|
ws_cull_keep = cfg.ws_cull_keep_sites;
|
|
ws_cull_sxy = cfg.ws_cull_sxy_sites;
|
|
ws_cull_packed_x = cfg.ws_cull_packed_x_sites;
|
|
ws_cull_masked_reject = cfg.ws_cull_masked_reject_sites;
|
|
ws_cull_scale = cfg.ws_cull_scale_sites;
|
|
ws_cull_angle = cfg.ws_cull_angle_sites;
|
|
draw_distance_clamps = cfg.draw_distance_clamp_sites;
|
|
ws_aspect_cone = cfg.ws_aspect_cone;
|
|
ws_cull_activation_guard_pixels =
|
|
cfg.ws_cull_activation_guard_pixels;
|
|
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;
|
|
vsync_horizon_sites = cfg.vsync_event_horizon_sites;
|
|
vsync_horizon_extra_sites = cfg.vsync_event_horizon_extra_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 == "--project-root" && i + 1 < argc) {
|
|
/* Already parsed in the pre-scan above (the BIOS-profile probe
|
|
* needs it before this loop runs). Consume the value here so it
|
|
* is never mistaken for another flag's argument. */
|
|
++i;
|
|
} 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());
|
|
mod_entry_funcs.insert(wscfg.mod_function_entry_funcs.begin(),
|
|
wscfg.mod_function_entry_funcs.end());
|
|
mod_instruction_sites.insert(wscfg.mod_instruction_sites.begin(), wscfg.mod_instruction_sites.end());
|
|
// Same TOML as --config adds nothing; a distinct one adds its own sites.
|
|
for (const auto& site : wscfg.mod_immediate_sites) {
|
|
const bool known = std::any_of(mod_immediate_sites.begin(), mod_immediate_sites.end(),
|
|
[&](const PSXRecompV4::ModImmediateSite& have) {
|
|
return (have.address & 0x1FFFFFFFu) == (site.address & 0x1FFFFFFFu) &&
|
|
have.expected == site.expected;
|
|
});
|
|
if (!known) mod_immediate_sites.push_back(site);
|
|
}
|
|
ws_cull_bias.insert(wscfg.ws_cull_bias_sites.begin(), wscfg.ws_cull_bias_sites.end());
|
|
ws_cull_bias_lower.insert(wscfg.ws_cull_bias_lower_sites.begin(), wscfg.ws_cull_bias_lower_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_slti_lower.insert(wscfg.ws_cull_slti_lower_sites.begin(),
|
|
wscfg.ws_cull_slti_lower_sites.end());
|
|
ws_cull_bltz.insert(wscfg.ws_cull_bltz_sites.begin(), wscfg.ws_cull_bltz_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());
|
|
netplay_local_viewport_width_sites.insert(wscfg.netplay_local_viewport_width_sites.begin(), wscfg.netplay_local_viewport_width_sites.end());
|
|
ws_cull_xclip_load.insert(wscfg.ws_cull_xclip_load_sites.begin(), wscfg.ws_cull_xclip_load_sites.end());
|
|
ws_cull_nclip_keep.insert(wscfg.ws_cull_nclip_keep_sites.begin(), wscfg.ws_cull_nclip_keep_sites.end());
|
|
ws_cull_nclip_exact.insert(wscfg.ws_cull_nclip_exact_sites.begin(), wscfg.ws_cull_nclip_exact_sites.end());
|
|
ws_cull_branch_keep.insert(wscfg.ws_cull_branch_keep_sites.begin(), wscfg.ws_cull_branch_keep_sites.end());
|
|
ws_cull_bgez.insert(wscfg.ws_cull_bgez_sites.begin(), wscfg.ws_cull_bgez_sites.end());
|
|
ws_cull_clip_edge_x_load.insert(wscfg.ws_cull_clip_edge_x_load_sites.begin(),
|
|
wscfg.ws_cull_clip_edge_x_load_sites.end());
|
|
if (!wscfg.ws_cull_clip_edge_x_load_sites.empty() && !ws_cull_clip_edge_width)
|
|
ws_cull_clip_edge_width = PSXRecompV4::ws_cull_clip_edge_width(wscfg);
|
|
if (ws_cull_keep.empty()) ws_cull_keep = wscfg.ws_cull_keep_sites;
|
|
if (ws_cull_sxy.empty()) ws_cull_sxy = wscfg.ws_cull_sxy_sites;
|
|
if (ws_cull_packed_x.empty()) ws_cull_packed_x = wscfg.ws_cull_packed_x_sites;
|
|
if (ws_cull_masked_reject.empty()) ws_cull_masked_reject = wscfg.ws_cull_masked_reject_sites;
|
|
if (ws_cull_scale.empty()) ws_cull_scale = wscfg.ws_cull_scale_sites;
|
|
if (ws_cull_angle.empty()) ws_cull_angle = wscfg.ws_cull_angle_sites;
|
|
if (ws_aspect_cone.sites.empty())
|
|
ws_aspect_cone = wscfg.ws_aspect_cone;
|
|
ws_cull_activation_guard_pixels =
|
|
wscfg.ws_cull_activation_guard_pixels;
|
|
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;
|
|
{
|
|
/* The directory the profile probe walks. --project-root when given,
|
|
* else the CWD (unchanged legacy behaviour). Named in every failure
|
|
* below, because "no BIOS profile found" without the directory it
|
|
* searched sends you looking for an unset key that is in fact set —
|
|
* or, worse, reads as a config error when it is a CWD error. */
|
|
const std::filesystem::path search_root =
|
|
project_root.empty() ? std::filesystem::path(".") : project_root;
|
|
|
|
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. A relative bios_config is
|
|
* documented as relative to the game project root, so resolve it there
|
|
* first when we were told what the root is; fall back to the bare path
|
|
* so callers that already relied on CWD keep working. */
|
|
if (!prof.empty() && !std::filesystem::exists(prof)) {
|
|
std::filesystem::path rooted;
|
|
if (!project_root.empty() && prof.is_relative()) {
|
|
rooted = project_root / prof;
|
|
}
|
|
if (!rooted.empty() && std::filesystem::exists(rooted)) {
|
|
prof = rooted;
|
|
} else {
|
|
fmt::print(stderr,
|
|
"psxrecomp-game: FATAL: [recompiler] bios_config = '{}' does "
|
|
"not exist (path is relative to the game project root; "
|
|
"searched '{}')\n",
|
|
prof.string(),
|
|
std::filesystem::absolute(search_root).string());
|
|
return 1;
|
|
}
|
|
}
|
|
if (prof.empty()) {
|
|
if (std::filesystem::exists(search_root / "bios/SCPH1001.toml")) {
|
|
prof = search_root / "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(search_root, 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. Searched '{}' "
|
|
"for bios/SCPH1001.toml and <framework>/bios/SCPH1001.toml. "
|
|
"Fix by setting [recompiler] bios_config in game.toml, passing "
|
|
"--project-root <dir> (the framework or game-project root), or "
|
|
"running from a root that contains one of those paths.\n",
|
|
std::filesystem::absolute(search_root).string());
|
|
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());
|
|
if (exe->has_analysis_guard()) {
|
|
// Only printed for a producer-tagged capture, so a non-overlay image's
|
|
// log stays byte-identical to the untagged build.
|
|
fmt::print(" Analysis End: 0x{:08X} ({} trailing delay-slot guard "
|
|
"byte(s), readable but not discoverable)\n",
|
|
exe->analysis_end_address(), exe->analysis_guard_bytes);
|
|
}
|
|
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);
|
|
} else if (exe->header.initial_pc >= exe->analysis_end_address()) {
|
|
fmt::print("WARNING: entry point 0x{:08X} is at or past Analysis End 0x{:08X} (delay-slot guard bytes, not analysable); it cannot produce a function\n", exe->header.initial_pc, exe->analysis_end_address());
|
|
}
|
|
|
|
// Config code sites are code identities, and code identity is the full
|
|
// virtual address in the image's link segment (docs/SEGMENT_AWARE_CODE.md
|
|
// §5.1, §5.3). The kinds checked below are matched exactly (by the
|
|
// emitter, or by the runtime against a PC or $ra), so a site that names
|
|
// this image's bytes in another segment would silently match nothing (the
|
|
// EXE parser no longer folds a KUSEG header to KSEG0): refuse it and name
|
|
// the spelling that matches. Not checked, because they are matched by
|
|
// physical address and a foreign spelling already reaches the same bytes:
|
|
// [[recompiler.patches]] (byte patches), [widescreen.cull] keep, angle and
|
|
// aspect_cone sites, [[widescreen.signed_x_bound]] (the emitter's
|
|
// full-word-guarded substitutions) and [widescreen.dome] call_sites (the
|
|
// runtime masks them). Overlay compiles are not checked: a capture view is
|
|
// wrapped at `segment | phys` in whichever segment it ran (§5.7), and the
|
|
// game config, passed as --ws-config, names overlay code by its bytes, so
|
|
// overlay_codegen_config() below moves each exact-match site that lies in
|
|
// the image into the view's segment.
|
|
if (!overlay_mode) {
|
|
std::vector<std::pair<const char*, uint32_t>> foreign;
|
|
auto check = [&](const char* what, uint32_t addr) {
|
|
if (addr != 0 && exe->contains_phys(addr) &&
|
|
(addr & PSXRecomp::kSegmentMask) != exe->link_segment())
|
|
foreign.emplace_back(what, addr);
|
|
};
|
|
auto check_set = [&](const char* what, const std::set<uint32_t>& set) {
|
|
for (uint32_t a : set) check(what, a);
|
|
};
|
|
check_set("widescreen.sprite_tag_funcs", ws_tag_funcs);
|
|
check_set("data_shards.funcs", ds_funcs);
|
|
check_set("recompiler.mod_function_entry_funcs", mod_entry_funcs);
|
|
check_set("recompiler.hot_funcs", hot_funcs);
|
|
check_set("recompiler.load_charge_batch_funcs", load_charge_batch_funcs);
|
|
for (const auto& [func, addrs] : vsync_query_hle_funcs) {
|
|
(void)addrs;
|
|
check("load_accel.vsync_query.func", func);
|
|
}
|
|
for (uint32_t site : vsync_horizon_sites)
|
|
check("load_accel.vsync_query.event_horizon_sites", site);
|
|
for (uint32_t site : vsync_horizon_extra_sites)
|
|
check("load_accel.vsync_query.event_horizon_extra_sites", site);
|
|
check_set("widescreen.cull.bias_sites", ws_cull_bias);
|
|
check_set("widescreen.cull.range_sites", ws_cull_range);
|
|
check_set("widescreen.cull.a1_sites", ws_cull_a1);
|
|
check_set("widescreen.cull.screen_x_sites", ws_cull_screen_x);
|
|
check_set("widescreen.cull.slti_sites", ws_cull_slti);
|
|
check_set("widescreen.cull.slti_lower_sites", ws_cull_slti_lower);
|
|
check_set("widescreen.cull.bltz_sites", ws_cull_bltz);
|
|
check_set("widescreen.cull.negsub_sites", ws_cull_negsub);
|
|
check_set("widescreen.cull.vxrange_sites", ws_cull_vxrange);
|
|
check_set("widescreen.cull.depth_sites", ws_cull_depth);
|
|
check_set("widescreen.cull.plane_nx_sites", ws_cull_plane_nx);
|
|
check_set("widescreen.cull.xclip_load_sites", ws_cull_xclip_load);
|
|
check_set("widescreen.cull.nclip_keep_sites", ws_cull_nclip_keep);
|
|
check_set("widescreen.cull.nclip_exact_sites", ws_cull_nclip_exact);
|
|
check_set("widescreen.cull.branch_keep_sites", ws_cull_branch_keep);
|
|
check_set("widescreen.cull.bgez_sites", ws_cull_bgez);
|
|
check_set("widescreen.cull.clip_edge_x_load_sites", ws_cull_clip_edge_x_load);
|
|
check_set("widescreen.backdrop.x_sites", ws_backdrop_x);
|
|
check_set("widescreen.backdrop.unsquash_funcs", ws_backdrop_unsquash);
|
|
check_set("persist_options.init_store_pc", persist_init_sites);
|
|
for (const auto& site : draw_distance_clamps)
|
|
check("draw_distance.clamp.address", site.address);
|
|
check("widescreen.bg2d.count_site", ws_bg2d_count_site);
|
|
check("widescreen.bg2d.startcol_site", ws_bg2d_startcol_site);
|
|
check("widescreen.bg2d.startx_site", ws_bg2d_startx_site);
|
|
check("widescreen.bg2d.stream_left_site", ws_bg2d_stream_left_site);
|
|
check("widescreen.bg2d.stream_right_site", ws_bg2d_stream_right_site);
|
|
check("widescreen.bg2d.bufbase_site", ws_bg2d_bufbase_site);
|
|
check("widescreen.bg2d.cap_site", ws_bg2d_cap_site);
|
|
check("widescreen.bg2d.init_func", ws_bg2d_init_func);
|
|
// [[widescreen.cull.sxy]] sites are matched physically, and every
|
|
// word of each fold and its consumer is part of the identity: check
|
|
// them all against the image up front, so a stale address fails the
|
|
// build even when no discovered function reaches the fold.
|
|
for (const auto& site : ws_cull_sxy) {
|
|
for (size_t i = 0; i <= site.vertices; i++) {
|
|
const bool consumer = i == site.vertices;
|
|
const uint32_t phys = (consumer ? site.consumer_address
|
|
: site.fold_addresses[i]) &
|
|
PSXRecomp::kPhysMask;
|
|
const uint32_t want = consumer ? site.consumer_expected
|
|
: site.fold_expected[i];
|
|
const uint32_t a = exe->link_segment() | phys;
|
|
if (!exe->contains_phys(a)) continue;
|
|
const auto got = exe->read_word(a);
|
|
if (!got || *got != want) {
|
|
fmt::print(stderr,
|
|
"ERROR: [[widescreen.cull.sxy]] expected 0x{:08X} at "
|
|
"0x{:08X}, found 0x{:08X}\n",
|
|
want, a, got ? *got : 0u);
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
if (!foreign.empty()) {
|
|
fmt::print(stderr,
|
|
"ERROR: {} config code site(s) name this image's bytes in another "
|
|
"segment than its link segment 0x{:08X}. Code identity is the full "
|
|
"PC (docs/SEGMENT_AWARE_CODE.md §5.1), so write each in the link "
|
|
"segment:\n", foreign.size(), exe->link_segment());
|
|
for (const auto& [what, addr] : foreign)
|
|
fmt::print(stderr, " {}: 0x{:08X} -> 0x{:08X}\n", what, addr,
|
|
exe->link_segment() | (addr & PSXRecomp::kPhysMask));
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
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.
|
|
*
|
|
* A seed is a PC, so it carries a segment (docs/SEGMENT_AWARE_CODE.md
|
|
* §5.3, §5.4). The bounds are checked by physical address: a seed in the
|
|
* image's link segment (0x000100E4 for a KUSEG-linked EXE, 0x800100E4
|
|
* for a KSEG0 one) is an entry of the image. A seed for the same bytes in
|
|
* another segment asks for a segment variant (§5.4): it is never folded
|
|
* into the home body; after the home compile, the home functions it
|
|
* reaches through direct edges are compiled again in its segment (see
|
|
* plan_segment_variants below). Seeds outside the image are counted. */
|
|
std::vector<uint32_t> file_seeds;
|
|
std::vector<std::pair<uint32_t, std::string>> variant_seeds; // PC, line kind
|
|
size_t outside_seeds = 0;
|
|
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->analysis_end_address();
|
|
std::vector<uint32_t> dropped_past_analysis_end;
|
|
std::vector<uint32_t> dropped_outside_image;
|
|
// The directive lines below name code by PC too, so they follow
|
|
// the seed rule (§5.3): an address that names this image's bytes
|
|
// in another segment is refused with its link-segment spelling,
|
|
// instead of failing the raw bounds check with no hint. (A
|
|
// variant request is a plain seed line; directives have none.)
|
|
// `end_exclusive`: the last address is a range end, which names
|
|
// the image when its last byte does.
|
|
auto foreign_directive = [&](const std::string& text,
|
|
std::initializer_list<uint32_t> addrs,
|
|
bool end_exclusive = false) {
|
|
auto in_image = [&](uint32_t a, bool is_end) {
|
|
return is_end ? a != 0 && exe->contains_phys(a - 1u)
|
|
: exe->contains_phys(a);
|
|
};
|
|
bool foreign = false;
|
|
size_t i = 0;
|
|
for (uint32_t a : addrs) {
|
|
const bool is_end = end_exclusive && ++i == addrs.size();
|
|
foreign |= in_image(a, is_end) &&
|
|
(a & PSXRecomp::kSegmentMask) != exe->link_segment();
|
|
}
|
|
if (!foreign) return false;
|
|
std::string fixed;
|
|
i = 0;
|
|
for (uint32_t a : addrs) {
|
|
const bool is_end = end_exclusive && ++i == addrs.size();
|
|
fixed += fmt::format(" 0x{:08X}", in_image(a, is_end)
|
|
? exe->link_segment() | (a & PSXRecomp::kPhysMask) : a);
|
|
}
|
|
fmt::print(stderr,
|
|
"ERROR: seeds directive names this image's bytes in another "
|
|
"segment than its link segment 0x{:08X} "
|
|
"(docs/SEGMENT_AWARE_CODE.md §5.3): {}\n write its "
|
|
"addresses as:{}\n", exe->link_segment(), text, fixed);
|
|
return true;
|
|
};
|
|
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() &&
|
|
foreign_directive(line, {lo, hi}, /*end_exclusive=*/true))
|
|
return 1;
|
|
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 (foreign_directive(line, {addr})) return 1;
|
|
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) &&
|
|
foreign_directive(line, {target, host}))
|
|
return 1;
|
|
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);
|
|
const uint32_t phys = addr & PSXRecomp::kPhysMask;
|
|
if (phys < (seed_lo & PSXRecomp::kPhysMask) ||
|
|
phys >= (seed_lo & PSXRecomp::kPhysMask) + (seed_hi - seed_lo)) {
|
|
++outside_seeds;
|
|
if (exe->contains_phys(addr)) dropped_past_analysis_end.push_back(addr);
|
|
else dropped_outside_image.push_back(addr);
|
|
} else if (!PSXRecomp::maps_physical(addr)) {
|
|
// Only KUSEG below 0x20000000, KSEG0 and KSEG1 alias the
|
|
// image's bytes; a variant in any other segment would
|
|
// give dispatch rows to PCs hardware cannot fetch as RAM.
|
|
fmt::print(stderr,
|
|
"ERROR: seed 0x{:08X} is in segment 0x{:08X}, which does not "
|
|
"map physical memory (only KUSEG below 0x20000000, KSEG0 and "
|
|
"KSEG1 do), so it names no code of this image and cannot ask "
|
|
"for a segment variant "
|
|
"(docs/SEGMENT_AWARE_CODE.md §5.4). The image's bytes at that "
|
|
"physical address are 0x{:08X} in its link segment.\n",
|
|
addr, addr & PSXRecomp::kSegmentMask,
|
|
exe->link_segment() | phys);
|
|
return 1;
|
|
} else if ((addr & PSXRecomp::kSegmentMask) != exe->link_segment()) {
|
|
variant_seeds.emplace_back(
|
|
addr, interior ? "interior" : trusted_root ? "dispatch_root"
|
|
: trusted_call_root ? "call_root" : "entry");
|
|
} else 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);
|
|
}
|
|
}
|
|
auto list_seeds = [](const std::vector<uint32_t>& seeds) {
|
|
std::string text;
|
|
for (size_t i = 0; i < seeds.size() && i < 16; i++)
|
|
text += fmt::format("{}0x{:08X}", i ? ", " : "", seeds[i]);
|
|
if (seeds.size() > 16) text += ", ...";
|
|
return text;
|
|
};
|
|
if (!dropped_past_analysis_end.empty()) {
|
|
fmt::print("WARNING: dropped {} requested seed(s) at or past "
|
|
"Analysis End 0x{:08X} (delay-slot guard bytes, "
|
|
"not analysable): {}\n",
|
|
dropped_past_analysis_end.size(), seed_hi,
|
|
list_seeds(dropped_past_analysis_end));
|
|
}
|
|
if (!dropped_outside_image.empty()) {
|
|
fmt::print("WARNING: dropped {} requested seed(s) outside the "
|
|
"image 0x{:08X}..0x{:08X}: {}\n",
|
|
dropped_outside_image.size(), seed_lo,
|
|
exe->end_address(),
|
|
list_seeds(dropped_outside_image));
|
|
}
|
|
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);
|
|
if (outside_seeds) {
|
|
fmt::print(" {} seed(s) lie outside this image (0x{:08X}-0x{:08X}) "
|
|
"and were not used\n",
|
|
outside_seeds, seed_lo, seed_hi);
|
|
}
|
|
if (!variant_seeds.empty()) {
|
|
auto seg_name = [](uint32_t seg) {
|
|
return seg == PSXRecomp::kSegKUSEG ? "KUSEG"
|
|
: seg == PSXRecomp::kSegKSEG0 ? "KSEG0"
|
|
: seg == PSXRecomp::kSegKSEG1 ? "KSEG1" : "an unmapped segment";
|
|
};
|
|
if (overlay_mode) {
|
|
fmt::print("WARNING: {} seed(s) name this image's bytes in another "
|
|
"segment than its link segment {}. Overlay compiles have "
|
|
"no segment variants yet (docs/SEGMENT_AWARE_CODE.md §5.7): "
|
|
"nothing is compiled for them.\n",
|
|
variant_seeds.size(), seg_name(exe->link_segment()));
|
|
} else {
|
|
fmt::print("{} seed(s) name this image's bytes in another segment than "
|
|
"its link segment {}. Each asks for a segment variant "
|
|
"(docs/SEGMENT_AWARE_CODE.md §5.4), compiled after the home "
|
|
"compile. A seed for the image's own code is written in {}.\n",
|
|
variant_seeds.size(), seg_name(exe->link_segment()),
|
|
seg_name(exe->link_segment()));
|
|
}
|
|
for (const auto& [addr, kind] : variant_seeds) {
|
|
fmt::print(" variant seed 0x{:08X} ({}, {}): home PC would be 0x{:08X}\n",
|
|
addr, kind, seg_name(addr & PSXRecomp::kSegmentMask),
|
|
exe->link_segment() | (addr & PSXRecomp::kPhysMask));
|
|
}
|
|
}
|
|
} 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->analysis_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());
|
|
// Overlay mode enforces the no-split guard on explicit roots too
|
|
// (compile_overlays.py applies the same rule first; this catches
|
|
// walk proofs only this analyzer makes). Classifier-promoted
|
|
// dispatch roots are exempt. Main-EXE reachable discovery keeps
|
|
// its existing partition.
|
|
analysis_result = analyzer.analyze_exact_entries(
|
|
roots_vec, producer_ranges, cross_call_allow,
|
|
/*no_split_guard=*/overlay_mode,
|
|
overlay_mode ? trusted_root_seeds : std::set<uint32_t>{});
|
|
}
|
|
|
|
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->analysis_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.mod_function_entry_funcs = mod_entry_funcs;
|
|
codegen_config.ws_cull_masked_reject_sites = ws_cull_masked_reject;
|
|
codegen_config.mod_instruction_sites = mod_instruction_sites;
|
|
codegen_config.mod_immediate_sites = mod_immediate_sites;
|
|
codegen_config.hot_funcs = hot_funcs;
|
|
codegen_config.load_charge_batch_funcs = load_charge_batch_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_bias_lower_sites = ws_cull_bias_lower;
|
|
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_activation_guard_pixels =
|
|
ws_cull_activation_guard_pixels;
|
|
codegen_config.ws_cull_slti_sites = ws_cull_slti;
|
|
codegen_config.ws_cull_slti_lower_sites = ws_cull_slti_lower;
|
|
codegen_config.ws_cull_bltz_sites = ws_cull_bltz;
|
|
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.netplay_local_viewport_width_sites = netplay_local_viewport_width_sites;
|
|
codegen_config.ws_cull_xclip_load_sites = ws_cull_xclip_load;
|
|
codegen_config.ws_cull_nclip_keep_sites = ws_cull_nclip_keep;
|
|
codegen_config.ws_cull_nclip_exact_sites = ws_cull_nclip_exact;
|
|
codegen_config.ws_cull_branch_keep_sites = ws_cull_branch_keep;
|
|
codegen_config.ws_cull_bgez_sites = ws_cull_bgez;
|
|
codegen_config.ws_cull_clip_edge_x_load_sites = ws_cull_clip_edge_x_load;
|
|
if (ws_cull_clip_edge_width)
|
|
codegen_config.ws_cull_clip_edge_width = ws_cull_clip_edge_width;
|
|
else if (!ws_cull_w_imms.empty())
|
|
codegen_config.ws_cull_clip_edge_width = ws_cull_w_imms.front();
|
|
codegen_config.ws_cull_keep_sites = ws_cull_keep;
|
|
codegen_config.ws_cull_sxy_sites = ws_cull_sxy;
|
|
codegen_config.ws_cull_packed_x_sites = ws_cull_packed_x;
|
|
codegen_config.ws_cull_scale_sites = ws_cull_scale;
|
|
codegen_config.ws_cull_angle_sites = ws_cull_angle;
|
|
codegen_config.draw_distance_clamp_sites = draw_distance_clamps;
|
|
codegen_config.ws_aspect_cone = ws_aspect_cone;
|
|
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);
|
|
}
|
|
|
|
// Overlay capture views (docs/SEGMENT_AWARE_CODE.md §5.7): every view of
|
|
// the same bytes gets the same hooks and substitutions, whatever segment
|
|
// it runs in and however the config spells the site.
|
|
if (overlay_mode)
|
|
codegen_config = PSXRecomp::overlay_codegen_config(codegen_config, *exe);
|
|
|
|
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;
|
|
}
|
|
}
|
|
// Data/untranslatable entries are emitted as fail-closed
|
|
// psx_unknown_dispatch stubs. Do not put those stubs in the callable game
|
|
// dispatch table: a runtime overlay can legitimately replace the same EXE
|
|
// address, and a tiny stub range (especially a single NOP) can
|
|
// coincidentally byte-match the live overlay continuation. Advertising the
|
|
// stub as native-safe then turns valid dynamic code into a false fail-fast.
|
|
for (const auto& gen_func : codegen.last_gen_funcs()) {
|
|
if (gen_func.dispatchable) continue;
|
|
const std::string prefix = "func_";
|
|
if (gen_func.function_name.rfind(prefix, 0) != 0 ||
|
|
gen_func.function_name.size() < prefix.size() + 8) {
|
|
continue;
|
|
}
|
|
const std::string hex =
|
|
gen_func.function_name.substr(prefix.size(), 8);
|
|
dispatch_addrs.erase(
|
|
(uint32_t)std::strtoul(hex.c_str(), nullptr, 16));
|
|
}
|
|
if (dispatch_addrs.empty() && codegen.last_gen_funcs().empty()) {
|
|
for (const auto& func : analysis_result.functions) {
|
|
dispatch_addrs.insert(func.start_addr);
|
|
}
|
|
}
|
|
|
|
// Segment variants (docs/SEGMENT_AWARE_CODE.md §5.4). Each segment named
|
|
// by a variant seed gets the direct-edge closure of the home functions
|
|
// its seeds reach, compiled again through a view of the image in that
|
|
// segment: its names, rows and every PC it bakes are that segment's, and
|
|
// a KSEG1 variant charges a fetch per instruction (§5.6). The bodies join
|
|
// the home shards, and their rows join the home dispatch table. Overlay
|
|
// compiles have none yet (§5.7).
|
|
std::vector<PSXRecomp::GeneratedFunction> all_gen_funcs = codegen.last_gen_funcs();
|
|
std::vector<std::string> variant_alias_decls;
|
|
std::string all_ranges_manifest = codegen.last_ranges_manifest().empty()
|
|
? codegen.generate_ranges_manifest(analysis_result.functions, all_cfgs)
|
|
: codegen.last_ranges_manifest();
|
|
std::vector<PSXRecomp::GameDispatchUnit> dispatch_units{{&codegen, dispatch_addrs}};
|
|
std::vector<std::unique_ptr<PSXRecomp::PS1Executable>> variant_views;
|
|
std::vector<std::unique_ptr<PSXRecomp::CodeGenerator>> variant_codegens;
|
|
if (!overlay_mode && !variant_seeds.empty()) {
|
|
std::vector<uint32_t> requested;
|
|
for (const auto& [addr, kind] : variant_seeds) {
|
|
(void)kind;
|
|
requested.push_back(addr);
|
|
}
|
|
const auto plans = PSXRecomp::plan_segment_variants(
|
|
*exe, codegen.last_functions(), codegen.last_cfgs(),
|
|
codegen.cps_continuations(), requested);
|
|
for (const auto& plan : plans) {
|
|
fmt::print("\n=== Segment variant 0x{:08X} (docs/SEGMENT_AWARE_CODE.md §5.4) ===\n",
|
|
plan.segment);
|
|
for (const auto& [pc, why] : plan.unplaced) {
|
|
fmt::print(stderr,
|
|
"ERROR: variant seed 0x{:08X} cannot be compiled: {}. Seed the home "
|
|
"PC first, or drop the variant seed.\n", pc, why);
|
|
}
|
|
if (!plan.unplaced.empty()) return 1;
|
|
fmt::print(" {} seed(s), direct-edge closure of {} function(s)\n",
|
|
plan.seeds.size(), plan.functions.size());
|
|
variant_views.push_back(std::make_unique<PSXRecomp::PS1Executable>(
|
|
PSXRecomp::segment_view(*exe, plan.segment)));
|
|
const PSXRecomp::PS1Executable& view = *variant_views.back();
|
|
PSXRecomp::ControlFlowAnalyzer view_cfg(view);
|
|
const auto variant_cfgs = view_cfg.analyze_all_functions(plan.functions);
|
|
variant_codegens.push_back(std::make_unique<PSXRecomp::CodeGenerator>(
|
|
view, PSXRecomp::rebase_codegen_config(codegen_config, *exe, plan.segment)));
|
|
PSXRecomp::CodeGenerator& vgen = *variant_codegens.back();
|
|
(void)vgen.generate_file(plan.functions, variant_cfgs);
|
|
std::set<uint32_t> variant_addrs;
|
|
for (const auto& gf : vgen.last_gen_funcs()) {
|
|
all_gen_funcs.push_back(gf);
|
|
if (gf.dispatchable && gf.function_name.rfind("func_", 0) == 0 &&
|
|
gf.function_name.size() == 13)
|
|
variant_addrs.insert(static_cast<uint32_t>(
|
|
std::strtoul(gf.function_name.c_str() + 5, nullptr, 16)));
|
|
}
|
|
for (const auto& decl : vgen.alias_body_decls()) variant_alias_decls.push_back(decl);
|
|
// The variant's F/R records, without the manifest's header comment.
|
|
std::istringstream vr(vgen.last_ranges_manifest());
|
|
for (std::string line; std::getline(vr, line);) {
|
|
if (!line.empty() && line[0] != '#') all_ranges_manifest += line + "\n";
|
|
}
|
|
dispatch_units.push_back({&vgen, variant_addrs});
|
|
fmt::print(" {} bodies, {} dispatch entries\n",
|
|
vgen.last_gen_funcs().size(), variant_addrs.size());
|
|
}
|
|
}
|
|
|
|
// 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::filesystem::path full_path(output_filename);
|
|
if (write_file_if_changed(full_path, full_c_code)) {
|
|
fmt::print("✓ Saved overlay monolith to {}\n", output_filename.string());
|
|
} else {
|
|
fmt::print("✓ Overlay monolith unchanged ({})\n", output_filename.string());
|
|
}
|
|
} else {
|
|
// 1. Shared declarations header (write-if-changed preserves Ninja mtimes).
|
|
std::filesystem::path decls_filename = out_dir / (exe_stem + "_decls.h");
|
|
{
|
|
const std::string decls =
|
|
codegen.build_shared_decls_header(all_gen_funcs, variant_alias_decls);
|
|
if (write_file_if_changed(decls_filename, decls)) {
|
|
fmt::print("✓ Saved shared decls header to {}\n", decls_filename.string());
|
|
} else {
|
|
fmt::print("✓ Shared decls header unchanged ({})\n", decls_filename.string());
|
|
}
|
|
}
|
|
|
|
// 2. Bucket generated functions into shards by cumulative line count.
|
|
const int SHARD_LINE_BUDGET = 40000;
|
|
const std::vector<PSXRecomp::GeneratedFunction>& gen_funcs_for_shards = all_gen_funcs;
|
|
std::string decls_basename = exe_stem + "_decls.h";
|
|
|
|
int shard_index = 0;
|
|
int shard_lines = 0;
|
|
int total_shard_lines = 0;
|
|
int shards_written = 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::string body = fmt::format(
|
|
"/* Generated by PSXRecomp - full.c shard {}. DO NOT EDIT. */\n"
|
|
"#include \"{}\"\n\n{}",
|
|
shard_index, decls_basename, shard_buf.str());
|
|
if (write_file_if_changed(shard_filename, body)) {
|
|
fmt::print("✓ Saved shard {} ({} lines) to {}\n",
|
|
shard_index, shard_lines, shard_filename.string());
|
|
++shards_written;
|
|
} else {
|
|
fmt::print("✓ Shard {} unchanged ({} lines)\n", shard_index, shard_lines);
|
|
}
|
|
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();
|
|
|
|
// 3. Drop orphan shards / monolith after emit (shrink must not leave stale TUs).
|
|
{
|
|
std::error_code rm_ec;
|
|
std::filesystem::remove(output_filename, rm_ec);
|
|
for (int stale = shard_index; 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);
|
|
}
|
|
}
|
|
|
|
fmt::print("✓ Wrote {} shards ({} total lines, {} updated) for {}\n\n",
|
|
shard_index, total_shard_lines, shards_written, 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 = all_ranges_manifest;
|
|
{
|
|
std::filesystem::path ranges_filename = out_dir / (exe_stem + "_full.ranges");
|
|
if (write_file_if_changed(ranges_filename, ranges_manifest)) {
|
|
fmt::print("✓ Saved code-range manifest to {}\n\n",
|
|
ranges_filename.string());
|
|
} else {
|
|
fmt::print("✓ Code-range manifest unchanged ({})\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::string dispatch_source, dispatch_error;
|
|
if (!PSXRecomp::emit_game_dispatch(dispatch_units, *exe,
|
|
ranges_manifest, dispatch_source,
|
|
dispatch_error)) {
|
|
fmt::print(stderr, "ERROR: {}\n", dispatch_error);
|
|
return 1;
|
|
}
|
|
// Entries of the home compile, plus those of its segment variants.
|
|
size_t variant_entries = 0;
|
|
for (size_t u = 1; u < dispatch_units.size(); ++u)
|
|
variant_entries += dispatch_units[u].dispatch_addrs.size();
|
|
const std::string variant_note = variant_entries
|
|
? fmt::format(", plus {} in segment variants", variant_entries) : std::string();
|
|
if (write_file_if_changed(dispatch_filename, dispatch_source)) {
|
|
fmt::print("✓ Dispatch table written ({} entries{})\n\n",
|
|
dispatch_addrs.size(), variant_note);
|
|
} else {
|
|
fmt::print("✓ Dispatch table unchanged ({} entries{})\n\n",
|
|
dispatch_addrs.size(), variant_note);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|