I implemented real double buffering for Swing. What does that mean? Up until now implemented double buffering like this: When painting for a component starts, it fetches an offscreen image from the RepaintManager, paints the component to (0,0) and when this is done, the painted area is copied onto the screen. The next component that is painted does the same etc. The RepaintManager has exactly one offscreen image available that is used by all componenents. While this allows for smooth drawing, it can be done better. Now the RepaintManager manages exactly one offscreen buffer for each top level window (Window or Applet instance). When a component fetches the offscreen buffer, it will always get the offscreen buffer from its toplevel window. Then it draws itself, not to (0,0) but instead to its real location. When this is done, the other components also draw themselves in the same manner. When all components are done, the (updated area of the) offscreen buffer is blitted to the screen all at once.
This has the following immediate advantage: - painting operations seem more atomic, each work request in the RepaintManager performs at most 1 blitting operation per toplevel window - performance is most likely increased because we need only one blit and not several (looking at GtkImage.c I would think that we can even optimize this a little more) However, there are more advantages: - Components can now assume that the offscreen buffer still has the state from the last painting operation. This will allow to optimize JViewport for example, where we manage an additional back buffer ATM to do double buffered scrolling. This can now be implemented to directly blit inside the offscreen buffer from the RepaintManager thus reducing memory consumption and saving 1 unnecessary blitting operation. - Uncovering a Window with Swing in it normally triggers an expensive repaint operation, during which the uncovered Window remains gray. This can be implemented to blit the offscreen buffer to screen before starting the repaint (if that is necessary at all then), thus making the GUI more responsive. BTW: AFAIK, this feature ('real doublebuffering') will ship with JDK6 for the first time in Sun's JDK. The above mentioned optimization in JViewport has been solved a little differently in the JDK (they directly blit from the screen into the Swing backbuffer). If somebody sees any regressions with that optimization, please ping me. 2006-02-13 Roman Kennke <[EMAIL PROTECTED]> * javax/swing/RepaintManager.java (offscreenBuffers): New field. (doubleBuffer): Removed field. (repaintUnderway): New field. (commitRequests): New field. (RepaintManager): Initialize new fields. (paintDirtyRegions): Handle repaintUnderway flag. Commit buffers when done. (getOffscreenBuffer): Returns the offscreen buffer for the corresponding root component. (commitBuffer): New method. (commitRemainingBuffers): New method. * javax/swing/JComponent.java (paint): Call paintDoubleBuffered with the current clip. (paintImmediately2): Don't paint on screen here. (paintDoubleBuffered): Rewritten for real double buffering. (paintSimple): Draw to screen in this method. /Roman
Index: javax/swing/JComponent.java =================================================================== RCS file: /cvsroot/classpath/classpath/javax/swing/JComponent.java,v retrieving revision 1.98 diff -u -r1.98 JComponent.java --- javax/swing/JComponent.java 6 Feb 2006 13:32:03 -0000 1.98 +++ javax/swing/JComponent.java 13 Feb 2006 22:28:21 -0000 @@ -1549,7 +1549,10 @@ // screen. if (!isPaintingDoubleBuffered && isDoubleBuffered() && rm.isDoubleBufferingEnabled()) - paintDoubleBuffered(g); + { + Rectangle clip = g.getClipBounds(); + paintDoubleBuffered(clip); + } else { if (g.getClip() == null) @@ -1750,33 +1753,29 @@ void paintImmediately2(Rectangle r) { RepaintManager rm = RepaintManager.currentManager(this); - Graphics g = getGraphics(); - g.setClip(r.x, r.y, r.width, r.height); if (rm.isDoubleBufferingEnabled() && isDoubleBuffered()) - paintDoubleBuffered(g); + paintDoubleBuffered(r); else - paintSimple(g); - g.dispose(); + paintSimple(r); } /** * Performs double buffered repainting. - * - * @param g the graphics context to paint to */ - void paintDoubleBuffered(Graphics g) + private void paintDoubleBuffered(Rectangle r) { - - Rectangle r = g.getClipBounds(); - if (r == null) - r = new Rectangle(0, 0, getWidth(), getHeight()); RepaintManager rm = RepaintManager.currentManager(this); // Paint on the offscreen buffer. - Image buffer = rm.getOffscreenBuffer(this, getWidth(), getHeight()); + Component root = SwingUtilities.getRoot(this); + Image buffer = rm.getOffscreenBuffer(this, root.getWidth(), + root.getHeight()); + //Rectangle targetClip = SwingUtilities.convertRectangle(this, r, root); + Point translation = SwingUtilities.convertPoint(this, 0, 0, root); Graphics g2 = buffer.getGraphics(); - g2 = getComponentGraphics(g2); + g2.translate(translation.x, translation.y); g2.setClip(r.x, r.y, r.width, r.height); + g2 = getComponentGraphics(g2); isPaintingDoubleBuffered = true; try { @@ -1787,20 +1786,27 @@ isPaintingDoubleBuffered = false; g2.dispose(); } - + // Paint the buffer contents on screen. - g.drawImage(buffer, 0, 0, this); + rm.commitBuffer(root, new Rectangle(translation.x + r.x, + translation.y + r.y, r.width, + r.height)); } /** * Performs normal painting without double buffering. * - * @param g the graphics context to use + * @param r the area that should be repainted */ - void paintSimple(Graphics g) + void paintSimple(Rectangle r) { + Graphics g = getGraphics(); Graphics g2 = getComponentGraphics(g); + g2.setClip(r); paint(g2); + g2.dispose(); + if (g != g2) + g.dispose(); } /** Index: javax/swing/RepaintManager.java =================================================================== RCS file: /cvsroot/classpath/classpath/javax/swing/RepaintManager.java,v retrieving revision 1.23 diff -u -r1.23 RepaintManager.java --- javax/swing/RepaintManager.java 2 Feb 2006 21:13:53 -0000 1.23 +++ javax/swing/RepaintManager.java 13 Feb 2006 22:28:21 -0000 @@ -40,6 +40,7 @@ import java.awt.Component; import java.awt.Dimension; +import java.awt.Graphics; import java.awt.Image; import java.awt.Rectangle; import java.awt.image.VolatileImage; @@ -48,6 +49,8 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; +import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; /** @@ -217,15 +220,25 @@ */ private boolean doubleBufferingEnabled; - /** - * The current offscreen buffer. This is reused for all requests for - * offscreen drawing buffers. It grows as necessary, up to [EMAIL PROTECTED] - * #doubleBufferMaximumSize}, but there is only one shared instance. - * - * @see #getOffscreenBuffer - * @see #doubleBufferMaximumSize + /** + * The offscreen buffers. This map holds one offscreen buffer per + * Window/Applet and releases them as soon as the Window/Applet gets garbage + * collected. + */ + private WeakHashMap offscreenBuffers; + + /** + * Indicates if the RepaintManager is currently repainting an area. + */ + private boolean repaintUnderway; + + /** + * This holds buffer commit requests when the RepaintManager is working. + * This maps Component objects (the top level components) to Rectangle + * objects (the area of the corresponding buffer that must be blitted on + * the component). */ - private Image doubleBuffer; + private HashMap commitRequests; /** * The maximum width and height to allocate as a double buffer. Requests @@ -248,6 +261,9 @@ repaintWorker = new RepaintWorker(); doubleBufferMaximumSize = new Dimension(2000,2000); doubleBufferingEnabled = true; + offscreenBuffers = new WeakHashMap(); + repaintUnderway = false; + commitRequests = new HashMap(); } /** @@ -533,6 +549,7 @@ if (comparator == null) comparator = new ComponentComparator(); Collections.sort(repaintOrder, comparator); + repaintUnderway = true; for (Iterator i = repaintOrder.iterator(); i.hasNext();) { JComponent comp = (JComponent) i.next(); @@ -544,6 +561,8 @@ comp.paintImmediately(damaged); dirtyComponents.remove(comp); } + repaintUnderway = false; + commitRemainingBuffers(); } } @@ -557,21 +576,88 @@ * @param proposedHeight The proposed height of the offscreen buffer * * @return A shared offscreen buffer for painting - * - * @see #doubleBuffer */ public Image getOffscreenBuffer(Component component, int proposedWidth, int proposedHeight) { - if (doubleBuffer == null - || (((doubleBuffer.getWidth(null) < proposedWidth) - || (doubleBuffer.getHeight(null) < proposedHeight)) - && (proposedWidth < doubleBufferMaximumSize.width) - && (proposedHeight < doubleBufferMaximumSize.height))) + Component root = SwingUtilities.getRoot(component); + Image buffer = (Image) offscreenBuffers.get(root); + if (buffer == null + || ((buffer.getWidth(null) < proposedWidth + || buffer.getHeight(null) < proposedHeight) + && proposedWidth < doubleBufferMaximumSize.width + && proposedHeight < doubleBufferMaximumSize.height)) { - doubleBuffer = component.createImage(proposedWidth, proposedHeight); + buffer = component.createImage(proposedWidth, proposedHeight); + offscreenBuffers.put(root, buffer); + } + return buffer; + } + + /** + * Blits the back buffer of the specified root component to the screen. If + * the RepaintManager is currently working on a paint request, the commit + * requests are queued up and committed at once when the paint request is + * done (by [EMAIL PROTECTED] #commitRemainingBuffers}). This is package private because + * it must get called by JComponent. + * + * @param root the component, either a Window or an Applet instance + * @param area the area to paint on screen + */ + void commitBuffer(Component root, Rectangle area) + { + // We synchronize on dirtyComponents here because that is what + // paintDirtyRegions also synchronizes on while painting. + synchronized (dirtyComponents) + { + // If the RepaintManager is not currently painting, then directly + // blit the requested buffer on the screen. + if (! repaintUnderway) + { + Graphics g = root.getGraphics(); + Image buffer = (Image) offscreenBuffers.get(root); + int dx1 = area.x; + int dy1 = area.y; + int dx2 = area.x + area.width; + int dy2 = area.y + area.height; + g.drawImage(buffer, dx1, dy1, dx2, dy2, + dx1, dy1, dx2, dy2, root); + g.dispose(); + } + // Otherwise queue this request up, until all the RepaintManager work + // is done. + else + { + Rectangle commitArea; + if (commitRequests.containsKey(root)) + commitArea = area.union((Rectangle) commitRequests.get(root)); + else + commitArea = area; + commitRequests.put(root, commitArea); + } + } + } + + /** + * Commits the queued up back buffers to screen all at once. + */ + private void commitRemainingBuffers() + { + // We synchronize on dirtyComponents here because that is what + // paintDirtyRegions also synchronizes on while painting. + synchronized (dirtyComponents) + { + Set entrySet = commitRequests.entrySet(); + Iterator i = entrySet.iterator(); + while (i.hasNext()) + { + Map.Entry entry = (Map.Entry) i.next(); + Component root = (Component) entry.getKey(); + Rectangle area = (Rectangle) entry.getValue(); + commitBuffer(root, area); + i.remove(); + } } - return doubleBuffer; } /**