feat(render): Phase 3 (Step B) — single render path rooted at the viewer cell (cutover flip)

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-07 19:06:27 +02:00
parent 5379f6ecd3
commit 445e861163

View file

@ -7384,7 +7384,16 @@ public sealed class GameWindow : IDisposable
bool playerIndoorGate = AcDream.Core.Rendering.RenderingDiagnostics.ShouldRenderIndoor( bool playerIndoorGate = AcDream.Core.Rendering.RenderingDiagnostics.ShouldRenderIndoor(
playerCellId, playerCellId,
playerRoot is not null); 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 string renderBranch = clipRoot is null
? "OutdoorRoot" ? "OutdoorRoot"
: "RetailPViewInside"; : "RetailPViewInside";
@ -7553,6 +7562,23 @@ public sealed class GameWindow : IDisposable
: sigSceneParticles; : sigSceneParticles;
sigOutdoorSceneryDrawn = pviewResult.Partition.Outdoor.Count > 0 sigOutdoorSceneryDrawn = pviewResult.Partition.Outdoor.Count > 0
&& pviewResult.ClipAssembly.OutsideViewSlices.Length > 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 else
{ {