acdream/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs

540 lines
20 KiB
C#

using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
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",
"prepare-clip:3",
"indoor-routing",
"prepare-cells",
"diagnostics",
"terrain-clip",
"clear-routing",
"landscape-early",
"terrain-clip",
"clear-routing",
"outstage-routing",
"landscape-late",
"landscape-alpha",
"indoor-routing",
"indoor-routing",
"exit-mask",
"indoor-routing",
"opaque-shells",
],
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",
"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 Particle_classifications_reset_before_an_empty_following_frame()
{
var classifications = new RetailPViewParticleClassifications();
classifications.ReplaceOutdoor([Entity(7u)]);
classifications.Visible.Add(8u);
classifications.Dynamics.Add(9u);
classifications.BeginFrame();
Assert.Empty(classifications.Outdoor);
Assert.Empty(classifications.Visible);
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);
Assert.Contains("phantom-objects", executor.Operations);
}
[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);
}
[Fact]
public void Renderer_source_preserves_retail_stage_order_through_typed_operations()
{
string source = RendererSource();
string drawInside = MethodBody(source, "public RetailPViewFrameResult DrawInside(");
AssertAppearsInOrder(
drawInside,
"passes.BeginFrame();",
"passes.EmitDiagnostics(ctx, result);",
"DrawLandscapeThroughOutsideView(ctx, passes",
"DrawExitPortalMasks(ctx, passes",
"DrawEnvCellShells(passes, pvFrame);",
"DrawCellObjectLists(ctx, passes",
"DrawDynamicsLast(ctx, passes");
string landscape = MethodBody(
source,
"private void DrawLandscapeThroughOutsideView(");
AssertAppearsInOrder(
landscape,
"passes.SetTerrainClip(slice.Planes);",
"passes.DrawLandscapeSlice(ctx",
"DrawBuildingLookIns(ctx, passes",
"passes.DrawLandscapeSliceLate(ctx",
"passes.DrawUnattachedSceneParticles(ctx);",
"passes.FlushLandscapeAlpha();",
"passes.ClearInteriorDepth();");
string lookIns = MethodBody(source, "private void DrawBuildingLookIns(");
AssertAppearsInOrder(
lookIns,
"passes.DrawLookInPortalPunch(ctx",
"passes.DrawOpaqueCellShells(_shellBatch);",
"passes.DrawCellParticles(ctx");
}
[Fact]
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
{
Assert.DoesNotContain(
typeof(RetailPViewFrameInput).GetProperties(),
property => typeof(Delegate).IsAssignableFrom(property.PropertyType));
FieldInfo[] fields = typeof(RetailPViewPassExecutor).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(CellVisibility));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameInput));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameResult));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(ClipFrameAssembly));
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
[Fact]
public void Concrete_executor_forwards_frame_reset_and_brackets_terrain_diagnostics()
{
string source = ExecutorClassSource();
string begin = MethodBody(source, "public void BeginFrame()");
Assert.Contains("_particleClassifications.BeginFrame();", begin);
string landscape = MethodBody(source, "public void DrawLandscapeSlice(");
AssertAppearsInOrder(
landscape,
"_terrainDiagnostics.Begin();",
"_terrain?.Draw(",
"_terrainDiagnostics.Complete();");
Assert.DoesNotContain("Console.WriteLine", source);
Assert.DoesNotContain("Console.WriteLine", RendererSource());
}
[Fact]
public void GameWindow_composes_one_executor_instead_of_a_draw_callback_bag()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Contains("new AcDream.App.Rendering.RetailPViewPassExecutor(", source);
Assert.Contains("new AcDream.App.Rendering.RetailPViewFrameInput", source);
Assert.DoesNotContain("RetailPViewDrawContext", source);
Assert.DoesNotContain("DrawLandscapeSlice =", source);
Assert.DoesNotContain("DrawExitPortalMasks =", source);
}
private static string RendererSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"RetailPViewRenderer.cs"));
private static string ExecutorSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"RetailPViewPassExecutor.cs"));
private static string ExecutorClassSource()
{
string source = ExecutorSource();
int start = source.IndexOf(
"internal sealed class RetailPViewPassExecutor",
StringComparison.Ordinal);
Assert.True(start >= 0, "Missing concrete RetailPViewPassExecutor.");
return source[start..];
}
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
{
RootCell = root,
NearbyBuildingCells = nearbyBuildingCells,
ViewerEyePos = Vector3.Zero,
ViewProjection = TestCamera.ViewProjection,
Cells = new DictionaryCellSource(cells),
Camera = new TestCamera(),
CameraWorldPosition = 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()
{
const uint vestibuleId = 0xA9B40170u;
const uint roomId = 0xA9B40171u;
var vestibule = new LoadedCell
{
CellId = vestibuleId,
BuildingId = 2u,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals =
[
new CellPortalInfo(0xFFFF, 0, 0, 0),
new CellPortalInfo(0x0171, 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 = 2u,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals = [new CellPortalInfo(0x0170, 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) => new()
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = [new MeshRef(1u, Matrix4x4.Identity)],
ParentCellId = parentCellId,
};
private static string MethodBody(string source, string signature)
{
int start = source.IndexOf(signature, StringComparison.Ordinal);
Assert.True(start >= 0, $"Missing method: {signature}");
int open = source.IndexOf('{', start);
int depth = 0;
for (int index = open; index < source.Length; index++)
{
if (source[index] == '{')
depth++;
else if (source[index] == '}' && --depth == 0)
return source[start..(index + 1)];
}
throw new InvalidOperationException($"Unterminated method: {signature}");
}
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 string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
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, IDisposable
{
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 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 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 EmitOutStageOwner(
WorldEntity entity,
Vector3 sphereCenter,
float sphereRadius,
int sliceIndex,
bool passed) => Operations.Add("outstage-owner");
public void EmitOutStageRouting(
int sliceIndex,
IReadOnlyList<WorldEntity> entities,
ViewconeCuller viewcone) => Operations.Add("outstage-routing");
public void EmitPhantomObjects(uint cellId, int survivorCount) =>
Operations.Add("phantom-objects");
public void DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context) => Operations.Add("landscape-early");
public void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context) => Operations.Add("landscape-late");
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) => Operations.Add("look-in-punch");
public void DrawUnattachedSceneParticles(RetailPViewFrameInput frame) => Operations.Add("unattached-particles");
public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha");
public void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles");
public void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> survivors) => Operations.Add("dynamics-particles");
public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics");
public void Dispose() => _clipFrame.Dispose();
}
}