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 }