@motion-core/motion-gpu
Version:
Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.
252 lines (251 loc) • 10.6 kB
JavaScript
import { assertUniformName } from "./uniforms.js";
import { assertTextureFormat, resolveTextureFormatCapabilities, textureSampleScalarType } from "./format-capabilities.js";
//#region src/lib/core/textures.ts
/**
* Default sampling filter for textures when no explicit value is provided.
*/
var DEFAULT_TEXTURE_FILTER = "linear";
/**
* Default addressing mode for textures when no explicit value is provided.
*/
var DEFAULT_TEXTURE_ADDRESS_MODE = "clamp-to-edge";
var TEXTURE_COLOR_SPACES = /* @__PURE__ */ new Set(["srgb", "linear"]);
var TEXTURE_UPDATE_MODES = /* @__PURE__ */ new Set([
"once",
"onInvalidate",
"perFrame"
]);
var TEXTURE_FILTER_MODES = /* @__PURE__ */ new Set(["nearest", "linear"]);
var TEXTURE_ADDRESS_MODES = /* @__PURE__ */ new Set([
"clamp-to-edge",
"repeat",
"mirror-repeat"
]);
/**
* Validates an optional boolean texture field without applying a default.
*/
function assertOptionalBoolean(name, value) {
if (value !== void 0 && typeof value !== "boolean") throw new Error(`${name} must be a boolean, got ${String(value)}.`);
}
/**
* Validates a required runtime enum value against its WebGPU allowlist.
*/
function assertEnumValue(name, value, allowed) {
if (typeof value !== "string" || !allowed.has(value)) throw new Error(`${name} must be one of ${Array.from(allowed).join(", ")}, got ${String(value)}.`);
}
/**
* Validates an optional runtime enum value when the caller supplied one.
*/
function assertOptionalEnumValue(name, value, allowed) {
if (value !== void 0) assertEnumValue(name, value, allowed);
}
/**
* Validates one concrete texture dimension before size calculations.
*/
function assertTextureDimension(name, value) {
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) throw new Error(`${name} must be a finite positive integer, got ${String(value)}.`);
}
/**
* Validates concrete 2D texture dimensions before mip calculation or GPU allocation.
*/
function assertTextureDimensions(width, height, label = "Texture") {
assertTextureDimension(`${label} width`, width);
assertTextureDimension(`${label} height`, height);
}
/**
* Validates dimensions against the active device's 2D texture limit.
*/
function assertTextureDimensionsWithinLimit(width, height, maxTextureDimension2D, label = "Texture") {
assertTextureDimensions(width, height, label);
assertTextureDimension("device.limits.maxTextureDimension2D", maxTextureDimension2D);
if (width > maxTextureDimension2D || height > maxTextureDimension2D) throw new Error(`${label} dimensions ${width}x${height} exceed device.limits.maxTextureDimension2D (${maxTextureDimension2D}).`);
}
/**
* Resolves binding sample types and coerces unsupported filters to nearest sampling.
*/
function resolveTextureSamplingLayout(input) {
const capabilities = resolveTextureFormatCapabilities(input.format, input.deviceFeatures);
if (capabilities.sampleType === "uint") return {
sampleType: "uint",
samplerType: "non-filtering",
effectiveFilter: "nearest",
filterWasCoerced: input.filter !== "nearest"
};
if (capabilities.sampleType === "sint") return {
sampleType: "sint",
samplerType: "non-filtering",
effectiveFilter: "nearest",
filterWasCoerced: input.filter !== "nearest"
};
if (capabilities.sampleType === "depth") return {
sampleType: "depth",
samplerType: "non-filtering",
effectiveFilter: "nearest",
filterWasCoerced: input.filter !== "nearest"
};
if (capabilities.sampleType === "unfilterable-float") return {
sampleType: "unfilterable-float",
samplerType: "non-filtering",
effectiveFilter: "nearest",
filterWasCoerced: input.filter !== "nearest"
};
return {
sampleType: "float",
samplerType: input.filter === "linear" ? "filtering" : "non-filtering",
effectiveFilter: input.filter,
filterWasCoerced: false
};
}
/**
* Validates and returns sorted texture keys.
*
* @param textures - Texture definition map.
* @returns Lexicographically sorted texture keys.
*/
function resolveTextureKeys(textures) {
const keys = Object.keys(textures).sort();
for (const key of keys) assertUniformName(key);
return keys;
}
/**
* Applies defaults and clamps to a single texture definition.
*
* @param definition - Optional texture definition.
* @returns Normalized definition with deterministic defaults.
*/
function normalizeTextureDefinition(definition) {
assertOptionalEnumValue("Texture colorSpace", definition?.colorSpace, TEXTURE_COLOR_SPACES);
assertOptionalEnumValue("Texture update", definition?.update, TEXTURE_UPDATE_MODES);
if (definition?.format !== void 0) assertTextureFormat(definition.format);
assertOptionalEnumValue("Texture filter", definition?.filter, TEXTURE_FILTER_MODES);
assertOptionalEnumValue("Texture addressModeU", definition?.addressModeU, TEXTURE_ADDRESS_MODES);
assertOptionalEnumValue("Texture addressModeV", definition?.addressModeV, TEXTURE_ADDRESS_MODES);
assertOptionalBoolean("Texture flipY", definition?.flipY);
assertOptionalBoolean("Texture generateMipmaps", definition?.generateMipmaps);
assertOptionalBoolean("Texture premultipliedAlpha", definition?.premultipliedAlpha);
assertOptionalBoolean("Texture storage", definition?.storage);
assertOptionalBoolean("Texture fragmentVisible", definition?.fragmentVisible);
if (definition?.width !== void 0) assertTextureDimension("Texture width", definition.width);
if (definition?.height !== void 0) assertTextureDimension("Texture height", definition.height);
const anisotropy = definition?.anisotropy ?? 1;
if (typeof anisotropy !== "number" || !Number.isFinite(anisotropy)) throw new Error(`Texture anisotropy must be a finite number, got ${String(anisotropy)}.`);
const isStorage = definition?.storage === true;
const defaultFormat = definition?.colorSpace === "linear" ? "rgba8unorm" : "rgba8unorm-srgb";
const format = definition?.format ?? defaultFormat;
const sampleScalar = textureSampleScalarType(format);
const explicitFragmentVisible = definition?.fragmentVisible;
if (explicitFragmentVisible === true && sampleScalar !== "f32") throw new Error(`Texture with storage format "${format}" cannot be fragmentVisible: fragment shader uses texture_2d<f32>, which is incompatible with ${sampleScalar} sample type. Set fragmentVisible: false or use a float-sampled storage format.`);
const fragmentVisible = explicitFragmentVisible ?? sampleScalar === "f32";
const normalized = {
source: definition?.source ?? null,
colorSpace: definition?.colorSpace ?? "srgb",
format,
flipY: definition?.flipY ?? true,
generateMipmaps: definition?.generateMipmaps ?? false,
premultipliedAlpha: definition?.premultipliedAlpha ?? false,
anisotropy: Math.max(1, Math.min(16, Math.floor(anisotropy))),
filter: definition?.filter ?? DEFAULT_TEXTURE_FILTER,
addressModeU: definition?.addressModeU ?? DEFAULT_TEXTURE_ADDRESS_MODE,
addressModeV: definition?.addressModeV ?? DEFAULT_TEXTURE_ADDRESS_MODE,
storage: isStorage,
fragmentVisible
};
if (definition?.width !== void 0) normalized.width = definition.width;
if (definition?.height !== void 0) normalized.height = definition.height;
if (definition?.update !== void 0) normalized.update = definition.update;
return normalized;
}
/**
* Normalizes all texture definitions for already-resolved texture keys.
*
* @param textures - Source texture definitions.
* @param textureKeys - Texture keys to normalize.
* @returns Normalized map keyed by `textureKeys`.
*/
function normalizeTextureDefinitions(textures, textureKeys) {
const out = {};
for (const key of textureKeys) out[key] = normalizeTextureDefinition(textures[key]);
return out;
}
/**
* Checks whether a texture value is a structured `{ source, width?, height? }` object.
*/
function isTextureData(value) {
return typeof value === "object" && value !== null && "source" in value;
}
/**
* Converts supported texture input variants to normalized `TextureData`.
*
* @param value - Texture value input.
* @returns Structured texture data or `null`.
*/
function toTextureData(value) {
if (value === null) return null;
if (isTextureData(value)) return value;
return { source: value };
}
/**
* Resolves effective runtime texture update strategy.
*/
function resolveTextureUpdateMode(input) {
if (input.override !== void 0) {
assertEnumValue("Texture update override", input.override, TEXTURE_UPDATE_MODES);
return input.override;
}
if (input.defaultMode !== void 0) {
assertEnumValue("Texture default update mode", input.defaultMode, TEXTURE_UPDATE_MODES);
return input.defaultMode;
}
if (isVideoTextureSource(input.source)) return "perFrame";
return "once";
}
/**
* Resolves texture dimensions from explicit values or source metadata.
*
* @param data - Texture payload.
* @returns Positive integer width/height.
* @throws {Error} When dimensions cannot be resolved to positive values.
*/
function resolveTextureSize(data) {
assertOptionalEnumValue("Texture colorSpace", data.colorSpace, TEXTURE_COLOR_SPACES);
assertOptionalEnumValue("Texture update", data.update, TEXTURE_UPDATE_MODES);
assertOptionalBoolean("Texture flipY", data.flipY);
assertOptionalBoolean("Texture generateMipmaps", data.generateMipmaps);
assertOptionalBoolean("Texture premultipliedAlpha", data.premultipliedAlpha);
const source = data.source;
const width = data.width ?? source.naturalWidth ?? source.videoWidth ?? source.width ?? 0;
const height = data.height ?? source.naturalHeight ?? source.videoHeight ?? source.height ?? 0;
assertTextureDimensions(width, height, "Texture source");
return {
width,
height
};
}
/**
* Computes the number of mipmap levels for a base texture size.
*
* @param width - Base width.
* @param height - Base height.
* @returns Total mip level count (minimum `1`).
*/
function getTextureMipLevelCount(width, height) {
assertTextureDimensions(width, height);
let levels = 1;
let currentWidth = Math.max(1, width);
let currentHeight = Math.max(1, height);
while (currentWidth > 1 || currentHeight > 1) {
currentWidth = Math.max(1, Math.floor(currentWidth / 2));
currentHeight = Math.max(1, Math.floor(currentHeight / 2));
levels += 1;
}
return levels;
}
/**
* Checks whether the source is an `HTMLVideoElement`.
*/
function isVideoTextureSource(source) {
return typeof HTMLVideoElement !== "undefined" && source instanceof HTMLVideoElement;
}
//#endregion
export { assertTextureDimensions, assertTextureDimensionsWithinLimit, assertTextureFormat, getTextureMipLevelCount, isTextureData, isVideoTextureSource, normalizeTextureDefinition, normalizeTextureDefinitions, resolveTextureKeys, resolveTextureSamplingLayout, resolveTextureSize, resolveTextureUpdateMode, toTextureData };
//# sourceMappingURL=textures.js.map