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 }