mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
91cd6db705
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>
288 lines
12 KiB
C
288 lines
12 KiB
C
#ifndef PSXRECOMP_PSX_CYCLES_H
|
|
#define PSXRECOMP_PSX_CYCLES_H
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* PSX guest CPU cycle clock — the single source of truth for guest-
|
|
* visible time. Peripherals derive all schedules from this counter. */
|
|
extern uint64_t psx_cycle_count;
|
|
|
|
/* Deadline-device model bookkeeping (written by psx_cycles.c). Hot path
|
|
* reads these from the inlined psx_advance_cycles below. */
|
|
extern uint64_t psx_next_service_cycle; /* absolute; 0 = dirty / recompute */
|
|
extern int psx_in_device_service; /* re-entrancy guard */
|
|
extern int g_event_step_conservative;
|
|
|
|
/* Diagnostic replay clock: advance the CPU-visible guest clock without
|
|
* servicing devices, then restore the authoritative live clock afterward.
|
|
* Used only while g_ls_replay_active is set. */
|
|
int psx_cycle_replay_begin(uint64_t start_cycle);
|
|
uint64_t psx_cycle_replay_end(void);
|
|
|
|
/* Transactional I-cache view for the overlay differential harness. The
|
|
* interpreter keeps the authoritative post-call cache; native replay starts
|
|
* from the saved entry tags, mutates a temporary view, then restores live. */
|
|
int psx_icache_shadow_record_begin(void);
|
|
int psx_icache_shadow_replay_begin(void);
|
|
void psx_icache_shadow_replay_end(void);
|
|
void psx_icache_shadow_abort(void);
|
|
|
|
/* Event-deadline device model: catch every device up to the charged
|
|
* guest-cycle position and force a deadline recompute. memory.c calls
|
|
* this at the top of every device-MMIO read/write. */
|
|
void psx_devices_mmio_sync(void);
|
|
void psx_devices_service_to_now(void);
|
|
|
|
/* Rare/slow advance path (COSIM, conservative 1-cycle stepping, lockstep). */
|
|
void psx_advance_cycles_slow(uint32_t cycles);
|
|
|
|
/* Sparse throttle fires (watchdog / PC sample) — not on the per-charge path. */
|
|
extern uint32_t psx_watchdog_throttle;
|
|
extern uint32_t psx_pc_sample_throttle;
|
|
void psx_cycles_watchdog_fire(void);
|
|
void psx_cycles_pc_sample_fire(void);
|
|
|
|
/* Lockstep replay flag (defined in dirty_ram_interp.c). */
|
|
extern int g_ls_replay_active;
|
|
|
|
/* Deferred under-deadline charges (MotK VLC load-charge batching).
|
|
* psx_cyc_charge accumulates here; publish via psx_cyc_batch_flush /
|
|
* psx_advance_cycles before IRQ checks, MMIO, or any cycle read that must
|
|
* match the published counter. Guest totals at those barriers are unchanged. */
|
|
extern uint32_t g_psx_cyc_batch;
|
|
extern uint32_t g_psx_cyc_batch_limit;
|
|
|
|
/* GCC/Clang-generated functions can defer deadline probes within a basic
|
|
* block. Interrupt/MMIO edges still publish the accumulated guest cycles. */
|
|
extern int g_psx_cyc_bb_defer;
|
|
|
|
/* Emitter-level VLC load-charge batching: when non-NULL, psx_cyc_charge
|
|
* accumulates into *g_psx_cyc_local_acc instead of g_psx_cyc_batch. Publish
|
|
* via psx_cyc_local_publish / psx_cyc_batch_flush before IRQ/MMIO barriers. */
|
|
extern uint32_t *g_psx_cyc_local_acc;
|
|
|
|
/* [timing] guest_cycle_scale — a title constant, not a player setting.
|
|
* No CPU is emulated: recompiled code charges the guest clock a fixed number
|
|
* of cycles per MIPS instruction it runs, and that clock is what VBlank,
|
|
* timers, CD, SPU and DMA are scheduled against. guest_cycle_scale = N
|
|
* charges each instruction 1/N of its cost; device time is unchanged.
|
|
* Larger N means the game never runs out of frame time; 1 is faithful and
|
|
* skips the scaling entirely. g_psx_gcs_recip_q16 is 65536/N, g_psx_gcs_frac
|
|
* the carried fraction (part of the rollback/savestate snapshot).
|
|
*
|
|
* Two kinds of guest-time charge, and only these two:
|
|
* psx_cpu_charge(n) CPU work: instruction base cost, load costs
|
|
* (fudge, region wait, completion), i-cache refills,
|
|
* BIOS HLE costs. Scaled. Every tier (native
|
|
* generated code, overlay DLLs, the dirty-RAM
|
|
* interpreter, the BIOS) charges CPU work through
|
|
* it; the batched psx_cyc_charge hot path hands its
|
|
* charge to it whenever the scale is active.
|
|
* psx_advance_cycles(n) device time and stalls already measured in guest
|
|
* time (DMA, idle skips, mul/div and GTE deadline
|
|
* waits). Never scaled.
|
|
* Mul/div and GTE latencies are CPU work too: psx_cpu_cycles() scales the
|
|
* deadline they set, so the later stall waits the scaled time. */
|
|
extern uint32_t g_psx_gcs_recip_q16;
|
|
extern uint32_t g_psx_gcs_frac;
|
|
void psx_guest_cycle_scale_set(uint32_t scale); /* 1..64; 1 = faithful */
|
|
/* Live scale: the configured one, or 1 while gated and the gate is shut. */
|
|
uint32_t psx_guest_cycle_scale(void);
|
|
uint32_t psx_guest_cycle_scale_config(void);
|
|
/* Gates (all must be open for the scale to apply; with none it always does):
|
|
* - RAM gate: [timing] guest_cycle_scale_gate, predicates on guest RAM
|
|
* words evaluated at every VBlank edge (deterministic, works online).
|
|
* - Mod gate: guest_cycle_scale_gated = true; a trusted plugin opens it
|
|
* with psx_mod_set_guest_cycle_scale_gate() (mod_plugins.h). */
|
|
void psx_guest_cycle_scale_set_gated(int gated);
|
|
void psx_guest_cycle_scale_gate_open(int open);
|
|
enum { PSX_GCS_RAM_GATE_MAX = 8 };
|
|
/* addr: guest main-RAM address (any segment), size 1/2/4, open while
|
|
* (word & mask) == value. Returns 0 for a bad predicate. Clear with
|
|
* psx_guest_cycle_scale_ram_gate_clear(). */
|
|
int psx_guest_cycle_scale_ram_gate_add(uint32_t addr, uint32_t size,
|
|
uint32_t mask, uint32_t value);
|
|
void psx_guest_cycle_scale_ram_gate_clear(void);
|
|
/* How the RAM gate reads guest RAM (main.cpp installs a main-RAM reader;
|
|
* phys is masked physical, aligned to size). */
|
|
void psx_guest_cycle_scale_set_ram_reader(uint32_t (*read)(uint32_t phys, uint32_t size));
|
|
/* Called on each VBlank edge (interrupts.c); no-op without a RAM gate. */
|
|
void psx_guest_cycle_scale_vblank(void);
|
|
/* Rollback/savestate: carried fraction + gate state (boot_state BS_SEC_GCS). */
|
|
enum { PSX_GCS_SNAPSHOT_BYTES = 12 };
|
|
void psx_guest_cycle_scale_snapshot(uint32_t out[3]);
|
|
void psx_guest_cycle_scale_restore(const uint32_t in[3]);
|
|
static inline uint32_t psx_gcs_recip_for(uint32_t scale) {
|
|
if (scale <= 1u) return 65536u;
|
|
if (scale > 64u) scale = 64u;
|
|
return (uint32_t)((65536u + scale / 2u) / scale);
|
|
}
|
|
static inline uint32_t psx_gcs_scale(uint32_t cpu_cycles) {
|
|
uint64_t t = (uint64_t)cpu_cycles * g_psx_gcs_recip_q16 + g_psx_gcs_frac;
|
|
g_psx_gcs_frac = (uint32_t)(t & 0xFFFFu);
|
|
return (uint32_t)(t >> 16);
|
|
}
|
|
#if defined(__GNUC__) || defined(__clang__)
|
|
#define PSX_GCS_ACTIVE() __builtin_expect(g_psx_gcs_recip_q16 != 65536u, 0)
|
|
#else
|
|
#define PSX_GCS_ACTIVE() (g_psx_gcs_recip_q16 != 65536u)
|
|
#endif
|
|
#if !defined(PSX_OVERLAY_DLL_BUILD)
|
|
/* CPU cycles -> guest cycles. Identity at scale 1 (no state touched). */
|
|
static inline uint32_t psx_cpu_cycles(uint32_t cpu_cycles) {
|
|
return PSX_GCS_ACTIVE() ? psx_gcs_scale(cpu_cycles) : cpu_cycles;
|
|
}
|
|
#endif
|
|
|
|
/* Advance guest time. Overlay DLLs forward this through their callback shim;
|
|
* normal runtime/generated code keeps the common production path inlined. */
|
|
#if defined(PSX_OVERLAY_DLL_BUILD)
|
|
void psx_advance_cycles(uint32_t cycles);
|
|
#else
|
|
/* The common production path is inlined: bump the
|
|
* counter and only service devices when the next event deadline is due.
|
|
* Guest-visible timing is unchanged (service_to_now replays exact events).
|
|
*
|
|
* Watchdog / PC-sample throttles live in psx_devices_service_to_now (fired
|
|
* on the HARD_CAP / event cadence, ≥ every 16K guest cycles) — not on every
|
|
* per-instruction charge. MotK VLC issues millions of advances/s; two add+
|
|
* branch pairs there were pure host tax. */
|
|
static inline void psx_advance_cycles(uint32_t cycles) {
|
|
#if !defined(PSX_COSIM)
|
|
if (g_psx_cyc_batch) {
|
|
uint32_t b = g_psx_cyc_batch;
|
|
g_psx_cyc_batch = 0;
|
|
g_psx_cyc_batch_limit = 0;
|
|
if (cycles <= UINT32_MAX - b) cycles += b;
|
|
else {
|
|
/* Extreme: publish b first, then continue with cycles. */
|
|
psx_cycle_count += (uint64_t)b;
|
|
if (!psx_in_device_service &&
|
|
(psx_next_service_cycle == 0u ||
|
|
psx_cycle_count >= psx_next_service_cycle)) {
|
|
psx_devices_service_to_now();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
if (cycles == 0u) return;
|
|
#if defined(PSX_COSIM)
|
|
psx_advance_cycles_slow(cycles);
|
|
return;
|
|
#else
|
|
#if defined(__GNUC__) || defined(__clang__)
|
|
if (__builtin_expect(g_ls_replay_active | g_event_step_conservative, 0)) {
|
|
#else
|
|
if (g_ls_replay_active || g_event_step_conservative) {
|
|
#endif
|
|
psx_advance_cycles_slow(cycles);
|
|
return;
|
|
}
|
|
if (psx_in_device_service) {
|
|
psx_cycle_count += (uint64_t)cycles;
|
|
return;
|
|
}
|
|
psx_cycle_count += (uint64_t)cycles;
|
|
if (psx_next_service_cycle == 0u ||
|
|
psx_cycle_count >= psx_next_service_cycle) {
|
|
psx_devices_service_to_now();
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
/* The CPU charge (see above): every tier's CPU work is scaled in this one
|
|
* function (psx_cpu_cycles does the same for mul/div and GTE deadlines,
|
|
* which are set, not charged). At scale 1 it is exactly psx_advance_cycles(cycles). Overlay DLLs
|
|
* accumulate it locally (overlay_dispatch_preamble.c.inc) and publish the
|
|
* raw total through the cpu_charge callback, which lands in this function. */
|
|
#if defined(PSX_OVERLAY_DLL_BUILD)
|
|
void psx_cpu_charge(uint32_t cycles);
|
|
#else
|
|
static inline void psx_cpu_charge(uint32_t cycles) {
|
|
if (PSX_GCS_ACTIVE()) {
|
|
cycles = psx_gcs_scale(cycles);
|
|
if (cycles == 0u) return;
|
|
}
|
|
psx_advance_cycles(cycles);
|
|
}
|
|
#endif
|
|
|
|
/* Publish deferred charges (IRQ edge / MMIO / savestate). Overlay DLLs keep
|
|
* their pending total in the callback shim rather than these host globals. */
|
|
#if defined(PSX_OVERLAY_DLL_BUILD)
|
|
PSX_OVERLAY_EXPORT void overlay_flush_cycles(void);
|
|
static inline void psx_cyc_local_publish(void) { }
|
|
static inline void psx_cyc_batch_flush(void) { overlay_flush_cycles(); }
|
|
#else
|
|
/* Publish function-local charges into the normal batch/advance path while
|
|
* keeping the local pointer installed (nested charges resume locally). */
|
|
static inline void psx_cyc_local_publish(void) {
|
|
#if !defined(PSX_COSIM)
|
|
uint32_t *acc = g_psx_cyc_local_acc;
|
|
if (!acc) return;
|
|
uint32_t v = *acc;
|
|
if (!v) return;
|
|
*acc = 0;
|
|
g_psx_cyc_local_acc = NULL;
|
|
psx_advance_cycles(v);
|
|
g_psx_cyc_local_acc = acc;
|
|
#endif
|
|
}
|
|
|
|
static inline void psx_cyc_batch_flush(void) {
|
|
#if !defined(PSX_COSIM)
|
|
psx_cyc_local_publish();
|
|
uint32_t b = g_psx_cyc_batch;
|
|
if (!b) return;
|
|
g_psx_cyc_batch = 0;
|
|
g_psx_cyc_batch_limit = 0;
|
|
psx_advance_cycles(b);
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
/* Read accessor for telemetry (includes deferred batch). */
|
|
uint64_t psx_get_cycle_count(void);
|
|
|
|
/* Idle-loop cycle skip (see psx_cycles.c "Idle-loop cycle skip"). */
|
|
struct CPUState;
|
|
void psx_idle_note_check(struct CPUState *cpu, uint32_t check_pc);
|
|
int psx_idle_skip_is_enabled(void);
|
|
/* Cycles until the nearest IRQ-observable device event (mask-aware); the bound an idle skip may not cross. */
|
|
uint32_t psx_idle_cycles_to_next_observable_event(void);
|
|
extern int g_idle_skip_enabled;
|
|
extern uint64_t g_idle_skip_count;
|
|
extern uint64_t g_idle_skip_cycles;
|
|
extern uint32_t g_idle_skip_last_pc;
|
|
extern uint32_t g_idle_skip_last_quantum;
|
|
|
|
/* Post-load probe cycle diagnostics (optional; main.cpp soft-load tooling). */
|
|
extern int g_plp_cycle_diag;
|
|
extern uint64_t g_plp_adv_calls;
|
|
extern uint32_t g_plp_adv_max_chunk;
|
|
extern uint64_t g_plp_adv_sum;
|
|
extern uint64_t g_plp_svc_calls;
|
|
|
|
/* Save-state restore: re-anchor the deadline device model after psx_cycle_count
|
|
* is overwritten from a snapshot. Pass the live CPU so GTE/muldiv completion
|
|
* stamps and load-absorb give-back are rewound with the guest clock. */
|
|
void psx_cycles_resync_after_restore(struct CPUState *cpu);
|
|
|
|
/* Soft rematch / session_reboot: zero the guest clock and deadline bookkeeping.
|
|
* Soft-exit longjmps out of vblank (inside psx_devices_service_to_now) leave
|
|
* psx_in_device_service stuck at 1 and a huge leftover cycle count — without
|
|
* this reset the next match never services devices or fires vblanks. */
|
|
void psx_cycles_reset_for_boot(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSXRECOMP_PSX_CYCLES_H */
|