Skip to content

Commit 4b5ec70

Browse files
committed
perf(particlesys): Implement batched rendering for particles (#)
1 parent 15b7f3c commit 4b5ec70

4 files changed

Lines changed: 187 additions & 61 deletions

File tree

Core/GameEngine/Include/GameClient/ParticleSys.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ class ParticleSystem : public MemoryPoolObject,
602602

603603
void setInitialDelay( UnsignedInt delay ) { m_delayLeft = delay; }
604604

605-
AsciiString getParticleTypeName() { return m_particleTypeName; } ///< return the name of the particles
605+
const AsciiString& getParticleTypeName() const { return m_particleTypeName; } ///< return the name of the particles
606606
Bool isUsingDrawables() { return (m_particleType == DRAWABLE) ? true : false; }
607607
Bool isUsingStreak() { return (m_particleType == STREAK) ? true : false; }
608608
Bool isUsingSmudge() { return (m_particleType == SMUDGE) ? true : false; }

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp

Lines changed: 174 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646

4747
W3DParticleSystemManager::W3DParticleSystemManager()
4848
{
49+
m_batchBillboard = true;
50+
m_batchShaderType = ParticleSystemInfo::INVALID_SHADER;
51+
m_batchTexture = nullptr;
52+
4953
m_pointGroup = nullptr;
5054
m_streakLine = nullptr;
5155
m_posBuffer = nullptr;
@@ -77,6 +81,11 @@ W3DParticleSystemManager::~W3DParticleSystemManager()
7781
REF_PTR_RELEASE(m_streakLine);
7882
}
7983

84+
if (m_batchTexture)
85+
{
86+
REF_PTR_RELEASE(m_batchTexture);
87+
}
88+
8089
REF_PTR_RELEASE(m_posBuffer);
8190
REF_PTR_RELEASE(m_RGBABuffer);
8291
REF_PTR_RELEASE(m_sizeBuffer);
@@ -159,24 +168,41 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
159168
if (sys->isUsingDrawables())
160169
continue;
161170

171+
// TheSuperHackers @performance Mauller 16/08/2026 Test if the particle system has any visible particles that can be drawn
172+
// Earlier visibility testing prevents the particle texture lookup which can cause a batch flush
173+
int particleCount = 0;
174+
for (Particle* vp = sys->getFirstParticle(); vp; vp = vp->m_systemNext)
175+
{
176+
const Coord3D* pos = vp->getPosition();
177+
Real psize = vp->getSize();
178+
vp->setIsCulled(true);
179+
180+
//Test if particle is at the screen or terrain edges.
181+
if (WWMath::Fabs(pos->x - bcX) > (beX + psize))
182+
continue;
183+
184+
if (WWMath::Fabs(pos->y - bcY) > (beY + psize))
185+
continue;
186+
187+
if (WWMath::Fabs(pos->z - bcZ) > (beZ + psize))
188+
continue;
189+
190+
vp->setIsCulled(false);
191+
particleCount++;
192+
}
193+
194+
// Particle system has no particles on screen
195+
if (particleCount == 0)
196+
continue;
197+
162198
//temporary hack that checks if texture name starts with "SMUD" - if so, we can assume it's a smudge type
163199
if (/*sys->isUsingSmudge()*/ *((DWORD *)sys->getParticleTypeName().str()) == 0x44554D53)
164200
{
165201
if (drawSmudge)
166202
{
167203
for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext)
168204
{
169-
const Coord3D *pos = p->getPosition();
170-
Real psize = p->getSize();
171-
172-
//Cull particle to edges of screen and terrain.
173-
if (WWMath::Fabs( pos->x - bcX ) > ( beX + psize ) )
174-
continue;
175-
176-
if (WWMath::Fabs( pos->y - bcY ) > ( beY + psize ) )
177-
continue;
178-
179-
if (WWMath::Fabs( pos->z - bcZ ) > ( beZ + psize ) )
205+
if (p->isCulled())
180206
continue;
181207

182208
if (Smudge *smudge = TheSmudgeManager->findSmudge(p))
@@ -189,10 +215,31 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
189215
continue;
190216
}
191217

192-
/// @todo lorenzen sez: declare these outside the sys loop, and put some in registers
193-
// initialize them here still, of course
218+
// TheSuperHackers @performance Ronin/Mauller 09/08/2026 Implement batched rendering for similar particles
219+
// Particles with the same blending will now be batched onto a single texture surface before being drawn
220+
// If a different particle type appears before the batch is filled, the previous batch will be drawn first
221+
TextureClass *texture = W3DDisplay::m_assetManager->Get_Texture( sys->getParticleTypeName().str() );
222+
const Bool canBatch = !( m_streakLine && sys->isUsingStreak() ) && ( sys->getVolumeParticleDepth() <= 1 );
223+
if (!canBatch ||
224+
texture != m_batchTexture ||
225+
sys->getShaderType() != m_batchShaderType ||
226+
sys->shouldBillboard() != m_batchBillboard)
227+
{
228+
flushParticleBatch(rinfo, pointCount);
229+
}
230+
231+
// setup a new particle batch texture if prior batch was flushed
232+
if (canBatch && m_batchTexture == nullptr)
233+
{
234+
m_batchTexture = texture;
235+
m_batchTexture->Add_Ref();
236+
m_batchShaderType = sys->getShaderType();
237+
m_batchBillboard = sys->shouldBillboard();
238+
}
239+
240+
Int startCount = pointCount;
241+
194242
// build W3D particle buffer
195-
pointCount = 0;
196243
Vector3 *posArray = m_posBuffer->Get_Array();
197244
Real *sizeArray = m_sizeBuffer->Get_Array();
198245
Vector4 *RGBAArray = m_RGBABuffer->Get_Array();
@@ -206,18 +253,11 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
206253
//set-up all the per-particle
207254
for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext)
208255
{
209-
pos = p->getPosition();
210-
psize = p->getSize();
211-
212-
//Cull particle to edges of screen and terrain.
213-
if (WWMath::Fabs(pos->x - bcX) > (beX + psize))
214-
continue;
215-
216-
if (WWMath::Fabs(pos->y - bcY) > (beY + psize))
256+
if (p->isCulled())
217257
continue;
218258

219-
if (WWMath::Fabs(pos->z - bcZ) > (beZ + psize))
220-
continue;
259+
pos = p->getPosition();
260+
psize = p->getSize();
221261

222262
m_fieldParticleCount += ( sys->getPriority() == AREA_EFFECT && sys->m_isGroundAligned != FALSE );
223263

@@ -239,13 +279,29 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
239279
angleArray[pointCount] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI));
240280

241281
if (++pointCount == MAX_POINTS_PER_GROUP)
242-
break;
282+
{
283+
if (!canBatch)
284+
{
285+
break;
286+
}
287+
288+
// TheSuperHackers @info The Buffer is full mid-system so draw what we have and carry on with the SAME system
289+
// This prevents particles being dropped. Bank the stats first as the flush resets count to 0.
290+
m_onScreenParticleCount += (pointCount - startCount);
291+
flushParticleBatch(rinfo, pointCount);
292+
m_batchTexture = texture;
293+
m_batchTexture->Add_Ref();
294+
m_batchShaderType = sys->getShaderType();
295+
m_batchBillboard = sys->shouldBillboard();
296+
startCount = 0;
297+
}
243298
}
244299

245-
if ( pointCount == 0 )
300+
if (pointCount == startCount)
301+
{
302+
texture->Release_Ref();
246303
continue; //this system has no particles to render
247-
248-
TextureClass *texture = W3DDisplay::m_assetManager->Get_Texture( sys->getParticleTypeName().str() );
304+
}
249305

250306
if ( m_streakLine && sys->isUsingStreak() && (pointCount >= 2) )
251307
{
@@ -298,51 +354,67 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
298354

299355
if ( m_pointGroup ) // this catches the particle and volumeparticle cases
300356
{
301-
// render all the systems' particles
302-
m_pointGroup->Set_Texture( texture );
303-
texture->Release_Ref();//release reference since it's held by pointGroup
304-
m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space
305-
306-
switch( sys->getShaderType() )
357+
if ( sys->getVolumeParticleDepth() > 1 )
307358
{
308-
case ParticleSystemInfo::ADDITIVE:
309-
m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader );
310-
break;
311-
case ParticleSystemInfo::ALPHA:
312-
m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader );
313-
break;
314-
case ParticleSystemInfo::ALPHA_TEST:
315-
m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader );
316-
break;
317-
case ParticleSystemInfo::MULTIPLY:
318-
m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader );
319-
break;
320-
}
359+
m_pointGroup->Set_Texture( texture );
360+
texture->Release_Ref();//release reference since it's held by pointGroup
361+
m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space
321362

322-
/// @todo Use both QUADS and TRIS for particles
323-
m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS );
324-
m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount);
325-
m_pointGroup->Set_Billboard(sys->shouldBillboard());
363+
switch( sys->getShaderType() )
364+
{
365+
case ParticleSystemInfo::ADDITIVE:
366+
m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader );
367+
break;
368+
case ParticleSystemInfo::ALPHA:
369+
m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader );
370+
break;
371+
case ParticleSystemInfo::ALPHA_TEST:
372+
m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader );
373+
break;
374+
case ParticleSystemInfo::MULTIPLY:
375+
m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader );
376+
break;
377+
}
326378

327-
/// @todo Support animated texture particles
328-
/// @todo lorenzen sez: unimplemented code wastes cpu cycles
329-
m_pointGroup->Set_Point_Frame( 0 );
379+
/// @todo Use both QUADS and TRIS for particles
380+
m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS );
381+
m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount );
382+
m_pointGroup->Set_Billboard(sys->shouldBillboard());
383+
m_pointGroup->Set_Point_Frame( 0 );
330384

331-
//RENDER IT!
332-
if( sys->getVolumeParticleDepth() > 1 )
333-
{
334385
m_pointGroup->RenderVolumeParticle( rinfo, sys->getVolumeParticleDepth() );
335-
}
386+
m_onScreenParticleCount += (pointCount - startCount);
387+
pointCount = startCount;
388+
}
336389
else
337-
m_pointGroup->Render( rinfo );
390+
{
391+
if ( m_batchTexture == nullptr )
392+
{
393+
m_batchTexture = texture;
394+
m_batchShaderType = sys->getShaderType();
395+
m_batchBillboard = sys->shouldBillboard();
396+
}
397+
else
398+
{
399+
texture->Release_Ref(); // same key as the pending batch so drop the duplicate ref
400+
}
338401

402+
if ( pointCount >= MAX_POINTS_PER_GROUP )
403+
{
404+
flushParticleBatch(rinfo, pointCount);
405+
}
406+
}
407+
}
408+
else
409+
{
410+
texture->Release_Ref();
339411
}
340412
}
341413

342414

343415
/// @todo lorenzen sez: this should be debug only:
344416
//add particle count to total
345-
m_onScreenParticleCount += pointCount;
417+
m_onScreenParticleCount += (pointCount - startCount);
346418

347419
/*
348420
// draw the wind vector for this particle system on the screen
@@ -364,6 +436,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
364436

365437
}
366438

439+
// TheSuperHackers @info Flush the last batch if one is pending
440+
flushParticleBatch(rinfo, pointCount);
441+
367442
/// @todo lorenzen sez: this should be debug only:
368443
TheParticleSystemManager->setOnScreenParticleCount(m_onScreenParticleCount);
369444

@@ -377,3 +452,42 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
377452
((W3DSmudgeManager *)TheSmudgeManager)->render(rinfo);
378453
}
379454
}
455+
456+
void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount)
457+
{
458+
if (pointCount > 0 && m_batchTexture != nullptr && m_pointGroup != nullptr)
459+
{
460+
m_pointGroup->Set_Texture(m_batchTexture);
461+
462+
switch (m_batchShaderType)
463+
{
464+
case ParticleSystemInfo::ADDITIVE:
465+
m_pointGroup->Set_Shader(ShaderClass::_PresetAdditiveSpriteShader);
466+
break;
467+
case ParticleSystemInfo::ALPHA:
468+
m_pointGroup->Set_Shader(ShaderClass::_PresetAlphaSpriteShader);
469+
break;
470+
case ParticleSystemInfo::ALPHA_TEST:
471+
m_pointGroup->Set_Shader(ShaderClass::_PresetATestSpriteShader);
472+
break;
473+
case ParticleSystemInfo::MULTIPLY:
474+
m_pointGroup->Set_Shader(ShaderClass::_PresetMultiplicativeSpriteShader);
475+
break;
476+
}
477+
478+
m_pointGroup->Set_Flag(PointGroupClass::TRANSFORM, true);
479+
m_pointGroup->Set_Point_Mode(PointGroupClass::QUADS);
480+
m_pointGroup->Set_Arrays(m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount);
481+
m_pointGroup->Set_Billboard(m_batchBillboard);
482+
m_pointGroup->Set_Point_Frame(0);
483+
m_pointGroup->Render(rinfo);
484+
}
485+
486+
if (m_batchTexture != nullptr)
487+
{
488+
m_batchTexture->Release_Ref();
489+
m_batchTexture = nullptr;
490+
}
491+
492+
pointCount = 0;
493+
}

Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,19 @@ class W3DParticleSystemManager : public ParticleSystemManager
5050
virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; }
5151

5252
private:
53+
void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount);
54+
5355
enum { MAX_POINTS_PER_GROUP = 512 };
5456

57+
TextureClass *m_batchTexture; ///< the texture used as the drawing surface for batched particle draws
5558
PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles
5659
StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks
5760
ShareBufferClass<Vector3> *m_posBuffer; ///< array of particle positions
5861
ShareBufferClass<Vector4> *m_RGBABuffer; ///< array of particle color and alpha
5962
ShareBufferClass<float> *m_sizeBuffer; ///< array of particle sizes
6063
ShareBufferClass<uint8> *m_angleBuffer; ///< array of particle orientations
64+
65+
ParticleSystemInfo::ParticleShaderType m_batchShaderType;
6166
Bool m_readyToRender; ///< if true, it is OK to render
67+
Bool m_batchBillboard;
6268
};

GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,19 @@ class W3DParticleSystemManager : public ParticleSystemManager
5050
virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; }
5151

5252
private:
53+
void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount);
54+
5355
enum { MAX_POINTS_PER_GROUP = 512 };
5456

57+
TextureClass *m_batchTexture; ///< the texture used as the drawing surface for batched particle draws
5558
PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles
5659
StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks
5760
ShareBufferClass<Vector3> *m_posBuffer; ///< array of particle positions
5861
ShareBufferClass<Vector4> *m_RGBABuffer; ///< array of particle color and alpha
5962
ShareBufferClass<float> *m_sizeBuffer; ///< array of particle sizes
6063
ShareBufferClass<uint8> *m_angleBuffer; ///< array of particle orientations
64+
65+
ParticleSystemInfo::ParticleShaderType m_batchShaderType;
6166
Bool m_readyToRender; ///< if true, it is OK to render
67+
Bool m_batchBillboard;
6268
};

0 commit comments

Comments
 (0)