Search code examples
exceptionhashmapjavafxrunnable

JavaFX thread crashes


I have a JavaFX application that animates robots (black dots) on the screen and paints a little white line on a lighgray background at wherever they've been (think Tron). To do this I keep all the coordinates of the robots and of all the white pixels. The behavior of the robots is controlled by a different thread implementing Runnable and can be changed while the simulation runs. The robot coordinates are stored in a HashMap with Coordinates beeing a class that extends Point and uses doubles for higher internal calculation precision for the x and y values. For the white dots I'm using a HashMap as integer precision is sufficient for them as they don't move and stay at those x and y coordinates on the screen indefinatly.

Now the program runs just fine but when the HashMap storing the points for the white dots grows it's getting increasingly likely to crash the JavaFX thread of the Application (come to think about it, more specifically it's only the canvas where the robots are drawn on.) The sliders of the controls stay responsive and the text fields for iterations and size of the HashMap keep updating. But nothing is animated and after a few seconds the canvas turns white. Increasing ms for Thread.sleep(ms) makes the program more stable but it's painfully slow as it is already. Also it happens more often and sooner on my slow netbook for school (running Win XP) than on my home desktop PC (running Win7 64Bit). The exception are also dofferent ones. For the desktop PC it is the following:

java.lang.InternalError: Unrecognized PGCanvas token: 68
at com.sun.javafx.sg.prism.NGCanvas.renderStream(NGCanvas.java:651)
at com.sun.javafx.sg.prism.NGCanvas.renderContent(NGCanvas.java:320)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:187)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:39)
at com.sun.javafx.sg.BaseNode.render(BaseNode.java:1145)
at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:204)
at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:420)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:187)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:39)
at com.sun.javafx.sg.BaseNode.render(BaseNode.java:1145)
at com.sun.javafx.tk.quantum.ViewPainter.doPaint(ViewPainter.java:117)
at com.sun.javafx.tk.quantum.AbstractPainter.paintImpl(AbstractPainter.java:175)
at com.sun.javafx.tk.quantum.PresentingPainter.run(PresentingPainter.java:73)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask$Sync.innerRunAndReset(FutureTask.java:351)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:178)
at com.sun.prism.render.RenderJob.run(RenderJob.java:37)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:98)
at java.lang.Thread.run(Thread.java:724)

For the netbook it's:

java.lang.IllegalArgumentException: alpha value out of range
at java.awt.AlphaComposite.<init>(AlphaComposite.java:624)
at java.awt.AlphaComposite.getInstance(AlphaComposite.java:689)
at java.awt.AlphaComposite.derive(AlphaComposite.java:761)
at com.sun.prism.j2d.J2DPrismGraphics.setExtraAlpha(J2DPrismGraphics.java:569)
at com.sun.javafx.sg.prism.NGCanvas.renderStream(NGCanvas.java:739)
at com.sun.javafx.sg.prism.NGCanvas.renderContent(NGCanvas.java:389)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:201)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:40)
at com.sun.javafx.sg.BaseNode.render(BaseNode.java:1145)
at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:204)
at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:420)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:201)
at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:40)
at com.sun.javafx.sg.BaseNode.render(BaseNode.java:1145)
at com.sun.javafx.tk.quantum.ViewPainter.doPaint(ViewPainter.java:117)
at com.sun.javafx.tk.quantum.AbstractPainter.paintImpl(AbstractPainter.java:182)
at com.sun.javafx.tk.quantum.PresentingPainter.run(PresentingPainter.java:73)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask$Sync.innerRunAndReset(FutureTask.java:351)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:178)
at com.sun.prism.render.RenderJob.run(RenderJob.java:37)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:108)
at java.lang.Thread.run(Thread.java:722)

Any help to fix or narrow down this problem would be greatly appreciated.

edit: As suggested I'm going to add a minimal example of my code that still exhibits the problem. The main method lies within the class Visualizer that draws the simulation area

import java.awt.Point;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;

public class Visualizer extends Application {

    private static GraphicsContext gc;
    private static Canvas canvas;
    private static BorderPane pane;
    private static Scene scene;
    private static Thread thread;
    private static Simulator sim = new Simulator();
    private static int optionsWidth = 200;
    private static HashSet<Point> manganCollected = new HashSet<Point>();
    private static HashMap<Integer, Coordinates> coordinates = new HashMap<Integer, Coordinates>();
    private static final int zoom = 4;
    private static Point cmpP;
    /**
     * Height in pixels that's available to draw the simulation on dependent on used monitor resolution 
     */
    public static double simHeight;
    /**
     * Width in pixels that's available to draw the simulation on dependent on used monitor resolution 
     */
    public static double simWidth;


    /**
     * Launches the JavaFX application
     * @param args command line arguments if there are any
     */
    public static void main(String[] args) {
        launch(args);   
    }

    /**
     * Sets up the GUI with all options and the canvas to draw the robots on 
     */
    @Override
    public void start(Stage stage) throws Exception {

        // Determine screen width of the monitor
        Screen screen = Screen.getPrimary();
        Rectangle2D bounds = screen.getVisualBounds();
        double screenHeight = bounds.getHeight();
        double screenWidth = bounds.getWidth();

        // set the stage
        stage.setFullScreen(true);
        stage.setHeight(screenHeight);
        stage.setWidth(screenWidth);
        stage.setTitle("Manganernte");

        // Canvas to draw the simulation on 
        canvas = new Canvas();
        simHeight = screenHeight;
        simWidth = screenWidth - optionsWidth;
        canvas.setHeight(simHeight);
        canvas.setWidth(simWidth);
        gc = canvas.getGraphicsContext2D();
        gc = canvas.getGraphicsContext2D();
        gc.setFill(Color.LIGHTGRAY);
        gc.fillRect(0, 0, simWidth, simHeight);
        gc.setStroke(Color.BLACK);
        gc.setLineWidth(3);
        gc.strokeRect(0, 0, simWidth, simHeight);

        // BorderPane containing the buttons box and the Simulator canvas
        pane = new BorderPane();
        pane.setCenter(canvas);

        // Scene containing the pane
        scene = new Scene(pane);

        // Show the whole stage
        stage.setScene(scene);
        stage.show();
        thread = new Thread(sim);
        thread.start();
    }

    /**
     * Transforms double coordinates as used by the simulator (0/0 in the center) to monitor coordinates (0/0 top left corner)
     * @param coordinates Floating point coordinates that should be transformed
     * @return Coordinates Floating point coordinates that have been transformed
     */
    private static Coordinates transform(Coordinates coordinates) {
        return new Coordinates(Math.round((simWidth / 2) + (zoom * coordinates.getX())), Math.round((simHeight / 2) + (zoom * coordinates.getY()))); 
    }

    /**
     * Transforms integer coordinates as used by the simulator (0/0 in the center) to monitor coordinates (0/0 top left corner)
     * @param Point Integer coordinates that should be transformed
     * @return Point Integer coordinates that have been transformed
     */
    private static Point transform(Point point) {
        return new Point((int)Math.round((simWidth / 2) + (zoom * point.getX())), (int)Math.round(((simHeight / 2) + (zoom * point.getY())))); 
    }

    /**
     * Clear the canvas by drawing a rectangle filled with light gray background
     */
    private static void clear () {
        gc.setFill(Color.LIGHTGRAY);
        gc.fillRect(0, 0, simWidth, simHeight);
        gc.setStroke(Color.BLACK);
        gc.setLineWidth(3);
        gc.strokeRect(0, 0, simWidth, simHeight);
    }

    /**
     * Clears the canvas and then draws first the collected mangan as white rectangles followed by robots as black circles the given coordinates
     * @param redraw boolean that's set to true if iteration hasn't changed but a redraw should be forced anyway (e.g. when simulation is paused and the zoom is used)
     */
    public static void DrawRobots() {
        coordinates = Simulator.coordinates;
        manganCollected = Simulator.manganCollected;
        // clear the canvas with light gray background
        clear();
        // draw harvested mangan as white dots
        gc.setFill(Color.WHITE);
        Iterator<Point> it = manganCollected.iterator(); 
        while(it.hasNext()) {
            cmpP = it.next().getLocation();
            double x = transform(cmpP).getX();
            double y = transform(cmpP).getY();
            gc.fillRect(x, y, zoom, zoom);
        }
        // draw robots
        gc.setFill(Color.BLACK);
        for(int i = 1; i <= coordinates.size(); i++) {
            double x = transform(coordinates.get(i)).getX();
            double y = transform(coordinates.get(i)).getY();
            gc.fillOval(x, y, zoom, zoom);
        }
    }
}

The Simulator class:

import java.awt.Point;
import java.util.HashMap;
import java.util.HashSet;

public class Simulator implements Runnable {

    // start variable declarations
    // HashMap of all robot objects the simulator controls
    private static HashMap<Integer, Robot> robots = new HashMap<Integer, Robot>();
    // HashMap of all coordinate objects the simulator controls
    public static HashMap<Integer, Coordinates> coordinates = new HashMap<Integer, Coordinates>();
    // HashMap of all point objects containing the coordinates of places where the mangan has already been collected
    public static HashSet<Point> manganCollected = new HashSet<Point>();
    /**
     * communication radius of the robots according to the requirements
     */
    public static int processSpeed = 100;
    // end variable declarations

    /**
     * Create a robot with x and y
     * @param x x-coordinate
     * @param y y-coordinate
     */
    public static void createRobot(int x, int y) {
        coordinates.put(coordinates.size() + 1, new Coordinates(x, y));
        robots.put(robots.size() + 1, new Robot());
    }

    /**
     * Checks the status, changes it if necessary and acts accordingly
     */
    @Override
    public void run() {
        for(int i = 0; i < 100; i++) createRobot(i - 50, 0);
        Visualizer.DrawRobots();
        while(true) {
            for(int i = 1; i <= robots.size(); i++) robots.get(i).think();
            for(int i = 1; i <= robots.size(); i++) {
                coordinates.get(i).add(robots.get(i).move());
                manganCollected.add(new Point((int)Math.round(coordinates.get(i).getX()), (int)Math.round(coordinates.get(i).getY())));
            }
            Visualizer.DrawRobots();
            try {
                Thread.sleep(processSpeed);
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

And finally the robot class that doesn't do much at all anymore in this minimal example:

public class Robot {

    Coordinates future = new Coordinates(0, 0);

    public void think () {
        future.setY(1.0);
    }

    public Coordinates move() {
        return future;
    }
}

UPDATE Just installed JDK 8 and now the exception is much more telling:

Exception in thread "Thread-4" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4

Searching the web a bit it seems like I need to use Platform.runLater() to avoid this from happening. But I'm not sure how to do this. I'm going to try and explain again how my project is structured right now:

I've got Class Visualizer that extends Application which also includes the main() method and draws the UI including the simulation area as a grey rectangle on a GraphicsContext Canvas. After the UI is done it creates a thread of Class Simulator that implements Runnable. The run() method within the Simulator class it enters a while(true) loop. As long as no button has been clicked the loop does nothing but thread.sleep(). If the start button is clicked some calculations are performed and some methods on Visualizer called that draw stuff on the simulation canvas. It is my understanding that these calls cause the exception because they are made from a non JavaFX thread.

Do I have to use runLater() to avoid this problem and if so where and how do I do this?

UPDATE 2 Finally got it to run stable (as far as I can tell by now) by wrapping every the code of public method that changes anything about the UI/Canvas into a runLater Block e.g.

public static void drawStuff() {
    Platform.runLater(new Runnable() {
    @Override
        public void run() {
            // draw stuff
        }
    });
}

Solution

  • Just posting this so it is immediately obvious to the reader that the issue actually has an answer/resolution.

    I had the same issue and as the author points out himself in the end as an "update".

    If you have this issue you are probably making ui canvas operations in another thread. You should do ui operations always on the Application Thread! Use

    Platform.runLater(()->{
        //your code
    });