org.jdesktop.swing.animation.rendering.JActiveRenderer Maven / Gradle / Ivy
package org.jdesktop.swing.animation.rendering;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.image.BufferedImage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import org.jdesktop.core.animation.i18n.I18N;
import org.jdesktop.core.animation.rendering.JRenderer;
import org.jdesktop.core.animation.rendering.JRendererTarget;
import org.jdesktop.core.animation.timing.TimingSource;
import org.jdesktop.core.animation.timing.WrappedRunnable;
import org.jdesktop.core.animation.timing.sources.ManualTimingSource;
/**
* Manages two-thread active rendering on a Swing {@link JRendererPanel}.
*
* To use this renderer a client constructs a {@link JRendererPanel} and passes
* it to the constructor with a flag indicating if the passed component will
* have children that need to be rendered and a {@link JRendererTarget}
* implementation. A typical sequence would be
*
*
* JFrame frame = new JFrame("Renderer Demonstration");
* final JRendererPanel on = new JRendererPanel();
* frame.setContentPane(on);
* final JRendererTarget<GraphicsConfiguration, Graphics2D> target = this;
* JRenderer renderer = new JActiveRenderer(on, target, true);
*
*
* In the above snippet on will be rendered to. Swing children will be
* added to on by the client code (indicated by passing true
* as the second argument) and should be shown. The enclosing instance,
* this, implements {@link JRendererTarget} and will be called to
* customize what is displayed on-screen.
*
* The {@link JRendererTarget} implementation is called according to the
* following protocol.
*
* - {@link JRendererTarget#renderSetup(Object)} is called once when
* on is made visible. It allows the client to perform any necessary
* setup.
* - {@link JRendererTarget#renderUpdate()} is called at the start of each
* rendering cycle to allow the client to update its state prior to rendering.
* - {@link JRendererTarget#render(Object, int, int)} is called after
* renderUpdate() during each rendering cycle to allow the client to
* control what is displayed on-screen.
* - {@link JRendererTarget#renderShutdown()} is called once after
* {@link JActiveRenderer#shutdown()} to allow the implementation to perform any
* necessary cleanup.
*
* The rendering cycle occurs as fast as the hardware can support. So
* renderUpdate() and render() will be called many times per
* second.
*
* There are two threads involved in active rendering: (1) a rendering thread
* and (2) the Swing Event Dispatch Thread (EDT). When the passed
* {@link JComponent} is made visible this is detected in the EDT and two tasks
* are submitted to be executed in the rendering thread: (1) The
* {@link JRendererTarget#renderSetup(Object)} method is called to let the
* client perform its setup. (2) The rendering loop is started.
*
* The rendering thread runs the following sequence as fast as it can:
*
* - A call is made to {@link JRendererTarget#renderUpdate()} to allow the
* client to update its state.
* - Internally, {@link CountDownLatch#await()} is called on the latch that is
* waiting for the EDT thread to finish painting (skipped during the first
* iteration).
* - A call is made to {@link JRendererTarget#render(Object, int, int)} to
* allow the client to render onto an off-screen image.
* - A call to {@link SwingUtilities#invokeLater(Runnable)} asks the EDT
* thread to paint the off-screen image on the screen.
*
*
* The EDT thread, in addition to its normal Swing processing, performs the
* following:
*
* - Paints the off-screen image to the screen.
* - Invokes {@link CountDownLatch#countDown()} to "pass" the off-screen image
* back to the animator thread.
*
*
* This two-thread active rendering approach has several advantages. First, it
* never blocks the EDT. Second, it allows the
* {@link JRendererTarget#renderUpdate()} to execute concurrently with painting
* to the screen. Third, while the render thread can block, this only occurs if
* painting to the screen takes longer than invoking
* {@link JRendererTarget#renderUpdate()}.
*
* The implementation depends upon the safe sharing of the {@link BufferedImage}
* used as the off-screen image between the rendering thread and the EDT. The
* use of {@link SwingUtilities#invokeLater(Runnable)} safely "passes" the image
* from the rendering thread to the EDT and the use of the
* {@link CountDownLatch} safely "passes" from the EDT back to the rendering
* thread.
*
* Clients can execute code in the rendering thread by calling
* {@link #invokeLater(Runnable)} (in a manner similar to
* {@link SwingUtilities#invokeLater(Runnable)}). This call is essential to
* safely notify the rendering thread about events that occur in the EDT or
* other program threads.
*
* Several statistics are tracked which can be queried to understand active
* rendering performance.
*
* - {@link #getFPS()} provides "frames per second" or how many times per
* second the screen is painted. This is the best measure of overall performance
* and is often displayed on-screen.
* - {@link #getAverageCycleTimeNanos()} provides how many nanoseconds, on
* average, it tools to execute one complete rendering cycle.
* - {@link #getAveragePaintTimeNanos()} provides how many nanoseconds, on
* average, it took the EDT to paint the off-screen image to the screen.
* - {@link #getAverageRenderTimeNanos()} provides how many nanoseconds, on
* average, it took the rendering thread to render to the off-screen image (by
* calling {@link JRendererTarget#render(Object, int, int)}).
* - {@link #getAveragePaintWaitTimeNanos()} provides how many nanoseconds, on
* average, the rendering thread "blocked" waiting for the EDT to finish
* painting to the screen. Because the rendering thread invokes
* {@link JRendererTarget#renderUpdate()} before it has to "block" and wait for
* the EDT to finish painting, this time can be significantly smaller than
* {@link #getAveragePaintTimeNanos()}. If this time is not smaller than
* {@link #getAveragePaintTimeNanos()} then this two-thread active rendering
* approach should probably not be used, i.e., a simple loop in a single thread
* will be more efficient (i.e., provide more FPS).
*
*
* Adding Swing components as children of on is supported. Ensure that
* true is passed as the third argument to
* {@link #JActiveRenderer(JRendererPanel, JRendererTarget, boolean)}. The
* children are drawn in the EDT and never accessed in the rendering thread.
*
* Use of the Timing Framework is supported via a {@link ManualTimingSource}
* that can be obtained via {@link #getTimingSource()}. This timing source is
* "ticked" once per rendering cycle. Client code should consider setting the
* timing source as the default for animations, similar to the snippet below.
*
*
* AnimatorBuilder.setDefaultTimingSource(renderer.getTimingSource());
*
*
* Then the timing source's tick() method is invoked within the
* rendering thread just prior to the call to renderUpdate().
*
* @author Tim Halloran
*
* @see JRendererTarget
* @see ManualTimingSource
*/
public final class JActiveRenderer implements JRenderer {
/*
* Shared state
*/
final ManualTimingSource f_ts = new ManualTimingSource();
final ExecutorService f_executor = Executors.newSingleThreadExecutor();
final AtomicBoolean f_renderingStarted = new AtomicBoolean(false);
final AtomicReference f_edtPaintLatch = new AtomicReference();
final AtomicReference f_renderingBuffer = new AtomicReference();
final AtomicReference f_replacementBuffer = new AtomicReference();
final AtomicBoolean f_shutdownRendering = new AtomicBoolean(false);
/*
* Statistics counters (shared)
*/
final AtomicLong f_totalRenderTime = new AtomicLong(0);
final AtomicLong f_renderCount = new AtomicLong(0);
final AtomicLong f_paintingRequestedNanos = new AtomicLong(0);
final AtomicLong f_totalPaintWaitTime = new AtomicLong(0);
final AtomicLong f_paintWaitCount = new AtomicLong(0);
final AtomicLong f_totalPaintTime = new AtomicLong();
final AtomicLong f_paintCount = new AtomicLong(0);
/*
* Thread-confined to the renderer thread (f_executor)
*/
final JRendererTarget f_target;
/*
* Thread-confined to the EDT thread
*/
final JRendererPanel f_on;
final boolean f_hasChildren;
/**
* Constructs a new active renderer.
*
* Should only be invoked from the Swing EDT.
*
* @param on
* the Swing component to render on.
* @param target
* to be called to control what is rendered.
* @param hasChildren
* {@code true} if on has child components that need to be
* painted. If on has no child components passing
* {@code false} can improve rendering performance.
*
* @throws IllegalArgumentException
* if either on or target are {@code null}.
* @throws IllegalStateException
* if invoked outside of the Swing EDT.
*/
public JActiveRenderer(JRendererPanel on, JRendererTarget target, boolean hasChildren) {
if (!SwingUtilities.isEventDispatchThread())
throw new IllegalStateException(I18N.err(100));
if (on == null)
throw new IllegalArgumentException(I18N.err(1, "on"));
f_on = on;
if (target == null)
throw new IllegalArgumentException(I18N.err(1, "target"));
f_target = target;
f_hasChildren = hasChildren;
/*
* Create and setup an on-screen panel to paint onto.
*/
f_on.setDoubleBuffered(false);
f_on.setOpaque(true);
f_on.setIgnoreRepaint(true);
f_on.addComponentListener(new ComponentAdapter() {
/**
* Used to detect resize notifications that don't really resize the Swing
* component we are rendering on.
*/
int f_width, f_height;
@Override
public void componentResized(ComponentEvent e) {
if (f_on.getWidth() < 1 || f_on.getHeight() < 1)
return;
if (f_on.getWidth() == f_width && f_on.getHeight() == f_height)
return;
f_width = f_on.getWidth();
f_height = f_on.getHeight();
final Insets insets = f_on.getInsets();
final GraphicsConfiguration gc = f_on.getGraphicsConfiguration();
if (gc != null) {
final BufferedImage buffer = gc.createCompatibleImage(f_width - insets.right - insets.left, f_height - insets.top
- insets.bottom);
f_replacementBuffer.set(buffer);
if (f_renderingStarted.compareAndSet(false, true)) {
/*
* The first time we have an on-screen panel and an off-screen
* buffer we are ready to begin rendering.
*/
invokeLater(new Runnable() {
@Override
public void run() {
f_target.renderSetup(gc);
}
});
invokeLater(f_renderTask);
}
}
}
});
}
public void shutdown() {
f_shutdownRendering.set(true);
f_executor.shutdown();
f_target.renderShutdown();
}
public long getFPS() {
final long avgCycleTime = getAverageCycleTimeNanos();
if (avgCycleTime != 0) {
return SECONDS.toNanos(1) / avgCycleTime;
} else
return 0;
}
public long getAverageCycleTimeNanos() {
final long renderCount = f_renderCount.get();
final long totalRenderTime = f_totalRenderTime.get();
final long totalPaintWaitTime = f_totalPaintWaitTime.get();
if (renderCount != 0) {
return (totalRenderTime + totalPaintWaitTime) / renderCount;
} else
return 0;
}
/**
* Calculates the average time spent rendering in the rendering thread. This
* is the time spent in the call to
* {@link JRendererTarget#render(Object, int, int)}.
*
* Safe to be called at any time within any thread.
*
* @return average time in nanoseconds.
*/
public long getAverageRenderTimeNanos() {
final long totalRenderTime = f_totalRenderTime.get();
final long renderCount = f_renderCount.get();
if (renderCount > 0)
return totalRenderTime / renderCount;
else
return totalRenderTime;
}
/**
* Gets the average amount of time spent waiting in the animator thread for
* the EDT thread to complete painting to the screen.
*
* Safe to be called at any time within any thread.
*
* @return average time in nanoseconds.
*/
public long getAveragePaintWaitTimeNanos() {
final long totalPaintWaitTime = f_totalPaintWaitTime.get();
final long paintWaitCount = f_paintWaitCount.get();
if (paintWaitCount > 0)
return totalPaintWaitTime / paintWaitCount;
else
return totalPaintWaitTime;
}
/**
* The time spent within the EDT thread painting to the screen.
*
* Safe to be called at any time within any thread.
*
* @return average time in nanoseconds.
*/
public long getAveragePaintTimeNanos() {
final long totalPaintTime = f_totalPaintTime.get();
final long paintCount = f_paintCount.get();
if (paintCount > 0)
return totalPaintTime / paintCount;
else
return totalPaintTime;
}
@Override
public TimingSource getTimingSource() {
return f_ts;
}
public void invokeLater(final Runnable task) {
/*
* Although the executor recovers from unhandled exceptions, it doesn't log
* them or give any indication whatsoever that a problem occurred. So we
* wrap the passed task to log any exception that is thrown during its
* execution.
*/
if (!f_executor.isShutdown())
f_executor.submit(new WrappedRunnable(task));
}
/**
* One cycle of the rendering loop. This task is always executed in the
* rendering thread.
*/
final Runnable f_renderTask = new Runnable() {
@Override
public void run() {
/*
* We tick any animations and then update the game state while the EDT is
* painting.
*/
f_ts.tick();
f_target.renderUpdate();
/*
* Wait for the EDT to finish painting.
*/
final CountDownLatch edtPaintLatch = f_edtPaintLatch.get();
if (edtPaintLatch != null) {
try {
edtPaintLatch.await();
} catch (InterruptedException e) {
Logger.getAnonymousLogger().log(Level.WARNING, I18N.err(101), e);
}
}
if (f_shutdownRendering.get())
return;
/*
* We will render onto an off-screen buffer image. This image has to be
* replaced if the window is resized.
*/
final BufferedImage replacementBuffer = f_replacementBuffer.getAndSet(null);
final BufferedImage buffer;
if (replacementBuffer != null) {
final BufferedImage oldBuffer = f_renderingBuffer.getAndSet(replacementBuffer);
if (oldBuffer != null)
oldBuffer.flush();
buffer = replacementBuffer;
f_totalRenderTime.set(0);
f_renderCount.set(0);
f_totalPaintWaitTime.set(0);
f_paintWaitCount.set(0);
} else {
buffer = f_renderingBuffer.get();
}
if (buffer != null) {
/*
* Render onto the off-screen image.
*/
long t1 = System.nanoTime();
final long paintingRequestedNanos = f_paintingRequestedNanos.get();
if (paintingRequestedNanos != 0) {
f_totalPaintWaitTime.getAndAdd(t1 - paintingRequestedNanos);
f_paintWaitCount.incrementAndGet();
}
Graphics2D g2d = buffer.createGraphics();
f_target.render(g2d, buffer.getWidth(), buffer.getHeight());
g2d.dispose();
final long now = System.nanoTime();
f_paintingRequestedNanos.set(now);
f_totalRenderTime.getAndAdd(now - t1);
f_renderCount.incrementAndGet();
/*
* Send the off-screen image to the EDT to be painted onto the screen.
*/
f_edtPaintLatch.set(new CountDownLatch(1));
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
paintOn(f_edtPaintLatch.get());
}
});
}
if (!f_shutdownRendering.get())
invokeLater(this);
}
};
/**
* When this method is called in the EDT it has access to the rendering buffer
* until it invokes {@link CountDownLatch#countDown()} which informs the
* rendering thread that painting to the on-screen panel is complete.
*
* @param paintingCompleted
* signals that painting to the screen is compete when
* {@link CountDownLatch#countDown()} is invoked.
*/
void paintOn(CountDownLatch paintingCompleted) {
final long t1 = System.nanoTime();
final Graphics g = f_on.getGraphics();
final BufferedImage buffer = f_renderingBuffer.get();
if (g != null && buffer != null) { // probably not visible
/*
* Paint the Swing children of this component, if necessary.
*/
if (f_hasChildren) {
final Graphics2D g2d = buffer.createGraphics();
f_on.renderChildren(g2d);
g2d.dispose();
}
g.drawImage(buffer, 0, 0, null);
}
f_totalPaintTime.getAndAdd(System.nanoTime() - t1);
f_paintCount.incrementAndGet();
paintingCompleted.countDown();
}
}