mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
timing: I-cache fetch Stage 2 — compiled-path emit (both emitters) + default-on
Charge the faithful R3000A I-cache fetch cost in BOTH static emitters at each cache-line leader, completing the model (interp-only since Stage 1). A leader is a block leader / mid-block jump-table target (a possibly-cold line entry) OR a 16-byte-line start (addr&0xC==0); intra-line fall-through followers are provable hits (+0) and emit nothing. Emitted BEFORE the per-instruction interlock/load so a fetch miss clears the pending load give-back first (Beetle ReadInstruction order). - code_generator.cpp (game): emit_pre_icache at the 3 timing sites (normal, branch, delay slot); `addr` is already the KSEG0 runtime PC. - full_function_emitter.cpp (BIOS): emit_icache_fetch at all 5 emit_insn_interlock sites (main loop + 4 orphaned delay slots), mapping the ROM/compile addr to the RUNTIME guest PC via relocate_ra (BIOS main stays KSEG1 0xBFC.. uncached; kernel Part 2 -> 0x500+, shell -> 0x80030000+). Same address value the interp passes for the same instruction, so the shared TV array stays consistent. - psx_icache.c: flip psx_icache_enabled() DEFAULT ON (PSX_ICACHE=0 to disable). Validated: ruler #2 compiled (4600) == Beetle (4382) EXACT on all 13 loops incl. icache_miss +14 (was +0); ruler #1 steady delta 0 (56==56) with native cold-refill spikes (77/84) now matching Beetle's range; Tomba 2 boots past the load wedge to the intro FMV + PS BIOS logo, no freeze. Default-on path re-validated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,30 @@ on a fixed region -> next.
|
||||
|
||||
## 5. Status / Log (update every session)
|
||||
|
||||
- **2026-06-27 (I-cache fetch — Stage 2 DONE: compiled-path emit + production default-on):**
|
||||
Both static emitters now charge the I-cache fetch cost at each cache-line LEADER, BEFORE
|
||||
the per-instruction interlock/load (Beetle ReadInstruction order, so a fetch miss clears
|
||||
the pending load give-back first). Leader = a block leader / mid-block jump-table target
|
||||
(any non-fall-through entry → possibly-cold line) OR a 16-byte-line start (addr&0xC==0);
|
||||
intra-line fall-through followers are provably hits (the leader refilled the line to its
|
||||
end) so they emit nothing (+0). code_generator (game): `addr` is already the KSEG0 runtime
|
||||
PC. full_function_emitter (BIOS): the loop addr is the ROM/compile addr, mapped to the
|
||||
RUNTIME guest PC via the existing `relocate_ra` (BIOS main stays in-place KSEG1 0xBFC..
|
||||
uncached; kernel Part 2 → 0x500+, shell → 0x80030000+) so the shared TV array evolves
|
||||
identically to the interp's cpu->pc and the KSEG1 (>=0xA0000000) uncached test sees the
|
||||
true virtual address. The compiled path emits at the SAME address value the interp would
|
||||
for the same instruction (they share s_icache_tv), so mixed compiled/interp stays
|
||||
consistent. VALIDATED: ruler #2 COMPILED (port 4600, PSX_ICACHE=1) == Beetle (4382) EXACT
|
||||
on all 13 loops incl. `icache_miss +14` (was +0 pre-Stage-2); ruler #1 [0x1C5C→0x1CA4]
|
||||
steady delta 0 (56==56) AND native now produces the I-cache cold-refill spikes (77/84,
|
||||
Beetle's range) that were absent before — exact per-hit magnitude varies run-to-run
|
||||
because the two processes free-run (Rule 16), not a bug. Tomba 2 boots past the load wedge
|
||||
→ intro FMV (jungle) + crisp PS BIOS logo, total_checks advancing, no freeze. Then flipped
|
||||
`psx_icache_enabled()` DEFAULT ON (PSX_ICACHE=0 still disables for A/B) — re-validated the
|
||||
default-on path (cyctest no-env == +14; Tomba 2 boots clean). psx_icache.c + both emitters.
|
||||
NEXT axis: DMA cycle-steal / device-region MMIO load waits (SPU +36 etc.), currently
|
||||
unmodeled. Eventually merge wt/tomba2-load-accuracy to master after cross-title regen+smoke.
|
||||
|
||||
- **2026-06-27 (I-cache fetch — MODEL built + interp-validated EXACT; Stage 1 of 2):**
|
||||
New runtime/src/psx_icache.c: faithful direct-mapped (4 KB / 256-line) instruction-cache
|
||||
fetch cost, transcribed from Beetle PS_CPU::ReadInstruction — HIT +0 (no give-back clear),
|
||||
|
||||
@@ -1308,6 +1308,24 @@ std::string CodeGenerator::translate_basic_block(
|
||||
psx_cyc_dep_res_mask(in));
|
||||
ss << "#endif\n";
|
||||
};
|
||||
// I-cache FETCH cost (faithful R3000A), emitted BEFORE the per-instruction
|
||||
// interlock/load — exactly like Beetle ReadInstruction precedes the base, and so a
|
||||
// fetch MISS clears any pending load give-back before the next load arms one. Only
|
||||
// emitted at cache-line LEADERS: a block leader / mid-block jump-table target (any
|
||||
// address reachable other than by fall-through, i.e. a possibly-cold cache entry) OR
|
||||
// a 16-byte-line start (addr&0xC==0, a sequential line crossing). Intra-line
|
||||
// followers reached by fall-through are guaranteed hits — the leader's fetch
|
||||
// refilled the line to its end — so they need no call (+0). Extra fetch points are
|
||||
// harmless (a hit is +0); only UNDER-counting a cold entry would diverge, which the
|
||||
// leader set prevents. The game runs at its KSEG0 load address, so `insn_addr` is
|
||||
// already the runtime guest PC (matching the dirty-RAM interp's cpu->pc and Beetle).
|
||||
auto emit_pre_icache = [&](uint32_t insn_addr, const std::string& indent) {
|
||||
if (!(insn_addr == block.start_addr || (insn_addr & 0xCu) == 0 ||
|
||||
extra_labels_.count(insn_addr))) return;
|
||||
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
|
||||
ss << indent << fmt::format("psx_icache_fetch(cpu, 0x{:08X}u);\n", insn_addr);
|
||||
ss << "#endif\n";
|
||||
};
|
||||
if (!cycle_per_insn && block_exec_cycles > 0) {
|
||||
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
|
||||
ss << config_.indent << fmt::format("psx_advance_cycles({}u);\n",
|
||||
@@ -1366,6 +1384,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
}
|
||||
|
||||
if (!is_cf) {
|
||||
if (cycle_per_insn) emit_pre_icache(addr, config_.indent);
|
||||
if (cycle_per_insn) emit_pre_timing(instr, config_.indent);
|
||||
ss << translate_instruction(addr, instr) << "\n";
|
||||
} else {
|
||||
@@ -1375,6 +1394,8 @@ std::string CodeGenerator::translate_basic_block(
|
||||
// runs at the branch PC, THEN the delay slot's at PC+4. Emit the branch
|
||||
// step FIRST (it is pure timing — does not touch GPR values, so it is
|
||||
// safe before the branch-condition capture below).
|
||||
if (cycle_per_insn)
|
||||
emit_pre_icache(exit_branch_addr, config_.indent);
|
||||
if (cycle_per_insn)
|
||||
emit_pre_timing(block.exit_instr.instruction, config_.indent);
|
||||
|
||||
@@ -1406,7 +1427,8 @@ std::string CodeGenerator::translate_basic_block(
|
||||
delay_saved_cond, cond);
|
||||
}
|
||||
ss << config_.indent << "/* delay slot (always executes) */\n";
|
||||
// Delay slot's own §1+deps+DO_LDS, before its body (skipped if a load).
|
||||
// Delay slot's own fetch + §1+deps+DO_LDS, before its body (skipped if a load).
|
||||
if (cycle_per_insn) emit_pre_icache(delay_slot_addr, config_.indent);
|
||||
if (cycle_per_insn) emit_pre_timing(delay_instr, config_.indent);
|
||||
ss << translate_instruction(delay_slot_addr, delay_instr) << "\n";
|
||||
}
|
||||
|
||||
@@ -388,6 +388,26 @@ bool FullFunctionEmitter::emit_function(
|
||||
psx_cyc_dep_res_mask(w));
|
||||
};
|
||||
|
||||
// I-cache FETCH cost (faithful R3000A), emitted BEFORE the per-instruction
|
||||
// interlock/load — like Beetle ReadInstruction precedes the base, so a fetch MISS
|
||||
// clears any pending load give-back before the next load arms one. Only emitted at
|
||||
// cache-line LEADERS: a block leader (any branch/dispatch entry — a possibly-cold
|
||||
// cache entry; cross-function targets are inserted into block_leaders above) OR a
|
||||
// 16-byte-line start (addr&0xC==0, a sequential line crossing). Intra-line followers
|
||||
// reached by fall-through are guaranteed hits (the leader refilled the line to its
|
||||
// end) → no call (+0). `rom_addr` is the ROM/compile-time address; relocate_ra maps
|
||||
// it to the RUNTIME guest PC the CPU actually fetches from (BIOS main stays in-place
|
||||
// KSEG1 0xBFC..; relocated kernel Part 2 → 0x500+, shell → 0x80030000+), so the
|
||||
// shared I-cache evolves identically to the dirty-RAM interp (cpu->pc) and Beetle —
|
||||
// and the KSEG1 uncached test (>=0xA0000000) sees the true virtual address. The
|
||||
// relocation preserves bits[3:0], so the line-leader test is space-independent.
|
||||
auto emit_icache_fetch = [&](uint32_t rom_addr) {
|
||||
if (!per_insn_cycles) return;
|
||||
if (!(block_leaders.count(rom_addr) || (rom_addr & 0xCu) == 0)) return;
|
||||
out += fmt::format("#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_icache_fetch(cpu, 0x{:08X}u);\n#endif\n",
|
||||
relocate_ra(rom_addr));
|
||||
};
|
||||
|
||||
for (auto it = addr_to_raw.begin(); it != addr_to_raw.end(); ++it) {
|
||||
uint32_t addr = it->first;
|
||||
uint32_t raw = it->second;
|
||||
@@ -435,6 +455,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
// running cycle count is exact at MULT/DIV/MFLO/MFHI (and later GTE) for
|
||||
// the completion-stall to absorb correctly. Orphaned (out-of-function)
|
||||
// delay slots are inlined elsewhere and charged at those sites.
|
||||
emit_icache_fetch(addr);
|
||||
emit_insn_interlock(raw);
|
||||
|
||||
// Decode and translate.
|
||||
@@ -561,6 +582,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
// Orphaned delay slot is inlined here (not an addr_to_raw
|
||||
// entry), so charge its interlock directly, BEFORE its body
|
||||
// (block mode already counts it in the owning block_cycles).
|
||||
emit_icache_fetch(ds_addr);
|
||||
emit_insn_interlock(ds_raw);
|
||||
out += fmt::format(" {}\n", ds_tr.c_code);
|
||||
}
|
||||
@@ -596,6 +618,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
// Orphaned delay slot is inlined here (not an addr_to_raw
|
||||
// entry), so charge its interlock directly, BEFORE its body
|
||||
// (block mode already counts it in the owning block_cycles).
|
||||
emit_icache_fetch(ds_addr);
|
||||
emit_insn_interlock(ds_raw);
|
||||
out += fmt::format(" {}\n", ds_tr.c_code);
|
||||
}
|
||||
@@ -620,6 +643,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
// Orphaned delay slot is inlined here (not an addr_to_raw
|
||||
// entry), so charge its interlock directly, BEFORE its body
|
||||
// (block mode already counts it in the owning block_cycles).
|
||||
emit_icache_fetch(ds_addr);
|
||||
emit_insn_interlock(ds_raw);
|
||||
out += fmt::format(" {}\n", ds_tr.c_code);
|
||||
}
|
||||
@@ -661,6 +685,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
// Orphaned delay slot is inlined here (not an addr_to_raw
|
||||
// entry), so charge its interlock directly, BEFORE its body
|
||||
// (block mode already counts it in the owning block_cycles).
|
||||
emit_icache_fetch(ds_addr);
|
||||
emit_insn_interlock(ds_raw);
|
||||
out += fmt::format(" {}\n", ds_tr.c_code);
|
||||
}
|
||||
|
||||
@@ -33,16 +33,18 @@
|
||||
|
||||
extern void psx_advance_cycles(uint32_t cycles);
|
||||
|
||||
/* Opt-in gate (default OFF). The I-cache fetch cost must be charged by BOTH backends
|
||||
* or NEITHER — charging it only in the interp (Stage 1) while the compiled path does
|
||||
* not would make the two backends disagree on fetch cost in mixed compiled/interp
|
||||
* execution (e.g. Tomba 2 overlays), forking timing. So until the compiled emitters
|
||||
* also charge it (Stage 2), this is enabled only for measurement (PSX_ICACHE=1, used
|
||||
* with PSX_FORCE_INTERP=1 so ALL code is interp'd → consistent). Stage 2 flips the
|
||||
* default on once both backends charge it. */
|
||||
/* Gate (default ON as of Stage 2). The I-cache fetch cost is now charged by BOTH
|
||||
* backends: the dirty-RAM interp (exec_one) AND both static emitters (code_generator
|
||||
* game path + full_function_emitter BIOS path, at each cache-line leader, using the
|
||||
* runtime guest PC). Because both charge it from a SHARED tag array, mixed
|
||||
* compiled/interp execution stays consistent and the cache evolves like Beetle.
|
||||
* Validated Stage 2 (2026-06-27): ruler #2 compiled == Beetle EXACT on all 13 loops
|
||||
* incl. icache_miss +14; ruler #1 steady delta 0 with native cold-refill spikes
|
||||
* (77/84) now matching Beetle's range; Tomba 2 FMV no-regression. Set PSX_ICACHE=0 to
|
||||
* disable (e.g. to A/B the fetch-cost component). */
|
||||
int psx_icache_enabled(void) {
|
||||
static int s = -1;
|
||||
if (s < 0) { const char* e = getenv("PSX_ICACHE"); s = (e && e[0] && e[0] != '0'); }
|
||||
if (s < 0) { const char* e = getenv("PSX_ICACHE"); s = (e == NULL || e[0] == '\0') ? 1 : (e[0] != '0'); }
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user