Volatile vs Buffered

I’m writing a small Asteroids clone (Stroids 8)), and on my first slightly playable build, I noticed that my graphics were lagging something horrible. So I logged on here, looked around, and noticed that VolatileImage is recommended for doing graphics. I was using BufferedImage and Graphics.drawImage(buffered, x, y, null), so I thought the volatile might work better.

So I wrote a simple class today and tested it. And much to my astonishment, BufferedImage is outperforming VolatileImage ( :o). This doesn’t make sense except that I have onboard video so there may be some hidden optimization. What I’m wondering is if anyone can spot something I did wrong in this test.

public class Form extends JFrame imple....
    ...
    private BufferedImage buffered1;
    private BufferedImage buffered2;
    private VolatileImage volatile1;
    private VolatileImage volatile2;
    private VolatileImage comp;
    private javax.swing.Timer timer;

public Form() 
    {
        super();
        
        setSize(800, 800); //This is the same size Stroids uses
        
        setVisible(true);
        
        //Load the buffered images
        try {
            buffered1 = ImageIO.read(new File("img1.png"));
            buffered2 = ImageIO.read(new File("img2.png"));
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        ... 

        x = 0; //Initalize the scrolling variables
        y = 0;
        timer = new javax.swing.Timer(30, this); //Create a timer to cause a repaint every 30 ms
        
        timer.start(); //Start the timer
    }

/*
 * One or more of the VolatileImages needs to be re-created, or created the first time
*/
private void restoreVolatiles()
    {
        if (volatile1 == null) //No image
            volatile1 = createVolatileImage(800, 800); //Create the image
        
        if (volatile2 == null) //No image
            volatile2 = createVolatileImage(20, 20); //Create the image
        
        if (comp == null) //No image (used for off-screen rendering)
            comp = createVolatileImage(800, 800); //Create the image
        
        Graphics g;
        if (volatile1.validate(getGraphicsConfiguration()) != VolatileImage.IMAGE_OK) //Not ok, so redraw it
        {
            g = volatile1.getGraphics();
            g.drawImage(buffered1, 0, 0, this);
        }
        
        if (volatile2.validate(getGraphicsConfiguration()) != VolatileImage.IMAGE_OK) //Not ok, so redraw it
        {
            g = volatile2.getGraphics();
            g.drawImage(buffered2, 0, 0, this);
        }
    }

public void paintComponent(Graphics _g2)
    {
        Graphics2D g2 = (Graphics2D)_g2;
        
        long volatileDuration, bufferedDuration; //Used to determine how long rendering takes
        
        long startNano = System.nanoTime(); //Get the start time
        
        if (volatile1 == null || volatile2 == null || 
                volatile1.validate(getGraphicsConfiguration()) != VolatileImage.IMAGE_OK || volatile2.validate(getGraphicsConfiguration()) != VolatileImage.IMAGE_OK)
        { //Check if anything needs to be fixed (always runs on first time through: volatile1 == null)
            restoreVolatiles(); //Restore the volatiles
        }
        
        Graphics2D g = comp.createGraphics(); //Get the graphics to composite it
        
        g.drawImage(volatile1, 0, 0, this); //Draw the background (big, 800x800)
        
        g.drawImage(volatile2, x, y, this); //Draw the random image (small, 20x20)
        
        g2.drawImage(comp, 0, 0, this); //Copy to the screen (big 800x800)
        
        volatileDuration = System.nanoTime() - startNano; //Get the time taken
        
        startNano = System.nanoTime(); //Get the start time
        
        g2.drawImage(buffered1, 0, 0, this); //Draw the background
        
        g2.drawImage(buffered2, x, y, this); //Draw the random image
        
        bufferedDuration = System.nanoTime() - startNano; //Get the end time
        
        //Now print out the stats
        try
        { //writer is initalized in the constructor, and is a FileWriter
            System.out.println("Buffered draw took " + bufferedDuration / 1000000.0 + " ms.");
            writer.write("Buffered draw took " + bufferedDuration / 1000000.0 + " ms.\n");
            System.out.println("Volatile draw took " + volatileDuration / 1000000.0 + " ms.");
            writer.write("Volatile draw took " + volatileDuration / 1000000.0 + " ms.\n");
            System.out.println("Volatile was faster by " + (bufferedDuration - volatileDuration) / 1000000.0 + " ms.");
            writer.write("Volatile was faster by " + (bufferedDuration - volatileDuration) / 1000000.0 + " ms.\n");
            
            writer.flush();
            
        } catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

The file that writer prints to gives this (only an excerpt):

[quote]Buffered draw took 0.058667 ms.
Volatile draw took 0.118451 ms.
Volatile was faster by -0.059784 ms.
Buffered draw took 0.060064 ms.
Volatile draw took 0.111466 ms.
Volatile was faster by -0.051402 ms.
Buffered draw took 0.059225 ms.
Volatile draw took 0.136889 ms.
Volatile was faster by -0.077664 ms.
Buffered draw took 0.059225 ms.
Volatile draw took 0.135213 ms.
Volatile was faster by -0.075988 ms.
Buffered draw took 0.056432 ms.
Volatile draw took 0.143874 ms.
Volatile was faster by -0.087442 ms.
Buffered draw took 0.059784 ms.
Volatile draw took 0.137168 ms.
Volatile was faster by -0.077384 ms.
Buffered draw took 0.062019 ms.
Volatile draw took 0.137168 ms.
Volatile was faster by -0.075149 ms.
[/quote]
I am missing something? Is my rendering strategy off? Or do I just need to render many more images per cycle to see the difference? ???

I just made a slight change to it to see if rendering an image multiple times (the small 20x20 100 times instead of 1) would have an effect. It does:

[quote]Buffered draw took 159.936807 ms.
Volatile draw took 364.469227 ms.
Volatile was faster by -204.53242 ms.
Total volatile faster by -204.53242 ms.
Buffered draw took 1.946337 ms.
Volatile draw took 10.244318 ms.
Volatile was faster by -8.297981 ms.
Total volatile faster by -212.830401 ms.
Buffered draw took 1.737651 ms.
Volatile draw took 10.188725 ms.
Volatile was faster by -8.451074 ms.
Total volatile faster by -221.281475 ms.
Buffered draw took 1.739048 ms.
Volatile draw took 1.656356 ms.
Volatile was faster by 0.082692 ms.
Total volatile faster by -221.198783 ms.
Buffered draw took 2.647823 ms.
Volatile draw took 1.361904 ms.
Volatile was faster by 1.285919 ms.
Total volatile faster by -219.91286399999998 ms.
Buffered draw took 1.922311 ms.
Volatile draw took 1.377829 ms.
Volatile was faster by 0.544482 ms.
Total volatile faster by -219.368382 ms.
Buffered draw took 2.191898 ms.
Volatile draw took 1.339835 ms.
Volatile was faster by 0.852063 ms.
Total volatile faster by -218.516319 ms.
[/quote]
It seems now that the major time consuming task is copying the image data from the buffered images to the volatile image.

The idea is to use BufferedImages (they’re all acceleratable as of 1.5) for your sprites, and
VolatileImage (or BufferStrategy) for your back-buffer.

This article may help:
http://weblogs.java.net/blog/chet/archive/2004/08/toolkitbuffered.html

The problem is when your sprite is translucent (as opposed to 1-bit transparent or opaque) -
the translucent images aren’t accelerated unless you use the non-default OpenGL pipeline.

Thanks,
Dmitri
Java2D Team

Ah. Now it makes sense :smiley:

Thanks for the very helpful link :slight_smile:

All those useful blogs should centralized and accessible via the Java Games home page or the games wiki page.

Are they accelerated under 1.6 by default?
I must admit that i was already pretty surprised by the perfs for translucent transformed blits in 1.5, so i thought they were somewhat accelerated.
Under 1.6, my graphical test got some more acceleration, but not much enough to say that hardware acceleration implementation for them was the cause of the difference (65 to 95/100fps).
But, it might be the case and i might have expected much more from my hardware than what i ought to. :-\

No, a opengl-like Direct3d pipeline has been added which should support more boards than opengl does on windows (since hw manufacturers don’t care about open protocolls and apis) but its disabled by default because of possible driver problems.

By default translucent blits are done in software, sorry :wink: (btw. as far as I know but I am maybe wrong)

lg Clemens

We’ve done some work on optimizing the software transforms (although I thought we’ve done that
in 5.0, but there may be something else we done in 6.0 that I’m forgetting), that’s probably why they’re
fast.

In fact, the all-software rendering (like to a BufferedImage) can be surprisingly fast.
The stuff gets slow when you mix hw and sw operations (like in the case of alpha compositing to
a VolatileImage).

Dmitri

i did a few performance tests.

900 rotated & scaled images (this one -> http://www.kittysafe.net/gallery/data/media/4/evil_hamster.jpg), stored in a bufferedimage, copied to volatile
on java5, 1.8 ghz opteron, geforce 7800gt -> 1-2 fps, counted myself.
bufferdimage to bufferedimage -> ~0.25 fps, counted myself.
volatile to volatile -> ~0.25, again counted myself.
java6 using d3d-pipeline -> 250 fps, measured. (the test calculates the fps every 100 frames, so this was the only one actually giving me numbers)
opengl should be around 250 fps, too.

i’ll upload the hamster rotate test when i’m back home.

[quote](this one -> http://www.kittysafe.net/gallery/data/media/4/evil_hamster.jpg)
[/quote]
Wow! That’s one evil hampster! Can I have it as my avatar? =)

Dmitri

/* Copyright Hamster Development 2006 */
package src;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.DisplayMode;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.Window;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Developed with pleasure :)
 *
 * @author HamsterofDeath
 */
public class SpinBench extends JFrame
{
// ------------------------------ FIELDS ------------------------------

  public static SpinBench instance;
  private JPanel jpMain;

  private String title;

// --------------------------- CONSTRUCTORS ---------------------------

  /**
   * Standard-konstruktor
   */
  public SpinBench(Image img)
  {
    super();
    instance = this;
    this.title = "Spinbench";
    setResizable(false);
    buildGUI();
    addListener();
    pack();
    center();

    Window w = new Window(this);
    getGraphicsConfiguration().getDevice().setFullScreenWindow(w);
    getGraphicsConfiguration().getDevice().setDisplayMode(new DisplayMode(1600, 1200, 32, 60));
    Image buffered = getImg();
    Graphics2D graphics;
    List<Graphics2D> g2d = new ArrayList<Graphics2D>();
    for (int i = 1; i < 31; i++)
    {
      for (int i2 = 1; i2 < 31; i2++)
      {
        Graphics2D g2 = (Graphics2D) buffered.getGraphics();
        g2.translate(i * 50, i2 * 50);
        g2.scale(0.1, 0.1);
        g2d.add(g2);
        //g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        //g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
      }
    }
    graphics = (Graphics2D) buffered.getGraphics();
    graphics.setColor(Color.black);
    while (true)
    {
      long startTime = System.nanoTime();
      int frames = 0;
      while (frames++ < 100)
      {
        graphics.fillRect(0, 0, 1600, 1200);
        for (Graphics2D graphics2D : g2d)
        {
          graphics2D.rotate(0.1 * Math.random());
          graphics2D.drawImage(img, 0, 0, null);
        }
        //aufs panel
        w.getGraphics().drawImage(buffered, 0, 0, null);
      }
      long elapsed = System.nanoTime() - startTime;
      double nsPerDraw = elapsed / (float) frames;
      System.out.println(nsPerDraw + " ns per frame = " + 1.0 / (nsPerDraw / 1000000000.0) + " fps");
    }
  }

  private Image getImg()
  {
    //  return new BufferedImage(1600, 1200, BufferedImage.TYPE_INT_ARGB);
    return this.createVolatileImage(1600, 1200);
  }

  /**
   * Gui aufbauen
   */
  protected void buildGUI()
  {
    setTitle(title);
    getContentPane().setLayout(new BorderLayout(6, 6));
    jpMain = new JPanel();
    getContentPane().add(jpMain, BorderLayout.CENTER);
    jpMain.setBorder(new EmptyBorder(6, 6, 6, 6));
    jpMain.setLayout(new BorderLayout(6, 6));

    jpMain.setPreferredSize(new Dimension(640, 480));
  }

  /**
   * Listener registrieren
   */
  protected void addListener()
  {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }

  private void center()
  {
    Toolkit tk = Toolkit.getDefaultToolkit();
    setLocation((int) (tk.getScreenSize().getWidth() / 2 - getWidth() / 2),
                (int) (tk.getScreenSize().getHeight() / 2 - getHeight() / 2));
    setVisible(true);
  }

// --------------------------- main() method ---------------------------

  public static void main(String[] args)
    throws IOException
  {
    Image image = ImageIO.read(new File("resource/hamster.jpg"));
    new SpinBench(image);
  }
}