docs(render): define OH1 retail world contract
This commit is contained in:
parent
5cd4fd2c71
commit
5d907ae9ad
7 changed files with 2040 additions and 39 deletions
205
docs/research/2026-09-01-overhaul/oh1-alpha-list-contract.md
Normal file
205
docs/research/2026-09-01-overhaul/oh1-alpha-list-contract.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# OH1 evidence — retail alpha classification and list contract
|
||||
|
||||
**Campaign:** OVERHAUL / OH1
|
||||
|
||||
**Status:** evidence complete; behavior belongs to OH7
|
||||
|
||||
**Retail build:** September 2013 End of Retail
|
||||
|
||||
**Arbitration:** named-retail pseudo-C plus Ghidra/disassembly for field offsets
|
||||
|
||||
This note closes the bounded alpha ambiguity left by the first OH1 synthesis.
|
||||
It is an executable contract for OH7, not permission to change alpha order
|
||||
before OH5 restores the landscape/building/object walk that owns the barriers.
|
||||
|
||||
## 1. Retail anchors
|
||||
|
||||
| Retail symbol | Address | Contract established here |
|
||||
|---|---:|---|
|
||||
| `D3DPolyRender::ConstructMesh` | `0x0059DFA0` | initial/final subset mask |
|
||||
| `D3DPolyRender::DrawMesh` | `0x0059D4A0` | immediate versus delayed branch table |
|
||||
| `D3DPolyRender::AddMeshToAlphaList` | `0x0059C230` | list selection, capacity, entry state |
|
||||
| `D3DPolyRender::FlushAlphaList` | `0x0059D2E0` | threshold and drain order |
|
||||
| `D3DPolyRender::RenderMeshSubset` | `0x0059CA10` | delayed replay/detail behavior |
|
||||
| `D3DPolyRender::SetSurface` | `0x0059C4D0` | clip-map override behavior |
|
||||
| `D3DPolyRender::Begin` | `0x0059EAD0` | delay-mask registry binding |
|
||||
| `RenderDeviceD3D::DrawEnvCell` | `0x0059F170` | environment-detail owner |
|
||||
| `RenderDeviceD3D::DrawBuilding` | `0x0059F2A0` | building-detail owner and barrier |
|
||||
| `RenderDeviceD3D::DrawPartCell` | `0x005A07A0` | ordinary-object detail reset |
|
||||
| `RenderDeviceD3D::DrawBlock` | `0x005A18D0` | per-land-cell emergency barrier |
|
||||
| `PView::DrawCells` | `0x005A4840` | post-landscape full barrier |
|
||||
| `SmartBox::RenderNormalMode` | `0x00453AA0` | top-level full barrier |
|
||||
| `LScape::CleanupDetailSurfaces` | `0x00504AE0` | detail-surface release/reset |
|
||||
| `LScape::SetDetailTexturing` | `0x00506B40` | detail-surface regeneration |
|
||||
| `LScape::ChangeRegion` | `0x00506CB0` | detail preference application |
|
||||
| `GameSky::Draw` | `0x00506FF0` | `m_currentlyDrawingSky` producer |
|
||||
| `Render::SetOverallGraphicsQuality` | `0x0054B020` | `MultiPassAlpha` quality defaults |
|
||||
| `Render::Startup` | `0x0054E9B0` | preference registration/safe override |
|
||||
|
||||
The named-retail pseudo-C establishes the control flow. Ghidra and instruction
|
||||
disassembly arbitrate the two facts for which a wrong field name changes the
|
||||
branch meaning: the renderer byte at `+0x7E0` is
|
||||
`m_currentlyDrawingSky`, while material-alpha mode is at `+0x794`; and polygon
|
||||
stippling is compared as a **signed** byte.
|
||||
|
||||
## 2. Exact constructed subset mask
|
||||
|
||||
For each source surface, `ConstructMesh` chooses exactly one initial mask by
|
||||
this priority:
|
||||
|
||||
```text
|
||||
if (Surface.Type & 0x00010300) != 0: mask = 0x02
|
||||
else if (Surface.Type & 0x00000004) != 0: mask = 0x08
|
||||
else if (Surface.Type & 0x00000010) != 0: mask = 0x04
|
||||
else: mask = 0x00
|
||||
```
|
||||
|
||||
`0x00010300` is the union of `ALPHA (0x100)`, `INVALPHA (0x200)`, and
|
||||
`ADDITIVE (0x10000)`. The order is material: a surface carrying both an
|
||||
alpha-family bit and `BASE1_CLIPMAP` receives mask `0x02`, not `0x08`.
|
||||
|
||||
For each polygon, retail then ORs bit `0x01` into the **positive surface's**
|
||||
mask only when `(signed char)polygon.stippling > 0`. Raw bytes `0x80..0xFF`
|
||||
therefore do not set the bit. The complete constructed mask domain is:
|
||||
|
||||
| Surface class | Without positive stippling | With positive stippling |
|
||||
|---|---:|---:|
|
||||
| plain | `0x00` | `0x01` |
|
||||
| alpha / inverse-alpha / additive | `0x02` | `0x03` |
|
||||
| translucent | `0x04` | `0x05` |
|
||||
| clip map | `0x08` | `0x09` |
|
||||
|
||||
Bit `0x01` is absent from the registry help text, but a configured delay mask
|
||||
still intersects it normally. It is a retail quirk, not a flag to normalize.
|
||||
|
||||
## 3. Delay-mask and preference state
|
||||
|
||||
`s_AlphaDelayMask @ 0x00820D88` has static default `0x0E`. Its only behavior
|
||||
read is in `DrawMesh`; `D3DPolyRender::Begin` exposes it as
|
||||
`RenderD3D.AlphaDelayMask` with help for Alpha=`2`, Translucent=`4`, and
|
||||
ClipMap=`8`. The registry may replace the value. Unsupported higher bits can
|
||||
keep the outer delay gate nonzero but never intersect a constructed mask.
|
||||
|
||||
`m_RenderPrefs.MultiPassAlpha @ 0x0081EF96` has static default false.
|
||||
`SetOverallGraphicsQuality` selects false for quality levels 1–3 and true for
|
||||
levels 4–5. Startup registers the user preference, and the safe-render branch
|
||||
forces it false. `DrawMesh` is its sole draw-path reader;
|
||||
`DetermineOverallGraphicsQuality` also reads it while deriving the aggregate
|
||||
quality setting.
|
||||
|
||||
The building and environment detail globals default null. Their only writers
|
||||
are `SetBuildingDetailSurface` and `SetEnvironmentDetailSurface`; region
|
||||
cleanup clears/releases them, and region detail setup regenerates both from
|
||||
the `EnvironmentDetailTextures` preference. Generation may still yield null.
|
||||
|
||||
## 4. Exact `DrawMesh` branch table
|
||||
|
||||
After the subset passes its ordinary draw-eligibility gate, apply these rows
|
||||
in order:
|
||||
|
||||
| Priority | Condition | Result |
|
||||
|---:|---|---|
|
||||
| 1 | `m_currentlyDrawingSky` **or** delay mask is zero **or** `curr_detail_surface != null` | render immediately; enqueue nothing |
|
||||
| 2 | `MultiPassAlpha && (subsetMask & 0x08) != 0` | append to **CLIP** with `overrideClipmap=true`, then also render immediately with `overrideClipmap=false` |
|
||||
| 3 | `(delayMask & subsetMask) != 0` | append to **CLIP** iff bit `0x08` is set, otherwise **ALPHA**; no immediate draw |
|
||||
| 4 | `(delayMask & 0x04) != 0 && currentMaterial != null && currentMaterial.has_alpha != 0` | append to **ALPHA**; no immediate draw |
|
||||
| 5 | otherwise | render immediately |
|
||||
|
||||
Row 2 depends on the global delay mask being nonzero because it is inside the
|
||||
outer delay gate, but it does **not** require delay-mask bit `0x08`. It is the
|
||||
clip-map base pass now plus an alpha-blended second pass later.
|
||||
|
||||
Under default delay mask `0x0E`:
|
||||
|
||||
- masks `0x02/0x03` and `0x04/0x05` enter ALPHA;
|
||||
- masks `0x08/0x09` enter CLIP, with the additional immediate pass when
|
||||
`MultiPassAlpha` is true;
|
||||
- masks `0x00/0x01` are immediate unless the material-alpha fallback applies.
|
||||
|
||||
`overrideClipmap=false` lets `SetSurface` enable the clip-map alpha-test arm.
|
||||
`overrideClipmap=true` selects the alpha-blend arm and suppresses that clip-map
|
||||
alpha-test arm. Immediate rendering passes
|
||||
`detailEnabled=(curr_detail_surface != null)`; delayed replay always passes
|
||||
`detailEnabled=false`.
|
||||
|
||||
## 5. List entry, capacity, and replay
|
||||
|
||||
Retail owns two independent static arrays of 3000 `AlphaListEntry` records and
|
||||
two `uint16` counts. `AddMeshToAlphaList` appends strictly FIFO; it performs no
|
||||
distance or material sort.
|
||||
|
||||
Each `DrawMesh` invocation owns two independent `first-for-this-list` flags.
|
||||
The first subset appended to each list captures the current `CMaterial` and
|
||||
object matrix and sets `bNewObject=true`. Later entries for that list inherit
|
||||
that state during replay. The entry also stores `overrideClipmap`; it stores no
|
||||
portal view, clip planes, cell, or part identity.
|
||||
|
||||
When a list is already at capacity, append returns false. A non-multipass
|
||||
delayed subset has no immediate fallback draw. Exact capacity behavior must be
|
||||
preserved and tested rather than silently expanded or recovered differently.
|
||||
|
||||
`FlushAlphaList(threshold)` returns false without changing either list only
|
||||
when **both** counts are below `threshold * 3000`. Otherwise it drains the
|
||||
entire CLIP list in append order, resets it, then drains the entire ALPHA list
|
||||
in append order and resets it. It restores the object matrix after replay; it
|
||||
does not restore the material. The DrawBlock threshold is the immutable global
|
||||
`0.75`, so equality at 2250 in either list triggers a full two-list drain.
|
||||
|
||||
## 6. Detail owner consequences
|
||||
|
||||
- `DrawEnvCell` installs the environment detail surface, calls the built
|
||||
`DrawMesh(skipNoTexture=true)` path, then clears the current detail. A
|
||||
non-null environment detail makes every eligible subset immediate.
|
||||
- `DrawBuilding` installs building detail, performs `FlushAlphaList(0)` before
|
||||
portal/part/shell work, then draws its forced portal-BSP pass and normal
|
||||
built shell. A non-null building detail makes the shell immediate.
|
||||
- `DrawBuildingLeaf` preserves outer state, but `DrawPartCell` explicitly
|
||||
forces current detail null. Ordinary leaf parts can therefore enqueue even
|
||||
when their containing building shell uses detail.
|
||||
- Ordinary object-cell GfxObj paths do not install the separate object detail
|
||||
surface; their current detail is normally null.
|
||||
|
||||
## 7. Exact normal-world flush barriers
|
||||
|
||||
1. `DrawBuilding` calls `FlushAlphaList(0)` after installing building detail
|
||||
and before any building portal/leaf/shell draw. Delayed replay itself has
|
||||
detail disabled.
|
||||
2. `DrawBlock` visits each admitted land cell as `DrawLandCell` followed by
|
||||
`DrawSortCell` (building, then objects), then calls
|
||||
`FlushAlphaList(0.75)`. This is normally an emergency capacity drain.
|
||||
3. `PView::DrawCells` calls `FlushAlphaList(0)` after outside landscape and
|
||||
before the mid-frame stamp/depth-clear/interior repaint block.
|
||||
4. `SmartBox::RenderNormalMode` calls `FlushAlphaList(0)` after the complete
|
||||
world path.
|
||||
|
||||
The fifth retail caller is the private `CreatureMode::Render @ 0x004529D0`
|
||||
(call site `0x00452BF0`). After `DrawObjCellForDummies(creature_cell)` and its
|
||||
frame pop, that private presentation mode calls `FlushAlphaList(0)`. It is not
|
||||
part of the normal-world OH5/OH7 ordering cutover, but it must remain an exact
|
||||
consumer of the shared two-list implementation.
|
||||
|
||||
Consequently, OH7 cannot be implemented faithfully while current acdream
|
||||
still stages all terrain before all building/object turns. OH5 must first
|
||||
restore the per-cell landscape interleave on which these barriers operate.
|
||||
|
||||
## 8. Required OH7 cutover
|
||||
|
||||
Current `RetailAlphaQueue` is a single scope-global, viewer-distance-sorted
|
||||
stream with an approximate `FlushFartherThan`. Retail instead requires:
|
||||
|
||||
- separate CLIP and ALPHA FIFO lists;
|
||||
- no queue-level sort;
|
||||
- preservation of upstream cell order, `CShadowPart` order, and authored
|
||||
subset order;
|
||||
- raw surface type plus the exact constructed mask and positive-surface
|
||||
stippling ownership;
|
||||
- `overrideClipmap` and independent per-list object-boundary state;
|
||||
- the multipass clip-map immediate-plus-delayed duplicate;
|
||||
- material `has_alpha` fallback;
|
||||
- sky/detail immediate gates and delayed `detailEnabled=false`;
|
||||
- exact capacity and threshold behavior;
|
||||
- full drains at the exact building, landscape, and top-level barriers.
|
||||
|
||||
Runtime state remains configurable: registry may override delay mask, the user
|
||||
preference may override MultiPassAlpha, and detail generation may return null.
|
||||
Those are input states to the exact branch table, not unresolved branch sense.
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
# OH1 evidence — retail built-mesh/view/stamp contract
|
||||
|
||||
Date: 2026-09-01
|
||||
|
||||
Binding worktree: `C:\Users\erikn\source\repos\acdream\.claude\worktrees\peaceful-blackburn-5333f0`
|
||||
|
||||
Branch / audited HEAD: `claude/campaign-w-retail-frame-walk` / `eaea8776620ad8b72f5f0aa44fbaffac6dde0a36`
|
||||
|
||||
Scope: evidence only. No production source, test, plan, or other research document was changed by this review.
|
||||
|
||||
## Verdict
|
||||
|
||||
Retail's portal views are **admission and portal-polygon-clip state**, not built-mesh triangle clip state.
|
||||
|
||||
The decisive contract is:
|
||||
|
||||
1. `Render::copy_view` builds one screen polygon plus one world plane per edge.
|
||||
2. `Render::set_view` installs one polygon's plane list and screen bounds in globals.
|
||||
3. `Render::viewconeCheck` classifies a **drawing sphere** against those planes.
|
||||
4. Once an ordinary built `CGfxObj` is admitted, `DrawMeshInternal` submits the **complete constructed mesh**. It does not pass the active portal polygon or its planes to `D3DPolyRender::DrawMesh`.
|
||||
5. A non-player `CPhysicsPart` is stamped at the first admitted view, before its mesh submission. Later admitted views and later cell memberships in the same render stamp do not resubmit it.
|
||||
6. A built `CEnvCell` stamps itself on the first `DrawEnvCell` call and submits its complete constructed shell. Later portal views and later flood encounters in that render stamp are no-ops.
|
||||
7. The local player's parts deliberately bypass the inner part stamp, so they can submit once for every admitted cell/view. This is the one ordinary built-object exception.
|
||||
8. Actual polygon clipping is used for portal-chain construction and portal depth fans/seals (`PView::GetClip` and `DrawPortalPolyInternal`, both through `ACRender::polyClipFinish`). It is not used on ordinary built mesh triangles.
|
||||
9. A delayed transparent subset is owned by the built-mesh submission that first admitted the part/cell. Every `AlphaListEntry` stores mesh/subset/surface/flags; the first entry for each list also stores material and world matrix under `bNewObject`, and later entries inherit that replay state. It stores **no portal view, clip-plane list, cell, or part identity**.
|
||||
|
||||
Therefore, multiple portal views do **not** mean multiple clipped copies of a normal built mesh. For non-player objects and EnvCells they mean “try views until one admits, then draw whole once.” Any implementation that submits one built mesh per portal slice, or hardware-clips an admitted built mesh to that slice, contradicts retail.
|
||||
|
||||
## Oracle and arbitration method
|
||||
|
||||
Named retail was read first:
|
||||
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
- `docs/research/named-retail/acclient.h`
|
||||
|
||||
Every admission, clipping, built/non-built, and stamp branch below was then arbitrated against the live Ghidra MCP on port 8081 using both:
|
||||
|
||||
- `http://127.0.0.1:8081/decompile_function?address=...`
|
||||
- `http://127.0.0.1:8081/disassemble_function?address=...`
|
||||
|
||||
The Ghidra checks matter here because several older notes inferred “one mesh draw per portal view” from the outer loop without following the inner stamp branch. The actual instruction order proves the opposite for non-player parts.
|
||||
|
||||
## Retail structures and state
|
||||
|
||||
Offsets below are 32-bit retail object offsets confirmed by the named header and the accessed addresses in Ghidra disassembly.
|
||||
|
||||
### View state
|
||||
|
||||
`portal_view_type` is `0x48` bytes:
|
||||
|
||||
| Offset | Field | Meaning |
|
||||
|---:|---|---|
|
||||
| `0x00` | `DArray<portal_info> portal` | per-portal `seen` / `inflag` state |
|
||||
| `0x10` | `view_type view` | shared polygon and vertex pools |
|
||||
| `0x34` | `float max_indist` | flood distance key |
|
||||
| `0x38` | `uint view_count` | installed polygon count |
|
||||
| `0x3c` | `int cell_view_done` | cell walk state |
|
||||
| `0x40` | `int view_timestamp` | view-generation stamp |
|
||||
| `0x44` | `int update_count` | view update count |
|
||||
|
||||
`view_type` is `0x24` bytes: `vertex_count_total @ +0x00`, `DArray<view_poly> @ +0x04`, `DArray<view_vertex> @ +0x14`.
|
||||
|
||||
`view_poly` is `0x18` bytes: `vertex_count`, `vertex_index`, `xmin`, `xmax`, `ymin`, `ymax` at `+0x00..+0x14`.
|
||||
|
||||
`view_vertex` is `0x18` bytes: `Vec2D pt @ +0x00` and `Plane plane @ +0x08`.
|
||||
|
||||
`PView` uses `outside_view @ +0x00`, `cell_draw_list.data @ +0x50`, `cell_draw_num @ +0x60`, and `lscape @ +0x78`.
|
||||
|
||||
### Built content and stamps
|
||||
|
||||
| Type | Relevant fields |
|
||||
|---|---|
|
||||
| `CPhysicsPart` | `draw_state +0x1c`, `gfxobj +0x20`, `gfxobj_scale +0x24`, `draw_pos +0x78`, `material +0xc0`, `surfaces +0xc4`, `m_current_render_frame_num +0xdc`, `physobj +0xe0` |
|
||||
| `CGfxObj` | `constructed_mesh +0x6c`, `use_built_mesh +0x70`, `drawing_sphere +0x90`, `drawing_bsp +0x94` |
|
||||
| `CEnvCell` | `num_surfaces +0xf8`, `surfaces +0xfc`, `structure +0x100`, `num_portals +0x108`, `portals +0x10c`, `constructed_mesh +0x128`, `use_built_mesh +0x12c`, `m_current_render_frame_num +0x130`, `num_view +0x134`, `portal_view +0x138` |
|
||||
| `RenderDevice` | `m_nFrameStamp +0xb0` |
|
||||
|
||||
`AlphaListEntry` is `0x54` bytes:
|
||||
|
||||
| Offset | Field |
|
||||
|---:|---|
|
||||
| `0x00` | `MeshBuffer *pMeshBuffer` |
|
||||
| `0x04` | `int surfaceNum` |
|
||||
| `0x08` | `CSurface *surface` |
|
||||
| `0x0c` | `CMaterial *material` |
|
||||
| `0x10` | `Matrix4 worldMatrix` (through `0x4f`) |
|
||||
| `0x50` | `bool bNewObject` |
|
||||
| `0x51` | `bool overrideClipmapSetting` |
|
||||
|
||||
There is no field for `portal_view_num`, `portal_vertex`, `portal_npnts`, a cell, a part, or a clip-plane mask.
|
||||
|
||||
## Function contract ledger
|
||||
|
||||
### `Render::copy_view` — `0x0054dfc0`
|
||||
|
||||
Purpose: append one portal view polygon and its edge planes to a `portal_view_type`.
|
||||
|
||||
Exact effects:
|
||||
|
||||
- If the source point array is null, append the full viewport quad `(0,H) (W,H) (W,0) (0,0)` plus the repeated closing vertex. The supplied point count is ignored.
|
||||
- Otherwise, perspective-divide every input point in place when `w != 1`, set `w = 1`, prune consecutive points within one pixel, prune collinear points including the closing wrap, reject fewer than three survivors, and cap the result at 31 vertices.
|
||||
- Rejection returns without incrementing `view_count`.
|
||||
- Success records bounds, stores a repeated closing point, unprojects the surviving screen points, creates one world plane per edge, and increments `view_count`.
|
||||
- The edge plane is constructed from the two eye rays and passes through `viewer_world_space.viewpoint`.
|
||||
|
||||
This function creates **view metadata**. It neither draws nor attaches the view to a mesh submission.
|
||||
|
||||
### `Render::set_view` — `0x0054d0e0`
|
||||
|
||||
Purpose: install one `view_poly` as global active view state.
|
||||
|
||||
Exact writes from Ghidra disassembly:
|
||||
|
||||
- `portal_view_num` (`0x008662dc`) = requested index
|
||||
- `portal_view` (`0x00846044`) = supplied `view_type *`
|
||||
- `portal_npnts` (`0x00846050`) = polygon vertex count
|
||||
- `portal_inmask` (`0x008661b8`) = `(1 << (portal_npnts + 1)) - 1`
|
||||
- `portal_vertex` (`0x00846040`) = the polygon's first `view_vertex`
|
||||
- `xmin/xmax/ymin/ymax` = polygon bounds
|
||||
|
||||
There is no render call, scissor installation, user clip-plane installation, or mesh mutation.
|
||||
|
||||
### `Render::obj_view_set` — `0x0054b9b0`
|
||||
|
||||
Purpose: transform the active world-space view edge planes, and the CY plane, into the current object frame.
|
||||
|
||||
It fills `Render::portal_obj_plane[]` from `portal_vertex[].plane` using `FrameCurrent`, and writes the object-local viewer CY plane. That state serves the legacy object/BSP/polygon machinery.
|
||||
|
||||
Crucial negative evidence:
|
||||
|
||||
- ordinary built `DrawMeshInternal(force=false)` does **not** call `obj_view_set`;
|
||||
- the built `D3DPolyRender::DrawMesh` overload has no plane/view argument;
|
||||
- `DrawEnvCell` calls `obj_view_set`, but its built branch still calls `D3DPolyRender::DrawMesh` directly with only surfaces and the constructed mesh;
|
||||
- the force/building branch calls `obj_view_set` because it walks `drawing_bsp` portals, not because it clips the constructed mesh.
|
||||
|
||||
The presence of `obj_view_set` is not evidence of GPU clipping of a built mesh.
|
||||
|
||||
### `Render::viewconeCheck` — `0x0054c250`
|
||||
|
||||
Purpose: sphere admission against the active view cone.
|
||||
|
||||
State effects:
|
||||
|
||||
- scale the authored sphere center and radius by `Render::object_scale`;
|
||||
- transform it through the current object/viewer frames;
|
||||
- publish `Render::local_object_center` and `Render::local_object_radius` before classification;
|
||||
- test the viewer CY plane, then every active edge plane.
|
||||
|
||||
Ghidra-arbitrated branch senses:
|
||||
|
||||
- CY reject at `0x0054c31b..0x0054c32e`: `distance < -radius` returns `OUTSIDE (0)`; equality is not rejected.
|
||||
- edge reject at `0x0054c37b..0x0054c3bd`: the same strict `distance < -radius` rule.
|
||||
- partial is inclusive: `distance <= radius` marks partial; `distance == +radius` is `PARTIALLY_INSIDE (1)`, not entirely inside.
|
||||
- only when every tested distance is `> radius` is the result `ENTIRELY_INSIDE (2)`.
|
||||
|
||||
This is a bounding-sphere classification. It does not clip any vertex or triangle.
|
||||
|
||||
`Render::SetObjectScale @ 0x0050d430` sets `object_scale` to the maximum of the part's X/Y/Z scale, matching the conservative sphere radius expansion used by the current port.
|
||||
|
||||
### `CPhysicsPart::Draw` — `0x0050d7a0`
|
||||
|
||||
Purpose: resolve the current degrade mesh and enter `RenderDeviceD3D::DrawMesh`.
|
||||
|
||||
Ghidra-arbitrated outer gates:
|
||||
|
||||
- `draw_state & 1` at `0x0050d7a3..0x0050d7a7`: set means skip.
|
||||
- ordinary call (`force == 0`) at `0x0050d7b8..0x0050d7c9`: if the part stamp equals the device stamp, skip.
|
||||
- force/building-portal call (`force != 0`) branches around that outer stamp comparison.
|
||||
- a null resolved degrade `CGfxObj` skips.
|
||||
|
||||
On entry to the device it installs current material, surfaces, maximum object scale, `s_current_physics_part`, and calls `DrawMesh(gfxobj, &draw_pos, force != 0)`. It clears `s_current_physics_part` afterward.
|
||||
|
||||
`CPhysicsPart::Draw` does not itself set the stamp. The first successful view admission sets it inside `DrawMeshInternal`.
|
||||
|
||||
### `RenderDeviceD3D::DrawMesh` — `0x005a0860`
|
||||
|
||||
Purpose: per-view sphere admission around `DrawMeshInternal`.
|
||||
|
||||
With `Render::PortalList == null`:
|
||||
|
||||
1. push the object position;
|
||||
2. call `viewconeCheck(drawing_sphere)` once;
|
||||
3. on non-`OUTSIDE`, run selection-ray state and call `DrawMeshInternal`;
|
||||
4. on `OUTSIDE`, call `DrawMeshInternal(force=true, OUTSIDE)` only for the force flavor.
|
||||
|
||||
With a portal list:
|
||||
|
||||
1. loop `i = 0 .. view_count-1`;
|
||||
2. skip indices excluded by `building_view` (`building_view == -1 || building_view == i` is the admission branch at `0x005a08c0..0x005a08d3`);
|
||||
3. `set_view(view, i)`;
|
||||
4. `viewconeCheck(drawing_sphere)`;
|
||||
5. call `DrawMeshInternal` on every non-`OUTSIDE` result;
|
||||
6. for `OUTSIDE`, call it only in force mode.
|
||||
|
||||
The selection-ray check runs once at the first admitted view. The loop can call `DrawMeshInternal` more than once, but the inner part stamp normally makes only the first admitted call submit a mesh.
|
||||
|
||||
### `RenderDeviceD3D::DrawMeshInternal` — `0x0059f360`
|
||||
|
||||
This function resolves both the duplicate-submission rule and the built/non-built rule.
|
||||
|
||||
#### Ordinary non-player stamp branch
|
||||
|
||||
Ghidra disassembly `0x0059f365..0x0059f38c`:
|
||||
|
||||
- force set -> bypass stamp;
|
||||
- no current physics part -> bypass stamp;
|
||||
- `IsPartOfPlayerObj` true -> bypass stamp;
|
||||
- otherwise, `GetDrawnThisFrame` true -> return immediately at `0x0059f3fc`;
|
||||
- otherwise, call `SetDrawnThisFrame` **before** lighting and mesh submission.
|
||||
|
||||
This placement is essential: a view that rejects the sphere never enters `DrawMeshInternal`, so it cannot stamp the part. The first admitted view stamps and submits; later admitted views immediately return.
|
||||
|
||||
#### Built/non-built branch
|
||||
|
||||
Ghidra disassembly `0x0059f3a3..0x0059f3fc`:
|
||||
|
||||
- `use_built_mesh == 0`: return without a mesh or polygon fallback in this function.
|
||||
- built + ordinary (`force == false`): call `D3DPolyRender::DrawMesh(gfxobj, constructed_mesh)` once. This is the complete constructed mesh.
|
||||
- built + force (`force == true`): call `obj_view_set`, save `building_view`, set it to `portal_view_num`, walk `drawing_bsp` with `build_draw_portals_only(..., 1)` and `(..., 2)`, restore `building_view`, and return. It does **not** submit `constructed_mesh`.
|
||||
|
||||
Thus the force flavor is a portal-only BSP pass, not a forced whole-mesh draw.
|
||||
|
||||
### `RenderDeviceD3D::DrawEnvCell` — `0x0059f170`
|
||||
|
||||
Ghidra-arbitrated order:
|
||||
|
||||
1. `GetDrawnThisFrame`; equal stamp jumps directly to return (`0x0059f175..0x0059f17e`).
|
||||
2. `SetDrawnThisFrame` immediately (`0x0059f184..0x0059f186`).
|
||||
3. `obj_view_set`, lighting minimization, and environment detail-state setup.
|
||||
4. if `use_built_mesh != 0`, update static-light vertex colors and call `D3DPolyRender::DrawMesh(num_surfaces, surfaces, constructed_mesh, skipNoTexture=true)`, then return.
|
||||
5. otherwise append every `structure` polygon to `Render::PolyList` with `planeMask = -1`, then call `polyListFinishInternal`.
|
||||
|
||||
The built shell has no sphere gate and no per-view mesh clip. The first call in the current render stamp submits the complete eligible surface set; all later calls are suppressed by the cell stamp.
|
||||
|
||||
The non-built fallback is also not a portal-triangle clip path: `D3DPolyRender::polyListFinishInternal @ 0x0059dba0` merely calls `DrawPolyInternal @ 0x0059d7c0` for each queued polygon, and `DrawPolyInternal` does not call `polyClipFinish`. `planeMask` is not consumed by that finish function.
|
||||
|
||||
### `D3DPolyRender::DrawMesh` — `0x0059d4a0` and `0x0059d790`
|
||||
|
||||
`DrawMesh(CGfxObj *, MeshBuffer *) @ 0x0059d790` forwards `CGfxObj::num_surfaces`, `Render::curr_surfaces`, and the constructed mesh to the surface-count overload.
|
||||
|
||||
`DrawMesh(numSurfaces, surfaces, MeshBuffer *, skipNoTexture) @ 0x0059d4a0` loops every eligible surface subset. For each subset it either:
|
||||
|
||||
- calls `RenderMeshSubset` immediately, or
|
||||
- appends that one subset to one of the delayed alpha lists.
|
||||
|
||||
The environment/building `skipNoTexture` rule can omit an ineligible no-texture subset. “Whole mesh” in this report means **every eligible authored surface subset of the one constructed mesh**, not an unconditional draw of data intentionally filtered by that retail rule.
|
||||
|
||||
The function accepts no active-view pointer, edge planes, scissor, clip mask, or `portal_view_num`. There is no portal polygon clipping in either built-mesh overload.
|
||||
|
||||
### Transparent-subset branch — `AddMeshToAlphaList @ 0x0059c230`, `FlushAlphaList @ 0x0059d2e0`
|
||||
|
||||
Delay is conditional, not universal. `DrawMesh @ 0x0059d4a0` can delay selected stipple/alpha/clip subsets only while the renderer is not drawing sky, `s_AlphaDelayMask != 0`, and `Render::curr_detail_surface == null`. If a detail surface is active, the subset goes through the immediate `RenderMeshSubset` branch. The complete branch table, mask priority, preference lifecycle, capacity behavior, and barriers are closed in `oh1-alpha-list-contract.md`.
|
||||
|
||||
When delayed:
|
||||
|
||||
- `AddMeshToAlphaList` appends one `AlphaListEntry` to either the clip list or the alpha list; both are fixed at 3000 entries.
|
||||
- Every entry captures mesh buffer, surface index/pointer, `bNewObject`, and
|
||||
clipmap-setting override. Only `bNewObject=true` entries capture material and
|
||||
object world matrix; later entries inherit the replay state.
|
||||
- It captures no portal-view state. Therefore a deferred subset cannot later be redrawn or clipped “per view.” It belongs to the first built-mesh submission that queued it.
|
||||
- `FlushAlphaList` drains the clip list in append order, then the alpha list in append order, calling `RenderMeshSubset` for each. It performs no distance sort.
|
||||
|
||||
Ordering is established before append. `DrawObjCellForDummies` sorts its `CShadowPart` list through `CShadowPart::insertion_sort @ 0x006b5130`, which orders by `CPhysicsPart::CYpt` far-to-near. The outer PView/cell walk supplies the rest of the submission order.
|
||||
|
||||
Environment and building detail surfaces matter: `DrawEnvCell` installs `environment_detail_surface`; `DrawBuilding` installs `building_detail_surface`. When those pointers are non-null, their subsets render immediately rather than entering the delayed lists. If the relevant detail surface is null, the ordinary delay-mask branch can apply. An exact port therefore cannot give every “transparent cell shell” one unconditional ownership policy independent of detail state.
|
||||
|
||||
### `DrawObjCellForDummies` and object-list path
|
||||
|
||||
Addresses:
|
||||
|
||||
- `RenderDeviceD3D::DrawObjCellForDummies` — `0x005a0760`
|
||||
- `RenderDeviceD3D::DrawObjCell` — `0x005a1a40`
|
||||
- `RenderDeviceD3D::DrawPartCell` — `0x005a07a0`
|
||||
- `CShadowPart::draw` — `0x006b50d0`
|
||||
|
||||
Order:
|
||||
|
||||
1. update the object cell;
|
||||
2. insertion-sort its shadow parts when there is more than one;
|
||||
3. `DrawObjCell -> DrawPartCell` walks each shadow part;
|
||||
4. `CShadowPart::draw` calls only `CPhysicsPart::Draw(part, 0)`.
|
||||
|
||||
Although `CShadowPart` owns legacy plane-list storage, its draw function does not forward those planes to the built mesh. Object portal admission occurs in `DrawMesh` through the current cell's `PortalList`; duplicate suppression occurs through the part stamp.
|
||||
|
||||
### Portal polygon clipping — what actually clips
|
||||
|
||||
Two explicit retail routes call `ACRender::polyClipFinish @ 0x006b6d00`:
|
||||
|
||||
1. `PView::GetClip @ 0x005a4320` transforms an authored portal polygon, fixes winding, and clips it to the current active view and near plane. Its output is passed to `Render::copy_view` by `PView::ConstructView`; this constructs the next view cone.
|
||||
2. `D3DPolyRender::DrawPortalPolyInternal @ 0x0059bc90` transforms a portal polygon, calls `polyClipFinish`, rejects fewer than three survivors, then draws the resulting triangle fan with the portal depth state. This is the exit seal / building punch geometry.
|
||||
|
||||
`polyClipFinish` clips against the active `portal_vertex` screen edges and the near-W boundary. These are small portal polygons. No ordinary built mesh enters this function.
|
||||
|
||||
Standard device view/frustum clipping still applies to submitted primitives, of course. The negative claim is specifically that retail does not apply the **portal view polygon** as a per-triangle built-mesh clip.
|
||||
|
||||
## Exact caller order
|
||||
|
||||
### `PView::DrawCells` — `0x005a4840`
|
||||
|
||||
When `outside_view.view_count != 0`:
|
||||
|
||||
1. `useSunlightSet(1)`;
|
||||
2. `PortalList = &outside_view`;
|
||||
3. `LScape::draw`;
|
||||
4. `FlushAlphaList(0)`;
|
||||
5. increment `RenderDevice::m_nFrameStamp` at `0x005a4886`;
|
||||
6. conditionally clear depth when `forceClear` is set or `portalsDrawnCount` was non-zero, then zero that count;
|
||||
7. reverse-walk the flood cells and every live view to draw clipped exit-portal seals.
|
||||
|
||||
Then, regardless of whether an outside view existed:
|
||||
|
||||
8. disable sunlight and restore lighting;
|
||||
9. reverse-walk all flood cells for shells; for each live view call `CEnvCell::setup_view @ 0x0052c430`, then `DrawEnvCell`;
|
||||
10. restart at the reverse end and walk all object lists; install that cell's top `portal_view_type` as `PortalList`, then call `DrawObjCellForDummies`;
|
||||
11. restore scale/sunlight state.
|
||||
|
||||
The shell loop can invoke `DrawEnvCell` once per view, but the first call stamps and draws the shell whole. The object loop can invoke a part through multiple cell lists/views, but the first admitted view stamps a non-player part and later routes cannot resubmit it.
|
||||
|
||||
The frame-stamp increment is inside the `outside_view.view_count != 0` branch. It deliberately re-arms both part and EnvCell stamps between the landscape/pre-clear scope and the interior repaint scope.
|
||||
|
||||
### `RenderDeviceD3D::DrawBuilding` — `0x0059f2a0`
|
||||
|
||||
After degrade/geometry admission:
|
||||
|
||||
1. install building detail state;
|
||||
2. `FlushAlphaList(0)`;
|
||||
3. `CPhysicsPart::Draw(part, 1)` — force portal-only BSP pass;
|
||||
4. set `ObjBuildingOrBuildingPart = 1`;
|
||||
5. `CPhysicsPart::Draw(part, 0)` — ordinary complete building shell;
|
||||
6. clear the flag/detail state.
|
||||
|
||||
The force pass bypasses stamps but does not submit the built mesh. It therefore cannot suppress the following ordinary shell: it never sets the part stamp. The ordinary shell follows normal per-view sphere admission and draws whole on the first admitted view.
|
||||
|
||||
## Direct answers
|
||||
|
||||
### Which built meshes draw whole?
|
||||
|
||||
| Content | Retail result |
|
||||
|---|---|
|
||||
| Ordinary built `CGfxObj` (`force=false`) | Complete eligible constructed mesh on first admitted view |
|
||||
| Built building shell ordinary pass | Same ordinary `CGfxObj` rule: complete eligible shell on first admitted view |
|
||||
| Built `CEnvCell` | Complete eligible constructed cell shell on first `DrawEnvCell` call in the stamp |
|
||||
| Built `CGfxObj` force/building-portal pass | No constructed-mesh draw; portal-only `drawing_bsp` walk in two passes |
|
||||
| Non-built `CGfxObj` in `DrawMeshInternal` | No mesh or polygon fallback in that function |
|
||||
| Non-built `CEnvCell` | Every structure polygon is sent to the legacy polygon draw loop; not a built mesh and not portal-polygon-clipped there |
|
||||
|
||||
### What polygon-clips?
|
||||
|
||||
- Portal polygons used to construct child views (`PView::GetClip -> polyClipFinish`).
|
||||
- Portal depth fans/seals (`DrawPortalPolyInternal -> polyClipFinish`).
|
||||
- Not ordinary built `CGfxObj` triangles.
|
||||
- Not built `CEnvCell` triangles.
|
||||
- Not the non-built EnvCell fallback in `DrawPolyInternal`.
|
||||
|
||||
### Do multiple portal views duplicate built-mesh submissions?
|
||||
|
||||
| Case | Duplicate whole-mesh submissions in one render stamp? |
|
||||
|---|---|
|
||||
| Non-player `CPhysicsPart` | No. First admitted view stamps/submits; later admitted views return before draw. |
|
||||
| Same non-player part present in multiple object cells | No, provided it is the same `CPhysicsPart`; the same stamp suppresses later cells. |
|
||||
| Built `CEnvCell` called once per view | No. First call stamps/submits; later calls return. |
|
||||
| Local-player `CPhysicsPart` | **Yes.** `IsPartOfPlayerObj` bypasses the inner stamp, so every admitted cell/view can submit the whole part. |
|
||||
| Force building portal pass | It may run once per selected view, but it submits portal polygons, not the built mesh. |
|
||||
|
||||
The owner is the **first admitted** view, not necessarily index zero: rejected views do not stamp a part.
|
||||
|
||||
### Which stamps suppress duplicates?
|
||||
|
||||
- Part stamp: `CPhysicsPart::m_current_render_frame_num +0xdc` versus `RenderDevice::m_nFrameStamp +0xb0`.
|
||||
- Outer skip: `CPhysicsPart::Draw @ 0x0050d7bd..0x0050d7c9`.
|
||||
- First-admission set and within-call view-loop suppression: `DrawMeshInternal @ 0x0059f37f..0x0059f38c`.
|
||||
- Force bypasses both; local player bypasses the inner get/set and normally never acquires the current stamp.
|
||||
- Cell stamp: `CEnvCell::m_current_render_frame_num +0x130` versus the same device stamp.
|
||||
- Get/set at `DrawEnvCell @ 0x0059f175..0x0059f186`.
|
||||
- It is set before lighting or mesh work.
|
||||
- Re-arm: `PView::DrawCells @ 0x005a4886` increments the device stamp after landscape alpha flush and before the possible depth clear/interior repaint, but only when an outside view exists.
|
||||
|
||||
## Current acdream comparison at `eaea8776`
|
||||
|
||||
### Faithful or materially aligned pieces
|
||||
|
||||
| Retail contract | Current source | Assessment |
|
||||
|---|---|---|
|
||||
| `copy_view` polygon/plane representation | `src/AcDream.App/Rendering/Walk/WalkViews.cs:28-58,80-280` | Strong match: 0x48 semantic fields, root quad, in-place divide, one-pixel prune, wrap checks, reject `<3`, cap 31, closing point, bounds, edge planes. |
|
||||
| strict-outside / inclusive-partial viewcone | `src/AcDream.App/Rendering/Walk/WalkVisibilityMath.cs:176-198` | Exact branch sense, with explicit boundary tests in `WalkVisibilityMathTests.cs:178-216`. |
|
||||
| whole-mesh admission rather than per-slice GPU clip | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs:527-567` | Correct: it tests all live views, but any admission returns `RetailWholeMeshSlot` (`slot 0`) rather than the admitted slice slots. |
|
||||
| one batch set after admission | `WbDrawDispatcher.WalkClassify.cs:391-418,444-471,824-867` | Correct for non-player content: one first admission emits every surviving mesh batch once. |
|
||||
| part stamp after admission | `WbDrawDispatcher.WalkClassify.cs:79-136` | Correct ordering: rejected parts do not stamp; accepted non-player parts dedupe by `(ProjectionId, PartIndex)`. |
|
||||
| local-player stamp bypass | `WbDrawDispatcher.WalkClassify.cs:126-136` | Deliberately matches retail's `IsPartOfPlayerObj` exception. |
|
||||
| whole EnvCell shell once per stamp | `src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs:1413-1453` and `RetailPViewPassExecutor.WalkLeaf.cs:246-272` | Correct geometric contract: reverse shell loop, CPU cell stamp set, slot-zero complete shell. |
|
||||
| mid-DrawCells stamp re-arm | `WalkFrameDriver.cs:1299-1319` and `WbDrawDispatcher.WalkClassify.cs:102-118` | Correctly re-arms parts and cells after a landscape turn. `OnLandscape` requires positive active views, so `_skyDrawnThisFrame` is the current semantic proxy for retail's outside-view branch. |
|
||||
| all shells before all object lists | `WalkFrameDriver.cs:1419-1453` | Matches the two reverse loops in `PView::DrawCells`. |
|
||||
| building order | `WalkFrameDriver.cs:1215-1268` and `RetailFrameWalk.cs:197-248` | Matches alpha barrier -> portal pass -> shell. `UseIndoorMembershipOnlyRouting` clears GPU clip routing before the walk (`RetailPViewPassExecutor.cs:212-217`), so building shells are not slice-clipped by the base slot. |
|
||||
|
||||
The focused current test `ClassifyEntityForWalk_PortalViewsAdmitOneCompleteUnclippedMesh` (`WalkStaticStreamPopulatorTests.cs:433-461`) uses two admitted view slots and asserts a single output batch at slot zero. `ClassifyEntityForWalk_FrameScopeDoesNotStampPortalRejectedPart` (`:348-392`) covers reject-then-admit. `ClassifyEntityForWalk_LocalPlayerBypassesDrawnPartStampLikeRetail` (`:394-431`) covers the player exception. `RepeatedFloodTurns_DrawEnvCellShellWholeOncePerRetailFrameStamp` and `LandscapeStampBoundary_RearmsWholeShellForPostClearRootRepaint` (`WalkFrameDriverTests.cs:589-681`) cover the cell stamp and its re-arm.
|
||||
|
||||
### Differences and high-risk candidates
|
||||
|
||||
| Priority | Current behavior | Exact retail behavior | Risk / required proof before changing |
|
||||
|---|---|---|---|
|
||||
| High | `RetailAlphaQueue` performs a scope-wide stable radix sort by viewer distance (`src/AcDream.App/Rendering/RetailAlphaQueue.cs:393-447`) and describes itself as one merged stream. | Retail sorts each `CObjCell`'s `CShadowPart` list by `CYpt` before submission, then appends subsets to **two separate FIFO lists** (clip first at flush, alpha second). `FlushAlphaList` does no sort. Outer cell/building walk order is part of the ordering contract. | A scope-wide resort can reorder subsets across cells, shell turns, particles, and renderer sources, and it collapses the clip-list/alpha-list phase split. This is an exact known difference, not established equivalent behavior. |
|
||||
| High | EnvCell shells are always split into an opaque pass followed by an immediate transparent pass per cell (`RetailPViewPassExecutor.WalkLeaf.cs:259-272`; `EnvCellRenderer.cs:832-842`). | Retail walks authored surface subsets once. Depending on `curr_detail_surface` and delay-mask state, each transparent subset is either immediate at its authored subset position or appended to the delayed list. | Current behavior is not state-equivalent across detail settings and can change subset/object interleaving. Do not “fix” it by blindly queueing all EnvCell alpha: retail is conditional. |
|
||||
| High | Building/static/entity transparent batches are routed through the current global queue via `WalkStaticStreamPopulator` / `SubmitWalkAlphaInstance` (`WbDrawDispatcher.WalkClassify.cs:882-918`). | A building with non-null `building_detail_surface` bypasses alpha delay and renders subsets immediately; with null detail surface, only mask-selected subsets delay. | The current unconditional translucency-class routing does not model the retail detail-surface gate. This can move building surfaces across the pre-building and end-of-scope flush boundaries. |
|
||||
| Medium | Part dedupe identity is `(RenderProjectionId, PartIndex)`. | Retail identity is the actual `CPhysicsPart *`, shared wherever that part is inserted. | Equivalence requires every repeated cell membership of one logical part to reuse the same projection id and composite part index. The static journal appears designed for stable ids, but an installed-data multi-cell identity audit is still required; duplicate projections would defeat the stamp. |
|
||||
| Medium | The port transforms the drawing sphere to world space and tests it against captured world planes, rather than caching `portal_obj_plane` through an `obj_view_set` equivalent. | Retail transforms the active planes/sphere through `FrameCurrent` and publishes `local_object_center/radius`. | Mathematically aligned for the current affine transform contract and maximum-axis radius scale, but side effects (`local_object_*`) and any nonstandard/skew transform consumer are not represented. Do not infer broader `obj_view_set` equivalence from the sphere result alone. |
|
||||
| Medium | Local-player stamp bypass is covered only as two repeated classifier calls without a real multi-cell/multi-view transparent part gate. | Retail can submit the local player's whole built part for every admitted cell/view and can therefore append delayed subsets repeatedly. | This seemingly duplicate behavior is retail. A generic “dedupe all parts” cleanup would reintroduce chopped-player seams; a live/installed-DAT transparent-player case remains unpinned. |
|
||||
| Low | Missing authored `SelectionSphere` falls back to “any view exists -> whole mesh.” | Retail dereferences `CGfxObj::drawing_sphere`; normal production assets are expected to have it. | Sensible corruption fallback, but not an exact retail branch. It must remain outside claims of byte-for-byte admission parity. |
|
||||
|
||||
### Dirty-worktree note
|
||||
|
||||
At evidence close, other agents had unrelated/uncommitted work visible in six source paths plus two other OH1 evidence files. Five source paths were content-identical to HEAD but worktree-marked (line-ending/index state); `LandblockBuildFactory.cs` had a one-blank-line deletion. None intersects the built-mesh/view functions audited here, and this report did not touch them. They must not be mistaken for fixes arising from this review.
|
||||
|
||||
## Contradictions retired by this evidence
|
||||
|
||||
1. **“Every admitted portal view redraws a non-player built mesh.”** False. The outer loop does call `DrawMeshInternal`, but the stamp check is inside that loop. Only the first admitted view submits. The statement is true only for the local-player exception; force mode is a separate portal-only path.
|
||||
2. **“`obj_view_set` proves the built mesh is clipped to the portal.”** False. It prepares legacy object/BSP plane state. Ordinary built `DrawMesh` receives none of it.
|
||||
3. **“DrawEnvCell once per view means one clipped shell copy per view.”** False. `DrawEnvCell` stamps before work and the built branch draws the complete shell on the first call.
|
||||
4. **“The alpha list remembers which portal admitted the subset.”** False. `AlphaListEntry` has no such state.
|
||||
5. **“All transparent shell subsets are delayed.”** False. A non-null current detail surface forces the immediate subset path.
|
||||
|
||||
One older research source, `docs/research/2026-06-11-holistic-map/wf1-statics-dynamics.md`, contains both the correct first-admission description and a later per-view-redraw inference. The later inference must not be used as an oracle; the Ghidra-arbitrated inner stamp branch above resolves it.
|
||||
|
||||
## Unresolved facts and bounded follow-up
|
||||
|
||||
These do not weaken the whole-mesh/stamp conclusion, but they remain necessary for a complete overhaul implementation:
|
||||
|
||||
1. Runtime registry/user inputs may override `s_AlphaDelayMask` and `MultiPassAlpha`, and detail generation may return null. These are explicit inputs to the exact branch table in `oh1-alpha-list-contract.md`, not unresolved branch sense.
|
||||
2. No mask/list classification ambiguity remains. The complete constructed mask domain, priority, signed-stippling quirk, clip-versus-alpha selection, material-alpha fallback, and multipass clip-map duplicate are established in `oh1-alpha-list-contract.md`.
|
||||
3. Installed-DAT proof that one logical static/dynamic part appearing in several cell lists always carries one stable `RenderProjectionId` and part index in acdream.
|
||||
4. A real multi-view local-player case containing delayed transparent subsets, to pin whether repeated retail submissions visibly overblend or are neutralized by depth/material state.
|
||||
5. The non-built `CGfxObj` route is a no-op in `DrawMeshInternal`; whether any surviving retail caller renders such content through another path is outside this slice. It must not be “filled in” without a separate caller census.
|
||||
6. `PView::DrawCells`'s `num_view == 0` decomp path forms a `0xffff` loop count. This appears to be an impossible/corrupt-state artifact under normal construction; current production should fail loud or preserve its established invariant rather than emulate 65,535 iterations.
|
||||
|
||||
## Binding constraints for Campaign OVERHAUL implementation slices
|
||||
|
||||
- Keep built meshes at slot zero/no portal GPU clip after sphere admission.
|
||||
- Preserve first-admitted-view semantics: rejected views cannot stamp.
|
||||
- Preserve separate part and EnvCell stamps, and the conditional mid-`DrawCells` device-stamp advance.
|
||||
- Preserve the local-player stamp exception.
|
||||
- Keep portal-plane polygon clipping confined to portal view construction and portal depth fans/seals unless another named-retail function proves a different consumer.
|
||||
- Do not “simplify” force mode into a whole building draw; it is a two-pass portal-only BSP walk followed by a separate ordinary shell draw.
|
||||
- Redesign transparent ownership from the exact detail-state + delay-mask branch and the two retail FIFO lists, not from a generic global distance-sort abstraction.
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
# OH1 retail construction and landscape-order contract
|
||||
|
||||
Date: 2026-09-01
|
||||
|
||||
Scope: the canonical cell-membership owner and the landscape/building/object
|
||||
draw order. This packet does not decide built-mesh clipping, CellStruct subset
|
||||
selection, or the `portalsDrawnCount` lifecycle; the sibling OH1/OH2 packets
|
||||
own those facts.
|
||||
|
||||
## Exact questions
|
||||
|
||||
1. What does retail construct when an EnvCell static is initialized?
|
||||
2. Which object, part, cell, and child lists own render membership?
|
||||
3. Which exact bounds cross a portal during static membership construction?
|
||||
4. In what order does retail visit landscape views, blocks, cells, terrain,
|
||||
buildings, ordinary cell objects, alpha barriers, and weather?
|
||||
5. Where does current acdream depart from that ownership and order?
|
||||
|
||||
## Sources and arbitration
|
||||
|
||||
Primary text:
|
||||
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
- `docs/research/named-retail/acclient.h`
|
||||
- `docs/research/named-retail/acclient.c`
|
||||
|
||||
Prior independently verified evidence used for cross-checking:
|
||||
|
||||
- `docs/research/2026-06-11-holistic-map/wf1-statics-dynamics.md`
|
||||
- `docs/research/2026-08-30-fw-walk-pseudocode.md`
|
||||
- `docs/research/2026-08-30-fw-walk-pseudocode-appendix.md`
|
||||
|
||||
Every branch in the contract below was re-read on 2026-09-01 through the live
|
||||
Ghidra HTTP bridge at `127.0.0.1:8081`, `patchmem.gpr`, using
|
||||
`/decompile_function?address=...`. Ghidra returned the named 2013 build and
|
||||
agreed with the named-retail control flow for:
|
||||
|
||||
- `CEnvCell::init_static_objects` `0x0052c350`
|
||||
- `CPhysicsObj::add_obj_to_cell` `0x005159e0`
|
||||
- `CPhysicsObj::calc_cross_cells_static` `0x00515160`
|
||||
- `CPhysicsObj::find_bbox_cell_list` `0x00510fc0`
|
||||
- `CPartArray::calc_cross_cells_static` `0x00518160`
|
||||
- `CEnvCell::find_transit_cells(parts)` `0x0052cae0`
|
||||
- `CPhysicsObj::add_shadows_to_cells` `0x00514ae0`
|
||||
- `CPartArray::AddPartsShadow` `0x00517e40`
|
||||
- `CPhysicsObj::remove_shadows_from_cells` `0x00511230`
|
||||
- `LScape::draw` `0x00506330`
|
||||
- `LScape::draw_check_blocks` `0x00505f80`
|
||||
- `LScape::landcell_check` `0x005050a0`
|
||||
- `RenderDeviceD3D::DrawBlock` `0x005a17c0`
|
||||
- `RenderDeviceD3D::DrawSortCell` `0x0059f140`
|
||||
- `RenderDeviceD3D::DrawObjCell` `0x005a1a40`
|
||||
- `RenderDeviceD3D::DrawBuilding` `0x0059f2a0`
|
||||
- `CShadowPart::insertion_sort` `0x006b5130`
|
||||
|
||||
No behavior below is inferred from a screenshot.
|
||||
|
||||
## Struct facts used by the contract
|
||||
|
||||
The following offsets are exact for the standalone 32-bit records in
|
||||
`acclient.h`; inherited class fields are named because the PDB-backed Ghidra
|
||||
decompile resolves them directly.
|
||||
|
||||
| Type/field | Offset | Meaning |
|
||||
|---|---:|---|
|
||||
| `CELLARRAY.added_outside` | `+0x00` | whether an exterior portal was crossed |
|
||||
| `CELLARRAY.do_not_load_cells` | `+0x04` | transit lookups may not synchronously load missing cells |
|
||||
| `CELLARRAY.num_cells` | `+0x08` | ordered member count |
|
||||
| `CELLARRAY.cells` | `+0x0C` | `DArray<CELLINFO>` preserving insertion order |
|
||||
| `CPartCell.vfptr` | `+0x00` | `add_part` / `remove_part` owner |
|
||||
| `CPartCell.num_shadow_parts` | `+0x04` | render-part count |
|
||||
| `CPartCell.shadow_part_list` | `+0x08` | ordered `DArray<CShadowPart*>` |
|
||||
| `CShadowPart.num_planes` | `+0x00` | number of portal clip-plane lists |
|
||||
| `CShadowPart.planes` | `+0x04` | borrowed `ClipPlaneList**` |
|
||||
| `CShadowPart.frame` | `+0x08` | owning cell-relative frame |
|
||||
| `CShadowPart.part` | `+0x0C` | exact `CPhysicsPart*` |
|
||||
| `CShadowObj.physobj` | after `LongHashData` | collision/object-list owner |
|
||||
| `CShadowObj.cell_id` | next field | registered cell identity |
|
||||
| `CShadowObj.cell` | next field | exact registered cell pointer |
|
||||
|
||||
Named inherited fields which are load-bearing here:
|
||||
|
||||
- `CPhysicsObj.part_array`, `parent`, `children`, `m_position`, `cell`,
|
||||
`num_shadow_objects`, `shadow_objects`, and `state`.
|
||||
- `CPartArray.num_parts` and `parts`.
|
||||
- `CObjCell.clip_planes`, `stab_list`, `seen_outside`, and the inherited
|
||||
`CPartCell.shadow_part_list`.
|
||||
- `CEnvCell.num_static_objects`, `static_object_ids`,
|
||||
`static_object_frames`, and `static_objects`.
|
||||
- `CLandBlock.side_cell_count`, `lcell`, `in_view`, `draw_array`, and
|
||||
`draw_array_size`. `CLandCell` stride is `0x108`; its `in_view` is `+0x104`.
|
||||
- `LScape.mid_width`, `land_blocks`, `block_draw_list`, viewer offsets, and
|
||||
`sky`.
|
||||
|
||||
## Contract A: EnvCell static construction
|
||||
|
||||
Retail call order is fixed:
|
||||
|
||||
```text
|
||||
CEnvCell::init_static_objects(cell)
|
||||
if static_objects == null:
|
||||
allocate exactly num_static_objects pointers
|
||||
for each authored static in source order:
|
||||
id == 0 ? null : CPhysicsObj::makeObject(id, 0, 0)
|
||||
non-null -> CPhysicsObj::add_obj_to_cell(obj, cell, authored Frame)
|
||||
else:
|
||||
for each existing non-null static in source order:
|
||||
if !obj->is_completely_visible(): obj->calc_cross_cells_static()
|
||||
|
||||
CPhysicsObj::add_obj_to_cell(obj, cell, frame)
|
||||
enter_cell(cell)
|
||||
copy authored Frame into obj->m_position.frame
|
||||
if not PARTICLE_EMITTER_PS and part_array != null: part_array->SetFrame(frame)
|
||||
UpdateChildrenInternal()
|
||||
calc_cross_cells_static()
|
||||
```
|
||||
|
||||
This proves the `static_object_ids/static_object_frames` arrays are an
|
||||
authored spawn manifest. They are not the draw list. Once constructed, the
|
||||
static follows the same `CPhysicsObj`/`CPartArray` membership machinery as an
|
||||
ordinary object.
|
||||
|
||||
### `calc_cross_cells_static` branch table
|
||||
|
||||
Ghidra at `0x00515160` resolves the branch exactly:
|
||||
|
||||
| Condition | Retail route |
|
||||
|---|---|
|
||||
| `(state & 0x10000) == 0 && part_array != null && GetNumCylsphere() != 0` | `CObjCell::find_cell_list(position, cylspheres, CELLARRAY, null)` |
|
||||
| otherwise | `find_bbox_cell_list(obj, CELLARRAY)` |
|
||||
|
||||
Before either route retail performs, in order:
|
||||
|
||||
```text
|
||||
cell_array.num_cells = 0
|
||||
cell_array.added_outside = 0
|
||||
cell_array.do_not_load_cells = 1
|
||||
```
|
||||
|
||||
After either route retail unconditionally calls
|
||||
`remove_shadows_from_cells(obj)` and then
|
||||
`add_shadows_to_cells(obj, &cell_array)`.
|
||||
|
||||
The bbox route is not an aggregate object AABB flood. It begins with the
|
||||
current `obj->cell`, then iterates the growing ordered CELLARRAY. For each live
|
||||
cell it calls `CPartArray::calc_cross_cells_static`, whose complete body is a
|
||||
virtual `cell->find_transit_cells(num_parts, parts, cell_array)` call.
|
||||
|
||||
For a `CEnvCell`, Ghidra `0x0052cae0` proves that transit is per portal and per
|
||||
visual part:
|
||||
|
||||
1. Take a part's `gfxobj->physics_sphere`; if null, take its
|
||||
`gfxobj->drawing_sphere`; if both are null, that part cannot open a portal.
|
||||
2. Transform the sphere center from the part frame into the current cell.
|
||||
3. Test signed portal-plane distance with radius plus `F_EPSILON` and the
|
||||
authored `portal_side`.
|
||||
4. For a possible crossing, obtain `CPhysicsPart::GetBoundingBox(part)`,
|
||||
transform that box into the current cell, and require
|
||||
`Plane::intersect_box(box) != portal_side`.
|
||||
5. For an indoor neighbor, resolve it with
|
||||
`CCellPortal::GetOtherCell(cell_array.do_not_load_cells)`, transform the
|
||||
same part box into the neighbor, and require
|
||||
`neighbor->structure->box_intersects_cell(box)` before ordered
|
||||
`CELLARRAY::add_cell`.
|
||||
6. For `other_cell_id == 0xFFFFFFFF`, remember an outside crossing. After all
|
||||
portals, call `CLandCell::add_all_outside_cells(num_parts, parts,
|
||||
cell_array)` once.
|
||||
|
||||
The `do_not_load_cells=1` state controls lookup only. This bbox route contains
|
||||
no later rule that removes the outside cells it just added.
|
||||
|
||||
## Contract B: one graph owns collision and render membership
|
||||
|
||||
`CPhysicsObj::add_shadows_to_cells` `0x00514ae0` consumes the ordered
|
||||
CELLARRAY twice for non-particle objects:
|
||||
|
||||
1. It sizes `shadow_objects`, creates one `CShadowObj` per cell, assigns the
|
||||
same owning `CPhysicsObj`, and copies each CELLARRAY `cell_id` in order.
|
||||
2. It visits the same cells in the same order. For every live cell it calls
|
||||
`CObjCell::add_shadow_object(cell, shadow, num_cells)` and then, when a
|
||||
`part_array` exists, `CPartArray::AddPartsShadow(part_array, cell,
|
||||
num_shadow_objects)`.
|
||||
|
||||
`CPartArray::AddPartsShadow` `0x00517e40` selects the clip owner exactly:
|
||||
|
||||
```text
|
||||
ClipPlaneList** planes = num_shadow_objects > 1 ? cell->clip_planes : null
|
||||
for each part in part-array order:
|
||||
if part != null: cell->add_part(part, planes, &cell->pos.frame, num_shadow_objects)
|
||||
```
|
||||
|
||||
Therefore collision-side `shadow_object_list` and render-side
|
||||
`shadow_part_list` are sibling products of one CELLARRAY transaction. They are
|
||||
not separately reconstructed lists. Each render entry identifies an exact
|
||||
part, not merely an aggregate object or Setup owner.
|
||||
|
||||
The particle branch is exact and different: `(state & 0x1000) != 0` calls
|
||||
`add_particle_shadow_to_cell`, registering in only `this->cell`.
|
||||
|
||||
After the root, `add_shadows_to_cells` recursively passes the same CELLARRAY
|
||||
to every object in `children`, in child-list order. This is the exact equipped
|
||||
child inheritance rule.
|
||||
|
||||
Removal is the inverse transaction. `remove_shadows_from_cells` `0x00511230`
|
||||
visits every current `CShadowObj` in order, removes it from its exact cell,
|
||||
calls `CPartArray::RemoveParts(part_array, cell)`, clears the root count, and
|
||||
then recurses through children. Publication and removal are therefore
|
||||
symmetrical at the same graph owner.
|
||||
|
||||
## Contract C: landscape visibility and painter order
|
||||
|
||||
`LScape::draw` `0x00506330` performs exactly:
|
||||
|
||||
```text
|
||||
if sky != null: GameSky::Draw(sky, 0)
|
||||
if block_draw_list != null:
|
||||
draw_check_blocks()
|
||||
for block_draw_list from last index down to zero:
|
||||
if block != null && block->in_view != OUTSIDE:
|
||||
RenderDevice::DrawBlock(block)
|
||||
if sky != null && weather_enabled: GameSky::Draw(sky, 1)
|
||||
```
|
||||
|
||||
It is called once for one active PView. Multiple portal views do not cause
|
||||
multiple full landscape passes.
|
||||
|
||||
### Visibility union
|
||||
|
||||
`draw_check_blocks` `0x00505f80` first clears every block and cell `in_view`.
|
||||
It then iterates every view in `Render::PortalList`; a null/zero-view list
|
||||
executes one default-view iteration. For each view it calls
|
||||
`Render::set_view`, evaluates every block with the shared clip-height interval
|
||||
grid, and calls `landcell_check` for blocks not outside.
|
||||
|
||||
`landcell_check` `0x005050a0` is exact:
|
||||
|
||||
- full `8×8` block and `ENTIRELY_INSIDE`: set every cell to
|
||||
`ENTIRELY_INSIDE`;
|
||||
- full `8×8` partial block: compute the 24 m per-cell interval grid and write
|
||||
a cell only when its current `IsInView()` is zero;
|
||||
- coarse LOD block (`side_cell_count != 8`): mark every represented cell
|
||||
`PARTIALLY_INSIDE`.
|
||||
|
||||
The `IsInView()==0` precheck makes multiple views a visibility union. A later
|
||||
view cannot downgrade a cell admitted by an earlier one.
|
||||
|
||||
### Block/cell leaf order
|
||||
|
||||
`RenderDeviceD3D::DrawBlock` `0x005a17c0` has two passes over the block's
|
||||
precomputed far-to-near `draw_array`:
|
||||
|
||||
1. For each visible cell: `UpdateObjCell`; if it has more than one shadow
|
||||
part, `CShadowPart::insertion_sort`.
|
||||
2. For each draw-array cell:
|
||||
- if visible, `DrawLandCell` first;
|
||||
- if `alwaysDrawObjects != 0` or visible, `DrawSortCell` second;
|
||||
- after that cell, conditionally `FlushAlphaList(flush)`.
|
||||
|
||||
`DrawSortCell` `0x0059f140` is exactly:
|
||||
|
||||
```text
|
||||
if cell->building != null: DrawBuilding(cell->building)
|
||||
DrawObjCell(cell)
|
||||
```
|
||||
|
||||
`DrawObjCell` delegates to `DrawPartCell` over that cell's real
|
||||
`shadow_part_list`. The building precedes ordinary statics/dynamics in the
|
||||
same landscape cell.
|
||||
|
||||
`CShadowPart::insertion_sort` `0x006b5130` walks the array backward and shifts
|
||||
entries while the next part's `CYpt` is greater. It produces stable
|
||||
far-to-near part order; equal distances do not swap.
|
||||
|
||||
`DrawBuilding` `0x0059f2a0` first publishes its portals, updates its shell
|
||||
part's degrade, and skips the complete body when the selected GfxObj pointer
|
||||
is null. Inside the gate its order is:
|
||||
|
||||
```text
|
||||
FlushAlphaList(0.0)
|
||||
CPhysicsPart::Draw(shellPart, 1) // portal walk: punches/look-ins
|
||||
ObjBuildingOrBuildingPart = 1
|
||||
CPhysicsPart::Draw(shellPart, 0) // plain shell mesh
|
||||
ObjBuildingOrBuildingPart = 0
|
||||
```
|
||||
|
||||
Thus the complete landscape painter sequence is:
|
||||
|
||||
```text
|
||||
sky pre-pass
|
||||
visibility union over all active views
|
||||
for blocks far-to-near:
|
||||
pre-update and stable-sort every visible cell's real shadow parts
|
||||
for cells far-to-near:
|
||||
terrain leaf
|
||||
building: alpha drain -> portal look-ins/punches -> shell
|
||||
ordinary cell shadow parts
|
||||
threshold alpha flush
|
||||
weather/sky post-pass
|
||||
```
|
||||
|
||||
## Current acdream comparison
|
||||
|
||||
| Retail fact | Current acdream | Disposition |
|
||||
|---|---|---|
|
||||
| EnvCell static construction installs a `CPhysicsObj` and publishes collision + exact per-part render membership from one CELLARRAY transaction. | `WalkProductionWorldData.BeginFrame` re-sweeps `RenderSceneIndex.IndoorCellStatic` and reconstructs a separate App-owned render index each frame. | Major duplicate owner. OH3 must publish/borrow exact part membership at the canonical collision/world graph boundary and delete the reconstruction. |
|
||||
| Bbox static transit is per visual part, using physics/drawing sphere for portal rejection and exact part bounding box for intersection/neighbor containment. | `ResolveStaticRenderCells` calls `ShadowShapeBuilder.FromStaticRenderParts`, converts prepared visual bounds into `ShadowShape`s, and drives `ComputeStaticRenderCells`; primitive Setups may instead reuse collision cells. | Candidate approximation. It is useful evidence, not the final retail owner. Recipe-7's DrawingBSP root sphere does not by itself prove the exact per-part box publication is present. |
|
||||
| Outside cells reached by the static bbox transit remain in the CELLARRAY. | Dirty `CellTransit` removed an outdoor prune globally, including collision callers. | Correct retail fact, wrong ownership boundary risk. OH3 must port the exact bbox route without mutating unrelated sphere/collision routes. |
|
||||
| Dynamics and attached children inherit the root's exact ordered CELLARRAY and publish exact per-part entries. | `WalkProductionWorldData` buckets whole `RenderProjectionRecord`s by `ShadowObjectRegistry.GetOwnerCells`, resolving equipped children by a parent callback. The draw classifier later tries to restore per-part stamps. | Membership is reconstructed from the collision registry, loses the canonical `CShadowPart` transaction/order, and creates a second model. |
|
||||
| `LScape::draw` runs once and visibility unions all views. | `RetailFrameWalk.DrawLandscape` correctly installs one view set and walks blocks/cells once. | Keep. |
|
||||
| Per landscape cell: terrain -> building -> ordinary objects. | `WalkFrameDriver.HandleLandscapeTurn` emits one whole `TerrainSlice(0)` immediately after sky; all terrain is submitted before `RetailFrameWalk` later emits building/object cell turns. The source comment explicitly calls this an intra-stage simplification. | Confirmed OH5 defect. It can expose background/behind-wall content because retail's depth/painter interleave no longer exists. |
|
||||
| Cell object draw consumes the cell's exact stable far-to-near `shadow_part_list`. | The driver materializes per-cell record buckets, classifies/batches them, and uses a scope-level ordered stream plus separate alpha collection. | OH3/OH7 must replace reconstructed membership/order with exact part entries and exact per-cell alpha barriers. |
|
||||
| Building is alpha drain -> portal pass -> shell, then cell objects. | `RetailFrameWalk.DrawBuilding` and the driver's building events model this order. | Mechanism broadly matches, but its correctness depends on the sibling OH1 built-mesh/view and depth-lifecycle findings. |
|
||||
|
||||
The architecture SSOT currently says that the walk records one retail-ordered
|
||||
stream including landscape cells, but the implementation and its own comments
|
||||
admit that terrain is one whole pre-stage. That is an architecture/code
|
||||
contradiction, not a documented adaptation. The same architecture section
|
||||
also states that every admitted portal slice GPU-clips the mesh; the dirty
|
||||
built-mesh candidate says the opposite. The sibling built-mesh evidence must
|
||||
arbitrate that claim before the architecture text or production path is
|
||||
treated as authoritative.
|
||||
|
||||
## Smallest exact ownership changes implied
|
||||
|
||||
This evidence does **not** authorize an immediate patch. It constrains later
|
||||
slices:
|
||||
|
||||
1. OH3a must define one generation-scoped, prepared-data-backed graph whose
|
||||
cell entries retain exact ordered part identity and the sibling
|
||||
collision-object relationship.
|
||||
2. OH3b/OH3c must publish/remove static, dynamic, particle, and child entries
|
||||
at the same canonical transaction boundary, preserving CELLARRAY and part
|
||||
order. App may expose borrowed immutable render views; it may not rebuild
|
||||
membership from aggregate record AABBs each frame.
|
||||
3. OH3d must delete `WalkProductionWorldData`'s static/dynamic membership
|
||||
reconstruction after consumer cutover.
|
||||
4. OH5 must replace the single whole-terrain event with real per-block,
|
||||
per-cell terrain leaves interleaved before that cell's building/object
|
||||
turn. It must preserve the existing exact block/cell order rather than
|
||||
recompute a second order in the renderer.
|
||||
5. OH7 must make the canonical per-cell part list and retail alpha flush sites
|
||||
the only transparent-world order owner.
|
||||
|
||||
## Transcript fields required from this packet
|
||||
|
||||
The OH1 frame transcript must include:
|
||||
|
||||
- active PView/view index and visibility-union result per block/cell;
|
||||
- block draw index and cell draw index;
|
||||
- `DrawLandCell`, `DrawBuilding`, `DrawObjCell`, and alpha-flush events in one
|
||||
total sequence;
|
||||
- CELLARRAY member order and `added_outside/do_not_load_cells` state;
|
||||
- root owner, child-inheritance source, exact part index/GfxObj, registered
|
||||
cell, plane-list presence, and part-list insertion index;
|
||||
- shadow removal in the same identity/order vocabulary;
|
||||
- `CYpt` and the post-sort cell part order.
|
||||
|
||||
## Tests that must fail before the cutovers
|
||||
|
||||
- A two-cell static with two visual parts whose part boxes cross different
|
||||
portals must produce the exact retail CELLARRAY and exact per-cell part
|
||||
lists; an aggregate AABB implementation must fail the fixture.
|
||||
- A child with no independent collision row must inherit its root's ordered
|
||||
CELLARRAY and install each child part in the same cell order.
|
||||
- A particle-emitter-state fixture must register only in the current cell.
|
||||
- Removal must delete both object and part membership recursively with no
|
||||
surviving render entry.
|
||||
- A one-block landscape fixture with two cells must assert the sequence
|
||||
`terrain(A), building(A), objects(A), terrain(B), objects(B)`. The current
|
||||
whole-terrain pre-stage must fail it.
|
||||
- A two-view landscape fixture must run one sky/landscape/weather pass while
|
||||
unioning block/cell `in_view` across both views.
|
||||
- Equal-`CYpt` parts must retain insertion order.
|
||||
|
||||
## Unresolved facts
|
||||
|
||||
None for the construction or landscape-order branches above. Exact built-mesh
|
||||
submission per active view, draw-stamp scope, CellStruct surface selection,
|
||||
and the previous-frame portal-depth latch remain deliberately unresolved here
|
||||
and are owned by the sibling evidence packets.
|
||||
520
docs/research/2026-09-01-overhaul/oh1-depth-lifecycle.md
Normal file
520
docs/research/2026-09-01-overhaul/oh1-depth-lifecycle.md
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
# OH1 — retail outside-view and depth lifecycle
|
||||
|
||||
**Date:** 2026-09-01
|
||||
|
||||
**Campaign:** OVERHAUL, OH1 evidence slice
|
||||
|
||||
**Scope:** the complete retail `PView::DrawCells` outside-view/depth lifecycle, from the roots that can reach it through landscape/building/cell order, including the persistent portal-draw counter, render frame stamp, far-Z building punches, true-depth exit seals, and the corresponding acdream implementation.
|
||||
**Production changes:** none. This file is the only artifact produced by this audit.
|
||||
|
||||
## Verdict
|
||||
|
||||
The current acdream walk is not executing retail's depth lifecycle. The largest semantic differences are deterministic, not visual guesses:
|
||||
|
||||
1. Retail wraps **landscape draw, alpha flush, an additional mid-frame render-stamp increment, previous-call counter consumption/reset, optional full depth clear, and current exit seals** in one `outside_view.view_count != 0` block. acdream emits `ClearInteriorDepth` and `ExitSeals` for every interior root, including `outside_view == 0`.
|
||||
2. Retail's `portalsDrawnCount` is a persistent `uint16` populated only by non-far portal-mask attempts. An eligible `DrawCells` consumes and clears the value produced by the **previous eligible outside-view call**, then the current true-depth exit-seal attempts repopulate it. acdream has no equivalent state and clears on the first eligible frame even when retail would not.
|
||||
3. Retail normally advances `RenderDevice::m_nFrameStamp` once per present in `RenderDeviceD3D::Flip @0x0059FE50/0x0059FEC8`. `DrawCells @0x005A4885` performs an **additional conditional mid-frame increment** only after an outside landscape and alpha flush, immediately before the counter/clear. That second barrier re-arms landscape-admitted content for the root repaint; it is not the ordinary frame-start/present increment. acdream's CPU stamp mirror is near the second turn, but is not tied to the missing counter state and operates at collection time rather than at the GPU call boundary.
|
||||
4. Retail's far punch uses exact depth bits `0x3F7FFFEF` (`0.999998987f`) and rejects a whole polygon if all **local input vertices, before `xformStart`,** lie on any `x/y == +/-12` boundary plane. acdream uses `0.99999988`, lacks that whole-polygon reject, and relies on a capped GPU clip-distance approximation.
|
||||
5. Retail interleaves `DrawLandCell` then `DrawSortCell` per landscape cell. acdream knowingly emits all terrain before all building/static turns. This changes the position of building far-Z punches relative to terrain depth.
|
||||
|
||||
These differences are sufficient to produce both opposite failure modes seen during the campaign: clearing/sealing where retail does neither can remove valid walls; failing to reproduce the previous-call gate and exact punch/seal production can leave stale or misplaced depth that exposes geometry behind walls.
|
||||
|
||||
## Evidence method and arbitration rule
|
||||
|
||||
The primary source was the named retail corpus, searched before any fresh disassembly:
|
||||
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
- `PView::DrawCells` around lines 432709–432889
|
||||
- `DrawPortalPolyInternal` around lines 424490–424698
|
||||
- `PView::ClipPortals`, both `ConstructView` overloads, `DrawInside`, and `DrawPortal`
|
||||
- `docs/research/named-retail/acclient.h`
|
||||
- `portal_view_type` around lines 32346–32355
|
||||
- `PView` around lines 45934–45944
|
||||
- `RenderDevice` around lines 38988–39016
|
||||
|
||||
Every behavior-changing branch sense reported below was then checked against the local Ghidra service on port 8081 with `decompile_function` and `disassemble_function`. Global producer/consumer claims were checked with `xrefs_to`; callers were checked with `function_xrefs`/`xrefs_to`. The decisive Ghidra functions were:
|
||||
|
||||
| Function | Address |
|
||||
|---|---:|
|
||||
| `SmartBox::RenderNormalMode` | `0x00453AA0` |
|
||||
| `LScape::draw` | `0x00506330` |
|
||||
| `PView::ClipPortals` | `0x005A5520` |
|
||||
| `PView::ConstructView(CEnvCell)` | `0x005A57B0` |
|
||||
| `PView::DrawInside` | `0x005A5860` |
|
||||
| `PView::ConstructView(CBldPortal)` | `0x005A59A0` |
|
||||
| `PView::DrawPortal` | `0x005A5AB0` |
|
||||
| `PView::DrawCells` | `0x005A4840` |
|
||||
| `DrawPortalPolyInternal` | `0x0059BC90` |
|
||||
| `RenderDeviceD3D::Clear` | `0x0059FD30` |
|
||||
| `RenderDeviceD3D::Flip` | `0x0059FE50` (`m_nFrameStamp` store at `0x0059FEC8`) |
|
||||
| `RenderDeviceD3D::DrawBlock` | `0x005A17C0` |
|
||||
| `RenderDeviceD3D::DrawSortCell` | `0x0059F140` |
|
||||
| `RenderDeviceD3D::DrawBuilding` | `0x0059F2A0` |
|
||||
| `RenderDeviceD3D::DrawMeshInternal` | `0x0059F360` |
|
||||
| `RenderDeviceD3D::DrawEnvCell` | `0x0059F170` |
|
||||
|
||||
## Retail state layout
|
||||
|
||||
The offsets below come from the named header where available and were cross-checked against the access widths/strides in Ghidra.
|
||||
|
||||
| Owner | Offset/address | Type | Meaning |
|
||||
|---|---:|---|---|
|
||||
| `PView` | `+0x00` | `portal_view_type` | `outside_view` |
|
||||
| `PView` | `+0x38` | `uint32` | `outside_view.view_count` |
|
||||
| `PView` | `+0x48` | `int32` | `draw_landscape` |
|
||||
| `PView` | `+0x4C` | pointer | `outdoor_portal_list` |
|
||||
| `PView` | `+0x50` | DArray | `cell_draw_list` |
|
||||
| `PView` | `+0x60` | `uint32` | `cell_draw_num` |
|
||||
| `PView` | `+0x64` | DArray | `cell_todo_list` |
|
||||
| `PView` | `+0x74` | `uint32` | `cell_todo_num` |
|
||||
| `PView` | `+0x78` | pointer | `lscape` |
|
||||
| `portal_view_type` | `+0x00` | DArray | portal refs |
|
||||
| `portal_view_type` | `+0x10` | `uint32` | total view-poly vertex count |
|
||||
| `portal_view_type` | `+0x14` | DArray | view polygons |
|
||||
| `portal_view_type` | `+0x24` | DArray | view vertices |
|
||||
| `portal_view_type` | `+0x34` | float | `max_indist` |
|
||||
| `portal_view_type` | `+0x38` | `uint32` | `view_count` |
|
||||
| `portal_view_type` | `+0x3C` | `uint32` | `cell_view_done` |
|
||||
| `portal_view_type` | `+0x40` | `uint32` | `view_timestamp` |
|
||||
| `portal_view_type` | `+0x44` | `uint32` | `update_count` |
|
||||
| `CEnvCell` | `+0x100` | pointer | `structure` |
|
||||
| `CEnvCell` | `+0x108` | `uint32` | `num_portals` |
|
||||
| `CEnvCell` | `+0x10C` | pointer | `CCellPortal[]` |
|
||||
| `CEnvCell` | `+0x130` | `uint32` | current render-frame stamp |
|
||||
| `CEnvCell` | `+0x134` | `uint16` | `num_view` |
|
||||
| `CEnvCell` | `+0x138` | DArray | portal views |
|
||||
| `CCellPortal` | `+0x00` | `uint32` | `other_cell_id`; `0xFFFFFFFF` means outside |
|
||||
| `CCellPortal` | `+0x04` | pointer | `other_cell_ptr` |
|
||||
| `CCellPortal` | `+0x08` | pointer | portal polygon |
|
||||
| `CCellPortal` | `+0x0C` | scalar | portal side |
|
||||
| `CCellPortal` | `+0x10` | scalar | other portal id |
|
||||
| `CCellPortal` | `+0x14` | scalar | exact-match flag |
|
||||
| `RenderDevice` | `+0xB0` | `uint32` | `m_nFrameStamp` |
|
||||
| global | `0x008719B4` | `uint16` | `portalsDrawnCount`, initialized zero |
|
||||
| global | `0x008ED824` | `int32` | `forceClear`, initialized zero |
|
||||
| global | `0x00820E14` | mode | `maxZ2 == 6`, true-depth mode |
|
||||
| global | `0x00820E18` | mode | `maxZ1 == 7`, far-Z mode |
|
||||
| data | `0x007E5450` | float bits | far-Z `0x3F7FFFEF` = `0.999998987f` |
|
||||
|
||||
`portal_view_type` is `0x48` bytes and `CCellPortal` is `0x18` bytes. Those sizes matter because `DrawCells` indexes the top portal view and walks portal arrays by those exact strides.
|
||||
|
||||
## Caller/callee order
|
||||
|
||||
### Root selection
|
||||
|
||||
`SmartBox::RenderNormalMode @0x00453AA0` decides from the camera cell id:
|
||||
|
||||
- low 16 bits `< 0x100`: outdoor root. It updates landscape/viewpoint state and calls `LScape::draw`. It does **not** call top-level `PView::DrawCells`; consequently there is no top-level full depth clear and no exit-seal loop.
|
||||
- low 16 bits `>= 0x100`: indoor root. It establishes the viewpoint and reaches `RenderDeviceD3D::DrawInside`, which calls `PView::DrawInside` on the indoor PView.
|
||||
- after either root, `FlushAlphaList` occurs at the top level.
|
||||
|
||||
Retail creates two different PViews during render-device initialization:
|
||||
|
||||
- indoor PView: constructed with `draw_landscape = 1`;
|
||||
- outdoor/building-look-in PView: constructed with `draw_landscape = 0`.
|
||||
|
||||
This distinction is binding. Only the indoor PView can accumulate `outside_view` from portals whose `other_cell_id == 0xFFFFFFFF`.
|
||||
|
||||
### Indoor root
|
||||
|
||||
`PView::DrawInside @0x005A5860` performs:
|
||||
|
||||
1. push/add the root view and stab views;
|
||||
2. copy the current full view into the root cell's top view;
|
||||
3. `ConstructView(root, 0xFFFF)`;
|
||||
4. `DrawCells(this, 0)`;
|
||||
5. pop/remove the temporary views.
|
||||
|
||||
`PView::ConstructView(CEnvCell) @0x005A57B0` clears `outside_view.view_count`, increments the separate `PView::master_timestamp`, clears the todo/draw counts, and floods through `ClipPortals`. `PView::master_timestamp` is not `RenderDevice::m_nFrameStamp`; it is a visibility-construction generation used by this flood.
|
||||
|
||||
`PView::ClipPortals @0x005A5520` installs the current cell's top portal view, accepts only live views, resolves ordinary neighbor cells, and handles an outside portal as follows:
|
||||
|
||||
- if `other_cell_id != 0xFFFFFFFF`, clip/propagate to the neighbor;
|
||||
- if `other_cell_id == 0xFFFFFFFF` and `draw_landscape != 0`, append the clipped portal view (or full view when `cliplandscape == 0`) to `outside_view`;
|
||||
- if `draw_landscape == 0`, do not produce an outside view.
|
||||
|
||||
### Landscape and building look-ins
|
||||
|
||||
`LScape::draw @0x00506330` orders sky, reverse visible-block traversal, and weather. Within each block, `RenderDeviceD3D::DrawBlock @0x005A17C0` walks its authored cell order. For each land cell it calls:
|
||||
|
||||
1. `DrawLandCell` when the land cell is in view;
|
||||
2. `DrawSortCell` when `alwaysDrawObjects != 0` (retail default is 1) or the cell is in view.
|
||||
|
||||
`DrawSortCell @0x0059F140` calls building draw first, then the land cell's object draw. `DrawBuilding @0x0059F2A0` does:
|
||||
|
||||
1. publish the outdoor PView's portal array;
|
||||
2. flush alpha;
|
||||
3. draw building part with portal flavor (`Draw(part, 1)`);
|
||||
4. set building flag and draw shell (`Draw(part, 0)`);
|
||||
5. restore the flag.
|
||||
|
||||
The portal-flavor `DrawMeshInternal @0x0059F360` installs `building_view`, then runs the building BSP's portal-only walk in two passes:
|
||||
|
||||
- pass 1: `ConstructView(CBldPortal)` emits `DrawPortalPolyInternal(poly, true)` — the colorless far-Z punch. `PView::DrawPortal` sees mode 1 and skips `DrawCells`.
|
||||
- pass 2: it recursively constructs the destination cell view, and `PView::DrawPortal` calls `DrawCells` for that look-in flood.
|
||||
|
||||
Because this is the outdoor PView (`draw_landscape = 0`), the look-in `DrawCells` has `outside_view.view_count == 0`. It therefore skips landscape, the **additional DrawCells stamp increment**, counter consumption, full clear, and exit seals; it draws its reverse shell pass and reverse object-cell pass, resets object scale, and ends with `useSunlightSet(1)`.
|
||||
|
||||
### Exact whole-frame ordering
|
||||
|
||||
Outdoor root:
|
||||
|
||||
```text
|
||||
LScape::draw
|
||||
sky
|
||||
each visible block (reverse block_draw_list)
|
||||
each authored land cell
|
||||
DrawLandCell
|
||||
DrawSortCell
|
||||
DrawBuilding
|
||||
alpha flush
|
||||
every building pass-1 far-Z punch
|
||||
every building pass-2 look-in DrawCells flood
|
||||
reverse shells, then reverse object cells
|
||||
reset object scale; useSunlightSet(1)
|
||||
building shell
|
||||
land-cell objects
|
||||
weather
|
||||
top-level alpha flush
|
||||
later RenderDeviceD3D::Flip present: m_nFrameStamp++
|
||||
```
|
||||
|
||||
Indoor root with at least one outside view:
|
||||
|
||||
```text
|
||||
ConstructView(root)
|
||||
DrawCells(indoor PView)
|
||||
install sunlight + outside_view
|
||||
LScape::draw (same per-cell interleave as above)
|
||||
alpha flush
|
||||
m_nFrameStamp++
|
||||
consume/reset previous portalsDrawnCount
|
||||
full depth clear iff consumed count != 0
|
||||
current exit portals: true-depth seal attempts
|
||||
useSunlightSet(0); restore_all_lighting
|
||||
all root-flood cell shells, reverse cell_draw_list
|
||||
all root-flood object cells, reverse cell_draw_list
|
||||
reset object scale; useSunlightSet(1)
|
||||
top-level alpha flush
|
||||
later RenderDeviceD3D::Flip present: m_nFrameStamp++
|
||||
```
|
||||
|
||||
Indoor root with no outside view:
|
||||
|
||||
```text
|
||||
ConstructView(root)
|
||||
DrawCells(indoor PView)
|
||||
useSunlightSet(0); restore_all_lighting
|
||||
all root-flood cell shells, reverse cell_draw_list
|
||||
all root-flood object cells, reverse cell_draw_list
|
||||
reset object scale; useSunlightSet(1)
|
||||
top-level alpha flush
|
||||
later RenderDeviceD3D::Flip present: m_nFrameStamp++
|
||||
```
|
||||
|
||||
There is no landscape, intermediate alpha flush, **additional DrawCells stamp increment**, counter test/reset, clear, or seal in the last case. The normal `Flip` increment still occurs when that frame is presented.
|
||||
|
||||
## `PView::DrawCells` exact lifecycle
|
||||
|
||||
`PView::DrawCells @0x005A4840` has only two callers in the binary: `DrawInside @0x005A595B` and `DrawPortal @0x005A5B53`. Its second integer parameter is unused.
|
||||
|
||||
| Order | PC/range | Condition | State before | Action | State after |
|
||||
|---:|---:|---|---|---|---|
|
||||
| 1 | `0x5A4849–0x5A4852` | `outside_view.view_count == 0` | any pending counter | jump to `0x5A49F1` | counter and stamp unchanged **within this call**; the normal later `Flip` increment is independent |
|
||||
| 2 | `0x5A485A–0x5A486B` | outside view nonzero | outside views installed by indoor flood | sunlight on; `Render::PortalList = &outside_view`; `LScape::draw(lscape)` | landscape/building/look-in depth and color exist |
|
||||
| 3 | `0x5A4872` | same | alpha may be queued | `FlushAlphaList(0)` | landscape stage drained |
|
||||
| 4 | `0x5A4885–0x5A4886` | same | current presented-frame render stamp | increment `RenderDevice::m_nFrameStamp` a second time, mid-frame | landscape-drawn shells/parts become eligible for interior repaint |
|
||||
| 5 | `0x5A488C–0x5A48A7` | same | persistent prior counter | if shipped `forceClear == 0`, read counter, test it, and always reset it to zero | prior value retained only in a register |
|
||||
| 6 | `0x5A48A9–0x5A48BD` | prior value nonzero, or theoretical `forceClear != 0` | depth contains landscape/look-ins | clear full depth attachment to 1.0 | color remains; depth is empty |
|
||||
| 7 | `0x5A48C0–0x5A49EB` | cell draw list nonempty | counter now zero in shipped path | reverse cells; for each live view, draw every portal with `other_cell_id == 0xFFFFFFFF` using `DrawPortalPolyInternal(false)` | counter increments modulo 65,536 per accepted seal attempt; true depth restores outside occlusion |
|
||||
| 8 | `0x5A49F1–0x5A49FB` | always | outside stage complete or skipped | sunlight off; restore lighting | indoor lighting active |
|
||||
| 9 | `0x5A4A00–0x5A4ADE` | drawing BSP exists | root/look-in view list | reverse cells, each live view: `setup_view`, then `DrawEnvCell` | complete constructed shell drawn once per render stamp |
|
||||
| 10 | `0x5A4ADE–0x5A4B1A` | cells exist | shells complete | reverse cells; install top portal view; `DrawObjCellForDummies` | statics/dynamics drawn through cell views |
|
||||
| 11 | `0x5A4B1C–0x5A4B5D` | always | all cell passes complete | reset object scale; `useSunlightSet(1)` | sunlight is enabled before returning to the caller and its top-level alpha flush |
|
||||
|
||||
The full clear is a true full-viewport depth-only clear. `RenderDeviceD3D::Clear @0x0059FD30` maps engine flag 4 to `D3DCLEAR_ZBUFFER`, uses `Count=0`/`pRects=null`, and clears Z to 1.0 without clearing color.
|
||||
|
||||
## Persistent counter: producer, consumer, and timing
|
||||
|
||||
The complete Ghidra xref set for `portalsDrawnCount @0x008719B4` contains exactly three accesses:
|
||||
|
||||
| Access | Function/PC | Meaning |
|
||||
|---|---:|---|
|
||||
| read/write | `DrawPortalPolyInternal @0x0059BD74` | word increment, modulo 65,536, for an accepted non-far mask attempt |
|
||||
| read | `DrawCells @0x005A4895` | consume previous value |
|
||||
| write | `DrawCells @0x005A489E` | reset to zero |
|
||||
|
||||
There is no frame-start reset and no other producer. The exact shipped state machine is:
|
||||
|
||||
```text
|
||||
if outside_view.view_count != 0:
|
||||
draw landscape
|
||||
flush alpha
|
||||
++RenderDevice.m_nFrameStamp
|
||||
|
||||
previous = portalsDrawnCount
|
||||
portalsDrawnCount = 0
|
||||
if previous != 0:
|
||||
clear full depth to 1.0
|
||||
|
||||
for cell in reverse(cell_draw_list):
|
||||
for each live view of cell:
|
||||
setup_view(cell, view)
|
||||
for portal in cell.portals:
|
||||
if portal.other_cell_id == 0xFFFFFFFF:
|
||||
DrawPortalPolyInternal(portal.poly, farZ=false)
|
||||
# accepted attempt increments the uint16 counter modulo 65536
|
||||
|
||||
draw shells in reverse(cell_draw_list)
|
||||
draw objects in reverse(cell_draw_list)
|
||||
reset object scale
|
||||
useSunlightSet(1)
|
||||
|
||||
# outside DrawCells, at the normal later present:
|
||||
RenderDeviceD3D::Flip increments m_nFrameStamp once
|
||||
```
|
||||
|
||||
`forceClear` is initialized zero and Ghidra finds no writes in this binary. If patched nonzero, it branches directly to the clear and bypasses both counter read and reset. That is a dormant/debug override, not the shipped lifecycle.
|
||||
|
||||
### Consecutive-call example
|
||||
|
||||
| Eligible call | `outside_view` | counter on entry | clear? | accepted current seals | counter on exit |
|
||||
|---:|---:|---:|---|---:|---:|
|
||||
| startup/N | `>0` | 0 | no | K | `K mod 65536` |
|
||||
| N+1 | `>0` | K | yes if K != 0 | M | `M mod 65536` |
|
||||
| N+2 | 0 | M | no; outside block skipped | none | M |
|
||||
| N+3 | 0 | M | no; outside block skipped | none | M |
|
||||
| N+4 | `>0` | M | yes if M != 0 | Q | `Q mod 65536` |
|
||||
|
||||
`K`, `M`, and `Q` are accepted-attempt counts; the stored counter is a machine `uint16`, and `INC word ptr [0x008719B4]` wraps modulo 65,536. In the pathological exactly-65,536-attempt case the stored value returns to zero and therefore does not arm the next clear.
|
||||
|
||||
Calling this merely “previous frame” is slightly imprecise. It is the previous **eligible outside-view `DrawCells` call**. Under the ordinary one-indoor-root-per-presented-frame path those are normally adjacent frames; a run of `outside_view == 0` frames leaves the pending count untouched.
|
||||
|
||||
## Far-Z punches and true-depth exit seals
|
||||
|
||||
Both use `DrawPortalPolyInternal @0x0059BC90`, but the boolean selects materially different depth:
|
||||
|
||||
| Use | Call site | Boolean | Mode | Depth | Counter delta |
|
||||
|---|---|---:|---:|---|---:|
|
||||
| building portal pass 1 | `ConstructView(CBldPortal) @0x5A5A7B` | true | 7 / `maxZ1` | exact far Z, bits `0x3F7FFFEF` | 0 |
|
||||
| outside exit seal | `DrawCells @0x5A49B7` | false | 6 / `maxZ2` | true projected `z/w` | +1 after boundary reject, before clip |
|
||||
| failed mode-3 portal fallback | `DrawPortal @0x5A5B7C` | false | 6 | true projected `z/w` | +1 if reached/accepted; not used by the two-pass building walker |
|
||||
|
||||
The common polygon path is:
|
||||
|
||||
1. initialize four whole-polygon predicates: every local input vertex on `x == +12`, `x == -12`, `y == +12`, or `y == -12`;
|
||||
2. for each original `CVertex`, compare its local `x/y` values first (`0x59BCD6–0x59BD20`), then call `xformStart` for that same vertex at `0x59BD28`;
|
||||
3. after all vertices, return without count/draw if any local-input predicate remains true;
|
||||
4. if boolean is false, increment `portalsDrawnCount` modulo 65,536;
|
||||
5. `polyClipFinish` against the installed render view;
|
||||
6. return if clipped vertex count `< 3`;
|
||||
7. submit a triangle fan with no texture, alpha test off, blend `SRCALPHA`/`INVSRCALPHA`/`ADD`, depth test `ALWAYS`, depth write on, cull none, and normal/default color writes still enabled. Modes 6 and 7 build vertices with source alpha zero, so blending preserves destination color even though the color attachment is writable.
|
||||
|
||||
The counter therefore records accepted **attempts**, not successful GPU fans: an exit polygon that passes the boundary test but clips to fewer than three vertices still increments the next-call clear gate. Far-Z punches never increment it.
|
||||
|
||||
## Ghidra branch arbitration table
|
||||
|
||||
This table records the behavior-changing senses used in the findings. It is intentionally more precise than the named pseudo-C wherever a decompiler inversion could matter.
|
||||
|
||||
| PC | Ghidra instruction/sense | Retail behavior |
|
||||
|---:|---|---|
|
||||
| `0x5A484C` / `0x5A4852` | `TEST EAX,EAX`; `JBE 0x5A49F1` | zero outside views skip the entire outside/depth stage |
|
||||
| `0x5A4891` / `0x5A4893` | test `forceClear`; `JNZ 0x5A48A9` | nonzero override goes straight to clear and bypasses counter reset |
|
||||
| `0x5A489C` / `0x5A48A7` | test prior counter; reset store occurs before `JZ 0x5A48C0` | shipped path always resets, and clears only when prior count was nonzero |
|
||||
| `0x5A48C3` / `0x5A48C9` | test reverse-list count; `JBE 0x5A49F1` | no exit-seal traversal when draw list empty |
|
||||
| `0x5A496C` / `0x5A496F` | test top view count; `JBE` | no seal work for cells without live views |
|
||||
| `0x5A49A9` / `0x5A49AF` | compare portal other id with `-1`; `JNZ` skips | only outside portals are sealed |
|
||||
| `0x59BCD6–0x59BD28`, then `0x59BD40–0x59BD66` | each source vertex's local `x/y` is compared before its `xformStart`; four final predicate tests branch to return | whole poly on any local-input `x/y == +/-12` boundary is rejected before count/clip |
|
||||
| `0x59BD70–0x59BD74` | test boolean; nonzero jumps past `INC word ptr [0x8719B4]` | false/true-depth seals increment modulo 65,536; true/far punches do not |
|
||||
| `0x59BDBC–0x59BDBF` | compare clipped count with 3; `JL` return | fewer than 3 clipped vertices submit no fan, after possible increment |
|
||||
| `0x5A5666–0x5A566C` | compare `other_cell_id` with `-1`; `JNZ` neighbor path | `0xFFFFFFFF` is outside path |
|
||||
| `0x5A5671–0x5A5673` | test `draw_landscape`; zero skips | only the indoor PView accumulates outside view |
|
||||
| `0x5A5A6E–0x5A5A7B` | mode 2 skips punch; mode 1 falls through to `DrawPortalPolyInternal(true)` | building pass 1 punches; pass 2 constructs destination |
|
||||
| `0x5A5B46–0x5A5B53` | success then compare mode 1; mode 1 skips `DrawCells` | far-punch pass has no look-in draw; later pass does |
|
||||
| `0x5A5B74–0x5A5B7C` | on failure, only mode 3 draws false-mode portal poly | failure fallback is true-depth only for mode 3 |
|
||||
| `0x59FEC0–0x59FEC8` | unconditional load, `INC`, store after the present/reset tail in `Flip` | normal per-present `m_nFrameStamp` advancement is independent of DrawCells' conditional mid-frame increment |
|
||||
|
||||
## Render-frame stamp versus visibility timestamp
|
||||
|
||||
Retail maintains two unrelated generation variables:
|
||||
|
||||
- `PView::master_timestamp @0x008ED6B8` increments in every `ConstructView(CEnvCell)` and drives visibility construction.
|
||||
- `RenderDevice::m_nFrameStamp @ RenderDevice+0xB0` is the render dedupe stamp. It has **two** increment sites relevant here:
|
||||
1. `RenderDeviceD3D::Flip @0x0059FE50`, load/increment/store at `0x0059FEC0–0x0059FEC8`, increments once after the normal swap-chain present, texture-stage unbind, dynamic-buffer reset, and profiler-frame reset;
|
||||
2. `DrawCells @0x005A4885–0x005A4886` adds a conditional mid-frame increment after an outside landscape/alpha flush and before the counter/clear.
|
||||
|
||||
The render stamp is consumed by at least:
|
||||
|
||||
- `CEnvCell+0x130`: `GetDrawnThisFrame @0x0052C0C0`, `SetDrawnThisFrame @0x0052C0E0`;
|
||||
- `CPhysicsPart+0xDC`: `GetDrawnThisFrame @0x0050D4D0`, `SetDrawnThisFrame @0x0050D4F0`.
|
||||
|
||||
`DrawEnvCell @0x0059F170` begins with the stamp gate and the built-mesh path draws the complete constructed shell once for that stamp. `DrawMeshInternal @0x0059F360` applies a corresponding stamp to non-local-player physics parts. The normal `Flip` increment makes the following presented frame eligible. When an indoor outside stage runs, content admitted during landscape/building look-ins is stamped with that frame's value; the additional `DrawCells` increment then re-arms it for the root repaint after the possible clear. A faithful port therefore needs both boundaries: the ordinary per-present boundary and the conditional mid-frame barrier. Neither may be conflated with the PView flood timestamp.
|
||||
|
||||
## Current acdream correspondence
|
||||
|
||||
The inspected production locations are in the binding worktree named at the top of this report.
|
||||
|
||||
| Retail mechanism | Current acdream | Assessment |
|
||||
|---|---|---|
|
||||
| separate indoor/outdoor PViews; outdoor `DrawLandscape=false` | `src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs:46–55`; `WalkPView.cs:39–50` | faithful basic ownership |
|
||||
| outside-view reset and `0xFFFFFFFF` producer gated by `DrawLandscape` | `WalkPView.cs:59–75`, `175–250` | faithful basic producer |
|
||||
| indoor flood calls landscape only when outside views exist | `RetailFrameWalk.cs:118–142` | faithful at walk level |
|
||||
| landscape then root-flood turn | `RetailFrameWalk.cs:118–142` | faithful coarse order |
|
||||
| persistent `uint16 portalsDrawnCount` across calls/frames | no production state found; source search finds only comments | **missing** |
|
||||
| counter populated only by accepted false-mode attempts | `OnPunchGeometry` records only punch events; exit seal draw is a replay callback | **missing** producer semantics |
|
||||
| outside-view block gates stamp, consume/reset, clear, and seals | `WalkFrameDriver.cs:1299–1339` emits clear/seals unconditionally for every interior flood | **wrong** |
|
||||
| first eligible call with counter zero does not clear | `WalkFrameDriverTests.cs:260–293` expects `CLEAR` on the first outside-view fixture | **wrong test contract** |
|
||||
| `outside_view==0` performs no clear/seal/**additional DrawCells stamp** | `WalkFrameDriverTests.cs:432–440` explicitly expects unconditional `CLEAR`, `SEALS` | **wrong test contract**; normal per-present frame boundary is unaffected |
|
||||
| normal stamp boundary once per present | `WalkFrameDriver.BeginFrame` at `WalkFrameDriver.cs:784–818` calls `BeginWalkPartFrame` and clears shell dedupe; `Wb/WbDrawDispatcher.WalkClassify.cs:89–99` clears part dedupe | semantically represents the ordinary per-frame eligibility boundary, though it is tied to collection rather than an actual present |
|
||||
| additional stamp barrier after landscape/flush and before clear | `WalkFrameDriver.cs:1304–1319` advances part-pass stamp and clears shell dedupe when `_skyDrawnThisFrame` | close in semantic placement and correctly conditional on a landscape turn; collection-time mirror, not exact GPU execution boundary |
|
||||
| full Z clear to 1.0, no color | `VulkanWorldPassScope.cs:87–120` | faithful clear operation |
|
||||
| shell pass then object pass, both reverse order | `WalkFrameDriver.cs:1419–1453` | faithful coarse ordering |
|
||||
| far punch depth exact bits `0x3F7FFFEF` | `Shaders/portal_depth.vert:53` uses decimal `0.99999988` | **wrong constant** |
|
||||
| whole-polygon **local-input, pre-`xformStart`** `x/y == +/-12` rejection | no equivalent in `PortalDepthMaskRenderer.Rhi.cs:106–156` or punch event production | **missing** |
|
||||
| CPU `polyClipFinish`, count after boundary reject but before clip success | GPU `gl_ClipDistance`, capped to `ClipFrame.MaxPlanes == 8`; no clipped-vertex result | **approximation**, not byte/vertex exact |
|
||||
| depth `ALWAYS`, depth write on, no cull; color writes enabled with `SRCALPHA`/`INVSRCALPHA` and source alpha zero | `PortalDepthMaskRenderer.Rhi.cs:58–104` uses `ColorWrite=false` | depth/color **outcome-equivalent**, but render state diverges from retail |
|
||||
| per-landcell `DrawLandCell` then `DrawSortCell` | `WalkFrameDriver.cs:105–114`, `1345–1379` explicitly emits all terrain before per-cell building/static turns | **known wrong order** |
|
||||
| `Render::PortalList=&outside_view` remains installed for the whole outside `LScape::draw` | `RetailPViewPassExecutor.WalkLeaf.cs:28–50`, `94–116` deliberately draws sky/terrain without exact slice clipping | approximation; leaf-level pixel consequence unresolved here |
|
||||
| true-depth seals emitted for each outside portal of each live view, reverse flood | `RetailPViewRenderer.cs:656–685` | geometry loop is broadly shaped correctly, but called under wrong gate and has no counter feedback |
|
||||
|
||||
The current comments in `WalkFrameDriver.cs:138–165` accurately label the clear gate as an acknowledged approximation, but the later implementation comment at `1321–1325` calls clear/seals “both unconditional.” Ghidra proves that statement false: the entire stage is skipped when outside-view count is zero, and the clear has a second persistent prior-counter gate.
|
||||
|
||||
### Current event ordering that must not be mistaken for retail
|
||||
|
||||
Today the interior event list is effectively:
|
||||
|
||||
```text
|
||||
BeginFrame:
|
||||
clear CPU part/shell dedupe # ordinary presented-frame boundary analogue
|
||||
if outside_view > 0:
|
||||
SKY
|
||||
TERRAIN (all terrain before buildings/statics)
|
||||
if sky was emitted:
|
||||
advance CPU render-stamp mirrors # additional DrawCells barrier analogue
|
||||
CLEAR # always, including outside_view == 0
|
||||
SEALS # always, including outside_view == 0
|
||||
reverse shells
|
||||
reverse cell contents
|
||||
```
|
||||
|
||||
Retail is:
|
||||
|
||||
```text
|
||||
# frame starts with the stamp established by the prior Flip
|
||||
if outside_view > 0:
|
||||
LANDSCAPE # terrain/building/object interleaved per landcell
|
||||
alpha flush
|
||||
++render stamp # additional mid-frame barrier
|
||||
prior = sealAttemptCount
|
||||
sealAttemptCount = 0
|
||||
if prior != 0:
|
||||
CLEAR
|
||||
SEAL CURRENT EXITS # accepted attempts repopulate counter
|
||||
reverse shells
|
||||
reverse object cells
|
||||
reset object scale; useSunlightSet(1)
|
||||
top-level alpha flush
|
||||
Flip/present: ++render stamp # ordinary per-present boundary
|
||||
```
|
||||
|
||||
## Required executable transcript
|
||||
|
||||
An implementation slice should not be visually gated before it can emit an exact, machine-assertable transcript. Aggregate owner ids are insufficient. Use one record per `DrawCells` call, one per mask attempt, and one per normal present boundary.
|
||||
|
||||
### DrawCells record
|
||||
|
||||
```text
|
||||
[oh1-dc] present=<u64> call=<u64> source=inside|building-portal \
|
||||
root=0x<8hex> pview=indoor|outdoor drawLandscape=0|1 outsideViews=<u32> \
|
||||
counterBefore=<u16> forceClear=0|1 landscape=0|1 alphaFlush=0|1 \
|
||||
stampBefore=<u32> midStampIncrement=0|1 stampAfter=<u32> \
|
||||
consumed=<u16> counterReset=0|1 depthClear=0|1 \
|
||||
sealAttempts=<u32> sealGpuSubmissions=<u32> counterAfter=<u16> \
|
||||
shellCells=<ordered cell ids> objectCells=<ordered cell ids>
|
||||
```
|
||||
|
||||
Assertions enabled by this line:
|
||||
|
||||
- `outsideViews==0` implies `landscape=alphaFlush=midStampIncrement=counterReset=depthClear=sealAttempts=0` and `stampAfter==stampBefore` within this call;
|
||||
- with shipped `forceClear==0`, `outsideViews>0` implies `consumed==counterBefore`, `counterReset==1`, and `depthClear==(consumed!=0)`;
|
||||
- after the reset, `counterAfter == sealAttempts mod 65536` unless another false-mode caller occurs before line emission; the general assertion is `counterAfter == (counterBase + acceptedAttempts) & 0xffff`;
|
||||
- `sealGpuSubmissions` is only CPU-side submission count. It is not retail `fanDrawn`, because the current GPU clip-distance path returns no surviving-vertex or fragment result;
|
||||
- shell and object lists are independently reverse ordered.
|
||||
|
||||
### Normal present/stamp record
|
||||
|
||||
```text
|
||||
[oh1-present] present=<u64> flipCall=<u64> stampBefore=<u32> \
|
||||
swapResult=ok|deviceLost stampAfter=<u32>
|
||||
```
|
||||
|
||||
After `Flip`'s Present retry loop terminates, every return path executes `stampAfter == stampBefore + 1 mod 2^32` (the device-lost result sets its flag first). This is independent of the optional `midStampIncrement` in a `DrawCells` record.
|
||||
|
||||
### Portal mask record
|
||||
|
||||
```text
|
||||
[oh1-mask] present=<u64> call=<u64> ordinal=<u32> \
|
||||
source=building-pass1|exit-seal|mode3-fallback cell=0x<8hex> \
|
||||
view=<u16> portal=<u16> other=0x<8hex> mode=far|true \
|
||||
inputVerts=<u16> localBoundaryWouldReject=0|1 boundaryRejectApplied=0|1 \
|
||||
gpuSubmitted=0|1 clipPath=gpu-clip-distance|retail-polyClipFinish \
|
||||
counterBefore=<u16> counterDelta=0|1 counterAfter=<u16> \
|
||||
depthBits=0x3f7fffef|projected depthTest=always depthWrite=1 cull=none \
|
||||
retailBlend=srcalpha/invsrcalpha retailSourceAlpha=0 retailColorWrite=1 \
|
||||
activeColorWrite=0|1
|
||||
```
|
||||
|
||||
Assertions enabled by this line:
|
||||
|
||||
- `mode=far` always has `counterDelta=0` and exact `depthBits=0x3f7fffef`;
|
||||
- `boundaryRejectApplied=1` implies `counterDelta=0` and `gpuSubmitted=0`;
|
||||
- an accepted `mode=true` attempt has conceptual `counterDelta=1`, with `counterAfter == (counterBefore + 1) & 0xffff`, even if retail clipping would later remove the fan;
|
||||
- the current pipeline must report `activeColorWrite=0`; a state-faithful retail port reports 1 with the listed blend/source-alpha state. Both preserve destination color, but they are not the same render state;
|
||||
- only `other=0xffffffff` appears with `source=exit-seal`.
|
||||
|
||||
The current GPU path cannot truthfully populate `clippedVerts` or `fanDrawn`: `gl_ClipDistance` runs after CPU submission and returns neither value. Do not synthesize them from `gpuSubmitted`. If `polyClipFinish` is ported or a retail capture is instrumented, emit a separate record:
|
||||
|
||||
```text
|
||||
[oh1-retail-clip] present=<u64> call=<u64> ordinal=<u32> \
|
||||
inputVerts=<u16> clippedVerts=<u16> fanDrawn=0|1
|
||||
```
|
||||
|
||||
Only that audited CPU clip result may assert `fanDrawn == (clippedVerts >= 3)`. Porting or line-by-line auditing `polyClipFinish @0x006B6D00` is required before claiming exact clip/fan parity.
|
||||
|
||||
### Landscape ordering record
|
||||
|
||||
```text
|
||||
[oh1-order] present=<u64> dcCall=<u64> ordinal=<u32> \
|
||||
stage=sky|landcell|building-flush|punch|lookin-shell|lookin-object|building-shell|landcell-object|weather|outside-flush|mid-stamp|clear|seal|root-shell|root-object|sunlight-tail|top-alpha-flush|flip-stamp \
|
||||
landblock=0x<8hex> landcell=0x<8hex> building=<stable instance key> \
|
||||
cell=0x<8hex> view=<i32> portal=<i32>
|
||||
```
|
||||
|
||||
This record makes the per-landcell interleave and punch position testable without asking a user to identify a moving slab visually.
|
||||
|
||||
## Implementation constraints derived from retail
|
||||
|
||||
This section is evidence-derived guidance, not a production edit.
|
||||
|
||||
1. Introduce one persistent `ushort` counter at the render/depth lifecycle owner. Do not reset it at frame start, world-walk start, or outdoor root start. Increment it with unchecked modulo-65,536 semantics.
|
||||
2. Gate the entire outside stage on `outside_view.view_count != 0`.
|
||||
3. Within that block preserve the exact order: landscape → alpha flush → **additional mid-frame render-stamp increment** → consume/reset counter → optional full depth clear → current true-depth exit seals.
|
||||
4. Evaluate the `+/-12` whole-polygon predicate from original local input vertex `x/y` before transformation. Count accepted true-depth mask **attempts** after that rejection and before final clipping decides whether a fan exists.
|
||||
5. Far punches never affect the counter. Use the exact far-Z bit pattern, not a rounded decimal approximation.
|
||||
6. Preserve both render-stamp increments: the ordinary `Flip`/present increment and the conditional mid-`DrawCells` barrier. Keep that render stamp distinct from PView's flood timestamp.
|
||||
7. Preserve independent reverse shell and object passes after the optional outside stage, then reset object scale and execute the final `useSunlightSet(1)` before the caller's top-level alpha flush.
|
||||
8. Restore retail's state-faithful portal fan setup: color writes enabled, `SRCALPHA`/`INVSRCALPHA` blending, and source alpha zero. The current `ColorWrite=false` path is outcome-equivalent for destination color but is not the same state.
|
||||
9. Port or line-by-line audit `polyClipFinish` before asserting clipped vertex counts or successful retail fan draws. Until then, trace only boundary decisions, attempt counts, and GPU submissions.
|
||||
10. Restore retail's per-landcell terrain/building/object interleave before claiming full order parity. Merely fixing the counter leaves punch-versus-terrain ordering different.
|
||||
11. Replace tests that demand unconditional clear/seals with a state-machine matrix: first eligible call, consecutive eligible calls, zero-outside gap, resume after gap, boundary-rejected seals, clipped-away accepted seals, far punches, and `ushort` wrap at 65,535/65,536 accepted attempts.
|
||||
|
||||
## Unresolved facts
|
||||
|
||||
1. The exact pixel-level effect of keeping `Render::PortalList = &outside_view` installed throughout retail sky/terrain rendering was not expanded through every `LScape` leaf in this slice. The top-level state and order are proven; whether each leaf clips geometry or only uses the view union requires a separate leaf audit.
|
||||
2. GPU `gl_ClipDistance` may be geometrically similar to retail `polyClipFinish`, but numerical/vertex-set equivalence is not proven. The current eight-plane cap, interpolation rules, and lack of a clipped vertex count prevent a claim of byte/execution parity.
|
||||
3. The dormant `forceClear` branch is fully decoded, but no write to `0x008ED824` exists in this retail binary. Its practical activation mechanism, if any, is external to the binary and irrelevant to shipped behavior.
|
||||
4. The mode-3 failure fallback exists in `DrawPortal`, but the examined two-pass building BSP walk supplies modes 1 and 2. No production caller reaching mode 3 was established in this slice.
|
||||
|
||||
## Closeout
|
||||
|
||||
The decisive OH1 correction is not “find the guilty slab.” Retail has a small persistent depth state machine whose timing crosses eligible `DrawCells` calls. acdream currently substitutes unconditional per-interior-root events and a differently ordered landscape stream. The next implementation slice can be deterministic: port the state machine and exact mask predicates/constants, emit the transcript above, and only then run the cathedral visual gate.
|
||||
440
docs/research/2026-09-01-overhaul/oh1-retail-world-contract.md
Normal file
440
docs/research/2026-09-01-overhaul/oh1-retail-world-contract.md
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
# OH1 — retail world construction and frame contract
|
||||
|
||||
**Date:** 2026-09-01
|
||||
|
||||
**Campaign:** OVERHAUL / OH1
|
||||
|
||||
**Status:** evidence synthesis; tooling implementation and review gate pending
|
||||
|
||||
**Behavioral changes:** none in this document
|
||||
|
||||
## 1. Decision
|
||||
|
||||
Retail does not build a set of portal-clipped copies of ordinary world meshes.
|
||||
It builds portal views, uses each installed view to perform Boolean admission,
|
||||
and submits a complete built mesh after the first successful admission. A
|
||||
render stamp suppresses later ordinary submissions in the same stamp. The
|
||||
local player is a deliberate exception to the ordinary part-stamp gate.
|
||||
|
||||
The solid world is therefore the composition of four exact mechanisms, not a
|
||||
single clipping heuristic:
|
||||
|
||||
1. one canonical object-registration transaction determines the ordered cells
|
||||
crossed by the object and each render part;
|
||||
2. the PView walk determines cell/view admission and exact traversal order;
|
||||
3. ordinary built parts use sphere admission followed by a whole-mesh
|
||||
submission, built EnvCell shells use a direct stamp gate followed by a
|
||||
whole-shell submission, and portal mask polygons alone use polygon clipping;
|
||||
4. the depth epoch, two render-stamp boundaries, landscape/building interleave,
|
||||
and two FIFO alpha lists determine which admitted surfaces finally cover
|
||||
which others.
|
||||
|
||||
The current acdream path already has a materially aligned candidate for the
|
||||
ordinary part rule: it tests the live views, selects slot zero after admission,
|
||||
emits one whole mesh, stamps after admission, and preserves the local-player
|
||||
bypass. The remaining divergences are membership reconstruction, landscape/
|
||||
depth order, the portal-fan approximation, and alpha ownership. Campaign
|
||||
OVERHAUL must preserve the proven ordinary-part behavior while repairing those
|
||||
owners rather than tune cathedral-specific culling.
|
||||
|
||||
## 2. Evidence packets and authority
|
||||
|
||||
This synthesis is backed by the following focused reports:
|
||||
|
||||
- `oh1-construction-landscape-contract.md` — static-object construction,
|
||||
crossed-cell membership, child propagation, and landscape leaf order;
|
||||
- `oh1-built-mesh-view-contract.md` — installed views, built-mesh admission,
|
||||
draw stamps, and alpha ownership;
|
||||
- `oh1-alpha-list-contract.md` — exact subset-mask priority, delayed-list
|
||||
classification, preference/detail inputs, capacity, and flush barriers;
|
||||
- `oh1-depth-lifecycle.md` — outside-view gate, persistent portal counter,
|
||||
depth clear/punch/seal state, and both render-stamp advances;
|
||||
- `oh2-cellstruct-surface-contract.md` — exact CellStruct side, surface,
|
||||
subset, mask, and built-EnvCell material admission contract.
|
||||
|
||||
The named Sept. 2013 retail corpus was inspected first. Branch facts that
|
||||
could change the result were independently arbitrated in the live Ghidra
|
||||
project. The load-bearing addresses are:
|
||||
|
||||
| Retail function | Address | Contract owned here |
|
||||
|---|---:|---|
|
||||
| `CPhysicsObj::calc_cross_cells_static` | `0x00515160` | object and part cell membership |
|
||||
| `CPartArray::AddPartsShadow` | `0x00517E40` | per-part render-shadow insertion |
|
||||
| `CPhysicsObj::add_shadows_to_cells` | `0x00514AE0` | shared ordered `CELLARRAY` consumption |
|
||||
| `PView::DrawCells` | `0x005A4840` | outside gate, list direction, depth epoch |
|
||||
| `Render::copy_view` / `Render::set_view` | named/Ghidra report | view polygon/planes and global installation |
|
||||
| `RenderDeviceD3D::DrawEnvCell` | `0x0059F170` | shell stamp and complete-shell draw |
|
||||
| `RenderDeviceD3D::DrawMeshInternal` | `0x0059F360` | ordinary part admission/stamp |
|
||||
| `D3DPolyRender::DrawMesh` | `0x0059D4A0` | material subset admission/alpha routing |
|
||||
| `D3DPolyRender::AddMeshToAlphaList` | `0x0059C230` | FIFO alpha-list ownership |
|
||||
| `D3DPolyRender::FlushAlphaList` | `0x0059D2E0` | clip-list then alpha-list drain |
|
||||
| `DrawPortalPolyInternal` | `0x0059BC90` | the actual polygon-clipped depth fan |
|
||||
| `RenderDeviceD3D::Flip` | `0x0059FE50` | normal per-present render-stamp advance |
|
||||
|
||||
No visual observation is used to decide an object identity, branch direction,
|
||||
surface side, or list order.
|
||||
|
||||
## 3. Canonical construction and membership
|
||||
|
||||
### 3.1 Static object registration
|
||||
|
||||
For an authored EnvCell static, retail creates the `CPhysicsObj`, enters it in
|
||||
the authored cell, copies the authored frame, updates children, and then runs
|
||||
static crossed-cell calculation. That calculation owns one `CELLARRAY` and
|
||||
produces both object-level physics membership and part-level render-shadow
|
||||
membership.
|
||||
|
||||
The exact high-level transaction is:
|
||||
|
||||
```text
|
||||
create static object
|
||||
add_obj_to_cell(authored cell)
|
||||
copy/set authored frame
|
||||
update children
|
||||
calc_cross_cells_static
|
||||
initialize CELLARRAY in source order
|
||||
choose retail sphere or per-part bounding-box route
|
||||
remove prior shadows
|
||||
add new CShadowObj entries to every crossed cell
|
||||
AddPartsShadow for the exact part/cell intersections
|
||||
propagate the same CELLARRAY to children
|
||||
```
|
||||
|
||||
The bounding-box route evaluates visual parts in part order. A part may use
|
||||
its physics sphere, or its drawing sphere for the quick portal-plane reject,
|
||||
then its exact transformed bounding box for portal intersection and neighbour
|
||||
containment. An outside crossing is retained by this route; it is not pruned
|
||||
later merely because an aggregate owner cell appears preferable.
|
||||
|
||||
`add_shadows_to_cells` consumes the same ordered `CELLARRAY` twice: once for
|
||||
object-level `CShadowObj` membership and again for exact part-shadow entries.
|
||||
Children inherit the root object's transaction. Removal is symmetric. The
|
||||
particle-state special case uses its one current-cell route and must remain an
|
||||
explicit typed case.
|
||||
|
||||
### 3.2 Ownership consequence
|
||||
|
||||
Render and physics membership are sibling outputs of one canonical
|
||||
registration operation. Neither is allowed to reconstruct membership from a
|
||||
per-frame scene record, an aggregate owner id, or a fresh AABB query.
|
||||
|
||||
Cell traversal order and part-shadow insertion order are related only at the
|
||||
draw join:
|
||||
|
||||
- registration fixes the stable order of each cell's shadow objects and
|
||||
parts;
|
||||
- PView fixes the cells and views visited in the current frame;
|
||||
- the object leaf draws the already-registered shadows for that cell, with
|
||||
retail's `CShadowPart::insertion_sort` establishing the cell-local order.
|
||||
|
||||
PView traversal must not recreate part membership, and membership construction
|
||||
must not predict a later PView order.
|
||||
|
||||
## 4. Canonical view and built-mesh contract
|
||||
|
||||
### 4.1 What a retail view does
|
||||
|
||||
`Render::copy_view` copies the view polygon and derives its edge planes.
|
||||
`Render::set_view` installs that view in render globals. It does not establish
|
||||
a hardware scissor or a general polygon clip for every following mesh.
|
||||
`Render::viewconeCheck` is the ordinary sphere admission test.
|
||||
|
||||
### 4.2 Ordinary built object
|
||||
|
||||
For an ordinary built GfxObj part, retail walks the live views for the cell:
|
||||
|
||||
1. install a view;
|
||||
2. test the part's sphere;
|
||||
3. if rejected, try the next view without stamping the part;
|
||||
4. on the first accepted view, enter `DrawMeshInternal`, stamp the non-player
|
||||
part, and submit the complete eligible constructed mesh;
|
||||
5. later accepted views encounter the stamp and do not submit another copy.
|
||||
|
||||
Thus multiple surviving object views normally produce repeated admission
|
||||
tests followed by one whole-mesh draw, not multiple portal-clipped copies.
|
||||
|
||||
The local player bypasses the inner ordinary-part stamp. It can therefore be
|
||||
submitted whole for more than one admitted cell/view. That retail exception is
|
||||
load-bearing at seams and must not be normalized into the ordinary object
|
||||
rule.
|
||||
|
||||
### 4.3 Built EnvCell shell
|
||||
|
||||
`DrawEnvCell` has no sphere admission. It gates the cell directly by the
|
||||
current render stamp before doing work.
|
||||
The built branch submits the complete eligible constructed shell once for that
|
||||
stamp. The forced building pass is separate: it draws the building portal BSP
|
||||
fans; it is not a clipped substitute for the ordinary shell.
|
||||
|
||||
### 4.4 Geometry that is actually clipped
|
||||
|
||||
Retail polygon clipping occurs in the portal machinery (`PView::GetClip` and
|
||||
`DrawPortalPolyInternal` through `polyClipFinish`). It produces the far-depth
|
||||
punch and true-depth seal fans. It is not the submission path for normal built
|
||||
EnvCell shells, static parts, dynamic parts, or the local player.
|
||||
|
||||
## 5. Canonical frame, landscape, depth, and stamp order
|
||||
|
||||
### 5.1 Two different timestamps and two render-stamp advances
|
||||
|
||||
`PView::master_timestamp` belongs to visibility construction.
|
||||
`RenderDevice::m_nFrameStamp` belongs to render deduplication. They are not
|
||||
interchangeable.
|
||||
|
||||
The render stamp normally advances once on every `RenderDeviceD3D::Flip`
|
||||
return path after the Present retry loop, including a device-lost result. When
|
||||
an interior `DrawCells` has at least one
|
||||
outside view, retail advances it a second time in the middle of that frame,
|
||||
after landscape and its alpha flush and before the previous-call portal
|
||||
counter is consumed. The mid-frame advance re-arms content drawn through
|
||||
landscape/building look-ins for the later interior-root repaint.
|
||||
|
||||
### 5.2 Interior `DrawCells`
|
||||
|
||||
The binding order is:
|
||||
|
||||
```text
|
||||
if outside_view.view_count != 0:
|
||||
useSunlightSet(1)
|
||||
draw landscape with Render::PortalList = outside_view
|
||||
sky
|
||||
for each admitted land cell in retail order:
|
||||
DrawLandCell
|
||||
DrawSortCell
|
||||
building pass 1 / look-in work / building shell as applicable
|
||||
land-cell object turn
|
||||
weather
|
||||
flush landscape alpha
|
||||
increment render stamp # conditional mid-frame barrier
|
||||
consume previous portalsDrawnCount
|
||||
reset that uint16 counter to zero
|
||||
full viewport depth clear iff consumed count != 0
|
||||
emit current true-depth exit-seal attempts
|
||||
for reverse cells, every live view, every outside portal
|
||||
|
||||
useSunlightSet(0)
|
||||
restore interior lighting
|
||||
draw EnvCell shells in reverse cell_draw_list order
|
||||
draw object cells in reverse cell_draw_list order
|
||||
reset object scale
|
||||
useSunlightSet(1)
|
||||
return to caller
|
||||
flush top-level alpha at the caller's barrier
|
||||
later Flip/present increments render stamp # normal frame boundary
|
||||
```
|
||||
|
||||
When `outside_view.view_count == 0`, the entire landscape/intermediate-flush/
|
||||
mid-stamp/counter-consume/clear/seal block is skipped. Any pending portal count
|
||||
survives the gap. The reverse shell/object passes and sunlight tail still run,
|
||||
and the normal later `Flip` increment still occurs.
|
||||
|
||||
### 5.3 Persistent portal counter
|
||||
|
||||
`portalsDrawnCount` is a persistent machine `uint16`. A non-far
|
||||
`DrawPortalPolyInternal` attempt increments it after the local-boundary reject
|
||||
but before `polyClipFinish`; the increment therefore does not prove that a fan
|
||||
survived clipping. The next eligible outside-view `DrawCells` consumes and
|
||||
resets the prior count. Arithmetic wraps modulo 65,536. Far-depth punches do
|
||||
not increment it.
|
||||
|
||||
### 5.4 Portal mask state
|
||||
|
||||
The far punch uses exact depth bits `0x3F7FFFEF`. Before `xformStart`, retail
|
||||
rejects the whole source polygon if all local input vertices lie on any one of
|
||||
`x == +12`, `x == -12`, `y == +12`, or `y == -12`.
|
||||
|
||||
An accepted portal fan uses no texture, alpha test off,
|
||||
`SRCALPHA/INVSRCALPHA/ADD`, depth test `ALWAYS`, depth write on, cull none,
|
||||
and normal color writes enabled. Its source alpha is zero, so the blend
|
||||
preserves destination color while depth changes. Disabling color writes is
|
||||
pixel-outcome-equivalent for that fan, but it is not the retail state.
|
||||
|
||||
The current GPU `gl_ClipDistance` route cannot report retail's clipped vertex
|
||||
count or whether `polyClipFinish` produced a fan. Those fields must remain
|
||||
explicitly unavailable until the CPU clip is ported or audited; they must not
|
||||
be inferred from CPU submission.
|
||||
|
||||
## 6. Canonical built-EnvCell surface and alpha contract
|
||||
|
||||
CellStruct sides are selected only by `sides_type`. `NoPos` and `NoNeg` mean
|
||||
that a UV-index array is absent; they do not remove a face. A missing UV array
|
||||
uses UV index/coordinates zero.
|
||||
|
||||
| `sides_type` | Ordered constructed candidates |
|
||||
|---:|---|
|
||||
| `ST_SINGLE` (`0`) | positive surface, positive normal, ordinary fan |
|
||||
| `ST_DOUBLE` (`1`) | positive candidate, then the same positive surface with negative normal and reversed fan |
|
||||
| `ST_BOTH` (`2`) | positive candidate, then negative surface/UV with negative normal and **no** index reversal |
|
||||
|
||||
The semantic subset/material owner is the source surface-array index. Nonempty
|
||||
subsets are emitted in ascending surface-index order. Equal Surface DIDs in two
|
||||
slots remain distinct. Stippling and format grouping do not create the owner.
|
||||
|
||||
The built EnvCell later draws a subset exactly when:
|
||||
|
||||
```text
|
||||
(Surface.Type & (BASE1_IMAGE | BASE1_CLIPMAP)) != 0
|
||||
```
|
||||
|
||||
Untextured candidates are constructed and then skipped by this built-cell
|
||||
draw rule. Ordinary GfxObj behavior has a different fallback and must not
|
||||
inherit this filter.
|
||||
|
||||
Retail owns two delayed mesh lists: clip first, then alpha. Both are FIFO; no
|
||||
distance sort is performed at flush. Ordering was already established by the
|
||||
cell walk and cell-local shadow-part sort. The exact constructed-mask priority,
|
||||
signed positive-surface stippling quirk, immediate/delayed branch table,
|
||||
material-alpha fallback, multipass clip-map duplicate, detail/sky gates,
|
||||
capacity behavior, preference defaults, and flush thresholds are established
|
||||
in `oh1-alpha-list-contract.md`. A delayed entry carries no portal view, cell,
|
||||
or part context, so queuing a portal-slice mesh for later cannot reproduce
|
||||
retail.
|
||||
|
||||
## 7. Direct answers required by OH1
|
||||
|
||||
**Which built meshes are submitted whole after a Boolean sphere test?**
|
||||
|
||||
Ordinary built GfxObj parts. They try live views until the first sphere
|
||||
admission and then submit the complete eligible mesh. Built EnvCell shells do
|
||||
not have a sphere test; they are stamp-gated directly and drawn complete.
|
||||
|
||||
**Which geometry is polygon-clipped by the installed portal view?**
|
||||
|
||||
Portal clip/punch/seal polygons in the PView and portal-depth paths. Normal
|
||||
built shells and object meshes are not clipped into per-aperture copies.
|
||||
|
||||
**Do multiple surviving views cause multiple built-mesh draws?**
|
||||
|
||||
Normally no: they cause repeated admission tests until the first accepted
|
||||
whole draw stamps the part. The local player deliberately bypasses that inner
|
||||
part stamp and is an exception.
|
||||
|
||||
**Which stamp applies to landscape look-ins versus the interior root?**
|
||||
|
||||
Landscape and its building look-ins draw under the stamp established by the
|
||||
prior `Flip`. An eligible interior `DrawCells` then advances the stamp
|
||||
mid-frame, so the root repaint uses the next stamp. Every `Flip` return path
|
||||
after the Present retry loop advances it again for the following frame,
|
||||
including the device-lost outcome.
|
||||
|
||||
**Which list owns transparent EnvCell subsets?**
|
||||
|
||||
When retail delays a subset, mask bit `0x08` selects the CLIP FIFO; other
|
||||
delayed masks select the ALPHA FIFO. Flush drains CLIP before ALPHA. A non-null
|
||||
current detail surface, sky rendering, or zero global delay mask forces
|
||||
immediate rendering. With default delay mask `0x0E`, masks `2/3/4/5` enter
|
||||
ALPHA and masks `8/9` enter CLIP; multipass clip maps also render an immediate
|
||||
base pass. Masks `0/1` are immediate unless the current material's alpha
|
||||
fallback applies. `oh1-alpha-list-contract.md` is the executable table.
|
||||
|
||||
**What increments `portalsDrawnCount`, and when is it cleared?**
|
||||
|
||||
Every accepted non-far portal-mask attempt after the local-boundary reject,
|
||||
even if clipping later yields fewer than three vertices. The next
|
||||
outside-view-bearing `DrawCells` consumes and clears it before current exit
|
||||
seals repopulate it. Zero-outside-view calls neither consume nor reset it.
|
||||
|
||||
**How do cell traversal and shadow insertion relate?**
|
||||
|
||||
Registration creates stable ordered shadow memberships first. PView later
|
||||
chooses and orders cell turns. The object leaf consumes the existing cell
|
||||
membership and retail sort; neither stage reconstructs the other.
|
||||
|
||||
## 8. Current acdream divergences and owning slices
|
||||
|
||||
| Current behavior | Retail contract | Owning slice |
|
||||
|---|---|---|
|
||||
| CellStruct face admission uses `NoPos`/`NoNeg`; batches are texture/format/stipple keyed | sides come from `sides_type`; UV absence only; source surface-index subsets | OH2 |
|
||||
| App rebuilds render buckets from scene records/AABBs and aggregate owner cells | one registration transaction publishes exact render/physics membership | OH3 |
|
||||
| ordinary parts already test views, select slot zero after admission, emit once, stamp after admission, and preserve the player bypass | materially aligned with retail whole-mesh admission; protect with executable transcript and remove only residue that contradicts it | OH4 validation/deletion |
|
||||
| all terrain is emitted before building/object turns | `DrawLandCell` then `DrawSortCell` per admitted land cell | OH5 |
|
||||
| clear/seals are emitted even with no outside view; no persistent prior-call counter | entire depth block is outside-view gated; persistent modulo-`uint16` counter | OH6 |
|
||||
| far depth uses an approximate constant/boundary/clip path | exact bits, local-input boundary reject, audited clip semantics | OH6 |
|
||||
| one distance-sorted alpha queue retains slice context | two FIFO lists, exact classification and barriers, no retained portal context | OH7 |
|
||||
| landscape/particle/light consumers use reconstructed visibility unions | each consumer borrows the exact retail walk and registration outputs | OH8/OH9 |
|
||||
|
||||
The current architecture document overstates several of these candidate
|
||||
mechanisms as accepted retail behavior. OH1 must correct that SSOT before its
|
||||
review closes; later slices then replace each explicitly recorded divergence.
|
||||
|
||||
## 9. Executable evidence contract
|
||||
|
||||
OH1 tooling uses a versioned semantic transcript. Stable source IDs, exact
|
||||
event order, integer states, and raw float bits participate in equality.
|
||||
Pointers, Vulkan handles, allocation addresses, timestamps, and upload offsets
|
||||
do not.
|
||||
|
||||
The required semantic record families are:
|
||||
|
||||
1. frame header and presented-stamp transition;
|
||||
2. camera/player/root identity and raw pose bits;
|
||||
3. PView creation, todo insertion/pop, clipped portal view, and draw-list
|
||||
append;
|
||||
4. landscape cell admission and exact per-cell leaf order;
|
||||
5. building pass, look-in, alpha barrier, punch, and shell events;
|
||||
6. EnvCell shell and object-cell turns;
|
||||
7. part admission, source membership, and render stamp result;
|
||||
8. particle owner/degrade result;
|
||||
9. alpha enqueue/list/ordinal and flush site;
|
||||
10. outside-block counter, clear, punch, seal, sunlight, and stamp events;
|
||||
11. final draw-leaf sequence.
|
||||
|
||||
The depth transcript distinguishes current GPU submission from retail-only
|
||||
CPU clip facts. It never fabricates `clippedVerts` or `fanDrawn` while the
|
||||
current renderer cannot observe them.
|
||||
|
||||
The geometry manifest follows source polygon and surface-array order and
|
||||
records raw `sides_type`, stippling/UV presence, surface indices/DIDs/types,
|
||||
ordered constructed candidates, exact winding/sign lane, built-cell admission,
|
||||
retail mask, subset ordinal, and deterministic emitted-data hashes.
|
||||
|
||||
The membership manifest records stable object incarnation/kind, Setup and part
|
||||
identity, root/part transforms as raw bits, drawing sphere/part bounds,
|
||||
resident cell, every crossed shadow cell in insertion order, inheritance
|
||||
source, typed render/physics membership, and any finite fallback reason.
|
||||
|
||||
Schema and serializer implementation plus legacy FW fixture conversion are
|
||||
completed in the OH1 tooling commit. Geometry generation lands with OH2 so
|
||||
A8 delegates to the same exact descriptor used by production rather than
|
||||
copying the current wrong `NoPos` rule. Installed-DAT membership generation
|
||||
lands with OH3 beside the canonical graph owner; implementing it earlier in A8
|
||||
would create the second membership owner this campaign deletes. OH1 still
|
||||
defines and tests both manifest schemas. Their exact type names are
|
||||
implementation details; the fields and equality rules above are binding.
|
||||
|
||||
## 10. Slice boundary and unresolved facts
|
||||
|
||||
- OH1 changes evidence and diagnostics only; it may not alter a draw result.
|
||||
- Existing FW0 captures cover only frame/pose/DrawInside/DrawCells/landscape/
|
||||
building entry. Full semantic equality requires a bounded richer retail
|
||||
capture before the owning later slices close; absent fields remain explicit
|
||||
`null`/uncovered and are never promoted from acdream's own output.
|
||||
- `polyClipFinish @0x006B6D00` still needs a direct port/audit before OH6 can
|
||||
claim exact clipped-vertex/fan parity.
|
||||
- Alpha branch sense is closed in `oh1-alpha-list-contract.md`. Runtime
|
||||
registry/user values and nullable generated detail surfaces remain inputs
|
||||
that OH7 must represent exactly.
|
||||
- The pixel-level use of the installed outside-view list by every landscape
|
||||
leaf remains an OH5/OH8 audit item; the top-level ownership and order are
|
||||
already proven.
|
||||
- The installed Environment CellStruct corpus examined for OH2 has no
|
||||
`ST_BOTH`; synthetic coverage is mandatory despite the decomp being clear.
|
||||
- The canonical cathedral/Facility cells' `NoPos` candidates are untextured,
|
||||
so OH2 is a real global correctness fix but not a standalone explanation of
|
||||
the cathedral leak.
|
||||
- No cathedral-specific suppression, surface blacklist, larger bounds, or
|
||||
screen-space workaround is permitted by this contract.
|
||||
|
||||
## 11. Review checklist
|
||||
|
||||
- [x] all direct OH1 questions have a cited, executable answer;
|
||||
- [x] both render-stamp advances are represented independently;
|
||||
- [x] zero-outside-view calls preserve the persistent portal counter;
|
||||
- [x] current GPU clip output is not misreported as retail CPU clip output;
|
||||
- [x] local-player stamp bypass remains explicit;
|
||||
- [x] whole-mesh admission is not described as portal polygon clipping;
|
||||
- [x] alpha lists are FIFO and ordered clip-before-alpha;
|
||||
- [x] membership is produced once and borrowed by render and physics;
|
||||
- [x] CellStruct NoPos/NoNeg are UV flags only;
|
||||
- [x] architecture SSOT is reconciled before OH1 closes;
|
||||
- [ ] no production draw result changes in the OH1 commits (close after tooling gate).
|
||||
Loading…
Add table
Add a link
Reference in a new issue