Files
RP412/MUNGA_L4/L4PARTICLES.cpp
T
CydandClaude Opus 5 4410febd4c The particle engine hands the device back at the end of the race
A fresh renderer is built per mission, and the particle engine's vertex
buffer is D3DPOOL_DEFAULT with a texture to match - both bound to the
device that made them. Initialize overwrote the two pointers with the
new device's resources without releasing the old ones, so the old device
kept a reference from resources nothing could reach any more.
~DPLRenderer's SAFE_RELEASE(mDevice) therefore never took it to zero.

Every race left a whole live device behind it - back buffer, depth
buffer and all, at whatever the render target is, which on the tester's
machine is 2560x1440. The next race's Initialize was the only thing that
ever let one go, so quitting from the front end let it go never.

Measured rather than assumed, with a standalone test using the same pool
and usage: release the device with the buffer outstanding and it reports
1 reference left, still alive. Release the buffer first and it reports 0.

Three parts to it:

- Destroy is null-safe now, and clears what it drops. It was neither,
  and it runs on the device-lost path AHEAD OF A RESET - so a texture
  that never loaded, which a missing VIDEO\particles.png is enough to
  cause, took the Reset down with it. A released pointer left in place
  is a dangling one the moment anything looks again.

- Initialize calls it first. The device-lost path already released
  before re-initialising; this is the same contract for the case where
  the device is not lost but REPLACED, which is what a new race is.

- ~DPLRenderer calls it before releasing the device, next to the texture
  cache flush that is there for exactly this reason and had missed this
  one. The device now dies with the mission that made it.

Destroy clearing mDevice is what makes the gap between it and the next
Initialize safe: the paint paths already test that pointer before they
touch anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 19:56:13 -05:00

576 lines
16 KiB
C++

#include "l4particles.h"
#include "../munga/time.h"
LPDIRECT3DDEVICE9 ParticleEngine::mDevice = NULL;
PARTICLE_EFFECT ParticleEngine::mInstalledEffects[MAX_PARTICLE_EFFECTS];
LPDIRECT3DTEXTURE9 ParticleEngine::mParticleTexture = NULL;
bool ParticleEngine::mActiveParticles = false;
LPDIRECT3DVERTEXBUFFER9 ParticleEngine::mVertBuffer = NULL;
Particle *ParticleEngine::mParticlesHead = NULL;
Particle *ParticleEngine::mParticlesTail = NULL;
long ParticleEngine::mTotalParticleCount = 0;
long ParticleEngine::mMaxParticleCount = 8192;
int compare(const Particle *a, const Particle *b, const D3DXMATRIX *view_matrix)
{
D3DXVECTOR3 aVec(a->mX, a->mY, a->mZ);
D3DXVECTOR3 bVec(b->mX, b->mY, b->mZ);
D3DXVec3TransformCoord(&aVec, &aVec, view_matrix);
D3DXVec3TransformCoord(&bVec, &bVec, view_matrix);
float aDist = aVec.x * aVec.x + aVec.y * aVec.y + aVec.z * aVec.z;
float bDist = bVec.x * bVec.x + bVec.y * bVec.y + bVec.z * bVec.z;
// if both particles are within a certain distance from
// each other then we'll sort them by effect instead of
// distance so that they will be grouped together when
// rendering
//if (abs(bDist - aDist) < EFFECT_GROUPING_EPSILON)
// return b->mEffect->id - a->mEffect->id;
//else
return (int)(bDist - aDist);
}
void mergesort(Particle **headPtr, Particle **tailPtr, const D3DXMATRIX *view_matrix)
{
Particle *list = *headPtr;
if (!list)
{
(*tailPtr) = NULL;
return;
}
Particle *p, *q, *e, *tail, *oldhead;
int insize, nmerges, psize, qsize, i;
insize = 1;
while (1)
{
p = list;
oldhead = list;
list = NULL;
tail = NULL;
nmerges = 0; // count number of merges we do in this pass
while (p)
{
nmerges++; // there exists a merge to be done
// step 'insize' places along from p
q = p;
psize = 0;
for (i=0; i<insize; i++)
{
psize++;
q = q->Next();
if (!q)
break;
}
// if q hasn't fallen off end, we have two lists to merge
qsize = insize;
// now we have two lists; merge them
while (psize > 0 || (qsize > 0 && q))
{
// decide whether next element of merge comes from p or q
if (psize == 0)
{
// p is empty; e must come from q
e = q;
q = q->Next();
qsize--;
}
else if (qsize == 0 || !q)
{
// q is empty; e must come from p
e = p;
p = p->Next();
psize--;
}
else if (compare(p, q, view_matrix) <= 0)
{
// first elements of p is lower (or same);
// e must come from p
e = p;
p = p->Next();
psize--;
}
else
{
// first element of q is lower; e must come from q
e = q;
q = q->Next();
qsize--;
}
// add the next element to the merged list
if (tail)
tail->mNextParticle = e;
else
list = e;
e->mPrevParticle = tail;
tail = e;
}
// now p has stepped 'insize' places along, and q has too
p = q;
}
tail->mNextParticle = NULL;
// if we have done only one merge, we're finished
if (nmerges <= 1) // allow for nmerges == 0, the empty list case
{
(*headPtr) = list;
(*tailPtr) = tail;
return;
}
// otherwise repeat, merging lists twice the size
insize *= 2;
}
}
Particle::Particle(PARTICLE_EFFECT *effect)
{
mEffect = effect;
mAge = 0.0f;
mX = 0.0f;
mY = 0.0f;
mZ = 0.0f;
mColor.argb = D3DCOLOR_ARGB(0xFF, 0xFF, 0x00, 0x00);
memset(&mVelocity, 0, sizeof(mVelocity));
memset(&mAcceleration, 0, sizeof(mAcceleration));
}
void Particle::Execute(Scalar dT)
{
if (!mEffect)
return;
if (IsAlive())
{
mX += mVelocity.x * dT;
mY += mVelocity.y * dT;
mZ += mVelocity.z * dT;
mVelocity.x += mAcceleration.x * dT;
mVelocity.y += mAcceleration.y * dT;
mVelocity.z += mAcceleration.z * dT;
mAge += dT;
Scalar percentComplete = mAge / mEffect->fragLifetime;
COLOR_POINT *cp0 = NULL;
COLOR_POINT *cp1 = NULL;
for (COLOR_POINT *cp = mEffect->colors + COLOR_POINT_COUNT - 2; cp >= mEffect->colors; cp--)
{
if (cp->active && percentComplete >= cp->time)
{
cp0 = cp;
cp1 = cp + 1;
break;
}
}
if (!cp0 || !cp1)
{
cp0 = mEffect->colors;
if (COLOR_POINT_COUNT > 1)
{
cp1 = mEffect->colors + 1;
} else
{
cp1 = cp0;
}
}
percentComplete = (percentComplete - cp0->time) / (cp1->time - cp0->time);
mColor.a = (unsigned char)(cp0->color.a + (cp1->color.a - cp0->color.a) * percentComplete);
mColor.r = (unsigned char)(cp0->color.r + (cp1->color.r - cp0->color.r) * percentComplete);
mColor.g = (unsigned char)(cp0->color.g + (cp1->color.g - cp0->color.g) * percentComplete);
mColor.b = (unsigned char)(cp0->color.b + (cp1->color.b - cp0->color.b) * percentComplete);
}
}
ParticleEmitter::ParticleEmitter()
: mEffect(NULL),
mActive(false),
mPosition(0.0f, 0.0f, 0.0f)
{
}
void ParticleEmitter::SetEffect(int effect)
{
mEffect = &ParticleEngine::mInstalledEffects[effect];
}
void ParticleEmitter::Execute()
{
if (mEffect == NULL)
return;
// calculate the number of particles we're going to need
if (mActive)
{
bool fire = false;
if (mEffect->id >= 1000)
{
// this is an independant effect
static Scalar lastEmitted = (Scalar)Now();
Scalar now = (Scalar)Now();
INDIE_EFFECT *indieEffect = (INDIE_EFFECT*)mEffect;
if ((now - lastEmitted) >= indieEffect->releasePeriod)
{
lastEmitted = now;
fire = true;
}
if ((now - mActivated) >= indieEffect->duration)
mActive = false;
}
else
{
fire = true;
mActive = false;
}
if (fire)
{
for (int i = 0; i < mEffect->fragCount; ++i)
ParticleEngine::CreateParticle(mPosition, mEffect);
}
}
}
//
// Drop everything bound to the device we were last given.
//
// Null-safe, and it clears what it drops. Neither was true before: this
// runs on the device-lost path ahead of a Reset, where a texture that
// never loaded (a missing VIDEO\particles.png is enough) left one of
// these NULL and took the Reset down with it, and a released pointer
// left in place is a dangling one the moment anything looks again.
//
void ParticleEngine::Destroy()
{
if (mVertBuffer != NULL)
{
mVertBuffer->Release();
mVertBuffer = NULL;
}
if (mParticleTexture != NULL)
{
mParticleTexture->Release();
mParticleTexture = NULL;
}
//
// The paint paths test this before touching anything, so clearing it
// makes the gap between a Destroy and the next Initialize safe.
//
mDevice = NULL;
}
void ParticleEngine::Initialize(LPDIRECT3DDEVICE9 device)
{
//
// Whatever is still held belongs to the PREVIOUS device, and holding
// it kept that device alive. A fresh renderer is built per mission,
// so a new device used to arrive here while the old one's vertex
// buffer (D3DPOOL_DEFAULT) and texture still referenced it -
// ~DPLRenderer's release never reached zero and the whole device
// survived the race that made it, back buffer and depth buffer and
// all. That is one leaked render target per race.
//
// The device-lost path already released before re-initialising; this
// is the same contract for the case where the device is not lost but
// replaced.
//
Destroy();
mDevice = device;
memset(mInstalledEffects, 0, sizeof(mInstalledEffects));
// particles left over from the previous mission (single-binary race
// loop) reference the old device's resources - drop them
while (mParticlesHead)
{
Particle *next = mParticlesHead->mNextParticle;
delete mParticlesHead;
mParticlesHead = next;
}
mParticlesTail = NULL;
mTotalParticleCount = 0;
ParticleEngine::mMaxParticleCount = atoi(getenv("MAXPARTICLES"));
// create the vertex buffer that will store the four vertices we need for the billboards
mDevice->CreateVertexBuffer(ParticleEngine::mMaxParticleCount * 6 * sizeof(L4BASICVERTEX),
D3DUSAGE_DYNAMIC,
L4BASICVERTEX_FVF,
D3DPOOL_DEFAULT,
&mVertBuffer,
NULL);
D3DXCreateTextureFromFile(mDevice, L"VIDEO\\particles.png", &mParticleTexture);
}
void ParticleEngine::InstallEffect(int effectNumber, PARTICLE_EFFECT effect)
{
mInstalledEffects[effectNumber] = effect;
mInstalledEffects[effectNumber].id = effectNumber;
}
void ParticleEngine::CreateParticle(D3DXVECTOR3 position, PARTICLE_EFFECT *effect)
{
if (effect == NULL || effect->fragCount <= 0)
return;
Particle *p = new Particle(effect);
// check to make sure we have room for this new particle
if (mTotalParticleCount + 1 >= ParticleEngine::mMaxParticleCount)
{
mTotalParticleCount--;
// there isn't room for this particle so we'll delete the oldest particle
if (mParticlesHead == mParticlesTail)
{
delete mParticlesHead;
mParticlesHead = NULL;
mParticlesTail = NULL;
}
else
{
Particle *temp = mParticlesTail;
mParticlesTail = mParticlesTail->mPrevParticle;
mParticlesTail->mNextParticle = NULL;
delete temp;
}
}
// insert the new particle into the linked list
if (mParticlesHead)
mParticlesHead->mPrevParticle = p;
p->mPrevParticle = NULL;
p->mNextParticle = mParticlesHead;
mParticlesHead = p;
if (!mParticlesTail)
mParticlesTail = p;
mTotalParticleCount++;
// now initialize all the particle data based on the effect
p->mX = position.x + ((float)rand() * 2.0f / RAND_MAX - 1.0f) * effect->varianceX;
p->mY = position.y + ((float)rand() * 2.0f / RAND_MAX - 1.0f) * effect->varianceY;
p->mZ = position.z + ((float)rand() * 2.0f / RAND_MAX - 1.0f) * effect->varianceZ;
D3DXVECTOR3 vec(
(float)rand() * 2.0f / RAND_MAX - 1.0f,
(float)rand() * 2.0f / RAND_MAX - 1.0f,
(float)rand() * 2.0f / RAND_MAX - 1.0f);
D3DXVec3Normalize(&vec, &vec);
p->mVelocity.x = vec.x * effect->velocity;
p->mVelocity.y = vec.y * effect->velocity;
p->mVelocity.z = vec.z * effect->velocity;
p->mAcceleration.x = 0.0f;
p->mAcceleration.y = effect->gravity;
p->mAcceleration.z = 0.0f;
// generate a random rotation amount in radians
float centerX = effect->textureBounds.left + (effect->textureBounds.right - effect->textureBounds.left) / 2.0f;
float centerY = effect->textureBounds.top + (effect->textureBounds.bottom - effect->textureBounds.top) / 2.0f;
D3DXVECTOR2 rotCenter(centerX, centerY);
D3DXMatrixTransformation2D(&p->mTextureTransform, NULL, 1, NULL, &rotCenter, ((float)rand() / RAND_MAX) * 2.0f * D3DX_PI, NULL);
p->mColor.argb = (effect->colors[0].active ? effect->colors[0].color.argb : 0);
}
int ParticleEngine::BuildParticleVertices(const Particle *p, L4BASICVERTEX *verts, D3DXMATRIX *inverse_view_matrix)
{
D3DXMATRIX rotation;
PARTICLE_EFFECT *effect = p->mEffect;
inverse_view_matrix->_41 = p->mX;
inverse_view_matrix->_42 = p->mY;
inverse_view_matrix->_43 = p->mZ;
float fragSize = effect->fragSize;
D3DXVECTOR3 vector3s[] =
{
D3DXVECTOR3(-fragSize, fragSize, 0.0f), // upper left
D3DXVECTOR3( fragSize, -fragSize, 0.0f), // lower right
D3DXVECTOR3(-fragSize, -fragSize, 0.0f), // lower left
D3DXVECTOR3(-fragSize, fragSize, 0.0f), // upper left
D3DXVECTOR3( fragSize, fragSize, 0.0f), // upper right
D3DXVECTOR3( fragSize, -fragSize, 0.0f) // lower right
};
D3DXVECTOR2 vector2s[] =
{
D3DXVECTOR2(effect->textureBounds.left, effect->textureBounds.top), // upper left
D3DXVECTOR2(effect->textureBounds.right, effect->textureBounds.bottom), // lower right
D3DXVECTOR2(effect->textureBounds.left, effect->textureBounds.bottom), // lower left
D3DXVECTOR2(effect->textureBounds.left, effect->textureBounds.top), // upper left
D3DXVECTOR2(effect->textureBounds.right, effect->textureBounds.top), // upper right
D3DXVECTOR2(effect->textureBounds.right, effect->textureBounds.bottom) // lower right
};
D3DXVECTOR3 normal, inNormal(0.0f, 0.0f, 1.0f);
D3DXVec3TransformCoord(&normal, &inNormal, inverse_view_matrix);
int count = (sizeof(vector3s) / sizeof(D3DXVECTOR3));
for (int i=0; i<count; i++)
{
D3DXVECTOR3 outVec3;
D3DXVECTOR2 outVec2;
D3DXVec3TransformCoord(&outVec3, &vector3s[i], inverse_view_matrix);
verts[i].x = outVec3.x;
verts[i].y = outVec3.y;
verts[i].z = outVec3.z;
verts[i].nx = normal.x;
verts[i].ny = normal.y;
verts[i].nz = normal.z;
if (effect->rotate)
D3DXVec2TransformCoord(&outVec2, &vector2s[i], &p->mTextureTransform);
else
outVec2 = vector2s[i];
verts[i].u = outVec2.x;
verts[i].v = outVec2.y;
verts[i].color = p->mColor.argb;
}
return count;
}
void ParticleEngine::ExecuteParticles(const D3DXMATRIX *view_matrix, Scalar timeSlice)
{
// if we don't have a device or we have no particles, early exit
if (!mDevice || mTotalParticleCount <= 0)
return; // we can't run without having been initialized with a device
// first we need to execute all the particles to update their positions, color, etc.
for (Particle *p = mParticlesHead; p;)
{
if (p->IsAlive())
{
p->Execute(timeSlice);
p = p->mNextParticle;
}
else
{
// remove from the list
if (p->mPrevParticle)
p->mPrevParticle->mNextParticle = p->mNextParticle;
if (p->mNextParticle)
p->mNextParticle->mPrevParticle = p->mPrevParticle;
// time to kill the particle
if (p == mParticlesHead)
{
mParticlesHead = p->mNextParticle;
}
if (p == mParticlesTail)
{
mParticlesTail = p->mPrevParticle;
}
Particle *temp = p;
p = p->mNextParticle;
mTotalParticleCount--;
delete temp;
}
}
// now we're going to sort the particle list based on distance to the camera
mergesort(&mParticlesHead, &mParticlesTail, view_matrix);
//Scalar beforeBuild = (Scalar)Now();
D3DXMATRIX inverse_view_matrix;
D3DXMatrixInverse(&inverse_view_matrix, NULL, view_matrix);
// and finally, we'll build the vertex list to send to Direct3D
L4BASICVERTEX *vertBuffer = NULL;
mVertBuffer->Lock(0, mTotalParticleCount * sizeof(L4BASICVERTEX), (void**)&vertBuffer, D3DLOCK_DISCARD);
int actualParticles = 0;
for (Particle *p = mParticlesHead; p; p = p->mNextParticle)
{
vertBuffer += BuildParticleVertices(p, vertBuffer, &inverse_view_matrix);
actualParticles++;
}
mVertBuffer->Unlock();
//Scalar afterBuild = (Scalar)Now();
//afterBuild = afterBuild;
}
void ParticleEngine::RenderParticles(const D3DXMATRIX *view_matrix, Scalar timeSlice)
{
// we always have to execute otherwise our timing
// variables get all screwed up when we don't run for a while
ExecuteParticles(view_matrix, timeSlice);
if (!mDevice || mTotalParticleCount <= 0)
return;
// setup stages for particle's texture
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
mDevice->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
mDevice->SetRenderState(D3DRS_EMISSIVEMATERIALSOURCE, D3DMCS_COLOR1);
mDevice->SetTexture(0, mParticleTexture);
mDevice->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);
mDevice->SetFVF(L4BASICVERTEX_FVF);
mDevice->SetStreamSource(0, mVertBuffer, 0, sizeof(L4BASICVERTEX));
//DWORD wrap_u, wrap_v;
//mDevice->GetSamplerState(0, D3DSAMP_ADDRESSU, &wrap_u);
//mDevice->GetSamplerState(0, D3DSAMP_ADDRESSV, &wrap_v);
//mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
//mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
D3DXMATRIX world_matrix;
D3DXMatrixIdentity(&world_matrix);
mDevice->SetTransform(D3DTS_WORLD, &world_matrix);
mDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, mTotalParticleCount * 2);
mDevice->SetRenderState(D3DRS_EMISSIVEMATERIALSOURCE, D3DMCS_MATERIAL);
mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR);
//mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, wrap_u);
//mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, wrap_v);
}