导语:验收前一周,「回响峡谷」在 RTX 3080 / 4K 下从 60fps 掉到 45。我认定是第 17 章刚上线的 PCG 森林把 Draw Call 打爆了,砍了三天树,Draw Call 从 3800 压到 2100,帧率从 45 涨到 45.3。第四天早上打开 Unreal Insights,时间轴上最刺眼的罪魁祸首是一条 220ms 着色器编译尖峰——每次开门必现,渲染反而安安静静。三天的树白砍了,PCG 规则还得回滚。这种学费每个性能工程师都交过,区别只在于交一次还是按年续费。
本章地图
mindmap
root((第18章 打磨与优化))
第一戒律
先测量 别猜
三个动手前必答的问题
Amdahl 劝退线
Insights 三视图
CPU 时间轴
GPU 各 Pass
内存曲线
健康帧对照病态帧
瓶颈画像四连
Draw Call 爆炸
着色器编译卡顿
物理过度模拟
动画线程拥堵
预算制度
16.6ms 工资表
内存分饼
收益乘工作量取舍
平台分档
主机 高端PC 入门PC
自动降级防抖动
达标实战
22ms 到 15.9ms 战报18.1 直觉在性能问题上的命中率,不如抛硬币
先把那次翻车复盘完。当时我看到的现象是"45fps",脑子里立刻跳出的解释是"树太多"。这个因果链看起来天衣无缝:第 17 章的 PCG 管线刚铺完峡谷南侧的森林,同一段时间帧率掉了,第一嫌疑人舍它其谁?
然而凶手另有其人。45fps 是平均帧率,而平均帧率会说谎。22ms 的稳态帧是 45fps,18ms 稳态加每十几秒一条 220ms 尖峰也是 45fps。这两种病吃完全不同的药:前者要砍渲染,后者只需要把着色器编译从玩家必经之路上挪走。我在第一种诊断下干了三天,病却在第二种里。
先明确一个世界观再谈规则:帧率是产品指标。技术指标只是它的影子——玩家说不出什么叫 Named Event,但手感的黏滞、开门的停顿、战斗高潮的那一下幻灯片,他全都记得住。性能工作修复的,是玩家对游戏的信任。数字只是便于追踪的替身。
这件事的教训值三条规则。
一、平均帧率只能帮你挂个号。确诊得看帧时间曲线和直方图。 先分清稳态超标还是尖峰超标,再谈修什么。
二、你写的代码很少是你以为的那种慢法。 算法课教的"贵"是循环和递归,游戏帧时间的"贵"是缓存未命中、线程互相等待、驱动状态切换、每帧几百次小内存分配。盯着代码行数猜瓶颈,命中率不如抛硬币。
三、没抓数据就改代码,等于没看日志就回滚数据库。抓一次 30 秒的 Insights 只要一杯咖啡的时间,比三天返工便宜三个数量级。
动手之前,先用 stat unit 回答三个问题:瓶颈在 CPU 还是 GPU(Game 高是逻辑侧,GPU 贴着 Frame 是渲染侧)?稳态还是尖峰?超标部分占帧时间的比例是多少?第三个问题决定了值不值得修。而判断"值不值得"有一把数学尺子——平均帧率最多就是个分诊台,诊断书要靠曲线和直方图来写。
Amdahl 定律:优化收益的天花板
设被优化部分占帧时间的比例为 ,你把它加速了 倍,整体加速比 为:
哪怕你把这部分优化到零耗时(),整体收益也收敛于 。
代入数字感受一下。背包 UI 重绘占帧时间 5%(),收益天花板是 ,也就是 5.3%。22ms 的帧,把它删干净了也只能到 20.9ms,离 16.6ms 的目标还差 4.3ms。同一段时间拿去砍占帧时间 40% 的渲染,哪怕只砍一半,省下 4.4ms,直接达标。同样是三天工作量,收益差出八倍。
所以预算纪律里有一条劝退线:占比不足 10%、又不是尖峰元凶的系统,默认不碰。Amdahl 定律不反对优化,它反对的是不看占比的优化。
再补一刀机会成本:工程师一周的时间同样是团队预算里的一格工资,花在 5% 收益的系统上,等于主动放弃了另一个 40% 收益系统的修复窗口。优化排期和资源排期,从来都是同一张表。
整个工作流是一张循环图。一次性冲刺?醒醒,那连热身都算不上:
flowchart TD
A["测量:stat unit + Insights 抓取"] --> B["定位:哪条线程、哪个系统、稳态还是尖峰"]
B --> C["假设:写下一个可证伪的假设"]
C --> D["最小修复:一次只改一件事"]
D --> E["复测:同路线同场景,三次取中位数"]
E --> F{达标?}
F -- 否 --> A
F -- 是 --> G["写入回归:CI 性能门禁 + 更新预算表"]
D -.->|改了还慢,假设错了| B注意两个细节。一次只改一件事,改三件就说不清哪件起作用;"复测三次取中位数"是因为机器状态会飘,单次数据没有法律效力。
测量环境同样有纪律:固定硬件、固定跑图路线、固定画质档,三者缺一个,两次数据就没有可比性。关掉 Steam 和 Discord 的覆盖层,它们会往帧里注入自己的开销;先热机两分钟再测稳态,冷启动数据里全是编译与加载的噪声;笔记本务必插电源,电池模式的功耗墙会让 CPU 降频,测出来的瓶颈是假瓶颈。
18.2 Unreal Insights:CPU、GPU、内存各看哪里
stat unit 是体温计,Unreal Insights 是核磁共振。前者花两秒告诉你烧不烧,后者花五分钟告诉你病灶在哪。抓取流程:游戏内控制台执行 trace.start cpu,gpu,frame,bookmark,玩 30 秒有代表性的场景,trace.stop 得到一个 .utrace 文件,用引擎目录下的 UnrealInsights.exe 打开。内存追踪开销大,单独跑一轮 trace=memory,别和帧时间测量混在同一轮互相污染。
还有两个老手习惯:尽量在打包版上抓,编辑器本身要吃掉 2~3ms,会扭曲判断;命令行加 -statnamedevents,Named Events 才够细。
整条数据链路是这样走的:
sequenceDiagram
participant P as 游戏进程(打包版)
participant T as Trace 服务
participant D as 磁盘 .utrace
participant I as UnrealInsights
P->>T: trace.start 后持续推送事件流
T->>D: 异步落盘为 .utrace 会话文件
P->>T: trace.stop 收尾并刷新缓冲
I->>D: 打开会话文件并建索引
Note over P,T: 事件流低开销异步传输
Note over P,T: 抓取本身对帧时间影响约 2%抓取开销在 2% 量级,不至于把病灶压没。这一点比不少第三方采样器强,那些工具自己常常就是最大的噪声源。
30 秒速查面板
抓 Insights 之前,这组控制台命令能完成 80% 的分诊,建议背下来:
stat fps ; 帧率与帧时间
stat unit ; Game / Draw / GPU / Frame 四行,分诊第一步
stat unitgraph ; 帧时间历史曲线,看稳态还是尖峰
stat scenerendering ; draw call、三角形、可见实例数
profilegpu ; GPU 各 Pass 一次性快照
stat anim ; 动画更新耗时
stat physics ; Chaos 解算耗时
stat memory ; 内存总量与流送池
stat streaming ; 纹理流送状态
obj list class=Texture2D ; 谁吃了纹理内存
r.ScreenPercentage 75 ; 快速验证分辨率缩放收益
t.MaxFPS 60 ; 锁帧做对照实验
trace.start cpu,gpu,frame,bookmark / trace.stopCPU 时间轴:三条线程的接力赛
Timing 面板的主体是 Game、Render、RHI 三条线程泳道,外加每帧的标记线。健康帧像一场交接顺畅的接力:Game 在 6.8ms 交棒,Render 在 12.5ms 交棒,GPU 在 15.9ms 冲线,谁都不贴 16.6ms 的红线。
gantt
title 一帧的 Insights CPU 时间轴(健康帧,16.6ms 预算内)
dateFormat HH:mm:ss.SSS
axisFormat %S.%L
section Game 线程
逻辑与物理 Tick :00:00:00.000, 00:00:04.000
动画更新 :00:00:04.000, 00:00:05.500
提交渲染命令 :00:00:05.500, 00:00:06.800
section Render 线程
场景遍历与 Draw 提交 :00:00:06.800, 00:00:12.500
section RHI 线程
命令转译并提交 GPU :00:00:12.500, 00:00:14.200
section GPU
本帧渲染执行 :00:00:08.000, 00:00:15.900病态帧有三种面相,对应三种病:
- 一根独苗:某个 Named Event 长条独占,比如
FAnimInstance更新一根 5ms 的条——单系统失控,去 18.3 查对应画像。 - 密集锯齿:几十根碎条积少成多,通常是每帧小分配加 GC,或者大量 Actor 各自 Tick。
- 线程空白:Game 早早干完,Render 干等,或者 GPU 空转等 CPU 喂——等锁、等流送、等同步,瓶颈在"等"而不在"算"。
| 指标 | 健康帧 | 病态帧 |
|---|---|---|
| Game 线程 | 6~8ms,各系统分段均匀 | 单条超过 4ms,或整体贴满 16.6ms |
| Render / RHI | 与 Game 交错接力,无大片空白 | 长空白(等 Game 喂数据或等 GPU 帧同步) |
| 帧标记线 | 间距均匀,约 16.6ms 一格 | 间距抖动,偶发孤立长帧 |
| Named Events | 最长条 < 1.5ms | 一根条吃掉整个系统的预算 |
下钻有两个手法能省一半时间。框选一段帧范围,Insights 会给出范围内每个 Named Event 的 Count、Total、Median、Max——对账预算用 Median,抓尖峰用 Max,别拿 Max 去填预算表,那是拿极端值定常态工资。另一个是 bookmark 通道:在代码里埋自定义书签(战斗开始、进入教堂、开门),时间轴上会出现对应的标记线,爆发帧与普通帧分开统计,不用肉眼大海捞针。
GPU 视图:按 Pass 对账
GPU 时间轴把一帧拆成 ShadowDepths、BasePass、Lumen、Translucency、PostProcessing 这些 Pass,每个 Pass 一根条。「回响峡谷」4K/60fps 目标的 GPU 预算是 9ms,健康分布大致是:BasePass 3.0ms、Lumen 2.5ms、ShadowDepths 1.2ms、半透明 0.6ms、后处理 1.4ms,加起来留一点零头。
| Pass | 健康帧 | 病态帧的典型长相 |
|---|---|---|
| ShadowDepths | 1.2ms | 5.8ms:远距植被全部投阴影,VSM 页表爆炸 |
| BasePass | 3.0ms | 6ms+:材质指令数失控或三角形密度过高 |
| Lumen | 2.5ms | 6.8ms:4K 下开硬件光追,追踪成本随分辨率平方涨 |
| Translucency | 0.6ms | 3.9ms:雨夜特效 Overdraw 爆炸(见第 13 章) |
| PostProcessing | 1.4ms | 3ms+:胶片颗粒、晕影全开还叠了自定义 Pass |
一个读法提醒:GPU 时间轴和 CPU 提交之间存在错位,GPU 忙不等于 CPU 可以加码。看到 GPU 贴满,处方是减 GPU 的活——把逻辑挪到异步线程无非是换了一条路继续堵。
GPU 超标先做 30 秒的分辨率缩放实验:r.ScreenPercentage 50 把内部分辨率砍半,帧时间如果几乎同比大降,病灶在像素侧——填充率、Lumen、半透明、后处理;如果几乎不动,病灶在几何与提交侧——三角形密度、Draw 数量、场景遍历。一条命令把 GPU 瓶颈分成两大门派,再决定翻哪本处方。
内存视图:曲线的形状比数值重要
Memory Insights 的主视图是内存随时间的曲线,配合 LLM(Low Level Memory)标签可以看到每一块内存归哪个系统。健康曲线的形状:阶梯式上升后进入平台期,GC 造成的锯齿规律出现,流送进出平衡——进峡谷升 200MB,出峡谷降回 190MB 左右。
| 曲线形状 | 健康 / 病态 | 含义 |
|---|---|---|
| 阶梯上升后平稳 | 健康 | 流送正常加载并驻留 |
| 单调爬升不回头 | 病态 | 泄漏或流送只进不出,迟早撞墙 |
| 锯齿又密又深 | 病态 | 每帧大量临时分配,GC 频繁,周期卡顿 |
| 直角尖峰 | 病态 | 大块资源走了同步加载,伴随帧尖峰 |
内存问题和帧时间问题会互相伪装。显存接近打满时,驱动会把资源往系统内存倒,GPU 时间莫名其妙翻倍。帧时间查不出原因的时候,先看内存曲线是不是已经贴着上限。
自带探针:让系统学会自我指认
引擎自带的 Named Events 覆盖不到你的游戏逻辑。在可疑系统里埋探针,Insights 时间轴上就会出现属于你的一根条,排查时不用猜:
DECLARE_STATS_GROUP(TEXT("ECInventory"), STATGROUP_ECInventory, STATCAT_Advanced);
void UECInventoryComponent::RebuildItemGrid()
{
// 进 Insights 时间轴(Named Event),也进 stat 组
TRACE_CPUPROFILER_EVENT_SCOPE(EC_RebuildItemGrid);
SCOPE_CYCLE_COUNTER(STAT_EC_RebuildGrid);
for (FItemSlot& Slot : Slots)
{
TRACE_CPUPROFILER_EVENT_SCOPE(EC_SortSlot);
Slot.Resort();
}
}两个宏的分工:TRACE_CPUPROFILER_EVENT_SCOPE 进 Trace/Insights 通道,SCOPE_CYCLE_COUNTER 进传统 stat 统计。5.8 中对应官方文档的 Trace 与 CpuProfiler 词条,宏的具体变体以文档为准。埋点本身有微秒级开销,发布版本会被自动剔除,可以放心留在代码里。
18.3 四种常见瓶颈的画像:症状、工具、处方
分诊有了,接下来是专科门诊。下面四张画像覆盖了「回响峡谷」开发期 80% 的性能工单,每张按症状、工具、处方开药。这张决策树是入口:
flowchart TD
S["stat unit 四行"] --> Q1{Frame 贴着谁?}
Q1 -->|贴 GPU| G["profilegpu 看各 Pass"]
Q1 -->|贴 Game| C["Insights CPU 时间轴下钻"]
Q1 -->|贴 Draw| D["stat scenerendering 看 draw 数"]
G --> G1{哪个 Pass 超支?}
G1 -->|ShadowDepths| P1["阴影:砍远距投送,调 VSM"]
G1 -->|Lumen| P2["Lumen:软追踪或降质量"]
G1 -->|Translucency| P3["Overdraw:压特效层级"]
G1 -->|BasePass| P4["材质指令数或三角形密度"]
C --> C1{最长的 Named Event}
C1 -->|动画| P5["URO 降频 + BudgetAllocator"]
C1 -->|物理| P6["休眠 + 降迭代 + 池化"]
C1 -->|孤立尖峰| P7["PSO 编译:预热清单"]
D --> P8["合批、实例化、HLOD、剔除"]画像一:Draw Call 爆炸
症状:stat unit 里 Draw 线程 12ms 以上,GPU 却有富余——典型的"CPU 喂不饱 GPU"。stat scenerendering 显示 Mesh draw commands 三千起步。
工具:stat scenerendering 看数量级;FreezeRendering 冻结视锥后转相机,排查"看不见却在画"的漏网之鱼;Insights 里 Render 线程的遍历段长度。
处方:同类小物件合并成 ISM/HISM 实例化组件,一片废墟从 400 次 draw 变 1 次;静态网格能开 Nanite 的开 Nanite,让簇管线接管(边界见第 7 章);远景走 HLOD;给小物件设合理的剔除距离。主流场景把 draw commands 压到 2000 以内,数量级因项目而异,但"Render 线程 < 4ms"这条线是硬的。实测对照:「回响峡谷」教堂废墟 412 个碎石块 Actor 改成 HISM,同机位 draw commands 从 2600 掉到 1180,Render 线程从 6.1ms 降到 2.9ms——这是本章所有修复里性价比最高的一次,工作量只有半天。
画像二:着色器编译卡顿
症状:首次进入新区域、某个特效第一次出现、第一次开门——一条 100~300ms 的孤立尖峰,第二次路过不再现。打包版比编辑器明显,因为编辑器后台编译一直在偷偷帮你热身。
工具:Insights 帧时间轴上的孤立尖峰形态是招牌;输出日志里 LogShaderCompilers 的编译记录对得上时间点;PSO 相关统计确认 miss 数。
处方:PSO 预缓存——打包时收集管线状态缓存,加载界面或流送间隙里预热,把编译从"玩家看到的那一刻"挪到"玩家没看到的时候"。更根本的一手是收敛材质变体:开关排列组合爆炸才是 PSO 数量失控的根源——10 个二态静态开关全排列就是 1024 个变体,每多加一个开关变体数翻倍,材质评审时先问一句"这个开关真的需要运行时切换吗"。异步编译相关 CVar 保持默认开启。5.8 中具体机制以官方文档 Pipeline State Cache 词条为准。
画像三:物理过度模拟
症状:stat physics 里 Chaos 解算 3ms 以上;场面越乱帧率越低;布料、碎块、载具一多,Game 线程跟着抖。
工具:stat physics;p.Chaos.DebugDraw 系列可视化当前活跃的刚体;Insights 物理通道的 Named Events。
处方:离屏和远距刚体强制休眠,睡着的物体不解算;布料只在镜头视锥内模拟;破坏碎块池化并设上限,超出就回收最老的;解算迭代次数按平台降档;能烘焙成动画的物理演出就别实时解。原则一句话:玩家看不到的物理,一帧都不该算。「回响峡谷」教堂穹顶崩塌的演出原先实时解算 60 块碎块,爆发帧物理段 4.7ms;改成关键帧烘焙的破坏动画后降到 0.2ms,观感差别要逐帧截图才找得到。实时物理的尊严,不该由帧率买单。
画像四:动画线程拥堵
症状:stat anim 居高不下;12 个 NPC 在场时 Game 线程动画段 5ms 以上,而主角自己只占 0.5ms——群众演员比主角还贵。
工具:stat anim;Insights 里 FAnimInstance 相关长条;Animation Budget Allocator 插件的统计面板。
处方:URO(Update Rate Optimization)按距离降频,30 米外的 NPC 跑 15Hz 人眼分辨不出;AnimBudgetAllocator 把动画更新按帧均摊,削平集体同步更新造成的浪涌;动画多线程评估保持开启;远处的群众演员从 Motion Matching 退回简单状态机,MM 的搜索成本只值得花在镜头焦点上(成本账见第 12 章)。降频的感知边界要自己标定:15Hz 在 30 米外无感,挪到 5 米内就露馅——脚步滑移和节奏迟滞会被核心玩家一眼看穿。URO 的距离分档要配合动画 LOD 一起调,贴脸的主角和重要 NPC 永远跑满帧率。
四张画像的速查表:
| 画像 | 一句话症状 | 第一工具 | 第一处方 |
|---|---|---|---|
| Draw Call 爆炸 | Draw 线程高、GPU 闲 | stat scenerendering | 实例化合批 + Nanite + HLOD |
| 着色器编译卡顿 | 首次出现的孤立尖峰 | Insights 尖峰形态 | PSO 预热 + 收敛变体 |
| 物理过度模拟 | 场面一乱就掉帧 | stat physics | 休眠 + 池化 + 降迭代 |
| 动画线程拥堵 | NPC 越多 Game 越慢 | stat anim | URO 降频 + 预算均摊 |
18.4 预算制度:给每个系统发"工资"
帧时间的目标是"不许超"。"尽量快"——这句话骗过多少团队。前者的系统各自为战,每个人都说自己"已经很快了",加起来照样超标;后者把 16.6ms 当成月度工资总额,每个系统月初领固定额度,花超了要写检讨。没有预算表的优化,等于没有秤的减肥。
「回响峡谷」的 16.6ms 工资表
验收目标 4K/60fps(RTX 3080 档硬件),总额 16.6ms,分配如下:
| 系统 | 预算 (ms) | 健康实测 (ms) | 明细与说明 |
|---|---|---|---|
| GPU 渲染总包 | 9.0 | 8.7 | BasePass 3.0 / Lumen 2.5 / VSM 阴影 1.5 / 半透明 0.6 / 后处理 1.4 |
| Game 逻辑(含物理) | 4.0 | 3.6 | 物理同步占其中 0.8 |
| 动画 | 1.5 | 1.4 | 主角 0.5,NPC 按预算器均摊 |
| 音频 | 0.4 | 0.3 | MetaSounds 图与衰减计算 |
| 流送摊销 | 0.5 | 0.4 | World Partition 格子进出(见第 16 章) |
| 尖峰缓冲余量 | 1.2 | — | 战斗爆发帧专用,平时冻结 |
| 合计 | 16.6 | 14.4 + 余量 | 60fps 达标线 |
用公式说,预算制度就是一条不等式:
每个 是系统领到的工资, 是任何人都不许动的战备金。优化工作的全部内容,就是让每个系统的实测值待在它自己的 里面——超了,要么减系统的活,要么开预算评审会重新分账,没有第三条路。
顺带回答一个评审会上必问的问题:为什么咬死 60fps,把 40 判了死刑?这背后是产品决策,技术偏好没有发言权。「回响峡谷」是第三人称动作游戏,60fps 下输入到画面的延迟比 30fps 低约 17ms,翻滚与格挡的手感差异,核心玩家一摸便知。换成回合制策略游戏,30fps 换更重的画质完全成立——预算表永远从产品定义倒推,不从硬件能力正推。
预算制度的三条执行规则:
- 预算是合同,建议值没有法律效力。 CI 每晚自动跑固定路线抓取数据,任何系统连续三次超支 5%,性能门禁直接打回提交。
- 超支先问"谁最近涨薪了"。 对照上周曲线找增量,新增内容默认要吃掉余量,吃超了就说明预算表该重审,重新分账是唯一的出路。
- 余量神圣不可挪用。 1.2ms 的缓冲是给"12 个 NPC 加特效全开"这种爆发帧准备的。平时把它分掉,爆发帧必然破 16.6ms,而玩家记住的永远是卡的那一下。
预算表本身是逆向工程出来的:先测一批健康系统的实测分布,取 P95 加 15% 作为初版额度,跑两个月再按实测收紧。新系统立项要先申请预算再动工——动画组想给 NPC 加全身 IK,先回答"预算从谁的额度里出",答不上来就回去改方案。这条流程听着官僚,实际省的是上线前那种全员救火。
内存也要发工资
帧时间有工资表,内存也得有。「回响峡谷」的入门档目标硬件可用内存约 8GB,按 LLM 标签切饼:
饼图里最需要解释的是那块"未分配余量"。它本质上只有一个身份:流送系统的安全气囊。玩家冲刺穿过三个格子时,加载峰值会瞬时超出稳态占用 1GB 以上。把这块分光,等于把安全气囊拆了换真皮座椅。
对账的日常动作是 stat LLM(需要细目时换 stat LLMFULL 看全标签):纹理池超了,先查纹理的 LOD Bias 与虚拟纹理流送池上限;几何超了,查 Nanite 簇缓存和关卡实例是否重复驻留。每周例会把六个标签的实测值贴进预算表,曲线连续三周上涨的标签,自动成为下一次预算评审的对象。
超支了砍谁:收益乘工作量矩阵
预算评审会上的每个候选项,都丢进这张矩阵里过一遍。横轴是工作量,纵轴是收益:
quadrantChart
title "优化项取舍矩阵"
x-axis "工作量小" --> "工作量大"
y-axis "收益低" --> "收益高"
quadrant-1 "立项攻坚"
quadrant-2 "立即动手"
quadrant-3 "顺手做"
quadrant-4 "放弃(Amdahl 劝退区)"
"Lumen 软追踪切换": [0.3, 0.9]
"NPC 动画 URO": [0.4, 0.75]
"植被远距阴影关闭": [0.2, 0.6]
"PSO 预热清单": [0.5, 0.8]
"重写背包 UI": [0.8, 0.15]
"自研内存分配器": [0.9, 0.5]左上象限的活不要犹豫,右下象限的活不要接。真正难的是右上:收益高但工作量大,比如重写流送策略——这种要单独立项排期,别指望顺手做完。
18.5 平台分档:一份代码跑三档硬件
「回响峡谷」要同时伺候三档硬件,它们的预算表根本不是一个量级:
| 档位 | 代表硬件 | 目标 | 关键配置 |
|---|---|---|---|
| 主机档 | PS5 / XSX | 动态 1440p→4K / 60fps | 动态分辨率兜底,Lumen 软追踪,VSM 中档 |
| 高端 PC | RTX 4070 及以上 | 原生 4K / 60fps | Epic 全高,Lumen 硬件光追 |
| 入门 PC | GTX 1660 / RX 6500 | 1080p / 60fps(允许 30 档) | 屏幕比例 66%,阴影与特效全 Low,GI 走烘焙 + SSGI |
入门档有一笔诚实的成本要交代:关掉 Lumen 后没有免费的动态 GI 替补,要么回退到烘焙光照加 SSGI,要么接受观感降级。「回响峡谷」选了前者,代价是光照要构建两套数据,关卡团队的烘焙时间翻倍。预算不够的时候,选项从来都不是"全都要"。
机制上,分档靠三样东西协作:Scalability 组(sg.ViewDistanceQuality、sg.ShadowQuality、sg.GlobalIlluminationQuality 等 0~3 档)管通用画质;GameUserSettings 负责持久化玩家的手动选择;DeviceProfile 给主机这种固定硬件写死配置。首次启动跑一次硬件基准测试定档,之后交还给玩家。
主机档还有一层外部约束:平台认证(索尼 TRC、微软 XR 系列规范)把帧率稳定性列为硬性条款,长时间跌破目标帧率的版本会被打回——帧率不合格直接导致上架被拒,这已经超出了体验讨论的范畴。主机配置用 DeviceProfile 按平台固化在配置文件的对应分段里,一份资产出三份画质。另外动态分辨率别只给主机开:入门 PC 档同样值得打开,玩家对分辨率轻微浮动的容忍度远高于对卡顿的容忍度——糊一点是风格,卡一下是事故。
自动降级:降得果断,升得克制
定档只是入场券。同一张显卡,同一场景,战斗爆发时照样可能破帧。运行时需要一个自动降级器,盯着滚动平均帧时间做决策:
stateDiagram-v2
[*] --> 基准测试: 首次启动
基准测试 --> 定档: 硬件评分映射档位
定档 --> 稳态运行: 应用 Scalability 档
稳态运行 --> 降档: 连续 240 帧超标
降档 --> 稳态运行: 降一档并清零计数
稳态运行 --> 升档试探: 连续 1800 帧富余超 25%
升档试探 --> 稳态运行: 升一档观察
升档试探 --> 降档: 试探失败立即回退
note right of 升档试探
升档阈值远高于降档阈值
且只在非战斗场景试探,防止来回抖动
end note核心原则是降档与升档不对称:240 帧(约 4 秒)超标就降,1800 帧(30 秒)富余才升,而且升档只在安全场景试探。玩家可以接受画质慢慢变好,不能接受打 boss 时画质反复横跳。实现骨架:
void AECPerfDirector::ApplyTier(EPerfTier Tier)
{
UGameUserSettings* S = UGameUserSettings::Get();
switch (Tier)
{
case EPerfTier::Console: // 主机:动态分辨率兜底 + 软追踪
S->SetOverallScalabilityLevel(2);
SetCVar(TEXT("r.DynamicRes.OperationMode"), 1);
SetCVar(TEXT("r.Lumen.HardwareRayTracing"), 0);
break;
case EPerfTier::HighPC: // 高端 PC:原生 4K + 硬件光追
S->SetOverallScalabilityLevel(3);
SetCVar(TEXT("r.Lumen.HardwareRayTracing"), 1);
break;
case EPerfTier::EntryPC: // 入门 PC:缩放渲染 + 全 Low
S->SetOverallScalabilityLevel(1);
SetCVar(TEXT("r.ScreenPercentage"), 66);
SetCVar(TEXT("r.Shadow.Virtual.Enable"), 0);
break;
}
S->ApplySettings(false);
}SetCVar 是三行的小封装:IConsoleManager::Get().FindConsoleVariable(...)->Set(...)。帧时间监控用 FApp::GetDeltaTime() 做环形缓冲滚动平均,配合上面状态机的两套阈值。玩家手动改过画质后,自动降级器只降不升——用户的选择永远优先于算法。
分档做完不算完,每档都要走一遍验收:固定路线跑图取帧时间中位数,同机位截图进画质评审。降级的目标是在预算内保住美术总监划的三条底线:光影氛围、角色特写、特效可读性。底线之外,都可以谈。入门档最容易被当成"能跑就行"的垃圾桶——阴影全关、材质糊成一片的版本确实能跑,但那已经越过了降级的底线,进入了放弃治疗的领域。
18.6 战报:「回响峡谷」从 22ms 到 15.9ms
卷二收尾,把前五节的流程完整跑一遍。以下记录取自验收冲刺的真实工作日志,数字做了归整,过程原样保留。
D0 09:40 · 超标确认。 验收环境:RTX 3080 / 4K / Epic 预设,固定跑图路线(营地→峡谷→教堂)。实测 22.0ms,约 45fps,目标 16.6ms。stat unit:Game 16.2 / Draw 11.4 / GPU 21.4。GPU 是主犯,Game 也超标,两边都有活要干。
D0 11:00 · 数据捕获。 打包版抓三轮 Insights(帧时间两轮、内存一轮)。时间轴供词三条:GPU 侧 Lumen 6.8ms——硬件光追在 4K 下的追踪成本失控;ShadowDepths 3.2ms——PCG 森林的远距植被全部在投阴影;Game 侧动画段 5.1ms——教堂场景 12 个 NPC 全员跑 Motion Matching。另有进门瞬间 220ms 的 PSO 编译尖峰,老熟人。
D0 18:00 · 基线归档。 未做任何修改的原始数据入库:.utrace 文件、stat 截图、机器配置、驱动版本。后续所有对比都以这份基线为锚——没有基线的"快了 6ms"证明不了任何事,也许只是机器那天心情好。
D0 14:00 · 假设与方案评审。 四条修复全部来自左上象限:Lumen 切软追踪并降质量档;远距植被关闭阴影投送并下调 VSM 预算;NPC 动画上 URO 加预算器;PSO 预热清单补收集。背包 UI 重绘 0.4ms 一项,按 Amdahl 劝退线标记"不修",写入预算表备注。
D1 全天 · 最小修复。 一次改一件,改完当场复测:软追踪切换后 Lumen 从 6.8ms 降到 4.2ms,画面差异需要放大截图才看得出;植被阴影处理后 ShadowDepths 降到 1.8ms,30 米外没人看得出树影少了;NPC 动画 URO 15Hz 加 BudgetAllocator 均摊,Game 线程动画段从 5.1ms 压到 1.6ms;PSO 预热后进门尖峰从 220ms 压到 18ms,埋在流送间隙里,玩家无感。
D2 10:00 · 验证。 同一路线三轮抓取取中位数:帧时间 15.9ms,稳定 60fps。GPU 实测 8.7ms,贴住 9.0ms 预算;1.2ms 余量完整未动。修复前后对账:
| 项目 | 修复前 | 修复后 | 手段 |
|---|---|---|---|
| 帧时间(中位数) | 22.0ms / 45fps | 15.9ms / 60fps | — |
| Lumen | 6.8ms | 4.2ms | 硬件光追 → 软追踪降档 |
| ShadowDepths | 3.2ms | 1.8ms | 远距植被关投送 + VSM 降预算 |
| Game 动画段 | 5.1ms | 1.6ms | URO 15Hz + BudgetAllocator |
| 进门尖峰 | 220ms | 18ms | PSO 预热清单 |
| 背包 UI | 0.4ms | 0.4ms(不修) | Amdahl 劝退 |
D2 16:00 · 写回归。 跑图路线接入 CI 性能门禁,预算表归档,超支 5% 自动打回。那次达标产出了一件比 60fps 更值钱的东西——从此以后每次超标都有一张表可以对照。达标是事件的终点。制度,则从这里出发。
最后留一段复盘,三条带走。一,四条假设命中三条,VSM 降预算那条收益只有预期的一半——假设可证伪的价值就在于,证伪了也不亏,数据照样进预算表。二,所有修复都回到验收原场景验证,测试关卡里跑出的漂亮数字不算数,玩家不在测试关卡里玩。三,门禁比自律可靠:没有 CI 打回机制,预算表活不过下一个版本节点。
战报收尾还有一条诚实备注:内存饼图显示纹理流送池实测 2.3GB,超出 2.0GB 预算 15%,已标记为下个 sprint 的头号评审项。帧时间达标不等于所有预算都健康——性能工作没有毕业典礼,只有下一轮测量。
小结
先测量,别猜。平均帧率会说谎,稳态和尖峰是两种病,抓 30 秒 Insights 比猜三天便宜三个数量级。Amdahl 定律给出收益天花板 :占比 5% 的系统优化到零也只有 5.3% 收益,占比不足 10% 又不是尖峰元凶的系统默认不碰。Insights 三视图各管一件事:CPU 时间轴看线程接力与长条,GPU 视图按 Pass 对账,内存视图看曲线形状。健康帧与病态帧的差异是结构性的,不是数值大小。预算制度把"尽量快"变成"不许超":每个系统领工资,超支写检讨,余量是战备金。自动降级要降得果断、升得克制,两套不对称阈值防抖动;玩家的手动设置永远优先。
上手任务
- 抓一次自己的现场。 在你的竖切工程里
trace.start cpu,gpu,frame,bookmark抓 30 秒跑图,写出 CPU 时间轴、GPU 各 Pass、内存曲线各一个观察结论。验收标准:报告里每条结论都带具体毫秒数和 Named Event 或 Pass 名,不许出现"感觉挺流畅"。 - 立一张工资表。 为你的项目写一份 16.6ms(或 33.3ms)预算表,逐项填实测值。验收标准:每项有数字来源,超支项有明确处理结论(修复 / 重审 / 劝退),余量不低于 1ms。
- 实现自动降级器。 滚动平均帧时间连续 240 帧超 17.5ms 降一档 Scalability。验收标准:用
t.MaxFPS 20模拟低端机,10 秒内自动降档;解除锁帧后 30 秒内不来回抖动。
下一章
帧率达标了,但玩家对游戏的第一触感来自界面,画质反而排在后面——而背包 UI 欠下的那 0.4ms,卷三开篇就要还。第 19 章进入 UMG 与 CommonUI:MMO 量级的界面复杂度,从数据绑定和输入路由的地基打起。
延伸阅读
- 官方文档:Unreal Engine → Unreal Insights(Trace 通道、Timing 与 Memory 面板词条)
- 官方文档:Unreal Engine → Rendering → Pipeline State Cache(PSO 预热与收集流程)
- 官方文档:Unreal Engine → Scalability 与 DeviceProfiles(分档机制的完整配置参考)
- GDC 历年关于帧预算与性能文化的分享(搜索关键词:frame budget、performance culture、optimization workflow)
- 引擎源码:
Engine/Source/Programs/UnrealInsights(Insights 前端)与Engine/Source/Runtime/TraceLog(Trace 协议实现);分档逻辑见GameUserSettings与 Scalability 相关实现