UNPKG

@motion-core/motion-gpu

Version:

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

2,378 lines 94.4 kB
import { attachShaderCompilationDiagnostics } from "./error-diagnostics.js";
import { packUniformsIntoFast } from "./uniforms.js";
import { buildPingPongShaderSourceWithMap, buildShaderSourceWithMap, formatShaderSourceLocation } from "./shader.js";
import { attachMotionGPUErrorContext, createMotionGPUError } from "./error-report.js";
import { assertFloatRenderableFormat, assertFloatSampledFormat, assertStorageTextureAccess, assertTextureFormat, assertTextureFormatSupported } from "./format-capabilities.js";
import { assertTextureDimensionsWithinLimit, getTextureMipLevelCount, normalizeTextureDefinitions, resolveTextureSamplingLayout, resolveTextureSize, resolveTextureUpdateMode, toTextureData } from "./textures.js";
import { normalizeStorageBufferDefinition } from "./storage-buffers.js";
import { buildRenderTargetSignature, resolveRenderTargetDefinitions } from "./render-targets.js";
import { isManagedComputePass, isManagedFeedbackPass } from "./pass-contract.js";
import { planRenderGraph } from "./render-graph.js";
import { buildComputeShaderSourceWithMap } from "./compute-shader.js";
import { createComputeBindGroupCache } from "./compute-bindgroup-cache.js";
import { createComputeExternalResolutionState, resolveComputePassResources } from "./compute-resources.js";
import { ComputeSampledFallbackTexturePool, toComputeSampledFallbackClass } from "./compute-fallback-textures.js";
import { MaterialResourceRegistry } from "./resource-registry.js";
import { resolvePresentationSourceSlot, validateBuiltInRenderPassFormats, validatePresentationSourceFormat, validateRenderTargetFormats, validateWorkingFormat } from "./render-format-validation.js";
import { buildCanvasConfiguration, buildPresentationShader, resolveColorPipeline, shouldConvertLinearToSrgb } from "./color-pipeline.js";
//#region src/lib/core/renderer.ts
/**
* Binding index for frame uniforms (`time`, `delta`, `resolution`).
*/
var FRAME_BINDING = 0;
/**
* Binding index for material uniform buffer.
*/
var UNIFORM_BINDING = 1;
/**
* First binding index used for texture sampler/texture pairs.
*/
var FIRST_TEXTURE_BINDING = 2;
var DEFAULT_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION = 65535;
var COMPUTE_DISPATCH_AXES = [
	"x",
	"y",
	"z"
];
var DEFAULT_MAX_TEXTURE_DIMENSION_2D = 8192;
/**
* Formats an invalid compute dispatch value for deterministic diagnostics.
*/
function formatComputeDispatchValue(value) {
	if (value === void 0) return "undefined";
	if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
	if (typeof value === "string") return `"${value}"`;
	try {
		return JSON.stringify(value) ?? String(value);
	} catch {
		return String(value);
	}
}
/**
* Reads the compute workgroup limit with a fallback for partial or mocked devices.
*/
function getMaxComputeWorkgroupsPerDimension(device) {
	const max = device.limits?.maxComputeWorkgroupsPerDimension;
	if (typeof max === "number" && Number.isFinite(max) && max > 0) return Math.floor(max);
	return DEFAULT_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION;
}
/**
* Reads the device 2D texture limit with a fallback for partial or mocked devices.
*/
function getMaxTextureDimension2D(device) {
	const max = device.limits?.maxTextureDimension2D;
	if (typeof max === "number" && Number.isFinite(max) && max > 0) return Math.floor(max);
	return DEFAULT_MAX_TEXTURE_DIMENSION_2D;
}
/**
* Checks a planned texture size against the active device before GPU allocation.
*/
function assertTextureAllocationSize(device, width, height, label) {
	assertTextureDimensionsWithinLimit(width, height, getMaxTextureDimension2D(device), label);
}
/**
* Reads a positive integer device limit or returns the supplied compatibility fallback.
*/
function getPositiveDeviceLimit(device, name, fallback) {
	const value = device.limits?.[name];
	return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
}
/**
* Captures the device limits used while validating compute resource bindings.
*/
function getComputeResourceResolverLimits(device) {
	return {
		maxBindingsPerBindGroup: getPositiveDeviceLimit(device, "maxBindingsPerBindGroup", 1e3),
		maxSampledTexturesPerShaderStage: getPositiveDeviceLimit(device, "maxSampledTexturesPerShaderStage", 16),
		maxSamplersPerShaderStage: getPositiveDeviceLimit(device, "maxSamplersPerShaderStage", 16),
		maxStorageTexturesPerShaderStage: getPositiveDeviceLimit(device, "maxStorageTexturesPerShaderStage", 4),
		maxStorageBuffersPerShaderStage: getPositiveDeviceLimit(device, "maxStorageBuffersPerShaderStage", 8),
		maxStorageBufferBindingSize: getPositiveDeviceLimit(device, "maxStorageBufferBindingSize", 134217728)
	};
}
/**
* Resolves and validates a three-axis dispatch tuple against the active device limit.
*/
function validateComputeDispatch(dispatch, maxWorkgroupsPerDimension, label) {
	if (!Array.isArray(dispatch)) throw new Error(`${label} dispatch must resolve to an array [x, y, z], got ${formatComputeDispatchValue(dispatch)}.`);
	const resolved = [
		dispatch[0],
		dispatch[1] ?? 1,
		dispatch[2] ?? 1
	];
	const output = [
		1,
		1,
		1
	];
	for (let index = 0; index < COMPUTE_DISPATCH_AXES.length; index += 1) {
		const axis = COMPUTE_DISPATCH_AXES[index];
		const value = resolved[index];
		if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) throw new Error(`${label} dispatch ${axis} must be a positive integer, got ${formatComputeDispatchValue(value)}.`);
		if (value > maxWorkgroupsPerDimension) throw new Error(`${label} dispatch ${axis} must be <= device.limits.maxComputeWorkgroupsPerDimension (${maxWorkgroupsPerDimension}), got ${value}.`);
		output[index] = value;
	}
	return output;
}
/**
* Returns sampler/texture binding slots for a texture index.
*/
function getTextureBindings(index) {
	const samplerBinding = FIRST_TEXTURE_BINDING + index * 2;
	return {
		samplerBinding,
		textureBinding: samplerBinding + 1
	};
}
/**
* Resizes canvas backing store to match client size and DPR.
*/
function resizeCanvas(canvas, dprInput, cssSize) {
	const dpr = Number.isFinite(dprInput) && dprInput > 0 ? dprInput : 1;
	const rect = cssSize ? null : canvas.getBoundingClientRect();
	const cssWidth = Math.max(0, cssSize?.width ?? rect?.width ?? 0);
	const cssHeight = Math.max(0, cssSize?.height ?? rect?.height ?? 0);
	const width = Math.max(1, Math.floor((cssWidth || 1) * dpr));
	const height = Math.max(1, Math.floor((cssHeight || 1) * dpr));
	if (canvas.width !== width || canvas.height !== height) {
		canvas.width = width;
		canvas.height = height;
	}
	return {
		width,
		height
	};
}
/**
* Throws when a shader module contains WGSL compilation errors.
*/
async function assertCompilation(module, options) {
	const errors = (await module.getCompilationInfo()).messages.filter((message) => message.type === "error");
	if (errors.length === 0) return;
	const diagnostics = errors.map((message) => ({
		generatedLine: message.lineNum,
		message: message.message,
		linePos: message.linePos,
		lineLength: message.length,
		sourceLocation: options?.lineMap?.[message.lineNum] ?? null
	}));
	const summary = diagnostics.map((diagnostic) => {
		const contextLabel = [formatShaderSourceLocation(diagnostic.sourceLocation), diagnostic.generatedLine > 0 ? `generated WGSL line ${diagnostic.generatedLine}` : null].filter((value) => Boolean(value));
		if (contextLabel.length === 0) return diagnostic.message;
		return `[${contextLabel.join(" | ")}] ${diagnostic.message}`;
	}).join("\n");
	const prefix = options?.errorPrefix ?? "WGSL compilation failed";
	const error = /* @__PURE__ */ new Error(`${prefix}:\n${summary}`);
	throw attachShaderCompilationDiagnostics(error, {
		kind: "shader-compilation",
		...options?.shaderStage !== void 0 ? { shaderStage: options.shaderStage } : {},
		diagnostics,
		fragmentSource: options?.fragmentSource ?? "",
		...options?.computeSource !== void 0 ? { computeSource: options.computeSource } : {},
		includeSources: options?.includeSources ?? {},
		...options?.defineBlockSource !== void 0 ? { defineBlockSource: options.defineBlockSource } : {},
		materialSource: options?.materialSource ?? null,
		...options?.runtimeContext !== void 0 ? { runtimeContext: options.runtimeContext } : {}
	});
}
function toSortedUniqueStrings(values) {
	return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
}
/**
* Best-effort line extraction from a raw GPU error/exception message.
*
* Used only as a fallback when WebGPU's structured `getCompilationInfo()` and
* `popErrorScope()` channels have no per-message line metadata — primarily to
* keep test mocks that throw synchronously from `createComputePipeline()`
* reproducible against the structured-diagnostics contract.
*/
function extractGeneratedLineFromComputeError(message) {
	const lineMatch = message.match(/\bline\s+(\d+)\b/i);
	if (lineMatch) {
		const parsed = Number.parseInt(lineMatch[1] ?? "", 10);
		if (Number.isFinite(parsed) && parsed > 0) return parsed;
	}
	const colonMatch = message.match(/:(\d+):\d+/);
	if (colonMatch) {
		const parsed = Number.parseInt(colonMatch[1] ?? "", 10);
		if (Number.isFinite(parsed) && parsed > 0) return parsed;
	}
	return null;
}
/**
* Builds a compute compilation Error with structured diagnostics attached.
*/
function buildComputeCompilationError(input) {
	const summary = input.diagnostics.map((diagnostic) => {
		const contextLabel = [formatShaderSourceLocation(diagnostic.sourceLocation), diagnostic.generatedLine > 0 ? `generated WGSL line ${diagnostic.generatedLine}` : null].filter((value) => Boolean(value));
		if (contextLabel.length === 0) return diagnostic.message;
		return `[${contextLabel.join(" | ")}] ${diagnostic.message}`;
	}).join("\n");
	const error = /* @__PURE__ */ new Error(`Compute shader compilation failed:\n${summary}`);
	return attachShaderCompilationDiagnostics(error, {
		kind: "shader-compilation",
		shaderStage: "compute",
		diagnostics: input.diagnostics,
		fragmentSource: "",
		computeSource: input.computeSource,
		includeSources: {},
		materialSource: null,
		runtimeContext: input.runtimeContext
	});
}
/**
* Fallback compute-compilation error builder used when the synchronous
* `createShaderModule` / `createComputePipeline` path itself throws — there is
* no compilation info or popped scope to inspect, so we extract whatever line
* hint we can from the raw exception message.
*/
function toComputeCompilationError(input) {
	const baseError = input.error instanceof Error ? input.error : new Error(String(input.error ?? "Unknown error"));
	const generatedLine = extractGeneratedLineFromComputeError(baseError.message) ?? 0;
	const sourceLocation = generatedLine > 0 ? input.lineMap[generatedLine] ?? null : null;
	return buildComputeCompilationError({
		diagnostics: [{
			generatedLine,
			message: baseError.message,
			sourceLocation
		}],
		computeSource: input.computeSource,
		runtimeContext: input.runtimeContext
	});
}
/**
* Awaits the async outputs of a compute shader module + pipeline creation
* sequence (compilation info + popped validation scope) and, if either reveals
* an error, returns a fully-attributed compute compilation Error. Returns
* `null` when both channels are clean.
*/
async function assertComputeCompilationAsync(input) {
	let compilationMessages = [];
	try {
		compilationMessages = (await input.module.getCompilationInfo()).messages.filter((message) => message.type === "error");
	} catch {}
	const validationError = await input.validationScope.catch(() => null);
	if (compilationMessages.length === 0 && !validationError) return null;
	return buildComputeCompilationError({
		diagnostics: compilationMessages.length > 0 ? compilationMessages.map((message) => ({
			generatedLine: message.lineNum,
			message: message.message,
			linePos: message.linePos,
			lineLength: message.length,
			sourceLocation: input.lineMap[message.lineNum] ?? null
		})) : [{
			generatedLine: 0,
			message: validationError.message,
			sourceLocation: null
		}],
		computeSource: input.computeSource,
		runtimeContext: input.runtimeContext
	});
}
/**
* Summarizes enabled pass inputs and outputs for shader compilation diagnostics.
*/
function buildPassGraphSnapshot(passes) {
	const declaredPasses = passes ?? [];
	let enabledPassCount = 0;
	const inputs = [];
	const outputs = [];
	for (const pass of declaredPasses) {
		if (pass.enabled === false) continue;
		enabledPassCount += 1;
		if (isManagedComputePass(pass)) continue;
		if (isManagedFeedbackPass(pass)) continue;
		const rp = pass;
		const needsSwap = rp.needsSwap ?? true;
		const input = rp.input ?? "source";
		const output = rp.output ?? (needsSwap ? "target" : "source");
		inputs.push(input);
		outputs.push(output);
	}
	return {
		passCount: declaredPasses.length,
		enabledPassCount,
		inputs: toSortedUniqueStrings(inputs),
		outputs: toSortedUniqueStrings(outputs)
	};
}
/**
* Captures render targets and pass topology at shader compilation time.
*/
function buildShaderCompilationRuntimeContext(options) {
	const passList = options.getPasses?.() ?? options.passes;
	const renderTargetMap = options.getRenderTargets?.() ?? options.renderTargets;
	return {
		...options.materialSignature ? { materialSignature: options.materialSignature } : {},
		passGraph: buildPassGraphSnapshot(passList),
		activeRenderTargets: Object.keys(renderTargetMap ?? {}).sort((a, b) => a.localeCompare(b))
	};
}
/**
* Creates typed descriptor for `copyExternalImageToTexture`.
*/
function createExternalCopySource(source, options) {
	return {
		source,
		...options.flipY ? { flipY: true } : {},
		...options.premultipliedAlpha ? { premultipliedAlpha: true } : {}
	};
}
/**
* Uploads source content to the base GPU texture level.
*/
function uploadTextureBaseLevel(device, texture, uploadOptions, source, width, height) {
	device.queue.copyExternalImageToTexture(createExternalCopySource(source, {
		flipY: uploadOptions.flipY,
		premultipliedAlpha: uploadOptions.premultipliedAlpha
	}), {
		texture,
		mipLevel: 0
	}, {
		width,
		height,
		depthOrArrayLayers: 1
	});
}
var GPU_MIPMAP_SHADER = `
struct VertexOutput {
	@builtin(position) position: vec4f,
	@location(0) uv: vec2f
};

@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
	var positions = array<vec2f, 3>(
		vec2f(-1.0, -3.0),
		vec2f(-1.0, 1.0),
		vec2f(3.0, 1.0)
	);
	let position = positions[vertexIndex];
	var out: VertexOutput;
	out.position = vec4f(position, 0.0, 1.0);
	out.uv = position * vec2f(0.5, -0.5) + vec2f(0.5, 0.5);
	return out;
}

@group(0) @binding(0) var mipSampler: sampler;
@group(0) @binding(1) var mipSource: texture_2d<f32>;

@fragment
fn fragmentMain(in: VertexOutput) -> @location(0) vec4f {
	return textureSample(mipSource, mipSampler, in.uv);
}
`;
function createGpuMipmapGenerator(device) {
	let sampler = null;
	let shaderModule = null;
	let bindGroupLayout = null;
	let pipelineLayout = null;
	const pipelineByFormat = /* @__PURE__ */ new Map();
	const ensureBindGroupLayout = () => {
		if (!bindGroupLayout) bindGroupLayout = device.createBindGroupLayout({ entries: [{
			binding: 0,
			visibility: GPUShaderStage.FRAGMENT,
			sampler: { type: "filtering" }
		}, {
			binding: 1,
			visibility: GPUShaderStage.FRAGMENT,
			texture: { sampleType: "float" }
		}] });
		return bindGroupLayout;
	};
	const ensurePipeline = (format) => {
		const cached = pipelineByFormat.get(format);
		if (cached) return cached;
		const layout = ensureBindGroupLayout();
		shaderModule ??= device.createShaderModule({ code: GPU_MIPMAP_SHADER });
		pipelineLayout ??= device.createPipelineLayout({ bindGroupLayouts: [layout] });
		const pipeline = device.createRenderPipeline({
			layout: pipelineLayout,
			vertex: {
				module: shaderModule,
				entryPoint: "vertexMain"
			},
			fragment: {
				module: shaderModule,
				entryPoint: "fragmentMain",
				targets: [{ format }]
			},
			primitive: { topology: "triangle-list" }
		});
		pipelineByFormat.set(format, pipeline);
		return pipeline;
	};
	return { generate: ({ commandEncoder, texture, format, mipLevelCount }) => {
		if (mipLevelCount <= 1) return;
		sampler ??= device.createSampler({
			minFilter: "linear",
			magFilter: "linear"
		});
		const layout = ensureBindGroupLayout();
		const pipeline = ensurePipeline(format);
		for (let level = 1; level < mipLevelCount; level += 1) {
			const sourceView = texture.createView({
				baseMipLevel: level - 1,
				mipLevelCount: 1
			});
			const targetView = texture.createView({
				baseMipLevel: level,
				mipLevelCount: 1
			});
			const bindGroup = device.createBindGroup({
				layout,
				entries: [{
					binding: 0,
					resource: sampler
				}, {
					binding: 1,
					resource: sourceView
				}]
			});
			const pass = commandEncoder.beginRenderPass({ colorAttachments: [{
				view: targetView,
				clearValue: {
					r: 0,
					g: 0,
					b: 0,
					a: 0
				},
				loadOp: "clear",
				storeOp: "store"
			}] });
			pass.setPipeline(pipeline);
			pass.setBindGroup(0, bindGroup);
			pass.draw(3);
			pass.end();
		}
	} };
}
function markTextureMipmapsDirty(binding, mipLevelCount) {
	if (binding.generateMipmaps && mipLevelCount > 1) binding.mipmapsDirty = true;
	else binding.mipmapsDirty = false;
}
/**
* Creates bind group layout entries for frame/uniform buffers plus texture bindings.
*/
function createBindGroupLayoutEntries(textureBindings) {
	const entries = [{
		binding: FRAME_BINDING,
		visibility: GPUShaderStage.FRAGMENT,
		buffer: {
			type: "uniform",
			minBindingSize: 16
		}
	}, {
		binding: UNIFORM_BINDING,
		visibility: GPUShaderStage.FRAGMENT,
		buffer: { type: "uniform" }
	}];
	for (const binding of textureBindings) {
		entries.push({
			binding: binding.samplerBinding,
			visibility: GPUShaderStage.FRAGMENT,
			sampler: { type: binding.samplerType }
		});
		entries.push({
			binding: binding.textureBinding,
			visibility: GPUShaderStage.FRAGMENT,
			texture: {
				sampleType: binding.resource.sampleType,
				viewDimension: "2d",
				multisampled: false
			}
		});
	}
	return entries;
}
/**
* Maximum gap (in floats) between two dirty ranges that triggers merge.
*
* Set to 4 (16 bytes) which covers one vec4f alignment slot.
*/
var DIRTY_RANGE_MERGE_GAP = 4;
/**
* Shared empty result returned when no float values differ between snapshots.
*
* Avoids allocating a new `[]` on every clean frame (the common steady-state
* case). Callers must not mutate this reference.
*/
var EMPTY_DIRTY_RANGES = [];
/**
* Computes dirty float ranges between two uniform snapshots.
*
* Adjacent dirty ranges separated by a gap smaller than or equal to
* {@link DIRTY_RANGE_MERGE_GAP} are merged to reduce `writeBuffer` calls.
*
* Returns a shared empty array reference when the buffers are identical —
* callers must not mutate the returned array.
*/
function findDirtyFloatRanges(previous, next, mergeGapThreshold = DIRTY_RANGE_MERGE_GAP) {
	let start = -1;
	let rangeCount = 0;
	const ranges = [];
	for (let index = 0; index < next.length; index += 1) {
		if (previous[index] !== next[index]) {
			if (start === -1) start = index;
			continue;
		}
		if (start !== -1) {
			ranges.push({
				start,
				count: index - start
			});
			rangeCount += 1;
			start = -1;
		}
	}
	if (start !== -1) {
		ranges.push({
			start,
			count: next.length - start
		});
		rangeCount += 1;
	}
	if (rangeCount === 0) return EMPTY_DIRTY_RANGES;
	if (rangeCount <= 1) return ranges;
	const merged = [ranges[0]];
	for (let index = 1; index < rangeCount; index += 1) {
		const prev = merged[merged.length - 1];
		const curr = ranges[index];
		if (curr.start - (prev.start + prev.count) <= mergeGapThreshold) prev.count = curr.start + curr.count - prev.start;
		else merged.push(curr);
	}
	return merged;
}
/**
* Allocates a render target texture with usage flags suitable for passes/blits.
*/
function createRenderTexture(device, width, height, format) {
	assertTextureFormat(format, "Render target");
	assertTextureAllocationSize(device, width, height, "Render target");
	const texture = device.createTexture({
		size: {
			width,
			height,
			depthOrArrayLayers: 1
		},
		format,
		usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC
	});
	return {
		texture,
		view: texture.createView(),
		width,
		height,
		format
	};
}
/**
* Destroys a render target texture if present.
*/
function destroyRenderTexture(target) {
	target?.texture.destroy();
}
function toClearValue(color) {
	return {
		r: color[0],
		g: color[1],
		b: color[2],
		a: color[3]
	};
}
function toPremultipliedCanvasClearValue(color) {
	const alpha = Math.min(Math.max(color[3], 0), 1);
	return {
		r: color[0] * alpha,
		g: color[1] * alpha,
		b: color[2] * alpha,
		a: alpha
	};
}
/**
* Creates the WebGPU renderer used by `FragCanvas`.
*
* @param options - Renderer creation options resolved from material/context state.
* @returns Renderer instance with `render` and `destroy`.
* @throws {Error} On WebGPU unavailability, shader compilation issues, or runtime setup failures.
*/
async function createRenderer(options) {
	if (!navigator.gpu) throw new Error("WebGPU is not available in this browser");
	const context = options.canvas.getContext("webgpu");
	if (!context) throw new Error("Canvas does not support webgpu context");
	const preferredCanvasFormat = navigator.gpu.getPreferredCanvasFormat();
	const colorPipeline = resolveColorPipeline({
		color: options.color,
		preferredCanvasFormat
	});
	const workingFormat = colorPipeline.workingFormat;
	const scenePipelineFormat = colorPipeline.requiresPresentationPass ? workingFormat : colorPipeline.canvasFormat;
	let effectiveCanvasFormat = colorPipeline.canvasFormat;
	let effectiveDynamicRange = colorPipeline.dynamicRange === "auto" ? "hdr" : colorPipeline.dynamicRange;
	const adapter = await navigator.gpu.requestAdapter(options.adapterOptions);
	if (!adapter) throw new Error("Unable to acquire WebGPU adapter");
	const device = await adapter.requestDevice(options.deviceDescriptor);
	const maxComputeWorkgroupsPerDimension = getMaxComputeWorkgroupsPerDimension(device);
	let isDestroyed = false;
	let deviceLostMessage = null;
	const uncapturedErrorMessages = [];
	const initializationCleanups = [];
	let acceptInitializationCleanups = true;
	const MAX_UNCAPTURED_ERROR_MESSAGES = 12;
	const destroyDevice = () => {
		try {
			device.destroy();
		} catch {}
	};
	const isDerivativeUncapturedMessage = (message) => {
		const normalized = message.toLowerCase();
		return normalized.includes("is invalid due to a previous error") || normalized.includes("too many warnings, no more warnings will be reported");
	};
	const consumeUncapturedErrorMessage = () => {
		if (uncapturedErrorMessages.length === 0) return null;
		const uniqueMessages = [];
		for (const message of uncapturedErrorMessages) if (!uniqueMessages.includes(message)) uniqueMessages.push(message);
		uncapturedErrorMessages.length = 0;
		const primaryIndex = uniqueMessages.findIndex((message) => !isDerivativeUncapturedMessage(message));
		if (primaryIndex === -1) return null;
		const primaryMessage = uniqueMessages[primaryIndex];
		if (!primaryMessage) return null;
		const relatedMessages = uniqueMessages.filter((_, index) => index !== primaryIndex);
		if (relatedMessages.length === 0) return `WebGPU uncaptured error: ${primaryMessage}`;
		return [
			`WebGPU uncaptured error: ${primaryMessage}`,
			`Additional uncaptured WebGPU errors (${relatedMessages.length}):`,
			...relatedMessages.map((message, index) => `[${index + 1}] ${message}`)
		].join("\n");
	};
	const registerInitializationCleanup = (cleanup) => {
		if (!acceptInitializationCleanups) return;
		options.__onInitializationCleanupRegistered?.();
		initializationCleanups.push(cleanup);
	};
	const runInitializationCleanups = () => {
		for (let index = initializationCleanups.length - 1; index >= 0; index -= 1) try {
			initializationCleanups[index]?.();
		} catch {}
		initializationCleanups.length = 0;
	};
	device.lost.then((info) => {
		if (isDestroyed) return;
		const reason = info.reason ? ` (${info.reason})` : "";
		const details = info.message?.trim();
		deviceLostMessage = details ? `WebGPU device lost: ${details}${reason}` : `WebGPU device lost${reason}`;
		options.requestRender?.();
	});
	const handleUncapturedError = (event) => {
		if (isDestroyed) return;
		const trimmedMessage = (event.error instanceof Error ? event.error.message : String(event.error?.message ?? event.error)).trim();
		const normalizedMessage = trimmedMessage.length > 0 ? trimmedMessage : "Unknown GPU validation error";
		if (uncapturedErrorMessages[uncapturedErrorMessages.length - 1] === normalizedMessage) return;
		uncapturedErrorMessages.push(normalizedMessage);
		if (uncapturedErrorMessages.length > MAX_UNCAPTURED_ERROR_MESSAGES) uncapturedErrorMessages.splice(0, uncapturedErrorMessages.length - MAX_UNCAPTURED_ERROR_MESSAGES);
		options.requestRender?.();
	};
	device.addEventListener("uncapturederror", handleUncapturedError);
	try {
		validateWorkingFormat(workingFormat, device.features);
		const presentationSamplingLayout = resolveTextureSamplingLayout({
			format: workingFormat,
			filter: "linear",
			deviceFeatures: device.features
		});
		const initialRenderTargetFormats = validateRenderTargetFormats(options.getRenderTargets ? void 0 : options.renderTargets, workingFormat, device.features);
		if (!options.getPasses && !options.getRenderTargets) {
			const initialPasses = options.passes ?? [];
			validateBuiltInRenderPassFormats({
				passes: initialPasses,
				workingFormat,
				namedFormats: initialRenderTargetFormats,
				deviceFeatures: device.features
			});
			const presentationSourceSlot = resolvePresentationSourceSlot(initialPasses);
			if (presentationSourceSlot !== null) validatePresentationSourceFormat({
				slot: presentationSourceSlot,
				workingFormat,
				namedFormats: initialRenderTargetFormats,
				deviceFeatures: device.features,
				requiresFilterableInput: presentationSamplingLayout.samplerType === "filtering"
			});
		}
		const runtimeContext = buildShaderCompilationRuntimeContext(options);
		const convertLinearToSrgb = !colorPipeline.requiresPresentationPass && shouldConvertLinearToSrgb(colorPipeline.outputEncoding, colorPipeline.canvasFormat, "sdr");
		const fragmentTextureKeys = options.textureKeys.filter((key) => options.textureDefinitions[key]?.fragmentVisible !== false);
		const buildSceneShader = (premultiplyOutputAlpha) => buildShaderSourceWithMap(options.fragmentWgsl, options.uniformLayout, fragmentTextureKeys, {
			convertLinearToSrgb,
			premultiplyOutputAlpha,
			fragmentLineMap: options.fragmentLineMap,
			...options.storageBufferKeys !== void 0 ? { storageBufferKeys: options.storageBufferKeys } : {},
			...options.storageBufferDefinitions !== void 0 ? { storageBufferDefinitions: options.storageBufferDefinitions } : {}
		});
		const builtShader = buildSceneShader(false);
		const shaderModule = device.createShaderModule({ code: builtShader.code });
		const assertSceneShaderCompilation = (module, builtSource) => assertCompilation(module, {
			lineMap: builtSource.lineMap,
			fragmentSource: options.fragmentSource,
			includeSources: options.includeSources,
			...options.defineBlockSource !== void 0 ? { defineBlockSource: options.defineBlockSource } : {},
			materialSource: options.materialSource ?? null,
			runtimeContext
		});
		await assertSceneShaderCompilation(shaderModule, builtShader);
		const builtDirectCanvasShader = !colorPipeline.requiresPresentationPass ? buildSceneShader(true) : null;
		const directCanvasShaderModule = builtDirectCanvasShader ? device.createShaderModule({ code: builtDirectCanvasShader.code }) : null;
		if (directCanvasShaderModule && builtDirectCanvasShader) await assertSceneShaderCompilation(directCanvasShaderModule, builtDirectCanvasShader);
		const normalizedTextureDefinitions = normalizeTextureDefinitions(options.textureDefinitions, options.textureKeys);
		for (const key of options.textureKeys) {
			const definition = normalizedTextureDefinitions[key];
			if (!definition) continue;
			assertTextureFormatSupported({
				format: definition.format,
				target: key,
				pass: "Material texture allocation",
				deviceFeatures: device.features
			});
			if (definition.fragmentVisible) assertFloatSampledFormat({
				format: definition.format,
				target: key,
				pass: "Material fragment texture",
				deviceFeatures: device.features
			});
			if (definition.storage) assertStorageTextureAccess({
				format: definition.format,
				target: key,
				pass: "Material storage texture allocation",
				access: "write-only",
				deviceFeatures: device.features
			});
		}
		const storageBufferKeys = options.storageBufferKeys ?? [];
		const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
		const storageTextureKeys = options.storageTextureKeys ?? [];
		const storageTextureKeySet = new Set(storageTextureKeys);
		const resourceRegistry = new MaterialResourceRegistry();
		const sampledFallbackPool = new ComputeSampledFallbackTexturePool(device);
		registerInitializationCleanup(() => sampledFallbackPool.destroy());
		const sampledFallbackUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST;
		const fragmentTextureIndexByKey = new Map(fragmentTextureKeys.map((key, index) => [key, index]));
		const textureBindings = options.textureKeys.map((key) => {
			const config = normalizedTextureDefinitions[key];
			if (!config) throw new Error(`Missing texture definition for "${key}"`);
			const fragmentTextureIndex = fragmentTextureIndexByKey.get(key);
			const fragmentVisible = fragmentTextureIndex !== void 0;
			const { samplerBinding, textureBinding } = getTextureBindings(fragmentTextureIndex ?? 0);
			const samplingLayout = resolveTextureSamplingLayout({
				format: config.format,
				filter: config.filter,
				deviceFeatures: device.features
			});
			if (config.generateMipmaps && samplingLayout.sampleType !== "float") throw new Error(`Texture "${key}" with format "${config.format}" cannot generate mipmaps because it is not filterable on this device.`);
			const sampler = device.createSampler({
				magFilter: samplingLayout.effectiveFilter,
				minFilter: samplingLayout.effectiveFilter,
				mipmapFilter: config.generateMipmaps ? samplingLayout.effectiveFilter : "nearest",
				addressModeU: config.addressModeU,
				addressModeV: config.addressModeV,
				maxAnisotropy: samplingLayout.samplerType === "filtering" && samplingLayout.effectiveFilter === "linear" ? config.anisotropy : 1
			});
			let fallbackView;
			let resource;
			if (config.storage) {
				if (!config.width || !config.height) throw new Error(`Storage texture "${key}" requires explicit positive width and height.`);
				assertTextureAllocationSize(device, config.width, config.height, `Texture "${key}"`);
				const storageUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST;
				const storageTexture = device.createTexture({
					size: {
						width: config.width,
						height: config.height,
						depthOrArrayLayers: 1
					},
					format: config.format,
					usage: storageUsage
				});
				registerInitializationCleanup(() => storageTexture.destroy());
				fallbackView = storageTexture.createView();
				resource = resourceRegistry.registerTexture({
					logicalId: key,
					ownedTexture: storageTexture,
					storageView: fallbackView,
					sampledView: fallbackView,
					format: config.format,
					width: config.width,
					height: config.height,
					mipLevelCount: 1,
					sampleType: samplingLayout.sampleType,
					usage: storageUsage
				});
			} else {
				fallbackView = sampledFallbackPool.get(toComputeSampledFallbackClass(samplingLayout.sampleType)).view;
				resource = resourceRegistry.registerTexture({
					logicalId: key,
					sampledView: fallbackView,
					format: config.format,
					mipLevelCount: 1,
					sampleType: samplingLayout.sampleType,
					usage: sampledFallbackUsage
				});
			}
			const runtimeBinding = {
				key,
				resource,
				samplerBinding,
				textureBinding,
				fragmentVisible,
				sampler,
				fallbackView,
				source: null,
				samplerType: samplingLayout.samplerType,
				effectiveFilter: samplingLayout.effectiveFilter,
				colorSpace: config.colorSpace,
				defaultColorSpace: config.colorSpace,
				flipY: config.flipY,
				defaultFlipY: config.flipY,
				generateMipmaps: config.generateMipmaps,
				defaultGenerateMipmaps: config.generateMipmaps,
				premultipliedAlpha: config.premultipliedAlpha,
				defaultPremultipliedAlpha: config.premultipliedAlpha,
				update: config.update ?? "once",
				lastToken: null,
				mipmapsDirty: false,
				feedbackViewActive: false
			};
			if (config.update !== void 0) runtimeBinding.defaultUpdate = config.update;
			return runtimeBinding;
		});
		const textureBindingByKey = new Map(textureBindings.map((binding) => [binding.key, binding]));
		const fragmentTextureBindings = textureBindings.filter((binding) => binding.fragmentVisible);
		const bindGroupLayout = device.createBindGroupLayout({ entries: createBindGroupLayoutEntries(fragmentTextureBindings) });
		const fragmentStorageBindGroupLayout = storageBufferKeys.length > 0 ? device.createBindGroupLayout({ entries: storageBufferKeys.map((_, index) => ({
			binding: index,
			visibility: GPUShaderStage.FRAGMENT,
			buffer: { type: "read-only-storage" }
		})) }) : null;
		const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: fragmentStorageBindGroupLayout ? [bindGroupLayout, fragmentStorageBindGroupLayout] : [bindGroupLayout] });
		const pipeline = device.createRenderPipeline({
			layout: pipelineLayout,
			vertex: {
				module: shaderModule,
				entryPoint: "motiongpuVertex"
			},
			fragment: {
				module: shaderModule,
				entryPoint: "motiongpuFragmentMain",
				targets: [{ format: scenePipelineFormat }]
			},
			primitive: { topology: "triangle-list" }
		});
		const directCanvasPipeline = directCanvasShaderModule ? device.createRenderPipeline({
			layout: pipelineLayout,
			vertex: {
				module: directCanvasShaderModule,
				entryPoint: "motiongpuVertex"
			},
			fragment: {
				module: directCanvasShaderModule,
				entryPoint: "motiongpuFragmentMain",
				targets: [{ format: colorPipeline.canvasFormat }]
			},
			primitive: { topology: "triangle-list" }
		}) : null;
		const presentationBindGroupLayout = device.createBindGroupLayout({ entries: [{
			binding: 0,
			visibility: GPUShaderStage.FRAGMENT,
			sampler: { type: presentationSamplingLayout.samplerType }
		}, {
			binding: 1,
			visibility: GPUShaderStage.FRAGMENT,
			texture: {
				sampleType: presentationSamplingLayout.sampleType,
				viewDimension: "2d",
				multisampled: false
			}
		}] });
		const presentationPipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [presentationBindGroupLayout] });
		const presentationPipelines = /* @__PURE__ */ new Map();
		const buildPresentationPipelineKey = (canvasFormat, dynamicRange, applyFinalTransform, premultiplyAlpha) => {
			return `${canvasFormat}|${dynamicRange}|${applyFinalTransform}|${premultiplyAlpha}`;
		};
		const createPresentationPipeline = async (canvasFormat, dynamicRange, applyFinalTransform, premultiplyAlpha) => {
			const key = buildPresentationPipelineKey(canvasFormat, dynamicRange, applyFinalTransform, premultiplyAlpha);
			if (presentationPipelines.has(key)) return;
			const convertPresentationLinearToSrgb = applyFinalTransform && shouldConvertLinearToSrgb(colorPipeline.outputEncoding, canvasFormat, dynamicRange);
			const presentationShaderModule = device.createShaderModule({ code: buildPresentationShader({
				toneMapping: applyFinalTransform ? colorPipeline.toneMapping : "none",
				convertLinearToSrgb: convertPresentationLinearToSrgb,
				dynamicRange,
				premultiplyAlpha
			}) });
			await assertCompilation(presentationShaderModule);
			presentationPipelines.set(key, device.createRenderPipeline({
				layout: presentationPipelineLayout,
				vertex: {
					module: presentationShaderModule,
					entryPoint: "motiongpuPresentationVertex"
				},
				fragment: {
					module: presentationShaderModule,
					entryPoint: "motiongpuPresentationFragment",
					targets: [{ format: canvasFormat }]
				},
				primitive: { topology: "triangle-list" }
			}));
		};
		await createPresentationPipeline(colorPipeline.canvasFormat, colorPipeline.dynamicRange === "auto" ? "hdr" : colorPipeline.dynamicRange, colorPipeline.requiresPresentationPass, true);
		if (colorPipeline.dynamicRange === "auto") await createPresentationPipeline(colorPipeline.fallbackCanvasFormat, "sdr", colorPipeline.requiresPresentationPass, true);
		const presentationSampler = device.createSampler({
			magFilter: presentationSamplingLayout.effectiveFilter,
			minFilter: presentationSamplingLayout.effectiveFilter,
			addressModeU: "clamp-to-edge",
			addressModeV: "clamp-to-edge"
		});
		let presentationBindGroupByView = /* @__PURE__ */ new WeakMap();
		const pingPongTexturePairs = /* @__PURE__ */ new Map();
		const pingPongShaderTexturePairs = /* @__PURE__ */ new Map();
		for (const key of storageBufferKeys) {
			const definition = storageBufferDefinitions[key];
			if (!definition) continue;
			const normalized = normalizeStorageBufferDefinition(definition);
			const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
			const buffer = device.createBuffer({
				size: normalized.size,
				usage
			});
			registerInitializationCleanup(() => {
				buffer.destroy();
			});
			if (definition.initialData !== void 0 && definition.initialData.byteLength > 0) {
				const data = definition.initialData;
				device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength);
			}
			resourceRegistry.registerStorageBuffer({
				logicalId: key,
				buffer,
				size: normalized.size,
				wgslType: normalized.type,
				access: normalized.access,
				usage
			});
		}
		const fragmentStorageBindGroup = fragmentStorageBindGroupLayout && storageBufferKeys.length > 0 ? device.createBindGroup({
			layout: fragmentStorageBindGroupLayout,
			entries: storageBufferKeys.map((key, index) => {
				return {
					binding: index,
					resource: { buffer: resourceRegistry.requireStorageBuffer(key).buffer }
				};
			})
		}) : null;
		const ensurePingPongTexturePair = (pass, logicalId) => {
			const existing = pingPongTexturePairs.get(pass);
			if (existing && existing.logicalId === logicalId) return existing;
			if (existing) {
				existing.textureA.destroy();
				existing.textureB.destroy();
				pingPongTexturePairs.delete(pass);
			}
			const config = normalizedTextureDefinitions[logicalId];
			if (!config || !config.storage) throw new Error(`PingPongComputePass resource "${logicalId}" must reference a texture declared with storage:true.`);
			if (!config.width || !config.height) throw new Error(`PingPongComputePass resource "${logicalId}" requires explicit texture width and height.`);
			assertTextureAllocationSize(device, config.width, config.height, `PingPongComputePass resource "${logicalId}"`);
			const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST;
			const textureA = device.createTexture({
				size: {
					width: config.width,
					height: config.height,
					depthOrArrayLayers: 1
				},
				format: config.format,
				usage
			});
			const textureB = device.createTexture({
				size: {
					width: config.width,
					height: config.height,
					depthOrArrayLayers: 1
				},
				format: config.format,
				usage
			});
			registerInitializationCleanup(() => {
				textureA.destroy();
			});
			registerInitializationCleanup(() => {
				textureB.destroy();
			});
			const pair = {
				logicalId,
				format: config.format,
				width: config.width,
				height: config.height,
				textureA,
				viewA: textureA.createView(),
				textureB,
				viewB: textureB.createView(),
				readFromA: true
			};
			pingPongTexturePairs.set(pass, pair);
			return pair;
		};
		const destroyPingPongShaderTexturePair = (pair) => {
			pair.textureA.destroy();
			pair.textureB.destroy();
		};
		const ensurePingPongShaderTexturePair = (pass, options) => {
			const existing = pingPongShaderTexturePairs.get(pass);
			if (existing && existing.target === options.target && existing.width === options.width && existing.height === options.height && existing.format === options.format && existing.filter === options.filter && existing.addressModeU === options.addressModeU && existing.addressModeV === options.addressModeV) return existing;
			if (existing) destroyPingPongShaderTexturePair(existing);
			assertTextureAllocationSize(device, options.width, options.height, `PingPongShaderPass target "${options.target}"`);
			const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST;
			const textureA = device.createTexture({
				size: {
					width: options.width,
					height: options.height,
					depthOrArrayLayers: 1
				},
				format: options.format,
				usage
			});
			const textureB = device.createTexture({
				size: {
					width: options.width,
					height: options.height,
					depthOrArrayLayers: 1
				},
				format: options.format,
				usage
			});
			const samplingLayout = resolveTextureSamplingLayout({
				format: options.format,
				filter: options.filter,
				deviceFeatures: device.features
			});
			const sampler = device.createSampler({
				magFilter: samplingLayout.effectiveFilter,
				minFilter: samplingLayout.effectiveFilter,
				addressModeU: options.addressModeU,
				addressModeV: options.addressModeV
			});
			const pair = {
				target: options.target,
				format: options.format,
				width: options.width,
				height: options.height,
				filter: options.filter,
				addressModeU: options.addressModeU,
				addressModeV: options.addressModeV,
				sampleType: samplingLayout.sampleType,
				samplerType: samplingLayout.samplerType,
				effectiveFilter: samplingLayout.effectiveFilter,
				textureA,
				viewA: textureA.createView(),
				textureB,
				viewB: textureB.createView(),
				sampler,
				previousBindGroupLayout: null,
				readABindGroup: null,
				readBBindGroup: null,
				needsClear: true
			};
			pingPongShaderTexturePairs.set(pass, pair);
			return pair;
		};
		const MAX_COMPUTE_PIPELINE_CACHE_ENTRIES = 32;
		const computePipelineCache = /* @__PURE__ */ new Map();
		let nextComputePipelineLabelIndex = 0;
		const computeResourceLimits = getComputeResourceResolverLimits(device);
		const computeUniformTopologyKey = options.uniformLayout.entries.map((entry) => `${entry.name}:${entry.type}`).join(",");
		const computeDeviceCapabilityKey = [...Array.from(device.features).sort(), ...Object.entries(computeResourceLimits).map(([name, value]) => `${name}:${value}`)].join(",");
		const requestRender = options.requestRender;
		const setComputePipelineCacheState = (cacheKey, state) => {
			if (computePipelineCache.has(cacheKey)) computePipelineCache.delete(cacheKey);
			computePipelineCache.set(cacheKey, state);
			while (computePipelineCache.size > MAX_COMPUTE_PIPELINE_CACHE_ENTRIES) {
				const oldestKey = computePipelineCache.keys().next().value;
				if (oldestKey === void 0) break;
				computePipelineCache.delete(oldestKey);
			}
		};
		const touchComputePipelineCacheState = (cacheKey, state) => {
			computePipelineCache.delete(cacheKey);
			computePipelineCache.set(cacheKey, state);
		};
		const computeBuildResult = (cacheKey, buildOptions) => {
			const builtComputeShader = buildComputeShaderSourceWithMap({
				compute: buildOptions.computeSource,
				uniformLayout: options.uniformLayout,
				resources: buildOptions.resources.entries
			});
			const labelIndex = nextComputePipelineLabelIndex += 1;
			const labelBase = `compute[${buildOptions.resources.topologyKey || "uniforms-only"}]#${labelIndex}`;
			const moduleLabel = `${labelBase}:module`;
			const pipelineLabel = `${labelBase}:pipeline`;
			const workgroupSize = [...buildOptions.workgroupSize];
			const computeUniformBGL = device.createBindGroupLayout({
				label: `${labelBase}:bgl-uniforms`,
				entries: [{
					binding: FRAME_BINDING,
					visibility: GPUShaderStage.COMPUTE,
					buffer: {
						type: "uniform",
						minBindingSize: 16
					}
				}, {
					binding: UNIFORM_BINDING,
					visibility: GPUShaderStage.COMPUTE,
					buffer: { type: "uniform" }
				}]
			});
			const resourceBindGroupLayout = buildOptions.resources.entries.length > 0 ? device.createBindGroupLayout({
				label: `${labelBase}:bgl-resources`,
				entries: buildOptions.resources.entries.map((entry) => entry.layoutEntry)
			}) : null;
			const bindGroupLayouts = [computeUniformBGL];
			if (resourceBindGroupLayout) bindGroupLayouts.push(resourceBindGroupLayout);
			const computePipelineLayout = device.createPipelineLayout({
				label: `${labelBase}:layout`,
				bindGroupLayouts
			});
			device.pushErrorScope("validation");
			let computeShaderModule;
			let pipeline;
			try {
				computeShaderModule = device.createShaderModule({
					label: moduleLabel,
					code: builtComputeShader.code
				});
				pipeline = device.createComputePipeline({
					label: pipelineLabel,
					layout: computePipelineLayout,
					compute: {
						module: computeShaderModule,
						entryPoint: "compute"
					}
				});
			} catch (jsError) {
				device.popErrorScope().catch(() => {});
				return {
					kind: "error",
					error: toComputeCompilationError({
						error: jsError,
						lineMap: builtComputeShader.lineMap,
						computeSource: buildOptions.computeSource,
						runtimeContext
					})
				};
			}
			const validationScope = device.popErrorScope();
			const computeUniformBindGroup = device.createBindGroup({
				label: `${labelBase}:bg-uniforms`,
				layout: computeUniformBGL,
				entries: [{
					binding: FRAME_BINDING,
					resource: { buffer: frameBuffer }
				}, {
					binding: UNIFORM_BINDING,
					resource: { buffer: uniformBuffer }
				}]
			});
			const entry = {
				pipeline,
				uniformBindGroup: computeUniformBindGroup,
				resourceBindGroupLayout,
				resourceBindGroupCaches: /* @__PURE__ */ new WeakMap(),
				pingPongResourceBindGroupCaches: /* @__PURE__ */ new WeakMap(),
				workgroupSize,
				computeSource: buildOptions.computeSource,
				topologyKey: buildOptions.resources.topologyKey
			};
			return {
				kind: "pending",
				entry,
				validation: (async () => {
					const compilationError = await assertComputeCompilationAsync({
						module: computeShaderModule,
						validationScope,
						lineMap: builtComputeShader.lineMap,
						computeSource: buildOptions.computeSource,
						runtimeContext
					});
					if (isDestroyed) return;
					const current = computePipelineCache.get(cacheKey);
					if (!current || current.kind !== "pending") return;
					if (compilationError) {
						setComputePipelineCacheState(cacheKey, {
							kind: "error",
							error: compilationError
						});
						uncapturedErrorMessages.length = 0;
						requestRender?.();
					} else setComputePipelineCacheState(cacheKey, {
						kind: "ready",
						entry
					});
				})()
			};
		};
		const buildComputePipelineEntry = (buildOptions) => {
			const cacheKey = `compute:${computeUniformTopologyKey}:${buildOptions.resources.topologyKey}:${computeDeviceCapabilityKey}:${buildOptions.workgroupSize.join(",")}:${buildOptions.computeSource}`;
			const cached = computePipelineCache.get(cacheKey);
			if (cached) {
				touchComputePipelineCacheState(cacheKey, cached);
				if (cached.kind === "error") {
					uncapturedErrorMessages.length = 0;
					throw cached.error;
				}
				return cached.entry;
			}
			const state = computeBuildResult(cacheKey, buildOptions);
			setComputePipelineCacheState(cacheKey, state);
			if (state.kind === "error") {
				uncapturedErrorMessages.length = 0;
				throw state.error;
			}
			return state.entry;
		};
		const pingPongShaderPipelineCache = /* @__PURE__ */ new Map();
		const getFragmentTextureBindingsForKeys = (keys) => keys.map((key, index) => {
			const binding = textureBindingByKey.get(key);
			if (!binding || !binding.fragmentVisible) throw new Error(`Missing fragment texture binding for "${key}".`);
			return {
				...binding,
				...getTextureBindings(index)
			};
		});
		const buildPingPongShaderPipelineEntry = (pass, format, target) => {
			assertFloatSampledFormat({
				format,
				target,
				pass: "PingPongShaderPass",
				deviceFeatures: device.features
			});
			assertFloatRenderableFormat({
				format,
				target,
				pass: "PingPongShaderPass",
				deviceFeatures: device.features
			});
			const fragment = pass.getFragment();
			if (!fragment) throw new Error("PingPongShaderPass must provide a fragment shader.");
			const feedbackTextureKeys = fragmentTextureKeys.filter((key) => key !== target);
			const previousSamplingLayout = resolveTextureSamplingLayout({
				format,
				filter: pass.getFilter(),
				deviceFeatures: device.features
			});
			const cacheKey = [
				format,
				target,
				previousSamplingLayout.sampleType,
				previousSamplingLayout.samplerType,
				previousSamplingLayout.effectiveFilter,
				feedbackTextureKeys.join(","),
				options.uniformLayout.entries.map((entry) => `${entry.name}:${entry.type}`).join(","),
				fragment
			].join("|");
			const cached = pingPongShaderPipelineCache.get(cacheKey);
			if (cached) return cached;
			const fragmentLineMap = pass.getFragmentLineMap();
			const builtShader = buildPingPongShaderSourceWithMap(fragment, options.uniformLayout, feedbackTextureKeys, { fragmentLineMap });
			const shaderModule = device.createShaderModule({ code: builtShader.code });
			const feedbackBindGroupLayout = device.createBindGroupLayout({ entries: createBindGroupLayoutEntries(getFragmentTextureBindingsForKeys(feedbackTextureKeys)) });
			const previousBindGroupLayout = device.createBindGroupLayout({ entries: [{
				binding: 0,
				visibility: GPUShaderStage.FRAGMENT,
				sampler: { type: previousSamplingLayout.samplerType }
			}, {
				binding: 1,
				visibility: GPUShaderStage.FRAGMENT,
				texture: {
					sampleType: previousSamplingLayout.sampleType,
					viewDimension: "2d",
					multisampled: false
				}
			}] });
			const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [feedbackBindGroupLayout, previousBindGroupLayout] });
			const entry = {
				pipeline: device.createRenderPipeline({
					layout: pipelineLayout,
					vertex: {
						module: shaderModule,
						entryPoint: "motiongpuPingPongVertex"
					},
					fragment: {
						module: shaderModule,
						entryPoint: "motiongpuPingPongFragment",
						targets: [{ format }]
					},
					primitive: { topology: "triangle-list" }
				}),
				bindGroupLayout: feedbackBindGroupLayout,
				previousBindGroupLayout,
				textureKeys: feedbackTextureKeys
			};
			pingPongShaderPipelineCache.set(cacheKey, entry);
			return entry;
		};
		const getComputeBindingResource = (entry) => {
			if (entry.source === "external") return entry.bindingResource;
			const logicalId = String(entry.logicalId);
			switch (entry.kind) {
				case "sampled-texture": {
					const resource = resourceRegistry.requireTexture(logicalId);
					if (entry.subresource.baseMipLevel === 0 && entry.subresource.mipLevelCount === resource.mipLevelCount) return resource.publishedView;
					return entry.bindingResource;
				}
				case "storage-texture": {
					const view = resourceRegistry.requireTexture(logicalId).storageView;
					if (!view) throw new Error(`Storage texture "${logicalId}" is not allocated.`);
					return view;
				}
				case "storage-buffer": return {
					buffer: resourceRegistry.requireStorageBuffer(logicalId).buffer,
					size: entry.size
				};
				case "sampler": {
					const binding = textureBindingByKey.get(logicalId);
					if (!binding) throw new Error(`Material sampler "${logicalId}" is not available.`);
					return binding.sampler;
				}
			}
		};
		const getBindingReference = (resource) => "buffer" in resource ? resource.buffer : resource;
		const createResolvedBindGroupEntries = (resources, pingPong) => {
			const entries = [];
			const refs = [];
			for (const entry of resources.entries) {
				let resource = getComputeBindingResource(entry);
				if (pingPong && entry.kind === "sampled-texture" && entry.pingPong === "read") resource = pingPong.readFromA ? pingPong.pair.viewA : pingPong.pair.viewB;
				else if (pingPong && entry.kind === "storage-texture" && entry.pingPong === "write") resource = pingPong.readFromA ? pingPong.pair.viewB : pingPong.pair.viewA;
				entries.push({
					binding: entry.binding,
					resource
				});
				refs.push(getBindingReference(resource));
			}
			return {
				entries,
				refs
			};
		};
		const getComputeResourceBindGroup = (pipelineEntry, pass, resources) => {
			if (!pipelineEntry.resourceBindGroupLayout) return null;
			let cache = pipelineEntry.resourceBindGroupCaches.get(pass);
			if (!cache) {
				cache = createComputeBindGroupCache(device);
				pipelineEntry.resourceBindGroupCaches.set(pass, cache);
			}
			const runtimeEntries = createResolvedBindGroupEntries(resources);
			return cache.getOrCreate({
				topologyKey: resources.topologyKey,
				layout: pipelineEntry.resourceBindGroupLayout,
				entries: runtimeEntries.entries,
				resourceRefs: runtimeEntries.refs
			});
		};
		const getPingPongResourceBindGroup = (pipelineEntry, pass, resources, pair, readFromA) => {
			if (!pipelineEntry.resourceBindGroupLayout) throw new Error("Ping-pong compute pipeline is missing its resource bind group layout.");
			let caches = pipelineEntry.pingPongResourceBindGroupCaches.get(pass);
			if (!caches) {
				caches = {
					readA: createComputeBindGroupCache(device),
					readB: createComputeBindGroupCache(device)
				};
				pipelineEntry.pingPongResourceBindGroupCaches.set(pass, caches);
			}
			const runtimeEntries = createResolvedBindGroupEntries(resources, {
				pair,
				readFromA
			});
			const bindGroup = (readFromA ? caches.readA : caches.readB).getOrCreate({
				topologyKey: resources.topologyKey,
				layout: pipelineEntry.resourceBindGroupLayout,
				entries: runtimeEntries.entries,
				resourceRefs: runtimeEntries.refs
			});
			if (!bindGroup) throw new Error("Ping-pong compute resource bind group is empty.");
			return bindGroup;
		};
		let externalTextureViewCache = /* @__PURE__ */ new WeakMap();
		const createCachedExternalTextureView = (texture, descriptor) => {
			const key = [
				descriptor.dimension ?? "2d",
				descriptor.baseMipLevel ?? 0,
				descriptor.mipLevelCount ?? 1,
				descriptor.baseArrayLayer ?? 0,
				descriptor.arrayLayerCount ?? 1
			].join(":");
			let views = externalTextureViewCache.get(texture);
			if (!views) {
				views = /* @__PURE__ */ new Map();
				externalTextureViewCache.set(texture, views);
			}
			let view = views.get(key);
			if (!view) {
				view = texture.createView(descriptor);
				views.set(key, view);
			}
			return view;
		};
		const getPingPongShaderPreviousBindGroup = (pair, layout, readFromA) => {
			if (pair.previousBindGroupLayout !== layout) {
				pair.previousBindGroupLayout = layout;
				pair.readABindGroup = null;
				pair.readBBindGroup = null;
			}
			if (readFromA) {
				if (!pair.readABindGroup) pair.readABindGroup = device.createBindGroup({
					layout,
					entries: [{
						binding: 0,
						resource: pair.sampler
					}, {
						binding: 1,
						resource: pair.viewA
					}]
				});
				return pair.readABindGroup;
			}
			if (!pair.readBBindGroup) pair.readBBindGroup = device.createBindGroup({
				layout,
				entries: [{
					binding: 0,
					resource: pair.sampler
				}, {
					binding: 1,
					resource: pair.viewB
				}]
			});
			return pair.readBBindGroup;
		};
		const frameBuffer = device.createBuffer({
			size: 16,
			usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
		});
		registerInitializationCleanup(() => {
			frameBuffer.destroy();
		});
		const uniformBuffer = device.createBuffer({
			size: options.uniformLayout.byteLength,
			usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
		});
		registerInitializationCleanup(() => {
			uniformBuffer.destroy();
		});
		const frameScratch = /* @__PURE__ */ new Float32Array(4);
		const uniformScratch = new Float32Array(options.uniformLayout.byteLength / 4);
		const uniformPrevious = new Float32Array(options.uniformLayout.byteLength / 4);
		let hasUniformSnapshot = false;
		const mipmapGenerator = createGpuMipmapGenerator(device);
		const writeFrameBuffer = (time, delta, width, height) => {
			frameScratch[0] = time;
			frameScratch[1] = delta;
			frameScratch[2] = width;
			frameScratch[3] = height;
			device.queue.writeBuffer(frameBuffer, 0, frameScratch.buffer, frameScratch.byteOffset, frameScratch.byteLength);
		};
		/**
		* Rebuilds a fragment bind group using current texture views.
		*/
		const createTextureBindGroup = (layout, bindings) => {
			const entries = [{
				binding: FRAME_BINDING,
				resource: { buffer: frameBuffer }
			}, {
				binding: UNIFORM_BINDING,
				resource: { buffer: uniformBuffer }
			}];
			for (const binding of bindings) {
				entries.push({
					binding: binding.samplerBinding,
					resource: binding.sampler
				});
				entries.push({
					binding: binding.textureBinding,
					resource: binding.resource.publishedView
				});
			}
			return device.createBindGroup({
				layout,
				entries
			});
		};
		const createBindGroup = () => createTextureBindGroup(bindGroupLayout, fragmentTextureBindings);
		const createPingPongShaderBindGroup = (entry) => createTextureBindGroup(entry.bindGroupLayout, getFragmentTextureBindingsForKeys(entry.textureKeys));
		const attachFeedbackTextureBinding = (binding, view) => {
			const resource = binding.resource;
			const changed = resource.publishedView !== view || !binding.feedbackViewActive;
			resource.ownedTexture?.destroy();
			resourceRegistry.replaceTextureAllocation(binding.key, {
				ownedTexture: null,
				storageView: null,
				sampledView: binding.fallbackView,
				format: resource.format,
				width: void 0,
				height: void 0,
				mipLevelCount: 1,
				usage: sampledFallbackUsage
			});
			resourceRegistry.publishTextureView(binding.key, view);
			binding.feedbackViewActive = true;
			binding.source = null;
			binding.lastToken = null;
			binding.mipmapsDirty = false;
			return changed;
		};
		/**
		* Synchronizes one runtime texture binding with incoming texture value.
		*
		* @returns `true` when bind group must be rebuilt.
		*/
		const updateTextureBinding = (binding, value, renderMode) => {
			const nextData = toTextureData(value);
			const resource = binding.resource;
			if (!nextData) {
				if (binding.source === null && resource.ownedTexture === null && !binding.feedbackViewActive) return false;
				resource.ownedTexture?.destroy();
				const changed = resourceRegistry.replaceTextureAllocation(binding.key, {
					ownedTexture: null,
					storageView: null,
					sampledView: binding.fallbackView,
					format: resource.format,
					width: void 0,
					height: void 0,
					mipLevelCount: 1,
					usage: sampledFallbackUsage
				});
				binding.feedbackViewActive = false;
				binding.source = null;
				binding.lastToken = null;
				binding.mipmapsDirty = false;
				return changed;
			}
			const source = nextData.source;
			const colorSpace = nextData.colorSpace ?? binding.defaultColorSpace;
			const format = resource.format;
			const flipY = nextData.flipY ?? binding.defaultFlipY;
			const premultipliedAlpha = nextData.premultipliedAlpha ?? binding.defaultPremultipliedAlpha;
			const generateMipmaps = nextData.generateMipmaps ?? binding.defaultGenerateMipmaps;
			const update = resolveTextureUpdateMode({
				source,
				...nextData.update !== void 0 ? { override: nextData.update } : {},
				...binding.defaultUpdate !== void 0 ? { defaultMode: binding.defaultUpdate } : {}
			});
			const { width, height } = resolveTextureSize(nextData);
			assertTextureAllocationSize(device, width, height, `Texture "${binding.key}"`);
			const mipLevelCount = generateMipmaps ? getTextureMipLevelCount(width, height) : 1;
			const sourceChanged = binding.source !== source;
			const tokenChanged = binding.lastToken !== value;
			if (!(resource.ownedTexture === null || binding.feedbackViewActive || resource.width !== width || resource.height !== height || resource.mipLevelCount !== mipLevelCount || resource.format !== format)) {
				if ((sourceChanged || update === "perFrame" || update === "onInvalidate" && (renderMode !== "always" || tokenChanged)) && resource.ownedTexture) {
					uploadTextureBaseLevel(device, resource.ownedTexture, {
						flipY,
						premultipliedAlpha
					}, source, width, height);
					binding.flipY = flipY;
					binding.generateMipmaps = generateMipmaps;
					binding.premultipliedAlpha = premultipliedAlpha;
					binding.colorSpace = colorSpace;
					markTextureMipmapsDirty(binding, mipLevelCount);
				}
				binding.source = source;
				binding.update = update;
				binding.lastToken = value;
				binding.feedbackViewActive = false;
				return false;
			}
			let textureUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
			if (storageTextureKeySet.has(binding.key)) textureUsage |= GPUTextureUsage.STORAGE_BINDING;
			const texture = device.createTexture({
				size: {
					width,
					height,
					depthOrArrayLayers: 1
				},
				format,
				mipLevelCount,
				usage: textureUsage
			});
			let view;
			try {
				uploadTextureBaseLevel(device, texture, {
					flipY,
					premultipliedAlpha
				}, source, width, height);
				view = texture.createView();
			} catch (error) {
				texture.destroy();
				throw error;
			}
			registerInitializationCleanup(() => {
				texture.destroy();
			});
			resource.ownedTexture?.destroy();
			const publishedViewChanged = resourceRegistry.replaceTextureAllocation(binding.key, {
				ownedTexture: texture,
				storageView: storageTextureKeySet.has(binding.key) ? view : null,
				sampledView: view,
				format,
				width,
				height,
				mipLevelCount,
				usage: textureUsage
			});
			binding.feedbackViewActive = false;
			binding.source = source;
			binding.update = update;
			binding.flipY = flipY;
			binding.generateMipmaps = generateMipmaps;
			binding.premultipliedAlpha = premultipliedAlpha;
			binding.colorSpace = colorSpace;
			binding.lastToken = value;
			markTextureMipmapsDirty(binding, mipLevelCount);
			return publishedViewChanged;
		};
		const generateDirtyTextureMipmaps = (commandEncoder) => {
			for (const binding of textureBindings) {
				const resource = binding.resource;
				if (!binding.mipmapsDirty || !resource.ownedTexture || !binding.generateMipmaps || resource.mipLevelCount <= 1) continue;
				mipmapGenerator.generate({
					commandEncoder,
					texture: resource.ownedTexture,
					format: resource.format,
					mipLevelCount: resource.mipLevelCount
				});
				binding.mipmapsDirty = false;
			}
		};
		for (const binding of textureBindings) {
			if (normalizedTextureDefinitions[binding.key]?.storage) continue;
			updateTextureBinding(binding, normalizedTextureDefinitions[binding.key]?.source ?? null, "always");
		}
		let bindGroup = createBindGroup();
		let sourceSlotTarget = null;
		let targetSlotTarget = null;
		let presentationSlotTarget = null;
		let renderTargetSignature = "";
		let renderTargetSnapshot = {};
		let renderTargetFormatSnapshot = {};
		let renderTargetKeys = [];
		let cachedGraphPlan = null;
		let cachedGraphRenderTargetSignature = "";
		const cachedGraphClearColor = [
			NaN,
			NaN,
			NaN,
			NaN
		];
		const cachedGraphPasses = [];
		let contextConfigured = false;
		let configuredWidth = 0;
		let configuredHeight = 0;
		let configuredCanvasFormat = null;
		let configuredDynamicRange = null;
		const runtimeRenderTargets = /* @__PURE__ */ new Map();
		const activePasses = [];
		const lifecyclePreviousSet = /* @__PURE__ */ new Set();
		const lifecycleNextSet = /* @__PURE__ */ new Set();
		const lifecycleUniquePasses = [];
		let lifecyclePassesRef = null;
		let passWidth = 0;
		let passHeight = 0;
		/**
		* Pre-allocated canvas surface object mutated in-place each frame.
		*
		* Avoids creating a new `RenderTarget` object on every `render()` call.
		* The `texture` and `view` fields are replaced with the current
		* swapchain texture before use.
		*/
		const canvasSurface = {
			texture: null,
			view: null,
			width: 0,
			height: 0,
			format: effectiveCanvasFormat
		};
		/**
		* Pre-allocated slots object mutated in-place each frame when passes are active.
		*
		* Avoids a new `{ source, target, canvas }` allocation on every `render()` call.
		*/
		const frameSlots = {
			source: null,
			target: null,
			canvas: canvasSurface
		};
		let frameSlotsActive = false;
		/**
		* Resolves active render pass list for current frame.
		*/
		const resolvePasses = () => {
			return options.getPasses?.() ?? options.passes ?? [];
		};
		/**
		* Resolves active render target declarations for current frame.
		*/
		const resolveRenderTargets = () => {
			return options.getRenderTargets?.() ?? options.renderTargets;
		};
		/**
		* Checks whether cached render-graph plan can be reused for this frame.
		*/
		const isGraphPlanCacheValid = (passes, clearColor) => {
			if (!cachedGraphPlan) return false;
			if (cachedGraphRenderTargetSignature !== renderTargetSignature) return false;
			if (cachedGraphClearColor[0] !== clearColor[0] || cachedGraphClearColor[1] !== clearColor[1] || cachedGraphClearColor[2] !== clearColor[2] || cachedGraphClearColor[3] !== clearColor[3]) return false;
			if (cachedGraphPasses.length !== passes.length) return false;
			for (let index = 0; index < passes.length; index += 1) {
				const pass = passes[index];
				const rp = pass;
				const snapshot = cachedGraphPasses[index];
				if (!pass || !snapshot || snapshot.pass !== pass) return false;
				if (snapshot.enabled !== pass.enabled || snapshot.needsSwap !== rp.needsSwap || snapshot.input !== rp.input || snapshot.output !== rp.output || snapshot.clear !== rp.clear || snapshot.preserve !== rp.preserve) return false;
				const passClearColor = rp.clearColor;
				const hasPassClearColor = passClearColor !== void 0;
				if (snapshot.hasClearColor !== hasPassClearColor) return false;
				if (passClearColor) {
					if (snapshot.clearColor0 !== passClearColor[0] || snapshot.clearColor1 !== passClearColor[1] || snapshot.clearColor2 !== passClearColor[2] || snapshot.clearColor3 !== passClearColor[3]) return false;
				}
			}
			return true;
		};
		/**
		* Updates render-graph cache with current pass set.
		*/
		const updateGraphPlanCache = (passes, clearColor, graphPlan) => {
			cachedGraphPlan = graphPlan;
			cachedGraphRenderTargetSignature = renderTargetSignature;
			cachedGraphClearColor[0] = clearColor[0];
			cachedGraphClearColor[1] = clearColor[1];
			cachedGraphClearColor[2] = clearColor[2];
			cachedGraphClearColor[3] = clearColor[3];
			cachedGraphPasses.length = passes.length;
			let index = 0;
			for (const pass of passes) {
				const rp = pass;
				const passClearColor = rp.clearColor;
				const hasPassClearColor = passClearColor !== void 0;
				const snapshot = cachedGraphPasses[index];
				if (!snapshot) {
					cachedGraphPasses[index] = {
						pass,
						enabled: pass.enabled,
						needsSwap: rp.needsSwap,
						input: rp.input,
						output: rp.output,
						clear: rp.clear,
						preserve: rp.preserve,
						hasClearColor: hasPassClearColor,
						clearColor0: passClearColor?.[0] ?? 0,
						clearColor1: passClearColor?.[1] ?? 0,
						clearColor2: passClearColor?.[2] ?? 0,
						clearColor3: passClearColor?.[3] ?? 0
					};
					index += 1;
					continue;
				}
				snapshot.pass = pass;
				snapshot.enabled = pass.enabled;
				snapshot.needsSwap = rp.needsSwap;
				snapshot.input = rp.input;
				snapshot.output = rp.output;
				snapshot.clear = rp.clear;
				snapshot.preserve = rp.preserve;
				snapshot.hasClearColor = hasPassClearColor;
				snapshot.clearColor0 = passClearColor?.[0] ?? 0;
				snapshot.clearColor1 = passClearColor?.[1] ?? 0;
				snapshot.clearColor2 = passClearColor?.[2] ?? 0;
				snapshot.clearColor3 = passClearColor?.[3] ?? 0;
				index += 1;
			}
		};
		/**
		* Synchronizes pass lifecycle callbacks and resize notifications.
		*/
		const syncPassLifecycle = (passes, width, height) => {
			const resized = passWidth !== width || passHeight !== height;
			if (!resized && lifecyclePassesRef === passes && passes.length === activePasses.length) {
				let isSameOrder = true;
				for (let index = 0; index < passes.length; index += 1) if (activePasses[index] !== passes[index]) {
					isSameOrder = false;
					break;
				}
				if (isSameOrder) return;
			}
			lifecycleNextSet.clear();
			lifecycleUniquePasses.length = 0;
			for (const pass of passes) {
				if (lifecycleNextSet.has(pass)) continue;
				lifecycleNextSet.add(pass);
				lifecycleUniquePasses.push(pass);
			}
			lifecyclePreviousSet.clear();
			for (const pass of activePasses) lifecyclePreviousSet.add(pass);
			for (const pass of activePasses) if (!lifecycleNextSet.has(pass)) pass.dispose?.();
			for (const pass of lifecycleUniquePasses) if (resized || !lifecyclePreviousSet.has(pass)) pass.setSize?.(width, height);
			activePasses.length = 0;
			for (const pass of lifecycleUniquePasses) activePasses.push(pass);
			lifecyclePassesRef = passes;
			passWidth = width;
			passHeight = height;
		};
		const syncPingPongShaderTextureLifecycle = (passes) => {
			const activeFeedbackPasses = /* @__PURE__ */ new Set();
			for (const pass of passes) if (isManagedFeedbackPass(pass)) activeFeedbackPasses.add(pass);
			for (const [pass, pair] of pingPongShaderTexturePairs.entries()) {
				if (activeFeedbackPasses.has(pass)) continue;
				destroyPingPongShaderTexturePair(pair);
				pingPongShaderTexturePairs.delete(pass);
			}
		};
		const syncPingPongComputeTextureLifecycle = (passes) => {
			const activeComputePasses = new Set(passes.filter(isManagedComputePass));
			for (const [pass, pair] of pingPongTexturePairs.entries()) {
				if (activeComputePasses.has(pass)) continue;
				pair.textureA.destroy();
				pair.textureB.destroy();
				pingPongTexturePairs.delete(pass);
			}
		};
		/**
		* Ensures internal ping-pong slot texture matches current canvas size/format.
		*/
		const ensureSlotTarget = (slot, width, height) => {
			const current = slot === "source" ? sourceSlotTarget : targetSlotTarget;
			if (current && current.width === width && current.height === height && current.format === workingFormat) return current;
			destroyRenderTexture(current);
			const next = createRenderTexture(device, width, height, workingFormat);
			if (slot === "source") sourceSlotTarget = next;
			else targetSlotTarget = next;
			return next;
		};
		const ensurePresentationTarget = (width, height) => {
			if (presentationSlotTarget && presentationSlotTarget.width === width && presentationSlotTarget.height === height && presentationSlotTarget.format === workingFormat) return presentationSlotTarget;
			destroyRenderTexture(presentationSlotTarget);
			presentationSlotTarget = createRenderTexture(device, width, height, workingFormat);
			return presentationSlotTarget;
		};
		/**
		* Creates/updates runtime render targets and returns immutable pass snapshot.
		*/
		const syncRenderTargets = (canvasWidth, canvasHeight) => {
			const definitions = resolveRenderTargets();
			const validatedFormats = validateRenderTargetFormats(definitions, workingFormat, device.features);
			const resolvedDefinitions = resolveRenderTargetDefinitions(definitions, canvasWidth, canvasHeight, workingFormat);
			const nextSignature = buildRenderTargetSignature(resolvedDefinitions);
			if (nextSignature !== renderTargetSignature) {
				const activeKeys = /* @__PURE__ */ new Set();
				for (const definition of resolvedDefinitions) activeKeys.add(definition.key);
				for (const [key, target] of runtimeRenderTargets.entries()) if (!activeKeys.has(key)) {
					target.texture.destroy();
					runtimeRenderTargets.delete(key);
				}
				for (const definition of resolvedDefinitions) {
					const current = runtimeRenderTargets.get(definition.key);
					if (current && current.width === definition.width && current.height === definition.height && current.format === definition.format) continue;
					current?.texture.destroy();
					runtimeRenderTargets.set(definition.key, createRenderTexture(device, definition.width, definition.height, definition.format));
				}
				renderTargetSignature = nextSignature;
				const nextSnapshot = {};
				const nextKeys = [];
				for (const definition of resolvedDefinitions) {
					const target = runtimeRenderTargets.get(definition.key);
					if (!target) continue;
					nextKeys.push(definition.key);
					nextSnapshot[definition.key] = {
						texture: target.texture,
						view: target.view,
						width: target.width,
						height: target.height,
						format: target.format
					};
				}
				renderTargetSnapshot = nextSnapshot;
				renderTargetFormatSnapshot = validatedFormats;
				renderTargetKeys = nextKeys;
			}
			return renderTargetSnapshot;
		};
		/**
		* Presents a texture view to the current canvas texture.
		*/
		const presentToCanvas = (commandEncoder, sourceView, canvasView, clearColor, applyFinalTransform) => {
			let bindGroup = presentationBindGroupByView.get(sourceView);
			if (!bindGroup) {
				bindGroup = device.createBindGroup({
					layout: presentationBindGroupLayout,
					entries: [{
						binding: 0,
						resource: presentationSampler
					}, {
						binding: 1,
						resource: sourceView
					}]
				});
				presentationBindGroupByView.set(sourceView, bindGroup);
			}
			const pass = commandEncoder.beginRenderPass({ colorAttachments: [{
				view: canvasView,
				clearValue: toPremultipliedCanvasClearValue(clearColor),
				loadOp: "clear",
				storeOp: "store"
			}] });
			const pipeline = presentationPipelines.get(buildPresentationPipelineKey(effectiveCanvasFormat, effectiveDynamicRange, applyFinalTransform, true));
			if (!pipeline) throw new Error(`Missing presentation pipeline for ${effectiveCanvasFormat}/${effectiveDynamicRange}.`);
			pass.setPipeline(pipeline);
			pass.setBindGroup(0, bindGroup);
			pass.draw(3);
			pass.end();
		};
		const flushStorageWrites = (writes) => {
			for (const write of writes) {
				const resource = resourceRegistry.getStorageBuffer(write.name);
				if (!resource) continue;
				const data = write.data;
				device.queue.writeBuffer(resource.buffer, write.offset, data.buffer, data.byteOffset, data.byteLength);
			}
		};
		/**
		* Executes a full frame render.
		*/
		const render = ({ time, delta, renderMode, uniforms, textures, canvasSize, pendingStorageWrites }) => {
			if (deviceLostMessage) throw new Error(deviceLostMessage);
			const uncapturedMessage = consumeUncapturedErrorMessage();
			if (uncapturedMessage) throw new Error(uncapturedMessage);
			const { width, height } = resizeCanvas(options.canvas, options.getDpr(), canvasSize);
			if (!contextConfigured || configuredWidth !== width || configuredHeight !== height || configuredCanvasFormat !== effectiveCanvasFormat || configuredDynamicRange !== effectiveDynamicRange) {
				try {
					context.configure(buildCanvasConfiguration({
						device,
						format: effectiveCanvasFormat,
						dynamicRange: effectiveDynamicRange,
						canvasColorSpace: colorPipeline.canvasColorSpace
					}));
				} catch (error) {
					if (colorPipeline.dynamicRange !== "auto" || effectiveDynamicRange !== "hdr") {
						if (colorPipeline.dynamicRange === "hdr" && effectiveDynamicRange === "hdr") {
							const detail = error instanceof Error ? error.message : String(error);
							throw new Error(`HDR canvas presentation is not supported: ${detail}`, { cause: error });
						}
						throw error;
					}
					effectiveCanvasFormat = colorPipeline.fallbackCanvasFormat;
					effectiveDynamicRange = "sdr";
					context.configure(buildCanvasConfiguration({
						device,
						format: effectiveCanvasFormat,
						dynamicRange: effectiveDynamicRange,
						canvasColorSpace: colorPipeline.canvasColorSpace
					}));
				}
				contextConfigured = true;
				configuredWidth = width;
				configuredHeight = height;
				configuredCanvasFormat = effectiveCanvasFormat;
				configuredDynamicRange = effectiveDynamicRange;
			}
			writeFrameBuffer(time, delta, width, height);
			packUniformsIntoFast(uniforms, options.uniformLayout, uniformScratch);
			if (!hasUniformSnapshot) {
				device.queue.writeBuffer(uniformBuffer, 0, uniformScratch.buffer, uniformScratch.byteOffset, uniformScratch.byteLength);
				uniformPrevious.set(uniformScratch);
				hasUniformSnapshot = true;
			} else {
				const dirtyRanges = findDirtyFloatRanges(uniformPrevious, uniformScratch);
				for (const range of dirtyRanges) {
					const byteOffset = range.start * 4;
					const byteLength = range.count * 4;
					device.queue.writeBuffer(uniformBuffer, byteOffset, uniformScratch.buffer, uniformScratch.byteOffset + byteOffset, byteLength);
				}
				if (dirtyRanges.length > 0) uniformPrevious.set(uniformScratch);
			}
			const passes = resolvePasses();
			const activePingPongShaderTargets = /* @__PURE__ */ new Set();
			for (const pass of passes) {
				if (pass.enabled === false) continue;
				if (isManagedFeedbackPass(pass)) {
					const target = pass.getTarget();
					if (target) activePingPongShaderTargets.add(target);
				}
			}
			const commandEncoder = device.createCommandEncoder();
			let bindGroupDirty = false;
			for (const binding of textureBindings) {
				if (normalizedTextureDefinitions[binding.key]?.storage) continue;
				if (activePingPongShaderTargets.has(binding.key)) continue;
				const nextTexture = textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null;
				if (updateTextureBinding(binding, nextTexture, renderMode) && binding.fragmentVisible) bindGroupDirty = true;
			}
			if (bindGroupDirty) {
				bindGroup = createBindGroup();
				bindGroupDirty = false;
			}
			if (pendingStorageWrites) flushStorageWrites(pendingStorageWrites);
			generateDirtyTextureMipmaps(commandEncoder);
			const clearColor = options.getClearColor();
			syncPassLifecycle(passes, width, height);
			syncPingPongComputeTextureLifecycle(passes);
			syncPingPongShaderTextureLifecycle(passes);
			const runtimeTargets = syncRenderTargets(width, height);
			const resolvedComputeResourcesByPass = /* @__PURE__ */ new Map();
			const computeLabelsByPass = /* @__PURE__ */ new Map();
			const computeExternalState = createComputeExternalResolutionState();
			let computeDeclarationIndex = 0;
			for (const pass of passes) {
				if (pass.enabled === false) continue;
				if (!isManagedComputePass(pass)) continue;
				const passLabel = `Compute pass #${computeDeclarationIndex}`;
				computeDeclarationIndex += 1;
				const resources = resolveComputePassResources(pass.getResources(), {
					passLabel,
					deviceFeatures: device.features,
					limits: computeResourceLimits,
					externalContext: {
						device,
						width,
						height,
						time,
						delta
					},
					getMaterialTexture: (logicalId) => resourceRegistry.getTexture(logicalId),
					getMaterialStorageBuffer: (logicalId) => resourceRegistry.getStorageBuffer(logicalId),
					getMaterialSampler: (logicalId) => {
						const binding = textureBindingByKey.get(logicalId);
						return binding ? {
							logicalId,
							sampler: binding.sampler,
							type: binding.samplerType,
							sampleType: binding.resource.sampleType
						} : void 0;
					},
					createTextureView: createCachedExternalTextureView,
					pingPong: pass.isPingPong === true,
					externalState: computeExternalState,
					diagnosticContext: runtimeContext
				});
				resolvedComputeResourcesByPass.set(pass, resources);
				computeLabelsByPass.set(pass, passLabel);
			}
			const graphPlan = isGraphPlanCacheValid(passes, clearColor) ? (() => {
				const cached = cachedGraphPlan;
				for (const step of cached.computeSteps) {
					const resources = resolvedComputeResourcesByPass.get(step.pass);
					if (!resources) throw new Error("Cached compute graph step is missing resolved resources.");
					step.resolvedResources = resources;
				}
				return cached;
			})() : (() => {
				let nextPlan;
				try {
					nextPlan = planRenderGraph(passes, clearColor, renderTargetKeys, {
						getResolvedResources: (pass) => resolvedComputeResourcesByPass.get(pass),
						getPassLabel: (pass) => computeLabelsByPass.get(pass) ?? "Compute pass"
					});
				} catch (error) {
					throw attachMotionGPUErrorContext(error, runtimeContext);
				}
				updateGraphPlanCache(passes, clearColor, nextPlan);
				return nextPlan;
			})();
			validateBuiltInRenderPassFormats({
				passes,
				workingFormat,
				namedFormats: renderTargetFormatSnapshot,
				deviceFeatures: device.features
			});
			if (graphPlan.renderSteps.length > 0) validatePresentationSourceFormat({
				slot: graphPlan.finalOutput,
				workingFormat,
				namedFormats: renderTargetFormatSnapshot,
				deviceFeatures: device.features,
				requiresFilterableInput: presentationSamplingLayout.samplerType === "filtering"
			});
			const canvasTexture = context.getCurrentTexture();
			canvasSurface.texture = canvasTexture;
			canvasSurface.view = canvasTexture.createView();
			canvasSurface.width = width;
			canvasSurface.height = height;
			canvasSurface.format = effectiveCanvasFormat;
			const presentationRequired = colorPipeline.requiresPresentationPass;
			const graphHasRenderSteps = graphPlan.renderSteps.length > 0;
			const presentationSurface = presentationRequired || graphHasRenderSteps ? ensurePresentationTarget(width, height) : null;
			if (graphHasRenderSteps) {
				frameSlots.source = ensureSlotTarget("source", width, height);
				frameSlots.target = ensureSlotTarget("target", width, height);
				frameSlots.canvas = presentationSurface;
				frameSlotsActive = true;
			} else frameSlotsActive = false;
			const slots = frameSlotsActive ? frameSlots : null;
			const sceneOutput = slots ? slots.source : presentationSurface ?? canvasSurface;
			let activeFrameBufferWidth = width;
			let activeFrameBufferHeight = height;
			const ensureFrameBufferResolution = (nextWidth, nextHeight) => {
				if (activeFrameBufferWidth === nextWidth && activeFrameBufferHeight === nextHeight) return;
				writeFrameBuffer(time, delta, nextWidth, nextHeight);
				activeFrameBufferWidth = nextWidth;
				activeFrameBufferHeight = nextHeight;
			};
			const clearFeedbackView = (view, clearColor) => {
				commandEncoder.beginRenderPass({ colorAttachments: [{
					view,
					clearValue: toClearValue(clearColor),
					loadOp: "clear",
					storeOp: "store"
				}] }).end();
			};
			let computeStepIndex = 0;
			let feedbackStepIndex = 0;
			for (const step of graphPlan.preSceneSteps) {
				if (step.kind === "compute") {
					ensureFrameBufferResolution(width, height);
					const computeStepLabel = step.computeLabel ?? `Compute pass #${computeStepIndex}`;
					computeStepIndex += 1;
					if (!isManagedComputePass(step.pass)) throw new Error(`${computeStepLabel} has an invalid managed pass contract.`);
					const computePass = step.pass;
					const computeSource = computePass.getCompute();
					const resources = step.resolvedResources;
					if (!resources) throw new Error(`${computeStepLabel} is missing resolved resources.`);
					const pingPongRead = resources.entries.find((entry) => entry.kind === "sampled-texture" && entry.pingPong === "read");
					const pingPongWrite = resources.entries.find((entry) => entry.kind === "storage-texture" && entry.pingPong === "write");
					let pingPongPair = null;
					if (computePass.isPingPong) {
						if (!pingPongRead || !pingPongWrite || pingPongRead.source !== "material" || pingPongWrite.source !== "material" || typeof pingPongRead.logicalId !== "string" || !Object.is(pingPongRead.logicalId, pingPongWrite.logicalId)) throw createMotionGPUError("PINGPONG_CONFIGURATION_INVALID", `${computeStepLabel} ping-pong pair must reference one renderer-managed material texture.`);
						pingPongPair = ensurePingPongTexturePair(computePass, pingPongRead.logicalId);
					}
					const workgroupSize = computePass.getWorkgroupSize();
					const pipelineEntry = buildComputePipelineEntry({
						computeSource,
						workgroupSize,
						resources
					});
					const resourceBindGroup = pingPongPair ? null : getComputeResourceBindGroup(pipelineEntry, computePass, resources);
					const iterations = computePass.isPingPong ? computePass.getIterations?.() ?? 1 : 1;
					if (!Number.isInteger(iterations) || iterations < 1) throw new Error(`${computeStepLabel} iterations must be a positive integer >= 1, got ${iterations}.`);
					for (let iter = 0; iter < iterations; iter += 1) {
						const dispatchLabel = iterations > 1 ? `${computeStepLabel} iteration ${iter + 1}` : computeStepLabel;
						const dispatch = validateComputeDispatch(computePass.resolveDispatch({
							width,
							height,
							time,
							delta,
							workgroupSize
						}), maxComputeWorkgroupsPerDimension, dispatchLabel);
						const cPass = commandEncoder.beginComputePass();
						cPass.setPipeline(pipelineEntry.pipeline);
						cPass.setBindGroup(0, pipelineEntry.uniformBindGroup);
						if (pingPongPair) cPass.setBindGroup(1, getPingPongResourceBindGroup(pipelineEntry, computePass, resources, pingPongPair, pingPongPair.readFromA));
						else if (resourceBindGroup) cPass.setBindGroup(1, resourceBindGroup);
						cPass.dispatchWorkgroups(dispatch[0], dispatch[1], dispatch[2]);
						cPass.end();
						if (pingPongPair) pingPongPair.readFromA = !pingPongPair.readFromA;
					}
					if (pingPongPair) {
						const latestView = pingPongPair.readFromA ? pingPongPair.viewA : pingPongPair.viewB;
						if (resourceRegistry.markTextureWritten(pingPongPair.logicalId, latestView)) {
							if (textureBindingByKey.get(pingPongPair.logicalId)?.fragmentVisible) bindGroupDirty = true;
						}
					} else {
						const written = /* @__PURE__ */ new Set();
						for (const entry of resources.entries) {
							if (entry.source !== "material" || written.has(String(entry.logicalId))) continue;
							if (entry.kind === "storage-texture") {
								written.add(String(entry.logicalId));
								resourceRegistry.markTextureWritten(String(entry.logicalId));
							} else if (entry.kind === "storage-buffer" && entry.access === "storage-read-write") {
								written.add(String(entry.logicalId));
								resourceRegistry.markStorageBufferWritten(String(entry.logicalId));
							}
						}
					}
					continue;
				}
				if (step.kind !== "feedback") continue;
				const feedbackStepLabel = `PingPongShaderPass #${feedbackStepIndex}`;
				feedbackStepIndex += 1;
				if (!isManagedFeedbackPass(step.pass)) throw new Error(`${feedbackStepLabel} has an invalid managed pass contract.`);
				const feedbackPass = step.pass;
				const target = feedbackPass.getTarget();
				if (!target) throw new Error("PingPongShaderPass must provide a target texture key.");
				const targetBinding = textureBindingByKey.get(target);
				if (!targetBinding) throw new Error(`PingPongShaderPass target "${target}" must reference a declared material texture.`);
				if (!targetBinding.fragmentVisible) throw new Error(`PingPongShaderPass target "${target}" must be visible to the fragment shader.`);
				if (normalizedTextureDefinitions[target]?.storage) throw new Error(`PingPongShaderPass target "${target}" must be declared as a sampled texture, not storage:true. Use PingPongComputePass for storage textures.`);
				const size = feedbackPass.resolveSize({
					width,
					height
				});
				const pair = ensurePingPongShaderTexturePair(feedbackPass, {
					target,
					width: size.width,
					height: size.height,
					format: feedbackPass.getFormat(),
					filter: feedbackPass.getFilter(),
					addressModeU: feedbackPass.getAddressModeU(),
					addressModeV: feedbackPass.getAddressModeV()
				});
				const pipelineEntry = buildPingPongShaderPipelineEntry(feedbackPass, pair.format, target);
				const feedbackBindGroup = createPingPongShaderBindGroup(pipelineEntry);
				const initializationColor = feedbackPass.consumeResetColor() ?? (pair.needsClear ? feedbackPass.getClearColor() : null);
				if (initializationColor) {
					clearFeedbackView(pair.viewA, initializationColor);
					clearFeedbackView(pair.viewB, initializationColor);
					pair.needsClear = false;
				}
				const iterations = feedbackPass.getIterations();
				if (!Number.isInteger(iterations) || iterations < 1) throw new Error(`${feedbackStepLabel} iterations must be a positive integer >= 1, got ${iterations}.`);
				ensureFrameBufferResolution(pair.width, pair.height);
				const readFromAAtIterationZero = feedbackPass.getCurrentOutput() !== `${pair.target}B`;
				for (let iter = 0; iter < iterations; iter += 1) {
					const readFromA = iter % 2 === 0 ? readFromAAtIterationZero : !readFromAAtIterationZero;
					const outputView = readFromA ? pair.viewB : pair.viewA;
					const previousBindGroup = getPingPongShaderPreviousBindGroup(pair, pipelineEntry.previousBindGroupLayout, readFromA);
					const pass = commandEncoder.beginRenderPass({ colorAttachments: [{
						view: outputView,
						clearValue: {
							r: 0,
							g: 0,
							b: 0,
							a: 0
						},
						loadOp: "load",
						storeOp: "store"
					}] });
					pass.setPipeline(pipelineEntry.pipeline);
					pass.setBindGroup(0, feedbackBindGroup);
					pass.setBindGroup(1, previousBindGroup);
					pass.draw(3);
					pass.end();
				}
				feedbackPass.advanceFrame();
				const latestView = feedbackPass.getCurrentOutput() === `${pair.target}B` ? pair.viewB : pair.viewA;
				if (attachFeedbackTextureBinding(targetBinding, latestView)) bindGroup = createBindGroup();
			}
			if (bindGroupDirty) bindGroup = createBindGroup();
			ensureFrameBufferResolution(width, height);
			const scenePass = commandEncoder.beginRenderPass({ colorAttachments: [{
				view: sceneOutput.view,
				clearValue: sceneOutput === canvasSurface ? toPremultipliedCanvasClearValue(clearColor) : toClearValue(clearColor),
				loadOp: "clear",
				storeOp: "store"
			}] });
			scenePass.setPipeline(!slots && !presentationRequired && directCanvasPipeline ? directCanvasPipeline : pipeline);
			scenePass.setBindGroup(0, bindGroup);
			if (fragmentStorageBindGroup) scenePass.setBindGroup(1, fragmentStorageBindGroup);
			scenePass.draw(3);
			scenePass.end();
			let finalPresentationSurface = sceneOutput;
			if (slots) {
				const resolveStepSurface = (slot) => {
					if (slot === "source") return slots.source;
					if (slot === "target") return slots.target;
					if (slot === "canvas") return slots.canvas;
					const named = runtimeTargets[slot];
					if (!named) throw new Error(`Render graph references unknown runtime target "${slot}".`);
					return named;
				};
				for (const step of graphPlan.renderSteps) {
					const input = resolveStepSurface(step.input);
					const output = resolveStepSurface(step.output);
					step.pass.render({
						device,
						commandEncoder,
						source: slots.source,
						target: slots.target,
						canvas: slots.canvas,
						input,
						output,
						targets: runtimeTargets,
						time,
						delta,
						width,
						height,
						clear: step.clear,
						clearColor: step.clearColor,
						preserve: step.preserve,
						beginRenderPass: (passOptions) => {
							const clear = passOptions?.clear ?? step.clear;
							const clearColor = passOptions?.clearColor ?? step.clearColor;
							const preserve = passOptions?.preserve ?? step.preserve;
							return commandEncoder.beginRenderPass({ colorAttachments: [{
								view: passOptions?.view ?? output.view,
								clearValue: toClearValue(clearColor),
								loadOp: clear ? "clear" : "load",
								storeOp: preserve ? "store" : "discard"
							}] });
						}
					});
					if (step.needsSwap) {
						const previousSource = slots.source;
						slots.source = slots.target;
						slots.target = previousSource;
					}
				}
				finalPresentationSurface = resolveStepSurface(graphPlan.finalOutput);
				if (!presentationRequired) presentToCanvas(commandEncoder, finalPresentationSurface.view, canvasSurface.view, clearColor, false);
			}
			if (presentationRequired) presentToCanvas(commandEncoder, finalPresentationSurface.view, canvasSurface.view, clearColor, true);
			device.queue.submit([commandEncoder.finish()]);
		};
		acceptInitializationCleanups = false;
		initializationCleanups.length = 0;
		return {
			render,
			flushStorageWrites,
			getStorageBuffer: (name) => {
				return resourceRegistry.getStorageBuffer(name)?.buffer;
			},
			getDevice: () => {
				return device;
			},
			destroy: () => {
				if (isDestroyed) return;
				isDestroyed = true;
				device.removeEventListener("uncapturederror", handleUncapturedError);
				frameBuffer.destroy();
				uniformBuffer.destroy();
				for (const key of storageBufferKeys) resourceRegistry.getStorageBuffer(key)?.buffer.destroy();
				for (const pair of pingPongTexturePairs.values()) {
					pair.textureA.destroy();
					pair.textureB.destroy();
				}
				pingPongTexturePairs.clear();
				for (const pair of pingPongShaderTexturePairs.values()) destroyPingPongShaderTexturePair(pair);
				pingPongShaderTexturePairs.clear();
				computePipelineCache.clear();
				externalTextureViewCache = /* @__PURE__ */ new WeakMap();
				pingPongShaderPipelineCache.clear();
				destroyRenderTexture(sourceSlotTarget);
				destroyRenderTexture(targetSlotTarget);
				destroyRenderTexture(presentationSlotTarget);
				for (const target of runtimeRenderTargets.values()) target.texture.destroy();
				runtimeRenderTargets.clear();
				for (const pass of activePasses) pass.dispose?.();
				activePasses.length = 0;
				lifecyclePassesRef = null;
				for (const binding of textureBindings) binding.resource.ownedTexture?.destroy();
				sampledFallbackPool.destroy();
				resourceRegistry.clear();
				presentationBindGroupByView = /* @__PURE__ */ new WeakMap();
				cachedGraphPlan = null;
				cachedGraphPasses.length = 0;
				renderTargetSnapshot = {};
				renderTargetKeys = [];
				destroyDevice();
			}
		};
	} catch (error) {
		isDestroyed = true;
		acceptInitializationCleanups = false;
		device.removeEventListener("uncapturederror", handleUncapturedError);
		runInitializationCleanups();
		destroyDevice();
		throw error;
	}
}
//#endregion
export { createRenderer, findDirtyFloatRanges };

//# sourceMappingURL=renderer.js.map