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