refactor(render): require the production frame walk
This commit is contained in:
parent
cd08cd2ae8
commit
1be4889c1c
1 changed files with 51 additions and 303 deletions
|
|
@ -162,75 +162,39 @@ public sealed class RetailPViewRenderer
|
|||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
ArgumentNullException.ThrowIfNull(passes);
|
||||
passes.BeginFrame();
|
||||
RecycleLookInFrames();
|
||||
ResetBuildingGroups();
|
||||
|
||||
// Campaign FW4 production cutover: decide the walk route before any
|
||||
// old PView construction. A concrete production executor must have all
|
||||
// walk registries; otherwise the frame is miswired and fails loud.
|
||||
RetailPViewPassExecutor? walkExecutor = passes as RetailPViewPassExecutor;
|
||||
bool walkRegistriesReady =
|
||||
_walkBuildings is not null
|
||||
&& _walkLandscape is not null
|
||||
&& _walkCellRegistry is not null
|
||||
&& _walkWorldData is not null;
|
||||
if (walkExecutor is not null
|
||||
&& _renderSceneShadow is not null
|
||||
&& !walkRegistriesReady)
|
||||
// Campaign FW4: there is one production renderer and one visibility
|
||||
// owner. The fake/legacy executor path was useful during the cutover,
|
||||
// but retaining it here kept the retired PortalVisibilityBuilder and
|
||||
// look-in seed machinery alive. Fail loudly if composition ever tries
|
||||
// to reintroduce that split.
|
||||
RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor
|
||||
?? throw new InvalidOperationException(
|
||||
"The retail frame walk requires RetailPViewPassExecutor.");
|
||||
if (_renderSceneShadow is null
|
||||
|| _walkBuildings is null
|
||||
|| _walkLandscape is null
|
||||
|| _walkCellRegistry is null
|
||||
|| _walkWorldData is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"RetailPViewRenderer has a concrete pass executor and a "
|
||||
+ "RenderSceneShadowRuntime but the 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.");
|
||||
"The retail frame walk requires the retained scene and all "
|
||||
+ "committed walk registries.");
|
||||
}
|
||||
bool walkActive =
|
||||
walkExecutor is not null
|
||||
&& _renderSceneShadow is not null
|
||||
&& walkRegistriesReady;
|
||||
|
||||
PortalVisibilityFrame pvFrame;
|
||||
if (walkActive)
|
||||
// Compatibility carrier only. Production visibility is populated
|
||||
// exclusively by RetailFrameWalk below; this frame supplies the clip
|
||||
// assembler's outdoor full-screen seed until that carrier is removed.
|
||||
PortalVisibilityFrame pvFrame = _mainPortalFrameScratch;
|
||||
pvFrame.ResetForBuild();
|
||||
if (ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
// Compatibility carrier only. Production visibility is populated
|
||||
// exclusively by RetailFrameWalk below; this frame supplies the
|
||||
// clip assembler's outdoor full-screen seed until that remaining
|
||||
// shell is deleted later in FW4.
|
||||
pvFrame = _mainPortalFrameScratch;
|
||||
pvFrame.ResetForBuild();
|
||||
if (ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
pvFrame.OutsideView.SetFullScreen();
|
||||
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);
|
||||
pvFrame.OutsideView.SetFullScreen();
|
||||
pvFrame.OrderedVisibleCells.Add(ctx.RootCell.CellId);
|
||||
}
|
||||
|
||||
var clipAssembly = passes.AssembleClipFrame(
|
||||
pvFrame,
|
||||
_clipAssemblyScratch);
|
||||
if (!walkActive)
|
||||
passes.AppendLookInClipFrames(_lookInFrames, clipAssembly);
|
||||
// FW4 slice 1: PrepareClipFrame (the one clip-region publication)
|
||||
// moved BELOW the walk block — an interior-rooted walk frame
|
||||
// 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
|
||||
// already be rebuilt for this frame before the walk starts.
|
||||
Walk.WalkFrameDriver? walkDriver = null;
|
||||
WalkProductionLeafRenderer? walkLeafRenderer = null;
|
||||
if (walkActive)
|
||||
WalkProductionLeafRenderer walkLeafRenderer;
|
||||
{
|
||||
Matrix4x4 view = ctx.CameraView;
|
||||
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);
|
||||
passes.PrepareClipFrame(terrainUploadCount);
|
||||
|
||||
// Production prepares exactly the walk's visited-cell set. The
|
||||
// legacy builder's main-flood/look-in union survives only for
|
||||
// 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;
|
||||
}
|
||||
// Production prepares exactly the one walk's visited-cell set.
|
||||
HashSet<uint> prepareCells = drawableCells;
|
||||
|
||||
// (#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
|
||||
|
|
@ -474,80 +414,12 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
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();
|
||||
try
|
||||
{
|
||||
// FW4 slice 3: the outside-stage predicate's flood-membership set
|
||||
// is THE WALK'S visited cells on walk frames (root flood +
|
||||
// look-in floods — retail draws all of their objects inside
|
||||
// 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.
|
||||
RenderProjectionCounts retainedCounts = _renderSceneShadow.Counts;
|
||||
RenderFrameDiagnosticCounts counts = WalkDiagnosticCounts(retainedCounts);
|
||||
RenderProjectionCounts sourceCounts = retainedCounts;
|
||||
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
||||
pvFrame,
|
||||
clipAssembly,
|
||||
|
|
@ -555,164 +427,40 @@ public sealed class RetailPViewRenderer
|
|||
prepareCells,
|
||||
counts,
|
||||
sourceCounts,
|
||||
partition);
|
||||
diagnosticPartition: null);
|
||||
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
|
||||
// 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)
|
||||
// The one collected walk owns terrain, cell shells, buildings,
|
||||
// statics, dynamics, particles, punches, and alpha barriers at
|
||||
// their retail turns. Interior-root landscape services run at the
|
||||
// pre-clear callback; outdoor roots have no clear and run after
|
||||
// replay.
|
||||
Walk.WalkFrameDriver capturedDriver = walkDriver!;
|
||||
_walkPreClearDynamics = () =>
|
||||
{
|
||||
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)
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
DrawLandscapeDynamicsPhase(
|
||||
ctx,
|
||||
passes,
|
||||
clipAssembly,
|
||||
capturedDriver);
|
||||
};
|
||||
try
|
||||
{
|
||||
// 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!;
|
||||
_walkPreClearDynamics = () =>
|
||||
DrawWalkDrivenStatics(ctx, walkExecutor, walkDriver!);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
if (ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
// 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();
|
||||
DrawLandscapeDynamicsPhase(
|
||||
ctx,
|
||||
passes,
|
||||
clipAssembly,
|
||||
capturedDriver);
|
||||
};
|
||||
try
|
||||
{
|
||||
DrawWalkDrivenStatics(ctx, walkExecutor!, walkDriver!);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
if (ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
DrawLandscapeDynamicsPhase(
|
||||
ctx,
|
||||
passes,
|
||||
clipAssembly,
|
||||
walkDriver!);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_walkPreClearDynamics = null;
|
||||
walkDriver!);
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
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);
|
||||
_walkPreClearDynamics = null;
|
||||
}
|
||||
|
||||
// OUTDOOR root: the LScape-boundary alpha drain deferred from the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue