Added ability to run demos from make.
include config.make
-java_MODULES=notzed.zcl notzed.zcl.demo
+java_MODULES=notzed.zcl notzed.zcl.demo notzed.zcl.fxdemo
notzed.zcl.demo_JDEPMOD=notzed.zcl
+notzed.zcl.fxdemo_JDEPMOD=notzed.zcl
+notzed.zcl.fxdemo_JAVAMODPATH=$(JAVAFX_HOME)/lib
+
include java.make
+
+# ######################################################################
+# Work in progress idea for java.make extension to create execution templates
+
+notzed.zcl.demo_DEMOS=au.notzed.zcl.tools.clinfo
+notzed.zcl.fxdemo_DEMOS=fxdemo.fract.Mandelbrot
+
+DEMOFLAGS=--add-exports jdk.incubator.foreign/jdk.incubator.foreign.unsafe=notzed.zcl
+
+# module class basename(class)
+define java_demo=
+run-$(3): $1
+ $(JAVA_HOME)/bin/java \
+ $(DEMOFLAGS) \
+ --module-path $$(subst $$(S),:,bin/modules $(JAVAMODPATH) $$($1_JAVAMODPATH)) \
+ -m $1/$2 $(ARGV)
+endef
+$(foreach module,$(java_MODULES),$(foreach demo,$($(module)_DEMOS),$(eval $(call java_demo,$(module),$(demo),$(lastword $(subst ., ,$(demo)))))))
Various native support code is in the temporary package api.*,
eventually to be moved to a new nativez.
+I've added 'run-X' targets to run the demos. Arguments are passed
+using ARGV.
+
+make run-clinfo
+make run-Mandelbrot
+make run-Mandelbrot ARGV="--gamma=15 --rotate=0"
+
INTRODUCTION
------------
TARGET ?= linux-amd64
JAVA_HOME ?= /usr/local/jdk-13+33
+JAVAFX_HOME ?= /home/notzed/src/jfx/build/sdk
JAVACFLAGS += -source 13
JAVACFLAGS += --add-exports jdk.incubator.foreign/jdk.incubator.foreign.unsafe=notzed.zcl
javac.compilerargs=-Xlint:unchecked --add-exports jdk.incubator.foreign/jdk.incubator.foreign.unsafe=notzed.zcl
javac.deprecation=false
javac.external.vm=false
-javac.modulepath=
+javac.modulepath=\
+ ${libs.JavaFX_13.classpath}
javac.processormodulepath=
javac.processorpath=\
${javac.classpath}
jlink.launcher=true
jlink.launcher.name=notzed.zcl
platform.active=JDK_15
+project.license=gpl3-notzed
run.classpath=
# Space-separated list of JVM arguments used when running the project.
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
--- /dev/null
+/*
+ * Copyright (C) 2020 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package fxdemo.fract;
+
+import static au.notzed.zcl.CL.*;
+import au.notzed.zcl.CLBuffer;
+import au.notzed.zcl.CLCommandQueue;
+import au.notzed.zcl.CLContext;
+import au.notzed.zcl.CLDevice;
+import au.notzed.zcl.CLException;
+import au.notzed.zcl.CLKernel;
+import au.notzed.zcl.CLObject;
+import au.notzed.zcl.CLPlatform;
+import au.notzed.zcl.CLProgram;
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import java.nio.ByteBuffer;
+
+public class Calculate {
+
+ CLDevice[] devs;
+ CLContext cl;
+ CLCommandQueue q;
+ CLBuffer image;
+ CLProgram prog;
+ int width, height;
+ CLKernel mandelbrot;
+ CLKernel dmandelbrot;
+
+ Calculate(int width, int height) throws CLException {
+ devs = new CLDevice[]{CLPlatform.getBestDevice(CL_DEVICE_TYPE_ALL)};
+ cl = CLContext.createContext(null, devs);
+ q = cl.createCommandQueue(devs[0], 0);
+
+ this.width = width;
+ this.height = height;
+
+ image = cl.createBuffer(0, width * height * 4);
+
+ prog = cl.createProgramWithSource(getClass().getResourceAsStream("mandelbrot.cl"));
+ prog.buildProgram(devs, null);
+
+ mandelbrot = prog.createKernel("mandelbrot");
+ mandelbrot.setArg(0, image);
+ mandelbrot.setArg(1, width, height);
+
+ dmandelbrot = prog.createKernel("dmandelbrot");
+ dmandelbrot.setArg(0, image);
+ dmandelbrot.setArg(1, width, height);
+ }
+
+ void release() {
+ CLObject.release(dmandelbrot, mandelbrot, prog, image, q, cl);
+ CLObject.release(devs);
+ }
+
+ public void run(ByteBuffer buffer, double ox, double oy, double sx, double sy, double arg, int m) {
+ try {
+ mandelbrot.setArg(2, ox, oy);
+ mandelbrot.setArg(3, sx, sy);
+ mandelbrot.setArg(4, sin(arg), cos(arg));
+ mandelbrot.setArg(5, m);
+
+ q.enqueue2DKernel(mandelbrot, 0, 0, width, height, 8, 8, null, null);
+ q.enqueueReadBuffer(image, true, 0, buffer.limit(), buffer, null, null);
+ q.finish();
+ } catch (CLException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ public void rund(ByteBuffer buffer, double ox, double oy, double sx, double sy, double arg, int m) {
+ try {
+ dmandelbrot.setArg(2, ox, oy);
+ dmandelbrot.setArg(3, sx, sy);
+ dmandelbrot.setArg(4, sin(arg), cos(arg));
+ dmandelbrot.setArg(5, m);
+
+ q.enqueue2DKernel(dmandelbrot, 0, 0, width, height, 8, 8, null, null);
+ q.enqueueReadBuffer(image, true, 0, buffer.limit(), buffer, null, null);
+ q.finish();
+ } catch (CLException ex) {
+ ex.printStackTrace();
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2020 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package fxdemo.fract;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.animation.Transition;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.event.ActionEvent;
+import javafx.scene.Scene;
+import javafx.scene.image.ImageView;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.WritableImage;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+import java.nio.ByteBuffer;
+import au.notzed.zcl.CLMemory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.stream.Stream;
+import javafx.geometry.Rectangle2D;
+import javafx.stage.Screen;
+
+/**
+ * Mandelbrot rot-zoomer.
+ * <p>
+ */
+public class Mandelbrot extends Application {
+
+ int width = 1920 / 2;
+ int height = 1200 / 2;
+ Calculate calc;
+ WritableImage image;
+ ByteBuffer buffer;
+ ExecutorService queue = Executors.newSingleThreadExecutor((r) -> {
+ Thread t = new Thread(r);
+ t.setDaemon(true);
+ return t;
+ });
+
+ long nframes;
+ long lframes;
+
+ static class Target {
+
+ String name;
+ double cx, cy;
+ double beta;
+
+ public Target(String name, double cx, double cy, double beta) {
+ this.name = name;
+ this.cx = cx;
+ this.cy = cy;
+ this.beta = beta;
+ }
+
+ }
+
+ // some from here, i made up the names http://paulbourke.net/fractals/mandelbrot/
+ static Target[] targets = {
+ new Target("jewel", -0.7746806106269039, -0.1374168856037867, 30),
+ new Target("starfish", -0.812223315621338, -0.185453926110785, 10),
+ new Target("spirals", -0.761574, -0.0847596, 12),
+ new Target("spikes", -1.62917, -0.0203968, 16),};
+
+ Target target = targets[0];
+ boolean isdouble = true;
+ double alpha = 0.005;
+ double beta = 0;
+ double gamma = 80;
+ double delta = -360;
+ boolean synchronous = false;
+ double time = 15;
+ double rate = 60;
+ boolean fullscreen;
+
+ static void usage() {
+ try (InputStream is = Mandelbrot.class.getResourceAsStream("usage.txt")) {
+ is.transferTo(System.out);
+ System.exit(0);
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ @Override
+ public void start(Stage stage) throws Exception {
+ // parse arguments
+ getParameters().getNamed().forEach((c, v) -> {
+ switch (c) {
+ case "target":
+ if (v.equals("list")) {
+ System.out.println("Available targets:");
+ for (Target t : targets) {
+ System.out.print("\t");
+ System.out.println(t.name);
+ }
+ System.exit(0);
+ }
+ target = Stream.of(targets).filter((t) -> t.name.equals(v)).findFirst().orElseGet(() -> {
+ String coords[] = v.split(",");
+ return new Target("user", Double.valueOf(coords[0].trim()), Double.valueOf(coords[1].trim()), 15);
+ });
+ break;
+ case "a":
+ case "alpha":
+ alpha = Double.valueOf(v);
+ break;
+ case "b":
+ case "beta":
+ beta = Double.valueOf(v);
+ break;
+ case "c":
+ case "gamma":
+ gamma = Double.valueOf(v);
+ break;
+ case "d":
+ case "rotate":
+ delta = Double.valueOf(v);
+ break;
+ case "time":
+ time = Double.valueOf(v);
+ break;
+ case "rate":
+ rate = Double.valueOf(v);
+ break;
+ case "size":
+ if (v.equals("full")) {
+ Rectangle2D size = Screen.getPrimary().getBounds();
+ width = (int) size.getWidth();
+ height = (int) size.getHeight();
+ fullscreen = true;
+ } else {
+ String coords[] = v.split(",");
+ width = Integer.valueOf(coords[0]);
+ height = Integer.valueOf(coords[1]);
+ }
+ break;
+ default:
+ System.out.println("Unknown option: '" + c + "'");
+ usage();
+ break;
+ }
+ });
+ getParameters().getUnnamed().forEach((c) -> {
+ switch (c) {
+ case "-f":
+ isdouble = false;
+ break;
+ case "-d":
+ isdouble = true;
+ break;
+ case "--sync":
+ case "-s":
+ synchronous = true;
+ break;
+ default:
+ System.out.println("Unknown option: " + c);
+ case "-h":
+ case "--help":
+ usage();
+ }
+ });
+
+ calc = new Calculate(width, height);
+ image = new WritableImage(width, height);
+
+ if (beta == 0.0)
+ beta = isdouble ? target.beta : Math.min(12, target.beta);
+
+ ImageView iv = new ImageView(image);
+ Text fps = new Text();
+
+ // sort of pixietube
+ fps.setStyle("-fx-font: 24 monospace; -fx-fill: orange; -fx-effect: dropshadow(two-pass-box, orangered, 6, 0.5, 0, 0);");
+
+ Scene scene = new Scene(new AnchorPane(iv, fps));
+
+ AnchorPane.setLeftAnchor(fps, 24.0);
+ AnchorPane.setTopAnchor(fps, 24.0);
+
+ scene.getAccelerators().put(KeyCombination.valueOf("ESC"), stage::close);
+ stage.setFullScreen(fullscreen);
+
+ stage.setScene(scene);
+ stage.show();
+
+ buffer = CLMemory.alloc(width * height * 4);
+
+ Timeline tofps = new Timeline(new KeyFrame(Duration.seconds(0.1), (ActionEvent event) -> {
+ fps.setText(String.format("fps %4d maxiter %d", (nframes - lframes) * 10, lastiter));
+ lframes = nframes;
+ }));
+ tofps.setCycleCount(Timeline.INDEFINITE);
+ tofps.play();
+
+ if (delta != 0.0) {
+ Transition spin = new Transition(rate) {
+ {
+ setCycleDuration(Duration.seconds(360 / (Math.abs(delta))));
+ }
+
+ @Override
+ protected void interpolate(double frac) {
+ arg = Math.toRadians(frac * delta);
+ }
+ };
+ spin.setCycleCount(Timeline.INDEFINITE);
+ spin.setInterpolator(Interpolator.LINEAR);
+ spin.play();
+ } else {
+ arg = Math.toRadians(90);
+ }
+
+ Zoom zoom = synchronous ? new Zoom() : new Zoom(rate);
+ zoom.setInterpolator(Interpolator.EASE_BOTH);
+ zoom.setAutoReverse(true);
+ zoom.setCycleCount(Timeline.INDEFINITE);
+ zoom.play();
+ }
+
+ @Override
+ public void stop() throws Exception {
+ calc.release();
+ }
+
+ class Zoom extends Transition {
+
+ Zoom() {
+ setCycleDuration(Duration.seconds(time));
+ }
+
+ Zoom(double rate) {
+ super(rate);
+ setCycleDuration(Duration.seconds(time));
+ }
+
+ @Override
+ protected void interpolate(double frac) {
+ double scale = alpha / Math.exp(frac * beta);
+ int depth = (int) (-Math.log(scale) * gamma);
+
+ recalc(target, isdouble, scale, arg, depth);
+ }
+ }
+
+ boolean busy;
+ double arg = 0;
+ int lastiter;
+
+ void recalc(Target target, boolean isdouble, double scale, double arg, int m) {
+ double x = target.cx - width * scale * 0.5;
+ double y = target.cy - height * scale * 0.5;
+
+ lastiter = m;
+
+ if (!synchronous) {
+ if (busy)
+ return;
+ busy = true;
+
+ queue.submit(() -> {
+ if (isdouble)
+ calc.rund(buffer, x, y, scale, scale, arg, m);
+ else
+ calc.run(buffer, x, y, scale, scale, arg, m);
+
+ Platform.runLater(() -> {
+ nframes++;
+ image.getPixelWriter()
+ .setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), buffer, width * 4);
+ busy = false;
+ });
+ });
+ } else {
+
+ if (isdouble)
+ calc.rund(buffer, x, y, scale, scale, arg, m);
+ else
+ calc.run(buffer, x, y, scale, scale, arg, m);
+ nframes++;
+ image.getPixelWriter()
+ .setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), buffer, width * 4);
+ }
+ }
+}
--- /dev/null
+
+#pragma OPENCL EXTENSION cl_khr_fp64 : enable
+
+#define LWS_X 8
+#define LWS_Y 8
+
+kernel void
+__attribute__((reqd_work_group_size(LWS_X, LWS_Y, 1)))
+mandelbrot(global uchar4 *image, int2 size, double2 origin, double2 scale, double2 sincos, int m) {
+ int dx = get_global_id(0);
+ int dy = get_global_id(1);
+
+ if (dx < size.x && dy < size.y) {
+#if 0
+ float2 c = (float2)(dx, dy) * scale + origin;
+#else
+ double2 s = convert_double2(size);
+ double2 v;
+
+ v = (double2)(dx, dy) - s * 0.5;
+ v = (double2)(v.x * sincos.x + v.y * sincos.y,
+ v.x * sincos.y - v.y * sincos.x);
+ v += s * 0.5;
+
+ v *= scale;
+ v += origin;
+
+ float2 c = convert_float2(v);
+#endif
+ int n = 0;
+ float2 z = 0;
+
+ do {
+ z = (float2)(z.x * z.x - z.y * z.y, 2.0f * z.x * z.y) + c;
+ n++;
+ } while (dot(z, z) < 4 && n < m);
+
+ // we use a simple cosine palette to determine color:
+ // http://iquilezles.org/www/articles/palettes/palettes.htm
+ float t = n * 10.0f / m;
+ float3 d = (float3)(0.5f, 0.5f, 0.5f);
+ float3 e = (float3)(0.5f, 0.5f, 0.5f);
+ float3 f = (float3)(1.0f, 1.0f, 1.0f);
+ float3 g = (float3)(0.00f, 0.33f, 0.67f);
+ float4 colour = (float4)( d + e * cos(6.28318f * (f * t + g)), 1.0f);
+
+ if (convert_int(n) == m)
+ colour = (float4)(0, 0, 0, 1);
+
+ // store the rendered mandelbrot set into a storage buffer:
+ image[dx + dy * size.x] = convert_uchar4(colour * 255.0f);
+ }
+}
+
+// rot-zoomer
+kernel void
+__attribute__((reqd_work_group_size(LWS_X, LWS_Y, 1)))
+dmandelbrot(global uchar4 *image, int2 size, double2 origin, double2 scale, double2 sincos, int m) {
+ int dx = get_global_id(0);
+ int dy = get_global_id(1);
+
+ if (dx < size.x && dy < size.y) {
+ double2 s = convert_double2(size);
+ double2 c;
+
+ c = (double2)(dx, dy) - s * 0.5;
+ c = (double2)(c.x * sincos.x + c.y * sincos.y,
+ c.x * sincos.y - c.y * sincos.x);
+ c += s * 0.5;
+
+ c *= scale;
+ c += origin;
+
+ int n = 0;
+ double2 z = 0;
+
+ do {
+ z = (double2)(z.x * z.x - z.y * z.y, 2.0f * z.x * z.y) + c;
+ n++;
+ } while (dot(z, z) < 4 && n < m);
+
+ // we use a simple cosine palette to determine color:
+ // http://iquilezles.org/www/articles/palettes/palettes.htm
+ float t = n * 10.0f / m;
+ float3 d = (float3)(0.5f, 0.5f, 0.5f);
+ float3 e = (float3)(0.5f, 0.5f, 0.5f);
+ float3 f = (float3)(1.0f, 1.0f, 1.0f);
+ float3 g = (float3)(0.00f, 0.33f, 0.67f);
+ float4 colour = (float4)( d + e * cos(6.28318f * (f * t + g)), 1.0f);
+
+ if (convert_int(n) == m)
+ colour = (float4)(0, 0, 0, 1);
+
+ // store the rendered mandelbrot set into a storage buffer:
+ image[dx + dy * size.x] = convert_uchar4(colour * 255.0f);
+ }
+}
--- /dev/null
+Usage: mandelbrot [args]
+
+ -f, -d
+ Set precision, float or double.
+
+ --size=full
+ --size=width,height
+ Set window size. Full will enable fullscreen mode.
+
+ --target=list
+ --target=named-target
+ --target=centre-x,centre-y
+ Set the centre zoom target point. list will list some
+ predefined locations from
+ http://paulbourke.net/fractals/mandelbrot/
+
+ --a=alpha [0.005]
+ Set zoom alpha parameter. Loosly the maximum outer zoom,
+ range is around 0.005.
+
+ --b=beta [target dependent]
+ Set the maximum inner zoom. This is an inverse exponential
+ and the maximum value depends on the precision. For float the
+ maximum is about 12, for double it is more than 30.
+
+ --c=gamma [80]
+ Set the iteration scaling rate. This will the precision,
+ speed of rendering, and some of the colour mapping. If you
+ have the horsepower 80-100 works well.
+
+ --rotate=degrees-per-second [360]
+ Set rotation rate in degrees per second. Negative values are
+ allowed. Use 0 to turn it off.
+
+ --time=seconds
+ Time in seconds for one zoom.
+
+ --rate=framerate
+ Target framereate.
+
+ --sync, -s
+ Set synchronous mode. In synchronous mode rendering occurs on
+ the javafx thread and there is no rate limiting.
+
+ --help, -h
+ Usage.
+
+ Press Escape to quit.
+
+ Try --gamma=20 or 15 for a trippier colouration.
--- /dev/null
+
+/**
+ * OpenCL demos that use JavaFX.
+ */
+module notzed.zcl.fxdemo {
+ requires javafx.base;
+ requires javafx.controls;
+ requires javafx.graphics;
+ requires javafx.swing;
+
+ requires notzed.zcl;
+
+ exports fxdemo.fract to javafx.graphics;
+
+}