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.
- package/README.md +19 -2
- package/android/build.gradle +15 -7
- package/android/src/main/java/com/reactnative/skia/BackingViewKind.java +24 -0
- package/android/src/main/java/com/reactnative/skia/FrameScheduler.java +104 -0
- package/android/src/main/java/com/reactnative/skia/SkiaView.java +55 -44
- package/android/src/test/java/com/reactnative/skia/FrameSchedulerTest.java +186 -0
- package/apple/MetalLayerColorSpace.mm +10 -8
- package/apple/RNSkApplePlatformContext.h +12 -0
- package/apple/RNSkApplePlatformContext.mm +17 -0
- package/apple/RNSkMetalCanvasProvider.mm +4 -2
- package/apple/RNSkiaModule.mm +10 -0
- package/cpp/rnskia/RNDawnContext.h +3 -2
- package/cpp/rnskia/RNDawnUtils.h +12 -0
- package/cpp/rnskia/RNDawnWindowContext.cpp +2 -1
- package/cpp/rnskia/RNDawnWindowContext.h +15 -7
- package/cpp/rnskia/RNMetalLayerColorSpace.h +4 -2
- package/cpp/rnskia/RNSkCanvasProvider.h +18 -8
- package/cpp/rnskia/RNSkGraphiteTarget.h +20 -7
- package/cpp/rnskia/RNSkPlatformContext.h +8 -0
- package/cpp/rnskia/RNSkView.h +53 -29
- package/lib/commonjs/external/reanimated/interpolators.js +1 -1
- package/lib/commonjs/external/reanimated/interpolators.js.map +1 -1
- package/lib/commonjs/renderer/Canvas.js +1 -1
- package/lib/commonjs/renderer/Canvas.js.map +1 -1
- package/lib/module/external/reanimated/interpolators.js +1 -1
- package/lib/module/external/reanimated/interpolators.js.map +1 -1
- package/lib/module/renderer/Canvas.js +1 -1
- package/lib/module/renderer/Canvas.js.map +1 -1
- package/package.json +1 -1
- package/react-native-skia.podspec +1 -1
- package/src/external/reanimated/interpolators.ts +1 -1
- 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
|
-
|
|
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
|
-
|
|
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).
|
package/android/build.gradle
CHANGED
|
@@ -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
|
|
21
|
-
* the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
83
|
-
if (
|
|
84
|
-
return
|
|
98
|
+
private BackingViewKind resolveKind() {
|
|
99
|
+
if (mRequestedKind != null) {
|
|
100
|
+
return mRequestedKind;
|
|
85
101
|
}
|
|
86
|
-
|
|
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(
|
|
109
|
+
private boolean resolveHighBitDepth(BackingViewKind kind) {
|
|
97
110
|
if (!mHighBitDepth) {
|
|
98
111
|
return false;
|
|
99
112
|
}
|
|
100
|
-
if (kind !=
|
|
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
|
-
|
|
113
|
-
boolean zOrderOnTop = kind ==
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
12
|
-
// (
|
|
13
|
-
//
|
|
14
|
-
// Tagging the float layer
|
|
15
|
-
//
|
|
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(
|
|
24
|
-
|
|
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
|