How does Canvas drawImage optimization work?

Problem

I tried to create a particle system using Canvas. Interestingly, the drawImage method is very fast. I got up to 90,000 particles at 60fps even using drawImage. However, it was only with the same image. When I used different images for the particles, the performance dropped significantly. Then I changed the order of the images so that e. d. all the particles of image1 are drawn first, then the entire image2, and so on, and the performance is good again.

Question

Does anyone know why this is? Is there some kind of internal caching mechanism in drawImage to consider?

code

Here's some sample code:

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class CanvasExample extends Application {

    GraphicsContext gc;
    double width = 800;
    double height = 600;
    Image[] images;

    @Override
    public void start(Stage primaryStage) {

        Canvas canvas = new Canvas( width, height);
        gc = canvas.getGraphicsContext2D();

        BorderPane root = new BorderPane();
        root.setCenter( canvas);

        Scene scene = new Scene( root, width, height);
        scene.setFill(Color.BEIGE);

        primaryStage.setScene(scene);
        primaryStage.show();

        createImages(255);

        AnimationTimer loop = new AnimationTimer() {

            double prev = 0;
            double frameCount = 0;
            double fps = 0;

            @Override
            public void handle(long now) {

                // very basic frame counter
                if( now - prev > 1_000_000_000) {

                    System.out.println( "FPS: " + frameCount);

                    fps = frameCount;

                    prev = now;
                    frameCount = 0;

                } else {
                    frameCount++;
                }

                // clear canvas
                gc.setFill( Color.BLACK);
                gc.fillRect(0, 0, width, height);

                // paint images
                int numIterations = 90000;
                for( int i=0; i < numIterations; i++) {

                    int index = i % 2; // <==== change here: i % 1 is fast, i % 2 is slow

                    gc.drawImage(images[ index], 100, 100);

                }

                gc.setFill(Color.WHITE);
                gc.fillText("fps: " + fps, 0, 10);
            }

        };

        loop.start();

    }

    public void createImages( int count) {

        Rectangle rect = new Rectangle(10,10);
        rect.setFill(Color.RED);

        images = new Image[count];

        for( int i=0; i < count; i++) {
            images[i] = createImage(rect);
        }

    }

    /**
     * Snapshot an image out of a node, consider transparency.
     * 
     * @param node
     * @return
     */
    public Image createImage(Node node) {

        WritableImage wi;

        SnapshotParameters parameters = new SnapshotParameters();
        parameters.setFill(Color.TRANSPARENT);

        int imageWidth = (int) node.getBoundsInLocal().getWidth();
        int imageHeight = (int) node.getBoundsInLocal().getHeight();

        wi = new WritableImage(imageWidth, imageHeight);
        node.snapshot(parameters, wi);

        return wi;

    }

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

}

      

It's very simple. The image is created from a rectangle, then placed in an array, and in the AnimationTimer loop, the image is drawn numerster-times onto the canvas.

When you use index = i % 1

i. i.e. the same image over and over, then the fps rate is 60fps on my system. If you are using index = i % 2

i. i.e. interleaved images, the fps on my system is 14 frames per second. This is a significant difference.

Many thanks for the help!

+1


source to share


1 answer


Background on how canvas works

The canvas saves the buffer. Every time you issue a command to the canvas, such as drawing an image, the command is added to the buffer. On the next render pulse, the buffer is flushed by processing each of the commands and rendering them to a texture. The texture is sent to the graphics card, which displays it on the screen.

Working with tracing in source code

Note. The linked code shown here is outdated and not official as it is a backport, but it is the simplest code to cross-reference online and the implementation is the same (or the same) as the official code in the JDK. So just trace through it:



You call drawImage and GraphicsContext writes the image to the buffer .

For the next JavaFX graphics system pulse or snapshot request, the buffer is empty, issuing render commands . The image command uses the cached texture from the factory resource to render the image.

Why is your application slowing down (I really don't know)

I figured that when you interleave the images you are not saving the "live" image to be displayed in the scene as part of the scene graph, so the texture cache displaces the old image, which beats the system and recreates the texture for the image ( just guess ) ... However, I went through the process in the debugger (remove the shortcut fps on screen and set a breakpoint in BaseShaderGraphics.drawTexture after your app has been running for a few seconds). You will see that the same cached textures are reused. The texture cache seems to be doing well and does the job of caching textures for each image, so I really don't know what the root cause of the observed slowdown would be.

+1


source







All Articles