revert(render): FW4 slices 4+5 - unanimous tri-review verdict
Reverts0d6cd5c0(slice 5) andf3a03efc(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:
parent
bb9212c70d
commit
7f500b97a3
3 changed files with 3 additions and 142 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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++)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue