18 KiB
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
- What does retail construct when an EnvCell static is initialized?
- Which object, part, cell, and child lists own render membership?
- Which exact bounds cross a portal during static membership construction?
- In what order does retail visit landscape views, blocks, cells, terrain, buildings, ordinary cell objects, alpha barriers, and weather?
- Where does current acdream depart from that ownership and order?
Sources and arbitration
Primary text:
docs/research/named-retail/acclient_2013_pseudo_c.txtdocs/research/named-retail/acclient.hdocs/research/named-retail/acclient.c
Prior independently verified evidence used for cross-checking:
docs/research/2026-06-11-holistic-map/wf1-statics-dynamics.mddocs/research/2026-08-30-fw-walk-pseudocode.mddocs/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_objects0x0052c350CPhysicsObj::add_obj_to_cell0x005159e0CPhysicsObj::calc_cross_cells_static0x00515160CPhysicsObj::find_bbox_cell_list0x00510fc0CPartArray::calc_cross_cells_static0x00518160CEnvCell::find_transit_cells(parts)0x0052cae0CPhysicsObj::add_shadows_to_cells0x00514ae0CPartArray::AddPartsShadow0x00517e40CPhysicsObj::remove_shadows_from_cells0x00511230LScape::draw0x00506330LScape::draw_check_blocks0x00505f80LScape::landcell_check0x005050a0RenderDeviceD3D::DrawBlock0x005a17c0RenderDeviceD3D::DrawSortCell0x0059f140RenderDeviceD3D::DrawObjCell0x005a1a40RenderDeviceD3D::DrawBuilding0x0059f2a0CShadowPart::insertion_sort0x006b5130
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, andstate.CPartArray.num_partsandparts.CObjCell.clip_planes,stab_list,seen_outside, and the inheritedCPartCell.shadow_part_list.CEnvCell.num_static_objects,static_object_ids,static_object_frames, andstatic_objects.CLandBlock.side_cell_count,lcell,in_view,draw_array, anddraw_array_size.CLandCellstride is0x108; itsin_viewis+0x104.LScape.mid_width,land_blocks,block_draw_list, viewer offsets, andsky.
Contract A: EnvCell static construction
Retail call order is fixed:
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:
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:
- Take a part's
gfxobj->physics_sphere; if null, take itsgfxobj->drawing_sphere; if both are null, that part cannot open a portal. - Transform the sphere center from the part frame into the current cell.
- Test signed portal-plane distance with radius plus
F_EPSILONand the authoredportal_side. - For a possible crossing, obtain
CPhysicsPart::GetBoundingBox(part), transform that box into the current cell, and requirePlane::intersect_box(box) != portal_side. - For an indoor neighbor, resolve it with
CCellPortal::GetOtherCell(cell_array.do_not_load_cells), transform the same part box into the neighbor, and requireneighbor->structure->box_intersects_cell(box)before orderedCELLARRAY::add_cell. - For
other_cell_id == 0xFFFFFFFF, remember an outside crossing. After all portals, callCLandCell::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:
- It sizes
shadow_objects, creates oneCShadowObjper cell, assigns the same owningCPhysicsObj, and copies each CELLARRAYcell_idin order. - 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 apart_arrayexists,CPartArray::AddPartsShadow(part_array, cell, num_shadow_objects).
CPartArray::AddPartsShadow 0x00517e40 selects the clip owner exactly:
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:
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×8block andENTIRELY_INSIDE: set every cell toENTIRELY_INSIDE; - full
8×8partial block: compute the 24 m per-cell interval grid and write a cell only when its currentIsInView()is zero; - coarse LOD block (
side_cell_count != 8): mark every represented cellPARTIALLY_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:
- For each visible cell:
UpdateObjCell; if it has more than one shadow part,CShadowPart::insertion_sort. - For each draw-array cell:
- if visible,
DrawLandCellfirst; - if
alwaysDrawObjects != 0or visible,DrawSortCellsecond; - after that cell, conditionally
FlushAlphaList(flush).
- if visible,
DrawSortCell 0x0059f140 is exactly:
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:
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:
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 ShadowShapes, 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 RenderProjectionRecords 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:
- OH3a must define one generation-scoped, prepared-data-backed graph whose cell entries retain exact ordered part identity and the sibling collision-object relationship.
- 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.
- OH3d must delete
WalkProductionWorldData's static/dynamic membership reconstruction after consumer cutover. - 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.
- 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_cellsstate; - 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;
CYptand 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_viewacross both views. - Equal-
CYptparts 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.