ข้ามไปยังเนื้อหา

Impeller and the GPU

The Render Pipeline อ้างวัตถุประสงค์หลักของ Impeller ไปแล้ว (“Predictable performance… compiles all shaders and reflection offline at build time”) และสถานะ default จริงต่อแพลตฟอร์มของ Flutter 3.47 — บทนี้ไม่ทำซ้ำทั้งสองอย่าง แต่ลงลึกไปอีกชั้น: สถาปัตยกรรม C++ ตัวจริงที่ Impeller สร้างขึ้นมา (ตรงจาก engine/src/flutter/impeller/README.md และ header file จริงที่ revision ที่คอร์สนี้ตรึงไว้) และแก้ข้อความอ้างที่ทีม Flutter Deep Dive ภายในจับได้ในร่างก่อนหน้าของคอร์สนี้ — ว่า Impeller “รวม UI thread กับ raster thread เข้าด้วยกัน” ซึ่งไม่จริง นั่นไม่ใช่แม้แต่การรวมที่เกิดขึ้นจริง การรวมตัวจริง อ้างไว้ด้านล่าง ต่างออกไป ใหม่กว่า และเกิดแค่สองแพลตฟอร์มเท่านั้น

impeller/README.md ของ engine เองที่ revision ที่คอร์สนี้ตรึงไว้ (6a19cca564 — ยืนยันแล้วว่าเหมือนกันระหว่าง path ของ framework และ engine เพราะ Flutter 3.x รวมทั้งสองเข้าเป็น monorepo เดียว) ระบุวัตถุประสงค์ทั้งห้าของ Impeller ตรงตัว — The Render Pipeline อ้างข้อแรกไปแล้ว อีกสี่ข้อใหม่ที่นี่:

“Instrumentable: All graphics resources (textures, buffers, pipeline state objects, etc..) are tagged and labeled. Animations can be captured and persisted to disk without affecting per-frame rendering performance.” “Portable: Not tied to a specific client rendering API. Shaders are authored once and converted as necessary.” “Uses Modern Graphics APIs Effectively: Makes heavy use of (but doesn’t depend on) features available in Modern APIs like Metal and Vulkan.” “Makes Effective Use of Concurrency: Can distribute single-frame workloads across multiple threads if necessary.”

(impeller/README.md#L18-L29)

หัวข้อ Project Organization ของ README เดียวกันระบุชื่อโมดูลที่แน่นอน — อ้างตรงตัว:

  • //impeller/renderer: “The very lowest level of the renderer that is still backend agnostic… Has utilities for creating allocators, generating pipeline state objects from bindings generated by //impeller/compiler, setting up render passes, managing jumbo uniform-buffers, tessellators, etc.”
  • //impeller/entity: “Sits one level above //impeller/renderer and provides a framework for building 2D renderers. Most of the pipeline state objects generated from shaders authored at build time reside in this framework.”
  • //impeller/display_list: “The replacement for //impeller/aiks to serve in the integration of Impeller in //flutter/flow… This sub-framework primarily provides a custom implementation of the flutter::DisplayListDispatcher that forwards Flutter rendering intent to Impeller.”

บรรทัดกลางนั้นน่าหยุดคิด: //impeller/aiks ไม่มีอยู่เป็นโมดูลของตัวเองอีกต่อไป ใน engine revision ที่คอร์สนี้ติดตั้งไว้ — มันถูกพับเข้าไปใน //impeller/display_list ซึ่ง README ระบุตรง ๆ ว่าเป็นตัวสืบทอดของมันโดยตรง Aiks (ชื่อเล่นภายใน “An Impeller Kit for Skia”) ยังอยู่รอดวันนี้แค่ในฐานะชิ้นเล็ก ๆ ข้างใน display_list: ไฟล์จริงที่ revision นี้เป๊ะ impeller/display_list/aiks_context.h กำหนด class AiksContext — doc comment ของมันเอง: “Construct a new AiksContext. context: The Impeller context that Aiks should use for allocating resources and executing device commands.” ถ้าเห็น “Aiks” ถูกอธิบายที่อื่นว่าเป็น public drawing API ระดับบนสุดของ Impeller คำอธิบายนั้นเก่ากว่า revision ที่คอร์สนี้ตรึงไว้ — ขอบเขตโมดูลย้ายแล้ว ชื่อรอดมาแค่บางส่วน

doc comment ของ Entity เอง: “Represents a renderable object within the Impeller scene. An Entity combines graphical content (Contents) with properties like transformation (Matrix), blend mode (BlendMode), and stencil clip depth. It serves as the primary unit for constructing and rendering scenes.” Contents (ไฟล์เดียวกันคือ contents/contents.h) คือ payload ที่วาดได้ซึ่ง Entity แบกไว้ — signature ของ RenderProc คือ bool Render(const ContentContext& renderer, const Entity& entity, RenderPass& pass) หมายความว่างานจริงของ Contents subclass ทุกตัวคือบันทึกงานวาดของมันลงใน RenderPass ซึ่ง doc comment ของมันเองคือ: “Render passes encode render commands directed as one specific render target into an underlying command buffer.”

flowchart TD
  dl["display_list
(DisplayListDispatcher -- Flutter's rendering intent arrives here)"] --> aiks["AiksContext
(the surviving piece of the old //impeller/aiks)"]
  aiks --> entity["entity
(Entity + Contents -- 2D renderer framework, PSOs built at compile time live here)"]
  entity -->|"Contents::Render(...) writes into"| pass["RenderPass
(encodes commands into a command buffer)"]
  pass --> renderer["renderer
(backend-agnostic: allocators, PSOs, command buffers)"]
  renderer --> backend["renderer/backend
(Metal / Vulkan / OpenGL -- platform-specific)"]
Entity + Contents บันทึกงานลงใน RenderPass ที่มี renderer แบบ backend-agnostic ข้างใต้

กลไกที่ The Render Pipeline ตั้งชื่อไปแล้ว — Skia compile shader ครั้งแรกที่เจอ draw operation แบบสด ๆ กลางเฟรม — คือต้นทุนจริงที่ engine source ของคอร์สนี้เองระบุปริมาณตรง ๆ ContentContextOptions struct ที่เป็น key ของ per-pipeline variant cache ของ Impeller มี doc comment นี้: “Pipeline state configuration. Each unique combination of these options requires a different pipeline state object to be built. This struct is used as a key for the per-pipeline variant cache. When adding fields to this key, reliant features should take care to limit the combinatorical explosion of variations. A sufficiently complicated Flutter application may easily require building hundreds of PSOs in total, but they shouldn’t require e.g. 10s of thousands.” นี่คือรูปร่างที่แน่นอนของ “precompiled at build time”: ไม่ใช่ shader ก้อนเดียว แต่เป็น PSO variant ที่อาจมีหลายร้อยตัว (หนึ่งตัวต่อการผสมที่มีความหมายของ blend mode, stencil mode, และตัวเลือกคล้ายกัน) ทุกตัวถูกสร้างโดย impellerc (compiler shader แบบ offline ที่ README ตั้งชื่อไว้) ก่อนที่แอปจะรันเลยด้วยซ้ำ — ไม่มีต้นทุนผสมผสานตัวไหนถูกจ่ายแบบสด ๆ บนเฟรม เหมือนที่ compile แบบ runtime ของ Skia ทำ

สถานะต่อแพลตฟอร์ม และความคลาดเคลื่อนจริงที่ควรพูดตรง ๆ

หัวข้อที่มีชื่อว่า “สถานะต่อแพลตฟอร์ม และความคลาดเคลื่อนจริงที่ควรพูดตรง ๆ”

The Render Pipeline ครอบคลุมสถานะ default ต่อแพลตฟอร์มที่เป็นทางการสำหรับ Flutter 3.47 ของคอร์สนี้ไปแล้ว (iOS แบบเดียว, Android default บนอุปกรณ์รุ่นใหม่, macOS/Windows/Linux default ตั้งแต่ 3.47) — บทนี้ไม่ทำซ้ำ สิ่งที่ควรพูดตรง ๆ แทน: ข้อความ CLI ที่ติดตั้งของ SDK นี้เองไม่ตรงกับ docs.flutter.dev เรื่อง default ปัจจุบันของ Android การรัน flutter run --help -v บน Mac เครื่องนี้เป๊ะ (output จริง ไม่ต้องมี device สำหรับ --help):

$ flutter run --help -v
--[no-]enable-impeller Whether to enable the Impeller rendering engine. Impeller is the
default renderer on iOS. On Android, Impeller is available but not
the default. This flag will cause Impeller to be used on Android.
On other platforms, this flag will be ignored.

นั่นขัดแย้งกับข้อความปัจจุบันของ docs.flutter.dev/perf/impeller (fetch ระหว่างค้นคว้าบทนี้) ที่บอกว่า Impeller “available and enabled by default on Android API 29+” บท Render Pipeline ของคอร์สนี้เอง ที่ยืนยันแยกต่างหากกับ release note ทางการของ Flutter 3.27 ตกลงใช้ “default บนอุปกรณ์ Android รุ่นใหม่ (รองรับ Vulkan)” เป็นข้อความที่อ้างอิงถูกต้องแล้ว — ประเด็นตรงนี้แคบกว่าและมีประโยชน์กว่าการเลือกผู้ชนะ: ข้อความ --help ของ CLI เองล้าหลังกว่า docs ปัจจุบันได้จริง แม้บน SDK ที่ติดตั้งอยู่จริงตัวนั้นเอง ปฏิบัติต่อข้อความ --help ของ tool ที่ติดตั้งไว้เหมือนภาพนิ่งครั้งหนึ่ง ไม่ใช่แหล่งข้อมูลที่มีชีวิต — ความสงสัยแบบเดียวกับที่คอร์สนี้สอนต่อข้อความอ้างที่ AI generate

นอกเหนือจาก CLI flag ที่อ้างข้างบน docs.flutter.dev/perf/impeller ให้วิธีจริงระดับ embedding ในการปิด Impeller ในแอปที่ shipped แล้ว (ไม่ใช่แค่ตอน flutter run):

  • Android, AndroidManifest.xml: <meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
  • iOS, Info.plist: <key>FLTEnableImpeller</key><false/>

ทั้งสองคือ key ที่มีเอกสารจริงที่ embedding อ่านตอน startup — ต่างจาก CLI flag flutter run --no-enable-impeller ซึ่งมีผลแค่ตอนรัน debug/profile จาก tool ไม่ใช่แอปที่ build และติดตั้งแล้ว

docs.flutter.dev/tools/devtools/performance อธิบายสอง thread ที่เวลาของเฟรมถูกแบ่งข้าม ด้วยคำพูดของมันเอง:

“The UI thread executes Dart code in the Dart VM. This includes code from your application as well as the Flutter framework. When your app creates and displays a scene, the UI thread creates a layer tree, a lightweight object containing device-agnostic painting commands, and sends the layer tree to the raster thread to be rendered on the device.”

“The raster thread executes graphics code from the Flutter Engine. This thread takes the layer tree and displays it by talking to the GPU (graphic processing unit)… Skia, the graphics library, runs on this thread. Impeller also uses this thread.”

สอง thread ที่แยกกันจริง ๆ ตรงตามที่คำอธิบาย showPerformanceOverlay จริงของ Production Concerns เองแสดงไว้ถูกต้องแล้ว (สองกราฟแยกกัน — “the UI thread’s per-frame time… and the raster thread’s per-frame time”) Impeller เปลี่ยนสิ่งที่รันบน raster thread (PSO ที่ precompile และการส่ง command buffer แทน compile shader แบบ runtime ของ Skia) — มันไม่ได้เปลี่ยนการแบ่ง thread เอง

ข้อความ help ตัวจริงที่ติดตั้งของ --trace-skia บน Mac เครื่องนี้:

$ flutter run --help -v
--trace-skia Enable tracing of Skia code. This is useful when debugging the raster thread
(formerly known as the GPU thread). By default, Flutter will not log Skia
code, as it introduces significant overhead that may affect recorded
performance metrics in a misleading way.

ควรพูดตรง ๆ: การค้นคว้าของบทนี้มองหา panel “Raster stats” เฉพาะทางใน DevTools ปัจจุบัน (feature จริงใน tooling ยุค Skia เก่า) และไม่พบว่ามีเอกสารรองรับบน docs.flutter.dev/tools/devtools/performance วันนี้ — tool ที่มีเอกสารจริงของ Performance view ปัจจุบันคือ Flutter frames chart, Frame analysis tab, Timeline events tab, และ (สำหรับคำถามเรื่องต้นทุน paint ที่บท Render Pipeline ของคอร์สนี้เองยกขึ้นมาพอดี) ตัวสลับสำหรับแสดง layer ของ Clip, Opacity, และ Physical Shape สภาพแวดล้อมนี้ไม่มี device ที่จะขับเคลื่อนสิ่งเหล่านี้จริง ๆ ดังนั้นหัวข้อนี้อ้างจาก docs ไม่ได้สาธิตจริง — พูดตรง ๆ ไม่ใช่เดา

Canvas.saveLayer — กลไกที่ Clip.antiAliasWithSaveLayer (การพิสูจน์ของ The Render Pipeline เอง) และ widget Opacity ที่ opacity เป็นเศษส่วน ต่างก็ใช้ในที่สุด — มีหัวข้อ “Performance considerations” ใน doc comment ของ dart:ui เอง อ้างตรงตัว:

“Generally speaking, saveLayer is relatively expensive. There are a several different hardware architectures for GPUs…, but most of them involve batching commands and reordering them for performance. When layers are used, they cause the rendering pipeline to have to switch render target (from one layer to another). Render target switches can flush the GPU’s command buffer, which typically means that optimizations that one could get with larger batching are lost. Render target switches also generate a lot of memory churn because the GPU needs to copy out the current frame buffer contents…, and then needs to copy it back in once the previous render target (layer) is restored.”

นี่คือกลไกที่แน่นอนเบื้องหลังคำแนะนำ “หลีกเลี่ยง saveLayer” ทุกอันในคอร์สนี้: ไม่ใช่ความช้าที่คลุมเครือ แต่คือต้นทุน GPU ที่มีชื่อเฉพาะ (การ flush command buffer + memory churn ของ frame buffer) ที่เกิดบน raster thread ที่พิสูจน์ข้างบน ทุกครั้งที่ layer บังคับให้สลับ render target ImageFilter.blur (ที่ BackdropFilter ใช้สำหรับ frosted-glass effect และ ImageFiltered) ผ่านกลไก offscreen-buffer เดียวกันนี้เพื่อใช้ Gaussian kernel ดังนั้นมันจึงสืบทอด cost profile นี้เป๊ะ — เหตุผลเดียวกับที่คอร์สนี้สอนไปแล้วให้หยิบมันมาใช้อย่างตั้งใจ ไม่ใช่เป็นค่า default

พิสูจน์แล้ว: image decode ยังเป็นต้นทุนจริงที่วัดได้ — แม้บน Impeller

หัวข้อที่มีชื่อว่า “พิสูจน์แล้ว: image decode ยังเป็นต้นทุนจริงที่วัดได้ — แม้บน Impeller”

Impeller กำจัด shader-compile jank ที่ขั้น raster; มันไม่ได้ทำอะไรกับต้นทุนของการแปลง byte ของภาพที่ถูกบีบอัด (PNG/JPEG/ฯลฯ) ให้เป็น pixel ดิบตั้งแต่แรก การ decode ที่จับเวลาจริงบน Mac เครื่องนี้:

test/decode_timing_test.dart
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter_test/flutter_test.dart';
Future<Uint8List> makePngBytes(int side) async {
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder);
final paint = ui.Paint();
for (var i = 0; i < 200; i++) {
paint.color = ui.Color.fromARGB(255, (i * 37) % 255, (i * 91) % 255, (i * 53) % 255);
canvas.drawCircle(ui.Offset((i * 13 % side).toDouble(), (i * 29 % side).toDouble()), 30, paint);
}
final picture = recorder.endRecording();
final image = await picture.toImage(side, side);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
return byteData!.buffer.asUint8List();
}
void main() {
test('instantiateImageCodec decode timing scales with image size', () async {
final small = await makePngBytes(64);
final large = await makePngBytes(2048);
final swSmall = Stopwatch()..start();
final codecSmall = await ui.instantiateImageCodec(small);
final frameSmall = await codecSmall.getNextFrame();
swSmall.stop();
final swLarge = Stopwatch()..start();
final codecLarge = await ui.instantiateImageCodec(large);
final frameLarge = await codecLarge.getNextFrame();
swLarge.stop();
// Real, printed numbers from this Mac -- see below.
// ignore: avoid_print
print('decode small (${frameSmall.image.width}x${frameSmall.image.height}): ${swSmall.elapsedMicroseconds}us');
// ignore: avoid_print
print('decode large (${frameLarge.image.width}x${frameLarge.image.height}): ${swLarge.elapsedMicroseconds}us');
expect(frameSmall.image.width, 64);
expect(frameLarge.image.width, 2048);
});
}
instantiateImageCodec decode timing scales with image size
small PNG bytes: 1166, large PNG bytes: 211695
decode small (64x64): 4650us
decode large (2048x2048): 11930us
00:00 +1: All tests passed!

(ตัวเลขจริงไม่ได้แก้ไขจากการรัน flutter test บน Mac เครื่องนี้ — เวลาผันแปรได้ในแต่ละรอบและแต่ละเครื่อง สิ่งที่พิสูจน์คือต้นทุนเชิงเปรียบเทียบ ไม่ใช่ตัวเลขสมบูรณ์ให้จำ)

11.93ms สำหรับ decode ก้อนใหญ่หนึ่งครั้งไม่ใช่ตัวเลขเล็กน้อยเมื่อเทียบกับงบเฟรม ~16.6ms (ตัวเลขของ The Render Pipeline เอง) — และการจับเวลานี้วัด latency เต็มของ await ที่โค้ดผู้เรียกเจอ ไม่ว่า thread ไหนจะทำงานจริงข้างใต้ ในความเป็นจริง Flutter สมัยใหม่ส่งงาน decode pixel จริงส่วนใหญ่ไปที่ IO thread ของ engine โดยเฉพาะเพื่อไม่บล็อก UI thread ตลอดการ decode เต็ม ๆ — แต่ UI thread ยังต้องบล็อกสั้น ๆ ตอน parse header ของภาพ และ issue ที่เปิดอยู่จริงของ flutter/flutter บันทึกว่า path ของ platform-codec บางแบบ (native HEIC decoder ของ iOS เรียกผ่าน ImageDescriptor::initEncoded) ถูกเรียกแบบ ซิงโครนัสบน UI thread ไม่ใช่ IO thread ซึ่งเป็น jank ประเภทที่ PSO precompilation ของ Impeller แตะไม่ถึงเลย เพราะมันไม่เกิดบน raster thread เลยด้วยซ้ำ — มันเกิดก่อนที่ขั้น paint ของเฟรมจะไปถึงด้วยซ้ำ precacheImage, cacheWidth/cacheHeight (เพื่อเลี่ยงการ decode pixel ที่จะถูกย่อทิ้งทันที) และการไม่ decode ภาพความละเอียดเต็มสำหรับ thumbnail คือทางแก้จริงที่ใช้ได้ — ไม่มีอันไหนเป็นการตั้งค่าของ Impeller เพราะต้นทุนนี้อยู่ต้นน้ำกว่าทุกอย่างที่ Impeller แตะถึง

ลองนึกภาพ pipeline ของ Skia เก่าเป็นโรงพิมพ์ที่ผสมหมึกสีใหม่แบบสด ๆ ครั้งแรกที่งานเรียกหามัน — ลูกค้าคนแรกที่ขอ “เขียวป่า” ต้องรอตอนที่หมึกถูกผสม impellerc ของ Impeller คือโรงพิมพ์เดียวกันที่ผสมหมึกทุกสีที่ catalog งานของโรงพิมพ์อาจต้องใช้ล่วงหน้า เป็นชุดใหญ่ ข้ามคืน ก่อนเปิดประตู — “หลายร้อย PSO ไม่ใช่หมื่น” ของ ContentContextOptions คือประมาณการของโรงพิมพ์เองว่า catalog นั้นต้องการสีกี่สีจริง ๆ Entity+Contents+RenderPass คือสายการผลิตภายในของโรงพิมพ์เอง (ออกแบบ → เลือกหมึก → รันแท่นพิมพ์จริง) ทั้งหมดเกิดบน raster thread ที่พิสูจน์ข้างบน อยู่ปลายน้ำของ “ใบสั่งซื้อ” layer tree ของ UI thread image decode คือผู้ขายคนละรายไปเลย — การแกะกล่องสินค้าที่พิมพ์มาล่วงหน้า — และไม่มีการผสมหมึกล่วงหน้าปริมาณเท่าไหร่ที่ช่วยเร่งการแกะกล่องใหญ่ได้

ทางเลือกข้อดีต้นทุน
PSO ที่ precompile (การออกแบบหลักของ Impeller พิสูจน์ข้างบน)ไม่มีการสะดุดจาก shader-compile สด ๆ ContentContextOptions จำกัดจำนวน variant อย่างตั้งใจพื้นที่ variant ต้องอยู่ในขอบเขตจริง ๆ (“หลายร้อย ไม่ใช่หมื่น”) — feature ของ engine ที่ทำให้ PSO combination ระเบิดจะบ่อนทำลายการออกแบบทั้งหมด
AiksContext/display_list เป็น integration surfaceจุดเชื่อมที่มีเอกสารและมั่นคงระหว่าง DisplayListDispatcher ของ Flutter กับภายในของ Impellerขอบเขตโมดูลเคยย้ายมาแล้ว (aiks → display_list) — โค้ดหรือ docs ที่อ้างชื่อโมดูลเก่ากำลังอธิบายประวัติศาสตร์ ไม่ใช่ revision นี้
saveLayer/ImageFilter.blureffect ทางภาพจริง (frosted glass, การ clip แบบ anti-aliased แม่นยำ) ที่ไม่มีทางเลือกอื่นต้นทุน GPU จริงที่มีชื่อ (flush command buffer, memory churn) พิสูจน์ข้างบนจาก doc comment ของ engine เอง — ไม่ฟรีแม้บน Impeller
การส่งงาน image decode ไปที่ IO threadเก็บต้นทุน decode ส่วนใหญ่ไว้นอก critical path ของ UI threadไม่ครอบคลุมทุกกรณี — GitHub issue จริงที่อ้างแล้วแสดงว่า path ของ platform-codec บางแบบยังบล็อก UI thread แบบซิงโครนัส cacheWidth/cacheHeight และ precacheImage คือทางแก้จริง ไม่ใช่ flag ของ Impeller
  • “Impeller รวม UI thread กับ raster thread เป็นตัวเดียว” พิสูจน์แล้วว่าผิดข้างบนด้วย docs ของ DevTools ที่อ้างจริง: UI thread กับ raster thread ยังแยกกันจริง ๆ การรวมจริงตั้งแต่ 3.29+ คือ UI+platform (iOS/Android) และไม่แตะ raster thread เลย
  • “//impeller/aiks คือ public API ระดับบนสุดปัจจุบันของ Impeller” พิสูจน์แล้วว่าผิดข้างบน ตรงจาก README ของ engine: aiks ถูกแทนที่ด้วย display_list; AiksContext รอดมาเป็น class เดียวข้างใน ไม่ใช่โมดูลเดี่ยว
  • “Impeller หมายความว่า image decode เร็วขึ้นด้วย” พิสูจน์แล้วว่าผิดข้างบนด้วย test ที่จับเวลาจริง: ต้นทุน decode เป็นเรื่องแยกต่างหากจริง ๆ จากอะไรก็ตามที่ PSO precompilation ของ Impeller แตะถึง และตาม GitHub issue จริง path decode ของ platform บางแบบยังรันแบบซิงโครนัสบน UI thread
  • “ข้อความ --help ของ CLI tool อัปเดตตรงกับ docs ทางการปัจจุบันเสมอ” พิสูจน์แล้วว่าผิดข้างบนด้วยการเทียบตรง ๆ: flutter run --help -v ของ SDK นี้เองกับ docs.flutter.dev/perf/impeller ไม่ตรงกันจริง ๆ เรื่อง default ปัจจุบันของ Impeller บน Android

โค้ด Flutter ที่ AI generate ซึ่งหยิบ effect แบบ frosted-glass หรือ blur มักห่อ BackdropFilter/ImageFilter.blur รอบ content ขนาดใหญ่ที่ rebuild บ่อย โดยไม่มี RepaintBoundary (การพิสูจน์ของ Layout and Paint Internals เอง) แยกมันออก — จ่ายต้นทุนจริงที่มีเอกสารของ saveLayer (พิสูจน์ข้างบน) ทุกครั้งที่มีอะไรใกล้เคียง rebuild ไม่ใช่แค่บริเวณที่ blur

// @expect-error compiles clean -- BackdropFilter's ImageFilter.blur forces the same expensive saveLayer-style offscreen pass proven above, and with no RepaintBoundary around it, every unrelated rebuild in this subtree re-triggers that cost
class FrostedHeader extends StatelessWidget {
const FrostedHeader({super.key, required this.content});
final Widget content;
@override
Widget build(BuildContext context) {
return Stack(
children: [
content,
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(color: Colors.white.withValues(alpha: 0.2)),
),
],
);
}
}

ก่อนเชื่อโค้ดที่ generate มาซึ่งหยิบ BackdropFilter/ImageFilter.blur/effect ที่ใช้ saveLayer ตรวจสอบ:

  • มี RepaintBoundary แยกบริเวณที่ blur/filter ออกจาก content ที่ไม่เกี่ยวข้องซึ่ง rebuild อยู่ใกล้ ๆ หรือไม่ — กลไกเดียวกับที่ Layout and Paint Internals พิสูจน์ด้วยการ reuse OffsetLayer?
  • ภาพ network/asset ขนาดใหญ่ถูก decode ที่ความละเอียดเต็มแค่เพื่อแสดงเป็น thumbnail เล็ก ๆ หรือไม่? cacheWidth/cacheHeight บน ImageProvider พิสูจน์ข้างบนว่ามีผลต่อต้นทุนการจับเวลา decode จริง คือทางแก้ — ไม่ใช่การตั้งค่าของ Impeller
  • โค้ดสมมติว่า --enable-impeller/FLTEnableImpeller ไม่เกี่ยวข้อง “เพราะ Impeller เปิดอยู่เสมอแล้ว” หรือไม่? พิสูจน์ข้างบน: iOS ไม่มีทาง opt-out แต่สถานะ default ของ Android ยังไม่ลงตัวจริง ๆ ระหว่างข้อความ --help ของ SDK นี้เองกับ docs ปัจจุบัน — แอปที่ต้อง render เหมือนกันข้ามอุปกรณ์ Android เก่าและใหม่อาจยังต้องทดสอบทั้งสอง path

🐛 หา bug ในโค้ดนี้

class ProductThumbnail extends StatelessWidget {
const ProductThumbnail({super.key, required this.imageUrl});
final String imageUrl;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 48,
height: 48,
child: Image.network(imageUrl, fit: BoxFit.cover),
);
}
}
เฉลย

Image.network ที่ไม่มี cacheWidth/cacheHeight decode ภาพต้นฉบับที่ความละเอียดเต็ม — พิสูจน์ข้างบนแล้วว่าเวลา decode (และหน่วยความจำ) ขึ้นกับจำนวน pixel และภาพสินค้าที่ถ่ายมาขนาด เช่น 3000x3000 สำหรับมุมมอง gallery เต็มจอ จ่ายต้นทุน decode เต็มแค่เพื่อถูกบีบลงใน thumbnail 48x48 ทางแก้: Image.network(imageUrl, fit: BoxFit.cover, cacheWidth: 96, cacheHeight: 96) (ประมาณ 2 เท่าของขนาดที่แสดงสำหรับจอความหนาแน่นสูง) — สิ่งนี้บอก ImageProvider ข้างใต้ให้ decode ที่ขนาดเป้าหมายที่เล็กลงมาก ลดต้นทุนจริงที่ test จับเวลาของบทนี้เองวัดไว้โดยตรง ไม่ใช่แค่ขนาดที่แสดงในที่สุด

ตาม README ของ engine เอง (อ้างข้างบน) เกิดอะไรขึ้นกับโมดูล //impeller/aiks?
อ้างตรงตัวจาก doc comment ของ ContentContextOptions เอง แอป Flutter ที่ "ซับซ้อนพอสมควร" ต้องการ PSO variant ประมาณเท่าไหร่?
ตาม docs.flutter.dev (อ้างข้างบน) ความสัมพันธ์ที่แท้จริงระหว่าง UI thread กับ raster thread คืออะไร?
อ้างตรงตัวข้างบนจาก docs.flutter.dev การรวม thread ตัวจริงที่เกิดขึ้นตั้งแต่ Flutter 3.29 คืออะไร และบนแพลตฟอร์มไหน?
พิสูจน์แล้วข้างบนด้วย test ที่จับเวลาจริง การวัดเวลา decode ของ instantiateImageCodec แสดงอะไร?
ตาม doc comment "Performance considerations" ของ Canvas.saveLayer เอง (อ้างข้างบน) อะไรที่ทำให้มันแพงบน GPU โดยเฉพาะ?