docs(render): define OH1 retail world contract

This commit is contained in:
Erik 2026-09-01 19:33:15 +02:00
parent 5cd4fd2c71
commit 5d907ae9ad
7 changed files with 2040 additions and 39 deletions

View file

@ -1089,49 +1089,63 @@ devtools, and screenshots; then closes the GPU-flight transaction in
--- ---
## Render Pipeline (SSOT — current accepted state) ## Render Pipeline (SSOT — current implementation state; OVERHAUL active)
> The modern path (Phase N.5, mandatory) is > The modern path (Phase N.5, mandatory) is
> `WbDrawDispatcher` (entities) + `EnvCellRenderer` (indoor cell shells) + > `WbDrawDispatcher` (entities) + `EnvCellRenderer` (indoor cell shells) +
> `TerrainModernRenderer` (terrain), fed by the unified PView stack. This is the > `TerrainModernRenderer` (terrain), fed by the unified PView stack. This is the
> authoritative current draw model; the 2026-05-31 reset handoff is historical. > authoritative current draw model; the 2026-05-31 reset handoff is historical.
**One visibility owner.** `RetailFrameWalk`, driven once per frame by **Walk owner and current reconstruction boundary.** `RetailFrameWalk`, driven
`WalkFrameDriver.Collect`, is the production authority for rooting, portal once per frame by `WalkFrameDriver.Collect`, is the production authority for
recursion, visible cells/buildings, per-view admission, and draw order. Its root rooting and portal recursion. Its root is the collided camera/viewer cell or
is the collided camera/viewer cell or retail's synthetic outdoor cell; the retail's synthetic outdoor cell; the player's current cell separately owns
player's current cell separately owns sunlight and indoor lighting. A null root sunlight and indoor lighting. A null root exists only for login/debug/streaming-
exists only for login/debug/streaming-gap fallback frames. The classes named gap fallback frames. `RetailPViewRenderer` and `RetailPViewPassExecutor` are
`RetailPViewRenderer` and `RetailPViewPassExecutor` remain composition/pass composition/pass facades, and `PortalVisibilityBuilder` has no production
facades around that one walk; they do not build a second visibility product. caller. This does **not** yet mean the complete world graph has one owner:
`PortalVisibilityBuilder` is retained only by tests and research diagnostics and `WalkProductionWorldData` still reconstructs frame buckets from scene records,
has no production caller. `CellVisibility` does not decide frame visibility. AABBs, and aggregate shadow-owner cells. Campaign OVERHAUL OH3 replaces that
boundary with one canonical published cell graph and typed render/physics
membership transaction.
**Current draw discipline.** The walk records one ordered event stream for **Current draw discipline and known non-retail order.** The walk records an
landscape cells, building portals, EnvCell shells, statics, dynamics, particles, ordered event stream for landscape cells, building portals, EnvCell shells,
depth boundaries, and alpha barriers. `WalkFrameDriver` replays those events in statics, dynamics, particles, depth boundaries, and alpha barriers, then
retail order through the Vulkan leaf renderers. Interior roots preserve retail's `WalkFrameDriver` replays it through the Vulkan leaves. Private portal/
conditional depth clear and true-depth exit seals; the synthetic outdoor root paperdoll viewports and retained UI execute after the world stream, and
retains world depth and stamps far-Z entry punches. World translucents and scene projectiles remain ordinary live-entity draws. Three current details are
particles share `RetailAlphaQueue` and drain at the walk's exact barriers. explicit Campaign OVERHAUL divergences, not accepted retail contracts:
Private portal/paperdoll viewports and retained UI execute only after the world
stream finishes. Projectiles are ordinary live-entity draws, never a separate
global pass.
**Portal-slice clipping is part of admission, not a later approximation.** Each - terrain is staged before all per-cell building/object turns instead of
walk visit retains the exact authored pixel-space portal polygon. The frame retail's `DrawLandCell` then `DrawSortCell` interleave for each land cell;
adapter converts it once to a Vulkan GPU clip slot, and the cell shell, statics, - depth clear/seal events are emitted for zero-outside-view roots and do not
and dynamics admitted by that visit draw through that same slot. A content consume retail's persistent previous-call `uint16 portalsDrawnCount` gate;
drawing sphere may reject a slice before submission, but an accepted slice - `RetailAlphaQueue` distance-sorts one shared queue, whereas retail owns two
never emits the whole mesh unclipped. The same cell can therefore draw more FIFO mesh lists and drains clip before alpha at exact barriers.
than once through distinct apertures, exactly as retail's `viewconeCheck` plus
`portal_view` loop requires. Particles remain unclipped at submission because The exact target and evidence are recorded in
retail contains them positionally through owner-cell ordering, opaque depth, `docs/research/2026-09-01-overhaul/oh1-retail-world-contract.md`; OH5, OH6, and
and alpha barriers. The complete visibility answer is the union of the walk's OH7 own these cutovers.
visited EnvCells and visited landscape land cells. EnvCell batch preparation
consumes only the former; particle, light, and shadow visibility consume the **Retail view admission is not general mesh clipping.** A walk visit retains
union. Omitting the landscape half disables every outdoor emitter even though the authored portal polygon and its installed view planes. Retail uses those
its owner still reaches the draw stream. planes for `viewconeCheck`-style sphere admission. For an ordinary built part,
the first accepted view submits the complete eligible mesh and stamps the
part; later views are suppressed by that stamp. A built EnvCell shell is also
drawn complete once per render stamp. The local player deliberately bypasses
the ordinary inner part-stamp gate and may submit whole for more than one
admitted cell/view. Actual polygon clipping belongs to portal clip/punch/seal
geometry (`PView::GetClip` / `DrawPortalPolyInternal`), not normal built shells
or objects. The current `PortalDepthMaskRenderer` GPU `gl_ClipDistance` route
therefore remains an OH6 portal-fan approximation. The current ordinary-part
classifier is a materially aligned candidate and OH4 must protect it while
removing only contradictory view/admission residue.
The walk's visited EnvCells and landscape land cells still form different
typed inputs. Outdoor particle, light, and shadow consumers must borrow the
landscape half rather than infer visibility from aggregate owner ids; OH8 owns
that final consumer cutover.
WorldBuilder contributes DAT decoding, mesh preparation, residency, and Vulkan WorldBuilder contributes DAT decoding, mesh preparation, residency, and Vulkan
batch mechanics only; it makes no visibility or ordering decision. Forced merge batch mechanics only; it makes no visibility or ordering decision. Forced merge
@ -1141,9 +1155,9 @@ inside a leaf may still use order-preserving MDI merging.
Retail anchors are `SmartBox::RenderNormalMode @ 0x00453AA0`, Retail anchors are `SmartBox::RenderNormalMode @ 0x00453AA0`,
`PView::DrawInside @ 0x005A5860`, `PView::DrawCells @ 0x005A4840`, `PView::DrawInside @ 0x005A5860`, `PView::DrawCells @ 0x005A4840`,
`LScape::draw @ 0x00506330`, `D3DPolyRender::FlushAlphaList @ 0x0059D2E0`, `LScape::draw @ 0x00506330`, `D3DPolyRender::FlushAlphaList @ 0x0059D2E0`,
and `SceneTool::EndFrame @ 0x0043FB30`. The deliberate modern/PView and `SceneTool::EndFrame @ 0x0043FB30`. The active OVERHAUL plan is
adaptations are audited in the retail divergence register; Slice 7 changes `docs/plans/2026-09-01-campaign-overhaul-world-solidity.md`; it replaces prior
their ownership only, not their behavior. claims of exactness only at each reviewed slice gate.
### Streaming publication ownership ### Streaming publication ownership

View file

@ -1540,7 +1540,7 @@ status only in chat.
| Slice | Status | Evidence packet | Implementation | Reviews | Automated gate | Owner gate | Notes | | Slice | Status | Evidence packet | Implementation | Reviews | Automated gate | Owner gate | Notes |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| OH0 | CLOSED | `docs/research/2026-09-01-overhaul/oh0-baseline.md` | `b3b7d922` recovery checkpoint | Read-only dirty-tree classification; every path accounted for | Release build green; focused gates 319 passed / 1 skipped; OH docs pass `git diff --check` | n/a | No production behavior changed by OH0; no unrelated semantic files found; candidate state is explicitly unaccepted | | OH0 | CLOSED | `docs/research/2026-09-01-overhaul/oh0-baseline.md` | `b3b7d922` recovery checkpoint | Read-only dirty-tree classification; every path accounted for | Release build green; focused gates 319 passed / 1 skipped; OH docs pass `git diff --check` | n/a | No production behavior changed by OH0; no unrelated semantic files found; candidate state is explicitly unaccepted |
| OH1 | PLANNED | — | — | — | — | conditional | Oracle and exact contracts only | | OH1 | IN PROGRESS | `oh1-retail-world-contract.md` plus construction, built-view, alpha-list, and depth packets | strict Core schema/codec and output-only App recorder in review | retail synthesis re-review passed after two blockers were corrected; final alpha/tooling review active | Core codec focused gate green before schema-expansion review | not required so far | No production draw decision may change; legacy FW semantic importer and recorder equivalence gate remain |
| OH2 | PLANNED | — | — | — | — | G1a | Retire AP-234 | | OH2 | PLANNED | — | — | — | — | G1a | Retire AP-234 |
| OH3a | PLANNED | — | — | — | — | folded | Read-only canonical graph | | OH3a | PLANNED | — | — | — | — | folded | Read-only canonical graph |
| OH3b | PLANNED | — | — | — | — | folded | Static membership | | OH3b | PLANNED | — | — | — | — | folded | Static membership |

View 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 13 and true for
levels 45. 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.

View file

@ -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.

View file

@ -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.

View 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 432709432889
- `DrawPortalPolyInternal` around lines 424490424698
- `PView::ClipPortals`, both `ConstructView` overloads, `DrawInside`, and `DrawPortal`
- `docs/research/named-retail/acclient.h`
- `portal_view_type` around lines 3234632355
- `PView` around lines 4593445944
- `RenderDevice` around lines 3898839016
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 | `0x5A48490x5A4852` | `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 | `0x5A485A0x5A486B` | 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 | `0x5A48850x5A4886` | 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 | `0x5A488C0x5A48A7` | 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 | `0x5A48A90x5A48BD` | 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 | `0x5A48C00x5A49EB` | 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 | `0x5A49F10x5A49FB` | always | outside stage complete or skipped | sunlight off; restore lighting | indoor lighting active |
| 9 | `0x5A4A000x5A4ADE` | 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 | `0x5A4ADE0x5A4B1A` | cells exist | shells complete | reverse cells; install top portal view; `DrawObjCellForDummies` | statics/dynamics drawn through cell views |
| 11 | `0x5A4B1C0x5A4B5D` | 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 (`0x59BCD60x59BD20`), 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 |
| `0x59BCD60x59BD28`, then `0x59BD400x59BD66` | 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 |
| `0x59BD700x59BD74` | test boolean; nonzero jumps past `INC word ptr [0x8719B4]` | false/true-depth seals increment modulo 65,536; true/far punches do not |
| `0x59BDBC0x59BDBF` | compare clipped count with 3; `JL` return | fewer than 3 clipped vertices submit no fan, after possible increment |
| `0x5A56660x5A566C` | compare `other_cell_id` with `-1`; `JNZ` neighbor path | `0xFFFFFFFF` is outside path |
| `0x5A56710x5A5673` | test `draw_landscape`; zero skips | only the indoor PView accumulates outside view |
| `0x5A5A6E0x5A5A7B` | mode 2 skips punch; mode 1 falls through to `DrawPortalPolyInternal(true)` | building pass 1 punches; pass 2 constructs destination |
| `0x5A5B460x5A5B53` | success then compare mode 1; mode 1 skips `DrawCells` | far-punch pass has no look-in draw; later pass does |
| `0x5A5B740x5A5B7C` | on failure, only mode 3 draws false-mode portal poly | failure fallback is true-depth only for mode 3 |
| `0x59FEC00x59FEC8` | 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 `0x0059FEC00x0059FEC8`, increments once after the normal swap-chain present, texture-stage unbind, dynamic-buffer reset, and profiler-frame reset;
2. `DrawCells @0x005A48850x005A4886` 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:4655`; `WalkPView.cs:3950` | faithful basic ownership |
| outside-view reset and `0xFFFFFFFF` producer gated by `DrawLandscape` | `WalkPView.cs:5975`, `175250` | faithful basic producer |
| indoor flood calls landscape only when outside views exist | `RetailFrameWalk.cs:118142` | faithful at walk level |
| landscape then root-flood turn | `RetailFrameWalk.cs:118142` | 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:12991339` emits clear/seals unconditionally for every interior flood | **wrong** |
| first eligible call with counter zero does not clear | `WalkFrameDriverTests.cs:260293` expects `CLEAR` on the first outside-view fixture | **wrong test contract** |
| `outside_view==0` performs no clear/seal/**additional DrawCells stamp** | `WalkFrameDriverTests.cs:432440` 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:784818` calls `BeginWalkPartFrame` and clears shell dedupe; `Wb/WbDrawDispatcher.WalkClassify.cs:8999` 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:13041319` 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:87120` | faithful clear operation |
| shell pass then object pass, both reverse order | `WalkFrameDriver.cs:14191453` | 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:106156` 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:58104` uses `ColorWrite=false` | depth/color **outcome-equivalent**, but render state diverges from retail |
| per-landcell `DrawLandCell` then `DrawSortCell` | `WalkFrameDriver.cs:105114`, `13451379` 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:2850`, `94116` 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:656685` | geometry loop is broadly shaped correctly, but called under wrong gate and has no counter feedback |
The current comments in `WalkFrameDriver.cs:138165` accurately label the clear gate as an acknowledged approximation, but the later implementation comment at `13211325` 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.

View 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).