Тяжелая задача рендеринга (в canvas) в GUI блоков JavaFX

Я хочу создать приложение, которое выполняет множество изображений на холсте. Обычный способ JavaFX блокирует GUI: очень трудно нажать кнопку в коде приложения ниже (запустите С Java 8).

Я искал в интернете, но JavaFX не поддерживает фоновый рендеринг: все операции рендеринга (например, strokeLine) хранятся в буфере и выполняются в потоке приложения JavaFX позже. Поэтому я даже не могу использовать два холста и обменять потом отрисовка.

также javafx.сцена.Узел.снимок (SnapshotParameters, WritableImage) не может использоваться для создания изображения в фоновом потоке, так как он должен работать внутри потока приложения JavaFX, и поэтому он также заблокирует GUI.

есть идеи иметь неблокирующий GUI со многими операциями рендеринга? (Я просто хочу нажать кнопки и т. д. в то время как рендеринг выполняется как-то в фоновом режиме или регулярно приостанавливается)

package canvastest;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;

public class DrawLinieTest extends Application
{
    int             interations     = 2;

    double          lineSpacing     = 1;

    Random          rand            = new Random(666);

    List<Color>     colorList;

    final VBox      root            = new VBox();

    Canvas          canvas          = new Canvas(1200, 800);

    Canvas          canvas2         = new Canvas(1200, 800);

    ExecutorService executorService = Executors.newSingleThreadExecutor();

    Future<?>       drawShapesFuture;

    {
        colorList = new ArrayList<>(256);
        colorList.add(Color.ALICEBLUE);
        colorList.add(Color.ANTIQUEWHITE);
        colorList.add(Color.AQUA);
        colorList.add(Color.AQUAMARINE);
        colorList.add(Color.AZURE);
        colorList.add(Color.BEIGE);
        colorList.add(Color.BISQUE);
        colorList.add(Color.BLACK);
        colorList.add(Color.BLANCHEDALMOND);
        colorList.add(Color.BLUE);
        colorList.add(Color.BLUEVIOLET);
        colorList.add(Color.BROWN);
        colorList.add(Color.BURLYWOOD);

    }

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

    @Override
    public void start(Stage primaryStage)
    {
        primaryStage.setTitle("Drawing Operations Test");

        System.out.println("Init...");

        // inital draw that creates a big internal operation buffer (GrowableDataBuffer)
        drawShapes(canvas.getGraphicsContext2D(), lineSpacing);
        drawShapes(canvas2.getGraphicsContext2D(), lineSpacing);

        System.out.println("Start testing...");
        new CanvasRedrawTask().start();

        Button btn = new Button("test " + System.nanoTime());
        btn.setOnAction((ActionEvent e) ->
        {
            btn.setText("test " + System.nanoTime());
        });

        root.getChildren().add(btn);
        root.getChildren().add(canvas);

        Scene scene = new Scene(root);

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

    private void drawShapes(GraphicsContext gc, double f)
    {
        System.out.println(">>> BEGIN: drawShapes ");

        gc.clearRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());

        gc.setLineWidth(10);

        gc.setLineCap(StrokeLineCap.ROUND);

        long time = System.nanoTime();

        double w = gc.getCanvas().getWidth() - 80;
        double h = gc.getCanvas().getHeight() - 80;
        int c = 0;

        for (int i = 0; i < interations; i++)
        {
            for (double x = 0; x < w; x += f)
            {
                for (double y = 0; y < h; y += f)
                {
                    gc.setStroke(colorList.get(rand.nextInt(colorList.size())));
                    gc.strokeLine(40 + x, 10 + y, 10 + x, 40 + y);
                    c++;
                }
            }
        }

        System.out.println("<<< END: drawShapes: " + ((System.nanoTime() - time) / 1000 / 1000) + "ms");
    }

    public synchronized void drawShapesAsyc(final double f)
    {
        if (drawShapesFuture != null && !drawShapesFuture.isDone())
            return;
        drawShapesFuture = executorService.submit(() ->
        {
            drawShapes(canvas2.getGraphicsContext2D(), lineSpacing);

            Platform.runLater(() ->
            {
                root.getChildren().remove(canvas);

                Canvas t = canvas;
                canvas = canvas2;
                canvas2 = t;

                root.getChildren().add(canvas);
            });

        });
    }

    class CanvasRedrawTask extends AnimationTimer
    {
        long time = System.nanoTime();

        @Override
        public void handle(long now)
        {
            drawShapesAsyc(lineSpacing);
            long f = (System.nanoTime() - time) / 1000 / 1000;
            System.out.println("Time since last redraw " + f + " ms");
            time = System.nanoTime();
        }
    }
}

редактировать Отредактировал код, чтобы показать, что фоновый поток, который отправляет операции рисования и чем exchange canvas не решает проблему! Потому что все операции рендеринга (например, strokeLine) хранятся в буфере и выполняются в потоке приложения JavaFX позже.

2 ответов


вы рисуете 1,6 миллиона линий на кадр. Это просто много строк и требует времени для рендеринга с помощью конвейера рендеринга JavaFX. Одно возможное решение этой проблемы-не вопрос, все команды рисования в одном кадре, но вместо того, чтобы оказывать постепенно, растягивая команды рисования, так что приложение остается относительно гибкой (например, вы можете закрыть его или взаимодействовать с кнопками и элементами управления на экране отображения). Очевидно, что есть некоторые компромиссы в extra сложность с этим подходом и результатом не так желательна, как просто возможность отображать чрезвычайно большое количество команд рисования в контексте одного кадра 60fps. Таким образом, представленный подход является приемлемым только для некоторых видов приложений.

некоторые способы выполнения инкрементного рендеринга:

  1. только выдавать максимальное количество вызовов каждого кадра.
  2. поместите вызовы рендеринга в буфер, такой как очередь блокировки, и просто слейте максимум количество вызовов каждого кадра из очереди.

вот пример первого варианта.

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.concurrent.*;
import javafx.scene.Scene;
import javafx.scene.canvas.*;
import javafx.scene.control.Button;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.*;

public class DrawLineIncrementalTest extends Application {
    private static final int FRAME_CALL_THRESHOLD = 25_000;

    private static final int ITERATIONS = 2;
    private static final double LINE_SPACING = 1;
    private final Random rand = new Random(666);
    private List<Color> colorList;
    private final WritableImage image = new WritableImage(ShapeService.W, ShapeService.H);

    private final Lock lock = new ReentrantLock();
    private final Condition rendered = lock.newCondition();
    private final ShapeService shapeService = new ShapeService();

    public DrawLineIncrementalTest() {
        colorList = new ArrayList<>(256);
        colorList.add(Color.ALICEBLUE);
        colorList.add(Color.ANTIQUEWHITE);
        colorList.add(Color.AQUA);
        colorList.add(Color.AQUAMARINE);
        colorList.add(Color.AZURE);
        colorList.add(Color.BEIGE);
        colorList.add(Color.BISQUE);
        colorList.add(Color.BLACK);
        colorList.add(Color.BLANCHEDALMOND);
        colorList.add(Color.BLUE);
        colorList.add(Color.BLUEVIOLET);
        colorList.add(Color.BROWN);
        colorList.add(Color.BURLYWOOD);
    }

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

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Drawing Operations Test");

        System.out.println("Start testing...");
        new CanvasRedrawHandler().start();

        Button btn = new Button("test " + System.nanoTime());
        btn.setOnAction(e -> btn.setText("test " + System.nanoTime()));

        Scene scene = new Scene(new VBox(btn, new ImageView(image)));
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private class CanvasRedrawHandler extends AnimationTimer {
        long time = System.nanoTime();

        @Override
        public void handle(long now) {
            if (!shapeService.isRunning()) {
                shapeService.reset();
                shapeService.start();
            }

            if (lock.tryLock()) {
                try {
                    System.out.println("Rendering canvas");
                    shapeService.canvas.snapshot(null, image);
                    rendered.signal();
                } finally {
                    lock.unlock();
                }
            }

            long f = (System.nanoTime() - time) / 1000 / 1000;
            System.out.println("Time since last redraw " + f + " ms");
            time = System.nanoTime();
        }
    }

    private class ShapeService extends Service<Void> {
        private Canvas canvas;

        private static final int W = 1200, H = 800;

        public ShapeService() {
            canvas = new Canvas(W, H);
        }

        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    drawShapes(canvas.getGraphicsContext2D(), LINE_SPACING);

                    return null;
                }
            };
        }

        private void drawShapes(GraphicsContext gc, double f) throws InterruptedException {
            lock.lock();
            try {
                System.out.println(">>> BEGIN: drawShapes ");

                gc.clearRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
                gc.setLineWidth(10);
                gc.setLineCap(StrokeLineCap.ROUND);

                long time = System.nanoTime();

                double w = gc.getCanvas().getWidth() - 80;
                double h = gc.getCanvas().getHeight() - 80;

                int nCalls = 0, nCallsPerFrame = 0;

                for (int i = 0; i < ITERATIONS; i++) {
                    for (double x = 0; x < w; x += f) {
                        for (double y = 0; y < h; y += f) {
                            gc.setStroke(colorList.get(rand.nextInt(colorList.size())));
                            gc.strokeLine(40 + x, 10 + y, 10 + x, 40 + y);
                            nCalls++;
                            nCallsPerFrame++;
                            if (nCallsPerFrame >= FRAME_CALL_THRESHOLD) {
                                System.out.println(">>> Pausing: drawShapes ");
                                rendered.await();
                                nCallsPerFrame = 0;
                                System.out.println(">>> Continuing: drawShapes ");
                            }
                        }
                    }
                }

                System.out.println("<<< END: drawShapes: " + ((System.nanoTime() - time) / 1000 / 1000) + "ms for " + nCalls + " ops");
            } finally {
                lock.unlock();
            }
        }
    }
}

обратите внимание, что для примера можно взаимодействовать со сценой, нажав кнопку Тест во время инкрементного рендеринга. При желании вы можете дополнительно улучшить это, чтобы удвоить буфер изображений моментальных снимков для холста, чтобы пользователь не видел инкрементный рендеринг. Кроме того, поскольку инкрементный рендеринг находится в Службе, можно использовать сервисные средства для отслеживания прогресса рендеринга и ретрансляции этого в пользовательский интерфейс через индикатор выполнения или любые механизмы, которые вы хотите.

для вышеуказанного образца вы можете поиграть с FRAME_CALL_THRESHOLD настройка для изменения максимального количества вызовов, которые выдаются каждому кадру. Текущая настройка 25,000 вызовов на кадр сохраняет пользовательский интерфейс очень отзывчивым. Параметр 2,000,000 будет таким же, как и полный рендеринг холста в одном кадре (поскольку вы выполняете 1,600,000 вызовов в кадре) и никакой инкрементный рендеринг не будет выполнен, однако пользовательский интерфейс не будет реагировать, пока операции рендеринга завершаются для этого фрейма.

Примечание

здесь есть что-то странное. Если вы удалите все материалы параллелизма и двойные холсты в коде в исходном вопросе и просто используете один холст со всей логикой в потоке приложения JavaFX, начальный вызов drawShapes занимает 27 секунд, а затем вызовы занимают меньше секунды, но во всех случаях логика приложения просит систему выполнить ту же задачу. Я не знаю, почему начальный вызов настолько медленный, это похоже на проблему производительности в реализации JavaFX canvas для меня, возможно, связанную с неэффективным распределением буфера. если это так, тогда, возможно, реализация холста JavaFX может быть изменена так, чтобы можно было предоставить подсказку для предлагаемого начального размера буфера, чтобы он был более эффективным выделяет пространство для его внутренней реализации growable buffer. Это может быть что-то стоит подача ошибки или обсуждать это на список рассылки разработчика JavaFX. Также обратите внимание, что проблема очень медленного начального рендеринга холста видна только при выпуске очень большого количества (например, > 500,000) вызовов рендеринга, поэтому это не повлияет на все приложения.


проблема, описанная здесь, также обсуждалась в списке рассылки JavaFX несколько месяцев назад в этом потоке http://mail.openjdk.java.net/pipermail/openjfx-dev/2015-September/017939.html Предлагаемое решение аналогично дается jewelsea.