Async image loading

Monkey Forums/Monkey Programming/Async image loading

Anatol(Posted 2012) [#1]
Hi all,

I'm just trying to find a way to replace normal image loading with async loading. It works fine with Mark's sample on http://marksibly.blogspot.co.nz/2012/09/ok-this-got-bit-longwinded-so-its_14.html , but the problem there is that an image field also needs to be in the OnLoadImageComplete method. That's a bit inconvenient. So I'm trying to replace the usual LoadImage with some async workaround.

I tried the following:
[monkeycode]
Strict

Import mojo

Function Main:Int()
New AsyncTestApp
Return 0
End

Class AsyncTestApp Extends App Implements IOnLoadImageComplete

Field testImage:Image
Field asyncImages:StringMap<Image>

Method OnCreate:Int()
SetUpdateRate(60)
asyncImages = New StringMap<Image>

testImage = LoadImage("myimage.png")
Return 0
End

Method LoadImage:Image(path$, frames%=1, flags%=Image.DefaultFlags)
Local image:Image = null
asyncImages.Add(path, image)
LoadImageAsync(path, frames, flags, Self)
Return image
End

Method OnLoadImageComplete:Void(image:Image, path$, source:IAsyncEventSource)
If image Then asyncImages.Set(path, image) ' works but doesn't change testImage
'If image Then asyncImages.Get(path) = image ' error message: cannot convert from image to string
End

Method OnUpdate:Int()
UpdateAsyncEvents()
Return 0
End

Method OnRender:Int()
Cls()
' works
If asyncImages.Get("myimage.png") Then DrawImage(asyncImages.Get("myimage.png"), 0, 0)

' doesn't work
If testImage Then DrawImage(testImage, 100, 0)
Return 0
End

End
[/monkeycode]

So basically I store async images in a StringMap and I don't need to worry about including individual images into OnLoadImageComplete.

I would, however, much prefer to be able to use the testImage Field, e.g.
[monkeycode]
DrawImage(testImage, 0, 0)
[/monkeycode]
than
[monkeycode]
DrawImage(asyncImages.Get("myimage.png")) ' this works
[/monkeycode]

So I think something in my OnLoadImageComplete method should be different to achieve this, but I just don't find a solution. I'm not very surprised that my code doesn't update testImage when the image is loaded, but any other method I tried failed completely.

I would appreciate any help. Maybe I'm completely on the wrong track.

Thanks!
Anatol


Anatol(Posted 2012) [#2]
OK, this is not really what I was looking for, but I'm loading async resources like this for now, unless I find a better way.

[monkeycode]
Strict

Import mojo

Function Main:Int()
New AsyncTestApp
Return 0
End

Class AsyncTestApp Extends App Implements IOnLoadImageComplete, IOnLoadSoundComplete

Field asyncImages:StringMap<Image>
Field asyncSounds:StringMap<Sound>

Method OnCreate:Int()
SetUpdateRate(60)
asyncImages = New StringMap<Image>
asyncSounds = New StringMap<Sound>

LoadImageAsync("myimage.png")
LoadSoundAsync("mysound.ogg")
Return 0
End

Method AllAsyncImagesLoaded:Bool()
For Local image:= Eachin asyncImages.Values()
If Not image Then Return False
Next
Return True
End

Method AllAsyncSoundsLoaded:Bool()
For Local sound:= Eachin asyncSounds.Values()
If Not sound Then Return False
Next
Return True
End

Method AllAsyncResourcesLoaded:Bool()
Return AllAsyncImagesLoaded() And AllAsyncSoundsLoaded()
End

Method LoadImageAsync:Void(path$, frames%=1, flags%=Image.DefaultFlags)
If asyncImages.Contains(path) Then Return ' the image is already in the LoadImageAsync map
asyncImages.Add(path, null)
asyncloaders.LoadImageAsync(path, frames, flags, Self)
End

Method LoadSoundAsync:Void(path$)
If asyncSounds.Contains(path) Then Return ' the sound is already in the LoadSoundAsync map
asyncSounds.Add(path, Null)
asyncloaders.LoadSoundAsync(path, Self)
End

Method OnLoadImageComplete:Void(image:Image, path$, source:IAsyncEventSource)
If image Then asyncImages.Set(path, image)
End

Method OnLoadSoundComplete:Void(sound:Sound, path$, source:IAsyncEventSource)
If sound Then asyncSounds.Set(path, sound)
End

Method DrawAsyncImage:Void(path$, x#, y#)
If asyncImages.Get(path) Then DrawImage(asyncImages.Get(path), x, y)
End

Method PlayAsyncSound:Void(path$, channel%=0, flags%=0)
If asyncSounds.Get(path) Then PlaySound(asyncSounds.Get(path), channel, flags)
End

Method OnUpdate:Int()
UpdateAsyncEvents()
If AllAsyncResourcesLoaded()
If TouchDown() Then PlayAsyncSound("mysound.ogg")
EndIf
Return 0
End

Method OnRender:Int()
Cls()
DrawAsyncImage("myimage.png", 0, 0)
Return 0
End

End
[/monkeycode]

Ideally I wanted to get some code that's somewhat "backwards compatible" to the usual DrawImage with an image field so that I don't need to go through all my code and change it.

But this also works well and saves me to declare Fields for every image and sound as these get simply added to the StringMaps asyncImages and asyncSounds. A positive side effect is that any image/sound that's already in these maps won't get loaded twice.

This is just some quick code (but it works) and may well need some refinement, e.g. a method that returns True at the moment that all resources have completed loading - that would be helpful to start playing any background music or to start rendering a scene only when all resources are available.

If anyone comes up with a way to use Fields as with non-async images please do post it.


Anatol(Posted 2012) [#3]
And another post on this subject. The code below is also not perfect but seems to work well without the need for a StringMap. It has the OnLoadImageComplete() method in an AsyncImage Class.

The sample code below loads a "normal" image and one image that's loaded asynchronously with LoadImage(), DrawImage() and LoadAsyncImage(), DrawAsyncImage() respectively.

[monkeycode]
Strict

Import mojo

Function Main:Int()
New AsyncTestApp
Return 0
End

Class AsyncTestApp Extends App

Field normalImage:Image
Field asyncImage:AsyncImage

Method OnCreate:Int()
SetUpdateRate(60)
normalImage = LoadImage("myimage.png")
asyncImage = LoadAsyncImage("myasyncimage.png")
Return 0
End

Method OnUpdate:Int()
UpdateAsyncEvents()
Return 0
End

Method OnRender:Int()
Cls()
DrawImage(normalImage, 0, 0)
DrawAsyncImage(asyncImage, 100, 0)
'asyncImage.Draw(100, 0) ' alternative to the line above
Return 0
End

End

Class AsyncImage Implements IOnLoadImageComplete

Private

Field image:Image


Public

Method Initialize:AsyncImage(path$, nframes%, iflags%)
LoadImageAsync(path, nframes, iflags, Self)
Return Self
End

Method OnLoadImageComplete:Void(_image:Image, path$, source:IAsyncEventSource)
image = _image
End

Method Draw:Void(x#, y#, frame%=0)
If image And image.Loaded() Then graphics.DrawImage(image, x, y, frame)
End

Method Draw:Void(x#, y#, rotation#, scaleX#, scaleY#, frame%=0)
If image And image.Loaded() Then graphics.DrawImage(image, x, y, rotation, scaleX, scaleY, frame)
End

' duplicating methods from Image class below (this needs to be updated if the Image class changes in a future release)

Method Width:Int()
Return image.Width()
End

Method Height:Int()
Return image.Height()
End

Method Loaded:Int()
Return image.Loaded()
End

Method Frames:Int()
Return image.Frames()
End

Method Flags:Int()
Return image.Flags()
End

Method HandleX:Float()
Return image.HandleX()
End

Method HandleY:Float()
Return image.HandleY()
End

Method GrabImage:Image(x%, y%, width%, height%, frames%=1, flags%=DefaultFlags)
Return image.GrabImage(x, y, width, height, frames, flags)
End

Method SetHandle:Int(tx#, ty#)
image.SetHandle(tx, ty)
Return 0
End

Method Discard:Void()
image.Discard()
End

Method WritePixels:Void(pixels%[], x#, y#, width#, height#, offset#=0, pitch#=0)
image.WritePixels(pixels, x, y, width, height, offset, pitch)
End

End

Function LoadAsyncImage:AsyncImage(path$, frameCount%=1, flags%=Image.DefaultFlags)
If path = "" Then Return null
Return (New AsyncImage).Initialize(path, frameCount, flags)
End

Function DrawAsyncImage:Void(asyncImage:AsyncImage, x#, y#, frame%=0)
If asyncImage Then asyncImage.Draw(x, y, frame)
End

Function DrawAsyncImage:Void(asyncImage:AsyncImage, x#, y#, rotation#, scaleX#, scaleY#, frame%=0)
If asyncImage Then asyncImage.Draw(x, y, rotation, scaleX, scaleY, frame)
End
[/monkeycode]

I would much prefer to have the AsyncImage class extend Image,

[monkeycode]
Class AsyncImage Extends Image Implements IOnLoadImageComplete
[/monkeycode]

which would probably allow to use the regular DrawImage() function with AsyncImage. However, I didn't find a way to do that because there are too many private Fields in Image class that I would need in AsyncImage.OnLoadImageComplete(), and I can't just do something like

[monkeycode]
Method OnLoadImageComplete:Void(_image:Image, path$, source:IAsyncEventSource)
Self = (AsyncImage)_image ' bad idea
End
[/monkeycode]

Anyway, any other input here would be great! Maybe I'm completely overlooking another and much easier solution.


AdamRedwoods(Posted 2012) [#4]
there are several ways to go about this.

this is the easiest way:
[monkeycode]
Class AsyncImage Implements IOnLoadImageComplete

Field image:Image
Field loaded:Bool = false

Method OnLoadImageComplete(_image:Image, path$, source:IAsyncEventSource)
image = _image
loaded = true
End
End

'' ...etc...
Field puppy:AsyncImage = New AsyncImage

Method OnRender()
'' you can check each image, or use a global ALL_IMAGES_LOADED
If Not puppy.loaded Then Return

DrawImage puppy.image,x,y
End
[/monkeycode]

I know what you're going after, but ideally you'd Extend Image and assign surface to the new surface-- but since it's private we can't do that.

Optimally, I'd create a resource manager and keep the Image Fields in that (ie. DrawImage MyImages.puppy) so when the file is loaded, it is assigned. Similar to what you have above.


Anatol(Posted 2012) [#5]
Thanks for the reply, Adam. Yes, the private surface Field is the main problem to extend the Image class.

Another "convenience option" with the AsyncImage class as above is to add a few DrawImage methods to the App class, that would then be called instead of the DrawImage function:
[monkeycode]
Class MyApp Extends App
'Method OnCreate:Int(), etc.

Method DrawImage:Void(image:Image, x#, y#, frame%=0)
graphics.DrawImage(image, x, y, frame)
End

Method DrawImage:Void(image:Image, x#, y#, rotation#, scaleX#, scaleY#, frame%=0)
graphics.DrawImage(image, x, y, rotation, scaleX, scaleY, frame)
End

Method DrawImage:Void(asyncImage:AsyncImage, x#, y#, frame%=0)
If asyncImage.image And asyncImage.image.Loaded() Then graphics.DrawImage(asyncImage.image, x, y, frame)
End

Method DrawImage:Void(asyncImage:AsyncImage, x#, y#, rotation#, scaleX#, scaleY#, frame%=0)
If asyncImage.image And asyncImage.image.Loaded() Then graphics.DrawImage(asyncImage.image, x, y, rotation, scaleX, scaleY, frame)
End
End
[/monkeycode]

But again, these are just workarounds for a "proper" extension of the Image class.

I'm using a resources manager which is quite important for my project, I'm just trying to keep the forum posts focused on the problem.
Cheers!

(Ah! I just discovered the forum tag "monkeycode" in square brackets!)