Files
psxrecomp/runtime/include/psx_runtime.h
Matthew Stanley e21fe2d59b coop: swap the actor context instead of redirecting the actor pointer
An extra actor's saved bytes are now copied INTO the storage the game keeps its
player in for the duration of that actor's pass, then copied back out. Every
access then finds the right actor however it is addressed: through a pointer,
absolute, $gp, a pointer some global cached frames ago, or from streamed
overlay code the recompiler never saw.

That deletes [coop] actor_base_sites entirely. Enumerating every instruction
that forms the actor pointer needed per-game knowledge the list could never be
proven complete against, and silently missed whatever it did not cover: on MMX6
damage landed on player 1 until one more site was found by watching a companion
get chewed on off-camera. config_loader now THROWS on the retired key rather
than ignoring it, so a config written against the old model cannot quietly stop
working.

Per-actor state the engine keeps outside the struct is declared with
psx_coop_add_swap_region() / TCP coop_region, so extending the swap set never
needs new generated code. Which regions those are is a measurement, not a
guess: every recorded write now carries the actor context that made it.

Two bugs fixed along the way:

- The replay hook returned to the jal address (delay slot - 4). Only BLOCK
  LEADERS are dispatch entries, and the jal is one only when it happens to sit
  at a previous call's return address. MMX6's sprite-submission stage has its
  jal at 0x80020D00 inside block_80020CF8, so that stage's replay had never
  actually been happening. The generator now emits the enclosing leader.

- Input delivery no longer writes a shared input global behind player 1's back
  (which left player 1 reading zero while a direction was held). Instead
  psx_coop_set_raw_pad() puts each actor's RAW pad where the game's controller
  poll leaves it, inside a declared swap region (enforced), and the engine's own
  replayed pad stage decodes it. That matters because engines do not store the
  pad in SIO bit order: MMX6 remaps every button, so an actor fed SIO-order bits
  reads LEFT as JUMP. Wire order (byteswap of the inverted SIO word) is the
  console's pad transfer format, not a per-game table.

Observability, all found by a measurement lying rather than by an audit:

- Enhancement-scratch stores were outside every write hook, so a RAM-claim
  report said "nothing written here" about a region written every frame.
- The RAM read watch saw only 32-bit reads, so a 16-bit field read with LHU
  produced zero hits, and it carried no PC at all. Block leaders now stamp
  g_debug_last_block, which the .ranges manifest resolves to a function.
- The read watch could only be armed from a boot-time environment variable,
  i.e. decide-what-to-watch-before-the-run. Added a live read_watch command.
- read_ram/write_ram raced the swap: the debug thread landed on whichever actor
  was momentarily resident, which manufactured false conclusions rather than
  noise. psx_coop_observe_ptr resolves an address to its owner's bytes.

Co-op off remains byte-identical: omit the [coop] block and nothing is
generated; leave the runtime unconfigured and every hook is inert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 10:43:44 -07:00

71 lines
3.1 KiB
C

#ifndef PSXRECOMP_PSX_RUNTIME_H
#define PSXRECOMP_PSX_RUNTIME_H
#include "cpu_state.h"
#include "debug_server.h"
#include "interrupts.h"
#include "psx_cycles.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Fix B exception-return escape state + helpers are declared in cpu_state.h
* (the lowest common header the generated BIOS/game both include). */
static inline void call_by_address(CPUState* cpu, uint32_t addr) {
psx_dispatch_call(cpu, addr, cpu->gpr[31]);
}
/* ---- Enhancement scratch RAM (generic, DEFAULT OFF; see runtime/src/memory.c)
*
* Guest-addressable memory that the game can never touch, for enhancements
* that need somewhere to park state. Mapped ABOVE the 4x main-RAM mirror
* (physical >= 0x00800000), a window that is unmapped on real hardware and
* open-bus here — so unlike a "free" hole inside main RAM it cannot be
* overwritten by the game (notably not by CD-DMA stage loads, which is exactly
* what invalidated a hand-picked reservation inside MMX6's heap->stack gap).
*
* Disabled until a game opts in, and the checks live off the main-RAM fast
* path, so a game that never calls configure() is byte-identical.
*
* Guest address = 0x80000000 | phys_base (KSEG0) for the configured window. */
int psx_enh_scratch_configure(uint32_t phys_base, uint32_t len);
void psx_enh_scratch_reset(void);
uint8_t *psx_enh_scratch_ptr(uint32_t *out_base, uint32_t *out_len);
int psx_enh_scratch_contains(uint32_t phys);
/* Host pointer for a guest DATA block in main RAM or the scratch window, or
* NULL if it does not lie wholly inside one. For bulk block moves an
* enhancement makes on its own behalf; guest-visible accesses still go through
* the psx_read / psx_write accessors, which carry the write-trace fan-out. */
uint8_t *psx_guest_block_ptr(uint32_t addr, uint32_t len);
/* ---- Simultaneous extra player actors (generic, DEFAULT OFF; coop.c) -------
*
* For an engine that runs exactly one player actor through a per-frame stage
* pipeline. Each stage declared in [coop] replay_sites is replayed once per
* extra actor, with that actor's saved state SWAPPED INTO the storage the game
* keeps its player in — so every access finds the right actor however it is
* addressed, including from streamed overlay code the recompiler never saw.
*
* Per-actor state the engine keeps outside that struct (an animation object, a
* pad-word global) is declared with psx_coop_add_swap_region() and swapped
* along with it, so extending the swap set never needs new generated code. */
int psx_coop_configure(uint32_t primary_base, uint32_t struct_len,
int companions, uint32_t scratch_phys_base);
int psx_coop_add_swap_region(uint32_t guest_base, uint32_t len);
void psx_coop_set_gate(int (*gate)(void));
void psx_coop_set_gate_state(uint32_t mode_addr, uint8_t mode_val,
const uint32_t *zero_addrs, int zero_count);
void psx_coop_set_input_layout(uint32_t cur_off, uint32_t edge_off, int enabled);
int psx_coop_spawn(int idx);
void psx_coop_despawn(int idx);
void psx_coop_reset(void);
#ifdef __cplusplus
}
#endif
#endif /* PSXRECOMP_PSX_RUNTIME_H */