Skip to content

Commit 4b202c1

Browse files
committed
add support for skipping frames to hit framerate
1 parent 44f0ce8 commit 4b202c1

File tree

4 files changed

+106
-28
lines changed

4 files changed

+106
-28
lines changed

include/polyscope/options.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ extern int maxFPS;
4141
// NOTE: some platforms may ignore the setting.
4242
extern bool enableVSync;
4343

44-
// When using the alternate `frameTick()` control flow instead of `show()`, should framerate-limiting maxFPS/vsync
45-
// features be respected? (which might caues the program to block on the call to frameTick()). (default: false)
46-
extern bool frameTickLimitFPS;
44+
// When using the alternate `frameTick()` control flow instead of `show()`, how should framerate-limiting maxFPS/vsync
45+
// features be respected? (which might caues the program to block on the call to frameTick()). (default:
46+
// LimitFPSMode::SkipFramesToHitTarget)
47+
extern LimitFPSMode frameTickLimitFPSMode;
4748

4849
// Read preferences (window size, etc) from startup file, write to same file on exit (default: true)
4950
extern bool usePrefsFile;

include/polyscope/types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum class TransparencyMode { None = 0, Simple, Pretty };
1515
enum class GroundPlaneMode { None, Tile, TileReflection, ShadowOnly };
1616
enum class GroundPlaneHeightMode { Automatic = 0, Manual };
1717
enum class BackFacePolicy { Identical, Different, Custom, Cull };
18+
enum class LimitFPSMode { IgnoreLimits = 0, BlockToHitTarget, SkipFramesToHitTarget};
1819

1920
enum class PointRenderMode { Sphere = 0, Quad };
2021
enum class MeshElement { VERTEX = 0, FACE, EDGE, HALFEDGE, CORNER };

src/options.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ bool enableVSync = false;
1919
#else
2020
bool enableVSync = true;
2121
#endif
22-
bool frameTickLimitFPS = false;
22+
LimitFPSMode frameTickLimitFPSMode = LimitFPSMode::SkipFramesToHitTarget;
2323

2424
bool usePrefsFile = true;
2525
bool initializeWithDefaultStructures = true;

src/polyscope.cpp

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ constexpr float INITIAL_RIGHT_WINDOWS_WIDTH = 500;
5353
float leftWindowsWidth = -1.;
5454
float 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

5861
const std::string prefsFilename = ".polyscope.ini";
5962

@@ -156,17 +159,57 @@ std::map<std::string, std::unique_ptr<Structure>>& getStructureMapCreateIfNeeded
156159
void 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

9901065
void mainLoopIteration() {
1066+
markLastFrameTime();
9911067

9921068
processLazyProperties();
9931069
processLazyPropertiesOutsideOfImGui();

0 commit comments

Comments
 (0)