From d01fe30ac0bf5e69295fa3b2d7850a27147a735c Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 7 Jun 2026 18:35:25 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Phase=203=20(Task=202)=20?= =?UTF-8?q?=E2=80=94=20build=20the=20outdoor=20node=20each=20frame=20(addi?= =?UTF-8?q?tive,=20unconsumed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds the synthetic outdoor cell node (OutdoorCellNode.Build) every outdoor frame from the nearby building-entrance portals (Chebyshev <=1 landblocks), stored in _outdoorNode. NOT yet rooted — clipRoot/viewerRoot unchanged, so behaviour is identical this commit. [outdoor-node] probe (ACDREAM_PROBE_FLAP) reports the live portal count so the next (cutover) step can confirm real building entrances were found before flipping the render root. App.Tests 214/214, build green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 99527c81..1632bd02 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -184,6 +184,14 @@ public sealed class GameWindow : IDisposable private readonly HashSet _outdoorRootNoCells = new(0); private readonly HashSet _exteriorPortalLandblocks = new(); private readonly List _exteriorPortalCandidateCells = new(); + + // Phase 3 (render unification, 2026-06-07): the synthetic outdoor cell node — the outdoor + // world as a flood-graph cell (spec 2026-06-07-render-unification-outdoor-as-cell). Rebuilt + // each outdoor frame from nearby building-entrance portals. ADDITIVE for now (built but not + // yet rooted; the clipRoot flip + OutsideView terrain integration is the cutover step). + private LoadedCell? _outdoorNode; + private readonly List _outdoorNodeBuildingCells = new(); + private readonly HashSet _outdoorNodeSeenLbs = new(); private readonly HashSet _outdoorSceneParticleEntityIds = new(); private readonly HashSet _visibleSceneParticleEntityIds = new(); private string? _lastRenderSignature; @@ -7339,6 +7347,39 @@ public sealed class GameWindow : IDisposable // (mesh SSBO) + binding=2 (terrain UBO); each renderer re-binds its // binding=2 defensively from the ids we hand it. _clipFrame ??= ClipFrame.NoClip(); + + // Phase 3 (render unification, additive): build the synthetic outdoor cell node when + // the eye is outdoors (no interior viewerRoot). Stored in _outdoorNode but NOT yet + // rooted — behaviour is unchanged this commit. The nearby-building enumeration mirrors + // the look-in candidate gather in the OUTDOOR branch below (Chebyshev <=1 landblocks); + // OutdoorCellNode.Build filters to exit portals internally. The clipRoot flip + + // OutsideView terrain integration that consumes this is the next (cutover) step. + _outdoorNode = null; + if (viewerRoot is null && viewerCellId != 0u) + { + _outdoorNodeBuildingCells.Clear(); + _outdoorNodeSeenLbs.Clear(); + int onGridX = playerLb.HasValue ? (int)((playerLb.Value >> 24) & 0xFFu) : -1; + int onGridY = playerLb.HasValue ? (int)((playerLb.Value >> 16) & 0xFFu) : -1; + foreach (var onEntry in _worldState.LandblockEntries) + { + uint onLb = (onEntry.LandblockId >> 16) & 0xFFFFu; + if (playerLb.HasValue) + { + int gX = (int)((onLb >> 8) & 0xFFu); + int gY = (int)(onLb & 0xFFu); + if (Math.Max(Math.Abs(gX - onGridX), Math.Abs(gY - onGridY)) > 1) continue; + } + if (!_outdoorNodeSeenLbs.Add(onLb)) continue; + foreach (var onCell in _cellVisibility.GetCellsForLandblock(onLb)) + _outdoorNodeBuildingCells.Add(onCell); + } + _outdoorNode = AcDream.App.Rendering.OutdoorCellNode.Build(viewerCellId, _outdoorNodeBuildingCells); + if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled) + Console.WriteLine(System.FormattableString.Invariant( + $"[outdoor-node] cell=0x{viewerCellId:X8} nearbyCells={_outdoorNodeBuildingCells.Count} portals={_outdoorNode.Portals.Count}")); + } + uint playerCellId = _physicsEngine.DataCache?.CellGraph.CurrCell?.Id ?? 0u; bool playerIndoorGate = AcDream.Core.Rendering.RenderingDiagnostics.ShouldRenderIndoor( playerCellId,