Bitmap and BitmapList classes

Post your FreeBASIC source, examples, tips and tricks here. Please don’t post code without including an explanation.
Post Reply
paul doe
Moderator
Posts: 1733
Joined: Jul 25, 2017 17:22
Location: Argentina

Bitmap and BitmapList classes

Post by paul doe »

Being mortally bored and rather depressed, I decided to post a simple convenient trick for handling bitmaps. Very useful for games and/or other things that need animation, or simply for convenience. The Bitmap class supports only .bmp and .tga files, which are easy to load and support alpha channel, but of course one can add any functionality to it (using a library to load images, for example).

Two simple classes are needed for the little demo:

fb-bitmap.bi:

Code: Select all

#include once "fbgfx.bi"

namespace gfx
	'' The TGA file format header
	type TGAHeader field = 1
		as ubyte idlength
		as ubyte colormaptype
		as ubyte datatypecode
		as short colormaporigin
		as short colormaplength
		as ubyte colormapdepth
		as short x_origin
		as short y_origin
		as short width
		as short height
		as ubyte bitsperpixel
		as ubyte imagedescriptor
	end type
		
	type Bitmap extends Object
		public:
			declare constructor( byval as integer, byval as integer )
			declare constructor( byref rhs as fb.image ptr )
			declare constructor( byref rhs as Bitmap )
			
			declare destructor()
	
			declare operator let( byref rhs as fb.image ptr )
			declare operator let( byref rhs as Bitmap )
			
			declare operator cast() as fb.image ptr
			
			declare property width() as integer
			declare property height() as integer
			
			declare static function fromBMP( byref as const string ) as fb.image ptr
			declare static function fromTGA( byref as const string ) as fb.image ptr
			
			declare function clone() as fb.image ptr
			declare sub dispose()
			
		private:
			declare constructor()
			
			m_bitmap		as fb.image ptr
	end type
	
	constructor Bitmap( byval w as integer, byval h as integer ) export
		m_bitmap = imageCreate( w, h, rgba( 0, 0, 0, 0 ), 32 )
	end constructor
	
	constructor Bitmap( byref rhs as fb.image ptr ) export
		dispose()
			
		m_bitmap = rhs
	end constructor
	
	constructor Bitmap( byref rhs as Bitmap ) export
		dispose()
		
		m_bitmap = rhs.clone()
	end constructor
	
	operator Bitmap.let( byref rhs as fb.image ptr ) export
		dispose()
			
		m_bitmap = rhs
	end operator
	
	operator Bitmap.let( byref rhs as Bitmap ) export
		dispose()
		
		m_bitmap = rhs.clone()
	end operator
	
	operator Bitmap.cast() as fb.image ptr export
		return( m_bitmap )
	end operator
	
	destructor Bitmap() export
		dispose()
	end destructor
	
	sub Bitmap.dispose() export
		if( m_bitmap <> 0 ) then
			imageDestroy( m_bitmap )
			m_bitmap = 0
			'' For testing purposes. Remove this when/if you use the code
			? "Bitmap destroyed"
		end if	
	end sub
	
	property Bitmap.width() as integer export
		return( m_bitmap->width )
	end property
	
	property Bitmap.height() as integer export
		return( m_bitmap->height )
	end property
	
	function Bitmap.fromBMP( byref fileName as const string ) as fb.image ptr
		/'					
			Loads a BMP file
			
			Be careful here, as passing whatever crap to the function will likely
			result in an invalid buffer and/or crashes. There's not much on the
			error handling side as this is a simple example
		'/
		dim as any ptr imgData = 0
		dim as integer fileNum = freeFile()
		dim as long w, h
		
		' Open file
		dim as integer result = open( fileName, for binary, access read, as fileNum )
		
		if( result = 0 ) then
			' Get width from file
			get #fileNum, 19, w
			
			' Get height from file
			get #fileNum, 23, h
			
			close( fileNum )
			
			imgData = imageCreate( w, h, rgba( 0, 0, 0, 0 ), 32 )
			
			dim as integer res = bload( fileName, imgData )
			
			if( res <> 0 ) then
				imageDestroy( imgData )
				imgData = 0
			end if
		end if
		
		return( imgData )				
	end function
	
	function Bitmap.fromTGA( byref fileName as const string ) as fb.image ptr export
		/'
			Loads a TGA file
			
			Currently this code only loads 32-bit uncompressed TGA files
			Note that you must specify the origin of the TGA file as 'top-left' in apps like
			GIMP, or else the bitmap is displayed upside down, as TGAs normally store the
			bitmap like that (makes working with APIs like OpenGL more comfortable)
			
			Yep, I'm too lazy to implement proper TGA loading XD		
		'/
		'' Read header
		dim as TGAHeader header
		
		dim as ulong ptr imagePixels
		dim as fb.image ptr imageData
		
		'' We need to calculate this value, as FB aligns image buffers to 16 byte boundaries
		dim as uinteger padding
		
		'' Open file
		dim as integer fileNum = freeFile()
		dim as integer result = open( filename, for binary, access read, as fileNum )
		
		if( result = 0 ) then
			'' Retrieve header first
			get #fileNum, , header
			
			'' Create a fb image buffer
			imageData = imageCreate( header.width, header.height )
			
			'' Pointer to pixel data				
			imagePixels = cast( ulong ptr, imageData )
			
			/'
				Calculate size of padding, as FB aligns the width of the images to a multiple of 16 bytes
				The size of the padding is calculated in pixels, not bytes, to allow for a pretty tight
				drawing loop
			'/
			padding = imageData->pitch \ imageData->bpp - header.width
			
			'' Skip header size ( in dwords / 4 bytes per pixel )
			imagePixels += sizeOf( fb.image ) \ sizeOf( ulong )
			
			'' This will hold the pixel read from file
			dim as ulong pixelRead
			
			'' Load pixel data into a fb.image buffer
			for y as integer = 0 to header.height - 1
				for x as integer = 0 to header.width - 1
					get #1, , pixelRead
					
					*imagePixels = pixelRead
					
					imagePixels += 1
				next
				'' Remember to add the padding at the end of horizontal scanline
				imagePixels += padding
			next
			
			close( fileNum )
		end if
		
		return( imageData )
	end function
	
	function Bitmap.clone() as fb.image ptr
		dim as fb.image ptr myClone = imageCreate( m_bitmap->width, m_bitmap->height, rgba( 0, 0, 0, 0 ), 32 )
		
		/'
			Creates a copy of this bitmap instance
			
			Here it just copies the pixel data to another buffer and returns it. Primarily used
			to create another image from already loaded data
		'/
		dim as ulong ptr d = cast( ulong ptr, myClone ) + sizeOf( fb.image ) \ sizeOf( ulong )
		dim as ulong ptr s = cast( ulong ptr, m_bitmap ) + sizeOf( fb.image ) \ sizeOf( ulong )
		dim as uinteger pixels = ( m_bitmap->pitch \ sizeOf( ulong ) ) * m_bitmap->height
		
		for i as uinteger = 0 to pixels - 1
			*d = *s
			
			d += 1
			s += 1
		next
			
		return( myClone )
	end function
end namespace
fb-bitmap-list.bi:

Code: Select all

#include once "fbgfx.bi"
#include once "fb-bitmap.bi"

/'
	A simple implementation of a list of images. Only barebones functionality
	is implemented, 	but you can easily extend it to include all sort of
	neat things, if you desire
'/
namespace gfx
	type BitmapList extends object
		public:
			declare constructor()
			declare destructor()
			
			declare sub add( byval as Bitmap ptr )
			declare function item( byval as integer ) as Bitmap ptr
			declare function count() as uinteger
			
		private:
			m_count						as uinteger
			m_bitmaps( any )	as Bitmap ptr
	end type
	
	constructor BitmapList()
	
	end constructor
	
	destructor BitmapList()
		for i as integer = 0 to m_count - 1
			delete( m_bitmaps( i ) )
		next
	end destructor
	
	sub BitmapList.add( byval bm as Bitmap ptr )
		m_count += 1
		redim preserve m_bitmaps( 0 to m_count - 1 )
		
		m_bitmaps( m_count - 1 ) = bm
	end sub
	
	function BitmapList.item( byval index as integer ) as Bitmap ptr
		return( m_bitmaps( index ) )
	end function
	
	function BitmapList.count() as uinteger
		return( m_count )
	end function
end namespace
And a little test code:

Code: Select all

#include once "fbgfx.bi"
#include once "fb-bitmap.bi"
#include once "fb-bitmap-list.bi"

/'
	Simple code to demonstrate two helper classes, Bitmap and BitmapList
'/
screenRes( 800, 600, 32, , fb.gfx_alpha_primitives )

scope
	'' Create a list of bitmaps
	dim as gfx.BitmapList blist
	
	'' Load some images (change them to anything you like, of course)
	with blist
		.add( new gfx.Bitmap( gfx.Bitmap.fromBMP( "mage002.bmp" ) ) )
		.add( new gfx.Bitmap( gfx.Bitmap.fromTGA( "star.tga" ) ) )
		'' ... add any image you wish
	end with
	
	'' Creates a bitmap from another one
	dim as gfx.Bitmap bm = blist.item( 1 )->clone()
	
	/'
		And display them. Note that the bitmaps returned directly from the list
		had to be dereferenced, as they're returned as raw fb.image pointers,
		so you can simply use the put statement to blit them to the screen
		
		If you don't do this, the code will simply crash
	'/
	put( 100, 100 ), *blist.item( 0 ), pset
	put( 200, 100 ), *blist.item( 1 ), alpha
	
	'' This one doesn't need dereferencing, courtesy of the cast() operator
	put( 300, 100 ), bm, alpha
end scope

/'
	The images don't have to be destroyed, since the Bitmap class takes care of
	that. The BitmapList simply deletes each of it's elements at destruction, sparing
	you from having to release each image manually
'/
sleep()
So there. Perhaps somebody has an use for it, or it can pass as only a tutorial for beginners ;)

EDIT: Fixed a tiny leak.
sancho3
Posts: 358
Joined: Sep 30, 2017 3:22

Re: Bitmap and BitmapList classes

Post by sancho3 »

Is copying pixel for pixel (inside Clone()) necessary when you are using FBImages? Why not just use Get?
paul doe
Moderator
Posts: 1733
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Bitmap and BitmapList classes

Post by paul doe »

sancho3 wrote:Is copying pixel for pixel (inside Clone()) necessary when you are using FBImages? Why not just use Get?
Because it's slightly faster that way. The implementation using get() would be:

Code: Select all

	function Bitmap.clone() as fb.image ptr
		dim as fb.image ptr myClone = imageCreate( m_bitmap->width, m_bitmap->height, rgba( 0, 0, 0, 0 ), 32 )
		
		get m_bitmap, ( 0, 0 ) - ( m_bitmap->width - 1, m_bitmap->height - 1 ), myClone
		
		return( myClone )
	end function
See for yourself which one is faster. Here, compiling with all optimizations is around 7-8%. Another reason is historical: I didn't use get() back in the day because it was buggy, so I simply did a memcpy, and the habit stood.
sero
Posts: 59
Joined: Mar 06, 2018 13:26
Location: USA

Re: Bitmap and BitmapList classes

Post by sero »

paul doe wrote:
sancho3 wrote:Is copying pixel for pixel (inside Clone()) necessary when you are using FBImages? Why not just use Get?
Because it's slightly faster that way.
I wonder if the speed increase has to do with bypassing overhead like checking if the bpp is the same or considering the clipping region or etc...
Also, your bitmap list makes me think of a texture atlas. Nice stuff.
paul doe
Moderator
Posts: 1733
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Bitmap and BitmapList classes

Post by paul doe »

sero wrote:I wonder if the speed increase has to do with bypassing overhead like checking if the bpp is the same or considering the clipping region or etc...
Perhaps. The FB functions need to do a lot of checking, and all this adds up. On the other hand, most of them use MMX, so they are really fast.
sero wrote:Also, your bitmap list makes me think of a texture atlas. Nice stuff.
Yes, indeed. The technique can be used like that with OpenGL or another library, but instead of storing bitmaps you would store the offsets into the texture (and/or other relevant data).
Post Reply