How to have BufferedImages take up less RAM

Hello! As the title says, BufferedImages take up the majority of the RAM in my Java game. I use mostly pngs; some of which are transparent.

I initiate the BufferedImage objects simply like so:


BufferedImage example = ImageIO.read(Runner.class.getResource("/exampleImage.png"));

Then, I use a getCompatibleImage() function to make it the best image for rendering on the system.


static BufferedImage getCompatibleImage(BufferedImage current) {
    GraphicsConfiguration gfxConfig = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
    if(current.getColorModel().equals(gfxConfig.getColorModel()))
        return current;
    BufferedImage optimized = gfxConfig.createCompatibleImage(current.getWidth(), current.getHeight(), current.getTransparency());
    Graphics2D g2d = optimized.createGraphics();
    g2d.drawImage(current, 0, 0, null);
    optimized.setAccelerationPriority(1);
    return optimized;
}

I use about 1219 BufferedImage objects, which is quite a lot. I read all of them at the beginning with a loading screen splash image and keep all of them as static b/c I do not want to reload them on a restart.

Please let me know what I can do to use less RAM. Thanks!

  • Java is an OO language, thus the term for a unit of code is ‘method’ not ‘function’.

  • Not wishing to reload the images is not a good reason to make them static.
    As your program develops, the scope of your resources is likely to change; encapsulating the resources within instanced objects makes refactoring your code to accommodate such changes easier.
    The typical example of this would be where your design transitions from:
    a) having a single level, where all resources are held in memory at once
    b) to multiple levels, where intermediary loading screens are presented to the user.
    c) to, perhaps, an infinite world where resources are streamed into memory in the background as the user navigates the world.

  • Images returned from ImageIO have been eligible for hardware acceleration for many years; there’s no need to copy them to a new image.

As to your question:

  1. Increase heap size; seems obvious, but you haven’t stated whether you’ve tried this.
  2. Don’t keep everything loaded at once. As suggested above, levels + load screens, or background streaming.

It’s also worth keeping in mind that every Image in the heap also occupies VRAM for its cached copy used by the h/w accelerated pipeline.
Depending upon the h/w rendering pipeline being used by Java2D (which will vary by platform, and cmdline flags), you might be wasting large amounts of VRAM because Java2D is creating power-of-2 textures that poorly fit the dimensions of your Image.
While this inefficiency won’t increase your heap usage, it might significantly increase vram usage - perhaps to the point where it negatively impacts performance.
The solution here is to adopt sprite sheets, along with the associated tooling to generate them, and code to draw them.

[quote]As your program develops, the scope of your resources is likely to change; encapsulating the resources within instanced objects makes refactoring your code to accommodate such changes easier.
[/quote]
Okay. I will try to remove the static keyword and try to use encapsulation more for that mater.

[quote]Images returned from ImageIO have been eligible for hardware acceleration for many years; there’s no need to copy them to a new image.
[/quote]
The part about writing to a new image is for making sure the graphics object is ideal for the graphics configuration on the system. So, it draws a compatible image onto the BufferedImage itself, replacing the other version. So, it’s not simply for hardware acceleration.

[quote]Increase heap size; seems obvious, but you haven’t stated whether you’ve tried this.
[/quote]
I have increased heap size using -Xmx if that’s what you mean. The application runs smoothly, so my wish of reducing the RAM usage does not have to do with the application’s performance.

[quote]Don’t keep everything loaded at once. As suggested above, levels + load screens, or background streaming.
[/quote]
However, in doing so, the user may get impatient as there would be a lot of loading going on, and it takes quite a while. So, the user would have to keep waiting every time the application switches from the start screen to the game screen and vice versa.

[quote]Depending upon the h/w rendering pipeline being used by Java2D (which will vary by platform, and cmdline flags), you might be wasting large amounts of VRAM because Java2D is creating power-of-2 textures that poorly fit the dimensions of your Image.
[/quote]
I see… I never knew that before. So, I have two questions based upon that:

1) Is there a JVM argument that prevents this waste?
2) Is loading one large image like a spritesheet more efficient than loading multiple images?

Thanks for taking the time to help!

As I understand it BufferedImage already does this under the hood, so you are essentially wasting your time here I think (could probably prove this to yourself with some benchmarks).

No. This is something you’ve got to get down and dirty with yourself, and it’s pretty much a rite of passage in games development. Use the biggest sprite sheets you can get away with - 1024x1024 is supported by all video hardware these days, and actual dedicated GPUs all manage 4096x4096.

Vastly.

Cas :slight_smile:

Thanks! Also, I heard that VolatileImages are more efficient if you don’t modify the image but draw them often, which fits my bill. So, would this be the best way to make an image into a volatile image, or am I doing this incorrectly?


Image example = ImageIO.read(MainClass.class.getResource("/example.png"));
example = getCompatibleImage(example);

static VolatileImage getCompatibleImage(Image current) {
    GraphicsConfiguration gfxConfig = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
    VolatileImage optimized = gc.createCompatibleVolatileImage(current.getWidth(), current.getHeight(), Transparency.TRANSLUCENT);
    Graphics2D g2d = optimized.createGraphics();
    g2d.drawImage(current, 0, 0, null);
    optimized.setAccelerationPriority(1);
    return optimized;
}

Seems right. The problem with VolatileImage is that they can randomly lose their contents so you need to keep the original around anyway to restore it.

Cas :slight_smile:

I see. Is there a way to detect whether the contents of a VolatileImage have been lost to check if it needs to be restored? If not, would it be better to use BufferedImages or generate the VolatileImage each time the image is to be drawn?

All in the docs: https://docs.oracle.com/javase/7/docs/api/java/awt/image/VolatileImage.html

Maybe more trouble than it’s worth though. Exactly how much blitting are you going to be doing?

Cas :slight_smile:

Hmmm… Based on what I looked up for “blitting,” I do resize some images in Graphics.drawImage() or use AffineTransform. I also have several images where I change the AlphaComposite to change the transparency. If I am misinterpreting the term “blitting,” please let me know.

Maybe I’m confused, but I recall that Java automatically caches a VolatileImage copy of a BufferedImage somewhere if you draw that image a lot, meaning that it is in fact best to use BufferedImages for your regular textures.

Essentially yes, VolatileImages are generally only of relevance for the target of a draw, not a source.
Typically of use if you’re doing some fancy compositing, or buffering.

If all you’re doing is drawing images, stay well clear of them.

So, the main thing I seem to be getting from this thread is that sprite sheets are a good way of reducing memory usage. The way my game is set up, the intermediate loading screen would not really work too well. Although, I’ll look into it anyway.

I’m going to use a profiler to see what things are taking the most resources. (I already know that BufferedImages take up around 99% of the RAM usage, so I’ll try and look more specifically at the methods.) I’ll update with any questions I have involving that later.

Other than that, I can’t seem to find many other ways to reduce the amount of RAM being taken up by the large quantity of images I have. If there are any other general tips anyone could provide when dealing with many resources, I would appreciate it.

There is no way around graphics taking up the amount of system RAM that they do. The best you can do is pack them all tightly into a single big BufferedImage and blit out chunks when you want.
“Blitting” btw is an old term, “blit” being short for “block image transfer”. In other words, copying a rectangle of pixels from one place to another. It’s almost always hardware accelerated these days, even with BufferedImage, and BufferedImage is a perfectly sensible way to do everything.

Of course if you wanted to do something a bit more modern you’d use JavaFX. And if you wanted to get a bit more fancy, LWJGL.

Cas :slight_smile:

My inclination is that a single BufferedImage might cause issues too? I’d meet in the middle, create a series of BufferedImage with multiple images on (sprite sheets), trying to group images that are used together on the same image. Maybe 1024x1024 (4MB) or 2048x2048 (16MB)? You want to give the images used in the same frame the best chance of being managed and cached in hardware together.

As @Abuse says, this is the opposite of the case. You want a VolatileImage if you are modifying the image a lot. If you don’t use a Graphics2D obtained from calling image.getGraphics() much or at all, you don’t need VolatileImage. Where people often fall down is they assume that managed images mean the Graphics2D from a BufferedImage is hardware accelerated - it isn’t!

I have some questions.

[quote]Java2D is creating power-of-2 textures that poorly fit the dimensions of your Image.
[/quote]
So, would it be better to have a 1024x1024 image with extra white or transparent pixels (whichever is more efficient) or have spritesheets of various sizes such as 960 x 905?

Also, would it be best to get the subImages when drawing each time (example two) or store them into BufferedImage objects and have them draw those without having to use the getSubImage() method any more times (example one)?

For example:


BufferedImage spriteSheet = ImageIO.read(fileLocation);

//Example one
BufferedImage exampleOne = spriteSheet.getSubimage(50, 50, 40, 40);
g.drawImage(exampleOne, 0, 0, null);

//Example two
g.drawImage(spriteSheet.getSubimage(50, 50, 40, 40), 0, 0, null);

Neither! Don’t create sub-images, use the variant of drawImage() that takes 8 arguments.

I see! That makes sense. Could you or someone else respond to the part in my previous post about the image dimensions and filler?

This is what I would do.
-> load tilesheet into a BufferedImage and convert it to int[]
-> create a BufferedImage that will handle the drawings with, again, an int[]
-> with this way, you will have to create your own drawing methods, since all image data is handled through int[]

I’m sceptical that Java2D is using power-of-two surfaces, but either way I’d use a uniform size as it’ll make things easier to deal with - 1024x1024 sounds like a good starting point. Definitely fill unused bits with transparent pixels if the image has transparency - there’s no difference in efficiency and no chance of getting any white pixels showing by accident.

What for?! For the purpose of ensuring there’s no way at all any capabilities of the GPU are used? :-\

Okay, you are right about that. But I don’t know the dimensions of his game. It would reduce the BufferedImages drastically and he would only need to get dta from the tilesheet int[] and copy it to the main int[] (the one that is being drawn)