OutOfMemoryException by changing (big) textures on an object. [SOLVED]

*Hey… i think i found a strange MemoryLeak within lwjgl code. * …forget about :wink:



I'm trying to change the texture of an object at a KeyEvent.

My purpose is a virtual presentation, so for that i use a PdfRenderer which creates an image of each pdfFile.

I recognized that the memoryallocation raises with each change of texture… until a OutOfMemoryException occurs.



So i build a little test.

All you have to do is to create 17 jpgs named and placed in c:imgtmp0.jpg (tmp16.jpg)

In my testcase the jpgs are 1024x1024, ~200kb each.

You switch the texture by the right/left arrow key.



I'll be thankfull if anyone might verify this.



In addition i run the tptp Profiler in Eclipse, where i recognized that this leak seems to be within the MipMap class of lwjgl.



Here a testCase, and below some screens of profiling results. In this case the OutOfMemoryException occurs at #14, if i don't change the VM's heapsize.


package test;

import java.net.URISyntaxException;
import java.util.logging.Level;
import java.util.logging.Logger;

import jmetest.effects.TestProjectedTexture;

import com.jme.app.SimpleGame;
import com.jme.image.Texture;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.math.Vector3f;
import com.jme.renderer.Renderer;
import com.jme.scene.BillboardNode;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.CullState;
import com.jme.scene.state.ShadeState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jme.util.resource.ResourceLocatorTool;
import com.jme.util.resource.SimpleResourceLocator;

/**
 * <code>TestProjectedTexture</code>
 *
 * @author Rikard Herlitz (MrCoder)
 */
public class VerifyMemoryLeakScaleImage extends SimpleGame
{
   private static final Logger logger = Logger.getLogger(VerifyMemoryLeakScaleImage.class.getName());



   private Texture projectedTexture2;

   private TextureState ts;

   private Quad frame;


   private boolean switchPage = false;

   private int desiredImageWidth;


   private int currentPage = 0;

   private int nextPage = 1;



   public VerifyMemoryLeakScaleImage()
   {
      super();
      desiredImageWidth = 1280;
   }



   protected void simpleUpdate()
   {

      if (KeyBindingManager.getKeyBindingManager().isValidCommand("nextPdfPage", false) && currentPage < 15)
      {
         switchPage = true;
         nextPage = (currentPage + 1);
         System.out.println(currentPage + "/" + nextPage);
      }

      if (KeyBindingManager.getKeyBindingManager().isValidCommand("lastPdfPage", false) && currentPage > 0)
      {
         switchPage = true;
         nextPage = (currentPage - 1);
         System.out.println(currentPage + "/" + nextPage);
      }


      if (switchPage)
      {
         System.out.println("prepare to show pdfPage: " + nextPage);

         if (nextPage > 0 || (nextPage < 15))
         {

            String path = "c:\img\tmp" + nextPage + ".jpg";
            System.out.println("try to load image: " + path);

            projectedTexture2 = TextureManager.loadTexture(path, Texture.MM_NONE, Texture.FM_NEAREST, 8, true);
            ts = display.getDisplaySystem().getRenderer().createTextureState();
            ts.setTexture(projectedTexture2, 0);
            frame.setRenderState(ts);
            frame.updateRenderState();

            currentPage = nextPage;
            switchPage = false;
         }
      }

   }


   protected void simpleInitGame()
   {
      cam.setFrustumFar(40000);
      try
      {
         try
         {
            ResourceLocatorTool.addResourceLocator(ResourceLocatorTool.TYPE_TEXTURE, new SimpleResourceLocator(
                  TestProjectedTexture.class.getClassLoader().getResource("")));
         }
         catch (URISyntaxException e1)
         {
            logger.log(Level.WARNING, "unable to setup texture directory.", e1);
         }

         display.setTitle("Projected Texture Test");

         cam.getLocation().set(new Vector3f(50, 50, 0));
         cam.lookAt(new Vector3f(), Vector3f.UNIT_Y);

         CullState cs = display.getRenderer().createCullState();
         cs.setCullMode(CullState.CS_BACK);
         cs.setEnabled(true);

         Renderer r = display.getRenderer();
         ShadeState s = r.createShadeState();
         s.setShade(ShadeState.SM_FLAT);


         String path = "c:\img\tmp" + currentPage + ".jpg";

         projectedTexture2 = TextureManager.loadTexture(path, Texture.MM_NONE, Texture.FM_NEAREST, 8, true);
         // projectedTexture2.setScale(new Vector3f(0.75f, 0.75f, 0.75f));
         ts = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
         ts.setTexture(projectedTexture2, 0);


         frame = new Quad("frame", 300, 300);
         // frame.setLocalRotation(new Quaternion().fromAngleAxis(45,
         // Vector3f.UNIT_Y));
         frame.getLocalTranslation().y = 150;
         frame.setRenderQueueMode(com.jme.renderer.Renderer.QUEUE_OPAQUE);
         frame.setRenderState(cs);

         frame.setRenderState(ts);
         // rootNode.attachChild(frame);

         BillboardNode billboardNode = new BillboardNode("bbNode");
         billboardNode.attachChild(frame);
         billboardNode.setAlignment(BillboardNode.AXIAL);
         rootNode.attachChild(billboardNode);

         Box floor = new Box("b", Vector3f.ZERO, 200, 1, 200);
         rootNode.attachChild(floor);


         TextureState ts2 = display.getRenderer().createTextureState();
         ts2.setEnabled(true);
         Texture t1 = TextureManager.loadTexture(VerifyMemoryLeakScaleImage.class.getClassLoader().getResource(
               "jmetest/data/texture/Detail.jpg"), Texture.MM_LINEAR, Texture.FM_LINEAR);
         ts2.setTexture(t1, 0);
         t1.setApply(Texture.AM_COMBINE);
         t1.setCombineFuncRGB(Texture.ACF_MODULATE);
         t1.setCombineSrc0RGB(Texture.ACS_TEXTURE);
         t1.setCombineOp0RGB(Texture.ACO_SRC_COLOR);
         t1.setCombineSrc1RGB(Texture.ACS_PRIMARY_COLOR);
         t1.setCombineOp1RGB(Texture.ACO_SRC_COLOR);
         t1.setCombineScaleRGB(1.0f);
         floor.setRenderState(ts2);

      }
      catch (Exception e)
      {
         logger.logp(Level.SEVERE, this.getClass().toString(), "simpleInitGame()", "Exception", e);
      }

      KeyBindingManager.getKeyBindingManager().add("nextPdfPage", KeyInput.KEY_RIGHT);
      KeyBindingManager.getKeyBindingManager().add("lastPdfPage", KeyInput.KEY_LEFT);
   }


   public static void main(String[] args)
   {
      VerifyMemoryLeakScaleImage app = new VerifyMemoryLeakScaleImage();
      app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);
      app.start();
   }

}





as mentioned. here the screens from tptp profiling in eclipse.

its obvious that the most memoryallocation happens in lwjgl.MipMap.

It seems that the float[] are kept, and never get released.  :?



ignore the red marks… first i thought it only happens when the textures have to be rescaled, but it happens without rescaling as well.



http://img387.imageshack.us/img387/8152/memoryanalysiswithoutschu5.jpg

http://img411.imageshack.us/img411/7562/memoryanalysisaa3.jpg

Can you show a stack trace for where the gluScaleImage calls come from? It should only be used for generating mipmaps and to scale non-square/non-power-of-two images.

of course…



produced with the testcase above.

the only thing done there is: TextureManager.loadTexture()



java.lang.OutOfMemoryError: Direct buffer memory

at java.nio.Bits.reserveMemory(Bits.java:632)

at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:95)

at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)

at org.lwjgl.BufferUtils.createByteBuffer(BufferUtils.java:60)

at com.jme.scene.state.lwjgl.LWJGLTextureState.load(LWJGLTextureState.java:339)

at com.jme.scene.state.lwjgl.LWJGLTextureState.apply(LWJGLTextureState.java:484)

at com.jme.renderer.lwjgl.LWJGLRenderer.applyStates(LWJGLRenderer.java:1609)

at com.jme.renderer.lwjgl.LWJGLRenderer.draw(LWJGLRenderer.java:955)

at com.jme.scene.batch.TriangleBatch.draw(TriangleBatch.java:255)

at com.jme.renderer.RenderQueue.renderOpaqueBucket(RenderQueue.java:250)

at com.jme.renderer.RenderQueue.renderBuckets(RenderQueue.java:238)

at com.jme.renderer.Renderer.renderQueue(Renderer.java:452)

at com.jme.renderer.lwjgl.LWJGLRenderer.displayBackBuffer(LWJGLRenderer.java:509)

at com.jme.app.BaseGame.start(BaseGame.java:85)

at test.VerifyMemoryLeakScaleImage.main(VerifyMemoryLeakScaleImage.java:189)

you say the jpegs are 200kb each - but thats just compression that can shrink the storage space.



What is the pixel resolution of the images ?? 512 x 512…

hey there,



do not forget that the jpeg image is stored in the memory uncompressed … also, if you want to use MipMapping, it will generate an image that is about twice that size … make a few of these and every memory will find its end.



i suggest you use Direct Draw Surface (.DDS) image files that were created for exactly that purpose. They can be stored in memory compressed and even though the image files are bigger in the file system, they will consume MUCH less memory when you load them into jME.



so long,

Andy

the resolution of these images is 1024x1024. Needed because they are applied to an virtual canvas for presentations. so you should be able to read text/images shown as virtual slides, even from a certain distance.

Because of that, i don't want to use MipMapping, which would decrease quality.



you can think of the scenario as a real presentation. A Room, a table, multiple auditors, and a speaker showing his slides.

On certain Events the current slide should change to the next/previous.



so there should be only one of these textures at time on the virtual canvas.

But changing the aplied texture increase the heapspace used by the application.

That's beacause … what i haven't known so far… the TextureManager keeps a certain amount of Textures within his own cache.



And dhdd is right… keeping few of those big textures in the cache kills every memory :wink:

Searching for the solution, i finally found one.

TextureManager.clearCache() keeps the used memory on a constant level. (Haven't been aware of this method yet  ://)



So if TextureManager keeps his cache with all those textures, and every Texture up to doubled size because of MipMapping…  :evil:



So i'll have to check if clearing the TextureManager's cach has some negative side-effects. But the memorything is solved…



thx guys!



edit

A more cleaner solution is the TextureManager.releaseTexture() i think.

It enables to clear a specified texture from the cache.

Exactly what is needed here, handling big textures of which only one is needed at every time.

Perhaps we could get a wiki entry together on memory/cleanup issues - as in what methods there in jME are and when to use them. It's definitely something I have been unsure about.



Regarding mipmaps - I have never undertaken a project like this, but shouldn't mipmaps (in theory at least) increase your readability? Since the slide will be scaled regardless by virtue of being in a 3d world, you are going to lose pixels. If a large texture is rendered onto a small surface, you don't know which pixels you're going to lose - where as if you use good mipmaps you should have more control?

well, mipmaps are there to increase visual quality and i guess one should use it when the same texture is applied to several geometries with different sizes, or on geometries that can move away and closer to the camera. mipmaps are quite the inexpensive way to achieve good texture rendering quality.



guys i cannot emphazise this more: use .DDS files, they can have the mipmaps rendered into the file wich GREATLY speeds up the startup of the application because jME doesnt have to compute the mipmap levels, and also the memory consumption is a lot less, since they are stored compressed in memory. it just takes downloading a plugin for the graphical editor of your choice.



so long,

Andy