1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.sg.prism;
  27 
  28 import com.sun.javafx.logging.PulseLogger;
  29 import javafx.scene.CacheHint;
  30 import java.util.List;
  31 import com.sun.javafx.geom.BaseBounds;
  32 import com.sun.javafx.geom.DirtyRegionContainer;
  33 import com.sun.javafx.geom.RectBounds;
  34 import com.sun.javafx.geom.Rectangle;
  35 import com.sun.javafx.geom.transform.Affine2D;
  36 import com.sun.javafx.geom.transform.Affine3D;
  37 import com.sun.javafx.geom.transform.BaseTransform;
  38 import com.sun.javafx.geom.transform.GeneralTransform3D;
  39 import com.sun.prism.Graphics;
  40 import com.sun.prism.RTTexture;
  41 import com.sun.prism.Texture;
  42 import com.sun.scenario.effect.Effect;
  43 import com.sun.scenario.effect.FilterContext;
  44 import com.sun.scenario.effect.Filterable;
  45 import com.sun.scenario.effect.ImageData;
  46 import com.sun.scenario.effect.impl.prism.PrDrawable;
  47 import com.sun.scenario.effect.impl.prism.PrFilterContext;
  48 import javafx.geometry.Insets;
  49 import javafx.scene.layout.Background;
  50 import javafx.scene.layout.BackgroundFill;
  51 import javafx.scene.paint.Color;
  52 
  53 /**
  54  * Base implementation of the Node.cache and cacheHint APIs.
  55  *
  56  * When all or a portion of the cacheHint becomes enabled, we should try *not*
  57  * to re-render the cache.  This avoids a big hiccup at the beginning of the
  58  * "use SPEED only while animating" use case:
  59  *   0) Under DEFAULT, we should already have a cached image
  60  *   1) scale/rotate caching is enabled (no expensive re-render required)
  61  *   2) animation happens, using the cached image
  62  *   3) animation completes, caching is disable and the node is re-rendered (at
  63  *      full-fidelity) with the final transform.
  64  *
  65  * Certain transform combinations are not supported, notably scaling by unequal
  66  * amounts in the x and y directions while also rotating.  Other than simple
  67  * translation, animations in this case will require re-rendering every frame.
  68  *
  69  * Ideally, a simple change to a Node's translation should never regenerate the
  70  * cached image.
  71  *
  72  * The CacheFilter is also capable of optimizing the scrolling of the cached contents.
  73  * For example, the ScrollView UI Control can define its content area as being cached,
  74  * such that when the user scrolls, we can shift the old content area and adjust the
  75  * dirty region so that it only includes the "newly exposed" area.
  76  */
  77 public class CacheFilter {
  78     /**
  79      * Defines the state when we're in the midst of scrolling a cached image
  80      */
  81     private static enum ScrollCacheState {
  82         CHECKING_PRECONDITIONS,
  83         ENABLED,
  84         DISABLED
  85     }
  86 
  87     // Garbage-reduction variables:
  88     private static final Rectangle TEMP_RECT = new Rectangle();
  89     private static final DirtyRegionContainer TEMP_CONTAINER = new DirtyRegionContainer(1);
  90     private static final Affine3D TEMP_CACHEFILTER_TRANSFORM = new Affine3D();
  91     private static final RectBounds TEMP_BOUNDS = new RectBounds();
  92     // Fun with floating point
  93     private static final double EPSILON = 0.0000001;
  94 
  95     private RTTexture tempTexture;
  96     private double lastXDelta;
  97     private double lastYDelta;
  98     private ScrollCacheState scrollCacheState = ScrollCacheState.CHECKING_PRECONDITIONS;
  99     // Note: this ImageData is always created and assumed to be untransformed.
 100     private ImageData cachedImageData;
 101     private Rectangle cacheBounds = new Rectangle();
 102     // Used to draw into the cache
 103     private final Affine2D cachedXform = new Affine2D();
 104 
 105     // The scale and rotate used to draw into the cache
 106     private double cachedScaleX;
 107     private double cachedScaleY;
 108     private double cachedRotate;
 109 
 110     private double cachedX;
 111     private double cachedY;
 112     private NGNode node;
 113 
 114     // Used to draw the cached image to the screen
 115     private final Affine2D screenXform = new Affine2D();
 116 
 117     // Cache hint settings
 118     private boolean scaleHint;
 119     private boolean rotateHint;
 120     // We keep this around for the sake of matchesHint
 121     private CacheHint cacheHint;
 122 
 123     // Was the last paint unsupported by the cache?  If so, will need to
 124     // regenerate the cache next time.
 125     private boolean wasUnsupported = false;
 126 
 127     /**
 128      * Compute the dirty region that must be re-rendered after scrolling
 129      */
 130     private Rectangle computeDirtyRegionForTranslate() {
 131         if (lastXDelta != 0) {
 132             if (lastXDelta > 0) {
 133                 TEMP_RECT.setBounds(0, 0, (int)lastXDelta, cacheBounds.height);
 134             } else {
 135                 TEMP_RECT.setBounds(cacheBounds.width + (int)lastXDelta, 0, -(int)lastXDelta, cacheBounds.height);
 136             }
 137         } else {
 138             if (lastYDelta > 0) {
 139                 TEMP_RECT.setBounds(0, 0, cacheBounds.width, (int)lastYDelta);
 140             } else {
 141                 TEMP_RECT.setBounds(0, cacheBounds.height + (int)lastYDelta, cacheBounds.width, -(int)lastYDelta);
 142             }
 143         }
 144         return TEMP_RECT;
 145     }
 146 
 147     protected CacheFilter(NGNode node, CacheHint cacheHint) {
 148         this.node = node;
 149         this.scrollCacheState = ScrollCacheState.CHECKING_PRECONDITIONS;
 150         setHint(cacheHint);
 151     }
 152 
 153     public void setHint(CacheHint cacheHint) {
 154         this.cacheHint = cacheHint;
 155         this.scaleHint = (cacheHint == CacheHint.SPEED ||
 156                           cacheHint == CacheHint.SCALE ||
 157                           cacheHint == CacheHint.SCALE_AND_ROTATE);
 158         this.rotateHint = (cacheHint == CacheHint.SPEED ||
 159                            cacheHint == CacheHint.ROTATE ||
 160                            cacheHint == CacheHint.SCALE_AND_ROTATE);
 161     }
 162 
 163     // These two methods exist only for the sake of testing.
 164     final boolean isScaleHint() { return scaleHint; }
 165     final boolean isRotateHint() { return rotateHint; }
 166 
 167     /**
 168      * Indicates whether this CacheFilter's hint matches the CacheHint
 169      * passed in.
 170      */
 171     boolean matchesHint(CacheHint cacheHint) {
 172         return this.cacheHint == cacheHint;
 173     }
 174 
 175     /**
 176      * Are we attempting to use cache for an unsupported transform mode?  Mostly
 177      * this is for trying to rotate while scaling the object by different
 178      * amounts in the x and y directions (this also includes shearing).
 179      */
 180     boolean unsupported(double[] xformInfo) {
 181         double scaleX = xformInfo[0];
 182         double scaleY = xformInfo[1];
 183         double rotate = xformInfo[2];
 184 
 185         // If we're trying to rotate...
 186         if (rotate > EPSILON || rotate < -EPSILON) {
 187             // ...and if scaleX != scaleY.  This can be in the render xform, or
 188             // may have made it into the cached image.
 189             if (scaleX > scaleY + EPSILON || scaleY > scaleX + EPSILON ||
 190                 scaleX < scaleY - EPSILON || scaleY < scaleX - EPSILON ||
 191                 cachedScaleX > cachedScaleY + EPSILON ||
 192                 cachedScaleY > cachedScaleX + EPSILON ||
 193                 cachedScaleX < cachedScaleY - EPSILON ||
 194                 cachedScaleY < cachedScaleX - EPSILON ) {
 195                     return true;
 196             }
 197         }
 198         return false;
 199     }
 200 
 201     private boolean isXformScrollCacheCapable(double[] xformInfo) {
 202         if (unsupported(xformInfo)) {
 203             return false;
 204         }
 205         double rotate = xformInfo[2];
 206         return rotateHint || rotate == 0;
 207     }
 208 
 209     /*
 210      * Do we need to regenerate the cached image?
 211      * Assumes that caller locked and validated the cachedImageData.untximage
 212      * if not null...
 213      */
 214     private boolean needToRenderCache(BaseTransform renderXform, double[] xformInfo) {
 215         if (cachedImageData == null) {
 216             return true;
 217         }
 218 
 219         if (lastXDelta != 0 || lastYDelta != 0) {
 220             if (Math.abs(lastXDelta) >= cacheBounds.width || Math.abs(lastYDelta) >= cacheBounds.height ||
 221                     Math.rint(lastXDelta) != lastXDelta || Math.rint(lastYDelta) != lastYDelta) {
 222                 node.clearDirtyTree(); // Need to clear dirty (by translation) flags in the children
 223                 lastXDelta = lastYDelta = 0;
 224                 return true;
 225             }
 226             if (scrollCacheState == ScrollCacheState.CHECKING_PRECONDITIONS) {
 227                 if (impl_scrollCacheCapable() && isXformScrollCacheCapable(xformInfo)) {
 228                     scrollCacheState = ScrollCacheState.ENABLED;
 229                 } else {
 230                     scrollCacheState = ScrollCacheState.DISABLED;
 231                     return true;
 232                 }
 233             }
 234         }
 235 
 236         // TODO: is == sufficient for floating point comparison here? (RT-23963)
 237         if (cachedXform.getMxx() == renderXform.getMxx() &&
 238             cachedXform.getMyy() == renderXform.getMyy() &&
 239             cachedXform.getMxy() == renderXform.getMxy() &&
 240             cachedXform.getMyx() == renderXform.getMyx()) {
 241             // It's just a translation - use cached Image
 242             return false;
 243         }
 244         // Not just a translation - if was or is unsupported, then must rerender
 245         if (wasUnsupported || unsupported(xformInfo)) {
 246             return true;
 247         }
 248 
 249         double scaleX = xformInfo[0];
 250         double scaleY = xformInfo[1];
 251         double rotate = xformInfo[2];
 252         if (scaleHint) {
 253             if (rotateHint) {
 254                 return false;
 255             } else {
 256                 // Not caching for rotate: regenerate cache if rotate changed
 257                 if (cachedRotate - EPSILON < rotate && rotate < cachedRotate + EPSILON) {
 258                     return false;
 259                 } else {
 260                     return true;
 261                 }
 262             }
 263         } else {
 264             if (rotateHint) {
 265                 // Not caching for scale: regenerate cache if scale changed
 266                 if (cachedScaleX - EPSILON < scaleX && scaleX < cachedScaleX + EPSILON &&
 267                     cachedScaleY - EPSILON < scaleY && scaleY < cachedScaleY + EPSILON) {
 268                     return false;
 269                 } else {// Scale is not "equal enough" - regenerate
 270                     return true;
 271                 }
 272             }
 273             else { // Not caching for anything; always regenerate
 274                 return true;
 275             }
 276         }
 277     }
 278 
 279     /*
 280      * Given the new xform info, update the screenXform as needed to correctly
 281      * paint the cache to the screen.
 282      */
 283     void updateScreenXform(double[] xformInfo) {
 284         // screenXform will be the difference between the cachedXform and the
 285         // render xform.
 286 
 287         if (scaleHint) {
 288             if (rotateHint) {
 289                 double screenScaleX = xformInfo[0] / cachedScaleX;
 290                 double screenScaleY = xformInfo[1] / cachedScaleY;
 291                 double screenRotate = xformInfo[2] - cachedRotate;
 292 
 293                 screenXform.setToScale(screenScaleX, screenScaleY);
 294                 screenXform.rotate(screenRotate);
 295             } else {
 296                 double screenScaleX = xformInfo[0] / cachedScaleX;
 297                 double screenScaleY = xformInfo[1] / cachedScaleY;
 298                 screenXform.setToScale(screenScaleX, screenScaleY);
 299             }
 300         } else {
 301             if (rotateHint) {
 302                 double screenRotate = xformInfo[2] - cachedRotate;
 303                 screenXform.setToRotation(screenRotate, 0.0, 0.0);
 304             } else {
 305                 // No caching, cache already rendered with xform; just paint it
 306                 screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM);
 307             }
 308         }
 309     }
 310 
 311     public void invalidate() {
 312         if (scrollCacheState == ScrollCacheState.ENABLED) {
 313             scrollCacheState = ScrollCacheState.CHECKING_PRECONDITIONS;
 314         }
 315         imageDataUnref();
 316         lastXDelta = lastYDelta = 0;
 317     }
 318 
 319     void imageDataUnref() {
 320         if (tempTexture != null) {
 321             tempTexture.dispose();
 322             tempTexture = null;
 323         }
 324         if (cachedImageData != null) {
 325             // While we hold on to this ImageData we leave the texture
 326             // unlocked so it can be reclaimed, but the default unref()
 327             // method assumes it was locked.
 328             Filterable implImage = cachedImageData.getUntransformedImage();
 329             if (implImage != null) {
 330                 implImage.lock();
 331             }
 332             cachedImageData.unref();
 333             cachedImageData = null;
 334         }
 335     }
 336 
 337     void invalidateByTranslation(double translateXDelta, double translateYDelta) {
 338         if (cachedImageData == null) {
 339             return;
 340         }
 341 
 342         if (scrollCacheState == ScrollCacheState.DISABLED) {
 343             imageDataUnref();
 344         } else {
 345              // When both mxt and myt change, we don't currently use scroll optimization
 346             if (translateXDelta != 0 && translateYDelta != 0) {
 347                 imageDataUnref();
 348             } else {
 349                 lastYDelta = translateYDelta;
 350                 lastXDelta = translateXDelta;
 351             }
 352         }
 353     }
 354 
 355     public void dispose() {
 356         invalidate();
 357         node = null;
 358     }
 359 
 360     /*
 361      * unmatrix() and the supporting functions are based on the code from
 362      * "Decomposing A Matrix Into Simple Transformations" by Spencer W. Thomas
 363      * from Graphics Gems II, as found at
 364      * http://tog.acm.org/resources/GraphicsGems/
 365      * which states, "All code here can be used without restrictions."
 366      *
 367      * The code was reduced from handling a 4x4 matrix (3D w/ perspective)
 368      * to handle just a 2x2 (2D scale/rotate, w/o translate, as that is handled
 369      * separately).
 370      */
 371 
 372     /**
 373      * Given a BaseTransform, decompose it into values for scaleX, scaleY and
 374      * rotate.
 375      *
 376      * The return value is a double[3], the values being:
 377      *   [0]: scaleX
 378      *   [1]: scaleY
 379      *   [2]: rotation angle, in radians, between *** and ***
 380      *
 381      * From unmatrix() in unmatrix.c
 382      */
 383     double[] unmatrix(BaseTransform xform) {
 384         double[] retVal = new double[3];
 385 
 386         double[][] row = {{xform.getMxx(), xform.getMxy()},
 387             {xform.getMyx(), xform.getMyy()}};
 388         final double xSignum = Math.signum(row[0][0]);
 389         final double ySignum = Math.signum(row[1][1]);
 390 
 391         // Compute X scale factor and normalize first row.
 392         // tran[U_SCALEX] = V3Length(&row[0]);
 393         // row[0] = *V3Scale(&row[0], 1.0);
 394 
 395         double scaleX = xSignum * v2length(row[0]);
 396         v2scale(row[0], xSignum);
 397 
 398         // Compute XY shear factor and make 2nd row orthogonal to 1st.
 399         // tran[U_SHEARXY] = V3Dot(&row[0], &row[1]);
 400         // (void)V3Combine(&row[1], &row[0], &row[1], 1.0, -tran[U_SHEARXY]);
 401         //
 402         // "this is too large by the y scaling factor"
 403         double shearXY = v2dot(row[0], row[1]);
 404 
 405         // Combine into row[1]
 406         v2combine(row[1], row[0], row[1], 1.0, -shearXY);
 407 
 408         // Now, compute Y scale and normalize 2nd row
 409         // tran[U_SCALEY] = V3Length(&row[1]);
 410         // V3Scale(&row[1], 1.0);
 411         // tran[U_SHEARXY] /= tran[U_SCALEY];
 412 
 413         double scaleY = ySignum * v2length(row[1]);
 414         v2scale(row[1], ySignum);
 415 
 416         // Now extract the rotation. (This is new code, not from the Gem.)
 417         //
 418         // In our matrix, we now have
 419         // [   cos(theta)    -sin(theta)    ]
 420         // [   sin(theta)     cos(theta)    ]
 421         //
 422         // TODO: assert: all 4 values are sane (RT-23962)
 423         //
 424         double sin = row[1][0];
 425         double cos = row[0][0];
 426         double angleRad = 0.0;
 427 
 428         // Recall:
 429         // arcsin works for theta: -90 -> 90
 430         // arccos works for theta:   0 -> 180
 431         if (sin >= 0) {
 432             // theta is 0 -> 180, use acos()
 433             angleRad = Math.acos(cos);
 434         } else {
 435             if (cos > 0) {
 436                 // sin < 0, cos > 0, so theta is 270 -> 360, aka -90 -> 0
 437                 // use asin(), add 360
 438                 angleRad = 2.0 * Math.PI + Math.asin(sin);
 439             } else {
 440                 // sin < 0, cos < 0, so theta 180 -> 270
 441                 // cos from 180 -> 270 is inverse of cos from 0->90,
 442                 // so take acos(-cos) and add 180
 443                 angleRad = Math.PI + Math.acos(-cos);
 444             }
 445         }
 446 
 447         retVal[0] = scaleX;
 448         retVal[1] = scaleY;
 449         retVal[2] = angleRad;
 450 
 451         return retVal;
 452     }
 453 
 454     /**
 455      * make a linear combination of two vectors and return the result
 456      * result = (v0 * scalarA) + (v1 * scalarB)
 457      *
 458      * From V3Combine() in GGVecLib.c
 459      */
 460     void v2combine(double v0[], double v1[], double result[], double scalarA, double scalarB) {
 461         // make a linear combination of two vectors and return the result.
 462         // result = (a * ascl) + (b * bscl)
 463         /*
 464         Vector3 *V3Combine (a, b, result, ascl, bscl)
 465         Vector3 *a, *b, *result;
 466         double ascl, bscl;
 467         {
 468                 result->x = (ascl * a->x) + (bscl * b->x);
 469                 result->y = (ascl * a->y) + (bscl * b->y);
 470                 result->z = (ascl * a->z) + (bscl * b->z);
 471                 return(result);
 472         */
 473 
 474         result[0] = scalarA*v0[0] + scalarB*v1[0];
 475         result[1] = scalarA*v0[1] + scalarB*v1[1];
 476     }
 477 
 478     /**
 479      * dot product of 2 vectors of length 2
 480      */
 481     double v2dot(double v0[], double v1[]) {
 482         return v0[0]*v1[0] + v0[1]*v1[1];
 483     }
 484 
 485     /**
 486      * scale v[] to be relative to newLen
 487      *
 488      * From V3Scale() in GGVecLib.c
 489      */
 490     void v2scale(double v[], double newLen) {
 491         double len = v2length(v);
 492         if (len != 0) {
 493             v[0] *= newLen / len;
 494             v[1] *= newLen / len;
 495         }
 496     }
 497 
 498     /**
 499      * returns length of input vector
 500      *
 501      * Based on V3Length() in GGVecLib.c
 502      */
 503     double v2length(double v[]) {
 504         return Math.sqrt(v[0]*v[0] + v[1]*v[1]);
 505     }
 506 
 507     void render(Graphics g) {
 508         // The following is safe; xform will not be mutated below
 509         BaseTransform xform = g.getTransformNoClone();
 510         FilterContext fctx = PrFilterContext.getInstance(g.getAssociatedScreen()); // getFilterContext
 511 
 512         double[] xformInfo = unmatrix(xform);
 513         boolean isUnsupported = unsupported(xformInfo);
 514 
 515         lastXDelta = lastXDelta * xformInfo[0];
 516         lastYDelta = lastYDelta * xformInfo[1];
 517 
 518         if (cachedImageData != null) {
 519             Filterable implImage = cachedImageData.getUntransformedImage();
 520             if (implImage != null) {
 521                 implImage.lock();
 522                 if (!cachedImageData.validate(fctx)) {
 523                     implImage.unlock();
 524                     invalidate();
 525                 }
 526             }
 527         }
 528         if (needToRenderCache(xform, xformInfo)) {
 529             if (PulseLogger.PULSE_LOGGING_ENABLED) PulseLogger.PULSE_LOGGER.renderIncrementCounter("CacheFilter rebuilding");
 530             if (cachedImageData != null) {
 531                 Filterable implImage = cachedImageData.getUntransformedImage();
 532                 if (implImage != null) {
 533                     implImage.unlock();
 534                 }
 535                 invalidate();
 536             }
 537             if (scaleHint) {
 538                 // do not cache the image at a small scale factor when
 539                 // scaleHint is set as it leads to poor rendering results
 540                 // when image is scaled up.
 541                 cachedScaleX = Math.max(NGNode.highestPixelScale, xformInfo[0]);
 542                 cachedScaleY = Math.max(NGNode.highestPixelScale, xformInfo[1]);
 543                 cachedRotate = 0;
 544                 cachedXform.setTransform(cachedScaleX, 0.0,
 545                                          0.0, cachedScaleX,
 546                                          0.0, 0.0);
 547                 updateScreenXform(xformInfo);
 548             } else {
 549                 cachedScaleX = xformInfo[0];
 550                 cachedScaleY = xformInfo[1];
 551                 cachedRotate = xformInfo[2];
 552 
 553                 // Update the cachedXform to the current xform (ignoring translate).
 554                 cachedXform.setTransform(xform.getMxx(), xform.getMyx(),
 555                                          xform.getMxy(), xform.getMyy(),
 556                                          0.0, 0.0);
 557 
 558                 // screenXform is always identity in this case, as we've just
 559                 // rendered into the cache using the render xform.
 560                 screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM);
 561             }
 562 
 563             cacheBounds = impl_getCacheBounds(cacheBounds, cachedXform);
 564             cachedImageData = impl_createImageData(fctx, cacheBounds);
 565             impl_renderNodeToCache(cachedImageData, cacheBounds, cachedXform, null);
 566 
 567             // cachedBounds includes effects, and is in *scene* coords
 568             Rectangle cachedBounds = cachedImageData.getUntransformedBounds();
 569 
 570             // Save out the (un-transformed) x & y coordinates.  This accounts
 571             // for effects and other reasons the untranslated location may not
 572             // be 0,0.
 573             cachedX = cachedBounds.x;
 574             cachedY = cachedBounds.y;
 575 
 576         } else {
 577             if (scrollCacheState == ScrollCacheState.ENABLED &&
 578                     (lastXDelta != 0 || lastYDelta != 0) ) {
 579                 impl_moveCacheBy(cachedImageData, lastXDelta, lastYDelta);
 580                 impl_renderNodeToCache(cachedImageData, cacheBounds, cachedXform, computeDirtyRegionForTranslate());
 581                 lastXDelta = lastYDelta = 0;
 582             }
 583             // Using the cached image; calculate screenXform to paint to screen.
 584             if (isUnsupported) {
 585                 // Only way we should be using the cached image in the
 586                 // unsupported case is for a change in translate only.  No other
 587                 // xform should be needed, so use identity.
 588 
 589                 // TODO: assert cachedXform == render xform (ignoring translate)
 590                 //   or  assert xforminfo == cachedXform info (RT-23962)
 591                 screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM);
 592             } else {
 593                 updateScreenXform(xformInfo);
 594             }
 595         }
 596         // If this render is unsupported, remember for next time.  We'll need
 597         // to regenerate the cache once we're in a supported scenario again.
 598         wasUnsupported = isUnsupported;
 599 
 600         Filterable implImage = cachedImageData.getUntransformedImage();
 601         if (implImage == null) {
 602             if (PulseLogger.PULSE_LOGGING_ENABLED) PulseLogger.PULSE_LOGGER.renderIncrementCounter("CacheFilter not used");
 603             impl_renderNodeToScreen(g);
 604         } else {
 605             double mxt = xform.getMxt();
 606             double myt = xform.getMyt();
 607             impl_renderCacheToScreen(g, implImage, mxt, myt);
 608             implImage.unlock();
 609         }
 610     }
 611 
 612     /**
 613      * Create the ImageData for the cached bitmap, with the specified bounds.
 614      */
 615     ImageData impl_createImageData(FilterContext fctx, Rectangle bounds) {
 616         Filterable ret;
 617         try {
 618             ret = Effect.getCompatibleImage(fctx,
 619                     bounds.width, bounds.height);
 620             Texture cachedTex = ((PrDrawable) ret).getTextureObject();
 621             cachedTex.contentsUseful();
 622         } catch (Throwable e) {
 623             ret = null;
 624         }
 625 
 626         return new ImageData(fctx, ret, bounds);
 627     }
 628 
 629     /**
 630      * Render node to cache.
 631      * @param cacheData the cache
 632      * @param cacheBounds cache bounds
 633      * @param xform transformation
 634      * @param dirtyBounds null or dirty rectangle to be rendered
 635      */
 636     void impl_renderNodeToCache(ImageData cacheData,
 637                                 Rectangle cacheBounds,
 638                                 BaseTransform xform,
 639                                 Rectangle dirtyBounds) {
 640         final PrDrawable image = (PrDrawable) cacheData.getUntransformedImage();
 641 
 642         if (image != null) {
 643             Graphics g = image.createGraphics();
 644             TEMP_CACHEFILTER_TRANSFORM.setToIdentity();
 645             TEMP_CACHEFILTER_TRANSFORM.translate(-cacheBounds.x, -cacheBounds.y);
 646             if (xform != null) {
 647                 TEMP_CACHEFILTER_TRANSFORM.concatenate(xform);
 648             }
 649             if (dirtyBounds != null) {
 650                 TEMP_CONTAINER.deriveWithNewRegion((RectBounds)TEMP_BOUNDS.deriveWithNewBounds(dirtyBounds));
 651                 // Culling might save us a lot when there's a dirty region
 652                 node.doPreCulling(TEMP_CONTAINER, TEMP_CACHEFILTER_TRANSFORM, new GeneralTransform3D());
 653                 g.setHasPreCullingBits(true);
 654                 g.setClipRectIndex(0);
 655                 g.setClipRect(dirtyBounds);
 656             }
 657             g.transform(TEMP_CACHEFILTER_TRANSFORM);
 658             if (node.getClipNode() != null) {
 659                 node.renderClip(g);
 660             } else if (node.getEffectFilter() != null) {
 661                 node.renderEffect(g);
 662             } else {
 663                 node.renderContent(g);
 664             }
 665         }
 666     }
 667 
 668     /**
 669      * Render the node directly to the screen, in the case that the cached
 670      * image is unexpectedly null.  See RT-6428.
 671      */
 672     void impl_renderNodeToScreen(Object implGraphics) {
 673         Graphics g = (Graphics)implGraphics;
 674         if (node.getEffectFilter() != null) {
 675             node.renderEffect(g);
 676         } else {
 677             node.renderContent(g);
 678         }
 679     }
 680 
 681     /**
 682      * Render the cached image to the screen, translated by mxt, myt.
 683      */
 684     void impl_renderCacheToScreen(Object implGraphics, Filterable implImage,
 685                                   double mxt, double myt)
 686     {
 687         Graphics g = (Graphics)implGraphics;
 688 
 689         g.setTransform(screenXform.getMxx(),
 690                        screenXform.getMyx(),
 691                        screenXform.getMxy(),
 692                        screenXform.getMyy(),
 693                        mxt, myt);
 694         g.translate((float)cachedX, (float)cachedY);
 695         Texture cachedTex = ((PrDrawable)implImage).getTextureObject();
 696         Rectangle cachedBounds = cachedImageData.getUntransformedBounds();
 697         g.drawTexture(cachedTex, 0, 0,
 698                       cachedBounds.width, cachedBounds.height);
 699         // FYI: transform state is restored by the NGNode.render() method
 700     }
 701 
 702     /**
 703      * True if we can use scrolling optimization on this node.
 704      */
 705     boolean impl_scrollCacheCapable() {
 706         if (!(node instanceof NGGroup)) {
 707             return false;
 708         }
 709         List<NGNode> children = ((NGGroup)node).getChildren();
 710         if (children.size() != 1) {
 711             return false;
 712         }
 713         NGNode child = children.get(0);
 714         if (!child.getTransform().is2D()) {
 715             return false;
 716         }
 717 
 718         NGNode clip = node.getClipNode();
 719         if (clip == null || !clip.isRectClip(BaseTransform.IDENTITY_TRANSFORM, false)) {
 720             return false;
 721         }
 722         
 723         if (node instanceof NGRegion) {
 724             NGRegion region = (NGRegion) node;
 725             if (!region.getBorder().isEmpty()) {
 726                 return false;
 727             }
 728             final Background background = region.getBackground();
 729 
 730             if (!background.isEmpty()) {
 731                 if (!background.getImages().isEmpty()
 732                         || background.getFills().size() != 1) {
 733                     return false;
 734                 }
 735                 BackgroundFill fill = background.getFills().get(0);
 736                 javafx.scene.paint.Paint fillPaint = fill.getFill();
 737                 BaseBounds clipBounds = clip.getCompleteBounds(TEMP_BOUNDS, BaseTransform.IDENTITY_TRANSFORM);
 738 
 739                 return fillPaint.isOpaque() && fillPaint instanceof Color && fill.getInsets().equals(Insets.EMPTY)
 740                         && clipBounds.getMinX() == 0 && clipBounds.getMinY() == 0
 741                         && clipBounds.getMaxX() == region.getWidth() && clipBounds.getMaxY() == region.getHeight();
 742             }
 743         }
 744         
 745         return true;
 746     }
 747 
 748     /**
 749      * Moves a subregion of the cache, "scrolling" the cache by x/y Delta.
 750      * On of xDelta/yDelta must be zero. The rest of the pixels will be cleared.
 751      * @param cachedImageData cache
 752      * @param xDelta x-axis delta
 753      * @param yDelta y-axis delta
 754      */
 755     void impl_moveCacheBy(ImageData cachedImageData, double xDelta, double yDelta) {
 756         PrDrawable drawable = (PrDrawable) cachedImageData.getUntransformedImage();
 757         final Rectangle r = cachedImageData.getUntransformedBounds();
 758         int x = (int)Math.max(0, (-xDelta));
 759         int y = (int)Math.max(0, (-yDelta));
 760         int destX = (int)Math.max(0, (xDelta));
 761         int destY = (int) Math.max(0, yDelta);
 762         int w = r.width - (int) Math.abs(xDelta);
 763         int h = r.height - (int) Math.abs(yDelta);
 764 
 765         final Graphics g = drawable.createGraphics();
 766         if (tempTexture != null) {
 767             tempTexture.lock();
 768             if (tempTexture.isSurfaceLost()) {
 769                 tempTexture = null;
 770             }
 771         }
 772         if (tempTexture == null) {
 773             tempTexture = g.getResourceFactory().
 774                 createRTTexture(drawable.getPhysicalWidth(), drawable.getPhysicalHeight(),
 775                                 Texture.WrapMode.CLAMP_NOT_NEEDED);
 776         }
 777         final Graphics tempG = tempTexture.createGraphics();
 778         tempG.clear();
 779         tempG.drawTexture(drawable.getTextureObject(), 0, 0, w, h, x, y, x + w, y + h);
 780         tempG.sync();
 781 
 782         g.clear();
 783         g.drawTexture(tempTexture, destX, destY, destX + w, destY + h, 0, 0, w, h);
 784         tempTexture.unlock();
 785     }
 786 
 787     /**
 788      * Get the cache bounds.
 789      * @param bounds rectangle to store bounds to
 790      * @param xform transformation
 791      */
 792     Rectangle impl_getCacheBounds(Rectangle bounds, BaseTransform xform) {
 793         final BaseBounds b = node.getClippedBounds(TEMP_BOUNDS, xform);
 794         bounds.setBounds(b);
 795         return bounds;
 796     }
 797     
 798     BaseBounds computeDirtyBounds(BaseBounds region, BaseTransform tx, GeneralTransform3D pvTx) {
 799         // For now, we just use the computed dirty bounds of the Node and
 800         // round them out before the transforms.
 801         // Later, we could use the bounds of the cache
 802         // to compute the dirty region directly (and more accurately).
 803         // See RT-34928 for more details.
 804         if (!node.dirtyBounds.isEmpty()) {
 805             region = region.deriveWithNewBounds(node.dirtyBounds);
 806         } else {
 807             region = region.deriveWithNewBounds(node.transformedBounds);
 808         }
 809 
 810         if (!region.isEmpty()) {
 811             region.roundOut();
 812             region = node.computePadding(region);
 813             region = tx.transform(region, region);
 814             region = pvTx.transform(region, region);
 815         }
 816         return region;
 817     }
 818 }