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;
   }
 
   /**

Reply via email to