Impeller Rendering Engine: Background, Metal Shader Compilation, Vector Rendering, and Flutter DisplayList
This article provides an in‑depth technical overview of Flutter's Impeller rendering engine, covering its origin, Jank classification, Metal shader compilation evolution, vector rendering fundamentals, DisplayList architecture, Impeller's rendering pipeline, and the ImpellerC shader compiler, with code examples and performance insights.
Impeller Project Background
In June 2022, Google officially merged the Impeller renderer from an independent repository into the Flutter Engine mainline, marking the first clear statement since 2021 that Impeller will replace Skia as Flutter's primary rendering solution. Impeller was introduced to address SkSL‑induced Jank problems, with the first Jank issue observed in 2015 and early optimizations focusing on AOT‑compiled Dart code. Before Impeller, most Flutter rendering performance improvements were limited to Skia‑level optimizations such as thread priority and shader compilation latency.
Jank is divided into Early‑onset Jank (first‑run stalls caused by runtime shader compilation) and non‑early‑onset Jank. Native UI frameworks typically pre‑compile shaders at OS startup, avoiding runtime compilation overhead, whereas Flutter's SkSL Warmup approach attempts to pre‑compile performance‑critical shaders but suffers from device‑specific configuration limitations.
After Apple deprecated OpenGL in 2019, Flutter quickly migrated to Metal, but this transition initially worsened Early‑onset Jank because the Warmup solution depended on Skia's Metal pre‑compilation support, which was delayed. Community feedback highlighted severe Jank on iOS, leading to temporary fallback to the GL backend, which improved first‑frame performance but degraded visual quality.
Metal Shader Compilation Evolution
Different rendering backends use distinct shader languages that undergo lexical analysis, syntax analysis, AST construction, IR optimization, and binary generation. The Mesa library demonstrates the full GLSL compilation pipeline, producing high‑level IR (GLSL IR) and low‑level IR (NIR) before generating executable GPU code. SPIR‑V, introduced with Vulkan and OpenGL 4.6, standardizes intermediate representation across graphics and compute domains, allowing drivers to skip front‑end compilation and improve performance.
On Metal, shaders written in Metal Shading Language (MSL) are first compiled to Apple IR (AIR). When shaders are included as source files, they are JIT‑compiled at runtime; when packaged as MetalLib, they are pre‑compiled, reducing runtime overhead. Metal Pipeline State Objects (PSOs) encapsulate all pipeline state, and creating many PSOs can be CPU‑intensive. Metal Binary Archive, introduced in Metal 2, provides a manual cache layer above the Metal Shader Cache, enabling offline PSO creation and dramatically reducing startup times (e.g., Fortnite startup reduced from 1 m 26 s to 3 s, a 28× speedup).
Metal 3 further adds offline Metal Binary Archive construction via a Pipeline Script (JSON) that describes PSO state, allowing developers to embed pre‑compiled shader binaries and control supported GPU architectures to manage app size.
Vector Rendering Fundamentals
Vector rendering assembles geometric primitives on a 2D canvas to produce resolution‑independent graphics. Core concepts include Paths, Contours, Segments, and the handling of complex shapes such as self‑intersecting polygons. High‑order Bézier curves are flattened into discrete points for GPU rasterization.
Two primary polygon fill strategies are used: stencil‑mask (as in NanoVG) and triangulation (as in Skia). Stencil‑mask uses the stencil buffer to record fill regions in two passes, while triangulation decomposes polygons into monotone pieces and then triangulates them, often using algorithms like EarCut or libtess2 with Delaunay optimization.
Flutter DisplayList
Before DisplayList, Skia used SkPicture to record drawing commands for later raster playback. DisplayList replaces SkPicture, offering higher operability for raster‑phase optimizations, better shader warm‑up support, and improved per‑frame performance analysis. The code snippet below shows how Canvas::drawRect records a DrawRectOp when a DisplayList recorder is active.
// https://github.com/flutter/engine/blob/main/lib/ui/painting/canvas.cc#L260
// lib/ui/painting/canvas.cc
void Canvas::drawRect(double left, double top, double right, double bottom, const Paint& paint, const PaintData& paint_data) {
if (display_list_recorder_) {
paint.sync_to(builder(), kDrawRectFlags);
builder()->drawRect(SkRect::MakeLTRB(left, top, right, bottom));
}
// 3.0 default enables DisplayList, old SkPicture path removed
}
DisplayListBuilder* builder() {
return display_list_recorder_->builder().get();
}The DrawRectOp definition (display_list_ops.h) illustrates how a single‑argument draw operation is encoded.
// https://github.com/flutter/engine/blob/main/display_list/display_list_ops.h#L554
#define DEFINE_DRAW_1ARG_OP(op_name, arg_type, arg_name) \
struct Draw##op_name##Op final : DLOp { \
static const auto kType = DisplayListOpType::kDraw##op_name; \
explicit Draw##op_name##Op(arg_type arg_name) : arg_name(arg_name) {} \
const arg_type arg_name; \
void dispatch(Dispatcher& dispatcher) const { dispatcher.draw##op_name(arg_name); } \
};
DEFINE_DRAW_1ARG_OP(Rect, SkRect, rect)
#undef DEFINE_DRAW_1ARG_OPDisplayList recording proceeds by creating a DisplayListCanvasRecorder, invoking drawing methods that push DrawOps into a storage buffer, and finally converting the buffer into a DisplayList object, which is dispatched to the appropriate renderer (Skia or Impeller).
Impeller Rendering Flow and Architecture
Impeller aims to provide predictable performance by replacing SkSL with GLSL 4.6, compiling shaders to MSL at build time via the ImpellerC compiler, and packaging them as MetalLib. This eliminates runtime shader compilation stalls and leverages modern graphics APIs for parallel command submission, yielding ~10 % performance gains over OpenGL.
During a frame, the Flutter Engine builds a LayerTree, which is transformed into an EntityPassTree. RenderObjects emit drawing commands that become DisplayList entries, which are then dispatched to Impeller. Entities encapsulate drawing state, with Contents holding compiled shader handles. PSOs are associated with each Entity's render command, and Metal drivers validate PSO state before execution.
ImpellerC compiles GLSL shaders to SPIR‑V using shaderc (glslang + SPIRV‑Tools), then uses SPIR‑V Cross to generate target‑specific code (MSL, GLSL) and reflection data describing uniform layouts. The reflector generates .h/.cc files that map uniform structs to buffer offsets, enabling uniform binding across backends (UBO for Metal/GLES3, glUniform for GLES2).
struct ShaderStructMemberMetadata {
ShaderType type; // bool, int, float, etc.
std::string name; // e.g., "frame_info.mvp"
size_t offset;
size_t size;
};At render time, Entities create Commands that bind vertex buffers and uniform data (e.g., LinearGradientContents rendering code), then add the command to a RenderPass for execution.
Summary
Impeller is Flutter's response to the performance penalties of SkSL runtime compilation, moving shader translation to build time and employing deterministic caching (Metal Binary Archive) to improve rendering latency. With Metal 3's offline binary archive support, Impeller can achieve high‑performance rendering across iOS devices. Future work includes exploring GPU‑accelerated tessellation to replace the current libtess2 implementation, adding Vulkan support, and potentially evolving into a standalone rendering solution that challenges Skia‑based approaches.
Author Information
Wei Guoliang – ByteDance Flutter Infra Engineer, Flutter Member, focusing on Flutter engine technology.
Yuan Xin – ByteDance Flutter Infra Engineer, specializing in rendering technology.
Xie Haochen – ByteDance Flutter Infra Engineer, Impeller contributor.
ByteDance Terminal Technology
Official account of ByteDance Terminal Technology, sharing technical insights and team updates.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.