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