Search code examples
javaswingconcurrencyrenderinggame-engine

Synchronized block on two threads in Swing not working


Let me describe to you the context of my problem before I outline the issue. I'm currently in the middle of writing a game engine level editor and I'm working on the class that is going to act as the screen that the user interacts with in order to build their levels. I want to make it so the screen is proportional to the size of the editor.

The issue in question occurs when I begin resizing my screen and drawing on it at the same time. I draw from one thread and at the same time I'm editing the size of the raw pixel array that I'm drawing onto, from another thread (the EDT). I know this is a big no-no so naturally, with no safety in place, I get the occasional IndexOutOfBounds Exception on a resize.

My thought was, I could add a synchronize block on both the resizing code and the drawing code. This way, there would be no conflict and the issue should be avoided. However, the synchronization is being ignored completely. I'm still getting the same error and I'm really quite confused on why it isn't working. Below are the two methods of interest:

public void setPixel(int r, int g, int b, int x, int y) {
    synchronized (pixels){
        System.out.println("Start Draw...");
        int color = (r << 16) | (g << 8) | b;
        pixels[y * screenWidth + x] = color;
        System.out.println("End Draw...");
    }
}


@Override
public void componentResized(ComponentEvent e) {
        synchronized (pixels) {
            System.out.println("Start resize");
            int width = e.getComponent().getWidth();
            int height = e.getComponent().getHeight();
            float aspectRatio = 4 / 3f;
            if (width > height) {
                width = (int) (height * aspectRatio);
            } else if (height > width) {
                height = width;
            }
            if (width < 0 || height < 0) {
                width = 1;
                height = 1;
            }
            this.screenWidth = width;
            this.screenHeight = height;
            image = new BufferedImage(screenWidth, screenHeight, BufferedImage.TYPE_INT_RGB);
            pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
            System.out.println("End Resize");
        }
    }

I don't know if it matters (it shouldn't right?) but my screen class extends a AWT Canvas. Also it is a listener on its parent component, so when that gets resized, it fires an event that triggers componentResized to be called. Anyway, thank you, any help is appreciated.

EDIT: My drawing code can be found below.

new Thread(new Runnable(){
    @Override
    public void run() {
        while(true) {
            for (int y = 0; y < screen.getHeight(); y++) {
                for(int x = 0; x < screen.getWidth(); x++){
                    int r = (int) (Math.random() * 255);
                    int g = (int) (Math.random() * 255);
                    int b = (int) (Math.random() * 255);
                    screen.setPixel(r, g, b, x, y);
                }
            }
        }
    }
}).start();

Solution

  • There is something else I can think of. Not only what is in the comments of the question.

    Let me modify first the setPixel method:

    public void setPixel(int r, int g, int b, int x, int y) {
        System.out.println("Called with: " + x + ", " + y); //Just added a print statement.
        synchronized (mySyncGuard){
            System.out.println("Start Draw...");
            int color = (r << 16) | (g << 8) | b;
            pixels[y * screenWidth + x] = color;
            System.out.println("End Draw...");
        }
    }
    

    Where mySyncGuard is a final property used as the synchronization guard to both setPixel and componentResized.

    And now imagine the following scenario:

    There is a loop which calls the setPixel method: this loop calls the method starting from x = 0 and y = 0 up to x < screenWidth and y < screenHeight! Like the following one:

    for (int x = 0; x < screenWidth; ++x)
        for (int y = 0; y < screenHeight; ++y) {
            int r = calculateNextR(),
                g = calculateNextG(),
                b = calculateNextB();
            setPixel(r, g, b, x, y);
        }
    

    Where calculateNextR(), calculateNextG() and calculateNextB() are methods that produce the next red, green and blue color components respectively.

    Now, for example, let screenWidth be 200 and screenHeight be also 200 and at some point resized to 100x100.

    Now the problem is that x and y would be lets say 150 and 150 respectively while the component was about to be resized to 100, 100.

    Our custom setPixel method is now called with x==150 and y==150. But before the printing statement we added which shows the x and the y values, the componentResized manages to be called and take the lock of the synchronization guard property mySyncGuard! So componentResized is now doing its job changing the size of the image to 100x100, which, in turn, changes the pixels array to something smaller than the previous data array.

    In parallel, setPixel now prints "Called with 150, 150" and waits at the synchronized block to obtain the lock of mySyncGuard, because componentResized has already currently obtained it and changing the image to 100x100 and the pixels array accordingly.

    So now the pixels array is smaller, the componentResized finishes resizing and finally setPixel can obtain the lock of mySyncGuard.

    And now is the problem: the array pixels is traversed at location 150,150 while it actually has size 100x100. So there you go! IndexOutOfBoundsException...

    Conclusion:

    1. We need more of your code to determine what's wrong.
    2. The variables changing (such as screenWidth, screenHeight, image and pixels) need to be synchronized everywhere, not only inside the setPixel and componentResized methods. For example, if your case is like the scenario I just described, then unfortunately the for loops should also be in a synchronized (mySyncGuard) block.
    3. This wouldn't fit in a comment. If you post some more code, then we can probably tell you what went wrong (and I may delete this answer if it is not needed).