资讯动态

从固定管线到Vulkan:可编程着色与图形API演进实录

发布时间:2026/9/11 13:59:06 来源:尧图企业网站定制
我把《实时渲染》里关于可编程着色与API演进这节内容结合自己这几年在渲染管线和图形驱动上踩过的坑重新串了一遍。这话题听起来像纯历史回顾但实际上直到今天每一代新GPU、每一版新API都还在沿着那条“把控制权还给开发者”的路线走。无论你是刚接触着色器的小白还是被Vulkan折磨过的老手这篇文章应该都能给你一点“原来如此”的瞬间。1. 从固定管线到可编程着色一场结构性的权力转移1.1 固定管线到底有多“固定”大概在DirectX 8之前GPU内部的工作方式是一套焊死的流程。顶点进来先乘模型视图投影矩阵再做光照计算通常是Phong或Blinn-Phong然后裁剪、光栅化逐像素查纹理、做雾效、深度测试、混合每一步都由硬件里预设的模块完成。开发者能做的只是用glEnable(GL_LIGHTING)、glLightfv()、glTexEnv()这类API去拨动开关。能用但想做点“超纲”的东西就非常痛苦。我在早期做引擎的时候想实现一个细胞边缘发光的效果只能靠多层Pass叠加、魔改纹理坐标和混合因子来硬凑。每改一次需求就要重新设计一整套状态组合。固定管线的问题不在于性能而在于它的“能力上限”非常低它把硬件潜能锁在一个很小的框里。1.2 可编程着色器为什么会成为转折点2001年前后NVIDIA GeForce 3和DirectX 8把“可编程顶点着色器”和“可编程像素着色器”带进主流视野。这两个东西的英文名很直白Vertex Shader和Pixel Shader。开发者第一次可以上传一段程序到GPU里让GPU在顶点处理阶段和像素处理阶段执行这段程序而不是调用硬件写死的模块。当时的着色器还非常原始顶点着色器只有约128条指令像素着色器更惨能做的操作极其有限而且语法接近汇编。但它意味着一个根本性转变GPU从“专用图形硬件”变成了“可编程并行处理器”。开发者不再受限于厂商预设的光照模型而是可以自己定义光照、阴影、扭曲、卡通描边、法线贴图。这就像过去你只能在食堂打饭窗口卖什么你吃什么现在食堂把后厨开放了允许你自己带菜谱进去做菜虽然一开始炊具简陋、调料有限但方向已经完全不同。1.3 着色器模型和特征分级硬件能力的“语言”可编程着色器出现后硬件厂商面临一个很现实的问题不同代GPU的能力差异太大API标准该如何定义微软的答案是“Shader Model着色器模型分级”。从SM 1.0到SM 3.0每一级都规定了指令数量、寄存器数量、支持的数据类型和特性集。游戏开发者就可以这样写逻辑如果硬件支持SM 3.0就用高精度浮点、动态分支否则就退回SM 2.0。这个分级制度影响至今Vulkan里的VkPhysicalDeviceProperties、DX12里的D3D_FEATURE_LEVEL本质上还是在做能力协商。这段历史的核心启示是API不只是“怎么调用GPU”的说明书它还是“厂商与开发者之间如何共同管理硬件复杂度”的契约。可编程着色器把能力交了出来但能力怎么被描述、怎么被约束就是API设计者们持续争论的事情。2. GPU架构随着色需求发生的剧变2.1 从“分工明确”到“统一着色器”早期的可编程GPU里面顶点着色器和像素着色器是两套完全不同的硬件单元。顶点着色器擅长跑标量/向量运算像素着色器更像一个小型SIMD机器。这种设计的问题是负载很容易不均衡——场景里顶点少、像素多顶点单元在偷懒像素单元在排队反过来也一样。到了Xbox 360的Xenos GPU和NVIDIA的G80核心时硬件设计改成了“统一着色器架构”同一组执行单元既能跑顶点着色器也能跑像素着色器甚至可以跑几何着色器、计算着色器。统一着色器架构的意义远不止“省晶体管”。它让GPU变成一个真正通用的并行计算平台。G80时代的CUDA之所以能横扫科学计算领域根源就是这套统一架构。放在今天看NVIDIA的Ampere、Ada LovelaceAMD的RDNA系列本质上依然是统一着色器架构的延续只不过在通用ALU阵列旁边增加了Tensor Core、RT Core这类专用单元。也就是说可编程着色器走完了一半路硬件又把一部分高频工作“固化”回去形成通用与专用并存的现状。2.2 SIMT、线程束与“假装流式”的真相现代GPU执行着色器的模型叫SIMT单指令多线程。CPU是少数几个强壮的核心各自跑不同的指令GPU则是几千个轻型核心执行同样一条指令但作用在大量数据上。以NVIDIA为例32个线程组成一个warp这32个线程同时执行同一条指令如果出现分支分歧一部分线程走if一部分走elseGPU会分两次执行浪费一半的吞吐。AMD里对应的概念叫wavefront通常是64个线程。做渲染优化的人平时说的“不要写线程间互相依赖的代码”“避免分支发散”根源就在这里。任何看似优雅的动态分支在GPU上都会变成代价高昂的串行化操作。这一点在可编程着色器刚出现的年代还不太明显因为那时的着色器短小、逻辑简单等到现代游戏里一个像素着色器动辄上百行分支发散带来的性能损耗就相当可观了。2.3 架构演进对开发者的直接影响GPU架构升级带来的第一个直接变化是着色器可以越来越长逻辑可以越来越复杂。SM 3.0开始支持动态分支SM 5.0加入计算着色器后GPU不再是只能干渲染的“显卡”而是可以处理通用计算任务。对开发者来说这意味着渲染器设计时必须考虑硬件内部并行度。比如用Wave Intrinsics线程束内指令做共享数据交换或者用subgroup操作来减少显存带宽消耗这些都是过去不可能有的工具。另外还有一个容易被忽略的点不同厂商GPU的底层着色器执行方式并不一样。所以“同一个着色器在不同卡上表现完全一致”从来都是错觉。实际开发时我通常会给关键Pass准备多套实现至少保证在最坏情况下能正确渲染再逐步针对主流GPU做调优。3. API演变的三个时代从状态机到显式控制3.1 OpenGL时代灵活与混乱并存OpenGL的历史比DirectX还早它源自SGI的IRIS GL设计哲学是“一个巨大的状态机”。你想让物体透明就设置混合状态你想关闭深度写入就设深度掩码。OpenGL的扩展机制GL_ARB_xxx、GL_EXT_xxx让它极具生命力但也带来了不一致性。我记得早期做跨平台渲染时最怕的就是在某块显卡上某个扩展可用在另一块上不可用代码里全是条件编译和运行时检查。OpenGL 2.0在2004年引入了GLSLOpenGL着色语言这对开发者是个巨大的解脱——终于可以在C语言风格的代码里写shader而不是面对一堆mov、mul指令。但OpenGL的设计问题依然明显驱动承担了大量隐藏状态管理和依赖检查CPU提交开销高而且多线程支持很弱。到了移动端OpenGL ES虽然优化了功耗但同样继承了状态机的复杂度和驱动黑盒问题。3.2 DirectX 9到DirectX 11微软的“重型标准”DirectX走的是一条与OpenGL完全不同的路线由微软统一推动版本迭代干脆但平台绑定严重。DX9时代是很多人心目中的“黄金时代”因为HLSLHigh-Level Shading Language和Effects框架让着色器开发效率大幅提升。配合Radeon 9700这个级别的硬件很多经典游戏画面就是在DX9下做出来的。DX9的缺点是状态管理依然繁琐GPU能力靠cap bits枚举特性组合爆炸驱动层已经有了“汇编器”的味道。DX10引入了Geometry Shader、统一着色器模型等新特性但它只支持Windows Vista用户覆盖率低很多工作室绕开它直接等DX11。DX11是比较成熟的一代Tessellation、Compute Shader、大统一特性集而且向下兼容DX9/10硬件。在很长一段时间里游戏工作室的默认目标是DX11因为它的抽象层次适中既能发挥GPU性能又不会像最新API那样把所有责任都甩给开发者。3.3 现代图形API革命Vulkan、Metal与DirectX 122014年到2016年图形API迎来一次真正的“低开销革命”。AMD的Mantle拉开了序幕随后微软推出DirectX 12苹果推出MetalKhronos推出了Vulkan。这套新API的共同点非常明确把驱动层的隐式管理搬到显式层让开发者直接控制命令缓冲区、资源屏障、同步对象、内存分配。换来的是更低的CPU开销、更可控的多线程渲染、更透明的资源生命周期。Vulkan在这三者里跨平台能力最强但学习曲线最陡。我刚上手Vulkan时的感觉是这完全不是“OpenGL换了个壳”而是让你用写驱动的方式做应用层开发。你必须自己创建实例、选择物理设备、创建逻辑设备、配置队列、创建交换链、构建渲染Pass、管理描述符布局、分配命令缓冲……光是把一个三角形画出来就需要几百行初始化代码。DirectX 12的复杂度类似但调试工具和文档更成熟Metal则胜在iOS/macOS生态内体验顺滑抽象层次比Vulkan稍高一点。3.4 为什么低开销API会赢得未来低开销API的本质是把控制权从驱动手里夺回来。过去驱动替你做的很多决定——比如什么时候切换状态最合适、资源什么时候可以安全复用——现在都由你的代码负责。这是一把双刃剑优化得当性能可以大幅提升优化不当运行效率甚至不如老API。我的实践经验是现代API最大的收益不是单帧渲染更快而是CPU多核利用率上去了。DX11时代你很难多线程并行记录渲染命令Vulkan里却能开多个线程同时往不同命令缓冲区里填数据再提交到同一个队列。对引擎开发者来说现代API的另一个价值是“可预测性”。驱动不再背着你在背后做缓存和重排你看到的就是GPU真正在执行的指令序列。配合RenderDoc和Nsight Graphics你能非常精确地定位到每个Draw Call的耗时、每段屏障的开销。这种透明性对于做渲染底层和性能分析的人来说价值怎么强调都不夸张。4. 现代图形API实操要点与避坑记录4.1 管线状态对象PSO性能与灵活性的平衡OpenGL时代切换一次状态驱动内部可能要编译、重排一堆东西Vulkan/DX12管这个叫Pipeline State Object管线状态对象可以理解成一个“完整烤好的配置组合”。你要在创建阶段就把着色器、顶点输入布局、光栅化状态、深度模板状态、混合状态全部确定下来生成一个不可变对象然后运行时切换管线就是一次指针替换。这样做的好处是驱动可以预编译、预优化整条状态流水线坏处是你需要提前枚举出所有会用到的状态组合。这里我踩过一个很深的坑项目早期为了灵活每帧动态生成PSO结果运行时同步等待驱动编译帧率掉到不忍直视。后来改成预创建PSO缓存并在加载界面用多线程预编译所有组合问题立刻解决。如果你用Vulkan建议把PSO缓存到磁盘pPipelineCache否则每次启动都要重新编译一遍加载时间会明显变长。4.2 资源绑定模型描述符、堆与Bindless现代API的资源绑定模型和老API差异非常大。OpenGL里你可以随手给uniform变量赋值驱动帮你处理一切Vulkan里你需要先创建DescriptorSetLayout和DescriptorPool再把纹理、Uniform Buffer、Storage Buffer绑定到描述符上最后在命令里绑定描述符集。这套机制听起来繁琐但好处是驱动能精确知道资源访问模式也不需要再做任何隐式状态扫描。描述符相关的常见错误包括描述符池分配耗尽、layout不匹配导致校验错误、在GPU还在读取资源时故意释放内存。强烈建议开发阶段开启Vulkan的Validation Layers它能帮你捕捉绝大多数资源绑定错误。Bindless技术把大量资源放进一个大描述符数组是现代引擎做大批量物体渲染的重要底牌但实现复杂度不低需要合理规划显存和描述符索引。4.3 同步原语渲染帧中的“交通规则”现代GPU是高度并行的异步设备CPU在准备第N2帧的命令GPU可能正在执行第N帧同时DMA引擎在拷贝第N1帧的资源。如果没有同步原语就会出现读写冲突、画面闪烁甚至驱动崩溃。Vulkan里常用的同步工具包括FenceCPU等待GPU完成、SemaphoreGPU队列之间的依赖、Event同一个队列内部依赖和资源状态转换Image Layout Transition。初学者最容易犯的错误是要么同步过度GPU流水线被卡成串行要么同步不足冒出奇怪的画面撕裂或闪烁。我用过的最有效的调试工具是这样的先保证每帧只有一个in-flight frame也就是CPU永远等待上一帧GPU完成虽然会浪费一点并行度但能快速判断是不是同步问题。确认稳定后再把in-flight frame数量提升到2到3同时检查vkQueueSubmit的依赖关系。这套“先保守后激进”的策略适合从零开始写现代API渲染器的朋友。4.4 GPU Crash与调试工具链搜索热词里出现过gpu crash dump triggered大多数情况下这不是GPU“坏了”而是你的命令序列里有非法操作访问了已经释放的资源、越界写、使用了不匹配的描述符、资源状态转换缺失。老API时代这类错误往往被驱动兜住最多出现黑屏或花屏现代API下驱动不再做防御性检查错误会直接触发GPU掉线和驱动重置。调试这类问题最有效的手段是用GPU厂商提供的调试工具NVIDIA的Nsight Graphics可以逐Draw Call检查资源状态RenderDoc能把每一帧的每个资源、每个Pipeline、每次绘制调用的参数全部记录下来。遇到摸不着头脑的花屏时先用RenderDoc回放单帧然后在Pixel History里逐Pass排查如果回放结果都正常再怀疑是不是多帧之间的异步问题。循序渐进地排查远胜于盲改代码赌运气。5. 可编程着色与现代GPU计算的交汇5.1 通用计算Compute Shader、CUDA与AI的“意外继承者”可编程着色器把GPU变成了可以执行任意数据的并行处理器这件事的最大受益者其实不是游戏行业而是通用计算领域。2007年CUDA发布后科学家和工程师开始用GPU做物理模拟、分子动力学、图像处理。然后就是深度学习爆发——矩阵乘法和卷积运算恰好是GPU最擅长的工作。今天大家讨论PyTorch到底怎么装GPU版、llama.cpp怎么让大模型跑在显卡上背后的核心逻辑和二十年前讨论“怎么让GPU执行非图形程序”其实是同一件事。从技术角度说PyTorch装上CUDA之后本质就是它调用cuBLAS、cuDNN这些GPU加速库在显卡上执行张量运算。llama.cpp想用GPU推理一般有CUDA后端、Vulkan后端或者Metal后端可选选型时要看你的显卡、显存和驱动支持情况。这些工具链的体验好不好往往不取决于模型本身而取决于你对底层硬件调度和显存管理的理解程度。5.2 从渲染到AI推理显存、驱动与运维的共通经验渲染和AI推理看起来是两个世界到了硬件层面却共用同一套规则GPU驱动版本必须匹配、显存是一等公民、温度功耗会影响稳定性。搜索热词“gpu服务器运维都做哪些工作”其实里面很大一部分工作和图形渲染团队里的“工具链维护”非常相似。你得会用nvidia-smi监控显存和功耗定期看dmesg有没有GPU总线错误记录知道驱动升级可能带来什么样的变化还得管理好运行时的显存分配防止碎片化和溢出。我也遇到过有人在游戏引擎的渲染机上安装老款Tesla P100、P40这类计算卡配合普通游戏显卡共存。操作流程一般是先确认主板PCIe通道数够用在系统里屏蔽默认的nouveau驱动用NVIDIA官方runfile安装对应版本驱动再设置persistence mode保持GPU常驻。这个流程和服务器运维里的“GPU驱动部署”几乎没有区别。别小看这些基础操作很多时候你的程序性能不佳根源不是代码而是驱动没配对、供电模式没切换、显存频率锁在低档。5.3 大模型API背后隐藏的GPU问题搜索热词里那些deepseek api如何调用、api error: 400 this models maximum context length表面上是一个HTTP接口的调用问题但如果你认真思考一下会发现它们背后都连着GPU资源规划。一个模型的上下文长度达到1048576个token意味着推理时需要为这么多token维护KV Cache显存占用会指数级上涨。如果你直接调用云端API服务商帮你处理了这部分算力如果你想把大模型部署在公司内网、用私有数据跑推理那GPU显存估算、显存带宽、并发请求调度就是你必须自己面对的事情。我知道不少团队会先租GPU云服务器做验证再决定采购什么卡。这个思路很务实先用最低成本验证模型的显存占用和推理速度再反过来规划你的硬件预算。GPU租用平台通常按小时计费用nvidia-smi看得清清楚楚——这和你本地调试渲染器时看显存占用曲线没有本质区别。了解GPU的人无论做渲染还是做AI思路都是通用的。最后一点实践经验如果你现在正准备学习可编程着色我的建议非常直接不要一上来就扎进Vulkan的大坑先用OpenGL或WebGL把顶点着色器和片段着色器的基本流程跑通。渲染一个会转的三角形给物体加一盏平行光再做一次简单的纹理采样——这些基础操作能帮你建立“着色器是在GPU上执行的小程序”这个直觉。有了这个直觉再去碰现代API你会更容易理解描述符、管线状态、同步原语这些东西到底在解决什么问题。我自己带过的不少新人最容易犯的错是过早追求高深技术第一天就在学PBR第二周就想写光追结果基础的光照模型和坐标系变换都没搞明白。图形学这件事“手熟”比“知道”重要得多。你今天用老API写的每一个简单例子都会成为日后理解新API的垫脚石。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价