Skip to content

Commit a71203d

Browse files
committed
fix(@typegpu/react): Handle device loss gracefully
1 parent ebb4389 commit a71203d

5 files changed

Lines changed: 41 additions & 8 deletions

File tree

.github/workflows/pkg-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
run: pnpm nightly-build
3838

3939
- name: Publish (pkg.pr.new)
40-
run: pnpm exec pkg-pr-new publish './packages/typegpu' './packages/typegpu-noise' './packages/unplugin-typegpu' --json output.json --comment=off --pnpm --no-compact
40+
run: pnpm exec pkg-pr-new publish './packages/typegpu' './packages/typegpu-noise' './packages/typegpu-react' './packages/unplugin-typegpu' --json output.json --comment=off --pnpm --no-compact
4141
- name: Post or update comment
4242
uses: actions/github-script@v6
4343
with:

apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,21 @@ function App() {
5757

5858
const { ref, ctxRef } = useConfigureContext({ alphaMode: 'premultiplied' });
5959
useFrame(({ elapsedSeconds }) => {
60-
if (!ctxRef.current) return;
60+
const ctx = ctxRef.current;
61+
if (!ctx) return;
6162

6263
time.write(elapsedSeconds);
63-
renderPipeline.withColorAttachment({ view: ctxRef.current }).draw(3);
64+
renderPipeline.withColorAttachment({ view: ctx }).draw(3);
6465
});
6566

66-
return <canvas ref={ref} className="aspect-square h-full max-h-[100vw]" />;
67+
return (
68+
<>
69+
<canvas ref={ref} className="aspect-square h-full max-h-[100vw]" />
70+
<button type="button" onClick={() => root.destroy()}>
71+
Destroy
72+
</button>
73+
</>
74+
);
6775
}
6876

6977
// #region Example controls and cleanup

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"test:browser": "vitest run --browser.enabled --project browser",
3333
"test:browser:watch": "vitest --browser.enabled --project browser",
3434
"test:coverage": "vitest --coverage run",
35-
"nightly-build": "SKIP_TESTS=true pnpm --filter typegpu --filter @typegpu/noise --filter unplugin-typegpu prepublishOnly --skip-publish-tag-check",
35+
"nightly-build": "SKIP_TESTS=true pnpm --filter typegpu --filter @typegpu/noise --filter @typegpu/react --filter unplugin-typegpu prepublishOnly --skip-publish-tag-check",
3636
"changes": "tgpu-dev-cli changes"
3737
},
3838
"devDependencies": {

packages/typegpu-react/src/browser/use-configure-context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const useResizer: UseResizerHook = () => {
2929
return;
3030
}
3131

32-
el.width = Math.round(box.inlineSize * dpr);
33-
el.height = Math.round(box.blockSize * dpr);
32+
el.width = Math.max(1, Math.round(box.inlineSize * dpr));
33+
el.height = Math.max(1, Math.round(box.blockSize * dpr));
3434
});
3535

3636
const attachResizing = useEffectEvent((el: HTMLCanvasElement | OffscreenCanvas | null) => {

packages/typegpu-react/src/core/root-context.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ class OwnRootContext implements RootContext {
7171
promise: tgpu.init().then(
7272
(root) => {
7373
this.#result = { status: 'resolved', value: root };
74+
root.device.lost.then(() => {
75+
// TODO: React to reason
76+
this.#result = undefined;
77+
});
7478
return root;
7579
},
7680
(error) => {
@@ -144,7 +148,28 @@ export function useRoot(): TgpuRoot {
144148
if (result.status === 'rejected') {
145149
throw result.error as Error;
146150
}
147-
return result.status === 'pending' ? use(result.promise) : result.value;
151+
const root = result.status === 'pending' ? use(result.promise) : result.value;
152+
153+
// NOTE: Useful docs: https://toji.dev/webgpu-best-practices/device-loss.html
154+
const [_, rerender] = useState(0);
155+
useEffect(() => {
156+
let cancelled = false;
157+
158+
root.device.lost.then(() => {
159+
// TODO: React to reason
160+
if (cancelled) {
161+
return;
162+
}
163+
164+
rerender((a) => a + 1);
165+
});
166+
167+
return () => {
168+
cancelled = true;
169+
};
170+
});
171+
172+
return root;
148173
}
149174

150175
/**

0 commit comments

Comments
 (0)