From 323f5a334d151be03a9889a37c0b01bf49f7be1c Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 3 Sep 2026 23:08:42 +0200 Subject: [PATCH] fix(render): S4 chunk 1 round 2 (LAST) - honest PM/PC gate, seeded portalsDrawnCount, the SOME-vs-SAME-plane pin, sixth pose, comment truth Round 1 (2bd353234) was three-lens reviewed: retail-faithful PASS, production PASS, gate-honesty FAIL. Every finding landed on the terrace-edge KnownFailure row plus one pin gap. This is the last round. R2-1 - gate honesty. WalkTraceConformanceTests.AlphaDepthTranscript.cs's own KnownFailure doc comment claimed "the PC sequence matches exactly"; it never did - Assert.Equal(expected.PmEvents, actualPm) threw first, so the PC Assert.Equal line never ran. RunAlphaDepthTranscriptGate now computes BOTH diffs (SequenceEqual, not Assert.Equal) before asserting either, and Assert.Fail's message prints both sequences together whenever either diverges - a written claim about one sequence is now only ever one this method actually evaluated. Re-running the OLD (unseeded, cold PortalsDrawnCount=0) code path through this new combined assertion shows terrace-edge's real divergence was WORSE than round 1 believed: both PM and PC diverged (PC expected counter=2, actual counter=0) - round 1's "PC matches" claim was not just untested, it was false. R2-2 - the harness fix. Retail's portalsDrawnCount (wo(008719b4)) is a PERSISTENT session global; each capture's own FIRST PM or PC line (cathedral-arrival 0, cathedral-leak 0, foundry-deep 1, holtburg-doorway-still 2, terrace-edge 2, cathedral-stair-arch 8) already carries the pre-capture value - terrace-edge's own sample sits in the "F 1" preamble text a cdb session prints before the first parsed frame marker (line 1209, ahead of "F 1" at line 1298), so no amount of replaying the CAPTURED frames from a cold start can ever reach it (this fixture's own turns never touch the counter - every PC line reads ov=0). New WalkAlphaDepthTrace.LoadInitialCounter(root, fixtureName) scans a fixture's raw lines, ignoring frame boundaries, for the first PM/PC match and returns its counter. RunAlphaDepthTranscriptGate seeds WalkFrameDriver.PortalsDrawnCount (already an internal, test-assembly- settable field) from this value, runs ONE Collect+Replay pass, and deletes the two-pass "priming" replay outright. Result: terrace-edge turns GREEN with no KnownFailure tag; every other pose stays exact (a fixture with real steady-state seal activity computes the identical value a priming pass used to converge to, since a steady state is by definition unchanged by one more pass). No pose diverges after seeding, so the packet's "write the divergence into section 9" fallback never triggers - nothing needed there this round. R2-3 - the SOME-vs-SAME-plane discriminator. T2's existing five cases never separated retail's real quantifier ("every vertex lies on the SAME one of the four +/-12 planes") from a weaker "every vertex lies on SOME plane" (per-vertex OR across the four planes, ANDed across vertices) - the existing split-plane case's third vertex sits on NO plane at all, so both readings admit it for the same reason. New case in WalkVisibilityMathTests.cs (Boundary_guard_admits_a_polygon_whose_every_vertex_is_on_SOME_plane_but_not_the_SAME_one) and a fourth sink.OnPunchGeometry call in WalkFrameDriverTests.cs's OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape pin close the gap: [(12,0,3),(0,12,3),(12,5,3)] - every vertex touches SOME plane (vertex1/3 on x=+12, vertex2 on y=+12) but no SINGLE plane holds all three (x=+12 cleared by vertex2's x=0, y=+12 cleared by vertex1's y=0) - retail's real four-accumulator guard admits it; the weaker per-vertex-OR form rejects it. R2-4 - the sixth pose. Cherry-picked 4f12cd06b (walk+alphadepth capture) and 3a6aa7eef (pmverts capture) from claude/campaign-w-retail-frame-walk onto 2bd353234 - both applied clean, no conflicts (README.md merged without a manual row). New AlphaDepthTranscript_CathedralStairArch_MatchesRetailFrame2 Fact: root 0xF4180114, seven outside views, sixteen mode=1 building punches (all counterBefore=0008 - far-Z punches never touch the counter) of which twelve are guard-rejected on local y=12.000, then eight mode=0 exit seals with counterBefore running 0..7 against the root's own ov=7 read-then-zero, settling this fixture's own steady-state counter at 8 - exactly the value LoadInitialCounter reads back out of its own preamble. PASS. R2-5 - comment truth. (a) RetailPViewPassExecutor.cs: DrawPortalDepthWrite's and DrawExitPortalMask's summaries corrected - neither the retired ">=3 vertices" pre-filter nor "always matches what reached the GPU" describe F2's actual count-before-clip behavior any more. (b) The F2 pin's doc comment (RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing) named TWO alternative mutations as if both failed; verified only one does (see mutations below) - the doc comment now says so explicitly and keeps only the pre-filter mutation. (c) WalkVisibilityMath.cs's doc comment stated DAT-scan figures as bare facts; independently re-verified this round with a freshly-written scan (own code, walking the same production EnvCell.CellPortals/CellStruct.Polygons shape WalkCellFactory itself uses, over the full 0x0000-0xFFFF landblock-prefix range) - every cited figure reproduced bit-exact (3,405 landblocks with cells; 1,854,237 portal polygons; 2,889 any-vertex; 2,163 all-on-plane, all exit, 0 interior; 16,939 exit portals total, 1,837,298 interior) - so the doc comment now states the counting rule (id range, what's counted, how unresolvable polygons/cells are skipped) alongside every figure instead of leaving them unqualified; landblocksWithCells=3,405 also independently matches CellStructSurfaceConstructionInstalledDatTests' own OH2 walk of the identical range (that test's own golden-count comment). The scratch scan itself is not part of this diff (deleted after use, same as F1's own datScan). (d) This commit body does not cite "register tests" as evidence - no test reads the register. Register truth is the human read: no row changes, AD-119 (the C2 blend-vs-write-mask row) unchanged. Mutations run this round (all verified against production code, then reverted): - R2-1/R2-2 (disable seeding, driver.PortalsDrawnCount = 0 instead of initialCounter): AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2 fails - "terrace-edge.alphadepth: PM DIVERGES - expected [(mode=1,counterBefore=2), (mode=1,counterBefore=2)], actual [(mode=1,counterBefore=0), (mode=1,counterBefore=0)]; PC DIVERGES - expected [(ov=0,counter=2,fc=0), (ov=0,counter=2,fc=0)], actual [(ov=0,counter=0,fc=0), (ov=0,counter=0,fc=0)]" - proving both the seeding fix (R2-2) and the honest dual-sequence message (R2-1) at once. - R2-3 (replace the four per-plane accumulators with a single per-vertex "on ANY plane" test ANDed across vertices): Boundary_guard_admits_a_polygon_whose_every_vertex_is_on_SOME_plane_but_not_the_SAME_one fails, Assert.False() Expected:False Actual:True; OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape fails, Assert.Equal() Expected:3 Actual:2. - R2-5b, mutation ONE (restore the old `localVertices.Length < 3` pre-filter ahead of the guard): DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing fails, Assert.Equal() Expected:1 Actual:0 - kept as the pin's cited mutation. - R2-5b, mutation TWO (move `submitted++` to AFTER `_portalDepthMask.DrawDepthFan(...)` instead of before): same test STILL PASSES - DrawDepthFan has no effect on the local `submitted` counter, so the final returned value is identical regardless of which side of that call the increment sits on. Confirmed unobservable exactly as R2-5b required; the doc comment no longer names this as a failing mutation. Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings/0 errors. dotnet build src/AcDream.App -c Release -> 0 warnings/0 errors. Hermetic lane (Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live& Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont& Purpose!=Diagnostic&Status!=KnownFailure): 6830/6830 passed (+1 over round 1's 6829, the new T2 case). InstalledDat lane (ACDREAM_RUN_INSTALLED_DAT_TESTS=1, --filter Lane=InstalledDat): 254 total (+1 over round 1's 253, the new sixth Fact), 249 passed, 4 failed - exactly the pre-existing four (LayoutImporterMediaBearingChildSweepTests and LayoutImporterInvisibleSweepTests, both #383; TowerAscentReplayTests' KnownFailure; Oh_doorway_still_first_frame_diff, #458) plus the pre-existing WalkLookInGateSweepTests skip - terrace-edge no longer fails, no new divergence anywhere else. All six AlphaDepthTranscript_*_MatchesRetailFrame2 Facts: 6/6 passed standalone. Shader classes (VulkanShaderDescriptorContractTests + VulkanShaderManifestTests + RenderPackSpirvValidatorTests + SkyVertexLayoutTests): 35/35. RetailPViewPassExecutorTests + WalkFrameDriverTests (full classes, regression check on the R2-3/R2-5b edits): 57/57. Commits: two cherry-picks (4f12cd06b, 3a6aa7eef) from claude/campaign-w-retail-frame-walk, both clean, plus this one fix commit on top of 2bd353234. Register: no new AD row; AD-119 unchanged (human read, not a test - see R2-5d above). Co-Authored-By: Claude Fable 5.1 --- .../Rendering/RetailPViewPassExecutor.cs | 28 ++- .../Rendering/Walk/WalkVisibilityMath.cs | 33 +++- .../Rendering/RetailPViewPassExecutorTests.cs | 17 +- .../Rendering/Walk/WalkAlphaDepthTrace.cs | 35 +++- .../Rendering/Walk/WalkFrameDriverTests.cs | 35 +++- ...ceConformanceTests.AlphaDepthTranscript.cs | 165 +++++++++++------- .../Rendering/Walk/WalkVisibilityMathTests.cs | 24 +++ 7 files changed, 246 insertions(+), 91 deletions(-) diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 502779f2..5143a780 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -286,10 +286,16 @@ public RetailPViewPassExecutor( _surface.ClearInteriorDepth(); } - /// Returns the number of exit-seal fan polygons actually - /// submitted (S3 §8.2 B2) — the SAME enumeration that draws them, so the - /// caller's persistent seal counter always matches what reached the GPU - /// this turn. + /// Returns the number of exit-seal fans ATTEMPTED this turn — + /// S4-c1 fix round 1 F2 corrected this from "matches what reached the + /// GPU": retail increments portalsDrawnCount BEFORE + /// polyClipFinish runs (0x59BD70-0x59BD74 precedes 0x59BDB0), so + /// the counter records guard-passing ATTEMPTS, not successful GPU fans. + /// A portal whose polygon has fewer than 3 vertices is counted here even + /// though the GPU renderer's own internal <3 guard + /// (DrawDepthFan) draws nothing for it + /// (RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing + /// pins exactly this). public int DrawExitPortalMask( RetailPViewFrameInput frame, uint cellId, @@ -366,10 +372,16 @@ public RetailPViewPassExecutor( /// Retail D3DPolyRender::DrawPortalPolyInternal /// @0x0059BC90. Main interior roots stamp true depth (seal); outdoor and /// look-in apertures stamp far depth (punch). The renderer owns that - /// choice. Returns the number of portals actually submitted — S3 §8.2 B2: - /// each portal with OtherCellId == 0xFFFF and >=3 vertices — - /// the same enumeration WalkFrameDriver.OnInteriorFloodDrawTurn - /// counts through DrawExitSeals's return value. + /// choice. Returns the number of portals ATTEMPTED (not necessarily + /// drawn) this turn — S4-c1 fix round 1 F2 retired the ">=3 vertices" + /// pre-filter that used to gate the count: every portal with + /// OtherCellId == 0xFFFF that survives the ±12 boundary guard + /// () + /// is counted here, BEFORE the vertex-count check that decides whether + /// its fan actually draws — matching retail's own count-BEFORE-clip + /// order (0x59BD70-0x59BD74 precedes 0x59BDB0). This is the same + /// enumeration WalkFrameDriver.OnInteriorFloodDrawTurn counts + /// through DrawExitSeals's return value. private int DrawPortalDepthWrite( uint cellId, ReadOnlySpan clipPlanes, diff --git a/src/AcDream.App/Rendering/Walk/WalkVisibilityMath.cs b/src/AcDream.App/Rendering/Walk/WalkVisibilityMath.cs index abb20f7b..9d5c0de2 100644 --- a/src/AcDream.App/Rendering/Walk/WalkVisibilityMath.cs +++ b/src/AcDream.App/Rendering/Walk/WalkVisibilityMath.cs @@ -222,13 +222,32 @@ public static class WalkVisibilityMath /// xformStart, the world transform — against exactly +12 /// and -12. Retail's order is reject → transform → clip → count /// (no transform, no clip, no portalsDrawnCount increment on a - /// reject). A scan of the installed cell DAT (full 0x0000-0xFFFF - /// landblock-prefix walk, 3,405 landblocks with cells, 1,854,237 portal - /// polygons; S4-c1 fix round 1 F1) found 2,889 polygons with at least - /// one vertex on a +/-12 plane and 2,163 polygons with EVERY vertex on - /// the SAME plane — all 2,163 are EXIT portals (OtherCellId == - /// 0xFFFF; 0 interior portals qualify), out of 16,939 exit portals - /// total. This guard therefore rejects real content: retail never + /// reject). + /// + /// DAT-scan figures (S4-c1 fix round 1 F1, independently + /// re-verified at fix round 2 R2-5c with a second, separately-written + /// scan over the SAME production EnvCell.CellPortals / + /// CellStruct.Polygons shape + /// itself walks — both scans agree on every figure below bit-for-bit). + /// Id range: the full 0x0000-0xFFFF landblock-prefix + /// space, each landblock's cells enumerated 0x0100 through + /// 0x0100 + LandBlockInfo.NumCells - 1 (the same range + /// CellStructSurfaceConstructionInstalledDatTests' own + /// independent OH2 walk uses — its pinned + /// landblocksWithCells=3,405 diagnostic already agrees). What is + /// counted: one entry per CellPortal whose PolygonId + /// resolves in its cell's CellStruct.Polygons AND has >=3 + /// vertex ids that all resolve in CellStruct.VertexArray — an + /// unresolvable PolygonId, a <3-vertex polygon, or a cell + /// whose own EnvironmentId/CellStructure does not resolve + /// is skipped ENTIRELY (not counted toward the total or either plane + /// tally, matching this method's own live callers, which never see such + /// a polygon in the first place). Under that rule: 3,405 landblocks with + /// cells, 1,854,237 portal polygons; 2,889 with at least one vertex on a + /// +/-12 plane, 2,163 with EVERY vertex on the SAME plane — all 2,163 + /// are EXIT portals (OtherCellId == 0xFFFF; 0 interior portals + /// qualify), out of 16,939 exit portals total (1,837,298 interior + /// portals). This guard therefore rejects real content: retail never /// punches or seals those 2,163 exit portals, which is very likely the /// "never-drawn portal polygon = panel" mechanism the PV campaign named /// (#456). diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs index 6352e6a6..75513f23 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs @@ -498,11 +498,18 @@ public sealed class RetailPViewPassExecutorTests /// < 3 continue ahead of both the boundary guard and the /// count — never observed on authored dat data (every real portal /// polygon has >= 3 vertices) but the wrong order all the same. - /// MUTATION: move the increment back to AFTER - /// (or restore the - /// old localVertices.Length < 3 pre-filter) — this pin's - /// synthetic 2-vertex polygon is no longer counted and the assertion - /// fails (submitted comes back 0, not 1). + /// MUTATION (verified, S4-c1 fix round 2 R2-5b): restore the old + /// localVertices.Length < 3 pre-filter ahead of the guard — + /// this pin's synthetic 2-vertex polygon is no longer counted and the + /// assertion fails (submitted comes back 0, not 1). Moving the + /// submitted++ increment itself to AFTER + /// — the mutation + /// this doc comment used to name alongside the pre-filter one — does + /// NOT fail this pin: DrawDepthFan has no effect on the local + /// submitted counter either way, so the final returned count is + /// identical regardless of which side of that call the increment sits + /// on. That reordering is unobservable to any assertion on the return + /// value; only the pre-filter mutation is a genuine regression check. /// [Fact] public void DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing() diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs index 1ea4611a..12a87710 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs @@ -87,11 +87,40 @@ public static class WalkAlphaDepthTrace } public static IReadOnlyList Load(string root, string fixtureName) + { + return Parse(File.ReadLines(ResolvePath(root, fixtureName))); + } + + /// S4-c1 fix round 2 (R2-2): retail's portalsDrawnCount + /// (wo(008719b4)) is a PERSISTENT session global carried by the running + /// client across every captured frame, so each capture's own FIRST PM or + /// PC line — wherever it falls, including the "F 1" preamble text a cdb + /// session prints before the first parsed frame marker (terrace-edge and + /// holtburg-doorway-still both have their first sample there) — already + /// carries the value a real session would have accumulated before the + /// capture began. Scans 's raw lines, + /// ignoring frame boundaries entirely, for the first line matching + /// either or and returns + /// its counter field. + public static int LoadInitialCounter(string root, string fixtureName) + { + foreach (string line in File.ReadLines(ResolvePath(root, fixtureName))) + { + Match pm = PmPattern.Match(line); + if (pm.Success) + return int.Parse(pm.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + Match pc = PcPattern.Match(line); + if (pc.Success) + return int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + throw new InvalidOperationException( + $"{fixtureName}: no PM/PC line found to seed PortalsDrawnCount from."); + } + + private static string ResolvePath(string root, string fixtureName) { string repoRoot = FindRepositoryRoot(); - string path = Path.Combine( - repoRoot, Path.Combine(root.Split('/')), fixtureName + ".log"); - return Parse(File.ReadLines(path)); + return Path.Combine(repoRoot, Path.Combine(root.Split('/')), fixtureName + ".log"); } private static string FindRepositoryRoot() diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs index 1aae8f00..ea821ee1 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs @@ -1090,17 +1090,42 @@ public sealed partial class WalkFrameDriverTests }, activeViewIndex: 0); + // Admitted (S4-c1 fix round 2, R2-3): every vertex sits on SOME + // +/-12 plane, but not all on the SAME one — vertex 1's y=0 clears + // the y=+12 accumulator, vertex 2's x=0 clears the x=+12 + // accumulator, so neither of the four per-plane accumulators + // survives to the end of the loop. This is the case that tells + // retail's real quantifier ("every vertex on the SAME plane") apart + // from the weaker "every vertex on SOME plane" (per-vertex OR + // across the four planes, ANDed across vertices) — every vertex + // here individually touches a plane, yet retail still draws it. + // MUTATION: replace the four independent per-plane accumulators + // with a single per-vertex "is this vertex on ANY of the four + // planes" test ANDed across vertices — this case starts failing + // (wrongly rejected): every vertex here IS on some plane, so the + // per-vertex-OR form rejects it. + sink.OnPunchGeometry( + building, + new WalkPolygon + { + Vertices = [new(12f, 0f, 3f), new(0f, 12f, 3f), new(12f, 5f, 3f)], + Plane = new WalkPlane(Vector3.UnitZ, -3f), + }, + activeViewIndex: 0); + driver.EndFrame(); driver.Replay(draw.Frame, draw.Pass); - // Exactly TWO punches reached the leaf — the all-on-plane polygon + // Exactly THREE punches reached the leaf — the all-on-plane polygon // produced no PunchFan event at all (not a punch that draws zero - // vertices; no event, full stop); the other two (one-vertex-on-plane, - // just-inside) are ordinary and both punched. - Assert.Equal(2, leaf.Punches.Count); + // vertices; no event, full stop); the other three (one-vertex-on- + // plane, just-inside, split-across-two-planes) are ordinary and all + // punched. + Assert.Equal(3, leaf.Punches.Count); Assert.Equal(new Vector3(12f, 0f, 3f), leaf.Punches[0].Vertices[1]); Assert.Equal(new Vector3(11.999f, 0f, 3f), leaf.Punches[1].Vertices[1]); - Assert.Equal(2, log.Count(entry => entry == "PUNCH:3@v0")); + Assert.Equal(new Vector3(0f, 12f, 3f), leaf.Punches[2].Vertices[1]); + Assert.Equal(3, log.Count(entry => entry == "PUNCH:3@v0")); } [Fact] diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs index 273cfabc..85da59d1 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Numerics; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Walk; @@ -44,15 +45,34 @@ namespace AcDream.App.Tests.Rendering.Walk; /// interleaved PM order without needing a shared Collect/Replay timeline). /// /// -/// Why two Collect+Replay passes per pose, not one. Retail's -/// portalsDrawnCount is a PERSISTENT global, unaffected by any reset -/// between captured frames whose interior turn never fires (see -/// terrace-edge below). holtburg-doorway-still and foundry-deep both show a -/// STEADY-STATE cycle (the same N seals accepted every captured frame, -/// carrying the SAME counter value into the next frame's own PC read) — -/// reachable from a cold PortalsDrawnCount=0 start after exactly one -/// full "priming" pass, so this gate discards a first Collect+Replay pass's -/// own output and compares only the second. +/// Why the counter is SEEDED, not primed by a throwaway pass +/// (S4-c1 fix round 2, R2-2). Retail's portalsDrawnCount is a +/// PERSISTENT session global (wo(008719b4)), unaffected by any reset +/// between captured frames — a running client carries whatever value the +/// counter last settled at into every subsequent capture. Each fixture's +/// own FIRST PM or PC line (wherever it falls — including the "F 1" +/// preamble text a cdb session prints before the first parsed frame +/// marker, which is where terrace-edge's and holtburg-doorway-still's own +/// first samples live) already carries that pre-capture value: +/// cathedral-arrival 0, cathedral-leak 0, foundry-deep 1, +/// holtburg-doorway-still 2, terrace-edge 2, cathedral-stair-arch 8 (see +/// 's own doc comment). +/// Fix round 1 tried to reach a matching steady state by running a +/// throwaway "priming" Collect+Replay pass first and discarding its +/// output — this works only for a fixture whose captured frames themselves +/// contain enough seal activity to climb from a cold +/// PortalsDrawnCount=0 start up to the needed value (holtburg- +/// doorway-still, foundry-deep); terrace-edge's own capture never runs a +/// qualifying interior turn at all (every PC line reads ov=0), so no +/// number of priming passes from a cold start can ever reach its +/// counterBefore=2 — the value has to come from BEFORE the capture, +/// not from replaying the capture against itself. Seeding +/// directly from the +/// fixture's own first sample before a SINGLE Collect+Replay pass fixes +/// this for every pose, terrace-edge included, without changing what a +/// pose with real seal activity computes (a fixture's own first sample +/// IS the steady-state value fix round 1's priming pass converged to, +/// since a steady state is by definition unchanged by one more pass). /// public sealed partial class WalkTraceConformanceTests { @@ -223,11 +243,18 @@ public sealed partial class WalkTraceConformanceTests public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) => Matrix4x4.Identity; } - /// Runs the two-pass replay (see class doc comment for why two) - /// and asserts the SECOND pass's PM/PC sequences against + /// Seeds from + /// 's own first observed sample (S4-c1 fix + /// round 2, R2-2 — see class doc comment), runs ONE Collect+Replay pass, + /// and asserts BOTH the PM and PC sequences against /// 's captured frame 2 /// ('s frames[1], matching every - /// other OH-capture row's own frame-2 convention in this file). + /// other OH-capture row's own frame-2 convention in this file). + /// R2-1 (gate honesty): both diffs are computed BEFORE either is + /// asserted, so a PM mismatch can never prevent the PC comparison from + /// running (or vice versa) — a written claim that one sequence + /// "matches" is only ever one this method actually evaluated. + /// private void RunAlphaDepthTranscriptGate(string fixtureName) { IReadOnlyList poseFrames = WalkOracleTrace.Load(OhCaptureRoot, fixtureName); @@ -240,6 +267,7 @@ public sealed partial class WalkTraceConformanceTests IReadOnlyList depthFrames = WalkAlphaDepthTrace.Load(OhCaptureRoot, fixtureName); Assert.True(depthFrames.Count > 1, $"{fixtureName}: need a captured frame 2's PM/PC content."); WalkAlphaDepthFrame expected = depthFrames[1]; + int initialCounter = WalkAlphaDepthTrace.LoadInitialCounter(OhCaptureRoot, fixtureName); using DatCollection dats = OpenDats(); WalkLandscapeDatBuilder.BuiltWorld world = @@ -257,40 +285,45 @@ public sealed partial class WalkTraceConformanceTests var sink = new AlphaDepthCollectSink(driver); var walk = new RetailFrameWalk(); - void RunOnePass() + // R2-2: seed the persistent counter from the fixture's own + // pre-capture value instead of priming a throwaway first pass. + driver.PortalsDrawnCount = initialCounter; + + using (WalkFrameDriverTests.DrawScope draw = fx.BeginDraw()) { - using (WalkFrameDriverTests.DrawScope draw = fx.BeginDraw()) - { - driver.BeginFrame(ctx, Matrix4x4.Identity, pose.Origin); - walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, sink); - driver.EndFrame(); - driver.Replay(draw.Frame, draw.Pass); - } - // DrawScope.Dispose ends only the pass/publication — the frame - // itself (RecordingGpuDevice's "one open frame" invariant) is - // ended separately, same order WalkOutsideViewReassemblyTests' - // own multi-draw loop uses. - fx.FrameLifetime.EndFrame(); + driver.BeginFrame(ctx, Matrix4x4.Identity, pose.Origin); + walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, sink); + driver.EndFrame(); + driver.Replay(draw.Frame, draw.Pass); } - - // Priming pass: reaches the steady state a real running session - // would already be in by the time retail's capture began (see class - // doc comment) — its own recorded output is discarded. - RunOnePass(); - sink.Punches.Clear(); - sink.PcEvents.Clear(); - leaf.Seals.Clear(); - - // Comparison pass. - RunOnePass(); + // DrawScope.Dispose ends only the pass/publication — the frame + // itself (RecordingGpuDevice's "one open frame" invariant) is + // ended separately, same order WalkOutsideViewReassemblyTests' + // own multi-draw loop uses. + fx.FrameLifetime.EndFrame(); var actualPm = new List<(int Mode, int CounterBefore)>(sink.Punches); actualPm.AddRange(leaf.Seals); + IReadOnlyList<(int Ov, int Counter, int ForceClear)> actualPc = sink.PcEvents; - Assert.Equal(expected.PmEvents, actualPm); - Assert.Equal(expected.PcEvents, sink.PcEvents); + bool pmMatches = expected.PmEvents.SequenceEqual(actualPm); + bool pcMatches = expected.PcEvents.SequenceEqual(actualPc); + if (!pmMatches || !pcMatches) + { + Assert.Fail( + $"{fixtureName}: PM {(pmMatches ? "matches" : "DIVERGES")} " + + $"— expected {FormatPm(expected.PmEvents)}, actual {FormatPm(actualPm)}; " + + $"PC {(pcMatches ? "matches" : "DIVERGES")} " + + $"— expected {FormatPc(expected.PcEvents)}, actual {FormatPc(actualPc)}"); + } } + private static string FormatPm(IReadOnlyList<(int Mode, int CounterBefore)> events) => + "[" + string.Join(", ", events.Select(e => $"(mode={e.Mode},counterBefore={e.CounterBefore})")) + "]"; + + private static string FormatPc(IReadOnlyList<(int Ov, int Counter, int ForceClear)> events) => + "[" + string.Join(", ", events.Select(e => $"(ov={e.Ov},counter={e.Counter},fc={e.ForceClear})")) + "]"; + [Fact] public void AlphaDepthTranscript_CathedralArrival_MatchesRetailFrame2() => RunAlphaDepthTranscriptGate("cathedral-arrival.alphadepth"); @@ -304,36 +337,42 @@ public sealed partial class WalkTraceConformanceTests => RunAlphaDepthTranscriptGate("holtburg-doorway-still.alphadepth"); /// - /// PINNED KnownFailure (S4-c1 fix round 1, F3): terrace-edge never runs - /// a qualifying interior turn anywhere in its own capture (every PC line - /// reads ov=0) — its two building punches' counterBefore=2 - /// is a PERSISTENT session value carried over from BEFORE the capture - /// even started (the file's own pre-"F 1" content already reads 2; no - /// mechanism inside the captured frames ever changes it). A fresh two- - /// pass replay from PortalsDrawnCount=0 has no way to derive that - /// leftover value — there is no seal activity anywhere in this fixture - /// to prime it — so this pose's own comparison pass reads - /// counterBefore=0 where retail shows 2. This is an initial- - /// condition gap in the fixture itself, not a guard/count defect: the - /// SAME replay harness reproduces holtburg-doorway-still's and foundry- - /// deep's own steady-state counter values exactly from a cold start. - /// Observed divergence (both PM events, the fixture's only two): - /// expected [(mode=1, counterBefore=2), (mode=1, counterBefore=2)], - /// actual [(mode=1, counterBefore=0), (mode=1, counterBefore=0)]; - /// the PC sequence (ov=0 both times) matches exactly. + /// S4-c1 fix round 2 (R2-2): seeding + /// from terrace-edge's own first observed sample (counterBefore=2, + /// carried in the file's pre-"F 1" preamble text — see + /// 's own doc + /// comment) closes fix round 1's KnownFailure: both of this fixture's + /// PM events now read the correct pre-capture value instead of a cold + /// 0, and its PC sequence (ov=0 throughout — this pose + /// never runs a qualifying interior turn at all) already matched. /// [Fact] - [Trait("Status", "KnownFailure")] public void AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2() => RunAlphaDepthTranscriptGate("terrace-edge.alphadepth"); - /// See 's - /// own doc comment — cathedral-leak carries the exact same steady state - /// (counter pinned at 0, every exit portal degenerate-onto-plane - /// rejected) as cathedral-arrival, so it is expected to pass; listed - /// here because the spec's own fixture count (four) undercounts the - /// five *.alphadepth.log files actually present in - /// oh-capture/ — see this round's commit body. + /// + /// S4-c1 fix round 2, R2-4: the sixth pose (#464 artifact pose, root + /// 0xF4180114, seven outside views). Sixteen building punches — + /// all mode=1, all counterBefore=0008 (far-Z punches never + /// touch the counter) — of which twelve are guard-rejected on local + /// y=12.000 (WalkVisibilityMath's ±12 boundary guard, S4-c1 F1), + /// leaving four admitted; then eight true-depth exit seals with + /// counterBefore running 0..7 against the root's own + /// ov=7 read-then-zero, settling this fixture's own steady-state + /// counter at 8 — the same value + /// reads back out of this fixture's own preamble, confirming the seed + /// and the replay agree on the same persistent value. + /// + [Fact] + public void AlphaDepthTranscript_CathedralStairArch_MatchesRetailFrame2() + => RunAlphaDepthTranscriptGate("cathedral-stair-arch.alphadepth"); + + /// Cathedral-leak carries the exact same steady state (counter + /// pinned at 0, every exit portal degenerate-onto-plane rejected) as + /// cathedral-arrival; listed here (S4-c1 fix round 1, F3) because the + /// original spec's own fixture count (four) undercounts the five + /// *.alphadepth.log files that were already present in + /// oh-capture/ at that round — see that round's commit body. [Fact] public void AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2() => RunAlphaDepthTranscriptGate("cathedral-leak.alphadepth"); diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkVisibilityMathTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkVisibilityMathTests.cs index 7d88e605..bbbb116d 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkVisibilityMathTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkVisibilityMathTests.cs @@ -271,6 +271,30 @@ public sealed class WalkVisibilityMathTests Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon)); } + [Fact] + public void Boundary_guard_admits_a_polygon_whose_every_vertex_is_on_SOME_plane_but_not_the_SAME_one() + { + // S4-c1 fix round 2, R2-3: the existing split-plane case above + // (12,0,3)/(0,12,3)/(-5,-5,3) does NOT discriminate retail's real + // quantifier ("every vertex on the SAME plane") from the weaker + // "every vertex on SOME plane" (per-vertex OR across the four + // planes, ANDed across vertices) — its third vertex sits on NO + // plane at all, so both readings admit it for the same reason. This + // case closes that gap: EVERY vertex here touches a plane — + // (12,0,3) and (12,5,3) sit on x=+12, (0,12,3) sits on y=+12 — yet + // no SINGLE plane holds all three: x=+12 is cleared by vertex 2 + // (x=0), y=+12 is cleared by vertex 1 (y=0). Retail's four + // independent per-plane accumulators admit it (neither survives to + // the end of the loop). + // MUTATION: replace the four per-plane accumulators with a single + // per-vertex "is this vertex on ANY of the four planes" test ANDed + // across vertices — this case starts failing (wrongly rejected): + // every vertex here IS on some plane, so the per-vertex-OR form + // rejects it. + Vector3[] polygon = [new Vector3(12f, 0f, 3f), new Vector3(0f, 12f, 3f), new Vector3(12f, 5f, 3f)]; + Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon)); + } + [Fact] public void Boundary_guard_ignores_the_vertical_z_component() {