phase1.0e-d: per-block guest cycle accounting in codegen

Codegen-side foundation for cycle-driven peripheral timing per
docs/CYCLE_TIMING_ARCH.md. Each emitted basic block now calls
psx_advance_cycles(N) where N is the conservative 1-cycle-per-MIPS-
instruction count for the block (sum from leader to next leader,
including delay slot).

Generic per-instruction emit, no card-specific paths. Macro-gated by
PSX_ENABLE_BLOCK_CYCLES so generated output is comparable on/off.

This slice does NOT wire psx_advance_cycles to peripheral advance.
Reason: sio_tick has a legacy IRQ-countdown body that decrements
unconditionally per call; calling it from psx_advance_cycles (which
fires per block, ~100M+ times/sec) drains that countdown in
microseconds and breaks pad timing. A future slice with a peripheral-
only sio_advance (no legacy side effect) will wire SIO. Card byte-15
abort persists for now — expected; the fix needs that peripheral wiring.

Files:
- recompiler/src/full_function_emitter.cpp:
  - precompute block_cycles map (instr count between leaders)
  - emit psx_advance_cycles(<N>u) under #ifdef PSX_ENABLE_BLOCK_CYCLES
    at every block leader, before psx_check_interrupts
  - emit extern decl in generated header
- generated/SCPH1001_full.c: regenerated; 12794 PSX_ENABLE_BLOCK_CYCLES
  guards, one per block leader
- runtime/CMakeLists.txt: PSX_ENABLE_BLOCK_CYCLES=1 added to both
  psx-runtime and psx-beetleoracle target_compile_definitions
- runtime/src/psx_cycles.c: psx_advance_cycles accumulates counter only;
  peripheral dispatch deferred to next slice (rationale in comment)

Validation against 5d4d4bd baseline:
- Build clean both targets
- t=20s tx_writes=928, t=30s tx_writes=5000 (matches baseline)
- pad arm audit 5056/5056 armed, no_ack=0
- 4x CROSS press all succeed
- card txn distribution unchanged: txns reach READ_DATA, "MC" bytes
  flow at txn 0 bytes 10-11 (0x4D 0x43)
- psx_cycle_count grows: 1.72B (t=20s), 2.33B (t=30s), 10.04B
  post-CROSS — proves codegen emit live and per-block accounting works
- Macro=0 build (verified before macro flip): output identical to
  baseline

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-04-29 20:03:07 -07:00
parent 5d4d4bd30b
commit 77e66f9da3
4 changed files with 38430 additions and 7 deletions
File diff suppressed because it is too large Load Diff
+38 -1
View File
@@ -265,6 +265,27 @@ bool FullFunctionEmitter::emit_function(
}
}
// Phase 1.0e-d: precompute per-block cycle estimates. For each block
// leader L, count instructions from L up to (but not including) the
// next leader address (or end of function). Conservative 1 cycle per
// instruction; precise R3000A timing (load delays, multiply/divide,
// memory stalls) is a future refinement. Includes terminator and
// delay slot in the block before the next leader. */
std::map<uint32_t, uint32_t> block_cycles;
for (auto it = addr_to_raw.begin(); it != addr_to_raw.end(); ++it) {
uint32_t leader = it->first;
if (!block_leaders.count(leader)) continue;
uint32_t count = 0;
auto walker = it;
while (walker != addr_to_raw.end()) {
count++;
++walker;
if (walker == addr_to_raw.end()) break;
if (block_leaders.count(walker->first)) break;
}
block_cycles[leader] = count;
}
for (auto it = addr_to_raw.begin(); it != addr_to_raw.end(); ++it) {
uint32_t addr = it->first;
uint32_t raw = it->second;
@@ -274,6 +295,16 @@ bool FullFunctionEmitter::emit_function(
// can service vblank and other hardware interrupts.
if (block_leaders.count(addr)) {
out += fmt::format("label_{:08X}:\n", addr);
// 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;
auto bcit = block_cycles.find(addr);
if (bcit != block_cycles.end()) bcyc = bcit->second;
if (bcyc > 0) {
out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
out += fmt::format(" psx_advance_cycles({}u);\n", bcyc);
out += "#endif\n";
}
out += " psx_check_interrupts(cpu);\n";
}
@@ -975,7 +1006,13 @@ EmitStats FullFunctionEmitter::emit(
full_c += "extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val);\n";
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 uint32_t g_debug_last_store_pc;\n\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";
full_c += " * activate cycle advancement at every block leader. */\n";
full_c += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
full_c += "extern void psx_advance_cycles(uint32_t cycles);\n";
full_c += "#endif\n\n";
// Forward declare ALL functions so intra-file calls resolve.
for (const auto& fn : dr.functions) {
+2
View File
@@ -43,6 +43,7 @@ target_include_directories(psx-runtime PRIVATE include)
target_compile_definitions(psx-runtime PRIVATE
PSX_NATIVE_BUILD=1
DEFAULT_DEBUG_PORT=4370
PSX_ENABLE_BLOCK_CYCLES=1
$<$<CXX_COMPILER_ID:MSVC>:SDL_MAIN_HANDLED>
)
@@ -168,6 +169,7 @@ if(NOT MSVC AND EXISTS "${BEETLE_LIB}")
PSX_NATIVE_BUILD=1
ENABLE_BEETLE_PSX_ORACLE=1
DEFAULT_DEBUG_PORT=4370
PSX_ENABLE_BLOCK_CYCLES=1
)
target_link_libraries(psx-beetleoracle PRIVATE
+8 -6
View File
@@ -1,9 +1,12 @@
/* psx_cycles.c — PSX guest CPU cycle clock.
*
* Phase 1.0e-a: scaffold only. psx_advance_cycles updates the counter
* but invokes no peripheral hooks. 1.0e-b will dispatch to
* sio_advance(cycles); 1.0e-c will add gpu_advance for VBlank; later
* slices add timers/CDROM. */
* Phase 1.0e-d minimal slice: receives per-block cycle counts from
* generated code (when compiled with -DPSX_ENABLE_BLOCK_CYCLES=1) and
* accumulates them in psx_cycle_count. NO peripheral hooks — wiring
* sio_tick here would corrupt the legacy IRQ countdown (it decrements
* unconditionally per call; millions of calls per second drain it in
* microseconds). A future slice with a peripheral-only sio_advance
* (no legacy countdown side effect) will wire SIO. */
#include "psx_cycles.h"
@@ -12,8 +15,7 @@ uint64_t psx_cycle_count = 0;
void psx_advance_cycles(uint32_t cycles) {
if (cycles == 0) return;
psx_cycle_count += (uint64_t)cycles;
/* Phase 1.0e-a: peripheral hooks empty.
* 1.0e-b will call: sio_advance(cycles), gpu_advance(cycles), etc. */
/* No peripheral dispatch in this slice — see comment above. */
}
uint64_t psx_get_cycle_count(void) {