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<ParticleSubmission> _submissionScratch = new(128);
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 DeferredParticleDraw[] _preparedAlpha = new DeferredParticleDraw[256];
private uint[] _preparedInstanceOffsets = new uint[256];
@ -251,22 +247,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
_scopedEmitterScratch,
excludedAttachedOwnerIds,
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);
Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13));
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> _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/
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
// + 2 Lists, plus one List<WorldEntity> per visible cell) every DrawInside
@ -177,7 +155,6 @@ public sealed class RetailPViewRenderer
passes.BeginFrame();
RecycleLookInFrames();
ResetBuildingGroups();
_preClearParticleOwnerScratch.Clear();
var pvFrame = PortalVisibilityBuilder.Build(
ctx.RootCell,
@ -366,15 +343,9 @@ 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
@ -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)
{
// 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
// 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,
@ -1463,10 +1392,6 @@ 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(
@ -1568,26 +1493,13 @@ public sealed class RetailPViewRenderer
});
}
// Late-particle union submission. OUTDOOR root: DynamicLast owners
// excluded (both stages drain at the same final flush there — a
// duplicate submission would double-composite), matching
// 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)
// Late-particle union submission — DynamicLast owners excluded, same
// as DrawLandscapeThroughOutsideView's own final submission.
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.ExceptRoute(
_staticParticleUnionScratch, in frameView, RenderFrameCandidateRoute.DynamicLast);
}
if (!ctx.RootCell.IsOutdoorNode)
_preClearParticleOwnerScratch.UnionWith(_staticParticleUnionScratch);
if (_staticParticleUnionScratch.Count > 0)
{
passes.DrawLandscapeStaticParticles(
@ -2245,10 +2157,6 @@ public sealed class RetailPViewRenderer
RenderFrameCandidateRoute.DynamicLast,
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();
DrawEntityRouteOrLegacy(
@ -2383,9 +2291,6 @@ public sealed class RetailPViewRenderer
if (InteriorEntityPartition.IsIndoorCellId(e.ParentCellId))
_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)
passes.DrawDynamicsParticles(ctx, _dynamicParticleOwnerScratch);
}
@ -2681,16 +2586,6 @@ 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++)

View file

@ -119,14 +119,6 @@ public sealed class WalkPView
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)
@ -209,12 +201,6 @@ public sealed class WalkPView
cell, portal.PortalSide,
cell.PortalPolygons[portal.PolygonIndex],
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 (portal.OtherCellId == 0xFFFFFFFFu)