Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

Rules and Whys behind Blobs and Colliders

Discussion in 'Entity Component System' started by DreamingImLatios, Jun 6, 2019.

  1. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,280
    For reference, this thread is a follow-up to the discussion in the Unity.Physics thread as the topic is quite complex and I feel these questions in particular deserve their own thread.

    From the other posts on this forum, I've gathered two very different interpretations of what Blob Assets are supposed to be:

    Interpretation 1: Blobs are immutable data accessible to jobs that are loaded and unloaded with prefabs and subscenes.

    This interpretation makes total sense to me for things like animation curves, static meshes, noise fields, and plenty of other things. But it does not make sense for Colliders since there's dozens of use cases where a developer would want to modify a collider.

    Interpretation 2: Blobs are lightweight mutable objects that are allocated on a specialized heap. They can be accessed from jobs on any thread any provide no mechanisms that I am aware of for reader-writer safety.

    This interpretation makes me not want to use them in any code other than maybe highly specific and internalized code that only the highest level of code ninjas are allowed to touch.


    Some will suggest that the second interpretation allows for a sort of polymorphism to take place which is required for Colliders.
    Others will suggest that it allows for the dynamic sizing of Mesh Colliders.
    In no way am I trying to call anyone wrong on these matters. I am simply trying to fill the gaps in my understanding on this not-well-discussed topic. To do that, I'm going to show how I am currently solving the problems with colliders in my CCDE. In particular, my solution maintains mutable colliders with thread safety and polymorphic-like representation of colliders while only using Interpretation 1 of Blobs.

    My hope is that this serves as a frame of reference for discussion. In particular, I would like to know what downsides my approach has that Blobs solve. And I would like to know how Blobs compare to my approach with respect to performance, API usability, and thread safety. What I am sharing isn't the actual code but does show the relevant strategies that I am using.

    Code (CSharp):
    1. using Unity.Collections.LowLevel.Unsafe;
    2. using Unity.Entities;
    3. using Unity.Mathematics;
    4. using UnityEngine.Assertions;
    5.  
    6. namespace Ex.Collision
    7. {
    8.     public struct SphereCollider : ICollider
    9.     {
    10.         public float3 center;
    11.         public float radius;
    12.  
    13.         //ICollider methods not shown here
    14.     }
    15.  
    16.     public struct MeshCollider : ICollider
    17.     {
    18.         public BlobAssetReference<MeshColliderData> meshData;
    19.  
    20.         //ICollider methods not shown here
    21.     }
    22.  
    23.     // Collider is the only collider type which implements IComponentData.
    24.     public struct Collider : IComponentData, ICollider
    25.     {
    26.         // By default, m_colliderType is set to ColliderType.Null.
    27.         // m_colliderType can only be modified through assignment.
    28.         // However, assignment also includes casting operations from the specialized
    29.         // collider types.
    30.         private ColliderType m_colliderType;
    31.         public ColliderType ColliderType => m_colliderType;
    32.  
    33.         //This is a union for the specialized data
    34.         private ColliderSpecializationStorage m_storage;
    35.  
    36.         public unsafe static implicit operator Collider(SphereCollider sphereCollider)
    37.         {
    38.             Collider collider;
    39.             collider.m_colliderType = ColliderType.Sphere;
    40.             UnsafeUtility.MemCpy(&collider.m_storage, &sphereCollider, sizeof(SphereCollider));
    41.             return collider;
    42.         }
    43.  
    44.         public unsafe static implicit operator SphereCollider(Collider collider)
    45.         {
    46. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    47.             Assert.AreEqual(collider.m_colliderType, ColliderType.Sphere, "Error: Collider is not a SphereCollider but is being casted to one.");
    48. #endif
    49.  
    50.             SphereCollider sphere;
    51.             UnsafeUtility.MemCpy(&collider.m_storage, &sphere, sizeof(SphereCollider));
    52.             return sphere;
    53.         }
    54.  
    55.         public unsafe static implicit operator Collider(MeshCollider meshCollider)
    56.         {
    57.             Collider collider;
    58.             collider.m_colliderType = ColliderType.Mesh;
    59.             UnsafeUtility.MemCpy(&collider.m_storage, &meshCollider, UnsafeUtility.SizeOf<MeshCollider>());
    60.             return collider;
    61.         }
    62.  
    63.         public unsafe static implicit operator MeshCollider(Collider collider)
    64.         {
    65. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    66.             Assert.AreEqual(collider.m_colliderType, ColliderType.Mesh, "Error: Collider is not a MeshCollider but is being casted to one.");
    67. #endif
    68.  
    69.             MeshCollider mesh;
    70.             UnsafeUtility.MemCpy(&collider.m_storage, &mesh, UnsafeUtility.SizeOf<MeshCollider>());
    71.             return mesh;
    72.         }
    73.  
    74.         //ICollider methods not shown here
    75.     }
    76. }
    This allows any collider to be allocated on the stack in any job and used in queries with automatic destruction on leaving scope. There's no special creation function or allocation required. Standard constructors are fine. Casting does not require the user to use unsafe code. Primitive colliders are stored directly in chunks with no indirection to the heap during IJobForEach. And it is impossible to accidentally copy the header of a blob to the stack.

    So my questions:
    • What are the rules with Blobs and Unity Colliders with regard to thread safety?
    • What are the rules with Blobs and Unity Colliders with regard to memory management?
    • What additional problems do Blobs and Unity Colliders solve that my approach overlooks?
    • What problems does my approach solve the Blobs and Unity Colliders do not?
    I'm hoping to learn a ton from this and perhaps others can learn from it too!
     
  2. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    * Blobs are immutable. (This is currently not enforced but we will enforce it in the future via static code analysis rules)
    * Blobs are lightweight, they should be temporarily allocatable on the stack but currently physics doesn't have an API for that
    * If you have readwrite access to an IComponentData holding a blob you can change the reference to the blob at any point. So if you want to change the size of the sphere collider you can create a new blob and replace it on the Collider struct


    Part of the equation is also the specific way in which we implemented the conversion pipeline. Currently compound colliders, result in a single IComponentData collider. We simply extract all colliders from children, and bake one big Compound shape containing all of it. Thus the entities representing the collision shape aren't even used after the conversion has happened. This is good for perf, but has some trade-offs when you want to animate primitive colliders that are authored as children of the same rigidbody.
     
  3. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,280
    Thank you so much! That clears up a lot of confusion I had! Sounds like Blobs are going to be the best of everything once their API is made safe (but even now the rules make it very obvious to me how to not screw them up). I'm guessing blobs also allow references to resources owned externally, which would make a ton of sense with regards to the Havok integration.

    I guess my final question is related to memory management. So let's imagine I have a game where a spawn randomly-sized enemies (and thus create new blobs). The enemies then have the ability to duplicate themselves thus sharing their blob references. They also get destroyed pretty frequently too.

    How do I make this not memory leak? Do blobs have some special mechanism for this use case? Or do I need to manually keep track of which blobs are being used?
     
  4. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    During conversion process we generate a single mega blob from all individual blobs. (one allocation) That one allocation is tracked with shared component on each entity using any individual blob from the mega blob for the whole converted scene & prefabs.

    Thus only when the last element of the scene or also instantiated prefab gets destroy, then we also destroy the mega blob. Whats good about it is that it's insanely fast at massive scale. Its essentially zero overhead even for millions of blobs. A common problem in this area is refcounting having massive overhead on per asset basis or garbage collection being totally useless at scale if you want to have 60FPS streaming games with massive amount of content.

    So for conversion pipeline it is completely automatic and "just works" even for Instantiated prefabs. And is basically zero overhead.

    We currently don't yet have a great solution for automatic tracking for the procedural gen case. In that situation i would say a system state component tracking destruction is a reasonable approach. We are also lacking debug / leak detection tooling in this area. So for sure there is work left to do, but everything can be done correctly manually and the conversion case works very well.
     
  5. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,280
    I knew that much, but what happens if that mega blob fills up because new colliders are created every frame? I know that is very unlikely for simple colliders like sphere colliders, but for a constantly deforming mesh collider that seems like a real issue. Eventually something is going to have to go through and clean up all the abandoned and unused colliders that were generated at runtime. I guess I'm wondering if I would have to do that manually and if so are there any tricks to do that more efficiently?
     
  6. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    >I knew that much, but what happens if that mega blob fills up because new colliders are created every frame?
    They are tracked seperately. Like i said you have to track those manually right now. Which is not amazing but thats what we have right now. They do not go into the mega blob so there is no issue with filling it up.
     
    DreamingImLatios likes this.
  7. recursive

    recursive

    Joined:
    Jul 12, 2012
    Posts:
    669
    @Joachim_Ante - on this note, and to add to what I asked in the other thread:

    How do cross-scene blob references work, or is there not yet a solution for that?
     
  8. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Currently they are duplicated. Eventually we want to have a concept of "packs" similar to asset bundles which allows for sharing. An alternative is runtime deduplication. Lots of options.
     
    recursive likes this.
  9. Justin_Larrabee

    Justin_Larrabee

    Joined:
    Apr 24, 2018
    Posts:
    106
    Will the static analysis warn when you make the mistake of assigning a blob struct member to a non-ref local variable? When you do so currently and access a BlobPtr or BlobArray on that copied struct, the pointer math under the hood will cause it to return junk memory (which could easily cause a crash from out of bounds memory access).
     
  10. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Yes. That's the primary mistake we want to avoid via static analysis.
     
  11. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,280
    Sorry.
    For some reason the last part of your previous post didn't show up on my screen this morning. But the mega-blob not being the proc-gen blob target would have tripped me up, so thanks for clarifying that!

    And thanks for clarifying blobs in general. It is really clear to me now the rules and roles they play in DOTS, not just for Unity.Physics, but in general.

    I think I'm still going to keep my CCDE's representation of colliders until the Blob API and tooling near a more finished state. The good news is that both representations have the same data access constraints which means I should be able to port code between either representation without having to redesign an algorithm. I now have a pretty good roadmap in my head for rebasing my CCDE on top of Unity.Physics.

    I'm pretty excited to try these out with a heightened sense of direction as to what I'm doing. SDFs sound like a good target for these guys.
     
  12. Justin_Larrabee

    Justin_Larrabee

    Joined:
    Apr 24, 2018
    Posts:
    106
    Great, glad to hear. Wasn't immediately obvious to me what was going on when I did it on accident. As a former C/C++ guy I'm having fun with all the unsafe code lurking around in the implementation. Very awesome stuff.