From 2229f9b70ef7575952a8e698e6de0014c5b8f530 Mon Sep 17 00:00:00 2001 From: Muzychenko Andrey <33288308+k4zmu2a@users.noreply.github.com> Date: Thu, 18 Nov 2021 17:58:53 +0300 Subject: [PATCH] Added hybrid sleep/spin wait mode. --- SpaceCadetPinball/options.cpp | 2 ++ SpaceCadetPinball/options.h | 1 + SpaceCadetPinball/winmain.cpp | 45 ++++++++++++++++++++++++++++++++--- SpaceCadetPinball/winmain.h | 28 ++++++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/SpaceCadetPinball/options.cpp b/SpaceCadetPinball/options.cpp index d78850a..733b432 100644 --- a/SpaceCadetPinball/options.cpp +++ b/SpaceCadetPinball/options.cpp @@ -99,6 +99,7 @@ void options::init() Options.UncappedUpdatesPerSecond = get_int("Uncapped Updates Per Second", false); Options.SoundChannels = get_int("Sound Channels", DefSoundChannels); Options.SoundChannels = std::min(MaxSoundChannels, std::max(MinSoundChannels, Options.SoundChannels)); + Options.HybridSleep = get_int("HybridSleep", false); winmain::UpdateFrameRate(); @@ -130,6 +131,7 @@ void options::uninit() set_int("ShowMenu", Options.ShowMenu); set_int("Uncapped Updates Per Second", Options.UncappedUpdatesPerSecond); set_int("Sound Channels", Options.SoundChannels); + set_int("HybridSleep", Options.HybridSleep); } diff --git a/SpaceCadetPinball/options.h b/SpaceCadetPinball/options.h index 5578cb6..2ea0505 100644 --- a/SpaceCadetPinball/options.h +++ b/SpaceCadetPinball/options.h @@ -73,6 +73,7 @@ struct optionsStruct bool ShowMenu; bool UncappedUpdatesPerSecond; int SoundChannels; + bool HybridSleep; }; struct ControlRef diff --git a/SpaceCadetPinball/winmain.cpp b/SpaceCadetPinball/winmain.cpp index c98f4ac..e302c59 100644 --- a/SpaceCadetPinball/winmain.cpp +++ b/SpaceCadetPinball/winmain.cpp @@ -42,6 +42,8 @@ std::string winmain::FpsDetails; double winmain::UpdateToFrameRatio; winmain::DurationMs winmain::TargetFrameTime; optionsStruct& winmain::Options = options::Options; +winmain::DurationMs winmain::SpinThreshold = DurationMs(0.005); +WelfordState winmain::SleepState{}; int winmain::WinMain(LPCSTR lpCmdLine) { @@ -115,7 +117,8 @@ int winmain::WinMain(LPCSTR lpCmdLine) // If HW fails, fallback to SW SDL renderer. SDL_Renderer* renderer = nullptr; - for (int i = 0; i < 2 && !renderer; i++) + auto swOffset = strstr(lpCmdLine, "-sw") != nullptr ? 1 : 0; + for (int i = swOffset; i < 2 && !renderer; i++) { Renderer = renderer = SDL_CreateRenderer ( @@ -288,7 +291,10 @@ int winmain::WinMain(LPCSTR lpCmdLine) TimePoint frameEnd; if (targetTimeDelta > DurationMs::zero() && !Options.UncappedUpdatesPerSecond) { - std::this_thread::sleep_for(targetTimeDelta); + if (Options.HybridSleep) + HybridSleep(targetTimeDelta); + else + std::this_thread::sleep_for(targetTimeDelta); frameEnd = Clock::now(); } else @@ -519,6 +525,12 @@ void winmain::RenderUi() { Options.UncappedUpdatesPerSecond ^= true; } + if (ImGui::MenuItem("Precise Sleep", nullptr, Options.HybridSleep)) + { + Options.HybridSleep ^= true; + SleepState = WelfordState{}; + SpinThreshold = DurationMs::zero(); + } if (changed) { @@ -947,7 +959,9 @@ void winmain::RenderFrameTimeDialog() auto target = static_cast(TargetFrameTime.count()); auto scale = 1 / (64 / 2 / target); - ImGui::Text("Target frame time:%03.04fms, 1px:%03.04fms", target, scale); + auto spin = Options.HybridSleep ? static_cast(SpinThreshold.count()) : 0; + ImGui::Text("Target frame time:%03.04fms, 1px:%03.04fms, SpinThreshold:%03.04fms", + target, scale, spin); gfr_display->BlitToTexture(); auto region = ImGui::GetContentRegionAvail(); ImGui::Image(gfr_display->Texture, region); @@ -955,3 +969,28 @@ void winmain::RenderFrameTimeDialog() ImGui::End(); ImGui::PopStyleVar(); } + +void winmain::HybridSleep(DurationMs sleepTarget) +{ + static constexpr double StdDevFactor = 0.5; + + // This nice concept is from https://blat-blatnik.github.io/computerBear/making-accurate-sleep-function/ + // Sacrifices some CPU time for smaller frame time jitter + while (sleepTarget > SpinThreshold) + { + auto start = Clock::now(); + std::this_thread::sleep_for(DurationMs(1)); + auto end = Clock::now(); + + auto actualDuration = DurationMs(end - start); + sleepTarget -= actualDuration; + + // Update expected sleep duration using Welford's online algorithm + // With bad timer, this will run away to 100% spin + SleepState.Advance(actualDuration.count()); + SpinThreshold = DurationMs(SleepState.mean + SleepState.GetStdDev() * StdDevFactor); + } + + // spin lock + for (auto start = Clock::now(); DurationMs(Clock::now() - start) < sleepTarget;); +} diff --git a/SpaceCadetPinball/winmain.h b/SpaceCadetPinball/winmain.h index c6ab002..289d142 100644 --- a/SpaceCadetPinball/winmain.h +++ b/SpaceCadetPinball/winmain.h @@ -33,6 +33,31 @@ struct SdlPerformanceClock } }; +struct WelfordState +{ + double mean; + double M2; + int64_t count; + + WelfordState() : mean(0.005), M2(0), count(1) + { + + } + + void Advance(double newValue) + { + ++count; + auto delta = newValue - mean; + mean += delta / count; + M2 += delta * (newValue - mean); //M2n = M2n-1 + (Xn - AvgXn-1) * (Xn - AvgXn) + } + + double GetStdDev() const + { + return std::sqrt(M2 / (count - 1)); // Sn^2 = M2n / (n - 1) + } +}; + class winmain { using Clock = SdlPerformanceClock; // Or std::chrono::steady_clock. @@ -75,7 +100,10 @@ private: static double UpdateToFrameRatio; static DurationMs TargetFrameTime; static struct optionsStruct& Options; + static DurationMs SpinThreshold; + static WelfordState SleepState; static void RenderUi(); static void RenderFrameTimeDialog(); + static void HybridSleep(DurationMs seconds); };