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.
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.
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?
Graphics Support For JavaFX applications to take advantage of the new hardware acceleration pipeline provided by JavaFX, your system must feature one of a wide range of GPUs currently available in the market.
Class GraphicsContext. This class is used to issue draw calls to a Canvas using a buffer. Each call pushes the necessary parameters onto the buffer where they will be later rendered onto the image of the Canvas node by the rendering thread at the end of a pulse.
Canvas is an image that can be drawn on using a set of graphics commands provided by a GraphicsContext . A Canvas node is constructed with a width and height that specifies the size of the image into which the canvas drawing commands are rendered. All drawing operations are clipped to the bounds of that image.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With