Need advice on LoadImage (big image crashes)

Monkey Targets Forums/Android/Need advice on LoadImage (big image crashes)

nori(Posted 2013) [#1]
On Android, does LoadImage create a "reference" and load the image into video memory when DrawImage needs it?

Or does it work the "traditional way" and load into video memory immediately?

And if the first is true, is this behaviour of loading Images part of Monkey or part of the Android API that Monkey uses? I mean, if I'd chose to develop with Android/Java directly, could I get an API that is capable of preparing an image into video memory immediately, not only on the first draw command?

Because for now it seems to me I'm able do successful LoadImage calls worth hundreds of MB and get an out of memory crash only when using DrawImage. With this API I've got no idea how to create an "all qualities version" of my game that loads assets correspondingly to how much video memory there is on the device. Would I have to catch the out of memory exception that DrawImage produces?

PS: I'm not trying to create any crazy stuff, just something that Gideros Mobile does, that is, creating an app that contains images of different resolutions and then judging which to use at run-time. I think, Gideros does do this correspondingly to the screen size, while my above text is about doing this depending on how much video ram there seems to be. Any way would do. What I try to omit is the "app / HD app" way where the developer just tests which version crashes on the users phones.


Gerry Quinn(Posted 2013) [#2]
I think you need to decide at the start what size of images you will need, and then load just the appropriate ones. That's presumably what Gideros are doing, and of course you could easily do the same on Monkey by checking the device width and height.

If you are deciding based on video memory, you will have to find a way of determining that, which probably means looking up the native API. At a pinch, you could run in a conservative resolution, and give the user an option of trying a higher resolution to see what happens (if you do this, be sure to create a way back if it can cause a crash).

It used to be not unknown for PC games to come with a window at the start in which you choose graphic options.


nori(Posted 2013) [#3]
Thanks for the help. Did that. It has an HD-button now :) Actually really true, PC games ask for the resolution too.


nori(Posted 2013) [#4]
I still can't make my app prevent a crash when trying to load a too big image.

At some point memory is tried to be allocated, which seems to correlate with the LoadImage call that returned Null.

I/[Monkey]( 5957): entering onrender
I/[Monkey]( 5957): leaving onrender
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): entering switch_quality(), only called inside onupdate
I/[Monkey]( 5957): loadimage returned Null!!!!!!!!
I/[Monkey]( 5957): asigned little, working, X-image instead
I/[Monkey]( 5957): checked, and it is non-null
I/[Monkey]( 5957): leaving switch_quality()
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onrender
I/[Monkey]( 5957): leaving onrender
E/dalvikvm-heap( 5957): Out of memory on a 9228500-byte allocation.
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
E/AndroidRuntime( 5957): Uncaught handler: thread GLThread 10 exiting due to uncaught exception
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate
E/AndroidRuntime( 5957): java.lang.OutOfMemoryError
E/AndroidRuntime( 5957): 	at java.nio.HeapByteBuffer.<init>(HeapByteBuffer.java:45)
E/AndroidRuntime( 5957): 	at java.nio.ReadWriteHeapByteBuffer.<init>(ReadWriteHeapByteBuffer.java:49)
E/AndroidRuntime( 5957): 	at java.nio.BufferFactory.newByteBuffer(BufferFactory.java:51)
E/AndroidRuntime( 5957): 	at java.nio.ByteBuffer.allocate(ByteBuffer.java:54)
E/AndroidRuntime( 5957): 	at com.monkeycoder.monkeygame.gxtkSurface.Bind(MonkeyGame.java:2225)
E/AndroidRuntime( 5957): 	at com.monkeycoder.monkeygame.gxtkGraphics.Flush(MonkeyGame.java:1606)
E/AndroidRuntime( 5957): 	at com.monkeycoder.monkeygame.gxtkGraphics.EndRender(MonkeyGame.java:1748)
E/AndroidRuntime( 5957): 	at com.monkeycoder.monkeygame.c_GameDelegate.RenderGame(MonkeyGame.java:3709)
E/AndroidRuntime( 5957): 	at com.monkeycoder.monkeygame.BBGame.RenderGame(MonkeyGame.java:574)
E/AndroidRuntime( 5957): 	at com.monkeycoder.monkeygame.BBAndroidGame.onDrawFrame(MonkeyGame.java:1310)
E/AndroidRuntime( 5957): 	at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1127)
E/AndroidRuntime( 5957): 	at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:975)
I/[Monkey]( 5957): entering onupdate
I/[Monkey]( 5957): leaving onupdate



AdamRedwoods(Posted 2013) [#5]
On Android, i believe that everything is loaded, then moved to video memory on first Draw(). Then the image is dumped from regular memory to prevent heap limitations. SO if you want to be more efficient, try loading a few images, then Draw() them, then load more.

This is particular to Mojo, not Monkey. (in miniB3D, I do it differently).


AdamRedwoods(Posted 2013) [#6]
I still can't make my app prevent a crash when trying to load a too big image.

One trick to try and get around the dreaded heap-allocation error, is to DrawImage() first, then load the next image. yes this means creating a loading loop that draws the image(then immediately Cls to hide it). this will put the image in video memory, then clear it out from the heap.


nori(Posted 2013) [#7]
Yes, it seems like this (loaded to video memory on first draw). So I made my app display a "Please wait message" when there are undrawn images and then "touch" (draw without use) all images that should appear fast to look nice.

But I can't figure out why Mojo tries to allocate memory and fails, I thought with the LoadImage call returning Null all should be cleaned-up and ok?..


nori(Posted 2013) [#8]
Yep, did that draw+cls trick, kinda.


nori(Posted 2013) [#9]
My Huawai U8500 fails the simplest test case. Is this a mojo bug?
I'm using Monkey 75d.

Import mojo

Class Game Extends App

	Field image:Image
	
	Method OnCreate()		
		SetUpdateRate 30

		'image = LoadImage("960x600.png") ' works
		image = LoadImage("1920x1200.png") ' crashes with "Out of memory on a 9228500-byte allocation."
	End

	Method OnRender()
		If image DrawImage(image, 0, 0)
	End
End

Function Main()
	New Game
End


Output with the 1920x1200.png:

BUILD SUCCESSFUL
Total time: 14 seconds
Starting: Intent { cmp=com.monkeycoder.monkeygame/.MonkeyGame }
E/Sensors ( 9326): ******NON U8120 compass*******
E/Sensors ( 9326): sensors_ak8973.c in get_board_info_for_conver,g_board_id = 0;
E/copybit ( 9326): Error opening frame buffer errno=13 (Permission denied)
E/copybit ( 9326): Error opening frame buffer errno=13 (Permission denied)
E/dalvikvm-heap( 9326): Out of memory on a 9228500-byte allocation.
E/AndroidRuntime( 9326): Uncaught handler: thread GLThread 9 exiting due to uncaught exception
E/AndroidRuntime( 9326): java.lang.OutOfMemoryError
E/AndroidRuntime( 9326): 	at java.nio.HeapByteBuffer.<init>(HeapByteBuffer.java:45)
E/AndroidRuntime( 9326): 	at java.nio.ReadWriteHeapByteBuffer.<init>(ReadWriteHeapByteBuffer.java:49)
E/AndroidRuntime( 9326): 	at java.nio.BufferFactory.newByteBuffer(BufferFactory.java:51)
E/AndroidRuntime( 9326): 	at java.nio.ByteBuffer.allocate(ByteBuffer.java:54)
E/AndroidRuntime( 9326): 	at com.monkeycoder.monkeygame.gxtkSurface.Bind(MonkeyGame.java:2222)
E/AndroidRuntime( 9326): 	at com.monkeycoder.monkeygame.gxtkGraphics.Flush(MonkeyGame.java:1603)
E/AndroidRuntime( 9326): 	at com.monkeycoder.monkeygame.gxtkGraphics.EndRender(MonkeyGame.java:1745)
E/AndroidRuntime( 9326): 	at com.monkeycoder.monkeygame.c_GameDelegate.RenderGame(MonkeyGame.java:2767)
E/AndroidRuntime( 9326): 	at com.monkeycoder.monkeygame.BBGame.RenderGame(MonkeyGame.java:571)
E/AndroidRuntime( 9326): 	at com.monkeycoder.monkeygame.BBAndroidGame.onDrawFrame(MonkeyGame.java:1307)
E/AndroidRuntime( 9326): 	at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1127)
E/AndroidRuntime( 9326): 	at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:975)


-----------------

PS: Also tried catching an exception. Didn't work either.

Import mojo

Class Game Extends App

	Field image:Image
	
	Method OnCreate()		
		SetUpdateRate 30

		'image = LoadImage("960x600.png") ' works

		Try
			image = LoadImage("1920x1200.png")
		Catch ex:Throwable
        		image = Null
		End				
	End

	Method OnRender()
		If image
			Print "image is not null"

			Try			
				DrawImage(image, 0, 0)
			Catch ex:Throwable
				image = Null
			End							
		Else
			Print "image is null"
		Endif
	End
End

Function Main()
	New Game
End



AdamRedwoods(Posted 2013) [#10]
1920*1400*4 = 9mb
that is a big image and yes you may get heap problems. i would stick to 1024x1024.

the other option is to do the BIG-NO-NO and do this in the android 2.3+ manifest XML:
android:largeHeap="true"

http://stackoverflow.com/questions/11275650/how-to-increase-heap-size-of-an-android-application
or Pre 2.3:
http://stackoverflow.com/questions/7598835/alternative-of-vmruntime-getruntime-setminimumheapsize-in-gingerbread/17069070

is the Huawei U8500 on android 2.3? if not, you'll definitely get heap problems.


nori(Posted 2013) [#11]
:) Thanks, I'll try that. It's running android 2.1.


AdamRedwoods(Posted 2013) [#12]
It's running android 2.1.

yikes! expect problems and work-arounds for anything less than android 2.3.


nori(Posted 2013) [#13]
It's cool, this way I'm increasing my audience *g ... I'm glad I bought a 63 Euro Huawai phone *cough...


nori(Posted 2013) [#14]
I don't really know how to lower the "Project target" from 13 to 8, which is required because from 9 on android had removed VMRuntime which I need to set a bigger heap size.

Build.xmls everywhere, none containing a 13. Hm.

PS: Now I've put the Bytebuffer.allocate() calls in mojo into exception handlers, so it no more crashes on the phone.

But using the VMRuntime is really mysterical to me as the hole API level stuff. From 9 on it was removed from the android API, I set minimum sdk level 3, and target sdk level 7. So why does Monkey or ant make use of the API level 13 at all and fails to find the symbol "VMRuntime"? I've not understood anything of that android API levels at all.

This is where the 13 comes from, and I've no idea what line and why. I've installed level 7/8 too, it should not fail because of this.

        <gettarget
                androidJarFileOut="project.target.android.jar"
                androidAidlFileOut="project.target.framework.aidl"
                bootClassPathOut="project.target.class.path"
                targetApiOut="project.target.apilevel"
                minSdkVersionOut="project.minSdkVersion" />



SLotman(Posted 2013) [#15]
1920*1400*4 = 9mb

It is even worse: that will probably be turned into the nearest power of 2 in the hardware, so it will become 2048*2048*4 = 16mb!

Most Android 2.1 can barely deal with 1024x1024. You would be better off choosing a single resolution and using autofit.


nori(Posted 2013) [#16]
ok, so i can actually forget about trying to access VMRuntime to get a bigger heap on android 2.2 and below.

Well.. at least I got rid of the crash. The texture bind() will instead just return, the textures won't show. Opportunity for the user to hit the HD button a second time.

in case anyone could need it, change the 4 allocating lines in mojo.android.java to something like this:
ByteBuffer buf;
try { buf=ByteBuffer.allocate( sz*4 ); } catch(Throwable t) { return; }

and this one:
int[] pixels;
try { pixels = new int[sz]; } catch(Throwable t) { return; }


PS: just wanted to add this:
I found a way around the heap, the non-transparent images can be loaded via Android. So I've removed all the pixels[]-code for them and maybe introduced problems with padding or the boarder. But now my cheap phone can load my 4 1024x1024 textures for background, before it could not.

my texture loading in mojo now looks like this:
		...
		GLES11.glTexParameteri( GLES11.GL_TEXTURE_2D,GLES11.GL_TEXTURE_WRAP_S,GLES11.GL_CLAMP_TO_EDGE );
		GLES11.glTexParameteri( GLES11.GL_TEXTURE_2D,GLES11.GL_TEXTURE_WRAP_T,GLES11.GL_CLAMP_TO_EDGE );
		
		if(hasAlpha)
		{
			int pwidth=(width==twidth) ? width : width+1;
			int pheight=(height==theight) ? height : height+1;

			int sz=pwidth*pheight;
			int[] pixels;
			try { pixels = new int[sz]; } catch(Throwable t) { return; }
			bitmap.getPixels( pixels,0,pwidth,0,0,width,height );
		
			//pad edges for non pow-2 images - not sexy!
			if( width!=pwidth ){
				for( int y=0;y<height;++y ){
					pixels[y*pwidth+width]=pixels[y*pwidth+width-1];
				}
			}
			if( height!=pheight ){
			for( int x=0;x<width;++x ){
				pixels[height*pwidth+x]=pixels[height*pwidth+x-pwidth];
				}
			}
			if( width!=pwidth && height!=pheight ){
				pixels[height*pwidth+width]=pixels[height*pwidth+width-pwidth-1];
			}
		
			GLES11.glPixelStorei( GLES11.GL_UNPACK_ALIGNMENT,1 );
				

			//RGBA8888...
			ByteBuffer buf;
			try { buf=ByteBuffer.allocate( sz*4 ); } catch(Throwable t) { return; }
			buf.order( ByteOrder.BIG_ENDIAN );

			for( int i=0;i<sz;++i ){
				int p=pixels[i];
				int a=(p>>24) & 255;
				int r=((p>>16) & 255)*a/255;
				int g=((p>>8) & 255)*a/255;
				int b=(p & 255)*a/255;
				buf.putInt( (r<<24)|(g<<16)|(b<<8)|a );
			}
			buf.position( 0 );
			GLES11.glTexImage2D( GLES11.GL_TEXTURE_2D,0,GLES11.GL_RGBA,twidth,theight,0,GLES11.GL_RGBA,GLES11.GL_UNSIGNED_BYTE,null );
			GLES11.glTexSubImage2D( GLES11.GL_TEXTURE_2D,0,0,0,pwidth,pheight,GLES11.GL_RGBA,GLES11.GL_UNSIGNED_BYTE,buf );

		} else {
			GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
		}
		...