Flutter 新一代图形渲染器 Impeller

Flutter在2022年的Roadmap中提出需要重新考虑着色器的使用方式,计划重写图像渲染后端。最近该渲染后端 Impeller(叶轮)初见端倪,本文将介绍 Impeller 解决的问题、目标、架构和渲染细节。

背景

Flutter在过去一年多时间解决了很多Jank问题,但着色器编译导致的Jank问题一直没有彻底解决。这里我们先了解下什么着色器编译Jank。Flutter底层使用了skia做为2D图形渲染库,而skia内部定义了一套SkSL(Skia shading language),SkSL 属于 GLSL 变体。在Flutter的光栅化阶段,当第一次使用着色器时Skia会根据绘图命令和设备参数生成 SkSL,然后再将 SkSL 转换为特定后端(GLSL、GLSL ES 或 Metal SL)着色器,并在设备上编译为着色器程序。而编译着色器可能花费几百毫秒,导致数十帧的丢失。定位着色器编译Jank问题可以查看trace信息是否有 GrGLProgramBuilder::finalize 调用。

Flutter 新一代图形渲染器 Impeller_第1张图片

Flutter为了解决该问题,在Flutter 1.20 版本中为GL后端实现了SkSL预热机制,支持离线收集应用程序中使用的 SkSL 着色器并保存为json文件,然后把该文件打包到应用程序中,最终用户首次打开应用程序时预编译 SkSL着色器,从而减少着色器编译 jank。随后,在 Flutter 2.5中支持了 iOS metal 着色器的预编译。

Flutter gallery应用预热前后,在Moto G4上从90ms减少到40ms,在iPhone 4s上从300ms减少到80ms,性能提升很明显。

在Flutter官方提供了SkSL着色器预热后,社区经常提到的一些高频问题收集如下:

Q1. 为什么不预编译用到的所有着色器?

为了获得最佳性能,Skia GPU backend在运行时会根据一些参数(如绘图命令,设备型号等)动态生成着色器。这些参数的组合会生成大量的着色器,无法在应用程序中预编译和内置。

Q2. 不同设备上捕获的 SkSL shader 通用吗? 理论上,没有机制保证在一台设备上捕获的 SkSL shader 在其他设备上也有效。实际上,(有限的)测试表明 SkSL shader 能表现的较好,即使在iOS上捕获的 SkSL 应用到Android设备,或者模拟器上捕获的SkSL应用到真机上。

Flutter 新一代图形渲染器 Impeller_第2张图片

Q3. 为什么不创建一个超级着色器并仅编译一次?

这样的着色器会非常大,本质上是重新实现 Skia GPU 功能。大shader需要更长的编译时间,从而引入更多的 Jank。

但SkSL着色器预热也存在自身的缺点和局限性:

1.应用包体积变大
2.应用启动时间变长,因为需要预编译 SkSL shader
3.开发体验不友好
4.SkSL shader 的通用性无保证且不可预测

以下时间线列举了Flutter在解决Jank问题上的努力和进展:

Flutter 新一代图形渲染器 Impeller_第3张图片

对于着色器编译Jank问题,官方经过多次尝试依然无法彻底解决,因此在2022年的roadmap中请明确提出要重新考虑使用着色器的方式,计划重写图像渲染后端。在2022年计划在iOS上将 Flutter 迁移到新架构上,然后根据经验将该解决方案移植到其他平台上。最近,该图形渲染后端 impeller(叶轮) 初见端倪,接下来让我们看看 impeller有什么独特之处。

Impeller架构

Impeller是为flutter量身定做的渲染器,目前处于早起原型阶段,仅实现了metal后端,支持iOS和Mac系统。工程方面,他依赖了 flutter fml 和 display list,并实现了display list dispatcher 接口,可以容易的替换skia。Impeller被flutter flow子系统所使用,因此得名。

Impeller核心目标:

1.可预测的性能:在编译时离线编译所有着色器,并根据着色器预先构建 pipeline state objects。
2.可检测:所有的图形资源(textures、buffers、pipeline state对象等)都被追踪和标记。动画可以被捕获并持久化到磁盘而不影响渲染性能。
3.可移植:没有与特定的渲染API相绑定,着色器编写一次并在需要时转换。
4.使用现代图形API:大量使用(但不依赖)现代图形API(如Metal和Vulkan)的特性。
5.有效利用并发性:可以在多线程上分发单帧工作负载。

impeller软件架构

Flutter 新一代图形渲染器 Impeller_第4张图片

impeller大致可以分为Compiler、Renderer,Entity、Aiks以及基础库Geomety和Base等几个模块。

1.Compiler: host端工具,包含着色器 Compiler 和 Reflector。Compiler用于把 GLSL 4.60 着色器源码离线编译为特定后端的着色器(如MSL)。Reflector 根据着色器离线生成 C++ shader bindings,以在运行时快速构建pipeline state objects (PSO)
2.Renderer: 用于创建buffer、从shader bindings生成pipeline state objects、设置RenderPass、管理uniform-buffers、细分曲面、执行渲染任务等
3.Entity:  用于构建2D渲染器,包含了着色器,shader bindings和pipeline state objects
4.Aiks:  封装 Entity 以提供类 Skia API,临时存在,便于对接到 flutter flow

Impeller着色器离线编译

impeller compiler模块是解决着色器编译Jank的关键所在。在编译阶段,首先把compiler相关源码编译为host工具impellerc binary。然后开始着色器的第一编译阶段,利用impellerc compiler 把//impeller/entity/shaders/目录下所有着色器源码(包括顶点着色器和片段着色)编译为着色器中间语言 SPIR-V。再开始着色的第二个编译阶段,把 SPIR-V 转换为特定后端的高级着色器语言(如Metal SL) ,随后(iOS上利用Metal Binary Archives)把特定后端的着色器源码(Metal着色器)编译为 shader library。同时,另外一条路径中利用impellerc reflector处理SPIR-V生成 C++ shader binding,用于在运行时快速创建pipeline state objecs(PSO)。Shader binding生成的头文件中包括了一些结构体(有适当的填充和对齐),使得可以将uniform data和vertex数据直接指定给着色器,而无需处理绑定和顶点描述符。最后把shader librarybinding sources编译进flutter engine中。

Flutter 新一代图形渲染器 Impeller_第5张图片

这样所有着色器在离线时被编译为shader library,在运行时不需要执行任何编译操作,从而提升首帧渲染性能,也彻底解决了着色器编译带来的jank问题。

Shader Bindings

impeller中的着色器仅需要基于 GLSL 4.60 语法编写一次,编译时转换为特定后端的着色器和binding。比如 solid_fill.vert 顶点着色器经过离线编译后生成了solid_fill.vert.metal,solid_fill.vert.h和solid_fill.vert.mm文件。solid_fill.vert:

uniform FrameInfo {mat4 mvp;vec4 color;
} frame_info;

in vec2 vertices;

out vec4 color;

void main() {gl_Position = frame_info.mvp * vec4(vertices, 0.0, 1.0); color = frame_info.color;
} 

solid_fill.vert.metal:

using namespace metal;
struct FrameInfo
{float4x4 mvp;float4 color;
};

struct solid_fill_vertex_main_out
{float4 color [[user(locn0)]];float4 gl_Position [[position]];
};

struct solid_fill_vertex_main_in
{float2 vertices [[attribute(0)]];
};

vertex solid_fill_vertex_main_out solid_fill_vertex_main(solid_fill_vertex_main_in in [[stage_in]],constant FrameInfo& frame_info [[buffer(0)]])
{solid_fill_vertex_main_out out = {};out.gl_Position = frame_info.mvp * float4(in.vertices, 0.0, 1.0);out.color = frame_info.color;return out;
} 

solid_fill.vert.h:

struct SolidFillVertexShader {// =========================================================================== // Stage Info ================================================================ // =========================================================================== static constexpr std::string_view kLabel = "SolidFill";static constexpr std::string_view kEntrypointName = "solid_fill_vertex_main";static constexpr ShaderStage kShaderStage = ShaderStage::kVertex;// =========================================================================== // Struct Definitions ======================================================== // ===========================================================================struct PerVertexData {Point vertices; // (offset 0, size 8)}; // struct PerVertexData (size 8)struct FrameInfo {Matrix mvp; // (offset 0, size 64) Vector4 color; // (offset 64, size 16) Padding<48> _PADDING_; // (offset 80, size 48) }; // struct FrameInfo (size 128)// =========================================================================== // Stage Uniform & Storage Buffers =========================================== // ===========================================================================static constexpr auto kResourceFrameInfo = ShaderUniformSlot { // FrameInfo "FrameInfo", // name 0u, // binding };// =========================================================================== // Stage Inputs ============================================================== // ===========================================================================static constexpr auto kInputVertices = ShaderStageIOSlot { // vertices "vertices", // name0u,// attribute location0u,// attribute set 0u, // attribute bindingShaderType::kFloat, // type32u,// bit width of type2u, // vec size1u // number of columns};static constexpr std::array kAllShaderStageInputs = { &kInputVertices, // vertices
 };
 
// =========================================================================== 
// Stage Outputs ============================================================= 
// =========================================================================== 
static constexpr auto kOutputColor= ShaderStageIOSlot { // color"color", // name0u,// attribute location 0u,// attribute set 0u, // attribute bindingShaderType::kFloat, // type32u,// bit width of type4u, // vec size1u // number of columns
};
static constexpr std::array kAllShaderStageOutputs = {&kOutputColor, // color
};

// =========================================================================== 
// Resource Binding Utilities ================================================ 
// ===========================================================================

/// Bind uniform buffer for resource named FrameInfo.
static bool BindFrameInfo(Command& command, BufferView view) {return command.BindResource(ShaderStage::kVertex, kResourceFrameInfo, std::move(view));}


};// struct SolidFillVertexShader 

solid_fill.vert.mm 文件仅对相应结构体进行填充和对齐校验,无实际功能。对于solid_fill.frag 同样的处理逻辑,生成了solid_fill.frag.metal,solid_fill.frag.h和solid_fill.frag.mm文件。

Flutter 新一代图形渲染器 Impeller_第6张图片

Shader binding文件包含了着色器所有描述信息,如入口点,输入/输出结构,以及对应的buffer slot。运行时根据shader binding可以快速生成为pipeline state objects。另外,bindings中输入/输出结构是有填充和对齐的,所以顶点和uniform数据可以直接内存映射。

Impeller渲染流程

Flutter 新一代图形渲染器 Impeller_第7张图片

impeller通过分别继承了IOSContext、IOSSurface和flow Surface,实现了IOSContextMetalImpeller、IOSSurfaceMetalImpeller和GPUSurfaceMetalImpeller结构对接到了flutter flow子系统中。在光栅化阶段,通过 DisplayListCanvasRecorder(继承自SkNoDrawCanvas并实现了所有SkCanvas的函数)合成 Layer Tree,把所有layer中的绘图命令转换为一个个的DLOps,并存储到DisplayList结构。DLOps中存储了绘图的所有数据信息,如常见的AnitiAliasOp,SetColorOp,DrawRectOp等,共有73种Ops

如下为drawRect的DrawRectOp的结构:

struct DrawRectOp final : DLOp {static const auto kType = DisplayListOpType::kDrawRect; explicit DrawRectOp(SkRect rect) : rect(rect) {}const SkRect rect;void dispatch(Dispatcher& dispatcher) const {dispatcher.drawRect(rect);}
}; 

接下来进入impeller的渲染流程,使用DisplayListDispatcher执行DisplayList中所有Ops,在Op的dispatch()函数中调用DisplayListDispatcher的相应函数,把绘图信息转换为EntityPass结构。如果有saveLayer操作,则创建子EntityPass,形成EntityPass树形结构。同时把多个相关联的Ops转换为Entity存储到EntityPass中。每个Entity会对应一种Contents,表示一种绘图操作(如drawRect/clipPath等),共有11种Contents(参见第五小节附录impeller类图)。可见,DisplayList记录了细粒度的Op信息,结构扁平,无层次关系;转换为EntityPass后,对Ops进行了组装,根据savaLayer操作生成了有层次结构EntityPass tree,更便于后续的渲染。

随后,使用RenderPass从Root EntityPass开始遍历,把EntityPass中每个Entity转换为Command结构,即从Shader Bindings生成GPU Pipeline,把Polygon转换为顶点数据,设置片段着色器的颜色或纹理数据,再把顶点数据和颜色或纹理数据转换为GPU buffer设置到GPU Pipeline中。遍历完成所有的Entity Passes后,所有Command都存储到了RenderPass中。

然后,开始渲染指令编码阶段,根据MTLCommandBuffer生成MTLRenderCommandEncoder,遍历所有的Commands,把每个Command中的PipelineState,Vertext Buffer,Fragment Buffer设置MTLRenderCommandEncoder中,最后。结束编码并提交command buffer。

如下为Entity Passes的结构图:

Flutter 新一代图形渲染器 Impeller_第8张图片

1.Canvas#saveLayer()操作会创建子EntityPass,用于离屏渲染;常见的需要离屏渲染的操作有:alpha blending,gradient,gaussian blur和expensive clips
2.EntityPass包含一系列Entity,每个Entity是一个绘图操作,对应于Canvas#drawXXX()
3.每个Entity对应一个Contents,表示一种绘图类型,共11种Contents
4.每种Contents在渲染时生成对应的Command,包含了顶点数据、片段着色器数据和GPU rendering pipeline信息

GPU绘图过程顶点数据至关重要,需要根据绘制的形状生成顶点数据,再生成vertext buffer object(VBO)关联到渲染管线上,如下为impeller中对顶点的处理过程:

Flutter 新一代图形渲染器 Impeller_第9张图片

以Rect类型为例,在生成EntityPass阶段会把Rect转换为Path结构,然后在创建Command阶段利用Tessellator(曲面细分器)根据Path生成顶点数据,存储到主存HostBuffer上,并把offset和length保存为BufferView关联到顶点或片段着色器的PSO上。在Encode Commands阶段把整个HostBuffer上传到GPU buffer,把该次绘制的Vertext/Fragment Buffer、offset和length信息设置到对应的GPU pipeline上。

附录:Impeller类图

Flutter 新一代图形渲染器 Impeller_第10张图片

总结

以上我们介绍了impeller要解决的问题、他的目标、架构和渲染细节。目前该项目的现状如下:

1.impeller离线编译shader为shader library,可有效提升首帧性能,避免着色器编译带来的jank问题
2.目前仅实现了 Metal backend,支持iOS和Mac
3.支持了73种Ops,11种Contents
4.代码量 18774 行,目前仍依赖了一些Skia数据结构,如SkNoDrawCanvas,SkPaint,SkRect, SkPicture等
5.项目处于早期原型阶段,一些功能还不支持,如stroke、color filter、image filter、path effect、mask filter、gradient,以及drawArc、drawPoints、drawImage、drawShadow等等。issue #95434 中记录了进展和计划。
6.整体工作量较大,相当于重写了 Skia GPU功能

由此可见,flutter为了解决jank问题、提升渲染性能不惜重写图像渲染后端,决心可见一斑。期待impeller能使flutter的渲染性能更上一层楼。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

你可能感兴趣的:(flutter,图形渲染,ios)