Repository navigation
feat!: run on react-native-worklets instead of react-native-worklets-core - #349
Open
eduardoborges wants to merge 23 commits into
Open
eduardoborges wants to merge 23 commits into
eduardoborges wants to merge 23 commits into
Conversation
…core Replaces react-native-worklets-core with react-native-worklets, the worklet runtime of Reanimated 4. Continues margelo#327. Native - The Filament worklet runtime is a react-native-worklets runtime created from JS with a custom AsyncQueue that forwards jobs to the render thread (WorkletAsyncQueue). Its global dispatcher runs jobs under the runtime lock (WorkletRuntimeDispatcher). - Every native call into a worklet runtime (frame callbacks, promise resolution, listeners) holds the react-native-worklets runtime mutex (WorkletRuntimeLock). The runtime is touched from other threads by hot reload, custom serializables and shared value reads, so entering it from the render thread without the lock traps in Hermes' reentrancy check. - The runtime collector global is renamed so it no longer clobbers the one react-native-worklets installs. - iOS gets the runtime and CallInvoker from RCTBridgeProxy and RCTCallInvokerModule; RCTCxxBridge is gone in React Native 0.87. - Android links the react-native-worklets prefab, compiles as C++20 and uses the folly flags its headers need. The CallInvoker comes from ReactContext. JS - Hooks run worklets with runOnRuntimeAsync / scheduleOnRuntime. - Reanimated shared values are accepted directly. Listeners live on the UI runtime (Reanimated only allows them there) and forward changes to the Filament runtime by listener id (useSharedValueListener). - Cross-runtime state (surface liveness, animator state, aspect ratio, last applied transform) uses createSynchronizable. - Render callbacks are kept in React state; the FilamentView re-installs its frame listener when the list changes. - useSyncSharedValue and useDerivedValue are removed: Reanimated's own shared values and useDerivedValue work directly. Peers: react-native-worklets >= 0.12, react-native-reanimated >= 4.
…own their asset Render callbacks are registered in a registry on the Filament runtime. Adding and removing them are jobs on that runtime, so they stay ordered with the release of the resources a callback uses. Keeping the list in React state made the FilamentView re-install its frame listener only after a re-render, and a frame could still run the old callback after its asset was destroyed. AnimatorWrapper and FilamentInstanceWrapper now hold a shared_ptr to the FilamentAsset that owns the gltfio Animator and FilamentInstance they wrap. A frame callback that still holds an animator after the asset was released no longer reads freed memory (SIGSEGV in Animator::getAnimationCount on unmount).
withCleanupScope deferred the release with InteractionManager.runAfterInteractions, which throws on React Native 0.87. A setTimeout does the same job: it runs after the other cleanup functions of the commit.
Without ndkVersion the Android Gradle Plugin of React Native 0.87 picks NDK 28 while React Native and the other modules use NDK 27. The libc++ ABIs differ and dlopen fails with a missing __cxa_init_primary_exception.
… subclass of AsyncQueue react-native-worklets finds the queue of a custom runtime with a dynamic_pointer_cast on the object it gets from JS. Android's libc++ compares type_info by pointer, and the type_info of worklets::AsyncQueue is a weak symbol every library gets its own copy of, bound in dlopen order. A subclass defined here fails the cast (silently: null queue, then an assert on the first schedule) whenever libworklets.so loads before this library, which Reanimated makes the common case. AsyncQueueUI is defined inside libworklets.so, so its type chain always matches. The Filament queue is now an AsyncQueueUI with a UIScheduler that forwards every job to the render thread.
Hermes finalizes HybridObjects on its GC thread (hades). When such a wrapper holds the last reference to a Filament resource, the resource deleter calls JDispatcher::runAsync from that thread, which is not attached to the JVM, and fbjni throws inside a destructor chain: std::terminate, "Unable to retrieve jni environment". Wrap scheduleTrigger() and the destructor in jni::ThreadScope::WithClassLoader like the other native finalizers do.
… with them Cycling every example screen exposed two teardown races that the old InteractionManager delay used to hide: - The recorder (and view) native window could be released while Filament still had createSwapChain queued on its backend thread, crashing inside eglCreateWindowSurface. The swapchain deleter now owns the surface or recorder until the swapchain is destroyed and flushed. - Releasing a material while a renderable still referenced one of its instances hit a Filament precondition abort. Material deleters now detach such renderables first, and RenderableManager.destroyEntity() lets components free shapes created with createPlane, createImageBackgroundShape or createDebugCubeWireframe. BackgroundImage and DebugBox use it.
…d up StrictMode runs the useBuffer effect twice. The first load could resolve after its cleanup scheduled the release, so the released buffer landed in state and the next createMaterial failed with "already been manually released". Also document that useWorkletCallback returns a JS trampoline that must not be captured inside another worklet or handed to native callbacks.
…allback The Stress screen pushes and pops every example screen for a number of rounds and logs progress as [stress:<platform>]. The 250 ms variant unmounts scenes while their assets are still loading. PhysicsCoin passed a useWorkletCallback trampoline to Bullet, which cannot call it from the worklet runtime. CastShadow destroys its shadow plane before the material is released.
tsc in examples/Shared failed on navigation.navigate('Test') because the
untyped useNavigation() resolves the route name to never.
…ting after unmount A buffer that finishes loading after its effect cleaned up has no consumer, so useBuffer releases it even when releaseOnUnmount is false. Under a fast mount and unmount loop those buffers piled up until Hermes collected the wrappers and the Android emulator ran out of Java heap while loading assets. Skybox now also releases its texture buffer on unmount; the KTX path copies the bytes into a bundle, so nothing else needs it. findFilamentView rejects when the native view is already gone. FilamentView swallows that only when the component unmounted in the meantime.
… it runs createNewHybridObject() returned an object whose enum member was never set, so logging it threw "Invalid Enum passed". The recording example stopped the recorder from a StrictMode cleanup before anything was recorded, which throws in native on both platforms.
…ds a skybox useLightEntity created a light per mount and never destroyed it. The example gets a 20 round soak entry on the stress screen.
Two FilamentScene instances side by side, each with its own engine and a Reanimated driven rotation. Part of the stress list.
Reanimated warns that the argument is only used by its web implementation.
eduardoborges
force-pushed
the
v2/react-native-worklets
branch
from
September 3, 2026 13:31
591443c to
978d132
Compare
eduardoborges
marked this pull request as ready for review
September 4, 2026 17:43
The macos-26 runners moved to Xcode 26, whose clang refuses the two virtual methods declared inside the final PlatformMetal class, and the Metal backend compiles with -Werror. Xcode 27 beta (Apple clang 21) does the same locally. Upstream dropped the virtual keyword in google/filament#9928. filament_clang21.patch carries that change until the submodule moves past it.
setup-android v3 still runs "sdkmanager tools", a package that no longer exists, so every Android build died before compiling anything. v4 installs only the packages it is given. The docs workflows ran yarn classic inside package/, which walks up to the root workspace and refuses it because the root is not private. The repo is a bun workspace, so install it with bun the way validate-js does, and mark the root private for anyone who still runs yarn there.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Continues #327 by @hannojg: drops
react-native-worklets-coreand runs onreact-native-worklets, the worklet runtime that ships with Reanimated 4. Opened as a draft because everything below was verified on simulators and emulators only.What changes for users
react-native-worklets >= 0.12andreact-native-reanimated >= 4. Thereact-native-worklets-corepatch and its Babel plugin go away; apps use thereact-native-worklets/pluginthey already have for Reanimated.useApplyTransformations, lights,Cameraand the rest.useSyncSharedValueanduseDerivedValueare removed, since Reanimated's ownuseSharedValueanduseDerivedValuework as they are.useWorkletCallbackreturns a JS function that schedules the worklet on the Filament runtime and resolves with the result. It cannot be captured inside another worklet or handed to native callbacks; pass a plain'worklet'function there. The PhysicsCoin example was doing exactly that with its collision callback.RenderableManager.destroyEntity()is new. Entities fromcreatePlane,createImageBackgroundShapeandcreateDebugCubeWireframewere never destroyed, and releasing their material hit a Filament precondition abort.BackgroundImageandDebugBoxclean up after themselves now, and the material deleters detach any renderable that still uses an instance.ndkVersion(AGP 9 defaults to NDK 28, whose libc++ fails to dlopen next to React Native's NDK 27 build) and as C++20.How it works
The Filament runtime is a regular
react-native-workletsruntime created from JS withcreateWorkletRuntime, backed by anAsyncQueueUIwhose scheduler forwards jobs to the render thread. Its__globalDispatcherruns jobs under the runtime lock. Every native entry into that runtime (frame callbacks, promise resolution, shared value listeners) takes thereact-native-workletsruntime mutex first. Hot reload, serializables and shared value reads touch the runtime from other threads, and Hermes' reentrancy check traps otherwise.Reanimated only allows shared value listeners on the UI runtime, so
useSharedValueListenerregisters there and forwards changes to the Filament runtime by listener id. State shared across runtimes (surface liveness, animator crossfade, last applied transform) lives increateSynchronizablevalues.Two Android details worth knowing while reviewing:
type_infoby pointer, andtypeinfo for worklets::AsyncQueueis a weak symbol with one copy per.so. Adynamic_pointer_castacross libraries then fails depending on dlopen order, so instead of subclassingAsyncQueuewe handreact-native-workletsanAsyncQueueUIwith a customUIScheduler.JDispatcherfrom a thread that is not attached to the JVM.JDispatchernow wraps its JNI calls injni::ThreadScope::WithClassLoader, like the other native finalizers already did.withCleanupScopeused to wait onInteractionManager, which React Native 0.87 removed from core. It now defers by one tick. The shorter delay exposed three teardown races that were already in the code base: a material destroyed while a renderable still used it, the recorder surface freed before Filament rancreateSwapChain, and the GC thread JNI call above. All three are fixed here.Testing
useAnimatedSensor.Not done: no physical device yet. Under the 250 ms loop the Android emulator logs Java
OutOfMemoryErrorwhile loading assets, without crashing; model buffers are only freed once Hermes collects the wrapper, so havinguseModelrelease its buffer afterloadResourceswould help. The offscreen recording example double starts under StrictMode and logs a rejected promise, which predates this branch.Since rc.5
A night of looping every example screen (3 rounds at 1.5 s, 5 rounds at 250 ms, a 20 round soak with memory sampling), JS reloads, Fast Refresh, backgrounding and rotation on both platforms, plus a React Native 0.87.1 app, turned up a few more things, all in this branch:
releaseOnUnmount: false; they piled up on Android until asset loads hitOutOfMemoryError.Skyboxreleases its texture buffer on unmount.useLightEntitydestroys its light on unmount.FilamentViewno longer rejects when the native view is gone beforefindFilamentViewresolves.enableTransparentRendering={false}documents that aSkyboxis required, and there is aTwoScenesexample with two engines on one screen.Memory over the soak stayed flat (Android PSS 688 MB from the first to the last sample, iOS RSS falling from 180 MB to 108 MB), thread counts stable, and Filament engines are created and destroyed in equal numbers once Hermes collects the wrappers.
CI
The workflows were already red before this branch touched them. Two commits at the tip fix that and can be cherry-picked on their own:
macos-latestnow means macos-26 with Xcode 26, whose clang refuses the twovirtualmethods declared inside thefinalPlatformMetalclass while the Metal backend compiles with-Werror.filament_clang21.patchapplies the two-line change from Fix macOS build with clang 21 google/filament#9928 until the submodule is bumped past it.android-actions/setup-android@v3runssdkmanager tools, a package that no longer exists, so both Android jobs died before compiling anything. v4 installs only the packages it is asked for.package/, which finds the root bun workspace and refuses it because the root is notprivate. They now install the workspace with bun the wayvalidate-jsdoes, and the root is marked private.Prereleases with the Filament and Bullet binaries are on the fork: https://github.com/eduardoborges/react-native-filament/releases (latest
v2.0.0-rc.6).