/* * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javafx.sg.prism; import com.sun.glass.ui.Screen; import com.sun.javafx.geom.BaseBounds; import com.sun.javafx.geom.BoxBounds; import com.sun.javafx.geom.DirtyRegionContainer; import com.sun.javafx.geom.DirtyRegionPool; import com.sun.javafx.geom.Point2D; import com.sun.javafx.geom.RectBounds; import com.sun.javafx.geom.Rectangle; import com.sun.javafx.geom.transform.Affine3D; import com.sun.javafx.geom.transform.BaseTransform; import com.sun.javafx.geom.transform.GeneralTransform3D; import com.sun.javafx.geom.transform.NoninvertibleTransformException; import com.sun.prism.CompositeMode; import com.sun.prism.Graphics; import com.sun.prism.GraphicsPipeline; import com.sun.prism.RTTexture; import com.sun.prism.ReadbackGraphics; import com.sun.prism.impl.PrismSettings; import com.sun.scenario.effect.Blend; import com.sun.scenario.effect.Effect; import com.sun.scenario.effect.FilterContext; import com.sun.scenario.effect.ImageData; import com.sun.scenario.effect.impl.prism.PrDrawable; import com.sun.scenario.effect.impl.prism.PrEffectHelper; import com.sun.scenario.effect.impl.prism.PrFilterContext; import javafx.scene.CacheHint; import java.util.ArrayList; import java.util.List; import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGER; import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; /** * NGNode is the abstract base class peer of Node, forming * the basis for Prism and Scenario render graphs. *

* During synchronization, the FX scene graph will pass down to us * the transform which takes us from local space to parent space, the * content bounds (ie: geom bounds), and the transformed bounds * (ie: boundsInParent), and the clippedBounds. The effect bounds have * already been passed to the Effect peer (if there is one). *

* Whenever the transformedBounds of the NGNode are changed, we update * the dirtyBounds, so that the next time we need to accumulate dirty * regions, we will have the information we need to make sure we create * an appropriate dirty region. *

* NGNode maintains a single "dirty" flag, which indicates that this * node itself is dirty and must contribute to the dirty region. More * specifically, it indicates that this node is now dirty with respect * to the back buffer. Any rendering of the scene which will go on the * back buffer will cause the dirty flag to be cleared, whereas a * rendering of the scene which is for an intermediate image will not * clear this dirty flag. */ public abstract class NGNode { protected static float highestPixelScale; static { // TODO: temporary until RT-27958 is fixed. Screens may be null or could be not initialized // when running unit tests try { for (Screen s : Screen.getScreens()) { highestPixelScale = Math.max(s.getScale(), highestPixelScale); } } catch (RuntimeException ex) { System.err.println("WARNING: unable to get max pixel scale for screens"); highestPixelScale = 1.0f; } } private final static GraphicsPipeline pipeline = GraphicsPipeline.getPipeline(); private final static Boolean effectsSupported = (pipeline == null ? false : pipeline.isEffectSupported()); public static enum DirtyFlag { CLEAN, // Means that the node is dirty, but only because of translation DIRTY_BY_TRANSLATION, DIRTY } /** * Used for debug purposes. Set during sync. */ private String name; /** * Temporary bounds for use by this class or subclasses, designed to * reduce the amount of garbage we generate. If we get to the point * where we have multi-threaded rasterization, we might need to make * this per-instance instead of static. */ private static final BoxBounds TEMP_BOUNDS = new BoxBounds(); private static final RectBounds TEMP_RECT_BOUNDS = new RectBounds(); protected static final Affine3D TEMP_TRANSFORM = new Affine3D(); /** * Statics for defining what the culling bits are. We use 2 bits to * determine culling status */ static final int DIRTY_REGION_INTERSECTS_NODE_BOUNDS = 0x1; static final int DIRTY_REGION_CONTAINS_NODE_BOUNDS = 0x2; static final int DIRTY_REGION_CONTAINS_OR_INTERSECTS_NODE_BOUNDS = DIRTY_REGION_INTERSECTS_NODE_BOUNDS | DIRTY_REGION_CONTAINS_NODE_BOUNDS; /** * The transform for this node. Although we are handed all the bounds * during synchronization (including the transformed bounds), we still * need the transform so that we can apply it to the clip and so forth * while accumulating dirty regions and rendering. */ private BaseTransform transform = BaseTransform.IDENTITY_TRANSFORM; /** * The cached transformed bounds. This is never null, but is frequently set * to be invalid whenever the bounds for the node have changed. These are * "complete" bounds, that is, with transforms and effect and clip applied. * Note that this is equivalent to boundsInParent in FX. */ protected BaseBounds transformedBounds = new RectBounds(); /** * The cached bounds. This is never null, but is frequently set to be * invalid whenever the bounds for the node have changed. These are the * "content" bounds, that is, without transforms or filters applied. */ protected BaseBounds contentBounds = new RectBounds(); /** * We keep a reference to the last transform bounds that were valid * and known. We do this to significantly speed up the rendering of the * scene by culling and clipping based on "dirty" regions, which are * essentially the rectangle formed by the union of the dirtyBounds * and the transformedBounds. */ BaseBounds dirtyBounds = new RectBounds(); /** * Whether the node is visible. We need to know about the visibility of * the node so that we can determine whether to cull it out, and perform * other such optimizations. */ private boolean visible = true; /** * Indicates that this NGNode is itself dirty and needs its full bounds * included in the next repaint. This means it is dirty with respect to * the back buffer. We don't bother differentiating between bounds dirty * and visuals dirty because we can simply inspect the dirtyBounds to * see if it is valid. If so, then bounds must be dirty. */ protected DirtyFlag dirty = DirtyFlag.DIRTY; /** * The parent of the node. In the case of a normal render graph node, * this will be an NGGroup. However, if this node is being used as * a clip node, then the parent is the node it is the clip for. */ private NGNode parent; /** * True if this node is a clip. This means the parent is clipped by this node. */ private boolean isClip; /** * The node used for specifying the clipping shape for this node. If null, * then there is no clip. */ private NGNode clipNode; /** * The opacity of this node. */ private float opacity = 1f; /** * The blend mode that controls how the pixels of this node blend into * the rest of the scene behind it. */ private Blend.Mode nodeBlendMode; /** * The depth test flag for this node. It is used when rendering if the window * into which we are rendering has a depth buffer. */ private boolean depthTest = true; /** * A filter used when the node is cached. If null, then the node is not * being cached. While in theory this could be created automatically by * the implementation due to some form of heuristic, currently we * only set this if the application has requested that the node be cached. */ private CacheFilter cacheFilter; /** * A filter used whenever an effect is placed on the node. Of course * effects can form a kind of tree, such that this one effect might be * an accumulation of several different effects. This will be null if * there are no effects on the FX scene graph node. */ private EffectFilter effectFilter; /** * If this node is an NGGroup, then this flag will be used to indicate * whether one or more of its children is dirty. While it would seem this * flag should be on NGGroup, the code turns out to be a bit cleaner with * this flag in the NGNode class. */ protected boolean childDirty = false; /** * How many children are going to be accumulated */ protected int dirtyChildrenAccumulated = 0; /** * Do not iterate over all children in group. Mark group as dirty * when threshold was reached. */ protected final static int DIRTY_CHILDREN_ACCUMULATED_THRESHOLD = 12; /** * Marks position of this node in dirty regions. */ protected int cullingBits = 0x0; private DirtyHint hint; /** * A cached representation of the opaque region for this node. This * cached version needs to be recomputed whenever the opaque region becomes * invalid, which includes local transform changes (translations included!). */ private RectBounds opaqueRegion = null; /** * To avoid object churn we keep opaqueRegion around, and just toggle this * boolean to indicate whether we need to recompute the opaqueRegion. */ private boolean opaqueRegionInvalid = true; /** * Used for debug purposes. This field will keep track of which nodes were * rendered as a result of different dirty regions. These correspond to the * same positions as the cullingBits. So for example, if a node was rendered * by dirty region 0, then painted will have the lowest bit set. If it * was rendered by dirty region 3, then it would have the 3rd bit from the * right set ( that is, 1 << 2) */ private int painted = 0; protected NGNode() { } /*************************************************************************** * * * Methods invoked during synchronization * * * **************************************************************************/ /** * Called by the FX scene graph to tell us whether we should be visible or not. * @param value whether it is visible */ public void setVisible(boolean value) { // If the visibility changes, we need to mark this node as being dirty. // If this node is being cached, changing visibility should have no // effect, since it doesn't affect the rendering of the content in // any way. If we were to release the cached image, that might thwart // the developer's attempt to improve performance for things that // rapidly appear and disappear but which are expensive to render. // Ancestors, of course, must still have their caches invalidated. if (visible != value) { this.visible = value; markDirty(); } } /** * Called by the FX scene graph to tell us what our new content bounds are. * @param bounds must not be null */ public void setContentBounds(BaseBounds bounds) { // Note, there isn't anything to do here. We're dirty if geom or // visuals or transformed bounds or effects or clip have changed. // There's no point dealing with it here. contentBounds = contentBounds.deriveWithNewBounds(bounds); } /** * Called by the FX scene graph to tell us what our transformed bounds are. * @param bounds must not be null */ public void setTransformedBounds(BaseBounds bounds, boolean byTransformChangeOnly) { if (transformedBounds.equals(bounds)) { // There has been no change, so ignore. It turns out this happens // a lot, because when a leaf has dirty bounds, all parents also // assume their bounds have changed, and only when they recompute // their bounds do we discover otherwise. This check could happen // on the FX side, however, then the FX side needs to cache the // former content bounds at the time of the last sync or needs to // be able to read state back from the NG side. Yuck. Just doing // it here for now. return; } // If the transformed bounds have changed, then we need to save off the // transformed bounds into the dirty bounds, so that the resulting // dirty region will be correct. If this node is cached, we DO NOT // invalidate the cache. The cacheFilter will compare its cached // transform to the accumulated transform to determine whether the // cache needs to be regenerated. So we will not invalidate it here. if (dirtyBounds.isEmpty()) { dirtyBounds = dirtyBounds.deriveWithNewBounds(transformedBounds); dirtyBounds = dirtyBounds.deriveWithUnion(bounds); } else { // TODO I think this is vestigial from Scenario and will never // actually occur in real life... (RT-23956) dirtyBounds = dirtyBounds.deriveWithUnion(transformedBounds); } transformedBounds = transformedBounds.deriveWithNewBounds(bounds); if (hasVisuals() && !byTransformChangeOnly) { markDirty(); } } /** * Called by the FX scene graph to tell us what our transform matrix is. * @param tx must not be null */ public void setTransformMatrix(BaseTransform tx) { if (transform.equals(tx)) { return; } // If the transform matrix has changed, then we need to update it, // and mark this node as dirty. If this node is cached, we DO NOT // invalidate the cache. The cacheFilter will compare its cached // transform to the accumulated transform to determine whether the // cache needs to be regenerated. So we will not invalidate it here. // This approach allows the cached image to be reused in situations // where only the translation parameters of the accumulated transform // are changing. The scene will still be marked dirty and cached // images of any ancestors will be invalidated. boolean useHint = false; // If the parent is cached, try to check if the transformation is only a translation if (parent != null && parent.cacheFilter != null && PrismSettings.scrollCacheOpt) { if (hint == null) { // If there's no hint created yet, this is the first setTransformMatrix // call and we have nothing to compare to yet. hint = new DirtyHint(); } else { if (transform.getMxx() == tx.getMxx() && transform.getMxy() == tx.getMxy() && transform.getMyy() == tx.getMyy() && transform.getMyx() == tx.getMyx() && transform.getMxz() == tx.getMxz() && transform.getMyz() == tx.getMyz() && transform.getMzx() == tx.getMzx() && transform.getMzy() == tx.getMzy() && transform.getMzz() == tx.getMzz() && transform.getMzt() == tx.getMzt()) { useHint = true; hint.translateXDelta = tx.getMxt() - transform.getMxt(); hint.translateYDelta = tx.getMyt() - transform.getMyt(); } } } transform = transform.deriveWithNewTransform(tx); if (useHint) { markDirtyByTranslation(); } else { markDirty(); } invalidateOpaqueRegion(); } /** * Called by the FX scene graph whenever the clip node for this node changes. * @param clipNode can be null if the clip node is being cleared */ public void setClipNode(NGNode clipNode) { // Whenever the clipNode itself has changed (that is, the reference to // the clipNode), we need to be sure to mark this node dirty and to // invalidate the cache of this node (if there is one) and all parents. if (clipNode != this.clipNode) { // Clear the "parent" property of the clip node, if there was one if (this.clipNode != null) this.clipNode.setParent(null); // Make the "parent" property of the clip node point to this if (clipNode != null) clipNode.setParent(this, true); // Keep the reference to the new clip node this.clipNode = clipNode; // Mark this node dirty, invalidate its cache, and all parents. visualsChanged(); invalidateOpaqueRegion(); } } /** * Called by the FX scene graph whenever the opacity for the node changes. * We create a special filter when the opacity is < 1. * @param opacity A value between 0 and 1. */ public void setOpacity(float opacity) { // Check the argument to make sure it is valid. if (opacity < 0 || opacity > 1) { throw new IllegalArgumentException("Internal Error: The opacity must be between 0 and 1"); } // If the opacity has changed, react. If this node is being cached, // then we do not want to invalidate the cache due to an opacity // change. However, as usual, all parent caches must be invalidated. if (opacity != this.opacity) { final float old = this.opacity; this.opacity = opacity; markDirty(); // Even though the opacity has changed, for example from .5 to .6, // we don't need to invalidate the opaque region unless it has toggled // from 1 to !1, or from !1 to 1. if (old < 1 && (opacity == 1 || opacity == 0) || opacity < 1 && (old == 1 || old == 0)) { invalidateOpaqueRegion(); } } } /** * Set by the FX scene graph. * @param blendMode may be null to indicate "default" */ public void setNodeBlendMode(Blend.Mode blendMode) { // The following code was a broken optimization that made an // incorrect assumption about null meaning the same thing as // SRC_OVER. In reality, null means "pass through blending // from children" and SRC_OVER means "intercept blending of // children, allow them to blend with each other, but pass // their result on in a single SRC_OVER operation into the bg". // For leaf nodes, those are mostly the same thing, but Regions // and Groups might behave differently for the two modes. // if (blendMode == Blend.Mode.SRC_OVER) { // blendMode = null; // } // If the blend mode has changed, react. If this node is being cached, // then we do not want to invalidate the cache due to a compositing // change. However, as usual, all parent caches must be invalidated. if (this.nodeBlendMode != blendMode) { this.nodeBlendMode = blendMode; markDirty(); invalidateOpaqueRegion(); } } /** * Called by the FX scene graph whenever the derived depth test flag for * the node changes. * @param depthTest indicates whether to perform a depth test operation * (if the window has a depth buffer). */ public void setDepthTest(boolean depthTest) { // If the depth test flag has changed, react. if (depthTest != this.depthTest) { this.depthTest = depthTest; // Mark this node dirty, invalidate its cache, and all parents. visualsChanged(); } } /** * Called by the FX scene graph whenever "cached" or "cacheHint" changes. * These hints provide a way for the developer to indicate whether they * want this node to be cached as a raster, which can be quite a performance * optimization in some cases (and lethal in others). * @param cached specifies whether or not this node should be cached * @param cacheHint never null, indicates some hint as to how to cache */ public void setCachedAsBitmap(boolean cached, CacheHint cacheHint) { // Validate the arguments if (cacheHint == null) { throw new IllegalArgumentException("Internal Error: cacheHint must not be null"); } if (cached) { if (cacheFilter == null) { cacheFilter = new CacheFilter(this, cacheHint); // We do not technically need to do a render pass here, but if // we wait for the next render pass to cache it, then we will // cache not the current visuals, but the visuals as defined // by any transform changes that happen between now and then. // Repainting now encourages the cached version to be as close // as possible to the state of the node when the cache hint // was set... markDirty(); } else { if (!cacheFilter.matchesHint(cacheHint)) { cacheFilter.setHint(cacheHint); // Different hints may have different requirements of // whether the cache is stale. We do not have enough info // right here to evaluate that, but it will be determined // naturally during a repaint cycle. // If the new hint is more relaxed (QUALITY => SPEED for // instance) then rendering should be quick. // If the new hint is more restricted (SPEED => QUALITY) // then we need to render to improve the results anyway. markDirty(); } } } else { if (cacheFilter != null) { cacheFilter.dispose(); cacheFilter = null; // A cache will often look worse than uncached rendering. It // may look the same in some circumstances, and this may then // be an unnecessary rendering pass, but we do not have enough // information here to be able to optimize that when possible. markDirty(); } } } /** * Called by the FX scene graph to set the effect. * @param effect the effect (can be null to clear it) */ public void setEffect(Effect effect) { final Effect old = getEffect(); // When effects are disabled, be sure to reset the effect filter if (PrismSettings.disableEffects) { effect = null; } // We only need to take action if the effect is different than what was // set previously. There are four possibilities. Of these, #1 and #3 matter: // 0. effectFilter == null, effect == null // 1. effectFilter == null, effect != null // 2. effectFilter != null, effectFilter.effect == effect // 3. effectFilter != null, effectFilter.effect != effect // In any case where the effect is changed, we must both invalidate // the cache for this node (if there is one) and all parents, and mark // this node as dirty. if (effectFilter == null && effect != null) { effectFilter = new EffectFilter(effect, this); visualsChanged(); } else if (effectFilter != null && effectFilter.getEffect() != effect) { effectFilter.dispose(); effectFilter = null; if (effect != null) { effectFilter = new EffectFilter(effect, this); } visualsChanged(); } // The only thing we do with the effect in #computeOpaqueRegion is to check // whether the effect is null / not null. If the answer to these question has // not changed from last time, then there is no need to recompute the opaque region. if (old != effect) { if (old == null || effect == null) { invalidateOpaqueRegion(); } } } /** * Called by the FX scene graph when an effect in the effect chain on the node * changes internally. */ public void effectChanged() { visualsChanged(); } /** * Return true if contentBounds is purely a 2D bounds, ie. it is a * RectBounds or its Z dimension is almost zero. */ public boolean isContentBounds2D() { return (contentBounds.is2D() || (Affine3D.almostZero(contentBounds.getMaxZ()) && Affine3D.almostZero(contentBounds.getMinZ()))); } /*************************************************************************** * * * Hierarchy, visibility, and other such miscellaneous NGNode properties * * * **************************************************************************/ /** * Gets the parent of this node. The parent might be an NGGroup. However, * if this node is a clip node on some other node, then the node on which * it is set as the clip will be returned. That is, suppose some node A * has a clip node B. The method B.getParent() will return A. */ public NGNode getParent() { return parent; } /** * Only called by this class, or by the NGGroup class. */ public void setParent(NGNode parent) { setParent(parent, false); } private void setParent(NGNode parent, boolean isClip) { this.parent = parent; this.isClip = isClip; } /** * Used for debug purposes. */ public final void setName(String value) { this.name = value; } /** * Used for debug purposes. */ public final String getName() { return name; } protected final Effect getEffect() { return effectFilter == null ? null : effectFilter.getEffect(); } /** * Gets whether this node's visible property is set */ public boolean isVisible() { return visible; } public final BaseTransform getTransform() { return transform; } public final float getOpacity() { return opacity; } public final Blend.Mode getNodeBlendMode() { return nodeBlendMode; } public final boolean isDepthTest() { return depthTest; } public final CacheFilter getCacheFilter() { return cacheFilter; } public final EffectFilter getEffectFilter() { return effectFilter; } public final NGNode getClipNode() { return clipNode; } public BaseBounds getContentBounds(BaseBounds bounds, BaseTransform tx) { if (tx.isTranslateOrIdentity()) { bounds = bounds.deriveWithNewBounds(contentBounds); if (!tx.isIdentity()) { float translateX = (float) tx.getMxt(); float translateY = (float) tx.getMyt(); float translateZ = (float) tx.getMzt(); bounds = bounds.deriveWithNewBounds( bounds.getMinX() + translateX, bounds.getMinY() + translateY, bounds.getMinZ() + translateZ, bounds.getMaxX() + translateX, bounds.getMaxY() + translateY, bounds.getMaxZ() + translateZ); } return bounds; } else { // This is a scale / rotate / skew transform. // We have contentBounds cached throughout the entire tree. // just walk down the tree and add everything up return computeBounds(bounds, tx); } } private BaseBounds computeBounds(BaseBounds bounds, BaseTransform tx) { // TODO: This code almost worked, but it ignored the local to // parent transforms on the nodes. The short fix is to disable // this block and use the more general form below, but we need // to revisit this and see if we can make it work more optimally. // @see RT-12105 http://javafx-jira.kenai.com/browse/RT-12105 if (false && this instanceof NGGroup) { List children = ((NGGroup)this).getChildren(); BaseBounds tmp = TEMP_BOUNDS; for (int i=0; i children = ((NGGroup)this).getChildren(); for (int i=0; i children = ((NGGroup) this).getChildren(); for (int i = 0; i < children.size(); ++i) { NGNode child = children.get(i); if (child.dirty != DirtyFlag.CLEAN || child.childDirty) { child.clearDirtyTree(); } } } } /** * Invalidates the cache, if it is in use. There are several operations * which need to cause the cached raster to become invalid so that a * subsequent render operation will result in the cached image being * reconstructed. */ protected final void invalidateCache() { if (cacheFilter != null) { cacheFilter.invalidate(); } } /** * Mark the cache as invalid due to a translation of a child. The cache filter * might use this information for optimizations. */ protected final void invalidateCacheByTranslation(DirtyHint hint) { if (cacheFilter != null) { cacheFilter.invalidateByTranslation(hint.translateXDelta, hint.translateYDelta); } } /*************************************************************************** * * * Dirty Regions * * * * Need to add documentation about dirty regions and how they work. One * * thing to be aware of is that during the dirty region accumulation phase * * we use precise floating point values, but during * * * **************************************************************************/ /** * Accumulates and returns the dirty regions in transformed coordinates for * this node. This method is designed such that a single downward traversal * of the tree is sufficient to update the dirty regions. *

* This method only accumulates dirty regions for parts of the tree which lie * inside the clip since there is no point in accumulating dirty regions which * lie outside the clip. The returned dirty regions bounds the same object * as that passed into the function. The returned dirty regions bounds will * always be adjusted such that they do not extend beyond the clip. *

* The given transform is the accumulated transform up to but not including the * transform of this node. * * @param clip must not be null, the clip in scene coordinates, supplied by the * rendering system. At most, this is usually the bounds of the window's * content area, however it might be smaller. * @param dirtyRegionTemp must not be null, the dirty region in scene coordinates. * When this method is initially invoked by the rendering system, the * dirtyRegion should be marked as invalid. * @param dirtyRegionContainer must not be null, the container of dirty regions in scene * coordinates. * @param tx must not be null, the accumulated transform up to but not * including this node's transform. When this method concludes, it must * restore this transform if it was changed within the function. * @param pvTx must not be null, it's the perspective transform of the current * perspective camera or identity transform if parallel camera is used. * @return The dirty region container. If the returned value is null, then that means * the clip should be used as the dirty region. This is a special * case indicating that there is no more need to walk the tree but * we can take a shortcut. Note that returning null is *always* * safe. Returning something other than null is simply an * optimization for cases where the dirty region is substantially * smaller than the clip. * TODO: Only made non-final for the sake of testing (see javafx-sg-prism tests) (RT-23957) */ public /*final*/ int accumulateDirtyRegions(final RectBounds clip, final RectBounds dirtyRegionTemp, DirtyRegionPool regionPool, final DirtyRegionContainer dirtyRegionContainer, final BaseTransform tx, final GeneralTransform3D pvTx) { // This is the main entry point, make sure to check these inputs for validity if (clip == null || dirtyRegionTemp == null || regionPool == null || dirtyRegionContainer == null || tx == null || pvTx == null) throw new NullPointerException(); // Even though a node with 0 visibility or 0 opacity doesn't get // rendered, it may contribute to the dirty bounds, for example, if it // WAS visible or if it HAD an opacity > 0 last time we rendered then // we must honor its dirty region. We have front-loaded this work so // that we don't mark nodes as having dirty flags or dirtyBounds if // they shouldn't contribute to the dirty region. So we can simply // treat all nodes, regardless of their opacity or visibility, as // though their dirty regions matter. They do. // If this node is clean then we can simply return the dirty region as // there is no need to walk any further down this branch of the tree. // The node is "clean" if neither it, nor its children, are dirty. if (dirty == DirtyFlag.CLEAN && !childDirty) { return DirtyRegionContainer.DTR_OK; } // We simply collect this nodes dirty region if it has its dirty flag // set, regardless of whether it is a group or not. However, if this // node is not dirty, then we can ask the accumulateGroupDirtyRegion // method to collect the dirty regions of the children. if (dirty != DirtyFlag.CLEAN) { return accumulateNodeDirtyRegion(clip, dirtyRegionTemp, dirtyRegionContainer, tx, pvTx); } else { assert childDirty; // this must be true by this point return accumulateGroupDirtyRegion(clip, dirtyRegionTemp, regionPool, dirtyRegionContainer, tx, pvTx); } } /** * Accumulates the dirty region of a node. * TODO: Only made non-final for the sake of testing (see javafx-sg-prism tests) (RT-23957) */ int accumulateNodeDirtyRegion(final RectBounds clip, final RectBounds dirtyRegionTemp, final DirtyRegionContainer dirtyRegionContainer, final BaseTransform tx, final GeneralTransform3D pvTx) { // Get the dirty bounds of this specific node in scene coordinates final BaseBounds bb = computeDirtyRegion(dirtyRegionTemp, tx, pvTx); // Note: dirtyRegion is strictly a 2D operation. We simply need the largest // rectangular bounds of bb. Hence the Z-axis projection of bb; taking // minX, minY, maxX and maxY values from this point on. Also, in many cases // bb == dirtyRegionTemp. In fact, the only time this won't be true is if // there is (or was) a perspective transform involved on this node. if (bb != dirtyRegionTemp) { bb.flattenInto(dirtyRegionTemp); } // If my dirty region is empty, or if it doesn't intersect with the // clip, then we can simply return since this node's dirty region is // not helpful if (dirtyRegionTemp.isEmpty() || clip.disjoint(dirtyRegionTemp)) { return DirtyRegionContainer.DTR_OK; } // If the clip is completely contained within the dirty region (including // if they are equal) then we return DTR_CONTAINS_CLIP if (dirtyRegionTemp.contains(clip)) { return DirtyRegionContainer.DTR_CONTAINS_CLIP; } // The only overhead in calling intersectWith, and contains (above) is the repeated checking // if the isEmpty state. But the code is cleaner and less error prone. dirtyRegionTemp.intersectWith(clip); // Add the dirty region to the container dirtyRegionContainer.addDirtyRegion(dirtyRegionTemp); return DirtyRegionContainer.DTR_OK; } /** * Accumulates the dirty region of an NGGroup. This is implemented here as opposed to * using polymorphism because we wanted to centralize all of the dirty region * management code in one place, rather than having it spread between Prism, * Scenario, and any other future toolkits. * TODO: Only made non-final for the sake of testing (see javafx-sg-prism tests) (RT-23957) */ int accumulateGroupDirtyRegion(final RectBounds clip, final RectBounds dirtyRegionTemp, final DirtyRegionPool regionPool, DirtyRegionContainer dirtyRegionContainer, final BaseTransform tx, final GeneralTransform3D pvTx) { // We should have only made it to this point if this node has a dirty // child. If this node itself is dirty, this method never would get called. // If this node was not dirty and had no dirty children, then this // method never should have been called. So at this point, the following // assertions should be correct. assert childDirty; assert dirty == DirtyFlag.CLEAN; int status = DirtyRegionContainer.DTR_OK; if (dirtyChildrenAccumulated > DIRTY_CHILDREN_ACCUMULATED_THRESHOLD) { status = accumulateNodeDirtyRegion(clip, dirtyRegionTemp, dirtyRegionContainer, tx, pvTx); return status; } // If we got here, then we are following a "bread crumb" trail down to // some child (perhaps distant) which is dirty. So we need to iterate // over all the children and accumulate their dirty regions. Before doing // so we, will save off the transform state and restore it after having // called all the children. double mxx = tx.getMxx(); double mxy = tx.getMxy(); double mxz = tx.getMxz(); double mxt = tx.getMxt(); double myx = tx.getMyx(); double myy = tx.getMyy(); double myz = tx.getMyz(); double myt = tx.getMyt(); double mzx = tx.getMzx(); double mzy = tx.getMzy(); double mzz = tx.getMzz(); double mzt = tx.getMzt(); BaseTransform renderTx = tx; if (this.transform != null) renderTx = renderTx.deriveWithConcatenation(this.transform); // If this group node has a clip, then we will perform some special // logic which will cause the dirty region accumulation loops to run // faster. We already have a system whereby if a node determines that // its dirty region exceeds that of the clip, it simply returns null, // short circuiting the accumulation process. We extend that logic // here by also taking into account the clipNode on the group. If // there is a clip node, then we will union the bounds of the clip // node (in boundsInScene space) with the current clip and pass this // new clip down to the children. If they determine that their dirty // regions exceed the bounds of this new clip, then they will return // null. We'll catch that here, and use that information to know that // we ought to simply accumulate the bounds of this group as if it // were dirty. This process will do all the other optimizations we // already have in place for getting the normal dirty region. RectBounds myClip = clip; //Save current dirty region so we can fast-reset to (something like) the last state //and possibly save a few intersects() calls DirtyRegionContainer originalDirtyRegion = null; BaseTransform originalRenderTx = null; if (effectFilter != null) { try { myClip = new RectBounds(); BaseBounds myClipBaseBounds = renderTx.inverseTransform(clip, TEMP_BOUNDS); myClipBaseBounds.flattenInto(myClip); } catch (NoninvertibleTransformException ex) { return DirtyRegionContainer.DTR_OK; } originalRenderTx = renderTx; renderTx = BaseTransform.IDENTITY_TRANSFORM; originalDirtyRegion = dirtyRegionContainer; dirtyRegionContainer = regionPool.checkOut(); } else if (clipNode != null) { originalDirtyRegion = dirtyRegionContainer; myClip = new RectBounds(); BaseBounds clipBounds = clipNode.getCompleteBounds(myClip, renderTx); pvTx.transform(clipBounds, clipBounds); clipBounds.flattenInto(myClip); myClip.intersectWith(clip); dirtyRegionContainer = regionPool.checkOut(); } //Accumulate also removed children to dirty region. List removed = ((NGGroup) this).getRemovedChildren(); if (removed != null) { NGNode removedChild; for (int i = removed.size() - 1; i >= 0; --i) { removedChild = removed.get(i); removedChild.dirty = DirtyFlag.DIRTY; status = removedChild.accumulateDirtyRegions(myClip, dirtyRegionTemp,regionPool, dirtyRegionContainer, renderTx, pvTx); if (status == DirtyRegionContainer.DTR_CONTAINS_CLIP) { break; } } } List children = ((NGGroup) this).getChildren(); int num = children.size(); for (int i=0; i roots) { s.append("\n*=Render Root\n"); s.append("d=Dirty\n"); s.append("dt=Dirty By Translation\n"); s.append("i=Dirty Region Intersects the NGNode\n"); s.append("c=Dirty Region Contains the NGNode\n"); s.append("ef=Effect Filter\n"); s.append("cf=Cache Filter\n"); s.append("cl=This node is a clip node\n"); s.append("b=Blend mode is set\n"); s.append("or=Opaque Region\n"); printDirtyOpts(s, this, BaseTransform.IDENTITY_TRANSFORM, "", roots); } /** * Used for debug purposes. Recursively visits all NGNodes and prints those that are possibly part of * the render operation and annotates each node. * * @param s The String builder * @param node The node that we're printing out information about * @param tx The transform * @param prefix Some prefix to put in front of the node output (mostly spacing) * @param roots The different dirty roots, if any. */ private final void printDirtyOpts(StringBuilder s, NGNode node, BaseTransform tx, String prefix, List roots) { if (!node.isVisible() || node.getOpacity() == 0) return; BaseTransform copy = tx.copy(); copy = copy.deriveWithConcatenation(node.getTransform()); List stuff = new ArrayList<>(); for (int i=0; iparent transform. * @param pvTx The perspective camera transform. * @param clipBounds The bounds in scene coordinates * @param colorBuffer A pixel array where each pixel contains a color indicating how many times * it has been "drawn" * @param dirtyRegionIndex the index of the dirty region we're gathering information for. This is * needed so we can shift the "painted" field to find out if this node * was drawn in this dirty region. */ public void drawDirtyOpts(final BaseTransform tx, final GeneralTransform3D pvTx, Rectangle clipBounds, int[] colorBuffer, int dirtyRegionIndex) { if ((painted & (1 << (dirtyRegionIndex * 2))) != 0) { // Transforming the content bounds (which includes the clip) to screen coordinates tx.copy().deriveWithConcatenation(getTransform()).transform(contentBounds, TEMP_BOUNDS); if (pvTx != null) pvTx.transform(TEMP_BOUNDS, TEMP_BOUNDS); RectBounds bounds = new RectBounds(); TEMP_BOUNDS.flattenInto(bounds); // Adjust the bounds so that they are relative to the clip. The colorBuffer is sized // exactly the same as the clip, and the elements of the colorBuffer represent the // pixels inside the clip. However the bounds of this node may overlap the clip in // some manner, so we adjust them such that x, y, w, h will be the adjusted bounds. assert clipBounds.width * clipBounds.height == colorBuffer.length; bounds.intersectWith(clipBounds); int x = (int) bounds.getMinX() - clipBounds.x; int y = (int) bounds.getMinY() - clipBounds.y; int w = (int) (bounds.getWidth() + .5); int h = (int) (bounds.getHeight() + .5); if (w == 0 || h == 0) { // I would normally say we should never reach this point, as it means something was // marked as painted but really couldn't have been. return; } // x, y, w, h are 0 based and will fit within the clip, so now we can simply update // all the pixels that fall within these bounds. for (int i = y; i < y+h; i++) { for (int j = x; j < x+w; j++) { final int index = i * clipBounds.width + j; int color = colorBuffer[index]; // This is kind of a dirty hack. The idea is to show green if 0 or 1 // times a pixel is drawn, Yellow for 2 or 3 times, and red for more // Than that. So I use 0x80007F00 as the first green color, and // 0x80008000 as the second green color, but their so close to the same // thing you probably won't be able to tell them apart, but I can tell // numerically they're different and increment (so I use the colors // as my counters). if (color == 0) { color = 0x8007F00; } else if ((painted & (3 << (dirtyRegionIndex * 2))) == 3) { switch (color) { case 0x80007F00: color = 0x80008000; break; case 0x80008000: color = 0x807F7F00; break; case 0x807F7F00: color = 0x80808000; break; case 0x80808000: color = 0x807F0000; break; default: color = 0x80800000; } } colorBuffer[index] = color; } } } } /*************************************************************************** * * * Identifying render roots * * * **************************************************************************/ protected static enum RenderRootResult { /** * A Node returns NO_RENDER_ROOT when it is not a render root because * it does not have an opaqueRegion which completely covers the area * of the clip. Maybe the node is dirty, but outside the dirty region * that we're currently processing. For an NGGroup, returning * NO_RENDER_ROOT means that there is no render root (occluder) within * this entire branch of the tree. */ NO_RENDER_ROOT, /** * A Node returns HAS_RENDER_ROOT when its opaque region completely * covers the clip. An NGGroup returns HAS_RENDER_ROOT when one of * its children either returned HAS_RENDER_ROOT or HAS_RENDER_ROOT_AND_IS_CLEAN. */ HAS_RENDER_ROOT, /** * A Node returns HAS_RENDER_ROOT_AND_IS_CLEAN when its opaque region * completely covers the clip and the Node is, itself, clean. An NGNode * returns HAS_RENDER_ROOT_AND_IS_CLEAN only if it had a child that * returned HAS_RENDER_ROOT_AND_IS_CLEAN and none of its children drawn * above the render root are dirty. * * This optimization allows us to recognize situations where perhaps there * were some dirty nodes, but they are completely covered by an occluder, * and therefore we don't actually have to draw anything. */ HAS_RENDER_ROOT_AND_IS_CLEAN, } /** * Called after preCullingBits in order to get the node * from which we should begin drawing. This is our support for occlusion culling. * This should only be called on the root node. * * If no render root was found, we need to render everything from this root, so the path will contain this node. * If no rendering is needed (everything dirty is occluded), the path will remain empty * * @param path node path to store the node path */ public final void getRenderRoot(NodePath path, RectBounds dirtyRegion, int cullingIndex, BaseTransform tx, GeneralTransform3D pvTx) { // This is the main entry point, make sure to check these inputs for validity if (path == null || dirtyRegion == null || tx == null || pvTx == null) { throw new NullPointerException(); } if (cullingIndex < -1 || cullingIndex > 15) { throw new IllegalArgumentException("cullingIndex cannot be < -1 or > 15"); } // This method must NEVER BE CALLED if the depth buffer is turned on. I don't have a good way to test // for that because NGNode doesn't have a reference to the scene it is a part of... RenderRootResult result = computeRenderRoot(path, dirtyRegion, cullingIndex, tx, pvTx); if (result == RenderRootResult.NO_RENDER_ROOT) { // We didn't find any render root, which means that no one node was large enough // to obscure the entire dirty region (or, possibly, some combination of nodes in an // NGGroup were not, together, large enough to do the job). So we need to render // from the root node, which is this node. path.add(this); } else if (result == RenderRootResult.HAS_RENDER_ROOT_AND_IS_CLEAN) { // We've found a render root, and it is clean and everything above it in painter order // is clean, so actually we have nothing to paint this time around (some stuff must // have been dirty which is completely occluded by the render root). So we can clear // the path, which indicates to the caller that nothing needs to be painted. path.clear(); } } /** * Searches for the last node that covers all of the specified dirty region with an opaque region, * in this node's subtree. Such a node can serve as a rendering root as all nodes preceding the node * will be covered by it. * * @param path the NodePath to populate with the path to the render root. Cannot be null. * @param dirtyRegion the current dirty region. Cannot be null. * @param cullingIndex index of culling information * @param tx current transform. Cannot be null. * @param pvTx current perspective transform. Cannot be null. * @return The result of visiting this node. */ RenderRootResult computeRenderRoot(NodePath path, RectBounds dirtyRegion, int cullingIndex, BaseTransform tx, GeneralTransform3D pvTx) { return computeNodeRenderRoot(path, dirtyRegion, cullingIndex, tx, pvTx); } private static Point2D[] TEMP_POINTS2D_4 = new Point2D[] { new Point2D(), new Point2D(), new Point2D(), new Point2D() }; // Whether (px, py) is clockwise or counter-clockwise to a->b private static int ccw(double px, double py, Point2D a, Point2D b) { return (int)Math.signum(((b.x - a.x) * (py - a.y)) - (b.y - a.y) * (px - a.x)); } private static boolean pointInConvexQuad(double x, double y, Point2D[] rect) { int ccw01 = ccw(x, y, rect[0], rect[1]); int ccw12 = ccw(x, y, rect[1], rect[2]); int ccw23 = ccw(x, y, rect[2], rect[3]); int ccw31 = ccw(x, y, rect[3], rect[0]); // Possible results after this operation: // 0 -> 0 (0x0) // 1 -> 1 (0x1) // -1 -> Integer.MIN_VALUE (0x80000000) ccw01 ^= (ccw01 >>> 1); ccw12 ^= (ccw12 >>> 1); ccw23 ^= (ccw23 >>> 1); ccw31 ^= (ccw31 >>> 1); final int union = ccw01 | ccw12 | ccw23 | ccw31; // This means all ccw* were either (-1 or 0) or (1 or 0), but not all of them were 0 return union == 0x80000000 || union == 0x1; // Or alternatively... // return (union ^ (union << 31)) < 0; } /** * Check if this node can serve as rendering root for this dirty region. * * @param path the NodePath to populate with the path to the render root. Cannot be null. * @param dirtyRegion the current dirty region. Cannot be null. * @param cullingIndex index of culling information, -1 means culling information should not be used * @param tx current transform. Cannot be null. * @param pvTx current perspective transform. Cannot be null. * @return NO_RENDER_ROOT if this node does not have an opaque * region that fills the entire dirty region. Returns HAS_RENDER_ROOT * if the opaque region fills the dirty region. */ final RenderRootResult computeNodeRenderRoot(NodePath path, RectBounds dirtyRegion, int cullingIndex, BaseTransform tx, GeneralTransform3D pvTx) { // Nodes outside of the dirty region can be excluded immediately. // This can be used only if the culling information is provided. if (cullingIndex != -1) { final int bits = cullingBits >> (cullingIndex * 2); if ((bits & DIRTY_REGION_CONTAINS_OR_INTERSECTS_NODE_BOUNDS) == 0x00) { return RenderRootResult.NO_RENDER_ROOT; } } if (!isVisible()) { return RenderRootResult.NO_RENDER_ROOT; } final RectBounds opaqueRegion = getOpaqueRegion(); if (opaqueRegion == null) return RenderRootResult.NO_RENDER_ROOT; final BaseTransform localToParentTx = getTransform(); BaseTransform localToSceneTx = TEMP_TRANSFORM.deriveWithNewTransform(tx).deriveWithConcatenation(localToParentTx); // Now check if the dirty region is fully contained in our opaque region. Suppose the above // transform included a rotation about Z. In these cases, the transformed // opaqueRegion might be some non-axis aligned quad. So what we need to do is to check // that each corner of the dirty region lies within the (potentially rotated) quad // of the opaqueRegion. if (checkBoundsInQuad(opaqueRegion, dirtyRegion, localToSceneTx, pvTx)) { // This node is a render root. path.add(this); return isClean() ? RenderRootResult.HAS_RENDER_ROOT_AND_IS_CLEAN : RenderRootResult.HAS_RENDER_ROOT; } return RenderRootResult.NO_RENDER_ROOT; } static boolean checkBoundsInQuad(RectBounds untransformedQuad, RectBounds innerBounds, BaseTransform tx, GeneralTransform3D pvTx) { if (pvTx.isIdentity() && (tx.getType() & ~(BaseTransform.TYPE_TRANSLATION | BaseTransform.TYPE_QUADRANT_ROTATION | BaseTransform.TYPE_MASK_SCALE)) == 0) { // If pvTx is identity and there's simple transformation that will result in axis-aligned rectangle, // we can do a quick test by using bound.contains() if (tx.isIdentity()) { TEMP_BOUNDS.deriveWithNewBounds(untransformedQuad); } else { tx.transform(untransformedQuad, TEMP_BOUNDS); } TEMP_BOUNDS.flattenInto(TEMP_RECT_BOUNDS); return TEMP_RECT_BOUNDS.contains(innerBounds); } else { TEMP_POINTS2D_4[0].setLocation(untransformedQuad.getMinX(), untransformedQuad.getMinY()); TEMP_POINTS2D_4[1].setLocation(untransformedQuad.getMaxX(), untransformedQuad.getMinY()); TEMP_POINTS2D_4[2].setLocation(untransformedQuad.getMaxX(), untransformedQuad.getMaxY()); TEMP_POINTS2D_4[3].setLocation(untransformedQuad.getMinX(), untransformedQuad.getMaxY()); for (Point2D p : TEMP_POINTS2D_4) { tx.transform(p, p); if (!pvTx.isIdentity()) { pvTx.transform(p, p); } } return (pointInConvexQuad(innerBounds.getMinX(), innerBounds.getMinY(), TEMP_POINTS2D_4) && pointInConvexQuad(innerBounds.getMaxX(), innerBounds.getMinY(), TEMP_POINTS2D_4) && pointInConvexQuad(innerBounds.getMaxX(), innerBounds.getMaxY(), TEMP_POINTS2D_4) && pointInConvexQuad(innerBounds.getMinX(), innerBounds.getMaxY(), TEMP_POINTS2D_4)); } } /** * Invalidates any cached representation of the opaque region for this node. On the next * call to getOpaqueRegion, the opaque region will be recalculated. Any changes to state * which is used in the {@link #hasOpaqueRegion()} call must invoke this method * or the opaque region calculations will be wrong. */ protected final void invalidateOpaqueRegion() { opaqueRegionInvalid = true; if (isClip) parent.invalidateOpaqueRegion(); } /** * This method exists only for the sake of testing. * @return value of opaqueRegionInvalid */ final boolean isOpaqueRegionInvalid() { return opaqueRegionInvalid; } /** * Gets the opaque region for this node, if there is one, or returns null. * @return The opaque region for this node, or null. */ public final RectBounds getOpaqueRegion() { // Note that when we invalidate the opaqueRegion of an NGNode, we don't // walk up the tree or communicate with the parents (unlike dirty flags). // An NGGroup does not compute an opaqueRegion based on the union of opaque // regions of its children (although this is a fine idea to consider!). See RT-32441 // If we ever fix RT-32441, we must be sure to handle the case of a Group being used // as a clip node (such that invalidating a child on the group invalidates the // opaque region of every node up to the root). // Because the Effect classes have no reference to NGNode, they cannot tell the // NGNode to invalidate the opaque region whenever properties on the Effect that // would impact the opaqueRegion change. As a result, when an Effect is specified // on the NGNode, we will always treat it as if it were invalid. A more invasive // (but better) change would be to give Effect the ability to invalidate the // NGNode's opaque region when needed. if (opaqueRegionInvalid || getEffect() != null) { opaqueRegionInvalid = false; if (supportsOpaqueRegions() && hasOpaqueRegion()) { opaqueRegion = computeOpaqueRegion(opaqueRegion == null ? new RectBounds() : opaqueRegion); // If we got a null result then we encountered an error condition where somebody // claimed supportsOpaqueRegions and hasOpaqueRegion, but then they // returned null! This should never happen, so we have an assert here. However since // assertions are disabled at runtime and we want to avoid the NPE, we also perform // a null check. assert opaqueRegion != null; if (opaqueRegion == null) { return null; } // If there is a clip, then we need to determine the opaque region of the clip, and // intersect that with our existing opaque region. For example, if I had a rectangle // with a circle for its clip (centered over the rectangle), then the result needs to // be the circle's opaque region. final NGNode clip = getClipNode(); if (clip != null) { final RectBounds clipOpaqueRegion = clip.getOpaqueRegion(); // Technically a flip/quadrant rotation is allowed as well, but we don't have a convenient // way to do that yet. if (clipOpaqueRegion == null || (clip.getTransform().getType() & ~(BaseTransform.TYPE_TRANSLATION | BaseTransform.TYPE_MASK_SCALE)) != 0) { // RT-25095: If this node has a clip who's opaque region cannot be determined, then // we cannot determine any opaque region for this node (in fact, it might not have one). // Also, if the transform is something other than identity, scale, or translate then // we're just going to bail (sorry, rotate, maybe next time!) return opaqueRegion = null; } // We have to take into account any transform specified on the clip to put // it into the same coordinate system as this node final BaseBounds b = clip.getTransform().transform(clipOpaqueRegion, TEMP_BOUNDS); b.flattenInto(TEMP_RECT_BOUNDS); opaqueRegion.intersectWith(TEMP_RECT_BOUNDS); } } else { // The opaqueRegion may have been non-null in the past, but there isn't an opaque region now, // so we will nuke it to save some memory opaqueRegion = null; } } return opaqueRegion; } /** * Gets whether this NGNode supports opaque regions at all. Most node types do not, * but some do. If an NGNode subclass is written to support opaque regions, it must override * this method to return true. The subclass must then also override the computeDirtyRegion method * to return the dirty region, or null if the node in its current state doesn't have one. * This method is intended to be immutable. * * @return Whether this NGNode implementation supports opaque regions. This could also have been * implemented via an interface that some NGNodes implemented, but then we'd have instanceof * checks which I'd rather avoid. */ protected boolean supportsOpaqueRegions() { return false; } /** * Called only on NGNode subclasses which override {@link #supportsOpaqueRegions()} to return * true, this method will return whether or not this NGNode is in a state where it has * an opaque region to actually return. If this method returns true, a subsequent call to * {@link #computeOpaqueRegion(com.sun.javafx.geom.RectBounds)} must return * a non-null result. Any state used in the computation of this method, when it changes, must * result in a call to {@link #invalidateOpaqueRegion()}. * * @return Whether this NGNode currently has an opaque region. */ protected boolean hasOpaqueRegion() { final NGNode clip = getClipNode(); final Effect effect = getEffect(); return (effect == null || !effect.reducesOpaquePixels()) && getOpacity() == 1f && (nodeBlendMode == null || nodeBlendMode == Blend.Mode.SRC_OVER) && (clip == null || (clip.supportsOpaqueRegions() && clip.hasOpaqueRegion())); } /** * Computes and returns the opaque region for this node. This method * @param opaqueRegion * @return */ protected RectBounds computeOpaqueRegion(RectBounds opaqueRegion) { return null; } /** * Returns whether a clip represented by this node can be rendered using * axis aligned rect clip. The default implementation returns false, * specific subclasses should override to return true when appropriate. * * @return whether this rectangle is axis aligned when rendered given node's * and rendering transform */ protected boolean isRectClip(BaseTransform xform, boolean permitRoundedRectangle) { return false; } /*************************************************************************** * * * Rendering * * * **************************************************************************/ /** * Render the tree of nodes to the specified G (graphics) object * descending from this node as the root. This method is designed to avoid * generated trash as much as possible while descending through the * render graph while rendering. This is the appropriate method both to * initiate painting of an entire scene, and for a branch. The NGGroup * implementation must call this method on each child, not doRender directly. * * @param g The graphics object we're rendering to. This must never be null. */ public final void render(Graphics g) { if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("Nodes visited during render"); // Clear the visuals changed flag clearDirty(); // If it isn't visible, then punt if (!visible || opacity == 0f) return; // We know that we are going to render this node, so we call the // doRender method, which subclasses implement to do the actual // rendering work. doRender(g); } // This node requires 2D graphics state for rendering boolean isShape3D() { return false; } /** * Invoked only by the final render method. Implementations * of this method should make sure to save & restore the transform state. */ protected void doRender(Graphics g) { g.setState3D(isShape3D()); boolean preCullingTurnedOff = false; if (PrismSettings.dirtyOptsEnabled) { if (g.hasPreCullingBits()) { //preculling bits available final int bits = cullingBits >> (g.getClipRectIndex() * 2); if ((bits & DIRTY_REGION_CONTAINS_OR_INTERSECTS_NODE_BOUNDS) == 0) { // If no culling bits are set for this region, this group // does not intersect (nor is covered by) the region return; } else if ((bits & DIRTY_REGION_CONTAINS_NODE_BOUNDS) != 0) { // When this group is fully covered by the region, // turn off the culling checks in the subtree, as everything // gets rendered g.setHasPreCullingBits(false); preCullingTurnedOff = true; } } } // save current depth test state boolean prevDepthTest = g.isDepthTest(); // Apply Depth test for this node // (note that this will only be used if we have a depth buffer for the // surface to which we are rendering) g.setDepthTest(isDepthTest()); // save current transform state BaseTransform prevXform = g.getTransformNoClone(); double mxx = prevXform.getMxx(); double mxy = prevXform.getMxy(); double mxz = prevXform.getMxz(); double mxt = prevXform.getMxt(); double myx = prevXform.getMyx(); double myy = prevXform.getMyy(); double myz = prevXform.getMyz(); double myt = prevXform.getMyt(); double mzx = prevXform.getMzx(); double mzy = prevXform.getMzy(); double mzz = prevXform.getMzz(); double mzt = prevXform.getMzt(); // filters are applied in the following order: // transform // blend mode // opacity // cache // clip // effect // The clip must be below the cache filter, as this is expected in the // CacheFilter in order to apply scrolling optimization g.transform(getTransform()); // Try to keep track of whether this node was *really* painted. Still an // approximation, but somewhat more accurate (at least it doesn't include // groups which don't paint anything themselves). boolean p = false; // NOTE: Opt out 2D operations on 3D Shapes, which are not yet handled by Prism if (!isShape3D() && g instanceof ReadbackGraphics && needsBlending()) { renderNodeBlendMode(g); p = true; } else if (!isShape3D() && getOpacity() < 1f) { renderOpacity(g); p = true; } else if (!isShape3D() && getCacheFilter() != null) { renderCached(g); p = true; } else if (!isShape3D() && getClipNode() != null) { renderClip(g); p = true; } else if (!isShape3D() && getEffectFilter() != null && effectsSupported) { renderEffect(g); p = true; } else { renderContent(g); if (PrismSettings.showOverdraw) { p = this instanceof NGRegion || !(this instanceof NGGroup); } } if (preCullingTurnedOff) { g.setHasPreCullingBits(true); } // restore previous transform state g.setTransform3D(mxx, mxy, mxz, mxt, myx, myy, myz, myt, mzx, mzy, mzz, mzt); // restore previous depth test state g.setDepthTest(prevDepthTest); if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("Nodes rendered"); // Used for debug purposes. This is not entirely accurate, as it doesn't measure the // number of times this node drew to the pixels, and in some cases reports a node as // having been drawn even when it didn't lay down any pixels. We'd need to integrate // with our shaders or do something much more invasive to get better data here. if (PrismSettings.showOverdraw) { if (p) { painted |= 3 << (g.getClipRectIndex() * 2); } else { painted |= 1 << (g.getClipRectIndex() * 2); } } } /** * Return true if this node has a blend mode that requires special * processing. * Regular nodes can handle null or SRC_OVER just by rendering into * the existing buffer. * Groups override this since they must collect their children into * a single rendering pass if their mode is explicitly SRC_OVER. * @return true if this node needs special blending support */ protected boolean needsBlending() { Blend.Mode mode = getNodeBlendMode(); return (mode != null && mode != Blend.Mode.SRC_OVER); } private void renderNodeBlendMode(Graphics g) { // The following is safe; curXform will not be mutated below BaseTransform curXform = g.getTransformNoClone(); BaseBounds clipBounds = getClippedBounds(new RectBounds(), curXform); if (clipBounds.isEmpty()) { clearDirtyTree(); return; } if (!isReadbackSupported(g)) { if (getOpacity() < 1f) { renderOpacity(g); } else if (getClipNode() != null) { renderClip(g); } else { renderContent(g); } return; } // TODO: optimize this (RT-26936) // Extract clip bounds Rectangle clipRect = new Rectangle(clipBounds); clipRect.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g)); // render the node content into the first offscreen image FilterContext fctx = getFilterContext(g); PrDrawable contentImg = (PrDrawable) Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height); if (contentImg == null) { clearDirtyTree(); return; } Graphics gContentImg = contentImg.createGraphics(); gContentImg.setHasPreCullingBits(g.hasPreCullingBits()); gContentImg.setClipRectIndex(g.getClipRectIndex()); gContentImg.translate(-clipRect.x, -clipRect.y); gContentImg.transform(curXform); if (getOpacity() < 1f) { renderOpacity(gContentImg); } else if (getCacheFilter() != null) { renderCached(gContentImg); } else if (getClipNode() != null) { renderClip(g); } else if (getEffectFilter() != null) { renderEffect(gContentImg); } else { renderContent(gContentImg); } // the above image has already been rendered in device space, so // just translate to the node origin in device space here... RTTexture bgRTT = ((ReadbackGraphics) g).readBack(clipRect); PrDrawable bgPrD = PrDrawable.create(fctx, bgRTT); Blend blend = new Blend(getNodeBlendMode(), new PassThrough(bgPrD, clipRect), new PassThrough(contentImg, clipRect)); CompositeMode oldmode = g.getCompositeMode(); g.setTransform(null); g.setCompositeMode(CompositeMode.SRC); PrEffectHelper.render(blend, g, 0, 0, null); g.setCompositeMode(oldmode); // transform state will be restored in render() method above... Effect.releaseCompatibleImage(fctx, contentImg); ((ReadbackGraphics) g).releaseReadBackBuffer(bgRTT); } private void renderRectClip(Graphics g, NGRectangle clipNode) { BaseBounds newClip = clipNode.getShape().getBounds(); if (!clipNode.getTransform().isIdentity()) { newClip = clipNode.getTransform().transform(newClip, newClip); } final BaseTransform curXform = g.getTransformNoClone(); final Rectangle curClip = g.getClipRectNoClone(); newClip = curXform.transform(newClip, newClip); newClip.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g)); if (newClip.isEmpty() || newClip.getWidth() == 0 || newClip.getHeight() == 0) { clearDirtyTree(); return; } // REMIND: avoid garbage by changing setClipRect to accept xywh g.setClipRect(new Rectangle(newClip)); renderForClip(g); g.setClipRect(curClip); clipNode.clearDirty(); // as render() is not called on the clipNode, // make sure the dirty flags are cleared } void renderClip(Graphics g) { // if clip's opacity is 0 there's nothing to render if (getClipNode().getOpacity() == 0.0) { clearDirtyTree(); return; } // The following is safe; curXform will not be mutated below BaseTransform curXform = g.getTransformNoClone(); BaseBounds clipBounds = getClippedBounds(new RectBounds(), curXform); if (clipBounds.isEmpty()) { clearDirtyTree(); return; } if (getClipNode() instanceof NGRectangle) { // optimized case for rectangular clip NGRectangle rectNode = (NGRectangle)getClipNode(); if (rectNode.isRectClip(curXform, false)) { renderRectClip(g, rectNode); return; } } // TODO: optimize this (RT-26936) // Extract clip bounds Rectangle clipRect = new Rectangle(clipBounds); clipRect.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g)); if (!curXform.is2D()) { Rectangle savedClip = g.getClipRect(); g.setClipRect(clipRect); NodeEffectInput clipInput = new NodeEffectInput(getClipNode(), NodeEffectInput.RenderType.FULL_CONTENT); NodeEffectInput nodeInput = new NodeEffectInput(this, NodeEffectInput.RenderType.CLIPPED_CONTENT); Blend blend = new Blend(Blend.Mode.SRC_IN, clipInput, nodeInput); PrEffectHelper.render(blend, g, 0, 0, null); clipInput.flush(); nodeInput.flush(); g.setClipRect(savedClip); // There may have been some errors in the application of the // effect and we would not know to what extent the nodes were // rendered and cleared or left dirty. clearDirtyTree() will // clear both this node its clip node, and it will not recurse // to the children unless they are still marked dirty. It should // be cheap if there was no problem and thorough if there was... clearDirtyTree(); return; } // render the node content into the first offscreen image FilterContext fctx = getFilterContext(g); PrDrawable contentImg = (PrDrawable) Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height); if (contentImg == null) { clearDirtyTree(); return; } Graphics gContentImg = contentImg.createGraphics(); gContentImg.setExtraAlpha(g.getExtraAlpha()); gContentImg.setHasPreCullingBits(g.hasPreCullingBits()); gContentImg.setClipRectIndex(g.getClipRectIndex()); gContentImg.translate(-clipRect.x, -clipRect.y); gContentImg.transform(curXform); renderForClip(gContentImg); // render the mask (clipNode) into the second offscreen image PrDrawable clipImg = (PrDrawable) Effect.getCompatibleImage(fctx, clipRect.width, clipRect.height); if (clipImg == null) { getClipNode().clearDirtyTree(); Effect.releaseCompatibleImage(fctx, contentImg); return; } Graphics gClipImg = clipImg.createGraphics(); gClipImg.translate(-clipRect.x, -clipRect.y); gClipImg.transform(curXform); getClipNode().render(gClipImg); // the above images have already been rendered in device space, so // just translate to the node origin in device space here... g.setTransform(null); Blend blend = new Blend(Blend.Mode.SRC_IN, new PassThrough(clipImg, clipRect), new PassThrough(contentImg, clipRect)); PrEffectHelper.render(blend, g, 0, 0, null); // transform state will be restored in render() method above... Effect.releaseCompatibleImage(fctx, contentImg); Effect.releaseCompatibleImage(fctx, clipImg); } void renderForClip(Graphics g) { if (getEffectFilter() != null) { renderEffect(g); } else { renderContent(g); } } private void renderOpacity(Graphics g) { if (getEffectFilter() != null || getCacheFilter() != null || getClipNode() != null || !hasOverlappingContents()) { // if the node has a non-null effect or cached==true, we don't // need to bother rendering to an offscreen here because the // contents will be flattened as part of rendering the effect // (or creating the cached image) float ea = g.getExtraAlpha(); g.setExtraAlpha(ea*getOpacity()); if (getCacheFilter() != null) { renderCached(g); } else if (getClipNode() != null) { renderClip(g); } else if (getEffectFilter() != null) { renderEffect(g); } else { renderContent(g); } g.setExtraAlpha(ea); return; } FilterContext fctx = getFilterContext(g); BaseTransform curXform = g.getTransformNoClone(); BaseBounds bounds = getContentBounds(new RectBounds(), curXform); Rectangle r = new Rectangle(bounds); r.intersectWith(PrEffectHelper.getGraphicsClipNoClone(g)); PrDrawable img = (PrDrawable) Effect.getCompatibleImage(fctx, r.width, r.height); if (img == null) { return; } Graphics gImg = img.createGraphics(); gImg.setHasPreCullingBits(g.hasPreCullingBits()); gImg.setClipRectIndex(g.getClipRectIndex()); gImg.translate(-r.x, -r.y); gImg.transform(curXform); renderContent(gImg); // img contents have already been rendered in device space, so // just translate to the node origin in device space here... g.setTransform(null); float ea = g.getExtraAlpha(); g.setExtraAlpha(getOpacity()*ea); g.drawTexture(img.getTextureObject(), r.x, r.y, r.width, r.height); g.setExtraAlpha(ea); // transform state will be restored in render() method above... Effect.releaseCompatibleImage(fctx, img); } private void renderCached(Graphics g) { // We will punt on 3D completely for caching. // The first check is for any of its children contains a 3D Transform. // The second check is for any of its parents and itself has a 3D Transform // The third check is for the printing case, which doesn't use cached // bitmaps for the screen and for which there is no cacheFilter. if (isContentBounds2D() && g.getTransformNoClone().is2D() && !(g instanceof com.sun.prism.PrinterGraphics)) { getCacheFilter().render(g); } else { renderContent(g); } } protected void renderEffect(Graphics g) { getEffectFilter().render(g); } protected abstract void renderContent(Graphics g); protected abstract boolean hasOverlappingContents(); /*************************************************************************** * * * Static Helper Methods. * * * **************************************************************************/ boolean isReadbackSupported(Graphics g) { return ((g instanceof ReadbackGraphics) && ((ReadbackGraphics) g).canReadBack()); } /*************************************************************************** * * * Filters (Cache, Effect, etc). * * * **************************************************************************/ static FilterContext getFilterContext(Graphics g) { Screen s = g.getAssociatedScreen(); if (s == null) { return PrFilterContext.getPrinterContext(g.getResourceFactory()); } else { return PrFilterContext.getInstance(s); } } /** * A custom effect implementation that has a filter() method that * simply wraps the given pre-rendered PrDrawable in an ImageData * and returns that result. This is only used by the renderClip() * implementation so we cut some corners here (for example, we assume * that the given PrDrawable image is already in device space). */ private static class PassThrough extends Effect { private PrDrawable img; private Rectangle bounds; PassThrough(PrDrawable img, Rectangle bounds) { this.img = img; this.bounds = bounds; } @Override public ImageData filter(FilterContext fctx, BaseTransform transform, Rectangle outputClip, Object renderHelper, Effect defaultInput) { return new ImageData(fctx, img, new Rectangle(bounds)); } @Override public RectBounds getBounds(BaseTransform transform, Effect defaultInput) { return new RectBounds(bounds); } @Override public AccelType getAccelType(FilterContext fctx) { return AccelType.INTRINSIC; } @Override public boolean reducesOpaquePixels() { return false; } @Override public DirtyRegionContainer getDirtyRegions(Effect defaultInput, DirtyRegionPool regionPool) { return null; //Never called } } /*************************************************************************** * * * Stuff * * * **************************************************************************/ public void release() { } @Override public String toString() { return name == null ? super.toString() : name; } public void applyTransform(final BaseTransform tx, DirtyRegionContainer drc) { for (int i = 0; i < drc.size(); i++) { drc.setDirtyRegion(i, (RectBounds) tx.transform(drc.getDirtyRegion(i), drc.getDirtyRegion(i))); if (drc.checkAndClearRegion(i)) { --i; } } } public void applyClip(final BaseBounds clipBounds, DirtyRegionContainer drc) { for (int i = 0; i < drc.size(); i++) { drc.getDirtyRegion(i).intersectWith(clipBounds); if (drc.checkAndClearRegion(i)) { --i; } } } public void applyEffect(final EffectFilter effectFilter, DirtyRegionContainer drc, DirtyRegionPool regionPool) { Effect effect = effectFilter.getEffect(); EffectDirtyBoundsHelper helper = EffectDirtyBoundsHelper.getInstance(); helper.setInputBounds(contentBounds); helper.setDirtyRegions(drc); final DirtyRegionContainer effectDrc = effect.getDirtyRegions(helper, regionPool); drc.deriveWithNewContainer(effectDrc); regionPool.checkIn(effectDrc); } private static class EffectDirtyBoundsHelper extends Effect { private BaseBounds bounds; private static EffectDirtyBoundsHelper instance = null; private DirtyRegionContainer drc; public void setInputBounds(BaseBounds inputBounds) { bounds = inputBounds; } @Override public ImageData filter(FilterContext fctx, BaseTransform transform, Rectangle outputClip, Object renderHelper, Effect defaultInput) { throw new UnsupportedOperationException(); } @Override public BaseBounds getBounds(BaseTransform transform, Effect defaultInput) { if (bounds.getBoundsType() == BaseBounds.BoundsType.RECTANGLE) { return bounds; } else { //RT-29453 - CCE: in case we get 3D bounds we need to "flatten" them return new RectBounds(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY()); } } @Override public Effect.AccelType getAccelType(FilterContext fctx) { return null; } public static EffectDirtyBoundsHelper getInstance() { if (instance == null) { instance = new EffectDirtyBoundsHelper(); } return instance; } @Override public boolean reducesOpaquePixels() { return true; } private void setDirtyRegions(DirtyRegionContainer drc) { this.drc = drc; } @Override public DirtyRegionContainer getDirtyRegions(Effect defaultInput, DirtyRegionPool regionPool) { DirtyRegionContainer ret = regionPool.checkOut(); ret.deriveWithNewContainer(drc); return ret; } } }