Skip to content

Instantly share code, notes, and snippets.

@ennerf
Last active December 27, 2022 11:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ennerf/56189cf6e14316f23dd0d50544e7ed13 to your computer and use it in GitHub Desktop.
Save ennerf/56189cf6e14316f23dd0d50544e7ed13 to your computer and use it in GitHub Desktop.
Multi-threaded Mandelbrot for JavaFX
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.stream.IntStream;
/**
* Multi-threaded JavaFX Mandelbrot. Adapted from
* https://introcs.cs.princeton.edu/java/32class/Mandelbrot.java.html
* https://www.hameister.org/projects_fractal.html
*
* @since 23 Dec 2022
*/
public class MandelbrotFX extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
// Range sliders
var xcSlider = new Slider(-5, 5, 0);
var ycSlider = new Slider(-5, 5, -0.7);
var sizeSlider = new Slider(0, 10, 3);
// Setup pixel buffer
var bb = ByteBuffer.allocateDirect(N * N * Integer.BYTES);
bb.order(ByteOrder.LITTLE_ENDIAN);
var buffer = bb.asIntBuffer();
var pxBuffer = new PixelBuffer<>(N, N, buffer, PixelFormat.getIntArgbPreInstance());
Runnable updateBuffer = () -> {
var xc = xcSlider.getValue();
var yc = ycSlider.getValue();
var size = sizeSlider.getValue();
// Update individual pixels in the buffer
// (can run on any thread)
IntStream.range(0, N).parallel().forEach(x -> {
for (int y = 0; y < N; y++) {
double x0 = xc - size / 2 + size * x / N;
double y0 = yc - size / 2 + size * y / N;
var count = computeIterations(x0, y0);
buffer.put((x * N) + y, chooseColor(count));
}
});
// Let JavaFX know that the underlying buffer has changed
// (needs to run on the FX thread)
pxBuffer.updateBuffer(obj -> null);
};
xcSlider.valueProperty().addListener((obs, prev, value) -> updateBuffer.run());
ycSlider.valueProperty().addListener((obs, prev, value) -> updateBuffer.run());
sizeSlider.valueProperty().addListener((obs, prev, value) -> updateBuffer.run());
updateBuffer.run();
// Layout
var imgView = new ImageView(new WritableImage(pxBuffer));
var imgPane = new Pane(imgView);
var fitSize = Bindings.min(imgPane.widthProperty(),imgPane.heightProperty());
imgView.fitWidthProperty().bind(fitSize);
imgView.fitHeightProperty().bind(fitSize);
xcSlider.setOrientation(Orientation.VERTICAL);
ycSlider.setOrientation(Orientation.HORIZONTAL);
sizeSlider.setOrientation(Orientation.VERTICAL);
var borderPane = new BorderPane();
borderPane.setLeft(xcSlider);
borderPane.setBottom(ycSlider);
borderPane.setCenter(imgPane);
borderPane.setRight(sizeSlider);
stage.setScene(new Scene(borderPane));
stage.show();
}
public int computeIterations(double ci, double c) {
double zi = 0;
double z = 0;
for (int i = 0; i < MAX_ITERATIONS; i++) {
double ziT = 2 * (z * zi);
double zT = z * z - (zi * zi);
z = zT + c;
zi = ziT + ci;
if (z * z + zi * zi >= 4.0) {
return i;
}
}
return MAX_ITERATIONS;
}
private int chooseColor(int iterations) {
int a = 0xFF;
int r = (iterations % 170);
int g = (iterations % 85);
int b = iterations;
return (a & 0xFF) << 24 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF);
}
private static final int N = 512;
private static final int MAX_ITERATIONS = 255;
}
import com.aparapi.Kernel;
import com.aparapi.Range;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.nio.IntBuffer;
import java.util.List;
import java.util.stream.IntStream;
/**
* JavaFX adaptation of Mandelbrot
* https://introcs.cs.princeton.edu/java/32class/Mandelbrot.java.html
* https://www.hameister.org/projects_fractal.html
* <p>
* with multi-threaded and GPU https://aparapi.com/ based implementations. Dependencies:
*
* <dependency>
* <groupId>com.aparapi</groupId>
* <artifactId>aparapi</artifactId>
* <version>3.0.0</version>
* </dependency>
* <p>
* NOTE: I've never used APARAPI before, so there may be a better way to do this. Inspired by
* https://foojay.io/today/high-performance-rendering-in-javafx/
* https://github.com/AlmasB/FXGL-FastRender
*
* @author ennerf
* @since 23 Dec 2022
*/
public class MandelbrotFX extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
// Range sliders
var xcSlider = new Slider(-5, 5, 0);
var ycSlider = new Slider(-5, 5, -0.7);
var sizeSlider = new Slider(0, 10, 3);
var stToggle = new ToggleButton("CPU single-threaded");
var mtToggle = new ToggleButton("CPU multi-threaded");
var gpuToggle = new ToggleButton("GPU");
// Setup pixel buffer
final int[] pixels = new int[N * N];
var pxBuffer = new PixelBuffer<>(N, N, IntBuffer.wrap(pixels), PixelFormat.getIntArgbPreInstance());
var gpuKernel = new Kernel() {
double xc;
double yc;
double size;
final Range range = Range.create(pixels.length);
{
setExplicit(true);
put(pixels); // copy buffer to GPU (only needed once)
}
public void computePixels(double xc, double yc, double size) {
this.xc = xc;
this.yc = yc;
this.size = size;
execute(range);
get(pixels); // retrieve buffer from GPU
}
@Override
public void run() {
int index = getGlobalId();
int x = index / N;
int y = index % N;
pixels[index] = computePixelColor(x, y, size, xc, yc);
}
};
Runnable updateBuffer = () -> {
var xc = xcSlider.getValue();
var yc = ycSlider.getValue();
var size = sizeSlider.getValue();
if (gpuToggle.isSelected()) {
// Compute pixels on GPU and retrieve the buffer
gpuKernel.computePixels(xc, yc, size);
} else {
// Update individual pixels in the buffer
// (can run on any thread)
var rowStream = IntStream.range(0, N);
if (mtToggle.isSelected()) {
rowStream = rowStream.parallel();
}
rowStream.forEach(x -> {
for (int y = 0; y < N; y++) {
pixels[(x * N) + y] = computePixelColor(x, y, size, xc, yc);
}
});
}
// Let JavaFX know that the underlying buffer has changed
// (needs to run on the FX thread)
pxBuffer.updateBuffer(obj -> null);
};
List.of(xcSlider.valueProperty(), ycSlider.valueProperty(), sizeSlider.valueProperty(), mtToggle.selectedProperty(), gpuToggle.selectedProperty())
.forEach(prop -> prop.addListener((obs, prev, value) -> updateBuffer.run()));
updateBuffer.run();
// Layout
var imgView = new ImageView(new WritableImage(pxBuffer));
var imgPane = new Pane(imgView);
var fitSize = Bindings.min(imgPane.widthProperty(), imgPane.heightProperty());
imgView.fitWidthProperty().bind(fitSize);
imgView.fitHeightProperty().bind(fitSize);
xcSlider.setOrientation(Orientation.VERTICAL);
ycSlider.setOrientation(Orientation.HORIZONTAL);
sizeSlider.setOrientation(Orientation.VERTICAL);
var modeToggles = new ToggleGroup();
List.of(stToggle, mtToggle, gpuToggle).forEach(toggle -> toggle.setToggleGroup(modeToggles));
modeToggles.selectedToggleProperty().addListener((obs, prev, value) -> {
if (value == null) {
modeToggles.selectToggle(stToggle);
}
});
var borderPane = new BorderPane();
borderPane.setTop(new HBox(10, stToggle, mtToggle, gpuToggle));
borderPane.setLeft(xcSlider);
borderPane.setBottom(ycSlider);
borderPane.setCenter(imgPane);
borderPane.setRight(sizeSlider);
stage.setScene(new Scene(borderPane));
stage.show();
}
public static int computePixelColor(int x, int y, double size, double xc, double yc) {
double x0 = xc - size / 2 + size * x / N;
double y0 = yc - size / 2 + size * y / N;
int count = computeIterations(x0, y0);
return chooseColor(count);
}
public static int computeIterations(double ci, double c) {
double zi = 0;
double z = 0;
for (int i = 0; i < MAX_ITERATIONS; i++) {
double ziT = 2 * (z * zi);
double zT = z * z - (zi * zi);
z = zT + c;
zi = ziT + ci;
if (z * z + zi * zi >= 4.0) {
return i;
}
}
return MAX_ITERATIONS;
}
private static int chooseColor(int iterations) {
int a = 0xFF;
int r = (iterations % 170);
int g = (iterations % 85);
int b = iterations;
return (a & 0xFF) << 24 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF);
}
private static final int N = 1024;
private static final int MAX_ITERATIONS = 255;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment