retroarch: refresh the dummy core's advertised fps on a refresh rate change

Menu BGM (Menu Sounds -> Enable BGM Sound) garbles the moment the
refresh rate is changed from Settings -> Video -> Output -> Screen
Resolution, e.g. 1320x2868 (120 Hz) -> 1320x2868 (60 Hz), and stays
garbled.

With no content loaded the running core is the dummy core.  Its
libretro_dummy_retro_get_system_av_info() reports

   info->timing.fps         = <GET_TARGET_REFRESH_RATE>
   info->timing.sample_rate = 48000.0

so timing.fps is the display rate as it stood when the core was
initialised.  Nothing re-queries the core when the display rate
changes afterwards, so timing.fps is stale from that point on.

audio_driver_menu_sample() emits

   floor(timing.sample_rate / timing.fps) * 2

int16s per runloop iteration, and the runloop is driven at the real
display rate.  The menu audio feed is therefore scaled by
(real rate / stale fps).  At 120 Hz with fps 120 that is 400 frames
per iteration * 120 iterations = 48000 frames/s, which is correct.
After the switch the runloop drops to 60 iterations/s while fps is
still 120, giving 24000 frames/s - half.  audio_driver_flush()
advances the mixer by the post-resample output frame count via
audio_mixer_mix(), so the mixer starves at half rate and the BGM
garbles.

This is not Apple-specific.  Any platform whose display server
honours a refresh change from that dropdown hits it; it is simply
most audible at 2x, and iOS reaches it easily now that ProMotion
reports 120 Hz.

2089f43b8d routed the dropdown through
RARCH_DRIVER_CTL_SET_REFRESH_RATE so the resampler ratio and
driver_adjust_system_rates() would be re-run.  That was necessary but
not sufficient: driver_adjust_system_rates() computes audio_st->input
from input_fps = av_info.timing.fps, so feeding it a stale fps just
re-derives the same wrong answer.  The stale value has to be
corrected before it is consumed.

Update timing.fps to the new rate in the SET_REFRESH_RATE case, ahead
of driver_adjust_system_rates().  Doing it here rather than in
menu_cbs_ok.c covers every path that changes the refresh rate
(VIDEO_REFRESH_RATE, _AUTO, _POLLED and the resolution dropdown), all
of which already funnel through driver_ctl().

Gated on current_core_type == CORE_TYPE_DUMMY: a real core's fps
describes the emulated system, not the display, and must never be
overwritten.  Also gated on a positive rate so a 0 Hz answer from a
display server cannot poison av_info.  There is precedent for writing
timing.fps from this side - video_display_server_restore_refresh_rate()
already does exactly this when restoring the original mode.

Compile-tested on a Linux build (retroarch.c is platform-neutral);
the audible behaviour still wants an on-device check.
This commit is contained in:
libretroadmin authored and libretroadmin committed 2026-07-27 06:56:25 +02:00
1 parent e3150f2fb8
commit b17daaf0f6
1 file changed
+27
+27
View File
@@ -2072,6 +2072,33 @@ bool driver_ctl(enum driver_ctl_state state, void *data)
video_monitor_set_refresh_rate(*hz);
/* With no content loaded it is the dummy core that is
* running, and its retro_get_system_av_info() reports
* timing.fps as whatever GET_TARGET_REFRESH_RATE answered
* at core init - that is, the display rate. Nothing
* re-queries it when the display rate changes afterwards,
* so it goes stale the moment the user switches mode.
*
* That matters because audio_driver_menu_sample() emits
* timing.sample_rate / timing.fps frames per runloop
* iteration, while the runloop is driven at the *actual*
* display rate. A stale fps scales the menu audio feed by
* (real rate / stale fps): after a 120 Hz -> 60 Hz switch
* it is halved, so the mixer - which audio_driver_flush()
* advances by the post-resample output frame count -
* starves and menu BGM garbles.
*
* driver_adjust_system_rates() below cannot repair this,
* because it derives audio_st->input from this very
* timing.fps and so re-derives the same wrong answer.
*
* Only the dummy core gets this treatment: a real core's
* fps is a property of the emulated system, not of the
* display, and must never be overwritten here. */
if ( *hz > 0.0f
&& runloop_st->current_core_type == CORE_TYPE_DUMMY)
video_st->av_info.timing.fps = *hz;
/* Sets audio monitor rate to new value. */
audio_st->src_ratio_orig =
audio_st->src_ratio_curr =