mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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>
This commit is contained in:
committed by
Matthew Stanley
parent
f461435d1c
commit
985d45ecf2
+43
-14
@@ -738,24 +738,53 @@ Title constants. They live in the game's `game.toml`, are never written to
|
||||
```toml
|
||||
[timing]
|
||||
guest_cycle_scale = 1 # 1..64; 1 = faithful (default)
|
||||
guest_cycle_scale_gated = false # scale only while the title's mod opens the gate
|
||||
# Optional declarative gate: the scale applies only while every predicate
|
||||
# holds, judged at each VBlank edge (one inline table or an array, max 8).
|
||||
guest_cycle_scale_gate = [
|
||||
{ addr = 0x800AC794, value = 0x180 }, # size = 4, mask = ~0
|
||||
# { addr = 0x80010003, size = 1, mask = 0x0F, value = 5 },
|
||||
]
|
||||
guest_cycle_scale_gated = false # also require the mod gate (custom logic)
|
||||
```
|
||||
|
||||
No CPU is emulated. Recompiled code charges the guest clock a fixed number of
|
||||
cycles for each MIPS instruction it runs (plus load, mul/div and GTE
|
||||
latencies), and VBlank, timers, CD, SPU and DMA are scheduled against that
|
||||
clock. `guest_cycle_scale = N` charges each instruction 1/N of its cost; the
|
||||
devices keep hardware time. Larger N means the game never runs out of frame
|
||||
time: N times as much game code fits between two VBlanks. A game that paces
|
||||
itself on VBlank keeps its speed; one that busy-waits on a frame counter
|
||||
spins longer in host time, which is why `guest_cycle_scale_gated` exists.
|
||||
With it set, the scale applies only while the title's trusted mod holds the
|
||||
gate open (`psx_guest_cycle_scale_gate_open(1)`), e.g. R4 opens it during
|
||||
races; menus and loading keep faithful timing.
|
||||
cycles for each MIPS instruction it runs, and VBlank, timers, CD, SPU and DMA
|
||||
are scheduled against that clock. `guest_cycle_scale = N` charges each
|
||||
instruction 1/N of its cost; the devices keep hardware time. Larger N means
|
||||
the game never runs out of frame time: N times as much game code fits
|
||||
between two VBlanks. A game that paces itself on VBlank keeps its speed; one
|
||||
that busy-waits on a frame counter spins longer in host time, which is what
|
||||
the gates are for.
|
||||
|
||||
A scale other than 1 is folded into the netplay content fingerprint, so peers
|
||||
that differ never match. `PSX_GUEST_CYCLE_SCALE=<n>` overrides the scale for
|
||||
testing only.
|
||||
Accounting is the same on every tier. Guest time has two entry points
|
||||
(`psx_cycles.h`): the **CPU charge** `psx_cpu_charge()` (instruction base
|
||||
cost, load fudge/region wait/completion, i-cache refills, BIOS HLE costs),
|
||||
which applies the scale, and the **device advance** `psx_advance_cycles()`
|
||||
(DMA, idle skips, mul/div and GTE deadline waits), which never does.
|
||||
Native generated code, overlay DLLs (their batched charge publishes through
|
||||
the `cpu_charge` callback into the same function), the dirty-RAM
|
||||
interpreter and the BIOS all charge CPU work through it; mul/div and GTE
|
||||
latencies are scaled where their deadline is set. At 1 the CPU charge is
|
||||
exactly the device advance and nothing else changes.
|
||||
|
||||
Gates (all present gates must be open; with none the scale always applies):
|
||||
|
||||
- `guest_cycle_scale_gate` — RAM predicates on aligned main-RAM words
|
||||
(`(word & mask) == value`, `size` 1/2/4), judged at every VBlank edge, so
|
||||
every netplay peer and every rollback re-simulation sees the same verdict.
|
||||
Shut until the first VBlank that sees them hold. Needs no mod.
|
||||
- `guest_cycle_scale_gated = true` — a trusted plugin opens the gate with
|
||||
`psx_mod_set_guest_cycle_scale_gate(1)` (`mod_plugins.h`) for logic a
|
||||
predicate cannot express, or to tie the scale to a mod that must be active
|
||||
for it (every mod session reset shuts the gate; mod plans are cleared
|
||||
online, so a mod-gated scale stays out of netplay). With only the
|
||||
declarative gate the scale applies online too.
|
||||
|
||||
The carried fraction of the scaled clock and both gate states are saved in
|
||||
savestates and the netplay rollback snapshot (`boot_state` section
|
||||
`BS_SEC_GCS`, written only when the scale is not 1). A scale other than 1 is
|
||||
folded into the netplay content fingerprint, so peers that differ never
|
||||
match. `PSX_GUEST_CYCLE_SCALE=<n>` overrides the scale for testing only.
|
||||
|
||||
## Audit block
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ extern uint32_t g_debug_last_store_pc;
|
||||
* Compile generated code with -DPSX_ENABLE_BLOCK_CYCLES=1 to
|
||||
* activate cycle advancement at every block leader. */
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
extern void psx_advance_cycles(uint32_t cycles);
|
||||
extern void psx_cpu_charge(uint32_t cycles);
|
||||
#endif
|
||||
|
||||
void OpenBIOS_func_1FC00000(CPUState* cpu);
|
||||
|
||||
@@ -103,6 +103,10 @@ enum {
|
||||
BS_SEC_SIO = 0x0D, /* SIO regs + pad-config FSM + memcard FSM */
|
||||
BS_SEC_DIRTY = 0x0E, /* dirty-RAM page bitmap (guest-written code pages) */
|
||||
BS_SEC_MDEC = 0x0F, /* MDEC command/FIFOs/quant/scale (FMV decode resume) */
|
||||
BS_SEC_GCS = 0x13, /* [timing] guest_cycle_scale: carried fraction + gate
|
||||
state (3 u32). Written only when the title's
|
||||
scale is not 1, so faithful states are unchanged;
|
||||
absent => fraction 0, gates as they are. */
|
||||
BS_SEC_ICACHE = 0x10, /* R3000A I-cache tag/valid words (1024 u32) — fetch
|
||||
cost model. Host-persistent otherwise: a warm load
|
||||
without it replays with the pre-load timeline's
|
||||
|
||||
@@ -230,11 +230,14 @@ typedef struct OverlayCallbacks {
|
||||
/* Interrupt check with the guest PC that should resume if a game-installed
|
||||
* handler later RFEs to the sentinel outside the synchronous host window. */
|
||||
void (*check_interrupts_at)(CPUState *cpu, uint32_t resume_pc);
|
||||
/* Guest-cycle accounting (ABI v7): block-cycle charge. Overlay code built
|
||||
/* Guest-cycle accounting (ABI v7): the CPU charge. Overlay code built
|
||||
* with PSX_ENABLE_BLOCK_CYCLES must charge the SAME shared host cycle/timer
|
||||
* timeline as the dirty-RAM interpreter and the BIOS, or timer-sensitive
|
||||
* code reads different values per backend (Tomba2 logo Timer1 fork). */
|
||||
void (*advance_cycles)(uint32_t cycles);
|
||||
* code reads different values per backend (Tomba2 logo Timer1 fork). The
|
||||
* host binds its psx_cpu_charge (psx_cycles.h), so [timing]
|
||||
* guest_cycle_scale applies to overlay code exactly as to native code.
|
||||
* (Named advance_cycles before; same slot, same arguments.) */
|
||||
void (*cpu_charge)(uint32_t cycles);
|
||||
/* GTE coprocessor 2 execution */
|
||||
void (*gte_execute)(CPUState *cpu, uint32_t cmd);
|
||||
/* MIPS syscall (break/syscall instructions). Returns 1 if control
|
||||
|
||||
@@ -8,7 +8,7 @@ static uint32_t s_pending_cycles;
|
||||
PSX_OVERLAY_EXPORT void overlay_flush_cycles(void) {
|
||||
uint32_t cycles = s_pending_cycles;
|
||||
s_pending_cycles = 0;
|
||||
if (cycles && g_cbs.advance_cycles) g_cbs.advance_cycles(cycles);
|
||||
if (cycles && g_cbs.cpu_charge) g_cbs.cpu_charge(cycles);
|
||||
}
|
||||
|
||||
/* Call-contract state (ABI v2): generated code reads/bumps the runtime's
|
||||
@@ -53,10 +53,16 @@ void psx_check_interrupts_at(CPUState *cpu, uint32_t resume_pc) {
|
||||
if (g_cbs.check_interrupts_at) g_cbs.check_interrupts_at(cpu, resume_pc);
|
||||
else g_cbs.check_interrupts(cpu);
|
||||
}
|
||||
void psx_advance_cycles(uint32_t cycles) {
|
||||
/* The CPU charge (psx_cycles.h): overlay code is guest CPU work, so its
|
||||
* charges accumulate here raw and publish through cpu_charge, the host's
|
||||
* psx_cpu_charge, which applies [timing] guest_cycle_scale exactly as it does
|
||||
* for native code. psx_advance_cycles is kept as the same accumulator for
|
||||
* code generated before the emitter named the CPU charge. */
|
||||
void psx_cpu_charge(uint32_t cycles) {
|
||||
if (cycles > UINT32_MAX - s_pending_cycles) overlay_flush_cycles();
|
||||
s_pending_cycles += cycles;
|
||||
}
|
||||
void psx_advance_cycles(uint32_t cycles) { psx_cpu_charge(cycles); }
|
||||
void gte_execute(CPUState *cpu, uint32_t cmd) {
|
||||
overlay_flush_cycles();
|
||||
g_cbs.gte_execute(cpu, cmd);
|
||||
|
||||
@@ -92,15 +92,16 @@ static inline void psx_cyc_local_cleanup(uint32_t **guard) {
|
||||
|
||||
static inline void psx_cyc_charge(uint32_t cycles) {
|
||||
if (cycles == 0u) return;
|
||||
#if !defined(PSX_OVERLAY_DLL_BUILD)
|
||||
if (PSX_GCS_ACTIVE()) {
|
||||
cycles = psx_gcs_scale(cycles);
|
||||
if (cycles == 0u) return;
|
||||
}
|
||||
#endif
|
||||
#if defined(PSX_OVERLAY_DLL_BUILD)
|
||||
psx_advance_cycles(cycles);
|
||||
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()) {
|
||||
psx_cpu_charge(cycles);
|
||||
return;
|
||||
}
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
if (__builtin_expect(g_ls_replay_active | g_event_step_conservative, 0)) {
|
||||
#else
|
||||
|
||||
@@ -70,22 +70,53 @@ extern uint32_t *g_psx_cyc_local_acc;
|
||||
* 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 (instruction, load, mul/div and
|
||||
* GTE latencies, overlay DLL batches); 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. */
|
||||
* 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);
|
||||
/* guest_cycle_scale_gated: the scale applies only while the title's mod
|
||||
* holds the gate open (psx_guest_cycle_scale_gate_open), e.g. only in a
|
||||
* paced race loop where it pays off; menus and loading keep faithful timing
|
||||
* (a scaled clock spends host time spinning in their poll loops). */
|
||||
/* 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;
|
||||
@@ -101,6 +132,12 @@ static inline uint32_t psx_gcs_scale(uint32_t cpu_cycles) {
|
||||
#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. */
|
||||
@@ -159,6 +196,23 @@ static inline void psx_advance_cycles(uint32_t cycles) {
|
||||
}
|
||||
#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)
|
||||
|
||||
@@ -165,7 +165,9 @@ std::string CodeGenerator::emit_mid_block_cycle_charge(uint32_t addr,
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
|
||||
ss << indent << fmt::format("psx_advance_cycles({}u);\n", cycles);
|
||||
// The CPU charge (psx_cycles.h): the same call in native and overlay
|
||||
// DLL code; [timing] guest_cycle_scale is applied inside it.
|
||||
ss << indent << fmt::format("psx_cpu_charge({}u);\n", cycles);
|
||||
ss << "#endif\n";
|
||||
return ss.str();
|
||||
}
|
||||
@@ -2111,7 +2113,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
};
|
||||
if (!cycle_per_insn && block_exec_cycles > 0) {
|
||||
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
|
||||
ss << config_.indent << fmt::format("psx_advance_cycles({}u);\n",
|
||||
ss << config_.indent << fmt::format("psx_cpu_charge({}u);\n",
|
||||
block_exec_cycles);
|
||||
ss << "#endif\n";
|
||||
}
|
||||
|
||||
@@ -637,7 +637,8 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
// it, so N times as much game code fits between two VBlanks while
|
||||
// VBlank, timers, CD, SPU and DMA keep hardware time. 1 = faithful;
|
||||
// clamped to 1..64. guest_cycle_scale_gated: the scale applies only
|
||||
// while the title's mod opens the gate (psx_guest_cycle_scale_gate_open).
|
||||
// while the mod gate is open (psx_mod_set_guest_cycle_scale_gate); see
|
||||
// guest_cycle_scale_gate below for the declarative RAM gate.
|
||||
if (cfg.contains("timing")) {
|
||||
const toml::value& tm = toml::find(cfg, "timing");
|
||||
if (tm.contains("guest_cycle_scale")) {
|
||||
@@ -648,6 +649,30 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
}
|
||||
if (tm.contains("guest_cycle_scale_gated"))
|
||||
rt.guest_cycle_scale_gated = toml::find<bool>(tm, "guest_cycle_scale_gated");
|
||||
// guest_cycle_scale_gate: one inline table or an array of them,
|
||||
// { addr, value, size = 4, mask = 0xFFFFFFFF }; all must hold.
|
||||
if (tm.contains("guest_cycle_scale_gate")) {
|
||||
const toml::value& g = toml::find(tm, "guest_cycle_scale_gate");
|
||||
std::vector<toml::value> preds;
|
||||
if (g.is_array()) preds = g.as_array();
|
||||
else preds.push_back(g);
|
||||
if (preds.size() > 8)
|
||||
throw std::runtime_error("[timing] guest_cycle_scale_gate: at most 8 predicates");
|
||||
for (const toml::value& pv : preds) {
|
||||
RuntimeConfig::GuestCycleScaleGatePred p;
|
||||
p.addr = (uint32_t)toml::find<long long>(pv, "addr");
|
||||
p.value = (uint32_t)toml::find<long long>(pv, "value");
|
||||
if (pv.contains("size")) p.size = (uint32_t)toml::find<long long>(pv, "size");
|
||||
if (pv.contains("mask")) p.mask = (uint32_t)toml::find<long long>(pv, "mask");
|
||||
const uint32_t phys = p.addr & 0x1FFFFFFFu;
|
||||
if ((p.size != 1 && p.size != 2 && p.size != 4) ||
|
||||
(phys & (p.size - 1u)) != 0 || phys >= 0x00800000u)
|
||||
throw std::runtime_error(
|
||||
"[timing] guest_cycle_scale_gate: addr must be an aligned "
|
||||
"main-RAM address and size 1, 2 or 4");
|
||||
rt.guest_cycle_scale_gate.push_back(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional [video] block — visual enhancement options. Kept on the same
|
||||
|
||||
@@ -566,6 +566,15 @@ struct RuntimeConfig {
|
||||
// constants from game.toml only. See config_loader.cpp.
|
||||
int guest_cycle_scale = 1;
|
||||
bool guest_cycle_scale_gated = false;
|
||||
// guest_cycle_scale_gate: declarative RAM gate, judged at every VBlank;
|
||||
// the scale applies only while every predicate holds.
|
||||
struct GuestCycleScaleGatePred {
|
||||
uint32_t addr = 0; // guest main-RAM address (any segment)
|
||||
uint32_t size = 4; // 1, 2 or 4 bytes, aligned
|
||||
uint32_t mask = 0xFFFFFFFFu;
|
||||
uint32_t value = 0; // open while (word & mask) == value
|
||||
};
|
||||
std::vector<GuestCycleScaleGatePred> guest_cycle_scale_gate;
|
||||
|
||||
// low_latency_input: re-sample the pad after the wall-clock pacer (just
|
||||
// before present) so the next CPU frame reads near-fresh input instead of
|
||||
|
||||
@@ -954,7 +954,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
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 += fmt::format(" psx_cpu_charge({}u);\n", bcyc);
|
||||
out += "#endif\n";
|
||||
}
|
||||
}
|
||||
@@ -2644,7 +2644,7 @@ EmitStats FullFunctionEmitter::emit(
|
||||
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 += "extern void psx_cpu_charge(uint32_t cycles);\n";
|
||||
full_c += "#endif\n\n";
|
||||
|
||||
// Forward declare ALL functions so intra-file calls resolve.
|
||||
|
||||
@@ -128,6 +128,7 @@ ICACHE_HARNESS = r"""
|
||||
int g_ls_replay_active = 0;
|
||||
static unsigned long long cycles;
|
||||
void psx_advance_cycles(uint32_t c) { cycles += c; }
|
||||
void psx_cpu_charge(uint32_t c) { cycles += c; }
|
||||
int main(void) {
|
||||
CPUState cpu; char line[64];
|
||||
memset(&cpu, 0, sizeof cpu);
|
||||
|
||||
@@ -105,6 +105,7 @@ HARNESS = r"""
|
||||
int g_ls_replay_active = 0;
|
||||
static unsigned long long cycles;
|
||||
void psx_advance_cycles(uint32_t c) { cycles += c; }
|
||||
void psx_cpu_charge(uint32_t c) { cycles += c; }
|
||||
int main(void) {
|
||||
static CPUState cpu;
|
||||
char line[64];
|
||||
|
||||
@@ -372,6 +372,47 @@ static void test_pgxp_title_keys() {
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
/* [timing] guest_cycle_scale and its declarative RAM gate (title constants). */
|
||||
static void test_timing_gate() {
|
||||
fs::path p = write_game_toml("ves_timing_none.toml", "");
|
||||
auto gc = PSXRecompV4::load_game_config(p);
|
||||
check(gc.runtime.guest_cycle_scale == 1, "timing: default scale 1");
|
||||
check(gc.runtime.guest_cycle_scale_gate.empty(), "timing: no gate by default");
|
||||
check(!gc.runtime.guest_cycle_scale_gated, "timing: mod gate off by default");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_game_toml("ves_timing_one.toml",
|
||||
"[timing]\nguest_cycle_scale = 64\n"
|
||||
"guest_cycle_scale_gate = { addr = 0x800AC794, value = 0x180 }\n");
|
||||
gc = PSXRecompV4::load_game_config(p);
|
||||
check(gc.runtime.guest_cycle_scale == 64, "timing: scale 64");
|
||||
check(gc.runtime.guest_cycle_scale_gate.size() == 1 &&
|
||||
gc.runtime.guest_cycle_scale_gate[0].addr == 0x800AC794u &&
|
||||
gc.runtime.guest_cycle_scale_gate[0].value == 0x180u &&
|
||||
gc.runtime.guest_cycle_scale_gate[0].size == 4u &&
|
||||
gc.runtime.guest_cycle_scale_gate[0].mask == 0xFFFFFFFFu,
|
||||
"timing: single inline-table gate with defaults");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_game_toml("ves_timing_arr.toml",
|
||||
"[timing]\nguest_cycle_scale = 8\nguest_cycle_scale_gated = true\n"
|
||||
"guest_cycle_scale_gate = [ { addr = 0x800AC794, value = 0x180 },\n"
|
||||
" { addr = 0x00010003, size = 1, mask = 0x0F, value = 5 } ]\n");
|
||||
gc = PSXRecompV4::load_game_config(p);
|
||||
check(gc.runtime.guest_cycle_scale_gate.size() == 2 &&
|
||||
gc.runtime.guest_cycle_scale_gate[1].size == 1u &&
|
||||
gc.runtime.guest_cycle_scale_gate[1].mask == 0x0Fu &&
|
||||
gc.runtime.guest_cycle_scale_gated, "timing: gate array + mod gate");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_game_toml("ves_timing_bad.toml",
|
||||
"[timing]\nguest_cycle_scale_gate = { addr = 0x1F801070, value = 1 }\n");
|
||||
bool rejected = false;
|
||||
try { (void)PSXRecompV4::load_game_config(p); } catch (const std::exception&) { rejected = true; }
|
||||
check(rejected, "timing: non-RAM gate address rejected");
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_internal_resolution_game_toml();
|
||||
test_internal_resolution_settings();
|
||||
@@ -385,6 +426,7 @@ int main() {
|
||||
test_user_settings_fov_scale();
|
||||
test_texture_window_batching();
|
||||
test_pgxp_title_keys();
|
||||
test_timing_gate();
|
||||
|
||||
if (failures) {
|
||||
std::fprintf(stderr, "video_enhancement_settings_test: %d failure(s)\n",
|
||||
|
||||
@@ -276,6 +276,16 @@ if(BUILD_TESTING)
|
||||
PSX_ENABLE_BLOCK_CYCLES=1)
|
||||
add_test(NAME psx_guest_cycle_scale_test COMMAND psx_guest_cycle_scale_test)
|
||||
|
||||
# One CPU charge for every tier, the declarative RAM gate and the mod gate,
|
||||
# and rollback re-simulation, against the real psx_cycles.c.
|
||||
add_executable(psx_guest_cycle_scale_gate_test
|
||||
tests/test_psx_guest_cycle_scale_gate.c
|
||||
src/psx_cycles.c)
|
||||
target_include_directories(psx_guest_cycle_scale_gate_test PRIVATE include)
|
||||
target_compile_definitions(psx_guest_cycle_scale_gate_test PRIVATE
|
||||
PSX_ENABLE_BLOCK_CYCLES=1)
|
||||
add_test(NAME psx_guest_cycle_scale_gate_test COMMAND psx_guest_cycle_scale_gate_test)
|
||||
|
||||
# Segment misses in static game code (docs/SEGMENT_AWARE_CODE.md §5.5):
|
||||
# classification against the dispatch rows, the ring and the per-PC
|
||||
# summary the TCP command and the exit report read.
|
||||
|
||||
@@ -103,6 +103,10 @@ enum {
|
||||
BS_SEC_SIO = 0x0D, /* SIO regs + pad-config FSM + memcard FSM */
|
||||
BS_SEC_DIRTY = 0x0E, /* dirty-RAM page bitmap (guest-written code pages) */
|
||||
BS_SEC_MDEC = 0x0F, /* MDEC command/FIFOs/quant/scale (FMV decode resume) */
|
||||
BS_SEC_GCS = 0x13, /* [timing] guest_cycle_scale: carried fraction + gate
|
||||
state (3 u32). Written only when the title's
|
||||
scale is not 1, so faithful states are unchanged;
|
||||
absent => fraction 0, gates as they are. */
|
||||
BS_SEC_ICACHE = 0x10, /* R3000A I-cache tag/valid words (1024 u32) — fetch
|
||||
cost model. Host-persistent otherwise: a warm load
|
||||
without it replays with the pre-load timeline's
|
||||
|
||||
@@ -84,6 +84,16 @@ void psx_mod_set_texture_filter(int mode);
|
||||
* Save/load and rewind must wait until that host context has returned. */
|
||||
int psx_mod_function_entry_active(void);
|
||||
|
||||
/* [timing] guest_cycle_scale mod gate (docs/config_schema.md, Timing block).
|
||||
* With guest_cycle_scale_gated = true in game.toml the scale applies only
|
||||
* while a trusted plugin holds this gate open (and any declarative
|
||||
* guest_cycle_scale_gate RAM predicates hold). Every mod session reset
|
||||
* shuts it; open it from the activation callback. Mod plans are cleared
|
||||
* online, so this gate keeps a scale that depends on a mod out of netplay.
|
||||
* The gate state is part of savestates and the netplay rollback snapshot. */
|
||||
void psx_mod_set_guest_cycle_scale_gate(int open);
|
||||
/* Live scale: the configured one while every gate is open, else 1. */
|
||||
uint32_t psx_mod_guest_cycle_scale(void);
|
||||
/* Narrow guest services available to trusted plugin callbacks. */
|
||||
int psx_mod_game_started(void);
|
||||
/* Read an original mounted-disc file without changing guest CD state/timing.
|
||||
|
||||
@@ -230,11 +230,14 @@ typedef struct OverlayCallbacks {
|
||||
/* Interrupt check with the guest PC that should resume if a game-installed
|
||||
* handler later RFEs to the sentinel outside the synchronous host window. */
|
||||
void (*check_interrupts_at)(CPUState *cpu, uint32_t resume_pc);
|
||||
/* Guest-cycle accounting (ABI v7): block-cycle charge. Overlay code built
|
||||
/* Guest-cycle accounting (ABI v7): the CPU charge. Overlay code built
|
||||
* with PSX_ENABLE_BLOCK_CYCLES must charge the SAME shared host cycle/timer
|
||||
* timeline as the dirty-RAM interpreter and the BIOS, or timer-sensitive
|
||||
* code reads different values per backend (Tomba2 logo Timer1 fork). */
|
||||
void (*advance_cycles)(uint32_t cycles);
|
||||
* code reads different values per backend (Tomba2 logo Timer1 fork). The
|
||||
* host binds its psx_cpu_charge (psx_cycles.h), so [timing]
|
||||
* guest_cycle_scale applies to overlay code exactly as to native code.
|
||||
* (Named advance_cycles before; same slot, same arguments.) */
|
||||
void (*cpu_charge)(uint32_t cycles);
|
||||
/* GTE coprocessor 2 execution */
|
||||
void (*gte_execute)(CPUState *cpu, uint32_t cmd);
|
||||
/* MIPS syscall (break/syscall instructions). Returns 1 if control
|
||||
|
||||
@@ -8,7 +8,7 @@ static uint32_t s_pending_cycles;
|
||||
PSX_OVERLAY_EXPORT void overlay_flush_cycles(void) {
|
||||
uint32_t cycles = s_pending_cycles;
|
||||
s_pending_cycles = 0;
|
||||
if (cycles && g_cbs.advance_cycles) g_cbs.advance_cycles(cycles);
|
||||
if (cycles && g_cbs.cpu_charge) g_cbs.cpu_charge(cycles);
|
||||
}
|
||||
|
||||
/* Call-contract state (ABI v2): generated code reads/bumps the runtime's
|
||||
@@ -53,10 +53,16 @@ void psx_check_interrupts_at(CPUState *cpu, uint32_t resume_pc) {
|
||||
if (g_cbs.check_interrupts_at) g_cbs.check_interrupts_at(cpu, resume_pc);
|
||||
else g_cbs.check_interrupts(cpu);
|
||||
}
|
||||
void psx_advance_cycles(uint32_t cycles) {
|
||||
/* The CPU charge (psx_cycles.h): overlay code is guest CPU work, so its
|
||||
* charges accumulate here raw and publish through cpu_charge, the host's
|
||||
* psx_cpu_charge, which applies [timing] guest_cycle_scale exactly as it does
|
||||
* for native code. psx_advance_cycles is kept as the same accumulator for
|
||||
* code generated before the emitter named the CPU charge. */
|
||||
void psx_cpu_charge(uint32_t cycles) {
|
||||
if (cycles > UINT32_MAX - s_pending_cycles) overlay_flush_cycles();
|
||||
s_pending_cycles += cycles;
|
||||
}
|
||||
void psx_advance_cycles(uint32_t cycles) { psx_cpu_charge(cycles); }
|
||||
void gte_execute(CPUState *cpu, uint32_t cmd) {
|
||||
overlay_flush_cycles();
|
||||
g_cbs.gte_execute(cpu, cmd);
|
||||
|
||||
@@ -92,15 +92,16 @@ static inline void psx_cyc_local_cleanup(uint32_t **guard) {
|
||||
|
||||
static inline void psx_cyc_charge(uint32_t cycles) {
|
||||
if (cycles == 0u) return;
|
||||
#if !defined(PSX_OVERLAY_DLL_BUILD)
|
||||
if (PSX_GCS_ACTIVE()) {
|
||||
cycles = psx_gcs_scale(cycles);
|
||||
if (cycles == 0u) return;
|
||||
}
|
||||
#endif
|
||||
#if defined(PSX_OVERLAY_DLL_BUILD)
|
||||
psx_advance_cycles(cycles);
|
||||
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()) {
|
||||
psx_cpu_charge(cycles);
|
||||
return;
|
||||
}
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
if (__builtin_expect(g_ls_replay_active | g_event_step_conservative, 0)) {
|
||||
#else
|
||||
|
||||
@@ -70,22 +70,53 @@ extern uint32_t *g_psx_cyc_local_acc;
|
||||
* 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 (instruction, load, mul/div and
|
||||
* GTE latencies, overlay DLL batches); 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. */
|
||||
* 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);
|
||||
/* guest_cycle_scale_gated: the scale applies only while the title's mod
|
||||
* holds the gate open (psx_guest_cycle_scale_gate_open), e.g. only in a
|
||||
* paced race loop where it pays off; menus and loading keep faithful timing
|
||||
* (a scaled clock spends host time spinning in their poll loops). */
|
||||
/* 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;
|
||||
@@ -101,6 +132,12 @@ static inline uint32_t psx_gcs_scale(uint32_t cpu_cycles) {
|
||||
#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. */
|
||||
@@ -159,6 +196,23 @@ static inline void psx_advance_cycles(uint32_t cycles) {
|
||||
}
|
||||
#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)
|
||||
|
||||
@@ -182,7 +182,7 @@ static void hle_open_event(CPUState* cpu, uint32_t base, uint32_t n)
|
||||
cpu->write_word(ev + EV_STATUS, EVST_DISABLED);
|
||||
cpu->gpr[2] = 0xF1000000u | slot;
|
||||
}
|
||||
psx_advance_cycles(40u + 6u * (slot == 0xFFFFFFFFu ? n : slot + 1u));
|
||||
psx_cpu_charge(40u + 6u * (slot == 0xFFFFFFFFu ? n : slot + 1u));
|
||||
}
|
||||
|
||||
/* CloseEvent (kernel 0x1E1C): status=free, return 1. */
|
||||
@@ -191,7 +191,7 @@ static void hle_close_event(CPUState* cpu, uint32_t base)
|
||||
uint32_t ev = base + (cpu->gpr[4] & 0xFFFFu) * EVCB_STRIDE;
|
||||
cpu->write_word(ev + EV_STATUS, EVST_FREE);
|
||||
cpu->gpr[2] = 1u;
|
||||
psx_advance_cycles(36u);
|
||||
psx_cpu_charge(36u);
|
||||
}
|
||||
|
||||
/* TestEvent (kernel 0x1EC8): ready => re-arm to enabled and return 1, else 0. */
|
||||
@@ -204,7 +204,7 @@ static void hle_test_event(CPUState* cpu, uint32_t base)
|
||||
} else {
|
||||
cpu->gpr[2] = 0u;
|
||||
}
|
||||
psx_advance_cycles(40u);
|
||||
psx_cpu_charge(40u);
|
||||
}
|
||||
|
||||
/* EnableEvent (kernel 0x1F10) / DisableEvent (kernel 0x1F4C): a non-free
|
||||
@@ -216,7 +216,7 @@ static void hle_set_event_status(CPUState* cpu, uint32_t base, uint32_t status)
|
||||
if (cpu->read_word(ev + EV_STATUS) != EVST_FREE)
|
||||
cpu->write_word(ev + EV_STATUS, status);
|
||||
cpu->gpr[2] = 1u;
|
||||
psx_advance_cycles(40u);
|
||||
psx_cpu_charge(40u);
|
||||
}
|
||||
|
||||
/* DeliverEvent (kernel 0x1B44): for every ENABLED EvCB matching class+spec,
|
||||
@@ -260,7 +260,7 @@ static void hle_deliver_event(CPUState* cpu, uint32_t base, uint32_t n)
|
||||
* caller may depend on it (libapi types DeliverEvent void). Set 0 for a
|
||||
* stable, observable value. */
|
||||
cpu->gpr[2] = 0u;
|
||||
psx_advance_cycles(36u + 12u * n);
|
||||
psx_cpu_charge(36u + 12u * n);
|
||||
}
|
||||
|
||||
/* B0 vector service. Returns 1 iff serviced in HLE. */
|
||||
|
||||
@@ -390,7 +390,8 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
|
||||
h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH;
|
||||
h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG;
|
||||
h.codegen_ver = (uint32_t)PSX_OVERLAY_CODEGEN_VER;
|
||||
h.section_count = 17 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u);
|
||||
h.section_count = 17 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u) +
|
||||
(psx_guest_cycle_scale_config() != 1u ? 1u : 0u);
|
||||
|
||||
ok = write_header_le(o, &h);
|
||||
|
||||
@@ -488,6 +489,17 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
|
||||
ok = pst_w_u32(&w, fntrace_is_game_started() ? 1u : 0u) &&
|
||||
write_section(o, BS_SEC_GAME_START, handoff, sizeof handoff);
|
||||
}
|
||||
if (ok && psx_guest_cycle_scale_config() != 1u) {
|
||||
/* Rollback re-simulation must resume with the same carried fraction
|
||||
* and gate verdicts, or scaled charges land a cycle apart. */
|
||||
uint32_t g[3];
|
||||
uint8_t gb[PSX_GCS_SNAPSHOT_BYTES];
|
||||
PstW w;
|
||||
psx_guest_cycle_scale_snapshot(g);
|
||||
pst_w_init(&w, gb, sizeof gb);
|
||||
ok = pst_w_u32(&w, g[0]) && pst_w_u32(&w, g[1]) && pst_w_u32(&w, g[2]) &&
|
||||
write_section(o, BS_SEC_GCS, gb, sizeof gb);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -624,6 +636,7 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
|
||||
pst_r_init(&r, p, len);
|
||||
if (!pst_r_u64(&r, &cyc)) return 0;
|
||||
psx_cycle_count = cyc;
|
||||
g_psx_gcs_frac = 0u; /* BS_SEC_GCS (if present) restores it */
|
||||
return 1;
|
||||
}
|
||||
case BS_SEC_GPU:
|
||||
@@ -703,6 +716,16 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
|
||||
case BS_SEC_GAME_START:
|
||||
fntrace_restore_game_started(p[0]);
|
||||
return 1;
|
||||
case BS_SEC_GCS: {
|
||||
PstR r;
|
||||
uint32_t g[3];
|
||||
if (len != PSX_GCS_SNAPSHOT_BYTES) return 0;
|
||||
pst_r_init(&r, p, len);
|
||||
if (!pst_r_u32(&r, &g[0]) || !pst_r_u32(&r, &g[1]) || !pst_r_u32(&r, &g[2]))
|
||||
return 0;
|
||||
psx_guest_cycle_scale_restore(g);
|
||||
return 1;
|
||||
}
|
||||
case BS_SEC_ICACHE: {
|
||||
PstR r;
|
||||
if (len != 1024u * 4u) return 0;
|
||||
@@ -762,6 +785,7 @@ static int validate_section(uint32_t tag, const uint8_t* p, uint32_t len) {
|
||||
case BS_SEC_GAME_START:
|
||||
return len == 4u && p[0] <= 1u && !p[1] && !p[2] && !p[3];
|
||||
case BS_SEC_ICACHE: return len == 1024u * 4u;
|
||||
case BS_SEC_GCS: return len == PSX_GCS_SNAPSHOT_BYTES;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +101,7 @@ static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask) {
|
||||
cpu->read_absorb[w]--;
|
||||
} else if (!g_ls_replay_active) {
|
||||
if (PSX_GCS_ACTIVE()) {
|
||||
uint32_t t = psx_gcs_scale(1u);
|
||||
if (t) psx_advance_cycles(t);
|
||||
psx_cpu_charge(1u); /* the CPU charge, scaled */
|
||||
psx_cyc_deps(cpu, reg_mask);
|
||||
psx_cyc_lds(cpu);
|
||||
return;
|
||||
|
||||
@@ -496,6 +496,9 @@ static void fire_vblank_edge(void) {
|
||||
g_vblank_raise_count++;
|
||||
event_ring_record(EV_ISTAT_RAISE, IRQ_VBLANK);
|
||||
gpu_vblank_tick(); /* Toggle LCF (GPUSTAT bit 31) */
|
||||
/* [timing] guest_cycle_scale_gate: RAM predicates are judged here, at a
|
||||
* guest-time point (no-op without one). */
|
||||
psx_guest_cycle_scale_vblank();
|
||||
#ifndef PSX_ENABLE_BLOCK_CYCLES
|
||||
timers_tick(33868); /* ~1 NTSC frame worth of cycles */
|
||||
cdrom_tick(); /* Process pending CDROM responses */
|
||||
|
||||
+24
-2
@@ -1633,6 +1633,10 @@ static void reset_mod_owned_presentation(void) {
|
||||
psx_mod_set_retained_scene_predicate(nullptr);
|
||||
psx_mod_set_adaptive_backdrop_preload(0);
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
/* [timing] guest_cycle_scale mod gate: shut until this session's
|
||||
* activation opens it (an online match clears the plan, so a mod-gated
|
||||
* scale never carries into netplay). */
|
||||
psx_mod_set_guest_cycle_scale_gate(0);
|
||||
g_bezel_path.clear();
|
||||
g_frame_interpolation_blend = g_frame_interpolation_blend_default;
|
||||
g_frame_interpolation_source = PSX_MOD_FRAME_SOURCE_VBLANK;
|
||||
@@ -14224,6 +14228,19 @@ namespace {
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
/* [timing] guest_cycle_scale_gate reader: main RAM only, side-effect free
|
||||
* (aligned little-endian word of the given size). */
|
||||
extern "C" uint8_t* memory_get_ram_ptr(void);
|
||||
static uint32_t gcs_gate_read_ram(uint32_t phys, uint32_t size) {
|
||||
const uint8_t* ram = (const uint8_t*)memory_get_ram_ptr();
|
||||
const uint32_t bytes = (uint32_t)memory_get_ram_bytes();
|
||||
if (!ram || !bytes) return 0;
|
||||
const uint32_t a = phys & (bytes - 1u);
|
||||
uint32_t v = 0;
|
||||
for (uint32_t i = 0; i < size; ++i) v |= (uint32_t)ram[a + i] << (8u * i);
|
||||
return v;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
/* Force line-buffered output so messages appear even if killed. */
|
||||
std::setvbuf(stdout, nullptr, _IOLBF, BUFSIZ);
|
||||
@@ -14700,16 +14717,21 @@ int main(int argc, char** argv) {
|
||||
* (no player setting). PSX_GUEST_CYCLE_SCALE overrides it for
|
||||
* testing only. */
|
||||
psx_guest_cycle_scale_set_gated(gc.runtime.guest_cycle_scale_gated ? 1 : 0);
|
||||
psx_guest_cycle_scale_ram_gate_clear();
|
||||
psx_guest_cycle_scale_set_ram_reader(gcs_gate_read_ram);
|
||||
for (const auto& gp : gc.runtime.guest_cycle_scale_gate)
|
||||
(void)psx_guest_cycle_scale_ram_gate_add(gp.addr, gp.size, gp.mask, gp.value);
|
||||
psx_guest_cycle_scale_set((uint32_t)gc.runtime.guest_cycle_scale);
|
||||
if (const char* gcs = getenv("PSX_GUEST_CYCLE_SCALE")) {
|
||||
const int v = atoi(gcs);
|
||||
if (v > 0) psx_guest_cycle_scale_set((uint32_t)v);
|
||||
}
|
||||
if (psx_guest_cycle_scale_config() != 1u)
|
||||
fprintf(stderr, "[timing] guest_cycle_scale %u%s (instructions charge 1/%u "
|
||||
fprintf(stderr, "[timing] guest_cycle_scale %u%s%s (instructions charge 1/%u "
|
||||
"of their guest cycles; VBlank/timers/CD/SPU/DMA unchanged)\n",
|
||||
psx_guest_cycle_scale_config(),
|
||||
gc.runtime.guest_cycle_scale_gated ? ", gated by the title's mod" : "",
|
||||
gc.runtime.guest_cycle_scale_gate.empty() ? "" : ", RAM gate",
|
||||
gc.runtime.guest_cycle_scale_gated ? ", mod gate" : "",
|
||||
psx_guest_cycle_scale_config());
|
||||
g_video_vsync = gc.runtime.video_vsync;
|
||||
g_frame_interpolation = gc.runtime.video_frame_interpolation ? 1 : 0;
|
||||
|
||||
@@ -2308,8 +2308,7 @@ extern int g_ls_replay_active;
|
||||
static inline void psx_load_charge_cycles(uint32_t cycles) {
|
||||
if (g_ls_replay_active || cycles == 0u) return;
|
||||
if (PSX_GCS_ACTIVE()) {
|
||||
cycles = psx_gcs_scale(cycles);
|
||||
if (cycles) psx_advance_cycles(cycles);
|
||||
psx_cpu_charge(cycles);
|
||||
return;
|
||||
}
|
||||
uint64_t next = psx_cycle_count + (uint64_t)cycles;
|
||||
@@ -2322,8 +2321,7 @@ static inline void psx_load_charge_cycles(uint32_t cycles) {
|
||||
}
|
||||
#else
|
||||
static inline void psx_load_charge_cycles(uint32_t cycles) {
|
||||
if (PSX_GCS_ACTIVE()) cycles = psx_gcs_scale(cycles);
|
||||
psx_advance_cycles(cycles);
|
||||
psx_cpu_charge(cycles);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2383,7 +2381,7 @@ static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size,
|
||||
uint32_t cost = region + compl_cost; /* LDAbsorb = region + completion */
|
||||
uint32_t fudge = (uint32_t)((cpu->read_fudge >> 4) & 2u);
|
||||
cpu->ld_absorb = cost;
|
||||
psx_advance_cycles(fudge + cost);
|
||||
psx_cpu_charge(fudge + cost); /* CPU charge: [timing] guest_cycle_scale */
|
||||
cpu->ld_which_t = (uint8_t)arm_rt;
|
||||
/* PROOF GATE (PSX_POLL_PROOF=N, default 0/off): a FLAT, non-absorbed extra N
|
||||
* cycles per main-RAM data read — replicates the historical "+6 cyc/main-RAM
|
||||
|
||||
@@ -9,12 +9,10 @@
|
||||
#include "debug_server.h"
|
||||
#include "psx_cycles.h"
|
||||
|
||||
/* Overlay DLLs batch their instruction charges and publish them through this
|
||||
* callback; scale them like psx_cyc_charge for [timing] guest_cycle_scale. */
|
||||
static void overlay_advance_cpu_cycles(uint32_t cycles) {
|
||||
if (PSX_GCS_ACTIVE()) cycles = psx_gcs_scale(cycles);
|
||||
psx_advance_cycles(cycles);
|
||||
}
|
||||
/* Out-of-line copy of the header-inline CPU charge for the overlay callback
|
||||
* table: overlay DLLs publish their accumulated CPU charge here, into the
|
||||
* same function native code charges (no scaling at this boundary). */
|
||||
static void overlay_cpu_charge(uint32_t cycles) { psx_cpu_charge(cycles); }
|
||||
#include "lockstep.h"
|
||||
#include "overlay_posix.h"
|
||||
#include "psx_memory.h"
|
||||
@@ -2500,7 +2498,7 @@ static void init_callbacks(void) {
|
||||
s_callbacks.check_interrupts = overlay_ci_wrapper;
|
||||
s_callbacks.check_interrupts_at = overlay_ci_at_wrapper;
|
||||
/* Address of the header inline → out-of-line copy in this TU (host side). */
|
||||
s_callbacks.advance_cycles = overlay_advance_cpu_cycles;
|
||||
s_callbacks.cpu_charge = overlay_cpu_charge;
|
||||
s_callbacks.gte_execute = gte_execute;
|
||||
s_callbacks.psx_syscall = psx_syscall;
|
||||
s_callbacks.psx_native_bad_entry = psx_native_bad_entry;
|
||||
|
||||
@@ -26,11 +26,19 @@ uint32_t g_psx_gcs_recip_q16 = 65536u;
|
||||
uint32_t g_psx_gcs_frac = 0u;
|
||||
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 */
|
||||
static int s_psx_gcs_open = 0;
|
||||
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) {
|
||||
uint32_t n = (!s_psx_gcs_gated || s_psx_gcs_open) ? s_psx_gcs_cfg : 1u;
|
||||
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);
|
||||
@@ -54,6 +62,64 @@ void psx_guest_cycle_scale_gate_open(int open) {
|
||||
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. */
|
||||
void psx_mod_set_guest_cycle_scale_gate(int open) { 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;
|
||||
@@ -755,7 +821,7 @@ 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();
|
||||
if (PSX_GCS_ACTIVE()) latency = psx_gcs_scale(latency);
|
||||
latency = psx_cpu_cycles(latency); /* [timing] guest_cycle_scale */
|
||||
cpu->muldiv_ts_done = psx_cycle_count + (uint64_t)latency;
|
||||
}
|
||||
|
||||
@@ -857,7 +923,7 @@ void psx_gte_set(CPUState* cpu, uint32_t latency) {
|
||||
if (cpu->gte_ts_done > psx_cycle_count) {
|
||||
psx_advance_cycles((uint32_t)(cpu->gte_ts_done - psx_cycle_count));
|
||||
}
|
||||
if (PSX_GCS_ACTIVE()) latency = psx_gcs_scale(latency);
|
||||
latency = psx_cpu_cycles(latency); /* [timing] guest_cycle_scale */
|
||||
cpu->gte_ts_done = psx_cycle_count + (uint64_t)latency;
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ void psx_icache_fetch_miss(CPUState* cpu, uint32_t addr) {
|
||||
cpu->read_absorb_which = 0u;
|
||||
|
||||
if (psx_fetch_uncached(addr)) { /* KSEG1 / uncached (BIOS ROM): every fetch */
|
||||
psx_advance_cycles(4u);
|
||||
psx_cpu_charge(4u); /* CPU charge: [timing] guest_cycle_scale */
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ void psx_icache_fetch_miss(CPUState* cpu, uint32_t addr) {
|
||||
g_psx_icache_tv[bidx + i] &= ~0x2u;
|
||||
cost++;
|
||||
}
|
||||
psx_advance_cycles(cost);
|
||||
psx_cpu_charge(cost);
|
||||
#else
|
||||
(void)cpu;
|
||||
(void)addr;
|
||||
|
||||
@@ -81,9 +81,8 @@ void psx_unknown_dispatch(CPUState *cpu, uint32_t addr, uint32_t phys) {
|
||||
}
|
||||
|
||||
void psx_advance_cycles(uint32_t cycles) { (void)cycles; }
|
||||
/* [timing] guest_cycle_scale state the loader scales overlay charges with: 1. */
|
||||
uint32_t g_psx_gcs_recip_q16 = 65536u;
|
||||
uint32_t g_psx_gcs_frac = 0u;
|
||||
/* The CPU charge the loader binds as the overlay cpu_charge callback. */
|
||||
void psx_cpu_charge(uint32_t cycles) { (void)cycles; }
|
||||
uint64_t psx_get_cycle_count(void) { return 0; }
|
||||
int psx_cycle_replay_begin(uint64_t cycle) { (void)cycle; return 1; }
|
||||
uint64_t psx_cycle_replay_end(void) { return 0; }
|
||||
|
||||
@@ -64,6 +64,12 @@ void gte_canonicalize_cpu_state(CPUState *cpu) { (void)cpu; }
|
||||
|
||||
/* ---- scheduler / interrupts ------------------------------------------------ */
|
||||
void psx_advance_cycles_slow(uint32_t c) { psx_cycle_count += c; }
|
||||
/* [timing] guest_cycle_scale at 1 (faithful) for the real memory.c/icache. */
|
||||
uint32_t g_psx_gcs_recip_q16 = 65536u;
|
||||
uint32_t g_psx_gcs_frac = 0u;
|
||||
uint32_t psx_guest_cycle_scale_config(void) { return 1u; }
|
||||
void psx_guest_cycle_scale_snapshot(uint32_t out[3]) { out[0] = out[1] = out[2] = 0u; }
|
||||
void psx_guest_cycle_scale_restore(const uint32_t in[3]) { (void)in; }
|
||||
void psx_devices_mmio_sync(void) {}
|
||||
void psx_devices_service_to_now(void) {}
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* [timing] guest_cycle_scale: instruction charges reach the guest clock at
|
||||
* 1/N of their cost; 1 leaves every charge exactly as it was. */
|
||||
#undef NDEBUG /* the checks below must run in Release too */
|
||||
#include "psx_cyc.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
/* [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;
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
int g_ls_replay_active = 0;
|
||||
static uint64_t cycles;
|
||||
void psx_advance_cycles(uint32_t count) { cycles += count; }
|
||||
/* I-cache refills are CPU work: they go through the CPU charge. */
|
||||
void psx_cpu_charge(uint32_t count) { psx_advance_cycles(count); }
|
||||
|
||||
static uint64_t run(int omit_body, uint32_t *giveback, uint32_t *tag) {
|
||||
CPUState cpu;
|
||||
|
||||
@@ -8,6 +8,8 @@ int g_ls_replay_active = 0;
|
||||
static uint64_t test_cycles = 0;
|
||||
|
||||
void psx_advance_cycles(uint32_t cycles) { test_cycles += cycles; }
|
||||
/* I-cache refills are CPU work: they go through the CPU charge. */
|
||||
void psx_cpu_charge(uint32_t cycles) { psx_advance_cycles(cycles); }
|
||||
|
||||
static int expect(int condition, const char *message) {
|
||||
if (condition) return 1;
|
||||
|
||||
@@ -36,7 +36,7 @@ int main(void) {
|
||||
OverlayCallbacks callbacks = {0};
|
||||
callbacks.ws_screen_x_bound = bound;
|
||||
callbacks.mod_function_entry = entry;
|
||||
callbacks.advance_cycles = advance;
|
||||
callbacks.cpu_charge = advance;
|
||||
overlay_init(&callbacks);
|
||||
assert(overlay_abi() == PSX_OVERLAY_ABI_TAG);
|
||||
assert(psx_ws_screen_x_bound(-256) == -512);
|
||||
|
||||
Reference in New Issue
Block a user