docs(render): define OH2 CellStruct contract
This commit is contained in:
parent
eaea877662
commit
5cd4fd2c71
1 changed files with 726 additions and 0 deletions
|
|
@ -0,0 +1,726 @@
|
|||
# OH2 — retail `CellStruct` surface/subset construction contract
|
||||
|
||||
**Date:** 2026-09-01
|
||||
**Campaign:** OVERHAUL / OH2
|
||||
**Scope:** the built-EnvCell path only: `CEnvCell::UnPack` →
|
||||
`D3DPolyRender::ConstructMesh` → `RenderDeviceD3D::DrawEnvCell` →
|
||||
`D3DPolyRender::DrawMesh(..., arg4 = 1)`
|
||||
**Status:** evidence complete; implementation not started
|
||||
**Retail oracle:** Sept. 2013 EoR named client, with live Ghidra arbitration of
|
||||
every branch whose decompiler rendering could change the result
|
||||
|
||||
## Executive result
|
||||
|
||||
Retail does **not** use `NoPos` or `NoNeg` to decide whether a polygon side
|
||||
exists. Those bits say that the corresponding UV-index array is absent. The
|
||||
side set comes only from `CPolygon::sides_type`:
|
||||
|
||||
| `sides_type` | Retail candidates, in construction order |
|
||||
|---:|---|
|
||||
| `ST_SINGLE` (`0`) | positive surface once, positive normal, ordinary fan winding |
|
||||
| `ST_DOUBLE` (`1`) | positive surface twice: first positive normal/ordinary winding, then negative normal/reversed winding |
|
||||
| `ST_BOTH` (`2`) | positive surface/positive UV/positive normal, then negative surface/negative UV/negative normal; the negative fan is **not** index-reversed |
|
||||
|
||||
Every candidate is constructed, including a candidate whose `Surface.Type` is
|
||||
untextured. The built EnvCell draw later skips a subset unless
|
||||
`Surface.Type & (BASE1_IMAGE | BASE1_CLIPMAP)` is nonzero. That later surface
|
||||
test, not `NoPos`, is the exact retail visibility rule.
|
||||
|
||||
The material/subset owner is the **surface-array index**. Retail allocates one
|
||||
batch accumulator and one mask byte per surface slot, writes triangle
|
||||
attributes with that slot index, and emits the nonempty attribute ranges in
|
||||
ascending surface-index order. It does not make `Stippling`, texture format,
|
||||
or `sides_type` part of the subset identity.
|
||||
|
||||
This requires an `acdream.pak` **bake-recipe bump from 7 to 8 and a full
|
||||
rebuild**, but **does not require a container-format bump**. `PakFormat` can
|
||||
remain 2: the container framing and asset directory do not change, while the
|
||||
serialized prepared-mesh record and its recipe do. Both Content and Launcher
|
||||
recipe constants must move together, with a `7 → 8` full-rebuild migration.
|
||||
|
||||
The canonical cathedral/Facility Hub cells have no textured `NoPos` polygon,
|
||||
so the exact rule produces the same visible/untextured admission split there.
|
||||
OH2 closes a real global contract gap (AP-234), but must not be presented as a
|
||||
standalone fix for the cathedral's camera-dependent leaking geometry.
|
||||
|
||||
## 1. Evidence method and authority
|
||||
|
||||
The named retail corpus was inspected first, as required:
|
||||
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
- `docs/research/named-retail/acclient.h`
|
||||
|
||||
The following functions were then decompiled independently through the live
|
||||
Ghidra MCP bridge on port 8081 (`patchmem` project). Ghidra was used as the
|
||||
mandatory branch-sense arbiter, not merely as a name lookup:
|
||||
|
||||
| Function | Address | Named-corpus anchor | Branch fact arbitrated in Ghidra |
|
||||
|---|---:|---:|---|
|
||||
| `CEnvCell::UnPack` | `0x0052D470` | pseudo-C `310850` | built EnvCells call `ConstructMesh(..., 3.0f, true, ...)` |
|
||||
| `CPolygon::UnPack` | `0x00538650` | `322296` | `NoPos`/`NoNeg` suppress UV-array unpack only; `ST_DOUBLE` aliases negative surface/UV to positive |
|
||||
| `copyVert` | `0x0059C080` | `424779` | normal is multiplied by `+1/-1`; absent or invalid UV writes zero UV coordinates |
|
||||
| `D3DPolyRender::DrawMesh` | `0x0059D4A0` | `426048` | built-cell `arg4=1` cannot take the ordinary-object untextured fallthrough |
|
||||
| `D3DPolyRender::ConstructMesh` | `0x0059DFA0` | `426717` | exact side loops, reverse-winding branch, surface mask, vertex-key lanes, and attribute ranges |
|
||||
| `RenderDeviceD3D::DrawEnvCell` | `0x0059F170` | `427885` | built branch calls `DrawMesh(..., 1)`; fallback is a different polygon-list path |
|
||||
|
||||
This matters especially for `ConstructMesh`: the pseudo-C reuses variables
|
||||
named `arg1` and `arg4` after their original parameters are dead. Ghidra
|
||||
confirms that, in the emission loops, the first is the side ordinal and the
|
||||
second is the duplicate-copy ordinal. Reading them as the function arguments
|
||||
would invert the `ST_BOTH` winding conclusion.
|
||||
|
||||
The installed DAT audit was read-only. The client data set used here has:
|
||||
|
||||
| DAT | SHA-256 |
|
||||
|---|---|
|
||||
| `client_portal.dat` | `DC6E500BA22E6B186DB7171E3F3345238B6444C85D798ADC85E550973B8D12E4` |
|
||||
| `client_cell_1.dat` | `6DB0ABF00FBCEED62C3F1EE842EE7C1F423D732BED77A5B7C102EE89A52AB99E` |
|
||||
| `client_highres.dat` | `503E0828D14F2F9CCBC31431E1055AC188464BF4B499DE37F4C3D5B2D9F3E727` |
|
||||
| `client_local_English.dat` | `E85C820280C88FAC7DF6C8043F5E24596E9C8774193AF4123D756546F78FB2BB` |
|
||||
|
||||
## 2. Retail source structures and flags
|
||||
|
||||
### 2.1 `CPolygon`
|
||||
|
||||
The verbatim retail header declares (`acclient.h:31855`):
|
||||
|
||||
| Offset | Field | Meaning in this contract |
|
||||
|---:|---|---|
|
||||
| `0x00` | `CVertex **vertices` | resolved authored vertices |
|
||||
| `0x04` | `uint16 *vertex_ids` | source vertex identity and fan order |
|
||||
| `0x08` | `Vec2Dscreen **screen` | unrelated here |
|
||||
| `0x0C` | `int16 poly_id` | authored polygon id |
|
||||
| `0x0E` | `char num_pts` | fan vertex count |
|
||||
| `0x0F` | `char stippling` | stipple bits plus UV-absence bits |
|
||||
| `0x10` | `int sides_type` | `ST_SINGLE` / `ST_DOUBLE` / `ST_BOTH` |
|
||||
| `0x14` | `char *pos_uv_indices` | null when `NO_POS_UVS` |
|
||||
| `0x18` | `char *neg_uv_indices` | independent only for `ST_BOTH`; null when `NO_NEG_UVS` |
|
||||
| `0x1C` | `uint16 pos_surface` | index into the EnvCell's ordered surface array |
|
||||
| `0x1E` | `uint16 neg_surface` | same; aliased to positive for `ST_DOUBLE` |
|
||||
| `0x20` | `Plane plane` | not a subset selector |
|
||||
|
||||
The retail stride is `0x30` bytes; `ConstructMesh` advances the polygon cursor
|
||||
by that stride (`pseudo-C:427191`).
|
||||
|
||||
### 2.2 Exact enums
|
||||
|
||||
`SidesType` (`acclient.h:7372`):
|
||||
|
||||
- `ST_SINGLE = 0`
|
||||
- `ST_DOUBLE = 1`
|
||||
- `ST_BOTH = 2`
|
||||
|
||||
`StipplingType` (`acclient.h:7380`):
|
||||
|
||||
- `NO_STIPPLING = 0`
|
||||
- `POSITIVE_STIPPLING = 1`
|
||||
- `NEGATIVE_STIPPLING = 2`
|
||||
- `BOTH_STIPPLING = 3`
|
||||
- `NO_POS_UVS = 4`
|
||||
- `NO_NEG_UVS = 8`
|
||||
- `NO_UVS = 0x14` (the retail value is unusual but verbatim)
|
||||
|
||||
The DRW enum name `CullMode` is misleading on `Polygon.SidesType`. It is the
|
||||
retail `SidesType`, not a GPU rasterizer cull mode.
|
||||
|
||||
Relevant `SurfaceType` bits (`acclient.h:5820`):
|
||||
|
||||
| Bit | Retail name |
|
||||
|---:|---|
|
||||
| `0x00000001` | `BASE1_SOLID` |
|
||||
| `0x00000002` | `BASE1_IMAGE` |
|
||||
| `0x00000004` | `BASE1_CLIPMAP` |
|
||||
| `0x00000010` | `TRANSLUCENT` |
|
||||
| `0x00000100` | `ALPHA` |
|
||||
| `0x00000200` | `INVALPHA` |
|
||||
| `0x00010000` | `ADDITIVE` |
|
||||
|
||||
### 2.3 `CPolygon::UnPack` proves the NoPos/NoNeg meaning
|
||||
|
||||
`CPolygon::UnPack @0x00538650` reads polygon id, point count, stippling,
|
||||
`sides_type`, both surface indices, and every vertex id first
|
||||
(`pseudo-C:322302-322345`). It then:
|
||||
|
||||
1. allocates and reads `pos_uv_indices` only when `(stippling & 4) == 0`
|
||||
(`322347-322367`);
|
||||
2. allocates and reads `neg_uv_indices` only when `sides_type == 2` and
|
||||
`(stippling & 8) == 0` (`322369-322387`);
|
||||
3. for `sides_type == 1`, aliases `neg_surface = pos_surface` and
|
||||
`neg_uv_indices = pos_uv_indices` (`322389+`, independently confirmed in
|
||||
Ghidra).
|
||||
|
||||
No branch removes a face. The flags named `NoPos`/`NoNeg` in DRW are retail's
|
||||
`NO_POS_UVS`/`NO_NEG_UVS`.
|
||||
|
||||
## 3. Exact retail construction algorithm
|
||||
|
||||
### 3.1 Caller and inputs
|
||||
|
||||
`CEnvCell::UnPack @0x0052D470` resolves the ordered EnvCell surface array and
|
||||
its `CCellStruct`, then calls (`pseudo-C:311081-311085`):
|
||||
|
||||
```text
|
||||
D3DPolyRender::ConstructMesh(
|
||||
num_surfaces,
|
||||
surfaces,
|
||||
&structure.vertex_array,
|
||||
structure.num_polygons,
|
||||
structure.polygons,
|
||||
3.0f,
|
||||
true,
|
||||
&constructed_mesh)
|
||||
```
|
||||
|
||||
That establishes that the surface array and its index order are direct
|
||||
construction inputs. The `3.0f` and `true` control detail-UV/FVF construction;
|
||||
they do not alter side selection.
|
||||
|
||||
### 3.2 Per-surface mask and batch allocation
|
||||
|
||||
`ConstructMesh` allocates:
|
||||
|
||||
- one mask byte per surface in `MeshBuffer::isStippledOrAlphaedMask`;
|
||||
- one `MeshBatchType` per surface index in a scratch array.
|
||||
|
||||
`MeshBuffer` is the `0x24`-byte record at `acclient.h:31949`; its mask pointer
|
||||
is offset `0x10`. `MeshBatchType` is the five-`uint32` record at
|
||||
`acclient.h:59629`:
|
||||
|
||||
```text
|
||||
FirstTriangleIndex, NumTriangles, CurTriangleIndex,
|
||||
FirstVertexIndex, LastVertexIndex
|
||||
```
|
||||
|
||||
The initial mask is derived solely from the corresponding surface's raw type
|
||||
(`pseudo-C:426788-426818`) and the branch precedence is exact:
|
||||
|
||||
```text
|
||||
if (type & 0x10300) != 0: mask = 2
|
||||
else if (type & BASE1_CLIPMAP) != 0: mask = 8
|
||||
else if (type & TRANSLUCENT) != 0: mask = 4
|
||||
else: mask = 0
|
||||
```
|
||||
|
||||
For each polygon, retail then ORs `(stippling > 0 ? 1 : 0)` into **only the
|
||||
positive surface's** mask (`426864-426871`). The instruction is signed-byte
|
||||
`SETG`, not an arbitrary raw-byte-nonzero test. It is deliberately broader
|
||||
than the low two stipple-side bits: every defined nonzero enum value is
|
||||
positive, so `NO_POS_UVS` and `NO_NEG_UVS` also set bit 0. Raw corrupt values
|
||||
`0x80..0xFF` do not. This does not create another subset.
|
||||
|
||||
### 3.3 Triangle counts and surface ownership
|
||||
|
||||
For each valid fan, baseline triangles are `num_pts - 2`.
|
||||
|
||||
- `ST_SINGLE`: one baseline count on the positive surface.
|
||||
- `ST_DOUBLE`: total geometry capacity doubles, and the positive surface's
|
||||
batch count receives the baseline twice.
|
||||
- `ST_BOTH`: total capacity doubles; positive and negative surface batches
|
||||
each receive one baseline.
|
||||
|
||||
The count loop is at `pseudo-C:426821-426871`, including the critical
|
||||
`ST_DOUBLE` second addition at `426857-426858`. Every reference is an index
|
||||
into the same surface-indexed `MeshBatchType` array. There is no texture key,
|
||||
surface DID, stippling, or texture-format grouping in this ownership decision.
|
||||
|
||||
### 3.4 Side/copy branch table
|
||||
|
||||
The emission loops at `pseudo-C:427047-427194`, confirmed instruction-for-
|
||||
instruction in Ghidra, reduce to this table:
|
||||
|
||||
| `sides_type` | Side ordinal | Copy ordinal | Surface | UV map | Normal sign | Fan index order |
|
||||
|---:|---:|---:|---|---|---:|---|
|
||||
| `0` | `0` | `0` | `pos_surface` | positive | `+1` | `[0, t+1, t+2]` |
|
||||
| `1` | `0` | `0` | `pos_surface` | positive | `+1` | `[0, t+1, t+2]` |
|
||||
| `1` | `0` | `1` | `pos_surface` | positive | `-1` | `[t+2, t+1, 0]` |
|
||||
| `2` | `0` | `0` | `pos_surface` | positive | `+1` | `[0, t+1, t+2]` |
|
||||
| `2` | `1` | `0` | `neg_surface` | negative | `-1` | `[0, t+1, t+2]` |
|
||||
|
||||
The counterintuitive last row is binding. Retail reverses indices when the
|
||||
**copy ordinal** is nonzero (`pseudo-C:427140-427145`). `ST_BOTH` changes the
|
||||
side ordinal, not the copy ordinal, so its negative candidate receives a
|
||||
negative normal but retains the base fan order. Do not normalize it to the
|
||||
more intuitive reversed winding.
|
||||
|
||||
Unknown raw `sides_type` values follow the single-side shape in retail's
|
||||
branching, but the installed data and header define only 0/1/2. The port
|
||||
should retain the raw value for evidence and quarantine/report invalid source
|
||||
data rather than silently invent a fourth public semantic.
|
||||
|
||||
### 3.5 UV absence and vertex identity
|
||||
|
||||
Retail builds an index map around:
|
||||
|
||||
```text
|
||||
(normal-sign lane, UV index, authored vertex id)
|
||||
```
|
||||
|
||||
More exactly, positive vertices address `(uvIndex * numVertices) + vertexId`.
|
||||
The inverted-normal lane adds the maximum-UV-span offset before that product
|
||||
(`pseudo-C:426881-426924`, `427129-427167`). Therefore:
|
||||
|
||||
- the same authored vertex and UV may be shared within one sign lane;
|
||||
- positive- and negative-normal copies cannot alias;
|
||||
- a null UV map uses UV index `0`, rather than suppressing the candidate;
|
||||
- `copyVert @0x0059C080` writes zero UV coordinates when the UV pointer is
|
||||
absent or the index is out of range (`pseudo-C:424797-424829`);
|
||||
- `copyVert` multiplies the authored normal by the selected `+1/-1`
|
||||
(`424791-424793`).
|
||||
|
||||
This is the exact reason NoPos/NoNeg cannot be treated as face-absence flags.
|
||||
|
||||
### 3.6 Attribute/subset ordering
|
||||
|
||||
After filling the mesh, retail scans `MeshBatchType` records from surface
|
||||
index zero upward (`pseudo-C:427214-427276`). For every record with a nonzero
|
||||
triangle count it appends one D3DX attribute range:
|
||||
|
||||
```text
|
||||
AttribId = surfaceIndex
|
||||
FaceStart = FirstTriangleIndex
|
||||
FaceCount = NumTriangles
|
||||
VertexStart = FirstVertexIndex
|
||||
VertexCount = LastVertexIndex - FirstVertexIndex + 1
|
||||
```
|
||||
|
||||
Consequences for the port:
|
||||
|
||||
1. nonempty subset order is ascending source surface index;
|
||||
2. duplicate surface DIDs in distinct slots remain distinct material owners;
|
||||
3. different stippling values on the same surface slot aggregate into one
|
||||
subset and one final mask byte;
|
||||
4. polygon triangles within a surface retain source polygon order;
|
||||
5. texture atlas/format grouping may optimize storage, but must not become
|
||||
the semantic subset/order owner.
|
||||
|
||||
## 4. Exact built-EnvCell draw admission
|
||||
|
||||
`RenderDeviceD3D::DrawEnvCell @0x0059F170` has two branches.
|
||||
|
||||
The built branch performs static-light color setup and calls
|
||||
`D3DPolyRender::DrawMesh(num_surfaces, surfaces, constructed_mesh, 1)`
|
||||
(`pseudo-C:427902-427907`). The fallback appends all structure polygons to the
|
||||
legacy polygon list (`427910-427932`). This OH2 contract targets the built
|
||||
branch used by prepared CellStruct meshes; the fallback is explicitly outside
|
||||
this slice.
|
||||
|
||||
`D3DPolyRender::DrawMesh @0x0059D4A0` admits a subset immediately when:
|
||||
|
||||
```text
|
||||
skipNoTexture == 0 || (surface.Type & 6) != 0
|
||||
```
|
||||
|
||||
(`pseudo-C:426064`). `skipNoTexture` is initialized to 1 in this client and no
|
||||
countervailing production write was found. When the type test fails, the only
|
||||
fallback requires both `ObjBuildingOrBuildingPart == 0` and `arg4 == 0`
|
||||
(`426067-426075`). `DrawEnvCell` supplies `arg4 == 1`, so its exact rule is:
|
||||
|
||||
```text
|
||||
draw built EnvCell subset iff
|
||||
(Surface.Type & (BASE1_IMAGE | BASE1_CLIPMAP)) != 0
|
||||
```
|
||||
|
||||
Thus:
|
||||
|
||||
- `BASE1_SOLID` (`0x1`) is constructed but skipped;
|
||||
- `BASE1_SOLID | TRANSLUCENT` (`0x11`) is constructed but skipped;
|
||||
- `BASE1_IMAGE` (`0x2`) draws;
|
||||
- `BASE1_CLIPMAP` (`0x4`) draws;
|
||||
- NoPos/NoNeg has no role in draw admission;
|
||||
- ordinary GfxObj `DrawMesh(..., arg4=0)` has a different fallthrough and
|
||||
must not inherit the built-EnvCell filter by accident.
|
||||
|
||||
## 5. Current acdream behavior and exact differences
|
||||
|
||||
### 5.1 `MeshExtractor.PrepareCellStructMeshData`
|
||||
|
||||
Current production extraction begins at
|
||||
`src/AcDream.Content/MeshExtractor.cs:733`.
|
||||
|
||||
| Contract point | Current behavior | Retail behavior | Required change |
|
||||
|---|---|---|---|
|
||||
| Side existence | `hasPos = !NoPos`, `hasNeg = !NoNeg` (`:770-779`) | only `sides_type` selects candidates | remove UV-bit gating; resolve candidates from the branch table |
|
||||
| Surface draw test | intentionally approximated before surface resolution (`:757-769`) | resolve surface, then built-cell admit with `(type & 6) != 0` | move exact admission after surface resolution |
|
||||
| `ST_DOUBLE` geometry | positive twice, second inverted/reversed | same | retain, but drive through the common descriptor |
|
||||
| `ST_BOTH` geometry | positive then negative, inverted/no reverse | same only when `hasNeg` | retain exact winding; remove NoNeg suppression |
|
||||
| UV absence | candidate is dropped | candidate exists; null UV means UV index/coordinate zero | preserve candidate and apply zero-UV fallback |
|
||||
| Subset key | `TextureKey` includes surface DID, palette, **full stippling**, solid flag; then also raw `SidesType` (`:973-990`) | source surface-array index only | make surface slot/ordinal the semantic owner |
|
||||
| Ordering | dictionary grouped first by `(width,height,format)` | ascending source surface index | preserve a separate immutable semantic order |
|
||||
| Duplicate slot | equal DID/key/sides may merge | distinct slots remain distinct subsets | retain source surface index in batch record |
|
||||
| Mask | only derived presentation booleans survive | exact initial/final mask per surface | store raw type plus retail mask (or a lossless equivalent) |
|
||||
| GPU cull | raw `SidesType` is stored in `TextureBatchData.CullMode` | geometry is expanded, then cell shells use fixed retail CW cull | separate authored sides metadata from rasterizer state |
|
||||
|
||||
The current fan and sign shape is close, but that does not make the current
|
||||
batch contract equivalent. `TextureKey.Stippling` splits polygons that retail
|
||||
aggregates, while a repeated surface DID can merge slots retail keeps apart.
|
||||
|
||||
`src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs:421-434` already records
|
||||
the later raster rule correctly: `Polygon.SidesType` is not GPU culling, and
|
||||
expanded cell geometry uses fixed retail clockwise culling. OH2 must make the
|
||||
prepared record express this distinction instead of continuing to overload
|
||||
`TextureBatchData.CullMode`.
|
||||
|
||||
### 5.2 Divergent Core helper
|
||||
|
||||
`src/AcDream.Core/Meshing/CellMesh.cs` carries the same NoPos approximation
|
||||
and only emits a positive side. It cannot remain an independent production
|
||||
interpretation. The exact side descriptor must either be used there too, or
|
||||
that helper's render-production role must be deleted. Collision code may keep
|
||||
its own explicitly documented source needs; it must not silently borrow
|
||||
render-side draw admission.
|
||||
|
||||
`src/AcDream.Core/Meshing/RetailUntexturedSurfacePolicy.cs` already has the
|
||||
correct surface fact: untextured means neither Base1Image nor Base1ClipMap.
|
||||
The defect is when and how CellStruct extraction applies it.
|
||||
|
||||
### 5.3 AP-234
|
||||
|
||||
`docs/architecture/retail-divergence-register.md:260` records precisely this
|
||||
approximation. The implementation commit must retire AP-234 with the retail
|
||||
addresses and the new tests, rather than merely changing its prose.
|
||||
|
||||
## 6. Installed-DAT before-state facts
|
||||
|
||||
The existing read-only `tools/A8CellAudit` was run against the requested
|
||||
canonical cells. Its `emitPos`/`skipped` columns report the **current NoPos
|
||||
proxy**, not retail truth:
|
||||
|
||||
| Cell | Environment | Struct | Polygons | Current positive proxy | Current NoPos skips | `ST_DOUBLE` polygons |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| `0xF4180100` | `0x0D00049D` | 0 | 20 | 18 | 2 | 0 |
|
||||
| `0xF4180101` | `0x0D00049D` | 1 | 45 | 43 | 2 | 0 |
|
||||
| `0xF4180104` | `0x0D00049E` | 1 | 41 | 36 | 5 | 8 |
|
||||
| `0xF4180106` | `0x0D00049F` | 0 | 79 | 76 | 3 | 0 |
|
||||
| `0xF4180107` | `0x0D00049F` | 1 | 84 | 78 | 6 | 0 |
|
||||
| `0xF4180112` | `0x0D0004A3` | 0 | 16 | 13 | 3 | 0 |
|
||||
| `0xF4180113` | `0x0D0004A3` | 1 | 7 | 3 | 4 | 0 |
|
||||
| `0xF4180114` | `0x0D0004A3` | 2 | 33 | 28 | 5 | 0 |
|
||||
| `0x8A02015E` | `0x0D00003A` | 0 | 19 | 14 | 5 | 0 |
|
||||
| `0x8A02015F` | `0x0D000044` | 0 | 9 | 7 | 2 | 0 |
|
||||
|
||||
For all canonical cells, every currently skipped NoPos polygon resolves to an
|
||||
untextured `Surface.Type == 0x11` (`BASE1_SOLID | TRANSLUCENT`). Therefore the
|
||||
exact path constructs those candidates and then skips their subsets at the
|
||||
built-EnvCell draw gate; visible admission is unchanged.
|
||||
|
||||
`0xF4180104` is an important geometry pin. Polygons `0x001C..0x0023` are
|
||||
`ST_DOUBLE` over surface slot 5, Surface DID `0x08000BFF`, type
|
||||
`BASE1_CLIPMAP (0x4)`. Retail emits 36 ordinary positive candidates plus 8
|
||||
reversed negative-normal copies: 44 drawable side calls, not the audit's
|
||||
36-positive-only headline.
|
||||
|
||||
Representative canonical surfaces:
|
||||
|
||||
- textured image (`0x2`): `0x08000BFB`, `0x08000ACC`, `0x0800055C`,
|
||||
`0x08000C00`, `0x08000BFC`, `0x08000377`, `0x08000379`;
|
||||
- clip map (`0x4`): `0x08000BFF`;
|
||||
- untextured solid/translucent (`0x11`): `0x08000C0F`, `0x08000C10`,
|
||||
`0x080000DD`, `0x08000034`.
|
||||
|
||||
The audit's complete Environment CellStruct catalog reported:
|
||||
|
||||
```text
|
||||
environments=772 cellStructs=3168 polygons=38189 triangles=70091 degenerate=861
|
||||
sides: ST_SINGLE=37843, ST_DOUBLE=346, ST_BOTH=0
|
||||
orientation: aligned=55347 opposed=4537 ambiguous=9346 inconsistentReliableFans=2
|
||||
```
|
||||
|
||||
No installed Environment CellStruct exercises `ST_BOTH`; its behavior is
|
||||
nevertheless fully implemented by retail and must be pinned synthetically.
|
||||
|
||||
An additional deterministic exploratory sample enumerated 734,976 candidate
|
||||
EnvCell ids, sorted the present ids, and inspected every 89th cell (8,259
|
||||
cells):
|
||||
|
||||
```text
|
||||
polygons=97020 surfaces=585
|
||||
ST_SINGLE=96922 ST_DOUBLE=98 ST_BOTH=0
|
||||
NoPos + textured surface = 0
|
||||
not-NoPos + untextured surface = 55
|
||||
```
|
||||
|
||||
The 55 `not-NoPos + untextured` cases are concrete current false admissions.
|
||||
Examples include:
|
||||
|
||||
- `0x00690114`, environment `0x0D0000E4`, struct 0, polygon 0,
|
||||
surface `0x080000DF`, type `0x1`;
|
||||
- `0x006A0109`, environment `0x0D0000C8`, struct 3, polygons `0x0..0xC`,
|
||||
surface `0x080000DF`, type `0x1`;
|
||||
- `0x008D018B`, environment `0x0D0000A0`, struct 0, polygons 0/1,
|
||||
surface `0x080000DF`;
|
||||
- `0x0090010D`, environment `0x0D000044`, polygons `0x0..0x6`,
|
||||
surface `0x080000DF`;
|
||||
- `0x6F9D010B`, environment `0x0D000453`, struct 1, polygons `0x0..0x4`,
|
||||
surface `0x08000139`, type `0x1`.
|
||||
|
||||
This stride sample is evidence of the defect class, not a substitute for the
|
||||
full installed-DAT post-implementation gate.
|
||||
|
||||
## 7. Required deterministic manifest
|
||||
|
||||
OH2 needs a tool-owned evidence manifest that can prove source-to-prepared
|
||||
equivalence without render-thread DAT reads. JSON is appropriate for the
|
||||
evidence artifact; production `acdream.pak` should carry only the minimum
|
||||
lossless subset metadata required at runtime.
|
||||
|
||||
### 7.1 Header/source identity
|
||||
|
||||
Required fields:
|
||||
|
||||
- manifest schema version;
|
||||
- bake recipe version;
|
||||
- Portal/Cell/HighRes DAT iteration and SHA-256 identity;
|
||||
- EnvCell id;
|
||||
- Environment DID;
|
||||
- CellStruct index;
|
||||
- exact cell transform as raw float bits;
|
||||
- ordered surface override array, each entry containing:
|
||||
- source surface slot/index;
|
||||
- raw `uint16` override;
|
||||
- qualified Surface DID;
|
||||
- raw `Surface.Type` `uint32`;
|
||||
- initial retail mask byte;
|
||||
- `drawsUnderBuiltEnvCell = ((type & 6) != 0)`.
|
||||
|
||||
### 7.2 Per-polygon source and candidate trace
|
||||
|
||||
For every polygon in exact source ordinal:
|
||||
|
||||
- ordinal and `poly_id`;
|
||||
- `num_pts` and ordered vertex ids;
|
||||
- raw stippling byte and decoded flag names;
|
||||
- raw `sides_type` integer;
|
||||
- positive and negative surface index, qualified DID, and raw type;
|
||||
- presence/absence of each UV map and ordered raw UV-index bytes;
|
||||
- ordered candidate list, with:
|
||||
- candidate ordinal;
|
||||
- source side (`positive`/`negative`);
|
||||
- duplicate-copy ordinal;
|
||||
- surface index/DID/type;
|
||||
- normal sign;
|
||||
- reverse-winding boolean;
|
||||
- `constructedByRetail`;
|
||||
- `drawsUnderBuiltEnvCell`;
|
||||
- explicit skip/failure reason;
|
||||
- exact fan source indices and emitted index order;
|
||||
- vertex-dedupe tuples `(vertexId, uvIndex, signLane)`;
|
||||
- emitted position/normal/UV float bits or a canonical hash over them.
|
||||
|
||||
Allowed reason taxonomy should be finite and tested: degenerate fan, invalid
|
||||
surface index, missing Surface DAT, untextured built-EnvCell draw skip, missing
|
||||
dependency. Retail assumes valid surface indices; corrupt/missing data is an
|
||||
extractor error/quarantine outcome, not license for an alternate rendering
|
||||
algorithm.
|
||||
|
||||
### 7.3 Per-surface subset trace
|
||||
|
||||
For every source surface slot:
|
||||
|
||||
- surface index and material owner identity;
|
||||
- initial mask and final mask after positive-surface stippling OR;
|
||||
- constructed triangle count;
|
||||
- drawable triangle count under built-EnvCell admission;
|
||||
- first triangle, triangle count, first vertex, last vertex;
|
||||
- final nonempty attribute/subset ordinal.
|
||||
|
||||
The final manifest hash must exclude pointers, Vulkan handles, upload offsets,
|
||||
timestamps, thread scheduling, and dictionary hash order.
|
||||
|
||||
## 8. Prepared package and migration impact
|
||||
|
||||
### 8.1 Current records
|
||||
|
||||
`src/AcDream.Content/ObjectMeshData.cs:146-157` currently gives
|
||||
`TextureBatchData` texture identity/data, indices, a DRW `CullMode`, derived
|
||||
translucency booleans, and wrapping-UV state. It has no source surface index,
|
||||
raw surface type, retail mask, or immutable subset ordinal.
|
||||
|
||||
`src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs:211-288` serializes that
|
||||
record inside the common prepared-mesh format. `PakFormat` currently declares:
|
||||
|
||||
```text
|
||||
CurrentFormatVersion = 2
|
||||
CurrentBakeToolVersion = 7
|
||||
```
|
||||
|
||||
Launcher independently pins recipe 7 at
|
||||
`LauncherInstallRecordStore.cs:37-41`; `ContentMigrationCatalog.cs:38-57`
|
||||
contains the recipe-step ledger through 7.
|
||||
|
||||
### 8.2 Minimum production record change
|
||||
|
||||
The prepared cell subset needs, directly or through an equally lossless typed
|
||||
record:
|
||||
|
||||
- immutable source surface index / semantic subset ordinal;
|
||||
- raw `Surface.Type`;
|
||||
- final retail mask byte;
|
||||
- authored raw `sides_type` only where evidence/validation needs it;
|
||||
- a separate fixed cell-shell raster cull state, rather than treating authored
|
||||
`sides_type` as GPU culling.
|
||||
|
||||
Texture payloads may remain deduplicated by texture identity. Indices and
|
||||
material/subset ownership must be recoverable in exact source-surface order;
|
||||
grouping them only by `(width,height,format)` is insufficient.
|
||||
|
||||
### 8.3 Version decision
|
||||
|
||||
**Decision: keep package format 2; bump bake recipe 7 → 8.**
|
||||
|
||||
Rationale:
|
||||
|
||||
- Pak header/directory framing and asset-kind identity do not change.
|
||||
- The prepared-mesh payload bytes and extraction recipe do change.
|
||||
- Production `PakPreparedAssetSource` validates the recipe before payload
|
||||
deserialization; bare `PakReader` is a lower-level framing reader.
|
||||
- Existing project precedent uses recipe bumps for prepared-record additions
|
||||
without changing the container format.
|
||||
- Every old EnvCell mesh is semantically stale, and the common mesh serializer
|
||||
changes, so an overlay migration cannot prove completeness.
|
||||
|
||||
Required migration work:
|
||||
|
||||
1. `PakFormat.CurrentBakeToolVersion = 8`.
|
||||
2. `LauncherInstallRecordStore.CurrentBakeToolVersion = 8` in lockstep.
|
||||
3. Add `ContentMigrationCatalog` step `7 → 8`, `FullRebuild`, with a reason
|
||||
naming exact CellStruct surface-index subset construction.
|
||||
4. Rebuild all prepared content; do not attempt an EnvCell-only overlay when
|
||||
the shared mesh record schema changed.
|
||||
5. Preserve `CurrentFormatVersion = 2` and add a test explicitly proving that
|
||||
this is intentional.
|
||||
|
||||
## 9. Smallest exact implementation boundary
|
||||
|
||||
The smallest implementation that closes the contract without importing DAT
|
||||
logic onto the render thread is:
|
||||
|
||||
1. **Pure Core descriptor.** Add one allocation-free/testable function that
|
||||
maps raw `sides_type` to the ordered candidate descriptors in §3.4. A
|
||||
descriptor carries source side, copy ordinal, normal sign, and reverse-
|
||||
winding. It does not resolve DATs or decide draw admission.
|
||||
2. **Content surface resolution.** In
|
||||
`MeshExtractor.PrepareCellStructMeshData`, resolve the candidate's source
|
||||
surface slot and raw Surface before deciding anything. NoPos/NoNeg select
|
||||
null/zero UV fallback only.
|
||||
3. **Surface-index subsets.** Aggregate candidates by the source surface index,
|
||||
update the exact retail mask, preserve source polygon order, and emit
|
||||
nonempty subsets in ascending source surface index. Apply the built-EnvCell
|
||||
`(type & 6) != 0` admission explicitly; retaining skipped construction in
|
||||
the manifest is sufficient if production chooses not to serialize vertices
|
||||
that can never draw.
|
||||
4. **Cull separation.** Preserve fixed retail cell-shell GPU culling after
|
||||
geometry expansion. Do not use `Polygon.SidesType` as the rasterizer state.
|
||||
5. **One interpretation.** Route `CellMesh.Build` through the same descriptor
|
||||
and policy, or remove its rendering production role.
|
||||
6. **Prepared schema.** Add the minimum source-surface/type/mask/order fields
|
||||
described in §8 and round-trip them through `ObjectMeshDataSerializer`.
|
||||
7. **Recipe/migration.** Apply the recipe-8 full rebuild and retire AP-234 in
|
||||
the same behavioral slice.
|
||||
|
||||
This boundary deliberately does **not** redesign portal views, shell ordering,
|
||||
depth lifecycle, landscape streaming, transparency passes, or camera/view
|
||||
selection. Those are other OVERHAUL slices.
|
||||
|
||||
## 10. Required test matrix
|
||||
|
||||
### 10.1 Pure construction tests
|
||||
|
||||
Create synthetic CellStruct fixtures covering the Cartesian product of:
|
||||
|
||||
- sides `0`, `1`, `2`;
|
||||
- positive/negative UV present and absent (`NoPos`, `NoNeg`);
|
||||
- surface types `0x1`, `0x2`, `0x4`, `0x11`.
|
||||
|
||||
Pin:
|
||||
|
||||
- exact candidate count and order;
|
||||
- exact fan index order;
|
||||
- `ST_DOUBLE` second-copy reversal and negative normal;
|
||||
- `ST_BOTH` negative side **without** reversal;
|
||||
- null UV → UV index/coordinates zero, not candidate removal;
|
||||
- sign-lane vertex separation and same-lane dedupe;
|
||||
- clip-map initial mask `8`;
|
||||
- alpha/invalpha/additive mask precedence `2`;
|
||||
- translucent mask `4`;
|
||||
- signed-byte positive (`> 0`) positive-surface stippling ORs mask bit `1`;
|
||||
- negative stippling does not update the negative surface mask;
|
||||
- untextured surfaces are constructed but fail built-EnvCell admission;
|
||||
- textured NoPos surfaces draw with zero UVs.
|
||||
|
||||
### 10.2 Subset/material tests
|
||||
|
||||
- two distinct surface slots with the same Surface DID remain two subsets;
|
||||
- polygons on one slot with different stippling values remain one subset and
|
||||
aggregate the mask;
|
||||
- polygons on one slot with different sides values remain one subset;
|
||||
- subset order follows ascending source surface index, independent of texture
|
||||
format and dictionary insertion/hash order;
|
||||
- source polygon order within a subset is stable;
|
||||
- fixed retail CW raster cull remains in force after geometry expansion.
|
||||
|
||||
### 10.3 Canonical evidence tests
|
||||
|
||||
- generate bit-identical manifests twice for all ten requested cells;
|
||||
- pin `0xF4180104`'s eight `ST_DOUBLE` clip-map polygons and 44 drawable side
|
||||
calls;
|
||||
- pin the canonical NoPos surfaces as type `0x11` and verify constructed-then-
|
||||
skipped reasoning;
|
||||
- require zero unexplained old/new geometry deltas.
|
||||
|
||||
### 10.4 Installed-DAT scan
|
||||
|
||||
Run the complete installed Environment/EnvCell corpus, not the exploratory
|
||||
stride:
|
||||
|
||||
- every old/new admission delta must have the exact `Surface.Type & 6`
|
||||
explanation;
|
||||
- the known `not-NoPos + untextured` examples must be corrected;
|
||||
- any textured NoPos or ST_BOTH record, if another installed DAT set contains
|
||||
one, must produce the retail candidate/UV behavior;
|
||||
- missing/corrupt dependencies must be explicitly counted and identified.
|
||||
|
||||
### 10.5 Package/launcher tests
|
||||
|
||||
- `TextureBatchData`/new subset record serializer round-trip;
|
||||
- truncated/corrupt new fields are rejected deterministically;
|
||||
- recipe-7 package is rejected by recipe-8 consumers before payload decode;
|
||||
- launcher resolves `7 → 8` to a full rebuild;
|
||||
- older-to-8 composed migrations remain full rebuild;
|
||||
- package container format remains exactly 2;
|
||||
- focused Release suites: Core construction, Content extraction/serializer,
|
||||
Bake package, Launcher.Core migration, App prepared-mesh consumption.
|
||||
|
||||
## 11. Unresolved facts and handoff cautions
|
||||
|
||||
1. The installed Environment CellStruct catalog contains no `ST_BOTH`; its
|
||||
exact code path is proved by two decompilers but needs synthetic coverage.
|
||||
2. The implementation must decide the final production type name and whether
|
||||
all manifest fields stay tool-only. The evidence schema above is binding;
|
||||
the pak should store only fields necessary to preserve semantics.
|
||||
3. OH7's eventual immutable ordered draw stream must consume, not erase, the
|
||||
source-surface subset order established here. Atlas grouping is storage,
|
||||
not semantic ownership.
|
||||
4. The non-built `DrawEnvCell` polygon-list fallback is a separate retail path
|
||||
and is not specified here.
|
||||
5. The canonical cells produce no visible admission delta under the exact
|
||||
surface rule. Do not claim OH2 alone resolves the cathedral leak/flicker.
|
||||
6. Run a complete installed-DAT scan after implementation; the deterministic
|
||||
stride sample is only a before-state signal.
|
||||
|
||||
## 12. Binding implementation checklist
|
||||
|
||||
- [ ] side candidates come only from `sides_type`;
|
||||
- [ ] NoPos/NoNeg mean UV absence only;
|
||||
- [ ] `ST_DOUBLE` second fan reverses; `ST_BOTH` negative fan does not;
|
||||
- [ ] normal sign is represented independently of index winding;
|
||||
- [ ] subset/material owner is source surface-array index;
|
||||
- [ ] subset order is ascending source surface index;
|
||||
- [ ] mask initialization and positive stippling OR are exact;
|
||||
- [ ] built EnvCell draw admission is `(Surface.Type & 6) != 0`;
|
||||
- [ ] ordinary GfxObj behavior is unchanged;
|
||||
- [ ] authored sides type is not used as GPU raster cull;
|
||||
- [ ] Core and Content do not retain divergent CellStruct interpretations;
|
||||
- [ ] no render-thread DAT lookup is introduced;
|
||||
- [ ] `acdream.pak` recipe becomes 8, format remains 2, migration is full;
|
||||
- [ ] AP-234 is retired with retail addresses and test evidence;
|
||||
- [ ] canonical and complete installed-DAT manifests explain every delta.
|
||||
Loading…
Add table
Add a link
Reference in a new issue