--- /dev/null 2018-06-08 10:15:09.479983295 +0200 +++ new/tests/system/src/test/java/test/com/sun/marlin/ClipShapeTest.java 2018-06-08 16:12:57.423686431 +0200 @@ -0,0 +1,1307 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package test.com.sun.marlin; + +import java.awt.BasicStroke; +import java.awt.Shape; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.embed.swing.SwingFXUtils; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.SnapshotParameters; +import javafx.scene.image.PixelReader; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.layout.BorderPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.ClosePath; +import javafx.scene.shape.CubicCurveTo; +import javafx.scene.shape.FillRule; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; +import javafx.scene.shape.PathElement; +import javafx.scene.shape.QuadCurveTo; +import javafx.scene.shape.StrokeLineCap; +import javafx.scene.shape.StrokeLineJoin; +import javafx.scene.text.Text; +import javafx.stage.Stage; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; + +import junit.framework.AssertionFailedError; +import org.junit.AfterClass; +import org.junit.Assert; +import static org.junit.Assert.assertEquals; +import org.junit.BeforeClass; +import org.junit.Test; + +import static test.util.Util.TIMEOUT; + +/** + * @test + * @bug + * @summary Verifies that Marlin rendering generates the same + * images with and without clipping optimization with all possible + * stroke (cap/join) and/or dashes or fill modes (EO rules) + * for paths made of either 9 lines, 4 quads, 2 cubics (random) + */ +public final class ClipShapeTest { + + // test options: + static int NUM_TESTS; + + // shape settings: + static ShapeMode SHAPE_MODE; + + static boolean USE_DASHES; + static boolean USE_VAR_STROKE; + + static int THRESHOLD_DELTA; + static long THRESHOLD_NBPIX; + + // constants: + static final boolean DO_FAIL = true; + + static final boolean TEST_STROKER = true; + static final boolean TEST_FILLER = true; + + static final int TESTW = 100; + static final int TESTH = 100; + + static final boolean SHAPE_REPEAT = true; + + // dump path on console: + static final boolean DUMP_SHAPE = true; + + static final int MAX_SHOW_FRAMES = 10; + static final int MAX_SAVE_FRAMES = 100; + + // use fixed seed to reproduce always same polygons between tests + static final boolean FIXED_SEED = true; + + static final double RAND_SCALE = 3.0; + static final double RANDW = TESTW * RAND_SCALE; + static final double OFFW = (TESTW - RANDW) / 2.0; + static final double RANDH = TESTH * RAND_SCALE; + static final double OFFH = (TESTH - RANDH) / 2.0; + + static enum ShapeMode { + TWO_CUBICS, + FOUR_QUADS, + FIVE_LINE_POLYS, + NINE_LINE_POLYS, + FIFTY_LINE_POLYS, + MIXED + } + + static final long SEED = 1666133789L; + // Fixed seed to avoid any difference between runs: + static final Random RANDOM = new Random(SEED); + + static final File OUTPUT_DIR = new File("."); + + static final AtomicBoolean isMarlin = new AtomicBoolean(); + static final AtomicBoolean isClipRuntime = new AtomicBoolean(); + + // Used to launch the application before running any test + private static final CountDownLatch launchLatch = new CountDownLatch(1); + + // Singleton Application instance + static MyApp myApp; + + static boolean doChecksFailed = false; + + private static final Logger log; + + static { + Locale.setDefault(Locale.US); + + // FIRST: Get Marlin runtime state from its log: + + // initialize j.u.l Looger: + log = Logger.getLogger("prism.marlin"); + log.addHandler(new Handler() { + @Override + public void publish(LogRecord record) { + final String msg = record.getMessage(); + if (msg != null) { + // last space to avoid matching other settings: + if (msg.startsWith("prism.marlin ")) { + isMarlin.set(msg.contains("DRenderer")); + } + if (msg.startsWith("prism.marlin.clip.runtime.enable")) { + isClipRuntime.set(msg.contains("true")); + } + } + + final Throwable th = record.getThrown(); + // detect any Throwable: + if (th != null) { + System.out.println("Test failed:\n" + record.getMessage()); + th.printStackTrace(System.out); + + doChecksFailed = true; + + throw new RuntimeException("Test failed: ", th); + } + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + }); + + // enable Marlin logging & internal checks: + System.setProperty("prism.marlin.log", "true"); + System.setProperty("prism.marlin.useLogger", "true"); + + // disable static clipping setting: + System.setProperty("prism.marlin.clip", "false"); + System.setProperty("prism.marlin.clip.runtime.enable", "true"); + + // enable subdivider: + System.setProperty("prism.marlin.clip.subdivider", "true"); + + // disable min length check: always subdivide curves at clip edges + System.setProperty("prism.marlin.clip.subdivider.minLength", "-1"); + + // If any curve, increase curve accuracy: + // curve length max error: + System.setProperty("prism.marlin.curve_len_err", "1e-4"); + + // cubic min/max error: + System.setProperty("prism.marlin.cubic_dec_d2", "1e-3"); + System.setProperty("prism.marlin.cubic_inc_d1", "1e-4"); // or disabled ~ 1e-6 + + // quad max error: + System.setProperty("prism.marlin.quad_dec_d2", "5e-4"); + + System.setProperty("javafx.animation.fullspeed", "true"); // full speed + } + + // Application class. An instance is created and initialized before running + // the first test, and it lives through the execution of all tests. + public static class MyApp extends Application { + + Stage stage = null; + + public MyApp() { + super(); + } + + @Override + public void init() { + ClipShapeTest.myApp = this; + } + + @Override + public void start(Stage primaryStage) throws Exception { + this.stage = primaryStage; + + BorderPane root = new BorderPane(); + + root.setBottom(new Text("running...")); + + Scene scene = new Scene(root); + stage.setScene(scene); + stage.setTitle("Testing"); + stage.show(); + + launchLatch.countDown(); + } + } + + boolean done; + + public synchronized void signalDone() { + done = true; + notifyAll(); + } + + public synchronized void waitDone() throws InterruptedException { + while (!done) { + wait(); + } + } + + private static void resetOptions() { + NUM_TESTS = 5000; + + // shape settings: + SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; + + USE_DASHES = false; + USE_VAR_STROKE = false; + } + + /** + * Test + * @param args + */ + public static void initArgs(String[] args) { + System.out.println("---------------------------------------"); + System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH); + + resetOptions(); + + boolean runSlowTests = false; + + for (String arg : args) { + if ("-slow".equals(arg)) { + runSlowTests = true; + } else if ("-doDash".equals(arg)) { + USE_DASHES = true; + } else if ("-doVarStroke".equals(arg)) { + USE_VAR_STROKE = true; + } else { + // shape mode: + if (arg.equalsIgnoreCase("-poly")) { + SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; + } else if (arg.equalsIgnoreCase("-bigpoly")) { + SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS; + } else if (arg.equalsIgnoreCase("-quad")) { + SHAPE_MODE = ShapeMode.FOUR_QUADS; + } else if (arg.equalsIgnoreCase("-cubic")) { + SHAPE_MODE = ShapeMode.TWO_CUBICS; + } else if (arg.equalsIgnoreCase("-mixed")) { + SHAPE_MODE = ShapeMode.MIXED; + } + } + } + + System.out.println("Shape mode: " + SHAPE_MODE); + + // adjust image comparison thresholds: + switch (SHAPE_MODE) { + case TWO_CUBICS: + // Define uncertainty for curves: +/* +Diff Pixels [Worst(All Test setups)][n: 647] sum: 15130 avg: 23.384 [1 | 174] { + 1 .. 2[n: 93] sum: 93 avg: 1.0 [1 | 1] + 2 .. 4[n: 92] sum: 223 avg: 2.423 [2 | 3] + 4 .. 8[n: 135] sum: 732 avg: 5.422 [4 | 7] + 8 .. 16[n: 109] sum: 1235 avg: 11.33 [8 | 15] + 16 .. 32[n: 82] sum: 1782 avg: 21.731 [16 | 31] + 32 .. 64[n: 59] sum: 2584 avg: 43.796 [32 | 62] + 64 .. 128[n: 52] sum: 4929 avg: 94.788 [64 | 127] + 128 .. 256[n: 25] sum: 3552 avg: 142.08 [129 | 174] } + +DASH: Diff Pixels [Worst(All Test setups)][n: 128] sum: 5399 avg: 42.179 [1 | 255] { + 1 .. 2[n: 54] sum: 54 avg: 1.0 [1 | 1] + 2 .. 4[n: 28] sum: 63 avg: 2.25 [2 | 3] + 4 .. 8[n: 6] sum: 33 avg: 5.5 [4 | 7] + 8 .. 16[n: 3] sum: 33 avg: 11.0 [9 | 15] + 16 .. 32[n: 4] sum: 87 avg: 21.75 [16 | 25] + 32 .. 64[n: 6] sum: 276 avg: 46.0 [37 | 60] + 64 .. 128[n: 6] sum: 568 avg: 94.666 [71 | 118] + 128 .. 256[n: 21] sum: 4285 avg: 204.047 [128 | 255] } +*/ + THRESHOLD_DELTA = 32; + THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 150; + break; + case FOUR_QUADS: + case MIXED: + // Define uncertainty for quads: + // curve subdivision causes curves to be smaller + // then curve offsets are different (more accurate) +/* +Diff Pixels [Worst(All Test setups)][n: 775] sum: 57659 avg: 74.398 [1 | 251] { + 1 .. 2[n: 21] sum: 21 avg: 1.0 [1 | 1] + 2 .. 4[n: 20] sum: 52 avg: 2.6 [2 | 3] + 4 .. 8[n: 44] sum: 236 avg: 5.363 [4 | 7] + 8 .. 16[n: 52] sum: 578 avg: 11.115 [8 | 15] + 16 .. 32[n: 75] sum: 1729 avg: 23.053 [16 | 31] + 32 .. 64[n: 152] sum: 7178 avg: 47.223 [32 | 63] + 64 .. 128[n: 274] sum: 25741 avg: 93.945 [64 | 127] + 128 .. 256[n: 137] sum: 22124 avg: 161.489 [128 | 251] } + +DASH: Diff Pixels [Worst(All Test setups)][n: 354] sum: 29638 avg: 83.723 [1 | 254] { + 1 .. 2[n: 31] sum: 31 avg: 1.0 [1 | 1] + 2 .. 4[n: 45] sum: 111 avg: 2.466 [2 | 3] + 4 .. 8[n: 22] sum: 113 avg: 5.136 [4 | 7] + 8 .. 16[n: 25] sum: 247 avg: 9.88 [8 | 15] + 16 .. 32[n: 26] sum: 579 avg: 22.269 [16 | 31] + 32 .. 64[n: 39] sum: 1698 avg: 43.538 [32 | 62] + 64 .. 128[n: 56] sum: 5284 avg: 94.357 [64 | 127] + 128 .. 256[n: 110] sum: 21575 avg: 196.136 [128 | 254] } +*/ + THRESHOLD_DELTA = 64; + THRESHOLD_NBPIX = (USE_DASHES) ? 180 : 420; + break; + default: + // Define uncertainty for lines: + // float variant have higher uncertainty +/* +DASH: Diff Pixels [Worst(All Test setups)][n: 7] sum: 8 avg: 1.142 [1 | 2] { + 1 .. 2[n: 6] sum: 6 avg: 1.0 [1 | 1] + 2 .. 4[n: 1] sum: 2 avg: 2.0 [2 | 2] } +*/ + THRESHOLD_DELTA = 2; + THRESHOLD_NBPIX = 4; // very low + } + + // TODO: define one more threshold on total result (total sum) ? + + System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA); + System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX); + + if (runSlowTests) { + NUM_TESTS = 10000; // or 100000 (very slow) + USE_DASHES = true; + USE_VAR_STROKE = true; + } + + System.out.println("NUM_TESTS: " + NUM_TESTS); + + if (USE_DASHES) { + System.out.println("USE_DASHES: enabled."); + } + if (USE_VAR_STROKE) { + System.out.println("USE_VAR_STROKE: enabled."); + } + + System.out.println("---------------------------------------"); + } + + private void runTests() { + + final DiffContext allCtx = new DiffContext("All Test setups"); + final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)"); + + int failures = 0; + final long start = System.nanoTime(); + try { + if (TEST_STROKER) { + final float[][] dashArrays = (USE_DASHES) ? +// small +// new float[][]{new float[]{1f, 2f}} +// normal + new float[][]{new float[]{13f, 7f}} +// large (prime) +// new float[][]{new float[]{41f, 7f}} +// none + : new float[][]{null}; + + System.out.println("dashes: " + Arrays.deepToString(dashArrays)); + + final float[] strokeWidths = (USE_VAR_STROKE) + ? new float[5] : + new float[]{10f}; + + int nsw = 0; + if (USE_VAR_STROKE) { + for (float width = 0.1f; width < 110f; width *= 5f) { + strokeWidths[nsw++] = width; + } + } else { + nsw = 1; + } + + System.out.println("stroke widths: " + Arrays.toString(strokeWidths)); + + // Stroker tests: + for (int w = 0; w < nsw; w++) { + final float width = strokeWidths[w]; + + for (float[] dashes : dashArrays) { + + for (int cap = 0; cap <= 2; cap++) { + + for (int join = 0; join <= 2; join++) { + + failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes)); + failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes)); + } + } + } + } + } + + if (TEST_FILLER) { + // Filler tests: + failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO)); + failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO)); + + failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD)); + failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD)); + } + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); + + allWorstCtx.dump(); + allCtx.dump(); + + if (DO_FAIL && (failures != 0)) { + throw new RuntimeException("Clip test failures : " + failures); + } + } + + int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException { + final long start = System.nanoTime(); + + if (FIXED_SEED) { + // Reset seed for random numbers: + RANDOM.setSeed(SEED); + } + + System.out.println("paintPaths: " + NUM_TESTS + + " paths (" + SHAPE_MODE + ") - setup: " + ts); + + final Path2D p2d = new Path2D.Double(ts.windingRule); + + final Path p = makePath(ts); + + final WritableImage imgOff = new WritableImage(TESTW, TESTH); + final PixelReader prOff = imgOff.getPixelReader(); + + final WritableImage imgOn = new WritableImage(TESTW, TESTH); + final PixelReader prOn = imgOn.getPixelReader(); + + final WritableImage imgDiff = new WritableImage(TESTW, TESTH); + final PixelWriter prDiff = imgDiff.getPixelWriter(); + + final DiffContext testSetupCtx = new DiffContext("Test setup"); + final DiffContext testWorstCtx = new DiffContext("Worst"); + final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)"); + + int nd = 0; + try { + final DiffContext testCtx = new DiffContext("Test"); + final DiffContext testThCtx = new DiffContext("Test(>threshold)"); + PixelWriter diffImage; + + for (int n = 0; n < NUM_TESTS; n++) { + genShape(p2d, ts); + + // Use directly Platform.runLater() instead of Util.runAndWait() + // that has too high latency (100ms polling) + done = false; + Platform.runLater(() -> { + /* + Note: as CachingShapeRep try caching the Path mask at the second rendering pass, + then its xformBounds corresponds to the shape not the giiiiven clip. + To avoid such side-effect, perform clipping first below (or call setPath again) + */ + setPath(p, p2d); + + // Runtime clip setting ON (FIRST): + paintShape(p, imgOn, true); + + // Runtime clip setting OFF (2ND): + paintShape(p, imgOff, false); + + signalDone(); + }); + try { + waitDone(); + } catch (InterruptedException ex) { + break; + } + + /* compute image difference if possible */ + diffImage = computeDiffImage(testCtx, testThCtx, prOn, prOff, prDiff); + + // Worst (total) + if (testCtx.isDiff()) { + if (testWorstCtx.isWorse(testCtx, false)) { + testWorstCtx.set(testCtx); + } + if (testWorstThCtx.isWorse(testCtx, true)) { + testWorstThCtx.set(testCtx); + } + // accumulate data: + testSetupCtx.add(testCtx); + } + if (diffImage != null) { + nd++; + + testThCtx.dump(); + testCtx.dump(); + + if (nd < MAX_SHOW_FRAMES) { + if (nd < MAX_SAVE_FRAMES) { + if (DUMP_SHAPE) { + dumpShape(p2d); + } + + final String testName = "Setup_" + ts.id + "_test_" + n; + + saveImage(imgOff, OUTPUT_DIR, testName + "-off.png"); + saveImage(imgOn, OUTPUT_DIR, testName + "-on.png"); + saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png"); + } + } + } + } + } finally { + if (nd != 0) { + System.out.println("paintPaths: " + NUM_TESTS + " paths - " + + "Number of differences = " + nd + + " ratio = " + (100f * nd) / NUM_TESTS + " %"); + } + + if (testWorstCtx.isDiff()) { + testWorstCtx.dump(); + if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) { + testWorstThCtx.dump(); + } + if (allWorstCtx.isWorse(testWorstThCtx, true)) { + allWorstCtx.set(testWorstThCtx); + } + } + testSetupCtx.dump(); + + // accumulate data: + allCtx.add(testSetupCtx); + } + System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); + return nd; + } + + public Path makePath(final TestSetup ts) { + final Path p = new Path(); + p.setCache(false); + p.setSmooth(true); + + if (ts.isStroke()) { + p.setFill(null); + p.setStroke(Color.BLACK); + + switch (ts.strokeCap) { + case BasicStroke.CAP_BUTT: + p.setStrokeLineCap(StrokeLineCap.BUTT); + break; + case BasicStroke.CAP_ROUND: + p.setStrokeLineCap(StrokeLineCap.ROUND); + break; + case BasicStroke.CAP_SQUARE: + p.setStrokeLineCap(StrokeLineCap.SQUARE); + break; + default: + } + + p.setStrokeLineJoin(StrokeLineJoin.MITER); + switch (ts.strokeJoin) { + case BasicStroke.JOIN_MITER: + p.setStrokeLineJoin(StrokeLineJoin.MITER); + break; + case BasicStroke.JOIN_ROUND: + p.setStrokeLineJoin(StrokeLineJoin.ROUND); + break; + case BasicStroke.JOIN_BEVEL: + p.setStrokeLineJoin(StrokeLineJoin.BEVEL); + break; + default: + } + + if (ts.dashes != null) { + ObservableList pDashes = p.getStrokeDashArray(); + pDashes.clear(); + for (float f : ts.dashes) { + pDashes.add(Double.valueOf(f)); + } + } + + p.setStrokeMiterLimit(10.0); + p.setStrokeWidth(ts.strokeWidth); + + } else { + p.setFill(Color.BLACK); + p.setStroke(null); + + switch (ts.windingRule) { + case Path2D.WIND_EVEN_ODD: + p.setFillRule(FillRule.EVEN_ODD); + break; + case Path2D.WIND_NON_ZERO: + p.setFillRule(FillRule.NON_ZERO); + break; + } + } + return p; + } + + public static void setPath(Path p, Path2D p2d) { + final ObservableList elements = p.getElements(); + elements.clear(); + + final double[] coords = new double[6]; + for (PathIterator pi = p2d.getPathIterator(null); !pi.isDone(); pi.next()) { + switch (pi.currentSegment(coords)) { + case PathIterator.SEG_MOVETO: + elements.add(new MoveTo(coords[0], coords[1])); + break; + case PathIterator.SEG_LINETO: + elements.add(new LineTo(coords[0], coords[1])); + break; + case PathIterator.SEG_QUADTO: + elements.add(new QuadCurveTo(coords[0], coords[1], + coords[2], coords[3])); + break; + case PathIterator.SEG_CUBICTO: + elements.add(new CubicCurveTo(coords[0], coords[1], + coords[2], coords[3], + coords[4], coords[5])); + break; + case PathIterator.SEG_CLOSE: + elements.add(new ClosePath()); + break; + default: + throw new InternalError("unexpected segment type"); + } + } + } + + private static void paintShape(Path p, WritableImage wimg, final boolean clip) { + // Enable or Disable clipping: + System.setProperty("prism.marlin.clip.runtime", (clip) ? "true" : "false"); + + final SnapshotParameters sp = new SnapshotParameters(); + sp.setViewport(new Rectangle2D(0, 0, TESTW, TESTH)); + + WritableImage out = p.snapshot(sp, wimg); + + if (out != wimg) { + System.out.println("different images !"); + } + // Or use (faster?) + // ShapeUtil.getMaskData(p, null, b, BaseTransform.IDENTITY_TRANSFORM, true, aa); + } + + static void genShape(final Path2D p2d, final TestSetup ts) { + p2d.reset(); + + final int end = (SHAPE_REPEAT) ? 2 : 1; + + for (int p = 0; p < end; p++) { + p2d.moveTo(randX(), randY()); + + switch (ts.shapeMode) { + case MIXED: + case FIFTY_LINE_POLYS: + case NINE_LINE_POLYS: + case FIVE_LINE_POLYS: + p2d.lineTo(randX(), randY()); + p2d.lineTo(randX(), randY()); + p2d.lineTo(randX(), randY()); + p2d.lineTo(randX(), randY()); + if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) { + // And an implicit close makes 5 lines + break; + } + p2d.lineTo(randX(), randY()); + p2d.lineTo(randX(), randY()); + p2d.lineTo(randX(), randY()); + p2d.lineTo(randX(), randY()); + if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) { + // And an implicit close makes 9 lines + break; + } + if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) { + for (int i = 0; i < 41; i++) { + p2d.lineTo(randX(), randY()); + } + // And an implicit close makes 50 lines + break; + } + case TWO_CUBICS: + p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY()); + p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY()); + if (ts.shapeMode == ShapeMode.TWO_CUBICS) { + break; + } + case FOUR_QUADS: + p2d.quadTo(randX(), randY(), randX(), randY()); + p2d.quadTo(randX(), randY(), randX(), randY()); + p2d.quadTo(randX(), randY(), randX(), randY()); + p2d.quadTo(randX(), randY(), randX(), randY()); + if (ts.shapeMode == ShapeMode.FOUR_QUADS) { + break; + } + default: + } + + if (ts.closed) { + p2d.closePath(); + } + } + } + + @BeforeClass + public static void setupOnce() { + // Start the Application + new Thread(() -> Application.launch(MyApp.class, (String[]) null)).start(); + + try { + if (!launchLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) { + throw new AssertionFailedError("Timeout waiting for Application to launch"); + } + + } catch (InterruptedException ex) { + AssertionFailedError err = new AssertionFailedError("Unexpected exception"); + err.initCause(ex); + throw err; + } + + assertEquals(0, launchLatch.getCount()); + } + + private void checkMarlin() { + if (!isMarlin.get()) { + throw new RuntimeException("Marlin renderer not used at runtime !"); + } + if (!isClipRuntime.get()) { + throw new RuntimeException("Marlin clipping not enabled at runtime !"); + } + } + + @AfterClass + public static void teardownOnce() { + Platform.exit(); + } + + @Test(timeout = 600000) + public void TestPoly() throws InterruptedException { + test(new String[]{"-poly"}); + test(new String[]{"-poly", "-doDash"}); + } + + @Test(timeout = 900000) + public void TestQuad() throws InterruptedException { + test(new String[]{"-quad"}); + test(new String[]{"-quad", "-doDash"}); + } + + @Test(timeout = 900000) + public void TestCubic() throws InterruptedException { + test(new String[]{"-cubic"}); + test(new String[]{"-cubic", "-doDash"}); + } + + private void test(String[] args) { + initArgs(args); + runTests(); + checkMarlin(); + Assert.assertFalse("Detected a problem.", doChecksFailed); + } + + private static void dumpShape(final Shape shape) { + final float[] coords = new float[6]; + + for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { + final int type = it.currentSegment(coords); + switch (type) { + case PathIterator.SEG_MOVETO: + System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");"); + break; + case PathIterator.SEG_LINETO: + System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");"); + break; + case PathIterator.SEG_QUADTO: + System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");"); + break; + case PathIterator.SEG_CUBICTO: + System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");"); + break; + case PathIterator.SEG_CLOSE: + System.out.println("p2d.closePath();"); + break; + default: + System.out.println("// Unsupported segment type= " + type); + } + } + System.out.println("--------------------------------------------------"); + } + + static double randX() { + return RANDOM.nextDouble() * RANDW + OFFW; + } + + static double randY() { + return RANDOM.nextDouble() * RANDH + OFFH; + } + + private final static class TestSetup { + + static final AtomicInteger COUNT = new AtomicInteger(); + + final int id; + final ShapeMode shapeMode; + final boolean closed; + // stroke + final float strokeWidth; + final int strokeCap; + final int strokeJoin; + final float[] dashes; + // fill + final int windingRule; + + TestSetup(ShapeMode shapeMode, final boolean closed, + final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) { + this.id = COUNT.incrementAndGet(); + this.shapeMode = shapeMode; + this.closed = closed; + this.strokeWidth = strokeWidth; + this.strokeCap = strokeCap; + this.strokeJoin = strokeJoin; + this.dashes = dashes; + this.windingRule = Path2D.WIND_NON_ZERO; + } + + TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) { + this.id = COUNT.incrementAndGet(); + this.shapeMode = shapeMode; + this.closed = closed; + this.strokeWidth = 0f; + this.strokeCap = this.strokeJoin = -1; // invalid + this.dashes = null; + this.windingRule = windingRule; + } + + boolean isStroke() { + return this.strokeWidth > 0f; + } + + @Override + public String toString() { + if (isStroke()) { + return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed + + ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin) + + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "") + + '}'; + } + return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed + + ", fill" + + ", windingRule=" + getWindingRule(windingRule) + '}'; + } + + private static String getCap(final int cap) { + switch (cap) { + case BasicStroke.CAP_BUTT: + return "CAP_BUTT"; + case BasicStroke.CAP_ROUND: + return "CAP_ROUND"; + case BasicStroke.CAP_SQUARE: + return "CAP_SQUARE"; + default: + return ""; + } + + } + + private static String getJoin(final int join) { + switch (join) { + case BasicStroke.JOIN_MITER: + return "JOIN_MITER"; + case BasicStroke.JOIN_ROUND: + return "JOIN_ROUND"; + case BasicStroke.JOIN_BEVEL: + return "JOIN_BEVEL"; + default: + return ""; + } + + } + + private static String getWindingRule(final int rule) { + switch (rule) { + case PathIterator.WIND_EVEN_ODD: + return "WIND_EVEN_ODD"; + case PathIterator.WIND_NON_ZERO: + return "WIND_NON_ZERO"; + default: + return ""; + } + } + } + + // --- utilities --- + private static final int DCM_ALPHA_MASK = 0xff000000; + + public static BufferedImage newImage(final int w, final int h) { + return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); + } + + public static PixelWriter computeDiffImage(final DiffContext testCtx, + final DiffContext testThCtx, + final PixelReader tstImage, + final PixelReader refImage, + final PixelWriter diffImage) { + + // reset diff contexts: + testCtx.reset(); + testThCtx.reset(); + + int ref, tst, dg, v; + + for (int y = 0; y < TESTH; y++) { + for (int x = 0; x < TESTW; x++) { + ref = refImage.getArgb(x, y); + tst = tstImage.getArgb(x, y); + + // grayscale diff: + dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst)); + + // max difference on grayscale values: + v = (int) Math.ceil(Math.abs(dg / 3.0)); + + if (v <= THRESHOLD_DELTA) { + diffImage.setArgb(x, y, 0); + } else { + diffImage.setArgb(x, y, toInt(v, v, v)); + testThCtx.add(v); + } + + if (v != 0) { + testCtx.add(v); + } + } + } + + if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) { + return null; + } + + return diffImage; + } + + static void saveImage(final WritableImage image, final File resDirectory, final String imageFileName) throws IOException { + saveImage(SwingFXUtils.fromFXImage(image, null), resDirectory, imageFileName); + } + + static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException { + final Iterator itWriters = ImageIO.getImageWritersByFormatName("PNG"); + if (itWriters.hasNext()) { + final ImageWriter writer = itWriters.next(); + + final ImageWriteParam writerParams = writer.getDefaultWriteParam(); + writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED); + + final File imgFile = new File(resDirectory, imageFileName); + + if (!imgFile.exists() || imgFile.canWrite()) { + System.out.println("saveImage: saving image as PNG [" + imgFile + "]..."); + imgFile.delete(); + + // disable cache in temporary files: + ImageIO.setUseCache(false); + + final long start = System.nanoTime(); + + // PNG uses already buffering: + final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile)); + + writer.setOutput(imgOutStream); + try { + writer.write(null, new IIOImage(image, null, null), writerParams); + } finally { + imgOutStream.close(); + + final long time = System.nanoTime() - start; + System.out.println("saveImage: duration= " + (time / 1000000l) + " ms."); + } + } + } + } + + static int r(final int v) { + return (v >> 16 & 0xff); + } + + static int g(final int v) { + return (v >> 8 & 0xff); + } + + static int b(final int v) { + return (v & 0xff); + } + + static int clamp127(final int v) { + return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255; + } + + static int toInt(final int r, final int g, final int b) { + return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b; + } + + /* stats */ + static class StatInteger { + + public final String name; + public long count = 0l; + public long sum = 0l; + public long min = Integer.MAX_VALUE; + public long max = Integer.MIN_VALUE; + + StatInteger(String name) { + this.name = name; + } + + void reset() { + count = 0l; + sum = 0l; + min = Integer.MAX_VALUE; + max = Integer.MIN_VALUE; + } + + void add(int val) { + count++; + sum += val; + if (val < min) { + min = val; + } + if (val > max) { + max = val; + } + } + + void add(long val) { + count++; + sum += val; + if (val < min) { + min = val; + } + if (val > max) { + max = val; + } + } + + void add(StatInteger stat) { + count += stat.count; + sum += stat.sum; + if (stat.min < min) { + min = stat.min; + } + if (stat.max > max) { + max = stat.max; + } + } + + public final double average() { + return ((double) sum) / count; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(128); + toString(sb); + return sb.toString(); + } + + public final StringBuilder toString(final StringBuilder sb) { + sb.append(name).append("[n: ").append(count); + sb.append("] sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average())); + sb.append(" [").append(min).append(" | ").append(max).append("]"); + return sb; + } + + } + + final static class Histogram extends StatInteger { + + static final int BUCKET = 2; + static final int MAX = 20; + static final int LAST = MAX - 1; + static final int[] STEPS = new int[MAX]; + static final int BUCKET_TH; + + static { + STEPS[0] = 0; + STEPS[1] = 1; + + for (int i = 2; i < MAX; i++) { + STEPS[i] = STEPS[i - 1] * BUCKET; + } +// System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS)); + + if (THRESHOLD_DELTA % 2 != 0) { + throw new IllegalStateException("THRESHOLD_DELTA must be odd"); + } + + BUCKET_TH = bucket(THRESHOLD_DELTA); + } + + static int bucket(int val) { + for (int i = 1; i < MAX; i++) { + if (val < STEPS[i]) { + return i - 1; + } + } + return LAST; + } + + private final StatInteger[] stats = new StatInteger[MAX]; + + public Histogram(String name) { + super(name); + for (int i = 0; i < MAX; i++) { + stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~"))); + } + } + + @Override + final void reset() { + super.reset(); + for (int i = 0; i < MAX; i++) { + stats[i].reset(); + } + } + + @Override + final void add(int val) { + super.add(val); + stats[bucket(val)].add(val); + } + + @Override + final void add(long val) { + add((int) val); + } + + void add(Histogram hist) { + super.add(hist); + for (int i = 0; i < MAX; i++) { + stats[i].add(hist.stats[i]); + } + } + + boolean isWorse(Histogram hist, boolean useTh) { + boolean worst = false; + if (!useTh && (hist.sum > sum)) { + worst = true; + } else { + long sumLoc = 0l; + long sumHist = 0l; + // use running sum: + for (int i = MAX - 1; i >= BUCKET_TH; i--) { + sumLoc += stats[i].sum; + sumHist += hist.stats[i].sum; + } + if (sumHist > sumLoc) { + worst = true; + } + } + /* + System.out.println("running sum worst:"); + System.out.println("this ? " + toString()); + System.out.println("worst ? " + hist.toString()); + */ + return worst; + } + + @Override + public final String toString() { + final StringBuilder sb = new StringBuilder(2048); + super.toString(sb).append(" { "); + + for (int i = 0; i < MAX; i++) { + if (stats[i].count != 0l) { + sb.append("\n ").append(stats[i].toString()); + } + } + + return sb.append(" }").toString(); + } + } + + /** + * Adjust the given double value to keep only 3 decimal digits + * @param value value to adjust + * @return double value with only 3 decimal digits + */ + static double trimTo3Digits(final double value) { + return ((long) (1e3d * value)) / 1e3d; + } + + static final class DiffContext { + + public final Histogram histPix; + + DiffContext(String name) { + histPix = new Histogram("Diff Pixels [" + name + "]"); + } + + void reset() { + histPix.reset(); + } + + void dump() { + if (isDiff()) { + System.out.println("Differences [" + histPix.name + "]:\n" + histPix.toString()); + } else { + System.out.println("No difference for [" + histPix.name + "]."); + } + } + + void add(int val) { + histPix.add(val); + } + + void add(DiffContext ctx) { + histPix.add(ctx.histPix); + } + + void set(DiffContext ctx) { + reset(); + add(ctx); + } + + boolean isWorse(DiffContext ctx, boolean useTh) { + return histPix.isWorse(ctx.histPix, useTh); + } + + boolean isDiff() { + return histPix.sum != 0l; + } + } +}