javafximageviewtaskcontrolsstage

Controller Stage Freezing Display Stage


I have a project that uses a controller stage to manipulate and color a display stage (which has an ImageView with a WritableImage). The problem I have been running into is that the display stage will freeze up before it has finished the task of coloring the entire image. It appears as though the problem is with the second stage popping up, since whenever it is removed and the display stage is filled automatically, it loads without a problem - except for the problem that there is no way to control the display stage.

Here are some results from playing around with this:

  1. Although clicking the play button once hardly ever works, clicking it multiple times usually gets the display to load.

  2. If the button is clicked a few times and the display still hasn't loaded, selecting the display stage will sometimes load it in.

  3. Showing the display or control stage first and using one or the other as the given primaryStage makes no difference.

  4. If either stage is put into another thread or task, it doesn't reach the show statement, crashing whenever it hits the new stage decleration.

    public void start(Stage display_stage) { // Given "primaryStage"
    // Create a basic ImageView to be colored
    WritableImage image = new WritableImage(1920, 1080);
    PixelWriter writer = image.getPixelWriter();
    ImageView view = new ImageView(image);
    
    // Set up display stage
    display_stage.setScene(new Scene(new Pane(view), 1920, 1080));
    
    // Set up play button with coloring action
    Button play_button = new Button("PLAY");
    play_button.setOnAction(e -> {
        new Thread( new Task<Void>() {
            @Override
            protected Void call() {
                // Iterate through every pixel and color it arbitrarily
                for (int y = 0; y < 1080; y++)
                    for (int x = 0; x < 1920; x++)
                        writer.setColor(x, y, Color.rgb(0, 0, 255));
    
                return null;
            }
        }).start();
    });
    
    // Set up control stage
    Stage control_stage = new Stage();
    control_stage.setScene(new Scene(play_button, 100, 100));
    
    // Show stages
    control_stage.show();
    display_stage.show();
    

    }

This is the reduced code - I am using IntelliJ IDEA if that matters. Any pointers are welcome - thanks in advance.

EDIT: I should mention that this is not simply about performing and completing a short task for an image. This for-loop should be expected to continue for a long period of time or indefinitely (unless stopped by the controller). This is why the control stage is needed and why the display must be updating repeatedly.


Solution

  • Task actually contains a very clever mechanism to avoid flooding the FXAT with jobs. It accumulates the changes in an AtomicReference which it updates in the background thread and then clears it out from the FXAT - atomically, so there are no concurrency issues.

    Here's the code for it:

        protected void updateValue(V var1) {
            if (this.isFxApplicationThread()) {
                this.value.set(var1);
            } else if (this.valueUpdate.getAndSet(var1) == null) {
                this.runLater(() -> {
                    this.value.set(this.valueUpdate.getAndSet((Object)null));
                });
            }
        }
    

    Here valueUpdate is the AtomicReference, and this.value is the ObservableValue which can be seen from outside Task. This is a short method, but it takes a little pondering to see how it works. Essentially, Platform.runLater() only gets called once each time the AtomicReference has been cleared out.

    For this use case, you need to have a list of pending PixelWriter updates that are processed on the FXAT. So I implemented it as an ObservableList and then put a Subscription on it to do the calls to PixelWriter on the FXAT.

    The AtomicReference is a bit funky because it's a List which means you have to keep replacing it as it grows which potentially might have some performance issues, although I didn't see any. The methodology is pretty much the same as the internal Task.updateValue() logic.

    This is Kotlin, but the ideas are exactly the same as Java:

    class Example0 : Application() {
        override fun start(stage: Stage) {
            stage.scene = Scene(createContent(), 1800.0, 960.0).apply {
                Example0::class.java.getResource("abc.css")?.toString()?.let { stylesheets += it }
            }
            stage.show()
        }
    
        private fun createContent(): Region = BorderPane().apply {
            val image = WritableImage(1920, 1080)
            val writer = image.pixelWriter
            val view: ImageView = ImageView(image)
            val pixelList: ObservableList<PixelValue> = FXCollections.observableArrayList<PixelValue>()
            pixelList.subscribe {
                pixelList.forEach { writer.setColor(it.x, it.y, it.color) }
                pixelList.clear()
            }
            top = TextField()
            center = Pane(view)
            bottom = Button("Go").apply {
                onAction = EventHandler {
                    isDisable = true
                    val task = object : Task<Unit>() {
                        val partialUpdate = AtomicReference<MutableList<PixelValue>>(mutableListOf())
                        override fun call() {
                            for (y in 0..879) for (x in 0..1780) {
                                doUpdate(PixelValue(x, y, Color.rgb(Random.nextInt(255), 0, 255)))
                            }
                        }
    
                        fun doUpdate(newVal: PixelValue) {
                            if (partialUpdate.getAndUpdate {
                                    mutableListOf<PixelValue>().apply {
                                        addAll(it)
                                        add(newVal)
                                    }
                                }.isEmpty()) {
                                Platform.runLater {
                                    pixelList.addAll(partialUpdate.getAndUpdate { mutableListOf() })
                                }
                            }
                        }
                    }
                    task.onSucceeded = EventHandler { this.isDisable = false }
                    Thread(task).apply { isDaemon = true }.start()
                }
            }
        }
    }
    
    data class PixelValue(val x: Int, val y: Int, val color: Color)
    
    fun main() = Application.launch(Example0::class.java)
    

    Note that you don't need multiple Stages or Scenes or any of that. It just works. I added a TextField at the top so that you could type away while the Task is running and see that it doesn't interfere with the UI.

    There's no need for buffer flipping or any of that either. PixelWriter has to be call a gazillion times on the FXAT, but it doesn't seem to affect the performance of the GUI to any degree. It could run forever and it doesn't seem to be a practical issue.