/* func_override.h — replace a guest function with hand-written C. * * WHY THIS EXISTS * * A recomp turns the game into C, but that C is a build artifact: regenerated * from the player's own disc, never hand-edited (CLAUDE.md rule 4). So there * is no supported way to say "this function should do something else" — which * is the single most important thing a port needs to grow beyond a faithful * replay of the original. * * This is that mechanism. Register hand-written C against a guest address and * the dispatcher calls yours instead of the recompiled original. * * NO SIZE LIMIT. This is not a ROM patch; there is no slot to fit inside, no * code cave, nothing to shift. Your replacement is native code in a native * binary — five instructions or five thousand lines. That freedom is the * entire point of recompiling rather than hacking bytes. * * WHERE IT HOOKS, AND WHY IT MATTERS * * The hook is consulted in psx_dispatch_impl after the BIOS tiers (so an * override can never shadow a kernel service vector) and BEFORE every game * code backend. That placement is deliberate and load-bearing: one * address-keyed hook covers the STATIC EXE, runtime-loaded OVERLAYS and * DIRTY RAM alike. The interpreter's JAL/JALR call-resolution tiers consult * it on the SAME terms — before enter-compiled, before overlay-native and * before the local pc-chain (the reserved CRES_OVERRIDE slot) — so no call * the interpreter resolves itself can bypass an override. Ordering there is * not cosmetic: interp_enter_compiled calls psx_dispatch_game_compiled -> * entry->fn(cpu) directly and never re-enters psx_dispatch_impl, so a hook * placed after it is unreachable for any override on a statically-compiled * function reached from interpreted code, while registration and the * func_override inventory both still look healthy. * * SCOPE: overrides are CALL-site keyed. A guest tail transfer (j / jr) into * an overridden address from the dirty-RAM block loop is a TRANSFER, not a * call, and does not consult the hook — see dirty_ram_interp.c's compiled * handoff sites. Registering on a function only ever entered by tail * transfer will show calls == 0. * * THE CONTRACT * * Your function receives the CPUState and must obey the guest ABI: * * int my_impl(CPUState *cpu) { * uint32_t a0 = cpu->gpr[4], a1 = cpu->gpr[5]; // arguments * ... * cpu->gpr[2] = result; // $v0 = return value * return 1; * } * * Return 1 and the guest resumes at $ra exactly as if the original had run * `jr $ra`. Return 0 and the original recompiled or interpreted code runs * untouched, so an override can decline case by case (a conditional * pre-hook: mutate state, return 0, original runs). * * POINTERS MUST BE GUEST ADDRESSES. The caller is recompiled too: when it * dereferences your return value it performs a guest load. A host pointer * will read garbage. If you need to hand back a buffer, write it into guest * RAM (psx_write_byte and friends, or psx_mod_alloc_guest_memory) and return * that address. * * DETERMINISM. Keep ALL mutable state in guest RAM. Rollback/rewind and * netplay snapshot guest state only; host-side statics in an override * desync replays. Reading host config that never changes mid-session is * fine. * * CYCLE ACCOUNTING — REQUIRED AT REGISTRATION. * On hardware the replaced function took guest time, and every device in * the machine schedules against that clock; a replacement that takes zero * cycles shifts IRQ phase for the rest of the session. So every * registration MUST state its timing intent via the `credit` argument — * there is no default to inherit. Two forms: * * credit >= 0 The tier charges exactly `credit` guest cycles * (psx_advance_cycles) on every HANDLED call, after * the body runs. Declines charge nothing — the * original runs and accrues its own cycles. 0 is * legal and means "deliberately free": right for a * player-toggled mod whose behavior has no hardware * analog, indefensible for an always-on * reimplementation claiming to be indistinguishable. * * FO_CREDIT_SELF The tier charges nothing; the BODY owns its timing * and calls psx_advance_cycles() itself. Use this * when the original's cost is data-dependent * (charge base + per_iteration * n, the bios_hle.c * TIMING NOTE pattern), and ALWAYS for wraps: the * original re-dispatched by func_override_call_original * accrues its exact cycles by executing, so a fixed * nonzero credit on a wrap double-counts. Guest code * invoked via func_override_guest_call likewise * self-charges — credit only the code you REPLACED. * * Credits are approximations of the original's dynamic instruction count * unless measured; LLE remains the timing oracle (run with the override * declined and sample the cycle delta across the call boundary — the * callret ring — to measure a real distribution). See FAITHFUL_TIMING_PLAN. * * WHAT IT DOES NOT DO * * It does not lift the game's own assumptions. If the caller copies your * result into a fixed-size field, that field is still fixed. The override * removes the code limit, not every limit. */ #ifndef PSXRECOMP_FUNC_OVERRIDE_H #define PSXRECOMP_FUNC_OVERRIDE_H #include #include struct CPUState; #ifdef __cplusplus extern "C" { #endif #define FO_MAX_OVERRIDES 128 #define FO_MAX_ID 64 #define FO_MAX_GUARD_WORDS 4 /* `credit` value: the override body states its own timing by calling * psx_advance_cycles() itself (mandatory for wraps — see CYCLE ACCOUNTING * above). Any other negative value is FO_ERR_ARGS. */ #define FO_CREDIT_SELF (-1) /* func_override_add return codes. */ #define FO_OK 0 #define FO_ERR_FULL 1 #define FO_ERR_ARGS 2 #define FO_ERR_DUPLICATE 3 /* An override implementation. Reads arguments from cpu->gpr[4..7], writes * the return value to cpu->gpr[2]. Returning nonzero means "handled"; * returning 0 declines this call and the original code runs instead. */ typedef int (*FuncOverrideFn)(struct CPUState *cpu); /* Register `fn` for guest address `addr` (KSEG or physical — normalised). * `id` is copied, used for diagnostics, and may be NULL. Registering the * same address twice is an error rather than a silent last-wins, because a * silent shadow is impossible to debug. `credit` is the guest-cycle policy * (CYCLE ACCOUNTING above): >= 0 charged per handled call, or * FO_CREDIT_SELF when the body charges its own. */ int func_override_add(const char *id, uint32_t addr, FuncOverrideFn fn, int32_t credit); /* Like func_override_add, plus a residency guard: before each consult the * first `n_words` guest words at `addr` are compared against * `expected_words`; on mismatch the override silently declines (the guard * miss is counted, see func_override_get_ex). Use this when the address * belongs to overlay/dirty RAM that other code may occupy at other times — * the guard makes "wrong code resident" a decline instead of a corruption. * n_words must be 1..FO_MAX_GUARD_WORDS. */ int func_override_add_guarded(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *expected_words, int n_words, int32_t credit); /* Call a guest function from inside an override (or any native code on the * dispatch thread): args already placed in cpu->gpr[4..7], returns after the * callee completes; result in cpu->gpr[2]. `site_ra` is the return address * the callee should observe in $ra — pass the original call site's return * address when re-creating a call the original code would have made, so * callees, fntrace and crash forensics see authentic values. $ra is * restored around the call. */ void func_override_guest_call(struct CPUState *cpu, uint32_t target, uint32_t site_ra); /* Run the ORIGINAL code of the function currently being overridden — the * wrap primitive. Callable only from inside an executing override; arms a * one-shot bypass for this address, re-dispatches it, and returns after the * original completes (result in cpu->gpr[2]). Typical post-hook: * * int wrap(CPUState *cpu) { * func_override_call_original(cpu); // original runs fully * ...inspect / adjust results... * return 1; // we handled the call * } * * The bypass is one-shot per invocation: if the original recursively calls * itself, the recursive calls consult the override again (matching what a * guest-level wrap would observe). */ void func_override_call_original(struct CPUState *cpu); /* Register on behalf of a mod package plan. Identical to the two calls above * (pass n_words 0 for unguarded) except the entry is tagged package-armed, so * func_override_reset_package_armed can drop it when the plan is cleared. * The mod runtime uses this; game code registering its own always-on * reimplementations should use func_override_add/_add_guarded. */ int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *expected_words, int n_words, int32_t credit); /* Scoped package variant. The owner supplies a stable plan context and * invokes fn inside that plugin's resource/completion context. The plan must * disarm package entries before replacing that context. */ typedef int (*FuncOverrideInvoker)(struct CPUState *cpu, FuncOverrideFn fn, const void *context); int func_override_add_package_scoped(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *expected_words, int n_words, int32_t credit, FuncOverrideInvoker invoke, const void *context); /* Nonzero while a native override stack is live, including nested guest * calls. Save/load and rewind must defer until this returns to zero. */ int func_override_active(void); /* Install the dispatcher hook. Call once at startup AFTER registering (the * mod runtime calls it again after arming package-gated overrides — safe). * Cheap with nothing registered — it leaves the hook NULL, so dispatch is * byte-identical to a build without the tier. */ void func_override_install(void); /* Drop every package-armed override, keep direct registrations, re-install * (so the hook goes back to NULL if the table empties). Returns how many were * dropped. THIS IS REQUIRED FOR CORRECTNESS, not a convenience: a cleared mod * plan stops activation/vblank callbacks simply by no longer being iterated, * but an armed override lives in this module's own table, so without an * explicit reset it keeps firing after the plan is gone — including after * netplay's clear-mods announces a vanilla session, which is a peer * divergence. Direct registrations are deliberately kept: they are the game's * own always-on faithful reimplementations, present identically in both * peers' builds and not player-selectable. */ int func_override_reset_package_armed(void); /* Introspection, surfaced by the `func_override` TCP command. An override * whose `calls` stays 0 was never reached: wrong address, or that code path * never ran. `guard_misses` counts consults declined by the residency * guard. `credit_out` reports the declared cycle policy (FO_CREDIT_SELF or * the fixed per-call charge), so every armed override's timing intent is * inspectable, not just documented. */ int func_override_count(void); int func_override_get(int index, char *id_out, size_t id_cap, uint32_t *addr_out, uint64_t *calls_out); int func_override_get_ex(int index, char *id_out, size_t id_cap, uint32_t *addr_out, uint64_t *calls_out, uint64_t *guard_misses_out, int *guarded_out, int32_t *credit_out); #ifdef __cplusplus } #endif #endif /* PSXRECOMP_FUNC_OVERRIDE_H */