## 本次调查的结论

2026-10-21，本机 Windows 样本 SHA-256：`af2f5a1227911d8aa06b3908d6bd0211838211cae14ea91099cb57d0df990781`。本报告是作者对静态数据流的独立重述，未修改游戏、未注入进程、未运行抓帧；不包含商业 EXE、地图 tile 或整段反编译输出。

本轮将 CPU 的 **52 字节工作记录**连接到了实际的 **12 / 4 字节渲染顶点打包**，确定了四边形的三角化方式，以及 **32 个三角形当量一组的 chunklet**。同时排除了一个很容易误判的入口：`brick_upload_voxel_data` 所在大函数属于水系统，不能用其树、SDF 或分块常数解释 Solid 地形。

以下地址都绑定该样本；`FUN_...` 是工具生成的函数名称。命名算法和字段解释是本文作者的重建判断，不是开发商原始符号。

## 证据索引

| 静态函数 / 文件 | REA Evidence ID | 本报告用途 |
| --- | --- | --- |
| `0x1401b4de0` / `r3-mesh-consumer-b.json` | `ev_4ab7c327a72f71d792c0cb5c341c00ac6aa0c3aaff0707ed8eff4ce0a7b12a88` | 渲染 staging、三种顶点格式、quad/triangle 分区 |
| 同函数汇编 / `r3-mesh-packing-assembly.json` | `ev_9919a1069178255745700ceba2341fd92f88242799628cd6d1b8c71c8fb9bc3c` | 区分整数 load 与伪代码中的错误 float 解释、材料 bit packing |
| `0x1400ca780` / `r3-mesh-consumer-a.json` | `ev_3fb234e770645e54b2bc22a6409f2a30084c3bd43a47745c2a330b2f80d3965b` | chunklet 边界球和法线聚合 |
| `0x1400d6190` / `r3-chunklet-helper-a.json` | `ev_5ec6f4f91f01d32347ce7b12b1c76fb11e529f1f6bad2f53ab1af57cde994340` | 从输入索引数得到三角形当量 |
| `0x1400d6160` / `r3-chunklet-helper-b.json` | `ev_4df7fa2315cb3a73f04f97be7a6e0472599c1dd9627c13a7524168253e473a07` | `ceil(triangleCount / 32)` |
| `0x1400dda40` / `r3-render-chunk-allocation.json` | `ev_83c588beba04515d1cbe2a4c1c0468b0c125887df9a7ceaafd0c8efeaa34de70` | staging → renderer 分配与 offset 换算 |
| `0x140083d40` / `r2-render-mesh-allocation.json` | `ev_2ef8ab1e1b9de838f332e01e7340f5730731e711785f932661859dea45adb57c` | Mesh heap 分配包装及失败路径 |
| `0x1401b0a70` / `r3-scalar-field-b.json` | `ev_a426a35f64dc533e236928391bea146a19fd026915fc386fcd49c47b90f198f1` | 单元 Mesh → chunklet → staging → renderer 调用联系 |
| `0x1401b6b20` / `r3-render-descriptor-builder.json` | `ev_0a02d01238735a6db65cce6bd273569c1cc5948f41526a16ccf31512ea972eb8` | LOD 节点递归与新旧 render 句柄切换 |
| `0x1401d19f0` / `r3-brick-upload.json` | `ev_c1747c54c8ff713e7d1592682b0b78656115e4c48cde534e325644634ddcb2d3` | 水 brick 路径辨别与排除 |

本机原始 Evidence 的 ID 便于持有相同样本者复核；ID 本身不是公开下载链接。公开报告提供可核对的地址、算式与字段关系，不声称已提供完整工具数据库。

## 已恢复的数据流

在 `0x1401b0a70` 的一条路径上，可观察到：

1. `0x1401af120` 产生中间顶点和索引。
2. `0x1400d6160` 计算需要的 chunklet 数量，为每项分配 32 字节。
3. `0x1400ca780` 从中间顶点和索引生成 chunklet。
4. `0x1401b4de0` 打包顶点、保留 quad stream、生成 triangle stream、复制 chunklet 并整理 staging descriptor。
5. `0x1400dda40` 分配 renderer 记录与 Mesh heap 空间，换算索引/顶点的全局起点，再提交复制相关调用。

这条联系足以确认 52 字节是**CPU 工作格式**，并非这条路径的最终渲染顶点大小。仍未恢复最终 shader 的读取器、实际 GPU buffer 绑定、draw 提交与剔除每一阶段的执行顺序。

## 三种顶点打包模式

打包函数的 mode 参数不能直接等同于资源的 LOD 层号。

| mode | 每个源记录 | 每个输出顶点 | 已观察的规则 |
| --- | ---: | ---: | --- |
| 0 | 52 B | 12 B | 14-bit 三轴位置、两项 6-bit 法线编码、材料/权重与属性位 |
| 1 | 52 B | 4 B | 三轴各 6-bit 位置，另有源属性和材料低位 |
| 2 | 52 B | 4 B | 三轴坐标量化后以 674 为基数展开，再乘 14 加方向分类 |

调用者会设置 renderer descriptor 的另一类别字段为 0、1、2；它与打包 mode 是不同参数，不能未经核对就写成同一套枚举。

### mode 0：位置与法线

在本地 Mesh 坐标中，对每个位置分量执行：

```text
q = clamp(round_away_from_zero(p * 128 + 2048), 0, 16383)
```

范围为 `[-16, 111.9921875]`，量化步长为 `1/128` **局部坐标单位**；没有将该单位认作米。未饱和时最大舍入误差为半个量化步长。

法线源位于工作记录的 `+0x0C / +0x10 / +0x14`。它先除以 `max(1, |nx|+|ny|+|nz|)`，下半球采用折叠，再将两个分量映射到 `0..62` 并舍入。这里可用“八面体式折叠编码”描述数学行为；目标 shader 的反解尚未恢复。

| 输出 word | 位 | 含义 |
| --- | --- | --- |
| word 0 | 0..13 | `qx` |
| word 0 | 14..27 | `qy` |
| word 1 | 0..13 | `qz` |
| word 1 | 14..19 | 第一项折叠法线编码 |
| word 1 | 20..25 | 第二项折叠法线编码 |
| word 1 | 30 | 工作记录 `f32 @ +0x24 > 0` |
| word 1 | 31 | 工作记录 `f32 @ +0x20 > 0` |

表中未列位在该写入表达式中没有设值，不赋予它们额外语义。

### mode 0：第三个 word

机器码 `0x1401b559d` 使用整数指令 `MOV EAX, [R8 + 0x2C]`。这一点纠正了伪代码把同一个字段误显示成 float 转换的歧义。

令工作记录中的 `m0 = u32 @ +0x2C`，`m1 = u8 @ +0x18`，`w1 = u8 @ +0x19`，`m2 = u8 @ +0x1A`，`w2 = u8 @ +0x1B`，`a = f32 @ +0x28`。实际位运算可重述为：

```text
attributeBin = round(clamp(a, 0, 1) * 4)
word2 = (m0 << 3)
      | ((w1 & 0xE0) << 5)
      | (m1 << 13)
      | ((w2 & 0xE0) << 15)
      | (m2 << 23)
      | attributeBin
```

结合上游材质 ID 小于 128 的路径，可以整理为三个 7-bit 材质候选、两个 3-bit 权重箱和一个 `0..4` 属性箱：

| 位 | 来源 |
| --- | --- |
| 0..2 | 属性箱 |
| 3..9 | 第一材质 ID |
| 10..12 | `w1 >> 5` |
| 13..19 | 第二材质 ID |
| 20..22 | `w2 >> 5` |
| 23..29 | 第三材质 ID |

不能从打包结构推断 shader 最终如何归一化这些权重、如何查纹理、如何混合，或低三位属性应叫 AO、边缘磨损还是其他效果。它们在本报告中保留为中性属性名。

### mode 1 与 mode 2

mode 1 的位置为 `clamp(round(p+16),0,63)`。三个坐标占 bit 0..17；`trunc(f32 @ +0x1C) & 0xFF` 占 18..25；`(u8 @ +0x18)&1` 占 bit 26；`(u32 @ +0x2C)&7` 占 27..29。源字段混有数值转换和整数读取，不能照搬反编译器临时类型。

mode 2 的三轴量化为 `clamp(round(p*16+81),0,673)`，输出为：

```text
packed = ((qz * 674 + qy) * 674 + qx) * 14 + directionClass
```

方向分类读取源法线，包含 `abs(ny)>0.85` 分支，其余分支按 `ny`、`abs(nx)` 与 `abs(nz)`、`nx+nz` 分类。本报告不把 mode 2 自动归为屋顶、远景或碰撞格式，也未验证 shader 解码和分类边界的浮点行为。

## 索引与 staging 区域

令输入索引元素数为 `I`、顶点数为 `V`：

```text
T = mode == 2 ? floor(I / 3) : 2 * floor(I / 4)
C = ceil(T / 32)
```

mode 0/1 的输入是四个 `u16` 一组。打包时保留原四边形流，同时生成六个 `u16`：

```text
(a, b, c, d) → (a, b, c), (a, c, d)
```

mode 2 不保留 quad stream，按原三角形索引复制。这里确认了特定 CPU 打包路径的三角化；不表示所有 GPU pass 都使用展开后的 triangle stream。

主 staging allocation 的区域为：

```text
vertexStride = mode == 0 ? 12 : 4
quadStart = vertexStride * V
triangleStart = quadStart + (mode == 2 ? 0 : 2 * I)
chunkletStart = align16(triangleStart + 6 * T)
visibilityStart = align16(chunkletStart + 32 * C)
totalBytes = visibilityStart + 4 * C
```

最后 `4*C` 字节区的用途由 downstream 的 visibility 偏移提供高可信线索；本打包函数仅预留，未在此看到明确初始化。不要把“分配到一个地址”和“数据已生成”混为一谈。

### 两种 descriptor 基址

打包函数输出 `P` 被调用者放在 renderer 上传 descriptor `D+0x10`。因此 `P = D+0x10`，两套偏移差 16 字节。下表统一采用 **P 基址**：

| P 偏移 | 内容 |
| --- | --- |
| 0x60 | 顶点数 V |
| 0x64 | quad 数，mode 2 为 0 |
| 0x68 | 三角形当量 T |
| 0x6C | 保留的 quad 索引元素数，mode 2 为 0 |
| 0x70 | triangle 索引元素数 `3*T` |
| 0x74 | chunklet 数 C |
| 0x78 | quadStart |
| 0x7C | triangleStart |
| 0x80 | chunkletStart |
| 0x84 | visibilityStart |
| 0x88 | vertexStride |
| 0x90 / 0xA0 | 从所有工作顶点累计的 min/max，各写入 16 字节 |
| 0xB0..0x10F | 可选附加 uint 数组的 staging 描述 |
| 0x110..0x11F | 对附加数组元素低 7 位构建的 128-bit 存在掩码 |

附加数组的语义尚未证明。即使它与材质数据相邻，也不把它直接改名为材质表。公开 GPU `VoxelChunkShaderData` 声明与这些运行时 staging descriptor 是不同布局。

## Chunklet 的 32 字节究竟是什么

`0x1400ca780` 对 mode 2 取每组最多 32 个三角形，对其他 mode 取最多 16 个四边形，即 32 个三角形当量。分组按输入 primitive 顺序连续截取，没有在这个函数中看到空间聚类重排。

每组先用一个 **4096-bit 掩码**统计顶点位置。掩码位置采用 `vertexIndex & 0xFFF`；读顶点地址仍使用完整 `u16` 索引。这是重要细节：相差 4096 的不同索引可能别名，不能称作全索引范围上的严格去重，更不能据此断言整个 chunk 最多 4096 个顶点。

中心是通过该掩码保留的位置之均值。随后半径遍历组内**所有引用的顶点**，取到该中心的最大距离再开方。因此掩码别名会改变中心估计，但不意味着半径跳过相同别名的其他顶点。

法线聚合每个 primitive 的前三个顶点；quad 路径每四个索引前进一次，triangle 路径每三个索引前进一次。叉积结果在平方长度大于 `1e-20` 时归一化，否则使用零向量。取这些代表法线的均值 `nMean`，**未再次归一化均值**。再计算：

```text
d = max(0, min(dot(nMean, nFace)))
w = sqrt(1 - d*d)
```

| 输出偏移 | 内容 |
| --- | --- |
| 0x00..0x08 | 中心 xyz |
| 0x0C | 最大距离半径 |
| 0x10..0x18 | 代表法线均值 xyz |
| 0x1C | 上式 w |

它精确吻合反射 `VoxelChunklet { sphere:float4; normal:float4 }` 的 32 字节 GPU storage 声明。可将后半解释为用于方向剔除的法线范围数据，但实际 culling shader 的判断式尚未取得；不要把 `normal.xyz` 直接当作单位锥轴。

## Renderer 的接收与生命周期

`0x1400dda40` 从自己的槽池分配一个 CPU render chunk 对象，然后从 Mesh heap 分配主 staging 数据空间。对象至少访问到 `+0x128`，明显不是仅 192 字节的 `VoxelChunkShaderData` GPU 记录。

令主 allocation 的字节起点为 `B`，上传 descriptor 为 `D`，CPU render 对象为 `R`：

| R 偏移 | 直接观察 |
| --- | --- |
| 0xA0..0xA8 | 主 heap allocation 描述；其中 `+0xA4` 被作为 B 使用 |
| 0xAC | `B / D.vertexStride`，顶点元素起点 |
| 0xB0 | 顶点数 |
| 0xB4 | `(B + D.quadStart) >> 1`，u16 元素起点 |
| 0xB8 | quad 索引元素数 |
| 0xBC | `(B + D.triangleStart) >> 1` |
| 0xC0 | triangle 索引元素数 |
| 0xC4 | `B + D.chunkletStart`，字节起点 |
| 0xC8 | `B + D.visibilityStart`，字节起点 |
| 0xCC | chunklet 数 |

同一对象包含“按元素计”的顶点/索引起点和“按字节计”的 chunklet/visibility 起点。自行实现时应采用不同类型或明确单位，不能用一个裸 offset 统一解释。

若存在附加源数据，还会以 16 字节对齐分配另一段并复制；在一个没有附加源、类别为 0 的分支，大小为 `triangleCount*32`。结合 `bakedTriangleMaterialDataOffset` 等 GPU 声明，这是后续材质烘焙值得追踪的联系；本轮未恢复这 32 字节的材质记录格式。

分配失败会调用 `0x1400da5b0` 释放/回滚已经取得的 render chunk，返回错误标志。末尾将发生变化的对象提交给 `0x1400d6e10`。这些行为支持“持久 render 槽 + heap 区间 + 变更队列”的局部合同，但尚未恢复完整线程同步或 GPU fence 生命周期。

`0x1401b6b20` 另有高价值行为：节点有 8 个 `0x68` 字节子项；坐标翻倍、按轴增加 32、层号加一后递归，尺度使用 `32 / 2^level`，并结合相机到节点盒的距离决定细分/合并及工作排队。准备中的新 render 句柄会等 readiness 查询通过，再释放旧句柄并交换新旧槽。这是 Mesh LOD/发布侧的树形组织证据，**不能直接等同于资源七层 tile 索引或宣称全部世界存储均为八叉树**。

## 排除：同名 brick 上传其实属于水系统

`0x1401d19f0` 内可见 `update_water_brick_tree`，并在实际 CPU staging 中写：

```text
word[0..3] = packed brick data
word[4] = brick index
```

它吻合反射 `WaterBrickInstanceUploadData` 的 `brickData @0 / brickDataIndex @16`，与普通 `BrickUploadInstanceData` 的 `brickIndex @0 / brickData @4` 顺序相反。尾部 48 字节参数又吻合 `WaterBrickInterpolationParameters`，包含 rootNodeCount、enabled 及 wave-front 控制值。这组独立交叉证据比泛称 `brick_sdf` 的 marker 更有辨识力。

因此该函数中的根网格除以 192、8 子项分配、`brick_sdf`、postprocess 和 interpolation，应作为**水的旁路调查**。它不能证明 Solid 地形通过 SDF meshing，也不能拿 192 当作 Solid chunk 尺寸。下一轮可另查普通 `brick_voxel_material_copy` 与 `brick_near_sdf_generation` 的引用。

## 独立实现：先复现合同，再决定优化

可先把源场、单元顶点、拓扑、chunklet、渲染打包和发布器分别实现。一个实用基线是：

- CPU 保留自己清楚定义的 float 顶点与材质字段；不必照抄 52 字节工作格式。
- 先输出普通 triangle mesh，验证符号、绕序和 chunk 边界；quad 保留与 12 字节压缩作为独立适配器添加。
- chunklet 可以先用完整索引去重计算边界球；是否复现低 12 位掩码取决于追求行为一致还是改进自己的边界估计。
- 存储 tile、CPU 工作区、Mesh LOD 节点、chunklet、brick atlas 各自保留坐标域、尺寸、单位、halo 和所有权合同，不能混用常数。
- 通过 revision / readiness 与原子发布让新 Mesh 替换旧 Mesh；尚未证实的游戏线程/GPU 同步方案采用自己的实现。

## 原创参考与验证

[mode 0 打包参考](./render-mode0-packing-reference.mjs) 与 [独立边界测试](./render-mode0-packing-reference.test.mjs) 用人工构造数据重述上述公式。解码器是作者为验证设计的逆公式，没有声称已恢复游戏 shader；JavaScript 使用 double 运算，目标代码使用 float32，量化阈值附近不保证逐 bit 一致。零法线等退化输入也不代表一般法线的可逆性。

测试命令：

```text
node --test public/research/enshrouded/2026-10-21/render-mode0-packing-reference.test.mjs
```

2026-10-21 验证通过 4 项测试：1000 组位置的误差/饱和、1000 个覆盖两半球的单位法线样本（角误差小于 4°）、材料与属性位边界隔离、小端输出和输入校验。它们验证原创模型自身的数学合同，**不是对真实游戏 GPU 输出的差分验证**。
