-
Notifications
You must be signed in to change notification settings - Fork 241
perf(particlesys): Batch same type particles to improve particle rendering performance by 15 - 30% #3155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
perf(particlesys): Batch same type particles to improve particle rendering performance by 15 - 30% #3155
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,24 +168,41 @@ 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) | ||
| { | ||
| if (drawSmudge) | ||
| { | ||
| 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() ); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RefCountPtr |
||
| 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(); | ||
|
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
|
||
| 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 ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make things simpler/more consistent if volume particles were batched as well?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. volume particles work in a different way as they have multiple surfaces. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are they really that different though, they appear to use the same input arrays. The only difference is that they call a different render function and render more surfaces
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would need to find something that uses volume particles, all my current tests don't show any activity down that path
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Found that the microwave tank uses them. |
||
| { | ||
| 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; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test exists more than once in this file. Can consolidate and simplify.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried by having the visibility test section put particles that are visible into a list, but the performance was lower than just testing again and running through all particles later on.
There is likely an element of memory locality to it which putting pointers to the particle system on a list loses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by setting the
culledvariable on the particles, they always had the variable but it has not been used till now.