'How to improve performance of JavaFX graphic drawing?

Situation :

I've created an app which needs to draw simple rectangles (1x1 - 3x3 size) depends on some variables stored in array of corresponding size (900x900 - 300x300 size) at 20, 40 or even 60 FPS rate.

Here's my drawing method :

protected void drawCurrentState() {
    for (int i = 0; i < gameLogic.size; i++) {
        for (int j = 0; j < gameLogic.size; j++) {
            if (gameLogic.previousStepTable == null || gameLogic.previousStepTable[i][j] != gameLogic.gameTable[i][j]) {
                if (gameLogic.gameTable[i][j].state)
                    graphicsContext.setFill(gameLogic.gameTable[i][j].color); 
                else
                    graphicsContext.setFill(Color.WHITE); 

                graphicsContext.fillRect(leftMargin + j * (cellSize + cellMargin), topMargin + i * (cellSize + cellMargin), cellSize, cellSize);
            }
        }
    }
}

As you probably noticed, drawing is performed only when it need to (when state has changed from previous step). All the other operations (computing variables states in gameLogic etc.) takes nearly no time and has no affect on performance.

Problem :

JavaFX performs this operation incredibly slow! 10 iterations (which should be drawed at 0.5sec on 20FPS rate) are drawed in 5-10 seconds, which obviously is unacceptable in this case.

Question :

Is there any way to massively speed-up that drawing operation to expected level of performance (40 or 60 iterations in a second for example)?

And whats the reason of such poor performance of drawing in this case?



Solution 1:[1]

I ran a few benchmarks on a couple of different implementations as below:

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.canvas.*;
import javafx.scene.image.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import java.nio.IntBuffer;

public class CanvasUpdater extends Application {
    private static final int CELL_SIZE = 2;
    private static final int BOARD_SIZE = 900;

    private static final int W = BOARD_SIZE * CELL_SIZE;
    private static final int H = BOARD_SIZE * CELL_SIZE;

    private static final double CYCLE_TIME_IN_MS = 5_000;

    private final WritablePixelFormat<IntBuffer> pixelFormat = 
            PixelFormat.getIntArgbPreInstance();

    private Canvas canvas = new Canvas(W, H);
    private GraphicsContext gc = canvas.getGraphicsContext2D();
    private int[] buffer = new int[W * H];

    @Override
    public void start(Stage stage) {
        final long start = System.nanoTime();
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                double t = 
                        ((now - start) % (1_000_000 * CYCLE_TIME_IN_MS * 2)) / 
                        (1_000_000.0 * CYCLE_TIME_IN_MS);
                if (t > 1) t = 1 - (t - 1);
                Color c1 = Color.RED.interpolate(Color.BLUE, t);
                Color c2 = Color.BLUE.interpolate(Color.RED, t);

                gc.clearRect(0, 0, W, H);

//                Draw using fillRect
//
//                for (int i = 0; i < W; i += CELL_SIZE) {
//                    for (int j = 0; j < H; j += CELL_SIZE) {
//                        gc.setFill(
//                                (i/CELL_SIZE + j/CELL_SIZE) % 2 == 0
//                                        ? c1
//                                        : c2
//                        );
//                        gc.fillRect(i, j, CELL_SIZE, CELL_SIZE);
//                    }
//                }

//                Draw using setColor
//
//                PixelWriter p = gc.getPixelWriter();
//                for (int i = 0; i < W; i += CELL_SIZE) {
//                    for (int j = 0; j < H; j += CELL_SIZE) {
//                        Color c =
//                                (i/CELL_SIZE + j/CELL_SIZE) % 2 == 0
//                                        ? c1
//                                        : c2;
//                        for (int dx = 0; dx < CELL_SIZE; dx++) {
//                            for (int dy = 0 ; dy < CELL_SIZE; dy++) {
//                                p.setColor(i + dx, j + dy, c);
//                            }
//                        }
//                    }
//                }

//              Draw using buffer
//
                int ci1 = toInt(c1);
                int ci2 = toInt(c2);

                for (int i = 0; i < W; i += CELL_SIZE) {
                    for (int j = 0; j < H; j += CELL_SIZE) {
                        int ci =
                                (i/CELL_SIZE + j/CELL_SIZE) % 2 == 0
                                        ? ci1
                                        : ci2;
                        for (int dx = 0; dx < CELL_SIZE; dx++) {
                            for (int dy = 0 ; dy < CELL_SIZE; dy++) {
                                buffer[i + dx + W * (j + dy)] = ci;
                            }
                        }
                    }
                }

                PixelWriter p = gc.getPixelWriter();
                p.setPixels(0, 0, W, H, pixelFormat, buffer, 0, W);
            }
        };
        timer.start();

        stage.setScene(new Scene(new Group(canvas)));
        stage.show();
    }

    private int toInt(Color c) {
        return
                (                      255  << 24) |
                ((int) (c.getRed()   * 255) << 16) |
                ((int) (c.getGreen() * 255) << 8)  |
                ((int) (c.getBlue()  * 255));
    }

    public static void main(String[] args) {
        launch(args);
    }
}

The above program was run with various implementations with the JavaFX PulseLogger switched on -Djavafx.pulseLogger=true. As you can see, using a buffer to set the pixels in a PixelWriter was 50 times faster than filling rectangles in the canvas and 100 times faster than invoking setColor on the PixelWriter.

fillrect

//PULSE: 81 [217ms:424ms]
//T15 (58 +0ms): CSS Pass
//T15 (58 +0ms): Layout Pass
//T15 (58 +0ms): Update bounds
//T15 (58 +155ms): Waiting for previous rendering
//T15 (214 +0ms): Copy state to render graph
//T12 (214 +0ms): Dirty Opts Computed
//T12 (214 +209ms): Painting
//T12 (424 +0ms): Presenting
//Counters:
//Nodes rendered: 2
//Nodes visited during render: 2

pixelwriter using setColor

//PULSE: 33 [370ms:716ms]
//T15 (123 +0ms): CSS Pass
//T15 (123 +0ms): Layout Pass
//T15 (123 +0ms): Update bounds
//T15 (123 +244ms): Waiting for previous rendering
//T15 (368 +0ms): Copy state to render graph
//T12 (368 +0ms): Dirty Opts Computed
//T12 (368 +347ms): Painting
//T12 (715 +0ms): Presenting
//Counters:
//Nodes rendered: 2
//Nodes visited during render: 2

pixelwriter using buffer

//PULSE: 270 [33ms:37ms]
//T15 (28 +0ms): CSS Pass
//T15 (28 +0ms): Layout Pass
//T15 (28 +0ms): Update bounds
//T15 (28 +0ms): Waiting for previous rendering
//T15 (28 +0ms): Copy state to render graph
//T12 (29 +0ms): Dirty Opts Computed
//T12 (29 +7ms): Painting
//T12 (36 +0ms): Presenting
//Counters:
//Nodes rendered: 2
//Nodes visited during render: 2

Solution 2:[2]

This looks like a standard problem: Drawing pixel-wise is damn slow. And drawing your tiny rectangles qualifies as nearly pixel-wise.

In case you're drawing in some canvas directly, try a BufferedImage, it's an in-normal-memory data structure and should be much faster than accessing graphic card memory. Then draw the image to where it belongs.

Otherwise, draw in an int[] rgbArray by setting pixels manually and use BufferedImage#setRGB or alike.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 maaartinus