mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
Reference in New Issue
Block a user