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> _staticParticleUnionScratch = new();
|
||||||
private readonly HashSet<uint> _cellParticleUnionScratch = 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);
|
private readonly HashSet<uint> _oneCell = new(1);
|
||||||
// Shell-batch scratch: all of a pass's cells collected for ONE batched
|
// 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
|
// 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
|
// contains them). drawableCells itself stays the MAIN flood: it feeds the
|
||||||
// seals, the outside-stage predicate, and the frame result.
|
// seals, the outside-stage predicate, and the frame result.
|
||||||
var prepareCells = drawableCells;
|
var prepareCells = drawableCells;
|
||||||
|
_lookInCellIds.Clear();
|
||||||
if (_lookInFrames.Count > 0)
|
if (_lookInFrames.Count > 0)
|
||||||
{
|
{
|
||||||
_lookInPrepareScratch.Clear();
|
_lookInPrepareScratch.Clear();
|
||||||
_lookInPrepareScratch.UnionWith(drawableCells);
|
_lookInPrepareScratch.UnionWith(drawableCells);
|
||||||
foreach (var f in _lookInFrames)
|
foreach (var f in _lookInFrames)
|
||||||
|
{
|
||||||
foreach (uint c in f.OrderedVisibleCells)
|
foreach (uint c in f.OrderedVisibleCells)
|
||||||
|
{
|
||||||
_lookInPrepareScratch.Add(c);
|
_lookInPrepareScratch.Add(c);
|
||||||
|
_lookInCellIds.Add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
prepareCells = _lookInPrepareScratch;
|
prepareCells = _lookInPrepareScratch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -532,12 +545,22 @@ public sealed class RetailPViewRenderer
|
||||||
{
|
{
|
||||||
PortalVisibilityFrame frame = _lookInFrames[frameIndex];
|
PortalVisibilityFrame frame = _lookInFrames[frameIndex];
|
||||||
|
|
||||||
// Retail enters DrawBuilding once per building and drains every
|
// Retail enters DrawBuilding once per building and drains the
|
||||||
// alpha submission accumulated by the preceding building before
|
// alpha accumulated by the preceding building before punching the
|
||||||
// punching the next building's portals. The first building uses
|
// next building's portals — and because retail's far→near walk
|
||||||
// the pre-look-in barrier in DrawLandscapeThroughOutsideView.
|
// 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)
|
if (frameIndex > 0)
|
||||||
passes.FlushLandscapeAlpha();
|
{
|
||||||
|
passes.FlushLandscapeAlphaFartherThan(
|
||||||
|
LookInBarrierDrainDistance(
|
||||||
|
frame,
|
||||||
|
ctx.Cells,
|
||||||
|
ctx.CameraWorldPosition));
|
||||||
|
}
|
||||||
|
|
||||||
// Pass 1: far-Z punch every aperture of this building.
|
// Pass 1: far-Z punch every aperture of this building.
|
||||||
foreach (ExteriorPortalSeed seed in frame.ExteriorSeedPortals)
|
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
|
// 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(
|
passes.DrawLandscapeStaticParticles(
|
||||||
ctx,
|
ctx,
|
||||||
new RetailPViewLandscapeStaticParticleContext(
|
new RetailPViewLandscapeStaticParticleContext(
|
||||||
_staticParticleUnionScratch));
|
_staticParticleUnionScratch));
|
||||||
_staticParticleUnionScratch.Clear();
|
_staticParticleUnionScratch.Clear();
|
||||||
passes.FlushLandscapeAlpha();
|
passes.FlushLandscapeAlphaFartherThan(
|
||||||
|
LookInBarrierDrainDistance(
|
||||||
|
_lookInFrames[0],
|
||||||
|
ctx.Cells,
|
||||||
|
ctx.CameraWorldPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
// #124: far-building look-ins draw HERE — still inside the landscape
|
// #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)");
|
$"cell=0x{(e.ParentCellId ?? 0):X8} indoor=False rootOutdoor={rootIsOutdoor} -> CULLED(outside-stage)");
|
||||||
continue;
|
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
|
bool visible = indoor
|
||||||
? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r)
|
? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r)
|
||||||
: viewcone.SphereVisibleOutside(c, r);
|
: viewcone.SphereVisibleOutside(c, r);
|
||||||
|
|
@ -1329,16 +1372,18 @@ public sealed class RetailPViewRenderer
|
||||||
// particles must not double-draw, unlike the depth-idempotent meshes).
|
// particles must not double-draw, unlike the depth-idempotent meshes).
|
||||||
if (frameEntityPasses is not null)
|
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(
|
RenderFrameRouteOwnerSelector.Replace(
|
||||||
_dynamicParticleOwnerScratch,
|
_dynamicParticleOwnerScratch,
|
||||||
in frameView,
|
in frameView,
|
||||||
RenderFrameCandidateRoute.DynamicLast,
|
RenderFrameCandidateRoute.DynamicLast,
|
||||||
0,
|
0,
|
||||||
0);
|
0);
|
||||||
RenderFrameRouteOwnerSelector.ExceptRoute(
|
|
||||||
_dynamicParticleOwnerScratch,
|
|
||||||
in frameView,
|
|
||||||
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1128,6 +1128,12 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
private RenderProjectionRecord[] _cell = [];
|
private RenderProjectionRecord[] _cell = [];
|
||||||
private RenderProjectionRecord[] _dirty = [];
|
private RenderProjectionRecord[] _dirty = [];
|
||||||
private RenderProjectionRecord[] _survivors = [];
|
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 RenderProjectionRecord[] _cellRoute = [];
|
||||||
private readonly Dictionary<RenderProjectionId, int>
|
private readonly Dictionary<RenderProjectionId, int>
|
||||||
_outdoorPositions = [];
|
_outdoorPositions = [];
|
||||||
|
|
@ -1528,6 +1534,16 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
RenderFrameWriter writer,
|
RenderFrameWriter writer,
|
||||||
in RenderScenePViewBuildInput input)
|
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;
|
int count = 0;
|
||||||
EnsureCapacity(ref _survivors, _dynamicCount);
|
EnsureCapacity(ref _survivors, _dynamicCount);
|
||||||
for (int i = 0; i < _dynamicCount; i++)
|
for (int i = 0; i < _dynamicCount; i++)
|
||||||
|
|
@ -1539,6 +1555,14 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
if (!input.RootIsOutdoor && !indoor)
|
if (!input.RootIsOutdoor && !indoor)
|
||||||
continue;
|
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);
|
Sphere(in record, out Vector3 center, out float radius);
|
||||||
bool visible = indoor
|
bool visible = indoor
|
||||||
? input.Viewcone.SphereVisibleInCell(
|
? input.Viewcone.SphereVisibleInCell(
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
"landscape-early",
|
"landscape-early",
|
||||||
"unattached-particles-outdoor",
|
"unattached-particles-outdoor",
|
||||||
"landscape-static-particles",
|
"landscape-static-particles",
|
||||||
"landscape-alpha",
|
"landscape-alpha-farther",
|
||||||
"look-in-punch",
|
"look-in-punch",
|
||||||
"landscape-late",
|
"landscape-late",
|
||||||
"landscape-alpha",
|
"landscape-alpha",
|
||||||
|
|
@ -380,7 +380,7 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
"look-in-punch",
|
"look-in-punch",
|
||||||
"landscape-building-shell",
|
"landscape-building-shell",
|
||||||
"landscape-static-particles",
|
"landscape-static-particles",
|
||||||
"landscape-alpha",
|
"landscape-alpha-farther",
|
||||||
"look-in-punch",
|
"look-in-punch",
|
||||||
"landscape-building-shell");
|
"landscape-building-shell");
|
||||||
}
|
}
|
||||||
|
|
@ -795,6 +795,8 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
? "unattached-particles-outdoor"
|
? "unattached-particles-outdoor"
|
||||||
: "unattached-particles-interior");
|
: "unattached-particles-interior");
|
||||||
public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha");
|
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 DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles");
|
||||||
public void DrawDynamicsParticles(
|
public void DrawDynamicsParticles(
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue