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 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; 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); } 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) { (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; if (!entity.Animated) { 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 { _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) { 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.LocalEntityId, (uint)setupPartIndex); if (opacity < 1f) reusableAcrossFrames = false; if (opacity <= 0f) continue; Matrix4x4 restPose = partTransform * meshRef.PartTransform; Matrix4x4 model = restPose * entity.RootWorld; if (!ClassifyPackedBatches( partData, restPose, model, in entity, meshRef, paletteIdentity, slot, lights, indoor, selectionLighting, opacity, cacheEntry)) { reusableAcrossFrames = false; } int selectionPartIndex = unchecked( (partIndex << 16) | (setupPartIndex & 0xFFFF)); cacheEntry?.SelectionParts.Add( new PackedClassifiedSelectionPart( selectionPartIndex, (uint)gfxObjId, restPose)); AddPackedSelectionPart( in entity, selectionPartIndex, (uint)gfxObjId, model, publishSelection); } } else { float opacity = PackedPartOpacity( entity.LocalEntityId, 0u); if (opacity < 1f) reusableAcrossFrames = false; if (opacity <= 0f) continue; Matrix4x4 restPose = meshRef.PartTransform; Matrix4x4 model = restPose * entity.RootWorld; 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); } } /// /// 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 localEntityId, uint setupPartIndex) { if (!_translucencyFades.TryGetCurrentValue( localEntityId, setupPartIndex, out float translucency)) { return 1f; } return translucency >= 1f ? 0f : 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) { bool reusableAcrossFrames = true; for (int batchIndex = 0; batchIndex < renderData.Batches.Count; batchIndex++) { ObjectRenderBatch batch = renderData.Batches[batchIndex]; TranslucencyKind translucency = batch.Translucency; if (opacity < 1f && IsOpaque(translucency)) translucency = TranslucencyKind.AlphaBlend; ResolvedTexture texture = ResolveTexture( in entity, meshRef, batch, paletteIdentity, out bool compositePending); if (compositePending) reusableAcrossFrames = false; if (texture.Handle == 0) continue; var key = new GroupKey( batch.FirstIndex, (int)batch.BaseVertex, batch.IndexCount, texture.Handle, texture.Layer, translucency, batch.CullMode); var classified = new PackedClassifiedBatch( key, restPose, renderData.SortCenter); cacheEntry?.Batches.Add(classified); AppendPackedClassification( in classified, model, slot, lights, indoor, 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, 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, float opacity, Vector2 selectionLighting) { InstanceGroup group = GetOrCreatePackedGroup(classified.Key); group.Matrices.Add(model); group.LocalSortCenters.Add( classified.LocalSortCenter); group.SubmissionOrders.Add( _nextPackedInstanceSubmissionOrder++); group.Slots.Add(slot); group.LightSets.Add(lights); group.IndoorFlags.Add(indoor ? 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."); } group = new InstanceGroup { FirstIndex = key.FirstIndex, BaseVertex = key.BaseVertex, IndexCount = key.IndexCount, BindlessTextureHandle = key.BindlessTextureHandle, TextureLayer = key.TextureLayer, Translucency = key.Translucency, CullMode = key.CullMode, Registration = _nextPackedGroupRegistration++, LastUsedFrame = _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); }