CD Code Digest Hub /GTA V

内存管理、资源寿命、存档与更新

6 分钟阅读 9 个章节

1. 需要区分的四类内存#

类别内容主要寿命
程序资源代码、字符串、Native 表、静态初值scrProgram 的引用和资源装载期
实例内存实例静态区、局部帧、求值栈、启动参数scrThread 实例
共享全局GLOBALS 块及其初值/签名全局块装载与游戏管理
引擎资源实体、模型请求、摄像机、网络对象等对应游戏系统与 script handler

当前主文法和 VM 数据模型未展示脚本可任意 new/delete 的通用对象堆、自动增长容器或 tracing GC;聚合值多内联在实例/全局区域。C++ 宿主仍大量动态分配,程序资源也有引用计数,所以“无脚本 GC”不能写成“游戏脚本绝无动态内存分配”。[S1]

2. 栈容量为什么包含实例数据#

Reset 把程序静态初值复制到线程栈底,再设置 SP。游戏通过多种栈容量类别和对应池数量分配线程,并非所有任务都使用引擎通用默认的 512 槽。[S2]

估算模型:

text
单实例所需槽位 ≈ 实例静态区
                 + 活跃调用链中的参数与局部
                 + 返回地址/帧信息
                 + 表达式及 Native 临时空间

单实例预留内存 = 分配到的栈类别槽数 × sizeof(scrValue)

所需峰值与预留值不是同一指标。多个低使用量实例仍可能占用多个较大栈;挂起不会释放栈。把一个大头文件拆为小函数,也不必然减少它带来的文件级变量。

3. GLOBALS 的布局与演化#

当前 24 位全局访问地址分为 6 位块号和 18 位块内偏移,支持 64 个块;游戏定义 STANDARD、TUNABLES、MP、MP_FM、TITLE_UPDATE 等实际分区。[S3]

这些块为多人、大型内容和更新提供分区空间,但仍是固定槽位 ABI。字段插入、数组扩容、类型变化会移动后续偏移;若相关脚本不是用相同声明重编译,可能读写错误位置。

CalculateGlobalHash 对槽初值的低 32 位数据求哈希,不是完整字段 schema。它有助于发现某些布局/内容不一致,不能证明全部布局变更兼容。字符串或函数地址跨程序保存在 GLOBALS 也被编译器警告,因为程序卸载可能影响引用有效性。[S4]

4. 脚本资源卸载与引擎对象清理#

CStreamedScripts 结合资源 store 的引用管理,卸载时 Release 程序对象。程序请求者释放其需要和运行实例结束是两个不同事件,加载/释放必须与实例引用协调。[S5]

script handler 清理资源列表时,依据 DetachOnCleanup 等策略决定脱离或销毁。实体资源可能需要转移、保留或交回游戏人口系统,而不是简单 delete。清理机制处理的是原生资源归属,不是脚本所有语义副作用:玩家控制、相机模式、UI 标记等仍可能需要专门复位。

5. VM 可保存状态不等于游戏存档保存整个 VM#

scrThread::Serialized 用程序 ID、PC、FP、SP 等形式保存执行控制状态,并有“避免指针便于保存”的设计注释。实际栈值仍可能含字符串/引用;不能由结构名推导出任意脚本、任意版本之间可无条件快照恢复。[S1]

GTA 的持久数据有显式登记流程:START_SAVE_DATA → 注册整数/浮点/布尔/枚举/文本和嵌套结构/数组 → STOP_SAVE_DATA。接口说明要求登记过程同一帧完成,STOP 时会把已加载数据写入匹配变量;GtaThread 在帧结束检查未闭合的保存结构。[S6]

它更接近通过名字/结构元数据管理持久字段,而不是把所有栈页原样写盘。底层 CScriptSaveData 包含 PSO/XML 树及活动/非活动脚本数据处理。[S7]

6. 真实持久化代码的含义#

Register_Array_Of_Saved_MP_GENERAL 显式调用 START_SAVE_STRUCT_WITH_SIZE,逐项 REGISTER,标签可能与变量名不同。[S8] 标签因此构成长期数据契约的一部分:重命名变量不必自动改变保存标签,随意修改标签却可能改变恢复匹配。

已有迁移和 patching 文件说明版本演化是单独职责;不能承诺新增字段、改类型、改数组长度后全部自动安全。新语言最好为存档数据提供稳定字段 ID、默认值、schema 版本和迁移函数,并在工具中显示兼容性差异。

7. 热更新应拆成三个问题#

  1. 资源更新:新字节码/平台资源能否进入资源系统。
  2. 实例更新:旧实例继续执行、终止重启,还是换成新实例。
  3. 状态迁移:旧 PC/栈/闭包/句柄怎样映射到新代码。

当前源码能证明程序加载、卸载、线程创建、调试和持久数据机制,不能证明完整的无损热重载。发生函数布局、局部变量或帧结构变化时,保留旧 PC/FP/SP 并直接替换代码尤其危险。

对新语言,建议第一阶段实现“新版本只用于新实例”,再提供显式安全点的状态导出/导入。保存业务状态比保存任意调用栈更容易长期兼容。

8. 可借鉴的取舍#

固定布局和预分配实例内存有利于预算估算、减少 GC 抖动、支持简单解释器;成本是大数组、大头文件和长寿命控制器的固定内存占用。适合保留这种可预测性,但应提供只读共享数据资源、显式容量容器和实例内存报告。

资源管理可采用 task scope 拥有资源句柄、取消时清理、显式 transfer/detach 的模型。跨帧借用和长期 Native 注册需要静态或运行期寿命检查,避免作者只能靠注释维持正确性。

源码证据#

  • S1 — scrThread::Serialized;scrValue 指针成员。
  • S2 — Reset 与静态区;游戏栈类别。
  • S3 — 全局地址布局;全局块名。
  • S4 — 全局初值哈希;指针类全局警告。
  • S5 — 资源 Remove/RemoveRef;handler 清理。
  • S6 — 存档 Native 契约;线程帧末检查。
  • S7 — CScriptSaveData。
  • S8 — MP 字段注册实例。