using System.Collections.Concurrent; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Reflection; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu.Vk; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Selection; using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Walk; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using AcDream.Core.Meshing; using AcDream.Core.World; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Lib.IO; using Microsoft.Extensions.Logging.Abstractions; namespace AcDream.App.Tests.Rendering.Walk; /// /// Campaign FW stage FW3.2a: the walk→draw population layer's data- /// equivalence referee. Covers /// (the shared per-entity classify seam), /// (opaque → , translucent → the alpha queue, /// selection publish), and the FW3.2a own-cull-scratch fix to the ordered /// submitter (PrepareOrderedStream/DrawOrderedRange as of /// Campaign FW stage FW3.4a). /// public sealed class WalkStaticStreamPopulatorTests { // ── Test doubles ──────────────────────────────────────────────────────── private sealed class RecordingSelectionSink : IRetailSelectionRenderSink { public readonly List<(uint ServerGuid, uint LocalEntityId, int PartIndex, uint GfxObjId, Matrix4x4 LocalToWorld)> Calls = new(); public void AddVisiblePart( uint serverGuid, uint localEntityId, int partIndex, uint gfxObjId, Matrix4x4 partWorld) => Calls.Add((serverGuid, localEntityId, partIndex, gfxObjId, partWorld)); } private sealed class FixedWalkViews(params uint[] slots) : IWalkLookInViewSource { public IReadOnlyList LookInCellTurns { get; } = [0x8C040112u]; public bool SphereVisibleInLookInTurn( int routeIndex, in Vector3 center, float radius, bool testSphere = true) => slots.Length != 0; } // ── Synthetic RenderProjectionRecord construction ────────────────────── private static RenderProjectionRecord MakeRecord( uint localEntityId, uint serverGuid, Vector3 position, IReadOnlyList meshRefs, bool isBuildingShell = false, uint parentCellId = 0u, RenderCasterIdentityKind casterIdentity = RenderCasterIdentityKind.Unclassified) => new( Id: RenderProjectionId.FromRaw(localEntityId), ProjectionClass: RenderProjectionClass.OutdoorStatic, OwnerIncarnation: RenderOwnerIncarnation.FromRaw(1), Transform: new RenderTransform(Matrix4x4.CreateTranslation(position)), PreviousTransform: default, MeshSet: default, Material: default, Residency: default, Bounds: default, Flags: RenderProjectionFlags.Draw, DegradeState: default, SortKey: new RenderSortKey(0), DirtyMask: default, Source: new RenderSourceMetadata( LocalEntityId: localEntityId, ServerGuid: serverGuid, SourceId: 0, ParentCellId: parentCellId, EffectCellId: 0, BuildingShellAnchorCellId: 0, TransformFingerprint: default, GeometryFingerprint: default, AppearanceFingerprint: default), EntityPayload: new RenderEntityPayload( MeshRefs: meshRefs, PaletteOverride: null, IsBuildingShell: isBuildingShell, CasterIdentity: casterIdentity)); private static ObjectRenderBatch MakeBatch( uint surfaceId, TranslucencyKind translucency, uint firstIndex, int baseVertex, int indexCount, uint textureSlotIndex, uint textureLayer = 0, CullMode cullMode = CullMode.CounterClockwise) => new() { Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false }, Translucency = translucency, FirstIndex = firstIndex, BaseVertex = (uint)baseVertex, IndexCount = indexCount, TextureSlot = new GpuTextureSlot(textureSlotIndex), TextureIndex = (int)textureLayer, }; private static ObjectRenderData MakeFlatMesh(params ObjectRenderBatch[] batches) => new() { Batches = new List(batches) }; private static ObjectRenderData MakeSortedMesh( Vector3 sortCenter, params ObjectRenderBatch[] batches) => new() { SortCenter = sortCenter, Batches = new List(batches) }; // ── Reflection seam: ObjectMeshManager owns no test-injection API, and // driving real GPU/GfxObj upload for a unit test is out of this stage's // scope — ObjectRenderData/ObjectRenderBatch are plain settable classes, // so this seeds the manager's private cache directly. ────────────────── private static void InjectRenderData(ObjectMeshManager manager, ulong id, ObjectRenderData data) { FieldInfo field = typeof(ObjectMeshManager).GetField( "_renderData", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new InvalidOperationException( "ObjectMeshManager._renderData field not found — test relies on this exact name."); var dict = (ConcurrentDictionary)field.GetValue(manager)!; dict[id] = data; } [Fact] public void PopulateCellObjects_UsesAuthoredSortCenterAndStableFarToNearOrder() { using var fx = new DispatcherFixture(); const ulong nearOrigin = 0x0100_0C01UL; const ulong farAuthoredCenter = 0x0100_0C02UL; InjectRenderData(fx.Manager, nearOrigin, MakeSortedMesh( Vector3.Zero, MakeBatch(101, TranslucencyKind.AlphaBlend, 101, 0, 3, 1))); InjectRenderData(fx.Manager, farAuthoredCenter, MakeSortedMesh( new Vector3(10, 0, 0), MakeBatch(202, TranslucencyKind.AlphaBlend, 202, 0, 3, 2))); RenderProjectionRecord[] records = [ MakeRecord(1, 0, new Vector3(5, 0, 0), [new MeshRef((uint)nearOrigin, Matrix4x4.Identity)]), MakeRecord(2, 0, Vector3.Zero, [new MeshRef((uint)farAuthoredCenter, Matrix4x4.Identity)]), ]; var alpha = new List(); var stream = new OrderedDrawStream(); var populator = new WalkStaticStreamPopulator(fx.Dispatcher); populator.PopulateCellObjects( stream, WalkDrawStage.CellStatic, 0x8C040112u, records, 0x8C04u, Vector3.Zero, Matrix4x4.Identity, views: null, viewRouteIndex: -1, alpha); Assert.Empty(stream.Keys); Assert.Equal([202u, 101u], alpha.Select(static batch => batch.Key.FirstIndex)); Assert.Equal([100f, 25f], alpha.Select(static batch => batch.SortDistanceSq)); } [Fact] public void PopulateCellObjects_EqualCyptRetainsEntityPartAndSubsetOrder() { using var fx = new DispatcherFixture(); const ulong setup = 0x1000_0C10UL; const ulong firstPart = 0x0100_0C11UL; const ulong secondPart = 0x0100_0C12UL; InjectRenderData(fx.Manager, firstPart, MakeSortedMesh( Vector3.Zero, MakeBatch(11, TranslucencyKind.AlphaBlend, 11, 0, 3, 1), MakeBatch(12, TranslucencyKind.AlphaBlend, 12, 0, 3, 2))); InjectRenderData(fx.Manager, secondPart, MakeSortedMesh( Vector3.Zero, MakeBatch(21, TranslucencyKind.AlphaBlend, 21, 0, 3, 3), MakeBatch(22, TranslucencyKind.AlphaBlend, 22, 0, 3, 4))); InjectRenderData(fx.Manager, setup, new ObjectRenderData { IsSetup = true, SetupParts = [ (firstPart, Matrix4x4.CreateTranslation(10, 0, 0)), (secondPart, Matrix4x4.CreateTranslation(0, 10, 0)), ], }); RenderProjectionRecord record = MakeRecord( 10, 0, Vector3.Zero, [new MeshRef((uint)setup, Matrix4x4.Identity)]); var alpha = new List(); new WalkStaticStreamPopulator(fx.Dispatcher).PopulateCellObjects( new OrderedDrawStream(), WalkDrawStage.CellStatic, 0x8C040112u, [record], 0x8C04u, Vector3.Zero, Matrix4x4.Identity, views: null, viewRouteIndex: -1, alpha); Assert.Equal([11u, 12u, 21u, 22u], alpha.Select(static batch => batch.Key.FirstIndex)); Assert.All(alpha, static batch => Assert.Equal(100f, batch.SortDistanceSq)); } [Fact] public void PopulateCellObjects_InterleavesStaticAndDynamicOpaquePartsByCypt() { using var fx = new DispatcherFixture(); const ulong nearStatic = 0x0100_0C21UL; const ulong farDynamic = 0x0100_0C22UL; const ulong middleStatic = 0x0100_0C23UL; InjectRenderData(fx.Manager, nearStatic, MakeFlatMesh( MakeBatch(1, TranslucencyKind.Opaque, 1, 0, 3, 1))); InjectRenderData(fx.Manager, farDynamic, MakeFlatMesh( MakeBatch(2, TranslucencyKind.Opaque, 2, 0, 3, 2))); InjectRenderData(fx.Manager, middleStatic, MakeFlatMesh( MakeBatch(3, TranslucencyKind.Opaque, 3, 0, 3, 3))); RenderProjectionRecord dynamic = MakeRecord( 2, 0, new Vector3(30, 0, 0), [new MeshRef((uint)farDynamic, Matrix4x4.Identity)]) with { ProjectionClass = RenderProjectionClass.LiveDynamicRoot }; RenderProjectionRecord[] records = [ MakeRecord(1, 0, new Vector3(10, 0, 0), [new MeshRef((uint)nearStatic, Matrix4x4.Identity)]), dynamic, MakeRecord(3, 0, new Vector3(20, 0, 0), [new MeshRef((uint)middleStatic, Matrix4x4.Identity)]), ]; var stream = new OrderedDrawStream(); new WalkStaticStreamPopulator(fx.Dispatcher).PopulateCellObjects( stream, WalkDrawStage.CellStatic, 0x8C040112u, records, 0x8C04u, Vector3.Zero, Matrix4x4.Identity, views: null, viewRouteIndex: -1, []); Assert.Equal([2u, 3u, 1u], stream.Keys.Select(static key => key.FirstIndex)); Assert.Equal( [WalkDrawStage.Dynamic, WalkDrawStage.CellStatic, WalkDrawStage.CellStatic], stream.Stages); } [Fact] public void WorldAlphaCyptContract_RetainsTheKeyWithoutDeadCameraSubmitThreading() { Type batchType = typeof(WbDrawDispatcher).GetNestedType( "WalkClassifiedBatch", BindingFlags.NonPublic) ?? throw new InvalidOperationException("WalkClassifiedBatch was not found."); PropertyInfo sortDistance = batchType.GetProperty("SortDistanceSq") ?? throw new InvalidOperationException("SortDistanceSq was not retained."); Assert.Equal(typeof(float), sortDistance.PropertyType); MethodInfo submit = typeof(WbDrawDispatcher).GetMethod( "SubmitWalkAlphaInstance", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new InvalidOperationException("SubmitWalkAlphaInstance was not found."); ParameterInfo[] parameters = submit.GetParameters(); Assert.Equal(2, parameters.Length); Assert.Equal("batch", parameters[0].Name); Assert.Equal(typeof(Matrix4x4), parameters[1].ParameterType); Assert.DoesNotContain(parameters, static parameter => parameter.ParameterType == typeof(Vector3)); string root = FindRepoRoot(); string populator = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "Walk", "WalkStaticStreamPopulator.cs")); Assert.Contains("batch.LocalSortCenter, batch.Transform", populator); Assert.Contains("SortDistanceSq = Vector3.DistanceSquared", populator); Assert.Contains("value.Batch.SortDistanceSq >", populator); Assert.Contains("AP-243", populator); Assert.Contains("greater-than-50 m", populator); Assert.Contains("0x005A06B7..0x005A0720", populator); } [Fact] public void WorldAlphaCyptDocumentationAndRegister_PinPerCellTruthAndThreeResiduals() { string root = FindRepoRoot(); string inventory = File.ReadAllText(Path.Combine( root, "docs", "architecture", "worldbuilder-inventory.md")); string architecture = File.ReadAllText(Path.Combine( root, "docs", "architecture", "acdream-architecture.md")); string registerPath = Path.Combine( root, "docs", "architecture", "retail-divergence-register.md"); string register = File.ReadAllText(registerPath); Assert.Contains("per-cell", inventory, StringComparison.OrdinalIgnoreCase); Assert.Contains("two FIFO", inventory, StringComparison.Ordinal); Assert.DoesNotContain( "distance-sorts one shared queue", inventory, StringComparison.OrdinalIgnoreCase); Assert.Contains("per-cell", architecture, StringComparison.OrdinalIgnoreCase); Assert.Contains("two FIFO", architecture, StringComparison.Ordinal); Assert.DoesNotContain( "one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase); Assert.Contains("162 active rows", register, StringComparison.Ordinal); int apSectionStart = register.IndexOf( "## 3. Documented approximation (AP)", StringComparison.Ordinal); int apSectionEnd = register.IndexOf( "## 4. Temporary stopgap (TS)", StringComparison.Ordinal); Assert.True(apSectionStart >= 0, "AP section heading must exist."); Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section."); foreach (string id in new[] { "AP-241", "AP-242", "AP-243" }) { int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal); Assert.True( rowIndex > apSectionStart && rowIndex < apSectionEnd, $"{id} row must be inside the AP table before the TS section; " + $"rowIndex={rowIndex}, AP=[{apSectionStart},{apSectionEnd})."); } Assert.Single(File.ReadLines(registerPath), static line => line.StartsWith("| AP-241 |", StringComparison.Ordinal)); Assert.Single(File.ReadLines(registerPath), static line => line.StartsWith("| AP-242 |", StringComparison.Ordinal)); Assert.Single(File.ReadLines(registerPath), static line => line.StartsWith("| AP-243 |", StringComparison.Ordinal)); Assert.Equal( 162, File.ReadLines(registerPath).Count(static line => line.StartsWith("| AP-", StringComparison.Ordinal))); Assert.Contains("greater than 50 m", register, StringComparison.Ordinal); Assert.Contains("0x005A06B7..0x005A0720", register, StringComparison.Ordinal); } private static string FindRepoRoot() { string? directory = AppContext.BaseDirectory; while (directory is not null) { if (File.Exists(Path.Combine(directory, "AcDream.slnx"))) return directory; directory = Directory.GetParent(directory)?.FullName; } throw new DirectoryNotFoundException("Could not locate AcDream.slnx."); } // ── Deliverable 1: ClassifyEntityForWalk data equivalence ───────────── [Fact] public void ClassifyEntityForWalk_OneOpaqueAndOneTranslucentPart_YieldsBatchesInRecordOrderWithCorrectIsOpaque() { using var fx = new DispatcherFixture(); const ulong opaqueGfxObj = 0x0100_0001UL; const ulong alphaGfxObj = 0x0100_0002UL; InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh( MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1))); InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh( MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4, indexCount: 6, textureSlotIndex: 2))); var meshRefs = new[] { new MeshRef((uint)opaqueGfxObj, Matrix4x4.CreateTranslation(1, 0, 0)), new MeshRef((uint)alphaGfxObj, Matrix4x4.CreateTranslation(0, 1, 0)), }; RenderProjectionRecord record = MakeRecord( localEntityId: 100, serverGuid: 0, position: new Vector3(5, 6, 7), meshRefs); var batches = new List(); var selectionParts = new List(); fx.Dispatcher.ClassifyEntityForWalk(in record, tupleLandblockId: 0x8C04u, batches, selectionParts); Assert.Equal(2, batches.Count); WbDrawDispatcher.WalkClassifiedBatch opaque = batches[0]; Assert.True(opaque.IsOpaque); Assert.Equal(TranslucencyKind.Opaque, opaque.Key.Translucency); Assert.Equal(0u, opaque.Key.FirstIndex); Assert.Equal(3, opaque.Key.IndexCount); Assert.Equal(1u, opaque.Key.TextureSlot.Index); Assert.Equal(1f, opaque.Alpha); Assert.Equal(meshRefs[0].PartTransform * record.Transform.LocalToWorld, opaque.Transform); WbDrawDispatcher.WalkClassifiedBatch translucent = batches[1]; Assert.False(translucent.IsOpaque); Assert.Equal(TranslucencyKind.AlphaBlend, translucent.Key.Translucency); Assert.Equal(3u, translucent.Key.FirstIndex); Assert.Equal(6, translucent.Key.IndexCount); Assert.Equal(2u, translucent.Key.TextureSlot.Index); Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform); Assert.Equal(2, selectionParts.Count); Assert.Equal(100u, selectionParts[0].LocalEntityId); Assert.Equal(0, selectionParts[0].PartIndex); Assert.Equal((uint)opaqueGfxObj, selectionParts[0].GfxObjId); Assert.Equal(opaque.Transform, selectionParts[0].LocalToWorld); Assert.Equal(1, selectionParts[1].PartIndex); Assert.Equal((uint)alphaGfxObj, selectionParts[1].GfxObjId); } [Fact] public void ClassifyEntityForWalk_NoDrawProjectileRecord_SubmitsNeitherMeshNorSelection() { using var fx = new DispatcherFixture(); const ulong projectileGfxObj = 0x0100_0003UL; InjectRenderData(fx.Manager, projectileGfxObj, MakeFlatMesh( MakeBatch( 0x08000003u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1))); RenderProjectionRecord projectile = MakeRecord( localEntityId: 101, serverGuid: 0x7000_0101u, position: Vector3.Zero, meshRefs: [new MeshRef((uint)projectileGfxObj, Matrix4x4.Identity)]) with { // ACE's projectile-impact SetState sets NoDraw immediately; // the later DeleteObject intentionally arrives five seconds // afterward. The journal projects that state as resident and // hidden, with Draw cleared. Flags = RenderProjectionFlags.SpatiallyResident | RenderProjectionFlags.Selectable | RenderProjectionFlags.Hidden, }; var batches = new List(); var selectionParts = new List(); fx.Dispatcher.ClassifyEntityForWalk( in projectile, tupleLandblockId: 0x8C04u, batches, selectionParts, liveDynamic: true); Assert.Empty(batches); Assert.Empty(selectionParts); } [Fact] public void ClassifyEntityForWalk_SetupComposite_EncodesPartAndSetupPartIndexLikePackedRoute() { using var fx = new DispatcherFixture(); const ulong setupGfxObj = 0x1000_0010UL; const ulong trunkGfxObj = 0x0100_0011UL; const ulong leavesGfxObj = 0x0100_0012UL; InjectRenderData(fx.Manager, trunkGfxObj, MakeFlatMesh( MakeBatch(0x08000011u, TranslucencyKind.Opaque, 0, 0, 3, 1))); InjectRenderData(fx.Manager, leavesGfxObj, MakeFlatMesh( MakeBatch(0x08000012u, TranslucencyKind.ClipMap, 3, 4, 6, 2))); InjectRenderData(fx.Manager, setupGfxObj, new ObjectRenderData { IsSetup = true, SetupParts = new List<(ulong GfxObjId, Matrix4x4 Transform)> { (trunkGfxObj, Matrix4x4.CreateTranslation(0, 0, 1)), (leavesGfxObj, Matrix4x4.CreateTranslation(0, 0, 2)), }, }); var meshRefs = new[] { new MeshRef((uint)setupGfxObj, Matrix4x4.Identity) }; RenderProjectionRecord record = MakeRecord(200, 0, Vector3.Zero, meshRefs); var batches = new List(); var selectionParts = new List(); fx.Dispatcher.ClassifyEntityForWalk(in record, 0x8C04u, batches, selectionParts); Assert.Equal(2, batches.Count); Assert.Equal(2, selectionParts.Count); // partIndex=0 (the entity's single top-level MeshRef) << 16 | setupPartIndex. Assert.Equal(0, selectionParts[0].PartIndex); Assert.Equal(1, selectionParts[1].PartIndex); Assert.Equal((uint)trunkGfxObj, selectionParts[0].GfxObjId); Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId); } [Fact] public void ClassifyEntityForWalk_FrameScopeStampsEachAdmittedSetupPartOncePerRetailPass() { using var fx = new DispatcherFixture(); const ulong setupGfxObj = 0x1000_0020UL; const ulong headGfxObj = 0x0100_0021UL; const ulong torsoGfxObj = 0x0100_0022UL; InjectRenderData(fx.Manager, headGfxObj, MakeFlatMesh( MakeBatch(0x08000021u, TranslucencyKind.Opaque, 0, 0, 3, 1))); InjectRenderData(fx.Manager, torsoGfxObj, MakeFlatMesh( MakeBatch(0x08000022u, TranslucencyKind.Opaque, 3, 4, 6, 2))); InjectRenderData(fx.Manager, setupGfxObj, new ObjectRenderData { IsSetup = true, SetupParts = new List<(ulong GfxObjId, Matrix4x4 Transform)> { (headGfxObj, Matrix4x4.CreateTranslation(0, 0, 2)), (torsoGfxObj, Matrix4x4.CreateTranslation(0, 0, 1)), }, }); RenderProjectionRecord record = MakeRecord( 220, 0, Vector3.Zero, [new MeshRef((uint)setupGfxObj, Matrix4x4.Identity)]); var batches = new List(); var selectionParts = new List(); fx.Dispatcher.BeginWalkPartFrame(); try { fx.Dispatcher.ClassifyEntityForWalk( in record, 0x8C04u, batches, selectionParts); Assert.Equal(2, batches.Count); Assert.Equal(2, selectionParts.Count); batches.Clear(); selectionParts.Clear(); fx.Dispatcher.ClassifyEntityForWalk( in record, 0x8C04u, batches, selectionParts); Assert.Empty(batches); Assert.Empty(selectionParts); // PView::DrawCells @0x005A4886 increments m_nFrameStamp after // the landscape pass and before its interior-cell repaint. The // same parts may therefore submit once again in the second pass. fx.Dispatcher.AdvanceWalkPartPassStamp(); fx.Dispatcher.ClassifyEntityForWalk( in record, 0x8C04u, batches, selectionParts); Assert.Equal(2, batches.Count); Assert.Equal(2, selectionParts.Count); } finally { fx.Dispatcher.EndWalkPartFrame(); } // Direct classifier calls outside a production walk frame remain // independent, as the conformance/referee tests require. batches.Clear(); selectionParts.Clear(); fx.Dispatcher.ClassifyEntityForWalk( in record, 0x8C04u, batches, selectionParts); Assert.Equal(2, batches.Count); Assert.Equal(2, selectionParts.Count); } [Fact] public void ClassifyEntityForWalk_FrameScopeDoesNotStampPortalRejectedPart() { using var fx = new DispatcherFixture(); const ulong gfxObj = 0x0100_0023UL; InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh( MakeBatch(0x08000023u, TranslucencyKind.Opaque, 0, 0, 3, 1))); RenderProjectionRecord record = MakeRecord( 221, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]); var batches = new List(); var selectionParts = new List(); fx.Dispatcher.BeginWalkPartFrame(); try { fx.Dispatcher.ClassifyEntityForWalk( in record, 0x8C04u, batches, selectionParts, liveDynamic: true, lookInViews: new FixedWalkViews(), lookInRouteIndex: 0); Assert.Empty(batches); Assert.Empty(selectionParts); fx.Dispatcher.ClassifyEntityForWalk( in record, 0x8C04u, batches, selectionParts, liveDynamic: true, lookInViews: new FixedWalkViews(7u), lookInRouteIndex: 1); Assert.Single(batches); Assert.Single(selectionParts); } finally { fx.Dispatcher.EndWalkPartFrame(); } } [Fact] public void ClassifyEntityForWalk_LocalPlayerBypassesDrawnPartStampLikeRetail() { using var fx = new DispatcherFixture(); const ulong gfxObj = 0x0100_0024UL; InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh( MakeBatch(0x08000024u, TranslucencyKind.Opaque, 0, 0, 3, 1))); RenderProjectionRecord player = MakeRecord( 222, 0x5000_0001u, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)], casterIdentity: RenderCasterIdentityKind.LocalPlayer); var batches = new List(); var selectionParts = new List(); fx.Dispatcher.BeginWalkPartFrame(); try { fx.Dispatcher.ClassifyEntityForWalk( in player, 0xF418u, batches, selectionParts, liveDynamic: true); Assert.Single(batches); Assert.Single(selectionParts); batches.Clear(); selectionParts.Clear(); fx.Dispatcher.ClassifyEntityForWalk( in player, 0xF418u, batches, selectionParts, liveDynamic: true); Assert.Single(batches); Assert.Single(selectionParts); } finally { fx.Dispatcher.EndWalkPartFrame(); } } [Fact] public void ClassifyEntityForWalk_PortalViewsAdmitOneCompleteUnclippedMesh() { using var fx = new DispatcherFixture(); const ulong gfxObj = 0x0100_0013UL; InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh( MakeBatch(0x08000013u, TranslucencyKind.Opaque, 0, 0, 3, 1))); RenderProjectionRecord record = MakeRecord( 201, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)], parentCellId: 0x8C040112u); var batches = new List(); var selectionParts = new List(); var views = new FixedWalkViews(7u, 9u); fx.Dispatcher.ClassifyEntityForWalk( in record, 0x8C04u, batches, selectionParts, liveDynamic: true, views, lookInRouteIndex: 0, lookInCellId: 0x8C040112u); WbDrawDispatcher.WalkClassifiedBatch batch = Assert.Single(batches); Assert.Equal(0u, batch.ClipSlot); Assert.Single(selectionParts); } // ── Deliverable 2: WalkStaticStreamPopulator routing ─────────────────── [Fact] public void PopulateCell_OpaqueBatchAppendsOrderedDrawCommandInRecordOrderWithStageAndCellProvenance() { using var fx = new DispatcherFixture(); const ulong gfxObj = 0x0100_0003UL; InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh( MakeBatch(0x08000003u, TranslucencyKind.Opaque, 10, 2, 12, 5))); var record = MakeRecord(300, 0, new Vector3(1, 2, 3), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) }); var populator = new WalkStaticStreamPopulator(fx.Dispatcher); var stream = new OrderedDrawStream(); populator.PopulateCell( stream, WalkDrawStage.CellStatic, cellId: 0x8C040100u, new[] { record }, tupleLandblockId: 0x8C04u, cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity); Assert.Equal(1, stream.Count); Assert.Equal(WalkDrawStage.CellStatic, stream.Stages[0]); Assert.Equal(0x8C040100u, stream.CellIds[0]); Assert.Equal(10u, stream.Keys[0].FirstIndex); Assert.Equal(12, stream.Keys[0].IndexCount); Assert.Equal(1f, stream.Alphas[0]); Assert.Equal(record.Transform.LocalToWorld, stream.Transforms[0]); } [Fact] public void PopulateOutdoorStatics_UsesTheOutdoorStaticStage() { using var fx = new DispatcherFixture(); const ulong gfxObj = 0x0100_0004UL; InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh( MakeBatch(0x08000004u, TranslucencyKind.Opaque, 0, 0, 3, 1))); var record = MakeRecord(400, 0, Vector3.Zero, new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) }); var populator = new WalkStaticStreamPopulator(fx.Dispatcher); var stream = new OrderedDrawStream(); populator.PopulateOutdoorStatics( stream, cellId: 0x8C040000u, new[] { record }, tupleLandblockId: 0x8C04u, cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity); Assert.Equal(1, stream.Count); Assert.Equal(WalkDrawStage.OutdoorStatic, stream.Stages[0]); } [Fact] public void PopulateCell_TranslucentBatchDoesNotAppendToTheStreamAndReachesTheAlphaQueue() { using var fx = new DispatcherFixture(withAlphaQueue: true); const ulong gfxObj = 0x0100_0005UL; InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh( MakeBatch(0x08000005u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1))); var record = MakeRecord(500, 0, new Vector3(0, 0, 10), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) }); var populator = new WalkStaticStreamPopulator(fx.Dispatcher); var stream = new OrderedDrawStream(); fx.AlphaQueue!.BeginFrame(); populator.PopulateCell( stream, WalkDrawStage.CellStatic, 0x8C040100u, new[] { record }, 0x8C04u, cameraWorldPosition: Vector3.Zero, viewProjection: Matrix4x4.Identity); Assert.Equal(0, stream.Count); Assert.Equal(1, fx.AlphaQueue.PendingCount); fx.AlphaQueue.AbortFrame(); } [Fact] public void PopulateCell_PublishesSelectionPartsForEveryClassifiedEntity() { var sink = new RecordingSelectionSink(); using var fx = new DispatcherFixture(selectionSink: sink); const ulong gfxObj = 0x0100_0006UL; InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh( MakeBatch(0x08000006u, TranslucencyKind.Opaque, 0, 0, 3, 1))); var record = MakeRecord(600, serverGuid: 0x8000_0060u, new Vector3(1, 1, 1), new[] { new MeshRef((uint)gfxObj, Matrix4x4.Identity) }); var populator = new WalkStaticStreamPopulator(fx.Dispatcher); var stream = new OrderedDrawStream(); populator.PopulateCell( stream, WalkDrawStage.CellStatic, 0x8C040100u, new[] { record }, 0x8C04u, Vector3.Zero, Matrix4x4.Identity); var call = Assert.Single(sink.Calls); Assert.Equal(0x8000_0060u, call.ServerGuid); Assert.Equal(600u, call.LocalEntityId); Assert.Equal(0, call.PartIndex); Assert.Equal((uint)gfxObj, call.GfxObjId); Assert.Equal(record.Transform.LocalToWorld, call.LocalToWorld); } // ── SubmitWalkAlphaInstance: same per-instance data and router decision // as DeferTransparentGroups, through the REAL RetailAlphaQueue — S4-c2 // fix round 1 (A5): this banner's stale "same viewer distance" text is // corrected; the FIFO cutover deleted viewer distance from the alpha // path entirely. ──────────────────────────────────────────────────── /// S4-c2: retail's queues are FIFO, not distance-sorted — this /// pins the routing decision instead (an AlphaBlend batch's constructed /// mask is 0x02, which has no ClipMap bit, so /// appends it to ALPHA, never /// CLIP), plus the exact token. Mutation check: routing AlphaBlend to /// CLIP instead makes ClipCount assert fail (1 instead of the /// expected 0) and AlphaCount fail (0 instead of 1). [Fact] public void SubmitWalkAlphaInstance_RoutesAlphaBlendBatchIntoTheAlphaList() { using var fx = new DispatcherFixture(withAlphaQueue: true); fx.AlphaQueue!.BeginFrame(); var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0); Vector3 localSortCenter = new(1, 2, 3); Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6); var batch = new WbDrawDispatcher.WalkClassifiedBatch( key, model, ClipSlot: 7, WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 1, Alpha: 0.5f, SelectionLighting: new Vector2(0.25f, 0.75f), DetailCategory: 1, IsOpaque: false, LocalSortCenter: localSortCenter); fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Matrix4x4.Identity); Assert.Equal(1, fx.AlphaQueue.PendingCount); Assert.Equal(1, fx.AlphaQueue.AlphaCount); Assert.Equal(0, fx.AlphaQueue.ClipCount); FieldInfo alphaListField = typeof(RetailAlphaQueue).GetField( "_alpha", BindingFlags.NonPublic | BindingFlags.Instance)!; var alphaList = (List)alphaListField.GetValue(fx.AlphaQueue)!; RetailAlphaEntry entry = Assert.Single(alphaList); Assert.Equal(0, entry.Token); fx.AlphaQueue.AbortFrame(); } /// /// S4-c2 final M3 production pin for SubmitWalkAlphaInstance: /// building shell + detail ON executes the immediate base+detail draws at /// the submit mark and queues nothing; detail OFF queues, then drains the /// base draw without a detail pass; an ordinary batch remains deferred /// even while the global detail option is on. Mutation check: hardcoding /// detailSurfaceActive=false at the production submit makes the /// first case's pending-count assertion fail with 1. [Fact] public void SubmitWalkAlphaInstance_DetailOnOffAndOrdinaryUseProductionOutcomes() { using (var on = new DispatcherFixture( withAlphaQueue: true, detailAvailable: true, detailEnabled: true)) using (DrawScope draw = on.BeginDraw(beginAlpha: true)) { WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u); on.Dispatcher.SubmitWalkAlphaInstance( in building, Matrix4x4.Identity); Assert.Equal(0, on.AlphaQueue!.PendingCount); Assert.Contains( on.Device.Calls.OfType(), call => call.PipelineName.Contains("alpha", StringComparison.Ordinal) && !call.PipelineName.Contains("detail", StringComparison.Ordinal)); Assert.Contains( on.Device.Calls.OfType(), call => call.PipelineName.Contains("retail-detail-alpha", StringComparison.Ordinal)); } using (var off = new DispatcherFixture( withAlphaQueue: true, detailAvailable: true, detailEnabled: false)) using (DrawScope draw = off.BeginDraw(beginAlpha: true)) { WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u); off.Dispatcher.SubmitWalkAlphaInstance( in building, Matrix4x4.Identity); Assert.Equal(1, off.AlphaQueue!.PendingCount); Assert.Empty(off.Device.Calls.OfType()); off.AlphaQueue.EndFrame(); Assert.DoesNotContain( off.Device.Calls.OfType(), call => call.PipelineName.Contains("retail-detail", StringComparison.Ordinal)); } using (var ordinary = new DispatcherFixture( withAlphaQueue: true, detailAvailable: true, detailEnabled: true)) using (DrawScope draw = ordinary.BeginDraw(beginAlpha: true)) { WbDrawDispatcher.WalkClassifiedBatch batch = AlphaWalkBatch(detailCategory: 0u); ordinary.Dispatcher.SubmitWalkAlphaInstance( in batch, Matrix4x4.Identity); Assert.Equal(1, ordinary.AlphaQueue!.PendingCount); Assert.Empty(ordinary.Device.Calls.OfType()); } } /// /// The sibling grouped-production site executes the same detail-on row 1, /// not merely the walk submit site. This invokes the real private /// DeferTransparentGroups after seeding its real retained group /// scratch. The same hardcoded-false mutation leaves one queued token and /// makes the pending-count assertion fail. [Fact] public void DeferTransparentGroups_BuildingDetailOnDrawsImmediateWithDetail() { using var fx = new DispatcherFixture( withAlphaQueue: true, detailAvailable: true, detailEnabled: true); using DrawScope draw = fx.BeginDraw(beginAlpha: true); WbDrawDispatcher.InstanceGroup group = AlphaInstanceGroup(detailCategory: 1u); FieldInfo field = typeof(WbDrawDispatcher).GetField( "_translucentDraws", BindingFlags.Instance | BindingFlags.NonPublic)!; var groups = (List)field.GetValue(fx.Dispatcher)!; groups.Add(group); MethodInfo defer = typeof(WbDrawDispatcher).GetMethod( "DeferTransparentGroups", BindingFlags.Instance | BindingFlags.NonPublic)!; defer.Invoke(fx.Dispatcher, [Vector3.Zero, Matrix4x4.Identity]); Assert.Equal(0, fx.AlphaQueue!.PendingCount); Assert.Contains( fx.Device.Calls.OfType(), call => call.PipelineName.Contains("retail-detail-alpha", StringComparison.Ordinal)); } private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(uint detailCategory) => new( new GroupKey( 0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0), Matrix4x4.Identity, ClipSlot: 0, WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 0, Alpha: 1f, SelectionLighting: Vector2.Zero, DetailCategory: detailCategory, IsOpaque: false, LocalSortCenter: Vector3.Zero); private static WbDrawDispatcher.InstanceGroup AlphaInstanceGroup(uint detailCategory) { var group = new WbDrawDispatcher.InstanceGroup { FirstIndex = 0, BaseVertex = 0, IndexCount = 3, TextureSlot = new GpuTextureSlot(1), TextureLayer = 0, Translucency = TranslucencyKind.AlphaBlend, }; group.Matrices.Add(Matrix4x4.Identity); group.LocalSortCenters.Add(Vector3.Zero); group.SubmissionOrders.Add(0); group.Slots.Add(0); group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled); group.IndoorFlags.Add(0); group.DetailCategories.Add(detailCategory); group.Opacities.Add(1f); group.SelectionLighting.Add(Vector2.Zero); return group; } [Fact] public void SubmitWalkAlphaInstance_RejectsAMismatchedViewProjectionInTheSameScope() { using var fx = new DispatcherFixture(withAlphaQueue: true); fx.AlphaQueue!.BeginFrame(); var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0); var batch = new WbDrawDispatcher.WalkClassifiedBatch( key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10)); fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Matrix4x4.Identity); Assert.Throws(() => fx.Dispatcher.SubmitWalkAlphaInstance( in batch, Matrix4x4.CreateTranslation(1, 0, 0))); fx.AlphaQueue.AbortFrame(); } // ── Deliverable 3: the ordered submitter's own cull scratch ──────────── [Fact] public void PrepareThenDrawOrderedStream_DoesNotReadOrCorruptTheSharedAlphaCullScratch() { using var fx = new DispatcherFixture(); using DrawScope draw = fx.BeginDraw(); // Poison the SHARED _drawCullModes scratch the alpha path owns — the // exact array the ordered submitter used to write into before FW3.2a. // Under the OLD shared-scratch behavior this test's second assertion // fails: the ordered path's own command overwrites index 0 with // its own cull mode (Clockwise), destroying the alpha path's poison. FieldInfo field = typeof(WbDrawDispatcher).GetField( "_drawCullModes", BindingFlags.NonPublic | BindingFlags.Instance)!; var poisonModes = (CullMode[])field.GetValue(fx.Dispatcher)!; poisonModes[0] = CullMode.None; var stream = new OrderedDrawStream(); stream.Append(new OrderedDrawCommand( new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise), Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0)); fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity); fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count); // (1) The ordered submission's OWN recorded cull call reflects the // STREAM's cull mode (Clockwise -> GpuCullMode.Front), not the // poisoned shared array's (None). List cullCalls = [.. fx.Device.Calls.OfType().Select(c => c.CullMode)]; Assert.Equal([GpuCullMode.Front], cullCalls); // (2) The shared _drawCullModes scratch is UNTOUCHED — the ordered // path never wrote through it. var afterModes = (CullMode[])field.GetValue(fx.Dispatcher)!; Assert.Equal(CullMode.None, afterModes[0]); } // ── Fixture ───────────────────────────────────────────────────────────── private readonly struct DrawScope : IDisposable { private readonly IDisposable _publication; private readonly IGpuPassEncoder _pass; public DrawScope(IGpuFrame frame, IGpuPassEncoder pass, IDisposable publication) { Frame = frame; _pass = pass; _publication = publication; } public IGpuFrame Frame { get; } public IGpuPassEncoder Pass => _pass; public void Dispose() { _publication.Dispose(); _pass.Dispose(); } } private sealed class DispatcherFixture : IDisposable { private readonly WbMeshAdapter _meshAdapter; private readonly TextureCache _textures; public DispatcherFixture( bool withAlphaQueue = false, IRetailSelectionRenderSink? selectionSink = null, bool detailAvailable = false, bool detailEnabled = false) { Device = new RecordingGpuDevice(); FrameLifetime = new GpuDeviceFrameLifetime(Device); Scope = new VulkanWorldPassScope(sampleCount: 1); _textures = new TextureCache(Device, new NoopDatReaderWriter()); _meshAdapter = new WbMeshAdapter( Device, new NoopDatReaderWriter(), new NullPreparedAssetSource(), NullLogger.Instance, Device.Retirement); var entitySpawnAdapter = new EntitySpawnAdapter( _textures, _ => throw new NotSupportedException("Not exercised by these tests.")); AlphaQueue = withAlphaQueue ? new RetailAlphaQueue() : null; Dispatcher = new WbDrawDispatcher( Device, FrameLifetime, Scope, _textures, _meshAdapter, entitySpawnAdapter, new EntityClassificationCache(), new AcDream.Core.Rendering.TranslucencyFadeManager(), selectionSink: selectionSink, alphaQueue: AlphaQueue, buildingDetail: detailAvailable ? new TerrainAtlas.RetailDetailTextureBinding( new GpuTextureSlot(99), Tiling: 2f, SurfaceTextureId: 1, RenderSurfaceId: 2, Width: 4, Height: 4) : default, buildingDetailEnabled: detailEnabled ? DetailOn : DetailOff); } public RecordingGpuDevice Device { get; } public GpuDeviceFrameLifetime FrameLifetime { get; } public VulkanWorldPassScope Scope { get; } public WbDrawDispatcher Dispatcher { get; } public RetailAlphaQueue? AlphaQueue { get; } public ObjectMeshManager Manager => _meshAdapter.MeshManager!; public DrawScope BeginDraw(bool beginAlpha = false) { if (beginAlpha) { Dispatcher.BeginFrame(frameSlot: 0); AlphaQueue!.BeginFrame(); } FrameLifetime.BeginFrame(); IGpuFrame frame = FrameLifetime.CurrentFrame!; IGpuPassEncoder pass = frame.BeginPass( GpuPassDescription.BackbufferClear( "fw3-2a-walk-populator-test", Vector4.Zero, sampleCount: 1)); IDisposable publication = Scope.Publish(pass); Device.Clear(); return new DrawScope(frame, pass, publication); } private static bool DetailOn() => true; private static bool DetailOff() => false; public void Dispose() { Dispatcher.Dispose(); _meshAdapter.Dispose(); _textures.Dispose(); Device.Dispose(); } } private sealed class NullPreparedAssetSource : IPreparedAssetSource { public PreparedAssetSourceStats Stats => default; public CacheStats DecodedTextureCacheStats => default; public PreparedAssetPresence Probe( AcDream.Content.Pak.PakAssetType type, uint sourceFileId) => PreparedAssetPresence.Missing; public PreparedAssetReadResult Read( in PreparedAssetRequest request, CancellationToken cancellationToken = default) => PreparedAssetReadResult.Missing; public void Dispose() { } } private sealed class NoopDatReaderWriter : IDatReaderWriter { private readonly StubDatabase _portal = new(); private readonly StubDatabase _highRes = new(); private readonly StubDatabase _language = new(); private readonly StubDatabase _cell = new(); public string SourceDirectory => string.Empty; public IDatDatabase Portal => _portal; public IDatDatabase Cell => _cell; public ReadOnlyDictionary CellRegions { get; } = new(new Dictionary()); public IDatDatabase HighRes => _highRes; public IDatDatabase Language => _language; public IDatDatabase Local => _language; public ReadOnlyDictionary RegionFileMap { get; } = new(new Dictionary()); public int PortalIteration => 0; public int CellIteration => 0; public int HighResIteration => 0; public int LanguageIteration => 0; public bool TryGetFileBytes( uint regionId, uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public IEnumerable ResolveId(uint id) => Array.Empty(); public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public bool TrySave( uint regionId, T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); [return: MaybeNull] public T Get(uint fileId) where T : IDBObj => default; public bool TryGet( uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = default; return false; } public void Dispose() { } private sealed class StubDatabase : IDatDatabase { public DatDatabase Db => throw new NotSupportedException(); public int Iteration => 0; public IEnumerable GetAllIdsOfType() where T : IDBObj => Array.Empty(); public bool TryGet( uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj { value = default; return false; } public bool TryGetFileBytes( uint fileId, [MaybeNullWhen(false)] out byte[] value) { value = null; return false; } public bool TryGetFileBytes( uint fileId, ref byte[] bytes, out int bytesRead) { bytesRead = 0; return false; } public bool TrySave(T obj, int iteration = 0) where T : IDBObj => throw new NotSupportedException(); public void Dispose() { } } } }