diff --git a/pom.xml b/pom.xml index fb617fb..616c8ee 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ org.marlin marlin jar - 0.7.5-Unsafe-OpenJDK + 0.8.0-Unsafe-OpenJDK Marlin software rasterizer https://github.com/bourgesl/marlin-renderer @@ -87,7 +87,7 @@ sun-java2d com/sun/imageio/** - java/awt/geom/Path2D* + java/awt/** sun/java2d/pipe/** diff --git a/src/main/java/java/awt/TexturePaint.java b/src/main/java/java/awt/TexturePaint.java new file mode 100644 index 0000000..7ea2e8a --- /dev/null +++ b/src/main/java/java/awt/TexturePaint.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 1997, 2008, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + +package java.awt; + +import java.awt.geom.Rectangle2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; + +/** + * The TexturePaint class provides a way to fill a + * {@link Shape} with a texture that is specified as + * a {@link BufferedImage}. The size of the BufferedImage + * object should be small because the BufferedImage data + * is copied by the TexturePaint object. + * At construction time, the texture is anchored to the upper + * left corner of a {@link Rectangle2D} that is + * specified in user space. Texture is computed for + * locations in the device space by conceptually replicating the + * specified Rectangle2D infinitely in all directions + * in user space and mapping the BufferedImage to each + * replicated Rectangle2D. + * @see Paint + * @see Graphics2D#setPaint + * @version 1.48, 06/05/07 + */ + +public class TexturePaint implements Paint { + + BufferedImage bufImg; + double tx; + double ty; + double sx; + double sy; + + /** + * Constructs a TexturePaint object. + * @param txtr the BufferedImage object with the texture + * used for painting + * @param anchor the Rectangle2D in user space used to + * anchor and replicate the texture + */ + public TexturePaint(BufferedImage txtr, + Rectangle2D anchor) { + this.bufImg = txtr; + this.tx = anchor.getX(); + this.ty = anchor.getY(); + this.sx = anchor.getWidth() / bufImg.getWidth(); + this.sy = anchor.getHeight() / bufImg.getHeight(); + } + + /** + * Returns the BufferedImage texture used to + * fill the shapes. + * @return a BufferedImage. + */ + public BufferedImage getImage() { + return bufImg; + } + + /** + * Returns a copy of the anchor rectangle which positions and + * sizes the textured image. + * @return the Rectangle2D used to anchor and + * size this TexturePaint. + */ + public Rectangle2D getAnchorRect() { + return new Rectangle2D.Double(tx, ty, + sx * bufImg.getWidth(), + sy * bufImg.getHeight()); + } + + /** + * Creates and returns a {@link PaintContext} used to + * generate a tiled image pattern. + * See the {@link Paint#createContext specification} of the + * method in the {@link Paint} interface for information + * on null parameter handling. + * + * @param cm the preferred {@link ColorModel} which represents the most convenient + * format for the caller to receive the pixel data, or {@code null} + * if there is no preference. + * @param deviceBounds the device space bounding box + * of the graphics primitive being rendered. + * @param userBounds the user space bounding box + * of the graphics primitive being rendered. + * @param xform the {@link AffineTransform} from user + * space into device space. + * @param hints the set of hints that the context object can use to + * choose between rendering alternatives. + * @return the {@code PaintContext} for + * generating color patterns. + * @see Paint + * @see PaintContext + * @see ColorModel + * @see Rectangle + * @see Rectangle2D + * @see AffineTransform + * @see RenderingHints + */ + public PaintContext createContext(ColorModel cm, + Rectangle deviceBounds, + Rectangle2D userBounds, + AffineTransform xform, + RenderingHints hints) { +// TODO: avoid allocating new transform for Identity ! + if (xform == null) { + xform = new AffineTransform(); + } else { + xform = (AffineTransform) xform.clone(); + } + xform.translate(tx, ty); + xform.scale(sx, sy); + + return TexturePaintContext.getContext(bufImg, xform, hints, + deviceBounds); + } + + /** + * Returns the transparency mode for this TexturePaint. + * @return the transparency mode for this TexturePaint + * as an integer value. + * @see Transparency + */ + public int getTransparency() { + return (bufImg.getColorModel()).getTransparency(); + } + +} diff --git a/src/main/java/java/awt/TexturePaintContext.java b/src/main/java/java/awt/TexturePaintContext.java new file mode 100644 index 0000000..e54e67a --- /dev/null +++ b/src/main/java/java/awt/TexturePaintContext.java @@ -0,0 +1,836 @@ +/* + * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + +package java.awt; + +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.awt.image.ColorModel; +import java.awt.image.DirectColorModel; +import java.awt.image.IndexColorModel; +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import java.lang.ref.WeakReference; +import sun.awt.image.SunWritableRaster; +import sun.awt.image.IntegerInterleavedRaster; +import sun.awt.image.ByteInterleavedRaster; + +abstract class TexturePaintContext implements PaintContext { + public static ColorModel xrgbmodel = + new DirectColorModel(24, 0xff0000, 0xff00, 0xff); + public static ColorModel argbmodel = ColorModel.getRGBdefault(); + + ColorModel colorModel; + int bWidth; + int bHeight; + int maxWidth; + + WritableRaster outRas; + + double xOrg; + double yOrg; + double incXAcross; + double incYAcross; + double incXDown; + double incYDown; + + int colincx; + int colincy; + int colincxerr; + int colincyerr; + int rowincx; + int rowincy; + int rowincxerr; + int rowincyerr; + + public static PaintContext getContext(BufferedImage bufImg, + AffineTransform xform, + RenderingHints hints, + Rectangle devBounds) { + WritableRaster raster = bufImg.getRaster(); + ColorModel cm = bufImg.getColorModel(); + int maxw = devBounds.width; + Object val = hints.get(RenderingHints.KEY_INTERPOLATION); + boolean filter = + (val == null + ? (hints.get(RenderingHints.KEY_RENDERING) == RenderingHints.VALUE_RENDER_QUALITY) + : (val != RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR)); + if (raster instanceof IntegerInterleavedRaster && + (!filter || isFilterableDCM(cm))) + { + IntegerInterleavedRaster iir = (IntegerInterleavedRaster) raster; + if (iir.getNumDataElements() == 1 && iir.getPixelStride() == 1) { +// TODO: cache ? + return new Int(iir, cm, xform, maxw, filter); + } + } else if (raster instanceof ByteInterleavedRaster) { + ByteInterleavedRaster bir = (ByteInterleavedRaster) raster; + if (bir.getNumDataElements() == 1 && bir.getPixelStride() == 1) { + if (filter) { + if (isFilterableICM(cm)) { +// TODO: cache ? + return new ByteFilter(bir, cm, xform, maxw); + } + } else { +// TODO: cache ? + return new Byte(bir, cm, xform, maxw); + } + } + } +// TODO: cache ? + return new Any(raster, cm, xform, maxw, filter); + } + + public static boolean isFilterableICM(ColorModel cm) { + if (cm instanceof IndexColorModel) { + IndexColorModel icm = (IndexColorModel) cm; + if (icm.getMapSize() <= 256) { + return true; + } + } + return false; + } + + public static boolean isFilterableDCM(ColorModel cm) { + if (cm instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) cm; + return (isMaskOK(dcm.getAlphaMask(), true) && + isMaskOK(dcm.getRedMask(), false) && + isMaskOK(dcm.getGreenMask(), false) && + isMaskOK(dcm.getBlueMask(), false)); + } + return false; + } + + public static boolean isMaskOK(int mask, boolean canbezero) { + if (canbezero && mask == 0) { + return true; + } + return (mask == 0xff || + mask == 0xff00 || + mask == 0xff0000 || + mask == 0xff000000); + } + + public static ColorModel getInternedColorModel(ColorModel cm) { + if (xrgbmodel == cm || xrgbmodel.equals(cm)) { + return xrgbmodel; + } + if (argbmodel == cm || argbmodel.equals(cm)) { + return argbmodel; + } + return cm; + } + + TexturePaintContext(ColorModel cm, AffineTransform xform, + int bWidth, int bHeight, int maxw) { + this.colorModel = getInternedColorModel(cm); + this.bWidth = bWidth; + this.bHeight = bHeight; + this.maxWidth = maxw; + +// TODO: check for identity if no transform (avoid allocating new at) + try { + xform = xform.createInverse(); + } catch (NoninvertibleTransformException e) { + xform.setToScale(0, 0); + } + this.incXAcross = mod(xform.getScaleX(), bWidth); + this.incYAcross = mod(xform.getShearY(), bHeight); + this.incXDown = mod(xform.getShearX(), bWidth); + this.incYDown = mod(xform.getScaleY(), bHeight); + this.xOrg = xform.getTranslateX(); + this.yOrg = xform.getTranslateY(); + this.colincx = (int) incXAcross; + this.colincy = (int) incYAcross; + this.colincxerr = fractAsInt(incXAcross); + this.colincyerr = fractAsInt(incYAcross); + this.rowincx = (int) incXDown; + this.rowincy = (int) incYDown; + this.rowincxerr = fractAsInt(incXDown); + this.rowincyerr = fractAsInt(incYDown); + + } + + static int fractAsInt(double d) { + return (int) ((d % 1.0) * Integer.MAX_VALUE); + } + + static double mod(double num, double den) { + num = num % den; + if (num < 0) { + num += den; + if (num >= den) { + // For very small negative numerators, the answer might + // be such a tiny bit less than den that the difference + // is smaller than the mantissa of a double allows and + // the result would then be rounded to den. If that is + // the case then we map that number to 0 as the nearest + // modulus representation. + num = 0; + } + } + return num; + } + + /** + * Release the resources allocated for the operation. + */ + public void dispose() { + dropRaster(colorModel, outRas); + } + + /** + * Return the ColorModel of the output. + */ + public ColorModel getColorModel() { + return colorModel; + } + + /** + * Return a Raster containing the colors generated for the graphics + * operation. + * @param x,y,w,h The area in device space for which colors are + * generated. + */ + public Raster getRaster(int x, int y, int w, int h) { + +// Called by AlphaPaintPipe (to check) + + if (outRas == null || + outRas.getWidth() < w || + outRas.getHeight() < h) + { + // If h==1, we will probably get lots of "scanline" rects + outRas = makeRaster((h == 1 ? Math.max(w, maxWidth) : w), h); + } + double X = mod(xOrg + x * incXAcross + y * incXDown, bWidth); + double Y = mod(yOrg + x * incYAcross + y * incYDown, bHeight); + + setRaster((int) X, (int) Y, fractAsInt(X), fractAsInt(Y), + w, h, bWidth, bHeight, + colincx, colincxerr, + colincy, colincyerr, + rowincx, rowincxerr, + rowincy, rowincyerr); + + SunWritableRaster.markDirty(outRas); + + return outRas; + } + + private static WeakReference xrgbRasRef; + private static WeakReference argbRasRef; + + synchronized static WritableRaster makeRaster(ColorModel cm, + Raster srcRas, + int w, int h) + { + if (xrgbmodel == cm) { + if (xrgbRasRef != null) { + WritableRaster wr = (WritableRaster) xrgbRasRef.get(); + if (wr != null && wr.getWidth() >= w && wr.getHeight() >= h) { + xrgbRasRef = null; + return wr; + } + } + // If we are going to cache this Raster, make it non-tiny + if (w <= 32 && h <= 32) { + w = h = 32; + } + } else if (argbmodel == cm) { + if (argbRasRef != null) { + WritableRaster wr = (WritableRaster) argbRasRef.get(); + if (wr != null && wr.getWidth() >= w && wr.getHeight() >= h) { + argbRasRef = null; + return wr; + } + } + // If we are going to cache this Raster, make it non-tiny + if (w <= 32 && h <= 32) { + w = h = 32; + } + } +// System.out.println("makeRaster: createCompatibleWritableRaster["+w+"x"+h+"]"); + if (srcRas != null) { + return srcRas.createCompatibleWritableRaster(w, h); + } else { + return cm.createCompatibleWritableRaster(w, h); + } + } + + synchronized static void dropRaster(ColorModel cm, Raster outRas) { + if (outRas == null) { + return; + } + if (xrgbmodel == cm) { + xrgbRasRef = new WeakReference<>(outRas); + } else if (argbmodel == cm) { + argbRasRef = new WeakReference<>(outRas); + } + } + + private static WeakReference byteRasRef; + + synchronized static WritableRaster makeByteRaster(Raster srcRas, + int w, int h) + { + if (byteRasRef != null) { + WritableRaster wr = (WritableRaster) byteRasRef.get(); + if (wr != null && wr.getWidth() >= w && wr.getHeight() >= h) { + byteRasRef = null; + return wr; + } + } + // If we are going to cache this Raster, make it non-tiny + if (w <= 32 && h <= 32) { + w = h = 32; + } + return srcRas.createCompatibleWritableRaster(w, h); + } + + synchronized static void dropByteRaster(Raster outRas) { + if (outRas == null) { + return; + } + byteRasRef = new WeakReference<>(outRas); + } + + public abstract WritableRaster makeRaster(int w, int h); + public abstract void setRaster(int x, int y, int xerr, int yerr, + int w, int h, int bWidth, int bHeight, + int colincx, int colincxerr, + int colincy, int colincyerr, + int rowincx, int rowincxerr, + int rowincy, int rowincyerr); + + /* + * Blends the four ARGB values in the rgbs array using the factors + * described by xmul and ymul in the following ratio: + * + * rgbs[0] * (1-xmul) * (1-ymul) + + * rgbs[1] * ( xmul) * (1-ymul) + + * rgbs[2] * (1-xmul) * ( ymul) + + * rgbs[3] * ( xmul) * ( ymul) + * + * xmul and ymul are integer values in the half-open range [0, 2^31) + * where 0 == 0.0 and 2^31 == 1.0. + * + * Note that since the range is half-open, the values are always + * logically less than 1.0. This makes sense because while choosing + * pixels to blend, when the error values reach 1.0 we move to the + * next pixel and reset them to 0.0. + */ + public static int blend(int rgbs[], int xmul, int ymul) { + // xmul/ymul are 31 bits wide, (0 => 2^31-1) + // shift them to 12 bits wide, (0 => 2^12-1) + xmul = (xmul >>> 19); + ymul = (ymul >>> 19); + int accumA, accumR, accumG, accumB; + accumA = accumR = accumG = accumB = 0; + for (int i = 0; i < 4; i++) { + int rgb = rgbs[i]; + // The complement of the [xy]mul values (1-[xy]mul) can result + // in new values in the range (1 => 2^12). Thus for any given + // loop iteration, the values could be anywhere in (0 => 2^12). + xmul = (1<<12) - xmul; + if ((i & 1) == 0) { + ymul = (1<<12) - ymul; + } + // xmul and ymul are each 12 bits (0 => 2^12) + // factor is thus 24 bits (0 => 2^24) + int factor = xmul * ymul; + if (factor != 0) { + // accum variables will accumulate 32 bits + // bytes extracted from rgb fit in 8 bits (0 => 255) + // byte * factor thus fits in 32 bits (0 => 255 * 2^24) + accumA += (((rgb >>> 24) ) * factor); + accumR += (((rgb >>> 16) & 0xff) * factor); + accumG += (((rgb >>> 8) & 0xff) * factor); + accumB += (((rgb ) & 0xff) * factor); + } + } + return ((((accumA + (1<<23)) >>> 24) << 24) | + (((accumR + (1<<23)) >>> 24) << 16) | + (((accumG + (1<<23)) >>> 24) << 8) | + (((accumB + (1<<23)) >>> 24) )); + } + + static class Int extends TexturePaintContext { + IntegerInterleavedRaster srcRas; + int inData[]; + int inOff; + int inSpan; + int outData[]; + int outOff; + int outSpan; + boolean filter; + + public Int(IntegerInterleavedRaster srcRas, ColorModel cm, + AffineTransform xform, int maxw, boolean filter) + { + super(cm, xform, srcRas.getWidth(), srcRas.getHeight(), maxw); + this.srcRas = srcRas; + this.inData = srcRas.getDataStorage(); + this.inSpan = srcRas.getScanlineStride(); + this.inOff = srcRas.getDataOffset(0); + this.filter = filter; + } + + public WritableRaster makeRaster(int w, int h) { + WritableRaster ras = makeRaster(colorModel, srcRas, w, h); + IntegerInterleavedRaster iiRas = (IntegerInterleavedRaster) ras; + outData = iiRas.getDataStorage(); + outSpan = iiRas.getScanlineStride(); + outOff = iiRas.getDataOffset(0); + return ras; + } + + public void setRaster(int x, int y, int xerr, int yerr, + int w, int h, int bWidth, int bHeight, + int colincx, int colincxerr, + int colincy, int colincyerr, + int rowincx, int rowincxerr, + int rowincy, int rowincyerr) { + int[] inData = this.inData; + int[] outData = this.outData; + int out = outOff; + int inSpan = this.inSpan; + int inOff = this.inOff; + int outSpan = this.outSpan; + boolean filter = this.filter; + boolean normalx = (colincx == 1 && colincxerr == 0 && + colincy == 0 && colincyerr == 0) && !filter; + int rowx = x; + int rowy = y; + int rowxerr = xerr; + int rowyerr = yerr; + if (normalx) { + outSpan -= w; + } + int rgbs[] = filter ? new int[4] : null; + for (int j = 0; j < h; j++) { + if (normalx) { + int in = inOff + rowy * inSpan + bWidth; + x = bWidth - rowx; + out += w; + if (bWidth >= 32) { + int i = w; + while (i > 0) { + int copyw = (i < x) ? i : x; + System.arraycopy(inData, in - x, + outData, out - i, + copyw); + i -= copyw; + if ((x -= copyw) == 0) { + x = bWidth; + } + } + } else { + for (int i = w; i > 0; i--) { + outData[out - i] = inData[in - x]; + if (--x == 0) { + x = bWidth; + } + } + } + } else { + x = rowx; + y = rowy; + xerr = rowxerr; + yerr = rowyerr; + for (int i = 0; i < w; i++) { + if (filter) { + int nextx, nexty; + if ((nextx = x + 1) >= bWidth) { + nextx = 0; + } + if ((nexty = y + 1) >= bHeight) { + nexty = 0; + } + rgbs[0] = inData[inOff + y * inSpan + x]; + rgbs[1] = inData[inOff + y * inSpan + nextx]; + rgbs[2] = inData[inOff + nexty * inSpan + x]; + rgbs[3] = inData[inOff + nexty * inSpan + nextx]; + outData[out + i] = + TexturePaintContext.blend(rgbs, xerr, yerr); + } else { + outData[out + i] = inData[inOff + y * inSpan + x]; + } + if ((xerr += colincxerr) < 0) { + xerr &= Integer.MAX_VALUE; + x++; + } + if ((x += colincx) >= bWidth) { + x -= bWidth; + } + if ((yerr += colincyerr) < 0) { + yerr &= Integer.MAX_VALUE; + y++; + } + if ((y += colincy) >= bHeight) { + y -= bHeight; + } + } + } + if ((rowxerr += rowincxerr) < 0) { + rowxerr &= Integer.MAX_VALUE; + rowx++; + } + if ((rowx += rowincx) >= bWidth) { + rowx -= bWidth; + } + if ((rowyerr += rowincyerr) < 0) { + rowyerr &= Integer.MAX_VALUE; + rowy++; + } + if ((rowy += rowincy) >= bHeight) { + rowy -= bHeight; + } + out += outSpan; + } + } + } + + static class Byte extends TexturePaintContext { + ByteInterleavedRaster srcRas; + byte inData[]; + int inOff; + int inSpan; + byte outData[]; + int outOff; + int outSpan; + + public Byte(ByteInterleavedRaster srcRas, ColorModel cm, + AffineTransform xform, int maxw) + { + super(cm, xform, srcRas.getWidth(), srcRas.getHeight(), maxw); + this.srcRas = srcRas; + this.inData = srcRas.getDataStorage(); + this.inSpan = srcRas.getScanlineStride(); + this.inOff = srcRas.getDataOffset(0); + } + + public WritableRaster makeRaster(int w, int h) { + WritableRaster ras = makeByteRaster(srcRas, w, h); + ByteInterleavedRaster biRas = (ByteInterleavedRaster) ras; + outData = biRas.getDataStorage(); + outSpan = biRas.getScanlineStride(); + outOff = biRas.getDataOffset(0); + return ras; + } + + public void dispose() { + dropByteRaster(outRas); + } + + public void setRaster(int x, int y, int xerr, int yerr, + int w, int h, int bWidth, int bHeight, + int colincx, int colincxerr, + int colincy, int colincyerr, + int rowincx, int rowincxerr, + int rowincy, int rowincyerr) { + byte[] inData = this.inData; + byte[] outData = this.outData; + int out = outOff; + int inSpan = this.inSpan; + int inOff = this.inOff; + int outSpan = this.outSpan; + boolean normalx = (colincx == 1 && colincxerr == 0 && + colincy == 0 && colincyerr == 0); + int rowx = x; + int rowy = y; + int rowxerr = xerr; + int rowyerr = yerr; + if (normalx) { + outSpan -= w; + } + for (int j = 0; j < h; j++) { + if (normalx) { + int in = inOff + rowy * inSpan + bWidth; + x = bWidth - rowx; + out += w; + if (bWidth >= 32) { + int i = w; + while (i > 0) { + int copyw = (i < x) ? i : x; + System.arraycopy(inData, in - x, + outData, out - i, + copyw); + i -= copyw; + if ((x -= copyw) == 0) { + x = bWidth; + } + } + } else { + for (int i = w; i > 0; i--) { + outData[out - i] = inData[in - x]; + if (--x == 0) { + x = bWidth; + } + } + } + } else { + x = rowx; + y = rowy; + xerr = rowxerr; + yerr = rowyerr; + for (int i = 0; i < w; i++) { + outData[out + i] = inData[inOff + y * inSpan + x]; + if ((xerr += colincxerr) < 0) { + xerr &= Integer.MAX_VALUE; + x++; + } + if ((x += colincx) >= bWidth) { + x -= bWidth; + } + if ((yerr += colincyerr) < 0) { + yerr &= Integer.MAX_VALUE; + y++; + } + if ((y += colincy) >= bHeight) { + y -= bHeight; + } + } + } + if ((rowxerr += rowincxerr) < 0) { + rowxerr &= Integer.MAX_VALUE; + rowx++; + } + if ((rowx += rowincx) >= bWidth) { + rowx -= bWidth; + } + if ((rowyerr += rowincyerr) < 0) { + rowyerr &= Integer.MAX_VALUE; + rowy++; + } + if ((rowy += rowincy) >= bHeight) { + rowy -= bHeight; + } + out += outSpan; + } + } + } + + static class ByteFilter extends TexturePaintContext { + ByteInterleavedRaster srcRas; + int inPalette[]; + byte inData[]; + int inOff; + int inSpan; + int outData[]; + int outOff; + int outSpan; + + public ByteFilter(ByteInterleavedRaster srcRas, ColorModel cm, + AffineTransform xform, int maxw) + { + super((cm.getTransparency() == Transparency.OPAQUE + ? xrgbmodel : argbmodel), + xform, srcRas.getWidth(), srcRas.getHeight(), maxw); + this.inPalette = new int[256]; + ((IndexColorModel) cm).getRGBs(this.inPalette); + this.srcRas = srcRas; + this.inData = srcRas.getDataStorage(); + this.inSpan = srcRas.getScanlineStride(); + this.inOff = srcRas.getDataOffset(0); + } + + public WritableRaster makeRaster(int w, int h) { + // Note that we do not pass srcRas to makeRaster since it + // is a Byte Raster and this colorModel needs an Int Raster + WritableRaster ras = makeRaster(colorModel, null, w, h); + IntegerInterleavedRaster iiRas = (IntegerInterleavedRaster) ras; + outData = iiRas.getDataStorage(); + outSpan = iiRas.getScanlineStride(); + outOff = iiRas.getDataOffset(0); + return ras; + } + + public void setRaster(int x, int y, int xerr, int yerr, + int w, int h, int bWidth, int bHeight, + int colincx, int colincxerr, + int colincy, int colincyerr, + int rowincx, int rowincxerr, + int rowincy, int rowincyerr) { + byte[] inData = this.inData; + int[] outData = this.outData; + int out = outOff; + int inSpan = this.inSpan; + int inOff = this.inOff; + int outSpan = this.outSpan; + int rowx = x; + int rowy = y; + int rowxerr = xerr; + int rowyerr = yerr; + int rgbs[] = new int[4]; + for (int j = 0; j < h; j++) { + x = rowx; + y = rowy; + xerr = rowxerr; + yerr = rowyerr; + for (int i = 0; i < w; i++) { + int nextx, nexty; + if ((nextx = x + 1) >= bWidth) { + nextx = 0; + } + if ((nexty = y + 1) >= bHeight) { + nexty = 0; + } + rgbs[0] = inPalette[0xff & inData[inOff + x + + inSpan * y]]; + rgbs[1] = inPalette[0xff & inData[inOff + nextx + + inSpan * y]]; + rgbs[2] = inPalette[0xff & inData[inOff + x + + inSpan * nexty]]; + rgbs[3] = inPalette[0xff & inData[inOff + nextx + + inSpan * nexty]]; + outData[out + i] = + TexturePaintContext.blend(rgbs, xerr, yerr); + if ((xerr += colincxerr) < 0) { + xerr &= Integer.MAX_VALUE; + x++; + } + if ((x += colincx) >= bWidth) { + x -= bWidth; + } + if ((yerr += colincyerr) < 0) { + yerr &= Integer.MAX_VALUE; + y++; + } + if ((y += colincy) >= bHeight) { + y -= bHeight; + } + } + if ((rowxerr += rowincxerr) < 0) { + rowxerr &= Integer.MAX_VALUE; + rowx++; + } + if ((rowx += rowincx) >= bWidth) { + rowx -= bWidth; + } + if ((rowyerr += rowincyerr) < 0) { + rowyerr &= Integer.MAX_VALUE; + rowy++; + } + if ((rowy += rowincy) >= bHeight) { + rowy -= bHeight; + } + out += outSpan; + } + } + } + + static class Any extends TexturePaintContext { + WritableRaster srcRas; + boolean filter; + + public Any(WritableRaster srcRas, ColorModel cm, + AffineTransform xform, int maxw, boolean filter) + { + super(cm, xform, srcRas.getWidth(), srcRas.getHeight(), maxw); + this.srcRas = srcRas; + this.filter = filter; + } + + public WritableRaster makeRaster(int w, int h) { + return makeRaster(colorModel, srcRas, w, h); + } + + public void setRaster(int x, int y, int xerr, int yerr, + int w, int h, int bWidth, int bHeight, + int colincx, int colincxerr, + int colincy, int colincyerr, + int rowincx, int rowincxerr, + int rowincy, int rowincyerr) { + Object data = null; + int rowx = x; + int rowy = y; + int rowxerr = xerr; + int rowyerr = yerr; + WritableRaster srcRas = this.srcRas; + WritableRaster outRas = this.outRas; + int rgbs[] = filter ? new int[4] : null; + for (int j = 0; j < h; j++) { + x = rowx; + y = rowy; + xerr = rowxerr; + yerr = rowyerr; + for (int i = 0; i < w; i++) { + data = srcRas.getDataElements(x, y, data); + if (filter) { + int nextx, nexty; + if ((nextx = x + 1) >= bWidth) { + nextx = 0; + } + if ((nexty = y + 1) >= bHeight) { + nexty = 0; + } + rgbs[0] = colorModel.getRGB(data); + data = srcRas.getDataElements(nextx, y, data); + rgbs[1] = colorModel.getRGB(data); + data = srcRas.getDataElements(x, nexty, data); + rgbs[2] = colorModel.getRGB(data); + data = srcRas.getDataElements(nextx, nexty, data); + rgbs[3] = colorModel.getRGB(data); + int rgb = + TexturePaintContext.blend(rgbs, xerr, yerr); + data = colorModel.getDataElements(rgb, data); + } + outRas.setDataElements(i, j, data); + if ((xerr += colincxerr) < 0) { + xerr &= Integer.MAX_VALUE; + x++; + } + if ((x += colincx) >= bWidth) { + x -= bWidth; + } + if ((yerr += colincyerr) < 0) { + yerr &= Integer.MAX_VALUE; + y++; + } + if ((y += colincy) >= bHeight) { + y -= bHeight; + } + } + if ((rowxerr += rowincxerr) < 0) { + rowxerr &= Integer.MAX_VALUE; + rowx++; + } + if ((rowx += rowincx) >= bWidth) { + rowx -= bWidth; + } + if ((rowyerr += rowincyerr) < 0) { + rowyerr &= Integer.MAX_VALUE; + rowy++; + } + if ((rowy += rowincy) >= bHeight) { + rowy -= bHeight; + } + } + } + } +} diff --git a/src/main/java/sun/java2d/marlin/Curve.java b/src/main/java/sun/java2d/marlin/Curve.java index 675a1d8..7880703 100644 --- a/src/main/java/sun/java2d/marlin/Curve.java +++ b/src/main/java/sun/java2d/marlin/Curve.java @@ -56,10 +56,10 @@ void set(float x1, float y1, float x3, float y3, float x4, float y4) { - ax = 3.0f * (x2 - x3) + x4 - x1; - ay = 3.0f * (y2 - y3) + y4 - y1; - bx = 3.0f * (x1 - 2.0f * x2 + x3); - by = 3.0f * (y1 - 2.0f * y2 + y3); + ax = 3.0f * (x2 - x3) + (x4 - x1); + ay = 3.0f * (y2 - y3) + (y4 - y1); + bx = 3.0f * ((x1 - x2) + (x3 - x2)); + by = 3.0f * ((y1 - y2) + (y3 - y2)); cx = 3.0f * (x2 - x1); cy = 3.0f * (y2 - y1); dx = x1; diff --git a/src/main/java/sun/java2d/marlin/DDasher.java b/src/main/java/sun/java2d/marlin/DDasher.java index 3999b9e..069b6cf 100644 --- a/src/main/java/sun/java2d/marlin/DDasher.java +++ b/src/main/java/sun/java2d/marlin/DDasher.java @@ -202,7 +202,7 @@ void dispose() { @Override public void moveTo(double x0, double y0) { - if (firstSegidx > 0) { + if (firstSegidx != 0) { out.moveTo(sx, sy); emitFirstSegments(); } @@ -251,104 +251,133 @@ private void emitFirstSegments() { private int firstSegidx; // precondition: pts must be in relative coordinates (relative to x0,y0) - private void goTo(double[] pts, int off, final int type) { - double x = pts[off + type - 4]; - double y = pts[off + type - 3]; - if (dashOn) { + private void goTo(double[] pts, int off, final int type, final boolean on) { + final int index = off + type; + final double x = pts[index - 4]; + final double y = pts[index - 3]; + + if (on) { if (starting) { - int len = type - 1; // - 2 + 1 - int segIdx = firstSegidx; - double[] buf = firstSegmentsBuffer; - if (segIdx + len > buf.length) { - if (DO_STATS) { - rdrCtx.stats.stat_array_dasher_firstSegmentsBuffer - .add(segIdx + len); - } - firstSegmentsBuffer = buf - = firstSegmentsBuffer_ref.widenArray(buf, segIdx, - segIdx + len); - } - buf[segIdx++] = type; - len--; - // small arraycopy (2, 4 or 6) but with offset: - System.arraycopy(pts, off, buf, segIdx, len); - segIdx += len; - firstSegidx = segIdx; + goTo_starting(pts, off, type); } else { if (needsMoveTo) { - out.moveTo(x0, y0); needsMoveTo = false; + out.moveTo(x0, y0); } emitSeg(pts, off, type); } } else { - starting = false; + if (starting) { + // low probability test (hotspot) + starting = false; + } needsMoveTo = true; } this.x0 = x; this.y0 = y; } + private void goTo_starting(final double[] pts, final int off, final int type) { + int len = type - 1; // - 2 + 1 + int segIdx = firstSegidx; + double[] buf = firstSegmentsBuffer; + + if (segIdx + len > buf.length) { + if (DO_STATS) { + rdrCtx.stats.stat_array_dasher_firstSegmentsBuffer + .add(segIdx + len); + } + firstSegmentsBuffer = buf + = firstSegmentsBuffer_ref.widenArray(buf, segIdx, + segIdx + len); + } + buf[segIdx++] = type; + len--; + // small arraycopy (2, 4 or 6) but with offset: + System.arraycopy(pts, off, buf, segIdx, len); + firstSegidx = segIdx + len; + } + @Override public void lineTo(double x1, double y1) { - double dx = x1 - x0; - double dy = y1 - y0; + final double dx = x1 - x0; + final double dy = y1 - y0; double len = dx*dx + dy*dy; if (len == 0.0d) { return; } - len = Math.sqrt(len); - // The scaling factors needed to get the dx and dy of the + // The scaling factors [0..1] needed to get the dx and dy of the // transformed dash segments. + len = Math.sqrt(len); final double cx = dx / len; final double cy = dy / len; final double[] _curCurvepts = curCurvepts; final double[] _dash = dash; + final int _dashLen = this.dashLen; + int _idx = idx; + boolean _dashOn = dashOn; + double _phase = phase; + double leftInThisDashSegment; - double dashdx, dashdy, p; + double d, dashdx, dashdy, p; while (true) { - leftInThisDashSegment = _dash[idx] - phase; + d = _dash[_idx]; + leftInThisDashSegment = d - _phase; if (len <= leftInThisDashSegment) { _curCurvepts[0] = x1; _curCurvepts[1] = y1; - goTo(_curCurvepts, 0, 4); + + goTo(_curCurvepts, 0, 4, _dashOn); // Advance phase within current dash segment - phase += len; + _phase += len; + // TODO: compare double values using epsilon: if (len == leftInThisDashSegment) { - phase = 0.0d; - idx = (idx + 1) % dashLen; - dashOn = !dashOn; + _phase = 0.0d; + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; } + + // Save local state: + idx = _idx; + dashOn = _dashOn; + phase = _phase; return; } - dashdx = _dash[idx] * cx; - dashdy = _dash[idx] * cy; + // TODO: out of loop if nb(seg) >> dashLen + // may avoid multiply by precomputation + dashdx = d * cx; + dashdy = d * cy; - if (phase == 0.0d) { + if (_phase == 0.0d) { + // most probable case (99%) _curCurvepts[0] = x0 + dashdx; _curCurvepts[1] = y0 + dashdy; } else { - p = leftInThisDashSegment / _dash[idx]; + // may avoid divide by precomputation + // TODO: out of loop if nb(seg) >> dashLen + p = leftInThisDashSegment / d; + _curCurvepts[0] = x0 + p * dashdx; _curCurvepts[1] = y0 + p * dashdy; } - goTo(_curCurvepts, 0, 4); + goTo(_curCurvepts, 0, 4, _dashOn); len -= leftInThisDashSegment; + // Advance to next dash segment - idx = (idx + 1) % dashLen; - dashOn = !dashOn; - phase = 0.0d; + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; + _phase = 0.0d; } } @@ -361,39 +390,55 @@ private void somethingTo(int type) { if (pointCurve(curCurvepts, type)) { return; } - li.initializeIterationOnCurve(curCurvepts, type); - + final LengthIterator _li = li; + final double[] _curCurvepts = curCurvepts; + final double[] _dash = dash; + final int _dashLen = this.dashLen; + + _li.initializeIterationOnCurve(_curCurvepts, type); + + int _idx = idx; + boolean _dashOn = dashOn; + double _phase = phase; + // initially the current curve is at curCurvepts[0...type] int curCurveoff = 0; double lastSplitT = 0.0d; double t; - double leftInThisDashSegment = dash[idx] - phase; + double leftInThisDashSegment = _dash[_idx] - _phase; - while ((t = li.next(leftInThisDashSegment)) < 1.0d) { + while ((t = _li.next(leftInThisDashSegment)) < 1.0d) { if (t != 0.0d) { DHelpers.subdivideAt((t - lastSplitT) / (1.0d - lastSplitT), - curCurvepts, curCurveoff, - curCurvepts, 0, - curCurvepts, type, type); + _curCurvepts, curCurveoff, + _curCurvepts, 0, + _curCurvepts, type, type); lastSplitT = t; - goTo(curCurvepts, 2, type); + goTo(_curCurvepts, 2, type, _dashOn); curCurveoff = type; } // Advance to next dash segment - idx = (idx + 1) % dashLen; - dashOn = !dashOn; - phase = 0.0d; - leftInThisDashSegment = dash[idx]; + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; + _phase = 0.0d; + leftInThisDashSegment = _dash[_idx]; } - goTo(curCurvepts, curCurveoff+2, type); - phase += li.lastSegLen(); - if (phase >= dash[idx]) { - phase = 0.0d; - idx = (idx + 1) % dashLen; - dashOn = !dashOn; + + goTo(_curCurvepts, curCurveoff+2, type, _dashOn); + + _phase += _li.lastSegLen(); + if (_phase >= _dash[_idx]) { + _phase = 0.0d; + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; } + // Save local state: + idx = _idx; + dashOn = _dashOn; + phase = _phase; + // reset LengthIterator: - li.reset(); + _li.reset(); } private static boolean pointCurve(double[] curve, int type) { @@ -669,11 +714,12 @@ private void goLeft() { // this is a bit of a hack. It returns -1 if we're not on a leaf, and // the length of the leaf if we are on a leaf. private double onLeaf() { - double[] curve = recCurveStack[recLevel]; + final double[] curve = recCurveStack[recLevel]; + final int _curveType = curveType; double polyLen = 0.0d; double x0 = curve[0], y0 = curve[1]; - for (int i = 2; i < curveType; i += 2) { + for (int i = 2; i < _curveType; i += 2) { final double x1 = curve[i], y1 = curve[i+1]; final double len = DHelpers.linelen(x0, y0, x1, y1); polyLen += len; @@ -683,8 +729,8 @@ private double onLeaf() { } final double lineLen = DHelpers.linelen(curve[0], curve[1], - curve[curveType-2], - curve[curveType-1]); + curve[_curveType-2], + curve[_curveType-1]); if ((polyLen - lineLen) < ERR || recLevel == REC_LIMIT) { return (polyLen + lineLen) / 2.0d; } @@ -717,7 +763,7 @@ public void quadTo(double x1, double y1, double x2, double y2) { @Override public void closePath() { lineTo(sx, sy); - if (firstSegidx > 0) { + if (firstSegidx != 0) { if (!dashOn || needsMoveTo) { out.moveTo(sx, sy); } @@ -728,7 +774,7 @@ public void closePath() { @Override public void pathDone() { - if (firstSegidx > 0) { + if (firstSegidx != 0) { out.moveTo(sx, sy); emitFirstSegments(); } diff --git a/src/main/java/sun/java2d/marlin/Dasher.java b/src/main/java/sun/java2d/marlin/Dasher.java index d8f9c9a..4fc9141 100644 --- a/src/main/java/sun/java2d/marlin/Dasher.java +++ b/src/main/java/sun/java2d/marlin/Dasher.java @@ -56,6 +56,7 @@ final class Dasher implements PathConsumer2D, MarlinConst { private float startPhase; private boolean startDashOn; private int startIdx; + private float dashTotal; private boolean starting; private boolean needsMoveTo; @@ -138,7 +139,7 @@ Dasher init(final PathConsumer2D out, float[] dash, int dashLen, dashOn = !dashOn; } } - } else if (phase > 0) { + } else if (phase > 0.0f) { if (cycles >= MAX_CYCLES) { phase = 0.0f; } else { @@ -158,14 +159,18 @@ Dasher init(final PathConsumer2D out, float[] dash, int dashLen, this.dash = dash; this.dashLen = dashLen; - this.startPhase = this.phase = phase; + this.dashTotal = sum; + this.phase = phase; + this.startPhase = phase; this.startDashOn = dashOn; this.startIdx = sidx; this.starting = true; - needsMoveTo = false; - firstSegidx = 0; + this.needsMoveTo = false; + this.firstSegidx = 0; this.recycleDashes = recycleDashes; + +// System.out.println("dashTotal: "+dashTotal); return this; // fluent API } @@ -203,7 +208,7 @@ float[] copyDashArray(final float[] dashes) { @Override public void moveTo(float x0, float y0) { - if (firstSegidx > 0) { + if (firstSegidx != 0) { out.moveTo(sx, sy); emitFirstSegments(); } @@ -211,8 +216,10 @@ public void moveTo(float x0, float y0) { this.idx = startIdx; this.dashOn = this.startDashOn; this.phase = this.startPhase; - this.sx = this.x0 = x0; - this.sy = this.y0 = y0; + this.sx = x0; + this.sy = y0; + this.x0 = x0; + this.y0 = y0; this.starting = true; } @@ -237,7 +244,7 @@ private void emitSeg(float[] buf, int off, int type) { private void emitFirstSegments() { final float[] fSegBuf = firstSegmentsBuffer; - for (int i = 0; i < firstSegidx; ) { + for (int i = 0, len = firstSegidx; i < len; ) { int type = (int)fSegBuf[i]; emitSeg(fSegBuf, i + 1, type); i += (type - 1); @@ -252,109 +259,206 @@ private void emitFirstSegments() { private int firstSegidx; // precondition: pts must be in relative coordinates (relative to x0,y0) - private void goTo(float[] pts, int off, final int type) { - float x = pts[off + type - 4]; - float y = pts[off + type - 3]; - if (dashOn) { + private void goTo(final float[] pts, final int off, final int type, final boolean on) { + final int index = off + type; + final float x = pts[index - 4]; + final float y = pts[index - 3]; + + if (on) { if (starting) { - int len = type - 1; // - 2 + 1 - int segIdx = firstSegidx; - float[] buf = firstSegmentsBuffer; - if (segIdx + len > buf.length) { - if (DO_STATS) { - rdrCtx.stats.stat_array_dasher_firstSegmentsBuffer - .add(segIdx + len); - } - firstSegmentsBuffer = buf - = firstSegmentsBuffer_ref.widenArray(buf, segIdx, - segIdx + len); - } - buf[segIdx++] = type; - len--; - // small arraycopy (2, 4 or 6) but with offset: - System.arraycopy(pts, off, buf, segIdx, len); - segIdx += len; - firstSegidx = segIdx; + goTo_starting(pts, off, type); } else { if (needsMoveTo) { - out.moveTo(x0, y0); needsMoveTo = false; + out.moveTo(x0, y0); } emitSeg(pts, off, type); } } else { - starting = false; + if (starting) { + // low probability test (hotspot) + starting = false; + } needsMoveTo = true; } this.x0 = x; this.y0 = y; } + private void goTo_starting(final float[] pts, final int off, final int type) { + int len = type - 1; // - 2 + 1 + int segIdx = firstSegidx; + float[] buf = firstSegmentsBuffer; + + if (segIdx + len > buf.length) { + if (DO_STATS) { + rdrCtx.stats.stat_array_dasher_firstSegmentsBuffer + .add(segIdx + len); + } + firstSegmentsBuffer = buf + = firstSegmentsBuffer_ref.widenArray(buf, segIdx, + segIdx + len); + } + buf[segIdx++] = type; + len--; + // small arraycopy (2, 4 or 6) but with offset: + System.arraycopy(pts, off, buf, segIdx, len); + firstSegidx = segIdx + len; + } + + private final static boolean USE_NAIVE_SUM = false; + @Override public void lineTo(float x1, float y1) { - float dx = x1 - x0; - float dy = y1 - y0; + final float dx = x1 - x0; + final float dy = y1 - y0; float len = dx*dx + dy*dy; if (len == 0.0f) { return; } - len = (float) Math.sqrt(len); - // The scaling factors needed to get the dx and dy of the + // The scaling factors [0..1] needed to get the dx and dy of the // transformed dash segments. + len = (float) Math.sqrt(len); final float cx = dx / len; final float cy = dy / len; - + + final float dcx = Math.ulp(cx); + final float dcy = Math.ulp(cy); +/* + System.out.println("cx: "+cx + " cy: " + cy); + System.out.println("len vs dash: "+len + " vs " + dashTotal + " ratio = "+(len / dashTotal)); +*/ final float[] _curCurvepts = curCurvepts; final float[] _dash = dash; + final int _dashLen = this.dashLen; + + int _idx = idx; + boolean _dashOn = dashOn; + float _phase = phase; float leftInThisDashSegment; - float dashdx, dashdy, p; + float el = 0.0f, ex = 0.0f, ey = 0.0f; + + float d, x, y, dashdx, dashdy, p; + float z, t; while (true) { - leftInThisDashSegment = _dash[idx] - phase; + d = _dash[_idx]; + /* 0 < leftInThisDashSegment < dashTotal ie SMALL */ + leftInThisDashSegment = d - _phase; if (len <= leftInThisDashSegment) { _curCurvepts[0] = x1; _curCurvepts[1] = y1; - goTo(_curCurvepts, 0, 4); + + goTo(_curCurvepts, 0, 4, _dashOn); // Advance phase within current dash segment - phase += len; + _phase += len; + // TODO: compare float values using epsilon: if (len == leftInThisDashSegment) { - phase = 0.0f; - idx = (idx + 1) % dashLen; - dashOn = !dashOn; + _phase = 0.0f; + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; } + + // Save local state: + idx = _idx; + dashOn = _dashOn; + phase = _phase; return; } - dashdx = _dash[idx] * cx; - dashdy = _dash[idx] * cy; - - if (phase == 0.0f) { - _curCurvepts[0] = x0 + dashdx; - _curCurvepts[1] = y0 + dashdy; + // TODO: out of loop if nb(seg) >> dashLen + // may avoid multiply by precomputation + dashdx = d * cx; + dashdy = d * cy; + + // Numeric precision problem: d << (x0,y0) + x = this.x0; + y = this.y0; + + if (_phase == 0.0f) { + // most probable case (99%) +if (USE_NAIVE_SUM) { + // naive sum: + _curCurvepts[0] = x + dashdx; + _curCurvepts[1] = y + dashdy; +} else { + // kahan sum: + z = dashdx - ex; + t = x + z; + ex = (t - x) - z - dcx; + _curCurvepts[0] = t; +// System.out.println("z: "+z + " t: "+t + " e: "+ex); + + // kahan sum: + z = dashdy - ey; + t = y + z; + ey = (t - y) - z - dcy; + _curCurvepts[1] = t; +// System.out.println("z: "+z + " t: "+t + " e: "+ey); +} } else { - p = leftInThisDashSegment / _dash[idx]; - _curCurvepts[0] = x0 + p * dashdx; - _curCurvepts[1] = y0 + p * dashdy; +// System.out.println("phase != 0 at: "+len); + + // may avoid divide by precomputation + // TODO: out of loop if nb(seg) >> dashLen + p = leftInThisDashSegment / d; + +if (USE_NAIVE_SUM) { + // naive sum: + _curCurvepts[0] = x + p * dashdx; + _curCurvepts[1] = y + p * dashdy; +} else { + // kahan sum: + z = (p * dashdx) - ex; + t = x + z; + ex = (t - x) - z - dcx; + _curCurvepts[0] = t; +// System.out.println("z: "+z + " t: "+t + " e: "+ex); + + // kahan sum: + z = (p * dashdy) - ey; + t = y + z; + ey = (t - y) - z - dcy; + _curCurvepts[1] = t; +// System.out.println("z: "+z + " t: "+t + " e: "+ey); +} } - goTo(_curCurvepts, 0, 4); + goTo(_curCurvepts, 0, 4, _dashOn); +if (USE_NAIVE_SUM) { + // naive sum: len -= leftInThisDashSegment; +} else { + // Very high dynamic: (len / dashTotal) > 4million + // TODO: find the proper threshold with accuracy ie dynamic range is larger than float precision ! + // see ulps ! ie smallest dash length vs line length ~ 5e6 ? + + // kahan sum: + z = leftInThisDashSegment - el; + t = len - z; + el = (len - t) - z; + len = t; +// System.out.println("z: "+z + " t: "+t + " e: "+el); +} + // Advance to next dash segment - idx = (idx + 1) % dashLen; - dashOn = !dashOn; - phase = 0.0f; + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; + _phase = 0.0f; } - } - + } + // shared instance in Dasher private final LengthIterator li = new LengthIterator(); + + private final float[] initCurvepts = new float[8]; // preconditions: curCurvepts must be an array of length at least 2 * type, // that contains the curve we want to dash in the first type elements @@ -362,39 +466,104 @@ private void somethingTo(int type) { if (pointCurve(curCurvepts, type)) { return; } - li.initializeIterationOnCurve(curCurvepts, type); + final LengthIterator _li = li; + final float[] _curCurvepts = curCurvepts; + final float[] _dash = dash; + final int _dashLen = this.dashLen; + + if (true) { + // backup (lolo): + // optimize arraycopy (8 values faster than 6 = type): + System.arraycopy(_curCurvepts, 0, initCurvepts, 0, 8); + } + _li.initializeIterationOnCurve(_curCurvepts, type); + + int _idx = idx; + boolean _dashOn = dashOn; + float _phase = phase; + // initially the current curve is at curCurvepts[0...type] - int curCurveoff = 0; + int curCurveoff = 0; // type float lastSplitT = 0.0f; float t; - float leftInThisDashSegment = dash[idx] - phase; - - while ((t = li.next(leftInThisDashSegment)) < 1.0f) { + float leftInThisDashSegment = _dash[_idx] - _phase; + +// int n = 0; + int c = 0; + + while ((t = _li.next(leftInThisDashSegment)) < 1.0f) { +/* + if (t != 0.0f && _dashOn) { + // special case next 2 lines for prevt=0 and t=1 +// left,right = subdivide curve at prevt + Helpers.subdivideAt(lastSplitT, + initCurvepts, 0, + _curCurvepts, 0, + _curCurvepts, type, type); +// left,right = subdivide right at (t-prevt)/(1-prevt) + Helpers.subdivideAt((t - lastSplitT) / (1.0f - lastSplitT), + _curCurvepts, type, + _curCurvepts, 0, + _curCurvepts, type, type); +// emit left; + goTo(_curCurvepts, 2, type, _dashOn); + // curve is not modified + // right is not saved + } +// prevt = t; + lastSplitT = t; + // Advance to next dash segment + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; + _phase = 0.0f; + leftInThisDashSegment = _dash[_idx]; +*/ if (t != 0.0f) { Helpers.subdivideAt((t - lastSplitT) / (1.0f - lastSplitT), - curCurvepts, curCurveoff, - curCurvepts, 0, - curCurvepts, type, type); + _curCurvepts, curCurveoff, + _curCurvepts, 0, + _curCurvepts, type, type); lastSplitT = t; - goTo(curCurvepts, 2, type); + goTo(_curCurvepts, 2, type, _dashOn); curCurveoff = type; } // Advance to next dash segment - idx = (idx + 1) % dashLen; - dashOn = !dashOn; - phase = 0.0f; - leftInThisDashSegment = dash[idx]; + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; + _phase = 0.0f; + leftInThisDashSegment = _dash[_idx]; + + if (true) { + if ((++c) == 10000) { + // Split from initial curve: + Helpers.subdivideAt(lastSplitT, + initCurvepts, 0, + _curCurvepts, 0, + _curCurvepts, type, type); + c = 0; + } + } +// n++; } - goTo(curCurvepts, curCurveoff+2, type); - phase += li.lastSegLen(); - if (phase >= dash[idx]) { - phase = 0.0f; - idx = (idx + 1) % dashLen; - dashOn = !dashOn; + +// System.out.println("somethingTo("+type+") : "+n+" iterations."); + + goTo(_curCurvepts, curCurveoff + 2, type, _dashOn); + + _phase += _li.lastSegLen(); + if (_phase >= _dash[_idx]) { + _phase = 0.0f; + _idx = (_idx + 1) % _dashLen; + _dashOn = !_dashOn; } + // Save local state: + idx = _idx; + dashOn = _dashOn; + phase = _phase; + // reset LengthIterator: - li.reset(); + _li.reset(); } private static boolean pointCurve(float[] curve, int type) { @@ -670,11 +839,12 @@ private void goLeft() { // this is a bit of a hack. It returns -1 if we're not on a leaf, and // the length of the leaf if we are on a leaf. private float onLeaf() { - float[] curve = recCurveStack[recLevel]; + final float[] curve = recCurveStack[recLevel]; + final int _curveType = curveType; float polyLen = 0.0f; float x0 = curve[0], y0 = curve[1]; - for (int i = 2; i < curveType; i += 2) { + for (int i = 2; i < _curveType; i += 2) { final float x1 = curve[i], y1 = curve[i+1]; final float len = Helpers.linelen(x0, y0, x1, y1); polyLen += len; @@ -684,8 +854,8 @@ private float onLeaf() { } final float lineLen = Helpers.linelen(curve[0], curve[1], - curve[curveType-2], - curve[curveType-1]); + curve[_curveType-2], + curve[_curveType-1]); if ((polyLen - lineLen) < ERR || recLevel == REC_LIMIT) { return (polyLen + lineLen) / 2.0f; } @@ -718,7 +888,7 @@ public void quadTo(float x1, float y1, float x2, float y2) { @Override public void closePath() { lineTo(sx, sy); - if (firstSegidx > 0) { + if (firstSegidx != 0) { if (!dashOn || needsMoveTo) { out.moveTo(sx, sy); } @@ -729,7 +899,7 @@ public void closePath() { @Override public void pathDone() { - if (firstSegidx > 0) { + if (firstSegidx != 0) { out.moveTo(sx, sy); emitFirstSegments(); } diff --git a/src/main/java/sun/java2d/marlin/Helpers.java b/src/main/java/sun/java2d/marlin/Helpers.java index 85b9f36..8ab4864 100644 --- a/src/main/java/sun/java2d/marlin/Helpers.java +++ b/src/main/java/sun/java2d/marlin/Helpers.java @@ -30,6 +30,9 @@ import static java.lang.Math.sqrt; import static java.lang.Math.cbrt; import static java.lang.Math.acos; +import java.util.Arrays; +import sun.awt.geom.PathConsumer2D; +import static sun.java2d.marlin.MarlinConst.INITIAL_EDGES_COUNT; final class Helpers implements MarlinConst { @@ -176,15 +179,6 @@ static int filterOutNotInAB(float[] nums, final int off, final int len, return ret; } - static float polyLineLength(float[] poly, final int off, final int nCoords) { - assert nCoords % 2 == 0 && poly.length >= off + nCoords : ""; - float acc = 0.0f; - for (int i = off + 2; i < off + nCoords; i += 2) { - acc += linelen(poly[i], poly[i+1], poly[i-2], poly[i-1]); - } - return acc; - } - static float linelen(float x1, float y1, float x2, float y2) { final float dx = x2 - x1; final float dy = y2 - y1; @@ -438,4 +432,271 @@ static void subdivideAt(float t, float[] src, int srcoff, return; } } + + // From sun.java2d.loops.GeneralRenderer: + + static final int OUTCODE_TOP = 1; + static final int OUTCODE_BOTTOM = 2; + static final int OUTCODE_LEFT = 4; + static final int OUTCODE_RIGHT = 8; + + static int outcode(final float x, final float y, + final float[] clipRect) + { + int code; + if (y < clipRect[0]) { + code = OUTCODE_TOP; + } else if (y >= clipRect[1]) { + code = OUTCODE_BOTTOM; + } else { + code = 0; + } + if (x < clipRect[2]) { + code |= OUTCODE_LEFT; + } else if (x >= clipRect[3]) { + code |= OUTCODE_RIGHT; + } + return code; + } + + // a stack of polynomial curves where each curve shares endpoints with + // adjacent ones. + static final class PolyStack { + private static final byte TYPE_LINETO = (byte) 0; + private static final byte TYPE_QUADTO = (byte) 1; + private static final byte TYPE_CUBICTO = (byte) 2; + + // curves capacity = edges count (8192) = edges x 2 (coords) + private static final int INITIAL_CURVES_COUNT = INITIAL_EDGES_COUNT << 1; + + // types capacity = edges count (4096) + private static final int INITIAL_TYPES_COUNT = INITIAL_EDGES_COUNT; + + float[] curves; + int end; + byte[] curveTypes; + int numCurves; + + // per-thread renderer context + final RendererContext rdrCtx; + + // curves ref (dirty) + final FloatArrayCache.Reference curves_ref; + // curveTypes ref (dirty) + final ByteArrayCache.Reference curveTypes_ref; + + // used marks (stats only) + int curveTypesUseMark; + int curvesUseMark; + + /** + * Constructor + * @param rdrCtx per-thread renderer context + */ + PolyStack(final RendererContext rdrCtx) { + this.rdrCtx = rdrCtx; + + curves_ref = rdrCtx.newDirtyFloatArrayRef(INITIAL_CURVES_COUNT); // 32K + curves = curves_ref.initial; + + curveTypes_ref = rdrCtx.newDirtyByteArrayRef(INITIAL_TYPES_COUNT); // 4K + curveTypes = curveTypes_ref.initial; + numCurves = 0; + end = 0; + + if (DO_STATS) { + curveTypesUseMark = 0; + curvesUseMark = 0; + } + } + + /** + * Disposes this PolyStack: + * clean up before reusing this instance + */ + void dispose() { + end = 0; + numCurves = 0; + + if (DO_STATS) { + rdrCtx.stats.stat_rdr_poly_stack_types.add(curveTypesUseMark); + rdrCtx.stats.stat_rdr_poly_stack_curves.add(curvesUseMark); + rdrCtx.stats.hist_rdr_poly_stack_curves.add(curvesUseMark); + + // reset marks + curveTypesUseMark = 0; + curvesUseMark = 0; + } + + // Return arrays: + // curves and curveTypes are kept dirty + curves = curves_ref.putArray(curves); + curveTypes = curveTypes_ref.putArray(curveTypes); + } + + boolean isEmpty() { + return (numCurves == 0); + } + + private void ensureSpace(final int n) { + // use substraction to avoid integer overflow: + if (curves.length - end < n) { + if (DO_STATS) { + rdrCtx.stats.stat_array_stroker_polystack_curves + .add(end + n); + } + curves = curves_ref.widenArray(curves, end, end + n); + } + if (curveTypes.length <= numCurves) { + if (DO_STATS) { + rdrCtx.stats.stat_array_stroker_polystack_curveTypes + .add(numCurves + 1); + } + curveTypes = curveTypes_ref.widenArray(curveTypes, + numCurves, + numCurves + 1); + } + } + + void pushCubic(float x0, float y0, + float x1, float y1, + float x2, float y2) + { + ensureSpace(6); + curveTypes[numCurves++] = TYPE_CUBICTO; + // we reverse the coordinate order to make popping easier + final float[] _curves = curves; + int e = end; + _curves[e++] = x2; _curves[e++] = y2; + _curves[e++] = x1; _curves[e++] = y1; + _curves[e++] = x0; _curves[e++] = y0; + end = e; + } + + void pushQuad(float x0, float y0, + float x1, float y1) + { + ensureSpace(4); + curveTypes[numCurves++] = TYPE_QUADTO; + final float[] _curves = curves; + int e = end; + _curves[e++] = x1; _curves[e++] = y1; + _curves[e++] = x0; _curves[e++] = y0; + end = e; + } + + void pushLine(float x, float y) { + ensureSpace(2); + curveTypes[numCurves++] = TYPE_LINETO; + curves[end++] = x; curves[end++] = y; + } + + void pullAll(PathConsumer2D io) { + if (DO_STATS) { + // update used marks: + if (numCurves > curveTypesUseMark) { + curveTypesUseMark = numCurves; + } + if (end > curvesUseMark) { + curvesUseMark = end; + } + } + final byte[] _curveTypes = curveTypes; + final float[] _curves = curves; + final int nc = numCurves; + int e = 0; + + for (int i = 0; i < nc; i++) { + switch(_curveTypes[i]) { + case TYPE_LINETO: + io.lineTo(_curves[e], _curves[e+1]); + e += 2; + continue; + case TYPE_QUADTO: + io.quadTo(_curves[e+0], _curves[e+1], + _curves[e+2], _curves[e+3]); + e += 4; + continue; + case TYPE_CUBICTO: + io.curveTo(_curves[e+0], _curves[e+1], + _curves[e+2], _curves[e+3], + _curves[e+4], _curves[e+5]); + e += 6; + continue; + default: + } + } + numCurves = 0; + end = 0; + } + + void popAll(PathConsumer2D io) { + if (DO_STATS) { + // update used marks: + if (numCurves > curveTypesUseMark) { + curveTypesUseMark = numCurves; + } + if (end > curvesUseMark) { + curvesUseMark = end; + } + } + final byte[] _curveTypes = curveTypes; + final float[] _curves = curves; + int nc = numCurves; + int e = end; + + while (nc != 0) { + switch(_curveTypes[--nc]) { + case TYPE_LINETO: + e -= 2; + io.lineTo(_curves[e], _curves[e+1]); + continue; + case TYPE_QUADTO: + e -= 4; + io.quadTo(_curves[e+0], _curves[e+1], + _curves[e+2], _curves[e+3]); + continue; + case TYPE_CUBICTO: + e -= 6; + io.curveTo(_curves[e+0], _curves[e+1], + _curves[e+2], _curves[e+3], + _curves[e+4], _curves[e+5]); + continue; + default: + } + } + numCurves = 0; + end = 0; + } + + @Override + public String toString() { + String ret = ""; + int nc = numCurves; + int last = end; + int len; + while (nc != 0) { + switch(curveTypes[--nc]) { + case TYPE_LINETO: + len = 2; + ret += "line: "; + break; + case TYPE_QUADTO: + len = 4; + ret += "quad: "; + break; + case TYPE_CUBICTO: + len = 6; + ret += "cubic: "; + break; + default: + len = 0; + } + last -= len; + ret += Arrays.toString(Arrays.copyOfRange(curves, last, last+len)) + + "\n"; + } + return ret; + } + } } diff --git a/src/main/java/sun/java2d/marlin/MarlinProperties.java b/src/main/java/sun/java2d/marlin/MarlinProperties.java index 0bb13ff..9ba8f0a 100644 --- a/src/main/java/sun/java2d/marlin/MarlinProperties.java +++ b/src/main/java/sun/java2d/marlin/MarlinProperties.java @@ -145,6 +145,11 @@ public static boolean isUseSimplifier() { return getBoolean("sun.java2d.renderer.useSimplifier", "false"); } + public static boolean isDoClip() { + return getBoolean("sun.java2d.renderer.clip", "true"); + } + + // TODO: remove public static boolean isDoClipCurves() { return getBoolean("sun.java2d.renderer.clip.curves", "false"); } diff --git a/src/main/java/sun/java2d/marlin/MarlinRenderingEngine.java b/src/main/java/sun/java2d/marlin/MarlinRenderingEngine.java index 217a787..95fa9c2 100644 --- a/src/main/java/sun/java2d/marlin/MarlinRenderingEngine.java +++ b/src/main/java/sun/java2d/marlin/MarlinRenderingEngine.java @@ -84,6 +84,9 @@ abstract PathIterator getNormalizingPathIterator(RendererContext rdrCtx, static final float UPPER_BND = Float.MAX_VALUE / 2.0f; static final float LOWER_BND = -UPPER_BND; + + static final boolean DO_CLIP = MarlinProperties.isDoClip(); + static final boolean DO_TRACE = false; /** * Public constructor @@ -324,6 +327,7 @@ final void strokeTo(final RendererContext rdrCtx, int dashLen = -1; boolean recycleDashes = false; + float scale = 1.0f; if (at != null && !at.isIdentity()) { final double a = at.getScaleX(); @@ -356,7 +360,7 @@ final void strokeTo(final RendererContext rdrCtx, // a*b == -c*d && a*a+c*c == b*b+d*d. In the actual check below, we // leave a bit of room for error. if (nearZero(a*b + c*d) && nearZero(a*a + c*c - (b*b + d*d))) { - final float scale = (float) Math.sqrt(a*a + c*c); + scale = (float) Math.sqrt(a*a + c*c); if (dashes != null) { recycleDashes = true; @@ -394,16 +398,24 @@ final void strokeTo(final RendererContext rdrCtx, at = null; } + final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D; + + if (DO_TRACE) { + // trace Stroker: + pc2d = transformerPC2D.traceStroker(pc2d); + } + if (USE_SIMPLIFIER) { // Use simplifier after stroker before Renderer // to remove collinear segments (notably due to cap square) pc2d = rdrCtx.simplifier.init(pc2d); } - - final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D; + + // deltaTransformConsumer may adjust the clip rectangle: pc2d = transformerPC2D.deltaTransformConsumer(pc2d, strokerat); - - pc2d = rdrCtx.stroker.init(pc2d, width, caps, join, miterlimit); + + // stroker will adjust the clip rectangle (width / miter limit): + pc2d = rdrCtx.stroker.init(pc2d, width, caps, join, miterlimit, scale); if (dashes != null) { if (!recycleDashes) { @@ -411,9 +423,22 @@ final void strokeTo(final RendererContext rdrCtx, } pc2d = rdrCtx.dasher.init(pc2d, dashes, dashLen, dashphase, recycleDashes); + } else if ((rdrCtx.doClip == 1) && (caps != Stroker.CAP_BUTT)) { + if (DO_TRACE) { + pc2d = transformerPC2D.traceClosedPathDetector(pc2d); + } + + // If no dash and clip is enabled: + // detect closedPaths (polygons) for caps + pc2d = transformerPC2D.detectClosedPath(pc2d); } pc2d = transformerPC2D.inverseDeltaTransformConsumer(pc2d, strokerat); + if (DO_TRACE) { + // trace Input: + pc2d = transformerPC2D.traceInput(pc2d); + } + final PathIterator pi = norm.getNormalizingPathIterator(rdrCtx, src.getPathIterator(at)); @@ -802,6 +827,18 @@ public AATileGenerator getAATileGenerator(Shape s, r = rdrCtx.renderer.init(clip.getLoX(), clip.getLoY(), clip.getWidth(), clip.getHeight(), PathIterator.WIND_NON_ZERO); + + if (DO_CLIP) { + // Define the initial clip bounds: + final float[] clipRect = rdrCtx.clipRect; + clipRect[0] = clip.getLoY(); + clipRect[1] = clip.getLoY() + clip.getHeight(); + clipRect[2] = clip.getLoX(); + clipRect[3] = clip.getLoX() + clip.getWidth(); + + // Enable clipping: + rdrCtx.doClip = 1; + } strokeTo(rdrCtx, s, _at, bs, thin, norm, true, r); } @@ -860,8 +897,8 @@ public final AATileGenerator getAATileGenerator(double x, double y, final RendererContext rdrCtx = getRendererContext(); try { r = rdrCtx.renderer.init(clip.getLoX(), clip.getLoY(), - clip.getWidth(), clip.getHeight(), - Renderer.WIND_EVEN_ODD); + clip.getWidth(), clip.getHeight(), + Renderer.WIND_EVEN_ODD); r.moveTo((float) x, (float) y); r.lineTo((float) (x+dx1), (float) (y+dy1)); @@ -1045,6 +1082,8 @@ private static void logSettings(final String reClass) { // optimisation parameters logInfo("sun.java2d.renderer.useSimplifier = " + MarlinConst.USE_SIMPLIFIER); + logInfo("sun.java2d.renderer.clip = " + + MarlinProperties.isDoClip()); logInfo("sun.java2d.renderer.clip.curves = " + MarlinProperties.isDoClipCurves()); diff --git a/src/main/java/sun/java2d/marlin/Renderer.java b/src/main/java/sun/java2d/marlin/Renderer.java index 1ca76b2..fdd2ced 100644 --- a/src/main/java/sun/java2d/marlin/Renderer.java +++ b/src/main/java/sun/java2d/marlin/Renderer.java @@ -49,6 +49,10 @@ final class Renderer implements PathConsumer2D, MarlinRenderer { static final float SUBPIXEL_SCALE_Y = (float) SUBPIXEL_POSITIONS_Y; static final int SUBPIXEL_MASK_X = SUBPIXEL_POSITIONS_X - 1; static final int SUBPIXEL_MASK_Y = SUBPIXEL_POSITIONS_Y - 1; + + static final float RDR_OFFSET_X = 0.501f / SUBPIXEL_POSITIONS_X; + static final float RDR_OFFSET_Y = 0.501f / SUBPIXEL_POSITIONS_Y; + // number of subpixels corresponding to a tile line private static final int SUBPIXEL_TILE @@ -231,6 +235,8 @@ private void quadBreakIntoLinesAndAdd(float x0, float y0, } } + private final static boolean USE_NAIVE_SUM = false; + // x0, y0 and x3,y3 are the endpoints of the curve. We could compute these // using c.xat(0),c.yat(0) and c.xat(1),c.yat(1), but this might introduce // numerical errors, and our callers already have the exact values. @@ -262,6 +268,9 @@ private void curveBreakIntoLinesAndAdd(float x0, float y0, final float _DEC_BND = CUB_DEC_BND; final float _INC_BND = CUB_INC_BND; + float z, t; + float ex = 0.0f, ey = 0.0f; + while (count > 0) { // divide step by half: while (Math.abs(ddx) + Math.abs(ddy) >= _DEC_BND) { @@ -296,12 +305,31 @@ private void curveBreakIntoLinesAndAdd(float x0, float y0, } } if (--count > 0) { +if (USE_NAIVE_SUM) { x1 += dx; dx += ddx; ddx += dddx; y1 += dy; dy += ddy; ddy += dddy; +} else { + // kahan sum: + z = dx - ex; + t = x1 + z; + ex = (t - x1) - z; + x1 = t; + // error are small enough: + dx += ddx; + ddx += dddx; + // kahan sum: + z = dy - ey; + t = y1 + z; + ey = (t - y1) - z; + y1 = t; + // error are small enough: + dy += ddy; + ddy += dddy; +} } else { x1 = x3; y1 = y3; @@ -313,6 +341,10 @@ private void curveBreakIntoLinesAndAdd(float x0, float y0, x0 = x1; y0 = y1; } +/* + System.out.println("Iterations : "+n); + System.out.println("Errors x/y : "+ex+" , "+ey); +*/ if (DO_STATS) { rdrCtx.stats.stat_rdr_curveBreak.add(nL); } @@ -862,9 +894,12 @@ private void min_max4(float v1, float v2, float v3, float v4, @Override public void closePath() { - addLine(x0, y0, sx0, sy0); - x0 = sx0; - y0 = sy0; + if (x0 != sx0 || y0 != sy0) { +// System.out.println("Renderer.closePath: from ("+x0+","+y0+") to ("+sx0+","+sy0+")"); + addLine(x0, y0, sx0, sy0); + x0 = sx0; + y0 = sy0; + } } @Override diff --git a/src/main/java/sun/java2d/marlin/RendererContext.java b/src/main/java/sun/java2d/marlin/RendererContext.java index 3b2546a..b4fd68e 100644 --- a/src/main/java/sun/java2d/marlin/RendererContext.java +++ b/src/main/java/sun/java2d/marlin/RendererContext.java @@ -25,6 +25,7 @@ package sun.java2d.marlin; +import java.awt.Rectangle; import java.awt.geom.Path2D; import java.lang.ref.WeakReference; import java.util.concurrent.atomic.AtomicInteger; @@ -75,6 +76,12 @@ static RendererContext createContext() { final MarlinCache cache; // flag indicating the shape is stroked (1) or filled (0) int stroking = 0; + // flag indicating to clip the shape + int doClip = 0; + // clip rectangle (ymin, ymax, xmin, xmax): + final float[] clipRect = new float[4]; + // flag indicating if the path is closed or not (in advance) to handle properly caps + int closedPath = 0; // Array caches: /* clean int[] cache (zero-filled) = 5 refs */ @@ -116,7 +123,7 @@ static RendererContext createContext() { nPQPathIterator = new NormalizingPathIterator.NearestPixelQuarter(float6); // MarlinRenderingEngine.TransformingPathConsumer2D - transformerPC2D = new TransformingPathConsumer2D(); + transformerPC2D = new TransformingPathConsumer2D(this); // Renderer: cache = new MarlinCache(this); @@ -138,7 +145,10 @@ void dispose() { } stats.totalOffHeap = 0L; } - stroking = 0; + stroking = 0; + doClip = 0; + closedPath = 0; + // if context is maked as DIRTY: if (dirty) { // may happen if an exception if thrown in the pipeline processing: diff --git a/src/main/java/sun/java2d/marlin/Stroker.java b/src/main/java/sun/java2d/marlin/Stroker.java index c98b3f0..6dc4da1 100644 --- a/src/main/java/sun/java2d/marlin/Stroker.java +++ b/src/main/java/sun/java2d/marlin/Stroker.java @@ -28,6 +28,7 @@ import java.util.Arrays; import sun.awt.geom.PathConsumer2D; +import sun.java2d.marlin.Helpers.PolyStack; // TODO: some of the arithmetic here is too verbose and prone to hard to // debug typos. We should consider making a small Point/Vector class that @@ -120,6 +121,20 @@ final class Stroker implements PathConsumer2D, MarlinConst { // dirty curve final Curve curve; + // Bounds of the drawing region, at pixel precision. + private float[] clipRect; + + // the outcode of the current point + private int cOutCode = 0; + + // the outcode of the starting point + private int sOutCode = 0; + + // flag indicating if the path is opened (clipped) + private boolean opened = false; + // flag indicating if the starting point's cap is done + private boolean capStart = false; + /** * Constructs a Stroker. * @param rdrCtx per-thread renderer context @@ -143,13 +158,15 @@ final class Stroker implements PathConsumer2D, MarlinConst { * JOIN_MITER, JOIN_ROUND or * JOIN_BEVEL. * @param miterLimit the desired miter limit + * @param scale scaling factor applied to clip boundaries * @return this instance */ - Stroker init(PathConsumer2D pc2d, - float lineWidth, - int capStyle, - int joinStyle, - float miterLimit) + Stroker init(final PathConsumer2D pc2d, + final float lineWidth, + final int capStyle, + final int joinStyle, + final float miterLimit, + final float scale) { this.out = pc2d; @@ -158,13 +175,50 @@ Stroker init(PathConsumer2D pc2d, this.capStyle = capStyle; this.joinStyle = joinStyle; - float limit = miterLimit * lineWidth2; + final float limit = miterLimit * lineWidth2; this.miterLimitSq = limit * limit; this.prev = CLOSE; rdrCtx.stroking = 1; + if (rdrCtx.doClip == 1) { + // Adjust the clipping rectangle with the stroker margin (miter limit, width) +// System.out.println("Stroker: adjust clip"); + + // round joins / caps: + final float widthLimit = + ((joinStyle == JOIN_ROUND) || (capStyle == CAP_ROUND)) ? C * lineWidth // why 0.55 ? + : lineWidth2; + + float boundsMargin; + if (joinStyle == JOIN_MITER) { + boundsMargin = Math.max(widthLimit, limit); + } else { + boundsMargin = widthLimit; + } + float rdrOffX = 0.0f, rdrOffY = 0.0f; + + if (scale != 1.0f) { + boundsMargin *= scale; + rdrOffX = scale * Renderer.RDR_OFFSET_X; + rdrOffY = scale * Renderer.RDR_OFFSET_Y; + } + + // bounds as half-open intervals: minX <= x < maxX and minY <= y < maxY + // adjust clip rectangle (ymin, ymax, xmin, xmax): + final float[] _clipRect = rdrCtx.clipRect; + // TODO: offset X/Y must be scaled also ! + _clipRect[0] -= boundsMargin - rdrOffY; + _clipRect[1] += boundsMargin + rdrOffY; + _clipRect[2] -= boundsMargin - rdrOffX; + _clipRect[3] += boundsMargin + rdrOffX; +// System.out.println("clip: "+Arrays.toString(_clipRect)); + + this.clipRect = _clipRect; + } else { + this.clipRect = null; + } return this; // fluent API } @@ -174,6 +228,9 @@ Stroker init(PathConsumer2D pc2d, */ void dispose() { reverse.dispose(); + + capStart = false; + opened = false; if (DO_CLEAN_DIRTY) { // Force zero-fill dirty arrays: @@ -446,18 +503,62 @@ private void drawMiter(final float pdx, final float pdy, @Override public void moveTo(float x0, float y0) { - if (prev == DRAWING_OP_TO) { - finish(); + moveTo(x0, y0, cOutCode); + // update starting point: + this.sx0 = x0; + this.sy0 = y0; + this.sdx = 1.0f; + this.sdy = 0.0f; + this.opened = false; + this.capStart = false; + + if (clipRect != null) { + final int outcode = Helpers.outcode(x0, y0, clipRect); + this.cOutCode = outcode; + this.sOutCode = outcode; + } + } + + private void moveTo(float x0, float y0, + final int outcode) + { + if (prev == MOVE_TO) { + this.cx0 = x0; + this.cy0 = y0; + } else { + if (prev == DRAWING_OP_TO) { + finish(outcode); + } + this.prev = MOVE_TO; + this.cx0 = x0; + this.cy0 = y0; + this.cdx = 1.0f; + this.cdy = 0.0f; } - this.sx0 = this.cx0 = x0; - this.sy0 = this.cy0 = y0; - this.cdx = this.sdx = 1.0f; - this.cdy = this.sdy = 0.0f; - this.prev = MOVE_TO; } @Override public void lineTo(float x1, float y1) { + lineTo(x1, y1, false); + } + + private void lineTo(float x1, float y1, boolean force) { + final int outcode0 = this.cOutCode; + if (!force && clipRect != null) { + final int outcode1 = Helpers.outcode(x1, y1, clipRect); + this.cOutCode = outcode1; + +// System.out.println("outcodes: "+outcode0 + " " + outcode1); + + // basic rejection criteria + if ((outcode0 & outcode1) != 0) { +// System.out.println("lineTo: Ignore: "+x1 + " "+y1); + moveTo(x1, y1, outcode0); + opened = true; + return; + } + } + float dx = x1 - cx0; float dy = y1 - cy0; if (dx == 0.0f && dy == 0.0f) { @@ -467,7 +568,7 @@ public void lineTo(float x1, float y1) { final float mx = offset0[0]; final float my = offset0[1]; - drawJoin(cdx, cdy, cx0, cy0, dx, dy, cmx, cmy, mx, my); + drawJoin(cdx, cdy, cx0, cy0, dx, dy, cmx, cmy, mx, my, outcode0); emitLineTo(cx0 + mx, cy0 + my); emitLineTo( x1 + mx, y1 + my); @@ -475,43 +576,67 @@ public void lineTo(float x1, float y1) { emitLineToRev(cx0 - mx, cy0 - my); emitLineToRev( x1 - mx, y1 - my); - this.cmx = mx; - this.cmy = my; - this.cdx = dx; - this.cdy = dy; + this.prev = DRAWING_OP_TO; this.cx0 = x1; this.cy0 = y1; - this.prev = DRAWING_OP_TO; + this.cdx = dx; + this.cdy = dy; + this.cmx = mx; + this.cmy = my; } @Override public void closePath() { - if (prev != DRAWING_OP_TO) { + // distinguish empty path at all vs opened path ? + if (prev != DRAWING_OP_TO && !opened) { if (prev == CLOSE) { return; } emitMoveTo(cx0, cy0 - lineWidth2); - this.cmx = this.smx = 0.0f; - this.cmy = this.smy = -lineWidth2; - this.cdx = this.sdx = 1.0f; - this.cdy = this.sdy = 0.0f; - finish(); + + this.sdx = 1.0f; + this.sdy = 0.0f; + this.cdx = 1.0f; + this.cdy = 0.0f; + + this.smx = 0.0f; + this.smy = -lineWidth2; + this.cmx = 0.0f; + this.cmy = -lineWidth2; + + finish(cOutCode); return; } + +// System.out.println("closePath: "+sx0 + " " + sy0 + " from "+cx0 + " "+cy0); if (cx0 != sx0 || cy0 != sy0) { - lineTo(sx0, sy0); + lineTo(sx0, sy0, true); } - drawJoin(cdx, cdy, cx0, cy0, sdx, sdy, cmx, cmy, smx, smy); + drawJoin(cdx, cdy, cx0, cy0, sdx, sdy, cmx, cmy, smx, smy, cOutCode); emitLineTo(sx0 + smx, sy0 + smy); - emitMoveTo(sx0 - smx, sy0 - smy); + if (opened) { + emitLineTo(sx0 - smx, sy0 - smy); + } else { + emitMoveTo(sx0 - smx, sy0 - smy); + } + +// System.out.println("emitReverse"); + + // Ignore caps like finish(false) emitReverse(); this.prev = CLOSE; - emitClose(); + + if (opened) { + // do not emit close + opened = false; + } else { + emitClose(); + } } private void emitReverse() { @@ -521,7 +646,7 @@ private void emitReverse() { @Override public void pathDone() { if (prev == DRAWING_OP_TO) { - finish(); + finish(cOutCode); } out.pathDone(); @@ -534,26 +659,45 @@ public void pathDone() { dispose(); } - private void finish() { - if (capStyle == CAP_ROUND) { - drawRoundCap(cx0, cy0, cmx, cmy); - } else if (capStyle == CAP_SQUARE) { - emitLineTo(cx0 - cmy + cmx, cy0 + cmx + cmy); - emitLineTo(cx0 - cmy - cmx, cy0 + cmx - cmy); - } - - emitReverse(); - - if (capStyle == CAP_ROUND) { - drawRoundCap(sx0, sy0, -smx, -smy); - } else if (capStyle == CAP_SQUARE) { - emitLineTo(sx0 + smy - smx, sy0 - smx - smy); - emitLineTo(sx0 + smy + smx, sy0 - smx + smy); + private void finish(final int outcode) { +// System.out.println("Stroker: closedPath : "+rdrCtx.closedPath); + + // Problem: impossible to guess if the path will be closed in advance + // 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 == 0) { + if (outcode == 0) { + // current point = end's cap: + if (capStyle == CAP_ROUND) { + drawRoundCap(cx0, cy0, cmx, cmy); + } else if (capStyle == CAP_SQUARE) { + emitLineTo(cx0 - cmy + cmx, cy0 + cmx + cmy); + emitLineTo(cx0 - cmy - cmx, cy0 + cmx - cmy); + } + } + emitReverse(); + + if (!capStart) { + capStart = true; + + if (sOutCode == 0) { + // starting point = initial cap: + if (capStyle == CAP_ROUND) { + drawRoundCap(sx0, sy0, -smx, -smy); + } else if (capStyle == CAP_SQUARE) { + emitLineTo(sx0 + smy - smx, sy0 - smx - smy); + emitLineTo(sx0 + smy + smx, sy0 - smx + smy); + } + } + } + } else { + emitReverse(); } - +// System.out.println("finish: "+sx0 + " " + sy0 + " from "+cx0 + " "+cy0); emitClose(); } - + private void emitMoveTo(final float x0, final float y0) { out.moveTo(x0, y0); } @@ -622,23 +766,28 @@ private void drawJoin(float pdx, float pdy, float x0, float y0, float dx, float dy, float omx, float omy, - float mx, float my) + float mx, float my, + final int outcode) { if (prev != DRAWING_OP_TO) { emitMoveTo(x0 + mx, y0 + my); - this.sdx = dx; - this.sdy = dy; - this.smx = mx; - this.smy = my; + if (!opened) { + this.sdx = dx; + this.sdy = dy; + this.smx = mx; + this.smy = my; + } } else { - boolean cw = isCW(pdx, pdy, dx, dy); - if (joinStyle == JOIN_MITER) { - drawMiter(pdx, pdy, x0, y0, dx, dy, omx, omy, mx, my, cw); - } else if (joinStyle == JOIN_ROUND) { - drawRoundJoin(x0, y0, - omx, omy, - mx, my, cw, - ROUND_JOIN_THRESHOLD); + final boolean cw = isCW(pdx, pdy, dx, dy); + if (outcode == 0) { + if (joinStyle == JOIN_MITER) { + drawMiter(pdx, pdy, x0, y0, dx, dy, omx, omy, mx, my, cw); + } else if (joinStyle == JOIN_ROUND) { + drawRoundJoin(x0, y0, + omx, omy, + mx, my, cw, + ROUND_JOIN_THRESHOLD); + } } emitLineTo(x0, y0, !cw); } @@ -943,10 +1092,30 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, return ret; } - @Override public void curveTo(float x1, float y1, - float x2, float y2, - float x3, float y3) + @Override + public void curveTo(float x1, float y1, + float x2, float y2, + float x3, float y3) { + final int outcode0 = this.cOutCode; + if (clipRect != null) { + final int outcode3 = Helpers.outcode(x3, y3, clipRect); + this.cOutCode = outcode3; + + if (outcode3 != 0) { + final int outcode1 = Helpers.outcode(x1, y1, clipRect); + final int outcode2 = Helpers.outcode(x2, y2, clipRect); + + // basic rejection criteria + if ((outcode0 & outcode1 & outcode2 & outcode3) != 0) { + // System.out.println("curveTo: Ignore: "+x3 + " "+y3); + moveTo(x3, y3, outcode0); + opened = true; + return; + } + } + } + final float[] mid = middle; mid[0] = cx0; mid[1] = cy0; @@ -955,7 +1124,7 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, mid[6] = x3; mid[7] = y3; // need these so we can update the state at the end of this method - final float xf = mid[6], yf = mid[7]; + final float xf = x3, yf = y3; float dxs = mid[2] - mid[0]; float dys = mid[3] - mid[1]; float dxf = mid[6] - mid[4]; @@ -981,6 +1150,10 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, } if (dxs == 0.0f && dys == 0.0f) { // this happens if the "curve" is just a point + // fix outcode0 for lineTo() call: + if (clipRect != null) { + this.cOutCode = outcode0; + } lineTo(mid[0], mid[1]); return; } @@ -999,7 +1172,7 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, } computeOffset(dxs, dys, lineWidth2, offset0); - drawJoin(cdx, cdy, cx0, cy0, dxs, dys, cmx, cmy, offset0[0], offset0[1]); + drawJoin(cdx, cdy, cx0, cy0, dxs, dys, cmx, cmy, offset0[0], offset0[1], outcode0); final int nSplits = findSubdivPoints(curve, mid, subdivTs, 8, lineWidth2); @@ -1034,16 +1207,37 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, emitLineToRev(r[kind - 2], r[kind - 1]); } - this.cmx = (l[kind - 2] - r[kind - 2]) / 2.0f; - this.cmy = (l[kind - 1] - r[kind - 1]) / 2.0f; - this.cdx = dxf; - this.cdy = dyf; + this.prev = DRAWING_OP_TO; this.cx0 = xf; this.cy0 = yf; - this.prev = DRAWING_OP_TO; + this.cdx = dxf; + this.cdy = dyf; + this.cmx = (l[kind - 2] - r[kind - 2]) / 2.0f; + this.cmy = (l[kind - 1] - r[kind - 1]) / 2.0f; } - @Override public void quadTo(float x1, float y1, float x2, float y2) { + @Override + public void quadTo(float x1, float y1, + float x2, float y2) + { + final int outcode0 = this.cOutCode; + if (clipRect != null) { + final int outcode2 = Helpers.outcode(x2, y2, clipRect); + this.cOutCode = outcode2; + + if (outcode2 != 0) { + final int outcode1 = Helpers.outcode(x1, y1, clipRect); + + // basic rejection criteria + if ((outcode0 & outcode1 & outcode2) != 0) { + // System.out.println("quadTo: Ignore: "+x2 + " "+y2); + moveTo(x2, y2, outcode0); + opened = true; + return; + } + } + } + final float[] mid = middle; mid[0] = cx0; mid[1] = cy0; @@ -1051,7 +1245,7 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, mid[4] = x2; mid[5] = y2; // need these so we can update the state at the end of this method - final float xf = mid[4], yf = mid[5]; + final float xf = x2, yf = y2; float dxs = mid[2] - mid[0]; float dys = mid[3] - mid[1]; float dxf = mid[4] - mid[2]; @@ -1062,6 +1256,10 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, } if (dxs == 0.0f && dys == 0.0f) { // this happens if the "curve" is just a point + // fix outcode0 for lineTo() call: + if (clipRect != null) { + this.cOutCode = outcode0; + } lineTo(mid[0], mid[1]); return; } @@ -1079,7 +1277,7 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, } computeOffset(dxs, dys, lineWidth2, offset0); - drawJoin(cdx, cdy, cx0, cy0, dxs, dys, cmx, cmy, offset0[0], offset0[1]); + drawJoin(cdx, cdy, cx0, cy0, dxs, dys, cmx, cmy, offset0[0], offset0[1], outcode0); int nSplits = findSubdivPoints(curve, mid, subdivTs, 6, lineWidth2); @@ -1114,214 +1312,16 @@ private static int findSubdivPoints(final Curve c, float[] pts, float[] ts, emitLineToRev(r[kind - 2], r[kind - 1]); } - this.cmx = (l[kind - 2] - r[kind - 2]) / 2.0f; - this.cmy = (l[kind - 1] - r[kind - 1]) / 2.0f; - this.cdx = dxf; - this.cdy = dyf; + this.prev = DRAWING_OP_TO; this.cx0 = xf; this.cy0 = yf; - this.prev = DRAWING_OP_TO; + this.cdx = dxf; + this.cdy = dyf; + this.cmx = (l[kind - 2] - r[kind - 2]) / 2.0f; + this.cmy = (l[kind - 1] - r[kind - 1]) / 2.0f; } @Override public long getNativeConsumer() { throw new InternalError("Stroker doesn't use a native consumer"); } - - // a stack of polynomial curves where each curve shares endpoints with - // adjacent ones. - static final class PolyStack { - private static final byte TYPE_LINETO = (byte) 0; - private static final byte TYPE_QUADTO = (byte) 1; - private static final byte TYPE_CUBICTO = (byte) 2; - - // curves capacity = edges count (8192) = edges x 2 (coords) - private static final int INITIAL_CURVES_COUNT = INITIAL_EDGES_COUNT << 1; - - // types capacity = edges count (4096) - private static final int INITIAL_TYPES_COUNT = INITIAL_EDGES_COUNT; - - float[] curves; - int end; - byte[] curveTypes; - int numCurves; - - // per-thread renderer context - final RendererContext rdrCtx; - - // curves ref (dirty) - final FloatArrayCache.Reference curves_ref; - // curveTypes ref (dirty) - final ByteArrayCache.Reference curveTypes_ref; - - // used marks (stats only) - int curveTypesUseMark; - int curvesUseMark; - - /** - * Constructor - * @param rdrCtx per-thread renderer context - */ - PolyStack(final RendererContext rdrCtx) { - this.rdrCtx = rdrCtx; - - curves_ref = rdrCtx.newDirtyFloatArrayRef(INITIAL_CURVES_COUNT); // 32K - curves = curves_ref.initial; - - curveTypes_ref = rdrCtx.newDirtyByteArrayRef(INITIAL_TYPES_COUNT); // 4K - curveTypes = curveTypes_ref.initial; - numCurves = 0; - end = 0; - - if (DO_STATS) { - curveTypesUseMark = 0; - curvesUseMark = 0; - } - } - - /** - * Disposes this PolyStack: - * clean up before reusing this instance - */ - void dispose() { - end = 0; - numCurves = 0; - - if (DO_STATS) { - rdrCtx.stats.stat_rdr_poly_stack_types.add(curveTypesUseMark); - rdrCtx.stats.stat_rdr_poly_stack_curves.add(curvesUseMark); - rdrCtx.stats.hist_rdr_poly_stack_curves.add(curvesUseMark); - - // reset marks - curveTypesUseMark = 0; - curvesUseMark = 0; - } - - // Return arrays: - // curves and curveTypes are kept dirty - curves = curves_ref.putArray(curves); - curveTypes = curveTypes_ref.putArray(curveTypes); - } - - private void ensureSpace(final int n) { - // use substraction to avoid integer overflow: - if (curves.length - end < n) { - if (DO_STATS) { - rdrCtx.stats.stat_array_stroker_polystack_curves - .add(end + n); - } - curves = curves_ref.widenArray(curves, end, end + n); - } - if (curveTypes.length <= numCurves) { - if (DO_STATS) { - rdrCtx.stats.stat_array_stroker_polystack_curveTypes - .add(numCurves + 1); - } - curveTypes = curveTypes_ref.widenArray(curveTypes, - numCurves, - numCurves + 1); - } - } - - void pushCubic(float x0, float y0, - float x1, float y1, - float x2, float y2) - { - ensureSpace(6); - curveTypes[numCurves++] = TYPE_CUBICTO; - // we reverse the coordinate order to make popping easier - final float[] _curves = curves; - int e = end; - _curves[e++] = x2; _curves[e++] = y2; - _curves[e++] = x1; _curves[e++] = y1; - _curves[e++] = x0; _curves[e++] = y0; - end = e; - } - - void pushQuad(float x0, float y0, - float x1, float y1) - { - ensureSpace(4); - curveTypes[numCurves++] = TYPE_QUADTO; - final float[] _curves = curves; - int e = end; - _curves[e++] = x1; _curves[e++] = y1; - _curves[e++] = x0; _curves[e++] = y0; - end = e; - } - - void pushLine(float x, float y) { - ensureSpace(2); - curveTypes[numCurves++] = TYPE_LINETO; - curves[end++] = x; curves[end++] = y; - } - - void popAll(PathConsumer2D io) { - if (DO_STATS) { - // update used marks: - if (numCurves > curveTypesUseMark) { - curveTypesUseMark = numCurves; - } - if (end > curvesUseMark) { - curvesUseMark = end; - } - } - final byte[] _curveTypes = curveTypes; - final float[] _curves = curves; - int nc = numCurves; - int e = end; - - while (nc != 0) { - switch(_curveTypes[--nc]) { - case TYPE_LINETO: - e -= 2; - io.lineTo(_curves[e], _curves[e+1]); - continue; - case TYPE_QUADTO: - e -= 4; - io.quadTo(_curves[e+0], _curves[e+1], - _curves[e+2], _curves[e+3]); - continue; - case TYPE_CUBICTO: - e -= 6; - io.curveTo(_curves[e+0], _curves[e+1], - _curves[e+2], _curves[e+3], - _curves[e+4], _curves[e+5]); - continue; - default: - } - } - numCurves = 0; - end = 0; - } - - @Override - public String toString() { - String ret = ""; - int nc = numCurves; - int last = end; - int len; - while (nc != 0) { - switch(curveTypes[--nc]) { - case TYPE_LINETO: - len = 2; - ret += "line: "; - break; - case TYPE_QUADTO: - len = 4; - ret += "quad: "; - break; - case TYPE_CUBICTO: - len = 6; - ret += "cubic: "; - break; - default: - len = 0; - } - last -= len; - ret += Arrays.toString(Arrays.copyOfRange(curves, last, last+len)) - + "\n"; - } - return ret; - } - } } diff --git a/src/main/java/sun/java2d/marlin/TransformingPathConsumer2D.java b/src/main/java/sun/java2d/marlin/TransformingPathConsumer2D.java index 65a502b..8b2c814 100644 --- a/src/main/java/sun/java2d/marlin/TransformingPathConsumer2D.java +++ b/src/main/java/sun/java2d/marlin/TransformingPathConsumer2D.java @@ -28,24 +28,58 @@ import sun.awt.geom.PathConsumer2D; import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; +import sun.java2d.marlin.Helpers.PolyStack; final class TransformingPathConsumer2D { - TransformingPathConsumer2D() { - // used by RendererContext - } + private final RendererContext rdrCtx; + + // recycled ClosedPathDetector instance from detectClosedPath() + private final ClosedPathDetector cpDetector; // recycled PathConsumer2D instance from wrapPath2d() - private final Path2DWrapper wp_Path2DWrapper = new Path2DWrapper(); + private final Path2DWrapper wp_Path2DWrapper = new Path2DWrapper(); + + // recycled PathConsumer2D instances from deltaTransformConsumer() + private final DeltaScaleFilter dt_DeltaScaleFilter = new DeltaScaleFilter(); + private final DeltaTransformFilter dt_DeltaTransformFilter = new DeltaTransformFilter(); + + // recycled PathConsumer2D instances from inverseDeltaTransformConsumer() + private final DeltaScaleFilter iv_DeltaScaleFilter = new DeltaScaleFilter(); + private final DeltaTransformFilter iv_DeltaTransformFilter = new DeltaTransformFilter(); + + // recycled PathTracer instances from tracer...() methods + private final PathTracer tracerInput = new PathTracer("[Input]"); + private final PathTracer tracerCPDetector = new PathTracer("ClosedPathDetector"); + private final PathTracer tracerStroker = new PathTracer("Stroker"); + + TransformingPathConsumer2D(final RendererContext rdrCtx) { + // used by RendererContext + this.rdrCtx = rdrCtx; + this.cpDetector = new ClosedPathDetector(rdrCtx); + } PathConsumer2D wrapPath2d(Path2D.Float p2d) { return wp_Path2DWrapper.init(p2d); } - // recycled PathConsumer2D instances from deltaTransformConsumer() - private final DeltaScaleFilter dt_DeltaScaleFilter = new DeltaScaleFilter(); - private final DeltaTransformFilter dt_DeltaTransformFilter = new DeltaTransformFilter(); + PathConsumer2D traceInput(PathConsumer2D out) { + return tracerInput.init(out); + } + + PathConsumer2D traceClosedPathDetector(PathConsumer2D out) { + return tracerCPDetector.init(out); + } + + PathConsumer2D traceStroker(PathConsumer2D out) { + return tracerStroker.init(out); + } + + PathConsumer2D detectClosedPath(PathConsumer2D out) + { + return cpDetector.init(out); + } PathConsumer2D deltaTransformConsumer(PathConsumer2D out, AffineTransform at) @@ -62,17 +96,82 @@ PathConsumer2D deltaTransformConsumer(PathConsumer2D out, if (mxx == 1.0f && myy == 1.0f) { return out; } else { + // Scale only + if (rdrCtx.doClip == 1) { + // adjust clip rectangle (ymin, ymax, xmin, xmax): + final float[] _clipRect = rdrCtx.clipRect; + _clipRect[0] += Renderer.RDR_OFFSET_Y; + _clipRect[1] += Renderer.RDR_OFFSET_Y; + _clipRect[2] += Renderer.RDR_OFFSET_X; + _clipRect[3] += Renderer.RDR_OFFSET_X; + +// System.out.println("deltaTransformConsumer: scale clip ie transformed points are scaled back to normal device"); + // Adjust the clipping rectangle (inverseDeltaTransform): + _clipRect[0] /= myy; + _clipRect[1] /= myy; + _clipRect[2] /= mxx; + _clipRect[3] /= mxx; + } return dt_DeltaScaleFilter.init(out, mxx, myy); } } else { + if (rdrCtx.doClip == 1) { + // adjust clip rectangle (ymin, ymax, xmin, xmax): + final float[] _clipRect = rdrCtx.clipRect; + _clipRect[0] += Renderer.RDR_OFFSET_Y; + _clipRect[1] += Renderer.RDR_OFFSET_Y; + _clipRect[2] += Renderer.RDR_OFFSET_X; + _clipRect[3] += Renderer.RDR_OFFSET_X; + +// System.out.println("deltaTransformConsumer: scale clip ie transformed points are scaled back to normal device"); + // Adjust the clipping rectangle (inverseDeltaTransform): + final float det = mxx * myy - mxy * myx; + final float imxx = myy / det; + final float imxy = -mxy / det; + final float imyx = -myx / det; + final float imyy = mxx / det; + + float xmin, xmax, ymin, ymax; + float x, y; + // xmin, ymin: + x = _clipRect[2] * imxx + _clipRect[0] * imxy; + y = _clipRect[2] * imyx + _clipRect[0] * imyy; + + xmin = xmax = x; + ymin = ymax = y; + + // xmax, ymin: + x = _clipRect[3] * imxx + _clipRect[0] * imxy; + y = _clipRect[3] * imyx + _clipRect[0] * imyy; + + if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } + if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } + + // xmin, ymax: + x = _clipRect[2] * imxx + _clipRect[1] * imxy; + y = _clipRect[2] * imyx + _clipRect[1] * imyy; + + if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } + if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } + + // xmax, ymax: + x = _clipRect[3] * imxx + _clipRect[1] * imxy; + y = _clipRect[3] * imyx + _clipRect[1] * imyy; + + if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } + if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } + + _clipRect[0] = ymin; + _clipRect[1] = ymax; + _clipRect[2] = xmin; + _clipRect[3] = xmax; + +// System.out.println("clip: "+java.util.Arrays.toString(_clipRect)); + } return dt_DeltaTransformFilter.init(out, mxx, mxy, myx, myy); } } - // recycled PathConsumer2D instances from inverseDeltaTransformConsumer() - private final DeltaScaleFilter iv_DeltaScaleFilter = new DeltaScaleFilter(); - private final DeltaTransformFilter iv_DeltaTransformFilter = new DeltaTransformFilter(); - PathConsumer2D inverseDeltaTransformConsumer(PathConsumer2D out, AffineTransform at) { @@ -91,7 +190,7 @@ PathConsumer2D inverseDeltaTransformConsumer(PathConsumer2D out, return iv_DeltaScaleFilter.init(out, 1.0f/mxx, 1.0f/myy); } } else { - float det = mxx * myy - mxy * myx; + final float det = mxx * myy - mxy * myx; return iv_DeltaTransformFilter.init(out, myy / det, -mxy / det, @@ -100,7 +199,6 @@ PathConsumer2D inverseDeltaTransformConsumer(PathConsumer2D out, } } - static final class DeltaScaleFilter implements PathConsumer2D { private PathConsumer2D out; private float sx, sy; @@ -275,4 +373,148 @@ public long getNativeConsumer() { throw new InternalError("Not using a native peer"); } } + + static final class ClosedPathDetector implements PathConsumer2D { + + private final RendererContext rdrCtx; + private final PolyStack stack; + + private PathConsumer2D out; + + ClosedPathDetector(final RendererContext rdrCtx) { + this.rdrCtx = rdrCtx; + this.stack = new PolyStack(rdrCtx); + } + + ClosedPathDetector init(PathConsumer2D out) { + this.out = out; + return this; // fluent API + } + + /** + * Disposes this instance: + * clean up before reusing this instance + */ + void dispose() { + stack.dispose(); + } + + @Override + public void pathDone() { + // previous path is not closed: + finish(0); + out.pathDone(); + + // TODO: fix possible leak if exception happened + // Dispose this instance: + dispose(); + } + + @Override + public void closePath() { + // path is closed + finish(1); + out.closePath(); + } + + @Override + public void moveTo(float x0, float y0) { + // previous path is not closed: + finish(0); + out.moveTo(x0, y0); + } + + private void finish(int closed) { + rdrCtx.closedPath = closed; + + if (!stack.isEmpty()) { + stack.pullAll(out); + } + } + + @Override + public void lineTo(float x1, float y1) { + stack.pushLine(x1, y1); + } + + @Override + public void curveTo(float x3, float y3, + float x2, float y2, + float x1, float y1) + { + stack.pushCubic(x1, y1, x2, y2, x3, y3); + } + + @Override + public void quadTo(float x2, float y2, float x1, float y1) { + stack.pushQuad(x1, y1, x2, y2); + } + + @Override + public long getNativeConsumer() { + throw new InternalError("Not using a native peer"); + } + } + + static final class PathTracer implements PathConsumer2D { + private final String prefix; + private PathConsumer2D out; + + PathTracer(String name) { + this.prefix = name + ": "; + } + + PathTracer init(PathConsumer2D out) { + this.out = out; + return this; // fluent API + } + + @Override + public void moveTo(float x0, float y0) { + log("moveTo (" + x0 + ", " + y0 + ')'); + out.moveTo(x0, y0); + } + + @Override + public void lineTo(float x1, float y1) { + log("lineTo (" + x1 + ", " + y1 + ')'); + out.lineTo(x1, y1); + } + + @Override + public void curveTo(float x1, float y1, + float x2, float y2, + float x3, float y3) + { + log("curveTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ") P3(" + 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 + ')'); + out.quadTo(x1, y1, x2, y2); + } + + @Override + public void closePath() { + log("closePath"); + out.closePath(); + } + + @Override + public void pathDone() { + log("pathDone"); + out.pathDone(); + } + + private void log(final String message) { + System.out.println(prefix + message); + } + + @Override + public long getNativeConsumer() { + throw new InternalError("Not using a native peer"); + } + } } diff --git a/src/main/java/sun/java2d/pipe/AlphaPaintPipe.java b/src/main/java/sun/java2d/pipe/AlphaPaintPipe.java new file mode 100644 index 0000000..1b26c7c --- /dev/null +++ b/src/main/java/sun/java2d/pipe/AlphaPaintPipe.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 1997, 2002, 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 sun.java2d.pipe; + +import java.lang.ref.WeakReference; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.PaintContext; +import java.awt.Transparency; +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.awt.image.BufferedImage; +import sun.awt.image.BufImgSurfaceData; +import sun.java2d.SunGraphics2D; +import sun.java2d.SurfaceData; +import sun.java2d.loops.Blit; +import sun.java2d.loops.MaskBlit; +import sun.java2d.loops.CompositeType; +import sun.java2d.loops.GraphicsPrimitiveMgr; + +/** + * This class implements a CompositePipe that renders path alpha tiles + * into a destination according to the Composite attribute of a + * SunGraphics2D. + */ +public class AlphaPaintPipe implements CompositePipe { + static WeakReference cachedLastRaster; + static WeakReference cachedLastColorModel; + static WeakReference cachedLastData; + + static class TileContext { + SunGraphics2D sunG2D; + PaintContext paintCtxt; + ColorModel paintModel; + WeakReference lastRaster; + WeakReference lastData; + MaskBlit lastMask; + Blit lastBlit; + SurfaceData dstData; + + public TileContext(SunGraphics2D sg, PaintContext pc) { + sunG2D = sg; + paintCtxt = pc; + paintModel = pc.getColorModel(); + dstData = sg.getSurfaceData(); + synchronized (AlphaPaintPipe.class) { + if (cachedLastColorModel != null && + cachedLastColorModel.get() == paintModel) + { + this.lastRaster = cachedLastRaster; + this.lastData = cachedLastData; + } + } + } + } + + public Object startSequence(SunGraphics2D sg, Shape s, Rectangle devR, + int[] abox) { + PaintContext paintContext = + sg.paint.createContext(sg.getDeviceColorModel(), + devR, + s.getBounds2D(), + sg.cloneTransform(), + sg.getRenderingHints()); + return new TileContext(sg, paintContext); + } + + public boolean needTile(Object context, int x, int y, int w, int h) { + return true; + } + + private static final int TILE_SIZE = 32; + + public void renderPathTile(Object ctx, + byte[] atile, int offset, int tilesize, + int x, int y, int w, int h) { + TileContext context = (TileContext) ctx; + PaintContext paintCtxt = context.paintCtxt; + SunGraphics2D sg = context.sunG2D; + SurfaceData dstData = context.dstData; + SurfaceData srcData = null; + Raster lastRas = null; + if (context.lastData != null && context.lastRaster != null) { + srcData = context.lastData.get(); + lastRas = context.lastRaster.get(); + if (srcData == null || lastRas == null) { + srcData = null; + lastRas = null; + } + } + ColorModel paintModel = context.paintModel; + + for (int rely = 0; rely < h; rely += TILE_SIZE) { + int ty = y + rely; + int th = Math.min(h-rely, TILE_SIZE); + for (int relx = 0; relx < w; relx += TILE_SIZE) { + int tx = x + relx; + int tw = Math.min(w-relx, TILE_SIZE); + + Raster srcRaster = paintCtxt.getRaster(tx, ty, tw, th); + if ((srcRaster.getMinX() != 0) || (srcRaster.getMinY() != 0)) { + srcRaster = srcRaster.createTranslatedChild(0, 0); + } + if (lastRas != srcRaster) { + lastRas = srcRaster; + context.lastRaster = new WeakReference<>(lastRas); + // REMIND: This will fail for a non-Writable raster! + BufferedImage bImg = + new BufferedImage(paintModel, + (WritableRaster) srcRaster, + paintModel.isAlphaPremultiplied(), + null); + srcData = BufImgSurfaceData.createData(bImg); + context.lastData = new WeakReference<>(srcData); + context.lastMask = null; + context.lastBlit = null; + } + + if (atile == null) { + if (context.lastBlit == null) { + CompositeType comptype = sg.imageComp; + if (CompositeType.SrcOverNoEa.equals(comptype) && + paintModel.getTransparency() == Transparency.OPAQUE) + { + comptype = CompositeType.SrcNoEa; + } + context.lastBlit = + Blit.getFromCache(srcData.getSurfaceType(), + comptype, + dstData.getSurfaceType()); + } + context.lastBlit.Blit(srcData, dstData, + sg.composite, null, + 0, 0, tx, ty, tw, th); + } else { + if (context.lastMask == null) { + CompositeType comptype = sg.imageComp; + if (CompositeType.SrcOverNoEa.equals(comptype) && + paintModel.getTransparency() == Transparency.OPAQUE) + { + comptype = CompositeType.SrcNoEa; + } + context.lastMask = + MaskBlit.getFromCache(srcData.getSurfaceType(), + comptype, + dstData.getSurfaceType()); + } + + int toff = offset + rely * tilesize + relx; + context.lastMask.MaskBlit(srcData, dstData, + sg.composite, null, + 0, 0, tx, ty, tw, th, + atile, toff, tilesize); + } + } + } + } + + public void skipTile(Object context, int x, int y) { + return; + } + + public void endSequence(Object ctx) { + TileContext context = (TileContext) ctx; + if (context.paintCtxt != null) { + context.paintCtxt.dispose(); + } + synchronized (AlphaPaintPipe.class) { + if (context.lastData != null) { + cachedLastRaster = context.lastRaster; + if (cachedLastColorModel == null || + cachedLastColorModel.get() != context.paintModel) + { + // Avoid creating new WeakReference if possible + cachedLastColorModel = + new WeakReference<>(context.paintModel); + } + cachedLastData = context.lastData; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/test/ShapeOutlineTest.java b/src/main/java/test/ShapeOutlineTest.java new file mode 100644 index 0000000..cbe4893 --- /dev/null +++ b/src/main/java/test/ShapeOutlineTest.java @@ -0,0 +1,174 @@ +/* + * 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 java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; + +/** + * Simple Line rendering test using GeneralPath to enable Pisces / marlin / + * ductus renderers + */ +public class ShapeOutlineTest { + + private final static int N = 10; + + private final static boolean DO_CIRCLE = false; + + private final static boolean DO_DRAW = true; + + private final static float CIRCLE_RADIUS = 1843200.0f * 10.0f; + + private final static float RECT_SIZE = 900f * 1024 * 5 * 30; + + private final static double sqrt2 = Math.sqrt(2); + + public static void main(String[] args) { + + final float lineStroke = 2f; + final int size = 1000; + + // First display which renderer is tested: + // JDK9 only: + System.setProperty("sun.java2d.renderer.verbose", "true"); + System.out.println("Testing renderer: "); + // Other JDK: + String renderer = "undefined"; + try { + renderer = sun.java2d.pipe.RenderingEngine.getInstance().getClass().getName(); + System.out.println(renderer); + } + catch (Throwable th) { + // may fail with JDK9 jigsaw (jake) + if (false) { + System.err.println("Unable to get RenderingEngine.getInstance()"); + th.printStackTrace(); + } + } + + System.out.println("ShapeOutlineTest: size = " + size); + + final BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + + final Graphics2D g2d = (Graphics2D) image.getGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + + g2d.setClip(0, 0, size, size); + g2d.setStroke( + new BasicStroke(lineStroke, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 20f, + new float[]{10f, 5f}, 0f + ) + ); + + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, size, size); + + // RECT: + // old: 4009.9072039999996 ms. + // fix: 4094.81685 + // with inlined variables: old = 3770 ms + // Ductus 1.8: 35934.7 ms + // DMarlinRenderingEngine: 4131.276773 ms. + + // CIRCLE: + // old: 2696.341058 ms. + // fix: 2442.098762 ms. + + // CPU fixed without clipping: 4357.567511 ms. + // Stroker clipping: 700 ms. + + for (int i = 0; i < N; i++) { + final long start = System.nanoTime(); + + paint(g2d, size); + + final long time = System.nanoTime() - start; + + System.out.println("paint: duration= " + (1e-6 * time) + " ms."); + } + + try { + final File file = new File("ShapeOutlineTest-" + + (DO_CIRCLE ? "circle" : "rect") + ".png"); + + System.out.println("Writing file: " + file.getAbsolutePath()); + ImageIO.write(image, "PNG", file); + } + catch (IOException ex) { + ex.printStackTrace(); + } + finally { + g2d.dispose(); + } + } + + private static void paint(final Graphics2D g2d, final float size) { + final Shape path = createPath(size); + + g2d.setColor(Color.BLUE); + g2d.fill(path); + + if (DO_DRAW) { + g2d.setColor(Color.RED); + g2d.draw(path); + } + } + + private static Shape createPath(final float size) { + if (DO_CIRCLE) { + final float c = (float)(0.5f * size - CIRCLE_RADIUS / sqrt2); + + return new Ellipse2D.Float( + c - CIRCLE_RADIUS, + c - CIRCLE_RADIUS, + 2.0f * CIRCLE_RADIUS, + 2.0f * CIRCLE_RADIUS + ); + + } else { + final double half = 0.5f * size; + + System.out.println("RECT_SIZE: " + RECT_SIZE); + + final Path2D p = new Path2D.Float(); + p.moveTo(half, half); + p.lineTo(-RECT_SIZE, -RECT_SIZE); + p.lineTo(0.0, -RECT_SIZE * 2.0); + p.lineTo(RECT_SIZE, -RECT_SIZE); + p.lineTo(half, half); + p.closePath(); + return p; + } + } +} diff --git a/src/main/java/test/StrokeClipTest.java b/src/main/java/test/StrokeClipTest.java new file mode 100644 index 0000000..ac63b8c --- /dev/null +++ b/src/main/java/test/StrokeClipTest.java @@ -0,0 +1,168 @@ +/* + * 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 java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; + +/** + * Simple Line rendering test using GeneralPath to enable Pisces / marlin / + * ductus renderers + */ +public class StrokeClipTest { + + private final static int N = 1; + + private final static boolean DO_CIRCLE = false; + + private final static boolean DO_DRAW = true; + + private final static boolean DO_CLOSE_PATH = false; + + private final static float CIRCLE_RADIUS = 100f; + + private final static float RECT_SIZE = 100f; + + private final static double sqrt2 = Math.sqrt(2); + + public static void main(String[] args) { + + final float lineStroke = 4f; + final int size = 400; + + // First display which renderer is tested: + // JDK9 only: + System.setProperty("sun.java2d.renderer.verbose", "true"); + System.out.println("Testing renderer: "); + // Other JDK: + String renderer = "undefined"; + try { + renderer = sun.java2d.pipe.RenderingEngine.getInstance().getClass().getName(); + System.out.println(renderer); + } + catch (Throwable th) { + // may fail with JDK9 jigsaw (jake) + if (false) { + System.err.println("Unable to get RenderingEngine.getInstance()"); + th.printStackTrace(); + } + } + + System.out.println("StrokeClipTest: size = " + size); + + final BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + + final Graphics2D g2d = (Graphics2D) image.getGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + + g2d.setClip(0, 0, size, size); + g2d.setStroke( + new BasicStroke(lineStroke, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 20f) + ); + + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, size, size); + + for (int i = 0; i < N; i++) { + final long start = System.nanoTime(); + + paint(g2d, size); + + final long time = System.nanoTime() - start; + + System.out.println("paint: duration= " + (1e-6 * time) + " ms."); + } + + try { + final File file = new File("StrokeClipTest-" + + (DO_CIRCLE ? "circle" : "rect") + ".png"); + + System.out.println("Writing file: " + file.getAbsolutePath()); + ImageIO.write(image, "PNG", file); + } + catch (IOException ex) { + ex.printStackTrace(); + } + finally { + g2d.dispose(); + } + } + + private static void paint(final Graphics2D g2d, final float size) { + final AffineTransform tx = g2d.getTransform(); + final Shape path = createPath(size); + + for (int i = 0; i < 600; i += 1500) { + g2d.setTransform(AffineTransform.getTranslateInstance(i, 0)); + + g2d.setColor(Color.BLUE); + g2d.fill(path); + + if (DO_DRAW) { + g2d.setColor(Color.RED); + g2d.draw(path); + } + } + g2d.setTransform(tx); + } + + private static Shape createPath(final float size) { + if (DO_CIRCLE) { + final float c = (float) (0.5f * size - CIRCLE_RADIUS / sqrt2); + + return new Ellipse2D.Float( + -CIRCLE_RADIUS, + 100, + 2.0f * CIRCLE_RADIUS, + 2.0f * CIRCLE_RADIUS + ); + + } else { + final Path2D p = new Path2D.Float(); + p.moveTo(100, 100); + p.lineTo(100.0, 50); + p.lineTo(-100, 10); + p.lineTo(-100, 100); + p.lineTo(300.0, 200); + p.lineTo(100, 300); + p.lineTo(120, 80); + + if (DO_CLOSE_PATH) { + p.closePath(); + } + return p; + } + } +}