Search Unity

Question How to use externals plugins (C++) in ECS?

Discussion in 'Entity Component System' started by Tigrian, Apr 19, 2023.

  1. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    124
    Hi! I am adapting one of my wrappers for an external plugin (written in C++, Nvidia blast) from Mono (where it works fine) to ECS. But I'm having a lot of problems trying this, because an IntPtr can't really be stored in an ECS component, and because Marshalling is Managed.

    What I'm doing, for now, is creating disposable classes that contain the pointers that are allocated in the plugin. And every entity that needs to call the plugin does so on the main thread, without Burst, and mostly with structural changes, by means of a dictionary<index, DisposableClass> that is stored in a singleton.

    This is really contrary to the principle of ECS, and counterintuitive. And as I implement the plugin, it becomes more and more difficult to ensure data transfer between the plugin and the ECS world data. I am most often forced to create Entities with the EntityManager, but this invalidates my DynamicBuffers. I also have a lot of NativeParallelMultiHashMap that act as an array of NativeArray, to pass all this data around.

    Should I use the Unsafe class? And reproduce the structs of my plugins in C#?
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    You can store pointers in ICleanupComponentData.

    I think I would need to see the C interfaces and memory ownership to better understand what you are trying to do. But if you are just looking for examples of interfacing C++ plugins with ECS, I currently have a wrapper around ACL (Animation Compression Library) and at bake time I compress clips using the plugin and copy the compressed results into blob assets. Then at runtime I give the pointers to the blob arrays back to the plugin for sampling the clips. That's all on my GitHub if you want to look around.
     
  3. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    124
    Hi @DreamingImLatios!
    Thanks for directing me to your plugin, it should already help me. I knew ACL, but I didn't know that someone had wrapped it for ECS. Your framework looks impressive, though. I should really dive into it one of these days (when I'm done with this plugin).
    So, for my plugin, about the C interfaces, almost (I've exposed some additional functionality, but it's mostly about the editor time part) everything is available on github if you really want precise details (PhysX/NvBlast.h at release/104.2 · NVIDIA-Omniverse/PhysX · GitHub). For memory ownership, memory allocation is done in the C# side, using Marshal.AllocHGlobal. This memory is then passed into C++, to be filled with data.

    Nvidia blast is a destruction library. My main problem is to make the link between the physical chunks (entities) and their representation in C++, in the plugins. There are mainly two types of IntPtr used at runtime, blast Families (representing destructible objects) and blast actors (representing a rigidbody, grouping several chunks). If I can put these IntPtrs into ICleanUpSharedComponentData and ICleanUpComponentData respectively, my problems may be solved.
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    I'm not familiar enough with how bindings work with pointers and references to structs on the C++ side. What I did was define my own API that exclusively used pointers and values of primitive types. Casting a received void* to another type is safe in this case because as far as the compiler is concerned, its the first time it has seen that pointer and so the cast assigns the strictly aliased type. Then on the C# side, I copy the exact same interface function, rather than use IntPtr. And the pointers I give it are allocated with Unity's allocators or stack-allocated. As long as you deallocate with the same allocator you allocate with, you're in the clear. And Burst absolutely loves these kinds of interfaces, because they look like any other function that it might call into.

    I don't know the thread-safety constraints of Blast. My ACL wrapper is stateless, so I can call into it in parallel jobs. But if it is thread-safe, I would just allocate the required memory using AllocatorManager and assign the allocated pointer to a cleanup component that gets added via ECB. You'll probably want to keep a hashset of those components around as well for exiting play mode, since cleanup components don't get an opportunity for cleanup when the world shuts down.
     
    Tigrian likes this.
  5. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    124
    I use the low level Blast, which, from blast description, has :
    • C-style API consisting of stateless functions, with no global framework or context.
    Does this mean thread safety?

    Otherwise, I see now from your explanations how I can implement it.
     
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    Looking at the docs, I found this:
    So yeah. This is totally stateless and can be called from parallel Burst jobs safely as long as your pointers are unique per-thread. It is no different than what I'm doing with ACL in that regard (except at runtime ACL can read the same pointers from multiple threads since it is just reading). Now I'm kinda excited to see what you do with this. It looks like a great fit for ECS!
     
  7. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    124
    Exactly! that's what I thought too, my implementation in OOP Unity was completely bound by the rendering and especially the physics, and limited the blast potential. I can't wait to see the result with Bursted Jobs, I'll post a video when it works. Anyway, thanks a lot for the help!
     
  8. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    124
    Actualy, I might need more info on pointer in ICleanUpComponentData struct. I can't find info on it. Can I really do something like this, or I misunderstood?
    Code (CSharp):
    1. public unsafe struct NvBlastFamily : ICleanupSharedComponentData
    2. {
    3.     public void* FamilyPtr;
    4. }
    I can't find examples on the forums, the documentation, or in your framework (as you read pointers from a Blob, or otherwise I missed it as your framework is quite huge). I just want to be sure, before putting some unsafe pointer in a struct, as it feel also not natural in a classic C# use.

    Also, Nvidia blast has a SetAllocatorCallback, so I can set the Allocator that blast uses in the c++ to the AllocatorManager.Allocate (and free to Deallocate). So everything should be allocated with AllocatorManager!
     
  9. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    Yes you can, but be wary of when the world gets destroyed. If forget if entities are still around when OnDestroy gets called. If they are, you can cleanup then. Otherwise, you'll need a copy in a hashmap.

    I put unsafe pointers in structs all over the place. Just not components currently. I might start doing it again for some new Myri features in the works.

    ACL has a similar thing, but I've never gotten the callback back to possibly-Bursted AllocatorManager to work. I ended up using ACL's ansi allocator. If you get it to work, I'd love to learn how you did it!
     
  10. Tigrian

    Tigrian

    Joined:
    Mar 21, 2021
    Posts:
    124
    Hi @DreamingImLatios,

    I'm making good progress on my blast integration at the moment. However, I have one problem in particular. I don't know who to ask, and since you seem to be pretty knowledgeable about C++ and pointers, I thought I'd ask you.

    An update on what I've done, and where I'm at:
    I managed to bake absolutely all the Fractured mesh info into blob assets (I was inspired by the way you bake animation clips to bake the blast asset into BlobArray<byte>, instead of loading a runtime file). Now, I can initialize the unfractured mesh with its colliders, everything works. The CleanUpComponentData work perfectly. So everything is perfect, and I get to the point where I need to implement fracturing.

    And then I have a problem:
    After fracturing in Blast, I have an NvBlastActorSplitEvent (a struct) that contains the new actors in an NvBlastActor**, so an array of NvBlastActors*. On the c# side, I just have a void*.
    How can I retrieve each pointer of the array knowing the size of the array. Note that the memory for this array has been allocated without knowing its final size, and therefore its capacity has been set to the maximum possible new actors (if the destructible breaks down entirely).
    This is how I did it before, with IntPtrs:
    Code (CSharp):
    1. //Buffer alloc
    2. newActorsBuffer = Marshal.AllocHGlobal((int)_leafChunkCount * Marshal.SizeOf(typeof(IntPtr)));
    3.  
    4. //In Split method
    5. for (int i = 0; i < count; i++)
    6. {
    7.     int elementSize = Marshal.SizeOf(typeof(IntPtr));
    8.     var ptr = Marshal.ReadIntPtr(split.newActors, elementSize * i);
    9.     var newActor = new NvBlastActor(_blastFamily, ptr);
    10. }
    On that, I did like this, and it seems to work.
    Code (CSharp):
    1.  
    2. #region DLL
    3.  
    4. [DllImport(NvBlastExtUnityWrapper.DLLName, CallingConvention = CallingConvention.Cdecl)]
    5. private static extern void NvBlastSetAllocatorCallback(
    6.     FunctionPointer<NvBlastAllocate> allocationCallback,
    7.     FunctionPointer<NvBlastDeallocate> deallocationCallback);
    8.  
    9. #endregion
    10. private delegate void* NvBlastAllocate(ulong size);
    11.  
    12. private delegate void NvBlastDeallocate(void* ptr);
    13.  
    14. [BurstCompile]
    15. [AOT.MonoPInvokeCallback(typeof(NvBlastAllocate))]
    16. public static void* AllocatorManagerAllocate(ulong size)
    17. {
    18.     return AllocatorManager.Allocate(AllocatorManager.Persistent,(int)size, 16);
    19. }
    20. [BurstCompile]
    21. [AOT.MonoPInvokeCallback(typeof(NvBlastDeallocate))]
    22. public static void AllocatorManagerFree(void* ptr)
    23. {
    24.     AllocatorManager.Free(AllocatorManager.Persistent, ptr);
    25. }
    26.  
    27. public static void SetAllocatorCallback()
    28. {
    29.     var allocateFunc =
    30.         BurstCompiler.CompileFunctionPointer<NvBlastAllocate>(AllocatorManagerAllocate);
    31.     var deallocateFunc =
    32.         BurstCompiler.CompileFunctionPointer<NvBlastDeallocate>(AllocatorManagerFree);
    33.  
    34.     NvBlastSetAllocatorCallback(allocateFunc, deallocateFunc);
    35. }
    36.  
    At least I was able to check this with the Debug methods, which are also callbacks that display the dll errors and their locations in c++ scripts. The debugs appear fine, although called in burst compiled methods. I don't know how to check if the allocation has been done by the AllocatorManager (unless adding a Debug in the method, but even then I would not be sure about the allocation). As I don't get any crashes or error messages, I assume it works. You are in a better position than me to judge whether this is the solution, so I'll wait for your opinion before I conclude that it works.
     
    DreamingImLatios likes this.
  11. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    I'd probably do it like this:
    Code (CSharp):
    1. unsafe struct NvBlastActorPtr
    2. {
    3.     public void* ptr;
    4. }
    5.  
    6. var newActorsBuffer = (NvBlastActorPtr*)SplitComment.newActors;
    You can then index newActorsBuffer using normal C# indexing. Though be warned as there is no bounds check. The size of pointer will be the same between C# and C++, being 8 bytes on most platforms Unity targets nowadays.

    If you are logging the callbacks from C# (so C# callbacks are executing), then it is probably working. I didn't think you could just pass the FunctionPointer like that, but knowing the Burst team, I shouldn't be surprised either. Nice job figuring that out!
     
    Tigrian likes this.