How do I do multithreading per chunk in a Minecraft voxel game

Godot Version

4.7

Question

I want to render each chunk in my voxel game separately from my main thread so I don’t get lag spikes each time I load part of the world, but I’m not sure how. I luckily have a diagram of my class structure below(I don’t know if this will format right on the forum here)

   Player node may access Level

    Level
      |
      L ChunkGrid
      |     |
      |     L ChunkGenerator
      |     |
      |     L ChunkRenderer
      |     |
      |     L Dictionary[Vector3i,Chunk]
      |          |
      |          |
      |          L PalettedVoxelGrid
      |                   |
      |                    L VoxelGrid
      |
      L (reference to filestructure)

 Level = Node3D class that contains data referring to a point in the filesystem, and the ChunkGrid.
 manages saving and loading to disk, and manages tracking the players that render the area around it
 ChunkGrid = Node3D class that lets you manage contained chunks,
and contains methods for modifying data
 ChunkGenerator = RefCounted class that contains data for how to generate a chunk,
 and generates a chunk when passed in
 Chunk = Node3D class that manages a single chunk, child of ChunkGrid, also has the mesh nodes as children
 ChunkRenderer = Static RefCounted class that contains the methods for creating a chunks mesh data
 VoxelGrid = simple RefCounted class for managing a 3d number array flattened into 1d
 PalettedVoxelGrid = extension of VoxelGrid that uses indexes to let every block be a variant

also:

Block = The main Dictionary that holds all of the data for every block type, including the data to generate faces, and whether a block should backface cull neighbors.

Chunks inside ChunkGrid are stored inside a Dictionary[Vector3i, Chunk] variable, this is also where the render_chunk() method lives, that accesses ChunkRenderer with the chunk passed in and then sets the chunk ArrayMesh to its return value. So it seems like this is a good candidate to manage the multi-threading.
I was able to ask an LLM, and it told me to use something called a WorkerThreadPool to hold a bunch of worker threads, but I’m not sure how to get the logic to work with this, since generating any chunk can happen spontaneously here, and there will clearly be a lot of chunks. Perhaps I need a stack of the indexes of each chunk to render, and then I allocate one of the free threads to it when available?

(I’m really stumped on how Minecraft did this in 2009 on Java with one thread :p)

What do you mean by “render”?

Did you profile the project to determine bottlenecks?

Ooh sorry forgot to explain that, by “render” I meant running the function that takes in a chunk class and it’s neighbors, scans through the PalettedVoxelGrid, generates faces, and outputs an ArrayMesh, which the chunk takes in and applies to its array mesh. Maybe face generation is the right term then. (This operation is very slow, and I’ve been meaning to rewrite it in C++)

Another feature of this, is I’m running array_mesh.create_trimesh_shape() on that arraymesh while it’s created to build the collision, which is also passed into the output chunk.

I’m not sure what you mean by profile the project, I do know that a 32^3 chunk face generation takes on average 200ms, and on average trimesh can take 10ms. Overall this slows down the project when new chunks are rendered immensely :confused: Either way I think multi threading is kinda the way.

I’m working on this exact same problem in my game right now.

I’ve got the basic system running.

You need:

  • A job scheduler
  • A job struct containing func < T > work (and whatever metadata you want to add like, job name, job category, date/ time depending on how ‘hardcore’ you want to be about it
  • The job scheduler has a ConcurrentQueue < T >(this is a specifically thread safe data structure)
  • A job worker (One per thread) that basically grabs jobs from the job queue as long as it’s active
  • Active threads will simply grab jobs as soon as they come up.
  • You can also have a job results struct with telemetry/ performance data if you want

That’s the simplest version of the system:

The main thread adds a bunch of Chunk bake jobs,

Threads grab them as fast as they can.

Boom. That’s pretty much it.

Worker thread loop:

    private void WorkerLoop() {

        while (running) {

            if (JobQueue.TryDequeue(out var boxedJob)) {

                ProcessJob(boxedJob);

            }

            else {

                Thread.Sleep(10); // prevent CPU spin

            }

        }

    }

And personally that’s about as far as I’m going to take it. Multithreading can become very complicated very quickly. Chunk bakes are pretty easy, low-hanging fruit because it’s unlikely that they’re going to have race conditions. Game logic, however? Multithreading patterns is a whole rabbit hole…

(Sorry a terrible formatting, I know. Phone)

I just realized though I was talking to c sharp if you’re using GD script, you will have to create a workaround to the concurrent queue because GD script doesn’t have one. It does have something called a mutex class which you could use to replicate a concurrent queue.

Ooh that makes more sense that it lacks those features, I was looking at your code a little puzzled on how I would translate it to GDScript. I will report back on my look into Mutex.

In general, you can have a single worker thread that (slowly) builds anticipated chunks in the background so they are ready when the player/camera is in position to see them.

Btw using create_trimesh_shape() wastes time because it downloads mesh data from the GPU and potentially blocks the thread. Better to just use set_faces().