Skip to content

fix(🧵): isolate WebGPU state across runtime reloads#427

Open
NikitaDudin wants to merge 3 commits into
wcandillon:mainfrom
NikitaDudin:fix/expo-updates-reload-lifecycle
Open

fix(🧵): isolate WebGPU state across runtime reloads#427
NikitaDudin wants to merge 3 commits into
wcandillon:mainfrom
NikitaDudin:fix/expo-updates-reload-lifecycle

Conversation

@NikitaDudin

Copy link
Copy Markdown

Fix WebGPU runtime lifecycle across Expo Updates reloads

Summary

Fixes WebGPU becoming unusable after expo-updates.reloadAsync().

Applications use WebGPU when they need direct and deterministic control over rendering and image quality. Reloading an update should replace the JavaScript runtime without leaving native GPU resources attached to the previous runtime.

Previously, the React Native runtime could be destroyed while process-wide WebGPU state survived. Depending on timing, the new runtime could encounter stale contexts, surfaces, JSI objects, or asynchronous callbacks owned by the previous runtime. This could result in a lost or black canvas after reload and introduced lifetime and thread-safety risks around runtime teardown.

Root cause

An Expo Updates reload recreates the React Native runtime without restarting the application process.

Several WebGPU objects could live longer than the runtime that created them:

  • runtime contexts and CallInvoker instances;
  • cached JSI prototypes and Promise callbacks;
  • pending asynchronous GPU operations and event listeners;
  • manager and surface registry entries;
  • native view callbacks arriving after runtime invalidation.

These objects were not consistently associated with a specific runtime generation. A newly installed runtime could therefore observe state belonging to the previous one, while pending native work could still attempt to dispatch callbacks to an invalid runtime.

Key changes

Runtime session ownership

  • Introduces a unique RNWebGPUSession for every native module installation.
  • Associates managers, canvases, surface registry entries, and native views with their owning session.
  • Rejects callbacks and surface operations belonging to an inactive or replaced session.
  • Exposes the session identifier internally so native canvas views cannot attach to state created by a previous runtime.

Deterministic invalidation

  • Invalidates the WebGPU session before the React Native runtime is destroyed.
  • Cancels pending asynchronous tasks and prevents further Promise or event delivery.
  • Clears runtime-owned manager and surface state during module teardown.
  • Ensures a newly installed runtime always starts with fresh WebGPU state.

JSI and async lifetime safety

  • Keeps prototype caches and Promise state scoped to the owning JSI runtime instead of process-wide storage.
  • Prevents asynchronous completions from accessing a destroyed jsi::Runtime.
  • Routes callbacks through the owning runtime context and CallInvoker.
  • Makes task cancellation and callback removal explicit during invalidation.

Native surface stabilization

  • Makes Android and Apple surface attachment session-aware.
  • Ignores delayed view callbacks from obsolete runtime sessions.
  • Balances native surface ownership across attach, detach, replacement, and teardown paths.
  • Prevents a canvas created after reload from aliasing a surface or context from the previous runtime.

Stability guarantees

The lifecycle now follows a small set of explicit invariants:

  • JSI values never cross runtime sessions.
  • An inactive session cannot deliver async callbacks.
  • Surface entries cannot be reused by a different runtime session.
  • Runtime teardown cancels native work before releasing runtime-owned state.
  • Reloading produces the same clean WebGPU installation state as the initial application launch.

The public WebGPU API remains unchanged. The session identifier is an internal lifecycle detail used by the native bridge and canvas implementation.

Diagnostics

Adds a runtime reload diagnostic to the example application.

The diagnostic:

  • creates an adapter and device;
  • registers a device.lost callback;
  • starts pending asynchronous GPU operations;
  • triggers a React Native reload;
  • displays the current WebGPU session identifier.

After a healthy reload, reopening the diagnostic should show a new session identifier and successfully create a fresh adapter and device.

Verification

  • git diff --check — clean.
  • yarn workspace react-native-webgpu tsc — passed.
  • iOS Debug simulator build for the react-native-webgpu scheme — succeeded.
  • Android :app:assembleDebug -x lint -x test — succeeded.
  • Independent review focused on correctness, native lifetime, and thread safety found no actionable issues.

Not covered here: an on-device end-to-end run using an actual expo-updates.reloadAsync() update. The included diagnostic currently exercises the same React Native runtime invalidation lifecycle through DevSettings.reload; an Expo Updates smoke test is recommended before merge.

@wcandillon

Copy link
Copy Markdown
Owner

This is a great suggestion thank you! Now #426 which is ready to ship and fixes many substancials will create a lot of conflict with this PR but we can do it.
It looks like you have a reproducible example in apps/example? could we start by merging this first in a separate PR?

@wcandillon

Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

@NikitaDudin

Copy link
Copy Markdown
Author

Thanks! Yes — the example is the ReloadLifecycle diagnostic in apps/example. It starts pending WebGPU work and triggers DevSettings.reload() to recreate the React Native runtime, so it is a minimal lifecycle regression case rather than a full expo Updates.reloadAsync() E2E test.
I agree that it should be merged separately first. I’ll extract a version compatible with the current main into a small PR, without the sessionId display since that is introduced by #427. Then I’ll remove the example-only changes from #427.
I’ve also merged the latest main, including #426, into #427 and resolved the conflicts.

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