fix(render): drawn-once look-in dynamics + farther-than-building alpha drains
Two retail-parity corrections on top of 684380d4, user-gated together
(no regressions; door/creature-through-opening rendering verified live
at Holtburg; cathedral waterfall and look-north casts pass):
1. Drawn-once look-in dynamics. Retail marks every drawn non-player
part for the frame (DrawMeshInternal @0x0059F360 GetDrawnThisFrame), so
an object draws once, with its cell. acdream drew a look-in cell's
dynamics twice under an outdoor root - once correctly with the look-in,
then again in dynamics-last after the boundary alpha drain, where the
second draw overpainted nearer flames. Both the accepted path
(_lookInCellIds) and the frame product (BuildDynamicLastRoute) now
exclude dynamics whose parent cell drew as a look-in.
2. Pre/inter-building barriers drain only content FARTHER than the
building they precede (FlushLandscapeAlphaFartherThan +
RetailAlphaQueue.FlushFartherThan + conservative anchor-origin
threshold). Retail's far-to-near walk guarantees a building's
FlushAlphaList(0f) @0x0059F2A0 has only farther content queued; a
nearer emitter composites at a later flush. AP-236 documents the
remaining barrier-order divergence.
The #132 candle-before-door overdraw is NOT yet fixed by these steps
and stays open: the current suspect is that houses without a
constructed look-in still draw their interior-parented door in
dynamics-last after the outdoor candle's boundary drain (see the
2026-08-29 ledger for the retail flush-after-objects hypothesis).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
684380d421
commit
fc30285fd7
3 changed files with 84 additions and 13 deletions
|
|
@ -34,6 +34,13 @@ public sealed class RetailPViewRenderer
|
|||
private readonly HashSet<uint> _staticParticleUnionScratch = new();
|
||||
private readonly HashSet<uint> _cellParticleUnionScratch = new();
|
||||
|
||||
// Every cell drawn as a building look-in this frame. Retail marks each
|
||||
// drawn non-player part for the frame (DrawMeshInternal @0x0059F360,
|
||||
// GetDrawnThisFrame), so an object whose cell drew with a look-in cannot
|
||||
// draw again in a later pass; dynamics-last consults this set to honor
|
||||
// the same drawn-once contract.
|
||||
private readonly HashSet<uint> _lookInCellIds = new();
|
||||
|
||||
private readonly HashSet<uint> _oneCell = new(1);
|
||||
// Shell-batch scratch: all of a pass's cells collected for ONE batched
|
||||
// opaque Render call (instead of one heavy Render per cell). Reused across
|
||||
|
|
@ -172,13 +179,19 @@ public sealed class RetailPViewRenderer
|
|||
// contains them). drawableCells itself stays the MAIN flood: it feeds the
|
||||
// seals, the outside-stage predicate, and the frame result.
|
||||
var prepareCells = drawableCells;
|
||||
_lookInCellIds.Clear();
|
||||
if (_lookInFrames.Count > 0)
|
||||
{
|
||||
_lookInPrepareScratch.Clear();
|
||||
_lookInPrepareScratch.UnionWith(drawableCells);
|
||||
foreach (var f in _lookInFrames)
|
||||
{
|
||||
foreach (uint c in f.OrderedVisibleCells)
|
||||
{
|
||||
_lookInPrepareScratch.Add(c);
|
||||
_lookInCellIds.Add(c);
|
||||
}
|
||||
}
|
||||
prepareCells = _lookInPrepareScratch;
|
||||
}
|
||||
|
||||
|
|
@ -532,12 +545,22 @@ public sealed class RetailPViewRenderer
|
|||
{
|
||||
PortalVisibilityFrame frame = _lookInFrames[frameIndex];
|
||||
|
||||
// Retail enters DrawBuilding once per building and drains every
|
||||
// alpha submission accumulated by the preceding building before
|
||||
// punching the next building's portals. The first building uses
|
||||
// the pre-look-in barrier in DrawLandscapeThroughOutsideView.
|
||||
// Retail enters DrawBuilding once per building and drains the
|
||||
// alpha accumulated by the preceding building before punching the
|
||||
// next building's portals — and because retail's far→near walk
|
||||
// has only inserted FARTHER content by then, that drain can never
|
||||
// composite an emitter nearer than this building
|
||||
// (FlushAlphaList(0f) @0x0059F2A0 under the walk; AP-236).
|
||||
// The first building uses the pre-look-in barrier in
|
||||
// DrawLandscapeThroughOutsideView.
|
||||
if (frameIndex > 0)
|
||||
passes.FlushLandscapeAlpha();
|
||||
{
|
||||
passes.FlushLandscapeAlphaFartherThan(
|
||||
LookInBarrierDrainDistance(
|
||||
frame,
|
||||
ctx.Cells,
|
||||
ctx.CameraWorldPosition));
|
||||
}
|
||||
|
||||
// Pass 1: far-Z punch every aperture of this building.
|
||||
foreach (ExteriorPortalSeed seed in frame.ExteriorSeedPortals)
|
||||
|
|
@ -910,13 +933,25 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
|
||||
// ONE unclipped submission for the union of every slice's cone
|
||||
// survivors, then retail's pre-building barrier flush.
|
||||
// survivors, then retail's pre-building barrier drain. Under
|
||||
// retail's far→near walk, DrawBuilding's FlushAlphaList(0f)
|
||||
// @0x0059F2A0 can only ever flush content from cells FARTHER
|
||||
// than the building it precedes — a nearer emitter (the Holtburg
|
||||
// candle in front of a door) has not been inserted yet and
|
||||
// composites at a later flush, after that building's opaques.
|
||||
// Drain the far prefix only; nearer entries stay queued for the
|
||||
// DrawCells-boundary flush, which runs after the late dynamics
|
||||
// (AP-236 retirement).
|
||||
passes.DrawLandscapeStaticParticles(
|
||||
ctx,
|
||||
new RetailPViewLandscapeStaticParticleContext(
|
||||
_staticParticleUnionScratch));
|
||||
_staticParticleUnionScratch.Clear();
|
||||
passes.FlushLandscapeAlpha();
|
||||
passes.FlushLandscapeAlphaFartherThan(
|
||||
LookInBarrierDrainDistance(
|
||||
_lookInFrames[0],
|
||||
ctx.Cells,
|
||||
ctx.CameraWorldPosition));
|
||||
}
|
||||
|
||||
// #124: far-building look-ins draw HERE — still inside the landscape
|
||||
|
|
@ -1283,6 +1318,14 @@ public sealed class RetailPViewRenderer
|
|||
$"cell=0x{(e.ParentCellId ?? 0):X8} indoor=False rootOutdoor={rootIsOutdoor} -> CULLED(outside-stage)");
|
||||
continue;
|
||||
}
|
||||
// Drawn-once (retail DrawMeshInternal @0x0059F360 marks every
|
||||
// non-player part for the frame): a dynamic whose cell drew as a
|
||||
// building LOOK-IN already rendered with that cell inside the
|
||||
// landscape stage (#131). Redrawing it here would land AFTER the
|
||||
// boundary alpha drain and overpaint nearer flames — the Holtburg
|
||||
// door repainting the candle in front of it.
|
||||
if (indoor && _lookInCellIds.Contains(e.ParentCellId!.Value))
|
||||
continue;
|
||||
bool visible = indoor
|
||||
? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r)
|
||||
: viewcone.SphereVisibleOutside(c, r);
|
||||
|
|
@ -1329,16 +1372,18 @@ public sealed class RetailPViewRenderer
|
|||
// particles must not double-draw, unlike the depth-idempotent meshes).
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
// Parent-cell stage split: every DynamicLast owner emits its
|
||||
// particles here. Pure-outdoor dynamics are absent from this
|
||||
// route (outside stage only), and interior straddlers — whose
|
||||
// meshes drew in both stages — must emit HERE so the interior
|
||||
// stage cannot repaint over them (matches the production
|
||||
// partition-null path above).
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_dynamicParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.DynamicLast,
|
||||
0,
|
||||
0);
|
||||
RenderFrameRouteOwnerSelector.ExceptRoute(
|
||||
_dynamicParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1128,6 +1128,12 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
private RenderProjectionRecord[] _cell = [];
|
||||
private RenderProjectionRecord[] _dirty = [];
|
||||
private RenderProjectionRecord[] _survivors = [];
|
||||
|
||||
// Cells drawn as building look-ins this frame — the DynamicLast route
|
||||
// honors retail's drawn-once contract (DrawMeshInternal @0x0059F360
|
||||
// marks every non-player part): an object whose cell drew with a look-in
|
||||
// must not enter the final dynamics route again.
|
||||
private readonly HashSet<uint> _lookInCellScratch = new();
|
||||
private RenderProjectionRecord[] _cellRoute = [];
|
||||
private readonly Dictionary<RenderProjectionId, int>
|
||||
_outdoorPositions = [];
|
||||
|
|
@ -1528,6 +1534,16 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
RenderFrameWriter writer,
|
||||
in RenderScenePViewBuildInput input)
|
||||
{
|
||||
_lookInCellScratch.Clear();
|
||||
for (int frameIndex = 0;
|
||||
frameIndex < input.LookInFrames.Count;
|
||||
frameIndex++)
|
||||
{
|
||||
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
|
||||
for (int i = 0; i < frame.OrderedVisibleCells.Count; i++)
|
||||
_lookInCellScratch.Add(frame.OrderedVisibleCells[i]);
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
EnsureCapacity(ref _survivors, _dynamicCount);
|
||||
for (int i = 0; i < _dynamicCount; i++)
|
||||
|
|
@ -1539,6 +1555,14 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
if (!input.RootIsOutdoor && !indoor)
|
||||
continue;
|
||||
|
||||
// Drawn-once (retail DrawMeshInternal @0x0059F360): a dynamic
|
||||
// whose cell drew as a building look-in already rendered with
|
||||
// that cell in the landscape stage; re-entering the final route
|
||||
// would draw it after the boundary alpha drain and overpaint
|
||||
// nearer flames (the Holtburg candle-behind-door class).
|
||||
if (indoor && _lookInCellScratch.Contains(parentCellId!.Value))
|
||||
continue;
|
||||
|
||||
Sphere(in record, out Vector3 center, out float radius);
|
||||
bool visible = indoor
|
||||
? input.Viewcone.SphereVisibleInCell(
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ public sealed class RetailPViewPassExecutorTests
|
|||
"landscape-early",
|
||||
"unattached-particles-outdoor",
|
||||
"landscape-static-particles",
|
||||
"landscape-alpha",
|
||||
"landscape-alpha-farther",
|
||||
"look-in-punch",
|
||||
"landscape-late",
|
||||
"landscape-alpha",
|
||||
|
|
@ -380,7 +380,7 @@ public sealed class RetailPViewPassExecutorTests
|
|||
"look-in-punch",
|
||||
"landscape-building-shell",
|
||||
"landscape-static-particles",
|
||||
"landscape-alpha",
|
||||
"landscape-alpha-farther",
|
||||
"look-in-punch",
|
||||
"landscape-building-shell");
|
||||
}
|
||||
|
|
@ -795,6 +795,8 @@ public sealed class RetailPViewPassExecutorTests
|
|||
? "unattached-particles-outdoor"
|
||||
: "unattached-particles-interior");
|
||||
public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha");
|
||||
public void FlushLandscapeAlphaFartherThan(float minViewerDistance) =>
|
||||
Operations.Add("landscape-alpha-farther");
|
||||
public void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles");
|
||||
public void DrawDynamicsParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue