Skip to main content
GameDev.net gamedev.net
🔒 Locked 🎮 Unity

What can I do to make this rendering code faster?

Started by Fire Lancer Jul 3, 2009 at 3:37 PM 29 replies 5.1k views
Original Post
Fire Lancer
Fire Lancer
This code for rendering batches of 2D particles seems to be creating a large bottleneck on the GPU. I can be at <10% CPU utilisation and still only get 15fps with high particle counts. This function renders batches of particles, due to the fact that alpha blended stuff must be drawn back to front, it often will only render 50 or so particles in each batch.

void Ps::Render(ps::System *sys, ps::ParticleType *type, const CONTAINER &particles)
{
	CONTAINER::const_iterator
		it  = particles.begin(),
		end = particles.end();
	if(it==end)return;//early exit if no particles
	//set device up
	if(type->Image)device->SetTexture(0, ((Texture*)type->Image)->GetD3dTexture());
	else device->SetTexture(0,0);
	device->SetFVF(PsVertexFVF);
	device->SetIndices(ib);
	device->SetStreamSource(0, vb, 0, sizeof(PsVertex));
	grf->SetBlendMode(type->BlendMode);
	//calc transforms
	if(type->WorldAligned)
		device->SetTransform(D3DTS_WORLD, &matIdentity);//particle position is already relative to the world
	else
	{
		//we need to transform all the particles relative to the sysetem
		D3DXMATRIX matSystem, matPos, matRot;
		D3DXMatrixRotationZ  (&matRot, sys->GetDir());
		D3DXMatrixTranslation(&matPos, sys->GetPos().x, sys->GetPos().y, 0);
		matSystem = matRot * matPos;
		device->SetTransform(D3DTS_WORLD, &matSystem);
	}
	unsigned count = 0;
	PsVertex *vertices;
	//direct3d can discard the old buffer and give us a new block of memory
	//this means we can lock the buffer even if the graphics card is still
	//rendering the last batch
	vb->Lock(0,0, (void**)&vertices, D3DLOCK_DISCARD);
	while(true)
	{
		unsigned c = (*it)->GetColourARGB();
		float    x = (*it)->GetPos().x;
		float    y = (*it)->GetPos().y;
		float    s = (*it)->GetSize();
		float    a = (*it)->GetImageAngle();
		//make vector
		D3DXVECTOR4 v1i(x-s,y-s, 0,1);
		D3DXVECTOR4 v2i(x+s,y-s, 0,1);
		D3DXVECTOR4 v3i(x+s,y+s, 0,1);
		D3DXVECTOR4 v4i(x-s,y+s, 0,1);
		D3DXVECTOR4 v1o,v2o,v3o,v4o;
		//make matrix
		D3DXMATRIX transform;
		D3DXMatrixRotationZ(&transform, a);
		//transform
		D3DXVec4Transform(&v1o,&v1i,&transform);
		D3DXVec4Transform(&v2o,&v2i,&transform);
		D3DXVec4Transform(&v3o,&v3i,&transform);
		D3DXVec4Transform(&v4o,&v4i,&transform);
		//fill vb
		vertices[0] = PsVertex(v1o.x,v1o.y, c, 0.0f,0.0f);
		vertices[1] = PsVertex(v2o.x,v2o.y, c, 1.0f,0.0f);
		vertices[2] = PsVertex(v3o.x,v3o.y, c, 1.0f,1.0f);
		vertices[3] = PsVertex(v4o.x,v4o.y, c, 0.0f,1.0f);
		//inc counters
		vertices += 4;
		count++;
		it++;
		//render if filled batch or done all particles
		if(count == BATCH_SIZE || it==end)
		{
			vb->Unlock();
			device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0,0, count*4, 0, count*2);
			if(it==end)break;
			else
			{
				count = 0;
				vb->Lock(0,0, (void**)&vertices, D3DLOCK_DISCARD);
			}
		}
	}
}


I was thinking of optimising out Set* calls where the state is already set, for things like vertex buffer, FVF, that would be most of the time for particle systems, since each system contains multiple particles type (meaning multiple batches) and often several systems are drawn in a row. Would that really make such a difference though? I assume d3d optimises out Set* calls that have no effect to avoid expensive kernal/user mode switches? What else can I do to speed this up, a few thousand quads isn't really that much compared to what many 3D games render? [Edited by - Fire Lancer on July 3, 2009 3:56:23 PM]
Erik Rufelt
Erik Rufelt
How many particles are there in total?
Transferring them to the GPU can be the bottle-neck. It seems strange that your loop isn't CPU-bound though, with so many CPU-side calculations.
feal87
feal87
Tell us also your configuration (CPU and GPU model) of test and the number of particles so we can have more informations.
Fire Lancer
Fire Lancer
Particle count around this points is between 3000 and 3500. If I disable the particle effects I get around 100fps rather than 20. Images are A8R8G8B8 format, same as those used throughout my game. Most of the effects are damage/destruction effects, so as a result batches of about 50 are the best I'm getting.

CPU is an Intel Core 2 Duo @ 2.0Ghz, Graphics is an ATI Mobility Radian 3450 HD, however ive had similar results on various other graphics chip sets, including some Nvidia ones.

I can run other games with large number of particles without any noticeable fps drops (Sword Of The Stars comes to mind, which has lots of alpha blended particles (so they would have the same batching problem I have).

feal87
feal87
Quote:
Original post by Fire Lancer
Particle count around this points is between 3000 and 3500. If I disable the particle effects I get around 100fps rather than 20. Images are A8R8G8B8 format, same as those used throughout my game. Most of the effects are damage/destruction effects, so as a result batches of about 50 are the best I'm getting.

CPU is an Intel Core 2 Duo @ 2.0Ghz, Graphics is an ATI Mobility Radian 3450 HD, however ive had similar results on various other graphics chip sets, including some Nvidia ones.

I can run other games with large number of particles without any noticeable fps drops (Sword Of The Stars comes to mind, which has lots of alpha blended particles (so they would have the same batching problem I have).


In effect it seems strange, i can have 13000 particles with my particle engine (at 60 FPS) on a Core 2 Duo 2.0 Ghz with an Intel X3100 graphic chipset (a VERY crappy GPU)

Anyway i'll give you some hints on what to improve :

1) Don't create the particles buffer each frame, too much work for nothing. You should have your update logic and draw logic work at different rate (generally Update logic is static while draw logic is free to run free).
2) If possible for your position, I suggest to consider switching to use HLSL and Hardware Instancing (Shader Model 3.0 only). This way you could do a single draw to draw ALL the particles.
3) You should batch the particles only by texture, all the other parameters are ininfluent.

Anyway without having an example app to test I cannot give more detailed information sorry.
Erik Rufelt
Erik Rufelt
Just another thing to check, unless you have already done so; do you get the same framerate if you zoom out, so that the particles only occupy a few pixels on the screen in total?
Fire Lancer
Fire Lancer
Quote:
Original post by feal87
In effect it seems strange, i can have 13000 particles with my particle engine (at 60 FPS) on a Core 2 Duo 2.0 Ghz with an Intel X3100 graphic chipset (a VERY crappy GPU)

Anyway i'll give you some hints on what to improve :

1) Don't create the particles buffer each frame, too much work for nothing. You should have your update logic and draw logic work at different rate (generally Update logic is static while draw logic is free to run free).

Seeing as the update rate is fixed at 50 steps per second, right now all the particles have moved/changed colour/died/whatever betweenframes being rendered, so I dont see how keeping the vertex buffer between frames would help, considering they would be completely outdated by the next frame.
Quote:

2) If possible for your position, I suggest to consider switching to use HLSL and Hardware Instancing (Shader Model 3.0 only). This way you could do a single draw to draw ALL the particles.

3) You should batch the particles only by texture, all the other parameters are ininfluent.

How does that solve the issue that the effect of drawing 3 alpha blended items ontop of each other in A B C order is different from C A B order, which is the entire reason I'm not batching the 1500 odd smoke particles in one go already..

I considered separate code paths for the different blend modes, since additive for example doesn't care about order, however seeing as 99.9% of the particles are alpha blended, I dont see the gain...


Eg consider:
	Grf->DrawRect(100,100,200,200, 0x80FF0000);	Grf->DrawRect(150,100,250,200, 0x8000FF00);	Grf->DrawRect(125,150,225,250, 0x800000FF);	Grf->DrawRect(325,150,425,250, 0x800000FF);	Grf->DrawRect(300,100,400,200, 0x80FF0000);	Grf->DrawRect(350,100,450,200, 0x8000FF00);


You can clearly see the draw order in the picture above, hence the reason I need to draw almost everything from back to front...


Fire Lancer
Fire Lancer
Quote:
Original post by Erik Rufelt
Just another thing to check, unless you have already done so; do you get the same frame rate if you zoom out, so that the particles only occupy a few pixels on the screen in total?


Changing the projection matrix (eg by making it 10000 by 10000, so my 800*600 game only came up as like 1 pixel in the window) had no effect, nor did changing the view port size to something small, although very small values (eg 20 by 20) did have some effect...
feal87
feal87
Quote:
Original post by Fire Lancer
Quote:
Original post by Erik Rufelt
Just another thing to check, unless you have already done so; do you get the same frame rate if you zoom out, so that the particles only occupy a few pixels on the screen in total?


Changing the projection matrix (eg by making it 10000 by 10000, so my 800*600 game only came up as like 1 pixel in the window) had no effect, nor did changing the view port size to something small, although very small values (eg 20 by 20) did have some effect...


Post a test executable so we can test and determine what is causing the load...without proper profiling with appropriate tools there is very little we can do by only by watching some lines of code. :P (exe + pdb in release mode preferably)
Fire Lancer
Fire Lancer
Well its late (1am...), and I need to package it up in some tidy way without the large amount of data files that wont fit on my ftp (seeing as you will just get a FileNotFound error displayed and logged, followed by the app closing if I dont), so I'll leave that till morning.

Although if in the meantime anyway has a good way to get around having to draw alpha blended stuff in depth order I would love to know it, I'm sure theres a solution since in 3D it becomes in practical to force each particle system instance to have its own "layer", ie right now smoke from an exposion cant be both in front, and behind other objects at once and drawing one particle at a time will never have good performance.
Fire Lancer
Fire Lancer
Ok I hacked a small example together that is practical for you to tesst (isnt a massive amount of data, and you wont have to pay for half an hour to get to the areas causing problems, since I havnt implmented saving and loading of games yet).


Arrows to move the ship around, shift is primary weapon (very low particle count, just a few particles on asteroid impact), ctrl is secondary fire (large number of particle heavy missiles).

You can see the fps and the number of particles rendered in the current frame in the bottom right.

I have intentionally not taken advantage of batching the smoke trails between different missiles, since in my actual game that never happens, seeing as there is only a few instances of the same effect at a given time in general, or they are tied to completely different depths (eg damage effects, which must be tied to the same depth as the ship so if two ships pass over/below each other the effects are draw correctly. 3500 particles I mentioned before was for around 150 different particle effect instances.


You will need the VS 2008 runtimes and D3DX9_39.dll in case you don't already have those. Ignore the audio warning if you get one, its just because I didn't included the audio dlls, which doesn't matter since I didn't include any sound/music files anyway lol...

Particle Problem Example Download
ET3D
ET3D
One thing I can think of is that IIRC there's a limit on the number of buffer renames, i.e, the number of times you can use D3DLOCK_DISCARD before there's a need to stall the pipeline. Try using several buffers, and see if that helps.
Adam_42
Adam_42
Here's a few things to try:

- You can render all the particles with one texture by merging the textures into one big one and using the appropriate UV coordinates for each particle. This means all particles can go into one vertex buffer, and you only need one draw call per frame.

- If you use additive blending you don't need to sort them. That doesn't look very good for some types of effect though.

- Make sure you do profiling in a release build, with debug runtimes off, or you can easily get CPU bound.

- If the game (including time spend in D3D and the driver) isn't using close to 100% of one core of your processor then something is probably going wrong. Do you have say a Sleep() call in your update loop? Are you getting blocked on a critical section? A good CPU profiler should be able to tell you what's going on there.



- Use DXT compressed textures. DXT5 for ones with an alpha channel, DXT1 otherwise. They are 1/4 (1/8th for DXT1) of the size of RGBA so the graphics card can read them noticeably quicker.

- Use an index buffer so the GPU can use its transform cache.
feal87
feal87
I tested your app, my impression over it :

1) You should try to minimize the calls to setrenderstate, even if the DX runtime will ignore almost all of them because duplicate it is still a good strain on the system. (they are almost always the same, why resetting them? try implementing a system that prevent to resetting the same state over and over again)
2) Same for the call to SetTexture and SetIndices and SetStreamSource and SetFvF. These calls are NOT for free and are not ignored by DX (they are instead quite costly), do them only one time if you have to reset the same values over and over again.

Doing this you should recover quite a lot of speed i think. If it is still not enough you should try to reduce the number of draw call you do by batching multiple particles group into one draw. The draw call number is too high (i get 26 fps at 3500 particles with almost 300 draws call with the Intel X3100 graphic card)

Is the update logic of the particles done on the same thread as the draw? if yes, can you do a multithread approach for it (basically the update thread prepare the data for the next draw in a lock-step way (take a look at my blog at the gameloop post to have some ideas))? You should get quite a good performance improvement by doing it.

Try these ideas, if they are still not enough, post a new executable with these modify implemented and i'll try to look to other places where to improve performance.
Fire Lancer
Fire Lancer
Quote:
Original post by feal87
I tested your app, my impression over it :

1) You should try to minimize the calls to setrenderstate, even if the DX runtime will ignore almost all of them because duplicate it is still a good strain on the system. (they are almost always the same, why resetting them? try implementing a system that prevent to resetting the same state over and over again)
2) Same for the call to SetTexture and SetIndices and SetStreamSource and SetFvF. These calls are NOT for free and are not ignored by DX (they are instead quite costly), do them only one time if you have to reset the same values over and over again.

So the ansewr to:
Quote:

I was thinking of optimising out Set* calls...

Would that really make such a difference though? I assume d3d optimises out Set* calls that have no effect...

Would be a no d3d doesnt do it, and yes it would be worthwhile implementing such a system myself.


Quote:

Is the update logic of the particles done on the same thread as the draw? if yes, can you do a multithread approach for it (basically the update thread prepare the data for the next draw in a lock-step way (take a look at my blog at the gameloop post to have some ideas))? You should get quite a good performance improvement by doing it.

Currently updates and rendering is in same thread.

In the quick hacked together sample I posted it does one draw and one render each loop. In my actaul game I made it skip frames to keep the updates at a constant rate.

I have been playing around recently with making render/update completly seperate threads, however theres a number of issues I need to resolve before I can make that happen, like many of my key resources, eg all the window, audio and graphics objects being referenced counted, and the AddRef and Release for them not being thread safe, so I need to think of another memory management solution since I think putting locks in those methods would be expensive...(if anyone has any info on multi threading with reference counting I'd be happy to know before I spend days trying to make it work safely)


feal87
feal87
I suggest not to use CComPtr for anything not related to DirectX cause they are VERY slow. (AddRef and Release are VERY slow operation)
Benden
Benden
If you haven't already, try minimising the amount of work you are doing while the vertex buffers are locked. I.e. do all the calculations you have there just inside the while loop and store the results, then do a memcpy to copy the whole lot at once into the vertex buffer.

Also try using more than one vertex buffer...you are rendering using a buffer and then trying to lock it immediately after if there are more particles to deal with...this to me seems very very bad.
Fire Lancer
Fire Lancer
Well I'm not using COM's AddRef and Release, or COM in general. The problem is my reference counting solution is not thread safe...
	int AddRef(){return ++refCnt;}	int Release()	{		int cnt = --refCnt;		if(!cnt)delete this;		return cnt;	}
Fire Lancer
Fire Lancer
Quote:
enden
If you haven't already, try minimising the amount of work you are doing while the vertex buffers are locked. I.e. do all the calculations you have there just inside the while loop and store the results, then do a memcpy to copy the whole lot at once into the vertex buffer.

Ok Ive done this.
Quote:

Also try using more than one vertex buffer...you are rendering using a buffer and then trying to lock it immediately after if there are more particles to deal with...this to me seems very very bad.

Isnt that the entire point of a dynamic VertexBuffer and the DISCARD flag?

Quote:

Original post by feal87
1) You should try to minimize the calls to setrenderstate, even if the DX runtime will ignore almost all of them because duplicate it is still a good strain on the system. (they are almost always the same, why resetting them? try implementing a system that prevent to resetting the same state over and over again)

Well most of them are due to SetBlendMode, which is 7 calls in worst case (blend mode uses separate alpha), or 4 otherwise.
I changed it to only call them if the blend mode object has different blend settings.
Quote:

2) Same for the call to SetTexture and SetIndices and SetStreamSource and SetFvF. These calls are NOT for free and are not ignored by DX (they are instead quite costly), do them only one time if you have to reset the same values over and over again.

The current settings for all those are now cached, with the device set methods only being called if setting to something different.


I also played around with the batch size a little, 50 seems to work better so I set it to that, presumably because its rare to have 100's in one batch. I think ill create several differently sized buffers upto 100 particles (eg 20,60,100), and then select one based on the number of particles to render.

It seems slightly better, but still nowhere near good enough...

Updated Direc3D9 dll and pbd


New PS Render method:
void Ps::Render(ps::System *sys, ps::ParticleType *type, const std::list<ps::Particle*> &particles){	std::list<ps::Particle*>::const_iterator		it  = particles.begin(),		end = particles.end();	if(it==end)return;//early exit if no particles	//set device up	if(type->Image)grf->SetTexture(((Texture*)type->Image)->GetD3dTexture());	else           grf->SetTexture(0);	grf->SetVB(PsVertexFVF, vb, sizeof(PsVertex));	grf->SetIB(ib);	grf->SetBlendMode(type->BlendMode);	//calc transforms	if(type->WorldAligned)		grf->SetWorldTransform(matIdentity);//particle position is already relative to the world	else	{		//we need to transform all the particles relative to the sysetem		D3DXMATRIX matSystem, matPos, matRot;		D3DXMatrixRotationZ  (&matRot, sys->GetDir());		D3DXMatrixTranslation(&matPos, sys->GetPos().x, sys->GetPos().y, 0);		matSystem = matRot * matPos;		grf->SetWorldTransform(matSystem);	}	unsigned count = 0;	PsVertex verticesStart[BATCH_SIZE*4];	PsVertex *vertices = verticesStart;	//direct3d can discard the old buffer and give us a new block of memory	//this means we can lock the buffer even if the graphics card is still	//rendering the last batch	while(true)	{		unsigned c = (*it)->GetColourARGB();		float    x = (*it)->GetPos().x;		float    y = (*it)->GetPos().y;		float    s = (*it)->GetSize();		float    a = (*it)->GetImageAngle();		//make vector		D3DXVECTOR4 v1i(x-s,y-s, 0,1);		D3DXVECTOR4 v2i(x+s,y-s, 0,1);		D3DXVECTOR4 v3i(x+s,y+s, 0,1);		D3DXVECTOR4 v4i(x-s,y+s, 0,1);		D3DXVECTOR4 v1o,v2o,v3o,v4o;		//make matrix		D3DXMATRIX transform;		D3DXMatrixRotationZ(&transform, a);		//transform		D3DXVec4Transform(&v1o,&v1i,&transform);		D3DXVec4Transform(&v2o,&v2i,&transform);		D3DXVec4Transform(&v3o,&v3i,&transform);		D3DXVec4Transform(&v4o,&v4i,&transform);		//fill vb		vertices[0] = PsVertex(v1o.x,v1o.y, c, 0.0f,0.0f);		vertices[1] = PsVertex(v2o.x,v2o.y, c, 1.0f,0.0f);		vertices[2] = PsVertex(v3o.x,v3o.y, c, 1.0f,1.0f);		vertices[3] = PsVertex(v4o.x,v4o.y, c, 0.0f,1.0f);		//inc counters		vertices += 4;		count++;		it++;		//render if filled batch or done all particles		if(count == BATCH_SIZE || it==end)		{			size_t bytes = count*4*sizeof(PsVertex);			PsVertex *verticesVB;			vb->Lock(0,bytes, (void**)&verticesVB, D3DLOCK_DISCARD);			memcpy((void*)verticesVB, (void*)verticesStart, bytes);			vb->Unlock();			device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0,0, count*4, 0, count*2);			if(it==end)break;			else			{				vertices = verticesStart;				count = 0;			}		}	}}
Benden
Benden
A dynamic vertex buffer is used so you can update its contents for rendering every frame. If you have say 5 particle effects going off then you should use 5 different dynamic vertex buffers. Alternatively you could just use the one and have the draw calls index into different portions of it but you wouldn't see much improvement.

The problem is you are filling a vertex buffer, then telling the graphics card to render using that vertex buffer. Then you are trying to lock the vertex buffer again for the next particles....problem is the graphics card might be using it to render (just because you told it to render with DrawIndexedPrimitive doesnt mean that the rendering begins that instant...commands are queued i believe), so we're having to wait for the rendering to actually finish before obtaining the lock.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.