From 0e23b0beddd5e40cc0c90f87cf03543dfcd2804a Mon Sep 17 00:00:00 2001 From: jdgleaver Date: Thu, 12 May 2022 14:43:52 +0100 Subject: [PATCH] (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 --- shell/libretro/audiostream.cpp | 92 ++++++++++++++++++++++++++ shell/libretro/libretro.cpp | 76 +++++++++++++++++++-- shell/libretro/libretro_core_options.h | 14 ++++ 3 files changed, 177 insertions(+), 5 deletions(-) diff --git a/shell/libretro/audiostream.cpp b/shell/libretro/audiostream.cpp index e6892512d..b27d7e495 100644 --- a/shell/libretro/audiostream.cpp +++ b/shell/libretro/audiostream.cpp @@ -24,8 +24,33 @@ #include #include +/* 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 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) { diff --git a/shell/libretro/libretro.cpp b/shell/libretro/libretro.cpp index bc7c152f2..375efddfc 100644 --- a/shell/libretro/libretro.cpp +++ b/shell/libretro/libretro.cpp @@ -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); + } } } diff --git a/shell/libretro/libretro_core_options.h b/shell/libretro/libretro_core_options.h index 83975e7b4..455e105cd 100644 --- a/shell/libretro/libretro_core_options.h +++ b/shell/libretro/libretro_core_options.h @@ -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",