using System; using System.Collections.Generic; using LanLib.UpdateManager.Extensions; using LanLib.UpdateManager.Internal; using Unity.Collections; using Unity.Jobs; using UnityEngine; namespace LanLib.UpdateManager.Jobs.Internal { public abstract class AUpdateJobManager : IJobManager, IUpdatable, IDisposable where TData : struct where TDataProvider : IInitialJobDataProvider where TJobData : UpdateJobData, new() { /// /// Event invoked when the update job is scheduled. /// /// /// This is used internally for implementing dependencies between managed jobs. /// public event Action OnJobScheduled; protected bool HavePendingProviderChanges => _dataProvidersToAdd.Count > 0 || _dataProvidersToRemove.Count > 0; protected readonly Dictionary _providerIndexMap = new Dictionary(); protected readonly List _dataProviders = new List(); protected readonly HashSet _dataProvidersToAdd = new HashSet(); protected readonly ReversedSortedList _dataProvidersToRemove = new ReversedSortedList(); protected readonly HashSet> _dataProvidersToSyncEveryFrame = new HashSet>(); protected readonly HashSet> _dataProvidersToSyncOnce = new HashSet>(); protected readonly List> _hashSetFrozenIterator = new List>(); protected readonly TJobData _jobData = new TJobData(); protected JobHandle _jobHandle; protected abstract JobHandle ScheduleJob(JobHandle dependsOn); #if HAVE_BURST protected static readonly bool IsJobBurstCompiled = UpdateJobOptions.GetIsBurstCompiled(); #endif private readonly IJobManager[] _dependencyManagers; private NativeArray _dependencyJobHandles; private int _lastProcessedFrame; private int _dependenciesScheduledCount; private bool _isPendingUpdate; public AUpdateJobManager() { _dependencyManagers = UpdateJobOptions.GetDependsOnManagers(); Application.quitting += Dispose; } ~AUpdateJobManager() { Application.quitting -= Dispose; Dispose(); } public void ManagedUpdate() { _jobHandle.Complete(); SynchronizeJobData(); if (HavePendingProviderChanges) { RefreshProviders(); } if (_dataProviders.Count == 0) { Dispose(); return; } _jobData.BackupData(); _isPendingUpdate = false; ScheduleJobIfDependenciesMet(); } /// /// Register to have its update job scheduled every frame. /// /// /// Registering job provider objects is O(1). /// Registering an object more than once is a no-op. /// public void Register(TDataProvider provider) { Register(provider, false); } /// /// Register to have its update job scheduled every frame /// and optionally synchronize job data every frame. /// /// Job data provider /// /// If true and implements , /// data synchronization will be called every frame. /// /// /// Registering job provider objects is O(1). /// Registering an object more than once is a no-op. /// public void Register(TDataProvider provider, bool syncEveryFrame) { if (syncEveryFrame && provider is IJobDataSynchronizer jobDataSynchronizer) { _dataProvidersToSyncEveryFrame.Add(jobDataSynchronizer); _dataProvidersToSyncOnce.Remove(jobDataSynchronizer); } // Avoid adding provider to pending addition list if it is already registered AND is not pending removal // Providers marked for unregistration should be removed and re-registered to reset their job data if (_providerIndexMap.TryGetValue(provider, out int index) && !_dataProvidersToRemove.Contains(index)) { return; } bool addedToPending = _dataProvidersToAdd.Add(provider); if (addedToPending && _dataProviders.Count == 0 && _dataProvidersToAdd.Count == 1) { StartUpdating(); } } /// /// Unregister , so its update job is not scheduled anymore. /// /// /// Unregistering job provider objects is O(1). /// Unregistering an object that wasn't registered is a no-op. /// public void Unregister(TDataProvider provider) { _dataProvidersToAdd.Remove(provider); if (_providerIndexMap.TryGetValue(provider, out int index)) { _dataProvidersToRemove.Add(index); } UnregisterSynchronization(provider); } /// /// Unregister from job data synchronization. /// /// /// Unregistering job provider objects is O(1). /// Unregistering an object that wasn't registered is a no-op. /// public void UnregisterSynchronization(TDataProvider provider) { if (provider is IJobDataSynchronizer jobDataSynchronizer) { _dataProvidersToSyncEveryFrame.Remove(jobDataSynchronizer); _dataProvidersToSyncOnce.Remove(jobDataSynchronizer); } } /// Check whether is registered or pending registration in manager. public bool IsRegistered(TDataProvider provider) { return _dataProvidersToAdd.Contains(provider) || (_providerIndexMap.TryGetValue(provider, out int index) && !_dataProvidersToRemove.Contains(index)); } /// /// Get a copy of the job data from the last processed frame for . /// /// /// Since update job managers use double buffering for the data, you can access a copy of the last processed data at any time, even while jobs are scheduled and/or processing. ///
/// Calling this with an unregistered provider returns provider.InitialJobData. ///
public TData GetData(TDataProvider provider) { return _providerIndexMap.TryGetValue(provider, out int index) ? _jobData[index] : provider.InitialJobData; } /// /// Schedules to synchronize its own job data only once, in the next processed frame. /// /// /// If is not registered for updates, does not implement /// or is already registered for data synchronization, this method is a no-op. /// public void SynchronizeJobDataOnce(TDataProvider provider) { if (IsRegistered(provider) && provider is IJobDataSynchronizer jobDataSynchronizer && !_dataProvidersToSyncEveryFrame.Contains(jobDataSynchronizer)) { _dataProvidersToSyncOnce.Add(jobDataSynchronizer); } } /// /// Completes current running job, if there is any, remove all job providers and clears up used memory. /// /// /// This is called automatically when all job providers are unregistered or when the manager is finalized. /// You shouldn't need to call this manually at any point. /// public void Dispose() { _jobHandle.Complete(); _jobData.Dispose(); _providerIndexMap.Clear(); _dataProviders.Clear(); _dataProvidersToAdd.Clear(); _dataProvidersToRemove.Clear(); _dataProvidersToSyncEveryFrame.Clear(); _dataProvidersToSyncOnce.Clear(); StopUpdating(); } private void RefreshProviders() { RemovePendingProviders(); int newDataSize = _dataProviders.Count + _dataProvidersToAdd.Count; _jobData.EnsureCapacity(newDataSize); AddPendingProviders(); } private void RemovePendingProviders() { if (_dataProvidersToRemove.Count == 0) { return; } foreach (int indexBeingRemoved in _dataProvidersToRemove) { _providerIndexMap.Remove(_dataProviders[indexBeingRemoved]); _dataProviders.RemoveAtSwapBack(indexBeingRemoved, out TDataProvider swappedProvider); if (swappedProvider != null) { _providerIndexMap[swappedProvider] = indexBeingRemoved; } _jobData.RemoveAtSwapBack(indexBeingRemoved); } _dataProvidersToRemove.Clear(); } private void AddPendingProviders() { if (_dataProvidersToAdd.Count == 0) { return; } foreach (TDataProvider provider in _dataProvidersToAdd) { int index = _dataProviders.Count; _jobData.Add(provider, index); _providerIndexMap[provider] = index; _dataProviders.Add(provider); } _dataProvidersToAdd.Clear(); } private void SynchronizeJobData() { SynchronizeJobData(_dataProvidersToSyncEveryFrame); SynchronizeJobData(_dataProvidersToSyncOnce); _dataProvidersToSyncOnce.Clear(); } private void SynchronizeJobData(HashSet> synchronizers) { if (synchronizers.Count == 0) { return; } _hashSetFrozenIterator.AddRange(synchronizers); foreach (IJobDataSynchronizer jobDataSynchronizer in _hashSetFrozenIterator) { Debug.Assert(jobDataSynchronizer is TDataProvider, "[UpdateJobManager] FIXME: job data synchronizer should be of a job provider type"); if (_providerIndexMap.TryGetValue((TDataProvider) jobDataSynchronizer, out int index)) { try { jobDataSynchronizer.SyncJobData(ref _jobData.DataRef.ItemRefAt(index)); } catch (Exception ex) { Debug.LogException(ex); } } } _hashSetFrozenIterator.Clear(); } private void StartUpdating() { _isPendingUpdate = true; _dependencyJobHandles.DisposeIfCreated(); _dependencyJobHandles = new NativeArray(_dependencyManagers.Length, Allocator.Persistent); for (int i = 0; i < _dependencyManagers.Length; i++) { _dependencyManagers[i].OnJobScheduled += ScheduleJobIfDependenciesMet; } UpdateManager.Instance.Register(this); } private void StopUpdating() { UpdateManager.Instance.Unregister(this); for (int i = 0; i < _dependencyManagers.Length; i++) { _dependencyManagers[i].OnJobScheduled -= ScheduleJobIfDependenciesMet; } _dependencyJobHandles.DisposeIfCreated(); } private void ScheduleJobIfDependenciesMet() { if (!AreAllDependenciesFulfilled()) { return; } _isPendingUpdate = true; _jobHandle = ScheduleJob(JobHandle.CombineDependencies(_dependencyJobHandles)); OnJobScheduled?.Invoke(_jobHandle); } private void ScheduleJobIfDependenciesMet(JobHandle dependecyJobHandle) { MarkDependencyMet(); _dependencyJobHandles[_dependenciesScheduledCount - 1] = dependecyJobHandle; ScheduleJobIfDependenciesMet(); } private bool AreAllDependenciesFulfilled() { return !_isPendingUpdate && _dependenciesScheduledCount >= _dependencyJobHandles.Length; } private void MarkDependencyMet() { int currentFrame = Time.frameCount; if (currentFrame != _lastProcessedFrame) { _dependenciesScheduledCount = 0; _lastProcessedFrame = currentFrame; } _dependenciesScheduledCount++; } } }