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..0526bd57fc --- /dev/null +++ b/apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx @@ -0,0 +1,117 @@ +--- +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 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 + +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], + ], + }; +}; +``` + +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 RN thread. + +After changing the babel config, clear the Metro cache with `npx expo start --clear`. + +## Example + +Create resources on the RN 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'; + +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.current; + 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. + +## Opting out + +To keep everything on the RN thread even with `react-native-worklets` installed, pass `disableWorklets` to the `Root` provider: + +```tsx + + + +``` + +`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 + +**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..9ec734cdd2 100644 --- a/packages/typegpu-react/README.md +++ b/packages/typegpu-react/README.md @@ -46,6 +46,12 @@ const App = (props: Props) => { }; ``` +# React Native + +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). + ## 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..383aaf7ad4 100644 --- a/packages/typegpu-react/package.json +++ b/packages/typegpu-react/package.json @@ -17,7 +17,11 @@ "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" + }, "./package.json": "./package.json" }, "publishConfig": { @@ -53,6 +57,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 +68,7 @@ "react": "^19.0.0", "react-native": "*", "react-native-webgpu": "*", + "react-native-worklets": "*", "typegpu": "workspace:^" }, "peerDependenciesMeta": { @@ -71,6 +77,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..cf802342fd 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(); @@ -156,7 +161,16 @@ 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; /** * An existing root to provide. If undefined (default), a new root will be initialized for * this provider's children. @@ -164,6 +178,13 @@ export interface RootProps { * @default undefined */ root?: TgpuRoot | undefined; + /** + * (React Native only) When true, `useFrame` runs on the RN thread even if + * `react-native-worklets` is installed. Ignored on the web + * + * @default false + */ + disableWorklets?: boolean | undefined; children?: ReactNode | undefined; } @@ -183,8 +204,8 @@ function WarnSuspense() { return null; } -export const Root = ({ children, root }: RootProps) => { - const [ownCtx] = useState(() => new OwnRootContext()); +export const Root = ({ children, options, root, disableWorklets = false }: RootProps) => { + const [ownCtx] = useState(() => new OwnRootContext(options)); const existingRootCtx = useMemo(() => { if (root) { return new ExistingRootContext(root); @@ -198,7 +219,9 @@ export const Root = ({ children, 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/index.ts b/packages/typegpu-react/src/react-native/index.ts index bfb514da65..2e6c3ce100 100644 --- a/packages/typegpu-react/src/react-native/index.ts +++ b/packages/typegpu-react/src/react-native/index.ts @@ -1,8 +1,14 @@ 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 './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 new file mode 100644 index 0000000000..5c184e6e12 --- /dev/null +++ b/packages/typegpu-react/src/react-native/serialization/register-serializables.ts @@ -0,0 +1,91 @@ +import { installWebGPU } from 'react-native-webgpu'; +import { + isNonTransferableResource, + isSnapshotableResource, + restoreResource, + snapshotResource, + type TgpuResourceSnapshot, +} from 'typegpu/~internal'; +import { + cacheTransferredResource, + getCachedTransferredResource, + getOrCreateTransferId, + getTransferredRoot, +} from './transfer-cache.ts'; +import { getWorkletsModule } from '../worklets-integration.ts'; + +export type PackedTgpuResource = { + id: number; + snapshot: TgpuResourceSnapshot; +}; + +let registered = false; + +export function registerTypegpuReactSerializables(): void { + if (registered) { + return; + } + const worklets = getWorkletsModule(); + if (!worklets) { + return; + } + registered = true; + + worklets.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 RN thread and transfer the result.', + ); + } + for (const [key, field] of Object.entries(snapshot)) { + // Inlined isWorkletFunction, the lazily resolved module cannot be captured in a worklet + if ( + typeof field === 'function' && + !(field as { __workletHash?: unknown }).__workletHash && + !(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..6c2fbb8dbf --- /dev/null +++ b/packages/typegpu-react/src/react-native/use-configure-worklet-context.ts @@ -0,0 +1,91 @@ +import { useEffect, useRef, type RefObject } from 'react'; +import type { Shareable } from 'react-native-worklets'; + +import { useWorkletsDisabled } from '../core/root-context.tsx'; +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 }; + +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 useConfigureContext( + options?: UseConfigureContextOptions, +): UseConfigureContextResult { + const result = useRNConfigureContext(options); + const workletsDisabled = useWorkletsDisabled(); + const shareableCtxRef = useRef(undefined); + + const shareableCtx = (shareableCtxRef.current ??= createShareableCtx(workletsDisabled)); + + useEffect(() => { + shareableCtx.setSync?.(result.ctxRef.current); + }); + + useEffect(() => { + return () => { + shareableCtx.setSync?.(null); + }; + }, [shareableCtx]); + + return { ref: result.ref, ctxRef: shareableCtx }; +} diff --git a/packages/typegpu-react/src/react-native/use-frame.ts b/packages/typegpu-react/src/react-native/use-frame.ts new file mode 100644 index 0000000000..0caaa0901c --- /dev/null +++ b/packages/typegpu-react/src/react-native/use-frame.ts @@ -0,0 +1,78 @@ +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'; + +type FrameCallback = (ctx: FrameCtx) => void; +type FrameCallbackRef = { current: FrameCallback }; +type UiValue = { + value: T; + 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 RN thread otherwise + */ +export function useFrame(cb: FrameCallback) { + const workletsDisabled = useWorkletsDisabled(); + const worklets = workletsDisabled ? null : getWorkletsModule(); + const runOnUI = worklets !== null && 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: latestCb.current }, + { initSynchronously: true }, + ) as UiValue; + const frameId = createShareable(UIRuntimeId, undefined) as UiValue; + uiCbRef.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 () => { + uiCbRef.current = undefined; + runOnUISync(() => { + 'worklet'; + if (frameId.value !== undefined) { + cancelAnimationFrame(frameId.value); + } + }); + }; + }, [runOnUI, worklets]); +} 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/tests/react-native/register-serializables.test.ts b/packages/typegpu-react/tests/react-native/register-serializables.test.ts new file mode 100644 index 0000000000..aa8b1c18b9 --- /dev/null +++ b/packages/typegpu-react/tests/react-native/register-serializables.test.ts @@ -0,0 +1,86 @@ +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'; + +const { registerCustomSerializable } = vi.hoisted(() => ({ + registerCustomSerializable: vi.fn(), +})); + +vi.mock('react-native-webgpu', () => ({ installWebGPU: vi.fn() })); +vi.mock('../../src/react-native/worklets-integration.ts', () => ({ + getWorkletsModule: () => ({ registerCustomSerializable }), +})); + +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 RN 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/react-native/use-frame.test.tsx b/packages/typegpu-react/tests/react-native/use-frame.test.tsx new file mode 100644 index 0000000000..9f9c197e5e --- /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/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/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..87f067b1df 100644 --- a/packages/typegpu-react/tsdown.config.ts +++ b/packages/typegpu-react/tsdown.config.ts @@ -1,5 +1,19 @@ 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'], outDir: 'dist', @@ -9,4 +23,5 @@ export default defineConfig({ unbundle: true, sourcemap: false, target: false, + plugins: [preserveOptionalRequire], }); diff --git a/packages/typegpu/src/core/buffer/buffer.ts b/packages/typegpu/src/core/buffer/buffer.ts index cab7ab00af..311a5a4365 100644 --- a/packages/typegpu/src/core/buffer/buffer.ts +++ b/packages/typegpu/src/core/buffer/buffer.ts @@ -21,7 +21,7 @@ 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 { @@ -63,7 +63,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 +117,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 +168,22 @@ export function INTERNAL_createBuffer( return new TgpuBufferImpl(group, typeSchema, initialOrBuffer); } +export function INTERNAL_applyBufferUsages( + buffer: TgpuBuffer, + usages: UsageLiteral[], +): void { + if (usages.length > 0) { + (buffer as TgpuBufferImpl).$usage(...usages); + } +} + // -------------- // 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 +214,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..8d1b6ee7a5 100644 --- a/packages/typegpu/src/core/constant/tgpuConstant.ts +++ b/packages/typegpu/src/core/constant/tgpuConstant.ts @@ -61,6 +61,13 @@ 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] + ); +} + // -------------- // 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..f93a7543f5 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -82,12 +82,18 @@ 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 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 +448,75 @@ 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 TgpuRenderPipelineSnapshotParts { + readonly device: GPUDevice; + readonly pipeline: GPURenderPipeline; + readonly fragmentOut: BaseData | 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_snapshotRenderPipelineParts( + pipeline: TgpuRenderPipeline, +): TgpuRenderPipelineSnapshotParts { + const internals = pipeline[$internal]; + const memo = internals.core.unwrap(); + const fragmentOut = + (internals.core.options.descriptor?.fragment as TgpuFragmentFn | undefined)?.shell + ?.returnType ?? memo.fragmentOut; + + return { + device: internals.root.device, + pipeline: memo.pipeline, + fragmentOut, + 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_restoreRenderPipelineParts( + parts: TgpuRenderPipelineSnapshotParts, + ctx: RestoreContext, +): TgpuRenderPipeline { + const root = ctx.getRoot(parts.device) as ExperimentalTgpuRoot; + const core = RenderPipelineCore.precompiled(root, { + pipeline: parts.pipeline, + usedBindGroupLayouts: parts.usedBindGroupLayouts, + catchall: undefined, + logResources: undefined, + usedVertexLayouts: parts.usedVertexLayouts, + fragmentOut: parts.fragmentOut, + }); + const pipeline: TgpuRenderPipeline = new TgpuRenderPipelineImpl(core, { + bindGroupLayoutMap: new Map(parts.bindGroups), + vertexLayoutMap: new Map(parts.vertexBuffers), + }); + return restoreTimestampPriors(pipeline, parts); +} + // -------------- // Implementation // -------------- @@ -479,7 +547,7 @@ type Memo = { catchall: [number, TgpuBindGroup] | undefined; logResources: LogResources | undefined; usedVertexLayouts: TgpuVertexLayout[]; - fragmentOut: BaseData; + fragmentOut: BaseData | undefined; }; const _lastAppliedRender = new WeakMap< @@ -723,35 +791,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 +1107,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 +1243,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 +1275,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 504f0dc2dd..a204199d5c 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, @@ -81,6 +81,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. @@ -102,7 +103,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; @@ -110,7 +113,7 @@ export class TgpuGuardedComputePipelineImpl< #lastSize: v3u; constructor( - root: ExperimentalTgpuRoot, + root: TgpuRoot, pipeline: TgpuComputePipeline, sizeUniform: TgpuUniform, workgroupSize: v3u, @@ -185,6 +188,14 @@ export class TgpuGuardedComputePipelineImpl< return this.#sizeUniform; } + get root() { + return this.#root; + } + + get workgroupSize() { + return this.#workgroupSize; + } + [$internal] = true; get [$getNameForward]() { return this.#pipeline; @@ -195,6 +206,63 @@ export class TgpuGuardedComputePipelineImpl< } } +export function isGuardedComputePipeline(maybe: unknown): maybe is TgpuGuardedComputePipeline { + return ( + (maybe as TgpuGuardedComputePipeline | undefined)?.resourceType === + 'guarded-compute-pipeline' && isMarkedInternal(maybe) + ); +} + +export function isRoot(maybe: unknown): maybe is TgpuRoot { + return (maybe as TgpuRoot | undefined)?.resourceType === 'root' && isMarkedInternal(maybe); +} + +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][]; @@ -283,6 +351,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; @@ -382,8 +451,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() { @@ -719,6 +791,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.'); @@ -731,13 +804,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 { @@ -746,7 +819,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 4fb1ac24e1..874b0b2560 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 // -------------- @@ -175,6 +210,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/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 5103ec081c..e77b40187b 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', 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..4bbb932f28 --- /dev/null +++ b/packages/typegpu/src/serial/registry.ts @@ -0,0 +1,295 @@ +import { INTERNAL_applyBufferUsages } from '../core/buffer/buffer.ts'; +import { + 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, + 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'; +import { deserializeDataSchema } from './schema.ts'; +import { + INTERNAL_restoreComputePipeline, + INTERNAL_snapshotComputePipeline, + type TgpuComputePipelineSnapshot, +} from '../core/pipeline/computePipeline.ts'; +import { isComputePipeline, isRenderPipeline } from '../core/pipeline/typeGuards.ts'; +import { + INTERNAL_restoreQuerySet, + INTERNAL_snapshotQuerySet, + isQuerySet, + type TgpuQuerySetSnapshot, +} from '../core/querySet/querySet.ts'; +import { isConst } 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_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 { 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 */ +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/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 new file mode 100644 index 0000000000..9c7a681311 --- /dev/null +++ b/packages/typegpu/src/serial/schema.ts @@ -0,0 +1,197 @@ +import * as d from '../data/index.ts'; +import { assertExhaustive } from '../shared/utilityTypes.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); + } + if (attrib.type === 'invariant') { + return d.invariant(schema as Parameters[0]); + } + assertExhaustive(attrib, 'schema.ts#applyAttrib'); +} + +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/std/bitcast.ts b/packages/typegpu/src/std/bitcast.ts index 755ee9ee54..a9d112dbb6 100644 --- a/packages/typegpu/src/std/bitcast.ts +++ b/packages/typegpu/src/std/bitcast.ts @@ -41,7 +41,7 @@ import type { } from '../data/wgslTypes.ts'; import { unifyStrict } from '../tgsl/conversion.ts'; import { SignatureNotSupportedError } from '../errors.ts'; -import { getName } from '../internal.ts'; +import { getName } from '../shared/meta.ts'; import type { Infer } from '../shared/repr.ts'; import { comptime } from '../core/function/comptime.ts'; diff --git a/packages/typegpu/src/tgpuBindGroupLayout.ts b/packages/typegpu/src/tgpuBindGroupLayout.ts index 1e88da8377..58a59de639 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 { BaseData } from './data/wgslTypes.ts'; import { invariant, NotUniformError } from './errors.ts'; import { NotStorageError, type StorageFlag } from './extension.ts'; @@ -231,6 +232,7 @@ export type TgpuBindGroup< Entries extends Record = Record, > = { readonly resourceType: 'bind-group'; + readonly root: TgpuRoot; readonly layout: TgpuBindGroupLayout; unwrap(unwrapper: Unwrapper): GPUBindGroup; }; @@ -442,13 +444,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: