fix(💥): detach swapchains before destroying the device (fixes Android Canvas unmount crash) - #469
Conversation
…s unmount crash) When a Canvas unmounts on Android, releasing the wgpu::Surface runs ~Surface -> SwapChain::DetachFromSurface, which reads the device's FencedDeleter. If the GPUDevice was already destroyed (e.g. three's WebGPURenderer.dispose() calls device.destroy() synchronously in the same teardown), that FencedDeleter is freed and Dawn dereferences it: SIGSEGV on the Vulkan backend (fault addr 0x20). Metal tolerates the order. Surface.Unconfigure() alone is not enough because the swapchain is detached by ~Surface, not by Unconfigure(). GPUDevice::destroy() now detaches and releases every surface bound to the device (running ~Surface while the device is still alive) before _instance.Destroy(). The native window pointer is kept so the later view teardown still returns it.
|
Let me know if you can provide me with a reproducible example |
|
Standalone reproduction (RN 0.87.1, react-native-webgpu 0.9.0, three 0.184.0): https://github.com/eduardoborges/react-native-webgpu-unmount-repro Mount the medal, then hide it. On an Android emulator with a real Vulkan GPU ( |
wcandillon
left a comment
There was a problem hiding this comment.
That's a great find, thank you for this 🙏
|
@eduardoborges I've made a bunch of commits to your PR but I will revert it. I am considering a completely different approach to that problem |
|
@wcandillon no problem! feel at home |
|
@eduardoborges Sorry I messed your branch and thank you for bringing this to my attention. I've made a PR against Dawn to fix it: google/dawn#84 closing in favor of #470 |
Fixes the native crash reported in #468.
The bug
When a
<Canvas>unmounts on Android, the app crashes about 2 out of 3 times with a SIGSEGV in Dawn's Vulkan backend:iOS (Metal) never crashes with the same code.
Root cause
three.js's
WebGPURenderer.dispose()callsdevice.destroy()synchronously in the React effect cleanup. On Android the native view is dropped one frame later, soSurfaceInfo::detach()releases awgpu::Surfacewhose swapchain is still attached.~Surfacethen runsSwapChain::DetachFromSurfaceImpl, which reads the already-destroyed device'sFencedDeleter. That pointer is freed, so Dawn dereferences null at0x20.Calling
Surface.Unconfigure()at destroy time is not enough. What touches the device is the swapchain detach inside~Surface, so the surface has to be released (its destructor has to run) while the device is still alive.The fix
GPUDevice::destroy()now walks theSurfaceRegistryand, for everySurfaceInfobound to this device, unconfigures and releases thewgpu::Surfaceright away, before_instance.Destroy(). Releasing it runs~SurfaceandSwapChain::DetachFromSurfaceagainst a live device. The native window pointer stays in_nativeSurface, so the laterSurfaceInfo::detach()from the view teardown still returns the window to the platform.Two small additions, with no change on the happy path.
SurfaceInfo::unconfigureIfDevice(device)detaches and releases the surface when it belongs todevice, andSurfaceRegistry::unconfigureDevice(device)calls it for every entry.Verification
Pixel emulator, Android API 37,
-gpu host(real Vulkan via virtio-gpu),minSdkVersion26, three.jsWebGPURendererrendering a glTF in a<Canvas>inside a React Navigation screen.Before the fix: navigate in, wait for the first frame, press back, repeat. Crashes 2 out of 3 times.
After the fix: the same loop with
renderer.dispose()called immediately in cleanup, no workaround on the app side. Five clean exits out of five, same PID throughout. Instrumentation confirms thedetachafter teardown now seessurface=0, already released.It also runs on iOS, where
unconfigureDeviceis a cheap walk that does nothing but keeps both platforms on the same teardown path.A standalone reproduction is at https://github.com/eduardoborges/react-native-webgpu-unmount-repro.