Skip to main content

Chapter 117: Real-Time Performance Constraints

实时渲染的约束来自一个固定事实:一帧必须在显示设备下一次需要图像之前完成足够多的工作,并且按稳定节奏交给呈现系统。本章讨论的对象是一帧城市街区飞行镜头:场景包含阴影、粒子、屏幕空间反射、后处理、资源流送和 UI。目标设备以 60 Hz 显示,工程设定 60 FPS,理论帧间隔是 16.67 ms。

读完本章后,读者应能定位一帧超预算的来源,判断 CPU、GPU、present、资源压力和调度策略分别承担什么责任,并设计一套能在负载波动下维持稳定帧时间的降级策略。这里的实时性能约束关注 frame time、frame pacing、latency 和稳定性。平均 FPS 只能说明一段时间内完成了多少帧,无法说明每帧是否按正确时间到达屏幕。

本章会持续使用同一帧作为贯穿材料。它的基础渲染路径是 CPU update → render submit → GPU passes → present → display。当相机进入高密度街区时,粒子数量增加,透明 overdraw 上升,阴影贴图更新增多,纹理流送进入高峰。这个场景可以同时暴露预算分配、监控指标、多线程调度和容错策略之间的关系。

实时约束的核心结论是:稳定帧时间来自预算、证据和回退策略三者闭合。预算给每类工作设置上限;证据告诉工程系统哪一段超限;回退策略在画质、分辨率、LOD、特效和资源加载之间按优先级收敛负载。

117.1 实时渲染的性能预算与帧率目标

实时渲染的性能预算是一个目标帧间隔内可使用的工作时间。60 FPS 对应 16.67 ms,120 FPS 对应 8.33 ms,90 FPS 对应 11.11 ms。这个数字来自显示刷新周期和目标帧率,因此它同时约束 CPU 逻辑、渲染提交、GPU 执行、present 队列和延迟余量。

帧率目标应先转换成 frame time 目标。FPS = 1000 / frame_time_ms 只适合做最终换算,工程判断应直接观察毫秒。一个 60 FPS 项目中,18 ms 的帧已经错过 16.67 ms 目标;一个 120 FPS 项目中,9 ms 也已经超出 8.33 ms 目标。毫秒视角能直接暴露哪个 pass、哪段 CPU job、哪次资源上传占用了预算。

目标刷新或帧率理论帧间隔建议预留余量后可用工作时间典型约束
30 FPS33.33 ms28 到 30 ms可容纳更重后处理,但输入延迟更高
60 FPS16.67 ms13 到 15 ms主机、PC 和移动实时渲染常见目标
90 FPS11.11 ms8.5 到 10 msVR 和高刷新交互场景常见目标
120 FPS8.33 ms6.5 到 7.5 ms需要严格控制 GPU pass 数量和 CPU 提交

预留余量的原因是帧时间包含波动。操作系统调度、驱动提交、shader 编译、资源流送、GPU clock 变化、后台任务和呈现节奏都会让单帧时间产生抖动。工程上把 60 FPS 的全部 16.67 ms 都分配给渲染工作,系统在任意一次波动时都会错过 present 时机。稳定实时渲染通常把目标帧间隔拆成可执行预算和安全余量。

贯穿帧中,城市街区镜头的目标是 60 FPS。一个可用预算可以先这样估算:CPU simulation 与可见性更新 3 ms,渲染命令构建 2 ms,GPU depth、shadow、main lighting、transparent、post processing 合计 9 ms,present 与调度余量 2 ms。这个分配的作用是建立第一版假设。真正执行后,需要用 CPU timer、GPU timestamp、frame capture 和 present 统计把假设替换成证据。

实时管线需满足的条件可以归纳成三层。第一层是单帧按时完成,CPU 和 GPU 各自的关键路径没有超过目标帧间隔。第二层是帧间节奏稳定,连续帧的交付间隔接近目标间隔。第三层是输入到显示的延迟受控,CPU 提前排队的帧数量、GPU 队列深度和显示同步策略没有把交互反馈推迟太久。

平均 FPS 在这里只作为汇总指标。假设 100 帧中有 95 帧是 12 ms,5 帧是 45 ms,平均值可能仍然看起来接近目标,但用户会感知到明显卡顿。实时约束的判断顺序应从每帧时间开始,再看 percentile、最长帧、连续超限区间和 present 间隔。这样才能区分稳定低帧率、偶发 hitch、持续 GPU 超载和呈现节奏错误。

帧率目标还决定画质策略。60 FPS 项目中,2 ms 的屏幕空间反射可能可以接受;120 FPS 项目中,同一个 pass 会消耗接近四分之一预算。VR 场景对 late pose、motion-to-photon latency 和稳定刷新更敏感,预算余量通常需要更保守。移动设备还会受到功耗和热约束影响,短时间达标不代表长时间稳定。

117.2 Frame Budget Allocation Across CPU GPU and Presentation

Frame budget allocation 的核心动作是把一帧拆成 CPU update、render submit、GPU passes、present 和 latency margin。每一项都对应不同证据:CPU update 看线程时间线和 job 等待;render submit 看 draw call、pipeline state、descriptor 或 binding 更新;GPU passes 看 timestamp 和 profiler counter;present 看 swap chain、vsync、compositor 或平台呈现统计;latency margin 看队列深度和输入到显示的路径。

下面的图把本章贯穿帧的预算路径压缩成一个可检查模型。图中节点不是引擎模块清单,而是一帧从输入到显示必须经过的时间段。

这个路径说明一个约束:预算分配需要看关键路径长度,线程耗时和 pass 耗时都要放回依赖关系中解释。CPU 线程可以并行运行,GPU pass 也可能通过 async compute 或多队列交叠一部分工作。最终决定帧是否按时的是关键路径上的最长依赖链。例如资源流送线程占用 6 ms 并不必然造成掉帧,只有当主线程等待它完成纹理上传、descriptor 更新或场景可见性结果时,它才进入当前帧关键路径。

CPU update 包含输入采样、游戏逻辑、动画、物理、可见性、粒子模拟、资源状态更新和渲染数据准备。它的预算不只看主线程耗时,还要看主线程等待哪些 job。贯穿帧中,粒子系统如果在 worker 上计算 4 ms,主线程在提交透明 pass 前等待 1.5 ms,那么进入关键路径的是这 1.5 ms 等待和相关同步成本。性能记录应把 job 自身耗时和主线程等待分开。

Render submit 是 CPU 把渲染意图转换成 API 命令和 GPU 可执行状态的阶段。它包含 pipeline state 选择、descriptor 或 bind group 更新、constant buffer 写入、resource barrier 组织、draw 或 dispatch 录制、command buffer 提交。显式 API 中,这一段的成本更容易被拆开;隐式 API 中,驱动可能把校验、状态转换和实际提交推迟到 draw 或 present 附近。不同 API 的差异需要用同平台工具确认,不能把一个 API 的提交形态写成通用结论。

GPU passes 是预算中最接近画面结果的部分。贯穿帧可以拆成 depth pre-pass、shadow map、G-buffer 或 forward lighting、透明粒子、screen space reflection、bloom、tone mapping、TAA 和 UI。每个 pass 的成本由分辨率、样本数、shader 指令、纹理采样、带宽、overdraw、render target 格式和同步关系共同决定。GPU timestamp 能给 pass 时间,frame capture 能说明资源绑定和 pipeline state,counter 能进一步说明瓶颈偏向 ALU、texture、bandwidth、occupancy 还是同步等待。

Presentation budget 连接渲染结果和显示系统。Windows DXGI flip model 的官方文档说明,flip presentation model 让 back buffer 可以交给 Desktop Window Manager 直接组合,并提供 present statistics 用于观察帧同步和 presentation glitch;这类资料支持的判断是:present 阶段属于实时约束的一部分,渲染 pass 完成时间需要与显示交付节奏一起观察。Microsoft DXGI flip model 提供的是 Windows DXGI 语境下的证据入口,Metal、Vulkan、Android 和主机平台需要对应平台的呈现指标。

Latency margin 是输入响应和队列深度的安全区。CPU 过度提前录制帧可以提高吞吐,但会增加输入到显示的延迟。GPU 保持轻微排队可以减少空泡,但队列过深会让性能波动更晚反馈到画质系统。交互型实时渲染通常需要在吞吐和延迟之间设上限:CPU ahead frame 数量、swap chain buffer count、present mode 和帧率限制器都属于这个约束面。

预算分配完成后,应形成一张可执行表。下面是贯穿帧在 60 FPS 目标下的起始预算。

时间段初始预算证据来源超限后的首要动作
CPU update3.0 ms主线程 timer、job timeline推迟非当前帧任务,压缩同步点
Render submit2.0 msAPI marker、command recording timer合并提交、缓存 pipeline 和绑定状态
GPU opaque 与 lighting4.0 msGPU timestamp、pass capture降低阴影更新、减少材质分支和纹理采样
GPU transparent 与 effects3.0 msoverdraw 视图、GPU counter限制粒子、降低半分辨率特效成本
GPU post 与 UI2.0 mspass timing、render target bandwidth降低后处理质量或分辨率
Present 与余量2.0 mspresent statistics、frame pacing 曲线调整同步策略和队列深度

这张表的价值在于把优化动作绑定到证据。opaque pass 超时不应直接降低 UI 质量;present 抖动也不应直接重写粒子 shader。预算分配把性能问题限制在可验证区域内,后续监控和降级才能按正确顺序执行。

117.3 帧时间监控与调整策略

帧时间监控要同时记录 CPU frame time、GPU frame time、percentile、budget violation、pass timing 和自动降级动作。CPU frame time 说明 CPU 端提交当前帧所需时间;GPU frame time 说明 GPU 完成当前帧命令所需时间;present interval 说明图像交给显示系统后的节奏。三者一起看,才能把掉帧归因到 CPU、GPU、呈现队列或资源压力。

监控系统的第一层是每帧采样。主线程记录 frame begin、input、simulation end、render submit end、present call 和 frame end。GPU 侧在关键 pass 前后插入 timestamp query。资源系统记录纹理上传大小、buffer 更新量、异步加载队列长度、VRAM 或统一内存压力。呈现层记录 present 返回状态、vsync 对齐、队列深度和显示间隔。

第二层是统计窗口。实时系统不应只对单帧波动作出剧烈反应。常见做法是维护短窗口和长窗口:短窗口观察最近 8 到 16 帧,用于快速发现连续超限;长窗口观察最近数百帧,用于判断趋势。percentile 指标能比平均值更好地描述稳定性。P95 或 P99 frame time 过高,表示少量慢帧已经影响体验。

第三层是 budget violation 分类。一次超限需要被标记为 CPU update、submit、GPU pass、present、streaming、shader compilation、resource pressure 或 unknown。unknown 的存在很正常,它表示证据不足。工程系统应保留 unknown 分类,并在后续 capture 或 profiler 中补证据。把所有 unknown 都归因到 GPU 会导致错误降级。

贯穿帧进入高密度街区后,监控可能得到下面的数据:CPU frame time 从 6 ms 升到 9 ms,GPU frame time 从 11 ms 升到 17.8 ms,transparent pass 从 1.2 ms 升到 4.5 ms,post pass 基本稳定,present 间隔出现 33.3 ms 的长帧。这个证据链说明主压力来自 GPU transparent pass,present 长帧是 GPU 超预算后的外部表现。此时首要动作是降低透明粒子和半透明 overdraw,CPU job 切分只作为后续复查项。

自动调整策略需要有触发条件、动作幅度、恢复条件和冷却时间。触发条件可以是连续 3 帧 GPU frame time 超过 15 ms,或 P95 超过 16.67 ms。动作幅度应逐级变化,例如动态分辨率每次降低 5%,粒子发射率每次降低 10%,阴影更新频率从每帧改为隔帧。恢复条件应比触发条件更保守,例如 GPU frame time 连续 120 帧低于 13 ms 才逐步恢复画质。冷却时间用于抑制质量频繁上下跳动。

Unity 的 Dynamic Resolution 文档提供了一个可迁移的思路:通过 frame timing 信息判断 CPU 或 GPU 性能下降,再调整 render target 的宽高缩放来维持目标帧率;它还说明动态分辨率通常通过缩放已分配 render target 的使用区域来减少 GPU 工作量。Unity Dynamic ResolutionControl scaling with Dynamic Resolution 支持的判断是:动态分辨率应由帧时间证据驱动,并且它主要作用于分辨率相关的 GPU 成本。

调整策略还要处理历史资源。Temporal anti-aliasing、screen space reflection、motion blur 和一些 denoiser 依赖前一帧历史纹理。动态分辨率改变 scale factor 时,历史纹理的采样坐标、有效区域和重投影关系会变化。Unity 文档中对动态缩放时历史 RenderTexture 数据保持的说明,提示了一个通用边界:带历史依赖的 pass 需要明确哪些 render target 随当前帧缩放,哪些历史资源保持有效并做坐标转换。

帧时间监控同时服务自动降级、回归测试和内容制作。关卡、材质、粒子、后处理和 UI 资源提交时,可以记录性能预算标签。内容进入高密度街区时,系统能指出是哪类资产让 transparent pass 或 shadow pass 超预算。这样优化动作可以回到内容约束,运行时降级只承担峰值兜底职责。

117.4 多线程与异步调度提升效率

多线程与异步调度的目标是缩短关键路径、减少等待、提高硬件利用率。它们不能改变目标帧间隔,但可以把原本串行的 CPU 工作拆到 worker,把部分 GPU compute 工作安排在图形队列空隙,把资源加载和准备移出当前帧关键路径。

CPU 多线程通常围绕 job system 组织。动画采样、骨骼矩阵计算、可见性裁剪、粒子模拟、场景更新、资源解析和 command list 构建都可以拆成 job。正确的拆分标准是依赖关系和同步位置。一个 job 即使并行运行,只要主线程每帧都在它后面等待,它仍然属于关键路径。提升效率的核心是减少等待点数量,或把等待点推迟到数据确实需要的位置。

贯穿帧中,城市街区有大量粒子和动态物体。一个稳定调度方案可以把 CPU 工作分成三条线:主线程采样输入并推进帧状态;worker 并行处理动画、粒子和可见性;渲染线程基于上一阶段已完成的数据录制 command buffer。帧间可以保留有限延迟,例如使用上一帧的静态可见性缓存或异步准备下一帧资源。这个方案用一点数据延迟换取当前帧关键路径收缩,适合视觉误差可控的子系统。

Render submit 也能并行化。Vulkan、Direct3D 12、Metal 等显式 API 更鼓励把 pass command recording 分配到多个线程,然后在主提交点合并。这里的风险是状态和资源生命周期。多线程录制需要稳定的资源引用、清晰的 descriptor 或 bind group 分配策略、可预期的 barrier 生成规则,以及调试 marker。缺少这些约束时,并行录制会把 CPU 时间从 command 构建转移到锁竞争、内存分配和资源状态修正上。

GPU 异步调度主要涉及 graphics queue、compute queue、copy queue 和平台允许的并发能力。Async compute 可以把粒子模拟、某些后处理、culling、prefix sum 或光照列表构建安排到图形 workload 的空隙中。它的收益取决于资源冲突和硬件利用率。如果 graphics pass 已经让计算单元、纹理单元和带宽同时饱和,额外 compute 会与主图形工作争用资源。此时异步只会增加同步复杂度。

Copy queue 和资源流送适合移出当前帧关键路径。纹理解码、mipmap 准备、buffer 上传、BLAS 或 TLAS 更新、shader cache 预热都应尽量在可控时机完成。资源进入可见范围之前,系统先完成加载和上传;当前帧只绑定已就绪的低级别 fallback 资源或完整资源。这样资源压力不会突然阻塞主线程或 GPU 队列。

异步调度需要配套 fence、semaphore、resource state 和生命周期管理。一个资源在 copy queue 写入后被 graphics queue 采样,必须有正确的队列所有权转移或状态转换。一个 compute pass 生成的粒子 buffer 被透明 pass 读取,也需要明确同步点。同步点过早会压缩并行空间,同步点过晚会产生数据竞争或未定义结果。调度设计应把依赖图画清楚,再把每条边转换成 API 对象。

多线程的性能收益应由时间线证明。CPU 侧看主线程等待是否下降,worker 是否有长尾 job,锁竞争是否增加。GPU 侧看异步 compute 是否与 graphics 发生真实重叠,queue idle 是否下降,barrier 是否造成新 stall。工具观察可以选择 RenderDoc、Nsight Graphics、PIX、Xcode GPU tools 或平台 profiler。工具名称本身不构成结论,时间线和 counter 才能说明调度是否缩短了关键路径。

贯穿帧的调度收敛顺序可以这样设计:先把透明粒子模拟移到 worker,并保证主线程只等待必要结果;再把粒子 GPU buffer 更新改成环形 buffer,降低当前帧写读冲突;然后把半分辨率粒子光照或后处理尝试放到 async compute;最后用 GPU timeline 验证 async compute 是否与 shadow 或 opaque pass 重叠。每一步都有证据边界,失败时能退回上一层稳定方案。

117.5 实时约束下的容错与稳定性策略

容错策略的目标是在负载超过预算时保持可接受的视觉连续性和交互响应。实时渲染无法保证每一帧的场景复杂度相同,因此系统要提前定义降级顺序。降级顺序应从视觉敏感度低、恢复成本低、状态影响小的项目开始,再进入用户容易察觉的画质项目。

Dynamic resolution 是最直接的 GPU 负载回退。它降低内部渲染分辨率,从而减少像素 shader、纹理采样、render target 带宽和部分后处理成本。它对 vertex-bound 或 CPU-bound 场景收益有限。贯穿帧中,transparent overdraw 和 post processing 造成 GPU 超时,动态分辨率能快速压缩像素成本;如果超时来自 CPU 可见性或资源解码,动态分辨率只能改善局部 pass。

Quality scaling 应按 pass 设计。阴影可以降低分辨率、减少级联数量、降低更新频率或缩短投射距离;屏幕空间反射可以降低 ray step、半分辨率运行或切换到反射探针;SSAO 可以降低采样数或分辨率;体积雾可以降低 froxel 分辨率;粒子可以限制发射率、寿命、最大实例数和透明排序范围。每个动作都需要绑定它影响的资源路径:像素成本、带宽、采样次数、draw count 或同步。

LOD 策略解决几何、材质和动画复杂度。几何 LOD 控制 vertex count 和 overdraw,材质 LOD 控制 shader 分支和纹理采样,动画 LOD 控制骨骼数量、更新频率和 blend 复杂度,光照 LOD 控制光源数量、阴影投射和反射更新。实时系统应把 LOD 选择接入预算,并把距离、屏幕占比、运动速度和帧时间放进同一判断。高密度街区中,如果相机运动速度高,远处立面材质细节和小型动态物体动画可以更积极降级。

Effect disable 是更强的回退动作。它适合处理峰值压力、低端设备或资源异常。关闭 motion blur、降低 bloom、冻结反射更新、关闭部分透明装饰、禁用远处粒子,都能快速收敛预算。这个策略需要定义视觉优先级。玩家交互相关的目标轮廓、UI、命中特效和关键光源通常优先保留;背景装饰、远处小粒子和二级屏幕空间效果优先降级。

Resource pressure fallback 处理内存、带宽和加载压力。纹理流送可以先使用较低 mip,mesh streaming 可以使用代理网格,材质可以使用简化纹理集,shader variant 可以提前预热或使用稳定 fallback variant。资源系统在压力高时应停止把新高精资源推入当前帧关键路径,而是先保证已可见对象拥有稳定占位资源。这样画面可能短时间变粗,但帧时间不会因为同步加载和上传发生长帧。

容错系统需要防抖。降级和恢复如果使用同一阈值,画质会在临界负载附近频繁跳动。稳定做法是使用两个阈值:超限阈值触发降级,恢复阈值要求更低负载并持续一段时间。例如 GPU P95 超过 16.67 ms 触发一级降级,GPU P95 连续 3 秒低于 13 ms 才恢复一级。恢复顺序也应反向执行,先恢复用户更敏感的效果,再恢复远处和装饰性效果。

实时容错还要保留可解释记录。每次降级应记录触发指标、动作、影响资源和恢复时间。一次长帧后,工程师可以看到系统在第 2450 帧因 transparent pass 超过预算而把粒子发射率降到 70%,随后因 GPU P95 恢复而逐级回升。这个记录能区分内容波动、设备压力、资源异常和算法问题。

贯穿帧的最终稳定方案可以这样收束:基础预算锁定 60 FPS;监控发现 transparent pass 和 post pass 是峰值压力;系统先降低动态分辨率到 0.9,再限制远处粒子数量,并把屏幕空间反射切换为半分辨率;如果资源流送队列超过阈值,远处建筑先使用低 mip 和代理材质;恢复时先回升反射质量,再恢复粒子,最后回升分辨率。这个顺序把用户感知、GPU 成本、资源状态和恢复稳定性放进同一套约束。

最小自检任务

给定一个 60 FPS 实时渲染场景,目标帧间隔为 16.67 ms。连续 180 帧监控结果如下:CPU frame time 平均 7 ms,P95 为 9 ms;GPU frame time 平均 14 ms,P95 为 19 ms;transparent pass 从 1.5 ms 上升到 5 ms;post processing 保持 2 ms;present 间隔中每隔几十帧出现一次 33.3 ms;资源流送队列稳定,没有同步加载记录。请判断主要瓶颈位置,并设计一套两级自动降级和恢复策略。

答案要点

主要瓶颈应定位在 GPU transparent pass。CPU P95 仍低于 16.67 ms,资源流送没有同步加载证据,post processing 稳定,present 的 33.3 ms 长间隔更像 GPU 超预算后错过显示时机的结果。判断顺序是先看 CPU 与 GPU frame time,再看 pass timing,最后把 present 长帧放回 GPU 是否按时完成的证据链中。

一级降级可以把透明粒子发射率或最大实例数降低 10% 到 20%,并把远处透明效果切到半分辨率或更低排序精度。触发条件可以设为 GPU P95 连续 30 帧超过 16.67 ms,或者 transparent pass 连续 30 帧超过 3 ms。二级降级可以同时启用动态分辨率 0.9,并降低透明材质采样或光照分支复杂度。触发条件可以设为 GPU P95 连续 60 帧超过 18 ms。

恢复策略应使用更低阈值和冷却时间。GPU P95 连续 180 帧低于 13.5 ms 时先恢复动态分辨率,再逐步恢复透明粒子数量;每次恢复后至少等待 60 帧重新观察。恢复顺序要保留画面稳定,防止分辨率、粒子和反射质量在临界负载附近频繁变化。

本章知识点总结

  • 帧预算:实时渲染应把目标 FPS 转换成毫秒预算,再为调度、present 和波动保留余量。
  • 帧时间优先:性能判断应优先观察每帧耗时、percentile 和长帧分布,平均 FPS 只能作为汇总指标。
  • 预算分配:CPU update、render submit、GPU passes、present 和 latency margin 应分别绑定证据来源和超限动作。
  • 关键路径:一帧是否按时完成取决于依赖链上的最长路径,线程总耗时和 pass 总耗时需要结合等待关系解释。
  • GPU证据:GPU timestamp、frame capture 和 profiler counter 能把超预算定位到具体 pass、资源路径和执行瓶颈。
  • 呈现约束:present 阶段决定图像到屏幕的节奏,渲染 pass 完成时间与显示节奏需要一起观察。
  • 监控闭环:帧时间监控应记录 CPU、GPU、present、资源压力和降级动作,形成可回放的证据链。
  • 动态分辨率:动态分辨率主要压缩分辨率相关的 GPU 成本,对 CPU-bound 或资源同步问题收益有限。
  • 异步调度:多线程和 async compute 的收益来自关键路径缩短和真实重叠,需要用时间线和 counter 验证。
  • 资源回退:资源压力下应优先使用低 mip、代理网格、简化材质和稳定 fallback,减少当前帧同步等待。
  • 质量降级:质量回退应按视觉优先级、资源路径和恢复成本排序,先处理用户感知较低且成本收益明确的项目。
  • 恢复防抖:降级和恢复应使用不同阈值、持续窗口和冷却时间,保证画质不会在临界负载附近频繁跳变。