Repository navigation
bugfix(dx8): Unbind device resources before Reset - #3450
tintinhamans wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
Walkthrough
ChangesDX8 device reset handling
Priority: ⬆️ High Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Suggested reviewers: Merge Risk: ⚪ Minimal · up to
|
[Medium impact] The PR appears safe to merge; no new blocking issues were found.
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
ba7740ea-2f70-4b9c-91b2-34285ac708e6
📒 Files selected for processing (2)
Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cppCore/Libraries/Source/WWVegas/WW3D2/dx8wrapper.h
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.
316e960 to
338fa02
Compare
bobtista
left a comment
There was a problem hiding this comment.
ok sweet so device-loss recovery can succeed, and retries after a failed Reset stop touching the device. Comments and variable names seem fine. LGTM
| //Need to explicitly set texture to null, otherwise app will not be able to | ||
| //set it to null because of redundant state checker. MW | ||
| if (_Get_D3D_Device8()) | ||
| if (_Get_D3D_Device8() && !DeviceResetFailed) |
There was a problem hiding this comment.
Is this a hack? The EA comment is confusing too.
There was a problem hiding this comment.
Not meant as a hack. After a failed Reset() the device should only get TestCooperativeLevel, Reset or Release, so I skip the SetTexture there. Added a note on DeviceResetFailed.
There was a problem hiding this comment.
Basically the device is not in a state to make use of certain functions and only a small subset of the API can be called on it at this point.
There was a problem hiding this comment.
the flag is gone now, see the second commit
There was a problem hiding this comment.
It’s back. I tested a Reset() that keeps failing: after a failed Reset() the next attempt crashes in SetStreamSource even though TestCooperativeLevel says the device can be reset. So the device isn’t touched until a Reset() succeeds, everything else is still released every attempt.
338fa02 to
c2678ec
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
b3984891-d68e-408c-9564-ff7054eb7204
📒 Files selected for processing (2)
Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cppCore/Libraries/Source/WWVegas/WW3D2/dx8wrapper.h
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
| { DX8CALL_HRES(Reset(&_PresentParameters),hr) | ||
| if (hr != D3D_OK) | ||
| { | ||
| // TheSuperHackers @bugfix arcticdolphin 09/10/2026 Reset() fails while the device still holds the last draw's resources. |
There was a problem hiding this comment.
Reworded for clarity - device and wrapper still hold references on textures and buffers of the last draw and Reset() fails while those are there
c2678ec to
3670989
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
9af5d55f-cbe8-44ab-9755-3007f0c0ebe3
📒 Files selected for processing: 1
Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.
xezon
left a comment
There was a problem hiding this comment.
I gave this to Claude to Review. Here are some findings:
Review
Unbinding the device's textures, streams and index buffer before Reset() addresses the real cause. Set_Vertex_Buffer(nullptr) and Set_Index_Buffer(nullptr) only update the wrapper. The device keeps its own reference to the last-drawn (default-pool) buffers, so Reset() fails. The pre-reset Invalidate_Cached_Render_States() and Set_Render_Target(nullptr) also correctly drop the wrapper's own references to textures and render targets.
My concerns are about the new DeviceResetFailed flag. Findings 1–4 all come from it. I think dropping the flag and always unbinding is both simpler and more robust. Findings 6 and 7 predate this PR and don't block it.
Findings in this change
1. A retry after a failed Reset no longer unbinds anything
Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp line 670 (Unbind_Device_Resources), together with line 435 (Invalidate_Cached_Render_States).
After one failed Reset(), DeviceResetFailed stays true until a Reset succeeds. On the next Reset_Device() call, Unbind_Device_Resources() returns early and Invalidate_Cached_Render_States() skips SetTexture(a, nullptr). The retry therefore depends on nothing having been bound on the device since the failure.
That isn't guaranteed. GUIEdit's EditWindow::draw ignores the result of WW3D::Begin_Render and still runs winRepaint(). That binds a default-pool dynamic VB/IB and textures through Apply_Render_State_Changes(), and a lost device accepts these calls. Then:
End_Render→End_Scene→PresentreturnsD3DERR_DEVICELOST.TestCooperativeLevelreturnsD3DERR_DEVICENOTRESET, soReset_Device()runs again.DynamicVBAccessClass::_Deinit()drops the wrapper's reference, but the unbind is skipped, so the device still holds the dynamic VB.Reset()fails again, the flag stays set, and every later retry repeats this. The device never recovers.
WorldBuilder's ObjectPreview also renders without checking Begin_Render. In the game itself, the draw paths are mostly guarded by TestCooperativeLevel, so this is less likely there, but the fix shouldn't depend on that.
Suggestion: always unbind before Reset(). That call is exactly what makes Reset() succeed, so it shouldn't depend on an earlier failure.
2. While the flag is set, the texture cache stops matching the device
dx8wrapper.cpp line 435.
Invalidate_Cached_Render_States() is public, and game code calls it mid-frame (W3DBridgeBuffer.cpp:1195, W3DVolumetricShadow.cpp:3659 and :3677). While DeviceResetFailed is set, it now sets Textures[a] = nullptr without calling SetTexture(a, nullptr) on the device. The next Set_DX8_Texture(a, nullptr) is then filtered out as redundant (dx8wrapper.h:913), so the old texture stays bound. That is the exact problem MW's comment above this line warns about.
This only matters if the device can render while the flag is set. One way: Reset() is rejected (for example for invalid present parameters) but TestCooperativeLevel still returns D3D_OK, and the caller ignores the failure (Set_Swap_Interval does). I haven't confirmed that the D3D8 runtime behaves this way. Still, the flag is sticky, and only a successful Reset or Create_Device() clears it, so any such state lasts the rest of the session.
3. The flag's documented rule is only checked in two places
dx8wrapper.h line 638.
The comment says that after a failed Reset, "the device only accepts TestCooperativeLevel, Reset and Release". But only Unbind_Device_Resources() and one call in Invalidate_Cached_Render_States() check the flag. The same failed-state paths keep calling other device methods:
Set_Render_Target(nullptr)in the same pre-reset block (dx8wrapper.cpp:640) callsSetRenderTargetwheneverDefaultRenderTargetis held.Shutdown()callsSet_Render_Target(nullptr)beforeRelease_Device().m_pCleanupHook->ReleaseResources()→W3DShaderManager::shutdown()callsDeletePixelShaderbefore the flag is ever checked.- Tool render paths (see 1) keep issuing draw calls.
So either calls on a failed or lost device are harmless (lost-device behaviour: they succeed and do nothing), and the flag isn't needed, or they aren't harmless, and the flag would have to cover far more than two sites. I'd go with the first and remove the flag, which also fixes 1, 2 and 4.
Question: was the flag added because the debug build asserted in Unbind_Device_Resources() (DX8CALL → Log_DX8_ErrorCode → WWASSERT(0)) after a failed Reset? If so, I'd rather fix it at the reporting level (see 6) than skip the unbind.
4. Unbind_Device_Resources silently does nothing after a failed Reset
dx8wrapper.cpp line 668.
The function name and its comment promise to unbind textures, streams and indices, but it returns early depending on global state. At both call sites, Reset_Device() and Release_Device() look like they always unbind, which hides the problem in 1. If some form of the flag stays, please check it in the caller that needs it, Release_Device(), so the helper does what its name says.
5. Texture stages are unbound twice before Reset
dx8wrapper.cpp lines 638–639.
Before Reset(), Unbind_Device_Resources() calls SetTexture(stage, nullptr) for all 8 stages, then Invalidate_Cached_Render_States() does the same for every stage again. That's 8 redundant calls per Reset, and two texture-unbind loops that have to be kept in sync.
Suggestion: have Unbind_Device_Resources() handle only streams and indices, and use Invalidate_Cached_Render_States() for textures, since it also clears the wrapper's Textures[] cache:
// Unbinds streams and indices on the device. Set_Vertex_Buffer and Set_Index_Buffer only update the wrapper until the next draw.
void DX8Wrapper::Unbind_Device_Resources()
{
for (unsigned stream = 0; stream < MAX_VERTEX_STREAMS; ++stream) {
DX8CALL(SetStreamSource(stream, nullptr, 0));
}
DX8CALL(SetIndices(nullptr, 0));
} HRESULT hr=_Get_D3D_Device8()->TestCooperativeLevel();
if (hr != D3DERR_DEVICELOST )
{
// TheSuperHackers @bugfix arcticdolphin 09/10/2026 Release everything the device and the wrapper still reference.
// Reset() fails while default pool resources are alive.
Unbind_Device_Resources();
Invalidate_Cached_Render_States();
Set_Render_Target((IDirect3DSurface8*)nullptr);
DX8CALL_HRES(Reset(&_PresentParameters),hr)
if (hr != D3D_OK)
return false; //reset failed.
}Release_Device() would then need to unbind textures itself, either by keeping its own SetTexture loop or by calling Invalidate_Cached_Render_States(), which also fixes 7.
Pre-existing issues (not caused by this PR, fine as follow-ups)
6. A failed Reset asserts in debug builds
dx8wrapper.cpp line 641.
DX8CALL_HRES(Reset(...), hr) passes a failing result to DX8_ErrorCode(), and Log_DX8_ErrorCode() hits WWASSERT(0) in WWDEBUG builds. WW3D::Begin_Render retries Reset_Device() on every frame that sees D3DERR_DEVICENOTRESET, so a persistent failure asserts every frame, and the recovery path can't really be tested in a debug build. This was already the case before the PR, but the PR now treats a failed Reset as an expected, recoverable state. A raw call with Non_Fatal_Log_DX8_ErrorCode() or WWDEBUG_SAY would fit better.
7. The Textures[] cache is never released at shutdown
dx8wrapper.cpp line 686 (Release_Device), together with line 344 (Shutdown).
Shutdown() releases Textures[i] only inside if (CurrentCaps). But it calls Release_Device() first, which reaches Do_Onetime_Device_Dependent_Shutdowns(), which deletes CurrentCaps and sets it to nullptr. So the loop never runs. The textures Set_DX8_Texture() AddRef'd into Textures[] keep their references after D3DDevice->Release(), and the D3D debug runtime will report them as leaked. Unbind_Device_Resources() clears only the device side. This predates the PR, and Release_Device() is the natural place for the wrapper-side release (see the suggestion in 5).
3670989 to
87b9883
Compare
8a22d7b to
3489fb3
Compare
|
I removed the flag and added a second commit that releases everything once per device loss and stops drawing while the device is lost, which covers 1 and 3. A failed Reset no longer asserts, and the texture leak is in a separate PR: #3543. Kept the texture unbind in |
3489fb3 to
3a4004c
Compare
xezon
left a comment
There was a problem hiding this comment.
Claude says:
Review (round 2)
Thanks for the update. Most of the first round is resolved:
- ✅
DeviceResetFailedis gone, andUnbind_Device_Resources()now runs before everyReset(), so a retry no longer depends on nothing having been bound since the last failure. - ✅
Invalidate_Cached_Render_States()is back to an unconditionalSetTexture(a, nullptr), so the texture cache can't drift from the device any more. - ✅ The helper no longer hides a skip condition.
- ✅ A failed
Reset()now logs throughNon_Fatal_Log_DX8_ErrorCode()and no longer asserts in debug builds.
The new release-once logic introduces a regression (1). Most of the new DeviceState machinery seems unnecessary now that the unbind always runs (2–4).
Findings in this change
1. Default-pool resources are released only once per reset cycle
Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp line 615.
Because of ResourcesReleasedForReset, Release_Reset_Resources() runs only on the first attempt. That function covers DX8TextureManagerClass::Release_Textures(), m_pCleanupHook->ReleaseResources() and WW3D::_Invalidate_Textures(). Any D3DPOOL_DEFAULT resource created between attempts is therefore never released, and every later Reset() fails.
One way this happens:
Reset_Device()releases everything and sets the flag. Then eitherReset()fails (for exampleD3DERR_OUTOFVIDEOMEMORY, or a default-pool resource this fix still misses), orTestCooperativeLevel()returnsD3DERR_DEVICELOSTat line 621. The latter can happen whenSet_Device_Resolution,Set_Swap_IntervalorSet_Render_DevicecallsReset_Device()directly, or when the user alt-tabs again between the check inBegin_Renderand the one inReset_Device.- Game logic keeps running. A map load runs
W3DShroud::init()→ReAcquireResources()→new TextureClass(..., POOL_DEFAULT), andDX8TextureManagerClasstracks every such texture. Water and projected-shadow render targets and the cleanup hook's dynamic buffers are recreated in similar ways. - On the next attempt the flag skips
Release_Textures()and the cleanup hook. The new D3D texture stays alive andReset()fails again. The flag staystrue, so the device never recovers.
Before this change, both releases ran on every attempt, and they are safe to repeat:
TextureTrackerClass::Release()just sets the D3D texture tonullptr.- The shader
shutdown()functions zero their handles. - The hooks use
REF_PTR_RELEASE.
Suggestion: release everything on every attempt and drop ResourcesReleasedForReset. Then Release_Dynamic_Buffers() doesn't need to be a separate step either:
if ((IsInitted) && (D3DDevice != nullptr)) {
// Release all non-MANAGED stuff
Release_Reset_Resources(); // includes the dynamic buffers
HRESULT hr=_Get_D3D_Device8()->TestCooperativeLevel();
if (hr == D3DERR_DEVICELOST) {
return false; //device is lost and can't be reset.
}
// TheSuperHackers @bugfix arcticdolphin 09/10/2026 Reset() fails while resources are still bound
Unbind_Device_Resources();
Invalidate_Cached_Render_States();
Set_Render_Target((IDirect3DSurface8*)nullptr);
hr = _Get_D3D_Device8()->Reset(&_PresentParameters);
Increment_DX8_CallCount();
if (hr != D3D_OK)
{
Non_Fatal_Log_DX8_ErrorCode(hr,__FILE__,__LINE__);
return false; //reset failed.
}
Restore_Reset_Resources(reload_assets);
WWDEBUG_SAY(("Device reset completed"));
return true;
}If the concern was the per-frame cost of WW3D::_Invalidate_Textures() while retrying, it would be fine to run only that part once. It only affects managed textures, and those don't block Reset().
2. The draw check covers only one of many draw paths
dx8wrapper.cpp line 2076.
// Skip draws on a lost device holds only for DX8Wrapper::Draw(). Many draw paths bypass it:
Draw_Sorting_IB_VB()(dx8wrapper.cpp:2052), which draws all sorted (translucent) geometry.- Direct
DrawIndexedPrimitive/DrawPrimitivecalls inW3DWater,W3DVolumetricShadow,W3DProjectedShadow,W3DShaderManager,W3DSmudge,W3DSnowandW3DScene.
As far as I know, D3D already treats draws on a lost device as no-ops. Any bindings those draws leave behind are now cleared by the unconditional unbind before Reset(). So this check doesn't make Reset() work. It adds a special case to the hottest call in the wrapper and promises something the code doesn't deliver.
Suggestion: remove the check. If you hit a concrete problem without it (an assert or a crash), please describe it, so we can fix it where every draw passes through.
3. Release_Device() stops unbinding on a lost device
dx8wrapper.cpp line 730.
The // Not safe on a lost device claim isn't backed up, and it contradicts the rest of the PR: Reset_Device() unbinds while the device is in D3DERR_DEVICENOTRESET. Retail always unbound here, and D3DDevice->Release() follows right after, so skipping the unbind gains nothing. It does:
- change retail behaviour without a
TheSuperHackerscomment, and - add a
TestCooperativeLevel()call that also updatesDeviceStateto a teardown path.
Suggestion: restore the unconditional Unbind_Device_Resources(). If you saw a specific failure here, please document it in the comment.
4. The DeviceState machinery only feeds the draw check
dx8wrapper.h line 642.
The following exist only for the check in 2, since Is_Device_Lost() has no callers:
- the
DeviceStateTypeenum andDeviceState Test_Cooperative_Level()andUpdate_Device_State()- the rewritten call sites in
End_Scene,Flip_To_PrimaryandRelease_Device - the
ww3d.cppchange
It is also incomplete: W3DDisplay::draw (W3DDisplay.cpp:1978) and W3DShroud::render (W3DShroud.cpp:533) still call TestCooperativeLevel() directly, so there are two sources of truth for the device state.
Suggestion: if 2 goes, revert all of this to the original IsDeviceLost handling, or drop it entirely, since nothing reads it.
5. The new comments in Reset_Device() describe the wrong mechanism
dx8wrapper.cpp lines 613 and 630.
// Draws can recreate these between attempts: with the draw check, draws are skipped while the device is not OK. The dynamic VB and IB are recreated when frame code constructsDynamicVBAccessClassandDynamicIBAccessClass, not byDraw(). The comment also suggests that dynamic buffers are the only thing recreated between attempts, which is how 1 slipped through.// Retried next frame: that's true only for theBegin_Render,End_SceneandFlip_To_Primarypaths.Set_Device_Resolution,Set_Swap_IntervalandSet_Render_Devicereturn the failure to their caller, andW3DDisplay::setDisplayMode()immediately retries with the old mode.
6. Texture stages are still unbound twice before Reset()
dx8wrapper.cpp lines 626–627.
This carries over from the last round. Unbind_Device_Resources() calls SetTexture(stage, nullptr) for all 8 stages, then Invalidate_Cached_Render_States() does it again. While a reset keeps failing, that now happens once per frame.
Suggestion: have Unbind_Device_Resources() handle only streams and indices, and use Invalidate_Cached_Render_States() for textures. In Release_Device(), that would also fix 7.
Pre-existing issue (not caused by this PR, fine as a follow-up)
7. The Textures[] cache is never released at shutdown
dx8wrapper.cpp line 725 (Release_Device), together with line 344 (Shutdown).
Shutdown() releases Textures[i] only inside if (CurrentCaps). But Release_Device() → Do_Onetime_Device_Dependent_Shutdowns() has already deleted CurrentCaps and set it to nullptr, so the loop never runs. The textures Set_DX8_Texture() AddRef'd into Textures[] outlive D3DDevice->Release(), and the D3D debug runtime reports them as leaked.
| @@ -610,67 +610,127 @@ bool DX8Wrapper::Reset_Device(bool reload_assets) | |||
| WWDEBUG_SAY(("Resetting device.")); | |||
| DX8_THREAD_ASSERT(); | |||
| if ((IsInitted) && (D3DDevice != nullptr)) { | |||
There was a problem hiding this comment.
While essentially rewriting the whole function, might as well make this an early return here.
| m_pCleanupHook->ReAcquireResources(); | ||
| } | ||
| } | ||
| Invalidate_Cached_Render_States(); |
There was a problem hiding this comment.
Why does this call a Invalidate function on Restore? Maybe it should be outside of this function and documented why it is called if it is not obvious?
There was a problem hiding this comment.
The release and restore helpers are gone, so the invalidate is back where it was, right after Reset().
3a4004c to
781c10f
Compare
|
This is back to almost the first version. Everything is released on every attempt again like retail, and the release-once logic, device state and draw check are gone. I did keep the flag: I tested a With the flag the device isn't touched until a A failed Reset no longer asserts, and textures are only unbound once since Skipping rendering in the tools when |
781c10f to
54bf04c
Compare
54bf04c to
c6bab1f
Compare
Reset()was called while the device could still hold the last draw's buffers, textures and render target, becauseSet_Vertex_BufferandSet_Index_Bufferare deferred.Reset_Devicenow unbinds the streams and indices on the device through a newUnbind_Device_Buffers(), whichRelease_Deviceuses too, and invalidates the cached render states and resets the render target beforeReset(). A failedReset()no longer asserts in debug.To reproduce, leave a render target bound and force a device loss. On main
Reset()then fails on every frame and never recovers, on both NVIDIA and Intel. With this change it resets on the first try.For context, Generals Online's crash reports show this as an access violation in
DestroyResource, which is not a game function. The game frames end atDX8Wrapper::Reset_Device. Below it,Reset()ind3d8.dllhas the display driver destroy the device's resources, including the ones this change now unbinds first. Intel'sigd9trinity32.dllis on the crashing stack in 279 of 281 reports of the last 90 days, and in 567 of 570 for a release that only released the render target beforeReset(). Their Sentry has about 8,800 of these since December 2025. The same change is in GeneralsOnlineDevelopmentTeam#634.