refactor(render): delete legacy pview execution machinery

This commit is contained in:
Erik 2026-08-31 05:27:25 +02:00
parent 1be4889c1c
commit 69e69408e8
9 changed files with 58 additions and 3627 deletions

View file

@ -494,13 +494,18 @@ internal sealed class FrameRootCompositionPhase
d.ParticleVisibility,
new WorldScenePViewRenderer(
new RetailPViewRenderer(
currentRenderSceneOracle,
live.RenderSceneShadow,
live.RenderSceneShadow
?? throw new InvalidOperationException(
"The retail frame walk requires the retained render scene."),
// Campaign FW3.2b-2: the walk's production world
// data, published/retired with each landblock by the
// render publisher (FW3.1).
live.LandblockPipeline.RenderPublisher?.WalkBuildings,
live.LandblockPipeline.RenderPublisher?.WalkLandscape,
live.LandblockPipeline.RenderPublisher?.WalkBuildings
?? throw new InvalidOperationException(
"The retail frame walk requires the building registry."),
live.LandblockPipeline.RenderPublisher?.WalkLandscape
?? throw new InvalidOperationException(
"The retail frame walk requires the landscape registry."),
d.CellVisibility),
retailPViewPassExecutor,
retailPViewPassExecutor),

View file

@ -86,37 +86,12 @@ internal interface IOutdoorSceneParticleOwnerSource
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
}
internal readonly record struct RenderFrameEntityDrawRequest(
RenderFrameView View,
RenderFrameCandidateRoute Route,
int RouteIndex,
uint CellId,
uint TupleLandblockId);
internal interface IRenderFrameEntityPassExecutor
{
void BeginEntityFrame(in RenderFrameView view);
bool DrawEntityRoute(
ICamera camera,
in RenderFrameView view,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId,
uint tupleLandblockId);
void CompleteEntityFrame(in RenderFrameView view);
void AbortEntityFrame();
}
/// <summary>
/// Campaign V slice V6j: backend-neutral. The order it implements is retail's and
/// is written once; the four places it touches graphics state directly are owned
/// by <see cref="IWorldPassSurface"/>.
/// </summary>
internal sealed partial class RetailPViewPassExecutor :
IRetailPViewPassExecutor,
IOutdoorSceneParticleOwnerSource
{
private readonly IWorldPassSurface _surface;
@ -134,15 +109,9 @@ internal sealed partial class RetailPViewPassExecutor :
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
private readonly RetailPViewParticleClassifications _particleClassifications = new();
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
private readonly Dictionary<uint, int> _singleCellClipRouting = new(1);
private readonly Dictionary<uint, int> _noCellClipRouting = new(0);
// ACDREAM_PROBE_WALK_ROOT / cathedral FW4: observation-only counters.
// They distinguish the walk-owned far-Z portal punches from the retired
// legacy look-in punch route. RetailPViewRenderer samples them for the
// diagnostic comparison after the walk-turn look-in routes have replayed.
// ACDREAM_PROBE_WALK_ROOT / cathedral FW4: observation-only counter for
// the walk-owned far-Z portal punches.
internal int WalkLookInPunchCountThisFrame { get; private set; }
internal int LegacyLookInPunchCountThisFrame { get; private set; }
/// <summary>
/// Borrowed until the next late landscape pass. The outdoor-root post-world
@ -190,7 +159,6 @@ internal sealed partial class RetailPViewPassExecutor :
{
AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "pre";
WalkLookInPunchCountThisFrame = 0;
LegacyLookInPunchCountThisFrame = 0;
}
}
@ -242,11 +210,6 @@ internal sealed partial class RetailPViewPassExecutor :
ClipFrameAssembly reuseAssembly) =>
ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
public void AppendLookInClipFrames(
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
ClipFrameAssembly assembly) =>
ClipFrameAssembler.AppendLookInFrames(_clipFrame, lookInFrames, assembly);
public void PrepareClipFrame(int terrainUploadCount) =>
_surface.PrepareClipFrame(terrainUploadCount);
@ -263,24 +226,6 @@ internal sealed partial class RetailPViewPassExecutor :
_entities.ClearClipRouting();
}
public void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice)
{
_singleCellClipRouting.Clear();
_singleCellClipRouting.Add(cellId, slice.Slot);
_envCells.SetClipRouting(_singleCellClipRouting);
// Retail DrawMesh only viewcone-checks an object's sphere under the
// installed PortalList and then draws the mesh whole. Hard clipping the
// object here slices a stationary player when the chase camera crosses
// into the opposite cathedral cell while the player remains behind.
_entities.ClearClipRouting();
}
private void UseOutdoorPortalViewRouting(ClipViewSlice slice) =>
_entities.SetClipRouting(
_noCellClipRouting,
outdoorSlot: slice.Slot,
outdoorVisible: true);
public void PrepareCellBatches(
RetailPViewFrameInput frame,
HashSet<uint> visibleCellIds) =>
@ -298,112 +243,9 @@ internal sealed partial class RetailPViewPassExecutor :
public bool CellHasTransparentShell(uint cellId) =>
_envCells.CellHasTransparent(cellId);
public void DrawTransparentCellShells(HashSet<uint> cellIds) =>
_envCells.Render(WbRenderPass.Transparent, cellIds);
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
_envCells.RenderTransparentOrdered(cellIds);
public void DrawEntityBucket(
RetailPViewFrameInput frame,
IReadOnlyList<WorldEntity> entities,
HashSet<uint>? visibleCellIds)
{
uint landblockId = frame.PlayerLandblockId ?? 0u;
var entry = (
landblockId,
Vector3.Zero,
Vector3.Zero,
entities,
(IReadOnlyDictionary<uint, WorldEntity>?)null);
_entities.Draw(
frame.Camera,
new[] { entry },
frame.Frustum,
neverCullLandblockId: frame.PlayerLandblockId,
visibleCellIds: visibleCellIds,
animatedEntityIds: frame.AnimatedEntityIds);
}
public void EmitClipRouteProbe(
ClipFrameAssembly clipAssembly,
ClipViewSlice slice,
int sliceIndex) =>
_diagnostics.EmitClipRouteProbe(
RenderingDiagnostics.ProbeClipRouteEnabled,
_clipFrame,
clipAssembly,
slice,
sliceIndex);
public void DrawLandscapeSlice(
RetailPViewFrameInput frame,
RetailPViewLandscapeSliceContext context)
{
ClipViewSlice slice = context.Slice;
bool scissor = BeginDoorwayScissor(slice.NdcAabb);
_diagnostics.EmitClipRouteScissorProbe(
RenderingDiagnostics.ProbeClipRouteEnabled,
scissor,
slice.NdcAabb);
_surface.BindTerrainClip();
EnableClipDistances();
if (frame.RenderSky)
{
_sky?.RenderSky(
frame.Camera,
frame.CameraWorldPosition,
frame.DayFraction,
frame.ActiveDayGroup,
frame.SkyKeyframe,
frame.EnvironOverrideActive);
}
DisableClipDistances();
if (frame.RenderSky && _particles is not null && _particleRenderer is not null)
{
_particleRenderer.Draw(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.SkyPreScene);
}
EnableClipDistances();
_terrainDiagnostics.Begin();
_terrain?.Draw(
frame.Camera,
frame.Frustum,
neverCullLandblockId: frame.PlayerLandblockId,
clipPlanes: slice.Planes,
ndcClipAabb: slice.NdcAabb);
_terrainDiagnostics.Complete();
DisableClipDistances();
if (context.OutdoorEntities.Count > 0)
{
var sceneryEntry = (
frame.PlayerLandblockId ?? 0u,
Vector3.Zero,
Vector3.Zero,
context.OutdoorEntities,
(IReadOnlyDictionary<uint, WorldEntity>?)null);
_entities.Draw(
frame.Camera,
new[] { sceneryEntry },
frame.Frustum,
neverCullLandblockId: frame.PlayerLandblockId,
visibleCellIds: null,
animatedEntityIds: frame.AnimatedEntityIds);
}
if (scissor)
_surface.EndScissor();
_entities.ClearClipRouting();
DisableClipDistances();
}
public void DrawLandscapeSliceLate(
RetailPViewFrameInput frame,
RetailPViewLandscapeLateSliceContext context)
@ -503,38 +345,6 @@ internal sealed partial class RetailPViewPassExecutor :
DisableClipDistances();
}
public void DrawLandscapeBuildingShellSlice(
RetailPViewFrameInput frame,
RetailPViewLandscapeBuildingShellSliceContext context)
{
UseOutdoorPortalViewRouting(context.Slice);
bool scissor = BeginDoorwayScissor(context.Slice.NdcAabb);
_surface.BindTerrainClip();
DisableClipDistances();
if (context.BuildingShells.Count > 0)
{
var buildingEntry = (
frame.PlayerLandblockId ?? 0u,
Vector3.Zero,
Vector3.Zero,
context.BuildingShells,
(IReadOnlyDictionary<uint, WorldEntity>?)null);
_entities.Draw(
frame.Camera,
new[] { buildingEntry },
frame.Frustum,
neverCullLandblockId: frame.PlayerLandblockId,
visibleCellIds: null,
animatedEntityIds: frame.AnimatedEntityIds);
}
if (scissor)
_surface.EndScissor();
_entities.ClearClipRouting();
DisableClipDistances();
}
public void ClearInteriorDepth()
{
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
@ -547,23 +357,6 @@ internal sealed partial class RetailPViewPassExecutor :
RetailPViewCellSliceContext context) =>
DrawPortalDepthWrite(context, frame, forceFarZ: frame.RootCell.IsOutdoorNode);
public void DrawLookInPortalPunch(
RetailPViewFrameInput frame,
RetailPViewCellSliceContext context,
int portalIndex)
{
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{
LegacyLookInPunchCountThisFrame++;
Console.WriteLine(
$"[lookin-punch] source=legacy viewer=0x{frame.ViewerCellId:X8} "
+ $"root=0x{frame.RootCell.CellId:X8} cell=0x{context.CellId:X8} "
+ $"portal={portalIndex} planes={context.Slice.Planes.Length} "
+ $"phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}");
}
DrawPortalDepthWrite(context, frame, forceFarZ: true, portalIndex);
}
public void DrawUnattachedSceneParticles(
RetailPViewFrameInput frame,
bool outdoorCells)
@ -643,9 +436,6 @@ internal sealed partial class RetailPViewPassExecutor :
public void FlushLandscapeAlpha() => _alpha.Flush();
public void FlushLandscapeAlphaFartherThan(float minViewerDistance) =>
_alpha.FlushFartherThan(minViewerDistance);
public void DrawCellParticles(
RetailPViewFrameInput frame,
RetailPViewCellSliceContext context)
@ -676,29 +466,6 @@ internal sealed partial class RetailPViewPassExecutor :
DisableClipDistances();
}
public void DrawDynamicsParticles(
RetailPViewFrameInput frame,
IReadOnlySet<uint> ownerIds)
{
if (_particles is null || _particleRenderer is null || ownerIds.Count == 0)
return;
HashSet<uint> dynamics = _particleClassifications.Dynamics;
dynamics.Clear();
dynamics.UnionWith(ownerIds);
if (dynamics.Count == 0)
return;
DisableClipDistances();
ProbeWalkParticleRoute("dyn-owners", dynamics);
_particleRenderer.DrawForOwners(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.Scene,
dynamics);
DisableClipDistances();
}
public void EmitDiagnostics(
RetailPViewFrameInput frame,
RetailPViewFrameResult result) =>

File diff suppressed because it is too large Load diff

View file

@ -30,8 +30,7 @@ namespace AcDream.App.Rendering.Walk;
/// </list>
///
/// The tuple landblock id handed to the classifier is the frame's player
/// landblock — the packed path's own convention for every
/// <c>RenderFrameEntityDrawRequest</c>.
/// landblock, matching the production walk's retained-scene query convention.
///
/// <para>Campaign FW3.4a: <see cref="GetCellStatics"/>, <see cref="GetOutdoorStatics"/>,
/// and <see cref="GetBuildingShellStatics"/> used to materialize their result

View file

@ -41,12 +41,12 @@ internal interface IPreparedWorldSceneFramePhase : IWorldSceneFramePhase
internal sealed class WorldScenePViewRenderer : IWorldScenePViewRenderer
{
private readonly RetailPViewRenderer _renderer;
private readonly IRetailPViewPassExecutor _passes;
private readonly RetailPViewPassExecutor _passes;
private readonly IOutdoorSceneParticleOwnerSource _particles;
public WorldScenePViewRenderer(
RetailPViewRenderer renderer,
IRetailPViewPassExecutor passes,
RetailPViewPassExecutor passes,
IOutdoorSceneParticleOwnerSource particles)
{
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));

View file

@ -1,252 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public sealed class BuildingGroupScratchTests
{
[Fact]
public void Rebuild_DropsHistoricalKeys_BoundsRetention_AndPreservesEncounterOrder()
{
var scratch = new BuildingGroupScratch();
int pathologicalCount = BuildingGroupScratch.MaxRetainedGroups * 4;
var pathological = new List<LoadedCell>(pathologicalCount);
for (int i = 0; i < pathologicalCount; i++)
{
pathological.Add(new LoadedCell
{
CellId = (uint)i + 1u,
BuildingId = 0x1000_0000u + (uint)i,
});
}
scratch.Rebuild(pathological);
Assert.Equal(pathologicalCount, scratch.ActiveGroupCount);
Assert.True(scratch.MapCapacity > BuildingGroupScratch.MaxRetainedGroups);
var firstA = new LoadedCell { CellId = 0xAA01u, BuildingId = 0x20u };
var second = new LoadedCell { CellId = 0xBB01u, BuildingId = 0x10u };
var firstB = new LoadedCell { CellId = 0xAA02u, BuildingId = 0x20u };
var unstamped = new LoadedCell { CellId = 0xCC01u };
scratch.Rebuild([firstA, second, firstB, unstamped]);
Assert.Equal(3, scratch.ActiveGroupCount);
Assert.Equal(
new uint[] { 0x20u, 0x10u, unstamped.CellId },
scratch.Groups.Keys.ToArray());
Assert.Equal(new[] { firstA, firstB }, scratch.Groups[0x20u]);
Assert.Equal(new[] { second }, scratch.Groups[0x10u]);
Assert.Equal(new[] { unstamped }, scratch.Groups[unstamped.CellId]);
Assert.DoesNotContain(
scratch.Groups.Keys,
key => key >= 0x1000_0000u);
Assert.InRange(
scratch.RetainedListCount,
0,
BuildingGroupScratch.MaxRetainedGroups);
Assert.InRange(
scratch.MapCapacity,
0,
BuildingGroupScratch.MaxRetainedGroups);
scratch.Reset();
Assert.Equal(0, scratch.ActiveGroupCount);
Assert.Empty(scratch.Groups);
Assert.InRange(
scratch.RetainedListCount,
0,
BuildingGroupScratch.MaxRetainedGroups);
Assert.InRange(
scratch.MapCapacity,
0,
BuildingGroupScratch.MaxRetainedGroups);
}
[Fact]
public void Reset_DoesNotRetainPathologicalGroupBackingArray()
{
var scratch = new BuildingGroupScratch();
var oversizedGroup = new List<LoadedCell>(
BuildingGroupScratch.MaxRetainedCellsPerGroup * 2);
for (int i = 0;
i < BuildingGroupScratch.MaxRetainedCellsPerGroup * 2;
i++)
{
oversizedGroup.Add(new LoadedCell
{
CellId = (uint)i + 1u,
BuildingId = 0x42u,
});
}
scratch.Rebuild(oversizedGroup);
Assert.Single(scratch.Groups);
Assert.True(
scratch.Groups[0x42u].Capacity
> BuildingGroupScratch.MaxRetainedCellsPerGroup);
scratch.Reset();
Assert.Equal(0, scratch.RetainedListCount);
Assert.Empty(scratch.Groups);
}
}
public sealed class RetailPViewScratchRetentionTests
{
[Fact]
public void ClearFrameBuffers_DropsPathologicalHighWater_AfterIdleHysteresis()
{
var retention = new RetailPViewScratchRetention();
int pathologicalCellCount =
RetailPViewScratchRetention.MaxRetainedCellItems * 4;
int pathologicalFrameCount =
RetailPViewScratchRetention.MaxRetainedLookInFrames * 4;
var lookInFrames = new List<PortalVisibilityFrame>(pathologicalFrameCount);
var lookInPrepare = new HashSet<uint>();
var drawableCells = new HashSet<uint>();
var shellBatch = new HashSet<uint>();
var orderedTransparent = new List<uint>(pathologicalCellCount);
for (int i = 0; i < pathologicalFrameCount; i++)
lookInFrames.Add(new PortalVisibilityFrame());
for (int i = 0; i < pathologicalCellCount; i++)
{
uint id = (uint)i;
lookInPrepare.Add(id);
drawableCells.Add(id);
shellBatch.Add(id);
orderedTransparent.Add(id);
}
Assert.True(
lookInFrames.Capacity
> RetailPViewScratchRetention.MaxRetainedLookInFrames);
Assert.True(
drawableCells.EnsureCapacity(0)
> RetailPViewScratchRetention.MaxRetainedCellItems);
retention.ClearFrameBuffers(
lookInFrames,
lookInPrepare,
drawableCells,
shellBatch,
orderedTransparent);
Assert.Empty(lookInFrames);
Assert.Empty(lookInPrepare);
Assert.Empty(drawableCells);
Assert.Empty(shellBatch);
Assert.Empty(orderedTransparent);
Assert.True(
lookInFrames.Capacity
> RetailPViewScratchRetention.MaxRetainedLookInFrames);
for (int i = 0; i < RetailPViewScratchRetention.CapacityTrimIdleFrames; i++)
{
retention.ClearFrameBuffers(
lookInFrames,
lookInPrepare,
drawableCells,
shellBatch,
orderedTransparent);
}
Assert.InRange(
lookInFrames.Capacity,
0,
RetailPViewScratchRetention.MaxRetainedLookInFrames);
Assert.InRange(
lookInPrepare.EnsureCapacity(0),
0,
RetailPViewScratchRetention.MaxRetainedCellItems);
Assert.InRange(
drawableCells.EnsureCapacity(0),
0,
RetailPViewScratchRetention.MaxRetainedCellItems);
Assert.InRange(
shellBatch.EnsureCapacity(0),
0,
RetailPViewScratchRetention.MaxRetainedCellItems);
Assert.InRange(
orderedTransparent.Capacity,
0,
RetailPViewScratchRetention.MaxRetainedCellItems);
}
[Fact]
public void ClearFrameBuffers_PreservesNormalWarmCapacity()
{
var retention = new RetailPViewScratchRetention();
var lookInFrames = new List<PortalVisibilityFrame>(8);
var lookInPrepare = new HashSet<uint>();
var drawableCells = new HashSet<uint>();
var shellBatch = new HashSet<uint>();
var orderedTransparent = new List<uint>(64);
for (uint id = 0; id < 64; id++)
{
lookInPrepare.Add(id);
drawableCells.Add(id);
shellBatch.Add(id);
orderedTransparent.Add(id);
}
int lookInCapacity = lookInFrames.Capacity;
int prepareCapacity = lookInPrepare.EnsureCapacity(0);
int drawableCapacity = drawableCells.EnsureCapacity(0);
int shellCapacity = shellBatch.EnsureCapacity(0);
int transparentCapacity = orderedTransparent.Capacity;
retention.ClearFrameBuffers(
lookInFrames,
lookInPrepare,
drawableCells,
shellBatch,
orderedTransparent);
Assert.Equal(lookInCapacity, lookInFrames.Capacity);
Assert.Equal(prepareCapacity, lookInPrepare.EnsureCapacity(0));
Assert.Equal(drawableCapacity, drawableCells.EnsureCapacity(0));
Assert.Equal(shellCapacity, shellBatch.EnsureCapacity(0));
Assert.Equal(transparentCapacity, orderedTransparent.Capacity);
}
[Fact]
public void ClearFrameBuffers_RecurringLargeWorkingSet_PreservesWarmCapacity()
{
const int recurringCount =
RetailPViewScratchRetention.MaxRetainedCellItems + 163;
var retention = new RetailPViewScratchRetention();
var lookInFrames = new List<PortalVisibilityFrame>();
var lookInPrepare = new HashSet<uint>();
var drawableCells = new HashSet<uint>();
var shellBatch = new HashSet<uint>();
var orderedTransparent = new List<uint>();
for (int iteration = 0;
iteration < RetailPViewScratchRetention.CapacityTrimIdleFrames + 5;
iteration++)
{
for (int i = 0; i < recurringCount; i++)
{
uint id = (uint)i;
drawableCells.Add(id);
orderedTransparent.Add(id);
}
int drawableCapacity = drawableCells.EnsureCapacity(0);
int transparentCapacity = orderedTransparent.Capacity;
retention.ClearFrameBuffers(
lookInFrames,
lookInPrepare,
drawableCells,
shellBatch,
orderedTransparent);
Assert.Equal(drawableCapacity, drawableCells.EnsureCapacity(0));
Assert.Equal(transparentCapacity, orderedTransparent.Capacity);
}
}
}

View file

@ -1,500 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #118 exit-walk harness (handoff 2026-06-11 §5): the character is clipped and then
/// vanishes for a moment when exiting a house — the window where the player is just
/// outside the door while the collided viewer (camera) is still indoors.
///
/// Per step of a deterministic eye+player path crossing the exit doorway of the
/// Holtburg corner building (cell 0xA9B40170, dat-loaded via the CornerFloodReplay
/// fixture loader), this drives the PRODUCTION decision stack headlessly:
/// viewer-cell resolution (healthy-sweep model, see below) →
/// <see cref="PortalVisibilityBuilder.Build"/> → <see cref="ClipFrameAssembler.Assemble"/> →
/// <see cref="ViewconeCuller.Build"/> → the exact DrawDynamicsLast visibility predicate
/// (RetailPViewRenderer.cs:375-401), PLUS the depth relationship DrawDynamicsLast is
/// subject to: under an INTERIOR root the exit-portal SEAL stamps the door fan at TRUE
/// depth (RetailPViewPassExecutor.DrawPortalDepthWrite, forceFarZ=false) after the full
/// depth clear, and dynamics draw depth-tested AFTER it.
///
/// The four candidates this pins (handoff §5 + this session's read):
/// 1. eye/cell incoherence under damping — EXONERATED BY READ for clean exits:
/// RetailChaseCamera publishes (Position, ViewerCellId) from the SAME SweepEye call
/// (RetailChaseCamera.cs:188-203), published==damped when nothing collides (an open
/// doorway), and GameWindow updates the camera (≈:6889) BEFORE the visibility read
/// (≈:7361) in the same frame.
/// 2. exit-portal side test culling at an ε-outside eye → OutsideView EMPTY →
/// SphereVisibleOutside culls ALL outdoor dynamics. Quantified by the stale-root
/// diagnostic below (the healthy walk should never produce the incoherent pair).
/// 3. doorway-aperture cone tightness → tested per step by the predicate replica.
/// 4. (new, this session) SEAL-DEPTH vs dynamics-last ordering: a player whose
/// fragments lie BEYOND the door plane z-fails against the seal across the whole
/// aperture → invisible while fully outside (and clipped at the plane while
/// straddling). Tested per step by the CPU depth check (same viewProj math the GPU
/// consumes).
///
/// Healthy-sweep model: the corner-seal replay (b21bb28) + the camera read above prove
/// the sweep resolves the eye's ACTUAL cell same-frame, so the harness derives the
/// viewer cell geometrically: door-plane side decides indoor/outdoor; AABB containment
/// (smallest containing volume) picks the interior cell. The outdoor root is
/// <see cref="OutdoorCellNode.Build"/> exactly as GameWindow builds it (full-screen
/// OutsideView ⇒ outdoor dynamics trivially cone-pass).
/// </summary>
[Collection(CameraDiagnosticsCollection.Name)]
[Trait("Lane", "InstalledDat")]
public class HouseExitWalkReplayTests
{
private readonly ITestOutputHelper _out;
public HouseExitWalkReplayTests(ITestOutputHelper output) => _out = output;
private const uint ExitCellId = CornerFloodReplayTests.Landblock | 0x0170u;
// Production humanoid entity sphere (RetailPViewRenderer.EntitySphere: AABB center +
// half-diagonal). A ~1.8 m × 0.6 m character AABB gives center ≈ feet+0.9, r ≈ 1.0.
private const float PlayerSphereRadius = 1.0f;
private static readonly Vector3 PlayerSphereCenterOffset = new(0f, 0f, 0.9f);
private sealed record ExitDoor(
int PortalIndex,
Vector3[] WorldVerts,
Vector3 WorldCentroid,
Vector3 OutwardNormal, // unit, world space, pointing OUT of the building
float FloorZ);
private sealed record WalkStep(
int Index,
float WalkS, // raw walk parameter (feet travel along the XY exit direction)
float CenterS, // signed distance of the SPHERE CENTER to the door plane (the production quantity)
float EyeS, // signed distance of the published eye
Vector3 PlayerFeet,
Vector3 Eye,
uint RootCellId, // 0 = outdoor root
uint PlayerParentCellId,
bool ConeVisible,
bool OutsideStage, // production stage assignment (DynamicDrawsInOutsideStage)
bool DepthCheckApplies,
bool DepthPass,
int OutsidePolys,
int FloodCells);
// ── fixture / geometry ──────────────────────────────────────────────
private static ExitDoor FindExitDoor(LoadedCell cell)
{
int best = -1;
float bestMinZ = float.MaxValue;
for (int i = 0; i < cell.Portals.Count; i++)
{
if (cell.Portals[i].OtherCellId != 0xFFFF) continue;
if (i >= cell.PortalPolygons.Count) continue;
var poly = cell.PortalPolygons[i];
if (poly is null || poly.Length < 3) continue;
float minZ = float.MaxValue;
foreach (var v in poly)
minZ = MathF.Min(minZ, Vector3.Transform(v, cell.WorldTransform).Z);
// a DOOR reaches the floor; a window doesn't — pick the lowest-silled exit portal
if (minZ < bestMinZ) { bestMinZ = minZ; best = i; }
}
Assert.True(best >= 0, $"cell 0x{cell.CellId:X8} has no exit portal (OtherCellId==0xFFFF)");
var local = cell.PortalPolygons[best];
var world = new Vector3[local.Length];
var centroid = Vector3.Zero;
float floorZ = float.MaxValue;
for (int v = 0; v < local.Length; v++)
{
world[v] = Vector3.Transform(local[v], cell.WorldTransform);
centroid += world[v];
floorZ = MathF.Min(floorZ, world[v].Z);
}
centroid /= local.Length;
// Outward = away from the cell interior. ClipPlanes[i].InsideSide encodes which
// side the cell centroid is on (CornerFloodReplayTests.LoadCell): InsideSide==0
// ⇒ interior satisfies dot ≥ 0 ⇒ outward is Normal; InsideSide==1 ⇒ +Normal.
var plane = cell.ClipPlanes[best];
var outwardLocal = plane.InsideSide == 0 ? -plane.Normal : plane.Normal;
var outwardWorld = Vector3.Normalize(Vector3.TransformNormal(outwardLocal, cell.WorldTransform));
return new ExitDoor(best, world, centroid, outwardWorld, floorZ);
}
private static float SignedSide(ExitDoor door, Vector3 p)
=> Vector3.Dot(door.OutwardNormal, p - door.WorldCentroid);
private static uint? ResolveInteriorCellByAabb(
Dictionary<uint, LoadedCell> cells, Vector3 worldPoint, float margin = 0.05f)
{
uint? best = null;
float bestVolume = float.MaxValue;
foreach (var (id, cell) in cells)
{
var local = Vector3.Transform(worldPoint, cell.InverseWorldTransform);
var min = cell.LocalBoundsMin - new Vector3(margin);
var max = cell.LocalBoundsMax + new Vector3(margin);
if (local.X < min.X || local.Y < min.Y || local.Z < min.Z
|| local.X > max.X || local.Y > max.Y || local.Z > max.Z)
continue;
var ext = cell.LocalBoundsMax - cell.LocalBoundsMin;
float volume = MathF.Max(ext.X, 1e-3f) * MathF.Max(ext.Y, 1e-3f) * MathF.Max(ext.Z, 1e-3f);
if (volume < bestVolume) { bestVolume = volume; best = id; }
}
return best;
}
// Outdoor landcell id for a landblock-local position (dat EnvCell positions are
// landblock-local). Only the (low word < 0x100) classification is load-bearing for
// the DrawDynamicsLast predicate; the exact landcell is kept honest anyway.
private static uint OutdoorLandcellId(Vector3 landblockLocalPos)
{
int cx = Math.Clamp((int)MathF.Floor(landblockLocalPos.X / 24f), 0, 7);
int cy = Math.Clamp((int)MathF.Floor(landblockLocalPos.Y / 24f), 0, 7);
return CornerFloodReplayTests.Landblock | (uint)(cx * 8 + cy + 1);
}
// Exact replica of RetailPViewRenderer.DrawDynamicsLast's visibility predicate
// (RetailPViewRenderer.cs:386-391). Keep in lockstep with production.
private static bool DynamicsConeVisible(ViewconeCuller cone, uint parentCellId, Vector3 c, float r)
{
bool indoor = (parentCellId & 0xFFFFu) >= 0x0100u && (parentCellId & 0xFFFFu) != 0xFFFFu;
return indoor
? cone.SphereVisibleInCell(parentCellId, c, r)
: cone.SphereVisibleOutside(c, r);
}
// ── the walk ────────────────────────────────────────────────────────
private List<WalkStep>? RunExitWalk()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return null; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = CornerFloodReplayTests.LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var exitCell = cells[ExitCellId];
var door = FindExitDoor(exitCell);
_out.WriteLine(FormattableString.Invariant(
$"exit door: cell=0x{ExitCellId:X8} portal[{door.PortalIndex}] centroid=({door.WorldCentroid.X:F2},{door.WorldCentroid.Y:F2},{door.WorldCentroid.Z:F2}) outward=({door.OutwardNormal.X:F2},{door.OutwardNormal.Y:F2},{door.OutwardNormal.Z:F2}) floorZ={door.FloorZ:F2} verts={door.WorldVerts.Length}"));
// Walk straight out through the door centroid along the outward normal's XY
// projection (doors are vertical; assert so).
var out2d = Vector3.Normalize(new Vector3(door.OutwardNormal.X, door.OutwardNormal.Y, 0f));
Assert.True(MathF.Abs(door.OutwardNormal.Z) < 0.3f, "exit door is not vertical — walk path invalid");
float yaw = MathF.Atan2(out2d.Y, out2d.X);
const float dt = 1f / 60f;
const float stepLen = 0.02f; // 2 cm per frame = 1.2 m/s walk
const float sStart = -1.2f, sEnd = 3.0f; // door plane at s=0
var velocity = out2d * (stepLen / dt);
var camera = new RetailChaseCamera { Aspect = 1280f / 720f };
Vector3 FeetAt(float s) => new(
door.WorldCentroid.X + out2d.X * s,
door.WorldCentroid.Y + out2d.Y * s,
door.FloorZ);
// Warm the damped boom to convergence at the start pose (retail's convergence
// snap freezes it once the lerp step is sub-epsilon).
for (int i = 0; i < 240; i++)
camera.Update(FeetAt(sStart), yaw, Vector3.Zero, isOnGround: true, Vector3.UnitZ, dt);
var clipFrame = ClipFrame.NoClip();
var steps = new List<WalkStep>();
int stepCount = (int)MathF.Round((sEnd - sStart) / stepLen);
for (int i = 0; i <= stepCount; i++)
{
float s = sStart + i * stepLen;
var feet = FeetAt(s);
camera.Update(feet, yaw, velocity, isOnGround: true, Vector3.UnitZ, dt);
var eye = camera.Position;
var viewProj = camera.View * camera.Projection;
float eyeS = SignedSide(door, eye);
var sphereC = feet + PlayerSphereCenterOffset;
float playerS = SignedSide(door, sphereC);
// membership model: the controller's pick is center point-in-cell (physics
// digest, P1) — the player's ParentCellId flips when the sphere CENTER
// crosses the door plane.
uint playerCell = playerS <= 0f
? ExitCellId
: OutdoorLandcellId(feet);
// healthy-sweep viewer resolution: plane side decides indoor/outdoor;
// AABB containment picks the interior cell the eye is in.
uint rootCellId = 0u;
LoadedCell? root = null;
if (eyeS <= 0f)
{
var contained = ResolveInteriorCellByAabb(cells, eye);
Assert.True(contained.HasValue,
FormattableString.Invariant(
$"step {i}: eye=({eye.X:F2},{eye.Y:F2},{eye.Z:F2}) (eyeS={eyeS:F2}) is inside the door plane but no loaded cell AABB contains it — adjust the walk"));
rootCellId = contained.Value;
root = cells[rootCellId];
}
else
{
root = OutdoorCellNode.Build(OutdoorLandcellId(eye));
}
var pv = PortalVisibilityBuilder.Build(root, eye, lookup, viewProj);
var asm = ClipFrameAssembler.Assemble(clipFrame, pv);
var cone = ViewconeCuller.Build(asm, viewProj);
bool coneVisible = DynamicsConeVisible(cone, playerCell, sphereC, PlayerSphereRadius);
// Production stage assignment (#118 fix): an outside-stage dynamic draws
// BEFORE the depth clear + seal (retail LScape::draw → DrawSortCell), so
// the seal PROTECTS its pixels instead of z-killing them.
var drawable = new HashSet<uint>(pv.OrderedVisibleCells);
bool outsideStage = rootCellId != 0u
&& RetailPViewRenderer.DynamicDrawsInOutsideStage(
playerCell,
sphereC,
PlayerSphereRadius,
drawable,
new TestCellSource(lookup));
// Candidate 4: the seal depth check. Applies when the root is INTERIOR
// (ClearDepthForInterior + the TRUE-depth seal run, GameWindow:7719-7729),
// the player sphere center lies BEYOND the door plane on the ray from the
// eye, AND the player draws in the post-seal last pass (not outside-stage).
bool depthApplies = false, depthPass = true;
if (rootCellId != 0u && coneVisible && playerS > 0f && eyeS < 0f && !outsideStage)
{
depthApplies = TrySealDepthCheck(door, eye, sphereC, viewProj, out depthPass);
}
steps.Add(new WalkStep(
i, s, playerS, eyeS, feet, eye, rootCellId, playerCell, coneVisible, outsideStage,
depthApplies, depthPass,
pv.OutsideView.Polygons.Count, pv.OrderedVisibleCells.Count));
}
return steps;
}
/// <summary>
/// CPU model of the depth relationship at the player-sphere-center pixel:
/// the SEAL (door fan at true depth, drawn after the full depth clear and before
/// dynamics — PortalDepthMaskRenderer, forceFarZ=false) vs the player fragment.
/// Returns true (out: pass) when the player's NDC depth at that pixel is ≤ the
/// seal's. Returns false (check N/A) when the eye→center ray misses the door fan
/// (the seal doesn't cover that pixel; depth there is the cleared far plane).
/// </summary>
private static bool TrySealDepthCheck(
ExitDoor door, Vector3 eye, Vector3 sphereCenter, in Matrix4x4 viewProj, out bool depthPass)
{
depthPass = true;
float dEye = Vector3.Dot(door.OutwardNormal, eye - door.WorldCentroid);
float dC = Vector3.Dot(door.OutwardNormal, sphereCenter - door.WorldCentroid);
if (dEye >= 0f || dC <= 0f) return false; // plane not between eye and center
float t = dEye / (dEye - dC);
var hit = eye + (sphereCenter - eye) * t;
// is the ray-plane hit inside the door polygon? (2D point-in-polygon in the
// plane's basis — the seal only stamps pixels inside the fan)
if (!PointInPolygon(door, hit)) return false;
var pc = Vector4.Transform(new Vector4(sphereCenter, 1f), viewProj);
var hc = Vector4.Transform(new Vector4(hit, 1f), viewProj);
if (pc.W <= 1e-6f || hc.W <= 1e-6f) return false; // behind the eye — no pixel
float playerZ = pc.Z / pc.W;
float sealZ = hc.Z / hc.W;
// GL depth test default LESS/LEQUAL: smaller NDC z = nearer = passes.
depthPass = playerZ <= sealZ + 1e-4f;
return true;
}
private static bool PointInPolygon(ExitDoor door, Vector3 worldPoint)
{
// build a 2D basis in the door plane
var n = door.OutwardNormal;
var u = Vector3.Normalize(Vector3.Cross(n, Vector3.UnitZ));
if (u.LengthSquared() < 1e-6f) u = Vector3.UnitX;
var v = Vector3.Cross(n, u);
Vector2 Project(Vector3 p) => new(
Vector3.Dot(p - door.WorldCentroid, u),
Vector3.Dot(p - door.WorldCentroid, v));
var pt = Project(worldPoint);
bool inside = false;
for (int i = 0, j = door.WorldVerts.Length - 1; i < door.WorldVerts.Length; j = i++)
{
var a = Project(door.WorldVerts[i]);
var b = Project(door.WorldVerts[j]);
if ((a.Y > pt.Y) != (b.Y > pt.Y)
&& pt.X < (b.X - a.X) * (pt.Y - a.Y) / (b.Y - a.Y) + a.X)
inside = !inside;
}
return inside;
}
private void DumpSteps(IEnumerable<WalkStep> steps, Func<WalkStep, bool>? filter = null)
{
foreach (var st in steps)
{
if (filter is not null && !filter(st)) continue;
string root = st.RootCellId == 0 ? "OUTDOOR " : FormattableString.Invariant($"0x{st.RootCellId:X8}");
string cone = st.ConeVisible ? "VIS " : "CULL";
string stage = st.OutsideStage ? "outside" : "last ";
string depth = st.DepthCheckApplies ? (st.DepthPass ? "pass" : "FAIL") : "n/a ";
_out.WriteLine(FormattableString.Invariant(
$"step={st.Index,3} s={st.WalkS,6:F2} cS={st.CenterS,6:F2} eyeS={st.EyeS,6:F2} root={root} pCell=0x{st.PlayerParentCellId:X8} cone={cone} stage={stage} depth={depth} outPolys={st.OutsidePolys} flood={st.FloodCells}"));
}
}
// ── the pins ────────────────────────────────────────────────────────
/// <summary>
/// Candidates 13 (cone level): per step of the exit walk, the player sphere must
/// survive the exact DrawDynamicsLast cone predicate. A failing step pins the
/// side-test / cone-tightness / incoherence family with its exact geometry.
/// </summary>
[Fact]
public void ExitWalk_PlayerStaysConeVisible_EveryStep()
{
var steps = RunExitWalk();
if (steps is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
var failures = steps.FindAll(s => !s.ConeVisible);
if (failures.Count > 0)
{
_out.WriteLine($"--- {failures.Count} cone-CULLED steps ---");
DumpSteps(failures);
}
Assert.True(failures.Count == 0,
$"{failures.Count}/{steps.Count} steps cone-cull the player (first at step {(failures.Count > 0 ? failures[0].Index : -1)}) — see output");
}
/// <summary>
/// Candidate 4 (depth level): on every step where the root is interior and the
/// cone admits the outdoor player, the player's fragments must also SURVIVE the
/// exit-portal SEAL's depth — otherwise DrawDynamicsLast paints nothing (the
/// vanish) and a straddling body is cut at the door plane (the clip).
/// </summary>
[Fact]
public void ExitWalk_PlayerSurvivesSealDepth_WhenConeVisible()
{
var steps = RunExitWalk();
if (steps is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
var applicable = steps.FindAll(s => s.DepthCheckApplies);
_out.WriteLine($"depth check applies on {applicable.Count}/{steps.Count} steps");
var failures = applicable.FindAll(s => !s.DepthPass);
if (failures.Count > 0)
{
_out.WriteLine($"--- {failures.Count} seal-depth-FAILED steps ---");
DumpSteps(failures);
}
Assert.True(failures.Count == 0,
$"{failures.Count}/{applicable.Count} applicable steps z-fail the player against the exit-portal seal — " +
"dynamics drawn after the TRUE-depth seal cannot appear beyond the door plane (see output)");
}
/// <summary>
/// The straddle ("clipped") phase: while the player sphere crosses the door
/// plane under an interior root, it must be assigned to the OUTSIDE stage
/// (drawn pre-seal — for indoor-classified straddlers that is retail's
/// per-overlapped-shadow-cell dual draw, DrawBlock pc:430056-430064), or the
/// beyond-plane body half is cut at the plane by the seal.
/// </summary>
[Fact]
public void ExitWalk_StraddlingPlayerDrawsInOutsideStage()
{
var steps = RunExitWalk();
if (steps is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
var straddling = steps.FindAll(s =>
s.RootCellId != 0u && MathF.Abs(s.CenterS) < PlayerSphereRadius);
Assert.True(straddling.Count > 0, "walk produced no interior-root straddle steps — geometry changed?");
var failures = straddling.FindAll(s => !s.OutsideStage);
if (failures.Count > 0)
{
_out.WriteLine($"--- {failures.Count} straddle steps NOT outside-stage ---");
DumpSteps(failures);
}
Assert.True(failures.Count == 0,
$"{failures.Count}/{straddling.Count} straddle steps are not outside-stage-assigned — the seal clips the body at the door plane");
}
/// <summary>Full per-step table for the handoff doc.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ExitWalk_PerStepTable()
{
var steps = RunExitWalk();
if (steps is null) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
DumpSteps(steps);
// transition summary
int firstOutPlayer = steps.FindIndex(s => s.CenterS > 0f);
int firstOutRoot = steps.FindIndex(s => s.RootCellId == 0u);
_out.WriteLine(FormattableString.Invariant(
$"player center exits at step {firstOutPlayer}; viewer root flips outdoor at step {firstOutRoot}; interior-root/outdoor-player window = {Math.Max(0, firstOutRoot - firstOutPlayer)} steps"));
}
/// <summary>
/// Candidate 2 quantifier (synthetic): the INCOHERENT (root=interior, eye outside)
/// pair the healthy sweep never produces. Documents how the flood degrades if the
/// root ever lagged the eye: within PortalSideEpsilon the exit portal still
/// traverses; beyond it the side test culls and OutsideView goes EMPTY (which would
/// cull ALL outdoor content — terrain included, not just the player).
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_StaleRootWindow_EyeJustOutside()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = CornerFloodReplayTests.LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var exitCell = cells[ExitCellId];
var door = FindExitDoor(exitCell);
var out2d = Vector3.Normalize(new Vector3(door.OutwardNormal.X, door.OutwardNormal.Y, 0f));
var eyeBase = new Vector3(door.WorldCentroid.X, door.WorldCentroid.Y, door.FloorZ + 1.8f);
var playerC = eyeBase + out2d * 1.5f; // player just outside, in front of the eye
var clipFrame = ClipFrame.NoClip();
foreach (float d in new[] { 0.005f, 0.02f, 0.05f, 0.10f, 0.25f })
{
var eye = eyeBase + out2d * d;
var view = Matrix4x4.CreateLookAt(eye, eye + out2d, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 1280f / 720f, 0.1f, 5000f);
var viewProj = view * proj;
var pv = PortalVisibilityBuilder.Build(exitCell, eye, lookup, viewProj);
var asm = ClipFrameAssembler.Assemble(clipFrame, pv);
var cone = ViewconeCuller.Build(asm, viewProj);
bool playerVisible = cone.SphereVisibleOutside(playerC, PlayerSphereRadius);
_out.WriteLine(FormattableString.Invariant(
$"eye {d * 100,5:F1} cm OUTSIDE + root still 0x{ExitCellId:X8}: outPolys={pv.OutsideView.Polygons.Count} flood={pv.OrderedVisibleCells.Count} playerConeVisible={playerVisible}"));
}
}
private sealed class TestCellSource(Func<uint, LoadedCell?> find) :
IRetailPViewCellSource
{
public LoadedCell? Find(uint cellId) => find(cellId);
}
}

View file

@ -1,140 +1,17 @@
using System.Numerics;
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Tests.Architecture;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering;
public sealed class RetailPViewPassExecutorTests
{
[Fact]
public void DrawInside_outdoor_executes_the_real_typed_sequence_without_interior_clear()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
LoadedCell root = OutdoorCellNode.Build(0xA9B40000u);
renderer.DrawInside(Frame(root), executor);
Assert.Equal(
[
"begin",
"assemble",
"append-look-in-clips",
// FW4 slice 1: the clip-region publication moved below the
// walk block (a walk-rooted interior frame re-derives the
// outside-view slices from the walk's own views first, and
// the appended slots must join the same single publication).
"indoor-routing",
"prepare-clip:3",
"prepare-cells",
"diagnostics",
"terrain-clip",
"clear-routing",
"landscape-early",
"terrain-clip",
"clear-routing",
"landscape-late",
"unattached-particles-outdoor",
// #132: an OUTDOOR root does NOT drain at the stage boundary.
// The far prefix drains at the pre-punch barrier (retail
// DrawBuilding @0x0059F2A0 flushes before its portal-only
// far-Z pass), and the full drain runs after the dynamics
// pass, where the frame's opaque world depth is complete.
"indoor-routing",
"indoor-routing",
"landscape-alpha-farther",
"exit-mask",
"indoor-routing",
"opaque-shells",
"landscape-alpha",
"unattached-particles-interior",
],
executor.Operations);
}
[Fact]
public void DrawInside_interior_exit_flushes_before_clear_and_resets_borrowed_result()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
LoadedCell outdoor = OutdoorCellNode.Build(0xA9B40000u);
RetailPViewFrameResult borrowed = renderer.DrawInside(Frame(outdoor), executor);
Assert.Contains(outdoor.CellId, borrowed.DrawableCells);
executor.Operations.Clear();
LoadedCell interior = InteriorWithExit(0xA9B40100u);
RetailPViewFrameResult reused = renderer.DrawInside(Frame(interior), executor);
Assert.Same(borrowed, reused);
Assert.DoesNotContain(outdoor.CellId, reused.DrawableCells);
Assert.Contains(interior.CellId, reused.DrawableCells);
AssertAppearsInOrder(
string.Join('|', executor.Operations),
"landscape-early",
"landscape-late",
"unattached-particles-outdoor",
"landscape-alpha",
"interior-depth-clear",
"indoor-routing",
"exit-mask");
}
[Fact]
public void DrawInside_interior_without_an_outside_slice_skips_landscape_and_depth_clear()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
var root = new LoadedCell
{
CellId = 0xA9B40100u,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
};
renderer.DrawInside(Frame(root), executor);
Assert.DoesNotContain("terrain-clip", executor.Operations);
Assert.DoesNotContain("landscape-early", executor.Operations);
Assert.DoesNotContain("landscape-late", executor.Operations);
Assert.DoesNotContain("landscape-alpha", executor.Operations);
Assert.DoesNotContain("interior-depth-clear", executor.Operations);
}
[Fact]
public void DrawInside_interior_without_an_outside_slice_still_draws_interior_unattached_particles()
{
// Repro (Sanctuary middle cell, looking north): spell ground effects
// vanished whenever no exit portal was in view, because unattached
// emitters submitted once PER outside slice under that slice's
// hardware clip slot — zero slices meant zero submissions. Retail
// draws such an emitter during its owner cell's walk turn
// (ShouldDrawParticles @0x0050FE60) and never clips it to a view.
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
var root = new LoadedCell
{
CellId = 0xA9B40100u,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
};
renderer.DrawInside(Frame(root), executor);
Assert.Contains("unattached-particles-interior", executor.Operations);
Assert.DoesNotContain(
"unattached-particles-outdoor",
executor.Operations);
}
[Fact]
public void Particle_classifications_reset_before_an_empty_following_frame()
{
var classifications = new RetailPViewParticleClassifications();
classifications.ReplaceOutdoor([Entity(7u)]);
classifications.ReplaceOutdoor(new HashSet<uint> { 7u });
classifications.Visible.Add(8u);
classifications.Dynamics.Add(9u);
@ -145,159 +22,6 @@ public sealed class RetailPViewPassExecutorTests
Assert.Empty(classifications.Dynamics);
}
[Fact]
public void DrawInside_real_fixtures_reach_entity_particle_and_transparent_shell_operations()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor { HasTransparentShells = true };
LoadedCell interior = InteriorWithExit(0xA9B40100u);
WorldEntity cellStatic = Entity(10u, parentCellId: interior.CellId);
WorldEntity dynamic = Entity(
11u,
serverGuid: 0x80000011u,
parentCellId: interior.CellId);
renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor);
Assert.Contains("opaque-shells", executor.Operations);
Assert.Contains("transparent-shells-ordered", executor.Operations);
Assert.Contains("entity-bucket", executor.Operations);
Assert.Contains("cell-particles", executor.Operations);
Assert.Contains("dynamics-particles", executor.Operations);
}
[Fact]
public void DrawInside_referee_records_the_exact_routed_entity_buckets()
{
var oracle = new CurrentRenderSceneOracle();
var renderer = new RetailPViewRenderer(oracle);
using var executor = new RecordingExecutor();
LoadedCell interior = InteriorWithExit(0xA9B40100u);
WorldEntity cellStatic = Entity(20u, parentCellId: interior.CellId);
WorldEntity dynamic = Entity(
21u,
serverGuid: 0x80000021u,
parentCellId: interior.CellId);
renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor);
Assert.Equal(1uL, oracle.Snapshot.CompletedPViewFrameSequence);
Assert.Equal(2, oracle.Snapshot.PViewCandidateCount);
Assert.Collection(
oracle.PViewCandidates,
candidate =>
{
Assert.Equal(CurrentRenderPViewRoute.CellStatic, candidate.Route);
Assert.Equal(cellStatic.Id, candidate.Projection.EntityId);
},
candidate =>
{
Assert.Equal(CurrentRenderPViewRoute.DynamicLast, candidate.Route);
Assert.Equal(dynamic.Id, candidate.Projection.EntityId);
});
}
[Fact]
public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
LoadedCell root = InteriorWithExit(0xA9B40100u);
root.BuildingId = 1u;
LoadedCell[] building = NearbyTwoCellBuilding();
renderer.DrawInside(
Frame(
root,
nearbyBuildingCells: building,
additionalCells: building),
executor);
Assert.Contains("look-in-punch", executor.Operations);
AssertAppearsInOrder(
string.Join('|', executor.Operations),
"landscape-early",
"unattached-particles-outdoor",
"landscape-static-particles",
"landscape-alpha-farther",
"look-in-punch",
"landscape-late",
"landscape-alpha",
"interior-depth-clear");
}
[Fact]
public void DrawInside_repaints_the_exterior_building_shell_after_its_look_in()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
LoadedCell root = InteriorWithExit(0xA9B40100u);
root.BuildingId = 1u;
LoadedCell[] building = NearbyTwoCellBuilding();
WorldEntity exteriorShell = Entity(
0x700u,
isBuildingShell: true,
buildingShellAnchorCellId: building[0].CellId);
renderer.DrawInside(
Frame(
root,
[exteriorShell],
nearbyBuildingCells: building,
additionalCells: building),
executor);
AssertAppearsInOrder(
string.Join('|', executor.Operations),
"landscape-early",
"look-in-punch",
"landscape-building-shell",
"landscape-late");
}
[Fact]
public void DrawInside_pairs_each_look_in_with_only_its_own_shell_and_alpha_barrier()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
LoadedCell root = InteriorWithExit(0xA9B40100u);
root.BuildingId = 1u;
LoadedCell[] first = NearbyTwoCellBuilding();
LoadedCell[] second = NearbyTwoCellBuilding(
0xA9B40172u,
0xA9B40173u,
buildingId: 3u);
LoadedCell[] buildings = [.. first, .. second];
WorldEntity[] shells =
[
Entity(
0x700u,
isBuildingShell: true,
buildingShellAnchorCellId: first[0].CellId),
Entity(
0x701u,
isBuildingShell: true,
buildingShellAnchorCellId: second[0].CellId),
];
renderer.DrawInside(
Frame(
root,
shells,
nearbyBuildingCells: buildings,
additionalCells: buildings),
executor);
AssertAppearsInOrder(
string.Join('|', executor.Operations),
"look-in-punch",
"landscape-building-shell",
"landscape-static-particles",
"landscape-alpha-farther",
"look-in-punch",
"landscape-building-shell");
}
[Fact]
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
{
@ -330,7 +54,8 @@ public sealed class RetailPViewPassExecutorTests
nameof(RetailPViewParticleClassifications.BeginFrame)) >= 0);
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
nameof(RetailPViewPassExecutor.DrawLandscapeSlice))!;
"DrawWalkTerrainSlice",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
int diagnosticsBegin = RequiredCallIndex(
landscapeCalls,
@ -350,7 +75,7 @@ public sealed class RetailPViewPassExecutorTests
}
[Fact]
public void GameWindow_composes_one_executor_instead_of_a_draw_callback_bag()
public void Frame_composition_constructs_one_walk_executor()
{
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
"ComposeCore",
@ -377,368 +102,15 @@ public sealed class RetailPViewPassExecutorTests
&& call.Target.Name == ".ctor");
}
private static RetailPViewFrameInput Frame(
LoadedCell root,
IReadOnlyList<WorldEntity>? entities = null,
IReadOnlyList<LoadedCell>? nearbyBuildingCells = null,
IReadOnlyList<LoadedCell>? additionalCells = null)
{
var cells = new Dictionary<uint, LoadedCell> { [root.CellId] = root };
if (additionalCells is not null)
{
foreach (LoadedCell cell in additionalCells)
cells[cell.CellId] = cell;
}
var entries = new List<(
uint LandblockId,
Vector3 AabbMin,
Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)>();
if (entities is not null)
{
entries.Add((
root.CellId & 0xFFFF0000u,
Vector3.Zero,
Vector3.Zero,
entities,
null));
}
return new RetailPViewFrameInput().Reset(
root,
nearbyBuildingCells,
Vector3.Zero,
TestCamera.ViewProjection,
new DictionaryCellSource(cells),
new TestCamera(),
Vector3.Zero,
frustum: null,
playerLandblockId: root.CellId & 0xFFFF0000u,
animatedEntityIds: null,
renderCenterLbX: 0,
renderCenterLbY: 0,
renderRadius: 1,
landblockEntries: entries,
renderSky: true,
renderWeather: true,
dayFraction: 0f,
activeDayGroup: null,
skyKeyframe: default,
environOverrideActive: false,
viewerCellId: root.CellId,
playerCellId: root.CellId,
playerViewPosition: Vector3.Zero,
cameraView: TestCamera.ViewMatrix,
cameraCellResolution: CameraCellResolution.None);
}
private static LoadedCell InteriorWithExit(uint cellId)
{
var cell = new LoadedCell
{
CellId = cellId,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals = [new CellPortalInfo(0xFFFF, 0, 0, 0)],
};
cell.PortalPolygons.Add(
[
new Vector3(-1f, -1f, -2f),
new Vector3(1f, -1f, -2f),
new Vector3(1f, 1f, -2f),
new Vector3(-1f, 1f, -2f),
]);
return cell;
}
private static LoadedCell[] NearbyTwoCellBuilding(
uint vestibuleId = 0xA9B40170u,
uint roomId = 0xA9B40171u,
uint buildingId = 2u)
{
var vestibule = new LoadedCell
{
CellId = vestibuleId,
BuildingId = buildingId,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals =
[
new CellPortalInfo(0xFFFF, 0, 0, 0),
new CellPortalInfo((ushort)(roomId & 0xFFFFu), 1, 0, 0),
],
ClipPlanes =
[
new PortalClipPlane
{
Normal = new Vector3(0, 0, 1),
D = 3f,
InsideSide = 1,
},
],
};
vestibule.PortalPolygons.Add(
[
new Vector3(-0.5f, -0.5f, -2f),
new Vector3(0.5f, -0.5f, -2f),
new Vector3(0.5f, 0.5f, -2f),
new Vector3(-0.5f, 0.5f, -2f),
]);
vestibule.PortalPolygons.Add(
[
new Vector3(-0.6f, -0.6f, -4f),
new Vector3(0.6f, -0.6f, -4f),
new Vector3(0.6f, 0.6f, -4f),
new Vector3(-0.6f, 0.6f, -4f),
]);
var room = new LoadedCell
{
CellId = roomId,
BuildingId = buildingId,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals =
[
new CellPortalInfo(
(ushort)(vestibuleId & 0xFFFFu),
0,
0,
1),
],
};
room.PortalPolygons.Add(
[
new Vector3(-0.6f, -0.6f, -4f),
new Vector3(0.6f, -0.6f, -4f),
new Vector3(0.6f, 0.6f, -4f),
new Vector3(-0.6f, 0.6f, -4f),
]);
return [vestibule, room];
}
private static WorldEntity Entity(
uint id,
uint serverGuid = 0,
uint? parentCellId = null,
bool isBuildingShell = false,
uint? buildingShellAnchorCellId = null) => new()
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = [new MeshRef(1u, Matrix4x4.Identity)],
ParentCellId = parentCellId,
IsBuildingShell = isBuildingShell,
BuildingShellAnchorCellId = buildingShellAnchorCellId,
};
private static void AssertAppearsInOrder(string source, params string[] needles)
{
int cursor = -1;
foreach (string needle in needles)
{
int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal);
Assert.True(next > cursor, $"Missing or out-of-order fragment: {needle}");
cursor = next;
}
}
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
Assert.True(
index >= 0,
$"Expected call to {declaringType.Name}.{methodName}.");
return index;
}
private sealed class DictionaryCellSource(
IReadOnlyDictionary<uint, LoadedCell> cells) : IRetailPViewCellSource
{
public LoadedCell? Find(uint cellId) =>
cells.TryGetValue(cellId, out LoadedCell? cell) ? cell : null;
}
private sealed class TestCamera : ICamera
{
public static Matrix4x4 ViewMatrix { get; } = Matrix4x4.CreateLookAt(
Vector3.Zero,
new Vector3(0, 0, -1),
Vector3.UnitY);
public static Matrix4x4 ViewProjection { get; } = ViewMatrix
* Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
public Matrix4x4 View => ViewMatrix;
public Matrix4x4 Projection { get; } =
Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
public float Aspect { get; set; } = 1f;
}
private sealed class RecordingExecutor :
IRetailPViewPassExecutor,
IRenderFrameEntityPassExecutor,
IDisposable
{
public void AbortFrame() => Operations.Add("abort");
private readonly ClipFrame _clipFrame = ClipFrame.NoClip();
public List<string> Operations { get; } = [];
public bool HasTransparentShells { get; init; }
public void BeginFrame() => Operations.Add("begin");
public ClipFrameAssembly AssembleClipFrame(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly reuseAssembly)
{
Operations.Add("assemble");
return ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
}
public void AppendLookInClipFrames(
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
ClipFrameAssembly assembly)
{
Operations.Add("append-look-in-clips");
ClipFrameAssembler.AppendLookInFrames(
_clipFrame,
lookInFrames,
assembly);
}
public void PrepareClipFrame(int terrainUploadCount) =>
Operations.Add($"prepare-clip:{terrainUploadCount}");
public void SetTerrainClip(ReadOnlySpan<Vector4> planes) =>
Operations.Add("terrain-clip");
public void ClearClipRouting() => Operations.Add("clear-routing");
public void UseIndoorMembershipOnlyRouting() => Operations.Add("indoor-routing");
public void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice) =>
Operations.Add($"cell-portal-routing:{cellId:X8}:{slice.Slot}");
public void PrepareCellBatches(RetailPViewFrameInput frame, HashSet<uint> visibleCellIds) =>
Operations.Add("prepare-cells");
public void DrawOpaqueCellShells(HashSet<uint> cellIds) => Operations.Add("opaque-shells");
public bool CellHasTransparentShell(uint cellId) => HasTransparentShells;
public void DrawTransparentCellShells(HashSet<uint> cellIds) => Operations.Add("transparent-shells");
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
Operations.Add("transparent-shells-ordered");
public void DrawEntityBucket(
RetailPViewFrameInput frame,
IReadOnlyList<WorldEntity> entities,
HashSet<uint>? visibleCellIds) => Operations.Add("entity-bucket");
public void EmitClipRouteProbe(
ClipFrameAssembly clipAssembly,
ClipViewSlice slice,
int sliceIndex) => Operations.Add("clip-probe");
public void DrawLandscapeSlice(
RetailPViewFrameInput frame,
RetailPViewLandscapeSliceContext context)
{
Operations.Add("landscape-early");
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView view = request.View;
DrawEntityRoute(
frame.Camera,
in view,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
}
public void DrawLandscapeSliceLate(
RetailPViewFrameInput frame,
RetailPViewLandscapeLateSliceContext context)
{
Operations.Add("landscape-late");
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView view = request.View;
DrawEntityRoute(
frame.Camera,
in view,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
}
public void DrawLandscapeStaticParticles(
RetailPViewFrameInput frame,
RetailPViewLandscapeStaticParticleContext context) =>
Operations.Add("landscape-static-particles");
public void DrawLandscapeBuildingShellSlice(
RetailPViewFrameInput frame,
RetailPViewLandscapeBuildingShellSliceContext context)
{
Operations.Add("landscape-building-shell");
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView view = request.View;
DrawEntityRoute(
frame.Camera,
in view,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
}
public void ClearInteriorDepth() => Operations.Add("interior-depth-clear");
public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask");
public void DrawLookInPortalPunch(
RetailPViewFrameInput frame,
RetailPViewCellSliceContext context,
int portalIndex) => Operations.Add("look-in-punch");
public void DrawUnattachedSceneParticles(
RetailPViewFrameInput frame,
bool outdoorCells) => Operations.Add(
outdoorCells
? "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,
IReadOnlySet<uint> ownerIds) =>
Operations.Add("dynamics-particles");
public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics");
public void BeginEntityFrame(in RenderFrameView view) =>
Operations.Add("entity-frame-begin");
public bool DrawEntityRoute(
ICamera camera,
in RenderFrameView view,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId,
uint tupleLandblockId)
{
Operations.Add(
$"entity-route:{route}:{routeIndex}:{cellId:X8}");
return true;
}
public void CompleteEntityFrame(in RenderFrameView view) =>
Operations.Add("entity-frame-complete");
public void AbortEntityFrame() =>
Operations.Add("entity-frame-abort");
public void Dispose() => _clipFrame.Dispose();
}
}

View file

@ -738,15 +738,14 @@ public sealed class WorldSceneRendererTests
_calls = calls;
// Distinct flood-only vs in-view sets: 0x01010003 is a look-in
// cell that is drawn but never part of the main flood.
var interiorPartition = new InteriorEntityPartition.Result();
_interiorResult = new RetailPViewFrameResult().Reset(
new PortalVisibilityFrame(),
new ClipFrameAssembly(),
[0x01010001u],
[0x01010001u, 0x01010003u],
RetailPViewRenderer.LegacyDiagnosticCounts(interiorPartition),
RetailPViewRenderer.LegacySourceCounts(interiorPartition),
interiorPartition);
default,
default,
diagnosticPartition: null);
var outdoorPortalFrame = new PortalVisibilityFrame();
outdoorPortalFrame.OutsideView.Add(new ViewPolygon(
[
@ -759,7 +758,10 @@ public sealed class WorldSceneRendererTests
outdoorPortalFrame,
ClipFrameAssembler.Assemble(ClipFrame.NoClip(), outdoorPortalFrame),
[],
new InteriorEntityPartition.Result());
[],
default,
default,
diagnosticPartition: null);
}
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; } =