开发者

How to limit framerate when using Android's GLSurfaceView.RENDERMODE_CONTINUOUSLY?

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开发者_如何学运维?


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.


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.


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


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;
}


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;
  }
}


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

Process.setThreadPriority(Process.THREAD_PRIORITY_LESS_FAVORABLE);
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜