TL;博士
即使根本不进行绘图,在 Android 设备上的 OpenGL ES 渲染线程上保持 60Hz 的更新率似乎也是不可能的。神秘的尖峰经常出现(在底部的代码中显示),我为弄清楚为什么或如何导致死胡同所做的一切努力。在使用自定义渲染线程的更复杂示例中,时间始终显示 eglSwapBuffers() 是罪魁祸首,通常超过 17ms-32ms。帮助?
更多细节
这尤其糟糕,因为我们项目的渲染要求是屏幕对齐的元素以固定的高速水平从屏幕一侧平滑滚动到另一侧。换句话说,一个平台游戏。从 60Hz 频繁下降会导致明显的爆裂和倾斜,无论有无基于时间的运动。由于滚动速度很高,因此不能选择以 30Hz 渲染,这是设计中不可协商的部分。
我们的项目是基于 Java 的,以最大限度地提高兼容性并使用 OpenGL ES 2.0。我们只深入了解 API 7-8 设备上的 OpenGL ES 2.0 渲染和 API 7 设备上的 ETC1 支持的 NDK。在它和下面给出的测试代码中,除了我无法控制的日志打印和自动线程之外,我没有验证分配/GC 事件。
我已经在使用库存 Android 类且没有 NDK 的单个文件中重新创建了该问题。下面的代码可以粘贴到在 Eclipse 中创建的新 Android 项目中,只要您选择 API 级别 8 或更高级别,它就应该可以开箱即用。
该测试已在具有一系列 GPU 和操作系统版本的各种设备上重现:
- 银河标签 10.1 (Android 3.1)
- Nexus S (Android 2.3.4)
- 银河 S II (Android 2.3.3)
- XPERIA Play (Android 2.3.2)
- 机器人不可思议 (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();
}