Files
BT411/engine/MUNGA_L4/L4PARTICLES.h
T
Joe DiPrimaandClaude Fable 5 dca2586aa8 the Owens crash: a device reset that never waited for the device (#35)
Eight byte-identical field stacks from night 6, all one player, all in an
Owens: ParticleEngine::Destroy +0x11, access=0 target=0x0, from the plain
per-frame render path. Nothing in the stack touches weapons or the Owens.
Conn Man's Surface Pro 9 (Iris Xe, 128 MB shared) is simply the only GPU in
the fleet that ever actually LOSES the D3D9 device -- his two-trigger
missile+laser bursts are what provoke the timeout, not what crashes.

What crashed is our device-loss handling, which was wrong three ways at once,
in two inline copies (the scene Present and the wait-screen Present):

  1. On D3DERR_DEVICELOST it called Reset() IMMEDIATELY. Reset on a
     still-lost device ALWAYS fails, and V() only logs. There was no
     TestCooperativeLevel gate at all.
  2. It then ran ParticleEngine::Initialize against the lost device. The
     creates fail there and NULL their out-params -- proven, not assumed:
     the bench repro faults at target=0x0, not at a dangling address.
  3. The next lost frame called ParticleEngine::Destroy again, which
     Release()d those NULLs blind. Read of vtable at 0x0. Dead.

So: lost frame 1 tears down and leaves NULLs, lost frame 2 crashes. Two
frames, every time, deterministic -- which is exactly why all 8 field stacks
are byte-identical.

Reproduced before fixing. BT_DEVICELOST_TEST=<frame>,crashrepro runs the
field sequence on the bench; on the unfixed build it died at Destroy +0x11,
access=0 target=0x0, and symbolized to the same four frames as the field
logs. Same shape, same offsets-modulo-hook. That run also proved the
out-param-nulling assumption the whole diagnosis rested on.

The fix -- one shared DPLRenderer::BTResetLostDevice() replacing both inline
copies:

  - Destroy() is idempotent and null-safe, and nulls after release.
  - Reset() is gated on TestCooperativeLevel() != D3DERR_DEVICELOST; while
    the driver still says lost, skip the frame and retry.
  - The Reset HRESULT is checked; on failure, log and retry next frame
    instead of driving on.
  - On success, re-create via the new CreateDeviceObjects(), NOT
    Initialize(): Initialize memsets the installed-effects table, so every
    reset that DID succeed silently killed all particle effects for the rest
    of the mission. The quieter sibling bug, fixed by the same split.
  - Initialize checks its HRESULTs and defends MAXPARTICLES<=0; the draw
    paths guard the NULL buffer, and ExecuteParticles keeps draining
    particles while the engine is dormant so they cannot pile up.

Verified: the crashrepro shape now logs SURVIVED and play continues; three
forced full loss/reset cycles each log "[render] device reset OK"; a plain
run is assert-free.

Found while verifying, worth its own line: VIDEO\particles.png has NEVER
existed -- not in the tree, not in BTL4.RES, not anywhere in git history.
The texture load has failed on every machine since the engine was written,
and every billboard particle ever rendered was untextured quads via
SetTexture(0, NULL). RenderParticles deliberately does NOT gate on the
texture -- that would disable all particles everywhere; untextured IS the
shipped look. Filed separately; a real particle sheet is a content task.

The field verification that counts is Conn Man flying his exact crash
loadout on this build: instead of a dead process he should see at worst a
brief hitch and "[render] device reset OK" in his log. #35 stays open until
that happens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 08:21:41 -05:00

164 lines
3.4 KiB
C++

#pragma once
#include <memory.h>
#include <d3d9.h>
#include <d3dx9.h>
#include "../munga/time.h"
#include "l4d3d.h"
#define COLOR_POINT_COUNT 10
#define MAX_PARTICLE_EFFECTS 32
#define EFFECT_GROUPING_EPSILON 1.0f
#define L4BASICVERTEX_FVF (D3DFVF_XYZ | D3DFVF_TEX1 | D3DFVF_DIFFUSE | D3DFVF_NORMAL)
struct L4BASICVERTEX
{
float x, y, z;
float nx, ny, nz;
DWORD color;
float u, v;
};
union L4COLOR
{
DWORD argb;
struct
{
unsigned char b;
unsigned char g;
unsigned char r;
unsigned char a;
};
};
struct COLOR_POINT
{
bool active;
Scalar time;
L4COLOR color;
};
struct PARTICLE_EFFECT
{
int id;
struct
{
float left, top, right, bottom;
} textureBounds;
bool rotate;
float fragSize;
float velocity;
COLOR_POINT colors[COLOR_POINT_COUNT];
float gravity;
float varianceX;
float varianceY;
float varianceZ;
int fragCount;
float fragLifetime;
float maxRepeat;
};
struct INDIE_EFFECT : public PARTICLE_EFFECT
{
int maxIssue;
float releasePeriod;
float duration;
};
class ParticleEngine;
class ParticleEmitter;
class Particle
{
friend class ParticleEngine;
public:
Particle(PARTICLE_EFFECT *effect);
void Execute(Scalar dT);
inline bool IsAlive() { return (mEffect && ( mAge < mEffect->fragLifetime)); }
inline Particle *Next() { return mNextParticle; }
inline Particle *Prev() { return mPrevParticle; }
private:
float mX, mY, mZ;
L4COLOR mColor;
D3DXVECTOR3 mVelocity;
D3DXVECTOR3 mAcceleration;
D3DXMATRIX mTextureTransform;
PARTICLE_EFFECT *mEffect;
Scalar mAge;
Particle *mNextParticle;
Particle *mPrevParticle;
friend int compare(const Particle *, const Particle *, const D3DXMATRIX *);
friend void mergesort(Particle **, Particle **, const D3DXMATRIX *);
};
class ParticleEmitter
{
public:
ParticleEmitter();
bool IsActive() { return mActive; }
void SetEffect(int effect);
void SetEffect(INDIE_EFFECT *effect) { mEffect = effect; }
float GetMaxRepeat() { return mEffect->maxRepeat; }
void Execute();
void Fire() { mActive = true; }
void Start()
{
mActive = true;
mActivated = (Scalar)Now();
}
void SetPosition(float x, float y, float z)
{
mPosition.x = x;
mPosition.y = y;
mPosition.z = z;
}
private:
PARTICLE_EFFECT *mEffect;
bool mActive;
Scalar mActivated;
D3DXVECTOR3 mPosition;
};
class ParticleEngine
{
friend class ParticleEmitter;
public:
static void Destroy();
static void Initialize(LPDIRECT3DDEVICE9 device);
// #35: the device-RESET path re-creates ONLY the D3D objects. Initialize()
// is full startup init -- it also wipes the installed-effects table, which
// a mid-mission reset must never do (it silently killed every particle
// effect for the rest of the mission).
static void CreateDeviceObjects(LPDIRECT3DDEVICE9 device);
static void InstallEffect(int effectNumber, PARTICLE_EFFECT effect);
static void RenderParticles(const D3DXMATRIX *view_matrix, Scalar timeSlice);
private:
static void CreateParticle(D3DXVECTOR3 position, PARTICLE_EFFECT *effect);
static int BuildParticleVertices(const Particle *p, L4BASICVERTEX *verts, D3DXMATRIX *view_matrix);
static void ExecuteParticles(const D3DXMATRIX *view_matrix, Scalar timeSlice);
static LPDIRECT3DDEVICE9 mDevice;
static PARTICLE_EFFECT mInstalledEffects[];
static LPDIRECT3DTEXTURE9 mParticleTexture;
static bool mActiveParticles;
static LPDIRECT3DVERTEXBUFFER9 mVertBuffer;
static Particle *mParticlesHead;
static Particle *mParticlesTail;
static long mTotalParticleCount;
static long mMaxParticleCount;
};