Android EGL/OpenGL ES 帧率卡顿

发布于 2024-12-01 18:31:34 字数 4327 浏览 1 评论 0原文

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 和操作系统版本的各种设备上重现:

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

⊕婉儿 2024-12-08 18:31:34

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

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

查看开源平台游戏 Relica Island 上的博客文章。游戏逻辑很重,但由于作者的管道/双缓冲解决方案,帧率很流畅。

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

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

迎风吟唱 2024-12-08 18:31:34

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

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

您的手机上正在运行其他内容,这有时会占用游戏的 CPU 时间。垃圾收集器也会这样做,尽管不会像以前那样冻结您的游戏(因为它现在在单独的线程中运行)

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

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

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文