Skip to content

Commit ec0207a

Browse files
authored
Merge pull request #24 from blazejkustra/feat/shared-gpu-device
perf(ShaderView): share one GPUDevice across all ShaderViews
2 parents ece9b1b + 3c99696 commit ec0207a

2 files changed

Lines changed: 49 additions & 27 deletions

File tree

src/hooks/useWGPUSetup.tsx

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
type RNCanvasContext,
66
} from 'react-native-webgpu';
77
import { useCallback, useEffect, useRef, useState } from 'react';
8-
import { scheduleOnRuntime, type WorkletRuntime } from 'react-native-worklets';
8+
import { type WorkletRuntime } from 'react-native-worklets';
99
import { BackgroundRuntime } from '../utils/backgroundRuntime';
10+
import { getSharedGPUDevice } from '../utils/gpuDevice';
1011

1112
type GPUResources = {
1213
device: GPUDevice;
@@ -38,27 +39,18 @@ export function useWGPUSetup(): WGPUSetupResult {
3839
const configuredSizeRef = useRef<{ width: number; height: number } | null>(
3940
null
4041
);
41-
// Kept so the device can be released on unmount.
42-
const deviceRef = useRef<GPUDevice | null>(null);
4342

4443
useEffect(() => {
4544
let cancelled = false;
4645

4746
(async () => {
48-
const adapter = await navigator.gpu.requestAdapter();
49-
if (!adapter || 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) {
5051
return;
5152
}
5253

53-
const device = await adapter.requestDevice();
54-
if (cancelled) {
55-
// Unmounted mid-init: the worklet never used this device, so it's safe
56-
// to drop it right here on the JS thread.
57-
device.destroy();
58-
return;
59-
}
60-
deviceRef.current = device;
61-
6254
const context = canvasRef.current!.getContext('webgpu')!;
6355
const canvas = context.canvas as CanvasWithSize;
6456
const dpr = PixelRatio.get();
@@ -82,19 +74,11 @@ export function useWGPUSetup(): WGPUSetupResult {
8274

8375
return () => {
8476
cancelled = true;
85-
const device = deviceRef.current;
86-
deviceRef.current = null;
87-
if (device) {
88-
// Release the device (and all GPU resources created from it: pipeline,
89-
// buffers, ...) on the render runtime — the thread that used it — after
90-
// the render loop has been signalled to stop, so we don't race an
91-
// in-flight frame. Any frame that does slip through hits a destroyed
92-
// device and is swallowed by the render loop's try/catch.
93-
scheduleOnRuntime(runtime, () => {
94-
'worklet';
95-
device.destroy();
96-
});
97-
}
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.
9882
};
9983
// eslint-disable-next-line react-hooks/exhaustive-deps
10084
}, []);

src/utils/gpuDevice.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)