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:
Although clicking the play button once hardly ever works, clicking it multiple times usually gets the display to load.
If the button is clicked a few times and the display still hasn't loaded, selecting the display stage will sometimes load it in.
Showing the display or control stage first and using one or the other as the given primaryStage makes no difference.
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.
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.