Kamujin's picture

How to get more accurate FPS without spinning the CPU.

The following code is a replacement for the Run method in GameWindow that I use. There is some room for minor speed optimizations, but this method is orders of magnitude more efficient then spinning the CPU with Thread.Sleep(0);

In my tests, this algorithm is able to achieve FPS rates withing 1-3 frames of the target parameter when measured in quarter second intervals. Sampling over 5 seconds yields actual FPS within 1 frame of target. The algorithm also quickly adapts to new loads such as those experienced during window resizing.

The approach uses a simple "balancing broom" style fuzzy logic algorithm to deal with the problem of Thread.Sleep(...) timers with resolutions too low to support a deterministic calculation of the time parameter needed to obtain the desired FPS.

For Example:
On most windows machines, the sleep timer has a resolution of only 15ms. This means that calling Thread.Sleep(1) can actually put the thread to sleep for up to 15ms. Therefore, you can not simply achieve 100FPS but calling Sleep(10) between frames. In the case of a 100FPS target, the algorithm will determine a blend of Sleep(0) and Sleep(1) calls that will achieve the desired frame rate. Even though some frames result in a Sleep(0) call between frames, the occasional Sleep(1) will dramatically lower the CPU load.

For a target of 50 FPS, the algorithm will most likely blend Sleep(15) and Sleep(16) calls.

One very attractive part of this algorithm is that no assumptions need be made about timer resolution. The algorithm will automatically adapt to whatever timer resolution is available.

Fiddler,
I would really like write access to the SVN to further develop this algorithm. I would like to extend this code with the following improvements,

1) Support Update and Render calls with different target frequencies.

2) A more even distribution of Sleep calls that will remove the "clustering" effect of the current system.

3) Remove the arrays.

4) Remove the need to search.

5) Increased precision.

I already have a system in mind to accomplish these improvements.

        public void RunSimple(int targetFps)
        {
            if (disposed) throw new ObjectDisposedException("GameWindow");
            try
            {
                TargetUpdateFrequency = targetFps;
                TargetRenderFrequency = targetFps;
 
                UpdateFrameEventArgs updateArgs = new UpdateFrameEventArgs();
                RenderFrameEventArgs renderArgs = new RenderFrameEventArgs();
 
                try
                {
                    OnLoadInternal(EventArgs.Empty);
                }
                catch (Exception e)
                {
                    Trace.WriteLine(String.Format("OnLoad failed: {0}", e.ToString()));
                    return;
                }
 
                Debug.Print("Entering main loop.");
                hasMainLoop = true;
 
                Stopwatch stopWatch = Stopwatch.StartNew();
 
                int[] sleepTimes = new int[15];
                for (int i = 0; i < sleepTimes.Length; i++) sleepTimes[i] = 1000 / targetFps;
                int frameSleepTime = 0;
 
                int frames = 0;
                double previousElapsedSeconds = 0;
                while (!isExiting)
                {
                    frames++;
 
                    double totalElapsedSeconds = stopWatch.Elapsed.TotalSeconds;
                    double frameElapsedSeconds = totalElapsedSeconds - previousElapsedSeconds;
                    previousElapsedSeconds = totalElapsedSeconds;
 
                    if (totalElapsedSeconds >= 0.25)
                    {
                        double fps = frames / totalElapsedSeconds;
 
                        if (fps < targetFps)
                        {
                            int max = 0;
                            for (int i = 1; i < sleepTimes.Length; i++)
                            {
                                if (sleepTimes[i] > sleepTimes[max]) max = i;
                            }
                            sleepTimes[max] = System.Math.Max(0, sleepTimes[max] - 1);
                        }
                        else
                        {
                            int min = 0;
                            for (int i = 1; i < sleepTimes.Length; i++)
                            {
                                if (sleepTimes[i] < sleepTimes[min]) min = i;
                            }
                            sleepTimes[min] += 1;
                        }
 
                        //Console.Write(string.Format("FPS:{0} Target:{1} SleepTimes:", fps, targetFps));
                        //for (int i = 0; i < sleepTimes.Length; i++)
                        //{
                        //    Console.Write(string.Format(" {0}", sleepTimes[i]));
                        //}
                        //Console.Write("\n");
 
                        stopWatch.Reset(); 
                        stopWatch.Start();
                        frames = 0;
                        previousElapsedSeconds = 0;
                    }
 
                    ProcessEvents();
 
                    updateArgs.Time = frameElapsedSeconds;
                    OnUpdateFrameInternal(updateArgs);
 
                    renderArgs.Time = frameElapsedSeconds;
	                OnRenderFrameInternal(renderArgs);
 
                    System.Threading.Thread.Sleep(sleepTimes[frameSleepTime]);
                    frameSleepTime = (frameSleepTime + 1) % sleepTimes.Length;
                }
            }
            catch (GameWindowExitException)
            {
                Debug.WriteLine("GameWindowExitException caught - exiting main loop.");
            }
            finally
            {
                Debug.Print("Restoring priority.");
                Thread.CurrentThread.Priority = ThreadPriority.Normal;
 
                OnUnloadInternal(EventArgs.Empty);
 
                if (Exists)
                {
                    glContext.Dispose();
                    glContext = null;
                    glWindow.DestroyWindow();
                }
                while (this.Exists)
                    this.ProcessEvents();
            }
        }

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
objarni's picture

Is this class something that

1) will replace functionality in GameWindow
2) will be an option for GameWindow-focused app-development?

Kamujin's picture

I am offering a scheduler and a reference implementation of a Run() loop. What Fiddler ultimately decides to do with it, I can not say.

I still have to complete some code documentation, as per Fiddlers request, to complete the submission. Beyond that I don't plan to advocate strongly for any particular outcome.

objarni's picture

In either case I'd like to have some example code to see how it is used - but I can see that you don't want to spend time writing that until Fiddler decides where the code goes ...

I'm likely to use the code in any case, so keep up the good work :)

the Fiddler's picture

Kamujin, can you please provide a patch of this code against the current SVN head or 0.9.1? Patches take a moment to create and apply, which isn't the case with raw code, especially when the code requires changes to the API (ints intsead of doubles).

So, please roll a patch and attach it to a bug report. This will definitely help things move faster.

Kamujin's picture

*moved to bug report*