Multithreading Problems: Method executed in separated thread still affects the main thread

Hi all again. This summer I started working again on a Voxel Engine. Working with three-dimensional arrays, ArrayLists and quads (and a bit of GeometryBatchFactory.optimize() ) I could get over 200 FPS in full zones and 1500 in zones without many quads. Every block is based on a “Cell”: “abstract” object that holds and decides what faces of the cube have to be shown. When a Chunk is updates, every Cell in it is updated. This usually takes about 15 or less milliseconds, but, randomly, it can take over 1500 milliseconds, making the whole thing unplayable. The idea was to execute the update method in a parallel thread, so that it wouldn’t affect the main thread. After more than two weeks of tries, I couldn’t get something that works. Even if the update method is called from a Callable, submitted by an executor, it still affects the Main Thread, making the whole game freeze for random time (depending on how much time the update method takes). I post here my code, can you please help me? I’m actually getting mad because of this. Searching in the internet I found various examples, but no one worked.

(The Callable that executes the chunk update method is at the end of the class)

package mygame;

import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.math.Vector3f;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static mygame.Chunk.chunkSideDim;

public class WorldProvider extends AbstractAppState {

Main app;
Random rand = new Random();

private final static int maxX = 20, maxY = 20, maxZ = 20;

public static Chunk[][][] chunks = new Chunk[maxX][maxY][maxZ];

//public static Chunk[][][] chunks = new Chunk[maxX][maxY][maxZ];
boolean worldGenerated = false;
public static boolean firstCellPlaced = false;

int pX, pY, pZ; //player coords
PlayerControlState playerControl;

int prevX, prevY, prevZ;
boolean first = false;

int renderDistance = 6;

int updateX, updateY, updateZ;

public static ExecutorService executor = Executors.newSingleThreadExecutor();

@Override
public void initialize(AppStateManager stateManager, Application app) {
    super.initialize(stateManager, app);
    this.app = (Main) app;
    playerControl = this.app.getStateManager().getState(PlayerControlState.class);
    //int worldLimitX = maxX * chunkSideDim;
    //int worldLimitZ = maxZ * chunkSideDim;
    int worldLimitX = maxX * (renderDistance + 2);
    int worldLimitZ = maxZ * (renderDistance + 2);

    for (int i = 0; i < worldLimitX; i++) {
        for (int j = 0; j < worldLimitZ; j++) {
            setCell(i, 0, j, Id.GRASS);
        }
    }

    for (int i = 0; i <= worldLimitX / 2; i++) {
        createHill(rand.nextInt(worldLimitX), rand.nextInt(worldLimitZ), rand.nextInt(chunkSideDim), rand.nextInt(chunkSideDim), (rand.nextInt(maxX) + 1));
    }

    for (int i = 0; i < maxX; i++) {
        for (int j = 0; j < maxY; j++) {
            for (int k = 0; k < maxZ; k++) {
                if (WorldProvider.chunks[i][j][k] != null) {
                    WorldProvider.chunks[i][j][k].update();
                }
            }
        }
    }
    executor.submit(r);
}

@Override
public void update(float tpf) {

    pX = playerControl.getX() / chunkSideDim;
    pY = playerControl.getY() / chunkSideDim;
    pZ = playerControl.getZ() / chunkSideDim;

    for (int i = 0; i < maxX; i++) {
        for (int j = 0; j < maxY; j++) {
            for (int k = 0; k < maxZ; k++) {

                updateX = i;
                updateY = j;
                updateZ = k;
                try {
                    if (chunks[i][0][k] != null) {
                        if (Math.abs(chunks[i][0][k].posX - pX) <= renderDistance - 1 && Math.abs(chunks[i][0][k].posZ - pZ) <= renderDistance - 1) {
                            chunks[i][0][k].loadChunk();
                            chunks[i][0][k].optimize();
                            try {
                                if (chunks[i][0][k].posX == pX && chunks[i][0][k].posZ == pZ || chunks[i][0][k].posX == pX && chunks[i][0][k].posZ == pZ + 1 || chunks[i][0][k].posX == pX && chunks[i][0][k].posZ == pZ - 1 || chunks[i][0][k].posX == pX + 1 && chunks[i][0][k].posZ == pZ || chunks[i][0][k].posX == pX - 1 && chunks[i][0][k].posZ == pZ) {
                                    chunks[i][0][k].loadPhysics();
                                } else {
                                    chunks[i][0][k].unloadPhysics();
                                }
                            } catch (Exception e2) {
                            }
                        } else {
                            chunks[i][0][k].unloadChunk();
                        }
                    } else {
                    }
                } catch (Exception e) {
                }
            }
        }
    }
}

//replaces the Cell.setId(id), and replaces making all the cell air when chunk is created
public void setCell(int i, int j, int k, Id id) {
    //System.out.println("Cell being placed in world coords: " + i + ", " + j + ", " + k);

    int plusX = i % chunkSideDim, plusY = j % chunkSideDim, plusZ = k % chunkSideDim;
    int chunkX = (i - plusX) / chunkSideDim, chunkY = (j - plusY) / chunkSideDim, chunkZ = (k - plusZ) / chunkSideDim;

    try {
        if (WorldProvider.chunks[chunkX][chunkY][chunkZ] != null) {
            WorldProvider.chunks[chunkX][chunkY][chunkZ].setCell(plusX, plusY, plusZ, id);
        } else {
            WorldProvider.chunks[chunkX][chunkY][chunkZ] = new Chunk(chunkX, chunkY, chunkZ);
            WorldProvider.chunks[chunkX][chunkY][chunkZ].genBase();

            WorldProvider.chunks[chunkX][chunkY][chunkZ].setCell(plusX, plusY, plusZ, id);
        }

        if (!firstCellPlaced) {
            PlayerControlState.respawnPoint = new Vector3f(8, 8, 8);
            PlayerControlState.playerControl.warp(new Vector3f(8, 8, 8));
            firstCellPlaced = true;
        }
    } catch (ArrayIndexOutOfBoundsException e2) {
        //System.out.println("Chunk coords at: " + chunkX + ", " + chunkY + ", " + chunkZ + " is out of the world");
    }
}

public Cell getCell(int i, int j, int k) {
    //System.out.println("Cell being placed in world coords: " + i + ", " + j + ", " + k);

    int plusX = i % chunkSideDim, plusY = j % chunkSideDim, plusZ = k % chunkSideDim;
    int chunkX = (i - plusX) / chunkSideDim, chunkY = (j - plusY) / chunkSideDim, chunkZ = (k - plusZ) / chunkSideDim;

    try {
        if (WorldProvider.chunks[chunkX][chunkY][chunkZ] != null) {
            try {
                return WorldProvider.chunks[chunkX][chunkY][chunkZ].getCell(plusX, plusY, plusZ);
            } catch (Exception e) {
                return null;
            }
        }
    } catch (Exception e1) {
        return null;

    }
    return null;
}

//returns the chunk is the specified coords
public Chunk getChunk(int i, int j, int k) {
    int plusX = i % chunkSideDim, plusY = j % chunkSideDim, plusZ = k % chunkSideDim;
    int chunkX = (i - plusX) / chunkSideDim, chunkY = (j - plusY) / chunkSideDim, chunkZ = (k - plusZ) / chunkSideDim;

    return chunks[chunkX][chunkY][chunkZ];

}

public void createHill(int x, int y, int initX, int initY, int height) {
    drawEllipse(x, y, initX, initY, 1);
    int difference = 0;
    int lastDifference = 0;

    for (int i = 0; i <= height; i++) {
        while (difference <= lastDifference) {
            difference = i + rand.nextInt(i + 1) + 1;
        }

        if (initX - difference > 0 && initY - difference > 0) {
            drawEllipse(x, y, initX - difference, initY - difference, i);
        }
        lastDifference = difference;
    }
}

public void drawEllipse(int originX, int originY, int height, int width, int yHeight) {
    int hh = height * height;
    int ww = width * width;
    int hhww = hh * ww;
    int x0 = width;
    int dx = 0;

    // do the horizontal diameter
    for (int x = -width; x <= width; x++) {
        setCell(originX + x, yHeight, originY, Id.GRASS);
    }

    // now do both halves at the same time, away from the diameter
    for (int y = 1; y <= height; y++) {
        int x1 = x0 - (dx - 1);  // try slopes of dx - 1 or more
        for (; x1 > 0; x1--) {
            if (x1 * x1 * hh + y * y * ww <= hhww) {
                break;
            }
        }
        dx = x0 - x1;  // current approximation of the slope
        x0 = x1;

        for (int x = -x0; x <= x0; x++) {
            setCell(originX + x, yHeight, originY - y, Id.GRASS);
            setCell(originX + x, yHeight, originY + y, Id.GRASS);
        }
    }
}

public void drawCircle(int centerX, int centerY, int radius) {

    for (int y = -radius; y <= radius; y++) {
        for (int x = -radius; x <= radius; x++) {
            if (x * x + y * y <= radius * radius) {
                setCell(centerX + x, 0, centerY + y, Id.GRASS);
            }
        }
    }

    for (int y = -radius; y <= radius; y++) {
        for (int x = -radius; x <= radius; x++) {
            if (x * x + y * y <= radius * radius) {
                setCell(centerX + x, 0, centerY + y, Id.GRASS);
            }
        }
    }
}

Callable r = new Callable<Object>() {

    @Override
    public Object call() throws Exception {
        System.out.println("Chunk Update Runnable started!");
        while (!executor.isShutdown()) {
            for(int i = 0; i <= pX +renderDistance; i++){
                for(int k = 0; k <= pZ + renderDistance; k++){
                    if(chunks[i][0][k] == null){
                        chunks[i][0][k] = new Chunk(i, 0, k);
                        chunks[i][0][k].genBase();
                        chunks[i][0][k].update();
                    }
                }
            }
        }
        return null;
    }
};

}

The chunk.loadChunk of chunk.unloadChunk method doesn’t affect the game, it just attach of detach the chunk from the terrainNode, attached to the rootNode. Only the update method does.

Thanks,
EmaMaker

Random freezes are most often due to garbage collection making a big clean up. Did you try to profile your game memory to see where it comes from?

As an aside, it’s not clear what you really mean by “affects the game”, but this

Sounds definitely like “affecting the game”. it may be not the source of your problem, but a separate thread should NOT affect the scene graph in ANY way (reading, modifying, changing a material param, etc)…
If you want to parallelize properly, do the heavy calculation in the separate thread, then enqueue all scene graph modfication resulting from the computation to the JME application. (application.enqueue(callable))

1 Like

I tried the app.enqueue() method, not worked. With “affects the game”. I mean “does not make the game freeze”. Thanks for the garbage collector option, tomorrow I’ll try to optimize the RAM usage where possibile. The cell update method does not attach or detach things from any node, but chunk.loadChunk() or unloadChunk() is always called from the main thread.

try to preload the Material and preload textures/ the mesh. what could aswell help is to load all the models at the beginnig.

One of the first things I did when I started coding this Voxel Engine. Every quad is being placed is just a Geometry.clone() of the same thing

Note that you can work with parts of a scenegraph in other threads (attach, detach stuff, change materials etc.), as long as this branch isn’t attached to the active root node.
Often it’s easier and more performant to prepare all these changes in the worker thread and then just enqueue the swapping of a whole branch.

The “Java Profiler” Plugin, which can be installed directly in the “Tools → Plugins” menu in the SDK, can help you verify that it’s indeed the garbage collector which causes the freezes.

1 Like

I have already replied to @nehon that the chunk.update() method, which is called from a secondary thread managed by an ExecutorService, does not manage the scene graph, it just add to an arraylist which quads have to be added to the scene graph. Modifying the scene graph is always done done from main thread.

Read it as a suggestion which could also fix what b00n was saying. We don’t know what the Chunk class is doing because it’s not included.

But the chunk load/unloading methods are not a problem, I’ve said that in the first post

Yeah, so there will be a TON of garbage generated with all of those unnecessary objects being created all the time.

So my bet is on GC issues.

It s not that they don’t believe you, the problem is that you generate too much garbage on the long term then Java GC as to drop it all at the same time. On my game with an infinite terrain, I actually resolved the issue by setting my object to null. Also if you want to add and remove object from a scene in Multithread, i would suggest making a Concurrent Thread and retreive the Generated Batch Node (Which would reduce lag) and attach it to the scene on task complete in the main thread. Removing it should be done in the main Thread to prevent any crash since as they said you can’t modify the scenegraph from an external thread.

Edit: Also if i m not wrong setting an object to null make it more possible to be remove from memory. Also you might want to take a look at your memory usage, My game seem to start slowing down if i go above 4 GO memory usage.
Edit2: In short make sure nothing keep a reference and beware of cross reference that might stay, they might leave the object stuck in memory until the GC figure out it was calling to an object that was calling to it. A <=> B for example are both calling each other which make it harder to GC.

And actually if your “chunks” are thousands of geometry objects then that’s also going to take a huge frame drop every time you add all of that to the scene.

To do a block world, you will have to construct a custom mesh and avoid the huge overhead of creating all of those geometries for no good reason.

It sounds like interesting, but what object are you talking about? The single cell, the chunk or the mesh of each side of the cube?

What should the custom mesh do? To create a side of the block I used Geometry.clone() of the same Geometry, which is just moved or rotated depending on the side type (up, down, left, right, front or back)

So for a single block you will have 6 geometries?!? So how many geometries are in a chunk???

A chunk should be one mesh per chunk per material (or if you use an atlas then probably one mesh per chunk).

Ok, reading for the 10th time your answer I should have understood. What you’re saying is to build a custom mesh with the same mesh of all the geometries’ ones out together. But, isn’t this done by simply GeometryBatchFactory.optimize(), or, better, with GeometryBatchFactory.mergeGeometries?

While I was writing this arrived your other answer: I don’t have 6 geometries for every block: only the side that can be seen from the player uses the Geometry, the others are set to null.

Sure, but if a block is floating in space then it’s six sides.

Creating a Geometry for each side and then batching them together is hugely wasteful. Instead of just creating those four points + attributes, you created a whole mesh, multiple vertex buffers, a whole geometry object with its transforms, etc. add all of that to a node with its child lists, dirty flags, transform refreshes, etc., etc… all just to squash it back into four points + attributes in a single mesh.

Just go straight to the single mesh. You will generate less garbage and it will be a few 1000x faster.

Trust me that I know what I’m talking about. I’ve done this before: http://mythruna.com/

1 Like

Lol was typing exactly that but alright :stuck_out_tongue: A chance i saw your answer popping :stuck_out_tongue:

Chunks are “usually” a power of 2 in size (e.g. 16x16x16) and stacked 16 high. It just allows for rapid regeneration of the mesh. A chunk should not be added until all parts are built. It also makes life easier for baked lighting and determining whether you are underground. Modifying a chunk just means rebuilding that particular 16x16x16 part.