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