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

@ -161,6 +161,10 @@ 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];
@ -247,6 +251,22 @@ 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));