Files
psxrecomp/recompiler/src/function_discovery.h
Henrique Guedes 6749b826d4 feat: OpenBIOS support — profile-driven BIOS recompilation
Adds PCSX-Redux's MIT-licensed OpenBIOS as a second BIOS the framework can
statically recompile, next to retail SCPH1001. Because OpenBIOS is
redistributable, a game verified against it ships its BIOS with the build —
players supply only a disc, and the launcher BIOS picker disappears.

The mechanism is general, not OpenBIOS-specific: every BIOS image gets a
bios/<STEM>.toml profile that is the single source of truth for its identity
and layout.

Recompiler
- bios/<STEM>.toml profiles: [program.image] (sha256 pin, redistributable
  flag), [recompiler.address_model] (boot-time ROM->RAM copy windows,
  kernel-bless window), install_slots, and [recompiler.runtime_exports]
  HLE anchors (0 = feature structurally unavailable on that BIOS).
- BiosAddressModel (new): validated model built from the profile, replacing
  ~9 hardcoded SCPH1001 window constants that had drifted apart across the
  emitter / discovery / game codegen. The emitted C normalize() derives
  from the same table. from_config rejects overlapping/misaligned windows
  and orderings where sequential folding != first-match. A CTest-registered
  equivalence test sweeps every RAM/ROM address against verbatim copies of
  the old helpers.
- Orphaned delay slots (the OpenBIOS-critical emitter fixes): jr/jalr
  slots are inlined before the jump resolves — previously their side
  effect was silently LOST (e.g. a case-return fragment's addiu $sp pop,
  which unwound a shifted frame and walked the exception handler's IntRP
  cursor into a wild dispatch). Orphaned branches resume at ds+4 on
  not-taken instead of re-executing the slot (a read-modify-write slot
  doubling twice broke OpenBIOS readPad). Jump targets latch pre-slot.
- MIPS-I load-delay modeling for dependent load pairs: the successor sees
  the pre-load value (OpenBIOS's cardfasttrack.s relies on it on purpose —
  "gotta break those bad emulators"). Unmodeled shapes warn at build time.
- Discovery hardening: branch-target dispatchability closure to fixpoint;
  jump tables placed at jr+8 no longer walked as code; cross-function
  continuations routed by an instruction->owner map.
- Signed MULT support (unreachable from the retail seed set; modern-GCC
  OpenBIOS emits it freely), latency-modeled like MULTU.
- Seeds from ELF symbol tables + data-section code-pointer harvest
  (tools/gen_bios_seeds_from_elf.py) — a source-built BIOS needs no
  disassembly pass. 646 OpenBIOS seeds, incl. 5 empirical dispatch-miss
  seeds.
- Stemmed outputs (<out_stem>_full.c / _dispatch.c) so two BIOSes coexist
  in one out_dir. tools/regen_bios.sh is profile-driven (--config).

Runtime
- psx_bios_image: the generated dispatch now self-describes the BIOS it
  was compiled from (identity: size/CRC32/SHA-256/wordsum; kernel-bless
  window; HLE anchors; bundled flag). Runtime SCPH1001 hardcodes removed.
- Launch check is a hard identity gate (a mismatched image under
  statically recompiled code is a guaranteed wild jump). Bundled builds
  hide the BIOS picker and treat a missing/tampered bundled image as an
  installation defect.
- Faithfulness fixes surfaced by OpenBIOS's tighter timing loops (all
  global, argued in code comments, regression-tested against SCPH1001):
  - SIO pad RX is cycle-paced like the memcard path (RX_RDY at
    shift-complete, ACK +170); total ack-IRQ latency unchanged.
    PSX_SIO_PAD_SYNC_RX=1 restores the legacy synchronous path for A/B.
  - SPUSTAT derived from SPUCNT instead of permanently-asserted busy.
  - GPUSTAT.13 (interlace field) alternates per vblank while interlaced,
    pinned 0 in progressive.
  - CD Init(0x0A) completes in ~3.9 ms instead of borrowing Beetle's
    34 ms Reset figure (OpenBIOS's short bounded retry loop livelocked).
  - Exception EPC guard accepts ROM-window block leaders, not just RAM.
- New TCP debug commands (documented in TCP_COMMANDS.md): bios_info,
  s3_smear_watch, callret_watch; write-trace ring gains $s0-$s5.

bios/openbios.bin is vendored (MIT — notice in bios/OpenBIOS.LICENSE,
attribution in THIRD_PARTY_ATTRIBUTION.md, source pin + build recipe in
bios/OpenBIOS.toml). The emitter fingerprint digest is now content-only:
existing generated/*.emitter.sha will read STALE once; re-run
tools/regen_bios.sh.
2026-07-23 17:10:23 -03:00

185 lines
7.2 KiB
C++

// function_discovery.h
// ----------------------------------------------------------------------------
// Phase 1c: deterministic function discovery pipeline.
//
// Given a set of seed addresses and a flat ROM image, discovers functions
// transitively via direct JAL targets. Each function is walked exactly
// once using BFS over basic blocks. Emits structured artifacts:
// - function_manifest.json (per-function metadata)
// - function_edges.json (caller->callee edges)
// - discovery_run.log.json (structured run statistics)
//
// Design constraints (from PLAN.md Phase 1c):
// - Every seed must come from an explicit seed file.
// - Functions are identified by normalized address (addr & 0x1FFFFFFF).
// - No function may be emitted twice under the same normalized address.
// - Indirect jumps (jr non-$ra, jalr) terminate the walk for that path
// and are recorded for Phase 1d. They are NOT resolved.
// - Every instruction is validated through StrictTranslator. An
// unsupported opcode is a hard failure.
// - Direct JAL targets outside the ROM range are recorded as edges
// but NOT walked (they may be RAM-side copies; Phase 1e handles that).
#pragma once
#include <cstdint>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "strict_translator.h"
namespace PSXRecompV4 {
struct Seed {
uint32_t address;
std::string label;
std::string rationale;
};
struct BasicBlockInfo {
uint32_t start_addr;
uint32_t end_addr; // address of last instruction (inclusive)
uint32_t instruction_count;
std::string termination; // "fall_through", "jr_ra", "jr_indirect", "jalr",
// "j_intra", "j_out", "branch", "jal", "rfe",
// "syscall", "break"
uint32_t branch_target; // target address for branches/jumps, 0 if N/A
};
struct DiscoveredFunction {
uint32_t entry_addr;
uint32_t normalized_addr; // entry_addr & 0x1FFFFFFF
uint32_t end_addr; // highest instruction address (inclusive)
uint32_t instruction_count;
std::string termination_reason; // primary termination: "jr_ra", "rfe",
// "j_out_of_function",
// "indirect_jump_recorded_for_1d",
// "mixed" (multiple exit types)
std::string discovered_by; // seed lineage description
std::vector<uint32_t> block_leaders; // sorted basic block start addresses
};
struct CallEdge {
uint32_t caller_func; // entry address of calling function
uint32_t caller_pc; // PC of the JAL instruction
uint32_t callee_addr; // target of the JAL
bool in_rom; // true if callee_addr is within ROM range
};
struct PrecedingInstr {
uint32_t address;
uint32_t raw;
std::string mnemonic;
};
struct IndirectJumpSite {
uint32_t func_addr; // entry address of containing function
uint32_t pc; // PC of the indirect jump
uint8_t reg; // register used (e.g. $t0 for jr $t0)
std::string kind; // "jr_non_ra", "jalr"
std::string classification; // "a0_b0_c0_dispatch", "computed_tail_call",
// "computed_call", "jump_table", "unknown"
std::vector<PrecedingInstr> context_window; // ~16 instructions before the jump
};
struct UnsupportedInstr {
uint32_t address;
uint32_t raw;
std::string reason;
};
struct DiscoveryResult {
bool ok = false;
std::vector<DiscoveredFunction> functions;
std::vector<CallEdge> edges;
std::vector<IndirectJumpSite> indirect_jumps;
std::vector<UnsupportedInstr> unsupported;
// Statistics
uint32_t total_instructions = 0;
uint32_t total_functions = 0;
uint32_t total_edges = 0;
uint32_t total_indirect_jumps = 0;
// Opcode histogram: mnemonic -> count
std::map<std::string, uint32_t> opcode_histogram;
};
class BiosAddressModel;
class FunctionDiscovery {
public:
// Set the active BIOS address model BEFORE any discover/walk call. The
// profile-derived model (bios_address_model.h) is the single source of
// truth for the relocation windows discovery follows J/JAL targets
// through; there is no built-in default. Must outlive the run.
static void set_address_model(const BiosAddressModel* m);
// Discover functions starting from the given seeds.
// rom: flat BIOS image bytes
// base_addr: virtual address of rom[0] (e.g. 0xBFC00000)
// rom_end: last valid address inclusive (e.g. 0xBFC7FFFF)
// seeds: explicit seed addresses with labels
static DiscoveryResult discover(
const std::vector<uint8_t>& rom,
uint32_t base_addr,
uint32_t rom_end,
const std::vector<Seed>& seeds);
// Walk a single function via BFS. Returns the set of visited
// (addr, raw) pairs, basic block info, edges, and indirect jump sites.
// Made public for Phase 2 full function emitter.
struct SingleFunctionResult {
std::vector<std::pair<uint32_t, uint32_t>> instructions; // (addr, raw) sorted
std::vector<BasicBlockInfo> blocks;
std::vector<CallEdge> edges;
std::vector<IndirectJumpSite> indirect_jumps;
std::vector<UnsupportedInstr> unsupported;
std::set<std::string> exit_types; // "jr_ra", "rfe", etc.
uint32_t end_addr = 0;
// Direct branch/J targets that escape [entry, hard_cap) but land in
// ROM. Input to discover()'s dispatchability closure: every published
// target must resolve to a dispatch key, so an escaping target covered
// by no function's instruction set becomes a new entry.
std::set<uint32_t> escaping_targets;
};
static SingleFunctionResult walk_function(
const std::vector<uint8_t>& rom,
uint32_t base_addr,
uint32_t rom_end,
uint32_t entry,
uint32_t hard_cap,
const std::string& lineage);
private:
static uint32_t read_u32_le(const std::vector<uint8_t>& rom, uint32_t offset);
// Control flow classification for a raw instruction at a given address.
enum class CFKind {
Normal, // falls through
CondBranch, // conditional branch (BEQ/BNE/BLEZ/BGTZ/BLTZ/BGEZ/BLTZAL/BGEZAL)
Jump, // unconditional J
Call, // JAL
JR_RA, // jr $ra (function return)
JR_Indirect, // jr $reg (not $ra)
JALR, // jalr (indirect call)
Syscall, // SYSCALL
Break, // BREAK
RFE, // return from exception
};
struct CFInfo {
CFKind kind;
uint32_t target; // computed target for CondBranch/Jump/Call, 0 otherwise
uint8_t reg; // register for JR_Indirect/JALR
};
static CFInfo classify_control_flow(uint32_t raw, uint32_t addr);
};
} // namespace PSXRecompV4