Java2D Optimization

I’m trying to get decent speed out of Java2D and it just isn’t happening. I don’t know if I’m doing it some dumb way or not, but I usually use OpenGL so I haven’t worried about it. This time, though I want to stick with Java2D if possible.

Here is a simple test app I made that creates a bunch of images and draws them while spinning them around and moving them. Basically it’s meant to be a simple stress test. But I find that even with very simple images and not very many of them I’m having really bad luck in terms of speed. If I increase window size to fill my screen, almost no matter what I end up getting only around 20 fps, which is just crazy.

I’ve got to be doing something wrong.


import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import java.util.ArrayList;

public class Test2 extends JFrame
{
	private TestCanvas canvas;
	
	public static final int NES_WIDTH = 256;
	public static final int NES_HEIGHT = 224;
	
	public Test2()
	{
		canvas = new TestCanvas();
		getContentPane().add(canvas);
		setSize(500,500);
		setVisible(true);
	}
	
	public void run()
	{
		canvas.run();
	}
	
	public static void main(String[] args)
	{
		new Test2().run();
	}
	
	private class TestCanvas extends Canvas
	{
		private AffineTransform affine;
		private BufferedImage image;
		
		private int lastWidth;
		private int lastHeight;
		
		private ArrayList<Sprite> sprites;
		
		private float fps;
		private int draws;
		
		public TestCanvas()
		{
			
			affine = new AffineTransform();
			sprites = new ArrayList<Sprite>();
			for (int i = 0; i < 20; i++)
			{
				sprites.add(new Sprite((float)(Math.random() * NES_WIDTH), (float)(Math.random() * NES_HEIGHT), (int)(Math.random() * 30+1), (int)(Math.random() * 30+1), (float)(Math.random()*5.0f+0.1f), (float)(Math.random() * Math.PI / 50.0f +0.1f)));
				sprites.get(sprites.size()-1).setTarget((float)(Math.random() * NES_WIDTH), (float)(Math.random() * NES_HEIGHT));
			}
			
			try
			{
				image = ImageIO.read(new File(System.getProperty("user.dir") + "/RCR.jpg"));
			}
			catch (Exception e)
			{
				e.printStackTrace();
			}
		}
		
		public void paint(Graphics g)
		{
			if (lastWidth != getWidth() || lastHeight != getHeight())
			{
				affine.setToScale(getWidth() / (NES_WIDTH+0.0), getHeight() / (NES_HEIGHT+0.0));
				lastWidth = getWidth();
				lastHeight = getHeight();
			}
			
			Graphics2D g2 = (Graphics2D) g;
			g2.setTransform(affine);
			
			g2.drawImage(image,0,0,null);
			
			for (int i = 0; i < sprites.size(); i++)
				sprites.get(i).draw(g2);
			
			g.setColor(Color.WHITE);
			g.drawString("FPS: " + fps,150,15);
			
			draws++;
		}
		
		public void run()
		{
			boolean running = true;
			float target = 1000 / 60.0f;
			float frameAverage = target;
			long lastFrame = System.currentTimeMillis();
			float yield = 10000f;
			float damping = 0.1f;

			long lastSecond = lastFrame;

			while (running)
			{
				long timeNow = System.currentTimeMillis();
				frameAverage = (frameAverage * 10 + (timeNow - lastFrame)) / 11;
				lastFrame = timeNow;

				yield += yield*((target/frameAverage)-1)*damping+0.05f;

				for(int i = 0; i < yield; i++)
					Thread.yield();
				
				if (timeNow - lastSecond >= 1000)
				{
					fps = draws;
					draws = 0;
					lastSecond = timeNow;
				}
				
				repaint();
			}
		}
	}
	
	private class Sprite
	{
		private BufferedImage image;
		private float x, y;
		private int width, height;
		private float rotation, rotationSpeed;
		private float targetX, targetY, speed;
		
		public Sprite(float newX, float newY, int wid, int hi, float shpeed, float rotSpeed)
		{
			x = newX;
			y = newY;
			width = wid;
			height = hi;
			speed = shpeed;
			rotationSpeed = rotSpeed;
			
			GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
			GraphicsDevice gd = ge.getDefaultScreenDevice();
			image = gd.getDefaultConfiguration().createCompatibleImage(width,height,Transparency.TRANSLUCENT);
			
			for (int i = 0; i < image.getWidth(); i++)
				for (int j = 0; j < image.getHeight(); j++)
					image.getRaster().setPixel(i,j,new int[]{(int)(Math.random()*256),(int)(Math.random()*256),(int)(Math.random()*256),(int)(Math.random()*256)});
		}
		
		public void draw(Graphics2D g)
		{
			//Move to the target.
			moveTarget();
			
			//Adjust the rotation.
			rotation += rotationSpeed;
			
			g.rotate(rotation, x+width/2, y+height/2);
			g.drawImage(image,(int)x,(int)y,null);
			g.rotate(-rotation, x+width/2, y+height/2);
		}
		
		private void moveTarget()
		{
			if (x != targetX && y != targetY)
			{
				float xDist = targetX - x;
				float yDist = targetY - y;
				float dist = (float) Math.sqrt(xDist * xDist + yDist * yDist);
				float newX = x + (xDist / dist) * speed;
				float newY = y + (yDist / dist) * speed;
				
				if ((x < targetX && newX > targetX) || (x > targetX && newX < targetX))
					x = targetX;
				else
					x = newX;
					
				if ((y < targetY && newY > targetY) || (y > targetY && newY < targetY))
					y = targetY;
				else
					y = newY;
			}
			else
			{
				//Set a new target.
				setTarget((float)(Math.random() * NES_WIDTH), (float)(Math.random() * NES_HEIGHT));
			}
		}
		
		public void setTarget(float targX, float targY)
		{
			targetX = targX;
			targetY = targY;
		}
	}
}

Also, I’ve tried with VolatileImages, just using fillRect to draw pixels, having a JPanel instead of a Canvas, and more, but no luck.

What happens if you switch from currentTimeMillis to nanoTime ?

Same results. I changed the target and the fps counter to match that as well obviously (using 1,000,000,000 instead of 1,000). So it ended up giving the exact same fps.

[EDIT] As a note, I just did the same test on OpenGL and when I have 2000 entities on screen at a bigger resolution I still get 50 fps, which is obviously better than Java2D’s 20 fps with 20 entities. The OpenGL test was using currentTimeMillis.

[EDIT2] It takes 5,000 entities on OpenGL to match the fps obtained with 20 entities on Java2D. That’s 250:1. There’s no way that can be right.

Are you using the java 6u10 runtime?
If not, the AffineTransform(s) will be relegating all of your rendering to software.

Other than that, you arn’t using BufferStrategy so the target surface of all your rendering may not be in graphics memory - again potencially causing all of the rendering to occur through software.
Ofcourse this again depends on Java version - i’m not sure if under-the-hood Canvas has been altered in the most recent JRE releases so that it always uses an accelerated surface for rendering.

3rdly, because the “RCR” image is obtained directly from ImageIO it will not be accelerated in older JRE’s (releases prior to 1.5, or maybe 1.4.2, I forget when that flaw was fixed).
In these JRE’s you need to copy it onto a compatible image for it to be eligable for acceleration.

Launch it with the java2d trace options enabled, to see which pipeline your rendering is going through.
( http://java.sun.com/j2se/1.5.0/docs/guide/2d/flags.html )

What if you change the run method to this:


public void run()
        {
            boolean running = true;
            float target = 1000 / 60.0f;
            float frameAverage = target;
            long lastFrame = System.currentTimeMillis();
            float yield = 10000f;
            float damping = 0.1f;

            long lastSecond = lastFrame;
            
            setIgnoreRepaint(true);
            createBufferStrategy(3);
            BufferStrategy b = getBufferStrategy();

            while (running)
            {
                
                long timeNow = System.currentTimeMillis();
                frameAverage = (frameAverage * 10 + (timeNow - lastFrame)) / 11;
                lastFrame = timeNow;

                yield += yield*((target/frameAverage)-1)*damping+0.05f;

                for(int i = 0; i < yield; i++)
                    Thread.yield();
                
                if (timeNow - lastSecond >= 1000)
                {
                    fps = draws;
                    draws = 0;
                    lastSecond = timeNow;
                }
                
                //repaint();
                Graphics g = b.getDrawGraphics();
                g.clearRect(0, 0, getWidth(), getHeight());
                paint(g);
                g.dispose();
                if (!b.contentsLost()) b.show();
            }
        }

You need to use VolatileImages instead of BufferedImages in your sprite class, that’s your problem. For some reason BufferedImages just don’t cut it any more, they’re usually un-accelerated for some reason.

Like Abuse said, if you run your program with the trace options (use this as an option: -Dsun.java2d.trace=,count) then you’ll see lots of non d3d calls which means it’s using software loops instead of hardware.

VolatileImages are a pain to use though, here’s some code I use:

public static VolatileImage createVolatileImage(int width, int height, int transparency) {	
		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
		GraphicsConfiguration gc = ge.getDefaultScreenDevice().getDefaultConfiguration();
		VolatileImage image = null;

		image = gc.createCompatibleVolatileImage(width, height, transparency);

		int valid = image.validate(gc);

		if (valid == VolatileImage.IMAGE_INCOMPATIBLE) {
			image = createVolatileImage(width, height, transparency);
		}
		System.out.println("created new VolatileImage");
		return image;
	}
public void render(JComponent component){
		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
		GraphicsConfiguration gc = ge.getDefaultScreenDevice().getDefaultConfiguration();
		if (vImage == null || getW() != vImage.getWidth() || getH() != vImage.getHeight() || vImage.validate(gc) != VolatileImage.IMAGE_OK) {
			vImage = createVolatileImage(getW(), getH(), Transparency.OPAQUE);
			drawOntoImage();
		}
		do {
			int valid = vImage.validate(gc);
			if (valid == VolatileImage.IMAGE_INCOMPATIBLE) {
				vImage = createVolatileImage();
				drawOntoImage();
			}
		} while (vImage.contentsLost());
		Graphics2D g2D = (Graphics2D)component.getGraphics();
		g2D.drawImage(vImage, x, y, null);
	}

And the trouble is this is all entirely random advice when applied to any particular combination of OS and VM :confused: It’s no wonder we made LWJGL.

Cas :slight_smile:

When I use Java2D to do something fast, I just limit myself to just doing simple things.

I think your main problem is using AffineTransform to scale (scaling using drawImage usually works quite fast), and Graphics.rotate is also slowing you down.
Without those things I still got a steady 60fps instead of a slideshow when rendering 2000 sprites (I also changed your run() method as Daniel_F suggested).
For rotation I myself use a class that pre-rotates an image.

Indeed, IMO Java2D was a poorly thought out API from the beginning.
The numerous attempts (between 1.3 & 1.6) to graft it ontop of different h/w accelerated native libraries across multiple platforms has left much of it’s functionality unreliable due to this fragmentation.

While each release has for the most part conformed to the formal specifications set down by the API, the most important aspect of a graphics rendering API - Performance - has no formal specification, and consequently has not been correctly managed.

I wonder - do any common development languages impose performance specifications upon formal design interfaces?
I presume this is a far greater consideration for real-time systems?

The only one that springs to mind is the specification for the C++ standard library. The containers and algorithms all have minimum big-O performance specified (eg. random access to a vector is guranteed to be O(1) ). This leads to the interesting side effect that while it’s possible to implement a (say) std::map with whatever data structure / algorithm the implementator chooses, the performance restrictions usually mean there’s a canonical data structure that everyone uses (like a red-black tree).

I’m not sure how well big-O notation would work for a graphics api though.

Even OpenGL has no actual requirement for performance. It just so happens you can more or less rely on most of this basic stuff without worrying about it.

Cas :slight_smile:

It’s been one of the major gripes (especially amongst the opengl.org community) recently though. It might be nice and whizzy for basic stuff but when you start getting into some of the more advanced extensions it can still be something of a performance lottery it seems.

Thanks for all the great responses, guys.

Looks like putting in a buffer strategy is what’s really going to make the big difference (I can’t actually test that at the moment), because all of the other suggestions I tried at one iteration or another, including volatile images, turning off rotation, turing off the affine transform, etc.

In the past I’ve only used buffer strategies for full screen modes, I didn’t even know that it was a logical thing to use it in Swing elements and Applets.

Okay doing all that seems to work okay, certainly to an acceptable level.

But then how do you guys suggest doing the things I now have to leave out (namely rotation and scaling)? I can scale the images with drawImage, but what about rotation?

Did VolatileImages work out?

BufferStrategies use VolatileImages under the hood, except that BufferStrategies can also do pointer-flipping (which is faster) if in full-screen. But you should still have to use VolatileImages for your sprites.

To rotate and scale and all that, just change the Graphics2D’s AffineTransform then paint your VolatileImage. That way everything should be accelerated if you’ve got java6u10 and a non-intel video card.

See this super-useful thread for the latest tips on using java2D - Kirill G of substance fame talks to Dmitri Trembovetski, our hero who’s making java2d better every day:

http://forums.java.net/jive/thread.jspa?threadID=39749&tstart=0

I suppose the thread kind of shows how hard java2D is to use - I mean kirill is a top java programmer whose libs are used by everyone, but he never even knew how to get hardware-acceleration.

I had already tried VolatileImages and noticed no differences. I just tried again and the only difference I noticed was that transparency stopped working.

Also do you mean this for rotation:


g.rotate(rotation, x+width/2, y+height/2);
g.drawImage(image,(int)x,(int)y,null);
g.rotate(-rotation, x+width/2, y+height/2);

Because that just rapes the performance.

Also does anyone know a way to just reduce the resolution in a window? Like I want the resolution to be say 300 x 300 but the window is 900 x 900, it should just draw all the pixels at 3x normal size. But I’m not talking a way of simulating this (drawing everything bigger) but actually doing it.

Scaling what you draw is the only way of achieving that.

Of course, if we’re only talking images here, you can simply draw a 300300 image into a 900900 once, and draw the 900*900 image from there on. (Sort of like the intermediate image technique). However, if you want everything to scale “real-time”, you’ll just have to scale on the fly.

Hmm, so are you using java 6 update 10 on a machine with a non-intel video card? For that setup, everything should be accelerated.

If you want to get to the bottom of the problem, post the print-out when you run with this VM option:
-Dsun.java2d.trace=,count