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