/* * 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
* 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