Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Entity Migration #232

Merged
merged 18 commits into from
May 2, 2023
Merged

Custom Entity Migration #232

merged 18 commits into from
May 2, 2023

Conversation

jkeon
Copy link
Member

@jkeon jkeon commented Apr 29, 2023

Unity will automatically remap Entity references on Components and in DynamicBuffers when Entities move from one World to another.

Entities that exist in TaskDrivers or EntityPersistentData however will not.

This PR gives functionality to provide a framework for allowing anything to be remapped and implements it for TaskDrivers and EntityPersistentData

What is the current behaviour?

No way to provide custom remapping.

What is the new behaviour?

Framework

Anything can implement the IMigrationObserver and register with the WorldEntityMigrationSystem to receive a notification when Entities have migrated to a new World.

The callback will provide the necessary information to perform the custom logic to remap Entity references.

MigrationUtil provides the functionality to actually do the remapping via RegisterTypeForEntityPatching and then the call to PatchEntityReferences.

All changes to the world should happen through WorldEntityMigrationSystem.MigrateTo. If you use the EntityManager.MoveEntitiesFrom vanilla function, we won't know and custom migration won't happen.

TaskDrivers and EntityPersistentData

I've handled both of these systems to automatically register their types and patch references.
Everything is jobified so that we can use Burst and in practice ends up being pretty quick.

  • TaskDrivers now can have a unique migration suffix to identify them if you happen to have two or more of the same type of TaskDriver located at the same place in the heirarchy.

Tag-Alongs

  • Fixed ENABLE_UNITY_COLLECTIONS_CHECKS to ANVIL_DEBUG_SAFETY where I found them.
  • Changed some type restrictions from struct to unmanaged
  • Added a GetKeyValueArrays to EntityPersistentDataWriter

What's Next

  • Deep Migration #233 There are still some ways you can miss Entities. If you have an EPD then you have a relationship of Key: Entity to Value: Something.

    • The Key can be remapped no problem
    • The Something might be a Collection like UnsafeParallelHashMap. If the key or something in that collection's value has an Entity reference, there's no way right now for us to know.
    • I think I can scan this an account for it, but I might have to add the check for each type of Unsafe Collection.
  • Migration Integrity #234 Integrity.

    • As we talked about, we should be able to build a lookup of all Entity references before the Migration occurs. After the Migration, we can run through that lookup and see if anything of our references were broken.
    • Either A) We referred to something that left us behind.
    • Or B) We referred to something that we left behind.
    • We can then alert the developer who didn't clean up before the migration.

What issues does this resolve?

What PRs does this depend on?

  • None

Does this introduce a breaking change?

  • Yes - If you were changing Worlds through EntityManager, you should use WorldEntityMigrationSystem now instead.
  • No

@jkeon jkeon requested a review from mbaker3 April 29, 2023 12:05
This was referenced Apr 30, 2023
Copy link
Member

@mbaker3 mbaker3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super exciting feature! Generally just things to discuss and TODOs to create.

Comment on lines 232 to 250
private sealed class SharedTypeOffsetInfo
{
public static readonly SharedStatic<UnsafeParallelHashMap<long, TypeOffsetInfo>> REF = SharedStatic<UnsafeParallelHashMap<long, TypeOffsetInfo>>.GetOrCreate<MigrationUtilContext, SharedTypeOffsetInfo>();
}

private sealed class SharedEntityOffsetInfo
{
public static readonly SharedStatic<IntPtr> REF = SharedStatic<IntPtr>.GetOrCreate<MigrationUtilContext, SharedEntityOffsetInfo>();
}

private sealed class SharedBlobAssetRefInfo
{
public static readonly SharedStatic<IntPtr> REF = SharedStatic<IntPtr>.GetOrCreate<MigrationUtilContext, SharedBlobAssetRefInfo>();
}

private sealed class SharedWeakAssetRefInfo
{
public static readonly SharedStatic<IntPtr> REF = SharedStatic<IntPtr>.GetOrCreate<MigrationUtilContext, SharedWeakAssetRefInfo>();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these wrapped in individual types vs just being statics in the MigrationUtil class?
If they have to be wrapped to they need to each be wrapped int heir own type or could they all be put into on private class?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just how SharedStatic's work. Unity itself has to do a similar scan for IComponentTypes and we need a way to differentiate between their IntPtr and my IntPtr. By wrapping in private sealed classes, I ensure my uniqueness and they ensure theirs.

It's a bunch of boiler plate but that's why I relegated it to the bottom of this class hidden away.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, weird pattern.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SharedStatic and FunctionPointers for Burst in general are weird patterns I agree, but I guess I see how they need to ensure the uniqueness of a memory address without using any managed code and that's really only possible by using a bunch of compile time generated generics with unique class types.

Annoying though.

{
//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<EntityProxyInstanceID> currentInstanceArray = m_CurrentStream.ToNativeArray(Allocator.Temp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

future optimization but would it be possible to build up the new stream (if needed) at the same time? The same way we do on each iteration of a task driver?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure what you mean, but I think we already are?

Entities that weren't moved get rewritten back to this world's stream. Entities that were moved get remapped and written to the new world's stream.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now you're copying your existing stream, clearing and then re-adding to it if the entity isn't remapped.
If you instead created a new stream you could avoid the full temp array copy.

The same way when the task driver system job updates it processes the current stream and writes any instances that need to continue to a new stream for the next frame. Similar to a double buffer approach.

Alternatively, if stream could support some sort of removeSwapBack type operation then maybe we could leave the current stream intact and just cherry pick out the entities that do get remapped to the new world?...unless we think order matters in a stream.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhhh I see what you mean. Interesting. I've added a TODO and referred to this.

I don't think order matters in the stream... removeSwapBack is really neat though, I think that could be possible and is probably the way to do this instead of adding a second stream and double buffering them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea just want to be explicit in our docs that DataStreams are unordered.


AddToMigrationLookup(
parentPath,
typeof(CancelCompleteDataStream).GetReadableName(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mild concern about using readable name for a uniqueID type situation.
Readable name is used for human readable formatting and if we ever changed that formatting it could break the path. Should we just use C#'s built in?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmn I'm not sure. If we changed the formatting it would break, but only if we changed the formatting mid-run. It's a runtime only id.

We could use the built in, it's just harder to parse when debugging. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so concerned about changing the formatting mid run as I am that our formatting doesn't provide proper uniqueness at some point.

In fact, today, this doesn't guarantee uniqueness between types. namespace1.MyType and namespace2.MyType will both resolve to the same readable name.

Maybe the type hash is a better solution? Then for debug-ability our ToString() methods can parse out and map the hash back to the type and generate the readable name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmn ok, fair point. I changed it to FullName for now to keep the uniqueness.

I added a TODO to hash the FullName to get shorter paths.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

teeechhnically you want the Assembly qualified one

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to using BurstRuntime to get the hash of the type which uses the Assembly qualified name under the hood

@jkeon jkeon mentioned this pull request May 1, 2023
2 tasks
jkeon added 5 commits May 1, 2023 14:49
# Conflicts:
#	Scripts/Runtime/Entities/TaskDriver/AbstractTaskDriver.cs
#	Scripts/Runtime/Entities/TaskDriver/System/AbstractTaskDriverSystem.cs
#	Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskData/DataStream/EntityProxyDataStream.cs
#	Scripts/Runtime/Entities/TaskDriver/TaskSet/TaskSet.cs
@jkeon jkeon requested a review from mbaker3 May 1, 2023 20:18
@jkeon
Copy link
Member Author

jkeon commented May 1, 2023

@mbaker3 Ready for re-review.

Also contains changing the name of the Task Driver System to the outside to be System and the internal one to be TaskDriverSystem instead of the flip-flop it was before.

@mbaker3
Copy link
Member

mbaker3 commented May 2, 2023

All addressed. Some discussions to sort out but this is looking good!

@jkeon jkeon merged commit 7de8eda into main May 2, 2023
@jkeon jkeon deleted the taskdriver-migration branch May 2, 2023 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

World Moving - Remap Entity data in VirtualData/TaskDrivers
2 participants