Search Unity

Main thread spikes, ECS jobs

Discussion in 'Entity Component System' started by LibooSoft, Sep 16, 2019.

  1. LibooSoft

    LibooSoft

    Joined:
    Jun 19, 2019
    Posts:
    20
    Ok, So Iv been doing some experiments with creating custom terrain meshes by LOD rules. Kind of like this one



    So there will be many chucks of game objects with different kinds of rules set for the deepening on distance from camera, and what kind of quad is their neighbor. Kind of a easy and pretty ok technique to avoid gaps in the terrain. I have been trying to push this more to an edge of what can be done now with the ECS and job system for example. It works better to place many of the build and rule settings in jobs and burst them.

    However, as I right now track them as objects in a dictionary in the main thread, it still can cause spikes in the main thread, making things freeze up when an update or a load of a new chuck needs to be set up. Because all the vertices, triangles and colliders for example needs to be set up in the main thread.

    Is there some way to off load this? Right now I believe this is liked to the update/new loop that tracks the quad chucks that should be set for render.

    I will post some code here to show


    This code below is how things are called from the "Update".
    1. The rules are generated for all possible quads to be rendered for the camera.
    2. These rules are used to setup the QuadData objects based on the data generated by the rules
    3. Now the meshes them self are about the be generated and updated. And I believe this step is part of the issue here for the spike in the main thread


    Code (CSharp):
    1. NativeArray<QuadLOD> quadLODRules; //all current rules for how visible quads should be renderd
    2. Dictionary<Vector2, QuadData> quadDictonary; //all currently existing quads available for possible render
    3.  
    4.  
    5.     void Update()
    6.     {
    7.         QuadLODRulesManager();
    8.      
    9.         if (!runnigQuadRenderer)
    10.         {
    11.             BuildQuadsByRules(quadLODRules);
    12.             BuildQuadMeshes(quadLODRules);
    13.        
    14.             foreach (KeyValuePair<Vector2, QuadData> data in quadDictonary)
    15.             {
    16.                 QuadData qd = quadDictonary[data.Key];
    17.                 qd.UpdatedVisibility(viewerPosition);
    18.             }
    19.         }
    20.         quadLODRules.Dispose();
    21.     }
    22.  
    23.  
    24.     private void BuildQuadsByRules(NativeArray<QuadLOD> quadLodArray)
    25.     {
    26.        for (int i = 0; i < quadLodArray.Length; i++)
    27.         {
    28.             QuadLOD quadLOD = quadLodArray[i];
    29.             //Add new non existing quads
    30.             if (!quadDictonary.ContainsKey(quadLOD.worldPos))
    31.             {
    32.                 int lodLevel = quadLOD.levelOfLOD;
    33.                 QuadData newQuad = new QuadData(viewerPosition, lodLevel, mapMaterial, transform, quadLOD, detailLevels);          
    34.                 quadDictonary.Add(quadLOD.worldPos, newQuad);
    35.             }
    36.         }
    37.     }
    38.  
    39.     private void BuildQuadMeshes(NativeArray<QuadLOD> quadLodArray)
    40.     {
    41.         quadsLODForUpdate = new List<QuadLOD>();
    42.         quadsForUpdate = new List<QuadData>();
    43.         quadsForNew = new List<QuadData>();
    44.  
    45.         for (int i = 0; i < quadLodArray.Length; i++)
    46.         {
    47.             QuadLOD quadLOD = quadLodArray[i];
    48.             QuadData qd;
    49.             if (quadDictonary.TryGetValue(quadLOD.worldPos, out qd))
    50.             {
    51.                 if (quadLOD.isVisibleForRender)
    52.                 {
    53.                     //update
    54.                     if (qd.quadDataReceived)
    55.                     {
    56.  
    57.                         if (quadLOD.lodBottom != qd.quadLOD.lodBottom ||
    58.                             quadLOD.lodLeft != qd.quadLOD.lodLeft ||
    59.                             quadLOD.lodRight != qd.quadLOD.lodRight ||
    60.                             quadLOD.lodTop != qd.quadLOD.lodTop ||
    61.                             quadLOD.levelOfLOD != qd.quadLOD.levelOfLOD)
    62.                         {
    63.                             quadsLODForUpdate.Add(quadLOD);
    64.                             quadsForUpdate.Add(qd);
    65.                         }
    66.                     }
    67.                     //new
    68.                     else
    69.                     {
    70.                         quadsForNew.Add(qd);
    71.                     }
    72.                 }
    73.             }
    74.         }
    75.  
    76.         for (int i = 0; i < quadsForUpdate.Count; i++)
    77.         {
    78.             QuadData data = quadsForUpdate[i];
    79.             data.UpdateQuad(quadsLODForUpdate[i], i, quadsForUpdate.Count);
    80.         }
    81.  
    82.         for (int i = 0; i < quadsForNew.Count; i++)
    83.         {
    84.             QuadData data = quadsForNew[i];
    85.             data.LoadQuad(i, quadsForNew.Count);
    86.         }    
    87.     }


    And here is part of what happens in the QuadData. Here are the meshes them self. And, yes there is some more here related to how they are setup in the previous step, but this is the chain that that is a part of the spike problem.
    The QuadMeshJob is an IJob right now that is used to speed up the process to generate the mesh data to be used. I use to have the height calculation here too, but have taken it out right now, thought I would alter this after the mesh data them self are setup. So one problem at a time. :)


    Code (CSharp):
    1. public void LoadQuad(int id, int numberOfJobs)
    2.     {
    3.         ECSMeshJob(this.position, id);
    4.     }
    5.  
    6.     public  void UpdateQuad(QuadLOD lod, int id, int numberOfJobs)
    7.     {
    8.         if (quadDataReceived)
    9.         {
    10.                 lod.isDirty = false;
    11.                 if (this.quadLOD.levelOfLOD != lod.levelOfLOD)
    12.                 {
    13.                     lod.isDirty = true;
    14.                 }
    15.  
    16.                 this.quadLOD = lod;
    17.                 quadLOD.levelOfLOD = lod.levelOfLOD;
    18.                 this.position = (worldCoord * meshSettings.quadMeshSize) - compLODVectors[levelOfDetail];
    19.  
    20.                 ECSMeshJob(this.position, id);
    21.         }
    22.     }
    23.  
    24.  
    25.         private void ECSMeshJob(Vector2 position, int id)
    26.         {
    27.  
    28.             JobMeshData(position, this.quadLOD, id);
    29.  
    30.             SetForRender(id);
    31.  
    32.             quadDataReceived = true;
    33.         }
    34.  
    35.         private void JobMeshData(Vector2 pos, QuadLOD quadLOD, int id)
    36.         {
    37.             jobQuadMesh = new QuadMeshJob();
    38.             jobQuadMesh.quadLOD = quadLOD;
    39.             jobQuadMesh.position = new Vector3() { x = pos.x, y = 0, z = pos.y };
    40.             jobQuadMesh.PreInit();
    41.             jobQuadMesh.Init();
    42.             jobQuadMesh.Schedule().Complete();
    43.             //job.Execute(id);
    44.  
    45.            // return quadMeshJob;
    46.         }
    47.  
    48.                 public void SetForRender(int id)
    49.         {
    50.             meshQuad.Clear();
    51.             //meshQuad = new Mesh();
    52.             meshQuad.SetVertices(jobQuadMesh.Vertices);
    53.             meshQuad.SetNormals(jobQuadMesh.Normals);
    54.             meshQuad.SetUVs(0, jobQuadMesh.Uvs);
    55.             int[] arr = new int[jobQuadMesh.Triangles.Length];
    56.             jobQuadMesh.Triangles.CopyTo(arr);
    57.             meshQuad.SetTriangles(arr, 0);
    58.             //meshQuad.RecalculateNormals();
    59.  
    60.             meshFilter.mesh = meshQuad;
    61.             meshCollider.sharedMesh = meshQuad;
    62.    
    63.             meshObject.transform.position = new Vector3(this.position.x, 0, this.position.y);
    64.      
    65.             quadLOD.NrVert = jobQuadMesh.Vertices.Length;
    66.             quadLOD.NrTri = arr.Length;
    67.             jobQuadMesh.Dispose();
    68.  
    69.         }

    Here you can see the spikes that happens when moving around and the update-loop kicks in.



    This might not be the most well written code, but it works. I just want to try to understand and learn how to improve it in performance when dealing with this sort of issue. If anyone has any tips or ideas how to solve this update-loop issue that now is in the main thread as it is used for updating the meshes associated with them.


    .
     
    Last edited: Sep 18, 2019
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,770
    Have you tried to apply burst?

    From top of my head, look in DOTS forum for Marching Cubes. Is not exactly same thing you do, but may give you some ideas. Try search engine, if forum search fails.
     
  3. LibooSoft

    LibooSoft

    Joined:
    Jun 19, 2019
    Posts:
    20
    Yes, I have burst running and it sure improves as the wait time sure is much shorter then before. But there still is this spike in the main thread as it need to update the data made from the burst job just been done on the mesh.

    Thanks. Will check this "Marching Cubes" out see if I can get some ideas.
     
    Last edited: Sep 18, 2019
  4. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Saw this post and meant to reply but I forgot.

    Anyway, you need to dive a little deeper into what exactly is causing your spike but my guess is simply this

    meshCollider.sharedMesh = meshQuad;

    Updating a a mesh collider is very expensive, if you're doing this more than once per frame you'll notice considerable spikes.
     
  5. LibooSoft

    LibooSoft

    Joined:
    Jun 19, 2019
    Posts:
    20
    Thanks for the reply. I believe that there is quiet a large numer of calls for candidates for update and new generation that is part of this problem. This dose not happen every frame. Only when these quad chunks comes in range for a change set up by the lod rules, as there are 5 levels max, of LOD levels. So the call only gets made when a mesh needs new info, so maybe about 20% of the candidates, but they are many.



    Aa seen in the screenshot above. You are probably right about the colliders being set and being apart of this problem. Not all of them but at least but maybe 20% of all of the chucks.
    Not sure how this could be solved right now.


    As colliders are not that important when it comes to being updated instantly here, a spontane but maybe kind of ugly idea is to que these colliders to be set and only let only one per frame call be set. Not sure if that might "spread" the load, maybe not freeze the frame until all is finished. Just spinnin bad ideas, not really what I want. :)
     
    Last edited: Sep 17, 2019
  6. LibooSoft

    LibooSoft

    Joined:
    Jun 19, 2019
    Posts:
    20
    Minor update. Tried to disable update for the collider and only have it update for new chucks. then I let the camera run in a circle, as the chuckes are cached once they are done, there is no need for them to reassign the colliders when they are called again, but these spikes still are there the next lap after the first one.
     
  7. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    What is allocating that 6.8MB of garbage? (it's cutoff)

    The UpdateQuad method is only 1.4% of the spike so it's not what is causing it. 6ms which is what I'd expect it to be about.
     
  8. LibooSoft

    LibooSoft

    Joined:
    Jun 19, 2019
    Posts:
    20


    There are a about three times more of these, as these are the updates calls set by the LOD rules that was updated. So it kind of sums up.

    Is it the Garbage Collection that could be a part of this problem?
    It varies from 117.5 KB to 22.7KB
     
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Wait, you're trying to update like ~100 meshes in a single frame? That's not going to fly. It's way too costly to pass data from the CPU to GPU.

    You can see just setting ~15 colliders is costing ~100ms
     
  10. LibooSoft

    LibooSoft

    Joined:
    Jun 19, 2019
    Posts:
    20
    Well, they need to get updated some how. Maybe not in the same frame.
    How would you approach this if you wanted like about 100 meshes to get an update?
     
  11. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    It's difficult to tell exactly what problem you are trying to solve. But the correct answer is more then likely there is a well known approach to your problem that doesn't involve updating all those meshes.

    There are really very few real world problems in games where that many meshes are updated per frame on the cpu. There are a select few but but you really don't want to burn all your resources for problems that don't actually require it.