Threading - how to do this?

BlitzMax Forums/BlitzMax Programming/Threading - how to do this?

GfK(Posted 2009) [#1]
When my game sections are loading, the mouse pointer freezes and I don't want it to, so I figured there are two ways of solving the problem using threading.

1. Load all my game assets in a separate thread, and let the main loop draw the mouse pointer until the thread is done loading.

2. Load the assets in the main thread (like they are now), and have a small 'draw' loop in a thread to keep redrawing the mouse pointer. Close the thread when all the assets have been loaded.

I'm leaning slightly towards option #2 for simplicities sake but I heard somebody mention, somewhere.... that you should do any drawing stuff in threads. Why not?

Thoughts?


Mark Tiffany(Posted 2009) [#2]
I heard somebody mention, somewhere.... that you should do any drawing stuff in threads. Why not?

I believe graphics contexts are not thread-safe / shared across threads in all instances. You might find #2 works for you, but might not work for all.

I'd go with #1, and have the thread raise an event (of your own devising) when the media is ready, then your main loop can waitevent on this / timerticks. (I reckon it ought to be possible to add a LoadImageAsync and EVENT_IMAGELOADED to Max2d that does just this).

Also by going with #1, if you are doing multiple things on load (pre-calcing data / unpacking data to memory / loading from multiple different sources e.g. incbin / file /web) then you can expand this by creating separate threads for each area where it would make sense (although no real point in doing 10 threads for loading e.g 10 files from hard disk).


ziggy(Posted 2009) [#3]
The easiest way is to use a single unsafe semaphore.

Pseudocode should look like this:

Global Loading:int = False
Function LoadMedia()
   Loading = True
   RunThread2
   While Loading
       DrawThings
       Flip
       Delay(1)
   Wend
End Function

Function RunThread2
    LoadEverything
    Loading = False
End Function

If you read a wrong value in the Loading var, on the main thread, it means it has been loaded anyway, so it works ok as long as you don't do anything else with the Loading var, and you're not trying to return a value from the Tread2 to be read on the Thread1.


GfK(Posted 2009) [#4]
Good tip. Thx!


ziggy(Posted 2009) [#5]
also, as the graphics context are not thread safe, I think you better load images as pixmaps and convert them to regular images on the main thread, and don't try to draw them while they're being loaded! Anyway, converting a pixmap to an image is way faster than loading an image from disk so, unless you're doing something very complicated, it shouldn't be a problem.


plash(Posted 2009) [#6]
When you load an image does it immediately put it into vram? I thought that was done automatically, when needed.


ziggy(Posted 2009) [#7]
@Plash: I'm not sure, maybe it's right that the images are just sent to the vram when needed by the rendering engine. I supose a look to the modules should make it clear. Has anybody took a look at it?


plash(Posted 2009) [#8]
It would seem that it does not do magic, for OpenGL at least.


ziggy(Posted 2009) [#9]
I've done a small silly test:
Strict
Global Image1:TImage
Global Image2:TImage
Global Image3:TImage
Global Loading:Int = False
SetGraphicsDriver(GLMax2DDriver())
Graphics(800, 600)
SetBlend ALPHABLEND
Load()
While Not KeyHit(KEY_ESCAPE)
	Cls
	DrawImage(Image1, 0, 0)
	DrawImage(Image2, 200, 0)
	DrawImage(Image3, 300, 300)
	Flip
WEnd

Function Load()
	Loading = True
	Local T:TThread = CreateThread(LoadData, Null)
	While Loading
		Cls
		DrawText("loading " + millisecs(), 0, 0)
		Flip
		Delay(1)
	Wend
End Function
Function LoadData:Object(dat:Object)
	Image1 = LoadImage("InstallIcon.png")
	Delay(500)    'If it's too fast, where's the fun?
	Image2 = LoadImage("InstallIcon.png")
	Delay(500)    'If it's too fast, where's the fun?
	Image3 = LoadImage("InstallIcon.png")
	Delay(500)    'If it's too fast, where's the fun?
	Loading = False
End Function

and it seems there's no problem loading directly images in OpenGL and DirectX. So, it seems the images are sent to vram when drawn for the first time! Yay!

EDIT: put a file called installicon.png or whatever in the same folder as the sample!


BlitzSupport(Posted 2009) [#10]
You should find another example of threaded background image loading in the BlitzMax samples, under Threads (assuming latest version).


and it seems there's no problem loading directly images in OpenGL and DirectX.


I'm a little surprised by this, yet that certainly works here. Maybe it's because they're not being drawn until the thread is finished? I thought they would have to be loaded in the main thread, though, since my understanding is (or was!) that the DirectX/OpenGL 'context' that they belong to is only accessible to the main thread.


ziggy(Posted 2009) [#11]
I think the problem on DX is on concurrent draw operations, while on OpenGL is the complete context. Now sure when the images are being stored on the vram, given the fact that Max seems to survive greaphicsend and graphics to switch full-screen and windowed mode, I "think" images are stored also as pixmas and sent to vram when needed?


GfK(Posted 2009) [#12]
I'm a little surprised by this, yet that certainly works here. Maybe it's because they're not being drawn until the thread is finished? I thought they would have to be loaded in the main thread, though, since my understanding is (or was!) that the DirectX/OpenGL 'context' that they belong to is only accessible to the main thread.
I read somewhere that LoadImage does some jiggery-pokery with Pixmaps behind the scenes anyway.

Also, its possible to change from DirectX to OpenGL mid-application without losing any graphics. So there's quite clearly something very clever - or possibly witchcraft - going on that none of us understand.

[edit] ...or to put it another way - what Ziggy said.


DStastny(Posted 2009) [#13]
Since I wrote the Direct X 9 Driver I am pretty familiar with the internals of TImage. The LoadImage command loads a pixmap. When its time to draw them the BMAX2d driver requests and TImageFrame from the Image it is at that time that the images are loaded into VRAM. This is also why TImages survive driver switches.

So you should be ok loading the Images I would say its probably safer to load Pixmaps in the background thread and use LoadImage after the thread is complete and not rely on the behavior of TImageFrame and on demand loading.

I typically after loading all my media draw it once before going into the main loop so there are no stutters as the images get loading into VRAM for the first time.

Doug Stastny


plash(Posted 2009) [#14]
Since I wrote the Direct X 9 Driver I am pretty familiar with the internals of TImage. The LoadImage command loads a pixmap. When its time to draw them the BMAX2d driver requests and TImageFrame from the Image it is at that time that the images are loaded into VRAM. This is also why TImages survive driver switches.
Precisely what I thought!


GfK(Posted 2009) [#15]
Hmm.... that seems like an awful lot of farting about.

The very idea of loading media in a thread, is so that I can have an animated mouse pointer, loading screen or some such while all the assets are loaded. Is there really any benefit to doing this if I have to convert it all to TImages from Pixmaps, from the safety of the main thread anyway?

And how about audio stuff? Is that thread-safe?

[edit] For what its worth, I just tested Ziggy's example on two PCs:

1. Athlon64 X2 5000+, 2GB RAM, 512MB Geforce 8500GT, Vista Home Premium.
2. P3-733MHz, 256MB RAM, 64MB GeForce2 MX400, Windows XP.

Works perfectly on both systems - even the cruddy old pentium.

I've just posted this thread to get some more general feedback on this.


BlitzSupport(Posted 2009) [#16]

Is there really any benefit to doing this if I have to convert it all to TImages from Pixmaps, from the safety of the main thread anyway?


Yep, try the example I mentioned. Loading images from pixmaps is pretty much instantaneous -- it's only the loading from disk that's relatively slow. In my example, the pixmaps load in the background while allowing animation to take place on-screen. (The example converts several pixmaps to images at once without any trouble here, but you could just convert each one as it's loaded if you preferred.)

That said, it sounds like LoadImage is fine. I'd just be slightly wary of the possibility that Mark might have to change things due to some unexpected threading issue later on. If you wrap the loading process up a little bit, though, you can at least make sure you would only have to change a small amount of code if that became necessary.


GfK(Posted 2009) [#17]
Yep, try the example I mentioned. Loading images from pixmaps is pretty much instantaneous
Ah. I really must learn to read. I did look for it earlier but I went in the hitoro folder. Didn't find anything. ;)


Beaker(Posted 2009) [#18]
If you wanted to play real safe you could always load the image into a bankStream and then LoadImage(myBankStream) when needed. I'm guessing this would also work for other media (sounds etc).


ziggy(Posted 2009) [#19]
I think you'll love this one:
Strict
Graphics 800, 600

'We create an MTImageLoader:
Local Loader:MTLoader = New MTLoader

'We feed this loader with URLs to load images from. This is done using the 
'ImageLoader object:
For Local i:Int = 0 To 5	'We will load 6 times the same image, for testing only.
	Local IL:ImageLoader = New ImageLoader
	IL.Origin = "C:\Users\Manel\Pictures\camafeos-baja.jpg" REPLACE WITH YOUR OWN FILE
	Loader.AddImageLoader("Image" + i, IL)	'Arguments are NAME, IMAGELOADER
Next

'Now the MTLoad has a list of the images we want to load in the background,
'we call the LoadImages method:
Loader.LoadImages()

'Now, the ImageLoader IS working in the background, so we check for it to complete:
While Loader.ImagesLoading() = True
	Cls
	DrawText("Loading!", 0, 0)
	DrawRect(Rand(0, 800), Rand(0, 600), 10, 10)	'Dummy draw to show activity
	Delay(10)
	Flip
Wend

'Now images are loaded, we want to get an image and set it to a regular TImage object:
Local IL:ImageLoader = Loader.GetImageLoaderbyName("Image0")
Local Image:TImage = IL.Image

'That's it! We show the image on screen:
While Not KeyHit(KEY_ESCAPE)
	Cls
	DrawImage(Image, 50, 50)
	Flip
Wend

End

'---------------------------------------------------------------------------------------

Type MTLoader
	Field _Loading:Int = False
	Field _Images:TMap = New TMap
	Method LoadImages()
		If brl.threads.CurrentThread() <> brl.threads.MainThread() Then
			Throw "Image loader has to be called from Main Thread."
		End If
		If _Loading = True Then
			Throw "Already loading images"
		End If
		_Loading = True
		Local T:TThread = New TThread
		T.Create (Self._ProcessDelegate, Self)
	End Method
	Function _ProcessDelegate:Object(Val:Object)
		Local MTL:MTLoader = MTLoader(Val)
		MTL._Process()
	End Function
	Method _Process()
		For Local IL:ImageLoader = EachIn _Images.Values()
			Print "loading " + IL.Origin
			IL.Image = LoadImage(IL.Origin)
		Next
		Self._Loading = False
	End Method
	Method ImagesLoading:Int()
		Return _Loading
	End Method
	Method AddImageLoader(Name:String, IL:ImageLoader)
		If _Loading Then
			Throw "Can't add images to the loader while loading is being performed!"
		End If
		If _images.Contains(Name.ToLower()) = False Then
			_images.Insert(Name.ToLower(), IL)
		Else
			Throw "Duplicate ImageLoader name!"
		EndIf
	End Method
	Method GetImageLoaderByName:ImageLoader (Name:String)
		Local IL:ImageLoader = ImageLoader(_Images.ValueForKey(Name.ToLower()))
		Return IL
	End Method
End Type

Type ImageLoader
	Field Origin:String
	Field Image:TImage
	Method Create(url:String)
		Self.Origin = url
	End Method
End Type

Every MTLoader object creates an async image loader thread that can be controlled from main thread. You assing a list of images to the MTLoader object, with a key for each image for later referencing, and then call the LoadImages() method (this method will create a scondary lading thread in the background) You will be able to know when all the images in the object have been loaded by calling the method ImagesLoaded(). when ImagesLoaded() return TRUE, you can get a loaded image by its internal key name, and assign it to a regular TImage object.
this also manages the pixmap need (in fact no-need) and everything discussed here.
Hope you'll find it usefull! This is just a small sample, take into account you could have several MTLoader objects to take proffit of several cores on a machine, so images could be loaded using 2 or 3 MTLoader objects at the same time, as long as you check for Loader1.IsLoading() or Loader2.IsLoading() or Lader3.IsLoading(), etc...

EDIT: Note that the NAME paramter on AddImageLoader and GetImageLoader is case-insensitive, so Image01 is the same as imAGE01.


BlitzSupport(Posted 2009) [#20]

If you wanted to play real safe you could always load the image into a bankStream and then LoadImage(myBankStream) when needed. I'm guessing this would also work for other media (sounds etc).


That's a pretty good idea, actually. With Pixmaps it's probably much the same (since they're just blocks of memory), but you could make a much more generic threaded media loader in the style of Ziggy's doing it with bank streams. (You'd just call LoadImage, LoadSound, etc, on each returned bank stream depending on a flag you set during the load request.)

This is a simple modification of the image downloading example found in 'samples\threads' (which loads one image at a time, drawing all images as they're loaded) to load local images instead (just makes the demo less flashy). Again, it should be easy to modify to use bank streams for loading all sorts of media.

For added simplicity, I'm pretty sure it'd be safe to just pass LoadPixmap directly to CreateThread, ie. CreateThread (LoadPixmap, Pic [index]). It certainly works, and LoadPixmap doesn't seem to depend on anything global, but wrapping it like this at least lets you fiddle with the pixmap/data before it's loaded/returned (as well as letting me add a delay for demo purposes)...



Note that most of the above is demo-specific -- the main section of interest is highlighted below (ie. the loading screen loop). If you take the comments out, you'll see how simple it really is...



Anyway, plenty of options.


ziggy(Posted 2009) [#21]
stupid post. forget it.