using System.Numerics; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Walk; using DatReaderWriter; namespace AcDream.App.Tests.Rendering.Walk; /// /// S4-c1 fix round 1, F3: the automated depth-event transcript gate. /// Replays each OH alpha/depth capture's frame 2 through the SAME ported /// walk ( + , /// + /// — the identical replay harness /// already uses) with a RECORDING sink/leaf pair that reproduces retail's /// own PM (mode, counterBefore) and PC (ov, counter, forceClear) sequences, /// then compares them against 's parse of /// the real capture, exactly (pointers ignored). /// /// Why two observation points, not one. Retail's PM line fires /// at DrawPortalPolyInternal's function ENTRY — before the four-plane /// boundary guard (S4-c1 F1) has run — so a PM line is emitted for every /// ATTEMPT, guard-rejected or not. A building punch's guard decision runs /// INSIDE (a Collect-time /// hook): a rejected punch never reaches /// at all, so intercepts /// OnPunchGeometry ITSELF, at Collect time, before the guard runs — /// reading there gives the /// PRE-reset value retail's own punches observe (Collect completes entirely /// before Replay starts, so reading the counter at Replay time would already /// see the root's own post-reset value — the wrong number, confirmed against /// holtburg-doorway-still's own counterBefore=0002 punches immediately /// followed by counterBefore=0000/0001 seals in the SAME turn). Exit seals /// have no such Collect-time per-portal hook at all ( takes no cell/portal /// argument — RetailPViewPassExecutor.DrawPortalDepthWrite owns the /// real per-portal loop entirely at Replay time), so /// reproduces that SAME loop (S4-c1 F1's guard, F2's count-before-clip order) /// directly over the walk's own /// / / — /// the identical LOCAL portal-polygon data the production seal path reads, /// at Replay time, which is also where retail's own exit-seal loop runs /// (established: punches always precede the single per-root ExitSeals /// Collect-time event, so simple list concatenation reproduces the exact /// 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. /// public sealed partial class WalkTraceConformanceTests { /// Collect-time decorator: forwards /// every hook to the real unchanged (so its /// own event collection stays complete and correct), while separately /// recording the two families this gate needs — see this file's own /// class doc comment for why punches must be captured HERE rather than /// at Replay. private sealed class AlphaDepthCollectSink : IWalkEventSink { private readonly WalkFrameDriver _driver; private readonly IWalkEventSink _inner; public AlphaDepthCollectSink(WalkFrameDriver driver) { _driver = driver; _inner = driver; } public readonly List<(int Mode, int CounterBefore)> Punches = new(); public readonly List<(int Ov, int Counter, int ForceClear)> PcEvents = new(); public void Emit(in WalkEvent walkEvent) { // PView::DrawCells @0x005a4840's own PC print fires at function // ENTRY for every call — the root's own turn AND every building // look-in's re-entrant call alike (oh1-depth-lifecycle.md: the // breakpoint doesn't distinguish). WalkEventKind.DrawCells's // Emit call is the walk's own "this DrawCells call happened" // signal at that SAME entry-order point (its own doc comment: // "sits at breakpoint-ENTRY order"); forceClear (oh1: no write // site found anywhere in the binary) is always 0. if (walkEvent.Kind == WalkEventKind.DrawCells) PcEvents.Add((walkEvent.OutsideViewCount, _driver.PortalsDrawnCount, 0)); _inner.Emit(walkEvent); } public void OnLandscapeViews(WalkPortalView activeViews) => _inner.OnLandscapeViews(activeViews); public void OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex) => _inner.OnLandCellTurn(landblockId, sideCellCount, cellIndex); public void OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex) => _inner.OnSortCellTurn(landblockId, sideCellCount, cellIndex); public void OnLandscapeCellTurn(uint cellId) => _inner.OnLandscapeCellTurn(cellId); public void OnLandscapeCellTurn(uint landblockId, int sideCellCount, int cellIndex) => _inner.OnLandscapeCellTurn(landblockId, sideCellCount, cellIndex); public void OnBuildingTurn(WalkBuilding building) => _inner.OnBuildingTurn(building); public void OnBuildingShellTurn(WalkBuilding building) => _inner.OnBuildingShellTurn(building); public void OnPunchGeometry( WalkBuilding building, WalkPolygon polygon, int activeViewIndex) { // Far-Z punches never touch portalsDrawnCount (R2); this reads // it purely as a snapshot for the transcript, BEFORE the SAME // interior root's own OnInteriorFloodDrawTurn reset — see class // doc comment. Punches.Add((1, _driver.PortalsDrawnCount)); _inner.OnPunchGeometry(building, polygon, activeViewIndex); } public void OnInteriorFloodDrawTurn(IReadOnlyList cells, int outsideViewCount) => _inner.OnInteriorFloodDrawTurn(cells, outsideViewCount); public void OnWeatherTurn(uint viewerCellId) => _inner.OnWeatherTurn(viewerCellId); } /// Replay-time : every /// member besides is a plain no-op (this /// gate cares only about the depth/count mechanism, not mesh content). /// reproduces /// RetailPViewPassExecutor.DrawPortalDepthWrite's real per-portal /// loop — S4-c1 F1's guard, F2's count-before-clip order — directly over /// the walk's own flood/cell data (no fake GPU clip needed: F2 /// established the count happens unconditionally once the guard /// passes). private sealed class AlphaDepthLeaf : IWalkFrameLeafRenderer { private readonly Dictionary _cells; public AlphaDepthLeaf(Dictionary cells) => _cells = cells; /// Set once, immediately after construction — the leaf and /// its owning are mutually referential /// (the driver's constructor requires the leaf), so this cannot be a /// constructor parameter. public WalkFrameDriver Driver { get; set; } = null!; public readonly List<(int Mode, int CounterBefore)> Seals = new(); public void DrawSky() { } public void DrawLandCellBatch( IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { } public bool HasRenderableEmittersInCell(uint cellId) => false; public void DrawCellShell(uint cellId) { } public void DrawStaticParticles(uint cellId) { } public void DrawCellParticles(uint cellId) { } public void ClearInteriorDepth() { } public void FlushLandscape() { } public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { } public void AlphaBarrier() { } public int DrawExitSeals() { int accepted = 0; List flood = Driver.InteriorFloodCells; // Reverse order — the SAME direction DrawWalkExitPortalMasks // walks driver.InteriorFloodCells in production // (RetailPViewRenderer.cs). for (int i = flood.Count - 1; i >= 0; i--) { if (!_cells.TryGetValue(flood[i], out WalkCell? cell)) continue; int sliceCount = Driver.InteriorFloodViewSliceCountAt(i); for (int slice = 0; slice < sliceCount; slice++) { for (int p = 0; p < cell.Portals.Length; p++) { WalkCellPortal portal = cell.Portals[p]; // Walk-side exit sentinel is 32-bit 0xFFFFFFFF // (WalkCellPortal's own doc comment) — the render- // side CellPortalInfo's 16-bit 0xFFFF is a DIFFERENT // representation of the same fact. if (portal.OtherCellId != 0xFFFFFFFFu) continue; if ((uint)portal.PolygonIndex >= (uint)cell.PortalPolygons.Length) continue; WalkPolygon poly = cell.PortalPolygons[portal.PolygonIndex]; // F2: count happens BEFORE clip (unconditionally, // once the guard passes) — counterBefore is the // running total BEFORE this specific portal's own // decision. Seals.Add((0, Driver.PortalsDrawnCount + accepted)); if (!WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard( poly.Vertices)) { accepted++; } } } } return accepted; } } /// Minimal : this gate never /// inspects mesh content, only the depth/count event sequence, so every /// query returns the shared empty record. private sealed class EmptyAlphaDepthWorldData : IWalkFrameWorldData { public WalkFrameStaticRecords GetCellStatics(uint cellId) => WalkFrameStaticRecords.Empty; public WalkFrameStaticRecords GetCellDynamics(uint cellId) => WalkFrameStaticRecords.Empty; public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) => WalkFrameStaticRecords.Empty; public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) => WalkFrameStaticRecords.Empty; public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building) => WalkFrameStaticRecords.Empty; 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 /// 's captured frame 2 /// ('s frames[1], matching every /// other OH-capture row's own frame-2 convention in this file). private void RunAlphaDepthTranscriptGate(string fixtureName) { IReadOnlyList poseFrames = WalkOracleTrace.Load(OhCaptureRoot, fixtureName); Assert.NotEmpty(poseFrames); Assert.True(poseFrames.Count > 1, $"{fixtureName}: need a captured frame 2."); WalkOracleFrame poseFrame = poseFrames[1]; Assert.NotNull(poseFrame.Pose); WalkOraclePose pose = poseFrame.Pose!; 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]; using DatCollection dats = OpenDats(); WalkLandscapeDatBuilder.BuiltWorld world = WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin); var ctx = new WalkTraceReplayContext(pose, world.Cells) { Buildings = world.Buildings }; WalkCell? camera = (pose.CellId & 0xFFFFu) >= 0x100 ? Assert.Contains(pose.CellId, world.Cells) : null; using var fx = new WalkFrameDriverTests.DispatcherFixture(); var worldData = new EmptyAlphaDepthWorldData(); var leaf = new AlphaDepthLeaf(world.Cells); var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData); leaf.Driver = driver; var sink = new AlphaDepthCollectSink(driver); var walk = new RetailFrameWalk(); void RunOnePass() { 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(); } // 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(); var actualPm = new List<(int Mode, int CounterBefore)>(sink.Punches); actualPm.AddRange(leaf.Seals); Assert.Equal(expected.PmEvents, actualPm); Assert.Equal(expected.PcEvents, sink.PcEvents); } [Fact] public void AlphaDepthTranscript_CathedralArrival_MatchesRetailFrame2() => RunAlphaDepthTranscriptGate("cathedral-arrival.alphadepth"); [Fact] public void AlphaDepthTranscript_FoundryDeep_MatchesRetailFrame2() => RunAlphaDepthTranscriptGate("foundry-deep.alphadepth"); [Fact] public void AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2() => 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. /// [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. [Fact] public void AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2() => RunAlphaDepthTranscriptGate("cathedral-leak.alphadepth"); }