作者
- 魏国梁:字节FlutterInfra工程师,Flutter Member,长时刻专注Flutter引擎技能
- 袁 欣:字节FlutterInfra工程师, 长时刻重视烘托技能开展
- 谢昊辰:字节FlutterInfra工程师,Impeller Contributor
Impeller项目发动布景
2022年6月在Flutter3.0版别中Google官方正式将烘托器Impeller从独立库房中合入Flutter Engine骨干进行迭代,这是2021年Flutter团队推进从头完结Flutter烘托后端以来,初次正式明确了Impeller未来代替Skia作为Flutter主烘托计划的定位。Impeller的呈现是Flutter团队用以彻底处理SkSL(Skia Shading Language) 引进的Jank问题所做的重要测验。官方初次注意到Flutter的Jank问题是在2015年,其时推出的最重要的优化是对Dart代码运用AOT编译优化履行效率。在Impeller呈现之前,Flutter对烘托功能的优化大多停留在Skia上层,如烘托线程优先级的提高,在上色器编译过久的状况下切换CPU制作等策略性优化。
Jank类型分为两种:初次运转卡顿(Early-onset Jank)和非初次运转卡顿, Early-onsetJank的实质是运转时上色器的编译行为堵塞了FlutterRaster线程对烘托指令的提交。在Native运用中,开发者一般会依据UIkit等体系级别的UI结构开发运用,很少需求自界说上色器,Core Animation等framework运用的上色器在OS发动阶段就能够完结编译,上色器编译产品对一切的app而言全局共享,所以Native运用很少呈现上色器编译引起的功能问题 , 更常见的是用户逻辑对UI线程过度占用 。 官方为了优化Early-onset Jank,推出了SkSL的Warmup计划,Warmup实质是将部分功能灵敏的SkSL生成时刻前置到编译期,依然需求在运转时将SkSL转换为MSL才能在GPU上履行。Warmup计划需求在开发期间在实在设备上捕获SkSL导出配置文件 , 在运用打包时经过编译参数能够将部分SkSL预置在运用中。此外由于SkSL创立进程中捕获了用户设备特定的参数,不同设备Warmup配置文件不能相互通用,这种计划带来的功能提高十分有限。
在2019年Apple宣布在其生态中废弃OpenGL后,Flutter敏捷完结了烘托层对Metal的适配。与预期不符的是,Metal的切换使得Early-onsetJank的状况愈加恶化,Warmup计划的完结需求依靠Skia团队对Metal的预编译做支撑,由于 Skia团队的排期问题,一度导致Warmup计划在Metal后端上不可用。与此一起社区中对iOS渠道Jank问题的反应愈加强烈,社区中一度呈现屏蔽Metal的Flutter Engine Build,回退到GL后端虽然能一定程度改善首帧功能但是在iOS渠道上会呈现视觉作用的退化,与之相对的是,由于Android渠道上拥有iOS缺失的上色器机器码的缓存才能,Android渠道呈现Jank的概率比iOS低许多。
除了社区中呈现的通用问题外,Flutterinfra团队也经常收到字节内部事务方遇到的Jank问题的反应,反应较会集的有转场动画初次卡顿、列表翻滚进程中随机卡顿等场景:
转场动画触发的上色器编译,耗时~100ms
列表滑动进程中随机触发的上色器编译,耗时~28ms
在这篇文章中,咱们测验从Metal上色器编译计划,矢量烘托器原理和FlutterEngine烘托层的接口规划三个维度去探求Impeller想要处理的问题和烘托器背后的相关技能。
Metal Shader Compilation演进
一般而言,不同的烘托后端会运用独立的上色器言语,与JavaScript等常见脚本言语的履行进程类似,不同言语编写的上色器程序为了能在GPU硬件上履行,需求经历完好的lexical analysis / syntax analysis/ AbstratSyntax Tree(笼统语法树,下文简称AST)构建,IR优化,binary generation的进程。上色器的编译处理是在厂商供给的驱动中完结,其间详细的完结对上层开发者并不可见。Mesa是一个在MIT许可证下开源的三维计算机图形库,以开源办法完结了OpenGL的api接口。经过Mesa中对GLSL的处理能够观察到完好的上色器处理流水线。如下图所示,上层供给的GLSL源文件被Mesa处理为AST后首要会被编译为GLSL IR, 这是一种High-Level IR,经过优化后会生成另一种Low-Level IR:NIR,NIR结合当时GPU的硬件信息被处理为真正的可履行文件。不同的IR用来履行不同粒度的优化操作,一般底层IR更面向可履行文件的生成,而上层IR能够进行比如dead code elimination等粗粒度优化。常见的高级言语(如Swift)的编译进程也存在High-Level IR(SwiftIL) 到Low-Level IR(LLVMIR)的转换。
跟着Vulkan的开展,OpenGL4.6标准中引进了对SPIR-V格局的支撑。SPIR-V(Standard Portable Intermediate Representation)是一种标准化的IR,统一了图形上色器言语与并行计算(GPGPU运用)范畴。它答应不同的上色器言语转化为标准化的中心表明,以便优化或转化为其他高级言语,或直接传给Vulkan、OpenGL或OpenCL驱动履行。SPIR-V消除了设备驱动程序中对高级言语前端编译器的需求,大大下降了驱动程序的杂乱性,使广泛的言语和结构前端能够在不同的硬件架构上运转。Mesa中运用SPIR-V格局的上色器程序能够在编译时直接对接到NIR层,缩短上色器机器码编译的开支, 有助于体系烘托功能的提高。
在Metal运用中, 运用Metal Shading Language(以下简称MSL)编写的上色器源码首要被处理为AIR (Apple IR) 格局的中心表明。假如上色器源码是以字符办法在工程中引证,这一步会在运转时在用户设备上进行,假如上色器被增加为工程的Target,上色器源码会在编译期在Xcode中跟随项目构建生成MetalLib: 一种规划用来存放AIR的容器格局。随后AIR会在运转时,依据当时设备GPU的硬件信息,被Metal Compiler Service用JIT编译为可供履行的机器码。比较源码办法,将上色器源码打包为MetalLib有助于下降运转时生上色器机器码的开支。上色器机器码的编译会在每一次烘托管线状况方针(P ipelineS tateO bject,下文简称PSO)创立时发生,一个PSO持有当时烘托管线相关的一切状况,包括光栅化各阶段的上色器机器码,色彩混合状况,深度信息,模版掩码状况,多重采样信息等等。PSO一般被规划为一个imutable object(不可变方针),假如需求更改PSO中的状况需求创立一个新的PSO拷贝。
由于PSO可能在运用生命周期中屡次创立, 为了防止上色器的重复编译开支,一切编译过的上色器机器码会被Metal缓存用来加快后续PSO的创立进程,这个缓存称为Metal Shader Cache,完全由Metal内部办理,不受开发者操控。运用一般会在发动阶段一次性创立很多PSO方针,由于此刻Metal中没有任何上色器的编译缓存,PSO的创立会触发一切的上色器完好履行从AIR到机器码的编译进程,整个会集编译阶段是一个CPU密集型操作。在游戏中一般在玩家进入新关卡前利用Loading Screen准备好下一场景所需的PSO,然而常规app中用户的预期是能够即点即用,一旦上色器编译时刻超越16ms,用户就会感受到明显的卡顿和掉帧。
在Metal 2中,Apple初次为开发者引进了手动操控上色器缓存的才能:Metal Binary Archive。Metal Binary Archive的缓存层次坐落Metal Shader Cache 之上, 这意味着Metal Binary Archive中的缓存在PSO创立时会被优先运用 。 在运转时,开发者能够经过MetalPipelineManager手动将功能灵敏的上色器函数增加至Metal Binary Archive方针中并序列化至磁盘中。运用再次冷启后,此刻创立相同的PSO即是一个轻量化操作,没有任何上色器编译开支。缓存的Binary Archive甚至能够二次分发给相同设备的用户,假如本地Binary Archive中缓存的机器码与当时设备的硬件信息不匹配,Metal会回落至完好的编译流水线,保证运用的正常履行。游戏堡垒之夜「Fortnite」 在发动阶段需求创立多达 1700 个PSO方针,经过运用Metal Binary Archive来加快PSO创立,发动耗时从1m26s优化为3s , 速度提高28倍。
Metal Binary Archive经过内存映射的办法供GPU直接访问文件体系中的上色器缓存,因而打开Metal Binary Archive时会占用设备名贵的虚拟内存地址空间。与缓存一切的上色器函数比较,更明智的做法是依据详细的事务场景将缓存分层,在页面退出后及时封闭对应的缓存 , 开释不必要的虚拟内存空间。Metal Shader Cache的黑盒办理机制无法保证上色器在运用时不会呈现二次编译 , 而Metal Binary Archive能够保证其间的缓存的上色器函数在运用生命周期内始终可用。Metal Binary Archive虽然答应开发者手动办理上色器缓存,却依然需求经过在运转时收集机器码来构建,无法保证运用初次装置时的运用体会。在2022年WWDC中,Metal 3终于弥补了这个留传的缺陷,为开发者带来了在离线构建Metal Binary Archive的才能:
构建离线Metal Binary Archive需求运用一种全新的配置文件PipelineScript,Pipeline Script其实是Pipeline State Descriptor的一种JSON表明,其间配置了PSO创立所需的各种状况信息,开发者能够直接编辑生成,也能够在运转时捕获PSO获得。给定Pipeline Script和MetalLib,经过Metal工具链供给的metal指令即可离线构建出包括上色器机器码的Metal Binary Archive。Metal Binary Archive中的机器码可能会包括多种GPU架构 , 由于Metal Binary Archive需求内置在运用中提交市场 , 开发者能够综合考虑包体积的因素剔除不必要的架构支撑。
经过离线构建Metal Binary Archive,上色器编译的开支只存在于编译阶段,运用发动阶段PSO的创立开支大大下降。Metal Binary Archive不止能够优化运用的首屏功能, 实在的事务场景下,一些PSO方针会迟滞到详细页面才会被创立,触发新的上色器编译流程。一旦编译耗时过长,就会影响当时RunLoop下Metal制作指令的提交,Metal Binary Archive能够保证在运用的生命周期内, 中心交互途径下的上色器缓存始终为可用状况,将节省的CPU时刻片用来处理与用户交互强相关的逻辑, 大大提高运用的呼应性和运用体会。
矢量烘托基础概念
矢量烘托泛指在平面坐标系内经过拼装几许图元来生成图像信息的手法,经过界说一套完好的制作指令,能够在不同的终端上复原出不失真的图形, 任何前端的视窗都能够被看作一个2D平面的矢量烘托画布,Chrome与Android烘托体系就是依据Google的2D图形库Skia构建。对运用开发而言,矢量烘托技能也扮演重要人物,如文本 / 图表 / 地图 /SVG/Lottie等都依靠矢量烘托才能来供给高品质的视觉作用。
矢量烘托的基础单元是Path(途径),Path能够包括单个或多个Contour(概括),Contour在一些烘托器中也称为SubPath,Contour由连续的Segment(直线/高阶贝塞尔曲线)组成,标准的几许构型(圆形/矩形)均可被视为一种特别的Path,一些特别的Path能够包括坑洞或许自交叉(如五角星⭐️),这类Path的处理需求一些特别的计划。围绕Path能够构造出各种杂乱的图形,闻名的山君SVG一共包括480条Path,经过对其间不同Path的描边和填充,能够呈现出极富体现力的视觉作用:
高阶贝塞尔经过起始点和额定的操控点来界说一条曲线, 在将这样的笼统曲线交付给后端进行烘托前,咱们需求首要要对贝塞尔曲线做插值来近似模仿这条曲线,这个操作一般称为Flatten,GPU实在烘托的是一由组离散的点来近似模仿的曲线。依据Path界说的差异, 这一组离散的点会构成不同品种的多边形,对Path的处理简化为了对多边形的处理,咱们以一个简略的凹多边形为例来了解Path的描边和填充操作是怎么完结的:
多边形的描边操作,由于描边宽度的存在,描边的实在上色区域会有一半落在Path界说的区域之外。遍历多边形的外边缘的每条边,依据每条边两侧的极点,描边宽度以及边缘的斜率能够拼装出一组模仿描边行为的三角形图元,如上图所示:一个方向上的描边是由两个相结合的三角形构成。针对不同的Line Join风格,结合处有可能需求做不同的处理, 但是原理类似。将描边的三角形提交GPU能够烘托得到正确的描边作用,除了纯色的描边,结合不同的上色器能够完结渐变和纹路的填充作用。多边形的填充办法比较描边愈加杂乱,现在干流的矢量烘托器有两种不同的完结思路:
依据模版掩码的填充( NanoVG )
依据模版掩码的填充是在OpenGL红宝书中所描绘的一种填充多边形的经典办法。Skia在简略的场景下也会运用这种办法做多边形的填充。这种制作办法分为两步:首要利用StencilBuffer来记载实践制作区域,这一步只写入StencilBuffer,不操作Color Attachment,然后再进行一次制作,经过StencilBuffer记载的模版掩码,只向特定的像素方位写入色彩信息。经过图例能够更直观的了解这个进程:第一步,打开StencilBuffer的写入开关,运用GL_TRIANGLE_FAN办法制作一切的极点,GL会主动依据极点索引拼装两组三角形基元0 -> 1 -> 2和0 -> 2 -> 3,GL中一般指定逆时针方向为三角形片元的正面, 0 -> 1 -> 2 三角形所包围的区域在StencilBuffer中做 +1 操作, 由于极点3是多边形的凹点,0 -> 2 -> 3三角形的环绕数被翻转为了顺时针,咱们能够在StencilBuffer中对顺时针包裹的区域做 -1 操作, 此刻StencilBuffer中一切标记为 1 的像素就是咱们所需求的制作区域,再次提交相同的极点进行制作,打开色彩写入,就能够得到正确的制作成果。这种办法奇妙的利用了凹多边形会改变部分三角形环绕方向的特性。
模版掩码能够正确处理杂乱的多边形, 但是由于需求进行两段式的制作, 关于杂乱的多边形功能制作功能瓶颈较明显, 此外StencilBuffer等操作都是由GL驱动层所完结,简直不可能进行任何的功能优化, 这种制作办法常在一些追求小尺度的矢量烘托器中运用(NanoVG), 在一些文章中一般也被称为Stencil & Cover。
依据三角剖分的填充( Skia )
Skia中对多边形的烘托是由Tesselation和 Triangulation两步构成,Tesselation原意指在多边形中新增极点来构造愈加细分的几许图元,Triangulation是指衔接多边形本身的极点构造能够填充满本身的若干三角图元(不增加极点的状况下) ,Triangulation能够认为是Tessellation的一种特例,在Skia 中描述的Tessellation其实是指一种对杂乱多边形的拆分操作,了解多边形的Triangulation首要咱们需求引进单调多边形的概念:
关于任意一个多边形p而言, 假如存在一条直线l,l的垂线与p相交的部分都在p的内部, 那么称多边形p是相关于l的单调多边形。单调多边形的单调性是相关于某一特定方向而言,针对上图的示例咱们能够很容易找到一个方向的直线作为反例。利用单调多边形在l方向上的左右两个极点能够把多边形进一步分拆为上下两条边,每条边上的极点在l方向上会保证是有序的,这个特功能够用来完结剖分算法。
以下图中的凹多边形为比如,杂乱多边形的完好处理思路是:首要运用Tesselation算法将其拆分为若干个单调多边形(下图中两个蓝色区域),一般会在多边形的凹点进行拆分,得到一组单调多边形的调集后, 再分别对每一个单调多边形进行三角化,单调多边形的Triangulation算法比较闻名的有 EarCut, 也有一些完结如libtess2能够一起对杂乱多边形进行Tesselation/Triangulation两步操作,libtess2运用Delaunay算法来对单调多边形完结剖分,Delaunay算法能够防止剖分呈现过于狭长的三角形。无论运用何种计划,终究的产品都是能够直接交付给GPU进行烘托的三角形Mesh调集。
针对上文中的凹多边形, 剖分后的产品会是如上图所示的两个三角形, 三角形能够被认为是一种最简略的单调多边形, 提交这两个三角形即可完结此凹多边形的正确填充。依据三角剖分的填充计划, 最大的瓶颈是拆分单调多边形和单调多边形三角化两个进程的的算法选择, 由于这两步完全由上层完结, 因而对后期优化愈加友爱, 现在业界最新的计划现已能够完结利用GPU或许深度学习的办法完结剖分的加快。
Flutter DisplayList
DisplayList呈现之前,Skia运用SkPicture来收集每一帧的制作指令,随后在Raster线程回放完结当时帧的制作。gl函数在进入GPU履行前,依然会有一部分逻辑如PSO状况检测 / 指令封装等操作在CPU上履行,录制回放才能能够防止制作操作占用名贵的主线程时刻片。DisplayList和SkPicture的作用类似,那么为什么还需求将SkPicture向DisplayList做搬迁 ?Skia对Flutter来说属于第三方依靠,涉及到SkPicture的优化一般需求由Skia团队支撑,对Skia团队而言 SkPicture的才能不只服务于Flutter事务,Flutter团队假如修正SkPicture的源码会对Skia的代码有比较大的侵略, 而为了处理长时刻留传的Jank问题,Flutter团队又不得不考虑在SkPicture这一层进行优化 。2020年3月,liyuqian在创立一个flutter issue中初次提出了DisplayList的想象,预期相较于SkPicture会有如下三个方面的优势:
- DisplayList比较SkPicture有更高的可操作性去优化光栅化时期发生的缓存;
- DisplayList有助于完结更好的上色器预热计划;
- DisplayList比较SkPicture能够更好的对每一帧进行功能剖析;
在FlutterRoadMap明确了Impeller的替换方针后,DisplayList能更好的完结Flutter Engine层对烘托器的解耦,从而保障后续烘托层能无缝的从Skia搬迁到Impeller中。在最新的Flutter 3.0代码,DisplayList相关的代码坐落github.com/flutter/eng…
DisplayList作为Recoder的进程和运用SkPicture不同不大,中心是在canvas.cc中进行了切换:
//https://github.com/flutter/engine/blob/main/lib/ui/painting/canvas.cc#L260
//lib/ui/painting/canvas.cc
voidCanvas::drawRect(doubleleft,
doubletop,
doubleright,
doublebottom,
constPaint&paint,
constPaintData&paint_data){
if(display_list_recorder_){
paint.sync_to(builder(),kDrawRectFlags);
builder()->drawRect(SkRect::MakeLTRB(left,top,right,bottom));
}
//3.0由于默认开启了DisplayList作为Recorder所以下面的现已删除
//elseif(canvas_){
//SkPaintsk_paint;
//canvas_->drawRect(SkRect::MakeLTRB(left,top,right,bottom),
//*paint.paint(sk_paint));
//}
}
//lib/ui/painting/canvas.h
DisplayListBuilder*builder(){
returndisplay_list_recorder_->builder().get();
}
从上面的代码能够看出,是在Canvas的DrawOp中进行了DisplayList还是SkPicture的选择,一次DrawOp的录制进程如下图所示:
DisplayList Record DrawOp 进程图中Push的操作,DrawRectOp界说在display_list_ops.h中:
//https://github.com/flutter/engine/blob/main/display_list/display_list_ops.h#L554
//display_list/display_list_ops.h
#defineDEFINE_DRAW_1ARG_OP(op_name,arg_type,arg_name)
structDraw##op_name##Opfinal:DLOp{
staticconstautokType=DisplayListOpType::kDraw##op_name;
explicitDraw##op_name##Op(arg_typearg_name):arg_name(arg_name){}
constarg_typearg_name;
voiddispatch(Dispatcher&dispatcher)const{
dispatcher.draw##op_name(arg_name);
}
};
DEFINE_DRAW_1ARG_OP(Rect,SkRect,rect)
DEFINE_DRAW_1ARG_OP(Oval,SkRect,oval)
DEFINE_DRAW_1ARG_OP(RRect,SkRRect,rrect)
#undefDEFINE_DRAW_1ARG_OP
将宏界说打开能够看到如下界说, 这儿DrawRectOp是一种单参数DLOp,DrawRectOp中的dispatch办法会将drawRect操作派发给dispatcher来实践履行 。
structDrawRectOpfinal:DLOp{
staticconstautokType=DisplayListOpType::kDrawRect;
explicitDrawRectOp(arg_typearg_name):rect(rect){}
constSkRectrect;
voiddispatch(Dispatcher&dispatcher)const{
dispatcher.drawRect(arg_name);
}
}
在LLDB中能够打印出DrawRectOp的相关信息:
Push中的Push函数的完结如下,storage_ 是一个一维数组,同来存储DrawOp,在增加元素前会先进行容量的判断,是否需求扩容,随后创立DrawRectOp并对Type和 参数rect进行赋值,并累加 op_count_,完结DrawOp的增加。
//https://github.com/flutter/engine/blob/main/display_list/display_list_builder.cc#L27
//display_list/display_list_builder.cc
void*DisplayListBuilder::Push(size_tpod,intop_inc,Args&&...args){
size_tsize=SkAlignPtr(sizeof(T)+pod);
//扩容
if(used_+size>allocated_){
//NextgreatermultipleofDL_BUILDER_PAGE.
allocated_=(used_+size+DL_BUILDER_PAGE)&~(DL_BUILDER_PAGE-1);
storage_.realloc(allocated_);
FML_DCHECK(storage_.get());
memset(storage_.get()+used_,0,allocated_-used_);
}
FML_DCHECK(used_+size<=allocated_);
//如newDrawRectOp
autoop=reinterpret_cast<T*>(storage_.get()+used_);
used_+=size;
new(op)T{std::forward<Args>(args)...};
op->type=T::kType;
op->size=size;
op_count_+=op_inc;
returnop+1;
}
DisplayList记载DrawOp的流程如下:
- 首要经过调用BeginRecording创立DisplayListCanvasRecoder(继承自SkCanvasNoDraw) 之后创立中心类 DisplayListBuilder并返回Canvas给运用层;
- 运用层经过Canvas调用如drawRect办法,将会被以DrawRectOp记载在DisplayListBuilder的 storage_ 中;
- 最后调用endRecording将DisplayListBuilder的 storage_ 转移到DisplayList中,后面在SceneBuilder阶段,DisplayList会被封装到DisplayListLayer中;
DisplayList中的几个中心概念:DisplayListCanvasRecorder作为指令记载的载体,其间包括了DisplayListBuilder。DisplayListBuilder的storage是实在记载DLOp的载体,DisplayList将会记载DisplayListBuilder的storage,并终究被包裹在DisplayListLayer中,作为记载DLOp的载体。DisplayListCanvasDispatcher作为最后派发至SkCanvas或许Impeller的Wrapper层。
Impeller 烘托流程和架构规划
Impeller 概览
Impeller的方针是为Flutter供给具有predictable performance的烘托支撑,Skia的烘托机制需求运用在发动进程中动态生成SkSL, 这一部分上色器需求在运转时转换为MSL,才能进一步被编译为可履行的机器码,整个编译进程会对Raster线程构成堵塞。Impeller抛弃了运用SkSL转而运用GLSL4.6作为上层的上色器言语,经过Impeller内置的ImpellerC编译器,在编译期即可将一切的上色器转换为Metal Shading language, 并运用MetalLib格局打包为AIR字节码内置在运用中。Impeller的另一个优势是很多运用Modern Graphics APIs,Metal的规划能够充分利用CPU多核优势并行提交烘托指令, 大幅减少了驱动层对PSO的状况校验, 相关于GL后端仅仅将上层烘托接口的调用切换为Metal就能够为运用带来约~10% 的烘托功能提高。
在一个Flutter运用中,RenderObject的Paint操作终究会转换为Canvas的draw options,制作操作在Engine层拼装成DisplayList之后经过DisplayListDispatcher分发到不同的烘托器来履行详细的烘托操作。Impeller中完结了DisplayListDispatcher接口,这意味着Impeller能够消费上层传递的DisplayList数据。Aiks层保护了Canvas,Paint等制作方针的句柄。Entity能够理解为Impeller中的一个原子制作行为,如drawRect操作,其间保存了履行一次制作一切的状况信息,Canvas会经过Entity中保存的状况设置画布的Transform,BlendMode等特点。Entity中最要害的组成部分是Contents。Contents中持有了上色器的编译产品, 被用来实践操控当时Entity的制作作用,Contents有多种子类,来承接填充/纹路上色等不同的制作任务。Renderer层能够理解为与详细烘托api交流的桥梁,Renderer会将Entity中的信息(包括Contents中保存的上色器句柄)转换为 *Metal/*OpenGL等烘托后端的详细api调用。
Impeller制作流程
FlutterEngine层的LayerTree在被Impeller制作前需求首要被转换为EntityPassTree , UI线程在接收到v-sync信号后会将 LayerTree从UI线程提交到Raster线程,在Raster线程中会遍历LayerTree的每个节点并经过DisplayListRecorder记载各个节点的制作信息以及saveLayer操作,LayerTree中能够做能够Raster Cache的子树其制作成果会被缓存为位图,DisplayListRecorder会将对应子树的制作操作转换为drawImage操作,加快后续烘托速度。DisplayListRecorder完结指令录制后,就能够提交当时帧。DisplayListRecorder 中的指令缓存会被用来创立DisplayList 方针,DisplayList被DisplayListDispatcher的完结者(Skia / Impeller)消费,回放 DisplayList其间一切的DisplayListOptions能够将制作操作转换为EntityPassTree。
完结EntityPassTree的构建之后,需求把EntityPassTree中的指令解析出来履行。EntityPassTree制作操作以Entity方针为单位,Impeller中运用Vector来办理一个制作上下文中多个不同的Entity方针。一般Canvas在履行杂乱制作操作时会运用SaveLayer拓荒一个新的制作上下文,在iOS上习惯称为离屏烘托,SaveLayer操作在Impeller中会被标记为创立一个新的EntityPass,用于记载独立上下文中的Entity,新的EntityPass会被记载到父节点的EntityPass列表中,EntityPass的创立流程如上图所示。
Metal在上层为设备的GPU硬件笼统了CommandQueue的概念,CommandQueue与GPU 数量一一对应,CommandQueue中可包括一个或许多个CommandBuffer。CommandBuffer是实践制作指令RenderCommand存放的队列,简略的运用能够只包括一个CommandBuffer, 不同的线程能够经过持有不同CommandBuffer来加快RenderCommand的提交。RenderCommand由RenderCommandEncoder 的 Encode操作发生,RenderCommandEncoder界说了此次制作成果的保存办法 , 制作成果的像素格局以及制作开端或结束时Framebuffer attachmement所需求做的操作(clear / store),RenderCommand包括了终究交付给Metal的实在drawcall操作。
Entity中的Command转化为真正的MTLRenderCommand 时, 还携带了一个重要的信息:PSO*。Entity*从DisplayList中继承的制作状况终究会变为MTLRenderCommand相关的PSO ,MTLRenderCommand被消费时Metal驱动层会首要读取PSO调整烘托管线状况,再履行上色器进行制作,完结当时的制作操作 。
ImpellerC 编译器规划
ImpellerC是Impeller内置的上色器编译处理计划,源码坐落Impeller的compiler目录下 ,它能够在编译期将Impeller上层编写的glsl源文件转化为两个产品:1. 方针渠道对应的上色器文件;2. 依据上色器uniform信息生成的反射文件,其间包括了上色器uniform的struct布局等信息。反射文件中的struct类型作为model层,使得上层运用无需关怀详细后端的uniform赋值办法,极大地增强了Impeller的跨渠道特点,为编写不同渠道的上色器代码供给了便当。
在编译FlutterEngine工程中Impeller部分时,gn会首要将compiler 目录下的文件编译出为ImpellerC可履行文件,再运用ImpellerC对entity/content/shaders目录下的一切上色器进行预处理。GL后端会将上色器源码处理为hex格局并整合到一个头文件中, 而Metal后端会在GLSL完结MSL的转译后进一步处理为MetalLib。
ImpellerC在处理glsl源文件时,会调用shaderc对glsl文件进行编译。shaderc是Google保护的上色器编译器,能够glsl源码编译为SPIR-V。shaderc的编译进程运用了glslang 和SPIRV-Tools两个开源工具:glslang是glsl的编译前端 , 负责将 glsl处理为AST,SPIRV-Tools能够接管剩余的作业将AST进一步编译为SPIR-V, 在这一步的编译进程中,为了能得到正确的反射信息,ImpellerC会对shaderc限制优化等级。
随后ImpellerC会调用SPIR-V Cross对上一进程得到的SPIR-V进行反汇编,得到SPIR-V IR, 这是一种SPIR-V Cross内部运用的数据结构,SPIR-V Cross会在其之上进行进一步优化。ImpellerC随后会调用SPIR-V Cross创立方针渠道的CompilerBackend(MSLCompiler/GLSLCompiler/SKSLCompiler),Compiler Backend中封装了方针渠道上色器言语的详细转译逻辑 。一起SPIR-V Cross会从SPIR-V IR中提取Uniform数量,变量类型和偏移值等反射信息,
structShaderStructMemberMetadata{
ShaderTypetype;//thedatatype(bool,int,float,etc.)
std::stringname;//theuniformmembername"frame_info.mvp"
size_toffset;
size_tsize;
};
Reflector在得到这些信息后,会对内置的 .h与 .cc模版进行填充,得到可供Impeller引证的 .h与.cc文件,上层能够反射文件的类型便利的生成数据memcpy到对应的buffer中完结与上色器的通讯。关于Metal和GLES3来说,由于原生支撑UBO,终究会经过对应后端供给的UBO接口来完结 传值,关于不支撑UBO的GLES2来说,对UBO的赋值需求转换为glUniform* 系列api对Uniform中每个字段的独自赋值,在shader program link后,Impeller在运转时经过glGetUniformLocation得到一切字段在buffer中的方位,与反射文件中提取出的偏移值结合,Impeller就能够得到每个Uniform字段的方位信息,这个进程会在Imepller Context创立时生成一次,随后Impeller会保护Uniform字段的信息。关于上层来说,不管是GLES2还是其他后端, 经过Reflector与上色器的通讯进程都是相同的。
完结上色器转译和反射文件提取后,就能够实践履行uniform数据的绑定,Entity在触发制作操作时会首要调用Content的Render函数, 其间会创立一个供Metal消费的Command方针,Command会提交到RenderPass中等候调度,uniform数据的绑定发生在Command 创立这一步。如下图所示:VS::FrameInfo和FS::GradientInfo是反射生成的两个Struct类型, 初始化VS::FrameInfo和FS::GradientInfo的实例并赋值后,经过VS::BindFrameInfo和FS::BindGradientInfo函数即可完结数据和uniform的绑定。
VS::FrameInfoframe_info;
frame_info.mvp=Matrix::MakeOrthographic(pass.GetRenderTargetSize())*entity.GetTransformation();
FS::GradientInfogradient_info;
gradient_info.start_point=start_point_;
gradient_info.end_point=end_point_;
gradient_info.start_color=colors_[0].Premultiply();
gradient_info.end_color=colors_[1].Premultiply();
Commandcmd;
cmd.label="LinearGradientFill";
cmd.pipeline=renderer.GetGradientFillPipeline(OptionsFromPassAndEntity(pass,entity));
cmd.stencil_reference=entity.GetStencilDepth();
cmd.BindVertices(vertices_builder.CreateVertexBuffer(pass.GetTransientsBuffer()));
cmd.primitive_type=PrimitiveType::kTriangle;
FS::BindGradientInfo(cmd,pass.GetTransientsBuffer().EmplaceUniform(gradient_info));
VS::BindFrameInfo(cmd,pass.GetTransientsBuffer().EmplaceUniform(frame_info));
returnpass.AddCommand(std::move(cmd));
LinearGradientContents Render函数完结
Impeller完好的上色器处理流水线如下图所示:
总结
Impeller是Flutter为了管理SkSL编译耗时引进的的功能问题所做的重要测验,Skia的烘托机制需求在运转时动态创立SkSL, 导致上色器编译的时刻后移,Impeller经过在编译期完结GLSL至MSL的转换,在iOS渠道上能够直接运用MetalLib构建上色器机器码,而且引进确定性的缓存策略来提高烘托功能体现。跟着本年WWDC中Apple补齐了离线构建Metal Binary Archive的才能,Metal 3现已具有了全场景下高功能烘托的才能。Impeller作为Flutter独占的烘托计划 , 没有Skia的历史负担 , 更容易充分利用Apple的技能优化,这意味着Impeller的功能体现还有进一步提高的可能。
Impeller现在运用了依据libtess2的三角剖分计划, 依据社区的RoadMap,Impeller还会继续探索GPU剖分等高阶的三角化计划用来替换陈腐的libtess2完结。Impeller总体是一个移动优先的烘托处理计划,现在现已具有GL和Metal两个完好的烘托后端完结 , Vulkan的支撑现在正在进行中,官方现在没有支撑CPU软绘的计划。Impeller短期内不会也没有可能作为Skia的替代品, 不过其优秀的架构规划使其依然有潜力剥离出Flutter成为一个独立的烘托处理计划, 未来可能会对依据Skia的自绘计划构成挑战, 咱们对Impeller后续的开展也会继续坚持重视。