Search Unity

BlobBuilder: Allow explicit length when allocating BlobPtr.

Discussion in 'Entity Component System' started by phreezie, Apr 24, 2020.

  1. phreezie

    phreezie

    Joined:
    Oct 3, 2019
    Posts:
    119
    My static
    BlobAssetReference
    contains a
    BlobArray<BlobPtr<Collider>>
    .
    Collider
    is base struct with a common header, but the actual data has additional fields. I'm casting it to the specific type by looking at the header. For example one method of
    Collider
    looks like this (simplified):

    Code (CSharp):
    1. public unsafe float HitTest()
    2. {
    3.     fixed (Collider* collider = &this) {
    4.         switch (collider->Type) {
    5.             case ColliderType.Circle:
    6.                 return ((CircleCollider*)collider)->HitTest();
    7.             // other types
    8.  
    9.             default:
    10.                 return -1;
    11.         }
    12.     }
    13. }
    This works well for structs that contain static data. But now I have a collider with a list of float3s, so C# doesn't let me cast it anymore. The DOTS physics code has a special
    BlobArray
    class for this case that is not typed (but comes with a typed accessor), but needs to be allocated manually.

    They do that with
    BlobAssetReference.Create(void* ptr, int length)
    , taking a pointer to the data and its length. Now, I still would like to use my
    BlobBuilder
    for that, but
    BlobBuilder.Allocate<T>(ref BlobPtr<T> ptr)
    takes its size from the type. What I would need is to explicitly provide the size of the struct that include my float3s. This works:

    Code (CSharp):
    1. public unsafe ref T Allocate<T>(ref BlobPtr<T> ptr, int length, out int* offsetPtr) where T : struct
    2. {
    3.     offsetPtr = (int*)UnsafeUtility.AddressOf(ref ptr.m_OffsetPtr);
    4.  
    5.     ValidateAllocation(offsetPtr);
    6.  
    7.     var allocation = Allocate(length, UnsafeUtility.AlignOf<T>());
    8.  
    9.     var patch = new OffsetPtrPatch
    10.     {
    11.         offsetPtr = offsetPtr,
    12.         target = allocation,
    13.         length = 0
    14.     };
    15.  
    16.     m_patches.Add(patch);
    17.     return ref UnsafeUtilityEx.AsRef<T>(AllocationToPointer(allocation));
    18. }
    It takes in the size and also returns the offset pointer, which allows me to set the data via the special
    BlobArray
    mentioned above.

    Now, my main problem is that I can't extend
    BlobBuilder
    to add this method, neither copy it into my code base, because it accesses a bunch of
    internal
    s from the
    Unity.Entities
    package. So I would need to clone the entire package, which is, uh, not optimal.

    Do you guys think that this would be actually useful, or are there other solutions to my problem?
     
    cvoigt and apkdev like this.
  2. cvoigt

    cvoigt

    Joined:
    Dec 29, 2013
    Posts:
    9
    I would also like to use the non-generic BlobArray from the Physics package with BlobBuilder.

    @phreezie: Did you find a solution for this problem? I tried to find out by reading through your code in your Github repository, but all I could find was the unfinished business in Poly3DCollider.
     
  3. phreezie

    phreezie

    Joined:
    Oct 3, 2019
    Posts:
    119
    Nope, no solution yet. Went on implementing my other colliders that are of fixed size, but I will eventually have to tackle this.

    Great to know that I'm not the only one though :)
     
  4. phreezie

    phreezie

    Joined:
    Oct 3, 2019
    Posts:
    119
  5. starikcetin

    starikcetin

    Joined:
    Dec 7, 2017
    Posts:
    340
    phreezie likes this.
  6. phreezie

    phreezie

    Joined:
    Oct 3, 2019
    Posts:
    119
    Dumb question: Would it be legal to put the Entities package up on GitHub, and add the fix to a branch? What's Unity's position on that?
     
  7. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    (If you're not targetting IL2CPP), you can use reflection to get the MethodInfo then generate a delegate to internal methods. The MethodInfo reflection is slow, but that's just done once at startup, and the generated delegate is just as fast as a regular delegate (eg cost of a virtual method call).
     
  8. phreezie

    phreezie

    Joined:
    Oct 3, 2019
    Posts:
    119
    @burningmime Hmm but then I would need to extend
    BlobBuilder
    , right? Which it doesn't let me ("Cannot inherit from sealed struct 'BlobBuilder'").
     
  9. SINePrime

    SINePrime

    Joined:
    Jan 24, 2019
    Posts:
    54
    Sorry the necropost, but I think this is important for anyone who found this topic through search engines.

    You can use the StructLayoutAttribtue to explicitly set the size of a struct; This is seemingly respected by the BlobBuilder. With the attribute, you can create polymorphic structs in BlobAsset when combined with Unity.Collections.LowLevel.Unsafe.UnsafeUtility:

    Code (CSharp):
    1. using System.Runtime.InteropServices;
    2. using NUnit.Framework;
    3. using Unity.Collections;
    4. using Unity.Collections.LowLevel.Unsafe;
    5. using Unity.Entities;
    6.  
    7. [TestFixture]
    8. public class AssumptionsBlobAssetPolymorphism
    9. {
    10.   [StructLayout(LayoutKind.Sequential, Size = 8)]
    11.   public struct Foo
    12.   {
    13.     public int Value;
    14.   }
    15.  
    16.   [StructLayout(LayoutKind.Sequential)]
    17.   public struct Bar
    18.   {
    19.     public int Value;
    20.  
    21.     public int Value2;
    22.   }
    23.  
    24.   public struct BlobAssetHasFoo
    25.   {
    26.     public BlobPtr<Foo> Foo;
    27.   }
    28.  
    29.   [Test]
    30.   public void AssumeAllocateStructRespectsSize()
    31.   {
    32.     BlobAssetReference<BlobAssetHasFoo> refReferencesFoo;
    33.     using (BlobBuilder builder = new BlobBuilder(Unity.Collections.Allocator.Temp))
    34.     {
    35.       // create root
    36.       ref BlobAssetHasFoo blobAssetHasFoo = ref builder.ConstructRoot<BlobAssetHasFoo>();
    37.  
    38.       ref Foo foo = ref builder.Allocate(ref blobAssetHasFoo.Foo); // allocate Foo
    39.       ref Bar fooAsBar = ref UnsafeUtility.As<Foo, Bar>(ref foo); // reinterpret Foo as Bar
    40.       fooAsBar = new Bar() { Value = 1, Value2 = 2 }; // set Foo value into Bar struct
    41.  
    42.       refReferencesFoo = builder.CreateBlobAssetReference<BlobAssetHasFoo>(Allocator.Persistent);
    43.     }
    44.  
    45.     // test values are allocated
    46.     Assert.AreEqual(refReferencesFoo.Value.Foo.Value.Value, 1);
    47.     Assert.AreEqual(UnsafeUtility.As<Foo, Bar>(ref refReferencesFoo.Value.Foo.Value).Value2, 2);
    48.   }
    49. }
    50.  
    You don't necessarily need to use BlobPtr, I used it for my own purposes.
     
  10. phreezie

    phreezie

    Joined:
    Oct 3, 2019
    Posts:
    119
    @SINePrime Interesting, thanks! Not sure how that helps my case though. The
    Size
    parameter of
    StructLayout
    is set at compile time, while my struct's size is determined at runtime.
     
  11. SINePrime

    SINePrime

    Joined:
    Jan 24, 2019
    Posts:
    54
    True, my solution is a bit of a workaround; the "base" struct has to be larger than any other "derived" struct.

    So I thought about it, and I came up with this solution instead ;)
    Code (CSharp):
    1. using NUnit.Framework;
    2. using Unity.Collections;
    3. using Unity.Collections.LowLevel.Unsafe;
    4. using Unity.Entities;
    5.  
    6. [TestFixture]
    7. public class AssumptionsBlobAsset
    8. {
    9.   public struct BlobAsset
    10.   {
    11.     public BlobPtr<Foo> Pointer;
    12.   }
    13.  
    14.   public struct Foo
    15.   {
    16.  
    17.   }
    18.  
    19.   public struct Bar
    20.   {
    21.     public float RandomValue;
    22.   }
    23.  
    24.   [Test]
    25.   public void TestReinterpretAndAllocateBlobPtr()
    26.   {
    27.     float random = new System.Random().Next();
    28.  
    29.     BlobAssetReference<BlobAsset> blobAssetReference;
    30.     using (BlobBuilder builder = new BlobBuilder(Allocator.Temp))
    31.     {
    32.       ref BlobAsset blobAsset = ref builder.ConstructRoot<BlobAsset>();
    33.  
    34.       ref Bar barRef = ref builder.AllocateAs<Foo, Bar>(ref blobAsset.Pointer);
    35.       barRef.RandomValue = random;
    36.  
    37.       blobAssetReference = builder.CreateBlobAssetReference<BlobAsset>(Allocator.Persistent);
    38.     }
    39.  
    40.     Bar bar = blobAssetReference.Value.Pointer.ValueAs<Foo, Bar>();
    41.  
    42.     UnityEngine.Debug.Log($"{random} == {bar.RandomValue}? {random == bar.RandomValue}");
    43.  
    44.     Assert.AreEqual(bar.RandomValue, random);
    45.   }
    46. }
    47.  
    48. static public class BlobBuilderExtensions
    49. {
    50.   /// <summary>
    51.   /// Allocates a <see cref="BlobPtr{T}"/> via <see cref="BlobBuilder.Allocate{T}(ref BlobPtr{T})"/> as a <typeparamref name="U"/>.
    52.   /// </summary>
    53.   static public ref U AllocateAs<T, U>(this BlobBuilder thisBlobBuilder, ref BlobPtr<T> blobPtr)
    54.     where T : struct
    55.     where U : struct
    56.   {
    57.     ref BlobPtr<U> pointerAsU = ref UnsafeUtility.As<BlobPtr<T>, BlobPtr<U>>(ref blobPtr);
    58.     return ref thisBlobBuilder.Allocate(ref pointerAsU);
    59.   }
    60.  
    61.   /// <summary>
    62.   /// Reinterprets <see cref="BlobPtr{T}.Value"/> as type <typeparamref name="U"/>.
    63.   /// </summary>
    64.   static public ref U ValueAs<T, U>(ref this BlobPtr<T> thisBlobPtr) where T : struct where U : struct
    65.   {
    66.     ref BlobPtr<U> pointerAsU = ref UnsafeUtility.As<BlobPtr<T>, BlobPtr<U>>(ref thisBlobPtr);
    67.     return ref pointerAsU.Value;
    68.   }
    69. }
    70.  
     
  12. phreezie

    phreezie

    Joined:
    Oct 3, 2019
    Posts:
    119
    But your
    Foo
    still has a static size? What I'd like to do is allocate a struct with an array of arbitrarily long data.
     
  13. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    Revisiting this, is there a reason your custom collider type doesn't have its own BlobArray<float3>? The collider type would then be fixed sized, because a BlobArray field is just a pointer and a count into more blob data, and you can nest these things to hardware limits.

    Beyond that though, I can confirm that the asmref trick works to access internals, and I use it in packages now: https://github.com/Dreaming381/Latios-Framework/tree/v0.3.1/EntitiesExposed

    But it really surprises me that you can't make the BlobArrays work, as I have been able to trick the C# to do a few weird things with BlobArrays without having to add to the assembly.
     
  14. phreezie

    phreezie

    Joined:
    Oct 3, 2019
    Posts:
    119
    @DreamingImLatios Good point, I think I've tried that and it somehow didn't work. I have meanwhile moved on and triangulated this collider, resulting in a number of fixed-size colliders. I might have another try at it.