UNPKG

webgpu-pipeline-kit

Version:

A type-safe declarative way of creating WebGPU pipelines

1,538 lines (1,517 loc) 90.4 kB
// src/logging.ts import log from "loglevel"; var prefix = "webgpu-pipeline-kit"; var getLogger = (namespace) => log.getLogger(`${prefix}:${namespace}`); var lazyWarn = (logger, warnMessageFunc) => { if (logger.getLevel() <= logger.levels.WARN) { logger.warn(warnMessageFunc()); } }; var lazyInfo = (logger, infoMessageFunc) => { if (logger.getLevel() <= logger.levels.INFO) { logger.info(infoMessageFunc()); } }; var lazyDebug = (logger, debugMessageFunc) => { if (logger.getLevel() <= logger.levels.DEBUG) { logger.debug(debugMessageFunc()); } }; var lazyTrace = (logger, traceMessageFunc) => { if (logger.getLevel() <= logger.levels.TRACE) { logger.trace(traceMessageFunc()); } }; var setLogLevel = (level, namespace) => { if (namespace !== void 0) { getLogger(namespace).setLevel(level); } else { const allLoggers = log.getLoggers(); for (const [name, logger] of Object.entries(allLoggers)) { if (name.startsWith(prefix)) { logger.setLevel(level); } } } }; // src/utils/compare.ts var equalityFactory = { ofDoubleEquals: () => (a, b) => a == b, ofTripleEquals: () => (a, b) => a === b }; // src/utils/array.ts var arrayFuncs = { equals: (a, b, equality = equalityFactory.ofTripleEquals()) => { if (a.length !== b.length) { return false; } return a.every((val, i) => equality(val, b[i])); }, shuffle: (array, random2) => { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = random2.intMinMax(0, i); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; }, merge: (...arrays) => arrays.flatMap((arr) => arr ?? []), toRecord: (array, toKey, toValue) => { return array.reduce((acc, element) => { const key = toKey(element); const value = toValue(element); acc[key] = value; return acc; }, {}); }, toMap: (array, toKey, toValue) => { return array.reduce((acc, element, index) => { const key = toKey(element, index); const value = toValue(element, index); acc.set(key, value); return acc; }, /* @__PURE__ */ new Map()); } }; // src/utils/BidiMap.ts var BidiMap = class { constructor() { this.keyValues = /* @__PURE__ */ new Map(); this.valueKeys = /* @__PURE__ */ new Map(); } get size() { return this.keyValues.size; } clear() { this.keyValues.clear(); this.valueKeys.clear(); } entries() { return this.keyValues.entries(); } forEach(callbackFn) { for (const [key, value] of this.keyValues) { callbackFn(value, key, this); } } delete(key) { const hasKey = this.keyValues.has(key); if (hasKey) { const value = this.keyValues.get(key); this.keyValues.delete(key); if (value !== void 0) { this.valueKeys.delete(value); } } return hasKey; } deleteValue(value) { const hasValue = this.valueKeys.has(value); if (hasValue) { const key = this.valueKeys.get(value); this.valueKeys.delete(value); if (key !== void 0) { this.keyValues.delete(key); } } return hasValue; } get(key) { return this.keyValues.get(key); } getKey(value) { return this.valueKeys.get(value); } set(key, value, removeDuplicateValue) { if (removeDuplicateValue || !this.valueKeys.has(value)) { this.keyValues.set(key, value); this.valueKeys.set(value, key); } return this; } has(key) { return this.keyValues.has(key); } hasValue(value) { return this.valueKeys.has(value); } keys() { return this.keyValues.keys(); } values() { return this.valueKeys.keys(); } }; // src/utils/Capacity.ts var Capacity = class { constructor(min, requiredMultiple, capacityMultiple) { this.min = min; this.requiredMultiple = requiredMultiple; this.capacityMultiple = capacityMultiple; this._capacity = 0; this._capacity = min; } get capacity() { return this._capacity; } set capacity(capacity) { this._capacity = capacity; } ensureCapacity(required) { const oldCapacity = this._capacity; const hasIncreased = required > this._capacity; if (hasIncreased) { this._capacity = Math.round( Math.max( this.min, required * this.requiredMultiple, oldCapacity * this.capacityMultiple ) ); } return hasIncreased; } }; // src/utils/changeDetector.ts var changeDetectorFactory = { of: (first, equals) => { let current = first; return { get() { return current; }, compareAndUpdate(next) { const hasChanged = current === void 0 || !equals(current, next); current = next; return hasChanged; } }; }, ofDoubleEquals: (first) => { return changeDetectorFactory.of(first, (a, b) => a == b); }, ofTripleEquals: (first) => { return changeDetectorFactory.of(first, (a, b) => a === b); } }; // src/utils/Color.ts var colorFactory = { BLACK: { r: 0, g: 0, b: 0, a: 1 }, RED: { r: 1, g: 0, b: 0, a: 1 }, GREEN: { r: 0, g: 1, b: 0, a: 1 }, BLUE: { r: 0, g: 0, b: 1, a: 1 }, WHITE: { r: 1, g: 1, b: 1, a: 1 }, of(r = 0, g = 0, b = 0, a = 1) { return { r, g, b, a }; } }; // src/utils/floats.ts var floatFuncs = { float32ToFloat16: (val) => { const floatView = new Float32Array(1); const intView = new Uint32Array(floatView.buffer); floatView[0] = val; const x = intView[0]; const sign = x >>> 31 << 15; const exponent = (x >> 23 & 255) - 127 + 15; const mantissa = x & 8388607; if (exponent <= 0) { if (exponent < -10) { return sign; } const m = (mantissa | 8388608) >> 1 - exponent; return sign | m + 4096 >> 13; } else if (exponent === 255 - 127 + 15) { if (mantissa === 0) { return sign | 31744; } return sign | 31744 | (mantissa >> 13 || 1); } else if (exponent >= 31) { return sign | 31744; } return sign | exponent << 10 | mantissa + 4096 >> 13; } }; // src/utils/functions.ts var callCreatorOf = () => (methodName) => (target, ...args) => target[methodName](...args); // src/utils/math.ts var mathFuncs = { HALF_PI: 0.5 * Math.PI, TWO_PI: 2 * Math.PI, clamp: (value, min, max) => { return Math.max(min, Math.min(max, value)); }, gcd: (...numbers) => { if (numbers.length === 0) { throw new Error("Array must contain at least one number."); } const gcdAB = (a, b) => { while (b !== 0) { const temp = b; b = a % b; a = temp; } return Math.abs(a); }; return numbers.reduce((acc, num) => gcdAB(acc, num)); }, geometricMean: (...elements) => { const product = elements.reduce((total, value) => total * value, 1); return Math.pow(product, 1 / elements.length); }, harmonicMean: (...elements) => { const sumReciprocals = elements.reduce((total, value) => total + 1 / value, 0); return elements.length / sumReciprocals; }, sumMapValues: (map) => Array.from(map.values()).reduce((sum, value) => sum + value, 0), nextMultipleOf: (num, multiple) => Math.ceil(num / multiple) * multiple }; // src/utils/random.ts import { Xoshiro128 } from "@thi.ng/random"; var randomFactory = { ofSeed: (seed) => { const xoshiro128 = new Xoshiro128([seed.a, seed.b, seed.c, seed.d]); return createRandomFromXoshiro128(xoshiro128); }, ofString: (str) => { const seed = createSeedFromString(str); const xoshiro128 = new Xoshiro128([seed.a, seed.b, seed.c, seed.d]); return createRandomFromXoshiro128(xoshiro128); } }; var createSeedFromString = (str) => { const length2 = str.length; if (length2 < 4) { throw Error(`Seed must have length >= 4 but is ${length2}`); } const a = createSeedElement(str, 0, 0); const b = createSeedElement(str, 0, Math.round(0.25 * length2)); const c = createSeedElement(str, 0, Math.round(0.5 * length2)); const d = createSeedElement(str, 0, Math.round(0.75 * length2)); return { a, b, c, d }; }; var createSeedElement = (seed, hashStart, index) => { const hashMiddle = hashChars(seed, hashStart, index, seed.length); const hashEnd = hashChars(seed, hashMiddle, 0, index); return hashEnd >>> 0; }; var hashChars = (seed, hash, startIndex, endIndex) => { for (let i = startIndex; i < endIndex; i++) { hash = Math.imul(hash ^ seed.charCodeAt(i), 1540483477); hash = hash ^ hash >> 13; } return hash; }; var createRandomFromXoshiro128 = (xoshiro128) => { return { getSeed() { const buffer = xoshiro128.buffer; return { a: buffer[0], b: buffer[1], c: buffer[2], d: buffer[3] }; }, boolean() { return this.true(0.5); }, false(proportion) { return this.true(1 - proportion); }, true(proportion) { return xoshiro128.float() < proportion; }, int() { return xoshiro128.int(); }, intRange(range) { return this.intMinMax(range.min, range.max); }, intMirrorZero(minMax) { const abs = Math.abs(minMax); return this.intMinMax(-abs, abs); }, intMinMax(min, max) { return xoshiro128.minmaxInt(min, max + 1); }, float() { return xoshiro128.float(); }, floatRange(range) { return this.floatMinMax(range.min, range.max); }, floatMirrorZero(minMax) { const abs = Math.abs(minMax); return this.floatMinMax(-abs, abs); }, floatMinMax(min, max) { return xoshiro128.minmax(min, max); }, lowerCharCode() { return this.intMinMax(97, 123); }, upperCharCode() { return this.intMinMax(65, 91); }, uuidV4Like() { const hex = []; for (let i = 0; i < 4; i++) { const r = this.int() >>> 0; hex.push(r.toString(16).padStart(8, "0")); } return hex[0] + "-" + hex[1].slice(0, 4) + "-4" + hex[1].slice(5, 8) + "-" + (parseInt(hex[2][0], 16) & 3 | 8).toString(16) + hex[2].slice(1, 4) + "-" + hex[2].slice(4) + hex[3]; }, word() { const min = this.intMinMax(2, 5); const max = this.intMinMax(5, 8); return this.wordMinMax(min, max); }, wordRange(range) { return this.wordMinMax(range.min, range.max); }, wordMinMax(min, max) { const charCodes = []; charCodes.push(this.upperCharCode()); const subsequent = xoshiro128.minmaxUint(min, max); for (let i = 1; i <= subsequent; i++) { charCodes.push(this.lowerCharCode()); } return String.fromCharCode(...charCodes); } }; }; // src/utils/record.ts var recordFuncs = { filter: (record, predicate) => { return Object.fromEntries( Object.entries(record).filter( ([key, value]) => predicate(value, key) ) ); }, forEach: (record, apply) => { Object.entries(record).map(([key, value]) => [key, apply(value, key)]); }, mapRecord: (record, transform) => { return Object.fromEntries( Object.entries(record).map(([key, value]) => [key, transform(value, key)]) ); }, toMap: (record, transform) => { return new Map(Object.entries(record).map(([key, value]) => [key, transform(value, key)])); }, toArray: (record, transform) => { return Object.entries(record).map(([key, value]) => transform(value, key)); } }; // src/utils/slice.ts var sliceFuncs = { ofInts: (nums) => { const slices = []; if (nums.length === 0) { return slices; } const sorted = nums.sort((a, b) => a - b); let min = sorted[0]; let max = min; for (let i = 1; i < sorted.length; i++) { const num = sorted[i]; if (!Number.isInteger(num)) { throw Error(`Cannot create slices with non-integer number ${num}`); } if (num === max || num === max + 1) { max = num; } else { slices.push({ min, length: max + 1 - min }); min = max = num; } } slices.push({ min, length: max + 1 - min }); return slices; }, ofMap: (map) => { const sortedEntries = Array.from(map.entries()).sort(([indexA], [indexB]) => indexA - indexB); const values = sortedEntries.map(([, value]) => value); const indexes = sortedEntries.map(([index]) => index); const slices = sliceFuncs.ofInts(indexes); const copySlices = Array.from({ length: slices.length }); let fromIndex = 0; for (const [sliceIndex, slice] of slices.entries()) { const { length: length2, min } = slice; copySlices[sliceIndex] = { min: fromIndex, length: length2, toIndex: min }; fromIndex += length2; } return { copySlices, values }; } }; // src/utils/string.ts var stringFuncs = { canBePositiveInt: (str) => "0" === str || /^[1-9]\d*$/.test(str) }; // src/utils/Vec3.ts var vec3Funcs = { ZERO: [0, 0, 0], ONE: [1, 1, 1], X: [1, 0, 0], Y: [0, 1, 0], Z: [0, 0, 1], of(x = 0, y = 0, z = 0) { return [x, y, z]; }, add(a, b) { return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }, subtract(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }, cross(a, b) { const a0 = a[0]; const a1 = a[1]; const a2 = a[2]; const b0 = b[0]; const b1 = b[1]; const b2 = b[2]; return [ a1 * b2 - a2 * b1, a2 * b0 - a0 * b2, a0 * b1 - a1 * b0 ]; }, dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }, distance(a, b) { return vec3Funcs.length(vec3Funcs.subtract(a, b)); }, distanceSquared(a, b) { return vec3Funcs.lengthSquared(vec3Funcs.subtract(a, b)); }, length(v) { return Math.hypot(v[0], v[1], v[2]); }, lengthSquared(v) { return v[0] ** 2 + v[1] ** 2 + v[2] ** 2; }, lerp(a, b, t) { const a0 = a[0]; const a1 = a[1]; const a2 = a[2]; return [ a0 + (b[0] - a0) * t, a1 + (b[1] - a1) * t, a2 + (b[2] - a2) * t ]; }, midpoint(a, b) { return vec3Funcs.normalize([ (a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2 ]); }, negate(v) { return [-v[0], -v[1], -v[2]]; }, normalize(v) { const len = vec3Funcs.length(v); return len === 0 ? [0, 0, 0] : vec3Funcs.scale(v, 1 / len); }, scale(v, scalar) { return [v[0] * scalar, v[1] * scalar, v[2] * scalar]; }, equals(a, b, epsilon = 1e-6) { return Math.abs(a[0] - b[0]) < epsilon && Math.abs(a[1] - b[1]) < epsilon && Math.abs(a[2] - b[2]) < epsilon; }, toString(v) { return `[${v[0].toFixed(3)}, ${v[1].toFixed(3)}, ${v[2].toFixed(3)}]`; } }; // src/cache.ts var LOGGER = getLogger("cache"); var random = randomFactory.ofString((/* @__PURE__ */ new Date()).toISOString()); var cacheFactory = { ofUniform: (_uniformFormat, initialUniform, mutable) => { lazyDebug(LOGGER, () => `Creating uniform cache from format ${JSON.stringify(_uniformFormat)}`); let previous = initialUniform; let next = initialUniform; const cache = { isMutable: mutable, isDirty: () => previous !== next, get() { previous = next; return next; } }; if (mutable) { lazyTrace(LOGGER, () => "Making uniform cache mutable"); const typedCached = cache; typedCached.mutate = (uniform) => next = uniform; } return cache; }, ofEntitiesFixedSize: (_entityFormat, mutable, ...elements) => { lazyDebug(LOGGER, () => `Creating fixed size entity cache from format ${JSON.stringify(_entityFormat)}`); const mutated = /* @__PURE__ */ new Map(); elements.forEach((element, index) => mutated.set(index, element)); const cache = { isMutable: mutable, isResizeable: false, count: () => elements.length, isDirty: () => mutated.size > 0, calculateChanges() { const slices = sliceFuncs.ofMap(mutated); mutated.clear(); lazyTrace(LOGGER, () => `Calculated entity cache changes ${JSON.stringify(slices)}`); return slices; } }; if (mutable) { lazyTrace(LOGGER, () => "Making entity cache mutable"); const typedCache = cache; typedCache.mutate = (index, instance) => { lazyTrace(LOGGER, () => `Mutating instance with index ${index}`); mutated.set(index, instance); }; } return cache; }, ofEntitiesResizeable: (_entityFormat, mutable) => { lazyDebug(LOGGER, () => `Creating resizeable entity cache from format ${JSON.stringify(_entityFormat)}`); const idIndexes = new BidiMap(); const backing = /* @__PURE__ */ new Map(); const added = /* @__PURE__ */ new Map(); const mutated = /* @__PURE__ */ new Map(); const removed = /* @__PURE__ */ new Set(); const cache = { isMutable: mutable, isResizeable: true, count: () => backing.size, isDirty: () => added.size > 0 || mutated.size > 0 || removed.size > 0, calculateChanges() { added.forEach((instance, id) => backing.set(id, instance)); mutated.forEach((instance, id) => backing.set(id, instance)); removed.forEach((id) => backing.delete(id)); const stagingIdInstances = [ ...added, ...mutated ]; const stagingIds = stagingIdInstances.map(([id]) => id); const stagingInstances = stagingIdInstances.map(([, instance]) => instance); const copySlices = []; const previousUsed = idIndexes.size; const newUsed = previousUsed + added.size - removed.size; const oldIds = [ ...mutated.keys(), ...removed.keys() ]; const oldIndexes = oldIds.map((id) => idIndexes.get(id)).filter((index) => index !== void 0); oldIndexes.forEach((oldIndex) => idIndexes.deleteValue(oldIndex)); const overwrittenIndexes = oldIndexes.filter((index) => index < newUsed); const overwrittenSlices = sliceFuncs.ofInts(overwrittenIndexes); let stagingIndexOffset = 0; for (const overwrittenSlice of overwrittenSlices) { const { min, length: length2 } = overwrittenSlice; const copySlice = { min: stagingIndexOffset, length: length2 }; copySlices.push({ ...copySlice, toIndex: min }); for (let i = 0; i < length2; i++) { const newIndex = min + i; const stagingIndex = stagingIndexOffset + i; const stagingId = stagingIds[stagingIndex]; idIndexes.set(stagingId, newIndex, true); } stagingIndexOffset += length2; } if (newUsed > previousUsed) { const appendCount = newUsed - previousUsed; const appendSlice = { min: stagingIndexOffset, length }; copySlices.push({ ...appendSlice, toIndex: previousUsed }); for (let i = 0; i < appendCount; i++) { const newIndex = previousUsed + i; const stagingIndex = stagingIndexOffset + i; const stagingId = stagingIds[stagingIndex]; idIndexes.set(stagingId, newIndex, true); } } else if (newUsed < previousUsed) { const replacedIndexes = oldIndexes.filter((index) => index >= newUsed); let movedIndex = previousUsed; for (const replacedIndex of replacedIndexes) { movedIndex--; while (movedIndex > newUsed && replacedIndexes.includes(movedIndex)) { movedIndex--; } if (movedIndex > replacedIndex) { const movedId = idIndexes.getKey(movedIndex); if (movedId !== void 0) { const movedSlice = { min: movedIndex, length: 1 }; copySlices.push({ ...movedSlice, toIndex: replacedIndex }); idIndexes.set(movedId, replacedIndex, true); } } } } const command = { values: stagingInstances, copySlices }; added.clear(); removed.clear(); mutated.clear(); lazyTrace(LOGGER, () => `Calculated entity cache changes ${JSON.stringify(command)}`); return command; }, add(element) { const id = random.uuidV4Like(); lazyTrace(LOGGER, () => `Add element with id ${id}`); added.set(id, element); return id; }, remove(id) { lazyTrace(LOGGER, () => `Remove element with id ${id}`); if (backing.has(id)) { added.delete(id); mutated.delete(id); removed.add(id); } }, indexOf(id) { const index = idIndexes.get(id); const validIndex = index === void 0 ? -1 : index; lazyTrace(LOGGER, () => `Index of id ${id} is ${validIndex}`); return validIndex; } }; if (mutable) { lazyTrace(LOGGER, () => "Making entity cache mutable"); const typedCache = cache; typedCache.mutate = (id, instance) => { lazyTrace(LOGGER, () => `Mutating instance with id ${id}`); if (!removed.has(id)) { if (backing.has(id)) { mutated.set(id, instance); } else if (added.has(id)) { added.set(id, instance); } } }; } return cache; } }; // src/mesh.ts var LOGGER2 = getLogger("mesh"); var meshFuncs = { UINT32_INDEX_COUNT: 1 << 16, VERTICES_PER_TRIANGLE: 3, cullMode: (mesh) => mesh.winding === "cw" ? "front" : "back", indicesType: (mesh) => mesh.indices.length < meshFuncs.UINT32_INDEX_COUNT ? "uint16" : "uint32", indicesCount: (mesh) => mesh.indices.length * 3, indicesBytesLength: (mesh) => { const indicesCount = meshFuncs.indicesCount(mesh); return indicesCount < meshFuncs.UINT32_INDEX_COUNT ? indicesCount * 2 : indicesCount * 4; }, toIndicesData: (mesh) => { const indicesType = meshFuncs.indicesType(mesh); const flatIndices = mesh.indices.flat(); const TypedArrayConstructor = indicesType === "uint16" ? Uint16Array : Uint32Array; const typedArray = new TypedArrayConstructor(flatIndices); const byteLength = typedArray.byteLength; if (byteLength % 4 === 0) { return typedArray.buffer; } const paddedByteLength = mathFuncs.nextMultipleOf(byteLength, 4); const buffer = new ArrayBuffer(paddedByteLength); const targetView = new TypedArrayConstructor(buffer); targetView.set(typedArray); return buffer; }, toVerticesData: (mesh) => { const flatVertices = mesh.vertices.flat(); const verticesArray = new Float32Array(flatVertices); return verticesArray.buffer; } }; var meshFactory = { of: (type, vertices, indices, winding) => { return { topology: type, vertices, indices, winding }; }, triangle: (topProportion, axis = vec3Funcs.Z) => { lazyDebug(LOGGER2, () => `Creating triangle mesh with top proportion ${topProportion}`); const axisNormalized = vec3Funcs.normalize(axis); const up = Math.abs(axisNormalized[1]) < 0.99 ? [0, 1, 0] : [1, 0, 0]; const tangent = vec3Funcs.normalize(vec3Funcs.cross(up, axisNormalized)); const bitangent = vec3Funcs.cross(axisNormalized, tangent); const baseY = -1; const topY = 1; const leftX = -1; const rightX = 1; const topX = leftX + (rightX - leftX) * topProportion; const localPoints = [ [leftX, baseY], [rightX, baseY], [topX, topY] ]; const vertices = []; for (const [x, y] of localPoints) { const vx = tangent[0] * x + bitangent[0] * y; const vy = tangent[1] * x + bitangent[1] * y; const vz = tangent[2] * x + bitangent[2] * y; vertices.push([vx, vy, vz]); } const indices = [[0, 1, 2]]; return { topology: "triangle-list", vertices, indices, winding: "cw" }; }, cube: () => { lazyDebug(LOGGER2, () => "Creating cube mesh"); const vertices = [ // Front face [-1, -1, 1], // front bottom left [1, -1, 1], // front bottom right [1, 1, 1], // front top right [-1, 1, 1], // front top left // Back face [-1, -1, -1], // back bottom left [-1, 1, -1], // back top left [1, 1, -1], // back top right [1, -1, -1] // back bottom right ]; const indices = [ // Front [0, 1, 2], [0, 2, 3], // Top [3, 2, 6], [3, 6, 5], // Back [5, 6, 7], [5, 7, 4], // Bottom [4, 7, 1], [4, 1, 0], // Left [4, 0, 3], [4, 3, 5], // Right [1, 7, 6], [1, 6, 2] ]; return { topology: "triangle-list", vertices, indices, winding: "cw" }; }, sphere(subdivisions) { lazyDebug(LOGGER2, () => `Creating sphere mesh with sub divisions ${subdivisions}`); const phi = (1 + Math.sqrt(5)) / 2; const unnormalizedVertices = [ [-1, phi, 0], [1, phi, 0], [-1, -phi, 0], [1, -phi, 0], [0, -1, phi], [0, 1, phi], [0, -1, -phi], [0, 1, -phi], [phi, 0, -1], [phi, 0, 1], [-phi, 0, -1], [-phi, 0, 1] ]; const vertices = unnormalizedVertices.map(vec3Funcs.normalize); const baseIndices = [ [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1] ]; const indices = []; const cache = /* @__PURE__ */ new Map(); const getOrCreateMidpoint = (a, b) => { const key = [Math.min(a, b), Math.max(a, b)].join("|"); if (cache.has(key)) { return cache.get(key); } const midpoint = vec3Funcs.midpoint(vertices[a], vertices[b]); const index = vertices.length; vertices.push(midpoint); cache.set(key, index); return index; }; const subdivide = (a, b, c, depth) => { if (depth === 0) { indices.push([a, b, c]); } else { const ab = getOrCreateMidpoint(a, b); const bc = getOrCreateMidpoint(b, c); const ca = getOrCreateMidpoint(c, a); subdivide(a, ab, ca, depth - 1); subdivide(b, bc, ab, depth - 1); subdivide(c, ca, bc, depth - 1); subdivide(ab, bc, ca, depth - 1); } }; for (const [a, b, c] of baseIndices) { subdivide(a, b, c, subdivisions); } return { topology: "triangle-list", vertices, indices, winding: "cw" }; } }; // src/buffer-factory.ts var MINIMUM_BYTES_LENGTH = 16; var VALID_BYTES_MULTIPLE = 4; var LOGGER3 = getLogger("buffer"); var checkNotDestroyed = (state) => { if (state === 3 /* Destroyed */) { throw Error("Cannot use managed buffer after being destroyed"); } }; var toValidSize = (label, bytesLength) => { const clampedBytesLength = Math.max(MINIMUM_BYTES_LENGTH, bytesLength); const validSize = mathFuncs.nextMultipleOf(clampedBytesLength, VALID_BYTES_MULTIPLE); if (validSize > bytesLength) { lazyDebug(LOGGER3, () => `Increasing desired length of buffer '${label}' from ${bytesLength} to valid length ${validSize}`); } return validSize; }; var bufferFactory = { ofData: (data, label, usage) => { lazyDebug(LOGGER3, () => `Creating buffer '${label}' of usage ${usageToString(usage)} from data of byte length ${data.byteLength}`); const size = toValidSize(label, data.byteLength); if (size > data.byteLength) { lazyTrace(LOGGER3, () => `Aligning buffer ${label} to new size ${size}`); const alignedBuffer = new ArrayBuffer(size); new Uint8Array(alignedBuffer).set(new Uint8Array(data)); data = alignedBuffer; } usage |= GPUBufferUsage.COPY_DST; let state = 0 /* Initialized */; let trackedBuffer; return { get(device, queue, _encoder) { checkNotDestroyed(state); if (trackedBuffer === void 0) { lazyTrace(LOGGER3, () => `Creating new buffer ${label} of size ${size} and usage ${usageToString(usage)}`); const buffer = device.createBuffer({ label, size, usage }); lazyTrace(LOGGER3, () => `Writing data to new buffer ${label} with queue`); queue.writeBuffer(buffer, 0, data); state = 1 /* New */; trackedBuffer = { isNew: true, buffer, destroy() { state = 3 /* Destroyed */; buffer.destroy(); } }; } else { lazyTrace(LOGGER3, () => `Reusing buffer ${label}`); if (state === 1 /* New */) { state = 2 /* Reused */; trackedBuffer = { ...trackedBuffer, isNew: false }; } } return trackedBuffer; } }; }, ofSize: (bytesLength, label, usage) => { lazyDebug(LOGGER3, () => `Creating buffer '${label}' of usage ${usageToString(usage)} of byte length ${bytesLength}`); const size = toValidSize(label, bytesLength); let state = 0 /* Initialized */; let trackedBuffer; return { get(device, _queue, _encoder) { checkNotDestroyed(state); if (trackedBuffer === void 0) { lazyTrace(LOGGER3, () => `Creating new buffer ${label} of size ${size} and usage ${usageToString(usage)}`); const buffer = device.createBuffer({ label, size, usage }); state = 1 /* New */; trackedBuffer = { buffer, isNew: true, destroy() { state = 3 /* Destroyed */; buffer.destroy(); } }; } else { lazyTrace(LOGGER3, () => `Reusing buffer ${label}`); if (state === 1 /* New */) { state = 2 /* Reused */; trackedBuffer = { ...trackedBuffer, isNew: false }; } } return trackedBuffer; } }; }, ofResizeable: (copyDataOnResize, label, usage) => { lazyDebug(LOGGER3, () => `Creating resizeable buffer '${label}' of usage ${usageToString(usage)}`); let previousBuffer; let currentBuffer; const capacity = new Capacity(MINIMUM_BYTES_LENGTH, 1.2, 1.5); let previousBytesLength = capacity.capacity; let desiredBytesLength = 0; let state = 0 /* Initialized */; let trackedBuffer; if (copyDataOnResize) { usage |= GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST; } return { resize(bytesLength) { lazyDebug(LOGGER3, () => `Resizing buffer ${label} to desired size ${desiredBytesLength}`); desiredBytesLength = bytesLength; }, get(device, queue, encoder) { checkNotDestroyed(state); if (previousBuffer !== void 0) { lazyDebug(LOGGER3, () => `Destroying previous buffer ${label}`); previousBuffer.destroy(); previousBuffer = void 0; } if (trackedBuffer === void 0 || desiredBytesLength > capacity.capacity) { previousBytesLength = capacity.capacity; capacity.ensureCapacity(desiredBytesLength); capacity.capacity = toValidSize(label, capacity.capacity); lazyTrace(LOGGER3, () => `Creating new buffer ${label} of size ${capacity.capacity} and usage ${usageToString(usage)}`); previousBuffer = currentBuffer; const newBuffer = currentBuffer = device.createBuffer({ label, size: capacity.capacity, usage }); state = 1 /* New */; trackedBuffer = { buffer: currentBuffer, isNew: true, destroy() { state = 3 /* Destroyed */; newBuffer.destroy(); } }; if (copyDataOnResize && previousBuffer !== void 0) { const copySize = Math.min(previousBytesLength, capacity.capacity); if (copySize > 0) { lazyTrace(LOGGER3, () => `Copy ${copySize} bytes of data to buffer ${label} from previous`); encoder.copyBufferToBuffer(previousBuffer, currentBuffer, copySize); } } } else if (trackedBuffer !== void 0 && state === 1 /* New */) { lazyTrace(LOGGER3, () => `Reusing buffer ${label}`); state = 2 /* Reused */; trackedBuffer = { ...trackedBuffer, isNew: false }; } return trackedBuffer; } }; }, ofMutable: (bytesLength, label, usage) => { lazyDebug(LOGGER3, () => `Creating mutable buffer '${label}' of usage ${usageToString(usage)} from data of byte length ${bytesLength}`); const size = toValidSize(label, bytesLength); let state = 0 /* Initialized */; let trackedBuffer; const mutatedDataArray = []; return { mutate(data, index) { LOGGER3.info(`Mutating data for mutable buffer ${label}`); mutatedDataArray.push({ data, index }); }, get(device, queue, _encoder) { checkNotDestroyed(state); if (trackedBuffer === void 0) { lazyTrace(LOGGER3, () => `Creating new buffer ${label} of size ${size} and usage ${usageToString(usage)}`); const buffer2 = device.createBuffer({ label, size, usage }); state = 1 /* New */; trackedBuffer = { buffer: buffer2, isNew: true, destroy() { state = 3 /* Destroyed */; buffer2.destroy(); } }; } else { lazyTrace(LOGGER3, () => `Reusing buffer ${label}`); if (state === 1 /* New */) { state = 2 /* Reused */; trackedBuffer = { ...trackedBuffer, isNew: false }; } } const { buffer } = trackedBuffer; for (const { data, index } of mutatedDataArray) { lazyTrace(LOGGER3, () => `Writing mutated data to buffer ${label} at index ${index} and length ${data.byteLength} using queue`); queue.writeBuffer(buffer, index, data); } mutatedDataArray.length = 0; return trackedBuffer; } }; }, ofStaged: (label, usage) => { lazyDebug(LOGGER3, () => `Creating staged buffer '${label}' of usage ${usageToString(usage)}`); const stagingLabel = `${label}-staging`; const backingLabel = `${label}-backing`; const staging = bufferFactory.ofResizeable(false, stagingLabel, GPUBufferUsage.COPY_SRC); const backing = bufferFactory.ofResizeable(true, backingLabel, usage | GPUBufferUsage.COPY_DST); let mutatedSlices = void 0; return { mutate(data, target) { LOGGER3.info(`Mutating data for staged buffer ${label}`); mutatedSlices = { values: data, copySlices: target }; }, get(device, queue, encoder) { const backingTrackedBuffer = backing.get(device, queue, encoder); if (mutatedSlices !== void 0) { lazyTrace(LOGGER3, () => `Staging data to buffer ${stagingLabel}`); const { values, copySlices } = mutatedSlices; const backingSizeRequired = copySlices.reduce((max, copySlice) => Math.max(max, copySlice.toIndex + copySlice.length), 0); lazyTrace(LOGGER3, () => `Resizing buffer ${stagingLabel} to ${values.byteLength}`); lazyTrace(LOGGER3, () => `Resizing buffer ${backingLabel} to ${backingSizeRequired}`); staging.resize(values.byteLength); backing.resize(backingSizeRequired); const stagingBuffer = staging.get(device, queue, encoder).buffer; const backingBuffer = backingTrackedBuffer.buffer; lazyTrace(LOGGER3, () => `Writing staging data of length ${values.byteLength} to buffer ${stagingLabel} using queue`); queue.writeBuffer(stagingBuffer, 0, values); for (const copySlice of copySlices) { const { length: length2, min, toIndex } = copySlice; lazyTrace(LOGGER3, () => `Copy staging data from ${stagingLabel} at index ${min} to ${backingLabel} at index ${toIndex} of length ${length2} using encoder`); encoder.copyBufferToBuffer(stagingBuffer, min, backingBuffer, toIndex, length2); } mutatedSlices = void 0; } return backingTrackedBuffer; } }; } }; var usageToString = (usage) => { const flagNames = []; if ((usage & GPUBufferUsage.COPY_DST) === GPUBufferUsage.COPY_DST) { flagNames.push("COPY_DST"); } if ((usage & GPUBufferUsage.COPY_SRC) === GPUBufferUsage.COPY_SRC) { flagNames.push("COPY_SRC"); } if ((usage & GPUBufferUsage.INDEX) === GPUBufferUsage.INDEX) { flagNames.push("INDEX"); } if ((usage & GPUBufferUsage.INDIRECT) === GPUBufferUsage.INDIRECT) { flagNames.push("INDIRECT"); } if ((usage & GPUBufferUsage.MAP_READ) === GPUBufferUsage.MAP_READ) { flagNames.push("MAP_READ"); } if ((usage & GPUBufferUsage.MAP_WRITE) === GPUBufferUsage.MAP_WRITE) { flagNames.push("MAP_WRITE"); } if ((usage & GPUBufferUsage.QUERY_RESOLVE) === GPUBufferUsage.QUERY_RESOLVE) { flagNames.push("QUERY_RESOLVE"); } if ((usage & GPUBufferUsage.STORAGE) === GPUBufferUsage.STORAGE) { flagNames.push("STORAGE"); } if ((usage & GPUBufferUsage.UNIFORM) === GPUBufferUsage.UNIFORM) { flagNames.push("UNIFORM"); } if ((usage & GPUBufferUsage.VERTEX) === GPUBufferUsage.VERTEX) { flagNames.push("VERTEX"); } return `[${flagNames.join(", ")}]`; }; // src/buffer-formats.ts var isUserFormatScalar = (userFormat) => userFormat.scalar !== void 0; var isUserFormatVec2 = (userFormat) => userFormat.vec2 !== void 0; var isUserFormatVec3 = (userFormat) => userFormat.vec3 !== void 0; var isUserFormatVec4 = (userFormat) => userFormat.vec4 !== void 0; var isUserFormatEntityIndex = (userFormat) => userFormat.entityIndexFromResizeableEntityCache !== void 0; // src/strides.ts var strideFuncs = { ofVertexFormat: (format) => { switch (format) { case "float16": return 2; case "float32": return 4; case "sint16": return 2; case "sint32": return 4; case "sint8": return 1; case "snorm16": return 2; case "snorm8": return 1; case "uint16": return 2; case "uint32": return 4; case "uint8": return 1; case "unorm16": return 2; case "unorm8": return 1; } }, dimensionMultipleOfUserFormat: (userFormat) => { if (isUserFormatScalar(userFormat)) { return 1; } else if (isUserFormatVec2(userFormat)) { return 2; } else if (isUserFormatVec3(userFormat)) { return 3; } else if (isUserFormatVec4(userFormat)) { return 4; } throw Error(`Cannot find dimension of user format ${JSON.stringify(userFormat)}`); }, dimensionMultipleOfLayout: (dimension) => { switch (dimension) { case "scalar": return 1; case "vec2": return 2; case "vec3": return 3; case "vec4": return 4; } }, ofLayout: (layout) => strideFuncs.dimensionMultipleOfLayout(layout.dimension) * strideFuncs.ofVertexFormat(layout.datumType), ofFormatLayout: (formatLayout) => { return formatLayout.reduce((acc, layout) => acc + strideFuncs.ofLayout(layout), 0); }, ofUserFormat: (userFormat) => { const datumStride = strideFuncs.ofVertexFormat(userFormat.datumType); const multiple = strideFuncs.dimensionMultipleOfUserFormat(userFormat); return datumStride * multiple; }, ofFormatMarshall: (format) => { return format.reduce((acc, userFormat) => acc + strideFuncs.ofUserFormat(userFormat), 0); } }; // src/data-extractor.ts var LOGGER4 = getLogger("data"); var createDatumSetters = () => { const dataViewCallCreator = callCreatorOf(); const map = /* @__PURE__ */ new Map(); const setInt8 = dataViewCallCreator("setInt8"); const setUint8 = dataViewCallCreator("setUint8"); const setInt16 = dataViewCallCreator("setInt16"); const setUint16 = dataViewCallCreator("setUint16"); const setInt32 = dataViewCallCreator("setInt32"); const setUint32 = dataViewCallCreator("setUint32"); const setFloat32 = dataViewCallCreator("setFloat32"); map.set("sint8", setInt8); map.set("snorm8", setInt8); map.set("uint8", setUint8); map.set("unorm8", setUint8); map.set("sint16", setInt16); map.set("snorm16", setInt16); map.set("uint16", setUint16); map.set("unorm16", setUint16); map.set("sint32", setInt32); map.set("uint32", setUint32); map.set("float16", (target, offset, value, littleEndian) => setUint16(target, offset, floatFuncs.float32ToFloat16(value), littleEndian)); map.set("float32", setFloat32); return map; }; var datumSetters = createDatumSetters(); var LITTLE_ENDIAN = true; var dataExtractorFactory = { of: (marshallFormats) => { lazyDebug(LOGGER4, () => "Create data extractor"); let totalStride = 0; const dataBridges = []; for (const userFormat of marshallFormats) { lazyTrace(LOGGER4, () => `Create ref from user format ${JSON.stringify(userFormat)}`); const userFormatRef = toMarshalledRef(userFormat); const { datumType } = userFormat; const datumStride = strideFuncs.ofVertexFormat(datumType); const stride = userFormatRef.datumCount * datumStride; const datumSetter = datumSetters.get(datumType); if (datumSetter === void 0) { throw Error(`Cannot set datum of type ${datumType}`); } const datumOffset = totalStride; dataBridges.push((offset, instance, dataView) => { const values = userFormatRef.valuesOf(instance); if (typeof values === "number") { lazyTrace(LOGGER4, () => `Setting value ${values} at offset ${offset + datumOffset}`); datumSetter(dataView, offset + datumOffset, values, LITTLE_ENDIAN); } else { for (const value of values) { lazyTrace(LOGGER4, () => `Setting value ${value} at offset ${offset + datumOffset}`); datumSetter(dataView, offset + datumOffset, value, LITTLE_ENDIAN); offset += datumStride; } } }); totalStride += stride; } return { extract(instances) { lazyDebug(LOGGER4, () => `Extract data from ${instances.length} instances`); const totalSize = instances.length * totalStride; lazyTrace(LOGGER4, () => `Creating data view to hold extracted instance data of size ${totalSize}`); const buffer = new ArrayBuffer(totalSize); const dataView = new DataView(buffer); instances.forEach((instance, index) => { lazyTrace(LOGGER4, () => `Extracting data from instance ${JSON.stringify(instance)}`); dataBridges.forEach((extractor) => extractor(index * totalStride, instance, dataView)); }); return buffer; } }; } }; var toMarshalledRef = (userFormat) => { if (isUserFormatScalar(userFormat)) { return ofScalar(userFormat.scalar); } else if (isUserFormatVec2(userFormat)) { return Array.isArray(userFormat.vec2) ? ofVecSplit(userFormat.vec2) : ofVecDirect(userFormat.vec2, 2); } else if (isUserFormatVec3(userFormat)) { return Array.isArray(userFormat.vec3) ? ofVecSplit(userFormat.vec3) : ofVecDirect(userFormat.vec3, 3); } else if (isUserFormatVec4(userFormat)) { return Array.isArray(userFormat.vec4) ? ofVecSplit(userFormat.vec4) : ofVecDirect(userFormat.vec4, 4); } else if (isUserFormatEntityIndex(userFormat)) { const { entityIndexFromResizeableEntityCache: { key, target } } = userFormat; return ofEntityIndex(key, target); } else { throw Error(`Cannot create format reference from ${JSON.stringify(userFormat)}`); } }; var ofScalar = (path) => { lazyDebug(LOGGER4, () => `Creating scalar ref from path '${path}'`); const refPath = toRefPath(path); return { datumCount: 1, valuesOf: (instance) => { const value = valueOfInstanceAtPath(instance, refPath); lazyTrace(LOGGER4, () => `Found value ${JSON.stringify(value)} at path '${path}'`); return value; } }; }; var ofVecDirect = (path, vecLength) => { lazyDebug(LOGGER4, () => `Creating vec direct ref from path '${path}'`); const refPath = toRefPath(path); return { datumCount: vecLength, valuesOf: (instance) => { const value = valueAtPath(instance, refPath, 0); if (Array.isArray(value)) { lazyTrace(LOGGER4, () => `Found array ${JSON.stringify(value)} at path '${path}'`); return value; } throw Error(`Value ${JSON.stringify(value)} at path ${refPath} is not an array`); } }; }; var ofVecSplit = (paths) => { lazyDebug(LOGGER4, () => `Creating vec split ref from path '${JSON.stringify(paths)}'`); const refPaths = paths.map(toRefPath); return { datumCount: paths.length, valuesOf: (instance) => { const values = refPaths.map((refPath) => valueOfInstanceAtPath(instance, refPath)); lazyTrace(LOGGER4, () => `Found array ${JSON.stringify(values)} at paths '${JSON.stringify(paths)}'`); return values; } }; }; var ofEntityIndex = (key, target) => { lazyDebug(LOGGER4, () => `Creating entity index ref from key '${key}'`); const refPath = toRefPath(key); return { datumCount: 1, valuesOf(instance) { const id = valueAtPath(instance, refPath, 0); if (typeof id !== "string") { throw Error(`Value found at path ${key} must be a string but was a ${typeof id}`); } else { const index = target.indexOf(id); if (index === -1) { lazyWarn(LOGGER4, () => `ID at path ${key} was '${id}' and could not be found, index falling back to -1`); } lazyTrace(LOGGER4, () => `Found entity index ${index} for id '${id}'`); return index; } } }; }; var toRefPath = (path) => { const parts = path.split("."); const refPath = parts.map((part) => stringFuncs.canBePositiveInt(part) ? Number(part) : part); lazyTrace(LOGGER4, () => `Converted path '${path}' to ref path ${JSON.stringify(refPath)}`); return refPath; }; var valueAtPath = (input, refPath, pathIndex) => { if (pathIndex > refPath.length) { throw Error(`Cannot use index ${pathIndex} larger than reference path. Path: ${refPath}. Input: ${JSON.stringify(input)}`); } if (pathIndex === refPath.length) { lazyTrace(LOGGER4, () => `Found value ${input} at path ${JSON.stringify(refPath)}`); return input; } const indexValue = refPath[pathIndex]; if (typeof input !== "object" || input === null) { throw Error(`Cannot index field ${input} with index ${indexValue}. Path: ${refPath}. Input: ${JSON.stringify(input)}`); } if (typeof indexValue === "string" || typeof indexValue === "number") { return valueAtPath(input[indexValue], refPath, pathIndex + 1); } throw Error(`Cannot index using non-integer or string field ${indexValue}. Path: ${refPath}. Input: ${JSON.stringify(input)}`); }; var valueOfInstanceAtPath = (instance, refPath) => { const value = valueAtPath(instance, refPath, 0); if (typeof value === "number") { return value; } throw Error(`Value ${JSON.stringify(value)} at path ${refPath} is not a number`); }; // src/buffer-resources.ts var LOGGER5 = getLogger("buffer"); var bufferResourcesFactory = { ofMesh: (name, mesh) => { LOGGER5.debug(`Creating mesh buffer ${name}`); const indices = bufferFactory.ofData(meshFuncs.toIndicesData(mesh), `${name}-indices`, GPUBufferUsage.INDEX); const vertices = bufferFactory.ofData(meshFuncs.toVerticesData(mesh), `${name}-vertices`, GPUBufferUsage.VERTEX); return { indices, vertices }; }, ofUniformAndInstances: (name, uniformCache, entityCache, bufferFormats, bufferUsages) => { const initialInstances = entityCache.calculateChanges().values; const uniformMutators = []; const instanceMutators = []; const buffers = {}; lazyDebug(LOGGER5, () => `Create buffer resources for ${name}`); for (const [key, bufferFormat] of Object.entries(bufferFormats)) { lazyTrace(LOGGER5, () => `Create buffer resources for ${name} key ${key}`); const { bufferType, contentType } = bufferFormat; const usage = bufferUsages[key]; const label = `${name}-buffer-${key}`; if (bufferType === "uniform") { lazyTrace(LOGGER5, () => `Create buffer resources for ${name} key ${key} of type uniform`); const extractor = dataExtractorFactory.of(bufferFormat.marshall); if (uniformCache.isMutable) { lazyTrace(LOGGER5, () => `Buffer resources ${name}:${key}:uniform is mutable`); const stride = strideFuncs.ofFormatMarshall(bufferFormat.marshall); const buffer = bufferFactory.ofMutable(stride, label, usage); const uniformMutator = { mutate(input) { const data = extractor.extract([input]); buffer.mutate(data, 0); } }; buffers[key] = buffer; uniformMutators.push(uniformMutator); } else { lazyTrace(LOGGER5, () => `Buffer resources ${name}:${key}:uniform is not mutable`); const data = extractor.extract([uniformCache.get()]); const buffer = bufferFactory.ofData(data, label, usage); buffers[key] = buffer; } } else if (bufferType ==