CPU、GPU 与屏幕绘制
这篇笔记用于理解一帧画面从“程序决定要画什么”到“显示器真正显示出来”的大致流程。重点不是图形 API 细节,而是厘清 CPU、GPU、Shader、显存、Frame Buffer、显示控制器之间的分工。
核心分工
现代图形系统里,CPU 和 GPU 都参与绘制,但职责不同:
| 组件 | 更擅长 | 在渲染中的典型职责 |
|---|---|---|
| CPU | 复杂控制流、调度、少量高分支逻辑 | 游戏逻辑、脚本、AI、物理调度、生成渲染命令 |
| GPU | 大量相似数据的并行计算 | 顶点处理、像素计算、纹理采样、后处理、合成 |
| 显存 / 图形内存 | 存放 GPU 直接读写的数据 | 模型、纹理、渲染目标、Frame Buffer |
| 显示控制器 | 按刷新节奏输出画面 | 从 Frame Buffer 扫描像素并输出到 HDMI / DP |
可以粗略理解为:CPU 决定画什么,GPU 计算怎么画,显示控制器负责把最终画面送到显示器。
Shader 是什么
Shader(着色器)本质上是在 GPU 上运行的小程序。它最早主要负责计算颜色,所以叫“着色器”;但现代 Shader 已经不只做着色,也可以处理顶点、粒子、后处理、通用并行计算等任务。
常见类型:
- Vertex Shader(顶点着色器):处理顶点位置、骨骼动画、坐标变换等。
- Pixel / Fragment Shader(像素 / 片元着色器):计算像素颜色,处理纹理、光照、阴影、材质等。
- Compute Shader(计算着色器):不一定直接画图,可用于粒子、后处理、图像处理、物理、AI 或其他通用并行计算。
Shader 和普通 CPU 程序的区别不在于“是不是代码”,而在于它运行在 GPU 上,并会被大量数据并行执行。例如一个 Pixel Shader 通常会被很多像素同时执行。
为什么要编译 Shader
开发者通常用 HLSL、GLSL、MSL、WGSL 等高级语言写 Shader,但 GPU 不能直接执行这些源代码。运行前需要把它们编译成当前显卡、驱动、图形 API 能执行的形式。
大致流程:
Shader 源码 / 中间表示
↓
图形 API / 驱动编译
↓
当前 GPU 可执行的机器代码或管线状态
↓
GPU 执行
游戏启动时看到“编译着色器”,通常是在为当前机器生成或准备这些 GPU 程序和管线缓存。第一次启动慢,之后变快,是因为结果会被游戏、图形 API 或显卡驱动缓存。
会重新编译的常见原因:
- 游戏更新,Shader 或材质组合发生变化。
- 显卡驱动更新,旧缓存失效。
- 更换显卡、系统或图形 API。
- 用户清理了 Shader Cache。
如果游戏不提前编译,而是在第一次遇到某个材质或特效时现场编译,就可能出现 Shader Stutter(着色器卡顿)。
一帧画面如何产生
以游戏或复杂 3D 应用为例,一帧画面通常不是 CPU 算完所有像素后交给 GPU,而是 CPU 准备数据和命令,GPU 自己完成主要绘制。
CPU
↓ 计算游戏逻辑、相机、动画、可见对象
CPU
↓ 生成 Command Buffer / Draw Calls
GPU
↓ 读取显存中的模型、纹理、常量数据
GPU
↓ 执行 Vertex Shader、Rasterization、Pixel Shader、Compute 等阶段
GPU
↓ 把结果写入 Render Target / Frame Buffer
Display Engine
↓ 按刷新节奏读取 Frame Buffer
显示器
关键点:GPU 渲染结果通常不会再交回 CPU 写入显存。GPU 自己就能写显存,显示控制器也能直接从显存读取最终画面。
CPU 能不能直接绘制
可以。CPU 直接计算每个像素并写入一块图像内存,这叫 Software Rendering(软件渲染)。
早期游戏、部分模拟器、远程桌面、软件光栅器都可能使用这种方式:
CPU
↓ 计算每个像素颜色
CPU
↓ 写入 Frame Buffer 或普通内存图像
显示系统
↓ 显示出来
但现代游戏和浏览器图形渲染通常不这样做,因为高分辨率、高帧率、高质量光照会带来巨大的并行计算量。GPU 拥有大量并行执行单元和高显存带宽,更适合处理“很多像素 / 很多顶点执行相似计算”的任务。
所以分工通常不是:
简单任务 → CPU
复杂任务 → GPU
而是:
控制流复杂、分支多、调度型任务 → CPU
数据量大、计算模式相似、可并行任务 → GPU
即使只是画一个简单矩形,现代图形系统也通常交给 GPU,因为混合 CPU/GPU 直接绘制会引入额外的数据复制、同步等待和实现复杂度。
CPU 和 GPU 如何访问显存
独立显卡
独立显卡通常有自己的 VRAM:
CPU / System RAM
↓ PCIe
GPU / VRAM
GPU 访问 VRAM 的带宽很高,是显存的主要操作者。CPU 也可以通过 PCIe 上传数据或读取结果,但速度和延迟都不适合频繁逐像素操作。
常见数据流:
- CPU 上传模型、纹理、常量 Buffer。
- CPU 提交命令。
- GPU 读取这些数据并渲染。
- GPU 把结果写回 VRAM 中的 Render Target / Frame Buffer。
- 显示控制器从 Frame Buffer 扫描输出。
集成显卡 / 统一内存
集成显卡和 Apple Silicon 这类统一内存架构中,CPU 和 GPU 可能共享同一块物理内存:
CPU
↘
Unified Memory
↗
GPU
这减少了 CPU 内存和显存之间的复制,但不代表可以随便同时读写同一份数据。CPU 和 GPU 仍然需要同步机制来保证读写顺序。
同步为什么重要
CPU 和 GPU 可以访问同一类资源,但不能无序地同时修改同一块数据。否则会出现读到一半新数据、一半旧数据的问题。
图形 API 会用同步机制约束访问顺序:
- Fence:CPU/GPU 之间等待某个任务完成。
- Semaphore:GPU 队列之间同步。
- Barrier:约束资源在不同阶段的读写顺序和状态转换。
同步不是细枝末节。过少会导致数据错误,过多会让 CPU 或 GPU 互相等待,降低并行度。
和浏览器渲染的关系
浏览器中的 DOM、CSSOM、Layout、Paint 更多发生在浏览器渲染流水线层面;GPU 主要参与合成层、Canvas、WebGL、WebGPU、视频、部分滤镜和动画等工作。
在前端性能语境里,常见结论是:
- 修改
width、height、top等布局相关属性,通常会触发 layout / paint / composite。 - 修改
transform、opacity等合成友好的属性,常常可以跳过 layout 和 paint,只做 composite。 will-change可以提示浏览器提前准备合成层,但滥用会增加内存和管理成本。
更完整的浏览器流水线见 render.md。
易混点
| 说法 | 更准确的理解 |
|---|---|
| GPU 算完后要交给 CPU 写屏幕 | 通常不用,GPU 写 Frame Buffer,显示控制器直接扫描输出 |
| Shader 只是负责上色 | 名字来自上色,但现代 Shader 是 GPU 程序,职责更广 |
| 简单图形给 CPU,复杂图形给 GPU | 实际按任务形态分工:控制调度偏 CPU,大规模并行偏 GPU |
| CPU 和 GPU 可以随便同时改显存 | 可以共享或访问,但需要同步,否则会产生数据竞争 |
| GPU 加速一定更快 | 需要考虑上传、同步、调度和内存成本,小任务未必划算 |
复习问题
- 为什么现代渲染通常是 CPU 提交命令,而不是 CPU 逐像素写屏幕?
- Shader 为什么需要针对显卡、驱动或图形 API 编译?
- Frame Buffer 是由谁写入,又由谁读出到显示器?
- 独立显卡和统一内存架构下,CPU/GPU 访问图形数据的差异是什么?
- 为什么 CPU/GPU 同步过多也会成为性能问题?