Files
psxrecomp/runtime/include/psx_cyc.h
T
Shokunin c79f5d8c84 gcs batch: publish on the guest clock alone
The batched scaled charge fell through to the scale-1 batch path, which
also defers while g_psx_cyc_bb_defer is nonzero and routes through the VLC
local sink. Both are host call-stack state (a longjmp escape leaks or
resets the depth), so which charge published the batch -- and so which
clock each device saw -- differed between hosts. R4 2P rollback Mac<->PC
with modern controls split by a few cycles in live play (clk/tim fork at
~3296 with cpu/ram equal), then baseline mismatches and a realign storm
dropped the match; with the batch off the same pair matched.

The scaled path now batches on the guest clock only (device deadline or
the 64-cycle soft cap). New check: the service log is identical with
bb_defer 0 or 338 and with a local sink installed (fails before). Re-stamp
the OpenBIOS emitter sha (psx_cyc.h is in the codegen hash).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:31:26 -07:00

362 lines
15 KiB
C

/* psx_cyc.h — R3000A load-delay pipeline interlock (ReadAbsorb / ReadFudge /
* LDAbsorb / LDWhich). TIMING ONLY — the load VALUE delay is handled by the
* existing load-delay correctness path; this models only the CPU cycle cost.
*
* FAITHFUL_TIMING_PLAN.md / accuracy/load_readfudge_ldabsorb.md. Transcribed
* (facts, not code) from the in-tree Beetle oracle
* psxrecomp/beetle-psx/mednafen/psx/cpu.cpp (RunReal §1 base 795-798, GPR_DEPRES
* 702-705, DO_LDS 800, ReadMemory 364-451) and libretro.cpp (main-RAM read wait
* = +3, libretro.cpp:884). Every backend — the dirty-RAM interpreter and BOTH
* static emitters (code_generator.cpp, full_function_emitter.cpp/strict_translator.cpp)
* — accounts guest CPU cycles THROUGH THESE HELPERS so they can never disagree.
*
* Model (per retired instruction, in program order):
* §1 base : if(ReadAbsorb[ReadAbsorbWhich]) ReadAbsorb[ReadAbsorbWhich]--;
* else timestamp++; -- a pending load's give-back is
* consumed INSTEAD of the +1 base (pipeline write-back overlap).
* deps : for each GPR this opcode reads/writes, ReadAbsorb[reg]=0 (ends a
* give-back when the loaded value is used). ReadAbsorb[0] preserved.
* DO_LDS : commit the PREVIOUS instruction's pending load TIMING:
* ReadAbsorb[LDWhich]=LDAbsorb; ReadFudge=LDWhich; ...; LDWhich=0x20.
* load only : ReadMemory — clear the current give-back slot, charge the +2 fudge
* iff the predecessor committed no load (ReadFudge==0x20), charge the
* region wait (main RAM +3) + completion (+2 CPU / +1 LWC2); the
* (region+completion) becomes this load's LDAbsorb give-back, armed on
* LDWhich=rt for the NEXT instruction's DO_LDS to commit.
*/
#ifndef PSX_CYC_H
#define PSX_CYC_H
#include <stdint.h>
#include <string.h>
#if defined(_MSC_VER)
#include <intrin.h> /* MSVC intrinsics: _BitScanForward (no __builtin_ctz) */
#endif
#include "cpu_state.h" /* CPUState (guard-safe: cpu_state.h includes us last) */
#include "psx_cycles.h" /* inline psx_advance_cycles */
#include "psx_memory.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Load-charge batching (MotK VLC): cache the nearer of the next device
* deadline or a 64-cycle soft publication limit once per batch. Generated
* GCC/Clang blocks may defer to their IRQ edge; MMIO also flushes. Pipeline
* state still updates per instruction, and guest totals at barriers match. */
enum { PSX_CYC_BATCH_SOFT = 64u };
#if defined(PSX_OVERLAY_DLL_BUILD)
/* The overlay shim already batches cycle charges until a host callback edge. */
static inline void psx_cyc_bb_defer_begin(void) { }
static inline void psx_cyc_bb_defer_end(void) { }
static inline void psx_cyc_bb_defer_flush(void) { overlay_flush_cycles(); }
static inline void psx_cyc_local_begin(uint32_t *acc) { (void)acc; }
static inline void psx_cyc_local_end(void) { }
#else
static inline void psx_cyc_bb_defer_begin(void) { g_psx_cyc_bb_defer++; }
static inline void psx_cyc_bb_defer_end(void) {
if (g_psx_cyc_bb_defer > 0) g_psx_cyc_bb_defer--;
if (g_psx_cyc_bb_defer == 0) psx_cyc_batch_flush();
}
static inline void psx_cyc_bb_defer_flush(void) { psx_cyc_batch_flush(); }
/* Install a function-local charge sink (MotK VLC leaves). Soft-cap publish
* at 1<<28 so uint32 cannot wrap; IRQ/MMIO still go through batch_flush. */
static inline void psx_cyc_local_begin(uint32_t *acc) {
#if !defined(PSX_COSIM)
if (acc) *acc = 0;
g_psx_cyc_local_acc = acc;
#else
(void)acc;
#endif
}
static inline void psx_cyc_local_end(void) {
#if !defined(PSX_COSIM)
psx_cyc_local_publish();
g_psx_cyc_local_acc = NULL;
#endif
}
#endif
#if defined(__GNUC__) || defined(__clang__)
static inline void psx_cyc_bb_defer_cleanup(int *guard) {
(void)guard;
psx_cyc_bb_defer_end();
}
static inline void psx_cyc_local_cleanup(uint32_t **guard) {
(void)guard;
psx_cyc_local_end();
}
#endif
static inline void psx_cyc_charge(uint32_t cycles) {
if (cycles == 0u) return;
#if defined(PSX_OVERLAY_DLL_BUILD)
psx_cpu_charge(cycles);
#else
/* [timing] guest_cycle_scale: a scaled charge is the CPU charge itself
* (the one place CPU work is scaled); the batch below is the scale-1
* production path, unchanged. */
if (PSX_GCS_ACTIVE()) {
/* Opt-in ([timing] guest_cycle_scale_batch, PSX_GCS_BATCH=0/1): scale
* here, exactly as psx_cpu_charge would (same fractional carry, same
* order), then batch the scaled cycles like the scale-1 path instead
* of publishing every 1-cycle step to the devices. The batch is
* flushed at the same observation points (stores, MMIO, interrupt
* checks, deadlines) as at scale 1. */
if (!g_psx_gcs_batch) {
psx_cpu_charge(cycles);
return;
}
cycles = psx_gcs_scale(cycles);
if (cycles == 0u) return;
#if !defined(PSX_COSIM)
/* Publish on the guest clock alone: the device deadline or the soft
* cap. The scale-1 path below also defers while g_psx_cyc_bb_defer is
* nonzero and routes through the VLC local sink; both are host call-
* stack state (longjmp escapes leak or reset the depth), so which
* charge published the batch -- and so which device saw which clock
* when -- differed between a Mac and a Windows peer, and the scaled
* race split by a few cycles (R4 2P Mac<->PC: clk/tim fork at ~3296
* with cpu/ram equal; matching with the batch off). */
if (!g_ls_replay_active && !g_event_step_conservative &&
!psx_in_device_service) {
uint32_t prior = g_psx_cyc_batch;
uint32_t sum = prior + cycles;
if (sum >= prior) {
if (prior == 0u) {
uint64_t room = 0u;
if (psx_next_service_cycle > psx_cycle_count)
room = psx_next_service_cycle - psx_cycle_count;
g_psx_cyc_batch_limit = room == 0u ? 1u :
(room < (uint64_t)PSX_CYC_BATCH_SOFT
? (uint32_t)room : (uint32_t)PSX_CYC_BATCH_SOFT);
}
if (sum < g_psx_cyc_batch_limit) {
g_psx_cyc_batch = sum;
return;
}
}
psx_advance_cycles(cycles);
return;
}
#endif
}
#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(cycles);
return;
}
if (psx_in_device_service) {
psx_cycle_count += (uint64_t)cycles;
return;
}
#if !defined(PSX_COSIM)
/* Emitter-gated VLC path: stack local, no deadline probe. */
if (g_psx_cyc_local_acc) {
uint32_t prior = *g_psx_cyc_local_acc;
if (cycles <= UINT32_MAX - prior && (prior + cycles) < (1u << 28)) {
*g_psx_cyc_local_acc = prior + cycles;
return;
}
*g_psx_cyc_local_acc = 0;
{
uint32_t *saved = g_psx_cyc_local_acc;
g_psx_cyc_local_acc = NULL;
if (prior) psx_advance_cycles(prior);
g_psx_cyc_local_acc = saved;
}
*g_psx_cyc_local_acc = cycles;
return;
}
{
uint32_t prior = g_psx_cyc_batch;
uint32_t sum = prior + cycles;
if (sum >= prior) { /* no uint32 wrap */
if (g_psx_cyc_bb_defer > 0) {
g_psx_cyc_batch = sum;
return;
}
if (prior == 0u) {
uint64_t room = 0u;
if (psx_next_service_cycle > psx_cycle_count)
room = psx_next_service_cycle - psx_cycle_count;
g_psx_cyc_batch_limit = room == 0u ? 1u :
(room < (uint64_t)PSX_CYC_BATCH_SOFT
? (uint32_t)room : (uint32_t)PSX_CYC_BATCH_SOFT);
}
if (sum < g_psx_cyc_batch_limit) {
g_psx_cyc_batch = sum;
return;
}
}
}
#endif
psx_advance_cycles(cycles); /* publishes any pending batch first */
#endif
}
/* §1 base (Beetle cpu.cpp:795-798). */
static inline void psx_cyc_base(CPUState* cpu) {
uint8_t w = cpu->read_absorb_which;
if (cpu->read_absorb[w]) cpu->read_absorb[w]--;
else psx_cyc_charge(1u);
}
/* GPR_DEPRES (Beetle cpu.cpp:702-705): zero ReadAbsorb[n] for every source/dest
* GPR of this instruction, preserving ReadAbsorb[0] (skipping bit 0 == Beetle's
* save/restore of ReadAbsorb[0]). */
static inline void psx_cyc_deps(CPUState* cpu, uint32_t reg_mask) {
reg_mask &= 0xFFFFFFFEu; /* never touch ReadAbsorb[0] */
if (reg_mask && (reg_mask & (reg_mask - 1u)) == 0u) {
#if defined(_MSC_VER)
unsigned long n;
_BitScanForward(&n, reg_mask);
cpu->read_absorb[(unsigned)n] = 0u;
#else
cpu->read_absorb[(unsigned)__builtin_ctz(reg_mask)] = 0u;
#endif
return;
}
while (reg_mask) {
#if defined(_MSC_VER)
unsigned long _psx_ctz_idx;
_BitScanForward(&_psx_ctz_idx, reg_mask);
unsigned n = (unsigned)_psx_ctz_idx;
#else
unsigned n = (unsigned)__builtin_ctz(reg_mask);
#endif
cpu->read_absorb[n] = 0u;
reg_mask &= reg_mask - 1u;
}
}
/* DO_LDS timing-commit (Beetle cpu.cpp:800). LDWhich==0x20 (no pending) writes the
* dummy slot read_absorb[32] and sets read_fudge=0x20 (=> next load gets +2 fudge). */
static inline void psx_cyc_lds(CPUState* cpu) {
uint8_t lw = cpu->ld_which_t;
cpu->read_absorb[lw] = (uint8_t)cpu->ld_absorb;
cpu->read_fudge = lw;
cpu->read_absorb_which = (uint8_t)(cpu->read_absorb_which | (lw & 0x1Fu));
cpu->ld_which_t = 0x20u;
}
/* Full per-instruction interlock for a NON-CPU-load instruction (ALU, shift,
* branch, jump, store, COP control, LWC2/SWC2 pre-step, mult/div, mfhi/mflo, ...).
* reg_mask from psx_cyc_dep_res_mask(). MUST be emitted BEFORE the instruction
* body so §1 precedes any muldiv/GTE deadline stall in the body (Beetle order). */
static inline void psx_cyc_step(CPUState* cpu, uint32_t reg_mask) {
psx_cyc_base(cpu);
psx_cyc_deps(cpu, reg_mask);
psx_cyc_lds(cpu);
}
/* The GPR dep+res bitmask used by psx_cyc_step lives in psx_instr_cost.h
* (psx_cyc_dep_res_mask) — a standalone pure function shared by the emitters
* (gen-time literal) and the interpreter (runtime), with no CPUState dependency. */
/* Main-RAM base + load-delay gate (memory.c). Inlined load helpers use these
* so MotK VLC / decode hot paths avoid an out-of-line call per LW/LH. */
extern uint8_t *g_psx_ram;
extern int g_psx_load_delay;
extern int g_ls_mode;
extern volatile int g_ds_recording;
int psx_load_delay_enabled(void);
uint32_t psx_cyc_load_word_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
uint16_t psx_cyc_load_half_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
/* Overlay DLLs route timed loads through the host callback shim so they share
* runtime timing and memory state. Normal generated code keeps the main-RAM
* fast path inline. */
#if defined(PSX_OVERLAY_DLL_BUILD)
uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask);
uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask);
#else
/* CPU data load value+timing. Full Beetle sequence for main RAM is inlined;
* MMIO / lockstep / data-shard fall through to *_slow in memory.c. */
static inline uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask) {
#ifdef PSX_ENABLE_BLOCK_CYCLES
uint32_t phys = addr & 0x1FFFFFFFu;
if (g_ls_mode == 0 && !g_ds_recording && phys < 0x00800000u) {
if (g_psx_load_delay < 0) (void)psx_load_delay_enabled();
if (g_psx_load_delay) {
psx_cyc_base(cpu);
psx_cyc_deps(cpu, reg_mask);
if (cpu->ld_which_t == rt) cpu->ld_which_t = 0u;
psx_cyc_lds(cpu);
cpu->read_absorb[cpu->read_absorb_which] = 0u;
cpu->read_absorb_which = 0u;
uint32_t fudge = (uint32_t)((cpu->read_fudge >> 4) & 2u);
cpu->ld_absorb = 5u; /* main-RAM wait 3 + completion 2 */
psx_cyc_charge(fudge + 5u);
cpu->ld_which_t = (uint8_t)rt;
}
uint32_t value;
memcpy(&value, g_psx_ram + (phys & g_psx_ram_mask), sizeof(value));
return value;
}
return psx_cyc_load_word_slow(cpu, addr, rt, reg_mask);
#else
(void)cpu; (void)rt; (void)reg_mask;
extern uint32_t psx_read_word(uint32_t a);
return psx_read_word(addr);
#endif
}
static inline uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask) {
#ifdef PSX_ENABLE_BLOCK_CYCLES
uint32_t phys = addr & 0x1FFFFFFFu;
if (g_ls_mode == 0 && !g_ds_recording && phys < 0x00800000u) {
if (g_psx_load_delay < 0) (void)psx_load_delay_enabled();
if (g_psx_load_delay) {
psx_cyc_base(cpu);
psx_cyc_deps(cpu, reg_mask);
if (cpu->ld_which_t == rt) cpu->ld_which_t = 0u;
psx_cyc_lds(cpu);
cpu->read_absorb[cpu->read_absorb_which] = 0u;
cpu->read_absorb_which = 0u;
uint32_t fudge = (uint32_t)((cpu->read_fudge >> 4) & 2u);
cpu->ld_absorb = 5u;
psx_cyc_charge(fudge + 5u);
cpu->ld_which_t = (uint8_t)rt;
}
uint16_t value;
memcpy(&value, g_psx_ram + (phys & g_psx_ram_mask), sizeof(value));
return value;
}
return psx_cyc_load_half_slow(cpu, addr, rt, reg_mask);
#else
(void)cpu; (void)rt; (void)reg_mask;
extern uint16_t psx_read_half(uint32_t a);
return psx_read_half(addr);
#endif
}
#endif
extern uint8_t psx_cyc_load_byte(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
/* Charge the exact timing/pipeline effects of a 32-bit CPU load without
* invoking the memory handler. Enhancement HLE may use this only when static
* analysis proves the loaded value and read side effects are unobservable. */
extern void psx_cyc_load_word_timing_only(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask);
/* LWC2 (GTE load) ReadMemory timing only (completion +1, NO LDWhich arm — the dest
* is a GTE register). Call AFTER psx_cyc_step(cpu,0) (§1+DO_LDS) and psx_gte_stall.
* Returns the raw 32-bit value. */
extern uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr);
#ifdef __cplusplus
}
#endif
#endif /* PSX_CYC_H */