Files
psxrecomp/runtime/src/psx_cycles.c
T

1223 lines
53 KiB
C

/* psx_cycles.c — PSX guest CPU cycle clock. */
#include "psx_cycles.h"
#include "psx_cycle_freeze.h"
#include "cpu_state.h"
#include <stdlib.h>
#include <string.h>
#if defined(_MSC_VER)
#include <intrin.h> /* MSVC intrinsics: _BitScanReverse (no __builtin_clz) */
#endif
#include "cdrom.h"
#include "dma.h"
#include "interrupts.h"
#include "sio.h"
#include "starvation_ring.h"
#include "timers.h"
#if defined(PSX_HAS_RECOMP_NET)
#include "psx_netplay.h"
#endif
#ifdef PSX_COSIM
#include "cosim_state.h"
#endif
uint64_t psx_cycle_count = 0;
uint32_t g_psx_gcs_recip_q16 = 65536u;
uint32_t g_psx_gcs_frac = 0u;
int g_psx_gcs_batch = 0; /* [timing] guest_cycle_scale_batch */
static uint32_t s_psx_gcs_cfg = 1u; /* [timing] guest_cycle_scale */
static uint32_t s_psx_gcs_live = 1u;
static int s_psx_gcs_gated = 0; /* guest_cycle_scale_gated (mod gate) */
static int s_psx_gcs_open = 0; /* mod gate state */
/* [timing] guest_cycle_scale_gate: RAM predicates, all must hold. */
typedef struct { uint32_t phys, size, mask, value; } PsxGcsRamPred;
static PsxGcsRamPred s_psx_gcs_ram[PSX_GCS_RAM_GATE_MAX];
static uint32_t s_psx_gcs_ram_n = 0;
static int s_psx_gcs_ram_open = 0; /* last VBlank's verdict */
static uint32_t (*s_psx_gcs_ram_read)(uint32_t phys, uint32_t size) = NULL;
static void psx_guest_cycle_scale_apply(void) {
const int open = (!s_psx_gcs_gated || s_psx_gcs_open) &&
(s_psx_gcs_ram_n == 0u || s_psx_gcs_ram_open);
uint32_t n = open ? s_psx_gcs_cfg : 1u;
if (n == s_psx_gcs_live) return;
s_psx_gcs_live = n;
g_psx_gcs_recip_q16 = psx_gcs_recip_for(n);
g_psx_gcs_frac = 0u;
}
void psx_guest_cycle_scale_set(uint32_t scale) {
if (scale < 1u) scale = 1u;
if (scale > 64u) scale = 64u;
s_psx_gcs_cfg = scale;
psx_guest_cycle_scale_apply();
}
void psx_guest_cycle_scale_set_gated(int gated) {
s_psx_gcs_gated = gated ? 1 : 0;
psx_guest_cycle_scale_apply();
}
void psx_guest_cycle_scale_gate_open(int open) {
s_psx_gcs_open = open ? 1 : 0;
psx_guest_cycle_scale_apply();
}
void psx_guest_cycle_scale_set_ram_reader(uint32_t (*read)(uint32_t phys, uint32_t size)) {
s_psx_gcs_ram_read = read;
}
int psx_guest_cycle_scale_ram_gate_add(uint32_t addr, uint32_t size,
uint32_t mask, uint32_t value) {
const uint32_t phys = addr & 0x1FFFFFFFu;
if (s_psx_gcs_ram_n >= (uint32_t)PSX_GCS_RAM_GATE_MAX) return 0;
if (size != 1u && size != 2u && size != 4u) return 0;
if ((phys & (size - 1u)) != 0u || phys >= 0x00800000u) return 0; /* main RAM */
if (size < 4u) mask &= (1u << (8u * size)) - 1u;
s_psx_gcs_ram[s_psx_gcs_ram_n].phys = phys;
s_psx_gcs_ram[s_psx_gcs_ram_n].size = size;
s_psx_gcs_ram[s_psx_gcs_ram_n].mask = mask;
s_psx_gcs_ram[s_psx_gcs_ram_n].value = value & mask;
s_psx_gcs_ram_n++;
s_psx_gcs_ram_open = 0; /* shut until the first VBlank */
psx_guest_cycle_scale_apply();
return 1;
}
void psx_guest_cycle_scale_ram_gate_clear(void) {
s_psx_gcs_ram_n = 0u;
s_psx_gcs_ram_open = 0;
psx_guest_cycle_scale_apply();
}
/* VBlank edge (interrupts.c fire_vblank_edge): a guest-time point, so every
* peer and every rollback re-simulation sees the same verdict. */
void psx_guest_cycle_scale_vblank(void) {
if (s_psx_gcs_ram_n == 0u || !s_psx_gcs_ram_read) return;
int open = 1;
for (uint32_t i = 0; open && i < s_psx_gcs_ram_n; i++) {
const PsxGcsRamPred *p = &s_psx_gcs_ram[i];
open = (s_psx_gcs_ram_read(p->phys, p->size) & p->mask) == p->value;
}
if (open == s_psx_gcs_ram_open) return;
s_psx_gcs_ram_open = open;
psx_guest_cycle_scale_apply();
}
/* Public mod API (mod_plugins.h): the mod gate. */
int psx_mod_host_output_scope(void);
void psx_mod_set_guest_cycle_scale_gate(int open) {
if (psx_mod_host_output_scope()) return;
psx_guest_cycle_scale_gate_open(open);
}
uint32_t psx_mod_guest_cycle_scale(void) { return s_psx_gcs_live; }
void psx_guest_cycle_scale_snapshot(uint32_t out[3]) {
out[0] = g_psx_gcs_frac;
out[1] = (uint32_t)s_psx_gcs_open;
out[2] = (uint32_t)s_psx_gcs_ram_open;
}
void psx_guest_cycle_scale_restore(const uint32_t in[3]) {
s_psx_gcs_open = in[1] ? 1 : 0;
s_psx_gcs_ram_open = in[2] ? 1 : 0;
psx_guest_cycle_scale_apply();
g_psx_gcs_frac = PSX_GCS_ACTIVE() ? (in[0] & 0xFFFFu) : 0u;
}
uint32_t psx_guest_cycle_scale(void) { return s_psx_gcs_live; }
uint32_t psx_guest_cycle_scale_config(void) { return s_psx_gcs_cfg; }
uint32_t g_psx_cyc_batch = 0;
uint32_t g_psx_cyc_batch_limit = 0;
int g_psx_cyc_bb_defer = 0;
uint32_t *g_psx_cyc_local_acc = NULL;
static int s_cycle_replay_active = 0;
static uint64_t s_cycle_replay_live = 0;
/* ---- guest-time freeze: render passes and uncharged calls
* (see psx_cycle_freeze.h) ---- */
int g_psx_render_pass_active = 0;
int g_psx_guest_time_frozen = 0;
static uint64_t s_freeze_start = 0;
static uint64_t s_freeze_limit = 0;
static void (*s_freeze_overrun)(void) = 0;
static int s_uncharged_active = 0;
static int s_uncharged_thawed = 0;
static void psx_cycle_freeze_tick(void) {
extern uint64_t g_psx_cycle_fast_limit;
/* Come back at most every 16K cycles for the watchdog; never service. */
psx_next_service_cycle = psx_cycle_count + 16384u;
g_psx_cycle_fast_limit = 0;
if (!s_freeze_limit || psx_cycle_count - s_freeze_start <= s_freeze_limit)
return;
if (s_uncharged_active) {
/* Over budget: the rest of the call runs on the live clock, and every
* cycle it executed so far is charged. Devices catch up from where
* they stopped at the next charge, in event-bounded steps. */
g_psx_guest_time_frozen = 0;
s_freeze_limit = 0;
s_uncharged_thawed = 1;
psx_next_service_cycle = 0;
return;
}
if (s_freeze_overrun) {
void (*overrun)(void) = s_freeze_overrun;
s_freeze_overrun = 0;
overrun();
}
}
int psx_cycle_replay_begin(uint64_t start_cycle) {
extern int g_ls_replay_active;
if (!g_ls_replay_active || s_cycle_replay_active) return 0;
s_cycle_replay_live = psx_cycle_count;
psx_cycle_count = start_cycle;
s_cycle_replay_active = 1;
return 1;
}
uint64_t psx_cycle_replay_end(void) {
uint64_t replay_cycle = psx_cycle_count;
if (s_cycle_replay_active) {
psx_cycle_count = s_cycle_replay_live;
s_cycle_replay_active = 0;
}
return replay_cycle;
}
/* Throttle watchdog check to once per ~64K cycles (header hot path). */
uint32_t psx_watchdog_throttle = 0;
uint32_t psx_pc_sample_throttle = 0;
/* Conservative event-granularity diagnostic (set via debug cmd
* overlay_native_event_granularity). Normally psx_advance_cycles charges a
* whole basic block's cycles in ONE step, so every device advances N cycles at
* once and any events that came due at sub-block cycles all fire together, in
* the fixed device order below (sio,cdrom,dma,timers,interrupts) — NOT in true
* due-cycle order. The dirty-RAM interpreter avoids this only because it calls
* us with N=1 per instruction. When this flag is set, a batched (N>1) advance
* is split into N single-cycle steps, so device events fire at their true
* due-cycle in order — i.e. native execution gets the same event timeline the
* interpreter produces. Diagnostic: if the village->overworld blue screen
* clears with this on, the root cause is per-block event-ordering, and the
* real fix is a due-cycle event scheduler (run-to-next-event), not this. */
int g_event_step_conservative = 0;
/* Env opt-in (PSX_EVENT_STEP_CONSERVATIVE=1) so the diagnostic is reachable in
* production builds too, where the TCP setter doesn't exist — the determinism
* A/B ("do the fast-limit and conservative paths present the same guest event
* timeline?") needs the production binary to run BOTH ways. */
void psx_event_step_conservative_env_init(void) {
const char *e = getenv("PSX_EVENT_STEP_CONSERVATIVE");
if (e && e[0] == '1') g_event_step_conservative = 1;
}
static void advance_devices(uint32_t c) {
psx_cycle_count += (uint64_t)c;
sio_advance(c);
cdrom_advance(c);
dma_advance(c);
timers_advance(c);
interrupts_advance_cycles(c);
psx_spu_sample_event_service();
}
/* ===== Event-deadline device servicing (production fast path) =================
*
* The faithful core charges guest cycles PER INSTRUCTION (psx_cyc.h §1), and the
* legacy path below walked EVERY device on EVERY charge — ~10 host calls per
* guest instruction, ~300M calls/s: the whole prod build pegged a core inside
* psx_advance_cycles (gdb-sampled 14/14, leaves = timers/cdrom/dma/interrupts).
*
* FAITHFUL_TIMING_PLAN.md's target architecture is the fix: cycles accumulate
* into the ONE global counter cheaply; devices are only SERVICED when the next
* event deadline is reached (cycles_to_next_event(): min over vblank / timer /
* cdrom / dma / sio IRQ distances), or when guest code touches device MMIO
* (memory.c calls psx_devices_mmio_sync() so reads see current state and writes
* that re-arm a device force a deadline recompute).
*
* Guest-visible semantics are IDENTICAL: servicing rewinds psx_cycle_count to
* the devices' synced position and re-plays the gap through the SAME
* vblank-bounded chunk loop the legacy path used, so device code observes the
* exact same cycle values it always did, and no event can fire late because the
* deadline is by construction <= the earliest device event (hard-capped at
* DEADLINE_HARD_CAP for IRQ-masked device progress + frame pacing).
*
* The exact per-charge path is kept verbatim for PSX_COSIM builds (oracle
* determinism / checkpoint alignment) and for the g_event_step_conservative
* diagnostic toggle. */
#define PSX_DEADLINE_HARD_CAP 16384u
static uint64_t s_devices_synced_cycle = 0; /* devices are advanced up to here */
/* Exported for the inlined psx_advance_cycles / psx_cyc_charge hot path. */
uint64_t psx_next_service_cycle = 0; /* absolute; 0 = dirty, recompute */
int psx_in_device_service = 0; /* re-entrancy guard */
int g_plp_cycle_diag = 0;
uint64_t g_plp_adv_calls = 0;
uint32_t g_plp_adv_max_chunk = 0;
uint64_t g_plp_adv_sum = 0;
uint64_t g_plp_svc_calls = 0;
#define s_next_service_cycle psx_next_service_cycle
#define s_in_device_service psx_in_device_service
static uint64_t s_next_watchdog = 0;
static uint64_t s_next_pc_sample = 0;
/* Guest-cycle profile sampler (guest_cycle_profile.c). Every `period` guest
* cycles the installed hook records where the guest is. The next sample point
* is folded into the service deadline, so a sample lands on the first charge
* that crosses it -- not on whatever MMIO access or device event happened to
* trigger servicing -- which keeps the attribution unbiased. A charge that
* jumps several periods at once (idle skip, a long stall) reports the number
* of periods crossed so those cycles stay credited to the code that spent
* them. Period 0 or no hook = off; the unit tests that link this file
* without the ring see NULL. */
uint32_t psx_cycle_sample_period = 0;
void (*psx_cycle_sample_hook)(uint64_t cycle, uint32_t periods) = NULL;
static uint64_t s_next_cycle_sample = 0;
static void psx_cycle_sample_check(uint64_t target) {
const uint32_t period = psx_cycle_sample_period;
if (!period || !psx_cycle_sample_hook) return;
/* First use, or the clock was rewound (savestate / boot reset). */
if (s_next_cycle_sample == 0u || s_next_cycle_sample > target + period) {
s_next_cycle_sample = target + period;
return;
}
if (target < s_next_cycle_sample) return;
uint64_t n = (target - s_next_cycle_sample) / period + 1u;
s_next_cycle_sample += n * (uint64_t)period;
psx_cycle_sample_hook(target, n > 0xFFFFFFFFull ? 0xFFFFFFFFu : (uint32_t)n);
}
/* Absolute inclusive limit for dirty_ram_interp.c's exact one-cycle path.
* Zero means that the next charge must visit psx_advance_cycles(). */
uint64_t g_psx_cycle_fast_limit = 0;
/* Distance to the nearest INTERNAL device event, mask-blind (i_mask =
* all-unmasked). This is the chunking bound for catch-up: the sio/cdrom/dma
* *_advance() functions fire at most ONE event boundary per call (their
* legacy caller stepped 1 cycle at a time), so every catch-up chunk must land
* exactly ON the nearest event so chained sequences (SIO byte -> ack -> next
* byte; CD sector trains) replay event-by-event, and so events armed while
* their IRQ is masked in I_MASK still process on time (games poll I_STAT). */
static uint32_t devices_cycles_to_next_internal_event(void) {
uint32_t best = interrupts_cycles_to_vblank(); /* frame pacing always */
uint32_t t = timers_cycles_to_irq(0xFFFFFFFFu); if (t < best) best = t;
uint32_t c = cdrom_cycles_to_irq(0xFFFFFFFFu); if (c < best) best = c;
uint32_t d = dma_cycles_to_internal_event(); if (d < best) best = d;
uint32_t s = sio_cycles_to_irq(0xFFFFFFFFu); if (s < best) best = s;
uint32_t a = psx_spu_sample_event_cycles_to_next(); if (a < best) best = a;
if (best == 0) best = 1; /* due/overdue: process within one cycle */
return best;
}
/* Wait-loop observation boundary. Device catch-up remains mask-blind and
* replays every intermediate timer/DMA/SIO transition, but a CPU loop with no
* stores or MMIO cannot observe a masked IRQ until it becomes deliverable.
* Stopping at every masked timer target turned a safe VBlank/CD wait skip into
* tens of thousands of host-side re-detections per second. VBlank remains an
* unconditional ceiling so frame pacing is never crossed in one jump. */
static uint32_t devices_cycles_to_next_idle_event(void) {
extern uint32_t i_mask;
uint32_t best = interrupts_cycles_to_vblank();
uint32_t t = timers_cycles_to_irq(i_mask); if (t < best) best = t;
uint32_t c = cdrom_cycles_to_irq(i_mask); if (c < best) best = c;
uint32_t d = dma_cycles_to_deliverable_irq(i_mask); if (d < best) best = d;
uint32_t s = sio_cycles_to_irq(i_mask); if (s < best) best = s;
/* SPU IRQ9 is raised by the guest-clock sample scheduler, not by a device
* *_advance(); a poll loop waiting on it must not be skipped across several
* 768-cycle sample boundaries before it can acknowledge and re-arm. Only an
* unmasked IRQ9 is observable, like the other sources above. */
if (i_mask & (1u << IRQ_SPU)) {
uint32_t a = psx_spu_sample_event_cycles_to_next(); if (a < best) best = a;
}
if (best == 0) best = 1;
return best;
}
/* Test/diagnostic accessor for the idle-skip observation boundary above. */
uint32_t psx_idle_cycles_to_next_observable_event(void) {
return devices_cycles_to_next_idle_event();
}
static void psx_devices_recompute_deadline(void) {
uint32_t next = devices_cycles_to_next_internal_event();
if (next > PSX_DEADLINE_HARD_CAP) next = PSX_DEADLINE_HARD_CAP;
if (psx_cycle_sample_period && s_next_cycle_sample > psx_cycle_count) {
uint64_t d = s_next_cycle_sample - psx_cycle_count;
if (d < next) next = (uint32_t)d;
}
psx_next_service_cycle = psx_cycle_count + (uint64_t)next;
}
void psx_devices_service_to_now(void) {
if (g_psx_guest_time_frozen) { psx_cycle_freeze_tick(); return; }
if (s_in_device_service) return; /* device code charged cycles: absorb */
if (g_plp_cycle_diag) g_plp_svc_calls++;
g_psx_cycle_fast_limit = 0;
s_in_device_service = 1;
uint64_t target = psx_cycle_count;
if (s_devices_synced_cycle < target) {
/* Rewind and re-play the gap in event-bounded chunks so device
* callbacks see the same incremental psx_cycle_count they always did
* and no chunk ever skips OVER a device event boundary. */
psx_cycle_count = s_devices_synced_cycle;
interrupts_service_scheduled_events();
while (psx_cycle_count < target) {
uint32_t remaining = (uint32_t)(target - psx_cycle_count);
uint32_t step = remaining;
uint32_t to_ev = devices_cycles_to_next_internal_event();
if (to_ev > 0 && to_ev < step) step = to_ev;
if (step == 0) step = 1;
/* Preserve interval causality at an advertised event boundary.
* Devices are advanced in a fixed dependency order (CD before DMA,
* MDEC input before output). Passing the whole D-cycle interval to
* each device lets a state created by an earlier device at cycle D
* be treated by a later device as though it existed for all D
* cycles. Advance the quiet prefix first, then the boundary cycle:
* this is exactly the conservative timeline with O(events), not
* O(cycles), host calls. */
if (step > 1u && to_ev == step) {
advance_devices(step - 1u);
step = 1u;
}
advance_devices(step);
interrupts_service_scheduled_events();
}
s_devices_synced_cycle = target;
}
psx_cycle_sample_check(target);
psx_devices_recompute_deadline();
psx_in_device_service = 0;
/* Sparse host maintenance (was on every psx_advance_cycles). HARD_CAP
* guarantees we land here at least every 16K guest cycles. */
if (target >= s_next_watchdog) {
s_next_watchdog = target + 65536ull;
psx_cycles_watchdog_fire();
#if defined(PSX_HAS_RECOMP_NET)
/* INPUT/CONFIRM retransmit during BIOS free-run (before vblank admit). */
if (psx_netplay_active())
psx_netplay_pump();
#endif
}
if (target >= s_next_pc_sample) {
s_next_pc_sample = target + 1048576ull;
psx_cycles_pc_sample_fire();
}
}
/* memory.c hook: called at the top of every device-MMIO read/write wrapper.
* Catch devices up so the handler sees current state, then recompute the
* deadline immediately (a write may re-arm a shorter event). Previously this
* set psx_next_service_cycle=0 ("dirty"), forcing the *next* per-instruction
* advance through service_to_now even when already synced — MotK FMV pays
* that on every GPU/CD/MDEC MMIO touch. Recompute here instead. */
void psx_devices_mmio_sync(void) {
psx_cyc_batch_flush();
if (g_psx_guest_time_frozen) { psx_cycle_freeze_tick(); return; }
if (s_devices_synced_cycle != psx_cycle_count) {
psx_devices_service_to_now();
} else {
psx_devices_recompute_deadline();
}
}
/* Exact per-charge path (legacy semantics). Used by PSX_COSIM builds and the
* g_event_step_conservative diagnostic; also keeps the deadline-path state
* coherent so the two can interleave (the runtime toggle flips mid-run). */
static void psx_advance_cycles_exact(uint32_t cycles) {
#ifdef PSX_COSIM
/* First-divergence oracle: the guest-cycle counter is the ONLY clock both backends
* share identically, so it is the alignment point for the full-state hash. */
cosim_tick();
#endif
interrupts_service_scheduled_events();
while (cycles > 0) {
uint32_t step = cycles;
if (g_event_step_conservative && step > 1u) {
step = 1u;
} else {
uint32_t to_vblank = interrupts_cycles_to_vblank();
if (to_vblank > 0 && to_vblank < step) step = to_vblank;
#ifdef PSX_COSIM
uint32_t to_cp = cosim_cycles_to_next_checkpoint();
if (to_cp > 0 && to_cp < step) step = to_cp;
#endif
}
if (step == 0) step = cycles;
advance_devices(step);
cycles -= step;
interrupts_service_scheduled_events();
#ifdef PSX_COSIM
cosim_tick();
#endif
}
s_devices_synced_cycle = psx_cycle_count;
psx_next_service_cycle = 0;
}
void psx_cycles_watchdog_fire(void) {
starvation_watchdog_check();
}
void psx_cycles_pc_sample_fire(void) {
starvation_ring_pc_sample();
}
/* Slow path for the inlined psx_advance_cycles (COSIM / lockstep / conservative). */
void psx_advance_cycles_slow(uint32_t cycles) {
{ extern int g_ls_replay_active;
if (g_ls_replay_active) {
/* Replay owns a private-in-time view of the global clock. Advance it
* so GTE/muldiv deadlines and memory wait states see the same time as
* the authoritative pass, but never service devices or observers. */
if (s_cycle_replay_active) psx_cycle_count += (uint64_t)cycles;
return;
}
}
if (cycles == 0) return;
if (g_psx_guest_time_frozen) {
psx_cycle_count += (uint64_t)cycles;
if (psx_cycle_count >= psx_next_service_cycle) psx_cycle_freeze_tick();
return;
}
#ifdef PSX_COSIM
psx_advance_cycles_exact(cycles);
#else
if (g_event_step_conservative) {
psx_advance_cycles_exact(cycles);
} else if (psx_in_device_service) {
psx_cycle_count += (uint64_t)cycles;
} else {
psx_cycle_count += (uint64_t)cycles;
if (psx_next_service_cycle == 0 || psx_cycle_count >= psx_next_service_cycle) {
psx_devices_service_to_now();
}
}
#endif
#if STARVATION_RING_ENABLED
psx_watchdog_throttle += cycles;
if (psx_watchdog_throttle >= 65536u) {
psx_watchdog_throttle = 0;
starvation_watchdog_check();
}
psx_pc_sample_throttle += cycles;
if (psx_pc_sample_throttle >= 1048576u) {
psx_pc_sample_throttle = 0;
psx_cycles_pc_sample_fire();
}
#endif
#if defined(PSX_HAS_RECOMP_NET)
/* Independent of starvation ring (off in PSX_NO_DEBUG_TOOLS release).
* Retransmit INPUT/CONFIRM during BIOS free-run before first vblank. */
{
static uint32_t s_net_pump_throttle;
s_net_pump_throttle += cycles;
if (s_net_pump_throttle >= 65536u) {
s_net_pump_throttle = 0;
if (psx_netplay_active())
psx_netplay_pump();
}
}
#endif
}
uint64_t psx_get_cycle_count(void) {
uint64_t n = psx_cycle_count + (uint64_t)g_psx_cyc_batch;
if (g_psx_cyc_local_acc) n += (uint64_t)(*g_psx_cyc_local_acc);
return n;
}
/* ===== Idle-loop cycle skip (wait-loop elision, 2026-07-06) ==================
*
* A pure poll loop (Tomba2 main loop: `do {} while (vbl_count < target)`)
* burns its full guest-cycle budget at real emulation cost even though every
* iteration is a provable no-op: no stores, no MMIO, and register state at
* the loop's interrupt-check boundary identical each time. Nothing such a
* loop can observe changes except via device servicing, which happens at
* deterministic cycle deadlines. So time is fast-forwarded to the next
* INTERNAL device event (mask-blind min over vblank pacing / timers / cdrom /
* dma / sio — the full set of async guest-visible mutators) in whole loop
* quanta, bit-exact vs executing the iterations:
* - real execution takes a deliverable IRQ at the first check boundary at
* or after the event cycle; the skip lands on exactly that boundary
* (k = ceil(dist / quantum) whole iterations).
* - device RAM writes (DMA) replay at their exact cycles inside
* psx_advance_cycles' deadline-servicing path either way.
* - if the event does not release the loop, the detector re-arms and
* skips to the next internal event.
*
* Detection is evidence-based; ALL gates must hold across IDLE_STREAK_MIN
* consecutive interrupt checks at the SAME resume PC with a STABLE quantum:
* 1. zero guest stores (g_guest_store_count unchanged — covers RAM,
* scratchpad, MMIO and DMA writes; memory.c chokepoints)
* 2. zero MMIO accesses (g_mmio_access_count unchanged — an MMIO read can
* be side-effecting or time-varying, strictly disqualifying. I_STAT-
* polling kernel loops therefore never skip; whitelisting side-effect-
* free I_STAT reads is a possible later refinement.)
* 3. identical GPR/hi/lo fingerprint at the check boundary — rules out
* progressing pure-load loops (strlen/memcmp-style scans) whose exit
* depends on register progress, not time. (COP0/GTE-held loop state is
* not fingerprinted; no realistic poll loop keys its exit on those.)
*
* Excluded contexts: exception delivery, bail unwinds, precise slicing,
* lockstep record/replay, and PSX_COSIM builds entirely (the cosim oracle
* compares per-instruction streams; elided iterations would false-diverge).
*
* Opt in: game config, PSX_IDLE_SKIP=1 (env), or the `idle_skip` TCP command.
* The always-on counters below are the observability surface. */
enum {
IDLE_STREAK_MIN = 4, /* stable same-PC windows before a skip */
IDLE_QUANTUM_MAX = 32768, /* interp pump gap fits; junk deltas don't */
IDLE_SKIP_MAX_CYCLES = 1200000 /* defensive cap (> one vblank interval) */
};
int g_idle_skip_enabled = -1; /* -1 = read PSX_IDLE_SKIP once; TCP-togglable */
uint64_t g_idle_skip_count = 0; /* skips performed */
uint64_t g_idle_skip_cycles = 0; /* guest cycles fast-forwarded */
uint32_t g_idle_skip_last_pc = 0; /* loop PC of the last skip */
uint32_t g_idle_skip_last_quantum = 0;
/* Overlay CPS code reports several interrupt-safe PCs per logical loop
* iteration. The loader suppresses internal/return observations so they do not
* erase evidence collected at the repeated external poll boundary; interrupt
* delivery still runs normally. */
int g_idle_note_suppress = 0;
static uint32_t s_idle_pc = 0;
static uint32_t s_idle_quantum = 0;
static uint32_t s_idle_streak = 0;
static int s_idle_have_snap = 0; /* GPR snapshot valid for s_idle_pc */
static uint32_t s_idle_gpr[32];
static uint32_t s_idle_hi = 0, s_idle_lo = 0;
static int s_idle_progress_reg = -2; /* -2 unknown, -1 stable, 1..31 countdown */
static int32_t s_idle_progress_delta = 0;
static uint64_t s_idle_last_cycle = 0;
static uint64_t s_idle_last_stores = 0;
static uint64_t s_idle_last_mmio = 0;
static void idle_snapshot_regs(const CPUState *cpu) {
for (int i = 1; i < 32; i++) s_idle_gpr[i] = cpu->gpr[i];
s_idle_hi = cpu->hi;
s_idle_lo = cpu->lo;
}
static int idle_skip_on(void) {
/* Cycle-skip is a host enhancement; under netplay it forks peers (detector
* streak / skip quanta are not part of the shared snap). Same for the
* solo resim self-check — keep the window on the faithful cycle path. */
extern int psx_netplay_active(void);
extern int psx_selfcheck_enabled(void);
if (psx_netplay_active() || psx_selfcheck_enabled())
return 0;
if (g_idle_skip_enabled < 0) {
/* No game config reached this process (for example a BIOS-only
* runtime). Keep the enhancement inert unless the environment opts in;
* normal game launches set g_idle_skip_enabled from RuntimeConfig. */
const char *e = getenv("PSX_IDLE_SKIP");
g_idle_skip_enabled = e ? (e[0] == '1') : 0;
}
return g_idle_skip_enabled;
}
int psx_idle_skip_is_enabled(void) { return idle_skip_on(); }
/* Store-counter loops (opt-in on top of idle_skip: [runtime]
* idle_skip_store_counters, PSX_IDLE_SKIP_EXT=1). PsyQ libetc's v_wait --
* the VSync(0)/VSync(n) spin every PsyQ title runs each frame -- is
*
* do { if (--timeout == -1) { timeout path } } while (VSync_count < target);
*
* with `timeout` a stack word: one guest store per iteration, and two block
* edges (interrupt-check boundaries) per iteration. The base detector needs
* zero stores and one edge, so it never fires there. The extension accepts
* - several edges per iteration: the first edge seen is the anchor and up
* to IDLE_EDGES_MAX other edges between two anchor visits are one
* iteration (their count must be stable too);
* - exactly one word store per iteration, to the same RAM/scratchpad
* address, whose value drops by exactly 1 each iteration (a register
* holding the same value may drop with it).
* Skips stay bit-exact: k whole iterations, landing on the anchor strictly
* BEFORE the next device event when the loop has more than one edge (an IRQ
* may be taken at any of them, so the last iteration runs for real), the
* counter (and its register) lowered by k and never to 1 or below, so the
* real timeout test still runs. */
int g_idle_skip_ext = -1; /* -1 = read PSX_IDLE_SKIP_EXT once */
/* Last-store tracking (memory.c writes it only while this is set) and the
* host writer used to lower a skipped counter. Both live here so every
* target that links the cycle model links them too. */
static int s_track_dummy;
static const uint32_t s_no_store = 0xFFFFFFFFu;
static int *s_track = &s_track_dummy;
static const uint32_t *s_last_addr = &s_no_store, *s_last_val = &s_no_store;
static void (*s_idle_host_write)(uint32_t addr, uint32_t val) = NULL;
void psx_idle_skip_set_host_writer(void (*w)(uint32_t, uint32_t)) { s_idle_host_write = w; }
void psx_idle_skip_set_store_tracker(int *gate, const uint32_t *addr, const uint32_t *val) {
s_track = gate ? gate : &s_track_dummy;
s_last_addr = addr ? addr : &s_no_store;
s_last_val = val ? val : &s_no_store;
}
enum { IDLE_EDGES_MAX = 8, IDLE_SITES_MAX = 16 };
/* Audited sites. The detector proves the loop's shape over the passes it
* sees; it cannot prove that no later counter-dependent exit exists (an exit
* at counter == 500 would be skipped over). Only sites a developer audited
* and declared are eligible. */
static uint32_t s_idle_sites[IDLE_SITES_MAX];
static int s_idle_site_count = 0;
void psx_idle_skip_set_store_counter_sites(const uint32_t *pcs, int n) {
if (n < 0 || !pcs) n = 0;
if (n > IDLE_SITES_MAX) n = IDLE_SITES_MAX;
for (int i = 0; i < n; i++) s_idle_sites[i] = pcs[i];
s_idle_site_count = n;
}
static int idle_site_listed(uint32_t pc) {
for (int i = 0; i < s_idle_site_count; i++)
if (s_idle_sites[i] == pc) return 1;
return 0;
}
static uint32_t s_idle_edges = 0; /* non-anchor edges since the anchor */
static uint32_t s_idle_iter_edges = 0; /* edges in the last whole iteration */
static uint32_t s_idle_store_addr = 0xFFFFFFFFu;
static uint32_t s_idle_store_val = 0;
static int s_idle_mem_progress = 0;
static int idle_skip_ext_on(void) {
if (g_idle_skip_ext < 0) {
const char *e = getenv("PSX_IDLE_SKIP_EXT");
g_idle_skip_ext = e ? (e[0] == '1') : 0;
}
/* Without a host writer the counter cannot be lowered: no extension. */
*s_track = g_idle_skip_ext > 0 && s_idle_host_write != NULL && s_track != &s_track_dummy;
return *s_track;
}
/* Stores since the last edge: 0, or (extension) one word store to RAM or
* the scratchpad. *addr/*val name it; addr 0xFFFFFFFF when none. */
static int idle_stores_ok(uint64_t stores_now, int ext, uint32_t *addr, uint32_t *val) {
*addr = 0xFFFFFFFFu;
*val = 0;
const uint64_t d = stores_now - s_idle_last_stores;
if (d == 0) return 1;
if (!ext || d != 1 || *s_last_addr == 0xFFFFFFFFu) return 0;
const uint32_t phys = *s_last_addr & 0x1FFFFFFFu;
if ((phys & 3u) != 0u) return 0;
if (!(phys < 0x00800000u || (phys >= 0x1F800000u && phys < 0x1F800400u))) return 0;
*addr = *s_last_addr;
*val = *s_last_val;
return 1;
}
static void idle_reset_progress(void) {
s_idle_streak = 0;
s_idle_quantum = 0;
s_idle_progress_reg = -2;
s_idle_progress_delta = 0;
s_idle_mem_progress = 0;
}
void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) {
#ifdef PSX_COSIM
(void)cpu; (void)check_pc;
return;
#else
extern int g_psx_call_bail, g_precise_mode, g_ls_mode, g_ls_replay_active;
extern int psx_get_in_exception(void);
extern uint64_t g_guest_store_count, g_mmio_access_count;
/* A speculative replay must not clear or train the authoritative live idle
* detector. Its clock/device view is transactional and cannot be skipped. */
if (g_ls_replay_active) return;
if (!idle_skip_on() || g_idle_note_suppress) return;
if (check_pc == 0 || psx_get_in_exception() || g_psx_call_bail ||
g_precise_mode || g_ls_mode != 0) {
s_idle_pc = 0;
s_idle_streak = 0;
s_idle_have_snap = 0;
s_idle_edges = 0;
return;
}
/* The extension applies only at an audited site: the anchor while
* counting its inner edges, else the PC being checked. */
const uint32_t site = (s_idle_pc != 0 && check_pc != s_idle_pc) ? s_idle_pc : check_pc;
const int ext = idle_skip_ext_on() && idle_site_listed(site);
/* Extension: an edge inside the anchor's iteration. Only MMIO is checked
* here; stores are counted over the whole iteration at the anchor. */
if (ext && s_idle_pc != 0 && check_pc != s_idle_pc && s_idle_edges < IDLE_EDGES_MAX &&
g_mmio_access_count == s_idle_last_mmio) {
s_idle_edges++;
return;
}
uint64_t cyc = psx_cycle_count;
uint32_t st_addr, st_val;
const int stores_ok = idle_stores_ok(g_guest_store_count, ext, &st_addr, &st_val);
if (check_pc == s_idle_pc && s_idle_have_snap && stores_ok &&
g_mmio_access_count == s_idle_last_mmio) {
int changed = -1, changed_count = 0;
for (int i = 1; i < 32; i++) {
if (cpu->gpr[i] != s_idle_gpr[i]) {
changed = i;
changed_count++;
if (changed_count > 1) break;
}
}
if (cpu->hi != s_idle_hi || cpu->lo != s_idle_lo) changed_count = 2;
int32_t progress_delta = changed_count == 1
? (int32_t)(cpu->gpr[changed] - s_idle_gpr[changed]) : 0;
/* Besides the original invariant-register loop, accept exactly one
* monotonically decrementing timeout register. Skipped iterations apply
* the same decrements and stop before zero so the real exit branch still
* executes. No stores/MMIO are allowed, so there is no hidden work. */
int progress_reg = changed_count == 0 ? -1
: (changed_count == 1 && progress_delta == -1 ? changed : -2);
/* Extension: the one store must be the same counter word, one lower
* than last iteration, positive; a progressing register must hold it. */
int mem_progress = 0;
if (st_addr != 0xFFFFFFFFu) {
if (st_addr == s_idle_store_addr && st_val == s_idle_store_val - 1u &&
(int32_t)st_val > 1 &&
(progress_reg <= 0 || cpu->gpr[progress_reg] == st_val))
mem_progress = 1;
else
progress_reg = -2;
} else if (s_idle_store_addr != 0xFFFFFFFFu) {
progress_reg = -2; /* the store pattern changed */
}
uint32_t quantum = (uint32_t)(cyc - s_idle_last_cycle);
if (progress_reg != -2 && quantum > 0 && quantum <= IDLE_QUANTUM_MAX) {
if (quantum == s_idle_quantum &&
progress_reg == s_idle_progress_reg &&
progress_delta == s_idle_progress_delta &&
mem_progress == s_idle_mem_progress &&
s_idle_edges == s_idle_iter_edges) {
if (s_idle_streak < 1000000u) s_idle_streak++;
} else {
s_idle_quantum = quantum;
s_idle_progress_reg = progress_reg;
s_idle_progress_delta = progress_delta;
s_idle_mem_progress = mem_progress;
s_idle_streak = 1;
}
} else {
idle_reset_progress();
}
s_idle_store_addr = st_addr;
s_idle_store_val = st_val;
s_idle_iter_edges = s_idle_edges;
idle_snapshot_regs(cpu);
} else if (check_pc == s_idle_pc && stores_ok &&
g_mmio_access_count == s_idle_last_mmio) {
/* Second consecutive observation of this PC with no stores/MMIO —
* take the baseline snapshot; the next hit can compare. Defers the
* 31-GPR copy away from branchy VLC/decode edges that change resume
* PC every check (pure host cost, never skipped). */
idle_snapshot_regs(cpu);
s_idle_have_snap = 1;
s_idle_store_addr = st_addr;
s_idle_store_val = st_val;
s_idle_iter_edges = s_idle_edges;
idle_reset_progress();
} else {
s_idle_pc = check_pc;
idle_reset_progress();
s_idle_have_snap = 0;
s_idle_store_addr = 0xFFFFFFFFu;
}
s_idle_edges = 0;
s_idle_last_cycle = cyc;
s_idle_last_stores = g_guest_store_count;
s_idle_last_mmio = g_mmio_access_count;
if (s_idle_streak < IDLE_STREAK_MIN) return;
/* Several edges per pass are accepted only for the store-counter shape
* (v_wait). Multi-edge loops with no counter store -- OpenBIOS's
* 0xBFC057C0 poll, kernel 0x16BC -- moved R4's boot timing when skipped
* (frame 192, cyc +3857), so they keep running for real. */
if (s_idle_iter_edges && !s_idle_mem_progress) return;
uint32_t q = s_idle_quantum;
uint32_t dist = devices_cycles_to_next_idle_event();
/* The devices report distances from where they were last serviced
* (s_devices_synced_cycle), which can trail psx_cycle_count by a few
* hundred cycles. For the store-counter shape, measure from now: an
* overstated distance lands the skip after the event and delivers the
* IRQ late (R4 boot, frame 961: +402 cycles). */
if (s_idle_iter_edges || s_idle_mem_progress) {
const uint64_t lag = psx_cycle_count > s_devices_synced_cycle
? psx_cycle_count - s_devices_synced_cycle : 0;
if ((uint64_t)dist <= lag) return;
dist -= (uint32_t)lag;
}
if (dist <= q) return; /* one real iteration reaches it */
uint32_t k;
if (s_idle_iter_edges == 0)
k = (dist + q - 1u) / q; /* first check boundary >= event */
else
k = (dist - 1u) / q; /* land on the anchor before it */
if (s_idle_progress_reg > 0) {
uint32_t value = cpu->gpr[s_idle_progress_reg];
uint32_t max_k = value > 1u ? value - 1u : 0u;
if (k > max_k) k = max_k;
}
if (s_idle_mem_progress) {
uint32_t max_k = (int32_t)s_idle_store_val > 1 ? s_idle_store_val - 1u : 0u;
if (k > max_k) k = max_k;
}
if (k == 0) return;
uint64_t skip = (uint64_t)k * q;
if (skip > IDLE_SKIP_MAX_CYCLES) return;
if (s_idle_progress_reg > 0)
cpu->gpr[s_idle_progress_reg] -= k;
if (s_idle_mem_progress)
s_idle_host_write(s_idle_store_addr, s_idle_store_val - k);
g_idle_skip_count++;
g_idle_skip_cycles += skip;
g_idle_skip_last_pc = check_pc;
g_idle_skip_last_quantum = q;
psx_advance_cycles((uint32_t)skip); /* replays device events exactly */
/* Device servicing may have written RAM / raised I_STAT — the caller
* (psx_check_interrupts) evaluates deliverability right after this with
* the post-skip state. Re-detect from scratch either way. */
s_idle_streak = 0;
idle_snapshot_regs(cpu);
s_idle_have_snap = 1;
s_idle_store_addr = s_idle_mem_progress ? s_idle_store_addr : 0xFFFFFFFFu;
s_idle_store_val = s_idle_store_val - (s_idle_mem_progress ? k : 0u);
s_idle_last_cycle = psx_cycle_count;
s_idle_last_stores = g_guest_store_count;
s_idle_last_mmio = g_mmio_access_count;
#endif
}
/* Save-state restore: psx_cycle_count was just overwritten from the snapshot, so
* the deadline-model bookkeeping (synced position + next deadline) is stale and
* would try to replay a bogus gap. Re-anchor devices at the restored cycle and
* force a fresh deadline on the next charge. */
void psx_cycles_resync_after_restore(CPUState *cpu) {
if (g_psx_cyc_local_acc) *g_psx_cyc_local_acc = 0;
g_psx_cyc_local_acc = NULL;
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
g_psx_cyc_bb_defer = 0;
s_devices_synced_cycle = psx_cycle_count;
psx_next_service_cycle = 0; /* recompute on next charge */
psx_in_device_service = 0;
/* Idle-skip detector latches absolute cycle/store counters from the
* pre-load timeline; drop them so a rewound clock cannot false-train. */
s_idle_pc = 0;
s_idle_streak = 0;
s_idle_have_snap = 0;
s_idle_progress_reg = -2;
s_idle_last_cycle = psx_cycle_count;
/* GTE/muldiv completion deadlines and load-absorb give-back are host-only
* absolute cycle stamps (not in BS_SEC_CPU). After a warm load they still
* hold the pre-load live timeline; the next psx_gte_stall / muldiv_stall
* would then advance (live_ts - restored_cycle) in one shot — tens of
* millions of cycles / N nested presents with zero IRQ checks (MotK
* transform CTC2 path). Anchor them at the restored clock. */
{
/* A snap with BS_SEC_CPUTIM carried them relative to its clock --
* keep those; older snaps anchor at the restored clock as before. */
extern int g_boot_state_cpu_timing_restored;
if (cpu && !g_boot_state_cpu_timing_restored) {
cpu->gte_ts_done = psx_cycle_count;
cpu->muldiv_ts_done = psx_cycle_count;
memset(cpu->read_absorb, 0, sizeof(cpu->read_absorb));
cpu->read_absorb_which = 0;
cpu->read_fudge = 0x20u; /* no committed predecessor load */
cpu->ld_which_t = 0x20u; /* no pending load dest */
cpu->ld_absorb = 0;
}
g_boot_state_cpu_timing_restored = 0;
}
/* Dirty-RAM interpreter load-delay writebacks live in host statics, not
* BS_SEC_CPU. Discard (do not flush): snap GPRs are already architectural.
* A stale pending v0 write from the pre-load timeline was forking MotK
* resim peers (countdown vs BIOS v0=1) at matched guest clocks. */
{
extern void dirty_ram_ld_delay_discard(void);
dirty_ram_ld_delay_discard();
}
/* Entry-poll %%64 stride + 4096-insn pump gap are host-only; reset so
* both peers take the first post-load dirty wait IRQ on the same phase. */
{
extern void dirty_ram_irq_ambient_resync_after_restore(void);
dirty_ram_irq_ambient_resync_after_restore();
}
}
void psx_cycles_reset_for_boot(void) {
if (g_psx_cyc_local_acc) *g_psx_cyc_local_acc = 0;
g_psx_cyc_local_acc = NULL;
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
g_psx_cyc_bb_defer = 0;
psx_cycle_count = 0;
s_devices_synced_cycle = 0;
s_next_service_cycle = 0;
s_in_device_service = 0;
g_psx_cycle_fast_limit = 0;
s_next_watchdog = 0;
s_next_pc_sample = 0;
s_next_cycle_sample = 0;
}
/* ---- Mult/div completion-stall timing (faithful R3000A; Beetle muldiv_ts_done) ----
*
* MULT/MULTU/DIV/DIVU don't stall at the op; they set a completion DEADLINE.
* A later MFLO/MFHI that reads HI/LO before the deadline STALLS (advances guest
* cycles) until it. Instructions executed in between absorb the latency — so the
* stall is (deadline - now), not a flat charge (this is why div+2filler+mflo costs
* the same as div+mflo: the fillers ran during the latency window). REQUIRES
* per-instruction cycle charging (PSX_CODEGEN_CYCLE_PER_INSN / the interp), so
* `now` is the true cycle position at the op — block-up-front charging breaks it.
* Charges still pending in the batch or a local accumulator are part of `now`:
* both helpers publish them before reading the clock (test_muldiv_deferred.c).
*
* Latencies transcribed from Beetle cpu.cpp: DIV/DIVU = 37 (fixed). MULT/MULTU =
* MULT_Tab24 indexed by the leading-zero count of the (sign-folded, for signed)
* first operand | 0x400 — i.e. 7 when it fits in 11 bits, 10 in 20 bits, 14
* otherwise. The | 0x400 caps the index at 21 (never l==0). */
static const uint8_t PSX_MULT_TAB24[24] = {
/* i<12: 7+4+3=14 */ 14,14,14,14,14,14,14,14,14,14,14,14,
/* 12<=i<21: 7+3=10 */ 10,10,10,10,10,10,10,10,10,
/* i>=21: 7 */ 7,7,7
};
static inline uint32_t psx_clz32(uint32_t v) {
/* v is never 0 here (callers OR in 0x400). */
#if defined(_MSC_VER)
unsigned long _idx;
_BitScanReverse(&_idx, v); /* index of highest set bit */
return (uint32_t)(31u - _idx);
#else
return (uint32_t)__builtin_clz(v);
#endif
}
uint32_t psx_mult_latency_s(uint32_t rs) { /* MULT (signed): sign-fold magnitude */
return PSX_MULT_TAB24[psx_clz32((rs ^ (uint32_t)((int32_t)rs >> 31)) | 0x400u)];
}
uint32_t psx_mult_latency_u(uint32_t rs) { /* MULTU (unsigned) */
return PSX_MULT_TAB24[psx_clz32(rs | 0x400u)];
}
/* DIV/DIVU latency is the fixed constant 37 — emitted directly at the op site. */
void psx_muldiv_set(CPUState* cpu, uint32_t latency) {
/* The deadline belongs to this instruction, including unpublished CPU
* work from generated blocks and local charge accumulators. */
psx_cyc_batch_flush();
latency = psx_cpu_cycles(latency); /* [timing] guest_cycle_scale */
cpu->muldiv_ts_done = psx_cycle_count + (uint64_t)latency;
}
void psx_muldiv_stall(CPUState* cpu) {
/* Publish once before comparing; otherwise advance would add the pending
* work a second time on top of a stall computed from a stale clock. */
psx_cyc_batch_flush();
/* MFLO/MFHI stall to the mult/div completion deadline (Beetle cpu.cpp:1723-1736).
* While stalling it CONSUMES a pending load-delay give-back (read_absorb) — each
* stalled cycle decrements read_absorb[read_absorb_which] — so cycles that would
* have been "free" for following instructions are spent here instead. Plus the
* off-by-one shortcut: a deadline exactly one cycle out just retracts (no stall).
* (No-load code has read_absorb==0, so this reduces to a plain advance.) */
if (cpu->muldiv_ts_done > psx_cycle_count) {
if (cpu->muldiv_ts_done == psx_cycle_count + 1u) {
cpu->muldiv_ts_done--; /* off-by-one: retract the deadline, no advance */
return;
}
uint32_t stall = (uint32_t)(cpu->muldiv_ts_done - psx_cycle_count);
uint8_t w = cpu->read_absorb_which; /* fixed during the stall */
uint32_t give = cpu->read_absorb[w];
cpu->read_absorb[w] = (uint8_t)(give > stall ? give - stall : 0u); /* consume */
psx_advance_cycles(stall);
}
}
/* MFC2/CFC2 (GTE register read → GPR): stall to the GTE command completion deadline
* AND hand the stall amount to the next instruction(s) as a load-delay give-back
* (Beetle cpu.cpp:1332-1341: LDAbsorb = gte_ts_done - timestamp, LDWhich = rt). The
* §1+DO_LDS that bracket this ran in the instruction's psx_cyc_step (COP2 is non-load).
* MTC2/CTC2 (writes) use psx_gte_stall (stall only, no give-back). */
void psx_gte_read(CPUState* cpu, uint32_t rt) {
/* Include deferred CPU work before computing a deadline or stall. A
* later advance publishes it too, so using the old clock double-counts
* that work in the stall and can arm a command deadline too early. */
psx_cyc_batch_flush();
if (cpu->gte_ts_done > psx_cycle_count) {
uint32_t stall = (uint32_t)(cpu->gte_ts_done - psx_cycle_count);
cpu->ld_absorb = stall;
psx_advance_cycles(stall);
} else {
cpu->ld_absorb = 0u;
}
cpu->ld_which_t = (uint8_t)rt;
}
/* ---- GTE (COP2) per-command completion-stall timing ----
*
* Faithful R3000A/GTE model (Beetle cpu.cpp:1410-1412 + gte.cpp GTE_Instruction
* return(ret-1)). Each GTE command takes `cost` cycles (gte.cpp per-op returns,
* verified from source); the COP2 instruction's own +1 base is charged
* separately by per-instruction charging, so the *added* deadline latency is
* cost-1. A later COP2 register access (MFC2/CFC2/MTC2/CTC2/LWC2/SWC2) stalls
* until the deadline. Back-to-back commands serialize: psx_gte_set stalls to the
* prior deadline before arming the next (Beetle stalls timestamp to gte_ts_done
* at the command site before computing the new gte_ts_done).
*
* Cost table = (cost-1), indexed by the 6-bit GTE command (instr & 0x3F).
* cost values transcribed + verified from beetle-psx/mednafen/psx/gte.cpp op
* returns: RTPS15 RTPT23 MVMVA8 SQR5 OP6 AVSZ3/4=5 NCLIP8 NCDS19 NCDT44 NCCS17
* NCCT39 NCS14 NCT30 CC11 CDP13 DPCS8 DPCT17 DCPL8 INTPL8 GPF5 GPL5. Unknown/
* undefined commands = 1 cycle (Beetle default ret=1) -> 0 added. */
static const uint8_t PSX_GTE_LAT_M1[64] = {
[0x00] = 14, [0x01] = 14, /* RTPS */
[0x06] = 7, /* NCLIP */
[0x0C] = 5, /* OP */
[0x10] = 7, /* DPCS */
[0x11] = 7, /* INTPL */
[0x12] = 7, /* MVMVA */
[0x13] = 18, /* NCDS */
[0x14] = 12, /* CDP */
[0x16] = 43, /* NCDT */
[0x1A] = 7, /* DCPL (alt of 0x29) */
[0x1B] = 16, /* NCCS */
[0x1C] = 10, /* CC */
[0x1E] = 13, /* NCS */
[0x20] = 29, /* NCT */
[0x28] = 4, /* SQR */
[0x29] = 7, /* DCPL */
[0x2A] = 16, /* DPCT */
[0x2D] = 4, /* AVSZ3 */
[0x2E] = 4, /* AVSZ4 */
[0x30] = 22, /* RTPT */
[0x3D] = 4, /* GPF */
[0x3E] = 4, /* GPL */
[0x3F] = 38, /* NCCT */
};
uint32_t psx_gte_cmd_latency(uint32_t cmd) {
return (uint32_t)PSX_GTE_LAT_M1[cmd & 0x3Fu];
}
void psx_gte_set(CPUState* cpu, uint32_t latency) {
/* Include deferred CPU work before computing a deadline or stall. A
* later advance publishes it too, so using the old clock double-counts
* that work in the stall and can arm a command deadline too early. */
psx_cyc_batch_flush();
/* Back-to-back GTE ops serialize: finish the prior op first. */
if (cpu->gte_ts_done > psx_cycle_count) {
psx_advance_cycles((uint32_t)(cpu->gte_ts_done - psx_cycle_count));
}
latency = psx_cpu_cycles(latency); /* [timing] guest_cycle_scale */
cpu->gte_ts_done = psx_cycle_count + (uint64_t)latency;
}
void psx_gte_stall(CPUState* cpu) {
/* Include deferred CPU work before computing a deadline or stall. A
* later advance publishes it too, so using the old clock double-counts
* that work in the stall and can arm a command deadline too early. */
psx_cyc_batch_flush();
if (cpu->gte_ts_done > psx_cycle_count) {
psx_advance_cycles((uint32_t)(cpu->gte_ts_done - psx_cycle_count));
}
}
static void freeze_save(PsxCycleFreeze *save) {
extern uint64_t g_psx_cycle_fast_limit;
save->cycle_count = psx_cycle_count;
save->next_service = psx_next_service_cycle;
save->fast_limit = g_psx_cycle_fast_limit;
save->batch = g_psx_cyc_batch;
save->batch_limit = g_psx_cyc_batch_limit;
save->local_acc = g_psx_cyc_local_acc;
save->local_acc_value = g_psx_cyc_local_acc ? *g_psx_cyc_local_acc : 0u;
save->in_device_service = psx_in_device_service;
save->bb_defer = g_psx_cyc_bb_defer;
psx_guest_cycle_scale_snapshot(save->guest_cycle_scale);
/* Pending deferred charges stay pending: they belong to the interrupted
* guest code and are republished, unchanged, after the freeze. */
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
g_psx_cyc_local_acc = NULL;
psx_in_device_service = 0;
s_freeze_start = psx_cycle_count;
}
/* The interrupted code's deferred-charge state, which the frozen code never
* owned, comes back in every case. */
static void freeze_restore_deferred(const PsxCycleFreeze *save) {
g_psx_cyc_batch = save->batch;
g_psx_cyc_batch_limit = save->batch_limit;
g_psx_cyc_local_acc = save->local_acc;
if (save->local_acc) *save->local_acc = save->local_acc_value;
psx_in_device_service = save->in_device_service;
g_psx_cyc_bb_defer = save->bb_defer;
}
static void freeze_restore_clock(const PsxCycleFreeze *save) {
extern uint64_t g_psx_cycle_fast_limit;
psx_cycle_count = save->cycle_count;
psx_next_service_cycle = save->next_service;
g_psx_cycle_fast_limit = save->fast_limit;
}
int psx_cycle_freeze_begin(PsxCycleFreeze *save, uint64_t watchdog_cycles,
void (*overrun)(void)) {
if (!save || g_psx_guest_time_frozen) return 0;
freeze_save(save);
s_freeze_limit = watchdog_cycles;
s_freeze_overrun = overrun;
g_psx_render_pass_active = 1;
g_psx_guest_time_frozen = 1;
psx_cycle_freeze_tick();
return 1;
}
void psx_cycle_freeze_end(const PsxCycleFreeze *save) {
g_psx_render_pass_active = 0;
g_psx_guest_time_frozen = 0;
s_freeze_overrun = 0;
s_freeze_limit = 0;
if (!save) return;
freeze_restore_clock(save);
freeze_restore_deferred(save);
psx_guest_cycle_scale_restore(save->guest_cycle_scale);
}
int psx_cycle_uncharged_begin(PsxCycleFreeze *save, uint64_t budget_cycles) {
if (!save || g_psx_guest_time_frozen) return 0;
freeze_save(save);
s_freeze_limit = budget_cycles;
s_freeze_overrun = 0;
s_uncharged_active = 1;
s_uncharged_thawed = 0;
g_psx_guest_time_frozen = 1;
psx_cycle_freeze_tick();
return 1;
}
uint64_t psx_cycle_uncharged_counted(void) {
if (!s_uncharged_active) return 0;
return psx_cycle_count - s_freeze_start + g_psx_cyc_batch;
}
int psx_cycle_uncharged_end(const PsxCycleFreeze *save) {
const int uncharged = !s_uncharged_thawed;
g_psx_guest_time_frozen = 0;
s_uncharged_active = 0;
s_uncharged_thawed = 0;
s_freeze_limit = 0;
if (!save) return 0;
if (uncharged) freeze_restore_clock(save);
freeze_restore_deferred(save);
return uncharged;
}