From 7492844c9702013dbef5040c2b810d7ddc22c5ac Mon Sep 17 00:00:00 2001 From: bourgesl Date: Sat, 30 Oct 2021 16:10:09 +0200 Subject: [PATCH] merge: updated to Marlin renderer 0.9.4.5 (fix Stroker and Filler path clipper to support huge coordinates up to 1E15) --- pom.xml | 9 +- ...yteArrayCache.java => ArrayCacheByte.java} | 55 +- ...eArrayCache.java => ArrayCacheDouble.java} | 55 +- ...{IntArrayCache.java => ArrayCacheInt.java} | 55 +- ...rrayCache.java => ArrayCacheIntClean.java} | 94 +- .../sun/marlin/DMarlinRenderingEngine.java | 1 + .../com/sun/marlin/DPQSSorterContext.java | 72 ++ src/main/java/com/sun/marlin/Dasher.java | 44 +- .../marlin/DualPivotQuicksort20191112Ext.java | 869 ++++++++++++++++++ src/main/java/com/sun/marlin/Helpers.java | 128 ++- .../java/com/sun/marlin/MarlinProperties.java | 7 +- src/main/java/com/sun/marlin/MergeSort.java | 88 +- src/main/java/com/sun/marlin/Renderer.java | 136 ++- .../java/com/sun/marlin/RendererContext.java | 45 +- .../java/com/sun/marlin/RendererNoAA.java | 102 +- .../java/com/sun/marlin/RendererStats.java | 8 +- src/main/java/com/sun/marlin/Stroker.java | 217 +++-- .../marlin/TransformingPathConsumer2D.java | 242 +++-- src/main/java/com/sun/marlin/Version.java | 2 +- src/main/java/com/sun/marlin/conv.sh | 29 - .../com/sun/marlin/makeIntFloatClasses.sh | 7 - src/main/java/test/BigLeftSide.java | 120 +++ src/main/java/test/PathApp.java | 140 +++ src/main/java/test/PathBug.java | 74 ++ src/main/java/test/PolygonClipTest.java | 227 +++++ src/main/java/test/PolygonTest.java | 110 +++ src/main/java/test/PolylineClipTest.java | 89 ++ src/main/java/test/PolylineWideClipTest.java | 326 +++++++ src/main/java/test/RasterPerf.java | 113 +++ src/main/java/test/ShapeOutlineBug.java | 37 + .../java/test/ShapeOutlineBugCirclePath.java | 82 ++ .../java/test/ShapeOutlineBugRectangle.java | 40 + src/main/java/test/ShapePerformanceBug.java | 36 + .../java/test/TestNonAARasterization.java | 669 ++++++++++++++ 34 files changed, 3839 insertions(+), 489 deletions(-) rename src/main/java/com/sun/marlin/{ByteArrayCache.java => ArrayCacheByte.java} (82%) rename src/main/java/com/sun/marlin/{DoubleArrayCache.java => ArrayCacheDouble.java} (82%) rename src/main/java/com/sun/marlin/{IntArrayCache.java => ArrayCacheInt.java} (82%) rename src/main/java/com/sun/marlin/{FloatArrayCache.java => ArrayCacheIntClean.java} (72%) create mode 100644 src/main/java/com/sun/marlin/DPQSSorterContext.java create mode 100644 src/main/java/com/sun/marlin/DualPivotQuicksort20191112Ext.java delete mode 100644 src/main/java/com/sun/marlin/conv.sh delete mode 100644 src/main/java/com/sun/marlin/makeIntFloatClasses.sh create mode 100644 src/main/java/test/BigLeftSide.java create mode 100644 src/main/java/test/PathApp.java create mode 100644 src/main/java/test/PathBug.java create mode 100644 src/main/java/test/PolygonClipTest.java create mode 100644 src/main/java/test/PolygonTest.java create mode 100644 src/main/java/test/PolylineClipTest.java create mode 100644 src/main/java/test/PolylineWideClipTest.java create mode 100644 src/main/java/test/RasterPerf.java create mode 100644 src/main/java/test/ShapeOutlineBug.java create mode 100644 src/main/java/test/ShapeOutlineBugCirclePath.java create mode 100644 src/main/java/test/ShapeOutlineBugRectangle.java create mode 100644 src/main/java/test/ShapePerformanceBug.java create mode 100644 src/main/java/test/TestNonAARasterization.java diff --git a/pom.xml b/pom.xml index c6e67aa..60fedf9 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ org.marlin marlinfx jar - 0.9.3.1-Unsafe-OpenJDK9 + 0.9.4.5-Unsafe-OpenJFX11 Marlin software rasterizer https://github.com/bourgesl/marlin-renderer @@ -67,8 +67,8 @@ maven-compiler-plugin 3.6.2 - 9 - 9 + 11 + 11 true UTF-8 true @@ -111,6 +111,9 @@ maven-jar-plugin 3.2.0 + + com/sun/** + true diff --git a/src/main/java/com/sun/marlin/ByteArrayCache.java b/src/main/java/com/sun/marlin/ArrayCacheByte.java similarity index 82% rename from src/main/java/com/sun/marlin/ByteArrayCache.java rename to src/main/java/com/sun/marlin/ArrayCacheByte.java index 660b6bf..4c19d39 100644 --- a/src/main/java/com/sun/marlin/ByteArrayCache.java +++ b/src/main/java/com/sun/marlin/ArrayCacheByte.java @@ -28,6 +28,13 @@ import static com.sun.marlin.ArrayCacheConst.ARRAY_SIZES; import static com.sun.marlin.ArrayCacheConst.BUCKETS; import static com.sun.marlin.ArrayCacheConst.MAX_ARRAY_SIZE; + +import static com.sun.marlin.MarlinConst.DO_STATS; +import static com.sun.marlin.MarlinConst.DO_CHECKS; +import static com.sun.marlin.MarlinConst.DO_CLEAN_DIRTY; +import static com.sun.marlin.MarlinConst.DO_LOG_WIDEN_ARRAY; +import static com.sun.marlin.MarlinConst.DO_LOG_OVERSIZE; + import static com.sun.marlin.MarlinUtils.logInfo; import static com.sun.marlin.MarlinUtils.logException; @@ -38,27 +45,23 @@ import com.sun.marlin.ArrayCacheConst.CacheStats; /* - * Note that the [BYTE/INT/FLOAT/DOUBLE]ArrayCache files are nearly identical except + * Note that the ArrayCache[BYTE/INT/FLOAT/DOUBLE] files are nearly identical except * for a few type and name differences. Typically, the [BYTE]ArrayCache.java file * is edited manually and then [INT/FLOAT/DOUBLE]ArrayCache.java * files are generated with the following command lines: */ -// % sed -e 's/(b\yte)[ ]*//g' -e 's/b\yte/int/g' -e 's/B\yte/Int/g' < B\yteArrayCache.java > IntArrayCache.java -// % sed -e 's/(b\yte)[ ]*0/0.0f/g' -e 's/(b\yte)[ ]*/(float) /g' -e 's/b\yte/float/g' -e 's/B\yte/Float/g' < B\yteArrayCache.java > FloatArrayCache.java -// % sed -e 's/(b\yte)[ ]*0/0.0d/g' -e 's/(b\yte)[ ]*/(double) /g' -e 's/b\yte/double/g' -e 's/B\yte/Double/g' < B\yteArrayCache.java > DoubleArrayCache.java -public final class ByteArrayCache implements MarlinConst { +final class ArrayCacheByte { - final boolean clean; + /* members */ private final int bucketCapacity; private WeakReference refBuckets = null; final CacheStats stats; - ByteArrayCache(final boolean clean, final int bucketCapacity) { - this.clean = clean; + ArrayCacheByte(final int bucketCapacity) { this.bucketCapacity = bucketCapacity; this.stats = (DO_STATS) ? - new CacheStats(getLogPrefix(clean) + "ByteArrayCache") : null; + new CacheStats("ArrayCacheByte(Dirty)") : null; } Bucket getCacheBucket(final int length) { @@ -75,7 +78,7 @@ private Bucket[] getBuckets() { buckets = new Bucket[BUCKETS]; for (int i = 0; i < BUCKETS; i++) { - buckets[i] = new Bucket(clean, ARRAY_SIZES[i], bucketCapacity, + buckets[i] = new Bucket(ARRAY_SIZES[i], bucketCapacity, (DO_STATS) ? stats.bucketStats[i] : null); } @@ -93,12 +96,10 @@ static final class Reference { // initial array reference (direct access) final byte[] initial; - private final boolean clean; - private final ByteArrayCache cache; + private final ArrayCacheByte cache; - Reference(final ByteArrayCache cache, final int initialSize) { + Reference(final ArrayCacheByte cache, final int initialSize) { this.cache = cache; - this.clean = cache.clean; this.initial = createArray(initialSize); if (DO_STATS) { cache.stats.totalInitial += initialSize; @@ -113,7 +114,7 @@ byte[] getArray(final int length) { cache.stats.oversize++; } if (DO_LOG_OVERSIZE) { - logInfo(getLogPrefix(clean) + "ByteArrayCache: " + logInfo("ArrayCacheByte(Dirty): " + "getArray[oversize]: length=\t" + length); } return createArray(length); @@ -141,7 +142,7 @@ byte[] widenArray(final byte[] array, final int usedSize, putArray(array, 0, usedSize); // ensure array is cleared if (DO_LOG_WIDEN_ARRAY) { - logInfo(getLogPrefix(clean) + "ByteArrayCache: " + logInfo("ArrayCacheByte(Dirty): " + "widenArray[" + res.length + "]: usedSize=\t" + usedSize + "\tlength=\t" + length + "\tneeded length=\t" + needSize); @@ -149,6 +150,10 @@ byte[] widenArray(final byte[] array, final int usedSize, return res; } + boolean doCleanRef(final byte[] array) { + return DO_CLEAN_DIRTY || (array != initial); + } + byte[] putArray(final byte[] array) { // dirty array helper: @@ -159,7 +164,7 @@ byte[] putArray(final byte[] array, final int fromIndex, final int toIndex) { if (array.length <= MAX_ARRAY_SIZE) { - if ((clean || DO_CLEAN_DIRTY) && (toIndex != 0)) { + if (DO_CLEAN_DIRTY && (toIndex != 0)) { // clean-up array of dirty part[fromIndex; toIndex[ fill(array, fromIndex, toIndex, (byte)0); } @@ -176,15 +181,13 @@ static final class Bucket { private int tail = 0; private final int arraySize; - private final boolean clean; private final byte[][] arrays; private final BucketStats stats; - Bucket(final boolean clean, final int arraySize, + Bucket(final int arraySize, final int capacity, final BucketStats stats) { this.arraySize = arraySize; - this.clean = clean; this.stats = stats; this.arrays = new byte[capacity][]; } @@ -208,7 +211,7 @@ byte[] getArray() { void putArray(final byte[] array) { if (DO_CHECKS && (array.length != arraySize)) { - logInfo(getLogPrefix(clean) + "ByteArrayCache: " + logInfo("ArrayCacheByte(Dirty): " + "bad length = " + array.length); return; } @@ -223,7 +226,7 @@ void putArray(final byte[] array) stats.updateMaxSize(tail); } } else if (DO_CHECKS) { - logInfo(getLogPrefix(clean) + "ByteArrayCache: " + logInfo("ArrayCacheByte(Dirty): " + "array capacity exceeded !"); } } @@ -243,8 +246,8 @@ static void fill(final byte[] array, final int fromIndex, } } - public static void check(final byte[] array, final int fromIndex, - final int toIndex, final byte value) + static void check(final byte[] array, final int fromIndex, + final int toIndex, final byte value) { if (DO_CHECKS) { // check zero on full array: @@ -262,8 +265,4 @@ public static void check(final byte[] array, final int fromIndex, } } } - - static String getLogPrefix(final boolean clean) { - return (clean) ? "Clean" : "Dirty"; - } } diff --git a/src/main/java/com/sun/marlin/DoubleArrayCache.java b/src/main/java/com/sun/marlin/ArrayCacheDouble.java similarity index 82% rename from src/main/java/com/sun/marlin/DoubleArrayCache.java rename to src/main/java/com/sun/marlin/ArrayCacheDouble.java index e1778ea..e20cd45 100644 --- a/src/main/java/com/sun/marlin/DoubleArrayCache.java +++ b/src/main/java/com/sun/marlin/ArrayCacheDouble.java @@ -28,6 +28,13 @@ import static com.sun.marlin.ArrayCacheConst.ARRAY_SIZES; import static com.sun.marlin.ArrayCacheConst.BUCKETS; import static com.sun.marlin.ArrayCacheConst.MAX_ARRAY_SIZE; + +import static com.sun.marlin.MarlinConst.DO_STATS; +import static com.sun.marlin.MarlinConst.DO_CHECKS; +import static com.sun.marlin.MarlinConst.DO_CLEAN_DIRTY; +import static com.sun.marlin.MarlinConst.DO_LOG_WIDEN_ARRAY; +import static com.sun.marlin.MarlinConst.DO_LOG_OVERSIZE; + import static com.sun.marlin.MarlinUtils.logInfo; import static com.sun.marlin.MarlinUtils.logException; @@ -38,27 +45,23 @@ import com.sun.marlin.ArrayCacheConst.CacheStats; /* - * Note that the [BYTE/INT/FLOAT/DOUBLE]ArrayCache files are nearly identical except + * Note that the ArrayCache[BYTE/INT/FLOAT/DOUBLE] files are nearly identical except * for a few type and name differences. Typically, the [BYTE]ArrayCache.java file * is edited manually and then [INT/FLOAT/DOUBLE]ArrayCache.java * files are generated with the following command lines: */ -// % sed -e 's/(b\yte)[ ]*//g' -e 's/b\yte/int/g' -e 's/B\yte/Int/g' < B\yteArrayCache.java > IntArrayCache.java -// % sed -e 's/(b\yte)[ ]*0/0.0f/g' -e 's/(b\yte)[ ]*/(float) /g' -e 's/b\yte/float/g' -e 's/B\yte/Float/g' < B\yteArrayCache.java > FloatArrayCache.java -// % sed -e 's/(b\yte)[ ]*0/0.0d/g' -e 's/(b\yte)[ ]*/(double) /g' -e 's/b\yte/double/g' -e 's/B\yte/Double/g' < B\yteArrayCache.java > DoubleArrayCache.java -public final class DoubleArrayCache implements MarlinConst { +final class ArrayCacheDouble { - final boolean clean; + /* members */ private final int bucketCapacity; private WeakReference refBuckets = null; final CacheStats stats; - DoubleArrayCache(final boolean clean, final int bucketCapacity) { - this.clean = clean; + ArrayCacheDouble(final int bucketCapacity) { this.bucketCapacity = bucketCapacity; this.stats = (DO_STATS) ? - new CacheStats(getLogPrefix(clean) + "DoubleArrayCache") : null; + new CacheStats("ArrayCacheDouble(Dirty)") : null; } Bucket getCacheBucket(final int length) { @@ -75,7 +78,7 @@ private Bucket[] getBuckets() { buckets = new Bucket[BUCKETS]; for (int i = 0; i < BUCKETS; i++) { - buckets[i] = new Bucket(clean, ARRAY_SIZES[i], bucketCapacity, + buckets[i] = new Bucket(ARRAY_SIZES[i], bucketCapacity, (DO_STATS) ? stats.bucketStats[i] : null); } @@ -93,12 +96,10 @@ static final class Reference { // initial array reference (direct access) final double[] initial; - private final boolean clean; - private final DoubleArrayCache cache; + private final ArrayCacheDouble cache; - Reference(final DoubleArrayCache cache, final int initialSize) { + Reference(final ArrayCacheDouble cache, final int initialSize) { this.cache = cache; - this.clean = cache.clean; this.initial = createArray(initialSize); if (DO_STATS) { cache.stats.totalInitial += initialSize; @@ -113,7 +114,7 @@ static final class Reference { cache.stats.oversize++; } if (DO_LOG_OVERSIZE) { - logInfo(getLogPrefix(clean) + "DoubleArrayCache: " + logInfo("ArrayCacheDouble(Dirty): " + "getArray[oversize]: length=\t" + length); } return createArray(length); @@ -141,7 +142,7 @@ static final class Reference { putArray(array, 0, usedSize); // ensure array is cleared if (DO_LOG_WIDEN_ARRAY) { - logInfo(getLogPrefix(clean) + "DoubleArrayCache: " + logInfo("ArrayCacheDouble(Dirty): " + "widenArray[" + res.length + "]: usedSize=\t" + usedSize + "\tlength=\t" + length + "\tneeded length=\t" + needSize); @@ -149,6 +150,10 @@ static final class Reference { return res; } + boolean doCleanRef(final double[] array) { + return DO_CLEAN_DIRTY || (array != initial); + } + double[] putArray(final double[] array) { // dirty array helper: @@ -159,7 +164,7 @@ static final class Reference { final int toIndex) { if (array.length <= MAX_ARRAY_SIZE) { - if ((clean || DO_CLEAN_DIRTY) && (toIndex != 0)) { + if (DO_CLEAN_DIRTY && (toIndex != 0)) { // clean-up array of dirty part[fromIndex; toIndex[ fill(array, fromIndex, toIndex, 0.0d); } @@ -176,15 +181,13 @@ static final class Bucket { private int tail = 0; private final int arraySize; - private final boolean clean; private final double[][] arrays; private final BucketStats stats; - Bucket(final boolean clean, final int arraySize, + Bucket(final int arraySize, final int capacity, final BucketStats stats) { this.arraySize = arraySize; - this.clean = clean; this.stats = stats; this.arrays = new double[capacity][]; } @@ -208,7 +211,7 @@ static final class Bucket { void putArray(final double[] array) { if (DO_CHECKS && (array.length != arraySize)) { - logInfo(getLogPrefix(clean) + "DoubleArrayCache: " + logInfo("ArrayCacheDouble(Dirty): " + "bad length = " + array.length); return; } @@ -223,7 +226,7 @@ void putArray(final double[] array) stats.updateMaxSize(tail); } } else if (DO_CHECKS) { - logInfo(getLogPrefix(clean) + "DoubleArrayCache: " + logInfo("ArrayCacheDouble(Dirty): " + "array capacity exceeded !"); } } @@ -243,8 +246,8 @@ static void fill(final double[] array, final int fromIndex, } } - public static void check(final double[] array, final int fromIndex, - final int toIndex, final double value) + static void check(final double[] array, final int fromIndex, + final int toIndex, final double value) { if (DO_CHECKS) { // check zero on full array: @@ -262,8 +265,4 @@ public static void check(final double[] array, final int fromIndex, } } } - - static String getLogPrefix(final boolean clean) { - return (clean) ? "Clean" : "Dirty"; - } } diff --git a/src/main/java/com/sun/marlin/IntArrayCache.java b/src/main/java/com/sun/marlin/ArrayCacheInt.java similarity index 82% rename from src/main/java/com/sun/marlin/IntArrayCache.java rename to src/main/java/com/sun/marlin/ArrayCacheInt.java index 00bd67e..c528ac0 100644 --- a/src/main/java/com/sun/marlin/IntArrayCache.java +++ b/src/main/java/com/sun/marlin/ArrayCacheInt.java @@ -28,6 +28,13 @@ import static com.sun.marlin.ArrayCacheConst.ARRAY_SIZES; import static com.sun.marlin.ArrayCacheConst.BUCKETS; import static com.sun.marlin.ArrayCacheConst.MAX_ARRAY_SIZE; + +import static com.sun.marlin.MarlinConst.DO_STATS; +import static com.sun.marlin.MarlinConst.DO_CHECKS; +import static com.sun.marlin.MarlinConst.DO_CLEAN_DIRTY; +import static com.sun.marlin.MarlinConst.DO_LOG_WIDEN_ARRAY; +import static com.sun.marlin.MarlinConst.DO_LOG_OVERSIZE; + import static com.sun.marlin.MarlinUtils.logInfo; import static com.sun.marlin.MarlinUtils.logException; @@ -38,27 +45,23 @@ import com.sun.marlin.ArrayCacheConst.CacheStats; /* - * Note that the [BYTE/INT/FLOAT/DOUBLE]ArrayCache files are nearly identical except + * Note that the ArrayCache[BYTE/INT/FLOAT/DOUBLE] files are nearly identical except * for a few type and name differences. Typically, the [BYTE]ArrayCache.java file * is edited manually and then [INT/FLOAT/DOUBLE]ArrayCache.java * files are generated with the following command lines: */ -// % sed -e 's/(b\yte)[ ]*//g' -e 's/b\yte/int/g' -e 's/B\yte/Int/g' < B\yteArrayCache.java > IntArrayCache.java -// % sed -e 's/(b\yte)[ ]*0/0.0f/g' -e 's/(b\yte)[ ]*/(float) /g' -e 's/b\yte/float/g' -e 's/B\yte/Float/g' < B\yteArrayCache.java > FloatArrayCache.java -// % sed -e 's/(b\yte)[ ]*0/0.0d/g' -e 's/(b\yte)[ ]*/(double) /g' -e 's/b\yte/double/g' -e 's/B\yte/Double/g' < B\yteArrayCache.java > DoubleArrayCache.java -public final class IntArrayCache implements MarlinConst { +final class ArrayCacheInt { - final boolean clean; + /* members */ private final int bucketCapacity; private WeakReference refBuckets = null; final CacheStats stats; - IntArrayCache(final boolean clean, final int bucketCapacity) { - this.clean = clean; + ArrayCacheInt(final int bucketCapacity) { this.bucketCapacity = bucketCapacity; this.stats = (DO_STATS) ? - new CacheStats(getLogPrefix(clean) + "IntArrayCache") : null; + new CacheStats("ArrayCacheInt(Dirty)") : null; } Bucket getCacheBucket(final int length) { @@ -75,7 +78,7 @@ private Bucket[] getBuckets() { buckets = new Bucket[BUCKETS]; for (int i = 0; i < BUCKETS; i++) { - buckets[i] = new Bucket(clean, ARRAY_SIZES[i], bucketCapacity, + buckets[i] = new Bucket(ARRAY_SIZES[i], bucketCapacity, (DO_STATS) ? stats.bucketStats[i] : null); } @@ -93,12 +96,10 @@ static final class Reference { // initial array reference (direct access) final int[] initial; - private final boolean clean; - private final IntArrayCache cache; + private final ArrayCacheInt cache; - Reference(final IntArrayCache cache, final int initialSize) { + Reference(final ArrayCacheInt cache, final int initialSize) { this.cache = cache; - this.clean = cache.clean; this.initial = createArray(initialSize); if (DO_STATS) { cache.stats.totalInitial += initialSize; @@ -113,7 +114,7 @@ int[] getArray(final int length) { cache.stats.oversize++; } if (DO_LOG_OVERSIZE) { - logInfo(getLogPrefix(clean) + "IntArrayCache: " + logInfo("ArrayCacheInt(Dirty): " + "getArray[oversize]: length=\t" + length); } return createArray(length); @@ -141,7 +142,7 @@ int[] widenArray(final int[] array, final int usedSize, putArray(array, 0, usedSize); // ensure array is cleared if (DO_LOG_WIDEN_ARRAY) { - logInfo(getLogPrefix(clean) + "IntArrayCache: " + logInfo("ArrayCacheInt(Dirty): " + "widenArray[" + res.length + "]: usedSize=\t" + usedSize + "\tlength=\t" + length + "\tneeded length=\t" + needSize); @@ -149,6 +150,10 @@ int[] widenArray(final int[] array, final int usedSize, return res; } + boolean doCleanRef(final int[] array) { + return DO_CLEAN_DIRTY || (array != initial); + } + int[] putArray(final int[] array) { // dirty array helper: @@ -159,7 +164,7 @@ int[] putArray(final int[] array, final int fromIndex, final int toIndex) { if (array.length <= MAX_ARRAY_SIZE) { - if ((clean || DO_CLEAN_DIRTY) && (toIndex != 0)) { + if (DO_CLEAN_DIRTY && (toIndex != 0)) { // clean-up array of dirty part[fromIndex; toIndex[ fill(array, fromIndex, toIndex, 0); } @@ -176,15 +181,13 @@ static final class Bucket { private int tail = 0; private final int arraySize; - private final boolean clean; private final int[][] arrays; private final BucketStats stats; - Bucket(final boolean clean, final int arraySize, + Bucket(final int arraySize, final int capacity, final BucketStats stats) { this.arraySize = arraySize; - this.clean = clean; this.stats = stats; this.arrays = new int[capacity][]; } @@ -208,7 +211,7 @@ int[] getArray() { void putArray(final int[] array) { if (DO_CHECKS && (array.length != arraySize)) { - logInfo(getLogPrefix(clean) + "IntArrayCache: " + logInfo("ArrayCacheInt(Dirty): " + "bad length = " + array.length); return; } @@ -223,7 +226,7 @@ void putArray(final int[] array) stats.updateMaxSize(tail); } } else if (DO_CHECKS) { - logInfo(getLogPrefix(clean) + "IntArrayCache: " + logInfo("ArrayCacheInt(Dirty): " + "array capacity exceeded !"); } } @@ -243,8 +246,8 @@ static void fill(final int[] array, final int fromIndex, } } - public static void check(final int[] array, final int fromIndex, - final int toIndex, final int value) + static void check(final int[] array, final int fromIndex, + final int toIndex, final int value) { if (DO_CHECKS) { // check zero on full array: @@ -262,8 +265,4 @@ public static void check(final int[] array, final int fromIndex, } } } - - static String getLogPrefix(final boolean clean) { - return (clean) ? "Clean" : "Dirty"; - } } diff --git a/src/main/java/com/sun/marlin/FloatArrayCache.java b/src/main/java/com/sun/marlin/ArrayCacheIntClean.java similarity index 72% rename from src/main/java/com/sun/marlin/FloatArrayCache.java rename to src/main/java/com/sun/marlin/ArrayCacheIntClean.java index b65a4fd..5bc5cd2 100644 --- a/src/main/java/com/sun/marlin/FloatArrayCache.java +++ b/src/main/java/com/sun/marlin/ArrayCacheIntClean.java @@ -28,6 +28,12 @@ import static com.sun.marlin.ArrayCacheConst.ARRAY_SIZES; import static com.sun.marlin.ArrayCacheConst.BUCKETS; import static com.sun.marlin.ArrayCacheConst.MAX_ARRAY_SIZE; + +import static com.sun.marlin.MarlinConst.DO_STATS; +import static com.sun.marlin.MarlinConst.DO_CHECKS; +import static com.sun.marlin.MarlinConst.DO_LOG_WIDEN_ARRAY; +import static com.sun.marlin.MarlinConst.DO_LOG_OVERSIZE; + import static com.sun.marlin.MarlinUtils.logInfo; import static com.sun.marlin.MarlinUtils.logException; @@ -38,27 +44,23 @@ import com.sun.marlin.ArrayCacheConst.CacheStats; /* - * Note that the [BYTE/INT/FLOAT/DOUBLE]ArrayCache files are nearly identical except + * Note that the ArrayCache[BYTE/INT/FLOAT/DOUBLE] files are nearly identical except * for a few type and name differences. Typically, the [BYTE]ArrayCache.java file * is edited manually and then [INT/FLOAT/DOUBLE]ArrayCache.java * files are generated with the following command lines: */ -// % sed -e 's/(b\yte)[ ]*//g' -e 's/b\yte/int/g' -e 's/B\yte/Int/g' < B\yteArrayCache.java > IntArrayCache.java -// % sed -e 's/(b\yte)[ ]*0/0.0f/g' -e 's/(b\yte)[ ]*/(float) /g' -e 's/b\yte/float/g' -e 's/B\yte/Float/g' < B\yteArrayCache.java > FloatArrayCache.java -// % sed -e 's/(b\yte)[ ]*0/0.0d/g' -e 's/(b\yte)[ ]*/(double) /g' -e 's/b\yte/double/g' -e 's/B\yte/Double/g' < B\yteArrayCache.java > DoubleArrayCache.java -public final class FloatArrayCache implements MarlinConst { +final class ArrayCacheIntClean { - final boolean clean; + /* members */ private final int bucketCapacity; private WeakReference refBuckets = null; final CacheStats stats; - FloatArrayCache(final boolean clean, final int bucketCapacity) { - this.clean = clean; + ArrayCacheIntClean(final int bucketCapacity) { this.bucketCapacity = bucketCapacity; this.stats = (DO_STATS) ? - new CacheStats(getLogPrefix(clean) + "FloatArrayCache") : null; + new CacheStats("ArrayCacheInt(Clean)") : null; } Bucket getCacheBucket(final int length) { @@ -75,7 +77,7 @@ private Bucket[] getBuckets() { buckets = new Bucket[BUCKETS]; for (int i = 0; i < BUCKETS; i++) { - buckets[i] = new Bucket(clean, ARRAY_SIZES[i], bucketCapacity, + buckets[i] = new Bucket(ARRAY_SIZES[i], bucketCapacity, (DO_STATS) ? stats.bucketStats[i] : null); } @@ -92,20 +94,18 @@ Reference createRef(final int initialSize) { static final class Reference { // initial array reference (direct access) - final float[] initial; - private final boolean clean; - private final FloatArrayCache cache; + final int[] initial; + private final ArrayCacheIntClean cache; - Reference(final FloatArrayCache cache, final int initialSize) { + Reference(final ArrayCacheIntClean cache, final int initialSize) { this.cache = cache; - this.clean = cache.clean; this.initial = createArray(initialSize); if (DO_STATS) { cache.stats.totalInitial += initialSize; } } - float[] getArray(final int length) { + int[] getArray(final int length) { if (length <= MAX_ARRAY_SIZE) { return cache.getCacheBucket(length).getArray(); } @@ -113,13 +113,13 @@ float[] getArray(final int length) { cache.stats.oversize++; } if (DO_LOG_OVERSIZE) { - logInfo(getLogPrefix(clean) + "FloatArrayCache: " + logInfo("ArrayCacheInt(Clean): " + "getArray[oversize]: length=\t" + length); } return createArray(length); } - float[] widenArray(final float[] array, final int usedSize, + int[] widenArray(final int[] array, final int usedSize, final int needSize) { final int length = array.length; @@ -132,7 +132,7 @@ float[] widenArray(final float[] array, final int usedSize, // maybe change bucket: // ensure getNewSize() > newSize: - final float[] res = getArray(ArrayCacheConst.getNewSize(usedSize, needSize)); + final int[] res = getArray(ArrayCacheConst.getNewSize(usedSize, needSize)); // use wrapper to ensure proper copy: System.arraycopy(array, 0, res, 0, usedSize); // copy only used elements @@ -141,7 +141,7 @@ float[] widenArray(final float[] array, final int usedSize, putArray(array, 0, usedSize); // ensure array is cleared if (DO_LOG_WIDEN_ARRAY) { - logInfo(getLogPrefix(clean) + "FloatArrayCache: " + logInfo("ArrayCacheInt(Clean): " + "widenArray[" + res.length + "]: usedSize=\t" + usedSize + "\tlength=\t" + length + "\tneeded length=\t" + needSize); @@ -149,19 +149,27 @@ float[] widenArray(final float[] array, final int usedSize, return res; } - float[] putArray(final float[] array) + boolean doSetRef(final int[] array) { + return (array != initial); + } + + int[] putArrayClean(final int[] array) { - // dirty array helper: - return putArray(array, 0, array.length); + // must be protected by doSetRef() call ! + if (array.length <= MAX_ARRAY_SIZE) { + // ensure to never store initial arrays in cache: + cache.getCacheBucket(array.length).putArray(array); + } + return initial; } - float[] putArray(final float[] array, final int fromIndex, + int[] putArray(final int[] array, final int fromIndex, final int toIndex) { if (array.length <= MAX_ARRAY_SIZE) { - if ((clean || DO_CLEAN_DIRTY) && (toIndex != 0)) { + if (toIndex != 0) { // clean-up array of dirty part[fromIndex; toIndex[ - fill(array, fromIndex, toIndex, 0.0f); + fill(array, fromIndex, toIndex, 0); } // ensure to never store initial arrays in cache: if (array != initial) { @@ -176,26 +184,24 @@ static final class Bucket { private int tail = 0; private final int arraySize; - private final boolean clean; - private final float[][] arrays; + private final int[][] arrays; private final BucketStats stats; - Bucket(final boolean clean, final int arraySize, + Bucket(final int arraySize, final int capacity, final BucketStats stats) { this.arraySize = arraySize; - this.clean = clean; this.stats = stats; - this.arrays = new float[capacity][]; + this.arrays = new int[capacity][]; } - float[] getArray() { + int[] getArray() { if (DO_STATS) { stats.getOp++; } // use cache: if (tail != 0) { - final float[] array = arrays[--tail]; + final int[] array = arrays[--tail]; arrays[tail] = null; return array; } @@ -205,10 +211,10 @@ float[] getArray() { return createArray(arraySize); } - void putArray(final float[] array) + void putArray(final int[] array) { if (DO_CHECKS && (array.length != arraySize)) { - logInfo(getLogPrefix(clean) + "FloatArrayCache: " + logInfo("ArrayCacheInt(Clean): " + "bad length = " + array.length); return; } @@ -223,18 +229,18 @@ void putArray(final float[] array) stats.updateMaxSize(tail); } } else if (DO_CHECKS) { - logInfo(getLogPrefix(clean) + "FloatArrayCache: " + logInfo("ArrayCacheInt(Clean): " + "array capacity exceeded !"); } } } - static float[] createArray(final int length) { - return new float[length]; + static int[] createArray(final int length) { + return new int[length]; } - static void fill(final float[] array, final int fromIndex, - final int toIndex, final float value) + static void fill(final int[] array, final int fromIndex, + final int toIndex, final int value) { // clear array data: Arrays.fill(array, fromIndex, toIndex, value); @@ -243,8 +249,8 @@ static void fill(final float[] array, final int fromIndex, } } - public static void check(final float[] array, final int fromIndex, - final int toIndex, final float value) + static void check(final int[] array, final int fromIndex, + final int toIndex, final int value) { if (DO_CHECKS) { // check zero on full array: @@ -262,8 +268,4 @@ public static void check(final float[] array, final int fromIndex, } } } - - static String getLogPrefix(final boolean clean) { - return (clean) ? "Clean" : "Dirty"; - } } diff --git a/src/main/java/com/sun/marlin/DMarlinRenderingEngine.java b/src/main/java/com/sun/marlin/DMarlinRenderingEngine.java index 7609119..42bdd7a 100644 --- a/src/main/java/com/sun/marlin/DMarlinRenderingEngine.java +++ b/src/main/java/com/sun/marlin/DMarlinRenderingEngine.java @@ -228,6 +228,7 @@ public static void logSettings(final String reClass) { + MarlinProperties.getQuadDecD2()); logInfo("Renderer settings:"); + logInfo("SORT = " + MergeSort.SORT_TYPE); logInfo("CUB_DEC_BND = " + Renderer.CUB_DEC_BND); logInfo("CUB_INC_BND = " + Renderer.CUB_INC_BND); logInfo("QUAD_DEC_BND = " + Renderer.QUAD_DEC_BND); diff --git a/src/main/java/com/sun/marlin/DPQSSorterContext.java b/src/main/java/com/sun/marlin/DPQSSorterContext.java new file mode 100644 index 0000000..906a074 --- /dev/null +++ b/src/main/java/com/sun/marlin/DPQSSorterContext.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 com.sun.marlin; + +/** + * DPQS Sorter context + */ +final class DPQSSorterContext { + + static final boolean LOG_ALLOC = false; + static final boolean CHECK_ALLOC = false && LOG_ALLOC; + + /** + * Max capacity of the index array for tracking runs. + */ + static final int MAX_RUN_CAPACITY = DualPivotQuicksort20191112Ext.MAX_RUN_CAPACITY; + + /* members */ + final int[] run; + int[] auxA; + int[] auxB; + boolean runInit; + + DPQSSorterContext() { + // preallocate max runs: + if (LOG_ALLOC) { + MarlinUtils.logInfo("alloc run: " + MAX_RUN_CAPACITY); + } + run = new int[MAX_RUN_CAPACITY]; + } + + void initBuffers(final int length, final int[] a, final int[] b) { + auxA = a; + if (CHECK_ALLOC && (a.length < length)) { + if (LOG_ALLOC) { + MarlinUtils.logInfo("alloc auxA: " + length); + } + auxA = new int[length]; + } + auxB = b; + if (CHECK_ALLOC && (b.length < length)) { + if (LOG_ALLOC) { + MarlinUtils.logInfo("alloc auxB: " + length); + } + auxB = new int[length]; + } + runInit = true; + } + +} diff --git a/src/main/java/com/sun/marlin/Dasher.java b/src/main/java/com/sun/marlin/Dasher.java index d9dc88c..afe4e09 100644 --- a/src/main/java/com/sun/marlin/Dasher.java +++ b/src/main/java/com/sun/marlin/Dasher.java @@ -90,9 +90,9 @@ public final class Dasher implements DPathConsumer2D, MarlinConst { private int firstSegidx; // dashes ref (dirty) - final DoubleArrayCache.Reference dashes_ref; + final ArrayCacheDouble.Reference dashes_ref; // firstSegmentsBuffer ref (dirty) - final DoubleArrayCache.Reference firstSegmentsBuffer_ref; + final ArrayCacheDouble.Reference firstSegmentsBuffer_ref; // Bounds of the drawing region, at pixel precision. private double[] clipRect; @@ -223,9 +223,13 @@ void dispose() { } // Return arrays: if (recycleDashes) { - dash = dashes_ref.putArray(dash); + if (dashes_ref.doCleanRef(dash)) { + dash = dashes_ref.putArray(dash); + } + } + if (firstSegmentsBuffer_ref.doCleanRef(firstSegmentsBuffer)) { + firstSegmentsBuffer = firstSegmentsBuffer_ref.putArray(firstSegmentsBuffer); } - firstSegmentsBuffer = firstSegmentsBuffer_ref.putArray(firstSegmentsBuffer); } public double[] copyDashArray(final float[] dashes) { @@ -392,6 +396,7 @@ public void lineTo(final double x1, final double y1) { skipLen(); } } + // Implicitely rdrCtx.isFirstSegment = true _lineTo(x1, y1); } @@ -538,7 +543,7 @@ public void skipLen() { // that contains the curve we want to dash in the first type elements private void somethingTo(final int type) { final double[] _curCurvepts = curCurvepts; - if (pointCurve(_curCurvepts, type)) { + if (Helpers.isPointCurve(_curCurvepts, type)) { return; } final LengthIterator _li = li; @@ -594,7 +599,7 @@ private void somethingTo(final int type) { private void skipSomethingTo(final int type) { final double[] _curCurvepts = curCurvepts; - if (pointCurve(_curCurvepts, type)) { + if (Helpers.isPointCurve(_curCurvepts, type)) { return; } final LengthIterator _li = li; @@ -614,15 +619,6 @@ private void skipSomethingTo(final int type) { this.starting = false; } - private static boolean pointCurve(final double[] curve, final int type) { - for (int i = 2; i < type; i++) { - if (curve[i] != curve[i-2]) { - return false; - } - } - return true; - } - // Objects of this class are used to iterate through curves. They return // t values where the left side of the curve has a specified length. // It does this by subdividing the input curve until a certain error @@ -704,7 +700,9 @@ void initializeIterationOnCurve(final double[] pts, final int type) { this.lenAtLastT = 0.0d; this.nextT = 0.0d; this.lenAtNextT = 0.0d; - goLeft(); // initializes nextT and lenAtNextT properly + // initializes nextT and lenAtNextT properly + goLeft(); + this.lenAtLastSplit = 0.0d; if (recLevel > 0) { this.sidesRight[0] = false; @@ -982,12 +980,19 @@ private void _curveTo(final double x1, final double y1, final int nSplits = monotonizer.nbSplits; final double[] mid = monotonizer.middle; + // Implicitely rdrCtx.isFirstSegment = true + for (int i = 0, off = 0; i <= nSplits; i++, off += 6) { // optimize arraycopy (8 values faster than 6 = type): System.arraycopy(mid, off, _curCurvepts, 0, 8); somethingTo(8); + + // set flag rdrCtx.isFirstSegment = false for other parts: + rdrCtx.isFirstSegment = false; // TODO: handle conflict with clipper } + // reset trigger to process further joins (normal operations) + rdrCtx.isFirstSegment = true; } private void skipCurveTo(final double x1, final double y1, @@ -1067,12 +1072,19 @@ private void _quadTo(final double x1, final double y1, final int nSplits = monotonizer.nbSplits; final double[] mid = monotonizer.middle; + // Implicitely rdrCtx.isFirstSegment = true + for (int i = 0, off = 0; i <= nSplits; i++, off += 4) { // optimize arraycopy (8 values faster than 6 = type): System.arraycopy(mid, off, _curCurvepts, 0, 8); somethingTo(6); + + // set flag rdrCtx.isFirstSegment = false for other parts: + rdrCtx.isFirstSegment = false; // TODO: handle conflict with clipper } + // reset trigger to process further joins (normal operations) + rdrCtx.isFirstSegment = true; } private void skipQuadTo(final double x1, final double y1, diff --git a/src/main/java/com/sun/marlin/DualPivotQuicksort20191112Ext.java b/src/main/java/com/sun/marlin/DualPivotQuicksort20191112Ext.java new file mode 100644 index 0000000..f1ae1a0 --- /dev/null +++ b/src/main/java/com/sun/marlin/DualPivotQuicksort20191112Ext.java @@ -0,0 +1,869 @@ +/* + * Copyright (c) 2009, 2019, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 com.sun.marlin; + +import java.util.Arrays; // TODO + +/** + * This class implements powerful and fully optimized versions, both + * sequential and parallel, of the Dual-Pivot Quicksort algorithm by + * Vladimir Yaroslavskiy, Jon Bentley and Josh Bloch. This algorithm + * offers O(n log(n)) performance on all data sets, and is typically + * faster than traditional (one-pivot) Quicksort implementations. + * + * There are also additional algorithms, invoked from the Dual-Pivot + * Quicksort, such as mixed insertion sort, merging of runs and heap + * sort, counting sort and parallel merge sort. + * + * @author Vladimir Yaroslavskiy + * @author Jon Bentley + * @author Josh Bloch + * @author Doug Lea + * + * @version 2018.08.18 + * + * @since 1.7 * 14 + */ +public final class DualPivotQuicksort20191112Ext { + + private static final boolean FAST_ISORT = false; + + /* + From OpenJDK14 source code: + 8226297: Dual-pivot quicksort improvements + Reviewed-by: dl, lbourges + Contributed-by: Vladimir Yaroslavskiy + Tue, 12 Nov 2019 13:49:40 -0800 + */ + /** + * Prevents instantiation. + */ + private DualPivotQuicksort20191112Ext() { + } + + /** + * Max array size to use mixed insertion sort. + */ + private static final int MAX_MIXED_INSERTION_SORT_SIZE = 65; + + /** + * Max array size to use insertion sort. + */ + private static final int MAX_INSERTION_SORT_SIZE = 44; + + /** + * Min array size to try merging of runs. + */ + private static final int MIN_TRY_MERGE_SIZE = 4 << 10; + + /** + * Min size of the first run to continue with scanning. + */ + private static final int MIN_FIRST_RUN_SIZE = 16; + + /** + * Min factor for the first runs to continue scanning. + */ + private static final int MIN_FIRST_RUNS_FACTOR = 7; + + /** + * Max capacity of the index array for tracking runs. + */ + /* private */ static final int MAX_RUN_CAPACITY = 5 << 10; + + /** + * Threshold of mixed insertion sort is incremented by this value. + */ + private static final int DELTA = 3 << 1; + + /** + * Max recursive partitioning depth before using heap sort. + */ + private static final int MAX_RECURSION_DEPTH = 64 * DELTA; + + + /** + * Sorts the specified range of the array. + * + * @param a the array to be sorted + * @param b the permutation array to be handled + * @param low the index of the first element, inclusive, to be sorted + * @param high the index of the last element, exclusive, to be sorted + */ + static void sort(DPQSSorterContext sorter, int[] a, int[] auxA, int b[], int[] auxB, int low, int high) { + /* + * LBO Shortcut: Invoke insertion sort on the leftmost part. + */ + if (FAST_ISORT && ((high - low) <= 24)) { + insertionSort(a, b, low, high); + return; + } + + sorter.initBuffers(high, auxA, auxB); + sort(sorter, a, b, 0, low, high); + } + + /** + * Sorts the specified array using the Dual-Pivot Quicksort and/or + * other sorts in special-cases, possibly with parallel partitions. + * + * @param sorter parallel context + * @param a the array to be sorted + * @param bits the combination of recursion depth and bit flag, where + * the right bit "0" indicates that array is the leftmost part + * @param low the index of the first element, inclusive, to be sorted + * @param high the index of the last element, exclusive, to be sorted + */ + private static void sort(DPQSSorterContext sorter, int[] a, int[] b, int bits, int low, int high) { + while (true) { + int end = high - 1, size = high - low; + + /* + * Run mixed insertion sort on small non-leftmost parts. + */ + if (size < MAX_MIXED_INSERTION_SORT_SIZE + bits && (bits & 1) > 0) { + mixedInsertionSort(a, b, low, high - 3 * ((size >> 5) << 3), high); + return; + } + + /* + * Invoke insertion sort on small leftmost part. + */ + if (size < MAX_INSERTION_SORT_SIZE) { + insertionSort(a, b, low, high); + return; + } + + /* + * Check if the whole array or large non-leftmost + * parts are nearly sorted and then merge runs. + */ + if ((bits == 0 || size > MIN_TRY_MERGE_SIZE && (bits & 1) > 0) + && tryMergeRuns(sorter, a, b, low, size)) { + return; + } + + /* + * Switch to heap sort if execution + * time is becoming quadratic. + */ + if ((bits += DELTA) > MAX_RECURSION_DEPTH) { + heapSort(a, b, low, high); + return; + } + + /* + * Use an inexpensive approximation of the golden ratio + * to select five sample elements and determine pivots. + */ + int step = (size >> 3) * 3 + 3; + + /* + * Five elements around (and including) the central element + * will be used for pivot selection as described below. The + * unequal choice of spacing these elements was empirically + * determined to work well on a wide variety of inputs. + */ + int e1 = low + step; + int e5 = end - step; + int e3 = (e1 + e5) >>> 1; + int e2 = (e1 + e3) >>> 1; + int e4 = (e3 + e5) >>> 1; + int a3 = a[e3]; + + /* + * Sort these elements in place by the combination + * of 4-element sorting network and insertion sort. + * + * 5 ------o-----------o------------ + * | | + * 4 ------|-----o-----o-----o------ + * | | | + * 2 ------o-----|-----o-----o------ + * | | + * 1 ------------o-----o------------ + */ + if (a[e5] < a[e2]) { int t = a[e5]; a[e5] = a[e2]; a[e2] = t; } + if (a[e4] < a[e1]) { int t = a[e4]; a[e4] = a[e1]; a[e1] = t; } + if (a[e5] < a[e4]) { int t = a[e5]; a[e5] = a[e4]; a[e4] = t; } + if (a[e2] < a[e1]) { int t = a[e2]; a[e2] = a[e1]; a[e1] = t; } + if (a[e4] < a[e2]) { int t = a[e4]; a[e4] = a[e2]; a[e2] = t; } + + if (a3 < a[e2]) { + if (a3 < a[e1]) { + a[e3] = a[e2]; a[e2] = a[e1]; a[e1] = a3; + } else { + a[e3] = a[e2]; a[e2] = a3; + } + } else if (a3 > a[e4]) { + if (a3 > a[e5]) { + a[e3] = a[e4]; a[e4] = a[e5]; a[e5] = a3; + } else { + a[e3] = a[e4]; a[e4] = a3; + } + } + + // Pointers + int lower = low; // The index of the last element of the left part + int upper = end; // The index of the first element of the right part + + /* + * Partitioning with 2 pivots in case of different elements. + */ + if (a[e1] < a[e2] && a[e2] < a[e3] && a[e3] < a[e4] && a[e4] < a[e5]) { + + /* + * Use the first and fifth of the five sorted elements as + * the pivots. These values are inexpensive approximation + * of tertiles. Note, that pivot1 < pivot2. + */ + int pivotA1 = a[e1]; + int pivotA2 = a[e5]; + int pivotB1 = b[e1]; + int pivotB2 = b[e5]; + + /* + * The first and the last elements to be sorted are moved + * to the locations formerly occupied by the pivots. When + * partitioning is completed, the pivots are swapped back + * into their final positions, and excluded from the next + * subsequent sorting. + */ + a[e1] = a[lower]; + a[e5] = a[upper]; + b[e1] = b[lower]; + b[e5] = b[upper]; + + /* + * Skip elements, which are less or greater than the pivots. + */ + while (a[++lower] < pivotA1); + while (a[--upper] > pivotA2); + + /* + * Backward 3-interval partitioning + * + * left part central part right part + * +------------------------------------------------------------+ + * | < pivot1 | ? | pivot1 <= && <= pivot2 | > pivot2 | + * +------------------------------------------------------------+ + * ^ ^ ^ + * | | | + * lower k upper + * + * Invariants: + * + * all in (low, lower] < pivot1 + * pivot1 <= all in (k, upper) <= pivot2 + * all in [upper, end) > pivot2 + * + * Pointer k is the last index of ?-part + */ + for (int unused = --lower, k = ++upper; --k > lower; ) { + int ak = a[k]; + int bk = b[k]; + + if (ak < pivotA1) { // Move a[k] to the left side + while (lower < k) { + if (a[++lower] >= pivotA1) { + if (a[lower] > pivotA2) { + a[k] = a[--upper]; + a[upper] = a[lower]; + b[k] = b[ upper]; + b[upper] = b[lower]; + } else { + a[k] = a[lower]; + b[k] = b[lower]; + } + a[lower] = ak; + b[lower] = bk; + break; + } + } + } else if (ak > pivotA2) { // Move a[k] to the right side + a[k] = a[--upper]; + a[upper] = ak; + b[k] = b[ upper]; + b[upper] = bk; + } + } + + /* + * Swap the pivots into their final positions. + */ + a[low] = a[lower]; a[lower] = pivotA1; + a[end] = a[upper]; a[upper] = pivotA2; + + b[low] = b[lower]; b[lower] = pivotB1; + b[end] = b[upper]; b[upper] = pivotB2; + + /* + * Sort non-left parts recursively (possibly in parallel), + * excluding known pivots. + */ + sort(sorter, a, b, bits | 1, lower + 1, upper); + sort(sorter, a, b, bits | 1, upper + 1, high); + + } else { // Use single pivot in case of many equal elements + + /* + * Use the third of the five sorted elements as the pivot. + * This value is inexpensive approximation of the median. + */ + int pivotA = a[e3]; + int pivotB = b[e3]; + + /* + * The first element to be sorted is moved to the + * location formerly occupied by the pivot. After + * completion of partitioning the pivot is swapped + * back into its final position, and excluded from + * the next subsequent sorting. + */ + a[e3] = a[lower]; + b[e3] = b[lower]; + + /* + * Traditional 3-way (Dutch National Flag) partitioning + * + * left part central part right part + * +------------------------------------------------------+ + * | < pivot | ? | == pivot | > pivot | + * +------------------------------------------------------+ + * ^ ^ ^ + * | | | + * lower k upper + * + * Invariants: + * + * all in (low, lower] < pivot + * all in (k, upper) == pivot + * all in [upper, end] > pivot + * + * Pointer k is the last index of ?-part + */ + for (int k = ++upper; --k > lower; ) { + int ak = a[k]; + + if (ak != pivotA) { + a[k] = pivotA; + int bk = b[k]; + + if (ak < pivotA) { // Move a[k] to the left side + while (a[++lower] < pivotA); + + if (a[lower] > pivotA) { + a[k] = a[--upper]; + a[upper] = a[lower]; + b[k] = b[ upper]; + b[upper] = b[lower]; + } else { + a[k] = a[lower]; + b[k] = b[lower]; + } + a[lower] = ak; + b[lower] = bk; + } else { // ak > pivot - Move a[k] to the right side + a[k] = a[--upper]; + a[upper] = ak; + b[k] = b[ upper]; + b[upper] = bk; + } + } + } + + /* + * Swap the pivot into its final position. + */ + a[low] = a[lower]; a[lower] = pivotA; + b[low] = b[lower]; b[lower] = pivotB; + + /* + * Sort the right part (possibly in parallel), excluding + * known pivot. All elements from the central part are + * equal and therefore already sorted. + */ + sort(sorter, a, b, bits | 1, upper, high); + } + high = lower; // Iterate along the left part + } + } + + /** + * Sorts the specified range of the array using mixed insertion sort. + * + * Mixed insertion sort is combination of simple insertion sort, + * pin insertion sort and pair insertion sort. + * + * In the context of Dual-Pivot Quicksort, the pivot element + * from the left part plays the role of sentinel, because it + * is less than any elements from the given part. Therefore, + * expensive check of the left range can be skipped on each + * iteration unless it is the leftmost call. + * + * @param a the array to be sorted + * @param low the index of the first element, inclusive, to be sorted + * @param end the index of the last element for simple insertion sort + * @param high the index of the last element, exclusive, to be sorted + */ + private static void mixedInsertionSort(int[] a, int[] b, int low, int end, int high) { + if (end == high) { + + /* + * Invoke simple insertion sort on tiny array. + */ + for (int i; ++low < end; ) { + int ai = a[i = low]; + + if (ai < a[i - 1]) { + int bi = b[i]; + + while (ai < a[--i]) { + a[i + 1] = a[i]; + b[i + 1] = b[i]; + } + a[i + 1] = ai; + b[i + 1] = bi; + } + } + } else { + + /* + * Start with pin insertion sort on small part. + * + * Pin insertion sort is extended simple insertion sort. + * The main idea of this sort is to put elements larger + * than an element called pin to the end of array (the + * proper area for such elements). It avoids expensive + * movements of these elements through the whole array. + */ + int pin = a[end]; + + for (int i, p = high; ++low < end; ) { + int ai = a[i = low]; + int bi = b[i]; + + if (ai < a[i - 1]) { // Small element + + /* + * Insert small element into sorted part. + */ + a[i] = a[i - 1]; + b[i] = b[--i]; + + while (ai < a[--i]) { + a[i + 1] = a[i]; + b[i + 1] = b[i]; + } + a[i + 1] = ai; + b[i + 1] = bi; + + } else if (p > i && ai > pin) { // Large element + + /* + * Find element smaller than pin. + */ + while (a[--p] > pin); + + /* + * Swap it with large element. + */ + if (p > i) { + ai = a[p]; + a[p] = a[i]; + bi = b[p]; + b[p] = b[i]; + } + + /* + * Insert small element into sorted part. + */ + while (ai < a[--i]) { + a[i + 1] = a[i]; + b[i + 1] = b[i]; + } + a[i + 1] = ai; + b[i + 1] = bi; + } + } + + /* + * Continue with pair insertion sort on remain part. + */ + for (int i; low < high; ++low) { + int a1 = a[i = low], a2 = a[++low]; + int b1 = b[i], b2 = b[ low]; + + /* + * Insert two elements per iteration: at first, insert the + * larger element and then insert the smaller element, but + * from the position where the larger element was inserted. + */ + if (a1 > a2) { + + while (a1 < a[--i]) { + a[i + 2] = a[i]; + b[i + 2] = b[i]; + } + a[++i + 1] = a1; + b[ i + 1] = b1; + + while (a2 < a[--i]) { + a[i + 1] = a[i]; + b[i + 1] = b[i]; + } + a[i + 1] = a2; + b[i + 1] = b2; + + } else if (a1 < a[i - 1]) { + + while (a2 < a[--i]) { + a[i + 2] = a[i]; + b[i + 2] = b[i]; + } + a[++i + 1] = a2; + b[ i + 1] = b2; + + while (a1 < a[--i]) { + a[i + 1] = a[i]; + b[i + 1] = b[i]; + } + a[i + 1] = a1; + b[i + 1] = b1; + } + } + } + } + + /** + * Sorts the specified range of the array using insertion sort. + * + * @param a the array to be sorted + * @param low the index of the first element, inclusive, to be sorted + * @param high the index of the last element, exclusive, to be sorted + */ + static void insertionSort(int[] a, int b[], int low, int high) { + for (int i, k = low; ++k < high; ) { + int ai = a[i = k]; + + if (ai < a[i - 1]) { + int bi = b[i]; + + while (--i >= low && ai < a[i]) { + a[i + 1] = a[i]; + b[i + 1] = b[i]; + } + a[i + 1] = ai; + b[i + 1] = bi; + } + } + } + + /** + * Sorts the specified range of the array using heap sort. + * + * @param a the array to be sorted + * @param low the index of the first element, inclusive, to be sorted + * @param high the index of the last element, exclusive, to be sorted + */ + private static void heapSort(int[] a, int b[], int low, int high) { + for (int k = (low + high) >>> 1; k > low; ) { + pushDown(a, b, --k, a[k], b[k], low, high); + } + while (--high > low) { + int maxA = a[low]; + int maxB = b[low]; + pushDown(a, b, low, a[high], b[high], low, high); + a[high] = maxA; + b[high] = maxB; + } + } + + /** + * Pushes specified element down during heap sort. + * + * @param a the given array + * @param p the start index + * @param valueA the given element + * @param low the index of the first element, inclusive, to be sorted + * @param high the index of the last element, exclusive, to be sorted + */ + private static void pushDown(int[] a, int b[], int p, int valueA, int valueB, int low, int high) { + for (int k;; a[p] = a[k], b[p] = b[p = k]) { + k = (p << 1) - low + 2; // Index of the right child + + if (k > high) { + break; + } + if (k == high || a[k] < a[k - 1]) { + --k; + } + if (a[k] <= valueA) { + break; + } + } + a[p] = valueA; + b[p] = valueB; + } + + /** + * Tries to sort the specified range of the array. + * + * @param sorter parallel context + * @param a the array to be sorted + * @param low the index of the first element to be sorted + * @param size the array size + * @return true if finally sorted, false otherwise + */ + private static boolean tryMergeRuns(DPQSSorterContext sorter, int[] a, int[] b, int low, int size) { + + /* + * The run array is constructed only if initial runs are + * long enough to continue, run[i] then holds start index + * of the i-th sequence of elements in non-descending order. + */ + int[] run = null; + int high = low + size; + int count = 1, last = low; + + /* + * Identify all possible runs. + */ + for (int k = low + 1; k < high; ) { + + /* + * Find the end index of the current run. + */ + if (a[k - 1] < a[k]) { + + // Identify ascending sequence + while (++k < high && a[k - 1] <= a[k]); + + } else if (a[k - 1] > a[k]) { + + // Identify descending sequence + while (++k < high && a[k - 1] >= a[k]); + + // Reverse into ascending order + for (int i = last - 1, j = k, t; ++i < --j && a[i] > a[j]; ) { + t = a[i]; a[i] = a[j]; a[j] = t; + t = b[i]; b[i] = b[j]; b[j] = t; + } + } else { // Identify constant sequence + for (int ak = a[k]; ++k < high && ak == a[k]; ); + + if (k < high) { + continue; + } + } + + /* + * Check special cases. + */ + if (sorter.runInit || run == null) { + sorter.runInit = false; // LBO + + if (k == high) { + + /* + * The array is monotonous sequence, + * and therefore already sorted. + */ + return true; + } + + if (k - low < MIN_FIRST_RUN_SIZE) { + + /* + * The first run is too small + * to proceed with scanning. + */ + return false; + } + +// System.out.println("alloc run"); +// run = new int[((size >> 10) | 0x7F) & 0x3FF]; + run = sorter.run; // LBO: prealloc + run[0] = low; + + } else if (a[last - 1] > a[last]) { + + if (count > (k - low) >> MIN_FIRST_RUNS_FACTOR) { + + /* + * The first runs are not long + * enough to continue scanning. + */ + return false; + } + + if (++count == MAX_RUN_CAPACITY) { + + /* + * Array is not highly structured. + */ + return false; + } + + if (false && count == run.length) { + + /* + * Increase capacity of index array. + */ +// System.out.println("alloc run (resize)"); + run = Arrays.copyOf(run, count << 1); + } + } + run[count] = (last = k); + + if (true) { + // fix ALMOST_CONTIGUOUS ie consecutive (ascending / descending runs) + if (k < high - 1) { + k++; // LBO + } + } + } + + /* + * Merge runs of highly structured array. + */ + if (count > 1) { + int[] auxA = sorter.auxA; + int[] auxB = sorter.auxB; + int offset = low; + + // LBO: prealloc + if ((auxA.length < size || auxB.length < size)) { +// System.out.println("alloc aux: "+size); + auxA = new int[size]; + auxB = new int[size]; + } + mergeRuns(a, auxA, b, auxB, offset, 1, run, 0, count); + } + return true; + } + + /** + * Merges the specified runs. + * + * @param srcA the source array + * @param dstA the temporary buffer used in merging + * @param offset the start index in the source, inclusive + * @param aim specifies merging: to source ( > 0), buffer ( < 0) or any ( == 0) + * @param run the start indexes of the runs, inclusive + * @param lo the start index of the first run, inclusive + * @param hi the start index of the last run, inclusive + * @return the destination where runs are merged + */ + private static int[] mergeRuns(int[] srcA, int[] dstA, int[] srcB, int[] dstB, int offset, + int aim, int[] run, int lo, int hi) { + + if (hi - lo == 1) { + if (aim >= 0) { + return srcA; + } + for (int i = run[hi], j = i - offset, low = run[lo]; i > low; + --j, --i, dstA[j] = srcA[i], dstB[j] = srcB[i] + ); + return dstA; + } + + /* + * Split into approximately equal parts. + */ + int mi = lo, rmi = (run[lo] + run[hi]) >>> 1; + while (run[++mi + 1] <= rmi); + + /* + * Merge the left and right parts. + */ + int[] a1, a2; + a1 = mergeRuns(srcA, dstA, srcB, dstB, offset, -aim, run, lo, mi); + a2 = mergeRuns(srcA, dstA, srcB, dstB, offset, 0, run, mi, hi); + + int[] b1, b2; + b1 = a1 == srcA ? srcB : dstB; + b2 = a2 == srcA ? srcB : dstB; + + int[] resA = a1 == srcA ? dstA : srcA; + int[] resB = a1 == srcA ? dstB : srcB; + + int k = a1 == srcA ? run[lo] - offset : run[lo]; + int lo1 = a1 == dstA ? run[lo] - offset : run[lo]; + int hi1 = a1 == dstA ? run[mi] - offset : run[mi]; + int lo2 = a2 == dstA ? run[mi] - offset : run[mi]; + int hi2 = a2 == dstA ? run[hi] - offset : run[hi]; + + mergeParts(resA, resB, k, a1, b1, lo1, hi1, a2, b2, lo2, hi2); + + return resA; + } + + /** + * Merges the sorted parts. + * + * @param dstA the destination where parts are merged + * @param k the start index of the destination, inclusive + * @param a1 the first part + * @param lo1 the start index of the first part, inclusive + * @param hi1 the end index of the first part, exclusive + * @param a2 the second part + * @param lo2 the start index of the second part, inclusive + * @param hi2 the end index of the second part, exclusive + */ + private static void mergeParts(int[] dstA, int[] dstB, int k, + int[] a1, int[] b1, int lo1, int hi1, int[] a2, int[] b2, int lo2, int hi2) { +// ... + /* + * Merge small parts sequentially. + */ + while (lo1 < hi1 && lo2 < hi2) { + if (a1[lo1] < a2[lo2]) { + dstA[k] = a1[lo1]; + dstB[k] = b1[lo1]; + k++; lo1++; + } else { + dstA[k] = a2[lo2]; + dstB[k] = b2[lo2]; + k++; lo2++; + } + } + if (dstA != a1 || k < lo1) { + while (lo1 < hi1) { + dstA[k] = a1[lo1]; + dstB[k] = b1[lo1]; + k++; lo1++; + } + } + if (dstA != a2 || k < lo2) { + while (lo2 < hi2) { + dstA[k] = a2[lo2]; + dstB[k] = b2[lo2]; + k++; lo2++; + } + } + } +} diff --git a/src/main/java/com/sun/marlin/Helpers.java b/src/main/java/com/sun/marlin/Helpers.java index 3acdc2e..eb7ecb0 100644 --- a/src/main/java/com/sun/marlin/Helpers.java +++ b/src/main/java/com/sun/marlin/Helpers.java @@ -31,15 +31,46 @@ final class Helpers implements MarlinConst { + private static final double EPS = 1e-9d; + private Helpers() { throw new Error("This is a non instantiable class"); } + static boolean within(final double x, final double y) { + return within(x, y, EPS); + } + static boolean within(final double x, final double y, final double err) { - final double d = y - x; + return withinD(y - x, err); + } + + static boolean withinD(final double d, final double err) { return (d <= err && d >= -err); } + static boolean withinD(final double dx, final double dy, final double err) + { + assert err > 0 : ""; + // compare taxicab distance. ERR will always be small, so using + // true distance won't give much benefit + return (withinD(dx, err) && // we want to avoid calling Math.abs + withinD(dy, err)); // this is just as good. + } + + static boolean isPointCurve(final double[] curve, final int type) { + return isPointCurve(curve, type, EPS); + } + + static boolean isPointCurve(final double[] curve, final int type, final double err) { + for (int i = 2; i < type; i++) { + if (!within(curve[i], curve[i - 2], err)) { + return false; + } + } + return true; + } + static double evalCubic(final double a, final double b, final double c, final double d, final double t) @@ -58,21 +89,24 @@ static int quadraticRoots(final double a, final double b, final double c, { int ret = off; if (a != 0.0d) { - final double dis = b*b - 4.0d * a * c; - if (dis > 0.0d) { - final double sqrtDis = Math.sqrt(dis); - // depending on the sign of b we use a slightly different - // algorithm than the traditional one to find one of the roots - // so we can avoid adding numbers of different signs (which - // might result in loss of precision). - if (b >= 0.0d) { - zeroes[ret++] = (2.0d * c) / (-b - sqrtDis); - zeroes[ret++] = (-b - sqrtDis) / (2.0d * a); - } else { - zeroes[ret++] = (-b + sqrtDis) / (2.0d * a); - zeroes[ret++] = (2.0d * c) / (-b + sqrtDis); + double d = b * b - 4.0d * a * c; + if (d > 0.0d) { + d = Math.sqrt(d); + // For accuracy, calculate one root using: + // (-b +/- d) / 2a + // and the other using: + // 2c / (-b +/- d) + // Choose the sign of the +/- so that b+d gets larger in magnitude + if (b < 0.0d) { + d = -d; + } + final double q = (b + d) / -2.0d; + // We already tested a for being 0 above + zeroes[ret++] = q / a; + if (q != 0.0d) { + zeroes[ret++] = c / q; } - } else if (dis == 0.0d) { + } else if (d == 0.0d) { zeroes[ret++] = -b / (2.0d * a); } } else if (b != 0.0d) { @@ -97,10 +131,6 @@ static int cubicRootsInAB(final double d, double a, double b, double c, // our own customized version). // normal form: x^3 + ax^2 + bx + c = 0 - - /* - * TODO: cleanup all that code after reading Roots3And4.c - */ a /= d; b /= d; c /= d; @@ -113,18 +143,30 @@ static int cubicRootsInAB(final double d, double a, double b, double c, // p = P/3 // q = Q/2 // instead and use those values for simplicity of the code. - final double sub = (1.0d / 3.0d) * a; final double sq_A = a * a; final double p = (1.0d / 3.0d) * ((-1.0d / 3.0d) * sq_A + b); + final double sub = (1.0d / 3.0d) * a; final double q = (1.0d / 2.0d) * ((2.0d / 27.0d) * a * sq_A - sub * b + c); // use Cardano's formula - final double cb_p = p * p * p; final double D = q * q + cb_p; int num; - if (D < 0.0d) { + + if (within(D, 0.0d)) { + if (within(q, 0.0d)) { + /* one triple solution */ + pts[off ] = (- sub); + num = 1; + } else { + /* one single and one double solution */ + final double u = Math.cbrt(-q); + pts[off ] = (2.0d * u - sub); + pts[off + 1] = (- u - sub); + num = 2; + } + } else if (D < 0.0d) { // see: http://en.wikipedia.org/wiki/Cubic_function#Trigonometric_.28and_hyperbolic.29_method final double phi = (1.0d / 3.0d) * Math.acos(-q / Math.sqrt(-cb_p)); final double t = 2.0d * Math.sqrt(-p); @@ -140,13 +182,7 @@ static int cubicRootsInAB(final double d, double a, double b, double c, pts[off ] = (u + v - sub); num = 1; - - if (within(D, 0.0d, 1e-8d)) { - pts[off + 1] = ((-1.0d / 2.0d) * (u + v) - sub); - num = 2; - } } - return filterOutNotInAB(pts, off, num, A, B) - off; } @@ -621,9 +657,9 @@ static final class PolyStack { int numCurves; // curves ref (dirty) - final DoubleArrayCache.Reference curves_ref; + final ArrayCacheDouble.Reference curves_ref; // curveTypes ref (dirty) - final ByteArrayCache.Reference curveTypes_ref; + final ArrayCacheByte.Reference curveTypes_ref; // used marks (stats only) int curveTypesUseMark; @@ -670,7 +706,7 @@ static final class PolyStack { * clean up before reusing this instance */ void dispose() { - end = 0; + end = 0; numCurves = 0; if (DO_STATS) { @@ -685,8 +721,12 @@ void dispose() { // Return arrays: // curves and curveTypes are kept dirty - curves = curves_ref.putArray(curves); - curveTypes = curveTypes_ref.putArray(curveTypes); + if (curves_ref.doCleanRef(curves)) { + curves = curves_ref.putArray(curves); + } + if (curveTypes_ref.doCleanRef(curveTypes)) { + curveTypes = curveTypes_ref.putArray(curveTypes); + } } private void ensureSpace(final int n) { @@ -865,7 +905,7 @@ static final class IndexStack { private int[] indices; // indices ref (dirty) - private final IntArrayCache.Reference indices_ref; + private final ArrayCacheInt.Reference indices_ref; // used marks (stats only) private int indicesUseMark; @@ -911,8 +951,10 @@ void dispose() { } // Return arrays: - // values is kept dirty - indices = indices_ref.putArray(indices); + // indices is kept dirty + if (indices_ref.doCleanRef(indices)) { + indices = indices_ref.putArray(indices); + } } boolean isEmpty() { @@ -950,14 +992,24 @@ void push(final int v) { } } - void pullAll(final double[] points, final DPathConsumer2D io) { + void pullAll(final double[] points, final DPathConsumer2D io, + final boolean moveFirst) + { final int nc = end; if (nc == 0) { return; } final int[] _values = indices; + + int i = 0; + + if (moveFirst) { + int j = _values[i] << 1; + io.moveTo(points[j], points[j + 1]); + i++; + } - for (int i = 0, j; i < nc; i++) { + for (int j; i < nc; i++) { j = _values[i] << 1; io.lineTo(points[j], points[j + 1]); } diff --git a/src/main/java/com/sun/marlin/MarlinProperties.java b/src/main/java/com/sun/marlin/MarlinProperties.java index 62d9ddf..604f960 100644 --- a/src/main/java/com/sun/marlin/MarlinProperties.java +++ b/src/main/java/com/sun/marlin/MarlinProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2021, 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 @@ -189,6 +189,10 @@ public static float getSubdividerMinLength() { return getFloat("prism.marlin.clip.subdivider.minLength", 100.0f, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY); } + public static boolean isUseDPQS() { + return getBoolean("prism.marlin.useDPQS", "true"); + } + // debugging parameters public static boolean isDoStats() { @@ -222,6 +226,7 @@ public static boolean isLogUnsafeMalloc() { } // quality settings + public static float getCurveLengthError() { return getFloat("prism.marlin.curve_len_err", 0.01f, 1e-6f, 1.0f); } diff --git a/src/main/java/com/sun/marlin/MergeSort.java b/src/main/java/com/sun/marlin/MergeSort.java index d51a814..e4cb5c9 100644 --- a/src/main/java/com/sun/marlin/MergeSort.java +++ b/src/main/java/com/sun/marlin/MergeSort.java @@ -25,6 +25,9 @@ package com.sun.marlin; +import java.util.Arrays; +import static com.sun.marlin.DualPivotQuicksort20191112Ext.sort; + /** * MergeSort adapted from (OpenJDK 8) java.util.Array.legacyMergeSort(Object[]) * to swap two arrays at the same time (x & y) @@ -32,8 +35,22 @@ */ final class MergeSort { - // insertion sort threshold - public static final int INSERTION_SORT_THRESHOLD = 14; + static final boolean USE_DPQS = MarlinProperties.isUseDPQS(); + + static final String SORT_TYPE = USE_DPQS ? "DPQS_20191112" : "MERGE"; + + static final int DPQS_THRESHOLD = 256; + static final int DISABLE_ISORT_THRESHOLD = 1000; + + private static final boolean CHECK_SORTED = false; + + static { + MarlinUtils.logInfo("MergeSort: DPQS_THRESHOLD: " + DPQS_THRESHOLD); + MarlinUtils.logInfo("MergeSort: DISABLE_ISORT_THRESHOLD: " + DISABLE_ISORT_THRESHOLD); + if (CHECK_SORTED) { + MarlinUtils.logInfo("MergeSort: CHECK_SORTED: " + CHECK_SORTED); + } + } /** * Modified merge sort: @@ -44,34 +61,61 @@ final class MergeSort { static void mergeSortNoCopy(final int[] x, final int[] y, final int[] auxX, final int[] auxY, final int toIndex, - final int insertionSortIndex) - { + final int insertionSortIndex, + final boolean skipISort, + final DPQSSorterContext sorter, + final boolean useDPQS) { + if ((toIndex > x.length) || (toIndex > y.length) || (toIndex > auxX.length) || (toIndex > auxY.length)) { // explicit check to avoid bound checks within hot loops (below): throw new ArrayIndexOutOfBoundsException("bad arguments: toIndex=" - + toIndex); + + toIndex); + } + if (skipISort) { + if (useDPQS) { + // sort full x/y in-place + sort(sorter, x, auxX, y, auxY, 0, toIndex); + } else { + // sort full auxX/auxY into x/y + mergeSort(auxX, auxY, auxX, x, auxY, y, 0, toIndex); + } + if (CHECK_SORTED) { + checkRange(x, 0, toIndex); + } + return; + } else { + if (useDPQS) { + // sort auxX/auxY in-place + sort(sorter, auxX, x, auxY, y, insertionSortIndex, toIndex); + } else { + // sort second part only using merge sort + // x/y into auxiliary storage (auxX/auxY) + mergeSort(x, y, x, auxX, y, auxY, insertionSortIndex, toIndex); + } } - - // sort second part only using merge / insertion sort - // in auxiliary storage (auxX/auxY) - mergeSort(x, y, x, auxX, y, auxY, insertionSortIndex, toIndex); // final pass to merge both // Merge sorted parts (auxX/auxY) into x/y arrays +/* +// low probability: deoptimization ? if ((insertionSortIndex == 0) - || (auxX[insertionSortIndex - 1] <= auxX[insertionSortIndex])) { + || (auxX[insertionSortIndex - 1] <= auxX[insertionSortIndex])) { // 34 occurences // no initial left part or both sublists (auxX, auxY) are sorted: // copy back data into (x, y): System.arraycopy(auxX, 0, x, 0, toIndex); System.arraycopy(auxY, 0, y, 0, toIndex); + + if (CHECK_SORTED) { + checkRange(x, 0, toIndex); + } return; } - +*/ for (int i = 0, p = 0, q = insertionSortIndex; i < toIndex; i++) { if ((q >= toIndex) || ((p < insertionSortIndex) - && (auxX[p] <= auxX[q]))) { + && (auxX[p] <= auxX[q]))) { x[i] = auxX[p]; y[i] = auxY[p]; p++; @@ -81,8 +125,15 @@ static void mergeSortNoCopy(final int[] x, final int[] y, q++; } } + + if (CHECK_SORTED) { + checkRange(x, 0, toIndex); + } } + // insertion sort threshold for MergeSort() + static final int INSERTION_SORT_THRESHOLD = 14; + /** * Src is the source array that starts at index 0 * Dest is the (possibly larger) array destination with a possible offset @@ -92,8 +143,7 @@ static void mergeSortNoCopy(final int[] x, final int[] y, private static void mergeSort(final int[] refX, final int[] refY, final int[] srcX, final int[] dstX, final int[] srcY, final int[] dstY, - final int low, final int high) - { + final int low, final int high) { final int length = high - low; /* @@ -124,7 +174,6 @@ private static void mergeSort(final int[] refX, final int[] refY, } // Recursively sort halves of dest into src - // note: use signed shift (not >>>) for performance // as indices are small enough to exceed Integer.MAX_VALUE final int mid = (low + high) >> 1; @@ -171,4 +220,13 @@ private static void mergeSort(final int[] refX, final int[] refY, private MergeSort() { } + + private static void checkRange(int[] x, int lo, int hi) { + for (int i = lo + 1; i < hi; i++) { + if (x[i - 1] > x[i]) { + MarlinUtils.logInfo("Bad sorted x [" + (i - 1) + "]" + Arrays.toString(Arrays.copyOf(x, hi))); + return; + } + } + } } diff --git a/src/main/java/com/sun/marlin/Renderer.java b/src/main/java/com/sun/marlin/Renderer.java index 3680f8d..9d5893d 100644 --- a/src/main/java/com/sun/marlin/Renderer.java +++ b/src/main/java/com/sun/marlin/Renderer.java @@ -130,14 +130,14 @@ public final class Renderer implements MarlinRenderer, MarlinConst { private int activeEdgeMaxUsed; // crossings ref (dirty) - private final IntArrayCache.Reference crossings_ref; + private final ArrayCacheInt.Reference crossings_ref; // edgePtrs ref (dirty) - private final IntArrayCache.Reference edgePtrs_ref; + private final ArrayCacheInt.Reference edgePtrs_ref; // merge sort initial arrays (large enough to satisfy most usages) (1024) // aux_crossings ref (dirty) - private final IntArrayCache.Reference aux_crossings_ref; + private final ArrayCacheInt.Reference aux_crossings_ref; // aux_edgePtrs ref (dirty) - private final IntArrayCache.Reference aux_edgePtrs_ref; + private final ArrayCacheInt.Reference aux_edgePtrs_ref; ////////////////////////////////////////////////////////////////////////////// // EDGE LIST @@ -157,9 +157,9 @@ public final class Renderer implements MarlinRenderer, MarlinConst { private int buckets_maxY; // edgeBuckets ref (clean) - private final IntArrayCache.Reference edgeBuckets_ref; + private final ArrayCacheIntClean.Reference edgeBuckets_ref; // edgeBucketCounts ref (clean) - private final IntArrayCache.Reference edgeBucketCounts_ref; + private final ArrayCacheIntClean.Reference edgeBucketCounts_ref; boolean useRLE = false; @@ -496,7 +496,7 @@ private void addLine(double x1, double y1, double x2, double y2) { private int[] alphaLine; // alphaLine ref (clean) - private final IntArrayCache.Reference alphaLine_ref; + private final ArrayCacheIntClean.Reference alphaLine_ref; private boolean enableBlkFlags = false; private boolean prevUseBlkFlags = false; @@ -505,7 +505,7 @@ private void addLine(double x1, double y1, double x2, double y2) { private int[] blkFlags; // blkFlags ref (clean) - private final IntArrayCache.Reference blkFlags_ref; + private final ArrayCacheIntClean.Reference blkFlags_ref; Renderer(final RendererContext rdrCtx) { this.rdrCtx = rdrCtx; @@ -600,14 +600,26 @@ public void dispose() { rdrCtx.stats.totalOffHeap += edges.length; } // Return arrays: - crossings = crossings_ref.putArray(crossings); - aux_crossings = aux_crossings_ref.putArray(aux_crossings); + if (crossings_ref.doCleanRef(crossings)) { + crossings = crossings_ref.putArray(crossings); + } + if (aux_crossings_ref.doCleanRef(aux_crossings)) { + aux_crossings = aux_crossings_ref.putArray(aux_crossings); + } - edgePtrs = edgePtrs_ref.putArray(edgePtrs); - aux_edgePtrs = aux_edgePtrs_ref.putArray(aux_edgePtrs); + if (edgePtrs_ref.doCleanRef(edgePtrs)) { + edgePtrs = edgePtrs_ref.putArray(edgePtrs); + } + if (aux_edgePtrs_ref.doCleanRef(aux_edgePtrs)) { + aux_edgePtrs = aux_edgePtrs_ref.putArray(aux_edgePtrs); + } - alphaLine = alphaLine_ref.putArray(alphaLine, 0, 0); // already zero filled - blkFlags = blkFlags_ref.putArray(blkFlags, 0, 0); // already zero filled + if (alphaLine_ref.doSetRef(alphaLine)) { + alphaLine = alphaLine_ref.putArrayClean(alphaLine); // already zero filled + } + if (blkFlags_ref.doSetRef(blkFlags)) { + blkFlags = blkFlags_ref.putArrayClean(blkFlags); // already zero filled + } if (edgeMinY != Integer.MAX_VALUE) { // if context is maked as DIRTY: @@ -625,8 +637,12 @@ public void dispose() { buckets_maxY + 1); } else { // unused arrays - edgeBuckets = edgeBuckets_ref.putArray(edgeBuckets, 0, 0); - edgeBucketCounts = edgeBucketCounts_ref.putArray(edgeBucketCounts, 0, 0); + if (edgeBuckets_ref.doSetRef(edgeBuckets)) { + edgeBuckets = edgeBuckets_ref.putArrayClean(edgeBuckets); + } + if (edgeBucketCounts_ref.doSetRef(edgeBucketCounts)) { + edgeBucketCounts = edgeBucketCounts_ref.putArrayClean(edgeBucketCounts); + } } // At last: resize back off-heap edges to initial size @@ -704,7 +720,7 @@ public void quadTo(final double pix_x1, final double pix_y1, @Override public void closePath() { - if (x0 != sx0 || y0 != sy0) { + if ((x0 != sx0) || (y0 != sy0)) { addLine(x0, y0, sx0, sy0); x0 = sx0; y0 = sy0; @@ -811,6 +827,8 @@ private void _endRendering(final int ymin, final int ymax, int lastY = -1; // last emited row + final DPQSSorterContext sorter = rdrCtx.sorterCtx; + boolean skipISort, useDPQS; // Iteration on scanlines for (; y < ymax; y++, bucket++) { @@ -823,7 +841,7 @@ private void _endRendering(final int ymin, final int ymax, // bucketCount indicates new edge / edge end: if (bucketcount != 0) { if (DO_STATS) { - rdrCtx.stats.stat_rdr_activeEdges_updates.add(numCrossings); + rdrCtx.stats.stat_rdr_activeEdges_updates.add(prevNumCrossings); } // last bit set to 1 means that edges ends @@ -832,7 +850,7 @@ private void _endRendering(final int ymin, final int ymax, // cache edges[] address + offset addr = addr0 + _OFF_YMAX; - for (i = 0, newCount = 0; i < numCrossings; i++) { + for (i = 0, newCount = 0; i < prevNumCrossings; i++) { // get the pointer to the edge ecur = _edgePtrs[i]; // random access so use unsafe: @@ -860,7 +878,7 @@ private void _endRendering(final int ymin, final int ymax, rdrCtx.stats.stat_array_renderer_edgePtrs.add(ptrEnd); } this.edgePtrs = _edgePtrs - = edgePtrs_ref.widenArray(_edgePtrs, numCrossings, + = edgePtrs_ref.widenArray(_edgePtrs, edgePtrsLen, // bad mark ? TODO: fix edge ptr mark ptrEnd); edgePtrsLen = _edgePtrs.length; @@ -929,7 +947,7 @@ private void _endRendering(final int ymin, final int ymax, * thresholds to switch to optimized merge sort * for newly added edges + final merge pass. */ - if ((ptrLen < 10) || (numCrossings < 40)) { + if (((numCrossings <= 40) || ((ptrLen <= 10) && (numCrossings <= MergeSort.DISABLE_ISORT_THRESHOLD)))) { if (DO_STATS) { rdrCtx.stats.hist_rdr_crossings.add(numCrossings); rdrCtx.stats.hist_rdr_crossings_adds.add(ptrLen); @@ -1015,7 +1033,7 @@ private void _endRendering(final int ymin, final int ymax, } else { j = i - 1; _crossings[i] = _crossings[j]; - _edgePtrs[i] = _edgePtrs[j]; + _edgePtrs[i] = _edgePtrs[j]; while ((--j >= 0) && (_crossings[j] > cross)) { _crossings[j + 1] = _crossings[j]; @@ -1042,6 +1060,13 @@ private void _endRendering(final int ymin, final int ymax, // and perform insertion sort on almost sorted data // (ie i < prevNumCrossings): + skipISort = (prevNumCrossings >= MergeSort.DISABLE_ISORT_THRESHOLD); + useDPQS = MergeSort.USE_DPQS && (skipISort || (ptrLen >= MergeSort.DPQS_THRESHOLD)); + + if (DO_STATS && useDPQS) { + rdrCtx.stats.stat_rdr_crossings_dpqs.add((skipISort) ? numCrossings : ptrLen); + } + lastCross = _MIN_VALUE; for (i = 0; i < numCrossings; i++) { @@ -1078,39 +1103,58 @@ private void _endRendering(final int ymin, final int ymax, rdrCtx.stats.stat_rdr_crossings_updates.add(numCrossings); } - if (i >= prevNumCrossings) { - // simply store crossing as edgePtrs is in-place: - // will be copied and sorted efficiently by mergesort later: - _crossings[i] = cross; - - } else if (cross < lastCross) { - if (DO_STATS) { - rdrCtx.stats.stat_rdr_crossings_sorts.add(i); + if (skipISort) { + if (useDPQS) { + // simply store crossing as edgePtrs is in-place: + // will be sorted efficiently by DPQS later: + _crossings[i] = cross; + } else { + // store crossing/edgePtrs in auxiliary arrays: + // will be sorted efficiently by MergeSort later: + _aux_crossings[i] = cross; + _aux_edgePtrs [i] = ecur; } - - // (straight) insertion sort of crossings: - j = i - 1; - _aux_crossings[i] = _aux_crossings[j]; - _aux_edgePtrs[i] = _aux_edgePtrs[j]; - - while ((--j >= 0) && (_aux_crossings[j] > cross)) { - _aux_crossings[j + 1] = _aux_crossings[j]; - _aux_edgePtrs [j + 1] = _aux_edgePtrs[j]; + } else if (i >= prevNumCrossings) { + if (useDPQS) { + // store crossing/edgePtrs in auxiliary arrays: + // will be sorted efficiently by DPQS later: + _aux_crossings[i] = cross; + _aux_edgePtrs [i] = ecur; + } else { + // simply store crossing as edgePtrs is in-place: + // will be sorted efficiently by MergeSort later: + _crossings[i] = cross; } - _aux_crossings[j + 1] = cross; - _aux_edgePtrs [j + 1] = ecur; - } else { - // auxiliary storage: - _aux_crossings[i] = lastCross = cross; - _aux_edgePtrs [i] = ecur; + if (cross < lastCross) { + if (DO_STATS) { + rdrCtx.stats.stat_rdr_crossings_sorts.add(i); + } + // (straight) insertion sort of crossings: + j = i - 1; + _aux_crossings[i] = _aux_crossings[j]; + _aux_edgePtrs [i] = _aux_edgePtrs[j]; + + while ((--j >= 0) && (_aux_crossings[j] > cross)) { + _aux_crossings[j + 1] = _aux_crossings[j]; + _aux_edgePtrs [j + 1] = _aux_edgePtrs[j]; + } + _aux_crossings[j + 1] = cross; + _aux_edgePtrs [j + 1] = ecur; + } else { + // auxiliary storage: + _aux_crossings[i] = lastCross = cross; + _aux_edgePtrs [i] = ecur; + } } } // use Mergesort using auxiliary arrays (sort only right part) MergeSort.mergeSortNoCopy(_crossings, _edgePtrs, _aux_crossings, _aux_edgePtrs, - numCrossings, prevNumCrossings); + numCrossings, prevNumCrossings, + skipISort, sorter, useDPQS + ); } // reset ptrLen diff --git a/src/main/java/com/sun/marlin/RendererContext.java b/src/main/java/com/sun/marlin/RendererContext.java index 6ec16f1..4d2ece5 100644 --- a/src/main/java/com/sun/marlin/RendererContext.java +++ b/src/main/java/com/sun/marlin/RendererContext.java @@ -33,6 +33,7 @@ import com.sun.marlin.TransformingPathConsumer2D.CurveBasicMonotonizer; import com.sun.marlin.TransformingPathConsumer2D.CurveClipSplitter; import com.sun.util.reentrant.ReentrantContext; +import com.sun.marlin.ArrayCacheIntClean; /** * This class is a renderer context dedicated to a single thread @@ -48,8 +49,7 @@ public final class RendererContext extends ReentrantContext implements MarlinCon * @return new RendererContext instance */ public static RendererContext createContext() { - return new RendererContext("ctx" - + Integer.toString(CTX_COUNT.getAndIncrement())); + return new RendererContext("ctx" + CTX_COUNT.getAndIncrement()); } // Smallest object used as Cleaner's parent reference @@ -83,8 +83,12 @@ public static RendererContext createContext() { public double clipInvScale = 0.0d; // CurveBasicMonotonizer instance public final CurveBasicMonotonizer monotonizer; + // flag indicating to force the stroker to process joins + public boolean isFirstSegment = true; // CurveClipSplitter instance final CurveClipSplitter curveClipSplitter; + // DPQS Sorter context + final DPQSSorterContext sorterCtx; // MarlinFX specific: // shared memory between renderer instances: @@ -97,13 +101,13 @@ public static RendererContext createContext() { // Array caches: /* clean int[] cache (zero-filled) = 5 refs */ - private final IntArrayCache cleanIntCache = new IntArrayCache(true, 5); + private final ArrayCacheIntClean cleanIntCache = new ArrayCacheIntClean(5); /* dirty int[] cache = 5 refs */ - private final IntArrayCache dirtyIntCache = new IntArrayCache(false, 5); + private final ArrayCacheInt dirtyIntCache = new ArrayCacheInt(5); /* dirty double[] cache = 4 refs (2 polystack) */ - private final DoubleArrayCache dirtyDoubleCache = new DoubleArrayCache(false, 4); + private final ArrayCacheDouble dirtyDoubleCache = new ArrayCacheDouble(4); /* dirty byte[] cache = 2 ref (2 polystack) */ - private final ByteArrayCache dirtyByteCache = new ByteArrayCache(false, 2); + private final ArrayCacheByte dirtyByteCache = new ArrayCacheByte(2); // RendererContext statistics final RendererStats stats; @@ -145,6 +149,8 @@ public static RendererContext createContext() { stroker = new Stroker(this); dasher = new Dasher(this); + + sorterCtx = (MergeSort.USE_DPQS) ? new DPQSSorterContext() : null; } /** @@ -162,6 +168,7 @@ public void dispose() { doClip = false; closedPath = false; clipInvScale = 0.0d; + isFirstSegment = true; // if context is maked as DIRTY: if (dirty) { @@ -208,19 +215,19 @@ OffHeapArray newOffHeapArray(final long initialSize) { return new OffHeapArray(cleanerObj, initialSize); } - IntArrayCache.Reference newCleanIntArrayRef(final int initialSize) { + ArrayCacheIntClean.Reference newCleanIntArrayRef(final int initialSize) { return cleanIntCache.createRef(initialSize); } - IntArrayCache.Reference newDirtyIntArrayRef(final int initialSize) { + ArrayCacheInt.Reference newDirtyIntArrayRef(final int initialSize) { return dirtyIntCache.createRef(initialSize); } - DoubleArrayCache.Reference newDirtyDoubleArrayRef(final int initialSize) { + ArrayCacheDouble.Reference newDirtyDoubleArrayRef(final int initialSize) { return dirtyDoubleCache.createRef(initialSize); } - ByteArrayCache.Reference newDirtyByteArrayRef(final int initialSize) { + ArrayCacheByte.Reference newDirtyByteArrayRef(final int initialSize) { return dirtyByteCache.createRef(initialSize); } @@ -230,25 +237,25 @@ static final class RendererSharedMemory { final OffHeapArray edges; // edgeBuckets ref (clean) - final IntArrayCache.Reference edgeBuckets_ref; + final ArrayCacheIntClean.Reference edgeBuckets_ref; // edgeBucketCounts ref (clean) - final IntArrayCache.Reference edgeBucketCounts_ref; + final ArrayCacheIntClean.Reference edgeBucketCounts_ref; // alphaLine ref (clean) - final IntArrayCache.Reference alphaLine_ref; + final ArrayCacheIntClean.Reference alphaLine_ref; // crossings ref (dirty) - final IntArrayCache.Reference crossings_ref; + final ArrayCacheInt.Reference crossings_ref; // edgePtrs ref (dirty) - final IntArrayCache.Reference edgePtrs_ref; - // merge sort initial arrays + final ArrayCacheInt.Reference edgePtrs_ref; + // merge sort initial arrays (large enough to satisfy most usages) (1024) // aux_crossings ref (dirty) - final IntArrayCache.Reference aux_crossings_ref; + final ArrayCacheInt.Reference aux_crossings_ref; // aux_edgePtrs ref (dirty) - final IntArrayCache.Reference aux_edgePtrs_ref; + final ArrayCacheInt.Reference aux_edgePtrs_ref; // blkFlags ref (clean) - final IntArrayCache.Reference blkFlags_ref; + final ArrayCacheIntClean.Reference blkFlags_ref; RendererSharedMemory(final RendererContext rdrCtx) { edges = rdrCtx.newOffHeapArray(INITIAL_EDGES_CAPACITY); // 96K diff --git a/src/main/java/com/sun/marlin/RendererNoAA.java b/src/main/java/com/sun/marlin/RendererNoAA.java index 3f008aa..e0408ff 100644 --- a/src/main/java/com/sun/marlin/RendererNoAA.java +++ b/src/main/java/com/sun/marlin/RendererNoAA.java @@ -122,14 +122,14 @@ public final class RendererNoAA implements MarlinRenderer, MarlinConst { private int activeEdgeMaxUsed; // crossings ref (dirty) - private final IntArrayCache.Reference crossings_ref; + private final ArrayCacheInt.Reference crossings_ref; // edgePtrs ref (dirty) - private final IntArrayCache.Reference edgePtrs_ref; + private final ArrayCacheInt.Reference edgePtrs_ref; // merge sort initial arrays (large enough to satisfy most usages) (1024) // aux_crossings ref (dirty) - private final IntArrayCache.Reference aux_crossings_ref; + private final ArrayCacheInt.Reference aux_crossings_ref; // aux_edgePtrs ref (dirty) - private final IntArrayCache.Reference aux_edgePtrs_ref; + private final ArrayCacheInt.Reference aux_edgePtrs_ref; ////////////////////////////////////////////////////////////////////////////// // EDGE LIST @@ -149,9 +149,9 @@ public final class RendererNoAA implements MarlinRenderer, MarlinConst { private int buckets_maxY; // edgeBuckets ref (clean) - private final IntArrayCache.Reference edgeBuckets_ref; + private final ArrayCacheIntClean.Reference edgeBuckets_ref; // edgeBucketCounts ref (clean) - private final IntArrayCache.Reference edgeBucketCounts_ref; + private final ArrayCacheIntClean.Reference edgeBucketCounts_ref; boolean useRLE = false; @@ -487,7 +487,7 @@ private void addLine(double x1, double y1, double x2, double y2) { private int[] alphaLine; // alphaLine ref (clean) - private final IntArrayCache.Reference alphaLine_ref; + private final ArrayCacheIntClean.Reference alphaLine_ref; private boolean enableBlkFlags = false; private boolean prevUseBlkFlags = false; @@ -496,7 +496,7 @@ private void addLine(double x1, double y1, double x2, double y2) { private int[] blkFlags; // blkFlags ref (clean) - private final IntArrayCache.Reference blkFlags_ref; + private final ArrayCacheIntClean.Reference blkFlags_ref; RendererNoAA(final RendererContext rdrCtx) { this.rdrCtx = rdrCtx; @@ -794,6 +794,8 @@ private void _endRendering(final int ymin, final int ymax, int lastY = -1; // last emited row + final DPQSSorterContext sorter = rdrCtx.sorterCtx; + boolean skipISort, useDPQS; // Iteration on scanlines for (; y < ymax; y++, bucket++) { @@ -806,7 +808,7 @@ private void _endRendering(final int ymin, final int ymax, // bucketCount indicates new edge / edge end: if (bucketcount != 0) { if (DO_STATS) { - rdrCtx.stats.stat_rdr_activeEdges_updates.add(numCrossings); + rdrCtx.stats.stat_rdr_activeEdges_updates.add(prevNumCrossings); } // last bit set to 1 means that edges ends @@ -815,7 +817,7 @@ private void _endRendering(final int ymin, final int ymax, // cache edges[] address + offset addr = addr0 + _OFF_YMAX; - for (i = 0, newCount = 0; i < numCrossings; i++) { + for (i = 0, newCount = 0; i < prevNumCrossings; i++) { // get the pointer to the edge ecur = _edgePtrs[i]; // random access so use unsafe: @@ -843,7 +845,7 @@ private void _endRendering(final int ymin, final int ymax, rdrCtx.stats.stat_array_renderer_edgePtrs.add(ptrEnd); } this.edgePtrs = _edgePtrs - = edgePtrs_ref.widenArray(_edgePtrs, numCrossings, + = edgePtrs_ref.widenArray(_edgePtrs, edgePtrsLen, // bad mark ? TODO: fix edge ptr mark ptrEnd); edgePtrsLen = _edgePtrs.length; @@ -912,7 +914,7 @@ private void _endRendering(final int ymin, final int ymax, * thresholds to switch to optimized merge sort * for newly added edges + final merge pass. */ - if ((ptrLen < 10) || (numCrossings < 40)) { + if (((numCrossings <= 40) || ((ptrLen <= 10) && (numCrossings <= MergeSort.DISABLE_ISORT_THRESHOLD)))) { if (DO_STATS) { rdrCtx.stats.hist_rdr_crossings.add(numCrossings); rdrCtx.stats.hist_rdr_crossings_adds.add(ptrLen); @@ -998,7 +1000,7 @@ private void _endRendering(final int ymin, final int ymax, } else { j = i - 1; _crossings[i] = _crossings[j]; - _edgePtrs[i] = _edgePtrs[j]; + _edgePtrs[i] = _edgePtrs[j]; while ((--j >= 0) && (_crossings[j] > cross)) { _crossings[j + 1] = _crossings[j]; @@ -1025,6 +1027,13 @@ private void _endRendering(final int ymin, final int ymax, // and perform insertion sort on almost sorted data // (ie i < prevNumCrossings): + skipISort = (prevNumCrossings >= MergeSort.DISABLE_ISORT_THRESHOLD); + useDPQS = MergeSort.USE_DPQS && (skipISort || (ptrLen >= MergeSort.DPQS_THRESHOLD)); + + if (DO_STATS && useDPQS) { + rdrCtx.stats.stat_rdr_crossings_dpqs.add((skipISort) ? numCrossings : ptrLen); + } + lastCross = _MIN_VALUE; for (i = 0; i < numCrossings; i++) { @@ -1061,39 +1070,58 @@ private void _endRendering(final int ymin, final int ymax, rdrCtx.stats.stat_rdr_crossings_updates.add(numCrossings); } - if (i >= prevNumCrossings) { - // simply store crossing as edgePtrs is in-place: - // will be copied and sorted efficiently by mergesort later: - _crossings[i] = cross; - - } else if (cross < lastCross) { - if (DO_STATS) { - rdrCtx.stats.stat_rdr_crossings_sorts.add(i); + if (skipISort) { + if (useDPQS) { + // simply store crossing as edgePtrs is in-place: + // will be sorted efficiently by DPQS later: + _crossings[i] = cross; + } else { + // store crossing/edgePtrs in auxiliary arrays: + // will be sorted efficiently by MergeSort later: + _aux_crossings[i] = cross; + _aux_edgePtrs [i] = ecur; } - - // (straight) insertion sort of crossings: - j = i - 1; - _aux_crossings[i] = _aux_crossings[j]; - _aux_edgePtrs[i] = _aux_edgePtrs[j]; - - while ((--j >= 0) && (_aux_crossings[j] > cross)) { - _aux_crossings[j + 1] = _aux_crossings[j]; - _aux_edgePtrs [j + 1] = _aux_edgePtrs[j]; + } else if (i >= prevNumCrossings) { + if (useDPQS) { + // store crossing/edgePtrs in auxiliary arrays: + // will be sorted efficiently by DPQS later: + _aux_crossings[i] = cross; + _aux_edgePtrs [i] = ecur; + } else { + // simply store crossing as edgePtrs is in-place: + // will be sorted efficiently by MergeSort later: + _crossings[i] = cross; } - _aux_crossings[j + 1] = cross; - _aux_edgePtrs [j + 1] = ecur; - } else { - // auxiliary storage: - _aux_crossings[i] = lastCross = cross; - _aux_edgePtrs [i] = ecur; + if (cross < lastCross) { + if (DO_STATS) { + rdrCtx.stats.stat_rdr_crossings_sorts.add(i); + } + // (straight) insertion sort of crossings: + j = i - 1; + _aux_crossings[i] = _aux_crossings[j]; + _aux_edgePtrs [i] = _aux_edgePtrs[j]; + + while ((--j >= 0) && (_aux_crossings[j] > cross)) { + _aux_crossings[j + 1] = _aux_crossings[j]; + _aux_edgePtrs [j + 1] = _aux_edgePtrs[j]; + } + _aux_crossings[j + 1] = cross; + _aux_edgePtrs [j + 1] = ecur; + } else { + // auxiliary storage: + _aux_crossings[i] = lastCross = cross; + _aux_edgePtrs [i] = ecur; + } } } // use Mergesort using auxiliary arrays (sort only right part) MergeSort.mergeSortNoCopy(_crossings, _edgePtrs, _aux_crossings, _aux_edgePtrs, - numCrossings, prevNumCrossings); + numCrossings, prevNumCrossings, + skipISort, sorter, useDPQS + ); } // reset ptrLen diff --git a/src/main/java/com/sun/marlin/RendererStats.java b/src/main/java/com/sun/marlin/RendererStats.java index f446928..5aa27ea 100644 --- a/src/main/java/com/sun/marlin/RendererStats.java +++ b/src/main/java/com/sun/marlin/RendererStats.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2021, 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 @@ -100,6 +100,8 @@ public static void dumpStats() { = new StatLong("renderer.crossings.bsearch"); final StatLong stat_rdr_crossings_msorts = new StatLong("renderer.crossings.msorts"); + final StatLong stat_rdr_crossings_dpqs + = new StatLong("renderer.crossings.dpqs"); final StatLong stat_str_polystack_curves = new StatLong("stroker.polystack.curves"); final StatLong stat_str_polystack_types @@ -195,6 +197,7 @@ public static void dumpStats() { stat_rdr_crossings_sorts, stat_rdr_crossings_bsearch, stat_rdr_crossings_msorts, + stat_rdr_crossings_dpqs, stat_str_polystack_types, stat_str_polystack_curves, stat_cpd_polystack_curves, @@ -333,7 +336,7 @@ void dump() { static final class RendererStatsHolder { // singleton - private static volatile RendererStatsHolder SINGLETON = null; + private static volatile RendererStatsHolder SINGLETON; static synchronized RendererStatsHolder getInstance() { if (SINGLETON == null) { @@ -353,6 +356,7 @@ static void dumpStats() { private final ConcurrentLinkedQueue allStats = new ConcurrentLinkedQueue(); + @SuppressWarnings("removal") private RendererStatsHolder() { AccessController.doPrivileged( (PrivilegedAction) () -> { diff --git a/src/main/java/com/sun/marlin/Stroker.java b/src/main/java/com/sun/marlin/Stroker.java index 5f14a09..71ccbd9 100644 --- a/src/main/java/com/sun/marlin/Stroker.java +++ b/src/main/java/com/sun/marlin/Stroker.java @@ -522,15 +522,9 @@ private void _moveTo(final double x0, final double y0, @Override public void lineTo(final double x1, final double y1) { - lineTo(x1, y1, false); - } - - private void lineTo(final double x1, final double y1, - final boolean force) - { final int outcode0 = this.cOutCode; - if (!force && clipRect != null) { + if (clipRect != null) { final int outcode1 = Helpers.outcode(x1, y1, clipRect); // Should clip @@ -616,18 +610,22 @@ public void closePath() { // basic acceptance criteria if ((sOutCode & cOutCode) == 0) { - if (cx0 != sx0 || cy0 != sy0) { - lineTo(sx0, sy0, true); + if ((cx0 != sx0) || (cy0 != sy0)) { + // may subdivide line: + lineTo(sx0, sy0); } - drawJoin(cdx, cdy, cx0, cy0, sdx, sdy, cmx, cmy, smx, smy, sOutCode); + // ignore starting point outside: + if (sOutCode == 0) { + drawJoin(cdx, cdy, cx0, cy0, sdx, sdy, cmx, cmy, smx, smy, sOutCode); - emitLineTo(sx0 + smx, sy0 + smy); + emitLineTo(sx0 + smx, sy0 + smy); - if (opened) { - emitLineTo(sx0 - smx, sy0 - smy); - } else { - emitMoveTo(sx0 - smx, sy0 - smy); + if (opened) { + emitLineTo(sx0 - smx, sy0 - smy); + } else { + emitMoveTo(sx0 - smx, sy0 - smy); + } } } // Ignore caps like finish(false) @@ -781,7 +779,8 @@ private void drawJoin(double pdx, double pdy, this.smx = mx; this.smy = my; } - } else { + } else if (rdrCtx.isFirstSegment) { + // Precision on isCW is causing instabilities with Dasher ! final boolean cw = isCW(pdx, pdy, dx, dy); if (outcode == 0) { if (joinStyle == JOIN_MITER) { @@ -792,23 +791,16 @@ private void drawJoin(double pdx, double pdy, } emitLineTo(x0, y0, !cw); } + if (!rdrCtx.isFirstSegment) { + // reset trigger to process further joins (normal operations) + rdrCtx.isFirstSegment = true; + } prev = DRAWING_OP_TO; } - private static boolean within(final double x1, final double y1, - final double x2, final double y2, - final double err) - { - assert err > 0 : ""; - // compare taxicab distance. ERR will always be small, so using - // true distance won't give much benefit - return (Helpers.within(x1, x2, err) && // we want to avoid calling Math.abs - Helpers.within(y1, y2, err)); // this is just as good. - } - - private void getLineOffsets(final double x1, final double y1, - final double x2, final double y2, - final double[] left, final double[] right) + private int getLineOffsets(final double x1, final double y1, + final double x2, final double y2, + final double[] left, final double[] right) { computeOffset(x2 - x1, y2 - y1, lineWidth2, offset0); final double mx = offset0[0]; @@ -822,6 +814,8 @@ private void getLineOffsets(final double x1, final double y1, right[1] = y1 - my; right[2] = x2 - mx; right[3] = y2 - my; + + return 4; } private int computeOffsetCubic(final double[] pts, final int off, @@ -835,24 +829,21 @@ private int computeOffsetCubic(final double[] pts, final int off, // the input curve at the cusp, and passes it to this function. // because of inaccuracies in the splitting, we consider points // equal if they're very close to each other. - final double x1 = pts[off ], y1 = pts[off + 1]; - final double x2 = pts[off + 2], y2 = pts[off + 3]; - final double x3 = pts[off + 4], y3 = pts[off + 5]; - final double x4 = pts[off + 6], y4 = pts[off + 7]; + final double x1 = pts[off ]; final double y1 = pts[off + 1]; + final double x2 = pts[off + 2]; final double y2 = pts[off + 3]; + final double x3 = pts[off + 4]; final double y3 = pts[off + 5]; + final double x4 = pts[off + 6]; final double y4 = pts[off + 7]; - double dx4 = x4 - x3; - double dy4 = y4 - y3; - double dx1 = x2 - x1; - double dy1 = y2 - y1; + double dx1 = x2 - x1; double dy1 = y2 - y1; + double dx4 = x4 - x3; double dy4 = y4 - y3; // if p1 == p2 && p3 == p4: draw line from p1->p4, unless p1 == p4, // in which case ignore if p1 == p2 - final boolean p1eqp2 = within(x1, y1, x2, y2, 6.0d * Math.ulp(y2)); - final boolean p3eqp4 = within(x3, y3, x4, y4, 6.0d * Math.ulp(y4)); + final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Math.ulp(y2)); + final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Math.ulp(y4)); if (p1eqp2 && p3eqp4) { - getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); - return 4; + return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); } else if (p1eqp2) { dx1 = x3 - x1; dy1 = y3 - y1; @@ -864,11 +855,11 @@ private int computeOffsetCubic(final double[] pts, final int off, // if p2-p1 and p4-p3 are parallel, that must mean this curve is a line double dotsq = (dx1 * dx4 + dy1 * dy4); dotsq *= dotsq; - double l1sq = dx1 * dx1 + dy1 * dy1, l4sq = dx4 * dx4 + dy4 * dy4; + final double l1sq = dx1 * dx1 + dy1 * dy1; + final double l4sq = dx4 * dx4 + dy4 * dy4; if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Math.ulp(dotsq))) { - getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); - return 4; + return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); } // What we're trying to do in this function is to approximate an ideal @@ -918,11 +909,12 @@ private int computeOffsetCubic(final double[] pts, final int off, // getting the inverse of the matrix above. Then we use [c1,c2] to compute // p2p and p3p. - double x = (x1 + 3.0d * (x2 + x3) + x4) / 8.0d; - double y = (y1 + 3.0d * (y2 + y3) + y4) / 8.0d; + final double xm = (x1 + x4 + 3.0d * (x2 + x3)) / 8.0d; + final double ym = (y1 + y4 + 3.0d * (y2 + y3)) / 8.0d; // (dxm,dym) is some tangent of B at t=0.5. This means it's equal to // c*B'(0.5) for some constant c. - double dxm = x3 + x4 - x1 - x2, dym = y3 + y4 - y1 - y2; + final double dxm = x3 + x4 - (x1 + x2); + final double dym = y3 + y4 - (y1 + y2); // this computes the offsets at t=0, 0.5, 1, using the property that // for any bezier curve the vectors p2-p1 and p4-p3 are parallel to @@ -930,49 +922,89 @@ private int computeOffsetCubic(final double[] pts, final int off, computeOffset(dx1, dy1, lineWidth2, offset0); computeOffset(dxm, dym, lineWidth2, offset1); computeOffset(dx4, dy4, lineWidth2, offset2); + + // left side: double x1p = x1 + offset0[0]; // start double y1p = y1 + offset0[1]; // point - double xi = x + offset1[0]; // interpolation - double yi = y + offset1[1]; // point + double xi = xm + offset1[0]; // interpolation + double yi = ym + offset1[1]; // point double x4p = x4 + offset2[0]; // end double y4p = y4 + offset2[1]; // point - double invdet43 = 4.0d / (3.0d * (dx1 * dy4 - dy1 * dx4)); + final double invdet43 = 4.0d / (3.0d * (dx1 * dy4 - dy1 * dx4)); + + double two_pi_m_p1_m_p4x = 2.0d * xi - (x1p + x4p); + double two_pi_m_p1_m_p4y = 2.0d * yi - (y1p + y4p); - double two_pi_m_p1_m_p4x = 2.0d * xi - x1p - x4p; - double two_pi_m_p1_m_p4y = 2.0d * yi - y1p - y4p; double c1 = invdet43 * (dy4 * two_pi_m_p1_m_p4x - dx4 * two_pi_m_p1_m_p4y); double c2 = invdet43 * (dx1 * two_pi_m_p1_m_p4y - dy1 * two_pi_m_p1_m_p4x); double x2p, y2p, x3p, y3p; - x2p = x1p + c1*dx1; - y2p = y1p + c1*dy1; - x3p = x4p + c2*dx4; - y3p = y4p + c2*dy4; + + if (c1 * c2 > 0.0) { +// System.out.println("Buggy solver (left): c1 = " + c1 + " c2 = " + c2); + + // use lower quality approximation but good enough + // to ensure cuve being in its convex hull + x2p = x2 + offset1[0]; // 2nd + y2p = y2 + offset1[1]; // point + x3p = x3 + offset1[0]; // 3nd + y3p = y3 + offset1[1]; // point + + safeComputeMiter(x1p, y1p, x1p+dx1, y1p+dy1, x2p, y2p, x2p-dxm, y2p-dym, leftOff); + x2p = leftOff[2]; y2p = leftOff[3]; + + safeComputeMiter(x4p, y4p, x4p+dx4, y4p+dy4, x3p, y3p, x3p-dxm, y3p-dym, leftOff); + x3p = leftOff[2]; y3p = leftOff[3]; + } else { + x2p = x1p + c1 * dx1; y2p = y1p + c1 * dy1; + x3p = x4p + c2 * dx4; y3p = y4p + c2 * dy4; + } leftOff[0] = x1p; leftOff[1] = y1p; leftOff[2] = x2p; leftOff[3] = y2p; leftOff[4] = x3p; leftOff[5] = y3p; leftOff[6] = x4p; leftOff[7] = y4p; - x1p = x1 - offset0[0]; y1p = y1 - offset0[1]; - xi = xi - 2.0d * offset1[0]; yi = yi - 2.0d * offset1[1]; - x4p = x4 - offset2[0]; y4p = y4 - offset2[1]; + // Right side: + x1p = x1 - offset0[0]; // start + y1p = y1 - offset0[1]; // point + xi = xm - offset1[0]; // interpolation + yi = ym - offset1[1]; // point + x4p = x4 - offset2[0]; // end + y4p = y4 - offset2[1]; // point + + two_pi_m_p1_m_p4x = 2.0d * xi - (x1p + x4p); + two_pi_m_p1_m_p4y = 2.0d * yi - (y1p + y4p); - two_pi_m_p1_m_p4x = 2.0d * xi - x1p - x4p; - two_pi_m_p1_m_p4y = 2.0d * yi - y1p - y4p; c1 = invdet43 * (dy4 * two_pi_m_p1_m_p4x - dx4 * two_pi_m_p1_m_p4y); c2 = invdet43 * (dx1 * two_pi_m_p1_m_p4y - dy1 * two_pi_m_p1_m_p4x); - x2p = x1p + c1*dx1; - y2p = y1p + c1*dy1; - x3p = x4p + c2*dx4; - y3p = y4p + c2*dy4; + if (c1 * c2 > 0.0) { +// System.out.println("Buggy solver (right): c1 = " + c1 + " c2 = " + c2); + + // use lower quality approximation but good enough + // to ensure cuve being in its convex hull + x2p = x2 - offset1[0]; // 2nd + y2p = y2 - offset1[1]; // point + x3p = x3 - offset1[0]; // 3nd + y3p = y3 - offset1[1]; // point + + safeComputeMiter(x1p, y1p, x1p+dx1, y1p+dy1, x2p, y2p, x2p-dxm, y2p-dym, rightOff); + x2p = rightOff[2]; y2p = rightOff[3]; + + safeComputeMiter(x4p, y4p, x4p+dx4, y4p+dy4, x3p, y3p, x3p-dxm, y3p-dym, rightOff); + x3p = rightOff[2]; y3p = rightOff[3]; + } else { + x2p = x1p + c1 * dx1; y2p = y1p + c1 * dy1; + x3p = x4p + c2 * dx4; y3p = y4p + c2 * dy4; + } rightOff[0] = x1p; rightOff[1] = y1p; rightOff[2] = x2p; rightOff[3] = y2p; rightOff[4] = x3p; rightOff[5] = y3p; rightOff[6] = x4p; rightOff[7] = y4p; + return 8; } @@ -983,16 +1015,14 @@ private int computeOffsetQuad(final double[] pts, final int off, final double[] leftOff, final double[] rightOff) { - final double x1 = pts[off ], y1 = pts[off + 1]; - final double x2 = pts[off + 2], y2 = pts[off + 3]; - final double x3 = pts[off + 4], y3 = pts[off + 5]; + final double x1 = pts[off ]; final double y1 = pts[off + 1]; + final double x2 = pts[off + 2]; final double y2 = pts[off + 3]; + final double x3 = pts[off + 4]; final double y3 = pts[off + 5]; - final double dx3 = x3 - x2; - final double dy3 = y3 - y2; - final double dx1 = x2 - x1; - final double dy1 = y2 - y1; + final double dx12 = x2 - x1; final double dy12 = y2 - y1; + final double dx23 = x3 - x2; final double dy23 = y3 - y2; - // if p1=p2 or p3=p4 it means that the derivative at the endpoint + // if p1=p2 or p2=p3 it means that the derivative at the endpoint // vanishes, which creates problems with computeOffset. Usually // this happens when this stroker object is trying to widen // a curve with a cusp. What happens is that curveTo splits @@ -1000,45 +1030,48 @@ private int computeOffsetQuad(final double[] pts, final int off, // because of inaccuracies in the splitting, we consider points // equal if they're very close to each other. - // if p1 == p2 && p3 == p4: draw line from p1->p4, unless p1 == p4, - // in which case ignore. - final boolean p1eqp2 = within(x1, y1, x2, y2, 6.0d * Math.ulp(y2)); - final boolean p2eqp3 = within(x2, y2, x3, y3, 6.0d * Math.ulp(y3)); + // if p1 == p2 or p2 == p3: draw line from p1->p3 + final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Math.ulp(y2)); + final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Math.ulp(y3)); if (p1eqp2 || p2eqp3) { - getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); - return 4; + return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); } - // if p2-p1 and p4-p3 are parallel, that must mean this curve is a line - double dotsq = (dx1 * dx3 + dy1 * dy3); + // if p2-p1 and p3-p2 are parallel, that must mean this curve is a line + double dotsq = (dx12 * dx23 + dy12 * dy23); dotsq *= dotsq; - double l1sq = dx1 * dx1 + dy1 * dy1, l3sq = dx3 * dx3 + dy3 * dy3; + final double l1sq = dx12 * dx12 + dy12 * dy12; + final double l3sq = dx23 * dx23 + dy23 * dy23; if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Math.ulp(dotsq))) { - getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); - return 4; + return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); } // this computes the offsets at t=0, 0.5, 1, using the property that - // for any bezier curve the vectors p2-p1 and p4-p3 are parallel to + // for any bezier curve the vectors p2-p1 and p3-p2 are parallel to // the (dx/dt, dy/dt) vectors at the endpoints. - computeOffset(dx1, dy1, lineWidth2, offset0); - computeOffset(dx3, dy3, lineWidth2, offset1); + computeOffset(dx12, dy12, lineWidth2, offset0); + computeOffset(dx23, dy23, lineWidth2, offset1); double x1p = x1 + offset0[0]; // start double y1p = y1 + offset0[1]; // point double x3p = x3 + offset1[0]; // end double y3p = y3 + offset1[1]; // point - safeComputeMiter(x1p, y1p, x1p+dx1, y1p+dy1, x3p, y3p, x3p-dx3, y3p-dy3, leftOff); + + safeComputeMiter(x1p, y1p, x1p+dx12, y1p+dy12, x3p, y3p, x3p-dx23, y3p-dy23, leftOff); leftOff[0] = x1p; leftOff[1] = y1p; leftOff[4] = x3p; leftOff[5] = y3p; - x1p = x1 - offset0[0]; y1p = y1 - offset0[1]; - x3p = x3 - offset1[0]; y3p = y3 - offset1[1]; - safeComputeMiter(x1p, y1p, x1p+dx1, y1p+dy1, x3p, y3p, x3p-dx3, y3p-dy3, rightOff); + x1p = x1 - offset0[0]; // start + y1p = y1 - offset0[1]; // point + x3p = x3 - offset1[0]; // end + y3p = y3 - offset1[1]; // point + + safeComputeMiter(x1p, y1p, x1p+dx12, y1p+dy12, x3p, y3p, x3p-dx23, y3p-dy23, rightOff); rightOff[0] = x1p; rightOff[1] = y1p; rightOff[4] = x3p; rightOff[5] = y3p; + return 6; } diff --git a/src/main/java/com/sun/marlin/TransformingPathConsumer2D.java b/src/main/java/com/sun/marlin/TransformingPathConsumer2D.java index da22849..4b07dda 100644 --- a/src/main/java/com/sun/marlin/TransformingPathConsumer2D.java +++ b/src/main/java/com/sun/marlin/TransformingPathConsumer2D.java @@ -494,8 +494,16 @@ public void quadTo(double x2, double y2, double x1, double y1) { static final class PathClipFilter implements DPathConsumer2D { + private static final boolean TRACE = false; + + private static final int MOVE_TO = 0; + private static final int DRAWING_OP_TO = 1; // ie. curve, line, or quad + private static final int CLOSE = 2; + private DPathConsumer2D out; + private int prev; + // Bounds of the drawing region, at pixel precision. private final double[] clipRect; @@ -507,6 +515,9 @@ static final class PathClipFilter implements DPathConsumer2D { // the current outcode of the current sub path private int cOutCode = 0; + // the outcode of the starting point + private int sOutCode = 0; + // the cumulated (and) outcode of the complete path private int gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; @@ -515,12 +526,9 @@ static final class PathClipFilter implements DPathConsumer2D { // The starting point of the path private double sx0, sy0; - // The current point (TODO stupid repeated info) + // The current point private double cx0, cy0; - // The current point OUTSIDE - private double cox0, coy0; - private boolean subdivide = MarlinConst.DO_CLIP_SUBDIVIDER; private final CurveClipSplitter curveSplitter; @@ -546,6 +554,7 @@ PathClipFilter init(final DPathConsumer2D out) { this.init_corners = true; this.gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; + this.prev = CLOSE; return this; // fluent API } @@ -559,14 +568,12 @@ void dispose() { } private void finishPath() { - if (outside) { - // criteria: inside or totally outside ? - if (gOutCode == 0) { - finish(); - } else { - this.outside = false; - stack.reset(); - } + // criteria: inside or totally outside ? + if (gOutCode == 0) { + finish(); + } else { + this.outside = false; + stack.reset(); } } @@ -592,19 +599,25 @@ private void finish() { _corners[6] = _clipRect[3]; _corners[7] = _clipRect[1]; } - stack.pullAll(corners, out); + stack.pullAll(corners, out, (prev == MOVE_TO)); + prev = DRAWING_OP_TO; } - out.lineTo(cox0, coy0); - this.cx0 = cox0; - this.cy0 = coy0; } @Override public void pathDone() { - finishPath(); + if (TRACE) { + MarlinUtils.logInfo("PathDone(" + sx0 + ", " + sy0 + ") prev: " + prev); + } + _closePath(); + // note: renderer's pathDone() must handle missing moveTo() if outside out.pathDone(); + // this shouldn't matter since this object won't be used + // after the call to this method. + this.prev = CLOSE; + // TODO: fix possible leak if exception happened // Dispose this instance: dispose(); @@ -612,27 +625,68 @@ public void pathDone() { @Override public void closePath() { - finishPath(); + if (TRACE) { + MarlinUtils.logInfo("ClosePath(" + sx0 + ", " + sy0 + ") prev: " + prev); + } + _closePath(); - out.closePath(); + if (prev == DRAWING_OP_TO) { + out.closePath(); + } + + // if outside, moveTo is needed + if (sOutCode != 0) { + this.prev = MOVE_TO; + } else { + this.prev = CLOSE; + } // back to starting point: - this.cOutCode = Helpers.outcode(sx0, sy0, clipRect); + this.cOutCode = sOutCode; this.cx0 = sx0; this.cy0 = sy0; } + + private void _closePath() { + // preserve outside flag for the lineTo call below + final boolean prevOutside = outside; + if (prevOutside) { + finishPath(); + } + + if (prev == DRAWING_OP_TO) { + // Should clip + final int orCode = (cOutCode | sOutCode); + if (orCode != 0) { + if ((cx0 != sx0) || (cy0 != sy0)) { + // restore outside flag before lineTo: + this.outside = prevOutside; + // may subdivide line: + lineTo(sx0, sy0); + // finish if outside caused by lineTo: + if (outside) { + finishPath(); + } + } + } + } + } @Override public void moveTo(final double x0, final double y0) { - finishPath(); + if (TRACE) { + MarlinUtils.logInfo("MoveTo(" + x0 + ", " + y0 + ") prev: " + prev); + } + _closePath(); - out.moveTo(x0, y0); + this.prev = MOVE_TO; // update starting point: - this.cOutCode = Helpers.outcode(x0, y0, clipRect); + final int outcode = Helpers.outcode(x0, y0, clipRect); + this.cOutCode = outcode; + this.sOutCode = outcode; this.cx0 = x0; this.cy0 = y0; - this.sx0 = x0; this.sy0 = y0; } @@ -642,6 +696,14 @@ public void lineTo(final double xe, final double ye) { final int outcode0 = this.cOutCode; final int outcode1 = Helpers.outcode(xe, ye, clipRect); + if (TRACE) { + if (subdivide) { + MarlinUtils.logInfo("----------------------"); + } + MarlinUtils.logInfo("LineTo c (" + cx0 + ", " + cy0 + ") outcode: " + outcode0); + MarlinUtils.logInfo("LineTo (" + xe + ", " + ye + ") outcode: " + outcode1 + " outside: " + outside); + } + // Should clip final int orCode = (outcode0 | outcode1); if (orCode != 0) { @@ -655,13 +717,8 @@ public void lineTo(final double xe, final double ye) { subdivide = false; boolean ret; // subdivide curve => callback with subdivided parts: - if (outside) { - ret = curveSplitter.splitLine(cox0, coy0, xe, ye, - orCode, this); - } else { - ret = curveSplitter.splitLine(cx0, cy0, xe, ye, - orCode, this); - } + ret = curveSplitter.splitLine(cx0, cy0, xe, ye, + orCode, this); // reentrance is done: subdivide = true; if (ret) { @@ -674,8 +731,12 @@ public void lineTo(final double xe, final double ye) { this.gOutCode &= sideCode; // keep last point coordinate before entering the clip again: this.outside = true; - this.cox0 = xe; - this.coy0 = ye; + this.cx0 = xe; + this.cy0 = ye; + + if (TRACE) { + MarlinUtils.logInfo("skipped: (" + cx0 + ", " + cy0 + ")"); + } clip(sideCode, outcode0, outcode1); return; @@ -687,11 +748,33 @@ public void lineTo(final double xe, final double ye) { if (outside) { finish(); + + // emit last point outside before entering again... + if (outcode0 != 0) { + if (TRACE) { + MarlinUtils.logInfo("add last point outside: (" + cx0 + ", " + cy0 + ")"); + } + if (prev == MOVE_TO) { + out.moveTo(cx0, cy0); + } else { + out.lineTo(cx0, cy0); + } + prev = DRAWING_OP_TO; + } } // clipping disabled: + if (prev == MOVE_TO) { + out.moveTo(cx0, cy0); + } + prev = DRAWING_OP_TO; + out.lineTo(xe, ye); this.cx0 = xe; this.cy0 = ye; + + if (TRACE && subdivide) { + MarlinUtils.logInfo("----------------------"); + } } private void clip(final int sideCode, @@ -741,6 +824,14 @@ public void curveTo(final double x1, final double y1, final int outcode2 = Helpers.outcode(x2, y2, clipRect); final int outcode3 = Helpers.outcode(xe, ye, clipRect); + if (TRACE) { + if (subdivide) { + MarlinUtils.logInfo("----------------------"); + } + MarlinUtils.logInfo("CurveTo c (" + cx0 + ", " + cy0 + ") outcode: " + outcode0); + MarlinUtils.logInfo("CurveTo (" + xe + ", " + ye + ") outcode: " + outcode3 + " outside: " + outside); + } + // Should clip final int orCode = (outcode0 | outcode1 | outcode2 | outcode3); if (orCode != 0) { @@ -754,15 +845,9 @@ public void curveTo(final double x1, final double y1, subdivide = false; // subdivide curve => callback with subdivided parts: boolean ret; - if (outside) { - ret = curveSplitter.splitCurve(cox0, coy0, x1, y1, - x2, y2, xe, ye, - orCode, this); - } else { - ret = curveSplitter.splitCurve(cx0, cy0, x1, y1, - x2, y2, xe, ye, - orCode, this); - } + ret = curveSplitter.splitCurve(cx0, cy0, x1, y1, + x2, y2, xe, ye, + orCode, this); // reentrance is done: subdivide = true; if (ret) { @@ -775,8 +860,12 @@ public void curveTo(final double x1, final double y1, this.gOutCode &= sideCode; // keep last point coordinate before entering the clip again: this.outside = true; - this.cox0 = xe; - this.coy0 = ye; + this.cx0 = xe; + this.cy0 = ye; + + if (TRACE) { + MarlinUtils.logInfo("skipped: (" + cx0 + ", " + cy0 + ")"); + } clip(sideCode, outcode0, outcode3); return; @@ -788,11 +877,33 @@ public void curveTo(final double x1, final double y1, if (outside) { finish(); + + // emit last point outside before entering again... + if (outcode0 != 0) { + if (TRACE) { + MarlinUtils.logInfo("add last point outside: (" + cx0 + ", " + cy0 + ")"); + } + if (prev == MOVE_TO) { + out.moveTo(cx0, cy0); + } else { + out.lineTo(cx0, cy0); + } + prev = DRAWING_OP_TO; + } } // clipping disabled: + if (prev == MOVE_TO) { + out.moveTo(cx0, cy0); + } + prev = DRAWING_OP_TO; + out.curveTo(x1, y1, x2, y2, xe, ye); this.cx0 = xe; this.cy0 = ye; + + if (TRACE && subdivide) { + MarlinUtils.logInfo("----------------------"); + } } @Override @@ -803,6 +914,14 @@ public void quadTo(final double x1, final double y1, final int outcode1 = Helpers.outcode(x1, y1, clipRect); final int outcode2 = Helpers.outcode(xe, ye, clipRect); + if (TRACE) { + if (subdivide) { + MarlinUtils.logInfo("----------------------"); + } + MarlinUtils.logInfo("QuadTo c (" + cx0 + ", " + cy0 + ") outcode: " + outcode0); + MarlinUtils.logInfo("QuadTo (" + xe + ", " + ye + ") outcode: " + outcode1 + " outside: " + outside); + } + // Should clip final int orCode = (outcode0 | outcode1 | outcode2); if (orCode != 0) { @@ -816,13 +935,8 @@ public void quadTo(final double x1, final double y1, subdivide = false; // subdivide curve => callback with subdivided parts: boolean ret; - if (outside) { - ret = curveSplitter.splitQuad(cox0, coy0, x1, y1, - xe, ye, orCode, this); - } else { - ret = curveSplitter.splitQuad(cx0, cy0, x1, y1, - xe, ye, orCode, this); - } + ret = curveSplitter.splitQuad(cx0, cy0, x1, y1, + xe, ye, orCode, this); // reentrance is done: subdivide = true; if (ret) { @@ -835,8 +949,8 @@ public void quadTo(final double x1, final double y1, this.gOutCode &= sideCode; // keep last point coordinate before entering the clip again: this.outside = true; - this.cox0 = xe; - this.coy0 = ye; + this.cx0 = xe; + this.cy0 = ye; clip(sideCode, outcode0, outcode2); return; @@ -848,11 +962,33 @@ public void quadTo(final double x1, final double y1, if (outside) { finish(); + + // emit last point outside before entering again... + if (outcode0 != 0) { + if (TRACE) { + MarlinUtils.logInfo("add last point outside: (" + cx0 + ", " + cy0 + ")"); + } + if (prev == MOVE_TO) { + out.moveTo(cx0, cy0); + } else { + out.lineTo(cx0, cy0); + } + prev = DRAWING_OP_TO; + } } // clipping disabled: + if (prev == MOVE_TO) { + out.moveTo(cx0, cy0); + } + prev = DRAWING_OP_TO; + out.quadTo(x1, y1, xe, ye); this.cx0 = xe; this.cy0 = ye; + + if (TRACE && subdivide) { + MarlinUtils.logInfo("----------------------"); + } } } diff --git a/src/main/java/com/sun/marlin/Version.java b/src/main/java/com/sun/marlin/Version.java index 0b519a7..c501e53 100644 --- a/src/main/java/com/sun/marlin/Version.java +++ b/src/main/java/com/sun/marlin/Version.java @@ -27,7 +27,7 @@ public final class Version { - private static final String VERSION = "marlinFX-0.9.3.2-Unsafe-OpenJDK"; + private static final String VERSION = "marlin-0.9.4.5-Unsafe-OpenJFX"; public static String getVersion() { return VERSION; diff --git a/src/main/java/com/sun/marlin/conv.sh b/src/main/java/com/sun/marlin/conv.sh deleted file mode 100644 index 94776e2..0000000 --- a/src/main/java/com/sun/marlin/conv.sh +++ /dev/null @@ -1,29 +0,0 @@ -FILES="CollinearSimplifier Curve Renderer RendererNoAA Stroker TransformingPathConsumer2D" -for f in $FILES -do - echo "Processing $f" - sed -e "s/$f/D$f/g" -e "s/\"D$f/\"$f/g" -e 's/import com.sun.javafx.geom.PathConsumer2D;//g' -e 's/PathConsumer2D/DPathConsumer2D/g' -e 's/DTransformingDPathConsumer2D/DTransformingPathConsumer2D/g' -e 's/(float) //g' -e 's/float/double/g' -e 's/Float/Double/g' -e 's/DoubleMath/FloatMath/g' -e 's/\([0-9]*\.\?[0-9]\+\)f/\1d/g' -e 's/ Curve/ DCurve/g' -e 's/Helpers/DHelpers/g' -e 's/MarlinRenderer/DMarlinRenderer/g' -e 's/RendererContext/DRendererContext/g' -e "s/DD$f/D$f/g" -e 's/MarlinDRenderer/DMarlinRenderer/g' -e 's/doubleing/floating/g' < $f.java > D$f.java -done - -echo "Processing Renderers (final)" -FILES="Renderer RendererNoAA" -for f in $FILES -do - echo "Processing D$f" - mv D$f.java D$f.java.orig - sed -e "s/x1d/x1/g" -e "s/y1d/y1/g" < D$f.java.orig > D$f.java - rm D$f.java.orig -done - - -# Dasher (convert type) -echo "Processing Dasher" -sed -e 's/Dasher/DDasher/g' -e 's/import com.sun.javafx.geom.PathConsumer2D;//g' -e 's/PathConsumer2D/DPathConsumer2D/g' -e 's/DTransformingDPathConsumer2D/DTransformingPathConsumer2D/g' -e 's/(float) //g' -e 's/float/double/g' -e 's/Float/Double/g' -e 's/DoubleMath/FloatMath/g' -e 's/\([0-9]*\.\?[0-9]\+\)f/\1d/g' -e 's/ Curve/ DCurve/g' -e 's/Helpers/DHelpers/g' -e 's/MarlinRenderer/DMarlinRenderer/g' -e 's/RendererContext/DRendererContext/g' -e 's/MarlinDRenderer/DMarlinRenderer/g' -e 's/copyDashArray(final double\[\] dashes)/copyDashArray(final float\[\] dashes)/g' -e 's/System.arraycopy(dashes, 0, newDashes, 0, len);/for \(int i = 0; i < len; i\+\+\) \{ newDashes\[i\] = dashes\[i\]; \}/g'< Dasher.java > DDasher.java - -echo "Processing Helpers" -sed -e 's/import com.sun.javafx.geom.PathConsumer2D;//g' -e 's/PathConsumer2D/DPathConsumer2D/g' -e 's/DTransformingDPathConsumer2D/DTransformingPathConsumer2D/g' -e 's/(float) //g' -e 's/float/double/g' -e 's/Float/Double/g' -e 's/DoubleMath/FloatMath/g' -e 's/\([0-9]*\.\?[0-9]\+\)f/\1d/g' -e 's/ Curve/ DCurve/g' -e 's/Helpers/DHelpers/g' -e 's/MarlinRenderer/DMarlinRenderer/g' -e 's/RendererContext/DRendererContext/g' -e 's/MarlinDRenderer/DMarlinRenderer/g' < Helpers.java > DHelpers.java - -# [( -+]+[0-9]+f matches all integer float like 0f 123f (missing .0) -# [0-9]+f matches all floats without .xx -# \.[0-9]+[ /\*\+\-\(\)] matches all missing double of float suffix - diff --git a/src/main/java/com/sun/marlin/makeIntFloatClasses.sh b/src/main/java/com/sun/marlin/makeIntFloatClasses.sh deleted file mode 100644 index 4c7f59f..0000000 --- a/src/main/java/com/sun/marlin/makeIntFloatClasses.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -# Replicate changes from the ByteArrayCache class to the [Int/Float/Double]ArrayCache classes - -sed -e 's/(b\yte)[ ]*//g' -e 's/b\yte/int/g' -e 's/B\yte/Int/g' < B\yteArrayCache.java > IntArrayCache.java -sed -e 's/(b\yte)[ ]*0/0.0f/g' -e 's/(b\yte)[ ]*/(float) /g' -e 's/b\yte/float/g' -e 's/B\yte/Float/g' < B\yteArrayCache.java > FloatArrayCache.java -sed -e 's/(b\yte)[ ]*0/0.0d/g' -e 's/(b\yte)[ ]*/(double) /g' -e 's/b\yte/double/g' -e 's/B\yte/Double/g' < B\yteArrayCache.java > DoubleArrayCache.java - diff --git a/src/main/java/test/BigLeftSide.java b/src/main/java/test/BigLeftSide.java new file mode 100644 index 0000000..4199bdd --- /dev/null +++ b/src/main/java/test/BigLeftSide.java @@ -0,0 +1,120 @@ +package test; + +import java.util.Arrays; + +import javafx.animation.AnimationTimer; +import javafx.application.Application; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; +import javafx.stage.Stage; + +public class BigLeftSide extends Application { + public static final int NUM_SHAPES = 100; + public static final int PATH_BORDER = 20; + public static final int PATH_SIZE = 400; + public static final int SHIFT_SIZE = 20; + public static final long[] FRAME_TIMES = new long[60]; + public static final double NANO_SCALE = 1000.0 * 1000.0 * 1000.0; + public static final double FRAME_AVG_SCALE = FRAME_TIMES.length * NANO_SCALE; + + @Override + public void start(Stage stage) { + int CV_DIM = PATH_BORDER + PATH_SIZE + SHIFT_SIZE + PATH_BORDER; + Canvas cv = new Canvas(CV_DIM, CV_DIM); + GraphicsContext gc = cv.getGraphicsContext2D(); + renderFrame(gc); + + Scene scene = new Scene(new Group(cv)); + stage.setScene(scene); + stage.show(); + + AnimationTimer timer = new AnimationTimer() { + long nsStart; + long nsAverageTotal; + int delay = 20; + + @Override + public void handle(long nsNow) { + if (nsStart > 0) { + long nsFrame = (nsNow - nsStart); + if (delay > 0) { + delay--; + System.out.printf("Warming up with %f fps\n", NANO_SCALE / nsFrame); + } else { + if (nsAverageTotal == 0) { + Arrays.fill(FRAME_TIMES, nsFrame); + nsAverageTotal = nsFrame * FRAME_TIMES.length; + } else { + nsAverageTotal -= FRAME_TIMES[0]; + System.arraycopy(FRAME_TIMES, 1, FRAME_TIMES, 0, FRAME_TIMES.length - 1); + FRAME_TIMES[FRAME_TIMES.length-1] = nsFrame; + nsAverageTotal += nsFrame; + } + System.out.printf("average frame rate = %f fps\n", + (FRAME_AVG_SCALE / nsAverageTotal)); + } + } + nsStart = nsNow; + renderFrame(gc); + } + }; + timer.start(); + } + + void renderFrame(GraphicsContext gc) { + int l1 = NUM_SHAPES / 4; + int l2 = NUM_SHAPES / 2; + int l3 = NUM_SHAPES * 3 / 4; + int l4 = NUM_SHAPES; + double s1 = l1; + double s2 = l2 - l1; + double s3 = l3 - l2; + double s4 = l4 - l3; + for (int i = 0; i < NUM_SHAPES; i++) { + gc.save(); + gc.setFill(new Color(Math.random(), Math.random(), Math.random(), 1.0)); + double x, y; + if (i < l1) { + x = i / s1; + y = 0.0; + } else if (i < l2) { + x = 1.0; + y = (i - l1) / s2; + } else if (i < l3) { + x = (l3 - i) / s3; + y = 1.0; + } else { + x = 0.0; + y = (l4 - i) / s4; + } + gc.translate(PATH_BORDER + x * SHIFT_SIZE, + PATH_BORDER + y * SHIFT_SIZE); + gc.beginPath(); + gc.moveTo(0.0, 0.0); + gc.lineTo(1.0, 0.0); + gc.lineTo(1.0, 1.0); + gc.lineTo(0.0, 1.0); + gc.closePath(); + gc.moveTo(PATH_SIZE, 0.0); + gc.lineTo(PATH_SIZE, PATH_SIZE); + gc.lineTo(PATH_SIZE - PATH_BORDER, PATH_SIZE); + gc.lineTo(PATH_SIZE - PATH_BORDER, 0.0); + gc.closePath(); + gc.fill(); + gc.restore(); + } + } + + /** + * Java main for when running without JavaFX launcher + */ + public static void main(String[] args) { + + System.setProperty("prism.verbose", "true"); + + launch(args); + } +} diff --git a/src/main/java/test/PathApp.java b/src/main/java/test/PathApp.java new file mode 100644 index 0000000..5fc097a --- /dev/null +++ b/src/main/java/test/PathApp.java @@ -0,0 +1,140 @@ +package test; + +import java.util.List; +import javafx.application.Application; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.scene.shape.ArcTo; +import javafx.scene.shape.ClosePath; +import javafx.scene.shape.CubicCurveTo; +import javafx.scene.shape.HLineTo; +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.VLineTo; +import javafx.stage.Stage; + +/** + * A sample that demonstrates two path shapes. + */ +public class PathApp extends Application { + + public Parent createContent() { + Pane root = new Pane(); + root.setPrefSize(505, 300); + root.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + root.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + // Create path shape - square + final Path path1 = new Path(); + path1.getElements().addAll( + new MoveTo(25, 25), + new HLineTo(65), + new VLineTo(65), + new LineTo(25, 65), + new ClosePath()); + path1.setFill(null); + path1.setStroke(Color.RED); + path1.setStrokeWidth(2); + + // Create path shape - curves + final Path path2 = new Path(); + path2.getElements().addAll( + new MoveTo(100, 45), + new CubicCurveTo(120, 20, 130, 80, 140, 45), + new QuadCurveTo(150, 0, 160, 45), + new ArcTo(20, 40, 0, 180, 45, true, true)); + path2.setFill(null); + path2.setStroke(Color.DODGERBLUE); + path2.setStrokeWidth(2); + path2.setTranslateY(36); + + // Create path shape - curves + final Path path3 = new Path(); + path3.getElements().addAll( + new MoveTo(100, 45), + new CubicCurveTo(120, 20, 130, 80, 140, 45), + new QuadCurveTo(150, 0, 160, 45), + new ArcTo(20, 40, 0, 180, 45, true, true)); + path3.setFill(Color.ORANGE); + path3.setStroke(null); + path3.setTranslateY(136); + + final Path path4 = new Path(); + path4.setFill(null); + path4.setStroke(Color.GREEN); + path4.setStrokeWidth(2); + + final List pe = path4.getElements(); + pe.add(new MoveTo(100, 100)); + + for (int i = 0; i < 20000; i++) { + pe.add(new LineTo(110 + 0.01f * i, 110)); + pe.add(new LineTo(111 + 0.01f * i, 100)); + } + + pe.add(new LineTo(Float.NaN, 200)); + pe.add(new LineTo(200, 200)); + pe.add(new LineTo(200, Float.NaN)); + pe.add(new LineTo(300, 300)); + pe.add(new LineTo(Float.NaN, Float.NaN)); + pe.add(new LineTo(100, 200)); + pe.add(new ClosePath()); + +// Test basic horizontal line (on pixel centers) + final Path path5 = new Path(); + path5.getElements().addAll( + new MoveTo(9.5, 9.5), + new HLineTo(100)); + path5.setStroke(Color.PINK); + path5.setStrokeWidth(1.0); + + // Fill contiguous shapes on NonAA rasterizer: + double[] x = new double[] { 33.3333, 100.56677}; + + final Path path6 = new Path(); + path6.getElements().addAll( + new MoveTo(9.5, 9.5), + new HLineTo(100), + new LineTo(x[0], x[1]), + new HLineTo(9.5), + new ClosePath()); + path6.setFill(Color.RED); + path6.setStroke(null); + path6.setSmooth(false); + + final Path path7 = new Path(); + path7.getElements().addAll( + new MoveTo(200, 9.5), + new HLineTo(100), + new LineTo(x[0], x[1]), + new HLineTo(200), + new ClosePath()); + path7.setFill(Color.BLUE); + path7.setStroke(null); + path7.setSmooth(false); + + root.getChildren().addAll(path6, path7, path1, path2, path3, path4, path5); + return root; + } + + @Override + public void start(Stage primaryStage) throws Exception { + primaryStage.setScene(new Scene(createContent())); + primaryStage.show(); + } + + /** + * Java main for when running without JavaFX launcher + */ + public static void main(String[] args) { + + System.setProperty("prism.verbose", "true"); + + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/test/PathBug.java b/src/main/java/test/PathBug.java new file mode 100644 index 0000000..d7fa2aa --- /dev/null +++ b/src/main/java/test/PathBug.java @@ -0,0 +1,74 @@ +/* + * 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; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.Stage; + +public class PathBug extends Application { + + @Override + public void start(Stage primaryStage) { + double w = 400; + double h = 400; + + Canvas canvas = new Canvas(w, h); + GraphicsContext gc = canvas.getGraphicsContext2D(); + + gc.setFill(Color.BLACK); + gc.fillRect(0, 0, w, h); + + if (true) { +// Setting the clip to exclude the lower 50 pixels. + gc.beginPath(); + gc.rect(0, 0, w, h - 50); + gc.closePath(); + gc.clip(); // Comment this line out to see the intended shape. + } + +// Constructing a path that should look like a square with a hole. + gc.beginPath(); + gc.rect(20, 20, w - 40, h - 40); + gc.arc(200, 200, 100, 100, 0, 360); + gc.closePath(); + + gc.setFill(Color.CORNFLOWERBLUE); + gc.fill(); + + StackPane root = new StackPane(); + root.getChildren().add(canvas); + Scene scene = new Scene(root); + + primaryStage.setScene(scene); + primaryStage.show(); + } + + public static void main(String[] args) { + Application.launch(args); + } +} diff --git a/src/main/java/test/PolygonClipTest.java b/src/main/java/test/PolygonClipTest.java new file mode 100644 index 0000000..2d586d2 --- /dev/null +++ b/src/main/java/test/PolygonClipTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2021, 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; + +import javafx.animation.AnimationTimer; +import javafx.application.Application; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.paint.Color; +import javafx.scene.shape.ClosePath; +import javafx.scene.shape.CubicCurveTo; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; +import javafx.scene.transform.Scale; +import javafx.stage.Stage; + +/** + * Test dedicated to study special cases in Path clipper + */ +public class PolygonClipTest extends Application { + + private static final boolean TEST_STROKE = false; + private static final int TEST_CASE = 13; + + private static final int SPEED = 60; + + private static final double SCENE_WIDTH = 100.0; + private static final double SCALE = 3.0; + + static { + System.setProperty("prism.marlin.subPixel_log2_X", "8"); + + // disable static clipping setting: + System.setProperty("prism.marlin.clip", "false"); + System.setProperty("prism.marlin.clip.runtime.enable", "true"); + + // disable min length check: always subdivide curves at clip edges + //System.setProperty("prism.marlin.clip.subdivider.minLength", "-1"); + if (false) { + System.setProperty("prism.marlin.clip.subdivider.minLength", "100"); + } else { + System.setProperty("prism.marlin.clip.subdivider.minLength", "-1"); + } + } + + @Override + public void start(Stage stage) { + final Path p = new Path(); + p.setFill(Color.STEELBLUE); + if (TEST_STROKE) { + p.setStroke(Color.RED); + p.setStrokeWidth(1.0 / SCALE); + } else { + p.setStroke(null); + } + p.setCache(false); + p.setSmooth(true); + + final Group group = new Group(p); + if (SCALE != 1.0) { + group.getTransforms().add(new Scale(SCALE, SCALE)); + } + + final Scene scene = new Scene(group, SCALE * SCENE_WIDTH, SCALE * SCENE_WIDTH, Color.LIGHTGRAY); + stage.setScene(scene); + stage.setTitle(this.getClass().getSimpleName()); + stage.show(); + + final AnimationTimer anim = new AnimationTimer() { + int numHandle = 0; + boolean clip = false; + + @Override + public void handle(long now) { + if ((numHandle++) % SPEED == 0) { + clip = !clip; + System.out.println("clip: " + clip); + + // Enable or Disable clipping: + System.setProperty("prism.marlin.clip.runtime", (clip) ? "true" : "false"); + + p.setFill((clip) ? Color.BLUE : Color.GREEN); + updatePath(p); + } + } + }; + anim.start(); + } + + private static void updatePath(final Path p) { + // modifying path for every test ensures no caching (bounds...) + switch (TEST_CASE) { + case 1: + p.getElements().setAll( + new MoveTo(-89.687675, 171.33438), + new LineTo(112.86113, 151.63348), + new LineTo(84.55856, 111.90166), + new LineTo(-2.169856, -22.49296), + new LineTo(155.98148, -65.42218), + new LineTo(100.07923, 96.86355), + new ClosePath() + ); + break; + case 2: + p.getElements().setAll( + new MoveTo(-57.112507, 79.05166), + new LineTo(136.05696, 26.617828), + new LineTo(92.853615, 62.25588), + new LineTo(59.66546, 56.429977), + new LineTo(79.59174, -54.074516), + new LineTo(-75.93232, 39.502163), + new ClosePath(), + new LineTo(86.28725, -18.620544), + new LineTo(87.534134, -69.6632), + new LineTo(-61.609844, -46.943455), + new LineTo(-53.029644, -40.836628), + new LineTo(89.01727, -43.499767), + new ClosePath() + ); + break; + case 3: + p.getElements().setAll( + new MoveTo(174.8631, 124.340775), + new LineTo(-13.485423, 120.01353), + new LineTo(-40.214275, -11.351073), + new LineTo(96.66595, 33.508484), + new LineTo(-31.891193, -17.238123), + new LineTo(-0.092007555, 49.85812), + new ClosePath(), + new LineTo(-35.58541, 126.748764), + new LineTo(13.534866, 105.12724), + new LineTo(-84.706535, 165.25713), + new LineTo(105.69439, 48.8846), + new LineTo(-34.655937, 88.94304), + new ClosePath() + ); + break; + case 4: + p.getElements().setAll( + new MoveTo(-99.77336, 35.190475), + new CubicCurveTo(-25.539629, 180.36601, 52.512184, 42.104904, -66.391945, -7.1875143), + new CubicCurveTo(97.41586, 79.37796, 102.07544, 10.436856, -7.376722, 18.136734) + ); + break; + case 5: + // move / close outside: + p.getElements().setAll( + new MoveTo(-99.77336, 35.190475), + new ClosePath() + ); + break; + case 6: + // move / close inside: + p.getElements().setAll( + new MoveTo(13.77336, 35.190475), + new ClosePath() + ); + break; + case 7: + // move / close outside: + p.getElements().setAll( + new MoveTo(-99.77336, 35.190475) + ); + break; + case 8: + // move / close outside: + p.getElements().setAll( + new MoveTo(-99.77336, 35.190475), + new LineTo(-133,-100) + ); + break; + case 9: + // move / close outside: + p.getElements().setAll( + new MoveTo(-99.77336, 35.190475), + new LineTo(-133,-100), + new ClosePath() + ); + break; + case 10: + // move / close inside: + p.getElements().setAll( + new MoveTo(13.77336, 35.190475), + new LineTo(73, 60), + new ClosePath() + ); + break; + case 13: + // move / line (outside over corners) then close => inside: + p.getElements().setAll( + new MoveTo(-99.77336, 35.190475), + new LineTo(-23, -37), + new LineTo(173, -37), + new LineTo(173, 57), + new ClosePath() + ); + break; + default: + } + } + + public static void main(String[] args) { + launch(args); + } + +} diff --git a/src/main/java/test/PolygonTest.java b/src/main/java/test/PolygonTest.java new file mode 100644 index 0000000..c9f630e --- /dev/null +++ b/src/main/java/test/PolygonTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2021, 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; + +import javafx.application.Application; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.paint.Color; +import javafx.scene.shape.Polygon; +import javafx.scene.transform.Translate; +import javafx.stage.Screen; +import javafx.stage.Stage; + +/** + */ +public class PolygonTest extends Application { + + // (2^31 = 1073741824) / 256 = 4194304 => overflow in DRenderer + private static final double LARGE_X_COORDINATE = 4194304.250; // -Dprism.marlin.subPixel_log2_X=8 +// private static final Double LARGE_X_COORDINATE = 134217728.250; // -Dprism.marlin.subPixel_log2_X=3 + +// private static final double epsilon = 1.5E10; // max power of ten before translation into viewport is wrong + private static final double epsilon = 1000; + + private static final double SCENE_WIDTH = 600.0; + + @Override + public void start(Stage stage) { + double dpi = Screen.getPrimary().getDpi(); + System.out.println("dpi: " + dpi); + + double dpiScale = 1.0; // Screen.getPrimary().getOutputScaleX(); + + double longWidth = LARGE_X_COORDINATE / dpiScale + SCENE_WIDTH + 0.001 + epsilon; + + final Polygon veryWidePolygon; + if (true) { + if (true) { + veryWidePolygon = new Polygon( + 0.0, -1000.0, + 100.0, -500.0, + longWidth, 200.0, + longWidth - 1000, 500.0 + ); + + } else { + // inverted test case => no large moveTo in Filler but bug in Stroker: + if (true) { + veryWidePolygon = new Polygon( + 0.0, 100.0, + 0.0, 0.0, + longWidth, 50.0, + longWidth, 100.0 + ); + } else { + veryWidePolygon = new Polygon( + longWidth, 50.0, + longWidth, 100.0, + 0.0, 100.0, + 0.0, 0.0 + ); + } + } + } else { + // original test case => large moveTo in Filler but no bug in Stroker: + veryWidePolygon = new Polygon( + 0.0, 0.0, + longWidth, 50.0, + longWidth, 100.0, + 0.0, 100.0); + } + + veryWidePolygon.setFill(Color.STEELBLUE); + veryWidePolygon.setStroke(Color.RED); + veryWidePolygon.setStrokeWidth(5); + + Group group = new Group(veryWidePolygon); + group.getTransforms().add(new Translate(-longWidth + SCENE_WIDTH, 100.0)); + + Scene scene = new Scene(group, SCENE_WIDTH, 400, Color.LIGHTGRAY); + stage.setScene(scene); + stage.setTitle("DPI scale: " + dpiScale); + stage.show(); + } + + public static void main(String[] args) { + launch(args); + } + +} diff --git a/src/main/java/test/PolylineClipTest.java b/src/main/java/test/PolylineClipTest.java new file mode 100644 index 0000000..002a281 --- /dev/null +++ b/src/main/java/test/PolylineClipTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017, 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; + +import javafx.animation.AnimationTimer; +import javafx.application.Application; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.paint.Color; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; +import javafx.stage.Stage; + +public class PolylineClipTest extends Application { + + static final int NUM_OFFSCREEN = 10000; + static boolean OMIT_OFFSCREEN = false; + + @Override + public void start(Stage stage) { + Path p = new Path(); + p.setStroke(Color.BLUE); + p.setFill(null); + + if (OMIT_OFFSCREEN) { + p.getElements().add(new MoveTo(-100, 100)); + } else { + p.getElements().add(new MoveTo(-500, 100)); + for (int i = 0; i < NUM_OFFSCREEN; i++) { + double x = Math.random() * 400 - 500; + double y = Math.random() * 200; + p.getElements().add(new LineTo(x, y)); + } + p.getElements().add(new LineTo(-100, 100)); + } + + final LineTo lt1 = new LineTo(50, 150); + final LineTo lt2 = new LineTo(150, 50); + p.getElements().add(lt1); + p.getElements().add(lt2); + p.setCache(false); + + Scene scene = new Scene(new Group(p), 200, 200); + stage.setScene(scene); + stage.show(); + + AnimationTimer anim = new AnimationTimer() { + @Override + public void handle(long now) { + double x1 = Math.random() * 20 + 40; + double y1 = Math.random() * 20 + 140; + double x2 = Math.random() * 20 + 140; + double y2 = Math.random() * 20 + 40; + lt1.setX(x1); + lt1.setY(y1); + lt2.setX(x2); + lt2.setY(y2); + } + }; + anim.start(); + } + + public static void main(String argv[]) { + OMIT_OFFSCREEN = (argv.length != 0); + System.out.println("OMIT_OFFSCREEN: " + OMIT_OFFSCREEN); + launch(argv); + } +} diff --git a/src/main/java/test/PolylineWideClipTest.java b/src/main/java/test/PolylineWideClipTest.java new file mode 100644 index 0000000..745c142 --- /dev/null +++ b/src/main/java/test/PolylineWideClipTest.java @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2021, 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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import javafx.animation.AnimationTimer; +import javafx.application.Application; +import javafx.collections.ObservableList; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.paint.Color; +import javafx.scene.shape.ClosePath; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; +import javafx.scene.shape.PathElement; +import javafx.stage.Stage; + +/** + * + */ +public class PolylineWideClipTest extends Application { + + private static final boolean TEST_FILL = true; + private static final boolean TEST_STROKE = true; // stroker passes (2021.10.17) + + private static final boolean CLOSE = false; + + private static final int SPEED = 10; // 60 means 1s + + // (2^31 = 1073741824) / 256 = 4194304 => overflow in DRenderer + // private static final double LARGE_X_COORDINATE = 4194304.250 + 1000.0013; + // max precision limited by CurveClipSplitter: 0.9999999999999997 ~ 1E-15 EPS + // => coords > ~1E15 will cause troubles (solver will give incorrect intersections + // that could be fixed using recursive subdivision or newton root refinement) + private static final double LARGE_X_COORDINATE = 1E15; // not ideal = Float.MAX_VALUE / 2.0 +// private static final double LARGE_X_COORDINATE = 1E12; + + private static final double SCENE_WIDTH = 1000.0; + + private static final int[][] combPts = new int[3][]; + private static final List comb2PtsIn8; + + private static final double[][] ptsIn = new double[4][2]; + private static final double[][] ptsOut = new double[8][2]; + + static final long SEED = 1666133789L; + // Fixed seed to avoid any difference between runs: + static final Random RANDOM = new Random(SEED); + + static { + // 0 1 2 or 1 2 0 or 2 0 1 + combPts[0] = new int[]{0, 1, 2}; + combPts[1] = new int[]{1, 2, 0}; + combPts[2] = new int[]{2, 0, 1}; + // System.out.println("comb2PtsIn8: " + Arrays.deepToString(combPts)); + + // Generate 1 pt inside: + ptsIn[0][0] = SCENE_WIDTH * 1 / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble(); + ptsIn[0][1] = SCENE_WIDTH * 1 / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble(); + + ptsIn[1][0] = SCENE_WIDTH * 2 / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble(); + ptsIn[1][1] = SCENE_WIDTH * 1 / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble(); + + ptsIn[2][0] = SCENE_WIDTH * 2 / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble(); + ptsIn[2][1] = SCENE_WIDTH * 2 / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble(); + + ptsIn[3][0] = SCENE_WIDTH * 1 / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble(); + ptsIn[3][1] = SCENE_WIDTH * 2 / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble(); + + System.out.println("ptsIn: " + Arrays.deepToString(ptsIn)); + + // Generate 1 pt outside in each quadrant outside: + comb2PtsIn8 = generateCombinations(8, 2); + /* + System.out.println("comb2PtsIn8: " + comb2PtsIn8.size()); + for (int[] comb2Pts : comb2PtsIn8) { + System.out.println(Arrays.toString(comb2Pts)); + } + */ + + int i = 0; + // LEFT + ptsOut[i][0] = -(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + ptsOut[i][1] = (SCENE_WIDTH / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble()); + i++; + + // TOP LEFT + ptsOut[i][0] = -(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + ptsOut[i][1] = -(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + i++; + + // TOP + ptsOut[i][0] = (SCENE_WIDTH / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble()); + ptsOut[i][1] = -(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + i++; + + // TOP RIGHT + ptsOut[i][0] = +(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + ptsOut[i][1] = -(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + i++; + + // RIGHT + ptsOut[i][0] = +(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + ptsOut[i][1] = (SCENE_WIDTH / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble()); + i++; + + // BOTTOM RIGHT + ptsOut[i][0] = +(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + ptsOut[i][1] = +(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + i++; + + // BOTTOM + ptsOut[i][0] = (SCENE_WIDTH / 3 + (SCENE_WIDTH / 10) * RANDOM.nextDouble()); + ptsOut[i][1] = +(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + i++; + + // BOTTOM LEFT + ptsOut[i][0] = -(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + ptsOut[i][1] = +(LARGE_X_COORDINATE + SCENE_WIDTH * RANDOM.nextDouble()); + i++; + + System.out.println("ptsOut: " + Arrays.deepToString(ptsOut)); + + System.out.println("Max tests: " + (combPts.length * ptsIn.length * comb2PtsIn8.size())); + + // Set Marlin properties: + /* + -Dprism.marlin=true + -Dprism.marlin.double=true + */ + System.setProperty("prism.marlin.log", "true"); + + System.setProperty("prism.marlin.subPixel_log2_X", "8"); + System.setProperty("prism.marlin.clip", "true"); + +// System.setProperty("prism.marlin.clip.subdivider.minLength", "-1"); +// System.setProperty("prism.marlin.clip.subdivider.minLength", "100"); + } + + @Override + public void start(Stage stage) { + final Path p = new Path(); + p.setFill((TEST_FILL) ? Color.STEELBLUE : null); + + p.setStroke((TEST_STROKE) ? Color.RED : null); + p.setStrokeWidth(4); + p.setCache(false); + + final Scene scene = new Scene(new Group(p), SCENE_WIDTH, SCENE_WIDTH); + stage.setTitle("PolylineWideClipTest"); + stage.setScene(scene); + stage.show(); + + final AnimationTimer anim = new AnimationTimer() { + int numHandle = 0; + int numTest = 0; + int i = 0; // triangle permutation + int j = 0; // in permutation + int k = 0; // out permutation + + final double[][] pts = new double[3][]; + + @Override + public void handle(long now) { + if ((numHandle++) % SPEED == 0) { + System.out.println("Test[" + numTest + "] i = " + i + " j = " + j + " k = " + k + " ----"); + + // 0 is inside + pts[0] = ptsIn[j]; + + final int[] cb = comb2PtsIn8.get(k); + pts[1] = ptsOut[cb[0]]; + pts[2] = ptsOut[cb[1]]; + + /* + System.out.println("P0: "+Arrays.toString(pts[0])); + System.out.println("P1: "+Arrays.toString(pts[1])); + System.out.println("P2: "+Arrays.toString(pts[2])); + */ + final int[] idxPt = combPts[i]; + + final ObservableList pathElements = p.getElements(); + if (CLOSE) { + pathElements.setAll( + new MoveTo(pts[idxPt[0]][0], pts[idxPt[0]][1]), + new LineTo(pts[idxPt[1]][0], pts[idxPt[1]][1]), + new LineTo(pts[idxPt[2]][0], pts[idxPt[2]][1]), + new ClosePath() + ); + } else { + pathElements.setAll( + new MoveTo(pts[idxPt[0]][0], pts[idxPt[0]][1]), + new LineTo(pts[idxPt[1]][0], pts[idxPt[1]][1]), + new LineTo(pts[idxPt[2]][0], pts[idxPt[2]][1]) + ); + } + + numTest++; + if ((++i) >= combPts.length) { + i = 0; + if ((++j) >= ptsIn.length) { + j = 0; + if ((++k) >= comb2PtsIn8.size()) { + k = 0; + System.out.println("All tests done !"); + + p.setFill((TEST_FILL) ? Color.GREEN : null); + p.setStroke((TEST_STROKE) ? Color.GREEN : null); + } + } + } + } + } + }; + anim.start(); + } + + public static void main(String argv[]) { + launch(argv); + } + + /** + * Generate all combinations (no repetition, no ordering) + * @param n number of elements + * @param k number of items to choose + * @return list of all combinations (integer arrays) + */ + public static List generateCombinations(final int n, final int k) { + final int count = comb(n, k); + + final List results = new ArrayList(count); + + recursiveCombinations(n, k, results, new int[k], 0, 0); + + return results; + } + + /** + * Recursive algorithm to generate all combinations + * @param n number of elements + * @param k number of items to choose + * @param results result array + * @param current current array + * @param position position in the array + * @param nextInt next integer value + */ + private static void recursiveCombinations(final int n, final int k, final List results, final int[] current, final int position, final int nextInt) { + for (int i = nextInt; i < n; i++) { + current[position] = i; + + if (position + 1 == k) { + // copy current result : + final int[] res = new int[k]; + System.arraycopy(current, 0, res, 0, k); + results.add(res); + } else { + recursiveCombinations(n, k, results, current, position + 1, i + 1); + } + } + } + + /** + * Return factorial(n) = n! + * @param n integer + * @return factorial(n) + */ + public static int fact(final int n) { + int res = 1; + for (int i = 1; i <= n; i++) { + res *= i; + } + return res; + } + + /** + * Return the number of arrangements without repetition + * @param n number of elements + * @param k number of items to choose + * @return number of arrangements + */ + public static int arr(final int n, final int k) { + int res = 1; + + // A-n-k = n! / (n - k)! + for (int i = n, min = n - k + 1; i >= min; i--) { + res *= i; + } + return res; + } + + /** + * Return the number of combinations (no repetition, no ordering) + * @param n number of elements + * @param k number of items to choose + * @return number of generateCombinations + */ + public static int comb(final int n, final int k) { + //C-n-k = A-n-k/k! + return arr(n, k) / fact(k); + } + +} diff --git a/src/main/java/test/RasterPerf.java b/src/main/java/test/RasterPerf.java new file mode 100644 index 0000000..34b5253 --- /dev/null +++ b/src/main/java/test/RasterPerf.java @@ -0,0 +1,113 @@ +package test; + +import com.sun.javafx.geom.Ellipse2D; +import com.sun.javafx.geom.Path2D; +import com.sun.javafx.geom.RectBounds; +import com.sun.javafx.geom.RoundRectangle2D; +import com.sun.javafx.geom.Shape; +import com.sun.javafx.geom.transform.BaseTransform; +import com.sun.prism.impl.shape.DMarlinRasterizer; +import com.sun.prism.impl.shape.MarlinRasterizer; +import com.sun.prism.impl.shape.NativePiscesRasterizer; +import com.sun.prism.impl.shape.OpenPiscesRasterizer; +import com.sun.prism.impl.shape.ShapeRasterizer; + +public class RasterPerf { + + static final int PRIME_CALLS = 1000; + static final long warmupns = 1000l * 1000l * 500l; + static final long targetns = 1000l * 1000l * 3000l; + + static String allresults = ""; + static final Path2D cubics, quads; + + static { + cubics = new Path2D(); + cubics.moveTo(10, 10); + cubics.curveTo(110, 10, 10, 15, 110, 20); + cubics.curveTo(10, 20, 110, 25, 10, 30); + cubics.curveTo(110, 30, 10, 35, 110, 40); + cubics.curveTo(10, 40, 110, 45, 10, 50); + cubics.curveTo(110, 50, 10, 55, 110, 60); + cubics.curveTo(10, 60, 110, 65, 10, 70); + cubics.curveTo(110, 70, 10, 75, 110, 80); + cubics.curveTo(10, 80, 110, 85, 10, 90); + cubics.curveTo(110, 90, 10, 95, 110, 100); + cubics.curveTo(10, 100, 110, 105, 10, 110); + quads = new Path2D(); + quads.moveTo(60, 10); + quads.quadTo(110, 15, 60, 20); + quads.quadTo(10, 25, 60, 30); + quads.quadTo(110, 35, 60, 40); + quads.quadTo(10, 45, 60, 50); + quads.quadTo(110, 55, 60, 60); + quads.quadTo(10, 65, 60, 70); + quads.quadTo(110, 75, 60, 80); + quads.quadTo(10, 85, 60, 90); + quads.quadTo(110, 95, 60, 100); + quads.quadTo(10, 105, 60, 110); + } + + public static void bench(ShapeRasterizer sr, Shape s, boolean aa, String name) { + RectBounds b = s.getBounds(); + for (int i = 0; i < PRIME_CALLS; i++) { + sr.getMaskData(s, null, b, BaseTransform.IDENTITY_TRANSFORM, true, aa); + } + long start, elapsed; + int n = 0; + start = System.nanoTime(); + do { + sr.getMaskData(s, null, b, BaseTransform.IDENTITY_TRANSFORM, true, aa); + n++; + elapsed = System.nanoTime() - start; + } while (elapsed < warmupns); + long limit = targetns * n / elapsed; + System.out.println("warmup: " + n + " iterations in " + elapsed + "ns"); + System.out.println("benchmarking " + limit + " iterations"); + System.out.flush(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + start = System.nanoTime(); + for (int i = 0; i < limit; i++) { + sr.getMaskData(s, null, b, BaseTransform.IDENTITY_TRANSFORM, true, aa); + } + long end = System.nanoTime(); + double ms = (end - start) / 1000.0 / 1000.0; + ms = Math.round(ms * 100.0) / 100.0; + String result = limit + " " + name + " rasterizations took " + + ms + "ms, " + (limit * 1000) / ms + " ops/sec\n"; + System.out.println(result); + allresults += result; + } + + public static void bench(ShapeRasterizer sr, String srname, boolean aa) { + bench(sr, new RoundRectangle2D(10, 10, 10, 10, 0, 0), aa, "10x10 Rectangle " + srname); + bench(sr, new RoundRectangle2D(10, 10, 100, 100, 0, 0), aa, "100x100 Rectangle " + srname); + bench(sr, new RoundRectangle2D(10, 10, 300, 300, 0, 0), aa, "300x300 Rectangle " + srname); + bench(sr, new Ellipse2D(10, 10, 10, 10), aa, "10x10 Ellipse " + srname); + bench(sr, new Ellipse2D(10, 10, 100, 100), aa, "100x100 Ellipse " + srname); + bench(sr, new Ellipse2D(10, 10, 300, 300), aa, "300x300 Ellipse " + srname); + bench(sr, cubics, aa, "100x100 Cubics " + srname); + bench(sr, quads, aa, "100x100 Quads " + srname); + } + + public static void bench(ShapeRasterizer sr, String srname) { + bench(sr, srname + " non-AA", false); + allresults += "\n"; + bench(sr, srname + " AA", true); + allresults += "\n\n"; + } + + public static void main(String argv[]) { + for (int n = 0; n < 2; n++) { +// bench(new NativePiscesRasterizer(), "native"); +// bench(new OpenPiscesRasterizer(), "Java"); + bench(new MarlinRasterizer(), "MarlinFX"); + bench(new DMarlinRasterizer(), "DMarlinFX"); + System.out.println(); + System.out.println(allresults); + } + } +} diff --git a/src/main/java/test/ShapeOutlineBug.java b/src/main/java/test/ShapeOutlineBug.java new file mode 100644 index 0000000..a5ab548 --- /dev/null +++ b/src/main/java/test/ShapeOutlineBug.java @@ -0,0 +1,37 @@ +package test; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.stage.Stage; + +public class ShapeOutlineBug extends Application { + + private final static double SIZE = 900; + private final static double D = 0.5 * SIZE; + private final static double sqrt2 = Math.sqrt(2); + + public static void main(String[] args) { + Application.launch(args); + } + + @Override + public void start(Stage stage) throws Exception { + double r = 1843200.0; + double c = D - r / sqrt2; + + Circle shape = new Circle(c, c, r, Color.GREY); + + shape.setStroke(Color.BLACK); + shape.setStrokeWidth(2.0); + shape.getStrokeDashArray().addAll(10.0, 5.0); + + Pane root = new Pane(); + root.getChildren().add(shape); + + stage.setScene(new Scene(root, SIZE, SIZE)); + stage.show(); + } +} diff --git a/src/main/java/test/ShapeOutlineBugCirclePath.java b/src/main/java/test/ShapeOutlineBugCirclePath.java new file mode 100644 index 0000000..9a2aba3 --- /dev/null +++ b/src/main/java/test/ShapeOutlineBugCirclePath.java @@ -0,0 +1,82 @@ +package test; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Shape; +import javafx.scene.shape.PathElement; +import javafx.scene.shape.Path; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.QuadCurveTo; +import javafx.scene.shape.CubicCurveTo; +import javafx.scene.shape.ClosePath; +import javafx.stage.Stage; + +public class ShapeOutlineBugCirclePath extends Application { + + private final static double SIZE = 900; + private final static double D = 0.5 * SIZE; + private final static double sqrt2 = Math.sqrt(2); + private static boolean SMALL = false; + + public static void main(String[] args) { + SMALL = args.length > 0; + Application.launch(args); + } + + public static void printShape(Shape s) { + if (!(s instanceof Path)) { + System.out.println("Not a path: " + s); + return; + } + System.out.println("Path["); + for (PathElement pe : ((Path) s).getElements()) { + if (pe instanceof MoveTo) { + MoveTo mt = (MoveTo) pe; + System.out.println(" MoveTo (" + mt.getX() + ", " + mt.getY() + ")"); + } else if (pe instanceof LineTo) { + LineTo lt = (LineTo) pe; + System.out.println(" LineTo (" + lt.getX() + ", " + lt.getY() + ")"); + } else if (pe instanceof QuadCurveTo) { + QuadCurveTo lt = (QuadCurveTo) pe; + System.out.println(" QuadCurveTo (" + lt.getControlX() + ", " + lt.getControlY() + ", "); + System.out.println(" " + lt.getX() + ", " + lt.getY() + ")"); + } else if (pe instanceof CubicCurveTo) { + CubicCurveTo lt = (CubicCurveTo) pe; + System.out.println(" CubicCurveTo(" + lt.getControlX1() + ", " + lt.getControlY1() + ", "); + System.out.println(" " + lt.getControlX2() + ", " + lt.getControlY2() + ", "); + System.out.println(" " + lt.getX() + ", " + lt.getY() + ")"); + } else if (pe instanceof ClosePath) { + System.out.println(" ClosePath ()"); + } else { + System.out.println("Unrecognized path element: " + pe); + } + } + System.out.println("]"); + } + + @Override + public void start(Stage stage) throws Exception { + double r = SMALL ? 100 : 1843200.0; + double c = D - r / sqrt2; + + Circle circle = new Circle(c, c, r, Color.GREY); + Circle littlecircle = new Circle(c, c, 10, Color.GREY); + Shape shape = Shape.union(circle, littlecircle); + printShape(shape); + + shape.setFill(Color.BLUE); + shape.setStroke(Color.RED); + shape.setStrokeWidth(2.0); + shape.getStrokeDashArray().addAll(10.0, 5.0); + + Pane root = new Pane(); + root.getChildren().add(shape); + + stage.setScene(new Scene(root, SIZE, SIZE)); + stage.show(); + } +} diff --git a/src/main/java/test/ShapeOutlineBugRectangle.java b/src/main/java/test/ShapeOutlineBugRectangle.java new file mode 100644 index 0000000..fd6ac89 --- /dev/null +++ b/src/main/java/test/ShapeOutlineBugRectangle.java @@ -0,0 +1,40 @@ +package test; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.ClosePath; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; +import javafx.stage.Stage; + +public class ShapeOutlineBugRectangle extends Application { + private final static double SIZE = 900 * 1024 * 5; + + public static void main(String[] args) { + Application.launch(args); + } + + @Override + public void start(Stage stage) throws Exception { + Path shape = new Path(new MoveTo(450, 450), + new LineTo(-SIZE, -SIZE), + new LineTo(0, -2 * SIZE), + new LineTo(SIZE, -SIZE), + new LineTo(450, 450), + new ClosePath()); + + shape.setFill(Color.BLUE); + shape.setStroke(Color.RED); + shape.setStrokeWidth(2.0); + shape.getStrokeDashArray().addAll(10.0, 5.0); + + Pane root = new Pane(); + root.getChildren().add(shape); + + stage.setScene(new Scene(root, 900, 900)); + stage.show(); + } +} diff --git a/src/main/java/test/ShapePerformanceBug.java b/src/main/java/test/ShapePerformanceBug.java new file mode 100644 index 0000000..0c44689 --- /dev/null +++ b/src/main/java/test/ShapePerformanceBug.java @@ -0,0 +1,36 @@ +package test; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.stage.Stage; + +public class ShapePerformanceBug extends Application { + private final static double SIZE = 900; + private final static double D = 0.5 * SIZE; + private final static double sqrt2 = Math.sqrt(2); + + public static void main(String[] args) { + Application.launch(args); + } + + @Override + public void start(Stage stage) throws Exception { + double r = 1843200.0; + double c = D - r/sqrt2; + + Circle shape = new Circle(c, c, r, Color.BLUE); + + shape.setStroke(Color.RED); + shape.setStrokeWidth(2.0); + shape.getStrokeDashArray().addAll(10.0, 5.0); + + Pane root = new Pane(); + root.getChildren().add(shape); + + stage.setScene(new Scene(root, SIZE, SIZE)); + stage.show(); + } +} diff --git a/src/main/java/test/TestNonAARasterization.java b/src/main/java/test/TestNonAARasterization.java new file mode 100644 index 0000000..137a7fc --- /dev/null +++ b/src/main/java/test/TestNonAARasterization.java @@ -0,0 +1,669 @@ +package test; + +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.awt.image.BufferedImage; +import java.util.Random; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import javafx.geometry.Rectangle2D; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.SnapshotParameters; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.PixelReader; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.ClosePath; +import javafx.scene.shape.CubicCurveTo; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; +import javafx.scene.shape.QuadCurveTo; +import javafx.scene.text.Text; +import javafx.scene.transform.Scale; +import javafx.stage.Stage; + +public class TestNonAARasterization extends Application { + + static enum ShapeMode { + TWO_CUBICS, + FOUR_QUADS, + FIVE_LINE_POLYS, + NINE_LINE_POLYS, + RECTANGLES, + OVALS, + OCTAGONS, + } + static final double OCT_C = 1.0 / (2.0 + Math.sqrt(2.0)); + + // original thresholds: + static double tolerance = 0.0001; + static double warn = 0.0005; + + static ShapeMode shapemode = ShapeMode.OVALS; // TWO_CUBICS; + static boolean useJava2D = false; + static boolean useJava2DClip = false; + static boolean exitWhenDone = false; + + static final int NUMTESTS = 100000; + static final int TESTW = 50; + static final int TESTH = 50; + static final int MAG = 18; + + static final int BGPIXEL = 0xffffffff; + static final int FGPIXEL = 0xff000000; + + static final Color BAD = Color.RED; + static final Color GOOD = Color.GREEN; + static final Color OK = Color.ORANGE; + static final Color IN = Color.YELLOW; + static final Color OUT = Color.WHITE; + static final Color GRAY = Color.CYAN; + + static final long SEED = 1666133789L; + static final Random RAND; + + static { + RAND = new Random(SEED); + } + + public static final class Result { + + final Path2D path2d; + final int numerrors; + final int numwarnings; + + public Result(Path2D path2d, int numerrors, int numwarnings) { + this.path2d = path2d; + this.numerrors = numerrors; + this.numwarnings = numwarnings; + } + + public boolean worseThan(Result other) { + if (other == null) { + return true; + } + if (numerrors > other.numerrors) { + return true; + } + if (numerrors < other.numerrors) { + return false; + } + return numwarnings > other.numwarnings; + } + } + + int numBadPaths = 0; + Result worst; + int numpathstested; + long totalerrors; + long totalwarnings; + Canvas resultcv; + Path resultpath; + Text resulttext; + Text progressLabel; + + @Override + public void start(Stage stage) { + BorderPane root = new BorderPane(); + + progressLabel = new Text("running..."); + root.setBottom(progressLabel); + + resultcv = new Canvas(TESTW * MAG, TESTH * MAG); + resultpath = new Path(); + resultpath.setCache(false); + resultpath.setSmooth(false); + resultpath.getTransforms().add(new Scale(MAG, MAG)); + resultpath.setFill(null); + resultpath.setStroke(Color.DARKGRAY); + resultpath.setStrokeWidth(1.0 / MAG); + root.setCenter(new Group(resultcv, resultpath)); + + HBox legend = new HBox(); + legend.getChildren().add(new Text("Good Pixel: ")); + legend.getChildren().add(makeStatusCanvas(FGPIXEL, true, false)); + legend.getChildren().add(new Text(" Bad Pixels: ")); + legend.getChildren().add(makeStatusCanvas(FGPIXEL, false, false)); + legend.getChildren().add(makeStatusCanvas(BGPIXEL, true, false)); + legend.getChildren().add(new Text(" Iffy Pixels: ")); + legend.getChildren().add(makeStatusCanvas(FGPIXEL, false, true)); + legend.getChildren().add(makeStatusCanvas(BGPIXEL, true, true)); + legend.getChildren().add(new Text(" Alpha(AA) Pixel: ")); + legend.getChildren().add(makeStatusCanvas(12, true, false)); + resulttext = new Text("(waiting for result)"); + VBox vb = new VBox(legend, resulttext); + vb.setAlignment(Pos.TOP_CENTER); + vb.setFillWidth(false); + root.setTop(vb); + + Scene scene = new Scene(root); + stage.setScene(scene); + stage.setTitle("Testing " + shapemode); + stage.show(); + + resultcv.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler() { + @Override + public void handle(MouseEvent event) { + dumpTest(); + } + }); + + Thread t = new Thread(() -> generatePaths()); + t.setDaemon(true); + t.start(); + } + + static final String svgcommand = "MLQCZ"; + static final int[] coordcount = {2, 2, 4, 6, 0}; + static final String[] pathCommands = new String[] {"moveTo", "lineTo", "quadTo", "curveTo", "closePath"}; + + public void dumpTest() { + dumpResult(worst); + } + + static void dumpResult(Result r) { + if (r != null) { + Path2D p2d = r.path2d; + double[] coords = new double[6]; + System.out.println("test case with " + + r.numerrors + " errors and " + + r.numwarnings + " warnings:"); + System.out.print("Path="); + PathIterator pi = p2d.getPathIterator(null); + while (!pi.isDone()) { + int type = pi.currentSegment(coords); + System.out.print(' '); + System.out.print(svgcommand.charAt(type)); + for (int i = 0; i < coordcount[type]; i++) { + if (i > 0) { + System.out.print(','); + } + System.out.print(coords[i]); + } + pi.next(); + } + System.out.println(); + // Path2D: + System.out.println("Path2D p = new Path2D.Double();"); + pi = p2d.getPathIterator(null); + while (!pi.isDone()) { + int type = pi.currentSegment(coords); + System.out.print("p."); + System.out.print(pathCommands[type]); + System.out.print("("); + for (int i = 0; i < coordcount[type]; i++) { + if (i > 0) { + System.out.print(", "); + } + System.out.print(coords[i]); + } + System.out.println(");"); + pi.next(); + } + } + } + + public void clear() { + GraphicsContext gc = resultcv.getGraphicsContext2D(); + gc.clearRect(0, 0, resultcv.getWidth(), resultcv.getHeight()); + } + + private BufferedImage bimgT = null; + private Graphics2D g2dT = null; + + private BufferedImage bimgD = null; + private Graphics2D g2dD = null; + + public void renderPath(Path2D p2d, Path p, WritableImage wimg, boolean test) { + if (useJava2D) { + final BufferedImage bimg; + final Graphics2D g2d; + if (test) { + if (bimgT == null) { + bimgT = new BufferedImage(TESTW, TESTH, BufferedImage.TYPE_INT_ARGB); + g2dT = bimgT.createGraphics(); + if (true) { + g2dT.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + g2dT.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_PURE); + g2dT.setBackground(java.awt.Color.WHITE); + g2dT.setColor(java.awt.Color.BLACK); + } + bimg = bimgT; + g2d = g2dT; + } else { + // use another image: + if (bimgD == null) { + bimgD = new BufferedImage(TESTW, TESTH, BufferedImage.TYPE_INT_ARGB); + g2dD = bimgD.createGraphics(); + g2dD.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_PURE); + g2dD.setBackground(java.awt.Color.WHITE); + g2dD.setColor(java.awt.Color.BLACK); + } + bimg = bimgD; + g2d = g2dD; + } + g2d.clearRect(0, 0, TESTW, TESTH); + if (useJava2DClip) { + g2d.setClip(p2d); + g2d.fillRect(0, 0, TESTW, TESTH); + g2d.setClip(null); + } else { + g2d.fill(p2d); + } + copy(bimg, wimg); + } else { + setPath(p, p2d); + SnapshotParameters sp = new SnapshotParameters(); + sp.setViewport(new Rectangle2D(0, 0, TESTW, TESTH)); + p.snapshot(sp, wimg); + } + } + + public static boolean near(Path2D p, double x, double y, double err) { + if (err == 0.0) { + return false; + } + return p.intersects(x - err, y - err, err * 2.0, err * 2.0) + && !p.contains(x - err, y - err, err * 2.0, err * 2.0); + } + + public void update(Path2D p2d) { + setPath(resultpath, p2d); + Path p = makePath(); + WritableImage wimg = new WritableImage(TESTW, TESTH); + renderPath(p2d, p, wimg, false); + PixelReader pr = wimg.getPixelReader(); + GraphicsContext gc = resultcv.getGraphicsContext2D(); + gc.save(); + for (int y = 0; y < TESTH; y++) { + for (int x = 0; x < TESTW; x++) { + boolean inpath = p2d.contains(x + 0.5, y + 0.5); + boolean nearpath = near(p2d, x + 0.5, y + 0.5, warn); + int pixel = pr.getArgb(x, y); + renderPixelStatus(gc, x, y, pixel, inpath, nearpath); + } + } + gc.restore(); + } + + Canvas makeStatusCanvas(int pixel, boolean inpath, boolean nearpath) { + Canvas cv = new Canvas(MAG, MAG); + renderPixelStatus(cv.getGraphicsContext2D(), 0, 0, pixel, inpath, nearpath); + return cv; + } + + void renderPixelStatus(GraphicsContext gc, int x, int y, int pixel, boolean inpath, boolean nearpath) { + Color cross = null; + Color circle = null; + Color fill; + if (pixel == FGPIXEL) { + fill = IN; + if (inpath) { + cross = GOOD; + } else if (nearpath) { + circle = OK; + } else { + cross = BAD; + } + } else if (pixel == BGPIXEL) { + fill = OUT; + if (inpath) { + if (nearpath) { + circle = OK; + } else { + cross = BAD; + } + } + } else { + fill = GRAY; + if (nearpath) { + circle = OK; + } else { + cross = BAD; + } + } + gc.setFill(fill); + gc.fillRect(x * MAG, y * MAG, MAG, MAG); + if (cross != null) { + gc.setFill(cross); + gc.fillRect(x * MAG + 2, y * MAG + MAG / 2, MAG - 4, 1); + gc.fillRect(x * MAG + MAG / 2, y * MAG + 2, 1, MAG - 4); + } + if (circle != null) { + gc.setStroke(circle); + gc.strokeOval(x * MAG + 2, y * MAG + 2, MAG - 4, MAG - 4); + } + } + + public void updateLabel() { + int numbadpaths = numBadPaths; + double percentbadpaths = numbadpaths * 100.0 / numpathstested; + double avgbadpixels = (totalerrors == 0) ? 0.0 : totalerrors * 1.0 / numbadpaths; + double avgwarnings = (totalwarnings == 0) ? 0.0 : totalwarnings * 1.0 / numbadpaths; + String progress = String.format("bad paths (%d/%d == %3.2f%%", + numbadpaths, numpathstested, percentbadpaths); + progress += String.format(", %d bad pixels (avg = %3.2f - max = %d)", totalerrors, avgbadpixels, + (worst != null) ? worst.numerrors : 0); + progress += String.format(", %d warnings (avg = %3.2f)", totalwarnings, avgwarnings); + progressLabel.setText(progress); + } + + public void update() { + clear(); + if (worst != null) { + update(worst.path2d); + resulttext.setText(worst.numerrors + " bad pixels, " + worst.numwarnings + " iffy pixels"); + } + } + + public static void setPath(Path p, Path2D p2d) { + p.getElements().clear(); + PathIterator pi = p2d.getPathIterator(null); + double[] coords = new double[6]; + while (!pi.isDone()) { + switch (pi.currentSegment(coords)) { + case PathIterator.SEG_MOVETO: + p.getElements().add(new MoveTo(coords[0], coords[1])); + break; + case PathIterator.SEG_LINETO: + p.getElements().add(new LineTo(coords[0], coords[1])); + break; + case PathIterator.SEG_QUADTO: + p.getElements().add(new QuadCurveTo(coords[0], coords[1], + coords[2], coords[3])); + break; + case PathIterator.SEG_CUBICTO: + p.getElements().add(new CubicCurveTo(coords[0], coords[1], + coords[2], coords[3], + coords[4], coords[5])); + break; + case PathIterator.SEG_CLOSE: + p.getElements().add(new ClosePath()); + break; + default: + throw new InternalError("unexpected segment type"); + } + pi.next(); + } + p.getElements().add(new ClosePath()); + } + + public Path makePath() { + Path p = new Path(); + p.setFill(Color.BLACK); + p.setStroke(null); + p.setCache(false); + p.setSmooth(false); + return p; + } + + public void addResult(Result r, int numtested) { + numBadPaths++; + if (r.worseThan(worst)) { + worst = r; +// dumpTest(); + } + totalerrors += r.numerrors; + totalwarnings += r.numwarnings; + numpathstested = numtested; + update(); + updateLabel(); + } + + public void updateProgress(int numtested) { + numpathstested = numtested; + updateLabel(); + } + + public static double rand(double d) { + return RAND.nextDouble() * d; + } + + boolean done; + + public synchronized void signalDone() { + done = true; + notifyAll(); + } + + public synchronized void waitDone() throws InterruptedException { + while (!done) { + wait(); + } + } + + public static void copy(BufferedImage bimg, WritableImage wimg) { + PixelWriter pw = wimg.getPixelWriter(); + for (int y = 0; y < TESTH; y++) { + for (int x = 0; x < TESTW; x++) { + pw.setArgb(x, y, bimg.getRGB(x, y)); + } + } + } + + static final Ellipse2D e2d = new Ellipse2D.Double(); + + static void genShape(Path2D p2d, ShapeMode mode) { + double rx, ry, rw, rh; + p2d.reset(); + switch (mode) { + case TWO_CUBICS: + p2d.moveTo(rand(TESTW), rand(TESTH)); + p2d.curveTo(rand(TESTW), rand(TESTH), rand(TESTW), rand(TESTH), rand(TESTW), rand(TESTH)); + p2d.curveTo(rand(TESTW), rand(TESTH), rand(TESTW), rand(TESTH), rand(TESTW), rand(TESTH)); + break; + case FOUR_QUADS: + p2d.moveTo(rand(TESTW), rand(TESTH)); + p2d.quadTo(rand(TESTW), rand(TESTH), rand(TESTW), rand(TESTH)); + p2d.quadTo(rand(TESTW), rand(TESTH), rand(TESTW), rand(TESTH)); + p2d.quadTo(rand(TESTW), rand(TESTH), rand(TESTW), rand(TESTH)); + p2d.quadTo(rand(TESTW), rand(TESTH), rand(TESTW), rand(TESTH)); + break; + case NINE_LINE_POLYS: + case FIVE_LINE_POLYS: + p2d.moveTo(rand(TESTW), rand(TESTH)); + p2d.lineTo(rand(TESTW), rand(TESTH)); + p2d.lineTo(rand(TESTW), rand(TESTH)); + p2d.lineTo(rand(TESTW), rand(TESTH)); + p2d.lineTo(rand(TESTW), rand(TESTH)); + if (shapemode == ShapeMode.FIVE_LINE_POLYS) { + // And an implicit close makes 5 lines + break; + } + p2d.lineTo(rand(TESTW), rand(TESTH)); + p2d.lineTo(rand(TESTW), rand(TESTH)); + p2d.lineTo(rand(TESTW), rand(TESTH)); + p2d.lineTo(rand(TESTW), rand(TESTH)); + // And an implicit close makes 9 lines + break; + case RECTANGLES: + rw = rand(TESTW); + rh = rand(TESTH); + rx = rand(TESTW - rw); + ry = rand(TESTH - rh); + p2d.moveTo(rx, ry); + p2d.lineTo(rx + rw, ry); + p2d.lineTo(rx + rw, ry + rh); + p2d.lineTo(rx, ry + rh); + break; + case OVALS: + rw = rand(TESTW); + rh = rand(TESTH); + rx = rand(TESTW - rw); + ry = rand(TESTH - rh); + e2d.setFrame(rx, ry, rw, rh); + p2d.append(e2d, false); + break; + case OCTAGONS: + rw = rand(TESTW); + rh = rand(TESTH); + rx = rand(TESTW - rw); + ry = rand(TESTH - rh); + double ow = rw * OCT_C; + double oh = rh * OCT_C; + p2d.moveTo(rx + ow, ry); + p2d.lineTo(rx + rw - ow, ry); + p2d.lineTo(rx + rw, ry + oh); + p2d.lineTo(rx + rw, ry + rh - oh); + p2d.lineTo(rx + rw - ow, ry + rh); + p2d.lineTo(rx + ow, ry + rh); + p2d.lineTo(rx, ry + rh - oh); + p2d.lineTo(rx, ry + oh); + break; + } + } + + public void generatePaths() { + final Path2D p2d = new Path2D.Double(); + final Path p = makePath(); + final WritableImage wimg = new WritableImage(TESTW, TESTH); + final PixelReader pr = wimg.getPixelReader(); + int n = 0; + while (n < NUMTESTS) { + genShape(p2d, shapemode); + done = false; + Platform.runLater(() -> { + renderPath(p2d, p, wimg, true); + signalDone(); + }); + try { + waitDone(); + } catch (InterruptedException ex) { + break; + } + int errors = 0; + int warnings = 0; + for (int y = 0; y < TESTH; y++) { + for (int x = 0; x < TESTW; x++) { + boolean inpath = p2d.contains(x + 0.5, y + 0.5); + int pixel = pr.getArgb(x, y); + if (pixel == FGPIXEL) { + if (!inpath) { + if (near(p2d, x + 0.5, y + 0.5, warn)) { + if (!near(p2d, x + 0.5, y + 0.5, tolerance)) { + warnings++; + } + } else { + errors++; + } + } + } else if (pixel == BGPIXEL) { + if (inpath) { + if (near(p2d, x + 0.5, y + 0.5, warn)) { + if (!near(p2d, x + 0.5, y + 0.5, tolerance)) { + warnings++; + } + } else { + errors++; + } + } + } else { + errors++; + } + } + } + ++n; + if (warnings + errors > 0) { + final int numtested = n; + final Result r = new Result(new Path2D.Double(p2d), errors, warnings); + Platform.runLater(() -> { + addResult(r, numtested); + }); + } else if (n % 100 == 0) { + final int numtested = n; + Platform.runLater(() -> updateProgress(numtested)); + } + } + Platform.runLater(() -> { + System.out.println(progressLabel.getText()); + dumpTest(); + }); + if (exitWhenDone) { + Platform.exit(); + } + } + + static void usage(int code) { + System.out.println("java TestNonAARasterization [-tolerance ] [-warn ]"); + System.out.println(" [-rect|-oval|-octagon|-quad|-cubic|-poly|-bigpoly]"); + System.out.println(" [-j2d|-j2dclip] [-exit]"); + System.exit(code); + } + + public static void main(String argv[]) { + if (argv.length > 0) { + for (int i = 0; i < argv.length; i++) { + String arg = argv[i]; + if (arg.equalsIgnoreCase("-tolerance")) { + if (++i >= argv.length) { + usage(-1); + } + tolerance = Double.parseDouble(argv[i]); + } else if (arg.equalsIgnoreCase("-warn")) { + if (++i >= argv.length) { + usage(-1); + } + warn = Double.parseDouble(argv[i]); + } else if (arg.equalsIgnoreCase("-poly")) { + shapemode = ShapeMode.FIVE_LINE_POLYS; + } else if (arg.equalsIgnoreCase("-bigpoly")) { + shapemode = ShapeMode.NINE_LINE_POLYS; + } else if (arg.equalsIgnoreCase("-rect")) { + shapemode = ShapeMode.RECTANGLES; + } else if (arg.equalsIgnoreCase("-oval")) { + shapemode = ShapeMode.OVALS; + } else if (arg.equalsIgnoreCase("-octagon")) { + shapemode = ShapeMode.OCTAGONS; + } else if (arg.equalsIgnoreCase("-cubic")) { + shapemode = ShapeMode.TWO_CUBICS; + } else if (arg.equalsIgnoreCase("-quad")) { + shapemode = ShapeMode.FOUR_QUADS; + } else if (arg.equalsIgnoreCase("-j2d")) { + useJava2D = true; + } else if (arg.equalsIgnoreCase("-j2dclip")) { + useJava2D = true; + useJava2DClip = true; + } else if (arg.equalsIgnoreCase("-exit")) { + exitWhenDone = true; + } else if (arg.equalsIgnoreCase("-help")) { + usage(0); + } else { + usage(-1); + } + } + } + + if (false) { + // 2018 - lower thresholds: + final Double dec = Double.parseDouble(System.getProperty("sun.java2d.renderer.cubic_dec_d2", "1.0")); + System.out.println("dec bind: "+dec); + + warn = dec / 8; // e = 8 x dec_binD + tolerance = warn * 0.9; + } + + System.out.println("useJava2D: " + useJava2D); + System.out.println("shapemode: " + shapemode); + System.out.println("tolerance: " + tolerance); + System.out.println("warn val : " + warn); + + launch(argv); + } +}