共享碰撞模型缓存与引用生命周期

本批交付

新增 NativeCollisionModelCache,并以原 EXE 缓存代码及实际 MSVCP90 字符串实现作对照。

  • 1,968 次 acquire/release/零引用回收操作,结果、条目顺序、容量和回调观察一致。
  • 覆盖相同/大小写不同/斜杠不同的请求、长字符串、中文、空请求、空模型、引用数回绕及受控重入。
  • 真实 collider 经 F2CollisionModelCacheHost 进入缓存:同请求加载一次、归零后回收前复用、回收后重新加载均通过。
  • 核心累计 1,439 条断言、264 项相关 Python 回归通过。
  • Godot C# 构建零警告、零错误,coresimlint 通过。

缓存控制内核已实现;完整 CCollisionModel 构造/析构和正式实例的资源替换仍未据此算作完成。

原入口及布局

地址作用
649520按请求获取/创建碰撞模型
648940按 wstring 找第一个匹配条目
648A10按模型指针找第一个匹配条目
648D00找到条目后引用数减一
6490B0删除零引用碰撞条目
648520CollisionModelRef 构造
648D80/6492A0CollisionModelRef 析构及虚析构包装
7D68B0按固定步长扩容

CMasterResourceManager 中的碰撞表:

  • +0xBC:条目指针数组。
  • +0xC0:count。
  • +0xC4:capacity。
  • +0xC8:增长步长,构造器 64A67F/64A69D 初始化为 10

条目为 44 字节:

  • +8:模型指针。
  • +12:引用数原始 DWORD。
  • +16:wstring 请求名。

Acquire 的实际顺序

648940 按数组顺序、wstring 精确相等查找。这里不转小写、不统一斜杠、不解析路径;字符装载器的 67FF70 规范化是调用前的另一步。

命中:

  1. 引用 DWORD 加一,按 32 位回绕。
  2. 返回条目的模型指针,包含已缓存的空指针。

未命中:

  1. 分配并构造 CollisionModelRef。
  2. 若满,按增长步长扩容。
  3. 把条目加入数组并增加 count。
  4. 分配/构造碰撞模型。
  5. 写入模型指针。
  6. 引用数写为 1。
  7. 最后发布请求名。

因此模型构造期间,条目已经能被观察,但名字仍为空、模型仍为空。不能先完整装载模型再把成品放入 Dictionary。

未初始化的引用字

648520 只初始化模型指针和空字符串,没有写 +12。不能从其他字段为零推出引用数也为零。

核心用 nullable 引用字表达这个未提供的堆内存值。正常装载完成后明确写为 1。如果受控重入在装载未完成前必须读取它:

  • 原内存值已提供时,按该值计算。
  • 原内存值未知时,显式报输入缺失。
  • 不擅自补 0,也不自动把这种条目视为可回收。

原生测试的分配器刻意填入已知 seed,确认模型构造回调观察到的仍是那个原始值。

路径和模型身份

  • 字符串用 Ordinal 等价语义匹配,对应实际 wstring 比较。
  • a 与 A、a/b 与 a\b 是不同缓存键。
  • 相同物理文件的不同拼写可能创建不同模型身份。
  • 释放按模型指针身份,而不是按请求名、几何值或数据相等比较。
  • 原 acquire 返回的模型对象与内部可选几何是两个概念。

NativeCollisionModelHandle 表达模型身份,Mesh 可以为空。缓存和几何 JSON 的文件去重不是同一层,不把导出器共享一份文件数据误当作原运行时已共享同一个模型。

Release 与零引用回收

648D00 只查找模型并对第一个匹配条目减一:

  • 找不到不修改。
  • 不在这一刻删除条目。
  • 不把零减一夹成零;原 DWORD 会回绕为 0xFFFFFFFF。
  • 空模型也没有特殊跳过:可匹配第一个模型指针为空的条目。

6490B0 仅回收引用 DWORD 恰好为零的条目,非零值(包括回绕的大值)保留。

回收顺序:

  1. 调用条目/模型析构。
  2. 回调期间,条目和模型仍可被查到。
  3. 清空槽。
  4. 递减 count,用末尾条目补当前空位。
  5. 重新检查补进来的当前槽;后续循环使用实时 count。

所以回收不是保持顺序的 List.RemoveAt,也不是提前复制待删除集合。容量不会因回收而缩小。

核心在销毁回调后标记 handle 已退休,并清除它持有的几何引用。通过已退休 handle 再访问 Mesh 会明确报错,而不是悄悄当成无碰撞模型。独立保留的不可变几何值及外部借用的正确管理仍由调用方负责。

重入参考

模型构造和销毁是明确的参考边界,测试在这些位置发起嵌套 acquire:

  • 构造期间再次请求相同的非空名字:外层未发布名字,因此可能创建第二条同名记录。之后查找仍取数组中第一条。
  • 构造期间请求空名:可能命中名字尚未发布的条目,此时不能假定引用字已初始化。
  • 销毁期间新增资源:新条目参与实时数组和末项补位处理,不在一份旧快照里被遗漏。

这些是缓存控制行为的验证,不证明实际 CCollisionModel 构造/析构一定会触发所有这类回调。

真实几何后端

F2CollisionModelCacheHost 用明确的原始资产读取函数创建 managed 几何模型:

text
CharacterCollisionTable.LoadGeometryRequest
  → 原索引/surface/法线和扫掠几何
  → NativeCollisionModelHandle
  → NativeCollisionModelCache

它只管理 CPU 几何数据,不拥有 Godot 节点或原 Ogre 对象;不能据此声明完整 C++ 模型析构已复原。缺失资产仍报明确错误,不返回默认灰模或未命中替身。

ARBITER 默认 collider 测试确认:

  • 连续获取相同请求两次,只有一次实际文件加载,返回同一个 handle。
  • 两次 release 使引用数为零,但回收前再次获取仍复用原 handle。
  • 零引用回收调用销毁,旧 handle 失效。
  • 后续再获取产生新身份,实际文件加载计数增加。

装载异常与输入域

核心在条目加入后才调用模型创建函数。创建函数抛错时,不凭空发布请求名或初始化引用字;保留明确的未完成 staging 状态,并要求调用方处理/丢弃失败上下文。

此 managed 异常测试不证明原 C++ 全部 SEH/OOM 展开过程已经复现。原生正常/空分配路径已纳入对照;任意内存损坏、无效容量、悬垂引用及 allocator 失败后的非法解引用不伪造结果。

当前实现针对自有有效存储(count <= capacity)。原损坏容量下重复访问槽零的行为不能从这批测试推广成已支持的外部内存输入。

完整 manager 析构、其他 BatchModel/资源表清理和所有调用方的引用配平仍在后续范围,CollectZeroReferences 不是整个 manager 的 Dispose。

实际原生执行范围

  • 原 649520、648940、648A10、648D00、6490B0。
  • 原条目构造/析构、7D68B0 扩容。
  • 使用本机 x86 Microsoft.VC90.CRT 9.0.30729.9635 的 MSVCP90/MSVCR90,记录文件路径与 SHA256。
  • 在私有 activation context 中加载 CRT;没有安装组件、修改系统配置或 attach 原游戏。
  • 模型构造/销毁以及受控分配存储为明确边界,字符串操作没有替换成 token 比较。

原程序确实导入 MSVCP90.dll,而不是 MSVCP100。若该运行时文件变化,参考产物的哈希检查要求重新验证。

文件

  • core_sim/NativeCollisionModelCache.cs。
  • core_sim.io/F2CollisionModelCacheHost.cs、F2CharacterCollision.cs。
  • core_sim.tests/NativeModelCacheChecks.cs。
  • tools/build_native_model_cache_oracle.py、native_model_cache_oracle.c。
  • tests/test_native_model_cache.py。
  • build/native_model_cache_oracle/results.json。
  • build/native_model_cache_mismatches.json(空)。
  • build/native_model_cache_core.log、native_model_cache_python.log、native_model_cache_godot.log。

EXE SHA256: 186472c3057b38f4cdff4696959a943c396ae6166995f7418997b5ea853e8a5e

下一批

后续进展:Room实例碰撞资源替换与共享所有权已把缓存接到实例 setter/teardown,并验证共享实际资源;完整 CCollisionModel 与单位装载仍需继续。

下一步把缓存用于实例装载/替换/释放,并补完整 CCollisionModel 构造及装备对应资源路径,继续活动位生命周期和正式地图宿主。acquire/release/零引用收集的顺序算法不再是未知项。

武器可构造覆盖仍为 1,053/1,419;正式攻击遮挡、攻击型 proc、动画、缺导出资产和全武器实机验收仍待完成。UniverseComplete=false 未放宽。

本批按 godot-master 的资源归属要求把缓存模型身份、几何数据和场景对象分开。main 原地工作,未提交、未改正式场景/UI,快照 v29 未变。默认存档 SHA256 保持: 9840fe858b6e311c73f8c2d4ed49912d59d71365f6278392811730e012d32a7f