diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index acd504f6838..223cf5d27a8 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -602,7 +602,7 @@ class ParticleSystem : public MemoryPoolObject, void setInitialDelay( UnsignedInt delay ) { m_delayLeft = delay; } - AsciiString getParticleTypeName() { return m_particleTypeName; } ///< return the name of the particles + const AsciiString& getParticleTypeName() const { return m_particleTypeName; } ///< return the name of the particles Bool isUsingDrawables() { return (m_particleType == DRAWABLE) ? true : false; } Bool isUsingStreak() { return (m_particleType == STREAK) ? true : false; } Bool isUsingSmudge() { return (m_particleType == SMUDGE) ? true : false; } diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index b1b792b3ac9..353a1949eda 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,6 +153,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) { @@ -156,6 +168,33 @@ 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(); + vp->setIsCulled(true); + + //Test if particle is at the screen or terrain edges. + 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)) + continue; + + vp->setIsCulled(false); + particleCount++; + } + + // Particle system has no particles on screen + if (particleCount == 0) + continue; + //temporary hack that checks if texture name starts with "SMUD" - if so, we can assume it's a smudge type if (/*sys->isUsingSmudge()*/ *((DWORD *)sys->getParticleTypeName().str()) == 0x44554D53) { @@ -163,17 +202,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)) @@ -186,10 +215,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 blending 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 = !( m_streakLine && sys->isUsingStreak() ) && ( sys->getVolumeParticleDepth() <= 1 ); + 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 - Int count = 0; Vector3 *posArray = m_posBuffer->Get_Array(); Real *sizeArray = m_sizeBuffer->Get_Array(); Vector4 *RGBAArray = m_RGBABuffer->Get_Array(); @@ -203,48 +253,57 @@ 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)) + if (p->isCulled()) continue; - if (WWMath::Fabs(pos->y - bcY) > (beY + psize)) - 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 ); //@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) - break; + if (++pointCount == MAX_POINTS_PER_GROUP) + { + 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 ( count == 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() && (count >= 2) ) + if ( m_streakLine && sys->isUsingStreak() && (pointCount >= 2) ) { m_streakLine->Reset_Line(); @@ -268,14 +327,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 @@ -295,51 +354,67 @@ 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() ) + if ( sys->getVolumeParticleDepth() > 1 ) { - 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_Texture( texture ); + texture->Release_Ref();//release reference since it's held by pointGroup + m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space - /// @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_Billboard(sys->shouldBillboard()); + 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 Support animated texture particles - /// @todo lorenzen sez: unimplemented code wastes cpu cycles - m_pointGroup->Set_Point_Frame( 0 ); + /// @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()); + m_pointGroup->Set_Point_Frame( 0 ); - //RENDER IT! - if( sys->getVolumeParticleDepth() > 1 ) - { m_pointGroup->RenderVolumeParticle( rinfo, sys->getVolumeParticleDepth() ); - } + 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 += count; + m_onScreenParticleCount += (pointCount - startCount); /* // draw the wind vector for this particle system on the screen @@ -361,6 +436,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); @@ -374,3 +452,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; +} diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 32cb43582f6..d2e4fc907bf 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Generals/Code/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/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 41481701474..36ff3636780 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/GeneralsMD/Code/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; };