Android EGL / OpenGL ES帧率口吃

时间:2011-08-29 21:48:40

标签: android opengl-es-2.0 egl

TL; DR

即使根本不进行绘图,似乎也无法在Android设备上的OpenGL ES渲染线程上保持60Hz的更新速率。神秘的尖峰经常出现(在底部的代码中展示),我所做的每一项努力都弄清楚为什么或如何导致死胡同。使用自定义渲染线程在更复杂的示例中进行定时一直表明eglSwapBuffers()是罪魁祸首,经常超过17ms-32ms。帮助

更多详情

这尤其令人讨厌,因为我们项目的渲染要求是屏幕对齐元素,从屏幕的一侧到另一侧以固定的高速率平滑地水平滚动。换句话说,一个平台游戏。从60Hz频繁下降会导致明显的弹跳和晃动,无论是否有基于时间的移动。由于滚动速度很高,因此在30Hz下渲染不是一个选项,这是设计中不可协商的部分。

我们的项目是基于Java的,以最大限度地提高兼容性并使用OpenGL ES 2.0。我们只深入了解NDK,用于API 7-8设备上的OpenGL ES 2.0渲染和API 7设备上的ETC1支持。在它和下面给出的测试代码中,我已经验证了除了日志打印和我无法控制的自动线程之外没有分配/ GC事件。

我在使用股票Android类而没有NDK的单个文件中重新创建了问题。下面的代码可以粘贴到Eclipse中创建的新Android项目中,只要您选择API级别8或更高版本,它就可以开箱即用。

该测试已在具有各种GPU和OS版本的各种设备上重现:

  • 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)(将API要求降至7级时)

示例输出(从运行时间不到1秒收集):

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

我一直在追逐这一个并且碰到了一堵砖墙。如果修复程序不可用,那么至少可以解释为什么会出现这种情况,以及在具有类似要求的项目中如何克服这一问题的建议将非常感激。

示例代码

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

2 个答案:

答案 0 :(得分:3)

不确定这是否是答案,但请注意,对eglSwapBuffers()的调用会阻塞至少16 ms,即使没有任何内容可以绘制。

在一个单独的线程中运行游戏逻辑可以赢回一段时间。

查看开源平台游戏Relica Island上的博客文章。游戏逻辑虽然很重,但由于作者管道/双缓冲解决方案,这种方法很顺利。

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

答案 1 :(得分:0)

您自己没有绑定帧率。所以它受CPU限制。

这意味着当CPU无事可做时它会快速运行,而当它正在做其他事情时它会慢一些。

手机上正在运行其他内容,这有时会占用游戏的CPU时间。垃圾收集器也会这样,虽然没有像过去那样冻结你的游戏(因为它现在在一个单独的线程中运行)

您会在正常使用的任何多道程序设计操作系统上看到这种情况。这不仅仅是Java的错误。

我的建议:如果需要恒定的比特率,将帧速率绑定到较低的值。 提示:使用Thread.sleep()和忙等待(while循环)的混合,因为睡眠可能会超过所需的等待时间。