22

I have a C++ game running through JNI in Android. The frame rate varies from about 20-45fps due to scene complexity. Anything above 30fps is silly for the game; it's just burning battery. I'd like to limit the frame rate to 30 fps.

  • I could switch to RENDERMODE_WHEN_DIRTY, and use a Timer or ScheduledThreadPoolExecutor to requestRender(). But that adds a whole mess of extra moving parts that might or might not work consistently and correctly.
  • I tried injecting Thread.sleep() when things are running quickly, but this doesn't seem to work at all for small time values. And it may just be backing events into the queue anyway, not actually pausing.

Is there a "capFramerate()" method hiding in the API? Any reliable way to do this?

grinliz
  • 580
  • 1
  • 4
  • 8

6 Answers6

40

The solution from Mark is almost good, but not entirely correct. The problem is that the swap itself takes a considerable amount of time (especially if the video driver is caching instructions). Therefore you have to take that into account or you'll end with a lower frame rate than desired. So the thing should be:

somewhere at the start (like the constructor):

startTime = System.currentTimeMillis();

then in the render loop:

public void onDrawFrame(GL10 gl)
{    
    endTime = System.currentTimeMillis();
    dt = endTime - startTime;
    if (dt < 33)
        Thread.Sleep(33 - dt);
    startTime = System.currentTimeMillis();

    UpdateGame(dt);
    RenderGame(gl);
}

This way you will take into account the time it takes to swap the buffers and the time to draw the frame.

Fili
  • 515
  • 5
  • 2
  • Nice Fili! I'm a little embarrassed I missed that, but your approach works well. Thank you. – grinliz Jun 12 '11 at 03:40
  • 4
    With this code, you don't have a constant FPS. On my game, i oscillate between 30 and 20 fps. The real method is an EGL eglSwapInterval call but it is not implemented on Android devices. – Ellis Aug 29 '11 at 09:57
  • @Ellis: it's strange, I've used this code in commercial games and I have a constant FPS. Also make sure that when measuring FPS you should make a mean of last 5-10 frames for example so that you eliminate any jerkiness (sometimes the phone does some background tasks like receiving stuff, checking mail, etc). – Fili Jan 03 '12 at 09:57
  • My FPS counter is an average on the last second. When there is a service, it is possible to have jerkiness but this is not my real problem. To have a good animation, i want (as on consoles) render 1 image at each two screen sync (as the old VBL). On Android you can't ... that's a shame. – Ellis Jan 09 '12 at 12:45
  • This loop is CAPED on 30 FPS, meaning it only limits your game to 30 FPS, it doesn't magically boost slow code to that speed. If your code is already running sub-30 FPS in some scenes, obviously, you aren't going to get constant 30 FPS rate. – Davor Jul 01 '12 at 00:12
  • 8
    This code is fundamentally flawed; what happens to the time which occurs between end/start time acquisitions? In order to prevent time leakage, the current end time should always be employed as the subsequent start time. This has ramifications for both FPS fidelity and multi-player sync. – Taliadon Jan 18 '13 at 02:37
  • 1
    @Taliadon: Are you suggesting that the sleep be included in the calculation of dt? That doesn't make sense, because you're trying to calculate the amount of time you need to sleep. What it should actually do is keep a continuous count of the point in time it needs to render - i.e. adding 33 to a variable each time - and using that to calculate the necessary sleep time. Then you have to do something if you fall behind though. – KNfLrPn Oct 16 '13 at 06:26
  • 5
    @KNfLrPn: Taliadon is saying `startTime = System.currentTimeMillis();` is wrong and that it should instead be `startTime = endTime;`. – kaka Mar 18 '14 at 22:09
  • I think Taladion's proposal is only valid if you want to average 30fps, so if you get a couple of slow frames, say 20fps, it will then try to run as fast as possible to make up for it. That isn't what the OP asked for and might even make the game appear less evenly paced. – realh Jun 27 '17 at 20:13
  • Doing this for me ends up with a blocked thread. This solution causes render blocking, and maybe ANR in the end (everything was initialized correctly on my end, it still blocks the thread) – Zoe Jun 28 '17 at 08:56
  • I run a game that varies the viewdepth to keep the fps around 60. This is the code I use to futureproof my game in case of 60+ framerates at my set maximum viewdepth with phones down the road. I had to use 'sleep()' with a small 's' and put it in a try block for android studio to accept. I'd also recommend using SystemClock.elapsedRealtime() for better reliability. In response to Taliadon, the sleeping actually moves your endTime forward, so you are still setting the startTime to the 'new' endTime. If you set the startTime to the time before the sleep, it will only sleep every other tic. – Androidcoder Jul 01 '22 at 18:58
9

When using GLSurfaceView, you perform the drawing in your Renderer's onDrawFrame which is handled in a separate thread by the GLSurfaceView. Simply make sure that each call to onDrawFrame takes (1000/[frames]) milliseconds, in your case something like 33ms.

To do this: (in your onDrawFrame)

  1. Measure the current time before your start drawing using System.currentTimeMillis (Let's call it startTime)
  2. Perform the drawing
  3. Measure time again (Let's call it endTime)
  4. deltaT = endTime - starTime
  5. if deltaT < 33, sleep (33-deltaT)

That's it.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
Lior
  • 7,845
  • 2
  • 34
  • 34
  • 1
    Understood. But how do I control when glSwap is happening? Android is calling onDrawFrame for me. I'm not aware of any controls to adjust when that occurs. (Although I hope they are there.) RENDERMODE_WHEN_DIRTY disables the behavior, but introduces new timing complexity. In either mode, Android is calling the glSwap after the onDrawFrame so calling onDrawFrame myself doesn't actually coordinate with the video update. (And in fact creates video display chaos. I've tried.) And since onDrawFrame is abstract void, obviously there isn't any super class behavior to rely on. – grinliz Jan 23 '11 at 18:58
  • You don't need to control it. GLSurfaceView handles that for you. And you don't have to call onDrawFrame yourself. Your question was how you control and limit the frame rate, and the solution I suggested does exactly that. What else are you missing? – Lior Jan 27 '11 at 12:03
3

Fili's answer looked great to me, bad sadly limited the FPS on my Android device to 25 FPS, even though I requested 30. I figured out that Thread.sleep() works not accurately enough and sleeps longer than it should.

I found this implementation from the LWJGL project to do the job: https://github.com/LWJGL/lwjgl/blob/master/src/java/org/lwjgl/opengl/Sync.java

Community
  • 1
  • 1
tyrondis
  • 3,364
  • 7
  • 32
  • 56
0

Fili's solution is failing for some people, so I suspect it's sleeping until immediately after the next vsync instead of immediately before. I also feel that moving the sleep to the end of the function would give better results, because there it can pad out the current frame before the next vsync, instead of trying to compensate for the previous one. Thread.sleep() is inaccurate, but fortunately we only need it to be accurate to the nearest vsync period of 1/60s. The LWJGL code tyrondis posted a link to seems over-complicated for this situation, it's probably designed for when vsync is disabled or unavailable, which should not be the case in the context of this question.

I would try something like this:

private long lastTick = System.currentTimeMillis();

public void onDrawFrame(GL10 gl)
{
    UpdateGame(dt);
    RenderGame(gl);

    // Subtract 10 from the desired period of 33ms to make generous
    // allowance for overhead and inaccuracy; vsync will take up the slack
    long nextTick = lastTick + 23;
    long now;
    while ((now = System.currentTimeMillis()) < nextTick)
        Thread.sleep(nextTick - now);
    lastTick = now;
}
realh
  • 962
  • 2
  • 7
  • 22
0

If you don't want to rely on Thread.sleep, use the following

double frameStartTime = (double) System.nanoTime()/1000000;
// start time in milliseconds
// using System.currentTimeMillis() is a bad idea
// call this when you first start to draw

int frameRate = 30;
double frameInterval = (double) 1000/frame_rate;
// 1s is 1000ms, ms is millisecond
// 30 frame per seconds means one frame is 1s/30 = 1000ms/30

public void onDrawFrame(GL10 gl)
{
  double endTime = (double) System.nanoTime()/1000000;
  double elapsedTime = endTime - frameStartTime;

  if (elapsed >= frameInterval)
  {
    // call GLES20.glClear(...) here

    UpdateGame(elapsedTime);
    RenderGame(gl);

    frameStartTime += frameInterval;
  }
}
Confuse
  • 5,646
  • 7
  • 36
  • 58
-4

You may also try and reduce the thread priority from onSurfaceCreated():

Process.setThreadPriority(Process.THREAD_PRIORITY_LESS_FAVORABLE);
olivierg
  • 10,200
  • 4
  • 30
  • 33
  • 1
    How does that accomplish what the OP asks, which is to lower the framerate to a controlled amount (but only when it is higher than desired)? IMHO, all your suggestion will do is UNPREDICTABLY lower the framerate. Likely the result will be ERRATIC, as it makes the process more interruptable. – ToolmakerSteve Jul 19 '14 at 21:13
  • 1
    This is likely to do nothing at all on devices with modern multi-core CPUs. Thread priority affects which task the kernel's scheduler picks. If the number of runnable threads is <= the number of active CPU cores, then all runnable threads will be running regardless of priority. – Brian Johnson Feb 20 '16 at 06:38