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>
124 lines
5.4 KiB
C#
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=<ptr> mode=<0|1> counterBefore=<hex></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=<n> counter=<hex> fc=<0|1></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 <n></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);
|