refactor(render): delete legacy pview execution machinery
This commit is contained in:
parent
1be4889c1c
commit
69e69408e8
9 changed files with 58 additions and 3627 deletions
|
|
@ -494,13 +494,18 @@ internal sealed class FrameRootCompositionPhase
|
||||||
d.ParticleVisibility,
|
d.ParticleVisibility,
|
||||||
new WorldScenePViewRenderer(
|
new WorldScenePViewRenderer(
|
||||||
new RetailPViewRenderer(
|
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
|
// Campaign FW3.2b-2: the walk's production world
|
||||||
// data, published/retired with each landblock by the
|
// data, published/retired with each landblock by the
|
||||||
// render publisher (FW3.1).
|
// render publisher (FW3.1).
|
||||||
live.LandblockPipeline.RenderPublisher?.WalkBuildings,
|
live.LandblockPipeline.RenderPublisher?.WalkBuildings
|
||||||
live.LandblockPipeline.RenderPublisher?.WalkLandscape,
|
?? 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),
|
d.CellVisibility),
|
||||||
retailPViewPassExecutor,
|
retailPViewPassExecutor,
|
||||||
retailPViewPassExecutor),
|
retailPViewPassExecutor),
|
||||||
|
|
|
||||||
|
|
@ -86,37 +86,12 @@ internal interface IOutdoorSceneParticleOwnerSource
|
||||||
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
|
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>
|
/// <summary>
|
||||||
/// Campaign V slice V6j: backend-neutral. The order it implements is retail's and
|
/// 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
|
/// is written once; the four places it touches graphics state directly are owned
|
||||||
/// by <see cref="IWorldPassSurface"/>.
|
/// by <see cref="IWorldPassSurface"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed partial class RetailPViewPassExecutor :
|
internal sealed partial class RetailPViewPassExecutor :
|
||||||
IRetailPViewPassExecutor,
|
|
||||||
IOutdoorSceneParticleOwnerSource
|
IOutdoorSceneParticleOwnerSource
|
||||||
{
|
{
|
||||||
private readonly IWorldPassSurface _surface;
|
private readonly IWorldPassSurface _surface;
|
||||||
|
|
@ -134,15 +109,9 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
|
||||||
private readonly RetailPViewParticleClassifications _particleClassifications = new();
|
private readonly RetailPViewParticleClassifications _particleClassifications = new();
|
||||||
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
|
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
|
||||||
private readonly Dictionary<uint, int> _singleCellClipRouting = new(1);
|
// ACDREAM_PROBE_WALK_ROOT / cathedral FW4: observation-only counter for
|
||||||
private readonly Dictionary<uint, int> _noCellClipRouting = new(0);
|
// the walk-owned far-Z portal punches.
|
||||||
|
|
||||||
// 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.
|
|
||||||
internal int WalkLookInPunchCountThisFrame { get; private set; }
|
internal int WalkLookInPunchCountThisFrame { get; private set; }
|
||||||
internal int LegacyLookInPunchCountThisFrame { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Borrowed until the next late landscape pass. The outdoor-root post-world
|
/// 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";
|
AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "pre";
|
||||||
WalkLookInPunchCountThisFrame = 0;
|
WalkLookInPunchCountThisFrame = 0;
|
||||||
LegacyLookInPunchCountThisFrame = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,11 +210,6 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
ClipFrameAssembly reuseAssembly) =>
|
ClipFrameAssembly reuseAssembly) =>
|
||||||
ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
|
ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
|
||||||
|
|
||||||
public void AppendLookInClipFrames(
|
|
||||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
|
||||||
ClipFrameAssembly assembly) =>
|
|
||||||
ClipFrameAssembler.AppendLookInFrames(_clipFrame, lookInFrames, assembly);
|
|
||||||
|
|
||||||
public void PrepareClipFrame(int terrainUploadCount) =>
|
public void PrepareClipFrame(int terrainUploadCount) =>
|
||||||
_surface.PrepareClipFrame(terrainUploadCount);
|
_surface.PrepareClipFrame(terrainUploadCount);
|
||||||
|
|
||||||
|
|
@ -263,24 +226,6 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
_entities.ClearClipRouting();
|
_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(
|
public void PrepareCellBatches(
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
HashSet<uint> visibleCellIds) =>
|
HashSet<uint> visibleCellIds) =>
|
||||||
|
|
@ -298,112 +243,9 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
public bool CellHasTransparentShell(uint cellId) =>
|
public bool CellHasTransparentShell(uint cellId) =>
|
||||||
_envCells.CellHasTransparent(cellId);
|
_envCells.CellHasTransparent(cellId);
|
||||||
|
|
||||||
public void DrawTransparentCellShells(HashSet<uint> cellIds) =>
|
|
||||||
_envCells.Render(WbRenderPass.Transparent, cellIds);
|
|
||||||
|
|
||||||
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
|
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
|
||||||
_envCells.RenderTransparentOrdered(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(
|
public void DrawLandscapeSliceLate(
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
RetailPViewLandscapeLateSliceContext context)
|
RetailPViewLandscapeLateSliceContext context)
|
||||||
|
|
@ -503,38 +345,6 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
DisableClipDistances();
|
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()
|
public void ClearInteriorDepth()
|
||||||
{
|
{
|
||||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||||
|
|
@ -547,23 +357,6 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
RetailPViewCellSliceContext context) =>
|
RetailPViewCellSliceContext context) =>
|
||||||
DrawPortalDepthWrite(context, frame, forceFarZ: frame.RootCell.IsOutdoorNode);
|
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(
|
public void DrawUnattachedSceneParticles(
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
bool outdoorCells)
|
bool outdoorCells)
|
||||||
|
|
@ -643,9 +436,6 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
|
|
||||||
public void FlushLandscapeAlpha() => _alpha.Flush();
|
public void FlushLandscapeAlpha() => _alpha.Flush();
|
||||||
|
|
||||||
public void FlushLandscapeAlphaFartherThan(float minViewerDistance) =>
|
|
||||||
_alpha.FlushFartherThan(minViewerDistance);
|
|
||||||
|
|
||||||
public void DrawCellParticles(
|
public void DrawCellParticles(
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
RetailPViewCellSliceContext context)
|
RetailPViewCellSliceContext context)
|
||||||
|
|
@ -676,29 +466,6 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
DisableClipDistances();
|
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(
|
public void EmitDiagnostics(
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
RetailPViewFrameResult result) =>
|
RetailPViewFrameResult result) =>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -30,8 +30,7 @@ namespace AcDream.App.Rendering.Walk;
|
||||||
/// </list>
|
/// </list>
|
||||||
///
|
///
|
||||||
/// The tuple landblock id handed to the classifier is the frame's player
|
/// The tuple landblock id handed to the classifier is the frame's player
|
||||||
/// landblock — the packed path's own convention for every
|
/// landblock, matching the production walk's retained-scene query convention.
|
||||||
/// <c>RenderFrameEntityDrawRequest</c>.
|
|
||||||
///
|
///
|
||||||
/// <para>Campaign FW3.4a: <see cref="GetCellStatics"/>, <see cref="GetOutdoorStatics"/>,
|
/// <para>Campaign FW3.4a: <see cref="GetCellStatics"/>, <see cref="GetOutdoorStatics"/>,
|
||||||
/// and <see cref="GetBuildingShellStatics"/> used to materialize their result
|
/// and <see cref="GetBuildingShellStatics"/> used to materialize their result
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,12 @@ internal interface IPreparedWorldSceneFramePhase : IWorldSceneFramePhase
|
||||||
internal sealed class WorldScenePViewRenderer : IWorldScenePViewRenderer
|
internal sealed class WorldScenePViewRenderer : IWorldScenePViewRenderer
|
||||||
{
|
{
|
||||||
private readonly RetailPViewRenderer _renderer;
|
private readonly RetailPViewRenderer _renderer;
|
||||||
private readonly IRetailPViewPassExecutor _passes;
|
private readonly RetailPViewPassExecutor _passes;
|
||||||
private readonly IOutdoorSceneParticleOwnerSource _particles;
|
private readonly IOutdoorSceneParticleOwnerSource _particles;
|
||||||
|
|
||||||
public WorldScenePViewRenderer(
|
public WorldScenePViewRenderer(
|
||||||
RetailPViewRenderer renderer,
|
RetailPViewRenderer renderer,
|
||||||
IRetailPViewPassExecutor passes,
|
RetailPViewPassExecutor passes,
|
||||||
IOutdoorSceneParticleOwnerSource particles)
|
IOutdoorSceneParticleOwnerSource particles)
|
||||||
{
|
{
|
||||||
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 1–3 (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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +1,17 @@
|
||||||
using System.Numerics;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.App.Composition;
|
using AcDream.App.Composition;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Scene;
|
|
||||||
using AcDream.App.Tests.Architecture;
|
using AcDream.App.Tests.Architecture;
|
||||||
using AcDream.Core.World;
|
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
public sealed class RetailPViewPassExecutorTests
|
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]
|
[Fact]
|
||||||
public void Particle_classifications_reset_before_an_empty_following_frame()
|
public void Particle_classifications_reset_before_an_empty_following_frame()
|
||||||
{
|
{
|
||||||
var classifications = new RetailPViewParticleClassifications();
|
var classifications = new RetailPViewParticleClassifications();
|
||||||
classifications.ReplaceOutdoor([Entity(7u)]);
|
classifications.ReplaceOutdoor(new HashSet<uint> { 7u });
|
||||||
classifications.Visible.Add(8u);
|
classifications.Visible.Add(8u);
|
||||||
classifications.Dynamics.Add(9u);
|
classifications.Dynamics.Add(9u);
|
||||||
|
|
||||||
|
|
@ -145,159 +22,6 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
Assert.Empty(classifications.Dynamics);
|
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]
|
[Fact]
|
||||||
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
|
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
|
||||||
{
|
{
|
||||||
|
|
@ -330,7 +54,8 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
nameof(RetailPViewParticleClassifications.BeginFrame)) >= 0);
|
nameof(RetailPViewParticleClassifications.BeginFrame)) >= 0);
|
||||||
|
|
||||||
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
|
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
|
||||||
nameof(RetailPViewPassExecutor.DrawLandscapeSlice))!;
|
"DrawWalkTerrainSlice",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
|
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
|
||||||
int diagnosticsBegin = RequiredCallIndex(
|
int diagnosticsBegin = RequiredCallIndex(
|
||||||
landscapeCalls,
|
landscapeCalls,
|
||||||
|
|
@ -350,7 +75,7 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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(
|
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
|
||||||
"ComposeCore",
|
"ComposeCore",
|
||||||
|
|
@ -377,368 +102,15 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
&& call.Target.Name == ".ctor");
|
&& 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(
|
private static int RequiredCallIndex(
|
||||||
IReadOnlyList<CompiledCall> calls,
|
IReadOnlyList<CompiledCall> calls,
|
||||||
Type declaringType,
|
Type declaringType,
|
||||||
string methodName)
|
string methodName)
|
||||||
{
|
{
|
||||||
int index = CompiledCallGraph.IndexOf(calls, declaringType, 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;
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -738,15 +738,14 @@ public sealed class WorldSceneRendererTests
|
||||||
_calls = calls;
|
_calls = calls;
|
||||||
// Distinct flood-only vs in-view sets: 0x01010003 is a look-in
|
// Distinct flood-only vs in-view sets: 0x01010003 is a look-in
|
||||||
// cell that is drawn but never part of the main flood.
|
// cell that is drawn but never part of the main flood.
|
||||||
var interiorPartition = new InteriorEntityPartition.Result();
|
|
||||||
_interiorResult = new RetailPViewFrameResult().Reset(
|
_interiorResult = new RetailPViewFrameResult().Reset(
|
||||||
new PortalVisibilityFrame(),
|
new PortalVisibilityFrame(),
|
||||||
new ClipFrameAssembly(),
|
new ClipFrameAssembly(),
|
||||||
[0x01010001u],
|
[0x01010001u],
|
||||||
[0x01010001u, 0x01010003u],
|
[0x01010001u, 0x01010003u],
|
||||||
RetailPViewRenderer.LegacyDiagnosticCounts(interiorPartition),
|
default,
|
||||||
RetailPViewRenderer.LegacySourceCounts(interiorPartition),
|
default,
|
||||||
interiorPartition);
|
diagnosticPartition: null);
|
||||||
var outdoorPortalFrame = new PortalVisibilityFrame();
|
var outdoorPortalFrame = new PortalVisibilityFrame();
|
||||||
outdoorPortalFrame.OutsideView.Add(new ViewPolygon(
|
outdoorPortalFrame.OutsideView.Add(new ViewPolygon(
|
||||||
[
|
[
|
||||||
|
|
@ -759,7 +758,10 @@ public sealed class WorldSceneRendererTests
|
||||||
outdoorPortalFrame,
|
outdoorPortalFrame,
|
||||||
ClipFrameAssembler.Assemble(ClipFrame.NoClip(), outdoorPortalFrame),
|
ClipFrameAssembler.Assemble(ClipFrame.NoClip(), outdoorPortalFrame),
|
||||||
[],
|
[],
|
||||||
new InteriorEntityPartition.Result());
|
[],
|
||||||
|
default,
|
||||||
|
default,
|
||||||
|
diagnosticPartition: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; } =
|
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; } =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue