package org.sunflow; import java.awt.image.BufferedImage; import java.io.IOException; import java.net.URL; import javax.imageio.ImageIO; import org.sunflow.core.Display; import org.sunflow.core.Tesselatable; import org.sunflow.core.camera.PinholeLens; import org.sunflow.core.display.FileDisplay; import org.sunflow.core.light.TriangleMeshLight; import org.sunflow.core.primitive.Sphere; import org.sunflow.core.primitive.TriangleMesh; import org.sunflow.core.shader.DiffuseShader; import org.sunflow.core.shader.GlassShader; import org.sunflow.core.shader.MirrorShader; import org.sunflow.core.tesselatable.Teapot; import org.sunflow.image.Color; import org.sunflow.math.Matrix4; import org.sunflow.math.Point3; import org.sunflow.math.Vector3; import org.sunflow.system.BenchmarkFramework; import org.sunflow.system.BenchmarkTest; import org.sunflow.system.UI; import org.sunflow.system.UserInterface; import org.sunflow.system.UI.Module; import org.sunflow.system.UI.PrintLevel; public class Benchmark implements BenchmarkTest, UserInterface, Display { private int resolution; private boolean showOutput; private boolean showBenchmarkOutput; private boolean saveOutput; private int threads; private int[] referenceImage; private int[] validationImage; private int errorThreshold; public static void main(String[] args) { if (args.length == 0) { System.out.println("Benchmark options:"); System.out.println(" -regen Regenerate reference images for a variety of sizes"); System.out.println(" -bench [threads] [resolution] Run a single iteration of the benchmark using the specified thread count and image resolution"); System.out.println(" Default: threads=0 (auto-detect cpus), resolution=256"); } else if (args[0].equals("-regen")) { int[] sizes = { 32, 64, 96, 128, 256, 384, 512 }; for (int s : sizes) { // run a single iteration to generate the reference image Benchmark b = new Benchmark(s, true, false, true); b.kernelMain(); } } else if (args[0].equals("-bench")) { int threads = 0, resolution = 256; if (args.length > 1) threads = Integer.parseInt(args[1]); if (args.length > 2) resolution = Integer.parseInt(args[2]); Benchmark benchmark = new Benchmark(resolution, false, true, false, threads); benchmark.kernelBegin(); benchmark.kernelMain(); benchmark.kernelEnd(); } } public Benchmark() { this(384, false, true, false); } public Benchmark(int resolution, boolean showOutput, boolean showBenchmarkOutput, boolean saveOutput) { this(resolution, showOutput, showBenchmarkOutput, saveOutput, 0); } public Benchmark(int resolution, boolean showOutput, boolean showBenchmarkOutput, boolean saveOutput, int threads) { UI.set(this); this.resolution = resolution; this.showOutput = showOutput; this.showBenchmarkOutput = showBenchmarkOutput; this.saveOutput = saveOutput; this.threads = threads; errorThreshold = 6; // fetch reference image from resources (jar file or classpath) if (saveOutput) return; URL imageURL = Benchmark.class.getResource(String.format("/resources/golden_%04X.png", resolution)); if (imageURL == null) UI.printError(Module.BENCH, "Unable to find reference frame!"); UI.printInfo(Module.BENCH, "Loading reference image from: %s", imageURL); try { BufferedImage bi = ImageIO.read(imageURL); if (bi.getWidth() != resolution || bi.getHeight() != resolution) UI.printError(Module.BENCH, "Reference image has invalid resolution! Expected %dx%d found %dx%d", resolution, resolution, bi.getWidth(), bi.getHeight()); referenceImage = new int[resolution * resolution]; for (int y = 0, i = 0; y < resolution; y++) for (int x = 0; x < resolution; x++, i++) referenceImage[i] = bi.getRGB(x, resolution - 1 - y); // flip } catch (IOException e) { UI.printError(Module.BENCH, "Unable to load reference frame!"); } } public void execute() { // 10 iterations maximum - 10 minute time limit BenchmarkFramework framework = new BenchmarkFramework(10, 600); framework.execute(this); } private class BenchmarkScene extends SunflowAPI { public BenchmarkScene() { build(); render(SunflowAPI.DEFAULT_OPTIONS, saveOutput ? new FileDisplay(String.format("resources/golden_%04X.png", resolution)) : Benchmark.this); } public void build() { // settings parameter("threads", threads); // spawn regular priority threads parameter("threads.lowPriority", false); parameter("resolutionX", resolution); parameter("resolutionY", resolution); parameter("aa.min", -1); parameter("aa.max", 1); parameter("filter", "triangle"); parameter("depths.diffuse", 2); parameter("depths.reflection", 2); parameter("depths.refraction", 2); parameter("bucket.order", "hilbert"); parameter("bucket.size", 32); // gi options parameter("gi.engine", "igi"); parameter("gi.igi.samples", 90); parameter("gi.igi.c", 0.000008f); options(SunflowAPI.DEFAULT_OPTIONS); buildCornellBox(); } private void buildCornellBox() { // camera parameter("eye", new Point3(0, 0, -600)); parameter("target", new Point3(0, 0, 0)); parameter("up", new Vector3(0, 1, 0)); parameter("fov", 45.0f); camera("main_camera", new PinholeLens()); parameter("camera", "main_camera"); options(SunflowAPI.DEFAULT_OPTIONS); // cornell box Color gray = new Color(0.70f, 0.70f, 0.70f); Color blue = new Color(0.25f, 0.25f, 0.80f); Color red = new Color(0.80f, 0.25f, 0.25f); Color emit = new Color(15, 15, 15); float minX = -200; float maxX = 200; float minY = -160; float maxY = minY + 400; float minZ = -250; float maxZ = 200; float[] verts = new float[] { minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, minX, maxY, maxZ, }; int[] indices = new int[] { 0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4, 1, 2, 5, 5, 6, 2, 2, 3, 6, 6, 7, 3, 0, 3, 4, 4, 7, 3 }; parameter("diffuse", gray); shader("gray_shader", new DiffuseShader()); parameter("diffuse", red); shader("red_shader", new DiffuseShader()); parameter("diffuse", blue); shader("blue_shader", new DiffuseShader()); // build walls parameter("triangles", indices); parameter("points", "point", "vertex", verts); parameter("faceshaders", new int[] { 0, 0, 0, 0, 1, 1, 0, 0, 2, 2 }); geometry("walls", new TriangleMesh()); // instance walls parameter("shaders", new String[] { "gray_shader", "red_shader", "blue_shader" }); instance("walls.instance", "walls"); // create mesh light parameter("points", "point", "vertex", new float[] { -50, maxY - 1, -50, 50, maxY - 1, -50, 50, maxY - 1, 50, -50, maxY - 1, 50 }); parameter("triangles", new int[] { 0, 1, 2, 2, 3, 0 }); parameter("radiance", emit); parameter("samples", 8); TriangleMeshLight light = new TriangleMeshLight(); light.init("light", this); // spheres parameter("eta", 1.6f); shader("Glass", new GlassShader()); sphere("glass_sphere", "Glass", -120, minY + 55, -150, 50); parameter("color", new Color(0.70f, 0.70f, 0.70f)); shader("Mirror", new MirrorShader()); sphere("mirror_sphere", "Mirror", 100, minY + 60, -50, 50); // scanned model geometry("teapot", (Tesselatable) new Teapot()); parameter("transform", Matrix4.translation(80, -50, 100).multiply(Matrix4.rotateX((float) -Math.PI / 6)).multiply(Matrix4.rotateY((float) Math.PI / 4)).multiply(Matrix4.rotateX((float) -Math.PI / 2).multiply(Matrix4.scale(1.2f)))); parameter("shaders", "gray_shader"); instance("teapot.instance1", "teapot"); parameter("transform", Matrix4.translation(-80, -160, 50).multiply(Matrix4.rotateY((float) Math.PI / 4)).multiply(Matrix4.rotateX((float) -Math.PI / 2).multiply(Matrix4.scale(1.2f)))); parameter("shaders", "gray_shader"); instance("teapot.instance2", "teapot"); } private void sphere(String name, String shaderName, float x, float y, float z, float radius) { geometry(name, new Sphere()); parameter("transform", Matrix4.translation(x, y, z).multiply(Matrix4.scale(radius))); parameter("shaders", shaderName); instance(name + ".instance", name); } } public void kernelBegin() { // allocate a fresh validation target validationImage = new int[resolution * resolution]; } public void kernelMain() { // this builds and renders the scene new BenchmarkScene(); } public void kernelEnd() { // make sure the rendered image was correct int diff = 0; if (referenceImage != null && validationImage.length == referenceImage.length) { for (int i = 0; i < validationImage.length; i++) { // count absolute RGB differences diff += Math.abs((validationImage[i] & 0xFF) - (referenceImage[i] & 0xFF)); diff += Math.abs(((validationImage[i] >> 8) & 0xFF) - ((referenceImage[i] >> 8) & 0xFF)); diff += Math.abs(((validationImage[i] >> 16) & 0xFF) - ((referenceImage[i] >> 16) & 0xFF)); } if (diff > errorThreshold) UI.printError(Module.BENCH, "Image check failed! - #errors: %d", diff); else UI.printInfo(Module.BENCH, "Image check passed!"); } else UI.printError(Module.BENCH, "Image check failed! - reference is not comparable"); } public void print(Module m, PrintLevel level, String s) { if (showOutput || (showBenchmarkOutput && m == Module.BENCH)) System.out.println(UI.formatOutput(m, level, s)); if (level == PrintLevel.ERROR) throw new RuntimeException(s); } public void taskStart(String s, int min, int max) { // render progress display not needed } public void taskStop() { // render progress display not needed } public void taskUpdate(int current) { // render progress display not needed } public void imageBegin(int w, int h, int bucketSize) { // we can assume w == h == resolution } public void imageEnd() { // nothing needs to be done - image verification is done externally } public void imageFill(int x, int y, int w, int h, Color c) { // this is not used } public void imagePrepare(int x, int y, int w, int h, int id) { // this is not needed } public void imageUpdate(int x, int y, int w, int h, Color[] data) { // copy bucket data to validation image for (int j = 0, index = 0; j < h; j++, y++) for (int i = 0, offset = x + resolution * (resolution - 1 - y); i < w; i++, index++, offset++) validationImage[offset] = data[index].copy().toNonLinear().toRGB(); } }