diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml
index 01a9ae0c5..bcbead1f9 100644
--- a/android/app/src/main/res/values/arrays.xml
+++ b/android/app/src/main/res/values/arrays.xml
@@ -485,4 +485,16 @@
- Port2Only
- BothPorts
+
+ - Always Display All Frames
+ - Display All Frames When Possible
+ - Skip Duplicate Frames
+ - Clamp To Max FPS
+
+
+ - DisplayAllFrames
+ - DisplayAllFramesWhenPossible
+ - SkipDuplicateFrames
+ - ClampToMaxFPS
+
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 861f7bc3e..554f88b5b 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -276,4 +276,5 @@
Ports
Port %d
Port %1$d%2$c
+ Frame Pacing
diff --git a/android/app/src/main/res/xml/advanced_preferences.xml b/android/app/src/main/res/xml/advanced_preferences.xml
index 96b41425a..316a47a0f 100644
--- a/android/app/src/main/res/xml/advanced_preferences.xml
+++ b/android/app/src/main/res/xml/advanced_preferences.xml
@@ -56,11 +56,13 @@
app:summary="@string/settings_summary_general_sync_to_host_refresh_rate"
app:dependency="Display/VSync"
app:iconSpaceReserved="false" />
-
Y1;
BitField Y2;
};
- } regs;
+ };
+
+ bool in_hblank;
+ bool in_vblank;
+ bool display_changed;
+
+ Regs regs;
u16 dot_clock_divider;
@@ -523,9 +529,6 @@ protected:
TickCount fractional_dot_ticks; // only used when timer0 is enabled
- bool in_hblank;
- bool in_vblank;
-
u8 interlaced_field; // 0 = odd, 1 = even
u8 interlaced_display_field;
u8 active_line_lsb;
diff --git a/src/core/host_display.cpp b/src/core/host_display.cpp
index 141fcad5e..ae86918ff 100644
--- a/src/core/host_display.cpp
+++ b/src/core/host_display.cpp
@@ -5,6 +5,7 @@
#include "common/log.h"
#include "common/string_util.h"
#include "common/timer.h"
+#include "settings.h"
#include "host_interface.h"
#include "stb_image.h"
#include "stb_image_resize.h"
@@ -27,6 +28,9 @@ void HostDisplay::SetDisplayMaxFPS(float max_fps)
bool HostDisplay::ShouldSkipDisplayingFrame()
{
+ if (!m_display_duplicate_frames && !m_display_changed)
+ return true;
+
if (m_display_frame_interval == 0.0f)
return false;
diff --git a/src/core/host_display.h b/src/core/host_display.h
index 1c683eb74..a2c59d036 100644
--- a/src/core/host_display.h
+++ b/src/core/host_display.h
@@ -8,7 +8,7 @@
#include
#include
-enum class HostDisplayPixelFormat : u32
+enum class HostDisplayPixelFormat : u8
{
Unknown,
RGBA8,
@@ -46,7 +46,7 @@ public:
OpenGLES
};
- enum class Alignment
+ enum class Alignment : u8
{
LeftOrTop,
Center,
@@ -135,6 +135,7 @@ public:
const float GetDisplayAspectRatio() const { return m_display_aspect_ratio; }
void SetDisplayMaxFPS(float max_fps);
+ void SetDisplayDuplicateFrames(bool enabled) { m_display_duplicate_frames = enabled; }
bool ShouldSkipDisplayingFrame();
void ClearDisplayTexture()
@@ -267,7 +268,7 @@ protected:
float m_display_frame_interval = 0.0f;
void* m_display_texture_handle = nullptr;
- HostDisplayPixelFormat m_display_texture_format = HostDisplayPixelFormat::Count;
+ std::unique_ptr m_cursor_texture;
s32 m_display_texture_width = 0;
s32 m_display_texture_height = 0;
s32 m_display_texture_view_x = 0;
@@ -276,13 +277,14 @@ protected:
s32 m_display_texture_view_height = 0;
s32 m_display_top_margin = 0;
- Alignment m_display_alignment = Alignment::Center;
-
- std::unique_ptr m_cursor_texture;
float m_cursor_texture_scale = 1.0f;
- bool m_display_linear_filtering = false;
+ HostDisplayPixelFormat m_display_texture_format = HostDisplayPixelFormat::Count;
+ Alignment m_display_alignment = Alignment::Center;
+
bool m_display_changed = false;
+ bool m_display_linear_filtering = false;
bool m_display_integer_scaling = false;
bool m_display_stretch = false;
+ bool m_display_duplicate_frames = false;
};
diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp
index bbb65fe1f..8fb82afa4 100644
--- a/src/core/host_interface.cpp
+++ b/src/core/host_interface.cpp
@@ -539,6 +539,8 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si)
si.SetIntValue("Display", "LineEndOffset", 0);
si.SetStringValue("Display", "AspectRatio",
Settings::GetDisplayAspectRatioName(Settings::DEFAULT_DISPLAY_ASPECT_RATIO));
+ si.SetStringValue("Display", "FramePacingMode",
+ Settings::GetDisplayFramePacingModeName(Settings::DEFAULT_DISPLAY_FRAME_PACING_MODE));
si.SetBoolValue("Display", "Force4_3For24Bit", false);
si.SetBoolValue("Display", "LinearFiltering", true);
si.SetBoolValue("Display", "IntegerScaling", false);
@@ -551,7 +553,6 @@ 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);
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 061477cf9..e38287203 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -227,7 +227,11 @@ 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);
+ display_frame_pacing_mode =
+ ParseDisplayFramePacingMode(
+ si.GetStringValue("Display", "FramePacingMode", GetDisplayFramePacingModeName(DEFAULT_DISPLAY_FRAME_PACING_MODE))
+ .c_str())
+ .value_or(DEFAULT_DISPLAY_FRAME_PACING_MODE);
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);
@@ -387,6 +391,7 @@ void Settings::Save(SettingsInterface& si) const
si.SetIntValue("Display", "LineEndOffset", display_line_end_offset);
si.SetBoolValue("Display", "Force4_3For24Bit", display_force_4_3_for_24bit);
si.SetStringValue("Display", "AspectRatio", GetDisplayAspectRatioName(display_aspect_ratio));
+ si.SetStringValue("Display", "FramePacingMode", GetDisplayAspectRatioName(display_aspect_ratio));
si.SetBoolValue("Display", "LinearFiltering", display_linear_filtering);
si.SetBoolValue("Display", "IntegerScaling", display_integer_scaling);
si.SetBoolValue("Display", "Stretch", display_stretch);
@@ -396,7 +401,6 @@ 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_resolution);
- si.SetBoolValue("Display", "DisplayAllFrames", display_all_frames);
si.SetBoolValue("Display", "VSync", video_sync_enabled);
if (display_post_process_chain.empty())
si.DeleteValue("Display", "PostProcessChain");
@@ -778,6 +782,38 @@ float Settings::GetDisplayAspectRatioValue(DisplayAspectRatio ar)
return s_display_aspect_ratio_values[static_cast(ar)];
}
+static std::array s_display_frame_pacing_mode_names = {
+ {"DisplayAllFrames", "DisplayAllFramesWhenPossible", "SkipDuplicateFrames", "ClampToMaxFPS"}};
+static std::array s_display_frame_pacing_mode_display_names = {
+ {TRANSLATABLE("DisplayFramePacingMode", "Display All Frames"),
+ TRANSLATABLE("DisplayFramePacingMode", "Display All Frames When Possible"),
+ TRANSLATABLE("DisplayFramePacingMode", "Skip Duplicate Frames"),
+ TRANSLATABLE("DisplayFramePacingMode", "Clamp To Max FPS")}};
+
+std::optional Settings::ParseDisplayFramePacingMode(const char* str)
+{
+ int index = 0;
+ for (const char* name : s_display_frame_pacing_mode_names)
+ {
+ if (StringUtil::Strcasecmp(name, str) == 0)
+ return static_cast(index);
+
+ index++;
+ }
+
+ return std::nullopt;
+}
+
+const char* Settings::GetDisplayFramePacingModeName(DisplayFramePacingMode mode)
+{
+ return s_display_frame_pacing_mode_names[static_cast(mode)];
+}
+
+const char* Settings::GetDisplayFramePacingModeDisplayName(DisplayFramePacingMode mode)
+{
+ return s_display_frame_pacing_mode_display_names[static_cast(mode)];
+}
+
static std::array s_audio_backend_names = {{
"Null",
"Cubeb",
diff --git a/src/core/settings.h b/src/core/settings.h
index 1531bb11e..f503a243d 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -127,6 +127,7 @@ struct Settings
bool gpu_pgxp_depth_buffer = false;
DisplayCropMode display_crop_mode = DisplayCropMode::None;
DisplayAspectRatio display_aspect_ratio = DisplayAspectRatio::Auto;
+ DisplayFramePacingMode display_frame_pacing_mode = DisplayFramePacingMode::DisplayAllFramesWhenPossible;
s16 display_active_start_offset = 0;
s16 display_active_end_offset = 0;
s8 display_line_start_offset = 0;
@@ -142,7 +143,6 @@ 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;
@@ -317,6 +317,10 @@ struct Settings
static const char* GetDisplayAspectRatioName(DisplayAspectRatio ar);
static float GetDisplayAspectRatioValue(DisplayAspectRatio ar);
+ static std::optional ParseDisplayFramePacingMode(const char* str);
+ static const char* GetDisplayFramePacingModeName(DisplayFramePacingMode mode);
+ static const char* GetDisplayFramePacingModeDisplayName(DisplayFramePacingMode mode);
+
static std::optional ParseAudioBackend(const char* str);
static const char* GetAudioBackendName(AudioBackend backend);
static const char* GetAudioBackendDisplayName(AudioBackend backend);
@@ -364,6 +368,8 @@ struct Settings
static constexpr DisplayCropMode DEFAULT_DISPLAY_CROP_MODE = DisplayCropMode::Overscan;
static constexpr DisplayAspectRatio DEFAULT_DISPLAY_ASPECT_RATIO = DisplayAspectRatio::Auto;
+ static constexpr DisplayFramePacingMode DEFAULT_DISPLAY_FRAME_PACING_MODE =
+ DisplayFramePacingMode::DisplayAllFramesWhenPossible;
static constexpr ControllerType DEFAULT_CONTROLLER_1_TYPE = ControllerType::DigitalController;
static constexpr ControllerType DEFAULT_CONTROLLER_2_TYPE = ControllerType::None;
static constexpr MemoryCardType DEFAULT_MEMORY_CARD_1_TYPE = MemoryCardType::PerGameTitle;
diff --git a/src/core/types.h b/src/core/types.h
index 30a383aee..f94065936 100644
--- a/src/core/types.h
+++ b/src/core/types.h
@@ -110,6 +110,15 @@ enum class DisplayAspectRatio : u8
Count
};
+enum class DisplayFramePacingMode : u8
+{
+ DisplayAllFrames,
+ DisplayAllFramesWhenPossible,
+ SkipDuplicateFrames,
+ ClampToFPS,
+ Count
+};
+
enum class AudioBackend : u8
{
Null,
diff --git a/src/duckstation-qt/displaysettingswidget.cpp b/src/duckstation-qt/displaysettingswidget.cpp
index 9a9312108..d82dfab40 100644
--- a/src/duckstation-qt/displaysettingswidget.cpp
+++ b/src/duckstation-qt/displaysettingswidget.cpp
@@ -28,6 +28,9 @@ DisplaySettingsWidget::DisplaySettingsWidget(QtHostInterface* host_interface, QW
SettingWidgetBinder::BindWidgetToEnumSetting(m_host_interface, m_ui.displayCropMode, "Display", "CropMode",
&Settings::ParseDisplayCropMode, &Settings::GetDisplayCropModeName,
Settings::DEFAULT_DISPLAY_CROP_MODE);
+ SettingWidgetBinder::BindWidgetToEnumSetting(
+ m_host_interface, m_ui.framePacingMode, "Display", "FramePacingMode", &Settings::ParseDisplayFramePacingMode,
+ &Settings::GetDisplayFramePacingModeName, Settings::DEFAULT_DISPLAY_FRAME_PACING_MODE);
SettingWidgetBinder::BindWidgetToEnumSetting(m_host_interface, m_ui.gpuDownsampleMode, "GPU", "DownsampleMode",
&Settings::ParseDownsampleModeName, &Settings::GetDownsampleModeName,
Settings::DEFAULT_GPU_DOWNSAMPLE_MODE);
@@ -39,8 +42,6 @@ DisplaySettingsWidget::DisplaySettingsWidget(QtHostInterface* host_interface, QW
SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.internalResolutionScreenshots, "Display",
"InternalResolutionScreenshots", false);
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);
@@ -112,7 +113,7 @@ 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("Optimal Frame Pacing"), tr("Unchecked"),
+ dialog->registerWidgetHelp(m_ui.framePacingMode, tr("Optimal Frame Pacing"), 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."));
@@ -140,7 +141,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, 2, 0, 1, 1);
+ m_ui.basicCheckboxGridLayout->addWidget(cb, 1, 1, 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 "
@@ -171,6 +172,13 @@ void DisplaySettingsWidget::setupAdditionalUi()
qApp->translate("DisplayCropMode", Settings::GetDisplayCropModeDisplayName(static_cast(i))));
}
+ for (u32 i = 0; i < static_cast(DisplayFramePacingMode::Count); i++)
+ {
+ m_ui.framePacingMode->addItem(
+ qApp->translate("DisplayFramePacingMode",
+ Settings::GetDisplayFramePacingModeDisplayName(static_cast(i))));
+ }
+
for (u32 i = 0; i < static_cast(GPUDownsampleMode::Count); i++)
{
m_ui.gpuDownsampleMode->addItem(
diff --git a/src/duckstation-qt/displaysettingswidget.ui b/src/duckstation-qt/displaysettingswidget.ui
index c8385b2e1..8db2b9a1a 100644
--- a/src/duckstation-qt/displaysettingswidget.ui
+++ b/src/duckstation-qt/displaysettingswidget.ui
@@ -62,7 +62,17 @@
-
- -
+
-
+
+
+ Frame Pacing Mode:
+
+
+
+ -
+
+
+ -
-
@@ -85,13 +95,6 @@
- -
-
-
- Optimal Frame Pacing
-
-
-
diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp
index c35e574d0..1953b90c0 100644
--- a/src/frontend-common/common_host_interface.cpp
+++ b/src/frontend-common/common_host_interface.cpp
@@ -792,7 +792,8 @@ 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;
+ m_display_all_frames =
+ (!m_throttler_enabled || (g_settings.display_frame_pacing_mode == DisplayFramePacingMode::DisplayAllFrames));
bool syncing_to_host = false;
if (g_settings.sync_to_host_refresh_rate && g_settings.audio_resampling && target_speed == 1.0f && m_display &&
@@ -815,12 +816,18 @@ void CommonHostInterface::UpdateSpeedLimiterState()
!System::IsRunning() || (m_throttler_enabled && g_settings.audio_sync_enabled && !is_non_standard_speed);
const bool video_sync_enabled =
!System::IsRunning() || (m_throttler_enabled && g_settings.video_sync_enabled && !is_non_standard_speed);
- const float max_display_fps = (!System::IsValid() || m_throttler_enabled) ? 0.0f : g_settings.display_max_fps;
+ const float max_display_fps = (g_settings.display_frame_pacing_mode != DisplayFramePacingMode::ClampToFPS &&
+ (!System::IsValid() || m_throttler_enabled)) ?
+ 0.0f :
+ g_settings.display_max_fps;
+ const bool display_duplicate_frames =
+ (g_settings.display_frame_pacing_mode != DisplayFramePacingMode::SkipDuplicateFrames) || !System::IsRunning();
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 (%s)", max_display_fps,
- m_display_all_frames ? "displaying all frames" : "skipping displaying frames when needed");
+ Log_InfoPrintf("Max display fps: %f", max_display_fps);
+ Log_InfoPrint(m_display_all_frames ? "Displaying all frames" : "Skipping displaying frames when needed");
+ Log_InfoPrint(display_duplicate_frames ? "Displaying duplicate frames" : "Skipping displaying duplicate frames");
if (System::IsValid())
{
@@ -979,7 +986,10 @@ void CommonHostInterface::OnSystemDestroyed()
{
// Restore present-all-frames behavior.
if (m_display)
+ {
m_display->SetDisplayMaxFPS(0.0f);
+ m_display->SetDisplayDuplicateFrames(true);
+ }
HostInterface::OnSystemDestroyed();
@@ -2697,7 +2707,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.display_frame_pacing_mode != old_settings.display_frame_pacing_mode ||
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/d3d11_host_display.cpp b/src/frontend-common/d3d11_host_display.cpp
index 12faaf857..5c4f195cd 100644
--- a/src/frontend-common/d3d11_host_display.cpp
+++ b/src/frontend-common/d3d11_host_display.cpp
@@ -706,6 +706,7 @@ bool D3D11HostDisplay::Render()
else
m_swap_chain->Present(BoolToUInt32(m_vsync), 0);
+ m_display_changed = false;
return true;
}
diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp
index f90575c64..0d1a283de 100644
--- a/src/frontend-common/fullscreen_ui.cpp
+++ b/src/frontend-common/fullscreen_ui.cpp
@@ -1895,10 +1895,12 @@ void DrawSettingsWindow()
break;
}
- settings_changed |= ToggleButton("Optimal Frame Pacing",
- "Ensures every frame generated is displayed for optimal pacing. Disable if "
- "you are having speed or sound issues.",
- &s_settings_copy.display_all_frames);
+ settings_changed |=
+ EnumChoiceButton("Frame Pacing Mode",
+ "Ensures every frame generated is displayed for optimal pacing. Disable if "
+ "you are having speed or sound issues.",
+ &s_settings_copy.display_frame_pacing_mode, &Settings::GetDisplayFramePacingModeDisplayName,
+ DisplayFramePacingMode::Count);
MenuHeading("Screen Display");
diff --git a/src/frontend-common/opengl_host_display.cpp b/src/frontend-common/opengl_host_display.cpp
index 2e2e7f7d1..d7b0a41d6 100644
--- a/src/frontend-common/opengl_host_display.cpp
+++ b/src/frontend-common/opengl_host_display.cpp
@@ -761,6 +761,7 @@ bool OpenGLHostDisplay::Render()
RenderSoftwareCursor();
m_gl_context->SwapBuffers();
+ m_display_changed = false;
return true;
}
diff --git a/src/frontend-common/vulkan_host_display.cpp b/src/frontend-common/vulkan_host_display.cpp
index 8d9529977..8d0cc96f9 100644
--- a/src/frontend-common/vulkan_host_display.cpp
+++ b/src/frontend-common/vulkan_host_display.cpp
@@ -636,6 +636,7 @@ bool VulkanHostDisplay::Render()
m_swap_chain->GetRenderingFinishedSemaphore(), m_swap_chain->GetSwapChain(),
m_swap_chain->GetCurrentImageIndex(), !m_swap_chain->IsVSyncEnabled());
g_vulkan_context->MoveToNextCommandBuffer();
+ m_display_changed = false;
return true;
}