react-native-webgpu 0.15.1 → 0.15.2
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.
|
@@ -156,9 +156,58 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceChanged(
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
namespace {
|
|
160
|
+
|
|
161
|
+
// Weak handle on a Java view and one of its no-argument methods, for native
|
|
162
|
+
// 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
|
+
struct WeakViewCall {
|
|
168
|
+
jweak view = nullptr;
|
|
169
|
+
jmethodID method = nullptr;
|
|
170
|
+
|
|
171
|
+
WeakViewCall(JNIEnv *env, jobject thiz, const char *methodName) {
|
|
172
|
+
view = env->NewWeakGlobalRef(thiz);
|
|
173
|
+
jclass cls = env->GetObjectClass(thiz);
|
|
174
|
+
method = env->GetMethodID(cls, methodName, "()V");
|
|
175
|
+
env->DeleteLocalRef(cls);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
~WeakViewCall() {
|
|
179
|
+
// May run on any thread; ThreadScope attaches if needed.
|
|
180
|
+
facebook::jni::ThreadScope scope;
|
|
181
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
182
|
+
if (env != nullptr && view != nullptr) {
|
|
183
|
+
env->DeleteWeakGlobalRef(view);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Called on a native thread, outside the SurfaceInfo locks.
|
|
188
|
+
void operator()() const {
|
|
189
|
+
facebook::jni::ThreadScope scope;
|
|
190
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
191
|
+
if (env == nullptr || method == nullptr) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
jobject local = env->NewLocalRef(view);
|
|
195
|
+
if (local == nullptr) {
|
|
196
|
+
return; // the view was garbage collected
|
|
197
|
+
}
|
|
198
|
+
env->CallVoidMethod(local, method);
|
|
199
|
+
if (env->ExceptionCheck()) {
|
|
200
|
+
env->ExceptionClear();
|
|
201
|
+
}
|
|
202
|
+
env->DeleteLocalRef(local);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
} // namespace
|
|
207
|
+
|
|
159
208
|
extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
|
|
160
209
|
JNIEnv *env, jobject thiz, jobject jSurface, jint contextId, jfloat width,
|
|
161
|
-
jfloat height) {
|
|
210
|
+
jfloat height, jboolean reportPresents) {
|
|
162
211
|
if (manager == nullptr) {
|
|
163
212
|
return;
|
|
164
213
|
}
|
|
@@ -179,6 +228,15 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
|
|
|
179
228
|
surface, [](void *nativeSurface) {
|
|
180
229
|
ANativeWindow_release(static_cast<ANativeWindow *>(nativeSurface));
|
|
181
230
|
});
|
|
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)(); });
|
|
237
|
+
} else {
|
|
238
|
+
info->setFramePresentedCallback(nullptr);
|
|
239
|
+
}
|
|
182
240
|
// The attach is adopted at the next frame boundary by the rendering thread;
|
|
183
241
|
// schedule a flush so contexts that are not currently rendering still pick
|
|
184
242
|
// it up (and present their last offscreen frame).
|
|
@@ -190,6 +248,7 @@ Java_com_webgpu_WebGPUView_switchToOffscreenSurface(JNIEnv *env, jobject thiz,
|
|
|
190
248
|
jint contextId) {
|
|
191
249
|
auto ®istry = rnwgpu::SurfaceRegistry::getInstance();
|
|
192
250
|
if (auto info = registry.getSurfaceInfo(contextId)) {
|
|
251
|
+
info->setFramePresentedCallback(nullptr);
|
|
193
252
|
info->switchToOffscreen();
|
|
194
253
|
}
|
|
195
254
|
}
|
|
@@ -202,6 +261,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onViewDestroyed(
|
|
|
202
261
|
// surface; see RNWebGPU::destroyContext for the ownership split.
|
|
203
262
|
auto ®istry = rnwgpu::SurfaceRegistry::getInstance();
|
|
204
263
|
if (auto info = registry.getSurfaceInfo(contextId)) {
|
|
264
|
+
info->setFramePresentedCallback(nullptr);
|
|
205
265
|
info->detachSurface();
|
|
206
266
|
}
|
|
207
267
|
registry.removeSurfaceInfo(contextId);
|
|
@@ -210,53 +270,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onViewDestroyed(
|
|
|
210
270
|
// --- WebGPUHardwareBufferView (AHB-pool presentation)
|
|
211
271
|
// ---------------------------------
|
|
212
272
|
|
|
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
273
|
// Turn on pool mode for this context. dpW/dpH is the canvas-client (dp) size;
|
|
261
274
|
// the native pool buffers are sized from the canvas drawing buffer lazily.
|
|
262
275
|
extern "C" JNIEXPORT void JNICALL
|
|
@@ -267,7 +280,7 @@ Java_com_webgpu_WebGPUHardwareBufferView_nEnablePool(JNIEnv *env, jobject thiz,
|
|
|
267
280
|
auto info = registry.getSurfaceInfoOrCreate(
|
|
268
281
|
contextId, rnwgpu::WebGPURuntime::shared().instance(),
|
|
269
282
|
static_cast<int>(dpW), static_cast<int>(dpH));
|
|
270
|
-
auto waker = std::make_shared<
|
|
283
|
+
auto waker = std::make_shared<WeakViewCall>(env, thiz, "onNativeFrameReady");
|
|
271
284
|
info->setPoolFrameReadyCallback([waker]() { (*waker)(); });
|
|
272
285
|
info->enablePool(static_cast<int>(dpW), static_cast<int>(dpH));
|
|
273
286
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
* 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.
|
|
30
|
+
*/
|
|
31
|
+
final class SurfaceFrameRateVote {
|
|
32
|
+
private static final String TAG = "SurfaceFrameRateVote";
|
|
33
|
+
|
|
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;
|
|
47
|
+
|
|
48
|
+
private final View mView;
|
|
49
|
+
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
|
50
|
+
private final Runnable mIdleCheck = this::checkIdle;
|
|
51
|
+
|
|
52
|
+
@Nullable
|
|
53
|
+
private Surface mSurface;
|
|
54
|
+
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;
|
|
61
|
+
|
|
62
|
+
/** The view the surface belongs to; its display decides the rate to ask for. */
|
|
63
|
+
SurfaceFrameRateVote(View view) {
|
|
64
|
+
mView = view;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The surface was created: it is voted on until detach(). Main thread. */
|
|
68
|
+
synchronized void attach(Surface surface) {
|
|
69
|
+
detach();
|
|
70
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
mSurface = surface;
|
|
74
|
+
}
|
|
75
|
+
|
|
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();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (mVoting && !mIdleCheckPending) {
|
|
91
|
+
scheduleIdleCheck(IDLE_AFTER_NANOS);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 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;
|
|
99
|
+
}
|
|
100
|
+
withdraw();
|
|
101
|
+
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();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private void withdraw() {
|
|
126
|
+
if (!mVoting) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
mVoting = false;
|
|
130
|
+
if (mSurface == null || !mSurface.isValid()) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
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);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
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) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
long idleNanos = System.nanoTime() - mLastAnimatingNanos;
|
|
151
|
+
if (idleNanos >= IDLE_AFTER_NANOS) {
|
|
152
|
+
withdraw();
|
|
153
|
+
} else {
|
|
154
|
+
scheduleIdleCheck(IDLE_AFTER_NANOS - idleNanos);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The highest rate a seamless switch from the current mode can reach, which
|
|
159
|
+
// is all the vote may ask for. Resolved when the vote is cast, so that it
|
|
160
|
+
// follows the display the view is on.
|
|
161
|
+
private float resolveMaxRefreshRate() {
|
|
162
|
+
Display display = mView.getDisplay();
|
|
163
|
+
if (display == null) {
|
|
164
|
+
return 0f;
|
|
165
|
+
}
|
|
166
|
+
Display.Mode current = display.getMode();
|
|
167
|
+
float maxRefreshRate = current.getRefreshRate();
|
|
168
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
169
|
+
for (float refreshRate : current.getAlternativeRefreshRates()) {
|
|
170
|
+
maxRefreshRate = Math.max(maxRefreshRate, refreshRate);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Android 11 does not tell which modes are a seamless switch away: the
|
|
174
|
+
// ones at the current resolution usually are.
|
|
175
|
+
for (Display.Mode mode : display.getSupportedModes()) {
|
|
176
|
+
if (mode.getPhysicalWidth() == current.getPhysicalWidth()
|
|
177
|
+
&& mode.getPhysicalHeight() == current.getPhysicalHeight()) {
|
|
178
|
+
maxRefreshRate = Math.max(maxRefreshRate, mode.getRefreshRate());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return maxRefreshRate;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -24,6 +24,9 @@ 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(this);
|
|
27
30
|
|
|
28
31
|
WebGPUView(Context context) {
|
|
29
32
|
super(context);
|
|
@@ -134,7 +137,11 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
|
|
|
134
137
|
float density = getResources().getDisplayMetrics().density;
|
|
135
138
|
float width = getWidth() / density;
|
|
136
139
|
float height = getHeight() / density;
|
|
137
|
-
|
|
140
|
+
boolean votesFrameRate = mAppliedKind == KIND_SURFACE_VIEW;
|
|
141
|
+
if (votesFrameRate) {
|
|
142
|
+
mFrameRateVote.attach(surface);
|
|
143
|
+
}
|
|
144
|
+
onSurfaceCreate(surface, mContextId, width, height, votesFrameRate);
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
@Override
|
|
@@ -147,9 +154,20 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
|
|
|
147
154
|
|
|
148
155
|
@Override
|
|
149
156
|
public void surfaceOffscreen() {
|
|
157
|
+
// The surface is still valid here, so the vote can be taken back.
|
|
158
|
+
mFrameRateVote.detach();
|
|
150
159
|
switchToOffscreenSurface(mContextId);
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Called from native (SurfaceInfo::presentFrame) on the thread that
|
|
164
|
+
* presented a frame to the surface, only for a SurfaceView.
|
|
165
|
+
*/
|
|
166
|
+
@DoNotStrip
|
|
167
|
+
void onFramePresented() {
|
|
168
|
+
mFrameRateVote.onFramePresented();
|
|
169
|
+
}
|
|
170
|
+
|
|
153
171
|
@Override
|
|
154
172
|
public int getContextId() {
|
|
155
173
|
return mContextId;
|
|
@@ -160,15 +178,19 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
|
|
|
160
178
|
* view: the view dies with its Canvas, so it retires the registry entry.
|
|
161
179
|
*/
|
|
162
180
|
public void destroy() {
|
|
181
|
+
mFrameRateVote.detach();
|
|
163
182
|
onViewDestroyed(mContextId);
|
|
164
183
|
}
|
|
165
184
|
|
|
185
|
+
// reportPresents asks native to call onFramePresented() for each frame
|
|
186
|
+
// presented to this surface.
|
|
166
187
|
@DoNotStrip
|
|
167
188
|
private native void onSurfaceCreate(
|
|
168
189
|
Surface surface,
|
|
169
190
|
int contextId,
|
|
170
191
|
float width,
|
|
171
|
-
float height
|
|
192
|
+
float height,
|
|
193
|
+
boolean reportPresents
|
|
172
194
|
);
|
|
173
195
|
|
|
174
196
|
@DoNotStrip
|
|
@@ -548,16 +548,27 @@ public:
|
|
|
548
548
|
if (device) {
|
|
549
549
|
dawn::native::metal::WaitForCommandsToBeScheduled(device.Get());
|
|
550
550
|
}
|
|
551
|
+
#endif
|
|
552
|
+
#if defined(__ANDROID__)
|
|
553
|
+
std::function<void()> framePresented;
|
|
551
554
|
#endif
|
|
552
555
|
{
|
|
553
556
|
std::unique_lock<std::shared_mutex> lock(_mutex);
|
|
554
557
|
if (_surface && _acquiredFromSurface) {
|
|
555
558
|
_surface.Present();
|
|
559
|
+
#if defined(__ANDROID__)
|
|
560
|
+
framePresented = _framePresented;
|
|
561
|
+
#endif
|
|
556
562
|
}
|
|
557
563
|
_acquiredFromSurface = false;
|
|
558
564
|
_frameInFlight = false;
|
|
559
565
|
_frameEpoch++;
|
|
560
566
|
}
|
|
567
|
+
#if defined(__ANDROID__)
|
|
568
|
+
if (framePresented) {
|
|
569
|
+
framePresented();
|
|
570
|
+
}
|
|
571
|
+
#endif
|
|
561
572
|
applyPendingAttach();
|
|
562
573
|
}
|
|
563
574
|
|
|
@@ -691,6 +702,16 @@ public:
|
|
|
691
702
|
std::lock_guard<std::mutex> poolLock(_poolMutex);
|
|
692
703
|
_poolFrameReady = std::move(cb);
|
|
693
704
|
}
|
|
705
|
+
|
|
706
|
+
// Invoked from the presenting thread (never under _mutex) after each frame
|
|
707
|
+
// presentFrame() shows on the attached surface; the blit of the last
|
|
708
|
+
// offscreen frame onto a newly attached surface does not count. The view
|
|
709
|
+
// uses it to vote the display's refresh rate while the canvas animates
|
|
710
|
+
// (see SurfaceFrameRateVote.java). Pass nullptr to unregister.
|
|
711
|
+
void setFramePresentedCallback(std::function<void()> cb) {
|
|
712
|
+
std::unique_lock<std::shared_mutex> lock(_mutex);
|
|
713
|
+
_framePresented = std::move(cb);
|
|
714
|
+
}
|
|
694
715
|
#endif
|
|
695
716
|
|
|
696
717
|
private:
|
|
@@ -1239,6 +1260,9 @@ private:
|
|
|
1239
1260
|
static constexpr int kPoolSize =
|
|
1240
1261
|
5; // 2 held + 1 rendering + 1 waiting + 1 ready
|
|
1241
1262
|
|
|
1263
|
+
// See setFramePresentedCallback(); read under _mutex, called outside it.
|
|
1264
|
+
std::function<void()> _framePresented;
|
|
1265
|
+
|
|
1242
1266
|
std::atomic<bool> _poolMode{false};
|
|
1243
1267
|
std::mutex _poolMutex;
|
|
1244
1268
|
std::condition_variable _freeCv; // a slot returned to Free
|