From 4bb3fb48f95daedc9025c056ecb2ee4d89c0aafa Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Thu, 28 Jan 2021 20:20:15 +1000 Subject: [PATCH] System: Add a new throttler/pacer which can catch up on lost time This can result in worse frame pacing, so if you have a decent machine you'll probably want to turn on "display all frames" in display settings. But, it's sadly needed for Android. --- .../app/src/cpp/android_host_interface.cpp | 5 +- android/app/src/main/res/values/strings.xml | 2 + .../src/main/res/xml/advanced_preferences.xml | 6 ++ src/core/host_interface.cpp | 1 + src/core/settings.cpp | 2 + src/core/settings.h | 1 + src/core/system.cpp | 89 ++++++++++++------- src/core/system.h | 1 + src/duckstation-qt/displaysettingswidget.cpp | 8 +- src/duckstation-qt/displaysettingswidget.ui | 9 +- src/duckstation-qt/qthostinterface.cpp | 6 +- src/duckstation-sdl/sdl_host_interface.cpp | 6 +- src/frontend-common/common_host_interface.cpp | 16 +++- src/frontend-common/common_host_interface.h | 1 + 14 files changed, 111 insertions(+), 42 deletions(-) diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index ce6a80f44..5a96a4a47 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -452,7 +452,10 @@ void AndroidHostInterface::EmulationThreadLoop(JNIEnv* env) // simulate the system if not paused if (System::IsRunning()) { - System::RunFrame(); + if (m_throttler_enabled) + System::RunFrames(); + else + System::RunFrame(); if (m_vibration_enabled) UpdateVibration(); diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index ea00af480..9a0f007f7 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -200,4 +200,6 @@ Change the list of directories used to search for games. Scanning subdirectories. Not scanning subdirectories. + Display All Frames + Enable this option will ensure every frame the console renders is displayed to the screen, for optimal frame pacing. If you are having difficulties maintaining full speed, or are getting audio glitches, try disabling this option. diff --git a/android/app/src/main/res/xml/advanced_preferences.xml b/android/app/src/main/res/xml/advanced_preferences.xml index 83dab7b56..3dd3acea7 100644 --- a/android/app/src/main/res/xml/advanced_preferences.xml +++ b/android/app/src/main/res/xml/advanced_preferences.xml @@ -94,6 +94,12 @@ app:defaultValue="MMap" app:summary="@string/settings_summary_cpu_recompiler_fastmem" app:iconSpaceReserved="false" /> + #include #include +#include Log_SetChannel(System); SystemBootParameters::SystemBootParameters() = default; @@ -95,10 +96,8 @@ static std::string s_running_game_title; static float s_throttle_frequency = 60.0f; static float s_target_speed = 1.0f; -static s32 s_throttle_period = 0; -static u64 s_last_throttle_time = 0; -static Common::Timer s_throttle_timer; -static Common::Timer s_speed_lost_time_timestamp; +static Common::Timer::Value s_frame_period = 0; +static Common::Timer::Value s_next_frame_time = 0; static float s_average_frame_time_accumulator = 0.0f; static float s_worst_frame_time_accumulator = 0.0f; @@ -781,10 +780,8 @@ bool Initialize(bool force_software_renderer) s_internal_frame_number = 1; s_throttle_frequency = 60.0f; - s_throttle_period = 0; - s_last_throttle_time = 0; - s_throttle_timer.Reset(); - s_speed_lost_time_timestamp.Reset(); + s_frame_period = 0; + s_next_frame_time = 0; s_average_frame_time_accumulator = 0.0f; s_worst_frame_time_accumulator = 0.0f; @@ -1316,6 +1313,8 @@ void RunFrame() DoRunFrame(); + s_next_frame_time += s_frame_period; + if (s_memory_saves_enabled) DoMemorySaveStates(); } @@ -1339,15 +1338,23 @@ void SetThrottleFrequency(float frequency) void UpdateThrottlePeriod() { - s_throttle_period = - static_cast(1000000000.0 / static_cast(s_throttle_frequency) / static_cast(s_target_speed)); + if (s_target_speed > std::numeric_limits::epsilon()) + { + const double target_speed = std::max(static_cast(s_target_speed), std::numeric_limits::epsilon()); + s_frame_period = + Common::Timer::ConvertSecondsToValue(1.0 / (static_cast(s_throttle_frequency) * target_speed)); + } + else + { + s_frame_period = 1; + } + ResetThrottler(); } void ResetThrottler() { - s_last_throttle_time = 0; - s_throttle_timer.Reset(); + s_next_frame_time = Common::Timer::GetValue(); } void Throttle() @@ -1355,44 +1362,60 @@ void Throttle() // Reset the throttler on audio buffer overflow, so we don't end up out of phase. if (g_host_interface->GetAudioStream()->DidUnderflow() && s_target_speed >= 1.0f) { - Log_DevPrintf("Audio buffer underflowed, resetting throttler"); + Log_VerbosePrintf("Audio buffer underflowed, resetting throttler"); ResetThrottler(); return; } // Allow variance of up to 40ms either way. - constexpr s64 MAX_VARIANCE_TIME = INT64_C(40000000); +#ifndef __ANDROID__ + static constexpr double MAX_VARIANCE_TIME_NS = 40 * 1000000; +#else + static constexpr double MAX_VARIANCE_TIME_NS = 50 * 1000000; +#endif // Don't sleep for <1ms or >=period. - constexpr s64 MINIMUM_SLEEP_TIME = INT64_C(1000000); + static constexpr double MINIMUM_SLEEP_TIME_NS = 1 * 1000000; // Use unsigned for defined overflow/wrap-around. - const u64 time = static_cast(s_throttle_timer.GetTimeNanoseconds()); - const s64 sleep_time = static_cast(s_last_throttle_time - time); - if (sleep_time < -MAX_VARIANCE_TIME) + const Common::Timer::Value time = Common::Timer::GetValue(); + const double sleep_time = (s_next_frame_time >= time) ? + Common::Timer::ConvertValueToNanoseconds(s_next_frame_time - time) : + -Common::Timer::ConvertValueToNanoseconds(time - s_next_frame_time); + if (sleep_time < -MAX_VARIANCE_TIME_NS) { -#ifndef _DEBUG // Don't display the slow messages in debug, it'll always be slow... - // Limit how often the messages are displayed. - if (s_speed_lost_time_timestamp.GetTimeSeconds() >= 1.0f) - { - Log_WarningPrintf("System too slow, lost %.2f ms", - static_cast(-sleep_time - MAX_VARIANCE_TIME) / 1000000.0); - s_speed_lost_time_timestamp.Reset(); - } +#ifndef _DEBUG + Log_VerbosePrintf("System too slow, lost %.2f ms", (-sleep_time - MAX_VARIANCE_TIME_NS) / 1000000.0); #endif ResetThrottler(); } - else if (sleep_time >= MINIMUM_SLEEP_TIME) + else { -#ifdef __ANDROID__ - Common::Timer::HybridSleep(sleep_time); -#else - Common::Timer::NanoSleep(sleep_time); -#endif + Common::Timer::SleepUntil(s_next_frame_time, true); + } +} + +void RunFrames() +{ + // If we're running more than this in a single loop... we're in for a bad time. + const u32 max_frames_to_run = 2; + u32 frames_run = 0; + + Common::Timer::Value value = Common::Timer::GetValue(); + while (frames_run < max_frames_to_run) + { + if (value < s_next_frame_time) + break; + + RunFrame(); + frames_run++; + + value = Common::Timer::GetValue(); } - s_last_throttle_time += s_throttle_period; + if (frames_run != 1) + Log_VerbosePrintf("Ran %u frames in a single host frame", frames_run); } void UpdatePerformanceCounters() diff --git a/src/core/system.h b/src/core/system.h index c8a36f295..f9e58ef62 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -153,6 +153,7 @@ bool RecreateGPU(GPURenderer renderer, bool update_display = true); void SingleStepCPU(); void RunFrame(); +void RunFrames(); /// Sets target emulation speed. float GetTargetSpeed(); diff --git a/src/duckstation-qt/displaysettingswidget.cpp b/src/duckstation-qt/displaysettingswidget.cpp index f81d92d64..c3d24c1fd 100644 --- a/src/duckstation-qt/displaysettingswidget.cpp +++ b/src/duckstation-qt/displaysettingswidget.cpp @@ -36,6 +36,8 @@ DisplaySettingsWidget::DisplaySettingsWidget(QtHostInterface* host_interface, QW SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.displayIntegerScaling, "Display", "IntegerScaling"); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.vsync, "Display", "VSync"); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.displayAllFrames, "Display", "DisplayAllFrames", + false); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.gpuThread, "GPU", "UseThread", true); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.threadedPresentation, "GPU", "ThreadedPresentation", true); @@ -96,6 +98,10 @@ DisplaySettingsWidget::DisplaySettingsWidget(QtHostInterface* host_interface, QW m_ui.vsync, tr("VSync"), tr("Checked"), tr("Enable this option to match DuckStation's refresh rate with your current monitor or screen. " "VSync is automatically disabled when it is not possible (e.g. running at non-100% speed).")); + dialog->registerWidgetHelp(m_ui.displayAllFrames, tr("Display All Frames"), tr("Unchecked"), + tr("Enable this option will ensure every frame the console renders is displayed to the " + "screen, for optimal frame pacing. If you are having difficulties maintaining full " + "speed, or are getting audio glitches, try disabling this option.")); dialog->registerWidgetHelp(m_ui.threadedPresentation, tr("Threaded Presentation"), tr("Checked"), tr("Presents frames on a background thread when fast forwarding or vsync is disabled. " "This can measurably improve performance in the Vulkan renderer.")); @@ -120,7 +126,7 @@ DisplaySettingsWidget::DisplaySettingsWidget(QtHostInterface* host_interface, QW { QCheckBox* cb = new QCheckBox(tr("Use Blit Swap Chain"), m_ui.basicGroupBox); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, cb, "Display", "UseBlitSwapChain", false); - m_ui.basicCheckboxGridLayout->addWidget(cb, 1, 1, 1, 1); + m_ui.basicCheckboxGridLayout->addWidget(cb, 2, 0, 1, 1); dialog->registerWidgetHelp(cb, tr("Use Blit Swap Chain"), tr("Unchecked"), tr("Uses a blit presentation model instead of flipping when using the Direct3D 11 " "renderer. This usually results in slower performance, but may be required for some " diff --git a/src/duckstation-qt/displaysettingswidget.ui b/src/duckstation-qt/displaysettingswidget.ui index e8c4a6b00..196dd8a14 100644 --- a/src/duckstation-qt/displaysettingswidget.ui +++ b/src/duckstation-qt/displaysettingswidget.ui @@ -85,6 +85,13 @@ + + + + Display All Frames + + + @@ -125,7 +132,7 @@ - + diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index 927c4ab96..19194706a 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -1415,7 +1415,11 @@ void QtHostInterface::threadEntryPoint() continue; } - System::RunFrame(); + if (m_display_all_frames) + System::RunFrame(); + else + System::RunFrames(); + UpdateControllerRumble(); if (m_frame_step_request) { diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp index e70c1aea6..046b47370 100644 --- a/src/duckstation-sdl/sdl_host_interface.cpp +++ b/src/duckstation-sdl/sdl_host_interface.cpp @@ -1856,7 +1856,11 @@ void SDLHostInterface::Run() if (System::IsRunning()) { - System::RunFrame(); + if (m_display_all_frames) + System::RunFrame(); + else + System::RunFrames(); + UpdateControllerRumble(); if (m_frame_step_request) { diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index f9df41675..59190aa94 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -614,10 +614,11 @@ void CommonHostInterface::UpdateSpeedLimiterState() g_settings.turbo_speed : (m_fast_forward_enabled ? g_settings.fast_forward_speed : g_settings.emulation_speed); m_throttler_enabled = (target_speed != 0.0f); + m_display_all_frames = !m_throttler_enabled || g_settings.display_all_frames; bool syncing_to_host = false; - if (g_settings.sync_to_host_refresh_rate && g_settings.audio_resampling && target_speed == 1.0f && - g_settings.video_sync_enabled && m_display && System::IsRunning()) + if (g_settings.sync_to_host_refresh_rate && g_settings.audio_resampling && target_speed == 1.0f && m_display && + System::IsRunning()) { float host_refresh_rate; if (m_display->GetHostRefreshRate(&host_refresh_rate)) @@ -640,7 +641,8 @@ void CommonHostInterface::UpdateSpeedLimiterState() Log_InfoPrintf("Target speed: %f%%", target_speed * 100.0f); Log_InfoPrintf("Syncing to %s%s", audio_sync_enabled ? "audio" : "", (audio_sync_enabled && video_sync_enabled) ? " and video" : (video_sync_enabled ? "video" : "")); - Log_InfoPrintf("Max display fps: %f", max_display_fps); + Log_InfoPrintf("Max display fps: %f (%s)", max_display_fps, + m_display_all_frames ? "displaying all frames" : "skipping displaying frames when needed"); if (System::IsValid()) { @@ -656,6 +658,7 @@ void CommonHostInterface::UpdateSpeedLimiterState() Log_InfoPrintf("Audio input sample rate: %u hz", input_sample_rate); m_audio_stream->SetInputSampleRate(input_sample_rate); + m_audio_stream->SetWaitForBufferFill(!m_display_all_frames); m_audio_stream->SetOutputVolume(GetAudioOutputVolume()); m_audio_stream->SetSync(audio_sync_enabled); if (audio_sync_enabled) @@ -671,8 +674,12 @@ void CommonHostInterface::UpdateSpeedLimiterState() if (g_settings.increase_timer_resolution) SetTimerResolutionIncreased(m_throttler_enabled); - if (syncing_to_host) + // When syncing to host and using vsync, we don't need to sleep. + if (syncing_to_host && video_sync_enabled && m_display_all_frames) + { + Log_InfoPrintf("Using host vsync for throttling."); m_throttler_enabled = false; + } } void CommonHostInterface::RecreateSystem() @@ -2305,6 +2312,7 @@ void CommonHostInterface::CheckForSettingsChanges(const Settings& old_settings) g_settings.emulation_speed != old_settings.emulation_speed || g_settings.fast_forward_speed != old_settings.fast_forward_speed || g_settings.display_max_fps != old_settings.display_max_fps || + g_settings.display_all_frames != old_settings.display_all_frames || g_settings.audio_resampling != old_settings.audio_resampling || g_settings.sync_to_host_refresh_rate != old_settings.sync_to_host_refresh_rate) { diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index e76c0a60d..2ebdf9d26 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -355,6 +355,7 @@ protected: bool m_turbo_enabled = false; bool m_timer_resolution_increased = false; bool m_throttler_enabled = true; + bool m_display_all_frames = true; private: void InitializeUserDirectory();