diag(render/physics): flap root-caused to physics rest µm-jitter; refute prior diagnoses

Apparatus + handoff for the indoor flap. Confirmed (primary evidence): the flap is the
portal-flood clip being µm-sensitive at the threshold, driven by a ~1-8µm jitter in the
player RenderPosition (physics resting position not bit-stable; Lerp surfaces it). REFUTES
the 2026-06-07 see-through/EnvCell/outdoor-node diagnosis (ModelId GfxObj 0x01000A2B IS the
solid exterior) AND an enqueue-once attempt (retail propagates late slices via AddToCell;
the existing PropagatesNewSlicesToExit test caught it; reverted). Adds: Build determinism
test, A8CellAudit gfxobj dump, [pv-input] 6dp probe + [render-sig] outRoot/bshell fields.
No functional fix shipped. Next: higher-precision physics rest trace -> port retail
kill_velocity/contact rest-stability. Canonical: docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-08 09:16:12 +02:00
parent d0b65c4170
commit d6aa526dd3
6 changed files with 300 additions and 1 deletions

View file

@ -30,6 +30,14 @@ else if (args.Length > 0 && string.Equals(args[0], "portals", StringComparison.O
foreach (var envCellId in ids)
DumpCellPortals(dats, envCellId);
}
else if (args.Length > 0 && string.Equals(args[0], "gfxobj", StringComparison.OrdinalIgnoreCase))
{
var ids = args.Length == 1
? new uint[] { 0x01000A2Bu }
: args.Skip(1).Select(ParseHex).ToArray();
foreach (var gfxObjId in ids)
DumpGfxObj(dats, gfxObjId);
}
else
{
var ids = args.Length == 0
@ -365,6 +373,98 @@ static (int RegistryBuildings, int ShellEntities) DumpLandblockBuildings(LandBlo
return (registryBuildingId - 1, shellEntities);
}
static void DumpGfxObj(DatCollection dats, uint gfxObjId)
{
Console.WriteLine($"=== GfxObj 0x{gfxObjId:X8} (RENDER polygons) ===");
var g = dats.Get<GfxObj>(gfxObjId);
if (g is null)
{
Console.WriteLine("missing GfxObj");
return;
}
var verts = g.VertexArray.Vertices;
var min = new Vector3(float.MaxValue);
var max = new Vector3(float.MinValue);
foreach (var v in verts.Values)
{
min = Vector3.Min(min, v.Origin);
max = Vector3.Max(max, v.Origin);
}
var centroid = (min + max) * 0.5f;
int walls = 0, floors = 0, ceilings = 0, slopes = 0;
int outwardWalls = 0, inwardWalls = 0;
int emitPos = 0, emitNeg = 0, skipped = 0;
foreach (var (polyId, poly) in g.Polygons.OrderBy(p => p.Key))
{
if (poly.VertexIds.Count < 3) continue;
bool hasPos = !poly.Stippling.HasFlag(StipplingType.NoPos);
bool hasNeg = poly.Stippling.HasFlag(StipplingType.Negative)
|| poly.Stippling.HasFlag(StipplingType.Both)
|| (!poly.Stippling.HasFlag(StipplingType.NoNeg) && poly.SidesType == CullMode.Clockwise);
if (hasPos) emitPos++;
if (hasNeg) emitNeg++;
if (!hasPos && !hasNeg) skipped++;
var n = ComputeNormalG(g, poly);
bool isWall = Math.Abs(n.Z) <= 0.15f;
bool isFloor = n.Z > 0.9f;
bool isCeiling = n.Z < -0.9f;
if (isFloor) floors++;
else if (isCeiling) ceilings++;
else if (isWall) walls++;
else slopes++;
if (isWall)
{
var pc = PolyCentroidG(g, poly);
var toFace = pc - centroid;
float outward = Vector3.Dot(n, toFace); // >0 => front face points away from center (exterior)
if (outward > 0) outwardWalls++; else inwardWalls++;
}
}
Console.WriteLine(
$"verts={verts.Count} renderPolys={g.Polygons.Count} hasPhysics={(g.PhysicsPolygons?.Count ?? 0)} " +
$"emitPos={emitPos} emitNeg={emitNeg} skipped={skipped}");
Console.WriteLine(
$"bbox min=({min.X:F2},{min.Y:F2},{min.Z:F2}) max=({max.X:F2},{max.Y:F2},{max.Z:F2}) " +
$"size=({max.X - min.X:F2},{max.Y - min.Y:F2},{max.Z - min.Z:F2})");
Console.WriteLine(
$"classify: walls={walls} (outwardFacing={outwardWalls} inwardFacing={inwardWalls}) " +
$"floors={floors} ceilings={ceilings} slopes={slopes}");
Console.WriteLine();
}
static Vector3 ComputeNormalG(GfxObj g, DatReaderWriter.Types.Polygon poly)
{
if (poly.VertexIds.Count < 3) return Vector3.Zero;
if (!g.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var a) ||
!g.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var b) ||
!g.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var c))
{
return Vector3.Zero;
}
var n = Vector3.Cross(b.Origin - a.Origin, c.Origin - a.Origin);
return n.LengthSquared() > 0f ? Vector3.Normalize(n) : Vector3.Zero;
}
static Vector3 PolyCentroidG(GfxObj g, DatReaderWriter.Types.Polygon poly)
{
var sum = Vector3.Zero;
int count = 0;
foreach (var vid in poly.VertexIds)
if (g.VertexArray.Vertices.TryGetValue((ushort)vid, out var v))
{
sum += v.Origin;
count++;
}
return count > 0 ? sum / count : Vector3.Zero;
}
static bool IsSupported(uint id)
{
uint type = id & 0xFF000000u;