【问题标题】:Heavy rendering task (in canvas) in JavaFX blocks GUIJavaFX 块 GUI 中的繁重渲染任务(在画布中)
【发布时间】:2016-02-12 11:46:18
【问题描述】:

我想创建一个在画布中执行许多渲染的应用程序。 普通的 JavaFX 方式会阻塞 GUI:在下面的应用程序代码中按下按钮非常困难(使用 Java 8 运行)。

我在网上搜索过,JavaFX 不支持后台渲染:所有的渲染操作(如strokeLine)都存储在缓冲区中,稍后在JavaFX 应用程序线程中执行。所以我什至不能使用两个画布并在渲染后进行交换。

此外,javafx.scene.Node.snapshot(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();
        }
    }
}

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

【问题讨论】:

  • 在过去,您绘制位图,然后在屏幕上进行 bitblt。
  • “所以我什至不能使用两个画布并在渲染后进行交换。” - 你应该可以这样做。 GraphicsContext 文档说:“一个 Canvas 只包含一个 GraphicsContext,并且只有一个缓冲区。如果它没有附加到任何场景,那么它可以被任何线程修改,只要它只从一个使用一次线程。”
  • @Romain Hippeau:“过去你在位图上绘制,然后在屏幕上进行 bitblt”:这就是我在旧版本中使用 BufferedImage 所做的。但现在我想要一个纯 JavaFX 解决方案。正如我所说:JavaFX 不允许在后台线程中创建快照。
  • @Marco13:好吧,我写的有点不对。您可以交换画布节点。但这并没有太大的区别。函数“drawShapes”只需要几毫秒。这就是我通过交换画布节点可以节省的全部内容。正如我所说:所有渲染操作(如strokeLine)都存储在缓冲区中,稍后在JavaFX应用程序线程中执行。
  • 我明白了。有一些相关的问题,但似乎没有一个有用。在某些时候,必须进行绘制,据我所知,所有可能的解决方法(例如Canvas#snapshot)都没有带来优势,因为它们(因此,实际的绘制)总是绑定到 JavaFX 线程.真遗憾。更丑陋的解决方法(比如使用 Swing 将这些东西绘制成 BufferedImage - 他们 仍然做得对)在这里可能是不可接受的。

标签: java canvas javafx-8


【解决方案1】:

您每帧绘制 160 万条线。使用 JavaFX 渲染管道进行渲染只是很多行并且需要时间。一种可能的解决方法是不在单个帧中发出所有绘图命令,而是增量渲染,将绘图命令间隔开,以便应用程序保持相对响应(例如,您可以在应用程序关闭它或与应用程序上的按钮和控件交互时)正在渲染)。显然,这种方法在额外复杂性方面存在一些折衷,其结果不如简单地能够在单个 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();
            }
        }
    }
}

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

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

旁注

这里有些奇怪。如果您删除原始问题中代码中的所有并发内容和双画布,并仅在 JavaFX 应用程序线程上使用具有所有逻辑的单个画布,则 drawShapes 的初始调用需要 27 秒,随后的调用需要更少的时间第二,但在所有情况下,应用程序逻辑都要求系统执行相同的任务。我不知道为什么初始调用这么慢,对我来说这似乎是 JavaFX 画布实现中的一个性能问题,可能与低效的缓冲区分配有关。 如果是这种情况,那么也许可以调整 JavaFX 画布实现,以便可以提供建议的初始缓冲区大小的提示,以便更有效地为其内部可增长缓冲区实现分配空间。这可能值得filing a bug 或在JavaFX developer mailing list 上讨论。另请注意,画布初始渲染速度非常慢的问题仅在您发出大量(例如 > 500,000)渲染调用时才可见,因此不会影响所有应用程序。

【讨论】:

  • 非常好的解决方案!
  • 很好的解决方案,一如既往@jewelsea!
【解决方案2】:

这里描述的问题也已在几个月前的 JavaFX 邮件列表中在此线程中进行了讨论 http://mail.openjdk.java.net/pipermail/openjfx-dev/2015-September/017939.html 建议的解决方案类似于jewelsea 给出的解决方案。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-16
    • 2013-05-30
    • 1970-01-01
    • 1970-01-01
    • 2017-12-20
    • 2012-11-23
    相关资源
    最近更新 更多