diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index 189e6dab8b..03e119a14b 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -34,6 +34,8 @@ #include "Core/Host.h" #include "Core/MemMap.h" #include "Core/System.h" +#include "Core/CoreTiming.h" +#include "Core/HW/Display.h" #include "GPU/GPUState.h" #include "GPU/GPUInterface.h" @@ -64,6 +66,20 @@ // frames, or 3 seconds of runtime... #define AUDIO_FRAMES_MOVING_AVG_ALPHA (1.0f / 180.0f) +// Calculated swap interval is 'stable' if the same +// value is recorded for a number of retro_run() +// calls equal to 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 +// Swap interval detection is only enabled if the +// core is running at 'normal' speed - i.e. if +// run speed is within VSYNC_SWAP_INTERVAL_RUN_SPEED_THRESHOLD +// percent of 100 +#define VSYNC_SWAP_INTERVAL_RUN_SPEED_THRESHOLD 5.0f + static bool libretro_supports_bitmasks = false; static std::string changeProAdhocServer; @@ -76,6 +92,139 @@ namespace Libretro static retro_input_state_t input_state_cb; } // namespace Libretro +namespace Libretro +{ + static bool detectVsyncSwapInterval = false; + static bool detectVsyncSwapIntervalOptShown = true; + + static s64 expectedTimeUsPerRun = 0; + static uint32_t vsyncSwapInterval = 1; + static uint32_t vsyncSwapIntervalLast = 1; + static uint32_t vsyncSwapIntervalCounter = 0; + static int numVBlanksLast = 0; + static double fpsTimeLast = 0.0; + static float runSpeed = 0.0f; + static s64 runTicksLast = 0; + + static void VsyncSwapIntervalReset() + { + expectedTimeUsPerRun = (s64)(1000000.0f / (60.0f / 1.001f)); + vsyncSwapInterval = 1; + vsyncSwapIntervalLast = 1; + vsyncSwapIntervalCounter = 0; + + numVBlanksLast = 0; + fpsTimeLast = 0.0; + runSpeed = 0.0f; + runTicksLast = 0; + + detectVsyncSwapIntervalOptShown = true; + } + + static void VsyncSwapIntervalDetect() + { + if (!detectVsyncSwapInterval) + return; + + // All bets are off if core is running at + // the 'wrong' speed (i.e. cycle count for + // this run will be meaningless if internal + // frame rate is dropping below expected + // value, or fast forward is enabled) + double fpsTime = time_now_d(); + int numVBlanks = __DisplayGetNumVblanks(); + int frames = numVBlanks - numVBlanksLast; + + if (frames >= VSYNC_SWAP_INTERVAL_FRAMES << 1) + { + double fps = (double)frames / (fpsTime - fpsTimeLast); + runSpeed = fps / ((60.0f / 1.001f) / 100.0f); + + fpsTimeLast = fpsTime; + numVBlanksLast = numVBlanks; + } + + float speedDelta = 100.0f - runSpeed; + speedDelta = (speedDelta < 0.0f) ? -speedDelta : speedDelta; + + // Speed is measured relative to a 60 Hz refresh + // rate. If we are transitioning from a low internal + // frame rate to a higher internal frame rate, then + // 'full speed' may actually equate to + // (100 / current_swap_interval)... + if ((vsyncSwapInterval > 1) && + (speedDelta >= VSYNC_SWAP_INTERVAL_RUN_SPEED_THRESHOLD)) + { + speedDelta = 100.0f - (runSpeed * (float)vsyncSwapInterval); + speedDelta = (speedDelta < 0.0f) ? -speedDelta : speedDelta; + } + + if (speedDelta >= VSYNC_SWAP_INTERVAL_RUN_SPEED_THRESHOLD) + { + // Swap interval detection is invalid - bail out + vsyncSwapIntervalCounter = 0; + return; + } + + // Get elapsed time (us) for this run + s64 runTicks = CoreTiming::GetTicks(); + s64 runTimeUs = cyclesToUs(runTicks - runTicksLast); + + // Check if current internal frame rate is a + // factor of the default ~60 Hz + float swapRatio = (float)runTimeUs / (float)expectedTimeUsPerRun; + uint32_t swapInteger; + float swapRemainder; + + // If internal frame rate is equal to (within threshold) + // or higher than the default ~60 Hz, fall back to a + // swap interval of 1 + if (swapRatio < (1.0f + VSYNC_SWAP_INTERVAL_THRESHOLD)) + { + swapInteger = 1; + swapRemainder = 0.0f; + } + else + { + swapInteger = (uint32_t)(swapRatio + 0.5f); + swapRemainder = swapRatio - (float)swapInteger; + swapRemainder = (swapRemainder < 0.0f) ? + -swapRemainder : swapRemainder; + } + + // > 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 ((swapRemainder <= VSYNC_SWAP_INTERVAL_THRESHOLD) && + (swapInteger != vsyncSwapInterval)) + { + vsyncSwapIntervalCounter = + (swapInteger == vsyncSwapIntervalLast) ? + (vsyncSwapIntervalCounter + 1) : 0; + + // Check whether swap interval is 'stable' + if (vsyncSwapIntervalCounter >= VSYNC_SWAP_INTERVAL_FRAMES) + { + vsyncSwapInterval = swapInteger; + vsyncSwapIntervalCounter = 0; + + // Notify frontend + retro_system_av_info avInfo; + retro_get_system_av_info(&avInfo); + environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avInfo); + } + + vsyncSwapIntervalLast = swapInteger; + } + else + vsyncSwapIntervalCounter = 0; + + runTicksLast = runTicks; + } +} // namespace Libretro + namespace Libretro { static std::mutex audioSampleLock_; @@ -168,14 +317,15 @@ namespace Libretro static void AudioUploadSamples() { - // - The core specifies a fixed frame rate of (60.0f / 1.001f) - // and a fixed sample rate of 44100 - // - This means the frontend expects exactly 735.735 - // sample frames per call of retro_run() - // - Provided that g_Config.bRenderDuplicateFrames is - // force enabled and frameskip is disabled, the mean - // of the buffer occupancy will approximate to this - // value in most cases + + // - If 'Detect Frame Rate Changes' is disabled, then + // the core specifies a fixed frame rate of (60.0f / 1.001f) + // - At the audio sample rate of 44100, this means the + // frontend expects exactly 735.735 sample frames per call of + // retro_run() + // - If g_Config.bRenderDuplicateFrames is enabled and + // frameskip is disabled, the mean of the buffer occupancy + // willapproximate to this value in most cases uint32_t framesAvailable = AudioBufferOccupancy(); if (framesAvailable > 0) @@ -405,6 +555,8 @@ static RetroOption ppsspp_gpu_hardware_transform("ppsspp_gpu_hardware_tran static RetroOption ppsspp_vertex_cache("ppsspp_vertex_cache", "Vertex Cache (Speedhack)", false); static RetroOption ppsspp_cheats("ppsspp_cheats", "Internal Cheats Support", false); static RetroOption ppsspp_io_timing_method("ppsspp_io_timing_method", "IO Timing Method", { { "Fast", IOTimingMethods::IOTIMING_FAST }, { "Host", IOTimingMethods::IOTIMING_HOST }, { "Simulate UMD delays", IOTimingMethods::IOTIMING_REALISTIC } }); +static RetroOption ppsspp_frame_duplication("ppsspp_frame_duplication", "Duplicate Frames in 30 Hz Games", false); +static RetroOption ppsspp_detect_vsync_swap_interval("ppsspp_detect_vsync_swap_interval", "Detect Frame Rate Changes (Notify Frontend)", false); static RetroOption ppsspp_software_skinning("ppsspp_software_skinning", "Software Skinning", true); static RetroOption ppsspp_ignore_bad_memory_access("ppsspp_ignore_bad_memory_access", "Ignore bad memory accesses", true); static RetroOption ppsspp_lazy_texture_caching("ppsspp_lazy_texture_caching", "Lazy texture caching (Speedup)", false); @@ -490,7 +642,27 @@ static bool set_variable_visibility(void) if (ppsspp_enable_upnp.Update(&g_Config.bEnableUPnP)) updated = true; - ppsspp_upnp_use_original_port.Show(g_Config.bEnableUPnP); + ppsspp_upnp_use_original_port.Show(g_Config.bEnableUPnP); + + bool detectVsyncSwapIntervalOptShownLast = detectVsyncSwapIntervalOptShown; + bool autoFrameSkip = false; + int frameSkip = 0; + bool renderDuplicateFrames = false; + + ppsspp_auto_frameskip.Update(&autoFrameSkip); + ppsspp_frameskip.Update(&frameSkip); + ppsspp_frame_duplication.Update(&renderDuplicateFrames); + + detectVsyncSwapIntervalOptShown = + !autoFrameSkip && + (frameSkip == 0) && + !renderDuplicateFrames; + + if (detectVsyncSwapIntervalOptShown != detectVsyncSwapIntervalOptShownLast) + { + ppsspp_detect_vsync_swap_interval.Show(detectVsyncSwapIntervalOptShown); + updated = true; + } return updated; } @@ -501,7 +673,7 @@ void retro_set_environment(retro_environment_t cb) struct retro_core_options_update_display_callback update_display_cb; update_display_cb.callback = set_variable_visibility; - cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK, &update_display_cb); + environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK, &update_display_cb); std::vector vars; vars.push_back(ppsspp_internal_resolution.GetOptions()); @@ -516,6 +688,8 @@ void retro_set_environment(retro_environment_t cb) vars.push_back(ppsspp_auto_frameskip.GetOptions()); vars.push_back(ppsspp_frameskip.GetOptions()); vars.push_back(ppsspp_frameskiptype.GetOptions()); + vars.push_back(ppsspp_frame_duplication.GetOptions()); + vars.push_back(ppsspp_detect_vsync_swap_interval.GetOptions()); vars.push_back(ppsspp_vertex_cache.GetOptions()); vars.push_back(ppsspp_fast_memory.GetOptions()); vars.push_back(ppsspp_block_transfer_gpu.GetOptions()); @@ -550,7 +724,7 @@ void retro_set_environment(retro_environment_t cb) vars.push_back(ppsspp_forced_first_connect.GetOptions()); vars.push_back({}); - cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void *)vars.data()); + environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void *)vars.data()); } static int get_language_auto(void) @@ -648,6 +822,8 @@ static void check_variables(CoreParameter &coreParam) ppsspp_cpu_core.Update((CPUCore *)&g_Config.iCpuCore); ppsspp_io_timing_method.Update((IOTimingMethods *)&g_Config.iIOTimingMethod); ppsspp_lower_resolution_for_effects.Update(&g_Config.iBloomHack); + ppsspp_frame_duplication.Update(&g_Config.bRenderDuplicateFrames); + ppsspp_detect_vsync_swap_interval.Update(&detectVsyncSwapInterval); ppsspp_software_skinning.Update(&g_Config.bSoftwareSkinning); ppsspp_ignore_bad_memory_access.Update(&g_Config.bIgnoreBadMemAccess); ppsspp_lazy_texture_caching.Update(&g_Config.bTextureBackoffCache); @@ -675,6 +851,21 @@ static void check_variables(CoreParameter &coreParam) g_Config.sLanguageIni = map_psp_language_to_i18n_locale(g_Config.iLanguage); i18nrepo.LoadIni(g_Config.sLanguageIni); + // Cannot detect refresh rate changes if: + // > Frame skipping is enabled + // > Frame duplication is enabled + detectVsyncSwapInterval &= + !g_Config.bAutoFrameSkip && + (g_Config.iFrameSkip == 0) && + !g_Config.bRenderDuplicateFrames; + + bool updateAvInfo = false; + if (!detectVsyncSwapInterval && (vsyncSwapInterval != 1)) + { + vsyncSwapInterval = 1; + updateAvInfo = true; + } + if (ppsspp_internal_resolution.Update(&g_Config.iInternalResolution) && !PSP_IsInited()) { coreParam.pixelWidth = coreParam.renderWidth = g_Config.iInternalResolution * 480; @@ -682,13 +873,21 @@ static void check_variables(CoreParameter &coreParam) if (gpu) { - retro_system_av_info av_info; - retro_get_system_av_info(&av_info); - environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &av_info); + retro_system_av_info avInfo; + retro_get_system_av_info(&avInfo); + environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avInfo); + updateAvInfo = false; gpu->Resized(); } } + if (updateAvInfo) + { + retro_system_av_info avInfo; + retro_get_system_av_info(&avInfo); + environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avInfo); + } + bool isFastForwarding = environ_cb(RETRO_ENVIRONMENT_GET_FASTFORWARDING, &isFastForwarding); coreParam.fastForward = isFastForwarding; @@ -736,6 +935,7 @@ void retro_set_input_state(retro_input_state_t cb) { input_state_cb = cb; } void retro_init(void) { + VsyncSwapIntervalReset(); AudioBufferInit(); g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count); @@ -779,7 +979,6 @@ void retro_init(void) g_Config.Load("", ""); g_Config.iInternalResolution = 0; - g_Config.bRenderDuplicateFrames = true; const char* nickname = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_USERNAME, &nickname) && nickname) @@ -822,6 +1021,7 @@ void retro_deinit(void) libretro_supports_bitmasks = false; + VsyncSwapIntervalReset(); AudioBufferDeinit(); } @@ -843,7 +1043,7 @@ void retro_get_system_info(struct retro_system_info *info) void retro_get_system_av_info(struct retro_system_av_info *info) { *info = {}; - info->timing.fps = 60.0f / 1.001f; + info->timing.fps = (60.0 / 1.001) / (double)vsyncSwapInterval; info->timing.sample_rate = SAMPLERATE; info->geometry.base_width = g_Config.iInternalResolution * 480; @@ -1103,6 +1303,7 @@ void retro_run(void) if( emuThreadState == EmuThreadState::PAUSED || emuThreadState == EmuThreadState::PAUSE_REQUESTED) { + VsyncSwapIntervalDetect(); AudioUploadSamples(); ctx->SwapBuffers(); return; @@ -1113,6 +1314,7 @@ void retro_run(void) if (!ctx->ThreadFrame()) { + VsyncSwapIntervalDetect(); AudioUploadSamples(); return; } @@ -1120,6 +1322,7 @@ void retro_run(void) else EmuFrame(); + VsyncSwapIntervalDetect(); AudioUploadSamples(); ctx->SwapBuffers(); } @@ -1247,7 +1450,11 @@ float System_GetPropertyFloat(SystemProperty prop) switch (prop) { case SYSPROP_DISPLAY_REFRESH_RATE: - return 60.f; + // Have to lie here and report 60 Hz instead + // of (60.0 / 1.001), otherwise the internal + // stereo resampler will output at the wrong + // frequency... + return 60.0f; case SYSPROP_DISPLAY_SAFE_INSET_LEFT: case SYSPROP_DISPLAY_SAFE_INSET_RIGHT: case SYSPROP_DISPLAY_SAFE_INSET_TOP: