diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 9e25e433..43443797 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -117,7 +117,6 @@ internal interface IRenderFrameEntityPassExecutor /// internal sealed partial class RetailPViewPassExecutor : IRetailPViewPassExecutor, - IRenderFrameEntityPassExecutor, IOutdoorSceneParticleOwnerSource { private readonly IWorldPassSurface _surface; @@ -214,30 +213,6 @@ internal sealed partial class RetailPViewPassExecutor : internal (int Width, int Height)? WalkAttachmentExtent => _entities.WalkAttachmentExtent; - public void BeginEntityFrame(in RenderFrameView view) => - _entities.BeginPackedProductionFrame(in view); - - public bool DrawEntityRoute( - ICamera camera, - in RenderFrameView view, - RenderFrameCandidateRoute route, - int routeIndex, - uint cellId, - uint tupleLandblockId) => - _entities.DrawPackedProductionRoute( - camera, - in view, - route, - routeIndex, - cellId, - tupleLandblockId); - - public void CompleteEntityFrame(in RenderFrameView view) => - _entities.CompletePackedProductionFrame(in view); - - public void AbortEntityFrame() => - _entities.AbortPackedProductionFrame(); - public void AbortFrame() { List? failures = null; @@ -406,18 +381,7 @@ internal sealed partial class RetailPViewPassExecutor : _terrainDiagnostics.Complete(); DisableClipDistances(); - if (context.EntityDraw is RenderFrameEntityDrawRequest request) - { - RenderFrameView drawView = request.View; - _entities.DrawPackedProductionRoute( - frame.Camera, - in drawView, - request.Route, - request.RouteIndex, - request.CellId, - request.TupleLandblockId); - } - else if (context.OutdoorEntities.Count > 0) + if (context.OutdoorEntities.Count > 0) { var sceneryEntry = ( frame.PlayerLandblockId ?? 0u, @@ -449,18 +413,7 @@ internal sealed partial class RetailPViewPassExecutor : _surface.BindTerrainClip(); DisableClipDistances(); - if (context.EntityDraw is RenderFrameEntityDrawRequest request) - { - RenderFrameView drawView = request.View; - _entities.DrawPackedProductionRoute( - frame.Camera, - in drawView, - request.Route, - request.RouteIndex, - request.CellId, - request.TupleLandblockId); - } - else if (context.Dynamics.Count > 0) + if (context.Dynamics.Count > 0) { var dynamicsEntry = ( frame.PlayerLandblockId ?? 0u, @@ -558,18 +511,7 @@ internal sealed partial class RetailPViewPassExecutor : _surface.BindTerrainClip(); DisableClipDistances(); - if (context.EntityDraw is RenderFrameEntityDrawRequest request) - { - RenderFrameView drawView = request.View; - _entities.DrawPackedProductionRoute( - frame.Camera, - in drawView, - request.Route, - request.RouteIndex, - request.CellId, - request.TupleLandblockId); - } - else if (context.BuildingShells.Count > 0) + if (context.BuildingShells.Count > 0) { var buildingEntry = ( frame.PlayerLandblockId ?? 0u, diff --git a/src/AcDream.App/Rendering/Wb/PackedProjectionClassificationCache.cs b/src/AcDream.App/Rendering/Wb/PackedProjectionClassificationCache.cs deleted file mode 100644 index 84142e63..00000000 --- a/src/AcDream.App/Rendering/Wb/PackedProjectionClassificationCache.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System.Numerics; -using System.Runtime.CompilerServices; -using AcDream.App.Rendering.Scene; - -namespace AcDream.App.Rendering.Wb; - -/// -/// The appearance-only identity of one retained scene projection. Root -/// transform, clip slot, light selection, and selection lighting deliberately -/// stay outside this key: G3 updates those instance fields without rebuilding -/// mesh/material classification. -/// -internal readonly record struct PackedClassificationIdentity( - RenderOwnerIncarnation Incarnation, - RenderMeshSet MeshSet, - RenderMaterialVariant Material, - RenderDegradeState DegradeState, - RenderSceneHash128 Geometry, - RenderSceneHash128 Appearance) -{ - public static PackedClassificationIdentity From( - in RenderProjectionRecord projection) => - new( - projection.OwnerIncarnation, - projection.MeshSet, - projection.Material, - projection.DegradeState, - projection.Source.GeometryFingerprint, - projection.Source.AppearanceFingerprint); -} - -internal readonly record struct PackedClassifiedBatch( - GroupKey Key, - Matrix4x4 RestPose, - Vector3 LocalSortCenter); - -internal readonly record struct PackedClassifiedSelectionPart( - int PartIndex, - uint GfxObjId, - Matrix4x4 RestPose); - -/// -/// Reusable variable-size payload for one projection. Lists grow only when an -/// appearance first exposes a higher batch/part count and are cleared in -/// place on the next appearance rebuild. -/// -internal sealed class PackedProjectionClassificationEntry -{ - public PackedClassificationIdentity Identity; - public readonly List Batches = []; - public readonly List SelectionParts = []; - public long LastBuiltFrame; - public long LastSeenFrame; - public bool ReusableAcrossFrames; - public long AccountedPayloadBytes; - - public void BeginRebuild( - in PackedClassificationIdentity identity, - long frame) - { - Identity = identity; - Batches.Clear(); - SelectionParts.Clear(); - LastBuiltFrame = frame; - LastSeenFrame = frame; - ReusableAcrossFrames = false; - } - - public void ReleaseStorage() - { - Batches.Clear(); - SelectionParts.Clear(); - Batches.TrimExcess(); - SelectionParts.TrimExcess(); - } -} - -internal readonly record struct PackedClassificationCacheSnapshot( - int ProjectionCount, - int StaticRebuildCount, - int SameFrameReuseCount, - int CrossFrameReuseCount, - int AnimatedClassificationCount, - int RetiredProjectionCount, - long RetainedPayloadBytes); - -/// -/// G3 retained classification owner. Structural classification is rebuilt -/// only for first sight, incarnation/appearance changes, incomplete resource -/// readiness, or active animation. Warm unchanged projections replay their -/// retained batch templates while current transforms, lights, clip slots, and -/// selection lighting are refreshed. -/// -internal sealed class PackedProjectionClassificationCache -{ - private const RenderDirtyMask ClassificationDirtyMask = - RenderDirtyMask.Appearance; - - private readonly Dictionary< - RenderProjectionId, - PackedProjectionClassificationEntry> _entries = []; - private readonly List _retired = []; - private RenderSceneGeneration _generation; - private bool _generationSet; - private long _frame; - private int _staticRebuildCount; - private int _sameFrameReuseCount; - private int _crossFrameReuseCount; - private int _animatedClassificationCount; - private int _retiredProjectionCount; - private long _retainedPayloadBytes; - - public long Frame => _frame; - - public PackedClassificationCacheSnapshot Snapshot => - new( - _entries.Count, - _staticRebuildCount, - _sameFrameReuseCount, - _crossFrameReuseCount, - _animatedClassificationCount, - _retiredProjectionCount, - _retainedPayloadBytes); - - public void BeginFrame(RenderSceneGeneration generation) - { - if (_frame == long.MaxValue) - { - throw new InvalidOperationException( - "Packed classification frame identity was exhausted."); - } - - if (!_generationSet || generation != _generation) - { - Clear(); - _generation = generation; - _generationSet = true; - } - - _frame++; - _staticRebuildCount = 0; - _sameFrameReuseCount = 0; - _crossFrameReuseCount = 0; - _animatedClassificationCount = 0; - _retiredProjectionCount = 0; - } - - public bool TryGetReusable( - RenderProjectionId id, - in PackedClassificationIdentity identity, - RenderDirtyMask dirtyMask, - out PackedProjectionClassificationEntry? entry) - { - if (!_entries.TryGetValue(id, out entry) - || entry.Identity != identity) - { - return false; - } - - entry.LastSeenFrame = _frame; - if (entry.LastBuiltFrame == _frame) - { - _sameFrameReuseCount++; - return true; - } - - if (!entry.ReusableAcrossFrames - || (dirtyMask & ClassificationDirtyMask) != 0) - { - return false; - } - - _crossFrameReuseCount++; - return true; - } - - public PackedProjectionClassificationEntry BeginRebuild( - RenderProjectionId id, - in PackedClassificationIdentity identity) - { - if (!_entries.TryGetValue(id, out var entry)) - { - entry = new PackedProjectionClassificationEntry(); - _entries.Add(id, entry); - } - - entry.BeginRebuild(in identity, _frame); - _staticRebuildCount++; - return entry; - } - - public void CompleteRebuild( - PackedProjectionClassificationEntry entry, - bool reusableAcrossFrames) - { - ArgumentNullException.ThrowIfNull(entry); - if (entry.LastBuiltFrame != _frame) - { - throw new InvalidOperationException( - "Only an entry rebuilt in the active frame can be completed."); - } - - entry.ReusableAcrossFrames = reusableAcrossFrames; - long payloadBytes = checked( - (long)entry.Batches.Capacity - * Unsafe.SizeOf() - + (long)entry.SelectionParts.Capacity - * Unsafe.SizeOf()); - _retainedPayloadBytes = checked( - _retainedPayloadBytes - - entry.AccountedPayloadBytes - + payloadBytes); - entry.AccountedPayloadBytes = payloadBytes; - } - - public void RecordAnimatedClassification() => - _animatedClassificationCount++; - - public void EndFrame() - { - _retired.Clear(); - foreach ((RenderProjectionId id, var entry) in _entries) - { - if (entry.LastSeenFrame != _frame) - { - _retainedPayloadBytes -= entry.AccountedPayloadBytes; - entry.AccountedPayloadBytes = 0; - entry.ReleaseStorage(); - _retired.Add(id); - } - } - - for (int i = 0; i < _retired.Count; i++) - _entries.Remove(_retired[i]); - _retiredProjectionCount = _retired.Count; - _retired.Clear(); - } - - public void Clear() - { - foreach (PackedProjectionClassificationEntry entry - in _entries.Values) - { - entry.ReleaseStorage(); - } - _entries.Clear(); - _retired.Clear(); - _frame = 0; - _retainedPayloadBytes = 0; - } -} diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs deleted file mode 100644 index 61611676..00000000 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs +++ /dev/null @@ -1,1001 +0,0 @@ -using System.Numerics; -using AcDream.Core.Rendering; -using AcDream.App.Rendering.Scene; -using AcDream.App.Rendering.Selection; -using AcDream.Core.Lighting; -using AcDream.Core.Meshing; -using AcDream.Core.Selection; -using AcDream.Core.World; -using AcDream.App.Rendering.Walk; -using DatReaderWriter.Enums; - -namespace AcDream.App.Rendering.Wb; - -/// -/// Scene-frame classification and production submission. The retained group -/// storage is also consumed by the G2/G3 compare oracle, while production -/// routes reuse the dispatcher's exact mesh, texture, light, translucency, -/// selection, upload, and draw owners. -/// -public sealed unsafe partial class WbDrawDispatcher -{ - private readonly Dictionary - _packedEntityById = []; - private readonly Dictionary _packedGroups = []; - private readonly List _packedRetiredGroupKeys = []; - private readonly List _packedOpaque = []; - private readonly List _packedTransparent = []; - private readonly List - _packedAlphaFingerprintScratch = []; - private readonly List - _packedSubmissions = []; - private readonly List - _packedSelectionParts = []; - private readonly HashSet _packedSelectionKeys = []; - private readonly PackedProjectionClassificationCache - _packedClassificationCache = new(); - private long _packedGroupFrame; - private long _nextPackedGroupRegistration = 1; - private int _nextPackedInstanceSubmissionOrder; - private bool _packedProductionFrameOpen; - private RenderSceneGeneration _packedProductionGeneration; - private ulong _packedProductionFrameSequence; - private int _packedProductionNextRange; - private readonly Dictionary<(int RouteIndex, uint LocalEntityId, int PartIndex, uint GfxObjId), string> - _probeLookInPartStates = []; - - internal IReadOnlyList - PackedDispatcherSubmissions => _packedSubmissions; - - internal IReadOnlyList - PackedSelectionParts => _packedSelectionParts; - - internal PackedClassificationCacheSnapshot - PackedClassificationSnapshot => - _packedClassificationCache.Snapshot; - - internal void BuildPackedDispatcherOracle( - in RenderFrameView view, - uint tupleLandblockId, - Vector3 cameraWorldPosition) - { - if (_packedProductionFrameOpen) - { - throw new InvalidOperationException( - "The packed dispatcher oracle cannot replace an active production frame."); - } - - BeginPackedFrameStorage(in view); - ReadOnlySpan ranges = - view.RouteRanges; - for (int rangeIndex = 0; - rangeIndex < ranges.Length; - rangeIndex++) - { - PackedRangeClassification classified = - ClassifyPackedRange( - in view, - rangeIndex, - tupleLandblockId, - publishSelection: false); - - bool deferTransparent = ShouldDeferPackedTransparent( - classified.AnyVao, - _alphaQueue?.IsCollecting == true); - InstanceLayoutCounts counts = PartitionInstanceGroups( - _packedGroups.Values, - deferTransparent, - cameraWorldPosition, - _packedOpaque, - _packedTransparent); - _packedOpaque.Sort(CompareOpaqueSubmissionOrder); - if (!deferTransparent) - { - _packedTransparent.Sort( - CompareTransparentSubmissionOrder); - } - - _packedSubmissions.Add(CreateDispatcherSubmission( - counts.VisibleInstances, - counts.ImmediateInstances, - deferTransparent, - _packedOpaque, - _packedTransparent, - cameraWorldPosition, - _packedAlphaFingerprintScratch)); - } - - _packedClassificationCache.EndFrame(); - } - - internal void BeginPackedProductionFrame( - in RenderFrameView view) - { - if (_packedProductionFrameOpen) - { - throw new InvalidOperationException( - "The packed dispatcher cannot begin a second production frame."); - } - - BeginPackedFrameStorage(in view); - _packedProductionFrameOpen = true; - _packedProductionGeneration = view.Generation; - _packedProductionFrameSequence = view.FrameSequence; - _packedProductionNextRange = 0; - } - - internal bool DrawPackedProductionRoute( - ICamera camera, - in RenderFrameView view, - RenderFrameCandidateRoute route, - int routeIndex, - uint cellId, - uint tupleLandblockId) - { - ValidatePackedProductionView(in view); - ReadOnlySpan ranges = - view.RouteRanges; - if (_packedProductionNextRange >= ranges.Length) - return false; - - RenderFrameCandidateRange range = - ranges[_packedProductionNextRange]; - if (range.Route != route - || range.RouteIndex != routeIndex - || range.CellId != cellId) - { - return false; - } - - int rangePosition = _packedProductionNextRange++; - bool diagnosticsEnabled = BeginEntityDispatch( - camera, - out Matrix4x4 viewProjection, - out Vector3 cameraWorldPosition); - PackedRangeClassification classified = - ClassifyPackedRange( - in view, - rangePosition, - tupleLandblockId, - publishSelection: true); - ExecuteClassifiedGroups( - viewProjection, - cameraWorldPosition, - classified.AnyVao, - _packedGroups.Values, - EntitySet.All, - classified.CandidateCount, - classified.MeshPartCount, - diagnosticsEnabled, - observeCurrentPath: false); - return true; - } - - internal void CompletePackedProductionFrame( - in RenderFrameView view) - { - ValidatePackedProductionView(in view); - int expected = view.RouteRanges.Length; - if (_packedProductionNextRange != expected) - { - throw new InvalidOperationException( - "The packed dispatcher did not consume the complete route stream: " - + $"consumed={_packedProductionNextRange} expected={expected}."); - } - - _packedClassificationCache.EndFrame(); - ResetPackedProductionFrame(); - } - - internal void AbortPackedProductionFrame() - { - if (!_packedProductionFrameOpen) - return; - - _packedClassificationCache.EndFrame(); - ResetPackedProductionFrame(); - } - - private void BeginPackedFrameStorage( - in RenderFrameView view) - { - if (_packedGroupFrame == long.MaxValue) - { - throw new InvalidOperationException( - "Packed dispatcher frame identity was exhausted."); - } - - _packedGroupFrame++; - PruneInstanceGroupsUnusedBeforeFrame( - _packedGroups, - _packedRetiredGroupKeys, - _packedGroupFrame - 1); - _packedEntityById.Clear(); - _packedSubmissions.Clear(); - _packedSelectionParts.Clear(); - _packedSelectionKeys.Clear(); - _packedClassificationCache.BeginFrame(view.Generation); - - ReadOnlySpan entities = - view.EntityCandidates; - for (int index = 0; index < entities.Length; index++) - { - if (!_packedEntityById.TryAdd( - entities[index].Projection.Id, - entities[index])) - { - throw new InvalidOperationException( - "Packed dispatcher received duplicate projection " - + $"{entities[index].Projection.Id}."); - } - } - } - - private PackedRangeClassification ClassifyPackedRange( - in RenderFrameView view, - int rangeIndex, - uint tupleLandblockId, - bool publishSelection) - { - ReadOnlySpan ranges = - view.RouteRanges; - if ((uint)rangeIndex >= (uint)ranges.Length) - throw new ArgumentOutOfRangeException(nameof(rangeIndex)); - - _nextPackedInstanceSubmissionOrder = 0; - foreach (InstanceGroup group in _packedGroups.Values) - group.ClearPerInstanceData(); - - uint anyVao = 0; - int meshPartCount = 0; - RenderFrameCandidateRange range = ranges[rangeIndex]; - ReadOnlySpan routeCandidates = - view.RouteCandidates; - ReadOnlySpan meshParts = view.MeshParts; - int end = checked(range.Offset + range.Count); - if ((uint)range.Offset > (uint)routeCandidates.Length - || (uint)end > (uint)routeCandidates.Length) - { - throw new InvalidOperationException( - $"Packed dispatcher route {rangeIndex} exceeds candidate storage."); - } - - for (int candidateIndex = range.Offset; - candidateIndex < end; - candidateIndex++) - { - RenderProjectionRecord projection = - routeCandidates[candidateIndex]; - if ((projection.Flags & RenderProjectionFlags.Draw) == 0) - continue; - if (!_packedEntityById.TryGetValue( - projection.Id, - out RenderFrameEntityCandidate source)) - { - throw new InvalidOperationException( - $"Packed route references absent entity {projection.Id}."); - } - - int meshEnd = checked( - source.MeshPartOffset + source.MeshPartCount); - if ((uint)source.MeshPartOffset > (uint)meshParts.Length - || (uint)meshEnd > (uint)meshParts.Length) - { - throw new InvalidOperationException( - $"Packed entity {projection.Id} exceeds mesh part storage."); - } - - meshPartCount = checked( - meshPartCount + source.MeshPartCount); - RenderInstanceCandidate candidate = - RenderInstanceCandidate.FromFrame( - in source, - tupleLandblockId); - ClassifyPackedEntity( - in projection, - in candidate, - meshParts.Slice( - source.MeshPartOffset, - source.MeshPartCount), - ref anyVao, - publishSelection, - range.Route is RenderFrameCandidateRoute.LookInObject - ? view.WalkLookInViews - : null, - range.RouteIndex, - range.CellId); - } - - return new PackedRangeClassification( - anyVao, - range.Count, - meshPartCount); - } - - private void ValidatePackedProductionView( - in RenderFrameView view) - { - if (!_packedProductionFrameOpen - || view.Generation != _packedProductionGeneration - || view.FrameSequence != _packedProductionFrameSequence) - { - throw new InvalidOperationException( - "The packed dispatcher received a stale or foreign production frame."); - } - } - - private void ResetPackedProductionFrame() - { - _packedProductionFrameOpen = false; - _packedProductionGeneration = default; - _packedProductionFrameSequence = 0; - _packedProductionNextRange = 0; - } - - private readonly record struct PackedRangeClassification( - uint AnyVao, - int CandidateCount, - int MeshPartCount); - - private void ClassifyPackedEntity( - in RenderProjectionRecord projection, - in RenderInstanceCandidate entity, - ReadOnlySpan meshParts, - ref uint anyVao, - bool publishSelection, - IWalkLookInViewSource? lookInViews, - int lookInRouteIndex, - uint lookInCellId) - { - (uint slot, bool culled) = ResolveSlotForFrame( - _clipRoutingActive, - entity.ServerGuid, - entity.ParentCell, - _cellIdToSlot, - _outdoorSlot, - _outdoorVisible); - if (culled) - return; - - ResolvePackedLightSet( - in entity, - out InstanceLightSet lights, - out bool indoor); - Vector2 selectionLighting = - _selectionLighting?.TryGetLighting( - entity.ServerGuid, - entity.LocalEntityId, - out RetailSelectionLighting lighting) == true - ? new Vector2( - lighting.Luminosity, - lighting.Diffuse) - : new Vector2(0f, 1f); - - PackedProjectionClassificationEntry? cacheEntry = null; - bool lookInConeActive = lookInViews is not null; - if (!entity.Animated && !lookInConeActive) - { - PackedClassificationIdentity identity = - PackedClassificationIdentity.From(in projection); - if (_packedClassificationCache.TryGetReusable( - entity.ProjectionId, - in identity, - projection.DirtyMask, - out cacheEntry)) - { - if (anyVao == 0 && !meshParts.IsEmpty) - { - ObjectRenderData? firstRenderData = - _meshAdapter.TryGetRenderData( - meshParts[0].MeshRef.GfxObjId); - if (firstRenderData is not null) - anyVao = firstRenderData.VAO; - } - ReplayPackedClassification( - cacheEntry!, - in entity, - slot, - lights, - indoor, - selectionLighting, - publishSelection); - return; - } - - cacheEntry = _packedClassificationCache.BeginRebuild( - entity.ProjectionId, - in identity); - } - else if (entity.Animated) - { - _packedClassificationCache.RecordAnimatedClassification(); - } - - PaletteCompositeIdentity paletteIdentity = default; - if (entity.PaletteOverride is not null) - { - paletteIdentity = TextureCache.GetPaletteIdentity( - entity.PaletteOverride); - } - - bool reusableAcrossFrames = !entity.Animated; - for (int meshIndex = 0; - meshIndex < meshParts.Length; - meshIndex++) - { - RenderFrameMeshPart packedPart = meshParts[meshIndex]; - if (packedPart.ProjectionId != entity.ProjectionId) - { - throw new InvalidOperationException( - $"Packed mesh part {packedPart.PartIndex} belongs " - + $"to {packedPart.ProjectionId}, expected " - + $"{entity.ProjectionId}."); - } - - int partIndex = packedPart.PartIndex; - MeshRef meshRef = packedPart.MeshRef; - ObjectRenderData? renderData = - _meshAdapter.TryGetRenderData(meshRef.GfxObjId); - if (renderData is null) - { - reusableAcrossFrames = false; - if (_missRequested.Add(meshRef.GfxObjId)) - _meshAdapter.EnsureLoaded(meshRef.GfxObjId); - continue; - } - if (anyVao == 0) - anyVao = renderData.VAO; - - if (renderData.IsSetup - && renderData.SetupParts.Count > 0) - { - // Campaign VM VM6 review fix round 2 (F1): same entity- - // scoped HasCutoutSubset OR the classic world-receiver loop - // computes (WbDrawDispatcher.cs) and the caster loop - // (DirectionalShadows.cs) — see - // FoliageWindClassification.ComputeEntityHasCutoutSubset's - // doc comment. A Setup composite's parts are separate - // GfxObjs with independently-cached HasCutoutSubset; without - // this bounded per-entity scan, the opaque trunk part of a - // production-classified tree would never get the trunk - // flag. The context-taking overload passes _meshAdapter - // explicitly to a static lambda (F3) — zero allocation per - // entity per frame. - bool entityHasCutoutSubset = FoliageWindClassification - .ComputeEntityHasCutoutSubset( - renderData.SetupParts, - _meshAdapter, - static (adapter, part) => adapter.TryGetRenderData(part.GfxObjId) - is { HasCutoutSubset: true }); - - for (int setupPartIndex = 0; - setupPartIndex < renderData.SetupParts.Count; - setupPartIndex++) - { - (ulong gfxObjId, Matrix4x4 partTransform) = - renderData.SetupParts[setupPartIndex]; - ObjectRenderData? partData = - _meshAdapter.TryGetRenderData(gfxObjId); - if (partData is null) - { - reusableAcrossFrames = false; - if (_missRequested.Add(gfxObjId)) - _meshAdapter.EnsureLoaded(gfxObjId); - continue; - } - - float opacity = PackedPartOpacity( - entity.ServerGuid, - entity.LocalEntityId, - (uint)setupPartIndex); - if (opacity < 1f) - reusableAcrossFrames = false; - if (opacity <= 0f) - continue; - - Matrix4x4 restPose = - partTransform * meshRef.PartTransform; - Matrix4x4 model = - restPose * entity.RootWorld; - int selectionPartIndex = unchecked( - (partIndex << 16) - | (setupPartIndex & 0xFFFF)); - if (!PartVisibleInLookInTurn( - lookInViews, - lookInRouteIndex, - lookInCellId, - in entity, - selectionPartIndex, - (uint)gfxObjId, - partData, - model)) - { - continue; - } - if (!ClassifyPackedBatches( - partData, - restPose, - model, - in entity, - meshRef, - paletteIdentity, - slot, - lights, - indoor, - selectionLighting, - opacity, - cacheEntry, - entityHasCutoutSubset)) - { - reusableAcrossFrames = false; - } - cacheEntry?.SelectionParts.Add( - new PackedClassifiedSelectionPart( - selectionPartIndex, - (uint)gfxObjId, - restPose)); - AddPackedSelectionPart( - in entity, - selectionPartIndex, - (uint)gfxObjId, - model, - publishSelection); - } - } - else - { - // #188/#32: the packed part ordinal IS the retail CPartArray - // part ordinal TransparentPartHook.PartIndex addresses (one - // bare-GfxObj MeshRef per Setup.Parts[i] for flattened live - // entities; trivially 0 for single-part objects). The previous - // constant 0 mirrored the legacy dispatcher's false - // one-part assumption and kept the Bind Stone's four - // hook-hidden shard parts visible. - float opacity = PackedPartOpacity( - entity.ServerGuid, - entity.LocalEntityId, - (uint)partIndex); - if (opacity < 1f) - reusableAcrossFrames = false; - if (opacity <= 0f) - continue; - - Matrix4x4 restPose = meshRef.PartTransform; - Matrix4x4 model = restPose * entity.RootWorld; - if (!PartVisibleInLookInTurn( - lookInViews, - lookInRouteIndex, - lookInCellId, - in entity, - partIndex, - meshRef.GfxObjId, - renderData, - model)) - { - continue; - } - if (!ClassifyPackedBatches( - renderData, - restPose, - model, - in entity, - meshRef, - paletteIdentity, - slot, - lights, - indoor, - selectionLighting, - opacity, - cacheEntry)) - { - reusableAcrossFrames = false; - } - cacheEntry?.SelectionParts.Add( - new PackedClassifiedSelectionPart( - partIndex, - meshRef.GfxObjId, - restPose)); - AddPackedSelectionPart( - in entity, - partIndex, - meshRef.GfxObjId, - model, - publishSelection); - } - } - - if (cacheEntry is not null) - { - _packedClassificationCache.CompleteRebuild( - cacheEntry, - reusableAcrossFrames); - } - } - - /// - /// Retail RenderDeviceD3D::DrawMesh @0x005A0860 admits each CGfxObj - /// independently by transforming its drawing sphere into the active - /// portal view. A whole-entity sphere is not equivalent for multipart - /// creatures: the union can be roughly ten metres wide and intersect an - /// aperture while every actual body/equipment part is behind its wall. - /// - private bool PartVisibleInLookInTurn( - IWalkLookInViewSource? lookInViews, - int routeIndex, - uint cellId, - in RenderInstanceCandidate entity, - int partIndex, - uint gfxObjId, - ObjectRenderData renderData, - Matrix4x4 localToWorld) - { - if (lookInViews is null) - return true; - - // ObjectRenderData.SelectionSphere is retained per GfxObj by the - // prepared mesh payload. It is never the entity-union sphere and is - // therefore the correct granularity for DrawMesh admission. The - // current package stores a conservative vertex-derived sphere here; - // this preserves whole-mesh drawing while avoiding the invalid - // aggregate-character admission that caused the cathedral bleed. - if (renderData.SelectionSphere is not { Radius: > 0f } sphere) - return true; - - bool visible = LookInDrawingSphereVisible( - lookInViews, - routeIndex, - sphere, - localToWorld, - out Vector3 center, - out float radius); - - if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled - && cellId == 0xF4180112u) - { - var key = (routeIndex, entity.LocalEntityId, partIndex, gfxObjId); - string state = - $"visible={(visible ? 1 : 0)} " - + $"center=({center.X:F2},{center.Y:F2},{center.Z:F2}) " - + $"r={radius:F2}"; - if (!_probeLookInPartStates.TryGetValue(key, out string? previous) - || previous != state) - { - _probeLookInPartStates[key] = state; - Console.WriteLine( - $"[lookin-part] route={routeIndex} cell=0x{cellId:X8} " - + $"id={entity.LocalEntityId:x} part={partIndex} " - + $"gfx=0x{gfxObjId:X8} {state}"); - } - } - - return visible; - } - - internal static bool LookInDrawingSphereVisible( - IWalkLookInViewSource lookInViews, - int routeIndex, - DatReaderWriter.Types.Sphere sphere, - Matrix4x4 localToWorld, - out Vector3 center, - out float radius) - { - ArgumentNullException.ThrowIfNull(lookInViews); - ArgumentNullException.ThrowIfNull(sphere); - - center = Vector3.Transform(sphere.Origin, localToWorld); - float scaleX = new Vector3( - localToWorld.M11, - localToWorld.M12, - localToWorld.M13).Length(); - float scaleY = new Vector3( - localToWorld.M21, - localToWorld.M22, - localToWorld.M23).Length(); - float scaleZ = new Vector3( - localToWorld.M31, - localToWorld.M32, - localToWorld.M33).Length(); - radius = sphere.Radius - * MathF.Max(scaleX, MathF.Max(scaleY, scaleZ)); - return lookInViews.SphereVisibleInLookInTurn( - routeIndex, - in center, - radius); - } - - /// - /// Mirrors the production dispatcher's no-VAO early return. That return - /// records an empty submission with transparent deferral disabled even - /// when the frame alpha queue is collecting. - /// - internal static bool ShouldDeferPackedTransparent( - uint anyVao, - bool alphaQueueCollecting) => - anyVao != 0 && alphaQueueCollecting; - - private float PackedPartOpacity( - uint serverGuid, - uint localEntityId, - uint setupPartIndex) - { - float opacity = EntityOpacity(serverGuid); - if (opacity <= 0f) - return 0f; - if (!_translucencyFades.TryGetCurrentValue( - localEntityId, - setupPartIndex, - out float translucency)) - { - return opacity; - } - - return translucency >= 1f - ? 0f - : opacity * (1f - translucency); - } - - private bool ClassifyPackedBatches( - ObjectRenderData renderData, - Matrix4x4 restPose, - Matrix4x4 model, - in RenderInstanceCandidate entity, - MeshRef meshRef, - PaletteCompositeIdentity paletteIdentity, - uint slot, - InstanceLightSet lights, - bool indoor, - Vector2 selectionLighting, - float opacity, - PackedProjectionClassificationEntry? cacheEntry, - // Campaign VM VM6 review fix round 2 (F1): mirrors the classic - // dispatcher's ClassifyBatches override exactly. HasCutoutSubset is - // a per-PART (per-GfxObj) fact; the caller passes the ENTITY/Setup- - // scoped OR across every resolved part for a Setup composite. null - // (the default) means "use renderData.HasCutoutSubset directly", - // which is already correct for a non-Setup single-mesh entity. - bool? entityHasCutoutSubsetOverride = null) - { - bool entityHasCutoutSubset = - entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset; - bool reusableAcrossFrames = true; - for (int batchIndex = 0; - batchIndex < renderData.Batches.Count; - batchIndex++) - { - // Campaign FW stage FW3.2a: mirrors the classic ClassifyBatches - // gate/promotion/resolve/foliage-classify sequence exactly — see - // the one shared core (WbDrawDispatcher.WalkClassify.cs's - // TryClassifyBatch) also used by ClassifyBatches and the walk - // classifier, so the classic and packed classifiers cannot drift - // (Campaign VM VM6). `survives=false` still applies - // compositePending exactly as before this extraction. - bool survives = TryClassifyBatch( - renderData, batchIndex, in entity, meshRef, paletteIdentity, - opacity, entityHasCutoutSubset, - out GroupKey key, out bool compositePending); - if (compositePending) - reusableAcrossFrames = false; - if (!survives) - continue; - - var classified = new PackedClassifiedBatch( - key, - restPose, - renderData.SortCenter); - cacheEntry?.Batches.Add(classified); - AppendPackedClassification( - in classified, - model, - slot, - lights, - indoor, - entity.IsBuildingShell, - opacity, - selectionLighting); - } - - return reusableAcrossFrames; - } - - private void ReplayPackedClassification( - PackedProjectionClassificationEntry entry, - in RenderInstanceCandidate entity, - uint slot, - InstanceLightSet lights, - bool indoor, - Vector2 selectionLighting, - bool publishSelection) - { - for (int i = 0; i < entry.Batches.Count; i++) - { - PackedClassifiedBatch classified = entry.Batches[i]; - AppendPackedClassification( - in classified, - classified.RestPose * entity.RootWorld, - slot, - lights, - indoor, - entity.IsBuildingShell, - opacity: 1f, - selectionLighting); - } - - for (int i = 0; i < entry.SelectionParts.Count; i++) - { - PackedClassifiedSelectionPart part = - entry.SelectionParts[i]; - AddPackedSelectionPart( - in entity, - part.PartIndex, - part.GfxObjId, - part.RestPose * entity.RootWorld, - publishSelection); - } - } - - private void AppendPackedClassification( - in PackedClassifiedBatch classified, - Matrix4x4 model, - uint slot, - InstanceLightSet lights, - bool indoor, - bool buildingDetail, - float opacity, - Vector2 selectionLighting) - { - InstanceGroup group = - GetOrCreatePackedGroup(classified.Key); - AppendPackedInstance( - group, - model, - classified.LocalSortCenter, - _nextPackedInstanceSubmissionOrder++, - slot, - lights, - indoor, - buildingDetail, - opacity, - selectionLighting); - } - - /// - /// Appends one packed-route instance and every per-instance attribute in - /// lockstep. Keeping the writer in one testable seam prevents a newly - /// introduced storage binding from covering the legacy classifier while - /// leaving the production packed classifier with a shorter parallel list. - /// - internal static void AppendPackedInstance( - InstanceGroup group, - Matrix4x4 model, - Vector3 localSortCenter, - int submissionOrder, - uint slot, - InstanceLightSet lights, - bool indoor, - bool buildingDetail, - float opacity, - Vector2 selectionLighting) - { - ArgumentNullException.ThrowIfNull(group); - group.Matrices.Add(model); - group.LocalSortCenters.Add(localSortCenter); - group.SubmissionOrders.Add(submissionOrder); - group.Slots.Add(slot); - group.LightSets.Add(lights); - group.IndoorFlags.Add(indoor ? 1u : 0u); - group.DetailCategories.Add(buildingDetail ? 1u : 0u); - group.Opacities.Add(opacity); - group.SelectionLighting.Add(selectionLighting); - } - - private InstanceGroup GetOrCreatePackedGroup(GroupKey key) - { - if (_packedGroups.TryGetValue( - key, - out InstanceGroup? group)) - { - group.LastUsedFrame = _packedGroupFrame; - return group; - } - if (_nextPackedGroupRegistration == long.MaxValue) - { - throw new InvalidOperationException( - "Packed instance-group registration space was exhausted."); - } - - // Campaign VM VM6 review fix round 3 (N2a): both routes share the - // ONE InstanceGroup-from-key seam now — see CreateGroupFromKey's - // doc comment. This is exactly the seam that closed the F1 bug - // (FoliageFlags copied on the classic side, dropped here) by - // construction: there is now exactly one place either route can - // build an InstanceGroup, and it always copies every GroupKey field. - group = CreateGroupFromKey( - key, - registration: _nextPackedGroupRegistration++, - frame: _packedGroupFrame); - _packedGroups.Add(key, group); - return group; - } - - private void ResolvePackedLightSet( - in RenderInstanceCandidate entity, - out InstanceLightSet lights, - out bool indoor) - { - indoor = IndoorObjectReceivesTorches(entity.ParentCell); - lights = InstanceLightSet.Disabled; - IReadOnlyList? snapshot = _pointSnapshot; - if (!indoor || snapshot is null || snapshot.Count == 0) - return; - - Vector3 center = - (entity.Bounds.Minimum + entity.Bounds.Maximum) * 0.5f; - float radius = - (entity.Bounds.Maximum - entity.Bounds.Minimum) - .Length() * 0.5f; - Span selected = - stackalloc int[LightManager.MaxLightsPerObject]; - selected.Fill(-1); - LightManager.SelectForObject( - snapshot, - center, - radius, - selected); - lights = InstanceLightSet.From(selected); - } - - private void AddPackedSelectionPart( - in RenderInstanceCandidate entity, - int partIndex, - uint gfxObjId, - Matrix4x4 localToWorld, - bool publishSelection) - { - if (!_packedSelectionKeys.Add(new PackedSelectionKey( - entity.LocalEntityId, - partIndex, - gfxObjId))) - { - return; - } - - if (publishSelection) - { - _selectionSink?.AddVisiblePart( - entity.ServerGuid, - entity.LocalEntityId, - partIndex, - gfxObjId, - localToWorld); - return; - } - - if (_selectionSink is not IRetailSelectionRenderOracle oracle - || !oracle.TryCreateVisiblePart( - entity.ServerGuid, - entity.LocalEntityId, - partIndex, - gfxObjId, - localToWorld, - out RetailSelectionPart part)) - { - return; - } - - _packedSelectionParts.Add( - new CurrentRenderSelectionFingerprint( - Sequence: _packedSelectionParts.Count, - ServerGuid: part.ServerGuid, - LocalEntityId: part.LocalEntityId, - PartIndex: part.PartIndex, - GfxObjId: gfxObjId, - LocalToWorld: part.LocalToWorld, - Geometry: CurrentRenderSceneOracle - .FingerprintSelectionGeometry(part.Mesh))); - } - - private readonly record struct PackedSelectionKey( - uint LocalEntityId, - int PartIndex, - uint GfxObjId); -} diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs index 0b3e244a..574386e1 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs @@ -2,6 +2,7 @@ using System.Numerics; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Selection; using AcDream.App.Rendering.Walk; +using AcDream.Core.Lighting; using AcDream.Core.Meshing; using AcDream.Core.World; using DatReaderWriter.Enums; @@ -166,7 +167,7 @@ public sealed partial class WbDrawDispatcher /// Classifies one static entity for the walk populator: resolves its clip /// slot / light set / selection lighting exactly as /// ClassifyPackedEntity does (via the shared - /// / ResolvePackedLightSet + /// / ResolveWalkLightSet /// helpers), walks its Setup parts or single mesh (mirroring /// ClassifyPackedEntity's own shape), and appends one /// per surviving batch to @@ -204,7 +205,7 @@ public sealed partial class WbDrawDispatcher if (culled) return; - ResolvePackedLightSet(in entity, out InstanceLightSet lights, out bool indoor); + ResolveWalkLightSet(in entity, out InstanceLightSet lights, out bool indoor); Vector2 selectionLighting = _selectionLighting?.TryGetLighting( entity.ServerGuid, entity.LocalEntityId, out RetailSelectionLighting lighting) == true @@ -261,7 +262,7 @@ public sealed partial class WbDrawDispatcher } float opacity = liveDynamic - ? PackedPartOpacity( + ? WalkPartOpacity( entity.ServerGuid, entity.LocalEntityId, (uint)setupPartIndex) @@ -298,7 +299,7 @@ public sealed partial class WbDrawDispatcher else { float opacity = liveDynamic - ? PackedPartOpacity( + ? WalkPartOpacity( entity.ServerGuid, entity.LocalEntityId, (uint)partIndex) @@ -330,6 +331,110 @@ public sealed partial class WbDrawDispatcher } } + private float WalkPartOpacity( + uint serverGuid, + uint localEntityId, + uint setupPartIndex) + { + float opacity = EntityOpacity(serverGuid); + if (opacity <= 0f) + return 0f; + if (!_translucencyFades.TryGetCurrentValue( + localEntityId, + setupPartIndex, + out float translucency)) + { + return opacity; + } + + return translucency >= 1f + ? 0f + : opacity * (1f - translucency); + } + + private void ResolveWalkLightSet( + in RenderInstanceCandidate entity, + out InstanceLightSet lights, + out bool indoor) + { + indoor = IndoorObjectReceivesTorches(entity.ParentCell); + lights = InstanceLightSet.Disabled; + IReadOnlyList? snapshot = _pointSnapshot; + if (!indoor || snapshot is null || snapshot.Count == 0) + return; + + Vector3 center = + (entity.Bounds.Minimum + entity.Bounds.Maximum) * 0.5f; + float radius = + (entity.Bounds.Maximum - entity.Bounds.Minimum) + .Length() * 0.5f; + Span selected = + stackalloc int[LightManager.MaxLightsPerObject]; + selected.Fill(-1); + LightManager.SelectForObject( + snapshot, + center, + radius, + selected); + lights = InstanceLightSet.From(selected); + } + + private static bool PartVisibleInLookInTurn( + IWalkLookInViewSource? lookInViews, + int routeIndex, + uint cellId, + in RenderInstanceCandidate entity, + int partIndex, + uint gfxObjId, + ObjectRenderData renderData, + Matrix4x4 localToWorld) + { + if (lookInViews is null) + return true; + if (renderData.SelectionSphere is not { Radius: > 0f } sphere) + return true; + + return LookInDrawingSphereVisible( + lookInViews, + routeIndex, + sphere, + localToWorld, + out _, + out _); + } + + internal static bool LookInDrawingSphereVisible( + IWalkLookInViewSource lookInViews, + int routeIndex, + DatReaderWriter.Types.Sphere sphere, + Matrix4x4 localToWorld, + out Vector3 center, + out float radius) + { + ArgumentNullException.ThrowIfNull(lookInViews); + ArgumentNullException.ThrowIfNull(sphere); + + center = Vector3.Transform(sphere.Origin, localToWorld); + float scaleX = new Vector3( + localToWorld.M11, + localToWorld.M12, + localToWorld.M13).Length(); + float scaleY = new Vector3( + localToWorld.M21, + localToWorld.M22, + localToWorld.M23).Length(); + float scaleZ = new Vector3( + localToWorld.M31, + localToWorld.M32, + localToWorld.M33).Length(); + radius = sphere.Radius + * MathF.Max(scaleX, MathF.Max(scaleY, scaleZ)); + return lookInViews.SphereVisibleInLookInTurn( + routeIndex, + in center, + radius); + } + /// /// Walks one resolved mesh's batches through /// and appends every surviving one to at diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 4002d188..ba713392 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -2305,18 +2305,9 @@ public sealed partial class WbDrawDispatcher : IDisposable /// /// Campaign VM VM6 review fix round 3 (N2a): the ONE shared - /// InstanceGroup-from-key construction seam. Before this there were two - /// near-identical new InstanceGroup { ... } initializers — the - /// classic route's and the packed - /// route's GetOrCreatePackedGroup — that had already drifted once - /// (the packed route's F1 bug: FoliageFlags copied on one side, dropped - /// on the other). and - /// are the only two values that legitimately - /// differ per route (each owns its own registration counter and frame - /// serial); every other field comes from , so a - /// field neither site remembers to set can no longer exist. Same - /// precedent as — one writer, tested - /// once, called from both routes. + /// InstanceGroup-from-key construction seam retained by the classic + /// diagnostic route. The production walk writes its ordered stream + /// directly and the former packed twin was deleted at Campaign FW4. /// internal static InstanceGroup CreateGroupFromKey( GroupKey key, diff --git a/tests/AcDream.App.Tests/Rendering/Wb/PackedDispatcherOracleTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/PackedDispatcherOracleTests.cs deleted file mode 100644 index c1664b8a..00000000 --- a/tests/AcDream.App.Tests/Rendering/Wb/PackedDispatcherOracleTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -using AcDream.App.Rendering.Wb; -using AcDream.App.Rendering.Gpu; -using AcDream.Core.Meshing; -using System.Numerics; - -namespace AcDream.App.Tests.Rendering.Wb; - -public sealed class PackedDispatcherOracleTests -{ - /// - /// Campaign VM VM6 review fix round 3 (N1): this test does NOT guard - /// the packed classifier itself — CreateGroupFromKey's own test - /// (InstanceGroupClearTests.CreateGroupFromKey_CopiesFoliageFlagsFromTheKey) - /// plus GroupKey.FoliageFlags being a required constructor argument are - /// what actually make F1 (the round-2 blocker: the packed classifier's - /// InstanceGroup silently carried FoliageFlags == 0) impossible to - /// reintroduce. What THIS test proves is the SHARED production step - /// downstream of classification: FoliageWindClassification.Classify, - /// called with the same argument shape ClassifyPackedBatches uses - /// (entity.LocalEntityId, exclusion membership, batch.Translucency, - /// entity-scoped HasCutoutSubset), resolves a real procedural-scenery - /// entity id (0x8…) to CutoutFoliageFlag (0x2); and a group carrying - /// that flags value reaches the literal BatchData.flags word through - /// BuildIndirectArrays — the same already-tested production step both - /// the classic and packed group lists feed into - /// (WbDrawDispatcherIndirectBuilderTests pins bit 0; this pins bits 1/2 - /// landing alongside it for a real scenery id). - /// - [Fact] - public void SceneryCutoutEntityClassificationReachesTheBatchDataFlagsWordAsBit0x2() - { - const uint proceduralSceneryTreeId = 0x80010203u; // top nibble 0x8 - - uint foliageFlags = FoliageWindClassification.Classify( - proceduralSceneryTreeId, - isExcluded: false, - TranslucencyKind.ClipMap, - meshHasCutoutSubset: true); - Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, foliageFlags); - Assert.Equal(0x2u, foliageFlags); - - var groups = new List - { - new( - IndexCount: 12, - FirstIndex: 0, - BaseVertex: 0, - InstanceCount: 1, - FirstInstance: 0, - TextureIndex: 0x5, - TextureLayer: 0, - Translucency: TranslucencyKind.ClipMap, - FoliageFlags: foliageFlags), - }; - var indirect = new DrawElementsIndirectCommand[4]; - var batches = new WbDrawDispatcher.BatchDataPublic[4]; - - WbDrawDispatcher.BuildIndirectArrays(groups, indirect, batches); - - // Bit 0 (the #226 built-mesh marker) | bit 1 (cutout foliage) = 0x3; - // isolating bit 1 with a mask proves the foliage classification - // specifically reached the word, not merely that SOME nonzero value - // did. - Assert.Equal(0x2u, batches[0].Flags & 0x2u); - } - - [Theory] - [InlineData(false, 0u)] - [InlineData(true, 1u)] - public void PackedInstanceWriter_AppendsDetailCategoryInParallel( - bool buildingDetail, - uint expectedCategory) - { - var group = new WbDrawDispatcher.InstanceGroup(); - - WbDrawDispatcher.AppendPackedInstance( - group, - Matrix4x4.Identity, - Vector3.One, - submissionOrder: 7, - slot: 3u, - lights: WbDrawDispatcher.InstanceLightSet.Disabled, - indoor: true, - buildingDetail: buildingDetail, - opacity: 0.5f, - selectionLighting: new Vector2(0.25f, 0.75f)); - - Assert.Single(group.Matrices); - Assert.Single(group.LocalSortCenters); - Assert.Single(group.SubmissionOrders); - Assert.Single(group.Slots); - Assert.Single(group.LightSets); - Assert.Single(group.IndoorFlags); - Assert.Equal(expectedCategory, Assert.Single(group.DetailCategories)); - Assert.Single(group.Opacities); - Assert.Single(group.SelectionLighting); - } - - [Theory] - [InlineData(0u, false, false)] - [InlineData(0u, true, false)] - [InlineData(7u, false, false)] - [InlineData(7u, true, true)] - public void TransparentDeferral_MatchesProductionNoVaoEarlyReturn( - uint anyVao, - bool alphaQueueCollecting, - bool expected) - { - Assert.Equal( - expected, - WbDrawDispatcher.ShouldDeferPackedTransparent( - anyVao, - alphaQueueCollecting)); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Wb/PackedProjectionClassificationCacheTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/PackedProjectionClassificationCacheTests.cs deleted file mode 100644 index 95e829bb..00000000 --- a/tests/AcDream.App.Tests/Rendering/Wb/PackedProjectionClassificationCacheTests.cs +++ /dev/null @@ -1,215 +0,0 @@ -using AcDream.App.Rendering.Scene; -using AcDream.App.Rendering.Wb; -using Xunit; - -namespace AcDream.App.Tests.Rendering.Wb; - -public sealed class PackedProjectionClassificationCacheTests -{ - [Fact] - public void UnchangedProjection_ReusesClassificationAcrossFrames() - { - var cache = new PackedProjectionClassificationCache(); - RenderSceneGeneration generation = - RenderSceneGeneration.FromRaw(1); - RenderProjectionId id = RenderProjectionId.FromRaw(0x1234); - PackedClassificationIdentity identity = Identity(7); - - cache.BeginFrame(generation); - PackedProjectionClassificationEntry entry = - cache.BeginRebuild(id, in identity); - cache.CompleteRebuild(entry, reusableAcrossFrames: true); - cache.EndFrame(); - - cache.BeginFrame(generation); - Assert.True(cache.TryGetReusable( - id, - in identity, - RenderDirtyMask.Transform, - out PackedProjectionClassificationEntry? reused)); - cache.EndFrame(); - - Assert.Same(entry, reused); - Assert.Equal(0, cache.Snapshot.StaticRebuildCount); - Assert.Equal(1, cache.Snapshot.CrossFrameReuseCount); - Assert.Equal(1, cache.Snapshot.ProjectionCount); - } - - [Fact] - public void AppearanceDirty_RequiresRebuildButTransformDirtyDoesNot() - { - var cache = new PackedProjectionClassificationCache(); - RenderSceneGeneration generation = - RenderSceneGeneration.FromRaw(1); - RenderProjectionId id = RenderProjectionId.FromRaw(0x1234); - PackedClassificationIdentity identity = Identity(7); - - cache.BeginFrame(generation); - PackedProjectionClassificationEntry entry = - cache.BeginRebuild(id, in identity); - cache.CompleteRebuild(entry, reusableAcrossFrames: true); - cache.EndFrame(); - - cache.BeginFrame(generation); - Assert.False(cache.TryGetReusable( - id, - in identity, - RenderDirtyMask.Appearance, - out _)); - PackedProjectionClassificationEntry rebuilt = - cache.BeginRebuild(id, in identity); - cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true); - - Assert.Same(entry, rebuilt); - Assert.Equal(1, cache.Snapshot.StaticRebuildCount); - } - - [Fact] - public void IncompleteBuild_CanReplayDuplicateRouteOnlyInSameFrame() - { - var cache = new PackedProjectionClassificationCache(); - RenderSceneGeneration generation = - RenderSceneGeneration.FromRaw(1); - RenderProjectionId id = RenderProjectionId.FromRaw(0x1234); - PackedClassificationIdentity identity = Identity(7); - - cache.BeginFrame(generation); - PackedProjectionClassificationEntry entry = - cache.BeginRebuild(id, in identity); - cache.CompleteRebuild(entry, reusableAcrossFrames: false); - - Assert.True(cache.TryGetReusable( - id, - in identity, - RenderDirtyMask.All, - out _)); - Assert.Equal(1, cache.Snapshot.SameFrameReuseCount); - cache.EndFrame(); - - cache.BeginFrame(generation); - Assert.False(cache.TryGetReusable( - id, - in identity, - RenderDirtyMask.None, - out _)); - } - - [Fact] - public void IncarnationOrAppearanceChange_CannotReuseRetainedPayload() - { - var cache = new PackedProjectionClassificationCache(); - RenderSceneGeneration generation = - RenderSceneGeneration.FromRaw(1); - RenderProjectionId id = RenderProjectionId.FromRaw(0x1234); - PackedClassificationIdentity original = Identity(7); - - cache.BeginFrame(generation); - PackedProjectionClassificationEntry entry = - cache.BeginRebuild(id, in original); - cache.CompleteRebuild(entry, reusableAcrossFrames: true); - cache.EndFrame(); - - cache.BeginFrame(generation); - PackedClassificationIdentity replacement = Identity(17); - Assert.False(cache.TryGetReusable( - id, - in replacement, - RenderDirtyMask.None, - out _)); - PackedProjectionClassificationEntry rebuilt = - cache.BeginRebuild(id, in replacement); - cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true); - cache.EndFrame(); - - Assert.Same(entry, rebuilt); - Assert.Equal(replacement, rebuilt.Identity); - Assert.Equal(1, cache.Snapshot.StaticRebuildCount); - Assert.Equal(0, cache.Snapshot.CrossFrameReuseCount); - } - - [Fact] - public void GenerationReplacementAndUnseenProjectionReleaseRetainedEntries() - { - var cache = new PackedProjectionClassificationCache(); - RenderProjectionId id = RenderProjectionId.FromRaw(0x1234); - PackedClassificationIdentity identity = Identity(7); - - cache.BeginFrame(RenderSceneGeneration.FromRaw(1)); - PackedProjectionClassificationEntry entry = - cache.BeginRebuild(id, in identity); - entry.Batches.Capacity = 32; - entry.SelectionParts.Capacity = 16; - cache.CompleteRebuild(entry, reusableAcrossFrames: true); - cache.EndFrame(); - Assert.True(cache.Snapshot.RetainedPayloadBytes > 0); - - cache.BeginFrame(RenderSceneGeneration.FromRaw(1)); - cache.EndFrame(); - - Assert.Equal(0, cache.Snapshot.ProjectionCount); - Assert.Equal(1, cache.Snapshot.RetiredProjectionCount); - Assert.Equal(0, entry.Batches.Capacity); - Assert.Equal(0, entry.SelectionParts.Capacity); - Assert.Equal(0, cache.Snapshot.RetainedPayloadBytes); - - cache.BeginFrame(RenderSceneGeneration.FromRaw(2)); - Assert.Equal(0, cache.Snapshot.ProjectionCount); - } - - [Fact] - public void WarmUnchangedProjectionCache_AllocatesZeroBytes() - { - var cache = new PackedProjectionClassificationCache(); - RenderSceneGeneration generation = - RenderSceneGeneration.FromRaw(1); - RenderProjectionId id = RenderProjectionId.FromRaw(0x1234); - PackedClassificationIdentity identity = Identity(7); - - cache.BeginFrame(generation); - PackedProjectionClassificationEntry entry = - cache.BeginRebuild(id, in identity); - cache.CompleteRebuild(entry, reusableAcrossFrames: true); - cache.EndFrame(); - - // #250: the measured window was a 1,000-iteration loop written inline. - ZeroAllocationProbe.AssertAllocatesNothing( - "PackedProjectionClassificationCache warm cache hit", - () => Hit(cache, generation, id, identity)); - } - - private static void Hit( - PackedProjectionClassificationCache cache, - RenderSceneGeneration generation, - RenderProjectionId id, - PackedClassificationIdentity identity) - { - cache.BeginFrame(generation); - if (!cache.TryGetReusable( - id, - in identity, - RenderDirtyMask.None, - out _)) - { - throw new InvalidOperationException( - "The warmed projection classification was not reusable."); - } - cache.EndFrame(); - } - - private static PackedClassificationIdentity Identity(ulong seed) => - new( - RenderOwnerIncarnation.FromRaw(seed), - new RenderMeshSet( - RenderAssetHandle.FromRaw(seed + 1), - MeshCount: 2, - Revision: seed + 2), - new RenderMaterialVariant( - PaletteKey: seed + 3, - TextureReplacementKey: seed + 4, - Opacity: 1f), - new RenderDegradeState( - Level: 0, - Revision: (uint)(seed + 5)), - new RenderSceneHash128(seed + 6, seed + 7), - new RenderSceneHash128(seed + 8, seed + 9)); -}