Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Core/GameEngine/Include/GameClient/ParticleSys.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
265 changes: 191 additions & 74 deletions Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand All @@ -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.

Copy link
Copy Markdown

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.

@Mauller Mauller Aug 16, 2026

Copy link
Copy Markdown
Author

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by setting the culled variable on the particles, they always had the variable but it has not been used till now.

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))
Expand All @@ -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() );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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();
Comment thread
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();
Expand All @@ -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();

Expand All @@ -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
Expand All @@ -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 )

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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?

@Mauller Mauller Aug 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

volume particles work in a different way as they have multiple surfaces.
The batching only really works with billboarded / flat particles

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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
Expand All @@ -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);

Expand All @@ -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;
}
Loading
Loading