Files
psxrecomp/runtime/tests/test_psx_guest_cycle_scale_gate.c
Shokunin 91cd6db705 guest_cycle_scale: one CPU charge for every tier, declarative gate, rollback
Review of #561. Guest time now has two entry points (psx_cycles.h): the
CPU charge psx_cpu_charge() (instruction base, load fudge/region/completion,
i-cache refills, BIOS HLE costs), which applies [timing] guest_cycle_scale,
and the unscaled device advance psx_advance_cycles() (DMA, idle skips,
mul/div and GTE deadline waits). The emitters' block-mode charges emit
psx_cpu_charge; overlay DLLs accumulate it raw and publish through the
cpu_charge callback (was advance_cycles, same slot) into the same host
function, so nothing is scaled at the DLL boundary. The batched
psx_cyc_charge path, the dirty-RAM interpreter, psx_cyc_readmem, i-cache
refills and bios_hle.c all charge through it. At scale 1 it is exactly
psx_advance_cycles.

Gate in psxrecomp: [timing] guest_cycle_scale_gate, RAM predicates judged
at every VBlank edge (deterministic, works online, no mod needed), plus
psx_mod_set_guest_cycle_scale_gate() in mod_plugins.h for custom logic or
to tie the scale to a mod (guest_cycle_scale_gated = true; every mod
session reset shuts it). The carried fraction and both gate states are a
boot_state section (BS_SEC_GCS, written only when the scale is not 1), so
savestates and the netplay rollback snapshot re-simulate the same cycles.
psx_guest_cycle_scale_gate_test pins tier identity, the gates and rollback
re-simulation against the real psx_cycles.c.

OpenBIOS regenerated (emitter stamp); retail SCPH1001 is not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:41:39 -07:00

248 lines
9.1 KiB
C

/* [timing] guest_cycle_scale against the real psx_cycles.c:
* - identical accounting across tiers: native per-instruction CPU charges,
* the batched psx_cyc_charge hot path and an overlay DLL's raw batch
* published once all reach the same guest total; at 1 the CPU charge is
* exactly psx_advance_cycles;
* - the declarative RAM gate (judged at VBlank) and the mod gate;
* - rollback: restoring the snapshot (fraction + gate state) makes a
* re-simulation land on the same cycle.
*
* Build/run: ctest -R psx_guest_cycle_scale_gate_test
*/
#include "psx_cyc.h"
#include "mod_plugins.h"
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
volatile int g_ds_recording = 0;
uint8_t *g_psx_ram = 0;
uint32_t g_psx_ram_mask = 0x1FFFFFu;
int g_psx_load_delay = 1;
/* assert() is compiled out in Release (NDEBUG); this check is not. */
#define assert(c) do { if (!(c)) { \
fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, __LINE__, #c); \
exit(1); } } while (0)
#include <stdint.h>
#include <stdio.h>
int g_ls_replay_active = 0;
int g_ls_mode = 0;
int g_precise_mode = 0;
int g_psx_call_bail = 0;
uint32_t i_mask = 0;
uint64_t g_guest_store_count = 0;
uint64_t g_mmio_access_count = 0;
static uint32_t s_cd_cycles_remaining = 5;
static int s_cd_ready = 0;
static uint32_t s_dma_ready_cycles = 0;
void sio_advance(uint32_t cycles) { (void)cycles; }
void cdrom_advance(uint32_t cycles) {
if (s_cd_ready) return;
if (cycles >= s_cd_cycles_remaining) {
s_cd_cycles_remaining = 0;
s_cd_ready = 1;
} else {
s_cd_cycles_remaining -= cycles;
}
}
void dma_advance(uint32_t cycles) {
if (s_cd_ready) s_dma_ready_cycles += cycles;
}
void timers_advance(uint32_t cycles) { (void)cycles; }
void interrupts_advance_cycles(uint32_t cycles) { (void)cycles; }
void interrupts_service_scheduled_events(void) {}
uint32_t interrupts_cycles_to_vblank(void) { return UINT32_MAX; }
uint32_t timers_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
uint32_t cdrom_cycles_to_irq(uint32_t mask) {
(void)mask;
return s_cd_ready ? UINT32_MAX : s_cd_cycles_remaining;
}
uint32_t dma_cycles_to_internal_event(void) { return UINT32_MAX; }
uint32_t dma_cycles_to_deliverable_irq(uint32_t mask) {
(void)mask;
return UINT32_MAX;
}
uint32_t sio_cycles_to_irq(uint32_t mask) { (void)mask; return UINT32_MAX; }
/* SPU sample-event scheduler (golden 1a973806): psx_cycles.c consults it; stub here. */
static uint32_t s_spu_next_sample = UINT32_MAX;
uint32_t psx_spu_sample_event_cycles_to_next(void) { return s_spu_next_sample; }
void psx_spu_sample_event_service(void) {}
int psx_get_in_exception(void) { return 0; }
void starvation_watchdog_check(void) {}
void starvation_ring_pc_sample(void) {}
int psx_netplay_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
void dirty_ram_ld_delay_discard(void) {}
void dirty_ram_irq_ambient_resync_after_restore(void) {}
static uint8_t s_ram[0x200000];
static uint32_t read_ram(uint32_t phys, uint32_t size) {
uint32_t v = 0;
for (uint32_t i = 0; i < size; i++) v |= (uint32_t)s_ram[(phys + i) & 0x1FFFFFu] << (8u * i);
return v;
}
static void wr32(uint32_t addr, uint32_t v) {
uint32_t a = addr & 0x1FFFFFu;
for (int i = 0; i < 4; i++) s_ram[a + (uint32_t)i] = (uint8_t)(v >> (8 * i));
}
static void reset_clock(void) {
psx_cycles_reset_for_boot();
g_psx_gcs_frac = 0u;
}
/* The same instruction stream, charged the three ways tiers charge it. */
static const uint32_t k_stream[] = { 1, 1, 7, 1, 2, 5, 1, 1, 1, 4, 3, 1, 9, 1, 1, 6 };
enum { K_N = sizeof k_stream / sizeof k_stream[0], K_REPS = 997 };
static uint64_t run_native(void) { /* psx_cpu_charge per charge */
reset_clock();
for (int r = 0; r < K_REPS; r++)
for (int i = 0; i < K_N; i++) psx_cpu_charge(k_stream[i]);
return psx_get_cycle_count();
}
static uint64_t run_batched(void) { /* psx_cyc_charge hot path */
reset_clock();
for (int r = 0; r < K_REPS; r++)
for (int i = 0; i < K_N; i++) psx_cyc_charge(k_stream[i]);
psx_cyc_batch_flush();
return psx_get_cycle_count();
}
static uint64_t run_dll(void) { /* raw DLL batch, published per rep */
reset_clock();
for (int r = 0; r < K_REPS; r++) {
uint32_t pending = 0;
for (int i = 0; i < K_N; i++) pending += k_stream[i];
psx_cpu_charge(pending); /* the cpu_charge callback */
}
return psx_get_cycle_count();
}
static uint64_t run_device(void) { /* the unscaled device advance */
reset_clock();
for (int r = 0; r < K_REPS; r++)
for (int i = 0; i < K_N; i++) psx_advance_cycles(k_stream[i]);
return psx_get_cycle_count();
}
int main(void) {
uint64_t raw = 0;
for (int i = 0; i < K_N; i++) raw += k_stream[i];
raw *= K_REPS;
/* Scale 1: every path is the faithful count, and the CPU charge is the
* device advance (pass-through, fraction untouched). */
psx_guest_cycle_scale_set(1);
assert(run_native() == raw && run_batched() == raw &&
run_dll() == raw && run_device() == raw);
assert(g_psx_gcs_frac == 0u);
/* Scaled: identical across tiers; device time is never scaled. */
for (uint32_t s = 2; s <= 64; s = s * 2 + 1) {
psx_guest_cycle_scale_set(s);
uint64_t n = run_native();
assert(run_batched() == n);
assert(run_dll() == n);
assert(n <= raw / s + 1 && n + 1 >= raw / s);
assert(run_device() == raw);
}
psx_guest_cycle_scale_set(1);
/* Declarative RAM gate: shut until a VBlank sees the predicate hold. */
psx_guest_cycle_scale_set_ram_reader(read_ram);
assert(!psx_guest_cycle_scale_ram_gate_add(0x800AC796u, 4, ~0u, 0x180u)); /* unaligned */
assert(!psx_guest_cycle_scale_ram_gate_add(0x1F800000u, 4, ~0u, 0x180u)); /* not RAM */
assert(!psx_guest_cycle_scale_ram_gate_add(0x800AC794u, 3, ~0u, 0x180u)); /* size */
assert(psx_guest_cycle_scale_ram_gate_add(0x800AC794u, 4, ~0u, 0x180u));
psx_guest_cycle_scale_set(64);
assert(psx_guest_cycle_scale() == 1u);
wr32(0x800AC794u, 0x180u);
assert(psx_guest_cycle_scale() == 1u); /* judged at VBlank only */
psx_guest_cycle_scale_vblank();
assert(psx_guest_cycle_scale() == 64u);
wr32(0x800AC794u, 0x100u);
psx_guest_cycle_scale_vblank();
assert(psx_guest_cycle_scale() == 1u);
/* Masked byte predicate ANDed with the first. */
assert(psx_guest_cycle_scale_ram_gate_add(0x00010003u, 1, 0x0Fu, 0x05u));
wr32(0x800AC794u, 0x180u);
s_ram[0x10003] = 0xF4;
psx_guest_cycle_scale_vblank();
assert(psx_guest_cycle_scale() == 1u);
s_ram[0x10003] = 0xA5;
psx_guest_cycle_scale_vblank();
assert(psx_guest_cycle_scale() == 64u);
/* Mod gate on top: both must be open. */
psx_guest_cycle_scale_set_gated(1);
assert(psx_guest_cycle_scale() == 1u);
psx_mod_set_guest_cycle_scale_gate(1);
assert(psx_mod_guest_cycle_scale() == 64u);
psx_guest_cycle_scale_ram_gate_clear();
assert(psx_guest_cycle_scale() == 64u); /* mod gate alone */
psx_mod_set_guest_cycle_scale_gate(0);
assert(psx_guest_cycle_scale() == 1u);
psx_guest_cycle_scale_set_gated(0);
/* Rollback: snapshot mid-stream (odd scale leaves a fraction), run a
* tail, restore, re-simulate: the same cycle. Restoring the clock alone
* (fraction lost) does not reproduce it. */
assert(psx_guest_cycle_scale_ram_gate_add(0x800AC794u, 4, ~0u, 0x180u));
wr32(0x800AC794u, 0x180u);
psx_guest_cycle_scale_set(3);
psx_guest_cycle_scale_vblank();
assert(psx_guest_cycle_scale() == 3u);
reset_clock();
for (int i = 0; i < 5; i++) psx_cpu_charge(1);
assert(g_psx_gcs_frac != 0u);
uint32_t snap[3];
psx_guest_cycle_scale_snapshot(snap);
const uint64_t snap_cyc = psx_get_cycle_count();
for (int i = 0; i < 1000; i++) psx_cpu_charge(k_stream[i % K_N]);
wr32(0x800AC794u, 0x100u); /* the tail shuts the gate */
psx_guest_cycle_scale_vblank();
for (int i = 0; i < 100; i++) psx_cpu_charge(1);
const uint64_t live = psx_get_cycle_count();
wr32(0x800AC794u, 0x180u); /* guest RAM is restored too */
psx_cycle_count = snap_cyc;
psx_guest_cycle_scale_restore(snap);
assert(psx_guest_cycle_scale() == 3u);
for (int i = 0; i < 1000; i++) psx_cpu_charge(k_stream[i % K_N]);
wr32(0x800AC794u, 0x100u);
psx_guest_cycle_scale_vblank();
for (int i = 0; i < 100; i++) psx_cpu_charge(1);
assert(psx_get_cycle_count() == live);
/* The fraction is load-bearing: two 1-cycle charges after the restore
* carry a cycle with it and none without it. */
wr32(0x800AC794u, 0x180u);
psx_guest_cycle_scale_vblank();
psx_cycle_count = snap_cyc;
psx_guest_cycle_scale_restore(snap);
psx_cpu_charge(1); psx_cpu_charge(1);
const uint64_t with_frac = psx_get_cycle_count();
psx_cycle_count = snap_cyc;
{
uint32_t lost[3] = { 0u, snap[1], snap[2] };
psx_guest_cycle_scale_restore(lost);
}
psx_cpu_charge(1); psx_cpu_charge(1);
assert(psx_get_cycle_count() != with_frac);
puts("psx_guest_cycle_scale_gate_test: ok");
return 0;
}