acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkPortalGateDumpTests.cs
Erik c1a029edf9 feat(render) Campaign FW1: NINE of ten fixtures conformant - the degrade arm
The moving-fixture divergence was the building degrade ladder:
GfxObjDegradeInfo::get_degrade @0x0051e4b0 (Ghidra-verified - BN's
FPU-flag pseudo-C misread BOTH arm selection and one formula) slides
each level's threshold from ideal toward MAX as the multiplier
approaches 1, and the live client runs the positive arm at ~0.99, so
level 0's portal-bearing BSP survives to ~max_dist (48 for the Holtburg
cottages), not ideal (24). The recon note's "deg_mul = -0.99" was a
sign misread; the negative arm's threshold slides toward MIN and
contradicts the fixtures from both directions. With the two-arm port,
holtburg-walkout, holtburg-transitions, and holtburg-walkabout pass
every pairable frame - the walkout-F2 microscope's prediction
(103,100 | 100x3 | 124 through the cottage exit views) landed exactly.

Also this round, falsified and reverted: a BN-driven swap of the
portal walker's negative/in-plane arms (Ghidra shows side 1 = negative
EMITS, side 2 = in-plane does not - the original port was correct; the
swap broke four fixtures). The walker docs and unit tests now pin the
Ghidra-verified truth table.

Parked with findings: foundry-entry F67 (right flood set, one
plane-side classification at the +/-eps boundary orders 11d before
116/118) and doorway-still (retail shows zero floods at a pose one
meter from walkout-F2's flooding pose; multi-portal clip boundary).

Suites: Walk 124/3 skips; hermetic 6,687/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 11:34:45 +02:00

229 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AcDream.App.Tests.Rendering;
using AcDream.App.Rendering.Walk;
using DatReaderWriter;
using DatReaderWriter.Options;
using System.Numerics;
using System.Text;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// FW1 look-in gate microscope: for the street fixture pose, dump every
/// gate input for building a9b4001a (retail punches it) vs a9b4001e
/// (retail does not): raw portal flags, the BSP-emitted polygon's plane,
/// the eye's signed distance and side, and the active-view clip count.
/// Writes the dump to the scratch directory; always passes.
/// </summary>
[Trait("Lane", "InstalledDat")]
public sealed class WalkPortalGateDumpTests
{
[Fact]
public void Dump_the_gate_inputs_for_a_punching_and_a_silent_building()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
}
IReadOnlyList<WalkOracleFrame> frames =
WalkOracleTrace.Load("posed/holtburg-street-outdoor");
WalkOracleFrame frame = frames[1];
Assert.NotNull(frame.Pose);
using var dats = new DatCollection(datDir!, DatAccessType.Read);
WalkLandscapeDatBuilder.BuiltWorld world =
WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId, frame.Pose.Origin);
var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells)
{
Buildings = world.Buildings,
};
// Install the outdoor default view as the active clip context.
var defaultView = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
defaultView, ctx.Rays, ctx.WorldViewpoint,
ctx.ViewportWidth, ctx.ViewportHeight);
ctx.SetActiveView(defaultView, 0);
var dump = new StringBuilder();
Span<WalkScreenPoint> clipped = stackalloc WalkScreenPoint[64];
dump.AppendLine($"pose cell={frame.Pose.CellId:x8} origin={frame.Pose.Origin}");
foreach (uint id in new[] { 0xA9B4001Au, 0xA9B4001Eu, 0xA9B40022u, 0xA9B40026u })
{
WalkBuilding building = world.Buildings.Keys.Single(b => b.PositionCellId == id);
float dist = ctx.ViewerDistanceTo(building);
Vector3 eyeInBuilding = ctx.ViewpointInBuilding(building);
dump.AppendLine(
$"building {id:x8}: dist={dist:f1} eff={MathF.Max(0f, dist - 100f):f1} "
+ $"eyeLocal={eyeInBuilding} portals={building.Portals.Length}");
WalkBspNode? bsp = building.SelectDrawingBsp(dist);
var refs = new List<WalkPortalRef>();
Collect(bsp, refs);
dump.AppendLine($" level BSP portal refs: {refs.Count}");
foreach (WalkPortalRef portalRef in refs)
{
ref WalkBldPortal bp = ref building.Portals[portalRef.PortalIndex];
float d = Vector3.Dot(portalRef.Polygon.Plane.Normal, eyeInBuilding)
+ portalRef.Polygon.Plane.D;
int side = d > WalkVisibilityMath.Epsilon ? 0
: d < -WalkVisibilityMath.Epsilon ? 1 : 2;
int n = ctx.ClipBuildingPolygon(building, portalRef.Polygon, side, clipped);
dump.AppendLine(
$" ref idx={portalRef.PortalIndex} other={bp.OtherCellId:x8} "
+ $"rawSide={bp.PortalSide} exact={bp.ExactMatch} "
+ $"planeD@eye={d:f2} eyeSide={side} clipN={n} "
+ $"gate(side==rawSide)={(side == bp.PortalSide ? "PASS" : "reject")}");
}
}
string path = Path.Combine(Path.GetTempPath(), "fw1-portal-gate-dump.txt");
File.WriteAllText(path, dump.ToString());
Assert.True(true);
}
private static void Collect(WalkBspNode? node, List<WalkPortalRef> into)
{
if (node is null) return;
if (node.InPortals is not null) into.AddRange(node.InPortals);
Collect(node.PosNode, into);
Collect(node.NegNode, into);
}
private sealed class NullSink : IWalkEventSink
{
public void Emit(in WalkEvent walkEvent) { }
}
/// <summary>
/// Walkout-F2 microscope: retail floods buildings a9b4001e (×4) and
/// a9b40026 (×1) through the cottage's exit views; the replay floods
/// neither, while the SAME machinery against the root view passes the
/// street fixture. Differential per portal: clip count against the
/// full-viewport window vs each exit-view window, plus each window's
/// vertices and signed area (winding).
/// </summary>
[Fact]
public void Dump_the_exit_view_look_in_inputs_for_walkout_f2()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
}
IReadOnlyList<WalkOracleFrame> frames =
WalkOracleTrace.Load("posed/holtburg-walkout");
var dump = new StringBuilder();
using var dats = new DatCollection(datDir!, DatAccessType.Read);
// Marker lag: F2's true camera lies between pose(F2) and pose(F3).
foreach (WalkOraclePose pose in new[] { frames[2].Pose!, frames[1].Pose! })
{
WalkLandscapeDatBuilder.BuiltWorld world =
WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin);
var ctx = new WalkTraceReplayContext(pose, world.Cells)
{
Buildings = world.Buildings,
};
WalkCell camera = world.Cells[pose.CellId];
var walk = new RetailFrameWalk();
walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, new NullSink());
WalkPortalView outside = walk.InteriorPView.OutsideView;
dump.AppendLine(
$"pose cell={pose.CellId:x8} origin={pose.Origin} ov={outside.ViewCount}");
for (int v = 0; v < outside.ViewCount; v++)
{
WalkViewPoly poly = outside.View.Polys[v];
var verts = new Vector2[poly.VertexCount];
for (int k = 0; k < poly.VertexCount; k++)
verts[k] = outside.View.Vertices[poly.VertexIndex + k].Point;
dump.AppendLine(
$" exit view {v}: n={poly.VertexCount} area={SignedArea(verts):f1} "
+ $"verts={string.Join(' ', verts.Select(p => $"({p.X:f1},{p.Y:f1})"))}");
}
var defaultView = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
defaultView, ctx.Rays, ctx.WorldViewpoint,
ctx.ViewportWidth, ctx.ViewportHeight);
{
WalkViewPoly rootPoly = defaultView.View.Polys[0];
var rootVerts = new Vector2[rootPoly.VertexCount];
for (int k = 0; k < rootPoly.VertexCount; k++)
rootVerts[k] = defaultView.View.Vertices[rootPoly.VertexIndex + k].Point;
dump.AppendLine(
$" root view: n={rootPoly.VertexCount} area={SignedArea(rootVerts):f1} "
+ $"verts={string.Join(' ', rootVerts.Select(p => $"({p.X:f1},{p.Y:f1})"))}");
}
foreach (uint id in new[] { 0xA9B4001Eu, 0xA9B40026u })
{
WalkBuilding building =
world.Buildings.Keys.Single(b => b.PositionCellId == id);
float dist = ctx.ViewerDistanceTo(building);
Vector3 eyeInBuilding = ctx.ViewpointInBuilding(building);
WalkBspNode? bsp = building.SelectDrawingBsp(dist);
dump.AppendLine(
$" building {id:x8}: dist={dist:f1} eff={MathF.Max(0f, dist - 100f):f1} "
+ $"bsp={(bsp is null ? "NULL" : "selected")} ports={CountPorts(bsp)}");
for (int li = 0; li < building.DegradeLevels.Length; li++)
{
WalkBuildingDegradeLevel lv = building.DegradeLevels[li];
dump.AppendLine(
$" level {li}: min={lv.MinDist:f1} ideal={lv.IdealDist:f1} "
+ $"max={lv.MaxDist:f1} ports={CountPorts(lv.DrawingBsp)} "
+ $"idealArmThr={lv.IdealDist:f1} "
+ $"negMulThr={lv.IdealDist - (lv.IdealDist - lv.MaxDist) * -0.99f:f1}");
}
if (bsp is null) continue;
var refs = new List<WalkPortalRef>();
WalkBuildingPortals.BuildDrawPortalsOnly(
bsp, 1, eyeInBuilding, (portalRef, _) => refs.Add(portalRef));
var clipped = new WalkScreenPoint[64];
foreach (WalkPortalRef portalRef in refs)
{
ref WalkBldPortal bp = ref building.Portals[portalRef.PortalIndex];
float d = Vector3.Dot(portalRef.Polygon.Plane.Normal, eyeInBuilding)
+ portalRef.Polygon.Plane.D;
int side = d > WalkVisibilityMath.Epsilon ? 0
: d < -WalkVisibilityMath.Epsilon ? 1 : 2;
ctx.SetActiveView(defaultView, 0);
int rootN = ctx.ClipBuildingPolygon(
building, portalRef.Polygon, side, clipped);
string rootVerts = string.Join(
' ',
clipped.Take(rootN).Select(p => $"({p.X:f1},{p.Y:f1},w={p.W:f3})"));
var perView = new StringBuilder();
for (int v = 0; v < outside.ViewCount; v++)
{
ctx.SetActiveView(outside, v);
int n = ctx.ClipBuildingPolygon(
building, portalRef.Polygon, side, clipped);
perView.Append($" view{v}N={n}");
}
dump.AppendLine(
$" ref idx={portalRef.PortalIndex} other={bp.OtherCellId:x8} "
+ $"rawSide={bp.PortalSide} eyeSide={side} "
+ $"gate={(side == bp.PortalSide ? "PASS" : "REJECT")} "
+ $"rootN={rootN}{perView}");
if (rootN > 0)
dump.AppendLine($" root-clipped: {rootVerts}");
}
}
}
string path = Path.Combine(Path.GetTempPath(), "fw1-walkout-f2-lookin-dump.txt");
File.WriteAllText(path, dump.ToString());
Assert.True(true);
}
private static int CountPorts(WalkBspNode? node)
=> node is null ? 0
: (node.IsPortal ? 1 : 0) + CountPorts(node.PosNode) + CountPorts(node.NegNode);
private static float SignedArea(Vector2[] verts)
{
float sum = 0f;
for (int i = 0; i < verts.Length; i++)
{
Vector2 a = verts[i];
Vector2 b = verts[(i + 1) % verts.Length];
sum += a.X * b.Y - b.X * a.Y;
}
return 0.5f * sum;
}
}