using AcDream.App.Rendering.Wb; using AcDream.App.Streaming; using AcDream.Core.World; namespace AcDream.App.Rendering.Scene; internal interface IRenderStaticProjectionJournalSink { void Reconcile( LandblockBuild build, GpuLandblockSpatialPublication publication); void Retire(GpuLandblockRetirement retirement); } /// /// Derived, non-authoritative mirror of committed static presentation facts. /// It owns no mesh, gameplay, GUID, cell, or streaming lifetime. Its only /// purpose in Slice F2 is to append exact render-scene deltas at the accepted /// publication and retirement receipts. /// internal sealed class StaticRenderProjectionJournal : IRenderStaticProjectionJournalSink { private const byte StaticEntityDomain = 1; private const byte EnvCellShellDomain = 2; private readonly RenderProjectionJournal _journal; private readonly Dictionary> _byLandblock = []; private readonly Dictionary _trackedById = []; private readonly Dictionary _idByEntity = new(ReferenceEqualityComparer.Instance); private readonly Dictionary> _entitiesByLandblock = []; private readonly List _candidates = []; private readonly List _removed = []; private ulong _nextIncarnation = 1; public StaticRenderProjectionJournal(RenderProjectionJournal journal) { _journal = journal ?? throw new ArgumentNullException(nameof(journal)); } public int ProjectionCount { get; private set; } public int LandblockCount => _byLandblock.Count; public void Reconcile( LandblockBuild build, GpuLandblockSpatialPublication publication) { ArgumentNullException.ThrowIfNull(build); ArgumentNullException.ThrowIfNull(publication); uint landblockId = Canonicalize(publication.LandblockId); if (Canonicalize(build.LandblockId) != landblockId) { throw new ArgumentException( "Static render projection publication belongs to another landblock.", nameof(build)); } if (!publication.RequiresActivation) return; if (publication.RenderTraversalOrder == 0) { throw new InvalidOperationException( $"Landblock 0x{landblockId:X8} has no render traversal order."); } _candidates.Clear(); for (int i = 0; i < publication.Landblock.Entities.Count; i++) { WorldEntity entity = publication.Landblock.Entities[i]; if (entity.ServerGuid == 0) { _candidates.Add(ProjectStaticEntity( landblockId, entity, RenderTraversalSortKey.Compose( publication.RenderTraversalOrder, i))); } } if (build.EnvCells is { } envCells) { for (int i = 0; i < envCells.Shells.Length; i++) _candidates.Add(ProjectEnvCellShell(landblockId, envCells.Shells[i])); } _candidates.Sort(ProjectionRecordComparer.Instance); if (!_byLandblock.TryGetValue( landblockId, out Dictionary? current)) { current = []; _byLandblock.Add(landblockId, current); } _removed.Clear(); foreach (RenderProjectionId id in current.Keys) _removed.Add(id); for (int i = 0; i < _candidates.Count; i++) { RenderProjectionRecord candidate = _candidates[i]; if (current.TryGetValue( candidate.Id, out TrackedProjection? retained) && retained is not null) { _removed.Remove(candidate.Id); RenderProjectionRecord accepted = candidate with { OwnerIncarnation = retained.Record.OwnerIncarnation, PreviousTransform = new PreviousRenderTransform( retained.Record.Transform.LocalToWorld), }; if (accepted.Source.GeometryFingerprint == retained.Record.Source.GeometryFingerprint && accepted.Source.AppearanceFingerprint == retained.Record.Source.AppearanceFingerprint && accepted.EntityPayload.IsBuildingShell == retained.Record.EntityPayload.IsBuildingShell) { accepted = accepted with { EntityPayload = retained.Record.EntityPayload, }; } if (accepted.ProjectionClass != retained.Record.ProjectionClass) { _journal.Unregister( retained.Record.Id, retained.Record.OwnerIncarnation); accepted = accepted with { OwnerIncarnation = NextIncarnation(), }; _journal.Register(in accepted); retained.Record = accepted; continue; } if (accepted != retained.Record) { _journal.AppendDifference(retained.Record, accepted); retained.Record = accepted; } continue; } RenderProjectionRecord registered = candidate with { OwnerIncarnation = NextIncarnation(), }; _journal.Register(in registered); var tracked = new TrackedProjection(registered); current.Add(registered.Id, tracked); _trackedById.Add(registered.Id, tracked); ProjectionCount++; } _removed.Sort(ProjectionIdComparer.Instance); for (int i = 0; i < _removed.Count; i++) { RenderProjectionId id = _removed[i]; TrackedProjection omitted = current[id]; _journal.Unregister(id, omitted.Record.OwnerIncarnation); current.Remove(id); _trackedById.Remove(id); ProjectionCount--; } if (current.Count == 0) _byLandblock.Remove(landblockId); ReplaceStaticEntityMap(landblockId, publication.Landblock.Entities); } public void Retire(GpuLandblockRetirement retirement) { ArgumentNullException.ThrowIfNull(retirement); uint landblockId = Canonicalize(retirement.LandblockId); if (!_byLandblock.Remove( landblockId, out Dictionary? current)) { return; } _removed.Clear(); foreach (RenderProjectionId id in current.Keys) _removed.Add(id); _removed.Sort(ProjectionIdComparer.Instance); for (int i = 0; i < _removed.Count; i++) { TrackedProjection tracked = current[_removed[i]]; _journal.Unregister( tracked.Record.Id, tracked.Record.OwnerIncarnation); } ProjectionCount -= current.Count; foreach (RenderProjectionId id in current.Keys) _trackedById.Remove(id); RemoveStaticEntityMap(landblockId); } public void Clear(RenderSceneGeneration replacementGeneration) { _byLandblock.Clear(); _trackedById.Clear(); _idByEntity.Clear(); _entitiesByLandblock.Clear(); _candidates.Clear(); _removed.Clear(); ProjectionCount = 0; _journal.Clear(replacementGeneration); } public void SynchronizeActiveAnimatedSources( IReadOnlyList activeEntities) { ArgumentNullException.ThrowIfNull(activeEntities); for (int i = 0; i < activeEntities.Count; i++) { WorldEntity entity = activeEntities[i]; if (!_idByEntity.TryGetValue( entity, out RenderProjectionId id) || !_trackedById.TryGetValue( id, out TrackedProjection? tracked)) { continue; } RenderProjectionRecord current = RenderProjectionRecordFactory.ProjectEntity( id, RenderProjectionClass.ActiveAnimatedStatic, tracked.Record.OwnerIncarnation, tracked.Record.Residency.OwnerLandblockId, tracked.Record.Residency.FullCellId, entity, spatiallyVisible: true) with { PreviousTransform = new PreviousRenderTransform( tracked.Record.Transform.LocalToWorld), SortKey = tracked.Record.SortKey, }; if (tracked.Record.ProjectionClass != RenderProjectionClass.ActiveAnimatedStatic) { _journal.Register(in current); } else { _journal.AppendDifference(tracked.Record, current); } tracked.Record = current; } } internal bool TryGet( RenderProjectionId id, out RenderProjectionRecord record) { foreach (Dictionary projections in _byLandblock.Values) { if (projections.TryGetValue( id, out TrackedProjection? tracked) && tracked is not null) { record = tracked.Record; return true; } } record = default; return false; } internal static RenderProjectionId StaticEntityId( uint landblockId, uint localEntityId) => CreateProjectionId( StaticEntityDomain, Canonicalize(landblockId), localEntityId); internal static RenderProjectionId EnvCellShellId( uint landblockId, uint cellId) => CreateProjectionId( EnvCellShellDomain, Canonicalize(landblockId), cellId); private RenderProjectionRecord ProjectStaticEntity( uint landblockId, WorldEntity entity, RenderSortKey sortKey) { RenderProjectionId id = StaticEntityId(landblockId, entity.Id); uint fullCellId = entity.ParentCellId ?? landblockId; return RenderProjectionRecordFactory.ProjectEntity( id, InteriorEntityPartition.IsIndoorCellId(entity.ParentCellId) ? RenderProjectionClass.IndoorCellStatic : RenderProjectionClass.OutdoorStatic, default, landblockId, fullCellId, entity, spatiallyVisible: true) with { SortKey = sortKey, }; } private static RenderProjectionRecord ProjectEnvCellShell( uint landblockId, EnvCellShellPlacement shell) { StableRenderHash128 transformHash = StableRenderHash128.Create(); transformHash.Add(shell.WorldPosition); transformHash.Add(shell.Rotation); transformHash.Add(1.0f); RenderSceneHash128 transformFingerprint = transformHash.Finish(); StableRenderHash128 geometryHash = StableRenderHash128.Create(); geometryHash.Add(shell.GeometryId); geometryHash.Add(shell.EnvironmentId); geometryHash.Add(shell.CellStructure); geometryHash.Add(shell.Surfaces.Length); for (int i = 0; i < shell.Surfaces.Length; i++) geometryHash.Add(shell.Surfaces[i]); RenderSceneHash128 geometryFingerprint = geometryHash.Finish(); RenderProjectionId id = EnvCellShellId(landblockId, shell.CellId); return new RenderProjectionRecord( id, RenderProjectionClass.IndoorCellStatic, default, new RenderTransform( shell.WorldPosition, shell.Rotation, 1.0f, shell.Transform), new PreviousRenderTransform(shell.Transform), new RenderMeshSet( RenderAssetHandle.FromRaw(shell.GeometryId), 1, 0), new RenderMaterialVariant(0, 0, 1.0f), new RenderSpatialResidency( RenderSpatialBucket.FromRaw(shell.CellId), landblockId, shell.CellId), new RenderWorldBounds( shell.WorldBounds.Min, shell.WorldBounds.Max), RenderProjectionFlags.Draw | RenderProjectionFlags.SpatiallyResident, default, id.ToSortKey(), RenderDirtyMask.All, new RenderSourceMetadata( shell.CellId, 0, unchecked((uint)shell.GeometryId), shell.CellId, shell.CellId, 0, transformFingerprint, geometryFingerprint, RenderSceneHash128.Empty)); } private RenderOwnerIncarnation NextIncarnation() { if (_nextIncarnation == ulong.MaxValue) { throw new InvalidOperationException( "Static render-projection incarnation exhausted."); } return RenderOwnerIncarnation.FromRaw(_nextIncarnation++); } private static RenderProjectionId CreateProjectionId( byte domain, uint canonicalLandblockId, uint localId) => RenderProjectionId.FromRaw( ((ulong)domain << 56) | ((ulong)(canonicalLandblockId >> 16) << 32) | localId); private static uint Canonicalize(uint landblockId) => (landblockId & 0xFFFF0000u) | 0xFFFFu; private void ReplaceStaticEntityMap( uint landblockId, IReadOnlyList entities) { RemoveStaticEntityMap(landblockId); var retained = new List(); for (int i = 0; i < entities.Count; i++) { WorldEntity entity = entities[i]; if (entity.ServerGuid != 0) continue; RenderProjectionId id = StaticEntityId(landblockId, entity.Id); if (!_trackedById.ContainsKey(id)) continue; _idByEntity.Add(entity, id); retained.Add(entity); } if (retained.Count > 0) _entitiesByLandblock.Add(landblockId, retained); } private void RemoveStaticEntityMap(uint landblockId) { if (!_entitiesByLandblock.Remove( landblockId, out List? entities)) { return; } for (int i = 0; i < entities.Count; i++) _idByEntity.Remove(entities[i]); } private sealed class TrackedProjection(RenderProjectionRecord record) { public RenderProjectionRecord Record { get; set; } = record; } private sealed class ProjectionRecordComparer : IComparer { public static ProjectionRecordComparer Instance { get; } = new(); public int Compare(RenderProjectionRecord x, RenderProjectionRecord y) => x.Id.CompareTo(y.Id); } private sealed class ProjectionIdComparer : IComparer { public static ProjectionIdComparer Instance { get; } = new(); public int Compare(RenderProjectionId x, RenderProjectionId y) => x.CompareTo(y); } }