/* * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javafx.sg.prism; import com.sun.javafx.logging.PulseLogger; import javafx.scene.CacheHint; import java.util.List; import com.sun.javafx.geom.BaseBounds; import com.sun.javafx.geom.DirtyRegionContainer; import com.sun.javafx.geom.RectBounds; import com.sun.javafx.geom.Rectangle; import com.sun.javafx.geom.transform.Affine2D; import com.sun.javafx.geom.transform.Affine3D; import com.sun.javafx.geom.transform.BaseTransform; import com.sun.javafx.geom.transform.GeneralTransform3D; import com.sun.prism.Graphics; import com.sun.prism.RTTexture; import com.sun.prism.Texture; import com.sun.scenario.effect.Effect; import com.sun.scenario.effect.FilterContext; import com.sun.scenario.effect.Filterable; import com.sun.scenario.effect.ImageData; import com.sun.scenario.effect.impl.prism.PrDrawable; import com.sun.scenario.effect.impl.prism.PrFilterContext; import javafx.geometry.Insets; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.paint.Color; /** * Base implementation of the Node.cache and cacheHint APIs. * * When all or a portion of the cacheHint becomes enabled, we should try *not* * to re-render the cache. This avoids a big hiccup at the beginning of the * "use SPEED only while animating" use case: * 0) Under DEFAULT, we should already have a cached image * 1) scale/rotate caching is enabled (no expensive re-render required) * 2) animation happens, using the cached image * 3) animation completes, caching is disable and the node is re-rendered (at * full-fidelity) with the final transform. * * Certain transform combinations are not supported, notably scaling by unequal * amounts in the x and y directions while also rotating. Other than simple * translation, animations in this case will require re-rendering every frame. * * Ideally, a simple change to a Node's translation should never regenerate the * cached image. * * The CacheFilter is also capable of optimizing the scrolling of the cached contents. * For example, the ScrollView UI Control can define its content area as being cached, * such that when the user scrolls, we can shift the old content area and adjust the * dirty region so that it only includes the "newly exposed" area. */ public class CacheFilter { /** * Defines the state when we're in the midst of scrolling a cached image */ private static enum ScrollCacheState { CHECKING_PRECONDITIONS, ENABLED, DISABLED } // Garbage-reduction variables: private static final Rectangle TEMP_RECT = new Rectangle(); private static final DirtyRegionContainer TEMP_CONTAINER = new DirtyRegionContainer(1); private static final Affine3D TEMP_CACHEFILTER_TRANSFORM = new Affine3D(); private static final RectBounds TEMP_BOUNDS = new RectBounds(); // Fun with floating point private static final double EPSILON = 0.0000001; private RTTexture tempTexture; private double lastXDelta; private double lastYDelta; private ScrollCacheState scrollCacheState = ScrollCacheState.CHECKING_PRECONDITIONS; // Note: this ImageData is always created and assumed to be untransformed. private ImageData cachedImageData; private Rectangle cacheBounds = new Rectangle(); // Used to draw into the cache private final Affine2D cachedXform = new Affine2D(); // The scale and rotate used to draw into the cache private double cachedScaleX; private double cachedScaleY; private double cachedRotate; private double cachedX; private double cachedY; private NGNode node; // Used to draw the cached image to the screen private final Affine2D screenXform = new Affine2D(); // Cache hint settings private boolean scaleHint; private boolean rotateHint; // We keep this around for the sake of matchesHint private CacheHint cacheHint; // Was the last paint unsupported by the cache? If so, will need to // regenerate the cache next time. private boolean wasUnsupported = false; /** * Compute the dirty region that must be re-rendered after scrolling */ private Rectangle computeDirtyRegionForTranslate() { if (lastXDelta != 0) { if (lastXDelta > 0) { TEMP_RECT.setBounds(0, 0, (int)lastXDelta, cacheBounds.height); } else { TEMP_RECT.setBounds(cacheBounds.width + (int)lastXDelta, 0, -(int)lastXDelta, cacheBounds.height); } } else { if (lastYDelta > 0) { TEMP_RECT.setBounds(0, 0, cacheBounds.width, (int)lastYDelta); } else { TEMP_RECT.setBounds(0, cacheBounds.height + (int)lastYDelta, cacheBounds.width, -(int)lastYDelta); } } return TEMP_RECT; } protected CacheFilter(NGNode node, CacheHint cacheHint) { this.node = node; this.scrollCacheState = ScrollCacheState.CHECKING_PRECONDITIONS; setHint(cacheHint); } public void setHint(CacheHint cacheHint) { this.cacheHint = cacheHint; this.scaleHint = (cacheHint == CacheHint.SPEED || cacheHint == CacheHint.SCALE || cacheHint == CacheHint.SCALE_AND_ROTATE); this.rotateHint = (cacheHint == CacheHint.SPEED || cacheHint == CacheHint.ROTATE || cacheHint == CacheHint.SCALE_AND_ROTATE); } // These two methods exist only for the sake of testing. final boolean isScaleHint() { return scaleHint; } final boolean isRotateHint() { return rotateHint; } /** * Indicates whether this CacheFilter's hint matches the CacheHint * passed in. */ boolean matchesHint(CacheHint cacheHint) { return this.cacheHint == cacheHint; } /** * Are we attempting to use cache for an unsupported transform mode? Mostly * this is for trying to rotate while scaling the object by different * amounts in the x and y directions (this also includes shearing). */ boolean unsupported(double[] xformInfo) { double scaleX = xformInfo[0]; double scaleY = xformInfo[1]; double rotate = xformInfo[2]; // If we're trying to rotate... if (rotate > EPSILON || rotate < -EPSILON) { // ...and if scaleX != scaleY. This can be in the render xform, or // may have made it into the cached image. if (scaleX > scaleY + EPSILON || scaleY > scaleX + EPSILON || scaleX < scaleY - EPSILON || scaleY < scaleX - EPSILON || cachedScaleX > cachedScaleY + EPSILON || cachedScaleY > cachedScaleX + EPSILON || cachedScaleX < cachedScaleY - EPSILON || cachedScaleY < cachedScaleX - EPSILON ) { return true; } } return false; } private boolean isXformScrollCacheCapable(double[] xformInfo) { if (unsupported(xformInfo)) { return false; } double rotate = xformInfo[2]; return rotateHint || rotate == 0; } /* * Do we need to regenerate the cached image? * Assumes that caller locked and validated the cachedImageData.untximage * if not null... */ private boolean needToRenderCache(BaseTransform renderXform, double[] xformInfo, float pixelScaleX, float pixelScaleY) { if (cachedImageData == null) { return true; } if (lastXDelta != 0 || lastYDelta != 0) { if (Math.abs(lastXDelta) >= cacheBounds.width || Math.abs(lastYDelta) >= cacheBounds.height || Math.rint(lastXDelta) != lastXDelta || Math.rint(lastYDelta) != lastYDelta) { node.clearDirtyTree(); // Need to clear dirty (by translation) flags in the children lastXDelta = lastYDelta = 0; return true; } if (scrollCacheState == ScrollCacheState.CHECKING_PRECONDITIONS) { if (impl_scrollCacheCapable() && isXformScrollCacheCapable(xformInfo)) { scrollCacheState = ScrollCacheState.ENABLED; } else { scrollCacheState = ScrollCacheState.DISABLED; return true; } } } // TODO: is == sufficient for floating point comparison here? (RT-23963) if (cachedXform.getMxx() == renderXform.getMxx() && cachedXform.getMyy() == renderXform.getMyy() && cachedXform.getMxy() == renderXform.getMxy() && cachedXform.getMyx() == renderXform.getMyx()) { // It's just a translation - use cached Image return false; } // Not just a translation - if was or is unsupported, then must rerender if (wasUnsupported || unsupported(xformInfo)) { return true; } double scaleX = xformInfo[0]; double scaleY = xformInfo[1]; double rotate = xformInfo[2]; if (scaleHint) { if (cachedScaleX < pixelScaleX || cachedScaleY < pixelScaleY) { // We have moved onto a screen with a higher pixelScale and // our cache was less than that pixel scale. Even though // we have the scaleHint, we always cache at a minimum of // the pixel scale of the screen so we need to re-cache. return true; } if (rotateHint) { return false; } else { // Not caching for rotate: regenerate cache if rotate changed if (cachedRotate - EPSILON < rotate && rotate < cachedRotate + EPSILON) { return false; } else { return true; } } } else { if (rotateHint) { // Not caching for scale: regenerate cache if scale changed if (cachedScaleX - EPSILON < scaleX && scaleX < cachedScaleX + EPSILON && cachedScaleY - EPSILON < scaleY && scaleY < cachedScaleY + EPSILON) { return false; } else {// Scale is not "equal enough" - regenerate return true; } } else { // Not caching for anything; always regenerate return true; } } } /* * Given the new xform info, update the screenXform as needed to correctly * paint the cache to the screen. */ void updateScreenXform(double[] xformInfo) { // screenXform will be the difference between the cachedXform and the // render xform. if (scaleHint) { if (rotateHint) { double screenScaleX = xformInfo[0] / cachedScaleX; double screenScaleY = xformInfo[1] / cachedScaleY; double screenRotate = xformInfo[2] - cachedRotate; screenXform.setToScale(screenScaleX, screenScaleY); screenXform.rotate(screenRotate); } else { double screenScaleX = xformInfo[0] / cachedScaleX; double screenScaleY = xformInfo[1] / cachedScaleY; screenXform.setToScale(screenScaleX, screenScaleY); } } else { if (rotateHint) { double screenRotate = xformInfo[2] - cachedRotate; screenXform.setToRotation(screenRotate, 0.0, 0.0); } else { // No caching, cache already rendered with xform; just paint it screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM); } } } public void invalidate() { if (scrollCacheState == ScrollCacheState.ENABLED) { scrollCacheState = ScrollCacheState.CHECKING_PRECONDITIONS; } imageDataUnref(); lastXDelta = lastYDelta = 0; } void imageDataUnref() { if (tempTexture != null) { tempTexture.dispose(); tempTexture = null; } if (cachedImageData != null) { // While we hold on to this ImageData we leave the texture // unlocked so it can be reclaimed, but the default unref() // method assumes it was locked. Filterable implImage = cachedImageData.getUntransformedImage(); if (implImage != null) { implImage.lock(); } cachedImageData.unref(); cachedImageData = null; } } void invalidateByTranslation(double translateXDelta, double translateYDelta) { if (cachedImageData == null) { return; } if (scrollCacheState == ScrollCacheState.DISABLED) { imageDataUnref(); } else { // When both mxt and myt change, we don't currently use scroll optimization if (translateXDelta != 0 && translateYDelta != 0) { imageDataUnref(); } else { lastYDelta = translateYDelta; lastXDelta = translateXDelta; } } } public void dispose() { invalidate(); node = null; } /* * unmatrix() and the supporting functions are based on the code from * "Decomposing A Matrix Into Simple Transformations" by Spencer W. Thomas * from Graphics Gems II, as found at * http://tog.acm.org/resources/GraphicsGems/ * which states, "All code here can be used without restrictions." * * The code was reduced from handling a 4x4 matrix (3D w/ perspective) * to handle just a 2x2 (2D scale/rotate, w/o translate, as that is handled * separately). */ /** * Given a BaseTransform, decompose it into values for scaleX, scaleY and * rotate. * * The return value is a double[3], the values being: * [0]: scaleX * [1]: scaleY * [2]: rotation angle, in radians, between *** and *** * * From unmatrix() in unmatrix.c */ double[] unmatrix(BaseTransform xform) { double[] retVal = new double[3]; double[][] row = {{xform.getMxx(), xform.getMxy()}, {xform.getMyx(), xform.getMyy()}}; final double xSignum = Math.signum(row[0][0]); final double ySignum = Math.signum(row[1][1]); // Compute X scale factor and normalize first row. // tran[U_SCALEX] = V3Length(&row[0]); // row[0] = *V3Scale(&row[0], 1.0); double scaleX = xSignum * v2length(row[0]); v2scale(row[0], xSignum); // Compute XY shear factor and make 2nd row orthogonal to 1st. // tran[U_SHEARXY] = V3Dot(&row[0], &row[1]); // (void)V3Combine(&row[1], &row[0], &row[1], 1.0, -tran[U_SHEARXY]); // // "this is too large by the y scaling factor" double shearXY = v2dot(row[0], row[1]); // Combine into row[1] v2combine(row[1], row[0], row[1], 1.0, -shearXY); // Now, compute Y scale and normalize 2nd row // tran[U_SCALEY] = V3Length(&row[1]); // V3Scale(&row[1], 1.0); // tran[U_SHEARXY] /= tran[U_SCALEY]; double scaleY = ySignum * v2length(row[1]); v2scale(row[1], ySignum); // Now extract the rotation. (This is new code, not from the Gem.) // // In our matrix, we now have // [ cos(theta) -sin(theta) ] // [ sin(theta) cos(theta) ] // // TODO: assert: all 4 values are sane (RT-23962) // double sin = row[1][0]; double cos = row[0][0]; double angleRad = 0.0; // Recall: // arcsin works for theta: -90 -> 90 // arccos works for theta: 0 -> 180 if (sin >= 0) { // theta is 0 -> 180, use acos() angleRad = Math.acos(cos); } else { if (cos > 0) { // sin < 0, cos > 0, so theta is 270 -> 360, aka -90 -> 0 // use asin(), add 360 angleRad = 2.0 * Math.PI + Math.asin(sin); } else { // sin < 0, cos < 0, so theta 180 -> 270 // cos from 180 -> 270 is inverse of cos from 0->90, // so take acos(-cos) and add 180 angleRad = Math.PI + Math.acos(-cos); } } retVal[0] = scaleX; retVal[1] = scaleY; retVal[2] = angleRad; return retVal; } /** * make a linear combination of two vectors and return the result * result = (v0 * scalarA) + (v1 * scalarB) * * From V3Combine() in GGVecLib.c */ void v2combine(double v0[], double v1[], double result[], double scalarA, double scalarB) { // make a linear combination of two vectors and return the result. // result = (a * ascl) + (b * bscl) /* Vector3 *V3Combine (a, b, result, ascl, bscl) Vector3 *a, *b, *result; double ascl, bscl; { result->x = (ascl * a->x) + (bscl * b->x); result->y = (ascl * a->y) + (bscl * b->y); result->z = (ascl * a->z) + (bscl * b->z); return(result); */ result[0] = scalarA*v0[0] + scalarB*v1[0]; result[1] = scalarA*v0[1] + scalarB*v1[1]; } /** * dot product of 2 vectors of length 2 */ double v2dot(double v0[], double v1[]) { return v0[0]*v1[0] + v0[1]*v1[1]; } /** * scale v[] to be relative to newLen * * From V3Scale() in GGVecLib.c */ void v2scale(double v[], double newLen) { double len = v2length(v); if (len != 0) { v[0] *= newLen / len; v[1] *= newLen / len; } } /** * returns length of input vector * * Based on V3Length() in GGVecLib.c */ double v2length(double v[]) { return Math.sqrt(v[0]*v[0] + v[1]*v[1]); } void render(Graphics g) { // The following is safe; xform will not be mutated below BaseTransform xform = g.getTransformNoClone(); FilterContext fctx = PrFilterContext.getInstance(g.getAssociatedScreen()); // getFilterContext double[] xformInfo = unmatrix(xform); boolean isUnsupported = unsupported(xformInfo); lastXDelta = lastXDelta * xformInfo[0]; lastYDelta = lastYDelta * xformInfo[1]; if (cachedImageData != null) { Filterable implImage = cachedImageData.getUntransformedImage(); if (implImage != null) { implImage.lock(); if (!cachedImageData.validate(fctx)) { implImage.unlock(); invalidate(); } } } float pixelScaleX = g.getPixelScaleFactorX(); float pixelScaleY = g.getPixelScaleFactorY(); if (needToRenderCache(xform, xformInfo, pixelScaleX, pixelScaleY)) { if (PulseLogger.PULSE_LOGGING_ENABLED) { PulseLogger.incrementCounter("CacheFilter rebuilding"); } if (cachedImageData != null) { Filterable implImage = cachedImageData.getUntransformedImage(); if (implImage != null) { implImage.unlock(); } invalidate(); } if (scaleHint) { // do not cache the image at a small scale factor when // scaleHint is set as it leads to poor rendering results // when image is scaled up. cachedScaleX = Math.max(pixelScaleX, xformInfo[0]); cachedScaleY = Math.max(pixelScaleY, xformInfo[1]); cachedRotate = 0; cachedXform.setTransform(cachedScaleX, 0.0, 0.0, cachedScaleX, 0.0, 0.0); updateScreenXform(xformInfo); } else { cachedScaleX = xformInfo[0]; cachedScaleY = xformInfo[1]; cachedRotate = xformInfo[2]; // Update the cachedXform to the current xform (ignoring translate). cachedXform.setTransform(xform.getMxx(), xform.getMyx(), xform.getMxy(), xform.getMyy(), 0.0, 0.0); // screenXform is always identity in this case, as we've just // rendered into the cache using the render xform. screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM); } cacheBounds = impl_getCacheBounds(cacheBounds, cachedXform); cachedImageData = impl_createImageData(fctx, cacheBounds); impl_renderNodeToCache(cachedImageData, cacheBounds, cachedXform, null); // cachedBounds includes effects, and is in *scene* coords Rectangle cachedBounds = cachedImageData.getUntransformedBounds(); // Save out the (un-transformed) x & y coordinates. This accounts // for effects and other reasons the untranslated location may not // be 0,0. cachedX = cachedBounds.x; cachedY = cachedBounds.y; } else { if (scrollCacheState == ScrollCacheState.ENABLED && (lastXDelta != 0 || lastYDelta != 0) ) { impl_moveCacheBy(cachedImageData, lastXDelta, lastYDelta); impl_renderNodeToCache(cachedImageData, cacheBounds, cachedXform, computeDirtyRegionForTranslate()); lastXDelta = lastYDelta = 0; } // Using the cached image; calculate screenXform to paint to screen. if (isUnsupported) { // Only way we should be using the cached image in the // unsupported case is for a change in translate only. No other // xform should be needed, so use identity. // TODO: assert cachedXform == render xform (ignoring translate) // or assert xforminfo == cachedXform info (RT-23962) screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM); } else { updateScreenXform(xformInfo); } } // If this render is unsupported, remember for next time. We'll need // to regenerate the cache once we're in a supported scenario again. wasUnsupported = isUnsupported; Filterable implImage = cachedImageData.getUntransformedImage(); if (implImage == null) { if (PulseLogger.PULSE_LOGGING_ENABLED) { PulseLogger.incrementCounter("CacheFilter not used"); } impl_renderNodeToScreen(g); } else { double mxt = xform.getMxt(); double myt = xform.getMyt(); impl_renderCacheToScreen(g, implImage, mxt, myt); implImage.unlock(); } } /** * Create the ImageData for the cached bitmap, with the specified bounds. */ ImageData impl_createImageData(FilterContext fctx, Rectangle bounds) { Filterable ret; try { ret = Effect.getCompatibleImage(fctx, bounds.width, bounds.height); Texture cachedTex = ((PrDrawable) ret).getTextureObject(); cachedTex.contentsUseful(); } catch (Throwable e) { ret = null; } return new ImageData(fctx, ret, bounds); } /** * Render node to cache. * @param cacheData the cache * @param cacheBounds cache bounds * @param xform transformation * @param dirtyBounds null or dirty rectangle to be rendered */ void impl_renderNodeToCache(ImageData cacheData, Rectangle cacheBounds, BaseTransform xform, Rectangle dirtyBounds) { final PrDrawable image = (PrDrawable) cacheData.getUntransformedImage(); if (image != null) { Graphics g = image.createGraphics(); TEMP_CACHEFILTER_TRANSFORM.setToIdentity(); TEMP_CACHEFILTER_TRANSFORM.translate(-cacheBounds.x, -cacheBounds.y); if (xform != null) { TEMP_CACHEFILTER_TRANSFORM.concatenate(xform); } if (dirtyBounds != null) { TEMP_CONTAINER.deriveWithNewRegion((RectBounds)TEMP_BOUNDS.deriveWithNewBounds(dirtyBounds)); // Culling might save us a lot when there's a dirty region node.doPreCulling(TEMP_CONTAINER, TEMP_CACHEFILTER_TRANSFORM, new GeneralTransform3D()); g.setHasPreCullingBits(true); g.setClipRectIndex(0); g.setClipRect(dirtyBounds); } g.transform(TEMP_CACHEFILTER_TRANSFORM); if (node.getClipNode() != null) { node.renderClip(g); } else if (node.getEffectFilter() != null) { node.renderEffect(g); } else { node.renderContent(g); } } } /** * Render the node directly to the screen, in the case that the cached * image is unexpectedly null. See RT-6428. */ void impl_renderNodeToScreen(Object implGraphics) { Graphics g = (Graphics)implGraphics; if (node.getEffectFilter() != null) { node.renderEffect(g); } else { node.renderContent(g); } } /** * Render the cached image to the screen, translated by mxt, myt. */ void impl_renderCacheToScreen(Object implGraphics, Filterable implImage, double mxt, double myt) { Graphics g = (Graphics)implGraphics; g.setTransform(screenXform.getMxx(), screenXform.getMyx(), screenXform.getMxy(), screenXform.getMyy(), mxt, myt); g.translate((float)cachedX, (float)cachedY); Texture cachedTex = ((PrDrawable)implImage).getTextureObject(); Rectangle cachedBounds = cachedImageData.getUntransformedBounds(); g.drawTexture(cachedTex, 0, 0, cachedBounds.width, cachedBounds.height); // FYI: transform state is restored by the NGNode.render() method } /** * True if we can use scrolling optimization on this node. */ boolean impl_scrollCacheCapable() { if (!(node instanceof NGGroup)) { return false; } List children = ((NGGroup)node).getChildren(); if (children.size() != 1) { return false; } NGNode child = children.get(0); if (!child.getTransform().is2D()) { return false; } NGNode clip = node.getClipNode(); if (clip == null || !clip.isRectClip(BaseTransform.IDENTITY_TRANSFORM, false)) { return false; } if (node instanceof NGRegion) { NGRegion region = (NGRegion) node; if (!region.getBorder().isEmpty()) { return false; } final Background background = region.getBackground(); if (!background.isEmpty()) { if (!background.getImages().isEmpty() || background.getFills().size() != 1) { return false; } BackgroundFill fill = background.getFills().get(0); javafx.scene.paint.Paint fillPaint = fill.getFill(); BaseBounds clipBounds = clip.getCompleteBounds(TEMP_BOUNDS, BaseTransform.IDENTITY_TRANSFORM); return fillPaint.isOpaque() && fillPaint instanceof Color && fill.getInsets().equals(Insets.EMPTY) && clipBounds.getMinX() == 0 && clipBounds.getMinY() == 0 && clipBounds.getMaxX() == region.getWidth() && clipBounds.getMaxY() == region.getHeight(); } } return true; } /** * Moves a subregion of the cache, "scrolling" the cache by x/y Delta. * On of xDelta/yDelta must be zero. The rest of the pixels will be cleared. * @param cachedImageData cache * @param xDelta x-axis delta * @param yDelta y-axis delta */ void impl_moveCacheBy(ImageData cachedImageData, double xDelta, double yDelta) { PrDrawable drawable = (PrDrawable) cachedImageData.getUntransformedImage(); final Rectangle r = cachedImageData.getUntransformedBounds(); int x = (int)Math.max(0, (-xDelta)); int y = (int)Math.max(0, (-yDelta)); int destX = (int)Math.max(0, (xDelta)); int destY = (int) Math.max(0, yDelta); int w = r.width - (int) Math.abs(xDelta); int h = r.height - (int) Math.abs(yDelta); final Graphics g = drawable.createGraphics(); if (tempTexture != null) { tempTexture.lock(); if (tempTexture.isSurfaceLost()) { tempTexture = null; } } if (tempTexture == null) { tempTexture = g.getResourceFactory(). createRTTexture(drawable.getPhysicalWidth(), drawable.getPhysicalHeight(), Texture.WrapMode.CLAMP_NOT_NEEDED); } final Graphics tempG = tempTexture.createGraphics(); tempG.clear(); tempG.drawTexture(drawable.getTextureObject(), 0, 0, w, h, x, y, x + w, y + h); tempG.sync(); g.clear(); g.drawTexture(tempTexture, destX, destY, destX + w, destY + h, 0, 0, w, h); tempTexture.unlock(); } /** * Get the cache bounds. * @param bounds rectangle to store bounds to * @param xform transformation */ Rectangle impl_getCacheBounds(Rectangle bounds, BaseTransform xform) { final BaseBounds b = node.getClippedBounds(TEMP_BOUNDS, xform); bounds.setBounds(b); return bounds; } BaseBounds computeDirtyBounds(BaseBounds region, BaseTransform tx, GeneralTransform3D pvTx) { // For now, we just use the computed dirty bounds of the Node and // round them out before the transforms. // Later, we could use the bounds of the cache // to compute the dirty region directly (and more accurately). // See RT-34928 for more details. if (!node.dirtyBounds.isEmpty()) { region = region.deriveWithNewBounds(node.dirtyBounds); } else { region = region.deriveWithNewBounds(node.transformedBounds); } if (!region.isEmpty()) { region.roundOut(); region = node.computePadding(region); region = tx.transform(region, region); region = pvTx.transform(region, region); } return region; } }