From c149d66d4d0882e299022d2b53127b90586ab066 Mon Sep 17 00:00:00 2001
From: Stenzek <stenzek@gmail.com>
Date: Sat, 13 Apr 2024 19:56:08 +1000
Subject: [PATCH] System: Add 'Reduce Input Latency' option

i.e. pre-frame sleep.
---
 src/core/fullscreen_ui.cpp                    |  70 ++++++-----
 src/core/imgui_overlays.cpp                   |  15 +--
 src/core/settings.cpp                         |   7 ++
 src/core/settings.h                           |   6 +-
 src/core/system.cpp                           | 109 ++++++++++++++++--
 src/core/system.h                             |   9 +-
 .../emulationsettingswidget.cpp               |  43 ++++++-
 src/duckstation-qt/emulationsettingswidget.h  |   2 +
 src/duckstation-qt/emulationsettingswidget.ui |  70 ++++++++---
 src/duckstation-qt/graphicssettingswidget.cpp |   5 +
 src/duckstation-qt/graphicssettingswidget.ui  |  71 +++++++-----
 src/util/imgui_manager.cpp                    |  27 ++---
 12 files changed, 320 insertions(+), 114 deletions(-)

diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp
index 2e59c63a2..8f9534964 100644
--- a/src/core/fullscreen_ui.cpp
+++ b/src/core/fullscreen_ui.cpp
@@ -3350,32 +3350,62 @@ void FullscreenUI::DrawEmulationSettingsPage()
 
   MenuHeading(FSUI_CSTR("Speed Control"));
   DrawFloatListSetting(
-    bsi, FSUI_CSTR("Emulation Speed"),
+    bsi, FSUI_ICONSTR(ICON_FA_STOPWATCH, "Emulation Speed"),
     FSUI_CSTR("Sets the target emulation speed. It is not guaranteed that this speed will be reached on all systems."),
     "Main", "EmulationSpeed", 1.0f, emulation_speed_titles.data(), emulation_speed_values.data(),
     emulation_speed_titles.size(), true);
   DrawFloatListSetting(
-    bsi, FSUI_CSTR("Fast Forward Speed"),
+    bsi, FSUI_ICONSTR(ICON_FA_BOLT, "Fast Forward Speed"),
     FSUI_CSTR("Sets the fast forward speed. It is not guaranteed that this speed will be reached on all systems."),
     "Main", "FastForwardSpeed", 0.0f, emulation_speed_titles.data(), emulation_speed_values.data(),
     emulation_speed_titles.size(), true);
   DrawFloatListSetting(
-    bsi, FSUI_CSTR("Turbo Speed"),
+    bsi, FSUI_ICONSTR(ICON_FA_BOLT, "Turbo Speed"),
     FSUI_CSTR("Sets the turbo speed. It is not guaranteed that this speed will be reached on all systems."), "Main",
     "TurboSpeed", 2.0f, emulation_speed_titles.data(), emulation_speed_values.data(), emulation_speed_titles.size(),
     true);
 
+  MenuHeading(FSUI_CSTR("Latency Control"));
+  DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_TV, "Vertical Sync (VSync)"),
+                    FSUI_CSTR("Synchronizes presentation of the console's frames to the host. GSync/FreeSync users "
+                              "should enable Optimal Frame Pacing instead."),
+                    "Display", "VSync", false);
+
+  DrawToggleSetting(
+    bsi, FSUI_ICONSTR(ICON_FA_LIGHTBULB, "Sync To Host Refresh Rate"),
+    FSUI_CSTR("Adjusts the emulation speed so the console's refresh rate matches the host when VSync is enabled."),
+    "Main", "SyncToHostRefreshRate", false);
+
+  DrawToggleSetting(
+    bsi, FSUI_ICONSTR(ICON_FA_TACHOMETER_ALT, "Optimal Frame Pacing"),
+    FSUI_CSTR("Ensures every frame generated is displayed for optimal pacing. Enable for variable refresh displays, "
+              "such as GSync/FreeSync. Disable if you are having speed or sound issues."),
+    "Display", "OptimalFramePacing", false);
+
+  const bool optimal_frame_pacing_active = GetEffectiveBoolSetting(bsi, "Display", "OptimalFramePacing", false);
+  DrawToggleSetting(
+    bsi, FSUI_ICONSTR(ICON_FA_STOPWATCH_20, "Reduce Input Latency"),
+    FSUI_CSTR("Reduces input latency by delaying the start of frame until closer to the presentation time."), "Display",
+    "PreFrameSleep", false, optimal_frame_pacing_active);
+  const bool pre_frame_sleep_active =
+    (optimal_frame_pacing_active && GetEffectiveBoolSetting(bsi, "Display", "PreFrameSleep", false));
+  DrawFloatRangeSetting(
+    bsi, FSUI_ICONSTR(ICON_FA_BATTERY_FULL, "Frame Time Buffer"),
+    FSUI_CSTR("Specifies the amount of buffer time added, which reduces the additional sleep time introduced."),
+    "Display", "PreFrameSleepBuffer", Settings::DEFAULT_DISPLAY_PRE_FRAME_SLEEP_BUFFER, 0.0f, 20.0f, "%.1f", 1.0f,
+    pre_frame_sleep_active);
+
   MenuHeading(FSUI_CSTR("Runahead/Rewind"));
 
-  DrawToggleSetting(bsi, FSUI_CSTR("Enable Rewinding"),
+  DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_BACKWARD, "Enable Rewinding"),
                     FSUI_CSTR("Saves state periodically so you can rewind any mistakes while playing."), "Main",
                     "RewindEnable", false);
   DrawFloatRangeSetting(
-    bsi, FSUI_CSTR("Rewind Save Frequency"),
+    bsi, FSUI_ICONSTR(ICON_FA_SAVE, "Rewind Save Frequency"),
     FSUI_CSTR("How often a rewind state will be created. Higher frequencies have greater system requirements."), "Main",
     "RewindFrequency", 10.0f, 0.0f, 3600.0f, FSUI_CSTR("%.2f Seconds"));
   DrawIntRangeSetting(
-    bsi, FSUI_CSTR("Rewind Save Slots"),
+    bsi, FSUI_ICONSTR(ICON_FA_GLASS_WHISKEY, "Rewind Save Slots"),
     FSUI_CSTR("How many saves will be kept for rewinding. Higher values have greater memory requirements."), "Main",
     "RewindSaveSlots", 10, 1, 10000, FSUI_CSTR("%d Frames"));
 
@@ -3389,7 +3419,7 @@ void FullscreenUI::DrawEmulationSettingsPage()
     FSUI_NSTR("8 Frames"), FSUI_NSTR("9 Frames"), FSUI_NSTR("10 Frames")};
 
   DrawIntListSetting(
-    bsi, FSUI_CSTR("Runahead"),
+    bsi, FSUI_ICONSTR(ICON_FA_RUNNING, "Runahead"),
     FSUI_CSTR(
       "Simulates the system ahead of time and rolls back/replays to reduce input lag. Very high system requirements."),
     "Main", "RunaheadFrameCount", 0, runahead_options.data(), runahead_options.size(), true);
@@ -4184,23 +4214,6 @@ void FullscreenUI::DrawDisplaySettingsPage()
       "GPU", "UseSoftwareRendererForReadbacks", false);
   }
 
-  DrawToggleSetting(bsi, FSUI_CSTR("VSync"),
-                    FSUI_CSTR("Synchronizes presentation of the console's frames to the host. GSync/FreeSync users "
-                              "should enable Optimal Frame Pacing instead."),
-                    "Display", "VSync", false);
-
-  DrawToggleSetting(
-    bsi, FSUI_CSTR("Sync To Host Refresh Rate"),
-    FSUI_CSTR("Adjusts the emulation speed so the console's refresh rate matches the host when VSync and Audio "
-              "Resampling are enabled."),
-    "Main", "SyncToHostRefreshRate", false);
-
-  DrawToggleSetting(
-    bsi, FSUI_CSTR("Optimal Frame Pacing"),
-    FSUI_CSTR("Ensures every frame generated is displayed for optimal pacing. Enable for variable refresh displays, "
-              "such as GSync/FreeSync. Disable if you are having speed or sound issues."),
-    "Display", "OptimalFramePacing", false);
-
   MenuHeading(FSUI_CSTR("Rendering"));
 
   DrawIntListSetting(
@@ -6983,7 +6996,7 @@ TRANSLATE_NOOP("FullscreenUI", "Add Shader");
 TRANSLATE_NOOP("FullscreenUI", "Adds a new directory to the game search list.");
 TRANSLATE_NOOP("FullscreenUI", "Adds a new shader to the chain.");
 TRANSLATE_NOOP("FullscreenUI", "Adds additional precision to PGXP data post-projection. May improve visuals in some games.");
-TRANSLATE_NOOP("FullscreenUI", "Adjusts the emulation speed so the console's refresh rate matches the host when VSync and Audio Resampling are enabled.");
+TRANSLATE_NOOP("FullscreenUI", "Adjusts the emulation speed so the console's refresh rate matches the host when VSync is enabled.");
 TRANSLATE_NOOP("FullscreenUI", "Advanced Settings");
 TRANSLATE_NOOP("FullscreenUI", "All Time: {}");
 TRANSLATE_NOOP("FullscreenUI", "Allow Booting Without SBI File");
@@ -7178,6 +7191,7 @@ TRANSLATE_NOOP("FullscreenUI", "Force NTSC Timings");
 TRANSLATE_NOOP("FullscreenUI", "Forces PAL games to run at NTSC timings, i.e. 60hz. Some PAL games will run at their \"normal\" speeds, while others will break.");
 TRANSLATE_NOOP("FullscreenUI", "Forces a full rescan of all games previously identified.");
 TRANSLATE_NOOP("FullscreenUI", "Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to disable background music in some games.");
+TRANSLATE_NOOP("FullscreenUI", "Frame Time Buffer");
 TRANSLATE_NOOP("FullscreenUI", "From File...");
 TRANSLATE_NOOP("FullscreenUI", "Fullscreen Resolution");
 TRANSLATE_NOOP("FullscreenUI", "GPU Adapter");
@@ -7226,6 +7240,7 @@ TRANSLATE_NOOP("FullscreenUI", "Interface Settings");
 TRANSLATE_NOOP("FullscreenUI", "Internal Resolution");
 TRANSLATE_NOOP("FullscreenUI", "Last Played");
 TRANSLATE_NOOP("FullscreenUI", "Last Played: %s");
+TRANSLATE_NOOP("FullscreenUI", "Latency Control");
 TRANSLATE_NOOP("FullscreenUI", "Launch Options");
 TRANSLATE_NOOP("FullscreenUI", "Launch a game by selecting a file/disc image.");
 TRANSLATE_NOOP("FullscreenUI", "Launch a game from a file, disc, or starts the console without any disc inserted.");
@@ -7335,8 +7350,10 @@ TRANSLATE_NOOP("FullscreenUI", "RAIntegration is being used instead of the built
 TRANSLATE_NOOP("FullscreenUI", "Read Speedup");
 TRANSLATE_NOOP("FullscreenUI", "Readahead Sectors");
 TRANSLATE_NOOP("FullscreenUI", "Recompiler Fast Memory Access");
+TRANSLATE_NOOP("FullscreenUI", "Reduce Input Latency");
 TRANSLATE_NOOP("FullscreenUI", "Reduces \"wobbly\" polygons by attempting to preserve the fractional component through memory transfers.");
 TRANSLATE_NOOP("FullscreenUI", "Reduces hitches in emulation by reading/decompressing CD data asynchronously on a worker thread.");
+TRANSLATE_NOOP("FullscreenUI", "Reduces input latency by delaying the start of frame until closer to the presentation time.");
 TRANSLATE_NOOP("FullscreenUI", "Reduces polygon Z-fighting through depth testing. Low compatibility with games.");
 TRANSLATE_NOOP("FullscreenUI", "Region");
 TRANSLATE_NOOP("FullscreenUI", "Region: ");
@@ -7455,6 +7472,7 @@ TRANSLATE_NOOP("FullscreenUI", "Smooths out the blockiness of magnified textures
 TRANSLATE_NOOP("FullscreenUI", "Sort By");
 TRANSLATE_NOOP("FullscreenUI", "Sort Reversed");
 TRANSLATE_NOOP("FullscreenUI", "Sound Effects");
+TRANSLATE_NOOP("FullscreenUI", "Specifies the amount of buffer time added, which reduces the additional sleep time introduced.");
 TRANSLATE_NOOP("FullscreenUI", "Spectator Mode");
 TRANSLATE_NOOP("FullscreenUI", "Speed Control");
 TRANSLATE_NOOP("FullscreenUI", "Speeds up CD-ROM reads by the specified factor. May improve loading speeds in some games, and break others.");
@@ -7523,8 +7541,8 @@ TRANSLATE_NOOP("FullscreenUI", "Uses game-specific settings for controllers for
 TRANSLATE_NOOP("FullscreenUI", "Uses perspective-correct interpolation for colors, which can improve visuals in some games.");
 TRANSLATE_NOOP("FullscreenUI", "Uses perspective-correct interpolation for texture coordinates, straightening out warped textures.");
 TRANSLATE_NOOP("FullscreenUI", "Uses screen positions to resolve PGXP data. May improve visuals in some games.");
-TRANSLATE_NOOP("FullscreenUI", "VSync");
 TRANSLATE_NOOP("FullscreenUI", "Value: {} | Default: {} | Minimum: {} | Maximum: {}");
+TRANSLATE_NOOP("FullscreenUI", "Vertical Sync (VSync)");
 TRANSLATE_NOOP("FullscreenUI", "When enabled and logged in, DuckStation will scan for achievements on startup.");
 TRANSLATE_NOOP("FullscreenUI", "When enabled, DuckStation will assume all achievements are locked and not send any unlock notifications to the server.");
 TRANSLATE_NOOP("FullscreenUI", "When enabled, DuckStation will list achievements from unofficial sets. These achievements are not tracked by RetroAchievements.");
diff --git a/src/core/imgui_overlays.cpp b/src/core/imgui_overlays.cpp
index aea39da2e..0958835b5 100644
--- a/src/core/imgui_overlays.cpp
+++ b/src/core/imgui_overlays.cpp
@@ -349,6 +349,12 @@ void ImGuiManager::DrawPerformanceOverlay()
       DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
     }
 
+    if (g_settings.display_show_latency_stats)
+    {
+      System::FormatLatencyStats(text);
+      DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
+    }
+
     if (g_settings.display_show_cpu_usage)
     {
       text.format("{:.2f}ms | {:.2f}ms | {:.2f}ms", System::GetMinimumFrameTime(), System::GetAverageFrameTime(),
@@ -410,15 +416,6 @@ void ImGuiManager::DrawPerformanceOverlay()
         FormatProcessorStat(text, System::GetSWThreadUsage(), System::GetSWThreadAverageTime());
         DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
       }
-
-#if 0
-      {
-        AudioStream* stream = g_spu.GetOutputStream();
-        const u32 frames = stream->GetBufferedFramesRelaxed();
-        text.fmt("Audio: {:<4u}f/{:<3u}ms", frames, AudioStream::GetMSForBufferSize(stream->GetSampleRate(), frames));
-        DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
-      }
-#endif
     }
 
     if (g_settings.display_show_gpu_usage && g_gpu_device->IsGPUTimingEnabled())
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index df231496d..a4344d669 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -267,6 +267,9 @@ void Settings::Load(SettingsInterface& si)
   display_screenshot_quality = static_cast<u8>(
     std::clamp<u32>(si.GetUIntValue("Display", "ScreenshotQuality", DEFAULT_DISPLAY_SCREENSHOT_QUALITY), 1, 100));
   display_optimal_frame_pacing = si.GetBoolValue("Display", "OptimalFramePacing", false);
+  display_pre_frame_sleep = si.GetBoolValue("Display", "PreFrameSleep", false);
+  display_pre_frame_sleep_buffer =
+    si.GetFloatValue("Display", "PreFrameSleepBuffer", DEFAULT_DISPLAY_PRE_FRAME_SLEEP_BUFFER);
   display_vsync = si.GetBoolValue("Display", "VSync", false);
   display_force_4_3_for_24bit = si.GetBoolValue("Display", "Force4_3For24Bit", false);
   display_active_start_offset = static_cast<s16>(si.GetIntValue("Display", "ActiveStartOffset", 0));
@@ -278,6 +281,7 @@ void Settings::Load(SettingsInterface& si)
   display_show_speed = si.GetBoolValue("Display", "ShowSpeed", false);
   display_show_gpu_stats = si.GetBoolValue("Display", "ShowGPUStatistics", false);
   display_show_resolution = si.GetBoolValue("Display", "ShowResolution", false);
+  display_show_latency_stats = si.GetBoolValue("Display", "ShowLatencyStatistics", false);
   display_show_cpu_usage = si.GetBoolValue("Display", "ShowCPU", false);
   display_show_gpu_usage = si.GetBoolValue("Display", "ShowGPU", false);
   display_show_frame_times = si.GetBoolValue("Display", "ShowFrameTimes", false);
@@ -521,6 +525,8 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
   si.SetStringValue("Display", "Alignment", GetDisplayAlignmentName(display_alignment));
   si.SetStringValue("Display", "Scaling", GetDisplayScalingName(display_scaling));
   si.SetBoolValue("Display", "OptimalFramePacing", display_optimal_frame_pacing);
+  si.SetBoolValue("Display", "PreFrameSleep", display_pre_frame_sleep);
+  si.SetFloatValue("Display", "PreFrameSleepBuffer", display_pre_frame_sleep_buffer);
   si.SetBoolValue("Display", "VSync", display_vsync);
   si.SetStringValue("Display", "ExclusiveFullscreenControl",
                     GetDisplayExclusiveFullscreenControlName(display_exclusive_fullscreen_control));
@@ -535,6 +541,7 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
     si.SetBoolValue("Display", "ShowFPS", display_show_fps);
     si.SetBoolValue("Display", "ShowSpeed", display_show_speed);
     si.SetBoolValue("Display", "ShowResolution", display_show_resolution);
+    si.SetBoolValue("Display", "ShowLatencyStatistics", display_show_latency_stats);
     si.SetBoolValue("Display", "ShowGPUStatistics", display_show_gpu_stats);
     si.SetBoolValue("Display", "ShowCPU", display_show_cpu_usage);
     si.SetBoolValue("Display", "ShowGPU", display_show_gpu_usage);
diff --git a/src/core/settings.h b/src/core/settings.h
index abb00859a..d431fdb5b 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -146,6 +146,7 @@ struct Settings
   s8 display_line_start_offset = 0;
   s8 display_line_end_offset = 0;
   bool display_optimal_frame_pacing : 1 = false;
+  bool display_pre_frame_sleep : 1 = false;
   bool display_vsync : 1 = false;
   bool display_force_4_3_for_24bit : 1 = false;
   bool gpu_24bit_chroma_smoothing : 1 = false;
@@ -154,6 +155,7 @@ struct Settings
   bool display_show_speed : 1 = false;
   bool display_show_gpu_stats : 1 = false;
   bool display_show_resolution : 1 = false;
+  bool display_show_latency_stats : 1 = false;
   bool display_show_cpu_usage : 1 = false;
   bool display_show_gpu_usage : 1 = false;
   bool display_show_frame_times : 1 = false;
@@ -161,8 +163,9 @@ struct Settings
   bool display_show_inputs : 1 = false;
   bool display_show_enhancements : 1 = false;
   bool display_stretch_vertically : 1 = false;
-  float display_osd_scale = 100.0f;
+  float display_pre_frame_sleep_buffer = DEFAULT_DISPLAY_PRE_FRAME_SLEEP_BUFFER;
   float display_max_fps = DEFAULT_DISPLAY_MAX_FPS;
+  float display_osd_scale = 100.0f;
   float gpu_pgxp_tolerance = -1.0f;
   float gpu_pgxp_depth_clear_threshold = DEFAULT_GPU_PGXP_DEPTH_THRESHOLD / GPU_PGXP_DEPTH_THRESHOLD_SCALE;
 
@@ -498,6 +501,7 @@ struct Settings
   static constexpr DisplayScreenshotMode DEFAULT_DISPLAY_SCREENSHOT_MODE = DisplayScreenshotMode::ScreenResolution;
   static constexpr DisplayScreenshotFormat DEFAULT_DISPLAY_SCREENSHOT_FORMAT = DisplayScreenshotFormat::PNG;
   static constexpr u8 DEFAULT_DISPLAY_SCREENSHOT_QUALITY = 85;
+  static constexpr float DEFAULT_DISPLAY_PRE_FRAME_SLEEP_BUFFER = 2.0f;
   static constexpr float DEFAULT_OSD_SCALE = 100.0f;
 
   static constexpr u8 DEFAULT_CDROM_READAHEAD_SECTORS = 8;
diff --git a/src/core/system.cpp b/src/core/system.cpp
index a161992c5..fb44a9071 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -45,6 +45,7 @@
 #include "util/postprocessing.h"
 #include "util/state_wrapper.h"
 
+#include "common/align.h"
 #include "common/error.h"
 #include "common/file_system.h"
 #include "common/log.h"
@@ -113,6 +114,9 @@ static void LogUnsafeSettingsToConsole(const std::string& messages);
 
 /// Throttles the system, i.e. sleeps until it's time to execute the next frame.
 static void Throttle(Common::Timer::Value current_time);
+static void UpdatePerformanceCounters();
+static void AccumulatePreFrameSleepTime();
+static void UpdatePreFrameSleepTime();
 
 static void SetRewinding(bool enabled);
 static bool SaveRewindState();
@@ -166,12 +170,6 @@ static const GameDatabase::Entry* s_running_game_entry = nullptr;
 static System::GameHash s_running_game_hash;
 static bool s_was_fast_booted;
 
-static float s_throttle_frequency = 0.0f;
-static float s_target_speed = 0.0f;
-static Common::Timer::Value s_frame_period = 0;
-static Common::Timer::Value s_next_frame_time = 0;
-static bool s_last_frame_skipped = false;
-
 static bool s_system_executing = false;
 static bool s_system_interrupted = false;
 static bool s_frame_step_request = false;
@@ -179,7 +177,20 @@ static bool s_fast_forward_enabled = false;
 static bool s_turbo_enabled = false;
 static bool s_throttler_enabled = false;
 static bool s_optimal_frame_pacing = false;
+static bool s_pre_frame_sleep = false;
 static bool s_syncing_to_host = false;
+static bool s_last_frame_skipped = false;
+
+static float s_throttle_frequency = 0.0f;
+static float s_target_speed = 0.0f;
+
+static Common::Timer::Value s_frame_period = 0;
+static Common::Timer::Value s_next_frame_time = 0;
+
+static Common::Timer::Value s_frame_start_time = 0;
+static Common::Timer::Value s_last_active_frame_time = 0;
+static Common::Timer::Value s_pre_frame_sleep_time = 0;
+static Common::Timer::Value s_max_active_frame_time = 0;
 
 static float s_average_frame_time_accumulator = 0.0f;
 static float s_minimum_frame_time_accumulator = 0.0f;
@@ -1861,7 +1872,15 @@ void System::FrameDone()
     SaveRunaheadState();
   }
 
-  const Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
+  Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
+
+  // pre-frame sleep accounting (input lag reduction)
+  const Common::Timer::Value pre_frame_sleep_until = s_next_frame_time + s_pre_frame_sleep_time;
+  s_last_active_frame_time = current_time - s_frame_start_time;
+  if (s_pre_frame_sleep)
+    AccumulatePreFrameSleepTime();
+
+  // explicit present (frame pacing)
   if (current_time < s_next_frame_time || s_syncing_to_host || s_optimal_frame_pacing || s_last_frame_skipped)
   {
     const bool throttle_before_present = (s_optimal_frame_pacing && s_throttler_enabled && !IsExecutionInterrupted());
@@ -1890,6 +1909,25 @@ void System::FrameDone()
     Throttle(current_time);
   }
 
+  // pre-frame sleep (input lag reduction)
+  current_time = Common::Timer::GetCurrentValue();
+  if (s_pre_frame_sleep)
+  {
+    // don't sleep if it's under 1ms, because we're just going to overshoot (or spin).
+    if (pre_frame_sleep_until > current_time &&
+        Common::Timer::ConvertValueToMilliseconds(pre_frame_sleep_until - current_time) >= 1)
+    {
+      Common::Timer::SleepUntil(pre_frame_sleep_until, true);
+      current_time = Common::Timer::GetCurrentValue();
+    }
+    else
+    {
+      Log_WarningPrint("Skipping pre-frame sleep");
+    }
+  }
+
+  s_frame_start_time = current_time;
+
   // Input poll already done above
   if (s_runahead_frames == 0)
   {
@@ -1940,6 +1978,7 @@ void System::UpdateThrottlePeriod()
 void System::ResetThrottler()
 {
   s_next_frame_time = Common::Timer::GetCurrentValue() + s_frame_period;
+  s_pre_frame_sleep_time = 0;
 }
 
 void System::Throttle(Common::Timer::Value current_time)
@@ -2624,6 +2663,9 @@ void System::UpdatePerformanceCounters()
   if (g_settings.display_show_gpu_stats)
     g_gpu->UpdateStatistics(frames_run);
 
+  if (s_pre_frame_sleep)
+    UpdatePreFrameSleepTime();
+
   Log_VerbosePrintf("FPS: %.2f VPS: %.2f CPU: %.2f GPU: %.2f Average: %.2fms Min: %.2fms Max: %.2f ms", s_fps, s_vps,
                     s_cpu_thread_usage, s_gpu_usage, s_average_frame_time, s_minimum_frame_time, s_maximum_frame_time);
 
@@ -2649,6 +2691,56 @@ void System::ResetPerformanceCounters()
   ResetThrottler();
 }
 
+void System::AccumulatePreFrameSleepTime()
+{
+  DebugAssert(s_pre_frame_sleep);
+
+  s_max_active_frame_time = std::max(s_max_active_frame_time, s_last_active_frame_time);
+
+  // in case one frame runs over, adjust to compensate
+  const Common::Timer::Value max_sleep_time_for_this_frame =
+    s_frame_period - std::min(s_last_active_frame_time, s_frame_period);
+  if (max_sleep_time_for_this_frame < s_pre_frame_sleep_time)
+  {
+    s_pre_frame_sleep_time = Common::AlignDown(max_sleep_time_for_this_frame,
+                                               static_cast<unsigned int>(Common::Timer::ConvertMillisecondsToValue(1)));
+    Log_DevFmt("Adjust pre-frame time to {} ms due to overrun of {} ms",
+               Common::Timer::ConvertValueToMilliseconds(s_pre_frame_sleep_time),
+               Common::Timer::ConvertValueToMilliseconds(s_last_active_frame_time));
+  }
+}
+
+void System::UpdatePreFrameSleepTime()
+{
+  DebugAssert(s_pre_frame_sleep);
+
+  const Common::Timer::Value expected_frame_time =
+    s_max_active_frame_time + Common::Timer::ConvertMillisecondsToValue(g_settings.display_pre_frame_sleep_buffer);
+  s_pre_frame_sleep_time = Common::AlignDown(s_frame_period - std::min(expected_frame_time, s_frame_period),
+                                             static_cast<unsigned int>(Common::Timer::ConvertMillisecondsToValue(1)));
+  Log_DevFmt("Set pre-frame time to {} ms (expected frame time of {} ms)",
+             Common::Timer::ConvertValueToMilliseconds(s_pre_frame_sleep_time),
+             Common::Timer::ConvertValueToMilliseconds(expected_frame_time));
+
+  s_max_active_frame_time = 0;
+}
+
+void System::FormatLatencyStats(SmallStringBase& str)
+{
+  AudioStream* audio_stream = SPU::GetOutputStream();
+  const u32 audio_latency =
+    AudioStream::GetMSForBufferSize(audio_stream->GetSampleRate(), audio_stream->GetBufferedFramesRelaxed());
+
+  const double active_frame_time = std::ceil(Common::Timer::ConvertValueToMilliseconds(s_last_active_frame_time));
+  const double pre_frame_time = std::ceil(Common::Timer::ConvertValueToMilliseconds(s_pre_frame_sleep_time));
+  const double input_latency = std::ceil(
+    Common::Timer::ConvertValueToMilliseconds(s_frame_period - s_pre_frame_sleep_time) -
+    Common::Timer::ConvertValueToMilliseconds(static_cast<Common::Timer::Value>(s_runahead_frames) * s_frame_period));
+
+  str.format("AF: {:.0f}ms | PF: {:.0f}ms | IL: {:.0f}ms | AL: {}ms", active_frame_time, pre_frame_time, input_latency,
+             audio_latency);
+}
+
 void System::UpdateSpeedLimiterState()
 {
   const float old_target_speed = s_target_speed;
@@ -2657,6 +2749,7 @@ void System::UpdateSpeedLimiterState()
                      (s_fast_forward_enabled ? g_settings.fast_forward_speed : g_settings.emulation_speed);
   s_throttler_enabled = (s_target_speed != 0.0f);
   s_optimal_frame_pacing = s_throttler_enabled && g_settings.display_optimal_frame_pacing;
+  s_pre_frame_sleep = s_throttler_enabled && g_settings.display_pre_frame_sleep;
 
   s_syncing_to_host = false;
   if (g_settings.sync_to_host_refresh_rate && (g_settings.audio_stretch_mode != AudioStretchMode::Off) &&
@@ -3786,6 +3879,8 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
         g_settings.fast_forward_speed != old_settings.fast_forward_speed ||
         g_settings.display_max_fps != old_settings.display_max_fps ||
         g_settings.display_optimal_frame_pacing != old_settings.display_optimal_frame_pacing ||
+        g_settings.display_pre_frame_sleep != old_settings.display_pre_frame_sleep ||
+        g_settings.display_pre_frame_sleep_buffer != old_settings.display_pre_frame_sleep_buffer ||
         g_settings.display_vsync != old_settings.display_vsync ||
         g_settings.sync_to_host_refresh_rate != old_settings.sync_to_host_refresh_rate)
     {
diff --git a/src/core/system.h b/src/core/system.h
index 953eaca18..0f469f24f 100644
--- a/src/core/system.h
+++ b/src/core/system.h
@@ -2,10 +2,13 @@
 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
 
 #pragma once
-#include "common/timer.h"
+
 #include "settings.h"
 #include "timing_event.h"
 #include "types.h"
+
+#include "common/timer.h"
+
 #include <memory>
 #include <optional>
 #include <string>
@@ -13,6 +16,7 @@
 class ByteStream;
 class CDImage;
 class Error;
+class SmallStringBase;
 class StateWrapper;
 
 class Controller;
@@ -230,6 +234,7 @@ float GetGPUUsage();
 float GetGPUAverageTime();
 const FrameTimeHistory& GetFrameTimeHistory();
 u32 GetFrameTimeHistoryPos();
+void FormatLatencyStats(SmallStringBase& str);
 
 /// Loads global settings (i.e. EmuConfig).
 void LoadSettings(bool display_osd_messages);
@@ -282,8 +287,6 @@ void SetThrottleFrequency(float frequency);
 /// Updates the throttle period, call when target emulation speed changes.
 void UpdateThrottlePeriod();
 void ResetThrottler();
-
-void UpdatePerformanceCounters();
 void ResetPerformanceCounters();
 
 /// Resets vsync/max present fps state.
diff --git a/src/duckstation-qt/emulationsettingswidget.cpp b/src/duckstation-qt/emulationsettingswidget.cpp
index 349303a03..7c9611aa4 100644
--- a/src/duckstation-qt/emulationsettingswidget.cpp
+++ b/src/duckstation-qt/emulationsettingswidget.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
 
 #include "emulationsettingswidget.h"
@@ -19,6 +19,9 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vsync, "Display", "VSync", false);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.syncToHostRefreshRate, "Main", "SyncToHostRefreshRate", false);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.optimalFramePacing, "Display", "OptimalFramePacing", false);
+  SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.preFrameSleep, "Display", "PreFrameSleep", false);
+  SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.preFrameSleepBuffer, "Display", "PreFrameSleepBuffer",
+                                                Settings::DEFAULT_DISPLAY_PRE_FRAME_SLEEP_BUFFER);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.rewindEnable, "Main", "RewindEnable", false);
   SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.rewindSaveFrequency, "Main", "RewindFrequency", 10.0f);
   SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.rewindSaveSlots, "Main", "RewindSaveSlots", 10);
@@ -69,6 +72,9 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget
   connect(m_ui.turboSpeed, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
           &EmulationSettingsWidget::onTurboSpeedIndexChanged);
   connect(m_ui.vsync, &QCheckBox::checkStateChanged, this, &EmulationSettingsWidget::onVSyncChanged);
+  connect(m_ui.optimalFramePacing, &QCheckBox::checkStateChanged, this,
+          &EmulationSettingsWidget::onOptimalFramePacingChanged);
+  connect(m_ui.preFrameSleep, &QCheckBox::checkStateChanged, this, &EmulationSettingsWidget::onPreFrameSleepChanged);
 
   connect(m_ui.rewindEnable, &QCheckBox::checkStateChanged, this, &EmulationSettingsWidget::updateRewind);
   connect(m_ui.rewindSaveFrequency, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
@@ -96,16 +102,25 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget
        "instead.</strong>"));
   dialog->registerWidgetHelp(
     m_ui.syncToHostRefreshRate, tr("Sync To Host Refresh Rate"), tr("Unchecked"),
-    tr("Adjusts the emulation speed so the console's refresh rate matches the host's refresh rate when both VSync and "
-       "Audio Resampling settings are enabled. This results in the smoothest animations possible, at the cost of "
-       "potentially increasing the emulation speed by less than 1%. Sync To Host Refresh Rate will not take effect if "
-       "the console's refresh rate is too far from the host's refresh rate. Users with variable refresh rate displays "
-       "should disable this option."));
+    tr(
+      "Adjusts the emulation speed so the console's refresh rate matches the host's refresh rate when VSync is "
+      "enabled. This results in the smoothest animations possible, at the cost of potentially increasing the emulation "
+      "speed by less than 1%. Sync To Host Refresh Rate will not take effect if the console's refresh rate is too far "
+      "from the host's refresh rate. Users with variable refresh rate displays should disable this option."));
   dialog->registerWidgetHelp(
     m_ui.optimalFramePacing, tr("Optimal Frame Pacing"), tr("Unchecked"),
     tr("Enabling this option will ensure every frame the console renders is displayed to the screen, at a consistent "
        "rate, for optimal frame pacing. If you have a GSync/FreeSync display, enable this option. If you are having "
        "difficulties maintaining full speed, or are getting audio glitches, try disabling this option."));
+  dialog->registerWidgetHelp(
+    m_ui.preFrameSleep, tr("Reduce Input Latency"), tr("Unchecked"),
+    tr("Reduces input latency by delaying the start of frame until closer to the presentation time. This may cause "
+       "dropped frames on slower systems with higher frame time variance, if the buffer size is not sufficient."));
+  dialog->registerWidgetHelp(m_ui.preFrameSleepBuffer, tr("Frame Time Buffer"),
+                             tr("%1 ms").arg(Settings::DEFAULT_DISPLAY_PRE_FRAME_SLEEP_BUFFER),
+                             tr("Specifies the amount of buffer time added, which reduces the additional sleep time "
+                                "introduced. Higher values increase input latency, but decrease the risk of overrun, "
+                                "or missed frames. Lower values require faster hardware."));
   dialog->registerWidgetHelp(
     m_ui.rewindEnable, tr("Rewinding"), tr("Unchecked"),
     tr("<b>Enable Rewinding:</b> Saves state periodically so you can rewind any mistakes while playing.<br> "
@@ -119,6 +134,7 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget
       "Simulates the system ahead of time and rolls back/replays to reduce input lag. Very high system requirements."));
 
   onVSyncChanged();
+  onOptimalFramePacingChanged();
   updateRewind();
 }
 
@@ -190,6 +206,21 @@ void EmulationSettingsWidget::onVSyncChanged()
   m_ui.syncToHostRefreshRate->setEnabled(vsync);
 }
 
+void EmulationSettingsWidget::onOptimalFramePacingChanged()
+{
+  const bool optimal_frame_pacing_enabled = m_dialog->getEffectiveBoolValue("Display", "OptimalFramePacing", false);
+  m_ui.preFrameSleep->setEnabled(optimal_frame_pacing_enabled);
+  onPreFrameSleepChanged();
+}
+
+void EmulationSettingsWidget::onPreFrameSleepChanged()
+{
+  const bool pre_frame_sleep_enabled = m_dialog->getEffectiveBoolValue("Display", "PreFrameSleep", false);
+  const bool show_buffer_size = (m_ui.preFrameSleep->isEnabled() && pre_frame_sleep_enabled);
+  m_ui.preFrameSleepBuffer->setVisible(show_buffer_size);
+  m_ui.preFrameSleepBufferLabel->setVisible(show_buffer_size);
+}
+
 void EmulationSettingsWidget::updateRewind()
 {
   const bool rewind_enabled = m_dialog->getEffectiveBoolValue("Main", "RewindEnable", false);
diff --git a/src/duckstation-qt/emulationsettingswidget.h b/src/duckstation-qt/emulationsettingswidget.h
index b66b1d7e9..d67ae6a15 100644
--- a/src/duckstation-qt/emulationsettingswidget.h
+++ b/src/duckstation-qt/emulationsettingswidget.h
@@ -22,6 +22,8 @@ private Q_SLOTS:
   void onFastForwardSpeedIndexChanged(int index);
   void onTurboSpeedIndexChanged(int index);
   void onVSyncChanged();
+  void onOptimalFramePacingChanged();
+  void onPreFrameSleepChanged();
   void updateRewind();
 
 private:
diff --git a/src/duckstation-qt/emulationsettingswidget.ui b/src/duckstation-qt/emulationsettingswidget.ui
index ce2b192e6..28cd56a91 100644
--- a/src/duckstation-qt/emulationsettingswidget.ui
+++ b/src/duckstation-qt/emulationsettingswidget.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>568</width>
-    <height>369</height>
+    <width>618</width>
+    <height>440</height>
    </rect>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
@@ -59,26 +59,62 @@
       <item row="2" column="1">
        <widget class="QComboBox" name="turboSpeed"/>
       </item>
-      <item row="3" column="0" colspan="2">
-       <layout class="QGridLayout" name="gridLayout">
-        <item row="0" column="0">
-         <widget class="QCheckBox" name="vsync">
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="title">
+      <string>Latency Control</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <widget class="QCheckBox" name="vsync">
+        <property name="text">
+         <string>Vertical Sync (VSync)</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QCheckBox" name="syncToHostRefreshRate">
+        <property name="text">
+         <string>Sync To Host Refresh Rate</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QCheckBox" name="optimalFramePacing">
+        <property name="text">
+         <string>Optimal Frame Pacing</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="QCheckBox" name="preFrameSleep">
+        <property name="text">
+         <string>Reduce Input Latency</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0" colspan="2">
+       <layout class="QHBoxLayout" name="preFrameSleepBufferLayout" stretch="0,1">
+        <item>
+         <widget class="QLabel" name="preFrameSleepBufferLabel">
           <property name="text">
-           <string>Vertical Sync (VSync)</string>
+           <string>Frame Time Buffer:</string>
           </property>
          </widget>
         </item>
-        <item row="0" column="2">
-         <widget class="QCheckBox" name="syncToHostRefreshRate">
-          <property name="text">
-           <string>Sync To Host Refresh Rate</string>
+        <item>
+         <widget class="QDoubleSpinBox" name="preFrameSleepBuffer">
+          <property name="suffix">
+           <string> Milliseconds</string>
           </property>
-         </widget>
-        </item>
-        <item row="1" column="0">
-         <widget class="QCheckBox" name="optimalFramePacing">
-          <property name="text">
-           <string>Optimal Frame Pacing</string>
+          <property name="decimals">
+           <number>1</number>
+          </property>
+          <property name="singleStep">
+           <double>0.500000000000000</double>
           </property>
          </widget>
         </item>
diff --git a/src/duckstation-qt/graphicssettingswidget.cpp b/src/duckstation-qt/graphicssettingswidget.cpp
index a6e87bbe8..5aacd8697 100644
--- a/src/duckstation-qt/graphicssettingswidget.cpp
+++ b/src/duckstation-qt/graphicssettingswidget.cpp
@@ -174,6 +174,8 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showGPU, "Display", "ShowGPU", false);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showInput, "Display", "ShowInputs", false);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showGPUStatistics, "Display", "ShowGPUStatistics", false);
+  SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showLatencyStatistics, "Display", "ShowLatencyStatistics",
+                                               false);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showStatusIndicators, "Display", "ShowStatusIndicators", true);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showFrameTimes, "Display", "ShowFrameTimes", false);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.showSettings, "Display", "ShowEnhancements", false);
@@ -426,6 +428,9 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
                              tr("Shows the host's GPU usage in the top-right corner of the display."));
   dialog->registerWidgetHelp(m_ui.showGPUStatistics, tr("Show GPU Statistics"), tr("Unchecked"),
                              tr("Shows information about the emulated GPU in the top-right corner of the display."));
+  dialog->registerWidgetHelp(
+    m_ui.showLatencyStatistics, tr("Show Latency Statistics"), tr("Unchecked"),
+    tr("Shows information about input and audio latency in the top-right corner of the display."));
   dialog->registerWidgetHelp(
     m_ui.showFrameTimes, tr("Show Frame Times"), tr("Unchecked"),
     tr("Shows the history of frame rendering times as a graph in the top-right corner of the display."));
diff --git a/src/duckstation-qt/graphicssettingswidget.ui b/src/duckstation-qt/graphicssettingswidget.ui
index ad5b81698..f3d19aa57 100644
--- a/src/duckstation-qt/graphicssettingswidget.ui
+++ b/src/duckstation-qt/graphicssettingswidget.ui
@@ -704,20 +704,6 @@
           </item>
           <item row="1" column="0" colspan="2">
            <layout class="QGridLayout" name="gridLayout_3">
-            <item row="4" column="0">
-             <widget class="QCheckBox" name="showInput">
-              <property name="text">
-               <string>Show Controller Input</string>
-              </property>
-             </widget>
-            </item>
-            <item row="5" column="0">
-             <widget class="QCheckBox" name="showStatusIndicators">
-              <property name="text">
-               <string>Show Status Indicators</string>
-              </property>
-             </widget>
-            </item>
             <item row="1" column="1">
              <widget class="QCheckBox" name="showFPS">
               <property name="text">
@@ -732,20 +718,6 @@
               </property>
              </widget>
             </item>
-            <item row="0" column="1">
-             <widget class="QCheckBox" name="showResolution">
-              <property name="text">
-               <string>Show Resolution</string>
-              </property>
-             </widget>
-            </item>
-            <item row="4" column="1">
-             <widget class="QCheckBox" name="showSettings">
-              <property name="text">
-               <string>Show Settings</string>
-              </property>
-             </widget>
-            </item>
             <item row="2" column="0">
              <widget class="QCheckBox" name="showCPU">
               <property name="text">
@@ -767,17 +739,52 @@
               </property>
              </widget>
             </item>
-            <item row="3" column="1">
+            <item row="2" column="1">
+             <widget class="QCheckBox" name="showGPU">
+              <property name="text">
+               <string>Show GPU Usage</string>
+              </property>
+             </widget>
+            </item>
+            <item row="5" column="1">
+             <widget class="QCheckBox" name="showSettings">
+              <property name="text">
+               <string>Show Settings</string>
+              </property>
+             </widget>
+            </item>
+            <item row="0" column="1">
+             <widget class="QCheckBox" name="showStatusIndicators">
+              <property name="text">
+               <string>Show Status Indicators</string>
+              </property>
+             </widget>
+            </item>
+            <item row="5" column="0">
+             <widget class="QCheckBox" name="showInput">
+              <property name="text">
+               <string>Show Controller Input</string>
+              </property>
+             </widget>
+            </item>
+            <item row="4" column="0">
              <widget class="QCheckBox" name="showFrameTimes">
               <property name="text">
                <string>Show Frame Times</string>
               </property>
              </widget>
             </item>
-            <item row="2" column="1">
-             <widget class="QCheckBox" name="showGPU">
+            <item row="3" column="1">
+             <widget class="QCheckBox" name="showResolution">
               <property name="text">
-               <string>Show GPU Usage</string>
+               <string>Show Resolution</string>
+              </property>
+             </widget>
+            </item>
+            <item row="4" column="1">
+             <widget class="QCheckBox" name="showLatencyStatistics">
+              <property name="text">
+               <string>Show Latency Statistics</string>
               </property>
              </widget>
             </item>
diff --git a/src/util/imgui_manager.cpp b/src/util/imgui_manager.cpp
index e5c756375..02c265ea2 100644
--- a/src/util/imgui_manager.cpp
+++ b/src/util/imgui_manager.cpp
@@ -561,19 +561,20 @@ ImFont* ImGuiManager::AddFixedFont(float size)
 bool ImGuiManager::AddIconFonts(float size)
 {
   static constexpr ImWchar range_fa[] = {
-    0xe086, 0xe086, 0xf002, 0xf002, 0xf005, 0xf005, 0xf007, 0xf007, 0xf00c, 0xf00e, 0xf011, 0xf011, 0xf013, 0xf013,
-    0xf017, 0xf017, 0xf019, 0xf019, 0xf01c, 0xf01c, 0xf021, 0xf021, 0xf023, 0xf023, 0xf025, 0xf025, 0xf027, 0xf028,
-    0xf02e, 0xf02e, 0xf030, 0xf030, 0xf03a, 0xf03a, 0xf03d, 0xf03d, 0xf049, 0xf04c, 0xf050, 0xf050, 0xf05e, 0xf05e,
-    0xf062, 0xf063, 0xf067, 0xf067, 0xf071, 0xf071, 0xf075, 0xf075, 0xf077, 0xf078, 0xf07b, 0xf07c, 0xf084, 0xf085,
-    0xf091, 0xf091, 0xf0a0, 0xf0a0, 0xf0ac, 0xf0ad, 0xf0c5, 0xf0c5, 0xf0c7, 0xf0c9, 0xf0cb, 0xf0cb, 0xf0d0, 0xf0d0,
-    0xf0dc, 0xf0dc, 0xf0e2, 0xf0e2, 0xf0e7, 0xf0e7, 0xf0eb, 0xf0eb, 0xf0f1, 0xf0f1, 0xf0f3, 0xf0f3, 0xf0fe, 0xf0fe,
-    0xf110, 0xf110, 0xf119, 0xf119, 0xf11b, 0xf11c, 0xf140, 0xf140, 0xf14a, 0xf14a, 0xf15b, 0xf15b, 0xf15d, 0xf15d,
-    0xf191, 0xf192, 0xf1ab, 0xf1ab, 0xf1dd, 0xf1de, 0xf1e6, 0xf1e6, 0xf1eb, 0xf1eb, 0xf1f8, 0xf1f8, 0xf1fc, 0xf1fc,
-    0xf242, 0xf242, 0xf245, 0xf245, 0xf26c, 0xf26c, 0xf279, 0xf279, 0xf2d0, 0xf2d0, 0xf2db, 0xf2db, 0xf2f2, 0xf2f2,
-    0xf3c1, 0xf3c1, 0xf3fd, 0xf3fd, 0xf410, 0xf410, 0xf466, 0xf466, 0xf4ce, 0xf4ce, 0xf500, 0xf500, 0xf51f, 0xf51f,
-    0xf538, 0xf538, 0xf545, 0xf545, 0xf547, 0xf548, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2, 0xf5aa, 0xf5aa, 0xf5e7, 0xf5e7,
-    0xf65d, 0xf65e, 0xf6a9, 0xf6a9, 0xf6cf, 0xf6cf, 0xf794, 0xf794, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815,
-    0xf818, 0xf818, 0xf84c, 0xf84c, 0xf8cc, 0xf8cc, 0x0,    0x0};
+    0xe06f, 0xe06f, 0xe086, 0xe086, 0xf002, 0xf002, 0xf005, 0xf005, 0xf007, 0xf007, 0xf00c, 0xf00e, 0xf011, 0xf011,
+    0xf013, 0xf013, 0xf017, 0xf017, 0xf019, 0xf019, 0xf01c, 0xf01c, 0xf021, 0xf021, 0xf023, 0xf023, 0xf025, 0xf025,
+    0xf027, 0xf028, 0xf02e, 0xf02e, 0xf030, 0xf030, 0xf03a, 0xf03a, 0xf03d, 0xf03d, 0xf049, 0xf04c, 0xf050, 0xf050,
+    0xf05e, 0xf05e, 0xf062, 0xf063, 0xf067, 0xf067, 0xf071, 0xf071, 0xf075, 0xf075, 0xf077, 0xf078, 0xf07b, 0xf07c,
+    0xf084, 0xf085, 0xf091, 0xf091, 0xf0a0, 0xf0a0, 0xf0ac, 0xf0ad, 0xf0c5, 0xf0c5, 0xf0c7, 0xf0c9, 0xf0cb, 0xf0cb,
+    0xf0d0, 0xf0d0, 0xf0dc, 0xf0dc, 0xf0e2, 0xf0e2, 0xf0e7, 0xf0e7, 0xf0eb, 0xf0eb, 0xf0f1, 0xf0f1, 0xf0f3, 0xf0f3,
+    0xf0fe, 0xf0fe, 0xf110, 0xf110, 0xf119, 0xf119, 0xf11b, 0xf11c, 0xf140, 0xf140, 0xf14a, 0xf14a, 0xf15b, 0xf15b,
+    0xf15d, 0xf15d, 0xf191, 0xf192, 0xf1ab, 0xf1ab, 0xf1dd, 0xf1de, 0xf1e6, 0xf1e6, 0xf1eb, 0xf1eb, 0xf1f8, 0xf1f8,
+    0xf1fc, 0xf1fc, 0xf240, 0xf240, 0xf242, 0xf242, 0xf245, 0xf245, 0xf26c, 0xf26c, 0xf279, 0xf279, 0xf2d0, 0xf2d0,
+    0xf2db, 0xf2db, 0xf2f2, 0xf2f2, 0xf3c1, 0xf3c1, 0xf3fd, 0xf3fd, 0xf410, 0xf410, 0xf466, 0xf466, 0xf4ce, 0xf4ce,
+    0xf500, 0xf500, 0xf51f, 0xf51f, 0xf538, 0xf538, 0xf545, 0xf545, 0xf547, 0xf548, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2,
+    0xf5aa, 0xf5aa, 0xf5e7, 0xf5e7, 0xf65d, 0xf65e, 0xf6a9, 0xf6a9, 0xf6cf, 0xf6cf, 0xf70c, 0xf70c, 0xf794, 0xf794,
+    0xf7a0, 0xf7a0, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815, 0xf818, 0xf818, 0xf84c, 0xf84c, 0xf8cc, 0xf8cc,
+    0x0,    0x0};
   static constexpr ImWchar range_pf[] = {
     0x2196, 0x2199, 0x219e, 0x21a1, 0x21b0, 0x21b3, 0x21ba, 0x21c3, 0x21c7, 0x21ca, 0x21d0, 0x21d4, 0x21dc, 0x21dd,
     0x21e0, 0x21e3, 0x21ed, 0x21ee, 0x21f7, 0x21f8, 0x21fa, 0x21fb, 0x227a, 0x227f, 0x2284, 0x2284, 0x235e, 0x235e,