#pragma once #include #include #include #include #include #include #include #include #include "ps1_exe_parser.h" #include "function_analysis.h" #include "control_flow.h" #include "../src/annotations.hpp" #include "../src/config_loader.h" namespace PSXRecompV4 { class BiosAddressModel; } namespace PSXRecomp { // CPU state structure (registers + memory access) struct CPUState { // General purpose registers (32 total) uint32_t gpr[32]; // $0-$31 uint32_t pc; // Program counter uint32_t hi, lo; // Multiply/divide results // Memory access functions (to be implemented in runtime) uint32_t (*read_word)(uint32_t addr); void (*write_word)(uint32_t addr, uint32_t value); uint16_t (*read_half)(uint32_t addr); void (*write_half)(uint32_t addr, uint16_t value); uint8_t (*read_byte)(uint32_t addr); void (*write_byte)(uint32_t addr, uint8_t value); }; // Code generation configuration struct CodeGenConfig { bool emit_comments; // Include MIPS instruction comments bool emit_line_numbers; // Include original address as comments bool optimize_zero_reg; // Optimize away $zero assignments bool use_switch_for_blocks; // Use switch instead of goto labels bool split_mid_function_targets; // Split branch targets into funcs for legacy main-EXE analysis // True when compiling an overlay image (psxrecomp-game --overlay). Overlay // regions hold different scene VARIANTS at the same addresses (the same addr // is backdrop code in one scene, unrelated code/data in another), so an // explicit per-address widescreen site (x_sites / cull_* sites) authored for // one variant won't match the bytes in another. In overlay mode such a shape // mismatch is EXPECTED (apply the transform only where the bytes match, skip // it elsewhere) rather than a hard config error. bool overlay_mode = false; std::vector ws_signed_x_bound_sites; std::string indent; // Indentation string (default: " ") // Widescreen sprite-tag hooks ([widescreen] sprite_tag_funcs): functions // that get a psx_ws_sprite_tag(cpu) callback emitted at entry, so the // runtime can record the per-prim pointer ($a0) + projected anchor for // proportion correction at GP0 submission. Empty = no hooks (default). std::set ws_sprite_tag_funcs; // Data-shard hooks ([data_shards] funcs): memoized pure-function replay. // Entry gets `if (psx_datashard_enter(cpu, KEY)) return;` (replay path), // every jr-$ra return gets `psx_datashard_ret(cpu)` (capture finalize). // See docs/DATA_SHARDS.md. Empty = no hooks (default). std::set data_shard_funcs; // Trusted game-mod entry hooks ([recompiler] mod_function_entry_funcs). // Only explicitly listed guest functions call the runtime dispatcher. std::set mod_function_entry_funcs; std::set mod_instruction_sites; // [[mod_immediate.sites]]: guarded instructions whose immediate a trusted mod // selects at run time; emitted as a selector switch over every variant. std::vector mod_immediate_sites; // [recompiler] hot_funcs: emit __attribute__((hot)) on these guest // addresses (MotK VLC leaves, etc.). Host locality hint only. std::set hot_funcs; // [recompiler] load_charge_batch(_funcs): install a function-local // cycle accumulator so load charges skip deadline probes until IRQ/MMIO // / function exit. Guest totals at those barriers are unchanged. std::set load_charge_batch_funcs; // [load_accel.vsync_query] verified PsyQ VSync functions whose mode=-1 // path may bypass its unused GPUSTAT/Timer1 reads. The map value is the // guest RAM VBlank counter returned by that query path. Empty = inert. // value = {VBlank counter, GPUSTAT-pointer global, Timer1-pointer global, // cached Timer1 global} std::map> vsync_query_hle_funcs; // MMX6-class 2D tile-ring stage initializer. Emit a reveal-invalidation // callback at entry so host-only wide pixels cannot survive a stage swap. uint32_t ws_bg2d_init_func = 0; // Widescreen far-backdrop un-squash ([widescreen] backdrop_unsquash_funcs): // functions whose body is bracketed by gte_ws_set_suppress(1)/(0) so the // GTE X-squash is OFF for their (far-backdrop) draws — the backdrop fills // the stretched 16:9 frame instead of leaving edge void (8C). Suppress is // set at entry and cleared before every jr-$ra return. Empty = none. std::set ws_backdrop_unsquash_funcs; // Widescreen cull-margin widening ([widescreen] cull_* sites). At these // addresses the immediate is emitted with a runtime margin term // psx_ws_x_margin() so the world-space draw cull widens with the aspect // (0 at 4:3). bias = addiu (+margin); range = sltiu (+2*margin); a1 = a // nop replaced with `a1 += margin`, or move rD,a1 replaced with // `rD = a1 + margin`. See config_loader.h. Empty = default. std::set ws_cull_bias_sites; // Lower endpoint counterpart: addi/addiu immediate minus activation margin. std::set ws_cull_bias_lower_sites; std::set ws_cull_range_sites; std::set ws_cull_a1_sites; std::set ws_cull_screen_x_sites; // Extra lead for bias/range activation windows only. Render/terrain // helpers continue to use psx_ws_x_margin() without this term. int ws_cull_activation_guard_pixels = 0; // Explicit signed right-edge widen sites ([widescreen.cull] slti_sites): // `slti rt, sx, W` emitted through psx_ws_cull_slti for funnel functions // the auto-detector cannot qualify (X-only test, no height compare). std::set ws_cull_slti_sites; // Signed lower-bound counterpart: `slti rt,sx,-W` compares against // `-W-x_margin`, preserving the original result at 4:3. std::set ws_cull_slti_lower_sites; // `bltz rs, reject` emitted through psx_ws_cull_bltz — the explicit // LEFT-edge counterpart to ws_cull_slti_sites ([widescreen.cull] // bltz_sites), for X-only funnels the auto-detector cannot qualify. std::set ws_cull_bltz_sites; // `bgez rs, keep` emitted through psx_ws_cull_bgez ([widescreen.cull] // bgez_sites): keep while rs >= -margin. Pairs with bltz_sites to make a // signed per-vertex left-edge chain exact. Identity at 4:3. std::set ws_cull_bgez_sites; // lh/lhu/lw screen-X clip-bound loads emitted through psx_ws_clip_edge_x // ([widescreen.cull] clip_edge_x_load_sites): 0 -> -margin and // ws_cull_clip_edge_width -> width+margin while revealed; identity at 4:3. std::set ws_cull_clip_edge_x_load_sites; uint32_t ws_cull_clip_edge_width = 0x140; // Explicit horizontal low-edge widen sites ([widescreen.cull] // negsub_sites): `subu rd, zero, rs` becomes `-rs - x_margin`. // Identity at 4:3; empty by default. std::set ws_cull_negsub_sites; // Explicit masked-16-bit screen-X window sites ([widescreen.cull] // vxrange_sites). The configured sltiu is routed through the shared // runtime helper so native and interpreted overlays use identical math. std::set ws_cull_vxrange_sites; // Explicit aspect-scaled far-bound sites ([widescreen.cull] // depth_sites). Configured slti/sltiu instructions route through the // runtime helper; empty by default. std::set ws_cull_depth_sites; // Side frustum-plane normal-X load sites ([widescreen.cull] // plane_nx_sites). The configured lw routes through the runtime helper // (inverse-aspect scale while revealed, identity at 4:3). std::set ws_cull_plane_nx_sites; std::set netplay_local_viewport_width_sites; // Per-primitive X-reject bound load sites ([widescreen.cull] // xclip_load_sites). The configured lw routes through the runtime helper // (INT32_MAX while revealed, vanilla at 4:3); empty by default. std::set ws_cull_xclip_load_sites; // Exact `bltz MAC0, reject`-style NCLIP/backface rejects to suppress while // widescreen reveals extra world. 4:3 keeps the original branch predicate. std::set ws_cull_nclip_keep_sites; // Exact `bltz MAC0, reject`-style NCLIP/backface sites that consume the // validated precise sign while wide, without changing guest-visible MAC0. std::set ws_cull_nclip_exact_sites; // Exact branch PCs whose reject target is suppressed while widescreen // reveals extra world. 4:3 keeps the original branch predicate. std::set ws_cull_branch_keep_sites; // Exact, full-word-guarded comparison sites whose result is forced only // while widescreen reveals extra world. 4:3 evaluates the original compare. std::vector ws_cull_keep_sites; // [[widescreen.cull.sxy]] packed-SXY AND folds (left edge widened). std::vector ws_cull_sxy_sites; std::vector ws_cull_masked_reject_sites; std::vector ws_cull_scale_sites; // Exact `addi[u] rt,zero,imm` 12-bit angular half-extents. The runtime // scales tan(angle) by the current horizontal reveal factor. std::vector ws_cull_angle_sites; // [[draw_distance.clamp]]: main-EXE ordering-table range guards. While // g_psx_draw_distance_clamp is set the site's register is clamped to its // max before the instruction runs; overlay code keeps its own code. std::vector draw_distance_clamp_sites; // Exact model-participation cosine compares that gain a camera-horizontal // aspect envelope while preserving the vanilla vertical cone. PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone; // Screen-extent signature immediates ([widescreen.cull] screen_w_imms / // screen_h_imms) — per-game display-width-derived bounds. Defaults are the // Tomba signature; Ape Escape uses 0x181 (+ 0xF1 height). std::vector ws_cull_w_imms = { 0x140, 0x141 }; std::vector ws_cull_h_imms = { 0xE0, 0xF1 }; // Widescreen automatic horizontal-FOV cull widening ([widescreen.cull] // auto_screen_x). When true, any function carrying the GTE screen-extent // reject signature — at least one `sltiu …,0x140/0x141` (right/width) AND // one `sltiu …,0xE0/0xF1` (bottom/height) — has every width compare emitted // with + 2*psx_ws_x_margin() (0 at 4:3). This routes the whole render-funnel // (≈100 sites in Tomba) through one helper with no per-address list, and is // generic to any PSX GTE title. The vertical (0xE0) bound is never touched. bool ws_auto_screen_x_cull = false; // Widescreen automatic far-backdrop column PRELOAD ([widescreen.cull] // auto_backdrop). When true, every scrolling-backdrop column-window generator // is auto-detected by its invariant (see ws_backdrop_detect.h) and its window // START/END finalize instructions are emitted through psx_ws_backdrop_value() // so the whole finite tile row preloads in 16:9 (0 at 4:3 => byte-identical). // Generators are overlay-resident, so this must reach the overlay compile. bool ws_auto_backdrop_preload = false; // Widescreen backdrop screenX squash ([widescreen.backdrop] x_sites). Each // address is a `sh rt,off(base)` storing a parallax 2D backdrop layer's // final screen-X; emitted as write_half(base+off, psx_ws_backdrop_x(rt)) // so the un-GTE'd backdrop is squashed around screen centre for the 16:9 // FOV (identity at 4:3). Overlay-resident; see config_loader.h. Empty=default. std::set ws_backdrop_x_sites; // Widescreen pure-2D background tile-loop widen ([widescreen.bg2d]). Three // instruction addresses in a 2D game's per-layer BG renderer (MMX6's // FUN_800270d0): the column-count load/compare, the start tile-column mask, // and the start screen-x calculation (MMX6: li count + sra start; MMX4/MMX5: // inline slti[u] count, sra or subu-zero start), rewritten through the gpu.c // psx_ws_bg2d_* helpers so the loop draws the 16:9 reveal columns on both // sides (identity at 4:3 / 512 hi-res). 0 = unset. Main-EXE addresses; // verified by opcode at gen time. uint32_t ws_bg2d_count_site = 0; uint32_t ws_bg2d_startcol_site = 0; uint32_t ws_bg2d_startx_site = 0; uint32_t ws_bg2d_stream_left_site = 0; // tile-ring streamer left leading-edge addiu uint32_t ws_bg2d_stream_right_site = 0; // tile-ring streamer right leading-edge addiu uint32_t ws_bg2d_bufbase_site = 0; // driver addu: BG packet-buffer address (relocate) uint32_t ws_bg2d_cap_site = 0; // renderer slti: per-frame BG tile cap (raise) // Persistent game-option init-store hooks ([persist_options] in // game_options.toml). Each entry is the PC of a boot-init sb/sh that writes a // config global's DEFAULT value; the store is rewritten to route the value // through psx_game_option_store(addr,val), so a value persisted from a prior // session overrides the default at initialization (issue #5). Identity when // nothing is persisted, so a fresh install is byte-identical. Empty=default. std::set persist_init_store_sites; CodeGenConfig() : emit_comments(true) , emit_line_numbers(true) , optimize_zero_reg(true) , use_switch_for_blocks(false) , split_mid_function_targets(true) , indent(" ") {} }; // Result of code generation for a function struct GeneratedFunction { std::string function_name; // e.g., "func_80016940" std::string signature; // e.g., "void func_80016940(CPUState* cpu)" std::string body; // C code body std::string full_code; // signature + body int line_count; // Number of lines generated // False for fail-closed data/untranslatable stubs whose only operation is // psx_unknown_dispatch. They remain emitted for direct diagnostics, but // must not advertise themselves as executable native game entries. bool dispatchable = true; }; class CodeGenerator { public: explicit CodeGenerator(const PS1Executable& exe, const CodeGenConfig& config = CodeGenConfig()); // Set the BIOS address model the game codegen folds shell/kernel RAM // aliases through (jump tables inside relocated BIOS code windows). // Resolved from the game's [recompiler] bios_config profile (default: // the SCPH1001 profile) in main_psx.cpp; there is no built-in window. // Must outlive code generation. static void set_bios_address_model(const PSXRecompV4::BiosAddressModel* m); // Generate C code for a single function. // fallthrough_name: if non-empty and the last block has no explicit control // flow (ControlFlowType::None), emit a tail call to that function before '}'. // This handles MIPS functions that fall through into the next function without // an explicit jr/j instruction. GeneratedFunction generate_function( const Function& func, const ControlFlowGraph& cfg, const std::string& fallthrough_name = ""); // Generate C code for all functions std::vector generate_all_functions( const std::vector& functions, const std::map& cfgs); // Generate a group of overlapping-alias entries that share one host // function: ONE static body holding the host-range blocks (restricted to // the union of blocks reachable from the entries) with an entry switch, // plus a dispatchable wrapper per entry. Returns one GeneratedFunction per // entry; the first carries the shared body. std::vector generate_alias_group( const std::vector& aliases, const ControlFlowGraph& cfg, const std::string& fallthrough_name); // Generate complete C file with all functions std::string generate_file( const std::vector& functions, const std::map& cfgs); // The generated functions from the most recent generate_file() call // (post mid-function-split pass). Consumed by main_psx to write the // split full.c shards + shared decls header. Not populated until // generate_file() has run. const std::vector& last_gen_funcs() const { return last_gen_funcs_; } // Build the shared-declarations header shared by every full.c shard: // the runtime extern prologue, the unaligned-access helpers (as static // inline, since this header is included by multiple translation units), // and forward declarations for every recompiled function plus every // overlapping-alias shared body. Byte-for-byte the same declaration set // the monolith emits inline, just hoisted into a header. // `extra_decls` adds the alias-body prototypes of other generators whose // functions share these shards (segment variants, §5.4). std::string build_shared_decls_header(const std::vector& gen_funcs, const std::vector& extra_decls = {}) const; // Prototypes of the psx_alias_body_XXXXXXXX() bodies the most recent // generate_all_functions() emitted. const std::vector& alias_body_decls() const { return alias_body_decls_; } // Generate the per-function code-range manifest consumed by the overlay // loader's per-entry validity hash (design §8). For each function, emits its // compiled code byte-ranges (union of basic-block extents), coalesced; // interleaved jump tables / constant pools fall in the gaps and are // excluded — which is what makes the runtime hash stable across reloads. // F one per function (virtual entry address) // R one per coalesced code range (virtual addr) std::string generate_ranges_manifest( const std::vector& functions, const std::map& cfgs); // Exact manifest for the final function/CFG set emitted by the most recent // generate_file() call, including functions synthesized by its split pass. const std::string& last_ranges_manifest() const { return last_ranges_manifest_; } // The final function set and CFGs the most recent generate_file() call // emitted, after its split pass: what a segment variant's direct-edge // closure is planned over (docs/SEGMENT_AWARE_CODE.md §5.4). const std::vector& last_functions() const { return last_functions_; } const std::map& last_cfgs() const { return last_cfgs_; } // Set known functions for this compilation unit (for linking) void set_known_functions(const std::set& functions) { known_functions_ = functions; } // Set annotation table (optional — no-op if not called) void set_annotations(const AnnotationTable* at) { annotations_ = at; } // The segment this compile's code executes in (docs/SEGMENT_AWARE_CODE.md // §5.2): KUSEG 0x00000000, KSEG0 0x80000000 or KSEG1 0xA0000000. Every // program counter the emitter bakes into guest-visible state goes through // runtime_pc(): link values, fetch tags (and the uncached-fetch test), // interrupt resume PCs (future EPCs), CPS exit PCs and continuation keys, // store-PC stamps, the reserved-instruction EPC, the slice resume PC and // dispatch rows. Identity keys (func_/block_ names, .ranges, entry-hook // ids) keep the compile address. The default is the segment of the // image's load address, which is the EXE's link segment (§5.3: the parser // keeps a KUSEG header's segment), so runtime_pc(addr) == addr for every // address in the image. Only a compile of the same bytes for another // segment (a variant, §5.4) sets a different code segment. static constexpr uint32_t kSegmentMask = 0xE0000000u; static constexpr uint32_t kPhysMask = 0x1FFFFFFFu; void set_code_segment(uint32_t seg) { code_seg_ = seg & kSegmentMask; } uint32_t code_segment() const { return code_seg_; } uint32_t runtime_pc(uint32_t compile_addr) const { return code_seg_ | (compile_addr & kPhysMask); } // A jump-table case whose value (the PC the jr reaches) names the same // bytes as `image_addr` in another segment than this body's. Such a case // is not a local edge: the body bakes its own segment's PCs, so the value // is dispatched with its full PC instead (§5.4). Never true when the table // holds this body's own segment, which is every title's home compile. bool foreign_segment_case(uint32_t case_value, uint32_t image_addr) const { return (case_value & kPhysMask) == (image_addr & kPhysMask) && case_value != runtime_pc(image_addr); } private: const PS1Executable& exe_; CodeGenConfig config_; uint32_t code_seg_; // set from the image in the constructor; see set_code_segment() std::set known_functions_; // Addresses of functions in this compilation unit std::string last_ranges_manifest_; std::vector last_functions_; std::map last_cfgs_; std::set extra_labels_; // Mid-block addresses that need inline labels (jump table targets) const AnnotationTable* annotations_ = nullptr; // Forward declarations for every psx_alias_body_XXXXXXXX() emitted by // generate_alias_group() during the most recent generate_all_functions() // pass. These bodies are externally linked (split-TU build) so every // shard/header that calls into another shard's alias body needs the // prototype. Cleared at the top of generate_all_functions(). std::vector alias_body_decls_; // The functions emitted by the most recent generate_file() call, stashed // for main_psx's split-shard writer. See last_gen_funcs(). std::vector last_gen_funcs_; // Emit the runtime-hook extern prologue (debug_server_log_call_entry ... // g_debug_last_store_pc). Shared verbatim between the monolith path // (generate_file) and the split-shard shared header // (build_shared_decls_header). void emit_runtime_externs(std::ostream& ss) const; // Emit the LWL/LWR/SWL/SWR unaligned-access reference helpers. // as_inline selects `static inline` (shared header, included by many // TUs) vs `static` (monolith, single TU) linkage for the 4 helper // function definitions; everything else is identical. void emit_unaligned_helpers(std::ostream& ss, bool as_inline) const; // RECURSION_BUG.md §25 — continuation-passing call/return (the universal fix // for the idle-freeze host-stack leak). When set (gen-time env PSX_CPS), // guest calls (jal/jalr/jr-table/external) are emitted as TAIL-TRANSFERS // (set $ra, cpu->pc = target, return) and each call's return point is // registered as a dispatchable continuation; jr $ra publishes cpu->pc. // The unified flat trampoline (psx_dispatch_impl) drives the whole // call/return via the guest's own $ra/$sp — so the host stack can never // mirror unbounded guest re-entrancy. The §8 leaking chain (func_8001A954 // ...) lives in GAME code, so the game emitter MUST honor CPS too. bool cps_enabled_ = false; // Continuations (return points) collected during the CURRENT function's // block translation; consumed by generate_function to emit the entry-switch. std::vector cps_cur_continuations_; // Global map: continuation address -> owning function entry, for the game // dispatch table (psx_dispatch_game_compiled) to route a returned-to // continuation into its function's entry-switch. std::map cps_continuation_owner_; // Cycle accounting for legal entries that land inside an already-emitted // block instead of at its leader. Block leaders charge the whole block; an // entry-switch or jump-table target that bypasses the leader must charge the // remaining instructions from the interior label to the end of that block. uint32_t partial_block_cycle_count(uint32_t addr, const ControlFlowGraph& cfg) const; std::string emit_mid_block_cycle_charge(uint32_t addr, const ControlFlowGraph& cfg, const std::string& indent) const; std::string emit_interrupt_check(uint32_t resume_pc, const std::string& indent) const; std::string emit_interrupt_check_expr(const std::string& resume_pc_expr, const std::string& indent) const; public: bool cps_enabled() const { return cps_enabled_; } // continuation_addr -> owning function entry (CPS dispatch routing). const std::map& cps_continuations() const { return cps_continuation_owner_; } private: // Set per-function by generate_function when config_.ws_auto_screen_x_cull is // on AND the function carries the GTE screen-extent reject signature; read by // translate_instruction to widen that function's width compares. See // func_has_screen_extent_cull(). bool ws_auto_cull_func_ = false; // LEFT-edge funnel bltz addresses in the current function (auto_screen_x, // signed min/max + center±halfwidth idioms — ws_cull_detect.h). Populated // per function alongside ws_auto_cull_func_; generate_branch_condition // emits a listed bltz through psx_ws_cull_bltz. Cleared per function. std::set ws_cull_bltz_pcs_; // True iff the function's instruction stream contains both a screen-width // reject (`sltiu …,0x140` or `…,0x141`) and a screen-height reject // (`sltiu …,0xE0` or `…,0xF1`) — the uniform GTE per-vertex trivial-reject. bool func_has_screen_extent_cull(const ControlFlowGraph& cfg) const; // Backdrop-preload rewrite sites for the CURRENT function ([widescreen.cull] // auto_backdrop). Maps each detected window-bound instruction address to // {kind (WS_BD_START / WS_BD_END), window_cols (|addiu offset|, the widen // margin scale)}. Repopulated per function by detect_backdrop_windows() // (gated on config_.ws_auto_backdrop_preload) and consulted by // translate_instruction; cleared every function so it never leaks. std::map> ws_backdrop_sites_; // Populate ws_backdrop_sites_ from the shared psx_ws_find_backdrop_windows() // detector over this function's instruction words; returns the window count. int detect_backdrop_windows(const ControlFlowGraph& cfg); // Populate ws_cull_bltz_pcs_ (LEFT-edge funnel bltz addresses) for the // current function; no-op unless ws_auto_cull_func_ is set. int detect_cull_bltz_sites(const ControlFlowGraph& cfg); // Instruction translation std::string translate_instruction(uint32_t addr, uint32_t instr); std::string translate_instruction_core(uint32_t addr, uint32_t instr, const std::string& memory_addr = "", uint32_t original_instruction = 0); // The [[draw_distance.clamp]] statement for (addr, instr), or "". std::string draw_distance_clamp_prefix(uint32_t addr, uint32_t instr) const; std::string mod_instruction_call(uint32_t addr, uint32_t instr) const; // The [[mod_immediate.sites]] entry for (addr, instr), or nullptr. In a main // EXE a word mismatch at a listed address is a hard error; overlay code may // hold unrelated words at a shared load address and is left unchanged. const PSXRecompV4::ModImmediateSite* find_mod_immediate_site(uint32_t addr, uint32_t instr) const; // The instruction translated once per selector: `switch (psx_mod_immediate_select(group))` // with case N running the word whose imm16 is values[N-1] and default the original. std::string mod_immediate_translation(uint32_t addr, uint32_t instr, const PSXRecompV4::ModImmediateSite& site); // Register name mapping static std::string reg_name(int reg_num); // Basic block translation std::string translate_basic_block( const BasicBlock& block, const ControlFlowGraph& cfg); // Detect jr jump tables across all blocks of a CFG. Fills extra_labels_ // (mid-block table targets needing inline labels) and out_edges // (jr block -> in-range rom targets), used for alias reachability. void scan_jr_tables( const ControlFlowGraph& cfg, std::map>& out_edges); // Control flow translation // Main-EXE opcode guard for bltz_sites, bgez_sites and branch_keep_sites // (exits 1 on a mismatch; overlay mode keeps vanilla code). void check_explicit_branch_site(uint32_t addr, uint32_t instr) const; std::string generate_branch_condition(uint32_t instr, uint32_t addr); std::string translate_branch(const ControlFlowInstr& cf, uint32_t fall_through); std::string translate_jump(const ControlFlowInstr& cf); // Arithmetic instructions std::string translate_add(uint32_t instr); std::string translate_addu(uint32_t instr); std::string translate_sub(uint32_t instr); std::string translate_subu(uint32_t instr); std::string translate_mult(uint32_t instr); std::string translate_multu(uint32_t instr); std::string translate_div(uint32_t instr); std::string translate_divu(uint32_t instr); std::string translate_mfhi(uint32_t instr); std::string translate_mflo(uint32_t instr); std::string translate_mthi(uint32_t instr); std::string translate_mtlo(uint32_t instr); // Logical instructions std::string translate_and(uint32_t instr); std::string translate_or(uint32_t instr); std::string translate_xor(uint32_t instr); std::string translate_nor(uint32_t instr); // Shift instructions std::string translate_sll(uint32_t instr); std::string translate_srl(uint32_t instr); std::string translate_sra(uint32_t instr); std::string translate_sllv(uint32_t instr); std::string translate_srlv(uint32_t instr); std::string translate_srav(uint32_t instr); // Load/store instructions std::string translate_lw(uint32_t instr, const std::string& memory_addr = ""); std::string translate_sw(uint32_t instr, const std::string& memory_addr = ""); std::string translate_lb(uint32_t instr, const std::string& memory_addr = ""); std::string translate_lbu(uint32_t instr, const std::string& memory_addr = ""); std::string translate_lh(uint32_t instr, const std::string& memory_addr = ""); std::string translate_lhu(uint32_t instr, const std::string& memory_addr = ""); std::string translate_lwl(uint32_t instr, const std::string& memory_addr = ""); std::string translate_lwr(uint32_t instr, const std::string& memory_addr = ""); // // LWL/LWR take their destination register as BOTH input (the merge base) // and output. Unlike every other load, they read that input late enough to // receive the forwarded result of an immediately preceding load, so they // are NOT subject to the load delay: after // // lw $t0, 12($a3) // lwr $t0, 10($a3) // // the LWR merges into the value the LW just fetched, not the stale $t0. // When the pair emitter defers a load's writeback into psx_ldd_, it // sets these so the LWL/LWR merge operand names that temporary instead of // cpu->gpr[rt]. Empty temp = no forwarding in effect. // // (The mirror case -- LWL/LWR as the PRODUCER -- needs nothing: those never // defer, so their writeback is already immediate.) void set_lwlr_merge_forward(uint32_t rt, const std::string& temp) { lwlr_merge_reg_ = rt; lwlr_merge_temp_ = temp; } void clear_lwlr_merge_forward() { lwlr_merge_temp_.clear(); } // Name to use for an LWL/LWR merge operand on register rt. std::string lwlr_merge_operand(uint32_t rt) { if (!lwlr_merge_temp_.empty() && rt == lwlr_merge_reg_) return lwlr_merge_temp_; return reg_name(rt); } uint32_t lwlr_merge_reg_ = 0xFFFFFFFFu; std::string lwlr_merge_temp_; std::string translate_swl(uint32_t instr, const std::string& memory_addr = ""); std::string translate_swr(uint32_t instr, const std::string& memory_addr = ""); std::string translate_sb(uint32_t instr, const std::string& memory_addr = ""); std::string translate_sh(uint32_t instr, const std::string& memory_addr = ""); // Immediate instructions std::string translate_addiu(uint32_t instr); std::string translate_andi(uint32_t instr); std::string translate_ori(uint32_t instr); std::string translate_xori(uint32_t instr); std::string translate_lui(uint32_t instr); // Set instructions std::string translate_slt(uint32_t instr); std::string translate_sltu(uint32_t instr); std::string translate_slti(uint32_t instr); std::string translate_sltiu(uint32_t instr); // Helper: Extract register fields static uint32_t get_rs(uint32_t instr) { return (instr >> 21) & 0x1F; } static uint32_t get_rt(uint32_t instr) { return (instr >> 16) & 0x1F; } static uint32_t get_rd(uint32_t instr) { return (instr >> 11) & 0x1F; } static uint32_t get_shamt(uint32_t instr) { return (instr >> 6) & 0x1F; } static uint32_t get_funct(uint32_t instr) { return instr & 0x3F; } static int16_t get_imm16(uint32_t instr) { return (int16_t)(instr & 0xFFFF); } static uint16_t get_imm16_u(uint32_t instr) { return (uint16_t)(instr & 0xFFFF); } }; } // namespace PSXRecomp