react-native-webgpu 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/android/cpp/cpp-adapter.cpp +35 -13
  2. package/android/src/main/java/com/webgpu/WebGPUAHBView.java +2 -2
  3. package/android/src/main/java/com/webgpu/WebGPUAPI.java +7 -2
  4. package/android/src/main/java/com/webgpu/WebGPUSurfaceView.java +3 -1
  5. package/android/src/main/java/com/webgpu/WebGPUTextureView.java +11 -7
  6. package/android/src/main/java/com/webgpu/WebGPUView.java +10 -7
  7. package/android/src/main/java/com/webgpu/WebGPUViewManager.java +6 -0
  8. package/apple/MetalView.mm +33 -8
  9. package/cpp/rnwgpu/RNWebGPUManager.cpp +14 -1
  10. package/cpp/rnwgpu/RNWebGPUManager.h +9 -0
  11. package/cpp/rnwgpu/SurfaceRegistry.h +469 -131
  12. package/cpp/rnwgpu/api/GPUAdapter.cpp +10 -3
  13. package/cpp/rnwgpu/api/GPUAdapter.h +5 -4
  14. package/cpp/rnwgpu/api/GPUBuffer.cpp +12 -2
  15. package/cpp/rnwgpu/api/GPUBuffer.h +3 -2
  16. package/cpp/rnwgpu/api/GPUCanvasContext.cpp +33 -19
  17. package/cpp/rnwgpu/api/GPUCanvasContext.h +3 -1
  18. package/cpp/rnwgpu/api/GPUDevice.cpp +32 -17
  19. package/cpp/rnwgpu/api/GPUDevice.h +9 -7
  20. package/cpp/rnwgpu/api/GPUQueue.cpp +6 -2
  21. package/cpp/rnwgpu/api/GPUQueue.h +3 -3
  22. package/cpp/rnwgpu/api/GPUShaderModule.cpp +7 -2
  23. package/cpp/rnwgpu/api/GPUShaderModule.h +3 -3
  24. package/cpp/rnwgpu/api/RNWebGPU.h +22 -1
  25. package/cpp/rnwgpu/async/RuntimeContext.h +9 -2
  26. package/lib/commonjs/Canvas.js +10 -0
  27. package/lib/commonjs/Canvas.js.map +1 -1
  28. package/lib/commonjs/WebPolyfillGPUModule.js +2 -0
  29. package/lib/commonjs/WebPolyfillGPUModule.js.map +1 -1
  30. package/lib/module/Canvas.js +11 -1
  31. package/lib/module/Canvas.js.map +1 -1
  32. package/lib/module/WebPolyfillGPUModule.js +2 -0
  33. package/lib/module/WebPolyfillGPUModule.js.map +1 -1
  34. package/lib/typescript/lib/commonjs/Canvas.d.ts.map +1 -1
  35. package/lib/typescript/lib/module/Canvas.d.ts.map +1 -1
  36. package/lib/typescript/src/Canvas.d.ts.map +1 -1
  37. package/lib/typescript/src/index.d.ts +1 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -1
  39. package/package.json +2 -6
  40. package/src/Canvas.tsx +11 -1
  41. package/src/WebPolyfillGPUModule.ts +2 -0
  42. package/src/index.tsx +3 -0
  43. package/android/src/main/java/com/webgpu/WebGPUBaseView.java +0 -61
@@ -1,9 +1,14 @@
1
1
  #pragma once
2
2
 
3
+ #include <algorithm>
4
+ #include <functional>
3
5
  #include <memory>
6
+ #include <mutex>
4
7
  #include <shared_mutex>
8
+ #include <stdexcept>
5
9
  #include <unordered_map>
6
10
  #include <utility>
11
+ #include <vector>
7
12
 
8
13
  #include "webgpu/webgpu_cpp.h"
9
14
 
@@ -33,190 +38,487 @@ struct Size {
33
38
  int height;
34
39
  };
35
40
 
41
+ // Invoked with the platform's native surface pointer once SurfaceInfo is done
42
+ // with it, so the platform can drop the reference it acquired on our behalf
43
+ // (ANativeWindow_release on Android, CFBridgingRelease of the retained
44
+ // CAMetalLayer on Apple platforms). May run on any thread.
45
+ using NativeSurfaceReleaser = std::function<void(void *)>;
46
+
47
+ // Bridges the asynchronous native surface lifecycle (surfaces appear and
48
+ // disappear on the platform UI thread) with the synchronous WebGPU canvas API
49
+ // (the JS render loop must always be able to acquire a texture).
50
+ //
51
+ // Ownership & threading model:
52
+ // - A registry entry is created on first use (by whichever of JS/native gets
53
+ // there first) and lives exactly as long as its JS Canvas: contextIds are
54
+ // never reused. It is removed by the native view's teardown (MetalView
55
+ // dealloc / WebGPUViewManager.onDropViewInstance) when a surface is
56
+ // attached, or by RNWebGPU.destroyContext (the Canvas unmount cleanup) when
57
+ // none ever was — see RNWebGPU::destroyContext for why the split. Surface
58
+ // destruction alone (backgrounding, TextureView teardown) never removes an
59
+ // entry; it only detaches the surface.
60
+ // - Attaching a surface is LATCHED: the UI thread stores it as pending
61
+ // (attachSurface) and it is adopted at the next frame boundary — start of
62
+ // getCurrentTexture or end of presentFrame — on whichever thread renders
63
+ // (main JS, Reanimated UI, or a worklet runtime). This preserves Dawn
64
+ // surface thread-affinity and guarantees a surface is never swapped in the
65
+ // middle of a frame. For contexts that are not actively rendering,
66
+ // RNWebGPUManager::flushPendingSurfaceTransition applies the attach from the
67
+ // JS thread instead.
68
+ // - Detaching (switchToOffscreen) is IMMEDIATE, because the platform destroys
69
+ // the surface as soon as its callback returns. A configured context falls
70
+ // back to rendering into an offscreen texture, so a running render loop
71
+ // keeps working; the in-flight frame, if any, is dropped at present(). When
72
+ // a new surface attaches, the latest offscreen frame is blitted onto it so
73
+ // content appears without waiting for the next render — the same mechanism
74
+ // that gives a fast time-to-first-frame when rendering starts before the
75
+ // native surface exists.
36
76
  class SurfaceInfo {
37
77
  public:
38
78
  SurfaceInfo(wgpu::Instance gpu, int width, int height)
39
- : gpu(std::move(gpu)), width(width), height(height) {}
79
+ : _gpu(std::move(gpu)), _width(width), _height(height) {}
80
+
81
+ ~SurfaceInfo() {
82
+ // Drop the Dawn objects before releasing the native surfaces they borrow.
83
+ _surface = nullptr;
84
+ _pendingSurface = nullptr;
85
+ _texture = nullptr;
86
+ if (_pendingReleaser && _pendingNativeSurface) {
87
+ _pendingReleaser(_pendingNativeSurface);
88
+ }
89
+ if (_releaser && _nativeSurface) {
90
+ _releaser(_nativeSurface);
91
+ }
92
+ }
40
93
 
41
- ~SurfaceInfo() { surface = nullptr; }
94
+ // --- Platform UI thread ---------------------------------------------------
42
95
 
43
- void reconfigure(int newWidth, int newHeight) {
44
- std::unique_lock<std::shared_mutex> lock(_mutex);
45
- config.width = newWidth;
46
- config.height = newHeight;
47
- _configure();
96
+ // Store a newly created on-screen surface. It becomes active at the next
97
+ // frame boundary (applyPendingAttach); callers should follow up with
98
+ // RNWebGPUManager::flushPendingSurfaceTransition so contexts that are not
99
+ // currently rendering also pick it up.
100
+ void attachSurface(void *nativeSurface, wgpu::Surface surface,
101
+ NativeSurfaceReleaser releaser) {
102
+ void *replacedSurface = nullptr;
103
+ NativeSurfaceReleaser replacedReleaser;
104
+ {
105
+ std::unique_lock<std::shared_mutex> lock(_mutex);
106
+ if (_hasPendingAttach) {
107
+ // Replaced before it was ever adopted.
108
+ replacedSurface = _pendingNativeSurface;
109
+ replacedReleaser = std::move(_pendingReleaser);
110
+ }
111
+ _hasPendingAttach = true;
112
+ _pendingNativeSurface = nativeSurface;
113
+ _pendingSurface = std::move(surface);
114
+ _pendingReleaser = std::move(releaser);
115
+ }
116
+ if (replacedReleaser && replacedSurface) {
117
+ replacedReleaser(replacedSurface);
118
+ }
48
119
  }
49
120
 
50
- void configure(wgpu::SurfaceConfiguration &newConfig) {
121
+ // The platform surface is being destroyed: detach immediately. If the
122
+ // context is configured, rendering continues into an offscreen texture whose
123
+ // content is blitted to the next attached surface; present() no-ops until
124
+ // then. Safe to call when already offscreen.
125
+ void switchToOffscreen() { detach(/* createFallbackTexture = */ true); }
126
+
127
+ // Detach without creating the offscreen fallback: used when the context is
128
+ // being destroyed and nothing will consume further frames.
129
+ void detachSurface() { detach(/* createFallbackTexture = */ false); }
130
+
131
+ // Reflects native view layout changes. Does not resize the drawing buffer:
132
+ // that tracks canvas.width/height (like on the web), see
133
+ // GPUCanvasContext::getCurrentTexture.
134
+ void resize(int newWidth, int newHeight) {
51
135
  std::unique_lock<std::shared_mutex> lock(_mutex);
52
- config = newConfig;
53
- config.width = width;
54
- config.height = height;
55
- config.presentMode = wgpu::PresentMode::Fifo;
56
- _configure();
136
+ _width = newWidth;
137
+ _height = newHeight;
57
138
  }
58
139
 
59
- void unconfigure() {
60
- std::unique_lock<std::shared_mutex> lock(_mutex);
61
- if (surface) {
62
- surface.Unconfigure();
63
- } else {
64
- texture = nullptr;
140
+ // --- Frame boundary (rendering thread, or the JS thread via
141
+ // RNWebGPUManager::flushPendingSurfaceTransition) ---------------------------
142
+
143
+ // Adopt a pending surface if no frame is in flight: configure it and, if
144
+ // frames were rendered offscreen, blit the most recent one onto it and
145
+ // present it, so content shows up without waiting for the render loop.
146
+ // Safe to call from any thread; no-ops when there is nothing pending.
147
+ //
148
+ // supersedeInFlightFrame is set by the rendering thread when it starts a new
149
+ // frame: a previous frame that never presented is abandoned and must not
150
+ // block adoption. The flush path (other threads) leaves it false so it never
151
+ // swaps the surface under a frame that is genuinely in flight.
152
+ void applyPendingAttach(bool supersedeInFlightFrame = false) {
153
+ bool presentBlit = false;
154
+ uint64_t blitEpoch = 0;
155
+ wgpu::Device device = nullptr;
156
+ void *replacedSurface = nullptr;
157
+ NativeSurfaceReleaser replacedReleaser;
158
+ {
159
+ std::unique_lock<std::shared_mutex> lock(_mutex);
160
+ if (supersedeInFlightFrame) {
161
+ _frameInFlight = false;
162
+ _acquiredFromSurface = false;
163
+ }
164
+ if (!_hasPendingAttach || _frameInFlight) {
165
+ return;
166
+ }
167
+ // Attach over attach without a detach in between: replace. Ownership
168
+ // tracks the native window pointer, not the Dawn surface handle (which
169
+ // can be null if surface creation failed).
170
+ replacedSurface = _nativeSurface;
171
+ replacedReleaser = std::move(_releaser);
172
+ _surface = std::move(_pendingSurface);
173
+ _nativeSurface = _pendingNativeSurface;
174
+ _releaser = std::move(_pendingReleaser);
175
+ _hasPendingAttach = false;
176
+ _pendingNativeSurface = nullptr;
177
+ _frameEpoch++;
178
+
179
+ // _surface can be null here when Dawn surface creation failed for a
180
+ // valid native window; the context then just keeps rendering offscreen.
181
+ if (_config.device != nullptr && _surface) {
182
+ bool blit = _texture != nullptr;
183
+ // The blit needs CopyDst on the surface. Configure with a widened
184
+ // copy while keeping _config at the usage the user asked for, so any
185
+ // later reconfigure drops the extra flag again.
186
+ wgpu::SurfaceConfiguration config = _config;
187
+ if (blit) {
188
+ config.usage |= wgpu::TextureUsage::CopyDst;
189
+ }
190
+ _surface.Configure(&config);
191
+ #ifdef __APPLE__
192
+ applyCAMetalLayerColorSpace(_nativeSurface, _config.format);
193
+ #endif
194
+ if (blit) {
195
+ presentBlit = blitOffscreenToSurfaceLocked();
196
+ device = _config.device;
197
+ // Consumed either way; on failure the next frame renders fresh.
198
+ _texture = nullptr;
199
+ blitEpoch = _frameEpoch;
200
+ }
201
+ }
65
202
  }
203
+ if (replacedReleaser && replacedSurface) {
204
+ replacedReleaser(replacedSurface);
205
+ }
206
+ if (presentBlit) {
207
+ #ifdef __APPLE__
208
+ if (device) {
209
+ dawn::native::metal::WaitForCommandsToBeScheduled(device.Get());
210
+ }
211
+ #endif
212
+ std::unique_lock<std::shared_mutex> lock(_mutex);
213
+ // Present only if the blitted texture is still the surface's current
214
+ // one. The epoch changes on any acquire, present, configure, detach, or
215
+ // adoption, so a frame that started - even one that already completed -
216
+ // or any other transition while we were unlocked skips this present
217
+ // (their newer content stands; presenting here would be a Dawn
218
+ // present-without-acquire error).
219
+ if (_surface && !_frameInFlight && _frameEpoch == blitEpoch) {
220
+ _surface.Present();
221
+ }
222
+ }
223
+ }
224
+
225
+ // --- Rendering thread
226
+ // -------------------------------------------------------
227
+
228
+ void configure(wgpu::SurfaceConfiguration &newConfig,
229
+ std::vector<wgpu::TextureFormat> viewFormats) {
230
+ applyPendingAttach(/* supersedeInFlightFrame = */ true);
231
+ std::unique_lock<std::shared_mutex> lock(_mutex);
232
+ _viewFormats = std::move(viewFormats);
233
+ _config = newConfig;
234
+ // The caller's viewFormats storage dies with the call; point the stored
235
+ // configuration at our own copy.
236
+ _config.viewFormats = _viewFormats.empty() ? nullptr : _viewFormats.data();
237
+ _config.viewFormatCount = _viewFormats.size();
238
+ // The drawing buffer starts at the canvas size. Clamp so a canvas that has
239
+ // not been laid out yet (0x0) configures instead of erroring.
240
+ _config.width = std::max(1, _width);
241
+ _config.height = std::max(1, _height);
242
+ _config.presentMode = wgpu::PresentMode::Fifo;
243
+ _texture = nullptr;
244
+ _frameEpoch++;
245
+ _configureLocked();
66
246
  }
67
247
 
68
- void *switchToOffscreen() {
248
+ // Resize the drawing buffer (canvas.width/height changed).
249
+ void reconfigure(int newWidth, int newHeight) {
69
250
  std::unique_lock<std::shared_mutex> lock(_mutex);
70
- // We only do this if the onscreen surface is configured.
71
- auto isConfigured = config.device != nullptr;
72
- if (isConfigured) {
73
- wgpu::TextureDescriptor textureDesc;
74
- textureDesc.usage = wgpu::TextureUsage::RenderAttachment |
75
- wgpu::TextureUsage::CopySrc |
76
- wgpu::TextureUsage::TextureBinding;
77
- textureDesc.format = config.format;
78
- textureDesc.size.width = config.width;
79
- textureDesc.size.height = config.height;
80
- texture = config.device.CreateTexture(&textureDesc);
251
+ if (_config.device == nullptr) {
252
+ return;
81
253
  }
82
- surface = nullptr;
83
- return nativeSurface;
254
+ _config.width = std::max(1, newWidth);
255
+ _config.height = std::max(1, newHeight);
256
+ _texture = nullptr;
257
+ _frameEpoch++;
258
+ _configureLocked();
84
259
  }
85
260
 
86
- void switchToOnscreen(void *newNativeSurface, wgpu::Surface newSurface) {
261
+ void unconfigure() {
87
262
  std::unique_lock<std::shared_mutex> lock(_mutex);
88
- nativeSurface = newNativeSurface;
89
- surface = std::move(newSurface);
90
- // If we are comming from an offscreen context, we need to configure the new
91
- // surface
92
- if (texture != nullptr) {
93
- config.usage = config.usage | wgpu::TextureUsage::CopyDst;
94
- _configure();
95
- // We flush the offscreen texture to the onscreen one
96
- // TODO: there is a faster way to do this without validation?
97
- wgpu::CommandEncoderDescriptor encoderDesc;
98
- auto device = config.device;
99
- wgpu::CommandEncoder encoder = device.CreateCommandEncoder(&encoderDesc);
100
-
101
- wgpu::TexelCopyTextureInfo sourceTexture = {};
102
- sourceTexture.texture = texture;
103
-
104
- wgpu::TexelCopyTextureInfo destinationTexture = {};
105
- wgpu::SurfaceTexture surfaceTexture;
106
- surface.GetCurrentTexture(&surfaceTexture);
107
- destinationTexture.texture = surfaceTexture.texture;
108
-
109
- wgpu::Extent3D size = {sourceTexture.texture.GetWidth(),
110
- sourceTexture.texture.GetHeight(),
111
- sourceTexture.texture.GetDepthOrArrayLayers()};
112
-
113
- encoder.CopyTextureToTexture(&sourceTexture, &destinationTexture, &size);
114
-
115
- wgpu::CommandBuffer commands = encoder.Finish();
116
- wgpu::Queue queue = device.GetQueue();
117
- queue.Submit(1, &commands);
118
- surface.Present();
119
- texture = nullptr;
263
+ if (_surface) {
264
+ _surface.Unconfigure();
120
265
  }
266
+ _texture = nullptr;
267
+ _config = {};
268
+ _viewFormats.clear();
269
+ _acquiredFromSurface = false;
270
+ _frameEpoch++;
121
271
  }
122
272
 
123
- void resize(int newWidth, int newHeight) {
273
+ bool isConfigured() {
274
+ std::shared_lock<std::shared_mutex> lock(_mutex);
275
+ return _config.device != nullptr;
276
+ }
277
+
278
+ // True while a native view owns a surface for this context (attached or
279
+ // pending adoption). Used to decide which side retires the registry entry:
280
+ // the native view's teardown when a surface exists, the JS Canvas cleanup
281
+ // otherwise (see RNWebGPU::destroyContext).
282
+ bool hasNativeSurface() {
283
+ std::shared_lock<std::shared_mutex> lock(_mutex);
284
+ return _nativeSurface != nullptr || _hasPendingAttach;
285
+ }
286
+
287
+ // Returns the texture for the current frame: the surface's swapchain texture
288
+ // when a surface is attached and healthy, an offscreen texture otherwise.
289
+ // Never returns null; throws when called before configure().
290
+ wgpu::Texture getCurrentTexture() {
291
+ // Start-of-frame boundary; a new acquire supersedes any previous frame
292
+ // that never presented.
293
+ applyPendingAttach(/* supersedeInFlightFrame = */ true);
124
294
  std::unique_lock<std::shared_mutex> lock(_mutex);
125
- width = newWidth;
126
- height = newHeight;
295
+ if (_config.device == nullptr) {
296
+ throw std::runtime_error(
297
+ "[WebGPU] getCurrentTexture() called on a canvas context that is "
298
+ "not configured; call context.configure() first");
299
+ }
300
+ _frameInFlight = true;
301
+ _acquiredFromSurface = false;
302
+ _frameEpoch++;
303
+ if (_surface) {
304
+ auto texture = acquireSurfaceTextureLocked();
305
+ if (texture) {
306
+ _acquiredFromSurface = true;
307
+ return texture;
308
+ }
309
+ // The surface is transiently unusable (e.g. mid-resize, lost while
310
+ // backgrounding): fall back to an offscreen texture so the render loop
311
+ // survives; this frame is simply not presented.
312
+ }
313
+ if (!_texture) {
314
+ _texture = createOffscreenTextureLocked();
315
+ }
316
+ return _texture;
127
317
  }
128
318
 
129
- // Present the current surface texture. Called synchronously from the thread
130
- // that did getCurrentTexture / submit (via GPUCanvasContext::present), so it
131
- // preserves Dawn surface thread-affinity. No-op when offscreen / unconfigured
132
- // (no surface).
319
+ // Present the current frame. Runs synchronously on the thread that did
320
+ // getCurrentTexture/submit (main JS, Reanimated UI, or a worklet runtime),
321
+ // preserving Dawn surface thread-affinity. Frames whose texture was not
322
+ // acquired from the attached surface (offscreen, detached mid-frame, or
323
+ // acquire failure) are dropped. This is also the end-of-frame boundary: it
324
+ // adopts a surface that attached while the frame was in flight.
133
325
  void presentFrame() {
134
326
  #ifdef __APPLE__
135
327
  // Ensure command buffers are scheduled before presenting. Read the device
136
328
  // under a shared lock, then wait without holding it (the wait can block).
137
- // The device may be reconfigured between the two locks; that is safe because
138
- // present() is called on the rendering thread right after submit(), the wait
139
- // just flushes that thread's already-submitted work, and the Present() below
140
- // re-checks `surface` under the unique lock before touching it.
141
329
  wgpu::Device device;
142
330
  {
143
331
  std::shared_lock<std::shared_mutex> lock(_mutex);
144
- device = config.device;
332
+ device = _config.device;
145
333
  }
146
334
  if (device) {
147
335
  dawn::native::metal::WaitForCommandsToBeScheduled(device.Get());
148
336
  }
149
337
  #endif
150
- std::unique_lock<std::shared_mutex> lock(_mutex);
151
- if (surface) {
152
- surface.Present();
338
+ {
339
+ std::unique_lock<std::shared_mutex> lock(_mutex);
340
+ if (_surface && _acquiredFromSurface) {
341
+ _surface.Present();
342
+ }
343
+ _acquiredFromSurface = false;
344
+ _frameInFlight = false;
345
+ _frameEpoch++;
153
346
  }
347
+ applyPendingAttach();
154
348
  }
155
349
 
156
- wgpu::Texture getCurrentTexture() {
350
+ NativeInfo getNativeInfo() {
157
351
  std::shared_lock<std::shared_mutex> lock(_mutex);
158
- if (surface) {
159
- wgpu::SurfaceTexture surfaceTexture;
160
- surface.GetCurrentTexture(&surfaceTexture);
161
- return surfaceTexture.texture;
162
- } else {
163
- return texture;
164
- }
352
+ // A surface that is still pending adoption is the one callers should see.
353
+ void *native = _hasPendingAttach ? _pendingNativeSurface : _nativeSurface;
354
+ return {.nativeSurface = native, .width = _width, .height = _height};
165
355
  }
166
356
 
167
- // True when an on-screen wgpu::Surface is attached (vs offscreen texture).
168
- bool hasSurface() {
357
+ Size getSize() {
169
358
  std::shared_lock<std::shared_mutex> lock(_mutex);
170
- return surface != nullptr;
359
+ return {.width = _width, .height = _height};
171
360
  }
172
361
 
173
- NativeInfo getNativeInfo() {
362
+ wgpu::SurfaceConfiguration getConfig() {
174
363
  std::shared_lock<std::shared_mutex> lock(_mutex);
175
- return {.nativeSurface = nativeSurface, .width = width, .height = height};
364
+ return _config;
176
365
  }
177
366
 
178
- Size getSize() {
179
- std::shared_lock<std::shared_mutex> lock(_mutex);
180
- return {.width = width, .height = height};
367
+ private:
368
+ void detach(bool createFallbackTexture) {
369
+ void *releasedSurfaces[2] = {nullptr, nullptr};
370
+ NativeSurfaceReleaser releasers[2];
371
+ {
372
+ std::unique_lock<std::shared_mutex> lock(_mutex);
373
+ // The platform is tearing surfaces down; a not-yet-adopted attach is
374
+ // stale, cancel it.
375
+ if (_hasPendingAttach) {
376
+ _hasPendingAttach = false;
377
+ _pendingSurface = nullptr;
378
+ releasedSurfaces[0] = _pendingNativeSurface;
379
+ releasers[0] = std::move(_pendingReleaser);
380
+ _pendingNativeSurface = nullptr;
381
+ }
382
+ if (_surface) {
383
+ if (createFallbackTexture && _config.device != nullptr) {
384
+ _texture = createOffscreenTextureLocked();
385
+ }
386
+ _surface = nullptr;
387
+ // The in-flight frame (if any) rendered into the destroyed surface;
388
+ // presentFrame() must not present it.
389
+ _acquiredFromSurface = false;
390
+ }
391
+ _frameEpoch++;
392
+ // Window ownership is independent of the Dawn surface handle (which can
393
+ // be null if surface creation failed): always return the window.
394
+ releasedSurfaces[1] = _nativeSurface;
395
+ releasers[1] = std::move(_releaser);
396
+ _nativeSurface = nullptr;
397
+ }
398
+ // Release outside the lock: the platform may do real work here.
399
+ for (int i = 0; i < 2; i++) {
400
+ if (releasers[i] && releasedSurfaces[i]) {
401
+ releasers[i](releasedSurfaces[i]);
402
+ }
403
+ }
181
404
  }
182
405
 
183
- wgpu::SurfaceConfiguration getConfig() {
184
- std::shared_lock<std::shared_mutex> lock(_mutex);
185
- return config;
406
+ // All *Locked helpers below require _mutex to be held exclusively.
407
+
408
+ wgpu::Texture createOffscreenTextureLocked() {
409
+ wgpu::TextureDescriptor descriptor;
410
+ // Union with the user's usage so offscreen frames stay compatible with
411
+ // whatever they configured (e.g. CopySrc readbacks). RenderAttachment |
412
+ // CopySrc | TextureBinding is what the fallback itself needs (rendering,
413
+ // the attach blit, sampling).
414
+ descriptor.usage = _config.usage | wgpu::TextureUsage::RenderAttachment |
415
+ wgpu::TextureUsage::CopySrc |
416
+ wgpu::TextureUsage::TextureBinding;
417
+ descriptor.format = _config.format;
418
+ descriptor.size.width = std::max(1u, _config.width);
419
+ descriptor.size.height = std::max(1u, _config.height);
420
+ descriptor.viewFormats = _config.viewFormats;
421
+ descriptor.viewFormatCount = _config.viewFormatCount;
422
+ return _config.device.CreateTexture(&descriptor);
186
423
  }
187
424
 
188
- wgpu::Device getDevice() {
189
- std::shared_lock<std::shared_mutex> lock(_mutex);
190
- return config.device;
425
+ // Acquire the surface's current texture, reconfiguring once when the surface
426
+ // reports it is stale (rotation, resize, coming back from background).
427
+ wgpu::Texture acquireSurfaceTextureLocked() {
428
+ wgpu::SurfaceTexture surfaceTexture;
429
+ _surface.GetCurrentTexture(&surfaceTexture);
430
+ if (!isAcquireSuccess(surfaceTexture)) {
431
+ if (surfaceTexture.status ==
432
+ wgpu::SurfaceGetCurrentTextureStatus::Error) {
433
+ return nullptr;
434
+ }
435
+ _surface.Configure(&_config);
436
+ _surface.GetCurrentTexture(&surfaceTexture);
437
+ if (!isAcquireSuccess(surfaceTexture)) {
438
+ return nullptr;
439
+ }
440
+ }
441
+ return surfaceTexture.texture;
191
442
  }
192
443
 
193
- private:
194
- void _configure() {
195
- if (surface) {
196
- surface.Configure(&config);
444
+ static bool isAcquireSuccess(const wgpu::SurfaceTexture &surfaceTexture) {
445
+ return (surfaceTexture.status ==
446
+ wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal ||
447
+ surfaceTexture.status ==
448
+ wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal) &&
449
+ surfaceTexture.texture != nullptr;
450
+ }
451
+
452
+ // Copy the last offscreen frame onto the freshly attached surface. Returns
453
+ // true when the copy was submitted and the surface should be presented.
454
+ bool blitOffscreenToSurfaceLocked() {
455
+ wgpu::SurfaceTexture surfaceTexture;
456
+ _surface.GetCurrentTexture(&surfaceTexture);
457
+ if (!isAcquireSuccess(surfaceTexture)) {
458
+ return false;
459
+ }
460
+
461
+ wgpu::TexelCopyTextureInfo source = {};
462
+ source.texture = _texture;
463
+ wgpu::TexelCopyTextureInfo destination = {};
464
+ destination.texture = surfaceTexture.texture;
465
+
466
+ // The offscreen frame and the new surface can disagree on size (e.g. the
467
+ // device rotated while detached); copy the shared region.
468
+ wgpu::Extent3D size = {
469
+ std::min(_texture.GetWidth(), surfaceTexture.texture.GetWidth()),
470
+ std::min(_texture.GetHeight(), surfaceTexture.texture.GetHeight()), 1};
471
+
472
+ wgpu::CommandEncoderDescriptor encoderDescriptor;
473
+ wgpu::CommandEncoder encoder =
474
+ _config.device.CreateCommandEncoder(&encoderDescriptor);
475
+ encoder.CopyTextureToTexture(&source, &destination, &size);
476
+ wgpu::CommandBuffer commands = encoder.Finish();
477
+ _config.device.GetQueue().Submit(1, &commands);
478
+ return true;
479
+ }
480
+
481
+ void _configureLocked() {
482
+ if (_surface) {
483
+ _surface.Configure(&_config);
197
484
  #ifdef __APPLE__
198
- applyCAMetalLayerColorSpace(nativeSurface, config.format);
485
+ applyCAMetalLayerColorSpace(_nativeSurface, _config.format);
199
486
  #endif
200
487
  } else {
201
- wgpu::TextureDescriptor textureDesc;
202
- textureDesc.format = config.format;
203
- textureDesc.size.width = config.width;
204
- textureDesc.size.height = config.height;
205
- textureDesc.usage = wgpu::TextureUsage::RenderAttachment |
206
- wgpu::TextureUsage::CopySrc |
207
- wgpu::TextureUsage::TextureBinding;
208
- texture = config.device.CreateTexture(&textureDesc);
488
+ _texture = createOffscreenTextureLocked();
209
489
  }
210
490
  }
211
491
 
212
492
  mutable std::shared_mutex _mutex;
213
- void *nativeSurface = nullptr;
214
- wgpu::Surface surface = nullptr;
215
- wgpu::Texture texture = nullptr;
216
- wgpu::Instance gpu;
217
- wgpu::SurfaceConfiguration config;
218
- int width;
219
- int height;
493
+ // Attached on-screen surface (null while offscreen).
494
+ void *_nativeSurface = nullptr;
495
+ wgpu::Surface _surface = nullptr;
496
+ NativeSurfaceReleaser _releaser;
497
+ // Offscreen fallback drawing buffer.
498
+ wgpu::Texture _texture = nullptr;
499
+ // Surface attached by the UI thread, awaiting adoption at a frame boundary.
500
+ bool _hasPendingAttach = false;
501
+ void *_pendingNativeSurface = nullptr;
502
+ wgpu::Surface _pendingSurface = nullptr;
503
+ NativeSurfaceReleaser _pendingReleaser;
504
+ // Frame state: set by getCurrentTexture, cleared by presentFrame.
505
+ bool _frameInFlight = false;
506
+ bool _acquiredFromSurface = false;
507
+ // Bumped on every acquire, present, configure/reconfigure/unconfigure,
508
+ // adoption, and detach. The deferred blit-present in applyPendingAttach
509
+ // revalidates against it so it never presents a texture that stopped being
510
+ // the surface's current one while the lock was released.
511
+ uint64_t _frameEpoch = 0;
512
+ // device == nullptr means "not configured". _viewFormats owns the storage
513
+ // that _config.viewFormats points at.
514
+ wgpu::SurfaceConfiguration _config;
515
+ std::vector<wgpu::TextureFormat> _viewFormats;
516
+ // Keeps the Dawn instance alive for as long as any canvas exists.
517
+ wgpu::Instance _gpu;
518
+ // Native view size in dp (surfaced as clientWidth/clientHeight on the JS
519
+ // canvas).
520
+ int _width;
521
+ int _height;
220
522
  };
221
523
 
222
524
  class SurfaceRegistry {
@@ -243,18 +545,56 @@ public:
243
545
  _registry.erase(id);
244
546
  }
245
547
 
246
- std::shared_ptr<SurfaceInfo> addSurfaceInfo(int id, wgpu::Instance gpu,
247
- int width, int height) {
548
+ std::shared_ptr<SurfaceInfo>
549
+ getSurfaceInfoOrCreate(int id, wgpu::Instance gpu, int width, int height) {
248
550
  std::unique_lock<std::shared_mutex> lock(_mutex);
249
- auto info = std::make_shared<SurfaceInfo>(gpu, width, height);
250
- _registry[id] = info;
551
+ return getSurfaceInfoOrCreateLocked(id, gpu, width, height);
552
+ }
553
+
554
+ // Find-or-create + attach as one atomic step under the registry lock, so it
555
+ // serializes with removeSurfaceInfoIfDetached: an attach can never land on
556
+ // an entry that a concurrent destroyContext is erasing (it either marks the
557
+ // entry attached before the check, or re-creates the entry after the
558
+ // erase). Lock order is registry -> SurfaceInfo, matching every other path.
559
+ std::shared_ptr<SurfaceInfo> attachSurface(int id, wgpu::Instance gpu,
560
+ int width, int height,
561
+ void *nativeSurface,
562
+ wgpu::Surface surface,
563
+ NativeSurfaceReleaser releaser) {
564
+ std::unique_lock<std::shared_mutex> lock(_mutex);
565
+ auto info = getSurfaceInfoOrCreateLocked(id, gpu, width, height);
566
+ info->attachSurface(nativeSurface, std::move(surface), std::move(releaser));
251
567
  return info;
252
568
  }
253
569
 
254
- std::shared_ptr<SurfaceInfo>
255
- getSurfaceInfoOrCreate(int id, wgpu::Instance gpu, int width, int height) {
570
+ // Erase the entry only if no native surface is attached or pending; the
571
+ // atomic counterpart of attachSurface above (see RNWebGPU::destroyContext
572
+ // for the ownership split this implements).
573
+ void removeSurfaceInfoIfDetached(int id) {
256
574
  std::unique_lock<std::shared_mutex> lock(_mutex);
257
575
  auto it = _registry.find(id);
576
+ if (it == _registry.end() || it->second->hasNativeSurface()) {
577
+ return;
578
+ }
579
+ _registry.erase(it);
580
+ }
581
+
582
+ // Drops all entries. Called when the RN instance tears down (dev reload):
583
+ // JS context ids restart from scratch, so surviving entries would alias new
584
+ // canvases onto dead surfaces.
585
+ void clear() {
586
+ std::unique_lock<std::shared_mutex> lock(_mutex);
587
+ _registry.clear();
588
+ }
589
+
590
+ private:
591
+ SurfaceRegistry() = default;
592
+
593
+ std::shared_ptr<SurfaceInfo> getSurfaceInfoOrCreateLocked(int id,
594
+ wgpu::Instance gpu,
595
+ int width,
596
+ int height) {
597
+ auto it = _registry.find(id);
258
598
  if (it != _registry.end()) {
259
599
  return it->second;
260
600
  }
@@ -263,8 +603,6 @@ public:
263
603
  return info;
264
604
  }
265
605
 
266
- private:
267
- SurfaceRegistry() = default;
268
606
  mutable std::shared_mutex _mutex;
269
607
  std::unordered_map<int, std::shared_ptr<SurfaceInfo>> _registry;
270
608
  };