react-native-webgpu 0.10.2 → 0.10.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.
@@ -6,6 +6,7 @@
6
6
  #include <jsi/jsi.h>
7
7
 
8
8
  #include <ReactCommon/CallInvokerHolder.h>
9
+ #include <android/hardware_buffer_jni.h>
9
10
  #include <android/native_window_jni.h>
10
11
  #include <webgpu/webgpu_cpp.h>
11
12
 
@@ -25,7 +26,8 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUModule_initializeNative(
25
26
  auto jsCallInvoker{
26
27
  facebook::jni::alias_ref<facebook::react::CallInvokerHolder::javaobject>{
27
28
  reinterpret_cast<facebook::react::CallInvokerHolder::javaobject>(
28
- jsCallInvokerHolder)} -> cthis()->getCallInvoker()};
29
+ jsCallInvokerHolder)} -> cthis()
30
+ ->getCallInvoker()};
29
31
  auto platformContext =
30
32
  std::make_shared<rnwgpu::AndroidPlatformContext>(globalBlobModule);
31
33
  manager = std::make_shared<rnwgpu::RNWebGPUManager>(runtime, jsCallInvoker,
@@ -91,3 +93,142 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onViewDestroyed(
91
93
  }
92
94
  registry.removeSurfaceInfo(contextId);
93
95
  }
96
+
97
+ // --- WebGPUHardwareBufferView (AHB-pool presentation)
98
+ // ---------------------------------
99
+
100
+ namespace {
101
+
102
+ // Weak handle on a WebGPUHardwareBufferView used by the native fence waiter to
103
+ // wake the UI thread when a frame is ready. Holding it weakly means a view that
104
+ // RN has dropped is not kept alive by a SurfaceInfo that outlives it (the
105
+ // registry keeps the SurfaceInfo until onDropViewInstance).
106
+ struct HardwareBufferViewWaker {
107
+ jweak view = nullptr;
108
+ jmethodID onFrameReady = nullptr;
109
+
110
+ HardwareBufferViewWaker(JNIEnv *env, jobject thiz) {
111
+ view = env->NewWeakGlobalRef(thiz);
112
+ jclass cls = env->GetObjectClass(thiz);
113
+ onFrameReady = env->GetMethodID(cls, "onNativeFrameReady", "()V");
114
+ env->DeleteLocalRef(cls);
115
+ }
116
+
117
+ ~HardwareBufferViewWaker() {
118
+ // May run on the waiter or JS thread; ThreadScope attaches if needed.
119
+ facebook::jni::ThreadScope scope;
120
+ JNIEnv *env = facebook::jni::Environment::current();
121
+ if (env != nullptr && view != nullptr) {
122
+ env->DeleteWeakGlobalRef(view);
123
+ }
124
+ }
125
+
126
+ // Called on the waiter thread, outside the pool lock.
127
+ void operator()() const {
128
+ facebook::jni::ThreadScope scope;
129
+ JNIEnv *env = facebook::jni::Environment::current();
130
+ if (env == nullptr || onFrameReady == nullptr) {
131
+ return;
132
+ }
133
+ jobject local = env->NewLocalRef(view);
134
+ if (local == nullptr) {
135
+ return; // the view was garbage collected
136
+ }
137
+ env->CallVoidMethod(local, onFrameReady);
138
+ if (env->ExceptionCheck()) {
139
+ env->ExceptionClear();
140
+ }
141
+ env->DeleteLocalRef(local);
142
+ }
143
+ };
144
+
145
+ } // namespace
146
+
147
+ // Turn on pool mode for this context. dpW/dpH is the canvas-client (dp) size;
148
+ // the native pool buffers are sized from the canvas drawing buffer lazily.
149
+ extern "C" JNIEXPORT void JNICALL
150
+ Java_com_webgpu_WebGPUHardwareBufferView_nEnablePool(JNIEnv *env, jobject thiz,
151
+ jint contextId, jint dpW,
152
+ jint dpH) {
153
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
154
+ auto info = registry.getSurfaceInfoOrCreate(
155
+ contextId, manager->_gpu, static_cast<int>(dpW), static_cast<int>(dpH));
156
+ auto waker = std::make_shared<HardwareBufferViewWaker>(env, thiz);
157
+ info->setPoolFrameReadyCallback([waker]() { (*waker)(); });
158
+ info->enablePool(static_cast<int>(dpW), static_cast<int>(dpH));
159
+ }
160
+
161
+ // Keep the canvas-client (dp) size in sync on resize.
162
+ extern "C" JNIEXPORT void JNICALL
163
+ Java_com_webgpu_WebGPUHardwareBufferView_nSetClientSize(JNIEnv *env,
164
+ jobject thiz,
165
+ jint contextId,
166
+ jint dpW, jint dpH) {
167
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
168
+ auto info = registry.getSurfaceInfo(contextId);
169
+ if (info != nullptr) {
170
+ info->setPoolClientSize(static_cast<int>(dpW), static_cast<int>(dpH));
171
+ }
172
+ }
173
+
174
+ // The HardwareBuffer backing a (generation, slot), for the view to wrap in a
175
+ // Bitmap. Returns null for a retired generation.
176
+ extern "C" JNIEXPORT jobject JNICALL
177
+ Java_com_webgpu_WebGPUHardwareBufferView_nGetHardwareBuffer(
178
+ JNIEnv *env, jobject thiz, jint contextId, jint generation, jint slot) {
179
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
180
+ auto info = registry.getSurfaceInfo(contextId);
181
+ if (info == nullptr) {
182
+ return nullptr;
183
+ }
184
+ void *ahb = info->poolBufferForDisplay(static_cast<uint32_t>(generation),
185
+ static_cast<int>(slot));
186
+ if (ahb == nullptr) {
187
+ return nullptr;
188
+ }
189
+ // toHardwareBuffer acquires its own ref for the returned jobject; the pool
190
+ // keeps the underlying buffer alive independently.
191
+ return AHardwareBuffer_toHardwareBuffer(env,
192
+ static_cast<AHardwareBuffer *>(ahb));
193
+ }
194
+
195
+ // Latest signaled frame ready to display, encoded (generation << 32 | slot), or
196
+ // -1 when nothing is new. Called on the UI thread after onNativeFrameReady.
197
+ extern "C" JNIEXPORT jlong JNICALL
198
+ Java_com_webgpu_WebGPUHardwareBufferView_nPollReady(JNIEnv *env, jobject thiz,
199
+ jint contextId) {
200
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
201
+ auto info = registry.getSurfaceInfo(contextId);
202
+ if (info == nullptr) {
203
+ return -1;
204
+ }
205
+ return info->poolPollReady();
206
+ }
207
+
208
+ // The view is done displaying (and holding) a slot; return it to the pool.
209
+ extern "C" JNIEXPORT void JNICALL
210
+ Java_com_webgpu_WebGPUHardwareBufferView_nReleaseSlot(JNIEnv *env, jobject thiz,
211
+ jint contextId,
212
+ jint generation,
213
+ jint slot) {
214
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
215
+ auto info = registry.getSurfaceInfo(contextId);
216
+ if (info != nullptr) {
217
+ info->poolReleaseSlot(static_cast<uint32_t>(generation),
218
+ static_cast<int>(slot));
219
+ }
220
+ }
221
+
222
+ // View detached / hidden: keep the canvas alive by falling back to an offscreen
223
+ // texture (mirrors switchToOffscreenSurface for the surface path).
224
+ extern "C" JNIEXPORT void JNICALL
225
+ Java_com_webgpu_WebGPUHardwareBufferView_nSwitchToOffscreen(JNIEnv *env,
226
+ jobject thiz,
227
+ jint contextId) {
228
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
229
+ auto info = registry.getSurfaceInfo(contextId);
230
+ if (info != nullptr) {
231
+ info->setPoolFrameReadyCallback(nullptr);
232
+ info->switchToOffscreen();
233
+ }
234
+ }
@@ -22,4 +22,6 @@ public interface WebGPUAPI {
22
22
  );
23
23
 
24
24
  void surfaceOffscreen();
25
+
26
+ int getContextId();
25
27
  }
@@ -0,0 +1,278 @@
1
+ package com.webgpu;
2
+
3
+ import android.annotation.SuppressLint;
4
+ import android.content.Context;
5
+ import android.graphics.Bitmap;
6
+ import android.graphics.Canvas;
7
+ import android.graphics.Paint;
8
+ import android.graphics.Rect;
9
+ import android.hardware.HardwareBuffer;
10
+ import android.os.Build;
11
+ import android.os.Handler;
12
+ import android.os.Looper;
13
+ import android.view.View;
14
+
15
+ import androidx.annotation.Keep;
16
+ import androidx.annotation.NonNull;
17
+ import androidx.annotation.RequiresApi;
18
+
19
+ import java.util.ArrayDeque;
20
+ import java.util.HashMap;
21
+ import java.util.Iterator;
22
+ import java.util.Map;
23
+ import java.util.concurrent.atomic.AtomicBoolean;
24
+
25
+ /**
26
+ * A "normal RN view" backend for the WebGPU canvas.
27
+ *
28
+ * <p>WebGPU renders into a native pool of AHardwareBuffers (sized from the canvas drawing buffer,
29
+ * like the swapchain) that Dawn imports as SharedTextureMemory; see SurfaceRegistry.h. Each finished
30
+ * buffer is drawn inline here via {@link Bitmap#wrapHardwareBuffer} + {@link Canvas#drawBitmap}, so
31
+ * this is a plain {@link View}: parent transforms, clipping, alpha, z-order and animations all
32
+ * apply, with no GL interop and no extra copy.
33
+ *
34
+ * <p>This view is a pure consumer: it enables pool mode, and whenever the native fence waiter
35
+ * signals that a frame is ready it asks native for the latest ready (gen, slot), wraps that buffer
36
+ * in a cached Bitmap, and draws it scaled to the current bounds. Acquire is rigorous (native blocks
37
+ * on Dawn's render-complete fence before a frame is ready); release is heuristic via a held-ring of
38
+ * {@link #HELD_MAX} frames.
39
+ */
40
+ @RequiresApi(api = Build.VERSION_CODES.Q)
41
+ @SuppressLint("ViewConstructor")
42
+ public class WebGPUHardwareBufferView extends View {
43
+
44
+ private static final int HELD_MAX = 2; // displayed frames kept before release (release safety)
45
+
46
+ private final WebGPUAPI mApi;
47
+ private final Paint mPaint;
48
+ private final Rect mSrc = new Rect();
49
+ private final Rect mDst = new Rect();
50
+
51
+ // token (generation << 32 | slot) -> wrapped hardware Bitmap (one per slot per generation).
52
+ private final Map<Long, Bitmap> mBitmaps = new HashMap<>();
53
+ // Held-ring of displayed frame tokens (newest last). The newest is what onDraw shows.
54
+ private final ArrayDeque<Long> mHeld = new ArrayDeque<>();
55
+
56
+ // Native -> UI thread wake-up. The fence waiter calls onNativeFrameReady() off the UI thread;
57
+ // we coalesce those into at most one pending consume() on the main looper.
58
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
59
+ private final AtomicBoolean mConsumePosted = new AtomicBoolean(false);
60
+ private final Runnable mConsume = this::consume;
61
+
62
+ private Bitmap mDisplayed;
63
+ private int mDisplayedW;
64
+ private int mDisplayedH;
65
+ private int mCurrentGen;
66
+
67
+ private boolean mAttached;
68
+ private boolean mEnabled;
69
+
70
+ public WebGPUHardwareBufferView(Context context, WebGPUAPI api) {
71
+ super(context);
72
+ mApi = api;
73
+ mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
74
+ setWillNotDraw(false);
75
+ }
76
+
77
+ private int contextId() {
78
+ return mApi.getContextId();
79
+ }
80
+
81
+ private static int genOf(long token) {
82
+ return (int) (token >>> 32);
83
+ }
84
+
85
+ private static int slotOf(long token) {
86
+ return (int) (token & 0xffffffffL);
87
+ }
88
+
89
+ // --- Sizing ----------------------------------------------------------------
90
+
91
+ private void enablePool(int w, int h) {
92
+ final float density = getResources().getDisplayMetrics().density;
93
+ nEnablePool(contextId(), Math.max(1, Math.round(w / density)),
94
+ Math.max(1, Math.round(h / density)));
95
+ mEnabled = true;
96
+ }
97
+
98
+ @Override
99
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
100
+ super.onSizeChanged(w, h, oldw, oldh);
101
+ if (w <= 0 || h <= 0) {
102
+ return;
103
+ }
104
+ if (!mEnabled) {
105
+ enablePool(w, h);
106
+ } else {
107
+ // Native reallocates the pool from the canvas drawing buffer; we only keep the dp
108
+ // canvas-client size in sync so JS computes the right canvas.width/height.
109
+ final float density = getResources().getDisplayMetrics().density;
110
+ nSetClientSize(contextId(), Math.max(1, Math.round(w / density)),
111
+ Math.max(1, Math.round(h / density)));
112
+ }
113
+ }
114
+
115
+ // --- Consume (UI thread, woken by the native fence waiter) -----------------
116
+
117
+ /** Called from the native fence-waiter thread (see cpp-adapter.cpp) when a frame is ready. */
118
+ @Keep
119
+ private void onNativeFrameReady() {
120
+ if (mConsumePosted.compareAndSet(false, true)) {
121
+ mMainHandler.post(mConsume);
122
+ }
123
+ }
124
+
125
+ private void consume() {
126
+ mConsumePosted.set(false);
127
+ if (!mAttached) {
128
+ return;
129
+ }
130
+ long token = nPollReady(contextId());
131
+ if (token >= 0) {
132
+ onFrameReady(token);
133
+ }
134
+ }
135
+
136
+ private void onFrameReady(long token) {
137
+ final int gen = genOf(token);
138
+ final int slot = slotOf(token);
139
+
140
+ if (gen > mCurrentGen) {
141
+ mCurrentGen = gen;
142
+ recycleStaleGenerations();
143
+ }
144
+
145
+ Bitmap bmp = mBitmaps.get(token);
146
+ if (bmp == null) {
147
+ HardwareBuffer hb = nGetHardwareBuffer(contextId(), gen, slot);
148
+ if (hb == null) {
149
+ return; // generation already retired; drop this stale frame
150
+ }
151
+ // wrapHardwareBuffer takes its own ref, so the wrapper can be closed immediately.
152
+ bmp = Bitmap.wrapHardwareBuffer(hb, null);
153
+ hb.close();
154
+ if (bmp == null) {
155
+ // Cannot display this frame; hand the slot straight back so the pool
156
+ // does not drain (native marked it Displayed when we polled it).
157
+ nReleaseSlot(contextId(), gen, slot);
158
+ return;
159
+ }
160
+ mBitmaps.put(token, bmp);
161
+ }
162
+
163
+ mDisplayed = bmp;
164
+ mDisplayedW = bmp.getWidth();
165
+ mDisplayedH = bmp.getHeight();
166
+
167
+ mHeld.addLast(token);
168
+ while (mHeld.size() > HELD_MAX) {
169
+ long old = mHeld.pollFirst();
170
+ if (genOf(old) == mCurrentGen) {
171
+ // Live generation reuses slots: hand it back once HWUI has had time to sample it.
172
+ nReleaseSlot(contextId(), mCurrentGen, slotOf(old));
173
+ } else {
174
+ // Old generation: its slot is never re-rendered, so just drop the Bitmap.
175
+ recycleToken(old);
176
+ }
177
+ }
178
+ invalidate();
179
+ }
180
+
181
+ private void recycleStaleGenerations() {
182
+ // Keep the current and previous generation (cross-fade); recycle older cached Bitmaps that are
183
+ // no longer held.
184
+ Iterator<Map.Entry<Long, Bitmap>> it = mBitmaps.entrySet().iterator();
185
+ while (it.hasNext()) {
186
+ Map.Entry<Long, Bitmap> e = it.next();
187
+ if (genOf(e.getKey()) < mCurrentGen - 1 && !mHeld.contains(e.getKey())) {
188
+ Bitmap b = e.getValue();
189
+ if (b != mDisplayed && !b.isRecycled()) {
190
+ b.recycle();
191
+ }
192
+ it.remove();
193
+ }
194
+ }
195
+ }
196
+
197
+ private void recycleToken(long token) {
198
+ Bitmap b = mBitmaps.remove(token);
199
+ if (b != null && b != mDisplayed && !b.isRecycled()) {
200
+ b.recycle();
201
+ }
202
+ }
203
+
204
+ @Override
205
+ protected void onDraw(Canvas canvas) {
206
+ super.onDraw(canvas);
207
+ Bitmap bmp = mDisplayed;
208
+ if (bmp == null || bmp.isRecycled()) {
209
+ return;
210
+ }
211
+ // Always scale the last good frame to the current bounds, so a buffer that lags the view size
212
+ // (mid-resize) is shown stretched rather than blank.
213
+ mSrc.set(0, 0, mDisplayedW, mDisplayedH);
214
+ mDst.set(0, 0, getWidth(), getHeight());
215
+ canvas.drawBitmap(bmp, mSrc, mDst, mPaint);
216
+ }
217
+
218
+ // --- Lifecycle -------------------------------------------------------------
219
+
220
+ @Override
221
+ protected void onAttachedToWindow() {
222
+ super.onAttachedToWindow();
223
+ mAttached = true;
224
+ if (!mEnabled && getWidth() > 0 && getHeight() > 0) {
225
+ enablePool(getWidth(), getHeight());
226
+ }
227
+ // Pick up anything that became ready while we were detached.
228
+ onNativeFrameReady();
229
+ }
230
+
231
+ @Override
232
+ protected void onDetachedFromWindow() {
233
+ super.onDetachedFromWindow();
234
+ mAttached = false;
235
+ mEnabled = false;
236
+ mMainHandler.removeCallbacks(mConsume);
237
+ mConsumePosted.set(false);
238
+
239
+ // Keep the canvas alive offscreen, then drop all GPU resources. The
240
+ // SurfaceInfo stays registered so a transient detach/re-attach keeps
241
+ // working; WebGPUViewManager.onDropViewInstance removes it when RN drops
242
+ // the view for good.
243
+ nSwitchToOffscreen(contextId());
244
+
245
+ mDisplayed = null;
246
+ mHeld.clear();
247
+ for (Bitmap b : mBitmaps.values()) {
248
+ if (!b.isRecycled()) {
249
+ b.recycle();
250
+ }
251
+ }
252
+ mBitmaps.clear();
253
+ mCurrentGen = 0;
254
+ }
255
+
256
+ @Override
257
+ protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
258
+ super.onVisibilityChanged(changedView, visibility);
259
+ if (visibility == VISIBLE && mAttached && !mEnabled
260
+ && getWidth() > 0 && getHeight() > 0) {
261
+ enablePool(getWidth(), getHeight());
262
+ }
263
+ }
264
+
265
+ // --- Native (cpp-adapter.cpp) ----------------------------------------------
266
+
267
+ private native void nEnablePool(int contextId, int dpW, int dpH);
268
+
269
+ private native void nSetClientSize(int contextId, int dpW, int dpH);
270
+
271
+ private native long nPollReady(int contextId);
272
+
273
+ private native HardwareBuffer nGetHardwareBuffer(int contextId, int generation, int slot);
274
+
275
+ private native void nReleaseSlot(int contextId, int generation, int slot);
276
+
277
+ private native void nSwitchToOffscreen(int contextId);
278
+ }
@@ -1,6 +1,7 @@
1
1
  package com.webgpu;
2
2
 
3
3
  import android.content.Context;
4
+ import android.os.Build;
4
5
  import android.view.Surface;
5
6
  import android.view.View;
6
7
 
@@ -10,12 +11,16 @@ import com.facebook.react.views.view.ReactViewGroup;
10
11
 
11
12
  public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
12
13
 
14
+ // Backing view kinds, see updateView().
15
+ private static final int KIND_SURFACE_VIEW = 0;
16
+ private static final int KIND_TEXTURE_VIEW = 1;
17
+ private static final int KIND_HARDWARE_BUFFER_VIEW = 2;
13
18
 
14
19
  private int mContextId;
15
20
  private boolean mOpaque = true;
16
21
  private String mSurfaceType = "auto";
17
22
  private boolean mZOrderOnTop = false;
18
- private boolean mAppliedTextureView;
23
+ private int mAppliedKind = -1;
19
24
  private boolean mAppliedZOrderOnTop;
20
25
  private WebGPUModule mModule;
21
26
  private View mView = null;
@@ -46,35 +51,62 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
46
51
  mZOrderOnTop = value;
47
52
  }
48
53
 
54
+ // Resolve the backing view from the props. "auto" picks SurfaceView for an
55
+ // opaque canvas and TextureView for a non-opaque one. WebGPUHardwareBufferView
56
+ // (a plain View drawing AHardwareBuffers inline, API 29+) is opt-in via
57
+ // surfaceType until it gets a runtime fallback to TextureView when the
58
+ // AHardwareBuffer import fails on a device; below API 29 the request itself
59
+ // falls back to TextureView.
60
+ private int resolveKind() {
61
+ if ("SurfaceView".equals(mSurfaceType)) {
62
+ return KIND_SURFACE_VIEW;
63
+ }
64
+ if ("TextureView".equals(mSurfaceType)) {
65
+ return KIND_TEXTURE_VIEW;
66
+ }
67
+ if ("HardwareBufferView".equals(mSurfaceType)) {
68
+ boolean hardwareBufferSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
69
+ return hardwareBufferSupported ? KIND_HARDWARE_BUFFER_VIEW : KIND_TEXTURE_VIEW;
70
+ }
71
+ return mOpaque ? KIND_SURFACE_VIEW : KIND_TEXTURE_VIEW;
72
+ }
73
+
49
74
  // Apply the complete prop transaction once, after contextId and all rendering
50
75
  // options have arrived. Only a change of backing view (or of zOrderOnTop,
51
76
  // which a SurfaceView must know before it attaches) replaces the child;
52
77
  // opacity is applied in place.
53
78
  public void updateView() {
54
- boolean textureView = "TextureView".equals(mSurfaceType)
55
- || (!"SurfaceView".equals(mSurfaceType) && !mOpaque);
56
- boolean zOrderOnTop = !textureView && mZOrderOnTop;
57
- if (mView == null || textureView != mAppliedTextureView
58
- || zOrderOnTop != mAppliedZOrderOnTop) {
79
+ int kind = resolveKind();
80
+ boolean zOrderOnTop = kind == KIND_SURFACE_VIEW && mZOrderOnTop;
81
+ if (mView == null || kind != mAppliedKind || zOrderOnTop != mAppliedZOrderOnTop) {
59
82
  if (mView != null) {
60
83
  removeView(mView);
61
84
  }
62
- mAppliedTextureView = textureView;
85
+ mAppliedKind = kind;
63
86
  mAppliedZOrderOnTop = zOrderOnTop;
64
87
  Context ctx = getContext();
65
- mView = textureView
66
- ? new WebGPUTextureView(ctx, this, mOpaque)
67
- : new WebGPUSurfaceView(ctx, this, zOrderOnTop, mOpaque);
88
+ switch (kind) {
89
+ case KIND_HARDWARE_BUFFER_VIEW:
90
+ mView = new WebGPUHardwareBufferView(ctx, this);
91
+ break;
92
+ case KIND_TEXTURE_VIEW:
93
+ mView = new WebGPUTextureView(ctx, this, mOpaque);
94
+ break;
95
+ default:
96
+ mView = new WebGPUSurfaceView(ctx, this, zOrderOnTop, mOpaque);
97
+ break;
98
+ }
68
99
  addView(mView);
69
100
  // ReactViewGroup.requestLayout() is a deliberate no-op, so addView outside
70
101
  // the layout pass never lays the child out; do it by hand or the new view
71
102
  // stays 0x0 and never gets a surface.
72
103
  layoutChild();
73
- } else if (textureView) {
104
+ } else if (kind == KIND_TEXTURE_VIEW) {
74
105
  ((WebGPUTextureView) mView).setOpaque(mOpaque);
75
- } else {
106
+ } else if (kind == KIND_SURFACE_VIEW) {
76
107
  ((WebGPUSurfaceView) mView).setOpaque(mOpaque);
77
108
  }
109
+ // WebGPUHardwareBufferView always composites with alpha; opacity is a no-op.
78
110
  }
79
111
 
80
112
  private void layoutChild() {
@@ -110,6 +142,11 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
110
142
  switchToOffscreenSurface(mContextId);
111
143
  }
112
144
 
145
+ @Override
146
+ public int getContextId() {
147
+ return mContextId;
148
+ }
149
+
113
150
  /**
114
151
  * Called from WebGPUViewManager.onDropViewInstance when React removes this
115
152
  * view: the view dies with its Canvas, so it retires the registry entry.
@@ -1,5 +1,6 @@
1
1
  #include "JSICache.h"
2
2
 
3
+ #include <algorithm>
3
4
  #include <iterator>
4
5
  #include <stdexcept>
5
6
  #include <string>
@@ -94,4 +95,26 @@ jsi::Object &JSICache::setPrototype(PrototypeKey key, jsi::Object prototype) {
94
95
  return it->second;
95
96
  }
96
97
 
98
+ jsi::Value JSICache::lockWrapper(jsi::Runtime &runtime,
99
+ const void *nativeObject) {
100
+ auto it = _wrappers.find(nativeObject);
101
+ if (it == _wrappers.end()) {
102
+ return jsi::Value::undefined();
103
+ }
104
+ return it->second.lock(runtime);
105
+ }
106
+
107
+ void JSICache::setWrapper(jsi::Runtime &runtime, const void *nativeObject,
108
+ const jsi::Object &wrapper) {
109
+ _wrappers.insert_or_assign(nativeObject, jsi::WeakObject(runtime, wrapper));
110
+ if (_wrappers.size() < _pruneWrappersAt) {
111
+ return;
112
+ }
113
+ for (auto it = _wrappers.begin(); it != _wrappers.end();) {
114
+ it = it->second.lock(runtime).isUndefined() ? _wrappers.erase(it)
115
+ : std::next(it);
116
+ }
117
+ _pruneWrappersAt = std::max(kMinPruneWrappersAt, _wrappers.size() * 2);
118
+ }
119
+
97
120
  } // namespace rnwgpu
@@ -13,7 +13,8 @@ namespace jsi = facebook::jsi;
13
13
 
14
14
  /**
15
15
  * Per-runtime store for JSI values that native code wants to keep across
16
- * calls (currently: the prototype object of every NativeObject class).
16
+ * calls: the prototype object of every NativeObject class, and the JS
17
+ * wrapper of every native object that has one on the runtime.
17
18
  *
18
19
  * Ownership model (borrowed from react-native-nitro-modules' JSICache):
19
20
  *
@@ -72,6 +73,26 @@ public:
72
73
  */
73
74
  jsi::Object &setPrototype(PrototypeKey key, jsi::Object prototype);
74
75
 
76
+ /**
77
+ * Returns the JS wrapper stored for `nativeObject` on this runtime, or
78
+ * undefined if none was stored or the wrapper has been collected.
79
+ *
80
+ * The key is the native object's address. A live wrapper keeps its native
81
+ * object alive (the object is the wrapper's NativeState), so as long as
82
+ * the entry locks it still belongs to that object. Once the wrapper has
83
+ * been collected the entry is stale and is skipped or replaced, so an
84
+ * address reused by a new object can never resolve to the old wrapper.
85
+ */
86
+ jsi::Value lockWrapper(jsi::Runtime &runtime, const void *nativeObject);
87
+
88
+ /**
89
+ * Stores `wrapper` as the JS wrapper of `nativeObject` on this runtime.
90
+ * Only a weak reference is kept: the wrapper stays collectable, and the
91
+ * native object is released when it goes away.
92
+ */
93
+ void setWrapper(jsi::Runtime &runtime, const void *nativeObject,
94
+ const jsi::Object &wrapper);
95
+
75
96
  JSICache() = default;
76
97
  ~JSICache() override = default;
77
98
  JSICache(const JSICache &) = delete;
@@ -81,6 +102,12 @@ private:
81
102
  static std::shared_ptr<JSICache> getOrCreateOnGlobal(jsi::Runtime &runtime);
82
103
 
83
104
  std::unordered_map<PrototypeKey, jsi::Object> _prototypes;
105
+ // Native object -> its wrapper on this runtime. Entries of collected
106
+ // wrappers are pruned in setWrapper once the map has doubled since the
107
+ // last pruning, so it stays proportional to the number of live wrappers.
108
+ std::unordered_map<const void *, jsi::WeakObject> _wrappers;
109
+ size_t _pruneWrappersAt = kMinPruneWrappersAt;
110
+ static constexpr size_t kMinPruneWrappersAt = 64;
84
111
 
85
112
  // Runtime -> cache. Weak on purpose: the runtime's global owns the cache.
86
113
  static std::mutex _registryMutex;