442 lines
33 KiB
Markdown
442 lines
33 KiB
Markdown
# 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.
|