From 1be4889c1cd969e3789964dad86d975b791cd0f9 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 31 Aug 2026 05:09:51 +0200 Subject: [PATCH] refactor(render): require the production frame walk --- .../Rendering/RetailPViewRenderer.cs | 354 +++--------------- 1 file changed, 51 insertions(+), 303 deletions(-) diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index f0d5d00c..1d9fe0b2 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -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 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 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