UNPKG

@motion-core/motion-gpu

Version:

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

1,578 lines 50.4 kB
import type {
	ComputeBufferReference,
	ComputeExternalResourceContext,
	ComputeExternalTextureReference,
	ComputeResourceDescriptor,
	ComputeResourceMap,
	ComputeResourceVersion,
	ComputeSamplerReference,
	ComputeTextureReference,
	ComputeTextureViewDescriptor,
	StorageBufferType
} from './types.js';
import { assertUniformName } from './uniforms.js';
import {
	attachMotionGPUErrorContext,
	createMotionGPUError,
	type MotionGPUErrorCode,
	type MotionGPUErrorContext
} from './error-report.js';
import type { RuntimeStorageBufferResource, RuntimeTextureResource } from './resource-registry.js';
import { STORAGE_TEXTURE_FORMATS } from './storage-buffers.js';

const RESERVED_COMPUTE_RESOURCE_ALIASES = new Set(['motiongpuFrame', 'motiongpuUniforms']);

const TEXTURE_DESCRIPTOR_KEYS = new Set(['texture', 'access', 'view', 'version', 'pingPong']);
const BUFFER_DESCRIPTOR_KEYS = new Set(['buffer', 'access', 'version']);
const SAMPLER_DESCRIPTOR_KEYS = new Set(['sampler']);
const EXTERNAL_TEXTURE_KEYS = new Set([
	'externalTexture',
	'resourceId',
	'format',
	'usage',
	'viewDimension'
]);
const EXTERNAL_TEXTURE_VIEW_KEYS = new Set([
	'externalView',
	'resourceId',
	'format',
	'usage',
	'viewDimension',
	'mipLevelCount'
]);
const EXTERNAL_BUFFER_KEYS = new Set(['externalBuffer', 'resourceId', 'wgslType', 'size', 'usage']);
const EXTERNAL_SAMPLER_KEYS = new Set(['externalSampler', 'resourceId', 'type']);
const TEXTURE_VIEW_KEYS = new Set([
	'baseMipLevel',
	'mipLevelCount',
	'baseArrayLayer',
	'arrayLayerCount'
]);

interface ComputeResourceIdentity {
	kind: 'material' | 'external';
	value: string | symbol;
}

export interface ComputePingPongResourcePair {
	readAlias: string;
	writeAlias: string;
	texture: ComputeTextureReference;
}

export interface ResolvedTextureSubresourceRange {
	baseMipLevel: number;
	mipLevelCount: number;
	baseArrayLayer: number;
	arrayLayerCount: 1;
}

export type ResolvedComputeResourceSource = 'material' | 'external';

interface ResolvedComputeResourceBase {
	alias: string;
	binding: number;
	logicalId: string | symbol;
	physicalId: object | string | symbol;
	source: ResolvedComputeResourceSource;
	layoutEntry: GPUBindGroupLayoutEntry;
	bindingResource: GPUBindingResource;
	topologyPart: string;
}

export interface ResolvedSampledTextureResource extends ResolvedComputeResourceBase {
	kind: 'sampled-texture';
	access: 'sampled';
	format: GPUTextureFormat;
	scalarType: 'f32' | 'u32' | 'i32';
	sampleType: GPUTextureSampleType;
	version: ComputeResourceVersion;
	pingPong: 'read' | undefined;
	subresource: ResolvedTextureSubresourceRange;
	bindingResource: GPUTextureView;
}

export interface ResolvedStorageTextureResource extends ResolvedComputeResourceBase {
	kind: 'storage-texture';
	access: 'storage-write';
	format: GPUTextureFormat;
	pingPong: 'write' | undefined;
	subresource: ResolvedTextureSubresourceRange;
	bindingResource: GPUTextureView;
}

export interface ResolvedStorageBufferResource extends ResolvedComputeResourceBase {
	kind: 'storage-buffer';
	access: 'storage-read' | 'storage-read-write';
	wgslType: StorageBufferType;
	size: number;
	version: ComputeResourceVersion | undefined;
	bindingResource: GPUBufferBinding;
}

export interface ResolvedSamplerResource extends ResolvedComputeResourceBase {
	kind: 'sampler';
	samplerType: GPUSamplerBindingType;
	bindingResource: GPUSampler;
}

export type ResolvedComputeResource =
	| ResolvedSampledTextureResource
	| ResolvedStorageTextureResource
	| ResolvedStorageBufferResource
	| ResolvedSamplerResource;

export interface ResolvedComputeAccess {
	alias: string;
	resourceKind: 'texture' | 'buffer';
	logicalId: string | symbol;
	physicalId: object | string | symbol;
	mode: 'read' | 'write';
	version: ComputeResourceVersion;
	subresource?: ResolvedTextureSubresourceRange;
}

export interface ResolvedComputePassResources {
	entries: readonly ResolvedComputeResource[];
	reads: readonly ResolvedComputeAccess[];
	writes: readonly ResolvedComputeAccess[];
	topologyKey: string;
	bindingCount: number;
}

export interface ComputeMaterialSamplerResource {
	logicalId: string;
	sampler: GPUSampler;
	type: GPUSamplerBindingType;
	sampleType: GPUTextureSampleType;
}

export interface ComputeResourceResolverLimits {
	maxBindingsPerBindGroup: number;
	maxSampledTexturesPerShaderStage: number;
	maxSamplersPerShaderStage: number;
	maxStorageTexturesPerShaderStage: number;
	maxStorageBuffersPerShaderStage: number;
	maxStorageBufferBindingSize: number;
}

export interface ComputeResourceResolverContext {
	passLabel: string;
	deviceFeatures: ReadonlySet<string>;
	limits: ComputeResourceResolverLimits;
	externalContext: ComputeExternalResourceContext;
	getMaterialTexture: (logicalId: string) => RuntimeTextureResource | undefined;
	getMaterialStorageBuffer: (logicalId: string) => RuntimeStorageBufferResource | undefined;
	getMaterialSampler: (logicalId: string) => ComputeMaterialSamplerResource | undefined;
	createTextureView?: (texture: GPUTexture, descriptor: GPUTextureViewDescriptor) => GPUTextureView;
	pingPong?: boolean;
	externalState?: ComputeExternalResolutionState;
	diagnosticContext?: MotionGPUErrorContext;
}

export interface ResolvedComputeTextureFormat {
	scalarType: 'f32' | 'u32' | 'i32';
	sampleType: GPUTextureSampleType;
}

const normalizedComputeResourceMaps = new WeakSet<object>();

function isObjectRecord(value: unknown): value is Record<string, unknown> {
	return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function assertNoUnknownKeys(
	label: string,
	value: Record<string, unknown>,
	allowedKeys: ReadonlySet<string>
): void {
	const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key));
	if (unknownKey !== undefined) {
		throw new Error(`${label} contains unsupported field "${unknownKey}".`);
	}
}

function assertResourceId(label: string, value: unknown): asserts value is string | symbol {
	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: string, value: unknown): void {
	if ((typeof value !== 'object' || value === null) && typeof value !== 'function') {
		throw new Error(`${label} must provide a WebGPU object or provider function.`);
	}
}

function assertUsage(label: string, value: unknown): void {
	if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
		throw new Error(`${label} usage must be a non-negative integer bitmask.`);
	}
}

function assertPositiveInteger(label: string, value: unknown): asserts value is number {
	if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
		throw new Error(`${label} must be a positive integer.`);
	}
}

function assertTextureViewDescriptor(
	alias: string,
	view: unknown
): asserts view is ComputeTextureViewDescriptor {
	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'] as const) {
		const value = view[key];
		if (value !== undefined && (!Number.isInteger(value) || (value as number) < 0)) {
			throw new Error(`Compute resource "${alias}" view.${key} must be a non-negative integer.`);
		}
	}
	if (view.mipLevelCount !== undefined) {
		assertPositiveInteger(`Compute resource "${alias}" view.mipLevelCount`, view.mipLevelCount);
	}
	if (view.arrayLayerCount !== undefined && view.arrayLayerCount !== 1) {
		throw new Error(`Compute resource "${alias}" view.arrayLayerCount must be 1.`);
	}
}

function assertExternalTextureReference(alias: string, reference: Record<string, unknown>): void {
	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 !== undefined && reference.viewDimension !== '2d') {
		throw new Error(`Compute resource "${alias}" viewDimension must be "2d".`);
	}
}

function assertExternalTextureViewReference(
	alias: string,
	reference: Record<string, unknown>
): void {
	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: string,
	reference: unknown
): asserts reference is ComputeTextureReference {
	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;
	const hasView = 'externalView' in reference;
	if (hasTexture === hasView) {
		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: string,
	reference: unknown
): asserts reference is ComputeBufferReference {
	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: string,
	reference: unknown
): asserts reference is ComputeSamplerReference {
	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: string,
	descriptor: unknown
): asserts descriptor is ComputeResourceDescriptor {
	if (!isObjectRecord(descriptor)) {
		throw new Error(`Compute resource "${alias}" descriptor must be an object.`);
	}

	const discriminants = ['texture', 'buffer', 'sampler'].filter((key) => key in descriptor);
	if (discriminants.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 !== undefined) {
			assertTextureViewDescriptor(alias, descriptor.view);
		}
		if (descriptor.access === 'sampled') {
			if (
				descriptor.version !== undefined &&
				descriptor.version !== 'current' &&
				descriptor.version !== 'initial'
			) {
				throw new Error(`Compute resource "${alias}" version must be "current" or "initial".`);
			}
			if (descriptor.pingPong !== undefined && 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 !== undefined && 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 !== undefined &&
				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: ComputeTextureReference): ComputeTextureReference {
	return typeof reference === 'string' ? reference : { ...reference };
}

function cloneBufferReference(reference: ComputeBufferReference): ComputeBufferReference {
	return typeof reference === 'string' ? reference : { ...reference };
}

function cloneSamplerReference(reference: ComputeSamplerReference): ComputeSamplerReference {
	return typeof reference === 'string' ? reference : { ...reference };
}

function cloneComputeResourceDescriptor(
	descriptor: ComputeResourceDescriptor
): ComputeResourceDescriptor {
	if ('texture' in descriptor) {
		return {
			...descriptor,
			texture: cloneTextureReference(descriptor.texture),
			...(descriptor.view !== undefined ? { view: { ...descriptor.view } } : {})
		};
	}
	if ('buffer' in descriptor) {
		return { ...descriptor, buffer: cloneBufferReference(descriptor.buffer) };
	}
	return { ...descriptor, sampler: cloneSamplerReference(descriptor.sampler) };
}

function freezeComputeResourceDescriptor(
	descriptor: ComputeResourceDescriptor
): ComputeResourceDescriptor {
	if ('texture' in descriptor) {
		if (typeof descriptor.texture !== 'string') {
			Object.freeze(descriptor.texture);
		}
		if (descriptor.view !== undefined) {
			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.
 */
export function normalizeComputeResourceMap(
	resources: ComputeResourceMap | undefined
): ComputeResourceMap {
	if (resources === undefined) {
		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: Record<string, ComputeResourceDescriptor> = Object.create(null) as Record<
		string,
		ComputeResourceDescriptor
	>;
	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.
 */
export function copyComputeResourceMap(resources: ComputeResourceMap): ComputeResourceMap {
	const copy: Record<string, ComputeResourceDescriptor> = Object.create(null) as Record<
		string,
		ComputeResourceDescriptor
	>;
	for (const alias of Object.keys(resources)) {
		const descriptor = resources[alias];
		if (descriptor !== undefined) {
			copy[alias] = cloneComputeResourceDescriptor(descriptor);
		}
	}
	return copy;
}

function textureResourceIdentity(reference: ComputeTextureReference): ComputeResourceIdentity {
	return typeof reference === 'string'
		? { kind: 'material', value: reference }
		: { kind: 'external', value: reference.resourceId };
}

function sameResourceIdentity(a: ComputeResourceIdentity, b: ComputeResourceIdentity): boolean {
	return a.kind === b.kind && Object.is(a.value, b.value);
}

/**
 * Validates and resolves the single read/write pair required by ping-pong compute.
 */
export function resolveComputePingPongResourcePair(
	resources: ComputeResourceMap
): ComputePingPongResourcePair {
	const reads: Array<[string, Extract<ComputeResourceDescriptor, { texture: unknown }>]> = [];
	const writes: Array<[string, Extract<ComputeResourceDescriptor, { texture: unknown }>]> = [];
	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 === undefined || write === undefined) {
		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(): GPUShaderStageFlags {
	return typeof GPUShaderStage === 'object' ? GPUShaderStage.COMPUTE : (4 as GPUShaderStageFlags);
}

function textureBindingUsage(): GPUTextureUsageFlags {
	return typeof GPUTextureUsage === 'object' ? GPUTextureUsage.TEXTURE_BINDING : 4;
}

function storageTextureBindingUsage(): GPUTextureUsageFlags {
	return typeof GPUTextureUsage === 'object' ? GPUTextureUsage.STORAGE_BINDING : 8;
}

function storageBufferBindingUsage(): GPUBufferUsageFlags {
	return typeof GPUBufferUsage === 'object' ? GPUBufferUsage.STORAGE : 128;
}

interface ResolvedTextureReference {
	logicalId: string | symbol;
	physicalId: object | string | symbol;
	source: ResolvedComputeResourceSource;
	format: GPUTextureFormat;
	usage: GPUTextureUsageFlags;
	mipLevelCount: number;
	baseView: GPUTextureView;
	texture: GPUTexture | null;
	isExternalView: boolean;
}

interface ResolvedBufferReference {
	logicalId: string | symbol;
	physicalId: object | string | symbol;
	source: ResolvedComputeResourceSource;
	buffer: GPUBuffer;
	size: number;
	wgslType: StorageBufferType;
	usage: GPUBufferUsageFlags;
	materialAccess: 'read' | 'read-write' | undefined;
}

interface ResolvedSamplerReference {
	logicalId: string | symbol;
	physicalId: object | string | symbol;
	source: ResolvedComputeResourceSource;
	sampler: GPUSampler;
	type: GPUSamplerBindingType;
	materialSampleType: GPUTextureSampleType | undefined;
}

export interface ComputeExternalResolutionState {
	providerResults: Map<object, object>;
	resourceIdByObject: Map<object, string | symbol>;
	metadataByResourceId: Map<string | symbol, string>;
	textureByResourceId: Map<string | symbol, object>;
	bufferByResourceId: Map<string | symbol, object>;
	samplerByResourceId: Map<string | symbol, object>;
}

export function createComputeExternalResolutionState(): ComputeExternalResolutionState {
	return {
		providerResults: new Map(),
		resourceIdByObject: new Map(),
		metadataByResourceId: new Map(),
		textureByResourceId: new Map(),
		bufferByResourceId: new Map(),
		samplerByResourceId: new Map()
	};
}

function resourceError(
	context: ComputeResourceResolverContext,
	alias: string,
	message: string,
	code: MotionGPUErrorCode = 'COMPUTE_RESOURCE_INCOMPATIBLE'
): Error {
	const error = createMotionGPUError(code, `${context.passLabel} resource "${alias}" ${message}`);
	return context.diagnosticContext
		? attachMotionGPUErrorContext(error, context.diagnosticContext)
		: error;
}

function externalResourceError(
	context: ComputeResourceResolverContext,
	alias: string,
	message: string
): Error {
	return resourceError(context, alias, message, 'COMPUTE_EXTERNAL_RESOURCE_INVALID');
}

function hasUsage(usage: number, required: number): boolean {
	return (usage & required) === required;
}

function identityLabel(identity: string | symbol): string {
	return typeof identity === 'symbol' ? (identity.description ?? identity.toString()) : identity;
}

function resolveProvider<T extends object>(
	provider: T | ((context: ComputeExternalResourceContext) => T),
	context: ComputeResourceResolverContext,
	alias: string,
	state: ComputeExternalResolutionState
): T {
	if (typeof provider !== 'function') {
		return provider;
	}
	const cached = state.providerResults.get(provider);
	if (cached) {
		return cached as T;
	}
	let result: T;
	try {
		result = provider(context.externalContext);
	} catch (error) {
		const detail = error instanceof Error ? `: ${error.message}` : '.';
		throw externalResourceError(context, alias, `external provider failed${detail}`);
	}
	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: object,
	resourceId: string | symbol,
	context: ComputeResourceResolverContext,
	alias: string,
	state: ComputeExternalResolutionState,
	kind?: 'texture' | 'buffer' | 'sampler'
): void {
	const existing = state.resourceIdByObject.get(object);
	if (existing !== undefined && !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 !== undefined && 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: string | symbol,
	metadata: string,
	context: ComputeResourceResolverContext,
	alias: string,
	state: ComputeExternalResolutionState
): void {
	const existing = state.metadataByResourceId.get(resourceId);
	if (existing !== undefined && 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.
 */
export function resolveComputeTextureFormat(
	format: GPUTextureFormat,
	deviceFeatures: ReadonlySet<string>
): ResolvedComputeTextureFormat {
	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: ComputeTextureViewDescriptor | undefined,
	availableMipLevelCount: number,
	access: 'sampled' | 'storage-write',
	context: ComputeResourceResolverContext,
	alias: string
): ResolvedTextureSubresourceRange {
	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: GPUTexture,
	range: ResolvedTextureSubresourceRange,
	context: ComputeResourceResolverContext,
	alias: string
): GPUTextureView {
	const descriptor: GPUTextureViewDescriptor = {
		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: ResolvedTextureSubresourceRange,
	mipLevelCount: number
): boolean {
	return (
		range.baseMipLevel === 0 &&
		range.mipLevelCount === mipLevelCount &&
		range.baseArrayLayer === 0 &&
		range.arrayLayerCount === 1
	);
}

function validateExternalTextureMetadata(
	texture: GPUTexture,
	reference: ComputeExternalTextureReference,
	context: ComputeResourceResolverContext,
	alias: string
): number {
	if (texture.format !== undefined && texture.format !== reference.format) {
		throw externalResourceError(
			context,
			alias,
			`declares format "${reference.format}" but the external texture reports "${texture.format}".`
		);
	}
	if (texture.usage !== undefined && texture.usage !== reference.usage) {
		throw externalResourceError(
			context,
			alias,
			`declares usage ${reference.usage} but the external texture reports ${texture.usage}.`
		);
	}
	if (texture.dimension !== undefined && texture.dimension !== '2d') {
		throw externalResourceError(context, alias, `external texture dimension must be "2d".`);
	}
	if (texture.sampleCount !== undefined && texture.sampleCount !== 1) {
		throw externalResourceError(
			context,
			alias,
			'multisampled external textures are not supported.'
		);
	}
	if (texture.depthOrArrayLayers !== undefined && texture.depthOrArrayLayers !== 1) {
		throw externalResourceError(
			context,
			alias,
			'array, cube, and 3D external textures are not supported.'
		);
	}
	return texture.mipLevelCount ?? 1;
}

function resolveTextureReferenceForAccess(
	reference: ComputeTextureReference,
	access: 'sampled' | 'storage-write',
	viewDescriptor: ComputeTextureViewDescriptor | undefined,
	context: ComputeResourceResolverContext,
	alias: string,
	state: ComputeExternalResolutionState
): { reference: ResolvedTextureReference; range: ResolvedTextureSubresourceRange } {
	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)) {
			const usageName = access === 'sampled' ? 'TEXTURE_BINDING' : 'STORAGE_BINDING';
			throw resourceError(
				context,
				alias,
				`references material texture "${reference}" without GPUTextureUsage.${usageName}.`
			);
		}
		const range = resolveTextureRange(
			viewDescriptor,
			resource.mipLevelCount,
			access,
			context,
			alias
		);
		let baseView: GPUTextureView;
		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: ComputeBufferReference,
	context: ComputeResourceResolverContext,
	alias: string,
	state: ComputeExternalResolutionState
): ResolvedBufferReference {
	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 !== undefined && buffer.size !== reference.size) {
		throw externalResourceError(
			context,
			alias,
			`declares size ${reference.size} but the external buffer reports ${buffer.size}.`
		);
	}
	if (buffer.usage !== undefined && 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: undefined
	};
}

function resolveSamplerReferenceForBinding(
	reference: ComputeSamplerReference,
	context: ComputeResourceResolverContext,
	alias: string,
	state: ComputeExternalResolutionState
): ResolvedSamplerReference {
	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: undefined
	};
}

function textureRangesOverlap(
	a: ResolvedTextureSubresourceRange,
	b: ResolvedTextureSubresourceRange
): boolean {
	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: ResolvedTextureSubresourceRange): string {
	return `mip ${range.baseMipLevel}..${range.baseMipLevel + range.mipLevelCount - 1}`;
}

function freezeResolvedEntry<T extends ResolvedComputeResource>(entry: T): T {
	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: readonly ResolvedComputeResource[],
	context: ComputeResourceResolverContext,
	pingPongPair: ComputePingPongResourcePair | null
): void {
	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;
				const isStructuralPingPongPair =
					pingPongPair !== null &&
					((left.alias === pingPongPair.readAlias && right.alias === pingPongPair.writeAlias) ||
						(left.alias === pingPongPair.writeAlias && right.alias === pingPongPair.readAlias));
				if (isStructuralPingPongPair) 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: readonly ResolvedComputeResource[],
	context: ComputeResourceResolverContext
): void {
	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: Array<[number, number, string]> = [
		[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: ResolvedSampledTextureResource | ResolvedStorageTextureResource,
	mode: 'read' | 'write',
	version: ComputeResourceVersion
): ResolvedComputeAccess {
	return {
		alias: entry.alias,
		resourceKind: 'texture',
		logicalId: entry.logicalId,
		physicalId: entry.physicalId,
		mode,
		version,
		subresource: entry.subresource
	};
}

function toBufferAccess(
	entry: ResolvedStorageBufferResource,
	mode: 'read' | 'write',
	version: ComputeResourceVersion
): ResolvedComputeAccess {
	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.
 */
export function resolveComputePassResources(
	resources: ComputeResourceMap,
	context: ComputeResourceResolverContext
): ResolvedComputePassResources {
	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 !== undefined) {
				throw resourceError(context, alias, 'declares a pingPong role on a normal ComputePass.');
			}
		}
	}

	const state = context.externalState ?? createComputeExternalResolutionState();
	const entries: ResolvedComputeResource[] = [];
	const reads: ResolvedComputeAccess[] = [];
	const writes: ResolvedComputeAccess[] = [];

	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: ResolvedComputeTextureFormat;
				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<ResolvedSampledTextureResource>({
					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<ResolvedStorageTextureResource>({
				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') : undefined;
			const entry = freezeResolvedEntry<ResolvedStorageBufferResource>({
				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 !== undefined) {
			if (resolved.materialSampleType !== 'float') {
				throw resourceError(
					context,
					alias,
					`filtering sampler is incompatible with ${resolved.materialSampleType} material texture sampling.`
				);
			}
		}
		const entry = freezeResolvedEntry<ResolvedSamplerResource>({
			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
	});
}