Home | History | Annotate | Download | only in graphics
      1 <html devsite>
      2   <head>
      3     <title>Game Loops</title>
      4     <meta name="project_path" value="/_project.yaml" />
      5     <meta name="book_path" value="/_book.yaml" />
      6   </head>
      7   <body>
      8   <!--
      9       Copyright 2017 The Android Open Source Project
     10 
     11       Licensed under the Apache License, Version 2.0 (the "License");
     12       you may not use this file except in compliance with the License.
     13       You may obtain a copy of the License at
     14 
     15           http://www.apache.org/licenses/LICENSE-2.0
     16 
     17       Unless required by applicable law or agreed to in writing, software
     18       distributed under the License is distributed on an "AS IS" BASIS,
     19       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     20       See the License for the specific language governing permissions and
     21       limitations under the License.
     22   -->
     23 
     24 
     25 
     26 <p>A very popular way to implement a game loop looks like this:</p>
     27 
     28 <pre class="devsite-click-to-copy">
     29 while (playing) {
     30     advance state by one frame
     31     render the new frame
     32     sleep until its time to do the next frame
     33 }
     34 </pre>
     35 
     36 <p>There are a few problems with this, the most fundamental being the idea that the
     37 game can define what a "frame" is.  Different displays will refresh at different
     38 rates, and that rate may vary over time.  If you generate frames faster than the
     39 display can show them, you will have to drop one occasionally.  If you generate
     40 them too slowly, SurfaceFlinger will periodically fail to find a new buffer to
     41 acquire and will re-show the previous frame.  Both of these situations can
     42 cause visible glitches.</p>
     43 
     44 <p>What you need to do is match the display's frame rate, and advance game state
     45 according to how much time has elapsed since the previous frame.  There are two
     46 ways to go about this: (1) stuff the BufferQueue full and rely on the "swap
     47 buffers" back-pressure; (2) use Choreographer (API 16+).</p>
     48 
     49 <h2 id=stuffing>Queue stuffing</h2>
     50 
     51 <p>This is very easy to implement: just swap buffers as fast as you can.  In early
     52 versions of Android this could actually result in a penalty where
     53 <code>SurfaceView#lockCanvas()</code> would put you to sleep for 100ms.  Now
     54 it's paced by the BufferQueue, and the BufferQueue is emptied as quickly as
     55 SurfaceFlinger is able.</p>
     56 
     57 <p>One example of this approach can be seen in <a
     58 href="https://code.google.com/p/android-breakout/">Android Breakout</a>.  It
     59 uses GLSurfaceView, which runs in a loop that calls the application's
     60 onDrawFrame() callback and then swaps the buffer.  If the BufferQueue is full,
     61 the <code>eglSwapBuffers()</code> call will wait until a buffer is available.
     62 Buffers become available when SurfaceFlinger releases them, which it does after
     63 acquiring a new one for display.  Because this happens on VSYNC, your draw loop
     64 timing will match the refresh rate.  Mostly.</p>
     65 
     66 <p>There are a couple of problems with this approach.  First, the app is tied to
     67 SurfaceFlinger activity, which is going to take different amounts of time
     68 depending on how much work there is to do and whether it's fighting for CPU time
     69 with other processes.  Since your game state advances according to the time
     70 between buffer swaps, your animation won't update at a consistent rate.  When
     71 running at 60fps with the inconsistencies averaged out over time, though, you
     72 probably won't notice the bumps.</p>
     73 
     74 <p>Second, the first couple of buffer swaps are going to happen very quickly
     75 because the BufferQueue isn't full yet.  The computed time between frames will
     76 be near zero, so the game will generate a few frames in which nothing happens.
     77 In a game like Breakout, which updates the screen on every refresh, the queue is
     78 always full except when a game is first starting (or un-paused), so the effect
     79 isn't noticeable.  A game that pauses animation occasionally and then returns to
     80 as-fast-as-possible mode might see odd hiccups.</p>
     81 
     82 <h2 id=choreographer>Choreographer</h2>
     83 
     84 <p>Choreographer allows you to set a callback that fires on the next VSYNC.  The
     85 actual VSYNC time is passed in as an argument.  So even if your app doesn't wake
     86 up right away, you still have an accurate picture of when the display refresh
     87 period began.  Using this value, rather than the current time, yields a
     88 consistent time source for your game state update logic.</p>
     89 
     90 <p>Unfortunately, the fact that you get a callback after every VSYNC does not
     91 guarantee that your callback will be executed in a timely fashion or that you
     92 will be able to act upon it sufficiently swiftly.  Your app will need to detect
     93 situations where it's falling behind and drop frames manually.</p>
     94 
     95 <p>The "Record GL app" activity in Grafika provides an example of this.  On some
     96 devices (e.g. Nexus 4 and Nexus 5), the activity will start dropping frames if
     97 you just sit and watch.  The GL rendering is trivial, but occasionally the View
     98 elements get redrawn, and the measure/layout pass can take a very long time if
     99 the device has dropped into a reduced-power mode.  (According to systrace, it
    100 takes 28ms instead of 6ms after the clocks slow on Android 4.4.  If you drag
    101 your finger around the screen, it thinks you're interacting with the activity,
    102 so the clock speeds stay high and you'll never drop a frame.)</p>
    103 
    104 <p>The simple fix was to drop a frame in the Choreographer callback if the current
    105 time is more than N milliseconds after the VSYNC time.  Ideally the value of N
    106 is determined based on previously observed VSYNC intervals.  For example, if the
    107 refresh period is 16.7ms (60fps), you might drop a frame if you're running more
    108 than 15ms late.</p>
    109 
    110 <p>If you watch "Record GL app" run, you will see the dropped-frame counter
    111 increase, and even see a flash of red in the border when frames drop.  Unless
    112 your eyes are very good, though, you won't see the animation stutter.  At 60fps,
    113 the app can drop the occasional frame without anyone noticing so long as the
    114 animation continues to advance at a constant rate.  How much you can get away
    115 with depends to some extent on what you're drawing, the characteristics of the
    116 display, and how good the person using the app is at detecting jank.</p>
    117 
    118 <h2 id=thread>Thread management</h2>
    119 
    120 <p>Generally speaking, if you're rendering onto a SurfaceView, GLSurfaceView, or
    121 TextureView, you want to do that rendering in a dedicated thread.  Never do any
    122 "heavy lifting" or anything that takes an indeterminate amount of time on the
    123 UI thread.</p>
    124 
    125 <p>Breakout and "Record GL app" use dedicated renderer threads, and they also
    126 update animation state on that thread.  This is a reasonable approach so long as
    127 game state can be updated quickly.</p>
    128 
    129 <p>Other games separate the game logic and rendering completely.  If you had a
    130 simple game that did nothing but move a block every 100ms, you could have a
    131 dedicated thread that just did this:</p>
    132 
    133 <pre class="devsite-click-to-copy">
    134     run() {
    135         Thread.sleep(100);
    136         synchronized (mLock) {
    137             moveBlock();
    138         }
    139     }
    140 </pre>
    141 
    142 <p>(You may want to base the sleep time off of a fixed clock to prevent drift --
    143 sleep() isn't perfectly consistent, and moveBlock() takes a nonzero amount of
    144 time -- but you get the idea.)</p>
    145 
    146 <p>When the draw code wakes up, it just grabs the lock, gets the current position
    147 of the block, releases the lock, and draws.  Instead of doing fractional
    148 movement based on inter-frame delta times, you just have one thread that moves
    149 things along and another thread that draws things wherever they happen to be
    150 when the drawing starts.</p>
    151 
    152 <p>For a scene with any complexity you'd want to create a list of upcoming events
    153 sorted by wake time, and sleep until the next event is due, but it's the same
    154 idea.</p>
    155 
    156   </body>
    157 </html>
    158