揭开Android Vulkan渲染封印:帧率暴增的底层指令

ps:本文内容较干,建议收藏后反复边跟进源码边思考设计思想。

渲染管线的基础架构

为什么叫渲染管线?这里是因为整个渲染的过程涉及多道工序,像管道里的流水线一样,一道一道的处理数据的过程,所以使用渲染管线还是比较形象的。接下来我们来看下渲染的整个架构。

Android的渲染过程需要按照是否开启硬件加速分别看待,默认是开启硬件加速的,那么在应用层使用Canvas的一些绘图API会预先转换成OpenGL指令或者Vulkan指令,然后由OpenGL/Vulkan直接操作GPU执行像素化的过程。而如果不开启硬件加速,Canvas的绘图指令就会调用skia库直接利用CPU绘制Bitmap图。

1、开启硬件加速


DisplayList到底是什么?

class DisplayListData {
  // 基础绘制指令
  Vector<DisplayListOp*> displayListOps;
  // 子视图的应用
  Vector<DrawRenderNodeOp*> children;
  // 命令分组<用于 Z 轴排序>
  Vector<Chunk> chunks;
}

这里说的DisplayList中的指令、GPU可执行的指令序列和GPU原生指令有什么不同呢?


Android应用程序窗口的根视图是虚拟的,抽象为一个Root Render Node。此外,一个视图如果设置有Background,那么这个Background也会抽象为一个Background Render Node。Root Render Node、Background Render Node和其它真实的子视图,除了TextureView和软件渲染的子视图之外,都具有Display List,并且是通过一个称为Display List Renderer的对象进行构建的。

TextureView不具有Display List,它们是通过一个称为Layer Renderer的对象以Open GL纹理的形式来绘制的,不过这个纹理也不是直接就进行渲染的,而是先记录在父视图的Display List中以后再进行渲染的。同样,软件渲染的子视图也不具有Display List,它们先绘制在一个Bitmap上,然后这个Bitmap再记录在父视图的Display List中以后再进行渲染的。

DisplayList中的指令是一种抽象化的GPU绘制指令(GPU是无法直接使用的,每个View对应一个),包含这些类型的指令(了解即可):

基础绘制操作

位图绘制:DrawBitmapOp(无法硬件渲染只能软件渲染的视图的Bitmap)

纹理绘制:DrawLayerOp(如TextureView的OpenGL 纹理)

图形绘制:DrawPathOp、DrawRectOp等(由Canvas.drawXXX()生成)

视图层级操作

子视图引用:DrawRenderNodeOp,封装子视图的RenderNode,递归执行其Display List

背景绘制:Background Render Node的绘制指令(独立DisplayList)

状态控制指令

ReorderBarrier:标记后续子视图需要按照Z轴排序(用于重叠视图)

InorderBarrier:标记后续子视图按默认顺序排序(无重叠)

Save/Restore:保存/恢复画布状态等等

概括起来说,DisplayList存储的指令=OpenGL/Vulkan命令的预处理抽象,这些指令在渲染线程中被转换为GPU可执行的OpenGL/Vulkan指令。

CPU可执行的指令序列是指在Render Thread中将Displaylist中的抽象指令转换成OpenGL/Vulkan的指令。

GPU原生指令是根据不同硬件生成的最基础的硬件操作指令,比如操作寄存器等。

2、关闭硬件加速

如果将整个执行流程按照从应用到底层GPU操作分层,可以这样分层:

渲染阶段过程(硬件加速)

1、UI线程DisplayList生成

当需要进行画面绘制的时候(VSync信号来临)ViewRootImpl.TraversalRunnable.run()收到回调,调用ViewRootImpl.doTraversal()方法,这个方法中调用ViewRootImpl中的performTraversal()方法到performDraw()再到draw()方法,判断是否开启硬件加速,如果不开启就调用drawSoftware();

如果开启就调用ThreadRenderer.draw()方法,再继续调用ThreadRenderer的updateRootDisplayList(),然后调到View的updateDisplayListDirty(),然后调到RenderNode.beginRecording()方法,这里面调到RecordingCanvas.obtain()方法,obtain()方法里面通过new RecordingCanvas(),然后通过JNI调用nCreateDisplayListCanvas()具体是调用Native哪个类?方法创建Native层的RecordingCanvas。

回到View的updateDisplayListDirty()方法里,执行完RenderNode.beginRecording()方法后得到RecordingCanvas的实例canvas,然后继续执行View里面的draw()方法,根据实际情况drawBackground(),再调onDraw(),以及draw子视图,里面都是调用RecordingCanvas的api,最终这些API都是JNI调用,在Native层调用的时候会将各种操作记录为各种抽象命令,并不直接进行画图。

2、渲染线程的并行化处理

上一步已经构建好DisplayList数据,ThreadedRenderer调用父类(HardwareRenderer)函数syncAndrDrawFrame(),函数里使用JNI调用nSyncAndDrawFrame(...),也就调用到android_graphics_HardwareRenderer.cpp中的android_view_ThreadedRenderer_syncAndDrawFrame()函数。

这个函数里调用RenderProxy.cpp的syncAndDrawFrame()函数,syncAndDrawFrame()函数里调用DrawFrameTask.cpp的drawFrame()函数,此函数调用函数当前类的postAndWait()函数,然后函数里调用RenderThread的queue()函数,将当前Task入到渲染线程的队列;当任务执行时,回调到DrawFrameTask.cpp中的run()函数,这个函数就是渲染的核心函数:

void DrawFrameTask::run() {
  ...
  if (CC_LIKELY(canDrawThisFrame)) {
        // 渲染上下文CanvasContext.draw()
        context->draw();
    } else {
        // wait on fences so tasks don't overlap next frame
        context->waitOnFences();
    }
  ...

}

真正的绘制是通过调用ContextCanvas的draw()函数,里面再通过mRenderPipeline->draw()函数将DisplayList命令转换成GPU命令,mRenderPipeLine这个就是实际实现渲染的管线,他有可能是通过OpenGL来实现或者通过Vulkan来实现,代码中有三种渲染管线类型(不同Android版本有一些区别):

SkiaOpenGLPipeline:基于OpenGL的Skia渲染管线

使用OpenGL API进行GPU渲染

兼容性好,支持大多数GPU

性能稳定

SkiaVulkanPipeline:基于Vulkan的Skia渲染管线

使用Vulkan API进行GPU渲染

更低的CPU开销

更好的多线程支持

更现代的GPU API

SkiaCpuPipeline:基于CPU的Skia渲染管线

用于非Android平台

纯CPU渲染,不依赖GPU

用于调试或者特殊环境

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值