From aef58c71d4469192d3f18ed98523ebeb7fef6ccc Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:07:57 +0100 Subject: [PATCH 1/7] chore(particlesys): Refactor point count into a class variable, ready for further optimisations --- .../W3DDevice/GameClient/W3DParticleSys.cpp | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index c0a901ec7e6..c784e2e7421 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -144,6 +144,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) TheSmudgeManager->resetDraw(); } + // Number of particle/points being rendered + UnsignedInt pointCount = 0; + ParticleSystemManager::ParticleSystemList &particleSysList = TheParticleSystemManager->getAllParticleSystems(); for( ParticleSystemManager::ParticleSystemListIt it = particleSysList.begin(); it != particleSysList.end(); ++it) { @@ -189,7 +192,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) /// @todo lorenzen sez: declare these outside the sys loop, and put some in registers // initialize them here still, of course // build W3D particle buffer - Int count = 0; + pointCount = 0; Vector3 *posArray = m_posBuffer->Get_Array(); Real *sizeArray = m_sizeBuffer->Get_Array(); Vector4 *RGBAArray = m_RGBABuffer->Get_Array(); @@ -219,32 +222,32 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) m_fieldParticleCount += ( sys->getPriority() == AREA_EFFECT && sys->m_isGroundAligned != FALSE ); //@todo lorenzen sez: use pointer arithmetic for these arrays - personalities[count] = p->getPersonality(); + personalities[pointCount] = p->getPersonality(); - posArray[count].X = pos->x; - posArray[count].Y = pos->y; - posArray[count].Z = pos->z; + posArray[pointCount].X = pos->x; + posArray[pointCount].Y = pos->y; + posArray[pointCount].Z = pos->z; - sizeArray[count] = psize; + sizeArray[pointCount] = psize; color = p->getColor(); - RGBAArray[count].X = color->red; - RGBAArray[count].Y = color->green; - RGBAArray[count].Z = color->blue; - RGBAArray[count].W = p->getAlpha(); + RGBAArray[pointCount].X = color->red; + RGBAArray[pointCount].Y = color->green; + RGBAArray[pointCount].Z = color->blue; + RGBAArray[pointCount].W = p->getAlpha(); - angleArray[count] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI)); + angleArray[pointCount] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI)); - if (++count == MAX_POINTS_PER_GROUP) + if (++pointCount == MAX_POINTS_PER_GROUP) break; } - if ( count == 0 ) + if ( pointCount == 0 ) continue; //this system has no particles to render TextureClass *texture = W3DDisplay::m_assetManager->Get_Texture( sys->getParticleTypeName().str() ); - if ( m_streakLine && sys->isUsingStreak() && (count >= 2) ) + if ( m_streakLine && sys->isUsingStreak() && (pointCount >= 2) ) { m_streakLine->Reset_Line(); @@ -268,14 +271,14 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //UPDATE THE STREAK'S ARRAYS m_streakLine->Set_LocsWidthsColors( - count, + pointCount, m_posBuffer->Get_Array(), m_sizeBuffer->Get_Array(), m_RGBABuffer->Get_Array(), &personalities[0] ); - //WWASSERT( m_streakLine->Get_Num_Points() == count ); + //WWASSERT( m_streakLine->Get_Num_Points() == pointCount ); // This is the happy place for this! RGBAArray[0].X = 0;//eliminates the scissor edge on the trailing edge of the streak @@ -318,7 +321,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) /// @todo Use both QUADS and TRIS for particles m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); - m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, count ); + m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount); m_pointGroup->Set_Billboard(sys->shouldBillboard()); /// @todo Support animated texture particles @@ -340,7 +343,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) /// @todo lorenzen sez: this should be debug only: //add particle count to total - m_onScreenParticleCount += count; + m_onScreenParticleCount += pointCount; /* // draw the wind vector for this particle system on the screen From d54cbc84e834311e34d30584cc5b164a0df95696 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:10:15 +0100 Subject: [PATCH 2/7] perf(particlesys): Implement batched rendering for particles (#) --- .../Include/GameClient/ParticleSys.h | 13 +- .../W3DDevice/GameClient/W3DParticleSys.h | 6 + .../W3DDevice/GameClient/W3DParticleSys.cpp | 241 +++++++++++++----- 3 files changed, 191 insertions(+), 69 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 5cd97e06e97..e23fcb21d7f 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -515,7 +515,7 @@ class ParticleSystemTemplate : public MemoryPoolObject, protected ParticleSystem void validate(); - AsciiString getName() const { return m_name; } + const AsciiString& getName() const { return m_name; } // This function was made const because of update modules' module data being all const. ParticleSystem *createSlaveSystem( Bool createSlaves = TRUE ) const ; ///< if returns non-null, it is a slave system for use @@ -607,11 +607,12 @@ class ParticleSystem : public MemoryPoolObject, void setInitialDelay( UnsignedInt delay ) { m_delayLeft = delay; } const AsciiString& getParticleTypeName() const { return m_particleTypeName; } ///< return the name of the particles - const Bool isUsingDrawables() const { return m_particleType == DRAWABLE; } - const Bool isUsingStreak() const { return m_particleType == STREAK; } - const Bool isUsingSmudge() const { return m_particleType == SMUDGE; } - const Bool isUsingVolumeParticles() const { return m_particleType == VOLUME_PARTICLE; } - const UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } + Bool isUsingParticles() const { return m_particleType == PARTICLE; } + Bool isUsingDrawables() const { return m_particleType == DRAWABLE; } + Bool isUsingStreak() const { return m_particleType == STREAK; } + Bool isUsingSmudge() const { return m_particleType == SMUDGE; } + Bool isUsingVolumeParticles() const { return m_particleType == VOLUME_PARTICLE; } + UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } Bool shouldBillboard() { return !m_isGroundAligned; } diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 41481701474..36ff3636780 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -50,13 +50,19 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: + void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); + enum { MAX_POINTS_PER_GROUP = 512 }; + TextureClass *m_batchTexture; ///< the texture used as the drawing surface for batched particle draws PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks ShareBufferClass *m_posBuffer; ///< array of particle positions ShareBufferClass *m_RGBABuffer; ///< array of particle color and alpha ShareBufferClass *m_sizeBuffer; ///< array of particle sizes ShareBufferClass *m_angleBuffer; ///< array of particle orientations + + ParticleSystemInfo::ParticleShaderType m_batchShaderType; Bool m_readyToRender; ///< if true, it is OK to render + Bool m_batchBillboard; }; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index c784e2e7421..e30a3725e37 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -46,6 +46,10 @@ W3DParticleSystemManager::W3DParticleSystemManager() { + m_batchBillboard = true; + m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; + m_batchTexture = nullptr; + m_pointGroup = nullptr; m_streakLine = nullptr; m_posBuffer = nullptr; @@ -77,6 +81,11 @@ W3DParticleSystemManager::~W3DParticleSystemManager() REF_PTR_RELEASE(m_streakLine); } + if (m_batchTexture) + { + REF_PTR_RELEASE(m_batchTexture); + } + REF_PTR_RELEASE(m_posBuffer); REF_PTR_RELEASE(m_RGBABuffer); REF_PTR_RELEASE(m_sizeBuffer); @@ -144,7 +153,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) TheSmudgeManager->resetDraw(); } - // Number of particle/points being rendered + // Number of particles/points being rendered. UnsignedInt pointCount = 0; ParticleSystemManager::ParticleSystemList &particleSysList = TheParticleSystemManager->getAllParticleSystems(); @@ -159,6 +168,31 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) if (sys->isUsingDrawables()) continue; + // TheSuperHackers @performance Mauller 16/08/2026 Test if the particle system has any visible particles that can be drawn. + // Earlier visibility testing prevents the particle texture lookup which can cause a batch flush. + int particleCount = 0; + for (Particle* vp = sys->getFirstParticle(); vp; vp = vp->m_systemNext) + { + const Coord3D* pos = vp->getPosition(); + Real psize = vp->getSize(); + + //Test if particle is at the screen or terrain edges. + if (WWMath::Fabs(pos->x - bcX) > (beX + psize) || + WWMath::Fabs(pos->y - bcY) > (beY + psize) || + WWMath::Fabs(pos->z - bcZ) > (beZ + psize)) + { + vp->setIsCulled(true); + continue; + } + + vp->setIsCulled(false); + particleCount++; + } + + // Particle system has no particles on screen + if (particleCount == 0) + continue; + // Handle smudge type particles if (sys->isUsingSmudge()) { @@ -167,17 +201,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { - const Coord3D *pos = p->getPosition(); - Real psize = p->getSize(); - - //Cull particle to edges of screen and terrain. - if (WWMath::Fabs( pos->x - bcX ) > ( beX + psize ) ) - continue; - - if (WWMath::Fabs( pos->y - bcY ) > ( beY + psize ) ) - continue; - - if (WWMath::Fabs( pos->z - bcZ ) > ( beZ + psize ) ) + if (p->isCulled()) continue; if (Smudge *smudge = TheSmudgeManager->findSmudge(p)) @@ -189,10 +213,31 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) continue; } - /// @todo lorenzen sez: declare these outside the sys loop, and put some in registers - // initialize them here still, of course + // TheSuperHackers @performance Ronin/Mauller 09/08/2026 Implement batched rendering for similar particles. + // Particles with the same properties will now be batched onto a single texture surface before being drawn. + // If a different particle type appears before the batch is filled, the previous batch will be drawn first. + TextureClass *texture = W3DDisplay::m_assetManager->Get_Texture( sys->getParticleTypeName().str() ); + const Bool canBatch = sys->isUsingParticles(); + if (!canBatch || + texture != m_batchTexture || + sys->getShaderType() != m_batchShaderType || + sys->shouldBillboard() != m_batchBillboard) + { + flushParticleBatch(rinfo, pointCount); + } + + // setup a new particle batch texture if prior batch was flushed. + if (canBatch && m_batchTexture == nullptr) + { + m_batchTexture = texture; + m_batchTexture->Add_Ref(); + m_batchShaderType = sys->getShaderType(); + m_batchBillboard = sys->shouldBillboard(); + } + + Int startCount = pointCount; + // build W3D particle buffer - pointCount = 0; Vector3 *posArray = m_posBuffer->Get_Array(); Real *sizeArray = m_sizeBuffer->Get_Array(); Vector4 *RGBAArray = m_RGBABuffer->Get_Array(); @@ -206,18 +251,11 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //set-up all the per-particle for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { - pos = p->getPosition(); - psize = p->getSize(); - - //Cull particle to edges of screen and terrain. - if (WWMath::Fabs(pos->x - bcX) > (beX + psize)) - continue; - - if (WWMath::Fabs(pos->y - bcY) > (beY + psize)) + if (p->isCulled()) continue; - if (WWMath::Fabs(pos->z - bcZ) > (beZ + psize)) - continue; + pos = p->getPosition(); + psize = p->getSize(); m_fieldParticleCount += ( sys->getPriority() == AREA_EFFECT && sys->m_isGroundAligned != FALSE ); @@ -239,13 +277,29 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) angleArray[pointCount] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI)); if (++pointCount == MAX_POINTS_PER_GROUP) - break; + { + if (!canBatch) + { + break; + } + + // TheSuperHackers @info The Buffer is full mid-system so draw what we have and carry on with the SAME system. + // This prevents particles being dropped. Bank the stats first as the flush resets count to 0. + m_onScreenParticleCount += (pointCount - startCount); + flushParticleBatch(rinfo, pointCount); + m_batchTexture = texture; + m_batchTexture->Add_Ref(); + m_batchShaderType = sys->getShaderType(); + m_batchBillboard = sys->shouldBillboard(); + startCount = 0; + } } - if ( pointCount == 0 ) + if (pointCount == startCount) + { + texture->Release_Ref(); continue; //this system has no particles to render - - TextureClass *texture = W3DDisplay::m_assetManager->Get_Texture( sys->getParticleTypeName().str() ); + } if ( m_streakLine && sys->isUsingStreak() && (pointCount >= 2) ) { @@ -298,52 +352,71 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) if ( m_pointGroup ) // this catches the particle and volumeparticle cases { - // render all the systems' particles - m_pointGroup->Set_Texture( texture ); - texture->Release_Ref();//release reference since it's held by pointGroup - m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space - - switch( sys->getShaderType() ) - { - case ParticleSystemInfo::ADDITIVE: - m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader ); - break; - case ParticleSystemInfo::ALPHA: - m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader ); - break; - case ParticleSystemInfo::ALPHA_TEST: - m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader ); - break; - case ParticleSystemInfo::MULTIPLY: - m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader ); - break; - } - - /// @todo Use both QUADS and TRIS for particles - m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); - m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount); - m_pointGroup->Set_Billboard(sys->shouldBillboard()); - - /// @todo Support animated texture particles - /// @todo lorenzen sez: unimplemented code wastes cpu cycles - m_pointGroup->Set_Point_Frame( 0 ); - - //RENDER IT! const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) { + m_pointGroup->Set_Texture( texture ); + texture->Release_Ref();//release reference since it's held by pointGroup + m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space + + switch( sys->getShaderType() ) + { + case ParticleSystemInfo::ADDITIVE: + m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader ); + break; + case ParticleSystemInfo::ALPHA: + m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader ); + break; + case ParticleSystemInfo::ALPHA_TEST: + m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader ); + break; + case ParticleSystemInfo::MULTIPLY: + m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader ); + break; + } + + /// @todo Use both QUADS and TRIS for particles + m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); + m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount ); + m_pointGroup->Set_Billboard(sys->shouldBillboard()); + + /// @todo Support animated texture particles + /// @todo lorenzen sez: unimplemented code wastes cpu cycles + m_pointGroup->Set_Point_Frame( 0 ); + m_pointGroup->RenderVolumeParticle( rinfo, volumeParticleDepth); + m_onScreenParticleCount += (pointCount - startCount); + pointCount = startCount; } else - m_pointGroup->Render( rinfo ); - + { + if ( m_batchTexture == nullptr ) + { + m_batchTexture = texture; + m_batchShaderType = sys->getShaderType(); + m_batchBillboard = sys->shouldBillboard(); + } + else + { + texture->Release_Ref(); // same key as the pending batch so drop the duplicate ref + } + + if ( pointCount >= MAX_POINTS_PER_GROUP ) + { + flushParticleBatch(rinfo, pointCount); + } + } + } + else + { + texture->Release_Ref(); } } /// @todo lorenzen sez: this should be debug only: //add particle count to total - m_onScreenParticleCount += pointCount; + m_onScreenParticleCount += (pointCount - startCount); /* // draw the wind vector for this particle system on the screen @@ -365,6 +438,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } + // TheSuperHackers @info Flush the last batch if one is pending. + flushParticleBatch(rinfo, pointCount); + /// @todo lorenzen sez: this should be debug only: TheParticleSystemManager->setOnScreenParticleCount(m_onScreenParticleCount); @@ -378,3 +454,42 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) ((W3DSmudgeManager *)TheSmudgeManager)->render(rinfo); } } + +void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount) +{ + if (pointCount > 0 && m_batchTexture != nullptr && m_pointGroup != nullptr) + { + m_pointGroup->Set_Texture(m_batchTexture); + + switch (m_batchShaderType) + { + case ParticleSystemInfo::ADDITIVE: + m_pointGroup->Set_Shader(ShaderClass::_PresetAdditiveSpriteShader); + break; + case ParticleSystemInfo::ALPHA: + m_pointGroup->Set_Shader(ShaderClass::_PresetAlphaSpriteShader); + break; + case ParticleSystemInfo::ALPHA_TEST: + m_pointGroup->Set_Shader(ShaderClass::_PresetATestSpriteShader); + break; + case ParticleSystemInfo::MULTIPLY: + m_pointGroup->Set_Shader(ShaderClass::_PresetMultiplicativeSpriteShader); + break; + } + + m_pointGroup->Set_Flag(PointGroupClass::TRANSFORM, true); + m_pointGroup->Set_Point_Mode(PointGroupClass::QUADS); + m_pointGroup->Set_Arrays(m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount); + m_pointGroup->Set_Billboard(m_batchBillboard); + m_pointGroup->Set_Point_Frame(0); + m_pointGroup->Render(rinfo); + } + + if (m_batchTexture != nullptr) + { + m_batchTexture->Release_Ref(); + m_batchTexture = nullptr; + } + + pointCount = 0; +} From 7fc3cdba9c4de2d61cbf711bc65504d0ebc5f5da Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:54:15 +0100 Subject: [PATCH 3/7] refactor(particlesys): Use refCountPtr objects to hold ref counted textures and cleanup batch initialization --- .../W3DDevice/GameClient/W3DParticleSys.h | 3 +- .../W3DDevice/GameClient/W3DParticleSys.cpp | 59 +++++++------------ 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 36ff3636780..424814dd291 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -50,11 +50,12 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: + void initializeBatch(ParticleSystem* system, const RefCountPtr& texture); void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); enum { MAX_POINTS_PER_GROUP = 512 }; - TextureClass *m_batchTexture; ///< the texture used as the drawing surface for batched particle draws + RefCountPtr m_batchTexture; ///< the texture used as the drawing surface for batched particle draws PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks ShareBufferClass *m_posBuffer; ///< array of particle positions diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index e30a3725e37..b2d456f049c 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -48,7 +48,6 @@ W3DParticleSystemManager::W3DParticleSystemManager() { m_batchBillboard = true; m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; - m_batchTexture = nullptr; m_pointGroup = nullptr; m_streakLine = nullptr; @@ -81,11 +80,6 @@ W3DParticleSystemManager::~W3DParticleSystemManager() REF_PTR_RELEASE(m_streakLine); } - if (m_batchTexture) - { - REF_PTR_RELEASE(m_batchTexture); - } - REF_PTR_RELEASE(m_posBuffer); REF_PTR_RELEASE(m_RGBABuffer); REF_PTR_RELEASE(m_sizeBuffer); @@ -216,12 +210,12 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // TheSuperHackers @performance Ronin/Mauller 09/08/2026 Implement batched rendering for similar particles. // Particles with the same properties will now be batched onto a single texture surface before being drawn. // If a different particle type appears before the batch is filled, the previous batch will be drawn first. - TextureClass *texture = W3DDisplay::m_assetManager->Get_Texture( sys->getParticleTypeName().str() ); + RefCountPtr texture; + texture.Assign_No_Add_Ref(W3DDisplay::m_assetManager->Get_Texture(sys->getParticleTypeName().str())); + const Bool canBatch = sys->isUsingParticles(); - if (!canBatch || - texture != m_batchTexture || - sys->getShaderType() != m_batchShaderType || - sys->shouldBillboard() != m_batchBillboard) + const Bool batchDone = texture.Peek() != m_batchTexture.Peek() || sys->getShaderType() != m_batchShaderType || sys->shouldBillboard() != m_batchBillboard; + if (!canBatch || batchDone) { flushParticleBatch(rinfo, pointCount); } @@ -229,10 +223,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // setup a new particle batch texture if prior batch was flushed. if (canBatch && m_batchTexture == nullptr) { - m_batchTexture = texture; - m_batchTexture->Add_Ref(); - m_batchShaderType = sys->getShaderType(); - m_batchBillboard = sys->shouldBillboard(); + initializeBatch(sys, texture); } Int startCount = pointCount; @@ -287,17 +278,13 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // This prevents particles being dropped. Bank the stats first as the flush resets count to 0. m_onScreenParticleCount += (pointCount - startCount); flushParticleBatch(rinfo, pointCount); - m_batchTexture = texture; - m_batchTexture->Add_Ref(); - m_batchShaderType = sys->getShaderType(); - m_batchBillboard = sys->shouldBillboard(); + initializeBatch(sys, texture); startCount = 0; } } if (pointCount == startCount) { - texture->Release_Ref(); continue; //this system has no particles to render } @@ -305,8 +292,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) { m_streakLine->Reset_Line(); - m_streakLine->Set_Texture( texture ); - texture->Release_Ref();//release reference since it's held by streakline + m_streakLine->Set_Texture( texture.Peek() ); switch( sys->getShaderType() ) { case ParticleSystemInfo::ADDITIVE: @@ -355,8 +341,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) { - m_pointGroup->Set_Texture( texture ); - texture->Release_Ref();//release reference since it's held by pointGroup + m_pointGroup->Set_Texture( texture.Peek() ); m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space switch( sys->getShaderType() ) @@ -392,13 +377,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) { if ( m_batchTexture == nullptr ) { - m_batchTexture = texture; - m_batchShaderType = sys->getShaderType(); - m_batchBillboard = sys->shouldBillboard(); - } - else - { - texture->Release_Ref(); // same key as the pending batch so drop the duplicate ref + initializeBatch(sys, texture); } if ( pointCount >= MAX_POINTS_PER_GROUP ) @@ -407,10 +386,6 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } } } - else - { - texture->Release_Ref(); - } } @@ -455,11 +430,18 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } } +void W3DParticleSystemManager::initializeBatch(ParticleSystem* system, const RefCountPtr& texture) +{ + m_batchTexture = texture; + m_batchShaderType = system->getShaderType(); + m_batchBillboard = system->shouldBillboard(); +} + void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount) { - if (pointCount > 0 && m_batchTexture != nullptr && m_pointGroup != nullptr) + if (pointCount > 0 && m_batchTexture != nullptr) { - m_pointGroup->Set_Texture(m_batchTexture); + m_pointGroup->Set_Texture(m_batchTexture.Peek()); switch (m_batchShaderType) { @@ -487,8 +469,7 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign if (m_batchTexture != nullptr) { - m_batchTexture->Release_Ref(); - m_batchTexture = nullptr; + m_batchTexture.Clear(); } pointCount = 0; From 2d7b721a00b05d57a161c1e355b145880c8ea040 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:01:25 +0100 Subject: [PATCH 4/7] refactor(particlesys): Remove unnecessary null checks on pointGroup and streakLine objects --- .../W3DDevice/GameClient/W3DParticleSys.cpp | 89 ++++++++----------- 1 file changed, 36 insertions(+), 53 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index b2d456f049c..e2cd4e8c8b4 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -288,7 +288,8 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) continue; //this system has no particles to render } - if ( m_streakLine && sys->isUsingStreak() && (pointCount >= 2) ) + // Handle drawing streak type particles. + if ( sys->isUsingStreak() && (pointCount >= 2) ) { m_streakLine->Reset_Line(); @@ -329,63 +330,45 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //RENDER STREAK! m_streakLine->Render( rinfo ); - + m_onScreenParticleCount += (pointCount - startCount); + pointCount = startCount; } - else - { - WWASSERT( m_pointGroup ); + // Handle volumetric type particle systems. + const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); + if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) + { + m_pointGroup->Set_Texture( texture.Peek() ); + m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space - if ( m_pointGroup ) // this catches the particle and volumeparticle cases + switch( sys->getShaderType() ) { - const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); - if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) - { - m_pointGroup->Set_Texture( texture.Peek() ); - m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space - - switch( sys->getShaderType() ) - { - case ParticleSystemInfo::ADDITIVE: - m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader ); - break; - case ParticleSystemInfo::ALPHA: - m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader ); - break; - case ParticleSystemInfo::ALPHA_TEST: - m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader ); - break; - case ParticleSystemInfo::MULTIPLY: - m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader ); - break; - } - - /// @todo Use both QUADS and TRIS for particles - m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); - m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount ); - m_pointGroup->Set_Billboard(sys->shouldBillboard()); - - /// @todo Support animated texture particles - /// @todo lorenzen sez: unimplemented code wastes cpu cycles - m_pointGroup->Set_Point_Frame( 0 ); - - m_pointGroup->RenderVolumeParticle( rinfo, volumeParticleDepth); - m_onScreenParticleCount += (pointCount - startCount); - pointCount = startCount; - } - else - { - if ( m_batchTexture == nullptr ) - { - initializeBatch(sys, texture); - } - - if ( pointCount >= MAX_POINTS_PER_GROUP ) - { - flushParticleBatch(rinfo, pointCount); - } - } + case ParticleSystemInfo::ADDITIVE: + m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader ); + break; + case ParticleSystemInfo::ALPHA: + m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader ); + break; + case ParticleSystemInfo::ALPHA_TEST: + m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader ); + break; + case ParticleSystemInfo::MULTIPLY: + m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader ); + break; } + + /// @todo Use both QUADS and TRIS for particles + m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); + m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount ); + m_pointGroup->Set_Billboard(sys->shouldBillboard()); + + /// @todo Support animated texture particles + /// @todo lorenzen sez: unimplemented code wastes cpu cycles + m_pointGroup->Set_Point_Frame( 0 ); + + m_pointGroup->RenderVolumeParticle( rinfo, volumeParticleDepth); + m_onScreenParticleCount += (pointCount - startCount); + pointCount = startCount; } From b69ee132ce1cde6b51e7829b245416a85a9b8279 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:32:07 +0100 Subject: [PATCH 5/7] refactor(particlesys): Allow the batching and rendering of untextured particles (#) --- .../Source/W3DDevice/GameClient/W3DParticleSys.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index e2cd4e8c8b4..49f098163b9 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -422,7 +422,7 @@ void W3DParticleSystemManager::initializeBatch(ParticleSystem* system, const Ref void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount) { - if (pointCount > 0 && m_batchTexture != nullptr) + if (pointCount > 0) { m_pointGroup->Set_Texture(m_batchTexture.Peek()); @@ -448,12 +448,14 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign m_pointGroup->Set_Billboard(m_batchBillboard); m_pointGroup->Set_Point_Frame(0); m_pointGroup->Render(rinfo); + + m_batchBillboard = false; + m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; + pointCount = 0; } if (m_batchTexture != nullptr) { m_batchTexture.Clear(); } - - pointCount = 0; } From 17b16874b40e7edede5b31a8a9ca01ec5d987b44 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:07:45 +0100 Subject: [PATCH 6/7] Review update --- .../W3DDevice/GameClient/W3DParticleSys.h | 1 + .../W3DDevice/GameClient/W3DParticleSys.cpp | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 424814dd291..8de4fc37ee6 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -50,6 +50,7 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: + Bool finishedBatch(ParticleSystem* system, const RefCountPtr& texture); void initializeBatch(ParticleSystem* system, const RefCountPtr& texture); void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index 49f098163b9..2ae42817595 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -162,13 +162,12 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) if (sys->isUsingDrawables()) continue; - // TheSuperHackers @performance Mauller 16/08/2026 Test if the particle system has any visible particles that can be drawn. - // Earlier visibility testing prevents the particle texture lookup which can cause a batch flush. - int particleCount = 0; + // TheSuperHackers @performance Mauller 16/08/2026 Skip processing particle system if no particles are in view. + UnsignedInt particleCount = 0; for (Particle* vp = sys->getFirstParticle(); vp; vp = vp->m_systemNext) { const Coord3D* pos = vp->getPosition(); - Real psize = vp->getSize(); + const Real psize = vp->getSize(); //Test if particle is at the screen or terrain edges. if (WWMath::Fabs(pos->x - bcX) > (beX + psize) || @@ -214,7 +213,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) texture.Assign_No_Add_Ref(W3DDisplay::m_assetManager->Get_Texture(sys->getParticleTypeName().str())); const Bool canBatch = sys->isUsingParticles(); - const Bool batchDone = texture.Peek() != m_batchTexture.Peek() || sys->getShaderType() != m_batchShaderType || sys->shouldBillboard() != m_batchBillboard; + const Bool batchDone = finishedBatch(sys, texture); if (!canBatch || batchDone) { flushParticleBatch(rinfo, pointCount); @@ -226,7 +225,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) initializeBatch(sys, texture); } - Int startCount = pointCount; + UnsignedInt startCount = pointCount; // build W3D particle buffer Vector3 *posArray = m_posBuffer->Get_Array(); @@ -413,6 +412,13 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } } +Bool W3DParticleSystemManager::finishedBatch(ParticleSystem* system, const RefCountPtr& texture) +{ + return texture.Peek() != m_batchTexture.Peek() || + system->getShaderType() != m_batchShaderType || + system->shouldBillboard() != m_batchBillboard; +} + void W3DParticleSystemManager::initializeBatch(ParticleSystem* system, const RefCountPtr& texture) { m_batchTexture = texture; @@ -449,13 +455,10 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign m_pointGroup->Set_Point_Frame(0); m_pointGroup->Render(rinfo); - m_batchBillboard = false; - m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; pointCount = 0; } - if (m_batchTexture != nullptr) - { - m_batchTexture.Clear(); - } + m_batchTexture.Clear(); + m_batchBillboard = false; + m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; } From 2cf39ec9759499848f6fdc3d8d1ca52719395d8b Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:32:10 +0100 Subject: [PATCH 7/7] make particle batch handling functions pass const references --- .../Include/GameClient/ParticleSys.h | 4 ++-- .../W3DDevice/GameClient/W3DParticleSys.h | 4 ++-- .../W3DDevice/GameClient/W3DParticleSys.cpp | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index e23fcb21d7f..4bf120e332d 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -614,9 +614,9 @@ class ParticleSystem : public MemoryPoolObject, Bool isUsingVolumeParticles() const { return m_particleType == VOLUME_PARTICLE; } UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } - Bool shouldBillboard() { return !m_isGroundAligned; } + Bool shouldBillboard() const { return !m_isGroundAligned; } - ParticleShaderType getShaderType() { return m_shaderType; } + ParticleShaderType getShaderType() const { return m_shaderType; } void setSlave( ParticleSystem *slave ); ///< set a slave system for us ParticleSystem *getSlave() { return m_slaveSystem; } diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 8de4fc37ee6..cf17783902e 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -50,8 +50,8 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: - Bool finishedBatch(ParticleSystem* system, const RefCountPtr& texture); - void initializeBatch(ParticleSystem* system, const RefCountPtr& texture); + Bool finishedBatch(const ParticleSystem& system, const RefCountPtr& texture); + void initializeBatch(const ParticleSystem& system, const RefCountPtr& texture); void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); enum { MAX_POINTS_PER_GROUP = 512 }; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index 2ae42817595..9ceadaa2894 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -213,7 +213,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) texture.Assign_No_Add_Ref(W3DDisplay::m_assetManager->Get_Texture(sys->getParticleTypeName().str())); const Bool canBatch = sys->isUsingParticles(); - const Bool batchDone = finishedBatch(sys, texture); + const Bool batchDone = finishedBatch(*sys, texture); if (!canBatch || batchDone) { flushParticleBatch(rinfo, pointCount); @@ -222,7 +222,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // setup a new particle batch texture if prior batch was flushed. if (canBatch && m_batchTexture == nullptr) { - initializeBatch(sys, texture); + initializeBatch(*sys, texture); } UnsignedInt startCount = pointCount; @@ -277,7 +277,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // This prevents particles being dropped. Bank the stats first as the flush resets count to 0. m_onScreenParticleCount += (pointCount - startCount); flushParticleBatch(rinfo, pointCount); - initializeBatch(sys, texture); + initializeBatch(*sys, texture); startCount = 0; } } @@ -412,18 +412,18 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } } -Bool W3DParticleSystemManager::finishedBatch(ParticleSystem* system, const RefCountPtr& texture) +Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const RefCountPtr& texture) { return texture.Peek() != m_batchTexture.Peek() || - system->getShaderType() != m_batchShaderType || - system->shouldBillboard() != m_batchBillboard; + system.getShaderType() != m_batchShaderType || + system.shouldBillboard() != m_batchBillboard; } -void W3DParticleSystemManager::initializeBatch(ParticleSystem* system, const RefCountPtr& texture) +void W3DParticleSystemManager::initializeBatch(const ParticleSystem& system, const RefCountPtr& texture) { m_batchTexture = texture; - m_batchShaderType = system->getShaderType(); - m_batchBillboard = system->shouldBillboard(); + m_batchShaderType = system.getShaderType(); + m_batchBillboard = system.shouldBillboard(); } void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount)