How to optimize quick repainting of large areas?


#1

Hello !

I’m implementing a digital phase oscilloscope with JUCE framework, but unfortunately, it is consuming a bit too much CPU - It does work, but it uses too much CPU!
I’ve made a little test application to illustrate the problems I’m encountering. At regular time interval, the main component redraws a set of random dots on itself. You can select the repainting rate, the number of dots and whether the position and/or the colour of the dots should be randomly generated at each repaint. [attachment=0]TestGuiApp3 - CMockOscillo.zip[/attachment] TestGuiApp3 - CMockOscillo.zip" contains a fully functional VS08 project (I actually have the problem under Win Vista, but I don’t think it is OS-related) as well as a Jucer file. I haven’t tried it, but it should allow you to generate any configuration you prefer. The *.jucer file was generated with the current most recent GIT Tip (5 Nov 2010 10:21:28)

My CMockOscillo class is a juce::Component with an Image* member (m_piBackGround) that stores the background of the oscillo (with no dot on it) At regular time interval (typically 30ms) a juce::Timer calls repaint(), and my paint methods does the following :
1° It ensures the stored image is still valid (regarding size, etc.) If not, it updates it.
2° It copies the image in the paint’s graphic context.
3° It finally adds all the dots with SetPixel() method.

…and that’s how simple it is. Here is my exact “paint” code (if you don’t feel like downloading all my app…)
Please look for the keyword “QUESTION” in the code below, these are my precise questions regarding how to optimize the code.

void CMockOscillo::paint(Graphics &g)
{
	// The core of the job is done in this function.


////////////////////////////////////
// 	If needed, reallocation and
//	repainting of the background
//	image.
////////////////////////////////////
	if(m_piBackGround)	// if we have an image already...
	{
		if(	m_piBackGround->getWidth()	!= getWidth()
		||	m_piBackGround->getHeight()	!= getHeight())	// ...but its size is inappropriate...
		{
			delete m_piBackGround; // ...then delete the image !
			m_piBackGround = NULL;
		}
	}
	if(m_piBackGround == NULL)	// If the image isn't ready.
								// NB :	either it's just been deleted, as size was wrong,
								//		or it has simply never been allocated. (first call of paint)
	{
		// allocate the image !
		m_piBackGround = new Image(Image::RGB, getWidth(), getHeight(), false, Image::SoftwareImage);
		jassert(m_piBackGround->isValid());
					// QUESTION :	What difference is there between a SoftwareImage
					//				and a NativeImage ? Which one would you recommend
					//				using in this case ?

		
		// Redraw the background.
		// NB :	due to the above mechanism, this is only going to happen
		//		when size has changed, so I don't really care if some
		//		time-consuming operation happen here.
		Graphics gBackGround(*m_piBackGround);
		ColourGradient grad(Colours::black,		0.f,								0.f,
							Colours::darkblue,	(float)m_piBackGround->getWidth(),	(float)m_piBackGround->getHeight(), false);
		gBackGround.setGradientFill(grad);
		gBackGround.fillAll();
	}


////////////////////////////////////
// 	actual painting job.
//	(performed at every call)
////////////////////////////////////
	
	if(m_bRandomizeDotsPositions)
	{
		// If the user want us to generate
		//	new dot position, do so !
		Random rdm(0);
		rdm.setSeedRandomly();
		for(int i = 0; i < m_nNbDots; ++i)
		{
			m_faDotsX[i] = rdm.nextFloat();
			m_faDotsY[i] = rdm.nextFloat();
		}
	}

	// Copy the (unaltered) background image
	g.drawImageAt(*m_piBackGround,0,0); // QUESTION : Is it possible to make it faster, using one or the other JUCE class ?
	
	// prepare dots' colour.
	g.setColour(m_bRandomizeDotsColours
		?	Colour(Random::getSystemRandom().nextInt()).withAlpha(1.f).withBrightness(0.8f)
		:	Colours::yellow);

	// draw the dots.
	for(int i = 0; i < m_nNbDots; ++i)
	{
		g.setPixel((int)(m_faDotsX[i] * getWidth()), (int)(m_faDotsY[i] * getHeight()));
		// QUESTION :	After doing a little profiling, this loop seems to be where most time
		//				is spend. I suppose I could gain efficiency here, but how ?
		//
		//				Would you recommend using Image::BitmapData ?
		//				- I don't need any aliasing (and SetPixel doesn't perform any anyway)
		//				- I can ensure that I don't jump out of the image as I know its size,
		//					and its size can btw only be changed by the present paint function.
		//					(I could add some critical section to protect from multiple thread
		//					calling this paint, just in case.)
		//
		//				Should I rather copy the background to another member Image* (sth like
		//				"Image* CMockOscillo::m_piForeGround"), then light the pixels on this
		//				new image, and finally copy the foreground to the Graphic g ?...
	}	
}
///////////////////////////////////////////////////////////////////////////////

Thank you in advance for your help and hints !

Val


#2

setPixel will be very very slow for that - a pixel is drawn as a rectangle, involving clipping, calls to the OS to render it, saving/restoring graphics contexts, colours etc etc. It’s not a feasible way to draw a lot of points. Using Image::BitmapData is the way to do it.

Use a native image where possible, and don’t use ‘new’ - Images are now copy-by-value objects, they deal with their own internal allocation. You shouldn’t ever need to use an Image* any more, just use references.


#3

Hi Jules !

Thank you very much for the very quick response. I’ve tried what you suggested, seems to solve the issue in my test app - I’ll test it in my real application as soon as possible, but I feel pretty confident it’ll work.

Now, for whoever would be interested in it, here’s my reviewed code : [attachment=0]TestGuiApp3 - CMockOscillo.zip[/attachment] [code]
void CMockOscillo::paint(Graphics &g)
{
// The core of the job is done in this function.

////////////////////////////////////
// If needed, reallocation and
// repainting of the background
// image.
////////////////////////////////////
jassert(m_iBackGround.isValid() && m_iForeGround.isValid());

if(	m_iBackGround.getWidth()	!= getWidth()
||	m_iBackGround.getHeight()	!= getHeight())
{
	m_iBackGround = Image(Image::RGB, getWidth(),				getHeight(),				false, Image::NativeImage);
	m_iForeGround = Image(Image::RGB, m_iBackGround.getWidth(), m_iBackGround.getHeight(),	false, Image::NativeImage);
	// QUESTION :	What difference is there between a SoftwareImage
	//				and a NativeImage ? Which one would you recommend
	//				using in this case ?
	// ANSWER :		jules : "Use a native image where possible"
	jassert(m_iBackGround.isValid() && m_iForeGround.isValid());

	// Redraw the background.
	// NB :	due to the above mechanism, this is only going to happen
	//		when size has changed, so I don't really care if some
	//		time-consuming operation happen here.	
	Graphics gBackGround(m_iBackGround);
	ColourGradient grad(Colours::black,		0.f,								0.f,
						Colours::darkblue,	(float)m_iBackGround.getWidth(),	(float)m_iBackGround.getHeight(), false);
	gBackGround.setGradientFill(grad);
	gBackGround.fillAll();
	
	// copy background to foreground.
	Graphics gForeGround(m_iForeGround);
	gForeGround.drawImageAt(m_iBackGround, 0,0);
}

////////////////////////////////////
// actual painting job.
// (performed at every call)
////////////////////////////////////

jassert(m_iBackGround.getWidth()	== m_iForeGround.getWidth());
jassert(m_iBackGround.getHeight()	== m_iForeGround.getHeight());

Image::BitmapData bdBackGround(m_iBackGround, 0,0, m_iBackGround.getWidth(), m_iBackGround.getHeight(), false);
Image::BitmapData bdForeGround(m_iForeGround, 0,0, m_iForeGround.getWidth(), m_iForeGround.getHeight(), true);

// erase dots from last time
for(int i = 0; i < kMaxNbDots; ++i)
{
	uint8* const pu8PixelSrc = bdBackGround.getPixelPointer((int)(m_faDotsX[i] * m_iBackGround.getWidth()),
															(int)(m_faDotsY[i] * m_iBackGround.getHeight()));
	uint8* const pu8PixelDst = bdForeGround.getPixelPointer((int)(m_faDotsX[i] * m_iForeGround.getWidth()),
															(int)(m_faDotsY[i] * m_iForeGround.getHeight()));

	*(pu8PixelDst)		= *(pu8PixelSrc);	
	*(pu8PixelDst+1)	= *(pu8PixelSrc+1);
	*(pu8PixelDst+2)	= *(pu8PixelSrc+2);
}


if(m_bRandomizeDotsPositions)
{
	// If the user want us to generate
	//	new dot position, do so !
	Random rdm(0);
	rdm.setSeedRandomly();
	for(int i = 0; i < m_nNbDots; ++i)
	{
		m_faDotsX[i] = rdm.nextFloat();
		m_faDotsY[i] = rdm.nextFloat();
	}
}


// prepare dots colour.
const Colour colDots(m_bRandomizeDotsColours
				?	Colour(Random::getSystemRandom().nextInt()).withAlpha(1.f).withBrightness(0.8f)
				:	Colours::yellow);
const uint8 u8Red	= colDots.getRed();
const uint8 u8Green = colDots.getGreen();
const uint8 u8Blue	= colDots.getBlue();


// draw the dots.
for(int i = 0; i < m_nNbDots; ++i)
{
	uint8* const pu8Pixel = bdForeGround.getPixelPointer(	(int)(m_faDotsX[i] * m_iForeGround.getWidth()),
															(int)(m_faDotsY[i] * m_iForeGround.getHeight()));

	*(pu8Pixel)		= u8Blue;
	*(pu8Pixel+1)	= u8Green;
	*(pu8Pixel+2)	= u8Red;
}

g.drawImageAt(m_iForeGround, 0,0);

}
[/code]

Thank you very much for your help !

Val