mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
242 lines
12 KiB
C
242 lines
12 KiB
C
/* 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 <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
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 */
|