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
JavaScript
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;
}