fix(render): DynamicLast admits only the walk''s own root flood

The [dyn-route] trace pinned the through-wall remote player (visible
from 0xF4180101 AND outdoors while parented at 0xF4180112 - a cell
with no retail sightline chain from either): the legacy visibility
builder invents cross-building cell views at the cathedral (REAL
3-5-plane cones for 0x112, not the zero-plane trapdoor), the viewcone
admits his sphere, and he rides the last dynamics pass - post-clear on
interior roots (walls'' depth wiped) and post-world outdoors.

The stage-set split (synthesis plan step 4): an interior-parented
dynamic may ride DynamicLast ONLY when its parent cell is in THE
WALK''S OWN ROOT FLOOD (oracle-trace-conformant; retail draws look-in
occupants inside the landscape stage through the composed portal
chain, and unreachable cells'' occupants not at all). The frame
product''s build input gains RootFloodCells (the driver''s
InteriorFloodCells as a per-frame set); non-walk/diagnostic frames and
the comparison wrapper keep the legacy drawableCells meaning. The
[dyn-route] probe logs the new rootflood-excluded state.

Hermetic 6,762/0; InstalledDat walk conformance 40/1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 22:13:48 +02:00
parent 4686a1ac74
commit fc2e6b79bc
3 changed files with 61 additions and 7 deletions

View file

@ -71,6 +71,10 @@ public sealed class RetailPViewRenderer
private readonly HashSet<uint> _cellParticleOwnerScratch = new();
private readonly HashSet<uint> _dynamicParticleOwnerScratch = new();
// The walk's TRUE root flood as a set, rebuilt per frame for the
// DynamicLast stage gate (the stage-set split — synthesis plan step 4).
private readonly HashSet<uint> _rootFloodSetScratch = new();
// MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
// + 2 Lists, plus one List<WorldEntity> per visible cell) every DrawInside
@ -507,6 +511,16 @@ public sealed class RetailPViewRenderer
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);
}
if (_sceneFrameProduct is not null)
{
frameView = _sceneFrameProduct.BuildAndBorrow(
@ -517,7 +531,11 @@ public sealed class RetailPViewRenderer
outsideStageFlood,
ctx.Cells,
ctx.AnimatedEntityIds,
ctx.RootCell.IsOutdoorNode);
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;

View file

@ -13,7 +13,8 @@ internal readonly record struct RenderScenePViewBuildInput(
HashSet<uint> DrawableCells,
IRetailPViewCellSource Cells,
HashSet<uint>? AnimatedEntityIds,
bool RootIsOutdoor);
bool RootIsOutdoor,
IReadOnlySet<uint> RootFloodCells);
internal readonly record struct RenderFrameProductComparisonSnapshot(
bool Enabled,
@ -188,7 +189,8 @@ internal sealed class RenderScenePViewFrameProductController :
HashSet<uint> drawableCells,
IRetailPViewCellSource cells,
HashSet<uint>? animatedEntityIds,
bool rootIsOutdoor)
bool rootIsOutdoor,
IReadOnlySet<uint> rootFloodCells)
{
ArgumentNullException.ThrowIfNull(portalFrame);
ArgumentNullException.ThrowIfNull(clipAssembly);
@ -215,7 +217,8 @@ internal sealed class RenderScenePViewFrameProductController :
drawableCells,
cells,
animatedEntityIds,
rootIsOutdoor);
rootIsOutdoor,
rootFloodCells);
_builder.Build(_exchange, frameSequence, in input);
return _exchange.BorrowLatest(
scene.Generation,
@ -235,8 +238,12 @@ internal sealed class RenderScenePViewFrameProductController :
HashSet<uint>? animatedEntityIds,
uint tupleLandblockId,
bool rootIsOutdoor,
Vector3 cameraWorldPosition = default)
Vector3 cameraWorldPosition = default,
IReadOnlySet<uint>? rootFloodCells = null)
{
// Comparison/diagnostic path: absent an explicit walk root flood,
// drawableCells keeps the pre-stage-split admission (drawableCells
// WAS the flood in the legacy meaning these fixtures pin).
RenderFrameView view = BuildAndBorrow(
portalFrame,
clipAssembly,
@ -245,7 +252,8 @@ internal sealed class RenderScenePViewFrameProductController :
drawableCells,
cells,
animatedEntityIds,
rootIsOutdoor);
rootIsOutdoor,
rootFloodCells ?? drawableCells);
try
{
Compare(
@ -1417,6 +1425,33 @@ internal sealed class RenderScenePViewFrameBuilder
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.
// Retail draws a look-in cell's occupants inside the landscape
// stage through the composed portal chain, and an unreachable
// cell's occupants not at all — the legacy visibility builder
// invents cross-building cell views at the cathedral (real
// 3-5-plane cones for 0xF4180112 from roots with no sightline),
// which let a remote player draw post-clear/post-world through
// opaque walls. The walk's flood is oracle-trace-conformant;
// key the route on it.
if (indoor && !input.RootFloodCells.Contains(parentCellId!.Value))
{
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{
string state = $"parent={parentCellId.Value:x8} rootflood-excluded";
if (!_probeDynRouteStates.TryGetValue(record.Source.LocalEntityId, out string? prev)
|| prev != state)
{
_probeDynRouteStates[record.Source.LocalEntityId] = state;
Console.WriteLine(
$"[dyn-route] id={record.Source.LocalEntityId:x} {state}");
}
}
continue;
}
Sphere(in record, out Vector3 center, out float radius);
bool visible = indoor
? input.Viewcone.SphereVisibleInCell(

View file

@ -162,7 +162,8 @@ public sealed class RenderScenePViewFrameProductTests
[Cell],
EmptyCellSource.Instance,
[],
RootIsOutdoor: true);
RootIsOutdoor: true,
RootFloodCells: new HashSet<uint> { Cell });
builder.Build(exchange, frameSequence, in input);
}
}