@react-native-gpui/worklets 0.0.0-stage → 0.13.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@react-native-gpui/worklets` are listed here. Each version's section becomes the notes of its GitHub release.
4
+
5
+ ## 0.13.0-alpha.1 - 2026-11-12
6
+
7
+ - Added a hook that runs after each frame's animation callbacks, which Reanimated uses to apply its pending updates.
8
+
9
+ ## 0.13.0-alpha.0 - 2026-11-01
10
+
11
+ - First release, for `react-native-worklets` 0.13.x.
12
+ - Runs worklets on desktop, driven by the renderer's frame clock.
13
+ - Works with the npm release of `react-native-worklets`: the C++ fixes it needs for MSVC and Linux are applied to a copy in the build directory.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Streetwriters
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,25 @@
1
- # Temporary Holding Version
1
+ # @react-native-gpui/worklets
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Adapter for [`react-native-worklets`](https://github.com/software-mansion/react-native-reanimated/tree/main/packages/react-native-worklets) (MIT): runs worklets on desktop, driven by the renderer's frame clock.
4
+
5
+ Part of [react-native-gpui](https://github.com/streetwriters/react-native-gpui), React Native for macOS, Windows and Linux.
6
+
7
+ ## Install
8
+
9
+ Install it next to `react-native-worklets`:
10
+
11
+ ```sh
12
+ npm install react-native-worklets@0.13.x @react-native-gpui/worklets
13
+ ```
14
+
15
+ Install the regular npm release of `react-native-worklets`. Its C++ needs small fixes to build with MSVC and on Linux; this package applies them to a copy in the desktop build directory and never changes `node_modules`.
16
+
17
+ Keep using `react-native-worklets` as you do on iOS and Android; its JavaScript API is unchanged. Autolinking builds the desktop side when the app is built.
18
+
19
+ ## Documentation
20
+
21
+ [Supported libraries](https://streetwriters.github.io/react-native-gpui/docs/supported-libraries)
22
+
23
+ ## License
24
+
25
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,52 @@
1
+ if(NOT RN_GPUI_PROJECT_ROOT)
2
+ message(FATAL_ERROR
3
+ "@react-native-gpui/worklets was added by the platform; RN_GPUI_PROJECT_ROOT must be set by its build.rs")
4
+ endif()
5
+ set(_gpui_app_root "${RN_GPUI_PROJECT_ROOT}")
6
+ find_program(_gpui_node node REQUIRED)
7
+ execute_process(
8
+ COMMAND "${_gpui_node}" -e "process.stdout.write(require.resolve('react-native-worklets/package.json', {paths: [process.argv[1]]}))" "${_gpui_app_root}"
9
+ RESULT_VARIABLE _worklets_resolve_result
10
+ OUTPUT_VARIABLE _worklets_package_json
11
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
12
+ if(NOT _worklets_resolve_result EQUAL 0 OR NOT EXISTS "${_worklets_package_json}")
13
+ message(FATAL_ERROR "react-native-worklets could not be resolved from ${_gpui_app_root}")
14
+ endif()
15
+ get_filename_component(_worklets_root "${_worklets_package_json}" DIRECTORY)
16
+ set(_worklets_common "${_worklets_root}/Common/cpp")
17
+ if(NOT EXISTS "${_worklets_common}/worklets/NativeModules/WorkletsModuleProxy.h")
18
+ message(FATAL_ERROR "react-native-worklets 0.13.x was not found in ${_gpui_app_root} or any ancestor node_modules directory")
19
+ endif()
20
+
21
+ # Upstream's C++ does not build with MSVC or on Linux as published; compile a
22
+ # patched copy in the build tree and leave node_modules untouched.
23
+ set(_worklets_patch "${CMAKE_CURRENT_LIST_DIR}/patches/react-native-worklets-0.13.0.patch")
24
+ execute_process(
25
+ COMMAND "${_gpui_node}" "${CMAKE_CURRENT_LIST_DIR}/patch-sources.mjs"
26
+ "${_worklets_root}" "${_worklets_patch}"
27
+ "${CMAKE_BINARY_DIR}/rn-gpui-patched/react-native-worklets"
28
+ RESULT_VARIABLE _worklets_patch_result
29
+ OUTPUT_VARIABLE _worklets_common
30
+ ERROR_VARIABLE _worklets_patch_error
31
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
32
+ if(NOT _worklets_patch_result EQUAL 0)
33
+ message(FATAL_ERROR "${_worklets_patch_error}")
34
+ endif()
35
+ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
36
+ "${_worklets_patch}" "${_worklets_package_json}")
37
+
38
+ file(GLOB_RECURSE _worklets_sources CONFIGURE_DEPENDS "${_worklets_common}/worklets/*.cpp")
39
+ set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES
40
+ ${_worklets_sources}
41
+ "${CMAKE_CURRENT_LIST_DIR}/cpp/GPUIWorkletsModule.cpp"
42
+ "${CMAKE_CURRENT_LIST_DIR}/cpp/GPUIPlatformLogger.cpp")
43
+ set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS
44
+ "${_worklets_common}"
45
+ "${_worklets_root}")
46
+ set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_DEFINITIONS
47
+ "WORKLETS_VERSION=0.13.0")
48
+ # The frame hook is how worklets see every frame; the platform compiles this
49
+ # glue only because the package is installed.
50
+ set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_SOURCES
51
+ "${CMAKE_CURRENT_LIST_DIR}/cpp/GpuiWorkletsFrameModule.cpp")
52
+ set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INSTALLERS worklets)
@@ -0,0 +1,27 @@
1
+ #include <worklets/Tools/PlatformLogger.h>
2
+
3
+ #include <glog/logging.h>
4
+
5
+ namespace worklets {
6
+
7
+ void PlatformLogger::log(const char* str) {
8
+ LOG(INFO) << str;
9
+ }
10
+
11
+ void PlatformLogger::log(const std::string& str) {
12
+ LOG(INFO) << str;
13
+ }
14
+
15
+ void PlatformLogger::log(const double value) {
16
+ LOG(INFO) << value;
17
+ }
18
+
19
+ void PlatformLogger::log(const int value) {
20
+ LOG(INFO) << value;
21
+ }
22
+
23
+ void PlatformLogger::log(const bool value) {
24
+ LOG(INFO) << (value ? "true" : "false");
25
+ }
26
+
27
+ } // namespace worklets
@@ -0,0 +1,353 @@
1
+ #include "GPUIWorkletsModule.h"
2
+
3
+ #include "rn-gpui-sys/src/lib.rs.h"
4
+
5
+ #include <glog/logging.h>
6
+ #include <worklets/Tools/JSLogger.h>
7
+ #include <worklets/WorkletRuntime/RuntimeBindings.h>
8
+ #include <worklets/WorkletRuntime/WorkletRuntime.h>
9
+
10
+ #include <algorithm>
11
+ #include <atomic>
12
+ #include <chrono>
13
+ #include <cstdlib>
14
+ #include <stdexcept>
15
+ #include <thread>
16
+ #include <utility>
17
+
18
+ #include "GpuiModules.h"
19
+
20
+ namespace {
21
+ bool animationTracingEnabled() {
22
+ static const bool enabled = std::getenv("RN_GPUI_DEBUG_ANIMATION") != nullptr;
23
+ return enabled;
24
+ }
25
+
26
+ std::mutex moduleMutex;
27
+ std::weak_ptr<gpui_worklets::Module> moduleInstance;
28
+ std::atomic<uint64_t> workletFrames{0};
29
+ std::atomic<uint64_t> workletRafRequests{0};
30
+
31
+ class GPUIScheduler final : public worklets::UIScheduler {
32
+ protected:
33
+ bool queryIsOnUIThread() const override {
34
+ return gpui_worklets::isOnMainThread();
35
+ }
36
+
37
+ public:
38
+ void scheduleOnUI(std::function<void()> job) override {
39
+ static std::atomic<uint64_t> scheduled{0};
40
+ const auto count = ++scheduled;
41
+ if (animationTracingEnabled() && count <= 5) {
42
+ LOG(INFO) << "[gpui-worklets] scheduleOnUI count=" << count
43
+ << " onUI=" << isOnUIThread();
44
+ }
45
+ if (isOnUIThread()) {
46
+ job();
47
+ return;
48
+ }
49
+ UIScheduler::scheduleOnUI(std::move(job));
50
+ rngpui::requestFrames("worklets", true);
51
+ }
52
+ };
53
+
54
+ worklets::RuntimeBindings::NativeLoggingHook nativeLoggingHook() {
55
+ return [](facebook::jsi::Runtime& runtime,
56
+ const facebook::jsi::Value&,
57
+ const facebook::jsi::Value* args,
58
+ size_t count) -> facebook::jsi::Value {
59
+ if (count != 2) {
60
+ throw std::invalid_argument("nativeLoggingHook takes 2 arguments");
61
+ }
62
+ const auto message = args[0].asString(runtime).utf8(runtime);
63
+ LOG(INFO) << "[worklet] " << message;
64
+ return facebook::jsi::Value::undefined();
65
+ };
66
+ }
67
+ } // namespace
68
+
69
+ namespace gpui_worklets {
70
+ namespace {
71
+ std::mutex uiThreadMutex;
72
+ std::thread::id uiThreadId;
73
+ } // namespace
74
+
75
+ bool isOnMainThread() {
76
+ // The runtime records the main thread before its loop runs. `onFrame` only
77
+ // notes it once a frame arrives, and `UIScheduler::isOnUIThread` caches the
78
+ // first answer per thread: a query that comes first, such as a layout
79
+ // animation flush, would pin "not the UI thread" on the main thread for good.
80
+ if (rngpui::rn_gpui_is_main_thread()) {
81
+ return true;
82
+ }
83
+ std::lock_guard<std::mutex> lock(uiThreadMutex);
84
+ return uiThreadId != std::thread::id{} &&
85
+ uiThreadId == std::this_thread::get_id();
86
+ }
87
+
88
+ void noteMainThread() {
89
+ std::lock_guard<std::mutex> lock(uiThreadMutex);
90
+ if (uiThreadId == std::thread::id{}) {
91
+ uiThreadId = std::this_thread::get_id();
92
+ }
93
+ }
94
+
95
+ Module::Module(std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
96
+ : NativeWorkletsModuleSpec(std::move(jsInvoker)) {}
97
+
98
+ Module::~Module() {
99
+ rngpui::reportPendingWork("worklets", 0);
100
+ rngpui::requestFrames("worklets", false);
101
+ if (initializer_) {
102
+ initializer_->invalidate();
103
+ }
104
+ std::lock_guard<std::mutex> lock(moduleMutex);
105
+ if (moduleInstance.lock().get() == this) {
106
+ moduleInstance.reset();
107
+ }
108
+ }
109
+
110
+ void Module::initialize(facebook::jsi::Runtime& runtime) {
111
+ if (initializer_)
112
+ return;
113
+ uiScheduler_ = std::make_shared<GPUIScheduler>();
114
+ runtimeStatus_ = std::make_shared<worklets::RNRuntimeStatus>();
115
+ jsScheduler_ = std::make_shared<worklets::JSScheduler>(
116
+ runtime, jsInvoker_, [] { return true; });
117
+ std::weak_ptr<Module> weakModule = shared_from_this();
118
+ auto runtimeBindings =
119
+ std::make_shared<worklets::RuntimeBindings>(worklets::RuntimeBindings{
120
+ .requestAnimationFrame =
121
+ [weakModule](std::function<void(double)> callback) {
122
+ if (auto module = weakModule.lock()) {
123
+ module->requestAnimationFrame(std::move(callback));
124
+ }
125
+ },
126
+ .nativeLoggingHook = nativeLoggingHook(),
127
+ .networkingBackend = nullptr,
128
+ });
129
+ initializer_ = std::make_shared<worklets::WorkletsModuleProxyInitializer>(
130
+ jsScheduler_, uiScheduler_, runtimeBindings, runtimeStatus_);
131
+ LOG(INFO) << "[gpui-worklets] initialized runtime bindings";
132
+ auto initializer = initializer_;
133
+ std::thread([initializer] { initializer->prepareProxy(); }).detach();
134
+ }
135
+
136
+ bool Module::installTurboModule(
137
+ facebook::jsi::Runtime& runtime,
138
+ bool bundleModeEnabled) {
139
+ initialize(runtime);
140
+ if (bundleModeEnabled) {
141
+ throw facebook::jsi::JSError(
142
+ runtime, "GPUI Worklets does not support Bundle Mode yet");
143
+ }
144
+ proxy_ = initializer_->finalize(runtime, false, {});
145
+ LOG(INFO) << "[gpui-worklets] WorkletsModule installed";
146
+ return true;
147
+ }
148
+
149
+ bool Module::prepareBundleMode(facebook::jsi::Runtime& runtime) {
150
+ throw facebook::jsi::JSError(
151
+ runtime, "GPUI Worklets does not support Bundle Mode yet");
152
+ }
153
+
154
+ bool Module::start(facebook::jsi::Runtime& runtime) {
155
+ if (!proxy_)
156
+ throw facebook::jsi::JSError(
157
+ runtime, "WorkletsModule.installTurboModule must run before start");
158
+ proxy_->start();
159
+ LOG(INFO) << "[gpui-worklets] UI runtime started";
160
+ return true;
161
+ }
162
+
163
+ bool Module::toggleSlowAnimationsOnUIRuntime(facebook::jsi::Runtime&) {
164
+ return false;
165
+ }
166
+
167
+ void Module::requestAnimationFrame(std::function<void(double)> callback) {
168
+ const auto count = ++workletRafRequests;
169
+ if (animationTracingEnabled() && count <= 5) {
170
+ LOG(INFO) << "[gpui-worklets] requestAnimationFrame count=" << count;
171
+ }
172
+ {
173
+ std::lock_guard<std::mutex> lock(callbacksMutex_);
174
+ frameCallbacks_.push_back(std::move(callback));
175
+ }
176
+ rngpui::requestFrames("worklets", true);
177
+ }
178
+
179
+ void Module::setAfterFrame(std::function<void()> afterFrame) {
180
+ std::lock_guard<std::mutex> lock(callbacksMutex_);
181
+ afterFrame_ = std::move(afterFrame);
182
+ }
183
+
184
+ void Module::onFrame(double timestampMs) {
185
+ const auto count = ++workletFrames;
186
+ if (animationTracingEnabled()) {
187
+ static double windowStartMs = timestampMs;
188
+ static double previousFrameMs = timestampMs;
189
+ static double maximumGapMs = 0;
190
+ maximumGapMs = std::max(maximumGapMs, timestampMs - previousFrameMs);
191
+ previousFrameMs = timestampMs;
192
+ if (count % 60 == 0) {
193
+ LOG(INFO) << "[gpui-worklets] native frame cadence frames=60 elapsed_ms="
194
+ << timestampMs - windowStartMs
195
+ << " max_frame_gap_ms=" << maximumGapMs;
196
+ windowStartMs = timestampMs;
197
+ maximumGapMs = 0;
198
+ }
199
+ }
200
+ if (animationTracingEnabled() && (count <= 5 || count % 600 == 0)) {
201
+ LOG(INFO) << "[gpui-worklets] GPUI frame count=" << count;
202
+ }
203
+ // Frame callbacks are platform entry points, outside Worklets' guarded
204
+ // invocation paths. Report JS exceptions on RN rather than unwinding across
205
+ // the CXX boundary and terminating the desktop process.
206
+ const auto reportError = [this](const facebook::jsi::JSError& error) {
207
+ LOG(ERROR) << "[gpui-worklets] " << error.what();
208
+ worklets::JSLogger::reportFatalErrorOnJS(
209
+ jsScheduler_,
210
+ {.message = error.getMessage(),
211
+ .stack = error.getStack(),
212
+ .name = "WorkletsError"});
213
+ };
214
+ try {
215
+ if (uiScheduler_)
216
+ uiScheduler_->triggerUI();
217
+ } catch (const facebook::jsi::JSError& error) {
218
+ reportError(error);
219
+ }
220
+ // Worklets has a perpetual native heartbeat even when its JS RAF queue is
221
+ // empty. Test synchronization observes application callbacks, not that beat.
222
+ if (std::getenv("RN_GPUI_TEST_DRIVER")) {
223
+ if (auto runtime = uiRuntime()) {
224
+ runtime->runSync([](facebook::jsi::Runtime& rt) {
225
+ auto global = rt.global();
226
+ if (!global.hasProperty(rt, "requestAnimationFrame") ||
227
+ (global.hasProperty(rt, "__gpuiTestRaf") &&
228
+ facebook::jsi::Value::strictEquals(
229
+ rt,
230
+ global.getProperty(rt, "requestAnimationFrame"),
231
+ global.getProperty(rt, "__gpuiTestRaf"))))
232
+ return;
233
+ global.setProperty(
234
+ rt,
235
+ "__gpuiReportPendingRaf",
236
+ facebook::jsi::Function::createFromHostFunction(
237
+ rt,
238
+ facebook::jsi::PropNameID::forAscii(
239
+ rt, "__gpuiReportPendingRaf"),
240
+ 1,
241
+ [](facebook::jsi::Runtime&,
242
+ const facebook::jsi::Value&,
243
+ const facebook::jsi::Value* args,
244
+ size_t count) {
245
+ if (count == 1 && args[0].isNumber())
246
+ rngpui::reportPendingWork(
247
+ "worklets", static_cast<uint64_t>(args[0].getNumber()));
248
+ return facebook::jsi::Value::undefined();
249
+ }));
250
+ rt.evaluateJavaScript(
251
+ std::make_shared<facebook::jsi::StringBuffer>(R"JS(
252
+ (() => {
253
+ const raf = globalThis.requestAnimationFrame;
254
+ const cancel = globalThis.cancelAnimationFrame;
255
+ const pending = new Set();
256
+ const report = () => globalThis.__gpuiReportPendingRaf(pending.size);
257
+ globalThis.requestAnimationFrame = callback => {
258
+ let id;
259
+ id = raf(timestamp => {
260
+ pending.delete(id); report(); callback(timestamp);
261
+ });
262
+ pending.add(id); report(); return id;
263
+ };
264
+ globalThis.cancelAnimationFrame = id => {
265
+ pending.delete(id); report(); cancel(id);
266
+ };
267
+ globalThis.__gpuiTestRaf = globalThis.requestAnimationFrame;
268
+ report();
269
+ })();
270
+ )JS"),
271
+ "gpui-test-raf.js");
272
+ });
273
+ }
274
+ }
275
+ std::vector<std::function<void(double)>> callbacks;
276
+ {
277
+ std::lock_guard<std::mutex> lock(callbacksMutex_);
278
+ callbacks.swap(frameCallbacks_);
279
+ }
280
+ if (animationTracingEnabled() && !callbacks.empty() &&
281
+ (count <= 5 || count % 60 == 0)) {
282
+ LOG(INFO) << "[gpui-worklets] callbacks on frame=" << callbacks.size();
283
+ }
284
+ for (auto& callback : callbacks) {
285
+ try {
286
+ callback(timestampMs);
287
+ } catch (const facebook::jsi::JSError& error) {
288
+ reportError(error);
289
+ }
290
+ }
291
+ std::function<void()> afterFrame;
292
+ {
293
+ std::lock_guard<std::mutex> lock(callbacksMutex_);
294
+ afterFrame = afterFrame_;
295
+ }
296
+ if (afterFrame) {
297
+ try {
298
+ afterFrame();
299
+ } catch (const facebook::jsi::JSError& error) {
300
+ reportError(error);
301
+ }
302
+ }
303
+ {
304
+ std::lock_guard<std::mutex> lock(callbacksMutex_);
305
+ if (frameCallbacks_.empty()) {
306
+ rngpui::requestFrames("worklets", false);
307
+ }
308
+ }
309
+ }
310
+
311
+ std::shared_ptr<worklets::WorkletRuntime> Module::uiRuntime() const {
312
+ return proxy_ ? proxy_->getUIWorkletRuntime() : nullptr;
313
+ }
314
+
315
+ std::shared_ptr<worklets::UIScheduler> Module::uiScheduler() const {
316
+ return uiScheduler_;
317
+ }
318
+
319
+ void onFrame(double timestampMs) {
320
+ noteMainThread();
321
+ std::shared_ptr<Module> module;
322
+ {
323
+ std::lock_guard<std::mutex> lock(moduleMutex);
324
+ module = moduleInstance.lock();
325
+ }
326
+ if (module)
327
+ module->onFrame(timestampMs);
328
+ }
329
+
330
+ std::shared_ptr<Module> currentModule() {
331
+ std::lock_guard<std::mutex> lock(moduleMutex);
332
+ return moduleInstance.lock();
333
+ }
334
+
335
+ std::shared_ptr<facebook::react::TurboModule> createModule(
336
+ const std::string& name,
337
+ std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
338
+ if (name != Module::kModuleName)
339
+ return nullptr;
340
+ auto module = std::make_shared<Module>(std::move(jsInvoker));
341
+ {
342
+ std::lock_guard<std::mutex> lock(moduleMutex);
343
+ moduleInstance = module;
344
+ }
345
+ return module;
346
+ }
347
+ } // namespace gpui_worklets
348
+
349
+ std::shared_ptr<facebook::react::TurboModule> create_gpui_worklets_module(
350
+ const std::string& name,
351
+ std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
352
+ return gpui_worklets::createModule(name, std::move(jsInvoker));
353
+ }
@@ -0,0 +1,64 @@
1
+ #pragma once
2
+
3
+ #include <GPUIWorkletsSpec.h>
4
+ #include <ReactCommon/CallInvoker.h>
5
+ #include <ReactCommon/TurboModule.h>
6
+ #include <worklets/NativeModules/WorkletsModuleProxy.h>
7
+ #include <worklets/NativeModules/WorkletsModuleProxyInitializer.h>
8
+ #include <worklets/Tools/JSScheduler.h>
9
+ #include <worklets/Tools/RNRuntimeStatus.h>
10
+ #include <worklets/Tools/UIScheduler.h>
11
+
12
+ #include <functional>
13
+ #include <memory>
14
+ #include <mutex>
15
+ #include <vector>
16
+
17
+ namespace gpui_worklets {
18
+ bool isOnMainThread();
19
+ void noteMainThread();
20
+
21
+ class Module final : public facebook::react::NativeWorkletsModuleSpec<Module> {
22
+ public:
23
+ explicit Module(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
24
+ ~Module() override;
25
+
26
+ void onFrame(double timestampMs);
27
+ void requestAnimationFrame(std::function<void(double)> callback);
28
+ /// Runs after every frame's animation callbacks, as the display link's
29
+ /// handler does on iOS.
30
+ void setAfterFrame(std::function<void()> afterFrame);
31
+ std::shared_ptr<worklets::WorkletRuntime> uiRuntime() const;
32
+ std::shared_ptr<worklets::UIScheduler> uiScheduler() const;
33
+
34
+ bool installTurboModule(
35
+ facebook::jsi::Runtime& runtime,
36
+ bool bundleModeEnabled);
37
+ bool prepareBundleMode(facebook::jsi::Runtime& runtime);
38
+ bool start(facebook::jsi::Runtime& runtime);
39
+ bool toggleSlowAnimationsOnUIRuntime(facebook::jsi::Runtime& runtime);
40
+
41
+ private:
42
+ void initialize(facebook::jsi::Runtime& runtime);
43
+
44
+ std::shared_ptr<worklets::UIScheduler> uiScheduler_;
45
+ std::shared_ptr<worklets::JSScheduler> jsScheduler_;
46
+ std::shared_ptr<worklets::RNRuntimeStatus> runtimeStatus_;
47
+ std::shared_ptr<worklets::WorkletsModuleProxyInitializer> initializer_;
48
+ std::shared_ptr<worklets::WorkletsModuleProxy> proxy_;
49
+ std::mutex callbacksMutex_;
50
+ std::vector<std::function<void(double)>> frameCallbacks_;
51
+ std::function<void()> afterFrame_;
52
+ };
53
+
54
+ void onFrame(double timestampMs);
55
+ std::shared_ptr<Module> currentModule();
56
+ std::shared_ptr<facebook::react::TurboModule> createModule(
57
+ const std::string& name,
58
+ std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
59
+
60
+ } // namespace gpui_worklets
61
+
62
+ std::shared_ptr<facebook::react::TurboModule> create_gpui_worklets_module(
63
+ const std::string& name,
64
+ std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
@@ -0,0 +1,13 @@
1
+ #include "GpuiModules.h"
2
+
3
+ namespace gpui_worklets {
4
+ void onFrame(double timestampMs);
5
+ }
6
+
7
+ namespace rngpui {
8
+ void rn_gpui_install_worklets_module() {
9
+ registerFrameCallback(
10
+ [](double timestampMs) { gpui_worklets::onFrame(timestampMs); });
11
+ }
12
+
13
+ } // namespace rngpui
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ // Copies an upstream package's `Common/cpp` into a build directory and applies a
3
+ // unified diff to the copy, so the app's node_modules is never modified.
4
+ //
5
+ // node patch-sources.mjs <package root> <patch file> <output dir>
6
+ //
7
+ // Prints the patched `Common/cpp` directory. The copy is reused while the
8
+ // package version and the patch are unchanged.
9
+ import { createHash } from 'node:crypto';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ /** Parses a unified diff into files, each with its hunks. */
15
+ export function parsePatch(text) {
16
+ const files = [];
17
+ let file = null;
18
+ let hunk = null;
19
+ // Lines still expected in the current hunk, from its header counts.
20
+ let oldLeft = 0;
21
+ let newLeft = 0;
22
+ for (const line of text.replace(/\r\n/gu, '\n').split('\n')) {
23
+ if (hunk !== null && (oldLeft > 0 || newLeft > 0)) {
24
+ const kind = line.length === 0 ? ' ' : line[0];
25
+ const body = line.slice(1);
26
+ if (kind === ' ' || kind === '-') {
27
+ hunk.old.push(body);
28
+ oldLeft--;
29
+ }
30
+ if (kind === ' ' || kind === '+') {
31
+ hunk.new.push(body);
32
+ newLeft--;
33
+ }
34
+ if (kind === '\\') continue;
35
+ } else if (line.startsWith('--- ')) {
36
+ file = { path: null, hunks: [] };
37
+ files.push(file);
38
+ hunk = null;
39
+ } else if (line.startsWith('+++ ') && file !== null) {
40
+ file.path = line.slice(4).split('\t')[0].replace(/^b\//u, '');
41
+ } else if (line.startsWith('@@ ') && file !== null) {
42
+ const match = /^@@ -(\d+)(?:,(\d+))? \+\d+(?:,(\d+))? @@/u.exec(line);
43
+ if (match === null) throw new Error(`bad hunk header: ${line}`);
44
+ hunk = { start: Number(match[1]), old: [], new: [] };
45
+ oldLeft = match[2] === undefined ? 1 : Number(match[2]);
46
+ newLeft = match[3] === undefined ? 1 : Number(match[3]);
47
+ file.hunks.push(hunk);
48
+ }
49
+ }
50
+ return files;
51
+ }
52
+
53
+ function matchesAt(lines, block, at) {
54
+ if (at < 0 || at + block.length > lines.length) return false;
55
+ return block.every((line, i) => lines[at + i] === line);
56
+ }
57
+
58
+ /** The line nearest `expected` where `block` starts, or -1. */
59
+ function find(lines, block, expected) {
60
+ for (let delta = 0; delta <= lines.length; delta++) {
61
+ if (matchesAt(lines, block, expected - delta)) return expected - delta;
62
+ if (matchesAt(lines, block, expected + delta)) return expected + delta;
63
+ }
64
+ return -1;
65
+ }
66
+
67
+ /**
68
+ * Applies one file's hunks to its text, keeping its line endings. A hunk whose
69
+ * result is already present
70
+ * is skipped, so sources that carry the fix upstream still build; any other
71
+ * mismatch throws.
72
+ */
73
+ export function applyHunks(text, hunks, name) {
74
+ const crlf = text.includes('\r\n');
75
+ const lines = text.replace(/\r\n/gu, '\n').split('\n');
76
+ let offset = 0;
77
+ for (const hunk of hunks) {
78
+ const expected = Math.max(hunk.start - 1, 0) + offset;
79
+ const at = find(lines, hunk.old, expected);
80
+ if (at !== -1) {
81
+ lines.splice(at, hunk.old.length, ...hunk.new);
82
+ offset = at - (hunk.start - 1) + hunk.new.length - hunk.old.length;
83
+ continue;
84
+ }
85
+ const applied = find(lines, hunk.new, expected);
86
+ if (applied === -1) {
87
+ throw new Error(`${name}: hunk at line ${hunk.start} does not apply`);
88
+ }
89
+ offset = applied - (hunk.start - 1);
90
+ }
91
+ return lines.join(crlf ? '\r\n' : '\n');
92
+ }
93
+
94
+ function main([packageRoot, patchFile, outputDir]) {
95
+ if (!packageRoot || !patchFile || !outputDir) {
96
+ throw new Error(
97
+ 'usage: patch-sources.mjs <package root> <patch file> <output dir>',
98
+ );
99
+ }
100
+ const manifest = JSON.parse(
101
+ fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
102
+ );
103
+ const patch = fs.readFileSync(patchFile, 'utf8');
104
+ const stamp = createHash('sha256')
105
+ .update(`${manifest.name}@${manifest.version}\n`)
106
+ .update(patch)
107
+ .digest('hex');
108
+ const target = path.join(outputDir, 'Common', 'cpp');
109
+ const stampFile = path.join(outputDir, '.patched');
110
+ const current = fs.existsSync(stampFile)
111
+ ? fs.readFileSync(stampFile, 'utf8')
112
+ : '';
113
+ if (current !== stamp) {
114
+ fs.rmSync(outputDir, { recursive: true, force: true });
115
+ fs.cpSync(path.join(packageRoot, 'Common', 'cpp'), target, {
116
+ recursive: true,
117
+ });
118
+ for (const file of parsePatch(patch)) {
119
+ const full = path.join(target, file.path);
120
+ const original = fs.existsSync(full) ? fs.readFileSync(full, 'utf8') : '';
121
+ let patched;
122
+ try {
123
+ patched = applyHunks(original, file.hunks, file.path);
124
+ } catch (error) {
125
+ fs.rmSync(outputDir, { recursive: true, force: true });
126
+ throw new Error(
127
+ `${path.basename(patchFile)} does not apply to ${manifest.name}@${manifest.version}: ${error.message}`,
128
+ );
129
+ }
130
+ fs.mkdirSync(path.dirname(full), { recursive: true });
131
+ fs.writeFileSync(full, patched);
132
+ }
133
+ fs.writeFileSync(stampFile, stamp);
134
+ }
135
+ // CMake reads backslashes as escapes.
136
+ process.stdout.write(target.split(path.sep).join('/'));
137
+ }
138
+
139
+ if (
140
+ process.argv[1] &&
141
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
142
+ ) {
143
+ try {
144
+ main(process.argv.slice(2));
145
+ } catch (error) {
146
+ process.stderr.write(`${error.message}\n`);
147
+ process.exit(1);
148
+ }
149
+ }
@@ -0,0 +1,107 @@
1
+ --- a/worklets/RunLoop/AsyncQueueImpl.cpp
2
+ +++ b/worklets/RunLoop/AsyncQueueImpl.cpp
3
+ @@ -4,6 +4,10 @@
4
+ #include <fbjni/fbjni.h>
5
+ #endif // ANDROID
6
+
7
+ +#if defined(__APPLE__) || defined(__linux__)
8
+ +#include <pthread.h>
9
+ +#endif
10
+ +
11
+ #include <memory>
12
+ #include <queue>
13
+ #include <string>
14
+ @@ -69,7 +73,11 @@
15
+ pthread_setname_np(pthread_self(), name.c_str());
16
+ jni::ThreadScope::WithClassLoader([state]() { AsyncQueueImpl::runLoop(state); });
17
+ #else
18
+ +#if defined(__APPLE__)
19
+ pthread_setname_np(name.c_str());
20
+ +#elif defined(__linux__)
21
+ + pthread_setname_np(pthread_self(), name.substr(0, 15).c_str());
22
+ +#endif
23
+ AsyncQueueImpl::runLoop(state);
24
+ #endif // ANDROID
25
+ });
26
+ --- a/worklets/RunLoop/EventLoop.cpp
27
+ +++ b/worklets/RunLoop/EventLoop.cpp
28
+ @@ -1,5 +1,6 @@
29
+ #include <worklets/RunLoop/EventLoop.h>
30
+
31
+ +#include <algorithm>
32
+ #include <memory>
33
+ #include <string>
34
+ #include <thread>
35
+ --- a/worklets/Tools/JSISerializer.cpp
36
+ +++ b/worklets/Tools/JSISerializer.cpp
37
+ @@ -1,7 +1,10 @@
38
+ #include <worklets/Tools/JSISerializer.h>
39
+
40
+ +#ifndef _MSC_VER
41
+ #include <cxxabi.h>
42
+ +#endif
43
+
44
+ +#include <algorithm>
45
+ #include <memory>
46
+ #include <sstream>
47
+ #include <string>
48
+ @@ -116,15 +119,20 @@
49
+ }
50
+
51
+ std::string JSISerializer::stringifyHostObject(jsi::HostObject &hostObject) {
52
+ +#ifdef _MSC_VER
53
+ + const char *hostObjClassName = typeid(hostObject).name();
54
+ +#else
55
+ int status = -1;
56
+ - std::unique_ptr<char, decltype(&std::free)> hostObjClassName(
57
+ + std::unique_ptr<char, decltype(&std::free)> demangled(
58
+ abi::__cxa_demangle(typeid(hostObject).name(), nullptr, nullptr, &status), std::free);
59
+ if (status != 0) {
60
+ return "[jsi::HostObject]";
61
+ }
62
+ + const char *hostObjClassName = demangled.get();
63
+ +#endif
64
+
65
+ std::stringstream ss;
66
+ - ss << "[jsi::HostObject(" << hostObjClassName.get() << ")";
67
+ + ss << "[jsi::HostObject(" << hostObjClassName << ")";
68
+
69
+ auto props = hostObject.getPropertyNames(rt_);
70
+ auto propsCount = props.size();
71
+ --- a/worklets/Tools/SingleInstanceChecker.h
72
+ +++ b/worklets/Tools/SingleInstanceChecker.h
73
+ @@ -4,7 +4,9 @@
74
+
75
+ #include <react/debug/react_native_assert.h>
76
+
77
+ +#ifndef _MSC_VER
78
+ #include <cxxabi.h>
79
+ +#endif
80
+
81
+ #include <atomic>
82
+ #include <cassert>
83
+ @@ -50,10 +52,14 @@
84
+
85
+ template <class T>
86
+ SingleInstanceChecker<T>::SingleInstanceChecker() {
87
+ +#ifdef _MSC_VER
88
+ + std::string className = typeid(T).name();
89
+ +#else
90
+ int status = 0;
91
+ char *demangled = __cxxabiv1::__cxa_demangle(typeid(T).name(), nullptr, nullptr, &status);
92
+ std::string className = (status == 0 && demangled != nullptr) ? demangled : typeid(T).name();
93
+ std::free(demangled);
94
+ +#endif
95
+
96
+ // React Native can spawn up to two instances of a Native Module at the same
97
+ // time. This happens during a reload when a new instance of React Native is
98
+ --- a/worklets/Tools/WorkletsJSIUtils.cpp
99
+ +++ b/worklets/Tools/WorkletsJSIUtils.cpp
100
+ @@ -2,6 +2,7 @@
101
+
102
+ #include <react/debug/react_native_assert.h>
103
+
104
+ +#include <iterator>
105
+ #include <memory>
106
+ #include <sstream>
107
+ #include <string>
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@react-native-gpui/worklets",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.13.0-alpha.1",
4
+ "description": "Adapter for react-native-worklets (MIT): runs worklets on desktop, driven by the renderer's frame clock",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/streetwriters/react-native-gpui-modules.git",
9
+ "directory": "modules/worklets"
10
+ },
11
+ "homepage": "https://github.com/streetwriters/react-native-gpui-modules/tree/main/modules/worklets#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/streetwriters/react-native-gpui-modules/issues"
14
+ },
15
+ "files": [
16
+ "desktop",
17
+ "react-native.config.js",
18
+ "src",
19
+ "README.md",
20
+ "LICENSE",
21
+ "CHANGELOG.md",
22
+ "!**/*.test.js",
23
+ "!**/*.test.mjs",
24
+ "!**/*.test.ts",
25
+ "!**/*.test.cjs"
26
+ ],
27
+ "peerDependencies": {
28
+ "react-native": "0.87.x",
29
+ "react-native-worklets": "0.13.x"
30
+ },
31
+ "codegenConfig": {
32
+ "name": "GPUIWorkletsSpec",
33
+ "type": "modules",
34
+ "jsSrcsDir": "src"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "tag": "latest"
39
+ }
40
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ desktop: {
5
+ cmake: 'desktop',
6
+ cppProvider: 'create_gpui_worklets_module',
7
+ },
8
+ },
9
+ },
10
+ };
@@ -0,0 +1,10 @@
1
+ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
+
3
+ export interface Spec extends TurboModule {
4
+ installTurboModule(bundleModeEnabled: boolean): boolean;
5
+ prepareBundleMode(): boolean;
6
+ toggleSlowAnimationsOnUIRuntime(): boolean;
7
+ start(): boolean;
8
+ }
9
+
10
+ export default TurboModuleRegistry.get<Spec>('WorkletsModule');