UNPKG

typegpu

Version:

A thin layer between JS and WebGPU/WGSL that improves development experience and allows for faster iteration.

296 lines (295 loc) 12.4 kB
import { snip } from "../../data/snippet.js"; import { Void } from "../../data/wgslTypes.js"; import { resolve } from "../../resolutionCtx.js"; import { getName, PERF, setName } from "../../shared/meta.js"; import { $getNameForward, $internal, $resolve, $soul } from "../../shared/symbols.js"; import { isBindGroup, isBindGroupLayout, } from "../../tgpuBindGroupLayout.js"; import { INTERNAL_adoptCommandEncoder, INTERNAL_createCommandEncoder, } from "../commandEncoder/commandEncoder.js"; import { INTERNAL_adoptComputePass } from "../commandEncoder/computePass.js"; import { emitComputeDispatch, finalizeOwnEncoder } from "./drawState.js"; import { isGPUCommandEncoder, isGPUComputePassEncoder, isTgpuCommandEncoder, isTgpuComputePass, } from "./typeGuards.js"; import { isGPUBuffer } from "../../types.js"; import { wgslEnableExtensions, wgslEnableExtensionToFeatureName } from "../../wgslExtensions.js"; import { namespace } from "../resolve/namespace.js"; import { warnIfOverflow } from "./webgpuLimitations.js"; import { collectBindGroupPairs, DISPATCH_INDIRECT_SIZE, resolveIndirectOffset, restoreTimestampPriors, } from "./pipelineUtils.js"; import { invariant } from "../../errors.js"; import { createWithPerformanceCallback, createWithTimestampWrites, } from "./timeable.js"; import { nonTransferablePriorsOf } from "./priors.js"; import { NullPerformanceTracker, PerformanceTrackerImpl, } from "./performanceTracker.js"; import { logger } from "../../tgpuLogger.js"; export function INTERNAL_createComputePipeline(root, slotBindings, descriptor) { return new TgpuComputePipelineImpl(new ComputePipelineCore(root, slotBindings, descriptor), {}); } export function INTERNAL_restoreComputePipeline(soul, ctx) { invariant(soul.raw, 'A compute pipeline soul is only complete once materialized.'); const root = ctx.getRoot(soul.device); const core = ComputePipelineCore.precompiled(root, { pipeline: soul.raw, usedBindGroupLayouts: soul.usedBindGroupLayouts ?? [], // The catchall group is already one of `bindGroups`, keyed by the layout it was resolved with catchall: undefined, logResources: undefined, }); const pipeline = new TgpuComputePipelineImpl(core, { bindGroupLayoutMap: new Map(soul.bindGroups), }); return restoreTimestampPriors(pipeline, soul); } class TgpuComputePipelineImpl { [$internal]; [$soul]; resourceType = 'compute-pipeline'; [$getNameForward]; constructor(core, priors) { this[$soul] = { type: 'compute-pipeline', device: core.root.device, raw: undefined, label: undefined, }; this[$internal] = { core, priors, root: core.root, materialize: () => { const soul = this[$soul]; if (!soul.raw) { const memo = core.unwrap(); soul.raw = memo.pipeline; soul.usedBindGroupLayouts = memo.usedBindGroupLayouts; soul.bindGroups = collectBindGroupPairs(memo.usedBindGroupLayouts, memo.catchall, priors.bindGroupLayoutMap); soul.timestampWrites = priors.timestampWrites; soul.performanceCallback = priors.performanceCallback; soul.nonTransferablePriors = nonTransferablePriorsOf(priors); } return soul.raw; }, }; this[$getNameForward] = core; } [$resolve](ctx) { return ctx.resolve(this[$internal].core); } toString() { return `computePipeline:${getName(this) ?? '<unnamed>'}`; } #withPriors(patch) { const { core, priors } = this[$internal]; return new TgpuComputePipelineImpl(core, { ...priors, ...patch }); } with(first, bindGroup) { const internals = this[$internal]; if (isTgpuComputePass(first)) { return this.#withPriors({ pass: first, encoder: undefined }); } if (isTgpuCommandEncoder(first)) { return this.#withPriors({ pass: undefined, encoder: first }); } if (isGPUComputePassEncoder(first)) { return this.#withPriors({ pass: INTERNAL_adoptComputePass(internals.root, first), encoder: undefined, }); } if (isGPUCommandEncoder(first)) { return this.#withPriors({ pass: undefined, encoder: INTERNAL_adoptCommandEncoder(internals.root, first), }); } if (isBindGroup(first) || isBindGroupLayout(first)) { const [layout, group] = isBindGroup(first) ? [first.layout, first] : [first, bindGroup]; return this.#withPriors({ bindGroupLayoutMap: new Map([ ...(internals.priors.bindGroupLayoutMap ?? []), [layout, group], ]), }); } throw new Error('Unsupported value passed into .with()'); } withPerformanceCallback(callback) { const internals = this[$internal]; if (internals.priors.timestampWrites) { return this.#withPriors({ performanceCallback: callback }); } const querySet = internals.core.performanceCallbackQuerySet; if (!querySet) { logger.warn('webgpu-feature-missing', 'Performance callback cannot be used because the timestamp-query feature is not enabled on the root.'); return this; } return this.#withPriors(createWithPerformanceCallback(internals.priors, callback, querySet)); } withTimestampWrites(options) { const internals = this[$internal]; return this.#withPriors(createWithTimestampWrites(internals.priors, options, internals.root)); } dispatchWorkgroups(x, y, z) { this.#execute((pass) => pass.dispatchWorkgroups(x, y, z)); } dispatchWorkgroupsIndirect(indirectBuffer, start) { const rawBuffer = isGPUBuffer(indirectBuffer) ? indirectBuffer : indirectBuffer.buffer; const offset = resolveIndirectOffset(indirectBuffer, start, DISPATCH_INDIRECT_SIZE, 'dispatchWorkgroupsIndirect'); this.#execute((pass) => pass.dispatchWorkgroupsIndirect(rawBuffer, offset)); } initAsync() { return this[$internal].core.initAsync(); } initSync() { this[$internal].core.initSync(); } #execute(dispatch) { const { core, priors, root } = this[$internal]; if (priors.pass) { emitComputeDispatch(root, priors.pass[$internal], this, dispatch); return; } const encoder = priors.encoder ?? INTERNAL_createCommandEncoder(root); const pass = encoder.beginComputePass({ label: getName(core) ?? '<unnamed>', timestampWrites: priors.timestampWrites, }); emitComputeDispatch(root, pass[$internal], this, dispatch, /* ownsPass */ true); pass.end(); finalizeOwnEncoder(encoder, core, core.unwrap().logResources, priors); } $name(label) { setName(this, label); return this; } } class ComputePipelineCore { [$internal] = true; root; #performanceTracker; #initAsyncPromise; #memo; #slotBindings; #descriptor; #performanceCallbackQuerySet; constructor(root, slotBindings, descriptor) { this.root = root; this.#slotBindings = slotBindings; this.#descriptor = descriptor; this.#performanceTracker = PERF?.enabled ? new PerformanceTrackerImpl() : new NullPerformanceTracker(); } static precompiled(root, memo) { const core = new ComputePipelineCore(root, [], undefined); core.#memo = memo; return core; } [$resolve](ctx) { const descriptor = this.#descriptor; if (!descriptor) { // Precompiled pipelines have nothing to contribute to the shader return snip('', Void, /* origin */ 'runtime'); } return ctx.withSlots(this.#slotBindings, () => { ctx.resolve(descriptor.compute); return snip('', Void, /* origin */ 'runtime'); }); } toString() { return 'computePipelineCore'; } get performanceCallbackQuerySet() { if (!this.root.enabledFeatures.has('timestamp-query')) { return undefined; } return (this.#performanceCallbackQuerySet ??= this.root.createQuerySet('timestamp', 2)); } /** * @privateRemarks * This function cannot be a regular async function * because when called multiple times before the promise finishes, * we want it to return the same promise each time. */ initAsync() { if (this.#memo !== undefined) { // the pipeline was already resolved & compiled return Promise.resolve(); } if (this.#initAsyncPromise === undefined) { // the pipeline did not start resolution & compilation const device = this.root.device; const { resolutionResult, module } = this.resolveAndCreateShaderModule(); const { usedBindGroupLayouts, catchall, logResources } = resolutionResult; this.#initAsyncPromise = device .createComputePipelineAsync({ label: getName(this) ?? '<unnamed>', layout: device.createPipelineLayout({ label: `${getName(this) ?? '<unnamed>'} - Pipeline Layout`, bindGroupLayouts: usedBindGroupLayouts.map((l) => this.root.unwrap(l)), }), compute: { module }, }) .then((pipeline) => { this.#memo = { pipeline, usedBindGroupLayouts, catchall, logResources }; this.#performanceTracker.measureCompile(device); }) .finally(() => { this.#initAsyncPromise = undefined; }); } return this.#initAsyncPromise; } initSync() { if (this.#memo !== undefined) { return; } if (this.#initAsyncPromise !== undefined) { throw new Error("'pipeline.initAsync()' was called and is not yet resolved."); } const device = this.root.device; const { resolutionResult, module } = this.resolveAndCreateShaderModule(); const { usedBindGroupLayouts, catchall, logResources } = resolutionResult; this.#memo = { pipeline: device.createComputePipeline({ label: getName(this) ?? '<unnamed>', layout: device.createPipelineLayout({ label: `${getName(this) ?? '<unnamed>'} - Pipeline Layout`, bindGroupLayouts: usedBindGroupLayouts.map((l) => this.root.unwrap(l)), }), compute: { module }, }), usedBindGroupLayouts, catchall, logResources, }; this.#performanceTracker.measureCompile(device); } unwrap() { this.initSync(); return this.#memo; } resolveAndCreateShaderModule() { const device = this.root.device; const enableExtensions = wgslEnableExtensions.filter((extension) => this.root.enabledFeatures.has(wgslEnableExtensionToFeatureName[extension])); // Resolving code const ns = namespace({ names: this.root.nameRegistrySetting }); const resolutionResult = this.#performanceTracker.measureResolve(() => resolve(this, { namespace: ns, minify: this.root.minify, enableExtensions, shaderGenerator: this.root.shaderGeneratorClass ? new this.root.shaderGeneratorClass() : undefined, root: this.root, })); const { code, usedBindGroupLayouts, catchall } = resolutionResult; if (catchall !== undefined) { usedBindGroupLayouts[catchall[0]]?.$name(`${getName(this) ?? '<unnamed>'} - Automatic Bind Group & Layout`); } warnIfOverflow(usedBindGroupLayouts, device.limits); const module = device.createShaderModule({ label: `${getName(this) ?? '<unnamed>'} - Shader`, code, }); return { resolutionResult, module }; } }