UNPKG

typegpu

Version:

A thin layer between JS and WebGPU/WGSL that improves development experience and allows for faster iteration.

107 lines (106 loc) 4.05 kB
import { isData } from "../data/dataTypes.js"; import { setName } from "../shared/meta.js"; import { $internal, $soul, isMarkedInternal } from "../shared/symbols.js"; import { soulRestorers } from "./restore.js"; import { deserializeDataSchema, serializeDataSchema } from "./schema.js"; /** * Every data schema is callable, and host serializers claim functions before * they ever ask whether a value is transferable, so schemas held by a soul are * replaced with a description of themselves on the way out */ const DATA_SCHEMA_KEY = '~tgpuDataSchema'; function isPlainRecord(value) { if (typeof value !== 'object' || value === null) { return false; } const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } /** * Rebuilds containers only when something inside them changed, so raw handles * are passed through untouched. `path` keeps self-referencing objects finite */ function mapContainer(value, path, mapField) { const isArray = Array.isArray(value); if (!isArray && !isPlainRecord(value)) { return value; } if (path.has(value)) { return value; } path.add(value); try { if (isArray) { const mapped = value.map((field) => mapField(field, path)); return mapped.some((field, idx) => field !== value[idx]) ? mapped : value; } const entries = Object.entries(value); const mapped = entries.map(([key, field]) => [key, mapField(field, path)]); return mapped.some(([, field], idx) => field !== entries[idx]?.[1]) ? Object.fromEntries(mapped) : value; } finally { path.delete(value); } } function describeSchemas(value, path = new Set()) { if (isData(value)) { return { [DATA_SCHEMA_KEY]: serializeDataSchema(value) }; } return mapContainer(value, path, describeSchemas); } function reviveSchemas(value, path = new Set()) { if (isPlainRecord(value)) { const described = value[DATA_SCHEMA_KEY]; if (described) { return deserializeDataSchema(described); } } return mapContainer(value, path, reviveSchemas); } function soulOf(value) { const soul = value?.[$soul]; return soul && soul.type in soulRestorers ? soul : undefined; } /** Whether the value is something {@link snapshotResource} can carry across a device boundary */ export function isTransferableResource(value) { return soulOf(value) !== undefined; } /** Whether the value is a class-like TypeGPU object that {@link snapshotResource} cannot carry */ export function isNonTransferableResource(value) { return (typeof value === 'object' && value !== null && !Array.isArray(value) && !isData(value) && isMarkedInternal(value) && !isTransferableResource(value)); } /** * Extracts what survives a device boundary: the resource's soul, completed by * materialization for resources whose definition is a compiled object */ export function snapshotResource(value) { const soul = soulOf(value); if (!soul) { return undefined; } value[$internal]?.materialize?.(); const nonTransferable = soul.nonTransferablePriors; if (nonTransferable?.length) { throw new Error(`TypeGPU '${soul.type}' cannot be transferred: ${nonTransferable.join(', ')} ${nonTransferable.length > 1 ? 'are' : 'is'} bound to this runtime. Apply them after the resource crosses the boundary.`); } return describeSchemas({ ...soul }); } export function restoreResource(snapshot, ctx) { const restore = soulRestorers[snapshot.type]; if (!restore) { throw new Error(`TypeGPU resource '${snapshot.type}' has no restorer registered.`); } const resource = restore(reviveSchemas(snapshot), ctx); // A root is restored by identity, it keeps the name the receiving runtime gave it if (snapshot.label !== undefined && snapshot.type !== 'root') { setName(resource, snapshot.label); } return resource; }