关于flutter: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为了解决该问题,在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着色器预热也存在本身的毛病和局限性:

  1. 利用包体积变大
  2. 利用启动工夫变长,因为须要预编译 SkSL shader
  3. 开发体验不敌对
  4. 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外围指标:

  5. 可预测的性能:在编译时离线编译所有着色器,并依据着色器事后构建 pipeline state objects。
  6. 可检测:所有的图形资源(textures、buffers、pipeline state对象等)都被追踪和标记。动画能够被捕捉并长久化到磁盘而不影响渲染性能。
  7. 可移植:没有与特定的渲染API相绑定,着色器编写一次并在须要时转换。
  8. 应用古代图形API:大量应用(但不依赖)古代图形API(如Metal和Vulkan)的个性。
  9. 无效利用并发性:能够在多线程上散发单帧工作负载。
    impeller软件架构

    impeller大抵能够分为Compiler、Renderer,Entity、Aiks以及根底库Geomety和Base等几个模块。

  10. Compiler: host端工具,蕴含着色器 Compiler 和 Reflector。Compiler用于把 GLSL 4.60 着色器源码离线编译为特定后端的着色器(如MSL)。Reflector 依据着色器离线生成 C++ shader bindings,以在运行时疾速构建pipeline state objects (PSO)
  11. Renderer: 用于创立buffer、从shader bindings生成pipeline state objects、设置RenderPass、治理uniform-buffers、细分曲面、执行渲染工作等
  12. Entity:  用于构建2D渲染器,蕴含了着色器,shader bindings和pipeline state objects
  13. 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的结构图:

  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中对顶点的处理过程:

    以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要解决的问题、他的指标、架构和渲染细节。目前该项目标现状如下:

  5. impeller离线编译shader为shader library,可无效晋升首帧性能,防止着色器编译带来的jank问题
  6. 目前仅实现了 Metal backend,反对iOS和Mac
  7. 反对了73种Ops,11种Contents
  8. 代码量 18774 行,目前仍依赖了一些Skia数据结构,如SkNoDrawCanvas,SkPaint,SkRect, SkPicture等
  9. 我的项目处于晚期原型阶段,一些性能还不反对,如stroke、color filter、image filter、path effect、mask filter、gradient,以及drawArc、drawPoints、drawImage、drawShadow等等。issue #95434 中记录了停顿和打算。
  10. 整体工作量较大,相当于重写了 Skia GPU性能
    由此可见,flutter为了解决jank问题、晋升渲染性能不惜重写图像渲染后端,信心可见一斑。期待impeller能使flutter的渲染性能更上一层楼。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理