i use requestAnimationFrame
in a js webgl project where i change the pixel color from black to white on each frame by accessing a texture that contains the content of the last frame ) but the framerate exceeds the Hz of my monitor and the flickering is inconsistent.
also i calculate the fps with the help of window.performance.now()
and the framerate value looks like
n_fps: 200
n_fps: 333.3333333333333
n_fps: 250
i thought the requestAnimationFrame
should sync the function call with the framerate of my monitor which is set to 239.96hz, but the flickering is inconsistent and the framerate sometimes exceeds 240fps, i cant figure out why but i suspect it has to do with v-sync.
Here some specs
OS
Distributor ID: Ubuntu
Description: Pop!_OS 22.04 LTS
GPU
lshw -c video
WARNING: you should run this program as super-user.
*-display
description: VGA compatible controller
product: Ellesmere [Radeon RX 470/480/570/570X/580/580X/590]
Monitor settings
xrandr –verbose
DisplayPort-0 connected primary 1920×1080+0+1200
…
TearFree: on
supported: off, on, auto
…
the important part of my js code looks like this
//...
const gl = canvas.getContext(
'webgl2',
{
desynchronized: false, //trying to force vsync?
}
);
///...
function render() {
nid = requestAnimationFrame(render);
let n_ts_ms_now = window.performance.now();
let n_ms_delta = n_ts_ms_now - n_ts_ms;
// console.log(n_ms_delta)
console.log(`n_fps: ${1000/n_ms_delta})`);
n_ts_ms = n_ts_ms_now;
n += 1;
// if(n_ms_delta > n_ms_max){
const nextTextureIndex = 1 - currentTextureIndex;
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[nextTextureIndex]);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[currentTextureIndex]);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Swap textures
currentTextureIndex = nextTextureIndex;
// Render to the canvas
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.bindTexture(gl.TEXTURE_2D, textures[currentTextureIndex]);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
console.log(n)
// }
}
what i tried:
- manually forcing/setting vsync with this command
xrandr --output DisplayPort-0 --set TearFree on
- running chrome and firefox with vblank_mode=1
vblank_mode=1 google-chrome
and then
when i manually throttle the fps (commented out code) the flickering looks consistent.
2
Answers
minimal reproducable example, the flickering is consistent when setting my display to 144hz and using google-chrome browser
i also have another question: in the end i want to make a cellular automata which means that i have to be able to access information from the last frame in the current frame. i know i can use frame buffers and as far as i understand this would be the rendering process:
FPS means frames per second: the number of drawn frames in the last second. You don’t count the number of frames, but estimate it, based on the delta time. This works fine if you know that delta time remains the same for all frames, but this is usually not the case. That’s why we multiply for instance movement with delta time, because it can be different in each frame. For example, if your 1st frame takes 3ms, and the 2nd frame takes 5ms, you estimate about 333 FPS (1000/3) in the 1st frame, then 200 FPS (1000/5) in the 2nd frame. The 1st is higher than your monitor’s refresh rate, and the 2nd is lower. But both can’t be true at the same time, maybe you render 333 frames in the first second, maybe 200, maybe 240, or perhaps something else. You can’t tell based on 2 delta times. You have to actually count it: increment a variable at each frame, and reset it to 0 after each second.
Most monitors have a fixed refresh rate, let’s say 60Hz. This means, that every 1/60 second, it refreshes the screen, which is called the vertical blank. It draws the image by drawing the top row of pixels, then the row below, all the way down to the last row (that’s why it’s called vertical blank). It draws the rows pretty fast, so you can’t see the individual rows appearing, but still, it takes some time. On the other hand, the GPU doesn’t have this fixed refresh rate, when it finishes drawing (again, delta time changes from frame to frame), it switches between the old and the new image. If this switch happens during the vertical blank, your monitor draws some rows from one image, and then some from another which is called screen tearing (if your GPU is fast you can have multiple tears). To prevent screen tearing you can enable vertical synchronization (V-sync) that makes the GPU wait for the vertical blank, and switch images just before it (that’s why it’s called vertical synchronization, it syncs with the vertical blank). If your GPU misses the vertical blank, it’ll wait for the next one. This means that if your delta time is about 18ms, it means that you’ll have about 55 FPS without V-sync, but 30 FPS with V-sync. V-sync can get rid of screen tearing, but it can reduce FPS. So when V-sync is enabled, you can’t render more frames than your screen’s refresh rate.
Another thing to note is that WebGL tries to hide the async nature of the GPU. So when you call
drawArrays
, you might think that when the function returned, it finished the drawing. However, this is not the case. When you call those functions, the driver doesn’t execute them immediately, only puts the commands into a queue, and it’ll start to execute them later. In more modern APIs, like Vulkan, or WebGPU, you have an explicit queue (or even queues) where you can record commands, and start the execution. But WebGL tries to hide it from you. This means, that when the browser calls therender
function to create the next frame on the CPU side, the GPU might still working on the last frame. So frames are interleaved on the CPU and the GPU.