Android: Understanding OnDrawFrame, FPS and VSync (OpenGL ES 2.0)
Asked Answered
C

1

13

For a while now I've experienced an intermittent 'stuttering' of the sprites that are in motion within my Android Game. It's a fiarly simple 2D OpenGL ES 2.0 game. (This is an ongoing problem which I have re-visited many times).

In my game loop, I have 2 'timers' - one which will log the number of frames in the previous second, and another which counts the time (in Milliseconds) from the end of the current onDrawFrame iteration to the start of the next.

This is what I've found:

When not rendering anything, I get 60fps (for the most part), and every time onDrawFrame is called, it reports itself as taking longer that 16.667ms. Now, If I render something (doesn't matter if it's 1 quad or 100 quads, the result is the same), I get 60fps (for the most part) but now, only about 20% of onDrawFrame calls report themselves as taking longer than 16.667ms from the last call.

I don't really understand why this happens, firstly, why, when onDrawFrame isn't doing anything, is it called so 'slowly' - and more importantly, why does any GL call (one simple quad), still make the time between onDrawFrame calls longer than 16.667ms (albeit much less frequently).

I should say that when onDrawFrame reports taking longer than 16.667ms from the last iteration, it is almost always accompanied by a drop in FPS (to 58 or 59), but not all of the time, sometimes, the FPS stays constant. And conversely, sometimes when the FPS drops, onDrawFrame is called within 16.667ms of the last iteration completing.

So......

I'm trying to fix my game-loop and eradicate these 'stutters' - some other things to note:

  • When I do method profiling, it shows glSwapBuffers, sometimes taking a long time
  • When I do a GL Trace, most scenes its says renders in less than 1ms, but sometimes the odd frame takes 3.5-4ms - same scene. Nothing changes apart from the time it takes
  • Almost every time a frame is dropped, or onDrawFrame reports a long delay (or both), there is a visual glitch, but not every time. Big visual glitches seems to coincide with multiple 'delayed' onDrawFrame calls and /or dropped frames.
  • I don't think this is a scene complexity issue for 2 reasons: 1) even if I render my scene twice, it doesn't make the problem any worse, I still for the most part, get 60FPS with the occasional drop, just as before and 2), even if I strip the scene bare, I still get the problem.

I obviously am misunderstanding something, so a push in the right direction would be appreciated.

OnDrawFrame

@Override
public void onDrawFrame(GL10 gl) {

    startTime = System.nanoTime();        
    fps++;                        
    totalTime = System.nanoTime() - timeSinceLastCalled;    

    if (totalTime > 16667000) {     
        Log.v("Logging","Time between onDrawFrame calls: " + (totalTime /(double)1000000));
    }

    //Grab time
    newTime = System.currentTimeMillis() * 0.001;
    frameTime = newTime - currentTime; //Time the last frame took

    if (frameTime > 0.25)
        frameTime = 0.25;

    currentTime = newTime;
    accumulator += frameTime;

    while (accumulator >= dt){              
      saveGameState();
      updateLogic();
      accumulator -= dt;
    }

    interpolation = (float) (accumulator / dt);

    Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);

    render(interpolation);

    if (startTime > lastSecond + 1000000000) {          
        lastSecond = startTime;
        Log.v("Logging","fps: "+fps);
        fps=0;          
    }

    endTime = startTime;
    timeSinceLastCalled = System.nanoTime();        
}

This game loop above is the one featured in this excellent article.

Croup answered 21/7, 2016 at 19:36 Comment(13)
Are the glitches coinciding with garbage collection events?Tell
@ReubenScratton, no, all objects are created up-front, there are no allocations in the loop and no GC. This seems to be a timing issue with the game-loop itself (I could be wrong, but it seems that way). ThanksCroup
Assuming you use a dedicated render thread, how do you sync state between it and the main thread? (FWIW I never use a dedicated render thread and prefer to be single-threaded, but then I've never written a complex game)Tell
Hi @ReubenScratton, if you mean do I update my logic on a separate thread to that which I render on, then no, I do rendering and logic updating on the same thread (GL Thread) - I take input from the main / UI thread, but this problem occurs regardless of whether input is being received or not. it's a really simple 2D game so I wanted to keep things as simple as possible - thanks!Croup
Might be nothing but "System.currentTimeMillis()*0.001" is multiplying a very large number by a very small one, something that's notorious for inaccurate results.Tell
Where does 'dt' come from and what are typical values? Could that inner loop where it's used be running many more times than you think?Tell
@ReubenScratton, dt is 1/60 (60 being ticks per second) - as I use a fixed time-step - the inner while loop is supposed to run when the loop can't render in time and needs to 'catch up' - hence keeping the game running at a constant 60fps, it does run twice occasionally, which is odd in itself (as the rendering is so basic, and I've confirmed that I am getting onDrawFrame being called 60 times per second (for the most part).Croup
Is there any typo in your code above? The if (frameTime > 0.25) actually affects only the statement frameTime = 0.25; however your indentation suggests otherwise.Paver
Well spotted, that was a formatting error @Paver - correctedCroup
Bear in mind the display doesn't run at exactly 60fps, and you are on a device that has other things running. You want to base delta-time on differences in timestamps from Choreographer, not an arbitrary fixed value. You can do reasonably well with the onDrawFrame() start time, but as mentioned in the article linked from my answer, that's based on queue back-pressure rather than VSYNC, and will vary a bit. That said, I used onDrawFrame() times in Android Breakout (github.com/fadden/android-breakout) and it looked fine.Intricacy
You say that your frame is slow but you measure the time between onDrawFrame calls and actually from the end of one frame to the start of the next one. For that duration to be 1/60 your code should take zero time. If your code takes non-zero time this value should always be < 1/60. The fact that you observe larger values (how large exactly?) means that the system just delays calling your code, not that your frame is slow. Some frames are expected to take longer (e.g. the first frame drawn by GL seems to be slower), but I would expect a glitch would probably have to do with wrong integrationPaver
Well, @c.s., the time seems to sometimes take up to 23+ms, and when it does, it correlates with a visible suttter - this can be regardless of how complicated the scene is. Re integration, I assume you're referring to how I move my sprites? I do so like this: yPos+=velocity*delta; and then calculate where to draw like so: yDrawPos=yPos*height; (or width when calculating X position - this refers to the width/height of the screen area). I also interpolate at render time. I use a fixed delta which is 1/ticks_per_second (and ticks_per_second is 60). ThanksCroup
By integration yes I mean the combination of sprite movement and interpolation. The thing is that even with a 23ms frame duration (this suggests a frame rate > 40fps) you should not be able to observe a glitch. So something else must be the culprit. If this value refers to duration between frames, it is the system that delays calling your code which I suspect is more difficult to troubleshoot. So I would first try to prove that my code is correct before taking the "why the system delays" path (unless of course it is something obvious like garbage collection)Paver
I
9

Some thoughts:

  • Don't use System.currentTimeMillis() for timing things. It's based on the wall clock, which can be updated by the network. Use System.nanoTime(), which is based off the monotonic clock.
  • See this appendix for some notes on game loops. Queue-stuffing is fine for many things, but understand that you're not exactly working off of VSYNC, so timings will tend to be inaccurate.
  • Some devices (notably those based on qcom SOCs) reduce CPU speed when they think they're idle. Always take timings while actively moving your finger around on the touch screen.
  • If you want to debug frame rate issues you need to be using systrace. The traceview profiling isn't that useful here.

See Grafika's "record GL app" Activity for an example of a simple GLES app that drops frames, but adjusts the animation such that it's rarely noticeable.

Intricacy answered 21/7, 2016 at 20:0 Comment(2)
The appendix link is broken (well, only the hash part) - source.android.com/devices/graphics/arch-gameloops seems to be the right one now.Lionhearted
@Karu: They split the document into multiple parts and rearranged some stuff, so there are a bunch of answers with broken links. :-( I've updated this one. If you see others you're more than welcome to edit the posts directly.Intricacy

© 2022 - 2024 — McMap. All rights reserved.