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", "Composition", "FrameRootComposition.cs")); Assert.Contains("new RetailPViewPassExecutor(", source); Assert.Contains("new WorldScenePViewRenderer(", source); Assert.DoesNotContain("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? entities = null, IReadOnlyList? nearbyBuildingCells = null, IReadOnlyList? additionalCells = null) { var cells = new Dictionary { [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 Entities, IReadOnlyDictionary? 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 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 { public void AbortFrame() => Operations.Add("abort"); private readonly ClipFrame _clipFrame = ClipFrame.NoClip(); public List 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 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 visibleCellIds) => Operations.Add("prepare-cells"); public void DrawOpaqueCellShells(HashSet cellIds) => Operations.Add("opaque-shells"); public bool CellHasTransparentShell(uint cellId) => HasTransparentShells; public void DrawTransparentCellShells(HashSet cellIds) => Operations.Add("transparent-shells"); public void DrawTransparentCellShellsOrdered(IReadOnlyList cellIds) => Operations.Add("transparent-shells-ordered"); public void DrawEntityBucket( RetailPViewFrameInput frame, IReadOnlyList entities, HashSet? 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 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 survivors) => Operations.Add("dynamics-particles"); public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics"); public void Dispose() => _clipFrame.Dispose(); } }