acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs
Erik 5de42a12f3 fix(render): S4 chunk 1 round 1 — the ±12 guard with retail's quantifier, seal count order, the PM/PC depth-event transcript gate
Three-lens review of c7ab5b6d8 found the round-0 port of
DrawPortalPolyInternal's degenerate-input guard inverted retail's
quantifier: it rejected a polygon when ANY vertex sat on ANY of the
four +/-12 planes. The PDB-paired bytes (VA 0x59BCD6-0x59BD66: four
per-plane fld/fcomp 12.0/fnstsw/test ah,0x44/jnp sequences, each
accumulating a "every vertex on this plane" boolean, cleared by the
first vertex NOT on it) and the named decomp's own nested-if reading
of the post-loop branch (0x59bd42-0x59bd6c) agree: retail rejects only
when EVERY local vertex lies on the SAME one of the four planes — a
polygon degenerate onto a plane, not one that merely touches a plane
at one vertex.

F1 — the quantifier. WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard
now tracks four booleans (everyVertexOnPlusX/MinusX/PlusY/MinusY), each
starting true and cleared by the first vertex that fails its own plane
test; the guard rejects iff any of the four survives the whole vertex
loop. T2 (WalkVisibilityMathTests.cs) is rewritten to the five §7
cases: every vertex on x=+12 -> rejected; every vertex on y=-12 ->
rejected; one vertex on x=+12, rest inside -> KEPT; every vertex at
11.999 -> kept; vertices split across x=+12 and y=+12 (no common
plane) -> kept. WalkFrameDriverTests' punch-side pin
(OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape)
carries the same correction — its old "one vertex on the plane"
polygon was wrongly rejected under round 0; it punches now, and a new
all-on-plane polygon is added as the actual rejected case. The doc
comment's "essentially never" sentence is deleted and replaced with a
fresh DAT-scan citation (see datScan below).
MUTATION (verified): restore the any-vertex form -> the one-vertex and
split-plane admit cases fail (Assert.False Expected:False Actual:True,
x2; the WalkFrameDriverTests punch pin's Assert.Equal Expected:2
Actual:1). Delete the guard (always return false) -> the all-on-plane
reject cases fail (Assert.True Expected:True Actual:False, x3 —
plusX-plane, minusY-plane, and the vacuous-empty-polygon pin; the
punch pin's Assert.Equal Expected:2 Actual:3).

datScan (DAT counts backing F1's doc comment): a throwaway xunit test
(deleted after this commit — not part of the diff) walked the full
0x0000-0xFFFF landblock-prefix space (LandBlockInfo/EnvCell carry no
usable DatCollection.GetAllIdsOfType range — both are landblock-
relative — matching the method CellStructSurfaceConstructionInstalledDatTests
already established), resolved each EnvCell's Environment/CellStruct,
and for every CellPortal's polygon tested each vertex's local x/y
against +/-12. Against the installed client_cell_1.dat: 3,405
landblocks with cells, 1,854,237 total portal polygons. 2,889 polygons
have at least one vertex on a +/-12 plane; 2,163 have EVERY vertex on
the SAME plane — all 2,163 are EXIT portals (OtherCellId==0xFFFF, out
of 16,939 exit portals total); 0 interior (non-exit) portals ever
qualify (interior: 1,837,298 total, 340 any-vertex, 0 all-on-plane).
Duration ~15s. This exactly reproduces the review's cited numbers.

F2 — seal-path count order. RetailPViewPassExecutor's exit-seal loop
dropped the `localVertices.Length < 3` pre-filter that ran BEFORE both
the boundary guard and the `submitted` increment (round 0's order:
length check -> guard -> transform -> draw -> count). Retail's real
order is guard -> transform -> clip -> count, with the counter
incremented BEFORE polyClipFinish runs (0x59BD70-0x59BD74 precedes
0x59BDB0) — the counter records accepted ATTEMPTS, not successful GPU
fans. `submitted++` now runs immediately after the guard-gated
transform+EmitSeamMask, before DrawDepthFan (whose own `< 3` guard is
this port's stand-in for retail's post-clip `var_4 >= 3` check). A new
pin (RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing)
constructs a real PortalDepthMaskRenderer (RecordingGpuDevice-backed)
and a constructor-bypassed executor (RuntimeHelpers.GetUninitializedObject,
the same pattern WalkOutsideViewReassemblyTests already uses) with a
2-vertex ordinary-coordinate portal polygon: DrawExitPortalMask returns
submitted=1 while zero draw calls reach the device.
MUTATION (verified): restore the `< 3` pre-filter ahead of the guard
-> the pin's Assert.Equal fails, Expected:1 Actual:0.

F3 — the depth-event transcript gate, automated. New
WalkAlphaDepthTrace.cs (beside WalkOracleTrace.cs) parses the four
`PM poly=<ptr> mode=<0|1> counterBefore=<hex>` / `PC ov=<n>
counter=<hex> fc=<0|1>` lines per oh-capture/*.alphadepth.log frame,
using the same F-marker frame delimiting as WalkOracleTrace. New
WalkTraceConformanceTests.AlphaDepthTranscript.cs (beside
WalkTraceConformanceTests.cs, which is now `partial`) replays each
pose's frame 2 through the real WalkFrameDriver/RetailFrameWalk over
WalkLandscapeDatBuilder-built installed-DAT world data, with:
  - AlphaDepthCollectSink: an IWalkEventSink decorator forwarding every
    hook to the real driver unchanged, while recording mode=1 punch
    events (OnPunchGeometry, counterBefore = driver.PortalsDrawnCount
    at COLLECT time — Collect finishes entirely before Replay starts,
    so reading the counter at Replay time would already see the SAME
    turn's post-reset value; holtburg-doorway-still's own capture
    proves this: its punches show counterBefore=2 immediately followed
    by its seals' counterBefore=0/1 in the identical turn) and every
    PC tuple (Emit, WalkEventKind.DrawCells — fires for the root's own
    turn AND every building look-in's re-entrant call, matching
    retail's breakpoint not distinguishing them).
  - AlphaDepthLeaf: an IWalkFrameLeafRenderer whose DrawExitSeals
    reproduces DrawPortalDepthWrite's real per-portal loop (F1's guard,
    F2's count-before-clip order) directly over
    driver.InteriorFloodCells / WalkCell.Portals / .PortalPolygons —
    the identical data the render-side production path reads — since
    the interface gives DrawExitSeals no cell/portal parameter at all.
  - Two Collect+Replay passes per pose (a fresh driver reaches the SAME
    steady-state a running session already carries by the time
    retail's capture began — holtburg-doorway-still and foundry-deep
    both show a stable per-frame accepted-seal count that regenerates
    identically every captured frame): the first pass's own output is
    discarded, only the second is compared.
Five Facts (one per pose — the spec's "four" undercounts the five
*.alphadepth.log files actually present in oh-capture/):
  AlphaDepthTranscript_CathedralArrival_MatchesRetailFrame2 — PASS
  AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2 — PASS
  AlphaDepthTranscript_FoundryDeep_MatchesRetailFrame2 — PASS
  AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2 — PASS
    (proves the incrementing 0/1/2 accepted-seal sequence AND the
    pre-reset counterBefore=2 punches, end to end, against real DAT
    portal data)
  AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2 — KnownFailure
    (tagged [Trait("Status","KnownFailure")]): terrace-edge never runs
    a qualifying interior turn in its own capture (every PC line reads
    ov=0) — its punches' counterBefore=2 is a session value carried
    over from BEFORE the capture even started (the file's own pre-"F 1"
    content already reads 2; nothing inside the captured frames ever
    touches the counter). A fresh two-pass replay from
    PortalsDrawnCount=0 has no mechanism to derive that leftover value.
    Printed divergence: expected [(mode=1,counterBefore=2) x2], actual
    [(mode=1,counterBefore=0) x2]; the PC sequence (ov=0 both times)
    matches exactly. An initial-condition gap in the fixture, not a
    guard/count defect — the identical harness reproduces holtburg-
    doorway-still's and foundry-deep's own steady states exactly from
    cold starts.
MUTATION (verified): swap AlphaDepthLeaf's recorded seal mode from 0
to 1 -> AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2
fails (Assert.Equal Collections differ at pos 5: expected
(0,0),(0,1)... actual (1,0),(1,1)...).
DispatcherFixture and DrawScope (WalkFrameDriverTests.cs) are widened
from private to internal so the new gate can drive a real
WbDrawDispatcher without duplicating that fixture.

F4 — manifest comment truth. VulkanShaderManifestTests.cs's
portal_depth.vert.spv re-pin comment had the ULP direction backward
(claimed the OLD 0x3F7FFFFE literal was NEARER the camera than
retail's real 0x3F7FFFEF constant; portal_depth.vert's own shader-
source comment already had this correct — FARTHER, larger z/w) and
cited T1 as living in WalkVisibilityMathTests when
PortalDepthVert_FarPunchConstant_MatchesRetailExactBits is in this
same file. Both corrected.

Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings/0
errors. Hermetic lane (Lane!=InstalledDat&...&Status!=KnownFailure):
6829/6829 passed. InstalledDat lane (real client_cell_1.dat): 253
total, 247 passed, 5 failed — exactly the two #383 layout tests
(LayoutImporterMediaBearingChildSweepTests/LayoutImporterInvisibleSweepTests),
TowerAscentReplayTests' KnownFailure, Oh_doorway_still_first_frame_diff
(#458), and this round's own AlphaDepthTranscript_TerraceEdge
KnownFailure — no other divergence. Shader tests (VulkanShaderDescriptorContractTests
+ VulkanShaderManifestTests + RenderPackSpirvValidatorTests +
SkyVertexLayoutTests): 35/35. Register/divergence tests: 52/52.
dotnet build src/AcDream.App -c Release: 0 warnings/0 errors.

Register: no new AD row. F1/F2 are straight bug fixes toward retail
fidelity (not deviations); AD-119 (the C2 blend-vs-write-mask row)
does not describe the count order, so its neighbour needed no
correction per F2's own instruction.

Deviation from the fix-round spec's literal text: §7 F1 said "swap the
any-vertex hits instead of the four per-plane accumulators" as the
split-plane case's mutation — the code actually ported uses per-vertex
`if (x != 12f) flag = false;` clears, not a per-vertex OR; the mutation
verified above (restore the any-vertex FORM entirely) exercises the
same defect class and is the mutation actually run. §7 also names
"four" alphadepth.log fixtures where five exist on disk
(cathedral-arrival, cathedral-leak, foundry-deep,
holtburg-doorway-still, terrace-edge); all five are gated rather than
four, with cathedral-leak an unremarked addition alongside the four
the spec names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 23:38:52 +02:00

124 lines
5.4 KiB
C#

using System.Globalization;
using System.Text.RegularExpressions;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// S4-c1 fix round 1, F3: parser for the OH alpha/depth captures
/// (docs/research/2026-09-01-overhaul/oh-capture/*.alphadepth.log —
/// tools/walk-oracle/oh/oh-capture-alpha-depth.cdb.template documents the
/// line formats). Reads the two lines this round's gate cares about:
///
/// <list type="bullet">
/// <item><c>PM poly=&lt;ptr&gt; mode=&lt;0|1&gt; counterBefore=&lt;hex&gt;</c> —
/// one per <c>D3DPolyRender::DrawPortalPolyInternal</c> @0x0059bc90 ENTRY
/// (mode 1 = far-Z building punch, mode 0 = true-depth exit seal). The
/// breakpoint is at function ENTRY, before the boundary guard runs inside
/// the function — a PM line is emitted for every ATTEMPT, guard-rejected
/// or not; <c>counterBefore</c> is the persistent
/// <c>portalsDrawnCount</c> global sampled before THIS call's own possible
/// increment.</item>
/// <item><c>PC ov=&lt;n&gt; counter=&lt;hex&gt; fc=&lt;0|1&gt;</c> — one per
/// <c>PView::DrawCells</c> @0x005a4840 ENTRY (root turn AND every building
/// look-in's own re-entrant call alike — the breakpoint doesn't
/// distinguish). <c>counter</c> is <c>portalsDrawnCount</c> sampled at
/// THIS call's entry, before its own possible read-then-zero.</item>
/// </list>
///
/// Frame delimiting mirrors <see cref="WalkOracleTrace"/> exactly (same
/// <c>F &lt;n&gt;</c> marker, same "events between F_n and F_(n+1) belong to
/// frame n" rule, same final-frame drop) so a caller can parse the SAME
/// capture file with both parsers and get position-consistent frame
/// numbering — <see cref="WalkOracleTrace"/> for the pose, this type for
/// the PM/PC sequences.
/// </summary>
public static class WalkAlphaDepthTrace
{
public static IReadOnlyList<WalkAlphaDepthFrame> Parse(IEnumerable<string> lines)
{
var frames = new List<WalkAlphaDepthFrame>();
List<(int Mode, int CounterBefore)>? currentPm = null;
List<(int Ov, int Counter, int ForceClear)>? currentPc = null;
int currentNumber = 0;
foreach (string line in lines)
{
Match frameMatch = FramePattern.Match(line);
if (frameMatch.Success)
{
if (currentPm is not null)
{
frames.Add(new WalkAlphaDepthFrame(currentNumber, currentPm, currentPc!));
}
currentNumber = int.Parse(
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
currentPm = new List<(int, int)>();
currentPc = new List<(int, int, int)>();
continue;
}
if (currentPm is null)
continue;
Match pm = PmPattern.Match(line);
if (pm.Success)
{
currentPm.Add((
int.Parse(pm.Groups[1].Value, CultureInfo.InvariantCulture),
int.Parse(pm.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)));
continue;
}
Match pc = PcPattern.Match(line);
if (pc.Success)
{
currentPc!.Add((
int.Parse(pc.Groups[1].Value, CultureInfo.InvariantCulture),
int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
int.Parse(pc.Groups[3].Value, CultureInfo.InvariantCulture)));
continue;
}
// Anything else (FL/AM lines, cdb chrome) is out of this
// parser's scope — ignored, matching WalkOracleTrace's own
// catch-all.
}
// The last STARTED frame is deliberately never appended — the
// truncated detach frame, same rule as WalkOracleTrace.
return frames;
}
public static IReadOnlyList<WalkAlphaDepthFrame> Load(string root, string fixtureName)
{
string repoRoot = FindRepositoryRoot();
string path = Path.Combine(
repoRoot, Path.Combine(root.Split('/')), fixtureName + ".log");
return Parse(File.ReadLines(path));
}
private static string FindRepositoryRoot()
{
DirectoryInfo? dir = new(AppContext.BaseDirectory);
while (dir is not null)
{
if (File.Exists(Path.Combine(dir.FullName, "AcDream.slnx")))
return dir.FullName;
dir = dir.Parent;
}
throw new InvalidOperationException(
"AcDream.slnx not found above the test base directory; walk-oracle fixtures unavailable.");
}
private static readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
private static readonly Regex PmPattern = new(
@"^PM poly=[0-9a-f]+ mode=([01]) counterBefore=([0-9a-f]{4})\s*$", RegexOptions.Compiled);
private static readonly Regex PcPattern = new(
@"^PC ov=(\d+) counter=([0-9a-f]{4}) fc=([01])\s*$", RegexOptions.Compiled);
}
/// <summary>One capture frame's PM (mode, counterBefore) and PC (ov,
/// counter, forceClear) sequences, in the exact order the retail trace
/// recorded them. Pointers are intentionally not carried — S4-c1 F3's own
/// comparison ignores them.</summary>
public sealed record WalkAlphaDepthFrame(
int Number,
IReadOnlyList<(int Mode, int CounterBefore)> PmEvents,
IReadOnlyList<(int Ov, int Counter, int ForceClear)> PcEvents);