Search Unity

Question Best way to Dispose Mesh.MeshDataArray after job completion?

Discussion in 'Entity Component System' started by joshrs926, Jul 13, 2022.

  1. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    115
    I use a MeshDataArray to read (not write) mesh vertices inside a job. The problem is that there isn't an overload of the Dispose method for it that takes a JobHandle argument so that I can schedule its Disposal upon the job completion.
    It looks like there's currently 3 options for disposing a Mesh.MeshDataArray when using it with a job:
    1. Check each frame for job completion and manually dispose the MeshDataArray upon job completion.
    2. Force the job to complete with JobHandle.Complete() in the same frame then manually dispose the MeshDataArray.
    3. Copy all needed mesh data such as vertices and indices into new arrays and manually dispose MeshDataArray before scheduling the job, then call Dispose(JobHandle)() on the new arrays before completing the job.
    Option 3 is the closest to what I want but the problem with it is that it requires allocating and copying over new arrays which is work that ideally could be skipped since the data is already available inside the MeshDataArray if you use the appropriate GetData methods which don't allocate new arrays. So my question is this: Is there any way to schedule a Mesh.MeshDataArray to be disposed upon job completion?

    (Edited to add in option 3.)
     
    Last edited: Jul 13, 2022
  2. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Copy NativeArray with required data. Pass it to the job. Dispose that collection after job completion instead.

    MeshDataArray can be disposed after copy just fine. (no need to wait for job completion)
     
  3. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    115
    If you mean copying the vertices and indices into new arrays, you're right that is another option and is what I'm doing now. I'll update my post to add that as an option. The problem there is that requires an allocation of new arrays and copying the data over. It'd be faster to just use the meshDataArray and call the GetData methods for vertices and indices since those arrays are already allocated. One other idea would be to create a NativeArray<Mesh.MeshData> and copy in the MeshDatas and then dispose of the MeshDataArray before scheduling the job, but when I tried that I get an error. I suppose somehow the MeshDatas are tied to the MeshDataArray and they know there parent MeshDataArray has been disposed. Here is some code to demonstrate:

    This works fine:
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using Unity.Collections;
    4. using Unity.Jobs;
    5. using Unity.Burst;
    6. public class MeshTest : MonoBehaviour
    7.     {    
    8.         public Mesh mesh;
    9.  
    10.         public void Start()
    11.         {
    12.             var mArray = Mesh.AcquireReadOnlyMeshData(mesh);
    13.             var job = new TestJob()
    14.             {
    15.                 data = new NativeArray<Mesh.MeshData>(1, Allocator.TempJob)
    16.             };
    17.             job.data[0] = mArray[0];
    18.             var handle = job.Schedule();
    19.             job.data.Dispose(handle);
    20.             handle.Complete();
    21.             mArray.Dispose();
    22.         }
    23.  
    24.         [BurstCompile]
    25.         public struct TestJob : IJob
    26.         {
    27.             public NativeArray<Mesh.MeshData> data;
    28.             public void Execute()
    29.             {
    30.                 Debug.Log(data[0].vertexCount);
    31.             }
    32.         }
    33.     }
    But then if you swap handle.Complete(); and mArray.Dispose(); then you get this error:
    InvalidOperationException: The UNKNOWN_OBJECT_TYPE has been declared as [WriteOnly] in the job, but you are reading from it.
    Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrowNoEarlyOut (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at <4a31731933e0419ca5a995305014ad37>:0)
    Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at <4a31731933e0419ca5a995305014ad37>:0)
    UnityEngine.Mesh+MeshData.CheckReadAccess () (at <4a31731933e0419ca5a995305014ad37>:0)
    UnityEngine.Mesh+MeshData.get_vertexCount () (at <4a31731933e0419ca5a995305014ad37>:0)
    MeshTest+TestJob.Execute () (at Assets/MeshTest.cs:30)
    Unity.Jobs.IJobExtensions+JobStruct`1[T].Execute (T& data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at <4a31731933e0419ca5a995305014ad37>:0)
    Unity.Jobs.JobHandle:ScheduleBatchedJobsAndComplete(JobHandle&)
    Unity.Jobs.JobHandle:Complete()
    MeshTest:Start() (at Assets/MeshTest.cs:21)
     
    Last edited: Jul 13, 2022
  4. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    115
  5. Fribur

    Fribur

    Joined:
    Jan 5, 2019
    Posts:
    136
    Maybe silly question: do you need to dispose it? As far as I recall (have not looked into this recently) AquireReadOnlyMeshData just gets pointer to existing memory, so you would not want to dispose that?
     
  6. Elapotp

    Elapotp

    Joined:
    May 14, 2014
    Posts:
    98
    Well, for such things I wrote EndOfFrameDisposeSystem.

    [UpdateInGroup(typeof(InitializationSystemGroup))]
    [UpdateBefore(typeof(SyncMeshVerticesIntoBufferECBS))]
    public partial class SyncMeshVerticesIntoBufferSystem : SystemBase
    {
    [Inject] private SyncMeshVerticesIntoBufferECBS _ecbs;
    [Inject] private EndOfFrameDisposeSystem _endOfFrameDisposeSystem;
    private EntityQuery _query;

    protected override void OnCreate()
    {
    base.OnCreate();
    _query = GetEntityQuery(this.R<SyncMeshVerticesData>());
    }

    protected override void OnUpdate()
    {
    var syncMeshVerticesDatas = _query.ToComponentDataArray<SyncMeshVerticesData>();
    var meshes = syncMeshVerticesDatas.Select(d => d.Mesh).ToArray();
    var jobHandles = new NativeArray<JobHandle>(meshes.Length, Allocator.Temp);
    var meshDataArray = Mesh.AcquireReadOnlyMeshData(meshes);

    for (int i = 0; i < meshDataArray.Length; i += 1)
    {
    var syncData = syncMeshVerticesDatas[i];
    var tr = syncData.Transform;

    jobHandles[i] = new ConvertLocalToWorldJob
    {
    ECB = _ecbs.CreateCommandBuffer(),
    BufferEntity = syncData.BufferEntity,
    MeshData = meshDataArray[i],
    Position = tr.position,
    Rotation = tr.rotation,
    Scale = tr.localScale,
    }.Schedule(Dependency);
    }

    Dependency = JobHandle.CombineDependencies(jobHandles);

    _ecbs.AddJobHandleForProducer(Dependency);
    jobHandles.Dispose();
    _endOfFrameDisposeSystem.RegisterThisFrame(meshDataArray);
    }
    }
    [AlwaysUpdateSystem]
    [UpdateInGroup(typeof(PresentationSystemGroup), OrderLast = true)]
    public partial class EndOfFrameDisposeSystem : SystemBase
    {
    private readonly List<IDisposable> _disposables = new();

    public void RegisterThisFrame(params IDisposable[] disposable)
    {
    _disposables.AddRange(disposable);
    }

    protected override void OnUpdate()
    {
    for (int i = 0; i < _disposables.Count; i += 1)
    {
    _disposables[i].Dispose();
    }
    _disposables.Clear();
    }
    }
     
  7. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    115
    Thanks for this. I was thinking of doing something like this though I'm not using Entities. I'm just using the Jobs, Burst, Mathematics, and Collections packages. I was considering writing some static class that updates on EditorApplication.update or perhaps an async method that gets run every x milliseconds where I can register a JobHandle to it and it will check if it's complete then dispose of whatever I want. For now, though, I'm just copying mesh data to UnsafeLists because there is also the issue of the index buffers and vertex buffers being in odd formats. I need to be able to handle any arbitrary mesh inputted into my system. So I'm using Mesh.MeshData.GetIndices and Mesh.MeshData.GetVertices which handle validating the data formats and make copies anyway.