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> _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();
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -155,6 +165,7 @@ 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,
|
||||||
|
|
@ -1493,13 +1504,26 @@ public sealed class RetailPViewRenderer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Late-particle union submission — DynamicLast owners excluded, same
|
// Late-particle union submission. OUTDOOR root: DynamicLast owners
|
||||||
// as DrawLandscapeThroughOutsideView's own final submission.
|
// excluded (both stages drain at the same final flush there — a
|
||||||
if (frameEntityPasses is not null)
|
// 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(
|
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(
|
||||||
|
|
@ -2157,6 +2181,10 @@ 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(
|
||||||
|
|
@ -2291,6 +2319,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue