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             // In case we have a valid cache, there might be sub-pixel differences between the computed
1289             // transformedBounds (and consequently also dirtyBounds) and the bounds of the cache.
1290             // At this point, we still don't know for sure if the cache fill be used (or if it will be recomputed).
1291             // For finding that out and then for computing the new cache bounds, we'd need to analyze the current tx
1292             // (using "unmatrix" of CacheFilter). Since the gain of tight bounds would be probably very low and so not
1293             // worth an extra computation, we just round out the region right here.
1294             if (cacheFilter != null && !tx.isTranslateOrIdentity()) {
1295                 region.roundOut();
1296             }
1297             // Now that we have the dirty region, we will simply apply the tx
1298             // to it (after slightly padding it for good luck) to get the scene
1299             // coordinates for this.
1300             region = computePadding(region);
1301             region = tx.transform(region, region);
1302             region = pvTx.transform(region, region);
1303         }
1304         return region;
1305     }
1306 
1307     /**
1308      * LCD Text creates some painful situations where, due to the LCD text
1309      * algorithm, we end up with some pixels touched that are normally outside
1310      * the bounds. To compensate, we need a hook for NGText to add padding.
1311      */
1312     protected BaseBounds computePadding(BaseBounds region) {
1313         return region;
1314     }
1315 
1316     /**
1317      * Marks if the node has some visuals and that the bounds change
1318      * should be taken into account when using the dirty region.
1319      * This will be false for NGGroup (but not for NGRegion)
1320      * @return true if the node has some visuals
1321      */
1322     protected boolean hasVisuals() {
1323         return true;
1324     }
1325 
1326     /***************************************************************************
1327      *                                                                         *
1328      * Culling                                                                 *
1329      *                                                                         *
1330      **************************************************************************/
1331 
1332     /**
1333      * Culling support for multiple dirty regions.
1334      * Set culling bits for the whole graph.
1335      * @param drc Array of dirty regions. Cannot be null.
1336      * @param tx The transform for this render operation. Cannot be null.
1337      * @param pvTx Perspective camera transformation. Cannot be null.
1338      */
1339     public final void doPreCulling(DirtyRegionContainer drc, BaseTransform tx, GeneralTransform3D pvTx) {
1340         if (drc == null || tx == null || pvTx == null) throw new NullPointerException();
1341         markCullRegions(drc, -1, tx, pvTx);
1342     }
1343 
1344     /**
1345      * Marks placement of the node in dirty region encoded into 2 bit flag:
1346      * 00 - node outside dirty region
1347      * 01 - node intersecting dirty region
1348      * 11 - node completely within dirty region
1349      *
1350      * 32 bits = 15 regions max. * 2 bit each. The first two bits are not used
1351      * because we have a special use case for -1, so they should only be set if
1352      * in that case.
1353      *
1354      * @param drc The array of dirty regions.
1355      * @param cullingRegionsBitsOfParent culling bits of parent. -1 if there's no parent.
1356      * @param tx The transform for this render operation. Cannot be null.
1357      * @param pvTx Perspective camera transform. Cannot be null.
1358      */
1359     void markCullRegions(
1360             DirtyRegionContainer drc,
1361             int cullingRegionsBitsOfParent,
1362             BaseTransform tx,
1363             GeneralTransform3D pvTx) {
1364 
1365         // Spent a long time tracking down how cullingRegionsBitsOfParent works. Note that it is
1366         // not just the parent's bits, but also -1 in the case of the "root", where the root is
1367         // either the actual root, or the root of a sub-render operation such as occurs with
1368         // render-to-texture for effects!
1369 
1370         if (tx.isIdentity()) {
1371             TEMP_BOUNDS.deriveWithNewBounds(transformedBounds);
1372         } else {
1373             tx.transform(transformedBounds, TEMP_BOUNDS);
1374         }
1375 
1376         if (!pvTx.isIdentity()) {
1377             pvTx.transform(TEMP_BOUNDS, TEMP_BOUNDS);
1378         }
1379 
1380         TEMP_BOUNDS.flattenInto(TEMP_RECT_BOUNDS);
1381 
1382         cullingBits = 0;
1383         RectBounds region;
1384         int mask = 0x1; // Check only for intersections
1385         for(int i = 0; i < drc.size(); i++) {
1386             region = drc.getDirtyRegion(i);
1387             if (region == null || region.isEmpty()) {
1388                 break;
1389             }
1390             // For each dirty region, we will check to see if this child
1391             // intersects with the dirty region and whether it contains the
1392             // dirty region. Note however, that we only care to mark those
1393             // child nodes which are inside a group that intersects. We don't
1394             // care about marking child nodes which are within a parent which
1395             // is wholly contained within the dirty region.
1396             if ((cullingRegionsBitsOfParent == -1 || (cullingRegionsBitsOfParent & mask) != 0) &&
1397                     region.intersects(TEMP_RECT_BOUNDS)) {
1398                 int b = DIRTY_REGION_INTERSECTS_NODE_BOUNDS;
1399                 if (region.contains(TEMP_RECT_BOUNDS)) {
1400                     b = DIRTY_REGION_CONTAINS_NODE_BOUNDS;
1401                 }
1402                 cullingBits = cullingBits | (b << (2 * i));
1403             }
1404             mask = mask << 2;
1405         }//for
1406 
1407         // If we are going to cull a node/group that's dirty,
1408         // make sure it's dirty flags are properly cleared.
1409         if (cullingBits == 0 && (dirty != DirtyFlag.CLEAN || childDirty)) {
1410             clearDirtyTree();
1411         }
1412 
1413 //        System.out.printf("%s bits: %s bounds: %s\n",
1414 //            this, Integer.toBinaryString(cullingBits), TEMP_RECT_BOUNDS);
1415     }
1416 
1417     /**
1418      * Fills the given StringBuilder with text representing the structure of the NG graph insofar as dirty
1419      * opts is concerned. Used for debug purposes. This is typically called on the root node. The List of
1420      * roots is the list of dirty roots as determined by successive calls to getRenderRoot for each dirty
1421      * region. The output will be prefixed with a key indicating how to interpret the printout.
1422      *
1423      * @param s A StringBuilder to fill with the output.
1424      * @param roots The list of render roots (may be empty, must not be null).
1425      */
1426     public final void printDirtyOpts(StringBuilder s, List<NGNode> roots) {
1427         s.append("\n*=Render Root\n");
1428         s.append("d=Dirty\n");
1429         s.append("dt=Dirty By Translation\n");
1430         s.append("i=Dirty Region Intersects the NGNode\n");
1431         s.append("c=Dirty Region Contains the NGNode\n");
1432         s.append("ef=Effect Filter\n");
1433         s.append("cf=Cache Filter\n");
1434         s.append("cl=This node is a clip node\n");
1435         s.append("b=Blend mode is set\n");
1436         s.append("or=Opaque Region\n");
1437         printDirtyOpts(s, this, BaseTransform.IDENTITY_TRANSFORM, "", roots);
1438     }
1439 
1440     /**
1441      * Used for debug purposes. Recursively visits all NGNodes and prints those that are possibly part of
1442      * the render operation and annotates each node.
1443      *
1444      * @param s The String builder
1445      * @param node The node that we're printing out information about
1446      * @param tx The transform
1447      * @param prefix Some prefix to put in front of the node output (mostly spacing)
1448      * @param roots The different dirty roots, if any.
1449      */
1450     private final void printDirtyOpts(StringBuilder s, NGNode node, BaseTransform tx, String prefix, List<NGNode> roots) {
1451         if (!node.isVisible() || node.getOpacity() == 0) return;
1452 
1453         BaseTransform copy = tx.copy();
1454         copy = copy.deriveWithConcatenation(node.getTransform());
1455         List<String> stuff = new ArrayList<>();
1456         for (int i=0; i<roots.size(); i++) {
1457             NGNode root = roots.get(i);
1458             if (node == root) stuff.add("*" + i);
1459         }
1460 
1461         if (node.dirty != NGNode.DirtyFlag.CLEAN) {
1462             stuff.add(node.dirty == NGNode.DirtyFlag.DIRTY ? "d" : "dt");
1463         }
1464 
1465         if (node.cullingBits != 0) {
1466             int mask = 0x11;
1467             for (int i=0; i<15; i++) {
1468                 int bits = node.cullingBits & mask;
1469                 if (bits != 0) {
1470                     stuff.add(bits == 1 ? "i" + i : bits == 0 ? "c" + i : "ci" + i);
1471                 }
1472                 mask = mask << 2;
1473             }
1474         }
1475 
1476         if (node.effectFilter != null) stuff.add("ef");
1477         if (node.cacheFilter != null) stuff.add("cf");
1478         if (node.nodeBlendMode != null) stuff.add("b");
1479 
1480         RectBounds opaqueRegion = node.getOpaqueRegion();
1481         if (opaqueRegion != null) {
1482             RectBounds or = new RectBounds();
1483             copy.transform(opaqueRegion, or);
1484             stuff.add("or=" + or.getMinX() + ", " + or.getMinY() + ", " + or.getWidth() + ", " + or.getHeight());
1485         }
1486 
1487         if (stuff.isEmpty()) {
1488             s.append(prefix + node.name + "\n");
1489         } else {
1490             String postfix = " [";
1491             for (int i=0; i<stuff.size(); i++) {
1492                 postfix = postfix + stuff.get(i);
1493                 if (i < stuff.size() - 1) postfix += " ";
1494             }
1495             s.append(prefix + node.name + postfix + "]\n");
1496         }
1497 
1498         if (node.getClipNode() != null) {
1499             printDirtyOpts(s, node.getClipNode(), copy, prefix + "  cl ", roots);
1500         }
1501 
1502         if (node instanceof NGGroup) {
1503             NGGroup g = (NGGroup)node;
1504             for (int i=0; i<g.getChildren().size(); i++) {
1505                 printDirtyOpts(s, g.getChildren().get(i), copy, prefix + "  ", roots);
1506             }
1507         }
1508     }
1509 
1510     /**
1511      * Helper method draws rectangles indicating the overdraw rectangles.
1512      *
1513      * @param tx The scene->parent transform.
1514      * @param pvTx The perspective camera transform.
1515      * @param clipBounds The bounds in scene coordinates
1516      * @param colorBuffer A pixel array where each pixel contains a color indicating how many times
1517      *                    it has been "drawn"
1518      * @param dirtyRegionIndex the index of the dirty region we're gathering information for. This is
1519      *                         needed so we can shift the "painted" field to find out if this node
1520      *                         was drawn in this dirty region.
1521      */
1522     public void drawDirtyOpts(final BaseTransform tx, final GeneralTransform3D pvTx,
1523                               Rectangle clipBounds, int[] colorBuffer, int dirtyRegionIndex) {
1524         if ((painted & (1 << (dirtyRegionIndex * 2))) != 0) {
1525             // Transforming the content bounds (which includes the clip) to screen coordinates
1526             tx.copy().deriveWithConcatenation(getTransform()).transform(contentBounds, TEMP_BOUNDS);
1527             if (pvTx != null) pvTx.transform(TEMP_BOUNDS, TEMP_BOUNDS);
1528             RectBounds bounds = new RectBounds();
1529             TEMP_BOUNDS.flattenInto(bounds);
1530 
1531             // Adjust the bounds so that they are relative to the clip. The colorBuffer is sized
1532             // exactly the same as the clip, and the elements of the colorBuffer represent the
1533             // pixels inside the clip. However the bounds of this node may overlap the clip in
1534             // some manner, so we adjust them such that x, y, w, h will be the adjusted bounds.
1535             assert clipBounds.width * clipBounds.height == colorBuffer.length;
1536             bounds.intersectWith(clipBounds);
1537             int x = (int) bounds.getMinX() - clipBounds.x;
1538             int y = (int) bounds.getMinY() - clipBounds.y;
1539             int w = (int) (bounds.getWidth() + .5);
1540             int h = (int) (bounds.getHeight() + .5);
1541 
1542             if (w == 0 || h == 0) {
1543                 // I would normally say we should never reach this point, as it means something was
1544                 // marked as painted but really couldn't have been.
1545                 return;
1546             }
1547 
1548             // x, y, w, h are 0 based and will fit within the clip, so now we can simply update
1549             // all the pixels that fall within these bounds.
1550             for (int i = y; i < y+h; i++) {
1551                 for (int j = x; j < x+w; j++) {
1552                     final int index = i * clipBounds.width + j;
1553                     int color = colorBuffer[index];
1554 
1555                     // This is kind of a dirty hack. The idea is to show green if 0 or 1
1556                     // times a pixel is drawn, Yellow for 2 or 3 times, and red for more
1557                     // Than that. So I use 0x80007F00 as the first green color, and
1558                     // 0x80008000 as the second green color, but their so close to the same
1559                     // thing you probably won't be able to tell them apart, but I can tell
1560                     // numerically they're different and increment (so I use the colors
1561                     // as my counters).
1562                     if (color == 0) {
1563                         color = 0x8007F00;
1564                     } else if ((painted & (3 << (dirtyRegionIndex * 2))) == 3) {
1565                         switch (color) {
1566                             case 0x80007F00:
1567                                 color = 0x80008000;
1568                                 break;
1569                             case 0x80008000:
1570                                 color = 0x807F7F00;
1571                                 break;
1572                             case 0x807F7F00:
1573                                 color = 0x80808000;
1574                                 break;
1575                             case 0x80808000:
1576                                 color = 0x807F0000;
1577                                 break;
1578                             default:
1579                                 color = 0x80800000;
1580                         }
1581                     }
1582                     colorBuffer[index] = color;
1583                 }
1584             }
1585         }
1586     }
1587 
1588     /***************************************************************************
1589      *                                                                         *
1590      * Identifying render roots                                                *
1591      *                                                                         *
1592      **************************************************************************/
1593     protected static enum RenderRootResult {
1594         /**
1595          * A Node returns NO_RENDER_ROOT when it is not a render root because
1596          * it does not have an opaqueRegion which completely covers the area
1597          * of the clip. Maybe the node is dirty, but outside the dirty region
1598          * that we're currently processing. For an NGGroup, returning
1599          * NO_RENDER_ROOT means that there is no render root (occluder) within
1600          * this entire branch of the tree.
1601          */
1602         NO_RENDER_ROOT,
1603         /**
1604          * A Node returns HAS_RENDER_ROOT when its opaque region completely
1605          * covers the clip. An NGGroup returns HAS_RENDER_ROOT when one of
1606          * its children either returned HAS_RENDER_ROOT or HAS_RENDER_ROOT_AND_IS_CLEAN.
1607          */
1608         HAS_RENDER_ROOT,
1609         /**
1610          * A Node returns HAS_RENDER_ROOT_AND_IS_CLEAN when its opaque region
1611          * completely covers the clip and the Node is, itself, clean. An NGNode
1612          * returns HAS_RENDER_ROOT_AND_IS_CLEAN only if it had a child that
1613          * returned HAS_RENDER_ROOT_AND_IS_CLEAN and none of its children drawn
1614          * above the render root are dirty.
1615          *
1616          * This optimization allows us to recognize situations where perhaps there
1617          * were some dirty nodes, but they are completely covered by an occluder,
1618          * and therefore we don't actually have to draw anything.
1619          */
1620         HAS_RENDER_ROOT_AND_IS_CLEAN,
1621     }
1622 
1623     /**
1624      * Called <strong>after</strong> preCullingBits in order to get the node
1625      * from which we should begin drawing. This is our support for occlusion culling.
1626      * This should only be called on the root node.
1627      *
1628      * If no render root was found, we need to render everything from this root, so the path will contain this node.
1629      * If no rendering is needed (everything dirty is occluded), the path will remain empty
1630      *
1631      * @param path node path to store the node path
1632      */
1633     public final void getRenderRoot(NodePath path, RectBounds dirtyRegion, int cullingIndex,
1634                                     BaseTransform tx, GeneralTransform3D pvTx) {
1635 
1636         // This is the main entry point, make sure to check these inputs for validity
1637         if (path == null || dirtyRegion == null || tx == null || pvTx == null) {
1638             throw new NullPointerException();
1639         }
1640         if (cullingIndex < -1 || cullingIndex > 15) {
1641             throw new IllegalArgumentException("cullingIndex cannot be < -1 or > 15");
1642         }
1643 
1644         // This method must NEVER BE CALLED if the depth buffer is turned on. I don't have a good way to test
1645         // for that because NGNode doesn't have a reference to the scene it is a part of...
1646 
1647         RenderRootResult result = computeRenderRoot(path, dirtyRegion, cullingIndex, tx, pvTx);
1648         if (result == RenderRootResult.NO_RENDER_ROOT) {
1649             // We didn't find any render root, which means that no one node was large enough
1650             // to obscure the entire dirty region (or, possibly, some combination of nodes in an
1651             // NGGroup were not, together, large enough to do the job). So we need to render
1652             // from the root node, which is this node.
1653             path.add(this);
1654         } else if (result == RenderRootResult.HAS_RENDER_ROOT_AND_IS_CLEAN) {
1655             // We've found a render root, and it is clean and everything above it in painter order
1656             // is clean, so actually we have nothing to paint this time around (some stuff must
1657             // have been dirty which is completely occluded by the render root). So we can clear
1658             // the path, which indicates to the caller that nothing needs to be painted.
1659             path.clear();
1660         }
1661     }
1662 
1663     /**
1664      * Searches for the last node that covers all of the specified dirty region with an opaque region,
1665      * in this node's subtree. Such a node can serve as a rendering root as all nodes preceding the node
1666      * will be covered by it.
1667      *
1668      * @param path the NodePath to populate with the path to the render root. Cannot be null.
1669      * @param dirtyRegion the current dirty region. Cannot be null.
1670      * @param cullingIndex index of culling information
1671      * @param tx current transform. Cannot be null.
1672      * @param pvTx current perspective transform. Cannot be null.
1673      * @return The result of visiting this node.
1674      */
1675     RenderRootResult computeRenderRoot(NodePath path, RectBounds dirtyRegion,
1676                                        int cullingIndex, BaseTransform tx, GeneralTransform3D pvTx) {
1677         return computeNodeRenderRoot(path, dirtyRegion, cullingIndex, tx, pvTx);
1678     }
1679 
1680     private static Point2D[] TEMP_POINTS2D_4 =
1681             new Point2D[] { new Point2D(), new Point2D(), new Point2D(), new Point2D() };
1682 
1683     // Whether (px, py) is clockwise or counter-clockwise to a->b
1684     private static int ccw(double px, double py, Point2D a, Point2D b) {
1685         return (int)Math.signum(((b.x - a.x) * (py - a.y)) - (b.y - a.y) * (px - a.x));
1686     }
1687 
1688     private static boolean pointInConvexQuad(double x, double y, Point2D[] rect) {
1689         int ccw01 = ccw(x, y, rect[0], rect[1]);
1690         int ccw12 = ccw(x, y, rect[1], rect[2]);
1691         int ccw23 = ccw(x, y, rect[2], rect[3]);
1692         int ccw31 = ccw(x, y, rect[3], rect[0]);
1693 
1694         // Possible results after this operation:
1695         // 0 -> 0 (0x0)
1696         // 1 -> 1 (0x1)
1697         // -1 -> Integer.MIN_VALUE (0x80000000)
1698         ccw01 ^= (ccw01 >>> 1);
1699         ccw12 ^= (ccw12 >>> 1);
1700         ccw23 ^= (ccw23 >>> 1);
1701         ccw31 ^= (ccw31 >>> 1);
1702 
1703         final int union = ccw01 | ccw12 | ccw23 | ccw31;
1704         // This means all ccw* were either (-1 or 0) or (1 or 0), but not all of them were 0
1705         return union == 0x80000000 || union == 0x1;
1706         // Or alternatively...
1707 //        return (union ^ (union << 31)) < 0;
1708     }
1709 
1710     /**
1711      * Check if this node can serve as rendering root for this dirty region.
1712      *
1713      * @param path the NodePath to populate with the path to the render root. Cannot be null.
1714      * @param dirtyRegion the current dirty region. Cannot be null.
1715      * @param cullingIndex index of culling information, -1 means culling information should not be used
1716      * @param tx current transform. Cannot be null.
1717      * @param pvTx current perspective transform. Cannot be null.
1718      * @return NO_RENDER_ROOT if this node does <em>not</em> have an opaque
1719      *         region that fills the entire dirty region. Returns HAS_RENDER_ROOT
1720      *         if the opaque region fills the dirty region.
1721      */
1722     final RenderRootResult computeNodeRenderRoot(NodePath path, RectBounds dirtyRegion,
1723                                                  int cullingIndex, BaseTransform tx, GeneralTransform3D pvTx) {
1724 
1725         // Nodes outside of the dirty region can be excluded immediately.
1726         // This can be used only if the culling information is provided.
1727         if (cullingIndex != -1) {
1728             final int bits = cullingBits >> (cullingIndex * 2);
1729             if ((bits & DIRTY_REGION_CONTAINS_OR_INTERSECTS_NODE_BOUNDS) == 0x00) {
1730                 return RenderRootResult.NO_RENDER_ROOT;
1731             }
1732         }
1733 
1734         if (!isVisible()) {
1735             return RenderRootResult.NO_RENDER_ROOT;
1736         }
1737 
1738         final RectBounds opaqueRegion = getOpaqueRegion();
1739         if (opaqueRegion == null) return RenderRootResult.NO_RENDER_ROOT;
1740 
1741         final BaseTransform localToParentTx = getTransform();
1742 
1743         BaseTransform localToSceneTx = TEMP_TRANSFORM.deriveWithNewTransform(tx).deriveWithConcatenation(localToParentTx);
1744 
1745         // Now check if the dirty region is fully contained in our opaque region. Suppose the above
1746         // transform included a rotation about Z. In these cases, the transformed
1747         // opaqueRegion might be some non-axis aligned quad. So what we need to do is to check
1748         // that each corner of the dirty region lies within the (potentially rotated) quad
1749         // of the opaqueRegion.
1750         if (checkBoundsInQuad(opaqueRegion, dirtyRegion, localToSceneTx, pvTx)) {
1751             // This node is a render root.
1752             path.add(this);
1753             return isClean() ? RenderRootResult.HAS_RENDER_ROOT_AND_IS_CLEAN : RenderRootResult.HAS_RENDER_ROOT;
1754         }
1755 
1756         return RenderRootResult.NO_RENDER_ROOT;
1757     }
1758 
1759     static boolean checkBoundsInQuad(RectBounds untransformedQuad,
1760             RectBounds innerBounds, BaseTransform tx, GeneralTransform3D pvTx) {
1761 
1762         if (pvTx.isIdentity() && (tx.getType() & ~(BaseTransform.TYPE_TRANSLATION
1763                 | BaseTransform.TYPE_QUADRANT_ROTATION
1764                 | BaseTransform.TYPE_MASK_SCALE)) == 0) {
1765             // If pvTx is identity and there's simple transformation that will result in axis-aligned rectangle,
1766             // we can do a quick test by using bound.contains()
1767             if (tx.isIdentity()) {
1768                 TEMP_BOUNDS.deriveWithNewBounds(untransformedQuad);
1769             } else {
1770                 tx.transform(untransformedQuad, TEMP_BOUNDS);
1771             }
1772 
1773             TEMP_BOUNDS.flattenInto(TEMP_RECT_BOUNDS);
1774 
1775             return TEMP_RECT_BOUNDS.contains(innerBounds);
1776         } else {
1777             TEMP_POINTS2D_4[0].setLocation(untransformedQuad.getMinX(), untransformedQuad.getMinY());
1778             TEMP_POINTS2D_4[1].setLocation(untransformedQuad.getMaxX(), untransformedQuad.getMinY());
1779             TEMP_POINTS2D_4[2].setLocation(untransformedQuad.getMaxX(), untransformedQuad.getMaxY());
1780             TEMP_POINTS2D_4[3].setLocation(untransformedQuad.getMinX(), untransformedQuad.getMaxY());
1781 
1782             for (Point2D p : TEMP_POINTS2D_4) {
1783                 tx.transform(p, p);
1784                 if (!pvTx.isIdentity()) {
1785                     pvTx.transform(p, p);
1786                 }
1787             }
1788 
1789             return (pointInConvexQuad(innerBounds.getMinX(), innerBounds.getMinY(), TEMP_POINTS2D_4)
1790                     && pointInConvexQuad(innerBounds.getMaxX(), innerBounds.getMinY(), TEMP_POINTS2D_4)
1791                     && pointInConvexQuad(innerBounds.getMaxX(), innerBounds.getMaxY(), TEMP_POINTS2D_4)
1792                     && pointInConvexQuad(innerBounds.getMinX(), innerBounds.getMaxY(), TEMP_POINTS2D_4));
1793         }
1794     }
1795 
1796     /**
1797      * Invalidates any cached representation of the opaque region for this node. On the next
1798      * call to getOpaqueRegion, the opaque region will be recalculated. Any changes to state
1799      * which is used in the {@link #hasOpaqueRegion()} call must invoke this method
1800      * or the opaque region calculations will be wrong.
1801      */
1802     protected final void invalidateOpaqueRegion() {
1803         opaqueRegionInvalid = true;
1804         if (isClip) parent.invalidateOpaqueRegion();
1805     }
1806 
1807     /**
1808      * This method exists only for the sake of testing.
1809      * @return value of opaqueRegionInvalid
1810      */
1811     final boolean isOpaqueRegionInvalid() {
1812         return opaqueRegionInvalid;
1813     }
1814 
1815     /**
1816      * Gets the opaque region for this node, if there is one, or returns null.
1817      * @return The opaque region for this node, or null.
1818      */
1819     public final RectBounds getOpaqueRegion() {
1820         // Note that when we invalidate the opaqueRegion of an NGNode, we don't
1821         // walk up the tree or communicate with the parents (unlike dirty flags).
1822         // An NGGroup does not compute an opaqueRegion based on the union of opaque
1823         // regions of its children (although this is a fine idea to consider!). See RT-32441
1824         // If we ever fix RT-32441, we must be sure to handle the case of a Group being used
1825         // as a clip node (such that invalidating a child on the group invalidates the
1826         // opaque region of every node up to the root).
1827 
1828         // Because the Effect classes have no reference to NGNode, they cannot tell the
1829         // NGNode to invalidate the opaque region whenever properties on the Effect that
1830         // would impact the opaqueRegion change. As a result, when an Effect is specified
1831         // on the NGNode, we will always treat it as if it were invalid. A more invasive
1832         // (but better) change would be to give Effect the ability to invalidate the
1833         // NGNode's opaque region when needed.
1834         if (opaqueRegionInvalid || getEffect() != null) {
1835             opaqueRegionInvalid = false;
1836             if (supportsOpaqueRegions() && hasOpaqueRegion()) {
1837                 opaqueRegion = computeOpaqueRegion(opaqueRegion == null ? new RectBounds() : opaqueRegion);
1838                 // If we got a null result then we encountered an error condition where somebody
1839                 // claimed supportsOpaqueRegions and hasOpaqueRegion, but then they
1840                 // returned null! This should never happen, so we have an assert here. However since
1841                 // assertions are disabled at runtime and we want to avoid the NPE, we also perform
1842                 // a null check.
1843                 assert opaqueRegion != null;
1844                 if (opaqueRegion == null) {
1845                     return null;
1846                 }
1847                 // If there is a clip, then we need to determine the opaque region of the clip, and
1848                 // intersect that with our existing opaque region. For example, if I had a rectangle
1849                 // with a circle for its clip (centered over the rectangle), then the result needs to
1850                 // be the circle's opaque region.
1851                 final NGNode clip = getClipNode();
1852                 if (clip != null) {
1853                     final RectBounds clipOpaqueRegion = clip.getOpaqueRegion();
1854                     // Technically a flip/quadrant rotation is allowed as well, but we don't have a convenient
1855                     // way to do that yet.
1856                     if (clipOpaqueRegion == null || (clip.getTransform().getType() & ~(BaseTransform.TYPE_TRANSLATION | BaseTransform.TYPE_MASK_SCALE)) != 0) {
1857                         // RT-25095: If this node has a clip who's opaque region cannot be determined, then
1858                         // we cannot determine any opaque region for this node (in fact, it might not have one).
1859                         // Also, if the transform is something other than identity, scale, or translate then
1860                         // we're just going to bail (sorry, rotate, maybe next time!)
1861                         return opaqueRegion = null;
1862                     }
1863                     // We have to take into account any transform specified on the clip to put
1864                     // it into the same coordinate system as this node
1865                     final BaseBounds b = clip.getTransform().transform(clipOpaqueRegion, TEMP_BOUNDS);
1866                     b.flattenInto(TEMP_RECT_BOUNDS);
1867                     opaqueRegion.intersectWith(TEMP_RECT_BOUNDS);
1868 
1869                 }
1870             } else {
1871                 // The opaqueRegion may have been non-null in the past, but there isn't an opaque region now,
1872                 // so we will nuke it to save some memory
1873                 opaqueRegion = null;
1874             }
1875         }
1876 
1877         return opaqueRegion;
1878     }
1879 
1880     /**
1881      * Gets whether this NGNode supports opaque regions at all. Most node types do not,
1882      * but some do. If an NGNode subclass is written to support opaque regions, it must override
1883      * this method to return true. The subclass must then also override the computeDirtyRegion method
1884      * to return the dirty region, or null if the node in its current state doesn't have one.
1885      * This method is intended to be immutable.
1886      *
1887      * @return Whether this NGNode implementation supports opaque regions. This could also have been
1888      *         implemented via an interface that some NGNodes implemented, but then we'd have instanceof
1889      *         checks which I'd rather avoid.
1890      */
1891     protected boolean supportsOpaqueRegions() { return false; }
1892 
1893     /**
1894      * Called only on NGNode subclasses which override {@link #supportsOpaqueRegions()} to return
1895      * true, this method will return whether or not this NGNode is in a state where it has
1896      * an opaque region to actually return. If this method returns true, a subsequent call to
1897      * {@link #computeOpaqueRegion(com.sun.javafx.geom.RectBounds)} <strong>must</strong> return
1898      * a non-null result. Any state used in the computation of this method, when it changes, must
1899      * result in a call to {@link #invalidateOpaqueRegion()}.
1900      *
1901      * @return Whether this NGNode currently has an opaque region.
1902      */
1903     protected boolean hasOpaqueRegion() {
1904         final NGNode clip = getClipNode();
1905         final Effect effect = getEffect();
1906         return (effect == null || !effect.reducesOpaquePixels()) &&
1907                getOpacity() == 1f &&
1908                (nodeBlendMode == null || nodeBlendMode == Blend.Mode.SRC_OVER) &&
1909                (clip == null ||
1910                (clip.supportsOpaqueRegions() && clip.hasOpaqueRegion()));
1911     }
1912 
1913     /**
1914      * Computes and returns the opaque region for this node. This method
1915      * @param opaqueRegion
1916      * @return
1917      */
1918     protected RectBounds computeOpaqueRegion(RectBounds opaqueRegion) {
1919         return null;
1920     }
1921 
1922     /**
1923      * Returns whether a clip represented by this node can be rendered using
1924      * axis aligned rect clip. The default implementation returns false,
1925      * specific subclasses should override to return true when appropriate.
1926      *
1927      * @return whether this rectangle is axis aligned when rendered given node's
1928      * and rendering transform
1929      */
1930     protected boolean isRectClip(BaseTransform xform, boolean permitRoundedRectangle) {
1931         return false;
1932     }
1933 
1934     /***************************************************************************
1935      *                                                                         *
1936      * Rendering                                                               *
1937      *                                                                         *
1938      **************************************************************************/
1939 
1940     /**
1941      * Render the tree of nodes to the specified G (graphics) object
1942      * descending from this node as the root. This method is designed to avoid
1943      * generated trash as much as possible while descending through the
1944      * render graph while rendering. This is the appropriate method both to
1945      * initiate painting of an entire scene, and for a branch. The NGGroup
1946      * implementation must call this method on each child, not doRender directly.
1947      *
1948      * @param g The graphics object we're rendering to. This must never be null.
1949      */
1950     public final void render(Graphics g) {
1951         if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("Nodes visited during render");
1952         // Clear the visuals changed flag
1953         clearDirty();
1954         // If it isn't visible, then punt
1955         if (!visible || opacity == 0f) return;
1956 
1957         // We know that we are going to render this node, so we call the
1958         // doRender method, which subclasses implement to do the actual
1959         // rendering work.
1960         doRender(g);
1961     }
1962 
1963     // This node requires 2D graphics state for rendering
1964     boolean isShape3D() {
1965         return false;
1966     }
1967 
1968     /**
1969      * Invoked only by the final render method. Implementations
1970      * of this method should make sure to save & restore the transform state.
1971      */
1972     protected void doRender(Graphics g) {
1973 
1974         g.setState3D(isShape3D());
1975 
1976         boolean preCullingTurnedOff = false;
1977         if (PrismSettings.dirtyOptsEnabled) {
1978             if (g.hasPreCullingBits()) {
1979                 //preculling bits available
1980                 final int bits = cullingBits >> (g.getClipRectIndex() * 2);
1981                 if ((bits & DIRTY_REGION_CONTAINS_OR_INTERSECTS_NODE_BOUNDS) == 0) {
1982                     // If no culling bits are set for this region, this group
1983                     // does not intersect (nor is covered by) the region
1984                     return;
1985                 } else if ((bits & DIRTY_REGION_CONTAINS_NODE_BOUNDS) != 0) {
1986                     // When this group is fully covered by the region,
1987                     // turn off the culling checks in the subtree, as everything
1988                     // gets rendered
1989                     g.setHasPreCullingBits(false);
1990                     preCullingTurnedOff = true;
1991                 }
1992             }
1993         }
1994 
1995         // save current depth test state
1996         boolean prevDepthTest = g.isDepthTest();
1997 
1998         // Apply Depth test for this node
1999         // (note that this will only be used if we have a depth buffer for the
2000         // surface to which we are rendering)
2001         g.setDepthTest(isDepthTest());
2002 
2003         // save current transform state
2004         BaseTransform prevXform = g.getTransformNoClone();
2005 
2006         double mxx = prevXform.getMxx();
2007         double mxy = prevXform.getMxy();
2008         double mxz = prevXform.getMxz();
2009         double mxt = prevXform.getMxt();
2010 
2011         double myx = prevXform.getMyx();
2012         double myy = prevXform.getMyy();
2013         double myz = prevXform.getMyz();
2014         double myt = prevXform.getMyt();
2015 
2016         double mzx = prevXform.getMzx();
2017         double mzy = prevXform.getMzy();
2018         double mzz = prevXform.getMzz();
2019         double mzt = prevXform.getMzt();
2020 
2021         // filters are applied in the following order:
2022         //   transform
2023         //   blend mode
2024         //   opacity
2025         //   cache
2026         //   clip
2027         //   effect
2028         // The clip must be below the cache filter, as this is expected in the
2029         // CacheFilter in order to apply scrolling optimization
2030         g.transform(getTransform());
2031         // Try to keep track of whether this node was *really* painted. Still an
2032         // approximation, but somewhat more accurate (at least it doesn't include
2033         // groups which don't paint anything themselves).
2034         boolean p = false;
2035         // NOTE: Opt out 2D operations on 3D Shapes, which are not yet handled by Prism
2036         if (!isShape3D() && g instanceof ReadbackGraphics && needsBlending()) {
2037             renderNodeBlendMode(g);
2038             p = true;
2039         } else if (!isShape3D() && getOpacity() < 1f) {
2040             renderOpacity(g);
2041             p = true;
2042         } else if (!isShape3D() && getCacheFilter() != null) {
2043             renderCached(g);
2044             p = true;
2045         } else if (!isShape3D() && getClipNode() != null) {
2046             renderClip(g);
2047             p = true;
2048         } else if (!isShape3D() && getEffectFilter() != null && effectsSupported) {
2049             renderEffect(g);
2050             p = true;
2051         } else {
2052             renderContent(g);
2053             if (PrismSettings.showOverdraw) {
2054                 p = this instanceof NGRegion || !(this instanceof NGGroup);
2055             }
2056         }
2057 
2058         if (preCullingTurnedOff) {
2059             g.setHasPreCullingBits(true);
2060         }
2061 
2062         // restore previous transform state
2063         g.setTransform3D(mxx, mxy, mxz, mxt,
2064                          myx, myy, myz, myt,
2065                          mzx, mzy, mzz, mzt);
2066 
2067         // restore previous depth test state
2068         g.setDepthTest(prevDepthTest);
2069 
2070         if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("Nodes rendered");
2071 
2072         // Used for debug purposes. This is not entirely accurate, as it doesn't measure the
2073         // number of times this node drew to the pixels, and in some cases reports a node as
2074         // having been drawn even when it didn't lay down any pixels. We'd need to integrate
2075         // with our shaders or do something much more invasive to get better data here.
2076         if (PrismSettings.showOverdraw) {
2077             if (p) {
2078                 painted |= 3 << (g.getClipRectIndex() * 2);
2079             } else {
2080                 painted |= 1 << (g.getClipRectIndex() * 2);
2081             }
2082         }
2083     }
2084 
2085     /**
2086      * Return true if this node has a blend mode that requires special
2087      * processing.
2088      * Regular nodes can handle null or SRC_OVER just by rendering into
2089      * the existing buffer.
2090      * Groups override this since they must collect their children into
2091      * a single rendering pass if their mode is explicitly SRC_OVER.
2092      * @return true if this node needs special blending support
2093      */
2094     protected boolean needsBlending() {
2095         Blend.Mode mode = getNodeBlendMode();
2096         return (mode != null && mode != Blend.Mode.SRC_OVER);
2097     }
2098 
2099     private void renderNodeBlendMode(Graphics g) {
2100         // The following is safe; curXform will not be mutated below
2101         BaseTransform curXform = g.getTransformNoClone();
2102 
2103         BaseBounds clipBounds = getClippedBounds(new RectBounds(), curXform);
2104         if (clipBounds.isEmpty()) {
2105             clearDirtyTree();
2106             return;
2107         }
2108 
2109         if (!isReadbackSupported(g)) {
2110             if (getOpacity() < 1f) {
2111                 renderOpacity(g);
2112             } else if (getClipNode() != null) {
2113                 renderClip(g);
2114             } else {
2115                 renderContent(g);
2116             }
2117             return;
2118         }
2119 
2120         // TODO: optimize this (RT-26936)
2121         // Extract clip bounds
2122         Rectangle clipRect = new Rectangle(clipBounds);
2123         clipRect.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g));
2124 
2125         // render the node content into the first offscreen image
2126         FilterContext fctx = getFilterContext(g);
2127         PrDrawable contentImg = (PrDrawable)
2128             Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height);
2129         if (contentImg == null) {
2130             clearDirtyTree();
2131             return;
2132         }
2133         Graphics gContentImg = contentImg.createGraphics();
2134         gContentImg.setHasPreCullingBits(g.hasPreCullingBits());
2135         gContentImg.setClipRectIndex(g.getClipRectIndex());
2136         gContentImg.translate(-clipRect.x, -clipRect.y);
2137         gContentImg.transform(curXform);
2138         if (getOpacity() < 1f) {
2139             renderOpacity(gContentImg);
2140         } else if (getCacheFilter() != null) {
2141             renderCached(gContentImg);
2142         } else if (getClipNode() != null) {
2143             renderClip(g);
2144         } else if (getEffectFilter() != null) {
2145             renderEffect(gContentImg);
2146         } else {
2147             renderContent(gContentImg);
2148         }
2149 
2150         // the above image has already been rendered in device space, so
2151         // just translate to the node origin in device space here...
2152         RTTexture bgRTT = ((ReadbackGraphics) g).readBack(clipRect);
2153         PrDrawable bgPrD = PrDrawable.create(fctx, bgRTT);
2154         Blend blend = new Blend(getNodeBlendMode(),
2155                                 new PassThrough(bgPrD, clipRect),
2156                                 new PassThrough(contentImg, clipRect));
2157         CompositeMode oldmode = g.getCompositeMode();
2158         g.setTransform(null);
2159         g.setCompositeMode(CompositeMode.SRC);
2160         PrEffectHelper.render(blend, g, 0, 0, null);
2161         g.setCompositeMode(oldmode);
2162         // transform state will be restored in render() method above...
2163 
2164         Effect.releaseCompatibleImage(fctx, contentImg);
2165         ((ReadbackGraphics) g).releaseReadBackBuffer(bgRTT);
2166     }
2167 
2168     private void renderRectClip(Graphics g, NGRectangle clipNode) {
2169         BaseBounds newClip = clipNode.getShape().getBounds();
2170         if (!clipNode.getTransform().isIdentity()) {
2171             newClip = clipNode.getTransform().transform(newClip, newClip);
2172         }
2173         final BaseTransform curXform = g.getTransformNoClone();
2174         final Rectangle curClip = g.getClipRectNoClone();
2175         newClip = curXform.transform(newClip, newClip);
2176         newClip.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g));
2177         if (newClip.isEmpty() ||
2178             newClip.getWidth() == 0 ||
2179             newClip.getHeight() == 0) {
2180             clearDirtyTree();
2181             return;
2182         }
2183         // REMIND: avoid garbage by changing setClipRect to accept xywh
2184         g.setClipRect(new Rectangle(newClip));
2185         renderForClip(g);
2186         g.setClipRect(curClip);
2187         clipNode.clearDirty(); // as render() is not called on the clipNode,
2188                                // make sure the dirty flags are cleared
2189     }
2190 
2191     void renderClip(Graphics g) {
2192         //  if clip's opacity is 0 there's nothing to render
2193         if (getClipNode().getOpacity() == 0.0) {
2194             clearDirtyTree();
2195             return;
2196         }
2197 
2198         // The following is safe; curXform will not be mutated below
2199         BaseTransform curXform = g.getTransformNoClone();
2200 
2201         BaseBounds clipBounds = getClippedBounds(new RectBounds(), curXform);
2202         if (clipBounds.isEmpty()) {
2203             clearDirtyTree();
2204             return;
2205         }
2206 
2207         if (getClipNode() instanceof NGRectangle) {
2208             // optimized case for rectangular clip
2209             NGRectangle rectNode = (NGRectangle)getClipNode();
2210             if (rectNode.isRectClip(curXform, false)) {
2211                 renderRectClip(g, rectNode);
2212                 return;
2213             }
2214         }
2215 
2216         // TODO: optimize this (RT-26936)
2217         // Extract clip bounds
2218         Rectangle clipRect = new Rectangle(clipBounds);
2219         clipRect.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g));
2220 
2221         if (!curXform.is2D()) {
2222             Rectangle savedClip = g.getClipRect();
2223             g.setClipRect(clipRect);
2224             NodeEffectInput clipInput =
2225                 new NodeEffectInput(getClipNode(),
2226                                     NodeEffectInput.RenderType.FULL_CONTENT);
2227             NodeEffectInput nodeInput =
2228                 new NodeEffectInput(this,
2229                                     NodeEffectInput.RenderType.CLIPPED_CONTENT);
2230             Blend blend = new Blend(Blend.Mode.SRC_IN, clipInput, nodeInput);
2231             PrEffectHelper.render(blend, g, 0, 0, null);
2232             clipInput.flush();
2233             nodeInput.flush();
2234             g.setClipRect(savedClip);
2235             // There may have been some errors in the application of the
2236             // effect and we would not know to what extent the nodes were
2237             // rendered and cleared or left dirty.  clearDirtyTree() will
2238             // clear both this node its clip node, and it will not recurse
2239             // to the children unless they are still marked dirty.  It should
2240             // be cheap if there was no problem and thorough if there was...
2241             clearDirtyTree();
2242             return;
2243         }
2244 
2245         // render the node content into the first offscreen image
2246         FilterContext fctx = getFilterContext(g);
2247         PrDrawable contentImg = (PrDrawable)
2248             Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height);
2249         if (contentImg == null) {
2250             clearDirtyTree();
2251             return;
2252         }
2253         Graphics gContentImg = contentImg.createGraphics();
2254         gContentImg.setExtraAlpha(g.getExtraAlpha());
2255         gContentImg.setHasPreCullingBits(g.hasPreCullingBits());
2256         gContentImg.setClipRectIndex(g.getClipRectIndex());
2257         gContentImg.translate(-clipRect.x, -clipRect.y);
2258         gContentImg.transform(curXform);
2259         renderForClip(gContentImg);
2260 
2261         // render the mask (clipNode) into the second offscreen image
2262         PrDrawable clipImg = (PrDrawable)
2263             Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height);
2264         if (clipImg == null) {
2265             getClipNode().clearDirtyTree();
2266             Effect.releaseCompatibleImage(fctx, contentImg);
2267             return;
2268         }
2269         Graphics gClipImg = clipImg.createGraphics();
2270         gClipImg.translate(-clipRect.x, -clipRect.y);
2271         gClipImg.transform(curXform);
2272         getClipNode().render(gClipImg);
2273 
2274         // the above images have already been rendered in device space, so
2275         // just translate to the node origin in device space here...
2276         g.setTransform(null);
2277         Blend blend = new Blend(Blend.Mode.SRC_IN,
2278                                 new PassThrough(clipImg, clipRect),
2279                                 new PassThrough(contentImg, clipRect));
2280         PrEffectHelper.render(blend, g, 0, 0, null);
2281         // transform state will be restored in render() method above...
2282 
2283         Effect.releaseCompatibleImage(fctx, contentImg);
2284         Effect.releaseCompatibleImage(fctx, clipImg);
2285     }
2286 
2287     void renderForClip(Graphics g) {
2288         if (getEffectFilter() != null) {
2289             renderEffect(g);
2290         } else {
2291             renderContent(g);
2292         }
2293     }
2294 
2295     private void renderOpacity(Graphics g) {
2296         if (getEffectFilter() != null ||
2297             getCacheFilter() != null ||
2298             getClipNode() != null ||
2299             !hasOverlappingContents())
2300         {
2301             // if the node has a non-null effect or cached==true, we don't
2302             // need to bother rendering to an offscreen here because the
2303             // contents will be flattened as part of rendering the effect
2304             // (or creating the cached image)
2305             float ea = g.getExtraAlpha();
2306             g.setExtraAlpha(ea*getOpacity());
2307             if (getCacheFilter() != null) {
2308                 renderCached(g);
2309             } else if (getClipNode() != null) {
2310                 renderClip(g);
2311             } else if (getEffectFilter() != null) {
2312                 renderEffect(g);
2313             } else {
2314                 renderContent(g);
2315             }
2316             g.setExtraAlpha(ea);
2317             return;
2318         }
2319 
2320         FilterContext fctx = getFilterContext(g);
2321         BaseTransform curXform = g.getTransformNoClone();
2322         BaseBounds bounds = getContentBounds(new RectBounds(), curXform);
2323         Rectangle r = new Rectangle(bounds);
2324         r.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g));
2325         PrDrawable img = (PrDrawable)
2326             Effect.getCompatibleImage(fctx, r.width, r.height);
2327         if (img == null) {
2328             return;
2329         }
2330         Graphics gImg = img.createGraphics();
2331         gImg.setHasPreCullingBits(g.hasPreCullingBits());
2332         gImg.setClipRectIndex(g.getClipRectIndex());
2333         gImg.translate(-r.x, -r.y);
2334         gImg.transform(curXform);
2335         renderContent(gImg);
2336         // img contents have already been rendered in device space, so
2337         // just translate to the node origin in device space here...
2338         g.setTransform(null);
2339         float ea = g.getExtraAlpha();
2340         g.setExtraAlpha(getOpacity()*ea);
2341         g.drawTexture(img.getTextureObject(), r.x, r.y, r.width, r.height);
2342         g.setExtraAlpha(ea);
2343         // transform state will be restored in render() method above...
2344         Effect.releaseCompatibleImage(fctx, img);
2345     }
2346 
2347     private void renderCached(Graphics g) {
2348         // We will punt on 3D completely for caching.
2349         // The first check is for any of its children contains a 3D Transform.
2350         // The second check is for any of its parents and itself has a 3D Transform
2351         // The third check is for the printing case, which doesn't use cached
2352         // bitmaps for the screen and for which there is no cacheFilter.
2353         if (isContentBounds2D() && g.getTransformNoClone().is2D() &&
2354                 !(g instanceof com.sun.prism.PrinterGraphics)) {
2355             getCacheFilter().render(g);
2356         } else {
2357             renderContent(g);
2358         }
2359     }
2360 
2361     protected void renderEffect(Graphics g) {
2362         getEffectFilter().render(g);
2363     }
2364 
2365     protected abstract void renderContent(Graphics g);
2366 
2367     protected abstract boolean hasOverlappingContents();
2368 
2369     /***************************************************************************
2370      *                                                                         *
2371      *                       Static Helper Methods.                            *
2372      *                                                                         *
2373      **************************************************************************/
2374 
2375     boolean isReadbackSupported(Graphics g) {
2376         return ((g instanceof ReadbackGraphics) &&
2377                 ((ReadbackGraphics) g).canReadBack());
2378     }
2379 
2380     /***************************************************************************
2381      *                                                                         *
2382      *                      Filters (Cache, Effect, etc).                      *
2383      *                                                                         *
2384      **************************************************************************/
2385 
2386     static FilterContext getFilterContext(Graphics g) {
2387         Screen s = g.getAssociatedScreen();
2388         if (s == null) {
2389             return PrFilterContext.getPrinterContext(g.getResourceFactory());
2390         } else {
2391             return PrFilterContext.getInstance(s);
2392         }
2393     }
2394 
2395     /**
2396      * A custom effect implementation that has a filter() method that
2397      * simply wraps the given pre-rendered PrDrawable in an ImageData
2398      * and returns that result.  This is only used by the renderClip()
2399      * implementation so we cut some corners here (for example, we assume
2400      * that the given PrDrawable image is already in device space).
2401      */
2402     private static class PassThrough extends Effect {
2403         private PrDrawable img;
2404         private Rectangle bounds;
2405 
2406         PassThrough(PrDrawable img, Rectangle bounds) {
2407             this.img = img;
2408             this.bounds = bounds;
2409         }
2410 
2411         @Override
2412         public ImageData filter(FilterContext fctx,
2413                                 BaseTransform transform,
2414                                 Rectangle outputClip,
2415                                 Object renderHelper,
2416                                 Effect defaultInput)
2417         {
2418             return new ImageData(fctx, img, new Rectangle(bounds));
2419         }
2420 
2421         @Override
2422         public RectBounds getBounds(BaseTransform transform,
2423                                   Effect defaultInput)
2424         {
2425             return new RectBounds(bounds);
2426         }
2427 
2428         @Override
2429         public AccelType getAccelType(FilterContext fctx) {
2430             return AccelType.INTRINSIC;
2431         }
2432 
2433         @Override
2434         public boolean reducesOpaquePixels() {
2435             return false;
2436         }
2437 
2438         @Override
2439         public DirtyRegionContainer getDirtyRegions(Effect defaultInput, DirtyRegionPool regionPool) {
2440             return null; //Never called
2441         }
2442     }
2443 
2444     /***************************************************************************
2445      *                                                                         *
2446      * Stuff                                                                   *
2447      *                                                                         *
2448      **************************************************************************/
2449 
2450     public void release() {
2451     }
2452 
2453     @Override public String toString() {
2454         return name == null ? super.toString() : name;
2455     }
2456 
2457     public void applyTransform(final BaseTransform tx, DirtyRegionContainer drc) {
2458         for (int i = 0; i < drc.size(); i++) {
2459             drc.setDirtyRegion(i, (RectBounds) tx.transform(drc.getDirtyRegion(i), drc.getDirtyRegion(i)));
2460             if (drc.checkAndClearRegion(i)) {
2461                 --i;
2462             }
2463         }
2464     }
2465 
2466     public void applyClip(final BaseBounds clipBounds, DirtyRegionContainer drc) {
2467         for (int i = 0; i < drc.size(); i++) {
2468             drc.getDirtyRegion(i).intersectWith(clipBounds);
2469             if (drc.checkAndClearRegion(i)) {
2470                 --i;
2471             }
2472         }
2473     }
2474 
2475     public void applyEffect(final EffectFilter effectFilter, DirtyRegionContainer drc, DirtyRegionPool regionPool) {
2476         Effect effect = effectFilter.getEffect();
2477         EffectDirtyBoundsHelper helper = EffectDirtyBoundsHelper.getInstance();
2478         helper.setInputBounds(contentBounds);
2479         helper.setDirtyRegions(drc);
2480         final DirtyRegionContainer effectDrc = effect.getDirtyRegions(helper, regionPool);
2481         drc.deriveWithNewContainer(effectDrc);
2482         regionPool.checkIn(effectDrc);
2483     }
2484 
2485     private static class EffectDirtyBoundsHelper extends Effect {
2486         private BaseBounds bounds;
2487         private static EffectDirtyBoundsHelper instance = null;
2488         private DirtyRegionContainer drc;
2489 
2490         public void setInputBounds(BaseBounds inputBounds) {
2491             bounds = inputBounds;
2492         }
2493 
2494         @Override
2495         public ImageData filter(FilterContext fctx,
2496                 BaseTransform transform,
2497                 Rectangle outputClip,
2498                 Object renderHelper,
2499                 Effect defaultInput) {
2500             throw new UnsupportedOperationException();
2501         }
2502 
2503         @Override
2504         public BaseBounds getBounds(BaseTransform transform, Effect defaultInput) {
2505             if (bounds.getBoundsType() == BaseBounds.BoundsType.RECTANGLE) {
2506                 return bounds;
2507             } else {
2508                 //RT-29453 - CCE: in case we get 3D bounds we need to "flatten" them
2509                 return new RectBounds(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY());
2510             }
2511         }
2512 
2513         @Override
2514         public Effect.AccelType getAccelType(FilterContext fctx) {
2515             return null;
2516         }
2517 
2518         public static EffectDirtyBoundsHelper getInstance() {
2519             if (instance == null) {
2520                 instance = new EffectDirtyBoundsHelper();
2521             }
2522             return instance;
2523         }
2524 
2525         @Override
2526         public boolean reducesOpaquePixels() {
2527             return true;
2528         }
2529 
2530         private void setDirtyRegions(DirtyRegionContainer drc) {
2531             this.drc = drc;
2532         }
2533 
2534         @Override
2535         public DirtyRegionContainer getDirtyRegions(Effect defaultInput, DirtyRegionPool regionPool) {
2536             DirtyRegionContainer ret = regionPool.checkOut();
2537             ret.deriveWithNewContainer(drc);
2538 
2539             return ret;
2540         }
2541 
2542     }
2543 }