mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
a91884a4bb
The GL renderer may skip SDL_GL_SwapWindow when the displayed rect is unchanged, so a double-buffered 30 Hz game does not swap an identical front buffer on the intervening vblank. The frontend calls present once per guest vblank, so eliding a swap also elides whatever that swap was blocking on. That is only safe while the wall-clock pacer owns cadence, because the pacer runs per present regardless of what the renderer does. When driver vsync owns cadence instead (~60 Hz panel, present_vsync_owns_cadence()), the pacer is deliberately not run -- the two are XOR to avoid double-blocking -- and the swap block becomes the only throttle. An elided frame then blocks on nothing, and a game presenting every other vblank advances two guest vblanks per block: exactly 2x real time. Measured on BreathOfFire3Recomp (SLPS-00990, 60 Hz panel, OpenGL): guest vblank counter sampled against wall clock ran 120.4 fps / 2.01x before, 59.8 fps / 0.997x after. Swaps per second are unchanged at 60 either way -- the fix removes a free-running guest frame, not a swap -- and per-frame GPU cost is unchanged (present_gpu_ms_avg 12.2 -> 12.6, scene_gpu_ms_avg 4.5 -> 4.1). Gate both skip sites (VRAM and wide-FBO paths) on the cadence owner, alongside the existing OSD and interpolation-cadence guards. Only the GL renderer has this optimisation; the software and Vulkan paths always present and were never affected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.6 KiB
C
63 lines
2.6 KiB
C
/*
|
|
* frame_pacing.h — race-free wall-clock frame pacing.
|
|
*
|
|
* Replaces the open-coded pacing loops in main.cpp (sdl_vblank_present)
|
|
* and beetle_main.cpp. The open-coded version read the performance
|
|
* counter twice per iteration (once in the while-condition, once to
|
|
* compute the remaining time); if the deadline was crossed between the
|
|
* two reads — a preemption-sized window — the unsigned subtraction
|
|
* underflowed and SDL_Delay received a ~24.7-day argument (2^64 ticks
|
|
* * 1000 / 10 MHz, truncated to Uint32). That was the Bug B hard
|
|
* freeze: the app parks in one SDL_Delay with every counter frozen,
|
|
* wedged between `i_stat |= VBLANK` and exception delivery.
|
|
*
|
|
* Rules the implementation must keep:
|
|
* - exactly ONE counter read per decision; compare before subtract
|
|
* - the computed sleep is clamped to one frame period
|
|
* - a deadline already in the past never sleeps
|
|
*/
|
|
#ifndef FRAME_PACING_H
|
|
#define FRAME_PACING_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
typedef struct FramePacer {
|
|
uint64_t next_deadline; /* perf-counter ticks; 0 = not started */
|
|
} FramePacer;
|
|
|
|
/* Pure decision function (unit-testable, no SDL).
|
|
* Given a single counter sample `now`, the current deadline, the counter
|
|
* frequency and the frame period (both in ticks), returns the number of
|
|
* milliseconds to sleep right now, or 0 if the caller should stop
|
|
* sleeping (deadline reached / close enough to spin).
|
|
* Never returns a value larger than one period's worth of ms + 1. */
|
|
uint32_t frame_pacing_sleep_ms(uint64_t now, uint64_t deadline,
|
|
uint64_t freq, uint64_t period);
|
|
|
|
/* Block until the pacer's next deadline (sleep + final sub-ms spin),
|
|
* then advance the deadline by one period. If the pacer has not started,
|
|
* or the caller fell beyond the implementation's bounded transient-catch-up
|
|
* window, re-anchor to now + period without sleeping. period_ms is the frame
|
|
* period in milliseconds (e.g. 1000.0 / 59.94). */
|
|
void frame_pacer_wait(FramePacer *p, double period_ms);
|
|
|
|
/* 1 while the display driver's swap block — not the pacer above — is what holds
|
|
* the guest to its vblank cadence (present_vsync_owns_cadence() in main.cpp).
|
|
* The two are XOR: when this returns 1 the frontend does not call
|
|
* frame_pacer_wait(), so the ONLY thing throttling the guest is the swap.
|
|
*
|
|
* A renderer that elides a redundant swap (unchanged front buffer) must
|
|
* therefore not do so while this is 1: the elided frame blocks on nothing, and
|
|
* a game that presents every other vblank runs at 2x. */
|
|
int psx_present_vsync_owns_cadence(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* FRAME_PACING_H */
|