[OpenJDK 2D-Dev] RFR JDK-8184429: Path clipper added in Marlin2D & MarlinFX 0.8.0

Laurent Bourgès bourges.laurent at gmail.com
Fri Sep 1 21:09:18 UTC 2017


Jim,

Here is the current code for my experimental PathClipFilter only to show
you what I have done so far:
- DMarlingRenderingEngine.getAATileGenerator():
...
                // fill shape:
                final PathIterator pi =
norm.getNormalizingPathIterator(rdrCtx,
                                                 s.getPathIterator(_at));

                final int windingRule = pi.getWindingRule();

                // note: Winding rule may be EvenOdd ONLY for fill
operations !
                r = rdrCtx.renderer.init(clip.getLoX(), clip.getLoY(),
                                         clip.getWidth(), clip.getHeight(),
                                         windingRule);

                if (windingRule == WIND_NON_ZERO) {
                    DPathConsumer2D pc2d = r;
                    if (rdrCtx.doClip) {
                        pc2d = rdrCtx.transformerPC2D.pathClipper(pc2d);
                    }

                    // TODO: subdivide quad/cubic curves into monotonic
curves ?
                    pathTo(rdrCtx, pi, pc2d);
                }
- PathClipper:
    static final class GeneralPathClipper implements DPathConsumer2D {

        private DPathConsumer2D out;

        // Bounds of the drawing region, at pixel precision.
        private final double[] clipRect;

        // the outcode of the starting point
        private int sOutCode = 0;

        // the current outcode of the current sub path
        private int cOutCode = 0;

        GeneralPathClipper(final DRendererContext rdrCtx) {
            this.clipRect = rdrCtx.clipRect;
        }

        GeneralPathClipper init(final DPathConsumer2D out) {
            this.out = out;

            // Adjust the clipping rectangle with the renderer offsets
            final double rdrOffX = DRenderer.RDR_OFFSET_X;
            final double rdrOffY = DRenderer.RDR_OFFSET_Y;

            final double[] _clipRect = this.clipRect;
            _clipRect[0] += rdrOffY;
            _clipRect[1] += rdrOffY;
            _clipRect[2] += rdrOffX;
            _clipRect[3] += rdrOffX;

            return this; // fluent API
        }

        @Override
        public void pathDone() {
            out.pathDone();
        }

        @Override
        public void closePath() {
            out.closePath();
            this.cOutCode = sOutCode;
        }

        private void finish() {
            if (outside) {
                this.outside = false;
                out.lineTo(cx0, cy0);
            }
        }

        @Override
        public void moveTo(final double x0, final double y0) {
            final int outcode = DHelpers.outcode(x0, y0, clipRect);
            // basic rejection criteria
            this.sOutCode = outcode;
            this.cOutCode = outcode;
            this.outside = false;
            out.moveTo(x0, y0);
        }

        boolean outside = false;
        double cx0, cy0;

        @Override
        public void lineTo(final double x1, final double y1) {
            final int outcode0 = this.cOutCode;
            final int outcode1 = DHelpers.outcode(x1, y1, clipRect);
            this.cOutCode = outcode1;

            final int sidecode = (outcode0 & outcode1);
            // basic rejection criteria:
            if (sidecode != 0) {
                // top or bottom or same:
                if ((outcode0 == outcode1)
                        || ((sidecode & DHelpers.OUTCODE_MASK_T_B) != 0) ) {
                    // corner or cross-boundary
                    this.outside = true;
                    // TODO: add to outside stack (throw if never enter
again ?)
                    this.cx0 = x1;
                    this.cy0 = y1;
                    return;
                } else {
                    // corner or cross-boundary on left or right side:
                    // TODO: add to outside stack (throw if never enter
again ?)
//                    line(P0-P1)
                }
            }
            finish();
            // clipping disabled:
            out.lineTo(x1, y1);
        }

        @Override
        public void curveTo(final double x1, final double y1,
                            final double x2, final double y2,
                            final double x3, final double y3)
        {
            final int outcode0 = this.cOutCode;
            final int outcode3 = DHelpers.outcode(x3, y3, clipRect);
            this.cOutCode = outcode3;

            // TODO: optimize ?
            final int outcode1 = DHelpers.outcode(x1, y1, clipRect);
            final int outcode2 = DHelpers.outcode(x2, y2, clipRect);

            // basic rejection criteria
            if ((outcode0 & outcode1 & outcode2 & outcode3) != 0) {
                // Different quadrant ?
                if ((outcode0 == outcode3) && (outcode0 == outcode1) &&
(outcode0 == outcode2)) {
                    // corner or cross-boundary
                    this.outside = true;
                    // TODO: add to outside stack (throw if never enter
again ?)
                    this.cx0 = x3;
                    this.cy0 = y3;
                    return;
                } else {
                    // corner or cross-boundary
                    // TODO: add to outside stack (throw if never enter
again ?)
//                    line(P0-P1)

                    finish();
                    // clipping disabled:
                    out.lineTo(x3, y3);
                    return;
                }
            }
            finish();
            // clipping disabled:
            out.curveTo(x1, y1, x2, y2, x3, y3);
        }

        @Override
        public void quadTo(final double x1, final double y1,
                           final double x2, final double y2)
        {
            final int outcode0 = this.cOutCode;
            final int outcode2 = DHelpers.outcode(x2, y2, clipRect);
            this.cOutCode = outcode2;

            // TODO: optimize ?
            final int outcode1 = DHelpers.outcode(x1, y1, clipRect);

            // basic rejection criteria
            if ((outcode0 & outcode1 & outcode2) != 0) {
                // Different quadrant ?
                if ((outcode0 == outcode2) && (outcode0 == outcode1)) {
                    // corner or cross-boundary
                    this.outside = true;
                    // TODO: add to outside stack (throw if never enter
again ?)
                    this.cx0 = x2;
                    this.cy0 = y2;
                    return;
                } else {
                    // corner or cross-boundary
                    // TODO: add to outside stack (throw if never enter
again ?)
//                    line(P0-P1)

                    finish();
                    // clipping disabled:
                    out.lineTo(x2, y2);
                    return;
                }
            }
            finish();
            // clipping disabled:
            out.quadTo(x1, y1, x2, y2);
        }

        @Override
        public long getNativeConsumer() {
            throw new InternalError("Not using a native peer");
        }
    }

Here is a screenshot illustrating the remaining paths in Renderer after
clipping a 4000x4000 spiral converted as stroked shape:
http://cr.openjdk.java.net/~lbourges/png/SpiralTest-dash-false.ser.png

You can see all rounds arround the clip that I expect soon to ignore too as
I plan to use a corner stack to remember turining points until the path
enters again or go back in reverse order...

clip off: ~ 145ms
clip on: ~ 106ms

TODO: handle corner points & turns arround / reverse the clip

Cheers,
Laurent


2017-09-01 22:09 GMT+02:00 Laurent Bourgès <bourges.laurent at gmail.com>:

> Dear Jim,
>
> I am not so good at explaining my early solution for NZ rule in english:
> - it mimics the Stroker approach
> - it skips all intermediate segments outside (except at corners ie
> different outcodes)
> - preserve only the last point of the entering segment
>
> So mostly invisible segments on the left and right sides are skipped from
> the original that makes the Renderer only processing visible segments like
> in the Stroker case where I opened the path.
>
> It improves the performance as less left / right segments are processed in
> addLine(). For now the Renderer accepts all segments and only rejects
> top/bottom parts and keeps all left/right edges in its processing: that
> costs a lot.
>
>
> I think we are discussing two different things.  I thought you were
> considering a separate filter for clipping filled only paths.  I agree that
> the Stroker webrev is still needed for stroked paths.  We should also
> create one for dashed paths that will clip before dashing.  There are
> multiple stages at which we can clip to reduce processing.
>
>
> Exactly I also like the pipeline approach that decouples stages into
> different parts.
>
>
> I take that case into account.  How is a prefilter that excludes those
> segments any simpler than the Renderer rejecting them itself?
>
> Keep in mind that they are already rejected.  Renderer.addLine() already
> does this.  In the case of a polygon, there isn't much code you can reduce
> because it goes from the loop that feeds the path in the rendering engine
> directly to Renderer.move/lineTo() which goes directly to addLine() which
> rejects all of those segments.
>
>
> Not on the left / right side.
> Closed subpaths or extra segments can be ignored for NZ rule = only
> keeping the path closed properly matters.
>
> Will send you the current code to illustrate the basic filter asap.
>
>
> In the case of curves, the path is nearly as direct, but first you run a
> DDA on it to break it into lines.  Simply modifying Renderer.quadTo and
> cubicTo to perform quick-rejection would be a very simple change (certainly
> simpler than adding a new class to do that) and would reject all of those
> curved segments much faster...
>
>
> Agreed, I have done it in the past and can provide the change.
>
> Laurent
>
>


-- 
-- 
Laurent Bourgès


More information about the openjfx-dev mailing list