diff --git a/Scripts/Runtime/Entities/Lifecycle/Migration.meta b/Scripts/Runtime/Entities/Lifecycle/Migration.meta new file mode 100644 index 00000000..2efc29e8 --- /dev/null +++ b/Scripts/Runtime/Entities/Lifecycle/Migration.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 89e47482399f494b92110e659d053925 +timeCreated: 1681919083 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationExtension.cs b/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationExtension.cs new file mode 100644 index 00000000..b25859a6 --- /dev/null +++ b/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationExtension.cs @@ -0,0 +1,105 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; + +namespace Anvil.Unity.DOTS.Entities +{ + /// + /// Helper class for when Entities are migrating from one to another. + /// + public static class EntityWorldMigrationExtension + { + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + + /// + /// Migrates Entities from this to the destination world with the provided query. + /// This will then handle notifying all s to have the chance to respond with + /// custom migration work. + /// NOTE: Use this instead of in order for migration callbacks + /// to occur in non IComponentData + /// + /// The source 's Entity Manager + /// The to move Entities to. + /// The to select the Entities to migrate. + public static void MoveEntitiesAndMigratableDataTo(this EntityManager srcEntityManager, World destinationWorld, EntityQuery entitiesToMigrateQuery) + { + EntityWorldMigrationSystem entityWorldMigrationSystem = srcEntityManager.World.GetOrCreateSystem(); + entityWorldMigrationSystem.MoveEntitiesAndMigratableDataTo(destinationWorld, entitiesToMigrateQuery); + } + + //************************************************************************************************************* + // BURST RUNTIME CALLS + //************************************************************************************************************* + + /// + /// Checks if the Entity was remapped by Unity during a world transfer. + /// + /// The current entity in this World + /// The remap array Unity provided. + /// The remapped Entity in the new World if it exists. + /// + /// true if this entity was moved to the new world and remaps to a new entity. + /// false if this entity did not move and stayed in this world. + /// + [BurstCompatible] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetRemappedEntity( + this Entity currentEntity, + ref NativeArray remapArray, + out Entity remappedEntity) + { + remappedEntity = EntityRemapUtility.RemapEntity(ref remapArray, currentEntity); + return remappedEntity != Entity.Null; + } + + /// + /// For a given struct and Unity provided remapping array, all references will be + /// remapped to the new entity reference in the new world. + /// Entities that remained in this world will not be remapped. + /// + /// The struct to patch + /// The Unity provided remap array + /// The type of struct to patch + /// + /// Occurs if this type was not registered via + /// + [BurstCompatible] + public static unsafe void PatchEntityReferences(this ref T instance, ref NativeArray remapArray) + where T : struct + { + long typeHash = BurstRuntime.GetHashCode64(); + //Easy way to check if we remembered to register our type. Unfortunately it's a lot harder to figure out which type is missing due to the hash + //but usually you're going to run into this right away and be able to figure it out. Not using the actual Type class so we can Burst this. + if (!EntityWorldMigrationSystem.SharedTypeOffsetInfo.REF.Data.TryGetValue(typeHash, out EntityWorldMigrationSystem.TypeOffsetInfo typeOffsetInfo)) + { + throw new InvalidOperationException($"Tried to patch type with BurstRuntime hash of {typeHash} but it wasn't registered. Did you call {nameof(EntityWorldMigrationSystem.RegisterForEntityPatching)}?"); + } + + //If there's nothing to remap, we'll just return + if (!typeOffsetInfo.CanRemap) + { + return; + } + + //Otherwise we'll get the memory address of the instance and run through all possible entity references + //to remap to the new entity + byte* instancePtr = (byte*)UnsafeUtility.AddressOf(ref instance); + //Beginning of the list + TypeManager.EntityOffsetInfo* entityOffsetInfoPtr = (TypeManager.EntityOffsetInfo*)EntityWorldMigrationSystem.SharedEntityOffsetInfo.REF.Data; + for (int i = typeOffsetInfo.EntityOffsetStartIndex; i < typeOffsetInfo.EntityOffsetEndIndex; ++i) + { + //Index into the list + TypeManager.EntityOffsetInfo* entityOffsetInfo = entityOffsetInfoPtr + i; + //Get offset info from list and offset into the instance memory + Entity* entityPtr = (Entity*)(instancePtr + entityOffsetInfo->Offset); + //Patch + *entityPtr = EntityRemapUtility.RemapEntity(ref remapArray, *entityPtr); + } + } + } +} diff --git a/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationExtension.cs.meta b/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationExtension.cs.meta new file mode 100644 index 00000000..ccefca9b --- /dev/null +++ b/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationExtension.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 534879a926794cac8340e9b357446d08 +timeCreated: 1682019695 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationSystem.cs b/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationSystem.cs new file mode 100644 index 00000000..63540411 --- /dev/null +++ b/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationSystem.cs @@ -0,0 +1,267 @@ +using Anvil.CSharp.Logging; +using System; +using System.Collections.Generic; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using UnityEngine; + +namespace Anvil.Unity.DOTS.Entities +{ + /// + /// World specific system for handling Migration. + /// Register s here to be notified when Migration occurs + /// + /// NOTE: Use on this System instead of directly interfacing with + /// + /// + public class EntityWorldMigrationSystem : AbstractDataSystem + { + private readonly HashSet m_MigrationObservers; + + // ReSharper disable once InconsistentNaming + private NativeList m_Dependencies_ScratchPad; + + public EntityWorldMigrationSystem() + { + m_MigrationObservers = new HashSet(); + m_Dependencies_ScratchPad = new NativeList(8, Allocator.Persistent); + } + + protected override void OnDestroy() + { + m_Dependencies_ScratchPad.Dispose(); + base.OnDestroy(); + } + + /// + /// Adds a to be notified when Migration occurs and be given the chance to + /// respond to it. + /// + /// The + public void RegisterMigrationObserver(IEntityWorldMigrationObserver entityWorldMigrationObserver) + { + m_MigrationObservers.Add(entityWorldMigrationObserver); + m_Dependencies_ScratchPad.ResizeUninitialized(m_MigrationObservers.Count); + } + + /// + /// Removes a if it no longer wishes to be notified of when a Migration occurs. + /// + /// The + public void UnregisterMigrationObserver(IEntityWorldMigrationObserver entityWorldMigrationObserver) + { + //We've already been destroyed, no need to unregister + if (!m_Dependencies_ScratchPad.IsCreated) + { + return; + } + m_MigrationObservers.Remove(entityWorldMigrationObserver); + m_Dependencies_ScratchPad.ResizeUninitialized(m_MigrationObservers.Count); + } + + /// + /// Migrates Entities from this to the destination world with the provided query. + /// This will then handle notifying all s to have the chance to respond with + /// custom migration work. + /// + /// The to move Entities to. + /// The to select the Entities to migrate. + public void MoveEntitiesAndMigratableDataTo(World destinationWorld, EntityQuery entitiesToMigrateQuery) + { + NativeArray remapArray = EntityManager.CreateEntityRemapArray(Allocator.TempJob); + //Do the actual move and get back the remap info + destinationWorld.EntityManager.MoveEntitiesFrom(EntityManager, entitiesToMigrateQuery, remapArray); + + //Let everyone have a chance to do any additional remapping + JobHandle dependsOn = NotifyObserversOfMigrateTo(destinationWorld, ref remapArray); + //Dispose the array based on those remapping jobs being complete + remapArray.Dispose(dependsOn); + //Immediately complete the jobs so migration is complete and the world's state is correct + dependsOn.Complete(); + } + + private JobHandle NotifyObserversOfMigrateTo(World destinationWorld, ref NativeArray remapArray) + { + int index = 0; + foreach (IEntityWorldMigrationObserver migrationObserver in m_MigrationObservers) + { + m_Dependencies_ScratchPad[index] = migrationObserver.MigrateTo(default, destinationWorld, ref remapArray); + index++; + } + return JobHandle.CombineDependencies(m_Dependencies_ScratchPad.AsArray()); + } + + //************************************************************************************************************* + // STATIC REGISTRATION + //************************************************************************************************************* + + private static UnsafeParallelHashMap s_TypeOffsetsLookup = new UnsafeParallelHashMap(256, Allocator.Persistent); + private static NativeList s_EntityOffsetList = new NativeList(32, Allocator.Persistent); + private static NativeList s_BlobAssetRefOffsetList = new NativeList(32, Allocator.Persistent); + private static NativeList s_WeakAssetRefOffsetList = new NativeList(32, Allocator.Persistent); + + private static bool s_AppDomainUnloadRegistered; + + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void Init() + { + //This pattern ensures we can setup and dispose properly the static native collections without Unity + //getting upset about memory leaks + if (s_AppDomainUnloadRegistered) + { + return; + } + AppDomain.CurrentDomain.DomainUnload += CurrentDomain_OnDomainUnload; + s_AppDomainUnloadRegistered = true; + + SharedTypeOffsetInfo.REF.Data = s_TypeOffsetsLookup; + UpdateSharedStatics(); + } + + private static void CurrentDomain_OnDomainUnload(object sender, EventArgs e) + { + SharedTypeOffsetInfo.REF.Data = default; + SharedEntityOffsetInfo.REF.Data = default; + SharedBlobAssetRefInfo.REF.Data = default; + SharedWeakAssetRefInfo.REF.Data = default; + + if (s_TypeOffsetsLookup.IsCreated) + { + s_TypeOffsetsLookup.Dispose(); + } + if (s_EntityOffsetList.IsCreated) + { + s_EntityOffsetList.Dispose(); + } + if (s_BlobAssetRefOffsetList.IsCreated) + { + s_BlobAssetRefOffsetList.Dispose(); + } + if (s_WeakAssetRefOffsetList.IsCreated) + { + s_WeakAssetRefOffsetList.Dispose(); + } + } + + private static unsafe void UpdateSharedStatics() + { + SharedEntityOffsetInfo.REF.Data = new IntPtr(s_EntityOffsetList.GetUnsafePtr()); + SharedBlobAssetRefInfo.REF.Data = new IntPtr(s_BlobAssetRefOffsetList.GetUnsafePtr()); + SharedWeakAssetRefInfo.REF.Data = new IntPtr(s_WeakAssetRefOffsetList.GetUnsafePtr()); + } + + /// + /// Registers the Type that may contain Entity references so that it can be used with + /// to remap Entity references. + /// + /// The type to register + public static void RegisterForEntityPatching() + where T : struct + { + RegisterForEntityPatching(typeof(T)); + } + + /// + /// + /// Occurs when the Type is not a Value type. + /// + public static void RegisterForEntityPatching(Type type) + { + if (!type.IsValueType) + { + throw new InvalidOperationException($"Type {type.GetReadableName()} must be a value type in order to register for Entity Patching."); + } + + long typeHash = BurstRuntime.GetHashCode64(type); + //We've already added this type, no need to do so again + if (s_TypeOffsetsLookup.ContainsKey(typeHash)) + { + return; + } + + int entityOffsetStartIndex = s_EntityOffsetList.Length; + + //We'll allow for a TypeOffset to be registered even if there's nothing to remap so that it's easy to detect + //when you forgot to register a type. We'll ignore the bools that this function returns. + EntityRemapUtility.CalculateFieldOffsetsUnmanaged( + type, + out bool hasEntityRefs, + out bool hasBlobRefs, + out bool hasWeakAssetRefs, + ref s_EntityOffsetList, + ref s_BlobAssetRefOffsetList, + ref s_WeakAssetRefOffsetList); + + + //Unity gives us back Blob Asset Refs and Weak Asset Refs as well but for now we're ignoring them. + //When the time comes to use those and do remapping with them, we'll need to add that info here along + //with the utils to actually do the remapping + s_TypeOffsetsLookup.Add( + typeHash, + new TypeOffsetInfo( + entityOffsetStartIndex, + s_EntityOffsetList.Length)); + + //The size of the underlying data could have changed such that we re-allocated the memory, so we'll update + //our shared statics + UpdateSharedStatics(); + } + + //************************************************************************************************************* + // HELPER TYPES + //************************************************************************************************************* + + internal readonly struct TypeOffsetInfo + { + public readonly int EntityOffsetStartIndex; + public readonly int EntityOffsetEndIndex; + + public bool CanRemap + { + get => EntityOffsetEndIndex > EntityOffsetStartIndex; + } + + public TypeOffsetInfo(int entityOffsetStartIndex, int entityOffsetEndIndex) + { + EntityOffsetStartIndex = entityOffsetStartIndex; + EntityOffsetEndIndex = entityOffsetEndIndex; + } + } + + + //************************************************************************************************************* + // SHARED STATIC REQUIREMENTS + //************************************************************************************************************* + + // ReSharper disable once ConvertToStaticClass + // ReSharper disable once ClassNeverInstantiated.Local + private sealed class MigrationUtilContext + { + private MigrationUtilContext() { } + } + + internal sealed class SharedTypeOffsetInfo + { + public static readonly SharedStatic> REF = SharedStatic>.GetOrCreate(); + } + + internal sealed class SharedEntityOffsetInfo + { + public static readonly SharedStatic REF = SharedStatic.GetOrCreate(); + } + + internal sealed class SharedBlobAssetRefInfo + { + public static readonly SharedStatic REF = SharedStatic.GetOrCreate(); + } + + internal sealed class SharedWeakAssetRefInfo + { + public static readonly SharedStatic REF = SharedStatic.GetOrCreate(); + } + } +} diff --git a/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationSystem.cs.meta b/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationSystem.cs.meta new file mode 100644 index 00000000..dfbe8927 --- /dev/null +++ b/Scripts/Runtime/Entities/Lifecycle/Migration/EntityWorldMigrationSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 102d640f6a5d4b78a55c7c7fb61b73ed +timeCreated: 1681919099 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/Lifecycle/Migration/IEntityWorldMigrationObserver.cs b/Scripts/Runtime/Entities/Lifecycle/Migration/IEntityWorldMigrationObserver.cs new file mode 100644 index 00000000..bd4361cf --- /dev/null +++ b/Scripts/Runtime/Entities/Lifecycle/Migration/IEntityWorldMigrationObserver.cs @@ -0,0 +1,28 @@ +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; + +namespace Anvil.Unity.DOTS.Entities +{ + /// + /// Implement and register with to receive a notification when + /// Entities are being migrated from one to another. This will allow for scheduling jobs to + /// handle any custom migration for data that refers to s but is not automatically handled by + /// Unity. + /// NOTE: The jobs that are scheduled will be completed immediately, but this allows for taking advantage of + /// multiple cores. + /// + public interface IEntityWorldMigrationObserver + { + /// + /// Implement to handle any custom migration work. + /// + /// The to wait on. + /// The the entities are moving to. + /// The remapping array for Entities that were in this world and are moving to + /// the next World. See and + /// for more details if custom usage is needed. + /// The that represents all the custom migration work to do. + public JobHandle MigrateTo(JobHandle dependsOn, World destinationWorld, ref NativeArray remapArray); + } +} diff --git a/Scripts/Runtime/Entities/Lifecycle/Migration/IEntityWorldMigrationObserver.cs.meta b/Scripts/Runtime/Entities/Lifecycle/Migration/IEntityWorldMigrationObserver.cs.meta new file mode 100644 index 00000000..6ca595fc --- /dev/null +++ b/Scripts/Runtime/Entities/Lifecycle/Migration/IEntityWorldMigrationObserver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4f520dd61044412aaca8cd01df7e80d9 +timeCreated: 1682434141 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/PersistentData/Data/EntityPersistentData.cs b/Scripts/Runtime/Entities/PersistentData/Data/EntityPersistentData.cs index c7f57c26..d2bd8c8e 100644 --- a/Scripts/Runtime/Entities/PersistentData/Data/EntityPersistentData.cs +++ b/Scripts/Runtime/Entities/PersistentData/Data/EntityPersistentData.cs @@ -1,5 +1,6 @@ using Anvil.Unity.DOTS.Entities.TaskDriver; using Anvil.Unity.DOTS.Jobs; +using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Entities; @@ -10,12 +11,15 @@ namespace Anvil.Unity.DOTS.Entities internal class EntityPersistentData : AbstractTypedPersistentData>, IDriverEntityPersistentData, ISystemEntityPersistentData, - IWorldEntityPersistentData - where T : struct, IEntityPersistentDataInstance + IWorldEntityPersistentData, + IMigratablePersistentData + where T : unmanaged, IEntityPersistentDataInstance { public EntityPersistentData() : base(new UnsafeParallelHashMap(ChunkUtil.MaxElementsPerChunk(), Allocator.Persistent)) { + //We don't know what will be stored in here, but if there are Entity references we want to be able to patch them + EntityWorldMigrationSystem.RegisterForEntityPatching(); } protected override void DisposeData() @@ -83,5 +87,76 @@ public void ReleaseWriter() { Release(); } + + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + + public JobHandle MigrateTo(JobHandle dependsOn, IMigratablePersistentData destinationPersistentData, ref NativeArray remapArray) + { + EntityPersistentData destinationEntityPersistentData = (EntityPersistentData)destinationPersistentData; + + //Launch the migration job to get that burst speed + dependsOn = JobHandle.CombineDependencies(dependsOn, + AcquireWriterAsync(out EntityPersistentDataWriter currentData), + destinationEntityPersistentData.AcquireWriterAsync(out EntityPersistentDataWriter destinationData)); + + MigrateJob migrateJob = new MigrateJob( + currentData, + destinationData, + ref remapArray); + dependsOn = migrateJob.Schedule(dependsOn); + + destinationEntityPersistentData.ReleaseWriterAsync(dependsOn); + ReleaseWriterAsync(dependsOn); + + return dependsOn; + } + + [BurstCompile] + private struct MigrateJob : IJob + { + private EntityPersistentDataWriter m_CurrentData; + private EntityPersistentDataWriter m_DestinationData; + [ReadOnly] private NativeArray m_RemapArray; + + public MigrateJob( + EntityPersistentDataWriter currentData, + EntityPersistentDataWriter destinationData, + ref NativeArray remapArray) + { + m_CurrentData = currentData; + m_DestinationData = destinationData; + m_RemapArray = remapArray; + } + + public void Execute() + { + //TODO: Optimization: Could pass through the array of entities that were moving to avoid the copy. See: https://github.com/decline-cookies/anvil-unity-dots/pull/232#discussion_r1181697951 + + //Can't remove while iterating so we collapse to an array first of our current keys/values + NativeKeyValueArrays currentEntries = m_CurrentData.GetKeyValueArrays(Allocator.Temp); + + for (int i = 0; i < currentEntries.Length; ++i) + { + Entity currentEntity = currentEntries.Keys[i]; + //If we don't exist in the new world we can just skip, we stayed in this world + if (!currentEntity.TryGetRemappedEntity(ref m_RemapArray, out Entity remappedEntity)) + { + continue; + } + + //Otherwise, remove us from this world's lookup + m_CurrentData.Remove(currentEntity); + + //Get our data and patch it + T currentValue = currentEntries.Values[i]; + currentValue.PatchEntityReferences(ref m_RemapArray); + + //Then write the newly remapped data to the new world's lookup + m_DestinationData[remappedEntity] = currentValue; + } + } + } } } diff --git a/Scripts/Runtime/Entities/PersistentData/Data/Interface/IMigratablePersistentData.cs b/Scripts/Runtime/Entities/PersistentData/Data/Interface/IMigratablePersistentData.cs new file mode 100644 index 00000000..ff125f04 --- /dev/null +++ b/Scripts/Runtime/Entities/PersistentData/Data/Interface/IMigratablePersistentData.cs @@ -0,0 +1,12 @@ +using System; +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; + +namespace Anvil.Unity.DOTS.Entities +{ + internal interface IMigratablePersistentData : IDisposable + { + public JobHandle MigrateTo(JobHandle dependsOn, IMigratablePersistentData destinationPersistentData, ref NativeArray remapArray); + } +} diff --git a/Scripts/Runtime/Entities/PersistentData/Data/Interface/IMigratablePersistentData.cs.meta b/Scripts/Runtime/Entities/PersistentData/Data/Interface/IMigratablePersistentData.cs.meta new file mode 100644 index 00000000..b8081593 --- /dev/null +++ b/Scripts/Runtime/Entities/PersistentData/Data/Interface/IMigratablePersistentData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 935dd787a0bf43a79fa8853abe712ebe +timeCreated: 1682969502 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/PersistentData/PersistentDataSystem.cs b/Scripts/Runtime/Entities/PersistentData/PersistentDataSystem.cs index 93e42cb7..1bf9e0ed 100644 --- a/Scripts/Runtime/Entities/PersistentData/PersistentDataSystem.cs +++ b/Scripts/Runtime/Entities/PersistentData/PersistentDataSystem.cs @@ -1,30 +1,54 @@ using Anvil.CSharp.Collections; +using Anvil.CSharp.Logging; using System; using System.Collections.Generic; +using System.Diagnostics; +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; namespace Anvil.Unity.DOTS.Entities { - internal partial class PersistentDataSystem : AbstractDataSystem + internal partial class PersistentDataSystem : AbstractDataSystem, + IEntityWorldMigrationObserver { + private const string WORLD_PATH = "World"; private static readonly Dictionary s_ThreadPersistentData = new Dictionary(); private static int s_InstanceCount; private readonly Dictionary m_EntityPersistentData; - + + private readonly Dictionary m_MigrationPersistentDataLookup; + // ReSharper disable once InconsistentNaming + private NativeList m_MigrationDependencies_ScratchPad; + private EntityWorldMigrationSystem m_EntityWorldMigrationSystem; + public PersistentDataSystem() { s_InstanceCount++; m_EntityPersistentData = new Dictionary(); + m_MigrationDependencies_ScratchPad = new NativeList(8, Allocator.Persistent); + m_MigrationPersistentDataLookup = new Dictionary(); + } + + protected override void OnCreate() + { + base.OnCreate(); + m_EntityWorldMigrationSystem = World.GetOrCreateSystem(); + m_EntityWorldMigrationSystem.RegisterMigrationObserver(this); } protected override void OnDestroy() { + m_MigrationDependencies_ScratchPad.Dispose(); m_EntityPersistentData.DisposeAllValuesAndClear(); s_InstanceCount--; if (s_InstanceCount <= 0) { s_ThreadPersistentData.DisposeAllValuesAndClear(); } + + m_EntityWorldMigrationSystem.UnregisterMigrationObserver(this); base.OnDestroy(); } @@ -48,9 +72,63 @@ public EntityPersistentData GetOrCreateEntityPersistentData() { persistentData = new EntityPersistentData(); m_EntityPersistentData.Add(type, persistentData); + AddToMigrationLookup(WORLD_PATH, (EntityPersistentData)persistentData); } return (EntityPersistentData)persistentData; } + + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + + JobHandle IEntityWorldMigrationObserver.MigrateTo(JobHandle dependsOn, World destinationWorld, ref NativeArray remapArray) + { + PersistentDataSystem destinationPersistentDataSystem = destinationWorld.GetOrCreateSystem(); + Debug_EnsureOtherWorldPersistentDataSystemExists(destinationWorld, destinationPersistentDataSystem); + + int index = 0; + foreach (KeyValuePair entry in m_MigrationPersistentDataLookup) + { + if (!destinationPersistentDataSystem.m_MigrationPersistentDataLookup.TryGetValue(entry.Key, out IMigratablePersistentData destinationPersistentData)) + { + throw new InvalidOperationException($"Current World {World} has Entity Persistent Data of {entry.Key} but it doesn't exist in the destination world {destinationWorld}!"); + } + m_MigrationDependencies_ScratchPad[index] = entry.Value.MigrateTo(dependsOn, destinationPersistentData, ref remapArray); + index++; + } + + return JobHandle.CombineDependencies(m_MigrationDependencies_ScratchPad.AsArray()); + } + + public void AddToMigrationLookup(string parentPath, IMigratablePersistentData entityPersistentData) + { + string path = $"{parentPath}-{entityPersistentData.GetType().GetReadableName()}"; + Debug_EnsureNoDuplicateMigrationData(path); + m_MigrationPersistentDataLookup.Add(path, entityPersistentData); + m_MigrationDependencies_ScratchPad.ResizeUninitialized(m_MigrationPersistentDataLookup.Count); + } + + //************************************************************************************************************* + // SAFETY + //************************************************************************************************************* + + [Conditional("ANVIL_DEBUG_SAFETY")] + private void Debug_EnsureOtherWorldPersistentDataSystemExists(World destinationWorld, PersistentDataSystem persistentDataSystem) + { + if (persistentDataSystem == null) + { + throw new InvalidOperationException($"Expected World {destinationWorld} to have a {nameof(PersistentDataSystem)} but it does not!"); + } + } + + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + private void Debug_EnsureNoDuplicateMigrationData(string path) + { + if (m_MigrationPersistentDataLookup.ContainsKey(path)) + { + throw new InvalidOperationException($"Trying to add Entity Persistent Data migration data for {this} but {path} is already in the lookup!"); + } + } } } \ No newline at end of file diff --git a/Scripts/Runtime/Entities/TaskDriver/AbstractTaskDriver.cs b/Scripts/Runtime/Entities/TaskDriver/AbstractTaskDriver.cs index 220bc705..9d1b5595 100644 --- a/Scripts/Runtime/Entities/TaskDriver/AbstractTaskDriver.cs +++ b/Scripts/Runtime/Entities/TaskDriver/AbstractTaskDriver.cs @@ -26,9 +26,11 @@ public abstract class AbstractTaskDriver : AbstractAnvilBase, ITaskSetOwner private static readonly Type TASK_DRIVER_SYSTEM_TYPE = typeof(TaskDriverSystem<>); private static readonly Type COMPONENT_SYSTEM_GROUP_TYPE = typeof(ComponentSystemGroup); + private readonly PersistentDataSystem m_PersistentDataSystem; private readonly List m_SubTaskDrivers; private readonly uint m_ID; + private readonly string m_UniqueMigrationSuffix; private bool m_IsHardened; private bool m_HasCancellableData; @@ -39,9 +41,9 @@ public abstract class AbstractTaskDriver : AbstractAnvilBase, ITaskSetOwner public World World { get; } internal AbstractTaskDriver Parent { get; private set; } - - internal AbstractTaskDriverSystem System { get; } - + + internal AbstractTaskDriverSystem TaskDriverSystem { get; } + internal TaskSet TaskSet { get; } /// @@ -62,7 +64,7 @@ public IDriverDataStream CancelCompleteDataStream AbstractTaskDriverSystem ITaskSetOwner.TaskDriverSystem { - get => System; + get => TaskDriverSystem; } TaskSet ITaskSetOwner.TaskSet @@ -88,14 +90,30 @@ bool ITaskSetOwner.HasCancellableData return m_HasCancellableData; } } - - protected ITaskDriverSystem TaskDriverSystem + + protected ITaskDriverSystem System { - get => new ContextTaskDriverSystemWrapper(System, this); + get => new ContextTaskDriverSystemWrapper(TaskDriverSystem, this); } - protected AbstractTaskDriver(World world) + /// + /// Creates a new instance of a + /// + /// The this Task Driver is a part of. + /// + /// An optional unique suffix to identify this TaskDriver by. This is necessary when there are two or more of the + /// same type of TaskDrivers at the same level in the hierarchy. + /// Ex. + /// ShootTaskDriver + /// - TimerTaskDriver (for time between shots) + /// - TimerTaskDriver (for reloading) + /// + /// Both TimerTaskDriver's would conflict as being siblings of the ShootTaskDriver so they would need a unique + /// migration suffix to distinguish them for ensuring migration happens properly between worlds. + /// + protected AbstractTaskDriver(World world, string uniqueMigrationSuffix = null) { + m_UniqueMigrationSuffix = uniqueMigrationSuffix ?? string.Empty; World = world; TaskDriverManagementSystem taskDriverManagementSystem = World.GetOrCreateSystem(); m_PersistentDataSystem = World.GetOrCreateSystem(); @@ -107,17 +125,17 @@ protected AbstractTaskDriver(World world) Type taskDriverSystemType = TASK_DRIVER_SYSTEM_TYPE.MakeGenericType(taskDriverType); //If this is the first TaskDriver of this type, then the System will have been created for this World. - System = (AbstractTaskDriverSystem)World.GetExistingSystem(taskDriverSystemType); + TaskDriverSystem = (AbstractTaskDriverSystem)World.GetExistingSystem(taskDriverSystemType); //If not, then we will want to explicitly create it and ensure it is part of the lifecycle. - if (System == null) + if (TaskDriverSystem == null) { - System = (AbstractTaskDriverSystem)Activator.CreateInstance(taskDriverSystemType, World); - World.AddSystem(System); + TaskDriverSystem = (AbstractTaskDriverSystem)Activator.CreateInstance(taskDriverSystemType, World); + World.AddSystem(TaskDriverSystem); ComponentSystemGroup systemGroup = GetSystemGroup(); - systemGroup.AddSystemToUpdateList(System); + systemGroup.AddSystemToUpdateList(TaskDriverSystem); } - System.RegisterTaskDriver(this); + TaskDriverSystem.RegisterTaskDriver(this); m_ID = taskDriverManagementSystem.GetNextID(); taskDriverManagementSystem.RegisterTaskDriver(this); @@ -135,7 +153,7 @@ protected override void DisposeSelf() public override string ToString() { - return $"{GetType().GetReadableName()}|{m_ID}"; + return $"{GetType().GetReadableName()}|{m_ID}|{m_UniqueMigrationSuffix}"; } private ComponentSystemGroup GetSystemGroup() @@ -169,7 +187,7 @@ protected TTaskDriver AddSubTaskDriver(TTaskDriver subTaskDriver) return subTaskDriver; } - + protected IDriverDataStream CreateDataStream(CancelRequestBehaviour cancelRequestBehaviour = CancelRequestBehaviour.Delete) where TInstance : unmanaged, IEntityProxyInstance { @@ -261,14 +279,14 @@ internal void Harden() } //Harden our TaskDriverSystem if it hasn't been already - System.Harden(); + TaskDriverSystem.Harden(); //Harden our own TaskSet TaskSet.Harden(); //TODO: #138 - Can we consolidate this into the TaskSet and have TaskSets aware of parenting instead m_HasCancellableData = TaskSet.ExplicitCancellationCount > 0 - || System.HasCancellableData + || TaskDriverSystem.HasCancellableData || m_SubTaskDrivers.Any(subtaskDriver => subtaskDriver.m_HasCancellableData); } @@ -282,10 +300,53 @@ void ITaskSetOwner.AddResolvableDataStreamsTo(Type type, List migrationTaskSetOwnerIDLookup, + Dictionary migrationActiveIDLookup, + PersistentDataSystem persistentDataSystem) + { + //Construct the unique path for this TaskDriver. By default, out unique migration suffix is empty but if we + //conflict with another, then we'll need to get the user to provide one. + string typeName = GetType().GetReadableName(); + string path = $"{parentPath}{typeName}{m_UniqueMigrationSuffix}-"; + Debug_EnsureNoDuplicateMigrationData(path, migrationTaskSetOwnerIDLookup); + migrationTaskSetOwnerIDLookup.Add(path, m_ID); + + //Get our TaskSet to populate all the possible ActiveIDs + TaskSet.AddToMigrationLookup(path, migrationActiveIDLookup, persistentDataSystem); + + //Try and do the same for our system (there can only be one), will gracefully fail if we have already done this + string systemPath = $"{typeName}-System"; + if (migrationTaskSetOwnerIDLookup.TryAdd(systemPath, TaskDriverSystem.ID)) + { + TaskDriverSystem.TaskSet.AddToMigrationLookup(systemPath, migrationActiveIDLookup, persistentDataSystem); + } + + //Then recurse downward to catch all the sub task drivers + foreach (AbstractTaskDriver subTaskDriver in m_SubTaskDrivers) + { + subTaskDriver.AddToMigrationLookup(path, migrationTaskSetOwnerIDLookup, migrationActiveIDLookup, persistentDataSystem); + } + } + //************************************************************************************************************* // SAFETY //************************************************************************************************************* + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + private void Debug_EnsureNoDuplicateMigrationData(string path, Dictionary migrationTaskSetOwnerIDLookup) + { + if (migrationTaskSetOwnerIDLookup.ContainsKey(path)) + { + throw new InvalidOperationException($"TaskDriver {this} at path {path} already exists. There are two or more of the same task driver at the same level. They will require a unique migration suffix to be set in their constructor."); + } + } + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] private void Debug_EnsureNotHardened() { diff --git a/Scripts/Runtime/Entities/TaskDriver/Migration.meta b/Scripts/Runtime/Entities/TaskDriver/Migration.meta new file mode 100644 index 00000000..9e7c6692 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/Migration.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cf2e30c6b6344c54b8386cf1e1394b10 +timeCreated: 1682534640 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/TaskDriver/Migration/DestinationWorldDataMap.cs b/Scripts/Runtime/Entities/TaskDriver/Migration/DestinationWorldDataMap.cs new file mode 100644 index 00000000..913cbed8 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/Migration/DestinationWorldDataMap.cs @@ -0,0 +1,49 @@ +using Anvil.CSharp.Core; +using System.Collections.Generic; +using Unity.Collections; + +namespace Anvil.Unity.DOTS.Entities.TaskDriver +{ + internal class DestinationWorldDataMap : AbstractAnvilBase + { + public NativeParallelHashMap TaskSetOwnerIDMapping; + public NativeParallelHashMap ActiveIDMapping; + + public DestinationWorldDataMap( + Dictionary srcTaskSetOwnerMapping, + Dictionary dstTaskSetOwnerMapping, + Dictionary srcActiveIDMapping, + Dictionary dstActiveIDMapping) + { + //TODO: Optimization - Could instead pass in the list of entities that moved and use that as the upper limit. In most cases it will be less than the dstMapping counts unless its a full world move. + //We're going to the Destination World so we can't have more than they have + TaskSetOwnerIDMapping = new NativeParallelHashMap(dstTaskSetOwnerMapping.Count, Allocator.Persistent); + ActiveIDMapping = new NativeParallelHashMap(dstActiveIDMapping.Count, Allocator.Persistent); + + foreach (KeyValuePair entry in srcTaskSetOwnerMapping) + { + if (!dstTaskSetOwnerMapping.TryGetValue(entry.Key, out uint dstTaskSetOwnerID)) + { + continue; + } + TaskSetOwnerIDMapping.Add(entry.Value, dstTaskSetOwnerID); + } + + foreach (KeyValuePair entry in srcActiveIDMapping) + { + if (!dstActiveIDMapping.TryGetValue(entry.Key, out uint dstActiveID)) + { + continue; + } + ActiveIDMapping.Add(entry.Value, dstActiveID); + } + } + + protected override void DisposeSelf() + { + TaskSetOwnerIDMapping.Dispose(); + ActiveIDMapping.Dispose(); + base.DisposeSelf(); + } + } +} diff --git a/Scripts/Runtime/Entities/TaskDriver/Migration/DestinationWorldDataMap.cs.meta b/Scripts/Runtime/Entities/TaskDriver/Migration/DestinationWorldDataMap.cs.meta new file mode 100644 index 00000000..90f88469 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/Migration/DestinationWorldDataMap.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e7bb882acc0f4d849d0183dbc35f7896 +timeCreated: 1682534677 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/TaskDriver/Migration/TaskDriverMigrationData.cs b/Scripts/Runtime/Entities/TaskDriver/Migration/TaskDriverMigrationData.cs new file mode 100644 index 00000000..cb8de93b --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/Migration/TaskDriverMigrationData.cs @@ -0,0 +1,103 @@ +using Anvil.CSharp.Collections; +using Anvil.CSharp.Core; +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; + +namespace Anvil.Unity.DOTS.Entities.TaskDriver +{ + internal class TaskDriverMigrationData : AbstractAnvilBase + { + private readonly Dictionary m_MigrationTaskSetOwnerIDLookup; + private readonly Dictionary m_MigrationActiveIDLookup; + + private readonly Dictionary m_DestinationWorldDataMaps; + private readonly Dictionary m_AllDataSources; + // ReSharper disable once InconsistentNaming + private NativeList m_MigrationDependencies_ScratchPad; + + public int TaskSetOwnerCount + { + get => m_MigrationTaskSetOwnerIDLookup.Count; + } + + public int ActiveIDCount + { + get => m_MigrationActiveIDLookup.Count; + } + + public TaskDriverMigrationData() + { + m_MigrationTaskSetOwnerIDLookup = new Dictionary(); + m_MigrationActiveIDLookup = new Dictionary(); + m_DestinationWorldDataMaps = new Dictionary(); + m_AllDataSources = new Dictionary(); + m_MigrationDependencies_ScratchPad = new NativeList(32, Allocator.Persistent); + } + + protected override void DisposeSelf() + { + m_DestinationWorldDataMaps.DisposeAllValuesAndClear(); + m_MigrationDependencies_ScratchPad.Dispose(); + base.DisposeSelf(); + } + + public void AddDataSource(T dataSource) + where T : class, IDataSource + { + Type type = dataSource.GetType(); + m_AllDataSources.Add(type, dataSource); + m_MigrationDependencies_ScratchPad.ResizeUninitialized(m_AllDataSources.Count); + } + + public void PopulateMigrationLookup(World world, List topLevelTaskDrivers) + { + //Generate a World ID + foreach (AbstractTaskDriver topLevelTaskDriver in topLevelTaskDrivers) + { + topLevelTaskDriver.AddToMigrationLookup( + string.Empty, + m_MigrationTaskSetOwnerIDLookup, + m_MigrationActiveIDLookup, + world.GetOrCreateSystem()); + } + } + + private DestinationWorldDataMap GetOrCreateDestinationWorldDataMapFor(World destinationWorld, TaskDriverMigrationData destinationMigrationData) + { + //TODO: Optimization: Might be able to jobify if we switch to fixed strings and UnsafeWorlds? + if (!m_DestinationWorldDataMaps.TryGetValue(destinationWorld, out DestinationWorldDataMap destinationWorldDataMap)) + { + destinationWorldDataMap = new DestinationWorldDataMap(m_MigrationTaskSetOwnerIDLookup, + destinationMigrationData.m_MigrationTaskSetOwnerIDLookup, + m_MigrationActiveIDLookup, + destinationMigrationData.m_MigrationActiveIDLookup); + + m_DestinationWorldDataMaps.Add(destinationWorld, destinationWorldDataMap); + } + return destinationWorldDataMap; + } + + public JobHandle MigrateTo(JobHandle dependsOn, World destinationWorld, TaskDriverMigrationData destinationTaskDriverMigrationData, ref NativeArray remapArray) + { + //Lazy create a World to World mapping lookup for ActiveIDs and TaskSetOwnerIDs + DestinationWorldDataMap destinationWorldDataMap = GetOrCreateDestinationWorldDataMapFor(destinationWorld, destinationTaskDriverMigrationData); + + Dictionary destinationDataSourcesByType = destinationTaskDriverMigrationData.m_AllDataSources; + + int index = 0; + foreach (KeyValuePair entry in m_AllDataSources) + { + //We may not have a corresponding destination Data Source in the destination world but we still want to process the migration so that + //we remove any references in this world. If we do have the corresponding data source, we'll transfer over to the other world. + destinationDataSourcesByType.TryGetValue(entry.Key, out IDataSource destinationDataSource); + m_MigrationDependencies_ScratchPad[index] = entry.Value.MigrateTo(dependsOn, destinationDataSource, ref remapArray, destinationWorldDataMap); + index++; + } + + return JobHandle.CombineDependencies(m_MigrationDependencies_ScratchPad); + } + } +} diff --git a/Scripts/Runtime/Entities/TaskDriver/Migration/TaskDriverMigrationData.cs.meta b/Scripts/Runtime/Entities/TaskDriver/Migration/TaskDriverMigrationData.cs.meta new file mode 100644 index 00000000..db59de09 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/Migration/TaskDriverMigrationData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e58230bcc0444fd8811d61fa44165671 +timeCreated: 1682534957 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/TaskDriver/System/AbstractTaskDriverSystem.cs b/Scripts/Runtime/Entities/TaskDriver/System/AbstractTaskDriverSystem.cs index ae20290e..e0b4e896 100644 --- a/Scripts/Runtime/Entities/TaskDriver/System/AbstractTaskDriverSystem.cs +++ b/Scripts/Runtime/Entities/TaskDriver/System/AbstractTaskDriverSystem.cs @@ -13,35 +13,36 @@ public abstract partial class AbstractTaskDriverSystem : AbstractAnvilSystemBase private static readonly NoOpJobConfig NO_OP_JOB_CONFIG = new NoOpJobConfig(); private static readonly List EMPTY_SUB_TASK_DRIVERS = new List(); - private readonly uint m_ID; private readonly List m_TaskDrivers; private BulkJobScheduler m_BulkJobScheduler; private bool m_IsHardened; private bool m_IsUpdatePhaseHardened; private bool m_HasCancellableData; - + //Note - This represents the World that was passed in by the TaskDriver during this system's construction. //Normally a system doesn't get a World until OnCreate is called and the System.World will return null. //We need a valid World in the constructor so we get one and assign it to this property instead. public new World World { get; } internal TaskSet TaskSet { get; } - + + internal uint ID { get; } + uint ITaskSetOwner.ID { - get => m_ID; + get => ID; } - + internal ISystemCancelRequestDataStream CancelRequestDataStream { get => TaskSet.CancelRequestsDataStream; } - + internal ISystemDataStream CancelCompleteDataStream { get => TaskSet.CancelCompleteDataStream; } - + internal bool HasCancellableData { get @@ -65,7 +66,7 @@ TaskSet ITaskSetOwner.TaskSet { get => TaskSet; } - + List ITaskSetOwner.SubTaskDrivers { get => EMPTY_SUB_TASK_DRIVERS; @@ -79,7 +80,7 @@ protected AbstractTaskDriverSystem(World world) m_TaskDrivers = new List(); - m_ID = taskDriverManagementSystem.GetNextID(); + ID = taskDriverManagementSystem.GetNextID(); TaskSet = new TaskSet(this); } @@ -104,7 +105,7 @@ protected override void OnDestroy() public override string ToString() { - return $"{GetType().GetReadableName()}|{m_ID}"; + return $"{GetType().GetReadableName()}|{ID}"; } internal void RegisterTaskDriver(AbstractTaskDriver taskDriver) @@ -120,7 +121,7 @@ internal ISystemDataStream CreateDataStream(AbstractTaskDr //Create a proxy DataStream that references the same data owned by the system but gives it the TaskDriver context return new EntityProxyDataStream(taskDriver, dataStream); } - + internal ISystemEntityPersistentData CreateEntityPersistentData() where T : unmanaged, IEntityPersistentDataInstance { diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskDriverManagementSystem.cs b/Scripts/Runtime/Entities/TaskDriver/TaskDriverManagementSystem.cs index eb4ce369..4addc767 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskDriverManagementSystem.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskDriverManagementSystem.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Unity.Collections; using Unity.Entities; using Unity.Jobs; @@ -13,7 +14,8 @@ namespace Anvil.Unity.DOTS.Entities.TaskDriver //TODO: #108 - Custom Profiling - https://github.com/decline-cookies/anvil-unity-dots/pull/111 //TODO: #86 - Revisit with Entities 1.0 for "Create Before/After" [UpdateInGroup(typeof(InitializationSystemGroup), OrderFirst = true)] - internal partial class TaskDriverManagementSystem : AbstractAnvilSystemBase + internal partial class TaskDriverManagementSystem : AbstractAnvilSystemBase, + IEntityWorldMigrationObserver { private readonly Dictionary m_EntityProxyDataSourcesByType; private readonly HashSet m_AllTaskDrivers; @@ -24,7 +26,9 @@ internal partial class TaskDriverManagementSystem : AbstractAnvilSystemBase private readonly CancelCompleteDataSource m_CancelCompleteDataSource; private readonly List m_CancelProgressFlows; private readonly Dictionary m_UnityEntityDataAccessControllers; - + private readonly TaskDriverMigrationData m_TaskDriverMigrationData; + + private bool m_IsInitialized; private bool m_IsHardened; private BulkJobScheduler m_EntityProxyDataSourceBulkJobScheduler; @@ -44,6 +48,20 @@ public TaskDriverManagementSystem() m_CancelCompleteDataSource = new CancelCompleteDataSource(this); m_CancelProgressFlows = new List(); m_UnityEntityDataAccessControllers = new Dictionary(); + + m_TaskDriverMigrationData = new TaskDriverMigrationData(); + m_TaskDriverMigrationData.AddDataSource(m_CancelRequestsDataSource); + m_TaskDriverMigrationData.AddDataSource(m_CancelProgressDataSource); + m_TaskDriverMigrationData.AddDataSource(m_CancelCompleteDataSource); + + EntityProxyInstanceID.Debug_EnsureOffsetsAreCorrect(); + } + + protected override void OnCreate() + { + base.OnCreate(); + EntityWorldMigrationSystem entityWorldMigrationSystem = World.GetOrCreateSystem(); + entityWorldMigrationSystem.RegisterMigrationObserver(this); } protected override void OnStartRunning() @@ -67,13 +85,14 @@ protected sealed override void OnDestroy() m_CancelProgressFlowBulkJobScheduler?.Dispose(); m_CancelProgressFlows.DisposeAllAndTryClear(); m_UnityEntityDataAccessControllers.DisposeAllValuesAndClear(); + m_TaskDriverMigrationData.Dispose(); m_CancelRequestsDataSource.Dispose(); m_CancelCompleteDataSource.Dispose(); m_CancelProgressDataSource.Dispose(); m_IDProvider.Dispose(); - + base.OnDestroy(); } @@ -121,6 +140,9 @@ private void Harden() .Select((topLevelTaskDriver) => new CancelProgressFlow(topLevelTaskDriver))); m_CancelProgressFlowBulkJobScheduler = new BulkJobScheduler(m_CancelProgressFlows.ToArray()); + + //Build the Migration Data for this world + m_TaskDriverMigrationData.PopulateMigrationLookup(World, m_TopLevelTaskDrivers); } public EntityProxyDataSource GetOrCreateEntityProxyDataSource() @@ -131,6 +153,7 @@ public EntityProxyDataSource GetOrCreateEntityProxyDataSource(this); m_EntityProxyDataSourcesByType.Add(type, dataSource); + m_TaskDriverMigrationData.AddDataSource(dataSource); } return (EntityProxyDataSource)dataSource; @@ -155,7 +178,7 @@ public void RegisterTaskDriver(AbstractTaskDriver taskDriver) { Debug_EnsureNotHardened(); m_AllTaskDrivers.Add(taskDriver); - m_AllTaskDriverSystems.Add(taskDriver.System); + m_AllTaskDriverSystems.Add(taskDriver.TaskDriverSystem); } public AccessController GetOrCreateCDFEAccessController() @@ -208,13 +231,24 @@ protected sealed override void OnUpdate() Dependency = dependsOn; } + + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + JobHandle IEntityWorldMigrationObserver.MigrateTo(JobHandle dependsOn, World destinationWorld, ref NativeArray remapArray) + { + TaskDriverManagementSystem destinationTaskDriverManagementSystem = destinationWorld.GetOrCreateSystem(); + Debug_EnsureOtherWorldTaskDriverManagementSystemExists(destinationWorld, destinationTaskDriverManagementSystem); + + return m_TaskDriverMigrationData.MigrateTo(dependsOn, destinationWorld, destinationTaskDriverManagementSystem.m_TaskDriverMigrationData, ref remapArray); + } //************************************************************************************************************* // SAFETY //************************************************************************************************************* - [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + [Conditional("ANVIL_DEBUG_SAFETY")] private void Debug_EnsureNotHardened() { if (m_IsHardened) @@ -222,5 +256,14 @@ private void Debug_EnsureNotHardened() throw new InvalidOperationException($"Expected {this} to not yet be Hardened but {nameof(Harden)} has already been called!"); } } + + [Conditional("ANVIL_DEBUG_SAFETY")] + private void Debug_EnsureOtherWorldTaskDriverManagementSystemExists(World destinationWorld, TaskDriverManagementSystem taskDriverManagementSystem) + { + if (taskDriverManagementSystem == null) + { + throw new InvalidOperationException($"Expected World {destinationWorld} to have a {nameof(TaskDriverManagementSystem)} but it does not!"); + } + } } } diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/Job/JobConfig/AbstractJobConfig.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/Job/JobConfig/AbstractJobConfig.cs index 78a57f78..0b3df370 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/Job/JobConfig/AbstractJobConfig.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/Job/JobConfig/AbstractJobConfig.cs @@ -41,7 +41,7 @@ internal static readonly BulkScheduleDelegate PREPARE_AND_SCH BindingFlags.Instance | BindingFlags.NonPublic); private static readonly Usage[] USAGE_TYPES = (Usage[])Enum.GetValues(typeof(Usage)); - + private readonly Dictionary m_AccessWrappers; private readonly List m_SchedulingAccessWrappers; private readonly PersistentDataSystem m_PersistentDataSystem; diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractDataSource.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractDataSource.cs index 32e13c2c..56509b7d 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractDataSource.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractDataSource.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using Unity.Collections; +using Unity.Entities; using Unity.Jobs; namespace Anvil.Unity.DOTS.Entities.TaskDriver @@ -132,6 +133,16 @@ protected void AddConsolidationData(AbstractData data, AccessType accessType) { m_ConsolidationData.Add(new DataAccessWrapper(data, accessType)); } + + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + + public abstract JobHandle MigrateTo( + JobHandle dependsOn, + IDataSource destinationDataSource, + ref NativeArray remapArray, + DestinationWorldDataMap destinationWorldDataMap); //************************************************************************************************************* // EXECUTION diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractEntityProxyInstanceIDDataSource.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractEntityProxyInstanceIDDataSource.cs new file mode 100644 index 00000000..75dfcfb3 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractEntityProxyInstanceIDDataSource.cs @@ -0,0 +1,142 @@ +using Anvil.Unity.DOTS.Data; +using Anvil.Unity.DOTS.Jobs; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; + +namespace Anvil.Unity.DOTS.Entities.TaskDriver +{ + internal abstract class AbstractEntityProxyInstanceIDDataSource : AbstractDataSource + { + protected AbstractEntityProxyInstanceIDDataSource(TaskDriverManagementSystem taskDriverManagementSystem) : base(taskDriverManagementSystem) + { + } + + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + + public override JobHandle MigrateTo( + JobHandle dependsOn, + IDataSource destinationDataSource, + ref NativeArray remapArray, + DestinationWorldDataMap destinationWorldDataMap) + { + CancelRequestsDataSource destination = destinationDataSource as CancelRequestsDataSource; + UnsafeTypedStream.Writer destinationWriter = default; + + if (destination == null) + { + dependsOn = JobHandle.CombineDependencies( + dependsOn, + PendingData.AcquireAsync(AccessType.ExclusiveWrite)); + } + else + { + dependsOn = JobHandle.CombineDependencies( + dependsOn, + PendingData.AcquireAsync(AccessType.ExclusiveWrite), + destination.PendingData.AcquireAsync(AccessType.ExclusiveWrite)); + + destinationWriter = destination.PendingWriter; + } + + + MigrateJob migrateJob = new MigrateJob( + PendingData.Pending, + destinationWriter, + remapArray, + destinationWorldDataMap.TaskSetOwnerIDMapping, + destinationWorldDataMap.ActiveIDMapping); + dependsOn = migrateJob.Schedule(dependsOn); + + PendingData.ReleaseAsync(dependsOn); + destination?.PendingData.ReleaseAsync(dependsOn); + + return dependsOn; + } + + [BurstCompile] + private struct MigrateJob : IJob + { + private const int UNSET_ID = -1; + + private UnsafeTypedStream m_CurrentStream; + private readonly UnsafeTypedStream.Writer m_DestinationStreamWriter; + [ReadOnly] private NativeArray m_RemapArray; + [ReadOnly] private readonly NativeParallelHashMap m_TaskSetOwnerIDMapping; + [ReadOnly] private readonly NativeParallelHashMap m_ActiveIDMapping; + [NativeSetThreadIndex] private readonly int m_NativeThreadIndex; + + public MigrateJob( + UnsafeTypedStream currentStream, + UnsafeTypedStream.Writer destinationStreamWriter, + NativeArray remapArray, + NativeParallelHashMap taskSetOwnerIDMapping, + NativeParallelHashMap activeIDMapping) + { + m_CurrentStream = currentStream; + m_DestinationStreamWriter = destinationStreamWriter; + m_RemapArray = remapArray; + m_TaskSetOwnerIDMapping = taskSetOwnerIDMapping; + m_ActiveIDMapping = activeIDMapping; + + m_NativeThreadIndex = UNSET_ID; + } + + public void Execute() + { + //TODO: Optimization - Look into adding a RemoveSwapBack like function the UnsafeTypedStream. We could then avoid + //this copy to the array and the clear and instead just iterate through the stream and remove the instances we don't need. + //See: https://github.com/decline-cookies/anvil-unity-dots/pull/232#discussion_r1181714399 + + //Can't modify while iterating so we collapse down to a single array and clean the underlying stream. + //We'll build this stream back up if anything should still remain + NativeArray currentInstanceArray = m_CurrentStream.ToNativeArray(Allocator.Temp); + m_CurrentStream.Clear(); + + int laneIndex = ParallelAccessUtil.CollectionIndexForThread(m_NativeThreadIndex); + + UnsafeTypedStream.LaneWriter currentLaneWriter = m_CurrentStream.AsLaneWriter(laneIndex); + UnsafeTypedStream.LaneWriter destinationLaneWriter = + m_DestinationStreamWriter.IsCreated + ? m_DestinationStreamWriter.AsLaneWriter(laneIndex) + : default; + + for (int i = 0; i < currentInstanceArray.Length; ++i) + { + EntityProxyInstanceID instanceID = currentInstanceArray[i]; + + //If we don't exist in the new world then we stayed in this world and we need to rewrite ourselves + //to our own stream + if (!instanceID.Entity.TryGetRemappedEntity(ref m_RemapArray, out Entity remappedEntity)) + { + currentLaneWriter.Write(ref instanceID); + continue; + } + + //If we don't have a destination in the new world, then we can just let these cease to exist + if (!destinationLaneWriter.IsCreated + || !m_TaskSetOwnerIDMapping.TryGetValue(instanceID.TaskSetOwnerID, out uint destinationTaskSetOwnerID) + || !m_ActiveIDMapping.TryGetValue(instanceID.ActiveID, out uint destinationActiveID)) + { + continue; + } + + //If we do have a destination, then we will want to patch the entity references + instanceID.PatchEntityReferences(ref m_RemapArray); + + //Rewrite the memory for the TaskSetOwnerID and ActiveID + instanceID.PatchIDs( + destinationTaskSetOwnerID, + destinationActiveID); + + //Write to the destination stream + destinationLaneWriter.Write(instanceID); + } + } + } + } +} diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractEntityProxyInstanceIDDataSource.cs.meta b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractEntityProxyInstanceIDDataSource.cs.meta new file mode 100644 index 00000000..5d4abfc5 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/AbstractEntityProxyInstanceIDDataSource.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b05cb314a49b4946a34dea034a4c7ea9 +timeCreated: 1682706186 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/CancelProgressDataSource.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/CancelProgressDataSource.cs index 91f5fe2d..8c20625b 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/CancelProgressDataSource.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/CancelProgressDataSource.cs @@ -1,15 +1,177 @@ +using Anvil.Unity.DOTS.Jobs; using System; +using System.Collections.Generic; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; using Unity.Jobs; namespace Anvil.Unity.DOTS.Entities.TaskDriver { - internal class CancelProgressDataSource : AbstractDataSource + internal class CancelProgressDataSource : AbstractEntityProxyInstanceIDDataSource { + + // ReSharper disable once InconsistentNaming + private NativeArray m_MigrationDependencies_ScratchPad; + public CancelProgressDataSource(TaskDriverManagementSystem taskDriverManagementSystem) : base(taskDriverManagementSystem) { } + protected override void DisposeSelf() + { + if (m_MigrationDependencies_ScratchPad.IsCreated) + { + m_MigrationDependencies_ScratchPad.Dispose(); + } + base.DisposeSelf(); + } + + protected override void HardenSelf() + { + base.HardenSelf(); + + //One extra for base dependency + m_MigrationDependencies_ScratchPad = new NativeArray(ActiveDataLookupByID.Count + 1, Allocator.Persistent); + } + protected override JobHandle ConsolidateSelf(JobHandle dependsOn) { throw new InvalidOperationException($"CancelProgress Data Never needs to be consolidated"); } + + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + + public override JobHandle MigrateTo( + JobHandle dependsOn, + IDataSource destinationDataSource, + ref NativeArray remapArray, + DestinationWorldDataMap destinationWorldDataMap) + { + //TODO: Optimization by using a list of entities that moved and iterating through that instead. See: https://github.com/decline-cookies/anvil-unity-dots/pull/232#discussion_r1181717999 + int index = 0; + foreach (KeyValuePair entry in ActiveDataLookupByID) + { + ActiveLookupData activeLookupData = (ActiveLookupData)entry.Value; + + m_MigrationDependencies_ScratchPad[index] = MigrateTo( + dependsOn, + activeLookupData, + destinationDataSource, + ref remapArray, + destinationWorldDataMap); + index++; + } + m_MigrationDependencies_ScratchPad[index] = base.MigrateTo( + dependsOn, + destinationDataSource, + ref remapArray, + destinationWorldDataMap); + + return JobHandle.CombineDependencies(m_MigrationDependencies_ScratchPad); + } + + private JobHandle MigrateTo( + JobHandle dependsOn, + ActiveLookupData currentLookupData, + IDataSource destinationDataSource, + ref NativeArray remapArray, + DestinationWorldDataMap destinationWorldDataMap) + { + ActiveLookupData destinationLookupData = null; + + //If we don't have a destination or a mapping to a destination active ID... + if (destinationDataSource is not CancelProgressDataSource destination + || !destinationWorldDataMap.ActiveIDMapping.TryGetValue( + currentLookupData.ID, + out uint destinationActiveID)) + { + //Then we can only deal with ourselves + dependsOn = JobHandle.CombineDependencies( + dependsOn, + currentLookupData.AcquireAsync(AccessType.ExclusiveWrite)); + } + else + { + destinationLookupData = (ActiveLookupData)destination.ActiveDataLookupByID[destinationActiveID]; + + dependsOn = JobHandle.CombineDependencies( + dependsOn, + currentLookupData.AcquireAsync(AccessType.ExclusiveWrite), + destinationLookupData.AcquireAsync(AccessType.ExclusiveWrite)); + } + + MigrateJob migrateJob = new MigrateJob( + currentLookupData.Lookup, + destinationLookupData?.Lookup ?? default, + ref remapArray, + destinationWorldDataMap.TaskSetOwnerIDMapping, + destinationWorldDataMap.ActiveIDMapping); + + dependsOn = migrateJob.Schedule(dependsOn); + + currentLookupData.ReleaseAsync(dependsOn); + destinationLookupData?.ReleaseAsync(dependsOn); + + return dependsOn; + } + + [BurstCompile] + private struct MigrateJob : IJob + { + private UnsafeParallelHashMap m_CurrentLookup; + private UnsafeParallelHashMap m_DestinationLookup; + [ReadOnly] private NativeArray m_RemapArray; + [ReadOnly] private readonly NativeParallelHashMap m_TaskSetOwnerIDMapping; + [ReadOnly] private readonly NativeParallelHashMap m_ActiveIDMapping; + + public MigrateJob( + UnsafeParallelHashMap currentLookup, + UnsafeParallelHashMap destinationLookup, + ref NativeArray remapArray, + NativeParallelHashMap taskSetOwnerIDMapping, + NativeParallelHashMap activeIDMapping) + { + m_CurrentLookup = currentLookup; + m_DestinationLookup = destinationLookup; + m_RemapArray = remapArray; + m_TaskSetOwnerIDMapping = taskSetOwnerIDMapping; + m_ActiveIDMapping = activeIDMapping; + } + + public void Execute() + { + //Can't remove while iterating so we collapse to an array first of our current keys/values + NativeKeyValueArrays currentEntries = m_CurrentLookup.GetKeyValueArrays(Allocator.Temp); + + for (int i = 0; i < currentEntries.Length; ++i) + { + EntityProxyInstanceID currentID = currentEntries.Keys[i]; + //If we don't exist in the new world, we can just skip, we stayed in this world + if (!currentID.Entity.TryGetRemappedEntity(ref m_RemapArray, out Entity remappedEntity)) + { + continue; + } + + //Otherwise, remove us from this world's lookup + m_CurrentLookup.Remove(currentID); + + //If we don't have a destination in the new world, then we can just let these cease to exist + if (!m_TaskSetOwnerIDMapping.TryGetValue(currentID.TaskSetOwnerID, out uint destinationTaskSetOwnerID) + || !m_ActiveIDMapping.TryGetValue(currentID.ActiveID, out uint destinationActiveID)) + { + continue; + } + + //Patch our ID with new values + currentID.PatchEntityReferences(ref m_RemapArray); + currentID.PatchIDs(destinationTaskSetOwnerID, destinationActiveID); + + //Write to the destination lookup + m_DestinationLookup.Add(currentID, currentEntries.Values[i]); + } + } + } } } diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/CancelRequestsDataSource.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/CancelRequestsDataSource.cs index fd322143..9d4e4711 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/CancelRequestsDataSource.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/CancelRequestsDataSource.cs @@ -4,7 +4,7 @@ namespace Anvil.Unity.DOTS.Entities.TaskDriver { - internal class CancelRequestsDataSource : AbstractDataSource + internal class CancelRequestsDataSource : AbstractEntityProxyInstanceIDDataSource { private CancelRequestsDataSourceConsolidator m_Consolidator; diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/EntityProxyDataSource.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/EntityProxyDataSource.cs index 8e360d20..52001d67 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/EntityProxyDataSource.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/EntityProxyDataSource.cs @@ -1,5 +1,9 @@ +using Anvil.Unity.DOTS.Data; using Anvil.Unity.DOTS.Jobs; using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; using Unity.Jobs; namespace Anvil.Unity.DOTS.Entities.TaskDriver @@ -9,7 +13,11 @@ internal class EntityProxyDataSource : AbstractDataSource m_Consolidator; - public EntityProxyDataSource(TaskDriverManagementSystem taskDriverManagementSystem) : base(taskDriverManagementSystem) { } + public EntityProxyDataSource(TaskDriverManagementSystem taskDriverManagementSystem) : base(taskDriverManagementSystem) + { + EntityWorldMigrationSystem.RegisterForEntityPatching>(); + EntityProxyInstanceWrapper.Debug_EnsureOffsetsAreCorrect(); + } protected override void DisposeSelf() { @@ -34,6 +42,127 @@ protected override void HardenSelf() m_Consolidator = new EntityProxyDataSourceConsolidator(PendingData, ActiveDataLookupByID); } + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + + public override JobHandle MigrateTo( + JobHandle dependsOn, + IDataSource destinationDataSource, + ref NativeArray remapArray, + DestinationWorldDataMap destinationWorldDataMap) + { + EntityProxyDataSource destination = destinationDataSource as EntityProxyDataSource; + UnsafeTypedStream>.Writer destinationWriter = default; + + if (destination == null) + { + dependsOn = JobHandle.CombineDependencies( + dependsOn, + PendingData.AcquireAsync(AccessType.ExclusiveWrite)); + } + else + { + dependsOn = JobHandle.CombineDependencies( + dependsOn, + PendingData.AcquireAsync(AccessType.ExclusiveWrite), + destination.PendingData.AcquireAsync(AccessType.ExclusiveWrite)); + + destinationWriter = destination.PendingWriter; + } + + MigrateJob migrateJob = new MigrateJob( + PendingData.Pending, + destinationWriter, + ref remapArray, + destinationWorldDataMap.TaskSetOwnerIDMapping, + destinationWorldDataMap.ActiveIDMapping); + dependsOn = migrateJob.Schedule(dependsOn); + + PendingData.ReleaseAsync(dependsOn); + destination?.PendingData.ReleaseAsync(dependsOn); + + return dependsOn; + } + + [BurstCompile] + private struct MigrateJob : IJob + { + private const int UNSET_ID = -1; + + private UnsafeTypedStream> m_CurrentStream; + private readonly UnsafeTypedStream>.Writer m_DestinationStreamWriter; + [ReadOnly] private NativeArray m_RemapArray; + [ReadOnly] private readonly NativeParallelHashMap m_TaskSetOwnerIDMapping; + [ReadOnly] private readonly NativeParallelHashMap m_ActiveIDMapping; + [NativeSetThreadIndex] private readonly int m_NativeThreadIndex; + + public MigrateJob( + UnsafeTypedStream> currentStream, + UnsafeTypedStream>.Writer destinationStreamWriter, + ref NativeArray remapArray, + NativeParallelHashMap taskSetOwnerIDMapping, + NativeParallelHashMap activeIDMapping) + { + m_CurrentStream = currentStream; + m_DestinationStreamWriter = destinationStreamWriter; + m_RemapArray = remapArray; + m_TaskSetOwnerIDMapping = taskSetOwnerIDMapping; + m_ActiveIDMapping = activeIDMapping; + + m_NativeThreadIndex = UNSET_ID; + } + + public void Execute() + { + //Can't modify while iterating so we collapse down to a single array and clean the underlying stream. + //We'll build this stream back up if anything should still remain + NativeArray> currentInstanceArray = m_CurrentStream.ToNativeArray(Allocator.Temp); + m_CurrentStream.Clear(); + + int laneIndex = ParallelAccessUtil.CollectionIndexForThread(m_NativeThreadIndex); + + UnsafeTypedStream>.LaneWriter currentLaneWriter = m_CurrentStream.AsLaneWriter(laneIndex); + UnsafeTypedStream>.LaneWriter destinationLaneWriter = + m_DestinationStreamWriter.IsCreated + ? m_DestinationStreamWriter.AsLaneWriter(laneIndex) + : default; + + for (int i = 0; i < currentInstanceArray.Length; ++i) + { + EntityProxyInstanceWrapper instance = currentInstanceArray[i]; + EntityProxyInstanceID instanceID = instance.InstanceID; + + //If we don't exist in the new world then we stayed in this world and we need to rewrite ourselves + //to our own stream + if (!instanceID.Entity.TryGetRemappedEntity(ref m_RemapArray, out Entity remappedEntity)) + { + currentLaneWriter.Write(ref instance); + continue; + } + + //If we don't have a destination in the new world, then we can just let these cease to exist + if (!destinationLaneWriter.IsCreated + || !m_TaskSetOwnerIDMapping.TryGetValue(instanceID.TaskSetOwnerID, out uint destinationTaskSetOwnerID) + || !m_ActiveIDMapping.TryGetValue(instanceID.ActiveID, out uint destinationActiveID)) + { + continue; + } + + //If we do have a destination, then we will want to patch the entity references + instance.PatchEntityReferences(ref m_RemapArray); + + //Rewrite the memory for the TaskSetOwnerID and ActiveID + instance.PatchIDs( + destinationTaskSetOwnerID, + destinationActiveID); + + //Write to the destination stream + destinationLaneWriter.Write(instance); + } + } + } + //************************************************************************************************************* // EXECUTION //************************************************************************************************************* diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/ICancellableDataStream.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/ICancellableDataStream.cs new file mode 100644 index 00000000..60610866 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/ICancellableDataStream.cs @@ -0,0 +1,11 @@ +using System; + +namespace Anvil.Unity.DOTS.Entities.TaskDriver +{ + internal interface ICancellableDataStream + { + public uint PendingCancelActiveID { get; } + + public Type InstanceType { get; } + } +} diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/ICancellableDataStream.cs.meta b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/ICancellableDataStream.cs.meta new file mode 100644 index 00000000..5cd19245 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/ICancellableDataStream.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ee4f9b67da8f41dcaefff7e66e8f5122 +timeCreated: 1682703596 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/IDataSource.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/IDataSource.cs index f4c17e0d..ac659158 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/IDataSource.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/DataSource/IDataSource.cs @@ -1,6 +1,8 @@ using Anvil.CSharp.Core; using Anvil.Unity.DOTS.Jobs; using System.Reflection; +using Unity.Collections; +using Unity.Entities; using Unity.Jobs; namespace Anvil.Unity.DOTS.Entities.TaskDriver @@ -11,5 +13,11 @@ internal interface IDataSource : IAnvilDisposable public void Harden(); public JobHandle Consolidate(JobHandle dependsOn); + + public JobHandle MigrateTo( + JobHandle dependsOn, + IDataSource destinationDataSource, + ref NativeArray remapArray, + DestinationWorldDataMap destinationWorldDataMap); } } diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/EntityProxyDataStream.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/EntityProxyDataStream.cs index 6e5171c9..b393b50c 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/EntityProxyDataStream.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/EntityProxyDataStream.cs @@ -1,5 +1,6 @@ using Anvil.Unity.DOTS.Data; using Anvil.Unity.DOTS.Jobs; +using System; using System.Runtime.CompilerServices; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; @@ -7,9 +8,10 @@ namespace Anvil.Unity.DOTS.Entities.TaskDriver { //TODO: #137 - Too much complexity that is not needed - internal class EntityProxyDataStream : AbstractDataStream, - IDriverDataStream, - ISystemDataStream + internal class EntityProxyDataStream : AbstractDataStream, + IDriverDataStream, + ISystemDataStream, + ICancellableDataStream where TInstance : unmanaged, IEntityProxyInstance { public static readonly int MAX_ELEMENTS_PER_CHUNK = ChunkUtil.MaxElementsPerChunk>(); @@ -26,6 +28,13 @@ public override uint ActiveID get => m_ActiveArrayData.ID; } + public uint PendingCancelActiveID + { + get => m_PendingCancelActiveArrayData.ID; + } + + public Type InstanceType { get; } + //TODO: #136 - Not good to expose these just for the CancelComplete case. public UnsafeTypedStream>.Writer PendingWriter { get; } public PendingData> PendingData { get; } @@ -46,6 +55,8 @@ public EntityProxyDataStream(ITaskSetOwner taskSetOwner, CancelRequestBehaviour } ScheduleInfo = m_ActiveArrayData.ScheduleInfo; + + InstanceType = typeof(TInstance); } public EntityProxyDataStream(AbstractTaskDriver taskDriver, EntityProxyDataStream systemDataStream) @@ -60,6 +71,8 @@ public EntityProxyDataStream(AbstractTaskDriver taskDriver, EntityProxyDataStrea //TODO: #136 - Not good to expose these just for the CancelComplete case. PendingData = systemDataStream.PendingData; PendingWriter = systemDataStream.PendingWriter; + + InstanceType = typeof(TInstance); } //TODO: #137 - Gross!!! This is a special case only for CancelComplete @@ -73,6 +86,8 @@ protected EntityProxyDataStream(ITaskSetOwner taskSetOwner) : base(taskSetOwner) //TODO: #136 - Not good to expose these just for the CancelComplete case. PendingWriter = m_DataSource.PendingWriter; PendingData = m_DataSource.PendingData; + + InstanceType = typeof(TInstance); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -223,4 +238,4 @@ public void ReleasePendingWriter() ReleasePending(); } } -} \ No newline at end of file +} diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceID.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceID.cs index 99b28844..9fb9f49c 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceID.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceID.cs @@ -1,7 +1,9 @@ using Anvil.Unity.DOTS.Util; using System; +using System.Diagnostics; using System.Runtime.InteropServices; using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; using Unity.Entities; namespace Anvil.Unity.DOTS.Entities.TaskDriver @@ -11,6 +13,10 @@ namespace Anvil.Unity.DOTS.Entities.TaskDriver [StructLayout(LayoutKind.Sequential, Size = 16)] internal readonly struct EntityProxyInstanceID : IEquatable { + //NOTE: Be careful messing with these - See Debug_EnsureOffsetsAreCorrect + public const int TASK_SET_OWNER_ID_OFFSET = 8; + public const int ACTIVE_ID_OFFSET = 12; + public static bool operator ==(EntityProxyInstanceID lhs, EntityProxyInstanceID rhs) { return lhs.Entity == rhs.Entity && lhs.TaskSetOwnerID == rhs.TaskSetOwnerID; @@ -71,5 +77,25 @@ public FixedString64Bytes ToFixedString() fs.Append(ActiveID); return fs; } + + //************************************************************************************************************* + // SAFETY + //************************************************************************************************************* + + [Conditional("ANVIL_DEBUG_SAFETY")] + public static void Debug_EnsureOffsetsAreCorrect() + { + int actualOffset = UnsafeUtility.GetFieldOffset(typeof(EntityProxyInstanceID).GetField(nameof(TaskSetOwnerID))); + if (actualOffset != TASK_SET_OWNER_ID_OFFSET) + { + throw new InvalidOperationException($"{nameof(TaskSetOwnerID)} has changed location in the struct. The hardcoded burst compatible offset of {nameof(TASK_SET_OWNER_ID_OFFSET)} = {TASK_SET_OWNER_ID_OFFSET} needs to be changed to {actualOffset}!"); + } + + actualOffset = UnsafeUtility.GetFieldOffset(typeof(EntityProxyInstanceID).GetField(nameof(ActiveID))); + if (actualOffset != ACTIVE_ID_OFFSET) + { + throw new InvalidOperationException($"{nameof(ActiveID)} has changed location in the struct. The hardcoded burst compatible offset of {nameof(ACTIVE_ID_OFFSET)} = {ACTIVE_ID_OFFSET} needs to be changed to {actualOffset}!"); + } + } } } diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapper.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapper.cs index 886f239e..8c859d54 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapper.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapper.cs @@ -1,8 +1,11 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using Unity.Burst; using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; using Unity.Entities; +using UnityEngine; namespace Anvil.Unity.DOTS.Entities.TaskDriver { @@ -11,6 +14,10 @@ namespace Anvil.Unity.DOTS.Entities.TaskDriver internal readonly struct EntityProxyInstanceWrapper : IEquatable> where TInstance : unmanaged, IEntityProxyInstance { + //NOTE: Be careful messing with this - See Debug_EnsureOffsetsAreCorrect + // ReSharper disable once StaticMemberInGenericType + public const int INSTANCE_ID_OFFSET = 0; + public static bool operator ==(EntityProxyInstanceWrapper lhs, EntityProxyInstanceWrapper rhs) { //Note that we are not checking if the Payload is equal because the wrapper is only for origin and lookup @@ -24,8 +31,8 @@ namespace Anvil.Unity.DOTS.Entities.TaskDriver return !(lhs == rhs); } - public readonly TInstance Payload; public readonly EntityProxyInstanceID InstanceID; + public readonly TInstance Payload; public EntityProxyInstanceWrapper(Entity entity, uint taskSetOwnerID, uint activeID, ref TInstance payload) { @@ -82,5 +89,15 @@ private static void Debug_EnsurePayloadsAreTheSame( throw new InvalidOperationException($"Equality check for {typeof(EntityProxyInstanceWrapper)} where the ID's are the same but the Payloads are different. This should never happen!"); } } + + [Conditional("ANVIL_DEBUG_SAFETY")] + public static void Debug_EnsureOffsetsAreCorrect() + { + int actualOffset = UnsafeUtility.GetFieldOffset(typeof(EntityProxyInstanceWrapper).GetField(nameof(InstanceID))); + if (actualOffset != INSTANCE_ID_OFFSET) + { + throw new InvalidOperationException($"{nameof(InstanceID)} has changed location in the struct. The hardcoded burst compatible offset of {nameof(INSTANCE_ID_OFFSET)} = {INSTANCE_ID_OFFSET} needs to be changed to {actualOffset}!"); + } + } } -} \ No newline at end of file +} diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapperExtension.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapperExtension.cs new file mode 100644 index 00000000..ecda988b --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapperExtension.cs @@ -0,0 +1,38 @@ +using Unity.Collections.LowLevel.Unsafe; + +namespace Anvil.Unity.DOTS.Entities.TaskDriver +{ + internal static class EntityProxyInstanceWrapperExtension + { + public static unsafe void PatchIDs( + this ref EntityProxyInstanceWrapper instanceWrapper, + uint taskSetOwnerID, + uint activeID) + where TInstance : unmanaged, IEntityProxyInstance + { + //Get the address of where the EntityProxyInstanceID is in the wrapper + byte* ptr = (byte*)UnsafeUtility.AddressOf(ref instanceWrapper) + EntityProxyInstanceWrapper.INSTANCE_ID_OFFSET; + //Get the address for the TaskSetOwnerID and set it + uint* taskSetOwnerIDPtr = (uint*)(ptr + EntityProxyInstanceID.TASK_SET_OWNER_ID_OFFSET); + *taskSetOwnerIDPtr = taskSetOwnerID; + //Get the address for the ActiveID and set it + uint* activeIDPtr = (uint*)(ptr + EntityProxyInstanceID.ACTIVE_ID_OFFSET); + *activeIDPtr = activeID; + } + + public static unsafe void PatchIDs( + this ref EntityProxyInstanceID instanceID, + uint taskSetOwnerID, + uint activeID) + { + //Get the address of the instance ID + byte* ptr = (byte*)UnsafeUtility.AddressOf(ref instanceID); + //Get the address for the TaskSetOwnerID and set it + uint* taskSetOwnerIDPtr = (uint*)(ptr + EntityProxyInstanceID.TASK_SET_OWNER_ID_OFFSET); + *taskSetOwnerIDPtr = taskSetOwnerID; + //Get the address for the ActiveID and set it + uint* activeIDPtr = (uint*)(ptr + EntityProxyInstanceID.ACTIVE_ID_OFFSET); + *activeIDPtr = activeID; + } + } +} diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapperExtension.cs.meta b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapperExtension.cs.meta new file mode 100644 index 00000000..a9b3a405 --- /dev/null +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/EntityProxyInstance/EntityProxyInstanceWrapperExtension.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6445b87fdf1841eabb6fe8535b97ff5f +timeCreated: 1682449657 \ No newline at end of file diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/JobDataInteraction/EntityPersistentDataWriter.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/JobDataInteraction/EntityPersistentDataWriter.cs index b9b4e04e..1d5df174 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/JobDataInteraction/EntityPersistentDataWriter.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/JobDataInteraction/EntityPersistentDataWriter.cs @@ -80,6 +80,12 @@ public NativeArray GetValueArray(AllocatorManager.AllocatorHandle allocat return m_Lookup.GetValueArray(allocator); } + /// > + public NativeKeyValueArrays GetKeyValueArrays(AllocatorManager.AllocatorHandle allocator) + { + return m_Lookup.GetKeyValueArrays(allocator); + } + /// > public UnsafeParallelHashMap.Enumerator GetEnumerator() { diff --git a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskSet.cs b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskSet.cs index aaa6bbf5..6f48bdd5 100644 --- a/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskSet.cs +++ b/Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskSet.cs @@ -1,19 +1,22 @@ using Anvil.CSharp.Collections; using Anvil.CSharp.Core; +using Anvil.CSharp.Logging; using System; using System.Collections.Generic; using System.Diagnostics; +using Unity.Burst; using Unity.Collections; using Unity.Entities; +using UnityEngine.UI; namespace Anvil.Unity.DOTS.Entities.TaskDriver { //TODO: #138 - Maybe we should have DriverTaskSet vs SystemTaskSet that extend AbstractTaskSet internal class TaskSet : AbstractAnvilBase { - private readonly List m_DataStreamsWithExplicitCancellation; + private readonly List m_DataStreamsWithExplicitCancellation; private readonly Dictionary m_PublicDataStreamsByType; - private readonly Dictionary m_EntityPersistentDataByType; + private readonly Dictionary m_MigratableEntityPersistentDataByType; private readonly List m_JobConfigs; private readonly HashSet m_JobConfigSchedulingDelegates; @@ -40,9 +43,9 @@ public TaskSet(ITaskSetOwner taskSetOwner) m_JobConfigs = new List(); m_JobConfigSchedulingDelegates = new HashSet(); - m_DataStreamsWithExplicitCancellation = new List(); + m_DataStreamsWithExplicitCancellation = new List(); m_PublicDataStreamsByType = new Dictionary(); - m_EntityPersistentDataByType = new Dictionary(); + m_MigratableEntityPersistentDataByType = new Dictionary(); //TODO: #138 - Move all Cancellation aspects into one class to make it easier/nicer to work with @@ -58,7 +61,7 @@ protected override void DisposeSelf() { CancelRequestsContexts.Dispose(); } - m_EntityPersistentDataByType.DisposeAllValuesAndClear(); + m_MigratableEntityPersistentDataByType.DisposeAllValuesAndClear(); base.DisposeSelf(); } @@ -112,10 +115,9 @@ public EntityPersistentData GetOrCreateEntityPersistentData() where T : unmanaged, IEntityPersistentDataInstance { Type type = typeof(T); - if (!m_EntityPersistentDataByType.TryGetValue(type, out AbstractPersistentData persistentData)) + if (!m_MigratableEntityPersistentDataByType.TryGetValue(type, out IMigratablePersistentData persistentData)) { persistentData = CreateEntityPersistentData(); - m_EntityPersistentDataByType.Add(type, persistentData); } return (EntityPersistentData)persistentData; @@ -125,6 +127,7 @@ public EntityPersistentData CreateEntityPersistentData() where T : unmanaged, IEntityPersistentDataInstance { EntityPersistentData entityPersistentData = new EntityPersistentData(); + m_MigratableEntityPersistentDataByType.Add(typeof(T), entityPersistentData); return entityPersistentData; } @@ -265,10 +268,69 @@ private void AddCancelRequestContextsTo(List contexts) } } + //************************************************************************************************************* + // MIGRATION + //************************************************************************************************************* + + public void AddToMigrationLookup( + string parentPath, + Dictionary migrationActiveIDLookup, + PersistentDataSystem persistentDataSystem) + { + foreach (KeyValuePair entry in m_PublicDataStreamsByType) + { + AddToMigrationLookup(parentPath, BurstRuntime.GetHashCode64(entry.Key), entry.Value.ActiveID, migrationActiveIDLookup); + } + + foreach (ICancellableDataStream entry in m_DataStreamsWithExplicitCancellation) + { + AddToMigrationLookup(parentPath, BurstRuntime.GetHashCode64(entry.InstanceType) ^ BurstRuntime.GetHashCode64(), entry.PendingCancelActiveID, migrationActiveIDLookup); + } + + AddToMigrationLookup( + parentPath, + BurstRuntime.GetHashCode64(typeof(CancelRequestsDataStream)), + CancelRequestsDataStream.ActiveID, + migrationActiveIDLookup); + + AddToMigrationLookup( + parentPath, + BurstRuntime.GetHashCode64(typeof(CancelProgressDataStream)), + CancelProgressDataStream.ActiveID, + migrationActiveIDLookup); + + AddToMigrationLookup( + parentPath, + BurstRuntime.GetHashCode64(typeof(CancelCompleteDataStream)), + CancelCompleteDataStream.ActiveID, + migrationActiveIDLookup); + + foreach (IMigratablePersistentData entry in m_MigratableEntityPersistentDataByType.Values) + { + persistentDataSystem.AddToMigrationLookup(parentPath, entry); + } + } + + private void AddToMigrationLookup(string parentPath, long typeHash, uint activeID, Dictionary migrationActiveIDLookup) + { + string path = $"{parentPath}-{typeHash}"; + Debug_EnsureNoDuplicateMigrationData(path, migrationActiveIDLookup); + migrationActiveIDLookup.Add(path, activeID); + } + //************************************************************************************************************* // SAFETY //************************************************************************************************************* + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + private void Debug_EnsureNoDuplicateMigrationData(string path, Dictionary migrationActiveIDLookup) + { + if (migrationActiveIDLookup.ContainsKey(path)) + { + throw new InvalidOperationException($"Trying to add ActiveID migration data for {this} but {path} is already in the lookup!"); + } + } + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] private void Debug_EnsureNotHardened() {