Advice. 3D Engine for BlitzMax

BlitzMax Forums/BlitzMax Programming/Advice. 3D Engine for BlitzMax

PhotonTom(Posted 2012) [#1]
Hi guys,
I'm making an application using blitzmax and it requires a 3D engine with the following features:
1. Able to have a separate thread to load textures
2. load textures very fast
3. Doesn't need to be able to do anything other than create cubes, transparency, load textures, 2d drawing and move cubes.

I've tested a few 3D engines I got my hands on.
Minib3d took a long time to load textures but other than that was quite good. Irrlicht is very fast (loaded some textures 10x faster than minib3d) but unable to multithread and requires me to learn the language.
I've seen my friend use blitz3d sdk and it looks good and quite fast (still not as fast as Irrlicht) but its not available for sale anymore :(
MaxB3d also seems good but I'm not sure it can be threaded.

Can any graphics engines be threaded?
Thanks for any answers, this is going to be a huge project so I don't want to start until I have an engine that will work well.


Kryzon(Posted 2012) [#2]
I don't think the rendering can be done on child threads, but the rest (changing states, loading resources) can.

Have you thought of working on a threaded texture loader for miniB3D? it shouldn't be that difficult, if you take your time to learn of the internals of the engine and how BMax's threads work; might be the cheapest path.


GNS(Posted 2012) [#3]
None of the available engines offer multithreaded texture loads, as far as I know.

It's been years since I looked at this (for OpenGL, unsure about D3D) so perhaps something has changed since then but threaded texture loads used to be very risky/tricky/convoluted. I seem to recall needing multiple contexts (one for the "main" program and another for the loader thread that would load the texture and pass it to the "main" context). There were also all sorts of driver issues (i.e. some would perform a "global" lock on both contexts thereby killing any performance benefit, some drivers would silently fail if you attempted to bind a texture in the "main" context before it had fully been loaded by the "loader" context while others would just bind a corrupted texture, etc.).


Noobody(Posted 2012) [#4]
Multithreading and graphics APIs don't go very well together. DirectX 11 offers utilities for that and OpenGL offers some multithreading capabilities through multiple contexts with shared resource lists, and while I admit to only have worked briefly with either of them (so I may be wrong), both methods seemed very cumbersome, especially when BMax inherently hides context creation from you to make life easier..
State changes from different threads (heck, even state changes from a different thread than the one who created the context) are considered unsafe and will likely lead to unexpected behaviour up to random crashes.

In your case though, you don't even need multithreaded graphics APIs - the expensive part of texture loading, which consists of file I/O and PNG decoding, is completely independent of the graphics API and can be done in a separate thread without any problems. After a texture is loaded, the worker thread just has to pass the pixmap (or your picture object of choice) to the main thread and have it pass the pixmap to the API. Simples!

Last edited 2012


ima747(Posted 2012) [#5]
Generally speaking graphics API access has to happen on the main thread. There are some exceptions but they're very technical, and beyond a reasonable scope for Bmax (as in, if you're really intent on this you're much better off with another language because you're going to have to do so much low level work anyway bmax is just going to slow you down).

That said, the *really* important thing to note was mentioned by Noobody: The expensive part of loading textures is file I/O and image decoding. This can absoloutely be multithreaded as it is all CPU based, and you simply have to pass the resulting pixmap into the graphics API on the main thread when you're done (this is not free of time commitment but with batching, by and large this is trivial). I do this with MiniB3d in one of my commercial projects. Images are loaded and decoded then added to a thread safe list (mutex managed). Each cycle the application checks for new graphics in the list, if it finds one it loads it into a texture, checks the time it took, and if it was fast enough tries again until either it runs out of it's time allotment or there's nothing left to load. If it runs out of time it will try again next pass. The result is you can budget the API dependent loading on the main thread, while the really heavy slow stuff all gets done on whatever thread you like, multiple threads if you like, what makes sense depends on the hardware config (how many cores, how fast are the discs, etc.) and your ultimate goal (screw the machine, load as fast as possible, or something more reasonable, or possibly something more flexible like map segment loading in google maps where the user might pass over an area before the map loads, so it can be skipped since it's no longer of relevance).


PhotonTom(Posted 2012) [#6]
@ima747
How did you get minib3d threaded? Have you got any example code?


ima747(Posted 2012) [#7]
Sory, no examples, all burried deep in business owned code. As outline dative though, loading images and making textures are 2 seperate things. Making a texture has to happen on the main thread, but you can load images anywhere. If it were simple everyone would do it :0) you need to understand the engine, mutlithreading, and what is safe where. There's no way to make a universal threaded texture loader, as how you work around the limitations of various components of the process is dependent on your needs...

Last edited 2012


PhotonTom(Posted 2012) [#8]
I've been looking into the code for minib3d and would I be correct in saying that anything todo with pixmaps can be stuck in a thread but open gl commands such glGenTextures, glBindTexture, glPixelStorei, and glTexImage2D have to stay in the main thread.
I timed it and the pixmap stuff takes about 800 millisecs and the gl stuff takes about 300 millisecs within the loadtexture command for a certain image. Is this the sort of improvement you saw with your loader putting the pixmaps into a separate thread?
And thanks ima747 for the help :)


ima747(Posted 2012) [#9]
Structurally yes, anything OpenGL relates to the Gpu and as a result you can only talk to it on the main thread. loading an image and decoding it to raw pixel data is all on the CPU and can be done on other threads as it's just generic processing. I load image files from disc, decode them, then hand the results to the main thread so it can be sent to th Gpu. I have specifically clocked it, but I did it for smoothness rather than explicitly speed. Since the loading happens on a background thread, the main thread can process and draw the scene (as much as is available) and allowing the user to interact etc. just as more stuff gets loaded it can be added, so there is no loading phase, the scene just starts and fills in as e resources are processed.


PhotonTom(Posted 2012) [#10]
I had a stab and came up with a solution. It works well, even large images only take 100-200 millisecs to load on the main thread. Here is the code:
'Add to sidesign.mod\minib3d.mod\inc\TTexture.bmx
Type TPixmapTexture Extends TTexture
	Global tex_pixlist:TList = CreateList()
	Field MipPixmaps:TPixmap[]
	
	Function LoadTexturePixmap:TPixmapTexture(file$ , flags:Int = 1 , tex:TPixmapTexture = null)
		If flags & 128 Then RuntimeError "Not Implimented"
		If tex = Null Then tex:TPixmapTexture = New TPixmapTexture
		
		If FileType(file$) = 0 Then Return Null
		
		tex.file$=file$
		tex.file_abs$=FileAbs$(file$)
		
		tex.flags=flags
		tex.FilterFlags()
		
		Local old_tex:TPixmapTexture
		old_tex=tex.TexInPixList()
		If old_tex<>Null And old_tex<>tex
			Return old_tex
		Else
			If old_tex<>tex
				ListAddLast(tex_pixlist,tex)
			EndIf
		EndIf		
		
		tex.pixmap=LoadPixmap(file$)

		Local alpha_present:Int=False
		If tex.pixmap.format=PF_RGBA8888 Or tex.pixmap.format=PF_BGRA8888 Or tex.pixmap.format=PF_A8 Then alpha_present=True

		' convert pixmap to appropriate format
		If tex.pixmap.format<>PF_RGBA8888
			tex.pixmap=tex.pixmap.Convert(PF_RGBA8888)
		EndIf
		
		' if alpha flag is true and pixmap doesn't contain alpha info, apply alpha based on color values
		If tex.flags&2 And alpha_present=False
			tex.pixmap=ApplyAlpha(tex.pixmap)
		EndIf		

		' if mask flag is true, mask pixmap
		If tex.flags&4
			tex.pixmap=MaskPixmap(tex.pixmap,0,0,0)
		EndIf
	
		tex.pixmap = AdjustPixmap(tex.pixmap)
		tex.width = tex.pixmap.width
		tex.height = tex.pixmap.height
		Local width:Int = tex.pixmap.width
		Local height:Int = tex.pixmap.height
		
		Local MipPixmaps:TList = CreateList()
		Local MipParams:TList = CreateList()
		
		Local Pixmap:TPixmap = tex.pixmap
		ListAddLast(MipPixmaps , Pixmap)
		
		If tex.flags & 8 Then
			Repeat
				If width=1 And height=1 Exit
				If width>1 width:/2
				If height>1 height:/2
				
				Pixmap = ResizePixmap(tex.pixmap,width,height)
				ListAddLast(MipPixmaps , Pixmap)
			Forever
		EndIf

		tex.MipPixmaps = TPixmap[](ListToArray(MipPixmaps) )
		
		Return tex			
	End function	

	Method TexInPixList:TTexture()

		' check if tex already exists in list and if so return it

		For Local tex:TPixmapTexture=EachIn tex_pixlist
			If file$=tex.file$ And flags=tex.flags And blend=tex.blend
				If u_scale#=tex.u_scale# And v_scale#=tex.v_scale# And u_pos#=tex.u_pos# And v_pos#=tex.v_pos# And angle#=tex.angle#
					Return tex
				EndIf
			EndIf
		Next
	
		Return Null
	
	End Method
End Type


'Add to TTexture type within sidesign.mod\minib3d.mod\inc\TTexture.bmx
	Function LoadTextureFromPixmap:TTexture(texPixmap:TPixmapTexture)
		If texPixmap = Null Then Return Null
		
		Local tex:TTexture = TTexture(texPixmap)
		
		Local old_tex:TTexture
		old_tex=tex.TexInList()
		If old_tex<>Null And old_tex<>tex
			Return old_tex
		Else
			If old_tex<>tex
				ListAddLast(tex_list,tex)
			EndIf
		EndIf
		
		Local pixmap:TPixmap
		
		Local width:Int=tex.width
		Local height:Int=tex.height

		Local name:Int
		glGenTextures 1,Varptr name
		glBindTexture GL_TEXTURE_2D,name

		Local mipmap:Int
		If tex.flags&8 Then mipmap=True
		Local mip_level:Int=0
		Repeat
			pixmap=texPixmap.MipPixmaps[mip_level]
			glPixelStorei GL_UNPACK_ROW_LENGTH,Pixmap.pitch/BytesPerPixel[Pixmap.format]
			glTexImage2D GL_TEXTURE_2D,mip_level,GL_RGBA8,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,pixmap.pixels
			If Not mipmap Then Exit
			If width=1 And height=1 Exit
			If width>1 width:/2
			If height>1 height:/2

			
			mip_level:+1
		Forever
		tex.no_mipmaps=mip_level

		tex.gltex[0]=name
		Return tex
		
	End Function



'Add to sidesign.mod\minib3d.mod\inc\functions.bmx
Function LoadTexturePixmap:TPixmapTexture(file$ , flags:Int = 1)
	Return TPixmapTexture.LoadTexturePixmap(file$,flags)
End Function

Function LoadTextureFromPixmap:TTexture(file:TPixmapTexture)
	Return TTexture.LoadTextureFromPixmap(file)
End function


Example usage Code:

Framework BRL.FileSystem
Import BRL.StandardIO
Import sidesign.minib3d
Import BRL.Threads

Global TexReady = false
Global texpix:TPixmapTexture
Global filetoload:String = "Put your texture file here!"

Function GetTextureThread:Object(in:Object)
	Print "Started Texture Thread"
	Delay 3000
	Print "Loading Texture"

	StartTime = MilliSecs()
	texpix = LoadTexturePixmap(filetoload)
	EndTime=MilliSecs()
	Print "Total pixmap load time:" +String(EndTime - StartTime)
	

	TexReady = True
	Print "Child Thread Completed"
	Repeat
		Delay 100
	Forever
End Function


width=640;height=480;depth=16;mode=0

Graphics3D width,height,depth,mode


AmbientLight 0,0,0

Const grav#=-.02,intensity=5

Type Frag
	Field ys#,alpha#,entity
End Type

fraglist:TList=CreateList()

pivot=CreatePivot()

camera=CreateCamera(pivot)
CameraClsMode camera,False,True

cube = CreateCube()

ScaleEntity cube , 1 , 1 , 1
PositionEntity camera, 0,0,-5
EntityFX cube,1




time=MilliSecs()

' used by fps code
Local old_ms=MilliSecs()
Local renders
Local fps

Local TexThread:TThread = CreateThread(GetTextureThread, Null)
MoveMouse 0,0
While Not KeyDown(KEY_ESCAPE)
	cls
	TurnEntity(cube , 0 , 1 , 0) 
	If TexReady = True
		Print "Setting Texture"
		StartT = MilliSecs()	
		tex:TTexture = LoadTextureFromPixmap:TTexture(texpix)
		EntityTexture cube , tex
		EndT = MilliSecs()
		Print "Set Texture Took: "+String(EndT - StartT)
		TexReady = false
	EndIf
	
	Repeat
		elapsed=MilliSecs()-time
	Until elapsed>0
	
	RenderWorld
	renders=renders+1

	' calculate fps
	If MilliSecs()-old_ms>=1000
		old_ms=MilliSecs()
		fps=renders
		renders=0
	EndIf
	
	Text 0,0,"FPS: "+String(fps)

	Flip
Wend

End


Note my coding isn't great so there is probably a lot of faults with this including the fact no mutexs are used, but it should work as is.


PhotonTom(Posted 2012) [#11]
I'm getting some problems with my program locking up but its a random error so I can't really debug it. Just wondering are the built in functions in Blitz safe to use in threads? For example can I use loadpixmap or millisecs() from two threads at once or is this what is causing the lock up?


Derron(Posted 2012) [#12]
as long as the pointers/variables you are things assigning to are mutex-locked it should not bring up errors.

Loadpixmap is reading and threadsafe (its like decoding images to pixelarrays)
millisecs() is reading not writing.


bye
Ron

ps: looking through your code i did not see "lockmutex".
You have to "lock" the access to a shared variable (eg. a "list of already loaded pixmaps").


PhotonTom(Posted 2012) [#13]
Yeah I know about the lockmutex's missing but i'm developing commercial code so I didn't want to post the code I'm using which does have mutex's in.
What would cause a program to dead lock then? Writing and reading a variable at the same time? Writing to a variable from two threads at the same time?
And I understand from above post that reading a variable from two threads at same time is safe. Correct?
I understand Mutex's but I'm trying to modify a graphics engine and I don't want to go mental mutexing everything if its not required.

Thanks again for any help


Gabriel(Posted 2012) [#14]
And I understand from above post that reading a variable from two threads at same time is safe. Correct?

It depends on the platform and the data type. If memory serves, reading integers is an atomic operation on all of the platforms BlitzMax supports. There are exceptions to that, but I believe they don't apply on modern hardware. *

For non-integer datatypes, it gets even more complex. In short, unless you know all of the ins and outs of CPU architecture, mutex everything which can be accessed in different threads.



* EDIT: Actually, if you tried to read an integer directly from memory using a non-aligned address, that wouldn't be safe.


col(Posted 2012) [#15]
Hiya,

Multi-thread engines are built with mt at the core, including all loading and access functions. To bolt mt onto a non mt engine could prove to be a bit of a nightmare.

However, you could try some lock-free functions I wrote a while ago, may not be what you need though.

For accessing integers you could use the standard blitz atomic functions - AtomicSwap or CompareAndSwap, the lock-free functions above use a slightly modified version.