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 }