refactor(render): require the production frame walk

This commit is contained in:
Erik 2026-08-31 05:09:51 +02:00
parent cd08cd2ae8
commit 1be4889c1c

View file

@ -162,75 +162,39 @@ public sealed class RetailPViewRenderer
ArgumentNullException.ThrowIfNull(ctx); ArgumentNullException.ThrowIfNull(ctx);
ArgumentNullException.ThrowIfNull(passes); ArgumentNullException.ThrowIfNull(passes);
passes.BeginFrame(); passes.BeginFrame();
RecycleLookInFrames(); // Campaign FW4: there is one production renderer and one visibility
ResetBuildingGroups(); // owner. The fake/legacy executor path was useful during the cutover,
// but retaining it here kept the retired PortalVisibilityBuilder and
// Campaign FW4 production cutover: decide the walk route before any // look-in seed machinery alive. Fail loudly if composition ever tries
// old PView construction. A concrete production executor must have all // to reintroduce that split.
// walk registries; otherwise the frame is miswired and fails loud. RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor
RetailPViewPassExecutor? walkExecutor = passes as RetailPViewPassExecutor; ?? throw new InvalidOperationException(
bool walkRegistriesReady = "The retail frame walk requires RetailPViewPassExecutor.");
_walkBuildings is not null if (_renderSceneShadow is null
&& _walkLandscape is not null || _walkBuildings is null
&& _walkCellRegistry is not null || _walkLandscape is null
&& _walkWorldData is not null; || _walkCellRegistry is null
if (walkExecutor is not null || _walkWorldData is null)
&& _renderSceneShadow is not null
&& !walkRegistriesReady)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"RetailPViewRenderer has a concrete pass executor and a " "The retail frame walk requires the retained scene and all "
+ "RenderSceneShadowRuntime but the walk registries " + "committed walk registries.");
+ "(WalkBuildingRegistry/WalkLandscapeAssembler/CellVisibility) are "
+ "not all wired — the Campaign FW3.2b-2 static cutover requires "
+ "every piece together; see FrameRootComposition's "
+ "RetailPViewRenderer construction.");
} }
bool walkActive =
walkExecutor is not null
&& _renderSceneShadow is not null
&& walkRegistriesReady;
PortalVisibilityFrame pvFrame;
if (walkActive)
{
// Compatibility carrier only. Production visibility is populated // Compatibility carrier only. Production visibility is populated
// exclusively by RetailFrameWalk below; this frame supplies the // exclusively by RetailFrameWalk below; this frame supplies the clip
// clip assembler's outdoor full-screen seed until that remaining // assembler's outdoor full-screen seed until that carrier is removed.
// shell is deleted later in FW4. PortalVisibilityFrame pvFrame = _mainPortalFrameScratch;
pvFrame = _mainPortalFrameScratch;
pvFrame.ResetForBuild(); pvFrame.ResetForBuild();
if (ctx.RootCell.IsOutdoorNode) if (ctx.RootCell.IsOutdoorNode)
{ {
pvFrame.OutsideView.SetFullScreen(); pvFrame.OutsideView.SetFullScreen();
pvFrame.OrderedVisibleCells.Add(ctx.RootCell.CellId); pvFrame.OrderedVisibleCells.Add(ctx.RootCell.CellId);
} }
}
else
{
// Standalone/fake executor compatibility. Production never calls
// PortalVisibilityBuilder after the FW4 cutover above.
pvFrame = PortalVisibilityBuilder.Build(
ctx.RootCell,
ctx.ViewerEyePos,
ctx.Cells.Find,
ctx.ViewProjection,
buildingMembership: null,
reuseFrame: _mainPortalFrameScratch);
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
MergeNearbyBuildingFloods(ctx, pvFrame);
if (!ctx.RootCell.IsOutdoorNode
&& ctx.NearbyBuildingCells is not null
&& pvFrame.OutsideView.Polygons.Count > 0)
BuildInteriorRootLookIns(ctx, pvFrame);
}
var clipAssembly = passes.AssembleClipFrame( var clipAssembly = passes.AssembleClipFrame(
pvFrame, pvFrame,
_clipAssemblyScratch); _clipAssemblyScratch);
if (!walkActive)
passes.AppendLookInClipFrames(_lookInFrames, clipAssembly);
// FW4 slice 1: PrepareClipFrame (the one clip-region publication) // FW4 slice 1: PrepareClipFrame (the one clip-region publication)
// moved BELOW the walk block — an interior-rooted walk frame // moved BELOW the walk block — an interior-rooted walk frame
// re-derives the outside-view slices from the walk's own views // re-derives the outside-view slices from the walk's own views
@ -262,8 +226,7 @@ public sealed class RetailPViewRenderer
// runs at append time, not at Replay time), so the world data must // runs at append time, not at Replay time), so the world data must
// already be rebuilt for this frame before the walk starts. // already be rebuilt for this frame before the walk starts.
Walk.WalkFrameDriver? walkDriver = null; Walk.WalkFrameDriver? walkDriver = null;
WalkProductionLeafRenderer? walkLeafRenderer = null; WalkProductionLeafRenderer walkLeafRenderer;
if (walkActive)
{ {
Matrix4x4 view = ctx.CameraView; Matrix4x4 view = ctx.CameraView;
var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33)); var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33));
@ -441,31 +404,8 @@ public sealed class RetailPViewRenderer
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2); int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
passes.PrepareClipFrame(terrainUploadCount); passes.PrepareClipFrame(terrainUploadCount);
// Production prepares exactly the walk's visited-cell set. The // Production prepares exactly the one walk's visited-cell set.
// legacy builder's main-flood/look-in union survives only for HashSet<uint> prepareCells = drawableCells;
// standalone fake executors until the shell-deletion slice.
var prepareCells = drawableCells;
_lookInCellIds.Clear();
if (walkActive)
{
_lookInPrepareScratch.Clear();
_lookInPrepareScratch.UnionWith(walkDriver!.VisitedCells);
prepareCells = _lookInPrepareScratch;
}
else if (_lookInFrames.Count > 0)
{
_lookInPrepareScratch.Clear();
_lookInPrepareScratch.UnionWith(drawableCells);
foreach (var f in _lookInFrames)
{
foreach (uint c in f.OrderedVisibleCells)
{
_lookInPrepareScratch.Add(c);
_lookInCellIds.Add(c);
}
}
prepareCells = _lookInPrepareScratch;
}
// (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran // (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran
// here was the seam-floor flicker mechanism — retail's visible_cell_table is // here was the seam-floor flicker mechanism — retail's visible_cell_table is
@ -474,80 +414,12 @@ public sealed class RetailPViewRenderer
passes.PrepareCellBatches(ctx, prepareCells); passes.PrepareCellBatches(ctx, prepareCells);
// T1 (fused BR-2/3): retail's frame order — static world, then the
// aperture depth writes, then interior cells WHOLE far→near, then
// per-cell statics, then ALL dynamics last (retail draws objects after
// cells: PView::DrawCells Ghidra 0x005a4840; DrawBuilding 0x0059f2a0).
// The geometric shell chop (gl_ClipDistance crop, 927fd8f/9ce335e) is
// DELETED — retail never clips cell geometry; aperture exactness comes
// from the punch/seal depth writes + the z-buffer, and the dynamics-
// last order is what makes the punch safe (the first BR-2 attempt
// punched after dynamics and erased the player, reverted 88be519).
// T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view,
// never clipped (Ghidra 0x0054c250). Built once per frame from the
// assembled slices + this frame's view-projection.
var viewcone = ViewconeCuller.Build(
clipAssembly,
ctx.ViewProjection,
_viewconeScratch);
// Packed frame routes no longer participate in production. These
// placeholders keep the standalone legacy/fake path self-contained
// until FW4 removes that shell completely.
IRenderFrameEntityPassExecutor? frameEntityPasses = null;
RenderFrameView frameView = default;
_candidateObserver?.BeginPViewFrame(); _candidateObserver?.BeginPViewFrame();
try try
{ {
// FW4 slice 3: the outside-stage predicate's flood-membership set RenderProjectionCounts retainedCounts = _renderSceneShadow.Counts;
// is THE WALK'S visited cells on walk frames (root flood + RenderFrameDiagnosticCounts counts = WalkDiagnosticCounts(retainedCounts);
// look-in floods — retail draws all of their objects inside RenderProjectionCounts sourceCounts = retainedCounts;
// LScape::draw, pre-clear). The old apparatus's drawableCells
// misses cells at the #456 seam band, dropping interior-parented
// outdoor emitters (the cathedral falls weenies) to the
// post-clear last pass, where the cleared depth lets them splat
// across terrain and water. drawableCells keeps its other roles
// (prepare filter, frame result) unchanged.
HashSet<uint> outsideStageFlood =
walkActive ? walkDriver!.VisitedCells : drawableCells;
// The stage-set split: the walk's TRUE root flood (never the
// visited union) keys which interior-parented dynamics may ride
// the last pass — see BuildOutsideDynamicRoutes' sibling gate.
_rootFloodSetScratch.Clear();
if (walkActive)
{
foreach (uint cellId in walkDriver!.InteriorFloodCells)
_rootFloodSetScratch.Add(cellId);
}
// The walk reads the retained scene directly. Rebuild the former
// WorldEntity partition only for the standalone fallback and
// explicitly enabled comparison/probe paths.
InteriorEntityPartition.Result? partition = null;
if (!walkActive || LegacyPartitionDiagnosticsEnabled)
{
InteriorEntityPartition.Partition(
_partitionResult,
prepareCells,
ctx.LandblockEntries,
_partitionObserver,
ctx.Frustum,
ctx.PlayerLandblockId ?? 0u);
partition = _partitionResult;
}
RenderProjectionCounts retainedCounts = walkActive
? _renderSceneShadow!.Counts
: default;
RenderFrameDiagnosticCounts counts = walkActive
? WalkDiagnosticCounts(retainedCounts)
: LegacyDiagnosticCounts(partition!);
RenderProjectionCounts sourceCounts = walkActive
? retainedCounts
: LegacySourceCounts(partition!);
// prepareCells is exactly "main flood look-in cells" — the cells
// this traversal actually reached, i.e. retail's in-view set.
RetailPViewFrameResult result = _frameResultScratch.Reset( RetailPViewFrameResult result = _frameResultScratch.Reset(
pvFrame, pvFrame,
clipAssembly, clipAssembly,
@ -555,74 +427,17 @@ public sealed class RetailPViewRenderer
prepareCells, prepareCells,
counts, counts,
sourceCounts, sourceCounts,
partition); diagnosticPartition: null);
passes.EmitDiagnostics(ctx, result); passes.EmitDiagnostics(ctx, result);
// Campaign FW3.2b-2 flip apparatus (ACDREAM_PROBE_WALK_SHADOW=1, // The one collected walk owns terrain, cell shells, buildings,
// throwaway — dies with the flip commit): run the PRODUCTION // statics, dynamics, particles, punches, and alpha barriers at
// retail frame walk in shadow and report set divergence vs this // their retail turns. Interior-root landscape services run at the
// frame's old-path visibility. No draws change. // pre-clear callback; outdoor roots have no clear and run after
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkShadowEnabled) // replay.
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
// full depth clear (pc:432731-432732) and the exit-portal SEALS
// (pc:432785-432786); DrawBlock draws every landcell's objects via
// DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our
// single last pass instead z-fails against the seal's true-depth stamp
// the moment it stands beyond the door plane — the house-exit
// clip+vanish (pinned by HouseExitWalkReplayTests). So under an
// interior root: outdoor-classified dynamics draw in the outside
// stage; an indoor dynamic whose sphere STRADDLES an exit portal
// draws in BOTH stages (retail's per-overlapped-cell shadow-part
// draw, DrawBlock pc:430056-430064) so neither body half clips at the
// plane. Outdoor roots keep ALL dynamics in the last pass — our
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
// BR-2 punch-after-dynamics lesson, reverted 88be519).
_outsideStageDynamics.Clear();
if (partition is not null && !ctx.RootCell.IsOutdoorNode)
{
foreach (var e in partition.Dynamics)
{
EntitySphere(e, out var c, out float r);
// FW4 slice 3: same walk-flood membership set as the
// frame product's BuildOutsideDynamicRoutes — the two
// predicate call sites must agree or routed and drawn
// stages diverge.
if (DynamicDrawsInOutsideStage(e.ParentCellId, c, r, outsideStageFlood, ctx.Cells))
_outsideStageDynamics.Add(e);
}
}
if (walkActive)
{
// Campaign FW3.2b-2: the walk owns every static draw —
// terrain/sky, building shells + punches + look-in cell
// statics, and the interior root's own flood shells +
// statics (with the depth clear + exit seals at their real
// retail turn). The OLD visibility (pvFrame/clipAssembly/
// viewcone, already built above) keeps running unchanged to
// feed the surviving dynamic routes only (plan §FW3 item 1's
// dual-compute split). Campaign FW3.4a: the walk itself
// already ran (Collect, above) — this is Replay only.
//
// FW3 visual-gate fix: for an INTERIOR root the dynamics
// phase (outside dynamics + look-in dynamics + outdoor
// particles + weather) must draw at the walk's PRE-CLEAR
// boundary — retail draws them inside LScape::draw, before
// the clear+seals — so the driver's clearInteriorDepth
// closure invokes it via _walkPreClearDynamics. For an
// OUTDOOR root there is no clear (retail has none) and the
// phase runs after the driver, matching the old order.
Walk.WalkFrameDriver capturedDriver = walkDriver!; Walk.WalkFrameDriver capturedDriver = walkDriver!;
_walkPreClearDynamics = () => _walkPreClearDynamics = () =>
{ {
// Static-owner emitters now submit AT THEIR OWN WALK
// TURNS during Replay (WalkFrameEventKind.StaticParticles
// — the #132 positional invariant), so the pre-clear
// closure carries only the dynamics phase.
passes.UseIndoorMembershipOnlyRouting(); passes.UseIndoorMembershipOnlyRouting();
DrawLandscapeDynamicsPhase( DrawLandscapeDynamicsPhase(
ctx, ctx,
@ -632,7 +447,7 @@ public sealed class RetailPViewRenderer
}; };
try try
{ {
DrawWalkDrivenStatics(ctx, walkExecutor!, walkDriver!); DrawWalkDrivenStatics(ctx, walkExecutor, walkDriver!);
passes.UseIndoorMembershipOnlyRouting(); passes.UseIndoorMembershipOnlyRouting();
if (ctx.RootCell.IsOutdoorNode) if (ctx.RootCell.IsOutdoorNode)
{ {
@ -647,73 +462,6 @@ public sealed class RetailPViewRenderer
{ {
_walkPreClearDynamics = null; _walkPreClearDynamics = null;
} }
}
else
{
DrawLandscapeThroughOutsideView(
ctx,
passes,
clipAssembly,
partition,
viewcone,
frameEntityPasses,
in frameView);
passes.UseIndoorMembershipOnlyRouting();
// Retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE
// its portal-only far-Z pass. In retail's strict far→near walk
// everything queued at that instant is FARTHER than the structure
// being punched, so no already-drained poly can meet a punched
// aperture's falsified depth, and everything drained later is
// NEARER than the punched structure and legitimately composites in
// front of it. The batched outdoor frame reproduces that invariant
// here: drain the far prefix — every entry at or beyond the
// nearest cell whose exit-portal mask is about to punch far-Z —
// against still-true landscape depth. Without this, an exterior
// waterfall beyond the cathedral drains after the punches and
// z-passes across every aperture pixel whose true depth the punch
// erased (#132 regression found at the 2026-08-29 cathedral gate).
// Interior roots keep their pre-clear stage-boundary drain.
if (ctx.RootCell.IsOutdoorNode)
{
passes.FlushLandscapeAlphaFartherThan(
ExitPortalMaskBarrierDistance(
pvFrame,
drawableCells,
ctx.Cells,
ctx.CameraWorldPosition));
}
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
DrawEnvCellShells(passes, pvFrame);
DrawCellObjectLists(
ctx,
passes,
pvFrame,
clipAssembly,
drawableCells,
partition,
viewcone,
frameEntityPasses,
in frameView);
}
// Campaign FW4: the retail walk now owns live objects at their
// exact DrawObjCell turns (outdoor cells, look-ins, and the root
// flood). The packed DynamicLast route is a legacy duplicate and
// must not run on a walk frame. Standalone/fake executors retain
// the old path only until the FW4 shell-deletion slice removes it.
if (!walkActive)
{
DrawDynamicsLast(
ctx,
passes,
partition,
viewcone,
ctx.RootCell.IsOutdoorNode,
frameEntityPasses,
in frameView);
}
// OUTDOOR root: the LScape-boundary alpha drain deferred from the // OUTDOOR root: the LScape-boundary alpha drain deferred from the
// landscape stage runs HERE, after punches, interior shells, cell // landscape stage runs HERE, after punches, interior shells, cell