From 445e861163d0a3db87b957a2bd2c19e15c57c0c1 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 7 Jun 2026 19:06:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Phase=203=20(Step=20B)=20?= =?UTF-8?q?=E2=80=94=20single=20render=20path=20rooted=20at=20the=20viewer?= =?UTF-8?q?=20cell=20(cutover=20flip)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CUTOVER FLIP that kills the indoor FLAP. Replace the two-branch gate (clipRoot = playerIndoorGate && viewerRoot ? viewerRoot : null) with clipRoot = viewerRoot ?? _outdoorNode, so EVERY frame routes through the one RetailPViewRenderer.DrawInside path rooted at the viewer cell — interior EnvCell when the eye is indoors, the synthetic outdoor node when outdoors. There is no inside/outside branch to toggle as the 3rd-person eye crosses the doorway, so the flap (textures battling at every transition) dies by construction. Matches retail SmartBox::RenderNormalMode -> DrawInside(viewer_cell) (0x453aa0 -> 0x5a5860). clipRoot is null only pre-spawn/login (viewerCellId==0 -> _outdoorNode null), so the outdoor LScape block still runs as the safety path and login keeps its live sky. playerIndoorGate stays computed for the [render-sig] probe. Preserve the LiveDynamic entity draw (server entities with no resolved ParentCellId — the transient unpositioned case) for the outdoor-node root: the old outdoor branch drew it; DrawInside does not, so re-issue it after DrawInside to keep the spec section 10 byte-identical-outdoor guarantee (no live entity blinks out). The old outdoor else block + DrawPortal/BuildFromExterior are now dead when clipRoot is non-null but are LEFT IN PLACE for the user visual gate (handoff section 4 Step D deletes them only after the user confirms). App 216/0, build green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 1632bd02..481eb6fe 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7384,7 +7384,16 @@ public sealed class GameWindow : IDisposable bool playerIndoorGate = AcDream.Core.Rendering.RenderingDiagnostics.ShouldRenderIndoor( playerCellId, playerRoot is not null); - var clipRoot = playerIndoorGate && viewerRoot is not null ? viewerRoot : null; + // Render unification (outdoor-as-cell, 2026-06-07 cutover): ONE render path rooted at the + // VIEWER cell. Eye indoors -> its interior EnvCell (viewerRoot); eye outdoors -> the + // synthetic outdoor node (_outdoorNode, built above from nearby building entrances). The + // result is null ONLY when neither exists (pre-spawn / login / legacy non-chase camera) -> + // the outdoor LScape block below still runs as the safety path (and login still shows the + // live sky). There is no inside/outside branch to TOGGLE as the chase eye crosses the + // doorway boundary, so the indoor FLAP dies by construction. playerIndoorGate stays + // computed for the [render-sig] probe but no longer selects the path (handoff + // docs/research/2026-06-07-render-unification-cutover-flip-handoff.md section 4 Step B). + var clipRoot = viewerRoot ?? _outdoorNode; string renderBranch = clipRoot is null ? "OutdoorRoot" : "RetailPViewInside"; @@ -7553,6 +7562,23 @@ public sealed class GameWindow : IDisposable : sigSceneParticles; sigOutdoorSceneryDrawn = pviewResult.Partition.Outdoor.Count > 0 && pviewResult.ClipAssembly.OutsideViewSlices.Length > 0; + + // Render unification: DrawInside draws the Outdoor bucket (through the landscape + // slice) and the per-cell ByCell buckets, but NOT LiveDynamic — server entities with + // no resolved ParentCellId (the transient just-spawned / unpositioned case the old + // outdoor branch drew at the bottom of its block). Preserve that draw for the + // outdoor-node root so no live entity blinks out outdoors (spec section 10 regression + // guard). DrawInside's tail clears entity clip routing and disables clip distances, so + // visibleCellIds:null draws them unclipped — identical to the old outdoor path. + if (ReferenceEquals(clipRoot, _outdoorNode) + && _interiorRenderer is not null + && pviewResult.Partition.LiveDynamic.Count > 0) + { + _interiorRenderer.DrawEntityBucket( + camera, frustum, playerLb, animatedIds, + pviewResult.Partition.LiveDynamic, visibleCellIds: null); + sigLiveDynamicDrawnCount = pviewResult.Partition.LiveDynamic.Count; + } } else {