react-native-skia 3.0.2 → 3.0.4

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 (32) hide show
  1. package/README.md +19 -2
  2. package/android/build.gradle +15 -7
  3. package/android/src/main/java/com/reactnative/skia/BackingViewKind.java +24 -0
  4. package/android/src/main/java/com/reactnative/skia/FrameScheduler.java +104 -0
  5. package/android/src/main/java/com/reactnative/skia/SkiaView.java +55 -44
  6. package/android/src/test/java/com/reactnative/skia/FrameSchedulerTest.java +186 -0
  7. package/apple/MetalLayerColorSpace.mm +10 -8
  8. package/apple/RNSkApplePlatformContext.h +12 -0
  9. package/apple/RNSkApplePlatformContext.mm +17 -0
  10. package/apple/RNSkMetalCanvasProvider.mm +4 -2
  11. package/apple/RNSkiaModule.mm +10 -0
  12. package/cpp/rnskia/RNDawnContext.h +3 -2
  13. package/cpp/rnskia/RNDawnUtils.h +12 -0
  14. package/cpp/rnskia/RNDawnWindowContext.cpp +2 -1
  15. package/cpp/rnskia/RNDawnWindowContext.h +15 -7
  16. package/cpp/rnskia/RNMetalLayerColorSpace.h +4 -2
  17. package/cpp/rnskia/RNSkCanvasProvider.h +18 -8
  18. package/cpp/rnskia/RNSkGraphiteTarget.h +20 -7
  19. package/cpp/rnskia/RNSkPlatformContext.h +8 -0
  20. package/cpp/rnskia/RNSkView.h +53 -29
  21. package/lib/commonjs/external/reanimated/interpolators.js +1 -1
  22. package/lib/commonjs/external/reanimated/interpolators.js.map +1 -1
  23. package/lib/commonjs/renderer/Canvas.js +1 -1
  24. package/lib/commonjs/renderer/Canvas.js.map +1 -1
  25. package/lib/module/external/reanimated/interpolators.js +1 -1
  26. package/lib/module/external/reanimated/interpolators.js.map +1 -1
  27. package/lib/module/renderer/Canvas.js +1 -1
  28. package/lib/module/renderer/Canvas.js.map +1 -1
  29. package/package.json +1 -1
  30. package/react-native-skia.podspec +1 -1
  31. package/src/external/reanimated/interpolators.ts +1 -1
  32. package/src/renderer/Canvas.tsx +1 -1
package/README.md CHANGED
@@ -4,6 +4,23 @@ High-performance 2d Graphics for React Native using Skia
4
4
 
5
5
  <img width="400" alt="skia" src="https://user-images.githubusercontent.com/306134/146549218-b7959ad9-0107-4c1c-b439-b96c780f5230.png">
6
6
 
7
- Checkout the full documentation [here](https://shopify.github.io/react-native-skia).
7
+ React Native Skia brings the Skia Graphics Library to React Native. Skia serves as the graphics engine for Google Chrome and Chrome OS, Android, Flutter, Mozilla Firefox and Firefox OS, and many other products.
8
8
 
9
- Documentation on the library development is available [here](https://github.com/Shopify/react-native-skia#library-development).
9
+ ## Getting Started
10
+
11
+ ```sh
12
+ yarn add react-native-skia
13
+ ```
14
+
15
+ Checkout the full documentation [here](https://wcandillon.github.io/react-native-skia).
16
+
17
+ ## v3 and v2
18
+
19
+ React Native Skia v3 renders with [Graphite](https://skia.org/docs/user/graphite/), Skia's new GPU backend, on iOS, Android (API level 26 and above), and macOS.
20
+
21
+ - To upgrade from v2 or from `@shopify/react-native-skia`, follow the [migration guide](https://wcandillon.github.io/react-native-skia/docs/getting-started/migration).
22
+ - v2 keeps the Ganesh backend and is still maintained. Install it with `yarn add react-native-skia@2`. Its documentation is available at [wcandillon.github.io/react-native-skia/v2](https://wcandillon.github.io/react-native-skia/v2/).
23
+
24
+ ## Contributing
25
+
26
+ Documentation on the library development is available [here](https://github.com/wcandillon/react-native-skia/blob/main/packages/skia/CONTRIBUTING.md).
@@ -147,7 +147,7 @@ logger.warn("react-native-skia: buildDir: ${buildDir}")
147
147
  logger.warn("react-native-skia: node_modules: ${nodeModules}")
148
148
 
149
149
  android {
150
- namespace "com.reactnative.skia"
150
+ namespace = "com.reactnative.skia"
151
151
 
152
152
  compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
153
153
  buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
@@ -155,10 +155,10 @@ android {
155
155
  // Used to override the NDK path/version on internal CI or by allowing
156
156
  // users to customize the NDK path/version from their root project.
157
157
  if (rootProject.hasProperty("ndkPath")) {
158
- ndkPath rootProject.ext.ndkPath
158
+ ndkPath = rootProject.ext.ndkPath
159
159
  }
160
160
  if (rootProject.hasProperty("ndkVersion")) {
161
- ndkVersion rootProject.ext.ndkVersion
161
+ ndkVersion = rootProject.ext.ndkVersion
162
162
  }
163
163
 
164
164
  defaultConfig {
@@ -181,8 +181,15 @@ android {
181
181
  }
182
182
  }
183
183
  }
184
+ // The Java sources use switch expressions (Java 14+). React Native's Gradle
185
+ // plugin aligns every module on 17 from the app's plugin block, but a host
186
+ // that opts out of that alignment must still compile this library.
187
+ compileOptions {
188
+ sourceCompatibility = JavaVersion.VERSION_17
189
+ targetCompatibility = JavaVersion.VERSION_17
190
+ }
184
191
  lintOptions {
185
- abortOnError false
192
+ abortOnError = false
186
193
  }
187
194
 
188
195
  publishing {
@@ -214,12 +221,12 @@ android {
214
221
  }
215
222
 
216
223
  buildFeatures {
217
- prefab true
218
- prefabPublishing true
224
+ prefab = true
225
+ prefabPublishing = true
219
226
  }
220
227
  prefab {
221
228
  rnskia {
222
- headers "${project.buildDir}/headers/rnskia/"
229
+ headers = "${project.buildDir}/headers/rnskia/"
223
230
  }
224
231
  }
225
232
  }
@@ -238,6 +245,7 @@ repositories {
238
245
  dependencies {
239
246
  //noinspection GradleDynamicVersion
240
247
  implementation 'com.facebook.react:react-native:+' // From node_modules
248
+ testImplementation 'junit:junit:4.13.2'
241
249
  }
242
250
 
243
251
  // Package all the cpp code in a flattened directory structure
@@ -0,0 +1,24 @@
1
+ package com.reactnative.skia;
2
+
3
+ import androidx.annotation.Nullable;
4
+
5
+ /** The Android view a SkiaView draws into, see SkiaView.updateView(). */
6
+ enum BackingViewKind {
7
+ SURFACE_VIEW,
8
+ TEXTURE_VIEW;
9
+
10
+ /**
11
+ * The kind the androidSurfaceType prop asks for, or null for "auto", which
12
+ * leaves the choice to the opaque prop.
13
+ */
14
+ @Nullable
15
+ static BackingViewKind fromSurfaceType(@Nullable String surfaceType) {
16
+ if ("SurfaceView".equals(surfaceType)) {
17
+ return SURFACE_VIEW;
18
+ }
19
+ if ("TextureView".equals(surfaceType)) {
20
+ return TEXTURE_VIEW;
21
+ }
22
+ return null;
23
+ }
24
+ }
@@ -0,0 +1,104 @@
1
+ package com.reactnative.skia;
2
+
3
+ import androidx.annotation.Nullable;
4
+
5
+ /**
6
+ * Decides when a SkiaView presents the recordings queued for it: a SurfaceView
7
+ * on the next vsync, a TextureView behind the draw its window has pending, so
8
+ * that the window has taken the previous frame by then. Keeps at most one
9
+ * frame callback and one posted present outstanding. Main thread.
10
+ */
11
+ final class FrameScheduler {
12
+ /** What the scheduler asks of the view. */
13
+ interface Host {
14
+ /** Calls onFrame() on the next Choreographer frame. */
15
+ void postFrameCallback();
16
+
17
+ /** Calls onPosted() from the main looper, behind any draw the window has pending. */
18
+ void postBehindPendingDraw();
19
+
20
+ /** Presents the queued recordings, returning whether any are left. */
21
+ boolean presentFrame();
22
+ }
23
+
24
+ private final Host mHost;
25
+ private boolean mFrameCallbackPosted = false;
26
+ private boolean mPresentPosted = false;
27
+
28
+ FrameScheduler(Host host) {
29
+ mHost = host;
30
+ }
31
+
32
+ /** A recording was submitted. Without a backing view there is no surface to present on yet. */
33
+ void requestFrame(@Nullable BackingViewKind kind) {
34
+ if (kind == null) {
35
+ return;
36
+ }
37
+ // A frame callback still outstanding (a retry, see presentOrRetry) presents
38
+ // the new recording along with the leftovers on that vsync.
39
+ if (mFrameCallbackPosted) {
40
+ return;
41
+ }
42
+ if (presentsOnVsync(kind)) {
43
+ postFrameCallback();
44
+ } else {
45
+ postBehindPendingDraw();
46
+ }
47
+ }
48
+
49
+ /** The Choreographer frame asked for with Host.postFrameCallback(). */
50
+ void onFrame(@Nullable BackingViewKind kind) {
51
+ mFrameCallbackPosted = false;
52
+ if (kind == null) {
53
+ return;
54
+ }
55
+ if (presentsOnVsync(kind)) {
56
+ presentOrRetry();
57
+ } else {
58
+ postBehindPendingDraw();
59
+ }
60
+ }
61
+
62
+ /** The message posted with Host.postBehindPendingDraw(). */
63
+ void onPosted() {
64
+ mPresentPosted = false;
65
+ presentOrRetry();
66
+ }
67
+
68
+ /** The view left its window, and the host removed both callbacks. */
69
+ void cancel() {
70
+ mFrameCallbackPosted = false;
71
+ mPresentPosted = false;
72
+ }
73
+
74
+ // The compositor takes a SurfaceView's buffer at the vsync. A TextureView's frame
75
+ // shows when its window draws, and its buffer queue replaces a frame not drawn yet.
76
+ private static boolean presentsOnVsync(BackingViewKind kind) {
77
+ return switch (kind) {
78
+ case SURFACE_VIEW -> true;
79
+ case TEXTURE_VIEW -> false;
80
+ };
81
+ }
82
+
83
+ // Recordings left over (the present failed, or more were submitted meanwhile)
84
+ // are tried again on the next vsync, never straight away.
85
+ private void presentOrRetry() {
86
+ if (mHost.presentFrame()) {
87
+ postFrameCallback();
88
+ }
89
+ }
90
+
91
+ private void postFrameCallback() {
92
+ if (!mFrameCallbackPosted) {
93
+ mFrameCallbackPosted = true;
94
+ mHost.postFrameCallback();
95
+ }
96
+ }
97
+
98
+ private void postBehindPendingDraw() {
99
+ if (!mPresentPosted) {
100
+ mPresentPosted = true;
101
+ mHost.postBehindPendingDraw();
102
+ }
103
+ }
104
+ }
@@ -8,6 +8,8 @@ import android.view.MotionEvent;
8
8
  import android.view.Surface;
9
9
  import android.view.View;
10
10
 
11
+ import androidx.annotation.Nullable;
12
+
11
13
  import com.facebook.jni.HybridData;
12
14
  import com.facebook.jni.annotations.DoNotStrip;
13
15
  import com.facebook.react.bridge.ReactContext;
@@ -17,14 +19,10 @@ import com.facebook.react.views.view.ReactViewGroup;
17
19
  /**
18
20
  * The Android view behind <Canvas>, <SkiaPictureView> and <SkiaGraphiteView>.
19
21
  * It is backed by a SurfaceView or a TextureView (see updateView()) that the
20
- * native side draws into with Graphite, presenting the queued recordings on
21
- * the Choreographer.
22
+ * native side draws into with Graphite, presenting the queued recordings when
23
+ * the backing view can show them (see FrameScheduler).
22
24
  */
23
25
  public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreographer.FrameCallback {
24
- // Backing view kinds, see updateView().
25
- private static final int KIND_SURFACE_VIEW = 0;
26
- private static final int KIND_TEXTURE_VIEW = 1;
27
-
28
26
  private static final String TAG = "SkiaView";
29
27
 
30
28
  @DoNotStrip
@@ -32,18 +30,36 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
32
30
 
33
31
  private View mView;
34
32
 
35
- // Props, applied together in updateView().
33
+ // Props, applied together in updateView(). A null kind is "auto".
36
34
  private boolean mOpaque = false;
37
- private String mSurfaceType = "auto";
35
+ @Nullable
36
+ private BackingViewKind mRequestedKind = null;
38
37
  private boolean mZOrderOnTop = false;
39
38
  private boolean mHighBitDepth = false;
40
39
 
41
- // What the current backing view was created with.
42
- private int mAppliedKind = -1;
40
+ // What the current backing view was created with; no kind before the first one.
41
+ @Nullable
42
+ private BackingViewKind mAppliedKind = null;
43
43
  private boolean mAppliedZOrderOnTop;
44
44
  private boolean mAppliedHighBitDepth;
45
45
 
46
- private boolean mFramePending = false;
46
+ private final Runnable mPostedPresent = this::onPostedPresent;
47
+ private final FrameScheduler mFrameScheduler = new FrameScheduler(new FrameScheduler.Host() {
48
+ @Override
49
+ public void postFrameCallback() {
50
+ Choreographer.getInstance().postFrameCallback(SkiaView.this);
51
+ }
52
+
53
+ @Override
54
+ public void postBehindPendingDraw() {
55
+ post(mPostedPresent);
56
+ }
57
+
58
+ @Override
59
+ public boolean presentFrame() {
60
+ return SkiaView.this.presentFrame();
61
+ }
62
+ });
47
63
 
48
64
  public SkiaView(Context context) {
49
65
  super(context);
@@ -65,8 +81,8 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
65
81
  mOpaque = value;
66
82
  }
67
83
 
68
- public void setSurfaceType(String value) {
69
- mSurfaceType = value == null ? "auto" : value;
84
+ public void setSurfaceType(@Nullable String value) {
85
+ mRequestedKind = BackingViewKind.fromSurfaceType(value);
70
86
  }
71
87
 
72
88
  public void setZOrderOnTop(boolean value) {
@@ -79,25 +95,22 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
79
95
 
80
96
  // Resolve the backing view from the props. "auto" picks SurfaceView for an
81
97
  // opaque canvas and TextureView for a non-opaque one.
82
- private int resolveKind() {
83
- if ("SurfaceView".equals(mSurfaceType)) {
84
- return KIND_SURFACE_VIEW;
98
+ private BackingViewKind resolveKind() {
99
+ if (mRequestedKind != null) {
100
+ return mRequestedKind;
85
101
  }
86
- if ("TextureView".equals(mSurfaceType)) {
87
- return KIND_TEXTURE_VIEW;
88
- }
89
- return mOpaque ? KIND_SURFACE_VIEW : KIND_TEXTURE_VIEW;
102
+ return mOpaque ? BackingViewKind.SURFACE_VIEW : BackingViewKind.TEXTURE_VIEW;
90
103
  }
91
104
 
92
105
  // The 10-bit buffer format only has 2 bits of alpha, which would visibly
93
106
  // break translucency, and the extra precision would be lost in the 8-bit
94
107
  // composition pass of a TextureView anyway. So the flag only applies to an
95
108
  // opaque SurfaceView.
96
- private boolean resolveHighBitDepth(int kind) {
109
+ private boolean resolveHighBitDepth(BackingViewKind kind) {
97
110
  if (!mHighBitDepth) {
98
111
  return false;
99
112
  }
100
- if (kind != KIND_SURFACE_VIEW || !mOpaque) {
113
+ if (kind != BackingViewKind.SURFACE_VIEW || !mOpaque) {
101
114
  Log.w(TAG, "highBitDepth requires an opaque SurfaceView on Android, falling back to the 8-bit format");
102
115
  return false;
103
116
  }
@@ -109,8 +122,8 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
109
122
  // it attaches (zOrderOnTop, the buffer format), replaces the child; opacity
110
123
  // is applied in place.
111
124
  void updateView() {
112
- int kind = resolveKind();
113
- boolean zOrderOnTop = kind == KIND_SURFACE_VIEW && mZOrderOnTop;
125
+ BackingViewKind kind = resolveKind();
126
+ boolean zOrderOnTop = kind == BackingViewKind.SURFACE_VIEW && mZOrderOnTop;
114
127
  boolean highBitDepth = resolveHighBitDepth(kind);
115
128
  if (mView == null
116
129
  || kind != mAppliedKind
@@ -122,9 +135,10 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
122
135
  mAppliedKind = kind;
123
136
  mAppliedZOrderOnTop = zOrderOnTop;
124
137
  mAppliedHighBitDepth = highBitDepth;
125
- mView = kind == KIND_SURFACE_VIEW
126
- ? new SkiaSurfaceView(getContext(), this, zOrderOnTop, mOpaque)
127
- : new SkiaTextureView(getContext(), this, mOpaque);
138
+ mView = switch (kind) {
139
+ case SURFACE_VIEW -> new SkiaSurfaceView(getContext(), this, zOrderOnTop, mOpaque);
140
+ case TEXTURE_VIEW -> new SkiaTextureView(getContext(), this, mOpaque);
141
+ };
128
142
  addView(mView);
129
143
  // React Native sizes native children explicitly through onLayout, so
130
144
  // the requestLayout triggered by addView is ignored; size the new
@@ -132,10 +146,11 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
132
146
  if (getWidth() > 0 || getHeight() > 0) {
133
147
  mView.layout(0, 0, getWidth(), getHeight());
134
148
  }
135
- } else if (kind == KIND_SURFACE_VIEW) {
136
- ((SkiaSurfaceView) mView).setOpaque(mOpaque);
137
149
  } else {
138
- ((SkiaTextureView) mView).setOpaque(mOpaque);
150
+ switch (kind) {
151
+ case SURFACE_VIEW -> ((SkiaSurfaceView) mView).setOpaque(mOpaque);
152
+ case TEXTURE_VIEW -> ((SkiaTextureView) mView).setOpaque(mOpaque);
153
+ }
139
154
  }
140
155
  }
141
156
 
@@ -157,31 +172,27 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
157
172
 
158
173
  // Frames --------------------------------------------------------------
159
174
 
160
- /**
161
- * Presents the queued recordings on the next vsync. Main thread; called
162
- * from native when a recording is submitted.
163
- */
175
+ /** A recording was submitted. Main thread; called from native. */
164
176
  @DoNotStrip
165
177
  public void scheduleFrame() {
166
- if (!mFramePending) {
167
- mFramePending = true;
168
- Choreographer.getInstance().postFrameCallback(this);
169
- }
178
+ mFrameScheduler.requestFrame(mAppliedKind);
170
179
  }
171
180
 
172
181
  @Override
173
182
  public void doFrame(long frameTimeNanos) {
174
- mFramePending = false;
175
- if (presentFrame()) {
176
- scheduleFrame();
177
- }
183
+ mFrameScheduler.onFrame(mAppliedKind);
184
+ }
185
+
186
+ private void onPostedPresent() {
187
+ mFrameScheduler.onPosted();
178
188
  }
179
189
 
180
190
  @Override
181
191
  protected void onDetachedFromWindow() {
182
192
  super.onDetachedFromWindow();
183
193
  Choreographer.getInstance().removeFrameCallback(this);
184
- mFramePending = false;
194
+ removeCallbacks(mPostedPresent);
195
+ mFrameScheduler.cancel();
185
196
  }
186
197
 
187
198
  @Override
@@ -242,6 +253,6 @@ public class SkiaView extends ReactViewGroup implements SkiaViewAPI, Choreograph
242
253
 
243
254
  private native void unregisterView();
244
255
 
245
- /** Choreographer tick: returns whether more recordings are waiting. */
256
+ /** Presents the queued recordings, returning whether any are left. */
246
257
  private native boolean presentFrame();
247
258
  }
@@ -0,0 +1,186 @@
1
+ package com.reactnative.skia;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+
5
+ import java.util.ArrayDeque;
6
+ import java.util.ArrayList;
7
+ import java.util.Arrays;
8
+ import java.util.Deque;
9
+ import java.util.List;
10
+
11
+ import org.junit.Test;
12
+
13
+ public class FrameSchedulerTest {
14
+ private enum Call {
15
+ POST_FRAME_CALLBACK,
16
+ POST_BEHIND_PENDING_DRAW,
17
+ PRESENT
18
+ }
19
+
20
+ private static final class RecordingHost implements FrameScheduler.Host {
21
+ final List<Call> calls = new ArrayList<>();
22
+ final Deque<Boolean> leftOverAfterPresent = new ArrayDeque<>();
23
+
24
+ @Override
25
+ public void postFrameCallback() {
26
+ calls.add(Call.POST_FRAME_CALLBACK);
27
+ }
28
+
29
+ @Override
30
+ public void postBehindPendingDraw() {
31
+ calls.add(Call.POST_BEHIND_PENDING_DRAW);
32
+ }
33
+
34
+ @Override
35
+ public boolean presentFrame() {
36
+ calls.add(Call.PRESENT);
37
+ return !leftOverAfterPresent.isEmpty() && leftOverAfterPresent.removeFirst();
38
+ }
39
+
40
+ List<Call> takeCalls() {
41
+ List<Call> taken = new ArrayList<>(calls);
42
+ calls.clear();
43
+ return taken;
44
+ }
45
+ }
46
+
47
+ private final RecordingHost mHost = new RecordingHost();
48
+ private final FrameScheduler mScheduler = new FrameScheduler(mHost);
49
+
50
+ @Test
51
+ public void surfaceViewFrameIsPresentedOnTheNextVsync() {
52
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
53
+ assertEquals(Arrays.asList(Call.POST_FRAME_CALLBACK), mHost.takeCalls());
54
+
55
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
56
+ assertEquals(Arrays.asList(Call.PRESENT), mHost.takeCalls());
57
+ }
58
+
59
+ @Test
60
+ public void textureViewFrameIsPresentedBehindThePendingDraw() {
61
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
62
+ assertEquals(Arrays.asList(Call.POST_BEHIND_PENDING_DRAW), mHost.takeCalls());
63
+
64
+ mScheduler.onPosted();
65
+ assertEquals(Arrays.asList(Call.PRESENT), mHost.takeCalls());
66
+ }
67
+
68
+ @Test
69
+ public void textureViewIsNeverPresentedInsideAChoreographerFrame() {
70
+ mScheduler.onFrame(BackingViewKind.TEXTURE_VIEW);
71
+ assertEquals(Arrays.asList(Call.POST_BEHIND_PENDING_DRAW), mHost.takeCalls());
72
+ }
73
+
74
+ @Test
75
+ public void textureViewLeftoverIsPresentedBehindTheDrawOfTheNextVsync() {
76
+ mHost.leftOverAfterPresent.add(true);
77
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
78
+ mScheduler.onPosted();
79
+ assertEquals(
80
+ Arrays.asList(Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT, Call.POST_FRAME_CALLBACK),
81
+ mHost.takeCalls());
82
+
83
+ mScheduler.onFrame(BackingViewKind.TEXTURE_VIEW);
84
+ mScheduler.onPosted();
85
+ assertEquals(Arrays.asList(Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT), mHost.takeCalls());
86
+ }
87
+
88
+ @Test
89
+ public void textureViewRequestWhileARetryIsOutstandingWaitsForThatVsync() {
90
+ mHost.leftOverAfterPresent.add(true);
91
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
92
+ mScheduler.onPosted();
93
+ assertEquals(
94
+ Arrays.asList(Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT, Call.POST_FRAME_CALLBACK),
95
+ mHost.takeCalls());
96
+
97
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
98
+ assertEquals(Arrays.asList(), mHost.takeCalls());
99
+
100
+ mScheduler.onFrame(BackingViewKind.TEXTURE_VIEW);
101
+ mScheduler.onPosted();
102
+ assertEquals(Arrays.asList(Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT), mHost.takeCalls());
103
+ }
104
+
105
+ @Test
106
+ public void surfaceViewLeftoverIsPresentedOnTheNextVsync() {
107
+ mHost.leftOverAfterPresent.add(true);
108
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
109
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
110
+ assertEquals(
111
+ Arrays.asList(Call.POST_FRAME_CALLBACK, Call.PRESENT, Call.POST_FRAME_CALLBACK),
112
+ mHost.takeCalls());
113
+
114
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
115
+ assertEquals(Arrays.asList(Call.PRESENT), mHost.takeCalls());
116
+ }
117
+
118
+ @Test
119
+ public void requestsWaitingForTheSameFrameArePostedOnce() {
120
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
121
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
122
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
123
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
124
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
125
+ assertEquals(
126
+ Arrays.asList(Call.POST_FRAME_CALLBACK, Call.PRESENT, Call.POST_BEHIND_PENDING_DRAW),
127
+ mHost.takeCalls());
128
+ }
129
+
130
+ @Test
131
+ public void requestAfterTheFrameRanIsPostedAgain() {
132
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
133
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
134
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
135
+ mScheduler.onFrame(BackingViewKind.SURFACE_VIEW);
136
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
137
+ mScheduler.onPosted();
138
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
139
+ assertEquals(
140
+ Arrays.asList(
141
+ Call.POST_FRAME_CALLBACK,
142
+ Call.PRESENT,
143
+ Call.POST_FRAME_CALLBACK,
144
+ Call.PRESENT,
145
+ Call.POST_BEHIND_PENDING_DRAW,
146
+ Call.PRESENT,
147
+ Call.POST_BEHIND_PENDING_DRAW),
148
+ mHost.takeCalls());
149
+ }
150
+
151
+ @Test
152
+ public void kindSwitchWhileAFrameCallbackIsOutstandingPresentsBehindThePendingDraw() {
153
+ mScheduler.requestFrame(BackingViewKind.SURFACE_VIEW);
154
+ // updateView() replaced the SurfaceView with a TextureView meanwhile.
155
+ mScheduler.requestFrame(BackingViewKind.TEXTURE_VIEW);
156
+ mScheduler.onFrame(BackingViewKind.TEXTURE_VIEW);
157
+ mScheduler.onPosted();
158
+ assertEquals(
159
+ Arrays.asList(Call.POST_FRAME_CALLBACK, Call.POST_BEHIND_PENDING_DRAW, Call.PRESENT),
160
+ mHost.takeCalls());
161
+ }
162
+
163
+ @Test
164
+ public void withoutABackingViewNothingIsPostedOrPresented() {
165
+ mScheduler.requestFrame(null);
166
+ mScheduler.onFrame(null);
167
+ assertEquals(Arrays.asList(), mHost.takeCalls());
168
+ }
169
+
170
+ @Test
171
+ public void cancelDropsOutstandingRequests() {
172
+ for (BackingViewKind kind : BackingViewKind.values()) {
173
+ mScheduler.requestFrame(kind);
174
+ mScheduler.cancel();
175
+ mScheduler.requestFrame(kind);
176
+ mScheduler.cancel();
177
+ }
178
+ assertEquals(
179
+ Arrays.asList(
180
+ Call.POST_FRAME_CALLBACK,
181
+ Call.POST_FRAME_CALLBACK,
182
+ Call.POST_BEHIND_PENDING_DRAW,
183
+ Call.POST_BEHIND_PENDING_DRAW),
184
+ mHost.takeCalls());
185
+ }
186
+ }
@@ -8,20 +8,22 @@
8
8
 
9
9
  namespace RNSkia {
10
10
 
11
- // WebGPU canvas values are sRGB-encoded regardless of the texture format
12
- // (GPUCanvasConfiguration.colorSpace defaults to "srgb"), so the same shader
13
- // output must display identically on bgra8unorm and rgba16float surfaces.
14
- // Tagging the float layer as (gamma-encoded) extended sRGB matches the
15
- // browser behavior: identical colors, with the extra precision of float16.
11
+ // The surface holds gamma-encoded values in the gamut the view renders in
12
+ // (sRGB or Display P3, see DawnUtils::viewColorSpace) regardless of the
13
+ // texture format, so the same content must display identically on bgra8unorm
14
+ // and rgba16float surfaces. Tagging the float layer with the gamma-encoded
15
+ // extended colorspace of that gamut gives identical colors, with the extra
16
+ // precision of float16.
16
17
  void applyCAMetalLayerColorSpace(void *nativeSurface,
17
- wgpu::TextureFormat format) {
18
+ wgpu::TextureFormat format,
19
+ bool useP3ColorSpace) {
18
20
  CALayer *layer = (__bridge CALayer *)nativeSurface;
19
21
  if (![layer isKindOfClass:[CAMetalLayer class]]) {
20
22
  return;
21
23
  }
22
24
  auto metalLayer = static_cast<CAMetalLayer *>(layer);
23
- setCAMetalLayerColorSpace(metalLayer,
24
- format == wgpu::TextureFormat::RGBA16Float, false);
25
+ setCAMetalLayerColorSpace(
26
+ metalLayer, format == wgpu::TextureFormat::RGBA16Float, useP3ColorSpace);
25
27
  // The change must be set synchronously so the first present already sees
26
28
  // it, and it must reach the render server. On a non-main thread (RN JS or
27
29
  // worklet runtime) the property lands in that thread's implicit
@@ -32,10 +32,21 @@ public:
32
32
  // Create screenshot manager
33
33
  _screenshotService =
34
34
  [[ViewScreenshotService alloc] initWithViewRegistry:viewRegistry];
35
+ _prefersP3ColorSpace = mainScreenSupportsP3();
35
36
  }
36
37
 
37
38
  ~RNSkApplePlatformContext() = default;
38
39
 
40
+ /**
41
+ Whether the main screen has a wide color gamut (Display P3). The screen is
42
+ asked once and the answer is kept: RNSkiaModule asks first, on the main
43
+ queue it is created on, so that the context (created on the JS thread)
44
+ only reads the answer.
45
+ */
46
+ static bool mainScreenSupportsP3();
47
+
48
+ bool prefersP3ColorSpace() override { return _prefersP3ColorSpace; }
49
+
39
50
  void runOnMainThread(std::function<void()>) override;
40
51
 
41
52
  sk_sp<SkImage> takeScreenshotFromViewTag(size_t tag) override;
@@ -66,6 +77,7 @@ public:
66
77
 
67
78
  private:
68
79
  ViewScreenshotService *_screenshotService;
80
+ bool _prefersP3ColorSpace = false;
69
81
  };
70
82
 
71
83
  } // namespace RNSkia