changeset 55816:13178f7e75d5

8228711: Path rendered incorrectly when it goes outside the clipping region Summary: fixed closePath() to preserve last position and its outcode in Stroker and TransformingPathConsumer2D.PathClipFilter Reviewed-by: prr, kcr
author lbourges
date Wed, 07 Aug 2019 10:25:50 +0200
parents f95327be136a
children 020f8fab32e2
files src/java.desktop/share/classes/sun/java2d/marlin/DDasher.java src/java.desktop/share/classes/sun/java2d/marlin/DHelpers.java src/java.desktop/share/classes/sun/java2d/marlin/DStroker.java src/java.desktop/share/classes/sun/java2d/marlin/DTransformingPathConsumer2D.java src/java.desktop/share/classes/sun/java2d/marlin/Dasher.java src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java src/java.desktop/share/classes/sun/java2d/marlin/TransformingPathConsumer2D.java src/java.desktop/share/classes/sun/java2d/marlin/Version.java test/jdk/sun/java2d/marlin/ClipShapeTest.java
diffstat 10 files changed, 623 insertions(+), 245 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.desktop/share/classes/sun/java2d/marlin/DDasher.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/DDasher.java	Wed Aug 07 10:25:50 2019 +0200
@@ -47,6 +47,8 @@
     static final double CURVE_LEN_ERR = MarlinProperties.getCurveLengthError(); // 0.01 initial
     static final double MIN_T_INC = 1.0d / (1 << REC_LIMIT);
 
+    static final double EPS = 1e-6d;
+
     // More than 24 bits of mantissa means we can no longer accurately
     // measure the number of times cycled through the dash array so we
     // punt and override the phase to just be 0 past that point.
@@ -269,6 +271,9 @@
 
     private void emitSeg(double[] buf, int off, int type) {
         switch (type) {
+        case 4:
+            out.lineTo(buf[off], buf[off + 1]);
+            return;
         case 8:
             out.curveTo(buf[off    ], buf[off + 1],
                         buf[off + 2], buf[off + 3],
@@ -278,9 +283,6 @@
             out.quadTo(buf[off    ], buf[off + 1],
                        buf[off + 2], buf[off + 3]);
             return;
-        case 4:
-            out.lineTo(buf[off], buf[off + 1]);
-            return;
         default:
         }
     }
@@ -361,7 +363,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -416,13 +418,13 @@
         boolean _dashOn = dashOn;
         double _phase = phase;
 
-        double leftInThisDashSegment, d;
+        double leftInThisDashSegment, rem;
 
         while (true) {
-            d = _dash[_idx];
-            leftInThisDashSegment = d - _phase;
+            leftInThisDashSegment = _dash[_idx] - _phase;
+            rem = len - leftInThisDashSegment;
 
-            if (len <= leftInThisDashSegment) {
+            if (rem <= EPS) {
                 _curCurvepts[0] = x1;
                 _curCurvepts[1] = y1;
 
@@ -431,8 +433,8 @@
                 // Advance phase within current dash segment
                 _phase += len;
 
-                // TODO: compare double values using epsilon:
-                if (len == leftInThisDashSegment) {
+                // compare values using epsilon:
+                if (Math.abs(rem) <= EPS) {
                     _phase = 0.0d;
                     _idx = (_idx + 1) % _dashLen;
                     _dashOn = !_dashOn;
@@ -440,17 +442,12 @@
                 break;
             }
 
-            if (_phase == 0.0d) {
-                _curCurvepts[0] = cx0 + d * cx;
-                _curCurvepts[1] = cy0 + d * cy;
-            } else {
-                _curCurvepts[0] = cx0 + leftInThisDashSegment * cx;
-                _curCurvepts[1] = cy0 + leftInThisDashSegment * cy;
-            }
+            _curCurvepts[0] = cx0 + leftInThisDashSegment * cx;
+            _curCurvepts[1] = cy0 + leftInThisDashSegment * cy;
 
             goTo(_curCurvepts, 0, 4, _dashOn);
 
-            len -= leftInThisDashSegment;
+            len = rem;
             // Advance to next dash segment
             _idx = (_idx + 1) % _dashLen;
             _dashOn = !_dashOn;
@@ -506,18 +503,18 @@
             _dashOn = (iterations + (_dashOn ? 1L : 0L) & 1L) == 1L;
         }
 
-        double leftInThisDashSegment, d;
+        double leftInThisDashSegment, rem;
 
         while (true) {
-            d = _dash[_idx];
-            leftInThisDashSegment = d - _phase;
+            leftInThisDashSegment = _dash[_idx] - _phase;
+            rem = len - leftInThisDashSegment;
 
-            if (len <= leftInThisDashSegment) {
+            if (rem <= EPS) {
                 // Advance phase within current dash segment
                 _phase += len;
 
-                // TODO: compare double values using epsilon:
-                if (len == leftInThisDashSegment) {
+                // compare values using epsilon:
+                if (Math.abs(rem) <= EPS) {
                     _phase = 0.0d;
                     _idx = (_idx + 1) % _dashLen;
                     _dashOn = !_dashOn;
@@ -525,7 +522,7 @@
                 break;
             }
 
-            len -= leftInThisDashSegment;
+            len = rem;
             // Advance to next dash segment
             _idx = (_idx + 1) % _dashLen;
             _dashOn = !_dashOn;
@@ -579,7 +576,9 @@
         goTo(_curCurvepts, curCurveoff + 2, type, _dashOn);
 
         _phase += _li.lastSegLen();
-        if (_phase >= _dash[_idx]) {
+
+        // compare values using epsilon:
+        if (_phase + EPS >= _dash[_idx]) {
             _phase = 0.0d;
             _idx = (_idx + 1) % _dashLen;
             _dashOn = !_dashOn;
@@ -938,7 +937,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -1024,7 +1023,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
--- a/src/java.desktop/share/classes/sun/java2d/marlin/DHelpers.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/DHelpers.java	Wed Aug 07 10:25:50 2019 +0200
@@ -243,7 +243,7 @@
         final double y12 = pts[3] - pts[1];
         // if the curve is already parallel to either axis we gain nothing
         // from rotating it.
-        if ((y12 != 0.0d && x12 != 0.0d)) {
+        if ((y12 != 0.0d) && (x12 != 0.0d)) {
             // we rotate it so that the first vector in the control polygon is
             // parallel to the x-axis. This will ensure that rotated quarter
             // circles won't be subdivided.
@@ -764,17 +764,17 @@
                     io.lineTo(_curves[e], _curves[e+1]);
                     e += 2;
                     continue;
-                case TYPE_QUADTO:
-                    io.quadTo(_curves[e],   _curves[e+1],
-                              _curves[e+2], _curves[e+3]);
-                    e += 4;
-                    continue;
                 case TYPE_CUBICTO:
                     io.curveTo(_curves[e],   _curves[e+1],
                                _curves[e+2], _curves[e+3],
                                _curves[e+4], _curves[e+5]);
                     e += 6;
                     continue;
+                case TYPE_QUADTO:
+                    io.quadTo(_curves[e],   _curves[e+1],
+                              _curves[e+2], _curves[e+3]);
+                    e += 4;
+                    continue;
                 default:
                 }
             }
@@ -806,17 +806,17 @@
                     e -= 2;
                     io.lineTo(_curves[e], _curves[e+1]);
                     continue;
-                case TYPE_QUADTO:
-                    e -= 4;
-                    io.quadTo(_curves[e],   _curves[e+1],
-                              _curves[e+2], _curves[e+3]);
-                    continue;
                 case TYPE_CUBICTO:
                     e -= 6;
                     io.curveTo(_curves[e],   _curves[e+1],
                                _curves[e+2], _curves[e+3],
                                _curves[e+4], _curves[e+5]);
                     continue;
+                case TYPE_QUADTO:
+                    e -= 4;
+                    io.quadTo(_curves[e],   _curves[e+1],
+                              _curves[e+2], _curves[e+3]);
+                    continue;
                 default:
                 }
             }
--- a/src/java.desktop/share/classes/sun/java2d/marlin/DStroker.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/DStroker.java	Wed Aug 07 10:25:50 2019 +0200
@@ -540,7 +540,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -634,6 +634,9 @@
         emitReverse();
 
         this.prev = CLOSE;
+        this.cx0 = sx0;
+        this.cy0 = sy0;
+        this.cOutCode = sOutCode;
 
         if (opened) {
             // do not emit close
@@ -668,7 +671,9 @@
         //          i.e. if caps must be drawn or not ?
         // Solution: use the ClosedPathDetector before Stroker to determine
         // if the path is a closed path or not
-        if (!rdrCtx.closedPath) {
+        if (rdrCtx.closedPath) {
+            emitReverse();
+        } else {
             if (outcode == 0) {
                 // current point = end's cap:
                 if (capStyle == CAP_ROUND) {
@@ -693,8 +698,6 @@
                     }
                 }
             }
-        } else {
-            emitReverse();
         }
         emitClose();
     }
@@ -1058,7 +1061,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -1206,7 +1209,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
--- a/src/java.desktop/share/classes/sun/java2d/marlin/DTransformingPathConsumer2D.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/DTransformingPathConsumer2D.java	Wed Aug 07 10:25:50 2019 +0200
@@ -530,6 +530,9 @@
 
         private boolean outside = false;
 
+        // The starting point of the path
+        private double sx0, sy0;
+
         // The current point (TODO stupid repeated info)
         private double cx0, cy0;
 
@@ -630,17 +633,26 @@
             finishPath();
 
             out.closePath();
+
+            // back to starting point:
+            this.cOutCode = DHelpers.outcode(sx0, sy0, clipRect);
+            this.cx0 = sx0;
+            this.cy0 = sy0;
         }
 
         @Override
         public void moveTo(final double x0, final double y0) {
             finishPath();
 
+            out.moveTo(x0, y0);
+
+            // update starting point:
             this.cOutCode = DHelpers.outcode(x0, y0, clipRect);
-            this.outside = false;
-            out.moveTo(x0, y0);
             this.cx0 = x0;
             this.cy0 = y0;
+
+            this.sx0 = x0;
+            this.sy0 = y0;
         }
 
         @Override
@@ -655,7 +667,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -754,7 +766,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -816,7 +828,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -1153,13 +1165,13 @@
 
         @Override
         public void moveTo(double x0, double y0) {
-            log("moveTo (" + x0 + ", " + y0 + ')');
+            log("p.moveTo(" + x0 + ", " + y0 + ");");
             out.moveTo(x0, y0);
         }
 
         @Override
         public void lineTo(double x1, double y1) {
-            log("lineTo (" + x1 + ", " + y1 + ')');
+            log("p.lineTo(" + x1 + ", " + y1 + ");");
             out.lineTo(x1, y1);
         }
 
@@ -1168,25 +1180,26 @@
                             double x2, double y2,
                             double x3, double y3)
         {
-            log("curveTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2  + ") P3(" + x3 + ", " + y3 + ')');
+            log("p.curveTo(" + x1 + ", " + y1 + ", " + x2 + ", " + y2  + ", " + x3 + ", " + y3 + ");");
             out.curveTo(x1, y1, x2, y2, x3, y3);
         }
 
         @Override
-        public void quadTo(double x1, double y1, double x2, double y2) {
-            log("quadTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2  + ')');
+        public void quadTo(double x1, double y1,
+                           double x2, double y2) {
+            log("p.quadTo(" + x1 + ", " + y1 + ", " + x2 + ", " + y2  + ");");
             out.quadTo(x1, y1, x2, y2);
         }
 
         @Override
         public void closePath() {
-            log("closePath");
+            log("p.closePath();");
             out.closePath();
         }
 
         @Override
         public void pathDone() {
-            log("pathDone");
+            log("p.pathDone();");
             out.pathDone();
         }
 
--- a/src/java.desktop/share/classes/sun/java2d/marlin/Dasher.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/Dasher.java	Wed Aug 07 10:25:50 2019 +0200
@@ -48,6 +48,8 @@
     static final float CURVE_LEN_ERR = MarlinProperties.getCurveLengthError(); // 0.01
     static final float MIN_T_INC = 1.0f / (1 << REC_LIMIT);
 
+    static final float EPS = 1e-6f;
+
     // More than 24 bits of mantissa means we can no longer accurately
     // measure the number of times cycled through the dash array so we
     // punt and override the phase to just be 0 past that point.
@@ -270,6 +272,9 @@
 
     private void emitSeg(float[] buf, int off, int type) {
         switch (type) {
+        case 4:
+            out.lineTo(buf[off], buf[off + 1]);
+            return;
         case 8:
             out.curveTo(buf[off    ], buf[off + 1],
                         buf[off + 2], buf[off + 3],
@@ -279,9 +284,6 @@
             out.quadTo(buf[off    ], buf[off + 1],
                        buf[off + 2], buf[off + 3]);
             return;
-        case 4:
-            out.lineTo(buf[off], buf[off + 1]);
-            return;
         default:
         }
     }
@@ -362,7 +364,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -417,13 +419,13 @@
         boolean _dashOn = dashOn;
         float _phase = phase;
 
-        float leftInThisDashSegment, d;
+        float leftInThisDashSegment, rem;
 
         while (true) {
-            d = _dash[_idx];
-            leftInThisDashSegment = d - _phase;
+            leftInThisDashSegment = _dash[_idx] - _phase;
+            rem = len - leftInThisDashSegment;
 
-            if (len <= leftInThisDashSegment) {
+            if (rem <= EPS) {
                 _curCurvepts[0] = x1;
                 _curCurvepts[1] = y1;
 
@@ -432,8 +434,8 @@
                 // Advance phase within current dash segment
                 _phase += len;
 
-                // TODO: compare float values using epsilon:
-                if (len == leftInThisDashSegment) {
+                // compare values using epsilon:
+                if (Math.abs(rem) <= EPS) {
                     _phase = 0.0f;
                     _idx = (_idx + 1) % _dashLen;
                     _dashOn = !_dashOn;
@@ -441,17 +443,12 @@
                 break;
             }
 
-            if (_phase == 0.0f) {
-                _curCurvepts[0] = cx0 + d * cx;
-                _curCurvepts[1] = cy0 + d * cy;
-            } else {
-                _curCurvepts[0] = cx0 + leftInThisDashSegment * cx;
-                _curCurvepts[1] = cy0 + leftInThisDashSegment * cy;
-            }
+            _curCurvepts[0] = cx0 + leftInThisDashSegment * cx;
+            _curCurvepts[1] = cy0 + leftInThisDashSegment * cy;
 
             goTo(_curCurvepts, 0, 4, _dashOn);
 
-            len -= leftInThisDashSegment;
+            len = rem;
             // Advance to next dash segment
             _idx = (_idx + 1) % _dashLen;
             _dashOn = !_dashOn;
@@ -507,18 +504,18 @@
             _dashOn = (iterations + (_dashOn ? 1L : 0L) & 1L) == 1L;
         }
 
-        float leftInThisDashSegment, d;
+        float leftInThisDashSegment, rem;
 
         while (true) {
-            d = _dash[_idx];
-            leftInThisDashSegment = d - _phase;
+            leftInThisDashSegment = _dash[_idx] - _phase;
+            rem = len - leftInThisDashSegment;
 
-            if (len <= leftInThisDashSegment) {
+            if (rem <= EPS) {
                 // Advance phase within current dash segment
                 _phase += len;
 
-                // TODO: compare float values using epsilon:
-                if (len == leftInThisDashSegment) {
+                // compare values using epsilon:
+                if (Math.abs(rem) <= EPS) {
                     _phase = 0.0f;
                     _idx = (_idx + 1) % _dashLen;
                     _dashOn = !_dashOn;
@@ -526,7 +523,7 @@
                 break;
             }
 
-            len -= leftInThisDashSegment;
+            len = rem;
             // Advance to next dash segment
             _idx = (_idx + 1) % _dashLen;
             _dashOn = !_dashOn;
@@ -580,7 +577,9 @@
         goTo(_curCurvepts, curCurveoff + 2, type, _dashOn);
 
         _phase += _li.lastSegLen();
-        if (_phase >= _dash[_idx]) {
+
+        // compare values using epsilon:
+        if (_phase + EPS >= _dash[_idx]) {
             _phase = 0.0f;
             _idx = (_idx + 1) % _dashLen;
             _dashOn = !_dashOn;
@@ -939,7 +938,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -1025,7 +1024,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
--- a/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java	Wed Aug 07 10:25:50 2019 +0200
@@ -251,7 +251,7 @@
         final float y12 = pts[3] - pts[1];
         // if the curve is already parallel to either axis we gain nothing
         // from rotating it.
-        if ((y12 != 0.0f && x12 != 0.0f)) {
+        if ((y12 != 0.0f) && (x12 != 0.0f)) {
             // we rotate it so that the first vector in the control polygon is
             // parallel to the x-axis. This will ensure that rotated quarter
             // circles won't be subdivided.
@@ -772,17 +772,17 @@
                     io.lineTo(_curves[e], _curves[e+1]);
                     e += 2;
                     continue;
-                case TYPE_QUADTO:
-                    io.quadTo(_curves[e],   _curves[e+1],
-                              _curves[e+2], _curves[e+3]);
-                    e += 4;
-                    continue;
                 case TYPE_CUBICTO:
                     io.curveTo(_curves[e],   _curves[e+1],
                                _curves[e+2], _curves[e+3],
                                _curves[e+4], _curves[e+5]);
                     e += 6;
                     continue;
+                case TYPE_QUADTO:
+                    io.quadTo(_curves[e],   _curves[e+1],
+                              _curves[e+2], _curves[e+3]);
+                    e += 4;
+                    continue;
                 default:
                 }
             }
@@ -814,17 +814,17 @@
                     e -= 2;
                     io.lineTo(_curves[e], _curves[e+1]);
                     continue;
-                case TYPE_QUADTO:
-                    e -= 4;
-                    io.quadTo(_curves[e],   _curves[e+1],
-                              _curves[e+2], _curves[e+3]);
-                    continue;
                 case TYPE_CUBICTO:
                     e -= 6;
                     io.curveTo(_curves[e],   _curves[e+1],
                                _curves[e+2], _curves[e+3],
                                _curves[e+4], _curves[e+5]);
                     continue;
+                case TYPE_QUADTO:
+                    e -= 4;
+                    io.quadTo(_curves[e],   _curves[e+1],
+                              _curves[e+2], _curves[e+3]);
+                    continue;
                 default:
                 }
             }
--- a/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java	Wed Aug 07 10:25:50 2019 +0200
@@ -542,7 +542,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -636,6 +636,9 @@
         emitReverse();
 
         this.prev = CLOSE;
+        this.cx0 = sx0;
+        this.cy0 = sy0;
+        this.cOutCode = sOutCode;
 
         if (opened) {
             // do not emit close
@@ -670,7 +673,9 @@
         //          i.e. if caps must be drawn or not ?
         // Solution: use the ClosedPathDetector before Stroker to determine
         // if the path is a closed path or not
-        if (!rdrCtx.closedPath) {
+        if (rdrCtx.closedPath) {
+            emitReverse();
+        } else {
             if (outcode == 0) {
                 // current point = end's cap:
                 if (capStyle == CAP_ROUND) {
@@ -695,8 +700,6 @@
                     }
                 }
             }
-        } else {
-            emitReverse();
         }
         emitClose();
     }
@@ -1060,7 +1063,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -1208,7 +1211,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
--- a/src/java.desktop/share/classes/sun/java2d/marlin/TransformingPathConsumer2D.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/TransformingPathConsumer2D.java	Wed Aug 07 10:25:50 2019 +0200
@@ -531,6 +531,9 @@
 
         private boolean outside = false;
 
+        // The starting point of the path
+        private float sx0, sy0;
+
         // The current point (TODO stupid repeated info)
         private float cx0, cy0;
 
@@ -631,17 +634,26 @@
             finishPath();
 
             out.closePath();
+
+            // back to starting point:
+            this.cOutCode = Helpers.outcode(sx0, sy0, clipRect);
+            this.cx0 = sx0;
+            this.cy0 = sy0;
         }
 
         @Override
         public void moveTo(final float x0, final float y0) {
             finishPath();
 
+            out.moveTo(x0, y0);
+
+            // update starting point:
             this.cOutCode = Helpers.outcode(x0, y0, clipRect);
-            this.outside = false;
-            out.moveTo(x0, y0);
             this.cx0 = x0;
             this.cy0 = y0;
+
+            this.sx0 = x0;
+            this.sy0 = y0;
         }
 
         @Override
@@ -656,7 +668,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -755,7 +767,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -817,7 +829,7 @@
 
                 // basic rejection criteria:
                 if (sideCode == 0) {
-                    // ovelap clip:
+                    // overlap clip:
                     if (subdivide) {
                         // avoid reentrance
                         subdivide = false;
@@ -1154,13 +1166,13 @@
 
         @Override
         public void moveTo(float x0, float y0) {
-            log("moveTo (" + x0 + ", " + y0 + ')');
+            log("p.moveTo(" + x0 + ", " + y0 + ");");
             out.moveTo(x0, y0);
         }
 
         @Override
         public void lineTo(float x1, float y1) {
-            log("lineTo (" + x1 + ", " + y1 + ')');
+            log("p.lineTo(" + x1 + ", " + y1 + ");");
             out.lineTo(x1, y1);
         }
 
@@ -1169,25 +1181,26 @@
                             float x2, float y2,
                             float x3, float y3)
         {
-            log("curveTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2  + ") P3(" + x3 + ", " + y3 + ')');
+            log("p.curveTo(" + x1 + ", " + y1 + ", " + x2 + ", " + y2  + ", " + x3 + ", " + y3 + ");");
             out.curveTo(x1, y1, x2, y2, x3, y3);
         }
 
         @Override
-        public void quadTo(float x1, float y1, float x2, float y2) {
-            log("quadTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2  + ')');
+        public void quadTo(float x1, float y1,
+                           float x2, float y2) {
+            log("p.quadTo(" + x1 + ", " + y1 + ", " + x2 + ", " + y2  + ");");
             out.quadTo(x1, y1, x2, y2);
         }
 
         @Override
         public void closePath() {
-            log("closePath");
+            log("p.closePath();");
             out.closePath();
         }
 
         @Override
         public void pathDone() {
-            log("pathDone");
+            log("p.pathDone();");
             out.pathDone();
         }
 
--- a/src/java.desktop/share/classes/sun/java2d/marlin/Version.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/src/java.desktop/share/classes/sun/java2d/marlin/Version.java	Wed Aug 07 10:25:50 2019 +0200
@@ -27,7 +27,7 @@
 
 public final class Version {
 
-    private static final String VERSION = "marlin-0.9.1.1-Unsafe-OpenJDK";
+    private static final String VERSION = "marlin-0.9.1.2-Unsafe-OpenJDK";
 
     public static String getVersion() {
         return VERSION;
--- a/test/jdk/sun/java2d/marlin/ClipShapeTest.java	Tue Aug 06 00:16:38 2019 -0700
+++ b/test/jdk/sun/java2d/marlin/ClipShapeTest.java	Wed Aug 07 10:25:50 2019 +0200
@@ -24,11 +24,14 @@
 import java.awt.Color;
 import java.awt.Graphics2D;
 import java.awt.RenderingHints;
+import java.awt.Stroke;
 import java.awt.Shape;
-import java.awt.Stroke;
+import java.awt.geom.CubicCurve2D;
 import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
 import java.awt.geom.Path2D;
 import java.awt.geom.PathIterator;
+import java.awt.geom.QuadCurve2D;
 import java.awt.image.BufferedImage;
 import java.awt.image.DataBufferInt;
 import java.io.File;
@@ -69,28 +72,31 @@
 */
 public final class ClipShapeTest {
 
-    static boolean TX_SCALE = false;
-    static boolean TX_SHEAR = false;
+    // test options:
+    static int NUM_TESTS;
+
+    // shape settings:
+    static ShapeMode SHAPE_MODE;
+
+    static boolean USE_DASHES;
+    static boolean USE_VAR_STROKE;
+
+    static int THRESHOLD_DELTA;
+    static long THRESHOLD_NBPIX;
+
+    // constants:
+    static final boolean DO_FAIL = Boolean.valueOf(System.getProperty("ClipShapeTest.fail", "true"));
 
     static final boolean TEST_STROKER = true;
     static final boolean TEST_FILLER = true;
 
-    // complementary tests in slow mode:
-    static boolean USE_DASHES = false;
-    static boolean USE_VAR_STROKE = false;
+    static final boolean SUBDIVIDE_CURVE = true;
+    static final double SUBDIVIDE_LEN_TH = 50.0;
+    static final boolean TRACE_SUBDIVIDE_CURVE = false;
 
-    static int NUM_TESTS = 5000;
     static final int TESTW = 100;
     static final int TESTH = 100;
 
-    // shape settings:
-    static ShapeMode SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
-
-    static int THRESHOLD_DELTA;
-    static long THRESHOLD_NBPIX;
-
-    static final boolean SHAPE_REPEAT = true;
-
     // dump path on console:
     static final boolean DUMP_SHAPE = true;
 
@@ -103,7 +109,8 @@
     static final int MAX_SAVE_FRAMES = 100;
 
     // use fixed seed to reproduce always same polygons between tests
-    static final boolean FIXED_SEED = false;
+    static final boolean FIXED_SEED = true;
+
     static final double RAND_SCALE = 3.0;
     static final double RANDW = TESTW * RAND_SCALE;
     static final double OFFW = (TESTW - RANDW) / 2.0;
@@ -126,6 +133,7 @@
     static final File OUTPUT_DIR = new File(".");
 
     static final AtomicBoolean isMarlin = new AtomicBoolean();
+    static final AtomicBoolean isMarlinFloat = new AtomicBoolean();
     static final AtomicBoolean isClipRuntime = new AtomicBoolean();
 
     static {
@@ -143,6 +151,7 @@
                     // last space to avoid matching other settings:
                     if (msg.startsWith("sun.java2d.renderer ")) {
                         isMarlin.set(msg.contains("MarlinRenderingEngine"));
+                        isMarlinFloat.set(!msg.contains("DMarlinRenderingEngine"));
                     }
                     if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) {
                         isClipRuntime.set(msg.contains("true"));
@@ -186,12 +195,22 @@
         // curve length max error:
         System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4");
 
+        // cubic min/max error:
+        System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3");
+        System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4");
+
         // quad max error:
         System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4");
+    }
 
-        // cubic min/max error:
-        System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3");
-        System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4"); // or disabled ~ 1e-6
+    private static void resetOptions() {
+        NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 5000);
+
+        // shape settings:
+        SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
+
+        USE_DASHES = false;
+        USE_VAR_STROKE = false;
     }
 
     /**
@@ -199,79 +218,119 @@
      * @param args
      */
     public static void main(String[] args) {
+        {
+            // Bootstrap: init Renderer now:
+            final BufferedImage img = newImage(TESTW, TESTH);
+            final Graphics2D g2d = initialize(img, null);
+
+            try {
+                paintShape(new Line2D.Double(0,0,100,100), g2d, true, false);
+            } finally {
+                g2d.dispose();
+            }
+
+            if (!isMarlin.get()) {
+                throw new RuntimeException("Marlin renderer not used at runtime !");
+            }
+            if (!isClipRuntime.get()) {
+                throw new RuntimeException("Marlin clipping not enabled at runtime !");
+            }
+        }
+
+        System.out.println("---------------------------------------");
+        System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH);
+
+        resetOptions();
+
         boolean runSlowTests = false;
 
         for (String arg : args) {
             if ("-slow".equals(arg)) {
-                System.out.println("slow: enabled.");
                 runSlowTests = true;
-            } else if ("-doScale".equals(arg)) {
-                System.out.println("doScale: enabled.");
-                TX_SCALE = true;
-            } else if ("-doShear".equals(arg)) {
-                System.out.println("doShear: enabled.");
-                TX_SHEAR = true;
             } else if ("-doDash".equals(arg)) {
-                System.out.println("doDash: enabled.");
                 USE_DASHES = true;
             } else if ("-doVarStroke".equals(arg)) {
-                System.out.println("doVarStroke: enabled.");
                 USE_VAR_STROKE = true;
-            }
-            // shape mode:
-            else if (arg.equalsIgnoreCase("-poly")) {
-                SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
-            } else if (arg.equalsIgnoreCase("-bigpoly")) {
-                SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS;
-            } else if (arg.equalsIgnoreCase("-quad")) {
-                SHAPE_MODE = ShapeMode.FOUR_QUADS;
-            } else if (arg.equalsIgnoreCase("-cubic")) {
-                SHAPE_MODE = ShapeMode.TWO_CUBICS;
-            } else if (arg.equalsIgnoreCase("-mixed")) {
-                SHAPE_MODE = ShapeMode.MIXED;
+            } else {
+                // shape mode:
+                if (arg.equalsIgnoreCase("-poly")) {
+                    SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
+                } else if (arg.equalsIgnoreCase("-bigpoly")) {
+                    SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS;
+                } else if (arg.equalsIgnoreCase("-quad")) {
+                    SHAPE_MODE = ShapeMode.FOUR_QUADS;
+                } else if (arg.equalsIgnoreCase("-cubic")) {
+                    SHAPE_MODE = ShapeMode.TWO_CUBICS;
+                } else if (arg.equalsIgnoreCase("-mixed")) {
+                    SHAPE_MODE = ShapeMode.MIXED;
+                }
             }
         }
 
         System.out.println("Shape mode: " + SHAPE_MODE);
 
         // adjust image comparison thresholds:
-        switch(SHAPE_MODE) {
+        switch (SHAPE_MODE) {
             case TWO_CUBICS:
                 // Define uncertainty for curves:
-                THRESHOLD_DELTA = 32; //  / 256
-                THRESHOLD_NBPIX = 128; //  / 10000
+                THRESHOLD_DELTA = 32;
+                THRESHOLD_NBPIX = (USE_DASHES) ? 50 : 200;
+                if (SUBDIVIDE_CURVE) {
+                    THRESHOLD_NBPIX = 4;
+                }
                 break;
             case FOUR_QUADS:
             case MIXED:
                 // Define uncertainty for quads:
                 // curve subdivision causes curves to be smaller
                 // then curve offsets are different (more accurate)
-                THRESHOLD_DELTA = 64;  // 64 / 256
-                THRESHOLD_NBPIX = 256; // 256 / 10000
+                THRESHOLD_DELTA = 64;
+                THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 420;
+                if (SUBDIVIDE_CURVE) {
+                    THRESHOLD_NBPIX = 10;
+                }
                 break;
             default:
                 // Define uncertainty for lines:
                 // float variant have higher uncertainty
-                THRESHOLD_DELTA = 8;
-                THRESHOLD_NBPIX = 8;
+                THRESHOLD_DELTA = 2;
+                THRESHOLD_NBPIX = (USE_DASHES) ?
+                    // float variant have higher uncertainty
+                    ((isMarlinFloat.get()) ? 30 : 6) // low for double
+                    : (isMarlinFloat.get()) ? 10 : 0;
         }
 
-        System.out.println("THRESHOLD_DELTA: "+THRESHOLD_DELTA);
-        System.out.println("THRESHOLD_NBPIX: "+THRESHOLD_NBPIX);
+// Visual inspection (low threshold):
+//        THRESHOLD_NBPIX = 2;
+
+        System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA);
+        System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX);
 
         if (runSlowTests) {
             NUM_TESTS = 10000; // or 100000 (very slow)
-            USE_DASHES = true;
             USE_VAR_STROKE = true;
         }
 
-        System.out.println("ClipShapeTests: image = " + TESTW + " x " + TESTH);
+        System.out.println("NUM_TESTS: " + NUM_TESTS);
+
+        if (USE_DASHES) {
+            System.out.println("USE_DASHES: enabled.");
+        }
+        if (USE_VAR_STROKE) {
+            System.out.println("USE_VAR_STROKE: enabled.");
+        }
+        if (!DO_FAIL) {
+            System.out.println("DO_FAIL: disabled.");
+        }
+
+        System.out.println("---------------------------------------");
+
+        final DiffContext allCtx = new DiffContext("All Test setups");
+        final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)");
 
         int failures = 0;
         final long start = System.nanoTime();
         try {
-            // TODO: test affine transforms ?
-
             if (TEST_STROKER) {
                 final float[][] dashArrays = (USE_DASHES) ?
 // small
@@ -291,7 +350,7 @@
 
                 int nsw = 0;
                 if (USE_VAR_STROKE) {
-                    for (float width = 0.1f; width < 110f; width *= 5f) {
+                    for (float width = 0.25f; width < 110f; width *= 5f) {
                         strokeWidths[nsw++] = width;
                     }
                 } else {
@@ -310,8 +369,8 @@
 
                             for (int join = 0; join <= 2; join++) {
 
-                                failures += paintPaths(new TestSetup(SHAPE_MODE, false, width, cap, join, dashes));
-                                failures += paintPaths(new TestSetup(SHAPE_MODE, true, width, cap, join, dashes));
+                                failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes));
+                                failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes));
                             }
                         }
                     }
@@ -320,29 +379,26 @@
 
             if (TEST_FILLER) {
                 // Filler tests:
-                failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO));
-                failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO));
+                failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO));
+                failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO));
 
-                failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD));
-                failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD));
+                failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD));
+                failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD));
             }
         } catch (IOException ioe) {
             throw new RuntimeException(ioe);
         }
         System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
 
-        if (!isMarlin.get()) {
-            throw new RuntimeException("Marlin renderer not used at runtime !");
-        }
-        if (!isClipRuntime.get()) {
-            throw new RuntimeException("Marlin clipping not enabled at runtime !");
-        }
-        if (failures != 0) {
+        allWorstCtx.dump();
+        allCtx.dump();
+
+        if (DO_FAIL && (failures != 0)) {
             throw new RuntimeException("Clip test failures : " + failures);
         }
     }
 
-    static int paintPaths(final TestSetup ts) throws IOException {
+    static int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException {
         final long start = System.nanoTime();
 
         if (FIXED_SEED) {
@@ -356,19 +412,24 @@
         final boolean fill = !ts.isStroke();
         final Path2D p2d = new Path2D.Double(ts.windingRule);
 
+        final Stroke stroke = (!fill) ? createStroke(ts) : null;
+
         final BufferedImage imgOn = newImage(TESTW, TESTH);
-        final Graphics2D g2dOn = initialize(imgOn, ts);
+        final Graphics2D g2dOn = initialize(imgOn, stroke);
 
         final BufferedImage imgOff = newImage(TESTW, TESTH);
-        final Graphics2D g2dOff = initialize(imgOff, ts);
+        final Graphics2D g2dOff = initialize(imgOff, stroke);
 
         final BufferedImage imgDiff = newImage(TESTW, TESTH);
 
-        final DiffContext globalCtx = new DiffContext("All tests");
+        final DiffContext testSetupCtx = new DiffContext("Test setup");
+        final DiffContext testWorstCtx = new DiffContext("Worst");
+        final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)");
 
         int nd = 0;
         try {
             final DiffContext testCtx = new DiffContext("Test");
+            final DiffContext testThCtx = new DiffContext("Test(>threshold)");
             BufferedImage diffImage;
 
             for (int n = 0; n < NUM_TESTS; n++) {
@@ -381,15 +442,24 @@
                 paintShape(p2d, g2dOn, fill, true);
 
                 /* compute image difference if possible */
-                diffImage = computeDiffImage(testCtx, imgOn, imgOff, imgDiff, globalCtx);
+                diffImage = computeDiffImage(testCtx, testThCtx, imgOn, imgOff, imgDiff);
 
-                final String testName = "Setup_" + ts.id + "_test_" + n;
-
+                // Worst (total)
+                if (testCtx.isDiff()) {
+                    if (testWorstCtx.isWorse(testCtx, false)) {
+                        testWorstCtx.set(testCtx);
+                    }
+                    if (testWorstThCtx.isWorse(testCtx, true)) {
+                        testWorstThCtx.set(testCtx);
+                    }
+                    // accumulate data:
+                    testSetupCtx.add(testCtx);
+                }
                 if (diffImage != null) {
                     nd++;
 
-                    final double ratio = (100.0 * testCtx.histPix.count) / testCtx.histAll.count;
-                    System.out.println("Diff ratio: " + testName + " = " + trimTo3Digits(ratio) + " %");
+                    testThCtx.dump();
+                    testCtx.dump();
 
                     if (nd < MAX_SHOW_FRAMES) {
                         if (SHOW_DETAILS) {
@@ -401,9 +471,12 @@
                             if (DUMP_SHAPE) {
                                 dumpShape(p2d);
                             }
+
+                            final String testName = "Setup_" + ts.id + "_test_" + n;
+
                             saveImage(imgOff, OUTPUT_DIR, testName + "-off.png");
                             saveImage(imgOn, OUTPUT_DIR, testName + "-on.png");
-                            saveImage(diffImage, OUTPUT_DIR, testName + "-diff.png");
+                            saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png");
                         }
                     }
                 }
@@ -418,13 +491,25 @@
                         + " ratio = " + (100f * nd) / NUM_TESTS + " %");
             }
 
-            globalCtx.dump();
+            if (testWorstCtx.isDiff()) {
+                testWorstCtx.dump();
+                if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) {
+                    testWorstThCtx.dump();
+                }
+                if (allWorstCtx.isWorse(testWorstThCtx, true)) {
+                    allWorstCtx.set(testWorstThCtx);
+                }
+            }
+            testSetupCtx.dump();
+
+            // accumulate data:
+            allCtx.add(testSetupCtx);
         }
         System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
         return nd;
     }
 
-    private static void paintShape(final Path2D p2d, final Graphics2D g2d,
+    private static void paintShape(final Shape p2d, final Graphics2D g2d,
                                    final boolean fill, final boolean clip) {
         reset(g2d);
 
@@ -438,26 +523,20 @@
     }
 
     private static Graphics2D initialize(final BufferedImage img,
-                                         final TestSetup ts) {
+                                         final Stroke s) {
         final Graphics2D g2d = (Graphics2D) img.getGraphics();
         g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
                 RenderingHints.VALUE_RENDER_QUALITY);
         g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
-                RenderingHints.VALUE_STROKE_PURE);
+// Test normalize:
+//                RenderingHints.VALUE_STROKE_NORMALIZE
+                RenderingHints.VALUE_STROKE_PURE
+        );
 
-        if (ts.isStroke()) {
-            g2d.setStroke(createStroke(ts));
+        if (s != null) {
+            g2d.setStroke(s);
         }
-        g2d.setColor(Color.GRAY);
-
-        // Test scale
-        if (TX_SCALE) {
-            g2d.scale(1.2, 1.2);
-        }
-        // Test shear
-        if (TX_SHEAR) {
-            g2d.shear(0.1, 0.2);
-        }
+        g2d.setColor(Color.BLACK);
 
         return g2d;
     }
@@ -482,20 +561,35 @@
     static void genShape(final Path2D p2d, final TestSetup ts) {
         p2d.reset();
 
-        final int end = (SHAPE_REPEAT) ? 2 : 1;
+        /*
+            Test closed path:
+            0: moveTo + (draw)To + closePath
+            1: (draw)To + closePath (closePath + (draw)To sequence)
+        */
+        final int end  = (ts.closed) ? 2 : 1;
+
+        final double[] in = new double[8];
+
+        double sx0 = 0.0, sy0 = 0.0, x0 = 0.0, y0 = 0.0;
 
         for (int p = 0; p < end; p++) {
-            p2d.moveTo(randX(), randY());
+            if (p <= 0) {
+                x0 = randX(); y0 = randY();
+                p2d.moveTo(x0, y0);
+                sx0 = x0; sy0 = y0;
+            }
 
             switch (ts.shapeMode) {
                 case MIXED:
+                case FIVE_LINE_POLYS:
+                case NINE_LINE_POLYS:
                 case FIFTY_LINE_POLYS:
-                case NINE_LINE_POLYS:
-                case FIVE_LINE_POLYS:
                     p2d.lineTo(randX(), randY());
                     p2d.lineTo(randX(), randY());
                     p2d.lineTo(randX(), randY());
                     p2d.lineTo(randX(), randY());
+                    x0 = randX(); y0 = randY();
+                    p2d.lineTo(x0, y0);
                     if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) {
                         // And an implicit close makes 5 lines
                         break;
@@ -503,29 +597,75 @@
                     p2d.lineTo(randX(), randY());
                     p2d.lineTo(randX(), randY());
                     p2d.lineTo(randX(), randY());
-                    p2d.lineTo(randX(), randY());
+                    x0 = randX(); y0 = randY();
+                    p2d.lineTo(x0, y0);
                     if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) {
                         // And an implicit close makes 9 lines
                         break;
                     }
                     if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) {
                         for (int i = 0; i < 41; i++) {
-                            p2d.lineTo(randX(), randY());
+                            x0 = randX(); y0 = randY();
+                            p2d.lineTo(x0, y0);
                         }
                         // And an implicit close makes 50 lines
                         break;
                     }
                 case TWO_CUBICS:
-                    p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY());
-                    p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY());
+                    if (SUBDIVIDE_CURVE) {
+                        in[0] = x0; in[1] = y0;
+                        in[2] = randX(); in[3] = randY();
+                        in[4] = randX(); in[5] = randY();
+                        x0 = randX(); y0 = randY();
+                        in[6] = x0; in[7] = y0;
+                        subdivide(p2d, 8, in);
+                        in[0] = x0; in[1] = y0;
+                        in[2] = randX(); in[3] = randY();
+                        in[4] = randX(); in[5] = randY();
+                        x0 = randX(); y0 = randY();
+                        in[6] = x0; in[7] = y0;
+                        subdivide(p2d, 8, in);
+                    } else {
+                        x0 = randX(); y0 = randY();
+                        p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);
+                        x0 = randX(); y0 = randY();
+                        p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);
+                    }
                     if (ts.shapeMode == ShapeMode.TWO_CUBICS) {
                         break;
                     }
                 case FOUR_QUADS:
-                    p2d.quadTo(randX(), randY(), randX(), randY());
-                    p2d.quadTo(randX(), randY(), randX(), randY());
-                    p2d.quadTo(randX(), randY(), randX(), randY());
-                    p2d.quadTo(randX(), randY(), randX(), randY());
+                    if (SUBDIVIDE_CURVE) {
+                        in[0] = x0; in[1] = y0;
+                        in[2] = randX(); in[3] = randY();
+                        x0 = randX(); y0 = randY();
+                        in[4] = x0; in[5] = y0;
+                        subdivide(p2d, 6, in);
+                        in[0] = x0; in[1] = y0;
+                        in[2] = randX(); in[3] = randY();
+                        x0 = randX(); y0 = randY();
+                        in[4] = x0; in[5] = y0;
+                        subdivide(p2d, 6, in);
+                        in[0] = x0; in[1] = y0;
+                        in[2] = randX(); in[3] = randY();
+                        x0 = randX(); y0 = randY();
+                        in[4] = x0; in[5] = y0;
+                        subdivide(p2d, 6, in);
+                        in[0] = x0; in[1] = y0;
+                        in[2] = randX(); in[3] = randY();
+                        x0 = randX(); y0 = randY();
+                        in[4] = x0; in[5] = y0;
+                        subdivide(p2d, 6, in);
+                    } else {
+                        x0 = randX(); y0 = randY();
+                        p2d.quadTo(randX(), randY(), x0, y0);
+                        x0 = randX(); y0 = randY();
+                        p2d.quadTo(randX(), randY(), x0, y0);
+                        x0 = randX(); y0 = randY();
+                        p2d.quadTo(randX(), randY(), x0, y0);
+                        x0 = randX(); y0 = randY();
+                        p2d.quadTo(randX(), randY(), x0, y0);
+                    }
                     if (ts.shapeMode == ShapeMode.FOUR_QUADS) {
                         break;
                     }
@@ -534,6 +674,111 @@
 
             if (ts.closed) {
                 p2d.closePath();
+                x0 = sx0; y0 = sy0;
+            }
+        }
+    }
+
+    static final int SUBDIVIDE_LIMIT = 5;
+    static final double[][] SUBDIVIDE_CURVES = new double[SUBDIVIDE_LIMIT + 1][];
+
+    static {
+        for (int i = 0, n = 1; i < SUBDIVIDE_LIMIT; i++, n *= 2) {
+            SUBDIVIDE_CURVES[i] = new double[8 * n];
+        }
+    }
+
+    static void subdivide(final Path2D p2d, final int type, final double[] in) {
+        if (TRACE_SUBDIVIDE_CURVE) {
+            System.out.println("subdivide: " + Arrays.toString(Arrays.copyOf(in, type)));
+        }
+
+        double curveLen = ((type == 8)
+                ? curvelen(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7])
+                : quadlen(in[0], in[1], in[2], in[3], in[4], in[5]));
+
+        if (curveLen > SUBDIVIDE_LEN_TH) {
+            if (TRACE_SUBDIVIDE_CURVE) {
+                System.out.println("curvelen: " + curveLen);
+            }
+
+            System.arraycopy(in, 0, SUBDIVIDE_CURVES[0], 0, 8);
+
+            int level = 0;
+            while (curveLen >= SUBDIVIDE_LEN_TH) {
+                level++;
+                curveLen /= 2.0;
+                if (TRACE_SUBDIVIDE_CURVE) {
+                    System.out.println("curvelen: " + curveLen);
+                }
+            }
+
+            if (TRACE_SUBDIVIDE_CURVE) {
+                System.out.println("level: " + level);
+            }
+
+            if (level > SUBDIVIDE_LIMIT) {
+                if (TRACE_SUBDIVIDE_CURVE) {
+                    System.out.println("max level reached : " + level);
+                }
+                level = SUBDIVIDE_LIMIT;
+            }
+
+            for (int l = 0; l < level; l++) {
+                if (TRACE_SUBDIVIDE_CURVE) {
+                    System.out.println("level: " + l);
+                }
+
+                double[] src = SUBDIVIDE_CURVES[l];
+                double[] dst = SUBDIVIDE_CURVES[l + 1];
+
+                for (int i = 0, j = 0; i < src.length; i += 8, j += 16) {
+                    if (TRACE_SUBDIVIDE_CURVE) {
+                        System.out.println("subdivide: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));
+                    }
+                    if (type == 8) {
+                        CubicCurve2D.subdivide(src, i, dst, j, dst, j + 8);
+                    } else {
+                        QuadCurve2D.subdivide(src, i, dst, j, dst, j + 8);
+                    }
+                    if (TRACE_SUBDIVIDE_CURVE) {
+                        System.out.println("left: " + Arrays.toString(Arrays.copyOfRange(dst, j, j + type)));
+                        System.out.println("right: " + Arrays.toString(Arrays.copyOfRange(dst, j + 8, j + 8 + type)));
+                    }
+                }
+            }
+
+            // Emit curves at last level:
+            double[] src = SUBDIVIDE_CURVES[level];
+
+            double len = 0.0;
+
+            for (int i = 0; i < src.length; i += 8) {
+                if (TRACE_SUBDIVIDE_CURVE) {
+                    System.out.println("curve: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));
+                }
+
+                if (type == 8) {
+                    if (TRACE_SUBDIVIDE_CURVE) {
+                        len += curvelen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);
+                    }
+                    p2d.curveTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);
+                } else {
+                    if (TRACE_SUBDIVIDE_CURVE) {
+                        len += quadlen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5]);
+                    }
+                    p2d.quadTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5]);
+                }
+            }
+
+            if (TRACE_SUBDIVIDE_CURVE) {
+                System.out.println("curveLen (final) = " + len);
+            }
+        } else {
+            if (type == 8) {
+                p2d.curveTo(in[2], in[3], in[4], in[5], in[6], in[7]);
+            } else {
+                p2d.quadTo(in[2], in[3], in[4], in[5]);
             }
         }
     }
@@ -754,18 +999,19 @@
         return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
     }
 
-    public static BufferedImage computeDiffImage(final DiffContext localCtx,
+    public static BufferedImage computeDiffImage(final DiffContext testCtx,
+                                                 final DiffContext testThCtx,
                                                  final BufferedImage tstImage,
                                                  final BufferedImage refImage,
-                                                 final BufferedImage diffImage,
-                                                 final DiffContext globalCtx) {
+                                                 final BufferedImage diffImage) {
 
         final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData();
         final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData();
         final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData();
 
-        // reset local diff context:
-        localCtx.reset();
+        // reset diff contexts:
+        testCtx.reset();
+        testThCtx.reset();
 
         int ref, tst, dg, v;
         for (int i = 0, len = aRefPix.length; i < len; i++) {
@@ -777,24 +1023,24 @@
 
             // max difference on grayscale values:
             v = (int) Math.ceil(Math.abs(dg / 3.0));
-
-// TODO: count warnings
             if (v <= THRESHOLD_DELTA) {
                 aDifPix[i] = 0;
             } else {
                 aDifPix[i] = toInt(v, v, v);
+                testThCtx.add(v);
+            }
 
-                localCtx.add(v);
+            if (v != 0) {
+                testCtx.add(v);
             }
-            globalCtx.add(v);
         }
 
-        if (!localCtx.isDiff() || (localCtx.histPix.count <= THRESHOLD_NBPIX)) {
+        testCtx.addNbPix(testThCtx.histPix.count);
+
+        if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) {
             return null;
         }
 
-        localCtx.dump();
-
         return diffImage;
     }
 
@@ -895,6 +1141,17 @@
             }
         }
 
+        void add(StatInteger stat) {
+            count += stat.count;
+            sum += stat.sum;
+            if (stat.min < min) {
+                min = stat.min;
+            }
+            if (stat.max > max) {
+                max = stat.max;
+            }
+        }
+
         public final double average() {
             return ((double) sum) / count;
         }
@@ -908,8 +1165,11 @@
 
         public final StringBuilder toString(final StringBuilder sb) {
             sb.append(name).append("[n: ").append(count);
-            sb.append("] sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average()));
-            sb.append(" [").append(min).append(" | ").append(max).append("]");
+            sb.append("] ");
+            if (count != 0) {
+                sb.append("sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average()));
+                sb.append(" [").append(min).append(" | ").append(max).append("]");
+            }
             return sb;
         }
 
@@ -921,6 +1181,7 @@
         static final int MAX = 20;
         static final int LAST = MAX - 1;
         static final int[] STEPS = new int[MAX];
+        static final int BUCKET_TH;
 
         static {
             STEPS[0] = 0;
@@ -930,6 +1191,12 @@
                 STEPS[i] = STEPS[i - 1] * BUCKET;
             }
 //            System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS));
+
+            if (THRESHOLD_DELTA % 2 != 0) {
+                throw new IllegalStateException("THRESHOLD_DELTA must be odd");
+            }
+
+            BUCKET_TH = bucket(THRESHOLD_DELTA);
         }
 
         static int bucket(int val) {
@@ -969,6 +1236,37 @@
             add((int) val);
         }
 
+        void add(Histogram hist) {
+            super.add(hist);
+            for (int i = 0; i < MAX; i++) {
+                stats[i].add(hist.stats[i]);
+            }
+        }
+
+        boolean isWorse(Histogram hist, boolean useTh) {
+            boolean worst = false;
+            if (!useTh && (hist.sum > sum)) {
+                worst = true;
+            } else {
+                long sumLoc = 0l;
+                long sumHist = 0l;
+                // use running sum:
+                for (int i = MAX - 1; i >= BUCKET_TH; i--) {
+                    sumLoc += stats[i].sum;
+                    sumHist += hist.stats[i].sum;
+                }
+                if (sumHist > sumLoc) {
+                    worst = true;
+                }
+            }
+            /*
+            System.out.println("running sum worst:");
+            System.out.println("this ? " + toString());
+            System.out.println("worst ? " + hist.toString());
+             */
+            return worst;
+        }
+
         @Override
         public final String toString() {
             final StringBuilder sb = new StringBuilder(2048);
@@ -995,38 +1293,88 @@
 
     static final class DiffContext {
 
-        public final Histogram histAll;
         public final Histogram histPix;
 
+        public final StatInteger nbPix;
+
         DiffContext(String name) {
-            histAll = new Histogram("All  Pixels [" + name + "]");
             histPix = new Histogram("Diff Pixels [" + name + "]");
+            nbPix = new StatInteger("NbPixels [" + name + "]");
         }
 
         void reset() {
-            histAll.reset();
             histPix.reset();
+            nbPix.reset();
         }
 
         void dump() {
             if (isDiff()) {
-                System.out.println("Differences [" + histAll.name + "]:");
-                System.out.println("Total [all pixels]:\n" + histAll.toString());
-                System.out.println("Total [different pixels]:\n" + histPix.toString());
+                System.out.println("Differences [" + histPix.name + "]:\n"
+                        + ((nbPix.count != 0) ? (nbPix.toString() + "\n") : "")
+                        + histPix.toString()
+                );
             } else {
-                System.out.println("No difference for [" + histAll.name + "].");
+                System.out.println("No difference for [" + histPix.name + "].");
             }
         }
 
         void add(int val) {
-            histAll.add(val);
-            if (val != 0) {
-                histPix.add(val);
+            histPix.add(val);
+        }
+
+        void add(DiffContext ctx) {
+            histPix.add(ctx.histPix);
+            if (ctx.nbPix.count != 0L) {
+                nbPix.add(ctx.nbPix);
             }
         }
 
+        void addNbPix(long val) {
+            if (val != 0L) {
+                nbPix.add(val);
+            }
+        }
+
+        void set(DiffContext ctx) {
+            reset();
+            add(ctx);
+        }
+
+        boolean isWorse(DiffContext ctx, boolean useTh) {
+            return histPix.isWorse(ctx.histPix, useTh);
+        }
+
         boolean isDiff() {
-            return histAll.sum != 0l;
+            return histPix.sum != 0l;
         }
     }
+
+
+    static double linelen(final double x0, final double y0,
+                          final double x1, final double y1)
+    {
+        final double dx = x1 - x0;
+        final double dy = y1 - y0;
+        return Math.sqrt(dx * dx + dy * dy);
+    }
+
+    static double quadlen(final double x0, final double y0,
+                          final double x1, final double y1,
+                          final double x2, final double y2)
+    {
+        return (linelen(x0, y0, x1, y1)
+                + linelen(x1, y1, x2, y2)
+                + linelen(x0, y0, x2, y2)) / 2.0d;
+    }
+
+    static double curvelen(final double x0, final double y0,
+                           final double x1, final double y1,
+                           final double x2, final double y2,
+                           final double x3, final double y3)
+    {
+        return (linelen(x0, y0, x1, y1)
+              + linelen(x1, y1, x2, y2)
+              + linelen(x2, y2, x3, y3)
+              + linelen(x0, y0, x3, y3)) / 2.0d;
+    }
 }