#include #include #include #include #include #include #include #include #include #include #include #include #include #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& alias_entries) { std::map> 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 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 \n", argv[0]); fmt::print(" {} [--seeds ] [--out-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 Resolve the BIOS profile (bios/SCPH1001.toml, or\n"); fmt::print(" /bios/SCPH1001.toml one level down) against\n"); fmt::print(" 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 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 : 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 : 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 ws_tag_funcs; // [widescreen] sprite_tag_funcs std::set ds_funcs; // [data_shards] funcs std::set mod_entry_funcs; // trusted game-mod entry hooks std::set hot_funcs; // [recompiler] hot_funcs std::set load_charge_batch_funcs; // [recompiler] load_charge_batch* std::map> vsync_query_hle_funcs; std::set ws_cull_bias, ws_cull_range, ws_cull_a1; // [widescreen.cull] std::set ws_cull_screen_x; // [widescreen.cull] screen_x_sites std::set ws_cull_slti; // [widescreen.cull] slti_sites std::set ws_cull_slti_lower; // [widescreen.cull] slti_lower_sites std::set ws_cull_bltz; // [widescreen.cull] bltz_sites std::set ws_cull_negsub; // [widescreen.cull] negsub_sites std::set ws_cull_vxrange; // [widescreen.cull] vxrange_sites std::set ws_cull_depth; // [widescreen.cull] depth_sites std::set ws_cull_plane_nx; // [widescreen.cull] plane_nx_sites std::set ws_cull_xclip_load; // [widescreen.cull] xclip_load_sites std::set ws_cull_nclip_keep; // [widescreen.cull] nclip_keep_sites std::set ws_cull_nclip_exact; // [widescreen.cull] nclip_exact_sites std::set ws_cull_branch_keep; // [widescreen.cull] branch_keep_sites std::set ws_cull_bgez; // [widescreen.cull] bgez_sites std::set 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 ws_cull_keep; std::vector ws_cull_angle; PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone; int ws_cull_activation_guard_pixels = 0; std::vector ws_cull_w_imms = { 0x140, 0x141 }; // [widescreen.cull] screen_w_imms std::vector ws_cull_h_imms = { 0xE0, 0xF1 }; // [widescreen.cull] screen_h_imms std::set ws_backdrop_x; // [widescreen.backdrop] x_sites std::set ws_backdrop_unsquash; // [widescreen.backdrop] unsquash_funcs bool ws_auto_screen_x_cull = false; // [widescreen.cull] auto_screen_x std::set 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 vsync_horizon_sites, vsync_horizon_extra_sites; std::vector instruction_patches; std::vector 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()); 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_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()); 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_angle = cfg.ws_cull_angle_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()); 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_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()); 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_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 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 /bios/SCPH1001.toml. " "Fix by setting [recompiler] bios_config in game.toml, passing " "--project-root (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); } // 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 wrap captures at KSEG0 and take // the game's config as --ws-config; their segment keys arrive with PR E // (§5.7). if (!overlay_mode) { std::vector> 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& 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); 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); 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 instr_freq; std::map opcode_freq; std::map 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> 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 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 file_seeds; std::vector> variant_seeds; // PC, line kind size_t outside_seeds = 0; std::vector interior_seeds; std::map hosted_interior_seeds; std::set hosted_interior_hosts; constexpr size_t kHostedInteriorTargetCap = 4096; constexpr size_t kHostedInteriorHostCap = 512; std::set trusted_root_seeds; std::set trusted_call_root_seeds; std::vector> producer_ranges; std::set 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(); // 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 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( std::strtoul(lo_text.c_str(), nullptr, 16)); uint32_t hi = static_cast( 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( 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(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; } 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); } } 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 roots; std::set interior; std::set 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 roots_vec(roots.begin(), roots.end()); analysis_result = analyzer.analyze_exact_entries( roots_vec, producer_ranges, cross_call_allow); } std::vector alias_entries; std::set 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 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 alias_entries; std::set 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 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 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 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(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(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.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_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.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_angle_sites = ws_cull_angle; 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/_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 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 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 all_gen_funcs = codegen.last_gen_funcs(); std::vector 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 dispatch_units{{&codegen, dispatch_addrs}}; std::vector> variant_views; std::vector> variant_codegens; if (!overlay_mode && !variant_seeds.empty()) { std::vector 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::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( 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 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( 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 _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 _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& 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; }