26

I ran into a very strange problem, I tried searching for an answer for days and days. My game just got a new particle system, but was too slow to be playable. Unfortunately, BufferedImage transformations are very slow. The explosion effect consists of about 200 white sprites loaded from a .png file, rotated, scaled and coloured randomly, moving with a random speed.

I tried to make the performance better with triple / double buffering, and ran into some problems.

My first try was with the JPanel the game was drawn on. I set up the buffers in the JFrame's class (Main), then done the drawing in the Game (extends JPanel) class, BUT, without Graphics g = bufferstrategy.getDrawGraphics();. Then, at the end of the drawing method, i showed the buffer IF it wasn't lost. The buffer was always "lost", as I didn't do the drawing with it's Graphics object. But! The game run as fast as hell! With no buffer in practical use! But how?

This try ended up with no graphical errors, and with a HUGE performance boost - but only on nVidia / AMD cards. Intel GPUs couldn't handle this, the screen was flashing white.

So, i ended up setting and using the BufferStrategy correctly. The Game class now extends Canvas, not JPanel, as getting the Graphics from a JFrame, and using it to draw on a JPanel ends up in an offset, as it is drawing under the title bar. Still fast, fix 60 FPS.

Now, when i created the BufferStrategy in the JFrame (Main class), there was no picture at all. I corrected this by setting up the BufferStrategy in the Game class (Canvas). The picture is correct now, but the game itself is slow like a snail. One explosion tears the FPS down to ~10, but only on nVidia / AMD. Ironic. Even old Intel GPUs handle it with 60 FPS, i managed to get 10000 particles in motion at 60 FPS on a 5-6 year old integrated Intel GPU. My card bumps up to 100% load when an explosion occurs.

Here is my major code (the whole code is unclear and long) :

public class Game extends Canvas {
 -snip-
 public void tick() {
    BufferStrategy bf = getBufferStrategy();
    Graphics g = null;
    try {
        g = bf.getDrawGraphics();
        paint(g);
    } finally {
        g.dispose();
    }
    if (!bf.contentsLost()) {
        bf.show();
    } else {
        System.err.println("Buffer lost!");
    }
    Toolkit.getDefaultToolkit().sync();
 }
 public void setBuffers() {
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice gs = ge.getDefaultScreenDevice();
    GraphicsConfiguration gc = gs.getDefaultConfiguration();

    if (gc.getBufferCapabilities().isMultiBufferAvailable()) {
        createBufferStrategy(3);
        System.out.println("Triple buffering active");
    } else {
        createBufferStrategy(2);
        System.err.println("Triple buffering not supported by the GPU");
        System.out.println("Double buffering active");
    }
    System.out.println("FullScreen required: " + getBufferStrategy().getCapabilities().isFullScreenRequired());
    System.out.println("Page flipping: " + getBufferStrategy().getCapabilities().isPageFlipping());
 }
 public void paint(Graphics g) {
    super.paint(g);
    //set up RenderingHints, draw stuff
 }
 -snip snip-
}

Of course, I call setBuffers() as soon as the Game is started.

I hope you can help me, this problem is very important, as using VolatileImage in my opinion won't give a performance boost, as image manipulating needs to be done using BufferedImage. I bet I'm missing something trivial, or doing something the wrong way.

Here are my computer specs, just to show it's not a hardware problem: Intel Core i7-3770k @ 4.3GHz, nVidia GTX 460, 12 GB ram

The "fast" computer: Intel Core 2 Duo @ 2.7 GHz, Integrated Intel Graphics, 2 GB ram

Thank you for your help and time! :)

EDIT Could VolatileImage help? If I know right, image manipulation must be done using BufferedImages, but drawing them is sluggish.

Simon Tamás
  • 281
  • 3
  • 8
  • 1) What's Java version on both systems? 2) You run app with JRE, or from JDK's JRE? 3) Provide us fully compiled example for benchmarking on our comps. – Andremoniy Jan 27 '13 at 19:09
  • @Andremoniy PCs I tried: Core 2 Duo with on-board Intel GPU (JRE 6), i3-3225 with HD4000 integrated GPU (JDK 7), Celeron laptop with HD2500 (JRE 7). On my computer, I get the same results in NetBeans (JDK) and using the standard JRE 7. I managed to reduce the lag a bit, but it's still laggy. Note that this game is heavily unfinished, to "benchmark" just shoot anything in singleplayer that moves, use missiles for bonus lag. Read the readme.txt for controls :) The FPS counter isn't accurate. Here: [link](https://dl.dropbox.com/u/20361268/Stellar%20Shooter/Stellar%20Shooter%201.5_03%20dev.rar) – Simon Tamás Jan 28 '13 at 19:51
  • Ok, I've tested it: Intel Core i5,8Gb RAM,NVidia Geforce GT540M. Works fine, no performance issues. I don't how anybody could help you, if this problem is not reproducible? – Andremoniy Jan 29 '13 at 08:04
  • @Andremoniy I said I have reduced the lag, but if you emit many explosions at once, there is a slight lag which is not acceptable in gaming terms. Try editing the database, add money, set your level to 100, then get the best rockets in the shop, test again in singleplayer, you will see. (every kill adds one level, more level means more enemies, it also generates lag, but you can produce a lot of explosions) Also, press F3 for particle debug info. Thanks! – Simon Tamás Jan 29 '13 at 20:13
  • FWIW I am running it on a Core I7 Extreme (6-core i.e. with hyperthreading 12 LP) @ 3.46GHz and GTX 580 SLI. It is sitting in the main menu (singleplayer, etc) and my whole system is lagging. SO is having a hard time keeping up with my typing in the comment box. Explosions pause the game for about 0.7 seconds – doug65536 Jan 31 '13 at 03:28
  • Don't get me wrong though. Just confirming that a really powerful gaming machine is having trouble. – doug65536 Jan 31 '13 at 03:33
  • I attached a debugger and found an endless stream of exceptions occurring. All access violations and the address isn't null. Setup your debugger to break on exceptions. – doug65536 Jan 31 '13 at 03:42
  • I put up [a pastebin](http://pastebin.com/jksa1Jzm) of the exception. Not extremely useful and may be erroneous due to lack of symbols, but seems usable to start looking. – doug65536 Jan 31 '13 at 03:48
  • Perhaps try doing active rendering? If you use Java's component's paint(Graphics g) method, it's only a request to be repaint(). When does it actually calls paint() is up to the state of your JVM. – ChaoSXDemon Jan 31 '13 at 04:42
  • @ChaoSXDemon Not overriding paint(Graphics g) won't help, and I have overridden the update() which equals tick() in the sample code, and I am calling repaint() in every tick (this calls update(), update() calls the drawing. – Simon Tamás Feb 01 '13 at 14:23
  • @doug65536 Thanks, this could actually be the problem! I'll try to get some help with the exceptions, I have never faced a problem like this. – Simon Tamás Feb 01 '13 at 14:24
  • If your debugger doesn't catch anything, it may be the Java VM implementation. Google for "excessive page faults java". A few common causes of high page faults are: creating threads, memory mapped files (image loader implementation may be implemented using file mappings), and the GC. – doug65536 Feb 01 '13 at 17:05
  • @doug65536 Thanks, I'll get googleing now. – Simon Tamás Feb 01 '13 at 17:25
  • I couldn't get any useful info, however Windows counts about ~35 million page faults / sec. Image loading? Tried a different way but no Image was shown, but the game lagged. I hope someone could at least just guess at the error source. – Simon Tamás Feb 01 '13 at 19:17
  • @SimonTamás From the performance issues, it sounds like the original code ran on the GPU+GDRAM and the new code runs on the GPU+RAM, causing memory transfers for nVidia/AMD which have GDRAM but not on Intel (which uses the main RAM). Try and profile the memory transfers. Try and enable two modes - HW and SW, where in the HW mode, you use the original code and in the SW mode you use the new code. Perhaps detect the optimal code by analyzing the HW. If this comment helped, let me know and I'll rewrite as a answer. – Danny Varod Feb 03 '13 at 16:38
  • It looks to me you do have a driver problem, try to update your drivers to the lates version and try again. – Peter Feb 04 '13 at 12:26
  • @DannyVarod this seems reasonable, and most likely it will be the problem. I'll look after this kind of memory management, but I have never done things like this before. Thank you in advance :) – Simon Tamás Feb 05 '13 at 20:13
  • @peer I tested the game on multiple computers, even some helpful stackoverflow members have the problem. – Simon Tamás Feb 05 '13 at 20:14
  • 35 x 10^6 faults/sec is terrible. The program working set is bigger than physical memory available to the process. You need to profile memory usage when lag is occurring. It could also be related to GPU memory mapping. Just a guess, but maybe nV is using memory management hardware to control CPU/GPU transfers and this is showing as high paging rates. Would be consistent with @SimonTamás. – Gene Feb 07 '13 at 01:24
  • @DannyVarod Could you give me more help on how should I set Java to render using the CPU+GDRAM? sun.java2d.ddforcevram won't help :( – Simon Tamás Feb 07 '13 at 20:54

3 Answers3

2

Here's a few things to check:


Without knowing the console/error output of your setBuffers function it's difficult to tell. Is Intel using createBufferStrategy(2); while NV uses createBufferStrategy(3); ? if so that could be part of the issue with it using extra memory.


Have you tried the java2d System.properties yet? http://docs.oracle.com/javase/1.5.0/docs/guide/2d/flags.html Especially the trace property for debugging.

Windows only

System.setProperty("sun.java2d.transaccel", "True");
System.setProperty("sun.java2d.d3d", "True");
System.setProperty("sun.java2d.ddforcevram", "True");

All platforms

System.setProperty("sun.java2d.opengl", "True");

For debugging

System.setProperty("sun.java2d.trace", "timestamp,log,count");
//// -Dsun.java2d.trace=[log[,timestamp]],[count],[out:<filename>],[help],[verbose]

Toolkit.getDefaultToolkit().sync(); doesn't actually force monitor VSync, that's done by BufferCapabilities (in your setBuffers function).

    BufferStrategy bf = getBufferStrategy();
    if (bf != null) {
        BufferCapabilities caps = bf.getCapabilities();
        try {
            Class ebcClass = Class.forName(
                "sun.java2d.pipe.hw.ExtendedBufferCapabilities");
            Class vstClass = Class.forName(
                "sun.java2d.pipe.hw.ExtendedBufferCapabilities$VSyncType");

            Constructor ebcConstructor = ebcClass.getConstructor(
                new Class[] { BufferCapabilities.class, vstClass });
            Object vSyncType = vstClass.getField("VSYNC_ON").get(null);

            BufferCapabilities newCaps = (BufferCapabilities)ebcConstructor.newInstance(
                new Object[] { caps, vSyncType });

            createBufferStrategy(2, newCaps);

            // TODO: if success, setCanChangeRefreshRate(false) and setRefreshRate(60). 
            // Possibly override refreshRateSync()?
        }
        catch (Throwable t) {
            // Ignore
            t.printStackTrace();
        }
    }

EDIT this code was from (http://pulpcore.googlecode.com/hg-history/3c4001969922b93048e0a166395115df059a2059/src/pulpcore/platform/applet/BufferStrategySurface.java)

Also, you should be calling setBuffers AFTER the Canvas constructor has run and it's been added to the JFrame.


I'm "relatively sure" you don't need to call super.paint(g); I don't know if its what's causing your specific issue, but you should remove the line from your paint function.


Though you don't show the code, your explosion mechanism using 200 randomly moving sprites could be a major cause for errors. Figure out the maximum number of explosions you want to have on the screen at one time and generate those sprites N * 200 before hand, put them in an array list ArrayList(N) and then in your code reference them cyclically. ExplosionClass = explosionList.get(currentExplosionIndex % explosionList.size());

Louis Ricci
  • 20,804
  • 5
  • 48
  • 62
  • Thanks, but unfortunately System properties don't seem to help. Vsync is not really the problem here, I think it may be @DannyVarod ^-s expectation of the error. – Simon Tamás Feb 07 '13 at 19:35
  • Also generating particles is super fast, but moving them is not. I tried it using a low particle count emitter. – Simon Tamás Feb 07 '13 at 19:40
  • @Simon Tamás - I would of thought at least the java2d.trace system property would allow you to come back with a more specific error message. – Louis Ricci Feb 07 '13 at 20:07
  • no error messages, but when no particle is active, D3DTextureToSurfaceBlit is being called, when there are particles, D3DSwtoSurfaceBlit is being called. Note that only the particles are BufferedImages, everything else is a simple Image. – Simon Tamás Feb 07 '13 at 20:18
  • Also, forcing opengl freezes the game, black screen, no trace :( – Simon Tamás Feb 07 '13 at 20:26
1

Check the size of the pictures compared to the caches, and try to diagnose the number of cache misses. Read up on data oriented design, it's usually a part of that.

An alternative is to change the way you do the explosions to a way where you don't load in 200 sprites and spread them out (do you reuse them, or do you load them every time you do an explosion?). Making an animated explosion takes less power, though it might not be as spectacular, and you would need to do some animating then.

Java also isn't the most optimal language for making games. You're working very high level, and the JVM slows things down a bit. My friends had similar problems when making small games, it was difficult to make a game that didn't lag tons.

Wertilq
  • 175
  • 1
  • 10
0

When you send things to the GPU, the GPU reads the info from the GDRAM. If you read the data into the memory by calling getDrawGraphics then the memory is probably read from graphics card back into the RAM. The CPU can only access the RAM (DRAM), GPUs can only access the GDRAM. With on-board GPUs this is however different, since they don't come with their own RAM, they use a part of the regular RAM.

To return cause your code to run quickly, determine which hardware you have, then accordingly call the appropriate method (the one before your code change or the one after).

Danny Varod
  • 17,324
  • 5
  • 69
  • 111
  • The thing is, I didn't call any other method, it was Java doing something different. Unfortunately changing code based on HW is not an option, because developing would be a lot harder, since one is a JPanel, one is Canvas, plus copying code from one to other is time consuming. – Simon Tamás Feb 08 '13 at 14:15
  • Have both options call a utilities class which does the work which isn't dependant on the caller, or uses an interface to the two callers which via adaptors provides abstraction, so that the utility can access them via the same interface. – Danny Varod Feb 09 '13 at 12:13