Android EGL/OpenGL ES Frame Rate Stuttering

TL;DR

Even when doing no drawing at all, it seems impossible to maintain a 60Hz update rate on an OpenGL ES rendering thread on an Android device. Mysterious spikes frequently crop up (demonstrated in the code at bottom), and every effort that I've made to figure out why or how has lead to a dead end. Timing in more complicated examples with a custom rendering thread has consistently shown eglSwapBuffers() to be the culprit, frequently coming in over 17ms-32ms. Help?

More Details

This is particularly damning because the rendering requirements for our project is screen-aligned elements smoothly scrolling horizontally at a fixed, high rate of speed from one side of the screen to the other. In other words, a platforming game. The frequent drops from 60Hz result in noticeable popping and lurching, both with and without time-based movement. Rendering at 30Hz isn't an option because of the high rate of scrolling speed, which is a non-negotiable part of the design.

Our project is Java-based to maximize compatibility and uses OpenGL ES 2.0. We only dip down into the NDK for OpenGL ES 2.0 rendering on API 7-8 devices and ETC1 support on API 7 devices. In both it and the test code given below, I have verified no allocations/GC events except for the log print and automatic threads beyond my control.

I've recreated the problem in a single file that uses stock Android classes and no NDK. The code below can be pasted into a new Android project created in Eclipse and should pretty much work out-of-the-box so long as you choose API level 8 or above.

The test has been reproduced on a variety of devices with a range of GPUs and OS versions:

  • Galaxy Tab 10.1 (Android 3.1)
  • Nexus S (Android 2.3.4)
  • Galaxy S II (Android 2.3.3)
  • XPERIA Play (Android 2.3.2)
  • Droid Incredible (Android 2.2)
  • Galaxy S (Android 2.1-update1) (when dropping API requirements down to level 7)
  • Sample output (gathered from under 1 second of run time):

    Spike: 0.017554
    Spike: 0.017767
    Spike: 0.018017
    Spike: 0.016855
    Spike: 0.016759
    Spike: 0.016669
    Spike: 0.024925
    Spike: 0.017083999
    Spike: 0.032984
    Spike: 0.026052998
    Spike: 0.017372
    

    I've been chasing this one for a while and have about hit a brick wall. If a fix isn't available, then at least an explanation about why this happens and advice on how this has been overcome in projects with similar requirements would be greatly appreciated.

    Example Code

    package com.test.spikeglsurfview;
    
    import javax.microedition.khronos.egl.EGLConfig;
    import javax.microedition.khronos.opengles.GL10;
    
    import android.app.Activity;
    import android.opengl.GLSurfaceView;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.LayoutInflater;
    import android.view.Window;
    import android.view.WindowManager;
    import android.widget.LinearLayout;
    
    /**
     * A simple Activity that demonstrates frequent frame rate dips from 60Hz,
     * even when doing no rendering at all.
     * 
     * This class targets API level 8 and is meant to be drop-in compatible with a
     * fresh auto-generated Android project in Eclipse.
     * 
     * This example uses stock Android classes whenever possible.
     * 
     * @author Bill Roeske
     */
    public class SpikeActivity extends Activity
    {
        @Override
        public void onCreate( Bundle savedInstanceState )
        {
            super.onCreate( savedInstanceState );
    
            // Make the activity fill the screen.
            requestWindowFeature( Window.FEATURE_NO_TITLE );
            getWindow().setFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN, 
                                  WindowManager.LayoutParams.FLAG_FULLSCREEN );
    
            // Get a reference to the default layout.
            final LayoutInflater factory = getLayoutInflater();
            final LinearLayout layout = (LinearLayout)factory.inflate( R.layout.main, null );
    
            // Clear the layout to remove the default "Hello World" TextView.
            layout.removeAllViews();
    
            // Create a GLSurfaceView and add it to the layout.
            GLSurfaceView glView = new GLSurfaceView( getApplicationContext() );
            layout.addView( glView );
    
            // Configure the GLSurfaceView for OpenGL ES 2.0 rendering with the test renderer.
            glView.setEGLContextClientVersion( 2 );
            glView.setRenderer( new SpikeRenderer() );
    
            // Apply the modified layout to this activity's UI.
            setContentView( layout );
        }
    }
    
    class SpikeRenderer implements GLSurfaceView.Renderer
    {
        @Override
        public void onDrawFrame( GL10 gl )
        {
            // Update base time values.
            final long  timeCurrentNS = System.nanoTime();
            final long  timeDeltaNS = timeCurrentNS - timePreviousNS;
            timePreviousNS = timeCurrentNS;
    
            // Determine time since last frame in seconds.
            final float timeDeltaS = timeDeltaNS * 1.0e-9f;
    
            // Print a notice if rendering falls behind 60Hz.
            if( timeDeltaS > (1.0f / 60.0f) )
            {
                Log.d( "SpikeTest", "Spike: " + timeDeltaS );
            }
    
            /*// Clear the screen.
            gl.glClear( GLES20.GL_COLOR_BUFFER_BIT );*/
        }
    
        @Override
        public void onSurfaceChanged( GL10 gl, int width, int height )
        {
        }
    
        @Override
        public void onSurfaceCreated( GL10 gl, EGLConfig config )
        {
            // Set clear color to purple.
            gl.glClearColor( 0.5f, 0.0f, 0.5f, 1.0f );
        }
    
        private long timePreviousNS = System.nanoTime();
    }
    

    Not sure if this is the answer, but note that the call to eglSwapBuffers() blocks for at least 16 ms, even if there is nothing to draw.

    Running the game logic in a separate thread could win back some time.

    Check out the blog post at the open source Platforming game Relica Island. The game logic is heavy, yet the framrate is smooth due to the authors pipeline/double buffer solution.

    http://replicaisland.blogspot.com/2009/10/rendering-with-two-threads.html


    You are not binding the framerate yourself. So it's bound by CPU.

    Which means that it will run fast when the CPU has nothing to do and slower when it's doing other stuff.

    Other stuff is running on your phone, which occasionally takes CPU time from your game. Garbage Collector will also to this, although not freezing your game like it used to do (since it now runs in a separate thread)

    You would see this happen on any multiprogramming operating system that was in normal use. It's not just Java's fault.

    My suggestion: bind the framerate to a lower value if constant bitrate is required. Hint: use a mix of Thread.sleep() and busy waiting (while loop), since sleep might go over the required waiting time.

    链接地址: http://www.djcxy.com/p/64636.html

    上一篇: GLES20.glClear(GLES20.GL)是否有其他选择?

    下一篇: Android EGL / OpenGL ES帧速率口吃