1. VIZ的三个端

在设计层面上 viz 的架构如下图所示:

在设计上 viz 分了三个端,分别是 client 端, host 端和 service 端。

client 端用于生成要显示的画面(CF)。应用中至少有一个 root client,可以有多个 child client,它们组成了一个 client 树,每个 Client 都有一个 FrameSinkId 和一个 LocalSurfaceId,如果父子 client 之间的 UI 需要嵌入,则子 client 作为 SurfaceDrawQuad 嵌入到父 client 中。在 Chromium 中,每一个浏览器窗口都对应一个 client 树,拥有一个 root client 和零个或多个子 client。比如,网页中的一个 OOPIF 可以是一个子 client,OffscreenCanvas 也是一个子 client。每个 client 可以独立进行刷新,生成独立的 CF。client 端的核心接口为 viz::mojom::CompositorFrameSinkClient, 开发者可以通过继承该类来创建一个 client。

host 端用于将 client 注册到 service,只能运行在特权端(没有沙箱,比如浏览器进程),负责协助 client 建立起和 service 的连接,也负责建立 client 和 client 之间的树形关系。所有的 client 想要生效必须经过 host 进行注册。host 端的核心接口为 viz::mojom::FrameSinkManagerClient,以及其实现类 viz::HostFrameSinkManager

service 端运行 Viz 的内核,进行 UI 合成以及最终的渲染。它接收所有 client 端生成的 CF,然后把这些 CF 进行合成,并最终显示在窗口中。它内部会为每个 client 创建一个 viz::CompositorFrameSink(Impl/Support),然后通过 viz::Display 将这些 CF 进行合成,最后通过 viz::DirectRenderer 将这些 CF 渲染到 viz::OutputSurface 中,viz::OutputSurface 包装了各种渲染目标,比如窗口,内存等。

在 Chromium 的具体实现中 viz 的架构可以用下图表示:

绿色部分为 client 的实现,负责提交 CF 到 GPU, 橙色部分 ui::Compositor 为 host 的实现,负责将所有的 client 注册到 service,红色部分为 service 端的实现,负责接收、合成,并最终渲染 CF。

2. VIZ 的线程架构

viz 是多线程架构,其中最重要的有两个线程,一个是 VizCompositorThread,也称为(viz的) Compositor 线程(注意和 cc 中的 Compositor 线程区分,它们没有关系)。另一个是 CrGpuMain (多进程架构下)或者 Chrome_InProcGpuThread(单进程架构下)线程,也称为 GPU 线程(只有在开启了硬件加速渲染时才存在)。帧率的调度,CF 的合成,DrawQuad 的绘制都发生在Compositor线程中,viz::Displayviz::DisplaySchedulerviz::SurfaceAggregatorviz::DirectRenderer 也运行在该线程中。而所有最终真实的绘制(比如 GL 的执行,Real SwapBuffer)最终都运行在 CrGpuMain 或 Chrome_InProcGpuThread 线程中。

如果要在架构上体现这两个线程,可以这样划分:

虚线之上运行在 Compositor 线程,虚线之下运行在 GPU 线程,OutputSurface 需要对接 DirectRenderer 和最下层的渲染,所以不同部分运行在不同的线程中。

目前 Compositor 线程和 GPU 线程之间的数据传递有两种方式,一种是先将 GL 调用放入进程内的 CommandBuffer,然后在 GPU 线程中取出 GL 命令并执行,GLOutputSurface 使用这种方式。另一种是直接通过 PostTask 将要渲染的操作发送到 GPU 线程,SkiaOutputSurface 使用这种方式(关于 GLOutputSurface 和 SkiaOutputSurface 见下文)。

3. VIZ 的类图

下面是 viz 更详细的类图:

  • viz::mojom::CompositorFrameSinkClient 作为 client,表示一个画面的来源;
  • viz::CompositorFrameSink(Impl/Support) 用于处理 CF 的地方,一个 client 可以有多个 CompositorFrameSink;
  • viz::RootCompositorFrameSinkImpl 作用和 CompositorFrameSink 类似,只不过专门处理 root client,每个 client 有且只有一个该对象。它还负责为对应的 client 初始化渲染环境,包括 OutputSurface, Display 的创建。
  • viz::FrameSinkManagerImpl 用于管理 CompositorFrameSink, 包括其创建和销毁;
  • viz::SurfaceAggregator 负责 Surface/CF 的合成,比如dirty区域的计算等,不负责绘制;
  • viz::OutputSurface 封装渲染目标,和各平台的渲染目标直接对接;
  • viz::DirectRenderer 封装绘制 DrawQuad 的方式,负责将 DrawQuad 绘制到 OutputSurface 上;
  • viz::Display 一个中控类,将 SurfaceAggregator/DirectRenderer 以及 Overlay 的功能串起来形成流水线;
  • viz::DisplayScheduler 调度 viz::Display 何时应该采取行动;

4. VIZ 的渲染目标

viz::DirectRenderer 和 viz::OutputSurface 用于管理渲染目标 。他们对理解 Chromium UI 的呈现方式至关重要。这两个类并不是相互独立的,在 Chromium 中他们有以下组合:

    1. viz::GLRenderer + viz::GLOutputSurface
      GLRenderer 已经被标记为 deprecated, 未来会被 SkiaRenderer 取代。它使用基于 CommandBuffer 的 GL Context 来渲染 DrawQuad 到 GLOutputSurface 上,GLOutputSurface 使用窗口句柄创建 Native GL Context。GL 调用发生在 VizCompositorThread 线程中,通过 InProcessCommandBuffer 这些 GL 调用最终在 CrGpuMain 线程中执行。关于 CommandBuffer 相关内容可以参考 Chromium Command Buffer
      GLOutputSurface 有一系列的子类,不同的子类对接不同平台的渲染目标,比如 GLOutputSurfaceAndroid 用于对接Android平台的渲染,GLOutputSurfaceOffscreen 用于支持 GL 的离屏渲染等。

    2. viz::SkiaRenderer + viz::SkiaOutputSurface(Impl) + viz::SkiaOutputDevice
      SkiaRenderer 是未来的发展方向,以后所有其他的渲染方式都会被这种方式取代。因为它具有最大的灵活性,同时支持GL渲染,Vulkan渲染,离屏渲染等。
      SkiaRenderer 将 DrawQuad 绘制到由 SkiaOutputSurfaceImpl 提供的 canvas 上,但是该 canvas 并不会进行真正的绘制动作,而是通过 skia 的 ddl(SkDeferredDisplayListRecorder) 机制把这些绘制操作记录下来,等到所有的 RenderPass 绘制完成,这些被记录下来的绘制操作会被通过 SkiaOutputSurfaceImpl::SubmitPaint 发送到 SkiaOutputSurfaceImplOnGpu 中进行真实的绘制,根据名字可知该类运行在 GPU 线程中。
      SkiaOutputSurface 对渲染目标的控制是通过 SkiaOutputDevice 实现的,后者有很多子类,其中 SkiaOutputDeviceOffscreen 用于实现离屏渲染,SkiaOutputDeviceGL 用于GL渲染。

    3. viz::SoftwareRenderer + viz::SoftwareOutputSurface + viz::SoftwareOutputDevice
      SoftwareRenderer 用于纯软件渲染,当关闭硬件加速的时候使用该种渲染方式。这种方式逻辑相对简单,因此留给读者去探索

5. VIZ 的数据流

viz 中的核心数据为 CF,当用户和网页进行交互时,会触发 UI 的改变,这最终会导致 viz client (比如 cc::LayerTreeHostImpl) 创建一个新的 CF 并使用 viz::mojom::CompositorFrameSink 接口将该 CF 提交到 service,service 中的 viz::CompositorFrameSinkSupport 会为该 CF 所属的 client 创建一个 viz::Surface,然后把 CF 放入该 viz::Surface 中。当 servcie 中的 viz::DisplayScheduler 到达下一个调度周期的时候,会通知 viz::Display 取出当前的 viz::Surface,并交给 viz::SurfaceAggregator 进行合成,合成的结果会被交给 viz::DirectRenderer 进行渲染。viz::DirectRenderer 并不直接渲染,它从 viz::OutputSurface 中取出渲染表面,然后让其子类在该渲染表面上进行绘制。viz::OutputSurface 封装了渲染表面,比如窗口,内存bitmap等。绘制完成之后,viz::Display 会调用 viz::DirectRenderer::SwapBuffer 将该 CF 最终显示出来。

service 绘制 CF 的核心逻辑位于 viz::Display::DrawAndSwap 中,下面是其主要的执行逻辑:

6. VIZ 的分层

从分层架构的角度看,viz 的 API 分了3层,分别为最底层核心实现,中间层mojo接口,最上层viz服务。

最底层是viz的核心实现,主要接口包括 viz::FrameSinkManager(Impl)viz::CompositorFrameSink(Support)viz::Displayviz::OutputSurface。 使用这些接口是使用 viz 最直接的方式,提供最大的灵活性。但这层接口不提供夸进程通信的能力。 Chromium中直接使用这一层的接口的地方不多,具体demo参考 chromium_demo/demo_viz_offscreen.cc at c/80.0.3987 · keyou/chromium_demo

中间mojo层主要将底层API包装到 mojo 接口中,这一层的核心接口包括 viz::HostFrameSinkManager,viz::mojom::FrameSinkManager,viz::mojom::CompositorFrameSink,viz::mojom::CompositorFrameSinkClient,viz::HostDisplayClient。这些接口大多对应底层的 API 接口,将对底层接口的调用转换为对mojo接口的调用,因此这层接口提供夸进程通信的能力。Chromium中几乎所有使用viz的地方都是使用该层接口。viz模块提供的官方demo也是使用该层接口,也可以参考我写的单文件demo,见 chromium_demo/demo_viz_gui.cc at c/80.0.3987 · keyou/chromium_demo

最上层viz服务接口主要将 viz 服务化,提供将viz运行在独立进程的能力。这一层的主要接口包括 viz::GpuHostImplviz::GpuServiceImplviz::VizMainImplviz::Gpu。这些接口需要和中间层mojo接口配合才能起作用,在 Chromium 的多进程架构中使用了该层接口。使用该层接口的demo参考 chromium_demo/demo_viz_gui_gpu.cc at c/80.0.3987 · keyou/chromium_demo

另外,在 viz 中还有一套专门用于 viz 中 GPU 渲染的接口 viz::*ContextProvider。它主要负责为 viz 初始化 GL 环境,使 viz 可以使用 GPU 进行渲染

7. VIZ 的 ID 设计

每一个 client 都至少对应一个 FrameSinkId 和 LocalSurfaceId,在 client 的整个生命周期中所有 FrameSink 的 client_id(见下文)都是固定的,而 LocalSurfaceId 会根据 client 显示画面的 size 或 scale factor 的改变而改变。他们两个共同组成了 SurfaceId,用于在 service 端全局标识一个 Surface 对象。也就是说对于每一个 Surface,都可以获得它是由谁在什么 size 或 scale facotr 下产生的。

FrameSinkId

  • client_id = uint32_t, 每个 client 都会有唯一的一个 ClientId 作为标识符,标识一个 CompositorFrame 是由哪个 client 产生的,也就是标识 CompositorFrame 的来源;
  • sink_id = uint32_t, 在 service 端标识一个 CompositorFrameSink 实例,Manager 会为每个 client 在 service 端创建一个 CompositorFrameSink,专门用于处理该 client 生成的 CompositorFrame,也就是标识 CompositorFrame 的处理端;
  • FrameSinkId = client_id + sink_id,将 CF 的来源和处理者关联起来。

FrameSinkId 可以由 FrameSinkIdAllocator 辅助类生成。

在实际使用中,往往一个进程是一个 client,专门负责一个业务模块 UI 的实现,而一个业务往往由很多的 UI 元素组成,因此可以让每个 FrameSink 负责一部分的 UI,此时就用到了单 client 多 FrameSink 机制,这种机制可以实现 UI 的局部刷新(多 client 也能实现这一点)。在 chrome 中浏览器主程序是一个 client,而每一个插件都是一个独立的 client,网页中除了一些特殊的元素比如 iframe和offscreen canvas位于单独的client中,其他元素全部都位于同一个client中。

LocalSurfaceId

  • parent_sequence_number = uint32_t, 当自己作为父 client,并且 surface 的 size 和 device scale factor 改变的时候改变;
  • child_sequence_number = uint32_t, 当自己作为子 client,并且 surface 的 size 和 device scale factor 改变的时候改变;
  • embed_token = 可以理解为一个随机数, 用于避免 LocalSurafaceId 可猜测,当父 client 和子 client 的父子关系改变的时候改变;
  • LocalSurfaceId = parent_sequence_number + child_sequence_number + embed_token,当 client 的 size 当前 client 产生的画面改变。

LocalSurfaceId 可以由 ParentLocalSurfaceIdAllocator 或者 ChildLocalSurfaceIdAllocator 这两个辅助类生成。前者用于由父 client 负责生成自己的 LocalSurfaceId 的时候,后者用于由子 client 自己负责生成自己的 LocalSurfaceId 的时候。使用哪种方式要看自己的 UI 组件之间的依赖关系的设计。

LocalSurfaceId 在很多时候都包装在 LocalSurfaceIdAllocation 内,该类记录了 LocalSurfaceId 的创建时间。改时间在创建 CF 的时候需要用到。

SurfaceId

SurfaceId = FrameSinkId + LocalSurfaceId

SurfaceId 全局唯一记录一个显示画面,它可以被嵌入其他的 CF 或者 RenderPass 中,从而实现显示界面的嵌入和局部刷新。除了在 Client 端使用 SurfaceDrawQuad 进行 Surface 嵌套之外,大部分应用场景都在 service 端。

8. 参考文献

https://keyou.github.io/blog/2020/07/29/how-viz-works/

Chromium VIZ架构详解的更多相关文章

  1. NopCommerce源码架构详解--初识高性能的开源商城系统cms

    很多人都说通过阅读.学习大神们高质量的代码是提高自己技术能力最快的方式之一.我觉得通过阅读NopCommerce的源码,可以从中学习很多企业系统.软件开发的规范和一些新的技术.技巧,可以快速地提高我们 ...

  2. 领域驱动设计(Domain Driven Design)参考架构详解

    摘要 本文将介绍领域驱动设计(Domain Driven Design)的官方参考架构,该架构分成了Interfaces.Applications和Domain三层以及包含各类基础设施的Infrast ...

  3. WeChatAPI 开源系统架构详解

    WeChatAPI 开源系统架构详解 如果使用WeChatAPI,它扮演着什么样的角色? 从图中我们可以看到主要分为3个部分: 1.业务系统 2.WeChatAPI: WeChatWebAPI,主要是 ...

  4. hdfs文件系统架构详解

    hdfs文件系统架构详解 官方hdfs分布式介绍 NameNode *Namenode负责文件系统的namespace以及客户端文件访问 *NameNode负责文件元数据操作,DataNode负责文件 ...

  5. NopCommerce源码架构详解

    NopCommerce源码架构详解--初识高性能的开源商城系统cms   很多人都说通过阅读.学习大神们高质量的代码是提高自己技术能力最快的方式之一.我觉得通过阅读NopCommerce的源码,可以从 ...

  6. RESTful 架构详解

    RESTful 架构详解 分类 编程技术 1. 什么是REST REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移. 它首次 ...

  7. Nop--NopCommerce源码架构详解专题目录

    最近在研究外国优秀的ASP.NET mvc电子商务网站系统NopCommerce源码架构.这个系统无论是代码组织结构.思想及分层都值得我们学习.对于没有一定开发经验的人要完全搞懂这个源码还是有一定的难 ...

  8. Zookeeper系列二:分布式架构详解、分布式技术详解、分布式事务

    一.分布式架构详解 1.分布式发展历程 1.1 单点集中式 特点:App.DB.FileServer都部署在一台机器上.并且访问请求量较少 1.2  应用服务和数据服务拆分  特点:App.DB.Fi ...

  9. [转载]领域驱动设计(Domain Driven Design)参考架构详解

    摘要 本文将介绍领域驱动设计(Domain Driven Design)的官方参考架构,该架构分成了Interfaces.Applications和Domain三层以及包含各类基础设施的Infrast ...

  10. 【菜鸟】RESTful 架构详解

    RESTful 架构详解 分类 编程技术 1. 什么是REST REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移. 它首次 ...

随机推荐

  1. Oracle随机生成大数据

    Oracle随机插入大数据 话不多说,安排 示例: -- 创建新表并批量插入五千万数据 create table TEST_TAB as select rownum as id, to_char(sy ...

  2. vue: 在页面中单独引入elment-ui

    引入资源 首先引入Vue,之后引入element-ui. 引入组件 返回数据 全部代码 <!DOCTYPE html> <html lang="en"> & ...

  3. (转)IBM Appscan9.0.3安全扫描简单安装、使用以及高危漏洞修复

    最近手上负责一个的项目要进行等保评测.请的第三方公司采用IBM Security AppScan Standard对项目进行安全测试.测试报告高危漏洞主要包含sql注入.sql盲注.跨站点脚本编制如下 ...

  4. 【Leaflet入门篇】 Leaflet快速入门

    0 前言 Leaflet 是一个开源并且对移动端友好的交互式地图 JavaScript 库. 它大小仅仅只有 42 KB of JS, 并且拥有绝大部分开发者所需要的所有地图特性 .Leaflet 简 ...

  5. mysql 命令安装

    1.   mysql  下载安装好压缩文件,下面我们进入正题,少废话. 09:39:112023-08-05 先到 mysql 官方网站下载:https://dev.mysql.com/downloa ...

  6. 如何创建Windows 10 虚拟机

    一 ,新建Windows 10 虚拟机 1.1 创建新的虚拟机 1,点击创建新的虚拟机 2,选择典型,点击下一步 3,选择稍后安装操作系统,点击下一步. 4,操作系统选择windwos,版本选着Win ...

  7. [Python]树基础

    关于树 树是一种数据结构,由n个有限节点组成的一个具有层次关系的集合.二叉树则是每个节点最多有两个子树的树结构.二叉树一般有以下性质: 二叉树第k层上的节点数目最多为 \(2^{k-1}\) 深度为 ...

  8. 调试linux内核(1): 环境准备和原理介绍

    开篇 现在流行的开源项目经历了长时间的开发, 积累了大量的代码, 想要一行一行地阅读代码去学习开源项目, 需要的时间成本是巨大的. 所以, 我们也需要用一种高效的方式去"阅读"代码 ...

  9. AVR汇编(一):搭建交叉编译环境

    AVR汇编(一):搭建交叉编译环境 几年间,陆陆续续接触了很多热门的单片机,如STC.STM8S.STM32.ESP32等.但一直都是抱着急功近利的心态去学习他们,基本上都是基于库函数和第三方组件进行 ...

  10. 二叉搜索树(Binary Search Tree,BST)

    二叉搜索树(Binary Search Tree,BST) 二叉搜索树(Binary Search Tree),也称二叉查找树或二叉排序树,是一种特殊的二叉树,它满足以下性质 对于二叉搜索树的每个节点 ...