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.
This commit is contained in:
Connor McLaughlin 2021-01-28 20:20:15 +10:00
parent 4e583890ea
commit 4bb3fb48f9
14 changed files with 111 additions and 42 deletions

View File

@ -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();

View File

@ -200,4 +200,6 @@
<string name="settings_summary_game_directories">Change the list of directories used to search for games.</string>
<string name="game_directories_scanning_subdirectories">Scanning subdirectories.</string>
<string name="game_directories_not_scanning_subdirectories">Not scanning subdirectories.</string>
<string name="settings_display_all_frames">Display All Frames</string>
<string name="settings_summary_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.</string>
</resources>

View File

@ -94,6 +94,12 @@
app:defaultValue="MMap"
app:summary="@string/settings_summary_cpu_recompiler_fastmem"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="Display/DisplayAllFrames"
app:title="@string/settings_display_all_frames"
app:defaultValue="false"
app:summary="@string/settings_summary_display_all_frames"
app:iconSpaceReserved="false" />
<ListPreference
app:key="Display/MaxFPS"
app:title="@string/settings_presented_frame_limit"

View File

@ -545,6 +545,7 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si)
si.SetBoolValue("Display", "ShowResolution", false);
si.SetBoolValue("Display", "Fullscreen", false);
si.SetBoolValue("Display", "VSync", true);
si.SetBoolValue("Display", "DisplayAllFrames", false);
si.SetStringValue("Display", "PostProcessChain", "");
si.SetFloatValue("Display", "MaxFPS", 0.0f);

View File

@ -196,6 +196,7 @@ void Settings::Load(SettingsInterface& si)
display_show_vps = si.GetBoolValue("Display", "ShowVPS", false);
display_show_speed = si.GetBoolValue("Display", "ShowSpeed", false);
display_show_resolution = si.GetBoolValue("Display", "ShowResolution", false);
display_all_frames = si.GetBoolValue("Display", "DisplayAllFrames", false);
video_sync_enabled = si.GetBoolValue("Display", "VSync", true);
display_post_process_chain = si.GetStringValue("Display", "PostProcessChain", "");
display_max_fps = si.GetFloatValue("Display", "MaxFPS", 0.0f);
@ -353,6 +354,7 @@ void Settings::Save(SettingsInterface& si) const
si.SetBoolValue("Display", "ShowVPS", display_show_vps);
si.SetBoolValue("Display", "ShowSpeed", display_show_speed);
si.SetBoolValue("Display", "ShowResolution", display_show_speed);
si.SetBoolValue("Display", "DisplayAllFrames", display_all_frames);
si.SetBoolValue("Display", "VSync", video_sync_enabled);
if (display_post_process_chain.empty())
si.DeleteValue("Display", "PostProcessChain");

View File

@ -138,6 +138,7 @@ struct Settings
bool display_show_vps = false;
bool display_show_speed = false;
bool display_show_resolution = false;
bool display_all_frames = false;
bool video_sync_enabled = true;
float display_max_fps = 0.0f;
float gpu_pgxp_tolerance = -1.0f;

View File

@ -37,6 +37,7 @@
#include <deque>
#include <fstream>
#include <limits>
#include <thread>
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<s32>(1000000000.0 / static_cast<double>(s_throttle_frequency) / static_cast<double>(s_target_speed));
if (s_target_speed > std::numeric_limits<double>::epsilon())
{
const double target_speed = std::max(static_cast<double>(s_target_speed), std::numeric_limits<double>::epsilon());
s_frame_period =
Common::Timer::ConvertSecondsToValue(1.0 / (static_cast<double>(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<u64>(s_throttle_timer.GetTimeNanoseconds());
const s64 sleep_time = static_cast<s64>(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<double>(-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()

View File

@ -153,6 +153,7 @@ bool RecreateGPU(GPURenderer renderer, bool update_display = true);
void SingleStepCPU();
void RunFrame();
void RunFrames();
/// Sets target emulation speed.
float GetTargetSpeed();

View File

@ -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 "

View File

@ -85,6 +85,13 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="displayAllFrames">
<property name="text">
<string>Display All Frames</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
@ -125,7 +132,7 @@
</item>
<item row="2" column="1">
<widget class="QComboBox" name="gpuDownsampleMode"/>
</item>
</item>
<item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">

View File

@ -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)
{

View File

@ -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)
{

View File

@ -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)
{

View File

@ -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();