Need help refactoring a simple mesh heightfield generator w/ proper use of DOTS stack

Howdy.

I am having issues wrapping my head around how to structure my code utilizing entities, jobs and burst. I feel like it’s possible to do the following:
Given a user authored value for how big a heightfield should be
Off of the main thread
Utilize burst so that each vertex in the field gets it’s height value calculated in parallel w/ Unity’s math.noise function
Utilize burst to generate the triangle indices for a regular triangle strip in parallel
After the previous two jobs have finished, copy the data out of a dynamics buffer (or native array) and into a RenderMesh
Only execute the above once, I will eventually want to dynamically add in a Tiling concept where I will want to add/remove this type of mesh on the fly as the character moves around

I’m really just after the most efficient way to generate a regular triangle network heightfield, so it’s possible my line of thinking is off above.

Here is my naive approach that forces me to use WithoutBurst & Run accordingly and outputs the mesh as expected. I’ve failed to break this code into what I am guessing is the correct flow I outlined previously.

    using UnityEngine;
    using Unity.Entities;
    using Unity.Mathematics;
    using System;

    [AddComponentMenu("OH/NoiseSeed")]
    public class NoiseSeedAuthoring : MonoBehaviour, IConvertGameObjectToEntity
    {
        public int Seed = 1337; // todo actually use this value
        public int Size = 10;
        public Material Material;

        public void Convert(Entity entity, EntityManager entityManager, GameObjectConversionSystem conversionSystem)
        {
            entityManager.AddComponentData(entity, new NoiseSeed
            {
                Seed = Seed,
                Size = Size,
                Loaded = false
            });
            entityManager.AddSharedComponentData(entity, new SimpleMeshRenderer
            {
                Mesh = new Mesh(),
                Material = Material
            });
        }
    }


    public struct NoiseSeed : IComponentData
    {
        public int Seed;
        public int Size;
        public bool Loaded; // refactor out
    }

    public struct SimpleMeshRenderer : ISharedComponentData, IEquatable<SimpleMeshRenderer>
    {
        public Mesh Mesh;
        public Material Material;

        // todo
        public bool Equals(SimpleMeshRenderer other)
        {
            return false;
        }

        // todo https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-5.0
        public override int GetHashCode()
        {
            return 1;
        }
    }

    public class NoiseBuilderSystem : SystemBase
    {
        private EntityQuery _query;

        protected override void OnCreate()
        {
            base.OnCreate();
            var queryDescription = new EntityQueryDesc
            {
                All = new ComponentType[]
                {
                    ComponentType.ReadWrite<NoiseSeed>(),
                    ComponentType.ReadWrite<SimpleMeshRenderer>(),
                }
            };
            _query = GetEntityQuery(queryDescription);
        }

        protected override void OnUpdate()
        {
            Entities
                .WithStoreEntityQueryInField(ref _query)
                .WithoutBurst() // todo refactor to WithBurst
                .ForEach((ref NoiseSeed seed, in SimpleMeshRenderer meshRenderer) =>
                {
                    if(!seed.Loaded)
                    {
                        LoadSeed(ref seed, in meshRenderer);
                    }
                  
                    Graphics.DrawMesh(meshRenderer.Mesh, new Vector3(), Quaternion.identity, meshRenderer.Material, 0);
                })
                .Run(); // todo refactor to ScheduleParallel()?
        }

        private static void LoadSeed(ref NoiseSeed noiseSeed, in SimpleMeshRenderer meshRenderer)
        {
            // verts, I expect this can be burst'd
            var rowSize = noiseSeed.Size;
            var colSize = noiseSeed.Size;
            var vertices = new Vector3[noiseSeed.Size * noiseSeed.Size];
            for (int row = 0, index = 0; row < rowSize; row++)
            {
                for (int col = 0; col < noiseSeed.Size; col++, index++)
                {
                    var heightPoint = new float2(row, col);
                    float height = noise.snoise(heightPoint);
                    vertices[index] = new Vector3(row, height, col);
                }
            }

            // tris, I expect this can be burst'd
            var quadRowSize = rowSize - 1;
            var quadColSize = colSize - 1;
            var triangles = new int[(quadRowSize * quadColSize) * (2*3)];
            for (int quadRow = 0, triangleIndex = 0; quadRow < quadRowSize; quadRow++)
            {
                for (int quadCol = 0; quadCol < quadColSize; quadCol++, triangleIndex += 6)
                {
                    var topLeft = (quadRow * rowSize) + quadCol;
                    var topRight = (quadRow * rowSize) + (quadCol + 1);
                    var bottomLeft = ((quadRow + 1) * rowSize) + quadCol;
                    var bottomRight = ((quadRow + 1) * rowSize) + (quadCol + 1);

                    triangles[triangleIndex] = topLeft; // 1st triangle
                    triangles[triangleIndex + 1] = topRight;
                    triangles[triangleIndex + 2] = bottomLeft;
                    triangles[triangleIndex + 3] = topRight; // 2nd triangle
                    triangles[triangleIndex + 4] = bottomRight;
                    triangles[triangleIndex + 5] = bottomLeft;
                }
            }

            // mesh, I presume this is where I would wait for the previous two handles to finish and perform a copy. I'm also confused on how to translate from my entity's DynamicBuffer<float3> to the Mesh's verticies Vector3[]
            meshRenderer.Mesh.vertices = vertices;
            meshRenderer.Mesh.triangles = triangles;

           
            meshRenderer.Mesh.RecalculateBounds(); // I believe this can just be set directly with a constant extent {.5, 1 .5}
            meshRenderer.Mesh.RecalculateNormals(); // I eventually plan on calculating the Normals in a similar manner / at the same time as the vertex's
            meshRenderer.Mesh.RecalculateTangents();

            // cache
            noiseSeed.Loaded = true;
        }
    }
}

And here is where I’m currently left off scratching my head in the refactor.

  public struct VertexBufferElement : IBufferElementData
    {
        public float3 Value;
    }

    public struct TriangleIndexBufferElement : IBufferElementData
    {
        public int Value;
    }


// .. system update function:


var commandBuffer = _entityCommandBufferSystem
                .CreateCommandBuffer()
                .AsParallelWriter();

            var spawnTileJobHandle = Entities
                .WithName("SpawnTile")
                .WithBurst(FloatMode.Default, FloatPrecision.Standard, true)
                .ForEach((Entity entity, int entityInQueryIndex, in NoiseSeed noiseSeed) =>
                {
                    var vertexBuffer = commandBuffer.AddBuffer<VertexBufferElement>(entityInQueryIndex, entity);
                    vertexBuffer.Length = noiseSeed.Size * noiseSeed.Size;

                    var rowSize = noiseSeed.Size;
                    var colSize = noiseSeed.Size;
                    for (int row = 0, index = 0; row < rowSize; row++)
                    {
                        for (int col = 0; col < noiseSeed.Size; col++, index++)
                        {
                            var heightPoint = new float2(row, col);
                            float height = noise.snoise(heightPoint);
                            vertexBuffer[index] = new VertexBufferElement
                            {
                                Value = new float3 (row, height, col)
                            };
                        }
                    }
                }).Schedule(Dependency);

            spawnTileJobHandle.Complete();

            Entities
                .ForEach((DynamicBuffer<VertexBufferElement> vertexBuffer, ref SimpleMeshRenderer meshRenderer) =>
                {
                    meshRenderer.Mesh = new Mesh
                    {
                        vertices = new Vector3[vertexBuffer.Length]
                    };
                    for(int i = 0; i < vertexBuffer.Length; i++)
                    {
                        meshRenderer.Mesh.vertices* = new Vector3(vertexBuffer*.Value.x, vertexBuffer*.Value.y, vertexBuffer*.Value.z);
                    }
                });

I’ve had similar project some time ago and here is what I come up with (more of less). Preview image shows 900 meshes with no seams between them (rather important).

preview
_
NOTE: RenderMesh is part of com.unity.rendering.hybrid package.

// src: https://gist.github.com/andrew-raphael-lukasik/104e5826015c2f4d47021ba564e9bfe8
// REQUIRES: com.unity.rendering.hybrid package
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using Unity.Entities;
using Unity.Rendering;
using Unity.Collections;
using Unity.Transforms;
using Unity.Jobs;

public class NoiseFieldAuthoring : MonoBehaviour
{
	[SerializeField] Material _material = null;
	[SerializeField] int2 _cells = new int2{ x=3 , y=5 };
	[SerializeField] float3 _size = new float3{ x=100 , y=0.1f , z=100 };
	[SerializeField][Range(2,256)] int _subdivision = 10;
	[SerializeField] float2 _noiseOrigin = 0;
	void OnEnable ()
	{
		var commander = World.DefaultGameObjectInjectionWorld.EntityManager;
		var archetype = commander.CreateArchetype(
				typeof(NoiseFieldSegment)
			,	typeof(LocalToWorld)
			,	typeof(RenderMesh)
			,	typeof(RenderBounds)
			,	typeof(WorldRenderBounds)
			,	ComponentType.ChunkComponent<ChunkWorldRenderBounds>()

			,	typeof(DIRTY)
		);

		float3 transformPosition = transform.position;
		for( int x=0 ; x<_cells.x ; x++ )
		for( int y=0 ; y<_cells.y ; y++ )
		{
			float3 cellPos = new float3{ x=x*_size.x/_subdivision , z=y*_size.z/_subdivision };
			float3 worldPosition = transformPosition + cellPos;
			Entity entity = commander.CreateEntity( archetype );
			commander.SetComponentData( entity , new NoiseFieldSegment{
				NoiseOrigin		= _noiseOrigin + new float2{ x=cellPos.x , y=cellPos.z } ,
				Subdivision		= _subdivision ,
				Size			= _size ,
			} );
			var mesh = new Mesh();
			mesh.name = $"mesh {entity}";
			commander.SetSharedComponentData( entity , new RenderMesh{
				mesh		= mesh ,
				material	= _material
			} );
			commander.SetComponentData( entity , new LocalToWorld{
				Value = float4x4.Translate( worldPosition )
			} );
			commander.SetComponentData<RenderBounds>( entity , new RenderBounds{ Value = new AABB{ Center=_size/2f , Extents=_size } } );
		}
	}
}

public struct DIRTY : IComponentData {}
public struct NoiseFieldSegment : IComponentData
{
	public float2 NoiseOrigin;
	public int Subdivision;
	public float3 Size;
}

[UpdateInGroup( typeof(InitializationSystemGroup) )]
public class NoiseFieldSegmentSystem : SystemBase
{
	EntityQuery _query;
	EndSimulationEntityCommandBufferSystem _endSimulationEcbSystem;
	NativeList<JobHandle> batches = new NativeList<JobHandle>( Allocator.Persistent );
	Queue<(Mesh mesh, NativeArray<float3> vertices,NativeArray<float3>normals,NativeArray<int> indices)> _output = new Queue<(Mesh,NativeArray<float3>,NativeArray<float3>,NativeArray<int>)>( 10 );
	protected override void OnCreate ()
	{
		_query = EntityManager.CreateEntityQuery(
				ComponentType.ReadOnly<NoiseFieldSegment>()
			,	ComponentType.ReadOnly<RenderMesh>()
			,	ComponentType.ReadOnly<DIRTY>()
		);
		_endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
	}
	protected override void OnDestroy ()
	{
		if( batches.IsCreated ) batches.Dispose();
		Dependency.Complete();
		while( _output.Count!=0 )
		{
			var output = _output.Dequeue();
			output.vertices.Dispose();
			output.indices.Dispose();
			output.normals.Dispose();
		}
	}
	protected override void OnUpdate ()
	{
		// apply work results:
		while( _output.Count!=0 )
		{
			var output = _output.Dequeue();
			var mesh = output.mesh;
			mesh.SetVertices( output.vertices );
			mesh.SetIndices( output.indices , MeshTopology.Triangles , 0 );
			mesh.SetNormals( output.normals );
			output.vertices.Dispose();
			output.indices.Dispose();
			output.normals.Dispose();

			#if DEBUG
			Debug.Log($"{mesh.name} - updated");
			#endif
		}

		// schedule work:
		if( _query.CalculateEntityCount()!=0 )
		{
			var cmd = _endSimulationEcbSystem.CreateCommandBuffer();
			var entities = _query.ToEntityArray( Allocator.Temp );
			var fieldData = GetComponentDataFromEntity<NoiseFieldSegment>( isReadOnly:true );
			foreach( Entity entity in entities.Slice() )
			{
				var mesh = EntityManager.GetSharedComponentData<RenderMesh>( entity ).mesh;
				var fieldSegment = fieldData[ entity ];
				int subdivision = fieldSegment.Subdivision;
				float3 scale = fieldSegment.Size / new float3{ x=subdivision-1 , y=1 , z=subdivision-1 };
				float2 noiseOrigin = fieldSegment.NoiseOrigin;

				int numVertices = subdivision*subdivision;
				int numTriangles = (subdivision-1)*(subdivision-1)*2;
				int numIndices = numTriangles*3;
				int numNormals = numVertices;
				NativeArray<float3> vertices = new NativeArray<float3>( numVertices , Allocator.TempJob , NativeArrayOptions.UninitializedMemory );
				NativeArray<float3> normals = new NativeArray<float3>( numNormals , Allocator.TempJob , NativeArrayOptions.ClearMemory );
				NativeArray<int> indices = new NativeArray<int>( numIndices , Allocator.TempJob , NativeArrayOptions.UninitializedMemory );
				
				var verticesJobHandle =  new VerticesJob{
					Width			= subdivision ,
					Scale			= scale ,
					NoiseOrigin		= noiseOrigin ,
					Vertices		= vertices
				}.Schedule( numVertices , 32 );
				var trianglesJobHandle = new TrianglesJob{
					Width		= subdivision ,
					Indices		= indices
				}.Schedule();
				var normalsJobHandle = new NormalsJob{
					Width			= subdivision ,
					Scale			= scale ,
					NoiseOrigin		= noiseOrigin ,
					Normals			= normals
				}.Schedule( numNormals , 32 , JobHandle.CombineDependencies(verticesJobHandle,trianglesJobHandle) );

				batches.Add( JobHandle.CombineDependencies( verticesJobHandle , normalsJobHandle ) );
				_output.Enqueue( ( mesh:mesh , vertices:vertices , normals:normals , indices:indices ) );
				cmd.RemoveComponent<DIRTY>( entity );

				#if DEBUG
				Debug.Log($"{mesh.name} - jobs scheduled");
				#endif
			}
			entities.Dispose();
			Dependency = JobHandle.CombineDependencies( batches );
			batches.Clear();

			_endSimulationEcbSystem.AddJobHandleForProducer( Dependency );
		}
	}
}

[Unity.Burst.BurstCompile]
public struct VerticesJob : IJobParallelFor
{
	public int Width;
	public float3 Scale;
	public float2 NoiseOrigin;
	[WriteOnly] public NativeArray<float3> Vertices;
	void IJobParallelFor.Execute ( int index )
	{
		Vertices[index] = Vertex(
			index2:			Index1dTo2d(index,Width) ,
			widthHeight:	new int2{ x=Width , y=Width } ,
			areaScale:		Scale ,
			noiseOrigin:	NoiseOrigin
		);
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	public static float3 Vertex ( int2 index2 , int2 widthHeight , float3 areaScale , float2 noiseOrigin )
	{
		float2 xy = (float2)index2 / (float2)widthHeight * new float2{ x=areaScale.x , y=areaScale.z };
		float height = noise.snoise( xy + noiseOrigin );
		return new float3{ x=xy.x , y=height*areaScale.y , z=xy.y };
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	public static int2 Index1dTo2d ( int index , int width ) => new int2{ x=index%width , y=index/width };
}

[Unity.Burst.BurstCompile]
public struct TrianglesJob : IJob
{
	public int Width;
	[NativeDisableParallelForRestriction][WriteOnly] public NativeArray<int> Indices;
	void IJob.Execute ()
	{
		int width = Width, height = Width;
		int numQuadRows = width - 1, numQuadCols = height - 1;
		for( int Y=0, triangleIndex=0 ; Y<numQuadRows ; Y++ )
		for( int X=0 ; X<numQuadCols ; X++)
		{
			int topLeft = ((Y + 1) * width) + X;
			int topRight = ((Y + 1) * width) + (X + 1);
			int bottomLeft = (Y * width) + X;
			int bottomRight = (Y * width) + (X + 1);
			
			Indices[triangleIndex++] = topLeft;
			Indices[triangleIndex++] = topRight;
			Indices[triangleIndex++] = bottomLeft;

			Indices[triangleIndex++] = topRight;
			Indices[triangleIndex++] = bottomRight;
			Indices[triangleIndex++] = bottomLeft;
		}
	}
}

[Unity.Burst.BurstCompile]
public struct NormalsJob : IJobParallelFor
{
	public int Width;
	public float3 Scale;
	public float2 NoiseOrigin;
	[WriteOnly] public NativeArray<float3> Normals;
	void IJobParallelFor.Execute ( int index )
	{
		Normals[index] = NoiseFieldNormal( VerticesJob.Index1dTo2d( index , Width ) );
	}
	float3 NoiseFieldNormal ( int2 i2 )
	{
		int2 i2x1 = i2 + new int2{ x=-1 };
		int2 i2x2 = i2 + new int2{ x=1 };
		int2 i2y1 = i2 + new int2{ y=1 };
		int2 i2y2 = i2 + new int2{ y=-1 };

		float3 vx1 = VerticesJob.Vertex(
			index2:			i2x1 ,
			widthHeight:	new int2{ x=Width , y=Width } ,
			areaScale:		Scale ,
			noiseOrigin:	NoiseOrigin
		);
		float3 vx2 = VerticesJob.Vertex(
			index2:			i2x2 ,
			widthHeight:	new int2{ x=Width , y=Width } ,
			areaScale:		Scale ,
			noiseOrigin:	NoiseOrigin
		);
		float3 vy1 = VerticesJob.Vertex(
			index2:			i2y1 ,
			widthHeight:	new int2{ x=Width , y=Width } ,
			areaScale:		Scale ,
			noiseOrigin:	NoiseOrigin
		);
		float3 vy2 = VerticesJob.Vertex(
			index2:			i2y2 ,
			widthHeight:	new int2{ x=Width , y=Width } ,
			areaScale:		Scale ,
			noiseOrigin:	NoiseOrigin
		);
		float3 vx = math.normalize( vx2 - vx1 );
		float3 vy = math.normalize( vy2 - vy1 );
		return math.cross( vx , vy );
	}
}