[OpenJDK 2D-Dev] RFR JDK-8184429: Path clipper added in Marlin2D & MarlinFX 0.8.0
Laurent Bourgès
bourges.laurent at gmail.com
Wed Sep 6 08:25:50 UTC 2017
Hi all,
As Jim is no more available to review & answer my questions on java2d /
computer graphics, I need another reviewer on this webrev implementing path
clipping in Marlin (huge potential gains).
Do you know someone else who can help me in the 2d / prism fields to
improve the Marlin renderer even more ?
Thanks,
Laurent
Le 5 sept. 2017 22:41, "Laurent Bourgès" <bourges.laurent at gmail.com> a
écrit :
> Hi Jim,
>
> I made good progress on my PathClipFilter that works now perfectly with
> many test maps for the NZ rule (EO has artefacts if I enable the filter in
> that case).
>
> Here is the updated code below to illustrate the approach:
> - use a new IndexStack in (D)Helpers to store only corner indexes (0/1 for
> Top/Bottom Left, 2/3 for Top/Bottom Right)
> - when the segment is out, I now check (L/R case) if the segment ends have
> different outcodes to insert needed corners that can be removed later if a
> segment does the same in the reverse order (same repeated corner is
> cancelled out): see IndexStack.push(int)
>
> - PathClipFilter:
>
> static final class PathClipFilter implements DPathConsumer2D {
>
> private DPathConsumer2D out;
>
> // Bounds of the drawing region, at pixel precision.
> private final double[] clipRect;
>
> private final double[] corners = new double[8];
> private boolean init_corners = false;
>
> private final IndexStack stack;
>
> // the outcode of the starting point
> private int sOutCode = 0;
>
> // the current outcode of the current sub path
> private int cOutCode = 0;
>
> private boolean outside = false;
> private double cx0, cy0;
>
> PathClipFilter(final DRendererContext rdrCtx) {
> this.clipRect = rdrCtx.clipRect;
> this.stack = (rdrCtx.stats != null) ?
> new IndexStack(rdrCtx,
> rdrCtx.stats.stat_pcf_idxstack_indices,
> rdrCtx.stats.hist_pcf_idxstack_indices,
> rdrCtx.stats.stat_array_pcf_idxstack_indices)
> : new IndexStack(rdrCtx);
> }
>
> PathClipFilter 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;
>
> // add a small rounding error:
> final double margin = 1e-3d;
>
> final double[] _clipRect = this.clipRect;
> _clipRect[0] -= margin - rdrOffY;
> _clipRect[1] += margin + rdrOffY;
> _clipRect[2] -= margin - rdrOffX;
> _clipRect[3] += margin + rdrOffX;
>
> init_corners = true;
>
> return this; // fluent API
> }
>
> /**
> * Disposes this instance:
> * clean up before reusing this instance
> */
> void dispose() {
> stack.dispose();
> }
>
> @Override
> public void pathDone() {
> out.pathDone();
>
> // TODO: fix possible leak if exception happened
> // Dispose this instance:
> dispose();
> }
>
> @Override
> public void closePath() {
> if (outside) {
> this.outside = false;
>
> if (sOutCode == 0) {
> finish();
> } else {
> stack.reset();
> }
> }
> out.closePath();
> this.cOutCode = sOutCode;
> }
>
> private void finish() {
> if (!stack.isEmpty()) {
> if (init_corners) {
> init_corners = false;
> // Top Left (0):
> corners[0] = clipRect[2];
> corners[1] = clipRect[0];
> // Bottom Left (1):
> corners[2] = clipRect[2];
> corners[3] = clipRect[1];
> // Top right (2):
> corners[4] = clipRect[3];
> corners[5] = clipRect[0];
> // Bottom Right (3):
> corners[6] = clipRect[3];
> corners[7] = clipRect[1];
> }
> stack.pullAll(corners, out);
> }
> out.lineTo(cx0, cy0);
> }
>
> @Override
> public void moveTo(final double x0, final double y0) {
> final int outcode = DHelpers.outcode(x0, y0, clipRect);
> this.sOutCode = outcode;
> this.cOutCode = outcode;
> this.outside = false;
> out.moveTo(x0, y0);
> }
>
> @Override
> public void lineTo(final double xe, final double ye) {
> final int outcode0 = this.cOutCode;
> final int outcode1 = DHelpers.outcode(xe, ye, clipRect);
> this.cOutCode = outcode1;
>
> final int sideCode = (outcode0 & outcode1);
>
> // basic rejection criteria:
> if (sideCode != 0) {
> // keep last point coordinate before entering the clip
> again:
> this.outside = true;
> this.cx0 = xe;
> this.cy0 = ye;
>
> clip(sideCode, outcode0, outcode1);
> return;
> }
> if (outside) {
> this.outside = false;
> finish();
> }
> // clipping disabled:
> out.lineTo(xe, ye);
> }
>
> private void clip(final int sideCode,
> final int outcode0,
> final int outcode1)
> {
> // corner or cross-boundary on left or right side:
> if ((outcode0 != outcode1)
> && ((sideCode & DHelpers.OUTCODE_MASK_T_B) != 0))
> {
> // combine outcodes:
> final int mergeCode = (outcode0 | outcode1);
> final int tbCode = mergeCode & DHelpers.OUTCODE_MASK_T_B;
> final int lrCode = mergeCode & DHelpers.OUTCODE_MASK_L_R;
> // add corners to outside stack:
> final int off = (lrCode == DHelpers.OUTCODE_LEFT) ? 0 : 2;
>
> switch (tbCode) {
> case DHelpers.OUTCODE_TOP:
> stack.push(off); // top
> return;
> case DHelpers.OUTCODE_BOTTOM:
> stack.push(off + 1); // bottom
> return;
> default:
> // both TOP / BOTTOM:
> if ((outcode0 & DHelpers.OUTCODE_TOP) != 0) {
> // top to bottom
> stack.push(off); // top
> stack.push(off + 1); // bottom
> } else {
> // bottom to top
> stack.push(off + 1); // bottom
> stack.push(off); // top
> }
> }
> }
> }
>
> @Override
> public void curveTo(final double x1, final double y1,
> final double x2, final double y2,
> final double xe, final double ye)
> {
> final int outcode0 = this.cOutCode;
> final int outcode3 = DHelpers.outcode(xe, ye, clipRect);
> this.cOutCode = outcode3;
>
> int sideCode = outcode0 & outcode3;
>
> if (sideCode != 0) {
> sideCode &= DHelpers.outcode(x1, y1, clipRect);
> sideCode &= DHelpers.outcode(x2, y2, clipRect);
>
> // basic rejection criteria:
> if (sideCode != 0) {
> // keep last point coordinate before entering the clip
> again:
> this.outside = true;
> this.cx0 = xe;
> this.cy0 = ye;
>
> clip(sideCode, outcode0, outcode3);
> return;
> }
> }
> if (outside) {
> this.outside = false;
> finish();
> }
> // clipping disabled:
> out.curveTo(x1, y1, x2, y2, xe, ye);
> }
>
> @Override
> public void quadTo(final double x1, final double y1,
> final double xe, final double ye)
> {
> final int outcode0 = this.cOutCode;
> final int outcode2 = DHelpers.outcode(xe, ye, clipRect);
> this.cOutCode = outcode2;
>
> int sideCode = outcode0 & outcode2;
>
> if (outcode2 != 0) {
> sideCode &= DHelpers.outcode(x1, y1, clipRect);
>
> // basic rejection criteria:
> if (sideCode != 0) {
> // keep last point coordinate before entering the clip
> again:
> this.outside = true;
> this.cx0 = xe;
> this.cy0 = ye;
>
> clip(sideCode, outcode0, outcode2);
> return;
> }
> }
> if (outside) {
> this.outside = false;
> finish();
> }
> // clipping disabled:
> out.quadTo(x1, y1, xe, ye);
> }
>
> @Override
> public long getNativeConsumer() {
> throw new InternalError("Not using a native peer");
> }
> }
>
> - DHelpers.IndexStack:
> // a stack of integer indices
> static final class IndexStack {
>
> // integer capacity = edges count / 4 ~ 1024
> private static final int INITIAL_COUNT = INITIAL_EDGES_COUNT >> 2;
>
> private int end;
> private int[] indices;
>
> // indices ref (dirty)
> private final IntArrayCache.Reference indices_ref;
>
> // used marks (stats only)
> private int indicesUseMark;
>
> private final StatLong stat_idxstack_indices;
> private final Histogram hist_idxstack_indices;
> private final StatLong stat_array_idxstack_indices;
>
> IndexStack(final DRendererContext rdrCtx) {
> this(rdrCtx, null, null, null);
> }
>
> IndexStack(final DRendererContext rdrCtx,
> final StatLong stat_idxstack_indices,
> final Histogram hist_idxstack_indices,
> final StatLong stat_array_idxstack_indices)
> {
> indices_ref = rdrCtx.newDirtyIntArrayRef(INITIAL_COUNT); // 4K
> indices = indices_ref.initial;
> end = 0;
>
> if (DO_STATS) {
> indicesUseMark = 0;
> }
> this.stat_idxstack_indices = stat_idxstack_indices;
> this.hist_idxstack_indices = hist_idxstack_indices;
> this.stat_array_idxstack_indices =
> stat_array_idxstack_indices;
> }
>
> /**
> * Disposes this PolyStack:
> * clean up before reusing this instance
> */
> void dispose() {
> end = 0;
>
> if (DO_STATS) {
> stat_idxstack_indices.add(indicesUseMark);
> hist_idxstack_indices.add(indicesUseMark);
>
> // reset marks
> indicesUseMark = 0;
> }
>
> // Return arrays:
> // values is kept dirty
> indices = indices_ref.putArray(indices);
> }
>
> boolean isEmpty() {
> return (end == 0);
> }
>
> void reset() {
> end = 0;
> }
>
> void push(final int v) {
> // remove redundant values (reverse order):
> int[] _values = indices;
> final int nc = end;
> if (nc != 0) {
> if (_values[nc - 1] == v) {
> // remove both duplicated values:
> end--;
> return;
> }
> }
> if (_values.length <= nc) {
> if (DO_STATS) {
> stat_array_idxstack_indices.add(nc + 1);
> }
> indices = _values = indices_ref.widenArray(_values, nc,
> nc + 1);
> }
> _values[end++] = v;
>
> if (DO_STATS) {
> // update used marks:
> if (end > indicesUseMark) {
> indicesUseMark = end;
> }
> }
> }
>
> void pullAll(final double[] points, final DPathConsumer2D io) {
> final int nc = end;
> if (nc == 0) {
> return;
> }
> final int[] _values = indices;
>
> for (int i = 0, j; i < nc; i++) {
> j = _values[i] << 1;
> io.lineTo(points[j], points[j + 1]);
> }
> end = 0;
> }
> }
>
>
> 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
>>
>
> Now all useless rounds are totally discarded from the path sent to the
> Renderer (removing lots of edges on the left/right sides)
>
>
>> clip off: ~ 145ms
>> clip on: ~ 106ms
>>
>
> clip on: ~ 68ms for this huge filled spiral ~ 50% faster
>
>
> Could you answer my previous email on EO questions ?
> How to deal with self intersections or is it possible to skip left
> segments in the EO case or not ?
> (I am a bit lost)
>
> I need a simple path to test clipping with the EO rule (redudant
> segments); any idea ?
>
> Cheers,
> Laurent
>
More information about the openjfx-dev
mailing list