Merge pull request #577 from RetroPortingToolKit/integrate/pr561-guest-cycle-scale-20261008

Land approved guest cycle scaling from #561
This commit is contained in:
Matt Stanley
2026-10-07 22:16:42 -07:00
committed by GitHub
40 changed files with 994 additions and 31 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ next guest PC, even if common images never cross that boundary.
Use a deterministic workload with fixed inputs and a fixed capture or replay
when possible. Keep renderer, audio backend, window state, display refresh,
vsync/pacer ownership, CPU overclock, CRTC multiplier, mods, RAM size, and
vsync/pacer ownership, guest cycle scale, CRTC multiplier, mods, RAM size, and
instrumentation identical. Warm caches outside the timed region unless cold
behavior is the stated metric.
+57
View File
@@ -58,6 +58,7 @@ How the two configs relate:
[recompiler]
[draw_distance] # optional; opt-in far-geometry clamps (below)
[runtime]
[timing] # optional; title timing constants (below)
[audit]
```
@@ -724,6 +725,62 @@ Reserved future fields:
- `default_disc_path` — game runtimes can pre-mount a disc
- `default_game_root` — for sibling-junction setups
## Timing block (`[timing]`)
Title constants. They live in the game's `game.toml`, are never written to
`settings.toml`, and are not shown in the launcher.
```toml
[timing]
guest_cycle_scale = 1 # 1..64; 1 = faithful (default)
# 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, 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.
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
See `docs/internal/audit_inventory.md` for the audit pipeline. The schema here is
+1 -1
View File
@@ -1 +1 @@
69641913e358f4c3128a93dadf5824c7929afeaf8df6c5b9a6cc645c0f43d0f5
01cfc201a7c71e2555e5828c67aeaa29f295bf4ebfb4e182a3c606e2a4812857
+1 -1
View File
@@ -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);
+4
View File
@@ -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
+6 -3
View File
@@ -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);
+8 -1
View File
@@ -93,8 +93,15 @@ 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)
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
+90
View File
@@ -66,6 +66,79 @@ extern int g_psx_cyc_bb_defer;
* 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)
@@ -123,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)
+4 -2
View File
@@ -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();
}
@@ -2087,7 +2089,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";
}
+54
View File
@@ -613,6 +613,60 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
rt.parappa_timing_extra_late = parse_window("extra_late");
}
// Optional [timing] block — title constants, never player settings.
// guest_cycle_scale: no CPU is emulated; recompiled code charges the
// guest clock a fixed cost per instruction, and scale N charges 1/N of
// it, so N times as much game code fits between two VBlanks while
// VBlank, timers, CD, SPU and DMA keep hardware time. 1 = faithful;
// values outside 1..64 are rejected. guest_cycle_scale_gated: the scale applies only
// 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")) {
const long long n = toml::find<long long>(tm, "guest_cycle_scale");
if (n < 1 || n > 64)
throw std::runtime_error("[timing] guest_cycle_scale must be 1..64");
rt.guest_cycle_scale = static_cast<int>(n);
}
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");
// Range-check the signed TOML integer before narrowing, so an
// oversized or negative value cannot wrap into a valid one.
auto u32_field = [](const toml::value& pv, const char* key) -> uint32_t {
const long long v = toml::find<long long>(pv, key);
if (v < 0 || v > 0xFFFFFFFFLL)
throw std::runtime_error(
std::string("[timing] guest_cycle_scale_gate: ") + key +
" must be 0..0xFFFFFFFF");
return static_cast<uint32_t>(v);
};
for (const toml::value& pv : preds) {
RuntimeConfig::GuestCycleScaleGatePred p;
p.addr = u32_field(pv, "addr");
p.value = u32_field(pv, "value");
if (pv.contains("size")) p.size = u32_field(pv, "size");
if (pv.contains("mask")) p.mask = u32_field(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
// RuntimeConfig so main.cpp consumes them alongside the other knobs.
if (cfg.contains("video")) {
+14
View File
@@ -583,6 +583,20 @@ struct RuntimeConfig {
// PSX_FRAME_GEN=0/1 overrides.
bool video_frame_generation = false;
// [timing] guest_cycle_scale (1 = faithful, 1..64) and its gate; title
// 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;
// present_thread: with render_thread, composed frames go to offscreen
// slots and a present thread (second, shared GL context on the window)
// does the copy and the swap, so the window compositor's wait does not
+2 -2
View File
@@ -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];
@@ -460,6 +460,70 @@ 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);
/* Oversized and negative values must be rejected, not wrapped to 32 bits. */
const struct { const char* body; const char* what; } bad[] = {
{ "guest_cycle_scale_gate = { addr = 0x100010000, value = 1 }", "oversized addr" },
{ "guest_cycle_scale_gate = { addr = -4, value = 1 }", "negative addr" },
{ "guest_cycle_scale_gate = { addr = 0x80010000, value = 0x100000001 }", "oversized value" },
{ "guest_cycle_scale_gate = { addr = 0x80010000, value = -1 }", "negative value" },
{ "guest_cycle_scale_gate = { addr = 0x80010000, value = 1, size = 0x100000004 }", "oversized size" },
{ "guest_cycle_scale_gate = { addr = 0x80010000, value = 1, size = -4 }", "negative size" },
{ "guest_cycle_scale_gate = { addr = 0x80010000, value = 1, mask = 0x1FFFFFFFF }", "oversized mask" },
{ "guest_cycle_scale_gate = { addr = 0x80010000, value = 1, mask = -1 }", "negative mask" },
{ "guest_cycle_scale = 0", "scale 0" },
{ "guest_cycle_scale = 65", "scale 65" },
{ "guest_cycle_scale = -8", "negative scale" },
{ "guest_cycle_scale = 0x100000008", "oversized scale" },
};
for (const auto& b : bad) {
p = write_game_toml("ves_timing_range.toml", std::string("[timing]\n") + b.body + "\n");
rejected = false;
try { (void)PSXRecompV4::load_game_config(p); } catch (const std::exception&) { rejected = true; }
check(rejected, (std::string("timing: rejected ") + b.what).c_str());
fs::remove(p);
}
}
int main() {
test_internal_resolution_game_toml();
test_internal_resolution_settings();
@@ -477,6 +541,7 @@ int main() {
test_present_thread();
test_pipeline_user_settings();
test_pgxp_title_keys();
test_timing_gate();
if (failures) {
std::fprintf(stderr, "video_enhancement_settings_test: %d failure(s)\n",
+17
View File
@@ -313,6 +313,23 @@ if(BUILD_TESTING)
PSX_ENABLE_BLOCK_CYCLES=1)
add_test(NAME psx_cyc_batch_test COMMAND psx_cyc_batch_test)
# [timing] guest_cycle_scale divides instruction charges only (1 = faithful).
add_executable(psx_guest_cycle_scale_test tests/test_psx_guest_cycle_scale.c)
target_include_directories(psx_guest_cycle_scale_test PRIVATE include)
target_compile_definitions(psx_guest_cycle_scale_test PRIVATE
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.
+4
View File
@@ -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
+10
View File
@@ -93,6 +93,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.
+6 -3
View File
@@ -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);
+8 -1
View File
@@ -93,8 +93,15 @@ 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)
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
+90
View File
@@ -66,6 +66,79 @@ extern int g_psx_cyc_bb_defer;
* 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)
@@ -123,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)
+5 -5
View File
@@ -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. */
+25 -1
View File
@@ -391,7 +391,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);
@@ -489,6 +490,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;
}
@@ -625,6 +637,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:
@@ -704,6 +717,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;
@@ -763,6 +786,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;
}
}
+6
View File
@@ -100,6 +100,12 @@ static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask) {
if (cpu->read_absorb[w]) {
cpu->read_absorb[w]--;
} else if (!g_ls_replay_active) {
if (PSX_GCS_ACTIVE()) {
psx_cpu_charge(1u); /* the CPU charge, scaled */
psx_cyc_deps(cpu, reg_mask);
psx_cyc_lds(cpu);
return;
}
uint64_t next = psx_cycle_count + 1u;
if (!g_event_step_conservative && g_psx_cycle_fast_limit != 0u &&
next <= g_psx_cycle_fast_limit) {
+3
View File
@@ -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 */
+57
View File
@@ -1728,6 +1728,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;
@@ -15304,6 +15308,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);
@@ -15782,6 +15799,26 @@ int main(int argc, char** argv) {
gc.runtime.video_texture_window_batching ? 1 : 0);
g_render_thread = gc.runtime.video_render_thread ? 1 : 0;
g_frame_generation = gc.runtime.video_frame_generation ? 1 : 0;
/* [timing] guest_cycle_scale is a title constant from game.toml
* (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%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_gate.empty() ? "" : ", RAM gate",
gc.runtime.guest_cycle_scale_gated ? ", mod gate" : "",
psx_guest_cycle_scale_config());
g_present_thread = gc.runtime.video_present_thread ? 1 : 0;
g_video_vsync = gc.runtime.video_vsync;
g_frame_interpolation = gc.runtime.video_frame_interpolation ? 1 : 0;
@@ -18878,6 +18915,26 @@ session_reboot:
(net_cfg.transport == 2 || !psx_lobby_match_caps() || !psx_lobby_match_caps()->valid))
std::snprintf(net_cfg.content_fingerprint, sizeof(net_cfg.content_fingerprint),
"%s", PSXRecompV4::mod_runtime_session_plan_fp().c_str());
// [timing] guest_cycle_scale changes guest timing, so every peer must
// run the same one. It is a title constant, but fold it into the
// content gate as a safety net (a peer that differs, e.g. through
// the test env override, never matches and the session does not
// start). 1 (faithful) leaves the fingerprint, and vanilla sessions,
// exactly as before.
if (psx_guest_cycle_scale_config() != 1u) {
char tag[9];
std::snprintf(tag, sizeof tag, "%08x", 0x6C000000u | psx_guest_cycle_scale_config());
if (std::strlen(net_cfg.content_fingerprint) != 64)
std::snprintf(net_cfg.content_fingerprint, sizeof(net_cfg.content_fingerprint),
"%s", "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c00000000");
for (int i = 0; i < 8; ++i) {
char* d = &net_cfg.content_fingerprint[56 + i];
auto hv = [](char c) { return c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10; };
*d = "0123456789abcdef"[hv(*d) ^ hv(tag[i])];
}
std::printf("psxrecomp: netplay requires guest_cycle_scale %u on every peer\n",
psx_guest_cycle_scale_config());
}
const int nrc = psx_netplay_start(&net_cfg);
if (nrc != 0) {
const char* const why = netplay_start_failure(nrc, net_cfg);
+6 -2
View File
@@ -2301,6 +2301,10 @@ extern int g_event_step_conservative;
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()) {
psx_cpu_charge(cycles);
return;
}
uint64_t next = psx_cycle_count + (uint64_t)cycles;
if (!g_event_step_conservative && g_psx_cycle_fast_limit != 0u &&
next >= psx_cycle_count && next <= g_psx_cycle_fast_limit) {
@@ -2311,7 +2315,7 @@ static inline void psx_load_charge_cycles(uint32_t cycles) {
}
#else
static inline void psx_load_charge_cycles(uint32_t cycles) {
psx_advance_cycles(cycles);
psx_cpu_charge(cycles);
}
#endif
@@ -2371,7 +2375,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
+6 -1
View File
@@ -8,6 +8,11 @@
#include "interrupts.h"
#include "debug_server.h"
#include "psx_cycles.h"
/* 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"
@@ -2493,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 = psx_advance_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;
+102
View File
@@ -22,6 +22,106 @@
#endif
uint64_t psx_cycle_count = 0;
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 (mod gate) */
static int s_psx_gcs_open = 0; /* mod gate state */
/* [timing] guest_cycle_scale_gate: RAM predicates, all must hold. */
typedef struct { uint32_t phys, size, mask, value; } PsxGcsRamPred;
static PsxGcsRamPred s_psx_gcs_ram[PSX_GCS_RAM_GATE_MAX];
static uint32_t s_psx_gcs_ram_n = 0;
static int s_psx_gcs_ram_open = 0; /* last VBlank's verdict */
static uint32_t (*s_psx_gcs_ram_read)(uint32_t phys, uint32_t size) = NULL;
static void psx_guest_cycle_scale_apply(void) {
const int open = (!s_psx_gcs_gated || s_psx_gcs_open) &&
(s_psx_gcs_ram_n == 0u || s_psx_gcs_ram_open);
uint32_t n = open ? s_psx_gcs_cfg : 1u;
if (n == s_psx_gcs_live) return;
s_psx_gcs_live = n;
g_psx_gcs_recip_q16 = psx_gcs_recip_for(n);
g_psx_gcs_frac = 0u;
}
void psx_guest_cycle_scale_set(uint32_t scale) {
if (scale < 1u) scale = 1u;
if (scale > 64u) scale = 64u;
s_psx_gcs_cfg = scale;
psx_guest_cycle_scale_apply();
}
void psx_guest_cycle_scale_set_gated(int gated) {
s_psx_gcs_gated = gated ? 1 : 0;
psx_guest_cycle_scale_apply();
}
void psx_guest_cycle_scale_gate_open(int open) {
s_psx_gcs_open = open ? 1 : 0;
psx_guest_cycle_scale_apply();
}
void psx_guest_cycle_scale_set_ram_reader(uint32_t (*read)(uint32_t phys, uint32_t size)) {
s_psx_gcs_ram_read = read;
}
int psx_guest_cycle_scale_ram_gate_add(uint32_t addr, uint32_t size,
uint32_t mask, uint32_t value) {
const uint32_t phys = addr & 0x1FFFFFFFu;
if (s_psx_gcs_ram_n >= (uint32_t)PSX_GCS_RAM_GATE_MAX) return 0;
if (size != 1u && size != 2u && size != 4u) return 0;
if ((phys & (size - 1u)) != 0u || phys >= 0x00800000u) return 0; /* main RAM */
if (size < 4u) mask &= (1u << (8u * size)) - 1u;
s_psx_gcs_ram[s_psx_gcs_ram_n].phys = phys;
s_psx_gcs_ram[s_psx_gcs_ram_n].size = size;
s_psx_gcs_ram[s_psx_gcs_ram_n].mask = mask;
s_psx_gcs_ram[s_psx_gcs_ram_n].value = value & mask;
s_psx_gcs_ram_n++;
s_psx_gcs_ram_open = 0; /* shut until the first VBlank */
psx_guest_cycle_scale_apply();
return 1;
}
void psx_guest_cycle_scale_ram_gate_clear(void) {
s_psx_gcs_ram_n = 0u;
s_psx_gcs_ram_open = 0;
psx_guest_cycle_scale_apply();
}
/* VBlank edge (interrupts.c fire_vblank_edge): a guest-time point, so every
* peer and every rollback re-simulation sees the same verdict. */
void psx_guest_cycle_scale_vblank(void) {
if (s_psx_gcs_ram_n == 0u || !s_psx_gcs_ram_read) return;
int open = 1;
for (uint32_t i = 0; open && i < s_psx_gcs_ram_n; i++) {
const PsxGcsRamPred *p = &s_psx_gcs_ram[i];
open = (s_psx_gcs_ram_read(p->phys, p->size) & p->mask) == p->value;
}
if (open == s_psx_gcs_ram_open) return;
s_psx_gcs_ram_open = open;
psx_guest_cycle_scale_apply();
}
/* Public mod API (mod_plugins.h): the mod gate. */
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;
uint32_t g_psx_cyc_batch_limit = 0;
int g_psx_cyc_bb_defer = 0;
@@ -721,6 +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();
latency = psx_cpu_cycles(latency); /* [timing] guest_cycle_scale */
cpu->muldiv_ts_done = psx_cycle_count + (uint64_t)latency;
}
@@ -822,6 +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));
}
latency = psx_cpu_cycles(latency); /* [timing] guest_cycle_scale */
cpu->gte_ts_done = psx_cycle_count + (uint64_t)latency;
}
+2 -2
View File
@@ -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,6 +81,8 @@ void psx_unknown_dispatch(CPUState *cpu, uint32_t addr, uint32_t phys) {
}
void psx_advance_cycles(uint32_t cycles) { (void)cycles; }
/* 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; }
@@ -69,6 +69,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; }
+2
View File
@@ -16,6 +16,8 @@ int g_ls_mode = 0;
volatile int g_ds_recording = 0;
uint8_t *g_psx_ram = 0;
int g_psx_load_delay = 1;
uint32_t g_psx_gcs_recip_q16 = 65536u;
uint32_t g_psx_gcs_frac = 0u;
static int service_count;
@@ -0,0 +1,60 @@
/* [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>
#include <stdint.h>
uint64_t psx_cycle_count = 0;
uint64_t psx_next_service_cycle = 0;
uint32_t g_psx_cyc_batch = 0;
uint32_t g_psx_cyc_batch_limit = 0;
int g_psx_cyc_bb_defer = 0;
uint32_t *g_psx_cyc_local_acc = 0;
int psx_in_device_service = 0;
int g_event_step_conservative = 0;
int g_ls_replay_active = 0;
int g_ls_mode = 0;
volatile int g_ds_recording = 0;
uint8_t *g_psx_ram = 0;
int g_psx_load_delay = 1;
uint32_t g_psx_gcs_recip_q16 = 65536u;
uint32_t g_psx_gcs_frac = 0u;
void psx_devices_service_to_now(void) { psx_next_service_cycle = psx_cycle_count + 100000u; }
void psx_advance_cycles_slow(uint32_t cycles) { psx_cycle_count += cycles; }
static uint64_t run(uint32_t scale, uint32_t charges, uint32_t each) {
g_psx_gcs_recip_q16 = psx_gcs_recip_for(scale);
g_psx_gcs_frac = 0;
psx_cycle_count = 0;
psx_next_service_cycle = 1;
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
for (uint32_t i = 0; i < charges; i++) psx_cyc_charge(each);
psx_cyc_batch_flush();
return psx_cycle_count;
}
int main(void) {
assert(psx_gcs_recip_for(1) == 65536u);
assert(psx_gcs_recip_for(0) == 65536u); /* never slower than faithful */
assert(psx_gcs_recip_for(2) == 32768u);
assert(psx_gcs_recip_for(16) == 4096u);
assert(psx_gcs_recip_for(999) == 1024u); /* clamped to 64 */
/* 1 (faithful): identical totals for every charge size. */
assert(run(1, 1000, 1) == 1000u);
assert(run(1, 1000, 7) == 7000u);
/* Power-of-two scales: no drift across many 1-cycle charges. */
assert(run(2, 1000, 1) == 500u);
assert(run(4, 1000, 5) == 1250u);
assert(run(16, 160000, 1) == 10000u);
/* Non-power-of-two keeps the fraction: scale 3 of 3000 cycles is ~1000. */
uint64_t t = run(3, 3000, 1);
assert(t >= 999u && t <= 1001u);
return 0;
}
@@ -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;
+2
View File
@@ -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);