Flutter 渲染引擎详解 - iOS Metal 篇

时间:2022-07-28
本文章向大家介绍Flutter 渲染引擎详解 - iOS Metal 篇,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

作者:易旭昕 原文链接:https://zhuanlan.zhihu.com/p/214099612 本文由作者授权发布。

写作费时,敬请点赞,关注,收藏三连。

Flutter 渲染引擎在 iOS 上支持三种渲染方式,分别是纯软件(CPU),Metal 和 GL。其中纯软件的方式仅限于特定的构建,需要在编译时开启 TARGET_IPHONE_SIMULATOR 宏,应该是用于在模拟器上的测试,实机运行只会使用 Metal 和 GL。Flutter 会在运行时先判断是否能够使用 Metal,如果设备不支持,才会降级到 GL。iOS 10 以上的版本默认使用 Metal,GL 只用于兼容 iOS 9 的老旧设备。

这篇文章的主要内容是讲解在 iOS 上,Flutter 渲染引擎:

  1. 需要的 Metal GPU 上下文环境是如何完成初始化;
  2. 目标输出 Surface 的设置过程;
  3. 渲染流水线执行光栅化的调用过程。

上图显示了 Flutter 渲染引擎在 iOS 上主要涉及的对象,绿色背景是 iOS SDK 原生对象,黄色背景是平台相关的适配对象,白色背景是平台无关的通用对象。后面的内容我们会频繁地引用图中的对象,这张图可以方便读者了解它们之间的关系。

Metal GPU 上下文环境初始化

上图显示了 iOS 应用在主线程初始化 Flutter Engine 的调用栈。FlutterViewController 在被系统初始化时创建了 FlutterEngine,并请求 engine 创建 Shell 对象,FlutterEngine 在 Shell 对象的创建过程中生成了 PlatformViewIOS 对象并将它传递给 Shell。

std::unique_ptr<IOSContext> IOSContext::Create(IOSRenderingAPI rendering_api) {
  switch (rendering_api) {
    case IOSRenderingAPI::kOpenGLES:
      return std::make_unique<IOSContextGL>();
    case IOSRenderingAPI::kSoftware:
      return std::make_unique<IOSContextSoftware>();
#if FLUTTER_SHELL_ENABLE_METAL
    case IOSRenderingAPI::kMetal:
      return std::make_unique<IOSContextMetal>();
#endif  // FLUTTER_SHELL_ENABLE_METAL
    default:
      break;
  }
  FML_CHECK(false);
  return nullptr;
}

PlatformViewIOS 一个主要的职责就是创建 IOSContext 对象,由它来为渲染引擎提供 GPU 上下文环境,在使用 Metal API 的情况下,创建的实际上是 IOSContextMetal 对象。

IOSContextMetal::IOSContextMetal() {
  device_.reset([MTLCreateSystemDefaultDevice() retain]);
  main_queue_.reset([device_ newCommandQueue]);
  ...
  main_context_ = GrContext::MakeMetal([device_ retain], [main_queue_ retain]);
  resource_context_ = GrContext::MakeMetal([device_ retain], [main_queue_ retain]);
  ...
}

从上面我们可以看到在 IOSContextMetal 的构造函数里面,它要做的就是:

  1. 创建或者获取系统默认的 MTLDevice(MTLCreateSystemDefaultDevice);
  2. 创建 MTLCommandQueue;
  3. 使用前面创建的 MTLDevice 和 MTLCommandQueue 分别创建两个 Skia GrContext,main context 用于在 raster 线程光栅化,resource context 用于在 io 线程做纹理上传;
sk_sp<GrContext> GrContext::MakeMetal(void* device, void* queue, const GrContextOptions& options) {
    sk_sp<GrContext> context(new GrLegacyDirectContext(GrBackendApi::kMetal, options));

    context->fGpu = GrMtlTrampoline::MakeGpu(context.get(), options, device, queue);
    if (!context->fGpu) {
        return nullptr;
    }

    if (!context->init(context->fGpu->refCaps())) {
        return nullptr;
    }
    return context;
}

Skia 内部创建了 GrLegacyDirectContext 和 GrMtlGpu 对象,在 GrMtlGpu 保持了对传递进来的 MTLDevice 和 MTLCommandQueue 对象的引用,后续它会使用 MTLCommandQueue 对象创建 MTLCommandBuffer 对象用于执行 GPU 绘图指令。

到目前为止,我们已经完成了 Metal GPU 上下文环境的初始化,并创建了两个 Skia GrContext 分别用于后续的 Skia 光栅化和纹理上传。但是为了完成真正的光栅化和屏幕输出,我们还需要获取目标输出的 Surface。

设置目标输出 Surface

当 FlutterViewController 加载 View 结束后被系统回调 viewDidLoad,触发了 PlatformViewIOS::attachView 被调用。

void PlatformViewIOS::attachView() {
  ios_surface_ =
      [static_cast<FlutterView*>(owner_controller_.get().view) createSurface:ios_context_];
  ...
}

PlatformViewIOS::attachView 通过 FlutterViewController 获取 FlutterView,然后调用它的 createSurface 方法创建 IOSSurface,传递 IOSContext 对象作为参数。

- (std::unique_ptr<flutter::IOSSurface>)createSurface:
    (std::shared_ptr<flutter::IOSContext>)ios_context {
  return flutter::IOSSurface::Create(
      std::move(ios_context),                              // context
      fml::scoped_nsobject<CALayer>{[self.layer retain]},  // layer
      [_delegate platformViewsController]                  // platform views controller
  );
}

FlutterView::createSurface 调用 IOSSurface::Create 方法创建 IOSSurface 对象,并传递自己的 layer 对象作为参数,在使用 Metal API 的情况下,layer 对象实际是 CAMetalLayer,创建的 IOSSurface 实际上是 IOSSurfaceMetal。IOSSurfaceMetal 的实现比较简单,它实际就是用来持有 IOSContextMetal 和 CAMetalLayer 用于后续创建 GPUSurfaceMetal,Surface 的子类。

系统调用 FlutterViewController::viewDidLayoutSubviews 通知它 FlutterView 布局计算完成,大小已经确定,会触发 PlatformView::NotifyCreated 被调用。在这里,主线程会同步请求 raster 线程创建 Rendering Surface,实际上就是请求之前创建的 IOSSurfaceMetal 创建 GPUSurfaceMetal。

GPUSurfaceMetal::GPUSurfaceMetal(GPUSurfaceDelegate* delegate,
                                 fml::scoped_nsobject<CAMetalLayer> layer,
                                 sk_sp<GrContext> context,
                                 fml::scoped_nsprotocol<id<MTLCommandQueue>> command_queue)
    : delegate_(delegate),
      layer_(std::move(layer)),
      context_(std::move(context)),
      command_queue_(std::move(command_queue)) {
  ...
}

GPUSurfaceMetal 实际上就是用来持有 CAMetalLayer 图层对象,IOSContextMetal 创建的光栅化用的 GrContext 对象,和 IOSContextMetal 创建的 MTLCommandQueue 对象。GPUSurfaceMetal 对象最终通过 Shell 传递给 Rasterizer 持有,到这里光栅化器就完成了目标输出 Surface 的设置,现在我们可以开始绘制第一帧了。

光栅化输出

关于 Flutter 渲染流水线比较完整的说明请参考我之前的文章Flutter 渲染流水线浅析,在这里我们只关注光栅化的部分。Flutter 光栅化的过程比较简单:

  1. 从目标输出的 Surface,也就是 CAMetalLayer,获取一个像素缓冲器( CAMetalDrawable 封装了该缓冲器);
  2. 将这个像素缓冲器包装成一个 SkSurface 对象,并取得对应的 SkCanvas 对象;
  3. 将生成的图层树里面的 DisplayList(SkPicture)通过上面的 SkCanvas 逐个绘制到 SkSurface 上,Skia 会先存储经过预处理的 2D 绘图指令;
  4. Flush SkCanvas,相当于生成相应的 Metal GPU 绘图指令,Encode 到 CommandBuffer,最后请求 Metal 执行;
  5. 等待执行完毕后,请求提交绘制完成的像素缓冲器,并请求 iOS 重绘 UI,CAMetalLayer 在被绘制的过程中输出新的像素缓冲器到屏幕上;
RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) {

  auto frame = surface_->AcquireFrame(layer_tree.frame_size());

  SkMatrix root_surface_transformation = surface_->GetRootTransformation();

  auto root_surface_canvas = frame->SkiaCanvas();

  auto compositor_frame = compositor_context_->AcquireFrame(
      surface_->GetContext(),       // skia GrContext
      root_surface_canvas,          // root surface canvas
      external_view_embedder,       // external view embedder
      root_surface_transformation,  // root surface transformation
      true,                         // instrumentation enabled
      frame->supports_readback(),   // surface supports pixel reads
      raster_thread_merger_         // thread merger
  );

  if (compositor_frame) {
    RasterStatus raster_status = compositor_frame->Raster(layer_tree, false);
    frame->Submit();
    return raster_status;
  }

  return RasterStatus::kFailed;
}

上面的代码显示了一个简化后的光栅化器光栅化图层树的流程(不考虑使用 ExternalViewEmbedder 的场景):

  1. Rasterizer 首先调用 GPUSurfaceMetal::AcquireFrame 获取一个 SurfaceFrame;
  2. 然后通过 SurfaceFrame 获取用于绘制目标缓冲器的 SkCanvas(frame->SkiaCanvas());
  3. 然后将 SkCanvas 包裹成一个 CompositorContext::ScopedFrame 对象,并请求它光栅化图层树(compositor_frame->Raster(layer_tree, false));
  4. 最后调用 SurfaceFrame::Submit 提交绘制结果(frame->Submit());
std::unique_ptr<SurfaceFrame> GPUSurfaceMetal::AcquireFrame(const SkISize& frame_size) {
  auto surface = SkSurface::MakeFromCAMetalLayer(context_.get(),            // context
                                                 layer_.get(),              // layer
                                                 kTopLeft_GrSurfaceOrigin,  // origin
                                                 1,                         // sample count
                                                 kBGRA_8888_SkColorType,    // color type
                                                 nullptr,                   // colorspace
                                                 nullptr,                   // surface properties
                                                 &next_drawable_  // drawable (transfer out)
  );

  auto submit_callback = [this](const SurfaceFrame& surface_frame, SkCanvas* canvas) -> bool {
    canvas->flush();

    auto command_buffer =
        fml::scoped_nsprotocol<id<MTLCommandBuffer>>([[command_queue_.get() commandBuffer] retain]);

    fml::scoped_nsprotocol<id<CAMetalDrawable>> drawable(
        reinterpret_cast<id<CAMetalDrawable>>(next_drawable_));
    next_drawable_ = nullptr;

    [command_buffer.get() commit];
    [command_buffer.get() waitUntilScheduled];
    [drawable.get() present];

    return true;
  };

  return std::make_unique<SurfaceFrame>(std::move(surface), true, submit_callback);
}

SkCanvas* SurfaceFrame::SkiaCanvas() {
  return surface_ != nullptr ? surface_->getCanvas() : nullptr;
}

上面的代码显示了简化后的 GPUSurfaceMetal::AcquireFrame 处理流程:

  1. 首先是使用初始化时获得的 GrContext 和 CAMetalLayer 生成一个 SkSurface,并获得 SkSurface 通过 CAMetalLayer 创建的 CAMetalDrawable 对象,Skia 在内部会使用该 CAMetalDrawable 作为 SkSurface 的像素缓冲器;
  2. 创建供 SurfaceFrame::Submit 调用的回调函数对象;
  3. 将上面的生成的 SkSurface 和 Submit Callback 封装成 SurfaceFrame 输出;
  4. SurfaceFrame::SkiaCanvas 返回 SkSurface 对应的 SkCanvas 对象供光栅化器使用;

当 SurfaceFrame::Submit 的时候:

  1. Flush 绘制图层树完毕的 SkCanvas,相当于请求 GrContext 创建一个 MTLCommandBuffer,然后再创建绑定 SkSurface 包装的 CAMetalDrawable 的 MTLRenderCommandEncoder 对象,根据输入的 2D 绘制指令生成 Metal GPU 绘制指令并 Encode,最后结束 Encode 并 Commit MTLCommandBuffer;
  2. 然后创建一个新的 MTLCommandBuffer 等待其被调度,这样可以保证前面的 MTLCommandBuffer 被执行完毕;
  3. 最后调用 CAMetalDrawable::present 方法,提交绘制完成的像素缓冲器,并请求 iOS 重绘;

如果读者对更多的具体细节感兴趣的话,可以去阅读 Skia 内部的实现代码,这部分相对来说就比较复杂了。