StyleLayerCache.java
package swingtree.style;
import org.jspecify.annotations.Nullable;
import sprouts.Tuple;
import swingtree.UI;
import swingtree.layout.Size;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.BiConsumer;
/**
* Caches and paints one {@link UI.Layer} of a component's style, by composing the
* {@link LayerRenderConfPartition}s that layer is made of. <br>
* <br>
* Ordinarily a layer is a single {@link LayerRenderConfPartition#WHOLE} part, and this is then nothing
* more than a wrapper around the one {@link LayerPartitionCache} holding it. The reason it exists is
* that a layer mixing style which caches in incompatible ways cannot be one rasterization, and
* then it is painted as several parts in order - see {@link LayerRenderConfPartition} for what those parts
* are and why source-over compositing makes painting them one after another identical to
* painting the layer whole.
*/
final class StyleLayerCache
{
private static final int PAINTS_UNTIL_REJOINED = 4;
// Sum type based states of what this cache was last brought in line with
private interface Validation {
final class None implements Validation { static final None INSTANCE = new None(); }
final class Done implements Validation {
final ComponentConf _conf;
final boolean _whileResizing;
final boolean _withTiling;
Done( ComponentConf conf, boolean whileResizing, boolean withTiling ) {
_conf = conf;
_whileResizing = whileResizing;
_withTiling = withTiling;
}
}
}
private final UI.Layer _layer;
private final BiConsumer<LayerRenderConf, Graphics2D> _renderer;
private LayerPartitionCache[] _parts;
private PartitioningPolicy _partitioningPolicy;
private LayerRenderConf _uncachedPartition;
private Size _lastSize;
private int _paintsAtThisSize;
private Validation _validation;
private int _paintCacheHitCount = 0;
private int _paintCacheMissCount = 0;
StyleLayerCache( UI.Layer layer ) {
_layer = Objects.requireNonNull(layer);
_renderer = ( conf, graphics ) -> StyleRenderer.renderStyleOn(_layer, conf, graphics);
_partitioningPolicy = PartitioningPolicy.NONE;
_parts = PartitioningPolicy.NONE.newPartsFor(layer);
_uncachedPartition = LayerRenderConf.none();
_lastSize = Size.unknown();
_paintsAtThisSize = PAINTS_UNTIL_REJOINED;
_validation = Validation.None.INSTANCE;
}
int paintCacheHitCount() { return _paintCacheHitCount; }
int paintCacheMissCount() { return _paintCacheMissCount; }
void validate( ComponentConf newConf ) {
_recordSize(newConf.currentBounds().size());
final boolean isResizing = _isResizing();
final boolean tilingIsOn = CacheBudget.tilingEnabled();
if ( _isSettledAndSameAsLastValidation(newConf, isResizing, tilingIsOn) )
return; // We avoid validation work if nothing changed!
final LayerRenderConf full = newConf.renderConfFor(_layer);
final PartitioningPolicy policy = _partitioningPolicyFor(full, isResizing);
if ( policy != _partitioningPolicy ) {
_partitioningPolicy = policy;
_parts = policy.newPartsFor(_layer);
}
for ( LayerPartitionCache part : _parts )
part.validate(newConf, isResizing);
_uncachedPartition = policy.uncachedPartitionOf(full);
_validation = new Validation.Done(newConf, isResizing, tilingIsOn);
}
@SuppressWarnings("ReferenceEquality") // Identity means the configuration is literally the one validated against.
private boolean _isSettledAndSameAsLastValidation(ComponentConf conf, boolean isResizing, boolean tilingIsOn ) {
if ( !(_validation instanceof Validation.Done) )
return false;
final Validation.Done done = (Validation.Done) _validation;
if ( done._conf != conf || done._whileResizing != isResizing || done._withTiling != tilingIsOn )
return false;
for ( LayerPartitionCache part : _parts )
if ( part.admissionDecisionLeftOpen() )
return false; // The cache is NOT settled because at least one part has a rejected cache attempt!
return true;
}
private PartitioningPolicy _partitioningPolicyFor( LayerRenderConf full, boolean isResizing ) {
if ( _canBeSplitAroundPainters(full) )
return PartitioningPolicy.AROUND_PAINTERS;
if ( isResizing && _canBeSplitAroundNoises(full) )
return PartitioningPolicy.AROUND_NOISES;
return PartitioningPolicy.NONE;
}
private boolean _canBeSplitAroundPainters( LayerRenderConf conf ) {
if ( !conf.layer().hasPaintersWhichCannotBeCached() )
return false; // Nothing to cut around, or nothing that stops the layer being cached whole.
final LayerRenderConf underPainters = LayerRenderConfPartition.UNDER_PAINTERS.restrict(conf);
if ( underPainters.rendersNothing() )
return false; // The painters are all there is, so a cut would only add bookkeeping.
return LayerPartitionCache.wouldBeAdmitted(_layer, underPainters);
}
private void _recordSize( Size size ) {
if ( size.equals(_lastSize) )
return;
final boolean grewFromNothing = !_lastSize.hasPositiveWidth() || !_lastSize.hasPositiveHeight();
_lastSize = size;
_paintsAtThisSize = grewFromNothing ? PAINTS_UNTIL_REJOINED : 0;
}
private boolean _isResizing() {
return _paintsAtThisSize < PAINTS_UNTIL_REJOINED;
}
private static boolean _canBeSplitAroundNoises( LayerRenderConf conf ) {
if ( !conf.layer().hasRenderableNoises() )
return false; // Nothing to cut around.
if ( !CacheBudget.tilingEnabled() )
return false;
if ( !StyleRenderer.allNoisesAreCheapToRepaint(conf) )
return false; // Repainting this noise every paint would cost more than it saves.
return _isWorthCuttingOut(LayerRenderConfPartition.UNDER_NOISE.restrict(conf))
&& _isWorthCuttingOut(LayerRenderConfPartition.OVER_NOISE.restrict(conf));
}
private static boolean _isWorthCuttingOut( LayerRenderConf part ) {
return part.rendersNothing() || LayerPartitionCache.wouldCompactADimension(part);
}
void paint( Graphics2D g2d ) {
boolean anythingRendered = false;
boolean anythingRenderedFromStyle = false;
boolean anythingRenderedFromCache = false;
for ( int i = 0; i < _parts.length; i++ ) {
LayerPartitionCache.PaintOutcome outcome = _parts[i].paint(g2d, _renderer);
anythingRendered |= ( outcome != LayerPartitionCache.PaintOutcome.NOTHING_RENDERED );
anythingRenderedFromStyle |= ( outcome == LayerPartitionCache.PaintOutcome.RENDERED_FROM_STYLE);
anythingRenderedFromCache |= ( outcome == LayerPartitionCache.PaintOutcome.RENDERED_FROM_CACHE);
// The lifted out piece always sits directly after the first cached part: between
// the two parts of a noise cut, and on top of the single part of a painter cut.
if ( i == 0 ) {
final Size size = _uncachedPartition.boxModel().size();
if ( size.hasPositiveWidth() && size.hasPositiveHeight() ) {
StyleRenderer.renderStyleOn(_layer, _uncachedPartition, g2d);
anythingRendered = true;
}
}
}
if ( _paintsAtThisSize < PAINTS_UNTIL_REJOINED )
_paintsAtThisSize++;
if ( !anythingRendered )
return; // Nothing reached the destination, so this was not a paint of this layer.
if ( anythingRenderedFromStyle || !anythingRenderedFromCache )
_paintCacheMissCount++;
else
_paintCacheHitCount++;
}
Tuple<BufferedImage> renderedImages() {
List<BufferedImage> images = new ArrayList<>(_parts.length);
for ( LayerPartitionCache part : _parts ) {
BufferedImage rendered = part.renderedImage();
if ( rendered != null )
images.add(rendered);
}
return Tuple.of(BufferedImage.class, images);
}
/**
* The shapes a layer is cached in. Each one names the cached parts it consists of and the
* uncached piece (if any), which is instead rendered straight onto the destination after
* the first of them - see {@link LayerRenderConfPartition} for why a layer is ever cut.
*/
private enum PartitioningPolicy
{
/** One cached rendering of everything. */
NONE( Tuple.of(LayerRenderConfPartition.WHOLE), null ),
/** Cut around the noises, so that everything else keeps a size independent cache
* entry across a resize. Temporary: only while the component is resizing. */
AROUND_NOISES(
Tuple.of(LayerRenderConfPartition.UNDER_NOISE, LayerRenderConfPartition.OVER_NOISE),
LayerRenderConfPartition.NOISES
),
/** Cut around the user painters, so that a layer which carries one is cacheable at
* all rather than being re-rendered whole on every paint. */
AROUND_PAINTERS(
Tuple.of(LayerRenderConfPartition.UNDER_PAINTERS),
LayerRenderConfPartition.PAINTERS
);
@SuppressWarnings("ImmutableEnumChecker") // A Tuple is immutable, it is simply not annotated as such.
private final Tuple<LayerRenderConfPartition> _cachedParts;
private final @Nullable LayerRenderConfPartition _uncachedPart;
PartitioningPolicy( Tuple<LayerRenderConfPartition> cachedParts, @Nullable LayerRenderConfPartition uncachedPart ) {
_cachedParts = cachedParts;
_uncachedPart = uncachedPart;
}
LayerPartitionCache[] newPartsFor( UI.Layer layer ) {
LayerPartitionCache[] parts = new LayerPartitionCache[_cachedParts.size()];
for ( int i = 0; i < parts.length; i++ )
parts[i] = new LayerPartitionCache(layer, _cachedParts.get(i));
return parts;
}
LayerRenderConf uncachedPartitionOf( LayerRenderConf full ) {
return ( _uncachedPart == null ? LayerRenderConf.none() : _uncachedPart.restrict(full) );
}
}
}