概念页
绘制调用与状态切换优化
Draw Calls & State Change Optimization
调用图形 API 的开销主要在 CPU 端的驱动里:状态切换昂贵,每次绘制调用有固定开销,大量小批次会让 GPU"饿死"。对策是:合理放置缓冲;按状态代价排序分组;共享纹理和着色器;合并网格和实例化以减少调用;并用一个整数排序键同时兼顾状态分组与前后排序。
缓冲的放置(18.4.2 节)
- 独立显卡的桌面系统 CPU 与 GPU 各有内存,驱动决定资源位置但可接受提示:每帧变化的数据用动态缓冲(不需要 GPU 上的永久存储);不变形或只靠着色器变形(如蒙皮)的物体用静态缓冲存放在 GPU 内存,避免每帧经总线传输——GPU 内部带宽远高于 CPU-GPU 总线。
- 主机、集成显卡笔记本和移动设备多为统一内存,仍应正确标记资源为仅 CPU 或仅 GPU:两者都访问的区域一方写入后另一方必须使缓存失效,代价很高。
状态切换的代价
- API 调用越多应用时间越多;依赖 GPU 返回值的查询函数可能因 CPU 同步停顿让帧率减半。
- GPU 可能是计算机科学中最复杂的状态机,状态切换的大部分开销在 CPU 上的驱动。GPU 与 API 匹配时代价可预测;功耗或面积受限、有硬件 bug 要绕过时,驱动可能付出意外的高代价。例:PowerVR 的混合是可编程的,驱动需要把当前混合状态打补丁进像素着色器。"效果因平台而异"。
- Everitt 与 McDonald 给出 2014 年 NVIDIA OpenGL 驱动上从贵到便宜的大致顺序:
| 状态 | 每秒约可执行次数 |
|---|---|
| 渲染目标 (FBO) | ~60k |
| 着色器程序 | ~300k |
| 混合模式 (ROP) | — |
| 纹理绑定 | ~1.5M |
| 顶点格式 | — |
| UBO 绑定 | — |
| 顶点绑定 | — |
| uniform 更新 | ~10M |
比这些更贵的是在 GPU 渲染模式与计算着色器模式之间切换。
减少状态切换
- 按状态排序(批处理 batching):先按着色器分组,再按纹理,依代价顺序往下。
- 共享数据:把多张纹理放进大图集,最好是纹理数组;支持时用无绑定纹理 (bindless)。着色器切换比更新 uniform 贵,所以同一类材质的变体可用带 if 的单个着色器表示以得到更大批次——但着色器变复杂可能降低 GPU 性能,只能测量判断(参见 着色频率与材质系统)。
- 成组设置:把多个 uniform 放进一个 uniform 缓冲(DirectX 称常量缓冲)一次绑定,省调用时间和逐调用错误检查。
- 冗余过滤:现代驱动通常把状态设置推迟到第一次绘制,并过滤冗余调用(用脏标记)。所以每次绘制后"恢复默认状态"(
Enable(X); Draw(M1); Disable(X);再Enable(X); Draw(M2); ...)会浪费大量时间。应用层往往更清楚何时真正需要切换,例如不透明的 replace 到透明的 over 每帧只需一次。Galeano 发现不过滤冗余调用会让他们的 WebGL 应用每帧多花近 2 ms;但驱动已高效过滤时,应用再逐调用检查就是浪费,投入多少取决于驱动。
小批次问题:合并与实例化
- 每次绘制调用都有与大小无关的固定开销,少量大网格远比大量小网格高效。2003 年 Wloka 测得每批两个三角形时吞吐量比峰值低 375 倍(每秒 0.4M 而非 150M 三角形),场景完全受 CPU/API 限制,GPU 无能为力。其经验法则:"每帧只能有 X 个批次";2003 年拐点约每物体 130 个三角形、建议每帧 300 次绘制,2006 年拐点升到 510 个三角形,2012 年有团队的上限是每帧 16,000 次。DirectX 12、Vulkan、Metal 能把驱动开销降到很低,但 GPU 自身对每个网格也有固定成本。
- 合并 (consolidation):把状态相同、彼此相对静止的物体合并成一个网格,一次生成反复使用;同时也减少应用处理的物体数。坑:批次过大会让视锥剔除等算法失效,可借助包围体层次把相邻静态物体分组;合并后物体不可区分,选择拾取时可在每个顶点存物体 ID。
- 实例化 (instancing):一次调用多次绘制同一基础模型,另有每实例数据结构(位置、朝向,以及叶子颜色、风吹弯曲等任何着色器可用的属性)。适合茂密丛林、植被(CryEngine 中同色物体一次绘制)和人群(从部件集合挑选身体部位、随机着色和贴花),可与 LOD 结合(远处用替身)。合并实例化 (merge-instancing) 让合并网格中的物体本身也能被实例化。
- 坑:几何着色器理论上能复制数据做实例化,但实例多时比 API 实例化慢,其本意是局部小规模放大;Mali 等分块渲染器上几何着色器是软件实现。Mali 最佳实践指南原话:"Find a better solution to your problem. Geometry shaders are not your solution."
排序键:状态排序与深度排序的折中(18.4.5 节)
按材质分组和按距离从前往后排序通常给出不同顺序,二者冲突。常见灵活方案是为每个物体建一个整数排序键,从高位到低位依次是:
flowchart LR A["透明位: 透明物体排在所有不透明物体之后"] --> B["低精度距离: 透明物体取反以从后往前"] B --> C["着色器 ID"] C --> D["纹理 ID"]
如上图,按整数从小到大排序即可。限制距离位数(甚至只分两三个深度区间)可以让同一距离范围内按着色器分组生效;深度和着色器都相同时再按纹理分组。可增删字段,例如混合状态、z-buffer 读写。也可混合方案:近处按深度、其余按材质。坑:一些移动端分块 GPU 从前往后排序毫无收益,只需优化状态排序。
GEA 的视角:为什么旧 API 的状态切换贵,以及 PSO(卷 II 11.4.8、11.5.2)
全局管线状态
- 每个管线阶段都有自己的状态(例如顶点着色器用哪个程序、输入输出格式、绑定哪些顶点缓冲;光栅化器的填充模式、剔除模式、是否保守光栅化),合起来叫管线状态。GPU 内部同时跑很多阶段实例,但在 API 的编程模型里只有"一条"管线:一个 draw call 的所有实例共享提交时生效的状态,不同网格需要不同配置就要在 draw call 之间重新设置(卷 II 11.4.8)。
- OpenGL 把状态当全局变量;D3D11 用设备上下文对象
ID3D11DeviceContext,但它实际上是单例(立即上下文),状态同样是全局的。API 在底层把这些调用翻译成 CPU 端的命令列表提交给 GPU,GPU 按收到的顺序执行。
状态切换为什么贵
- D3D11 中各阶段状态分开设置,例如用
OMSetBlendState设置混合状态。但在现代 GPU 上多个阶段的状态往往相互依赖,改混合状态可能牵动光栅化器状态,所以 OpenGL 和 D3D11 每改一个阶段都可能被迫做昂贵的全管线重配置(卷 II 11.4.8.4)。 - 因此有经验的程序员都知道要尽量减少状态切换:把共享同一组材质属性的网格归成批次,每批先配置一次状态,再用一次 draw call 画完。这与本页「减少状态切换」一致。
按材质排序与过度绘制的冲突
- 按材质排序会增加过度绘制,而 early-z 要求从前往后画。GEA 的解决办法是 z 预通道:第一遍从前往后只写深度,第二遍按材质排序画颜色(卷 II 11.5.2,详见 深度缓冲 中的 GEA 部分)。本页「排序键」一节用一个排序键折中状态排序和深度排序,是另一种思路。
管线状态对象 (PSO)
- D3D12 把完整管线状态封装进 PSO(
ID3D12PipelineState),其中既有应用请求的高层配置,也有 GPU 用的设备专用数据。创建 PSO 时解析跨阶段依赖,并把着色器编译成本机 GPU 的机器码;之后用它配置硬件就很快(卷 II 11.4.8.5)。 - 通常每种状态组合(大致每种材质)建一个 PSO,可以在启动时全部创建,相当于把状态切换的代价预付掉,不必每帧付多次。
- 代价与折中:PSO 创建可能很慢,玩家会因首次启动太久而不满。办法之一是按 GPU-驱动组合把 PSO 缓存到磁盘,只付一次;但首次启动仍可能很慢,所以许多游戏改为在运行中按需创建 PSO,在"每帧付状态切换代价"和"启动时付全部 PSO 创建代价"之间取得平衡。
- 补充:GEA 没有提到按需创建的副作用。PSO 创建(含驱动编译着色器)通常要几毫秒到上百毫秒,在渲染线程上首次遇到新材质时同步创建就会造成明显的卡顿,即 PC 游戏常见的"着色器编译卡顿";所以引擎通常在后台线程预缓存(在加载阶段根据将要用到的材质组合提前异步创建),而不是等到绘制时才建,例如 UE5.1 起的 PSO Precaching(Epic "PSO Precaching for Unreal Engine" 文档)。
- D3D12、Vulkan、Metal 没有设备上下文,直接暴露命令列表,不再受单线程立即上下文的限制;D3D11 的延迟上下文只是把命令录进内存,必须经立即上下文提交,用来让多个线程并行录制(卷 II 11.4.8.2–11.4.8.3,见 多线程渲染与任务系统)。API 层面的更多内容见 图形 API 设备、管线状态与资源。
出处
- realtime_rendering_4ed.pdf 第 18 章 Pipeline Optimization
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 11 章 11.4.8、11.5.2 节
相关页面
图形学渲染瓶颈定位与性能度量Bottleneck Location & Performance Measurement渲染管线中最慢的阶段(瓶颈)决定整体吞吐量。优化就是"找到瓶颈 → 优化到瓶颈转移 → 重复",并让非瓶颈阶段做到与瓶颈同样多的工作以换取画质。定位靠逐阶段增减负载的实验和性能工具;度量要用毫秒帧时间而不是 FPS。两条箴言:了解你的架构 (KNOW YOUR ARCHITECTURE);测量、测量、再测量。图形学像素与帧缓冲优化Pixel & Framebuffer Optimization需要着色的像素通常远多于顶点,所以像素处理优化往往最划算。核心手段:保证占用率以隐藏纹理延迟、按距离切换着色器 LOD、利用 early-z 并通过从前往后排序或 z-prepass 减少过度绘制、缩小帧缓冲每像素存储(色度子采样、YCoCg 棋盘),以及只在需要时开启混合、避免 GPU 回读。几何与光栅化阶段的常用优化也一并列出。游戏开发多线程渲染与任务系统Multithreaded Rendering & Task SystemsCPU 时钟频率约在 2003 年停在 3.4 GHz 附近后,性能来自多核。应用阶段的优化一是让代码与内存访问更缓存友好,二是利用多核:多处理器流水线(时间并行,提高吞吐但增加延迟)、并行处理(空间并行,延迟更低)以及两者结合的任务系统;图形 API 用命令缓冲让多个线程并行录制绘制命令,新一代 API(DirectX 12、Vulkan、Metal)把更多责任交给应用以降低驱动开销。图形学三角形带、扇与索引网格Triangle Strips, Fans & Indexed Meshes让共享顶点的三角形成组送入管线,可以减少顶点着色器调用。三角形扇和带把每三角形发送的顶点数降到接近 1;现代 GPU 的标准做法是带单一索引缓冲的三角形网格,理论上每三角形只需处理约 0.5 个顶点,实际效果取决于变换后顶点缓存,由三角形顺序优化(Forsyth、Tipsify 等)决定。图形学着色频率与材质系统Shading Frequency & Material Systems实现着色时要按"计算频率"划分工作:整个 draw call 不变的放到应用端作为 uniform,其余放在相应的可编程阶段。实践中大多数着色在像素级完成,顶点着色器负责变换和变形。材质系统把面向美术的材质与底层着色器组织起来,处理海量着色器变体的组合问题。图形学替身与深度精灵Impostors & Depth Sprites替身 (impostor) 是把复杂物体从当前视点渲染成纹理、贴在视点朝向公告板上的表示,可在多个实例或多帧间复用以摊薄生成成本。公告板云用少量相交的镂空平面近似网格;深度精灵在纹理中加入每像素深度偏移,能与周围几何正确穿插;浮雕纹理、浮雕替身和几何图像进一步模糊了图像与网格的界线(RTR4 13.6.4、13.6.5、13.7 节)。图形学空间数据结构Spatial Data Structures空间数据结构把几何体按 n 维空间组织起来,通常是层次化的树,使"几何体是否重叠"一类查询从 O(n) 降到大约 O(\log n)。常见类型有包围体层次 (BVH)、BSP 树(轴对齐的 k-d 树与多边形对齐两种)、四叉树/八叉树(含松散八叉树),以及更偏向组织模型关系的场景图。它们是剔除、光线求交、碰撞检测等加速算法的基础。图形学遮挡剔除Occlusion Culling遮挡剔除尽早剔除被其他物体挡住的物体,避免它们在 z-buffer 中被光栅化、深度比较甚至着色。本页覆盖遮挡剔除的分类与通用框架、GPU 遮挡查询、层次 Z 缓冲 (HZB) 及其在现代引擎中的简化变体(含两遍法、软件光栅化遮挡),以及在 CPU/GPU 上按物体—簇—三角形多粒度组合剔除并用间接绘制提交的剔除系统。图形学VR 立体渲染优化VR Stereo Rendering OptimizationVR 每帧要为两只眼渲染,并经合成器做镜头畸变与色差校正。本页讲厂商 SDK/API 的帧流程与内建优化(分层合成、隐藏区域网格、裁剪过的畸变网格),立体渲染如何避免工作量翻倍(合并视锥剔除、实例化、多视图扩展、多 GPU 广播、远景单目渲染),以及利用光学和人眼特性减少外围像素着色的技术:多分辨率着色、镜头匹配着色、径向密度遮罩、固定注视点渲染与注视点渲染。游戏开发 · UE5Nanite 虚拟化几何体Nanite Virtualized GeometryNanite 是 UE5 的虚拟化几何体 (Virtualized Geometry) 系统:导入时把网格切成分层的三角形簇 (Cluster),渲染时按相机视角逐簇选择细节层次并无缝拼接,按需从磁盘流送,并在绕过传统绘制调用 (Draw Call) 的独立通道中渲染,使帧预算不再受多边形数、绘制调用和网格内存约束。游戏开发 · UE5UE5 性能分析UE5 ProfilingUE5 性能分析的基本流程是:先用 stat unit 判断瓶颈在游戏线程、渲染线程还是 GPU,再用对应的 stat 组、profilegpu/GPU 统计或 Unreal Insights 的 Timing/Memory 等视图向下钻取;渲染特性(RDG、VSM、TSR)各自提供作用域和可视化。测量时要避开会扭曲数据的设置(调试构建、异步计算、可视化模式)。游戏开发 · UE5UE5 渲染架构UE5 Rendering ArchitectureUE5 的渲染器是独立的 Renderer 模块,在渲染线程 (Rendering Thread) 上以「场景代理」镜像游戏线程的组件状态,每帧按固定顺序执行深度预通道、G-buffer 基础通道、光照、雾、半透明和后处理。现代渲染代码用渲染依赖图 (Render Dependency Graph, RDG) 声明通道与资源,由 RDG 自动排屏障、复用瞬态内存、并行录制;最终命令经 RHI 线程翻译成 D3D12/Vulkan 等图形 API 调用。默认走延迟着色 (Deferred Shading),也可切换前向着色 (Forward Shading)。综合帧时间预算与渲染性能Frame Budget & Rendering Performance本页把 wiki 中分散在 RTR4 第 18~23 章的性能知识串成一条线:一帧的时间预算由最慢的管线阶段决定,先用毫秒帧时间度量并定位瓶颈,再按瓶颈所在位置——CPU/驱动、几何、像素、带宽/内存——选用对应手段;最后从 GPU 硬件机制(占用率、HiZ/early-z、压缩、分块架构)理解这些手段为何有效。各结论的细节与出处见所链接的概念页。游戏开发 · UE5移动端渲染优化Mobile Rendering Optimization移动端优化的核心是认清设备的 CPU/GPU 速度、线程、带宽和内存,然后控制最主要的几项开销:Draw Call(官方称其为当前导致图形性能低下的最大因素,在 Galaxy Tab S6 上建议约 700 个,低端设备少于 500 个)、材质复杂度和 overdraw。合并网格和材质(Substance 把多种材质画进同一张纹理,用顶点色作遮罩)是减少 Draw Call 最直接的办法。游戏开发享元模式Flyweight Pattern享元模式 (Flyweight Pattern) 把对象的数据拆成可共享的“内在状态”和每个实例独有的“外在状态”,让大量对象共用一份内在状态。森林里成千上万棵树共用一份网格和纹理,以及瓦片地图中每格只存一个指向共享地形对象的指针,都是它的典型应用。游戏开发脏标记Dirty Flag脏标记 (Dirty Flag) 用一个布尔标记记录“派生数据是否已经过时”:原始数据变化时置位;需要派生数据时,如果标记已置位就重新计算并清除标记,否则直接用缓存的结果。场景图里由局部变换求世界变换是经典例子。它只在原始数据变化比派生数据被使用更频繁、而且难以增量更新时才划算,最大的风险是漏置标记导致使用过时数据。图形学图形 API 设备、管线状态与资源Graphics API Devices, Pipeline State & Resources所有图形 API(OpenGL、DirectX、Vulkan、Metal)都围绕同一组概念:代表 GPU 的设备、描述各阶段配置的管线状态、承载数据的资源及其视图。GEA 把 API 分成高层(OpenGL、D3D11 及更早)和底层(D3D12、Vulkan、Metal、PlayStation 的 Gnm),并介绍 D3D11 的特性级别、设备上下文与延迟上下文、D3D12 的命令列表、PSO 和根签名。本页按 GEA 的整理讲这些跨 API 的共同概念,不是任何一个 API 的教程。图形学图形栈与渲染引擎架构Graphics Stack & Rendering Engine Architecture渲染引擎要解决的"渲染问题"是:给定由场景元素、虚拟光源和虚拟相机组成的虚拟世界,生成相机看到的二维彩色图像。GEA 把它拆成可见性问题和光照问题,按解法把引擎分为光线追踪、深度缓冲光栅化、辐射度和混合四类;光栅化引擎通常采用立即模式的两层结构:下层是图形管线,上层是负责剔除、提交几何和设置着色器参数的应用层。本页讲这套整体架构,管线内部见 渲染管线。图形学深度缓冲Z-Buffer深度缓冲是几乎所有图形硬件用来解决可见性的算法:为每个像素存一个当前最近图元的深度值,新片元更近才写入颜色和深度。它简单、和图元数量成线性关系、允许大多数图元以任意顺序绘制,但不能直接处理半透明。