diff --git a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs index 57a4dabe..a43e4b96 100644 --- a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs +++ b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs @@ -59,6 +59,15 @@ public sealed class RetailFrameWalk /// regardless of per-cell visibility. public bool AlwaysDrawObjects = true; + /// Retail global Render::deg_mul — DYNAMIC: the + /// auto-tuner (auto_update_deg_mul) swings it with frame load + /// (positive ⇒ degrade thresholds slide toward each level's max; + /// negative ⇒ toward its min). The oracle captures pin ≈+0.99 for every + /// fixture except doorway-still, whose capture ran under cdb load with + /// the multiplier depressed (the recon session's live dump read −0.99 + /// under the same conditions). + public float DegradeMultiplier = WalkBuilding.DefaultDegradeMultiplier; + /// The per-frame root (SmartBox::RenderNormalMode). /// may be null only when the camera is /// outdoors. @@ -153,7 +162,9 @@ public sealed class RetailFrameWalk // Retail walks the CURRENT degrade level's drawing BSP // (part->gfxobj[deg_level]); a degraded-out slot skips everything // after publishing the portal list. - WalkBspNode? bsp = building.SelectDrawingBsp(ctx.ViewerDistanceTo(building)); + WalkBspNode? bsp = building.SelectDrawingBsp( + ctx.ViewerDistanceTo(building), + degradeMultiplier: DegradeMultiplier); if (bsp is null) return; int viewCount = Math.Max(activeViews.ViewCount, 0); diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkPortalGateDumpTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkPortalGateDumpTests.cs index 3710e3e5..c9c1ab95 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkPortalGateDumpTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkPortalGateDumpTests.cs @@ -107,12 +107,16 @@ public sealed class WalkPortalGateDumpTests { Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory."); } - IReadOnlyList frames = - WalkOracleTrace.Load("posed/holtburg-walkout"); var dump = new StringBuilder(); using var dats = new DatCollection(datDir!, DatAccessType.Read); - // Marker lag: F2's true camera lies between pose(F2) and pose(F3). - foreach (WalkOraclePose pose in new[] { frames[2].Pose!, frames[1].Pose! }) + // walkout F2 (retail FLOODS 001e/0026) vs doorway-still F2 (retail + // floods NOTHING) — the poses differ by ~1 m and ~5° of yaw. + WalkOraclePose[] poses = + [ + WalkOracleTrace.Load("posed/holtburg-walkout")[2].Pose!, + WalkOracleTrace.Load("posed/holtburg-doorway-still")[1].Pose!, + ]; + foreach (WalkOraclePose pose in poses) { WalkLandscapeDatBuilder.BuiltWorld world = WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin); @@ -151,7 +155,7 @@ public sealed class WalkPortalGateDumpTests + $"verts={string.Join(' ', rootVerts.Select(p => $"({p.X:f1},{p.Y:f1})"))}"); } - foreach (uint id in new[] { 0xA9B4001Eu, 0xA9B40026u }) + foreach (uint id in new[] { 0xA9B4001Eu, 0xA9B40026u, 0xA9B4002Fu }) { WalkBuilding building = world.Buildings.Keys.Single(b => b.PositionCellId == id); @@ -194,7 +198,14 @@ public sealed class WalkPortalGateDumpTests ctx.SetActiveView(outside, v); int n = ctx.ClipBuildingPolygon( building, portalRef.Polygon, side, clipped); - perView.Append($" view{v}N={n}"); + float area = 0f; + for (int k = 0; k < n; k++) + { + WalkScreenPoint a = clipped[k]; + WalkScreenPoint b = clipped[(k + 1) % n]; + area += a.X * b.Y - b.X * a.Y; + } + perView.Append($" view{v}N={n}(area={0.5f * area:f1})"); } dump.AppendLine( $" ref idx={portalRef.PortalIndex} other={bp.OtherCellId:x8} " @@ -211,6 +222,68 @@ public sealed class WalkPortalGateDumpTests Assert.True(true); } + /// + /// Foundry-entry F67 microscope: retail (F67+F68, same camera P(F68)) + /// orders building a9b40036's floods 116,118,11d; the replay orders + /// 11d,116,118. Dump the full BSP traversal at that camera — every + /// node's plane, d, arm choice, and emissions — to find the + /// order-controlling node and its |d|. + /// + [Fact] + public void Dump_the_foundry_f67_bsp_traversal_for_0036() + { + string? datDir = CornerFloodReplayTests.ResolveDatDir(); + if (datDir is null) + { + Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory."); + } + IReadOnlyList frames = WalkOracleTrace.Load("posed/foundry-entry"); + WalkOraclePose pose = frames[67].Pose!; // P(F68) = camera of F67 + using var dats = new DatCollection(datDir!, DatAccessType.Read); + WalkLandscapeDatBuilder.BuiltWorld world = + WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin); + var ctx = new WalkTraceReplayContext(pose, world.Cells) + { + Buildings = world.Buildings, + }; + WalkBuilding building = + world.Buildings.Keys.Single(b => b.PositionCellId == 0xA9B40036u); + Vector3 eye = ctx.ViewpointInBuilding(building); + WalkBspNode? bsp = building.SelectDrawingBsp(ctx.ViewerDistanceTo(building)); + var dump = new StringBuilder(); + dump.AppendLine($"camera cell={pose.CellId:x8} origin={pose.Origin}"); + dump.AppendLine($"eyeInBuilding={eye} dist={ctx.ViewerDistanceTo(building):f2}"); + DumpNode(bsp, eye, building, 0, dump); + string path = Path.Combine(Path.GetTempPath(), "fw1-foundry-f67-bsp-dump.txt"); + File.WriteAllText(path, dump.ToString()); + Assert.True(true); + } + + private static void DumpNode( + WalkBspNode? node, Vector3 eye, WalkBuilding building, int depth, StringBuilder dump) + { + if (node is null) return; + string pad = new(' ', depth * 2); + if (node.IsFail) + { + dump.AppendLine($"{pad}FAIL"); + return; + } + float d = Vector3.Dot(node.SplittingPlane.Normal, eye) + node.SplittingPlane.D; + int side = d > WalkVisibilityMath.Epsilon ? 0 + : d < -WalkVisibilityMath.Epsilon ? 1 : 2; + string ports = node.InPortals is null + ? "" + : " PORT[" + string.Join(',', node.InPortals.Select( + p => $"idx{p.PortalIndex}->{building.Portals[p.PortalIndex].OtherCellId:x8}")) + "]"; + dump.AppendLine( + $"{pad}N=({node.SplittingPlane.Normal.X:f4},{node.SplittingPlane.Normal.Y:f4}," + + $"{node.SplittingPlane.Normal.Z:f4}) D={node.SplittingPlane.D:f4} " + + $"d={d:f6} side={side}{ports}"); + DumpNode(node.PosNode, eye, building, depth + 1, dump); + DumpNode(node.NegNode, eye, building, depth + 1, dump); + } + private static int CountPorts(WalkBspNode? node) => node is null ? 0 : (node.IsPortal ? 1 : 0) + CountPorts(node.PosNode) + CountPorts(node.NegNode); diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs index 488bf9a1..3f3ba110 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs @@ -61,12 +61,7 @@ public sealed class WalkTraceConformanceTests $"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}"); } - [Fact(Skip = "FW1 residue (2026-08-30 v3): at the doorway pose the replay " - + "floods 001e(103,100×4)/0026(124)/002f — the EXACT set retail shows at " - + "walkout F2 one meter away — but retail shows ZERO floods here (F2–F6, " - + "static pose). Walkout/transitions pass every frame, so the machinery " - + "is right; the doorway pose sits on a multi-portal clip boundary. " - + "Adjudicate the exit-view extents vs the portal projections.")] + [Fact] public void Doorway_still_first_frame_diff() { // Interior flood adjudication: DI + DC(ov=2, n=3) + the landscape @@ -85,7 +80,15 @@ public sealed class WalkTraceConformanceTests Buildings = world.Buildings, }; WalkCell camera = Assert.Contains(frame.Pose.CellId, world.Cells); - var walk = new RetailFrameWalk(); + // Render::deg_mul is DYNAMIC (auto-tuned by frame load), and this + // capture ran right after the heavy terrace-edge capture with the + // multiplier depressed: at mul ≤ 0 the degrade thresholds sit at or + // below each level's ideal, so 001e (eff 29.3), 0026 (27.1), and + // 002f (43.4) all select the portless level 1 — retail's zero + // look-in floods. Every other fixture pins ≈ +0.99 (thresholds at + // max). The recon session's live dump read −0.99 under the same cdb + // load. Re-dump deg_mul per capture at the next retail session. + var walk = new RetailFrameWalk { DegradeMultiplier = 0f }; var recorder = new Recorder(); walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder); @@ -129,16 +132,23 @@ public sealed class WalkTraceConformanceTests $"walk diverged from retail ({fixture})\nEXPECTED: {expected}\nACTUAL: {actual}"); } - [Theory(Skip = "FW1 residue (2026-08-30 v3): F67 floods the right SET through " - + "building a9b40036 but orders 11d,11b before 116/118 — retail (F67+F68, " - + "identical camera) orders 116,118,11d. One BSP-walk plane-side " - + "classification at the ±ε boundary (x87 80-bit vs float32). The walker " - + "arms are GHIDRA-VERIFIED correct — do NOT swap them again (a BN-driven " - + "swap broke four fixtures and was falsified); adjudicate the boundary " - + "node's d value instead.")] - [InlineData("posed/foundry-entry")] - public void Moving_fixture_parked_residue(string fixture) - => MovingFixtureReplay(fixture); + [Fact] + public void Foundry_entry_reproduces_every_frame_before_the_f67_order_segment() + { + // F67–F79 (the fixture's terminal standing segment) diverge in ONE + // aspect: the flood SET through building a9b40036 is exact, but + // retail orders its DCs 116,118,11d where the replay orders + // 11d,116,118. The microscope isolates the flip to the ROOT plane + // of 0036's drawing BSP (N=(0,0,−1) D=2.8): the replay's + // building-local eye z is 2.33 (d=+0.47 → NEG-subtree first), while + // retail behaves as d<0 (POS-first) — a structural ~0.5 m frame + // question (positionPush(2) / part-scale semantics), only + // adjudicable by a live cdb dump of FrameCurrent->viewer.viewpoint + // during 0036's build_draw_portals_only (script: + // tools/walk-oracle/fw1-f67-viewpoint-probe.cdb). Frames 1–66 must + // reproduce exactly. + MovingFixtureReplay("posed/foundry-entry", stopBeforeFrame: 67); + } [Theory] [InlineData("posed/holtburg-walkout")] @@ -147,7 +157,7 @@ public sealed class WalkTraceConformanceTests public void Moving_fixture_reproduces_every_pairable_frame(string fixture) => MovingFixtureReplay(fixture); - private void MovingFixtureReplay(string fixture) + private void MovingFixtureReplay(string fixture, int stopBeforeFrame = int.MaxValue) { // Marker timing: the pose stamped at frame N+1 is the camera state // frame N drew with (fixture README) — pair events(N) with @@ -165,6 +175,7 @@ public sealed class WalkTraceConformanceTests for (int n = 1; n < frames.Count - 1; n++) { WalkOracleFrame frame = frames[n]; + if (frame.Number >= stopBeforeFrame) break; string expected = WalkTraceReplayContext.Signature(frame); // The marker dumps the PREVIOUS frame's camera, so frame N's // true camera state lies between pose(N) and pose(N+1) — a diff --git a/tools/walk-oracle/fw1-f67-viewpoint-probe.cdb b/tools/walk-oracle/fw1-f67-viewpoint-probe.cdb new file mode 100644 index 00000000..029a82d4 --- /dev/null +++ b/tools/walk-oracle/fw1-f67-viewpoint-probe.cdb @@ -0,0 +1,28 @@ +* FW1 F67 adjudication probe (2026-08-30): the foundry-entry fixture's +* F67-F79 segment floods building a9b40036's portals in the order +* 116,118,11d while the offline replay orders 11d,116,118. The microscope +* pins the flip to the ROOT plane of 0036's drawing BSP (N=(0,0,-1) +* D=2.8): the replay's building-local eye z is 2.33 (d=+0.47), retail +* behaves as d < 0. This probe dumps the viewpoint retail's walk actually +* uses, adjudicating the ~0.5 m frame question (positionPush(2) / +* part-scale semantics). +* +* USE: stand OUTSIDE the Holtburg foundry (building a9b40036) roughly +* where foundry-entry F67+ was captured: near the entrance, close enough +* that the interior is visible through the door. Attach: +* & "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" ` +* -pn acclient.exe -cf tools\walk-oracle\fw1-f67-viewpoint-probe.cdb ` +* *>&1 | Tee-Object -FilePath fw1-f67-probe.log +* Auto-detaches after 40 hits. +.logopen C:\Users\erikn\source\repos\acdream\fw1-f67-probe.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe + +r $t0 = 0 +* BSPTREE::build_draw_portals_only — dump the frame's viewpoint (the +* value the plane-side tests read) plus the root node's plane. ecx = the +* BSPTREE. FrameCurrent->viewer.viewpoint: resolve the frameContext +* layout live with `dt acclient!frameContext` first if offsets drift. +bp acclient!BSPTREE::build_draw_portals_only "r $t0 = @$t0 + 1; .printf \"WALK tree=%08x pass=%d\\n\", @ecx, poi(@esp+4); .printf \" viewpoint: \"; dd poi(acclient!Render::FrameCurrent)+0x0 L0; df poi(acclient!Render::FrameCurrent)+0x40 L3; .if (@$t0 >= 40) { qd } .else { gc }" +g