JavaFX中的重渲染任务(在画布中)阻止GUI [英] Heavy rendering task (in canvas) in JavaFX blocks GUI

查看:645
本文介绍了JavaFX中的重渲染任务(在画布中)阻止GUI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想创建一个在画布中执行许多渲染的应用程序。
正常的JavaFX方式阻止了GUI:按下下面的应用程序代码中的按钮真的很难(用Java 8运行)。

I want to create an application that performs many renderings in a canvas. The normal JavaFX way blocks the GUI: It is realy hard to press the button in the application code below (run with Java 8).

我搜索了web,但JavaFX不支持后台渲染:所有渲染操作(如strokeLine)都存储在缓冲区中,稍后在JavaFX应用程序线程中执行。因此我甚至无法使用两个画布并在渲染后进行交换。

I searched the web, but JavaFX does not support background rendering: All rendering operation (like strokeLine) are stored in a buffer and are executed in the JavaFX application thread later. So I cannot even use two canvases and exchange then after rendering.

此外,javafx.scene.Node.snapshot(SnapshotParameters,WritableImage)不能用于创建图像一个后台线程,因为它需要在JavaFX应用程序线程内部运行,所以它也会阻止GUI。

Also the javafx.scene.Node.snapshot(SnapshotParameters, WritableImage) cannot be used to create an image in a background thread, as it needs to run inside the JavaFX application thread and so it will block the GUI also.

任何有非阻塞GUI和许多渲染操作的想法? (我只想按下按钮等,同时在背景中以某种方式执行渲染或定期暂停)

Any ideas to have a non blocking GUI with many rendering operations? (I just want to press buttons etc. while the rendering is performed somehow in background or paused regularly)

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();
        }
    }
}

编辑编辑代码以显示发送绘制操作和交换画布的后台线程无法解决问题!因为所有渲染操作(如strokeLine)都存储在缓冲区中,稍后会在JavaFX应用程序线程中执行。

EDIT Edited the code to show that a background thread that sends the draw operations and than exchange the canvas does not resolve the problem! Because All rendering operation (like strokeLine) are stored in a buffer and are executed in the JavaFX application thread later.

推荐答案

每帧绘制160万行。它只是很多行,并且需要时间来使用JavaFX渲染管道进行渲染。一种可能的解决方法是不在一个帧中发出所有绘图命令,而是以递增方式渲染,间隔绘制命令,以便应用程序保持相对响应(例如,您可以关闭它或与应用程序上的按钮和控件交互)正在渲染)。显然,使用这种方法存在一些额外复杂性的权衡,并且结果不如仅仅能够在单个60fps帧的上下文中呈现极大量的绘制命令那样令人满意。因此,所提出的方法仅适用于某些类型的应用程序。

You are drawing 1.6 million lines per frame. It is simply a lot of lines and takes time to render using the JavaFX rendering pipeline. One possible workaround is not to issue all drawing commands in a single frame, but instead render incrementally, spacing out drawing commands, so that the application remains relatively responsive (e.g. you can close it down or interact with buttons and controls on the app while it is rendering). Obviously, there are some tradeoffs in extra complexity with this approach and the result is not as desirable as simply being able to render extremely large amounts of draw commands within the context of single 60fps frame. So the presented approach is only acceptable for some kinds of applications.

执行增量渲染的一些方法是:

Some ways to perform an incremental render are:


  1. 每帧只发出一次最大调用次数。

  2. 将渲染调用放入缓冲区,例如阻塞队列,只消耗最多的调用次数队列中的每一帧。

以下是第一个选项的示例。

Here is a sample of the first option.

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();
            }
        }
    }
}

注意对于样本,可以在增量渲染过程中通过单击测试按钮与场景进行交互。如果需要,您可以进一步增强此功能,以便对画布的快照图像进行双重缓冲,以便用户看不到增量渲染。此外,由于增量渲染位于服务中,您可以使用服务工具跟踪渲染进度,并通过进度条或您希望的任何机制将其转发到UI。

Note that for the sample, it is possible to interact with the scene by clicking the test button while the incremental rendering is in progress. If desired, you could further enhance this to double buffer the snapshot images for the canvas so that the user doesn't see the incremental rendering. Also because the incremental rendering is in a Service, you can use the service facilities to track rendering progress and relay that to the UI via a progress bar or whatever mechanisms you wish.

对于上面的示例,您可以使用 FRAME_CALL_THRESHOLD 设置来改变每帧发出的最大呼叫数。每帧25,000个呼叫的当前设置使UI非常灵敏。设置为2,000,000与在单个帧中完全渲染画布相同(因为您在帧中发出1,600,000个调用)并且不会执行增量渲染,但是在渲染操作完成时UI不会响应对于那个框架。

For the above sample you can play around with the FRAME_CALL_THRESHOLD setting to vary the maximum number of calls which are issued each frame. The current setting of 25,000 calls per frame keeps the UI very responsive. A setting of 2,000,000 would be the same as fully rendering the canvas in a single frame (because you are issuing 1,600,000 calls in the frame) and no incremental rendering will be performed, however the UI will not be responsive while the rendering operations are being completed for that frame.

边注

这里有一些奇怪的东西。如果删除原始问题中代码中的所有并发内容和双重画布,并且只使用JavaFX应用程序线程上的所有逻辑的单个画布,则drawShapes的初始调用需要27秒,后续调用需要的时间少于第二,但在所有情况下,应用程序逻辑都要求系统执行相同的任务。我不知道为什么初始调用这么慢,这似乎是JavaFX canvas实现中的性能问题,可能与低效的缓冲区分配有关。 如果就是这种情况,那么也许可以调整JavaFX画布实现,以便可以提供建议的初始缓冲区大小的提示,以便更有效地为其内部可扩展缓冲区实现分配空间。可能值得提交错误或在 JavaFX开发人员邮件列表。另请注意,只有当您发出非常大的数字(例如> 500,000)渲染调用时,画布的初始渲染速度非常慢才会出现,因此它不会影响所有应用程序。

There is something weird here. If you remove all of the concurrency stuff and the double canvases in the code in the original question and just use a single canvas with all logic on the JavaFX application thread, the initial invocation of drawShapes takes 27 seconds, and subsequent invocations take less that a second, but in all cases the application logic is asking the system to perform the same task. I don't know why the initial call is so slow, it seems like a performance issue in the JavaFX canvas implementation to me, perhaps related to inefficient buffer allocation. If that is the case, then perhaps the JavaFX canvas implementation could be tweaked so that a hint for a suggested initial buffer size could be provided, so that it more efficiently allocates space for its internal growable buffer implementation. It might be something worth filing a bug or discussing it on the JavaFX developer mailing list. Also note that the issue of a very slow initial rendering of the canvas is only visible when you issue a very large number (e.g. > 500,000) of rendering calls, so it won't effect all applications.

这篇关于JavaFX中的重渲染任务(在画布中)阻止GUI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆