移动设备的渲染优化

您好亲爱的读者,爱好者和编程图形专家!我们引起您注意的一系列文章致力于优化移动设备的渲染:基于iOS和Android的手机和平板电脑。该周期将包括三个部分。在第一部分中,我们将研究Mobile上流行的GPU tile架构的功能。在第二部分中,我们将介绍现代设备中提供的主要GPU系列,并考虑它们的优缺点。在第三部分中,我们将了解着色器优化的功能。

因此,让我们进入第一部分。

在台式机和控制台上进行视频卡的开发时,没有明显的功耗限制。随着用于移动设备的视频卡的出现,工程师面临着确保在可比较的台式机分辨率下获得可接受性能的任务,而这种视频卡的功耗应降低2个数量级。 



该解决方案是在一种名为“ 基于图块的渲染”(TBR)的特殊体系结构中找到的。对于具有PC开发经验的程序员,当他熟悉移动开发时,一切似乎都很熟悉:使用了类似的OpenGL ES API,并且图形管道的结构相同。但是,移动GPU的图块体系结构与PC / 即时模式控制台上使用的结构显着不同。了解TBR的优点和缺点将帮助您做出正确的决定,并通过Mobile获得出色的性能。

下面是第三个十年在PC和控制台上使用的经典图形管道的简化图。


在几何处理阶段,从GPU视频内存中读取顶点属性。经过各种转换(顶点着色器)后,将按原始顺序(FIFO)准备渲染的图元传递到光栅化器,光栅化器将图元划分为像素。之后,执行每个像素的片段处理步骤(Fragment Shader),并将获得的颜色值写入屏幕缓冲区,该缓冲区也位于视频存储器中。传统“即时模式”架构的一个功能是,在处理单个绘制调用时,将片段着色器的结果记录在屏幕缓冲区的任意部分中。因此,对于每个绘图调用,可能需要访问整个屏幕缓冲区。使用大量内存需要适当的总线带宽band),并且与高功耗相关。因此,移动GPU开始采用不同的方法。在移动视频卡典型的图块体系结构上,渲染是在与屏幕部分(图块)相对应的一小段内存中完成的。磁贴的尺寸很小(例如Mali视频卡为16x16像素,PowerVR为32x32)使您可以将其直接放置在视频卡芯片上,这使得访问速度与访问着色器核心寄存器的速度相当。非常快。


但是,由于图元可以落入屏幕缓冲区的任意部分,并且图块仅覆盖其中的一小部分,因此需要在图形管线中执行其他步骤。以下是管道如何与图块体系结构一起使用的简化图。


在处理了顶点并构造了图元之后,后者没有被发送到片段流水线,而是落入了所谓的Tiler中在这里,图元由图块分布,它们落入其像素中。在这种分配(通常覆盖指向一个帧缓冲区对象(又称为“ 渲染目标”)的所有绘制调用)之后,将顺序渲染图块。对于每个图块,执行以下操作序列:

  1. 从系统内存中加载旧的FBO内容Load) 
  2. 渲染图元的效果
  3. 将新的FBO内容上载到系统内存(存储


应当注意的是,可以将加载”操作视为“全屏纹理”的附加叠加而无需压缩。如果可能,请避免执行此操作,即 不要让FBO来回切换。如果在FBO中渲染之前清除了所有内容,则不会执行加载”操作。但是,为了向驱动程序发送正确的信号,此类清洁的参数必须满足某些条件:

  1. 必须禁用剪刀矩形
  2. 应该允许在所有颜色通道和alpha中进行记录。

为防止深度缓冲区和模具的“ 加载”操作,在开始渲染之前还需要清理它们。

还可以避免对深度/模板缓冲区进行存储操作毕竟,这些缓冲区的内容不会以任何方式显示在屏幕上。glSwapBuffers操作之前,可以调用glDiscardFramebufferEXTglInvalidateFramebuffer

const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
glDiscardFramebufferEXT (GL_FRAMEBUFFER, 2, attachments);

const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, attachments);

在某些渲染方案中,不需要在系统内存中放置深度/模板缓冲区以及MSAA缓冲区。例如,如果使用深度缓冲区FBO中的渲染是连续的,并且未使用前一帧的深度信息,则深度缓冲区不需要在渲染开始之前加载到图块内存中,也不需要在渲染完成后卸载。因此,无法在深度缓冲区下分配系统内存。现代图形API,如福尔康金属允许您明确设置内存模式为您的FBO同行  (MTLStorageModeMemoryless金属,VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT + VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BITVulkan中)。

特别要注意的是在瓦片体系结构上实施MSAA通过将FBO拆分为更多图块MSAA的高分辨率缓冲区不会离开图块内存。例如,对于MSAA 2x2,存储操作期间会将16x16的图块解析为8x8,即总共需要处理4倍的图块。但是不需要MSAA的额外内存,并且由于在快速切片内存中进行渲染,因此不会出现明显的带宽限制但是使用拼贴架构上的MSAA增加了Tiler的负载这可能会对具有大量几何图形的场景的渲染性能产生负面影响。

综上所述,我们提出了在瓷砖架构上使用FBO的理想方案:

// 1.   ,    auxFBO
glBindFramebuffer(GL_FRAMEBUFFER, auxFBO);
glDisable(GL_SCISSOR);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
// glClear,     
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | 
           GL_STENCIL_BUFFER_BIT);

renderAuxFBO();         

//   /      
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, depth_and_stencil);
// 2.   mainFBO
glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
glDisable(GL_SCISSOR);

glClear(...);
//   mainFBO    auxFBO
renderMainFBO(auxFBO);

glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, depth_and_stencil);

如果mainFBO形成的中间切换到auxFBO渲染,则会获得不必要的“ 加载和存储”操作,这会显着增加帧形成时间。在我们的实践中,即使在闲置的FBO设置(即 没有实际的渲染。由于引擎的架构,我们的旧电路如下所示:

//   mainFBO
glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
//   
glBindFramebuffer(GL_FRAMEBUFFER, auxFBO);
//  auxFBO
renderAuxFBO();

glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
//   mainFBO
renderMainFBO(auxFBO);

尽管在首次安装mainFBO之后缺少gl调用,但是在某些设备上,我们仍然进行了不必要的加载和存储操作,并且性能下降

为了增进对使用中间FBO开销的了解,我们使用综合测试测量了切换全屏FBO的时间损失。该表显示了在一帧中多次切换FBO存储操作上花费的时间(给出了这样的操作的时间)。glClear导致没有加载操作,即 测量了更有利的情况。在设备上使用的权限已贡献。它或多或少与已安装的GPU的功能相对应。因此,这些数字仅给出了一个概观,即在多代移动视频卡上切换目标的成本有多高。
显卡毫秒显卡毫秒
阿德雷诺3205.2
肾上腺素5120.74
PowerVR G62003.3肾上腺素6150.7
马里4003.2肾上腺素5300.4
马里t7201.9马里g510.32
PowerVR SXG 5441.4马里-t830
0.15

根据获得的数据,我们可以建议不要在每帧使用至少一个或两个FBO开关,至少对于较旧的视频卡而言。如果游戏对低端设备有单独的代码传递,建议不要在此处使用FBO更改。但是,在低端,降低分辨率的问题通常变得很重要。在Android上,您可以通过调用SurfaceHolder.setFixedSize()来降低渲染分辨率,而无需诉诸使用中间FBO

surfaceView.getHolder().setFixedSize(...)

如果通过主Surface应用程序渲染游戏(使用NativeActivity的典型方案),则此方法将不起作用如果使用主Surface,则可以通过调用本机函数ANativeWindow_setBuffersGeometry设置较低的分辨率

JNIEXPORT void JNICALL Java_com_organization_app_AppNativeActivity_setBufferGeometry(JNIEnv *env, jobject thiz, jobject surface, jint width, jint height)
{
ANativeWindow* window = ANativeWindow_fromSurface(env, surface); 
ANativeWindow_setBuffersGeometry(window, width, height, AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM); 
}

在Java中:

private static native void setBufferGeometry(Surface surface, int width , int height )
...
//   SurfaceHolder.Callback
@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
     setBufferGeometry(holder.getSurface(), 768, 1366); /* ... */
...

最后,我们提到了方便的ADB命令,用于控制Android上的选定表面缓冲区:

adb shell dumpsys surfaceflinger

您可以获得类似的结论,可以估算表面缓冲区的内存消耗:


屏幕截图显示了系统突出显示的用于GLSurfaceView游戏的三个缓冲区的三个缓冲区(以黄色突出显示),以及用于主Surface的 2个缓冲区(以红色突出显示)。如果使用主Surface渲染(这是使用NativeActivity时的默认方案),则可以避免分配额外的缓冲区。 

目前为止就这样了。在以下文章中,我们将对移动GPU进行分类,并分析为它们优化着色器的方法。

All Articles