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

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:

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

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:

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.