Reputation: 2854
First of all, I know that Application.targetFrameRate
exists, and it does a good enough job, but I want something more accurate. For me, it limits the frame rate to around 60.3 when set to 60, and around 204 when set to 200. Btw these are measured in builds (not in the editor) using RTSS 7.2.
So I set out to create my custom frame limiter in Unity using low level timers, but it just doesn't work correctly for some reason. Here's my code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System;
public class FrameLimiter : MonoBehaviour
{
private FrameLimiter m_Instance;
public FrameLimiter Instance { get { return m_Instance; } }
public double FPSLimit = 300.0;
private long lastTime = HighResolutionDateTime.UtcNow.Ticks;
void Awake()
{
m_Instance = this;
}
void OnDestroy()
{
m_Instance = null;
}
void Update()
{
if (FPSLimit == 0.0) return;
lastTime += TimeSpan.FromSeconds(1.0 / FPSLimit).Ticks;
var now = HighResolutionDateTime.UtcNow.Ticks;
if (now >= lastTime)
{
lastTime = now;
return;
}
else
{
SpinWait.SpinUntil(() => { return (HighResolutionDateTime.UtcNow.Ticks >= lastTime); });
}
}
}
Basically this is a singleton that's set to execute before any other script in the script execution order, and it blocks execution until the right time has been reached.
The way it works is it keeps track of the precise time when it last allowed a frame to be rendered (it obtains the current time from a very precise low level timer, here's more detail about that). Then in every call to its Update()
function, it adds 1.0 / FPSLimit
seconds to it to get the time when the next frame should be rendered, then if the current time is less than this time, it blocks execution until that timestamp has been reached.
SpinWait
is basically just an efficient way to block execution without pinning the CPU like an empty while
loop would. But just for the record, I did try an empty while
loop too, and got the same results, except with much higher CPU usage.
So if you understand how this code is supposed to work, you should see that in theory this should lock the frame rate very precisely, especially given that this timer has a precision better than 1μs (0.001ms) according to Microsoft. But despite all that, I get about 58.8 FPS when I set this to lock at 60, which I really don't understand. I'm running the build in exclusive full screen mode with V-sync disabled. Btw I'm getting way higher frame rates with no limiting, so the base performance of the game isn't the issue.
Any help would be appreciated!
Upvotes: 0
Views: 2076
Reputation: 2854
The problem was weirder than I thought. It seems like this implementation of DateTime
rounds the stored time internally to a whole number of milliseconds, so my frame limiter was pacing frames at 17ms instead of 16.6666ms when set to 60 FPS.
The solution was to alter the code for the timer I'm using, and just get the raw value returned by GetSystemTimePreciseAsFileTime()
instead of encapsulating it in a DateTime
object.
With this change, RTSS shows a perfect 60.0 FPS lock in the build, with occasional dips to 59.9, if the limiter is set to 60.
Here's what it looks like with a frametime graph as well. I was looking around the map at different things to try to excercise the frame limiter's consistency a bit. Safe to say, I made a much better frame limiter than Unity's own :)
Upvotes: 1
Reputation: 2877
If your program works perfectly and the spinwait finishes exactly at the calculated time you get exactly 60fps.
Nothing is perfect so it will take a bit more than the calculated time giving you a lower frame rate.
Upvotes: 0