好的,下面开始关于Linux DRM/KMS嵌入式环境下运行Flutter的技术讲座。
Linux DRM/KMS 嵌入:在无窗口管理器(No-X11/Wayland)环境下运行 Flutter
大家好,今天我们来深入探讨如何在嵌入式 Linux 系统上,在没有 X11 或 Wayland 等传统窗口管理器的环境下运行 Flutter 应用。这对于资源受限的嵌入式设备,以及需要更精细控制图形输出的应用场景尤为重要。
1. DRM/KMS 简介
DRM (Direct Rendering Manager) 是 Linux 内核中处理图形设备的核心子系统。KMS (Kernel Mode Setting) 是 DRM 的一部分,负责设置显示模式,例如分辨率、刷新率等。它们共同提供了一种直接操作硬件图形设备的方式,绕过了 X Server 或 Wayland Compositor。
-
DRM 的作用:
- 管理图形设备(GPU)。
- 分配和管理显存。
- 提供渲染上下文。
- 处理 VSync 信号。
-
KMS 的作用:
- 设置显示模式。
- 控制显示 pipeline (例如:双缓冲)。
- 处理热插拔事件。
2. 为什么选择 DRM/KMS?
- 资源效率: 避免了运行重量级的窗口管理器,降低了内存和 CPU 占用。
- 性能: 直接访问硬件,减少了中间层,提高了渲染效率。
- 控制: 允许对图形输出进行更精细的控制,例如定制显示流程。
- 启动速度: 无需启动窗口管理器,缩短了启动时间。
3. Flutter 与 DRM/KMS 的集成挑战
Flutter 引擎默认设计依赖于窗口系统提供的表面 (Surface) 来渲染。在没有窗口管理器的情况下,我们需要自己提供一个符合 Flutter 引擎要求的 Surface,并将其与 DRM/KMS 结合起来。主要的挑战包括:
- Surface 创建: 需要使用 DRM API 创建 framebuffer,并将其作为 Flutter 引擎的渲染目标。
- 输入处理: 需要直接从输入设备(例如触摸屏、键盘)读取事件,并将它们转换为 Flutter 引擎可以理解的格式。
- 平台通道: 需要实现 Flutter 平台通道,以便 Flutter 代码可以与底层 DRM/KMS 系统交互。
- 线程模型: 需要正确处理 Flutter 引擎的线程模型,确保渲染和事件处理在正确的线程上执行。
- EGLContext 创建: 需要使用eglCreateContext和eglMakeCurrent创建EGL上下文并绑定到DRM/KMS。
4. 实现步骤
下面是一个实现 Flutter DRM/KMS 集成的基本步骤:
4.1 初始化 DRM/KMS
首先,我们需要初始化 DRM 设备并设置显示模式。
#include <fcntl.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <stdexcept>
#include <iostream>
class DRMContext {
public:
DRMContext(const char* device_path) {
fd_ = open(device_path, O_RDWR);
if (fd_ < 0) {
throw std::runtime_error("Failed to open DRM device");
}
// 获取资源信息
res_ = drmModeGetResources(fd_);
if (!res_) {
close(fd_);
throw std::runtime_error("Failed to get DRM resources");
}
// 找到第一个连接的显示器
connector_ = nullptr;
for (int i = 0; i < res_->count_connectors; ++i) {
connector_ = drmModeGetConnector(fd_, res_->connectors[i]);
if (connector_ && connector_->connection == DRM_MODE_CONNECTED) {
break;
}
drmModeFreeConnector(connector_);
connector_ = nullptr;
}
if (!connector_) {
drmModeFreeResources(res_);
close(fd_);
throw std::runtime_error("No connected connector found");
}
// 选择最佳模式
mode_ = nullptr;
for (int i = 0; i < connector_->count_modes; ++i) {
mode_ = &connector_->modes[i];
break; // 选择第一个模式,可以根据需求选择最佳模式
}
if (!mode_) {
drmModeFreeConnector(connector_);
drmModeFreeResources(res_);
close(fd_);
throw std::runtime_error("No mode found for connector");
}
// 找到可用的 encoder
encoder_ = drmModeGetEncoder(fd_, connector_->encoder_id);
if (!encoder_) {
drmModeFreeConnector(connector_);
drmModeFreeResources(res_);
close(fd_);
throw std::runtime_error("No encoder found for connector");
}
// 找到 framebuffer
uint32_t fb_id;
uint32_t handles[4] = {0}, pitches[4] = {0}, offsets[4] = {0};
int ret = drmModeAddFB2(fd_, mode_->hdisplay, mode_->vdisplay, DRM_FORMAT_XRGB8888,
handles, pitches, offsets, &fb_id, 0);
if (ret) {
drmModeFreeEncoder(encoder_);
drmModeFreeConnector(connector_);
drmModeFreeResources(res_);
close(fd_);
throw std::runtime_error("Failed to add framebuffer");
}
fb_id_ = fb_id;
// 激活模式
ret = drmModeSetCrtc(fd_, encoder_->crtc_id, fb_id_, 0, 0, &connector_->connector_id, 1, mode_);
if (ret) {
drmModeRmFB(fd_, fb_id_);
drmModeFreeEncoder(encoder_);
drmModeFreeConnector(connector_);
drmModeFreeResources(res_);
close(fd_);
throw std::runtime_error("Failed to set CRTC");
}
crtc_id_ = encoder_->crtc_id;
width_ = mode_->hdisplay;
height_ = mode_->vdisplay;
std::cout << "DRM initialized: " << width_ << "x" << height_ << std::endl;
}
~DRMContext() {
// 关闭 DRM
drmModeSetCrtc(fd_, crtc_id_, 0, 0, 0, &connector_->connector_id, 1, mode_);
drmModeRmFB(fd_, fb_id_);
drmModeFreeEncoder(encoder_);
drmModeFreeConnector(connector_);
drmModeFreeResources(res_);
close(fd_);
}
int fd() const { return fd_; }
uint32_t crtc_id() const { return crtc_id_; }
uint32_t fb_id() const {return fb_id_;}
int width() const { return width_; }
int height() const { return height_; }
private:
int fd_;
drmModeRes* res_;
drmModeConnector* connector_;
drmModeEncoder* encoder_;
drmModeModeInfo* mode_;
uint32_t crtc_id_;
uint32_t fb_id_;
int width_;
int height_;
};
int main() {
try {
DRMContext drm("/dev/dri/card0");
// 在此处添加 Flutter 引擎初始化和渲染代码
std::cout << "Flutter Engine will be initialized here" << std::endl;
sleep(5); // 保持运行一段时间
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
这段代码首先打开 DRM 设备,然后获取连接器、编码器和模式信息,最后设置 CRTC(Cathode Ray Tube Controller),激活显示。注意,这只是一个基本的示例,实际应用中可能需要更复杂的错误处理和模式选择逻辑。
4.2 创建 Flutter 引擎
接下来,我们需要创建 Flutter 引擎实例,并配置其渲染目标为 DRM framebuffer。这通常涉及到修改 Flutter 引擎的源码或者使用自定义的平台嵌入器。
#include "flutter_embedder.h"
#include "drm_context.h" // 假设 DRMContext 定义在此头文件中
// Flutter 引擎配置
FlutterRendererConfig CreateRendererConfig(DRMContext& drm_context) {
FlutterRendererConfig config = {};
config.type = kFlutterRendererTypeOpenGL;
config.open_gl.struct_size = sizeof(FlutterOpenGLRendererConfig);
config.open_gl.make_current = [](void* userdata) -> bool {
// 使用 EGL 或 GLX 创建 OpenGL 上下文,并将其绑定到 DRM framebuffer
// 这里需要根据具体的 OpenGL 实现进行调整
return true; // 成功返回 true,失败返回 false
};
config.open_gl.clear_current = [](void* userdata) -> bool {
// 清除当前 OpenGL 上下文
return true;
};
config.open_gl.present = [](void* userdata) -> bool {
// 交换 framebuffer,显示渲染结果
// 使用 DRM API 进行 buffer 交换
return true;
};
config.open_gl.fbo_callback = [](void* userdata) -> uint32_t {
// 返回 DRM framebuffer 的 ID
return drm_context.fb_id();
};
config.open_gl.gl_proc_resolver = [](void* userdata, const char* procname) -> void* {
// 获取 OpenGL 函数指针
return eglGetProcAddress(procname); // 假设使用 EGL
};
return config;
}
FlutterProjectArgs CreateProjectArgs() {
FlutterProjectArgs args = {};
args.struct_size = sizeof(FlutterProjectArgs);
args.assets_path = "flutter_assets"; // Flutter assets 目录
args.icu_data_path = "icudtl.dat"; // ICU 数据文件
args.platform_plugins_c_api_register_callback = [](FlutterPluginRegistry* registry,
void* user_data) {
// 注册平台插件
};
return args;
}
int main() {
try {
DRMContext drm("/dev/dri/card0");
// 创建 Flutter 引擎
FlutterEngine engine = nullptr;
FlutterRendererConfig renderer_config = CreateRendererConfig(drm);
FlutterProjectArgs project_args = CreateProjectArgs();
FlutterEngineResult result =
FlutterEngineInitialize(&renderer_config, &project_args, &engine);
if (result != kFlutterEngineSuccess) {
std::cerr << "Failed to initialize Flutter engine: " << result << std::endl;
return 1;
}
// 运行 Flutter 引擎
FlutterEngineRunResult run_result = FlutterEngineRun(engine, nullptr, "main");
if (run_result != kSuccess) {
std::cerr << "Failed to run Flutter engine: " << run_result << std::endl;
FlutterEngineShutdown(engine);
return 1;
}
// 等待 Flutter 引擎完成渲染
sleep(5);
// 关闭 Flutter 引擎
FlutterEngineShutdown(engine);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
这段代码展示了如何配置 Flutter 引擎的渲染器,使其使用 OpenGL 上下文,并将渲染目标设置为 DRM framebuffer。CreateRendererConfig 函数负责设置 OpenGL 相关的回调函数,例如 make_current、clear_current 和 present。这些回调函数需要根据具体的 OpenGL 实现和 DRM API 进行调整。CreateProjectArgs 函数负责设置 Flutter 项目的参数,例如 assets 目录和 ICU 数据文件。
4.3 处理输入事件
我们需要直接从输入设备读取事件,并将它们转换为 Flutter 引擎可以理解的格式。这通常涉及到使用 Linux 的 input 子系统。
#include <linux/input.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
// 假设 FlutterEngineSendPointerEvent 函数存在于 Flutter 引擎 API 中
// void FlutterEngineSendPointerEvent(FlutterEngine engine, const FlutterPointerEvent* event);
class InputHandler {
public:
InputHandler(FlutterEngine engine, const char* input_device_path) : engine_(engine) {
fd_ = open(input_device_path, O_RDONLY);
if (fd_ < 0) {
throw std::runtime_error("Failed to open input device");
}
}
~InputHandler() {
close(fd_);
}
void ProcessEvents() {
struct input_event event;
while (read(fd_, &event, sizeof(event)) > 0) {
if (event.type == EV_ABS) {
// 处理触摸事件
if (event.code == ABS_MT_POSITION_X) {
x_ = event.value;
} else if (event.code == ABS_MT_POSITION_Y) {
y_ = event.value;
}
// 构造 FlutterPointerEvent
FlutterPointerEvent pointer_event = {};
pointer_event.struct_size = sizeof(FlutterPointerEvent);
pointer_event.phase = kDown; // 假设是按下事件,需要根据实际情况调整
pointer_event.x = x_;
pointer_event.y = y_;
pointer_event.timestamp = event.time.tv_sec * 1000 + event.time.tv_usec / 1000; // 毫秒
// 发送事件到 Flutter 引擎
// FlutterEngineSendPointerEvent(engine_, &pointer_event);
std::cout << "Touch event: x=" << x_ << ", y=" << y_ << std::endl;
} else if (event.type == EV_KEY) {
// 处理按键事件
std::cout << "Key event: code=" << event.code << ", value=" << event.value << std::endl;
}
}
}
private:
int fd_;
FlutterEngine engine_;
int x_ = 0;
int y_ = 0;
};
int main() {
try {
// 假设已经初始化了 Flutter 引擎
FlutterEngine engine = nullptr; // 需要正确初始化
InputHandler input_handler(engine, "/dev/input/event0");
while (true) {
input_handler.ProcessEvents();
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
这段代码展示了如何从输入设备读取事件,并将触摸事件转换为 Flutter 引擎可以理解的格式。ProcessEvents 函数负责读取输入事件,并根据事件类型进行处理。EV_ABS 类型表示绝对坐标事件,通常用于触摸屏。EV_KEY 类型表示按键事件。
4.4 实现平台通道
我们需要实现 Flutter 平台通道,以便 Flutter 代码可以与底层 DRM/KMS 系统交互。例如,Flutter 代码可能需要获取屏幕分辨率或者设置显示模式。
#include "flutter_embedder.h"
#include <iostream>
#include <string>
// 定义平台通道名称
const char* kPlatformChannelName = "com.example.platform_channel";
// 处理平台通道消息的回调函数
static void HandlePlatformMessage(const FlutterPlatformMessage* message,
void* user_data) {
if (message->message == nullptr) {
std::cerr << "Received null message" << std::endl;
return;
}
std::string message_str(reinterpret_cast<const char*>(message->message),
message->message_size);
std::cout << "Received message: " << message_str << std::endl;
// 根据消息内容执行相应的操作
std::string response_str = "Response from native code: " + message_str;
// 创建响应消息
FlutterPlatformMessageResponseHandle* response_handle = message->response_handle;
if (response_handle != nullptr) {
FlutterPlatformMessageResponse message_response = {
.struct_size = sizeof(FlutterPlatformMessageResponse),
.callback = [](const uint8_t* response, size_t response_size,
void* user_data) {
// 释放响应消息的内存
free((void*)response);
},
.user_data = nullptr,
};
// 复制响应消息的内容到堆上
uint8_t* response_data = (uint8_t*)malloc(response_str.size());
memcpy(response_data, response_str.data(), response_str.size());
// 发送响应消息
FlutterEngineSendPlatformMessageResponse(
static_cast<FlutterEngine>(user_data), response_handle, response_data,
response_str.size());
}
}
// 注册平台通道
void RegisterPlatformChannel(FlutterEngine engine) {
FlutterEngineRegisterPlatformMessageCallback(
engine, kPlatformChannelName, HandlePlatformMessage, engine);
}
int main() {
// 假设已经初始化了 Flutter 引擎
FlutterEngine engine = nullptr; // 需要正确初始化
//注册平台通道
RegisterPlatformChannel(engine);
std::cout << "Platform channel registered." << std::endl;
sleep(10);
return 0;
}
这段代码展示了如何注册一个平台通道,并处理来自 Flutter 代码的消息。RegisterPlatformChannel 函数负责注册平台通道的回调函数。HandlePlatformMessage 函数负责处理平台通道消息,并根据消息内容执行相应的操作。
5. 代码组织和编译
- 将 DRM 初始化、Flutter 引擎集成、输入处理和平台通道的代码组织成模块化的组件。
- 使用 CMake 或其他构建系统来编译代码。
- 确保链接 Flutter 引擎库和相关的 DRM/OpenGL 库。
6. 优化和调试
- 使用性能分析工具来识别性能瓶颈。
- 优化 OpenGL 渲染代码,减少 GPU 负载。
- 使用调试工具来调试 DRM/KMS 相关的错误。
- 确保输入事件处理的正确性,避免延迟或丢帧。
表格:DRM/KMS 相关 API
| API 函数 | 描述 | 头文件 |
|---|---|---|
drmOpen |
打开 DRM 设备 | <xf86drm.h> |
drmModeGetResources |
获取 DRM 资源信息 | <xf86drmMode.h> |
drmModeGetConnector |
获取连接器信息 | <xf86drmMode.h> |
drmModeGetEncoder |
获取编码器信息 | <xf86drmMode.h> |
drmModeSetCrtc |
设置 CRTC | <xf86drmMode.h> |
drmModeAddFB2 |
创建 framebuffer | <xf86drmMode.h> |
drmModeRmFB |
删除 framebuffer | <xf86drmMode.h> |
eglGetDisplay |
获取 EGL 显示连接 | <EGL/egl.h> |
eglInitialize |
初始化 EGL | <EGL/egl.h> |
eglChooseConfig |
选择 EGL 配置 | <EGL/egl.h> |
eglCreateContext |
创建 EGL 上下文 | <EGL/egl.h> |
eglMakeCurrent |
绑定 EGL 上下文到当前线程 | <EGL/egl.h> |
eglSwapBuffers |
交换 framebuffer | <EGL/egl.h> |
FlutterEngineInitialize |
初始化 Flutter 引擎 | flutter_embedder.h |
FlutterEngineRun |
运行 Flutter 引擎 | flutter_embedder.h |
FlutterEngineShutdown |
关闭 Flutter 引擎 | flutter_embedder.h |
FlutterEngineSendPointerEvent |
发送指针事件到 Flutter 引擎 | flutter_embedder.h |
FlutterEngineRegisterPlatformMessageCallback |
注册平台消息回调函数 | flutter_embedder.h |
FlutterEngineSendPlatformMessageResponse |
发送平台消息回复 | flutter_embedder.h |
7. 总结:关键步骤和注意事项
总而言之,在无窗口管理器环境下运行 Flutter 需要直接与 DRM/KMS 交互,处理 Surface 创建、输入事件和平台通道。正确理解和实现这些关键步骤是成功集成的关键。
8. 未来展望:进一步的探索和优化方向
未来可以进一步探索 Vulkan 渲染、更高级的输入处理和更完善的平台通道实现,以提升性能和功能。