跳到主要内容
约 10 分钟6 张卡

图形渲染管线

图形渲染管线把几何数据、纹理与绘制状态转换成渲染目标中的像素。提交一条绘制命令、GPU 完成渲染和屏幕显示新画面,是三个不同的时刻。

移动端项目即使用 UIKit、Android View 或跨平台框架,也会遇到图片显示、透明叠加和帧延迟。理解这些机制,才能区分问题来自布局、资源准备、GPU 工作还是最终合成。本文以传统光栅化管线为主,Metal 示例只演示顶点与片元阶段,不要求实现完整图形引擎。

1. 面试核心​

先记住这几条,再读正文
  • CPU 准备数据与命令,GPU 按渲染状态处理几何和像素。二者可以重叠工作,但提交完成不等于 GPU 已执行完成。卡 · CPU 与 GPU 的分工
  • 几何处理、光栅化与片元处理负责不同步骤。顶点描述几何,光栅化确定覆盖的采样位置,片元着色生成候选输出,最终写入还受测试与混合影响。卡 · 几何到片元
  • 裁剪、透视除法和视口变换连接几何坐标与屏幕区域。坐标约定依赖图形 API,不能直接套用另一套深度范围。卡 · 顶点与坐标变换
  • 纹理是供着色器读取的图像资源,渲染目标是接收绘制结果的资源。文件解码、上传或共享资源、纹理采样与显示属于不同责任。卡 · 图片到屏幕
  • 透明混合需要正确的颜色表示、混合因子与绘制顺序。透明对象仍可能消耗片元处理与带宽,不能只看是否遮住背景。卡 · 透明混合与颜色表示
  • 应用渲染、系统合成和屏幕刷新需要协调。缓冲提供并行空间,也占用内存并可能增加等待;缓冲数量增加不保证帧率或输入响应改善。卡 · 合成、缓冲与显示

2. CPU 与 GPU 的分工​

CPU 一侧处理输入、业务状态和布局,准备顶点、纹理与绘制命令。GPU 一侧根据这些命令处理几何与大量采样位置,适合并行执行具有相似结构的工作。

命令缓冲区保存提交给 GPU 的一组命令。在传统 Metal 接口中,应用用 command encoder 编码绘制操作,结束编码后提交 command buffer。提交只是把工作交给执行链路,不能立刻读取尚未完成的结果。

应用可以在 GPU 处理上一批命令时准备下一批,但不能随意覆盖 GPU 仍在使用的数据。需要按资源的存储模式和同步约定安排读写。每帧都让 CPU 同步等待 GPU,可能把本来能够重叠的工作串行化。

UI 布局也不等于图形渲染。计算按钮的位置和尺寸,解决的是界面几何关系;将文字、背景和图片转成可合成的像素,还需要后续绘制。框架可能缓存绘制结果、合并操作或采用软件路径,因此也不能把每个 View 无条件等同于一次 GPU draw call。

3. 几何到片元​

3.1 顶点与坐标变换​

顶点描述几何上的点,并可携带颜色、纹理坐标等属性。多个顶点组成三角形等图元;仅有几个顶点,并不意味着最终只计算几个屏幕像素。

三维场景一般把模型自身坐标转换到世界、观察和投影空间。顶点着色器输出齐次裁剪坐标 (x, y, z, w);w 参与后续透视除法,不能把四个分量都直接当成屏幕像素位置。

对于直接绘制二维三角形的示例,可以跳过模型与相机矩阵,直接输出裁剪坐标。这只是输入已经处于合适空间,不代表三维渲染不需要坐标变换。

3.2 裁剪、透视除法与视口​

裁剪处理图元超出可见裁剪范围的部分。跨越边界的三角形可能保留可见部分,不是只要一个顶点越界就删除整个三角形。

裁剪后,用 w 对坐标作透视除法,得到归一化设备坐标。以 Metal 的约定为例,标准范围内的 x、y 在 [-1, 1],z 在 [0, 1];不要把某些 OpenGL 约定中的 z 范围直接复制过来。视口再把这些坐标映射到渲染目标中的一个矩形区域及深度范围。

剪裁矩形(scissor rectangle)进一步限制可写入的目标区域。它与几何裁剪、UI 框架的裁切策略有关联,但不是同一个操作;使用某种 UI 裁切效果,也不能据此断言底层一定采用某个固定实现。

3.3 光栅化与插值​

光栅化把图元转换为离散的覆盖与采样工作。三角形覆盖哪里、哪些采样位置需要处理,在这一阶段确定;顶点携带的属性也会按照相应规则插值,供片元阶段使用。

片元是光栅化产生的候选处理单元,不应简单等同于最终屏幕像素。一个像素可能涉及多个采样位置、多个重叠图元,也可能有片元在深度测试等环节被丢弃。多重采样抗锯齿主要改善几何边缘的覆盖表达,并不意味着片元着色器必然对每个采样位置各执行一次。

3.4 片元、测试与写入​

片元着色器根据插值属性、纹理和其他输入,计算候选颜色等输出。纹理采样还涉及坐标、过滤、边界处理和颜色空间,不能把它当成无条件读取某个原始文件字节。

深度和模板测试根据状态决定哪些结果能够保留;颜色混合决定新输出怎样与目标已有颜色组合。这里描述的是逻辑责任,硬件可以在满足条件时提前做测试,不要求所有实现都按“先运行全部片元着色器,再逐个测试”的固定顺序执行。

深度测试通过也不代表一定更新深度缓冲:是否写入深度由独立状态控制。透明物体的排序、深度测试与深度写入要结合策略决定,不能只打开一个“透明”开关。

4. Metal 三角形示例​

下面的 Metal Shading Language 代码以 MSL 2.4 为基线,直接提供三个顶点。triangleVertex 根据顶点编号输出位置,redFragment 对进入片元阶段的覆盖区域返回不透明红色。

#include <metal_stdlib>
using namespace metal;

struct VertexOutput {
float4 position [[position]];
};

vertex VertexOutput triangleVertex(uint index [[vertex_id]]) {
const float2 positions[3] = {
float2(-0.8, -0.8),
float2( 0.8, -0.8),
float2( 0.0, 0.8)
};
VertexOutput output;
output.position = float4(positions[index], 0.5, 1.0);
return output;
}

fragment float4 redFragment() {
return float4(1.0, 0.0, 0.0, 1.0);
}

这两个函数需要由宿主程序创建 Metal library 与 render pipeline state,并设置匹配的颜色附件格式。绘制时使用 triangle 图元、vertexStart = 0、vertexCount = 3,保证 index 落在数组范围内。示例没有展示可供任意顶点数量使用的通用顶点缓冲方案。

将目标设置为 64 × 64 的 rgba8Unorm 纹理,清除为不透明蓝色,关闭剔除与颜色混合,再绘制这个三角形:中心采样到红色,三角形外仍为蓝色。这里 z 为 0.5、w 为 1,处于 Metal 标准裁剪范围;目标保持单采样,因此不在边缘像素上断言某个跨配置通用的颜色。

离屏测试可以在 GPU 完成后读取目标像素,验证顶点、覆盖和片元输出的关系。测试中的等待用于取得确定结果,不是建议在生产每帧调用 waitUntilCompleted();离屏像素正确,也不能证明窗口合成、刷新时机或手机帧率已经通过验证。

5. 图片、透明与渲染目标​

5.1 图片到屏幕​

显示一张图片,需要先取得文件或网络数据,将编码图片解码为可用的像素表示,再让绘制链路能够使用这些数据。解码尺寸、方向和缓存责任见 移动端图片加载。

在直接使用 GPU 的一种实现中,应用把图片放入纹理,用两个三角形覆盖目标矩形,通过纹理坐标采样颜色,再把结果写入渲染目标。实际平台也可能共享已有图像缓冲或复用缓存,不一定每次显示都重新复制完整图片。

后续还要把应用产生的内容交给系统合成,并在合适的显示时机呈现。图片加载完成、纹理可用和屏幕出现图片,是三个可分别记录的阶段;把耗时都叫作“图片绘制慢”,会掩盖需要优化的具体环节。

5.2 透明混合与颜色表示​

Alpha 混合根据颜色、透明度与配置的混合因子,组合新片元和目标已有内容。对于不透明背景、普通 source-over 合成、且源 RGB 未预乘 alpha 的简化情况,颜色可写为:

结果 RGB = 源 RGB × 源 alpha + 背景 RGB × (1 − 源 alpha)

例如半透明红色覆盖不透明蓝色,在线性数值空间中得到 (0.5, 0, 0.5)。这个例子假定已经选择正确颜色空间和混合状态,不能拿编码后的 sRGB 字节直接代表所有显示场景的计算过程。

预乘 alpha把源 RGB 预先乘以 alpha。此时对应的 source-over RGB 混合应使用源因子 1,避免再乘一次。若把预乘颜色误当成未预乘颜色,半透明边缘可能变暗;反过来配置也可能使颜色不正确。

普通透明混合通常不满足交换律:红色叠在蓝色上,与蓝色叠在红色上可能不同。正确结果要考虑绘制顺序、背景、深度测试和所选混合方式,不能任意排序所有透明 draw call。

5.3 多次绘制与中间目标​

过度绘制指同一区域被多次绘制。后面的不透明内容完全盖住前面时,前面的某些工作可能没有最终可见贡献;透明内容则可能确实需要读取并混合背景。成本要结合覆盖面积、着色复杂度和带宽分析,不能仅按层数判断。

离屏渲染先把结果画到暂不直接显示的目标,供后续使用,例如模糊、中间合成或纹理生成。它本身是工具,不是错误;额外目标、pass、读写和同步才是需要评估的成本。不同框架与硬件会采用不同优化,不能断言所有圆角或阴影都必然触发同样的离屏路径。

渲染 pass 还需要明确附件开始与结束时的行为:清除、保留已有内容或不关心,以及是否保存结果。下一步需要读取的结果不能被当作“不必保留”,也不能把未初始化内容当成稳定背景。

6. 合成、缓冲与显示​

合成把应用与系统界面的多个图层按位置、层级和透明等信息组合成显示内容。Android 的 SurfaceFlinger 接收图层缓冲,并与 Hardware Composer 协作决定合成方式;不能把每个应用的绘制都描述成直接写屏幕。

在 Apple 平台,Core Animation 用图层组织内容与视觉属性,并参与动画和显示流程。应用提交图层变化后,仍有后续渲染与呈现工作;不要把 Android 的 BufferQueue、SurfaceFlinger 名称直接套到 iOS。

多缓冲让生产者准备新内容时,消费者继续使用已有内容,减少争用。代价包括额外资源和可能排队的帧;生产太慢时仍然没有新画面,生产过快而队列积压时也可能增加显示延迟。

VSync 与呈现调度用于协调显示节奏,但不能保证应用准时完成。错过某次呈现机会,屏幕可能继续显示旧内容;“GPU 用时小于一帧”也不代表整条输入到显示链路没有等待。

诊断时分别记录 CPU 准备、GPU 执行、缓冲可用与呈现时机,并结合 移动端性能指标 的设备和采样条件解释结果。同步等待、资源上传、着色器成本与系统合成的问题,需要不同证据。

7 · 面试题卡片​

本页题目与参考回答

7.1 图形渲染管线包含哪些主要阶段?​

考察重点:几何、覆盖、片元与目标写入的分工。

相关内容:几何到片元、CPU 与 GPU 的分工

参考回答

应用准备数据和绘制命令,GPU 进行顶点处理、裁剪与坐标映射、光栅化和片元处理,再结合测试、混合等状态写入渲染目标。 这些是逻辑责任,硬件可以在不改变规定结果的前提下优化执行顺序。结果还要进入合成和呈现流程,不能把最后一次 draw call 当成屏幕已显示。

7.2 怎样把一张图片渲染到屏幕上?​

考察重点:资源取得、解码、纹理采样、绘制与显示。

相关内容:图片到屏幕、合成、缓冲与显示

参考回答

先取得并解码图片,让绘制链路获得可用像素,再生成可合成的内容并提交显示。 直接使用 GPU 时,可以把图片作为纹理,用覆盖目标矩形的几何进行采样,写入渲染目标。具体实现可能复用或共享资源,不能假定每帧都重新下载、解码和上传;最终显示还受合成与刷新调度影响。

7.3 CPU 提交绘制命令后,能否马上读取 GPU 结果?​

考察重点:异步提交、完成条件与资源同步。

相关内容:CPU 与 GPU 的分工

参考回答

不能只凭提交完成就读取结果。 要确认 GPU 已完成相应工作,并满足资源存储模式规定的可见性与同步条件。生产路径优先安排依赖与多缓冲,避免每帧同步等待把 CPU、GPU 工作串行化。

7.4 Metal 顶点输出中的 w 有什么作用,视口是否负责透视除法?​

考察重点:裁剪坐标、归一化坐标与目标映射。

相关内容:顶点与坐标变换、裁剪、透视除法与视口

参考回答

w 是齐次裁剪坐标的一部分,参与裁剪后的透视除法;视口负责把归一化设备坐标映射到目标区域。 Metal 的标准归一化深度范围是 [0, 1]。示例中 w 为 1,所以除法不会改变 x、y、z,但这不代表所有投影下都能忽略 w。

7.5 为什么半透明图片边缘变暗时,要检查预乘 alpha 和混合状态?​

考察重点:颜色表示与混合因子的对应关系。

相关内容:透明混合与颜色表示

参考回答

预乘颜色已经乘过 alpha,再按未预乘方式乘一次,会使半透明部分贡献过小。 应核对纹理实际表示、混合因子和颜色空间;同时检查素材和解码过程,不能只凭暗边就断言唯一原因。

7.6 为什么增加缓冲数量不一定能解决掉帧,还可能增加延迟?​

考察重点:生产消费速度、排队与呈现机会。

相关内容:合成、缓冲与显示

参考回答

缓冲提供并行与排队空间,不会自动缩短一帧的计算时间。 生产太慢时仍然没有新帧,积累过多已完成帧则可能让新输入对应的画面更晚显示。需要结合 CPU、GPU、同步与呈现记录判断瓶颈,再决定缓冲策略。

8. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明