From d56aa25ae73cc89f752d58b5e76327a750c413ae Mon Sep 17 00:00:00 2001 From: Not Zed Date: Sat, 25 Jan 2020 12:28:03 +1030 Subject: [PATCH] Added javafx mandelbrot zoomer demo. Added ability to run demos from make. --- Makefile | 23 +- README | 7 + config.make.in | 1 + nbproject/project.properties | 4 +- .../classes/fxdemo/fract/Calculate.java | 102 ++++++ .../classes/fxdemo/fract/Mandelbrot.java | 307 ++++++++++++++++++ .../classes/fxdemo/fract/mandelbrot.cl | 97 ++++++ .../classes/fxdemo/fract/usage.txt | 50 +++ .../classes/module-info.java | 15 + 9 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 src/notzed.zcl.fxdemo/classes/fxdemo/fract/Calculate.java create mode 100644 src/notzed.zcl.fxdemo/classes/fxdemo/fract/Mandelbrot.java create mode 100644 src/notzed.zcl.fxdemo/classes/fxdemo/fract/mandelbrot.cl create mode 100644 src/notzed.zcl.fxdemo/classes/fxdemo/fract/usage.txt create mode 100644 src/notzed.zcl.fxdemo/classes/module-info.java diff --git a/Makefile b/Makefile index bcc5f51..45703c9 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,28 @@ dist_EXTRA=README \ 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))))))) diff --git a/README b/README index 591c71f..673c5b7 100644 --- a/README +++ b/README @@ -8,6 +8,13 @@ other issues. 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 ------------ diff --git a/config.make.in b/config.make.in index dba9824..9c4401b 100644 --- a/config.make.in +++ b/config.make.in @@ -2,6 +2,7 @@ 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 diff --git a/nbproject/project.properties b/nbproject/project.properties index 20b0bb1..faa0316 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -43,7 +43,8 @@ javac.classpath= 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} @@ -75,6 +76,7 @@ jlink.additionalparam= 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. diff --git a/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Calculate.java b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Calculate.java new file mode 100644 index 0000000..ab069b1 --- /dev/null +++ b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Calculate.java @@ -0,0 +1,102 @@ +/* + * 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 . + */ +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(); + } + + } + +} diff --git a/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Mandelbrot.java b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Mandelbrot.java new file mode 100644 index 0000000..4a2cb1c --- /dev/null +++ b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/Mandelbrot.java @@ -0,0 +1,307 @@ +/* + * 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 . + */ +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. + *

+ */ +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); + } + } +} diff --git a/src/notzed.zcl.fxdemo/classes/fxdemo/fract/mandelbrot.cl b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/mandelbrot.cl new file mode 100644 index 0000000..bb7009c --- /dev/null +++ b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/mandelbrot.cl @@ -0,0 +1,97 @@ + +#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); + } +} diff --git a/src/notzed.zcl.fxdemo/classes/fxdemo/fract/usage.txt b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/usage.txt new file mode 100644 index 0000000..0b363af --- /dev/null +++ b/src/notzed.zcl.fxdemo/classes/fxdemo/fract/usage.txt @@ -0,0 +1,50 @@ +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. diff --git a/src/notzed.zcl.fxdemo/classes/module-info.java b/src/notzed.zcl.fxdemo/classes/module-info.java new file mode 100644 index 0000000..1774cb3 --- /dev/null +++ b/src/notzed.zcl.fxdemo/classes/module-info.java @@ -0,0 +1,15 @@ + +/** + * 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; + +} -- 2.39.2