2016-02-12 29 views
6

Desidero creare un'applicazione che esegua molti rendering in un'area di disegno. Il normale modo JavaFX blocca la GUI: è davvero difficile premere il pulsante nel codice dell'applicazione sottostante (eseguito con Java 8).Attività di rendering pesante (in tela) in blocchi JavaFX GUI

Ho cercato sul Web, ma JavaFX non supporta il rendering in background: tutte le operazioni di rendering (come strokeLine) vengono archiviate in un buffer e vengono eseguite successivamente nel thread dell'applicazione JavaFX. Quindi non posso nemmeno usare due tele e scambiare poi dopo il rendering.

Anche javafx.scene.Node.snapshot (SnapshotParameters, WritableImage) non può essere utilizzato per creare un'immagine in un thread in background, poiché deve essere eseguito all'interno del thread dell'applicazione JavaFX e pertanto bloccherà anche la GUI.

Qualche idea per avere una GUI non bloccante con molte operazioni di rendering? (Voglio solo di premere pulsanti, ecc, mentre il rendering viene eseguito in qualche modo in background o in pausa regolarmente)

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

EDIT A cura il codice per dimostrare che un thread in background che invia le operazioni di disegno e di scambiare la tela non risolve il problema! Perché Tutte le operazioni di rendering (come strokeLine) vengono archiviate in un buffer e vengono eseguite successivamente nel thread dell'applicazione JavaFX.

+1

In passato si disegnava su una bitmap e poi su un bitblt sullo schermo. –

+0

* "Quindi non posso nemmeno usare due tele e scambiare dopo il rendering." * - dovresti essere in grado di farlo. La documentazione [GraphicsContext] (https://docs.oracle.com/javase/8/javafx/api/javafx/scene/canvas/GraphicsContext.html) dice: ** "Un Canvas contiene solo un GraphicsContext e un solo buffer Se non è collegato a nessuna scena, può essere modificato da qualsiasi thread, purché venga utilizzato solo da un thread alla volta. "** – Marco13

+0

@Romain Hippeau: _" Nei vecchi tempi si disegnava una bitmap e poi bitblt sullo schermo "_: questo è quello che ho fatto nella vecchia versione con BufferedImage. Ma ora voglio una soluzione JavaFX pura. E come ho detto: JavaFX non consente di creare un'istantanea in un thread in background. – Mahe

risposta

5

Si sta disegnando 1,6 milioni di righe per fotogramma. Sono solo un sacco di linee e richiede tempo per eseguire il rendering utilizzando la pipeline di rendering JavaFX. Una soluzione possibile non è quella di emettere tutti i comandi di disegno in un singolo fotogramma, ma di eseguire il rendering in modo incrementale, distanziando i comandi di disegno, in modo che l'applicazione rimanga relativamente reattiva (ad esempio puoi chiuderla o interagire con pulsanti e controlli sull'app mentre è rendering). Ovviamente, ci sono alcuni compromessi in termini di complessità extra con questo approccio e il risultato non è tanto desiderabile quanto semplicemente essere in grado di eseguire una quantità estremamente grande di comandi di disegno nel contesto del singolo frame a 60fps. Quindi l'approccio presentato è accettabile solo per alcuni tipi di applicazioni.

Alcuni modi per eseguire un rendering incrementale sono:

  1. emettere soltanto un numero massimo di chiamate ogni telaio.
  2. Posiziona le chiamate di rendering in un buffer come una coda di blocco e scarica semplicemente un numero massimo di chiamate per ciascun fotogramma dalla coda.

Ecco un esempio della prima opzione.

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

Si noti che per il campione, è possibile interagire con la scena facendo clic sul pulsante di test, mentre la resa incrementale è in corso. Se lo si desidera, è possibile migliorare ulteriormente questo per raddoppiare il buffer delle immagini di istantanee per il canvas in modo che l'utente non veda il rendering incrementale. Inoltre, poiché il rendering incrementale si trova in un servizio, puoi utilizzare le strutture del servizio per tenere traccia dell'avanzamento del rendering e inoltrarlo all'interfaccia utente tramite una barra di avanzamento o qualsiasi altro meccanismo desideri.

Per il campione precedente è possibile giocare con l'impostazione FRAME_CALL_THRESHOLD per variare il numero massimo di chiamate che vengono emesse per ciascun fotogramma. L'impostazione attuale di 25.000 chiamate per frame mantiene l'interfaccia utente molto reattiva.Un'impostazione di 2.000.000 equivale a eseguire il rendering completo dell'area di disegno in un singolo fotogramma (poiché si emettono 1.600.000 chiamate nel fotogramma) e non verrà eseguito alcun rendering incrementale, tuttavia l'interfaccia utente non sarà reattiva mentre vengono completate le operazioni di rendering. per quella cornice.

Nota a margine

C'è qualcosa di strano qui. Se rimuovi tutta la roba relativa alla concorrenza e le doppie tele nel codice nella domanda originale e utilizzi solo una singola tela con tutta la logica sul thread dell'applicazione JavaFX, l'invocazione iniziale di drawShapes richiede 27 secondi e le successive chiamate richiedono meno in secondo luogo, ma in tutti i casi la logica dell'applicazione chiede al sistema di eseguire lo stesso compito. Non so perché la chiamata iniziale sia così lenta, mi sembra un problema di prestazioni nell'implementazione della tela JavaFX, forse correlato all'allocazione del buffer inefficiente. Se è il caso, allora forse l'implementazione della tela JavaFX potrebbe essere ottimizzata in modo da fornire un suggerimento per una dimensione del buffer iniziale suggerita, in modo da allocare in modo più efficiente lo spazio per l'implementazione interna del buffer. Potrebbe essere qualcosa che vale filing a bug o discuterne sul JavaFX developer mailing list. Si noti inoltre che il problema di un rendering iniziale molto lento della tela è visibile solo quando si emette un numero molto elevato (ad es.> 500.000) di chiamate di rendering, quindi non avrà effetto su tutte le applicazioni.

+1

Soluzione davvero bella! – Mahe

+0

Soluzione ben descritta, come sempre @jewelsea! – Birdasaur