Skip to content

fix(🐛): pump an imported device's async work on the instance that owns it - #590

Merged
wcandillon merged 2 commits into
wcandillon:mainfrom
ryuhzk:fix/imported-device-owning-instance
Oct 23, 2026
Merged

wcandillon merged 2 commits into
wcandillon:mainfrom
ryuhzk:fix/imported-device-owning-instance

Conversation

@ryuhzk

@ryuhzk ryuhzk commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #589. Complements #582 (#581): that change makes react-native-webgpu share Skia's instance when librnskia.so is loaded first. This one covers the case where the instances are not shared, which after #584 was closed still depends on import order on Android, and is the only case for an exporter other than Skia.

Problem

importDevice binds the imported device to the calling runtime's RuntimeContext for the shared WebGPURuntime instance. If the device belongs to another instance, that context's pump calls ProcessEvents() on the wrong instance and the device's AllowProcessEvents callbacks never run: createRenderPipelineAsync, popErrorScope and mapAsync stay pending.

Changes

  • RNWebGPU::importDevice gets the instance from the device itself (device.GetAdapter().GetInstance()) and binds the device to the context for that instance. It throws if Dawn reports no instance.
  • RuntimeContext is now one per (JS runtime, Dawn instance) pair instead of one per runtime. The runtime data holds a map keyed by WGPUInstance; get takes the instance; getOrCreate adds to the existing map instead of replacing the runtime data. Each context keeps its own pump, mailbox and pump counter, so the shared instance and an imported device's instance are pumped independently on the same runtime thread. processEventsMutex stays process-wide.

Nothing changes when the instance is shared: there is a single map entry and getOrCreate(runtime, _async->instance()) in the request/response ops (#425) returns the same context as before. Since those call sites already pass the object's own instance, an imported device and the objects created from it keep resolving to the context of their instance on any calling runtime.

Testing

Not built in this environment: I could not build Dawn or the example app here, and I did not run yarn test. What I did run on this branch:

  • cpplint with the package's flags on the three files: no findings.
  • clang-format on the changed lines: clean (the remaining differences in these files are pre-existing lines on main).
  • clang++ -fsyntax-only of RuntimeContext.cpp and of a translation unit including RNWebGPU.h, against the Dawn 157.0.0 headers from react-native-webgpu-dawn and React Native 0.88 headers: no errors.

The same change, as a patch on react-native-webgpu 0.17.0, ships in the Android app Muqun (Play preview builds) with react-native-skia 3.7.3. Its home screen imports Skia's Graphite device with importDevice(Skia.getNativeDevice()), renders a 3D scene on it and captures React Native views with drawElementImageToTexture. Before the patch its pipeline creation and error scopes hung when the instances were not shared; with it they settle. The diff here is that patch ported to main with reworded comments; the code is the same apart from line wrapping.

…s it

importDevice() bound the imported device to a RuntimeContext for the
shared WebGPURuntime instance. When the exporter's device lives on a
different instance (react-native-skia loaded after react-native-webgpu,
or any other exporter), that pump never processes the device's events
and createRenderPipelineAsync, popErrorScope and mapAsync stay pending.

Resolve the device's own instance through its adapter, and keep one
RuntimeContext per (runtime, instance) pair so each instance has a pump.
@wcandillon
wcandillon self-requested a review October 23, 2026 06:05
@ryuhzk ryuhzk closed this Oct 23, 2026
@ryuhzk ryuhzk reopened this Oct 23, 2026
…vice

Follow-up to the per-(runtime, instance) RuntimeContext:

- ProcessEvents() was serialized by one process-wide mutex. Pumping an
  imported device's instance can block on that device's own lock (Skia's
  Graphite device during a submit), which stalled the pumps of our own
  instance on other runtimes. The mutex is now per instance, resolved
  once per context.
- getOrCreate() reads the runtimeData slot once and fills the map in a
  single pass; get() is gone, instance() returns a const reference and
  the unreachable null-instance check in importDevice is removed.
- Comments and the public JSDoc no longer claim that importDevice needs
  the exporter to share webgpu's instance, or that native init order
  does not matter (on Android it decides whether Skia's instance is
  adopted).
- ImportDevice.spec.ts imports Skia's Graphite device on the Tests
  screen and awaits popErrorScope, createRenderPipelineAsync,
  onSubmittedWorkDone and mapAsync on it. The example app installs
  webgpu before Skia, so on Android the device lives on a second Dawn
  instance: the spec hangs on the previous binding and passes with
  this one (verified on a Pixel 8, full suite green).
@wcandillon
wcandillon merged commit c523753 into wcandillon:main Oct 23, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.19.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

ryuhzk added a commit to osuki-dev/muqun-app that referenced this pull request Oct 23, 2026
…drop the WebGPU patch (#151)

react-native-webgpu 0.19.0 contains our upstream fix
(wcandillon/react-native-webgpu#590): an imported Skia device's
asynchronous work is pumped on the Dawn instance that owns it. It also
contains #582, which finds Skia's Dawn instance on Android. The local
patch for this is removed.

react-native-skia 3.7.4 is a fix release: Canvas children render with
React 19.2's reconciler, canvases share a recorder pool, and Android
canvases present off the main thread.

Both use Dawn 157, so the shared GPU stack stays on one copy. This changes
the native fingerprint and needs a new store build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Imported device's async work never settles when its instance is not the default one (load order after #582)

2 participants