@@ -53,7 +53,10 @@ constexpr float INITIAL_RIGHT_WINDOWS_WIDTH = 500;
5353float leftWindowsWidth = -1 .;
5454float rightWindowsWidth = -1 .;
5555
56- auto lastMainLoopIterTime = std::chrono::steady_clock::now();
56+ auto prevMainLoopTime = std::chrono::steady_clock::now();
57+ float rollingMainLoopDurationMicrosec = 0 .;
58+ float rollingMainLoopEMA = 0.05 ;
59+ float lastMainLoopDurationMicrosec = 0 .;
5760
5861const std::string prefsFilename = " .polyscope.ini" ;
5962
@@ -156,17 +159,57 @@ std::map<std::string, std::unique_ptr<Structure>>& getStructureMapCreateIfNeeded
156159void sleepForFramerate () {
157160 // If needed, block the program execution to hit the intended framerate
158161 // (if not used, the render loop may busy-run maxed out at 1000+ fps and waste resources)
162+
163+ // WARNING: similar logic duplicated in this function and in shouldSkipFrameForFramerate()
164+
159165 if (options::maxFPS != -1 ) {
160166 auto currTime = std::chrono::steady_clock::now ();
161167 long microsecPerLoop = 1000000 / options::maxFPS;
162168 microsecPerLoop = (95 * microsecPerLoop) / 100 ; // give a little slack so we actually hit target fps
163- while (std::chrono::duration_cast<std::chrono::microseconds>(currTime - lastMainLoopIterTime ).count () <
169+ while (std::chrono::duration_cast<std::chrono::microseconds>(currTime - prevMainLoopTime ).count () <
164170 microsecPerLoop) {
165171 std::this_thread::yield ();
166172 currTime = std::chrono::steady_clock::now ();
167173 }
168174 }
169- lastMainLoopIterTime = std::chrono::steady_clock::now ();
175+ }
176+
177+ bool shouldSkipFrameForFramerate () {
178+ // Returns true if the current frame should be skipped to maintain the framerate
179+
180+ // NOTE: right now this logic is pretty simplistic, it just allows the frame to happen if 95% of the frametime has
181+ // already passed. This might lead to choppiness in application loops which run at e.g. 150% of the target FPS, or
182+ // miss opporunities for better timing if frameTick() is called in extremely tight loops.
183+ //
184+ // In the future we could write a fancier version of this function implementing smarter policies using
185+ // rollingMainLoopDurationMicrosec
186+
187+ // WARNING: similar logic duplicated in this function and in sleepForFramerate()
188+
189+ if (options::maxFPS <= 0 ) return false ;
190+
191+ auto currTime = std::chrono::steady_clock::now ();
192+ float microsecPerLoop = 1000000 / options::maxFPS;
193+ microsecPerLoop = (95 * microsecPerLoop) / 100 ; // give a little slack so we actually hit target fps
194+ // NOTE: we could incorporate rollingMainLoopDurationMicrosec here, but since the loop time is recorded at the
195+ // beginning of each frame, the previous frame's time is _already_ incorporated into the timing.
196+ auto nextLoopStartTimeToHitTarget =
197+ prevMainLoopTime + std::chrono::microseconds (static_cast <int64_t >(std::round (microsecPerLoop)));
198+
199+ return currTime < nextLoopStartTimeToHitTarget;
200+ }
201+
202+ void markLastFrameTime () {
203+ auto currTime = std::chrono::steady_clock::now ();
204+
205+ // update the prev & rolling average frame time
206+ lastMainLoopDurationMicrosec =
207+ std::chrono::duration_cast<std::chrono::microseconds>(currTime - prevMainLoopTime).count ();
208+ rollingMainLoopDurationMicrosec =
209+ (1 . - rollingMainLoopEMA) * rollingMainLoopDurationMicrosec + rollingMainLoopEMA * lastMainLoopDurationMicrosec;
210+
211+ // mark the time of this frame
212+ prevMainLoopTime = currTime;
170213}
171214
172215} // namespace
@@ -304,27 +347,36 @@ void frameTick() {
304347 exception (" You called frameTick() while a previous call was in the midst of executing. Do not re-enter frameTick() "
305348 " or call it recursively." );
306349 }
307- frameTickStack++;
308350
309- // Make sure we're visible
310- render::engine->showWindow ();
311351
312- bool savedVsyncValue = false ; // see below
313- bool needToRestoreVSyncValue =
314- false ; // we need this as a saved bool, because the setting could change during mainLoopIteration()
315- if (options::frameTickLimitFPS) {
316- sleepForFramerate ();
317- } else {
352+ // == Logic for frame tick FPS limits
353+ bool savedVsyncValue = false ; // see below
354+ bool needToRestoreVSyncValue = false ; // need to save this, the setting could change during mainLoopIteration()
355+ switch (options::frameTickLimitFPSMode) {
356+ case LimitFPSMode::IgnoreLimits:
318357 // Ugly workaround to preserve the API:
319- // We want vsync to be disabled unless frameTick(limitFPS=True), so that we don't slow down user's application.
320- // But it's currently a bool read in the render call and I don't want to change that API. So we temporarily set
321- // it to false and restore the value after.
322- // ONEDAY: when we have a major version update, change the API on the vsync setting to make this unecessary
358+ // We want vsync to be disabled if we're ignoring fps limits (otherwise the platform will potentially block on
359+ // render swap to match the displays refresh rate). But it's currently a bool read in the render call and I don't
360+ // want to change that API. So we temporarily set it to false and restore the value after. ONEDAY: when we have a
361+ // major version update, change the API on the vsync setting to make this unecessary
323362 savedVsyncValue = options::enableVSync;
324363 options::enableVSync = false ;
325364 needToRestoreVSyncValue = true ;
365+ break ;
366+ case LimitFPSMode::BlockToHitTarget:
367+ sleepForFramerate ();
368+ break ;
369+ case LimitFPSMode::SkipFramesToHitTarget:
370+ if (shouldSkipFrameForFramerate ()) {
371+ return ;
372+ }
373+ break ;
326374 }
327375
376+
377+ frameTickStack++;
378+ render::engine->showWindow ();
379+
328380 mainLoopIteration ();
329381
330382 if (needToRestoreVSyncValue) {
@@ -724,19 +776,42 @@ void buildPolyscopeGui() {
724776 ImGui::SetNextItemOpen (false , ImGuiCond_FirstUseEver);
725777 if (ImGui::TreeNode (" Render" )) {
726778
727- // fps
728- ImGui::Text (" Rolling: %.1f ms/frame (%.1f fps)" , 1000 . 0f / ImGui::GetIO (). Framerate , ImGui::GetIO (). Framerate );
729- ImGui::Text ( " Last: %.1f ms/frame (%.1f fps) " , ImGui::GetIO (). DeltaTime * 1000 . f , 1 . f / ImGui::GetIO (). DeltaTime );
730-
731- ImGui::PushItemWidth ( 40 * options::uiScale );
779+ // fps display
780+ ImGui::Text (" Rolling: %.1f ms/frame (%.1f fps)" , 1e- 3f * rollingMainLoopDurationMicrosec,
781+ 1 . e6f / rollingMainLoopDurationMicrosec );
782+ ImGui::Text ( " Last: %.1f ms/frame (%.1f fps) " , 1e- 3f * lastMainLoopDurationMicrosec,
783+ 1 . e6f / lastMainLoopDurationMicrosec );
732784
733785 bool isFrameTickShow = frameTickStack > 0 ;
734786 if (isFrameTickShow) {
735- ImGui::Checkbox (" limit fps" , &options::frameTickLimitFPS);
736- ImGui::SameLine ();
787+ // build a little combo box to pick fps mode
788+ constexpr std::array<LimitFPSMode, 3 > limitFPSModeVals = {
789+ LimitFPSMode::IgnoreLimits, LimitFPSMode::BlockToHitTarget, LimitFPSMode::SkipFramesToHitTarget};
790+ auto to_string = [](LimitFPSMode x) -> std::string {
791+ switch (x) {
792+ case LimitFPSMode::IgnoreLimits:
793+ return " ignore limits" ;
794+ case LimitFPSMode::BlockToHitTarget:
795+ return " block to hit target" ;
796+ case LimitFPSMode::SkipFramesToHitTarget:
797+ return " skip frames to hit target" ;
798+ }
799+ return " " ; // unreachable
800+ };
801+ if (ImGui::BeginCombo (" fps mode##frame tick limit fps mode" , to_string (options::frameTickLimitFPSMode).c_str ())) {
802+ for (LimitFPSMode x : limitFPSModeVals) {
803+ if (ImGui::Selectable (to_string (x).c_str (), options::frameTickLimitFPSMode == x)) {
804+ options::frameTickLimitFPSMode = x;
805+ ImGui::SetItemDefaultFocus ();
806+ }
807+ }
808+ ImGui::EndCombo ();
809+ }
737810 }
738811
739- ImGui::BeginDisabled (isFrameTickShow && !options::frameTickLimitFPS);
812+ ImGui::BeginDisabled (isFrameTickShow && options::frameTickLimitFPSMode == LimitFPSMode::IgnoreLimits);
813+
814+ ImGui::PushItemWidth (40 * options::uiScale);
740815
741816 if (ImGui::InputInt (" max fps" , &options::maxFPS, 0 )) {
742817 if (options::maxFPS < 1 && options::maxFPS != -1 ) {
@@ -988,6 +1063,7 @@ void draw(bool withUI, bool withContextCallback) {
9881063
9891064
9901065void mainLoopIteration () {
1066+ markLastFrameTime ();
9911067
9921068 processLazyProperties ();
9931069 processLazyPropertiesOutsideOfImGui ();
0 commit comments