flycast icon indicating copy to clipboard operation
flycast copied to clipboard

[libretro] Core breaches frontend timing (i.e. retro_system_av_info:timing.fps is not observed)

Open jdgleaver opened this issue 3 years ago • 0 comments

This is a general libretro core issue identified while investigating audio sample pacing. I don't know if it's possible to fix (an almost identical problem was recently found in the PPSSPP core, with no general solution...) but I'm reporting it for the sake of record keeping :)

The issue is this:

  • In retro_get_system_av_info() a core specifies sample_rate and fps. This represents a contract with the frontend:
    • The frontend will call retro_run() exactly fps times per second (discounting fast forward/rewind/etc.)
    • The frontend will expect from the core exactly (sample_rate / fps) audio samples per call of retro_run()
  • When flycast iterates content that runs natively at the set fps, this contract is broadly observed: one call of retro_run() seems to emulate one frame's worth of runtime, and audio samples (batched in chunks of 512) average out to ~(sample_rate / fps)
  • However, when flycast iterates content that doesn't run at the set fps (or when internal frame skipping is enabled), the contract is breached. For example:
    • If fps is set to 60 and the content runs 'natively' at 30 fps, then one call of retro_run() seems to emulate two frames' worth of runtime, and double the expected number of audio samples are sent to the frontend.
    • The core is therefore running effectively at double speed - but by over-saturating the audio buffer, the frontend is forced to run in 'slow motion'
    • The two effects kinda-sorta cancel out, such that frames are presented to the user at approximately the expected rate...
    • ...but this behaviour is out of spec/illegal, and completely bamboozles the frontend timing - such that proper frame pacing is impossible

There are essentially two possible fixes:

  1. The core is modified so each call of dc_run() (or the threaded equivalent) in retro_run() only emulates (1 / fps) seconds' worth of clock cycles, regardless of the native content frame rate
  2. The core detects the current effective frame rate of the running content, and updates the fps value reported to the frontend via RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO where appropriate - so dc_run() gets called by the frontend at the proper expected rate

(1) would allow for better frame pacing overall (running at less than the user's display refresh rate is never ideal) but I imagine that neither solution is easy (or even practical)...

jdgleaver avatar Dec 08 '21 14:12 jdgleaver