Estava investigando uma queda muito pesada de FPS na Skill Birds (Chaotic Diseier / Dark Lord). Em algumas situações, essa skill praticamente derrubava o jogo sozinho.
Inicialmente parecia que o problema poderia ser a quantidade de partículas ou a geometria dos efeitos, mas não era nada disso.
O problema estava na forma como o sistema estava enviando as partículas para renderização.
Basicamente, o sistema estava fazendo um "flush" no batch praticamente a cada quad renderizado.
Para explicar de forma simples: o batch serve justamente para juntar vários desenhos parecidos e enviar tudo de uma vez para a placa de vídeo. Só que, nesse caso, o estado de blend estava sendo alterado várias vezes sem necessidade.
Então acontecia algo parecido com isso para uma única partícula:
EnableAlphaBlend(); // muda o estado e gera flush
EnableAlphaBlendMinus(); // muda novamente e gera outro flush
Ou seja, o próprio código se desfazia dentro da mesma
A solução foi identificar qual blend cada partícula realmente vai usar antes de desenhá-la.
Depois disso, as partículas passaram a ser organizadas primeiro pelo tipo de blend e, em seguida, pelos outros estados que também influenciam a renderização, como tipo, subtipo e textura.
Com isso, em vez de ficar alternando:
Blend normal
Blend minus
Blend normal
Blend minus
Blend normal
o sistema consegue agrupar melhor:
Todas com Blend normal
Todas com Blend minus
Todas com Blend3
Isso reduziu drasticamente a quantidade de flushes e draw calls.
Vou deixar abaixo o código que usei para fazer a correção, caso alguém queira acoplar na sua source
A alteração é basicamente no ZzzEffectParticle.cpp.
A ideia é identificar o tipo de blend que cada partícula realmente vai utilizar antes de renderizar. Com isso, o sistema consegue ordenar as partículas de uma forma que evita ficar trocando o blend e dando flush no batch a cada quad.
1. Adicione essa função antes do ParticleSortClass
enum
{
PBLEND_DEFAULT = 0,
PBLEND_BLEND,
PBLEND_MINUS,
PBLEND_BLEND3
};
static int ParticleFinalBlend(const PARTICLE* o)
{
switch (o->Type)
{
case BITMAP_CLUD64:
return (o->SubType == 0 ||
o->SubType == 5 ||
o->SubType == 11)
? PBLEND_MINUS
: PBLEND_DEFAULT;
case BITMAP_TWINTAIL_WATER:
return PBLEND_BLEND;
case BITMAP_SMOKE:
return (o->SubType == 2 ||
o->SubType == 5 ||
o->SubType == 12 ||
o->SubType == 14 ||
o->SubType == 15 ||
o->SubType == 20 ||
o->SubType == 21 ||
o->SubType == 29 ||
o->SubType == 37 ||
o->SubType == 38 ||
o->SubType == 59)
? PBLEND_MINUS
: PBLEND_DEFAULT;
case BITMAP_SMOKE + 1:
case BITMAP_SMOKE + 4:
return PBLEND_BLEND3;
case BITMAP_SMOKE + 3:
return (o->SubType == 3 ||
o->SubType == 4)
? PBLEND_MINUS
: PBLEND_DEFAULT;
case BITMAP_LIGHT + 2:
return (o->SubType == 3 ||
o->SubType == 4 ||
o->SubType == 6)
? PBLEND_MINUS
: PBLEND_DEFAULT;
case BITMAP_SPARK:
return (o->SubType == 10)
? PBLEND_MINUS
: PBLEND_DEFAULT;
case BITMAP_SMOKELINE2:
return (o->SubType == 3)
? PBLEND_MINUS
: PBLEND_DEFAULT;
case BITMAP_CLOUD:
return (o->SubType == 7 ||
o->SubType == 14 ||
o->SubType == 16)
? PBLEND_MINUS
: PBLEND_DEFAULT;
}
return PBLEND_DEFAULT;
}
2. Altere a ordenação das partículas
No lugar da ordenação antiga, use o blend como primeira prioridade:
struct ParticleSortPred
{
bool operator()(int ia, int ib) const
{
const PARTICLE& A = Particles[ia];
const PARTICLE& B = Particles[ib];
const int ba = ParticleFinalBlend(&A);
const int bb = ParticleFinalBlend(&B);
if (ba != bb)
return ba < bb;
const int ca = ParticleSortClass(&A);
const int cb = ParticleSortClass(&B);
if (ca != cb)
return ca < cb;
if (A.Type != B.Type)
return A.Type < B.Type;
if (A.SubType != B.SubType)
return A.SubType < B.SubType;
return A.TexType < B.TexType;
}
};
3. Altere o início do RenderParticles
Se atualmente estiver assim:
if (pBitmap->Components == 3)
{
EnableAlphaBlend();
}
else
{
EnableAlphaTest(false);
}
troque por:
switch (ParticleFinalBlend(o))
{
case PBLEND_BLEND:
EnableAlphaBlend();
break;
case PBLEND_MINUS:
EnableAlphaBlendMinus();
break;
case PBLEND_BLEND3:
EnableAlphaBlend3();
break;
default:
if (pBitmap->Components == 3)
EnableAlphaBlend();
else
EnableAlphaTest(false);
break;
}
Importante: se o seu Main tiver Type/SubType customizados, confira os case do seu próprio RenderParticles e adicione eles no ParticleFinalBlend(). A lógica é a mesma: o blend que a partícula realmente usa precisa ser conhecido antes da renderização.
Se alguém quiser aplicar a mesma ideia em outros sistemas de efeito, vale procurar por situações onde o código fica alternando EnableAlphaBlend, EnableAlphaBlendMinus, EnableAlphaBlend3, etc. dentro do loop de renderização. É aí que normalmente começa esse tipo de problema de flush.