Search code examples
c#.net-coreframe-rate

How to accurately throttle FPS in C#


I am working on an application that continuously polls windows for graphics drawn in video and games, takes a screen capture, and then does some processing. I am trying to implement a configurable FPS throttle to avoid using resources unnecessarily when it doesn't need to be running at such a high frame rate.

I've created this loop class and use a Stopwatch with elapsed ticks to calculate how long each frame takes to process and to throttle the current frame if it's processing too fast for the desired FPS.

Running this results in the FPS sitting around 35-40 when the target FPS is 60, and changing the target FPS results in an actual FPS in the same ratio as 35-40 / 60 actual to target. I've double checked the math and can't seem to figure out what's going wrong, as debugging it is tricky because breakpoints affect the elapsed time. Any suggestions on how to fix my loop or what's going wrong?

    public class ProcessLoop
    {
        const int TARGET_FPS = 60;
        const long OPTIMAL_TIME = (TimeSpan.TicksPerSecond / TARGET_FPS);

        private static Stopwatch _time = new Stopwatch();
        private int _frames = 0;
        private int _fps = 0;

        public void Run(CancellationToken stoppingToken, Action process)
        {
            _time.Start();
            long lastLoopTime = _time.ElapsedTicks;
            long lastSecondUpdate = lastLoopTime;

            while (!stoppingToken.IsCancellationRequested)
            {
                lastLoopTime = _time.ElapsedTicks;

                // Process one frame here
                process();
                _frames++;

                // The time after processing work
                var now = _time.ElapsedTicks;

                // How long it took to process the work done this frame
                var deltatime = now - lastLoopTime;

                if (now - lastSecondUpdate >= TimeSpan.TicksPerSecond)
                {
                    _fps = _frames;
                    _frames = 0;
                    lastSecondUpdate = 0;
                    _time.Restart();
                    Console.WriteLine($"FPS: {_fps}");
                }

                // Sleep to approach optimal time per frame
                if (deltatime < OPTIMAL_TIME)
                {
                    Thread.Sleep(TimeSpan.FromTicks(OPTIMAL_TIME - deltatime));
                }
            }
        }
    }

Solution

  • Thread.Sleep

    There's your problem, relinquishing a thread in a protected OS like Windows offers no guaratee that you'll get it back in the time specified. In fact, you are guaranteed to not get it back in the time you request, ever. It even says so in the official documentation.

    Either busy wait until the right time (high CPU usage), or use a hybrid approach (if it's essential to use as little CPU as possible) where you do tiny sleeps until you get "close enough" to the target time (~3ms with high precision timing enabled) then busy wait the rest of the way there.