// // $Id: FrameManager.java,v 1.19 2002/11/15 23:58:34 mdb Exp $ package com.threerings.media; import java.applet.Applet; import java.awt.Component; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsDevice; import java.awt.Image; import java.awt.Insets; import java.awt.Rectangle; import java.awt.Window; import java.awt.image.BufferStrategy; import java.awt.image.VolatileImage; import java.awt.EventQueue; import javax.swing.JComponent; import javax.swing.JLayeredPane; import javax.swing.RepaintManager; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import com.samskivert.util.Interval; import com.samskivert.util.IntervalManager; import com.samskivert.util.ObserverList; import com.samskivert.util.StringUtil; import com.threerings.media.util.PerformanceMonitor; import com.threerings.media.util.PerformanceObserver; /** * Provides a central point from which the computation for each "frame" or * tick can be dispatched. This assumed that the application structures * its activity around the rendering of each frame, which is a common * architecture for games. The animation and sprite support provided by * other classes in this package are structured for use in an application * that uses a frame manager to tick everything once per frame. * *
The frame manager goes through a simple two part procedure every * frame: * *
The ticking and rendering takes place on the AWT thread so as to * avoid the need for complicated coordination between AWT event handler * code and frame code. However, this means that all AWT (and Swing) event * handlers must not perform any complicated processing. After * each frame, control of the AWT thread is given back to the AWT which * processes all pending AWT events before giving the frame manager an * opportunity to process the next frame. Thus the convenience of * everything running on the AWT thread comes with the price of requiring * that AWT event handlers not block or perform any intensive processing. * In general, this is a sensible structure for an application anyhow, so * this organization tends to be preferable to an organization where the * AWT and frame threads are separate and must tread lightly so as not to * collide. * *
Note: the way that JScrollPane goes about improving
* performance when scrolling complicated contents cannot work with active
* rendering. If you use a JScrollPane in an application that
* uses the frame manager, you should either use the provided {@link
* SafeScrollPane} or set your scroll panes' viewports to
* SIMPLE_SCROLL_MODE.
*/
public class FrameManager
implements PerformanceObserver
{
/**
* Constructs a frame manager that will do its rendering to the
* supplied frame. It is likely that the caller will want to have put
* the frame into full-screen exclusive mode prior to providing it to
* the frame manager so that the frame manager can take advantage of
* optimizations available in that mode.
*
* @see GraphicsDevice#setFullScreenWindow
*/
public FrameManager (Frame frame)
{
_frame = frame;
_frame.setIgnoreRepaint(true);
// set up our custom repaint manager
_remgr = new FrameRepaintManager(_frame);
RepaintManager.setCurrentManager(_remgr);
// turn off double buffering for the whole business because we
// handle repaints
_remgr.setDoubleBufferingEnabled(false);
// register with the performance monitor
PerformanceMonitor.register(this, "frame-rate", 1000l);
}
/**
* Instructs the frame manager to target the specified number of
* frames per second. If the computation and rendering for a frame are
* completed with time to spare, the frame manager will wait until the
* proper time to begin processing for the next frame. If a frame
* takes longer than its alotted time, the frame manager will
* immediately begin processing on the next frame.
*/
public void setTargetFrameRate (int fps)
{
// compute the number of milliseconds per frame
_millisPerFrame = 1000/fps;
}
/**
* Registers a frame participant. The participant will be given the
* opportunity to do processing and rendering on each frame.
*/
public void registerFrameParticipant (FrameParticipant participant)
{
_participants.add(participant);
}
/**
* Removes a frame participant.
*/
public void removeFrameParticipant (FrameParticipant participant)
{
_participants.remove(participant);
}
/**
* Starts up the per-frame tick
*/
public void start ()
{
if (_timer == null) {
_timer = new Timer(true);
_timer.scheduleAtFixedRate(_callTick, new Date(), _millisPerFrame);
}
}
/**
* Stops the per-frame tick.
*/
public synchronized void stop ()
{
if (_timer != null) {
_timer.cancel();
_timer = null;
}
}
/**
* Returns true if the tick interval is be running (not necessarily at
* that instant, but in general).
*/
public synchronized boolean isRunning ()
{
return (_timer != null);
}
/**
* Returns true if we are in the middle of a call to {@link #tick}.
*/
protected synchronized boolean isTicking ()
{
return _ticking;
}
/**
* Called to perform the frame processing and rendering.
*/
protected void tick (long tickStamp)
{
try {
synchronized (this) {
_ticking = true;
}
// if our frame is not showing (or is impossibly sized), don't try
// rendering anything
if (_frame.isShowing() &&
_frame.getWidth() > 0 && _frame.getHeight() > 0) {
// tick our participants
tickParticipants(tickStamp);
// repaint our participants
paintParticipants(tickStamp);
}
// note that we've done a frame
// PerformanceMonitor.tick(this, "frame-rate");
} finally {
synchronized (this) {
_ticking = false;
}
}
}
/**
* Called once per frame to invoke {@link FrameParticipant#tick} on
* all of our frame participants.
*/
protected void tickParticipants (long tickStamp)
{
// validate any invalid components
try {
_remgr.validateComponents();
} catch (Throwable t) {
Log.warning("Failure validating components.");
Log.logStackTrace(t);
}
// tick all of our frame participants
_participantTickOp.setTickStamp(tickStamp);
_participants.apply(_participantTickOp);
}
/**
* Called once per frame to invoke {@link Component#paint} on all of
* our frame participants' components.
*/
protected void paintParticipants (long tickStamp)
{
// // create our buffer strategy if we don't already have one
// if (_bufstrat == null) {
// _frame.createBufferStrategy(2);
// _bufstrat = _frame.getBufferStrategy();
// }
// start out assuming we can do an incremental render
boolean incremental = true;
do {
GraphicsConfiguration gc = _frame.getGraphicsConfiguration();
// create our off-screen buffer if necessary
if (_backimg == null) {
createBackBuffer(gc);
}
// make sure our back buffer hasn't disappeared
int valres = _backimg.validate(gc);
// if we've changed resolutions, recreate the buffer
if (valres == VolatileImage.IMAGE_INCOMPATIBLE) {
// Log.info("Back buffer incompatible, recreating.");
createBackBuffer(gc);
}
// if the image wasn't A-OK, we need to rerender the whole
// business rather than just the dirty parts
if (valres != VolatileImage.IMAGE_OK) {
// Log.info("Lost back buffer, redrawing.");
incremental = false;
}
// g = _bufstrat.getDrawGraphics();
// dirty everything if we're not incrementally rendering
if (!incremental) {
_frame.update(_bgfx);
}
// paint our frame participants (which want to be handled
// specially)
_participantPaintOp.setGraphics(_bgfx);
_participants.apply(_participantPaintOp);
// repaint any widgets that have declared there need to be
// repainted since the last tick
_remgr.paintComponents(_bgfx, this);
// we cache our frame's graphics object so that we can avoid
// instantiating a new one on every tick
if (_fgfx == null) {
_fgfx = _frame.getGraphics();
}
_fgfx.drawImage(_backimg, 0, 0, null);
// _bufstrat.show();
// if we loop through a second time, we'll need to rerender
// everything
incremental = false;
} while (_backimg.contentsLost());
}
/**
* Renders all components in all {@link JLayeredPane} layers that
* intersect the supplied bounds.
*/
protected void renderLayers (Graphics g, Component pcomp, Rectangle bounds,
boolean[] clipped)
{
JLayeredPane lpane =
JLayeredPane.getLayeredPaneAbove(pcomp);
if (lpane != null) {
renderLayer(g, bounds, lpane, clipped, JLayeredPane.PALETTE_LAYER);
renderLayer(g, bounds, lpane, clipped, JLayeredPane.MODAL_LAYER);
renderLayer(g, bounds, lpane, clipped, JLayeredPane.POPUP_LAYER);
renderLayer(g, bounds, lpane, clipped, JLayeredPane.DRAG_LAYER);
}
}
/**
* Renders all components in the specified layer of the supplied
* layered pane that intersect the supplied bounds.
*/
protected void renderLayer (Graphics g, Rectangle bounds, JLayeredPane pane,
boolean[] clipped, Integer layer)
{
// stop now if there are no components in that layer
int ccount = pane.getComponentCountInLayer(layer.intValue());
if (ccount == 0) {
return;
}
// render them up
Component[] comps = pane.getComponentsInLayer(layer.intValue());
for (int ii = 0; ii < ccount; ii++) {
Component comp = comps[ii];
_lbounds.setBounds(0, 0, comp.getWidth(), comp.getHeight());
getRoot(comp, _lbounds);
if (!_lbounds.intersects(bounds)) {
continue;
}
// if the clipping region has not yet been set during this
// render pass, the time has come to do so
if (!clipped[0]) {
g.setClip(bounds);
clipped[0] = true;
}
// translate into the components coordinate system and render
g.translate(_lbounds.x, _lbounds.y);
comp.paint(g);
g.translate(-_lbounds.x, -_lbounds.y);
}
}
// documentation inherited
public void checkpoint (String name, int ticks)
{
Log.info("Frames in last second: " + ticks);
}
/**
* Creates the off-screen buffer used to perform double buffered
* rendering of the animated panel.
*/
protected void createBackBuffer (GraphicsConfiguration gc)
{
// if we have an old image, clear it out
if (_backimg != null) {
_backimg.flush();
_bgfx.dispose();
}
// create the offscreen buffer
int width = _frame.getWidth(), height = _frame.getHeight();
_backimg = gc.createCompatibleVolatileImage(width, height);
// fill the back buffer with white
_bgfx = _backimg.getGraphics();
_bgfx.fillRect(0, 0, width, height);
// clear out our frame graphics in case that became invalid for
// the same reasons our back buffer became invalid
_fgfx = null;
}
/**
* Returns the root component for the supplied component or null if it
* is not part of a rooted hierarchy or if any parent along the way is
* found to be hidden or without a peer. Along the way, it adjusts the
* supplied component-relative rectangle to be relative to the
* returned root component.
*/
public static Component getRoot (Component comp, Rectangle rect)
{
for (Component c = comp; c != null; c = c.getParent()) {
if (!c.isVisible() || !c.isDisplayable()) {
return null;
}
if (c instanceof Window || c instanceof Applet) {
return c;
}
rect.x += c.getX();
rect.y += c.getY();
}
return null;
}
/**
* An observer operation that calls {@link FrameParticipant#tick} with
* a specified tick stamp for all {@link FrameParticipant} objects in
* the observer list to which this operation is applied.
*/
protected class ParticipantTickOp
implements ObserverList.ObserverOp
{
/**
* Sets the tick stamp to be applied to the participants.
*/
public void setTickStamp (long tickStamp)
{
_tickStamp = tickStamp;
}
// documentation inherited
public boolean apply (Object observer)
{
try {
((FrameParticipant)observer).tick(_tickStamp);
} catch (Throwable t) {
Log.warning("Frame participant choked during tick " +
"[part=" +
StringUtil.safeToString(observer) + "].");
Log.logStackTrace(t);
}
return true;
}
/** The tick stamp to be applied to each frame participant. */
protected long _tickStamp;
}
/**
* An observer operation that paints the components associated with
* all {@link FrameParticipant} objects in the observer list to which
* this operation is applied.
*/
protected class ParticipantPaintOp
implements ObserverList.ObserverOp
{
/**
* Sets the graphics context to which the frame participants
* render themselves.
*/
public void setGraphics (Graphics g)
{
_g = g;
}
// documentation inherited
public boolean apply (Object observer)
{
FrameParticipant part = (FrameParticipant)observer;
Component pcomp = part.getComponent();
if (pcomp == null) {
return true;
}
// get the bounds of this component
pcomp.getBounds(_bounds);
// the bounds adjustment we're about to call will add in the
// components initial bounds offsets, so we remove them here
_bounds.setLocation(0, 0);
// convert them into top-level coordinates; also note that if
// this component does not have a valid or visible root, we
// don't want to paint it either
if (getRoot(pcomp, _bounds) == null) {
return true;
}
try {
// render this participant; we don't set the clip because
// frame participants are expected to handle clipping
// themselves; otherwise we might pointlessly set the clip
// here, creating a few Rectangle objects in the process,
// only to have the frame participant immediately set the
// clip to something more sensible
_g.translate(_bounds.x, _bounds.y);
pcomp.paint(_g);
_g.translate(-_bounds.x, -_bounds.y);
} catch (Throwable t) {
String ptos = StringUtil.safeToString(part);
Log.warning("Frame participant choked during paint " +
"[part=" + ptos + "].");
Log.logStackTrace(t);
}
// render any components in our layered pane that are not in
// the default layer
_clipped[0] = false;
renderLayers(_g, pcomp, _bounds, _clipped);
return true;
}
/** The graphics context to which the participants render. */
protected Graphics _g;
/** A handy rectangle that we reuse time and again to avoid having
* to instantiate a new rectangle in the midst of the core
* rendering loop. */
protected Rectangle _bounds = new Rectangle();
}
/** The frame into which we do our rendering. */
protected Frame _frame;
/** Our custom repaint manager. */
protected FrameRepaintManager _remgr;
// /** The buffer strategy used to do our rendering. */
// protected BufferStrategy _bufstrat;
/** The image used to render off-screen. */
protected VolatileImage _backimg;
/** The number of milliseconds per frame (14 by default, which gives
* an fps of ~71). */
protected long _millisPerFrame = 14;
/** The timer that dispatches our frame ticks. */
protected Timer _timer;
/** Used to detect when we need to drop frames. */
protected boolean _ticking;
/** The graphics object from our back buffer. */
protected Graphics _bgfx;
/** The graphics object from our frame. */
protected Graphics _fgfx;
/** Used to avoid creating rectangles when rendering layered
* components. */
protected Rectangle _lbounds = new Rectangle();
/** Used to lazily set the clip when painting popups and other
* "layered" components. */
protected boolean[] _clipped = new boolean[1];
/** Used to effect periodic calls to {@link #tick}. */
protected TimerTask _callTick = new TimerTask () {
public void run () {
if (EventQueue.isDispatchThread()) {
tick(System.currentTimeMillis());
} else if (!isTicking()) {
EventQueue.invokeLater(this);
} else {
Log.info("Dropped frame.");
}
}
};
/** The entites that are ticked each frame. */
protected ObserverList _participants =
new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY);
/** The observer operation applied to all frame participants each tick. */
protected ParticipantTickOp _participantTickOp = new ParticipantTickOp();
/** The observer operation applied to all frame participants each time
* the frame is rendered. */
protected ParticipantPaintOp _participantPaintOp = new ParticipantPaintOp();
}