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