CD Code Digest Hub /GTA V

流式加载、资源依赖与内存驻留

范围:framework/src/streaming,连接 base/src/paging、资源内存和场景 Store。静态分析,未运行。入口:Summary。

15 分钟阅读 12 个章节 2 张图

1. 大世界的基础是控制驻留集合#

Streaming 的作用不只是异步读文件,而是把玩法/空间需求转换为有限内存中可用的对象集合。RAGE 将“谁想要资源”“资源依赖谁”“在哪里读”“如何装配”“何时公开”“何时能移除”分到不同对象。

层次主要对象责任
需求生成场景 box streamer、游戏 streaming、脚本/任务兴趣位置、资产类别、请求标志和保护
资源身份strIndex、strLocalIndex、module manager全局资源索引与模块局部索引互换
驻留与请求strStreamingInfoManager状态、请求列表、依赖、引用和回收候选
装载调度strStreamingLoader/Manager设备、slots、内存、读盘、placement、取消
类型适配strStreamingModule 派生 StoreLoad/Place/SetResource、依赖与引用、defrag
字节输送pgStreamer、文件设备异步读取、解压相关工作与完成通知
内存管理streaming allocator、defragmentation资源页分配、回收、可移动资源重定位

strIndex 和 strLocalIndex 是不同包装类型。一个 drawable 的局部编号不能拿去查询动画 module;统一全局身份让调度器能够跨纹理、模型、碰撞和动画追踪依赖。

2. 两层状态机必须分开#

公开驻留状态只有 NOTLOADED、LOADREQUESTED、LOADING、LOADED。loader slot 则有 IDLE、LOADING、CANCELED,另以 m_IsPlaced、m_Finished 和 STRFLAG_INTERNAL_PLACING 表达内部进度。

图是跨对象的逻辑过程,不是额外存在一个含所有方框的 enum。重点在于 IO 完成不是 Loaded。后台 placement 做完也不能被外部当成已发布;PerformMainThreadPlacement → MarkPlacementComplete → FinalizePlacement 明确分三步。

3. 依赖闭包决定请求真正的代价#

strStreamingModule::GetDependencies 将类型知识交给模块;请求管理器递归请求所需依赖,传播 force/priority 语义,并维护 dependent count。可供读取的资源要求依赖已驻留,包本身也可能需要先驻留。ConvertToObject 再次检查依赖,是对前面调度契约的防御性核对。

这产生了一个容易忽视的限制:磁盘可以很快,但深依赖链仍可能按层形成装载延迟。A 需要 B、B 需要 C 时,资源图的关键路径比 A 的文件大小更能决定“何时可实例化”。把所有对象设成 priority 并不能消除这条链。

依赖数组常量 STREAMING_MAX_DEPENDENCIES=100 是当前实现的单次接口容量,AddDependencyOutput 里有越界诊断,不是无限扩展容器。strStreamingInfo 的 dependent count 使用 14 位,flags 使用 16 位,status 使用 2 位。资源密度、共享依赖扇出、标志扩展都受数据布局约束。

对于新项目,应将依赖深度、单资产依赖数、共享依赖的活跃引用峰值纳入离线校验。模型面数只是资源成本的一部分;材质引用、动画字典和碰撞共享方式会改变驻留闭包。

4. 请求标志是生命周期协议#

DONTDELETE、MISSION_REQUIRED、CUTSCENE_REQUIRED、INTERIOR_REQUIRED、ZONEDASSET_REQUIRED 汇入不可删除掩码。FORCE_LOAD 表示在成功加载前保持请求存活;PRIORITY_LOAD 改变调度优先级;LOADSCENE 服务阻塞场景加载。它们并非同一种“优先级”。

引用又细分 render/script/other;REF_DEFRAG 复用 render 类别。资源“不在画面中”不足以推出可删除:脚本、任务、依赖对象、在途 placement、搬移操作可能仍持有它。VerifyFlagChange 还约束某些状态下不得直接改标志,要求通过管理器维护列表一致性。

因此资源泄漏排查应输出“谁保护它、谁依赖它、是否在途”,而不仅是 last visible frame。长期任务/过场 pin 会压缩可回收集合;把内存预算调大只能暂时掩盖不平衡的生命周期。

5. 读盘策略反映光盘和机械盘时代的约束#

loader 显式区分 ODD/HDD 调度策略,GetNextRequestsOnCd 使用当前位置、设备掩码、seek tolerance 和连续请求批次。底层 pgStreamer 的接口文档说明读取按指定顺序进行,优先化由更高层负责。

当前定义有每设备 32 个 load slots、两个 streaming schedulers、普通/priority 两类请求;loader 初始 pending threshold 为 30,连续链长度通常设为 24。g_AREA51 分支会改为单请求并切换排序;这些是特定实现值,不能推断任何平台的最佳 IO queue depth。

常规 pending 字节预算是 8192 KiB,Orbis 分支 32768 KiB;常规 convert 时间参数 8 ms。burst mode 放宽字节额度并将对应时间阈值设为 20 ms。它们限制的是特定 loader 循环中的处理和排队,不是“整帧 streaming 必定低于 8 ms”的硬实时保证:一次不可分割 placement、主线程收尾或资源释放仍可能超出期望。

在 NVMe 上迁移时应重新测量:小 IO 合并收益、解压并发、CPU 发布成本和 GPU 上传往往比机械寻道更重要。保留依赖/预算/完成协议,替换基于设备的启发式比整体照搬更合理。

6. Placement:资源格式与线程安全的交界#

datResourceMap 描述虚拟/物理资源块;读入后的资源要恢复指针、对象内部布局和平台资源。module 可以选择异步 placement;最终 SetResource 仍在主线程执行。

ProcessAsyncFiles 遍历后台完成的条目,先执行主线程部分、再公开 Loaded、最后释放 slot。对于已取消的条目,仍需按合法流程清理。MarkPlacementComplete 会释放临时装载内存,或在允许时把资源注册给 defragmentation。

这里的“虚拟/物理”是 RAGE 资源内存分类,不应简单等同于现代操作系统的虚拟地址与物理页;涉及 GPU 资源的具体含义还取决于平台后端。

重要推论是:主线程发布预算可能成为高速存储后的瓶颈。若后台一次完成很多地图块,下一帧集中 SetResource/场景注册仍会产生尖峰。可借鉴的改进是记录 ready-to-publish 队列,按内容重要性分批提交;但不能打断模块要求原子完成的装配过程。

7. 取消不是立即回收#

pgStreamer::Cancel 注释明确说明缓冲区应在完成通知时回收,而不是 Cancel 返回时。loader 的 CANCELED 表示请求已经被读取器接手,必须等它结束。RemoveObject 对正在 placement 的对象还要保留 LOADING,防止过早移除它依赖的资源。

这是跨线程系统最重要的安全契约之一:用户意图已经撤回,不代表执行者停止访问内存。正确实现需要同时维护“是否仍需要结果”和“是否已经结束内存访问”两个条件。

新项目的取消测试应覆盖读盘前、读盘中、解压中、后台装配后/主线程发布前、发布后等窗口,检查引用是否准确归还。直接从请求队列移除 ID 并释放内存不够。

8. OOM 与碎片是不同问题#

资源名义大小、实际 buddy 页占用和临时峰值并不相同。ComputeOccupiedVirtualSize 明确包含页填充。loader 区分 EMERGENCY_OUT_OF_MEMORY 和 EMERGENCY_FRAGMENTED_MEMORY;PC 还存在受宏控制的 resource-cache 处理路径。

即使总空闲量足够,也可能不能组成所需分配;大量被 pin 的小资源又可能阻断压缩。一个大资源加载时可能同时占用压缩输入、解压输出、额外 module 内存以及正在搬移的新旧块。预算应考虑过程峰值,而不是只累加 Loaded 的最终尺寸。

诊断至少应分别观察:已加载占用、待加载预留、临时装配内存、最大空闲块、可回收资源量、不可移动页量和在途搬移量。源码中的字节计数与时间统计为这种诊断提供入口,但本次没有运行采样。

9. Defrag 是跨帧重定位事务#

USE_DEFRAGMENTATION 受 known references、主机/PC 及 FREE_PHYSICAL_RESOURCES 控制。不能仅凭 defragmentation.cpp 存在就断言当前所有 PC 配置都启用它。

其状态包含 Idle、CopyPhysical、Copy、Place、CheckCopyPhysical、WaitOneFrame、Finalise;同时有资源 Lock/Unlock、copy lock count、阻止模块搬移的查询、渲染线程 Flush 和工作线程 defrag 临界区。

具体平台可能跳过部分状态,以上是职责图。设计价值在于让外部引用、GPU 访问和对象地址迁移一致,而不是单纯 memmove。长期缓存裸指针、未注册的引用、没有结束的渲染访问都会妨碍安全移动。

对新项目而言,稳定句柄可以减少外部指针修复面,但不能自动解决 GPU descriptor、物理对象、动画任务中缓存地址的生命周期。应先决定哪些资源允许移动、哪些只允许卸载重建,再选择 allocator 和引用方案。

10. 阻塞装载是产品能力,也会改变性能语义#

LoadAllRequestedObjects、timeout 变体、Flush、keep-alive callback 支持场景切换和需要明确完成点的流程。这里存在超时与诊断路径,调用结束不应被不加条件地解释为所有请求永久成功。

普通 gameplay 如果频繁依赖这些同步入口,异步预取的隐藏延迟能力会消失。应将传送、过场切换、室内进入等作为明确装载事务,并定义缺失资源时如何等待、显示替代物或取消;不能只用“提高 IO 优先级”承诺瞬间到达完整世界。

11. 对新项目最有价值的模型#

以下是分析模型,不是源码已有公式:

可见前完成时间 ≈ 需求发现 + 队列等待 + 依赖关键路径 + IO/解压 + placement + 主线程发布等待。

连续移动的预取距离可从 v × 加载延迟分位数 + 安全余量 估算,但还需考虑转向、垂直移动、多兴趣点和传送。视距增加时不只是磁盘流量增加:依赖闭包、scene nodes、纹理占用和发布 CPU 都会一起增长。

建议先建立端到端资源追踪 ID,把 request、read、decompress、place、publish、evict 统一在一条时间线上;再根据观测决定增加 IO 并发还是减少依赖、调整块大小。这样才可能找出“磁盘很闲但地图没加载”的真实原因。

12. 源码证据#

文件阅读位置
streamingdefs.h51–156 配置、flags、引用类别、预算、依赖容量
streaminginfo.h40–45 状态;91–100 标志约束;123–142 占用大小;200–205 位域
streaminginfo.cppRequestObject;RemoveObject;GetNextFilesOnCdNew;DeleteNextLeastUsedObject
streamingmodule.h模块契约、GetDependencies、引用、异步装配与 AddDependencyOutput
streamingloader.h113–147 slot 状态;placement queue;EmergencyState
streamingloader.cpp297–370 收尾;418–588 调度;2383 起依赖复查;2589–2658 发布;2968 起后台 placement
defragmentation.h60–73 锁定协议;108–148 状态与容量
defragmentation.cppFlushAndDisable、Cancel、Update、Process 等搬移路径
streamer.h54–65 排序责任;118–126 Cancel 生命周期

关联:大世界场景、资源格式、内存、性能。