using System.Numerics; using AcDream.App.Rendering.Walk; namespace AcDream.App.Tests.Rendering.Walk; /// /// The FW1 conformance replay harness: reconstructs the camera state from a /// pose-stamped oracle frame and drives the ported walk over /// adapter-built world data. Convention notes (adjudicate against the /// fixtures, loudly, on any mismatch): /// /// The dumped quaternion is retail Frame storage order w,x,y,z /// (q0=w) — unit-norm verified on the captures. /// Retail's frame axes: +Y forward, +Z up (the camera looks along /// the rotated +Y). /// Pose origin is landblock-local, the same space the adapter's /// cell transforms produce. /// /// public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalkContext { // Retail projection globals, dumped live from the capture client // (recon 2026-08-30 evening: Render::bw/bh/xinvscale/yinvscale/tx/ty/vdst). public const float RetailViewportWidth = 1024f; public const float RetailViewportHeight = 720f; public const float RetailXInvScale = 0.00025f; public const float RetailYInvScale = 0.00025f; public const float RetailTx = 0.127875f; public const float RetailTy = 0.089875f; public const float RetailVdst = 0.1330766976f; private sealed class RetailRayCaster( Vector3 right, Vector3 forward, Vector3 up) : IWalkRayCaster { // Retail's unproject (copy_view's ray path; equal to // ScreenToViewTransform for these globals): // u = sx·xinvscale − tx; w = sy·yinvscale − ty // ray = Xaxis·u + Yaxis·vdst − Zaxis·w public Vector3 RayThrough(float screenX, float screenY) { float u = screenX * RetailXInvScale - RetailTx; float w = screenY * RetailYInvScale - RetailTy; return right * u + forward * RetailVdst - up * w; } } private readonly Dictionary _cells; private readonly Matrix4x4 _viewProjection; private readonly IWalkRayCaster _rays; public WalkTraceReplayContext(WalkOraclePose pose, Dictionary cells) { _cells = cells; // S3 review fix round 1 (F4b): the SAME cell id the harness hands // WalkFrame's own cameraCellId (every call site in // WalkTraceConformanceTests passes pose.CellId) — production wires // the identical invariant (RetailPViewFrameInput.ViewerCellId == // WalkFrame's cameraCellId, see WalkFrameDriverTranscriptTests' // own citation). Needed so a trailing weather "OC" this context's // WeatherGateOpen now legitimately opens carries the SAME cell id // retail's own capture recorded. ViewerCellId = pose.CellId; WorldViewpoint = pose.Origin; var rotation = new Quaternion(pose.Q1, pose.Q2, pose.Q3, pose.Q0); // Basis convention RE-pinned 2026-08-30 (second pass): storage // w,x,y,z; forward = rotated +Y — the retail Frame convention. The // motion sweep briefly favored +X, but the walkabout camera was // mouse-turned off the run direction; the terrace-edge fixture's // EXTERNAL ground truth (the ledge faces the F518 vista, east) // decodes east ONLY under +Y-forward, and +Y makes the street // fixture's punch on/off-screen pattern match retail 4-for-4. Vector3 forward = Vector3.Transform(Vector3.UnitY, rotation); Vector3 up = Vector3.Transform(Vector3.UnitZ, rotation); Vector3 right = Vector3.Transform(Vector3.UnitX, rotation); // The exact retail frustum: tan(halfFovY) = ty/vdst, aspect = tx/ty. float fovY = 2f * MathF.Atan(RetailTy / RetailVdst); Matrix4x4 view = Matrix4x4.CreateLookAt(pose.Origin, pose.Origin + forward, up); Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView( fovY, RetailTx / RetailTy, 0.1f, 5000f); _viewProjection = view * projection; ViewportWidth = RetailViewportWidth; ViewportHeight = RetailViewportHeight; _rays = new RetailRayCaster(right, forward, up); // The retail CY near plane: N = forward, d = −dot(eye, forward) − znear. CyPlane = new WalkPlane(forward, -Vector3.Dot(pose.Origin, forward) - 0.1f); } /// Building placements (camera-block-local) for the landscape /// fixtures; empty for the interior-only ones. public Dictionary Buildings { get; set; } = new(); private Vector2[] _activeViewVerts = new Vector2[32]; private int _activeViewVertCount; public Vector3 ViewpointIn(WalkCell cell) => Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform); public Matrix4x4 ObjectToClip(WalkCell cell) => cell.WorldTransform * _viewProjection; public WalkCell? GetVisible(uint cellId) => _cells.GetValueOrDefault(cellId); public IWalkRayCaster Rays => _rays; public Vector3 WorldViewpoint { get; } public float ViewportWidth { get; } public float ViewportHeight { get; } public WalkPlane CyPlane { get; } public IWalkFrameContext CellContext => this; /// S3 review fix round 1 (F4b): overrides /// 's default 0 — see the /// constructor's own doc comment for why this equals the harness's own /// WalkFrame cameraCellId argument. public uint ViewerCellId { get; } /// S3 review fix round 1 (F4b): overrides /// 's default false with retail's /// own gate — SmartBox::is_player_outside @0x00451e80, /// (cellId & 0xFFFF) < 0x100 — so an outdoor-rooted /// fixture's trailing weather turn fires during replay exactly where /// retail's own capture recorded it, and an interior-rooted fixture's /// never does (retail's own gate is unconditionally false whenever the /// viewer's cell id has local part >= 0x100, regardless of whether an /// exit view survives). This decomp-port context has no App-level /// render-toggle concept to AND against — see the interface member's own /// doc comment for why that is safe here (a pure walk conformance /// harness assumes both toggles on, matching retail's default). public bool WeatherGateOpen => (ViewerCellId & 0xFFFFu) < 0x100u; public void SetActiveView(WalkPortalView views, int index) { WalkViewPoly poly = views.View.Polys[index]; if (_activeViewVerts.Length < poly.VertexCount) _activeViewVerts = new Vector2[poly.VertexCount]; for (int k = 0; k < poly.VertexCount; k++) _activeViewVerts[k] = views.View.Vertices[poly.VertexIndex + k].Point; _activeViewVertCount = poly.VertexCount; } public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Transform(WorldViewpoint, Buildings[building].InverseWorldTransform); public float ViewerDistanceTo(WalkBuilding building) => Vector3.Distance( WorldViewpoint, Vector3.Transform(building.SortCenter, Buildings[building].WorldTransform)); public int ClipBuildingPolygon( WalkBuilding building, WalkPolygon polygon, int side, Span output) { Matrix4x4 objectToClip = Buildings[building].WorldTransform * _viewProjection; Span projected = stackalloc WalkScreenPoint[polygon.Vertices.Length]; for (int i = 0; i < polygon.Vertices.Length; i++) projected[i] = WalkScreenClip.TransformToScreen( polygon.Vertices[i], objectToClip, ViewportWidth, ViewportHeight); if (side != 0) projected.Reverse(); return WalkScreenClip.ClipAgainstView( projected, _activeViewVerts.AsSpan(0, _activeViewVertCount), output); } // ---- signatures for comparing walk output to oracle frames ---- public static string Signature(IEnumerable events) => string.Join("|", events.Select(e => e.Kind switch { WalkEventKind.Landscape => "LS", WalkEventKind.Building => $"BLD:{e.CellId:x8}", WalkEventKind.DrawInside => $"DI:{e.CellId:x8}", WalkEventKind.DrawCells => $"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}", _ => "?", })); /// /// S3 chunk 1 (§11.2 B3): the OH captures interleave LC/SC/ /// EC/OC lines the pre-chunk-3 FW0 fixtures never had. /// 's own WalkEvent vocabulary has /// exactly four kinds (Landscape/Building/DrawInside/DrawCells) — LC/SC/ /// EC/OC are separate hooks /// never overrides, so the replay side of a /// signature diff is silent on them by construction. Filtering them out /// here (rather than mapping to a "?" placeholder) keeps this /// comparison at the SAME DI/DC/BLD/LS level on both sides — S3's own /// scope note ("no speculative pins" for LC/SC/EC/OC content) means this /// method must not even attempt to compare them, not merely fail to. /// public static string Signature(WalkOracleFrame frame) => string.Join("|", frame.Events .Where(e => e.Kind is WalkOracleEventKind.Landscape or WalkOracleEventKind.Building or WalkOracleEventKind.DrawInside or WalkOracleEventKind.DrawCells) .Select(e => e.Kind switch { WalkOracleEventKind.Landscape => "LS", WalkOracleEventKind.Building => $"BLD:{e.CellId!.Value:x8}", WalkOracleEventKind.DrawInside => $"DI:{e.CellId!.Value:x8}", WalkOracleEventKind.DrawCells => $"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}", _ => "?", })); /// /// S3 chunk 1 fix round 1 (G10): the OH kit-pose captures' full /// eight-kind vocabulary — LS/BLD/DI/DC/LC/SC/EC/OC. ONLY the OH-rooted /// conformance rows use this (WalkTraceConformanceTests' /// OhCaptureRoot rows) — the older FW0 fixtures /// (docs/research/2026-08-30-fw-walk-oracle/) predate the LC/SC/EC/OC /// cdb breakpoints and carry none of those lines at all, so comparing /// them at this level would spuriously diverge on every frame (the /// replay always emits LC/SC; the FW0 oracle frame never has any). /// /// S3 chunk 1 fix round 2 (§11.6 H3): the two sides of this comparison /// are DELIBERATELY ASYMMETRIC. LC/SC/EC/OC all come straight from /// 's own literal per-hook calls — the REPLAY /// side's real turns at their real positions, EC/OC included ('s Recorder derives them via /// at the exact hook that fires for each /// flood — see that type's own doc comment). below, the ORACLE side, used to /// derive EC/OC the SAME way from each DC's declared cell list — but /// that derivation can never disagree with itself, so it silently /// verified nothing about EC/OC placement or content (round 1's own /// blind spot: G7's SC-ordering regression shipped green for the same /// reason with LC/SC). Round 2 fixes this: the oracle side now reads its /// own CAPTURED EC/OC lines verbatim, in the order retail's /// cdb breakpoints actually recorded them — a real comparison against a /// real trace, not a derivation compared to itself. /// /// /// S3 review fix round 1 (F4b): round 2's own exclusion of the trailing /// per-frame weather OC is GONE — 's Recorder now implements /// IWalkEventSink.OnWeatherTurn (the interface's default was a /// silent no-op), so the replay side records that exact "OC" line too, /// at the exact point RetailFrameWalk.DrawLandscape fires it. Both /// sides now carry the weather turn's own trailing OC literally — the /// eight-kind signature pins its placement and value like every other /// line, rather than the two sides mutually agreeing to stay silent /// about it. /// /// public static string Signature8(IReadOnlyList tokens) => string.Join("|", tokens); /// The oracle-frame half of — /// see that overload's doc comment for why this reads EC/OC literally /// rather than deriving them. public static string Signature8(WalkOracleFrame frame) { var tokens = new List(); IReadOnlyList events = frame.Events; for (int i = 0; i < events.Count; i++) { WalkOracleEvent e = events[i]; switch (e.Kind) { case WalkOracleEventKind.Landscape: tokens.Add("LS"); break; case WalkOracleEventKind.Building: tokens.Add($"BLD:{e.CellId!.Value:x8}"); break; case WalkOracleEventKind.DrawInside: tokens.Add($"DI:{e.CellId!.Value:x8}"); break; case WalkOracleEventKind.DrawCells: tokens.Add( $"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}"); break; case WalkOracleEventKind.LandCell: tokens.Add($"LC:{e.CellId!.Value:x8}"); break; case WalkOracleEventKind.SortCell: tokens.Add($"SC:{e.CellId!.Value:x8}"); break; case WalkOracleEventKind.EnvCellShell: tokens.Add($"EC:{e.CellId!.Value:x8}"); break; case WalkOracleEventKind.ObjectCellTurn: // S3 review fix round 1 (F4b): every ObjectCellTurn now // reads literally, trailing weather OC included — see // this method's own class doc for why the former // position/value exclusion is gone (the replay side now // records the same line through // WalkTraceConformanceTests.Recorder.OnWeatherTurn). tokens.Add($"OC:{e.CellId!.Value:x8}"); break; } } return string.Join("|", tokens); } /// PView::DrawCells's two complete reverse loops: every /// EnvCell shell far-to-near, THEN every object-list turn far-to-near — /// the SAME reversed cell order twice (not one reversed EC/OC pair per /// cell). S3 chunk 1 fix round 2 (§11.6 H3): this is now ONLY the /// REPLAY side's derivation — 's /// Recorder calls this at the exact hook that fires for each /// flood (a look-in's own DrawCells immediately; the interior /// root's own flood at OnInteriorFloodDrawTurn), matching /// RetailFrameWalk's real two-reverse-loop order /// (WalkFrameDriver.EmitFloodTurns's own comment has the same /// citation). no longer calls /// this — the ORACLE side reads its own captured EC/OC lines literally /// instead of re-deriving them (see that method's own doc comment for /// why: a derivation compared to itself proves nothing). internal static void AppendFloodTurns(List tokens, IReadOnlyList cells) { for (int i = cells.Count - 1; i >= 0; i--) tokens.Add($"EC:{cells[i]:x8}"); for (int i = cells.Count - 1; i >= 0; i--) tokens.Add($"OC:{cells[i]:x8}"); } }