mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
feat(timing): single-source cycle-cost seam (psx_instr_base_cycles), identity
P3 step 1 (FAITHFUL_TIMING_PLAN.md): both backends now account guest CPU cycles through ONE function, runtime/include/psx_instr_cost.h::psx_instr_base_cycles(insn), so they can never disagree (the -8 drift class). Interp routes its 3 per-insn charges (exec_delay_slot, dirty dispatch loop, precise slice) through it; the recompiler sums it over each block (+ the outside delay-slot clone) to fold into the compile-time block charge. Stage 1 = identity (1 cycle/insn) = the proven baseline, so the seam is BEHAVIOR-PRESERVING: regen is byte-identical (verified, full.c + dispatch.c) and Tomba 2 still reaches the intro FMV (i_stat 0x8D streaming). Stage 2 edits ONLY psx_instr_base_cycles (transcribe real R3000A costs from in-tree Beetle cpu.cpp/gte.cpp + psx-spx, verify each vs Beetle) and both backends update together. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
// included via relative path to avoid an include-dir collision (recompiler and
|
||||
// runtime both ship a gte.h).
|
||||
#include "../../runtime/include/ws_backdrop_detect.h"
|
||||
#include "../../runtime/include/psx_instr_cost.h" /* single-source CPU cycle cost (shared with interp) */
|
||||
#include <fmt/format.h>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
@@ -1208,8 +1209,24 @@ std::string CodeGenerator::translate_basic_block(
|
||||
block.exit_instr.has_delay_slot &&
|
||||
block.exit_instr.type != ControlFlowType::None &&
|
||||
block.exit_instr.address == block.end_addr;
|
||||
const uint32_t block_exec_cycles =
|
||||
static_cast<uint32_t>(block.instruction_count) + (exit_delay_slot_outside ? 1u : 0u);
|
||||
// Block cycle charge = sum of the single-source per-instruction cost
|
||||
// (psx_instr_cost.h) over every instruction the block executes, INCLUDING
|
||||
// the always-executed delay-slot clone when the exit branch sits at end_addr
|
||||
// (delay slot outside [start,end]). The interpreter charges the same cost
|
||||
// per instruction through the same function, so the two backends cannot
|
||||
// disagree. Stage 1 cost is identity (1/insn) => this equals
|
||||
// instruction_count(+1 outside delay) exactly => byte-identical regen; Stage 2
|
||||
// edits ONLY psx_instr_base_cycles and both backends update together.
|
||||
uint32_t block_exec_cycles = 0;
|
||||
for (uint32_t a = block.start_addr; a <= block.end_addr; a += 4) {
|
||||
auto w = exe_.read_word(a);
|
||||
if (!w.has_value()) break;
|
||||
block_exec_cycles += psx_instr_base_cycles(*w);
|
||||
}
|
||||
if (exit_delay_slot_outside) {
|
||||
auto dw = exe_.read_word(block.end_addr + 4u); // the delay-slot clone
|
||||
if (dw.has_value()) block_exec_cycles += psx_instr_base_cycles(*dw);
|
||||
}
|
||||
|
||||
{
|
||||
uint32_t slice_bcyc = block_exec_cycles;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/* psx_instr_cost.h — SINGLE SOURCE OF TRUTH for R3000A guest CPU cycle cost.
|
||||
*
|
||||
* FAITHFUL_TIMING_PLAN.md P3/Stage-2. Both execution backends — the dirty-RAM
|
||||
* interpreter (runtime/src/dirty_ram_interp.c) and the static recompiler
|
||||
* (recompiler/src/code_generator.cpp, which sums this over a block at gen time
|
||||
* to fold it into one compile-time block charge) — MUST account guest cycles
|
||||
* THROUGH THIS FUNCTION so they can never disagree (the -8 drift class).
|
||||
*
|
||||
* STAGE 1 (now): identity, 1 cycle per instruction. This is the proven baseline
|
||||
* both backends already used, so routing them through here is BEHAVIOR-PRESERVING
|
||||
* (a regen must be byte-identical) and merely establishes the seam.
|
||||
*
|
||||
* STAGE 2 (next): replace the body with the documented R3000A model, transcribed
|
||||
* (facts, not code) from the in-tree oracle psxrecomp/beetle-psx/mednafen/psx/
|
||||
* (cpu.cpp MULT_Tab/muldiv, gte.cpp GTE_Instruction per-command table) and
|
||||
* psx-spx, each value VERIFIED against Beetle at runtime before it is trusted.
|
||||
* Memory-access wait-states are charged separately in the psx_read/write path
|
||||
* (they are address/region-dependent, not knowable from the opcode alone), so
|
||||
* this function returns ONLY the CPU instruction/execute base cost. Keep that
|
||||
* split clean in both backends to avoid double-counting.
|
||||
*
|
||||
* Header is plain C (static inline) so both the C runtime and the C++ recompiler
|
||||
* can include it as the one definition.
|
||||
*/
|
||||
#ifndef PSX_INSTR_COST_H
|
||||
#define PSX_INSTR_COST_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* CPU instruction base cycle cost (execute cycles only; excludes data-access
|
||||
* wait states, which the memory path charges). `insn` is the raw 32-bit MIPS
|
||||
* word. Stage 1: identity. */
|
||||
static inline uint32_t psx_instr_base_cycles(uint32_t insn) {
|
||||
(void)insn;
|
||||
return 1u;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PSX_INSTR_COST_H */
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "debug_server.h"
|
||||
#include "interrupts.h"
|
||||
#include "psx_cycles.h"
|
||||
#include "psx_instr_cost.h" /* psx_instr_base_cycles — single-source cycle cost */
|
||||
#include "gpu.h" /* psx_ws_is_backdrop_site / psx_ws_backdrop_x (interp hook) */
|
||||
#include "ws_backdrop_detect.h" /* shared backdrop-window detector (auto_backdrop) */
|
||||
|
||||
@@ -904,9 +905,10 @@ static void exec_delay_slot(CPUState *cpu, uint32_t pc) {
|
||||
* pair costs 2 — matching the recompiler's block.instruction_count (which
|
||||
* counts both) and hardware. Without this the dirty-interp under-charges 1
|
||||
* cycle per taken branch vs compiled-overlay code, drifting the shared
|
||||
* guest-cycle timeline and forking timer-sensitive code (Tomba2 logo). */
|
||||
* guest-cycle timeline and forking timer-sensitive code (Tomba2 logo).
|
||||
* Charged through the single-source cost fn (psx_instr_cost.h). */
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_advance_cycles(1u);
|
||||
psx_advance_cycles(psx_instr_base_cycles(insn));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1586,7 +1588,7 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) {
|
||||
g_unsupported_seen = 0;
|
||||
int transferred = exec_one(cpu, pc, &next_pc);
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_advance_cycles(1u);
|
||||
psx_advance_cycles(psx_instr_base_cycles(fetch_word(pc & 0x1FFFFFFFu)));
|
||||
#endif
|
||||
g_dirty_ram_insns_run++;
|
||||
if (g_unsupported_seen) {
|
||||
@@ -1861,7 +1863,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
}
|
||||
}
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_advance_cycles(1u);
|
||||
psx_advance_cycles(psx_instr_base_cycles(insn)); /* single-source cost */
|
||||
#endif
|
||||
dirty_ram_log_instruction(cpu, pc, insn, before_s0, next_pc,
|
||||
transferred ? cpu->pc : next_pc,
|
||||
|
||||
Reference in New Issue
Block a user