feat(timing): ruler #2 foundation — cycle micro-benchmark test ROM + universal block observe

Ruler #2 (the in-tree cycle-model integration-test ecosystem):
- tools/cycle_testrom/gen_testrom.py: hand-encoded MIPS PS-X EXE of cycle-
  isolation loops (baseline/alu/load/load2/div/div_spaced/mult). Each loop top is
  a block leader, so a cyc_watch single-anchor Δ = one iteration; baseline
  subtraction isolates each component (overhead + fetch cancel). Encoding
  validated by a clean recompile (divu -> gpr[10]/gpr[11], etc.).
- game.toml/seeds.txt/README.md; .gitignore for the regenerable EXE/json/generated.

Universal per-block-leader observe:
- Added debug_server_cyc_observe at every block leader in the GAME emitter
  (code_generator.cpp) to match the BIOS emitter, so ANY recompiled code (game,
  BIOS, overlay) is anchorable on both backends — not just function entries.
  #ifndef PSX_NO_DEBUG_TOOLS so prod = zero overhead.

Regression-checked: regen Tomba 2 + rebuild, FMV still streams (i_stat 0x8D), no
visual regression. Builds clean.

Remaining for ruler #2: native test-runtime delivery (boot->entry handoff) +
Beetle .exe sideload + the per-component compare harness (see README TODO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-26 22:30:14 -07:00
parent 9cec60a30d
commit 2b5ad88dc6
6 changed files with 328 additions and 0 deletions
+13
View File
@@ -1179,6 +1179,16 @@ std::string CodeGenerator::translate_basic_block(
// Block label
ss << fmt::format("block_{:08X}:\n", block.start_addr);
// Per-block-leader cycle observe (cyc_watch ruler — universal with the BIOS
// emitter). Sampled at the block leader BEFORE any cycle is charged, matching
// Beetle's before-instruction sample and the cycle_compare.py anchor
// semantics, so ANY block-leader PC is anchorable on both backends (not just
// function entries). Debug-only: prod (PSX_NO_DEBUG_TOOLS) emits nothing.
ss << "#ifndef PSX_NO_DEBUG_TOOLS\n";
ss << config_.indent
<< fmt::format("debug_server_cyc_observe(0x{:08X}u);\n", block.start_addr);
ss << "#endif\n";
// Cycle-budgeted precise event slicing (PRECISE_IRQ_SLICE.md). At the block
// leader — before any cycle is charged or the body runs — divert to the
// per-instruction interpreter when an interrupt could be taken inside this
@@ -2322,6 +2332,9 @@ std::string CodeGenerator::generate_file(
// This provides CPUState, GTE/trap declarations, and call_by_address().
ss << "#include \"psx_runtime.h\"\n\n";
ss << "extern void debug_server_log_call_entry(uint32_t func_addr);\n";
ss << "#ifndef PSX_NO_DEBUG_TOOLS\n";
ss << "extern void debug_server_cyc_observe(uint32_t block_leader_phys);\n";
ss << "#endif\n";
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n";
ss << "extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* ws auto screen-x cull (gpu.c) */\n";