From 1d47ede007ead510092a63ded365d4f736713d1b Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 31 May 2026 10:44:37 +0200 Subject: [PATCH] =?UTF-8?q?diag(render):=20Phase=20U.4c=20=E2=80=94=20ACDR?= =?UTF-8?q?EAM=5FPROBE=5FFLAP=20per-frame=20convergence=20probe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-frame (not cell-change-throttled, so it catches the flicker at a stable root): [flap] line from the builder — root cell's per-portal side-test D + traverse/cull + NDC projection, plus OutsideView poly count + visible-cell count; localEye exposes when the eye has crossed an interior portal plane. Paired [flap-cam] line from the draw site — FindCameraCell resolution branch (CameraCellResolution enum, new), eyeInRoot AABB flag (stale-root signal), eye + player worldpos, and the frame's TerrainMode/OutdoorVisible outcome. Disambiguates side-cull vs empty-projection vs stale-root. Inert when off (gated). Throwaway apparatus to converge the flap fix. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/CellVisibility.cs | 34 +++++++++++++ src/AcDream.App/Rendering/GameWindow.cs | 16 ++++++ .../Rendering/PortalVisibilityBuilder.cs | 50 +++++++++++++++++++ .../Rendering/RenderingDiagnostics.cs | 18 +++++++ 4 files changed, 118 insertions(+) diff --git a/src/AcDream.App/Rendering/CellVisibility.cs b/src/AcDream.App/Rendering/CellVisibility.cs index 84638cf..7dde694 100644 --- a/src/AcDream.App/Rendering/CellVisibility.cs +++ b/src/AcDream.App/Rendering/CellVisibility.cs @@ -142,6 +142,25 @@ public struct PortalClipPlane public int InsideSide; } +/// +/// Phase U.4c flap probe (diagnostic): which branch of +/// resolved the camera cell. +/// +public enum CameraCellResolution +{ + /// No cell contains the eye (outdoors), or not yet resolved. + None, + /// The eye is inside the previously-cached cell (fast path). + Cache, + /// The eye is inside a one-hop portal neighbour of the cached cell. + Neighbour, + /// The eye is inside a cell found by the full brute-force scan. + BruteForce, + /// The eye is inside NO cell, but the previous cell is kept alive for a + /// few grace frames — the "stale root" case the flap probe watches for. + Grace, +} + /// /// Result of a portal-based visibility BFS from the camera cell. /// @@ -213,6 +232,14 @@ public sealed class CellVisibility /// The last visibility result produced by . public VisibilityResult? LastVisibilityResult { get; private set; } + /// + /// Phase U.4c flap probe (diagnostic): which branch + /// resolved the camera cell on the most recent call. A + /// (or ) result while the eye is NOT actually inside the + /// returned cell is the "stale root" signature the flap probe looks for. + /// + public CameraCellResolution LastCameraCellResolution { get; private set; } = CameraCellResolution.None; + // ------------------------------------------------------------------ // Registration // ------------------------------------------------------------------ @@ -330,7 +357,10 @@ public sealed class CellVisibility { // 1. Fast path: cached cell. if (_lastCameraCell != null && PointInCell(cameraPos, _lastCameraCell)) + { + LastCameraCellResolution = CameraCellResolution.Cache; return _lastCameraCell; + } // 2. One-hop neighbours of the cached cell. if (_lastCameraCell != null) @@ -347,6 +377,7 @@ public sealed class CellVisibility { _lastCameraCell = neighbour; _cellSwitchGraceFrames = CellSwitchGraceFrameCount; + LastCameraCellResolution = CameraCellResolution.Neighbour; return neighbour; } } @@ -361,6 +392,7 @@ public sealed class CellVisibility { _lastCameraCell = cell; _cellSwitchGraceFrames = CellSwitchGraceFrameCount; + LastCameraCellResolution = CameraCellResolution.BruteForce; return cell; } } @@ -370,11 +402,13 @@ public sealed class CellVisibility if (_lastCameraCell != null && _cellSwitchGraceFrames > 0) { _cellSwitchGraceFrames--; + LastCameraCellResolution = CameraCellResolution.Grace; return _lastCameraCell; } // 5. Camera is outside all cells. _lastCameraCell = null; + LastCameraCellResolution = CameraCellResolution.None; return null; } diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index cf84efe..8cc0ce6 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7315,6 +7315,22 @@ public sealed class GameWindow : IDisposable clipAssembly.OutsidePlaneCount, clipAssembly.PerCellPlaneCounts, clipAssembly.ScissorFallbacks); + + // Phase U.4c flap probe (ACDREAM_PROBE_FLAP) — paired with the builder's + // per-frame [flap] line. res = which FindCameraCell branch chose the root; + // eyeInRoot = is the EYE actually inside clipRoot's AABB (n ⇒ stale root via + // cache/grace, the leading flap hypothesis); terrain/outVisible = the frame's + // outcome (Skip/false ⇒ terrain+shells flapped off this frame). + if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled) + { + var flapPlayer = _playerController?.Position ?? camPos; + bool eyeInRoot = CellVisibility.PointInCell(camPos, clipRoot); + Console.WriteLine( + $"[flap-cam] root=0x{clipRoot.CellId:X8} res={_cellVisibility.LastCameraCellResolution} " + + $"eyeInRoot={(eyeInRoot ? "Y" : "n")} eye=({camPos.X:F2},{camPos.Y:F2},{camPos.Z:F2}) " + + $"player=({flapPlayer.X:F2},{flapPlayer.Y:F2},{flapPlayer.Z:F2}) " + + $"terrain={clipAssembly.TerrainMode} outVisible={clipAssembly.OutdoorVisible}"); + } } else { diff --git a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs index 5276196..e7f47d1 100644 --- a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs +++ b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs @@ -230,9 +230,59 @@ public static class PortalVisibilityBuilder if (pvDump) Console.WriteLine($"[pv-dump] OUTSIDEVIEW polys={frame.OutsideView.Polygons.Count} bfsCellViews={frame.CellViews.Count} crossBldg={frame.CrossBuildingViews.Count}"); + // Phase U.4c flap probe (ACDREAM_PROBE_FLAP) — read-only per-frame snapshot of the + // root cell's per-portal side-test + projection + the frame's exit/visible counts. + if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled) + EmitFlapProbe(cameraCell, cameraPos, viewProj, frame); + return frame; } + // Phase U.4c flap probe. One [flap] line per Build: the root cell's per-portal + // signed distance D (eye→portal plane), traverse/cull decision, and NDC projection + // vertex count, plus the frame's OutsideView polygon count + visible-cell count. + // `localEye` is the eye in root-local space — its component along an interior portal + // plane reveals when the eye has crossed past that plane (the stale-root region that + // makes the side test cull a still-needed portal). Read-only recompute; no effect on + // the returned frame. Throwaway apparatus — strip with the probe. + private static void EmitFlapProbe( + LoadedCell cameraCell, Vector3 cameraPos, Matrix4x4 viewProj, PortalVisibilityFrame frame) + { + var localEye = Vector3.Transform(cameraPos, cameraCell.InverseWorldTransform); + var sb = new System.Text.StringBuilder(220); + sb.Append("[flap] root=0x").Append(cameraCell.CellId.ToString("X8")); + sb.Append(" eye=(").Append(cameraPos.X.ToString("F2")).Append(',') + .Append(cameraPos.Y.ToString("F2")).Append(',').Append(cameraPos.Z.ToString("F2")).Append(')'); + sb.Append(" localEye=(").Append(localEye.X.ToString("F2")).Append(',') + .Append(localEye.Y.ToString("F2")).Append(',').Append(localEye.Z.ToString("F2")).Append(')'); + for (int i = 0; i < cameraCell.Portals.Count; i++) + { + var portal = cameraCell.Portals[i]; + float d = float.NaN; + bool side = true; + if (i < cameraCell.ClipPlanes.Count && cameraCell.ClipPlanes[i].Normal.LengthSquared() >= 1e-8f) + { + var pl = cameraCell.ClipPlanes[i]; + d = Vector3.Dot(pl.Normal, localEye) + pl.D; + side = CameraOnInteriorSide(cameraCell, i, cameraPos); + } + int projN = -1; + if (i < cameraCell.PortalPolygons.Count) + { + var poly = cameraCell.PortalPolygons[i]; + if (poly != null && poly.Length >= 3) + projN = PortalProjection.ProjectToNdc(poly, cameraCell.WorldTransform, viewProj).Length; + } + sb.Append(" | p").Append(i).Append("->0x").Append(portal.OtherCellId.ToString("X4")); + sb.Append(" D=").Append(float.IsNaN(d) ? "na" : d.ToString("F2")); + sb.Append(side ? " TRV" : " CULL"); + sb.Append(" proj=").Append(projN); + } + sb.Append(" || outPolys=").Append(frame.OutsideView.Polygons.Count); + sb.Append(" vis=").Append(frame.OrderedVisibleCells.Count); + Console.WriteLine(sb.ToString()); + } + // Mirrors CellVisibility's portal-side test (InsideSide convention). private static bool CameraOnInteriorSide(LoadedCell cell, int portalIndex, Vector3 cameraPos) { diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs index b373aac..14687b9 100644 --- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs +++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs @@ -96,6 +96,24 @@ public static class RenderingDiagnostics public static bool ProbeVisibilityEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1"; + /// + /// Phase U.4c (2026-05-31) flap-convergence probe. When true, the portal + /// visibility pass emits, EVERY frame the camera root is an indoor cell, a + /// [flap] line (root cell's per-portal side-test D + traverse/cull + + /// projection, plus the frame's OutsideView/visible counts) and the call site + /// emits a paired [flap-cam] line (FindCameraCell resolution reason, + /// camera EYE worldpos, player worldpos, eye-in-root-AABB flag). Unlike the + /// cell-change-throttled probe, this fires + /// per-frame so it captures the flicker (the exit cell dropping in/out at a + /// STABLE root). Pinpoints WHY the exit cell drops: side-test cull (eye past an + /// interior portal plane), empty projection, or a stale root (eye outside the + /// cell while FindCameraCell still reports it via cache/grace). Throwaway + /// apparatus — strip once the flap mechanism is confirmed. + /// Initial state from ACDREAM_PROBE_FLAP=1. + /// + public static bool ProbeFlapEnabled { get; set; } = + Environment.GetEnvironmentVariable("ACDREAM_PROBE_FLAP") == "1"; + // Cell-change gate for EmitVis. The probe fires once per distinct root cell // so launch.log stays readable under motion (the per-frame call is a no-op // when the root is unchanged). Sentinel 0 = "no root yet" — the first real