#118: outdoor dynamics draw in the outside stage under interior roots - the house-exit clip+vanish was the SEAL z-killing the player
Root cause (pinned by the new deterministic exit-walk harness, NOT guessed):
under an interior render root, the exit-portal SEAL stamps the door fan at
TRUE depth after the gated full depth clear, and T1's "ALL dynamics last"
pass then drew the outdoor-classified player depth-tested - every fragment
beyond the door plane z-failed against the seal across the whole aperture.
Harness measured the full window: from the moment the sphere center crosses
the plane until the eye follows (~2.6 m of camera lag, ~2.2 s at walk speed)
the player is invisible; while straddling, the beyond-plane body half clips
at the plane. The handoff's three cone-level candidates are all EXONERATED:
the cone walk passes every step; (eye, ViewerCellId) come from the same
SweepEye call with camera-update-before-visibility-read in the same frame;
the side-test window is sub-epsilon under healthy resolution.
Retail oracle (grep-named-first): PView::DrawCells 0x005a4840 runs
LScape::draw FIRST (pc:432719), then the gated depth clear (pc:432731-32)
and the exit-portal seals (pc:432785-86); outdoor cell objects draw inside
the landscape stage (DrawBlock 0x005a17c0 -> DrawSortCell pc:430124), and
an object draws once per overlapped shadow cell (pc:430056-64) - the
straddling body composes from both stages, neither half clips.
Fix: RetailPViewRenderer assigns dynamics to the OUTSIDE stage under an
interior root when outdoor-classified OR sphere-straddling an exit-portal
plane of their flood-visible cell (DynamicDrawsInOutsideStage - pure, the
harness drives it as the ordering contract); they ride the landscape slice
draw (pre-clear, seal-protected) with the same per-slice cone test as
outdoor statics. Indoor dynamics keep the last pass (retail loop C);
straddlers draw in both (retail shadow dual-draw). Outdoor roots keep
all-dynamics-last - the BR-2 punch-after-dynamics lesson (88be519) stands.
Apparatus: HouseExitWalkReplayTests - dat-backed corner-building exit walk
driving the production stack headlessly (RetailChaseCamera damping ->
healthy-sweep viewer resolution -> PortalVisibilityBuilder.Build ->
ClipFrameAssembler -> ViewconeCuller -> the DrawDynamicsLast predicate +
a CPU seal-depth model). 5 tests: cone pin, seal-depth pin, straddle
dual-draw pin, per-step table, stale-root window quantifier (#118 cand 2).
Suites: App 232 (227+5), Core 1416+2skip, UI 420, Net 294.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
acaaeae434
commit
5a80a2ee24
5 changed files with 628 additions and 10 deletions
|
|
@ -73,9 +73,14 @@ public static class InteriorEntityPartition
|
|||
return result;
|
||||
}
|
||||
|
||||
private static bool IsIndoorCellId(uint cellId)
|
||||
/// <summary>Shared indoor classification — keep DrawDynamicsLast, the
|
||||
/// outside-stage assignment (#118), and the partition in lockstep.</summary>
|
||||
public static bool IsIndoorCellId(uint cellId)
|
||||
{
|
||||
uint low = cellId & 0xFFFFu;
|
||||
return low >= 0x0100u && low != 0xFFFFu;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IsIndoorCellId(uint)"/>
|
||||
public static bool IsIndoorCellId(uint? cellId) => cellId is uint c && IsIndoorCellId(c);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,12 +109,39 @@ public sealed class RetailPViewRenderer
|
|||
// assembled slices + this frame's view-projection.
|
||||
var viewcone = ViewconeCuller.Build(clipAssembly, ctx.ViewProjection);
|
||||
|
||||
// #118: stage assignment for dynamics under an INTERIOR root. Retail
|
||||
// draws the OUTSIDE world's objects inside the landscape stage —
|
||||
// PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated
|
||||
// full depth clear (pc:432731-432732) and the exit-portal SEALS
|
||||
// (pc:432785-432786); DrawBlock draws every landcell's objects via
|
||||
// DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our
|
||||
// single last pass instead z-fails against the seal's true-depth stamp
|
||||
// the moment it stands beyond the door plane — the house-exit
|
||||
// clip+vanish (pinned by HouseExitWalkReplayTests). So under an
|
||||
// interior root: outdoor-classified dynamics draw in the outside
|
||||
// stage; an indoor dynamic whose sphere STRADDLES an exit portal
|
||||
// draws in BOTH stages (retail's per-overlapped-cell shadow-part
|
||||
// draw, DrawBlock pc:430056-430064) so neither body half clips at the
|
||||
// plane. Outdoor roots keep ALL dynamics in the last pass — our
|
||||
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
|
||||
// BR-2 punch-after-dynamics lesson, reverted 88be519).
|
||||
_outsideStageDynamics.Clear();
|
||||
if (!ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
foreach (var e in partition.Dynamics)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (DynamicDrawsInOutsideStage(e.ParentCellId, c, r, drawableCells, ctx.CellLookup))
|
||||
_outsideStageDynamics.Add(e);
|
||||
}
|
||||
}
|
||||
|
||||
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition, viewcone);
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(pvFrame);
|
||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
||||
DrawDynamicsLast(ctx, partition, viewcone);
|
||||
DrawDynamicsLast(ctx, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -216,6 +243,17 @@ public sealed class RetailPViewRenderer
|
|||
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
|
||||
_outdoorStaticScratch.Add(e);
|
||||
}
|
||||
// #118: outside-stage dynamics ride the landscape pass like retail's
|
||||
// per-landcell DrawSortCell (DrawBlock 0x005a17c0, pc:430124) — drawn
|
||||
// BEFORE the depth clear + seals so the seal PROTECTS their pixels in
|
||||
// the aperture instead of z-killing them. Same per-slice cone test as
|
||||
// the statics above. Empty under outdoor roots (see DrawInside).
|
||||
foreach (var e in _outsideStageDynamics)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
|
||||
_outdoorStaticScratch.Add(e);
|
||||
}
|
||||
probeSliceIndex++;
|
||||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
|
||||
}
|
||||
|
|
@ -375,7 +413,8 @@ public sealed class RetailPViewRenderer
|
|||
private void DrawDynamicsLast(
|
||||
IRetailPViewCellDrawContext ctx,
|
||||
InteriorEntityPartition.Result partition,
|
||||
ViewconeCuller viewcone)
|
||||
ViewconeCuller viewcone,
|
||||
bool rootIsOutdoor)
|
||||
{
|
||||
if (partition.Dynamics.Count == 0)
|
||||
return;
|
||||
|
|
@ -384,8 +423,16 @@ public sealed class RetailPViewRenderer
|
|||
foreach (var e in partition.Dynamics)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
bool indoor = e.ParentCellId is uint cell
|
||||
&& (cell & 0xFFFFu) >= 0x0100u && (cell & 0xFFFFu) != 0xFFFFu;
|
||||
bool indoor = InteriorEntityPartition.IsIndoorCellId(e.ParentCellId);
|
||||
// #118: under an interior root, outdoor-classified dynamics drew in
|
||||
// the outside stage (pre-clear, seal-protected) — retail draws them
|
||||
// via LScape::draw's per-landcell DrawSortCell, never in the
|
||||
// post-seal cell-object epilogue (PView::DrawCells pc:432719 vs
|
||||
// pc:432878). Drawing them here instead z-fails them against the
|
||||
// seal. Indoor dynamics (incl. exit-portal straddlers, which drew
|
||||
// in BOTH stages) stay — this pass is retail's loop C.
|
||||
if (!rootIsOutdoor && !indoor)
|
||||
continue;
|
||||
bool visible = indoor
|
||||
? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r)
|
||||
: viewcone.SphereVisibleOutside(c, r);
|
||||
|
|
@ -459,6 +506,57 @@ public sealed class RetailPViewRenderer
|
|||
private readonly List<WorldEntity> _outdoorStaticScratch = new();
|
||||
private readonly List<WorldEntity> _cellStaticScratch = new();
|
||||
private readonly List<WorldEntity> _dynamicsScratch = new();
|
||||
// #118: dynamics assigned to the OUTSIDE stage this frame (interior roots
|
||||
// only) — outdoor-classified + exit-portal straddlers. Cleared per frame.
|
||||
private readonly List<WorldEntity> _outsideStageDynamics = new();
|
||||
|
||||
/// <summary>
|
||||
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
|
||||
/// in the OUTSIDE (landscape) stage — before the gated depth clear and the
|
||||
/// exit-portal seals — like retail's per-landcell object draw
|
||||
/// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at
|
||||
/// the top of PView::DrawCells pc:432719)?
|
||||
///
|
||||
/// True for outdoor-classified dynamics (their fragments lie beyond the
|
||||
/// door plane and would z-fail the seal in the last pass), and for INDOOR
|
||||
/// dynamics whose sphere straddles an exit-portal plane of their flood-
|
||||
/// visible cell — retail draws an object once per overlapped shadow cell
|
||||
/// (DrawBlock pc:430056-430064), so a threshold-straddling body draws in
|
||||
/// both stages and neither half clips at the plane. Pure — also driven
|
||||
/// headlessly by HouseExitWalkReplayTests as the ordering contract.
|
||||
/// </summary>
|
||||
public static bool DynamicDrawsInOutsideStage(
|
||||
uint? parentCellId,
|
||||
Vector3 sphereCenter,
|
||||
float sphereRadius,
|
||||
HashSet<uint> drawableCells,
|
||||
Func<uint, LoadedCell?> cellLookup)
|
||||
{
|
||||
if (!InteriorEntityPartition.IsIndoorCellId(parentCellId))
|
||||
return true;
|
||||
|
||||
uint cellId = parentCellId!.Value;
|
||||
if (!drawableCells.Contains(cellId))
|
||||
return false; // not in the flood — the last-pass cone cull owns it
|
||||
var cell = cellLookup(cellId);
|
||||
if (cell is null)
|
||||
return false;
|
||||
|
||||
var localC = Vector3.Transform(sphereCenter, cell.InverseWorldTransform);
|
||||
int n = Math.Min(cell.Portals.Count, cell.ClipPlanes.Count);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
if (cell.Portals[i].OtherCellId != 0xFFFF)
|
||||
continue;
|
||||
var plane = cell.ClipPlanes[i];
|
||||
if (plane.Normal.LengthSquared() < 1e-8f)
|
||||
continue;
|
||||
float dist = Vector3.Dot(plane.Normal, localC) + plane.D;
|
||||
if (MathF.Abs(dist) < sphereRadius)
|
||||
return true; // sphere straddles the exit-portal plane
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Conservative bounding sphere from the entity's cached AABB — the same
|
||||
// bounds source the dispatcher's frustum cull uses.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue