diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 44683e0f..eacbe427 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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 diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index dabb7584..228b735b 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -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. */ diff --git a/runtime/include/psx_cycle_freeze.h b/runtime/include/psx_cycle_freeze.h index ba300bb0..4e6f5c78 100644 --- a/runtime/include/psx_cycle_freeze.h +++ b/runtime/include/psx_cycle_freeze.h @@ -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 diff --git a/runtime/src/dma.c b/runtime/src/dma.c index 4690a8f1..b60d8add 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -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(); diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 0ae27908..de5bccec 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -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); diff --git a/runtime/src/mod_guest_services.c b/runtime/src/mod_guest_services.c index 141a5b4e..d644ff49 100644 --- a/runtime/src/mod_guest_services.c +++ b/runtime/src/mod_guest_services.c @@ -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 @@ -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); diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 4bfe7460..1869205d 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -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; +} diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index 2cbbe4bb..8424792c 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -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; diff --git a/runtime/tests/psx_ram_runtime_map_stubs.c b/runtime/tests/psx_ram_runtime_map_stubs.c index 2e98819d..edef5b7d 100644 --- a/runtime/tests/psx_ram_runtime_map_stubs.c +++ b/runtime/tests/psx_ram_runtime_map_stubs.c @@ -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(); } diff --git a/runtime/tests/test_dma_gpu_list_cpu_hold.c b/runtime/tests/test_dma_gpu_list_cpu_hold.c index d3a70a15..02c2ce70 100644 --- a/runtime/tests/test_dma_gpu_list_cpu_hold.c +++ b/runtime/tests/test_dma_gpu_list_cpu_hold.c @@ -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; diff --git a/runtime/tests/test_render_pass_guards.py b/runtime/tests/test_render_pass_guards.py index 035a4d5a..d8285fa5 100644 --- a/runtime/tests/test_render_pass_guards.py +++ b/runtime/tests/test_render_pass_guards.py @@ -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") diff --git a/runtime/tests/test_uncharged_span.c b/runtime/tests/test_uncharged_span.c new file mode 100644 index 00000000..0e7bc22d --- /dev/null +++ b/runtime/tests/test_uncharged_span.c @@ -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 +#include + +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; +}