UNPKG

@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
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