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