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( std::clamp(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(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(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(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(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 #include #include @@ -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 +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // 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::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::of(&QDoubleSpinBox::valueChanged), this, @@ -96,16 +102,25 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget "instead.")); 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("Enable Rewinding: Saves state periodically so you can rewind any mistakes while playing.
" @@ -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 @@ 0 0 - 568 - 369 + 618 + 440 @@ -59,26 +59,62 @@ - - - - + + + + + + + Latency Control + + + + + + Vertical Sync (VSync) + + + + + + + Sync To Host Refresh Rate + + + + + + + Optimal Frame Pacing + + + + + + + Reduce Input Latency + + + + + + + - Vertical Sync (VSync) + Frame Time Buffer: - - - - Sync To Host Refresh Rate + + + + Milliseconds - - - - - - Optimal Frame Pacing + + 1 + + + 0.500000000000000 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,80 +704,87 @@ - - + + - Show Controller Input + Show FPS - - + + - Show Status Indicators + Show OSD Messages - - + + - Show FPS + Show CPU Usage - - + + - Show OSD Messages + Show GPU Statistics - - + + - Show Resolution + Show Emulation Speed - + + + + Show GPU Usage + + + + Show Settings - - + + - Show CPU Usage + Show Status Indicators - - + + - Show GPU Statistics + Show Controller Input - - + + - Show Emulation Speed + Show Frame Times - + - Show Frame Times + Show Resolution - - + + - Show GPU Usage + Show Latency Statistics 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,