probe(physics): ACDREAM_PROBE_SUPPORT + ACDREAM_WIRE_MESH — separate #337's three candidates

The user is wedged at the top of Neftet rock plateaus, jumps sink into the
mesh, and a corpse falls straight through. ACDREAM_PROBE_REACH already ruled
out its own domain: blocked=0, every candidate tested-ok. Three candidates
remain — terrain support, a collision mesh not where its visual is, or the
transition wedging on an unobstructed path.

ACDREAM_PROBE_RESOLVE alone cannot separate them. It prints a three-value
contact-plane token, no plane normal, no plane height, no terrain sample and
no plane provenance, so all three produce the same line. Two additions:

[support] — one line per resolve for EVERY body, not just the player. A corpse
is a plain physics body with no player-specific logic, so its fall-through is
the cheapest available control on "movement code vs geometry data", and it is
invisible to any player-filtered probe. The line samples the outdoor terrain
INDEPENDENTLY at the body's own out-XY and prints the contact plane's own
height at that same XY. Two heights at one point make support=terrain /
object / none a measurement rather than an inference, and cpSrc= names the
site that asserted the plane so provenance and classification cross-check.

[geom] — once per GfxObj that comes near a mover: the object's physics-BSP
vertex cloud against its visual mesh AABB in the same local frame, through the
same prepared accessors the resolver queries. verdict=coincident REFUTES the
working hypothesis for that object outright; no-physics-bsp / empty-physics-bsp
/ displaced / extent-mismatch each name a specific data defect. Built to
refute, not to confirm — two diagnoses on this defect's lineage have already
been refuted by measurement.

ACDREAM_WIRE_MESH upgrades the existing F2 overlay, which drew a broadphase
proxy cylinder for BSP objects and so could not answer the question at all, to
the real physics-BSP polygon edges (cyan) beside the visual mesh box (magenta)
and the terrain surface (yellow). Own class per code-structure rule 1.

The provenance latch lives on PhysicsDiagnostics, not on CollisionInfo. Two
fields there first — the obvious home — broke the flat/graph differential
referee and the scratch-reset poison test, both of which compare CollisionInfo
member-for-member. Teaching either to skip a member is a one-line green fix
that puts a permanent hole in a referee whose whole job is comparing
everything. Captured as feedback_probe_state_off_compared_types.

Seven tests cover the support classifier's boundaries: a wrong classifier does
not fail to answer, it answers confidently wrong.

Gates: Release build 0 errors; complete suite 11,225 passed / 4 skipped / 0
failed from a cleaned tree — baseline 11,218/4/0 plus exactly the seven new
tests, skips unchanged.

Issue #337 filed with the symptom set, what is ruled out, and a table of what
each possible output means. All of this is TEMPORARY and recorded for
stripping with the physics-probe family.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-06 19:49:59 +02:00
parent 13fcf38138
commit 49a7e90652
9 changed files with 1376 additions and 1 deletions

View file

@ -64,6 +64,10 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
private readonly DebugVmRenderFactsPublisher _debugVm;
private readonly bool _debugVmConsumerActive;
private int _debugDrawLogCount;
// #337 (TEMPORARY): built on first use so the ordinary overlay path and
// every headless/no-window host pay nothing for it.
private CollisionMeshWireframe? _meshWireframe;
private CollisionMeshWireframeStats _lastMeshStats = new(-1, -1, -1, -1, false);
public WorldSceneDiagnosticsController(
WorldRenderDiagnostics diagnostics,
@ -215,6 +219,18 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
if (!_state.CollisionWireframesVisible || _lines is null)
return;
// #337 (2026-08-06 — TEMPORARY): ACDREAM_WIRE_MESH=1 replaces the
// broadphase-proxy overlay below with the objects' real physics-BSP
// polygon edges beside their visual mesh boxes and the terrain surface
// — see CollisionMeshWireframe for why the proxy cannot answer the
// question this mode exists for. Default off; F2 keeps its old
// behaviour otherwise.
if (RenderingDiagnostics.CollisionMeshWireframeEnabled)
{
DrawCollisionMesh(in camera);
return;
}
_lines.Begin();
int drawn = 0;
foreach (ShadowEntry shadow in _physics.ShadowObjects.AllEntriesForDebug())
@ -255,6 +271,50 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
_lines.Flush(camera.Camera.View, camera.Projection);
}
/// <summary>
/// #337 (2026-08-06 — TEMPORARY, strip with the probe family). Centres on
/// the player when there is one, else on the camera, so the fly-camera
/// mode can inspect geometry too.
/// </summary>
private void DrawCollisionMesh(in WorldCameraFrame camera)
{
Vector3 centre = _mode.IsPlayerMode && _player.Controller is { } controller
? controller.Position
: camera.Position;
_meshWireframe ??= new CollisionMeshWireframe(_physics);
_lines!.Begin();
CollisionMeshWireframeStats stats = _meshWireframe.Draw(_lines, centre);
if (_mode.IsPlayerMode && _player.Controller is { } player)
{
_lines.AddCylinder(
player.Position,
DebugVmRenderFactsPublisher.PlayerCollisionRadius,
1.8f,
new Vector3(1f, 0f, 0f));
}
_lines.Flush(camera.Camera.View, camera.Projection);
// A capped frame is showing PARTIAL geometry, which would otherwise be
// indistinguishable from an object that has none — exactly the
// misreading this overlay exists to prevent. Say so, throttled, rather
// than letting the picture lie.
if (stats != _lastMeshStats)
{
_lastMeshStats = stats;
Console.WriteLine(string.Format(
System.Globalization.CultureInfo.InvariantCulture,
"[wire-mesh] centre=({0:F2},{1:F2},{2:F2}) objects={3} polys={4} " +
"noPhysicsGeometry={5} lines={6} capped={7}",
centre.X, centre.Y, centre.Z,
stats.ObjectsConsidered, stats.PolygonsDrawn,
stats.ObjectsWithoutPhysicsGeometry, stats.LinesDrawn, stats.Capped));
}
}
private void LogNearbyCollisionObjects(Vector3 playerPosition, int drawn)
{
if (_debugDrawLogCount >= 5)