Skip to content

Commit ad4ca93

Browse files
committed
Make hooks resilient to race conditions
1 parent 9c0f48a commit ad4ca93

8 files changed

Lines changed: 196 additions & 155 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { Canvas, useCanvasEffect } from "react-native-wgpu";
3838
import { redFragWGSL, triangleVertWGSL } from "./triangle";
3939

4040
export function HelloTriangle() {
41-
const ref = useCanvasEffect(async () => {
41+
const ref = useCanvasEffect(React.useCallback(async () => {
4242
const adapter = await navigator.gpu.requestAdapter();
4343
if (!adapter) {
4444
throw new Error("No adapter");
@@ -108,7 +108,7 @@ export function HelloTriangle() {
108108
device.queue.submit([commandEncoder.finish()]);
109109

110110
context.present();
111-
});
111+
}, []));
112112

113113
return (
114114
<View style={style.container}>

apps/example/src/ThreeJS/Cube.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as THREE from "three";
2+
import { useCallback } from "react";
23
import { Canvas, useCanvasEffect } from "react-native-wgpu";
34
import { View } from "react-native";
45

56
import { makeWebGPURenderer } from "./components/makeWebGPURenderer";
67

78
export const Cube = () => {
8-
const ref = useCanvasEffect(async () => {
9+
const ref = useCanvasEffect(useCallback(async () => {
910
const context = ref.current!.getContext("webgpu")!;
1011
const { width, height } = context.canvas;
1112

@@ -34,7 +35,7 @@ export const Cube = () => {
3435
return () => {
3536
renderer.setAnimationLoop(null);
3637
};
37-
});
38+
}, []));
3839

3940
return (
4041
<View style={{ flex: 1 }}>

apps/example/src/ThreeJS/components/FiberCanvas.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as THREE from "three";
2-
import React, { useRef } from "react";
2+
import React, { useCallback, useRef } from "react";
33
import type { ReconcilerRoot, RootState } from "@react-three/fiber";
44
import {
55
extend,
@@ -32,7 +32,7 @@ export const FiberCanvas = ({
3232

3333
React.useMemo(() => extend(THREE), []);
3434

35-
const canvasRef = useCanvasEffect(async () => {
35+
const canvasRef = useCanvasEffect(useCallback(async () => {
3636
const context = canvasRef.current!.getContext("webgpu")!;
3737
const renderer = makeWebGPURenderer(context);
3838

@@ -74,7 +74,7 @@ export const FiberCanvas = ({
7474
unmountComponentAtNode(canvas!);
7575
}
7676
};
77-
});
77+
}, [scene, camera]));
7878

7979
return <Canvas ref={canvasRef} style={style} />;
8080
};

apps/example/src/Triangle/HelloTriangle.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1-
import React from "react";
1+
import React, { useCallback } from "react";
22
import { StyleSheet, View, PixelRatio } from "react-native";
33
import { Canvas, useCanvasEffect } from "react-native-wgpu";
44

55
import { redFragWGSL, triangleVertWGSL } from "./triangle";
66

7+
const onAbort = (signal: AbortSignal, cb: () => unknown) => {
8+
signal.addEventListener('abort', cb);
9+
}
10+
711
export function HelloTriangle() {
8-
const ref = useCanvasEffect(async () => {
12+
const ref = useCanvasEffect(useCallback(async (signal) => {
913
const adapter = await navigator.gpu.requestAdapter();
1014
if (!adapter) {
1115
throw new Error("No adapter");
1216
}
1317
const device = await adapter.requestDevice();
18+
onAbort(signal, () => device.destroy());
19+
20+
if (signal.aborted) {
21+
return;
22+
}
23+
1424
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
1525

1626
const context = ref.current!.getContext("webgpu")!;
@@ -75,7 +85,7 @@ export function HelloTriangle() {
7585
device.queue.submit([commandEncoder.finish()]);
7686

7787
context.present();
78-
});
88+
}, []));
7989

8090
return (
8191
<View style={style.container}>

apps/example/src/Triangle/HelloTriangleMSAA.tsx

Lines changed: 78 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,96 @@
1-
import React from "react";
1+
import React, { useCallback } from "react";
22
import { StyleSheet, View } from "react-native";
33
import { Canvas, useCanvasEffect } from "react-native-wgpu";
44

55
import { redFragWGSL, triangleVertWGSL } from "./triangle";
66

77
export function HelloTriangleMSAA() {
8-
const ref = useCanvasEffect(() => {
9-
(async () => {
10-
const adapter = await navigator.gpu.requestAdapter();
11-
if (!adapter) {
12-
throw new Error("No adapter");
13-
}
14-
const device = await adapter.requestDevice();
15-
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
8+
const ref = useCanvasEffect(useCallback(async (signal) => {
9+
const adapter = await navigator.gpu.requestAdapter();
10+
if (!adapter) {
11+
throw new Error("No adapter");
12+
}
13+
const device = await adapter.requestDevice();
1614

17-
const context = ref.current!.getContext("webgpu")!;
18-
const { canvas } = context;
19-
if (!context) {
20-
throw new Error("No context");
21-
}
22-
context.configure({
23-
device,
24-
format: presentationFormat,
25-
alphaMode: "premultiplied",
26-
});
15+
if (signal.aborted) {
16+
device.destroy();
17+
return;
18+
}
2719

28-
const sampleCount = 4;
20+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
21+
const context = ref.current!.getContext("webgpu")!;
22+
const { canvas } = context;
23+
if (!context) {
24+
throw new Error("No context");
25+
}
26+
context.configure({
27+
device,
28+
format: presentationFormat,
29+
alphaMode: "premultiplied",
30+
});
2931

30-
const pipeline = device.createRenderPipeline({
31-
layout: "auto",
32-
vertex: {
33-
module: device.createShaderModule({
34-
code: triangleVertWGSL,
35-
}),
36-
},
37-
fragment: {
38-
module: device.createShaderModule({
39-
code: redFragWGSL,
40-
}),
41-
targets: [
42-
{
43-
format: presentationFormat,
44-
},
45-
],
46-
},
47-
primitive: {
48-
topology: "triangle-list",
49-
},
50-
multisample: {
51-
count: sampleCount,
52-
},
53-
});
32+
const sampleCount = 4;
5433

55-
const texture = device.createTexture({
56-
size: [canvas.width, canvas.height],
57-
sampleCount,
58-
format: presentationFormat,
59-
usage: GPUTextureUsage.RENDER_ATTACHMENT,
60-
});
61-
const view = texture.createView();
34+
const pipeline = device.createRenderPipeline({
35+
layout: "auto",
36+
vertex: {
37+
module: device.createShaderModule({
38+
code: triangleVertWGSL,
39+
}),
40+
},
41+
fragment: {
42+
module: device.createShaderModule({
43+
code: redFragWGSL,
44+
}),
45+
targets: [
46+
{
47+
format: presentationFormat,
48+
},
49+
],
50+
},
51+
primitive: {
52+
topology: "triangle-list",
53+
},
54+
multisample: {
55+
count: sampleCount,
56+
},
57+
});
6258

63-
function frame() {
64-
const commandEncoder = device.createCommandEncoder();
59+
const texture = device.createTexture({
60+
size: [canvas.width, canvas.height],
61+
sampleCount,
62+
format: presentationFormat,
63+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
64+
});
65+
const view = texture.createView();
66+
67+
const commandEncoder = device.createCommandEncoder();
68+
69+
const renderPassDescriptor: GPURenderPassDescriptor = {
70+
colorAttachments: [
71+
{
72+
view,
73+
resolveTarget: context.getCurrentTexture().createView(),
74+
clearValue: [0, 0, 0, 1],
75+
loadOp: "clear",
76+
storeOp: "discard",
77+
},
78+
],
79+
};
6580

66-
const renderPassDescriptor: GPURenderPassDescriptor = {
67-
colorAttachments: [
68-
{
69-
view,
70-
resolveTarget: context.getCurrentTexture().createView(),
71-
clearValue: [0, 0, 0, 1],
72-
loadOp: "clear",
73-
storeOp: "discard",
74-
},
75-
],
76-
};
81+
const passEncoder =
82+
commandEncoder.beginRenderPass(renderPassDescriptor);
83+
passEncoder.setPipeline(pipeline);
84+
passEncoder.draw(3);
85+
passEncoder.end();
7786

78-
const passEncoder =
79-
commandEncoder.beginRenderPass(renderPassDescriptor);
80-
passEncoder.setPipeline(pipeline);
81-
passEncoder.draw(3);
82-
passEncoder.end();
87+
device.queue.submit([commandEncoder.finish()]);
8388

84-
device.queue.submit([commandEncoder.finish()]);
85-
}
89+
context.present();
8690

87-
frame();
88-
context.present();
89-
})();
90-
});
91+
// Cleanup
92+
device.destroy();
93+
}, []));
9194

9295
return (
9396
<View style={style.container}>

apps/example/src/components/useWebGPU.ts

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef } from "react";
1+
import { useEffect } from "react";
22
import { PixelRatio } from "react-native";
33
import { useGPUContext, useDevice, type NativeCanvas } from "react-native-wgpu";
44

@@ -16,52 +16,61 @@ type Scene = (props: SceneProps) => RenderScene | void | Promise<RenderScene>;
1616
export const useWebGPU = (scene: Scene) => {
1717
const { device } = useDevice();
1818
const { ref, context } = useGPUContext();
19-
const animationFrameId = useRef<number | null>(null);
2019
useEffect(() => {
21-
(async () => {
22-
if (!context || !device) {
23-
return;
24-
}
20+
if (!context || !device) {
21+
return undefined; // No cleanup
22+
}
2523

26-
const canvas = context.canvas as HTMLCanvasElement;
27-
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
28-
canvas.width = canvas.clientWidth * PixelRatio.get();
29-
canvas.height = canvas.clientHeight * PixelRatio.get();
30-
context.configure({
31-
device,
32-
format: presentationFormat,
33-
alphaMode: "premultiplied",
34-
});
24+
const canvas = context.canvas as HTMLCanvasElement;
25+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
26+
canvas.width = canvas.clientWidth * PixelRatio.get();
27+
canvas.height = canvas.clientHeight * PixelRatio.get();
28+
context.configure({
29+
device,
30+
format: presentationFormat,
31+
alphaMode: "premultiplied",
32+
});
3533

36-
const sceneProps: SceneProps = {
37-
context,
38-
device,
39-
gpu: navigator.gpu,
40-
presentationFormat,
41-
canvas: context.canvas as unknown as NativeCanvas,
42-
};
34+
const sceneProps: SceneProps = {
35+
context,
36+
device,
37+
gpu: navigator.gpu,
38+
presentationFormat,
39+
canvas: context.canvas as unknown as NativeCanvas,
40+
};
41+
42+
const r = scene(sceneProps);
4343

44-
const r = scene(sceneProps);
44+
let handle: number | undefined;
45+
const abortCtrl = new AbortController();
46+
47+
(async () => {
4548
let renderScene: RenderScene;
46-
if (r instanceof Promise) {
49+
if (r && 'then' in r) {
4750
renderScene = await r;
4851
} else {
4952
renderScene = r as RenderScene;
5053
}
51-
if (typeof renderScene === "function") {
52-
const render = () => {
53-
const timestamp = Date.now();
54-
renderScene(timestamp);
55-
context.present();
56-
animationFrameId.current = requestAnimationFrame(render);
57-
};
5854

59-
animationFrameId.current = requestAnimationFrame(render);
55+
if (typeof renderScene !== "function" || abortCtrl.signal.aborted) {
56+
// No scene to render, or already aborted
57+
return;
6058
}
59+
60+
const render = () => {
61+
const timestamp = Date.now();
62+
renderScene(timestamp);
63+
context.present();
64+
handle = requestAnimationFrame(render);
65+
};
66+
67+
render();
6168
})();
69+
6270
return () => {
63-
if (animationFrameId.current) {
64-
cancelAnimationFrame(animationFrameId.current);
71+
abortCtrl.abort();
72+
if (handle !== undefined) {
73+
cancelAnimationFrame(handle);
6574
}
6675
};
6776
}, [context, device, scene]);

packages/webgpu/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { Canvas, useCanvasEffect } from "react-native-wgpu";
3838
import { redFragWGSL, triangleVertWGSL } from "./triangle";
3939

4040
export function HelloTriangle() {
41-
const ref = useCanvasEffect(async () => {
41+
const ref = useCanvasEffect(React.useCallback(async () => {
4242
const adapter = await navigator.gpu.requestAdapter();
4343
if (!adapter) {
4444
throw new Error("No adapter");
@@ -108,7 +108,7 @@ export function HelloTriangle() {
108108
device.queue.submit([commandEncoder.finish()]);
109109

110110
context.present();
111-
});
111+
}, []));
112112

113113
return (
114114
<View style={style.container}>

0 commit comments

Comments
 (0)