acdream/docs/research/2026-07-25-slice-i-collision-layout-oracle.md

345 lines
15 KiB
Markdown

# Slice I collision layout and scratch oracle
**Status:** I0 fixed evidence
**Scope:** storage layout, traversal order, scratch lifetime, production
retention, and allocation baseline for
[`2026-07-25-modern-runtime-slice-i.md`](../plans/2026-07-25-modern-runtime-slice-i.md).
This note does not authorize a collision-math change.
## 1. Sources and authority
Named retail is the behavior oracle:
| Mechanism | Retail symbol | Address | Named pseudo-C |
|---|---|---:|---:|
| scratch acquisition | `CTransition::makeTransition` | `0x0050B150` | 272873 |
| scratch release | `CTransition::cleanupTransition` | `0x00509DC0` | 271938 |
| transition reset | `CTransition::init` | `0x00509DD0` | 271946 |
| collision reset | `COLLISIONINFO::init` | `0x00509D60` | 271910 |
| path reset | `SPHEREPATH::init` | `0x0050C330` | 273903 |
| path initialization | `SPHEREPATH::init_path` | `0x0050CE20` | 274359 |
| movement loop | `CTransition::find_transitional_position` | `0x0050BDF0` | 273613 |
| object entry point | `CPhysicsObj::transition` | `0x00512DC0` | 280904 |
| six-path BSP dispatch | `BSPTREE::find_collisions` | `0x0053A440` | 323725 |
| cell point predicate | `BSPNODE::point_inside_cell_bsp` | `0x0053C1F0` | 325508 |
| cell sphere predicate | `BSPNODE::sphere_intersects_cell_bsp` | `0x0053C260` | 325546 |
| walkable internal node | `BSPNODE::find_walkable` | `0x0053CC80` | 326211 |
| walkable leaf | `BSPLEAF::find_walkable` | `0x0053D6F0` | 326793 |
| polygon overlap | `BSPNODE::sphere_intersects_poly` | `0x0053CA30` | 326091 |
| solid overlap | `BSPNODE::sphere_intersects_solid` | `0x0053CAF0` | 326130 |
| solid/polygon overlap | `BSPNODE::sphere_intersects_solid_poly` | `0x0053CD50` | 326246 |
Interpretation aids:
- ACE `master` commit
`65f092dd02505c04532b701c48d11af117cbc815`:
`Physics/Transition.cs`, `SpherePath.cs`, `ObjectInfo.cs`,
`Collision/CollisionInfo.cs`, and `Physics/BSP/{BSPTree,BSPNode,BSPLeaf}.cs`.
- Chorizite DatReaderWriter `master` commit
`c5359870963fecb55c9b90e8143a92fc7fe88e11`; acdream consumes package
`2.1.7`. Its `PhysicsBSPNode` and `CellBSPNode` unpackers establish the
source child-presence and stream order.
- Existing acdream translations:
[`transition_pseudocode.md`](transition_pseudocode.md),
[`2026-06-24-obstruction-ethereal-pseudocode.md`](2026-06-24-obstruction-ethereal-pseudocode.md),
[`2026-07-05-ccylsphere-collision-family-pseudocode.md`](2026-07-05-ccylsphere-collision-family-pseudocode.md),
[`2026-07-07-csphere-collision-family-pseudocode.md`](2026-07-07-csphere-collision-family-pseudocode.md),
and the physics digest.
Retail wins if an interpretation aid disagrees. Slice I keeps the current graph
implementation executable until every flat query is compared against it. A
pre-existing graph-vs-retail mismatch, if found, is adjudicated separately; it
must not be hidden inside the layout cutover.
## 2. Retail scratch lifetime
`CTransition::makeTransition` is not a heap constructor per movement call:
```text
on first use:
construct static CTransition transit[10]
level = transition_level
if level >= 10:
return null
transition = &transit[level]
CTransition::init(transition)
transition_level++
return transition
```
`CTransition::cleanupTransition` only decrements `transition_level`. This is a
ten-deep, stack-disciplined scratch arena. Nested transitions receive distinct
records and release in LIFO order. The object graph and embedded arrays retain
their storage identity; `init` resets the logical state before each lease.
Retail reset pseudocode:
```text
CTransition::init:
object_info.object = null
object_info.state = 0
object_info.targetID = 0
SPHEREPATH::init(sphere_path)
collision_info.last_known_contact_plane_valid = false
collision_info.contact_plane_valid = false
collision_info.sliding_normal_valid = false
collision_info.collision_normal_valid = false
collision_info.num_collide_object = 0
clear last_collided_object and adjacent collision result fields
collision_info.contact_plane_cell_id = 0
SPHEREPATH::init:
num_sphere = 0
begin_cell/begin_pos/curr_cell/check_cell = null
insert_type = TRANSITION
step_down/step_up/collide = false
hits_interior_cell/bldg_check/obstruction_ethereal = false
backup_cell = null
walkable_allowance = 0
walkable = null
check_walkable/cell_array_valid/neg_step_up/neg_poly_hit = false
placement_allows_sliding = true
```
The acdream reset must cover additional modern representations of the same
logical state: cell IDs instead of pointers, orientations, carried block
origin, retained vertex buffers, diagnostic counters, GUID lists, target and
self IDs, physics-state flags, water bits, and value-type planes/vectors.
Retained storage may survive only if its logical contents are reset completely.
## 3. Immutable source storage
### Physics BSP
The source graph fields consumed by collision are:
```text
PhysicsBSPNode:
Type
SplittingPlane
PosNode, NegNode
LeafIndex
Solid
BoundingSphere (origin + radius)
Polygons[] (ordered ushort ids)
```
DatReaderWriter decodes child presence from `BSPNodeType`. When both children
exist, positive is read before negative. A flat representation therefore uses
explicit signed child indices (`-1` = absent) and preserves source preorder:
node, positive subtree, negative subtree.
### Cell-containment BSP
```text
CellBSPNode:
Type
SplittingPlane
PosNode, NegNode
LeafIndex
```
Containment trees do not carry physics leaf polygons or bounding spheres. They
must remain a separate schema from the physics BSP rather than wasting fields
or accidentally applying physics-node traversal rules.
### Resolved polygons
Runtime physics reads:
```text
ResolvedPolygon:
Id
Plane (normal + D)
SidesType
NumPoints
Vertices[] in source order
```
The flat asset stores one polygon table and one contiguous vertex stream.
Every polygon has an explicit `(vertexStart, vertexCount)` and every leaf has an
explicit `(polygonIndexStart, polygonIndexCount)`. Polygon IDs are resolved to
indices during preparation. Missing IDs are corrupt prepared content, never a
runtime dictionary fallback.
Every float is copied through its `SingleToInt32Bits` representation. Planes
are copied, not reconstructed from vertices.
### Setup and cell payload
`SetupPhysics` consumes ordered `CylSpheres`, ordered `Spheres`, `Height`,
`Radius`, `StepUpHeight`, and `StepDownHeight`.
`CellPhysics` currently combines immutable geometry with publication state:
physics BSP, resolved physics polygons, cell-containment BSP, world and inverse
world transforms, ordered portals, resolved portal polygons, visible-cell IDs,
and `SeenOutside`. The transforms and topology are per EnvCell placement.
Immutable CellStruct collision geometry may alias only when its bytes and
source identity agree; placement/topology may not be aliased merely because
geometry matches.
## 4. Load-bearing traversal order
### Walkable search
```text
find_walkable(node, path, mutableSphere, hitPoly, movement, up, changed):
if node.boundingSphere does not intersect mutableSphere:
return
distance = signedDistance(node.splittingPlane, mutableSphere.center)
threshold = mutableSphere.radius - 0.0002
if distance >= threshold:
visit positive only
else if distance <= -threshold:
visit negative only
else:
visit positive
visit negative
find_walkable(leaf, ...):
if no polygons or leaf bounds miss:
return
for polygon in leaf.polygons IN STORED ORDER:
if polygon.walkable_hits_sphere(...)
and polygon.adjust_sphere_to_plane(...):
changed = true
hitPoly = polygon
```
The second child sees the sphere position mutated by the first child. A later
successful polygon replaces `hitPoly`. Consequently child order, leaf order,
and reference-style sphere mutation are behavior, not implementation detail.
### Six-path collision dispatch
`BSPTREE::find_collisions` evaluates in this order:
1. placement or obstruction-ethereal: solid test, primary sphere then secondary;
2. `check_walkable`;
3. `step_down`;
4. pending `collide`: walkable search and adjustment;
5. existing contact: primary collision and step-up, then secondary slide/negative hit;
6. airborne/default: primary collision/set-collide, then secondary hard collision.
The dispatcher observes and mutates `SpherePath`, `ObjectInfo`, and
`CollisionInfo`. The flat port must preserve every early return and the
primary-before-secondary order.
### Cell containment
Point and radius-aware containment are distinct entry points.
`sphere_intersects_cell_bsp` expands the radius by exactly `0.01`. Null-child,
on-plane, and near-plane behavior is pinned by the current conformance tests and
must be compared bit-for-bit during I4. No generic “BSP traversal helper” may
merge containment, walkable, solid, and collision traversal; their null-child
and return semantics differ.
## 5. Mutable query state and returned side effects
The query scratch is:
- `ObjectInfo`: state, step heights, scale, ethereal/step-down state, mover
physics state, target ID, self ID, gravity state, and velocity-killed result.
- `CollisionInfo`: live and last-known contact planes including cell/water
metadata, sliding and collision normals, environment hit, stationary-fall
count, adjustment offset, collision GUID list, last collision GUID, and
diagnostic write count.
- `SpherePath`: all local/global/current spheres, four positions and
orientations, current/check/backup cells, carried block origin, movement
offset, step-up/down state, mutable current/last walkable state, negative-hit
state, insertion mode, placement policy, and building/interior/ethereal flags.
- per-query `CollisionSphere` values used as reference-mutated valid positions.
Returned behavior includes more than `ResolveResult`: body contact and
last-contact state, walkable polygon, stationary-fall/transient flags, sliding
normal, killed velocity, selected hit polygon, collision GUIDs, and swept cell
membership. Differential tests must compare these side effects as well as the
boolean/enum return and final position.
## 6. Production graph-call inventory
| Entry point | Production consumers |
|---|---|
| `PointInsideCellBsp` | `World/Cells/EnvCell`; `CellTransit` point/portal membership; `Transition.CheckOtherCells` diagnostics and admission |
| `SphereIntersectsCellBsp` | `CellTransit` candidate, current-cell, and transit membership; `Transition.CheckOtherCells` |
| `FindCrossedEdge` | precipice slide and nearest-inside-edge handling in `SpherePath`/`Transition` |
| `FindWalkableSphere` | indoor walkable-plane probe in `Transition.TryFindIndoorWalkablePlane` |
| `FindCollisions` | EnvCell physics BSP, static/multipart GfxObj BSP, and building-shell BSP in `Transition` |
The legacy dictionary/`VertexArray` overloads and static
`SphereIntersectsPoly*` entry points currently serve tests and diagnostics, not
the production movement call graph. I6 removes them from production ownership
only after their required tooling users are explicit.
## 7. Parsed graph retention inventory
The current worker `LandblockBuildFactory` creates a `PhysicsDatBundle` holding
raw `LandBlockInfo`, `EnvCell`, `Environment`, `Setup`, and `GfxObj` DBObjs in
per-build dictionaries. It also opportunistically populates
`PhysicsDataCache` while gathering entity and scenery bounds.
On the update thread, `LandblockPhysicsPublisher`:
1. resolves EnvCell → Environment → CellStruct from that raw bundle;
2. calls `CacheCellStruct`, retaining physics BSP, containment BSP, raw polygon
dictionaries/vertex arrays, resolved polygons, and topology;
3. calls `CacheGfxObj`, retaining GfxObj physics BSP, raw polygon/vertex graphs,
resolved polygons, and bounds;
4. reads Setup graphs for collision shapes, buildings, and multipart statics.
`LandblockStaticPresentationPublisher` also reads raw Setup light definitions.
`LandblockStreamResultCost` currently counts bundle dictionary entries but not
the complete transitive DBObj graph. I3 must replace collision publication with
prepared immutable payloads without removing the separately required render or
lighting data.
## 8. Fixed fixture ledger
| Required shape | Existing fixed evidence |
|---|---|
| outdoor/no BSP | `PhysicsEngineTests`, `CellMarchLandblockPreservationTests`, projectile terrain fixtures |
| Facility Hub indoor | `Issue137CorridorSeamReplayTests` (`0x8A02016E↔0x8A02017A`), `Issue180CorridorSweepHysteresisReplayTests` (`0x8A020164`) |
| Holtburg cottage indoor | `ThresholdPortalCrossingReplayTests`, `CameraCornerSealReplayTests`, `Fixtures/flap-doorway/*` |
| staircase/ramp | `BSPStepUpFixtures`, `Issue185OutdoorStairsSeamReplayTests` |
| cellar lip | `CellarLipWedgeTests`, `CellarUpTrajectoryReplayTests`, `Fixtures/cellar-lip/*`, `Fixtures/issue98/*` |
| multipart door/building shell | `DoorCollisionApparatusTests`, `DoorBugTrajectoryReplayTests`, GfxObj fixture `0x01000A2B` |
| projectile/thin wall | `ProjectilePhysicsStepperTests.RegisterThinBspWall` |
| asymmetric/null children | `SphereIntersectsCellBspTests`, `PhysicsEngineAdjustPositionTests`, real DAT containment fixtures |
| multi-polygon/equal candidate order | `BSPQueryTests`, `IndoorWalkablePlaneTests`, `BSPStepUpFixtures`; I2 adds explicit flat-order structural cases before flattening gates |
I0 graph-path fixture command:
```powershell
dotnet test tests\AcDream.Core.Tests\AcDream.Core.Tests.csproj -c Release --no-build `
--filter "FullyQualifiedName~BSPQueryTests|FullyQualifiedName~SphereIntersectsCellBspTests|FullyQualifiedName~CellarLipWedgeTests|FullyQualifiedName~CellarUpTrajectoryReplayTests|FullyQualifiedName~CameraCornerSealReplayTests|FullyQualifiedName~ProjectilePhysicsStepperTests|FullyQualifiedName~DoorCollisionApparatusTests|FullyQualifiedName~Issue137CorridorSeamReplayTests|FullyQualifiedName~ThresholdPortalCrossingReplayTests|FullyQualifiedName~Issue185OutdoorStairsSeamReplayTests|FullyQualifiedName~CylSphereFamilyTests|FullyQualifiedName~SphereCollisionFamilyTests|FullyQualifiedName~TransitionAllocationBaselineTests"
```
Result on 2026-07-25: **111 passed, 0 skipped, 0 failed** against the current
object-graph route and installed retail DATs.
## 9. Allocation baseline
`TransitionAllocationBaselineTests` warms 256 calls, measures 4,096 Release
resolves on one thread, and exercises a fixed BSP wall through the four
production mover call shapes:
| Mover | Current graph path |
|---|---:|
| player, two spheres | 4,448 B/resolve |
| remote, two spheres | 4,448 B/resolve |
| projectile, one 5 cm sphere | 6,848 B/resolve |
| camera/viewer, one 30 cm sphere | 2,512 B/resolve |
The projectile crosses the same distance using a much smaller sphere, causing
more retail substeps and therefore more per-step query-scratch allocation.
This is why allocation must be measured by mover family rather than quoting one
average.
I1's gate is zero steady allocation attributable to transition construction,
walkable buffer cloning, and `CollisionSphere` query scratch after warmup.
It does not claim that every optional diagnostic/capture path is allocation
free.