4646
4747W3DParticleSystemManager::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+ }
0 commit comments