refactor(render): move root-cell dynamics onto walk turns

This commit is contained in:
Erik 2026-08-31 03:44:32 +02:00
parent f9a80fbc44
commit 474b05e7dc
4 changed files with 54 additions and 29 deletions

View file

@ -1354,7 +1354,9 @@ internal sealed class RenderScenePViewFrameBuilder
RenderProjectionRecord record = _dynamics[i]; RenderProjectionRecord record = _dynamics[i];
uint? parentCellId = ParentCell(in record); uint? parentCellId = ParentCell(in record);
if (parentCellId is uint parentCell if (parentCellId is uint parentCell
&& _lookInCellScratch.Contains(parentCell)) && (_lookInCellScratch.Contains(parentCell)
|| (input.WalkLookInViews is not null
&& input.RootFloodCells.Contains(parentCell))))
continue; continue;
Sphere(in record, out Vector3 center, out float radius); Sphere(in record, out Vector3 center, out float radius);
bool outside = RetailPViewRenderer.DynamicDrawsInOutsideStage( bool outside = RetailPViewRenderer.DynamicDrawsInOutsideStage(
@ -1427,6 +1429,17 @@ internal sealed class RenderScenePViewFrameBuilder
continue; 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 // The stage-set split (synthesis plan step 4, [dyn-route]-pinned
// 2026-08-30): an interior-parented dynamic rides the LAST pass // 2026-08-30): an interior-parented dynamic rides the LAST pass
// ONLY when its parent cell is in THE WALK'S OWN ROOT FLOOD. // ONLY when its parent cell is in THE WALK'S OWN ROOT FLOOD.

View file

@ -518,7 +518,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
private bool _skyDrawnThisFrame; private bool _skyDrawnThisFrame;
private WalkDrawStage? _currentDcStage; private WalkDrawStage? _currentDcStage;
private bool _readyToReplay; private bool _readyToReplay;
private int _lookInRouteIndex; private int _cellViewRouteIndex;
internal WalkFrameDriver( internal WalkFrameDriver(
WbDrawDispatcher dispatcher, WbDrawDispatcher dispatcher,
@ -626,7 +626,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
VisitedBuildings.Clear(); VisitedBuildings.Clear();
VisitedLandscapeCellIds.Clear(); VisitedLandscapeCellIds.Clear();
InteriorFloodCells.Clear(); InteriorFloodCells.Clear();
_lookInRouteIndex = 0; _cellViewRouteIndex = 0;
} }
/// <summary>Records the final segment mark (plan §FW3.2b-1's "at frame /// <summary>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, _stream, stage, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection); _cameraWorldPosition, _viewProjection);
MarkIfGrown(); 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) 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); LookInCellTurns.Add(cellId);
LookInCells.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(); IWalkBuildingFrameContext ctx = RequireOpenFrame();
WalkCell? cell = ctx.GetVisible(cellId); WalkCell? cell = ctx.GetVisible(cellId);

View file

@ -269,7 +269,15 @@ public sealed class RenderScenePViewFrameProductTests
0x0100_0000_0000_0022, 0x0100_0000_0000_0022,
RenderProjectionClass.LiveDynamicRoot, RenderProjectionClass.LiveDynamicRoot,
parentCell: lookInCell); 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); PortalVisibilityFrame portal = Portal(Cell);
ClipFrameAssembly clip = FullScreenClip(Cell); ClipFrameAssembly clip = FullScreenClip(Cell);
@ -298,6 +306,9 @@ public sealed class RenderScenePViewFrameProductTests
Assert.DoesNotContain( Assert.DoesNotContain(
frame.RouteRanges.ToArray(), frame.RouteRanges.ToArray(),
candidate => candidate.Route == RenderFrameCandidateRoute.LookInObject); candidate => candidate.Route == RenderFrameCandidateRoute.LookInObject);
Assert.DoesNotContain(
frame.RouteCandidates.ToArray(),
candidate => candidate.Id == rootDynamic.Id);
Assert.NotNull(frame.WalkLookInViews); Assert.NotNull(frame.WalkLookInViews);
Assert.False(frame.WalkLookInViews!.SphereVisibleInLookInTurn( Assert.False(frame.WalkLookInViews!.SphereVisibleInLookInTurn(
routeIndex: 0, routeIndex: 0,

View file

@ -262,7 +262,8 @@ public sealed class WalkFrameDriverTests
{ {
"SKY", "TERRAIN:0", "CLEAR", "SEALS", "SKY", "TERRAIN:0", "CLEAR", "SEALS",
"SHELL:00000101", "SHELL:00000100", "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); log);
@ -336,7 +337,8 @@ public sealed class WalkFrameDriverTests
new[] new[]
{ {
"CLEAR", "SEALS", "SHELL:00000101", "SHELL:00000100", "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); log);