gluBuild2DMipmaps() mem-leak on GLError

Started by Skippy0, January 25, 2005, 12:44:07

Previous topic - Next topic

Skippy0

I've been using MipMap.gluBuild2DMipmaps() to create texture-mipmaps.

When using huge textures I began to run into trouble:
- texture size: 4096*4096 RGBA (64MB raw)

I expected RAM-usage to expode here, when the mipmaps were being generated, and indeed it did: ~200MB.
That's not so bad, if it would released too, but it's not!

Loading the same texture without mipmap-ing makes the java-process consume ~20MB ram, as the texture is stored in vRAM. With mipmapping, the ram-usage of the java-process stays at >240MB, swapping to page-file initially, increasing the startup-time by a few minutes (!).

So why is MipMap.gluBuild2DMipmaps() not releasing it's buffers? (or: what am I missing?)

Specs:
- WinXP, ATi Radeon 9700pro, 512RAM, P2.4GHz, JRE 1.5.0_00

Skippy0

I narrowed it down a bit:

It only appears to be happening when a GLError occurs inside MipMap.gluBuild2DMipmaps()
(if it doesn't support 4096^2 textures)

Even if this is the case, that shouldn't prevent it from releasing the buffers.

Matzon

are you nulling the (invalid) buffer afterwards? - else it won't be reclaimed

Skippy0

Yes.

A 64MB buffer can't take 200MB ram anyway. I suspect there are new buffers being created and not properly nullified on error in the mipmap-method.

tomb

Have you looked at the gluBuild2DMipmaps() source? It's in the src\java\org\lwjgl\opengl\glu\MipMap.java file in the source package that can be downloaded from sourceforge with the rest of lwjgl.

I've had a quick look and can't see anything wrong. The buffers are created inside the function and no referance is given away as I can tell. Although it can be optimized to reduce the number of buffers that are created. A new one don't have to be created at every mipmap level. It can instead reuse the previous one.

Have you tried a System.gc() to make sure a full gc is performed? Have you tried on a different computer to rule out driver bugs?

princec

Buffers work differently in Java. They are allocated on the heap, totally bypassing the GC and never triggering collection - only the stub Java object is held on the Java heap.

All well and good.

However, they are not disposed immediately as they have a finalizer. This means that they are collected on a whim at an indeterminate point in the future.

The end result is.... this is not a good way to do it. We'd better find an alternative.

Cas :)

tomb

Quote from: "princec"Buffers work differently in Java. They are allocated on the heap, totally bypassing the GC and never triggering collection - only the stub Java object is held on the Java heap.

All well and good.

However, they are not disposed immediately as they have a finalizer. This means that they are collected on a whim at an indeterminate point in the future.

The end result is.... this is not a good way to do it. We'd better find an alternative.

Cas :)

So native buffers are not collected in the same way as the rest of the garbage? I assumed that it was part of the gc algo, and that it would be collected if a full gc was performed, like when you call System.gc().

Matzon

I think that Cas is saying that they behave like any other object in Java - that is, it is garbage collected when the GC'er determins to do so. For instance when you suggest a garbage collection via System.gc();

So you have to "wait" for the garbage collection when you're done with a large bytebuffer (which can be anything from miliseconds to seconds). A better way would be to be able to call Buffer.dispose() or something like that, so you're sure that is deleted reight now.

spasi

I think this situation is worse with direct buffers. Java buffers are plain java arrays, normally collected as usual, but maybe collecting memory outside the Java heap, for direct ones, is handled differently.

Anyway, tomb is right, that method can be optimized. I'll fix it one of these days (if noone else does in the meantime).

Matzon

trying to see if there is anything we can do to force a dispose - but I can't find anything usefull...

A buffer is implemented in j2se/src/share/classes/java/nio/Direct-X-Buffer.java. It does not implement finalize and all reclaiming is done using sun.misc.Cleaner (PhantomReference) and sun.misc.Unsafe. However it all seems to be run automatically - thats is, I can't find any method (public or private) for activating the cleaning sequence of the Buffer... Will investigate more, later

princec

Spasi - yes, that's exactly what I'm trying to say. The memory is allocated outside the heap and is only cleared by a finalizer. This is unfortunate because it may take a long time before the GC decides to clear it, and it can easily cause the machine to start swapping.

<edit>Actually Matzon's got it right but the mechanism is roughly the same - you've got to wait for the reference queue to get around to noticing the buffer is a phantom. Which may be never.

Cas :)

spasi

I commited a change with which at most 2 buffers are allocated, for all levels. But...

I highly recommend that noone uses at runtime this evil method!

Especially for so large textures that Skippy uses. It may be ok as an off-line preprocess, but if there's absolutely a need for runtime mipmap generation one should either implement custom image scaling or use a GL extension to do it (SGIS_generate_mipmap is widely supported).

Some details:

1) Because this is a public interface, the input buffer cannot be modified. In a custom implementation one would certainly reuse it after sending the first level to GL. One buffer less.

2) gluScaleImage allocates two float arrays (!), one for input and one for output, for each level! Highly inefficient and I'm pretty sure that's where Skippy's 200mb came from.

3) The implementation is generally a not so good port of the C equivalent (e.g. the buffer set/get with explicit offsets in gluScaleImage).

Skippy0

To comment on your reply, spasi:

I've already changed it, it's using 16 1024x1024 textures now, better to cull too :wink:

I don't know whether or not reducing the allocated buffers will be much faster, as I suspect the actual scaling to be 99% of the cpu-cycles. RAM usage could be a problem on low-end systems.

spasi

Quote from: "Skippy0"I don't know whether or not reducing the allocated buffers will be much faster, as I suspect the actual scaling to be 99% of the cpu-cycles. RAM usage could be a problem on low-end systems.

Yeah, speed should not be too much of problem, it's done only once at load-time anyway, the point is that ram usage is just too wasteful. Everything should be garbage collected immediately of course, but you can't be sure how it would affect load time and how the OS would handle your app.

Please, have a look at the two methods (gluBuild2DMipmaps and gluScaleImage that is used by it) and calculate the amount of memory allocated for each image, for each mip level. The total for 16 1K images is enormous!

Now that I think about it, GLU should be moved to the util package, to make it more obvious that it shouldn't be used for production purposes. What do others say?

tomb

GLU has to stay as part of the core. Any performance problems can be noted in the javadoc.

Btw, I'm using gluBuild2DMipmaps in Squareheads and I haven't noticed any huge performance problems. But I'm creating so much garbage at loading it's hard to tell where it's all coming from :)