fix(render): FW4 slice 4 - straddler particles emit pre-clear, once
The cathedral falls survived slices 2 and 3; the [walk-dyn] dump then
pinned the real gate: the falls emitters are interior-parented
exit-plane STRADDLERS that pass BOTH the outside-stage predicate and
the viewcone (outside=1 cone=1) - they were in the pre-clear union all
along, and the deliberate ExceptRoute(DynamicLast) stripped them so
they emitted in the post-clear last pass instead ("straddlers emit
HERE"). That placement protects an indoor flame from interior repaint,
but a straddling emitter whose particles hang OUTSIDE the exit portal
has no depth left to occlude them after the clear - they splat across
terrain and water.
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.
Under an interior root the late-union now keeps the outside-stage
owners (straddlers included), records them, and BOTH last-pass arms
subtract the recorded set so every owner emits exactly once. Outdoor
roots keep the old except-at-union placement: both stages drain at the
same final flush there, so the last-pass emission is already correct
and a pre-emission would double-composite.
Hermetic 6,762/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5f7ccdeab5
commit
f3a03efc81
1 changed files with 34 additions and 3 deletions
|
|
@ -71,6 +71,16 @@ 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();
|
||||
|
||||
// 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
|
||||
|
|
@ -155,6 +165,7 @@ public sealed class RetailPViewRenderer
|
|||
passes.BeginFrame();
|
||||
RecycleLookInFrames();
|
||||
ResetBuildingGroups();
|
||||
_preClearParticleOwnerScratch.Clear();
|
||||
|
||||
var pvFrame = PortalVisibilityBuilder.Build(
|
||||
ctx.RootCell,
|
||||
|
|
@ -1493,13 +1504,26 @@ public sealed class RetailPViewRenderer
|
|||
});
|
||||
}
|
||||
|
||||
// Late-particle union submission — DynamicLast owners excluded, same
|
||||
// as DrawLandscapeThroughOutsideView's own final submission.
|
||||
if (frameEntityPasses is not null)
|
||||
// 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)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.ExceptRoute(
|
||||
_staticParticleUnionScratch, in frameView, RenderFrameCandidateRoute.DynamicLast);
|
||||
}
|
||||
if (!ctx.RootCell.IsOutdoorNode)
|
||||
_preClearParticleOwnerScratch.UnionWith(_staticParticleUnionScratch);
|
||||
if (_staticParticleUnionScratch.Count > 0)
|
||||
{
|
||||
passes.DrawLandscapeStaticParticles(
|
||||
|
|
@ -2157,6 +2181,10 @@ 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(
|
||||
|
|
@ -2291,6 +2319,9 @@ 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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue