diff --git a/ACCURACY_BURNDOWN.md b/ACCURACY_BURNDOWN.md index 73179c9b..432b47c6 100644 --- a/ACCURACY_BURNDOWN.md +++ b/ACCURACY_BURNDOWN.md @@ -84,14 +84,27 @@ Status: STRONG (recompiler core; proven byte-identical to interp on many funcs). ## Axis 2 — Cycle/timing ← ACTIVE (see FAITHFUL_TIMING_PLAN.md) Status: Stage-1 (1 cycle/insn, single-source seam in place); Stage-2 in progress. +Oracle model fully transcribed in `CYCLE_MODEL_BEETLE.md`. Game-independent +BIOS-kernel ruler BUILT (region [0x80001C5C→0x80001CA4]); per-block-leader cycle +observe added to the recompiler so ANY block leader is anchorable on both backends. - [x] Single-source `psx_instr_base_cycles` seam (identity), both backends. -- [ ] Mult/div latency — Beetle cpu.cpp MULT_Tab/muldiv; psx-spx. +- [x] cyc_watch dispatcher+prologue double-fire dedupe (was corrupting native Δ). +- [x] Per-block-leader observe (native now samples like Beetle, not only fn-entry). +- [x] **Load double-count FIXED**: Stage-2 #1a put +2 data-access in + psx_instr_base_cycles while memory.c already charged +6/main-RAM-load → counted + twice. Reverted opcode fn to pure execute base; memory.c is the single address- + keyed owner. Ruler [c5c→ca4]: native 34→30 (exact), no FMV regression. +- [ ] Memory wait-state CALIBRATION: memory.c flat +6/RAM-read vs Beetle ReadMemory + (region wait + completion +2/+1 − load-delay absorb; scratchpad=0). Δ-gate. +- [ ] Mult/div latency — Beetle cpu.cpp MULT_Tab/muldiv; psx-spx. (Ruler shows the + unmodeled divu→mflo stall is the dominant remaining −26 in [c5c→ca4].) - [ ] GTE per-command cycles — Beetle gte.cpp GTE_Instruction; DuckStation; psx-spx. -- [ ] Memory wait-states by region (RAM/BIOS-ROM/scratchpad/MMIO) — Beetle - ReadMemory/PSX_MemRead timing; psx-spx "Memory Control". -- [ ] Instruction-fetch / I-cache timing — Beetle ReadInstruction; psx-spx. -- [ ] Validation: native cumulative cycles == Beetle at convergence (needs the - comparator above). +- [ ] Instruction-fetch / I-cache timing — Beetle ReadInstruction (+0 hit / +4 KSEG1 + / +3+refill miss). Ruler's 56→84 cold spread is the I-cache line-refill transient. +- [ ] **HW test-ROM ruler (#2)** — Amidog CPU/GTE timing ROMs for hand-crafted + single-COMPONENT isolation (div-only, load-only loops) that organic BIOS code + can't give (the prologue combines div+loads in one block). Strongest validator. +- [ ] Validation: native cumulative cycles == Beetle == analytic on the rulers. ## Axis 3 — Interrupt / event timing diff --git a/CYCLE_MODEL_BEETLE.md b/CYCLE_MODEL_BEETLE.md new file mode 100644 index 00000000..3af721b8 --- /dev/null +++ b/CYCLE_MODEL_BEETLE.md @@ -0,0 +1,136 @@ +# R3000A Cycle Model — extracted from the Beetle/Mednafen oracle + +Companion to FAITHFUL_TIMING_PLAN.md (§3b/§3c) and ACCURACY_BURNDOWN.md (axis 2). +This is the **ground-truth model** our shared `psx_instr_base_cycles` (+ the +memory-path wait-state charger) must reproduce. Every number here is transcribed +**verbatim from the in-tree oracle** `psxrecomp/beetle-psx/mednafen/psx/cpu.cpp` +(main checkout) with the exact line cited, cross-referenced against nocash psx-spx. +These are HARDWARE FACTS (cycle counts), not GPL-protected expression — we +re-implement the model in our own code, we do not paste Beetle's code. + +> Method (CLAUDE.md §15, Rule -1): a number here is only trusted once it has been +> VALIDATED at runtime via the cyc_watch Δ gate (native==Beetle==analytic on a +> clean ruler). Until then it is a transcription HYPOTHESIS. + +--- + +## The accumulator + +Beetle accumulates a single per-slice `pscpu_timestamp_t timestamp`. The cyc_watch +oracle samples it **at block entry, BEFORE the anchor instruction executes** +(cpu.cpp:748-749), identical to the native capture point. So the Δ between two +consecutive same-anchor hits (or a two-anchor A→B region) is the total guest +cycles charged for exactly the code between them. THIS is what we match. + +## 1. Per-instruction base cost (cpu.cpp:795-798) + +``` +if (ReadAbsorb[ReadAbsorbWhich]) ReadAbsorb[ReadAbsorbWhich]--; // absorb a pending load-delay +else timestamp++; // otherwise +1 +``` + +- **Base = +1 per retired instruction.** This is the Stage-1 identity model we + already have. +- BUT a pending **load-delay absorb** (see §3) is consumed *instead* of charging + the +1. So an instruction that reads a just-loaded register can be effectively + free. This is the pipeline overlap; it makes naive "loads cost +2" an + OVER-count when the result is consumed immediately. + +## 2. Instruction fetch / I-cache (cpu.cpp:534-601, ReadInstruction) + +Charged into `timestamp` at fetch time, BEFORE the base +1. Depends on the +address region and I-cache state: + +- **I-cache HIT** (`ICache[idx].TV == address`): **+0**. (Steady-state of any loop + living in cached RAM after its first pass.) +- **Cache-DISABLED** — `address >= 0xA0000000` (KSEG1) or BIU bit 0x800 clear: + **+4 every fetch** (cpu.cpp:550; "approximate best-case cache-disabled time, + can be 5 in some nop runs"). **BIOS ROM at 0xBFC00000 is KSEG1 → +4/fetch + always.** Game/kernel code relocated to KSEG0/KUSEG RAM is cached. +- **Cache-ENABLED MISS** (cached RAM, line tag mismatch): **+3** (cpu.cpp:565) + then **+1 per word refilled** from the missing word index to the end of the + 4-word (16-byte) line (fall-through switch cpu.cpp:568-595). So a miss on: + - word 0 of the line → +3 +4 = **+7**, then next 3 fetches in that line HIT (+0) + - word 1 → +3 +3 = +6 ; word 2 → +5 ; word 3 → +4. + Average over a long straight cached run ≈ (7+0+0+0)/4 ≈ **+1.75/instr**; a tight + loop fully resident in cache after warm-up → **+0/instr**. + +Recompiler consequence: fetch is a per-PC/region constant we can fold into the +per-block charge (cache-enabled steady state for RAM; flat +4 for KSEG1 ROM +blocks). The cached-miss warm-up is a per-line transient — model the steady state, +account the warm-up only where a region is entered cold. + +## 3. Data loads — ReadMemory (cpu.cpp:364-451) + +For a load instruction (LB/LH/LWL/LW/LBU/LHU/LWR; LWC2 = LWC timing): + +1. **Scratchpad** `0x1F800000..0x1F8003FF` (cpu.cpp:414-422): early return, + `LDAbsorb=0`, **+0**. Scratchpad is the D-cache — fast, no stall. +2. Otherwise: `timestamp += (ReadFudge>>4)&2` (cpu.cpp:424) — a **+0 or +2 + read-after-load "fudge"** (penalty when the previous op set ReadFudge). +3. Region wait-state: `PSX_MemRead*(lts, address)` advances `lts` by the bus + wait-state for that region (main RAM, BIOS ROM, MMIO each differ — see the bus/ + memory map; main RAM is the fast case). +4. `lts += (LWC_timing ? 1 : 2)` (cpu.cpp:442-445) — the load completion cost + (**+2** normal CPU load, **+1** LWC2/GTE load). +5. `LDAbsorb = lts - timestamp` (cpu.cpp:447) — the whole data-access cost becomes + the **load-delay absorb**: it is handed to `ReadAbsorb[rt]` in DO_LDS + (cpu.cpp:800), so the next instruction(s) that read `rt` consume it via §1 + instead of charging their own +1. Net: a load costs (fudge + region + 2), but up + to that many following dependent-instruction +1's are absorbed back. + +> Our current `psx_instr_base_cycles` charges a flat +3 for CPU loads (1 base + 2), +> +2 for LWC2. That captures step 4 only — it MISSES scratchpad=+0 (over-counts), +> the region wait-state (under-counts non-RAM), the ReadFudge (+0/2), and the +> absorb give-back (over-counts when the load result is used immediately). Those +> are the next Δ-gated refinements; some push native UP, some DOWN. + +## 4. Stores — WriteMemory (cpu.cpp:453+) + +Stores are **POSTED**: WriteMemory adds **no CPU stall** to `timestamp` (scratchpad +and RAM both just write). So a store = the +1 base only. (Bus-level write latency +is hidden behind the write buffer.) + +## 5. Mult/Div latency (cpu.cpp: MULT_Tab24 ~L101, muldiv_ts_done ~L154, 724/758/764) + +MULT/DIV set a **completion timestamp** `muldiv_ts_done`; a later **MFHI/MFLO +stalls** until then (does not block other instructions). Documented latencies: +- **MULT/MULTU**: ~6–13 cycles by operand magnitude (`MULT_Tab24` lookup on the + leading bits of the multiplier). +- **DIV/DIVU**: ~**36** cycles (fixed). +Model: a small latency attached to the mult/div op + a stall-on-read at MFHI/MFLO +if the consumer arrives before `muldiv_ts_done`. (In straight code with enough +spacing the stall is hidden.) + +## 6. GTE / COP2 per-command (gte.cpp GTE_Instruction ~L1713) + +Each COP2 op returns its own cycle count; GTE sets `gte_ts_done` and a COP2 read +stalls until then (cpu.cpp:1334/1369 set LDAbsorb from `gte_ts_done`). Well-known +table (psx-spx; verify each against the gte.cpp op-fn return before trusting): +RTPS=15 RTPT=23 MVMVA=8 SQR=5 OP=6 AVSZ3=5 AVSZ4=6 NCLIP=8 NCDS=19 NCDT=44 +NCCS=17 NCCT=39 NCS=14 NCT=30 CC=11 CDP=13 DPCS=8 DPCT=17 DCPL=8 INTPL=8 +GPF=5 GPL=5. + +## 7. Branches / jumps + +No extra base cost beyond §1 (+1) and the fetch of the delay-slot instruction. +The R3000 has no branch-likely; the delay slot always executes and is charged its +own +1 (this is the −8 delay-slot-ownership class already fixed in our emitter). +Exception entry has its own path (Exception()). + +--- + +## What our model currently has vs. this + +| Component | Beetle (this doc) | Our psx_instr_base_cycles | Gap | +|------------------|--------------------------------------|--------------------------------|-----| +| base/instr | +1 (minus absorb) | +1 | absorb give-back not modeled | +| CPU load | fudge + region + 2, becomes absorb | flat +3 | scratchpad/region/fudge/absorb | +| LWC2 load | region + 1, becomes absorb | +2 | same | +| store | +0 (posted) | +1 (the base) — OK | — | +| fetch / I-cache | +0 hit / +4 KSEG1 / +3+refill miss | not modeled (0) | the big RAM-cached vs ROM lever | +| mult/div | mult 6-13, div 36, stall-on-read | not modeled (+1) | yes | +| GTE per-command | per-op table 5..44 | not modeled (+1) | yes | + +Implement one row at a time, each Δ-gated on a clean ruler (BIOS leaf fn + +HW test ROM), each verified native==Beetle==analytic, FMV-no-regression, commit. diff --git a/FAITHFUL_TIMING_PLAN.md b/FAITHFUL_TIMING_PLAN.md index e058a402..6d03572f 100644 --- a/FAITHFUL_TIMING_PLAN.md +++ b/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,50 @@ on a fixed region -> next. ## 5. Status / Log (update every session) +- **2026-06-26 (RULER #1 BUILT + load double-count bug found & fixed):** Built the + game-independent BIOS-kernel cycle ruler the §3c "TOOLING NEXT" called for, and + it immediately paid off. Details: + - **New oracle-model doc `CYCLE_MODEL_BEETLE.md`** — transcribed the full R3000A + cycle model verbatim from in-tree Beetle cpu.cpp (base +1/insn minus load-delay + absorb; I-cache fetch +0 hit / +4 KSEG1 / +3+refill miss; ReadMemory loads + scratchpad=0/region-wait+2, posted stores; mult 6-13 / div 36 stall-on-MFHI/LO; + GTE per-command table). This is the calibration ground truth. + - **cyc_watch double-fire FIXED** (debug_server.c): observe was called from BOTH + the dispatcher (trace_dispatch) AND the function prologue (log_call_entry) at the + same cycle → every dispatched entry double-recorded. Added (phys,cycle) dedupe. + Real tooling bug (Rule 15); native deltas were corrupted before this. + - **Per-block-leader cycle observe ADDED** (full_function_emitter.cpp → + debug_server_cyc_observe, #ifndef PSX_NO_DEBUG_TOOLS so prod = zero overhead). + Native previously observed only at FUNCTION ENTRIES; now it samples at EVERY + compiled block leader, matching Beetle's before-every-instruction sample. This + lets cyc_watch anchor ANY block-leader PC (interior loop tops, prologue exits) → + a clean KNOWN-instruction region on both backends. Emitted at normalize_address() + (runtime phys) so relocated-kernel anchors match. + - **THE RULER:** BIOS kernel EvCB-search fn at guest 0x80001C5C (relocated ROM, + identical in every PSX title). Region [0x80001C5C→0x80001CA4] (18-insn prologue, + contains divu+mflo + 2 RAM loads, no MMIO/GTE). Both backends now record it. + - **BUG FOUND — loads double-counted.** Native [c5c→ca4] = 34, fully decomposed: + block c5c advance 20 (=14×1 + **2 loads×3**) + block c9c 2 + **memory.c +6×2 = 12** + = 34. The Stage-2 #1a commit (2ef47bd) made psx_instr_base_cycles return 3 for + loads (+2 data-access) WHILE memory.c's charge_main_ram_read already charged +6 + per main-RAM read — the load data-access cost was counted TWICE. The opaque + 0x80017FC4 window hid this because the load over-charge masked the entirely- + unmodeled divu→mflo stall (~30 cyc Beetle, 0 native). **FIX:** reverted + psx_instr_base_cycles to pure execute base (loads=1), per the header's own stated + "data access charged separately in the memory path" contract. memory.c is the + single address-keyed owner (like Beetle's ReadMemory). Regen BIOS+game, rebuild. + - **RESULT:** native [c5c→ca4] 34→**30** (exact: 16+2+12), dead stable. Beetle 56 + steady (84 cold = I-cache line refill). FMV still streams (i_stat 0x8D, no + regression). The remaining −26 gap is now HONEST and decomposed: native is + missing the divu→mflo execute stall, and memory.c's flat +6/load needs Beetle + calibration. NEXT: isolate those two components — the BIOS prologue combines + div+loads in one block (leader anchors can't split them), so the principled next + step is **ruler #2 (HW test ROM, Amidog)** for hand-crafted single-component + isolation loops (div-only, load-only), per the user's "do both rulers / + completeness not convenience" directive. Then Δ-gate each EXECUTE-latency + component (mult/div, GTE) into psx_instr_base_cycles and CALIBRATE memory.c's + wait-state per region. All on wt/tomba2-cycle-audit, uncommitted. + - **2026-06-26 (measure: Beetle cycle clock BUILT + VALIDATED):** Added absolute guest-cycle exposure to the Beetle oracle (MAIN checkout, additive diagnostic): beetle-psx/libretro.cpp accumulates per-frame `timestamp` (CPU->Run slice) into diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index 6893e185..b9f01a87 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -371,6 +371,19 @@ bool FullFunctionEmitter::emit_function( // can service vblank and other hardware interrupts. if (block_leaders.count(addr)) { out += fmt::format("label_{:08X}:\n", addr); + // Per-block-leader cycle observe (cyc_watch ruler). Sampled BEFORE + // this block's cycle advance, so it reports cumulative cycles for + // all PRIOR blocks — matching Beetle's before-instruction sample + // and the cycle_compare.py anchor semantics. Debug-only: prod + // (PSX_NO_DEBUG_TOOLS) emits nothing → zero overhead. + out += "#ifndef PSX_NO_DEBUG_TOOLS\n"; + // Use the NORMALIZED (runtime) phys so anchors match the address + // space the entry hook + cyc_watch use — relocated kernel funcs + // (ROM 0x1FC10000+) run at RAM 0x500+, so the raw ROM addr would + // never match a RAM anchor. + out += fmt::format(" debug_server_cyc_observe(0x{:08X}u);\n", + normalize_address(addr)); + out += "#endif\n"; // Phase 1.0e-d: advance guest cycles for this block. Macro- // gated; when off, generated code matches pre-1.0e-d output. uint32_t bcyc = 0; @@ -1473,6 +1486,9 @@ EmitStats FullFunctionEmitter::emit( full_c += "extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);\n"; full_c += "extern void debug_server_log_call_entry(uint32_t func_addr);\n"; full_c += "extern void debug_server_log_probe(uint32_t pc, CPUState *cpu);\n"; + full_c += "#ifndef PSX_NO_DEBUG_TOOLS\n"; + full_c += "extern void debug_server_cyc_observe(uint32_t block_leader_phys);\n"; + full_c += "#endif\n"; full_c += "extern uint32_t g_debug_last_store_pc;\n"; full_c += "/* Phase 1.0e-d: per-block guest cycle accounting.\n"; full_c += " * Compile generated code with -DPSX_ENABLE_BLOCK_CYCLES=1 to\n"; diff --git a/runtime/include/psx_instr_cost.h b/runtime/include/psx_instr_cost.h index 3006cb63..7e7a2713 100644 --- a/runtime/include/psx_instr_cost.h +++ b/runtime/include/psx_instr_cost.h @@ -36,27 +36,27 @@ extern "C" { * each component verified against Beetle via the cyc_watch DELTA gate * (FAITHFUL_TIMING_PLAN.md §3c). Both backends consume this one function. * - * STAGE 2 component #1 — data-memory access (the dominant ~2x gap): - * Beetle PS_CPU::ReadMemory: a normal CPU load adds `lts += 2` (LWC loads +1); - * stores are POSTED (WriteMemory adds no CPU stall); scratchpad reads add 0. - * So: load opcode = 1 (execute) + 2 (data access) = 3; LWC2 = 2; store/other = 1. - * NOT YET MODELED (later Δ-gated components, will adjust here / in the mem path): - * - scratchpad-vs-RAM region (scratchpad load should be 1, not 3), - * - the read-after-load "fudge" (+0/2) and load-delay ABSORB (next-insn discount), - * - I-cache instruction-fetch timing, mult/div latency, GTE per-command costs. - * This RAM-load base is an upper bound on the load component (no absorb yet); the - * Δ gate shows whether we under/overshoot and what to add next. */ + * DATA-MEMORY ACCESS IS *NOT* CHARGED HERE — it lives in the memory path. + * The runtime already owns the load/store data-access cost address-keyed in + * runtime/src/memory.c (charge_main_ram_read → psx_guest_read_*: +6 cyc per + * main-RAM read wait-state, scratchpad/MMIO/ROM excluded), exactly like Beetle + * charges it in PS_CPU::ReadMemory (region wait + completion), NOT in the + * opcode dispatch. The opcode word alone cannot know the target region + * (RAM vs scratchpad vs MMIO), so a per-opcode load surcharge here is wrong by + * construction and DOUBLE-COUNTS what memory.c already charges. + * + * HISTORY: Stage-2 #1a (commit 2ef47bd) returned 3 for loads / 2 for LWC2 + * ("1 execute + 2 data-access"). The BIOS-kernel cycle ruler (region + * [0x80001C5C→0x80001CA4]) proved this double-counts: native charged the 2 + * loads +2 each HERE *and* +6 each in memory.c (8 over base), reconciling the + * opaque 0x80017FC4 window only because that over-charge masked the entirely- + * unmodeled divu→mflo stall. Reverted to pure execute base (this header's own + * stated contract). The real Stage-2 components are EXECUTE-pipeline latencies + * (mult/div stall, GTE per-command) added here, and CALIBRATING the memory-path + * wait-state (memory.c) per Beetle region — each Δ-gated on the ruler. */ static inline uint32_t psx_instr_base_cycles(uint32_t insn) { - uint32_t op = insn >> 26; - switch (op) { - case 0x20: case 0x21: case 0x22: case 0x23: /* LB LH LWL LW */ - case 0x24: case 0x25: case 0x26: /* LBU LHU LWR */ - return 3u; /* 1 execute + 2 data-access */ - case 0x32: /* LWC2 (GTE load): +1 */ - return 2u; - default: - return 1u; - } + (void)insn; + return 1u; /* pure CPU execute base; data access charged in memory.c */ } #ifdef __cplusplus diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index e861cf2f..8f0816e7 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -2077,22 +2077,40 @@ static uint64_t s_cyc_watch_region_start = 0; * (debug_server_trace_dispatch), the universal function-entry hook * (debug_server_log_call_entry), and the dirty-RAM path * (debug_server_dirty_break_maybe_pause). `block_leader_phys` is physical. */ +/* Dedupe state for the dispatch+prologue double-fire (see below). */ +static uint32_t s_cyc_watch_last_phys = 0xFFFFFFFFu; +static uint64_t s_cyc_watch_last_cycle = 0xFFFFFFFFFFFFFFFFull; + static inline void cyc_watch_observe(uint32_t block_leader_phys) { if (!s_cyc_watch_armed) return; /* disarmed: no cost */ + /* DEDUPE the double-fire: a block reached via the dispatcher is observed + * BOTH by debug_server_trace_dispatch (routing) AND by the function's own + * debug_server_log_call_entry (prologue) — same phys, same cycle, 0 cycles + * apart. That double-records every dispatched entry (a function reached by a + * direct jal fires only the prologue, so it looked like alternating pairs in + * the ring). A genuine re-entry of the same PC is always ≥1 cycle later, so + * keying on (phys,cycle) drops ONLY the spurious double, never a real hit. */ + uint64_t cyc_now = psx_get_cycle_count(); + if (block_leader_phys == s_cyc_watch_last_phys && + cyc_now == s_cyc_watch_last_cycle) { + return; + } + s_cyc_watch_last_phys = block_leader_phys; + s_cyc_watch_last_cycle = cyc_now; + if (s_cyc_watch_end_phys != 0u) { /* ── REGION mode (A..B Δ) ── */ if (!s_cyc_watch_in_region) { if (block_leader_phys == s_cyc_watch_anchor_phys) { - s_cyc_watch_region_start = psx_get_cycle_count(); + s_cyc_watch_region_start = cyc_now; s_cyc_watch_in_region = 1; } } else if (block_leader_phys == s_cyc_watch_end_phys) { - uint64_t now = psx_get_cycle_count(); CycWatchEntry *e = &s_cyc_watch_ring[s_cyc_watch_hits]; e->hit_index = s_cyc_watch_hits; e->pc = block_leader_phys; - e->psx_cycle_count = now - s_cyc_watch_region_start; /* Δ(B-A) */ + e->psx_cycle_count = cyc_now - s_cyc_watch_region_start; /* Δ(B-A) */ s_cyc_watch_hits++; s_cyc_watch_in_region = 0; if (s_cyc_watch_hits >= s_cyc_watch_max_hits) s_cyc_watch_armed = 0; @@ -2108,11 +2126,27 @@ static inline void cyc_watch_observe(uint32_t block_leader_phys) CycWatchEntry *e = &s_cyc_watch_ring[s_cyc_watch_hits]; e->hit_index = s_cyc_watch_hits; e->pc = block_leader_phys; - e->psx_cycle_count = psx_get_cycle_count(); + e->psx_cycle_count = cyc_now; s_cyc_watch_hits++; if (s_cyc_watch_hits >= s_cyc_watch_max_hits) s_cyc_watch_armed = 0; } +/* Exported per-basic-block-leader cycle observer. Emitted by the recompiler at + * EVERY compiled block leader (under #ifndef PSX_NO_DEBUG_TOOLS, so prod builds + * emit nothing — zero overhead) so native's cycle observation matches Beetle's + * (which samples before every instruction). This lets cyc_watch anchor ANY + * block-leader PC — interior loop tops, prologue exits — not just function + * entries, which is required for a clean, KNOWN-instruction-sequence ruler. + * Disarmed cost = one volatile load + return (see cyc_watch_observe). */ +void debug_server_cyc_observe(uint32_t block_leader_phys) { +#ifdef PSX_NO_DEBUG_TOOLS + (void)block_leader_phys; + return; +#else + cyc_watch_observe(block_leader_phys & 0x1FFFFFFFu); +#endif +} + void debug_server_trace_dispatch(uint32_t func_addr) { #ifdef PSX_NO_DEBUG_TOOLS (void)func_addr; @@ -7266,6 +7300,8 @@ static void handle_cyc_watch(int id, const char *json) s_cyc_watch_region_start= 0; s_cyc_watch_max_hits = (uint32_t)n; s_cyc_watch_hits = 0; + s_cyc_watch_last_phys = 0xFFFFFFFFu; /* reset dedupe state per arm */ + s_cyc_watch_last_cycle = 0xFFFFFFFFFFFFFFFFull; memset(s_cyc_watch_ring, 0, sizeof(s_cyc_watch_ring)); s_cyc_watch_armed = 1;