< 返回博客

使用 CALayer 进行跨进程渲染

Kyrylo Pshenychnyi

2021 年 6 月 18 日

您是否想开发一个具有多进程架构的 macOS 应用程序,其中一个进程中生成的图片应该在另一个进程的窗口中渲染? 如果您不仅需要显示静态图片,还需要显示 60fps 的 4k 全屏视频怎么办? 并且您不希望在客户的 Mac 上烧毁 CPU ;)如果这就是您所寻找的,那么本文就是为您准备的。

我的名字是凯里洛。 多年来,我一直在开发一个商业跨平台库,允许将 Chromium Web 浏览器控件集成到 Java 和 .NET 桌面应用程序中。 要将 Chromium 集成到第三方应用程序中,我们必须解决许多具有挑战性的目标。

最重要和最雄心勃勃的任务之一是渲染。 它的复杂性与 Chromium 多进程架构有关。 Chromium 在单独的 GPU 进程中渲染网页内容,我们需要在另一个 Java 或 .NET 进程中显示生成的内容。 这个问题可以这样描述:在进程B中显示进程A的图形内容:

与不同进程共享渲染结果

此任务在每个支持的平台上的解决方式有所不同。 例如,在 Windows 和 Linux 上,这是通过将 Chromium 窗口嵌入到目标应用程序窗口中来完成的。 这是本机 API 的常见用例:Win32 API 的 SetParent 或 XLib 的 ReparentWindow 函数。

不幸的是,这种方法在 macOS 上不起作用。 禁止将一个进程的窗口嵌入到另一个进程的窗口中。 然而,有一些方法可以解决这个问题。

在本文中,我将展示如何使用 CALayer 共享方法在 macOS 上的另一个进程的窗口中渲染在一个进程中创建的内容。 这个概念将通过一个简单的应用程序来说明。

Chromium 多处理架构

在本节中,我们简要回顾一下 Chromium 架构。

Chromium 的主要思想是使用单独的进程来显示网页。 每个进程都与其他进程以及系统的其余部分隔离。

在下图中,浏览器代表主进程(顶层窗口)。 渲染器负责解释和布局 HTML(浏览器选项卡)。

这样做是出于安全原因,并使 Chromium 类似于操作系统概念:一个应用程序(GPU、渲染进程等)崩溃不会导致整个系统崩溃。

Chromium 多进程架构(取自此处)

同样的概念也适用于图形。 Chromium 为 GPU 相关操作托管一个单独的进程。

Chromium GPU 进程

纹理共享

让我们回到从不同进程获取像素的最初任务。 在深入研究实现细节之前,有必要了解 macOS 中使用哪些实体进行渲染。

简化的 CoreAnimation 模型

在上图中您可以看到用于显示图形内容的主要对象以及它们之间的关系:

  • NSWindow 代表一个屏幕窗口。 该类提供了一个用于嵌入其他视图的区域,接受和分发由用户通过鼠标和键盘交互引起的事件;
  • UIView — 在其内容范围内呈现内容的基本构建块。 这是 UI 控件(如标签、按钮、滑块)的基类;
  • CALayer — 用于为视图提供后备存储的对象。 CALayer 实例也可用于在没有父视图的情况下显示视觉内容。 图层用于视图定制,例如添加半径、阴影、突出显示等。

我们对 CALayer 很感兴趣,因为它为内容显示提供了更大的灵活性。 确切地说,我们将使用它的子类——CALayerHost,它对于纹理共享至关重要。 那么什么是“纹理共享”呢?

纹理共享可以描述为将一个进程中创建的图形内容与另一个进程一起使用的过程。 这是本文中描述的多进程概念的实现。

在一个进程中创建图形内容 - 在不同的进程中显示

IOSurface

Apple 提供了用于所述目的的方法。 这是 IOSurface 框架。 该 API 基于 IOSurface 对象,可以通过称为 mach_ports 的低级 macOS 内核原语从远程进程访问该对象。 IOSurface 方法在 Chromium 中使用,我们在项目中使用它已经很长时间了。 它具有良好的性能和稳定的API。 然而,一段时间后,我们遇到了一个问题,即在 Chromium 中启用 IOSurface 渲染流程时,某些网站无法正常显示。 这促使我们研究 CALayerHost 方法,这是 macOS 上 Chromium 中的默认方法。

CALayerHost

正如我已经提到的,CALayerHost 是 CALayer 子类,它渲染另一个层的渲染上下文。 通过 CALayerHost 进行纹理共享涉及以下实体:

  • CAContext — CoreAnimation 对象,表示环境信息,用于跨进程共享 CALayer;
  • CALayerHost — 一个 CALayer 子类,可以渲染远程层的内容;
  • CAContextID — 用于标识 CAContext 的全局唯一标识符。 值得一提的是,这个令牌可以直接跨进程传递,而 mach_port 传递需要额外的操作。

渲染共享所需的类

例子

我将通过一个简单的应用程序来说明纹理共享的想法。 该应用程序将类似于 Chromium 结构,即针对不同目标的单独进程。

GPU 进程将使用 CALayer 作为后台缓冲区来完成所有绘图工作。 绘制完成后,这个CALayer的CAContextID就会被发送到主进程。 主进程将使用 CAContextID 创建 CALayerHost 对象,用于在创建的 NSWindow 对象上显示内容。

实施细节

该应用程序由两个进程组成:

  • 渲染器App——负责创建共享纹理;
  • 主机应用程序 — 显示在渲染器应用程序中创建的内容。

进程之间的通信是通过一个简单的 IPC 库进行的。

示例应用程序架构

上述结构是一种很好的客户端-服务器方法,其中 Host App 向 Renderer App 发出相应的请求。

让我们浏览一下该应用程序的重要部分。

main 函数随后启动这两个进程。 要渲染的纹理数量由常量变量配置:

声明必要的API

CALayerHost 类以及层共享所需的其他对象是一个运行时 API。 因此它必须在我们的代码中显式声明。

渲染器应用程序

渲染器应用程序完成两个主要任务:

  • 通过 OpenGL API 渲染内容。 这是在继承自CAOpenGLLayer的ClientGlLayer类中完成的;
  • 通过 CAContext 和 IPC 库将新创建的层公开给主机应用程序。

共享 CALayer 的代码很简单:

  • 初始化CAContext;
  • 通过 CAContext::setLayer() 方法公开要导出的图层;
  • 通过 IPC 库将 CAContext 的标识符传递给渲染器应用程序。 它将用于创建 CALayerHost 实例。

主机应用程序

Host App 的主要目标是显示 Renderer App 中渲染的内容。 每一层都由一个单独的 NSWindow 提供服务。 首先,我们需要使用从渲染器应用程序进程接收到的 CAContextID 来初始化 CALayerHost:

为了方便起见,我们将接收到的 id 保存到 context_id 字段中。 然后,CALayerHost 必须嵌入到新创建窗口的 NSView 中:

重要提示:setWantsLayer 属性必须设置为 YES,以便视图将使用 CALayer 来管理其呈现的内容。

下面您可以看到两个窗口的应用程序结果。 请注意,每个窗口在渲染应用程序中使用相同的 CALayer。

使用两个窗口运行应用程序

您可以在此处找到示例应用程序的完整源代码以及构建指令。

结论

将 GPU 渲染放入单独的进程中是 Chromium 这样的大型项目中常用的方法。 这提高了整体产品的安全性并提高了代码的可维护性。 在本文中,我们测试了基于 CALayer 共享的方法。 所描述主题的主要思想是让一个进程在 CALayer 上执行绘图,并为另一个可以在目标视图中显示渲染图层的进程共享最终图层。





   |    备案号:京ICP备09015132号-1044