From dca2586aa897b23ac833f6dfe8eabc5a9e0e75cb Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 29 Jul 2026 08:21:41 -0500 Subject: [PATCH] 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=,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 --- context/decomp-reference.md | 1 + context/multiplayer.md | 4 +- context/wintesla-port.md | 24 +++++ engine/MUNGA_L4/L4PARTICLES.cpp | 96 ++++++++++++++++-- engine/MUNGA_L4/L4PARTICLES.h | 5 + engine/MUNGA_L4/L4VIDEO.cpp | 171 +++++++++++++++++++++++++------- engine/MUNGA_L4/L4VIDEO.h | 6 ++ 7 files changed, 259 insertions(+), 48 deletions(-) diff --git a/context/decomp-reference.md b/context/decomp-reference.md index fa1e0cb..3cdd759 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -473,6 +473,7 @@ default-ON (`'0'` disables). | `BT_SELF_DAMAGE=` | dispatch an unaimed `TakeDamage` at your OWN mech once a second, through the real `Entity::Dispatch` path, so the whole RESPAWN family is bench-testable solo (nothing else can kill the local pilot: `BT_MP_FORCE_DMG` only targets replicants). **Latches off at first death** so everything after the respawn is the respawn's doing, not the harness still shooting you | | `BT_POWER_DETACH_TEST=` | drop a subsystem's voltage link + force Auto, so the auto-hunt must recover it. `1` = first powered subsystem to tick; a NAME (`PPC_1`, `Myomers`) targets one, which is what proves FAILOVER to a different generator rather than a same-generator re-attach | | `BT_AUDIO_SOURCES=` | request `n` OpenAL mono sources instead of the driver default (~256). **Opt-in on purpose** — the cap doubles as a governor, and with EFX reverb live a higher ceiling means more simultaneous voices mixing during heavy combat. Measure frame time. See [[wintesla-port]] | +| `BT_DEVICELOST_TEST=[,crashrepro]` | #35 bench hook. `` forces the D3D9 DEVICELOST branch at that render frame (+600/+1200 = 3 cycles), driving the REAL `BTResetLostDevice` recovery. `,crashrepro` runs the field null-teardown shape (double `ParticleEngine::Destroy`) — pre-fix this reproduced the field crash byte-for-byte (`Destroy +0x11`, `target=0x0`); post-fix it must log `SURVIVED`. See [[wintesla-port]] §Device-loss | Full render/locomotion gates (BT_RAMP, BT_MATPRI, BT_CULL, BT_SHADOW_*, BT_LODSEL, BT_ADDLOD, BT_PUNCH, …) are catalogued in [[rendering]]. Warp visuals: [[translocation-warp]]. diff --git a/context/multiplayer.md b/context/multiplayer.md index 892d770..76e5393 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -676,7 +676,9 @@ Only the relay host needs port forwarding. Verified: scripted protocol suite in 2-node mission launched + stopped purely over the socket. ## The interest-teardown audio crash (2026-07-24) [T2 stack-captured + fixed] -THE layout-shifting release-only crash (weeks of field flakes, #35's prime suspect) finally +THE layout-shifting release-only crash (weeks of field flakes; was #35's prime suspect until +2026-07-29 CLEARED it — #35 turned out to be the ParticleEngine device-reset null-deref, root-caused +from 8 symbolized field stacks and fixed; see [[wintesla-port]] §Device-loss) finally hit a crash-filter build: `InterestManager::OrphanInterestOrigin → RemoveUninterestingEntity → DestroyEntityAudioObjects → {Static,Dynamic}3DPatchSource::IsAudioSourceClipped` — AV reading NULL+0x38: the authentic burning-mech death-silence check dereferences diff --git a/context/wintesla-port.md b/context/wintesla-port.md index c538ae3..c8c3ba5 100644 --- a/context/wintesla-port.md +++ b/context/wintesla-port.md @@ -16,6 +16,30 @@ on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`. ## What WinTesla already did (do NOT rebuild) - **Renderer bypass: DONE** — `MUNGA_L4/L4D3D.cpp` + `DXUtils` replace libDPL / the IG board with Direct3D9. No `libdpl.lib` in the tree. (The early from-scratch D3D9 viewer was retired and removed.) [T1] +- **Device-loss: the #35 "Owens crash" — ROOT-CAUSED + FIXED 2026-07-29** [T2, bench-reproduced]. + 8 byte-identical field stacks (one player, Surface Pro 9 / **Iris Xe 128 MB** — the only GPU in + the fleet that ever actually loses the device): `ParticleEngine::Destroy +0x11`, `access=0 + target=0x0`, from the per-frame render path. **Not a weapons bug** — the Owens two-trigger + missile+laser combo is merely what provokes the GPU timeout on that iGPU. The broken protocol + (both Present sites, inline copies): on `D3DERR_DEVICELOST` they called `Reset()` IMMEDIATELY — + which **always fails while still lost** (`V()` only logged) — then `ParticleEngine::Initialize()` + ran against the lost device, its creates failed and **NULLed the out-params** (verified: the bench + repro faulted at `target=0x0`, not a dangling address), and the NEXT lost frame's blind + `mVertBuffer->Release()` read a vtable at 0x0. FIX (`L4VIDEO.cpp` + `L4PARTICLES.cpp`): one shared + `DPLRenderer::BTResetLostDevice()` — release POOL_DEFAULT resources every lost frame (idempotent, + null-safe `Destroy`), **gate `Reset()` on `TestCooperativeLevel() != D3DERR_DEVICELOST`**, check + the Reset HRESULT, and on success re-create via **`CreateDeviceObjects()` — NOT `Initialize()`**, + whose `memset(mInstalledEffects)` silently killed every particle effect for the rest of the + mission on each reset that DID succeed (the quieter sibling bug). Draw paths guard the NULL buffer + (`ExecuteParticles` keeps draining particles while dormant). Bench: `BT_DEVICELOST_TEST` repro'd + the field crash byte-for-byte pre-fix, and post-fix the same shape logs `SURVIVED` + 3 forced + loss/reset cycles recover (`[render] device reset OK`). + ⚠ **Discovery from the verify: `VIDEO\particles.png` has NEVER existed** — absent from the tree, + the RES, and all of git history — so `mParticleTexture` has been NULL on every machine since the + engine was written and **all billboard particles draw as untextured quads** (`SetTexture(0,NULL)` + = the shipped look). Deliberately NOT gated on in `RenderParticles` — that would disable all + particles everywhere. Filed on the tracker; a real texture (or the original particle sheet) is a + content task. Possibly related to the field "missile artifact looks wrong" observation. - **Audio SOURCE BUDGET — the dropout complaint was fixed 2026-07-23; the leftover is NOT the same bug** [T2, 2026-07-28]. **The reported bug is closed**: `a999e5c` (per-source delete instead of the spec-atomic bulk `alDeleteSources`) stopped the pool leaking, and there have been **no field diff --git a/engine/MUNGA_L4/L4PARTICLES.cpp b/engine/MUNGA_L4/L4PARTICLES.cpp index 599cdc4..3cdd4fc 100644 --- a/engine/MUNGA_L4/L4PARTICLES.cpp +++ b/engine/MUNGA_L4/L4PARTICLES.cpp @@ -1,5 +1,7 @@ #include "l4particles.h" #include "../munga/time.h" +#include "../munga/style.h" // DEBUG_STREAM (#35 diagnostics) +#include LPDIRECT3DDEVICE9 ParticleEngine::mDevice = NULL; PARTICLE_EFFECT ParticleEngine::mInstalledEffects[MAX_PARTICLE_EFFECTS]; @@ -251,26 +253,89 @@ void ParticleEmitter::Execute() void ParticleEngine::Destroy() { - mVertBuffer->Release(); - mParticleTexture->Release(); + // #35 (the field "Owens crash"). This runs on the DEVICELOST path, and a + // device can stay lost for several frames -- so this must be idempotent + // and null-safe. It used to Release() blind: the first lost frame + // released the buffer, the failed re-create on the still-lost device left + // both statics NULL, and the SECOND lost frame's Release() read the + // vtable at 0x0 (8/8 field stacks byte-identical at Destroy +0x11; + // reproduced on the bench with BT_DEVICELOST_TEST=,crashrepro). + if (mVertBuffer != NULL) + { + mVertBuffer->Release(); + mVertBuffer = NULL; + } + if (mParticleTexture != NULL) + { + mParticleTexture->Release(); + mParticleTexture = NULL; + } } void ParticleEngine::Initialize(LPDIRECT3DDEVICE9 device) { - mDevice = device; + // Full STARTUP init only. The device-reset path must use + // CreateDeviceObjects() below -- coming through here would memset the + // installed-effects table and kill every particle effect for the rest of + // the mission (#35's quieter sibling). memset(mInstalledEffects, 0, sizeof(mInstalledEffects)); - 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), + const char *max_env = getenv("MAXPARTICLES"); + ParticleEngine::mMaxParticleCount = (max_env != NULL) ? atoi(max_env) : 0; + if (ParticleEngine::mMaxParticleCount <= 0) + ParticleEngine::mMaxParticleCount = 8192; // btl4main defaults the env; belt + braces + + CreateDeviceObjects(device); +} + +void ParticleEngine::CreateDeviceObjects(LPDIRECT3DDEVICE9 device) +{ + // (Re)create the D3D resources. #35: CHECK the HRESULTs -- on a lost + // device (or an out-of-video-memory iGPU, the field machine is a 128 MB + // Iris Xe) these FAIL and null their out params; the old code drove + // straight on, and the NULLs then killed the next Destroy(). On failure + // we log and leave the engine dormant -- Execute/Render guard on the + // NULLs, particles drain but don't draw -- and the next successful reset + // brings it back. + mDevice = device; + + mVertBuffer = NULL; + mParticleTexture = NULL; + + // create the vertex buffer that will store the six vertices we need for the billboards + HRESULT hr = mDevice->CreateVertexBuffer( + ParticleEngine::mMaxParticleCount * 6 * sizeof(L4BASICVERTEX), D3DUSAGE_DYNAMIC, L4BASICVERTEX_FVF, D3DPOOL_DEFAULT, &mVertBuffer, NULL); + if (FAILED(hr)) + { + mVertBuffer = NULL; // belt + braces vs runtime behavior + DEBUG_STREAM << "[particles] CreateVertexBuffer FAILED hr=0x" + << std::hex << (unsigned long)hr << std::dec + << " (max=" << ParticleEngine::mMaxParticleCount + << ") -- particles dormant until the next successful reset" + << std::endl << std::flush; + } - D3DXCreateTextureFromFile(mDevice, L"VIDEO\\particles.png", &mParticleTexture); + // NB: VIDEO\particles.png has never shipped (verified 2026-07-29: absent + // from the tree, the RES, and all of git history), so this load has + // failed on every machine since the engine was written and particles + // draw untextured. The failure is expected + logged once; rendering + // proceeds without the texture (SetTexture(0, NULL) = the shipped look). + hr = D3DXCreateTextureFromFile(mDevice, L"VIDEO\\particles.png", &mParticleTexture); + if (FAILED(hr)) + mParticleTexture = NULL; + + if (mVertBuffer != NULL) + { + DEBUG_STREAM << "[particles] device objects created (max=" + << ParticleEngine::mMaxParticleCount + << ", texture=" << (mParticleTexture != NULL ? "loaded" : "MISSING (untextured quads -- the shipped look)") + << ")" << std::endl << std::flush; + } } void ParticleEngine::InstallEffect(int effectNumber, PARTICLE_EFFECT effect) @@ -453,6 +518,12 @@ void ParticleEngine::ExecuteParticles(const D3DXMATRIX *view_matrix, Scalar time } } + // #35: with the device objects torn down (device lost mid-reset) the + // update/expire loop above must still run -- particles keep draining -- + // but there is no buffer to build into. + if (mVertBuffer == NULL) + return; + // now we're going to sort the particle list based on distance to the camera mergesort(&mParticlesHead, &mParticlesTail, view_matrix); @@ -484,7 +555,14 @@ void ParticleEngine::RenderParticles(const D3DXMATRIX *view_matrix, Scalar timeS // variables get all screwed up when we don't run for a while ExecuteParticles(view_matrix, timeSlice); - if (!mDevice || mTotalParticleCount <= 0) + // (#35: the buffer is legitimately NULL while the device is lost. The + // TEXTURE is deliberately NOT part of this gate: VIDEO\particles.png has + // never existed -- not in the tree, the RES, or git history -- so + // mParticleTexture has been NULL on every machine since the engine was + // written, and SetTexture(0, NULL) drawing untextured quads IS the + // shipped look. Gating on it would silently disable all billboard + // particles everywhere.) + if (!mDevice || mVertBuffer == NULL || mTotalParticleCount <= 0) return; // setup stages for particle's texture diff --git a/engine/MUNGA_L4/L4PARTICLES.h b/engine/MUNGA_L4/L4PARTICLES.h index 49fe90c..27f4e3a 100644 --- a/engine/MUNGA_L4/L4PARTICLES.h +++ b/engine/MUNGA_L4/L4PARTICLES.h @@ -136,6 +136,11 @@ class ParticleEngine 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); diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 01a8657..c0acade 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -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=[,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: + // 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. + // ,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 - } } } // diff --git a/engine/MUNGA_L4/L4VIDEO.h b/engine/MUNGA_L4/L4VIDEO.h index 6ed5999..98d0363 100644 --- a/engine/MUNGA_L4/L4VIDEO.h +++ b/engine/MUNGA_L4/L4VIDEO.h @@ -306,6 +306,12 @@ public: // dpl_ZONE* MakeNewZone(); + // #35: the one true device-loss recovery -- both Present sites route here. + // Correct D3D9 protocol: release POOL_DEFAULT resources, then Reset ONLY + // once TestCooperativeLevel says the device is ready; a Reset while still + // lost always fails, and the old inline copies drove on past that failure. + void BTResetLostDevice(); + void MarkDCSHiearchy(dpl_DCS *root_DCS, Entity *entity); void FlushBitSliceTexture(unsigned int *local_storage); void LoadBitSliceTexture(BitMap *bitmap_to_load, LPDIRECT3DTEXTURE9 local_storage);