Last active
December 27, 2022 11:33
-
-
Save ennerf/56189cf6e14316f23dd0d50544e7ed13 to your computer and use it in GitHub Desktop.
Multi-threaded Mandelbrot for JavaFX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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