Benchmark: BufferStrategy vs double buffering via .drawImage()

So I was using double buffering by manually drawing everything to offscreen image and then drawing that image on screen. Now I deceided to use swing in my game and convert rendering code to use buffer strategy. For activly rendering swing I used stuff described here.
After putting it all together I noticed drop of fps on my slower computer from 55 to 44. I was very dissapointed becouse I was actually expecting fps increase as I heard that double buffering uses all avaliable speed up methodes. So to check if it was from converting to buffer stragegy I created a small benchmark similar to my game rendering engine.
For test I drawed 100 ovals in a loop, 50 rectangles and 50 20x20 png images with alpha channel. Resoults are good, without png images BS rendering goes to 220 fps, and BI rendering only 90. With png images BS drops to 80, but it’s still a lot better then 60 BI render gets. So conclusion of this small benchmark is this: BS rendering is noticablely faster then BI rendering, and extreamly faster when dealing with java shapes.
This is my first benchmark test so don’t take it too seriously and if you see any bugs or improper measuring please tell. Why fps droped down in my game… I don’t know, probably becouse synchronizing for being able to render swing components. This is what I’ll test next.

Here is the code, 2 classes, switch rendering mode by setting “BS” or “BI” in VitkroijePanel.RENDER_METHOD :

Viktorije:


package viktorije;

import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.JFrame;

public class Viktorije extends JFrame {

	public final static int DEFAULT_FPS = 60;
	
	public static int res_height;
	public static int res_width;
    
///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////	
	
	// MAIN
	public static void main(String[] args) {
		int fps = DEFAULT_FPS;
	    long period = (long) 1000.0/fps;
	    System.out.println("fps: " + fps + "; period: " + period + " ms");
	    new Viktorije("Viktorije", period*1000000L);     // ms --> nanosecs
	}
	
	// CONSTRUCTOR
	public Viktorije(String title, long period) {
		super(title);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setIgnoreRepaint(true);
		setUndecorated(false);
		Dimension screen_dim = Toolkit.getDefaultToolkit().getScreenSize();
		res_width = (int)screen_dim.getWidth();
		res_height = (int)screen_dim.getHeight();
		setBounds(0,0,res_width,res_height);
		setResizable(false);

		setVisible(true);	// must be called before createBufferStrategy() or exception is thrown, don't know why
		createBufferStrategy(2);
        
		ViktorijePanel viktorije_panel = new ViktorijePanel(this, period);
		setContentPane(viktorije_panel);
		
		viktorije_panel.startAnim();		// Kova: starts game loop thread
	}

}

ViktorijePanel:


package viktorije;

import java.awt.*;
import java.awt.image.*;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.JPanel;

public class ViktorijePanel extends JPanel implements Runnable {
    
	private Thread animator;	
	
	private volatile boolean running = false;
    private long used_time = 0;
    private int[] fps_store = new int[50];
    private int fps = 0;
    private int j = 0;
    private static final String RENDER_METHOD = "BI"; // Kova: if not BS it will assume BI 
    private Graphics dbg;
    private Image dbImage;
    
	private GraphicsDevice device;
	private DisplayMode display_mode;
	private BufferStrategy buffer_strategy;
	private Viktorije viktorije;
    
//    BufferedImage ball = loadImage("images/lopta_1.png");
	
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////

	// CONSTRUCTOR
	public ViktorijePanel(Viktorije viktorije, long period) {
		this.viktorije = viktorije;
		this.buffer_strategy = viktorije.getBufferStrategy();
		setSize(1024,768);
		setIgnoreRepaint(true);
		setOpaque(true);
	    setLayout(null);
	}  // end: constructor
	
	public void paint(Graphics g) {
		System.out.println("ViktorijePanel.paint(): this should rarely (best never) be called");
	}
	
	// Kova: create and start main game thread
	public void startAnim() {
		if (animator == null || !running) {
	        animator = new Thread(this, "Viktorije Animator");
	        animator.start();
		}
	} // kraj: startAnim()

	// Kova: start main game thread
	public void run() {
	    running = true;
	    GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
	    device = environment.getDefaultScreenDevice();
	    display_mode = new DisplayMode(1024,768,32,DisplayMode.REFRESH_RATE_UNKNOWN);
	    System.out.println("device.isFullScreenSupported() == " + device.isFullScreenSupported());

	    while(running) {
            long start_time = System.nanoTime();
	    	
            if (RENDER_METHOD.equals("BI")) {
                gameRenderBI();
                paintScreenBI();                
            } else {
                gameRenderBS();
                paintScreenBS();                
            }

	        sleep();
            
            used_time = System.nanoTime() - start_time;
            fps_store[j] = 1000/(int)(used_time/1000000);
            j++;
            if (j>49) j=0;
            fps = 0;
            for (int fps1: fps_store) {
                fps += fps1;
            }
            fps /= 50;
	    }
	    
	} // end: run()

    // Kova: only yields
	private void sleep() {
//		try {
//			Thread.sleep(10);	// Kova: 100 fps
//        } catch (InterruptedException e) {
//        	e.printStackTrace();
//        }
        Thread.yield();
	}

	// Kova: load images for testing if you like...
    public static BufferedImage loadImage(String path) {
    	URL url = null;
        try {
        	url = Viktorije.class.getClassLoader().getResource(path);
            return ImageIO.read(url);
        } catch (Exception e) {
        	System.out.println("Error loading image: " + path + " " + url);
        	System.exit(0);
        	return null;
        }
    }

    // Kova: render graphics using BufferStrategy
    private void gameRenderBS() {
		Graphics2D g2d = null;
		g2d = (Graphics2D)buffer_strategy.getDrawGraphics();	
		drawMyStuff(g2d);
	}

    // Kova: paint using BufferStrategy
	private void paintScreenBS() {
	    try {
	    	buffer_strategy.show();
	        Toolkit.getDefaultToolkit().sync();			        // Sync the display on some systems (on Linux, this fixes event queue problems)
	    }
	    catch (Exception e) { // quite commonly seen at applet destruction
	    	System.out.println("Graphics error: " + e);
	    	e.printStackTrace();
	    }
	} // end of paintScreen()

    // Kova: render graphics using offscreen BufferedImage
    private void gameRenderBI() {
        if (dbImage == null) {
            dbImage = createImage(1024, 768);
            if (dbImage == null) {
                return;
            } 
            dbg = dbImage.getGraphics();
        }
        drawMyStuff(dbg);
    }
    
    // Kova: paint using .drawImage() using BufferedImage as offscreen buffer 
    private void paintScreenBI() {
        Graphics g;
        try {
            g = this.getGraphics();
            if ((g != null) && (dbImage != null))
                g.drawImage(dbImage, 0, 0, null);
            Toolkit.getDefaultToolkit().sync();
            g.dispose();
        }
        catch (Exception e) {
            System.out.println("Graphics error: " + e);  
        }
    }
    
	public void setFullScreen(boolean fullScreen) throws IllegalArgumentException {
		if (isFullScreen() == fullScreen) {
			return;				// CommanderKeith: we're already in fullscreen, so return.
		}
		if (fullScreen) {
			viktorije.setResizable(false);
			device.setFullScreenWindow(viktorije);
			if (display_mode != null && device.isDisplayChangeSupported()) {
				try {
					device.setDisplayMode(display_mode);
					System.out.println("display mode set");
				} catch (IllegalArgumentException e) {
					System.out.println("display mode not supported");
					device.setFullScreenWindow(null);
					viktorije.setResizable(true);
					throw e;
				}
			} else {
				device.setFullScreenWindow(null);
				viktorije.setResizable(true);
				throw new IllegalArgumentException("device.isDisplayChangeSupported() == false");
			}
		} else {
			device.setFullScreenWindow(null);
			viktorije.setResizable(true);
		}
		viktorije.createBufferStrategy(2);
		this.buffer_strategy = viktorije.getBufferStrategy();
	}
	
	public boolean isFullScreen() {
		if (device.getFullScreenWindow() == null){
			return false;
		}
		return true;
	}

    // Kova: draw your stuff for testing
    private void drawMyStuff(Graphics g) {
        g.setFont(new Font("Arial", Font.BOLD, 18));
        g.drawString(RENDER_METHOD, 20, 70);
        g.setColor(Color.GREEN);
        g.fillRect(0, 0, Viktorije.res_width, Viktorije.res_height);      // Kova: clear the background

        g.setColor(Color.white);
        g.fillRect(0, 45, Viktorije.res_width, 6);    
   
        for (int i=0; i<50; i++) {
            g.drawOval(10*i, 10*i, 50, 50);
            g.drawOval(10*i, 768/(i+1), 50, 50);
            g.fillRect(10*i, 500, 50, 50);
//            g.drawImage(ball, 20*i, 600, null);
        }
        g.setColor(Color.BLACK);
        g.drawString("say: ", 25, 725);

        if (used_time > 0) {
            g.drawString("FPS: " + fps, 20, 40); 
        }
    }

}  // end: ViktorijePanel

That’s weird, in window mode BufferStrategy & manual double-buffering with images should be the same. The reason may be because your BufferedImages aren’t accelerated until they are copied a couple of times, so they’d be slower at the start. If you used VolatileImages there wouldn’t be this slow down, but its not a big deal since its only at the start.

In full screen mode BufferStrategy is likely to be a lot faster since it can use a flip strategy (pointer shifting) rather than blitting (copying pixels).

Anti-aliasing those circles will probably have a big affect on performance too.

Keith

What do you mean only at start? Start is the first frame, every other should be accelarated, so I’ve read somewhere… but it dosen’t even matter since my 20x20 image has alpha channel and isn’t surely accelarated under windows (default directdraw).

flip strategy only becomes avaliable in full screen? Ah, didn’t know that, I’ll test in full screen also.

don’t know if aa is automaticly on or off, but I didn’t change a thing so my draw method should be the same weight for both rendering methodes.