tooling: interp-path cycle ruler — emit cyc_observe from the dirty interp

The dirty-RAM interpreter charged cycles but emitted no cyc_observe, so
interp-executed PCs were not anchorable by cyc_watch — interp-side cycle work
(the muldiv + GTE completion-stalls just added) could only be validated by
construction, not measured. Add a gated debug_server_cyc_observe(pc) at the top
of the interp's per-instruction loop (parity with the compiled emitter's
block-leader observe), so any interpreted PC becomes a cyc_watch anchor. Gated
#ifndef PSX_NO_DEBUG_TOOLS (zero prod overhead); early-returns when cyc_watch is
disarmed.

Validated: Tomba2 boots to FMV no-regression (safe over 150M+ interp insns); a
live overlay interp loop (0x8010724C) records 10 stable cyc_watch hits at 38
cycles/iter, parity with compiled-PC anchoring. This is the interp-path ruler
enabler; a fully isolated single-component interp loop (cyctest dirty-RAM loop)
remains TODO — the cyctest harness does not route indirect jumps to the dirty
interp (dirty_ram_insns=0 there), so that fixture needs interp-dispatch wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-27 12:59:37 -07:00
parent 2df9b84b34
commit fc85d8b34d
+9
View File
@@ -1866,8 +1866,17 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
enum { MAX_INSNS_PER_DISPATCH = 1000000 };
uint32_t pc = addr;
int insns_executed = 0;
#ifndef PSX_NO_DEBUG_TOOLS
extern void debug_server_cyc_observe(uint32_t block_leader_phys);
#endif
for (int i = 0; i < MAX_INSNS_PER_DISPATCH; i++) {
uint32_t next_pc = 0;
#ifndef PSX_NO_DEBUG_TOOLS
/* Interp-path cycle ruler: make every interpreted PC anchorable by
* cyc_watch (parity with the compiled emitter's block-leader observe).
* Early-returns when cyc_watch is disarmed → ~free in normal runs. */
debug_server_cyc_observe(pc & 0x1FFFFFFFu);
#endif
uint32_t insn = fetch_word(pc & 0x1FFFFFFFu);
uint32_t before_s0 = cpu->gpr[16];
uint32_t before_ra = cpu->gpr[31];