godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
89.43k stars 20.25k forks source link

ArrayMesh.AddSurfaceFromArrays performs poorly and sometimes freezes in multithread contexts. #70325

Open BlazeTheOldHunter opened 1 year ago

BlazeTheOldHunter commented 1 year ago

Godot version

Godot Engine v4.0.beta7.mono.official.0bb1e89fb

System information

Windows 10 v10.0.19045 Build 19045, AMD Ryzen 7 3800X, Vulkan API 1.2.0 , Nvidia GeForce RTX 2070 Super (driver v 526.98))

Issue description

I am using the ArrayMesh.AddSurfaceFromArrays to combine several smaller meshes in a loop to merge them into one larger mesh to support world chunking in my game. To increase performance I added multi threading to my code to create the individual chunks in separate threads. My expectation would be to see 2x to 4x performance increase due to my cpu having 8 cores and creating 4 separate worker threads. My code became much much slower when i utilized multi threading. In best cases the multi threaded version of my code would complete the same task 4x to 8x slower than the single threaded option. In some cases the threads would never finish processing and It would run for forever. In one instance I waited over 30 mins without the threads ever completing before having to manually stop the program while the single threaded version with same inputs ran in about 10 seconds for the same input.

Upon further investigation, the call to the AddSurfaceFromArrays is the problem area in my code. Calls to it take significantly longer when using multiple threads. I tried multiple different configurations for number of threads with no change. I also tried using Godots built in threading classes , along with using Tasks and Parallel.For from C# and the results were the same. The number of threads allocated seems to have some effect , but that only determines weather the program completes or just keeps processing forever.

Steps to reproduce

using Godot;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;

public partial class ProceduralMeshGeneration : Node
{
    // Called when the node enters the scene tree for the first time.

    [Export]
    public int MaxDegreeOfParallelism = 4;
    [Export]
    public int Chunks = 5;
    [Export]
    public int ChunkSize = 10;
    public override void _Ready()
    {
        SingleThreadTest();
        MultiThreadTest();
    }

    // Called every frame. 'delta' is the elapsed time since the previous frame.
    public override void _Process(double delta)
    {
    }

    public void SingleThreadTest()
    {
        Stopwatch watch = new Stopwatch();

        List<Tuple<int, int, int, int>> locations = GetChunkLocations(Chunks, ChunkSize);
        List<List<ArrayMesh>> chunkMeshes = new List<List<ArrayMesh>>();
        for (int i = 0; i < locations.Count; i++)
        {
            List<ArrayMesh> meshes = CreateChunks(locations[i].Item1, locations[i].Item2, locations[i].Item3, locations[i].Item4);
            chunkMeshes.Add(meshes);
        }
        GD.Print("Single Thread Test start");
        watch.Start();
        for (int i = 0; i < chunkMeshes.Count; i++)
        {
            ArrayMesh chunk = CreateMultiSurfaceMesh(chunkMeshes[i]);
        }
        watch.Stop();
        GD.Print($"Single Thread Test end. Time elasped: {watch.Elapsed}");
    }

    public void MultiThreadTest()
    {
        Stopwatch watch = new Stopwatch();

        List<Tuple<int, int, int, int>> locations = GetChunkLocations(Chunks, ChunkSize);

        List<List<ArrayMesh>> chunkMeshes = new List<List<ArrayMesh>>();
        for (int i = 0; i < locations.Count; i++)
        {
            List<ArrayMesh> meshes = CreateChunks(locations[i].Item1, locations[i].Item2, locations[i].Item3, locations[i].Item4);
            chunkMeshes.Add(meshes);
        }

        GD.Print("Multi Thread Test start");
        watch.Start();

        Parallel.For(0, chunkMeshes.Count, new ParallelOptions() { MaxDegreeOfParallelism = MaxDegreeOfParallelism }, i =>
        {
            ArrayMesh chunk = CreateMultiSurfaceMesh(chunkMeshes[i]);
        });

        watch.Stop();
        GD.Print($"MultiThread Thread Test end. Time elasped: {watch.Elapsed}");
    }

    public List<Tuple<int, int, int, int>> GetChunkLocations(int chunks, int chunkSize)
    {
        List<Tuple<int, int, int, int>> locations = new List<Tuple<int, int, int, int>>();
        for (int i = 0; i < chunks; i++)
        {
            for (int j = 0; j < chunks; j++)
            {
                locations.Add(new Tuple<int, int, int, int>(chunkSize * i, chunkSize * (i + 1), chunkSize * j, chunkSize * (j + 1)));
            }
        }

        return locations;
    }

    public List<ArrayMesh> CreateChunks(int xStart, int xEnd, int yStart, int yEnd)
    {
        List<ArrayMesh> meshes = new List<ArrayMesh>();
        for (int i = xStart; i < xEnd; i++)
        {
            for (int j = yStart; j < yEnd; j++)
            {
                meshes.Add(CreateSquareMesh(i, j));
            }
        }
        return meshes;
    }

    public ArrayMesh CreateSquareMesh(int xOffSet, int zOffSet)
    {
        List<Vector3> normals = new List<Vector3>();
        List<Vector3> vertices = new List<Vector3>();
        List<int> indices = new List<int>();

        normals.Add(new Vector3(0, 1, 0));
        normals.Add(new Vector3(0, 1, 0));
        normals.Add(new Vector3(0, 1, 0));
        normals.Add(new Vector3(0, 1, 0));

        vertices.Add(new Vector3(xOffSet, 0, zOffSet));
        vertices.Add(new Vector3(xOffSet, 0, 1 + zOffSet));
        vertices.Add(new Vector3(1 + xOffSet, 0, 1 + zOffSet));
        vertices.Add(new Vector3(1 + xOffSet, 0, zOffSet));

        indices.Add(0);
        indices.Add(3);
        indices.Add(1);

        indices.Add(1);
        indices.Add(3);
        indices.Add(2);

        Godot.Collections.Array arrays = new Godot.Collections.Array();
        arrays.Resize((int)Mesh.ArrayType.Max);
        arrays[(int)Mesh.ArrayType.Vertex] = vertices.ToArray();
        arrays[(int)Mesh.ArrayType.Normal] = normals.ToArray();
        arrays[(int)Mesh.ArrayType.Index] = indices.ToArray();
        ArrayMesh arrayMesh = new ArrayMesh();
        arrayMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
        return arrayMesh;
    }

    public static ArrayMesh CreateMultiSurfaceMesh(List<ArrayMesh> meshes)
    {

        Stopwatch addSurfaceTimer = new Stopwatch();
        Stopwatch loopTimer = new Stopwatch();
        loopTimer.Start();
        ArrayMesh arrayMesh = new ArrayMesh();
        for (int i = 0; i < meshes.Count; i++)
        {

            List<Vector3> normal_array = new List<Vector3>();
            List<Vector3> vertex_array = new List<Vector3>();
            List<int> index_array = new List<int>();
            List<Vector2> uv_array = new List<Vector2>();
            List<Color> color_array = new List<Color>();

            Godot.Collections.Array data = meshes[i].SurfaceGetArrays(0);
            Vector3[] vertices = (Vector3[])data[(int)Mesh.ArrayType.Vertex];
            Vector3[] normals = (Vector3[])data[(int)Mesh.ArrayType.Normal];
            Vector2[] uvs = (Vector2[])data[(int)Mesh.ArrayType.TexUv];
            Color[] colors = (Color[])data[(int)Mesh.ArrayType.Color];
            int[] indexes = (int[])data[(int)Mesh.ArrayType.Index];

            int vertexIndexesOffset = (vertex_array.Count);
            for (int a = 0; a < indexes.Length; a++)
            {
                int index = indexes[a] + vertexIndexesOffset;
                index_array.Add(index);
            }

            for (int a = 0; a < vertices.Length; a++)
            {
                Vector3 vertex = vertices[a];
                vertex_array.Add(vertex);
            }

            normal_array.AddRange(normals);
            if (uvs != null)
                uv_array.AddRange(uvs);

            if (colors != null)
                color_array.AddRange(colors);

            Godot.Collections.Array arrays = new Godot.Collections.Array();
            arrays.Resize((int)Mesh.ArrayType.Max);
            arrays[(int)Mesh.ArrayType.Vertex] = vertex_array.ToArray();
            arrays[(int)Mesh.ArrayType.Normal] = normal_array.ToArray();
            arrays[(int)Mesh.ArrayType.Index] = index_array.ToArray();
            if (uv_array.Count > 0)
                arrays[(int)Mesh.ArrayType.TexUv] = uv_array.ToArray();

            if (color_array.Count > 0)
                arrays[(int)Mesh.ArrayType.Color] = color_array.ToArray();

            addSurfaceTimer.Start();
            arrayMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
            addSurfaceTimer.Stop();
        }
        loopTimer.Stop();

        GD.Print($"CreateMultiMesh total Time {loopTimer.Elapsed} :  Add surface time {addSurfaceTimer.Elapsed}");
        return arrayMesh;
    }
}

Minimal reproduction project

ArrayMeshThreadIssue.zip

Zireael07 commented 1 year ago

IIRC this function calls to OpenGL, which is NOT thread safe. Many other mesh functions suffer from the same problem

clayjohn commented 1 year ago

Related: https://github.com/godotengine/godot/issues/56524