FpsManager – A C++ helper class for framerate independent movement

Update – September 2013: Fixed an issue whereby the enforceFPS function only returned the time it took to run the enforceFPS function itself because I forgot to add the frameDuration. Fixed another issue where I reset the frameCount to 1 when it should have been reset to 0. Oops…


I wrote only a few months back that I didn’t want to write another piece of FPS code, ever. But this was before I started taking framerate independent movement seriously. In my past coding I’ve just enabled VSync and been done with it – as long as the machine had enough processing capacity to perform at 60fps everything was fine, and nearly everything I wrote was so simple that it didn’t task the box too much.

However, as I’ve been working on a lot of Android code recently where the processing capacity of the device can easily vary by orders of magnitude, I’ve started thinking more that I really need to be able to cater to framerate changes gracefully. And for this, I’ve reinvented the wheel – if only to be absolutely sure in no uncertain terms about how the wheel frickn’ works.

So to put this to the test, I wrote the FpsManager class, and rewrote the camera from my old post on Simple OpenGL FPS Controls into a proper class suitable for reuse in multiple projects and capable of working in a framerate independent manner. That’s the next post…

…first things first: The FpsManager! ;-)

FpsManager.hpp

  1. #include <string>
  2. #include <sstream>
  3.  
  4. #ifndef __glfw_h_
  5.     #include <GL/glfw.h> // We need GLFW for this, so let's check for it- although it'd be a doddle to convert to non-GLFW using code.
  6. #endif
  7.  
  8. /** The FpsManager class is designed to work with GLFW and enforces a specified framerate on an application.
  9.   * It can also display the current framerate at user-specified intervals, and in addition returns the time
  10.   * duration since the last frame, which can be used to implement framerate independent movement.
  11.   *
  12.   * Author: r3dux
  13.   * Revision: 0.3
  14.   * Date: 1st September 2013
  15.   *
  16.   * ---- Creation examples (it's most useful to create your fpsManager object globally in your Main.cpp file): ----
  17.   *
  18.   *     FpsManager fpsManager(60.0);                    // Lock to 60fps, no reporting of framerate
  19.   *
  20.   *     FpsManager fpsManager(85.0, 3.0);               // Lock to 85fps, output FPS to console once every three seconds
  21.   *
  22.   *     FpsManager fpsManager(30.0, 0.5, "My App");     // Lock to 30fps, output FPS to console & window title every half second
  23.   *
  24.   *
  25.   * ---- Using the fpsManager in your main loop: ----
  26.   *
  27.   * bool running     = true;
  28.   * double deltaTime = 0.0;
  29.   *
  30.   * while (running)
  31.   * {
  32.   *     // Calculate our camera movement
  33.   *     cam->move(deltaTime);
  34.   *
  35.   *     // Draw our scene
  36.   *     drawScene();
  37.   *
  38.   *     // Exit if ESC was pressed or window was closed
  39.   *     running = !glfwGetKey(GLFW_KEY_ESC) && glfwGetWindowParam(GLFW_OPENED);
  40.   *
  41.   *     // Call our fpsManager to limit the FPS and get the frame duration to pass to the cam->move method
  42.   *     deltaTime = fpsManager.enforceFPS();
  43.   * }
  44.   *
  45.   * That's it! =D
  46.   */
  47.  
  48. class FpsManager
  49. {
  50.  
  51.     private:
  52.         double frameStartTime;         // Frame start time
  53.         double frameEndTime;           // Frame end time
  54.         double frameDuration;          // How many milliseconds between the last frame and this frame
  55.  
  56.         double targetFps;              // The desired FPS to run at (i.e. maxFPS)
  57.         double currentFps;             // The current FPS value
  58.         int    frameCount;             // How many frames have been drawn s
  59.  
  60.         double targetFrameDuration;    // How many milliseconds each frame should take to hit a target FPS value (i.e. 60fps = 1.0 / 60 = 0.016ms)
  61.         double sleepDuration;          // How long to sleep if we're exceeding the target frame rate duration
  62.  
  63.         double lastReportTime;         // The timestamp of when we last reported
  64.         double reportInterval;         // How often to update the FPS value
  65.  
  66.         std::string windowTitle;       // Window title to update view GLFW
  67.  
  68.         bool verbose;                  // Whether or not to output FPS details to the console or update the window
  69.  
  70.         // Limit the minimum and maximum target FPS value to relatively sane values
  71.         static const double MIN_TARGET_FPS = 20.0;
  72.         static const double MAX_TARGET_FPS = 60.0; // If you set this above the refresh of your monitor and enable VSync it'll break! Be aware!
  73.  
  74.         // Private method to set relatively sane defaults. Called by constructors before overwriting with more specific values as required.
  75.         void init(double theTargetFps, bool theVerboseSetting)
  76.         {
  77.             setTargetFps(theTargetFps);
  78.  
  79.             frameCount     = 0;
  80.             currentFps     = 0.0;
  81.             sleepDuration  = 0.0;
  82.             frameStartTime = glfwGetTime();
  83.             frameEndTime   = frameStartTime + 1;
  84.             frameDuration  = 1;
  85.             lastReportTime = frameStartTime;
  86.             reportInterval = 1.0f;
  87.             windowTitle    = "NONE";
  88.             verbose        = theVerboseSetting;
  89.         }
  90.  
  91.     public:
  92.  
  93.         // Single parameter constructor - just set a desired framerate and let it go.
  94.         // Note: No FPS reporting by default, although you can turn it on or off later with the setVerbose(true/false) method
  95.         FpsManager(int theTargetFps)
  96.         {
  97.             init(theTargetFps, false);
  98.         }
  99.  
  100.         // Two parameter constructor which sets a desired framerate and a reporting interval in seconds
  101.         FpsManager(int theTargetFps, double theReportInterval)
  102.         {
  103.             init(theTargetFps, true);
  104.  
  105.             setReportInterval(theReportInterval);
  106.         }
  107.  
  108.         // Three parameter constructor which sets a desired framerate, how often to report, and the window title to append the FPS to
  109.         FpsManager(int theTargetFps, float theReportInterval, std::string theWindowTitle)
  110.         {
  111.             init(theTargetFps, true); // If you specify a window title it's safe to say you want the FPS to update there ;)
  112.  
  113.             setReportInterval(theReportInterval);
  114.  
  115.             windowTitle = theWindowTitle;
  116.         }
  117.  
  118.         // Getter and setter for the verbose property
  119.         bool getVerbose()
  120.         {
  121.             return verbose;
  122.         }
  123.         void setVerbose(bool theVerboseValue)
  124.         {
  125.             verbose = theVerboseValue;
  126.         }
  127.  
  128.         // Getter and setter for the targetFps property
  129.         int getTargetFps()
  130.         {
  131.             return targetFps;
  132.         }
  133.  
  134.         void setTargetFps(int theFpsLimit)
  135.         {
  136.             // Make at least some attempt to sanitise the target FPS...
  137.             if (theFpsLimit < MIN_TARGET_FPS)
  138.             {
  139.                 theFpsLimit = MIN_TARGET_FPS;
  140.                 std::cout << "Limiting FPS rate to legal minimum of " << MIN_TARGET_FPS << " frames per second." << std::endl;
  141.             }
  142.             if (theFpsLimit > MAX_TARGET_FPS)
  143.             {
  144.                 theFpsLimit = MAX_TARGET_FPS;
  145.                 std::cout << "Limiting FPS rate to legal maximum of " << MAX_TARGET_FPS << " frames per second." << std::endl;
  146.             }
  147.  
  148.             // ...then set it and calculate the target duration of each frame at this framerate
  149.             targetFps = theFpsLimit;
  150.             targetFrameDuration = 1.0 / targetFps;
  151.         }
  152.  
  153.         double getFrameDuration() { return frameDuration; } // Returns the time it took to complete the last frame in milliseconds
  154.  
  155.         // Setter for the report interval (how often the FPS is reported) - santises input.
  156.         void setReportInterval(float theReportInterval)
  157.         {
  158.             // Ensure the time interval between FPS checks is sane (low cap = 0.1s, high-cap = 10.0s)
  159.             // Negative numbers are invalid, 10 fps checks per second at most, 1 every 10 secs at least.
  160.             if (theReportInterval < 0.1)
  161.             {
  162.                 theReportInterval = 0.1;
  163.             }
  164.             if (theReportInterval > 10.0)
  165.             {
  166.                 theReportInterval = 10.0;
  167.             }
  168.             reportInterval = theReportInterval;
  169.         }
  170.  
  171.         // Method to force our application to stick to a given frame rate and return how long it took to process a frame
  172.         double enforceFPS()
  173.         {
  174.             // Get the current time
  175.             frameEndTime = glfwGetTime();
  176.  
  177.             // Calculate how long it's been since the frameStartTime was set (at the end of this method)
  178.             frameDuration = frameEndTime - frameStartTime;
  179.  
  180.             if (reportInterval != 0.0f)
  181.             {
  182.  
  183.                 // Calculate and display the FPS every specified time interval
  184.                 if ((frameEndTime - lastReportTime) > reportInterval)
  185.                 {
  186.                     // Update the last report time to be now
  187.                     lastReportTime = frameEndTime;
  188.  
  189.                     // Calculate the FPS as the number of frames divided by the interval in seconds
  190.                     currentFps =  (double)frameCount / reportInterval;
  191.  
  192.                     // Reset the frame counter to 1 (and not zero - which would make our FPS values off)
  193.                     frameCount = 1;
  194.  
  195.                     if (verbose)
  196.                     {
  197.                         std::cout << "FPS: " << currentFps << std::endl;
  198.  
  199.                         // If the user specified a window title to append the FPS value to...
  200.                         if (windowTitle != "NONE")
  201.                         {
  202.                             // Convert the fps value into a string using an output stringstream
  203.                             std::ostringstream stream;
  204.                             stream << currentFps;
  205.                             std::string fpsString = stream.str();
  206.  
  207.                             // Append the FPS value to the window title details
  208.                             std::string tempWindowTitle = windowTitle + " | FPS: " + fpsString;
  209.  
  210.                             // Convert the new window title to a c_str and set it
  211.                             const char* pszConstString = tempWindowTitle.c_str();
  212.                             glfwSetWindowTitle(pszConstString);
  213.                         }
  214.  
  215.                     } // End of if verbose section
  216.  
  217.                 }
  218.                 else // FPS calculation time interval hasn't elapsed yet? Simply increment the FPS frame counter
  219.                 {
  220.                     ++frameCount;
  221.                 }
  222.  
  223.             } // End of if we specified a report interval section
  224.  
  225.             // Calculate how long we should sleep for to stick to our target frame rate
  226.             sleepDuration = targetFrameDuration - frameDuration;
  227.  
  228.             // If we're running faster than our target duration, sleep until we catch up!
  229.             if (sleepDuration > 0.0)
  230.                 glfwSleep(targetFrameDuration - frameDuration);
  231.  
  232.             // Reset the frame start time to be now - this means we only need put a single call into the main loop
  233.             frameStartTime = glfwGetTime();
  234.  
  235.             // Pass back our total frame duration (including any sleep and the time it took to run this function) to be used as our deltaTime value
  236.             return frameDuration + (frameStartTime - frameEndTime);
  237.  
  238.         } // End of our enforceFPS method
  239.  
  240. };

Comments? Suggestions? Think I’ve designed it badly, or quite well? Know why it works just fine (from a usability standpoint) but provides a framerate just under that requested?

Feel free to let me know in the comments below! Cheers! =D