Files
narya/src/java/com/threerings/media/FrameManager.java
T
Michael Bayne 2d0897ed4b Default to flip-buffer rendering on the MacOS because the volatile back
buffer approach don't work.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2535 542714f4-19e9-0310-aa3c-eee0fc999fb1
2003-05-02 23:53:55 +00:00

844 lines
28 KiB
Java

//
// $Id: FrameManager.java,v 1.41 2003/05/02 23:53:55 mdb Exp $
package com.threerings.media;
import java.applet.Applet;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.EventQueue;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLayeredPane;
import javax.swing.RepaintManager;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import com.samskivert.swing.Label;
import com.samskivert.util.DebugChords;
import com.samskivert.util.ObserverList;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.RuntimeAdjust;
import com.samskivert.util.StringUtil;
import com.threerings.media.timer.MediaTimer;
import com.threerings.media.timer.SystemMediaTimer;
import com.threerings.media.util.TrailingAverage;
/**
* 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.
*
* <p> The frame manager goes through a simple two part procedure every
* frame:
*
* <ul>
* <li> Ticking all of the frame participants: in {@link
* FrameParticipant#tick}, any processing that need be performed during
* this frame should be performed. Care should be taken not to execute
* code that will take unduly long, instead such processing should be
* broken up so that it can be performed in small pieces every frame (or
* performed on a separate thread with the results safely communicated
* back to the frame participants for incorporation into the rendering
* loop).
*
* <li> Painting the user interface hierarchy: the top-level component
* (the frame) is painted (via a call to {@link JFrame#paint}) into a flip
* buffer (if supported, an off-screen buffer if not). Updates that were
* computed during the tick should be rendered in this call to paint. The
* paint call will propagate down to all components in the UI hierarchy,
* some of which may be {@link FrameParticipant}s and will have prepared
* themselves for their upcoming painting in the previous call to {@link
* FrameParticipant#tick}. When the call to paint completes, the flip
* buffer is flipped and the process starts all over again. </ul>
*
* <p> 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 <em>must not</em> 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.
*
* <p> Note: the way that <code>JScrollPane</code> goes about improving
* performance when scrolling complicated contents cannot work with active
* rendering. If you use a <code>JScrollPane</code> in an application that
* uses the frame manager, you should either use the provided {@link
* SafeScrollPane} or set your scroll panes' viewports to
* <code>SIMPLE_SCROLL_MODE</code>.
*/
public abstract class FrameManager
{
/**
* Creates a frame manager that will use a {@link SystemMediaTimer} to
* obtain timing information, which is available on every platform,
* but returns inaccurate time stamps on many platforms.
*
* @see #newInstance(JFrame, MediaTimer)
*/
public static FrameManager newInstance (JFrame frame)
{
return newInstance(frame, new SystemMediaTimer());
}
/**
* 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 static FrameManager newInstance (JFrame frame, MediaTimer timer)
{
FrameManager fmgr;
if (_useFlip.getValue()) {
Log.info("Creating flip frame manager.");
fmgr = new FlipFrameManager();
} else {
Log.info("Creating back frame manager.");
fmgr = new BackFrameManager();
}
fmgr.init(frame, timer);
return fmgr;
}
/**
* Initializes this frame manager and prepares it for operation.
*/
protected void init (JFrame frame, MediaTimer timer)
{
_frame = frame;
if (frame instanceof ManagedJFrame) {
((ManagedJFrame)_frame).init(this);
}
_timer = timer;
// 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);
if (DEBUG_EVENTS) {
addTestListeners();
}
}
/**
* Adds a variety of listeners to the frame in order to provide
* visibility into the various events received by the frame.
*/
protected void addTestListeners ()
{
// add a test window listener
_frame.addWindowListener(new WindowListener() {
public void windowActivated (WindowEvent e) {
Log.info("Window activated [evt=" + e + "].");
}
public void windowClosed (WindowEvent e) {
Log.info("Window closed [evt=" + e + "].");
}
public void windowClosing (WindowEvent e) {
Log.info("Window closing [evt=" + e + "].");
}
public void windowDeactivated (WindowEvent e) {
Log.info("Window deactivated [evt=" + e + "].");
}
public void windowDeiconified (WindowEvent e) {
Log.info("Window deiconified [evt=" + e + "].");
}
public void windowIconified (WindowEvent e) {
Log.info("Window iconified [evt=" + e + "].");
}
public void windowOpened (WindowEvent e) {
Log.info("Window opened [evt=" + e + "].");
}
});
// add a component listener
_frame.addComponentListener(new ComponentListener() {
public void componentHidden (ComponentEvent e) {
Log.info("Window component hidden [evt=" + e + "].");
}
public void componentShown (ComponentEvent e) {
Log.info("Window component shown [evt=" + e + "].");
}
public void componentMoved (ComponentEvent e) {
Log.info("Window component moved [evt=" + e + "].");
}
public void componentResized (ComponentEvent e) {
Log.info("Window component resized [evt=" + e + "].");
}
});
// add test ancestor focus listener
_frame.getRootPane().addAncestorListener(
new AncestorListener() {
public void ancestorAdded (AncestorEvent e) {
Log.info("Root pane ancestor added [e=" + e + "].");
}
public void ancestorRemoved (AncestorEvent e) {
Log.info("Root pane ancestor removed [e=" + e + "].");
}
public void ancestorMoved (AncestorEvent e) {
Log.info("Root pane ancestor moved [e=" + e + "].");
}
});
// add test key event dispatcher
KeyboardFocusManager.getCurrentKeyboardFocusManager().
addKeyEventDispatcher(new KeyEventDispatcher() {
public boolean dispatchKeyEvent (KeyEvent e) {
// if ((e.getModifiersEx() & KeyEvent.ALT_DOWN_MASK) != 0 &&
// e.getKeyCode() == KeyEvent.VK_TAB) {
// Log.info("Detected alt-tab key event " +
// "[e=" + e + "].");
// // attempt to eat the event so that windows
// // doesn't alt-tab into unhappy land
// e.consume();
// return true;
// }
return false;
}
});
}
/**
* 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);
}
/**
* Returns a millisecond granularity time stamp using the {@link
* MediaTimer} with which this frame manager was configured.
* <em>Note:</em> this should only be called from the AWT thread.
*/
public long getTimeStamp ()
{
return _timer.getElapsedMillis();
}
/**
* Starts up the per-frame tick
*/
public void start ()
{
if (_ticker == null) {
_ticker = new Ticker();
_ticker.start();
_lastTickStamp = 0;
}
}
/**
* Stops the per-frame tick.
*/
public synchronized void stop ()
{
if (_ticker != null) {
_ticker.cancel();
_ticker = null;
}
}
/**
* Returns true if the tick interval is be running (not necessarily at
* that instant, but in general).
*/
public synchronized boolean isRunning ()
{
return (_ticker != null);
}
/**
* Returns the number of ticks executed in the last second.
*/
public int getPerfTicks ()
{
return Math.round(_fps[1]);
}
/**
* Returns the number of ticks requested in the last second.
*/
public int getPerfTries ()
{
return Math.round(_fps[0]);
}
/**
* Returns debug performance metrics.
*/
public TrailingAverage[] getPerfMetrics ()
{
if (_metrics == null) {
_metrics = new TrailingAverage[] {
new TrailingAverage(150),
new TrailingAverage(150),
new TrailingAverage(150) };
}
return _metrics;
}
/**
* Called to perform the frame processing and rendering.
*/
protected void tick (long tickStamp)
{
long start = 0L, paint = 0L;
if (MediaPanel._perfDebug.getValue()) {
start = paint = System.currentTimeMillis();
}
// 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);
paint = System.currentTimeMillis();
// repaint our participants and components
paint(tickStamp);
}
if (MediaPanel._perfDebug.getValue()) {
long end = System.currentTimeMillis();
getPerfMetrics()[1].record((int)(paint-start));
getPerfMetrics()[2].record((int)(end-paint));
}
}
/**
* Called once per frame to invoke {@link FrameParticipant#tick} on
* all of our frame participants.
*/
protected void tickParticipants (long tickStamp)
{
long gap = tickStamp - _lastTickStamp;
if (_lastTickStamp != 0 && gap > (HANG_DEBUG ? HANG_GAP : BIG_GAP)) {
Log.debug("Long tick delay [delay=" + gap + "ms].");
}
_lastTickStamp = 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 and all dirty components managed
* by our {@link FrameRepaintManager}.
*/
protected abstract void paint (long tickStamp);
/**
* Paints our frame participants and any dirty components via the
* repaint manager.
*
* @return true if anything was painted, false if not.
*/
protected boolean paint (Graphics2D gfx)
{
// paint our frame participants (which want to be handled
// specially)
_participantPaintOp.init(gfx);
_participants.apply(_participantPaintOp);
boolean ppart = _participantPaintOp.paintedSomething();
// repaint any widgets that have declared they need to be
// repainted since the last tick
boolean pcomp = _remgr.paintComponents(gfx, this);
// let the caller know if anybody painted anything
return (ppart || pcomp);
}
/**
* Called by the {@link ManagedJFrame} when our window was hidden and
* reexposed.
*/
protected abstract void restoreFromBack (Rectangle dirty);
/**
* Renders all components in all {@link JLayeredPane} layers that
* intersect the supplied bounds.
*/
protected void renderLayers (Graphics2D 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 (Graphics2D 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);
}
/**
* 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 {
long start = 0L;
if (HANG_DEBUG) {
start = System.currentTimeMillis();
}
((FrameParticipant)observer).tick(_tickStamp);
if (HANG_DEBUG) {
long delay = (System.currentTimeMillis() - start);
if (delay > HANG_GAP) {
Log.info("Whoa nelly! Ticker took a long time " +
"[part=" + observer +
", time=" + delay + "ms].");
}
}
} 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 init (Graphics2D g)
{
_g = g;
_painted = 0;
}
/**
* Returns true if we painted at least one component in our last
* application.
*/
public boolean paintedSomething ()
{
return (_painted > 0);
}
// documentation inherited
public boolean apply (Object observer)
{
FrameParticipant part = (FrameParticipant)observer;
Component pcomp = part.getComponent();
if (pcomp == null || !part.needsPaint()) {
return true;
}
long start = 0L;
if (HANG_DEBUG) {
start = System.currentTimeMillis();
}
// 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);
_painted++;
} 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);
if (HANG_DEBUG) {
long delay = (System.currentTimeMillis() - start);
if (delay > HANG_GAP) {
Log.warning("Whoa nelly! Painter took a long time " +
"[part=" + observer +
", time=" + delay + "ms].");
}
}
return true;
}
/** The graphics context to which the participants render. */
protected Graphics2D _g;
/** The number of participants that were actually painted. */
protected int _painted;
/** 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();
}
/** Used to effect periodic calls to {@link #tick}. */
protected class Ticker extends Thread
{
public void run ()
{
while (_running) {
long start = 0L;
if (MediaPanel._perfDebug.getValue()) {
start = _timer.getElapsedMillis();
}
try {
int sleepGran = _sleepGranularity.getValue();
if (sleepGran > 0) {
Thread.sleep(sleepGran);
} else {
Thread.yield();
}
} catch (InterruptedException ie) {
Log.warning("Ticker thread interrupted.");
}
long woke = _timer.getElapsedMillis();
if (start > 0L) {
getPerfMetrics()[0].record((int)(woke-start));
}
if (woke - _lastAttempt >= _millisPerFrame) {
_lastAttempt = woke;
if (testAndSet()) {
EventQueue.invokeLater(_awtTicker);
}
// else: drop the frame
}
}
}
public void cancel ()
{
_running = false;
}
protected final synchronized boolean testAndSet ()
{
_tries++;
if (!_ticking) {
_ticking = true;
return true;
}
return false;
}
protected final synchronized void clearTicking (long elapsed)
{
if (++_ticks == 100) {
long time = (elapsed - _lastTick);
_fps[0] = _tries * 1000f / time;
_fps[1] = _ticks * 1000f / time;
_lastTick = elapsed;
_ticks = _tries = 0;
}
_ticking = false;
}
/** Used to invoke the call to {@link #tick} on the AWT event
* queue thread. */
protected Runnable _awtTicker = new Runnable ()
{
public void run ()
{
long elapsed = _timer.getElapsedMillis();
try {
tick(elapsed);
} finally {
clearTicking(elapsed);
}
}
};
/** Used to stick a fork in our ticker when desired. */
protected transient boolean _running = true;
/** Used to detect when we need to drop frames. */
protected boolean _ticking;
/** The time at which we last attempted to tick. */
protected long _lastAttempt;
/** Used to compute metrics. */
protected int _tries, _ticks, _time;
/** Used to compute metrics. */
protected long _lastTick;
};
/** The frame into which we do our rendering. */
protected JFrame _frame;
/** Used to obtain timing measurements. */
protected MediaTimer _timer;
/** Our custom repaint manager. */
protected FrameRepaintManager _remgr;
/** The number of milliseconds per frame (14 by default, which gives
* an fps of ~71). */
protected long _millisPerFrame = 14;
/** Used to track big delays in calls to our tick method. */
protected long _lastTickStamp;
/** The thread that dispatches our frame ticks. */
protected Ticker _ticker;
/** Used to track and report frames per second. */
protected float[] _fps = new float[2];
/** Used to track performance metrics. */
protected TrailingAverage[] _metrics;
/** 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];
/** 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();
/** If we don't get ticked for 500ms, that's worth complaining about. */
protected static final long BIG_GAP = 500L;
/** If we don't get ticked for 100ms and we're hang debugging,
* complain. */
protected static final long HANG_GAP = 100L;
/** Enable this to log warnings when ticking or painting takes too
* long. */
protected static final boolean HANG_DEBUG = false;
/** A debug hook that toggles debug rendering of sprite paths. */
protected static RuntimeAdjust.BooleanAdjust _useFlip =
new RuntimeAdjust.BooleanAdjust(
"When active a flip-buffer will be used to manage our " +
"rendering, otherwise a volatile back buffer is used " +
"[requires restart]", "narya.media.frame",
// back buffer rendering doesn't work on the Mac, so we
// default to flip buffer on that platform; we still allow it
// to be toggled so that we can easily test things when they
// release new JVMs
MediaPrefs.config, RunAnywhere.isMacOS());
/** Allows us to tweak the sleep granularity. */
protected static RuntimeAdjust.IntAdjust _sleepGranularity =
new RuntimeAdjust.IntAdjust(
"The number of milliseconds slept before checking to see if " +
"it's time to queue up a new frame tick.", "narya.media.sleep_gran",
MediaPrefs.config, 7);
/** Whether to enable AWT event debugging for the frame. */
protected static final boolean DEBUG_EVENTS = false;
}