阿里跨平台技术人才储备丰富,独行快,众行远,欢迎优秀的你加入【大淘宝技术-跨平台技术团队】,一起打造靠谱的跨平台方案!这里有H5容器、Weex、Flutter、小程序、游戏互动等诸多解决方案,既有技术深度也有广泛业务场景,欢迎优秀的小伙伴来一起搞事情,一起把技术做稳一起为业务提效,手淘跨平台技术团队欢迎你的加入!
Flutter在2022年的Roadmap中提出需要重新考虑着色器的使用方式,计划重写图像渲染后端。最近该渲染后端 Impeller(叶轮)初见端倪,本文将介绍 Impeller 解决的问题、目标、架构和渲染细节。
背景
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着色器预热后,社区经常提到的一些高频问题收集如下:
为了获得最佳性能,Skia GPU backend在运行时会根据一些参数(如绘图命令,设备型号等)动态生成着色器。这些参数的组合会生成大量的着色器,无法在应用程序中预编译和内置。
Q3. 为什么不创建一个超级着色器并仅编译一次?
应用包体积变大
应用启动时间变长,因为需要预编译 SkSL shader
开发体验不友好
SkSL shader 的通用性无保证且不可预测
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着色器离线编译
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;
}
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;
}
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
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);
}
};
Canvas#saveLayer()操作会创建子EntityPass,用于离屏渲染;常见的需要离屏渲染的操作有:alpha blending,gradient,gaussian blur和expensive clips
EntityPass包含一系列Entity,每个Entity是一个绘图操作,对应于Canvas#drawXXX()
每个Entity对应一个Contents,表示一种绘图类型,共11种Contents
每种Contents在渲染时生成对应的Command,包含了顶点数据、片段着色器数据和GPU rendering pipeline信息
附录: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功能
团队介绍
阿里跨平台技术人才储备丰富,独行快,众行远,欢迎优秀的你加入【大淘宝技术-跨平台技术团队】,一起打造靠谱的跨平台方案!这里有H5容器、Weex、Flutter、小程序、游戏互动等诸多解决方案,既有技术深度也有广泛业务场景,欢迎优秀的小伙伴来一起搞事情,一起把技术做稳一起为业务提效,手淘跨平台技术团队欢迎你的加入!
文章评论