Issue with using EntityCommandBuffer within SystemBase

So let me preface this entire thing by stating that I have no idea what the hell I am doing.

Okay, so I am attempting to create an Octree system within my game that creates and octree with leaf nodes. Each leaf node is a chunk that must be meshed based on the voxels that fall within its bounds.

Now with that context, let me explain my actual issue.

My job acts on entities that contain the “octreeNeedsInit” component attached to them. So one might thing: “Hey, chuck a ForEach in there and you’re good”. Well, I have reservations about doing this because there may be multiple octrees within the player’s view distance due to there being multiple voxel bodies (like a moon within proximity of the planet the player is on). Because of this, the processing time of actually constructing these octrees may go over the time I want each frame to fall within.

Here is my first question: Does Entities.ForEach limit the number of iterations it does based on how long it takes each iteration to execute and then save the next iterations for the next frame?

After creating a Job that fills a Hashmap with octree nodes and adds leaf nodes to the entitie’s dynamic buffer within the SystemBase class, I want to remove the component “octreeNeedsInit” so that I don’t keep adding to the Hashmap. However, I keep getting this error:

ArgumentException: The previously scheduled job OctreeTestSystem:removeNeedsInit writes to the Unity.Entities.EntityCommandBuffer removeNeedsInit.ecb. You must call JobHandle.Complete() on the job OctreeTestSystem:removeNeedsInit, before you can write to the Unity.Entities.EntityCommandBuffer safely.
EntityCommandBuffer was recorded in OctreeTestSystem and played back in Unity.Entities.EndSimulationEntityCommandBufferSystem.
  at (wrapper managed-to-native) Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckWriteAndThrowNoEarlyOut_Injected(Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle&)
  at Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckWriteAndThrowNoEarlyOut (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) [0x00000] in <fdd4f5823e2a41e8be8d5dcbd0bfd5b1>:0
  at Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckWriteAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) [0x00021] in <fdd4f5823e2a41e8be8d5dcbd0bfd5b1>:0
  at Unity.Entities.EntityCommandBuffer.EnforceSingleThreadOwnership () [0x00015] in C:\Users\mattb\Desktop\Radon Unity Tests\Library\PackageCache\com.unity.entities@0.11.0-preview.7\Unity.Entities\EntityCommandBuffer.cs:893
  at Unity.Entities.EntityCommandBuffer.PlaybackInternal (Unity.Entities.EntityDataAccess* mgr) [0x00000] in C:\Users\mattb\Desktop\Radon Unity Tests\Library\PackageCache\com.unity.entities@0.11.0-preview.7\Unity.Entities\EntityCommandBuffer.cs:1275
  at Unity.Entities.EntityCommandBuffer.Playback (Unity.Entities.EntityManager mgr) [0x00000] in C:\Users\mattb\Desktop\Radon Unity Tests\Library\PackageCache\com.unity.entities@0.11.0-preview.7\Unity.Entities\EntityCommandBuffer.cs:1260
  at Unity.Entities.EntityCommandBufferSystem.FlushPendingBuffers (System.Boolean playBack) [0x0004b] in C:\Users\mattb\Desktop\Radon Unity Tests\Library\PackageCache\com.unity.entities@0.11.0-preview.7\Unity.Entities\EntityCommandBufferSystem.cs:220

Unity.Entities.EntityCommandBufferSystem.FlushPendingBuffers (System.Boolean playBack) (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/EntityCommandBufferSystem.cs:286)
Unity.Entities.EntityCommandBufferSystem.OnUpdate () (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/EntityCommandBufferSystem.cs:188)
Unity.Entities.ComponentSystem.Update () (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/ComponentSystem.cs:109)
Unity.Entities.ComponentSystemGroup.UpdateAllSystems () (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/ComponentSystemGroup.cs:445)
UnityEngine.Debug:LogException(Exception)
Unity.Debug:LogException(Exception) (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/Stubs/Unity/Debug.cs:19)
Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/ComponentSystemGroup.cs:450)
Unity.Entities.ComponentSystemGroup:OnUpdate() (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/ComponentSystemGroup.cs:398)
Unity.Entities.ComponentSystem:Update() (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/ComponentSystem.cs:109)
Unity.Entities.DummyDelegateWrapper:TriggerUpdate() (at Library/PackageCache/com.unity.entities@0.11.0-preview.7/Unity.Entities/ScriptBehaviourUpdateOrder.cs:192)

The same error occurs when it comes to the DynamicBuffer whenever I don’t use the second job to remove the component.

Here is my code:

using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using static Unity.Mathematics.math;

public class OctreeTestSystem : SystemBase
{
    NativeHashMap<ulong, OctreeNode> octreeStructure;
   
    NativeArray<int3> nodeOffsets;


    const int maxDepth = 7;

    const int rootNodeSize = 128;

    const int chunkSize = 64;

    private EntityQuery octreeQuery;
   
    int3[] offsets =
    {
        int3(0),
        int3(0, 0, 1),
        int3(0, 1, 0),
        int3(0, 1, 1),
        int3(1, 0, 0),
        int3(1, 0, 1),
        int3(1, 1, 0),
        int3(1, 1, 1)

    };

    protected override void OnCreate()
    {
        int hashMapSize = (int)((pow(8, maxDepth + 1) - 1) / 7);

        octreeStructure = new NativeHashMap<ulong, OctreeNode>(hashMapSize, Allocator.Persistent);
        nodeOffsets = new NativeArray<int3>(8, Allocator.Persistent);
        octreeQuery = EntityManager.CreateEntityQuery(typeof(octreeNeedsInit), typeof(octreeNodeBufferElement));
       
        nodeOffsets.CopyFrom(offsets);
    }

    protected override void OnUpdate()
    {


        if (octreeQuery.CalculateEntityCount() == 0)
        {
            return;
        }
       
       
        NativeArray<Entity> octrees = octreeQuery.ToEntityArray(Allocator.Persistent);
        Entity octree = octrees[0]; //TODO: Distance prioritization
        octrees.Dispose();
       
       
       
        octreeStructure.Clear();
       
       
       
        FillLinearOctreeJob fillOctreeJob = new FillLinearOctreeJob
        {
            maxDepth = maxDepth,
            rootNodeSize = rootNodeSize,
            octreeStructure = octreeStructure,
            leafNodes = EntityManager.GetBuffer<octreeNodeBufferElement>(octree),
            nodeOffsets = nodeOffsets,
            chunkSize = chunkSize
        };

        JobHandle fillOctreeHandle = fillOctreeJob.Schedule(this.Dependency);

        removeNeedsInit removeNeedsInitJob = new removeNeedsInit
        {
            ecb = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>().CreateCommandBuffer(),
            entity = octree
        };

        JobHandle removeNeedsInitHandle = removeNeedsInitJob.Schedule(fillOctreeHandle);
       
        //EntityManager.RemoveComponent(octree, typeof(octreeNeedsInit));
       
        this.Dependency = removeNeedsInitHandle;


    }


    protected override void OnDestroy()
    {
        octreeStructure.Dispose();
       
        nodeOffsets.Dispose();
    }

    struct removeNeedsInit : IJob
    {
        public EntityCommandBuffer ecb;
        public Entity entity;
       
        public void Execute()
        {
            ecb.RemoveComponent(entity, typeof(octreeNeedsInit));
           
        }
    }
}

And:

using Unity.Mathematics;
using Unity.Jobs;
using Unity.Collections;

using Unity.Burst;
using Unity.Entities;
using UnityEngine;
using static Unity.Mathematics.math;

[BurstCompile]
public struct FillLinearOctreeJob : IJob
{

    [WriteOnly] public NativeHashMap<ulong, OctreeNode> octreeStructure;
   
   
    [ReadOnly] public NativeArray<int3> nodeOffsets;

    [WriteOnly] public DynamicBuffer<octreeNodeBufferElement> leafNodes;

    public int maxDepth;
    public int rootNodeSize;
    public int chunkSize;

   

    public void Execute()
    {

        ulong rootNodeCode = 1;
        OctreeNode node;
        node.locCode = rootNodeCode;
        node.hasChild = 1;

        addNode(node, float3(0, 0, 0), rootNodeSize);

       
    }

    int getNodeTreeDepth(ulong locCode)
    {
        return (int)log2(locCode)/3;
       
       
       
    }


    void addNode(OctreeNode node, float3 pos, int size)
    {
        octreeStructure.Add(node.locCode, node);
        shouldDivide(ref node, float3(4096, 4096, 4096), pos, size);

        if ((getNodeTreeDepth(node.locCode) < maxDepth) && (node.hasChild == 1))
        {
            for(uint i = 0; i < 8; i++)
            {
                int newSize = size / 2;
                addNode(createChildNode(node.locCode, i), pos + (newSize * nodeOffsets[(int)i] * chunkSize), newSize);

            }
        }
        else
        {
            octreeNodeBufferElement leafNode;
            leafNode.value = node;
            leafNodes.Add(leafNode);
        }
       
    }

    OctreeNode createChildNode(ulong parentLocCode, uint child)
    {
        ulong childLocCode = (parentLocCode << 3) + child;

        OctreeNode childNode;
        childNode.locCode = childLocCode;
        childNode.hasChild = 0;
        return childNode;


    }


    void shouldDivide(ref OctreeNode node, float3 cameraPos, float3 nodePos, int nodeSize)
    {

        float3 octreeMiddle = nodePos + float3((nodeSize / 2) * chunkSize);

        float d = distance(cameraPos, nodePos + octreeMiddle);

        if(d < (chunkSize * nodeSize))
        {
            node.hasChild = 1;
        } else
        {
            node.hasChild = 0;
        }


    }



}

To end this post, thank you for being such a great forum! I really do appreciate the people who camp this forum and answers people’s questions.

You may have forgotten to call AddJobHandleForProducer().

var commandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();

removeNeedsInit removeNeedsInitJob = new removeNeedsInit {
    ecb = commandBufferSystem.CreateCommandBuffer(),
    entity = octree
};
JobHandle removeNeedsInitHandle = removeNeedsInitJob.Schedule(fillOctreeHandle);

commandBufferSystem.AddJobHandleForProducer(removeNeedsInitHandle);
1 Like

You, good sir, are the goat. I can’t believe I forgot to do this! Thank you so much!

In terms of how I setting this System up, are there any improvements in terms of structure that could maybe make things run how Unity intended?

How you did it is fine. Here are some few nitpicks:

  • You might want to cache EndSimulationEntityCommandBufferSystem in OnCreate() so you don’t call World.GetOrCreateSystem() on every frame.
  • That lone octree entity could probably be implemented as a singleton instead.

I will definitely cache the Ecb. However, the lone entity is just a test at the moment. There will be multiple octrees in the future. This is why I clear the hashmap in order to make room for the new octree being constructed (after this, the leaf nodes are passed to the octree entity for the next step, which is generating voxel data and then meshing). Thank you for the tips!

Hey, I passed by this forum because i ran into the same issue.

Is the octree implementation for voxel data faster then a byte array? (Currently using byte arrays)

Also, damn ECS is mad fast. I’m getting so much results already. Then I switched my systems to use command buffers. And wow.

On a side note, is there any way to push mesh data in a job? Or Use cameras in pure ecs yet? :slight_smile:

Camera’s should not use Convert and Destroy, but instead use Convert and Inject. So unfortunately, no, cameras cannot be read from/written to inside of a jobified/bursted Entities.ForEach. You might be better off working on other aspects of your game project while waiting for other Unity features to join ECS. For example, the Unity.Physics package is quite mature and you won’t lose much work to future breaking changes by working on your game’s physics

Not true. Add HYBRID_ENTITIES_CAMERA_CONVERSION to your Scripting Define Symbols.

1 Like