UNPKG

@motion-core/motion-gpu

Version:

Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.

797 lines (796 loc) 39.2 kB
import { assertUniformName } from "./uniforms.js"; import { STORAGE_TEXTURE_FORMATS } from "./storage-buffers.js"; import { attachMotionGPUErrorContext, createMotionGPUError } from "./error-report.js"; //#region src/lib/core/compute-resources.ts var RESERVED_COMPUTE_RESOURCE_ALIASES = /* @__PURE__ */ new Set(["motiongpuFrame", "motiongpuUniforms"]); var TEXTURE_DESCRIPTOR_KEYS = /* @__PURE__ */ new Set([ "texture", "access", "view", "version", "pingPong" ]); var BUFFER_DESCRIPTOR_KEYS = /* @__PURE__ */ new Set([ "buffer", "access", "version" ]); var SAMPLER_DESCRIPTOR_KEYS = /* @__PURE__ */ new Set(["sampler"]); var EXTERNAL_TEXTURE_KEYS = /* @__PURE__ */ new Set([ "externalTexture", "resourceId", "format", "usage", "viewDimension" ]); var EXTERNAL_TEXTURE_VIEW_KEYS = /* @__PURE__ */ new Set([ "externalView", "resourceId", "format", "usage", "viewDimension", "mipLevelCount" ]); var EXTERNAL_BUFFER_KEYS = /* @__PURE__ */ new Set([ "externalBuffer", "resourceId", "wgslType", "size", "usage" ]); var EXTERNAL_SAMPLER_KEYS = /* @__PURE__ */ new Set([ "externalSampler", "resourceId", "type" ]); var TEXTURE_VIEW_KEYS = /* @__PURE__ */ new Set([ "baseMipLevel", "mipLevelCount", "baseArrayLayer", "arrayLayerCount" ]); var normalizedComputeResourceMaps = /* @__PURE__ */ new WeakSet(); function isObjectRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function assertNoUnknownKeys(label, value, allowedKeys) { const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key)); if (unknownKey !== void 0) throw new Error(`${label} contains unsupported field "${unknownKey}".`); } function assertResourceId(label, value) { if ((typeof value !== "string" || value.length === 0) && typeof value !== "symbol") throw new Error(`${label} resourceId must be a non-empty string or symbol.`); } function assertExternalProvider(label, value) { if ((typeof value !== "object" || value === null) && typeof value !== "function") throw new Error(`${label} must provide a WebGPU object or provider function.`); } function assertUsage(label, value) { if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`${label} usage must be a non-negative integer bitmask.`); } function assertPositiveInteger(label, value) { if (typeof value !== "number" || !Number.isInteger(value) || value < 1) throw new Error(`${label} must be a positive integer.`); } function assertTextureViewDescriptor(alias, view) { if (!isObjectRecord(view)) throw new Error(`Compute resource "${alias}" view must be an object.`); assertNoUnknownKeys(`Compute resource "${alias}" view`, view, TEXTURE_VIEW_KEYS); for (const key of ["baseMipLevel", "baseArrayLayer"]) { const value = view[key]; if (value !== void 0 && (!Number.isInteger(value) || value < 0)) throw new Error(`Compute resource "${alias}" view.${key} must be a non-negative integer.`); } if (view.mipLevelCount !== void 0) assertPositiveInteger(`Compute resource "${alias}" view.mipLevelCount`, view.mipLevelCount); if (view.arrayLayerCount !== void 0 && view.arrayLayerCount !== 1) throw new Error(`Compute resource "${alias}" view.arrayLayerCount must be 1.`); } function assertExternalTextureReference(alias, reference) { assertNoUnknownKeys(`Compute resource "${alias}" external texture`, reference, EXTERNAL_TEXTURE_KEYS); assertExternalProvider(`Compute resource "${alias}" externalTexture`, reference.externalTexture); assertResourceId(`Compute resource "${alias}"`, reference.resourceId); if (typeof reference.format !== "string" || reference.format.length === 0) throw new Error(`Compute resource "${alias}" format must be a non-empty string.`); assertUsage(`Compute resource "${alias}"`, reference.usage); if (reference.viewDimension !== void 0 && reference.viewDimension !== "2d") throw new Error(`Compute resource "${alias}" viewDimension must be "2d".`); } function assertExternalTextureViewReference(alias, reference) { assertNoUnknownKeys(`Compute resource "${alias}" external texture view`, reference, EXTERNAL_TEXTURE_VIEW_KEYS); assertExternalProvider(`Compute resource "${alias}" externalView`, reference.externalView); assertResourceId(`Compute resource "${alias}"`, reference.resourceId); if (typeof reference.format !== "string" || reference.format.length === 0) throw new Error(`Compute resource "${alias}" format must be a non-empty string.`); assertUsage(`Compute resource "${alias}"`, reference.usage); if (reference.viewDimension !== "2d") throw new Error(`Compute resource "${alias}" viewDimension must be "2d".`); assertPositiveInteger(`Compute resource "${alias}" mipLevelCount`, reference.mipLevelCount); } function assertTextureReference(alias, reference) { if (typeof reference === "string") { if (reference.length === 0) throw new Error(`Compute resource "${alias}" texture key must not be empty.`); return; } if (!isObjectRecord(reference)) throw new Error(`Compute resource "${alias}" texture must be a material key or external reference.`); const hasTexture = "externalTexture" in reference; if (hasTexture === "externalView" in reference) throw new Error(`Compute resource "${alias}" texture reference must provide exactly one of externalTexture or externalView.`); if (hasTexture) { assertExternalTextureReference(alias, reference); return; } assertExternalTextureViewReference(alias, reference); } function assertBufferReference(alias, reference) { if (typeof reference === "string") { if (reference.length === 0) throw new Error(`Compute resource "${alias}" buffer key must not be empty.`); return; } if (!isObjectRecord(reference)) throw new Error(`Compute resource "${alias}" buffer must be a material key or external reference.`); assertNoUnknownKeys(`Compute resource "${alias}" external buffer`, reference, EXTERNAL_BUFFER_KEYS); assertExternalProvider(`Compute resource "${alias}" externalBuffer`, reference.externalBuffer); assertResourceId(`Compute resource "${alias}"`, reference.resourceId); if (typeof reference.wgslType !== "string" || reference.wgslType.length === 0) throw new Error(`Compute resource "${alias}" wgslType must be a non-empty string.`); assertPositiveInteger(`Compute resource "${alias}" size`, reference.size); assertUsage(`Compute resource "${alias}"`, reference.usage); } function assertSamplerReference(alias, reference) { if (typeof reference === "string") { if (reference.length === 0) throw new Error(`Compute resource "${alias}" sampler key must not be empty.`); return; } if (!isObjectRecord(reference)) throw new Error(`Compute resource "${alias}" sampler must be a material key or external reference.`); assertNoUnknownKeys(`Compute resource "${alias}" external sampler`, reference, EXTERNAL_SAMPLER_KEYS); assertExternalProvider(`Compute resource "${alias}" externalSampler`, reference.externalSampler); assertResourceId(`Compute resource "${alias}"`, reference.resourceId); if (reference.type !== "filtering" && reference.type !== "non-filtering" && reference.type !== "comparison") throw new Error(`Compute resource "${alias}" sampler type is invalid.`); } function assertComputeResourceDescriptor(alias, descriptor) { if (!isObjectRecord(descriptor)) throw new Error(`Compute resource "${alias}" descriptor must be an object.`); if ([ "texture", "buffer", "sampler" ].filter((key) => key in descriptor).length !== 1) throw new Error(`Compute resource "${alias}" must provide exactly one of texture, buffer, or sampler.`); if ("texture" in descriptor) { assertNoUnknownKeys(`Compute resource "${alias}"`, descriptor, TEXTURE_DESCRIPTOR_KEYS); assertTextureReference(alias, descriptor.texture); if (descriptor.access !== "sampled" && descriptor.access !== "storage-write") throw new Error(`Compute resource "${alias}" texture access must be "sampled" or "storage-write".`); if (descriptor.view !== void 0) assertTextureViewDescriptor(alias, descriptor.view); if (descriptor.access === "sampled") { if (descriptor.version !== void 0 && descriptor.version !== "current" && descriptor.version !== "initial") throw new Error(`Compute resource "${alias}" version must be "current" or "initial".`); if (descriptor.pingPong !== void 0 && descriptor.pingPong !== "read") throw new Error(`Compute resource "${alias}" sampled pingPong role must be "read".`); return; } if ("version" in descriptor) throw new Error(`Compute resource "${alias}" storage-write descriptor cannot set version.`); if (descriptor.pingPong !== void 0 && descriptor.pingPong !== "write") throw new Error(`Compute resource "${alias}" storage-write pingPong role must be "write".`); return; } if ("buffer" in descriptor) { assertNoUnknownKeys(`Compute resource "${alias}"`, descriptor, BUFFER_DESCRIPTOR_KEYS); assertBufferReference(alias, descriptor.buffer); if (descriptor.access !== "storage-read" && descriptor.access !== "storage-read-write") throw new Error(`Compute resource "${alias}" buffer access must be "storage-read" or "storage-read-write".`); if (descriptor.access === "storage-read") { if (descriptor.version !== void 0 && descriptor.version !== "current" && descriptor.version !== "initial") throw new Error(`Compute resource "${alias}" version must be "current" or "initial".`); return; } if ("version" in descriptor) throw new Error(`Compute resource "${alias}" storage-read-write descriptor cannot set version.`); return; } assertNoUnknownKeys(`Compute resource "${alias}"`, descriptor, SAMPLER_DESCRIPTOR_KEYS); assertSamplerReference(alias, descriptor.sampler); } function cloneTextureReference(reference) { return typeof reference === "string" ? reference : { ...reference }; } function cloneBufferReference(reference) { return typeof reference === "string" ? reference : { ...reference }; } function cloneSamplerReference(reference) { return typeof reference === "string" ? reference : { ...reference }; } function cloneComputeResourceDescriptor(descriptor) { if ("texture" in descriptor) return { ...descriptor, texture: cloneTextureReference(descriptor.texture), ...descriptor.view !== void 0 ? { view: { ...descriptor.view } } : {} }; if ("buffer" in descriptor) return { ...descriptor, buffer: cloneBufferReference(descriptor.buffer) }; return { ...descriptor, sampler: cloneSamplerReference(descriptor.sampler) }; } function freezeComputeResourceDescriptor(descriptor) { if ("texture" in descriptor) { if (typeof descriptor.texture !== "string") Object.freeze(descriptor.texture); if (descriptor.view !== void 0) Object.freeze(descriptor.view); } else if ("buffer" in descriptor) { if (typeof descriptor.buffer !== "string") Object.freeze(descriptor.buffer); } else if (typeof descriptor.sampler !== "string") Object.freeze(descriptor.sampler); return Object.freeze(descriptor); } /** * Validates, clones and freezes a deterministic compute resource topology. */ function normalizeComputeResourceMap(resources) { if (resources === void 0) { const normalized = Object.freeze({}); normalizedComputeResourceMaps.add(normalized); return normalized; } if (!isObjectRecord(resources)) throw createMotionGPUError("COMPUTE_RESOURCE_DESCRIPTOR_INVALID", "Compute resources must be an object map keyed by WGSL aliases."); if (normalizedComputeResourceMaps.has(resources)) return resources; const normalized = Object.create(null); for (const alias of Object.keys(resources).sort()) { try { assertUniformName(alias); } catch (error) { throw createMotionGPUError("COMPUTE_RESOURCE_ALIAS_COLLISION", error instanceof Error ? error.message : `Invalid compute resource alias "${alias}".`, { cause: error }); } if (RESERVED_COMPUTE_RESOURCE_ALIASES.has(alias)) throw createMotionGPUError("COMPUTE_RESOURCE_ALIAS_COLLISION", `Compute resource alias "${alias}" is reserved by MotionGPU.`); const descriptor = resources[alias]; try { assertComputeResourceDescriptor(alias, descriptor); } catch (error) { throw createMotionGPUError("COMPUTE_RESOURCE_DESCRIPTOR_INVALID", error instanceof Error ? error.message : `Compute resource "${alias}" descriptor is invalid.`, { cause: error }); } normalized[alias] = freezeComputeResourceDescriptor(cloneComputeResourceDescriptor(descriptor)); } const frozen = Object.freeze(normalized); normalizedComputeResourceMaps.add(frozen); return frozen; } /** * Returns a deep-enough defensive copy while preserving external GPU handles/providers. */ function copyComputeResourceMap(resources) { const copy = Object.create(null); for (const alias of Object.keys(resources)) { const descriptor = resources[alias]; if (descriptor !== void 0) copy[alias] = cloneComputeResourceDescriptor(descriptor); } return copy; } function textureResourceIdentity(reference) { return typeof reference === "string" ? { kind: "material", value: reference } : { kind: "external", value: reference.resourceId }; } function sameResourceIdentity(a, b) { return a.kind === b.kind && Object.is(a.value, b.value); } /** * Validates and resolves the single read/write pair required by ping-pong compute. */ function resolveComputePingPongResourcePair(resources) { const reads = []; const writes = []; for (const [alias, descriptor] of Object.entries(resources)) { if (!("texture" in descriptor)) continue; if (descriptor.pingPong === "read") reads.push([alias, descriptor]); else if (descriptor.pingPong === "write") writes.push([alias, descriptor]); } if (reads.length !== 1 || writes.length !== 1) throw new Error("PingPongComputePass resources must contain exactly one pingPong read texture and one pingPong write texture."); const read = reads[0]; const write = writes[0]; if (read === void 0 || write === void 0) throw new Error("PingPongComputePass resource pair resolution failed."); const [readAlias, readDescriptor] = read; const [writeAlias, writeDescriptor] = write; if (readDescriptor.access !== "sampled" || writeDescriptor.access !== "storage-write") throw new Error("PingPongComputePass read resource must be sampled and write resource must be storage-write."); if (!sameResourceIdentity(textureResourceIdentity(readDescriptor.texture), textureResourceIdentity(writeDescriptor.texture))) throw new Error("PingPongComputePass read and write resources must reference the same texture."); return { readAlias, writeAlias, texture: readDescriptor.texture }; } function computeShaderVisibility() { return typeof GPUShaderStage === "object" ? GPUShaderStage.COMPUTE : 4; } function textureBindingUsage() { return typeof GPUTextureUsage === "object" ? GPUTextureUsage.TEXTURE_BINDING : 4; } function storageTextureBindingUsage() { return typeof GPUTextureUsage === "object" ? GPUTextureUsage.STORAGE_BINDING : 8; } function storageBufferBindingUsage() { return typeof GPUBufferUsage === "object" ? GPUBufferUsage.STORAGE : 128; } function createComputeExternalResolutionState() { return { providerResults: /* @__PURE__ */ new Map(), resourceIdByObject: /* @__PURE__ */ new Map(), metadataByResourceId: /* @__PURE__ */ new Map(), textureByResourceId: /* @__PURE__ */ new Map(), bufferByResourceId: /* @__PURE__ */ new Map(), samplerByResourceId: /* @__PURE__ */ new Map() }; } function resourceError(context, alias, message, code = "COMPUTE_RESOURCE_INCOMPATIBLE") { const error = createMotionGPUError(code, `${context.passLabel} resource "${alias}" ${message}`); return context.diagnosticContext ? attachMotionGPUErrorContext(error, context.diagnosticContext) : error; } function externalResourceError(context, alias, message) { return resourceError(context, alias, message, "COMPUTE_EXTERNAL_RESOURCE_INVALID"); } function hasUsage(usage, required) { return (usage & required) === required; } function identityLabel(identity) { return typeof identity === "symbol" ? identity.description ?? identity.toString() : identity; } function resolveProvider(provider, context, alias, state) { if (typeof provider !== "function") return provider; const cached = state.providerResults.get(provider); if (cached) return cached; let result; try { result = provider(context.externalContext); } catch (error) { throw externalResourceError(context, alias, `external provider failed${error instanceof Error ? `: ${error.message}` : "."}`); } if (typeof result !== "object" || result === null) throw externalResourceError(context, alias, "external provider returned an invalid WebGPU object."); state.providerResults.set(provider, result); return result; } function registerExternalIdentity(object, resourceId, context, alias, state, kind) { const existing = state.resourceIdByObject.get(object); if (existing !== void 0 && !Object.is(existing, resourceId)) throw externalResourceError(context, alias, `uses external object identity "${identityLabel(resourceId)}" but the same object was already declared as "${identityLabel(existing)}".`); state.resourceIdByObject.set(object, resourceId); if (kind) { const objects = kind === "texture" ? state.textureByResourceId : kind === "buffer" ? state.bufferByResourceId : state.samplerByResourceId; const existingObject = objects.get(resourceId); if (existingObject !== void 0 && existingObject !== object) throw externalResourceError(context, alias, `external ${kind} identity "${identityLabel(resourceId)}" resolved to multiple physical objects in one frame.`); objects.set(resourceId, object); } } function registerExternalMetadata(resourceId, metadata, context, alias, state) { const existing = state.metadataByResourceId.get(resourceId); if (existing !== void 0 && existing !== metadata) throw externalResourceError(context, alias, `declares metadata "${metadata}" for external identity "${identityLabel(resourceId)}", previously declared as "${existing}".`); state.metadataByResourceId.set(resourceId, metadata); } /** * Maps a supported 2D color texture format to its compute sampled contract. */ function resolveComputeTextureFormat(format, deviceFeatures) { const normalized = String(format).toLowerCase(); if (normalized.includes("depth") || normalized.includes("stencil")) throw new Error(`Texture format "${format}" is not a supported 2D color format.`); if (normalized.endsWith("uint")) return { scalarType: "u32", sampleType: "uint" }; if (normalized.endsWith("sint")) return { scalarType: "i32", sampleType: "sint" }; if ((normalized === "r32float" || normalized === "rg32float" || normalized === "rgba32float") && !deviceFeatures.has("float32-filterable")) return { scalarType: "f32", sampleType: "unfilterable-float" }; return { scalarType: "f32", sampleType: "float" }; } function resolveTextureRange(descriptor, availableMipLevelCount, access, context, alias) { const baseMipLevel = descriptor?.baseMipLevel ?? 0; const defaultMipLevelCount = access === "storage-write" ? 1 : availableMipLevelCount - baseMipLevel; const mipLevelCount = descriptor?.mipLevelCount ?? defaultMipLevelCount; const baseArrayLayer = descriptor?.baseArrayLayer ?? 0; const arrayLayerCount = descriptor?.arrayLayerCount ?? 1; if (baseMipLevel < 0 || mipLevelCount < 1 || baseMipLevel + mipLevelCount > availableMipLevelCount) throw resourceError(context, alias, `selects mip range ${baseMipLevel}..${baseMipLevel + mipLevelCount - 1}, outside ${availableMipLevelCount} available mip level(s).`); if (access === "storage-write" && mipLevelCount !== 1) throw resourceError(context, alias, "storage-write view must expose exactly one mip level."); if (baseArrayLayer !== 0 || arrayLayerCount !== 1) throw resourceError(context, alias, "only one 2D array layer at baseArrayLayer 0 is supported."); return { baseMipLevel, mipLevelCount, baseArrayLayer, arrayLayerCount: 1 }; } function createTextureView(texture, range, context, alias) { const descriptor = { dimension: "2d", baseMipLevel: range.baseMipLevel, mipLevelCount: range.mipLevelCount, baseArrayLayer: range.baseArrayLayer, arrayLayerCount: range.arrayLayerCount }; if (context.createTextureView) return context.createTextureView(texture, descriptor); if (typeof texture.createView !== "function") throw resourceError(context, alias, "cannot create the requested texture view."); return texture.createView(descriptor); } function isFullTextureRange(range, mipLevelCount) { return range.baseMipLevel === 0 && range.mipLevelCount === mipLevelCount && range.baseArrayLayer === 0 && range.arrayLayerCount === 1; } function validateExternalTextureMetadata(texture, reference, context, alias) { if (texture.format !== void 0 && texture.format !== reference.format) throw externalResourceError(context, alias, `declares format "${reference.format}" but the external texture reports "${texture.format}".`); if (texture.usage !== void 0 && texture.usage !== reference.usage) throw externalResourceError(context, alias, `declares usage ${reference.usage} but the external texture reports ${texture.usage}.`); if (texture.dimension !== void 0 && texture.dimension !== "2d") throw externalResourceError(context, alias, `external texture dimension must be "2d".`); if (texture.sampleCount !== void 0 && texture.sampleCount !== 1) throw externalResourceError(context, alias, "multisampled external textures are not supported."); if (texture.depthOrArrayLayers !== void 0 && texture.depthOrArrayLayers !== 1) throw externalResourceError(context, alias, "array, cube, and 3D external textures are not supported."); return texture.mipLevelCount ?? 1; } function resolveTextureReferenceForAccess(reference, access, viewDescriptor, context, alias, state) { if (typeof reference === "string") { const resource = context.getMaterialTexture(reference); if (!resource) throw resourceError(context, alias, `references unknown material texture "${reference}".`, "COMPUTE_RESOURCE_UNKNOWN"); const requiredUsage = access === "sampled" ? textureBindingUsage() : storageTextureBindingUsage(); if (!hasUsage(resource.usage, requiredUsage)) throw resourceError(context, alias, `references material texture "${reference}" without GPUTextureUsage.${access === "sampled" ? "TEXTURE_BINDING" : "STORAGE_BINDING"}.`); const range = resolveTextureRange(viewDescriptor, resource.mipLevelCount, access, context, alias); let baseView; if (access === "sampled" && isFullTextureRange(range, resource.mipLevelCount)) baseView = resource.publishedView; else if (access === "storage-write" && range.baseMipLevel === 0 && resource.storageView) baseView = resource.storageView; else if (resource.ownedTexture) baseView = createTextureView(resource.ownedTexture, range, context, alias); else throw resourceError(context, alias, `cannot create the requested view for material texture "${reference}" without an allocated texture.`); return { reference: { logicalId: resource.logicalId, physicalId: resource.ownedTexture ?? resource.publishedView, source: "material", format: resource.format, usage: resource.usage, mipLevelCount: resource.mipLevelCount, baseView, texture: resource.ownedTexture, isExternalView: false }, range }; } if ("externalTexture" in reference) { const texture = resolveProvider(reference.externalTexture, context, alias, state); registerExternalIdentity(texture, reference.resourceId, context, alias, state, "texture"); registerExternalMetadata(reference.resourceId, `texture:${reference.format}:${reference.usage}`, context, alias, state); const mipLevelCount = validateExternalTextureMetadata(texture, reference, context, alias); const requiredUsage = access === "sampled" ? textureBindingUsage() : storageTextureBindingUsage(); if (!hasUsage(reference.usage, requiredUsage)) { const usageName = access === "sampled" ? "TEXTURE_BINDING" : "STORAGE_BINDING"; throw externalResourceError(context, alias, `external texture "${identityLabel(reference.resourceId)}" lacks GPUTextureUsage.${usageName}.`); } const range = resolveTextureRange(viewDescriptor, mipLevelCount, access, context, alias); return { reference: { logicalId: reference.resourceId, physicalId: reference.resourceId, source: "external", format: reference.format, usage: reference.usage, mipLevelCount, baseView: createTextureView(texture, range, context, alias), texture, isExternalView: false }, range }; } const view = resolveProvider(reference.externalView, context, alias, state); registerExternalIdentity(view, reference.resourceId, context, alias, state); registerExternalMetadata(reference.resourceId, `texture:${reference.format}:${reference.usage}`, context, alias, state); const requiredUsage = access === "sampled" ? textureBindingUsage() : storageTextureBindingUsage(); if (!hasUsage(reference.usage, requiredUsage)) { const usageName = access === "sampled" ? "TEXTURE_BINDING" : "STORAGE_BINDING"; throw externalResourceError(context, alias, `external texture view "${identityLabel(reference.resourceId)}" lacks GPUTextureUsage.${usageName}.`); } const range = resolveTextureRange(viewDescriptor, reference.mipLevelCount, access, context, alias); if (!isFullTextureRange(range, reference.mipLevelCount)) throw externalResourceError(context, alias, "externalView already represents a fixed view and cannot be narrowed by a view descriptor."); return { reference: { logicalId: reference.resourceId, physicalId: reference.resourceId, source: "external", format: reference.format, usage: reference.usage, mipLevelCount: reference.mipLevelCount, baseView: view, texture: null, isExternalView: true }, range }; } function resolveBufferReferenceForAccess(reference, context, alias, state) { if (typeof reference === "string") { const resource = context.getMaterialStorageBuffer(reference); if (!resource) throw resourceError(context, alias, `references unknown material storage buffer "${reference}".`, "COMPUTE_RESOURCE_UNKNOWN"); return { logicalId: resource.logicalId, physicalId: resource.buffer, source: "material", buffer: resource.buffer, size: resource.size, wgslType: resource.wgslType, usage: resource.usage, materialAccess: resource.access }; } const buffer = resolveProvider(reference.externalBuffer, context, alias, state); registerExternalIdentity(buffer, reference.resourceId, context, alias, state, "buffer"); registerExternalMetadata(reference.resourceId, `buffer:${reference.wgslType}:${reference.size}:${reference.usage}`, context, alias, state); if (buffer.size !== void 0 && buffer.size !== reference.size) throw externalResourceError(context, alias, `declares size ${reference.size} but the external buffer reports ${buffer.size}.`); if (buffer.usage !== void 0 && buffer.usage !== reference.usage) throw externalResourceError(context, alias, `declares usage ${reference.usage} but the external buffer reports ${buffer.usage}.`); return { logicalId: reference.resourceId, physicalId: reference.resourceId, source: "external", buffer, size: reference.size, wgslType: reference.wgslType, usage: reference.usage, materialAccess: void 0 }; } function resolveSamplerReferenceForBinding(reference, context, alias, state) { if (typeof reference === "string") { const resource = context.getMaterialSampler(reference); if (!resource) throw resourceError(context, alias, `references unknown material sampler "${reference}".`, "COMPUTE_RESOURCE_UNKNOWN"); return { logicalId: resource.logicalId, physicalId: resource.sampler, source: "material", sampler: resource.sampler, type: resource.type, materialSampleType: resource.sampleType }; } const sampler = resolveProvider(reference.externalSampler, context, alias, state); registerExternalIdentity(sampler, reference.resourceId, context, alias, state, "sampler"); registerExternalMetadata(reference.resourceId, `sampler:${reference.type}`, context, alias, state); return { logicalId: reference.resourceId, physicalId: reference.resourceId, source: "external", sampler, type: reference.type, materialSampleType: void 0 }; } function textureRangesOverlap(a, b) { const mipsOverlap = a.baseMipLevel < b.baseMipLevel + b.mipLevelCount && b.baseMipLevel < a.baseMipLevel + a.mipLevelCount; const layersOverlap = a.baseArrayLayer < b.baseArrayLayer + b.arrayLayerCount && b.baseArrayLayer < a.baseArrayLayer + a.arrayLayerCount; return mipsOverlap && layersOverlap; } function formatRange(range) { return `mip ${range.baseMipLevel}..${range.baseMipLevel + range.mipLevelCount - 1}`; } function freezeResolvedEntry(entry) { if ("subresource" in entry) Object.freeze(entry.subresource); if (entry.layoutEntry.texture) Object.freeze(entry.layoutEntry.texture); if (entry.layoutEntry.storageTexture) Object.freeze(entry.layoutEntry.storageTexture); if (entry.layoutEntry.buffer) Object.freeze(entry.layoutEntry.buffer); if (entry.layoutEntry.sampler) Object.freeze(entry.layoutEntry.sampler); Object.freeze(entry.layoutEntry); return Object.freeze(entry); } function validateResolvedResourceHazards(entries, context, pingPongPair) { for (let leftIndex = 0; leftIndex < entries.length; leftIndex += 1) { const left = entries[leftIndex]; if (!left) continue; for (let rightIndex = leftIndex + 1; rightIndex < entries.length; rightIndex += 1) { const right = entries[rightIndex]; if (!right || !Object.is(left.physicalId, right.physicalId)) continue; if ((left.kind === "sampled-texture" || left.kind === "storage-texture") && (right.kind === "sampled-texture" || right.kind === "storage-texture")) { if (!textureRangesOverlap(left.subresource, right.subresource)) continue; if (pingPongPair !== null && (left.alias === pingPongPair.readAlias && right.alias === pingPongPair.writeAlias || left.alias === pingPongPair.writeAlias && right.alias === pingPongPair.readAlias)) continue; if (left.kind === "sampled-texture" && right.kind === "sampled-texture") continue; throw resourceError(context, right.alias, `overlaps ${context.passLabel} resource "${left.alias}" on ${formatRange(right.subresource)} while at least one alias is writable.`, "COMPUTE_RESOURCE_HAZARD"); } if (left.kind === "storage-buffer" && right.kind === "storage-buffer") { if (left.access === "storage-read" && right.access === "storage-read") continue; throw resourceError(context, right.alias, `aliases the same storage buffer as ${context.passLabel} resource "${left.alias}" with an incompatible writable access.`, "COMPUTE_RESOURCE_HAZARD"); } } } } function validateResolvedResourceLimits(entries, context) { const { limits } = context; const counts = { bindings: entries.length, sampledTextures: entries.filter((entry) => entry.kind === "sampled-texture").length, samplers: entries.filter((entry) => entry.kind === "sampler").length, storageTextures: entries.filter((entry) => entry.kind === "storage-texture").length, storageBuffers: entries.filter((entry) => entry.kind === "storage-buffer").length }; const checks = [ [ counts.bindings, limits.maxBindingsPerBindGroup, "maxBindingsPerBindGroup" ], [ counts.sampledTextures, limits.maxSampledTexturesPerShaderStage, "maxSampledTexturesPerShaderStage" ], [ counts.samplers, limits.maxSamplersPerShaderStage, "maxSamplersPerShaderStage" ], [ counts.storageTextures, limits.maxStorageTexturesPerShaderStage, "maxStorageTexturesPerShaderStage" ], [ counts.storageBuffers, limits.maxStorageBuffersPerShaderStage, "maxStorageBuffersPerShaderStage" ] ]; for (const [required, limit, name] of checks) if (required > limit) { const error = createMotionGPUError("COMPUTE_RESOURCE_LIMIT_EXCEEDED", `${context.passLabel} requires ${required} ${name}, device limit is ${limit}.`); throw context.diagnosticContext ? attachMotionGPUErrorContext(error, context.diagnosticContext) : error; } for (const entry of entries) if (entry.kind === "storage-buffer" && entry.size > limits.maxStorageBufferBindingSize) throw resourceError(context, entry.alias, `requires ${entry.size} bytes, exceeding maxStorageBufferBindingSize ${limits.maxStorageBufferBindingSize}.`, "COMPUTE_RESOURCE_LIMIT_EXCEEDED"); } function toTextureAccess(entry, mode, version) { return { alias: entry.alias, resourceKind: "texture", logicalId: entry.logicalId, physicalId: entry.physicalId, mode, version, subresource: entry.subresource }; } function toBufferAccess(entry, mode, version) { return { alias: entry.alias, resourceKind: "buffer", logicalId: entry.logicalId, physicalId: entry.physicalId, mode, version }; } /** * Resolves one immutable public resource map into the single model consumed by * shader generation, layouts, bind groups, graph planning, and dispatch. */ function resolveComputePassResources(resources, context) { const normalized = normalizeComputeResourceMap(resources); const pingPongPair = context.pingPong ? resolveComputePingPongResourcePair(normalized) : null; if (!context.pingPong) { for (const [alias, descriptor] of Object.entries(normalized)) if ("texture" in descriptor && descriptor.pingPong !== void 0) throw resourceError(context, alias, "declares a pingPong role on a normal ComputePass."); } const state = context.externalState ?? createComputeExternalResolutionState(); const entries = []; const reads = []; const writes = []; for (const [alias, descriptor] of Object.entries(normalized)) { const binding = entries.length; if ("texture" in descriptor) { const resolved = resolveTextureReferenceForAccess(descriptor.texture, descriptor.access, descriptor.view, context, alias, state); if (descriptor.access === "sampled") { let format; try { format = resolveComputeTextureFormat(resolved.reference.format, context.deviceFeatures); } catch (error) { throw resourceError(context, alias, error instanceof Error ? error.message : "has an unsupported sampled format."); } const version = descriptor.version ?? "current"; const entry = freezeResolvedEntry({ kind: "sampled-texture", alias, binding, logicalId: resolved.reference.logicalId, physicalId: resolved.reference.physicalId, source: resolved.reference.source, access: "sampled", format: resolved.reference.format, scalarType: format.scalarType, sampleType: format.sampleType, version, pingPong: descriptor.pingPong, subresource: resolved.range, layoutEntry: { binding, visibility: computeShaderVisibility(), texture: { sampleType: format.sampleType, viewDimension: "2d", multisampled: false } }, bindingResource: resolved.reference.baseView, topologyPart: `${alias}:${binding}:sampled:${format.scalarType}:${format.sampleType}:${resolved.range.baseMipLevel}:${resolved.range.mipLevelCount}` }); entries.push(entry); reads.push(toTextureAccess(entry, "read", version)); continue; } if (!STORAGE_TEXTURE_FORMATS.has(resolved.reference.format)) throw resourceError(context, alias, `uses format "${resolved.reference.format}" which is not storage-write compatible.`); const entry = freezeResolvedEntry({ kind: "storage-texture", alias, binding, logicalId: resolved.reference.logicalId, physicalId: resolved.reference.physicalId, source: resolved.reference.source, access: "storage-write", format: resolved.reference.format, pingPong: descriptor.pingPong, subresource: resolved.range, layoutEntry: { binding, visibility: computeShaderVisibility(), storageTexture: { access: "write-only", format: resolved.reference.format, viewDimension: "2d" } }, bindingResource: resolved.reference.baseView, topologyPart: `${alias}:${binding}:storage-write:${resolved.reference.format}:${resolved.range.baseMipLevel}` }); entries.push(entry); writes.push(toTextureAccess(entry, "write", "current")); continue; } if ("buffer" in descriptor) { const resolved = resolveBufferReferenceForAccess(descriptor.buffer, context, alias, state); if (!hasUsage(resolved.usage, storageBufferBindingUsage())) throw resourceError(context, alias, `storage buffer "${identityLabel(resolved.logicalId)}" lacks GPUBufferUsage.STORAGE.`); if (descriptor.access === "storage-read-write" && resolved.materialAccess === "read") throw resourceError(context, alias, `cannot write material storage buffer "${identityLabel(resolved.logicalId)}" declared with access "read".`); const version = descriptor.access === "storage-read" ? descriptor.version ?? "current" : void 0; const entry = freezeResolvedEntry({ kind: "storage-buffer", alias, binding, logicalId: resolved.logicalId, physicalId: resolved.physicalId, source: resolved.source, access: descriptor.access, wgslType: resolved.wgslType, size: resolved.size, version, layoutEntry: { binding, visibility: computeShaderVisibility(), buffer: { type: descriptor.access === "storage-read" ? "read-only-storage" : "storage" } }, bindingResource: Object.freeze({ buffer: resolved.buffer, size: resolved.size }), topologyPart: `${alias}:${binding}:${descriptor.access}:${resolved.wgslType}` }); entries.push(entry); reads.push(toBufferAccess(entry, "read", version ?? "initial")); if (descriptor.access === "storage-read-write") writes.push(toBufferAccess(entry, "write", "current")); continue; } const resolved = resolveSamplerReferenceForBinding(descriptor.sampler, context, alias, state); if (resolved.type === "comparison") throw resourceError(context, alias, "comparison samplers are outside the color texture contract."); if (resolved.type === "filtering" && resolved.materialSampleType !== void 0) { if (resolved.materialSampleType !== "float") throw resourceError(context, alias, `filtering sampler is incompatible with ${resolved.materialSampleType} material texture sampling.`); } const entry = freezeResolvedEntry({ kind: "sampler", alias, binding, logicalId: resolved.logicalId, physicalId: resolved.physicalId, source: resolved.source, samplerType: resolved.type, layoutEntry: { binding, visibility: computeShaderVisibility(), sampler: { type: resolved.type } }, bindingResource: resolved.sampler, topologyPart: `${alias}:${binding}:sampler:${resolved.type}` }); entries.push(entry); } validateResolvedResourceHazards(entries, context, pingPongPair); validateResolvedResourceLimits(entries, context); return Object.freeze({ entries: Object.freeze(entries), reads: Object.freeze(reads.map((access) => Object.freeze(access))), writes: Object.freeze(writes.map((access) => Object.freeze(access))), topologyKey: entries.map((entry) => entry.topologyPart).join("|"), bindingCount: entries.length }); } //#endregion export { copyComputeResourceMap, createComputeExternalResolutionState, normalizeComputeResourceMap, resolveComputePassResources, resolveComputePingPongResourcePair, resolveComputeTextureFormat }; //# sourceMappingURL=compute-resources.js.map