☰
深入解析 Flutter Engine Embedder GLFW 脏区域渲染示例(glfw_drm)
2026/11/5 23:39:17 网站建设 项目流程

深入解析 Flutter Engine Embedder GLFW 脏区域渲染示例(glfw_drm)

【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter

Flutter 官方提供了一套针对 Android、iOS、Web 与桌面平台的现成渲染宿主,而本篇示例glfw_drm面向的是另一类更进阶的受众:需要把 Flutter 内容渲染到自己全新宿主环境中的引擎嵌入(Embedder)开发者。该示例以 GLFW 为窗口与事件宿主、以 EGL 为图形接口,演示了如何在自定义宿主中启动 Flutter 引擎,并且区别于普通版 GLFW 示例,完整实现了基于 Flutter 引擎提供的frame_damage/buffer_damage信息进行脏区域管理(Dirty Region Management)的渲染管线。读完本文,你将掌握 Embedder 渲染回调的整体装配方式、FlutterPresentInfo与FlutterDamage的底层语义,以及如何用 EGL 的 partial update / swap buffers with damage 扩展只重绘发生变化的帧区域,把这份经验迁移到任何需要自研宿主渲染的应用场景。

一、示例定位:为什么需要自定义 Embedder

Flutter 的官方运行环境(Android、iOS、桌面、Web)本质上都围绕同一个引擎内核,外围则由各平台的“Embedder”负责提供窗口系统、事件输入、渲染表面与应用生命周期。当你需要支持 Flutter 官方尚未覆盖的宿主环境时,就需要使用 C/C++ 层的 Embedder API 自行承担这些职责。官方在 Custom-Flutter-Engine-Embedders.md 中有体系化的说明,而本目录(engine/src/flutter/examples/glfw_drm)则是一个可以直接编译运行的最小实例。

该示例与同目录姊妹示例 glfw(GLFW 基础版) 的核心差异正如其 README 所述:glfw_drm 实现了脏区域管理——不再每帧无条件渲染/提交整幅画面,而是只提交两帧之间发生变化的区域,从而降低 GPU 与合成开销。要理解这套机制,建议先阅读仓库内 Flutter 引擎自定义 Embedder 说明。

需要留意的是:这是进阶主题,面向已经能独立构建(或获取)Flutter Engine 的开发者,不适合初学者直接上手。

二、目录结构与运行前提

示例目录(engine/src/flutter/examples/glfw_drm/)内的文件非常精简,职责边界清晰:

文件职责
FlutterEmbedderGLFW.cc全部 C++ 宿主代码:GLFW 窗口、EGL 表面、Embedder 渲染/输入回调、脏区域算法
CMakeLists.txtCMake 构建脚本,负责找到 GLFW、EGL 与 Flutter 引擎库
run.sh一键脚本:依次编译 C++ 宿主、生成 Flutter 工程并产出 assets、最后启动嵌入程序
main.dart被嵌入的 Flutter “guest” 应用源码
BUILD.gn引擎源码树内部的 GN 构建目标(输出名embedder_example_drm)

运行环境与依赖

README 明确指出:本示例在 Linux 上验证过,通过少量调整可移植到其他 *nix 与 Windows,但由于依赖 EGL,不兼容 macOS。依赖清单如下:

  • GLFW—— 窗口、事件与 EGL 上下文管理,sudo apt-get install libglfw3;
  • CMake—— 构建工具,sudo apt-get install cmake;
  • EGL—— 图形表面抽象层,README 给出的命令为sudo apt-get install libglfw3-dev;
  • Flutter SDK—— 用于生成 guest 工程与构建 assets(安装方式见 flutter.dev 官方文档 对应入门页);
  • Flutter Engine—— 可以本地构建或直接下载现成产物(详见 Custom-Flutter-Engine-Embedders.md)。

一键构建运行

在满足上述依赖的前提下,进入本示例目录直接执行:

./run.sh

run.sh 内部会依次完成三件事:

  1. 编译 C++ 宿主程序:在debug/子目录执行cmake -DCMAKE_BUILD_TYPE=Debug .. && make;
  2. 准备 guest Flutter 工程:若debug/myapp不存在,则flutter create myapp并flutter pub add flutter_spinkit;随后将仓库内的main.dart覆盖进lib/,并以flutter build bundle生成运行所需资源,同时显式指定本地引擎:
flutter build bundle \ --local-engine-src-path ../../../../../ \ --local-engine=host_debug_unopt \ --local-engine-host=host_debug_unopt

这里host_debug_unopt对应本地构建出的 Flutter 引擎目标名(即下文 CMake 在out/host_debug_unopt中查找引擎库的原因); 3.启动嵌入程序:执行./flutter_glfw ./myapp ../../../third_party/icu/common/icudtl.dat,把 Flutter 工程目录与引擎所需的icudtl.dat国际化数据文件作为命令行参数传入。

从 GN 构建目标看输出命名

除 CMake 外,示例还提供 GN 构建入口:BUILD.gn 在build_embedder_examples开关开启时编译glfw_drm,输出名为embedder_example_drm,并链接//flutter/shell/platform/embedder、//flutter/third_party/glfw与EGL库。这与FlutterEmbedderGLFW.cc中printUsage()打印的embedder_example_drm <path to project> <path to icudtl.dat>用法一一对应,说明两个构建体系最终产出的可执行文件用途一致。

三、CMake 构建脚本解析:三个容易踩坑的查找路径

CMakeLists.txt 虽然简短,却是官方指出的最主要排障点。它把工程拆成三段:

(1)宿主可执行目标

cmake_minimum_required(VERSION 3.15) project(FlutterEmbedderGLFW) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11" ) add_executable(flutter_glfw FlutterEmbedderGLFW.cc)

即编译生成可执行文件flutter_glfw(注意与 GN 的输出名embedder_example_drm不同,两者是各自独立构建体系下的产物名)。

(2)GLFW 依赖

find_package(OpenGL REQUIRED COMPONENTS EGL) include_directories(${OPENGL_INCLUDE_DIRS}) add_subdirectory(${CMAKE_SOURCE_DIR}/../../third_party/glfw glfw) target_link_libraries(flutter_glfw glfw OpenGL::EGL) include_directories(${CMAKE_SOURCE_DIR}/../../third_party/glfw/include)

这里不是去系统查找 GLFW,而是把 Flutter 引擎仓库内置的 GLFW 源码以add_subdirectory的方式直接编译,并关闭其示例/测试/文档目标。CMake 需要能够通过find_package(OpenGL ... COMPONENTS EGL)找到 EGL。

(3)Flutter 引擎依赖

include_directories(${CMAKE_SOURCE_DIR}/../../shell/platform/embedder) find_library(FLUTTER_LIB flutter_engine PATHS ${CMAKE_SOURCE_DIR}/../../../out/host_debug_unopt) target_link_libraries(flutter_glfw ${FLUTTER_LIB})

embedder.h头文件来自引擎源码树内 shell/platform/embedder,引擎动态库则假定位于out/host_debug_unopt——这正是本地构建 Flutter Engine 的产物目录。构建脚本末尾还有一段POST_BUILD自定义命令,把引擎动态库复制到当前二进制目录,以便运行时通过动态链接直接加载。

README Troubleshooting 一节提到的主要改动点全部集中在这份 CMake 文件与宿主源码中:

  • Flutter Engine 位置:若引擎头文件/库不在上述假设位置,需修改include_directories与find_library的路径;
  • GLFW 位置:若找不到 GLFW,需检查上面的add_subdirectory相对路径或换成系统安装的 GLFW;
  • Pixel Ratio:若画面缩放比例不对,需要调整FlutterEmbedderGLFW.cc中的kPixelRatio(源码中的实际变量是g_pixelRatio,初始为 1.0,见下文)。

四、宿主程序装配全流程:从 GLFW 窗口到引擎渲染回调

FlutterEmbedderGLFW.cc 是整套示例的灵魂。它把“如何用 Embedder API 接活一个 Flutter 引擎”拆解为清晰可复制的若干步骤。

4.1 版本守卫与关键常量

文件顶部首先用static_assert(FLUTTER_ENGINE_VERSION == 1, ...)锁定 Embedder API 版本,注释特别提醒:若 API 出现严重破坏性变更,需要先阅读 ChangeLog 再决定是否升级断言。随后定义了一组贯穿全文的关键常量与全局状态:

static double g_pixelRatio = 1.0; // 窗口创建后由实际帧缓冲尺寸反算 static const size_t kInitialWindowWidth = 800; // 初始窗口宽 static const size_t kInitialWindowHeight = 600; // 初始窗口高 // 最大损伤历史长度——三重缓冲需要记录最近两帧;部分 Android 设备(如 Pixel 4)使用四重缓冲 static const int kMaxHistorySize = 10; static constexpr FlutterViewId kImplicitViewId = 0; std::list<FlutterRect> damage_history_; // 最近若干帧的 frame damage std::unordered_map<intptr_t, FlutterRect*> existing_damage_map_; // 各 FBO 已分配的 existing damage EGLDisplay display_; EGLSurface surface_;

其中kImplicitViewId = 0对应 Flutter 多视图(multi-view)机制中的“隐式视图”:本示例只支持单窗口,因此所有窗口指标事件与指针事件都标记为发生在唯一的隐式视图上。损伤历史列表与 FBO 映射则是脏区域算法的核心数据结构,将在第五节详述。

4.2 窗口创建与 EGL 表面获取

main()的执行顺序体现了宿主初始化的典型路径:

  1. 参数校验:argc != 3时打印usage: embedder_example_drm <path to project> <path to icudtl.dat>并退出;
  2. glfwSetErrorCallback注册错误回调,glfwInit()初始化 GLFW;
  3. 在 Linux 上通过glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API)强制使用 EGL 创建上下文(这是本示例绑定 EGL 的起点,也是 README 声明“不兼容 macOS”的原因所在);
  4. glfwCreateWindow(800, 600, "Flutter", ...)创建窗口;
  5. 由glfwGetFramebufferSize得到的物理像素尺寸除以逻辑窗口宽(800)得到pixel ratio,存入全局变量g_pixelRatio——这正是 README 中“画面比例不对就调g_pixelRatio”的落点;
  6. 通过glfwGetEGLDisplay()/glfwGetEGLSurface(window)取出本示例后续提交帧所需的display_与surface_;
  7. 调用RunFlutter(window, project_path, icudtl_path)启动引擎;
  8. 注册键盘(ESC 关闭窗口)、窗口尺寸变化、鼠标按键等 GLFW 回调;
  9. 进入主循环:while (!glfwWindowShouldClose(window)) glfwWaitEvents();——即事件驱动、空闲挂起的窗口循环。

4.3 用 FlutterRendererConfig 装配渲染回调

RunFlutter首先构造并填充FlutterRendererConfig:

FlutterRendererConfig config = {}; config.type = kOpenGL; // 声明走 OpenGL/EGL 渲染路径 config.open_gl.struct_size = sizeof(config.open_gl); config.open_gl.make_current = [](void* userdata) -> bool { glfwMakeContextCurrent(static_cast<GLFWwindow*>(userdata)); return true; }; config.open_gl.clear_current = [](void*) -> bool { glfwMakeContextCurrent(nullptr); return true; };

并依次挂上后续小节要展开的几个关键回调:present_with_info、fbo_callback、populate_existing_damage、gl_proc_resolver,同时设置config.open_gl.fbo_reset_after_present = true(每次 present 后重置 FBO)。这些回调分别对应 embedder.h 中FlutterOpenGLRendererConfig的字段:FBO 回调返回当前帧缓冲对象 ID,gl_proc_resolver用glfwGetProcAddress为引擎解析 GL 函数地址,而present_with_info/populate_existing_damage正是脏区域机制的核心接口。

4.4 引擎启动与窗口指标同步

随后准备FlutterProjectArgs并启动引擎:

std::string assets_path = project_path + "/build/flutter_assets"; FlutterProjectArgs args = { .struct_size = sizeof(FlutterProjectArgs), .assets_path = assets_path.c_str(), // 由 flutter build bundle 生成 .icu_data_path = icudtl_path.c_str(), // 通常位于引擎 bin/cache }; FlutterEngine engine = nullptr; FlutterEngineResult result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, window, &engine);

注意assets_path的拼接假设:guest 工程下必须存在由flutter build bundle生成的build/flutter_assets目录,这也是 run.sh 先执行flutter build bundle的原因。启动成功后,把引擎句柄写入窗口用户指针(glfwSetWindowUserPointer),并主动调用一次GLFWwindowSizeCallback(window, 800, 600),把初始窗口尺寸以FlutterWindowMetricsEvent形式同步给引擎,从而触发第一帧布局。

4.5 事件上报:把输入翻译成 Embedder 事件

示例演示了三类输入事件的翻译,全部遵循“坐标乘g_pixelRatio+ 时间戳取微秒 +view_id指向隐式视图”的模板:

  • 指针事件:GLFWcursorPositionCallbackAtPhase构造FlutterPointerEvent,按 GLFW 状态封装为kMove/kDown/kUp相位,再调用FlutterEngineSendPointerEvent(engine, &event, 1)上报。鼠标左键按下时开启光标位置监听,松开时移除监听,从而只在下按期间跟踪拖拽;
  • 窗口指标事件:窗口尺寸变化时GLFWwindowSizeCallback构造FlutterWindowMetricsEvent(宽、高均已乘以 pixel ratio),调用FlutterEngineSendWindowMetricsEvent通知引擎重新布局;
  • 键盘事件:ESC 键按下时通过glfwSetWindowShouldClose关闭窗口,由主循环退出。

这一小节是任何新宿主“让 Flutter 应用可交互”的必修课:引擎本身并不感知 GLFW,所有窗口坐标、指针相位、时序都必须由 Embedder 显式翻译。

五、脏区域管理:本示例的技术精华

README 明确说明,glfw_drm与同目录基础版 GLFW 示例的关键区别就在于实现了脏区域管理:只渲染两帧之间发生变化的区域,而不是整帧重绘。要实现这一点,需要同时具备两个前提:

  1. 引擎侧提供信息:在 present 回调中通过FlutterPresentInfo下发两套损伤信息——frame_damage(本帧相对上一帧的损伤区域)与buffer_damage(缓冲区内需要刷新的区域);
  2. EGL 侧提供能力:利用EGL_KHR_partial_update与EGL_EXT_swap_buffers_with_damage(或其 KHR 同名扩展)让合成器只消费被声明的区域。

从 embedder.h 中FlutterPresentInfo的结构可以看到,其frame_damage与buffer_damage字段均为FlutterDamage(由矩形数量与矩形数组组成),并通过注释特别说明该结构为冻结 ABI,追加成员会破坏兼容性——这从侧面印证了损伤区域 API 是 Embedder 渲染协议中的一等公民。

5.1 三个辅助函数

脏区域算法建立在一组小工具之上:

static std::array<EGLint, 4> RectToInts(const FlutterRect rect); static void JoinFlutterRect(FlutterRect* rect, FlutterRect additional_rect); static bool HasExtension(const char* extensions, const char* name);
  • RectToInts:把逻辑坐标系下的FlutterRect(top-left 原点)转换为 EGL 期望的[x, y, width, height]整数数组,其中 y 轴必须做翻转(height - rect.bottom),因为 EGL 损伤区域的坐标系原点在左下角;
  • JoinFlutterRect:对两个矩形取包围盒(min 左/上、max 右/下),用于把历史多帧损伤合并为一个整体区域;
  • HasExtension:在扩展名字符串中精确匹配(要求扩展名后紧跟空格或\0),避免strstr误命中前缀相似的扩展名。

5.2 present 回调:提交帧并携带损伤区域

present_with_info回调完整实现如下逻辑(对应源码 FlutterEmbedderGLFW.cc):

  1. 释放上一帧为该 FBO 分配的 existing damage 内存,防止泄漏;
  2. 用eglQueryString(display_, EGL_EXTENSIONS)探测扩展。若支持EGL_KHR_partial_update,则通过eglGetProcAddress取回eglSetDamageRegionKHR;若支持EGL_EXT_swap_buffers_with_damage(否则退回EGL_KHR_swap_buffers_with_damage),则取回eglSwapBuffersWithDamageEXT/KHR;
  3. 声明缓冲损伤:若拿到eglSetDamageRegionKHR,把info->buffer_damage.damage[0]经RectToInts转换后作为eglSetDamageRegionKHR(display_, surface_, rects, 1)的参数,让驱动程序只认为这些区域需要修复;
  4. 记录帧损伤:把info->frame_damage.damage[0]压入damage_history_,若历史长度超过kMaxHistorySize(10)则弹出最旧的一帧——该容量上限的注释解释了原因:三重缓冲需要保存最近两帧损伤,而部分 Android 设备(如 Pixel 4)使用四重缓冲;
  5. 带损伤提交:若拿到了eglSwapBuffersWithDamageEXT类函数,则把本帧frame_damage区域转换后传入并返回调用结果;否则退化为传统整帧提交eglSwapBuffers(display_, surface_)(此时只能在缺少部分重绘扩展的平台上做全量重绘)。

这段回调清晰展示了一条能力降级链:EGL 扩展齐全 → 精确的“声明缓冲损伤 + 带损伤交换”;只有 swap-with-damage 而无 partial update → 仅声明交换损伤;扩展缺失 → 完整重绘。这样即使目标平台不支持扩展,示例也不会崩溃。

5.3 populate existing damage:推算“从 FBO 上次使用以来”的损伤

populate_existing_damage回调解决的问题是:在缓冲轮转(buffer age)机制下,嵌入端拿到的后备缓冲可能残留着数个帧前的画面,引擎需要知道这些“既有损伤”才能决定用 partial update 修复多少内容。其实现要点如下:

  • 通过glfwExtensionSupported("GL_EXT_buffer_age")判断扩展,再eglQuerySurface(display_, surface_, EGL_BUFFER_AGE_EXT, &age)查询当前缓冲年龄(即后备缓冲已被轮转使用的次数);若不支持该扩展,则直接把age置为 4,注释的解释是“实际驱动几乎没有交换链长度超过 4 的”;
  • 为当前 FBO 分配一块FlutterRect内存(放入existing_damage_map_),默认初始化为整窗矩形{0, 0, 800, 600},并将num_rects设为 1;
  • 当age > 1时,从damage_history_的末尾(最近一帧)向前合并最多age - 1个历史帧损伤——即缓冲年龄为几,就把最近几帧的变化全部并集起来,形成“上次使用该 FBO 至今”的真实损伤区域。这正是上一帧把 frame damage 压入历史列表的原因,也解释了为什么历史容量必须大于等于最大可能的后备缓冲深度。

把 5.2 与 5.3 连起来看,整个机制闭环是:引擎渲染后把 frame/buffer damage 交给宿主 → 宿主把 frame damage 记入历史、把 buffer damage 交给eglSetDamageRegionKHR→ 下一帧到来时宿主根据缓冲年龄回放历史损伤 →populate_existing_damage把“既有损伤”交还引擎 → 引擎据此只重绘真正变化的部分。相比基础版 GLFW 示例的整帧重绘,这就是“脏区域管理”的完整语义。

六、guest 侧 Flutter 工程:一段精心设计的演示 App

示例并不是随便找一个 Flutter 应用来“被嵌入”,main.dart 在启动处藏着一个对自研 Embedder 至关重要的细节:

void main() { // 让 Flutter 认为自己运行在 Fuchsia 上, // 否则会因“不支持的平台”而报错。 debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; runApp(MyApp()); }

由于自定义 Embedder 不在 Flutter SDK 官方支持的平台列表中,Dart 侧若按默认平台检查会直接拒绝启动,因此这里用debugDefaultTargetPlatformOverride把目标平台伪装成TargetPlatform.fuchsia以绕过限制。这是一个极具代表性的提醒:接新宿主时,Framework 的平台判定是必须在 guest 侧一并处理的关卡。

界面本身是一个标准 Material 计数 Demo,但主页 body 特意包了一层RepaintBoundary并放置一个不断旋转的加载指示器SpinKitRotatingCircle(来自flutter_spinkit包):

child: RepaintBoundary( child: SpinKitRotatingCircle(color: Colors.blue, size: 50.0), ),

旋转动画意味着每一帧都在产生局部变化,而这恰好能直观验证脏区域渲染是否生效——如果嵌入正确,屏幕刷新会集中在动画区域。这也是为什么 run.sh 在创建工程后要额外flutter pub add flutter_spinkit并把仓库自带的main.dart覆盖进工程的原因。

七、运行验证与排障要点

把前面各部分拼起来,完整的运行链路是:

  1. 准备好可用的 Flutter Engine(本地构建产出位于out/host_debug_unopt);
  2. apt-get安装 GLFW/CMake/EGL 相关依赖;
  3. 在示例目录执行./run.sh,等待宿主编译、guest 工程创建与flutter build bundle完成;
  4. 程序自动以./flutter_glfw ./myapp ../../../third_party/icu/common/icudtl.dat启动,出现 800×600 的 “Flutter” 窗口并播放旋转动画。

如果过程中出现问题,README 与源码共同指向以下几个高发排障点:

  • 引擎库/头文件找不到:检查 CMakeLists.txt 中include_directories(../shell/platform/embedder)与find_library(flutter_engine PATHS ../out/host_debug_unopt)是否指向你真实引擎所在位置;
  • GLFW 找不到:确认add_subdirectory使用的相对路径../../third_party/glfw有效(该路径相对于CMAKE_SOURCE_DIR,即本示例目录);
  • 画面比例不对/模糊或裁切:调整 FlutterEmbedderGLFW.cc 中窗口创建后由帧缓冲尺寸反算的g_pixelRatio计算逻辑;
  • 运行期报“不支持平台”:确认 guest 工程沿用仓库 main.dart 中的debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
  • build/flutter_assets不存在:确认已对 guest 工程执行过flutter build bundle(run.sh 已自动完成);
  • 平台兼容性:由于强依赖 EGL,示例不适合在 macOS 上运行;Linux 已验证可用。

八、小结:从这里出发,接入任意新宿主

glfw_drm的价值不在于 GLFW 本身,而在于它把“接入一个新宿主”所需的全部知识点压缩到了一个可运行示例中:用FlutterRendererConfig装配 OpenGL/EGL 回调、用FlutterProjectArgs提供 assets 与 ICU 数据、用窗口指标与指针事件喂给引擎、用FlutterPresentInfo与populate_existing_damage打通脏区域渲染。如果你需要把 Flutter 引入游戏引擎、车载系统、专业显示设备或任何 Flutter 官方未覆盖的宿主环境,这份示例加上 Custom-Flutter-Engine-Embedders.md 的协议说明,就是最直接的起点;而若暂时不需要局部刷新优化,可以先从基础版 glfw 示例 入门,再对照本示例逐步理解损伤区域机制的每一环。

【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询