react-native-webgpu 0.15.2 → 0.16.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.
Files changed (73) hide show
  1. package/android/CMakeLists.txt +5 -0
  2. package/android/build.gradle +11 -1
  3. package/android/cpp/cpp-adapter.cpp +109 -11
  4. package/android/src/main/java/com/webgpu/SurfaceFrameRateVote.java +97 -85
  5. package/android/src/main/java/com/webgpu/WebGPUView.java +97 -42
  6. package/apple/ApplePlatformContext.h +1 -0
  7. package/cpp/jsi/JSICache.cpp +2 -0
  8. package/cpp/jsi/NativeObject.h +81 -17
  9. package/cpp/rnwgpu/RNWebGPUManager.cpp +51 -41
  10. package/cpp/rnwgpu/RNWebGPUManager.h +10 -0
  11. package/cpp/rnwgpu/SurfaceRegistry.h +4 -4
  12. package/cpp/rnwgpu/api/GPUDevice.cpp +96 -0
  13. package/cpp/rnwgpu/api/GPUDevice.h +22 -0
  14. package/cpp/rnwgpu/api/GPUError.h +1 -0
  15. package/cpp/rnwgpu/api/GPUExtent2D.h +56 -0
  16. package/cpp/rnwgpu/api/GPUExternalTexture.cpp +32 -408
  17. package/cpp/rnwgpu/api/GPUExternalTexture.h +12 -37
  18. package/cpp/rnwgpu/api/GPUInternalError.h +1 -0
  19. package/cpp/rnwgpu/api/GPUNativeBufferExternalTexture.h +60 -0
  20. package/cpp/rnwgpu/api/GPUNativeBufferTexture.h +52 -0
  21. package/cpp/rnwgpu/api/GPUNativeBufferWrapper.cpp +144 -0
  22. package/cpp/rnwgpu/api/GPUNativeBufferWrapper.h +143 -0
  23. package/cpp/rnwgpu/api/GPUOutOfMemoryError.h +1 -0
  24. package/cpp/rnwgpu/api/GPUQueue.h +1 -0
  25. package/cpp/rnwgpu/api/GPUSharedFence.h +1 -0
  26. package/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp +12 -44
  27. package/cpp/rnwgpu/api/GPUSharedTextureMemory.h +1 -0
  28. package/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h +1 -0
  29. package/cpp/rnwgpu/api/GPUValidationError.h +1 -0
  30. package/cpp/rnwgpu/api/NativeFrameImport.cpp +638 -0
  31. package/cpp/rnwgpu/api/NativeFrameImport.h +86 -0
  32. package/cpp/rnwgpu/api/VideoFrame.h +21 -7
  33. package/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h +153 -1
  34. package/cpp/rnwgpu/api/descriptors/GPUNativeBufferTextureDescriptor.h +55 -0
  35. package/cpp/rnwgpu/async/RuntimeContext.cpp +1 -0
  36. package/lib/commonjs/Canvas.js +51 -0
  37. package/lib/commonjs/Canvas.js.map +1 -1
  38. package/lib/commonjs/external/reanimated/registerWebGPUForReanimated.js.map +1 -1
  39. package/lib/commonjs/index.js.map +1 -1
  40. package/lib/commonjs/install.js +26 -7
  41. package/lib/commonjs/install.js.map +1 -1
  42. package/lib/module/Canvas.js +51 -0
  43. package/lib/module/Canvas.js.map +1 -1
  44. package/lib/module/external/reanimated/registerWebGPUForReanimated.js.map +1 -1
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/install.js +26 -7
  47. package/lib/module/install.js.map +1 -1
  48. package/lib/typescript/lib/commonjs/Canvas.d.ts.map +1 -1
  49. package/lib/typescript/lib/commonjs/install.d.ts +10 -7
  50. package/lib/typescript/lib/commonjs/install.d.ts.map +1 -1
  51. package/lib/typescript/lib/module/Canvas.d.ts.map +1 -1
  52. package/lib/typescript/lib/module/install.d.ts.map +1 -1
  53. package/lib/typescript/src/Canvas.d.ts +20 -35
  54. package/lib/typescript/src/Canvas.d.ts.map +1 -1
  55. package/lib/typescript/src/external/reanimated/registerWebGPUForReanimated.d.ts +1 -0
  56. package/lib/typescript/src/external/reanimated/registerWebGPUForReanimated.d.ts.map +1 -1
  57. package/lib/typescript/src/index.d.ts +4 -2
  58. package/lib/typescript/src/index.d.ts.map +1 -1
  59. package/lib/typescript/src/install.d.ts +10 -7
  60. package/lib/typescript/src/install.d.ts.map +1 -1
  61. package/lib/typescript/src/types.d.ts +34 -1
  62. package/lib/typescript/src/types.d.ts.map +1 -1
  63. package/package.json +6 -5
  64. package/plugin/build/index.js +2 -1
  65. package/plugin/build/setMinSdkVersion.d.ts +9 -0
  66. package/plugin/build/setMinSdkVersion.js +25 -0
  67. package/plugin/build/withMinSdkVersion.d.ts +11 -0
  68. package/plugin/build/withMinSdkVersion.js +20 -0
  69. package/src/Canvas.tsx +74 -40
  70. package/src/external/reanimated/registerWebGPUForReanimated.ts +4 -0
  71. package/src/index.tsx +24 -0
  72. package/src/install.ts +26 -7
  73. package/src/types.ts +98 -1
@@ -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
@@ -75,6 +75,16 @@ def getExtOrIntegerDefault(name) {
75
75
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Wgpu_" + name]).toInteger()
76
76
  }
77
77
 
78
+ // androidMinSdkVersion in package.json is the lowest API level the native code
79
+ // builds for: it calls the AHardwareBuffer API, from Android 8 (API 26). An
80
+ // app's lower minSdkVersion does not lower it, so such an app gets the
81
+ // manifest merger's error, with its suggested fix, instead of the compiler's.
82
+ def resolveMinSdkVersion() {
83
+ def floor = new groovy.json.JsonSlurper().parse(file("../package.json")).androidMinSdkVersion as Integer
84
+ def app = rootProject.ext.has("minSdkVersion") ? rootProject.ext.get("minSdkVersion").toString().toInteger() : floor
85
+ return Math.max(app, floor)
86
+ }
87
+
78
88
  def supportsNamespace() {
79
89
  def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
80
90
  def major = parsed[0].toInteger()
@@ -88,7 +98,7 @@ android {
88
98
  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
89
99
 
90
100
  defaultConfig {
91
- minSdkVersion getExtOrIntegerDefault("minSdkVersion")
101
+ minSdkVersion resolveMinSdkVersion()
92
102
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
93
103
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
94
104
  externalNativeBuild {
@@ -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>
@@ -160,10 +164,10 @@ namespace {
160
164
 
161
165
  // Weak handle on a Java view and one of its no-argument methods, for native
162
166
  // threads to call into it: the fence waiter wakes a WebGPUHardwareBufferView
163
- // when a frame is ready, the presenting thread reports each frame a
164
- // SurfaceView shows. Holding the view weakly means a view that RN has dropped
165
- // is not kept alive by a SurfaceInfo that outlives it (the registry keeps the
166
- // SurfaceInfo until onDropViewInstance).
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).
167
171
  struct WeakViewCall {
168
172
  jweak view = nullptr;
169
173
  jmethodID method = nullptr;
@@ -203,11 +207,84 @@ struct WeakViewCall {
203
207
  }
204
208
  };
205
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
+
206
283
  } // namespace
207
284
 
208
285
  extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
209
286
  JNIEnv *env, jobject thiz, jobject jSurface, jint contextId, jfloat width,
210
- jfloat height, jboolean reportPresents) {
287
+ jfloat height, jboolean reportAnimation) {
211
288
  if (manager == nullptr) {
212
289
  return;
213
290
  }
@@ -228,13 +305,15 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
228
305
  surface, [](void *nativeSurface) {
229
306
  ANativeWindow_release(static_cast<ANativeWindow *>(nativeSurface));
230
307
  });
231
- // A SurfaceView asks to hear about each presented frame, for its refresh
232
- // rate vote (WebGPUView.onFramePresented).
233
- if (reportPresents) {
234
- auto reporter =
235
- std::make_shared<WeakViewCall>(env, thiz, "onFramePresented");
236
- info->setFramePresentedCallback([reporter]() { (*reporter)(); });
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(); });
237
315
  } else {
316
+ setAnimationReporter(contextId, nullptr);
238
317
  info->setFramePresentedCallback(nullptr);
239
318
  }
240
319
  // The attach is adopted at the next frame boundary by the rendering thread;
@@ -246,6 +325,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
246
325
  extern "C" JNIEXPORT void JNICALL
247
326
  Java_com_webgpu_WebGPUView_switchToOffscreenSurface(JNIEnv *env, jobject thiz,
248
327
  jint contextId) {
328
+ setAnimationReporter(contextId, nullptr);
249
329
  auto &registry = rnwgpu::SurfaceRegistry::getInstance();
250
330
  if (auto info = registry.getSurfaceInfo(contextId)) {
251
331
  info->setFramePresentedCallback(nullptr);
@@ -253,12 +333,30 @@ Java_com_webgpu_WebGPUView_switchToOffscreenSurface(JNIEnv *env, jobject thiz,
253
333
  }
254
334
  }
255
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
+
256
353
  extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onViewDestroyed(
257
354
  JNIEnv *env, jobject thiz, jint contextId) {
258
355
  // The view dies with its Canvas (contextIds are never reused), so view
259
356
  // teardown retires the registry entry. The JS-side cleanup
260
357
  // (RNWebGPU.destroyContext) only handles entries that never had a native
261
358
  // surface; see RNWebGPU::destroyContext for the ownership split.
359
+ setAnimationReporter(contextId, nullptr);
262
360
  auto &registry = rnwgpu::SurfaceRegistry::getInstance();
263
361
  if (auto info = registry.getSurfaceInfo(contextId)) {
264
362
  info->setFramePresentedCallback(nullptr);
@@ -24,134 +24,146 @@ import androidx.annotation.Nullable;
24
24
  * drops back to a few redraws a second: a layer with a vote counts as active
25
25
  * for SurfaceFlinger even when it no longer draws.
26
26
  *
27
- * attach() and detach() run on the main thread with the surface callbacks;
28
- * onFramePresented() runs on whichever thread presents (the JS thread, the
29
- * Reanimated UI runtime or a worklet runtime), so the state is locked.
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.
30
36
  */
31
37
  final class SurfaceFrameRateVote {
32
38
  private static final String TAG = "SurfaceFrameRateVote";
33
39
 
34
- // Presents this close together are an animation (faster than 40 fps). The
35
- // threshold is absolute rather than relative to the current refresh rate,
36
- // so that a canvas that only manages 60 fps at 120 Hz keeps its vote rather
37
- // than oscillating between the two rates. A canvas that redraws a few times
38
- // a second never votes.
39
- private static final long ANIMATING_INTERVAL_NANOS = 25_000_000L;
40
-
41
- // The vote is taken back once the canvas has not animated (no two presents
42
- // within ANIMATING_INTERVAL_NANOS) for this long. Measured from the last
43
- // back-to-back present rather than the last present, so that a canvas that
44
- // slows down to a few redraws a second lets the vote lapse like one that
45
- // stops drawing.
46
- private static final long IDLE_AFTER_NANOS = 500_000_000L;
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
+ }
47
58
 
48
59
  private final View mView;
60
+ private final AnimationSource mSource;
49
61
  private final Handler mHandler = new Handler(Looper.getMainLooper());
50
62
  private final Runnable mIdleCheck = this::checkIdle;
51
63
 
52
64
  @Nullable
53
65
  private Surface mSurface;
54
66
  private boolean mVoting = false;
55
- private boolean mIdleCheckPending = false;
56
- // 0 before the first present on the surface.
57
- private long mLastPresentNanos = 0;
58
- // The last present that followed its predecessor within
59
- // ANIMATING_INTERVAL_NANOS; 0 until the first.
60
- private long mLastAnimatingNanos = 0;
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;
61
70
 
62
71
  /** The view the surface belongs to; its display decides the rate to ask for. */
63
- SurfaceFrameRateVote(View view) {
72
+ SurfaceFrameRateVote(View view, AnimationSource source) {
64
73
  mView = view;
74
+ mSource = source;
65
75
  }
66
76
 
67
77
  /** The surface was created: it is voted on until detach(). Main thread. */
68
- synchronized void attach(Surface surface) {
78
+ void attach(Surface surface) {
69
79
  detach();
70
80
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
71
81
  return;
72
82
  }
73
- mSurface = surface;
83
+ float refreshRate = resolveMaxRefreshRate();
84
+ synchronized (this) {
85
+ mSurface = surface;
86
+ mRefreshRate = refreshRate;
87
+ }
74
88
  }
75
89
 
76
- /** A frame was presented on the surface. Any thread. */
77
- synchronized void onFramePresented() {
78
- if (mSurface == null) {
79
- return;
80
- }
81
- long now = System.nanoTime();
82
- boolean animating = mLastPresentNanos != 0 && now - mLastPresentNanos <= ANIMATING_INTERVAL_NANOS;
83
- mLastPresentNanos = now;
84
- if (animating) {
85
- mLastAnimatingNanos = now;
86
- if (!mVoting) {
87
- vote();
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;
88
100
  }
101
+ surface = mSurface;
102
+ refreshRate = mRefreshRate;
103
+ mVoting = refreshRate > 0;
89
104
  }
90
- if (mVoting && !mIdleCheckPending) {
91
- scheduleIdleCheck(IDLE_AFTER_NANOS);
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);
92
113
  }
93
114
  }
94
115
 
95
116
  /** The surface is going away (it is still valid here) or the view left its window. Main thread. */
96
- synchronized void detach() {
97
- if (mSurface == null) {
98
- return;
117
+ void detach() {
118
+ Surface surface;
119
+ boolean voting;
120
+ synchronized (this) {
121
+ surface = mSurface;
122
+ voting = mVoting;
123
+ mSurface = null;
124
+ mVoting = false;
99
125
  }
100
- withdraw();
101
126
  mHandler.removeCallbacks(mIdleCheck);
102
- mIdleCheckPending = false;
103
- mLastPresentNanos = 0;
104
- mLastAnimatingNanos = 0;
105
- mSurface = null;
106
- }
107
-
108
- private void vote() {
109
- float refreshRate = resolveMaxRefreshRate();
110
- if (refreshRate <= 0 || !mSurface.isValid()) {
111
- return;
112
- }
113
- try {
114
- // This form only switches when the display can do so seamlessly, like
115
- // the vote the window casts for its own views: a canvas animation is
116
- // not worth a blank screen. Surface.setFrameRate is thread-safe.
117
- mSurface.setFrameRate(refreshRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
118
- mVoting = true;
119
- } catch (RuntimeException e) {
120
- Log.w(TAG, "Could not vote the refresh rate, leaving it to the display", e);
121
- detach();
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);
122
131
  }
123
132
  }
124
133
 
125
- private void withdraw() {
126
- if (!mVoting) {
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);
127
138
  return;
128
139
  }
129
- mVoting = false;
130
- if (mSurface == null || !mSurface.isValid()) {
131
- return;
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
+ }
132
153
  }
133
- try {
134
- mSurface.setFrameRate(0, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
135
- } catch (RuntimeException e) {
136
- Log.w(TAG, "Could not withdraw the refresh rate vote", e);
154
+ if (surface != null && voting) {
155
+ setFrameRate(surface, 0);
137
156
  }
138
157
  }
139
158
 
140
- private void scheduleIdleCheck(long delayNanos) {
141
- mIdleCheckPending = true;
142
- mHandler.postDelayed(mIdleCheck, (delayNanos + 999_999) / 1_000_000);
143
- }
144
-
145
- private synchronized void checkIdle() {
146
- mIdleCheckPending = false;
147
- if (!mVoting) {
159
+ private static void setFrameRate(Surface surface, float refreshRate) {
160
+ if (!surface.isValid()) {
148
161
  return;
149
162
  }
150
- long idleNanos = System.nanoTime() - mLastAnimatingNanos;
151
- if (idleNanos >= IDLE_AFTER_NANOS) {
152
- withdraw();
153
- } else {
154
- scheduleIdleCheck(IDLE_AFTER_NANOS - idleNanos);
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);
155
167
  }
156
168
  }
157
169