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 javafx.scene.CacheHint; 29 import java.util.ArrayList; 30 import java.util.List; 31 import com.sun.glass.ui.Screen; 32 import com.sun.javafx.geom.BaseBounds; 33 import com.sun.javafx.geom.BoxBounds; 34 import com.sun.javafx.geom.DirtyRegionContainer; 35 import com.sun.javafx.geom.DirtyRegionPool; 36 import com.sun.javafx.geom.Point2D; 37 import com.sun.javafx.geom.RectBounds; 38 import com.sun.javafx.geom.Rectangle; 39 import com.sun.javafx.geom.transform.Affine3D; 40 import com.sun.javafx.geom.transform.BaseTransform; 41 import com.sun.javafx.geom.transform.GeneralTransform3D; 42 import com.sun.javafx.geom.transform.NoninvertibleTransformException; 43 import com.sun.prism.CompositeMode; 44 import com.sun.prism.Graphics; 45 import com.sun.prism.GraphicsPipeline; 46 import com.sun.prism.RTTexture; 47 import com.sun.prism.ReadbackGraphics; 48 import com.sun.prism.impl.PrismSettings; 49 import com.sun.scenario.effect.Blend; 50 import com.sun.scenario.effect.Effect; 51 import com.sun.scenario.effect.FilterContext; 52 import com.sun.scenario.effect.ImageData; 53 import com.sun.scenario.effect.impl.prism.PrDrawable; 54 import com.sun.scenario.effect.impl.prism.PrEffectHelper; 55 import com.sun.scenario.effect.impl.prism.PrFilterContext; 56 import com.sun.javafx.logging.PulseLogger; 57 import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; 58 59 /** 60 * NGNode is the abstract base class peer of Node, forming 61 * the basis for Prism and Scenario render graphs. 62 * <p> 63 * During synchronization, the FX scene graph will pass down to us 64 * the transform which takes us from local space to parent space, the 65 * content bounds (ie: geom bounds), and the transformed bounds 66 * (ie: boundsInParent), and the clippedBounds. The effect bounds have 67 * already been passed to the Effect peer (if there is one). 68 * <p> 69 * Whenever the transformedBounds of the NGNode are changed, we update 70 * the dirtyBounds, so that the next time we need to accumulate dirty 71 * regions, we will have the information we need to make sure we create 72 * an appropriate dirty region. 73 * <p> 74 * NGNode maintains a single "dirty" flag, which indicates that this 75 * node itself is dirty and must contribute to the dirty region. More 76 * specifically, it indicates that this node is now dirty with respect 77 * to the back buffer. Any rendering of the scene which will go on the 78 * back buffer will cause the dirty flag to be cleared, whereas a 79 * rendering of the scene which is for an intermediate image will not 80 * clear this dirty flag. 81 */ 82 public abstract class NGNode { 83 private final static GraphicsPipeline pipeline = 84 GraphicsPipeline.getPipeline(); 85 86 private final static Boolean effectsSupported = 87 (pipeline == null ? false : pipeline.isEffectSupported()); 88 89 public static enum DirtyFlag { 90 CLEAN, 91 // Means that the node is dirty, but only because of translation 92 DIRTY_BY_TRANSLATION, 93 DIRTY 94 } 95 96 /** 97 * Used for debug purposes. Set during sync. 98 */ 99 private String name; 100 101 /** 102 * Temporary bounds for use by this class or subclasses, designed to 103 * reduce the amount of garbage we generate. If we get to the point 104 * where we have multi-threaded rasterization, we might need to make 105 * this per-instance instead of static. 106 */ 107 private static final BoxBounds TEMP_BOUNDS = new BoxBounds(); 108 private static final RectBounds TEMP_RECT_BOUNDS = new RectBounds(); 109 protected static final Affine3D TEMP_TRANSFORM = new Affine3D(); 110 111 /** 112 * Statics for defining what the culling bits are. We use 2 bits to 113 * determine culling status 114 */ 115 static final int DIRTY_REGION_INTERSECTS_NODE_BOUNDS = 0x1; 116 static final int DIRTY_REGION_CONTAINS_NODE_BOUNDS = 0x2; 117 static final int DIRTY_REGION_CONTAINS_OR_INTERSECTS_NODE_BOUNDS = 118 DIRTY_REGION_INTERSECTS_NODE_BOUNDS | DIRTY_REGION_CONTAINS_NODE_BOUNDS; 119 120 /** 121 * The transform for this node. Although we are handed all the bounds 122 * during synchronization (including the transformed bounds), we still 123 * need the transform so that we can apply it to the clip and so forth 124 * while accumulating dirty regions and rendering. 125 */ 126 private BaseTransform transform = BaseTransform.IDENTITY_TRANSFORM; 127 128 /** 129 * The cached transformed bounds. This is never null, but is frequently set 130 * to be invalid whenever the bounds for the node have changed. These are 131 * "complete" bounds, that is, with transforms and effect and clip applied. 132 * Note that this is equivalent to boundsInParent in FX. 133 */ 134 protected BaseBounds transformedBounds = new RectBounds(); 135 136 /** 137 * The cached bounds. This is never null, but is frequently set to be 138 * invalid whenever the bounds for the node have changed. These are the 139 * "content" bounds, that is, without transforms or filters applied. 140 */ 141 protected BaseBounds contentBounds = new RectBounds(); 142 143 /** 144 * We keep a reference to the last transform bounds that were valid 145 * and known. We do this to significantly speed up the rendering of the 146 * scene by culling and clipping based on "dirty" regions, which are 147 * essentially the rectangle formed by the union of the dirtyBounds 148 * and the transformedBounds. 149 */ 150 BaseBounds dirtyBounds = new RectBounds(); 151 152 /** 153 * Whether the node is visible. We need to know about the visibility of 154 * the node so that we can determine whether to cull it out, and perform 155 * other such optimizations. 156 */ 157 private boolean visible = true; 158 159 /** 160 * Indicates that this NGNode is itself dirty and needs its full bounds 161 * included in the next repaint. This means it is dirty with respect to 162 * the back buffer. We don't bother differentiating between bounds dirty 163 * and visuals dirty because we can simply inspect the dirtyBounds to 164 * see if it is valid. If so, then bounds must be dirty. 165 */ 166 protected DirtyFlag dirty = DirtyFlag.DIRTY; 167 168 /** 169 * The parent of the node. In the case of a normal render graph node, 170 * this will be an NGGroup. However, if this node is being used as 171 * a clip node, then the parent is the node it is the clip for. 172 */ 173 private NGNode parent; 174 175 /** 176 * True if this node is a clip. This means the parent is clipped by this node. 177 */ 178 private boolean isClip; 179 180 /** 181 * The node used for specifying the clipping shape for this node. If null, 182 * then there is no clip. 183 */ 184 private NGNode clipNode; 185 186 /** 187 * The opacity of this node. 188 */ 189 private float opacity = 1f; 190 191 /** 192 * The blend mode that controls how the pixels of this node blend into 193 * the rest of the scene behind it. 194 */ 195 private Blend.Mode nodeBlendMode; 196 197 /** 198 * The depth test flag for this node. It is used when rendering if the window 199 * into which we are rendering has a depth buffer. 200 */ 201 private boolean depthTest = true; 202 203 /** 204 * A filter used when the node is cached. If null, then the node is not 205 * being cached. While in theory this could be created automatically by 206 * the implementation due to some form of heuristic, currently we 207 * only set this if the application has requested that the node be cached. 208 */ 209 private CacheFilter cacheFilter; 210 211 /** 212 * A filter used whenever an effect is placed on the node. Of course 213 * effects can form a kind of tree, such that this one effect might be 214 * an accumulation of several different effects. This will be null if 215 * there are no effects on the FX scene graph node. 216 */ 217 private EffectFilter effectFilter; 218 219 /** 220 * If this node is an NGGroup, then this flag will be used to indicate 221 * whether one or more of its children is dirty. While it would seem this 222 * flag should be on NGGroup, the code turns out to be a bit cleaner with 223 * this flag in the NGNode class. 224 */ 225 protected boolean childDirty = false; 226 227 /** 228 * How many children are going to be accumulated 229 */ 230 protected int dirtyChildrenAccumulated = 0; 231 232 /** 233 * Do not iterate over all children in group. Mark group as dirty 234 * when threshold was reached. 235 */ 236 protected final static int DIRTY_CHILDREN_ACCUMULATED_THRESHOLD = 12; 237 238 /** 239 * Marks position of this node in dirty regions. 240 */ 241 protected int cullingBits = 0x0; 242 private DirtyHint hint; 243 244 /** 245 * A cached representation of the opaque region for this node. This 246 * cached version needs to be recomputed whenever the opaque region becomes 247 * invalid, which includes local transform changes (translations included!). 248 */ 249 private RectBounds opaqueRegion = null; 250 251 /** 252 * To avoid object churn we keep opaqueRegion around, and just toggle this 253 * boolean to indicate whether we need to recompute the opaqueRegion. 254 */ 255 private boolean opaqueRegionInvalid = true; 256 257 /** 258 * Used for debug purposes. This field will keep track of which nodes were 259 * rendered as a result of different dirty regions. These correspond to the 260 * same positions as the cullingBits. So for example, if a node was rendered 261 * by dirty region 0, then painted will have the lowest bit set. If it 262 * was rendered by dirty region 3, then it would have the 3rd bit from the 263 * right set ( that is, 1 << 2) 264 */ 265 private int painted = 0; 266 267 protected NGNode() { } 268 269 /*************************************************************************** 270 * * 271 * Methods invoked during synchronization * 272 * * 273 **************************************************************************/ 274 275 /** 276 * Called by the FX scene graph to tell us whether we should be visible or not. 277 * @param value whether it is visible 278 */ 279 public void setVisible(boolean value) { 280 // If the visibility changes, we need to mark this node as being dirty. 281 // If this node is being cached, changing visibility should have no 282 // effect, since it doesn't affect the rendering of the content in 283 // any way. If we were to release the cached image, that might thwart 284 // the developer's attempt to improve performance for things that 285 // rapidly appear and disappear but which are expensive to render. 286 // Ancestors, of course, must still have their caches invalidated. 287 if (visible != value) { 288 this.visible = value; 289 markDirty(); 290 } 291 } 292 293 /** 294 * Called by the FX scene graph to tell us what our new content bounds are. 295 * @param bounds must not be null 296 */ 297 public void setContentBounds(BaseBounds bounds) { 298 // Note, there isn't anything to do here. We're dirty if geom or 299 // visuals or transformed bounds or effects or clip have changed. 300 // There's no point dealing with it here. 301 contentBounds = contentBounds.deriveWithNewBounds(bounds); 302 } 303 304 /** 305 * Called by the FX scene graph to tell us what our transformed bounds are. 306 * @param bounds must not be null 307 */ 308 public void setTransformedBounds(BaseBounds bounds, boolean byTransformChangeOnly) { 309 if (transformedBounds.equals(bounds)) { 310 // There has been no change, so ignore. It turns out this happens 311 // a lot, because when a leaf has dirty bounds, all parents also 312 // assume their bounds have changed, and only when they recompute 313 // their bounds do we discover otherwise. This check could happen 314 // on the FX side, however, then the FX side needs to cache the 315 // former content bounds at the time of the last sync or needs to 316 // be able to read state back from the NG side. Yuck. Just doing 317 // it here for now. 318 return; 319 } 320 // If the transformed bounds have changed, then we need to save off the 321 // transformed bounds into the dirty bounds, so that the resulting 322 // dirty region will be correct. If this node is cached, we DO NOT 323 // invalidate the cache. The cacheFilter will compare its cached 324 // transform to the accumulated transform to determine whether the 325 // cache needs to be regenerated. So we will not invalidate it here. 326 if (dirtyBounds.isEmpty()) { 327 dirtyBounds = dirtyBounds.deriveWithNewBounds(transformedBounds); 328 dirtyBounds = dirtyBounds.deriveWithUnion(bounds); 329 } else { 330 // TODO I think this is vestigial from Scenario and will never 331 // actually occur in real life... (RT-23956) 332 dirtyBounds = dirtyBounds.deriveWithUnion(transformedBounds); 333 } 334 transformedBounds = transformedBounds.deriveWithNewBounds(bounds); 335 if (hasVisuals() && !byTransformChangeOnly) { 336 markDirty(); 337 } 338 } 339 340 /** 341 * Called by the FX scene graph to tell us what our transform matrix is. 342 * @param tx must not be null 343 */ 344 public void setTransformMatrix(BaseTransform tx) { 345 if (transform.equals(tx)) { 346 return; 347 } 348 // If the transform matrix has changed, then we need to update it, 349 // and mark this node as dirty. If this node is cached, we DO NOT 350 // invalidate the cache. The cacheFilter will compare its cached 351 // transform to the accumulated transform to determine whether the 352 // cache needs to be regenerated. So we will not invalidate it here. 353 // This approach allows the cached image to be reused in situations 354 // where only the translation parameters of the accumulated transform 355 // are changing. The scene will still be marked dirty and cached 356 // images of any ancestors will be invalidated. 357 boolean useHint = false; 358 359 // If the parent is cached, try to check if the transformation is only a translation 360 if (parent != null && parent.cacheFilter != null && PrismSettings.scrollCacheOpt) { 361 if (hint == null) { 362 // If there's no hint created yet, this is the first setTransformMatrix 363 // call and we have nothing to compare to yet. 364 hint = new DirtyHint(); 365 } else { 366 if (transform.getMxx() == tx.getMxx() 367 && transform.getMxy() == tx.getMxy() 368 && transform.getMyy() == tx.getMyy() 369 && transform.getMyx() == tx.getMyx() 370 && transform.getMxz() == tx.getMxz() 371 && transform.getMyz() == tx.getMyz() 372 && transform.getMzx() == tx.getMzx() 373 && transform.getMzy() == tx.getMzy() 374 && transform.getMzz() == tx.getMzz() 375 && transform.getMzt() == tx.getMzt()) { 376 useHint = true; 377 hint.translateXDelta = tx.getMxt() - transform.getMxt(); 378 hint.translateYDelta = tx.getMyt() - transform.getMyt(); 379 } 380 } 381 } 382 383 transform = transform.deriveWithNewTransform(tx); 384 if (useHint) { 385 markDirtyByTranslation(); 386 } else { 387 markDirty(); 388 } 389 invalidateOpaqueRegion(); 390 } 391 392 /** 393 * Called by the FX scene graph whenever the clip node for this node changes. 394 * @param clipNode can be null if the clip node is being cleared 395 */ 396 public void setClipNode(NGNode clipNode) { 397 // Whenever the clipNode itself has changed (that is, the reference to 398 // the clipNode), we need to be sure to mark this node dirty and to 399 // invalidate the cache of this node (if there is one) and all parents. 400 if (clipNode != this.clipNode) { 401 // Clear the "parent" property of the clip node, if there was one 402 if (this.clipNode != null) this.clipNode.setParent(null); 403 // Make the "parent" property of the clip node point to this 404 if (clipNode != null) clipNode.setParent(this, true); 405 // Keep the reference to the new clip node 406 this.clipNode = clipNode; 407 // Mark this node dirty, invalidate its cache, and all parents. 408 visualsChanged(); 409 invalidateOpaqueRegion(); 410 } 411 } 412 413 /** 414 * Called by the FX scene graph whenever the opacity for the node changes. 415 * We create a special filter when the opacity is < 1. 416 * @param opacity A value between 0 and 1. 417 */ 418 public void setOpacity(float opacity) { 419 // Check the argument to make sure it is valid. 420 if (opacity < 0 || opacity > 1) { 421 throw new IllegalArgumentException("Internal Error: The opacity must be between 0 and 1"); 422 } 423 // If the opacity has changed, react. If this node is being cached, 424 // then we do not want to invalidate the cache due to an opacity 425 // change. However, as usual, all parent caches must be invalidated. 426 if (opacity != this.opacity) { 427 final float old = this.opacity; 428 this.opacity = opacity; 429 markDirty(); 430 // Even though the opacity has changed, for example from .5 to .6, 431 // we don't need to invalidate the opaque region unless it has toggled 432 // from 1 to !1, or from !1 to 1. 433 if (old < 1 && (opacity == 1 || opacity == 0) || opacity < 1 && (old == 1 || old == 0)) { 434 invalidateOpaqueRegion(); 435 } 436 } 437 } 438 439 /** 440 * Set by the FX scene graph. 441 * @param blendMode may be null to indicate "default" 442 */ 443 public void setNodeBlendMode(Blend.Mode blendMode) { 444 // The following code was a broken optimization that made an 445 // incorrect assumption about null meaning the same thing as 446 // SRC_OVER. In reality, null means "pass through blending 447 // from children" and SRC_OVER means "intercept blending of 448 // children, allow them to blend with each other, but pass 449 // their result on in a single SRC_OVER operation into the bg". 450 // For leaf nodes, those are mostly the same thing, but Regions 451 // and Groups might behave differently for the two modes. 452 // if (blendMode == Blend.Mode.SRC_OVER) { 453 // blendMode = null; 454 // } 455 456 // If the blend mode has changed, react. If this node is being cached, 457 // then we do not want to invalidate the cache due to a compositing 458 // change. However, as usual, all parent caches must be invalidated. 459 460 if (this.nodeBlendMode != blendMode) { 461 this.nodeBlendMode = blendMode; 462 markDirty(); 463 invalidateOpaqueRegion(); 464 } 465 } 466 467 /** 468 * Called by the FX scene graph whenever the derived depth test flag for 469 * the node changes. 470 * @param depthTest indicates whether to perform a depth test operation 471 * (if the window has a depth buffer). 472 */ 473 public void setDepthTest(boolean depthTest) { 474 // If the depth test flag has changed, react. 475 if (depthTest != this.depthTest) { 476 this.depthTest = depthTest; 477 // Mark this node dirty, invalidate its cache, and all parents. 478 visualsChanged(); 479 } 480 } 481 482 /** 483 * Called by the FX scene graph whenever "cached" or "cacheHint" changes. 484 * These hints provide a way for the developer to indicate whether they 485 * want this node to be cached as a raster, which can be quite a performance 486 * optimization in some cases (and lethal in others). 487 * @param cached specifies whether or not this node should be cached 488 * @param cacheHint never null, indicates some hint as to how to cache 489 */ 490 public void setCachedAsBitmap(boolean cached, CacheHint cacheHint) { 491 // Validate the arguments 492 if (cacheHint == null) { 493 throw new IllegalArgumentException("Internal Error: cacheHint must not be null"); 494 } 495 496 if (cached) { 497 if (cacheFilter == null) { 498 cacheFilter = new CacheFilter(this, cacheHint); 499 // We do not technically need to do a render pass here, but if 500 // we wait for the next render pass to cache it, then we will 501 // cache not the current visuals, but the visuals as defined 502 // by any transform changes that happen between now and then. 503 // Repainting now encourages the cached version to be as close 504 // as possible to the state of the node when the cache hint 505 // was set... 506 markDirty(); 507 } else { 508 if (!cacheFilter.matchesHint(cacheHint)) { 509 cacheFilter.setHint(cacheHint); 510 // Different hints may have different requirements of 511 // whether the cache is stale. We do not have enough info 512 // right here to evaluate that, but it will be determined 513 // naturally during a repaint cycle. 514 // If the new hint is more relaxed (QUALITY => SPEED for 515 // instance) then rendering should be quick. 516 // If the new hint is more restricted (SPEED => QUALITY) 517 // then we need to render to improve the results anyway. 518 markDirty(); 519 } 520 } 521 } else { 522 if (cacheFilter != null) { 523 cacheFilter.dispose(); 524 cacheFilter = null; 525 // A cache will often look worse than uncached rendering. It 526 // may look the same in some circumstances, and this may then 527 // be an unnecessary rendering pass, but we do not have enough 528 // information here to be able to optimize that when possible. 529 markDirty(); 530 } 531 } 532 } 533 534 /** 535 * Called by the FX scene graph to set the effect. 536 * @param effect the effect (can be null to clear it) 537 */ 538 public void setEffect(Effect effect) { 539 final Effect old = getEffect(); 540 // When effects are disabled, be sure to reset the effect filter 541 if (PrismSettings.disableEffects) { 542 effect = null; 543 } 544 545 // We only need to take action if the effect is different than what was 546 // set previously. There are four possibilities. Of these, #1 and #3 matter: 547 // 0. effectFilter == null, effect == null 548 // 1. effectFilter == null, effect != null 549 // 2. effectFilter != null, effectFilter.effect == effect 550 // 3. effectFilter != null, effectFilter.effect != effect 551 // In any case where the effect is changed, we must both invalidate 552 // the cache for this node (if there is one) and all parents, and mark 553 // this node as dirty. 554 if (effectFilter == null && effect != null) { 555 effectFilter = new EffectFilter(effect, this); 556 visualsChanged(); 557 } else if (effectFilter != null && effectFilter.getEffect() != effect) { 558 effectFilter.dispose(); 559 effectFilter = null; 560 if (effect != null) { 561 effectFilter = new EffectFilter(effect, this); 562 } 563 visualsChanged(); 564 } 565 566 // The only thing we do with the effect in #computeOpaqueRegion is to check 567 // whether the effect is null / not null. If the answer to these question has 568 // not changed from last time, then there is no need to recompute the opaque region. 569 if (old != effect) { 570 if (old == null || effect == null) { 571 invalidateOpaqueRegion(); 572 } 573 } 574 } 575 576 /** 577 * Called by the FX scene graph when an effect in the effect chain on the node 578 * changes internally. 579 */ 580 public void effectChanged() { 581 visualsChanged(); 582 } 583 584 /** 585 * Return true if contentBounds is purely a 2D bounds, ie. it is a 586 * RectBounds or its Z dimension is almost zero. 587 */ 588 public boolean isContentBounds2D() { 589 return (contentBounds.is2D() 590 || (Affine3D.almostZero(contentBounds.getMaxZ()) 591 && Affine3D.almostZero(contentBounds.getMinZ()))); 592 } 593 594 /*************************************************************************** 595 * * 596 * Hierarchy, visibility, and other such miscellaneous NGNode properties * 597 * * 598 **************************************************************************/ 599 600 /** 601 * Gets the parent of this node. The parent might be an NGGroup. However, 602 * if this node is a clip node on some other node, then the node on which 603 * it is set as the clip will be returned. That is, suppose some node A 604 * has a clip node B. The method B.getParent() will return A. 605 */ 606 public NGNode getParent() { return parent; } 607 608 /** 609 * Only called by this class, or by the NGGroup class. 610 */ 611 public void setParent(NGNode parent) { 612 setParent(parent, false); 613 } 614 615 private void setParent(NGNode parent, boolean isClip) { 616 this.parent = parent; 617 this.isClip = isClip; 618 } 619 620 /** 621 * Used for debug purposes. 622 */ 623 public final void setName(String value) { 624 this.name = value; 625 } 626 627 /** 628 * Used for debug purposes. 629 */ 630 public final String getName() { 631 return name; 632 } 633 634 protected final Effect getEffect() { return effectFilter == null ? null : effectFilter.getEffect(); } 635 636 /** 637 * Gets whether this node's visible property is set 638 */ 639 public boolean isVisible() { return visible; } 640 641 public final BaseTransform getTransform() { return transform; } 642 public final float getOpacity() { return opacity; } 643 public final Blend.Mode getNodeBlendMode() { return nodeBlendMode; } 644 public final boolean isDepthTest() { return depthTest; } 645 public final CacheFilter getCacheFilter() { return cacheFilter; } 646 public final EffectFilter getEffectFilter() { return effectFilter; } 647 public final NGNode getClipNode() { return clipNode; } 648 649 public BaseBounds getContentBounds(BaseBounds bounds, BaseTransform tx) { 650 if (tx.isTranslateOrIdentity()) { 651 bounds = bounds.deriveWithNewBounds(contentBounds); 652 if (!tx.isIdentity()) { 653 float translateX = (float) tx.getMxt(); 654 float translateY = (float) tx.getMyt(); 655 float translateZ = (float) tx.getMzt(); 656 bounds = bounds.deriveWithNewBounds( 657 bounds.getMinX() + translateX, 658 bounds.getMinY() + translateY, 659 bounds.getMinZ() + translateZ, 660 bounds.getMaxX() + translateX, 661 bounds.getMaxY() + translateY, 662 bounds.getMaxZ() + translateZ); 663 } 664 return bounds; 665 } else { 666 // This is a scale / rotate / skew transform. 667 // We have contentBounds cached throughout the entire tree. 668 // just walk down the tree and add everything up 669 return computeBounds(bounds, tx); 670 } 671 } 672 673 private BaseBounds computeBounds(BaseBounds bounds, BaseTransform tx) { 674 // TODO: This code almost worked, but it ignored the local to 675 // parent transforms on the nodes. The short fix is to disable 676 // this block and use the more general form below, but we need 677 // to revisit this and see if we can make it work more optimally. 678 // @see RT-12105 http://javafx-jira.kenai.com/browse/RT-12105 679 if (false && this instanceof NGGroup) { 680 List<NGNode> children = ((NGGroup)this).getChildren(); 681 BaseBounds tmp = TEMP_BOUNDS; 682 for (int i=0; i<children.size(); i++) { 683 float minX = bounds.getMinX(); 684 float minY = bounds.getMinY(); 685 float minZ = bounds.getMinZ(); 686 float maxX = bounds.getMaxX(); 687 float maxY = bounds.getMaxY(); 688 float maxZ = bounds.getMaxZ(); 689 NGNode child = children.get(i); 690 bounds = child.computeBounds(bounds, tx); 691 tmp = tmp.deriveWithNewBounds(minX, minY, minZ, maxX, maxY, maxZ); 692 bounds = bounds.deriveWithUnion(tmp); 693 } 694 return bounds; 695 } else { 696 bounds = bounds.deriveWithNewBounds(contentBounds); 697 return tx.transform(contentBounds, bounds); 698 } 699 } 700 701 /** 702 */ 703 public final BaseBounds getClippedBounds(BaseBounds bounds, BaseTransform tx) { 704 BaseBounds effectBounds = getEffectBounds(bounds, tx); 705 if (clipNode != null) { 706 // there is a clip in place, so we will save off the effect/content 707 // bounds (so as not to generate garbage) and will then get the 708 // bounds of the clip node and do an intersection of the two 709 float ex1 = effectBounds.getMinX(); 710 float ey1 = effectBounds.getMinY(); 711 float ez1 = effectBounds.getMinZ(); 712 float ex2 = effectBounds.getMaxX(); 713 float ey2 = effectBounds.getMaxY(); 714 float ez2 = effectBounds.getMaxZ(); 715 effectBounds = clipNode.getCompleteBounds(effectBounds, tx); 716 effectBounds.intersectWith(ex1, ey1, ez1, ex2, ey2, ez2); 717 } 718 return effectBounds; 719 } 720 721 public final BaseBounds getEffectBounds(BaseBounds bounds, BaseTransform tx) { 722 if (effectFilter != null) { 723 return effectFilter.getBounds(bounds, tx); 724 } else { 725 return getContentBounds(bounds, tx); 726 } 727 } 728 729 public final BaseBounds getCompleteBounds(BaseBounds bounds, BaseTransform tx) { 730 if (tx.isIdentity()) { 731 bounds = bounds.deriveWithNewBounds(transformedBounds); 732 return bounds; 733 } else if (transform.isIdentity()) { 734 return getClippedBounds(bounds, tx); 735 } else { 736 double mxx = tx.getMxx(); 737 double mxy = tx.getMxy(); 738 double mxz = tx.getMxz(); 739 double mxt = tx.getMxt(); 740 double myx = tx.getMyx(); 741 double myy = tx.getMyy(); 742 double myz = tx.getMyz(); 743 double myt = tx.getMyt(); 744 double mzx = tx.getMzx(); 745 double mzy = tx.getMzy(); 746 double mzz = tx.getMzz(); 747 double mzt = tx.getMzt(); 748 BaseTransform boundsTx = tx.deriveWithConcatenation(this.transform); 749 bounds = getClippedBounds(bounds, tx); 750 if (boundsTx == tx) { 751 tx.restoreTransform(mxx, mxy, mxz, mxt, 752 myx, myy, myz, myt, 753 mzx, mzy, mzz, mzt); 754 } 755 return bounds; 756 } 757 } 758 759 /*************************************************************************** 760 * * 761 * Dirty States * 762 * * 763 **************************************************************************/ 764 765 /** 766 * Invoked by subclasses whenever some change to the geometry or visuals 767 * has occurred. This will mark the node as dirty and invalidate the cache. 768 */ 769 protected void visualsChanged() { 770 invalidateCache(); 771 markDirty(); 772 } 773 774 protected void geometryChanged() { 775 invalidateCache(); 776 invalidateOpaqueRegion(); 777 if (hasVisuals()) { 778 markDirty(); 779 } 780 } 781 782 /** 783 * Makes this node dirty, meaning that it needs to be included in the 784 * next repaint to the back buffer, and its bounds should be included 785 * in the dirty region. This flag means that this node itself is dirty. 786 * In contrast, the childDirty flag indicates that a child of the node 787 * (maybe a distant child) is dirty. This method does not invalidate the 788 * cache of this node. However, it ends up walking up the tree marking 789 * all parents as having a dirty child and also invalidating their caches. 790 * This method has no effect if the node is already dirty. 791 */ 792 public final void markDirty() { 793 if (dirty != DirtyFlag.DIRTY) { 794 dirty = DirtyFlag.DIRTY; 795 markTreeDirty(); 796 } 797 } 798 799 /** 800 * Mark the node as DIRTY_BY_TRANSLATION. This will call special cache invalidation 801 */ 802 private void markDirtyByTranslation() { 803 if (dirty == DirtyFlag.CLEAN) { 804 if (parent != null && parent.dirty == DirtyFlag.CLEAN && !parent.childDirty) { 805 dirty = DirtyFlag.DIRTY_BY_TRANSLATION; 806 parent.childDirty = true; 807 parent.dirtyChildrenAccumulated++; 808 parent.invalidateCacheByTranslation(hint); 809 parent.markTreeDirty(); 810 } else { 811 markDirty(); 812 } 813 } 814 } 815 816 //Mark tree dirty, but make sure this node's 817 // dirtyChildrenAccumulated has not been incremented. 818 // Useful when a markTree is called on a node that's not 819 // the dirty source of change, e.g. group knows it has new child 820 // or one of it's child has been removed 821 protected final void markTreeDirtyNoIncrement() { 822 if (parent != null && (!parent.childDirty || dirty == DirtyFlag.DIRTY_BY_TRANSLATION)) { 823 markTreeDirty(); 824 } 825 } 826 827 /** 828 * Notifies the parent (whether an NGGroup or just a NGNode) that 829 * a child has become dirty. This walk will continue all the way up 830 * to the root of the tree. If a node is encountered which is already 831 * dirty, or which already has childDirty set, then this loop will 832 * terminate (ie: there is no point going further so we might as well 833 * just bail). This method ends up invalidating the cache of each 834 * parent up the tree. Since it is possible for a node to already 835 * have its dirty bit set, but not have its cache invalidated, this 836 * method is careful to make sure the first parent it encounters 837 * which is already marked dirty still has its cache invalidated. If 838 * this turns out to be expensive due to high occurrence, we can add 839 * a quick "invalidated" flag to every node (at the cost of yet 840 * another bit). 841 */ 842 protected final void markTreeDirty() { 843 NGNode p = parent; 844 boolean atClip = isClip; 845 boolean byTranslation = dirty == DirtyFlag.DIRTY_BY_TRANSLATION; 846 while (p != null && p.dirty != DirtyFlag.DIRTY && (!p.childDirty || atClip || byTranslation)) { 847 if (atClip) { 848 p.dirty = DirtyFlag.DIRTY; 849 } else if (!byTranslation) { 850 p.childDirty = true; 851 p.dirtyChildrenAccumulated++; 852 } 853 p.invalidateCache(); 854 atClip = p.isClip; 855 byTranslation = p.dirty == DirtyFlag.DIRTY_BY_TRANSLATION; 856 p = p.parent; 857 } 858 // if we stopped on a parent that already has dirty children, increase it's 859 // dirty children count. 860 // Note that when incrementDirty is false, we don't increment in this case. 861 if (p != null && p.dirty == DirtyFlag.CLEAN && !atClip && !byTranslation) { 862 p.dirtyChildrenAccumulated++; 863 } 864 // Must make sure this happens. In some cases, a parent might 865 // already be marked dirty (for example, its opacity may have 866 // changed) but its cache has not been made invalid. This call 867 // will make sure it is invalidated in that case 868 if (p != null) p.invalidateCache(); 869 } 870 871 /** 872 * Gets whether this SGNode is clean. This will return true only if 873 * this node and any / all child nodes are clean. 874 */ 875 public final boolean isClean() { 876 return dirty == DirtyFlag.CLEAN && !childDirty; 877 } 878 879 /** 880 * Clears the dirty flag. This should only happen during rendering. 881 */ 882 protected void clearDirty() { 883 dirty = DirtyFlag.CLEAN; 884 childDirty = false; 885 dirtyBounds.makeEmpty(); 886 dirtyChildrenAccumulated = 0; 887 } 888 889 /** 890 * Walks down the tree clearing the "painted" bits for each node. This is only 891 * called if we're drawing dirty rectangles or overdraw rectangles. 892 */ 893 public void clearPainted() { 894 painted = 0; 895 if (this instanceof NGGroup) { 896 List<NGNode> children = ((NGGroup)this).getChildren(); 897 for (int i=0; i<children.size(); i++) { 898 children.get(i).clearPainted(); 899 } 900 } 901 } 902 903 public void clearDirtyTree() { 904 clearDirty(); 905 if (getClipNode() != null) { 906 getClipNode().clearDirtyTree(); 907 } 908 if (this instanceof NGGroup) { 909 List<NGNode> children = ((NGGroup) this).getChildren(); 910 for (int i = 0; i < children.size(); ++i) { 911 NGNode child = children.get(i); 912 if (child.dirty != DirtyFlag.CLEAN || child.childDirty) { 913 child.clearDirtyTree(); 914 } 915 } 916 } 917 } 918 919 /** 920 * Invalidates the cache, if it is in use. There are several operations 921 * which need to cause the cached raster to become invalid so that a 922 * subsequent render operation will result in the cached image being 923 * reconstructed. 924 */ 925 protected final void invalidateCache() { 926 if (cacheFilter != null) { 927 cacheFilter.invalidate(); 928 } 929 } 930 931 /** 932 * Mark the cache as invalid due to a translation of a child. The cache filter 933 * might use this information for optimizations. 934 */ 935 protected final void invalidateCacheByTranslation(DirtyHint hint) { 936 if (cacheFilter != null) { 937 cacheFilter.invalidateByTranslation(hint.translateXDelta, hint.translateYDelta); 938 } 939 } 940 941 /*************************************************************************** 942 * * 943 * Dirty Regions * 944 * * 945 * Need to add documentation about dirty regions and how they work. One * 946 * thing to be aware of is that during the dirty region accumulation phase * 947 * we use precise floating point values, but during * 948 * * 949 **************************************************************************/ 950 951 /** 952 * Accumulates and returns the dirty regions in transformed coordinates for 953 * this node. This method is designed such that a single downward traversal 954 * of the tree is sufficient to update the dirty regions. 955 * <p> 956 * This method only accumulates dirty regions for parts of the tree which lie 957 * inside the clip since there is no point in accumulating dirty regions which 958 * lie outside the clip. The returned dirty regions bounds the same object 959 * as that passed into the function. The returned dirty regions bounds will 960 * always be adjusted such that they do not extend beyond the clip. 961 * <p> 962 * The given transform is the accumulated transform up to but not including the 963 * transform of this node. 964 * 965 * @param clip must not be null, the clip in scene coordinates, supplied by the 966 * rendering system. At most, this is usually the bounds of the window's 967 * content area, however it might be smaller. 968 * @param dirtyRegionTemp must not be null, the dirty region in scene coordinates. 969 * When this method is initially invoked by the rendering system, the 970 * dirtyRegion should be marked as invalid. 971 * @param dirtyRegionContainer must not be null, the container of dirty regions in scene 972 * coordinates. 973 * @param tx must not be null, the accumulated transform up to but not 974 * including this node's transform. When this method concludes, it must 975 * restore this transform if it was changed within the function. 976 * @param pvTx must not be null, it's the perspective transform of the current 977 * perspective camera or identity transform if parallel camera is used. 978 * @return The dirty region container. If the returned value is null, then that means 979 * the clip should be used as the dirty region. This is a special 980 * case indicating that there is no more need to walk the tree but 981 * we can take a shortcut. Note that returning null is *always* 982 * safe. Returning something other than null is simply an 983 * optimization for cases where the dirty region is substantially 984 * smaller than the clip. 985 * TODO: Only made non-final for the sake of testing (see javafx-sg-prism tests) (RT-23957) 986 */ 987 public /*final*/ int accumulateDirtyRegions(final RectBounds clip, 988 final RectBounds dirtyRegionTemp, 989 DirtyRegionPool regionPool, 990 final DirtyRegionContainer dirtyRegionContainer, 991 final BaseTransform tx, 992 final GeneralTransform3D pvTx) 993 { 994 // This is the main entry point, make sure to check these inputs for validity 995 if (clip == null || dirtyRegionTemp == null || regionPool == null || dirtyRegionContainer == null || 996 tx == null || pvTx == null) throw new NullPointerException(); 997 998 // Even though a node with 0 visibility or 0 opacity doesn't get 999 // rendered, it may contribute to the dirty bounds, for example, if it 1000 // WAS visible or if it HAD an opacity > 0 last time we rendered then 1001 // we must honor its dirty region. We have front-loaded this work so 1002 // that we don't mark nodes as having dirty flags or dirtyBounds if 1003 // they shouldn't contribute to the dirty region. So we can simply 1004 // treat all nodes, regardless of their opacity or visibility, as 1005 // though their dirty regions matter. They do. 1006 1007 // If this node is clean then we can simply return the dirty region as 1008 // there is no need to walk any further down this branch of the tree. 1009 // The node is "clean" if neither it, nor its children, are dirty. 1010 if (dirty == DirtyFlag.CLEAN && !childDirty) { 1011 return DirtyRegionContainer.DTR_OK; 1012 } 1013 1014 // We simply collect this nodes dirty region if it has its dirty flag 1015 // set, regardless of whether it is a group or not. However, if this 1016 // node is not dirty, then we can ask the accumulateGroupDirtyRegion 1017 // method to collect the dirty regions of the children. 1018 if (dirty != DirtyFlag.CLEAN) { 1019 return accumulateNodeDirtyRegion(clip, dirtyRegionTemp, dirtyRegionContainer, tx, pvTx); 1020 } else { 1021 assert childDirty; // this must be true by this point 1022 return accumulateGroupDirtyRegion(clip, dirtyRegionTemp, regionPool, 1023 dirtyRegionContainer, tx, pvTx); 1024 } 1025 } 1026 1027 /** 1028 * Accumulates the dirty region of a node. 1029 * TODO: Only made non-final for the sake of testing (see javafx-sg-prism tests) (RT-23957) 1030 */ 1031 int accumulateNodeDirtyRegion(final RectBounds clip, 1032 final RectBounds dirtyRegionTemp, 1033 final DirtyRegionContainer dirtyRegionContainer, 1034 final BaseTransform tx, 1035 final GeneralTransform3D pvTx) { 1036 1037 // Get the dirty bounds of this specific node in scene coordinates 1038 final BaseBounds bb = computeDirtyRegion(dirtyRegionTemp, tx, pvTx); 1039 1040 // Note: dirtyRegion is strictly a 2D operation. We simply need the largest 1041 // rectangular bounds of bb. Hence the Z-axis projection of bb; taking 1042 // minX, minY, maxX and maxY values from this point on. Also, in many cases 1043 // bb == dirtyRegionTemp. In fact, the only time this won't be true is if 1044 // there is (or was) a perspective transform involved on this node. 1045 if (bb != dirtyRegionTemp) { 1046 bb.flattenInto(dirtyRegionTemp); 1047 } 1048 1049 // If my dirty region is empty, or if it doesn't intersect with the 1050 // clip, then we can simply return since this node's dirty region is 1051 // not helpful 1052 if (dirtyRegionTemp.isEmpty() || clip.disjoint(dirtyRegionTemp)) { 1053 return DirtyRegionContainer.DTR_OK; 1054 } 1055 1056 // If the clip is completely contained within the dirty region (including 1057 // if they are equal) then we return DTR_CONTAINS_CLIP 1058 if (dirtyRegionTemp.contains(clip)) { 1059 return DirtyRegionContainer.DTR_CONTAINS_CLIP; 1060 } 1061 1062 // The only overhead in calling intersectWith, and contains (above) is the repeated checking 1063 // if the isEmpty state. But the code is cleaner and less error prone. 1064 dirtyRegionTemp.intersectWith(clip); 1065 1066 // Add the dirty region to the container 1067 dirtyRegionContainer.addDirtyRegion(dirtyRegionTemp); 1068 1069 return DirtyRegionContainer.DTR_OK; 1070 } 1071 1072 /** 1073 * Accumulates the dirty region of an NGGroup. This is implemented here as opposed to 1074 * using polymorphism because we wanted to centralize all of the dirty region 1075 * management code in one place, rather than having it spread between Prism, 1076 * Scenario, and any other future toolkits. 1077 * TODO: Only made non-final for the sake of testing (see javafx-sg-prism tests) (RT-23957) 1078 */ 1079 int accumulateGroupDirtyRegion(final RectBounds clip, 1080 final RectBounds dirtyRegionTemp, 1081 final DirtyRegionPool regionPool, 1082 DirtyRegionContainer dirtyRegionContainer, 1083 final BaseTransform tx, 1084 final GeneralTransform3D pvTx) { 1085 // We should have only made it to this point if this node has a dirty 1086 // child. If this node itself is dirty, this method never would get called. 1087 // If this node was not dirty and had no dirty children, then this 1088 // method never should have been called. So at this point, the following 1089 // assertions should be correct. 1090 assert childDirty; 1091 assert dirty == DirtyFlag.CLEAN; 1092 1093 int status = DirtyRegionContainer.DTR_OK; 1094 1095 if (dirtyChildrenAccumulated > DIRTY_CHILDREN_ACCUMULATED_THRESHOLD) { 1096 status = accumulateNodeDirtyRegion(clip, dirtyRegionTemp, dirtyRegionContainer, tx, pvTx); 1097 return status; 1098 } 1099 1100 // If we got here, then we are following a "bread crumb" trail down to 1101 // some child (perhaps distant) which is dirty. So we need to iterate 1102 // over all the children and accumulate their dirty regions. Before doing 1103 // so we, will save off the transform state and restore it after having 1104 // called all the children. 1105 double mxx = tx.getMxx(); 1106 double mxy = tx.getMxy(); 1107 double mxz = tx.getMxz(); 1108 double mxt = tx.getMxt(); 1109 1110 double myx = tx.getMyx(); 1111 double myy = tx.getMyy(); 1112 double myz = tx.getMyz(); 1113 double myt = tx.getMyt(); 1114 1115 double mzx = tx.getMzx(); 1116 double mzy = tx.getMzy(); 1117 double mzz = tx.getMzz(); 1118 double mzt = tx.getMzt(); 1119 BaseTransform renderTx = tx; 1120 if (this.transform != null) renderTx = renderTx.deriveWithConcatenation(this.transform); 1121 1122 // If this group node has a clip, then we will perform some special 1123 // logic which will cause the dirty region accumulation loops to run 1124 // faster. We already have a system whereby if a node determines that 1125 // its dirty region exceeds that of the clip, it simply returns null, 1126 // short circuiting the accumulation process. We extend that logic 1127 // here by also taking into account the clipNode on the group. If 1128 // there is a clip node, then we will union the bounds of the clip 1129 // node (in boundsInScene space) with the current clip and pass this 1130 // new clip down to the children. If they determine that their dirty 1131 // regions exceed the bounds of this new clip, then they will return 1132 // null. We'll catch that here, and use that information to know that 1133 // we ought to simply accumulate the bounds of this group as if it 1134 // were dirty. This process will do all the other optimizations we 1135 // already have in place for getting the normal dirty region. 1136 RectBounds myClip = clip; 1137 //Save current dirty region so we can fast-reset to (something like) the last state 1138 //and possibly save a few intersects() calls 1139 1140 DirtyRegionContainer originalDirtyRegion = null; 1141 BaseTransform originalRenderTx = null; 1142 if (effectFilter != null) { 1143 try { 1144 myClip = new RectBounds(); 1145 BaseBounds myClipBaseBounds = renderTx.inverseTransform(clip, TEMP_BOUNDS); 1146 myClipBaseBounds.flattenInto(myClip); 1147 } catch (NoninvertibleTransformException ex) { 1148 return DirtyRegionContainer.DTR_OK; 1149 } 1150 1151 originalRenderTx = renderTx; 1152 renderTx = BaseTransform.IDENTITY_TRANSFORM; 1153 originalDirtyRegion = dirtyRegionContainer; 1154 dirtyRegionContainer = regionPool.checkOut(); 1155 } else if (clipNode != null) { 1156 originalDirtyRegion = dirtyRegionContainer; 1157 myClip = new RectBounds(); 1158 BaseBounds clipBounds = clipNode.getCompleteBounds(myClip, renderTx); 1159 pvTx.transform(clipBounds, clipBounds); 1160 clipBounds.flattenInto(myClip); 1161 myClip.intersectWith(clip); 1162 dirtyRegionContainer = regionPool.checkOut(); 1163 } 1164 1165 1166 //Accumulate also removed children to dirty region. 1167 List<NGNode> removed = ((NGGroup) this).getRemovedChildren(); 1168 if (removed != null) { 1169 NGNode removedChild; 1170 for (int i = removed.size() - 1; i >= 0; --i) { 1171 removedChild = removed.get(i); 1172 removedChild.dirty = DirtyFlag.DIRTY; 1173 status = removedChild.accumulateDirtyRegions(myClip, 1174 dirtyRegionTemp,regionPool, dirtyRegionContainer, renderTx, pvTx); 1175 if (status == DirtyRegionContainer.DTR_CONTAINS_CLIP) { 1176 break; 1177 } 1178 } 1179 } 1180 1181 List<NGNode> children = ((NGGroup) this).getChildren(); 1182 int num = children.size(); 1183 for (int i=0; i<num && status == DirtyRegionContainer.DTR_OK; i++) { 1184 NGNode child = children.get(i); 1185 // The child will check the dirty bits itself. If we tested it here 1186 // (as we used to), we are just doing the check twice. True, it might 1187 // mean fewer method calls, but hotspot will probably inline this all 1188 // anyway, and doing the check in one place is less error prone. 1189 status = child.accumulateDirtyRegions(myClip, dirtyRegionTemp, regionPool, 1190 dirtyRegionContainer, renderTx, pvTx); 1191 if (status == DirtyRegionContainer.DTR_CONTAINS_CLIP) { 1192 break; 1193 } 1194 } 1195 1196 if (effectFilter != null && status == DirtyRegionContainer.DTR_OK) { 1197 //apply effect on effect dirty regions 1198 applyEffect(effectFilter, dirtyRegionContainer, regionPool); 1199 1200 if (clipNode != null) { 1201 myClip = new RectBounds(); 1202 BaseBounds clipBounds = clipNode.getCompleteBounds(myClip, renderTx); 1203 applyClip(clipBounds, dirtyRegionContainer); 1204 } 1205 1206 //apply transform on effect dirty regions 1207 applyTransform(originalRenderTx, dirtyRegionContainer); 1208 renderTx = originalRenderTx; 1209 1210 originalDirtyRegion.merge(dirtyRegionContainer); 1211 regionPool.checkIn(dirtyRegionContainer); 1212 } 1213 1214 // If the process of applying the transform caused renderTx to not equal 1215 // tx, then there is no point restoring it since it will be a different 1216 // reference and will therefore be gc'd. 1217 if (renderTx == tx) { 1218 tx.restoreTransform(mxx, mxy, mxz, mxt, myx, myy, myz, myt, mzx, mzy, mzz, mzt); 1219 } 1220 1221 // If the dirty region is null and there is a clip node specified, then what 1222 // happened is that the dirty region of content within this group exceeded 1223 // the clip of this group, and thus, we should accumulate the bounds of 1224 // this group into the dirty region. If the bounds of the group exceeds 1225 // the bounds of the dirty region, then we end up returning null in the 1226 // end. But the implementation of accumulateNodeDirtyRegion handles this. 1227 if (clipNode != null && effectFilter == null) { 1228 if (status == DirtyRegionContainer.DTR_CONTAINS_CLIP) { 1229 status = accumulateNodeDirtyRegion(clip, dirtyRegionTemp, originalDirtyRegion, tx, pvTx); 1230 } else { 1231 originalDirtyRegion.merge(dirtyRegionContainer); 1232 } 1233 regionPool.checkIn(dirtyRegionContainer); 1234 } 1235 return status; 1236 } 1237 1238 /** 1239 * Computes the dirty region for this Node. The specified region is in 1240 * scene coordinates. The specified tx can be used to convert local bounds 1241 * to scene bounds (it includes everything up to but not including my own 1242 * transform). 1243 * 1244 * @param dirtyRegionTemp A temporary RectBounds that this method can use for scratch. 1245 * In the case that no perspective transform occurs, it is best if 1246 * the returned BaseBounds is this instance. 1247 * @param tx Any transform that needs to be applied 1248 * @param pvTx must not be null, it's the perspective transform of the current 1249 * perspective camera or identity transform if parallel camera is used. 1250 */ 1251 private BaseBounds computeDirtyRegion(final RectBounds dirtyRegionTemp, 1252 final BaseTransform tx, 1253 final GeneralTransform3D pvTx) 1254 { 1255 if (cacheFilter != null) { 1256 return cacheFilter.computeDirtyBounds(dirtyRegionTemp, tx, pvTx); 1257 } 1258 // The passed in region is a scratch object that exists for me to use, 1259 // such that I don't have to create a temporary object. So I just 1260 // hijack it right here to start with. Note that any of the calls 1261 // in computeDirtyRegion might end up changing the region instance 1262 // from dirtyRegionTemp (which is a RectBounds) to a BoxBounds if any 1263 // of the other bounds / transforms involve a perspective transformation. 1264 BaseBounds region = dirtyRegionTemp; 1265 if (!dirtyBounds.isEmpty()) { 1266 region = region.deriveWithNewBounds(dirtyBounds); 1267 } else { 1268 // If dirtyBounds is empty, then we will simply set the bounds to 1269 // be the same as the transformedBounds (since that means the bounds 1270 // haven't changed and right now we don't support dirty sub regions 1271 // for generic nodes). This can happen if, for example, this is 1272 // a group with a clip and the dirty area of child nodes within 1273 // the group exceeds the bounds of the clip on the group. Just trust me. 1274 region = region.deriveWithNewBounds(transformedBounds); 1275 } 1276 1277 // We shouldn't do anything with empty region, as we may accidentally make 1278 // it non empty or turn it into some nonsense (like (-1,-1,0,0) ) 1279 if (!region.isEmpty()) { 1280 // Now that we have the dirty region, we will simply apply the tx 1281 // to it (after slightly padding it for good luck) to get the scene 1282 // coordinates for this. 1283 region = computePadding(region); 1284 region = tx.transform(region, region); 1285 region = pvTx.transform(region, region); 1286 } 1287 return region; 1288 } 1289 1290 /** 1291 * LCD Text creates some painful situations where, due to the LCD text 1292 * algorithm, we end up with some pixels touched that are normally outside 1293 * the bounds. To compensate, we need a hook for NGText to add padding. 1294 */ 1295 protected BaseBounds computePadding(BaseBounds region) { 1296 return region; 1297 } 1298 1299 /** 1300 * Marks if the node has some visuals and that the bounds change 1301 * should be taken into account when using the dirty region. 1302 * This will be false for NGGroup (but not for NGRegion) 1303 * @return true if the node has some visuals 1304 */ 1305 protected boolean hasVisuals() { 1306 return true; 1307 } 1308 1309 /*************************************************************************** 1310 * * 1311 * Culling * 1312 * * 1313 **************************************************************************/ 1314 1315 /** 1316 * Culling support for multiple dirty regions. 1317 * Set culling bits for the whole graph. 1318 * @param drc Array of dirty regions. Cannot be null. 1319 * @param tx The transform for this render operation. Cannot be null. 1320 * @param pvTx Perspective camera transformation. Cannot be null. 1321 */ 1322 public final void doPreCulling(DirtyRegionContainer drc, BaseTransform tx, GeneralTransform3D pvTx) { 1323 if (drc == null || tx == null || pvTx == null) throw new NullPointerException(); 1324 markCullRegions(drc, -1, tx, pvTx); 1325 } 1326 1327 /** 1328 * Marks placement of the node in dirty region encoded into 2 bit flag: 1329 * 00 - node outside dirty region 1330 * 01 - node intersecting dirty region 1331 * 11 - node completely within dirty region 1332 * 1333 * 32 bits = 15 regions max. * 2 bit each. The first two bits are not used 1334 * because we have a special use case for -1, so they should only be set if 1335 * in that case. 1336 * 1337 * @param drc The array of dirty regions. 1338 * @param cullingRegionsBitsOfParent culling bits of parent. -1 if there's no parent. 1339 * @param tx The transform for this render operation. Cannot be null. 1340 * @param pvTx Perspective camera transform. Cannot be null. 1341 */ 1342 void markCullRegions( 1343 DirtyRegionContainer drc, 1344 int cullingRegionsBitsOfParent, 1345 BaseTransform tx, 1346 GeneralTransform3D pvTx) { 1347 1348 // Spent a long time tracking down how cullingRegionsBitsOfParent works. Note that it is 1349 // not just the parent's bits, but also -1 in the case of the "root", where the root is 1350 // either the actual root, or the root of a sub-render operation such as occurs with 1351 // render-to-texture for effects! 1352 1353 if (tx.isIdentity()) { 1354 TEMP_BOUNDS.deriveWithNewBounds(transformedBounds); 1355 } else { 1356 tx.transform(transformedBounds, TEMP_BOUNDS); 1357 } 1358 1359 if (!pvTx.isIdentity()) { 1360 pvTx.transform(TEMP_BOUNDS, TEMP_BOUNDS); 1361 } 1362 1363 TEMP_BOUNDS.flattenInto(TEMP_RECT_BOUNDS); 1364 1365 cullingBits = 0; 1366 RectBounds region; 1367 int mask = 0x1; // Check only for intersections 1368 for(int i = 0; i < drc.size(); i++) { 1369 region = drc.getDirtyRegion(i); 1370 if (region == null || region.isEmpty()) { 1371 break; 1372 } 1373 // For each dirty region, we will check to see if this child 1374 // intersects with the dirty region and whether it contains the 1375 // dirty region. Note however, that we only care to mark those 1376 // child nodes which are inside a group that intersects. We don't 1377 // care about marking child nodes which are within a parent which 1378 // is wholly contained within the dirty region. 1379 if ((cullingRegionsBitsOfParent == -1 || (cullingRegionsBitsOfParent & mask) != 0) && 1380 region.intersects(TEMP_RECT_BOUNDS)) { 1381 int b = DIRTY_REGION_INTERSECTS_NODE_BOUNDS; 1382 if (region.contains(TEMP_RECT_BOUNDS)) { 1383 b = DIRTY_REGION_CONTAINS_NODE_BOUNDS; 1384 } 1385 cullingBits = cullingBits | (b << (2 * i)); 1386 } 1387 mask = mask << 2; 1388 }//for 1389 1390 // If we are going to cull a node/group that's dirty, 1391 // make sure it's dirty flags are properly cleared. 1392 if (cullingBits == 0 && (dirty != DirtyFlag.CLEAN || childDirty)) { 1393 clearDirtyTree(); 1394 } 1395 1396 // System.out.printf("%s bits: %s bounds: %s\n", 1397 // this, Integer.toBinaryString(cullingBits), TEMP_RECT_BOUNDS); 1398 } 1399 1400 /** 1401 * Fills the given StringBuilder with text representing the structure of the NG graph insofar as dirty 1402 * opts is concerned. Used for debug purposes. This is typically called on the root node. The List of 1403 * roots is the list of dirty roots as determined by successive calls to getRenderRoot for each dirty 1404 * region. The output will be prefixed with a key indicating how to interpret the printout. 1405 * 1406 * @param s A StringBuilder to fill with the output. 1407 * @param roots The list of render roots (may be empty, must not be null). 1408 */ 1409 public final void printDirtyOpts(StringBuilder s, List<NGNode> roots) { 1410 s.append("\n*=Render Root\n"); 1411 s.append("d=Dirty\n"); 1412 s.append("dt=Dirty By Translation\n"); 1413 s.append("i=Dirty Region Intersects the NGNode\n"); 1414 s.append("c=Dirty Region Contains the NGNode\n"); 1415 s.append("ef=Effect Filter\n"); 1416 s.append("cf=Cache Filter\n"); 1417 s.append("cl=This node is a clip node\n"); 1418 s.append("b=Blend mode is set\n"); 1419 s.append("or=Opaque Region\n"); 1420 printDirtyOpts(s, this, BaseTransform.IDENTITY_TRANSFORM, "", roots); 1421 } 1422 1423 /** 1424 * Used for debug purposes. Recursively visits all NGNodes and prints those that are possibly part of 1425 * the render operation and annotates each node. 1426 * 1427 * @param s The String builder 1428 * @param node The node that we're printing out information about 1429 * @param tx The transform 1430 * @param prefix Some prefix to put in front of the node output (mostly spacing) 1431 * @param roots The different dirty roots, if any. 1432 */ 1433 private final void printDirtyOpts(StringBuilder s, NGNode node, BaseTransform tx, String prefix, List<NGNode> roots) { 1434 if (!node.isVisible() || node.getOpacity() == 0) return; 1435 1436 BaseTransform copy = tx.copy(); 1437 copy = copy.deriveWithConcatenation(node.getTransform()); 1438 List<String> stuff = new ArrayList<>(); 1439 for (int i=0; i<roots.size(); i++) { 1440 NGNode root = roots.get(i); 1441 if (node == root) stuff.add("*" + i); 1442 } 1443 1444 if (node.dirty != NGNode.DirtyFlag.CLEAN) { 1445 stuff.add(node.dirty == NGNode.DirtyFlag.DIRTY ? "d" : "dt"); 1446 } 1447 1448 if (node.cullingBits != 0) { 1449 int mask = 0x11; 1450 for (int i=0; i<15; i++) { 1451 int bits = node.cullingBits & mask; 1452 if (bits != 0) { 1453 stuff.add(bits == 1 ? "i" + i : bits == 0 ? "c" + i : "ci" + i); 1454 } 1455 mask = mask << 2; 1456 } 1457 } 1458 1459 if (node.effectFilter != null) stuff.add("ef"); 1460 if (node.cacheFilter != null) stuff.add("cf"); 1461 if (node.nodeBlendMode != null) stuff.add("b"); 1462 1463 RectBounds opaqueRegion = node.getOpaqueRegion(); 1464 if (opaqueRegion != null) { 1465 RectBounds or = new RectBounds(); 1466 copy.transform(opaqueRegion, or); 1467 stuff.add("or=" + or.getMinX() + ", " + or.getMinY() + ", " + or.getWidth() + ", " + or.getHeight()); 1468 } 1469 1470 if (stuff.isEmpty()) { 1471 s.append(prefix + node.name + "\n"); 1472 } else { 1473 String postfix = " ["; 1474 for (int i=0; i<stuff.size(); i++) { 1475 postfix = postfix + stuff.get(i); 1476 if (i < stuff.size() - 1) postfix += " "; 1477 } 1478 s.append(prefix + node.name + postfix + "]\n"); 1479 } 1480 1481 if (node.getClipNode() != null) { 1482 printDirtyOpts(s, node.getClipNode(), copy, prefix + " cl ", roots); 1483 } 1484 1485 if (node instanceof NGGroup) { 1486 NGGroup g = (NGGroup)node; 1487 for (int i=0; i<g.getChildren().size(); i++) { 1488 printDirtyOpts(s, g.getChildren().get(i), copy, prefix + " ", roots); 1489 } 1490 } 1491 } 1492 1493 /** 1494 * Helper method draws rectangles indicating the overdraw rectangles. 1495 * 1496 * @param tx The scene->parent transform. 1497 * @param pvTx The perspective camera transform. 1498 * @param clipBounds The bounds in scene coordinates 1499 * @param colorBuffer A pixel array where each pixel contains a color indicating how many times 1500 * it has been "drawn" 1501 * @param dirtyRegionIndex the index of the dirty region we're gathering information for. This is 1502 * needed so we can shift the "painted" field to find out if this node 1503 * was drawn in this dirty region. 1504 */ 1505 public void drawDirtyOpts(final BaseTransform tx, final GeneralTransform3D pvTx, 1506 Rectangle clipBounds, int[] colorBuffer, int dirtyRegionIndex) { 1507 if ((painted & (1 << (dirtyRegionIndex * 2))) != 0) { 1508 // Transforming the content bounds (which includes the clip) to screen coordinates 1509 tx.copy().deriveWithConcatenation(getTransform()).transform(contentBounds, TEMP_BOUNDS); 1510 if (pvTx != null) pvTx.transform(TEMP_BOUNDS, TEMP_BOUNDS); 1511 RectBounds bounds = new RectBounds(); 1512 TEMP_BOUNDS.flattenInto(bounds); 1513 1514 // Adjust the bounds so that they are relative to the clip. The colorBuffer is sized 1515 // exactly the same as the clip, and the elements of the colorBuffer represent the 1516 // pixels inside the clip. However the bounds of this node may overlap the clip in 1517 // some manner, so we adjust them such that x, y, w, h will be the adjusted bounds. 1518 assert clipBounds.width * clipBounds.height == colorBuffer.length; 1519 bounds.intersectWith(clipBounds); 1520 int x = (int) bounds.getMinX() - clipBounds.x; 1521 int y = (int) bounds.getMinY() - clipBounds.y; 1522 int w = (int) (bounds.getWidth() + .5); 1523 int h = (int) (bounds.getHeight() + .5); 1524 1525 if (w == 0 || h == 0) { 1526 // I would normally say we should never reach this point, as it means something was 1527 // marked as painted but really couldn't have been. 1528 return; 1529 } 1530 1531 // x, y, w, h are 0 based and will fit within the clip, so now we can simply update 1532 // all the pixels that fall within these bounds. 1533 for (int i = y; i < y+h; i++) { 1534 for (int j = x; j < x+w; j++) { 1535 final int index = i * clipBounds.width + j; 1536 int color = colorBuffer[index]; 1537 1538 // This is kind of a dirty hack. The idea is to show green if 0 or 1 1539 // times a pixel is drawn, Yellow for 2 or 3 times, and red for more 1540 // Than that. So I use 0x80007F00 as the first green color, and 1541 // 0x80008000 as the second green color, but their so close to the same 1542 // thing you probably won't be able to tell them apart, but I can tell 1543 // numerically they're different and increment (so I use the colors 1544 // as my counters). 1545 if (color == 0) { 1546 color = 0x8007F00; 1547 } else if ((painted & (3 << (dirtyRegionIndex * 2))) == 3) { 1548 switch (color) { 1549 case 0x80007F00: 1550 color = 0x80008000; 1551 break; 1552 case 0x80008000: 1553 color = 0x807F7F00; 1554 break; 1555 case 0x807F7F00: 1556 color = 0x80808000; 1557 break; 1558 case 0x80808000: 1559 color = 0x807F0000; 1560 break; 1561 default: 1562 color = 0x80800000; 1563 } 1564 } 1565 colorBuffer[index] = color; 1566 } 1567 } 1568 } 1569 } 1570 1571 /*************************************************************************** 1572 * * 1573 * Identifying render roots * 1574 * * 1575 **************************************************************************/ 1576 protected static enum RenderRootResult { 1577 /** 1578 * A Node returns NO_RENDER_ROOT when it is not a render root because 1579 * it does not have an opaqueRegion which completely covers the area 1580 * of the clip. Maybe the node is dirty, but outside the dirty region 1581 * that we're currently processing. For an NGGroup, returning 1582 * NO_RENDER_ROOT means that there is no render root (occluder) within 1583 * this entire branch of the tree. 1584 */ 1585 NO_RENDER_ROOT, 1586 /** 1587 * A Node returns HAS_RENDER_ROOT when its opaque region completely 1588 * covers the clip. An NGGroup returns HAS_RENDER_ROOT when one of 1589 * its children either returned HAS_RENDER_ROOT or HAS_RENDER_ROOT_AND_IS_CLEAN. 1590 */ 1591 HAS_RENDER_ROOT, 1592 /** 1593 * A Node returns HAS_RENDER_ROOT_AND_IS_CLEAN when its opaque region 1594 * completely covers the clip and the Node is, itself, clean. An NGNode 1595 * returns HAS_RENDER_ROOT_AND_IS_CLEAN only if it had a child that 1596 * returned HAS_RENDER_ROOT_AND_IS_CLEAN and none of its children drawn 1597 * above the render root are dirty. 1598 * 1599 * This optimization allows us to recognize situations where perhaps there 1600 * were some dirty nodes, but they are completely covered by an occluder, 1601 * and therefore we don't actually have to draw anything. 1602 */ 1603 HAS_RENDER_ROOT_AND_IS_CLEAN, 1604 } 1605 1606 /** 1607 * Called <strong>after</strong> preCullingBits in order to get the node 1608 * from which we should begin drawing. This is our support for occlusion culling. 1609 * This should only be called on the root node. 1610 * 1611 * If no render root was found, we need to render everything from this root, so the path will contain this node. 1612 * If no rendering is needed (everything dirty is occluded), the path will remain empty 1613 * 1614 * @param path node path to store the node path 1615 */ 1616 public final void getRenderRoot(NodePath path, RectBounds dirtyRegion, int cullingIndex, 1617 BaseTransform tx, GeneralTransform3D pvTx) { 1618 1619 // This is the main entry point, make sure to check these inputs for validity 1620 if (path == null || dirtyRegion == null || tx == null || pvTx == null) { 1621 throw new NullPointerException(); 1622 } 1623 if (cullingIndex < -1 || cullingIndex > 15) { 1624 throw new IllegalArgumentException("cullingIndex cannot be < -1 or > 15"); 1625 } 1626 1627 // This method must NEVER BE CALLED if the depth buffer is turned on. I don't have a good way to test 1628 // for that because NGNode doesn't have a reference to the scene it is a part of... 1629 1630 RenderRootResult result = computeRenderRoot(path, dirtyRegion, cullingIndex, tx, pvTx); 1631 if (result == RenderRootResult.NO_RENDER_ROOT) { 1632 // We didn't find any render root, which means that no one node was large enough 1633 // to obscure the entire dirty region (or, possibly, some combination of nodes in an 1634 // NGGroup were not, together, large enough to do the job). So we need to render 1635 // from the root node, which is this node. 1636 path.add(this); 1637 } else if (result == RenderRootResult.HAS_RENDER_ROOT_AND_IS_CLEAN) { 1638 // We've found a render root, and it is clean and everything above it in painter order 1639 // is clean, so actually we have nothing to paint this time around (some stuff must 1640 // have been dirty which is completely occluded by the render root). So we can clear 1641 // the path, which indicates to the caller that nothing needs to be painted. 1642 path.clear(); 1643 } 1644 } 1645 1646 /** 1647 * Searches for the last node that covers all of the specified dirty region with an opaque region, 1648 * in this node's subtree. Such a node can serve as a rendering root as all nodes preceding the node 1649 * will be covered by it. 1650 * 1651 * @param path the NodePath to populate with the path to the render root. Cannot be null. 1652 * @param dirtyRegion the current dirty region. Cannot be null. 1653 * @param cullingIndex index of culling information 1654 * @param tx current transform. Cannot be null. 1655 * @param pvTx current perspective transform. Cannot be null. 1656 * @return The result of visiting this node. 1657 */ 1658 RenderRootResult computeRenderRoot(NodePath path, RectBounds dirtyRegion, 1659 int cullingIndex, BaseTransform tx, GeneralTransform3D pvTx) { 1660 return computeNodeRenderRoot(path, dirtyRegion, cullingIndex, tx, pvTx); 1661 } 1662 1663 private static Point2D[] TEMP_POINTS2D_4 = 1664 new Point2D[] { new Point2D(), new Point2D(), new Point2D(), new Point2D() }; 1665 1666 // Whether (px, py) is clockwise or counter-clockwise to a->b 1667 private static int ccw(double px, double py, Point2D a, Point2D b) { 1668 return (int)Math.signum(((b.x - a.x) * (py - a.y)) - (b.y - a.y) * (px - a.x)); 1669 } 1670 1671 private static boolean pointInConvexQuad(double x, double y, Point2D[] rect) { 1672 int ccw01 = ccw(x, y, rect[0], rect[1]); 1673 int ccw12 = ccw(x, y, rect[1], rect[2]); 1674 int ccw23 = ccw(x, y, rect[2], rect[3]); 1675 int ccw31 = ccw(x, y, rect[3], rect[0]); 1676 1677 // Possible results after this operation: 1678 // 0 -> 0 (0x0) 1679 // 1 -> 1 (0x1) 1680 // -1 -> Integer.MIN_VALUE (0x80000000) 1681 ccw01 ^= (ccw01 >>> 1); 1682 ccw12 ^= (ccw12 >>> 1); 1683 ccw23 ^= (ccw23 >>> 1); 1684 ccw31 ^= (ccw31 >>> 1); 1685 1686 final int union = ccw01 | ccw12 | ccw23 | ccw31; 1687 // This means all ccw* were either (-1 or 0) or (1 or 0), but not all of them were 0 1688 return union == 0x80000000 || union == 0x1; 1689 // Or alternatively... 1690 // return (union ^ (union << 31)) < 0; 1691 } 1692 1693 /** 1694 * Check if this node can serve as rendering root for this dirty region. 1695 * 1696 * @param path the NodePath to populate with the path to the render root. Cannot be null. 1697 * @param dirtyRegion the current dirty region. Cannot be null. 1698 * @param cullingIndex index of culling information, -1 means culling information should not be used 1699 * @param tx current transform. Cannot be null. 1700 * @param pvTx current perspective transform. Cannot be null. 1701 * @return NO_RENDER_ROOT if this node does <em>not</em> have an opaque 1702 * region that fills the entire dirty region. Returns HAS_RENDER_ROOT 1703 * if the opaque region fills the dirty region. 1704 */ 1705 final RenderRootResult computeNodeRenderRoot(NodePath path, RectBounds dirtyRegion, 1706 int cullingIndex, BaseTransform tx, GeneralTransform3D pvTx) { 1707 1708 // Nodes outside of the dirty region can be excluded immediately. 1709 // This can be used only if the culling information is provided. 1710 if (cullingIndex != -1) { 1711 final int bits = cullingBits >> (cullingIndex * 2); 1712 if ((bits & DIRTY_REGION_CONTAINS_OR_INTERSECTS_NODE_BOUNDS) == 0x00) { 1713 return RenderRootResult.NO_RENDER_ROOT; 1714 } 1715 } 1716 1717 if (!isVisible()) { 1718 return RenderRootResult.NO_RENDER_ROOT; 1719 } 1720 1721 final RectBounds opaqueRegion = getOpaqueRegion(); 1722 if (opaqueRegion == null) return RenderRootResult.NO_RENDER_ROOT; 1723 1724 final BaseTransform localToParentTx = getTransform(); 1725 1726 BaseTransform localToSceneTx = TEMP_TRANSFORM.deriveWithNewTransform(tx).deriveWithConcatenation(localToParentTx); 1727 1728 // Now check if the dirty region is fully contained in our opaque region. Suppose the above 1729 // transform included a rotation about Z. In these cases, the transformed 1730 // opaqueRegion might be some non-axis aligned quad. So what we need to do is to check 1731 // that each corner of the dirty region lies within the (potentially rotated) quad 1732 // of the opaqueRegion. 1733 if (checkBoundsInQuad(opaqueRegion, dirtyRegion, localToSceneTx, pvTx)) { 1734 // This node is a render root. 1735 path.add(this); 1736 return isClean() ? RenderRootResult.HAS_RENDER_ROOT_AND_IS_CLEAN : RenderRootResult.HAS_RENDER_ROOT; 1737 } 1738 1739 return RenderRootResult.NO_RENDER_ROOT; 1740 } 1741 1742 static boolean checkBoundsInQuad(RectBounds untransformedQuad, 1743 RectBounds innerBounds, BaseTransform tx, GeneralTransform3D pvTx) { 1744 1745 if (pvTx.isIdentity() && (tx.getType() & ~(BaseTransform.TYPE_TRANSLATION 1746 | BaseTransform.TYPE_QUADRANT_ROTATION 1747 | BaseTransform.TYPE_MASK_SCALE)) == 0) { 1748 // If pvTx is identity and there's simple transformation that will result in axis-aligned rectangle, 1749 // we can do a quick test by using bound.contains() 1750 if (tx.isIdentity()) { 1751 TEMP_BOUNDS.deriveWithNewBounds(untransformedQuad); 1752 } else { 1753 tx.transform(untransformedQuad, TEMP_BOUNDS); 1754 } 1755 1756 TEMP_BOUNDS.flattenInto(TEMP_RECT_BOUNDS); 1757 1758 return TEMP_RECT_BOUNDS.contains(innerBounds); 1759 } else { 1760 TEMP_POINTS2D_4[0].setLocation(untransformedQuad.getMinX(), untransformedQuad.getMinY()); 1761 TEMP_POINTS2D_4[1].setLocation(untransformedQuad.getMaxX(), untransformedQuad.getMinY()); 1762 TEMP_POINTS2D_4[2].setLocation(untransformedQuad.getMaxX(), untransformedQuad.getMaxY()); 1763 TEMP_POINTS2D_4[3].setLocation(untransformedQuad.getMinX(), untransformedQuad.getMaxY()); 1764 1765 for (Point2D p : TEMP_POINTS2D_4) { 1766 tx.transform(p, p); 1767 if (!pvTx.isIdentity()) { 1768 pvTx.transform(p, p); 1769 } 1770 } 1771 1772 return (pointInConvexQuad(innerBounds.getMinX(), innerBounds.getMinY(), TEMP_POINTS2D_4) 1773 && pointInConvexQuad(innerBounds.getMaxX(), innerBounds.getMinY(), TEMP_POINTS2D_4) 1774 && pointInConvexQuad(innerBounds.getMaxX(), innerBounds.getMaxY(), TEMP_POINTS2D_4) 1775 && pointInConvexQuad(innerBounds.getMinX(), innerBounds.getMaxY(), TEMP_POINTS2D_4)); 1776 } 1777 } 1778 1779 /** 1780 * Invalidates any cached representation of the opaque region for this node. On the next 1781 * call to getOpaqueRegion, the opaque region will be recalculated. Any changes to state 1782 * which is used in the {@link #hasOpaqueRegion()} call must invoke this method 1783 * or the opaque region calculations will be wrong. 1784 */ 1785 protected final void invalidateOpaqueRegion() { 1786 opaqueRegionInvalid = true; 1787 if (isClip) parent.invalidateOpaqueRegion(); 1788 } 1789 1790 /** 1791 * This method exists only for the sake of testing. 1792 * @return value of opaqueRegionInvalid 1793 */ 1794 final boolean isOpaqueRegionInvalid() { 1795 return opaqueRegionInvalid; 1796 } 1797 1798 /** 1799 * Gets the opaque region for this node, if there is one, or returns null. 1800 * @return The opaque region for this node, or null. 1801 */ 1802 public final RectBounds getOpaqueRegion() { 1803 // Note that when we invalidate the opaqueRegion of an NGNode, we don't 1804 // walk up the tree or communicate with the parents (unlike dirty flags). 1805 // An NGGroup does not compute an opaqueRegion based on the union of opaque 1806 // regions of its children (although this is a fine idea to consider!). See RT-32441 1807 // If we ever fix RT-32441, we must be sure to handle the case of a Group being used 1808 // as a clip node (such that invalidating a child on the group invalidates the 1809 // opaque region of every node up to the root). 1810 1811 // Because the Effect classes have no reference to NGNode, they cannot tell the 1812 // NGNode to invalidate the opaque region whenever properties on the Effect that 1813 // would impact the opaqueRegion change. As a result, when an Effect is specified 1814 // on the NGNode, we will always treat it as if it were invalid. A more invasive 1815 // (but better) change would be to give Effect the ability to invalidate the 1816 // NGNode's opaque region when needed. 1817 if (opaqueRegionInvalid || getEffect() != null) { 1818 opaqueRegionInvalid = false; 1819 if (supportsOpaqueRegions() && hasOpaqueRegion()) { 1820 opaqueRegion = computeOpaqueRegion(opaqueRegion == null ? new RectBounds() : opaqueRegion); 1821 // If we got a null result then we encountered an error condition where somebody 1822 // claimed supportsOpaqueRegions and hasOpaqueRegion, but then they 1823 // returned null! This should never happen, so we have an assert here. However since 1824 // assertions are disabled at runtime and we want to avoid the NPE, we also perform 1825 // a null check. 1826 assert opaqueRegion != null; 1827 if (opaqueRegion == null) { 1828 return null; 1829 } 1830 // If there is a clip, then we need to determine the opaque region of the clip, and 1831 // intersect that with our existing opaque region. For example, if I had a rectangle 1832 // with a circle for its clip (centered over the rectangle), then the result needs to 1833 // be the circle's opaque region. 1834 final NGNode clip = getClipNode(); 1835 if (clip != null) { 1836 final RectBounds clipOpaqueRegion = clip.getOpaqueRegion(); 1837 // Technically a flip/quadrant rotation is allowed as well, but we don't have a convenient 1838 // way to do that yet. 1839 if (clipOpaqueRegion == null || (clip.getTransform().getType() & ~(BaseTransform.TYPE_TRANSLATION | BaseTransform.TYPE_MASK_SCALE)) != 0) { 1840 // RT-25095: If this node has a clip who's opaque region cannot be determined, then 1841 // we cannot determine any opaque region for this node (in fact, it might not have one). 1842 // Also, if the transform is something other than identity, scale, or translate then 1843 // we're just going to bail (sorry, rotate, maybe next time!) 1844 return opaqueRegion = null; 1845 } 1846 // We have to take into account any transform specified on the clip to put 1847 // it into the same coordinate system as this node 1848 final BaseBounds b = clip.getTransform().transform(clipOpaqueRegion, TEMP_BOUNDS); 1849 b.flattenInto(TEMP_RECT_BOUNDS); 1850 opaqueRegion.intersectWith(TEMP_RECT_BOUNDS); 1851 1852 } 1853 } else { 1854 // The opaqueRegion may have been non-null in the past, but there isn't an opaque region now, 1855 // so we will nuke it to save some memory 1856 opaqueRegion = null; 1857 } 1858 } 1859 1860 return opaqueRegion; 1861 } 1862 1863 /** 1864 * Gets whether this NGNode supports opaque regions at all. Most node types do not, 1865 * but some do. If an NGNode subclass is written to support opaque regions, it must override 1866 * this method to return true. The subclass must then also override the computeDirtyRegion method 1867 * to return the dirty region, or null if the node in its current state doesn't have one. 1868 * This method is intended to be immutable. 1869 * 1870 * @return Whether this NGNode implementation supports opaque regions. This could also have been 1871 * implemented via an interface that some NGNodes implemented, but then we'd have instanceof 1872 * checks which I'd rather avoid. 1873 */ 1874 protected boolean supportsOpaqueRegions() { return false; } 1875 1876 /** 1877 * Called only on NGNode subclasses which override {@link #supportsOpaqueRegions()} to return 1878 * true, this method will return whether or not this NGNode is in a state where it has 1879 * an opaque region to actually return. If this method returns true, a subsequent call to 1880 * {@link #computeOpaqueRegion(com.sun.javafx.geom.RectBounds)} <strong>must</strong> return 1881 * a non-null result. Any state used in the computation of this method, when it changes, must 1882 * result in a call to {@link #invalidateOpaqueRegion()}. 1883 * 1884 * @return Whether this NGNode currently has an opaque region. 1885 */ 1886 protected boolean hasOpaqueRegion() { 1887 final NGNode clip = getClipNode(); 1888 final Effect effect = getEffect(); 1889 return (effect == null || !effect.reducesOpaquePixels()) && 1890 getOpacity() == 1f && 1891 (nodeBlendMode == null || nodeBlendMode == Blend.Mode.SRC_OVER) && 1892 (clip == null || 1893 (clip.supportsOpaqueRegions() && clip.hasOpaqueRegion())); 1894 } 1895 1896 /** 1897 * Computes and returns the opaque region for this node. This method 1898 * @param opaqueRegion 1899 * @return 1900 */ 1901 protected RectBounds computeOpaqueRegion(RectBounds opaqueRegion) { 1902 return null; 1903 } 1904 1905 /** 1906 * Returns whether a clip represented by this node can be rendered using 1907 * axis aligned rect clip. The default implementation returns false, 1908 * specific subclasses should override to return true when appropriate. 1909 * 1910 * @return whether this rectangle is axis aligned when rendered given node's 1911 * and rendering transform 1912 */ 1913 protected boolean isRectClip(BaseTransform xform, boolean permitRoundedRectangle) { 1914 return false; 1915 } 1916 1917 /*************************************************************************** 1918 * * 1919 * Rendering * 1920 * * 1921 **************************************************************************/ 1922 1923 /** 1924 * Render the tree of nodes to the specified G (graphics) object 1925 * descending from this node as the root. This method is designed to avoid 1926 * generated trash as much as possible while descending through the 1927 * render graph while rendering. This is the appropriate method both to 1928 * initiate painting of an entire scene, and for a branch. The NGGroup 1929 * implementation must call this method on each child, not doRender directly. 1930 * 1931 * @param g The graphics object we're rendering to. This must never be null. 1932 */ 1933 public final void render(Graphics g) { 1934 if (PULSE_LOGGING_ENABLED) { 1935 PulseLogger.incrementCounter("Nodes visited during render"); 1936 } 1937 // Clear the visuals changed flag 1938 clearDirty(); 1939 // If it isn't visible, then punt 1940 if (!visible || opacity == 0f) return; 1941 1942 // We know that we are going to render this node, so we call the 1943 // doRender method, which subclasses implement to do the actual 1944 // rendering work. 1945 doRender(g); 1946 } 1947 1948 /** 1949 * Called on every render pulse for all nodes in case they have render-time 1950 * operations that must be completed on a pulse, but were not otherwise 1951 * rendered by the ordinary damage management logic. 1952 * The graphics argument will be the graphics that was used to render the 1953 * scene if it is available, but may be null for cases when the scene 1954 * required no visible updates and thus no back buffer graphics was 1955 * actually obtained. Implementors must have a backup plan for that 1956 * case when the Graphics object is null. 1957 * 1958 * @param gOptional the Graphics object that was used to render the 1959 * Scene, or null 1960 */ 1961 public void renderForcedContent(Graphics gOptional) { 1962 } 1963 1964 // This node requires 2D graphics state for rendering 1965 boolean isShape3D() { 1966 return false; 1967 } 1968 1969 /** 1970 * Invoked only by the final render method. Implementations 1971 * of this method should make sure to save & restore the transform state. 1972 */ 1973 protected void doRender(Graphics g) { 1974 1975 g.setState3D(isShape3D()); 1976 1977 boolean preCullingTurnedOff = false; 1978 if (PrismSettings.dirtyOptsEnabled) { 1979 if (g.hasPreCullingBits()) { 1980 //preculling bits available 1981 final int bits = cullingBits >> (g.getClipRectIndex() * 2); 1982 if ((bits & DIRTY_REGION_CONTAINS_OR_INTERSECTS_NODE_BOUNDS) == 0) { 1983 // If no culling bits are set for this region, this group 1984 // does not intersect (nor is covered by) the region 1985 return; 1986 } else if ((bits & DIRTY_REGION_CONTAINS_NODE_BOUNDS) != 0) { 1987 // When this group is fully covered by the region, 1988 // turn off the culling checks in the subtree, as everything 1989 // gets rendered 1990 g.setHasPreCullingBits(false); 1991 preCullingTurnedOff = true; 1992 } 1993 } 1994 } 1995 1996 // save current depth test state 1997 boolean prevDepthTest = g.isDepthTest(); 1998 1999 // Apply Depth test for this node 2000 // (note that this will only be used if we have a depth buffer for the 2001 // surface to which we are rendering) 2002 g.setDepthTest(isDepthTest()); 2003 2004 // save current transform state 2005 BaseTransform prevXform = g.getTransformNoClone(); 2006 2007 double mxx = prevXform.getMxx(); 2008 double mxy = prevXform.getMxy(); 2009 double mxz = prevXform.getMxz(); 2010 double mxt = prevXform.getMxt(); 2011 2012 double myx = prevXform.getMyx(); 2013 double myy = prevXform.getMyy(); 2014 double myz = prevXform.getMyz(); 2015 double myt = prevXform.getMyt(); 2016 2017 double mzx = prevXform.getMzx(); 2018 double mzy = prevXform.getMzy(); 2019 double mzz = prevXform.getMzz(); 2020 double mzt = prevXform.getMzt(); 2021 2022 // filters are applied in the following order: 2023 // transform 2024 // blend mode 2025 // opacity 2026 // cache 2027 // clip 2028 // effect 2029 // The clip must be below the cache filter, as this is expected in the 2030 // CacheFilter in order to apply scrolling optimization 2031 g.transform(getTransform()); 2032 // Try to keep track of whether this node was *really* painted. Still an 2033 // approximation, but somewhat more accurate (at least it doesn't include 2034 // groups which don't paint anything themselves). 2035 boolean p = false; 2036 // NOTE: Opt out 2D operations on 3D Shapes, which are not yet handled by Prism 2037 if (!isShape3D() && g instanceof ReadbackGraphics && needsBlending()) { 2038 renderNodeBlendMode(g); 2039 p = true; 2040 } else if (!isShape3D() && getOpacity() < 1f) { 2041 renderOpacity(g); 2042 p = true; 2043 } else if (!isShape3D() && getCacheFilter() != null) { 2044 renderCached(g); 2045 p = true; 2046 } else if (!isShape3D() && getClipNode() != null) { 2047 renderClip(g); 2048 p = true; 2049 } else if (!isShape3D() && getEffectFilter() != null && effectsSupported) { 2050 renderEffect(g); 2051 p = true; 2052 } else { 2053 renderContent(g); 2054 if (PrismSettings.showOverdraw) { 2055 p = this instanceof NGRegion || !(this instanceof NGGroup); 2056 } 2057 } 2058 2059 if (preCullingTurnedOff) { 2060 g.setHasPreCullingBits(true); 2061 } 2062 2063 // restore previous transform state 2064 g.setTransform3D(mxx, mxy, mxz, mxt, 2065 myx, myy, myz, myt, 2066 mzx, mzy, mzz, mzt); 2067 2068 // restore previous depth test state 2069 g.setDepthTest(prevDepthTest); 2070 2071 if (PULSE_LOGGING_ENABLED) { 2072 PulseLogger.incrementCounter("Nodes rendered"); 2073 } 2074 2075 // Used for debug purposes. This is not entirely accurate, as it doesn't measure the 2076 // number of times this node drew to the pixels, and in some cases reports a node as 2077 // having been drawn even when it didn't lay down any pixels. We'd need to integrate 2078 // with our shaders or do something much more invasive to get better data here. 2079 if (PrismSettings.showOverdraw) { 2080 if (p) { 2081 painted |= 3 << (g.getClipRectIndex() * 2); 2082 } else { 2083 painted |= 1 << (g.getClipRectIndex() * 2); 2084 } 2085 } 2086 } 2087 2088 /** 2089 * Return true if this node has a blend mode that requires special 2090 * processing. 2091 * Regular nodes can handle null or SRC_OVER just by rendering into 2092 * the existing buffer. 2093 * Groups override this since they must collect their children into 2094 * a single rendering pass if their mode is explicitly SRC_OVER. 2095 * @return true if this node needs special blending support 2096 */ 2097 protected boolean needsBlending() { 2098 Blend.Mode mode = getNodeBlendMode(); 2099 return (mode != null && mode != Blend.Mode.SRC_OVER); 2100 } 2101 2102 private void renderNodeBlendMode(Graphics g) { 2103 // The following is safe; curXform will not be mutated below 2104 BaseTransform curXform = g.getTransformNoClone(); 2105 2106 BaseBounds clipBounds = getClippedBounds(new RectBounds(), curXform); 2107 if (clipBounds.isEmpty()) { 2108 clearDirtyTree(); 2109 return; 2110 } 2111 2112 if (!isReadbackSupported(g)) { 2113 if (getOpacity() < 1f) { 2114 renderOpacity(g); 2115 } else if (getClipNode() != null) { 2116 renderClip(g); 2117 } else { 2118 renderContent(g); 2119 } 2120 return; 2121 } 2122 2123 // TODO: optimize this (RT-26936) 2124 // Extract clip bounds 2125 Rectangle clipRect = new Rectangle(clipBounds); 2126 clipRect.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g)); 2127 2128 // render the node content into the first offscreen image 2129 FilterContext fctx = getFilterContext(g); 2130 PrDrawable contentImg = (PrDrawable) 2131 Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height); 2132 if (contentImg == null) { 2133 clearDirtyTree(); 2134 return; 2135 } 2136 Graphics gContentImg = contentImg.createGraphics(); 2137 gContentImg.setHasPreCullingBits(g.hasPreCullingBits()); 2138 gContentImg.setClipRectIndex(g.getClipRectIndex()); 2139 gContentImg.translate(-clipRect.x, -clipRect.y); 2140 gContentImg.transform(curXform); 2141 if (getOpacity() < 1f) { 2142 renderOpacity(gContentImg); 2143 } else if (getCacheFilter() != null) { 2144 renderCached(gContentImg); 2145 } else if (getClipNode() != null) { 2146 renderClip(g); 2147 } else if (getEffectFilter() != null) { 2148 renderEffect(gContentImg); 2149 } else { 2150 renderContent(gContentImg); 2151 } 2152 2153 // the above image has already been rendered in device space, so 2154 // just translate to the node origin in device space here... 2155 RTTexture bgRTT = ((ReadbackGraphics) g).readBack(clipRect); 2156 PrDrawable bgPrD = PrDrawable.create(fctx, bgRTT); 2157 Blend blend = new Blend(getNodeBlendMode(), 2158 new PassThrough(bgPrD, clipRect), 2159 new PassThrough(contentImg, clipRect)); 2160 CompositeMode oldmode = g.getCompositeMode(); 2161 g.setTransform(null); 2162 g.setCompositeMode(CompositeMode.SRC); 2163 PrEffectHelper.render(blend, g, 0, 0, null); 2164 g.setCompositeMode(oldmode); 2165 // transform state will be restored in render() method above... 2166 2167 Effect.releaseCompatibleImage(fctx, contentImg); 2168 ((ReadbackGraphics) g).releaseReadBackBuffer(bgRTT); 2169 } 2170 2171 private void renderRectClip(Graphics g, NGRectangle clipNode) { 2172 BaseBounds newClip = clipNode.getShape().getBounds(); 2173 if (!clipNode.getTransform().isIdentity()) { 2174 newClip = clipNode.getTransform().transform(newClip, newClip); 2175 } 2176 final BaseTransform curXform = g.getTransformNoClone(); 2177 final Rectangle curClip = g.getClipRectNoClone(); 2178 newClip = curXform.transform(newClip, newClip); 2179 newClip.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g)); 2180 if (newClip.isEmpty() || 2181 newClip.getWidth() == 0 || 2182 newClip.getHeight() == 0) { 2183 clearDirtyTree(); 2184 return; 2185 } 2186 // REMIND: avoid garbage by changing setClipRect to accept xywh 2187 g.setClipRect(new Rectangle(newClip)); 2188 renderForClip(g); 2189 g.setClipRect(curClip); 2190 clipNode.clearDirty(); // as render() is not called on the clipNode, 2191 // make sure the dirty flags are cleared 2192 } 2193 2194 void renderClip(Graphics g) { 2195 // if clip's opacity is 0 there's nothing to render 2196 if (getClipNode().getOpacity() == 0.0) { 2197 clearDirtyTree(); 2198 return; 2199 } 2200 2201 // The following is safe; curXform will not be mutated below 2202 BaseTransform curXform = g.getTransformNoClone(); 2203 2204 BaseBounds clipBounds = getClippedBounds(new RectBounds(), curXform); 2205 if (clipBounds.isEmpty()) { 2206 clearDirtyTree(); 2207 return; 2208 } 2209 2210 if (getClipNode() instanceof NGRectangle) { 2211 // optimized case for rectangular clip 2212 NGRectangle rectNode = (NGRectangle)getClipNode(); 2213 if (rectNode.isRectClip(curXform, false)) { 2214 renderRectClip(g, rectNode); 2215 return; 2216 } 2217 } 2218 2219 // TODO: optimize this (RT-26936) 2220 // Extract clip bounds 2221 Rectangle clipRect = new Rectangle(clipBounds); 2222 clipRect.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g)); 2223 2224 if (!curXform.is2D()) { 2225 Rectangle savedClip = g.getClipRect(); 2226 g.setClipRect(clipRect); 2227 NodeEffectInput clipInput = 2228 new NodeEffectInput(getClipNode(), 2229 NodeEffectInput.RenderType.FULL_CONTENT); 2230 NodeEffectInput nodeInput = 2231 new NodeEffectInput(this, 2232 NodeEffectInput.RenderType.CLIPPED_CONTENT); 2233 Blend blend = new Blend(Blend.Mode.SRC_IN, clipInput, nodeInput); 2234 PrEffectHelper.render(blend, g, 0, 0, null); 2235 clipInput.flush(); 2236 nodeInput.flush(); 2237 g.setClipRect(savedClip); 2238 // There may have been some errors in the application of the 2239 // effect and we would not know to what extent the nodes were 2240 // rendered and cleared or left dirty. clearDirtyTree() will 2241 // clear both this node its clip node, and it will not recurse 2242 // to the children unless they are still marked dirty. It should 2243 // be cheap if there was no problem and thorough if there was... 2244 clearDirtyTree(); 2245 return; 2246 } 2247 2248 // render the node content into the first offscreen image 2249 FilterContext fctx = getFilterContext(g); 2250 PrDrawable contentImg = (PrDrawable) 2251 Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height); 2252 if (contentImg == null) { 2253 clearDirtyTree(); 2254 return; 2255 } 2256 Graphics gContentImg = contentImg.createGraphics(); 2257 gContentImg.setExtraAlpha(g.getExtraAlpha()); 2258 gContentImg.setHasPreCullingBits(g.hasPreCullingBits()); 2259 gContentImg.setClipRectIndex(g.getClipRectIndex()); 2260 gContentImg.translate(-clipRect.x, -clipRect.y); 2261 gContentImg.transform(curXform); 2262 renderForClip(gContentImg); 2263 2264 // render the mask (clipNode) into the second offscreen image 2265 PrDrawable clipImg = (PrDrawable) 2266 Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height); 2267 if (clipImg == null) { 2268 getClipNode().clearDirtyTree(); 2269 Effect.releaseCompatibleImage(fctx, contentImg); 2270 return; 2271 } 2272 Graphics gClipImg = clipImg.createGraphics(); 2273 gClipImg.translate(-clipRect.x, -clipRect.y); 2274 gClipImg.transform(curXform); 2275 getClipNode().render(gClipImg); 2276 2277 // the above images have already been rendered in device space, so 2278 // just translate to the node origin in device space here... 2279 g.setTransform(null); 2280 Blend blend = new Blend(Blend.Mode.SRC_IN, 2281 new PassThrough(clipImg, clipRect), 2282 new PassThrough(contentImg, clipRect)); 2283 PrEffectHelper.render(blend, g, 0, 0, null); 2284 // transform state will be restored in render() method above... 2285 2286 Effect.releaseCompatibleImage(fctx, contentImg); 2287 Effect.releaseCompatibleImage(fctx, clipImg); 2288 } 2289 2290 void renderForClip(Graphics g) { 2291 if (getEffectFilter() != null) { 2292 renderEffect(g); 2293 } else { 2294 renderContent(g); 2295 } 2296 } 2297 2298 private void renderOpacity(Graphics g) { 2299 if (getEffectFilter() != null || 2300 getCacheFilter() != null || 2301 getClipNode() != null || 2302 !hasOverlappingContents()) 2303 { 2304 // if the node has a non-null effect or cached==true, we don't 2305 // need to bother rendering to an offscreen here because the 2306 // contents will be flattened as part of rendering the effect 2307 // (or creating the cached image) 2308 float ea = g.getExtraAlpha(); 2309 g.setExtraAlpha(ea*getOpacity()); 2310 if (getCacheFilter() != null) { 2311 renderCached(g); 2312 } else if (getClipNode() != null) { 2313 renderClip(g); 2314 } else if (getEffectFilter() != null) { 2315 renderEffect(g); 2316 } else { 2317 renderContent(g); 2318 } 2319 g.setExtraAlpha(ea); 2320 return; 2321 } 2322 2323 FilterContext fctx = getFilterContext(g); 2324 BaseTransform curXform = g.getTransformNoClone(); 2325 BaseBounds bounds = getContentBounds(new RectBounds(), curXform); 2326 Rectangle r = new Rectangle(bounds); 2327 r.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g)); 2328 PrDrawable img = (PrDrawable) 2329 Effect.getCompatibleImage(fctx, r.width, r.height); 2330 if (img == null) { 2331 return; 2332 } 2333 Graphics gImg = img.createGraphics(); 2334 gImg.setHasPreCullingBits(g.hasPreCullingBits()); 2335 gImg.setClipRectIndex(g.getClipRectIndex()); 2336 gImg.translate(-r.x, -r.y); 2337 gImg.transform(curXform); 2338 renderContent(gImg); 2339 // img contents have already been rendered in device space, so 2340 // just translate to the node origin in device space here... 2341 g.setTransform(null); 2342 float ea = g.getExtraAlpha(); 2343 g.setExtraAlpha(getOpacity()*ea); 2344 g.drawTexture(img.getTextureObject(), r.x, r.y, r.width, r.height); 2345 g.setExtraAlpha(ea); 2346 // transform state will be restored in render() method above... 2347 Effect.releaseCompatibleImage(fctx, img); 2348 } 2349 2350 private void renderCached(Graphics g) { 2351 // We will punt on 3D completely for caching. 2352 // The first check is for any of its children contains a 3D Transform. 2353 // The second check is for any of its parents and itself has a 3D Transform 2354 // The third check is for the printing case, which doesn't use cached 2355 // bitmaps for the screen and for which there is no cacheFilter. 2356 if (isContentBounds2D() && g.getTransformNoClone().is2D() && 2357 !(g instanceof com.sun.prism.PrinterGraphics)) { 2358 getCacheFilter().render(g); 2359 } else { 2360 renderContent(g); 2361 } 2362 } 2363 2364 protected void renderEffect(Graphics g) { 2365 getEffectFilter().render(g); 2366 } 2367 2368 protected abstract void renderContent(Graphics g); 2369 2370 protected abstract boolean hasOverlappingContents(); 2371 2372 /*************************************************************************** 2373 * * 2374 * Static Helper Methods. * 2375 * * 2376 **************************************************************************/ 2377 2378 boolean isReadbackSupported(Graphics g) { 2379 return ((g instanceof ReadbackGraphics) && 2380 ((ReadbackGraphics) g).canReadBack()); 2381 } 2382 2383 /*************************************************************************** 2384 * * 2385 * Filters (Cache, Effect, etc). * 2386 * * 2387 **************************************************************************/ 2388 2389 static FilterContext getFilterContext(Graphics g) { 2390 Screen s = g.getAssociatedScreen(); 2391 if (s == null) { 2392 return PrFilterContext.getPrinterContext(g.getResourceFactory()); 2393 } else { 2394 return PrFilterContext.getInstance(s); 2395 } 2396 } 2397 2398 /** 2399 * A custom effect implementation that has a filter() method that 2400 * simply wraps the given pre-rendered PrDrawable in an ImageData 2401 * and returns that result. This is only used by the renderClip() 2402 * implementation so we cut some corners here (for example, we assume 2403 * that the given PrDrawable image is already in device space). 2404 */ 2405 private static class PassThrough extends Effect { 2406 private PrDrawable img; 2407 private Rectangle bounds; 2408 2409 PassThrough(PrDrawable img, Rectangle bounds) { 2410 this.img = img; 2411 this.bounds = bounds; 2412 } 2413 2414 @Override 2415 public ImageData filter(FilterContext fctx, 2416 BaseTransform transform, 2417 Rectangle outputClip, 2418 Object renderHelper, 2419 Effect defaultInput) 2420 { 2421 img.lock(); 2422 ImageData id = new ImageData(fctx, img, new Rectangle(bounds)); 2423 id.setReusable(true); 2424 return id; 2425 } 2426 2427 @Override 2428 public RectBounds getBounds(BaseTransform transform, 2429 Effect defaultInput) 2430 { 2431 return new RectBounds(bounds); 2432 } 2433 2434 @Override 2435 public AccelType getAccelType(FilterContext fctx) { 2436 return AccelType.INTRINSIC; 2437 } 2438 2439 @Override 2440 public boolean reducesOpaquePixels() { 2441 return false; 2442 } 2443 2444 @Override 2445 public DirtyRegionContainer getDirtyRegions(Effect defaultInput, DirtyRegionPool regionPool) { 2446 return null; //Never called 2447 } 2448 } 2449 2450 /*************************************************************************** 2451 * * 2452 * Stuff * 2453 * * 2454 **************************************************************************/ 2455 2456 public void release() { 2457 } 2458 2459 @Override public String toString() { 2460 return name == null ? super.toString() : name; 2461 } 2462 2463 public void applyTransform(final BaseTransform tx, DirtyRegionContainer drc) { 2464 for (int i = 0; i < drc.size(); i++) { 2465 drc.setDirtyRegion(i, (RectBounds) tx.transform(drc.getDirtyRegion(i), drc.getDirtyRegion(i))); 2466 if (drc.checkAndClearRegion(i)) { 2467 --i; 2468 } 2469 } 2470 } 2471 2472 public void applyClip(final BaseBounds clipBounds, DirtyRegionContainer drc) { 2473 for (int i = 0; i < drc.size(); i++) { 2474 drc.getDirtyRegion(i).intersectWith(clipBounds); 2475 if (drc.checkAndClearRegion(i)) { 2476 --i; 2477 } 2478 } 2479 } 2480 2481 public void applyEffect(final EffectFilter effectFilter, DirtyRegionContainer drc, DirtyRegionPool regionPool) { 2482 Effect effect = effectFilter.getEffect(); 2483 EffectDirtyBoundsHelper helper = EffectDirtyBoundsHelper.getInstance(); 2484 helper.setInputBounds(contentBounds); 2485 helper.setDirtyRegions(drc); 2486 final DirtyRegionContainer effectDrc = effect.getDirtyRegions(helper, regionPool); 2487 drc.deriveWithNewContainer(effectDrc); 2488 regionPool.checkIn(effectDrc); 2489 } 2490 2491 private static class EffectDirtyBoundsHelper extends Effect { 2492 private BaseBounds bounds; 2493 private static EffectDirtyBoundsHelper instance = null; 2494 private DirtyRegionContainer drc; 2495 2496 public void setInputBounds(BaseBounds inputBounds) { 2497 bounds = inputBounds; 2498 } 2499 2500 @Override 2501 public ImageData filter(FilterContext fctx, 2502 BaseTransform transform, 2503 Rectangle outputClip, 2504 Object renderHelper, 2505 Effect defaultInput) { 2506 throw new UnsupportedOperationException(); 2507 } 2508 2509 @Override 2510 public BaseBounds getBounds(BaseTransform transform, Effect defaultInput) { 2511 if (bounds.getBoundsType() == BaseBounds.BoundsType.RECTANGLE) { 2512 return bounds; 2513 } else { 2514 //RT-29453 - CCE: in case we get 3D bounds we need to "flatten" them 2515 return new RectBounds(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY()); 2516 } 2517 } 2518 2519 @Override 2520 public Effect.AccelType getAccelType(FilterContext fctx) { 2521 return null; 2522 } 2523 2524 public static EffectDirtyBoundsHelper getInstance() { 2525 if (instance == null) { 2526 instance = new EffectDirtyBoundsHelper(); 2527 } 2528 return instance; 2529 } 2530 2531 @Override 2532 public boolean reducesOpaquePixels() { 2533 return true; 2534 } 2535 2536 private void setDirtyRegions(DirtyRegionContainer drc) { 2537 this.drc = drc; 2538 } 2539 2540 @Override 2541 public DirtyRegionContainer getDirtyRegions(Effect defaultInput, DirtyRegionPool regionPool) { 2542 DirtyRegionContainer ret = regionPool.checkOut(); 2543 ret.deriveWithNewContainer(drc); 2544 2545 return ret; 2546 } 2547 2548 } 2549 }