using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Scene.Arch; using AcDream.App.Rendering.Wb; using AcDream.Core.World; namespace AcDream.App.Tests.Rendering; public sealed class RenderScenePViewFrameProductTests { private const uint Landblock = 0xA9B4FFFF; private const uint Cell = 0xA9B40170; private static readonly RenderSceneGeneration Generation = RenderSceneGeneration.FromRaw(1); // Campaign FW3.2b-2 (adjudicated 2026-08-30): Builder_PreservesPViewRoutesAndExcludesShellsAndWithdrawnOwners, // Builder_OrdersMultiSliceBuildingShellsAfterLookIns, and // Builder_InterleavesEachLookInWithItsOwnPackedBuildingShellRoutes are // DELETED — all three asserted route-range interleaving for // LandscapeOutdoorStatic/LandscapeBuildingShell/CellStatic, which // RenderScenePViewFrameBuilder no longer emits at all (WalkFrameDriver // draws every outdoor static, building shell, and cell static — look-in // included — directly through OrderedDrawStream; plan §FW3 // "FW3.2b-2 — the production rooting", item 3). There is no successor // assertion to write: the routes themselves are retired, not relocated. [Fact] public void Builder_ReusesOrderedIndicesWhileRefreshingDirtyRecords() { // Campaign FW3.2b-2 (adjudicated 2026-08-30): re-expressed against // the DYNAMIC index (LiveDynamicRoot / DynamicCandidates) instead of // the now-dead OutdoorStatic index — LoadSceneIndices' dirty-record // refresh + sort-key reorder mechanism this test proves is SHARED // between the two indices; the OutdoorStatic side was deleted along // with BuildOutdoorRoutes (its only reader). using var scene = new ArchRenderScene(Generation); RenderProjectionRecord later = Record( 0x0100_0000_0000_0012, RenderProjectionClass.LiveDynamicRoot); RenderProjectionRecord earlier = Record( 0x0100_0000_0000_0011, RenderProjectionClass.LiveDynamicRoot); scene.Apply( [ RenderProjectionDelta.Register(Generation, 1, later), RenderProjectionDelta.Register(Generation, 2, earlier), ]); PortalVisibilityFrame portal = Portal(Cell); ClipFrameAssembly clip = FullScreenClip(Cell); ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity); var exchange = new RenderFrameExchange(); var builder = new RenderScenePViewFrameBuilder(); Build(frameSequence: 1); RenderFrameView first = exchange.BorrowLatest(Generation, 1); try { Assert.Equal( [earlier.Id, later.Id], first.DynamicCandidates.ToArray() .Select(static record => record.Id)); } finally { exchange.Release(in first); } scene.ClearDirty(); ulong retainedRevision = scene.OpenQuery().IndexRevision; Matrix4x4 movedTransform = Matrix4x4.CreateTranslation(0.25f, 0, 0); RenderProjectionRecord moved = later with { Transform = new RenderTransform(movedTransform), PreviousTransform = new PreviousRenderTransform( later.Transform.LocalToWorld), Bounds = new RenderWorldBounds( new Vector3(-0.75f, -1, -1), new Vector3(1.25f, 1, 1)), }; scene.Apply( [ RenderProjectionDelta.Update( RenderProjectionDeltaKind.UpdateTransform, Generation, 3, moved), ]); Assert.Equal( retainedRevision, scene.OpenQuery().IndexRevision); Build(frameSequence: 2); RenderFrameView second = exchange.BorrowLatest(Generation, 2); try { RenderProjectionRecord projected = second.EntityCandidates.ToArray() .Single(candidate => candidate.Projection.Id == later.Id) .Projection; Assert.Equal( movedTransform, projected.Transform.LocalToWorld); Assert.Equal( [earlier.Id, later.Id], second.DynamicCandidates.ToArray() .Select(static record => record.Id)); } finally { exchange.Release(in second); } scene.ClearDirty(); RenderProjectionRecord reordered = moved with { SortKey = new RenderSortKey(0), }; scene.Apply( [ RenderProjectionDelta.Update( RenderProjectionDeltaKind.UpdateTransform, Generation, 4, reordered), ]); Assert.True( scene.OpenQuery().IndexRevision > retainedRevision); Build(frameSequence: 3); RenderFrameView third = exchange.BorrowLatest(Generation, 3); try { Assert.Equal( [later.Id, earlier.Id], third.DynamicCandidates.ToArray() .Select(static record => record.Id)); } finally { exchange.Release(in third); } void Build(ulong frameSequence) { RenderSceneQuery query = scene.OpenQuery(); var input = new RenderScenePViewBuildInput( query, new RenderSceneDigest( query.Generation, query.Counts, default), portal, clip, viewcone, [], [Cell], EmptyCellSource.Instance, [], RootIsOutdoor: true); builder.Build(exchange, frameSequence, in input); } } [Fact] public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate() { // Campaign FW3.2b-2 (adjudicated 2026-08-30): no static entities here // — LandscapeOutdoorStatic/CellStatic no longer flow through the // packed product at all (WalkFrameDriver owns every static now), so // a static observed on the LEGACY oracle side with nothing on the // packed side would report a permanent mismatch unrelated to what // this test is proving (withdrawal detection on the SURVIVING // DynamicLast route). WorldEntity outdoorDynamic = Entity(1_000_012, 0x8000_0012, parentCell: null); WorldEntity cellDynamic = Entity(1_000_013, 0x8000_0013, parentCell: Cell); WorldEntity[] entities = [outdoorDynamic, cellDynamic]; var oracle = new CurrentRenderSceneOracle(); var partition = new InteriorEntityPartition.Result(); InteriorEntityPartition.Partition( partition, [Cell], [Entry(entities)], oracle); oracle.BeginPViewFrame(); oracle.ObservePViewBucket( CurrentRenderPViewRoute.DynamicLast, 0, 0, [outdoorDynamic, cellDynamic]); oracle.CompletePViewFrame(); oracle.BeginDispatcherFrame(); oracle.ObserveDispatcherDraw( WbDrawDispatcher.EntitySet.All, entitiesWalked: 2, [ (outdoorDynamic, 0, Landblock), (cellDynamic, 0, Landblock), ]); using var shadow = new RenderSceneShadowRuntime(Generation); foreach (WorldEntity entity in entities) { RenderProjectionRecord record = Project(entity); shadow.Journal.Register(in record); } shadow.DrainUpdateBoundary(); var logs = new List(); var controller = new RenderScenePViewFrameProductController( shadow, oracle, logs.Add); PortalVisibilityFrame portal = Portal(Cell); ClipFrameAssembly clip = FullScreenClip(Cell); ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity); controller.BuildAndCompare( portal, clip, viewcone, [], [Cell], EmptyCellSource.Instance, [], Landblock, rootIsOutdoor: true); RenderFrameProductComparisonSnapshot matched = controller.Snapshot; Assert.True(matched.Enabled); Assert.Equal(1uL, matched.ComparisonCount); Assert.Equal(1uL, matched.SuccessfulComparisonCount); Assert.Equal(2, matched.ExpectedCandidateCount); Assert.Equal(2, matched.ActualCandidateCount); Assert.Equal(0, matched.MismatchCount); Assert.Null(matched.FirstMismatch); Assert.Equal(matched.ExpectedDigest, matched.ActualDigest); Assert.Equal(1uL, matched.PackedInputComparisonCount); Assert.Equal(1uL, matched.PackedInputSuccessfulComparisonCount); Assert.Equal(2, matched.PackedInputExpectedCount); Assert.Equal(2, matched.PackedInputActualCount); Assert.Equal(0, matched.PackedInputMismatchCount); Assert.Null(matched.PackedInputFirstMismatch); Assert.Equal( matched.PackedInputExpectedDigest, matched.PackedInputActualDigest); Assert.Empty(logs); RenderProjectionId withdrawnId = LiveRenderProjectionJournal.ProjectionId(cellDynamic.Id); shadow.Journal.Unregister( withdrawnId, RenderOwnerIncarnation.FromRaw(cellDynamic.Id)); shadow.DrainUpdateBoundary(); controller.BuildAndCompare( portal, clip, viewcone, [], [Cell], EmptyCellSource.Instance, [], Landblock, rootIsOutdoor: true); RenderFrameProductComparisonSnapshot mismatch = controller.Snapshot; Assert.Equal(2uL, mismatch.ComparisonCount); Assert.Equal(1uL, mismatch.SuccessfulComparisonCount); Assert.Equal(1, mismatch.MismatchCount); Assert.Contains("field=count expected=2 actual=1", mismatch.FirstMismatch); Assert.Equal(1, mismatch.PackedInputMismatchCount); Assert.Contains( "field=count expected=2 actual=1", mismatch.PackedInputFirstMismatch); Assert.Equal(2, logs.Count); } [Fact] public void Controller_RejectsEqualMembershipInDifferentTraversalOrder() { WorldEntity first = Entity(1_000_001, 0x8000_0001, parentCell: null); WorldEntity second = Entity(1_000_002, 0x8000_0002, parentCell: null); var oracle = new CurrentRenderSceneOracle(); var partition = new InteriorEntityPartition.Result(); InteriorEntityPartition.Partition( partition, [], [Entry([first, second])], oracle); oracle.BeginPViewFrame(); oracle.ObservePViewBucket( CurrentRenderPViewRoute.DynamicLast, 0, 0, [first, second]); oracle.CompletePViewFrame(); oracle.BeginDispatcherFrame(); oracle.ObserveDispatcherDraw( WbDrawDispatcher.EntitySet.All, entitiesWalked: 2, [ (first, 0, Landblock), (second, 0, Landblock), ]); using var shadow = new RenderSceneShadowRuntime(Generation); RenderProjectionRecord firstRecord = Project(first) with { SortKey = new RenderSortKey(2), }; RenderProjectionRecord secondRecord = Project(second) with { SortKey = new RenderSortKey(1), }; shadow.Journal.Register(in firstRecord); shadow.Journal.Register(in secondRecord); shadow.DrainUpdateBoundary(); var controller = new RenderScenePViewFrameProductController( shadow, oracle); PortalVisibilityFrame portal = Portal(Cell); ClipFrameAssembly clip = FullScreenClip(Cell); ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity); controller.BuildAndCompare( portal, clip, viewcone, [], [], EmptyCellSource.Instance, [], Landblock, rootIsOutdoor: true); RenderFrameProductComparisonSnapshot snapshot = controller.Snapshot; Assert.Equal(1, snapshot.MismatchCount); Assert.Contains( $"ProjectionId = {LiveRenderProjectionJournal.ProjectionId(first.Id)}", snapshot.FirstMismatch); Assert.Contains( $"ProjectionId = {LiveRenderProjectionJournal.ProjectionId(second.Id)}", snapshot.FirstMismatch); Assert.Equal(1, snapshot.PackedInputMismatchCount); } private static RenderProjectionRecord Project(WorldEntity entity) { bool dynamic = entity.ServerGuid != 0; RenderProjectionId id = dynamic ? LiveRenderProjectionJournal.ProjectionId(entity.Id) : StaticRenderProjectionJournal.StaticEntityId( Landblock, entity.Id); RenderProjectionClass projectionClass = dynamic ? RenderProjectionClass.LiveDynamicRoot : entity.ParentCellId is uint parentCell && InteriorEntityPartition.IsIndoorCellId(parentCell) ? RenderProjectionClass.IndoorCellStatic : RenderProjectionClass.OutdoorStatic; return RenderProjectionRecordFactory.ProjectEntity( id, projectionClass, RenderOwnerIncarnation.FromRaw(entity.Id), Landblock, entity.ParentCellId ?? Landblock, entity, spatiallyVisible: true); } private static RenderProjectionRecord Record( ulong id, RenderProjectionClass projectionClass, uint? parentCell = null, RenderProjectionFlags flags = RenderProjectionFlags.Draw | RenderProjectionFlags.SpatiallyResident, bool isBuildingShell = false, uint buildingShellAnchorCellId = 0) { uint fullCellId = parentCell ?? Landblock; Matrix4x4 transform = Matrix4x4.Identity; return new RenderProjectionRecord( RenderProjectionId.FromRaw(id), projectionClass, RenderOwnerIncarnation.FromRaw(id), new RenderTransform(transform), new PreviousRenderTransform(transform), new RenderMeshSet(RenderAssetHandle.FromRaw(id), 1, 1), new RenderMaterialVariant(0, 0, 1), new RenderSpatialResidency( RenderSpatialBucket.FromRaw(fullCellId), Landblock, fullCellId), new RenderWorldBounds( new Vector3(-1), new Vector3(1)), flags, default, new RenderSortKey(id), RenderDirtyMask.All, new RenderSourceMetadata( LocalEntityId: (uint)id, ServerGuid: projectionClass is RenderProjectionClass.LiveDynamicRoot ? (uint)id : 0, SourceId: 1, ParentCellId: parentCell ?? 0, EffectCellId: 0, BuildingShellAnchorCellId: buildingShellAnchorCellId, TransformFingerprint: default, GeometryFingerprint: default, AppearanceFingerprint: default), new RenderEntityPayload( [new MeshRef((uint)id, Matrix4x4.Identity)], PaletteOverride: null, IsBuildingShell: isBuildingShell)); } private static WorldEntity Entity( uint id, uint serverGuid, uint? parentCell) => new() { Id = id, ServerGuid = serverGuid, SourceGfxObjOrSetupId = 0x0200_0001, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = [ new MeshRef(0x0100_0001, Matrix4x4.Identity), ], ParentCellId = parentCell, }; private static PortalVisibilityFrame Portal(uint cellId) { var frame = new PortalVisibilityFrame(); frame.OrderedVisibleCells.Add(cellId); return frame; } private static ClipFrameAssembly FullScreenClip(uint cellId) { var slice = new ClipViewSlice( Slot: 0, NdcAabb: new Vector4(-1, -1, 1, 1), Planes: []); var assembly = new ClipFrameAssembly(); assembly.SetOutsideViewSlices([slice]); assembly.CellIdToViewSlices[cellId] = [slice]; return assembly; } private static ClipFrameAssembly TwoSliceClip(uint cellId) { var first = new ClipViewSlice( Slot: 0, NdcAabb: new Vector4(-1, -1, 0, 1), Planes: []); var second = new ClipViewSlice( Slot: 1, NdcAabb: new Vector4(0, -1, 1, 1), Planes: []); var assembly = new ClipFrameAssembly(); assembly.SetOutsideViewSlices([first, second]); assembly.CellIdToViewSlices[cellId] = [first]; return assembly; } private static ( uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities, IReadOnlyDictionary? AnimatedById) Entry(IReadOnlyList entities) => ( Landblock, Vector3.Zero, Vector3.One, entities, null); private sealed class EmptyCellSource : IRetailPViewCellSource { public static EmptyCellSource Instance { get; } = new(); private EmptyCellSource() { } public LoadedCell? Find(uint cellId) => null; } private sealed class DictionaryCellSource : IRetailPViewCellSource { private readonly Dictionary _cells; public DictionaryCellSource(params LoadedCell[] cells) => _cells = cells.ToDictionary(static cell => cell.CellId); public LoadedCell? Find(uint cellId) => _cells.TryGetValue(cellId, out LoadedCell? cell) ? cell : null; } }