diff --git a/.github/workflows/pkg-pr.yml b/.github/workflows/pkg-pr.yml
index b8cabf72c3..48e55487ae 100644
--- a/.github/workflows/pkg-pr.yml
+++ b/.github/workflows/pkg-pr.yml
@@ -37,7 +37,7 @@ jobs:
run: pnpm nightly-build
- name: Publish (pkg.pr.new)
- run: pnpm exec pkg-pr-new publish './packages/typegpu' './packages/typegpu-noise' './packages/unplugin-typegpu' --json output.json --comment=off --pnpm --no-compact
+ 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
- name: Post or update comment
uses: actions/github-script@v6
with:
diff --git a/apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx b/apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx
index a1bcf3352e..35cc6e5a46 100644
--- a/apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx
+++ b/apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx
@@ -57,13 +57,21 @@ function App() {
const { ref, ctxRef } = useConfigureContext({ alphaMode: 'premultiplied' });
useFrame(({ elapsedSeconds }) => {
- if (!ctxRef.current) return;
+ const ctx = ctxRef.current;
+ if (!ctx) return;
time.write(elapsedSeconds);
- renderPipeline.withColorAttachment({ view: ctxRef.current }).draw(3);
+ renderPipeline.withColorAttachment({ view: ctx }).draw(3);
});
- return ;
+ return (
+ <>
+
+
+ >
+ );
}
// #region Example controls and cleanup
diff --git a/package.json b/package.json
index 9c4c629f92..bb183f91d8 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
"test:browser": "vitest run --browser.enabled --project browser",
"test:browser:watch": "vitest --browser.enabled --project browser",
"test:coverage": "vitest --coverage run",
- "nightly-build": "SKIP_TESTS=true pnpm --filter typegpu --filter @typegpu/noise --filter unplugin-typegpu prepublishOnly --skip-publish-tag-check",
+ "nightly-build": "SKIP_TESTS=true pnpm --filter typegpu --filter @typegpu/noise --filter @typegpu/react --filter unplugin-typegpu prepublishOnly --skip-publish-tag-check",
"changes": "tgpu-dev-cli changes"
},
"devDependencies": {
diff --git a/packages/typegpu-react/src/browser/use-configure-context.ts b/packages/typegpu-react/src/browser/use-configure-context.ts
index 039f8d9e7e..0c04949af9 100644
--- a/packages/typegpu-react/src/browser/use-configure-context.ts
+++ b/packages/typegpu-react/src/browser/use-configure-context.ts
@@ -29,8 +29,8 @@ const useResizer: UseResizerHook = () => {
return;
}
- el.width = Math.round(box.inlineSize * dpr);
- el.height = Math.round(box.blockSize * dpr);
+ el.width = Math.max(1, Math.round(box.inlineSize * dpr));
+ el.height = Math.max(1, Math.round(box.blockSize * dpr));
});
const attachResizing = useEffectEvent((el: HTMLCanvasElement | OffscreenCanvas | null) => {
diff --git a/packages/typegpu-react/src/core/root-context.tsx b/packages/typegpu-react/src/core/root-context.tsx
index 0c27b6ab4d..900084aa74 100644
--- a/packages/typegpu-react/src/core/root-context.tsx
+++ b/packages/typegpu-react/src/core/root-context.tsx
@@ -4,6 +4,7 @@ import React, {
useContext,
useEffect,
useMemo,
+ useRef,
useState,
} from 'react';
import tgpu, { type TgpuRoot } from 'typegpu';
@@ -58,6 +59,7 @@ type RootContextResult =
| { status: 'rejected'; error: unknown };
interface RootContext {
+ readonly rootRequested: boolean;
initOrGetRoot(): RootContextResult;
}
@@ -71,6 +73,10 @@ class OwnRootContext implements RootContext {
promise: tgpu.init().then(
(root) => {
this.#result = { status: 'resolved', value: root };
+ root.device.lost.then(() => {
+ // TODO: React to reason
+ this.#result = undefined;
+ });
return root;
},
(error) => {
@@ -83,6 +89,10 @@ class OwnRootContext implements RootContext {
return this.#result;
}
+
+ get rootRequested() {
+ return this.#result !== undefined;
+ }
}
class ExistingRootContext implements RootContext {
@@ -95,6 +105,10 @@ class ExistingRootContext implements RootContext {
initOrGetRoot(): RootContextResult {
return this.result;
}
+
+ get rootRequested() {
+ return this.result !== undefined;
+ }
}
/**
@@ -144,7 +158,32 @@ export function useRoot(): TgpuRoot {
if (result.status === 'rejected') {
throw result.error as Error;
}
- return result.status === 'pending' ? use(result.promise) : result.value;
+ const root = result.status === 'pending' ? use(result.promise) : result.value;
+
+ // NOTE: Useful docs: https://toji.dev/webgpu-best-practices/device-loss.html
+ const [_, rerender] = useState(0);
+ const lostRootsRef = useRef>(new WeakSet());
+ useEffect(() => {
+ let cancelled = false;
+
+ root.device.lost.then(() => {
+ // TODO: React to reason
+ if (cancelled || lostRootsRef.current.has(root)) {
+ return;
+ }
+ lostRootsRef.current.add(root);
+
+ if (!context.rootRequested) {
+ rerender((a) => a + 1);
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [root]);
+
+ return root;
}
/**