@motion-core/motion-gpu
Version:
Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.
232 lines (231 loc) • 7.49 kB
JavaScript
//#region src/lib/core/uniforms.ts
/**
* Valid WGSL identifier pattern used for uniform and texture keys.
*/
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
/**
* Rounds a value up to the nearest multiple of `alignment`.
*/
function roundUp(value, alignment) {
return Math.ceil(value / alignment) * alignment;
}
/**
* Returns WGSL std140-like alignment and size metadata for a uniform type.
*/
function getTypeLayout(type) {
switch (type) {
case "f32": return {
alignment: 4,
size: 4
};
case "vec2f": return {
alignment: 8,
size: 8
};
case "vec3f": return {
alignment: 16,
size: 12
};
case "vec4f": return {
alignment: 16,
size: 16
};
case "mat4x4f": return {
alignment: 16,
size: 64
};
default: throw new Error(`Unsupported uniform type: ${type}`);
}
}
/**
* Type guard for explicitly typed uniform objects.
*/
function isTypedUniformValue(value) {
return typeof value === "object" && value !== null && "type" in value && "value" in value;
}
/**
* Validates numeric tuple input with a fixed length.
*/
function isTuple(value, size) {
return Array.isArray(value) && value.length === size && value.every((entry) => typeof entry === "number" && Number.isFinite(entry));
}
/**
* Type guard for accepted 4x4 matrix uniform values.
*/
function isMat4Value(value) {
if (value instanceof Float32Array) return value.length === 16;
return Array.isArray(value) && value.length === 16 && value.every((entry) => typeof entry === "number" && Number.isFinite(entry));
}
/**
* Asserts that a name can be safely used as a WGSL identifier.
*
* @param name - Candidate uniform/texture name.
* @throws {Error} When the identifier is invalid.
*/
function assertUniformName(name) {
if (!IDENTIFIER_PATTERN.test(name)) throw new Error(`Invalid uniform name: ${name}`);
}
/**
* Infers the WGSL type tag from a runtime uniform value.
*
* @param value - Uniform input value.
* @returns Inferred uniform type.
* @throws {Error} When the value does not match any supported shape.
*/
function inferUniformType(value) {
if (isTypedUniformValue(value)) return value.type;
if (typeof value === "number") return "f32";
if (Array.isArray(value)) {
if (value.length === 2) return "vec2f";
if (value.length === 3) return "vec3f";
if (value.length === 4) return "vec4f";
}
throw new Error("Uniform value must resolve to f32, vec2f, vec3f, vec4f or mat4x4f");
}
/**
* Validates that a uniform value matches an explicit uniform type declaration.
*
* @param type - Declared WGSL type.
* @param value - Runtime value to validate.
* @throws {Error} When the value shape is incompatible with the declared type.
*/
function assertUniformValueForType(type, value) {
const input = isTypedUniformValue(value) ? value.value : value;
if (type === "f32") {
if (typeof input !== "number" || !Number.isFinite(input)) throw new Error("Uniform f32 value must be a finite number");
return;
}
if (type === "vec2f") {
if (!isTuple(input, 2)) throw new Error("Uniform vec2f value must be a tuple with 2 numbers");
return;
}
if (type === "vec3f") {
if (!isTuple(input, 3)) throw new Error("Uniform vec3f value must be a tuple with 3 numbers");
return;
}
if (type === "vec4f") {
if (!isTuple(input, 4)) throw new Error("Uniform vec4f value must be a tuple with 4 numbers");
return;
}
if (!isMat4Value(input)) throw new Error("Uniform mat4x4f value must contain 16 numbers");
}
/**
* Resolves a deterministic packed uniform buffer layout from a uniform map.
*
* @param uniforms - Input uniform definitions.
* @returns Sorted layout with byte offsets and final buffer byte length.
*/
function resolveUniformLayout(uniforms) {
const names = Object.keys(uniforms).sort();
let offset = 0;
const entries = [];
const byName = {};
for (const name of names) {
assertUniformName(name);
const type = inferUniformType(uniforms[name]);
const { alignment, size } = getTypeLayout(type);
offset = roundUp(offset, alignment);
const entry = {
name,
type,
offset,
size
};
entries.push(entry);
byName[name] = entry;
offset += size;
}
return {
entries,
byName,
byteLength: Math.max(16, roundUp(offset, 16))
};
}
/**
* Writes one uniform value into the output float buffer without re-validating.
*
* Callers are responsible for ensuring the value has already been validated
* against `type` (e.g. via {@link assertUniformValueForType}).
*
* Uses `Float32Array.set()` for mat4x4f values backed by a Float32Array,
* which is significantly faster than an element-by-element loop.
*/
function writeUniformValueFast(type, value, data, base) {
const input = isTypedUniformValue(value) ? value.value : value;
if (type === "f32") {
data[base] = input;
return;
}
if (type === "mat4x4f") {
const matrix = input;
if (matrix instanceof Float32Array) {
data.set(matrix, base);
return;
}
const arr = matrix;
for (let index = 0; index < 16; index += 1) data[base + index] = arr[index] ?? 0;
return;
}
const tuple = input;
const length = type === "vec2f" ? 2 : type === "vec3f" ? 3 : 4;
for (let index = 0; index < length; index += 1) data[base + index] = tuple[index] ?? 0;
}
/**
* Packs uniforms into a newly allocated `Float32Array`.
*
* @param uniforms - Uniform values to pack.
* @param layout - Target layout definition.
* @returns Packed float buffer sized to `layout.byteLength`.
*/
function packUniforms(uniforms, layout) {
const data = new Float32Array(layout.byteLength / 4);
packUniformsInto(uniforms, layout, data);
return data;
}
/**
* Packs uniforms into an existing output buffer and zeroes missing values.
*
* Values are validated against their declared types before being written.
* Uses an optimised fast-write path internally to avoid redundant type checks
* after validation has already been performed for each entry.
*
* @param uniforms - Uniform values to pack.
* @param layout - Target layout metadata.
* @param data - Destination float buffer.
* @throws {Error} When `data` size does not match the required layout size.
*/
function packUniformsInto(uniforms, layout, data) {
const requiredLength = layout.byteLength / 4;
if (data.length !== requiredLength) throw new Error(`Uniform output buffer size mismatch. Expected ${requiredLength}, got ${data.length}`);
data.fill(0);
for (const entry of layout.entries) {
const raw = uniforms[entry.name];
if (raw === void 0) continue;
assertUniformValueForType(entry.type, raw);
const base = entry.offset / 4;
writeUniformValueFast(entry.type, raw, data, base);
}
}
/**
* Packs uniforms into an existing output buffer without per-entry validation.
*
* Intended for the renderer render loop where all values have already been
* validated at {@link setUniform} call time, making per-write re-validation
* redundant. Skips the size guard and validation to minimise hot-path overhead.
*
* @internal
* @param uniforms - Pre-validated uniform values to pack.
* @param layout - Target layout metadata.
* @param data - Destination float buffer (must match `layout.byteLength / 4`).
*/
function packUniformsIntoFast(uniforms, layout, data) {
data.fill(0);
for (const entry of layout.entries) {
const raw = uniforms[entry.name];
if (raw === void 0) continue;
writeUniformValueFast(entry.type, raw, data, entry.offset / 4);
}
}
//#endregion
export { assertUniformName, assertUniformValueForType, inferUniformType, packUniforms, packUniformsInto, packUniformsIntoFast, resolveUniformLayout };
//# sourceMappingURL=uniforms.js.map