1 /*
   2  * Copyright (c) 2011, 2015, 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, float pixelScale) {
 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 (cachedScaleX < pixelScale || cachedScaleY < pixelScale) {
 254                 // We have moved onto a screen with a higher pixelScale and
 255                 // our cache was less than that pixel scale.  Even though
 256                 // we have the scaleHint, we always cache at a minimum of
 257                 // the pixel scale of the screen so we need to re-cache.
 258                 return true;
 259             }
 260             if (rotateHint) {
 261                 return false;
 262             } else {
 263                 // Not caching for rotate: regenerate cache if rotate changed
 264                 if (cachedRotate - EPSILON < rotate && rotate < cachedRotate + EPSILON) {
 265                     return false;
 266                 } else {
 267                     return true;
 268                 }
 269             }
 270         } else {
 271             if (rotateHint) {
 272                 // Not caching for scale: regenerate cache if scale changed
 273                 if (cachedScaleX - EPSILON < scaleX && scaleX < cachedScaleX + EPSILON &&
 274                     cachedScaleY - EPSILON < scaleY && scaleY < cachedScaleY + EPSILON) {
 275                     return false;
 276                 } else {// Scale is not "equal enough" - regenerate
 277                     return true;
 278                 }
 279             }
 280             else { // Not caching for anything; always regenerate
 281                 return true;
 282             }
 283         }
 284     }
 285 
 286     /*
 287      * Given the new xform info, update the screenXform as needed to correctly
 288      * paint the cache to the screen.
 289      */
 290     void updateScreenXform(double[] xformInfo) {
 291         // screenXform will be the difference between the cachedXform and the
 292         // render xform.
 293 
 294         if (scaleHint) {
 295             if (rotateHint) {
 296                 double screenScaleX = xformInfo[0] / cachedScaleX;
 297                 double screenScaleY = xformInfo[1] / cachedScaleY;
 298                 double screenRotate = xformInfo[2] - cachedRotate;
 299 
 300                 screenXform.setToScale(screenScaleX, screenScaleY);
 301                 screenXform.rotate(screenRotate);
 302             } else {
 303                 double screenScaleX = xformInfo[0] / cachedScaleX;
 304                 double screenScaleY = xformInfo[1] / cachedScaleY;
 305                 screenXform.setToScale(screenScaleX, screenScaleY);
 306             }
 307         } else {
 308             if (rotateHint) {
 309                 double screenRotate = xformInfo[2] - cachedRotate;
 310                 screenXform.setToRotation(screenRotate, 0.0, 0.0);
 311             } else {
 312                 // No caching, cache already rendered with xform; just paint it
 313                 screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM);
 314             }
 315         }
 316     }
 317 
 318     public void invalidate() {
 319         if (scrollCacheState == ScrollCacheState.ENABLED) {
 320             scrollCacheState = ScrollCacheState.CHECKING_PRECONDITIONS;
 321         }
 322         imageDataUnref();
 323         lastXDelta = lastYDelta = 0;
 324     }
 325 
 326     void imageDataUnref() {
 327         if (tempTexture != null) {
 328             tempTexture.dispose();
 329             tempTexture = null;
 330         }
 331         if (cachedImageData != null) {
 332             // While we hold on to this ImageData we leave the texture
 333             // unlocked so it can be reclaimed, but the default unref()
 334             // method assumes it was locked.
 335             Filterable implImage = cachedImageData.getUntransformedImage();
 336             if (implImage != null) {
 337                 implImage.lock();
 338             }
 339             cachedImageData.unref();
 340             cachedImageData = null;
 341         }
 342     }
 343 
 344     void invalidateByTranslation(double translateXDelta, double translateYDelta) {
 345         if (cachedImageData == null) {
 346             return;
 347         }
 348 
 349         if (scrollCacheState == ScrollCacheState.DISABLED) {
 350             imageDataUnref();
 351         } else {
 352              // When both mxt and myt change, we don't currently use scroll optimization
 353             if (translateXDelta != 0 && translateYDelta != 0) {
 354                 imageDataUnref();
 355             } else {
 356                 lastYDelta = translateYDelta;
 357                 lastXDelta = translateXDelta;
 358             }
 359         }
 360     }
 361 
 362     public void dispose() {
 363         invalidate();
 364         node = null;
 365     }
 366 
 367     /*
 368      * unmatrix() and the supporting functions are based on the code from
 369      * "Decomposing A Matrix Into Simple Transformations" by Spencer W. Thomas
 370      * from Graphics Gems II, as found at
 371      * http://tog.acm.org/resources/GraphicsGems/
 372      * which states, "All code here can be used without restrictions."
 373      *
 374      * The code was reduced from handling a 4x4 matrix (3D w/ perspective)
 375      * to handle just a 2x2 (2D scale/rotate, w/o translate, as that is handled
 376      * separately).
 377      */
 378 
 379     /**
 380      * Given a BaseTransform, decompose it into values for scaleX, scaleY and
 381      * rotate.
 382      *
 383      * The return value is a double[3], the values being:
 384      *   [0]: scaleX
 385      *   [1]: scaleY
 386      *   [2]: rotation angle, in radians, between *** and ***
 387      *
 388      * From unmatrix() in unmatrix.c
 389      */
 390     double[] unmatrix(BaseTransform xform) {
 391         double[] retVal = new double[3];
 392 
 393         double[][] row = {{xform.getMxx(), xform.getMxy()},
 394             {xform.getMyx(), xform.getMyy()}};
 395         final double xSignum = Math.signum(row[0][0]);
 396         final double ySignum = Math.signum(row[1][1]);
 397 
 398         // Compute X scale factor and normalize first row.
 399         // tran[U_SCALEX] = V3Length(&row[0]);
 400         // row[0] = *V3Scale(&row[0], 1.0);
 401 
 402         double scaleX = xSignum * v2length(row[0]);
 403         v2scale(row[0], xSignum);
 404 
 405         // Compute XY shear factor and make 2nd row orthogonal to 1st.
 406         // tran[U_SHEARXY] = V3Dot(&row[0], &row[1]);
 407         // (void)V3Combine(&row[1], &row[0], &row[1], 1.0, -tran[U_SHEARXY]);
 408         //
 409         // "this is too large by the y scaling factor"
 410         double shearXY = v2dot(row[0], row[1]);
 411 
 412         // Combine into row[1]
 413         v2combine(row[1], row[0], row[1], 1.0, -shearXY);
 414 
 415         // Now, compute Y scale and normalize 2nd row
 416         // tran[U_SCALEY] = V3Length(&row[1]);
 417         // V3Scale(&row[1], 1.0);
 418         // tran[U_SHEARXY] /= tran[U_SCALEY];
 419 
 420         double scaleY = ySignum * v2length(row[1]);
 421         v2scale(row[1], ySignum);
 422 
 423         // Now extract the rotation. (This is new code, not from the Gem.)
 424         //
 425         // In our matrix, we now have
 426         // [   cos(theta)    -sin(theta)    ]
 427         // [   sin(theta)     cos(theta)    ]
 428         //
 429         // TODO: assert: all 4 values are sane (RT-23962)
 430         //
 431         double sin = row[1][0];
 432         double cos = row[0][0];
 433         double angleRad = 0.0;
 434 
 435         // Recall:
 436         // arcsin works for theta: -90 -> 90
 437         // arccos works for theta:   0 -> 180
 438         if (sin >= 0) {
 439             // theta is 0 -> 180, use acos()
 440             angleRad = Math.acos(cos);
 441         } else {
 442             if (cos > 0) {
 443                 // sin < 0, cos > 0, so theta is 270 -> 360, aka -90 -> 0
 444                 // use asin(), add 360
 445                 angleRad = 2.0 * Math.PI + Math.asin(sin);
 446             } else {
 447                 // sin < 0, cos < 0, so theta 180 -> 270
 448                 // cos from 180 -> 270 is inverse of cos from 0->90,
 449                 // so take acos(-cos) and add 180
 450                 angleRad = Math.PI + Math.acos(-cos);
 451             }
 452         }
 453 
 454         retVal[0] = scaleX;
 455         retVal[1] = scaleY;
 456         retVal[2] = angleRad;
 457 
 458         return retVal;
 459     }
 460 
 461     /**
 462      * make a linear combination of two vectors and return the result
 463      * result = (v0 * scalarA) + (v1 * scalarB)
 464      *
 465      * From V3Combine() in GGVecLib.c
 466      */
 467     void v2combine(double v0[], double v1[], double result[], double scalarA, double scalarB) {
 468         // make a linear combination of two vectors and return the result.
 469         // result = (a * ascl) + (b * bscl)
 470         /*
 471         Vector3 *V3Combine (a, b, result, ascl, bscl)
 472         Vector3 *a, *b, *result;
 473         double ascl, bscl;
 474         {
 475                 result->x = (ascl * a->x) + (bscl * b->x);
 476                 result->y = (ascl * a->y) + (bscl * b->y);
 477                 result->z = (ascl * a->z) + (bscl * b->z);
 478                 return(result);
 479         */
 480 
 481         result[0] = scalarA*v0[0] + scalarB*v1[0];
 482         result[1] = scalarA*v0[1] + scalarB*v1[1];
 483     }
 484 
 485     /**
 486      * dot product of 2 vectors of length 2
 487      */
 488     double v2dot(double v0[], double v1[]) {
 489         return v0[0]*v1[0] + v0[1]*v1[1];
 490     }
 491 
 492     /**
 493      * scale v[] to be relative to newLen
 494      *
 495      * From V3Scale() in GGVecLib.c
 496      */
 497     void v2scale(double v[], double newLen) {
 498         double len = v2length(v);
 499         if (len != 0) {
 500             v[0] *= newLen / len;
 501             v[1] *= newLen / len;
 502         }
 503     }
 504 
 505     /**
 506      * returns length of input vector
 507      *
 508      * Based on V3Length() in GGVecLib.c
 509      */
 510     double v2length(double v[]) {
 511         return Math.sqrt(v[0]*v[0] + v[1]*v[1]);
 512     }
 513 
 514     void render(Graphics g) {
 515         // The following is safe; xform will not be mutated below
 516         BaseTransform xform = g.getTransformNoClone();
 517         FilterContext fctx = PrFilterContext.getInstance(g.getAssociatedScreen()); // getFilterContext
 518 
 519         double[] xformInfo = unmatrix(xform);
 520         boolean isUnsupported = unsupported(xformInfo);
 521 
 522         lastXDelta = lastXDelta * xformInfo[0];
 523         lastYDelta = lastYDelta * xformInfo[1];
 524 
 525         if (cachedImageData != null) {
 526             Filterable implImage = cachedImageData.getUntransformedImage();
 527             if (implImage != null) {
 528                 implImage.lock();
 529                 if (!cachedImageData.validate(fctx)) {
 530                     implImage.unlock();
 531                     invalidate();
 532                 }
 533             }
 534         }
 535         float pixelScale = g.getAssociatedScreen().getScale();
 536         if (needToRenderCache(xform, xformInfo, pixelScale)) {
 537             if (PulseLogger.PULSE_LOGGING_ENABLED) {
 538                 PulseLogger.incrementCounter("CacheFilter rebuilding");
 539             }
 540             if (cachedImageData != null) {
 541                 Filterable implImage = cachedImageData.getUntransformedImage();
 542                 if (implImage != null) {
 543                     implImage.unlock();
 544                 }
 545                 invalidate();
 546             }
 547             if (scaleHint) {
 548                 // do not cache the image at a small scale factor when
 549                 // scaleHint is set as it leads to poor rendering results
 550                 // when image is scaled up.
 551                 cachedScaleX = Math.max(pixelScale, xformInfo[0]);
 552                 cachedScaleY = Math.max(pixelScale, xformInfo[1]);
 553                 cachedRotate = 0;
 554                 cachedXform.setTransform(cachedScaleX, 0.0,
 555                                          0.0, cachedScaleX,
 556                                          0.0, 0.0);
 557                 updateScreenXform(xformInfo);
 558             } else {
 559                 cachedScaleX = xformInfo[0];
 560                 cachedScaleY = xformInfo[1];
 561                 cachedRotate = xformInfo[2];
 562 
 563                 // Update the cachedXform to the current xform (ignoring translate).
 564                 cachedXform.setTransform(xform.getMxx(), xform.getMyx(),
 565                                          xform.getMxy(), xform.getMyy(),
 566                                          0.0, 0.0);
 567 
 568                 // screenXform is always identity in this case, as we've just
 569                 // rendered into the cache using the render xform.
 570                 screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM);
 571             }
 572 
 573             cacheBounds = impl_getCacheBounds(cacheBounds, cachedXform);
 574             cachedImageData = impl_createImageData(fctx, cacheBounds);
 575             impl_renderNodeToCache(cachedImageData, cacheBounds, cachedXform, null);
 576 
 577             // cachedBounds includes effects, and is in *scene* coords
 578             Rectangle cachedBounds = cachedImageData.getUntransformedBounds();
 579 
 580             // Save out the (un-transformed) x & y coordinates.  This accounts
 581             // for effects and other reasons the untranslated location may not
 582             // be 0,0.
 583             cachedX = cachedBounds.x;
 584             cachedY = cachedBounds.y;
 585 
 586         } else {
 587             if (scrollCacheState == ScrollCacheState.ENABLED &&
 588                     (lastXDelta != 0 || lastYDelta != 0) ) {
 589                 impl_moveCacheBy(cachedImageData, lastXDelta, lastYDelta);
 590                 impl_renderNodeToCache(cachedImageData, cacheBounds, cachedXform, computeDirtyRegionForTranslate());
 591                 lastXDelta = lastYDelta = 0;
 592             }
 593             // Using the cached image; calculate screenXform to paint to screen.
 594             if (isUnsupported) {
 595                 // Only way we should be using the cached image in the
 596                 // unsupported case is for a change in translate only.  No other
 597                 // xform should be needed, so use identity.
 598 
 599                 // TODO: assert cachedXform == render xform (ignoring translate)
 600                 //   or  assert xforminfo == cachedXform info (RT-23962)
 601                 screenXform.setTransform(BaseTransform.IDENTITY_TRANSFORM);
 602             } else {
 603                 updateScreenXform(xformInfo);
 604             }
 605         }
 606         // If this render is unsupported, remember for next time.  We'll need
 607         // to regenerate the cache once we're in a supported scenario again.
 608         wasUnsupported = isUnsupported;
 609 
 610         Filterable implImage = cachedImageData.getUntransformedImage();
 611         if (implImage == null) {
 612             if (PulseLogger.PULSE_LOGGING_ENABLED) {
 613                 PulseLogger.incrementCounter("CacheFilter not used");
 614             }
 615             impl_renderNodeToScreen(g);
 616         } else {
 617             double mxt = xform.getMxt();
 618             double myt = xform.getMyt();
 619             impl_renderCacheToScreen(g, implImage, mxt, myt);
 620             implImage.unlock();
 621         }
 622     }
 623 
 624     /**
 625      * Create the ImageData for the cached bitmap, with the specified bounds.
 626      */
 627     ImageData impl_createImageData(FilterContext fctx, Rectangle bounds) {
 628         Filterable ret;
 629         try {
 630             ret = Effect.getCompatibleImage(fctx,
 631                     bounds.width, bounds.height);
 632             Texture cachedTex = ((PrDrawable) ret).getTextureObject();
 633             cachedTex.contentsUseful();
 634         } catch (Throwable e) {
 635             ret = null;
 636         }
 637 
 638         return new ImageData(fctx, ret, bounds);
 639     }
 640 
 641     /**
 642      * Render node to cache.
 643      * @param cacheData the cache
 644      * @param cacheBounds cache bounds
 645      * @param xform transformation
 646      * @param dirtyBounds null or dirty rectangle to be rendered
 647      */
 648     void impl_renderNodeToCache(ImageData cacheData,
 649                                 Rectangle cacheBounds,
 650                                 BaseTransform xform,
 651                                 Rectangle dirtyBounds) {
 652         final PrDrawable image = (PrDrawable) cacheData.getUntransformedImage();
 653 
 654         if (image != null) {
 655             Graphics g = image.createGraphics();
 656             TEMP_CACHEFILTER_TRANSFORM.setToIdentity();
 657             TEMP_CACHEFILTER_TRANSFORM.translate(-cacheBounds.x, -cacheBounds.y);
 658             if (xform != null) {
 659                 TEMP_CACHEFILTER_TRANSFORM.concatenate(xform);
 660             }
 661             if (dirtyBounds != null) {
 662                 TEMP_CONTAINER.deriveWithNewRegion((RectBounds)TEMP_BOUNDS.deriveWithNewBounds(dirtyBounds));
 663                 // Culling might save us a lot when there's a dirty region
 664                 node.doPreCulling(TEMP_CONTAINER, TEMP_CACHEFILTER_TRANSFORM, new GeneralTransform3D());
 665                 g.setHasPreCullingBits(true);
 666                 g.setClipRectIndex(0);
 667                 g.setClipRect(dirtyBounds);
 668             }
 669             g.transform(TEMP_CACHEFILTER_TRANSFORM);
 670             if (node.getClipNode() != null) {
 671                 node.renderClip(g);
 672             } else if (node.getEffectFilter() != null) {
 673                 node.renderEffect(g);
 674             } else {
 675                 node.renderContent(g);
 676             }
 677         }
 678     }
 679 
 680     /**
 681      * Render the node directly to the screen, in the case that the cached
 682      * image is unexpectedly null.  See RT-6428.
 683      */
 684     void impl_renderNodeToScreen(Object implGraphics) {
 685         Graphics g = (Graphics)implGraphics;
 686         if (node.getEffectFilter() != null) {
 687             node.renderEffect(g);
 688         } else {
 689             node.renderContent(g);
 690         }
 691     }
 692 
 693     /**
 694      * Render the cached image to the screen, translated by mxt, myt.
 695      */
 696     void impl_renderCacheToScreen(Object implGraphics, Filterable implImage,
 697                                   double mxt, double myt)
 698     {
 699         Graphics g = (Graphics)implGraphics;
 700 
 701         g.setTransform(screenXform.getMxx(),
 702                        screenXform.getMyx(),
 703                        screenXform.getMxy(),
 704                        screenXform.getMyy(),
 705                        mxt, myt);
 706         g.translate((float)cachedX, (float)cachedY);
 707         Texture cachedTex = ((PrDrawable)implImage).getTextureObject();
 708         Rectangle cachedBounds = cachedImageData.getUntransformedBounds();
 709         g.drawTexture(cachedTex, 0, 0,
 710                       cachedBounds.width, cachedBounds.height);
 711         // FYI: transform state is restored by the NGNode.render() method
 712     }
 713 
 714     /**
 715      * True if we can use scrolling optimization on this node.
 716      */
 717     boolean impl_scrollCacheCapable() {
 718         if (!(node instanceof NGGroup)) {
 719             return false;
 720         }
 721         List<NGNode> children = ((NGGroup)node).getChildren();
 722         if (children.size() != 1) {
 723             return false;
 724         }
 725         NGNode child = children.get(0);
 726         if (!child.getTransform().is2D()) {
 727             return false;
 728         }
 729 
 730         NGNode clip = node.getClipNode();
 731         if (clip == null || !clip.isRectClip(BaseTransform.IDENTITY_TRANSFORM, false)) {
 732             return false;
 733         }
 734         
 735         if (node instanceof NGRegion) {
 736             NGRegion region = (NGRegion) node;
 737             if (!region.getBorder().isEmpty()) {
 738                 return false;
 739             }
 740             final Background background = region.getBackground();
 741 
 742             if (!background.isEmpty()) {
 743                 if (!background.getImages().isEmpty()
 744                         || background.getFills().size() != 1) {
 745                     return false;
 746                 }
 747                 BackgroundFill fill = background.getFills().get(0);
 748                 javafx.scene.paint.Paint fillPaint = fill.getFill();
 749                 BaseBounds clipBounds = clip.getCompleteBounds(TEMP_BOUNDS, BaseTransform.IDENTITY_TRANSFORM);
 750 
 751                 return fillPaint.isOpaque() && fillPaint instanceof Color && fill.getInsets().equals(Insets.EMPTY)
 752                         && clipBounds.getMinX() == 0 && clipBounds.getMinY() == 0
 753                         && clipBounds.getMaxX() == region.getWidth() && clipBounds.getMaxY() == region.getHeight();
 754             }
 755         }
 756         
 757         return true;
 758     }
 759 
 760     /**
 761      * Moves a subregion of the cache, "scrolling" the cache by x/y Delta.
 762      * On of xDelta/yDelta must be zero. The rest of the pixels will be cleared.
 763      * @param cachedImageData cache
 764      * @param xDelta x-axis delta
 765      * @param yDelta y-axis delta
 766      */
 767     void impl_moveCacheBy(ImageData cachedImageData, double xDelta, double yDelta) {
 768         PrDrawable drawable = (PrDrawable) cachedImageData.getUntransformedImage();
 769         final Rectangle r = cachedImageData.getUntransformedBounds();
 770         int x = (int)Math.max(0, (-xDelta));
 771         int y = (int)Math.max(0, (-yDelta));
 772         int destX = (int)Math.max(0, (xDelta));
 773         int destY = (int) Math.max(0, yDelta);
 774         int w = r.width - (int) Math.abs(xDelta);
 775         int h = r.height - (int) Math.abs(yDelta);
 776 
 777         final Graphics g = drawable.createGraphics();
 778         if (tempTexture != null) {
 779             tempTexture.lock();
 780             if (tempTexture.isSurfaceLost()) {
 781                 tempTexture = null;
 782             }
 783         }
 784         if (tempTexture == null) {
 785             tempTexture = g.getResourceFactory().
 786                 createRTTexture(drawable.getPhysicalWidth(), drawable.getPhysicalHeight(),
 787                                 Texture.WrapMode.CLAMP_NOT_NEEDED);
 788         }
 789         final Graphics tempG = tempTexture.createGraphics();
 790         tempG.clear();
 791         tempG.drawTexture(drawable.getTextureObject(), 0, 0, w, h, x, y, x + w, y + h);
 792         tempG.sync();
 793 
 794         g.clear();
 795         g.drawTexture(tempTexture, destX, destY, destX + w, destY + h, 0, 0, w, h);
 796         tempTexture.unlock();
 797     }
 798 
 799     /**
 800      * Get the cache bounds.
 801      * @param bounds rectangle to store bounds to
 802      * @param xform transformation
 803      */
 804     Rectangle impl_getCacheBounds(Rectangle bounds, BaseTransform xform) {
 805         final BaseBounds b = node.getClippedBounds(TEMP_BOUNDS, xform);
 806         bounds.setBounds(b);
 807         return bounds;
 808     }
 809     
 810     BaseBounds computeDirtyBounds(BaseBounds region, BaseTransform tx, GeneralTransform3D pvTx) {
 811         // For now, we just use the computed dirty bounds of the Node and
 812         // round them out before the transforms.
 813         // Later, we could use the bounds of the cache
 814         // to compute the dirty region directly (and more accurately).
 815         // See RT-34928 for more details.
 816         if (!node.dirtyBounds.isEmpty()) {
 817             region = region.deriveWithNewBounds(node.dirtyBounds);
 818         } else {
 819             region = region.deriveWithNewBounds(node.transformedBounds);
 820         }
 821 
 822         if (!region.isEmpty()) {
 823             region.roundOut();
 824             region = node.computePadding(region);
 825             region = tx.transform(region, region);
 826             region = pvTx.transform(region, region);
 827         }
 828         return region;
 829     }
 830 }