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