Reputation: 107
I'm using delta times to measure how much time has passed between frames in my loop. The value of the time
variable is used as a multiplier for animation speeds. This makes sure that a user who has a 60 Hz display will experience the same animation speed as a user with a 144 Hz display (albeit at different frame rates). Without this multiplier, the 144 Hz user would experience animations that are way faster than the person with a 60 Hz display. This is how I achieve this:
let time;
let last = window.performance.now();
function loop(current) {
time = current - last;
last = current;
// run game logic and render
requestAnimationFrame(loop);
}
Even though this measurement works great, it also has its flaws. Browsers aren't as consistent as native apps and the time
variable's value will vary greatly during stutters, resulting in "jumpy" animations.
Are there any better ways to detect a browser's frame rate without relying on requestAnimationFrame's inconsistent numbers?
Upvotes: 3
Views: 3200
Reputation: 54069
As the timers on browsers are not very accurate it is best to use a running mean.
A running mean is the average over the previous n frames.
From the running mean we can estimate the frame rate by selecting the closest frame rate above the mean
The example implements the running mean via the object FrameRate
To create const rate = FrameRate(sample)
where samples os the number of sample to take the mean of.
To add a sample assign any value to the property rate.tick
it use performance.now
to measure time.
rate.rate
holds the current mean in fps. If there are not enough samples it will return a rate of 1
rate.FPS
holds the number calculated frames per second. This is an estimate and will set the number of dropped frames when you get the value.
rate.dropped
Is the estimated number of frames dropped per sample count at the current frame rate returned by rate.FPS
Note dropped should be queried after FPS
function FrameRate(samples = 20) {
const times = [];
var s = samples;
while(s--) { times.push(0) }
var head = 0, total = 0, frame = 0, previouseNow = 0, rate = 0, dropped = 0;
const rates = [0, 10, 12, 15, 20, 30, 60, 90, 120, 144, 240];
const rateSet = rates.length;
const API = {
sampleCount: samples,
reset() {
frame = total = head = 0;
previouseNow = performance.now();
times.fill(0);
},
set tick(soak) {
const now = performance.now()
total -= times[head];
total += (times[head++] = now - previouseNow);
head %= samples;
frame ++;
previouseNow = now
},
get rate() { return frame > samples ? 1000 / (total / samples) : 1 },
get FPS() {
var r = API.rate, rr = r | 0, i = 0;
while (i < rateSet && rr > rates[i]) { i++ }
rate = rates[i];
dropped = Math.round((total - samples * (1000 / rate)) / (1000 / rate));
return rate;
},
get dropped() { return dropped },
};
return API;
}
const fRate = FrameRate();
var frame = 0;
requestAnimationFrame(loop);
fRate.reset();
function loop() {
frame++;
fRate.tick = 1;
meanRateEl.textContent = "Mean FPS: " + fRate.rate.toFixed(3);
FPSEl.textContent = "FPS: " + fRate.FPS;
droppedEl.textContent = "Dropped frames: " + fRate.dropped + " per " + fRate.sampleCount + " samples" ;
requestAnimationFrame(loop);
}
body {user-select: none;}
<div id="meanRateEl"></div>
<div id="FPSEl"></div>
<div id="droppedEl"></div>
If you get a consistent FPS of 90, 120, 144, or 240 and you have not disabled frame limiting and/or Vsnyc and are not using FireFox please make a comment. It is currently unclear how many have these setting as the default
The example lets you set 3 frame rates. It does this by calling multiple frame requests. For 240FPS it request 4 frames per sync
If you have turned of frame vsync on the browser the frame rates 120 and 240 will not report accurately. To see actual frame rate just use the 60FPS setting.
You can add a load to the frame. The load is in fraction of requested frame rate's frame time.
var forceRate = 60;
var current = 60;
var load = 0, loadTime = 0;
function FrameRate(samples = 20) {
const times = [];
var s = samples;
while(s--) { times.push(0) }
var head = 0, total = 0, frame = 0, previouseNow = 0, rate = 0, dropped = 0;
const rates = [0, 10, 12, 15, 20, 30, 60, 90, 120, 144, 240];
const rateSet = rates.length;
const API = {
sampleCount: samples,
reset() {
frame = total = head = 0;
previouseNow = performance.now();
times.fill(0);
},
set tick(soak) {
const now = performance.now()
total -= times[head];
total += (times[head++] = now - previouseNow);
head %= samples;
frame ++;
previouseNow = now
},
get rate() { return frame > samples ? 1000 / (total / samples) : 1 },
get FPS() {
var r = API.rate, rr = r | 0, i = 0;
while (i < rateSet && rr > rates[i]) { i++ }
rate = rates[i];
dropped = Math.round((total - samples * (1000 / rate)) / (1000 / rate));
return rate;
},
get dropped() { return dropped },
};
return API;
}
const fRate = FrameRate();
var frame = 0;
requestAnimationFrame(loop);
fRate.reset();
function loop() {
frame++;
fRate.tick = 1;
if (load) {
const pnow = performance.now() + loadTime;
while (performance.now() < pnow);
}
meanRateEl.textContent = "Mean FPS: " + fRate.rate.toFixed(3);
FPSEl.textContent = "FPS: " + fRate.FPS;
droppedEl.textContent = "Dropped frames: " + fRate.dropped + " per " + fRate.sampleCount + " samples" ;
if (current > forceRate) {
current /= 2;
} else {
requestAnimationFrame(loop);
if (current < forceRate) {
current *= 2;
requestAnimationFrame(loop);
}
}
}
options.addEventListener("click", (e) => {
if (e.target.dataset.load !== undefined) {
load = Number(e.target.dataset.load);
loadTime = (1000 / forceRate) * load;
loadEl.textContent = "Load " + loadTime.toFixed(3) + "ms " + (load * 100).toFixed(0) + "%";
return;
}
if (e.target.dataset.rate !== undefined && e.target.dataset.rate != forceRate) {
forceRate = e.target.dataset.rate | 0;
forcedRateEl.textContent = "Requested " + forceRate + "FPS";
loadTime = (1000 / forceRate) * load;
loadEl.textContent = "Load " + loadTime.toFixed(3) + "ms " + (load * 100).toFixed(0) + "%";
}
});
body {user-select: none;}
<div id="forcedRateEl">Requested 60FPS</div>
<div id="loadEl">Load 0ms 0% frame time</div>
<div id="meanRateEl"></div>
<div id="FPSEl"></div>
<div id="droppedEl"></div>
<div id="options">
Frame rate</br>
<button data-rate="240">240FPS</button>
<button data-rate="120">120FPS</button>
<button data-rate="60">60FPS</button><br>
Frame load</br>
<button data-load="0.0">0%</button>
<button data-load="0.25">25%</button>
<button data-load="0.5">50%</button>
<button data-load="0.75">75%</button>
<button data-load="0.90">90%</button>
<button data-load="1.1">110%</button>
<button data-load="1.5">150%</button>
<button data-load="2">200%</button>
</div>
If you have a game that can frequently come under a heavy load causing the frame rate to drop (say 60 down to 30) users will notice, It is best to cap the rate to 30 so that the game maintains a consistent rate and the slow down is not noticed.
Also be aware that doubling the frame rate more than halves the amount of CPU time you have to render the scene. Allowing users to control the frames rate can result in negative perceptions of game quality if it causes the game to frequently jump between rates, which is much more likely at higher frame rates.
98% of users can not tell the difference between 60 and 30FPS unless they have a side by side comparison. Even further almost all users can be completely fooled by simply adding a bogus frame rate indicator to the game, as long as you have a consistent frame rate, and it is above human visual persistence.
Upvotes: 5