(libretro) Add option to detect and notifiy frontend of internal frame rate changes (60 <-> 30 <-> 20 fps, etc.)

Improves frame pacing in games with locked 30 fps and 20 fps frame rates
This commit is contained in:
jdgleaver 2022-05-12 14:43:52 +01:00 committed by flyinghead
parent 903c768f7f
commit 0e23b0bedd
3 changed files with 177 additions and 5 deletions

View File

@ -24,8 +24,33 @@
#include <vector>
#include <mutex>
/* Detect output refresh rate changes by monitoring
* the last 'VSYNC_SWAP_INTERVAL_FRAMES' frames:
* - Measure average (mean) audio samples per upload
* operation
* - Determine vsync swap interval based on
* expected samples at 60 (or 50) Hz
* - Check that vsync swap interval remains
* 'stable' for at least 'VSYNC_SWAP_INTERVAL_FRAMES' */
#define VSYNC_SWAP_INTERVAL_FRAMES 6
/* Calculated swap interval is 'valid' if it is
* within 'VSYNC_SWAP_INTERVAL_THRESHOLD' of an integer
* value */
#define VSYNC_SWAP_INTERVAL_THRESHOLD 0.05f
extern void setAVInfo(retro_system_av_info& avinfo);
extern retro_environment_t environ_cb;
extern retro_audio_sample_batch_t audio_batch_cb;
extern float libretro_expected_audio_samples_per_run;
extern unsigned libretro_vsync_swap_interval;
extern bool libretro_detect_vsync_swap_interval;
static float audio_samples_per_frame_avg;
static unsigned vsync_swap_interval_last;
static unsigned vsync_swap_interval_conter;
static std::mutex audio_buffer_mutex;
static std::vector<int16_t> audio_buffer;
static size_t audio_buffer_idx;
@ -57,6 +82,10 @@ void retro_audio_init(void)
audio_out_buffer = (int16_t*)malloc(audio_buffer_size * sizeof(int16_t));
drop_samples = false;
audio_samples_per_frame_avg = 0.0f;
vsync_swap_interval_last = 1;
vsync_swap_interval_conter = 0;
}
void retro_audio_deinit(void)
@ -72,6 +101,10 @@ void retro_audio_deinit(void)
audio_out_buffer = nullptr;
drop_samples = true;
audio_samples_per_frame_avg = 0.0f;
vsync_swap_interval_last = 1;
vsync_swap_interval_conter = 0;
}
void retro_audio_flush_buffer(void)
@ -100,6 +133,65 @@ void retro_audio_upload(void)
audio_buffer_mutex.unlock();
/* Attempt to detect changes in output refresh rate */
if (libretro_detect_vsync_swap_interval &&
(num_frames > 0))
{
/* Simple running average (leaky-integrator) */
audio_samples_per_frame_avg = ((1.0f / (float)VSYNC_SWAP_INTERVAL_FRAMES) * (float)num_frames) +
((1.0f - (1.0f / (float)VSYNC_SWAP_INTERVAL_FRAMES)) * audio_samples_per_frame_avg);
float swap_ratio = audio_samples_per_frame_avg /
libretro_expected_audio_samples_per_run;
unsigned swap_integer;
float swap_remainder;
/* If internal frame rate is equal to (within threshold)
* or higher than the default 60 (or 50) Hz, fall back
* to a swap interval of 1 */
if (swap_ratio < (1.0f + VSYNC_SWAP_INTERVAL_THRESHOLD))
{
swap_integer = 1;
swap_remainder = 0.0f;
}
else
{
swap_integer = (unsigned)(swap_ratio + 0.5f);
swap_remainder = swap_ratio - (float)swap_integer;
swap_remainder = (swap_remainder < 0.0f) ?
-swap_remainder : swap_remainder;
}
/* > Swap interval is considered 'valid' if it is
* within VSYNC_SWAP_INTERVAL_THRESHOLD of an integer
* value
* > If valid, check if new swap interval differs from
* previously logged value */
if ((swap_remainder <= VSYNC_SWAP_INTERVAL_THRESHOLD) &&
(swap_integer != libretro_vsync_swap_interval))
{
vsync_swap_interval_conter =
(swap_integer == vsync_swap_interval_last) ?
(vsync_swap_interval_conter + 1) : 0;
/* Check whether swap interval is 'stable' */
if (vsync_swap_interval_conter >= VSYNC_SWAP_INTERVAL_FRAMES)
{
libretro_vsync_swap_interval = swap_integer;
vsync_swap_interval_conter = 0;
/* Notify frontend */
retro_system_av_info avinfo;
setAVInfo(avinfo);
environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avinfo);
}
vsync_swap_interval_last = swap_integer;
}
else
vsync_swap_interval_conter = 0;
}
int16_t *audio_out_buffer_ptr = audio_out_buffer;
while (num_frames > 0)
{

View File

@ -120,6 +120,7 @@ static bool platformIsDreamcast = true;
static bool platformIsArcade = false;
static bool threadedRenderingEnabled = true;
static bool oitEnabled = false;
static bool autoSkipFrameEnabled = false;
#ifndef TARGET_NO_OPENMP
static bool textureUpscaleEnabled = false;
#endif
@ -164,6 +165,10 @@ static int framebufferHeight;
static int maxFramebufferWidth;
static int maxFramebufferHeight;
float libretro_expected_audio_samples_per_run;
unsigned libretro_vsync_swap_interval = 1;
bool libretro_detect_vsync_swap_interval = false;
static retro_perf_callback perf_cb;
static retro_get_cpu_features_t perf_get_cpu_features_cb;
@ -172,8 +177,8 @@ static retro_log_printf_t log_cb;
static retro_video_refresh_t video_cb;
static retro_input_poll_t poll_cb;
static retro_input_state_t input_cb;
retro_audio_sample_batch_t audio_batch_cb;
static retro_environment_t environ_cb;
retro_audio_sample_batch_t audio_batch_cb;
retro_environment_t environ_cb;
static retro_rumble_interface rumble;
@ -357,11 +362,14 @@ void retro_deinit()
platformIsArcade = false;
threadedRenderingEnabled = true;
oitEnabled = false;
autoSkipFrameEnabled = false;
#ifndef TARGET_NO_OPENMP
textureUpscaleEnabled = false;
#endif
vmuScreenSettingsShown = true;
lightgunSettingsShown = true;
libretro_vsync_swap_interval = 1;
libretro_detect_vsync_swap_interval = false;
LogManager::Shutdown();
retro_audio_deinit();
@ -500,6 +508,24 @@ static bool set_variable_visibility(void)
}
#endif
// Only if automatic frame skipping is disabled
bool autoSkipFrameWasEnabled = autoSkipFrameEnabled;
autoSkipFrameEnabled = false;
var.key = CORE_OPTION_NAME "_auto_skip_frame";
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && strcmp(var.value, "disabled"))
autoSkipFrameEnabled = true;
if (first_run ||
(autoSkipFrameEnabled != autoSkipFrameWasEnabled) ||
(threadedRenderingEnabled != threadedRenderingWasEnabled))
{
option_display.visible = (!autoSkipFrameEnabled || !threadedRenderingEnabled);
option_display.key = CORE_OPTION_NAME "_detect_vsync_swap_interval";
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
updated = true;
}
// If categories are supported, no further action is required
if (categoriesSupported)
return updated;
@ -596,11 +622,16 @@ static void setGameGeometry(retro_game_geometry& geometry)
geometry.base_height = 480;
}
static void setAVInfo(retro_system_av_info& avinfo)
void setAVInfo(retro_system_av_info& avinfo)
{
double sample_rate = 44100.0;
double fps = SPG_CONTROL.NTSC ? 59.94 : SPG_CONTROL.PAL ? 50.0 : 60.0;
setGameGeometry(avinfo.geometry);
avinfo.timing.sample_rate = 44100.0;
avinfo.timing.fps = SPG_CONTROL.NTSC ? 59.94 : SPG_CONTROL.PAL ? 50.0 : 60.0;
avinfo.timing.sample_rate = sample_rate;
avinfo.timing.fps = fps / (double)libretro_vsync_swap_interval;
libretro_expected_audio_samples_per_run = sample_rate / fps;
}
static void setRotation()
@ -628,6 +659,7 @@ static void update_variables(bool first_startup)
int prevMaxFramebufferHeight = maxFramebufferHeight;
int prevMaxFramebufferWidth = maxFramebufferWidth;
bool prevRotateScreen = rotate_screen;
bool prevDetectVsyncSwapInterval = libretro_detect_vsync_swap_interval;
config::Settings::instance().setRetroEnvironment(environ_cb);
config::Settings::instance().setOptionDefinitions(option_defs_us);
config::Settings::instance().load(false);
@ -749,6 +781,22 @@ static void update_variables(bool first_startup)
config::PixelBufferSize = 0x20000000u;
#endif
if ((config::AutoSkipFrame != 0) && config::ThreadedRendering)
libretro_detect_vsync_swap_interval = false;
else
{
var.key = CORE_OPTION_NAME "_detect_vsync_swap_interval";
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
{
if (!strcmp(var.value, "enabled"))
libretro_detect_vsync_swap_interval = true;
else if (!strcmp(var.value, "disabled"))
libretro_detect_vsync_swap_interval = false;
}
else
libretro_detect_vsync_swap_interval = false;
}
if (first_startup)
{
if (config::ThreadedRendering)
@ -972,6 +1020,16 @@ static void update_variables(bool first_startup)
if (rotate_game)
config::Widescreen.override(false);
setFramebufferSize();
bool avInfoChanged = false;
if ((libretro_detect_vsync_swap_interval != prevDetectVsyncSwapInterval) &&
!libretro_detect_vsync_swap_interval &&
(libretro_vsync_swap_interval != 1))
{
libretro_vsync_swap_interval = 1;
avInfoChanged = true;
}
if ((prevMaxFramebufferWidth < maxFramebufferWidth || prevMaxFramebufferHeight < maxFramebufferHeight)
// TODO crash with dx11
&& config::RendererType != RenderType::DirectX11 && config::RendererType != RenderType::DirectX11_OIT)
@ -980,6 +1038,7 @@ static void update_variables(bool first_startup)
setAVInfo(avinfo);
environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avinfo);
rend_resize_renderer();
avInfoChanged = false;
}
else if (prevFramebufferWidth != framebufferWidth || prevFramebufferHeight != framebufferHeight || geometryChanged)
{
@ -988,6 +1047,13 @@ static void update_variables(bool first_startup)
environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, &geometry);
rend_resize_renderer();
}
if (avInfoChanged)
{
retro_system_av_info avinfo;
setAVInfo(avinfo);
environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avinfo);
}
}
}

View File

@ -473,6 +473,20 @@ struct retro_core_option_v2_definition option_defs_us[] = {
},
"disabled",
},
{
CORE_OPTION_NAME "_detect_vsync_swap_interval",
"Detect Frame Rate Changes",
NULL,
"Notify frontend when internal frame rate changes (e.g. from 60 fps to 30 fps). Improves frame pacing in games that run at a locked 30 fps or 20 fps, but should be disabled for games with unlocked (unstable) frame rates (e.g. Ecco the Dolphin, Unreal Tournament). Note: Unavailable when 'Auto Skip Frame' is enabled.",
NULL,
"video",
{
{ "disabled", NULL },
{ "enabled", NULL },
{ NULL, NULL },
},
"disabled",
},
{
CORE_OPTION_NAME "_pvr2_filtering",
"PowerVR2 Post-processing Filter",