diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index 793ce392..1bdb3105 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -347,20 +347,13 @@ internal sealed class FrameRootCompositionPhase live.EntityEffects, d.Log); IWorldSceneFramePhase? worldSceneRenderer = null; - CurrentRenderSceneOracle? currentRenderSceneOracle = - interaction.RetainedUi?.Screenshots is not null - && d.Options.AutomationArtifactDirectory is not null - ? new CurrentRenderSceneOracle() - : null; - RenderSceneShadowComparisonController? renderSceneShadowComparison = - currentRenderSceneOracle is not null - && live.RenderSceneShadow is not null - ? new RenderSceneShadowComparisonController( - live.RenderSceneShadow, - currentRenderSceneOracle, - message => d.Log("[UI-PROBE] " + message), - acknowledgeDirty: false) - : null; + // Campaign FW4: the retail walk reads RenderSceneShadowRuntime + // directly. The old current-partition oracle, shadow comparison, and + // packed PView frame product no longer participate in production or + // automation; their null snapshot slots remain until the diagnostic + // contract cleanup later in this campaign. + CurrentRenderSceneOracle? currentRenderSceneOracle = null; + RenderSceneShadowComparisonController? renderSceneShadowComparison = null; RenderScenePViewFrameProductController? renderFrameProduct = null; { // Campaign V slice V6j: the world scene is composed on BOTH arms. @@ -487,17 +480,8 @@ internal sealed class FrameRootCompositionPhase live.SkyRenderer, content.ParticleSystem, live.ParticleRenderer); - renderFrameProduct = - live.RenderSceneShadow is not null - ? new RenderScenePViewFrameProductController( - live.RenderSceneShadow, - currentRenderSceneOracle, - message => d.Log("[UI-PROBE] " + message), - live.DrawDispatcher!) - : null; - // After G4 the retained product is the production object source. - // The old dispatcher/selection observer is intentionally detached; - // automation still compares the independently built PView route list. + // The retained scene is the walk's production object source. The + // old dispatcher/selection observer remains detached. live.DrawDispatcher!.SetCurrentRenderSceneObserver(null); live.SelectionScene.SetCurrentRenderSceneObserver(null); worldSceneRenderer = new WorldSceneRenderer( @@ -512,7 +496,7 @@ internal sealed class FrameRootCompositionPhase new WorldScenePViewRenderer( new RetailPViewRenderer( currentRenderSceneOracle, - renderFrameProduct, + live.RenderSceneShadow, // Campaign FW3.2b-2: the walk's production world // data, published/retired with each landblock by the // render publisher (FW3.1). diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index d17bddbf..a464a7bf 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -15,8 +15,7 @@ public sealed class RetailPViewRenderer { private readonly InteriorEntityPartition.IObserver? _partitionObserver; private readonly ICurrentRenderPViewObserver? _candidateObserver; - private readonly RenderScenePViewFrameProductController? - _sceneFrameProduct; + private readonly RenderSceneShadowRuntime? _renderSceneShadow; private readonly PortalVisibilityFrame _mainPortalFrameScratch = new(); private readonly ClipFrameAssembly _clipAssemblyScratch = new(); private readonly ViewconeCuller _viewconeScratch = new(); @@ -133,7 +132,7 @@ public sealed class RetailPViewRenderer internal RetailPViewRenderer( InteriorEntityPartition.IObserver? partitionObserver, - RenderScenePViewFrameProductController? sceneFrameProduct = null, + RenderSceneShadowRuntime? renderSceneShadow = null, Walk.WalkBuildingRegistry? walkBuildings = null, Walk.WalkLandscapeAssembler? walkLandscape = null, CellVisibility? walkCellRegistry = null) @@ -146,7 +145,7 @@ public sealed class RetailPViewRenderer : null; _partitionObserver = partitionObserver; _candidateObserver = partitionObserver as ICurrentRenderPViewObserver; - _sceneFrameProduct = sceneFrameProduct; + _renderSceneShadow = renderSceneShadow; } // T2 (BR-4): retail has NO distance constant on the flood-admission chain @@ -225,7 +224,7 @@ public sealed class RetailPViewRenderer // required — the walk submits through WbDrawDispatcher.SubmitOrderedStream // and needs a real GPU frame/encoder (RequireWalkSubmission), which no // test IRetailPViewPassExecutor fake can supply. Whenever a concrete - // executor IS present, the packed entity-route product must ALSO be + // executor IS present, the retained render scene must ALSO be // wired with all three walk registries — a scene product without the // walk data (or vice versa) is a production miswiring, not a // legacy/diagnostic shape, so it fails loud rather than silently @@ -237,12 +236,12 @@ public sealed class RetailPViewRenderer && _walkCellRegistry is not null && _walkWorldData is not null; if (walkExecutor is not null - && _sceneFrameProduct is not null + && _renderSceneShadow is not null && !walkRegistriesReady) { throw new InvalidOperationException( "RetailPViewRenderer has a concrete pass executor and a " - + "RenderScenePViewFrameProductController but the walk registries " + + "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 " @@ -250,7 +249,7 @@ public sealed class RetailPViewRenderer } bool walkActive = walkExecutor is not null - && _sceneFrameProduct is not null + && _renderSceneShadow is not null && walkRegistriesReady; // Campaign FW3.4a: THE ONE WALK. Builds walkContext/walkLandscape/ @@ -320,7 +319,7 @@ public sealed class RetailPViewRenderer } _walkWorldData!.BeginFrame( - _sceneFrameProduct!.SceneQuery, + _renderSceneShadow!.Query, ctx.PlayerLandblockId ?? 0u, ctx.RenderCenterLbX, ctx.RenderCenterLbY); @@ -496,16 +495,11 @@ public sealed class RetailPViewRenderer 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; - if (_sceneFrameProduct is not null) - { - frameEntityPasses = passes as IRenderFrameEntityPassExecutor - ?? throw new InvalidOperationException( - "The production frame product requires a packed entity-pass executor."); - } RenderFrameView frameView = default; - bool frameViewBorrowed = false; - bool entityFrameOpen = false; _candidateObserver?.BeginPViewFrame(); try { @@ -531,32 +525,11 @@ public sealed class RetailPViewRenderer _rootFloodSetScratch.Add(cellId); } - if (_sceneFrameProduct is not null) - { - frameView = _sceneFrameProduct.BuildAndBorrow( - pvFrame, - clipAssembly, - viewcone, - walkActive ? walkDriver!.LookInCellTurns : [], - walkActive ? walkDriver : null, - outsideStageFlood, - ctx.Cells, - ctx.AnimatedEntityIds, - ctx.RootCell.IsOutdoorNode, - // Non-walk frames (diagnostic fakes / legacy fallback) - // keep the pre-split admission: drawableCells WAS the - // flood in the legacy meaning. - walkActive ? _rootFloodSetScratch : drawableCells); - frameViewBorrowed = true; - frameEntityPasses!.BeginEntityFrame(in frameView); - entityFrameOpen = true; - } - - // The retained scene product is the production object source. - // Rebuild the former WorldEntity partition only for the standalone - // fallback and explicitly enabled comparison/probe paths. + // 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 (_sceneFrameProduct is null || LegacyPartitionDiagnosticsEnabled) + if (!walkActive || LegacyPartitionDiagnosticsEnabled) { InteriorEntityPartition.Partition( _partitionResult, @@ -568,11 +541,14 @@ public sealed class RetailPViewRenderer partition = _partitionResult; } - RenderFrameDiagnosticCounts counts = frameViewBorrowed - ? frameView.DiagnosticCounts + RenderProjectionCounts retainedCounts = walkActive + ? _renderSceneShadow!.Counts + : default; + RenderFrameDiagnosticCounts counts = walkActive + ? WalkDiagnosticCounts(retainedCounts) : LegacyDiagnosticCounts(partition!); - RenderProjectionCounts sourceCounts = frameViewBorrowed - ? frameView.SourceDigest.Counts + 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. @@ -761,27 +737,14 @@ public sealed class RetailPViewRenderer // Outdoor-cell unattached emitters drew in the landscape stage. passes.DrawUnattachedSceneParticles(ctx, outdoorCells: false); - if (entityFrameOpen) - { - frameEntityPasses!.CompleteEntityFrame(in frameView); - entityFrameOpen = false; - } _candidateObserver?.CompletePViewFrame(); - _sceneFrameProduct?.CompleteProduction(in frameView); return result; } catch { - if (entityFrameOpen) - frameEntityPasses!.AbortEntityFrame(); _candidateObserver?.AbortPViewFrame(); throw; } - finally - { - if (frameViewBorrowed) - _sceneFrameProduct!.Release(in frameView); - } } // R-A2: group the nearby building cells by BuildingId and run one per-building flood per group @@ -2408,6 +2371,27 @@ public sealed class RetailPViewRenderer || AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled || AcDream.App.Streaming.EntityVanishProbe.Enabled; + private static RenderFrameDiagnosticCounts WalkDiagnosticCounts( + RenderProjectionCounts source) + { + int dynamics = checked( + source.LiveDynamicRoot + + source.ActiveAnimatedStatic + + source.EquippedChild); + return new RenderFrameDiagnosticCounts( + source.OutdoorStatic, + source.IndoorCellStatic, + dynamics, + TransformCount: source.Total, + OpaqueClassificationCount: 0, + AlphaClassificationCount: 0, + LightSetCount: 0, + SelectionPartCount: 0, + RouteCandidateCount: source.Total, + EntityCandidateCount: source.Total, + MeshPartCount: 0); + } + internal static RenderFrameDiagnosticCounts LegacyDiagnosticCounts( InteriorEntityPartition.Result partition) { diff --git a/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs b/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs index 991c7b80..d59a1eec 100644 --- a/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs @@ -43,12 +43,7 @@ public sealed class FrameRootCompositionTests IReadOnlyList calls = CompiledCallGraph.Read(compose); int resources = CallIndex(calls, typeof(RenderFrameResourceController), ".ctor"); - int comparison = CallIndex( - calls, - typeof(RenderSceneShadowComparisonController), - ".ctor", - resources + 1); - int scene = CallIndex(calls, typeof(WorldSceneRenderer), ".ctor", comparison + 1); + int scene = CallIndex(calls, typeof(WorldSceneRenderer), ".ctor", resources + 1); int automation = CallIndex( calls, typeof(WorldLifecycleAutomationController), diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs index 48b7dd51..dbabad28 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs @@ -197,95 +197,6 @@ public sealed class RetailPViewPassExecutorTests }); } - [Fact] - public void DrawInside_production_product_supplies_the_ordered_entity_routes() - { - // Campaign FW3.2b-2: no cell-static entity here (adjudicated - // 2026-08-30) — CellStatic no longer flows through the packed - // product at all (WalkFrameDriver owns every cell static now, via - // OrderedDrawStream, not this route); a static entity registered on - // the LEGACY side with nothing on the packed side would report a - // permanent MismatchCount, which is not what this test is proving. - // The route-consumption-order assertion below still exercises the - // one route that DID survive the cutover (DynamicLast). - LoadedCell interior = InteriorWithExit(0xA9B40100u); - WorldEntity dynamic = Entity( - 31u, - serverGuid: 0x80000031u, - parentCellId: interior.CellId); - uint landblockId = interior.CellId & 0xFFFF0000u; - var oracle = new CurrentRenderSceneOracle(); - using var shadow = new RenderSceneShadowRuntime( - RenderSceneGeneration.FromRaw(1)); - RenderProjectionRecord dynamicRecord = - RenderProjectionRecordFactory.ProjectEntity( - LiveRenderProjectionJournal.ProjectionId(dynamic.Id), - RenderProjectionClass.LiveDynamicRoot, - RenderOwnerIncarnation.FromRaw(dynamic.Id), - landblockId, - interior.CellId, - dynamic, - spatiallyVisible: true); - shadow.Journal.Register(in dynamicRecord); - shadow.DrainUpdateBoundary(); - var product = new RenderScenePViewFrameProductController( - shadow, - oracle); - var renderer = new RetailPViewRenderer(oracle, product); - using var executor = new RecordingExecutor(); - - renderer.DrawInside( - Frame(interior, [dynamic]), - executor); - - AssertAppearsInOrder( - string.Join('|', executor.Operations), - "entity-frame-begin", - "entity-route:DynamicLast:0:00000000", - "entity-frame-complete"); - Assert.Equal(0, product.Snapshot.MismatchCount); - Assert.Equal( - product.Snapshot.ExpectedDigest, - product.Snapshot.ActualDigest); - } - - [Fact] - public void DrawInside_production_product_uses_retained_routes_without_legacy_partition_when_diagnostics_are_disabled() - { - LoadedCell interior = InteriorWithExit(0xA9B40100u); - WorldEntity dynamic = Entity( - 32u, - serverGuid: 0x80000032u, - parentCellId: interior.CellId); - uint landblockId = interior.CellId & 0xFFFF0000u; - using var shadow = new RenderSceneShadowRuntime( - RenderSceneGeneration.FromRaw(1)); - RenderProjectionRecord dynamicRecord = - RenderProjectionRecordFactory.ProjectEntity( - LiveRenderProjectionJournal.ProjectionId(dynamic.Id), - RenderProjectionClass.LiveDynamicRoot, - RenderOwnerIncarnation.FromRaw(dynamic.Id), - landblockId, - interior.CellId, - dynamic, - spatiallyVisible: true); - shadow.Journal.Register(in dynamicRecord); - shadow.DrainUpdateBoundary(); - var product = new RenderScenePViewFrameProductController(shadow); - var renderer = new RetailPViewRenderer(null, product); - using var executor = new RecordingExecutor(); - - renderer.DrawInside(Frame(interior, [dynamic]), executor); - - AssertAppearsInOrder( - string.Join('|', executor.Operations), - "entity-frame-begin", - "entity-route:DynamicLast:0:00000000", - "entity-frame-complete"); - Assert.Equal(0uL, product.Snapshot.ComparisonCount); - Assert.Equal(1, product.Snapshot.ActualCandidateCount); - } - [Fact] public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch() {