Android EGL/OpenGL ES Частота кадров зависает

TL;DR

Даже если вообще не рисовать, кажется невозможным поддерживать частоту обновления 60 Гц в потоке рендеринга OpenGL ES на устройстве Android. Часто возникают загадочные всплески (показаны в коде внизу), и все мои попытки выяснить, почему и как, ведут в тупик. Время в более сложных примерах с пользовательским потоком рендеринга постоянно показывало, что eglSwapBuffers() является виновником, часто превышающим 17–32 мс. Помощь?

Подробнее

Это особенно опасно, потому что требования к рендерингу для нашего проекта — выровненные по экрану элементы, плавно прокручивающиеся по горизонтали с фиксированной высокой скоростью от одной стороны экрана к другой. Другими словами, платформер. Частые падения с 60 Гц приводят к заметным хлопкам и рывкам, как с движением, зависящим от времени, так и без него. Рендеринг с частотой 30 Гц невозможен из-за высокой скорости прокрутки, которая является неотъемлемой частью дизайна.

Наш проект основан на Java для максимальной совместимости и использует OpenGL ES 2.0. Мы углубляемся только в NDK для рендеринга OpenGL ES 2.0 на устройствах API 7-8 и поддержки ETC1 на устройствах API 7. Как в нем, так и в тестовом коде, приведенном ниже, я не проверил никаких выделений/событий GC, за исключением печати журнала и автоматических потоков, находящихся вне моего контроля.

Я воссоздал проблему в одном файле, который использует стандартные классы Android и не использует NDK. Приведенный ниже код можно вставить в новый проект Android, созданный в Eclipse, и он должен в значительной степени работать «из коробки», если вы выберете уровень API 8 или выше.

Тест был воспроизведен на различных устройствах с различными графическими процессорами и версиями ОС:

  • Galaxy Tab 10.1 (Android 3.1)
  • Nexus S (Android 2.3.4)
  • Galaxy SII (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();
}

person Bill Roeske    schedule 29.08.2011    source источник
comment
посмотрите вывод logcat для сообщений GC, это факт из жизни, если вы создаете его на Java. более новые версии Android имеют параллельный GC, но трудно избежать случайного полного GC и сопутствующих пауз. вы должны быть довольны 40 кадрами в секунду и стремиться к одновременному сбору мусора.   -  person escape-llc    schedule 22.11.2011


Ответы (2)


Не уверен, что это ответ, но обратите внимание, что вызов eglSwapBuffers() блокируется как минимум на 16 мс, даже если рисовать нечего.

Запуск игровой логики в отдельном потоке может выиграть некоторое время.

Ознакомьтесь с сообщением в блоге о платформере с открытым исходным кодом Relica Island. Логика игры тяжелая, но частота кадров плавная из-за решения авторов конвейера/двойного буфера.

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

person uhmdown    schedule 29.12.2011

Вы не привязываете частоту кадров к себе. Так что это связано с процессором.

Это означает, что он будет работать быстро, когда ЦП нечего делать, и медленнее, когда он занимается другими делами.

На вашем телефоне запущены другие вещи, которые иногда отнимают процессорное время у вашей игры. Сборщик мусора также сделает это, хотя и не заморозит вашу игру, как раньше (поскольку теперь он работает в отдельном потоке).

Вы бы видели, как это происходит в любой мультипрограммной операционной системе, которая обычно используется. Это не только вина Java.

Мое предложение: привяжите частоту кадров к более низкому значению, если требуется постоянный битрейт. Подсказка: используйте сочетание Thread.sleep() и ожидания (цикл while), так как сон может превысить требуемое время ожидания.

person olafure    schedule 16.12.2011