mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
b486c13c3a
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>
75 lines
2.6 KiB
C
75 lines
2.6 KiB
C
#ifndef PSXRECOMP_V4_INTERRUPTS_H
|
|
#define PSXRECOMP_V4_INTERRUPTS_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
struct CPUState;
|
|
|
|
/* IRQ bit positions in I_STAT/I_MASK */
|
|
#define IRQ_VBLANK 0
|
|
#define IRQ_GPU 1
|
|
#define IRQ_CDROM 2
|
|
#define IRQ_DMA 3
|
|
#define IRQ_TIMER0 4
|
|
#define IRQ_TIMER1 5
|
|
#define IRQ_TIMER2 6
|
|
#define IRQ_SIO0 7
|
|
#define IRQ_SIO1 8
|
|
#define IRQ_SPU 9
|
|
#define IRQ_PIO 10
|
|
|
|
void interrupts_init(void);
|
|
|
|
/* Called from the dispatch loop after each function returns.
|
|
* Fires vblank on schedule, checks (i_stat & i_mask), and if
|
|
* 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);
|
|
|
|
/* Snapshot internal counters for the freeze_check diagnostic. Any out_*
|
|
* pointer may be NULL. All counters are monotonically non-decreasing
|
|
* (dispatch_count resets each VBlank). */
|
|
void psx_get_freeze_diag(uint64_t *out_total_checks,
|
|
uint32_t *out_dispatch_count,
|
|
int *out_in_exception,
|
|
int *out_post_exc_cooldown,
|
|
uint64_t *out_exc_entries,
|
|
uint64_t *out_exc_reentry_blocks);
|
|
|
|
/* longjmp back to psx_check_interrupts, unwinding the exception handler.
|
|
* Called by ReturnFromException (B0:0x17 or SYSCALL(3)) when inside
|
|
* the exception handler to model real hardware's RFE+JR $k0 unwind. */
|
|
void psx_exception_longjmp(void);
|
|
|
|
/* longjmp back to psx_check_interrupts for RestoreState redirection.
|
|
*
|
|
* On real hardware, RestoreState (A0:0x14) restores all GPRs from a
|
|
* save buffer and does `jr $ra`, which jumps to the restored $ra —
|
|
* abandoning the current call stack. In our model, a plain `return;`
|
|
* would go back to the C caller (the chain walker), not to $ra.
|
|
*
|
|
* This function longjmps with code 2 (vs. 1 for ReturnFromException),
|
|
* so psx_check_interrupts can re-dispatch to cpu->pc while still in
|
|
* exception context. The redirected code (e.g., VSync callback loop)
|
|
* will eventually call ReturnFromException (longjmp code 1) to exit. */
|
|
void psx_restore_state_escape(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSXRECOMP_V4_INTERRUPTS_H */
|