From a4f54248e079d2802982c75385216452bde724de Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Thu, 16 Jul 2026 00:00:34 +0200 Subject: [PATCH 1/4] feat: transfer TypeGPU resources to worklet runtimes Snapshot protocol for serializing roots, buffers, bindings, textures, bind groups, layouts, pipelines, query sets, samplers, slots, accessors and constants across JS runtimes sharing a GPU device, plus react-native-worklets integration in @typegpu/react (registerSerializables, useFrame on the UI runtime, worklet root context). --- apps/typegpu-docs/astro.config.mjs | 5 + .../integration/react-native/worklets.mdx | 108 ++ packages/typegpu-react/README.md | 6 + packages/typegpu-react/package.json | 21 +- .../typegpu-react/src/core/root-context.tsx | 15 +- .../src/react-native/core/use-frame.ts | 65 + .../serialization/register-serializables.ts | 86 ++ .../serialization/transfer-cache.ts | 117 ++ .../use-configure-worklet-context.ts | 37 + .../src/react-native/worklets.ts | 14 + .../register-serializables.test.ts | 84 ++ .../typegpu-react/tests/root-context.test.tsx | 29 + packages/typegpu-react/tsdown.config.ts | 2 +- packages/typegpu/src/core/buffer/buffer.ts | 70 +- .../typegpu/src/core/constant/tgpuConstant.ts | 31 + .../src/core/pipeline/computePipeline.ts | 76 +- .../src/core/pipeline/pipelineUtils.ts | 41 + .../src/core/pipeline/renderPipeline.ts | 160 ++- .../typegpu/src/core/pipeline/typeGuards.ts | 25 +- .../typegpu/src/core/querySet/querySet.ts | 51 +- packages/typegpu/src/core/root/init.ts | 90 +- packages/typegpu/src/core/root/rootTypes.ts | 4 + packages/typegpu/src/core/sampler/sampler.ts | 37 + packages/typegpu/src/core/slot/accessor.ts | 30 + packages/typegpu/src/core/slot/slot.ts | 13 + packages/typegpu/src/core/texture/texture.ts | 67 +- .../src/core/vertexLayout/vertexLayout.ts | 47 +- packages/typegpu/src/data/index.ts | 2 + packages/typegpu/src/indexNamedExports.ts | 5 + packages/typegpu/src/internal.ts | 14 + packages/typegpu/src/resolutionCtx.ts | 2 + packages/typegpu/src/serial/dataValue.ts | 46 + packages/typegpu/src/serial/layoutEntries.ts | 183 +++ packages/typegpu/src/serial/registry.ts | 306 ++++ packages/typegpu/src/serial/schema.ts | 193 +++ packages/typegpu/src/serial/types.ts | 6 + packages/typegpu/src/tgpuBindGroupLayout.ts | 71 + .../typegpu/tests/computePipeline.test.ts | 88 +- .../typegpu/tests/internal/typeGuards.test.ts | 42 + packages/typegpu/tests/renderPipeline.test.ts | 49 + packages/typegpu/tests/root.test.ts | 21 + packages/typegpu/tests/serial.test.ts | 240 ++++ .../typegpu/tests/serializeDataSchema.test.ts | 283 ++++ pnpm-lock.yaml | 1267 ++++++++++++++++- 44 files changed, 4023 insertions(+), 126 deletions(-) create mode 100644 apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx create mode 100644 packages/typegpu-react/src/react-native/core/use-frame.ts create mode 100644 packages/typegpu-react/src/react-native/serialization/register-serializables.ts create mode 100644 packages/typegpu-react/src/react-native/serialization/transfer-cache.ts create mode 100644 packages/typegpu-react/src/react-native/use-configure-worklet-context.ts create mode 100644 packages/typegpu-react/src/react-native/worklets.ts create mode 100644 packages/typegpu-react/tests/react-native/register-serializables.test.ts create mode 100644 packages/typegpu/src/serial/dataValue.ts create mode 100644 packages/typegpu/src/serial/layoutEntries.ts create mode 100644 packages/typegpu/src/serial/registry.ts create mode 100644 packages/typegpu/src/serial/schema.ts create mode 100644 packages/typegpu/src/serial/types.ts create mode 100644 packages/typegpu/tests/internal/typeGuards.test.ts create mode 100644 packages/typegpu/tests/serial.test.ts create mode 100644 packages/typegpu/tests/serializeDataSchema.test.ts diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index 1ac7e12b31..379d9c4853 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -243,6 +243,11 @@ export default defineConfig({ label: 'React Native', slug: 'integration/react-native', }, + { + label: 'React Native Worklets', + slug: 'integration/react-native/worklets', + badge: { text: 'experimental', variant: 'caution' }, + }, { label: 'WESL Interoperability', slug: 'integration/wesl-interoperability', diff --git a/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx new file mode 100644 index 0000000000..d2eb51defc --- /dev/null +++ b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx @@ -0,0 +1,108 @@ +--- +title: React Native Worklets +description: A guide on running TypeGPU render loops on the UI thread with react-native-worklets. +--- + +With [react-native-worklets](https://docs.swmansion.com/react-native-worklets/), per-frame GPU work can run on the UI thread, unaffected by a busy JS thread. +TypeGPU resources created on the JS thread can be captured by worklets directly, they are transferred between runtimes automatically. + +## Setup + +Follow the [React Native guide](/TypeGPU/integration/react-native/) first, then install `react-native-worklets` and enable its babel plugin with [Bundle Mode](https://docs.swmansion.com/react-native-worklets/docs/bundleMode/): + +```diff lang=js title="babel.config.js" ++const workletsPluginOptions = { ++ bundleMode: true, ++ importForwarding: { ++ moduleNames: ['typegpu'], ++ // Directories with your module-scope shader definitions ++ relativePaths: ['my-app/components'], ++ }, ++}; + +module.exports = (api) => { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [ + 'unplugin-typegpu/babel', ++ ['react-native-worklets/plugin', workletsPluginOptions], + ], + }; +}; +``` + +Import the hooks from the dedicated entrypoint - importing it also registers the transfer support for TypeGPU resources: + +```ts +import { useRoot, useFrame, useConfigureContext, useUniform } from '@typegpu/react/react-native-worklets'; +``` + +After changing the babel config, clear the Metro cache with `npx expo start --clear`. + +## Example + +Create resources on the JS thread, then use them freely inside a `useFrame` worklet: + +```tsx +import { useMemo } from 'react'; +import { Canvas } from 'react-native-webgpu'; +import tgpu, { common, d } from 'typegpu'; +import { useConfigureContext, useFrame, useRoot, useUniform } from '@typegpu/react/react-native-worklets'; + +export function Pulse() { + const root = useRoot(); + const color = useUniform(d.vec3f, { initial: d.vec3f(0.114, 0.447, 0.941) }); + + const pipeline = useMemo( + () => + root.createRenderPipeline({ + vertex: common.fullScreenTriangle, + fragment: () => { + 'use gpu'; + return d.vec4f(color.$, 1); + }, + }), + [root, color], + ); + + const { ref, ctxRef } = useConfigureContext({ alphaMode: 'premultiplied' }); + + // Runs each frame on the UI thread + useFrame(({ elapsedSeconds }) => { + 'worklet'; + const ctx = ctxRef.value; + if (!ctx) return; + + color.write(d.vec3f(0.5 + Math.sin(elapsedSeconds) * 0.5, 0.447, 0.941)); + pipeline.withColorAttachment({ view: ctx }).draw(3); + ctx.present?.(); + }); + + return ; +} +``` + +The `color` uniform and `pipeline` captured by the worklet are transferred to the UI runtime on first use. +Both runtimes share the same underlying GPU objects, and transferring the same resource again yields the same object back. + +This works for buffers (including `createUniform`/`createMutable`/`createReadonly`), textures, samplers, bind groups and their layouts, vertex layouts, query sets, pipelines, roots, slots, accessors, consts, and vector/matrix instances. + +## Rules of transfer + +**Definitions are runtime-local.** +Shader functions (`tgpu.fn`, entry functions), `tgpu.comptime`, and schemas cannot be serialized. +Make sure to create them on the runtime they will be used on, or keep them at module scope in files covered by `importForwarding`; worklets then re-import them natively on the UI runtime instead of transferring them. + +**Functions crossing runtimes must be worklets.** +Any plain function reachable from a transferred resource (e.g. a `withPerformanceCallback` callback) has to be marked with the `'worklet'` directive. + +:::note +Definitions created dynamically (inside components or worklets) cannot cross runtimes and throw. +If you create pipelines on the UI thread, memoize them - resolving a pipeline every frame is wasted work. +::: + +:::caution +Render pipelines created on the UI runtime must specify an explicit target format (e.g. `targets: { format: 'bgra8unorm' }`), the default relies on +`navigator.gpu.getPreferredCanvasFormat()`, and `navigator` is not available on worklet runtimes by default. +::: diff --git a/packages/typegpu-react/README.md b/packages/typegpu-react/README.md index bb7eb5d3ea..0b476a5d30 100644 --- a/packages/typegpu-react/README.md +++ b/packages/typegpu-react/README.md @@ -46,6 +46,12 @@ const App = (props: Props) => { }; ``` +# React Native + +The `@typegpu/react/react-native-worklets` entrypoint lets per-frame GPU work run on the UI thread. +TypeGPU resources captured by worklets are transferred between runtimes automatically. +See the [React Native Worklets guide](https://typegpu.com/integration/react-native/worklets). + ## TypeGPU is created by Software Mansion [![swm](https://logo.swmansion.com/logo?color=white&variant=desktop&width=150&tag=typegpu-github 'Software Mansion')](https://swmansion.com) diff --git a/packages/typegpu-react/package.json b/packages/typegpu-react/package.json index 46ed2a9291..f5b8e0ada0 100644 --- a/packages/typegpu-react/package.json +++ b/packages/typegpu-react/package.json @@ -17,7 +17,16 @@ "sideEffects": false, "types": "./src/browser/index.ts", "exports": { - ".": "./src/browser/index.ts", + ".": { + "react-native": "./src/react-native/index.ts", + "browser": "./src/browser/index.ts", + "default": "./src/browser/index.ts" + }, + "./react-native-worklets": { + "types": "./src/react-native/worklets.ts", + "react-native": "./src/react-native/worklets.ts", + "default": "./src/react-native/worklets.ts" + }, "./package.json": "./package.json" }, "publishConfig": { @@ -29,6 +38,11 @@ "browser": "./dist/browser/index.js", "react-native": "./dist/react-native/index.js", "default": "./dist/browser/index.js" + }, + "./react-native-worklets": { + "types": "./dist/react-native/worklets.d.ts", + "react-native": "./dist/react-native/worklets.js", + "default": "./dist/react-native/worklets.js" } }, "linkDirectory": false, @@ -53,6 +67,7 @@ "react": "catalog:", "react-dom": "catalog:", "react-native": "0.84.1", + "react-native-worklets": "0.10.2", "tsdown": "catalog:build", "typegpu": "workspace:*", "typegpu-testing-utility": "workspace:^", @@ -63,6 +78,7 @@ "react": "^19.0.0", "react-native": "*", "react-native-webgpu": "*", + "react-native-worklets": "*", "typegpu": "workspace:^" }, "peerDependenciesMeta": { @@ -71,6 +87,9 @@ }, "react-native-webgpu": { "optional": true + }, + "react-native-worklets": { + "optional": true } } } diff --git a/packages/typegpu-react/src/core/root-context.tsx b/packages/typegpu-react/src/core/root-context.tsx index 790638a6f8..2048a9cabd 100644 --- a/packages/typegpu-react/src/core/root-context.tsx +++ b/packages/typegpu-react/src/core/root-context.tsx @@ -9,7 +9,7 @@ import React, { useRef, useState, } from 'react'; -import { tgpu, type TgpuRoot } from 'typegpu'; +import { tgpu, type InitOptions, type TgpuRoot } from 'typegpu'; import { useDeferredCleanup } from './helper-hooks.ts'; import { useBailOnServer } from './use-bail-on-server.ts'; @@ -83,6 +83,11 @@ interface RootContext { class OwnRootContext implements RootContext { #result: RootContextResult | undefined; #destroyed: boolean = false; + readonly #options: InitOptions | undefined; + + constructor(options?: InitOptions) { + this.#options = options; + } initOrGetRoot(): RootContextResult { if (this.#destroyed) { @@ -91,7 +96,7 @@ class OwnRootContext implements RootContext { } if (!this.#result) { - const promise = tgpu.init().then( + const promise = tgpu.init(this.#options).then( (root) => { if (this.#destroyed) { root.destroy(); @@ -157,6 +162,8 @@ const globalRootContextValue = new OwnRootContext(); const rootContext = createContext(null); export interface RootProps { + /** Options used when this provider creates its own root, ignored when `root` is provided */ + options?: InitOptions | undefined; /** * An existing root to provide. If undefined (default), a new root will be initialized for * this provider's children. @@ -183,8 +190,8 @@ function WarnSuspense() { return null; } -export const Root = ({ children, root }: RootProps) => { - const [ownCtx] = useState(() => new OwnRootContext()); +export const Root = ({ children, options, root }: RootProps) => { + const [ownCtx] = useState(() => new OwnRootContext(options)); const existingRootCtx = useMemo(() => { if (root) { return new ExistingRootContext(root); diff --git a/packages/typegpu-react/src/react-native/core/use-frame.ts b/packages/typegpu-react/src/react-native/core/use-frame.ts new file mode 100644 index 0000000000..3c0d48a320 --- /dev/null +++ b/packages/typegpu-react/src/react-native/core/use-frame.ts @@ -0,0 +1,65 @@ +import { useEffect, useRef } from 'react'; +import { runOnUISync, createShareable, UIRuntimeId } from 'react-native-worklets'; + +interface FrameCtx { + readonly deltaSeconds: number; + readonly elapsedSeconds: number; +} + +type FrameCallback = (ctx: FrameCtx) => void; +type FrameCallbackRef = { current: FrameCallback }; +type UiValue = { + value: T; + setSync(value: T | ((prev: T) => T)): void; +}; + +export function useFrame(cb: FrameCallback) { + const latestCb = useRef | undefined>(undefined); + + useEffect(() => { + const cbRef = createShareable( + UIRuntimeId, + { current: cb }, + { initSynchronously: true }, + ) as UiValue; + const frameId = createShareable(UIRuntimeId, undefined) as UiValue; + latestCb.current = cbRef; + + runOnUISync(() => { + 'worklet'; + let startTime: number | undefined; + let lastTime: number | undefined; + + function loop(timestamp?: number) { + frameId.value = requestAnimationFrame(loop); + + const now = timestamp ?? performance.now(); + if (lastTime === undefined || startTime === undefined) { + startTime = now; + lastTime = now; + } + cbRef.value.current({ + deltaSeconds: (now - lastTime) / 1000, + elapsedSeconds: (now - startTime) / 1000, + }); + lastTime = now; + } + + loop(); + }); + + return () => { + latestCb.current = undefined; + runOnUISync(() => { + 'worklet'; + if (frameId.value !== undefined) { + cancelAnimationFrame(frameId.value); + } + }); + }; + }, []); + + useEffect(() => { + latestCb.current?.setSync({ current: cb }); + }, [cb]); +} diff --git a/packages/typegpu-react/src/react-native/serialization/register-serializables.ts b/packages/typegpu-react/src/react-native/serialization/register-serializables.ts new file mode 100644 index 0000000000..fa4d64b8b9 --- /dev/null +++ b/packages/typegpu-react/src/react-native/serialization/register-serializables.ts @@ -0,0 +1,86 @@ +import { installWebGPU } from 'react-native-webgpu'; +import { isWorkletFunction, registerCustomSerializable } from 'react-native-worklets'; +import { + isNonTransferableResource, + isSnapshotableResource, + restoreResource, + snapshotResource, + type TgpuResourceSnapshot, +} from 'typegpu/~internal'; +import { + cacheTransferredResource, + getCachedTransferredResource, + getOrCreateTransferId, + getTransferredRoot, +} from './transfer-cache.ts'; + +export type PackedTgpuResource = { + id: number; + snapshot: TgpuResourceSnapshot; +}; + +let registered = false; + +export function registerTypegpuReactSerializables(): void { + if (registered) { + return; + } + registered = true; + + registerCustomSerializable({ + name: 'TypeGPU', + determine(value: object): value is object { + 'worklet'; + // Non-transferable TypeGPU objects are claimed too, so pack() fails loudly + return isSnapshotableResource(value) || isNonTransferableResource(value); + }, + pack(value: object): PackedTgpuResource { + 'worklet'; + const snapshot = snapshotResource(value); + if (!snapshot) { + const resourceType = (value as { resourceType?: string }).resourceType ?? 'unknown'; + throw new Error( + `[typegpu-react] TypeGPU object '${resourceType}' cannot be transferred to a worklet. ` + + 'Definitions (functions, comptime, derived) are runtime-local: import them from a module ' + + 'covered by importForwarding, or build pipelines on the JS thread and transfer the result.', + ); + } + for (const [key, field] of Object.entries(snapshot)) { + if ( + typeof field === 'function' && + !isWorkletFunction(field) && + !(field as { __bundleData?: unknown }).__bundleData + ) { + throw new Error( + `[typegpu-react] Cannot transfer '${snapshot.type}': its '${key}' is a plain function. ` + + "Only worklets can cross runtimes - mark it with 'worklet'. If it is a schema or " + + 'TypeGPU definition, it cannot be transferred yet.', + ); + } + } + return { id: getOrCreateTransferId(value), snapshot }; + }, + unpack(payload: PackedTgpuResource): object { + 'worklet'; + try { + const cached = getCachedTransferredResource(payload.id); + if (cached) { + return cached; + } + + installWebGPU(); + const resource = restoreResource(payload.snapshot, { + getRoot: getTransferredRoot, + }) as object; + cacheTransferredResource(payload.id, resource); + return resource; + } catch (err) { + const details = err instanceof Error ? (err.stack ?? err.message) : String(err); + throw new Error( + `[typegpu-react] Failed to restore '${payload?.snapshot?.type}' (id ${payload?.id}). Cause: ${details}`, + { cause: err }, + ); + } + }, + }); +} diff --git a/packages/typegpu-react/src/react-native/serialization/transfer-cache.ts b/packages/typegpu-react/src/react-native/serialization/transfer-cache.ts new file mode 100644 index 0000000000..1cb03c8551 --- /dev/null +++ b/packages/typegpu-react/src/react-native/serialization/transfer-cache.ts @@ -0,0 +1,117 @@ +import { tgpu, type TgpuRoot } from 'typegpu'; + +export type TransferredResourceRef = { + deref(): object | undefined; +}; + +type ResourceWeakRefConstructor = new ( + target: T, +) => { + deref(): T | undefined; +}; + +type WeakRefGlobals = { + WeakRef?: ResourceWeakRefConstructor; +}; + +type ResourceFinalizationRegistry = { + register(target: object, heldValue: number): void; +}; + +type ResourceFinalizationRegistryConstructor = new ( + cleanup: (heldValue: number) => void, +) => ResourceFinalizationRegistry; + +type TypegpuReactTransferGlobals = typeof globalThis & { + __TYPEGPU_REACT_NEXT_TRANSFER_ID__?: number; + __TYPEGPU_REACT_TRANSFER_IDS__?: WeakMap; + __TYPEGPU_REACT_TRANSFERRED_RESOURCES__?: Map; + __TYPEGPU_REACT_TRANSFER_CACHE_CLEANUP__?: ResourceFinalizationRegistry; + __TYPEGPU_REACT_STRONG_TRANSFER_CACHE_WARNING_SHOWN__?: boolean; + __TYPEGPU_REACT_ROOTS__?: WeakMap; +}; + +export function getTransferredRoot(device: GPUDevice): TgpuRoot { + 'worklet'; + const global = globalThis as TypegpuReactTransferGlobals; + const roots = (global.__TYPEGPU_REACT_ROOTS__ ??= new WeakMap()); + let root = roots.get(device); + if (!root) { + root = tgpu.initFromDevice({ device }); + roots.set(device, root); + } + return root; +} + +export function getOrCreateTransferId(value: object): number { + 'worklet'; + const global = globalThis as TypegpuReactTransferGlobals; + const ids = (global.__TYPEGPU_REACT_TRANSFER_IDS__ ??= new WeakMap()); + let id = ids.get(value); + if (id === undefined) { + id = global.__TYPEGPU_REACT_NEXT_TRANSFER_ID__ ?? 0; + global.__TYPEGPU_REACT_NEXT_TRANSFER_ID__ = id + 1; + ids.set(value, id); + } + return id; +} + +export function getTransferredResourceCache(): Map { + 'worklet'; + const global = globalThis as TypegpuReactTransferGlobals; + return (global.__TYPEGPU_REACT_TRANSFERRED_RESOURCES__ ??= new Map()); +} + +export function getCachedTransferredResource(id: number): object | undefined { + 'worklet'; + return getTransferredResourceCache().get(id)?.deref(); +} + +function getWeakRef(): ResourceWeakRefConstructor | undefined { + 'worklet'; + return (globalThis as unknown as WeakRefGlobals).WeakRef; +} + +export function createTransferredResourceRef(resource: object): TransferredResourceRef { + 'worklet'; + const WeakRefCtor = getWeakRef(); + if (WeakRefCtor) { + return new WeakRefCtor(resource); + } + + const global = globalThis as TypegpuReactTransferGlobals; + if (!global.__TYPEGPU_REACT_STRONG_TRANSFER_CACHE_WARNING_SHOWN__) { + global.__TYPEGPU_REACT_STRONG_TRANSFER_CACHE_WARNING_SHOWN__ = true; + console.warn( + 'WeakRef is not available in this worklet runtime. TypeGPU transferred resources will use a strong identity cache.', + ); + } + + return { deref: () => resource }; +} + +function getCacheCleanupRegistry(): ResourceFinalizationRegistry | undefined { + 'worklet'; + const FinalizationRegistryCtor = ( + globalThis as { FinalizationRegistry?: ResourceFinalizationRegistryConstructor } + ).FinalizationRegistry; + if (!FinalizationRegistryCtor) { + return undefined; + } + const global = globalThis as TypegpuReactTransferGlobals; + return (global.__TYPEGPU_REACT_TRANSFER_CACHE_CLEANUP__ ??= new FinalizationRegistryCtor((id) => { + const cache = getTransferredResourceCache(); + // The id may have been repopulated with a live resource in the meantime + if (cache.get(id)?.deref() === undefined) { + cache.delete(id); + } + })); +} + +// The worklets babel plugin turns workletized declarations into `const`s initialized in source +// order, so functions captured by worklets here must be declared above their dependents +export function cacheTransferredResource(id: number, resource: object): void { + 'worklet'; + getTransferredResourceCache().set(id, createTransferredResourceRef(resource)); + getCacheCleanupRegistry()?.register(resource, id); +} diff --git a/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts b/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts new file mode 100644 index 0000000000..f2091ed868 --- /dev/null +++ b/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts @@ -0,0 +1,37 @@ +import { useEffect, useRef } from 'react'; +import { createShareable, UIRuntimeId } from 'react-native-worklets'; + +import type { CanvasRef, UseConfigureContextOptions } from '../core/use-configure-context.ts'; +import { useConfigureContext } from './use-configure-context.ts'; + +type CanvasContext = GPUCanvasContext & { present?: () => void }; + +export type WorkletCanvasContextRef = { + value: CanvasContext | null; + setSync(value: CanvasContext | null): void; +}; + +export function useConfigureWorkletContext(options?: UseConfigureContextOptions): { + ref: React.RefCallback; + ctxRef: WorkletCanvasContextRef; +} { + const result = useConfigureContext(options); + const workletCtxRef = useRef(undefined); + workletCtxRef.current ??= createShareable(UIRuntimeId, null, { + initSynchronously: true, + }) as WorkletCanvasContextRef; + + const ctxRef = workletCtxRef.current; + + useEffect(() => { + ctxRef.setSync(result.ctxRef.current); + }); + + useEffect(() => { + return () => { + ctxRef.setSync(null); + }; + }, [ctxRef]); + + return { ref: result.ref, ctxRef }; +} diff --git a/packages/typegpu-react/src/react-native/worklets.ts b/packages/typegpu-react/src/react-native/worklets.ts new file mode 100644 index 0000000000..505a357515 --- /dev/null +++ b/packages/typegpu-react/src/react-native/worklets.ts @@ -0,0 +1,14 @@ +import { WebGPUModule } from 'react-native-webgpu'; + +import { registerTypegpuReactSerializables } from './serialization/register-serializables.ts'; + +WebGPUModule.install(); +registerTypegpuReactSerializables(); + +export * from '../shared-exports.ts'; +// Intentionally shadows the browser `useFrame`, this one runs the frame loop on the UI runtime +export { useFrame } from './core/use-frame.ts'; +export { + useConfigureWorkletContext as useConfigureContext, + type WorkletCanvasContextRef, +} from './use-configure-worklet-context.ts'; diff --git a/packages/typegpu-react/tests/react-native/register-serializables.test.ts b/packages/typegpu-react/tests/react-native/register-serializables.test.ts new file mode 100644 index 0000000000..c82deffdbf --- /dev/null +++ b/packages/typegpu-react/tests/react-native/register-serializables.test.ts @@ -0,0 +1,84 @@ +import { registerCustomSerializable } from 'react-native-worklets'; +import { it } from 'typegpu-testing-utility'; +import { tgpu, d } from 'typegpu'; +import { describe, expect, vi } from 'vitest'; +import { registerTypegpuReactSerializables } from '../../src/react-native/serialization/register-serializables.ts'; + +vi.mock('react-native-webgpu', () => ({ installWebGPU: vi.fn() })); +vi.mock('react-native-worklets', () => ({ + registerCustomSerializable: vi.fn(), + isWorkletFunction: (value: unknown) => typeof value === 'function' && '__workletHash' in value, +})); + +type Serializer = { + determine(value: object): boolean; + pack(value: object): object; + unpack(value: object): object; +}; + +function getSerializer(): Serializer { + registerTypegpuReactSerializables(); + + const serializer = vi.mocked(registerCustomSerializable).mock.calls[0]?.[0] as + | Serializer + | undefined; + if (!serializer) { + throw new Error('TypeGPU serializer was not registered.'); + } + return serializer; +} + +describe('react-native serializable registration', () => { + it('round-trips buffers end to end', ({ root }) => { + const serializer = getSerializer(); + const buffer = root.createBuffer(d.arrayOf(d.u32, 3)).$usage('storage'); + const rawBuffer = root.unwrap(buffer); + + expect(serializer.determine(buffer)).toBe(true); + + const restored = serializer.unpack(serializer.pack(buffer)) as typeof buffer; + expect(restored.usableAsStorage).toBe(true); + expect(restored.root.device).toBe(root.device); + expect(restored.root.unwrap(restored)).toBe(rawBuffer); + }); + + it('round-trips roots by device identity', ({ root }) => { + const serializer = getSerializer(); + + expect(serializer.determine(root)).toBe(true); + + const restored = serializer.unpack(serializer.pack(root)) as typeof root; + expect(restored.resourceType).toBe('root'); + expect(restored.device).toBe(root.device); + expect(serializer.unpack(serializer.pack(root))).toBe(restored); + }); + + it('fails loudly for non-transferable TypeGPU objects', ({ root }) => { + const serializer = getSerializer(); + const view = root + .createTexture({ size: [2, 2], format: 'rgba8unorm' }) + .$usage('sampled') + .createView(); + + expect(serializer.determine(view)).toBe(true); + expect(() => serializer.pack(view)).toThrowErrorMatchingInlineSnapshot( + `[Error: [typegpu-react] TypeGPU object 'texture-view' cannot be transferred to a worklet. Definitions (functions, comptime, derived) are runtime-local: import them from a module covered by importForwarding, or build pipelines on the JS thread and transfer the result.]`, + ); + }); + + it('rejects plain-function performance callbacks', ({ root }) => { + const serializer = getSerializer(); + const pipeline = root + .createComputePipeline({ + compute: tgpu.computeFn({ workgroupSize: [1] })(() => { + 'use gpu'; + }), + }) + .withTimestampWrites({ querySet: root.createQuerySet('timestamp', 2) }) + .withPerformanceCallback(vi.fn()); + + expect(() => serializer.pack(pipeline)).toThrowErrorMatchingInlineSnapshot( + `[Error: [typegpu-react] Cannot transfer 'compute-pipeline': its 'performanceCallback' is a plain function. Only worklets can cross runtimes - mark it with 'worklet'. If it is a schema or TypeGPU definition, it cannot be transferred yet.]`, + ); + }); +}); diff --git a/packages/typegpu-react/tests/root-context.test.tsx b/packages/typegpu-react/tests/root-context.test.tsx index d5db80a6d4..4c8a2ce682 100644 --- a/packages/typegpu-react/tests/root-context.test.tsx +++ b/packages/typegpu-react/tests/root-context.test.tsx @@ -54,6 +54,35 @@ describe('Root unmount cleanup', () => { expect(() => unmount()).not.toThrow(); }); + it('should pass options to owned root init', async ({ adapter }) => { + function TestConsumer() { + useRootWithStatus(); + return null; + } + + render( + + + , + ); + + await act(async () => { + await Promise.resolve(); + }); + + expect(adapter.requestDevice.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "requiredFeatures": [ + "timestamp-query", + ], + }, + ], + ] + `); + }); + it('should destroy root when init promise resolves after unmount', async ({ stallDeviceRequest, }) => { diff --git a/packages/typegpu-react/tsdown.config.ts b/packages/typegpu-react/tsdown.config.ts index 24110e68b5..f5d7887b6c 100644 --- a/packages/typegpu-react/tsdown.config.ts +++ b/packages/typegpu-react/tsdown.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsdown'; export default defineConfig({ - entry: ['src/browser/index.ts', 'src/react-native/index.ts'], + entry: ['src/browser/index.ts', 'src/react-native/index.ts', 'src/react-native/worklets.ts'], outDir: 'dist', format: 'esm', dts: true, diff --git a/packages/typegpu/src/core/buffer/buffer.ts b/packages/typegpu/src/core/buffer/buffer.ts index cab7ab00af..9fd7ae5a1e 100644 --- a/packages/typegpu/src/core/buffer/buffer.ts +++ b/packages/typegpu/src/core/buffer/buffer.ts @@ -21,9 +21,15 @@ import type { import { $internal } from '../../shared/symbols.ts'; import type { Prettify, UnionToIntersection } from '../../shared/utilityTypes.ts'; import { isGPUBuffer } from '../../types.ts'; -import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; +import type { ExperimentalTgpuRoot, TgpuRoot } from '../root/rootTypes.ts'; import { calculateOffsets, readFromArrayBuffer, writeToArrayBuffer } from '../../data/dataIO.ts'; import { patchArrayBuffer } from '../../data/partialIO.ts'; +import { + deserializeDataSchema, + serializeDataSchema, + type SerializedDataSchema, +} from '../../serial/schema.ts'; +import type { RestoreContext } from '../../serial/types.ts'; import { mutable, readonly, @@ -63,7 +69,7 @@ export interface IndirectFlag { */ export type Vertex = VertexFlag; -type UsageLiteral = 'uniform' | 'storage' | 'vertex' | 'index' | 'indirect'; +export type UsageLiteral = 'uniform' | 'storage' | 'vertex' | 'index' | 'indirect'; type LiteralToUsageType = T extends 'uniform' ? UniformFlag @@ -117,6 +123,7 @@ export type BufferInitialData = export interface TgpuBuffer extends TgpuNamable { readonly [$internal]: true; readonly resourceType: 'buffer'; + readonly root: TgpuRoot; readonly dataType: TData; readonly initial?: InferInput | undefined; readonly arrayBuffer: ArrayBuffer; @@ -167,12 +174,70 @@ export function INTERNAL_createBuffer( return new TgpuBufferImpl(group, typeSchema, initialOrBuffer); } +export interface TgpuBufferSnapshot { + readonly type: 'buffer'; + readonly device: GPUDevice; + readonly buffer: GPUBuffer; + readonly schema: SerializedDataSchema; + readonly usages: UsageLiteral[]; +} + +function getBufferUsages(buffer: TgpuBuffer): UsageLiteral[] { + const usages: UsageLiteral[] = []; + if (buffer.usableAsUniform) { + usages.push('uniform'); + } + if (buffer.usableAsStorage) { + usages.push('storage'); + } + if (buffer.usableAsVertex) { + usages.push('vertex'); + } + if (buffer.usableAsIndex) { + usages.push('index'); + } + if (buffer.usableAsIndirect) { + usages.push('indirect'); + } + return usages; +} + +export function INTERNAL_snapshotBuffer(buffer: TgpuBuffer): TgpuBufferSnapshot { + return { + type: 'buffer', + device: buffer.root.device, + buffer: buffer.buffer, + schema: serializeDataSchema(buffer.dataType), + usages: getBufferUsages(buffer), + }; +} + +export function INTERNAL_applyBufferUsages( + buffer: TgpuBuffer, + usages: UsageLiteral[], +): void { + if (usages.length > 0) { + (buffer as TgpuBufferImpl).$usage(...usages); + } +} + +export function INTERNAL_restoreBuffer( + snapshot: TgpuBufferSnapshot, + ctx: RestoreContext, +): TgpuBuffer { + const root = ctx.getRoot(snapshot.device); + const buffer = root.createBuffer(deserializeDataSchema(snapshot.schema), snapshot.buffer); + INTERNAL_applyBufferUsages(buffer, snapshot.usages); + return buffer; +} + // -------------- // Implementation // -------------- class TgpuBufferImpl implements TgpuBuffer { readonly [$internal] = true; readonly resourceType = 'buffer'; + readonly root: ExperimentalTgpuRoot; flags: GPUBufferUsageFlags = GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC; readonly dataType: TData; @@ -203,6 +268,7 @@ class TgpuBufferImpl implements TgpuBuffer { initialOrBuffer?: BufferInitialData | GPUBuffer, disallowedUsages?: UsageLiteral[], ) { + this.root = root; this.dataType = dataType; this.#disallowedUsages = disallowedUsages; this.#device = root.device; diff --git a/packages/typegpu/src/core/constant/tgpuConstant.ts b/packages/typegpu/src/core/constant/tgpuConstant.ts index 12b13a3f55..1775e123be 100644 --- a/packages/typegpu/src/core/constant/tgpuConstant.ts +++ b/packages/typegpu/src/core/constant/tgpuConstant.ts @@ -7,6 +7,11 @@ import { getName, setName } from '../../shared/meta.ts'; import type { InferGPU } from '../../shared/repr.ts'; import { $gpuValueOf, $internal, $ownSnippet, $resolve } from '../../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; +import { + deserializeDataSchema, + serializeDataSchema, + type SerializedDataSchema, +} from '../../serial/schema.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; // ---------- @@ -61,6 +66,32 @@ export function constant( return new TgpuConstImpl(dataType, value); } +export function isConst(value: unknown): value is TgpuConst { + return ( + (value as TgpuConst | undefined)?.resourceType === 'const' && + !!(value as { [$internal]?: unknown } | undefined)?.[$internal] + ); +} + +export interface TgpuConstSnapshot { + readonly type: 'const'; + readonly schema: SerializedDataSchema; + readonly value: unknown; +} + +export function INTERNAL_snapshotConst(value: TgpuConst): TgpuConstSnapshot { + const impl = value as TgpuConstImpl; + return { + type: 'const', + schema: serializeDataSchema(impl.dataType), + value: impl.$, + }; +} + +export function INTERNAL_restoreConst(snapshot: TgpuConstSnapshot): TgpuConst { + return constant(deserializeDataSchema(snapshot.schema), snapshot.value); +} + // -------------- // Implementation // -------------- diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index 36e80ddca7..ea17b51cce 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -27,7 +27,12 @@ import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; import type { TgpuSlot } from '../slot/slotTypes.ts'; import type { PrimitiveOffsetInfo } from '../../data/offsetUtils.ts'; -import { resolveIndirectOffset } from './pipelineUtils.ts'; +import { + collectBindGroupPairs, + resolveIndirectOffset, + restoreTimestampPriors, +} from './pipelineUtils.ts'; +import type { RestoreContext } from '../../serial/types.ts'; import { createWithPerformanceCallback, createWithTimestampWrites, @@ -44,6 +49,7 @@ import { } from './performanceTracker.ts'; interface ComputePipelineInternals { + readonly core: ComputePipelineCore; readonly rawPipeline: GPUComputePipeline; readonly priors: TgpuComputePipelinePriors & TimestampWritesPriors; readonly root: ExperimentalTgpuRoot; @@ -113,12 +119,60 @@ export function INTERNAL_createComputePipeline( return new TgpuComputePipelineImpl(new ComputePipelineCore(branch, slotBindings, descriptor), {}); } +export interface TgpuComputePipelineSnapshot { + readonly type: 'compute-pipeline'; + readonly device: GPUDevice; + readonly pipeline: GPUComputePipeline; + readonly usedBindGroupLayouts: TgpuBindGroupLayout[]; + readonly bindGroups: [TgpuBindGroupLayout, TgpuBindGroup | GPUBindGroup][]; + readonly timestampWrites: TimestampWritesPriors['timestampWrites']; + readonly performanceCallback: TimestampWritesPriors['performanceCallback']; +} + +export function INTERNAL_snapshotComputePipeline( + pipeline: TgpuComputePipeline, +): TgpuComputePipelineSnapshot { + const internals = pipeline[$internal]; + const memo = internals.core.unwrap(); + + return { + type: 'compute-pipeline', + device: internals.root.device, + pipeline: memo.pipeline, + usedBindGroupLayouts: memo.usedBindGroupLayouts, + bindGroups: collectBindGroupPairs( + memo.usedBindGroupLayouts, + memo.catchall, + internals.priors.bindGroupLayoutMap, + ), + timestampWrites: internals.priors.timestampWrites, + performanceCallback: internals.priors.performanceCallback, + }; +} + +export function INTERNAL_restoreComputePipeline( + snapshot: TgpuComputePipelineSnapshot, + ctx: RestoreContext, +): TgpuComputePipeline { + const root = ctx.getRoot(snapshot.device) as ExperimentalTgpuRoot; + const core = ComputePipelineCore.precompiled(root, { + pipeline: snapshot.pipeline, + usedBindGroupLayouts: snapshot.usedBindGroupLayouts, + catchall: undefined, + logResources: undefined, + }); + const pipeline: TgpuComputePipeline = new TgpuComputePipelineImpl(core, { + bindGroupLayoutMap: new Map(snapshot.bindGroups), + }); + return restoreTimestampPriors(pipeline, snapshot); +} + // -------------- // Implementation // -------------- type TgpuComputePipelinePriors = { - readonly bindGroupLayoutMap?: Map; + readonly bindGroupLayoutMap?: Map | undefined; readonly externalEncoder?: GPUCommandEncoder | undefined; readonly externalPass?: GPUComputePassEncoder | undefined; } & TimestampWritesPriors; @@ -145,6 +199,7 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { this.#priors = priors; this[$internal] = { + core, get rawPipeline() { return core.unwrap().pipeline; }, @@ -349,13 +404,13 @@ class ComputePipelineCore implements SelfResolvable { #memo: Memo | undefined; #slotBindings: [TgpuSlot, unknown][]; - #descriptor: TgpuComputePipeline.Descriptor; + #descriptor: TgpuComputePipeline.Descriptor | undefined; #performanceCallbackQuerySet: TgpuQuerySet<'timestamp'> | undefined; constructor( root: ExperimentalTgpuRoot, slotBindings: [TgpuSlot, unknown][], - descriptor: TgpuComputePipeline.Descriptor, + descriptor: TgpuComputePipeline.Descriptor | undefined, ) { this.root = root; this.#slotBindings = slotBindings; @@ -365,9 +420,20 @@ class ComputePipelineCore implements SelfResolvable { : new NullPerformanceTracker(); } + static precompiled(root: ExperimentalTgpuRoot, memo: Memo): ComputePipelineCore { + const core = new ComputePipelineCore(root, [], undefined); + core.#memo = memo; + return core; + } + [$resolve](ctx: ResolutionCtx) { + const descriptor = this.#descriptor; + if (!descriptor) { + // Precompiled pipelines have nothing to contribute to the shader + return snip('', Void, /* origin */ 'runtime'); + } return ctx.withSlots(this.#slotBindings, () => { - ctx.resolve(this.#descriptor.compute); + ctx.resolve(descriptor.compute); return snip('', Void, /* origin */ 'runtime'); }); } diff --git a/packages/typegpu/src/core/pipeline/pipelineUtils.ts b/packages/typegpu/src/core/pipeline/pipelineUtils.ts index 80027b7386..dcc1bbe21b 100644 --- a/packages/typegpu/src/core/pipeline/pipelineUtils.ts +++ b/packages/typegpu/src/core/pipeline/pipelineUtils.ts @@ -2,7 +2,48 @@ import type { IndirectFlag, TgpuBuffer } from '../buffer/buffer.ts'; import { memoryLayoutOf, type PrimitiveOffsetInfo } from '../../data/offsetUtils.ts'; import { sizeOf } from '../../data/sizeOf.ts'; import type { BaseData } from '../../data/wgslTypes.ts'; +import type { TgpuBindGroup, TgpuBindGroupLayout } from '../../tgpuBindGroupLayout.ts'; +import type { TgpuVertexLayout } from '../vertexLayout/vertexLayout.ts'; import { isGPUBuffer } from '../../types.ts'; +import type { Timeable, TimestampWritesPriors } from './timeable.ts'; + +export function collectBindGroupPairs( + layouts: TgpuBindGroupLayout[], + catchall: [number, TgpuBindGroup] | undefined, + map: Map | undefined, +): [TgpuBindGroupLayout, TgpuBindGroup | GPUBindGroup][] { + return layouts.flatMap((layout, idx): [TgpuBindGroupLayout, TgpuBindGroup | GPUBindGroup][] => { + const resource = catchall && idx === catchall[0] ? catchall[1] : map?.get(layout); + return resource ? [[layout, resource]] : []; + }); +} + +export function collectVertexBufferPairs( + layouts: TgpuVertexLayout[], + map: Map | undefined, +): [TgpuVertexLayout, Buffer][] { + return layouts.flatMap((layout): [TgpuVertexLayout, Buffer][] => { + const resource = map?.get(layout); + return resource ? [[layout, resource]] : []; + }); +} + +export function restoreTimestampPriors( + pipeline: T, + priors: { + readonly timestampWrites: TimestampWritesPriors['timestampWrites'] | undefined; + readonly performanceCallback: TimestampWritesPriors['performanceCallback'] | undefined; + }, +): T { + let result = pipeline; + if (priors.timestampWrites) { + result = result.withTimestampWrites(priors.timestampWrites); + } + if (priors.performanceCallback) { + result = result.withPerformanceCallback(priors.performanceCallback); + } + return result; +} type IndirectOperation = 'dispatchWorkgroupsIndirect' | 'drawIndirect' | 'drawIndexedIndirect'; const IndirectOperationToRequiredData = { diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 64d53cdc4a..4b055690fc 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -82,12 +82,23 @@ import { triggerPerformanceCallback, } from './timeable.ts'; import { type PrimitiveOffsetInfo } from '../../data/offsetUtils.ts'; -import { resolveIndirectOffset } from './pipelineUtils.ts'; +import { + collectBindGroupPairs, + collectVertexBufferPairs, + resolveIndirectOffset, + restoreTimestampPriors, +} from './pipelineUtils.ts'; import { NullPerformanceTracker, PerformanceTrackerImpl, type PerformanceTracker, } from './performanceTracker.ts'; +import { + deserializeDataSchema, + serializeDataSchema, + type SerializedDataSchema, +} from '../../serial/schema.ts'; +import type { RestoreContext } from '../../serial/types.ts'; const DRAW_INDIRECT_SIZE = 16; // 4 x 4 const DRAW_INDEXED_INDIRECT_SIZE = 20; // 5 x 4 @@ -442,13 +453,77 @@ export type AnyFragmentColorAttachment = ColorAttachment | Record, unknown][]; - descriptor: TgpuRenderPipeline.Descriptor; + /** Undefined for precompiled pipelines, which are never resolved again */ + descriptor: TgpuRenderPipeline.Descriptor | undefined; }; export function INTERNAL_createRenderPipeline(options: RenderPipelineCoreOptions) { return new TgpuRenderPipelineImpl(new RenderPipelineCore(options), {}); } +export interface TgpuRenderPipelineSnapshot { + readonly type: 'render-pipeline'; + readonly device: GPUDevice; + readonly pipeline: GPURenderPipeline; + readonly fragmentOut: SerializedDataSchema | undefined; + readonly usedBindGroupLayouts: TgpuBindGroupLayout[]; + readonly bindGroups: [TgpuBindGroupLayout, TgpuBindGroup | GPUBindGroup][]; + readonly usedVertexLayouts: TgpuVertexLayout[]; + readonly vertexBuffers: [TgpuVertexLayout, (TgpuBuffer & VertexFlag) | GPUBuffer][]; + readonly timestampWrites: TimestampWritesPriors['timestampWrites']; + readonly performanceCallback: TimestampWritesPriors['performanceCallback']; +} + +export function INTERNAL_snapshotRenderPipeline( + pipeline: TgpuRenderPipeline, +): TgpuRenderPipelineSnapshot { + const internals = pipeline[$internal]; + const memo = internals.core.unwrap(); + const fragmentOut = + (internals.core.options.descriptor?.fragment as TgpuFragmentFn | undefined)?.shell + ?.returnType ?? memo.fragmentOut; + + return { + type: 'render-pipeline', + device: internals.root.device, + pipeline: memo.pipeline, + fragmentOut: fragmentOut ? serializeDataSchema(fragmentOut) : undefined, + usedBindGroupLayouts: memo.usedBindGroupLayouts, + bindGroups: collectBindGroupPairs( + memo.usedBindGroupLayouts, + memo.catchall, + internals.priors.bindGroupLayoutMap, + ), + usedVertexLayouts: memo.usedVertexLayouts, + vertexBuffers: collectVertexBufferPairs( + memo.usedVertexLayouts, + internals.priors.vertexLayoutMap, + ), + timestampWrites: internals.priors.timestampWrites, + performanceCallback: internals.priors.performanceCallback, + }; +} + +export function INTERNAL_restoreRenderPipeline( + snapshot: TgpuRenderPipelineSnapshot, + ctx: RestoreContext, +): TgpuRenderPipeline { + const root = ctx.getRoot(snapshot.device) as ExperimentalTgpuRoot; + const core = RenderPipelineCore.precompiled(root, { + pipeline: snapshot.pipeline, + usedBindGroupLayouts: snapshot.usedBindGroupLayouts, + catchall: undefined, + logResources: undefined, + usedVertexLayouts: snapshot.usedVertexLayouts, + fragmentOut: snapshot.fragmentOut ? deserializeDataSchema(snapshot.fragmentOut) : undefined, + }); + const pipeline: TgpuRenderPipeline = new TgpuRenderPipelineImpl(core, { + bindGroupLayoutMap: new Map(snapshot.bindGroups), + vertexLayoutMap: new Map(snapshot.vertexBuffers), + }); + return restoreTimestampPriors(pipeline, snapshot); +} + // -------------- // Implementation // -------------- @@ -479,7 +554,7 @@ type Memo = { catchall: [number, TgpuBindGroup] | undefined; logResources: LogResources | undefined; usedVertexLayouts: TgpuVertexLayout[]; - fragmentOut: BaseData; + fragmentOut: BaseData | undefined; }; const _lastAppliedRender = new WeakMap< @@ -723,35 +798,36 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { const { root, descriptor } = internals.core.options; const memo = internals.core.unwrap(); - const colorAttachments = descriptor.fragment - ? (connectAttachmentToShader( - (descriptor.fragment as TgpuFragmentFn)?.shell?.returnType ?? memo.fragmentOut, - internals.priors.colorAttachment ?? {}, - ).map((_attachment) => { - const attachment = { - loadOp: 'clear', - storeOp: 'store', - ..._attachment, - }; - - if (isTexture(attachment.view)) { - attachment.view = root.unwrap(attachment.view).createView(); - } else if (isTextureView(attachment.view)) { - attachment.view = root.unwrap(attachment.view); - } else if (isGPUCanvasContext(attachment.view)) { - attachment.view = attachment.view.getCurrentTexture().createView(); - } - - if (isTexture(attachment.resolveTarget)) { - attachment.resolveTarget = root.unwrap(attachment.resolveTarget).createView(); - } else if (isTextureView(attachment.resolveTarget)) { - attachment.resolveTarget = root.unwrap(attachment.resolveTarget); - } else if (isGPUCanvasContext(attachment.resolveTarget)) { - attachment.resolveTarget = attachment.resolveTarget.getCurrentTexture().createView(); - } - - return attachment; - }) as GPURenderPassColorAttachment[]) + const fragmentOut = + (descriptor?.fragment as TgpuFragmentFn | undefined)?.shell?.returnType ?? memo.fragmentOut; + const colorAttachments = fragmentOut + ? (connectAttachmentToShader(fragmentOut, internals.priors.colorAttachment ?? {}).map( + (_attachment) => { + const attachment = { + loadOp: 'clear', + storeOp: 'store', + ..._attachment, + }; + + if (isTexture(attachment.view)) { + attachment.view = root.unwrap(attachment.view).createView(); + } else if (isTextureView(attachment.view)) { + attachment.view = root.unwrap(attachment.view); + } else if (isGPUCanvasContext(attachment.view)) { + attachment.view = attachment.view.getCurrentTexture().createView(); + } + + if (isTexture(attachment.resolveTarget)) { + attachment.resolveTarget = root.unwrap(attachment.resolveTarget).createView(); + } else if (isTextureView(attachment.resolveTarget)) { + attachment.resolveTarget = root.unwrap(attachment.resolveTarget); + } else if (isGPUCanvasContext(attachment.resolveTarget)) { + attachment.resolveTarget = attachment.resolveTarget.getCurrentTexture().createView(); + } + + return attachment; + }, + ) as GPURenderPassColorAttachment[]) : []; const renderPassDescriptor: GPURenderPassDescriptor = { @@ -1038,9 +1114,22 @@ class RenderPipelineCore implements SelfResolvable { : new NullPerformanceTracker(); } + static precompiled(root: ExperimentalTgpuRoot, memo: Memo): RenderPipelineCore { + const core = new RenderPipelineCore({ + root, + slotBindings: [], + descriptor: undefined, + }); + core.#memo = memo; + return core; + } + [$resolve](ctx: ResolutionCtx): ResolvedSnippet { const { slotBindings } = this.options; - const { vertex, fragment, attribs = {} } = this.options.descriptor; + const { vertex, fragment, attribs = {} } = this.options.descriptor ?? {}; + if (!vertex) { + return snip('', Void, /* origin */ 'runtime'); + } this.#latestAutoVertexIn = undefined; this.#latestAutoFragmentOut = undefined; @@ -1161,6 +1250,9 @@ class RenderPipelineCore implements SelfResolvable { public resolveAndCreateShaderModule() { const { root, descriptor: tgpuDescriptor } = this.options; + if (!tgpuDescriptor) { + throw new Error('Precompiled pipelines are never resolved again.'); + } const device = root.device; const enableExtensions = wgslEnableExtensions.filter((extension) => root.enabledFeatures.has(wgslEnableExtensionToFeatureName[extension]), @@ -1190,7 +1282,7 @@ class RenderPipelineCore implements SelfResolvable { code, }); - const { vertex, fragment, attribs = {}, targets } = this.options.descriptor; + const { vertex, fragment, attribs = {}, targets } = tgpuDescriptor; const connectedAttribs = connectAttributesToShader( (vertex as TgpuVertexFn)?.shell?.in ?? this.#latestAutoVertexIn ?? {}, attribs, diff --git a/packages/typegpu/src/core/pipeline/typeGuards.ts b/packages/typegpu/src/core/pipeline/typeGuards.ts index 903f7f2c35..b93ee49f98 100644 --- a/packages/typegpu/src/core/pipeline/typeGuards.ts +++ b/packages/typegpu/src/core/pipeline/typeGuards.ts @@ -20,8 +20,8 @@ export function isGPUCommandEncoder(value: unknown): value is GPUCommandEncoder return ( !!value && typeof value === 'object' && - 'beginRenderPass' in value && - 'beginComputePass' in value + typeof (value as GPUCommandEncoder).beginRenderPass === 'function' && + typeof (value as GPUCommandEncoder).beginComputePass === 'function' ); } @@ -29,23 +29,28 @@ export function isGPUComputePassEncoder(value: unknown): value is GPUComputePass return ( !!value && typeof value === 'object' && - 'dispatchWorkgroups' in value && - !('beginRenderPass' in value) + typeof (value as GPUComputePassEncoder).dispatchWorkgroups === 'function' && + typeof (value as GPUCommandEncoder).beginRenderPass !== 'function' ); } export function isGPURenderPassEncoder(value: unknown): value is GPURenderPassEncoder { - return !!value && typeof value === 'object' && 'executeBundles' in value && 'draw' in value; + return ( + !!value && + typeof value === 'object' && + typeof (value as GPURenderPassEncoder).executeBundles === 'function' && + typeof (value as GPURenderPassEncoder).draw === 'function' + ); } export function isGPURenderBundleEncoder(value: unknown): value is GPURenderBundleEncoder { return ( !!value && typeof value === 'object' && - 'draw' in value && - 'finish' in value && - !('executeBundles' in value) && - !('beginRenderPass' in value) && - !('dispatchWorkgroups' in value) + typeof (value as GPURenderBundleEncoder).draw === 'function' && + typeof (value as GPURenderBundleEncoder).finish === 'function' && + typeof (value as GPURenderPassEncoder).executeBundles !== 'function' && + typeof (value as GPUCommandEncoder).beginRenderPass !== 'function' && + typeof (value as GPUComputePassEncoder).dispatchWorkgroups !== 'function' ); } diff --git a/packages/typegpu/src/core/querySet/querySet.ts b/packages/typegpu/src/core/querySet/querySet.ts index d895c70d57..60a0b70a61 100644 --- a/packages/typegpu/src/core/querySet/querySet.ts +++ b/packages/typegpu/src/core/querySet/querySet.ts @@ -1,9 +1,11 @@ import { setName, type TgpuNamable } from '../../shared/meta.ts'; -import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; +import type { ExperimentalTgpuRoot, TgpuRoot } from '../root/rootTypes.ts'; +import type { RestoreContext } from '../../serial/types.ts'; import { $internal } from '../../shared/symbols.ts'; export interface TgpuQuerySet extends TgpuNamable { readonly resourceType: 'query-set'; + readonly root: TgpuRoot; readonly type: T; readonly count: number; @@ -35,13 +37,42 @@ export function isQuerySet(value: unknown): value is Tgp return maybe?.resourceType === 'query-set' && !!maybe[$internal]; } +export interface TgpuQuerySetSnapshot { + readonly type: 'query-set'; + readonly device: GPUDevice; + readonly querySet: GPUQuerySet; + readonly queryType: GPUQueryType; + readonly count: number; +} + +export function INTERNAL_snapshotQuerySet( + querySet: TgpuQuerySet, +): TgpuQuerySetSnapshot { + return { + type: 'query-set', + device: querySet.root.device, + querySet: querySet.querySet, + queryType: querySet.type, + count: querySet.count, + }; +} + +export function INTERNAL_restoreQuerySet( + snapshot: TgpuQuerySetSnapshot, + ctx: RestoreContext, +): TgpuQuerySet { + return ctx + .getRoot(snapshot.device) + .createQuerySet(snapshot.queryType, snapshot.count, snapshot.querySet); +} + class TgpuQuerySetImpl implements TgpuQuerySet { readonly resourceType = 'query-set' as const; + readonly root: TgpuRoot; readonly type: T; readonly count: number; readonly #rawQuerySet: GPUQuerySet | undefined; - readonly #device: GPUDevice; #querySet: GPUQuerySet | undefined; readonly #ownQuerySet: boolean; #destroyed = false; @@ -50,7 +81,7 @@ class TgpuQuerySetImpl implements TgpuQuerySet { #resolveBuffer: GPUBuffer | undefined = undefined; constructor(root: ExperimentalTgpuRoot, type: T, count: number, rawQuerySet?: GPUQuerySet) { - this.#device = root.device; + this.root = root; this.type = type; this.count = count; this.#rawQuerySet = rawQuerySet; @@ -69,7 +100,7 @@ class TgpuQuerySetImpl implements TgpuQuerySet { return this.#querySet; } - this.#querySet = this.#device.createQuerySet({ + this.#querySet = this.root.device.createQuerySet({ type: this.type, count: this.count, }); @@ -90,7 +121,7 @@ class TgpuQuerySetImpl implements TgpuQuerySet { return { get readBuffer(): GPUBuffer { if (!self.#readBuffer) { - self.#readBuffer = self.#device.createBuffer({ + self.#readBuffer = self.root.device.createBuffer({ size: self.count * BigUint64Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -99,7 +130,7 @@ class TgpuQuerySetImpl implements TgpuQuerySet { }, get resolveBuffer(): GPUBuffer { if (!self.#resolveBuffer) { - self.#resolveBuffer = self.#device.createBuffer({ + self.#resolveBuffer = self.root.device.createBuffer({ size: self.count * BigUint64Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC, }); @@ -125,9 +156,9 @@ class TgpuQuerySetImpl implements TgpuQuerySet { throw new Error('This QuerySet is busy resolving or reading.'); } - const commandEncoder = this.#device.createCommandEncoder(); + const commandEncoder = this.root.device.createCommandEncoder(); commandEncoder.resolveQuerySet(this.querySet, 0, this.count, this[$internal].resolveBuffer, 0); - this.#device.queue.submit([commandEncoder.finish()]); + this.root.device.queue.submit([commandEncoder.finish()]); } async read(): Promise { @@ -137,7 +168,7 @@ class TgpuQuerySetImpl implements TgpuQuerySet { this.#available = false; try { - const commandEncoder = this.#device.createCommandEncoder(); + const commandEncoder = this.root.device.createCommandEncoder(); commandEncoder.copyBufferToBuffer( this[$internal].resolveBuffer, 0, @@ -145,7 +176,7 @@ class TgpuQuerySetImpl implements TgpuQuerySet { 0, this.count * BigUint64Array.BYTES_PER_ELEMENT, ); - this.#device.queue.submit([commandEncoder.finish()]); + this.root.device.queue.submit([commandEncoder.finish()]); const readBuffer = this[$internal].readBuffer; await readBuffer.mapAsync(GPUMapMode.READ); diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index e59e871422..ce8b2d80fa 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -79,6 +79,7 @@ import { u32 } from '../../data/numeric.ts'; import { ceil } from '../../std/numeric.ts'; import { allEq } from '../../std/boolean.ts'; import { getName, setName } from '../../shared/meta.ts'; +import type { RestoreContext } from '../../serial/types.ts'; /** * Changes the given array to a vec of 3 numbers, filling missing values with 1. @@ -100,7 +101,9 @@ const workgroupSizeConfigs = [ export class TgpuGuardedComputePipelineImpl< TArgs extends number[], > implements TgpuGuardedComputePipeline { - #root: ExperimentalTgpuRoot; + readonly resourceType = 'guarded-compute-pipeline' as const; + + #root: TgpuRoot; #pipeline: TgpuComputePipeline; #sizeUniform: TgpuUniform; #workgroupSize: v3u; @@ -108,7 +111,7 @@ export class TgpuGuardedComputePipelineImpl< #lastSize: v3u; constructor( - root: ExperimentalTgpuRoot, + root: TgpuRoot, pipeline: TgpuComputePipeline, sizeUniform: TgpuUniform, workgroupSize: v3u, @@ -183,6 +186,14 @@ export class TgpuGuardedComputePipelineImpl< return this.#sizeUniform; } + get root() { + return this.#root; + } + + get workgroupSize() { + return this.#workgroupSize; + } + [$internal] = true; get [$getNameForward]() { return this.#pipeline; @@ -193,6 +204,66 @@ export class TgpuGuardedComputePipelineImpl< } } +export function isGuardedComputePipeline(maybe: unknown): maybe is TgpuGuardedComputePipeline { + return ( + (maybe as TgpuGuardedComputePipeline | undefined)?.resourceType === + 'guarded-compute-pipeline' && !!(maybe as { [$internal]?: boolean } | undefined)?.[$internal] + ); +} + +export function isRoot(maybe: unknown): maybe is TgpuRoot { + return ( + (maybe as TgpuRoot | undefined)?.resourceType === 'root' && + !!(maybe as { [$internal]?: unknown } | undefined)?.[$internal] + ); +} + +export interface TgpuRootSnapshot { + readonly type: 'root'; + readonly device: GPUDevice; +} + +export function INTERNAL_snapshotRoot(root: TgpuRoot): TgpuRootSnapshot { + return { type: 'root', device: root.device }; +} + +export function INTERNAL_restoreRoot(snapshot: TgpuRootSnapshot, ctx: RestoreContext): TgpuRoot { + return ctx.getRoot(snapshot.device); +} + +export interface TgpuGuardedComputePipelineSnapshot { + readonly type: 'guarded-compute-pipeline'; + readonly device: GPUDevice; + readonly pipeline: TgpuComputePipeline; + readonly sizeUniform: TgpuUniform; + readonly workgroupSize: [number, number, number]; +} + +export function INTERNAL_snapshotGuardedComputePipeline( + pipeline: TgpuGuardedComputePipeline, +): TgpuGuardedComputePipelineSnapshot { + const impl = pipeline as TgpuGuardedComputePipelineImpl; + return { + type: 'guarded-compute-pipeline', + device: impl.root.device, + pipeline: impl.pipeline, + sizeUniform: impl.sizeUniform, + workgroupSize: [impl.workgroupSize.x, impl.workgroupSize.y, impl.workgroupSize.z], + }; +} + +export function INTERNAL_restoreGuardedComputePipeline( + snapshot: TgpuGuardedComputePipelineSnapshot, + ctx: RestoreContext, +): TgpuGuardedComputePipeline { + return new TgpuGuardedComputePipelineImpl( + ctx.getRoot(snapshot.device), + snapshot.pipeline, + snapshot.sizeUniform, + vec3u(...snapshot.workgroupSize), + ); +} + class WithBindingImpl implements WithBinding { readonly #getRoot: () => ExperimentalTgpuRoot; readonly #slotBindings: [TgpuSlot, unknown][]; @@ -281,6 +352,7 @@ class WithBindingImpl implements WithBinding { class TgpuRootImpl extends WithBindingImpl implements TgpuRoot, ExperimentalTgpuRoot { '~unstable': TgpuRoot['~unstable']; + readonly resourceType = 'root' as const; readonly device: GPUDevice; readonly nameRegistrySetting: 'random' | 'strict'; readonly shaderGenerator: ShaderGenerator | undefined; @@ -380,8 +452,11 @@ class TgpuRootImpl extends WithBindingImpl implements TgpuRoot, ExperimentalTgpu createBindGroup< Entries extends Record = Record, - >(layout: TgpuBindGroupLayout, entries: ExtractBindGroupInputFromLayout) { - return new TgpuBindGroupImpl(layout, entries); + >( + layout: TgpuBindGroupLayout, + entries: ExtractBindGroupInputFromLayout, + ): TgpuBindGroup { + return new TgpuBindGroupImpl(this, layout, entries); } destroy() { @@ -711,6 +786,7 @@ export async function init(options?: InitOptions): Promise { unstable_names: names = 'strict', unstable_logOptions, } = options ?? {}; + const { optionalFeatures, ...deviceDescriptor } = deviceOpt ?? {}; if (!navigator.gpu) { throw new Error('WebGPU is not supported by this browser.'); @@ -723,13 +799,13 @@ export async function init(options?: InitOptions): Promise { } const availableFeatures: GPUFeatureName[] = []; - for (const feature of deviceOpt?.requiredFeatures ?? []) { + for (const feature of deviceDescriptor.requiredFeatures ?? []) { if (!adapter.features.has(feature)) { throw new Error(`Requested feature "${feature}" is not supported by the adapter.`); } availableFeatures.push(feature); } - for (const feature of deviceOpt?.optionalFeatures ?? []) { + for (const feature of optionalFeatures ?? []) { if (adapter.features.has(feature)) { availableFeatures.push(feature); } else { @@ -738,7 +814,7 @@ export async function init(options?: InitOptions): Promise { } const device = await adapter.requestDevice({ - ...deviceOpt, + ...deviceDescriptor, requiredFeatures: availableFeatures, }); diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index e06c2da95b..f34f861809 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -52,6 +52,8 @@ import type { TgpuVertexLayout } from '../vertexLayout/vertexLayout.ts'; // ---------- export interface TgpuGuardedComputePipeline extends TgpuNamable { + readonly resourceType: 'guarded-compute-pipeline'; + /** * Returns a pipeline wrapper with the specified bind group bound. * Analogous to `TgpuComputePipeline.with(bindGroup)`. @@ -733,6 +735,8 @@ export interface TgpuRoot extends Unwrapper, WithBinding { logOptions: LogGeneratorOptions; }; + readonly resourceType: 'root'; + /** * The GPU device associated with this root. */ diff --git a/packages/typegpu/src/core/sampler/sampler.ts b/packages/typegpu/src/core/sampler/sampler.ts index 01e6d4ac46..d19df89016 100644 --- a/packages/typegpu/src/core/sampler/sampler.ts +++ b/packages/typegpu/src/core/sampler/sampler.ts @@ -14,10 +14,15 @@ import { type WgslSampler, } from '../../data/sampler.ts'; import { inCodegenMode } from '../../execMode.ts'; +import { invariant } from '../../errors.ts'; +import type { RestoreContext } from '../../serial/types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; interface SamplerInternals { readonly unwrap?: (() => GPUSampler) | undefined; + // Only present on fixed samplers + readonly props?: WgslSamplerProps | WgslComparisonSamplerProps | undefined; + readonly device?: GPUDevice | undefined; } // ---------- @@ -78,6 +83,36 @@ export function isComparisonSampler(resource: unknown): resource is TgpuComparis return maybe?.resourceType === 'sampler-comparison' && !!maybe[$internal]; } +export interface TgpuSamplerSnapshot { + readonly type: 'sampler' | 'sampler-comparison'; + readonly device: GPUDevice; + readonly props: WgslSamplerProps | WgslComparisonSamplerProps; +} + +export function INTERNAL_isSnapshotableSampler( + value: unknown, +): value is TgpuSampler | TgpuComparisonSampler { + return (isSampler(value) || isComparisonSampler(value)) && value[$internal].props !== undefined; +} + +export function INTERNAL_snapshotSampler( + sampler: TgpuSampler | TgpuComparisonSampler, +): TgpuSamplerSnapshot { + const { props, device } = sampler[$internal]; + invariant(props && device, 'Only samplers created from props can be snapshotted.'); + return { type: sampler.resourceType, device, props }; +} + +export function INTERNAL_restoreSampler( + snapshot: TgpuSamplerSnapshot, + ctx: RestoreContext, +): TgpuSampler | TgpuComparisonSampler { + const root = ctx.getRoot(snapshot.device); + return snapshot.type === 'sampler' + ? root.createSampler(snapshot.props as WgslSamplerProps) + : root.createComparisonSampler(snapshot.props as WgslComparisonSamplerProps); +} + // -------------- // Implementation // -------------- @@ -174,6 +209,8 @@ class TgpuFixedSamplerImpl return this.#sampler; }, + props, + device: branch.device, }; // Based on https://www.w3.org/TR/webgpu/#sampler-creation diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index 109e1aa782..0e7366f3d1 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -22,6 +22,11 @@ import { type SelfResolvable, } from '../../types.ts'; import { isTgpuFn } from '../function/tgpuFn.ts'; +import { + deserializeDataSchema, + serializeDataSchema, + type SerializedDataSchema, +} from '../../serial/schema.ts'; import { getGpuValueRecursively, valueProxyHandler } from '../valueProxyUtils.ts'; import { slot } from './slot.ts'; import type { TgpuAccessor, TgpuMutableAccessor, TgpuSlot } from './slotTypes.ts'; @@ -64,6 +69,31 @@ export function mutableAccessor AnyData) ) as unknown as TgpuMutableAccessor>; } +export interface TgpuAccessorSnapshot { + readonly type: 'accessor' | 'mutable-accessor'; + readonly schema: SerializedDataSchema; + readonly defaultValue: unknown; +} + +export function INTERNAL_snapshotAccessor( + value: TgpuAccessor | TgpuMutableAccessor, +): TgpuAccessorSnapshot { + return { + type: value.resourceType, + schema: serializeDataSchema(value.schema), + defaultValue: value.defaultValue, + }; +} + +export function INTERNAL_restoreAccessor( + snapshot: TgpuAccessorSnapshot, +): TgpuAccessor | TgpuMutableAccessor { + const schema = deserializeDataSchema(snapshot.schema); + return snapshot.type === 'accessor' + ? accessor(schema, snapshot.defaultValue) + : mutableAccessor(schema, snapshot.defaultValue as TgpuMutableAccessor.In); +} + // -------------- // Implementation // -------------- diff --git a/packages/typegpu/src/core/slot/slot.ts b/packages/typegpu/src/core/slot/slot.ts index 387b1a553d..be54785fe6 100644 --- a/packages/typegpu/src/core/slot/slot.ts +++ b/packages/typegpu/src/core/slot/slot.ts @@ -13,6 +13,19 @@ export function slot(defaultValue?: T): TgpuSlot { return new TgpuSlotImpl(defaultValue); } +export interface TgpuSlotSnapshot { + readonly type: 'slot'; + readonly defaultValue: unknown; +} + +export function INTERNAL_snapshotSlot(value: TgpuSlot): TgpuSlotSnapshot { + return { type: 'slot', defaultValue: value.defaultValue }; +} + +export function INTERNAL_restoreSlot(snapshot: TgpuSlotSnapshot): TgpuSlot { + return slot(snapshot.defaultValue); +} + // -------------- // Implementation // -------------- diff --git a/packages/typegpu/src/core/texture/texture.ts b/packages/typegpu/src/core/texture/texture.ts index 140f4b9474..0ea2b166d0 100644 --- a/packages/typegpu/src/core/texture/texture.ts +++ b/packages/typegpu/src/core/texture/texture.ts @@ -23,7 +23,8 @@ import { $gpuValueOf, $internal, $ownSnippet, $repr, $resolve } from '../../shar import type { Default, TypedArray, UnionToIntersection } from '../../shared/utilityTypes.ts'; import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; -import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; +import type { TgpuRoot } from '../root/rootTypes.ts'; +import type { RestoreContext } from '../../serial/types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; import type { TextureProps } from './textureProps.ts'; import type { @@ -137,6 +138,7 @@ type CopyCompatibleTexture = TgpuTexture<{ export interface TgpuTexture extends TgpuNamable { readonly [$internal]: TextureInternals; readonly resourceType: 'texture'; + readonly root: TgpuRoot; readonly props: TProps; // <- storing to be able to differentiate structurally between different textures. readonly destroyed: boolean; @@ -199,9 +201,55 @@ export interface TgpuTextureRenderView { export function INTERNAL_createTexture( props: TextureProps, - branch: ExperimentalTgpuRoot, + branch: TgpuRoot, + rawTexture?: GPUTexture, ): TgpuTexture { - return new TgpuTextureImpl(props, branch); + return new TgpuTextureImpl(props, branch, rawTexture); +} + +export type TextureUsageLiteral = 'sampled' | 'storage' | 'render'; + +export interface TgpuTextureSnapshot { + readonly type: 'texture'; + readonly device: GPUDevice; + readonly texture: GPUTexture; + readonly props: TextureProps; + readonly usages: TextureUsageLiteral[]; +} + +export function INTERNAL_snapshotTexture(texture: TgpuTexture): TgpuTextureSnapshot { + const usages: TextureUsageLiteral[] = []; + if (texture.usableAsSampled) { + usages.push('sampled'); + } + if (texture.usableAsStorage) { + usages.push('storage'); + } + if (texture.usableAsRender) { + usages.push('render'); + } + return { + type: 'texture', + device: texture.root.device, + texture: texture.root.unwrap(texture), + props: texture.props, + usages, + }; +} + +export function INTERNAL_restoreTexture( + snapshot: TgpuTextureSnapshot, + ctx: RestoreContext, +): TgpuTexture { + const texture = INTERNAL_createTexture( + snapshot.props, + ctx.getRoot(snapshot.device), + snapshot.texture, + ); + if (snapshot.usages.length > 0) { + texture.$usage(...(snapshot.usages as AllowedUsages[])); + } + return texture; } export function isTexture(value: unknown): value is TgpuTexture { @@ -222,6 +270,7 @@ export function isTextureView(value: unknown): value is TgpuTextureView { class TgpuTextureImpl implements TgpuTexture { readonly [$internal]: TextureInternals; readonly resourceType = 'texture'; + readonly root: TgpuRoot; readonly props: TProps; usableAsSampled = false; usableAsStorage = false; @@ -232,10 +281,14 @@ class TgpuTextureImpl implements TgpuTexture implements TgpuTexture arrayOf(elementType, count), snapshot.stepMode); + } + if (isDisarray(schema)) { + const elementType = schema.elementType as AnyData; + return vertexLayout((count) => disarrayOf(elementType, count), snapshot.stepMode); + } + throw new Error('TypeGPU vertex layout payload could not be reconstructed.'); +} + // -------------- // Implementation // -------------- diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index ec5679da22..99bf2e42fd 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -27,6 +27,7 @@ export { isBuiltinAttrib, isDecorated, isInterpolateAttrib, + isInvariantAttrib, isLocationAttrib, isPtr, isSizeAttrib, @@ -52,6 +53,7 @@ export type { F32, I32, Interpolate, + Invariant, Location, m2x2f, m3x3f, diff --git a/packages/typegpu/src/indexNamedExports.ts b/packages/typegpu/src/indexNamedExports.ts index ab260d04f0..5313c0cbe4 100644 --- a/packages/typegpu/src/indexNamedExports.ts +++ b/packages/typegpu/src/indexNamedExports.ts @@ -23,11 +23,15 @@ export { isUsableAsStorage, } from './types.ts'; export { isBufferBinding, isBufferShorthand } from './core/buffer/bufferBinding.ts'; +export { isBindGroup, isBindGroupLayout } from './tgpuBindGroupLayout.ts'; export { isTgpuFn } from './core/function/tgpuFn.ts'; export { isTgpuFragmentFn } from './core/function/tgpuFragmentFn.ts'; export { isTgpuVertexFn } from './core/function/tgpuVertexFn.ts'; export { isTgpuComputeFn } from './core/function/tgpuComputeFn.ts'; +export { isComputePipeline, isPipeline, isRenderPipeline } from './core/pipeline/typeGuards.ts'; +export { isQuerySet } from './core/querySet/querySet.ts'; export { isVariable } from './core/variable/tgpuVariable.ts'; +export { isVertexLayout } from './core/vertexLayout/vertexLayout.ts'; export type { /** @deprecated Import from 'typegpu/~internal' instead */ ShaderGenerator, } from './tgsl/shaderGenerator.ts'; @@ -62,6 +66,7 @@ export type { TgpuBuffer, Uniform, UniformFlag, + UsageLiteral, ValidUsagesFor, Vertex, VertexFlag, diff --git a/packages/typegpu/src/internal.ts b/packages/typegpu/src/internal.ts index 348233398a..2d08cbebcf 100644 --- a/packages/typegpu/src/internal.ts +++ b/packages/typegpu/src/internal.ts @@ -5,6 +5,20 @@ export { UnknownData } from './data/dataTypes.ts'; export { getName } from './shared/meta.ts'; export { WgslGenerator } from './tgsl/wgslGenerator.ts'; export { snip } from './data/snippet.ts'; +export { + isNonTransferableResource, + isSnapshotableResource, + restoreResource, + resourceSnapshotters, + snapshotResource, +} from './serial/registry.ts'; +export type { TgpuResourceSnapshot, TransferableResourceType } from './serial/registry.ts'; +export type { RestoreContext } from './serial/types.ts'; +export { + deserializeDataSchema, + serializeDataSchema, + type SerializedDataSchema, +} from './serial/schema.ts'; // types export type { ResolutionCtx, FunctionArgument, TgpuShaderStage } from './types.ts'; diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index e85a79eb61..521de5cc01 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -1123,6 +1123,8 @@ export function resolve(item: Wgsl, options: ResolutionCtxImplOptions): Resoluti return [ catchallIdx, new TgpuBindGroupImpl( + // Undefined only in rootless `tgpu.resolve()`, where the group is never unwrapped + options.root as ExperimentalTgpuRoot, catchallLayout, Object.fromEntries( // oxlint-disable-next-line typescript/no-explicit-any -- it's fine diff --git a/packages/typegpu/src/serial/dataValue.ts b/packages/typegpu/src/serial/dataValue.ts new file mode 100644 index 0000000000..76de1251a2 --- /dev/null +++ b/packages/typegpu/src/serial/dataValue.ts @@ -0,0 +1,46 @@ +import { readFromArrayBuffer, writeToArrayBuffer } from '../data/dataIO.ts'; +import { sizeOf } from '../data/sizeOf.ts'; +import { + type AnyMatInstance, + type AnyVecInstance, + type AnyWgslData, + isMatInstance, + isVecInstance, +} from '../data/wgslTypes.ts'; +import * as d from '../data/index.ts'; + +export interface TgpuDataValueSnapshot { + readonly type: 'data-value'; + readonly kind: string; + readonly bytes: ArrayBuffer; +} + +export function isSnapshotableDataValue(value: unknown): value is AnyVecInstance | AnyMatInstance { + return isVecInstance(value) || isMatInstance(value); +} + +function schemaForKind(kind: string): AnyWgslData { + // Boolean vectors report kinds like 'vec2', but their schemas are exported as 'vec2b' + const key = kind.replace('', 'b'); + const schema = (d as unknown as Record)[key]; + if (!schema) { + throw new Error(`Data value of kind '${kind}' cannot be serialized.`); + } + return schema; +} + +export function INTERNAL_snapshotDataValue( + value: AnyVecInstance | AnyMatInstance, +): TgpuDataValueSnapshot { + const schema = schemaForKind(value.kind); + const bytes = new ArrayBuffer(sizeOf(schema)); + writeToArrayBuffer(bytes, schema, value); + return { type: 'data-value', kind: value.kind, bytes }; +} + +export function INTERNAL_restoreDataValue( + snapshot: TgpuDataValueSnapshot, +): AnyVecInstance | AnyMatInstance { + const schema = schemaForKind(snapshot.kind); + return readFromArrayBuffer(snapshot.bytes, schema) as AnyVecInstance | AnyMatInstance; +} diff --git a/packages/typegpu/src/serial/layoutEntries.ts b/packages/typegpu/src/serial/layoutEntries.ts new file mode 100644 index 0000000000..f0129d6960 --- /dev/null +++ b/packages/typegpu/src/serial/layoutEntries.ts @@ -0,0 +1,183 @@ +import * as d from '../data/index.ts'; +import type { TgpuLayoutEntry } from '../tgpuBindGroupLayout.ts'; +import type { TgpuShaderStage } from '../types.ts'; +import { deserializeDataSchema, serializeDataSchema, type SerializedDataSchema } from './schema.ts'; + +type SerializedTextureSchema = + | { + kind: 'sampled'; + type: d.WgslTexture['type']; + sampleType: SerializedDataSchema; + } + | { + kind: 'storage'; + type: d.WgslStorageTexture['type']; + format: d.WgslStorageTexture['format']; + access: d.WgslStorageTexture['access']; + }; + +export type SerializedLayoutEntry = + | null + | { type: 'uniform'; schema: SerializedDataSchema; visibility?: TgpuShaderStage[] | undefined } + | { + type: 'storage'; + schema: SerializedDataSchema; + access?: 'mutable' | 'readonly' | undefined; + visibility?: TgpuShaderStage[] | undefined; + } + | { + type: 'sampler'; + sampler: 'filtering' | 'non-filtering' | 'comparison'; + visibility?: TgpuShaderStage[] | undefined; + } + | { + type: 'texture'; + schema: SerializedTextureSchema; + sampleType?: GPUTextureSampleType | undefined; + visibility?: TgpuShaderStage[] | undefined; + } + | { + type: 'storage-texture'; + schema: SerializedTextureSchema; + visibility?: TgpuShaderStage[] | undefined; + } + | { + type: 'external-texture'; + visibility?: TgpuShaderStage[] | undefined; + }; + +function serializeTextureSchema( + schema: d.WgslTexture | d.WgslStorageTexture, +): SerializedTextureSchema { + if ('multisampled' in schema) { + return { + kind: 'sampled', + type: schema.type, + sampleType: serializeDataSchema(schema.sampleType), + }; + } + return { + kind: 'storage', + type: schema.type, + format: schema.format, + access: schema.access, + }; +} + +const sampledTextureConstructors = { + texture_1d: (sampleType) => d.texture1d(sampleType), + texture_2d: (sampleType) => d.texture2d(sampleType), + texture_2d_array: (sampleType) => d.texture2dArray(sampleType), + texture_3d: (sampleType) => d.texture3d(sampleType), + texture_cube: (sampleType) => d.textureCube(sampleType), + texture_cube_array: (sampleType) => d.textureCubeArray(sampleType), + texture_multisampled_2d: (sampleType) => d.textureMultisampled2d(sampleType), + texture_depth_2d: () => d.textureDepth2d(), + texture_depth_2d_array: () => d.textureDepth2dArray(), + texture_depth_cube: () => d.textureDepthCube(), + texture_depth_cube_array: () => d.textureDepthCubeArray(), + texture_depth_multisampled_2d: () => d.textureDepthMultisampled2d(), +} satisfies Record unknown>; + +const storageTextureConstructors = { + texture_storage_1d: (format, access) => d.textureStorage1d(format, access), + texture_storage_2d: (format, access) => d.textureStorage2d(format, access), + texture_storage_2d_array: (format, access) => d.textureStorage2dArray(format, access), + texture_storage_3d: (format, access) => d.textureStorage3d(format, access), +} satisfies Record< + d.WgslStorageTexture['type'], + (format: d.WgslStorageTexture['format'], access: d.WgslStorageTexture['access']) => unknown +>; + +function deserializeTextureSchema(schema: SerializedTextureSchema) { + if (schema.kind === 'sampled') { + const constructor = sampledTextureConstructors[schema.type]; + if (!constructor) { + throw new Error(`TypeGPU texture schema '${schema.type}' could not be reconstructed.`); + } + return constructor(deserializeDataSchema(schema.sampleType) as d.WgslTexture['sampleType']); + } + const constructor = storageTextureConstructors[schema.type]; + if (!constructor) { + throw new Error(`TypeGPU storage texture schema '${schema.type}' could not be reconstructed.`); + } + return constructor(schema.format, schema.access); +} + +export function serializeLayoutEntry(entry: TgpuLayoutEntry | null): SerializedLayoutEntry { + if (entry === null) { + return null; + } + const visibility = entry.visibility; + if ('uniform' in entry) { + return { type: 'uniform', schema: serializeDataSchema(entry.uniform), visibility }; + } + if ('storage' in entry) { + return { + type: 'storage', + // The layout a runtime-sized entry produces is count-independent, so a zero stand-in works + schema: serializeDataSchema('type' in entry.storage ? entry.storage : entry.storage(0)), + access: entry.access, + visibility, + }; + } + if ('sampler' in entry) { + return { type: 'sampler', sampler: entry.sampler, visibility }; + } + if ('texture' in entry) { + return { + type: 'texture', + schema: serializeTextureSchema(entry.texture), + sampleType: entry.sampleType, + visibility, + }; + } + if ('storageTexture' in entry) { + return { + type: 'storage-texture', + schema: serializeTextureSchema(entry.storageTexture), + visibility, + }; + } + if ('externalTexture' in entry) { + return { type: 'external-texture', visibility }; + } + throw new Error('Only buffer, sampler, and texture bind group layout entries can be serialized.'); +} + +export function deserializeLayoutEntry(entry: SerializedLayoutEntry): TgpuLayoutEntry | null { + if (entry === null) { + return null; + } + const visibility = entry.visibility ? { visibility: entry.visibility } : {}; + if (entry.type === 'uniform') { + return { uniform: deserializeDataSchema(entry.schema), ...visibility }; + } + if (entry.type === 'storage') { + return { + storage: deserializeDataSchema(entry.schema), + ...(entry.access ? { access: entry.access } : {}), + ...visibility, + }; + } + if (entry.type === 'sampler') { + return { sampler: entry.sampler, ...visibility }; + } + if (entry.type === 'texture') { + return { + texture: deserializeTextureSchema(entry.schema) as d.WgslTexture, + ...(entry.sampleType ? { sampleType: entry.sampleType } : {}), + ...visibility, + }; + } + if (entry.type === 'storage-texture') { + return { + storageTexture: deserializeTextureSchema(entry.schema) as d.WgslStorageTexture, + ...visibility, + }; + } + if (entry.type === 'external-texture') { + return { externalTexture: d.textureExternal(), ...visibility }; + } + throw new Error('TypeGPU bind group layout entry payload could not be reconstructed.'); +} diff --git a/packages/typegpu/src/serial/registry.ts b/packages/typegpu/src/serial/registry.ts new file mode 100644 index 0000000000..88fe27fed6 --- /dev/null +++ b/packages/typegpu/src/serial/registry.ts @@ -0,0 +1,306 @@ +import { + INTERNAL_applyBufferUsages, + INTERNAL_restoreBuffer, + INTERNAL_snapshotBuffer, + type TgpuBufferSnapshot, +} from '../core/buffer/buffer.ts'; +import { isBuffer } from '../types.ts'; +import { isBufferBinding, type TgpuBufferBinding } from '../core/buffer/bufferBinding.ts'; +import type { AnyWgslData, BaseData } from '../data/wgslTypes.ts'; +import { deserializeDataSchema } from './schema.ts'; +import { + INTERNAL_restoreComputePipeline, + INTERNAL_snapshotComputePipeline, + type TgpuComputePipelineSnapshot, +} from '../core/pipeline/computePipeline.ts'; +import { + INTERNAL_restoreRenderPipeline, + INTERNAL_snapshotRenderPipeline, + type TgpuRenderPipelineSnapshot, +} from '../core/pipeline/renderPipeline.ts'; +import { isComputePipeline, isRenderPipeline } from '../core/pipeline/typeGuards.ts'; +import { + INTERNAL_restoreQuerySet, + INTERNAL_snapshotQuerySet, + isQuerySet, + type TgpuQuerySetSnapshot, +} from '../core/querySet/querySet.ts'; +import { + INTERNAL_restoreConst, + INTERNAL_snapshotConst, + isConst, + type TgpuConstSnapshot, +} from '../core/constant/tgpuConstant.ts'; +import { + INTERNAL_restoreGuardedComputePipeline, + INTERNAL_restoreRoot, + INTERNAL_snapshotGuardedComputePipeline, + INTERNAL_snapshotRoot, + isGuardedComputePipeline, + isRoot, + type TgpuGuardedComputePipelineSnapshot, + type TgpuRootSnapshot, +} from '../core/root/init.ts'; +import { + INTERNAL_restoreAccessor, + INTERNAL_snapshotAccessor, + type TgpuAccessorSnapshot, +} from '../core/slot/accessor.ts'; +import { + INTERNAL_restoreSlot, + INTERNAL_snapshotSlot, + type TgpuSlotSnapshot, +} from '../core/slot/slot.ts'; +import { isAccessor, isMutableAccessor, isSlot } from '../core/slot/slotTypes.ts'; +import { + INTERNAL_isSnapshotableSampler, + INTERNAL_restoreSampler, + INTERNAL_snapshotSampler, + type TgpuSamplerSnapshot, +} from '../core/sampler/sampler.ts'; +import { + INTERNAL_restoreDataValue, + INTERNAL_snapshotDataValue, + isSnapshotableDataValue, + type TgpuDataValueSnapshot, +} from './dataValue.ts'; +import { $internal } from '../shared/symbols.ts'; +import { + INTERNAL_restoreTexture, + INTERNAL_snapshotTexture, + isTexture, + type TgpuTextureSnapshot, +} from '../core/texture/texture.ts'; +import { + INTERNAL_restoreVertexLayout, + INTERNAL_snapshotVertexLayout, + isVertexLayout, + type TgpuVertexLayoutSnapshot, +} from '../core/vertexLayout/vertexLayout.ts'; +import { + INTERNAL_restoreBindGroup, + INTERNAL_restoreBindGroupLayout, + INTERNAL_snapshotBindGroup, + INTERNAL_snapshotBindGroupLayout, + isBindGroup, + isBindGroupLayout, + type TgpuBindGroupLayoutSnapshot, + type TgpuBindGroupSnapshot, +} from '../tgpuBindGroupLayout.ts'; +import type { RestoreContext } from './types.ts'; + +/** Plain objects that make a resource recreatable in another JS runtime sharing the same device */ +export type TgpuResourceSnapshot = + | TgpuBufferSnapshot + | TgpuBufferBindingSnapshot + | TgpuTextureSnapshot + | TgpuBindGroupSnapshot + | TgpuBindGroupLayoutSnapshot + | TgpuVertexLayoutSnapshot + | TgpuQuerySetSnapshot + | TgpuComputePipelineSnapshot + | TgpuRenderPipelineSnapshot + | TgpuGuardedComputePipelineSnapshot + | TgpuRootSnapshot + | TgpuSlotSnapshot + | TgpuAccessorSnapshot + | TgpuConstSnapshot + | TgpuSamplerSnapshot + | TgpuDataValueSnapshot; + +// Derived from the snapshot union, so a snapshot type with no snapshotter below fails to compile +export type TransferableResourceType = TgpuResourceSnapshot['type']; + +// Unlike `Extract`, this matches snapshotters shared between resource types (e.g. buffer bindings) +type SnapshotFor = T extends { + type: infer Type; +} + ? K extends Type + ? T + : never + : never; + +type SnapshotterContract = { + [K in TransferableResourceType]: { + is(value: unknown): boolean; + snapshot(resource: never): SnapshotFor; + restore(snapshot: never, ctx: RestoreContext): unknown; + }; +}; + +export interface TgpuBufferBindingSnapshot { + readonly type: 'uniform' | 'mutable' | 'readonly'; + readonly device: GPUDevice; + readonly buffer: TgpuBufferSnapshot; +} + +export function INTERNAL_snapshotBufferBinding( + binding: TgpuBufferBinding, +): TgpuBufferBindingSnapshot { + return { + type: binding.resourceType, + device: binding.buffer.root.device, + buffer: INTERNAL_snapshotBuffer(binding.buffer), + }; +} + +export function INTERNAL_restoreBufferBinding( + snapshot: TgpuBufferBindingSnapshot, + ctx: RestoreContext, +): TgpuBufferBinding { + const root = ctx.getRoot(snapshot.device); + const schema = deserializeDataSchema(snapshot.buffer.schema) as AnyWgslData; + const rawBuffer = snapshot.buffer.buffer; + const binding = + snapshot.type === 'uniform' + ? root.createUniform(schema, rawBuffer) + : snapshot.type === 'mutable' + ? root.createMutable(schema, rawBuffer) + : root.createReadonly(schema, rawBuffer); + INTERNAL_applyBufferUsages(binding.buffer, snapshot.buffer.usages); + return binding; +} + +const bufferBindingSnapshotter = { + is: isBufferBinding, + snapshot: INTERNAL_snapshotBufferBinding, + restore: INTERNAL_restoreBufferBinding, +}; + +export const resourceSnapshotters = { + buffer: { + is: isBuffer, + snapshot: INTERNAL_snapshotBuffer, + restore: INTERNAL_restoreBuffer, + }, + uniform: bufferBindingSnapshotter, + mutable: bufferBindingSnapshotter, + readonly: bufferBindingSnapshotter, + texture: { + is: isTexture, + snapshot: INTERNAL_snapshotTexture, + restore: INTERNAL_restoreTexture, + }, + 'bind-group': { + is: isBindGroup, + snapshot: INTERNAL_snapshotBindGroup, + restore: INTERNAL_restoreBindGroup, + }, + 'bind-group-layout': { + is: isBindGroupLayout, + snapshot: INTERNAL_snapshotBindGroupLayout, + restore: INTERNAL_restoreBindGroupLayout, + }, + 'vertex-layout': { + is: isVertexLayout, + snapshot: INTERNAL_snapshotVertexLayout, + restore: INTERNAL_restoreVertexLayout, + }, + 'query-set': { + is: isQuerySet, + snapshot: INTERNAL_snapshotQuerySet, + restore: INTERNAL_restoreQuerySet, + }, + 'compute-pipeline': { + is: isComputePipeline, + snapshot: INTERNAL_snapshotComputePipeline, + restore: INTERNAL_restoreComputePipeline, + }, + 'render-pipeline': { + is: isRenderPipeline, + snapshot: INTERNAL_snapshotRenderPipeline, + restore: INTERNAL_restoreRenderPipeline, + }, + 'guarded-compute-pipeline': { + is: isGuardedComputePipeline, + snapshot: INTERNAL_snapshotGuardedComputePipeline, + restore: INTERNAL_restoreGuardedComputePipeline, + }, + root: { + is: isRoot, + snapshot: INTERNAL_snapshotRoot, + restore: INTERNAL_restoreRoot, + }, + slot: { + is: isSlot, + snapshot: INTERNAL_snapshotSlot, + restore: INTERNAL_restoreSlot, + }, + accessor: { + is: isAccessor, + snapshot: INTERNAL_snapshotAccessor, + restore: INTERNAL_restoreAccessor, + }, + 'mutable-accessor': { + is: isMutableAccessor, + snapshot: INTERNAL_snapshotAccessor, + restore: INTERNAL_restoreAccessor, + }, + const: { + is: isConst, + snapshot: INTERNAL_snapshotConst, + restore: INTERNAL_restoreConst, + }, + sampler: { + is: INTERNAL_isSnapshotableSampler, + snapshot: INTERNAL_snapshotSampler, + restore: INTERNAL_restoreSampler, + }, + 'sampler-comparison': { + is: INTERNAL_isSnapshotableSampler, + snapshot: INTERNAL_snapshotSampler, + restore: INTERNAL_restoreSampler, + }, + // Vector/matrix instances have no `resourceType`, see `getSnapshotterFor` + 'data-value': { + is: isSnapshotableDataValue, + snapshot: INTERNAL_snapshotDataValue, + restore: INTERNAL_restoreDataValue, + }, +} satisfies SnapshotterContract; + +type LooseSnapshotter = { + is(value: unknown): boolean; + snapshot(resource: unknown): TgpuResourceSnapshot; + restore(snapshot: TgpuResourceSnapshot, ctx: RestoreContext): unknown; +}; + +function getSnapshotterFor(value: unknown): LooseSnapshotter | undefined { + const resourceType = (value as { resourceType?: unknown } | undefined)?.resourceType; + const type = + typeof resourceType === 'string' + ? resourceType + : isSnapshotableDataValue(value) + ? 'data-value' + : undefined; + if (type === undefined || !(type in resourceSnapshotters)) { + return undefined; + } + const snapshotter = (resourceSnapshotters as Record)[type]; + return snapshotter?.is(value) ? snapshotter : undefined; +} + +export function isSnapshotableResource(value: unknown): boolean { + return getSnapshotterFor(value) !== undefined; +} + +/** Whether the value is a TypeGPU object that {@link snapshotResource} does not support */ +export function isNonTransferableResource(value: unknown): boolean { + return ( + typeof value === 'object' && + value !== null && + $internal in value && + getSnapshotterFor(value) === undefined + ); +} + +export function snapshotResource(value: unknown): TgpuResourceSnapshot | undefined { + return getSnapshotterFor(value)?.snapshot(value); +} + +export function restoreResource(snapshot: TgpuResourceSnapshot, ctx: RestoreContext): unknown { + return (resourceSnapshotters as Record)[snapshot.type]?.restore( + snapshot, + ctx, + ); +} diff --git a/packages/typegpu/src/serial/schema.ts b/packages/typegpu/src/serial/schema.ts new file mode 100644 index 0000000000..b9163a95e3 --- /dev/null +++ b/packages/typegpu/src/serial/schema.ts @@ -0,0 +1,193 @@ +import * as d from '../data/index.ts'; + +type SerializedDataAttrib = + | { type: 'align'; value: number } + | { type: 'size'; value: number } + | { type: 'location'; value: number } + | { type: 'interpolate'; value: string } + | { type: 'builtin'; value: string } + | { type: 'invariant' }; + +export type SerializedDataSchema = + | { type: 'd'; key: string } + | { type: 'array'; element: SerializedDataSchema; count: number } + | { type: 'disarray'; element: SerializedDataSchema; count: number } + | { type: 'struct'; props: [string, SerializedDataSchema][] } + | { type: 'unstruct'; props: [string, SerializedDataSchema][] } + | { type: 'atomic'; inner: SerializedDataSchema } + | { type: 'decorated'; inner: SerializedDataSchema; attribs: SerializedDataAttrib[] }; + +// Maps schema singletons (e.g. `d.f32`) back to their `d` export names +let leafKeys: Map | undefined; + +function getDataSchemaKey(schema: d.BaseData): string | undefined { + if (!leafKeys) { + leafKeys = new Map(); + for (const [key, value] of Object.entries(d)) { + if ((typeof value === 'object' || typeof value === 'function') && value !== null) { + leafKeys.set(value, key); + } + } + } + return leafKeys.get(schema); +} + +let builtinsByName: Map | undefined; + +function getBuiltinByName(value: string): d.AnyData { + if (!builtinsByName) { + builtinsByName = new Map(); + for (const candidate of Object.values(d.builtin) as d.AnyData[]) { + if (!d.isDecorated(candidate) && !d.isLooseDecorated(candidate)) { + continue; + } + const builtin = candidate.attribs.find(d.isBuiltinAttrib); + if (builtin) { + builtinsByName.set(builtin.params[0], candidate); + } + } + } + const builtin = builtinsByName.get(value); + if (!builtin) { + throw new Error(`TypeGPU builtin '${value}' could not be reconstructed.`); + } + return builtin; +} + +function serializeAttrib(attrib: unknown): SerializedDataAttrib { + if (d.isAlignAttrib(attrib)) { + return { type: 'align', value: attrib.params[0] }; + } + if (d.isSizeAttrib(attrib)) { + return { type: 'size', value: attrib.params[0] }; + } + if (d.isLocationAttrib(attrib)) { + return { type: 'location', value: attrib.params[0] }; + } + if (d.isInterpolateAttrib(attrib)) { + return { type: 'interpolate', value: attrib.params[0] }; + } + if (d.isBuiltinAttrib(attrib)) { + return { type: 'builtin', value: attrib.params[0] }; + } + if (d.isInvariantAttrib(attrib)) { + return { type: 'invariant' }; + } + throw new Error('This TypeGPU schema decorator cannot be serialized yet.'); +} + +function applyAttrib(schema: d.AnyData, attrib: SerializedDataAttrib): d.AnyData { + if (attrib.type === 'align') { + return d.align(attrib.value, schema); + } + if (attrib.type === 'size') { + return d.size(attrib.value, schema); + } + if (attrib.type === 'location') { + return d.location(attrib.value, schema); + } + if (attrib.type === 'interpolate') { + return d.interpolate(attrib.value as never, schema as never); + } + if (attrib.type === 'builtin') { + return getBuiltinByName(attrib.value); + } + return d.invariant(schema as Parameters[0]); +} + +function serializeProps(propTypes: Record): [string, SerializedDataSchema][] { + return Object.entries(propTypes).map(([prop, propType]) => [prop, serializeDataSchema(propType)]); +} + +function deserializeProps(props: [string, SerializedDataSchema][]): Record { + return Object.fromEntries(props.map(([prop, schema]) => [prop, deserializeDataSchema(schema)])); +} + +export function serializeDataSchema(schema: d.BaseData): SerializedDataSchema { + const key = getDataSchemaKey(schema); + if (key) { + return { type: 'd', key }; + } + + if (d.isDecorated(schema) || d.isLooseDecorated(schema)) { + return { + type: 'decorated', + inner: serializeDataSchema(schema.inner as d.AnyData), + attribs: schema.attribs.map(serializeAttrib), + }; + } + + if (d.isAtomic(schema)) { + return { type: 'atomic', inner: serializeDataSchema(schema.inner as d.AnyData) }; + } + + if (d.isWgslArray(schema)) { + return { + type: 'array', + element: serializeDataSchema(schema.elementType as d.AnyData), + count: schema.elementCount, + }; + } + + if (d.isDisarray(schema)) { + return { + type: 'disarray', + element: serializeDataSchema(schema.elementType as d.AnyData), + count: schema.elementCount, + }; + } + + if (d.isWgslStruct(schema)) { + return { type: 'struct', props: serializeProps(schema.propTypes) }; + } + + if (d.isUnstruct(schema)) { + return { type: 'unstruct', props: serializeProps(schema.propTypes) }; + } + + throw new Error(`TypeGPU schema '${schema.type}' cannot be serialized yet.`); +} + +export function deserializeDataSchema(schema: SerializedDataSchema): d.AnyData { + if (schema.type === 'd') { + const leaf = (d as unknown as Record)[schema.key]; + if (!leaf) { + throw new Error(`TypeGPU schema 'd.${schema.key}' could not be reconstructed.`); + } + return leaf; + } + + if (schema.type === 'array') { + return d.arrayOf( + deserializeDataSchema(schema.element) as d.AnyWgslData, + schema.count, + ) as d.AnyData; + } + + if (schema.type === 'disarray') { + return d.disarrayOf(deserializeDataSchema(schema.element), schema.count) as d.AnyData; + } + + if (schema.type === 'struct') { + return d.struct(deserializeProps(schema.props) as Record) as d.AnyData; + } + + if (schema.type === 'unstruct') { + return d.unstruct(deserializeProps(schema.props)) as d.AnyData; + } + + if (schema.type === 'atomic') { + return d.atomic(deserializeDataSchema(schema.inner) as d.U32 | d.I32) as d.AnyData; + } + + if (schema.type === 'decorated') { + let result = deserializeDataSchema(schema.inner); + for (let i = schema.attribs.length - 1; i >= 0; i--) { + const attrib = schema.attribs[i] as SerializedDataAttrib; + result = applyAttrib(result, attrib); + } + return result; + } + + throw new Error('TypeGPU schema payload could not be reconstructed.'); +} diff --git a/packages/typegpu/src/serial/types.ts b/packages/typegpu/src/serial/types.ts new file mode 100644 index 0000000000..1d73fb87e4 --- /dev/null +++ b/packages/typegpu/src/serial/types.ts @@ -0,0 +1,6 @@ +import type { TgpuRoot } from '../core/root/rootTypes.ts'; + +/** Lets restored resources resolve the root they belong to, identified by the shared `GPUDevice` */ +export interface RestoreContext { + getRoot(device: GPUDevice): TgpuRoot; +} diff --git a/packages/typegpu/src/tgpuBindGroupLayout.ts b/packages/typegpu/src/tgpuBindGroupLayout.ts index 1ede124d55..4202d083cb 100644 --- a/packages/typegpu/src/tgpuBindGroupLayout.ts +++ b/packages/typegpu/src/tgpuBindGroupLayout.ts @@ -30,6 +30,7 @@ import { type WgslStorageTexture, type WgslTexture, } from './data/texture.ts'; +import type { TgpuRoot } from './core/root/rootTypes.ts'; import type { AnyWgslData, BaseData } from './data/wgslTypes.ts'; import { invariant, NotUniformError } from './errors.ts'; import { NotStorageError, type StorageFlag } from './extension.ts'; @@ -41,6 +42,12 @@ import { $gpuValueOf, $internal } from './shared/symbols.ts'; import type { NullableToOptional, Prettify } from './shared/utilityTypes.ts'; import type { ResolvableObject, TgpuShaderStage } from './types.ts'; import type { Unwrapper } from './unwrapper.ts'; +import { + deserializeLayoutEntry, + serializeLayoutEntry, + type SerializedLayoutEntry, +} from './serial/layoutEntries.ts'; +import type { RestoreContext } from './serial/types.ts'; import type { WgslComparisonSampler, WgslSampler } from './data/sampler.ts'; import { TgpuLaidOutBufferImpl } from './core/buffer/laidOutBuffer.ts'; @@ -215,6 +222,7 @@ export type TgpuBindGroup< Entries extends Record = Record, > = { readonly resourceType: 'bind-group'; + readonly root: TgpuRoot; readonly layout: TgpuBindGroupLayout; unwrap(unwrapper: Unwrapper): GPUBindGroup; }; @@ -233,6 +241,66 @@ export function isBindGroup(value: unknown): value is TgpuBindGroup { return !!value && (value as TgpuBindGroup).resourceType === 'bind-group'; } +export interface TgpuBindGroupLayoutSnapshot { + readonly type: 'bind-group-layout'; + readonly entries: [string, SerializedLayoutEntry][]; + readonly index: number | undefined; +} + +export function INTERNAL_snapshotBindGroupLayout( + layout: TgpuBindGroupLayout, +): TgpuBindGroupLayoutSnapshot { + return { + type: 'bind-group-layout', + entries: Object.entries(layout.entries).map(([key, entry]) => [ + key, + serializeLayoutEntry(entry), + ]), + index: layout.index, + }; +} + +export function INTERNAL_restoreBindGroupLayout( + snapshot: TgpuBindGroupLayoutSnapshot, +): TgpuBindGroupLayout { + const layout = bindGroupLayout( + Object.fromEntries( + snapshot.entries.map(([key, entry]) => [key, deserializeLayoutEntry(entry)]), + ), + ); + return layout.$idx(snapshot.index); +} + +export interface TgpuBindGroupSnapshot { + readonly type: 'bind-group'; + readonly device: GPUDevice; + readonly layout: TgpuBindGroupLayout; + readonly bindGroup: GPUBindGroup; +} + +export function INTERNAL_snapshotBindGroup(bindGroup: TgpuBindGroup): TgpuBindGroupSnapshot { + return { + type: 'bind-group', + device: bindGroup.root.device, + layout: bindGroup.layout, + bindGroup: bindGroup.root.unwrap(bindGroup), + }; +} + +export function INTERNAL_restoreBindGroup( + snapshot: TgpuBindGroupSnapshot, + ctx: RestoreContext, +): TgpuBindGroup { + const root = ctx.getRoot(snapshot.device); + const bindGroup = snapshot.bindGroup; + return { + resourceType: 'bind-group', + root, + layout: snapshot.layout, + unwrap: () => bindGroup, + }; +} + /** * @category Errors */ @@ -426,13 +494,16 @@ export class TgpuBindGroupImpl< Entries extends Record = Record, > implements TgpuBindGroup { readonly resourceType = 'bind-group' as const; + readonly root: TgpuRoot; readonly layout: TgpuBindGroupLayout; readonly entries: ExtractBindGroupInputFromLayout; constructor( + root: TgpuRoot, layout: TgpuBindGroupLayout, entries: ExtractBindGroupInputFromLayout, ) { + this.root = root; this.layout = layout; this.entries = entries; diff --git a/packages/typegpu/tests/computePipeline.test.ts b/packages/typegpu/tests/computePipeline.test.ts index e39927a5a7..a55af3f99d 100644 --- a/packages/typegpu/tests/computePipeline.test.ts +++ b/packages/typegpu/tests/computePipeline.test.ts @@ -1,5 +1,6 @@ import { describe, expect, expectTypeOf, vi } from 'vitest'; -import { d, MissingBindGroupsError, tgpu, type TgpuComputePipeline } from 'typegpu'; +import { d, isBindGroup, MissingBindGroupsError, tgpu, type TgpuComputePipeline } from 'typegpu'; +import { restoreResource, snapshotResource } from 'typegpu/~internal'; import { it } from 'typegpu-testing-utility'; import { extensionEnabled } from 'typegpu/std'; @@ -187,6 +188,91 @@ describe('TgpuComputePipeline', () => { }); }); + it('should wrap raw compute pipelines with bind groups', ({ root, commandEncoder }) => { + const manualLayout = tgpu.bindGroupLayout({ params: { uniform: d.f32 } }); + const manualBindGroup = root.createBindGroup(manualLayout, { + params: root.createBuffer(d.f32).$usage('uniform'), + }); + const fixedUniform = root.createUniform(d.f32); + + const sourcePipeline = root + .createComputePipeline({ + compute: tgpu.computeFn({ workgroupSize: [1] })(() => { + 'use gpu'; + fixedUniform.$; + manualLayout.$.params; + }), + }) + .with(manualBindGroup); + + const snapshot = snapshotResource(sourcePipeline); + if (snapshot?.type !== 'compute-pipeline') { + throw new Error('Expected a compute pipeline snapshot'); + } + + const pipeline = restoreResource(snapshot, { getRoot: () => root }) as TgpuComputePipeline; + + expect(snapshot.device).toBe(root.device); + expect(snapshot.bindGroups).toHaveLength(2); + expect(snapshot.bindGroups.some(([, bindGroup]) => bindGroup === manualBindGroup)).toBe(true); + + pipeline.dispatchWorkgroups(1); + + const computePass = commandEncoder.mock.beginComputePass.mock.results[0]!.value as { + setPipeline: ReturnType; + setBindGroup: ReturnType; + }; + + expect(computePass.setPipeline).toHaveBeenCalledWith(snapshot.pipeline); + for (const [layout, bindGroup] of snapshot.bindGroups) { + expect(computePass.setBindGroup).toHaveBeenCalledWith( + snapshot.usedBindGroupLayouts.indexOf(layout), + isBindGroup(bindGroup) ? root.unwrap(bindGroup) : bindGroup, + ); + } + }); + + it('should let .with() override preset bind groups on raw compute pipelines', ({ + root, + commandEncoder, + }) => { + const manualLayout = tgpu.bindGroupLayout({ params: { uniform: d.f32 } }); + const manualBindGroup = root.createBindGroup(manualLayout, { + params: root.createBuffer(d.f32).$usage('uniform'), + }); + const overrideBindGroup = root.createBindGroup(manualLayout, { + params: root.createBuffer(d.f32).$usage('uniform'), + }); + + const sourcePipeline = root + .createComputePipeline({ + compute: tgpu.computeFn({ workgroupSize: [1] })(() => { + 'use gpu'; + manualLayout.$.params; + }), + }) + .with(manualBindGroup); + + const snapshot = snapshotResource(sourcePipeline); + if (snapshot?.type !== 'compute-pipeline') { + throw new Error('Expected a compute pipeline snapshot'); + } + + const pipeline = ( + restoreResource(snapshot, { getRoot: () => root }) as TgpuComputePipeline + ).with(overrideBindGroup); + + pipeline.dispatchWorkgroups(1); + + const computePass = commandEncoder.mock.beginComputePass.mock.results[0]!.value as { + setBindGroup: ReturnType; + }; + + expect(computePass.setBindGroup).toHaveBeenCalledTimes(1); + expect(computePass.setBindGroup.mock.calls[0]![0]).toBe(0); + expect(computePass.setBindGroup.mock.calls[0]![1]).toBe(root.unwrap(overrideBindGroup)); + }); + it('enables language extensions when their corresponding feature is enabled', ({ root, device, diff --git a/packages/typegpu/tests/internal/typeGuards.test.ts b/packages/typegpu/tests/internal/typeGuards.test.ts new file mode 100644 index 0000000000..5ca8a52685 --- /dev/null +++ b/packages/typegpu/tests/internal/typeGuards.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { + isGPUCommandEncoder, + isGPUComputePassEncoder, + isGPURenderBundleEncoder, + isGPURenderPassEncoder, +} from '../../src/core/pipeline/typeGuards.ts'; + +// JSI HostObjects expose native methods through property lookup, but their `has` trap reports false +function jsiLike(methods: Record void>): object { + return new Proxy( + {}, + { + get: (_target, key) => methods[String(key)], + has: () => false, + }, + ); +} + +describe('pipeline WebGPU type guards', () => { + it('recognizes a JSI-like command encoder without relying on `in`', () => { + const encoder = jsiLike({ beginRenderPass() {}, beginComputePass() {} }); + + expect(isGPUCommandEncoder(encoder)).toBe(true); + expect(isGPUComputePassEncoder(encoder)).toBe(false); + }); + + it('recognizes JSI-like compute and render pass encoders', () => { + const computePass = jsiLike({ dispatchWorkgroups() {} }); + const renderPass = jsiLike({ executeBundles() {}, draw() {} }); + + expect(isGPUComputePassEncoder(computePass)).toBe(true); + expect(isGPURenderPassEncoder(renderPass)).toBe(true); + }); + + it('keeps a JSI-like render bundle distinct from a render pass', () => { + const bundle = jsiLike({ draw() {}, finish() {} }); + + expect(isGPURenderBundleEncoder(bundle)).toBe(true); + expect(isGPURenderPassEncoder(bundle)).toBe(false); + }); +}); diff --git a/packages/typegpu/tests/renderPipeline.test.ts b/packages/typegpu/tests/renderPipeline.test.ts index 2ad3410849..ef2a24ecbe 100644 --- a/packages/typegpu/tests/renderPipeline.test.ts +++ b/packages/typegpu/tests/renderPipeline.test.ts @@ -3,6 +3,7 @@ import { tgpu, common, d, + isBindGroup, MissingBindGroupsError, type TgpuFragmentFn, type TgpuFragmentFnShell, @@ -11,6 +12,7 @@ import { type TgpuVertexFn, type TgpuVertexFnShell, } from 'typegpu'; +import { restoreResource, snapshotResource } from 'typegpu/~internal'; import { it } from 'typegpu-testing-utility'; describe('render pipeline behavior', () => { @@ -485,6 +487,53 @@ describe('render pipeline behavior', () => { expect(renderPassEncoder.setStencilReference).toHaveBeenNthCalledWith(2, 7); }); + it('should wrap raw render pipelines with bind groups', ({ root, renderPassEncoder }) => { + const manualLayout = tgpu.bindGroupLayout({ params: { uniform: d.f32 } }); + const manualBindGroup = root.createBindGroup(manualLayout, { + params: root.createBuffer(d.f32).$usage('uniform'), + }); + const fixedUniform = root.createUniform(d.f32); + + const sourcePipeline = root + .createRenderPipeline({ + vertex: common.fullScreenTriangle, + fragment: () => { + 'use gpu'; + return d.vec4f(fixedUniform.$, manualLayout.$.params, 0, 1); + }, + }) + .with(manualBindGroup); + + const snapshot = snapshotResource(sourcePipeline); + if (snapshot?.type !== 'render-pipeline') { + throw new Error('Expected a render pipeline snapshot'); + } + + const pipeline = ( + restoreResource(snapshot, { getRoot: () => root }) as TgpuRenderPipeline + ).withColorAttachment({ view: {} as unknown as GPUTextureView }); + + expect(snapshot.device).toBe(root.device); + expect(snapshot.fragmentOut).toMatchInlineSnapshot(` + { + "key": "vec4f", + "type": "d", + } + `); + expect(snapshot.bindGroups).toHaveLength(2); + expect(snapshot.bindGroups.some(([, bindGroup]) => bindGroup === manualBindGroup)).toBe(true); + + pipeline.draw(3); + + expect(renderPassEncoder.mock.setPipeline).toHaveBeenCalledWith(snapshot.pipeline); + for (const [layout, bindGroup] of snapshot.bindGroups) { + expect(renderPassEncoder.mock.setBindGroup).toHaveBeenCalledWith( + snapshot.usedBindGroupLayouts.indexOf(layout), + isBindGroup(bindGroup) ? root.unwrap(bindGroup) : bindGroup, + ); + } + }); + it('should onlly allow for drawIndexed with assigned index buffer', ({ root }) => { const vertexFn = tgpu .vertexFn({ diff --git a/packages/typegpu/tests/root.test.ts b/packages/typegpu/tests/root.test.ts index 76fc701895..4e2d40a198 100644 --- a/packages/typegpu/tests/root.test.ts +++ b/packages/typegpu/tests/root.test.ts @@ -4,6 +4,27 @@ import { tgpu, d } from 'typegpu'; import { it } from 'typegpu-testing-utility'; describe('TgpuRoot', () => { + describe('init', () => { + it('does not forward optionalFeatures to requestDevice', async ({ adapter }) => { + const root = await tgpu.init({ + device: { optionalFeatures: ['timestamp-query'] }, + }); + + expect(adapter.requestDevice.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "requiredFeatures": [ + "timestamp-query", + ], + }, + ], + ] + `); + root.destroy(); + }); + }); + describe('.createBuffer', () => { it('should create buffer with no initialization', ({ root }) => { const dataBuffer = root.createBuffer(d.u32).$usage('uniform'); diff --git a/packages/typegpu/tests/serial.test.ts b/packages/typegpu/tests/serial.test.ts new file mode 100644 index 0000000000..795f199cdb --- /dev/null +++ b/packages/typegpu/tests/serial.test.ts @@ -0,0 +1,240 @@ +import { describe, expect, vi } from 'vitest'; +import { tgpu, d, type TgpuRoot } from 'typegpu'; +import { deepEqual } from 'typegpu/data'; +import { isNonTransferableResource, restoreResource, snapshotResource } from 'typegpu/~internal'; +import { it } from 'typegpu-testing-utility'; + +function roundTrip(value: T, root: TgpuRoot): T { + const snapshot = snapshotResource(value); + if (!snapshot) { + throw new Error('Expected the value to be snapshotable.'); + } + return restoreResource(snapshot, { + getRoot: (device) => { + expect(device).toBe(root.device); + return root; + }, + }) as T; +} + +describe('resource snapshot protocol', () => { + it('round-trips buffers and buffer bindings', ({ root }) => { + const buffer = root.createBuffer(d.arrayOf(d.u32, 3)).$usage('storage', 'indirect'); + const rawBuffer = root.unwrap(buffer); + + const restored = roundTrip(buffer, root); + expect(restored.usableAsUniform).toBe(false); + expect(restored.usableAsStorage).toBe(true); + expect(restored.usableAsVertex).toBe(false); + expect(restored.usableAsIndex).toBe(false); + expect(restored.usableAsIndirect).toBe(true); + expect(restored.root.device).toBe(root.device); + expect(root.unwrap(restored)).toBe(rawBuffer); + + const uniform = root.createUniform(d.vec2f, d.vec2f(1, 2)); + const restoredUniform = roundTrip(uniform, root); + expect(restoredUniform.resourceType).toBe('uniform'); + expect(root.unwrap(restoredUniform.buffer)).toBe(root.unwrap(uniform.buffer)); + }); + + it('round-trips bind group layouts, bind groups and textures', ({ root }) => { + const layout = tgpu + .bindGroupLayout({ + video: { externalTexture: d.textureExternal(), visibility: ['fragment'] }, + color: { + texture: d.texture2d(d.f32), + sampleType: 'unfilterable-float', + visibility: ['fragment'], + }, + target: { + storageTexture: d.textureStorage3d('rgba8unorm', 'read-write'), + visibility: ['compute'], + }, + cells: { storage: d.arrayOf(d.vec4f), access: 'mutable', visibility: ['compute'] }, + }) + .$idx(2); + + const restoredLayout = roundTrip(layout, root); + expect(restoredLayout.index).toBe(2); + const { cells, ...staticEntries } = restoredLayout.entries; + const { cells: _, ...originalStaticEntries } = layout.entries; + expect(staticEntries).toEqual(originalStaticEntries); + if (cells?.storage && 'type' in cells.storage && d.isWgslArray(cells.storage)) { + expect(cells.storage.elementCount).toBe(0); + expect(cells.storage.elementType).toBe(d.vec4f); + } else { + throw new Error('Expected a runtime-sized array storage layout entry.'); + } + + const groupLayout = tgpu.bindGroupLayout({ + values: { storage: d.arrayOf(d.u32, 4), access: 'mutable' }, + }); + const buffer = root.createBuffer(d.arrayOf(d.u32, 4)).$usage('storage'); + const bindGroup = root.createBindGroup(groupLayout, { values: buffer }); + const restoredGroup = roundTrip(bindGroup, root); + expect(restoredGroup.resourceType).toBe('bind-group'); + expect(restoredGroup.unwrap(root)).toBe(root.unwrap(bindGroup)); + + const texture = root + .createTexture({ size: [2, 2], format: 'rgba8unorm' }) + .$usage('sampled', 'render'); + const rawTexture = root.unwrap(texture); + const restoredTexture = roundTrip(texture, root); + expect(restoredTexture.props).toEqual(texture.props); + expect(restoredTexture.usableAsSampled).toBe(true); + expect(restoredTexture.usableAsStorage).toBe(false); + expect(restoredTexture.usableAsRender).toBe(true); + expect(root.unwrap(restoredTexture)).toBe(rawTexture); + }); + + it('round-trips compute, render and guarded compute pipelines', ({ root }) => { + const querySet = root.createQuerySet('timestamp', 2); + const callback = vi.fn(); + const computePipeline = root + .createComputePipeline({ + compute: tgpu.computeFn({ workgroupSize: [1] })(() => { + 'use gpu'; + }), + }) + .withTimestampWrites({ + querySet, + beginningOfPassWriteIndex: 0, + endOfPassWriteIndex: 1, + }) + .withPerformanceCallback(callback); + + const computeSnapshot = snapshotResource(roundTrip(computePipeline, root)); + if (computeSnapshot?.type !== 'compute-pipeline') { + throw new Error('Expected a compute pipeline snapshot'); + } + expect(computeSnapshot.performanceCallback).toBe(callback); + expect(computeSnapshot.timestampWrites?.beginningOfPassWriteIndex).toBe(0); + expect(computeSnapshot.timestampWrites?.endOfPassWriteIndex).toBe(1); + + const vertexLayout = tgpu.vertexLayout(d.arrayOf(d.vec2f)); + const vertexBuffer = root.createBuffer(vertexLayout.schemaForCount(3)).$usage('vertex'); + const shelledFragment = tgpu.fragmentFn({ out: d.vec4f })(() => { + 'use gpu'; + return d.vec4f(1, 0, 0, 1); + }); + const renderPipeline = root + .createRenderPipeline({ + attribs: { position: vertexLayout.attrib }, + vertex: ({ position }) => { + 'use gpu'; + return { $position: d.vec4f(position, 0, 1) }; + }, + fragment: shelledFragment, + targets: { format: 'rgba8unorm' }, + }) + .with(vertexLayout, vertexBuffer); + + const renderSnapshot = snapshotResource(roundTrip(renderPipeline, root)); + if (renderSnapshot?.type !== 'render-pipeline') { + throw new Error('Expected a render pipeline snapshot'); + } + // Shelled fragments carry their output on the descriptor, not the memo + expect(renderSnapshot.fragmentOut).toMatchInlineSnapshot(` + { + "attribs": [ + { + "type": "location", + "value": 0, + }, + ], + "inner": { + "key": "vec4f", + "type": "d", + }, + "type": "decorated", + } + `); + expect(renderSnapshot.usedVertexLayouts).toEqual([vertexLayout]); + expect(renderSnapshot.vertexBuffers).toEqual([[vertexLayout, vertexBuffer]]); + + const groupLayout = tgpu.bindGroupLayout({ + values: { storage: d.arrayOf(d.u32, 4), access: 'mutable' }, + }); + const buffer = root.createBuffer(d.arrayOf(d.u32, 4)).$usage('storage'); + const bindGroup = root.createBindGroup(groupLayout, { values: buffer }); + const guarded = root + .createGuardedComputePipeline((x: number, y: number) => { + 'use gpu'; + groupLayout.$.values[x + y] = x; + }) + .with(bindGroup); + + const guardedSnapshot = snapshotResource(roundTrip(guarded, root)); + if (guardedSnapshot?.type !== 'guarded-compute-pipeline') { + throw new Error('Expected a guarded compute pipeline snapshot'); + } + expect(guardedSnapshot.workgroupSize).toEqual([16, 16, 1]); + expect(guardedSnapshot.sizeUniform.resourceType).toBe('uniform'); + const innerSnapshot = snapshotResource(guardedSnapshot.pipeline); + if (innerSnapshot?.type !== 'compute-pipeline') { + throw new Error('Expected a compute pipeline snapshot'); + } + expect(innerSnapshot.bindGroups.some(([, group]) => group === bindGroup)).toBe(true); + }); + + it('round-trips slots, accessors, consts, samplers, query sets and vertex layouts', ({ + root, + }) => { + const slot = tgpu.slot(42); + const restoredSlot = roundTrip(slot, root); + expect(restoredSlot.resourceType).toBe('slot'); + expect(restoredSlot.defaultValue).toBe(42); + + const accessor = tgpu.accessor(d.vec3f, d.vec3f(1, 2, 3)); + const restoredAccessor = roundTrip(accessor, root); + expect(restoredAccessor.resourceType).toBe('accessor'); + expect(restoredAccessor.schema).toBe(d.vec3f); + expect(restoredAccessor.defaultValue).toEqual(d.vec3f(1, 2, 3)); + + const constant = tgpu['~unstable'].const(d.arrayOf(d.f32, 3), [1, 2, 3]); + const restoredConst = roundTrip(constant, root); + expect(restoredConst.resourceType).toBe('const'); + expect(restoredConst.$).toEqual([1, 2, 3]); + + const sampler = root.createSampler({ magFilter: 'linear', minFilter: 'linear' }); + expect(roundTrip(sampler, root).resourceType).toBe('sampler'); + const comparison = root.createComparisonSampler({ compare: 'less' }); + expect(roundTrip(comparison, root).resourceType).toBe('sampler-comparison'); + + const querySet = root.createQuerySet('timestamp', 2); + const restoredQuerySet = roundTrip(querySet, root); + expect(restoredQuerySet.resourceType).toBe('query-set'); + expect(restoredQuerySet.type).toBe('timestamp'); + expect(restoredQuerySet.count).toBe(2); + expect(restoredQuerySet.querySet).toBe(querySet.querySet); + + const vertexLayout = tgpu.vertexLayout( + (count) => d.arrayOf(d.struct({ position: d.location(0, d.vec2f) }), count), + 'instance', + ); + const restoredVertexLayout = roundTrip(vertexLayout, root); + expect(restoredVertexLayout.resourceType).toBe('vertex-layout'); + expect(restoredVertexLayout.stepMode).toBe(vertexLayout.stepMode); + expect(restoredVertexLayout.stride).toBe(vertexLayout.stride); + expect(deepEqual(restoredVertexLayout.schemaForCount(4), vertexLayout.schemaForCount(4))).toBe( + true, + ); + }); + + it('round-trips vector and matrix instances, rejects non-transferable resources', ({ root }) => { + const vec = d.vec3f(1.5, -2, 3.25); + const restoredVec = roundTrip(vec, root); + expect(restoredVec).not.toBe(vec); + expect(restoredVec).toEqual(vec); + + const mat = d.mat3x3f(1, 2, 3, 4, 5, 6, 7, 8, 9); + expect(roundTrip(mat, root)).toEqual(mat); + + const view = root + .createTexture({ size: [2, 2], format: 'rgba8unorm' }) + .$usage('sampled') + .createView(); + expect(snapshotResource(view)).toBeUndefined(); + expect(isNonTransferableResource(view)).toBe(true); + }); +}); diff --git a/packages/typegpu/tests/serializeDataSchema.test.ts b/packages/typegpu/tests/serializeDataSchema.test.ts new file mode 100644 index 0000000000..84377221c9 --- /dev/null +++ b/packages/typegpu/tests/serializeDataSchema.test.ts @@ -0,0 +1,283 @@ +import { describe, expect, it } from 'vitest'; +import { d } from 'typegpu'; +import { deepEqual } from 'typegpu/data'; +import { deserializeDataSchema, serializeDataSchema } from 'typegpu/~internal'; + +const schemas = [ + d.f32, + d.vec3f, + d.arrayOf( + d.struct({ + position: d.vec3f, + life: d.f32, + }), + 2, + ), + d.disarrayOf(d.unstruct({ id: d.u32, packed: d.uint16x2 }), 3), + d.atomic(d.u32), + d.align(16, d.size(32, d.struct({ value: d.vec2f }))), + d.location(2, d.vec4f), + d.interpolate('linear, sample', d.vec2f), + d.interpolate('flat, either', d.u32), + d.builtin.vertexIndex, + d.builtin.position, + d.invariant(d.builtin.position), + d.struct({ + position: d.invariant(d.builtin.position), + color: d.location(0, d.interpolate('linear, centroid', d.vec4f)), + index: d.location(1, d.interpolate('flat, either', d.u32)), + }), +]; + +describe('data schema serialization', () => { + it('serializes transferable schemas', () => { + expect(schemas.map(serializeDataSchema)).toMatchInlineSnapshot(` + [ + { + "key": "f32", + "type": "d", + }, + { + "key": "vec3f", + "type": "d", + }, + { + "count": 2, + "element": { + "props": [ + [ + "position", + { + "key": "vec3f", + "type": "d", + }, + ], + [ + "life", + { + "key": "f32", + "type": "d", + }, + ], + ], + "type": "struct", + }, + "type": "array", + }, + { + "count": 3, + "element": { + "props": [ + [ + "id", + { + "key": "u32", + "type": "d", + }, + ], + [ + "packed", + { + "key": "uint16x2", + "type": "d", + }, + ], + ], + "type": "unstruct", + }, + "type": "disarray", + }, + { + "inner": { + "key": "u32", + "type": "d", + }, + "type": "atomic", + }, + { + "attribs": [ + { + "type": "align", + "value": 16, + }, + { + "type": "size", + "value": 32, + }, + ], + "inner": { + "props": [ + [ + "value", + { + "key": "vec2f", + "type": "d", + }, + ], + ], + "type": "struct", + }, + "type": "decorated", + }, + { + "attribs": [ + { + "type": "location", + "value": 2, + }, + ], + "inner": { + "key": "vec4f", + "type": "d", + }, + "type": "decorated", + }, + { + "attribs": [ + { + "type": "interpolate", + "value": "linear, sample", + }, + ], + "inner": { + "key": "vec2f", + "type": "d", + }, + "type": "decorated", + }, + { + "attribs": [ + { + "type": "interpolate", + "value": "flat, either", + }, + ], + "inner": { + "key": "u32", + "type": "d", + }, + "type": "decorated", + }, + { + "attribs": [ + { + "type": "builtin", + "value": "vertex_index", + }, + ], + "inner": { + "key": "u32", + "type": "d", + }, + "type": "decorated", + }, + { + "attribs": [ + { + "type": "builtin", + "value": "position", + }, + ], + "inner": { + "key": "vec4f", + "type": "d", + }, + "type": "decorated", + }, + { + "attribs": [ + { + "type": "invariant", + }, + { + "type": "builtin", + "value": "position", + }, + ], + "inner": { + "key": "vec4f", + "type": "d", + }, + "type": "decorated", + }, + { + "props": [ + [ + "position", + { + "attribs": [ + { + "type": "invariant", + }, + { + "type": "builtin", + "value": "position", + }, + ], + "inner": { + "key": "vec4f", + "type": "d", + }, + "type": "decorated", + }, + ], + [ + "color", + { + "attribs": [ + { + "type": "location", + "value": 0, + }, + { + "type": "interpolate", + "value": "linear, centroid", + }, + ], + "inner": { + "key": "vec4f", + "type": "d", + }, + "type": "decorated", + }, + ], + [ + "index", + { + "attribs": [ + { + "type": "location", + "value": 1, + }, + { + "type": "interpolate", + "value": "flat, either", + }, + ], + "inner": { + "key": "u32", + "type": "d", + }, + "type": "decorated", + }, + ], + ], + "type": "struct", + }, + ] + `); + }); + + it('round-trips transferable schemas', () => { + for (const [index, schema] of schemas.entries()) { + const restored = deserializeDataSchema(serializeDataSchema(schema)); + expect(deepEqual(restored, schema), `schema #${index} (${schema.type})`).toBe(true); + } + }); + + it('rejects unsupported schemas', () => { + expect(() => serializeDataSchema(d.ptrFn(d.f32))).toThrowErrorMatchingInlineSnapshot( + `[Error: TypeGPU schema 'ptr' cannot be serialized yet.]`, + ); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f2fb169d7..0842b9997d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,10 +343,10 @@ importers: version: 7.1.0 jotai: specifier: ^2.15.0 - version: 2.15.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.6) + version: 2.15.0(@babel/core@7.29.0)(@babel/template@7.29.7)(@types/react@19.2.14)(react@19.2.6) jotai-location: specifier: ^0.6.2 - version: 0.6.2(jotai@2.15.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.6)) + version: 0.6.2(jotai@2.15.0(@babel/core@7.29.0)(@babel/template@7.29.7)(@types/react@19.2.14)(react@19.2.6)) lodash: specifier: ^4.18.1 version: 4.18.1 @@ -839,7 +839,7 @@ importers: dependencies: react-native-webgpu: specifier: '*' - version: 0.5.15(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.2.6))(react@19.2.6) + version: 0.5.15(react-native-worklets@0.10.2(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6))(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6) devDependencies: '@testing-library/dom': specifier: ^10.4.1 @@ -873,7 +873,10 @@ importers: version: 19.2.6(react@19.2.6) react-native: specifier: 0.84.1 - version: 0.84.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.2.6) + version: 0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6) + react-native-worklets: + specifier: 0.10.2 + version: 0.10.2(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6) tsdown: specifier: catalog:build version: 0.15.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(tsover@6.0.2)(unrun@0.2.31(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)) @@ -1177,10 +1180,18 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} @@ -1189,28 +1200,79 @@ packages: resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0-rc.2': resolution: {integrity: sha512-oCQ1IKPwkzCeJzAPb7Fv8rQ9k5+1sG8mf2uoHiMInPYvkRfrDJxbTIbH51U+jstlkghus0vAi3EBvkfvEsYNLQ==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.29.7': + resolution: {integrity: sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.6': resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.27.1': resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} @@ -1219,6 +1281,26 @@ packages: resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.29.7': + resolution: {integrity: sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -1227,12 +1309,20 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.0-rc.2': resolution: {integrity: sha512-xExUBkuXWJjVuIbO7z6q7/BA9bgfJDEhVL0ggrggLMbg0IzCUWGT1hZGE8qUH7Il7/RD/a6cZ3AAFrrlp1LF/A==} engines: {node: ^20.19.0 || >=22.12.0} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.29.7': + resolution: {integrity: sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==} engines: {node: '>=6.9.0'} '@babel/helpers@7.28.6': @@ -1269,6 +1359,12 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + '@babel/plugin-proposal-export-default-from@7.29.7': + resolution: {integrity: sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -1290,6 +1386,23 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-export-default-from@7.29.7': + resolution: {integrity: sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.29.7': + resolution: {integrity: sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-attributes@7.28.6': resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} engines: {node: '>=6.9.0'} @@ -1306,6 +1419,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -1348,6 +1467,114 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-arrow-functions@7.29.7': + resolution: {integrity: sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.7': + resolution: {integrity: sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.29.7': + resolution: {integrity: sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.29.7': + resolution: {integrity: sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.29.7': + resolution: {integrity: sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-classes@7.29.7': + resolution: {integrity: sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.29.7': + resolution: {integrity: sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.29.7': + resolution: {integrity: sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.29.7': + resolution: {integrity: sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7': + resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.29.7': + resolution: {integrity: sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.29.7': + resolution: {integrity: sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.29.7': + resolution: {integrity: sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.29.7': + resolution: {integrity: sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.29.7': + resolution: {integrity: sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -1360,6 +1587,54 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx@7.29.7': + resolution: {integrity: sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.7': + resolution: {integrity: sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.29.7': + resolution: {integrity: sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.29.7': + resolution: {integrity: sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.29.7': + resolution: {integrity: sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.29.7': + resolution: {integrity: sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.29.7': + resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.26.9': resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} engines: {node: '>=6.9.0'} @@ -1372,10 +1647,18 @@ packages: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -3313,12 +3596,28 @@ packages: resolution: {integrity: sha512-lAJ6PDZv95FdT9s9uhc9ivhikW1Zwh4j9XdXM7J2l4oUA3t37qfoBmTSDLuPyE3Bi+Xtwa11hJm0BUTT2sc/gg==} engines: {node: '>= 20.19.4'} + '@react-native/babel-plugin-codegen@0.86.0': + resolution: {integrity: sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/babel-preset@0.86.0': + resolution: {integrity: sha512-bYQcWiPySNvF4dns9Ls9gMmwgq66ohvM9Fwc/Kn8r85t66UNHxch3p1QwPiSorDelFauZwJbgo9+ReibTgvpbA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + peerDependencies: + '@babel/core': '*' + '@react-native/codegen@0.84.1': resolution: {integrity: sha512-n1RIU0QAavgCg1uC5+s53arL7/mpM+16IBhJ3nCFSd/iK5tUmCwxQDcIDC703fuXfpub/ZygeSjVN8bcOWn0gA==} engines: {node: '>= 20.19.4'} peerDependencies: '@babel/core': '*' + '@react-native/codegen@0.86.0': + resolution: {integrity: sha512-uTs9DBo3+/lUqinsGZK0FKJRBVClrwMXoZToaDxE1Q2SL2e55vs2GwyZfIKzPl5uJnbu4PfFMIp0/mLXLWUMuA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + peerDependencies: + '@babel/core': '*' + '@react-native/community-cli-plugin@0.84.1': resolution: {integrity: sha512-f6a+mJEJ6Joxlt/050TqYUr7uRRbeKnz8lnpL7JajhpsgZLEbkJRjH8HY5QiLcRdUwWFtizml4V+vcO3P4RxoQ==} engines: {node: '>= 20.19.4'} @@ -3351,6 +3650,20 @@ packages: resolution: {integrity: sha512-UsTe2AbUugsfyI7XIHMQq4E7xeC8a6GrYwuK+NohMMMJMxmyM3JkzIk+GB9e2il6ScEQNMJNaj+q+i5za8itxQ==} engines: {node: '>= 20.19.4'} + '@react-native/js-polyfills@0.86.0': + resolution: {integrity: sha512-zYy/Cjd1VTnZ2iCNaG9bDF9C3l2ntESiPRscjIlI5FKugu6aeTwsDSv1aI8Bc4Kp3vEdoVg+UQhLAhE4svREaQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/metro-babel-transformer@0.86.0': + resolution: {integrity: sha512-SjKej3E5qIahqo/G+rSOrmJUQM44RyKtWtO+VfmKAAMoJWkBFomM22hTLKCIS5cdbIAJ9COAmU+KAi2wVSO0wQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + peerDependencies: + '@babel/core': '*' + + '@react-native/metro-config@0.86.0': + resolution: {integrity: sha512-7v+xbTeEci9ZcQ/Z1OqI4RXcqN69wSMDYL5BAMvOReZ7U04+aDQ0/SQhClYPn6x2/RxM4WzMKSAuNyLKqvYVtw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + '@react-native/normalize-colors@0.84.1': resolution: {integrity: sha512-/UPaQ4jl95soXnLDEJ6Cs6lnRXhwbxtT4KbZz+AFDees7prMV2NOLcHfCnzmTabf5Y3oxENMVBL666n4GMLcTA==} @@ -4851,9 +5164,30 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-syntax-hermes-parser@0.32.0: resolution: {integrity: sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==} + babel-plugin-syntax-hermes-parser@0.36.0: + resolution: {integrity: sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q==} + + babel-plugin-transform-flow-enums@0.0.2: + resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==} + babel-preset-current-node-syntax@1.2.0: resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} peerDependencies: @@ -5187,6 +5521,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + cosmiconfig@9.0.1: resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} @@ -6114,12 +6451,24 @@ packages: hermes-estree@0.33.3: resolution: {integrity: sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==} + hermes-estree@0.35.0: + resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==} + + hermes-estree@0.36.0: + resolution: {integrity: sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==} + hermes-parser@0.32.0: resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} hermes-parser@0.33.3: resolution: {integrity: sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==} + hermes-parser@0.35.0: + resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==} + + hermes-parser@0.36.0: + resolution: {integrity: sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -6718,6 +7067,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -6934,60 +7286,118 @@ packages: resolution: {integrity: sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA==} engines: {node: '>=20.19.4'} + metro-babel-transformer@0.84.4: + resolution: {integrity: sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-cache-key@0.83.5: resolution: {integrity: sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw==} engines: {node: '>=20.19.4'} + metro-cache-key@0.84.4: + resolution: {integrity: sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-cache@0.83.5: resolution: {integrity: sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng==} engines: {node: '>=20.19.4'} + metro-cache@0.84.4: + resolution: {integrity: sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-config@0.83.5: resolution: {integrity: sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w==} engines: {node: '>=20.19.4'} + metro-config@0.84.4: + resolution: {integrity: sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-core@0.83.5: resolution: {integrity: sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ==} engines: {node: '>=20.19.4'} + metro-core@0.84.4: + resolution: {integrity: sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-file-map@0.83.5: resolution: {integrity: sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ==} engines: {node: '>=20.19.4'} + metro-file-map@0.84.4: + resolution: {integrity: sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-minify-terser@0.83.5: resolution: {integrity: sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q==} engines: {node: '>=20.19.4'} + metro-minify-terser@0.84.4: + resolution: {integrity: sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-resolver@0.83.5: resolution: {integrity: sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A==} engines: {node: '>=20.19.4'} + metro-resolver@0.84.4: + resolution: {integrity: sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-runtime@0.83.5: resolution: {integrity: sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA==} engines: {node: '>=20.19.4'} + metro-runtime@0.84.4: + resolution: {integrity: sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-source-map@0.83.5: resolution: {integrity: sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ==} engines: {node: '>=20.19.4'} + metro-source-map@0.84.4: + resolution: {integrity: sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-symbolicate@0.83.5: resolution: {integrity: sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA==} engines: {node: '>=20.19.4'} hasBin: true + metro-symbolicate@0.84.4: + resolution: {integrity: sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + metro-transform-plugins@0.83.5: resolution: {integrity: sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw==} engines: {node: '>=20.19.4'} + metro-transform-plugins@0.84.4: + resolution: {integrity: sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro-transform-worker@0.83.5: resolution: {integrity: sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA==} engines: {node: '>=20.19.4'} + metro-transform-worker@0.84.4: + resolution: {integrity: sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + metro@0.83.5: resolution: {integrity: sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ==} engines: {node: '>=20.19.4'} hasBin: true + metro@0.84.4: + resolution: {integrity: sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + mhchemparser@4.2.1: resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} @@ -7306,6 +7716,10 @@ packages: resolution: {integrity: sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg==} engines: {node: '>=20.19.4'} + ob1@0.84.4: + resolution: {integrity: sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -7827,6 +8241,14 @@ packages: react-native-worklets: optional: true + react-native-worklets@0.10.2: + resolution: {integrity: sha512-LX27ejYI8veeDp59Z3rjo2pYyPa9euzSH8GUlem7cnNqfsDtGum8PQpkbzrqhLsWH0CjdeHR7p3sncCyYbwaVw==} + peerDependencies: + '@babel/core': '*' + '@react-native/metro-config': '*' + react: '*' + react-native: 0.83 - 0.86 + react-native@0.84.1: resolution: {integrity: sha512-0PjxOyXRu3tZ8EobabxSukvhKje2HJbsZikR0U+pvS0pYZza2hXKjcSBiBdFN4h9D0S3v6a8kkrDK6WTRKMwzg==} engines: {node: '>= 20.19.4'} @@ -7908,6 +8330,13 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} @@ -7923,6 +8352,17 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.2: + resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} + hasBin: true + rehype-expressive-code@0.41.7: resolution: {integrity: sha512-25f8ZMSF1d9CMscX7Cft0TSQIqdwjce2gDOvQ+d/w0FovsMwrSt3ODP4P3Z7wO1jsIJ4eYyaDRnIR/27bd/EMQ==} @@ -8787,10 +9227,26 @@ packages: resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} engines: {node: '>=4'} + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -9738,8 +10194,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.29.7': {} + '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -9768,6 +10232,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/generator@8.0.0-rc.2': dependencies: '@babel/parser': 8.0.0-rc.3 @@ -9777,19 +10249,78 @@ snapshots: '@types/jsesc': 2.5.1 jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.0 + '@babel/helper-compilation-targets@7.28.6': dependencies: '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 + '@babel/helper-validator-option': 7.29.7 browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.29.7 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -9799,21 +10330,71 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.0 + '@babel/helper-plugin-utils@7.27.1': {} '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-wrap-function': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-identifier@8.0.0-rc.2': {} - '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helper-wrap-function@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color '@babel/helpers@7.28.6': dependencies: @@ -9844,80 +10425,234 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/plugin-proposal-export-default-from@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-export-default-from@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-flow@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-flow-strip-types@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.0) + + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: @@ -9929,6 +10664,72 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/preset-typescript@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/runtime@7.26.9': dependencies: regenerator-runtime: 0.14.1 @@ -9941,6 +10742,12 @@ snapshots: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.0 + '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -9953,6 +10760,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -11439,6 +12258,52 @@ snapshots: '@react-native/assets-registry@0.84.1': {} + '@react-native/babel-plugin-codegen@0.86.0(@babel/core@7.29.0)': + dependencies: + '@babel/traverse': 7.29.7 + '@react-native/codegen': 0.86.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@react-native/babel-preset@0.86.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.0) + '@react-native/babel-plugin-codegen': 0.86.0(@babel/core@7.29.0) + babel-plugin-syntax-hermes-parser: 0.36.0 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) + react-refresh: 0.14.2 + transitivePeerDependencies: + - supports-color + '@react-native/codegen@0.84.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -11449,7 +12314,17 @@ snapshots: tinyglobby: 0.2.16 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.84.1': + '@react-native/codegen@0.86.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.7 + hermes-parser: 0.36.0 + invariant: 2.2.4 + nullthrows: 1.1.1 + tinyglobby: 0.2.16 + yargs: 17.7.2 + + '@react-native/community-cli-plugin@0.84.1(@react-native/metro-config@0.86.0(@babel/core@7.29.0))': dependencies: '@react-native/dev-middleware': 0.84.1 debug: 4.4.3 @@ -11458,6 +12333,8 @@ snapshots: metro-config: 0.83.5 metro-core: 0.83.5 semver: 7.7.4 + optionalDependencies: + '@react-native/metro-config': 0.86.0(@babel/core@7.29.0) transitivePeerDependencies: - bufferutil - supports-color @@ -11496,14 +12373,37 @@ snapshots: '@react-native/js-polyfills@0.84.1': {} + '@react-native/js-polyfills@0.86.0': {} + + '@react-native/metro-babel-transformer@0.86.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@react-native/babel-preset': 0.86.0(@babel/core@7.29.0) + hermes-parser: 0.36.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@react-native/metro-config@0.86.0(@babel/core@7.29.0)': + dependencies: + '@react-native/js-polyfills': 0.86.0 + '@react-native/metro-babel-transformer': 0.86.0(@babel/core@7.29.0) + metro-config: 0.84.4 + metro-runtime: 0.84.4 + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - supports-color + - utf-8-validate + '@react-native/normalize-colors@0.84.1': {} - '@react-native/virtualized-lists@0.84.1(@types/react@19.1.8)(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.2.6))(react@19.2.6)': + '@react-native/virtualized-lists@0.84.1(@types/react@19.1.8)(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.6 - react-native: 0.84.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.2.6) + react-native: 0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6) optionalDependencies: '@types/react': 19.1.8 @@ -13005,7 +13905,7 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -13020,10 +13920,44 @@ snapshots: '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.7 + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + babel-plugin-syntax-hermes-parser@0.32.0: dependencies: hermes-parser: 0.32.0 + babel-plugin-syntax-hermes-parser@0.36.0: + dependencies: + hermes-parser: 0.36.0 + + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): + dependencies: + '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -13351,6 +14285,10 @@ snapshots: cookie@1.1.1: {} + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.1 + cosmiconfig@9.0.1(tsover@6.0.2): dependencies: env-paths: 2.2.1 @@ -14519,6 +15457,10 @@ snapshots: hermes-estree@0.33.3: {} + hermes-estree@0.35.0: {} + + hermes-estree@0.36.0: {} + hermes-parser@0.32.0: dependencies: hermes-estree: 0.32.0 @@ -14527,6 +15469,14 @@ snapshots: dependencies: hermes-estree: 0.33.3 + hermes-parser@0.35.0: + dependencies: + hermes-estree: 0.35.0 + + hermes-parser@0.36.0: + dependencies: + hermes-estree: 0.36.0 + hookable@5.5.3: {} hookable@6.0.1: {} @@ -14706,7 +15656,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -14831,14 +15781,14 @@ snapshots: jiti@2.6.1: {} - jotai-location@0.6.2(jotai@2.15.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.6)): + jotai-location@0.6.2(jotai@2.15.0(@babel/core@7.29.0)(@babel/template@7.29.7)(@types/react@19.2.14)(react@19.2.6)): dependencies: - jotai: 2.15.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.6) + jotai: 2.15.0(@babel/core@7.29.0)(@babel/template@7.29.7)(@types/react@19.2.14)(react@19.2.6) - jotai@2.15.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.6): + jotai@2.15.0(@babel/core@7.29.0)(@babel/template@7.29.7)(@types/react@19.2.14)(react@19.2.6): optionalDependencies: '@babel/core': 7.29.0 - '@babel/template': 7.28.6 + '@babel/template': 7.29.7 '@types/react': 19.2.14 react: 19.2.6 @@ -15064,6 +16014,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.debounce@4.0.8: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} @@ -15413,10 +16365,24 @@ snapshots: transitivePeerDependencies: - supports-color + metro-babel-transformer@0.84.4: + dependencies: + '@babel/core': 7.29.0 + flow-enums-runtime: 0.0.6 + hermes-parser: 0.35.0 + metro-cache-key: 0.84.4 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + metro-cache-key@0.83.5: dependencies: flow-enums-runtime: 0.0.6 + metro-cache-key@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + metro-cache@0.83.5: dependencies: exponential-backoff: 3.1.3 @@ -15426,6 +16392,15 @@ snapshots: transitivePeerDependencies: - supports-color + metro-cache@0.84.4: + dependencies: + exponential-backoff: 3.1.3 + flow-enums-runtime: 0.0.6 + https-proxy-agent: 7.0.6 + metro-core: 0.84.4 + transitivePeerDependencies: + - supports-color + metro-config@0.83.5: dependencies: connect: 3.7.0 @@ -15441,12 +16416,33 @@ snapshots: - supports-color - utf-8-validate + metro-config@0.84.4: + dependencies: + connect: 3.7.0 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.84.4 + metro-cache: 0.84.4 + metro-core: 0.84.4 + metro-runtime: 0.84.4 + yaml: 2.8.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + metro-core@0.83.5: dependencies: flow-enums-runtime: 0.0.6 lodash.throttle: 4.1.1 metro-resolver: 0.83.5 + metro-core@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.84.4 + metro-file-map@0.83.5: dependencies: debug: 4.4.3 @@ -15461,20 +16457,48 @@ snapshots: transitivePeerDependencies: - supports-color + metro-file-map@0.84.4: + dependencies: + debug: 4.4.3 + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + metro-minify-terser@0.83.5: dependencies: flow-enums-runtime: 0.0.6 terser: 5.44.1 + metro-minify-terser@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.44.1 + metro-resolver@0.83.5: dependencies: flow-enums-runtime: 0.0.6 + metro-resolver@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + metro-runtime@0.83.5: dependencies: '@babel/runtime': 7.26.9 flow-enums-runtime: 0.0.6 + metro-runtime@0.84.4: + dependencies: + '@babel/runtime': 7.26.9 + flow-enums-runtime: 0.0.6 + metro-source-map@0.83.5: dependencies: '@babel/traverse': 7.29.0 @@ -15489,6 +16513,20 @@ snapshots: transitivePeerDependencies: - supports-color + metro-source-map@0.84.4: + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.0 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.84.4 + nullthrows: 1.1.1 + ob1: 0.84.4 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + metro-symbolicate@0.83.5: dependencies: flow-enums-runtime: 0.0.6 @@ -15500,12 +16538,34 @@ snapshots: transitivePeerDependencies: - supports-color + metro-symbolicate@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.84.4 + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + metro-transform-plugins@0.83.5: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.84.4: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: @@ -15531,6 +16591,26 @@ snapshots: - supports-color - utf-8-validate + metro-transform-worker@0.84.4: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.0 + flow-enums-runtime: 0.0.6 + metro: 0.84.4 + metro-babel-transformer: 0.84.4 + metro-cache: 0.84.4 + metro-cache-key: 0.84.4 + metro-minify-terser: 0.84.4 + metro-source-map: 0.84.4 + metro-transform-plugins: 0.84.4 + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + metro@0.83.5: dependencies: '@babel/code-frame': 7.29.0 @@ -15578,6 +16658,52 @@ snapshots: - supports-color - utf-8-validate + metro@0.84.4: + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.0 + accepts: 2.0.0 + ci-info: 2.0.0 + connect: 3.7.0 + debug: 4.4.3 + error-stack-parser: 2.1.4 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + hermes-parser: 0.35.0 + image-size: 1.2.1 + invariant: 2.2.4 + jest-worker: 29.7.0 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.84.4 + metro-cache: 0.84.4 + metro-cache-key: 0.84.4 + metro-config: 0.84.4 + metro-core: 0.84.4 + metro-file-map: 0.84.4 + metro-resolver: 0.84.4 + metro-runtime: 0.84.4 + metro-source-map: 0.84.4 + metro-symbolicate: 0.84.4 + metro-transform-plugins: 0.84.4 + metro-transform-worker: 0.84.4 + mime-types: 3.0.2 + nullthrows: 1.1.1 + serialize-error: 2.1.0 + source-map: 0.5.7 + throat: 5.0.0 + ws: 7.5.10 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + mhchemparser@4.2.1: {} micromark-core-commonmark@2.0.3: @@ -16042,6 +17168,10 @@ snapshots: dependencies: flow-enums-runtime: 0.0.6 + ob1@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + obug@2.1.1: {} ofetch@1.5.1: @@ -16607,21 +17737,44 @@ snapshots: react-is@18.3.1: {} - react-native-webgpu@0.5.15(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.2.6))(react@19.2.6): + react-native-webgpu@0.5.15(react-native-worklets@0.10.2(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6))(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + react-native: 0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6) + optionalDependencies: + react-native-worklets: 0.10.2(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6) + + react-native-worklets@0.10.2(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6): dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.0) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@react-native/metro-config': 0.86.0(@babel/core@7.29.0) + convert-source-map: 2.0.0 react: 19.2.6 - react-native: 0.84.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.2.6) + react-native: 0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6) + semver: 7.7.4 + transitivePeerDependencies: + - supports-color - react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.2.6): + react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.84.1 '@react-native/codegen': 0.84.1(@babel/core@7.29.0) - '@react-native/community-cli-plugin': 0.84.1 + '@react-native/community-cli-plugin': 0.84.1(@react-native/metro-config@0.86.0(@babel/core@7.29.0)) '@react-native/gradle-plugin': 0.84.1 '@react-native/js-polyfills': 0.84.1 '@react-native/normalize-colors': 0.84.1 - '@react-native/virtualized-lists': 0.84.1(@types/react@19.1.8)(react-native@0.84.1(@babel/core@7.29.0)(@types/react@19.1.8)(react@19.2.6))(react@19.2.6) + '@react-native/virtualized-lists': 0.84.1(@types/react@19.1.8)(react-native@0.84.1(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.8)(react@19.2.6))(react@19.2.6) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -16737,6 +17890,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + regenerator-runtime@0.13.11: {} regenerator-runtime@0.14.1: {} @@ -16751,6 +17910,21 @@ snapshots: dependencies: regex-utilities: 2.3.0 + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.2 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.2: + dependencies: + jsesc: 3.1.0 + rehype-expressive-code@0.41.7: dependencies: expressive-code: 0.41.7 @@ -17821,8 +18995,19 @@ snapshots: undici@6.23.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} + unicode-emoji-modifier-base@1.0.0: {} + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: From 7a0d625089c43ed0aa281f79de926051752b618b Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Thu, 16 Jul 2026 15:06:31 +0200 Subject: [PATCH 2/4] auto-detect worklets --- .../integration/react-native/worklets.mdx | 23 ++++--- packages/typegpu-react/README.md | 2 +- packages/typegpu-react/package.json | 10 ---- .../typegpu-react/src/core/root-context.tsx | 20 ++++++- packages/typegpu-react/src/core/use-frame.ts | 60 ++++++++++--------- .../src/react-native/core/use-frame.ts | 41 ++++++++----- .../typegpu-react/src/react-native/index.ts | 10 ++++ .../serialization/register-serializables.ts | 11 +++- .../use-configure-worklet-context.ts | 26 ++++++-- .../src/react-native/worklets-integration.ts | 25 ++++++++ .../src/react-native/worklets.ts | 14 ----- .../register-serializables.test.ts | 10 ++-- .../tests/react-native/use-frame.test.tsx | 58 ++++++++++++++++++ packages/typegpu-react/tsdown.config.ts | 17 +++++- 14 files changed, 238 insertions(+), 89 deletions(-) create mode 100644 packages/typegpu-react/src/react-native/worklets-integration.ts delete mode 100644 packages/typegpu-react/src/react-native/worklets.ts create mode 100644 packages/typegpu-react/tests/react-native/use-frame.test.tsx diff --git a/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx index d2eb51defc..9b5ca7793a 100644 --- a/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx +++ b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx @@ -32,11 +32,8 @@ module.exports = (api) => { }; ``` -Import the hooks from the dedicated entrypoint - importing it also registers the transfer support for TypeGPU resources: - -```ts -import { useRoot, useFrame, useConfigureContext, useUniform } from '@typegpu/react/react-native-worklets'; -``` +No extra imports are needed - `@typegpu/react` detects `react-native-worklets` at runtime and registers the transfer support for TypeGPU resources automatically. +`useFrame` runs its callback on the UI thread whenever the callback is marked with the `'worklet'` directive; plain callbacks keep running on the JS thread. After changing the babel config, clear the Metro cache with `npx expo start --clear`. @@ -48,7 +45,7 @@ Create resources on the JS thread, then use them freely inside a `useFrame` work import { useMemo } from 'react'; import { Canvas } from 'react-native-webgpu'; import tgpu, { common, d } from 'typegpu'; -import { useConfigureContext, useFrame, useRoot, useUniform } from '@typegpu/react/react-native-worklets'; +import { useConfigureWorkletContext, useFrame, useRoot, useUniform } from '@typegpu/react'; export function Pulse() { const root = useRoot(); @@ -66,7 +63,7 @@ export function Pulse() { [root, color], ); - const { ref, ctxRef } = useConfigureContext({ alphaMode: 'premultiplied' }); + const { ref, ctxRef } = useConfigureWorkletContext({ alphaMode: 'premultiplied' }); // Runs each frame on the UI thread useFrame(({ elapsedSeconds }) => { @@ -88,6 +85,18 @@ Both runtimes share the same underlying GPU objects, and transferring the same r This works for buffers (including `createUniform`/`createMutable`/`createReadonly`), textures, samplers, bind groups and their layouts, vertex layouts, query sets, pipelines, roots, slots, accessors, consts, and vector/matrix instances. +## Opting out + +To keep everything on the JS thread even with `react-native-worklets` installed, pass `disableWorklets` to the `Root` provider: + +```tsx + + + +``` + +`useFrame` then runs its callbacks on the JS thread, and `useConfigureWorkletContext` returns a plain object of the same shape, so the code above keeps working unchanged. + ## Rules of transfer **Definitions are runtime-local.** diff --git a/packages/typegpu-react/README.md b/packages/typegpu-react/README.md index 0b476a5d30..9ec734cdd2 100644 --- a/packages/typegpu-react/README.md +++ b/packages/typegpu-react/README.md @@ -48,7 +48,7 @@ const App = (props: Props) => { # React Native -The `@typegpu/react/react-native-worklets` entrypoint lets per-frame GPU work run on the UI thread. +When `react-native-worklets` is installed, per-frame GPU work can run on the UI thread - `useFrame` picks it up automatically for worklet callbacks. TypeGPU resources captured by worklets are transferred between runtimes automatically. See the [React Native Worklets guide](https://typegpu.com/integration/react-native/worklets). diff --git a/packages/typegpu-react/package.json b/packages/typegpu-react/package.json index f5b8e0ada0..383aaf7ad4 100644 --- a/packages/typegpu-react/package.json +++ b/packages/typegpu-react/package.json @@ -22,11 +22,6 @@ "browser": "./src/browser/index.ts", "default": "./src/browser/index.ts" }, - "./react-native-worklets": { - "types": "./src/react-native/worklets.ts", - "react-native": "./src/react-native/worklets.ts", - "default": "./src/react-native/worklets.ts" - }, "./package.json": "./package.json" }, "publishConfig": { @@ -38,11 +33,6 @@ "browser": "./dist/browser/index.js", "react-native": "./dist/react-native/index.js", "default": "./dist/browser/index.js" - }, - "./react-native-worklets": { - "types": "./dist/react-native/worklets.d.ts", - "react-native": "./dist/react-native/worklets.js", - "default": "./dist/react-native/worklets.js" } }, "linkDirectory": false, diff --git a/packages/typegpu-react/src/core/root-context.tsx b/packages/typegpu-react/src/core/root-context.tsx index 2048a9cabd..ba64510203 100644 --- a/packages/typegpu-react/src/core/root-context.tsx +++ b/packages/typegpu-react/src/core/root-context.tsx @@ -161,6 +161,13 @@ const globalRootContextValue = new OwnRootContext(); const rootContext = createContext(null); +const workletsDisabledContext = createContext(false); + +/** @internal Reads the `disableWorklets` flag from the nearest provider */ +export function useWorkletsDisabled(): boolean { + return useContext(workletsDisabledContext); +} + export interface RootProps { /** Options used when this provider creates its own root, ignored when `root` is provided */ options?: InitOptions | undefined; @@ -171,6 +178,13 @@ export interface RootProps { * @default undefined */ root?: TgpuRoot | undefined; + /** + * (React Native only) When true, `useFrame` runs on the JS thread even if + * `react-native-worklets` is installed. Ignored on the web + * + * @default false + */ + disableWorklets?: boolean | undefined; children?: ReactNode | undefined; } @@ -190,7 +204,7 @@ function WarnSuspense() { return null; } -export const Root = ({ children, options, root }: RootProps) => { +export const Root = ({ children, options, root, disableWorklets = false }: RootProps) => { const [ownCtx] = useState(() => new OwnRootContext(options)); const existingRootCtx = useMemo(() => { if (root) { @@ -205,7 +219,9 @@ export const Root = ({ children, options, root }: RootProps) => { return ( - }>{children} + + }>{children} + ); }; diff --git a/packages/typegpu-react/src/core/use-frame.ts b/packages/typegpu-react/src/core/use-frame.ts index c47498c729..d69e93871f 100644 --- a/packages/typegpu-react/src/core/use-frame.ts +++ b/packages/typegpu-react/src/core/use-frame.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; -interface FrameCtx { +export interface FrameCtx { /** * Time elapsed since the last frame */ @@ -11,6 +11,35 @@ interface FrameCtx { readonly elapsedSeconds: number; } +export function startFrameLoop(cb: (ctx: FrameCtx) => void): () => void { + let frameId: number | undefined; + let startTime: number | undefined; + let lastTime: number | undefined; + + const loop = () => { + frameId = requestAnimationFrame(loop); + + const now = performance.now(); + if (lastTime === undefined || startTime === undefined) { + startTime = now; + lastTime = now; + } + cb({ + deltaSeconds: (now - lastTime) / 1000, + elapsedSeconds: (now - startTime) / 1000, + }); + lastTime = now; + }; + + loop(); + + return () => { + if (frameId !== undefined) { + cancelAnimationFrame(frameId); + } + }; +} + export function useFrame(cb: (ctx: FrameCtx) => void) { const latestCb = useRef(cb); @@ -18,32 +47,5 @@ export function useFrame(cb: (ctx: FrameCtx) => void) { latestCb.current = cb; }, [cb]); - useEffect(() => { - let frameId: number | undefined; - let startTime: number | undefined; - let lastTime: number | undefined; - - const loop = () => { - frameId = requestAnimationFrame(loop); - - const now = performance.now(); - if (lastTime === undefined || startTime === undefined) { - startTime = now; - lastTime = now; - } - latestCb.current({ - deltaSeconds: (now - lastTime) / 1000, - elapsedSeconds: (now - startTime) / 1000, - }); - lastTime = now; - }; - - loop(); - - return () => { - if (frameId !== undefined) { - cancelAnimationFrame(frameId); - } - }; - }, []); + useEffect(() => startFrameLoop((ctx) => latestCb.current(ctx)), []); } diff --git a/packages/typegpu-react/src/react-native/core/use-frame.ts b/packages/typegpu-react/src/react-native/core/use-frame.ts index 3c0d48a320..ff2abea437 100644 --- a/packages/typegpu-react/src/react-native/core/use-frame.ts +++ b/packages/typegpu-react/src/react-native/core/use-frame.ts @@ -1,10 +1,8 @@ import { useEffect, useRef } from 'react'; -import { runOnUISync, createShareable, UIRuntimeId } from 'react-native-worklets'; -interface FrameCtx { - readonly deltaSeconds: number; - readonly elapsedSeconds: number; -} +import { useWorkletsDisabled } from '../../core/root-context.tsx'; +import { type FrameCtx, startFrameLoop } from '../../core/use-frame.ts'; +import { getWorkletsModule } from '../worklets-integration.ts'; type FrameCallback = (ctx: FrameCtx) => void; type FrameCallbackRef = { current: FrameCallback }; @@ -13,17 +11,36 @@ type UiValue = { setSync(value: T | ((prev: T) => T)): void; }; +/** + * Runs the frame loop on the UI runtime when the callback is a worklet and + * `react-native-worklets` is available, on the JS thread otherwise + */ export function useFrame(cb: FrameCallback) { - const latestCb = useRef | undefined>(undefined); + const workletsDisabled = useWorkletsDisabled(); + const worklets = getWorkletsModule(); + const runOnUI = worklets !== null && !workletsDisabled && worklets.isWorkletFunction(cb); + + const latestCb = useRef(cb); + const uiCbRef = useRef | undefined>(undefined); + + useEffect(() => { + latestCb.current = cb; + uiCbRef.current?.setSync({ current: cb }); + }, [cb]); useEffect(() => { + if (!runOnUI || !worklets) { + return startFrameLoop((ctx) => latestCb.current(ctx)); + } + + const { runOnUISync, createShareable, UIRuntimeId } = worklets; const cbRef = createShareable( UIRuntimeId, - { current: cb }, + { current: latestCb.current }, { initSynchronously: true }, ) as UiValue; const frameId = createShareable(UIRuntimeId, undefined) as UiValue; - latestCb.current = cbRef; + uiCbRef.current = cbRef; runOnUISync(() => { 'worklet'; @@ -49,7 +66,7 @@ export function useFrame(cb: FrameCallback) { }); return () => { - latestCb.current = undefined; + uiCbRef.current = undefined; runOnUISync(() => { 'worklet'; if (frameId.value !== undefined) { @@ -57,9 +74,5 @@ export function useFrame(cb: FrameCallback) { } }); }; - }, []); - - useEffect(() => { - latestCb.current?.setSync({ current: cb }); - }, [cb]); + }, [runOnUI, worklets]); } diff --git a/packages/typegpu-react/src/react-native/index.ts b/packages/typegpu-react/src/react-native/index.ts index bfb514da65..7e9c594ca3 100644 --- a/packages/typegpu-react/src/react-native/index.ts +++ b/packages/typegpu-react/src/react-native/index.ts @@ -1,8 +1,18 @@ import { WebGPUModule } from 'react-native-webgpu'; +import { registerTypegpuReactSerializables } from './serialization/register-serializables.ts'; + // Making sure the WebGPU module is installed before navigator.gpu is accessed WebGPUModule.install(); +// No-ops when react-native-worklets is not installed +registerTypegpuReactSerializables(); export * from '../shared-exports.ts'; export { useConfigureContext } from './use-configure-context.ts'; +// Intentionally shadows the browser `useFrame`, this one can run the frame loop on the UI runtime +export { useFrame } from './core/use-frame.ts'; +export { + useConfigureWorkletContext, + type WorkletCanvasContextRef, +} from './use-configure-worklet-context.ts'; diff --git a/packages/typegpu-react/src/react-native/serialization/register-serializables.ts b/packages/typegpu-react/src/react-native/serialization/register-serializables.ts index fa4d64b8b9..630a90e910 100644 --- a/packages/typegpu-react/src/react-native/serialization/register-serializables.ts +++ b/packages/typegpu-react/src/react-native/serialization/register-serializables.ts @@ -1,5 +1,4 @@ import { installWebGPU } from 'react-native-webgpu'; -import { isWorkletFunction, registerCustomSerializable } from 'react-native-worklets'; import { isNonTransferableResource, isSnapshotableResource, @@ -13,6 +12,7 @@ import { getOrCreateTransferId, getTransferredRoot, } from './transfer-cache.ts'; +import { getWorkletsModule } from '../worklets-integration.ts'; export type PackedTgpuResource = { id: number; @@ -25,9 +25,13 @@ export function registerTypegpuReactSerializables(): void { if (registered) { return; } + const worklets = getWorkletsModule(); + if (!worklets) { + return; + } registered = true; - registerCustomSerializable({ + worklets.registerCustomSerializable({ name: 'TypeGPU', determine(value: object): value is object { 'worklet'; @@ -46,9 +50,10 @@ export function registerTypegpuReactSerializables(): void { ); } for (const [key, field] of Object.entries(snapshot)) { + // Inlined isWorkletFunction, the lazily resolved module cannot be captured in a worklet if ( typeof field === 'function' && - !isWorkletFunction(field) && + !(field as { __workletHash?: unknown }).__workletHash && !(field as { __bundleData?: unknown }).__bundleData ) { throw new Error( diff --git a/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts b/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts index f2091ed868..520d4270d2 100644 --- a/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts +++ b/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts @@ -1,8 +1,9 @@ import { useEffect, useRef } from 'react'; -import { createShareable, UIRuntimeId } from 'react-native-worklets'; +import { useWorkletsDisabled } from '../core/root-context.tsx'; import type { CanvasRef, UseConfigureContextOptions } from '../core/use-configure-context.ts'; import { useConfigureContext } from './use-configure-context.ts'; +import { getWorkletsModule } from './worklets-integration.ts'; type CanvasContext = GPUCanvasContext & { present?: () => void }; @@ -11,15 +12,32 @@ export type WorkletCanvasContextRef = { setSync(value: CanvasContext | null): void; }; +/** + * Same as `useConfigureContext`, but exposes the canvas context through a ref readable + * on the UI runtime, or a same-shape JS-thread object when worklets are unavailable or + * disabled. The choice is fixed on mount, flipping `disableWorklets` requires a remount + */ export function useConfigureWorkletContext(options?: UseConfigureContextOptions): { ref: React.RefCallback; ctxRef: WorkletCanvasContextRef; } { const result = useConfigureContext(options); + const workletsDisabled = useWorkletsDisabled(); const workletCtxRef = useRef(undefined); - workletCtxRef.current ??= createShareable(UIRuntimeId, null, { - initSynchronously: true, - }) as WorkletCanvasContextRef; + + if (workletCtxRef.current === undefined) { + const worklets = workletsDisabled ? null : getWorkletsModule(); + workletCtxRef.current = worklets + ? (worklets.createShareable(worklets.UIRuntimeId, null, { + initSynchronously: true, + }) as WorkletCanvasContextRef) + : { + value: null, + setSync(value) { + this.value = value; + }, + }; + } const ctxRef = workletCtxRef.current; diff --git a/packages/typegpu-react/src/react-native/worklets-integration.ts b/packages/typegpu-react/src/react-native/worklets-integration.ts new file mode 100644 index 0000000000..e77b52f39e --- /dev/null +++ b/packages/typegpu-react/src/react-native/worklets-integration.ts @@ -0,0 +1,25 @@ +type WorkletsModule = typeof import('react-native-worklets'); + +declare const require: (id: string) => unknown; + +let cached: WorkletsModule | null | undefined; + +/** Returns `react-native-worklets` when installed and recent enough, null otherwise */ +export function getWorkletsModule(): WorkletsModule | null { + if (cached === undefined) { + try { + // Metro treats a require inside `try` as optional, apps without the package still bundle + const worklets = require('react-native-worklets') as WorkletsModule; + cached = + typeof worklets?.registerCustomSerializable === 'function' && + typeof worklets.isWorkletFunction === 'function' && + typeof worklets.runOnUISync === 'function' && + typeof worklets.createShareable === 'function' + ? worklets + : null; + } catch { + cached = null; + } + } + return cached; +} diff --git a/packages/typegpu-react/src/react-native/worklets.ts b/packages/typegpu-react/src/react-native/worklets.ts deleted file mode 100644 index 505a357515..0000000000 --- a/packages/typegpu-react/src/react-native/worklets.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { WebGPUModule } from 'react-native-webgpu'; - -import { registerTypegpuReactSerializables } from './serialization/register-serializables.ts'; - -WebGPUModule.install(); -registerTypegpuReactSerializables(); - -export * from '../shared-exports.ts'; -// Intentionally shadows the browser `useFrame`, this one runs the frame loop on the UI runtime -export { useFrame } from './core/use-frame.ts'; -export { - useConfigureWorkletContext as useConfigureContext, - type WorkletCanvasContextRef, -} from './use-configure-worklet-context.ts'; diff --git a/packages/typegpu-react/tests/react-native/register-serializables.test.ts b/packages/typegpu-react/tests/react-native/register-serializables.test.ts index c82deffdbf..2a1e701824 100644 --- a/packages/typegpu-react/tests/react-native/register-serializables.test.ts +++ b/packages/typegpu-react/tests/react-native/register-serializables.test.ts @@ -1,13 +1,15 @@ -import { registerCustomSerializable } from 'react-native-worklets'; import { it } from 'typegpu-testing-utility'; import { tgpu, d } from 'typegpu'; import { describe, expect, vi } from 'vitest'; import { registerTypegpuReactSerializables } from '../../src/react-native/serialization/register-serializables.ts'; -vi.mock('react-native-webgpu', () => ({ installWebGPU: vi.fn() })); -vi.mock('react-native-worklets', () => ({ +const { registerCustomSerializable } = vi.hoisted(() => ({ registerCustomSerializable: vi.fn(), - isWorkletFunction: (value: unknown) => typeof value === 'function' && '__workletHash' in value, +})); + +vi.mock('react-native-webgpu', () => ({ installWebGPU: vi.fn() })); +vi.mock('../../src/react-native/worklets-integration.ts', () => ({ + getWorkletsModule: () => ({ registerCustomSerializable }), })); type Serializer = { diff --git a/packages/typegpu-react/tests/react-native/use-frame.test.tsx b/packages/typegpu-react/tests/react-native/use-frame.test.tsx new file mode 100644 index 0000000000..76eb888531 --- /dev/null +++ b/packages/typegpu-react/tests/react-native/use-frame.test.tsx @@ -0,0 +1,58 @@ +import { render } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useFrame } from '../../src/react-native/core/use-frame.ts'; + +const holder = vi.hoisted(() => ({ + worklets: null as object | null, +})); + +vi.mock('../../src/react-native/worklets-integration.ts', () => ({ + getWorkletsModule: () => holder.worklets, +})); + +function FrameUser({ cb }: { cb: (ctx: unknown) => void }) { + useFrame(cb); + return null; +} + +describe('react-native useFrame dispatch', () => { + beforeEach(() => { + vi.stubGlobal( + 'requestAnimationFrame', + vi.fn(() => 1), + ); + vi.stubGlobal('cancelAnimationFrame', vi.fn()); + }); + + afterEach(() => { + holder.worklets = null; + vi.unstubAllGlobals(); + }); + + it('runs on the JS thread when worklets are unavailable', () => { + const cb = vi.fn(); + + const { unmount } = render(); + + expect(cb).toHaveBeenCalledWith({ deltaSeconds: 0, elapsedSeconds: 0 }); + unmount(); + expect(cancelAnimationFrame).toHaveBeenCalled(); + }); + + it('never dispatches plain callbacks to the UI runtime even with worklets installed', () => { + const runOnUISync = vi.fn(); + holder.worklets = { + isWorkletFunction: (value: unknown) => + typeof value === 'function' && !!(value as { __workletHash?: unknown }).__workletHash, + runOnUISync, + createShareable: vi.fn(), + UIRuntimeId: 1, + }; + const cb = vi.fn(); + + render(); + + expect(runOnUISync).not.toHaveBeenCalled(); + expect(cb).toHaveBeenCalledWith({ deltaSeconds: 0, elapsedSeconds: 0 }); + }); +}); diff --git a/packages/typegpu-react/tsdown.config.ts b/packages/typegpu-react/tsdown.config.ts index f5d7887b6c..87f067b1df 100644 --- a/packages/typegpu-react/tsdown.config.ts +++ b/packages/typegpu-react/tsdown.config.ts @@ -1,7 +1,21 @@ import { defineConfig } from 'tsdown'; +// Rolldown rewrites `require` into a helper Metro cannot statically analyze, +// restoring the literal call keeps react-native-worklets an optional dependency +const preserveOptionalRequire = { + name: 'preserve-optional-require', + renderChunk(code: string) { + if (!code.includes('__require("react-native-worklets")')) { + return null; + } + return code + .replace('__require("react-native-worklets")', 'require("react-native-worklets")') + .replace(/import \{ __require \} from "[^"]+";\n/, ''); + }, +}; + export default defineConfig({ - entry: ['src/browser/index.ts', 'src/react-native/index.ts', 'src/react-native/worklets.ts'], + entry: ['src/browser/index.ts', 'src/react-native/index.ts'], outDir: 'dist', format: 'esm', dts: true, @@ -9,4 +23,5 @@ export default defineConfig({ unbundle: true, sourcemap: false, target: false, + plugins: [preserveOptionalRequire], }); From 01cbff19bba93b304201f56b228d3cf666d91d3c Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Thu, 16 Jul 2026 20:40:38 +0200 Subject: [PATCH 3/4] Provide .current access on worklets (#2734) --- .../integration/react-native/worklets.mdx | 8 +- .../typegpu-react/src/react-native/index.ts | 6 +- .../use-configure-worklet-context.ts | 100 ++++++++++++------ 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx index 9b5ca7793a..cf018fad17 100644 --- a/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx +++ b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx @@ -45,7 +45,7 @@ Create resources on the JS thread, then use them freely inside a `useFrame` work import { useMemo } from 'react'; import { Canvas } from 'react-native-webgpu'; import tgpu, { common, d } from 'typegpu'; -import { useConfigureWorkletContext, useFrame, useRoot, useUniform } from '@typegpu/react'; +import { useConfigureContext, useFrame, useRoot, useUniform } from '@typegpu/react'; export function Pulse() { const root = useRoot(); @@ -63,12 +63,12 @@ export function Pulse() { [root, color], ); - const { ref, ctxRef } = useConfigureWorkletContext({ alphaMode: 'premultiplied' }); + const { ref, ctxRef } = useConfigureContext({ alphaMode: 'premultiplied' }); // Runs each frame on the UI thread useFrame(({ elapsedSeconds }) => { 'worklet'; - const ctx = ctxRef.value; + const ctx = ctxRef.current; if (!ctx) return; color.write(d.vec3f(0.5 + Math.sin(elapsedSeconds) * 0.5, 0.447, 0.941)); @@ -95,7 +95,7 @@ To keep everything on the JS thread even with `react-native-worklets` installed, ``` -`useFrame` then runs its callbacks on the JS thread, and `useConfigureWorkletContext` returns a plain object of the same shape, so the code above keeps working unchanged. +`useFrame` then runs its callbacks on the JS thread, and `useConfigureContext` returns a plain object of the same shape, so the code above keeps working unchanged. ## Rules of transfer diff --git a/packages/typegpu-react/src/react-native/index.ts b/packages/typegpu-react/src/react-native/index.ts index 7e9c594ca3..65d606fe18 100644 --- a/packages/typegpu-react/src/react-native/index.ts +++ b/packages/typegpu-react/src/react-native/index.ts @@ -9,10 +9,6 @@ registerTypegpuReactSerializables(); export * from '../shared-exports.ts'; -export { useConfigureContext } from './use-configure-context.ts'; // Intentionally shadows the browser `useFrame`, this one can run the frame loop on the UI runtime export { useFrame } from './core/use-frame.ts'; -export { - useConfigureWorkletContext, - type WorkletCanvasContextRef, -} from './use-configure-worklet-context.ts'; +export { useConfigureContext } from './use-configure-worklet-context.ts'; diff --git a/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts b/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts index 520d4270d2..6c2fbb8dbf 100644 --- a/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts +++ b/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts @@ -1,55 +1,91 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, type RefObject } from 'react'; +import type { Shareable } from 'react-native-worklets'; import { useWorkletsDisabled } from '../core/root-context.tsx'; -import type { CanvasRef, UseConfigureContextOptions } from '../core/use-configure-context.ts'; -import { useConfigureContext } from './use-configure-context.ts'; +import type { + UseConfigureContextOptions, + UseConfigureContextResult, +} from '../core/use-configure-context.ts'; +import { useConfigureContext as useRNConfigureContext } from './use-configure-context.ts'; import { getWorkletsModule } from './worklets-integration.ts'; type CanvasContext = GPUCanvasContext & { present?: () => void }; -export type WorkletCanvasContextRef = { - value: CanvasContext | null; - setSync(value: CanvasContext | null): void; -}; +type ShareableContext = Shareable< + CanvasContext | null, + RefObject, + RefObject +>; + +function createShareableCtx(workletsDisabled: boolean): ShareableContext { + const worklets = workletsDisabled ? null : getWorkletsModule(); + + if (!worklets) { + return { + value: null as CanvasContext | null, + get current() { + return this.value as CanvasContext | null; + }, + setSync(value: CanvasContext | null) { + this.value = value; + }, + } as ShareableContext; + } + + return worklets.createShareable< + CanvasContext | null, + RefObject, + RefObject + >(worklets.UIRuntimeId, null, { + initSynchronously: true, + hostDecorator(shareable) { + 'worklet'; + Object.defineProperty(shareable, 'current', { + get() { + return shareable.value; + }, + enumerable: true, + }); + return shareable; + }, + guestDecorator(shareable) { + 'worklet'; + Object.defineProperty(shareable, 'current', { + get() { + throw new Error( + `Result of useConfigureContext() is only available on the UI thread. If you'd like to disable worklet support, wrap your component in ...`, + ); + }, + enumerable: true, + }); + return shareable; + }, + }); +} /** * Same as `useConfigureContext`, but exposes the canvas context through a ref readable * on the UI runtime, or a same-shape JS-thread object when worklets are unavailable or * disabled. The choice is fixed on mount, flipping `disableWorklets` requires a remount */ -export function useConfigureWorkletContext(options?: UseConfigureContextOptions): { - ref: React.RefCallback; - ctxRef: WorkletCanvasContextRef; -} { - const result = useConfigureContext(options); +export function useConfigureContext( + options?: UseConfigureContextOptions, +): UseConfigureContextResult { + const result = useRNConfigureContext(options); const workletsDisabled = useWorkletsDisabled(); - const workletCtxRef = useRef(undefined); - - if (workletCtxRef.current === undefined) { - const worklets = workletsDisabled ? null : getWorkletsModule(); - workletCtxRef.current = worklets - ? (worklets.createShareable(worklets.UIRuntimeId, null, { - initSynchronously: true, - }) as WorkletCanvasContextRef) - : { - value: null, - setSync(value) { - this.value = value; - }, - }; - } + const shareableCtxRef = useRef(undefined); - const ctxRef = workletCtxRef.current; + const shareableCtx = (shareableCtxRef.current ??= createShareableCtx(workletsDisabled)); useEffect(() => { - ctxRef.setSync(result.ctxRef.current); + shareableCtx.setSync?.(result.ctxRef.current); }); useEffect(() => { return () => { - ctxRef.setSync(null); + shareableCtx.setSync?.(null); }; - }, [ctxRef]); + }, [shareableCtx]); - return { ref: result.ref, ctxRef }; + return { ref: result.ref, ctxRef: shareableCtx }; } From e6ada544285ccfee2a04972db5241ceaf5b0a337 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Fri, 17 Jul 2026 00:53:04 +0200 Subject: [PATCH 4/4] review fixes --- .../integration/react-native/worklets.mdx | 12 +- .../typegpu-react/src/core/root-context.tsx | 2 +- .../typegpu-react/src/react-native/index.ts | 2 +- .../serialization/register-serializables.ts | 2 +- .../src/react-native/{core => }/use-frame.ts | 12 +- .../register-serializables.test.ts | 2 +- .../tests/react-native/use-frame.test.tsx | 2 +- packages/typegpu/src/core/buffer/buffer.ts | 54 ---- .../typegpu/src/core/constant/tgpuConstant.ts | 24 -- .../src/core/pipeline/renderPipeline.ts | 37 ++- packages/typegpu/src/core/root/init.ts | 9 +- packages/typegpu/src/core/slot/accessor.ts | 30 --- .../src/core/vertexLayout/vertexLayout.ts | 47 +--- packages/typegpu/src/serial/registry.ts | 57 ++--- packages/typegpu/src/serial/resources.ts | 242 ++++++++++++++++++ packages/typegpu/src/serial/schema.ts | 6 +- packages/typegpu/src/tgpuBindGroupLayout.ts | 66 ----- 17 files changed, 309 insertions(+), 297 deletions(-) rename packages/typegpu-react/src/react-native/{core => }/use-frame.ts (82%) create mode 100644 packages/typegpu/src/serial/resources.ts diff --git a/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx index cf018fad17..0526bd57fc 100644 --- a/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx +++ b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx @@ -3,8 +3,8 @@ title: React Native Worklets description: A guide on running TypeGPU render loops on the UI thread with react-native-worklets. --- -With [react-native-worklets](https://docs.swmansion.com/react-native-worklets/), per-frame GPU work can run on the UI thread, unaffected by a busy JS thread. -TypeGPU resources created on the JS thread can be captured by worklets directly, they are transferred between runtimes automatically. +With [react-native-worklets](https://docs.swmansion.com/react-native-worklets/), per-frame GPU work can be scheduled on the UI thread, unaffected by tasks running on the default React Native thread (or RN thread). +TypeGPU resources created on the RN thread can be captured by worklets directly, they are transferred between runtimes automatically. ## Setup @@ -33,13 +33,13 @@ module.exports = (api) => { ``` No extra imports are needed - `@typegpu/react` detects `react-native-worklets` at runtime and registers the transfer support for TypeGPU resources automatically. -`useFrame` runs its callback on the UI thread whenever the callback is marked with the `'worklet'` directive; plain callbacks keep running on the JS thread. +`useFrame` runs its callback on the UI thread whenever the callback is marked with the `'worklet'` directive; plain callbacks keep running on the RN thread. After changing the babel config, clear the Metro cache with `npx expo start --clear`. ## Example -Create resources on the JS thread, then use them freely inside a `useFrame` worklet: +Create resources on the RN thread, then use them freely inside a `useFrame` worklet: ```tsx import { useMemo } from 'react'; @@ -87,7 +87,7 @@ This works for buffers (including `createUniform`/`createMutable`/`createReadonl ## Opting out -To keep everything on the JS thread even with `react-native-worklets` installed, pass `disableWorklets` to the `Root` provider: +To keep everything on the RN thread even with `react-native-worklets` installed, pass `disableWorklets` to the `Root` provider: ```tsx @@ -95,7 +95,7 @@ To keep everything on the JS thread even with `react-native-worklets` installed, ``` -`useFrame` then runs its callbacks on the JS thread, and `useConfigureContext` returns a plain object of the same shape, so the code above keeps working unchanged. +`useFrame` then runs its callbacks on the RN thread, and `useConfigureContext` returns a plain object of the same shape, so the code above keeps working unchanged. ## Rules of transfer diff --git a/packages/typegpu-react/src/core/root-context.tsx b/packages/typegpu-react/src/core/root-context.tsx index ba64510203..cf802342fd 100644 --- a/packages/typegpu-react/src/core/root-context.tsx +++ b/packages/typegpu-react/src/core/root-context.tsx @@ -179,7 +179,7 @@ export interface RootProps { */ root?: TgpuRoot | undefined; /** - * (React Native only) When true, `useFrame` runs on the JS thread even if + * (React Native only) When true, `useFrame` runs on the RN thread even if * `react-native-worklets` is installed. Ignored on the web * * @default false diff --git a/packages/typegpu-react/src/react-native/index.ts b/packages/typegpu-react/src/react-native/index.ts index 65d606fe18..2e6c3ce100 100644 --- a/packages/typegpu-react/src/react-native/index.ts +++ b/packages/typegpu-react/src/react-native/index.ts @@ -10,5 +10,5 @@ registerTypegpuReactSerializables(); export * from '../shared-exports.ts'; // Intentionally shadows the browser `useFrame`, this one can run the frame loop on the UI runtime -export { useFrame } from './core/use-frame.ts'; +export { useFrame } from './use-frame.ts'; export { useConfigureContext } from './use-configure-worklet-context.ts'; diff --git a/packages/typegpu-react/src/react-native/serialization/register-serializables.ts b/packages/typegpu-react/src/react-native/serialization/register-serializables.ts index 630a90e910..5c184e6e12 100644 --- a/packages/typegpu-react/src/react-native/serialization/register-serializables.ts +++ b/packages/typegpu-react/src/react-native/serialization/register-serializables.ts @@ -46,7 +46,7 @@ export function registerTypegpuReactSerializables(): void { throw new Error( `[typegpu-react] TypeGPU object '${resourceType}' cannot be transferred to a worklet. ` + 'Definitions (functions, comptime, derived) are runtime-local: import them from a module ' + - 'covered by importForwarding, or build pipelines on the JS thread and transfer the result.', + 'covered by importForwarding, or build pipelines on the RN thread and transfer the result.', ); } for (const [key, field] of Object.entries(snapshot)) { diff --git a/packages/typegpu-react/src/react-native/core/use-frame.ts b/packages/typegpu-react/src/react-native/use-frame.ts similarity index 82% rename from packages/typegpu-react/src/react-native/core/use-frame.ts rename to packages/typegpu-react/src/react-native/use-frame.ts index ff2abea437..0caaa0901c 100644 --- a/packages/typegpu-react/src/react-native/core/use-frame.ts +++ b/packages/typegpu-react/src/react-native/use-frame.ts @@ -1,8 +1,8 @@ import { useEffect, useRef } from 'react'; -import { useWorkletsDisabled } from '../../core/root-context.tsx'; -import { type FrameCtx, startFrameLoop } from '../../core/use-frame.ts'; -import { getWorkletsModule } from '../worklets-integration.ts'; +import { useWorkletsDisabled } from '../core/root-context.tsx'; +import { type FrameCtx, startFrameLoop } from '../core/use-frame.ts'; +import { getWorkletsModule } from './worklets-integration.ts'; type FrameCallback = (ctx: FrameCtx) => void; type FrameCallbackRef = { current: FrameCallback }; @@ -13,12 +13,12 @@ type UiValue = { /** * Runs the frame loop on the UI runtime when the callback is a worklet and - * `react-native-worklets` is available, on the JS thread otherwise + * `react-native-worklets` is available, on the RN thread otherwise */ export function useFrame(cb: FrameCallback) { const workletsDisabled = useWorkletsDisabled(); - const worklets = getWorkletsModule(); - const runOnUI = worklets !== null && !workletsDisabled && worklets.isWorkletFunction(cb); + const worklets = workletsDisabled ? null : getWorkletsModule(); + const runOnUI = worklets !== null && worklets.isWorkletFunction(cb); const latestCb = useRef(cb); const uiCbRef = useRef | undefined>(undefined); diff --git a/packages/typegpu-react/tests/react-native/register-serializables.test.ts b/packages/typegpu-react/tests/react-native/register-serializables.test.ts index 2a1e701824..aa8b1c18b9 100644 --- a/packages/typegpu-react/tests/react-native/register-serializables.test.ts +++ b/packages/typegpu-react/tests/react-native/register-serializables.test.ts @@ -64,7 +64,7 @@ describe('react-native serializable registration', () => { expect(serializer.determine(view)).toBe(true); expect(() => serializer.pack(view)).toThrowErrorMatchingInlineSnapshot( - `[Error: [typegpu-react] TypeGPU object 'texture-view' cannot be transferred to a worklet. Definitions (functions, comptime, derived) are runtime-local: import them from a module covered by importForwarding, or build pipelines on the JS thread and transfer the result.]`, + `[Error: [typegpu-react] TypeGPU object 'texture-view' cannot be transferred to a worklet. Definitions (functions, comptime, derived) are runtime-local: import them from a module covered by importForwarding, or build pipelines on the RN thread and transfer the result.]`, ); }); diff --git a/packages/typegpu-react/tests/react-native/use-frame.test.tsx b/packages/typegpu-react/tests/react-native/use-frame.test.tsx index 76eb888531..9f9c197e5e 100644 --- a/packages/typegpu-react/tests/react-native/use-frame.test.tsx +++ b/packages/typegpu-react/tests/react-native/use-frame.test.tsx @@ -1,6 +1,6 @@ import { render } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { useFrame } from '../../src/react-native/core/use-frame.ts'; +import { useFrame } from '../../src/react-native/use-frame.ts'; const holder = vi.hoisted(() => ({ worklets: null as object | null, diff --git a/packages/typegpu/src/core/buffer/buffer.ts b/packages/typegpu/src/core/buffer/buffer.ts index 9fd7ae5a1e..311a5a4365 100644 --- a/packages/typegpu/src/core/buffer/buffer.ts +++ b/packages/typegpu/src/core/buffer/buffer.ts @@ -24,12 +24,6 @@ import { isGPUBuffer } from '../../types.ts'; import type { ExperimentalTgpuRoot, TgpuRoot } from '../root/rootTypes.ts'; import { calculateOffsets, readFromArrayBuffer, writeToArrayBuffer } from '../../data/dataIO.ts'; import { patchArrayBuffer } from '../../data/partialIO.ts'; -import { - deserializeDataSchema, - serializeDataSchema, - type SerializedDataSchema, -} from '../../serial/schema.ts'; -import type { RestoreContext } from '../../serial/types.ts'; import { mutable, readonly, @@ -174,44 +168,6 @@ export function INTERNAL_createBuffer( return new TgpuBufferImpl(group, typeSchema, initialOrBuffer); } -export interface TgpuBufferSnapshot { - readonly type: 'buffer'; - readonly device: GPUDevice; - readonly buffer: GPUBuffer; - readonly schema: SerializedDataSchema; - readonly usages: UsageLiteral[]; -} - -function getBufferUsages(buffer: TgpuBuffer): UsageLiteral[] { - const usages: UsageLiteral[] = []; - if (buffer.usableAsUniform) { - usages.push('uniform'); - } - if (buffer.usableAsStorage) { - usages.push('storage'); - } - if (buffer.usableAsVertex) { - usages.push('vertex'); - } - if (buffer.usableAsIndex) { - usages.push('index'); - } - if (buffer.usableAsIndirect) { - usages.push('indirect'); - } - return usages; -} - -export function INTERNAL_snapshotBuffer(buffer: TgpuBuffer): TgpuBufferSnapshot { - return { - type: 'buffer', - device: buffer.root.device, - buffer: buffer.buffer, - schema: serializeDataSchema(buffer.dataType), - usages: getBufferUsages(buffer), - }; -} - export function INTERNAL_applyBufferUsages( buffer: TgpuBuffer, usages: UsageLiteral[], @@ -221,16 +177,6 @@ export function INTERNAL_applyBufferUsages( } } -export function INTERNAL_restoreBuffer( - snapshot: TgpuBufferSnapshot, - ctx: RestoreContext, -): TgpuBuffer { - const root = ctx.getRoot(snapshot.device); - const buffer = root.createBuffer(deserializeDataSchema(snapshot.schema), snapshot.buffer); - INTERNAL_applyBufferUsages(buffer, snapshot.usages); - return buffer; -} - // -------------- // Implementation // -------------- diff --git a/packages/typegpu/src/core/constant/tgpuConstant.ts b/packages/typegpu/src/core/constant/tgpuConstant.ts index 1775e123be..8d1b6ee7a5 100644 --- a/packages/typegpu/src/core/constant/tgpuConstant.ts +++ b/packages/typegpu/src/core/constant/tgpuConstant.ts @@ -7,11 +7,6 @@ import { getName, setName } from '../../shared/meta.ts'; import type { InferGPU } from '../../shared/repr.ts'; import { $gpuValueOf, $internal, $ownSnippet, $resolve } from '../../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; -import { - deserializeDataSchema, - serializeDataSchema, - type SerializedDataSchema, -} from '../../serial/schema.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; // ---------- @@ -73,25 +68,6 @@ export function isConst(value: unknown): value is TgpuConst { ); } -export interface TgpuConstSnapshot { - readonly type: 'const'; - readonly schema: SerializedDataSchema; - readonly value: unknown; -} - -export function INTERNAL_snapshotConst(value: TgpuConst): TgpuConstSnapshot { - const impl = value as TgpuConstImpl; - return { - type: 'const', - schema: serializeDataSchema(impl.dataType), - value: impl.$, - }; -} - -export function INTERNAL_restoreConst(snapshot: TgpuConstSnapshot): TgpuConst { - return constant(deserializeDataSchema(snapshot.schema), snapshot.value); -} - // -------------- // Implementation // -------------- diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 4b055690fc..f93a7543f5 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -93,11 +93,6 @@ import { PerformanceTrackerImpl, type PerformanceTracker, } from './performanceTracker.ts'; -import { - deserializeDataSchema, - serializeDataSchema, - type SerializedDataSchema, -} from '../../serial/schema.ts'; import type { RestoreContext } from '../../serial/types.ts'; const DRAW_INDIRECT_SIZE = 16; // 4 x 4 @@ -461,11 +456,10 @@ export function INTERNAL_createRenderPipeline(options: RenderPipelineCoreOptions return new TgpuRenderPipelineImpl(new RenderPipelineCore(options), {}); } -export interface TgpuRenderPipelineSnapshot { - readonly type: 'render-pipeline'; +export interface TgpuRenderPipelineSnapshotParts { readonly device: GPUDevice; readonly pipeline: GPURenderPipeline; - readonly fragmentOut: SerializedDataSchema | undefined; + readonly fragmentOut: BaseData | undefined; readonly usedBindGroupLayouts: TgpuBindGroupLayout[]; readonly bindGroups: [TgpuBindGroupLayout, TgpuBindGroup | GPUBindGroup][]; readonly usedVertexLayouts: TgpuVertexLayout[]; @@ -474,9 +468,9 @@ export interface TgpuRenderPipelineSnapshot { readonly performanceCallback: TimestampWritesPriors['performanceCallback']; } -export function INTERNAL_snapshotRenderPipeline( +export function INTERNAL_snapshotRenderPipelineParts( pipeline: TgpuRenderPipeline, -): TgpuRenderPipelineSnapshot { +): TgpuRenderPipelineSnapshotParts { const internals = pipeline[$internal]; const memo = internals.core.unwrap(); const fragmentOut = @@ -484,10 +478,9 @@ export function INTERNAL_snapshotRenderPipeline( ?.returnType ?? memo.fragmentOut; return { - type: 'render-pipeline', device: internals.root.device, pipeline: memo.pipeline, - fragmentOut: fragmentOut ? serializeDataSchema(fragmentOut) : undefined, + fragmentOut, usedBindGroupLayouts: memo.usedBindGroupLayouts, bindGroups: collectBindGroupPairs( memo.usedBindGroupLayouts, @@ -504,24 +497,24 @@ export function INTERNAL_snapshotRenderPipeline( }; } -export function INTERNAL_restoreRenderPipeline( - snapshot: TgpuRenderPipelineSnapshot, +export function INTERNAL_restoreRenderPipelineParts( + parts: TgpuRenderPipelineSnapshotParts, ctx: RestoreContext, ): TgpuRenderPipeline { - const root = ctx.getRoot(snapshot.device) as ExperimentalTgpuRoot; + const root = ctx.getRoot(parts.device) as ExperimentalTgpuRoot; const core = RenderPipelineCore.precompiled(root, { - pipeline: snapshot.pipeline, - usedBindGroupLayouts: snapshot.usedBindGroupLayouts, + pipeline: parts.pipeline, + usedBindGroupLayouts: parts.usedBindGroupLayouts, catchall: undefined, logResources: undefined, - usedVertexLayouts: snapshot.usedVertexLayouts, - fragmentOut: snapshot.fragmentOut ? deserializeDataSchema(snapshot.fragmentOut) : undefined, + usedVertexLayouts: parts.usedVertexLayouts, + fragmentOut: parts.fragmentOut, }); const pipeline: TgpuRenderPipeline = new TgpuRenderPipelineImpl(core, { - bindGroupLayoutMap: new Map(snapshot.bindGroups), - vertexLayoutMap: new Map(snapshot.vertexBuffers), + bindGroupLayoutMap: new Map(parts.bindGroups), + vertexLayoutMap: new Map(parts.vertexBuffers), }); - return restoreTimestampPriors(pipeline, snapshot); + return restoreTimestampPriors(pipeline, parts); } // -------------- diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index ce8b2d80fa..c27c4b65f3 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -5,7 +5,7 @@ import type { AnyWgslData, BaseData, v3u, Vec3u, WgslArray } from '../../data/wg import { WeakMemo } from '../../memo.ts'; import { clearTextureUtilsCache } from '../texture/textureUtils.ts'; import type { BufferInitialData } from '../buffer/buffer.ts'; -import { $getNameForward, $internal } from '../../shared/symbols.ts'; +import { $getNameForward, $internal, isMarkedInternal } from '../../shared/symbols.ts'; import type { ExtractBindGroupInputFromLayout, TgpuBindGroup, @@ -207,15 +207,12 @@ export class TgpuGuardedComputePipelineImpl< export function isGuardedComputePipeline(maybe: unknown): maybe is TgpuGuardedComputePipeline { return ( (maybe as TgpuGuardedComputePipeline | undefined)?.resourceType === - 'guarded-compute-pipeline' && !!(maybe as { [$internal]?: boolean } | undefined)?.[$internal] + 'guarded-compute-pipeline' && isMarkedInternal(maybe) ); } export function isRoot(maybe: unknown): maybe is TgpuRoot { - return ( - (maybe as TgpuRoot | undefined)?.resourceType === 'root' && - !!(maybe as { [$internal]?: unknown } | undefined)?.[$internal] - ); + return (maybe as TgpuRoot | undefined)?.resourceType === 'root' && isMarkedInternal(maybe); } export interface TgpuRootSnapshot { diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index 0e7366f3d1..109e1aa782 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -22,11 +22,6 @@ import { type SelfResolvable, } from '../../types.ts'; import { isTgpuFn } from '../function/tgpuFn.ts'; -import { - deserializeDataSchema, - serializeDataSchema, - type SerializedDataSchema, -} from '../../serial/schema.ts'; import { getGpuValueRecursively, valueProxyHandler } from '../valueProxyUtils.ts'; import { slot } from './slot.ts'; import type { TgpuAccessor, TgpuMutableAccessor, TgpuSlot } from './slotTypes.ts'; @@ -69,31 +64,6 @@ export function mutableAccessor AnyData) ) as unknown as TgpuMutableAccessor>; } -export interface TgpuAccessorSnapshot { - readonly type: 'accessor' | 'mutable-accessor'; - readonly schema: SerializedDataSchema; - readonly defaultValue: unknown; -} - -export function INTERNAL_snapshotAccessor( - value: TgpuAccessor | TgpuMutableAccessor, -): TgpuAccessorSnapshot { - return { - type: value.resourceType, - schema: serializeDataSchema(value.schema), - defaultValue: value.defaultValue, - }; -} - -export function INTERNAL_restoreAccessor( - snapshot: TgpuAccessorSnapshot, -): TgpuAccessor | TgpuMutableAccessor { - const schema = deserializeDataSchema(snapshot.schema); - return snapshot.type === 'accessor' - ? accessor(schema, snapshot.defaultValue) - : mutableAccessor(schema, snapshot.defaultValue as TgpuMutableAccessor.In); -} - // -------------- // Implementation // -------------- diff --git a/packages/typegpu/src/core/vertexLayout/vertexLayout.ts b/packages/typegpu/src/core/vertexLayout/vertexLayout.ts index d1b8d59718..3ea7c5e3a3 100644 --- a/packages/typegpu/src/core/vertexLayout/vertexLayout.ts +++ b/packages/typegpu/src/core/vertexLayout/vertexLayout.ts @@ -1,21 +1,9 @@ import { alignmentOf, customAlignmentOf } from '../../data/alignmentOf.ts'; -import { arrayOf } from '../../data/array.ts'; -import { disarrayOf } from '../../data/disarray.ts'; -import type { AnyData, Disarray } from '../../data/dataTypes.ts'; -import { - getCustomLocation, - isDisarray, - isLooseDecorated, - isUnstruct, -} from '../../data/dataTypes.ts'; +import type { Disarray } from '../../data/dataTypes.ts'; +import { getCustomLocation, isLooseDecorated, isUnstruct } from '../../data/dataTypes.ts'; import { sizeOf } from '../../data/sizeOf.ts'; -import type { AnyWgslData, BaseData, WgslArray } from '../../data/wgslTypes.ts'; -import { isDecorated, isWgslArray, isWgslStruct } from '../../data/wgslTypes.ts'; -import { - deserializeDataSchema, - serializeDataSchema, - type SerializedDataSchema, -} from '../../serial/schema.ts'; +import type { BaseData, WgslArray } from '../../data/wgslTypes.ts'; +import { isDecorated, isWgslStruct } from '../../data/wgslTypes.ts'; import { roundUp } from '../../mathUtils.ts'; import type { TgpuNamable } from '../../shared/meta.ts'; import { setName } from '../../shared/meta.ts'; @@ -59,33 +47,6 @@ export function isVertexLayout(value: unknown): value is TgpuVertexLayout { return (value as TgpuVertexLayout)?.resourceType === 'vertex-layout'; } -export interface TgpuVertexLayoutSnapshot { - readonly type: 'vertex-layout'; - readonly schema: SerializedDataSchema; - readonly stepMode: 'vertex' | 'instance'; -} - -export function INTERNAL_snapshotVertexLayout(layout: TgpuVertexLayout): TgpuVertexLayoutSnapshot { - return { - type: 'vertex-layout', - schema: serializeDataSchema(layout.schemaForCount(0)), - stepMode: layout.stepMode, - }; -} - -export function INTERNAL_restoreVertexLayout(snapshot: TgpuVertexLayoutSnapshot): TgpuVertexLayout { - const schema = deserializeDataSchema(snapshot.schema); - if (isWgslArray(schema)) { - const elementType = schema.elementType as AnyWgslData; - return vertexLayout((count) => arrayOf(elementType, count), snapshot.stepMode); - } - if (isDisarray(schema)) { - const elementType = schema.elementType as AnyData; - return vertexLayout((count) => disarrayOf(elementType, count), snapshot.stepMode); - } - throw new Error('TypeGPU vertex layout payload could not be reconstructed.'); -} - // -------------- // Implementation // -------------- diff --git a/packages/typegpu/src/serial/registry.ts b/packages/typegpu/src/serial/registry.ts index 88fe27fed6..4bbb932f28 100644 --- a/packages/typegpu/src/serial/registry.ts +++ b/packages/typegpu/src/serial/registry.ts @@ -1,9 +1,27 @@ +import { INTERNAL_applyBufferUsages } from '../core/buffer/buffer.ts'; import { - INTERNAL_applyBufferUsages, + INTERNAL_restoreAccessor, + INTERNAL_restoreBindGroup, + INTERNAL_restoreBindGroupLayout, INTERNAL_restoreBuffer, + INTERNAL_restoreConst, + INTERNAL_restoreRenderPipeline, + INTERNAL_restoreVertexLayout, + INTERNAL_snapshotAccessor, + INTERNAL_snapshotBindGroup, + INTERNAL_snapshotBindGroupLayout, INTERNAL_snapshotBuffer, + INTERNAL_snapshotConst, + INTERNAL_snapshotRenderPipeline, + INTERNAL_snapshotVertexLayout, + type TgpuAccessorSnapshot, + type TgpuBindGroupLayoutSnapshot, + type TgpuBindGroupSnapshot, type TgpuBufferSnapshot, -} from '../core/buffer/buffer.ts'; + type TgpuConstSnapshot, + type TgpuRenderPipelineSnapshot, + type TgpuVertexLayoutSnapshot, +} from './resources.ts'; import { isBuffer } from '../types.ts'; import { isBufferBinding, type TgpuBufferBinding } from '../core/buffer/bufferBinding.ts'; import type { AnyWgslData, BaseData } from '../data/wgslTypes.ts'; @@ -13,11 +31,6 @@ import { INTERNAL_snapshotComputePipeline, type TgpuComputePipelineSnapshot, } from '../core/pipeline/computePipeline.ts'; -import { - INTERNAL_restoreRenderPipeline, - INTERNAL_snapshotRenderPipeline, - type TgpuRenderPipelineSnapshot, -} from '../core/pipeline/renderPipeline.ts'; import { isComputePipeline, isRenderPipeline } from '../core/pipeline/typeGuards.ts'; import { INTERNAL_restoreQuerySet, @@ -25,12 +38,7 @@ import { isQuerySet, type TgpuQuerySetSnapshot, } from '../core/querySet/querySet.ts'; -import { - INTERNAL_restoreConst, - INTERNAL_snapshotConst, - isConst, - type TgpuConstSnapshot, -} from '../core/constant/tgpuConstant.ts'; +import { isConst } from '../core/constant/tgpuConstant.ts'; import { INTERNAL_restoreGuardedComputePipeline, INTERNAL_restoreRoot, @@ -41,11 +49,6 @@ import { type TgpuGuardedComputePipelineSnapshot, type TgpuRootSnapshot, } from '../core/root/init.ts'; -import { - INTERNAL_restoreAccessor, - INTERNAL_snapshotAccessor, - type TgpuAccessorSnapshot, -} from '../core/slot/accessor.ts'; import { INTERNAL_restoreSlot, INTERNAL_snapshotSlot, @@ -71,22 +74,8 @@ import { isTexture, type TgpuTextureSnapshot, } from '../core/texture/texture.ts'; -import { - INTERNAL_restoreVertexLayout, - INTERNAL_snapshotVertexLayout, - isVertexLayout, - type TgpuVertexLayoutSnapshot, -} from '../core/vertexLayout/vertexLayout.ts'; -import { - INTERNAL_restoreBindGroup, - INTERNAL_restoreBindGroupLayout, - INTERNAL_snapshotBindGroup, - INTERNAL_snapshotBindGroupLayout, - isBindGroup, - isBindGroupLayout, - type TgpuBindGroupLayoutSnapshot, - type TgpuBindGroupSnapshot, -} from '../tgpuBindGroupLayout.ts'; +import { isVertexLayout } from '../core/vertexLayout/vertexLayout.ts'; +import { isBindGroup, isBindGroupLayout } from '../tgpuBindGroupLayout.ts'; import type { RestoreContext } from './types.ts'; /** Plain objects that make a resource recreatable in another JS runtime sharing the same device */ diff --git a/packages/typegpu/src/serial/resources.ts b/packages/typegpu/src/serial/resources.ts new file mode 100644 index 0000000000..299c17e428 --- /dev/null +++ b/packages/typegpu/src/serial/resources.ts @@ -0,0 +1,242 @@ +import { + INTERNAL_applyBufferUsages, + type TgpuBuffer, + type UsageLiteral, +} from '../core/buffer/buffer.ts'; +import { constant, type TgpuConst } from '../core/constant/tgpuConstant.ts'; +import { + INTERNAL_restoreRenderPipelineParts, + INTERNAL_snapshotRenderPipelineParts, + type TgpuRenderPipeline, + type TgpuRenderPipelineSnapshotParts, +} from '../core/pipeline/renderPipeline.ts'; +import { accessor, mutableAccessor } from '../core/slot/accessor.ts'; +import type { TgpuAccessor, TgpuMutableAccessor } from '../core/slot/slotTypes.ts'; +import { vertexLayout, type TgpuVertexLayout } from '../core/vertexLayout/vertexLayout.ts'; +import { arrayOf } from '../data/array.ts'; +import { disarrayOf } from '../data/disarray.ts'; +import { isDisarray, type AnyData } from '../data/dataTypes.ts'; +import { isWgslArray, type AnyWgslData, type BaseData } from '../data/wgslTypes.ts'; +import { + bindGroupLayout, + type TgpuBindGroup, + type TgpuBindGroupLayout, +} from '../tgpuBindGroupLayout.ts'; +import { + deserializeLayoutEntry, + serializeLayoutEntry, + type SerializedLayoutEntry, +} from './layoutEntries.ts'; +import { deserializeDataSchema, serializeDataSchema, type SerializedDataSchema } from './schema.ts'; +import type { RestoreContext } from './types.ts'; + +export interface TgpuBufferSnapshot { + readonly type: 'buffer'; + readonly device: GPUDevice; + readonly buffer: GPUBuffer; + readonly schema: SerializedDataSchema; + readonly usages: UsageLiteral[]; +} + +function getBufferUsages(buffer: TgpuBuffer): UsageLiteral[] { + const usages: UsageLiteral[] = []; + if (buffer.usableAsUniform) { + usages.push('uniform'); + } + if (buffer.usableAsStorage) { + usages.push('storage'); + } + if (buffer.usableAsVertex) { + usages.push('vertex'); + } + if (buffer.usableAsIndex) { + usages.push('index'); + } + if (buffer.usableAsIndirect) { + usages.push('indirect'); + } + return usages; +} + +export function INTERNAL_snapshotBuffer(buffer: TgpuBuffer): TgpuBufferSnapshot { + return { + type: 'buffer', + device: buffer.root.device, + buffer: buffer.buffer, + schema: serializeDataSchema(buffer.dataType), + usages: getBufferUsages(buffer), + }; +} + +export function INTERNAL_restoreBuffer( + snapshot: TgpuBufferSnapshot, + ctx: RestoreContext, +): TgpuBuffer { + const root = ctx.getRoot(snapshot.device); + const buffer = root.createBuffer(deserializeDataSchema(snapshot.schema), snapshot.buffer); + INTERNAL_applyBufferUsages(buffer, snapshot.usages); + return buffer; +} + +export interface TgpuBindGroupLayoutSnapshot { + readonly type: 'bind-group-layout'; + readonly entries: [string, SerializedLayoutEntry][]; + readonly index: number | undefined; +} + +export function INTERNAL_snapshotBindGroupLayout( + layout: TgpuBindGroupLayout, +): TgpuBindGroupLayoutSnapshot { + return { + type: 'bind-group-layout', + entries: Object.entries(layout.entries).map(([key, entry]) => [ + key, + serializeLayoutEntry(entry), + ]), + index: layout.index, + }; +} + +export function INTERNAL_restoreBindGroupLayout( + snapshot: TgpuBindGroupLayoutSnapshot, +): TgpuBindGroupLayout { + const layout = bindGroupLayout( + Object.fromEntries( + snapshot.entries.map(([key, entry]) => [key, deserializeLayoutEntry(entry)]), + ), + ); + return layout.$idx(snapshot.index); +} + +export interface TgpuBindGroupSnapshot { + readonly type: 'bind-group'; + readonly device: GPUDevice; + readonly layout: TgpuBindGroupLayout; + readonly bindGroup: GPUBindGroup; +} + +export function INTERNAL_snapshotBindGroup(bindGroup: TgpuBindGroup): TgpuBindGroupSnapshot { + return { + type: 'bind-group', + device: bindGroup.root.device, + layout: bindGroup.layout, + bindGroup: bindGroup.root.unwrap(bindGroup), + }; +} + +export function INTERNAL_restoreBindGroup( + snapshot: TgpuBindGroupSnapshot, + ctx: RestoreContext, +): TgpuBindGroup { + const root = ctx.getRoot(snapshot.device); + const bindGroup = snapshot.bindGroup; + return { + resourceType: 'bind-group', + root, + layout: snapshot.layout, + unwrap: () => bindGroup, + }; +} + +export interface TgpuVertexLayoutSnapshot { + readonly type: 'vertex-layout'; + readonly schema: SerializedDataSchema; + readonly stepMode: 'vertex' | 'instance'; +} + +export function INTERNAL_snapshotVertexLayout(layout: TgpuVertexLayout): TgpuVertexLayoutSnapshot { + return { + type: 'vertex-layout', + schema: serializeDataSchema(layout.schemaForCount(0)), + stepMode: layout.stepMode, + }; +} + +export function INTERNAL_restoreVertexLayout(snapshot: TgpuVertexLayoutSnapshot): TgpuVertexLayout { + const schema = deserializeDataSchema(snapshot.schema); + if (isWgslArray(schema)) { + const elementType = schema.elementType as AnyWgslData; + return vertexLayout((count) => arrayOf(elementType, count), snapshot.stepMode); + } + if (isDisarray(schema)) { + const elementType = schema.elementType as AnyData; + return vertexLayout((count) => disarrayOf(elementType, count), snapshot.stepMode); + } + throw new Error('TypeGPU vertex layout payload could not be reconstructed.'); +} + +export interface TgpuConstSnapshot { + readonly type: 'const'; + readonly schema: SerializedDataSchema; + readonly value: unknown; +} + +export function INTERNAL_snapshotConst(value: TgpuConst): TgpuConstSnapshot { + const impl = value as TgpuConst & { dataType: AnyData }; + return { + type: 'const', + schema: serializeDataSchema(impl.dataType), + value: impl.$, + }; +} + +export function INTERNAL_restoreConst(snapshot: TgpuConstSnapshot): TgpuConst { + return constant(deserializeDataSchema(snapshot.schema), snapshot.value); +} + +export interface TgpuAccessorSnapshot { + readonly type: 'accessor' | 'mutable-accessor'; + readonly schema: SerializedDataSchema; + readonly defaultValue: unknown; +} + +export function INTERNAL_snapshotAccessor( + value: TgpuAccessor | TgpuMutableAccessor, +): TgpuAccessorSnapshot { + return { + type: value.resourceType, + schema: serializeDataSchema(value.schema), + defaultValue: value.defaultValue, + }; +} + +export function INTERNAL_restoreAccessor( + snapshot: TgpuAccessorSnapshot, +): TgpuAccessor | TgpuMutableAccessor { + const schema = deserializeDataSchema(snapshot.schema); + return snapshot.type === 'accessor' + ? accessor(schema, snapshot.defaultValue) + : mutableAccessor(schema, snapshot.defaultValue as TgpuMutableAccessor.In); +} + +export interface TgpuRenderPipelineSnapshot extends Omit< + TgpuRenderPipelineSnapshotParts, + 'fragmentOut' +> { + readonly type: 'render-pipeline'; + readonly fragmentOut: SerializedDataSchema | undefined; +} + +export function INTERNAL_snapshotRenderPipeline( + pipeline: TgpuRenderPipeline, +): TgpuRenderPipelineSnapshot { + const parts = INTERNAL_snapshotRenderPipelineParts(pipeline); + return { + ...parts, + type: 'render-pipeline', + fragmentOut: parts.fragmentOut ? serializeDataSchema(parts.fragmentOut) : undefined, + }; +} + +export function INTERNAL_restoreRenderPipeline( + snapshot: TgpuRenderPipelineSnapshot, + ctx: RestoreContext, +): TgpuRenderPipeline { + return INTERNAL_restoreRenderPipelineParts( + { + ...snapshot, + fragmentOut: snapshot.fragmentOut ? deserializeDataSchema(snapshot.fragmentOut) : undefined, + }, + ctx, + ); +} diff --git a/packages/typegpu/src/serial/schema.ts b/packages/typegpu/src/serial/schema.ts index b9163a95e3..9c7a681311 100644 --- a/packages/typegpu/src/serial/schema.ts +++ b/packages/typegpu/src/serial/schema.ts @@ -1,4 +1,5 @@ import * as d from '../data/index.ts'; +import { assertExhaustive } from '../shared/utilityTypes.ts'; type SerializedDataAttrib = | { type: 'align'; value: number } @@ -92,7 +93,10 @@ function applyAttrib(schema: d.AnyData, attrib: SerializedDataAttrib): d.AnyData if (attrib.type === 'builtin') { return getBuiltinByName(attrib.value); } - return d.invariant(schema as Parameters[0]); + if (attrib.type === 'invariant') { + return d.invariant(schema as Parameters[0]); + } + assertExhaustive(attrib, 'schema.ts#applyAttrib'); } function serializeProps(propTypes: Record): [string, SerializedDataSchema][] { diff --git a/packages/typegpu/src/tgpuBindGroupLayout.ts b/packages/typegpu/src/tgpuBindGroupLayout.ts index 4202d083cb..779af90173 100644 --- a/packages/typegpu/src/tgpuBindGroupLayout.ts +++ b/packages/typegpu/src/tgpuBindGroupLayout.ts @@ -42,12 +42,6 @@ import { $gpuValueOf, $internal } from './shared/symbols.ts'; import type { NullableToOptional, Prettify } from './shared/utilityTypes.ts'; import type { ResolvableObject, TgpuShaderStage } from './types.ts'; import type { Unwrapper } from './unwrapper.ts'; -import { - deserializeLayoutEntry, - serializeLayoutEntry, - type SerializedLayoutEntry, -} from './serial/layoutEntries.ts'; -import type { RestoreContext } from './serial/types.ts'; import type { WgslComparisonSampler, WgslSampler } from './data/sampler.ts'; import { TgpuLaidOutBufferImpl } from './core/buffer/laidOutBuffer.ts'; @@ -241,66 +235,6 @@ export function isBindGroup(value: unknown): value is TgpuBindGroup { return !!value && (value as TgpuBindGroup).resourceType === 'bind-group'; } -export interface TgpuBindGroupLayoutSnapshot { - readonly type: 'bind-group-layout'; - readonly entries: [string, SerializedLayoutEntry][]; - readonly index: number | undefined; -} - -export function INTERNAL_snapshotBindGroupLayout( - layout: TgpuBindGroupLayout, -): TgpuBindGroupLayoutSnapshot { - return { - type: 'bind-group-layout', - entries: Object.entries(layout.entries).map(([key, entry]) => [ - key, - serializeLayoutEntry(entry), - ]), - index: layout.index, - }; -} - -export function INTERNAL_restoreBindGroupLayout( - snapshot: TgpuBindGroupLayoutSnapshot, -): TgpuBindGroupLayout { - const layout = bindGroupLayout( - Object.fromEntries( - snapshot.entries.map(([key, entry]) => [key, deserializeLayoutEntry(entry)]), - ), - ); - return layout.$idx(snapshot.index); -} - -export interface TgpuBindGroupSnapshot { - readonly type: 'bind-group'; - readonly device: GPUDevice; - readonly layout: TgpuBindGroupLayout; - readonly bindGroup: GPUBindGroup; -} - -export function INTERNAL_snapshotBindGroup(bindGroup: TgpuBindGroup): TgpuBindGroupSnapshot { - return { - type: 'bind-group', - device: bindGroup.root.device, - layout: bindGroup.layout, - bindGroup: bindGroup.root.unwrap(bindGroup), - }; -} - -export function INTERNAL_restoreBindGroup( - snapshot: TgpuBindGroupSnapshot, - ctx: RestoreContext, -): TgpuBindGroup { - const root = ctx.getRoot(snapshot.device); - const bindGroup = snapshot.bindGroup; - return { - resourceType: 'bind-group', - root, - layout: snapshot.layout, - unwrap: () => bindGroup, - }; -} - /** * @category Errors */