Files
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
..
2026-07-21 21:38:04 -07:00
2026-07-17 03:54:27 -05:00
2026-05-17 21:58:55 -07:00
2026-07-21 14:29:38 -07:00
2026-07-21 21:38:04 -07:00
2026-07-21 21:38:04 -07:00
2026-07-21 21:38:04 -07:00