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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4c7f6fd9b1
commit
dca2586aa8
+133
-38
@@ -8197,6 +8197,75 @@ static void BTVerifyCockpitCanvasAfterReset(LPDIRECT3DDEVICE9 device)
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Execute Method, performs the rendering of one frame
|
||||
//
|
||||
//
|
||||
//#############################################################################
|
||||
// BTResetLostDevice -- the one true D3D9 device-loss recovery (#35).
|
||||
//
|
||||
// Both Present sites (the scene frame and the wait-screen idle frame) used to
|
||||
// carry an inline copy of this, and both copies were wrong the same way:
|
||||
// on DEVICELOST they called Reset() IMMEDIATELY. Reset() on a still-lost
|
||||
// device always fails; V() only logged it; ParticleEngine::Initialize then ran
|
||||
// against the lost device, its creates failed and NULLed the statics, and the
|
||||
// NEXT lost frame's ParticleEngine::Destroy() read a vtable at 0x0 -- the
|
||||
// field crash (8/8 byte-identical stacks at Destroy +0x11 on the one machine
|
||||
// whose GPU ever actually loses the device, a 128 MB Iris Xe).
|
||||
//
|
||||
// Correct protocol: release the POOL_DEFAULT resources every lost frame
|
||||
// (Destroy is idempotent now), but Reset ONLY once TestCooperativeLevel stops
|
||||
// answering D3DERR_DEVICELOST; until then skip and retry next frame. On a
|
||||
// successful Reset re-create ONLY the device objects -- NOT Initialize(),
|
||||
// which memsets the installed-effects table and killed every particle effect
|
||||
// for the rest of the mission on every reset that did succeed.
|
||||
//#############################################################################
|
||||
//
|
||||
void DPLRenderer::BTResetLostDevice()
|
||||
{
|
||||
if (mWaitOverlaySurface != NULL)
|
||||
{
|
||||
mWaitOverlaySurface->Release(); // pre-Reset, like every non-MANAGED resource
|
||||
mWaitOverlaySurface = NULL;
|
||||
}
|
||||
ParticleEngine::Destroy(); // idempotent + null-safe (#35)
|
||||
|
||||
HRESULT coop = mDevice->TestCooperativeLevel();
|
||||
if (coop == D3DERR_DEVICELOST)
|
||||
{
|
||||
static unsigned long s_lostLogAt = 0;
|
||||
unsigned long now_ms = GetTickCount();
|
||||
if (now_ms - s_lostLogAt > 2000)
|
||||
{
|
||||
s_lostLogAt = now_ms;
|
||||
DEBUG_STREAM << "[render] device LOST -- waiting for the driver "
|
||||
"before Reset" << std::endl << std::flush;
|
||||
}
|
||||
return; // cannot reset yet; retry next frame
|
||||
}
|
||||
|
||||
int bbCount = mPresentParams.BackBufferCount;
|
||||
int bbWidth = mPresentParams.BackBufferWidth;
|
||||
int bbHeight = mPresentParams.BackBufferHeight;
|
||||
|
||||
HRESULT reset_hr = mDevice->Reset(&mPresentParams);
|
||||
|
||||
mPresentParams.BackBufferCount = bbCount; // Reset writes actuals back;
|
||||
mPresentParams.BackBufferWidth = bbWidth; // keep the app's own intent
|
||||
mPresentParams.BackBufferHeight = bbHeight;
|
||||
|
||||
if (FAILED(reset_hr))
|
||||
{
|
||||
DEBUG_STREAM << "[render] device Reset FAILED hr=0x"
|
||||
<< std::hex << (unsigned long)reset_hr << std::dec
|
||||
<< " -- will retry next frame" << std::endl << std::flush;
|
||||
return;
|
||||
}
|
||||
|
||||
ParticleEngine::CreateDeviceObjects(mDevice); // NOT Initialize (effects table!)
|
||||
this->SetCoreRenderStates();
|
||||
BTVerifyCockpitCanvasAfterReset(mDevice); // Gitea #56 guard
|
||||
DEBUG_STREAM << "[render] device reset OK (lost -> restored)"
|
||||
<< std::endl << std::flush;
|
||||
}
|
||||
|
||||
void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::InterestingEntityIterator* all_iterator)
|
||||
{
|
||||
Component *component;
|
||||
@@ -8913,27 +8982,71 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
sAcc = 0.0; sFrames = 0; sMaxD = 0.0; sMaxP = 0.0;
|
||||
}
|
||||
}
|
||||
// #35 BENCH HOOK (BT_DEVICELOST_TEST=<frame>[,crashrepro], off by default).
|
||||
// A real device loss needs a GPU that actually hangs (Conn Man's Iris Xe);
|
||||
// no bench machine here can produce one on demand. Two modes:
|
||||
// <frame> force the DEVICELOST branch below at that render
|
||||
// frame and twice more at +600/+1200 -- drives the
|
||||
// REAL recovery code, not a copy of it.
|
||||
// <frame>,crashrepro run the exact field sequence at that frame:
|
||||
// teardown, a re-Initialize forced to fail
|
||||
// (MAXPARTICLES=0 -> CreateVertexBuffer(0) fails ->
|
||||
// NULL out-params), then a second teardown -- the
|
||||
// two-lost-frames-in-a-row shape from the field logs.
|
||||
{
|
||||
static long s_dltFrame = -2; // -2 unparsed, -1 off
|
||||
static long s_dltTick = 0;
|
||||
static int s_dltRepro = 0;
|
||||
if (s_dltFrame == -2)
|
||||
{
|
||||
const char *e = getenv("BT_DEVICELOST_TEST");
|
||||
s_dltFrame = -1;
|
||||
if (e != NULL && *e != '\0')
|
||||
{
|
||||
s_dltFrame = strtol(e, NULL, 10);
|
||||
if (s_dltFrame <= 0) s_dltFrame = -1;
|
||||
if (strstr(e, "crashrepro") != NULL) s_dltRepro = 1;
|
||||
}
|
||||
}
|
||||
if (s_dltFrame > 0)
|
||||
{
|
||||
++s_dltTick;
|
||||
if (s_dltRepro)
|
||||
{
|
||||
if (s_dltTick == s_dltFrame)
|
||||
{
|
||||
// The field null-teardown shape: Destroy releases + nulls,
|
||||
// the second Destroy runs against the NULL statics -- the
|
||||
// exact state the field crash died in. (The PRE-fix repro
|
||||
// of 2026-07-29 used Destroy / MAXPARTICLES=0-forced-
|
||||
// failed-Initialize / Destroy, and faulted at Destroy
|
||||
// +0x11 with target=0x0, matching all 8 field stacks; the
|
||||
// hardened Initialize defends against that injection now,
|
||||
// so the double-Destroy is the surviving regression form.)
|
||||
DEBUG_STREAM << "[dltest] crashrepro: Destroy / Destroy on NULL statics"
|
||||
<< std::endl << std::flush;
|
||||
ParticleEngine::Destroy();
|
||||
ParticleEngine::Destroy(); // field crash site
|
||||
DEBUG_STREAM << "[dltest] crashrepro SURVIVED; restoring device objects"
|
||||
<< std::endl << std::flush;
|
||||
ParticleEngine::CreateDeviceObjects(mDevice);
|
||||
}
|
||||
}
|
||||
else if (s_dltTick >= s_dltFrame
|
||||
&& ((s_dltTick - s_dltFrame) % 600) == 0
|
||||
&& (s_dltTick - s_dltFrame) <= 1200)
|
||||
{
|
||||
DEBUG_STREAM << "[dltest] forcing DEVICELOST branch (cycle "
|
||||
<< ((s_dltTick - s_dltFrame) / 600 + 1) << "/3)"
|
||||
<< std::endl << std::flush;
|
||||
hr = D3DERR_DEVICELOST;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hr == D3DERR_DEVICELOST)
|
||||
{
|
||||
if (mWaitOverlaySurface != NULL)
|
||||
{
|
||||
mWaitOverlaySurface->Release(); // D3DPOOL_DEFAULT: pre-Reset
|
||||
mWaitOverlaySurface = NULL;
|
||||
}
|
||||
int bbCount = mPresentParams.BackBufferCount;
|
||||
int bbWidth = mPresentParams.BackBufferWidth;
|
||||
int bbHeight = mPresentParams.BackBufferHeight;
|
||||
|
||||
ParticleEngine::Destroy();
|
||||
V(mDevice->Reset(&mPresentParams));
|
||||
ParticleEngine::Initialize(mDevice);
|
||||
this->SetCoreRenderStates();
|
||||
|
||||
mPresentParams.BackBufferCount = bbCount;
|
||||
mPresentParams.BackBufferWidth = bbWidth;
|
||||
mPresentParams.BackBufferHeight = bbHeight;
|
||||
|
||||
BTVerifyCockpitCanvasAfterReset(mDevice); // Gitea #56 guard
|
||||
BTResetLostDevice(); // #35: the guarded recovery
|
||||
}
|
||||
|
||||
ticks = HiResNowTicks();
|
||||
@@ -9199,27 +9312,9 @@ void DPLRenderer::ExecuteIdle()
|
||||
BTWaitScreenPaint(wait_line1, wait_line2);
|
||||
}
|
||||
if (present_hr == D3DERR_DEVICELOST)
|
||||
{
|
||||
if (mWaitOverlaySurface != NULL)
|
||||
{
|
||||
mWaitOverlaySurface->Release(); // D3DPOOL_DEFAULT: must go pre-Reset
|
||||
mWaitOverlaySurface = NULL;
|
||||
BTResetLostDevice(); // #35: the guarded recovery
|
||||
}
|
||||
int bbCount = mPresentParams.BackBufferCount;
|
||||
int bbWidth = mPresentParams.BackBufferWidth;
|
||||
int bbHeight = mPresentParams.BackBufferHeight;
|
||||
|
||||
ParticleEngine::Destroy();
|
||||
V(mDevice->Reset(&mPresentParams));
|
||||
ParticleEngine::Initialize(mDevice);
|
||||
this->SetCoreRenderStates();
|
||||
|
||||
mPresentParams.BackBufferCount = bbCount;
|
||||
mPresentParams.BackBufferWidth = bbWidth;
|
||||
mPresentParams.BackBufferHeight = bbHeight;
|
||||
|
||||
BTVerifyCockpitCanvasAfterReset(mDevice); // Gitea #56 guard
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user