Linux DRM/KMS 嵌入:在无窗口管理器(No-X11/Wayland)环境下运行 Flutter

好的,下面开始关于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_currentclear_currentpresent。这些回调函数需要根据具体的 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 渲染、更高级的输入处理和更完善的平台通道实现,以提升性能和功能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注