d3d11: refcount per-HWND Metal view to fix ANGLE white screen (#183) - #184
d3d11: refcount per-HWND Metal view to fix ANGLE white screen (#183)#184seanellul wants to merge 3 commits into
Conversation
…#183) winemac.drv returns a per-window singleton Metal view; the swapchain create/released it per-swapchain with no refcount, so an ANGLE client that recreates its swapchain on the same HWND had the old swapchain's destructor detach the view the live swapchain presents into — silent white window. Refcount the view per HWND (create-before-lock, release-after-lock on the canonical handle, recycled-HWND detection). Single-swapchain apps are behavior-identical (one create, one release).
|
Hmm this is another proof of how fragile the integration between DXMT and Wine is. Is it legal to create multiple swapchain from the same HWND or are there some missing lockings? It would be still incorrect if the app expect two separate swapchains which share a single metal layer.
Any source code from ANGLE can prove that? |
…reation, not an in-surface resize ANGLE never recreates a surface's DXGI swapchain on resize (SwapChain11::reset early-returns to ResizeBuffers when a chain exists). The overlapping-lifetime trigger is an EGL client destroying its window surface while it is current: EGL defers destruction until the surface is no longer current, so the new surface's swapchain is created before the old one's is released. Comment-only change.
3502469 to
63ce29c
Compare
Comment-only. Plain punctuation, same content.
|
Took another look at the ANGLE source to answer this properly. The game ships ANGLE 2.1.23410 (git Basically: ANGLE doesn't recreate the swapchain on resize. The game recreates its EGL surface. At this revision a surface's swapchain is only created once. So the two Why the old swapchain is still alive when the new one is created This part is required by EGL. Not a timing accident. Destroying a surface that is still current only schedules the destruction: "If the EGL surface surface is not current to any thread, eglDestroySurface destroys it immediately. Otherwise, surface is destroyed when it becomes not current to any thread." (https://registry.khronos.org/EGL/sdk/docs/man/html/eglDestroySurface.xhtml) In ANGLE:
So for any EGL client that recreates its surface while it's current, the new swapchain is created before the old one is released, every time. I wanted to double check this, so I re-ran an instrumented build, once with the stock view lifetime, and once with this PR's refcount: Trace with stock view lifetime (probes only, no fix): the detach, then 1000+ presents into the orphaned viewWindow capture at t+30s: 100.0% white pixels, 1 distinct color. Same run with this PR's refcount: detach intercepted, game rendersWindow capture at t+30s: 0.2% white pixels, roughly 4,700 distinct colors (rendered title screen). Happy to attach the full logs if you need. The repro is a free Steam game, My Singing Monsters (appid 1419170). Is it legal? I believe so? It's a single-threaded deterministic sequence, so there is no missing locking. The app never holds two swapchains itself; the overlap comes from the EGL-mandated deferral. The chain is blt-model ( I can't find anything that forbids it for blt chains, and this exact destroy-then-recreate sequence is what the game does on Windows, where it renders fine. So I think the backend does have to tolerate briefly overlapping swapchain lifetimes on the same HWND. On two swapchains sharing one metal layer Agreed that would be incorrect if both were actually presenting. But that limitation comes from winemac.drv, which only has one metal view per window ( Happy to move the refcount into winemetal if you'd rather keep the policy there? |
Fixes #183.
Problem
Games that render through ANGLE (GLES over D3D11; they ship libEGL.dll + libGLESv2.dll) present a pure white window under DXMT. Audio and input work and there are no errors from the game, ANGLE, or DXMT. The same binaries render correctly under D3DMetal, so the game and ANGLE are fine; the gap is DXMT-side.
Root cause
winemac.drv's
newMetalViewWithDevice:returns a per-window singleton (if (_metalView) return _metalView;, no extra retain), andmacdrv_view_release_metal_viewdoes removeFromSuperview + release (dealloc). The D3D11 swapchain creates/releases that view per swapchain with no refcount.The repro game (My Singing Monsters, appid 1419170, free, 32-bit) destroys and recreates its EGL window surface during startup. EGL requires deferred destruction there: destroying a surface that is still current only schedules it, and the surface is really destroyed once it stops being current. ANGLE implements exactly that, so at the D3D level the order is always: swapchain #2 gets created on the same HWND first, then the old surface dies on the next eglMakeCurrent and swapchain #1's destructor runs. Both chains hold the same singleton view, so the old destructor detaches the view the live swapchain is presenting into. Every later present succeeds into an orphaned view and the window shows its blank background. Apps that create one swapchain per window never hit this.
Trace from an instrumented build (logging only, stock view lifetime):
Window capture during this run is 100% white. A diagnostic build that cleared every drawable to magenta still produced a pure white on-screen window, which is how the detached-layer theory was confirmed in the first place. ANGLE source links backing the surface-recreation mechanism are in the comments below.
Fix
A process-global refcounted HWND -> {view, layer, refcount} map in d3d11_swapchain.cpp. The view is created once per window and released only when the last swapchain on that HWND is destroyed. The tricky parts:
With the fix, the same startup sequence logs CREATE refcount=1, REUSE refcount=2, KEPT ALIVE refcount=1, and the game renders (window capture 0.2% white, ~4,700 distinct colors).
Why d3d11, not winemetal.so
The lifetime bug is a property of how the swapchain owns the view, so the refcount belongs with the swapchain's create/destroy. Keeping it PE-side also means it ships without an ABI-coupled winemetal.so rebuild. If you'd prefer the refcount to live in winemetal as a single source of truth for any future caller, happy to move it.
Testing
Known minor edge
g_metal_views.emplacecould throw std::bad_alloc and leak the freshly created view on OOM. It's an allocation-failure-only path, no worse than the surrounding code, so I left it unguarded to keep the diff minimal. Happy to wrap it if you want it hardened.