Android: 理解OnDrawFrame、FPS和VSync(OpenGL ES 2.0)

13
我在我的Android游戏中经常会遇到精灵卡顿的问题。这是一个相当简单的2D OpenGL ES 2.0游戏。(这是一个我已经多次重复访问的持续性问题)。
在我的游戏循环中,我有两个“计时器” - 一个将记录上一秒钟的帧数,另一个将计算从当前onDrawFrame迭代结束到下一个迭代开始的时间(以毫秒为单位)。
这是我找到的东西:
当没有渲染任何东西时,我得到60 fps(在大多数情况下),每次调用onDrawFrame时,它都报告自己花费了超过16.667ms的时间。现在,如果我渲染一些东西(无论是1个四边形还是100个四边形,结果都是相同的),我可以得到60fps(在大多数情况下),但现在只有约20%的onDrawFrame调用报告自己与上次调用相比需要超过16.667ms的时间。
我真的不明白为什么会发生这种情况,首先,为什么当onDrawFrame什么都不做时,它被称为“缓慢” - 更重要的是,为什么任何GL调用(一个简单的四边形)仍然会导致onDrawFrame调用之间的时间超过16.667ms(虽然发生得不那么频繁)。
我应该说,当onDrawFrame报告从上一次迭代花费超过16.667ms的时间时,它几乎总是伴随着FPS下降(到58或59),但并非每次都是如此。反之亦然,有时候FPS下降时,onDrawFrame会在上一个迭代完成后的16.667毫秒内调用。
所以......
我正在尝试修复我的游戏循环并消除这些“卡顿”-还要注意以下几点:
当我进行方法分析时,它显示glSwapBuffers有时需要很长时间
当我进行GL跟踪时,大多数情况下,它说在不到1ms的时间内呈现场景,但有时候奇怪的帧需要3.5-4ms-相同的场景。没有改变除了花费的时间之外。
每次丢失一帧或onDrawFrame报告长时间延迟(或两者均有)时,几乎都会出现视觉故障,但不是每次。大的视觉故障似乎与多个“延迟”的onDrawFrame调用和/或丢帧同时发生。
我认为这不是一个场景复杂性问题,原因有两个:1)即使我渲染场景两次,它也不会使问题变得更糟,我仍然在大多数情况下得到60FPS,只是偶尔会有一些掉帧,就像以前一样。2)即使我把场景剥离出来,我仍然会遇到问题。
显然,我误解了某些东西,所以需要指点迷津。
@Override
public void onDrawFrame(GL10 gl) {

    startTime = System.nanoTime();        
    fps++;                        
    totalTime = System.nanoTime() - timeSinceLastCalled;    

    if (totalTime > 16667000) {     
        Log.v("Logging","Time between onDrawFrame calls: " + (totalTime /(double)1000000));
    }

    //Grab time
    newTime = System.currentTimeMillis() * 0.001;
    frameTime = newTime - currentTime; //Time the last frame took

    if (frameTime > 0.25)
        frameTime = 0.25;

    currentTime = newTime;
    accumulator += frameTime;

    while (accumulator >= dt){              
      saveGameState();
      updateLogic();
      accumulator -= dt;
    }

    interpolation = (float) (accumulator / dt);

    Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);

    render(interpolation);

    if (startTime > lastSecond + 1000000000) {          
        lastSecond = startTime;
        Log.v("Logging","fps: "+fps);
        fps=0;          
    }

    endTime = startTime;
    timeSinceLastCalled = System.nanoTime();        
}

上面的游戏循环是在这篇优秀的文章中介绍的。


这些故障是否与垃圾回收事件同时发生? - Reuben Scratton
嗨@ReubenScratton,如果你的意思是我是否在不同的线程上更新逻辑和渲染,那么不,我在同一个线程(GL线程)上进行渲染和逻辑更新 - 我从主线程/ UI线程获取输入,但无论是否接收到输入,这个问题都会发生。这是一个非常简单的2D游戏,所以我想尽可能保持简单 - 谢谢! - Zippy
“dt”来自哪里,典型的值是什么?那个内部循环在使用它的时候可能比你想象的要运行更多次吗? - Reuben Scratton
@ReubenScratton,dt 是 1/60(60 为每秒的 ticks 数)- 因为我使用了固定时间步长 - 当循环无法及时渲染并需要“赶上”时,内部 while 循环应该运行 - 因此保持游戏以恒定的 60fps 运行,它偶尔会运行两次,这本身就很奇怪(因为渲染非常基本,并且我已经确认我正在以每秒 60 次的速度调用 onDrawFrame(在大多数情况下)。 - Zippy
请记住,显示器并不完全以60帧每秒运行,并且您正在使用其他正在运行的设备。您希望基于Choreographer中时间戳之间的差异而不是任意固定值来计算增量时间。您可以使用onDrawFrame()开始时间进行相当好的处理,但正如我的答案链接文章中提到的那样,它是基于队列反压而不是VSYNC,因此会有些变化。话虽如此,我在Android Breakout(https://github.com/fadden/android-breakout)中使用了`onDrawFrame()`时间,看起来很好。 - fadden
显示剩余8条评论
1个回答

9

一些想法:

  • 不要使用System.currentTimeMillis()来计时。它基于墙上的时钟,可以被网络更新。使用System.nanoTime(),它基于单调时钟。
  • 有关游戏循环的一些说明,请参见此附录。对于许多事情,排队填充是可以的,但要了解您并非完全依靠VSYNC,因此时间会倾向于不准确。
  • 某些设备(特别是基于qcom SOC的设备)在认为自己处于空闲状态时会降低CPU速度。始终在触摸屏幕上活动手指时进行计时。
  • 如果您想调试帧率问题,则需要使用systrace。跟踪视图分析在这里并不那么有用。

请参见Grafika's“记录GL应用程序”Activity,以获取一个简单的GLES应用程序示例,该应用程序会丢帧,但会调整动画,使其很少被注意到。


附录链接已经失效(仅哈希部分),https://source.android.com/devices/graphics/arch-gameloops 现在似乎是正确的。 - Karu
@Karu:他们将文档分成多个部分并重新排列了一些内容,因此有很多答案链接已经失效了。我已经更新了这一个。如果你看到其他的失效链接,欢迎直接编辑帖子。 - fadden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接