Transition.FindObjCollisionsInCell discarded a shadow candidate when |currPos - obj.Position| > sphereRadius + obj.Radius + movement.Length() + 2f obj.Position is the part ORIGIN; obj.Radius is the physics-BSP ROOT BOUNDING SPHERE's radius, measured about a centre AP-156 established is frequently metres from that origin (376 of 973 installed physics-BSP parts sit further from their part origin than half their own radius, worst 20.762 m). Geometry deep inside the real bounding sphere was therefore thrown away before BSPQuery ever ran: solid near the origin, permeable in a bounded shell beyond it. For the Neftet rock 0xC8766009 / gfx=0x01004751 the two points are 23.556 m apart, which is #337 — wedged on the plateau, jumps sinking into the mesh, corpses falling through. A live capture recorded 7,225 rejections on that one owner, every single one with wouldAcceptAtCenter=True. Deleted rather than re-centred. Retail has no distance pre-filter, disassembled from the PDB-paired v11.4186 binary (CodeView GUID 9e847e2f-777c-4bd9-886c-22256bb87f32) rather than read from Binary Ninja: CObjCell::find_obj_collisions @0x0052b750 walks shadow_object_list and calls CPhysicsObj::FindObjCollisions (0x0052b78b) UNCONDITIONALLY; its only early-out is insert_type == INITIAL_PLACEMENT_INSERT (0x0052b759). CPhysicsObj::FindObjCollisions @0x0050f050 contains no float compare at all. CPartArray::FindObjCollisions @0x00518180 is a bare do/while over parts, and CPhysicsPart::find_obj_collisions @0x0050d8d0 is two null checks plus a call. Retail's only spatial rejection is the BSP node bounding-sphere test inside the walk — correctly centred, which is exactly what the deleted filter was not. Re-centring it (carry BoundsCenter on ShadowEntry) would have preserved an invention retail does not have, including a +2f slack and a movement.Length() term with no retail counterpart, and left a second reach budget to be tuned forever. Retail's own cross-cell slack constant is F_EPSILON = 0.0002 m, not 2 m. The method's comment claimed the filter was "the analog of the part sorting-sphere early-outs inside retail's CPhysicsObj::FindObjCollisions — response-neutral, pure perf". Both halves were false and cost #333 and #337; it is replaced by the disassembly above. Gate: Issue333BroadphaseReachFilterTests drives the production path end-to-end (ResolveWithTransition -> FindObjCollisionsInCell -> CollisionTraversal) on a DAT-free fixture so it runs everywhere, as a discriminating pair. Sabotage-verified: restore the pre-check and OffCentreBspFloorStopsAFallingMover reaches z=37.800 — exactly the unobstructed fall, blockedAtLeastOnce=False — while CentredBspFloorStopsAFallingMover keeps passing. Without the control a fixture unable to fall would pass the first test for the wrong reason. Issue337's skipped TheBroadphaseAdmitsTheSurfaceTheMoverIsStandingOn asserted the now-deleted predicate and could never have gone green; it is rewritten as installed-DAT evidence pinning BOTH halves of the diagnosis and is no longer skipped. Perf measured, not assumed (Release, synthetic all-BSP cell, per ResolveWithTransition): at 38 candidates — the live maximum — 10.61 us -> 16.68 us (1.57x); at a deliberately unreachable 200, 17.34 -> 39.48 us (2.28x); ~0.16 us per additional candidate tested. Over 19,701 live [reach-q] samples the in-cell count is p50 = 9, p99 = 32, max 38. The ACDREAM_PROBE_REACH rejectedReach column is kept and is now structurally 0, so a post-fix capture stays comparable with the pre-fix one; dropping it would make the two incomparable. AP-158 retired (110 active AP rows). #333 and #337 closed pending the user's live acceptance at Neftet. Solution suite 11,231 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5a1eeace73
commit
ea83b043df
7 changed files with 478 additions and 85 deletions
|
|
@ -1158,7 +1158,13 @@ public static class PhysicsDiagnostics
|
|||
// confirm any one of them:
|
||||
//
|
||||
// (a) the object IS a candidate in the cell but the broadphase reach
|
||||
// filter rejects it before its BSP is consulted (AP-158 / #333);
|
||||
// filter rejects it before its BSP is consulted (AP-158 / #333).
|
||||
// CONFIRMED and CLOSED 2026-08-06: this was the cause of #337, and
|
||||
// the filter is now DELETED — retail has none. `rejectedReach` is
|
||||
// retained as a structurally-zero column so a post-fix capture is
|
||||
// directly comparable with the pre-fix one that recorded 7,225
|
||||
// rejections on one owner, every single one with
|
||||
// wouldAcceptAtCenter=True;
|
||||
// (b) the object is NOT in the cell's candidate set at all — a
|
||||
// membership / registration failure (AP-156's territory, which did
|
||||
// not fix this, or the static publication path never registered it);
|
||||
|
|
@ -1199,7 +1205,9 @@ public static class PhysicsDiagnostics
|
|||
/// <item><c>[reach-obj]</c> — one per candidate, carrying its identity
|
||||
/// (mover guid, target entity id, GfxObj id, cell) and its
|
||||
/// <c>disposition</c>: <c>exempt-self</c>, <c>exempt-missile</c>,
|
||||
/// <c>rejected-reach</c>, <c>exempt-rule</c>,
|
||||
/// <c>exempt-rule</c>
|
||||
/// (<c>rejected-reach</c> is retired — #333 deleted the filter that
|
||||
/// produced it),
|
||||
/// <c>exempt-ethereal-stepdown</c>, <c>no-shape</c>,
|
||||
/// <c>bsp-only-skip</c>, or <c>tested:<result></c>. For BSP
|
||||
/// candidates it also carries the origin-measured distance the filter
|
||||
|
|
@ -1250,8 +1258,10 @@ public static class PhysicsDiagnostics
|
|||
/// <param name="distCenter">What it should have measured: the distance to
|
||||
/// the target's physics-BSP root sphere CENTRE. Negative when not
|
||||
/// applicable.</param>
|
||||
/// <param name="budget">The filter's admission threshold, 2 m slack
|
||||
/// included.</param>
|
||||
/// <param name="budget">What the deleted filter's admission threshold WOULD
|
||||
/// have been, 2 m slack included. Since #333 no live predicate reads it; it
|
||||
/// is kept so the acceptance capture shows which candidates the old filter
|
||||
/// would have thrown away.</param>
|
||||
/// <param name="centerBudget">The same threshold WITHOUT the slack — the
|
||||
/// honest conservative bound once the real centre is used.</param>
|
||||
public static void LogReachCandidate(
|
||||
|
|
@ -1318,10 +1328,13 @@ public static class PhysicsDiagnostics
|
|||
/// </summary>
|
||||
/// <param name="inCell">Shadow entries the cell yielded, before any
|
||||
/// exemption. Zero here at a spot with visible geometry is outcome (b).</param>
|
||||
/// <param name="reached">Candidates that survived the exemptions and were
|
||||
/// measured by the reach filter.</param>
|
||||
/// <param name="reached">Candidates that survived the exemptions and went
|
||||
/// on to a shape dispatch. Before #333 this was "and were measured by the
|
||||
/// reach filter"; the filter is gone, so the two are now the same set.</param>
|
||||
/// <param name="rejectedReach">Of those, how many the reach filter
|
||||
/// rejected — outcome (a).</param>
|
||||
/// rejected — outcome (a). <b>Structurally 0 since #333 deleted the
|
||||
/// filter</b>, and retained precisely so that a post-fix capture reading 0
|
||||
/// is comparable against the pre-fix capture that read 7,225.</param>
|
||||
/// <param name="noShape">Candidates that passed the filter but resolved to
|
||||
/// no usable shape — outcome (c).</param>
|
||||
/// <param name="tested">Candidates that actually reached a shape test.</param>
|
||||
|
|
|
|||
|
|
@ -3719,9 +3719,41 @@ public sealed class Transition
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The per-object distance pre-check below is the analog of the part
|
||||
/// sorting-sphere early-outs inside retail's
|
||||
/// <c>CPhysicsObj::FindObjCollisions</c> — response-neutral, pure perf.
|
||||
/// <b>There is no per-object distance pre-check here, because retail has
|
||||
/// none.</b> An earlier comment on this method claimed the filter that used
|
||||
/// to sit in the candidate loop was "the analog of the part sorting-sphere
|
||||
/// early-outs inside retail's <c>CPhysicsObj::FindObjCollisions</c> —
|
||||
/// response-neutral, pure perf". Both halves were false, and it cost
|
||||
/// issues #333 and #337. Disassembled from the PDB-paired v11.4186 binary
|
||||
/// (CodeView GUID <c>9e847e2f-777c-4bd9-886c-22256bb87f32</c>):
|
||||
/// <list type="bullet">
|
||||
/// <item><c>CObjCell::find_obj_collisions</c> @0x0052b750 walks
|
||||
/// <c>shadow_object_list</c> and calls <c>CPhysicsObj::FindObjCollisions</c>
|
||||
/// (@0x0050f050, <c>0x0052b78b call</c>) UNCONDITIONALLY; its only
|
||||
/// early-out is <c>insert_type == INITIAL_PLACEMENT_INSERT</c>
|
||||
/// (<c>0x0052b759 cmp dword [ebx+0x174],2</c>).</item>
|
||||
/// <item><c>CPhysicsObj::FindObjCollisions</c> @0x0050f050 contains no
|
||||
/// float compare at all. Its BSP branch reaches
|
||||
/// <c>CPartArray::FindObjCollisions</c> (<c>0x0050f18d call 0x518180</c>)
|
||||
/// directly; the cylsphere and sphere loops are bounded only by
|
||||
/// <c>GetNumCylsphere</c>/<c>GetNumSphere</c> counts and call the real
|
||||
/// <c>CCylSphere::intersects_sphere</c> / <c>CSphere::intersects_sphere</c>
|
||||
/// per primitive — tests, not pre-filters. There are no "sorting-sphere
|
||||
/// early-outs" in it.</item>
|
||||
/// <item><c>CPartArray::FindObjCollisions</c> @0x00518180 is a bare
|
||||
/// do/while over <c>parts[i]</c>, and
|
||||
/// <c>CPhysicsPart::find_obj_collisions</c> @0x0050d8d0 does two null
|
||||
/// checks (<c>gfxobj</c>, then <c>gfxobj->physics_bsp</c> at
|
||||
/// <c>[ecx+0x78]</c>), <c>SPHEREPATH::cache_localspace_sphere</c>, and
|
||||
/// <c>CGfxObj::find_obj_collisions</c> @0x00534700. Neither contains a
|
||||
/// compare or any float math.</item>
|
||||
/// </list>
|
||||
/// Retail's only spatial rejection is the BSP node bounding-sphere test
|
||||
/// INSIDE the walk, which is centred on the node's own sphere origin and
|
||||
/// is therefore correct where the deleted filter was not: it measured to
|
||||
/// the part ORIGIN and compared against the BSP ROOT SPHERE's radius, two
|
||||
/// points that are 23.556 m apart for the Neftet rock of #337. See
|
||||
/// <c>docs/research/2026-08-06-337-neftet-wedge-mechanism.md</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private TransitionState FindObjCollisionsInCell(PhysicsEngine engine, uint cellId)
|
||||
|
|
@ -3774,7 +3806,13 @@ public sealed class Transition
|
|||
using var nearbyObjs = ShadowEntrySnapshot.Capture(objsInCell);
|
||||
|
||||
// #334 probe tallies — see LogReachQuery for what each one decides.
|
||||
int rExempt = 0, rReached = 0, rRejected = 0, rNoShape = 0,
|
||||
// `rejectedReach` is deliberately still REPORTED and structurally 0
|
||||
// since #333 deleted the filter: a run of the acceptance capture whose
|
||||
// [reach-q] lines read rejectedReach=0 where the pre-fix capture read
|
||||
// 7,225 rejections is the evidence the filter is gone, and dropping the
|
||||
// column would make the two captures incomparable.
|
||||
const int rRejected = 0;
|
||||
int rExempt = 0, rReached = 0, rNoShape = 0,
|
||||
rTested = 0, rBlocked = 0;
|
||||
|
||||
foreach (ShadowEntry obj in nearbyObjs.Entries)
|
||||
|
|
@ -3836,25 +3874,12 @@ public sealed class Transition
|
|||
continue;
|
||||
}
|
||||
|
||||
// Broad-phase: can the moving sphere reach this object?
|
||||
Vector3 deltaToCurr = currPos - obj.Position;
|
||||
float distToCurr;
|
||||
if (obj.CollisionType == ShadowCollisionType.Cylinder)
|
||||
distToCurr = MathF.Sqrt(deltaToCurr.X * deltaToCurr.X + deltaToCurr.Y * deltaToCurr.Y);
|
||||
else
|
||||
distToCurr = deltaToCurr.Length();
|
||||
float maxReach = sphereRadius + obj.Radius + movement.Length() + 2f;
|
||||
// NO BROAD-PHASE DISTANCE FILTER — see the method's remarks. Retail
|
||||
// walks the cell's shadow list unconditionally (#333, closing #337).
|
||||
// Cell membership IS the broad phase, and the BSP walk's own root
|
||||
// node bounding-sphere test — correctly centred, unlike the deleted
|
||||
// filter — is the early-out that made this one unnecessary.
|
||||
if (reachProbe) rReached++;
|
||||
if (distToCurr > maxReach)
|
||||
{
|
||||
if (reachProbe)
|
||||
{
|
||||
rRejected++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
"rejected-reach", sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Commit C 2026-04-29 — retail exemption block at the top of
|
||||
// CPhysicsObj::FindObjCollisions
|
||||
|
|
@ -4338,10 +4363,12 @@ public sealed class Transition
|
|||
? MathF.Sqrt(dOrigin.X * dOrigin.X + dOrigin.Y * dOrigin.Y)
|
||||
: dOrigin.Length();
|
||||
|
||||
// World-space offset from the part ORIGIN (what the filter measures
|
||||
// against) to the BSP root sphere CENTRE (what it should measure
|
||||
// against). Zero for non-BSP shapes, whose Position already IS their
|
||||
// centre.
|
||||
// World-space offset from the part ORIGIN (what the DELETED filter
|
||||
// measured against) to the BSP root sphere CENTRE (what it should have
|
||||
// measured against). Both distances are still emitted after #333: the
|
||||
// pair is what proved the diagnosis, and it stays comparable across the
|
||||
// pre-fix and post-fix captures. Zero for non-BSP shapes, whose
|
||||
// Position already IS their centre.
|
||||
Vector3 bspCentreOffset = Vector3.Zero;
|
||||
if (obj.CollisionType == ShadowCollisionType.BSP)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue