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