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 着色器預熱也存在自身的缺點和侷限性:
-
應用包體積變大
-
應用啓動時間變長,因爲需要預編譯 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 的渲染性能更上一層樓。
作者 | 谷鳴
編輯 | 橙子君
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/PLvlSt3tlX6AjufDm0XVMA