runtime: fix VBlank pacing to use guest cycles, not dispatch count

VBlank firing in psx_check_interrupts was gated on dispatch_count
(block-dispatch count, ~5-6 cycles each), not on guest cycles.
50000 dispatches ≈ 289k cycles per VBlank, vs real PSX 564480
cycles per VBlank. Effect: game-internal time ran at ~51% of real
PSX rate, breaking every cycle-paced game flow including FMV.

This was the root cause of TombaRecomp ISSUES.md #4 — FMV played
at 6.7 fps because Tomba's FMV state machine is driven by DMA Ch1
(MDEC-out done) IRQ, which in turn fires once per full FMV-frame
pipeline iteration, which is bounded by cycle-paced steps
(CDROM sector ready, MDEC decode "complete", DMA transfer).
At 51% guest-time rate, a Tomba-designed 15-fps FMV becomes 7-8 fps.

Fix:
- New VBLANK_CYCLES = 564480 (33.8688 MHz / 60 Hz NTSC).
- Add interrupts_advance_cycles(cycles) called from
  psx_advance_cycles() so the cycle counter for VBlank gating
  tracks emitted guest cycles.
- Change the trigger from `dispatch_count >= VBLANK_INTERVAL` to
  `cycles_since_vblank >= VBLANK_CYCLES`.
- On VBlank fire, subtract one period (not reset to 0) so cycle
  overshoot from long blocks carries forward.
- dispatch_count is kept for the SIO card-protocol stale check —
  that uses total_checks, which is the real load-bearing field
  there, dispatch_count itself is now incidental.

Verified:
- Cycles/VBlank: 289k -> 564k (exactly VBLANK_CYCLES).
- Cycles/sec: 20.6 M -> 41 M (121% of real PSX; slight wall-clock
  overshoot to investigate, but game-internal pacing now correct).
- FMV decode rate: 0.111 -> 0.250 decodes/VBlank = 15.0 fps exactly,
  matching Tomba's designed FMV rate.
- Measured via tools/_mdec_pace.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-05-13 14:09:11 -07:00
parent f42c597090
commit b486c13c3a
3 changed files with 27 additions and 2 deletions
+7
View File
@@ -29,6 +29,13 @@ void interrupts_init(void);
* pending + COP0 allows, dispatches the exception handler. */
void psx_check_interrupts(struct CPUState* cpu);
/* Accumulate emitted PSX cycles toward the next VBlank trigger.
* Called from psx_advance_cycles() so the VBlank rate is gated on
* guest cycles (correct PSX timing) rather than block-dispatch
* count (which was 5-6x too fast and squeezed game-time to ~60% of
* real). One real-PSX VBlank = 564480 cycles (33.8688 MHz / 60). */
void interrupts_advance_cycles(uint32_t cycles);
/* Query whether we are currently inside an exception handler dispatch. */
int psx_get_in_exception(void);
+18 -2
View File
@@ -46,9 +46,15 @@ extern uint32_t i_stat;
extern uint32_t i_mask;
/* Dispatch counter for vblank scheduling. */
#define VBLANK_INTERVAL 50000
#define VBLANK_INTERVAL 50000 /* legacy: dispatch-count fallback (unused for VBlank gating now) */
#define VBLANK_CYCLES 564480u /* 33.8688 MHz / 60 Hz — real PSX NTSC VBlank period */
static uint32_t dispatch_count;
static uint64_t total_checks;
static uint32_t cycles_since_vblank; /* incremented by interrupts_advance_cycles */
void interrupts_advance_cycles(uint32_t cycles) {
cycles_since_vblank += cycles;
}
/* Reentrancy guard: prevent interrupt handler from triggering interrupts. */
static int in_exception;
@@ -177,7 +183,13 @@ void psx_check_interrupts(CPUState* cpu) {
#endif
#endif
dispatch_count++;
if (dispatch_count >= VBLANK_INTERVAL) {
/* VBlank pacing is now cycle-based (real PSX has VBlank at
* VBLANK_CYCLES = 564480 = 33.8688 MHz / 60 Hz). Previously this
* used dispatch_count >= 50000 which fired at ~5-6 cycles per
* dispatch = ~300k cycles per VBlank — half of real PSX, so
* guest time ran at ~60% real rate. That broke FMV pacing
* (TombaRecomp ISSUES.md #4: FMV at 6.7 fps vs 15 fps target). */
if (cycles_since_vblank >= VBLANK_CYCLES) {
/* Defer VBlank while a card SIO transaction is mid-flight.
* On real hardware a 140-byte sector read finishes (~4.5ms)
* well before the next VBlank (16.67ms). Our dispatch-count
@@ -194,6 +206,10 @@ void psx_check_interrupts(CPUState* cpu) {
uint64_t since_progress = total_checks - total_checks_at_progress;
int progress_stale = since_progress >= VBLANK_DEFER_STALE;
if (!card_active || progress_stale) {
/* Subtract one VBlank period rather than reset to 0 so
* cycle overshoot carries forward. Prevents long-running
* blocks from rounding multiple VBlanks together. */
cycles_since_vblank -= VBLANK_CYCLES;
dispatch_count = 0;
i_stat |= (1 << IRQ_VBLANK);
gpu_vblank_tick(); /* Toggle LCF (GPUSTAT bit 31) */
+2
View File
@@ -2,6 +2,7 @@
#include "psx_cycles.h"
#include "cdrom.h"
#include "interrupts.h"
#include "sio.h"
#include "starvation_ring.h"
#include "timers.h"
@@ -20,6 +21,7 @@ void psx_advance_cycles(uint32_t cycles) {
sio_advance(cycles);
cdrom_advance(cycles);
timers_advance(cycles);
interrupts_advance_cycles(cycles);
s_watchdog_throttle += cycles;
if (s_watchdog_throttle >= 65536u) {
s_watchdog_throttle = 0;