react-native-effects 0.2.0 → 0.3.0

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.
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
- import { PixelRatio, StyleSheet } from 'react-native';
4
- import { Canvas } from 'react-native-webgpu';
5
- import { useEffect, useRef } from 'react';
3
+ import { AppState, PixelRatio, StyleSheet } from 'react-native';
4
+ import { Canvas, installWebGPU } from 'react-native-webgpu';
5
+ import { useEffect, useRef, useState } from 'react';
6
6
  import { createSynchronizable, scheduleOnRuntime } from 'react-native-worklets';
7
7
  import { colorToVec4 } from "../../utils/colors.js";
8
8
  import { useWGPUSetup } from "../../hooks/useWGPUSetup.js";
@@ -29,8 +29,22 @@ export default function ShaderView({
29
29
  const {
30
30
  canvasRef,
31
31
  runtime,
32
- resources
32
+ resources,
33
+ onCanvasLayout
33
34
  } = useWGPUSetup();
35
+
36
+ // Pause the render loop while the app is backgrounded. The rAF loop otherwise
37
+ // keeps churning frames against a surface that's offscreen (and often
38
+ // transiently invalid), wasting battery/GPU. We only pause on a true
39
+ // 'background' transition — iOS 'inactive' (app switcher peek, notification
40
+ // pulldown) is left running to avoid flicker on brief, foreground interruptions.
41
+ const [appActive, setAppActive] = useState(() => (AppState.currentState ?? 'active') !== 'background');
42
+ useEffect(() => {
43
+ const subscription = AppState.addEventListener('change', state => {
44
+ setAppActive(state !== 'background');
45
+ });
46
+ return () => subscription.remove();
47
+ }, []);
34
48
  const propsSync = useRef(createSynchronizable(new Float64Array(SYNC_SIZE))).current;
35
49
 
36
50
  // Convert props to flat floats and push to synchronizable
@@ -78,9 +92,12 @@ export default function ShaderView({
78
92
  };
79
93
  }, [propsSync]);
80
94
 
81
- // Start render loop when GPU resources are ready
95
+ // Start render loop when GPU resources are ready and the app is foregrounded.
96
+ // When the app backgrounds, `appActive` flips false and this effect's cleanup
97
+ // tears the loop down (via the `cancelled` token below); on return to the
98
+ // foreground it re-runs and starts a fresh loop.
82
99
  useEffect(() => {
83
- if (!resources) {
100
+ if (!resources || !appActive) {
84
101
  return;
85
102
  }
86
103
  const {
@@ -98,6 +115,12 @@ export default function ShaderView({
98
115
  scheduleOnRuntime(runtime, () => {
99
116
  'worklet';
100
117
 
118
+ // Worklet runtimes start without the WebGPU flag constants
119
+ // (GPUBufferUsage, GPUTextureUsage, ...). installWebGPU() captures them
120
+ // into this runtime so they're available below. Idempotent / safe no-op
121
+ // if already installed.
122
+ installWebGPU();
123
+
101
124
  // Create pipeline once
102
125
  const pipeline = device.createRenderPipeline({
103
126
  layout: 'auto',
@@ -138,15 +161,37 @@ export default function ShaderView({
138
161
  const uniformData = new Float32Array(UNIFORM_FLOAT_COUNT);
139
162
  let accumulatedTime = 0;
140
163
  let lastTimestamp = 0;
164
+ let warned = false;
165
+ let bufferDestroyed = false;
166
+
167
+ // Free this loop's uniform buffer when the loop ends (alive=0 / superseded
168
+ // by Fast Refresh / unmount). On a fragmentShader change the effect
169
+ // re-runs and schedules a fresh loop with a new buffer while the device
170
+ // persists, so without this the old buffer leaks every shader swap.
171
+ function destroyBuffer() {
172
+ if (bufferDestroyed) {
173
+ return;
174
+ }
175
+ bufferDestroyed = true;
176
+ try {
177
+ uniformBuffer.destroy();
178
+ } catch {
179
+ // The device may already have been destroyed on unmount — the buffer
180
+ // is gone either way, so there's nothing to recover.
181
+ return;
182
+ }
183
+ }
141
184
  function render(timestamp) {
142
185
  const props = propsSync.getDirty();
143
186
  if (props[IDX_ALIVE] === 0) {
187
+ destroyBuffer();
144
188
  return;
145
189
  }
146
190
 
147
191
  // This loop was superseded (Fast Refresh / unmount) — bail without
148
192
  // scheduling another frame so it can be garbage-collected.
149
193
  if (cancelled.getDirty()[0] === 1) {
194
+ destroyBuffer();
150
195
  return;
151
196
  }
152
197
 
@@ -211,23 +256,40 @@ export default function ShaderView({
211
256
  uniformData[26] = live[2];
212
257
  uniformData[27] = live[3];
213
258
  }
214
- device.queue.writeBuffer(uniformBuffer, 0, uniformData);
215
- const commandEncoder = device.createCommandEncoder();
216
- const textureView = context.getCurrentTexture().createView();
217
- const passEncoder = commandEncoder.beginRenderPass({
218
- colorAttachments: [{
219
- view: textureView,
220
- clearValue: transparent ? [0, 0, 0, 0] : [0, 0, 0, 1],
221
- loadOp: 'clear',
222
- storeOp: 'store'
223
- }]
224
- });
225
- passEncoder.setPipeline(pipeline);
226
- passEncoder.setBindGroup(0, bindGroup);
227
- passEncoder.draw(3);
228
- passEncoder.end();
229
- device.queue.submit([commandEncoder.finish()]);
230
- context.present();
259
+
260
+ // GPU work can throw when the surface is transiently invalid — the app
261
+ // backgrounded, the view detached, or the device was lost. Swallow the
262
+ // failed frame so the loop survives and recovers when the surface comes
263
+ // back (rather than the worklet crashing or the loop dying silently).
264
+ try {
265
+ device.queue.writeBuffer(uniformBuffer, 0, uniformData);
266
+ const commandEncoder = device.createCommandEncoder();
267
+ const textureView = context.getCurrentTexture().createView();
268
+ const passEncoder = commandEncoder.beginRenderPass({
269
+ colorAttachments: [{
270
+ view: textureView,
271
+ clearValue: transparent ? [0, 0, 0, 0] : [0, 0, 0, 1],
272
+ loadOp: 'clear',
273
+ storeOp: 'store'
274
+ }]
275
+ });
276
+ passEncoder.setPipeline(pipeline);
277
+ passEncoder.setBindGroup(0, bindGroup);
278
+ passEncoder.draw(3);
279
+ passEncoder.end();
280
+ device.queue.submit([commandEncoder.finish()]);
281
+ context.present();
282
+ } catch (e) {
283
+ // Warn once to avoid spamming the console every frame on a persistent
284
+ // failure (e.g. a lost device).
285
+ if (!warned) {
286
+ warned = true;
287
+ console.warn('[react-native-effects] render frame failed:', e);
288
+ }
289
+ }
290
+
291
+ // Always reschedule animated loops, even after a caught failure, so the
292
+ // effect self-heals once the surface is valid again.
231
293
  if (!isStatic) {
232
294
  requestAnimationFrame(render);
233
295
  }
@@ -237,11 +299,16 @@ export default function ShaderView({
237
299
  return () => {
238
300
  cancelled.setBlocking(() => Float64Array.of(1));
239
301
  };
240
- }, [resources, runtime, propsSync, paramsSynchronizable, fragmentShader, isStatic, transparent]);
302
+ }, [resources, appActive, runtime, propsSync, paramsSynchronizable, fragmentShader, isStatic, transparent]);
241
303
  return /*#__PURE__*/_jsx(Canvas, {
242
304
  ref: canvasRef,
305
+ transparent: transparent,
243
306
  style: [styles.canvas, style],
244
- ...viewProps
307
+ ...viewProps,
308
+ onLayout: event => {
309
+ onCanvasLayout(event);
310
+ viewProps.onLayout?.(event);
311
+ }
245
312
  });
246
313
  }
247
314
  const styles = StyleSheet.create({
@@ -1 +1 @@
1
- {"version":3,"names":["PixelRatio","StyleSheet","Canvas","useEffect","useRef","createSynchronizable","scheduleOnRuntime","colorToVec4","useWGPUSetup","TRIANGLE_VERTEX_SHADER","UNIFORM_BUFFER_SIZE","UNIFORM_FLOAT_COUNT","jsx","_jsx","SYNC_SIZE","IDX_SPEED","IDX_PARAMS","IDX_ALIVE","ShaderView","fragmentShader","colors","speed","params","isStatic","transparent","paramsSynchronizable","style","viewProps","canvasRef","runtime","resources","propsSync","Float64Array","current","data","undefined","c0","r","g","b","a","c1","i","setBlocking","prev","device","context","presentationFormat","dpr","get","cancelled","pipeline","createRenderPipeline","layout","vertex","module","createShaderModule","code","entryPoint","fragment","targets","format","primitive","topology","uniformBuffer","createBuffer","size","usage","GPUBufferUsage","UNIFORM","COPY_DST","bindGroup","createBindGroup","getBindGroupLayout","entries","binding","resource","buffer","uniformData","Float32Array","accumulatedTime","lastTimestamp","render","timestamp","props","getDirty","dt","currentSpeed","canvas","width","height","aspect","live","queue","writeBuffer","commandEncoder","createCommandEncoder","textureView","getCurrentTexture","createView","passEncoder","beginRenderPass","colorAttachments","view","clearValue","loadOp","storeOp","setPipeline","setBindGroup","draw","end","submit","finish","present","requestAnimationFrame","of","ref","styles","create","flex"],"sourceRoot":"../../../../src","sources":["components/ShaderView/index.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["AppState","PixelRatio","StyleSheet","Canvas","installWebGPU","useEffect","useRef","useState","createSynchronizable","scheduleOnRuntime","colorToVec4","useWGPUSetup","TRIANGLE_VERTEX_SHADER","UNIFORM_BUFFER_SIZE","UNIFORM_FLOAT_COUNT","jsx","_jsx","SYNC_SIZE","IDX_SPEED","IDX_PARAMS","IDX_ALIVE","ShaderView","fragmentShader","colors","speed","params","isStatic","transparent","paramsSynchronizable","style","viewProps","canvasRef","runtime","resources","onCanvasLayout","appActive","setAppActive","currentState","subscription","addEventListener","state","remove","propsSync","Float64Array","current","data","undefined","c0","r","g","b","a","c1","i","setBlocking","prev","device","context","presentationFormat","dpr","get","cancelled","pipeline","createRenderPipeline","layout","vertex","module","createShaderModule","code","entryPoint","fragment","targets","format","primitive","topology","uniformBuffer","createBuffer","size","usage","GPUBufferUsage","UNIFORM","COPY_DST","bindGroup","createBindGroup","getBindGroupLayout","entries","binding","resource","buffer","uniformData","Float32Array","accumulatedTime","lastTimestamp","warned","bufferDestroyed","destroyBuffer","destroy","render","timestamp","props","getDirty","dt","currentSpeed","canvas","width","height","aspect","live","queue","writeBuffer","commandEncoder","createCommandEncoder","textureView","getCurrentTexture","createView","passEncoder","beginRenderPass","colorAttachments","view","clearValue","loadOp","storeOp","setPipeline","setBindGroup","draw","end","submit","finish","present","e","console","warn","requestAnimationFrame","of","ref","styles","onLayout","event","create","flex"],"sourceRoot":"../../../../src","sources":["components/ShaderView/index.tsx"],"mappings":"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","ignoreList":[]}
@@ -2,36 +2,44 @@
2
2
 
3
3
  import { PixelRatio } from 'react-native';
4
4
  import { useCanvasRef } from 'react-native-webgpu';
5
- import { useEffect, useState } from 'react';
6
- import { initWebGPU } from "../utils/initWebGPU.js";
5
+ import { useCallback, useEffect, useRef, useState } from 'react';
7
6
  import { BackgroundRuntime } from "../utils/backgroundRuntime.js";
7
+ import { getSharedGPUDevice } from "../utils/gpuDevice.js";
8
8
  export function useWGPUSetup() {
9
9
  const canvasRef = useCanvasRef();
10
10
  const [resources, setResources] = useState(null);
11
11
  const runtime = BackgroundRuntime;
12
+
13
+ // Physical-pixel size the surface is currently configured for. Used to drive
14
+ // resize (and to skip redundant reconfigures when the layout pass reports the
15
+ // same size).
16
+ const configuredSizeRef = useRef(null);
12
17
  useEffect(() => {
13
18
  let cancelled = false;
14
19
  (async () => {
15
- const adapter = await navigator.gpu.requestAdapter();
16
- if (!adapter || cancelled) {
17
- return;
18
- }
19
- const device = await adapter.requestDevice();
20
- if (cancelled) {
20
+ // Shared across every ShaderView — see getSharedGPUDevice. Returns null if
21
+ // no adapter is available.
22
+ const device = await getSharedGPUDevice();
23
+ if (!device || cancelled) {
21
24
  return;
22
25
  }
23
26
  const context = canvasRef.current.getContext('webgpu');
24
27
  const canvas = context.canvas;
25
28
  const dpr = PixelRatio.get();
26
- canvas.width = canvas.width * dpr;
27
- canvas.height = canvas.height * dpr;
29
+ const width = Math.max(1, Math.round(canvas.width * dpr));
30
+ const height = Math.max(1, Math.round(canvas.height * dpr));
31
+ canvas.width = width;
32
+ canvas.height = height;
28
33
  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
29
34
  context.configure({
30
35
  device,
31
36
  format: presentationFormat,
32
37
  alphaMode: 'premultiplied'
33
38
  });
34
- initWebGPU(runtime);
39
+ configuredSizeRef.current = {
40
+ width,
41
+ height
42
+ };
35
43
  if (!cancelled) {
36
44
  setResources({
37
45
  device,
@@ -42,13 +50,54 @@ export function useWGPUSetup() {
42
50
  })();
43
51
  return () => {
44
52
  cancelled = true;
53
+ // The device is shared across all ShaderViews and lives for the JS
54
+ // runtime's lifetime, so it must NOT be destroyed here. This view's own
55
+ // GPU resources (the uniform buffer) are torn down by the render loop when
56
+ // it stops; the pipeline/shader modules are released once the loop's
57
+ // worklet closure is garbage-collected.
45
58
  };
46
59
  // eslint-disable-next-line react-hooks/exhaustive-deps
47
60
  }, []);
61
+
62
+ // Resize the drawing buffer and reconfigure the surface whenever the view's
63
+ // layout changes (most notably device rotation). Without this the buffer keeps
64
+ // its original dimensions and the shader renders stretched / wrong-aspect. The
65
+ // render loop reads `canvas.width/height` each frame, so the resolution uniform
66
+ // picks up the new size on the next frame. No-op until GPU resources are ready
67
+ // (initial sizing is handled by the setup effect above).
68
+ const onCanvasLayout = useCallback(event => {
69
+ if (!resources) {
70
+ return;
71
+ }
72
+ const {
73
+ width,
74
+ height
75
+ } = event.nativeEvent.layout;
76
+ const dpr = PixelRatio.get();
77
+ const w = Math.max(1, Math.round(width * dpr));
78
+ const h = Math.max(1, Math.round(height * dpr));
79
+ const prev = configuredSizeRef.current;
80
+ if (prev && prev.width === w && prev.height === h) {
81
+ return;
82
+ }
83
+ const canvas = resources.context.canvas;
84
+ canvas.width = w;
85
+ canvas.height = h;
86
+ resources.context.configure({
87
+ device: resources.device,
88
+ format: resources.presentationFormat,
89
+ alphaMode: 'premultiplied'
90
+ });
91
+ configuredSizeRef.current = {
92
+ width: w,
93
+ height: h
94
+ };
95
+ }, [resources]);
48
96
  return {
49
97
  canvasRef,
50
98
  runtime,
51
- resources
99
+ resources,
100
+ onCanvasLayout
52
101
  };
53
102
  }
54
103
  //# sourceMappingURL=useWGPUSetup.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["PixelRatio","useCanvasRef","useEffect","useState","initWebGPU","BackgroundRuntime","useWGPUSetup","canvasRef","resources","setResources","runtime","cancelled","adapter","navigator","gpu","requestAdapter","device","requestDevice","context","current","getContext","canvas","dpr","get","width","height","presentationFormat","getPreferredCanvasFormat","configure","format","alphaMode"],"sourceRoot":"../../../src","sources":["hooks/useWGPUSetup.tsx"],"mappings":";;AAAA,SAASA,UAAU,QAAQ,cAAc;AACzC,SACEC,YAAY,QAGP,qBAAqB;AAC5B,SAASC,SAAS,EAAEC,QAAQ,QAAQ,OAAO;AAE3C,SAASC,UAAU,QAAQ,wBAAqB;AAChD,SAASC,iBAAiB,QAAQ,+BAA4B;AAc9D,OAAO,SAASC,YAAYA,CAAA,EAAoB;EAC9C,MAAMC,SAAS,GAAGN,YAAY,CAAC,CAAC;EAChC,MAAM,CAACO,SAAS,EAAEC,YAAY,CAAC,GAAGN,QAAQ,CAAsB,IAAI,CAAC;EACrE,MAAMO,OAAO,GAAGL,iBAAiB;EAEjCH,SAAS,CAAC,MAAM;IACd,IAAIS,SAAS,GAAG,KAAK;IAErB,CAAC,YAAY;MACX,MAAMC,OAAO,GAAG,MAAMC,SAAS,CAACC,GAAG,CAACC,cAAc,CAAC,CAAC;MACpD,IAAI,CAACH,OAAO,IAAID,SAAS,EAAE;QACzB;MACF;MAEA,MAAMK,MAAM,GAAG,MAAMJ,OAAO,CAACK,aAAa,CAAC,CAAC;MAC5C,IAAIN,SAAS,EAAE;QACb;MACF;MAEA,MAAMO,OAAO,GAAGX,SAAS,CAACY,OAAO,CAAEC,UAAU,CAAC,QAAQ,CAAE;MACxD,MAAMC,MAAM,GAAGH,OAAO,CAACG,MAGtB;MACD,MAAMC,GAAG,GAAGtB,UAAU,CAACuB,GAAG,CAAC,CAAC;MAC5BF,MAAM,CAACG,KAAK,GAAGH,MAAM,CAACG,KAAK,GAAGF,GAAG;MACjCD,MAAM,CAACI,MAAM,GAAGJ,MAAM,CAACI,MAAM,GAAGH,GAAG;MAEnC,MAAMI,kBAAkB,GAAGb,SAAS,CAACC,GAAG,CAACa,wBAAwB,CAAC,CAAC;MACnET,OAAO,CAACU,SAAS,CAAC;QAChBZ,MAAM;QACNa,MAAM,EAAEH,kBAAkB;QAC1BI,SAAS,EAAE;MACb,CAAC,CAAC;MAEF1B,UAAU,CAACM,OAAO,CAAC;MAEnB,IAAI,CAACC,SAAS,EAAE;QACdF,YAAY,CAAC;UAAEO,MAAM;UAAEE,OAAO;UAAEQ;QAAmB,CAAC,CAAC;MACvD;IACF,CAAC,EAAE,CAAC;IAEJ,OAAO,MAAM;MACXf,SAAS,GAAG,IAAI;IAClB,CAAC;IACD;EACF,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO;IAAEJ,SAAS;IAAEG,OAAO;IAAEF;EAAU,CAAC;AAC1C","ignoreList":[]}
1
+ {"version":3,"names":["PixelRatio","useCanvasRef","useCallback","useEffect","useRef","useState","BackgroundRuntime","getSharedGPUDevice","useWGPUSetup","canvasRef","resources","setResources","runtime","configuredSizeRef","cancelled","device","context","current","getContext","canvas","dpr","get","width","Math","max","round","height","presentationFormat","navigator","gpu","getPreferredCanvasFormat","configure","format","alphaMode","onCanvasLayout","event","nativeEvent","layout","w","h","prev"],"sourceRoot":"../../../src","sources":["hooks/useWGPUSetup.tsx"],"mappings":";;AAAA,SAASA,UAAU,QAAgC,cAAc;AACjE,SACEC,YAAY,QAGP,qBAAqB;AAC5B,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,OAAO;AAEhE,SAASC,iBAAiB,QAAQ,+BAA4B;AAC9D,SAASC,kBAAkB,QAAQ,uBAAoB;AAqBvD,OAAO,SAASC,YAAYA,CAAA,EAAoB;EAC9C,MAAMC,SAAS,GAAGR,YAAY,CAAC,CAAC;EAChC,MAAM,CAACS,SAAS,EAAEC,YAAY,CAAC,GAAGN,QAAQ,CAAsB,IAAI,CAAC;EACrE,MAAMO,OAAO,GAAGN,iBAAiB;;EAEjC;EACA;EACA;EACA,MAAMO,iBAAiB,GAAGT,MAAM,CAC9B,IACF,CAAC;EAEDD,SAAS,CAAC,MAAM;IACd,IAAIW,SAAS,GAAG,KAAK;IAErB,CAAC,YAAY;MACX;MACA;MACA,MAAMC,MAAM,GAAG,MAAMR,kBAAkB,CAAC,CAAC;MACzC,IAAI,CAACQ,MAAM,IAAID,SAAS,EAAE;QACxB;MACF;MAEA,MAAME,OAAO,GAAGP,SAAS,CAACQ,OAAO,CAAEC,UAAU,CAAC,QAAQ,CAAE;MACxD,MAAMC,MAAM,GAAGH,OAAO,CAACG,MAAwB;MAC/C,MAAMC,GAAG,GAAGpB,UAAU,CAACqB,GAAG,CAAC,CAAC;MAC5B,MAAMC,KAAK,GAAGC,IAAI,CAACC,GAAG,CAAC,CAAC,EAAED,IAAI,CAACE,KAAK,CAACN,MAAM,CAACG,KAAK,GAAGF,GAAG,CAAC,CAAC;MACzD,MAAMM,MAAM,GAAGH,IAAI,CAACC,GAAG,CAAC,CAAC,EAAED,IAAI,CAACE,KAAK,CAACN,MAAM,CAACO,MAAM,GAAGN,GAAG,CAAC,CAAC;MAC3DD,MAAM,CAACG,KAAK,GAAGA,KAAK;MACpBH,MAAM,CAACO,MAAM,GAAGA,MAAM;MAEtB,MAAMC,kBAAkB,GAAGC,SAAS,CAACC,GAAG,CAACC,wBAAwB,CAAC,CAAC;MACnEd,OAAO,CAACe,SAAS,CAAC;QAChBhB,MAAM;QACNiB,MAAM,EAAEL,kBAAkB;QAC1BM,SAAS,EAAE;MACb,CAAC,CAAC;MACFpB,iBAAiB,CAACI,OAAO,GAAG;QAAEK,KAAK;QAAEI;MAAO,CAAC;MAE7C,IAAI,CAACZ,SAAS,EAAE;QACdH,YAAY,CAAC;UAAEI,MAAM;UAAEC,OAAO;UAAEW;QAAmB,CAAC,CAAC;MACvD;IACF,CAAC,EAAE,CAAC;IAEJ,OAAO,MAAM;MACXb,SAAS,GAAG,IAAI;MAChB;MACA;MACA;MACA;MACA;IACF,CAAC;IACD;EACF,CAAC,EAAE,EAAE,CAAC;;EAEN;EACA;EACA;EACA;EACA;EACA;EACA,MAAMoB,cAAc,GAAGhC,WAAW,CAC/BiC,KAAwB,IAAK;IAC5B,IAAI,CAACzB,SAAS,EAAE;MACd;IACF;IACA,MAAM;MAAEY,KAAK;MAAEI;IAAO,CAAC,GAAGS,KAAK,CAACC,WAAW,CAACC,MAAM;IAClD,MAAMjB,GAAG,GAAGpB,UAAU,CAACqB,GAAG,CAAC,CAAC;IAC5B,MAAMiB,CAAC,GAAGf,IAAI,CAACC,GAAG,CAAC,CAAC,EAAED,IAAI,CAACE,KAAK,CAACH,KAAK,GAAGF,GAAG,CAAC,CAAC;IAC9C,MAAMmB,CAAC,GAAGhB,IAAI,CAACC,GAAG,CAAC,CAAC,EAAED,IAAI,CAACE,KAAK,CAACC,MAAM,GAAGN,GAAG,CAAC,CAAC;IAE/C,MAAMoB,IAAI,GAAG3B,iBAAiB,CAACI,OAAO;IACtC,IAAIuB,IAAI,IAAIA,IAAI,CAAClB,KAAK,KAAKgB,CAAC,IAAIE,IAAI,CAACd,MAAM,KAAKa,CAAC,EAAE;MACjD;IACF;IAEA,MAAMpB,MAAM,GAAGT,SAAS,CAACM,OAAO,CAACG,MAAwB;IACzDA,MAAM,CAACG,KAAK,GAAGgB,CAAC;IAChBnB,MAAM,CAACO,MAAM,GAAGa,CAAC;IACjB7B,SAAS,CAACM,OAAO,CAACe,SAAS,CAAC;MAC1BhB,MAAM,EAAEL,SAAS,CAACK,MAAM;MACxBiB,MAAM,EAAEtB,SAAS,CAACiB,kBAAkB;MACpCM,SAAS,EAAE;IACb,CAAC,CAAC;IACFpB,iBAAiB,CAACI,OAAO,GAAG;MAAEK,KAAK,EAAEgB,CAAC;MAAEZ,MAAM,EAAEa;IAAE,CAAC;EACrD,CAAC,EACD,CAAC7B,SAAS,CACZ,CAAC;EAED,OAAO;IAAED,SAAS;IAAEG,OAAO;IAAEF,SAAS;IAAEwB;EAAe,CAAC;AAC1D","ignoreList":[]}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+
3
+ let devicePromise = null;
4
+
5
+ /**
6
+ * Lazily requests a single GPUDevice shared by every ShaderView.
7
+ *
8
+ * Requesting an adapter + device per view is expensive (real GPU/memory cost and
9
+ * slower init), and a screen that mounts several effects would otherwise spin up
10
+ * N devices on the one background runtime. The promise is cached for the lifetime
11
+ * of the JS runtime so every view awaits the same device.
12
+ *
13
+ * If the device is ever lost, the cache is cleared so the next ShaderView mount
14
+ * requests a fresh one instead of awaiting a dead device forever.
15
+ */
16
+ export function getSharedGPUDevice() {
17
+ if (devicePromise) {
18
+ return devicePromise;
19
+ }
20
+ const promise = (async () => {
21
+ const adapter = await navigator.gpu.requestAdapter();
22
+ if (!adapter) {
23
+ return null;
24
+ }
25
+ const device = await adapter.requestDevice();
26
+ device.lost?.then(() => {
27
+ // Only clear if we're still the cached promise, so we don't clobber a
28
+ // newer device that a later mount may already have requested.
29
+ if (devicePromise === promise) {
30
+ devicePromise = null;
31
+ }
32
+ });
33
+ return device;
34
+ })();
35
+ devicePromise = promise;
36
+ return promise;
37
+ }
38
+ //# sourceMappingURL=gpuDevice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["devicePromise","getSharedGPUDevice","promise","adapter","navigator","gpu","requestAdapter","device","requestDevice","lost","then"],"sourceRoot":"../../../src","sources":["utils/gpuDevice.ts"],"mappings":";;AAAA,IAAIA,aAA+C,GAAG,IAAI;;AAE1D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,kBAAkBA,CAAA,EAA8B;EAC9D,IAAID,aAAa,EAAE;IACjB,OAAOA,aAAa;EACtB;EAEA,MAAME,OAAO,GAAG,CAAC,YAAY;IAC3B,MAAMC,OAAO,GAAG,MAAMC,SAAS,CAACC,GAAG,CAACC,cAAc,CAAC,CAAC;IACpD,IAAI,CAACH,OAAO,EAAE;MACZ,OAAO,IAAI;IACb;IAEA,MAAMI,MAAM,GAAG,MAAMJ,OAAO,CAACK,aAAa,CAAC,CAAC;IAC5CD,MAAM,CAACE,IAAI,EAAEC,IAAI,CAAC,MAAM;MACtB;MACA;MACA,IAAIV,aAAa,KAAKE,OAAO,EAAE;QAC7BF,aAAa,GAAG,IAAI;MACtB;IACF,CAAC,CAAC;IACF,OAAOO,MAAM;EACf,CAAC,EAAE,CAAC;EAEJP,aAAa,GAAGE,OAAO;EACvB,OAAOA,OAAO;AAChB","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/ShaderView/index.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAS/C,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EACjC,cAAc,EACd,MAAW,EACX,KAAW,EACX,MAAW,EACX,QAAgB,EAChB,WAAmB,EACnB,oBAAoB,EACpB,KAAK,EACL,GAAG,SAAS,EACb,EAAE,eAAe,+BAiOjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/ShaderView/index.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAS/C,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EACjC,cAAc,EACd,MAAW,EACX,KAAW,EACX,MAAW,EACX,QAAgB,EAChB,WAAmB,EACnB,oBAAoB,EACpB,KAAK,EACL,GAAG,SAAS,EACb,EAAE,eAAe,+BAwSjB"}
@@ -1,5 +1,6 @@
1
+ import { type LayoutChangeEvent } from 'react-native';
1
2
  import { type CanvasRef, type RNCanvasContext } from 'react-native-webgpu';
2
- import type { WorkletRuntime } from 'react-native-worklets';
3
+ import { type WorkletRuntime } from 'react-native-worklets';
3
4
  type GPUResources = {
4
5
  device: GPUDevice;
5
6
  context: RNCanvasContext;
@@ -9,6 +10,8 @@ type WGPUSetupResult = {
9
10
  canvasRef: React.RefObject<CanvasRef>;
10
11
  runtime: WorkletRuntime;
11
12
  resources: GPUResources | null;
13
+ /** Wire to `<Canvas onLayout>` so the surface resizes on rotation/layout change. */
14
+ onCanvasLayout: (event: LayoutChangeEvent) => void;
12
15
  };
13
16
  export declare function useWGPUSetup(): WGPUSetupResult;
14
17
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"useWGPUSetup.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useWGPUSetup.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D,KAAK,YAAY,GAAG;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,eAAe,CAAC;IACzB,kBAAkB,EAAE,gBAAgB,CAAC;CACtC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,OAAO,EAAE,cAAc,CAAC;IACxB,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,wBAAgB,YAAY,IAAI,eAAe,CAiD9C"}
1
+ {"version":3,"file":"useWGPUSetup.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useWGPUSetup.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D,KAAK,YAAY,GAAG;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,eAAe,CAAC;IACzB,kBAAkB,EAAE,gBAAgB,CAAC;CACtC,CAAC;AAOF,KAAK,eAAe,GAAG;IACrB,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,OAAO,EAAE,cAAc,CAAC;IACxB,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,oFAAoF;IACpF,cAAc,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACpD,CAAC;AAEF,wBAAgB,YAAY,IAAI,eAAe,CA0F9C"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Lazily requests a single GPUDevice shared by every ShaderView.
3
+ *
4
+ * Requesting an adapter + device per view is expensive (real GPU/memory cost and
5
+ * slower init), and a screen that mounts several effects would otherwise spin up
6
+ * N devices on the one background runtime. The promise is cached for the lifetime
7
+ * of the JS runtime so every view awaits the same device.
8
+ *
9
+ * If the device is ever lost, the cache is cleared so the next ShaderView mount
10
+ * requests a fresh one instead of awaiting a dead device forever.
11
+ */
12
+ export declare function getSharedGPUDevice(): Promise<GPUDevice | null>;
13
+ //# sourceMappingURL=gpuDevice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpuDevice.d.ts","sourceRoot":"","sources":["../../../../src/utils/gpuDevice.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAwB9D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-effects",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "WebGPU-powered visual effects running on a background thread in React Native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -89,7 +89,7 @@
89
89
  "react-native-builder-bob": "^0.41.0",
90
90
  "react-native-gesture-handler": "2.31.1",
91
91
  "react-native-reanimated": "4.3.1",
92
- "react-native-webgpu": "0.5.14",
92
+ "react-native-webgpu": "0.5.15",
93
93
  "react-native-worklets": "0.8.3",
94
94
  "release-it": "^20.2.0",
95
95
  "turbo": "^2.9.16",
@@ -1,6 +1,6 @@
1
- import { PixelRatio, StyleSheet } from 'react-native';
2
- import { Canvas } from 'react-native-webgpu';
3
- import { useEffect, useRef } from 'react';
1
+ import { AppState, PixelRatio, StyleSheet } from 'react-native';
2
+ import { Canvas, installWebGPU } from 'react-native-webgpu';
3
+ import { useEffect, useRef, useState } from 'react';
4
4
  import { createSynchronizable, scheduleOnRuntime } from 'react-native-worklets';
5
5
  import { colorToVec4 } from '../../utils/colors';
6
6
  import { useWGPUSetup } from '../../hooks/useWGPUSetup';
@@ -29,7 +29,22 @@ export default function ShaderView({
29
29
  style,
30
30
  ...viewProps
31
31
  }: ShaderViewProps) {
32
- const { canvasRef, runtime, resources } = useWGPUSetup();
32
+ const { canvasRef, runtime, resources, onCanvasLayout } = useWGPUSetup();
33
+
34
+ // Pause the render loop while the app is backgrounded. The rAF loop otherwise
35
+ // keeps churning frames against a surface that's offscreen (and often
36
+ // transiently invalid), wasting battery/GPU. We only pause on a true
37
+ // 'background' transition — iOS 'inactive' (app switcher peek, notification
38
+ // pulldown) is left running to avoid flicker on brief, foreground interruptions.
39
+ const [appActive, setAppActive] = useState(
40
+ () => (AppState.currentState ?? 'active') !== 'background'
41
+ );
42
+ useEffect(() => {
43
+ const subscription = AppState.addEventListener('change', (state) => {
44
+ setAppActive(state !== 'background');
45
+ });
46
+ return () => subscription.remove();
47
+ }, []);
33
48
 
34
49
  const propsSync = useRef(
35
50
  createSynchronizable<Float64Array>(new Float64Array(SYNC_SIZE))
@@ -81,9 +96,12 @@ export default function ShaderView({
81
96
  };
82
97
  }, [propsSync]);
83
98
 
84
- // Start render loop when GPU resources are ready
99
+ // Start render loop when GPU resources are ready and the app is foregrounded.
100
+ // When the app backgrounds, `appActive` flips false and this effect's cleanup
101
+ // tears the loop down (via the `cancelled` token below); on return to the
102
+ // foreground it re-runs and starts a fresh loop.
85
103
  useEffect(() => {
86
- if (!resources) {
104
+ if (!resources || !appActive) {
87
105
  return;
88
106
  }
89
107
 
@@ -99,6 +117,12 @@ export default function ShaderView({
99
117
  scheduleOnRuntime(runtime, () => {
100
118
  'worklet';
101
119
 
120
+ // Worklet runtimes start without the WebGPU flag constants
121
+ // (GPUBufferUsage, GPUTextureUsage, ...). installWebGPU() captures them
122
+ // into this runtime so they're available below. Idempotent / safe no-op
123
+ // if already installed.
124
+ installWebGPU();
125
+
102
126
  // Create pipeline once
103
127
  const pipeline = device.createRenderPipeline({
104
128
  layout: 'auto',
@@ -128,16 +152,38 @@ export default function ShaderView({
128
152
  const uniformData = new Float32Array(UNIFORM_FLOAT_COUNT);
129
153
  let accumulatedTime = 0;
130
154
  let lastTimestamp = 0;
155
+ let warned = false;
156
+ let bufferDestroyed = false;
157
+
158
+ // Free this loop's uniform buffer when the loop ends (alive=0 / superseded
159
+ // by Fast Refresh / unmount). On a fragmentShader change the effect
160
+ // re-runs and schedules a fresh loop with a new buffer while the device
161
+ // persists, so without this the old buffer leaks every shader swap.
162
+ function destroyBuffer() {
163
+ if (bufferDestroyed) {
164
+ return;
165
+ }
166
+ bufferDestroyed = true;
167
+ try {
168
+ uniformBuffer.destroy();
169
+ } catch {
170
+ // The device may already have been destroyed on unmount — the buffer
171
+ // is gone either way, so there's nothing to recover.
172
+ return;
173
+ }
174
+ }
131
175
 
132
176
  function render(timestamp: number) {
133
177
  const props = propsSync.getDirty();
134
178
  if (props[IDX_ALIVE] === 0) {
179
+ destroyBuffer();
135
180
  return;
136
181
  }
137
182
 
138
183
  // This loop was superseded (Fast Refresh / unmount) — bail without
139
184
  // scheduling another frame so it can be garbage-collected.
140
185
  if (cancelled.getDirty()[0] === 1) {
186
+ destroyBuffer();
141
187
  return;
142
188
  }
143
189
 
@@ -206,29 +252,44 @@ export default function ShaderView({
206
252
  uniformData[27] = live[3]!;
207
253
  }
208
254
 
209
- device.queue.writeBuffer(uniformBuffer, 0, uniformData);
210
-
211
- const commandEncoder = device.createCommandEncoder();
212
- const textureView = context.getCurrentTexture().createView();
213
- const passEncoder = commandEncoder.beginRenderPass({
214
- colorAttachments: [
215
- {
216
- view: textureView,
217
- clearValue: transparent ? [0, 0, 0, 0] : [0, 0, 0, 1],
218
- loadOp: 'clear',
219
- storeOp: 'store',
220
- },
221
- ],
222
- });
223
-
224
- passEncoder.setPipeline(pipeline);
225
- passEncoder.setBindGroup(0, bindGroup);
226
- passEncoder.draw(3);
227
- passEncoder.end();
228
-
229
- device.queue.submit([commandEncoder.finish()]);
230
- context.present();
255
+ // GPU work can throw when the surface is transiently invalid — the app
256
+ // backgrounded, the view detached, or the device was lost. Swallow the
257
+ // failed frame so the loop survives and recovers when the surface comes
258
+ // back (rather than the worklet crashing or the loop dying silently).
259
+ try {
260
+ device.queue.writeBuffer(uniformBuffer, 0, uniformData);
261
+
262
+ const commandEncoder = device.createCommandEncoder();
263
+ const textureView = context.getCurrentTexture().createView();
264
+ const passEncoder = commandEncoder.beginRenderPass({
265
+ colorAttachments: [
266
+ {
267
+ view: textureView,
268
+ clearValue: transparent ? [0, 0, 0, 0] : [0, 0, 0, 1],
269
+ loadOp: 'clear',
270
+ storeOp: 'store',
271
+ },
272
+ ],
273
+ });
274
+
275
+ passEncoder.setPipeline(pipeline);
276
+ passEncoder.setBindGroup(0, bindGroup);
277
+ passEncoder.draw(3);
278
+ passEncoder.end();
279
+
280
+ device.queue.submit([commandEncoder.finish()]);
281
+ context.present();
282
+ } catch (e) {
283
+ // Warn once to avoid spamming the console every frame on a persistent
284
+ // failure (e.g. a lost device).
285
+ if (!warned) {
286
+ warned = true;
287
+ console.warn('[react-native-effects] render frame failed:', e);
288
+ }
289
+ }
231
290
 
291
+ // Always reschedule animated loops, even after a caught failure, so the
292
+ // effect self-heals once the surface is valid again.
232
293
  if (!isStatic) {
233
294
  requestAnimationFrame(render);
234
295
  }
@@ -242,6 +303,7 @@ export default function ShaderView({
242
303
  };
243
304
  }, [
244
305
  resources,
306
+ appActive,
245
307
  runtime,
246
308
  propsSync,
247
309
  paramsSynchronizable,
@@ -251,7 +313,16 @@ export default function ShaderView({
251
313
  ]);
252
314
 
253
315
  return (
254
- <Canvas ref={canvasRef} style={[styles.canvas, style]} {...viewProps} />
316
+ <Canvas
317
+ ref={canvasRef}
318
+ transparent={transparent}
319
+ style={[styles.canvas, style]}
320
+ {...viewProps}
321
+ onLayout={(event) => {
322
+ onCanvasLayout(event);
323
+ viewProps.onLayout?.(event);
324
+ }}
325
+ />
255
326
  );
256
327
  }
257
328
 
@@ -1,13 +1,13 @@
1
- import { PixelRatio } from 'react-native';
1
+ import { PixelRatio, type LayoutChangeEvent } from 'react-native';
2
2
  import {
3
3
  useCanvasRef,
4
4
  type CanvasRef,
5
5
  type RNCanvasContext,
6
6
  } from 'react-native-webgpu';
7
- import { useEffect, useState } from 'react';
8
- import type { WorkletRuntime } from 'react-native-worklets';
9
- import { initWebGPU } from '../utils/initWebGPU';
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
+ import { type WorkletRuntime } from 'react-native-worklets';
10
9
  import { BackgroundRuntime } from '../utils/backgroundRuntime';
10
+ import { getSharedGPUDevice } from '../utils/gpuDevice';
11
11
 
12
12
  type GPUResources = {
13
13
  device: GPUDevice;
@@ -15,10 +15,17 @@ type GPUResources = {
15
15
  presentationFormat: GPUTextureFormat;
16
16
  };
17
17
 
18
+ type CanvasWithSize = RNCanvasContext['canvas'] & {
19
+ width: number;
20
+ height: number;
21
+ };
22
+
18
23
  type WGPUSetupResult = {
19
24
  canvasRef: React.RefObject<CanvasRef>;
20
25
  runtime: WorkletRuntime;
21
26
  resources: GPUResources | null;
27
+ /** Wire to `<Canvas onLayout>` so the surface resizes on rotation/layout change. */
28
+ onCanvasLayout: (event: LayoutChangeEvent) => void;
22
29
  };
23
30
 
24
31
  export function useWGPUSetup(): WGPUSetupResult {
@@ -26,28 +33,31 @@ export function useWGPUSetup(): WGPUSetupResult {
26
33
  const [resources, setResources] = useState<GPUResources | null>(null);
27
34
  const runtime = BackgroundRuntime;
28
35
 
36
+ // Physical-pixel size the surface is currently configured for. Used to drive
37
+ // resize (and to skip redundant reconfigures when the layout pass reports the
38
+ // same size).
39
+ const configuredSizeRef = useRef<{ width: number; height: number } | null>(
40
+ null
41
+ );
42
+
29
43
  useEffect(() => {
30
44
  let cancelled = false;
31
45
 
32
46
  (async () => {
33
- const adapter = await navigator.gpu.requestAdapter();
34
- if (!adapter || cancelled) {
35
- return;
36
- }
37
-
38
- const device = await adapter.requestDevice();
39
- if (cancelled) {
47
+ // Shared across every ShaderView — see getSharedGPUDevice. Returns null if
48
+ // no adapter is available.
49
+ const device = await getSharedGPUDevice();
50
+ if (!device || cancelled) {
40
51
  return;
41
52
  }
42
53
 
43
54
  const context = canvasRef.current!.getContext('webgpu')!;
44
- const canvas = context.canvas as typeof context.canvas & {
45
- width: number;
46
- height: number;
47
- };
55
+ const canvas = context.canvas as CanvasWithSize;
48
56
  const dpr = PixelRatio.get();
49
- canvas.width = canvas.width * dpr;
50
- canvas.height = canvas.height * dpr;
57
+ const width = Math.max(1, Math.round(canvas.width * dpr));
58
+ const height = Math.max(1, Math.round(canvas.height * dpr));
59
+ canvas.width = width;
60
+ canvas.height = height;
51
61
 
52
62
  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
53
63
  context.configure({
@@ -55,8 +65,7 @@ export function useWGPUSetup(): WGPUSetupResult {
55
65
  format: presentationFormat,
56
66
  alphaMode: 'premultiplied',
57
67
  });
58
-
59
- initWebGPU(runtime);
68
+ configuredSizeRef.current = { width, height };
60
69
 
61
70
  if (!cancelled) {
62
71
  setResources({ device, context, presentationFormat });
@@ -65,9 +74,48 @@ export function useWGPUSetup(): WGPUSetupResult {
65
74
 
66
75
  return () => {
67
76
  cancelled = true;
77
+ // The device is shared across all ShaderViews and lives for the JS
78
+ // runtime's lifetime, so it must NOT be destroyed here. This view's own
79
+ // GPU resources (the uniform buffer) are torn down by the render loop when
80
+ // it stops; the pipeline/shader modules are released once the loop's
81
+ // worklet closure is garbage-collected.
68
82
  };
69
83
  // eslint-disable-next-line react-hooks/exhaustive-deps
70
84
  }, []);
71
85
 
72
- return { canvasRef, runtime, resources };
86
+ // Resize the drawing buffer and reconfigure the surface whenever the view's
87
+ // layout changes (most notably device rotation). Without this the buffer keeps
88
+ // its original dimensions and the shader renders stretched / wrong-aspect. The
89
+ // render loop reads `canvas.width/height` each frame, so the resolution uniform
90
+ // picks up the new size on the next frame. No-op until GPU resources are ready
91
+ // (initial sizing is handled by the setup effect above).
92
+ const onCanvasLayout = useCallback(
93
+ (event: LayoutChangeEvent) => {
94
+ if (!resources) {
95
+ return;
96
+ }
97
+ const { width, height } = event.nativeEvent.layout;
98
+ const dpr = PixelRatio.get();
99
+ const w = Math.max(1, Math.round(width * dpr));
100
+ const h = Math.max(1, Math.round(height * dpr));
101
+
102
+ const prev = configuredSizeRef.current;
103
+ if (prev && prev.width === w && prev.height === h) {
104
+ return;
105
+ }
106
+
107
+ const canvas = resources.context.canvas as CanvasWithSize;
108
+ canvas.width = w;
109
+ canvas.height = h;
110
+ resources.context.configure({
111
+ device: resources.device,
112
+ format: resources.presentationFormat,
113
+ alphaMode: 'premultiplied',
114
+ });
115
+ configuredSizeRef.current = { width: w, height: h };
116
+ },
117
+ [resources]
118
+ );
119
+
120
+ return { canvasRef, runtime, resources, onCanvasLayout };
73
121
  }
@@ -0,0 +1,38 @@
1
+ let devicePromise: Promise<GPUDevice | null> | null = null;
2
+
3
+ /**
4
+ * Lazily requests a single GPUDevice shared by every ShaderView.
5
+ *
6
+ * Requesting an adapter + device per view is expensive (real GPU/memory cost and
7
+ * slower init), and a screen that mounts several effects would otherwise spin up
8
+ * N devices on the one background runtime. The promise is cached for the lifetime
9
+ * of the JS runtime so every view awaits the same device.
10
+ *
11
+ * If the device is ever lost, the cache is cleared so the next ShaderView mount
12
+ * requests a fresh one instead of awaiting a dead device forever.
13
+ */
14
+ export function getSharedGPUDevice(): Promise<GPUDevice | null> {
15
+ if (devicePromise) {
16
+ return devicePromise;
17
+ }
18
+
19
+ const promise = (async () => {
20
+ const adapter = await navigator.gpu.requestAdapter();
21
+ if (!adapter) {
22
+ return null;
23
+ }
24
+
25
+ const device = await adapter.requestDevice();
26
+ device.lost?.then(() => {
27
+ // Only clear if we're still the cached promise, so we don't clobber a
28
+ // newer device that a later mount may already have requested.
29
+ if (devicePromise === promise) {
30
+ devicePromise = null;
31
+ }
32
+ });
33
+ return device;
34
+ })();
35
+
36
+ devicePromise = promise;
37
+ return promise;
38
+ }
@@ -1,40 +0,0 @@
1
- "use strict";
2
-
3
- import { scheduleOnRuntime } from 'react-native-worklets';
4
-
5
- /**
6
- * Installs WebGPU globals on the given worklet runtime.
7
- *
8
- * Worklet runtimes run on a separate JS context that lacks browser-like globals.
9
- * This function captures WebGPU constants and `navigator.gpu` from the RN thread
10
- * and injects them into the worklet's `globalThis` so that WebGPU code can run
11
- * as if it were in a browser environment.
12
- *
13
- * @param runtime - The worklet runtime to initialize
14
- */
15
- export function initWebGPU(runtime) {
16
- const navigator = globalThis.navigator;
17
- const GPUBufferUsage = globalThis.GPUBufferUsage;
18
- const GPUColorWrite = globalThis.GPUColorWrite;
19
- const GPUMapMode = globalThis.GPUMapMode;
20
- const GPUShaderStage = globalThis.GPUShaderStage;
21
- const GPUTextureUsage = globalThis.GPUTextureUsage;
22
- scheduleOnRuntime(runtime, () => {
23
- 'worklet';
24
-
25
- if (globalThis.self) {
26
- return;
27
- }
28
- globalThis.self = globalThis;
29
- globalThis.navigator = {
30
- gpu: navigator.gpu
31
- };
32
- globalThis.GPUBufferUsage = GPUBufferUsage;
33
- globalThis.GPUColorWrite = GPUColorWrite;
34
- globalThis.GPUMapMode = GPUMapMode;
35
- globalThis.GPUShaderStage = GPUShaderStage;
36
- globalThis.GPUTextureUsage = GPUTextureUsage;
37
- globalThis.setImmediate = globalThis.requestAnimationFrame;
38
- });
39
- }
40
- //# sourceMappingURL=initWebGPU.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["scheduleOnRuntime","initWebGPU","runtime","navigator","globalThis","GPUBufferUsage","GPUColorWrite","GPUMapMode","GPUShaderStage","GPUTextureUsage","self","gpu","setImmediate","requestAnimationFrame"],"sourceRoot":"../../../src","sources":["utils/initWebGPU.ts"],"mappings":";;AAAA,SAASA,iBAAiB,QAA6B,uBAAuB;;AAE9E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAACC,OAAuB,EAAE;EAClD,MAAMC,SAAS,GAAGC,UAAU,CAACD,SAAyB;EACtD,MAAME,cAAc,GAAGD,UAAU,CAACC,cAAc;EAChD,MAAMC,aAAa,GAAGF,UAAU,CAACE,aAAa;EAC9C,MAAMC,UAAU,GAAGH,UAAU,CAACG,UAAU;EACxC,MAAMC,cAAc,GAAGJ,UAAU,CAACI,cAAc;EAChD,MAAMC,eAAe,GAAGL,UAAU,CAACK,eAAe;EAElDT,iBAAiB,CAACE,OAAO,EAAE,MAAM;IAC/B,SAAS;;IAET,IAAIE,UAAU,CAACM,IAAI,EAAE;MACnB;IACF;IACAN,UAAU,CAACM,IAAI,GAAGN,UAAU;IAC5BA,UAAU,CAACD,SAAS,GAAG;MAAEQ,GAAG,EAAER,SAAS,CAACQ;IAAI,CAAyB;IACrEP,UAAU,CAACC,cAAc,GAAGA,cAAc;IAC1CD,UAAU,CAACE,aAAa,GAAGA,aAAa;IACxCF,UAAU,CAACG,UAAU,GAAGA,UAAU;IAClCH,UAAU,CAACI,cAAc,GAAGA,cAAc;IAC1CJ,UAAU,CAACK,eAAe,GAAGA,eAAe;IAC5CL,UAAU,CAACQ,YAAY,GACrBR,UAAU,CAACS,qBAA4C;EAC3D,CAAC,CAAC;AACJ","ignoreList":[]}
@@ -1,23 +0,0 @@
1
- import { type WorkletRuntime } from 'react-native-worklets';
2
- /**
3
- * Installs WebGPU globals on the given worklet runtime.
4
- *
5
- * Worklet runtimes run on a separate JS context that lacks browser-like globals.
6
- * This function captures WebGPU constants and `navigator.gpu` from the RN thread
7
- * and injects them into the worklet's `globalThis` so that WebGPU code can run
8
- * as if it were in a browser environment.
9
- *
10
- * @param runtime - The worklet runtime to initialize
11
- */
12
- export declare function initWebGPU(runtime: WorkletRuntime): void;
13
- declare global {
14
- var self: typeof globalThis;
15
- var _WORKLET: boolean | undefined;
16
- var performance: {
17
- now(): number;
18
- };
19
- interface Navigator {
20
- gpu: GPU;
21
- }
22
- }
23
- //# sourceMappingURL=initWebGPU.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"initWebGPU.d.ts","sourceRoot":"","sources":["../../../../src/utils/initWebGPU.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE/E;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,QAwBjD;AAED,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,IAAI,EAAE,OAAO,UAAU,CAAC;IAC5B,IAAI,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAClC,IAAI,WAAW,EAAE;QAAE,GAAG,IAAI,MAAM,CAAA;KAAE,CAAC;IAEnC,UAAU,SAAS;QACjB,GAAG,EAAE,GAAG,CAAC;KACV;CACF"}
@@ -1,47 +0,0 @@
1
- import { scheduleOnRuntime, type WorkletRuntime } from 'react-native-worklets';
2
-
3
- /**
4
- * Installs WebGPU globals on the given worklet runtime.
5
- *
6
- * Worklet runtimes run on a separate JS context that lacks browser-like globals.
7
- * This function captures WebGPU constants and `navigator.gpu` from the RN thread
8
- * and injects them into the worklet's `globalThis` so that WebGPU code can run
9
- * as if it were in a browser environment.
10
- *
11
- * @param runtime - The worklet runtime to initialize
12
- */
13
- export function initWebGPU(runtime: WorkletRuntime) {
14
- const navigator = globalThis.navigator as NavigatorGPU;
15
- const GPUBufferUsage = globalThis.GPUBufferUsage;
16
- const GPUColorWrite = globalThis.GPUColorWrite;
17
- const GPUMapMode = globalThis.GPUMapMode;
18
- const GPUShaderStage = globalThis.GPUShaderStage;
19
- const GPUTextureUsage = globalThis.GPUTextureUsage;
20
-
21
- scheduleOnRuntime(runtime, () => {
22
- 'worklet';
23
-
24
- if (globalThis.self) {
25
- return;
26
- }
27
- globalThis.self = globalThis;
28
- globalThis.navigator = { gpu: navigator.gpu } as unknown as Navigator;
29
- globalThis.GPUBufferUsage = GPUBufferUsage;
30
- globalThis.GPUColorWrite = GPUColorWrite;
31
- globalThis.GPUMapMode = GPUMapMode;
32
- globalThis.GPUShaderStage = GPUShaderStage;
33
- globalThis.GPUTextureUsage = GPUTextureUsage;
34
- globalThis.setImmediate =
35
- globalThis.requestAnimationFrame as typeof setImmediate;
36
- });
37
- }
38
-
39
- declare global {
40
- var self: typeof globalThis;
41
- var _WORKLET: boolean | undefined;
42
- var performance: { now(): number };
43
-
44
- interface Navigator {
45
- gpu: GPU;
46
- }
47
- }