https://blogs.igalia.com/itoral/2014/11/11/a-brief-overview-of-the-3d-pipeline/
回顾
在上一篇文章中,我讨论了 Mesa 开发环境,并为新手提供了一些提示,但在我们开始编写代码之前,我们应该先看看现代 GPU 的样子,因为对于驱动程序代码,设计和实现有一定的影响。
固定功能与可编程硬件
在GLSL等着色语言出现之前,我们无法随意对 3D 硬件进行编程。相反,硬件将具有专门用于实现某些操作(如顶点变换)的特定单元,这些操作只能通过特定的 API 使用,例如 OpenGL 公开的那些。这些单元通常被标记为Fixed Function,以将它们与现代 GPU 区分开来,后者也公开了完全可编程的单元。
我们现在在现代 GPU 中拥有的是一个完全可编程的管道,图形开发人员可以在其中使用GLSL等高级编程语言编写各种类型的图形算法。然后将这些程序编译并加载到 GPU 中以执行特定任务。这为图形开发人员提供了大量的*和权力,因为他们不再局限于暴露固定功能的预设 API(例如旧的 OpenGL 闪电模型==OpenGL lightning models)。
现代图形驱动程序
当然,今天图形开发人员享受的所有这些灵活性和功能都以明显更复杂的硬件和驱动程序为代价,因为驱动程序负责向开发人员展示所有这些灵活性,同时确保硬件在每个场景中仍然获得最佳性能。
驱动程序不仅充当像 OpenGL 这样的固定 API 和固定功能硬件之间的桥梁,还需要处理用高级语言编写的通用图形程序。这是一个很大的变化。在 OpenGL 的情况下,这意味着驱动程序需要提供GLSL语言的实现,所以突然间,驱动程序需要包含一个完整的编译器并处理属于编译器领域的各种问题,例如选择程序代码 (IR) 的中间表示,执行优化传递并为 GPU 生成本机代码。
现代 3D 管道概述
我已经提到现代 GPU 公开了完全可编程的硬件单元。这些被称为着色单元,其思想是将这些单元连接在一个管道中,以便着色单元的输出成为下一个着色单元的输入。在此模型中,应用程序开发人员将顶点推送到管道的一端,通常在另一侧获取渲染像素。在这两端之间有许多单元使这种转换成为可能,其中许多单元是可编程的,这意味着图形开发人员可以控制这些顶点在不同阶段如何转换为像素。
要理解这部分内容,需要有openegl的基本概念,建议查看learnopengl
下图显示了 3D 图形管道的简化示例,在这种情况下由OpenGL 4.3规范公开。让我们快速浏览一下它的一些主要部分:
OpenGL 4.3 3D 管道(图片来自 www.brightsideofnews.com)
顶点着色器 (Vertex Shader)
该可编程着色单元将顶点作为输入并生成顶点作为输出。它的主要工作是以图形开发人员认为合适的任何方式转换这些顶点。通常,这是我们会进行顶点投影、旋转、平移之类的变换,并且通常会计算我们不会提供给管道后期阶段的每个顶点属性。
顶点着色器处理由glDrawArrays或glDrawElements等API 提供的顶点数据,并输出着色顶点,这些顶点将按照 OpenGL 绘制命令(GL_TRIANGLES、GL_LINES等)的指示组装成图元。
几何着色器(Geometry Shader)
几何着色器类似于顶点着色器,但它们不是在单个顶点上操作,而是在几何级别(即直线、三角形等)上操作,因此它们可以将顶点着色器的输出作为其输入。
几何着色器单元是可编程的,可用于从图元中添加或删除顶点、裁剪图元、生成全新的图元或修改图元的几何形状(如将三角形转换为四边形或点转换为三角形等)。几何着色器也可用于实现基本的曲面细分,即使现代硬件中的专用曲面细分单元更适合这项工作。
在GLSL 中,一些操作,如分层渲染(允许在同一程序中渲染多个纹理)只能通过几何着色器访问,尽管现在这也可以通过特定扩展,在顶点着色器中实现。
几何着色器的输出也是图元。
光栅化(Rasterization)
到目前为止,我们讨论的所有阶段都操纵了顶点和几何。然而,在某些时候,我们需要渲染像素。为此,需要对图元进行光栅化,这是将它们分解成单个片段的过程,这些片段然后由片段着色器(fragment shader)着色并最终变成帧缓冲区中的像素。光栅化由光栅化器固定功能单元处理。
光栅化过程还为这些片段分配深度信息。当我们有一个 3D 场景,其中多个多边形在屏幕上重叠时,我们需要决定哪些多边形的片段应该被渲染,哪些应该被丢弃,因为它们被其他多边形隐藏了。
最后,光栅化还对每个顶点的属性进行插值,以计算相应的片段值。例如,假设我们有一个线图元,其中每个顶点都有不同的颜色属性,一个红色和一个绿色。对于行中的每个片段,光栅化器将根据片段与每个顶点的接近或远近,通过组合红色和绿色来计算内插颜色值。这样,我们将在红色顶点一侧获得红色片段,当我们靠近绿色顶点时,这些片段将平滑过渡到绿色。
总之,光栅化器的输入是来自顶点、曲面细分或几何着色器的图元,输出是构建投影到屏幕上的图元表面的片段,包括颜色、深度和其他每个顶点的内插属性。
片段着色器 (Fragment Shader)
可编程片段着色器单元采用光栅化过程产生的片段,并执行图形开发人员提供的算法来计算每个片段的最终颜色、深度和模板值。该单元可用于实现多种视觉效果,包括各种后处理过滤器,通常我们将采样纹理以对多边形表面进行着色等。
这涵盖了 3D 图形管道中的一些最重要的元素,现在应该足以了解驱动程序的一些基础知识。但是请注意,这并未涵盖诸如变换反馈、曲面细分或计算着色器之类的内容。我希望我能在以后的帖子中介绍其中的一些。
但在我们完成 3D 管道的概述之前,我们应该讨论另一个对硬件工作方式至关重要的主题:并行化。
并行化
图形处理是一项非常需要资源的任务。我们以每秒 30/60 次的速度不断更新和重绘我们的图形。对于 1920×1080 的全高清分辨率,这意味着我们需要在每次运行中重绘超过 200 万像素(如果我们以 60 FPS 的速度运行,则为每秒 124.416.000 像素),好多啊。
为了解决这个问题,GPU 的架构是大规模并行的,这意味着流水线可以同时处理许多顶点/像素。例如,在 Intel Haswell GPU 的情况下,VS 和 GS 等可编程单元有多个执行单元 (EU),每个都有自己的一组ALU等,每个单元最多可以产生 70 个线程(对于 GS 和 VS)而片段着色器最多可以产生 102 个线程。但这并不是并行的唯一来源:每个线程可以同时处理多个对象(顶点或像素,具体取决于情况)。例如,英特尔硬件中的 VS 线程可以同时着色两个顶点,而 FS 线程可以一次着色多达 8 (SIMD8) 或 16 (SIMD16) 个像素。
其中一些并行方法对驱动程序开发人员相对透明,而另一些则不然。例如,SIMD8 与 SIMD16 或单顶点着色与双顶点着色需要特定配置并编写与所选配置对齐的驱动程序代码。线程更透明,但在某些情况下,驱动程序开发人员在编写可能需要所有正在运行的线程之间同步的代码时可能需要小心,这显然会损害性能,或者至少要小心做那种事情时对性能的影响最小。
接下来的是
所以这是对现代 3D 管道外观的非常简短的介绍。还有很多东西我没有涉及,但我认为我们可以在以后的帖子中深入研究驱动程序代码。我的下一篇文章将讨论 Mesa 如何模拟我在这里介绍的各种可编程流水线阶段,敬请期待!