docs(physics): pin Slice I collision oracle
This commit is contained in:
parent
f1a8d36682
commit
624e1119ca
3 changed files with 827 additions and 0 deletions
345
docs/research/2026-07-25-slice-i-collision-layout-oracle.md
Normal file
345
docs/research/2026-07-25-slice-i-collision-layout-oracle.md
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue