From 474b05e7dca99f7da405d4b55ecdf867d9607380 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 31 Aug 2026 03:44:32 +0200 Subject: [PATCH] refactor(render): move root-cell dynamics onto walk turns --- .../Scene/RenderScenePViewFrameProduct.cs | 15 +++++- .../Rendering/Walk/WalkFrameDriver.cs | 49 +++++++++---------- .../RenderScenePViewFrameProductTests.cs | 13 ++++- .../Rendering/Walk/WalkFrameDriverTests.cs | 6 ++- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs b/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs index 5937515e..135f7880 100644 --- a/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs +++ b/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs @@ -1354,7 +1354,9 @@ internal sealed class RenderScenePViewFrameBuilder RenderProjectionRecord record = _dynamics[i]; uint? parentCellId = ParentCell(in record); if (parentCellId is uint parentCell - && _lookInCellScratch.Contains(parentCell)) + && (_lookInCellScratch.Contains(parentCell) + || (input.WalkLookInViews is not null + && input.RootFloodCells.Contains(parentCell)))) continue; Sphere(in record, out Vector3 center, out float radius); bool outside = RetailPViewRenderer.DynamicDrawsInOutsideStage( @@ -1427,6 +1429,17 @@ internal sealed class RenderScenePViewFrameBuilder continue; } + // FW4: an active walk draws every root-flood cell's complete + // object list at that cell's own PView::DrawCells turn. The + // packed last pass must not duplicate those live objects after + // the walk has already established their correct wall depth. + if (indoor + && input.WalkLookInViews is not null + && input.RootFloodCells.Contains(parentCellId!.Value)) + { + continue; + } + // The stage-set split (synthesis plan step 4, [dyn-route]-pinned // 2026-08-30): an interior-parented dynamic rides the LAST pass // ONLY when its parent cell is in THE WALK'S OWN ROOT FLOOD. diff --git a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs index 6b08ee1b..4f5528da 100644 --- a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs +++ b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs @@ -518,7 +518,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource private bool _skyDrawnThisFrame; private WalkDrawStage? _currentDcStage; private bool _readyToReplay; - private int _lookInRouteIndex; + private int _cellViewRouteIndex; internal WalkFrameDriver( WbDrawDispatcher dispatcher, @@ -626,7 +626,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource VisitedBuildings.Clear(); VisitedLandscapeCellIds.Clear(); InteriorFloodCells.Clear(); - _lookInRouteIndex = 0; + _cellViewRouteIndex = 0; } /// Records the final segment mark (plan §FW3.2b-1's "at frame @@ -981,36 +981,35 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource _stream, stage, cellId, records.Records, records.TupleLandblockId, _cameraWorldPosition, _viewProjection); MarkIfGrown(); + + // PView::DrawObjCellForDummies draws this cell's live objects at the + // same turn as its statics. Capture the currently installed portal + // views before the walk pops them, then apply retail's per-GfxObj + // drawing-sphere viewcone check in the walk classifier. + int viewRouteIndex = _cellViewRouteIndex++; + CaptureCellViews(cellId); if (stage == WalkDrawStage.LookInStatic) { - // Retail draws a look-in cell's complete object list at this - // re-entrant DrawCells turn. The packed dynamic route used to run - // much later at the pre-clear boundary, after nearer building - // shells, which let the cathedral's 0x112 remote player overpaint - // opaque walls. Keep animation/fade in the packed route, but replay - // it here between this cell's content and the building shell. - int routeIndex = _lookInRouteIndex++; LookInCellTurns.Add(cellId); LookInCells.Add(cellId); - CaptureLookInViews(cellId); - - WalkFrameStaticRecords dynamics = _worldData.GetCellDynamics(cellId); - _populator.PopulateCellDynamics( - _stream, - cellId, - dynamics.Records, - dynamics.TupleLandblockId, - _cameraWorldPosition, - _viewProjection, - this, - routeIndex); - MarkIfGrown(); - if (HasAnyOwner(records) || HasAnyOwner(dynamics)) - _events.Add(WalkFrameEvent.CellParticles(cellId)); } + + WalkFrameStaticRecords dynamics = _worldData.GetCellDynamics(cellId); + _populator.PopulateCellDynamics( + _stream, + cellId, + dynamics.Records, + dynamics.TupleLandblockId, + _cameraWorldPosition, + _viewProjection, + this, + viewRouteIndex); + MarkIfGrown(); + if (HasAnyOwner(records) || HasAnyOwner(dynamics)) + _events.Add(WalkFrameEvent.CellParticles(cellId)); } - private void CaptureLookInViews(uint cellId) + private void CaptureCellViews(uint cellId) { IWalkBuildingFrameContext ctx = RequireOpenFrame(); WalkCell? cell = ctx.GetVisible(cellId); diff --git a/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs b/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs index 03780c03..6ac865a6 100644 --- a/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs @@ -269,7 +269,15 @@ public sealed class RenderScenePViewFrameProductTests 0x0100_0000_0000_0022, RenderProjectionClass.LiveDynamicRoot, parentCell: lookInCell); - scene.Apply([RenderProjectionDelta.Register(Generation, 1, lookInDynamic)]); + RenderProjectionRecord rootDynamic = Record( + 0x0100_0000_0000_0023, + RenderProjectionClass.LiveDynamicRoot, + parentCell: Cell); + scene.Apply( + [ + RenderProjectionDelta.Register(Generation, 1, lookInDynamic), + RenderProjectionDelta.Register(Generation, 2, rootDynamic), + ]); PortalVisibilityFrame portal = Portal(Cell); ClipFrameAssembly clip = FullScreenClip(Cell); @@ -298,6 +306,9 @@ public sealed class RenderScenePViewFrameProductTests Assert.DoesNotContain( frame.RouteRanges.ToArray(), candidate => candidate.Route == RenderFrameCandidateRoute.LookInObject); + Assert.DoesNotContain( + frame.RouteCandidates.ToArray(), + candidate => candidate.Id == rootDynamic.Id); Assert.NotNull(frame.WalkLookInViews); Assert.False(frame.WalkLookInViews!.SphereVisibleInLookInTurn( routeIndex: 0, diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs index 0e11944e..ceaab8d9 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs @@ -262,7 +262,8 @@ public sealed class WalkFrameDriverTests { "SKY", "TERRAIN:0", "CLEAR", "SEALS", "SHELL:00000101", "SHELL:00000100", - "FLUSH:1:CellStatic", "FLUSH:1:CellStatic", + "FLUSH:1:CellStatic", "CELL-PARTICLES:00000101:66", + "FLUSH:1:CellStatic", "CELL-PARTICLES:00000100:65", }, log); @@ -336,7 +337,8 @@ public sealed class WalkFrameDriverTests new[] { "CLEAR", "SEALS", "SHELL:00000101", "SHELL:00000100", - "FLUSH:1:CellStatic", "FLUSH:1:CellStatic", + "FLUSH:1:CellStatic", "CELL-PARTICLES:00000101:66", + "FLUSH:1:CellStatic", "CELL-PARTICLES:00000100:65", }, log);