15 KiB
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.
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
mastercommit65f092dd02505c04532b701c48d11af117cbc815:Physics/Transition.cs,SpherePath.cs,ObjectInfo.cs,Collision/CollisionInfo.cs, andPhysics/BSP/{BSPTree,BSPNode,BSPLeaf}.cs. - Chorizite DatReaderWriter
mastercommitc5359870963fecb55c9b90e8143a92fc7fe88e11; acdream consumes package2.1.7. ItsPhysicsBSPNodeandCellBSPNodeunpackers establish the source child-presence and stream order. - Existing acdream translations:
transition_pseudocode.md,2026-06-24-obstruction-ethereal-pseudocode.md,2026-07-05-ccylsphere-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:
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:
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:
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
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:
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
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:
- placement or obstruction-ethereal: solid test, primary sphere then secondary;
check_walkable;step_down;- pending
collide: walkable search and adjustment; - existing contact: primary collision and step-up, then secondary slide/negative hit;
- 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
CollisionSpherevalues 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:
- resolves EnvCell → Environment → CellStruct from that raw bundle;
- calls
CacheCellStruct, retaining physics BSP, containment BSP, raw polygon dictionaries/vertex arrays, resolved polygons, and topology; - calls
CacheGfxObj, retaining GfxObj physics BSP, raw polygon/vertex graphs, resolved polygons, and bounds; - 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:
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.