Merge pull request #541 from RetroPortingToolKit/feat/mod-uncharged-guest-call

Add uncharged guest calls: run a guest function in zero guest time
This commit is contained in:
Matt Stanley
2026-10-06 11:13:08 -07:00
committed by GitHub
12 changed files with 331 additions and 39 deletions
+8
View File
@@ -1008,6 +1008,14 @@ if(BUILD_TESTING)
target_include_directories(render_pass_freeze_test PRIVATE include)
add_test(NAME render_pass_freeze_test COMMAND render_pass_freeze_test)
# Uncharged guest-time spans (psx_mod_call_guest_uncharged's time layer)
# against the same mock devices.
add_executable(uncharged_span_test
tests/test_uncharged_span.c
src/psx_cycles.c)
target_include_directories(uncharged_span_test PRIVATE include)
add_test(NAME uncharged_span_test COMMAND uncharged_span_test)
# Render-pass watchdog abort from nested guest frames: the real
# render_pass.c and psx_cycles.c against mock memory, devices and
# presenter (the frames use GCC/Clang cleanup handlers, as generated
+17
View File
@@ -144,6 +144,23 @@ void psx_mod_write_code_word(uint32_t address, uint32_t value);
uint32_t psx_mod_call_guest(struct CPUState* cpu, uint32_t function,
uint32_t return_address, uint32_t a0, uint32_t a1,
uint32_t a2, uint32_t a3);
/* psx_mod_call_guest in zero guest time, for CPU/RAM work a game spends guest
* time on while it loads (allocators, decoders, table builds). Every RAM,
* register and device effect of the callee is kept, as if it executed in an
* instant at the call's guest cycle: none of its cycles are charged, no
* device advances and no interrupt is taken while it runs, and the caller's
* pending mult/div, GTE and load-pipeline timing is as it was before the
* call. Device work the callee starts (a DMA, a CD command) proceeds in guest
* time after the return. A callee that executes more than budget_cycles
* (0 = no limit) is charged in full from then on, so one that waits on a
* device still completes; *charged (optional) is then set to 1. Inside a
* render pass or another uncharged call, time is already frozen and this is
* psx_mod_call_guest. The outcome depends only on guest state, so netplay
* peers and rollback re-simulation agree. Returns v0. */
uint32_t psx_mod_call_guest_uncharged(struct CPUState* cpu, uint32_t function,
uint32_t return_address, uint32_t a0,
uint32_t a1, uint32_t a2, uint32_t a3,
uint32_t budget_cycles, int* charged);
/* Deliver disc sectors into RAM exactly as a completed CD-ROM DMA would
* (overlay capture, executable-page invalidation, CD DMA log when lba >= 0).
* Word-aligned address and length. Returns 0 when the span leaves RAM. */
+18 -1
View File
@@ -1,7 +1,8 @@
#ifndef PSX_CYCLE_FREEZE_H
#define PSX_CYCLE_FREEZE_H
/* Render-pass time freeze (render_pass.c, docs/RENDER_PASSES.md).
/* Guest-time freeze: render passes (render_pass.c, docs/RENDER_PASSES.md)
* and uncharged guest calls (mod_plugins.h psx_mod_call_guest_uncharged).
*
* Runtime-only: generated and overlay code never sees this header. It is kept
* out of psx_cycles.h on purpose, because psx_cycles.h is part of the
@@ -24,6 +25,10 @@ extern "C" {
#endif
extern int g_psx_render_pass_active;
/* Set while guest time is frozen, by a render pass or an uncharged call:
* devices are not serviced and no interrupt is delivered. Pass-only effects
* (rolled-back stores, synchronous DMA) key on g_psx_render_pass_active. */
extern int g_psx_guest_time_frozen;
typedef struct PsxCycleFreeze {
uint64_t cycle_count;
uint64_t next_service;
@@ -42,6 +47,18 @@ int psx_cycle_freeze_begin(PsxCycleFreeze *save, uint64_t watchdog_cycles,
void (*overrun)(void));
void psx_cycle_freeze_end(const PsxCycleFreeze *save);
/* Uncharged span: the same freeze without the pass's rollback. Code run
* between begin and end keeps every effect on RAM, registers and devices, as
* if it executed in an instant at the start cycle: end() puts the clock back,
* so none of its cycles are charged, no device advanced and no interrupt was
* taken meanwhile. A span that executes more than `budget_cycles` (0 = no
* limit) thaws: from then on it runs on the live clock and all its cycles are
* charged, so a callee that waits on a device still completes. begin()
* returns 0 while time is already frozen (the caller then simply runs the
* code). end() returns 1 when the span stayed uncharged. Spans do not nest. */
int psx_cycle_uncharged_begin(PsxCycleFreeze *save, uint64_t budget_cycles);
int psx_cycle_uncharged_end(const PsxCycleFreeze *save);
#ifdef __cplusplus
}
#endif
+1 -1
View File
@@ -844,7 +844,7 @@ static uint64_t hold_host_ticks_per_sec(void) {
}
static void hold_cpu_for_gpu_linked_list(void) {
if (psx_in_device_service || g_ls_replay_active || g_psx_render_pass_active)
if (psx_in_device_service || g_ls_replay_active || g_psx_guest_time_frozen)
return;
const uint64_t cyc0 = psx_cycle_count;
const uint64_t host0 = hold_host_ticks();
+6 -5
View File
@@ -1087,8 +1087,9 @@ void psx_interrupt_check_path_diag(uint64_t *entry, uint64_t *fast_sr,
}
int psx_interrupt_delivery_needed(const CPUState* cpu) {
/* Render passes run in frozen guest time: nothing is delivered. */
if (g_psx_render_pass_active) return 0;
/* Render passes and uncharged calls run in frozen guest time: nothing
* is delivered. */
if (g_psx_guest_time_frozen) return 0;
if (s_defer_switch_pending) { s_need_defer++; return 1; }
if ((i_stat & i_mask) == 0) { s_skip_none++; return 0; }
@@ -1113,9 +1114,9 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) {
}
void psx_check_interrupts(CPUState* cpu) {
/* Render passes run in frozen guest time (psx_cycles.h): no delivery, and
* no check-boundary bookkeeping that the stock timeline would not do. */
if (g_psx_render_pass_active) return;
/* Frozen guest time (psx_cycle_freeze.h): no delivery, and no
* check-boundary bookkeeping that the stock timeline would not do. */
if (g_psx_guest_time_frozen) return;
psx_cyc_batch_flush();
extern int g_ls_suppress_record;
extern int psx_netplay_active(void);
+47
View File
@@ -6,6 +6,7 @@
#include "dma.h"
#include "gpu.h"
#include "interrupts.h"
#include "psx_cycle_freeze.h"
#include "psx_memory.h"
#include "spu.h"
#include <string.h>
@@ -32,6 +33,52 @@ uint32_t psx_mod_call_guest(struct CPUState* cpu, uint32_t function,
return result;
}
/* The caller's in-flight timing state: deadlines are absolute guest cycles
* and the load pipeline describes the instructions around the call, so an
* uncharged callee must leave them as they were. */
typedef struct CallerTiming {
uint64_t muldiv_ts_done, gte_ts_done;
uint8_t read_absorb[33];
uint8_t read_absorb_which, read_fudge, ld_which_t;
uint32_t ld_absorb;
} CallerTiming;
static void timing_save(CallerTiming* t, const struct CPUState* cpu) {
t->muldiv_ts_done = cpu->muldiv_ts_done;
t->gte_ts_done = cpu->gte_ts_done;
memcpy(t->read_absorb, cpu->read_absorb, sizeof t->read_absorb);
t->read_absorb_which = cpu->read_absorb_which;
t->read_fudge = cpu->read_fudge;
t->ld_which_t = cpu->ld_which_t;
t->ld_absorb = cpu->ld_absorb;
}
static void timing_restore(struct CPUState* cpu, const CallerTiming* t) {
cpu->muldiv_ts_done = t->muldiv_ts_done;
cpu->gte_ts_done = t->gte_ts_done;
memcpy(cpu->read_absorb, t->read_absorb, sizeof t->read_absorb);
cpu->read_absorb_which = t->read_absorb_which;
cpu->read_fudge = t->read_fudge;
cpu->ld_which_t = t->ld_which_t;
cpu->ld_absorb = t->ld_absorb;
}
uint32_t psx_mod_call_guest_uncharged(struct CPUState* cpu, uint32_t function,
uint32_t return_address, uint32_t a0,
uint32_t a1, uint32_t a2, uint32_t a3,
uint32_t budget_cycles, int* charged) {
PsxCycleFreeze save;
CallerTiming timing;
timing_save(&timing, cpu);
const int frozen = psx_cycle_uncharged_begin(&save, budget_cycles);
const uint32_t result =
psx_mod_call_guest(cpu, function, return_address, a0, a1, a2, a3);
const int uncharged = frozen ? psx_cycle_uncharged_end(&save) : 1;
if (frozen && uncharged) timing_restore(cpu, &timing);
if (charged) *charged = !uncharged;
return result;
}
int psx_mod_dma_write_ram(uint32_t address, const void* data, uint32_t bytes,
int lba) {
return dma_host_cdrom_write(address, (const uint8_t*)data, bytes, lba);
+79 -25
View File
@@ -29,20 +29,34 @@ uint32_t *g_psx_cyc_local_acc = NULL;
static int s_cycle_replay_active = 0;
static uint64_t s_cycle_replay_live = 0;
/* ---- render-pass freeze (see psx_cycle_freeze.h) ---- */
/* ---- guest-time freeze: render passes and uncharged calls
* (see psx_cycle_freeze.h) ---- */
int g_psx_render_pass_active = 0;
int g_psx_guest_time_frozen = 0;
static uint64_t s_freeze_start = 0;
static uint64_t s_freeze_limit = 0;
static void (*s_freeze_overrun)(void) = 0;
static int s_uncharged_active = 0;
static int s_uncharged_thawed = 0;
static void psx_cycle_freeze_tick(void) {
extern uint64_t g_psx_cycle_fast_limit;
/* Come back at most every 16K cycles for the watchdog; never service. */
psx_next_service_cycle = psx_cycle_count + 16384u;
g_psx_cycle_fast_limit = 0;
if (s_freeze_limit &&
psx_cycle_count - s_freeze_start > s_freeze_limit &&
s_freeze_overrun) {
if (!s_freeze_limit || psx_cycle_count - s_freeze_start <= s_freeze_limit)
return;
if (s_uncharged_active) {
/* Over budget: the rest of the call runs on the live clock, and every
* cycle it executed so far is charged. Devices catch up from where
* they stopped at the next charge, in event-bounded steps. */
g_psx_guest_time_frozen = 0;
s_freeze_limit = 0;
s_uncharged_thawed = 1;
psx_next_service_cycle = 0;
return;
}
if (s_freeze_overrun) {
void (*overrun)(void) = s_freeze_overrun;
s_freeze_overrun = 0;
overrun();
@@ -202,7 +216,7 @@ static void psx_devices_recompute_deadline(void) {
}
void psx_devices_service_to_now(void) {
if (g_psx_render_pass_active) { psx_cycle_freeze_tick(); return; }
if (g_psx_guest_time_frozen) { psx_cycle_freeze_tick(); return; }
if (s_in_device_service) return; /* device code charged cycles: absorb */
if (g_plp_cycle_diag) g_plp_svc_calls++;
g_psx_cycle_fast_limit = 0;
@@ -265,7 +279,7 @@ void psx_devices_service_to_now(void) {
* that on every GPU/CD/MDEC MMIO touch. Recompute here instead. */
void psx_devices_mmio_sync(void) {
psx_cyc_batch_flush();
if (g_psx_render_pass_active) { psx_cycle_freeze_tick(); return; }
if (g_psx_guest_time_frozen) { psx_cycle_freeze_tick(); return; }
if (s_devices_synced_cycle != psx_cycle_count) {
psx_devices_service_to_now();
} else {
@@ -327,7 +341,7 @@ void psx_advance_cycles_slow(uint32_t cycles) {
}
}
if (cycles == 0) return;
if (g_psx_render_pass_active) {
if (g_psx_guest_time_frozen) {
psx_cycle_count += (uint64_t)cycles;
if (psx_cycle_count >= psx_next_service_cycle) psx_cycle_freeze_tick();
return;
@@ -821,10 +835,8 @@ void psx_gte_stall(CPUState* cpu) {
}
}
int psx_cycle_freeze_begin(PsxCycleFreeze *save, uint64_t watchdog_cycles,
void (*overrun)(void)) {
static void freeze_save(PsxCycleFreeze *save) {
extern uint64_t g_psx_cycle_fast_limit;
if (!save || g_psx_render_pass_active) return 0;
save->cycle_count = psx_cycle_count;
save->next_service = psx_next_service_cycle;
save->fast_limit = g_psx_cycle_fast_limit;
@@ -835,28 +847,17 @@ int psx_cycle_freeze_begin(PsxCycleFreeze *save, uint64_t watchdog_cycles,
save->in_device_service = psx_in_device_service;
save->bb_defer = g_psx_cyc_bb_defer;
/* Pending deferred charges stay pending: they belong to the interrupted
* guest code and are republished, unchanged, after the pass. */
* guest code and are republished, unchanged, after the freeze. */
g_psx_cyc_batch = 0;
g_psx_cyc_batch_limit = 0;
g_psx_cyc_local_acc = NULL;
psx_in_device_service = 0;
s_freeze_start = psx_cycle_count;
s_freeze_limit = watchdog_cycles;
s_freeze_overrun = overrun;
g_psx_render_pass_active = 1;
psx_cycle_freeze_tick();
return 1;
}
void psx_cycle_freeze_end(const PsxCycleFreeze *save) {
extern uint64_t g_psx_cycle_fast_limit;
g_psx_render_pass_active = 0;
s_freeze_overrun = 0;
s_freeze_limit = 0;
if (!save) return;
psx_cycle_count = save->cycle_count;
psx_next_service_cycle = save->next_service;
g_psx_cycle_fast_limit = save->fast_limit;
/* The interrupted code's deferred-charge state, which the frozen code never
* owned, comes back in every case. */
static void freeze_restore_deferred(const PsxCycleFreeze *save) {
g_psx_cyc_batch = save->batch;
g_psx_cyc_batch_limit = save->batch_limit;
g_psx_cyc_local_acc = save->local_acc;
@@ -864,3 +865,56 @@ void psx_cycle_freeze_end(const PsxCycleFreeze *save) {
psx_in_device_service = save->in_device_service;
g_psx_cyc_bb_defer = save->bb_defer;
}
static void freeze_restore_clock(const PsxCycleFreeze *save) {
extern uint64_t g_psx_cycle_fast_limit;
psx_cycle_count = save->cycle_count;
psx_next_service_cycle = save->next_service;
g_psx_cycle_fast_limit = save->fast_limit;
}
int psx_cycle_freeze_begin(PsxCycleFreeze *save, uint64_t watchdog_cycles,
void (*overrun)(void)) {
if (!save || g_psx_guest_time_frozen) return 0;
freeze_save(save);
s_freeze_limit = watchdog_cycles;
s_freeze_overrun = overrun;
g_psx_render_pass_active = 1;
g_psx_guest_time_frozen = 1;
psx_cycle_freeze_tick();
return 1;
}
void psx_cycle_freeze_end(const PsxCycleFreeze *save) {
g_psx_render_pass_active = 0;
g_psx_guest_time_frozen = 0;
s_freeze_overrun = 0;
s_freeze_limit = 0;
if (!save) return;
freeze_restore_clock(save);
freeze_restore_deferred(save);
}
int psx_cycle_uncharged_begin(PsxCycleFreeze *save, uint64_t budget_cycles) {
if (!save || g_psx_guest_time_frozen) return 0;
freeze_save(save);
s_freeze_limit = budget_cycles;
s_freeze_overrun = 0;
s_uncharged_active = 1;
s_uncharged_thawed = 0;
g_psx_guest_time_frozen = 1;
psx_cycle_freeze_tick();
return 1;
}
int psx_cycle_uncharged_end(const PsxCycleFreeze *save) {
const int uncharged = !s_uncharged_thawed;
g_psx_guest_time_frozen = 0;
s_uncharged_active = 0;
s_uncharged_thawed = 0;
s_freeze_limit = 0;
if (!save) return 0;
if (uncharged) freeze_restore_clock(save);
freeze_restore_deferred(save);
return uncharged;
}
+1 -1
View File
@@ -349,7 +349,7 @@ static uint32_t transaction_status(int stereo) {
if (psx_presentation_fast_forward()) return PSX_MOD_RENDER_PASS_FAST_FORWARD;
gl = stereo ? gl_renderer_stereo_unavailable() : gl_renderer_pass_unavailable();
if (gl != PSX_MOD_RENDER_PASS_READY) return gl;
if (s_nesting || g_psx_render_pass_active || psx_get_in_exception() ||
if (s_nesting || g_psx_guest_time_frozen || psx_get_in_exception() ||
dma_gpu_linked_list_active())
return PSX_MOD_RENDER_PASS_BUSY;
return PSX_MOD_RENDER_PASS_READY;
@@ -113,6 +113,7 @@ void gpu_ws_restore_linked_list_rank(uint32_t r) { (void)r; }
* linked list synchronously while a pass runs; with the flag at 0 neither
* path is taken, so the doubles trap if they ever are. */
int g_psx_render_pass_active;
int g_psx_guest_time_frozen;
int render_pass_store_to(const void *t, uint32_t a, uint32_t v, uint32_t w) {
(void)t; (void)a; (void)v; (void)w; __builtin_trap();
}
@@ -36,6 +36,7 @@ int psx_in_device_service;
int g_event_step_conservative;
int g_ls_replay_active;
int g_psx_render_pass_active;
int g_psx_guest_time_frozen;
uint32_t g_psx_cyc_batch;
uint32_t g_psx_cyc_batch_limit;
+11 -6
View File
@@ -4,8 +4,10 @@
A render pass runs guest draw code in frozen time and restores the machine
afterwards. That only holds if every path that could let time pass, deliver an
interrupt, touch a device the restore does not cover, or record the pass into
the live timeline checks g_psx_render_pass_active first. These are the choke
points; each guard below names the hole it closes.
the live timeline checks g_psx_render_pass_active first. The time freeze
itself is shared with uncharged guest calls and keys on g_psx_guest_time_frozen,
which every pass sets. These are the choke points; each guard below names the
hole it closes.
"""
from pathlib import Path
@@ -23,21 +25,24 @@ def body(text, signature):
cycles = (SRC / "psx_cycles.c").read_text(encoding="utf-8")
b = body(cycles, "int psx_cycle_freeze_begin(PsxCycleFreeze *save, uint64_t watchdog_cycles,")
assert "g_psx_render_pass_active = 1;" in b and "g_psx_guest_time_frozen = 1;" in b, (
"a pass must freeze guest time")
b = body(cycles, "void psx_devices_service_to_now(void) {")
assert re.search(r"if \(g_psx_render_pass_active\)[^\n]*return;", b), (
assert re.search(r"if \(g_psx_guest_time_frozen\)[^\n]*return;", b), (
"device servicing must stop during a pass (time would advance)")
b = body(cycles, "void psx_devices_mmio_sync(void) {")
assert "g_psx_render_pass_active" in b, (
assert "g_psx_guest_time_frozen" in b, (
"MMIO sync must not catch devices up during a pass")
b = body(cycles, "void psx_advance_cycles_slow(uint32_t cycles) {")
assert "g_psx_render_pass_active" in b, (
assert "g_psx_guest_time_frozen" in b, (
"the slow/conservative cycle path must not advance devices during a pass")
irq = (SRC / "interrupts.c").read_text(encoding="utf-8")
for sig in ("int psx_interrupt_delivery_needed(const CPUState* cpu) {",
"void psx_check_interrupts(CPUState* cpu) {"):
lines = body(irq, sig).splitlines()[:6]
assert any("g_psx_render_pass_active" in l and "return" in l for l in lines), (
assert any("g_psx_guest_time_frozen" in l and "return" in l for l in lines), (
"no interrupt may be delivered inside a pass: " + sig)
dma = (SRC / "dma.c").read_text(encoding="utf-8")
+141
View File
@@ -0,0 +1,141 @@
/* Uncharged guest-time spans (psx_cycle_freeze.h psx_cycle_uncharged_*),
* the time layer of mod_plugins.h psx_mod_call_guest_uncharged. Code in a
* span keeps its effects but consumes no guest time; a span over budget
* thaws and is charged in full. Build/run: ctest -R uncharged_span_test */
#include "psx_cycles.h"
#include "psx_cycle_freeze.h"
#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;
/* Mock devices: count every advance and event. The "CD" raises an event
* every 1000 cycles; the VBlank every 564480. */
static uint64_t s_dev_cycles = 0;
static uint64_t s_dev_calls = 0;
static uint64_t s_cd_events = 0;
static uint32_t s_cd_phase = 0;
static uint64_t s_vblank_phase = 0;
void sio_advance(uint32_t cycles) { (void)cycles; }
void cdrom_advance(uint32_t cycles) {
s_dev_calls++;
s_dev_cycles += cycles;
s_cd_phase += cycles;
while (s_cd_phase >= 1000u) { s_cd_phase -= 1000u; s_cd_events++; }
}
void dma_advance(uint32_t cycles) { (void)cycles; }
void timers_advance(uint32_t cycles) { (void)cycles; }
void interrupts_advance_cycles(uint32_t cycles) { s_vblank_phase += cycles; }
void interrupts_service_scheduled_events(void) {}
uint32_t interrupts_cycles_to_vblank(void) {
return (uint32_t)(564480u - (s_vblank_phase % 564480u));
}
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 1000u - s_cd_phase;
}
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; }
uint32_t psx_spu_sample_event_cycles_to_next(void) { return UINT32_MAX; }
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) {}
/* memory.c mod arenas (render_pass_mod_store): nothing journaled here. */
void render_pass_mod_arenas_rollback(void) {}
uint64_t render_pass_mod_arenas_hash(void) { return 0; }
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 int failures;
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
static uint64_t s_cd_phase_total(void) { return s_dev_cycles; }
int main(void) {
PsxCycleFreeze save, pass;
uint64_t cyc0, next0, dev0, cd0;
uint32_t local = 7;
psx_cycles_resync_after_restore(NULL);
psx_advance_cycles(12345);
g_psx_cyc_batch = 40;
g_psx_cyc_local_acc = &local;
cyc0 = psx_cycle_count;
next0 = psx_next_service_cycle;
dev0 = s_cd_phase_total();
cd0 = s_cd_events;
/* 1. A span within budget: counted inside, nothing charged after. */
CHECK(psx_cycle_uncharged_begin(&save, 4000000u), "span begins");
CHECK(g_psx_guest_time_frozen == 1 && g_psx_render_pass_active == 0,
"time frozen, but this is not a render pass (stores stay live)");
CHECK(!psx_cycle_uncharged_begin(&pass, 0), "spans do not nest");
CHECK(!psx_cycle_freeze_begin(&pass, 0, NULL), "no render pass inside a span");
CHECK(g_psx_cyc_batch == 0 && g_psx_cyc_local_acc == NULL,
"the caller's pending charges are parked");
for (int i = 0; i < 1000; i++) psx_advance_cycles(1000);
psx_advance_cycles_slow(5000);
psx_devices_service_to_now();
psx_devices_mmio_sync();
CHECK(psx_cycle_count - cyc0 == 1005000u, "cycles are counted inside the span");
CHECK(s_cd_phase_total() == dev0 && s_cd_events == cd0,
"no device advanced and no device event fired");
CHECK(psx_cycle_uncharged_end(&save) == 1, "span stayed uncharged");
CHECK(g_psx_guest_time_frozen == 0, "time is live again");
CHECK(psx_cycle_count == cyc0 && psx_next_service_cycle == next0,
"clock and service deadline restored exactly: zero guest time");
CHECK(g_psx_cyc_batch == 40 && g_psx_cyc_local_acc == &local && local == 7,
"the caller's pending charges come back untouched");
g_psx_cyc_batch = 0;
g_psx_cyc_local_acc = NULL;
/* 2. A render pass refuses to start a span inside it. */
CHECK(psx_cycle_freeze_begin(&pass, 0, NULL), "pass begins");
CHECK(!psx_cycle_uncharged_begin(&save, 0), "no span inside a pass");
psx_cycle_freeze_end(&pass);
CHECK(g_psx_guest_time_frozen == 0 && g_psx_render_pass_active == 0, "pass over");
/* 3. Over budget: the span thaws and everything it ran is charged.
* Devices catch up from where they stopped, so the CD raises exactly
* the events the elapsed time implies (a callee waiting on a device
* would see it complete). */
cyc0 = psx_cycle_count;
dev0 = s_cd_phase_total();
cd0 = s_cd_events;
{
const uint32_t phase0 = s_cd_phase;
CHECK(psx_cycle_uncharged_begin(&save, 200000u), "budgeted span begins");
int thawed_at = -1;
for (int i = 0; i < 1000; i++) {
psx_advance_cycles(1000);
if (thawed_at < 0 && !g_psx_guest_time_frozen) thawed_at = i;
}
CHECK(thawed_at > 0 && thawed_at < 260, "thawed just past the budget");
CHECK(psx_cycle_uncharged_end(&save) == 0, "span reports it was charged");
psx_devices_service_to_now();
CHECK(psx_cycle_count - cyc0 == 1000000u, "every cycle stays charged");
CHECK(s_cd_phase_total() - dev0 == 1000000u,
"devices caught up over the whole span");
CHECK(s_cd_events - cd0 == (uint64_t)((phase0 + 1000000u) / 1000u),
"device events fired for the whole elapsed time");
}
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}