react-native-webgpu 0.15.1 → 0.16.0

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.
Files changed (47) hide show
  1. package/android/CMakeLists.txt +5 -0
  2. package/android/cpp/cpp-adapter.cpp +160 -49
  3. package/android/src/main/java/com/webgpu/SurfaceFrameRateVote.java +196 -0
  4. package/android/src/main/java/com/webgpu/WebGPUView.java +29 -2
  5. package/apple/ApplePlatformContext.h +1 -0
  6. package/cpp/jsi/JSICache.cpp +2 -0
  7. package/cpp/jsi/NativeObject.h +36 -12
  8. package/cpp/rnwgpu/RNWebGPUManager.cpp +4 -0
  9. package/cpp/rnwgpu/SurfaceRegistry.h +28 -4
  10. package/cpp/rnwgpu/api/GPUDevice.cpp +96 -0
  11. package/cpp/rnwgpu/api/GPUDevice.h +22 -0
  12. package/cpp/rnwgpu/api/GPUError.h +1 -0
  13. package/cpp/rnwgpu/api/GPUExtent2D.h +56 -0
  14. package/cpp/rnwgpu/api/GPUExternalTexture.cpp +32 -408
  15. package/cpp/rnwgpu/api/GPUExternalTexture.h +12 -37
  16. package/cpp/rnwgpu/api/GPUInternalError.h +1 -0
  17. package/cpp/rnwgpu/api/GPUNativeBufferExternalTexture.h +60 -0
  18. package/cpp/rnwgpu/api/GPUNativeBufferTexture.h +52 -0
  19. package/cpp/rnwgpu/api/GPUNativeBufferWrapper.cpp +144 -0
  20. package/cpp/rnwgpu/api/GPUNativeBufferWrapper.h +142 -0
  21. package/cpp/rnwgpu/api/GPUOutOfMemoryError.h +1 -0
  22. package/cpp/rnwgpu/api/GPUQueue.h +1 -0
  23. package/cpp/rnwgpu/api/GPUSharedFence.h +1 -0
  24. package/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp +12 -44
  25. package/cpp/rnwgpu/api/GPUSharedTextureMemory.h +1 -0
  26. package/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h +1 -0
  27. package/cpp/rnwgpu/api/GPUValidationError.h +1 -0
  28. package/cpp/rnwgpu/api/NativeFrameImport.cpp +638 -0
  29. package/cpp/rnwgpu/api/NativeFrameImport.h +86 -0
  30. package/cpp/rnwgpu/api/VideoFrame.h +21 -7
  31. package/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h +153 -1
  32. package/cpp/rnwgpu/api/descriptors/GPUNativeBufferTextureDescriptor.h +55 -0
  33. package/cpp/rnwgpu/async/RuntimeContext.cpp +1 -0
  34. package/lib/commonjs/Canvas.js.map +1 -1
  35. package/lib/commonjs/index.js.map +1 -1
  36. package/lib/module/Canvas.js.map +1 -1
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/typescript/src/Canvas.d.ts +2 -31
  39. package/lib/typescript/src/Canvas.d.ts.map +1 -1
  40. package/lib/typescript/src/index.d.ts +4 -2
  41. package/lib/typescript/src/index.d.ts.map +1 -1
  42. package/lib/typescript/src/types.d.ts +33 -0
  43. package/lib/typescript/src/types.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/Canvas.tsx +3 -36
  46. package/src/index.tsx +24 -0
  47. package/src/types.ts +97 -0
@@ -46,6 +46,8 @@ add_library(${PACKAGE_NAME} SHARED
46
46
  ../cpp/rnwgpu/api/GPUSharedFence.cpp
47
47
  ../cpp/rnwgpu/api/GPUSharedTextureMemory.cpp
48
48
  ../cpp/rnwgpu/api/GPUExternalTexture.cpp
49
+ ../cpp/rnwgpu/api/GPUNativeBufferWrapper.cpp
50
+ ../cpp/rnwgpu/api/NativeFrameImport.cpp
49
51
  ../cpp/rnwgpu/api/GPURenderBundleEncoder.cpp
50
52
  ../cpp/rnwgpu/api/GPURenderPassEncoder.cpp
51
53
  ../cpp/rnwgpu/api/GPURenderPipeline.cpp
@@ -96,6 +98,8 @@ if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)
96
98
  log
97
99
  jnigraphics
98
100
  android
101
+ # sync_merge, for the one fence endAccess() returns
102
+ sync
99
103
  fbjni::fbjni
100
104
  ReactAndroid::jsi
101
105
  ReactAndroid::reactnative
@@ -108,6 +112,7 @@ else()
108
112
  log
109
113
  jnigraphics
110
114
  android
115
+ sync
111
116
  fbjni::fbjni
112
117
  ReactAndroid::jsi
113
118
  ReactAndroid::reactnativejni
@@ -1,7 +1,11 @@
1
1
  #include <unistd.h>
2
2
 
3
+ #include <atomic>
4
+ #include <chrono>
5
+ #include <cstdint>
3
6
  #include <cstring>
4
7
  #include <memory>
8
+ #include <mutex>
5
9
  #include <string>
6
10
  #include <unordered_map>
7
11
  #include <utility>
@@ -156,9 +160,131 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceChanged(
156
160
  }
157
161
  }
158
162
 
163
+ namespace {
164
+
165
+ // Weak handle on a Java view and one of its no-argument methods, for native
166
+ // threads to call into it: the fence waiter wakes a WebGPUHardwareBufferView
167
+ // when a frame is ready, the presenting thread tells a SurfaceView it is
168
+ // animating (AnimationReporter). Holding the view weakly means a view that RN
169
+ // has dropped is not kept alive by a SurfaceInfo that outlives it (the
170
+ // registry keeps the SurfaceInfo until onDropViewInstance).
171
+ struct WeakViewCall {
172
+ jweak view = nullptr;
173
+ jmethodID method = nullptr;
174
+
175
+ WeakViewCall(JNIEnv *env, jobject thiz, const char *methodName) {
176
+ view = env->NewWeakGlobalRef(thiz);
177
+ jclass cls = env->GetObjectClass(thiz);
178
+ method = env->GetMethodID(cls, methodName, "()V");
179
+ env->DeleteLocalRef(cls);
180
+ }
181
+
182
+ ~WeakViewCall() {
183
+ // May run on any thread; ThreadScope attaches if needed.
184
+ facebook::jni::ThreadScope scope;
185
+ JNIEnv *env = facebook::jni::Environment::current();
186
+ if (env != nullptr && view != nullptr) {
187
+ env->DeleteWeakGlobalRef(view);
188
+ }
189
+ }
190
+
191
+ // Called on a native thread, outside the SurfaceInfo locks.
192
+ void operator()() const {
193
+ facebook::jni::ThreadScope scope;
194
+ JNIEnv *env = facebook::jni::Environment::current();
195
+ if (env == nullptr || method == nullptr) {
196
+ return;
197
+ }
198
+ jobject local = env->NewLocalRef(view);
199
+ if (local == nullptr) {
200
+ return; // the view was garbage collected
201
+ }
202
+ env->CallVoidMethod(local, method);
203
+ if (env->ExceptionCheck()) {
204
+ env->ExceptionClear();
205
+ }
206
+ env->DeleteLocalRef(local);
207
+ }
208
+ };
209
+
210
+ // Tells a SurfaceView it starts animating, for its refresh rate vote
211
+ // (WebGPUView.onAnimating), from the frames presented to its surface. A call
212
+ // into Java on the render path costs a JNI transition and attaches a presenting
213
+ // thread the JVM doesn't know for the length of the call, so there is one call
214
+ // per animation: the first frame presented right after its predecessor calls,
215
+ // the frames after it only record the time. The view polls that time from the
216
+ // main thread (releaseIfIdle) and, once the animation has stopped, takes its
217
+ // vote back and re-arms the call for the next animation. Lock-free, since it
218
+ // runs on every present.
219
+ class AnimationReporter {
220
+ public:
221
+ AnimationReporter(JNIEnv *env, jobject view)
222
+ : _onAnimating(env, view, "onAnimating") {}
223
+
224
+ void onFramePresented() {
225
+ const int64_t now = nowNanos();
226
+ const int64_t previous = _lastPresent.exchange(now);
227
+ if (now - previous > kAnimatingIntervalNanos) {
228
+ return;
229
+ }
230
+ _lastAnimating.store(now);
231
+ if (!_reported.exchange(true)) {
232
+ _onAnimating();
233
+ }
234
+ }
235
+
236
+ // Main thread. Returns how much longer the animation must stay stopped
237
+ // before it counts as idle, or 0 once it has: the next animating frame then
238
+ // calls into Java again.
239
+ int64_t releaseIfIdle(int64_t idleAfterNanos) {
240
+ const int64_t idle = nowNanos() - _lastAnimating.load();
241
+ if (idle < idleAfterNanos) {
242
+ return idleAfterNanos - idle;
243
+ }
244
+ _reported.store(false);
245
+ return 0;
246
+ }
247
+
248
+ private:
249
+ // Presents this close together are an animation (faster than 40 fps).
250
+ static constexpr int64_t kAnimatingIntervalNanos = 25'000'000;
251
+
252
+ static int64_t nowNanos() {
253
+ return std::chrono::duration_cast<std::chrono::nanoseconds>(
254
+ std::chrono::steady_clock::now().time_since_epoch())
255
+ .count();
256
+ }
257
+
258
+ WeakViewCall _onAnimating;
259
+ // INT64_MIN / 2 rather than a sentinel: the first present is never "right
260
+ // after" it, and the subtractions above don't overflow.
261
+ std::atomic<int64_t> _lastPresent{INT64_MIN / 2};
262
+ std::atomic<int64_t> _lastAnimating{INT64_MIN / 2};
263
+ // Java has been told about the current animation.
264
+ std::atomic<bool> _reported{false};
265
+ };
266
+
267
+ // The reporters by contextId, for WebGPUView.releaseFrameRateVoteIfIdle. The
268
+ // surface callbacks and the idle check all run on the main thread; the lock
269
+ // only guards against a caller that doesn't.
270
+ std::mutex animationReportersMutex;
271
+ std::unordered_map<int, std::shared_ptr<AnimationReporter>> animationReporters;
272
+
273
+ void setAnimationReporter(int contextId,
274
+ std::shared_ptr<AnimationReporter> reporter) {
275
+ std::lock_guard<std::mutex> lock(animationReportersMutex);
276
+ if (reporter) {
277
+ animationReporters[contextId] = std::move(reporter);
278
+ } else {
279
+ animationReporters.erase(contextId);
280
+ }
281
+ }
282
+
283
+ } // namespace
284
+
159
285
  extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
160
286
  JNIEnv *env, jobject thiz, jobject jSurface, jint contextId, jfloat width,
161
- jfloat height) {
287
+ jfloat height, jboolean reportAnimation) {
162
288
  if (manager == nullptr) {
163
289
  return;
164
290
  }
@@ -179,6 +305,17 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
179
305
  surface, [](void *nativeSurface) {
180
306
  ANativeWindow_release(static_cast<ANativeWindow *>(nativeSurface));
181
307
  });
308
+ // A SurfaceView asks to hear when it animates, for its refresh rate vote
309
+ // (WebGPUView.onAnimating).
310
+ if (reportAnimation) {
311
+ auto reporter = std::make_shared<AnimationReporter>(env, thiz);
312
+ setAnimationReporter(contextId, reporter);
313
+ info->setFramePresentedCallback(
314
+ [reporter]() { reporter->onFramePresented(); });
315
+ } else {
316
+ setAnimationReporter(contextId, nullptr);
317
+ info->setFramePresentedCallback(nullptr);
318
+ }
182
319
  // The attach is adopted at the next frame boundary by the rendering thread;
183
320
  // schedule a flush so contexts that are not currently rendering still pick
184
321
  // it up (and present their last offscreen frame).
@@ -188,20 +325,41 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
188
325
  extern "C" JNIEXPORT void JNICALL
189
326
  Java_com_webgpu_WebGPUView_switchToOffscreenSurface(JNIEnv *env, jobject thiz,
190
327
  jint contextId) {
328
+ setAnimationReporter(contextId, nullptr);
191
329
  auto &registry = rnwgpu::SurfaceRegistry::getInstance();
192
330
  if (auto info = registry.getSurfaceInfo(contextId)) {
331
+ info->setFramePresentedCallback(nullptr);
193
332
  info->switchToOffscreen();
194
333
  }
195
334
  }
196
335
 
336
+ // See AnimationReporter::releaseIfIdle; 0 when the surface reports nothing.
337
+ extern "C" JNIEXPORT jlong JNICALL
338
+ Java_com_webgpu_WebGPUView_releaseFrameRateVoteIfIdle(JNIEnv *env, jobject thiz,
339
+ jint contextId,
340
+ jlong idleAfterNanos) {
341
+ std::shared_ptr<AnimationReporter> reporter;
342
+ {
343
+ std::lock_guard<std::mutex> lock(animationReportersMutex);
344
+ auto it = animationReporters.find(contextId);
345
+ if (it == animationReporters.end()) {
346
+ return 0;
347
+ }
348
+ reporter = it->second;
349
+ }
350
+ return static_cast<jlong>(reporter->releaseIfIdle(idleAfterNanos));
351
+ }
352
+
197
353
  extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onViewDestroyed(
198
354
  JNIEnv *env, jobject thiz, jint contextId) {
199
355
  // The view dies with its Canvas (contextIds are never reused), so view
200
356
  // teardown retires the registry entry. The JS-side cleanup
201
357
  // (RNWebGPU.destroyContext) only handles entries that never had a native
202
358
  // surface; see RNWebGPU::destroyContext for the ownership split.
359
+ setAnimationReporter(contextId, nullptr);
203
360
  auto &registry = rnwgpu::SurfaceRegistry::getInstance();
204
361
  if (auto info = registry.getSurfaceInfo(contextId)) {
362
+ info->setFramePresentedCallback(nullptr);
205
363
  info->detachSurface();
206
364
  }
207
365
  registry.removeSurfaceInfo(contextId);
@@ -210,53 +368,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onViewDestroyed(
210
368
  // --- WebGPUHardwareBufferView (AHB-pool presentation)
211
369
  // ---------------------------------
212
370
 
213
- namespace {
214
-
215
- // Weak handle on a WebGPUHardwareBufferView used by the native fence waiter to
216
- // wake the UI thread when a frame is ready. Holding it weakly means a view that
217
- // RN has dropped is not kept alive by a SurfaceInfo that outlives it (the
218
- // registry keeps the SurfaceInfo until onDropViewInstance).
219
- struct HardwareBufferViewWaker {
220
- jweak view = nullptr;
221
- jmethodID onFrameReady = nullptr;
222
-
223
- HardwareBufferViewWaker(JNIEnv *env, jobject thiz) {
224
- view = env->NewWeakGlobalRef(thiz);
225
- jclass cls = env->GetObjectClass(thiz);
226
- onFrameReady = env->GetMethodID(cls, "onNativeFrameReady", "()V");
227
- env->DeleteLocalRef(cls);
228
- }
229
-
230
- ~HardwareBufferViewWaker() {
231
- // May run on the waiter or JS thread; ThreadScope attaches if needed.
232
- facebook::jni::ThreadScope scope;
233
- JNIEnv *env = facebook::jni::Environment::current();
234
- if (env != nullptr && view != nullptr) {
235
- env->DeleteWeakGlobalRef(view);
236
- }
237
- }
238
-
239
- // Called on the waiter thread, outside the pool lock.
240
- void operator()() const {
241
- facebook::jni::ThreadScope scope;
242
- JNIEnv *env = facebook::jni::Environment::current();
243
- if (env == nullptr || onFrameReady == nullptr) {
244
- return;
245
- }
246
- jobject local = env->NewLocalRef(view);
247
- if (local == nullptr) {
248
- return; // the view was garbage collected
249
- }
250
- env->CallVoidMethod(local, onFrameReady);
251
- if (env->ExceptionCheck()) {
252
- env->ExceptionClear();
253
- }
254
- env->DeleteLocalRef(local);
255
- }
256
- };
257
-
258
- } // namespace
259
-
260
371
  // Turn on pool mode for this context. dpW/dpH is the canvas-client (dp) size;
261
372
  // the native pool buffers are sized from the canvas drawing buffer lazily.
262
373
  extern "C" JNIEXPORT void JNICALL
@@ -267,7 +378,7 @@ Java_com_webgpu_WebGPUHardwareBufferView_nEnablePool(JNIEnv *env, jobject thiz,
267
378
  auto info = registry.getSurfaceInfoOrCreate(
268
379
  contextId, rnwgpu::WebGPURuntime::shared().instance(),
269
380
  static_cast<int>(dpW), static_cast<int>(dpH));
270
- auto waker = std::make_shared<HardwareBufferViewWaker>(env, thiz);
381
+ auto waker = std::make_shared<WeakViewCall>(env, thiz, "onNativeFrameReady");
271
382
  info->setPoolFrameReadyCallback([waker]() { (*waker)(); });
272
383
  info->enablePool(static_cast<int>(dpW), static_cast<int>(dpH));
273
384
  }
@@ -0,0 +1,196 @@
1
+ package com.webgpu;
2
+
3
+ import android.os.Build;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
6
+ import android.util.Log;
7
+ import android.view.Display;
8
+ import android.view.Surface;
9
+ import android.view.View;
10
+
11
+ import androidx.annotation.Nullable;
12
+
13
+ /**
14
+ * Votes the display's refresh rate for a SurfaceView canvas while it animates.
15
+ *
16
+ * A SurfaceView is composited in a layer of its own, outside the window that
17
+ * votes for the app's views, so SurfaceFlinger picks the layer's refresh rate
18
+ * from its present history alone: a canvas drawing every frame on a display
19
+ * idling at 60 Hz looks like 60 Hz content and never gets more. Since Android
20
+ * 11 a layer can say what it wants with Surface.setFrameRate. Once the canvas
21
+ * presents frames back to back, this asks for the highest rate the display can
22
+ * switch to seamlessly, as the window does for its own views, and takes the
23
+ * vote back once the animation stops, whether the canvas stops drawing or
24
+ * drops back to a few redraws a second: a layer with a vote counts as active
25
+ * for SurfaceFlinger even when it no longer draws.
26
+ *
27
+ * Native code tells onAnimating() once per animation, on the thread that
28
+ * presents (the JS thread, the Reanimated UI runtime or a worklet runtime),
29
+ * and the vote is cast right there, with no hop to a main thread that may be
30
+ * busy. The rate is resolved ahead of time on the main thread, where the
31
+ * view's display can be read. The idle check runs on the main thread and asks
32
+ * native code how long ago the canvas last animated; once it has stopped, it
33
+ * takes the vote back and native code calls onAnimating() again at the next
34
+ * animation. The lock only guards the state: the binder calls
35
+ * (Surface.setFrameRate) are made outside it.
36
+ */
37
+ final class SurfaceFrameRateVote {
38
+ private static final String TAG = "SurfaceFrameRateVote";
39
+
40
+ // The vote is taken back once the canvas has not animated for this long,
41
+ // whether it stops drawing or drops back to a few redraws a second. Native
42
+ // code decides what animating means (presents faster than 40 fps, see
43
+ // AnimationReporter in cpp-adapter.cpp). The threshold is absolute rather
44
+ // than relative to the current refresh rate, so that a canvas that only
45
+ // manages 60 fps at 120 Hz keeps its vote rather than oscillating between
46
+ // the two rates. A canvas that redraws a few times a second never votes.
47
+ static final long IDLE_AFTER_NANOS = 500_000_000L;
48
+
49
+ /** Native side of the vote, see AnimationReporter in cpp-adapter.cpp. */
50
+ interface AnimationSource {
51
+ /**
52
+ * How much longer the canvas must stay still before it counts as idle,
53
+ * or 0 once it does, in which case the next animation calls
54
+ * onAnimating() again. Main thread.
55
+ */
56
+ long releaseIfIdle(long idleAfterNanos);
57
+ }
58
+
59
+ private final View mView;
60
+ private final AnimationSource mSource;
61
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
62
+ private final Runnable mIdleCheck = this::checkIdle;
63
+
64
+ @Nullable
65
+ private Surface mSurface;
66
+ private boolean mVoting = false;
67
+ // The rate to ask for, resolved on the main thread; 0 when unknown, in
68
+ // which case animations don't vote until the next idle check resolves it.
69
+ private float mRefreshRate = 0f;
70
+
71
+ /** The view the surface belongs to; its display decides the rate to ask for. */
72
+ SurfaceFrameRateVote(View view, AnimationSource source) {
73
+ mView = view;
74
+ mSource = source;
75
+ }
76
+
77
+ /** The surface was created: it is voted on until detach(). Main thread. */
78
+ void attach(Surface surface) {
79
+ detach();
80
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
81
+ return;
82
+ }
83
+ float refreshRate = resolveMaxRefreshRate();
84
+ synchronized (this) {
85
+ mSurface = surface;
86
+ mRefreshRate = refreshRate;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * The canvas started animating (presenting frames back to back). Called
92
+ * once per animation, on the presenting thread.
93
+ */
94
+ void onAnimating() {
95
+ Surface surface;
96
+ float refreshRate;
97
+ synchronized (this) {
98
+ if (mSurface == null) {
99
+ return;
100
+ }
101
+ surface = mSurface;
102
+ refreshRate = mRefreshRate;
103
+ mVoting = refreshRate > 0;
104
+ }
105
+ // Whether it votes or not, the idle check re-arms native code once the
106
+ // animation stops.
107
+ mHandler.postDelayed(mIdleCheck, IDLE_AFTER_NANOS / 1_000_000);
108
+ if (refreshRate > 0) {
109
+ // This form only switches when the display can do so seamlessly, like
110
+ // the vote the window casts for its own views: a canvas animation is
111
+ // not worth a blank screen. Surface.setFrameRate is thread-safe.
112
+ setFrameRate(surface, refreshRate);
113
+ }
114
+ }
115
+
116
+ /** The surface is going away (it is still valid here) or the view left its window. Main thread. */
117
+ void detach() {
118
+ Surface surface;
119
+ boolean voting;
120
+ synchronized (this) {
121
+ surface = mSurface;
122
+ voting = mVoting;
123
+ mSurface = null;
124
+ mVoting = false;
125
+ }
126
+ mHandler.removeCallbacks(mIdleCheck);
127
+ // A vote cast concurrently on the presenting thread may land after this;
128
+ // it dies with the surface, which is going away.
129
+ if (surface != null && voting) {
130
+ setFrameRate(surface, 0);
131
+ }
132
+ }
133
+
134
+ private void checkIdle() {
135
+ long remainingNanos = mSource.releaseIfIdle(IDLE_AFTER_NANOS);
136
+ if (remainingNanos > 0) {
137
+ mHandler.postDelayed(mIdleCheck, (remainingNanos + 999_999) / 1_000_000);
138
+ return;
139
+ }
140
+ // Native code is re-armed: the next onAnimating() can only come after
141
+ // this point, so it doesn't race with the withdrawal below.
142
+ float refreshRate = resolveMaxRefreshRate();
143
+ Surface surface;
144
+ boolean voting;
145
+ synchronized (this) {
146
+ surface = mSurface;
147
+ voting = mVoting;
148
+ mVoting = false;
149
+ if (surface != null) {
150
+ // Follows the display the view is on, for the next animation.
151
+ mRefreshRate = refreshRate;
152
+ }
153
+ }
154
+ if (surface != null && voting) {
155
+ setFrameRate(surface, 0);
156
+ }
157
+ }
158
+
159
+ private static void setFrameRate(Surface surface, float refreshRate) {
160
+ if (!surface.isValid()) {
161
+ return;
162
+ }
163
+ try {
164
+ surface.setFrameRate(refreshRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
165
+ } catch (RuntimeException e) {
166
+ Log.w(TAG, "Could not set the surface's frame rate, leaving it to the display", e);
167
+ }
168
+ }
169
+
170
+ // The highest rate a seamless switch from the current mode can reach, which
171
+ // is all the vote may ask for. Resolved when the vote is cast, so that it
172
+ // follows the display the view is on.
173
+ private float resolveMaxRefreshRate() {
174
+ Display display = mView.getDisplay();
175
+ if (display == null) {
176
+ return 0f;
177
+ }
178
+ Display.Mode current = display.getMode();
179
+ float maxRefreshRate = current.getRefreshRate();
180
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
181
+ for (float refreshRate : current.getAlternativeRefreshRates()) {
182
+ maxRefreshRate = Math.max(maxRefreshRate, refreshRate);
183
+ }
184
+ } else {
185
+ // Android 11 does not tell which modes are a seamless switch away: the
186
+ // ones at the current resolution usually are.
187
+ for (Display.Mode mode : display.getSupportedModes()) {
188
+ if (mode.getPhysicalWidth() == current.getPhysicalWidth()
189
+ && mode.getPhysicalHeight() == current.getPhysicalHeight()) {
190
+ maxRefreshRate = Math.max(maxRefreshRate, mode.getRefreshRate());
191
+ }
192
+ }
193
+ }
194
+ return maxRefreshRate;
195
+ }
196
+ }
@@ -24,6 +24,10 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
24
24
  private boolean mAppliedZOrderOnTop;
25
25
  private WebGPUModule mModule;
26
26
  private View mView = null;
27
+ // Only a SurfaceView votes: a TextureView or HardwareBufferView is
28
+ // composited into the window, which votes for it.
29
+ private final SurfaceFrameRateVote mFrameRateVote = new SurfaceFrameRateVote(
30
+ this, idleAfterNanos -> releaseFrameRateVoteIfIdle(mContextId, idleAfterNanos));
27
31
 
28
32
  WebGPUView(Context context) {
29
33
  super(context);
@@ -134,7 +138,11 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
134
138
  float density = getResources().getDisplayMetrics().density;
135
139
  float width = getWidth() / density;
136
140
  float height = getHeight() / density;
137
- onSurfaceCreate(surface, mContextId, width, height);
141
+ boolean votesFrameRate = mAppliedKind == KIND_SURFACE_VIEW;
142
+ if (votesFrameRate) {
143
+ mFrameRateVote.attach(surface);
144
+ }
145
+ onSurfaceCreate(surface, mContextId, width, height, votesFrameRate);
138
146
  }
139
147
 
140
148
  @Override
@@ -147,9 +155,20 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
147
155
 
148
156
  @Override
149
157
  public void surfaceOffscreen() {
158
+ // The surface is still valid here, so the vote can be taken back.
159
+ mFrameRateVote.detach();
150
160
  switchToOffscreenSurface(mContextId);
151
161
  }
152
162
 
163
+ /**
164
+ * Called from native (AnimationReporter in cpp-adapter.cpp) on the thread
165
+ * that presents to the surface, once per animation, only for a SurfaceView.
166
+ */
167
+ @DoNotStrip
168
+ void onAnimating() {
169
+ mFrameRateVote.onAnimating();
170
+ }
171
+
153
172
  @Override
154
173
  public int getContextId() {
155
174
  return mContextId;
@@ -160,15 +179,19 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
160
179
  * view: the view dies with its Canvas, so it retires the registry entry.
161
180
  */
162
181
  public void destroy() {
182
+ mFrameRateVote.detach();
163
183
  onViewDestroyed(mContextId);
164
184
  }
165
185
 
186
+ // reportAnimation asks native to call onAnimating() when frames start being
187
+ // presented to this surface back to back.
166
188
  @DoNotStrip
167
189
  private native void onSurfaceCreate(
168
190
  Surface surface,
169
191
  int contextId,
170
192
  float width,
171
- float height
193
+ float height,
194
+ boolean reportAnimation
172
195
  );
173
196
 
174
197
  @DoNotStrip
@@ -185,4 +208,8 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
185
208
  @DoNotStrip
186
209
  private native void onViewDestroyed(int contextId);
187
210
 
211
+ // See SurfaceFrameRateVote.AnimationSource.
212
+ @DoNotStrip
213
+ private native long releaseFrameRateVoteIfIdle(int contextId, long idleAfterNanos);
214
+
188
215
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include "PlatformContext.h"
4
4
  #include <functional>
5
+ #include <memory>
5
6
  #include <string>
6
7
 
7
8
  namespace rnwgpu {
@@ -2,8 +2,10 @@
2
2
 
3
3
  #include <algorithm>
4
4
  #include <iterator>
5
+ #include <memory>
5
6
  #include <stdexcept>
6
7
  #include <string>
8
+ #include <unordered_map>
7
9
  #include <utility>
8
10
 
9
11
  namespace rnwgpu {
@@ -14,6 +14,7 @@
14
14
  #include <typeindex>
15
15
  #include <unordered_map>
16
16
  #include <utility>
17
+ #include <vector>
17
18
 
18
19
  #include "EnforceRange.h"
19
20
  #include "JSICache.h"
@@ -640,19 +641,26 @@ protected:
640
641
  rt, std::move(result));
641
642
  }
642
643
  });
644
+ defineGetter(runtime, prototype, name, getterFunc);
645
+ }
643
646
 
644
- // Use Object.defineProperty to create a proper getter
645
- auto objectCtor = runtime.global().getPropertyAsObject(runtime, "Object");
646
- auto defineProperty =
647
- objectCtor.getPropertyAsFunction(runtime, "defineProperty");
648
-
649
- jsi::Object descriptor(runtime);
650
- descriptor.setProperty(runtime, "get", getterFunc);
651
- descriptor.setProperty(runtime, "enumerable", true);
652
- descriptor.setProperty(runtime, "configurable", true);
653
-
654
- defineProperty.call(runtime, prototype,
655
- jsi::String::createFromUtf8(runtime, name), descriptor);
647
+ /**
648
+ * Install a getter whose native implementation builds the JS value itself,
649
+ * from the calling jsi::Runtime: for values no JSIConverter produces (null
650
+ * or a BigInt, say).
651
+ */
652
+ static void installGetter(jsi::Runtime &runtime, jsi::Object &prototype,
653
+ const char *name,
654
+ jsi::Value (Derived::*getter)(jsi::Runtime &)) {
655
+ auto getterFunc = jsi::Function::createFromHostFunction(
656
+ runtime, jsi::PropNameID::forUtf8(runtime, std::string("get_") + name),
657
+ 0,
658
+ [getter](jsi::Runtime &rt, const jsi::Value &thisVal,
659
+ const jsi::Value *args, size_t count) -> jsi::Value {
660
+ auto native = Derived::fromValue(rt, thisVal);
661
+ return (native.get()->*getter)(rt);
662
+ });
663
+ defineGetter(runtime, prototype, name, getterFunc);
656
664
  }
657
665
 
658
666
  /**
@@ -757,6 +765,22 @@ protected:
757
765
  }
758
766
 
759
767
  private:
768
+ // Use Object.defineProperty to create a proper getter
769
+ static void defineGetter(jsi::Runtime &runtime, jsi::Object &prototype,
770
+ const char *name, const jsi::Function &getterFunc) {
771
+ auto objectCtor = runtime.global().getPropertyAsObject(runtime, "Object");
772
+ auto defineProperty =
773
+ objectCtor.getPropertyAsFunction(runtime, "defineProperty");
774
+
775
+ jsi::Object descriptor(runtime);
776
+ descriptor.setProperty(runtime, "get", getterFunc);
777
+ descriptor.setProperty(runtime, "enumerable", true);
778
+ descriptor.setProperty(runtime, "configurable", true);
779
+
780
+ defineProperty.call(runtime, prototype,
781
+ jsi::String::createFromUtf8(runtime, name), descriptor);
782
+ }
783
+
760
784
  // Helper to call a method that takes the calling jsi::Runtime as its first
761
785
  // parameter, with JSI argument conversion for the rest and JSI conversion of
762
786
  // the result.
@@ -22,6 +22,8 @@
22
22
  #include "GPUError.h"
23
23
  #include "GPUExternalTexture.h"
24
24
  #include "GPUInternalError.h"
25
+ #include "GPUNativeBufferExternalTexture.h"
26
+ #include "GPUNativeBufferTexture.h"
25
27
  #include "GPUOutOfMemoryError.h"
26
28
  #include "GPUPipelineLayout.h"
27
29
  #include "GPUQuerySet.h"
@@ -102,6 +104,8 @@ RNWebGPUManager::RNWebGPUManager(
102
104
  GPUDeviceLostInfo::installConstructor(*_jsRuntime);
103
105
  GPUError::installConstructor(*_jsRuntime);
104
106
  GPUExternalTexture::installConstructor(*_jsRuntime);
107
+ GPUNativeBufferExternalTexture::installConstructor(*_jsRuntime);
108
+ GPUNativeBufferTexture::installConstructor(*_jsRuntime);
105
109
  GPUInternalError::installConstructor(*_jsRuntime);
106
110
  GPUOutOfMemoryError::installConstructor(*_jsRuntime);
107
111
  GPUValidationError::installConstructor(*_jsRuntime);