JavaFX中的重渲染任务(在canvas中)阻止GUI

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

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

此外,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 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("<< { 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(); } } } 

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

你每帧绘制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 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 { private Canvas canvas; private static final int W = 1200, H = 800; public ShapeService() { canvas = new Canvas(W, H); } @Override protected Task createTask() { return new Task() { @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(); } } } } 

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

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

边注

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

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