fix(render): FW4 slice 5 - straddling cell statics'' particles emit pre-clear

The cathedral falls'' true identity, pinned by the emitter/owner dumps
and the owner''s retail-vs-ACE oracle (retail renders them perfectly):
they are EnvCell STAB-LIST statics (interior-cell dat objects, ids
minted by InteriorEntityIdAllocator) whose geometry hangs out over the
lake. Our cell-statics route submitted every interior flood cell''s
static particle owners POST-clear ("walls own depth") - true for
content that stays inside, but a straddler''s outside half meets
seal/cleared depth and splats across the vista.

Retail''s mechanism is the outdoor shadow-cell draw: an object
overlapping outdoor landcells draws during LScape::draw (DrawBlock
pc:430056-430064, once per overlapped shadow cell), so its particles
submit in the landscape scope and drain at the pre-clear boundary
flush over true terrain depth; the cell''s own turn skips them
(drawn-once). This slice ports that rule for particles: after Collect,
the interior flood cells'' static records are classified by the shared
exit-plane straddle test (extracted as SphereStraddlesExitPlane from
the #118 dynamic split); straddlers'' owners submit in the pre-clear
closure and the post-replay cell-owners union subtracts them, so every
owner emits exactly once. Also adds the [walk-emit] probe dump that
falsified the unattached-route theory.

Hermetic 6,762/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 19:58:04 +02:00
parent f3a03efc81
commit 0d6cd5c06e
3 changed files with 108 additions and 0 deletions

View file

@ -81,6 +81,18 @@ public sealed class RetailPViewRenderer
// shine-through, fourth surface).
private readonly HashSet<uint> _preClearParticleOwnerScratch = new();
// FW4 slice 5: interior flood-cell STATICS whose bounds straddle their
// cell's exit-portal plane (the cathedral falls — EnvCell stab-list
// objects hanging out over the lake). Retail draws such an object
// during LScape::draw via the outdoor shadow-cell lists (DrawBlock
// pc:430056-430064, once per overlapped shadow cell), so its particles
// submit in the landscape scope and drain at the pre-clear boundary
// flush over true terrain depth; the cell's own post-clear turn skips
// them (drawn-once). Computed after Collect from the walk's flood;
// submitted by the pre-clear closure; subtracted from the post-replay
// cell-owners union.
private readonly HashSet<uint> _preClearCellStaticOwnerScratch = 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
@ -354,9 +366,15 @@ public sealed class RetailPViewRenderer
walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
walkDriver = new Walk.WalkFrameDriver(walkExecutor!.Dispatcher, leafRenderer, _walkWorldData);
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame =
_probeWalkRootFrame % 90 == 0;
}
walkDriver.Collect(
_frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext,
ctx.ViewProjection, ctx.CameraWorldPosition);
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame = false;
// FW4 slice 1: an interior root's terrain/sky/punch clip slices
// come from THE WALK'S OWN outside_view — retail's one
@ -576,6 +594,37 @@ public sealed class RetailPViewRenderer
}
}
// FW4 slice 5: classify the interior flood cells' STATICS by the
// exit-plane straddle test — a straddler's particles (the
// cathedral falls: EnvCell stab objects hanging over the lake)
// submit pre-clear like retail's outdoor shadow-cell draw; see
// _preClearCellStaticOwnerScratch's field comment. GetCellStatics
// is per-frame cached, so this also pre-warms the post-replay
// cell-owners pass.
_preClearCellStaticOwnerScratch.Clear();
if (walkActive && !ctx.RootCell.IsOutdoorNode)
{
foreach (uint cellId in walkDriver!.VisitedCells)
{
if ((cellId & 0xFFFFu) < 0x100u || _lookInCellIds.Contains(cellId))
continue;
LoadedCell? staticCell = ctx.Cells.Find(cellId);
if (staticCell is null)
continue;
Walk.WalkFrameStaticRecords cellRecords =
_walkWorldData!.GetCellStatics(cellId);
foreach (RenderProjectionRecord record in cellRecords.Records)
{
if (record.Source.LocalEntityId == 0)
continue;
Vector3 c = (record.Bounds.Minimum + record.Bounds.Maximum) * 0.5f;
float r = (record.Bounds.Maximum - record.Bounds.Minimum).Length() * 0.5f;
if (SphereStraddlesExitPlane(staticCell, c, r))
_preClearCellStaticOwnerScratch.Add(record.Source.LocalEntityId);
}
}
}
if (walkActive)
{
// Campaign FW3.2b-2: the walk owns every static draw —
@ -608,6 +657,17 @@ public sealed class RetailPViewRenderer
// BEFORE the pre-clear drain so they composite against
// still-true landscape depth, never the seals.
SubmitWalkLandscapeStaticParticles(ctx, walkExecutor!, capturedDriver);
// FW4 slice 5: straddling flood-cell statics' emitters
// (the falls) — retail's shadow-cell landscape draw.
if (_preClearCellStaticOwnerScratch.Count > 0)
{
_preClearParticleOwnerScratch.UnionWith(
_preClearCellStaticOwnerScratch);
passes.DrawLandscapeStaticParticles(
ctx,
new RetailPViewLandscapeStaticParticleContext(
_preClearCellStaticOwnerScratch));
}
passes.UseIndoorMembershipOnlyRouting();
DrawLandscapeDynamicsPhase(
ctx,
@ -1403,6 +1463,10 @@ public sealed class RetailPViewRenderer
continue;
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch);
}
// FW4 slice 5: exit-plane straddlers (the falls) already emitted at
// the pre-clear boundary — retail's shadow-cell landscape draw;
// every owner emits exactly once.
_cellParticleOwnerScratch.ExceptWith(_preClearCellStaticOwnerScratch);
if (_cellParticleOwnerScratch.Count > 0)
{
passes.DrawCellParticles(
@ -2617,6 +2681,16 @@ public sealed class RetailPViewRenderer
if (cell is null)
return false;
return SphereStraddlesExitPlane(cell, sphereCenter, sphereRadius);
}
/// <summary>The exit-plane straddle test shared by the #118 dynamic
/// stage split and the FW4 slice-5 cell-static particle split — retail
/// draws a straddling object once per overlapped outdoor shadow cell
/// inside LScape::draw (DrawBlock pc:430056-430064).</summary>
internal static bool SphereStraddlesExitPlane(
LoadedCell cell, Vector3 sphereCenter, float sphereRadius)
{
var localC = Vector3.Transform(sphereCenter, cell.InverseWorldTransform);
int n = Math.Min(cell.Portals.Count, cell.ClipPlanes.Count);
for (int i = 0; i < n; i++)