From 878533597d58af504ffbaf2173a701e998f5e69a Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 30 Aug 2026 14:47:15 +0200 Subject: [PATCH] feat(render) Campaign FW3.2b-2: the walk shadow probe (I5 pattern) ACDREAM_PROBE_WALK_SHADOW=1 (documented row; throwaway - dies with the flip commit) runs the PRODUCTION retail frame walk per frame in shadow over the FW3.1 registries - WalkProductionFrameContext from the live camera, SetViewer recentring, interior rooting via LoadedCell.Walk - and prints one [walk-shadow] divergence line per frame whose visited cell set differs from the old path (main flood + look-ins), plus a loud PROBE FAULT line on any exception. No draws change. This is the I5 dual-shadow pattern applied to the FW3 static cutover: it proves the production walk world data live and quantifies old-vs-walk divergence before any pixel moves. CellVisibility joins the renderer plumbing as the walk cell registry. Suites: full Release build 0 warnings; hermetic 6,753/0; LaunchOptionsDocumentationTests green (both directions). Co-Authored-By: Claude Fable 5 --- docs/launch-options.md | 1 + .../Composition/FrameRootComposition.cs | 3 +- .../Rendering/RetailPViewRenderer.cs | 119 +++++++++++++++++- .../Rendering/RenderingDiagnostics.cs | 11 ++ 4 files changed, 131 insertions(+), 3 deletions(-) diff --git a/docs/launch-options.md b/docs/launch-options.md index 9decd203..7037b2a2 100644 --- a/docs/launch-options.md +++ b/docs/launch-options.md @@ -314,6 +314,7 @@ issue is closed, the strip was missed; delete both. | `ACDREAM_PROBE_CELL_CACHE` | indoor walking phase d | `=1` | gates one `[cell-cache]` line per EnvCell first-cached in `PhysicsDataCache.CacheCellStruct` (poly counts, BSP root structure) | print-only; fires at most once per EnvCell (cache is no-op after first population); no DebugPanel mirror | `PhysicsDiagnostics.ProbeCellCacheEnabled` | | `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` | | `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` | +| `ACDREAM_PROBE_WALK_SHADOW` | Campaign FW3.2b-2 flip apparatus — dies with the flip commit | `=1` | runs the PRODUCTION retail frame walk per frame in SHADOW (no draws change) and prints one `[walk-shadow]` line per frame whose visited cell/building sets diverge from the old visibility path (the I5 dual-shadow pattern applied to the FW3 static cutover) | print-only; adds the per-frame CPU cost of the shadow walk while set | `RenderingDiagnostics.ProbeWalkShadowEnabled` | | `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` | | `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B | | `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` | diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index c92d5320..793ce392 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -517,7 +517,8 @@ internal sealed class FrameRootCompositionPhase // data, published/retired with each landblock by the // render publisher (FW3.1). live.LandblockPipeline.RenderPublisher?.WalkBuildings, - live.LandblockPipeline.RenderPublisher?.WalkLandscape), + live.LandblockPipeline.RenderPublisher?.WalkLandscape, + d.CellVisibility), retailPViewPassExecutor, retailPViewPassExecutor), retailPViewCells, diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index dadbdc9c..81c38464 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -96,18 +96,21 @@ public sealed class RetailPViewRenderer // Campaign FW3.2b-2: the walk's production world-data registries // (published/retired by LandblockRenderPublisher) plus the per-frame // driver state. Null until the composition passes them; the static - // cutover requires both. + // cutover requires all three. private readonly Walk.WalkBuildingRegistry? _walkBuildings; private readonly Walk.WalkLandscapeAssembler? _walkLandscape; + private readonly CellVisibility? _walkCellRegistry; internal RetailPViewRenderer( InteriorEntityPartition.IObserver? partitionObserver, RenderScenePViewFrameProductController? sceneFrameProduct = null, Walk.WalkBuildingRegistry? walkBuildings = null, - Walk.WalkLandscapeAssembler? walkLandscape = null) + Walk.WalkLandscapeAssembler? walkLandscape = null, + CellVisibility? walkCellRegistry = null) { _walkBuildings = walkBuildings; _walkLandscape = walkLandscape; + _walkCellRegistry = walkCellRegistry; _partitionObserver = partitionObserver; _candidateObserver = partitionObserver as ICurrentRenderPViewObserver; _sceneFrameProduct = sceneFrameProduct; @@ -293,6 +296,13 @@ public sealed class RetailPViewRenderer partition); passes.EmitDiagnostics(ctx, result); + // Campaign FW3.2b-2 flip apparatus (ACDREAM_PROBE_WALK_SHADOW=1, + // throwaway — dies with the flip commit): run the PRODUCTION + // retail frame walk in shadow and report set divergence vs this + // frame's old-path visibility. No draws change. + if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkShadowEnabled) + RunWalkShadowProbe(ctx, prepareCells); + // #118: stage assignment for dynamics under an INTERIOR root. Retail // draws the OUTSIDE world's objects inside the landscape stage — // PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated @@ -870,6 +880,111 @@ public sealed class RetailPViewRenderer } } + /// Campaign FW3.2b-2 flip apparatus (the I5 dual-shadow + /// pattern): drive the production walk over the FW3.1 registries with a + /// set-collecting sink and print one [walk-shadow] line per frame + /// whose visited cells diverge from the old path's + /// (main flood ∪ look-ins). Divergence is + /// EXPECTED where the walk's retail model deliberately differs from the + /// old builder — the probe's value is proving the production data + /// pipeline live and QUANTIFYING the difference for the flip review. A + /// probe exception prints loudly and never kills the frame (it is the + /// probe's own signal, not a production fault). + private void RunWalkShadowProbe( + RetailPViewFrameInput ctx, HashSet oldPathCells) + { + if (_walkBuildings is null || _walkLandscape is null + || _walkCellRegistry is null) + { + return; + } + try + { + // Forward = -(view column 3): System.Numerics CreateLookAt's + // zaxis is eye-target (backward). Viewport: the walk's SETS are + // viewport-scale-tolerant (every screen projection shares the + // same constants), so the shadow pins retail's capture size; the + // flip itself will use the real attachment extent. + Matrix4x4 view = ctx.CameraView; + var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33)); + var context = new Walk.WalkProductionFrameContext( + _walkCellRegistry, + _walkBuildings, + ctx.ViewerEyePos, + forward, + ctx.ViewProjection, + viewportWidth: 1024f, + viewportHeight: 720f); + Walk.WalkLandscape landscape = _walkLandscape.Landscape; + _walkLandscape.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos); + Walk.WalkCell? cameraCell = null; + if ((ctx.ViewerCellId & 0xFFFFu) >= 0x100) + { + cameraCell = _walkCellRegistry.TryGetCell(ctx.ViewerCellId, out LoadedCell? loaded) + ? loaded?.Walk + : null; + if (cameraCell is null) + { + Console.WriteLine( + $"[walk-shadow] root={ctx.ViewerCellId:x8} interior camera cell has no walk data"); + return; + } + } + var sink = new WalkShadowSink(); + _walkShadowFrameWalk.WalkFrame( + ctx.ViewerCellId, cameraCell, landscape, context, sink); + + int onlyWalk = 0; + foreach (uint id in sink.Cells) + if (!oldPathCells.Contains(id)) + onlyWalk++; + int onlyOld = 0; + foreach (uint id in oldPathCells) + if (!sink.Cells.Contains(id)) + onlyOld++; + if (onlyWalk != 0 || onlyOld != 0) + { + Console.WriteLine( + $"[walk-shadow] root={ctx.ViewerCellId:x8} walkCells={sink.Cells.Count} " + + $"oldCells={oldPathCells.Count} onlyWalk={onlyWalk} onlyOld={onlyOld} " + + $"walkBuildings={sink.BuildingCount} landscapeTurns={sink.LandscapeCount}"); + } + } + catch (Exception failure) + { + Console.WriteLine($"[walk-shadow] PROBE FAULT root={ctx.ViewerCellId:x8}: {failure}"); + } + } + + private readonly Walk.RetailFrameWalk _walkShadowFrameWalk = new(); + + private sealed class WalkShadowSink : Walk.IWalkEventSink + { + public readonly HashSet Cells = new(); + public int BuildingCount; + public int LandscapeCount; + + public void Emit(in Walk.WalkEvent walkEvent) + { + switch (walkEvent.Kind) + { + case Walk.WalkEventKind.DrawInside: + Cells.Add(walkEvent.CellId); + break; + case Walk.WalkEventKind.DrawCells: + foreach (uint id in walkEvent.Cells) + Cells.Add(id); + break; + case Walk.WalkEventKind.Building: + BuildingCount++; + break; + case Walk.WalkEventKind.Landscape: + LandscapeCount++; + break; + } + } + } + private void DrawLandscapeThroughOutsideView( RetailPViewFrameInput ctx, IRetailPViewPassExecutor passes, diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs index 634627f4..d7dcdef9 100644 --- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs +++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs @@ -160,6 +160,17 @@ public static class RenderingDiagnostics public static bool ProbeClipRouteEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1"; + /// + /// Campaign FW3.2b-2 flip apparatus (2026-08-30): when true and the walk + /// registries are wired, RetailPViewRenderer runs the PRODUCTION retail + /// frame walk per frame in SHADOW (no draws change) and emits one + /// [walk-shadow] line per frame whose cell/building sets diverge from the + /// old visibility path — the I5 dual-shadow pattern applied to the FW3 + /// static cutover. Throwaway apparatus — dies with the flip commit. + /// + public static bool ProbeWalkShadowEnabled { get; set; } = + Environment.GetEnvironmentVariable("ACDREAM_PROBE_WALK_SHADOW") == "1"; + /// /// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits /// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,