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 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 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();
}
Ответы
Ответ 1
Не уверен, что это ответ, но обратите внимание, что вызов блоков eglSwapBuffers() не менее 16 мс, даже если рисовать нечего.
Запуск игровой логики в отдельном потоке может отыграться некоторое время.
Отправляйте сообщение в блоге в Open Source Platforming game Relica Island. Логика игры тяжелая, но скорость кадра является плавной из-за решения автора/двойного буфера.
http://replicaisland.blogspot.com/2009/10/rendering-with-two-threads.html
Ответ 2
Вы не привязываете частоту кадров самостоятельно. Поэтому он связан с процессором.
Это означает, что он будет работать быстро, когда процессор не имеет ничего общего и медленнее, когда он делает другие вещи.
На вашем телефоне работает другой материал, изредка занимающий процессорное время из вашей игры. Garbage Collector также будет заниматься этим, хотя и не замораживая вашу игру, как раньше (поскольку теперь она работает в отдельном потоке)
Вы увидите, что это происходит в любой многопрограммной операционной системе, которая была в обычном режиме. Это не просто ошибка Java.
Мое предложение: привяжите частоту кадров к более низкому значению, если требуется постоянный бит.
Подсказка: используйте сочетание Thread.sleep() и ожидание ожидания (while loop), так как сон может переместиться через необходимое время ожидания.