RequestAnimationFrame firing at 30FPS, targeting 60FPS - javascript

Chrome/Firefox are deciding to fire requestAnimationFrame at 30FPS. The profiler shows most of the time is spent idling, so its not cpu cycles being burned up.
I tried writing a delta timing to "catch up" / stay synced. The FPS is correct according to my FPS meter, but the result is very choppy animating. I also tried Drawing immediately after each Update, still choppy.
I am running a very vanilla rAF paradigm and notice the window size affects the FPS, but the profiler doesn't seem to explain why the FPS drops at full screen
let start = null;
let step = (timestamp)=>{
if(thisTimerID !== scope._timerID){
return;
}
start = start || timestamp + delay;
let current = timestamp;
let delta = current - start;
if(delta >= delay) {
this.Update();
this.Draw();
start = timestamp;
}
requestAnimationFrame(step);
};
thisTimerID = this._timerID = requestAnimationFrame(step);
Should I try requestAnimationFrame, and if I detect a low FPS when initializing, fall back to setInterval? Should I just use requestAnimationFrame anyway? Maybe there's a flaw in my delta timing logic? I know theres a huge flaw because if you switch tabs then come back, it will try to catch up and might think it missed thousands of frames!
Thank you
EDIT:
requestAnimationFrame IS firing at 60fps under minimal load. It is the Draw method that is causing the frame rate to drop. But why? I have 2 profiles showing a smaller resolution runs at 60fps, full screen does not.
60FPS 350x321 Resolution:
38FPS 1680x921 Resolution:
What's really strange to me, is the full screen version is actually doing LESS work according to the profiler.

Related

PixiJS Fixed-step loop - Stutter / Jitter

I've been facing a strange issue beyond my understanding of how to fix it.
I'm trying to create the following multiplayer game structure:
The server running at 20-30 fps
The client logic loop at the same FPS as the server
The client render loop
I'm using PixiJS for UI and here is where I got stuck.
( I've opened a thread here as well )
And I have a demo here: https://playcode.io/1045459
Ok, now let's explain the issue!
private update = () => {
let elapsed = PIXI.Ticker.shared.elapsedMS
if (elapsed > 1000) elapsed = this.frameDuration
this.lag += elapsed
//Update the frame if the lag counter is greater than or
//equal to the frame duration
while (this.lag >= this.frameDuration) {
//Update the logic
console.log(`[Update] FPS ${Math.round(PIXI.Ticker.shared.FPS)}`)
this.updateInputs(now())
//Reduce the lag counter by the frame duration
this.lag -= this.frameDuration
}
// Render elements in-between states
const lagOffset = this.lag / this.frameDuration
this.interpolateSpaceships(lagOffset)
}
In the client loop I keep track of both logic & render parts, limiting the logic one at 20FPS. It all works "cookies and clouds" until the browser has a sudden frame rate drop from 120fps to 60fps. Based on my investigation and a nice & confusing spreadsheet that I've put together when the frame rate drops, the "player" moves 2x more ( eg. 3.3 instead of 1.66 ) On paper it's normal and the math is correct, BUT this creates a small bounce / jitter / stutter or whatever naming this thing has.
In the demo that I've created in playcode it's not visible. My assumption is that the code is too basic and the framerate never drops.
Considering that the math and the algorithm are correct ( which I'm not yet sure ), I've turned my eyes to other parts that might affect this. I'm using pixi-viewport to follow the character. Could it be that the following part creates this bounce?
Does anyone have experience writing such a game loop?
Update:
Okkkk, mindblowing result. I just found out that this happens even with the most simple version of the game loop ever. Just by multiplying x = x + speed * delta every frame.
For the same reason. Sudden drops in FPS.
Ok, I've found the solution. Will post it here as there is not a lot of info about it. The solution is to smooth out sudden fps drops over multiple frames. Easy right? 😅
const ticker = new PIXI.Ticker();
// The number of frames to use for smoothing
const smoothingFrames = 10;
// The smoothed frame duration
let smoothedFrameDuration = 0;
ticker.add((deltaTime) => {
// Add the current frame duration to the smoothing array
smoothedFrameDuration = (smoothedFrameDuration * (smoothingFrames - 1) + deltaTime) / smoothingFrames;
// Update the game logic here
// Use the smoothed frame duration instead of the raw deltaTime value
});
ticker.start();

Animating HTML Video with requestAnimationFrame

I would like to use requestAnimationFrame to play an HTML <video> element. This is useful because it offers greater control over the playback (e.g. can play certain sections, control the speed, etc). However, I'm running into an issue with the following approach:
function playAnimation() {
window.cancelAnimationFrame(animationFrame);
var duration = video.seekable.end(0);
var start = null;
var step = function(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
const time = progress / 1000;
video.currentTime = time;
console.log(video.currentTime);
if (time > duration) {
start = null;
}
animationFrame = window.requestAnimationFrame(step);
}
animationFrame = window.requestAnimationFrame(step);
}
In Google Chrome, the video plays a little bit but then freezes. In Firefox it freezes even more. The console shows that the video's currentTime is being updated as expected, but it's not rendering the new time. Additionally, in the instances when the video is frozen, the ontimeupdate event does not fire, even though the currentTime is being updated.
A simple demo can be found here: https://codepen.io/TGordon18/pen/bGVQaXM
Any idea what's breaking?
Update:
Interestingly, controlling/throttling the animationFrame actually helps in Firefox.
setTimeout(() => {
animationFrame = window.requestAnimationFrame(step);
}, 1000 / FPS);
This doesn't seem like the right approach though
The seeking of the video is usually slower than one frame of requestAnimationFrame. One ideal frame of requestAnimationFrame is about 16.6ms (60 FPS), but the duration of the seek depends on how the video is encoded and where in the video you want to seek. When in step function you set video.currentTime and then do the same thing in the next frame, the previous seek operation most likely has not finished yet. As you continue calling video.currentTime over and over again, browser still tries to execute old tasks until the point it starts freezing because it is overwhelmed with the number of tasks. It might also influence how it fires the events like timeupdate.
The solution might be to explicitly wait for the seek to finish and only after that asking for the next animation frame.
video.onseeked = () => {
window.requestAnimationFrame(step);
}
https://codepen.io/mradionov/pen/vYNvyym?editors=0010
Nevertheless you most likely won't be able to achieve the same smooth real-time playback like in the video tag, because of how the seeking operation works. Unless you are willing to drop some current frames when the previous frame is still not ready.
Basically storing an entire image for each video frame is very expensive. One of the core video compression techniques is to store full video frame only in some intervals, like every 1 second (they are called key-frames of I-frames). The rest of the frames in between will store the difference from the previous frame (P-frames), which is pretty small compared to entire image. When video plays as usual, it already has previous frame in buffer, the only thing it needs to do is apply the difference for the next frame. But when you make a seek operation, there is no previous frame to calculate the difference from. Video decoder has to find the nearest key-frame with the full image and then apply the difference for all of the following frames up until the point it finally reaches the frame you wanted to seek to.
If you use my suggestion to wait for previous seek operation to complete before requesting for the next seek, you will see that video starts smooth, but when it gets closer to 2.5 seconds it will stutter more in more, until it reaches 2.5s+ and becomes smooth again. But then again it will start stuttering up to the point of 5s, and become smooth again after 5s+. That's because key-frame interval for this video is 2.5 seconds and the farther the timestamp you want to seek to from the key-frame, the longer it will take, because more frames need to be decoded.

Why is most of the browser frame time empty yet the frame still takes more than 16ms

I'm trying to make an animation where the frame rate is below 16ms. For many of the frames I get this pattern:
Where the majority of the frame is empty. The yellow is my requestAnimationFrame script, then rendering and painting. But why, when this only takes a small fraction of the frame time, is the frame still nearly 20ms long?
Here's some code that demonstrates it:
var square = document.getElementById('square');
document.addEventListener('mousemove', function(e) {
square.style.top = e.clientY - 25 + 'px';
square.style.left = e.clientX - 25 + 'px';
});
Fiddle: https://jsfiddle.net/5peLpf53/
Screenshot of timeline from fiddle:
Most, if not all, browsers run at 60 fps maximum. This is the same as once every 16.6 ms. There is no need for any higher, since many monitors are not higher than 60 Hz.
requestAnimationFrame just tells the browser to run some code before the next repaint of the screen. You can't update the screen any faster than this.
If you have some logic that requires more frequent update, then you should put it somewhere else.

Is it safe to assume 60 fps for browser rendering?

I want to make a JavaScript animation take 5 seconds to complete using requestAnimationFrame().
I don't want a strict and precise timing, so anything close to 5 seconds is OK and I want my code to be simple and readable, so solutions like this won't work for me.
My question is, is it safe to assume most browsers render the page at 60 fps? i.e. if I want my animation to take 5 seconds to complete, I'll divide it to 60 * 5 = 300 steps and with each call of function draw() using requestAnimationFrame(), draw the next step of animation. (Given the fact the animation is pretty simple, just moving a colored div around.)
By the way, I can't use jQuery.
Edit: Let me rephrase the question this way: Do all browsers 'normally' try to render the page at 60 fps? I want to know if Chrome for example renders at 75 fps or Firefox renders at 70 fps.
(Normal condition: CPU isn't highly loaded, RAM is not full, there are no storage failures, room is properly ventilated and nobody tries to throw my laptop out the window.)
Relying on 60 frames per second is very unsafe, because the browser isn't always in the same conditions, and even if it tries to render the page at the maximum fps possible, there's always a chance of the processor/cpu/gpu being busy doing something else, causing the FPS to drop down.
If you want to rely on FPS (although I wouldn't suggest you so), you should first detect the current fps, and adjust the speed of your animation frame per frame. Here's an example:
var lastCall, fps;
function detectFps() {
var delta;
if (lastCall) {
delta = (Date.now() - lastCall)/1000;
lastCall = Date.now();
fps = 1/delta;
} else {
lastCall = Date.now();
fps = 0;
}
}
function myFunc() {
detectFps();
// Calculate the speed using the var fps
// Animate...
requestAnimationFrame(myFunc);
}
detectFps(); // Initialize fps
requestAnimationFrame(myFunc); // Start your animation
It depends on the GPU and monitor combination. I have a good GPU and a 120 hertz monitor, so it renders at 120 fps. During the render, If I move to 60 hertz monitor, it will max out at 60 fps.
Another factor, that happens in some browsers/OS, is the iGPU being used instead of the discrete gpu.
As already stated by others, it isn't.
But if you need to end your animation in approximately 5 seconds and it's not crucial not to miss any frames in the animation, you can use the old setTimeout() way. That way you can miss a target by a few milliseconds, and some of the frames in your animation will be skipped (not rendered) because of the fps mismatch, but this can be a "good enough" solution, especially if your animation is simple as you state it is, there's a chance that users won't even see the glitch.
It's not safe to assume everyone can handle animation.
People will have different needs.
A lot of common animations, and common web design practices, give me awful migraines, so I set my browser to 1 frame per second to kill the animation without causing too much fast flashing.

Achieve somewhat stable framerate with requestAnimationFrame?

I am playing around with the requestAnimationFrame but I get very jerky animations in any other browser than Chrome.
I create an object like this:
var object = function() {
var lastrender = (new Date()).getTime();
var delta = 0;
return {
update: function() {
//do updates using delta value in calculations.
},
loop: function() {
var looptimestamp = (new Date()).getTime();
delta = looptimestamp - lastrender;
lastrender = looptimestamp;
this.update();
window.requestAnimationFrame(this.loop.bind(this));
}
};
}
Right now I am just drawing a single rectangle on a canvas element and moving it around. It is a very lightweight operation on the processor. This is running pretty smoothly in Chrome, and when I console log the delta value, it is almost consistant around ~17. However, if I do the same in Firefox or Safari I get the following delta values:
17-3-17-2-32-34-32-31-19-31-17-3-0-14-3-14-35-31-16-3-17-2 ... and so on
It looks as if the browser is not syncing with the display very nicely, and in all other cases than Chrome, one would get smoother animations using the old setTimeout method with 16ms as the target timeout.
Does anyone know, if it is possible to get smoother animations using requestAnimationFrame in browsers other than Chrome? Has anyone succeded in getting more stable delta values than the ones posted above in Firefox?
The reason the smooth framerate of your animation decreases is because of the memory of your browser with regards to the canvas. I don't know the real details of the performance in browsers but firefox almost immediately has a framerate drop and in chrome the drop occurs some time later.
The real problem of the framerate drop is because of the memory occupied by the canvas element. Each time you draw something to the canvas that operation is saved to the path of the canvas. This path occupies more memory each time you draw something on the canvas. If you don't empty the path of the canvas you will have framerate drops. The canvas path can't be emptied by clearing the canvas with context.clearRect(x, y, w, h);, instead you have to reset the canvas path by beginning a new path with context.beginPath();. For example:
// function that draws you content or animation
function draw(){
// start a new path/empty canvas path
canvas.beginPath();
// actual drawing of content or animation here
}
You might get smoother animation if you skip updates when delta < threshold, for example:
if (delta > 5) this.update();

Categories

Resources