Skip to content

fix(💥): detach swapchains before destroying the device (fixes Android Canvas unmount crash) - #469

Closed
eduardoborges wants to merge 4 commits into
wcandillon:mainfrom
eduardoborges:fix/android-canvas-unmount-crash
Closed

fix(💥): detach swapchains before destroying the device (fixes Android Canvas unmount crash)#469
eduardoborges wants to merge 4 commits into
wcandillon:mainfrom
eduardoborges:fix/android-canvas-unmount-crash

Conversation

@eduardoborges

@eduardoborges eduardoborges commented Sep 2, 2026

Copy link
Copy Markdown

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:

F libc    : Fatal signal 11 (SIGSEGV), fault addr 0x20
  dawn::native::vulkan::FencedDeleter::DeleteWhenUnused
  dawn::native::vulkan::SwapChain::DetachFromSurfaceImpl
  dawn::native::Surface::~Surface
  rnwgpu::SurfaceInfo::detach(bool)
  Java_com_webgpu_WebGPUView_onViewDestroyed

iOS (Metal) never crashes with the same code.

Root cause

three.js's WebGPURenderer.dispose() calls device.destroy() synchronously in the React effect cleanup. On Android the native view is dropped one frame later, so SurfaceInfo::detach() releases a wgpu::Surface whose swapchain is still attached. ~Surface then runs SwapChain::DetachFromSurfaceImpl, which reads the already-destroyed device's FencedDeleter. That pointer is freed, so Dawn dereferences null at 0x20.

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 the SurfaceRegistry and, for every SurfaceInfo bound to this device, unconfigures and releases the wgpu::Surface right away, before _instance.Destroy(). Releasing it runs ~Surface and SwapChain::DetachFromSurface against a live device. The native window pointer stays in _nativeSurface, so the later SurfaceInfo::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 to device, and SurfaceRegistry::unconfigureDevice(device) calls it for every entry.

Verification

Pixel emulator, Android API 37, -gpu host (real Vulkan via virtio-gpu), minSdkVersion 26, three.js WebGPURenderer rendering 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 the detach after teardown now sees surface=0, already released.

It also runs on iOS, where unconfigureDevice is 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.

…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.
@wcandillon

Copy link
Copy Markdown
Owner

Let me know if you can provide me with a reproducible example

@eduardoborges

Copy link
Copy Markdown
Author

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 (-gpu host) the app crashes on hide without this PR, and survives every toggle with it.

@wcandillon wcandillon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great find, thank you for this 🙏

@wcandillon
wcandillon self-requested a review September 8, 2026 19:33
@wcandillon

Copy link
Copy Markdown
Owner

@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

@eduardoborges

Copy link
Copy Markdown
Author

@wcandillon no problem! feel at home

@wcandillon

Copy link
Copy Markdown
Owner

@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
Now I am on the verge to merge/publish a workaround for it in the meantime: #470

closing in favor of #470

@wcandillon wcandillon closed this Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants