revert(render): FW4 slices 4+5 - unanimous tri-review verdict

Reverts 0d6cd5c0 (slice 5) and f3a03efc (slice 4). All three
independent cathedral reviews (fable, opus, external) plus the DAT
geometry ground truth condemned both:

- Slice 5 misidentified its targets: owners 0x4F418012-15 are cell
  0xF4180106''s TORCHES + a prop + a plant (InteriorEntityIdAllocator
  namespace), not the falls - the real waterfall emitters are outdoor
  landblock objects in the 0xC namespace (0xCF418000-13). Moving torch
  flames pre-clear is the owner-reported in-cathedral particle
  regression.
- Slice 4''s straddle rule is invented (retail stages by actual
  shadow-cell membership), and its "emit once" contradicts retail''s
  deliberate mid-frame m_nFrameStamp re-arm @0x005a4886 (a part
  overlapping both scopes legitimately draws twice).

The synthesis of all seven review reports and the adjudicated fix plan
live in docs/research/2026-08-30-cathedral-synthesis.md (next commit).

Hermetic 6,762/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 20:50:52 +02:00
parent bb9212c70d
commit 7f500b97a3
3 changed files with 3 additions and 142 deletions

View file

@ -161,10 +161,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
private readonly List<MeshParticleInstance> _meshRunScratch = new(64); private readonly List<MeshParticleInstance> _meshRunScratch = new(64);
private readonly List<ParticleSubmission> _submissionScratch = new(128); private readonly List<ParticleSubmission> _submissionScratch = new(128);
private readonly List<RuntimeParticleEmitter> _scopedEmitterScratch = new(64); private readonly List<RuntimeParticleEmitter> _scopedEmitterScratch = new(64);
// ACDREAM_PROBE_WALK_ROOT companion (throwaway): rate limiter for the
// [walk-emit] interior-unattached emitter dump.
private uint _probeEmitterDumpCounter;
private readonly List<DeferredParticleDraw> _deferredAlpha = new(128); private readonly List<DeferredParticleDraw> _deferredAlpha = new(128);
private DeferredParticleDraw[] _preparedAlpha = new DeferredParticleDraw[256]; private DeferredParticleDraw[] _preparedAlpha = new DeferredParticleDraw[256];
private uint[] _preparedInstanceOffsets = new uint[256]; private uint[] _preparedInstanceOffsets = new uint[256];
@ -251,22 +247,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
_scopedEmitterScratch, _scopedEmitterScratch,
excludedAttachedOwnerIds, excludedAttachedOwnerIds,
unattachedCellScope); unattachedCellScope);
// ACDREAM_PROBE_WALK_ROOT companion (throwaway): identify WHICH
// emitters ride the interior-unattached route (the falls hunt) —
// owner cell, anchor position, attachment.
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled
&& unattachedCellScope == UnattachedEmitterCellScope.InteriorCells
&& ++_probeEmitterDumpCounter % 120 == 0)
{
foreach (var em in _scopedEmitterScratch)
{
Console.WriteLine(
$"[walk-emit] cell={em.OwnerCellId:x8} attached={em.AttachedObjectId:x} "
+ $"anchor=({em.AnchorPos.X:F1},{em.AnchorPos.Y:F1},{em.AnchorPos.Z:F1}) "
+ $"active={em.ActiveCount} "
+ $"phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}");
}
}
Matrix4x4.Invert(camera.View, out Matrix4x4 invView); Matrix4x4.Invert(camera.View, out Matrix4x4 invView);
Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13)); Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13));
Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23)); Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23));

View file

@ -71,28 +71,6 @@ public sealed class RetailPViewRenderer
private readonly HashSet<uint> _cellParticleOwnerScratch = new(); private readonly HashSet<uint> _cellParticleOwnerScratch = new();
private readonly HashSet<uint> _dynamicParticleOwnerScratch = new(); private readonly HashSet<uint> _dynamicParticleOwnerScratch = new();
// FW4 slice 4: owners whose particles ALREADY submitted in the
// pre-clear outside stage this frame (interior roots only). The last
// pass subtracts them — retail submits an emitter's polys at its
// object's FIRST draw (the landscape stage for anything overlapping
// outdoor shadow cells) and drains them at the pre-clear boundary
// flush over true landscape depth; a straddler re-emitted post-clear
// has no outdoor depth left to occlude it (the cathedral falls
// 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/ // MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary // Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
// + 2 Lists, plus one List<WorldEntity> per visible cell) every DrawInside // + 2 Lists, plus one List<WorldEntity> per visible cell) every DrawInside
@ -177,7 +155,6 @@ public sealed class RetailPViewRenderer
passes.BeginFrame(); passes.BeginFrame();
RecycleLookInFrames(); RecycleLookInFrames();
ResetBuildingGroups(); ResetBuildingGroups();
_preClearParticleOwnerScratch.Clear();
var pvFrame = PortalVisibilityBuilder.Build( var pvFrame = PortalVisibilityBuilder.Build(
ctx.RootCell, ctx.RootCell,
@ -366,15 +343,9 @@ public sealed class RetailPViewRenderer
walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals); walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
walkDriver = new Walk.WalkFrameDriver(walkExecutor!.Dispatcher, leafRenderer, _walkWorldData); walkDriver = new Walk.WalkFrameDriver(walkExecutor!.Dispatcher, leafRenderer, _walkWorldData);
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame =
_probeWalkRootFrame % 90 == 0;
}
walkDriver.Collect( walkDriver.Collect(
_frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext, _frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext,
ctx.ViewProjection, ctx.CameraWorldPosition); ctx.ViewProjection, ctx.CameraWorldPosition);
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame = false;
// FW4 slice 1: an interior root's terrain/sky/punch clip slices // FW4 slice 1: an interior root's terrain/sky/punch clip slices
// come from THE WALK'S OWN outside_view — retail's one // come from THE WALK'S OWN outside_view — retail's one
@ -594,37 +565,6 @@ 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) if (walkActive)
{ {
// Campaign FW3.2b-2: the walk owns every static draw — // Campaign FW3.2b-2: the walk owns every static draw —
@ -657,17 +597,6 @@ public sealed class RetailPViewRenderer
// BEFORE the pre-clear drain so they composite against // BEFORE the pre-clear drain so they composite against
// still-true landscape depth, never the seals. // still-true landscape depth, never the seals.
SubmitWalkLandscapeStaticParticles(ctx, walkExecutor!, capturedDriver); 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(); passes.UseIndoorMembershipOnlyRouting();
DrawLandscapeDynamicsPhase( DrawLandscapeDynamicsPhase(
ctx, ctx,
@ -1463,10 +1392,6 @@ public sealed class RetailPViewRenderer
continue; continue;
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch); 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) if (_cellParticleOwnerScratch.Count > 0)
{ {
passes.DrawCellParticles( passes.DrawCellParticles(
@ -1568,26 +1493,13 @@ public sealed class RetailPViewRenderer
}); });
} }
// Late-particle union submission. OUTDOOR root: DynamicLast owners // Late-particle union submission — DynamicLast owners excluded, same
// excluded (both stages drain at the same final flush there — a // as DrawLandscapeThroughOutsideView's own final submission.
// duplicate submission would double-composite), matching if (frameEntityPasses is not null)
// DrawLandscapeThroughOutsideView's own final submission. INTERIOR
// root (FW4 slice 4): the outside-stage owners — exit-plane
// STRADDLERS included — submit HERE, pre-clear, and the last pass
// subtracts them instead: retail submits an emitter's polys at its
// object's FIRST draw (the landscape stage) and drains them at the
// pre-clear boundary flush over true landscape depth; the former
// except-here/emit-last placement left a straddler's outside half
// with no depth to occlude it after the clear (the cathedral falls
// bleeding through terrain and water, probe-pinned:
// outside=1 cone=1 yet phase=post).
if (frameEntityPasses is not null && ctx.RootCell.IsOutdoorNode)
{ {
RenderFrameRouteOwnerSelector.ExceptRoute( RenderFrameRouteOwnerSelector.ExceptRoute(
_staticParticleUnionScratch, in frameView, RenderFrameCandidateRoute.DynamicLast); _staticParticleUnionScratch, in frameView, RenderFrameCandidateRoute.DynamicLast);
} }
if (!ctx.RootCell.IsOutdoorNode)
_preClearParticleOwnerScratch.UnionWith(_staticParticleUnionScratch);
if (_staticParticleUnionScratch.Count > 0) if (_staticParticleUnionScratch.Count > 0)
{ {
passes.DrawLandscapeStaticParticles( passes.DrawLandscapeStaticParticles(
@ -2245,10 +2157,6 @@ public sealed class RetailPViewRenderer
RenderFrameCandidateRoute.DynamicLast, RenderFrameCandidateRoute.DynamicLast,
0, 0,
0); 0);
// FW4 slice 4: owners already emitted pre-clear (the outside
// stage's late union — straddlers included) emit ONCE; see
// _preClearParticleOwnerScratch's field comment.
_dynamicParticleOwnerScratch.ExceptWith(_preClearParticleOwnerScratch);
passes.UseIndoorMembershipOnlyRouting(); passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy( DrawEntityRouteOrLegacy(
@ -2383,9 +2291,6 @@ public sealed class RetailPViewRenderer
if (InteriorEntityPartition.IsIndoorCellId(e.ParentCellId)) if (InteriorEntityPartition.IsIndoorCellId(e.ParentCellId))
_dynamicParticleOwnerScratch.Add(e.Id); _dynamicParticleOwnerScratch.Add(e.Id);
} }
// FW4 slice 4: pre-clear-emitted owners (straddlers) emit once —
// see _preClearParticleOwnerScratch's field comment.
_dynamicParticleOwnerScratch.ExceptWith(_preClearParticleOwnerScratch);
if (_dynamicParticleOwnerScratch.Count > 0) if (_dynamicParticleOwnerScratch.Count > 0)
passes.DrawDynamicsParticles(ctx, _dynamicParticleOwnerScratch); passes.DrawDynamicsParticles(ctx, _dynamicParticleOwnerScratch);
} }
@ -2681,16 +2586,6 @@ public sealed class RetailPViewRenderer
if (cell is null) if (cell is null)
return false; 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); var localC = Vector3.Transform(sphereCenter, cell.InverseWorldTransform);
int n = Math.Min(cell.Portals.Count, cell.ClipPlanes.Count); int n = Math.Min(cell.Portals.Count, cell.ClipPlanes.Count);
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)

View file

@ -119,14 +119,6 @@ public sealed class WalkPView
flags.InView = true; // portal polygon faces the viewer flags.InView = true; // portal polygon faces the viewer
} }
} }
if (AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame)
{
Console.WriteLine(
$"[walk-portal] init cell={cell.CellId:x8} i={i} "
+ $"dest={cell.Portals[i].OtherCellId:x8} d={d:F3} "
+ $"pside={cell.Portals[i].PortalSide} "
+ $"inview={(flags.InView ? 1 : 0)}");
}
} }
if (flags.InView) if (flags.InView)
@ -209,12 +201,6 @@ public sealed class WalkPView
cell, portal.PortalSide, cell, portal.PortalSide,
cell.PortalPolygons[portal.PolygonIndex], cell.PortalPolygons[portal.PolygonIndex],
doClip: true, ctx, _clipScratch); doClip: true, ctx, _clipScratch);
if (AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame)
{
Console.WriteLine(
$"[walk-portal] clip cell={cell.CellId:x8} view={i} j={j} "
+ $"dest={portal.OtherCellId:x8} n={n}");
}
if (n == 0) continue; if (n == 0) continue;
if (portal.OtherCellId == 0xFFFFFFFFu) if (portal.OtherCellId == 0xFFFFFFFFu)