加载解码:二进制只消掉解析,不会消掉物化尖峰

自动关联目录:加载解码:二进制只消掉解析,不会消掉物化尖峰

核心判断:把 JSON 换成二进制,只能减少字节解析和中间对象构造;如果恢复后的数据仍在游戏线程同步创建脚本对象、Actor 和 Component,首帧尖峰只是从 Parse 移到了 Materialize。

一、业务冲突:文件变小了,进入场景为什么仍然卡

复杂场景的加载不只包含下载和反序列化,还包含依赖解析、逻辑实例创建、UE 对象物化、组件注册、资源绑定和首帧提交。优化文件格式后,输入字节和解析耗时会下降,但玩家关心的是从发起进入到可操作的端到端时间,以及期间是否出现长时间主线程停顿。

工程实践中已经存在两条读取路径:受信任且编译指纹兼容时优先恢复二进制;否则读取经过完整性校验的文本。二进制恢复完成后,两条路径最终都会调用同一套场景构建逻辑。真正的瓶颈因此取决于对象数、属性数、跨引用和物化成本,而不只取决于文件字节数。

二、实际实现:选择格式严谨,但 Decode 边界过宽

当前读取器做对了三件事:先校验路径归属、文件大小和摘要;只有编译环境兼容且来源可信时才接受二进制;二进制失败但尚未恢复任何数据时才允许回退文本,一旦已经部分恢复则失败关闭,避免在同一 VM 中重复物化。

问题在于 Decode 同时承担了“字节变成值”和“值变成运行时对象”。场景加载在游戏线程开启资源收集,随后同步进入脚本侧构建实例,结束后才处理初始资源请求。日志虽然记录了编码、输入字节和总耗时,却无法回答耗时究竟落在校验、解析、建树、属性应用、引用解析还是 UE 物化。

Fetch -> Verify -> Decompress -> Parse/Restore
      -> Build logical graph -> Resolve references
      -> Materialize UE objects -> Register components
      -> Bind dependencies -> First visible frame

三、值得保留:二进制路径首先是正确性协议

  • 摘要和边界检查发生在 VM 对象创建之前,损坏输入不会污染当前世界。
  • 编译指纹与来源信任是二进制执行的前置条件,不把“文件存在”误当兼容。
  • 部分恢复后禁止自动回退,避免重复 ID、重复订阅和半棵对象树。
  • 文本路径保留为兼容和诊断通道,使格式升级可以双读而不双写运行时状态。

这些机制比单纯追求更快更重要。错误的快速恢复会把确定性的加载失败变成难以复现的世界污染。

四、结构性缺陷:一次同步事务吞掉了所有长尾

总耗时可写为:

Tready=Tio+Tverify+Tdecode+Tgraph+Tmaterialize+Tdeps+TcommitT_{ready}=T_{io}+T_{verify}+T_{decode}+T_{graph}+T_{materialize}+T_{deps}+T_{commit}

二进制主要压缩TdecodeT_{decode},而海量对象常由Tgraph+Tmaterialize+TcommitT_{graph}+T_{materialize}+T_{commit} 主导。若每个逻辑节点都立即生成 UObject、Actor 或 Component,成本近似:

Tmaterialize≈NoCo+NpCp+NrCrT_{materialize}\approx N_o C_o+N_p C_p+N_r C_r

其中NoN_o 是对象数,NpN_p 是属性写入数,NrN_r 是引用边数。格式再快,也改变不了这一数量级。

另一个风险是事务不可切分:脚本构建已经产生副作用后,不能随意让出一帧;失败也难以回滚。结果要么阻塞游戏线程保证原子性,要么分帧后暴露半成品世界。

五、目标设计:Prepare 与 Commit 分离

后台阶段只生成不可变、可验证的 ScenePlan:紧凑节点数组、属性块、引用表、依赖闭包和物化优先级。它不能访问 UWorld,也不能触发脚本副作用。游戏线程只按预算提交当前可见和玩法必要的部分,其余节点先保留为逻辑记录。

ScenePlan plan = DecodeAndValidateOffThread(bytes);
for (Chunk& chunk : plan.OrderByCriticality()) {
    CommitQueue.Enqueue({
        .estimatedUs = EstimateCommitCost(chunk),
        .generation = worldGeneration,
        .apply = [chunk] { MaterializeOnGameThread(chunk); }
    });
}

原子性不再等于“一帧完成全部对象”,而是以可观察边界提交:先建立稳定身份和必要父子关系,再提交出生区碰撞与角色,随后提交视觉对象。未提交节点对脚本查询返回 Pending,不能伪装成不存在或 Ready。

六、取舍与验证

Prepare/Commit 增加了中间制品和状态机,也要求属性应用可重入、引用可延迟。但它把解析吞吐和主线程帧预算解耦,才真正允许场景规模增长。

验收必须分别记录输入吞吐、解析峰值内存、逻辑节点吞吐、单次 Commit 长尾、每帧 Commit 总时长和 TimeToPlayable。合成示例:若解析从 40 ms 降到 8 ms,但同步物化仍为 180 ms,端到端只从 220 ms 降到 188 ms;这个示例仅说明口径,不代表历史数据。

最终决策规则:二进制是数据格式优化;只有逻辑图与 UE 物化解耦,才是加载架构优化。