engine icon indicating copy to clipboard operation
engine copied to clipboard

Add 'target framerate' API

Open yaustar opened this issue 2 years ago • 7 comments

We should give users the ability to try to target a framerate that's independent from the monitor refresh rate.

Having the ability to cap at 60 FPS in the world where more and more monitors are now high refresh rate would really help some developers ensure a consistent experience between users.

Along the same lines, being able to target a lower framerate (eg 30fps) would help with reducing battery drain on mobile devices and also help the experience with users on lower end devices

yaustar avatar Aug 30 '22 08:08 yaustar

duplicate of https://github.com/playcanvas/engine/issues/3370

mvaligursky avatar Aug 30 '22 08:08 mvaligursky

Maybe this can be the opposite of @LeXXik's issue: lets say a player/user wants 125 FPS on a 60 Hz screen, is that possible somehow aswell?

Its a field called "trickjumping", that requires special FPS to work out: https://www.google.com/search?q=125+fps+trick+jumping

Probably it doesn't make sense to render 125 FPS on a 60 Hz screen, only the physics/player simulation would require that.

Its just that some engines knit render/physics together, and then I guess it would make sense for the sake of porting such game engines.

kungfooman avatar Aug 30 '22 09:08 kungfooman

Not sure if that's possible in the browser as we are limited by requestAnimation I believe? I don't think we can a smaller polling step than that @kungfooman

yaustar avatar Aug 30 '22 15:08 yaustar

Not sure if that's possible in the browser as we are limited by requestAnimation I believe?

I made a little script to test this out. First, using PlayCanvas Extras for pcx.MiniStats:

<script src="http://127.0.0.1/playcanvas-engine/build/playcanvas-extras.js"></script>
<script>
  const miniStats = new pcx.MiniStats(app);
</script>

Two little helper functions:

const originalRequestAnimationFrame = window.requestAnimationFrame;
function setTargetFPS(fps) {
  window.requestAnimationFrame = cb => setTimeout(cb, 1000 / fps);
}
function clearTargetFPS() {
  window.requestAnimationFrame = originalRequestAnimationFrame;
}

My default screen Hz is 60 Hz (1000 / 16.7ms ~= 59.88 FPS):

image

setTargetFPS(10);

image

setTargetFPS(200);

image

11ms is 1000 / 11ms ~= 90.91 FPS, which is the maximum FPS I can set using setTimeout. However, yesterday I made a little test via setInterval (without PlayCanvas "load"), and I calculated that 250 FPS should be possible (4ms minimum delay). So there is at least one way to aim for higher FPS through setTimeout and setInterval could be able to go for higher FPS.

To use requestAnimationFrame again:

clearTargetFPS();

kungfooman avatar Aug 31 '22 08:08 kungfooman

IIRC, setTimeout/setInterval isn't that precise? It be interesting to try regardless and see if the deltatime is stable.

I was reading this article about how they were managing their tick which what got me to write the ticket: https://blog.flevar.com/the-making-of-flevar?utm_source=pocket_mylist#heading-custom-function

I also wonder how often the input called be polled/if input events can be received faster than the monitor refresh rate.

yaustar avatar Aug 31 '22 08:08 yaustar

@yaustar Interesting, I just took makeTick and rewrote it a little bit, so it can be pasted into devtools and used with setInterval:

// static data
const _frameEndData = {};
function tickForInterval() {
    const application = app;
    const frame = undefined;
    const {now, math, Debug} = pc;
    const TRACEID_RENDER_FRAME = 'RenderFrame';
    if (!app.graphicsDevice)
        return;
    const currentTime = application._processTimestamp() || now();
    const ms = currentTime - (application._time || currentTime);
    let dt = ms / 1000.0;
    dt = math.clamp(dt, 0, application.maxDeltaTime);
    dt *= application.timeScale;
    application._time = currentTime;
    if (application.graphicsDevice.contextLost)
        return;
    application._fillFrameStatsBasic(currentTime, dt, ms);
    // #if _PROFILER
    application._fillFrameStats();
    // #endif
    application._inFrameUpdate = true;
    application.fire("frameupdate", ms);
    if (frame) {
        application.xr?.update(frame);
        application.graphicsDevice.defaultFramebuffer = frame.session.renderState.baseLayer.framebuffer;
    } else {
        application.graphicsDevice.defaultFramebuffer = null;
    }
    application.update(dt);
    application.fire("framerender");
    Debug.trace(TRACEID_RENDER_FRAME, `--- Frame ${application.frame}`);
    if (application.autoRender || application.renderNextFrame) {
        application.updateCanvasSize();
        application.render();
        application.renderNextFrame = false;
    }
    // set event data
    _frameEndData.timestamp = now();
    _frameEndData.target = application;
    application.fire("frameend", _frameEndData);
    application._inFrameUpdate = false;
    if (application._destroyRequested) {
        application.destroy();
    }
}

It can be called like this (first line is killing the default tick):

app.tick = () => {};
lastId = setInterval(tickForInterval, 100);

Successive calls need to clear the interval id:

clearInterval(id);
lastId = setInterval(tickForInterval, 0.1);

I am still stuck at 90 FPS, but maybe your Web Worker idea could help with a higher FPS (and a precisely timed lower FPS).

I can also imagine that every browser does these things a little bit different implementation-wise.

It is very hard for me to tell the difference between "setTimeout 60 FPS" and "requestAnimationFrame 60 FPS", but anything lower (lets say 50 FPS) feels immediately quite clunky, at least when the game is a bit fast-paced (I am testing in a first-person shooter).

kungfooman avatar Aug 31 '22 10:08 kungfooman

rAF - ensures to run a callback as close as possible before screen refresh, ensuring least latency from JS to actual screen. Anything related to rendering, should use rAF. Best way to limit framerate reliably, would be by skipping application update and render inside of some rAF callbacks, but not by using timeouts and intervals.

Maksims avatar Sep 03 '22 20:09 Maksims