[java] My code seems a little slow (am I doing this right?)

Started by
9 comments, last by overeasy 14 years, 2 months ago
My desktop has a Core 2 Q4950 processor, so I was surprised that I wasn't getting at least 30fps when covering my whole screen. Nonetheless, here is the Java code to a simple active-graphics implementation I intend to use as the basis for the graphical client in a client/server 2D RTS game. When I run the program, I get really fast framerates when the window is around 640x480 pixels or so, but from that point if I make it larger it grinds to a halt. Is this insurmountable? Note how I have logic to recreate the buffer when the form is resized (and have it a synchronized block because AWT events are on a separate thread). Am I doing this right? (My GUI experience lies in .NET WinForms, not Java Swing).

package tj.client.ui;

import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.image.*;
import java.util.Random;

import javax.swing.*;

public class GameWindow extends JFrame implements ComponentListener {
	
	private Canvas _canvas;
	
	private Object         _drawLock = new Object();
	private BufferedImage  _bi; // the offscreen buffer
	private BufferStrategy _bs;
	
	private boolean _run;
	
	public GameWindow() {
		
		setIgnoreRepaint(true);
		setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		
		_canvas = new Canvas();
		_canvas.setIgnoreRepaint(true);
		_canvas.setSize( 1024, 768 );
		
		this.add( _canvas );
		this.pack();
		this.setVisible(true);
		this.addComponentListener(this);
		
		resetBuffer();
	}
	
	private void resetBuffer() {
		
		synchronized(_drawLock) {
			
			if( _bi != null )
				if( getWidth() == _bi.getWidth() && getHeight() == _bi.getHeight() ) return;
			
			_canvas.setSize( this.getWidth(), this.getHeight() );
			_canvas.createBufferStrategy( 2 ); // 2 is the number of buffers, it is not a magic number or some other const
			
			_bs = _canvas.getBufferStrategy();
			
			GraphicsEnvironment   ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
			GraphicsDevice        gd = ge.getDefaultScreenDevice();
			GraphicsConfiguration gc = gd.getDefaultConfiguration();
			
			_bi = gc.createCompatibleImage( _canvas.getWidth(), _canvas.getHeight() );
			
		}
	}
	
	public @Override void componentHidden(ComponentEvent arg0) {
	}

	public @Override void componentMoved(ComponentEvent arg0) {
	}

	public @Override void componentResized(ComponentEvent arg0) {
		
		resetBuffer();
	}

	public @Override void componentShown(ComponentEvent arg0) {
	}
	
	public void doLoop() {
		
		_run = true;
		while( _run ) {
			
			Graphics2D g = null;
			Graphics   h = null;
			
			try {
				
				synchronized(_drawLock) {
					
					g = _bi.createGraphics();
					
					doPainting( g );
					
					h =  _bs.getDrawGraphics();
					h.drawImage( _bi, 0, 0, null );
					if( !_bs.contentsLost() ) _bs.show();
					
				}
				
				Thread.yield();
			
			} finally {
				
				if( g != null ) g.dispose();
				if( h != null ) h.dispose();
			}
			
		}
		
	}
	
	Random rng = new Random();
	
	private void doPainting(Graphics2D g) {
		
		int width  = _canvas.getWidth();
		int height = _canvas.getHeight();
		
		for (int i = 0; i < 20; ++i) {
			int re = rng.nextInt(256);
			int gr = rng.nextInt(256);
			int bl = rng.nextInt(256);
			
			g.setColor(new Color(re, gr, bl));
			
			int x = rng.nextInt(width  / 2);
			int y = rng.nextInt(height / 2);
			int w = rng.nextInt(width  / 2);
			int h = rng.nextInt(height / 2);
			
			g.fillRect(x, y, w, h);
		}
	}
	
}
Printed on 100% Recycled Pixels
Advertisement
Download the individual gifs from here and store them 0 .. 16.gif.

Fix the path in frames = new Frame(".... below so it will load those gifs.
package com.anim.test;import java.awt.AlphaComposite;import java.awt.Canvas;import java.awt.Color;import java.awt.DisplayMode;import java.awt.FontMetrics;import java.awt.Graphics;import java.awt.Graphics2D;import java.awt.GraphicsConfiguration;import java.awt.Rectangle;import java.awt.Transparency;import java.awt.event.KeyAdapter;import java.awt.event.KeyEvent;import java.awt.event.WindowEvent;import java.awt.image.BufferStrategy;import java.awt.image.BufferedImage;import java.awt.image.VolatileImage;import java.io.File;import java.io.IOException;import javax.imageio.ImageIO;import javax.swing.JDialog;import javax.swing.JFrame;import javax.swing.WindowConstants;public class AnimTest extends Canvas implements Runnable {    private static class Sprite {        public int x;        public int y;        public int offset;        public int speed;        public int lastFrame;    }    private final Frame[] frames;    private final Sprite[] sprites;        private long oldTime = System.currentTimeMillis();    //private VolatileImage buffer;    private int oldW;    private int oldH;    private static int totalBytes = 0;    private class Frame {        private final int x;        private final int y;        private final int w;        private final int h;        private final BufferedImage base;        private VolatileImage img;        public Frame(String filename) throws IOException {            final BufferedImage orig = ImageIO.read(new File(filename));            int minx = Integer.MAX_VALUE;            int miny = Integer.MAX_VALUE;            int maxx = Integer.MIN_VALUE;            int maxy = Integer.MIN_VALUE;                        for (int y = 0; y < orig.getHeight(null); y++) {                for (int x = 0; x < orig.getWidth(null); x++) {                    if (orig.getRGB(x, y) != 0xFFC0C0C0) {                        minx = (x < minx) ? x : minx;                        miny = (y < miny) ? y : miny;                        maxx = (x > maxx) ? x : maxx;                        maxy = (y > maxy) ? y : maxy;                    }                }            }            x = minx;            y = miny;            w = maxx - minx;            h = maxy - miny;                        base = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);            final Graphics2D g2d = (Graphics2D)base.getGraphics();            g2d.setComposite(AlphaComposite.Clear);            g2d.fillRect(0, 0, w, h);            g2d.setComposite(AlphaComposite.SrcOver);            g2d.drawImage(orig, -x, -y, null);            g2d.dispose();                        for (int row = 0; row < h; row++) {                for (int col = 0; col < w; col++) {                    final int rgb = orig.getRGB(col+x, row+y);                    base.setRGB(col, row, (rgb == 0xFFC0C0C0) ? Transparency.TRANSLUCENT : rgb);                }            }        }        public void draw(Graphics g, int ox, int oy) {            final GraphicsConfiguration gc = AnimTest.this.getGraphicsConfiguration();            if (img == null || img.contentsLost()  ) {                img = gc.createCompatibleVolatileImage(w, h, Transparency.BITMASK);                                final Graphics2D g2d = (Graphics2D)img.getGraphics();                g2d.setComposite(AlphaComposite.Clear);                g2d.fillRect(0, 0, w, h);                g2d.setComposite(AlphaComposite.SrcOver);                g2d.drawImage(base, 0, 0, null);                g2d.dispose();            }                        Graphics2D g2d = (Graphics2D)g;            g2d.drawImage(img, ox+x, oy+y, null);           }                public int getWidth() {            return w;        }                public int getHeight() {            return h;        }    }        public AnimTest() throws IOException {        setSize(600, 600);        setIgnoreRepaint(true);        frames = new Frame[17];        for (int i = 0; i < frames.length; i++) {            frames = new Frame("j:/res/t" + i + ".gif");        }                sprites = new Sprite[20];        for (int i = 0; i < sprites.length; i++) {            sprites = new Sprite();            sprites.x = rand(500);            sprites.y = i*500/sprites.length;            sprites.speed = 59+rand(58);            sprites.offset = rand(frames.length);        }    }        private static int rand(int max) {        return (int)(Math.random()*max);    }    private double timeAccum = 0;    private int timeCount = 0;    private String lastFPS = "";        protected void paintComponent(Graphics g) {        final int w = getWidth();        final int h = getHeight();        if (oldH != h) {            final int n = sprites.length;            for (int i = 0; i < n; i++)                sprites.y = i*(h-80)/n;            oldH = h;        }        if (oldH != h || oldW != w) {            oldW = w;            oldH = h;        }        /*        if (rebuildBuffer(w, h)) {            oldW = w;            oldH = h;            buffer = createVolatileImage(w, h);        }*/        final long time = System.nanoTime();        //final Graphics gi = buffer.getGraphics();        final Graphics gi = g;        gi.setColor(Color.LIGHT_GRAY);        gi.fillRect(0, 0, w, h);                        final int n = frames.length;        final long time_ms = time / 1000 / 1000;        for (Sprite s : sprites) {            final int idx = (int)(((time_ms / s.speed) + s.offset) % n);            final Frame img = frames[idx];             if (s.lastFrame != idx) {                if (idx == 0) {                    s.x -= 55;                    if (s.x < -img.getWidth()) s.x = w + img.getWidth() + s.x;                }                s.lastFrame = idx;            }            img.draw(gi, s.x, s.y);        }        gi.setColor(Color.BLACK);                        final long delta = time - oldTime;        timeAccum += delta;        timeCount++;        if (timeCount > 20) {            lastFPS = "FPS: " + (int)(1.0e9 / timeAccum * timeCount);            timeAccum = 0;            timeCount = 0;                    }        //final String s = "FPS: " + (int)(1.0e9 / delta);        final String s = lastFPS;                final FontMetrics fm = g.getFontMetrics();        final Rectangle rs = fm.getStringBounds(s, g).getBounds();        final int d = fm.getDescent();        rs.translate(10, rs.height - d + 10);        gi.setColor(Color.LIGHT_GRAY);        gi.fillRect(rs.x, rs.y, rs.width, rs.height);        gi.setColor(Color.BLACK);        gi.drawString(s, rs.x, rs.y + rs.height - d);                oldTime = time;                //g.drawImage(buffer, 0, 0, null);    }        public void run() {        try {            createBufferStrategy(2);            final BufferStrategy bs = getBufferStrategy();                        Graphics g = bs.getDrawGraphics();            while (running) {                if (oldW != getWidth() || oldH != getHeight()) {                    g = bs.getDrawGraphics();                }                paintComponent(g);                if (!bs.contentsLost()) {                    bs.show();                    getToolkit().sync();                }                //Thread.sleep(0,1);                Thread.yield();            }        } catch (Throwable e) {            e.printStackTrace();        }    }        public volatile boolean running = true;        public static void main(String[] args) {        try {            final JFrame frame = new JFrame();            final AnimTest at = new AnimTest();            final Thread t = new Thread(at, "GameLoop");                        frame.addKeyListener( new KeyAdapter() {                public void keyPressed( KeyEvent e ) {                  if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {                      at.running = false;                      try {                      t.join();                      } catch (Throwable t) {                          //                      }                      frame.getToolkit().getSystemEventQueue().postEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));                   }                }            });            frame.setLocation(0, 0);            frame.setSize(600, 600);            frame.add(at);            frame.pack();            frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);            frame.setVisible(true);                        t.setDaemon(true);            t.start();                                } catch (IOException e) {            e.printStackTrace();        }    }}

If it works, you'll see Dukes tumbling right to left.

With 20 dukes (the 'sprites = new Sprite[20];' line determines the count), I get 200FPS in full screen.

This is just some code I was using to test rendering, so it's messy, but it does do the job. There's a lot of redundant stuff in there as well.

I seem to remember having some issues with proper format of the buffer, but at the end it's mostly about getting the volatile image working properly.

I have no real inclination to debug your code, fast rendering is just tedious under Java.
Consider using a game engine, like Slick2D, PulpCore, etc. These also have the option to use OpenGL for hardware acceleration for better effects and higher framerates.
W3bbo, your problem is that you are allocating Graphics objects without calling the dispose() method when you are done. The simplest way to fix your code is to dispose your Graphics object just before you exit the synchronized block in your main loop. This alone will fix your framerate.

Furthermore, I recommend you move all of your drawing operations into an override of the paintComponent(Graphics) method, and begin the redraw operation by calling the repaint() method of your GameWindow.

There is no need to resize the canvas to fill your window in your own code. The default layout manager does this for you, and it also takes into account your window insets. Call setPreferredSize(Dimension) on your canvas to make the pack() method work as you expect.

Eventually, you will need to keep your main loop from modifying data while it is being rendered to the screen - the easiest way to do this is by passing anonymous Runnable objects to the EventQueue via the invokeLater(Runnable) method.
Quote:Original post by Dathgale
W3bbo, your problem is that you are allocating Graphics objects without calling the dispose() method when you are done. The simplest way to fix your code is to dispose your Graphics object just before you exit the synchronized block in your main loop. This alone will fix your framerate.

Furthermore, I recommend you move all of your drawing operations into an override of the paintComponent(Graphics) method, and begin the redraw operation by calling the repaint() method of your GameWindow.

There is no need to resize the canvas to fill your window in your own code. The default layout manager does this for you, and it also takes into account your window insets. Call setPreferredSize(Dimension) on your canvas to make the pack() method work as you expect.

Eventually, you will need to keep your main loop from modifying data while it is being rendered to the screen - the easiest way to do this is by passing anonymous Runnable objects to the EventQueue via the invokeLater(Runnable) method.


Could you elaborate on the event queue usage? This would presumably fix concurrent modification exceptions, right :X?
You also don't need the offscreen buffer. All swing components are already double buffered.
-LuctusIn the beginning the Universe was created. This has made a lot of people very angry and been widely regarded as a bad move - Douglas Adams
I'm getting conflicting information here.

I understand that using "active rendering" with VolatileImage yields the best performance in rendering, and that gScreen.drawImage is the most expensive operation. Why are some people pointing me towards using ordinary BufferedImage approaches, or passive-rendering?

IRT Momoko_Fan: Not an option. This Java project is part of a coursework assignment.

IRT Dathgale: I do dispose of both Graphics instances already. I modified my code so the synchronized(){} block wraps the try{}finally{} but this made no difference to the performance.

IRT Luctus: If I were to use Swing/AWT's rendering loop I wouldn't be doing active-rendering. I understand there are performance and latency issues with using Swing's paintComponent system.
Printed on 100% Recycled Pixels
Quote:Original post by W3bbo
I understand that using "active rendering" with VolatileImage yields the best performance in rendering, and that gScreen.drawImage is the most expensive operation.


I believe you are following my tutorial - Active Rendering

I agree with you on this one. I have tried doing graphics many ways. For applications with swing components, active rendering causes trouble, and calling repaint() over and over can work just fine. But if you are making a game, it is easy to use active rendering and skip all the Swing rendering thread issues.

There are many ways to do graphics with Java, and I will be the first to admit that I don't know everything about Java, but I still find the active rendering approach the easiest. Although, if you follow the discussion, a lot of these points have already been brought up, and I've made faster versions by leaving some of the code out.

And please remember the tutorial was written three years ago. Java has made a lot of improvements since then.

Your code looks good to me, and I think you could get some more speed by removing the synchronized locks. Did you put these in for a reason, or just because you thought it would be safe. I haven't done anything like this, and I've had no trouble so far.

Also, please check out the discussion on the Active Rendering Article. There have been many posts and speed updates already. If you find some more, please post them.

Active Rendering Discussion

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

Quote:Original post by W3bbo
IRT Dathgale: I do dispose of both Graphics instances already. I modified my code so the synchronized(){} block wraps the try{}finally{} but this made no difference to the performance.

Oh, I see it now. Indeed, wrapping it in try{}finally{} shouldn't affect the performance. It doesn't have an appreciable effect unless you're expecting your drawing code to throw exceptions, which is something I would avoid.

Quote:Original post by W3bbo
I'm getting conflicting information here.

I understand that using "active rendering" with VolatileImage yields the best performance in rendering, and that gScreen.drawImage is the most expensive operation. Why are some people pointing me towards using ordinary BufferedImage approaches, or passive-rendering?

It depends on what exactly you're trying to do.

Active rendering only serves to stipulate when you redraw the screen. If you don't mix active and passive rendering and keep CPU intensive calculations out of the event dispatch thread, you won't have performance issues. Passive rendering can even improve performance by immediately redrawing the screen when the window is changed, and discarding extra redraw operations when many are invoked in quick succession.

The VolatileImage stores pixel data in hardware. If you interact with pixels directly from software, for example by drawing rectangles to the image, you will encounter a bottleneck. The BufferedImage has a similar problem. Use the VolatileImage if your only graphics operations will be blitting and transforming images.

If you need to process your images in software, use the java.awt.image.MemoryImageSource. Use the Component.createImage(ImageProducer) method to get an image that can be rendered directly to the screen. This is the method I used for Battlement, and it gives good performance.


Quote:
Could you elaborate on the event queue usage? This would presumably fix concurrent modification exceptions, right :X?

Yes. (However multithreading is not the only way to cause a concurrent modification exception. I'll come back to this point.) Any Runnable object you pass to the EventQueue.invokeLater(Runnable) method will be put in a queue and its run() method will be called by the event dispatch thread as soon as possible. This is the same thread that automatically calls the event listener and paint methods. If you enqueue lots of runnables in a short period of time, they will be executed one at a time in order. This makes it so that exactly one thread is modifying your GUI at any one time, which prevents deadlock.

Concurrent modification exceptions are also caused when you modify a collection while iterating over it. If you are iterating over a list, for example, and then insert an element onto the head of the list, the iterator doesn't know how it is supposed to behave at that point. If you must modify data while iterating over it, either iterate over a copy of the collection, or use the modification methods of the Iterator and ListIterator interfaces.
Quote:Original post by Luctus
You also don't need the offscreen buffer. All swing components are already double buffered.

Swing components do not save the contents of their offscreen buffer, so without saving the buffer yourself, you will waste CPU if the screen is repainted more often than the environment changes. Passive rendering can cause this when the window is revalidated.

This topic is closed to new replies.

Advertisement