mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
feat(timing): BIOS-kernel cycle ruler + fix load double-count
Build the game-independent cycle-attribution ruler the plan called for, and
fix two real bugs it exposed.
Tooling (debug_server.c, full_function_emitter.cpp):
- cyc_watch double-fire dedupe: observe fired from BOTH the dispatcher and the
function prologue at the same cycle, double-recording every dispatched entry.
Key on (phys,cycle); a genuine re-entry is always >=1 cycle later.
- Per-block-leader cycle observe (debug_server_cyc_observe), emitted by the BIOS
emitter under #ifndef PSX_NO_DEBUG_TOOLS (prod = zero overhead). Native now
samples at EVERY compiled block leader (matching Beetle's before-instruction
sample), so cyc_watch can anchor any block-leader PC, not just fn entries.
Emitted at normalize_address() so relocated-kernel anchors match the RAM space.
Bug fix (psx_instr_cost.h):
- Loads were double-counted. Stage-2 #1a (2ef47bd) returned 3 for loads (+2
data-access) in psx_instr_base_cycles WHILE memory.c charge_main_ram_read
already charged +6 per main-RAM read. The ruler region [0x80001C5C->0x80001CA4]
decomposed native=34 as 16(base) + 2 + 12(mem) with loads counted twice. The
opaque 0x80017FC4 window hid it because the load over-charge masked the entirely
unmodeled divu->mflo stall. Reverted the opcode fn to pure execute base (loads=1)
per its own stated contract; memory.c is the single address-keyed data-access
owner (like Beetle ReadMemory). Ruler: native 34->30 (exact), FMV unaffected.
Docs: CYCLE_MODEL_BEETLE.md (full R3000A model transcribed from in-tree Beetle
cpu.cpp, cited), plan + burndown updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+19
-6
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user