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>
This commit is contained in:
Cyd
2026-08-08 19:56:13 -05:00
co-authored by Claude Opus 5
parent 3482de5147
commit 4410febd4c
2 changed files with 49 additions and 2 deletions
+39 -2
View File
@@ -249,14 +249,51 @@ void ParticleEmitter::Execute()
}
}
//
// 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()
{
mVertBuffer->Release();
mParticleTexture->Release();
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));
+10
View File
@@ -3605,6 +3605,16 @@ DPLRenderer::~DPLRenderer()
// the next race of the single-binary loop - drop them with the device
d3d_OBJECT::FlushTextureCache();
//
// The particle engine is one of those caches and was missed. Its
// vertex buffer is D3DPOOL_DEFAULT and its texture belongs to this
// device, so while they were held the release below never reached
// zero: every race left a whole live device behind it, and the next
// race's Initialize was the only thing that ever let one go. Drop
// them here and the device dies with the mission that made it.
//
ParticleEngine::Destroy();
SAFE_RELEASE(mDevice);
SAFE_RELEASE(gD3D);
//STUBBED: DPL RB 1/14/07