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 为了解决该问题,在 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 利用到真机上。
Q3. 为什么不创立一个超级着色器并仅编译一次?
这样的着色器会十分大,实质上是从新实现 Skia GPU 性能。大 shader 须要更长的编译工夫,从而引入更多的 Jank。
但 SkSL 着色器预热也存在本身的毛病和局限性:
- 利用包体积变大
- 利用启动工夫变长,因为须要预编译 SkSL shader
- 开发体验不敌对
-
SkSL shader 的通用性无保障且不可预测
以下工夫线列举了 Flutter 在解决 Jank 问题上的致力和停顿:对于着色器编译 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 外围指标: - 可预测的性能:在编译时离线编译所有着色器,并依据着色器事后构建 pipeline state objects。
- 可检测:所有的图形资源(textures、buffers、pipeline state 对象等)都被追踪和标记。动画能够被捕捉并长久化到磁盘而不影响渲染性能。
- 可移植:没有与特定的渲染 API 相绑定,着色器编写一次并在须要时转换。
- 应用古代图形 API:大量应用(但不依赖)古代图形 API(如 Metal 和 Vulkan)的个性。
- 无效利用并发性:能够在多线程上散发单帧工作负载。
impeller 软件架构impeller 大抵能够分为 Compiler、Renderer,Entity、Aiks 以及根底库 Geomety 和 Base 等几个模块。
- Compiler: host 端工具,蕴含着色器 Compiler 和 Reflector。Compiler 用于把 GLSL 4.60 着色器源码离线编译为特定后端的着色器(如 MSL)。Reflector 依据着色器离线生成 C++ shader bindings,以在运行时疾速构建 pipeline state objects (PSO)
- Renderer: 用于创立 buffer、从 shader bindings 生成 pipeline state objects、设置 RenderPass、治理 uniform-buffers、细分曲面、执行渲染工作等
- Entity: 用于构建 2D 渲染器,蕴含了着色器,shader bindings 和 pipeline state objects
-
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 library 和 binding sources 编译进 flutter engine 中。
这样所有着色器在离线时被编译为 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
"FrameInfo", // name
0u, // binding
};
// ===========================================================================
// Stage Inputs ==============================================================
// ===========================================================================
static constexpr auto kInputVertices = ShaderStageIOSlot { // vertices
"vertices", // name
0u, // attribute location
0u, // attribute set
0u, // attribute binding
ShaderType::kFloat, // type
32u, // bit width of type
2u, // vec size
1u // number of columns
};
static constexpr std::array<const ShaderStageIOSlot*, 1> kAllShaderStageInputs = {&kInputVertices, // vertices};
// ===========================================================================
// Stage Outputs =============================================================
// ===========================================================================
static constexpr auto kOutputColor = ShaderStageIOSlot { // color
"color", // name
0u, // attribute location
0u, // attribute set
0u, // attribute binding
ShaderType::kFloat, // type
32u, // bit width of type
4u, // vec size
1u // number of columns
};
static constexpr std::array<const ShaderStageIOSlot*, 1> 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 文件。
Shader binding 文件蕴含了着色器所有形容信息,如入口点,输出 / 输入构造,以及对应的 buffer slot。运行时依据 shader binding 能够疾速生成为 pipeline state objects。另外,bindings 中输出 / 输入构造是有填充和对齐的,所以顶点和 uniform 数据能够间接内存映射。
Impeller 渲染流程
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 的结构图:
- Canvas#saveLayer()操作会创立子 EntityPass,用于离屏渲染;常见的须要离屏渲染的操作有:alpha blending,gradient,gaussian blur 和 expensive clips
- EntityPass 蕴含一系列 Entity,每个 Entity 是一个绘图操作,对应于 Canvas#drawXXX()
- 每个 Entity 对应一个 Contents,示意一种绘图类型,共 11 种 Contents
-
每种 Contents 在渲染时生成对应的 Command,蕴含了顶点数据、片段着色器数据和 GPU rendering pipeline 信息
GPU 绘图过程顶点数据至关重要,须要依据绘制的形态生成顶点数据,再生成 vertext buffer object(VBO)关联到渲染管线上,如下为 impeller 中对顶点的处理过程:以 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 类图
总结
以上咱们介绍了 impeller 要解决的问题、他的指标、架构和渲染细节。目前该项目标现状如下:
- impeller 离线编译 shader 为 shader library,可无效晋升首帧性能,防止着色器编译带来的 jank 问题
- 目前仅实现了 Metal backend,反对 iOS 和 Mac
- 反对了 73 种 Ops,11 种 Contents
- 代码量 18774 行,目前仍依赖了一些 Skia 数据结构,如 SkNoDrawCanvas,SkPaint,SkRect, SkPicture 等
- 我的项目处于晚期原型阶段,一些性能还不反对,如 stroke、color filter、image filter、path effect、mask filter、gradient,以及 drawArc、drawPoints、drawImage、drawShadow 等等。issue #95434 中记录了停顿和打算。
- 整体工作量较大,相当于重写了 Skia GPU 性能
由此可见,flutter 为了解决 jank 问题、晋升渲染性能不惜重写图像渲染后端,信心可见一斑。期待 impeller 能使 flutter 的渲染性能更上一层楼。