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

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 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. * Note: 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; }