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