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