深度探索Linux操作系统 —— Linux图形原理探讨2

简介: 深度探索Linux操作系统 —— Linux图形原理探讨

深度探索Linux操作系统 —— Linux图形原理探讨1:https://developer.aliyun.com/article/1598095

三、2D渲染

    这一节,我们结合 X 窗口系统,讨论 2D 程序的渲染过程。我们可以形象地将 2D 渲染过程比喻为绘画,其中有两个关键的地方:一个是画布,另外一个是画笔。

   X 服务器启动后,将加载 GPU 的 2D 驱动,2D 驱动将请求内核中的 DRM 模块创建帧缓冲,这个帧缓冲就相当于画布。然后 X 服务器按照绘画需要,从画笔盒子中挑选合适的画笔进行绘画。


   X 的画笔保存在结构体 GCOps 中,其中包含了基本的绘制操作,如绘制矩形的 PolyRectangle ,绘制圆弧的 PolyArc ,绘制实心多边形的 FillPolygon ,等等。代码如下:

// xorg-server-1.12.2/include/gcstruct.h:

typedef struct _GCOps {
  ...
  void (*PolyRectangle) (DrawablePtr /*pDrawable */ , ...);
  void (*PolyArc) (DrawablePtr /*pDrawable */ , ...);
  void (*FillPolygon) (DrawablePtr /*pDrawable */ , ...);
  void (*PolyFillRect) (DrawablePtr /*pDrawable */ , ...);
  ...
} GCOps;

   最初,这些绘制操作均由 CPU 负责完成,也就是我们通常所说的软件渲染。X 中的 fb 层就是软件渲染的实现,代码如下:


// xorg-server-1.12.2/fb/fbgc.c:

const GCOps fbGCOps = {
  fbFillSpans,
  ...
  fbPolySegment,
  fbPolyRectangle,
  fbPolyArc,
  miFillPolygon,
  ...
};  

   但是随着 GPU 的不断发展,其计算能力越来越强。于是 X 的开发者们不断改进 X 的渲染部分,希望能充分利用 GPU 擅长的图形操作以大幅提高计算机的图形能力,而又可以解放 CPU,使其专心于控制逻辑。也就是说,X 的开发者们希望画笔盒子中的画笔更多地来自 GPU 。


   当然,任何事物都不是一蹴而就的,GPU 的渲染能力也是螺旋演进的,对于 GPU 尚未实现的或者相比来说 CPU 更适合的渲染操作还是需要 CPU 来完成,因此,X 的渲染架构也随着 GPU 的演进不断地改进。在 XFree86 3.3 的时候,X 的开发者设计了 XAA(XFree86 Acceleration Architecture)架构;在 X.Org Server 6.9 版本,开发者用改进的 EXA 取代了 XAA;当 DRM 中使用了 GEM 后,Intel 的 GPU 驱动开发者们重新实现了 EXA ,并命名为 UXA(Unified Acceleration Architecture);随着 Intel 推出 Sandy Bridge 及 ivy Birdge 芯片组,Intel 又开发了 SNA(SandyBridge’s New Acceleration)。


   后续,我们以成熟且稳定的 UXA 为例进行讨论。在 UXA 架构下,X 的画笔盒子如下:


// xf86-video-intel-2.19.0/uxa/uxa-accel.c:

const GCOps uxa_ops = {
  uxa_fill_spans,
  ...
  uxa_poly_lines,
  uxa_poly_segment,
  miPolyRectangle,
  uxa_check_poly_arc,
  miFillPolygon,
  ...
};

   我们看到 uxa_ops 包含在 Intel 的 GPU 驱动中,当然,这是非常合理的,因为只有 GPU 自己最清楚哪些渲染自己可以胜任,哪些还需要 CPU 来负责。在 uxa_ops 中,有一部分画笔来自 GPU ,另外一部分来自 CPU 。


   对于每一个绘制操作,UXA 首先检查 GPU 是否支持这个绘制操作,或者说在某些条件下,对于这个绘制操作,GPU 渲染的比 CPU 更快。如果 GPU 支持这个绘制操作,UXA 首先将绘制的命令翻译为 GPU 可以识别的指令,并将这个指令、绘制所需的相关数据,以及保存像素阵列的 BO 在显存地址空间中的地址,一同保存在用户空间的批量缓冲(Batch Buffer),然后通过 DRM 将用户空间的批量缓冲复制到内核为批量缓冲创建的 BO ,之后通知 GPU 从 BO 中读取指令和数据进行绘制。实际上,DRM 按照 Intel GPU 的要求在批量缓冲和 GPU 之间还组织了一个环形缓冲区(Ring buffer),但是我们暂时忽略它,这对于理解 2D 渲染过程没有任何影响,后面在讨论 3D 渲染过程时,我们会简单的讨论这个环形缓冲区。


   如果 GPU 不支持这个绘制操作,那么 UXA 将代表帧缓冲的 BO 映射到 X 服务器的用户空间,X 服务器借助 fb 层中的实现,使用 CPU 进行绘制。


   也就是说,UXA 在 fb 和 GPU 加速的上面封装了一层,其根据具体绘制动作选择使用来自 GPU 的画笔或来自 CPU 的画笔。


   综上,X 的 2D 渲染过程如图 8-5 所示。


c167048fd2395e65d884ef5f1d951fe7.png


   不知读者是否注意到,无论是 fbGCOps,还是 uxa_ops,其中均有个别的绘制函数以 “mi” 开头。这些以 “mi” 开头的函数包含在 X 的 mi 层中。mi 是 Machine Independent 的缩写,顾名思义,是与机器无关的实现。笔者没有找到 X 中关于这个层的非常明确的解释,但是根据 mi 中的代码来看,其中的绘致函数根据不同的绘制条件,被拆分为调用其他 GCOps 中的绘制函数。


   基本上,拆分的原因无外乎 GPU 支持的绘制原语有限,所以有些绘制操作需要分解为 GPU 可以支持的动作。或者出于绘制效率的考虑,将某些绘制操作拆分为效率更好的绘制原语。因此,X 将这些与具体绘制实现无关的代码剥离到

一个单独的模块 mi 中。从这个角度或许能解释 X 为什么将这个层命名为 Machine Independent。


1、创建前缓冲

   在 X 环境下,在不开启复合(Composite)扩展的情况下,所有程序共享一个前缓冲。对于 2D 程序,所有的绘制动作生成的图像的像素阵列最终都输出到这个前缓冲上,窗口只不过是前缓冲中的一块区域而已。


   但是一旦开启了复合扩展,那么每个窗口都将被分配一个离屏(offscreen)的缓冲,类似于 OpenGL 环境中的后缓冲。应用将生成的像素阵列输出到这个离屏的缓冲中,在绘制完成后,X 服务器将向复合管理器(Composite Manager)发送 Damage 事件,复合管理器收到这个事件后,将离屏缓冲区的内容合成到前缓冲。为了避免复合扩展干扰我们探讨图形渲染的本质,在讨论 2D、包括后面的 3D 渲染时,我们都不考虑复合扩展开启的情况。


   在 X 中,Window 和 Pixmap 是两个绘制发生的地方,Window 代表屏幕上的窗口,Pixmap 则代表离屏的一个存储区域。所以自然而然的,X 使用数据结构 Pixmap 来表示前缓冲。因为这个前缓冲对应整个屏幕,而且不属于某一个应用,因此开发者也将代表前缓冲的这个 Pixmap 称为 Screen Pixmap 。后续为了行文方便,我们有时也使用 Screen Pixmap 这个词来代表前缓冲的这个 Pixmap 对象。显然,这个Screen Pixmap 也是显示器(Screen)的资源,所以X将其保存到了代表显示器的结构体_Screen中。


2、GPU渲染

   GPU 渲染,也就是我们通常所说的硬件加速,从软件的层面所做的工作就是将数学模型按照 GPU 的规定,翻译为 GPU 可以识别的指令和数据,传递给 GPU,生成像素阵列等图像密集型计算则由 GPU 负责完成。可见,当使用 GPU 进行渲染时,在软件层面,实质上就是组织命令和数据而已。


   Intel GPU 的 2D 驱动是如何将这些命令和数据传递给 GPU 的呢?读者一定想到了 BO 。在 Intel GPU 的 2D 驱动中,定义了使用了一种所谓的批量缓冲来保存这些命令和数据,这里所谓的批量就是将驱动准备命令和数据放到这个缓冲,然后批量地让 GPU 来读取,这就是批量缓冲的由来。


3、CPU渲染

   根据上节讨论的函数 uxa_poly_fill_rect ,我们看到,GPU 并不是接收全部的绘制实心矩形的操作。对于不满足GPU条件的实心矩形,则将求助于 CPU 绘制,对应的函数是 uxa_check_poly_fill_rect 。

8127133ed82e50ba4bb14a7ab8fee192.png


   BO 是由 DRM 模块在内核空间分配的,因此运行在用户空间的 X( 2D驱动)要想访问这个内存,必须首先要将其映射到用户空间,这是由函数 uxa_prepare_access 来完成的。然后,X 使用 CPU 在映射到用户空间的 BO 上进行绘制。看到以 fb 开头的函数 fbPolyFillRect,读者一定猜到了,这就是 X 的 fb 层的函数,而 fb 层正是软件渲染的实现。


(1)映射 BO 到用户空间

   函数 uxa_check_poly_fill_rect 调用 uxa_prepare_access 将 BO 映射到用户空间:


1ce266291f36d8e43130e789813df8e0.png


   函数 intel_uxa_prepare_access 通过 libdrm 库中的函数 drm_intel_gem_bo_map_gtt 申请内核中的 DRM 模块将保存前缓冲的像素阵列的 BO 映射到用户空间:

aea3eb5e6ba8e2c25a4033f42f12756a.png


   看到熟悉的函数 mmap ,读者应该一切都明白了。从 CPU 的角度看,BO 与普通内存并无区别,所以,映射 BO 与映射普通内存完全相同。其中 bufmgr_gem->fd 指向的就是代表 BO 的共享内存。


(2)使用 CPU 在映射到用户空间的 BO 上进行绘制

   X 的软件渲染层(即 fb 这一层),或者借助库 pixman 中的 API,或者自己直接操作像素数组,完成图形的绘制。其原理非常简单,就是直接设置像素数组中的颜色值或索引。


   经过对 2D 渲染的探讨,我们看到,所谓的软件渲染和硬件加速,本质上都是生成图像的像素阵列,只不过一个是由 CPU 来计算的,另外一个是由 GPU 来计算的。当然,对于硬件加速,CPU 要充当一个翻译,将数学模型按照 GPU 的要求翻译为其可以识别的指令和数据。


四、3D渲染

   运行在 X 上的 2D 程序,都将绘制请求发给 X 服务器,由 X 服务器来完成绘制。但是对于 3D 图形的绘制,X 应用需要通过套接字向 X 服务器传递大量的数据,这种机制严重影响了图形的渲染效率。为了解决效率问题,X 的开发者们设计了 DRI 机制,即 X 应用不再将绘制图形的请求发送给 X 服务器了,而是由应用自行绘制。


   在 Linux 平台上,OpenGL 的实现是 Mesa ,所以在本节中,我们结合 Mesa,探讨 3D 的渲染过程。我们可以认为 Mesa 分为两个关键部分:


   ◆ 一部分是一套兼容 OpenGL 标准的实现,为应用程序提供标准的 OpenGL API 。


   ◆ 另外一部分是 DRI 驱动,通常也被称为 3D 驱动,其中包括 Pipleline 的软件实现,也就是说,即使 GPU 没有任何 3D 计算能力,那么 Mesa 也完全可以使用 CPU 完成 3D 渲染功能。3D 驱动还负责将 3D 渲染命令翻译为 GPU 可以理解并能执行的指令。不同的 GPU 有各自的 “指令集” ,因此,在 Mesa 中不同的 GPU 都有各自的 3D 驱动。


   Pipeline 最后将生成好的像素阵列输出到帧缓冲,但是这还不够,因为最后的输出需要显示到屏幕上。而屏幕的显示是由具体的窗口系统控制的,因此,帧缓冲还需要与具体的窗口系统相结合。但是 X 的核心协议并不包含 OpenGL 相关的协议,因此,开发者们开发了 GL 的扩展 GLX(GL Extension)。为了支持 DRI,开发者们又开发了 DRI 扩展。显然,GLX 以及 DRI 扩展在 X 和 Mesa 中均需要实现。


   基本上,运行在 X 窗口系统上的 OpenGL 程序的渲染过程,可以划分为三个阶段,如图 8-6 所示。

3f50b62dcea33c0d8acf3db73ec963a0.png



   1)应用创建 OpenGL 的上下文,包括向 X 服务器申请创建帧缓冲。应用为什么不自己直接向内核的 DRM 模块请求创建帧缓冲呢?从技术上讲,应用自己请求 DRM 创建请求创建帧缓冲没有任何问题,但是为了将帧缓冲与具体的窗口系统绑定,应用只能委屈一下,放低姿态请求 X 服务器为其创建帧缓冲。这样,X 服务器就掌握了应用的帧缓冲的一手材料,在需要时,将帧缓冲显示到屏幕。帧缓冲是应用程序的 “画板” ,因此创建完成后,X 服务器需要将帧缓冲的 BO 的信息返回给应用。


   2)应用程序建立数学模型,并通过 OpenGL 的 API 将数学模型的数据写入顶点缓冲(vertex buffer);更新 GPU 的状态,如指定后缓冲,用来存储 Pipeline 输出的像素阵列;然后启动 Pipeline 进行渲染。


   3)渲染完成后,应用程序向 X 服务器发出交换(swap)请求。这里的交换有两种方式,一种是复制(copy),所谓复制就是将后缓冲中的内容复制到前缓冲,这是由 GPU 中 BLT 引擎负责的。但是复制的效率相对较低,所以,开发者们又设计了一种称为页翻转(page flip)的模式,在这种模式下,不需要复制动作,而是通过 GPU 的显示引擎控制显示控制器扫描哪个帧缓冲,这个被扫描的缓冲此时扮演前缓冲,而另外一个不被扫描的帧缓冲则作为应用的 “画板” ,也就是所说的后缓冲。


   接下来我们就围绕这三个阶段,讨论 3D 程序的渲染过程。


1、创建帧缓冲

   在 2D 渲染中,渲染过程都由 X 服务器完成,所以毫无争议,前缓冲由而且只能由 X 服务器创建。但是对于 DRI 程序来说,其渲染是在应用中完成,应用当然需要知道帧缓冲,但是 X 服务器控制着窗口的显示,所以 X 服务器也需要知道帧缓冲。所以,帧缓冲或者由 X 服务器创建,然后告知应用;或者由应用创建,然后再告知 X 服务器。X 采用的是前者。


   虽然 OpenGL 中的帧缓冲的概念与 2D 相比有些不同,但本质上并无差别,帧缓冲中的每个缓冲都对应着一个 BO 。为了管理方便,Mesa 为帧缓冲以及其中的各个缓冲分别抽象了相应的数据结构,代码如下:

// Mesa-8.0.3/src/mesa/main/mtypes.h

struct gl_framebuffer {
  ...
  struct gl_renderbuffer_attachment Attachment[BUFFER_COUNT];
  ...
};

   其中,结构体 gl_framebuffer 是帧缓冲的抽象。结构体 gl_renderbuffer 是颜色缓冲、深度缓冲等的抽象。gl_framebuffer 中的数组 Attachment 中保存的就是颜色缓冲、深度缓冲等。


   在具体的 3D 驱动中,通常会以 gl_renderbuffer 作为基类,派生出自己的类。如对于 Intel GPU 的 3D 驱动,派生的数据结构为 intel_renderbuffer :


// Mesa-8.0.3/src/mesa/drivers/dri/intel/intel_fbo.h

struct intel_renderbuffer {
  struct swrast_renderbuffer Base;
  struct intel_mipmap_tree *mt; /** < The renderbuffer storage. */
};

   其中指针 mt 间接指向缓冲区对应的 BO 。


   如同在 Intel GPU 的 2D 驱动中,使用结构体 intel_pixmap 封装了 BO 一样,Intel GPU的 3D 驱动也在 BO 之上包装了一层 intel_region 。intel_region 中除了包括 BO 外,还包括缓冲区的一些信息,如缓冲区的宽度、高度等:

// Mesa-8.0.3/src/mesa/drivers/dri/intel/intel_regions.h:

struct intel_region {
  drm_intel_bo *bo; /**< buffer manager's buffer */
  GLuint refcount; /**< Reference count for region */
  GLuint cpp; /**< bytes per pixel */
  GLuint width; /**< in pixels */
  ...
};

   当 OpenGL 应用调用 glXMakeCurrent 时,就开启了创建帧缓冲的过程,这个过程可分为三个阶段:


   1)OpenGL 应用向 X 服务器请求为指定窗口创建帧缓冲对应的 BO 。帧缓冲中包含多个缓冲,所以当然是创建多个 BO 了。


   2)X 服务器收到应用的请求后,为各个缓冲创建 BO 。在创建完成后,将 BO 的名字等相关信息发送给应用。


   3)应用收到 BO 信息后,将更新 GPU 的状态。比如告诉 GPU 画板在哪里。

深度探索Linux操作系统 —— Linux图形原理探讨3:https://developer.aliyun.com/article/1598097

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
8天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
29 9
|
8天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
7天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
24 2
|
11天前
|
缓存 网络协议 Linux
Linux操作系统内核
Linux操作系统内核 1、进程管理: 进程调度 进程创建与销毁 进程间通信 2、内存管理: 内存分配与回收 虚拟内存管理 缓存管理 3、驱动管理: 设备驱动程序接口 硬件抽象层 中断处理 4、文件和网络管理: 文件系统管理 网络协议栈 网络安全及防火墙管理
34 4
|
10天前
|
安全 网络协议 Linux
Linux操作系统的内核升级与优化策略####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统内核升级的重要性,并详细阐述了一系列优化策略,旨在帮助系统管理员和高级用户提升系统的稳定性、安全性和性能。通过实际案例分析,我们展示了如何安全有效地进行内核升级,以及如何利用调优技术充分发挥Linux系统的潜力。 ####
30 1
|
13天前
|
物联网 Linux 云计算
Linux操作系统的演变与未来趋势####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统从诞生至今的发展历程,分析了其在服务器、桌面及嵌入式系统领域的应用现状,并展望了云计算、物联网时代下Linux的未来趋势。通过回顾历史、剖析现状、预测未来,本文旨在为读者提供一个全面而深入的视角,以理解Linux在当今技术生态中的重要地位及其发展潜力。 ####
|
19天前
|
边缘计算 人工智能 运维
Linux操作系统:开源力量的崛起与影响###
一场技术革命的回顾 回溯至1991年,当Linus Torvalds宣布Linux操作系统的诞生时,世界或许并未意识到这一举措将如何深刻地改变技术领域的面貌。本文旨在探讨Linux操作系统的发展历程、核心特性、以及它如何引领了一场开源运动,重塑了软件行业的生态。从最初的个人爱好项目成长为全球最广泛采用的服务器操作系统之一,Linux的故事是技术创新与社区精神共同推动下的辉煌篇章。 ###
|
17天前
|
人工智能 安全 Linux
|
20天前
|
物联网 Linux 5G
Linux操作系统的演变与未来趋势####
本文深入探讨了Linux操作系统的发展历程,从最初的一个学生项目到如今全球最流行的开源操作系统之一。文章将分析Linux的核心优势、关键特性以及它在云计算、物联网和嵌入式系统中的应用前景。通过具体案例展示Linux如何推动技术创新,并预测其在未来技术生态中的角色。本文旨在为读者提供一个全面而深入的理解,帮助他们认识到Linux在现代计算环境中的重要性及其未来的潜力。 ####
|
20天前
|
人工智能 安全 物联网
Linux操作系统的演变与未来:从开源精神到万物互联的基石###
本文是关于Linux操作系统的演变、现状与未来的深度探索。Linux,这一基于Unix的开源操作系统,自1991年由林纳斯·托瓦兹(Linus Torvalds)学生时代创造以来,已经彻底改变了我们的数字世界。文章首先追溯了Linux的起源,解析其作为开源项目的独特之处;随后,详细阐述了Linux如何从一个小众项目成长为全球最广泛采用的操作系统之一,特别是在服务器、云计算及嵌入式系统领域的主导地位。此外,文章还探讨了Linux在推动技术创新、促进协作开发模式以及保障信息安全方面的作用,最后展望了Linux在未来技术趋势中的角色,包括物联网、人工智能和量子计算等前沿领域的潜在影响。 ###