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