From 43b472402bbfd03f22d8b64efdbf187f2032fda9 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:06:16 +0200 Subject: [PATCH 01/17] Implement initAsync --- .../typegpu-testing-utility/src/extendedIt.ts | 1 + .../src/core/pipeline/computePipeline.ts | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/packages/typegpu-testing-utility/src/extendedIt.ts b/packages/typegpu-testing-utility/src/extendedIt.ts index 00ba17e02c..ea3b4a774b 100644 --- a/packages/typegpu-testing-utility/src/extendedIt.ts +++ b/packages/typegpu-testing-utility/src/extendedIt.ts @@ -114,6 +114,7 @@ export const it = base return commandEncoder; }), createComputePipeline: vi.fn(() => mockComputePipeline), + createComputePipelineAsync: vi.fn(async () => mockComputePipeline), createPipelineLayout: vi.fn(() => 'mockPipelineLayout'), createQuerySet: vi.fn(({ type, count }: GPUQuerySetDescriptor) => { const querySet = Object.create(mockQuerySet); diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index f5e8dd7590..a048207851 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -67,6 +67,8 @@ export interface TgpuComputePipeline extends TgpuNamable, SelfResolvable, Timeab dispatchWorkgroups(x: number, y?: number, z?: number): void; + initAsync(): Promise; + /** * Dispatches compute workgroups using parameters read from a buffer. * The buffer must contain 3 consecutive u32 values (x, y, z workgroup counts). @@ -249,6 +251,10 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { this._executeComputePass((pass) => pass.dispatchWorkgroupsIndirect(rawBuffer, offset)); } + initAsync(): Promise { + return this.#core.initAsync(); + } + private _applyComputeState(pass: GPUComputePassEncoder): void { const memo = this.#core.unwrap(); const { root } = this.#core; @@ -318,6 +324,7 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { class ComputePipelineCore implements SelfResolvable { readonly [$internal] = true; readonly root: ExperimentalTgpuRoot; + private _initAsyncPromise: Promise | undefined; private _memo: Memo | undefined; #slotBindings: [TgpuSlot, unknown][]; @@ -352,7 +359,105 @@ class ComputePipelineCore implements SelfResolvable { return (this.#performanceCallbackQuerySet ??= this.root.createQuerySet('timestamp', 2)); } + initAsync(): Promise { + if (this._memo !== undefined) { + // the pipeline was already resolved & compiled + console.log('memo was present'); + return Promise.resolve(); + } + console.log('memo was not present'); + + if (!this._initAsyncPromise) { + // the pipeline did not start resolution & compilation + + const device = this.root.device; + const enableExtensions = wgslEnableExtensions.filter((extension) => + this.root.enabledFeatures.has(wgslEnableExtensionToFeatureName[extension]), + ); + + // Resolving code + let resolutionResult: ResolutionResult; + + let resolveMeasure: PerformanceMeasure | undefined; + const ns = namespace({ names: this.root.nameRegistrySetting }); + if (PERF?.enabled) { + const resolveStart = performance.mark('typegpu:resolution:start'); + resolutionResult = resolve(this, { + namespace: ns, + enableExtensions, + shaderGenerator: this.root.shaderGenerator, + root: this.root, + }); + resolveMeasure = performance.measure('typegpu:resolution', { + start: resolveStart.name, + }); + } else { + resolutionResult = resolve(this, { + namespace: ns, + enableExtensions, + shaderGenerator: this.root.shaderGenerator, + root: this.root, + }); + } + + const { code, usedBindGroupLayouts, catchall, logResources } = resolutionResult; + + if (catchall !== undefined) { + usedBindGroupLayouts[catchall[0]]?.$name( + `${getName(this) ?? ''} - Automatic Bind Group & Layout`, + ); + } + + const module = device.createShaderModule({ + label: `${getName(this) ?? ''} - Shader`, + code, + }); + + this._initAsyncPromise = device + .createComputePipelineAsync({ + label: getName(this) ?? '', + layout: device.createPipelineLayout({ + label: `${getName(this) ?? ''} - Pipeline Layout`, + bindGroupLayouts: usedBindGroupLayouts.map((l) => this.root.unwrap(l)), + }), + compute: { module }, + }) + .then((pipeline) => { + // resolve + this._memo = { + pipeline, + usedBindGroupLayouts, + catchall, + logResources, + }; + + if (PERF?.enabled) { + void (async () => { + const start = performance.mark('typegpu:compile-start'); + await device.queue.onSubmittedWorkDone(); + const compileMeasure = performance.measure('typegpu:compiled', { + start: start.name, + }); + + PERF?.record('resolution', { + resolveDuration: resolveMeasure?.duration, + compileDuration: compileMeasure.duration, + wgslSize: code.length, + }); + })(); + } + + this._initAsyncPromise = undefined; + }); + } + return this._initAsyncPromise; + } + public unwrap(): Memo { + if (this._initAsyncPromise) { + throw new Error("'pipeline.initAsync()' was called and is not yet resolved."); + } + if (this._memo === undefined) { const device = this.root.device; const enableExtensions = wgslEnableExtensions.filter((extension) => From e5168a66414377695ebe956161617eddc35aa8e3 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:06:21 +0200 Subject: [PATCH 02/17] Add tests --- .../typegpu/tests/pipelineInitAsync.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/typegpu/tests/pipelineInitAsync.test.ts diff --git a/packages/typegpu/tests/pipelineInitAsync.test.ts b/packages/typegpu/tests/pipelineInitAsync.test.ts new file mode 100644 index 0000000000..09666b201e --- /dev/null +++ b/packages/typegpu/tests/pipelineInitAsync.test.ts @@ -0,0 +1,51 @@ +import { describe, expect } from 'vitest'; +import tgpu from '../src/index.js'; +import { it } from 'typegpu-testing-utility'; + +describe('initAsync', () => { + const computeFn = tgpu.computeFn({ workgroupSize: [1, 1, 1] })(() => {}); + + describe('compute pipeline', () => { + it('resolves and creates a pipeline', async ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + await pipeline.initAsync(); + + expect(device.mock.createComputePipelineAsync).toHaveBeenCalled(); + expect(() => root.unwrap(pipeline)).not.toThrow(); // this means that memo already exists + expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` + "@compute @workgroup_size(1, 1, 1) fn computeFn() { + + }" + `); + }); + + it('throws when attempting to unwrap when not resolved', async ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + pipeline.initAsync(); + + expect(() => root.unwrap(pipeline)).toThrowErrorMatchingInlineSnapshot( + `[Error: 'pipeline.initAsync()' was called and is not yet resolved.]`, + ); + }); + + it('returns the promise again if not resolved', async ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + const p1 = pipeline.initAsync(); + const p2 = pipeline.initAsync(); + + expect(p1).toBe(p2); + }); + + it('returns a resolved promise if pipeline was already created', async ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + root.unwrap(pipeline); // resolves & compiles the pipeline + await pipeline.initAsync(); // should not enqueue device operations + + expect(device.mock.createComputePipelineAsync).not.toHaveBeenCalled(); + }); + }); +}); From 11607d8272296970e0adcde4f7e50576a6c010bc Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:19:15 +0200 Subject: [PATCH 03/17] Abstract PerformanceCollector from unwrap --- .../src/core/pipeline/computePipeline.ts | 97 +++++++++++-------- .../typegpu/tests/pipelineInitAsync.test.ts | 2 + 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index a048207851..f05809a323 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -326,6 +326,7 @@ class ComputePipelineCore implements SelfResolvable { readonly root: ExperimentalTgpuRoot; private _initAsyncPromise: Promise | undefined; private _memo: Memo | undefined; + private performanceCollector: PerformanceCollector; #slotBindings: [TgpuSlot, unknown][]; #descriptor: TgpuComputePipeline.Descriptor; @@ -339,6 +340,9 @@ class ComputePipelineCore implements SelfResolvable { this.root = root; this.#slotBindings = slotBindings; this.#descriptor = descriptor; + this.performanceCollector = PERF?.enabled + ? new PerformanceCollectorImpl() + : new PerformanceCollectorNullImpl(); } [$resolve](ctx: ResolutionCtx) { @@ -465,31 +469,16 @@ class ComputePipelineCore implements SelfResolvable { ); // Resolving code - let resolutionResult: ResolutionResult; - - let resolveMeasure: PerformanceMeasure | undefined; const ns = namespace({ names: this.root.nameRegistrySetting }); - if (PERF?.enabled) { - const resolveStart = performance.mark('typegpu:resolution:start'); - resolutionResult = resolve(this, { - namespace: ns, - enableExtensions, - shaderGenerator: this.root.shaderGenerator, - root: this.root, - }); - resolveMeasure = performance.measure('typegpu:resolution', { - start: resolveStart.name, - }); - } else { - resolutionResult = resolve(this, { - namespace: ns, - enableExtensions, - shaderGenerator: this.root.shaderGenerator, - root: this.root, - }); - } - - const { code, usedBindGroupLayouts, catchall, logResources } = resolutionResult; + const { code, usedBindGroupLayouts, catchall, logResources } = + this.performanceCollector.measureResolve(() => + resolve(this, { + namespace: ns, + enableExtensions, + shaderGenerator: this.root.shaderGenerator, + root: this.root, + }), + ); if (catchall !== undefined) { usedBindGroupLayouts[catchall[0]]?.$name( @@ -516,23 +505,53 @@ class ComputePipelineCore implements SelfResolvable { logResources, }; - if (PERF?.enabled) { - void (async () => { - const start = performance.mark('typegpu:compile-start'); - await device.queue.onSubmittedWorkDone(); - const compileMeasure = performance.measure('typegpu:compiled', { - start: start.name, - }); - - PERF?.record('resolution', { - resolveDuration: resolveMeasure?.duration, - compileDuration: compileMeasure.duration, - wgslSize: code.length, - }); - })(); - } + this.performanceCollector.measureCompile(device); } return this._memo; } } + +interface PerformanceCollector { + measureResolve(callback: () => ResolutionResult): ResolutionResult; + measureCompile(device: GPUDevice): void; +} + +class PerformanceCollectorImpl implements PerformanceCollector { + resolveMeasure: PerformanceMeasure | undefined; + wgslSize: number | undefined; + + measureResolve(callback: () => ResolutionResult): ResolutionResult { + const resolveStart = performance.mark('typegpu:resolution:start'); + const result = callback(); + this.resolveMeasure = performance.measure('typegpu:resolution', { + start: resolveStart.name, + }); + this.wgslSize = result.code.length; + return result; + } + + measureCompile(device: GPUDevice): void { + void (async () => { + const start = performance.mark('typegpu:compile-start'); + await device.queue.onSubmittedWorkDone(); + const compileMeasure = performance.measure('typegpu:compiled', { + start: start.name, + }); + + PERF?.record('resolution', { + resolveDuration: this.resolveMeasure?.duration, + compileDuration: compileMeasure.duration, + wgslSize: this.wgslSize, + }); + })(); + } +} + +class PerformanceCollectorNullImpl implements PerformanceCollector { + measureResolve(callback: () => ResolutionResult): ResolutionResult { + return callback(); + } + + measureCompile(): void {} +} diff --git a/packages/typegpu/tests/pipelineInitAsync.test.ts b/packages/typegpu/tests/pipelineInitAsync.test.ts index 09666b201e..fe5a311ab4 100644 --- a/packages/typegpu/tests/pipelineInitAsync.test.ts +++ b/packages/typegpu/tests/pipelineInitAsync.test.ts @@ -18,6 +18,7 @@ describe('initAsync', () => { }" `); + expect(device.mock.createComputePipeline).not.toHaveBeenCalled(); }); it('throws when attempting to unwrap when not resolved', async ({ root, device }) => { @@ -45,6 +46,7 @@ describe('initAsync', () => { root.unwrap(pipeline); // resolves & compiles the pipeline await pipeline.initAsync(); // should not enqueue device operations + expect(device.mock.createComputePipeline).toHaveBeenCalled(); expect(device.mock.createComputePipelineAsync).not.toHaveBeenCalled(); }); }); From c35287630dbe27d5231a2d72eebe65c7bbec2435 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:25:44 +0200 Subject: [PATCH 04/17] Abstract `resolveAndCreateShaderModule` --- .../src/core/pipeline/computePipeline.ts | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index f05809a323..02967fc762 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -464,32 +464,8 @@ class ComputePipelineCore implements SelfResolvable { if (this._memo === undefined) { 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 { code, usedBindGroupLayouts, catchall, logResources } = - this.performanceCollector.measureResolve(() => - resolve(this, { - namespace: ns, - enableExtensions, - shaderGenerator: this.root.shaderGenerator, - root: this.root, - }), - ); - - if (catchall !== undefined) { - usedBindGroupLayouts[catchall[0]]?.$name( - `${getName(this) ?? ''} - Automatic Bind Group & Layout`, - ); - } - - const module = device.createShaderModule({ - label: `${getName(this) ?? ''} - Shader`, - code, - }); + const { resolutionResult, module } = this.resolveAndCreateShaderModule(); + const { usedBindGroupLayouts, catchall, logResources } = resolutionResult; this._memo = { pipeline: device.createComputePipeline({ @@ -510,6 +486,38 @@ class ComputePipelineCore implements SelfResolvable { return this._memo; } + + private 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.performanceCollector.measureResolve(() => + resolve(this, { + namespace: ns, + enableExtensions, + shaderGenerator: this.root.shaderGenerator, + root: this.root, + }), + ); + const { code, usedBindGroupLayouts, catchall } = resolutionResult; + + if (catchall !== undefined) { + usedBindGroupLayouts[catchall[0]]?.$name( + `${getName(this) ?? ''} - Automatic Bind Group & Layout`, + ); + } + + const module = device.createShaderModule({ + label: `${getName(this) ?? ''} - Shader`, + code, + }); + + return { resolutionResult, module }; + } } interface PerformanceCollector { From 8852e650d64a1f21348b0d1cf990046ee1e964bc Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:29:20 +0200 Subject: [PATCH 05/17] Clean up initAsync --- .../src/core/pipeline/computePipeline.ts | 65 ++----------------- 1 file changed, 4 insertions(+), 61 deletions(-) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index 02967fc762..a54a0caafd 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -366,56 +366,14 @@ class ComputePipelineCore implements SelfResolvable { initAsync(): Promise { if (this._memo !== undefined) { // the pipeline was already resolved & compiled - console.log('memo was present'); return Promise.resolve(); } - console.log('memo was not present'); - if (!this._initAsyncPromise) { + if (this._initAsyncPromise === undefined) { // the pipeline did not start resolution & compilation - const device = this.root.device; - const enableExtensions = wgslEnableExtensions.filter((extension) => - this.root.enabledFeatures.has(wgslEnableExtensionToFeatureName[extension]), - ); - - // Resolving code - let resolutionResult: ResolutionResult; - - let resolveMeasure: PerformanceMeasure | undefined; - const ns = namespace({ names: this.root.nameRegistrySetting }); - if (PERF?.enabled) { - const resolveStart = performance.mark('typegpu:resolution:start'); - resolutionResult = resolve(this, { - namespace: ns, - enableExtensions, - shaderGenerator: this.root.shaderGenerator, - root: this.root, - }); - resolveMeasure = performance.measure('typegpu:resolution', { - start: resolveStart.name, - }); - } else { - resolutionResult = resolve(this, { - namespace: ns, - enableExtensions, - shaderGenerator: this.root.shaderGenerator, - root: this.root, - }); - } - - const { code, usedBindGroupLayouts, catchall, logResources } = resolutionResult; - - if (catchall !== undefined) { - usedBindGroupLayouts[catchall[0]]?.$name( - `${getName(this) ?? ''} - Automatic Bind Group & Layout`, - ); - } - - const module = device.createShaderModule({ - label: `${getName(this) ?? ''} - Shader`, - code, - }); + const { resolutionResult, module } = this.resolveAndCreateShaderModule(); + const { usedBindGroupLayouts, catchall, logResources } = resolutionResult; this._initAsyncPromise = device .createComputePipelineAsync({ @@ -435,22 +393,7 @@ class ComputePipelineCore implements SelfResolvable { logResources, }; - if (PERF?.enabled) { - void (async () => { - const start = performance.mark('typegpu:compile-start'); - await device.queue.onSubmittedWorkDone(); - const compileMeasure = performance.measure('typegpu:compiled', { - start: start.name, - }); - - PERF?.record('resolution', { - resolveDuration: resolveMeasure?.duration, - compileDuration: compileMeasure.duration, - wgslSize: code.length, - }); - })(); - } - + this.performanceCollector.measureCompile(device); this._initAsyncPromise = undefined; }); } From fcbed038c7a6856f0298231dc68b83d3228b9f05 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:53:36 +0200 Subject: [PATCH 06/17] Add jsDocs --- packages/typegpu/src/core/pipeline/computePipeline.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index a54a0caafd..91b29a6c71 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -67,6 +67,12 @@ export interface TgpuComputePipeline extends TgpuNamable, SelfResolvable, Timeab dispatchWorkgroups(x: number, y?: number, z?: number): void; + /** + * Immediately resolves the pipeline, then calls `device.createComputePipelineAsync()`. + * + * NOTE: while it is not necessary to initialize pipeline manually (it is initialized automatically when necessary), + * it is generally preferable to use this method whenever possible, as it prevents blocking of GPU operation execution on pipeline compilation. + */ initAsync(): Promise; /** From fa5c7068960dba0ef1ffa4137f96828038a56cbc Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:59:15 +0200 Subject: [PATCH 07/17] Extract unwrap to initSync --- .../src/core/pipeline/computePipeline.ts | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index 91b29a6c71..fb932d8586 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -392,12 +392,7 @@ class ComputePipelineCore implements SelfResolvable { }) .then((pipeline) => { // resolve - this._memo = { - pipeline, - usedBindGroupLayouts, - catchall, - logResources, - }; + this._memo = { pipeline, usedBindGroupLayouts, catchall, logResources }; this.performanceCollector.measureCompile(device); this._initAsyncPromise = undefined; @@ -406,34 +401,39 @@ class ComputePipelineCore implements SelfResolvable { return this._initAsyncPromise; } - public unwrap(): Memo { - if (this._initAsyncPromise) { - throw new Error("'pipeline.initAsync()' was called and is not yet resolved."); + initSync() { + if (this._memo !== undefined) { + return; } - if (this._memo === undefined) { - const device = this.root.device; - const { resolutionResult, module } = this.resolveAndCreateShaderModule(); - const { usedBindGroupLayouts, catchall, logResources } = resolutionResult; + if (this._initAsyncPromise !== undefined) { + throw new Error("'pipeline.initAsync()' was called and is not yet resolved."); + } - this._memo = { - pipeline: device.createComputePipeline({ - label: getName(this) ?? '', - layout: device.createPipelineLayout({ - label: `${getName(this) ?? ''} - Pipeline Layout`, - bindGroupLayouts: usedBindGroupLayouts.map((l) => this.root.unwrap(l)), - }), - compute: { module }, + const device = this.root.device; + const { resolutionResult, module } = this.resolveAndCreateShaderModule(); + const { usedBindGroupLayouts, catchall, logResources } = resolutionResult; + + this._memo = { + pipeline: device.createComputePipeline({ + label: getName(this) ?? '', + layout: device.createPipelineLayout({ + label: `${getName(this) ?? ''} - Pipeline Layout`, + bindGroupLayouts: usedBindGroupLayouts.map((l) => this.root.unwrap(l)), }), - usedBindGroupLayouts, - catchall, - logResources, - }; + compute: { module }, + }), + usedBindGroupLayouts, + catchall, + logResources, + }; - this.performanceCollector.measureCompile(device); - } + this.performanceCollector.measureCompile(device); + } - return this._memo; + public unwrap(): Memo { + this.initSync(); + return this._memo as Memo; } private resolveAndCreateShaderModule() { From 3f4379d6537ff186279d7c04c07d0b3fb19fb54b Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:05:23 +0200 Subject: [PATCH 08/17] Add initSync to the interface --- .../src/core/pipeline/computePipeline.ts | 17 +++++ packages/typegpu/tests/pipelineInit.test.ts | 70 +++++++++++++++++++ .../typegpu/tests/pipelineInitAsync.test.ts | 53 -------------- 3 files changed, 87 insertions(+), 53 deletions(-) create mode 100644 packages/typegpu/tests/pipelineInit.test.ts delete mode 100644 packages/typegpu/tests/pipelineInitAsync.test.ts diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index fb932d8586..63f4ff8645 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -75,6 +75,13 @@ export interface TgpuComputePipeline extends TgpuNamable, SelfResolvable, Timeab */ initAsync(): Promise; + /** + * Immediately resolves the pipeline and creates WebGPU resources. + * + * NOTE: it is not necessary to initialize pipeline manually (it is initialized automatically when necessary). + */ + initSync(): void; + /** * Dispatches compute workgroups using parameters read from a buffer. * The buffer must contain 3 consecutive u32 values (x, y, z workgroup counts). @@ -261,6 +268,10 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { return this.#core.initAsync(); } + initSync() { + this.#core.initSync(); + } + private _applyComputeState(pass: GPUComputePassEncoder): void { const memo = this.#core.unwrap(); const { root } = this.#core; @@ -369,6 +380,12 @@ class ComputePipelineCore implements SelfResolvable { 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(): Promise { if (this._memo !== undefined) { // the pipeline was already resolved & compiled diff --git a/packages/typegpu/tests/pipelineInit.test.ts b/packages/typegpu/tests/pipelineInit.test.ts new file mode 100644 index 0000000000..91c1880879 --- /dev/null +++ b/packages/typegpu/tests/pipelineInit.test.ts @@ -0,0 +1,70 @@ +import { describe, expect } from 'vitest'; +import tgpu from '../src/index.js'; +import { it } from 'typegpu-testing-utility'; + +describe('pipeline initialization', () => { + describe('compute pipeline', () => { + const computeFn = tgpu.computeFn({ workgroupSize: [1, 1, 1] })(() => {}); + describe('initSync', () => { + it('resolves and creates a pipeline', ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + pipeline.initSync(); + + expect(device.mock.createComputePipeline).toHaveBeenCalled(); + expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` + "@compute @workgroup_size(1, 1, 1) fn computeFn() { + + }" + `); + }); + }); + + describe('initAsync', () => { + it('resolves and creates a pipeline', async ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + await pipeline.initAsync(); + + expect(device.mock.createComputePipelineAsync).toHaveBeenCalled(); + expect(() => root.unwrap(pipeline)).not.toThrow(); // this means that memo already exists + expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` + "@compute @workgroup_size(1, 1, 1) fn computeFn() { + + }" + `); + expect(device.mock.createComputePipeline).not.toHaveBeenCalled(); + }); + + it('throws when attempting to unwrap when not resolved', async ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + // oxlint-disable-next-line typescript-eslint/no-floating-promises -- it's a test + pipeline.initAsync(); + + expect(() => root.unwrap(pipeline)).toThrowErrorMatchingInlineSnapshot( + `[Error: 'pipeline.initAsync()' was called and is not yet resolved.]`, + ); + }); + + it('returns the promise again if not resolved', async ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + const p1 = pipeline.initAsync(); + const p2 = pipeline.initAsync(); + + expect(p1).toBe(p2); + }); + + it('returns a resolved promise if pipeline was already created', async ({ root, device }) => { + const pipeline = root.createComputePipeline({ compute: computeFn }); + + root.unwrap(pipeline); // resolves & compiles the pipeline + await pipeline.initAsync(); // should not enqueue device operations + + expect(device.mock.createComputePipeline).toHaveBeenCalled(); + expect(device.mock.createComputePipelineAsync).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/packages/typegpu/tests/pipelineInitAsync.test.ts b/packages/typegpu/tests/pipelineInitAsync.test.ts deleted file mode 100644 index fe5a311ab4..0000000000 --- a/packages/typegpu/tests/pipelineInitAsync.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, expect } from 'vitest'; -import tgpu from '../src/index.js'; -import { it } from 'typegpu-testing-utility'; - -describe('initAsync', () => { - const computeFn = tgpu.computeFn({ workgroupSize: [1, 1, 1] })(() => {}); - - describe('compute pipeline', () => { - it('resolves and creates a pipeline', async ({ root, device }) => { - const pipeline = root.createComputePipeline({ compute: computeFn }); - - await pipeline.initAsync(); - - expect(device.mock.createComputePipelineAsync).toHaveBeenCalled(); - expect(() => root.unwrap(pipeline)).not.toThrow(); // this means that memo already exists - expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` - "@compute @workgroup_size(1, 1, 1) fn computeFn() { - - }" - `); - expect(device.mock.createComputePipeline).not.toHaveBeenCalled(); - }); - - it('throws when attempting to unwrap when not resolved', async ({ root, device }) => { - const pipeline = root.createComputePipeline({ compute: computeFn }); - - pipeline.initAsync(); - - expect(() => root.unwrap(pipeline)).toThrowErrorMatchingInlineSnapshot( - `[Error: 'pipeline.initAsync()' was called and is not yet resolved.]`, - ); - }); - - it('returns the promise again if not resolved', async ({ root, device }) => { - const pipeline = root.createComputePipeline({ compute: computeFn }); - - const p1 = pipeline.initAsync(); - const p2 = pipeline.initAsync(); - - expect(p1).toBe(p2); - }); - - it('returns a resolved promise if pipeline was already created', async ({ root, device }) => { - const pipeline = root.createComputePipeline({ compute: computeFn }); - - root.unwrap(pipeline); // resolves & compiles the pipeline - await pipeline.initAsync(); // should not enqueue device operations - - expect(device.mock.createComputePipeline).toHaveBeenCalled(); - expect(device.mock.createComputePipelineAsync).not.toHaveBeenCalled(); - }); - }); -}); From 674c6b24d6cd6bd5c10b54aa3a577c5702cf6b86 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:52:28 +0200 Subject: [PATCH 09/17] Update examples --- .../examples/algorithms/matrix-next/index.ts | 1 + .../algorithms/mnist-inference/index.ts | 1 + .../rendering/function-visualizer/index.ts | 1 + .../src/examples/simulation/gravity/index.ts | 2 + .../examples/simulation/stable-fluid/index.ts | 13 ++++ .../matrix-next.test.ts | 35 ++++++++- .../mnist-inference.test.ts | 26 ++++++- .../stable-fluid.test.ts | 71 ++++++++++++++++++- 8 files changed, 146 insertions(+), 4 deletions(-) diff --git a/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts b/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts index afc3e10130..746792525d 100644 --- a/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts @@ -35,6 +35,7 @@ const buffers = { let bindGroup = createBindGroup(); const pipelines = createPipelines(); +await Promise.all([pipelines['gpu-optimized'].initAsync(), pipelines['gpu-simple'].initAsync()]); function createBindGroup() { return root.createBindGroup(computeLayout, { diff --git a/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts b/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts index 4fe7ed0d89..1cf04ebe7b 100644 --- a/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts @@ -85,6 +85,7 @@ const pipelines = { ? root.createComputePipeline({ compute: subgroupCompute }) : null, }; +await Promise.all([pipelines.default.initAsync(), pipelines.subgroup?.initAsync()]); // Definitions for the network diff --git a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts index b426e67e53..3cc73a5e32 100644 --- a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts @@ -90,6 +90,7 @@ const createComputePipeline = (exprCode: string) => { const computePipelines: Array = initialFunctions.map( (functionData, _) => createComputePipeline(functionData.code), ); +await Promise.all(computePipelines.map((guarded) => guarded.pipeline.initAsync())); // Render background shader diff --git a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts index 39601f5c9e..a3551150b2 100644 --- a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts @@ -113,6 +113,8 @@ const renderPipeline = root }, }); +await Promise.all([computeCollisionsPipeline.initAsync(), computeGravityPipeline.initAsync()]); + let depthTexture = root.device.createTexture({ size: [canvas.width, canvas.height, 1], format: 'depth24plus', diff --git a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts index 420c81d294..a63732c0a5 100644 --- a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts @@ -126,6 +126,19 @@ const projectPipeline = createComputePipeline(c.projectFn); const advectInkPipeline = createComputePipeline(c.advectInkFn); const addInkPipeline = createComputePipeline(c.addInkFn); +// Eagerly initialize all pipelines +await Promise.all([ + brushPipeline.initSync(), + addForcePipeline.initSync(), + advectPipeline.initSync(), + diffusionPipeline.initSync(), + divergencePipeline.initSync(), + pressurePipeline.initSync(), + projectPipeline.initSync(), + advectInkPipeline.initSync(), + addInkPipeline.initSync(), +]); + // Create render pipelines function createRenderPipeline(fragmentFn: TgpuFragmentFn<{ uv: d.Vec2f }, d.Vec4f>) { return root.createRenderPipeline({ diff --git a/apps/typegpu-docs/tests/individual-example-tests/matrix-next.test.ts b/apps/typegpu-docs/tests/individual-example-tests/matrix-next.test.ts index fd5ba40cfe..6dac9c8e87 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/matrix-next.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/matrix-next.test.ts @@ -15,7 +15,7 @@ describe('matrix(next) example', () => { category: 'algorithms', name: 'matrix-next', controlTriggers: ['Compute'], - expectedCalls: 1, + expectedCalls: 2, }, device, ); @@ -84,6 +84,39 @@ describe('matrix(next) example', () => { let outputIndex = getIndex(globalRow, globalCol, (*dimensions).secondColumnCount); resultMatrix[outputIndex] = accumulatedResult; } + } + + struct MatrixInfo { + firstRowCount: u32, + firstColumnCount: u32, + secondColumnCount: u32, + } + + @group(0) @binding(3) var dimensions: MatrixInfo; + + fn getIndex(row: u32, col: u32, columns: u32) -> u32 { + return (col + (row * columns)); + } + + @group(0) @binding(0) var firstMatrix: array; + + @group(0) @binding(1) var secondMatrix: array; + + @group(0) @binding(2) var resultMatrix: array; + + @compute @workgroup_size(16, 16) fn computeSimple(@builtin(global_invocation_id) gid: vec3u) { + let row = gid.x; + let col = gid.y; + if (((row >= dimensions.firstRowCount) || (col >= dimensions.secondColumnCount))) { + return; + } + var result = 0; + for (var k = 0u; (k < dimensions.firstColumnCount); k++) { + let aValue = firstMatrix[getIndex(row, k, dimensions.firstColumnCount)]; + let bValue = secondMatrix[getIndex(k, col, dimensions.secondColumnCount)]; + result += (aValue * bValue); + } + resultMatrix[getIndex(row, col, dimensions.secondColumnCount)] = result; }" `); }); diff --git a/apps/typegpu-docs/tests/individual-example-tests/mnist-inference.test.ts b/apps/typegpu-docs/tests/individual-example-tests/mnist-inference.test.ts index 2174bedee4..1160c266ea 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/mnist-inference.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/mnist-inference.test.ts @@ -23,7 +23,31 @@ describe('mnist inference example', () => { ); expect(shaderCodes).toMatchInlineSnapshot(` - "enable subgroups; + "@group(0) @binding(0) var input: array; + + @group(1) @binding(0) var weights: array; + + @group(1) @binding(1) var biases: array; + + @group(0) @binding(1) var output: array; + + fn relu(x: f32) -> f32 { + return max(0f, x); + } + + @compute @workgroup_size(1) fn defaultCompute(@builtin(global_invocation_id) gid: vec3u) { + let inputSize = arrayLength(&input); + let i = gid.x; + let weightsOffset = (i * inputSize); + var sum = 0f; + for (var j = 0u; (j < inputSize); j++) { + sum = fma(input[j], weights[(weightsOffset + j)], sum); + } + let total = (sum + biases[i]); + output[i] = relu(total); + } + + enable subgroups; @group(0) @binding(0) var input: array; diff --git a/apps/typegpu-docs/tests/individual-example-tests/stable-fluid.test.ts b/apps/typegpu-docs/tests/individual-example-tests/stable-fluid.test.ts index 27477efbed..8d5d6fa4d3 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/stable-fluid.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/stable-fluid.test.ts @@ -19,13 +19,67 @@ describe('stable-fluid example', () => { mockImageLoading(); mockCreateImageBitmap(); }, - expectedCalls: 7, + expectedCalls: 9, }, device, ); expect(shaderCodes).toMatchInlineSnapshot(` - "@group(0) @binding(0) var src: texture_2d; + "struct BrushParams { + pos: vec2i, + delta: vec2f, + radius: f32, + forceScale: f32, + inkAmount: f32, + } + + @group(0) @binding(0) var brushParams: BrushParams; + + @group(0) @binding(1) var forceDst: texture_storage_2d; + + @group(0) @binding(2) var inkDst: texture_storage_2d; + + @compute @workgroup_size(16, 16) fn brushFn(@builtin(global_invocation_id) gid: vec3u) { + let pixelPos = gid.xy; + let brushSettings = (&brushParams); + var forceVec = vec2f(); + var inkAmount = 0f; + let deltaX = (f32(pixelPos.x) - f32((*brushSettings).pos.x)); + let deltaY = (f32(pixelPos.y) - f32((*brushSettings).pos.y)); + let distSquared = ((deltaX * deltaX) + (deltaY * deltaY)); + let radiusSquared = ((*brushSettings).radius * (*brushSettings).radius); + if ((distSquared < radiusSquared)) { + let brushWeight = exp((-(distSquared) / radiusSquared)); + forceVec = (((*brushSettings).forceScale * brushWeight) * (*brushSettings).delta); + inkAmount = ((*brushSettings).inkAmount * brushWeight); + } + textureStore(forceDst, pixelPos, vec4f(forceVec, 0f, 1f)); + textureStore(inkDst, pixelPos, vec4f(inkAmount, 0f, 0f, 1f)); + } + + @group(0) @binding(0) var src: texture_2d; + + @group(0) @binding(2) var force: texture_2d; + + struct ShaderParams { + dt: f32, + viscosity: f32, + } + + @group(0) @binding(3) var simParams: ShaderParams; + + @group(0) @binding(1) var dst: texture_storage_2d; + + @compute @workgroup_size(16, 16) fn addForcesFn(@builtin(global_invocation_id) gid: vec3u) { + let pixelPos = gid.xy; + let currentVel = textureLoad(src, pixelPos, 0).xy; + let forceVec = textureLoad(force, pixelPos, 0).xy; + let timeStep = simParams.dt; + let newVel = (currentVel + (timeStep * forceVec)); + textureStore(dst, pixelPos, vec4f(newVel, 0f, 1f)); + } + + @group(0) @binding(0) var src: texture_2d; @group(0) @binding(1) var dst: texture_storage_2d; @@ -248,6 +302,19 @@ describe('stable-fluid example', () => { textureStore(dst, pixelPos, inkVal); } + @group(0) @binding(2) var add: texture_2d; + + @group(0) @binding(0) var src: texture_2d; + + @group(0) @binding(1) var dst: texture_storage_2d; + + @compute @workgroup_size(16, 16) fn addInkFn(@builtin(global_invocation_id) gid: vec3u) { + let pixelPos = gid.xy; + let addVal = textureLoad(add, pixelPos, 0).x; + let srcVal = textureLoad(src, pixelPos, 0).x; + textureStore(dst, pixelPos, vec4f((addVal + srcVal), 0f, 0f, 1f)); + } + struct renderFn_Output { @builtin(position) pos: vec4f, @location(0) uv: vec2f, From 32c9d168f607d5aaaf7d3d59ede52894f8ba4d09 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:08:50 +0200 Subject: [PATCH 10/17] Update docs --- .../src/content/docs/apis/pipelines.mdx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/typegpu-docs/src/content/docs/apis/pipelines.mdx b/apps/typegpu-docs/src/content/docs/apis/pipelines.mdx index 00a1926688..b298561cd0 100644 --- a/apps/typegpu-docs/src/content/docs/apis/pipelines.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/pipelines.mdx @@ -242,6 +242,29 @@ tgpu.resolve([innerPipeline]); ``` ::: +## Initialization + +Pipeline initialization involves resolving the pipeline code, creating the shader module, and creating the underlying WebGPU pipeline. +This happens automatically the first time the pipeline is executed via `.draw`, `.dispatchWorkgroups`, or a similar method. +The `initSync` and `initAsync` methods let you perform this work ahead of time, avoiding a stall on first execution. + +```ts twoslash +import tgpu from 'typegpu'; +const root = await tgpu.init(); +const mainCompute = tgpu.computeFn({ workgroupSize: [1] })(() => {}); +const pipeline = root.createComputePipeline({ + compute: mainCompute, +}); +const myPipelines = [root.createComputePipeline({ + compute: mainCompute, +})]; +// ---cut--- +pipeline.initSync(); + +await Promise.all(myPipelines.map((pipeline) => pipeline.initAsync())); +``` + + ## Execution ```ts From e3ee1403cb5c71a68c8c87583c5f6ae78da4a789 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:21:57 +0200 Subject: [PATCH 11/17] Update examples --- .../background-segmentation/index.ts | 2 ++ .../examples/simulation/stable-fluid/index.ts | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts b/apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts index 0c08feb8f6..8e44ea37bd 100644 --- a/apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts +++ b/apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts @@ -113,6 +113,7 @@ let blurBindGroups: TgpuBindGroup[]; const prepareModelInputPipeline = root .with(paramsAccess, paramsUniform) .createGuardedComputePipeline(prepareModelInput); +prepareModelInputPipeline.pipeline.initSync(); let currentModelIndex = 0; let session = await prepareSession( @@ -138,6 +139,7 @@ async function switchModel(modelIndex: number) { } const generateMaskFromOutputPipeline = root.createGuardedComputePipeline(generateMaskFromOutput); +generateMaskFromOutputPipeline.pipeline.initSync(); const blurPipelines = [false, true].map((flip) => root.with(flipAccess, flip).createComputePipeline({ compute: computeFn }), diff --git a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts index a63732c0a5..a53ea37370 100644 --- a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts @@ -128,15 +128,15 @@ const addInkPipeline = createComputePipeline(c.addInkFn); // Eagerly initialize all pipelines await Promise.all([ - brushPipeline.initSync(), - addForcePipeline.initSync(), - advectPipeline.initSync(), - diffusionPipeline.initSync(), - divergencePipeline.initSync(), - pressurePipeline.initSync(), - projectPipeline.initSync(), - advectInkPipeline.initSync(), - addInkPipeline.initSync(), + brushPipeline.initAsync(), + addForcePipeline.initAsync(), + advectPipeline.initAsync(), + diffusionPipeline.initAsync(), + divergencePipeline.initAsync(), + pressurePipeline.initAsync(), + projectPipeline.initAsync(), + advectInkPipeline.initAsync(), + addInkPipeline.initAsync(), ]); // Create render pipelines From af270aef56e45d1be5304b3554927a030a652e1f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:37:17 +0200 Subject: [PATCH 12/17] Rename PerformanceCollector to PerformanceTracker --- .../src/core/pipeline/computePipeline.ts | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index 63f4ff8645..7aff85348a 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -70,15 +70,16 @@ export interface TgpuComputePipeline extends TgpuNamable, SelfResolvable, Timeab /** * Immediately resolves the pipeline, then calls `device.createComputePipelineAsync()`. * - * NOTE: while it is not necessary to initialize pipeline manually (it is initialized automatically when necessary), - * it is generally preferable to use this method whenever possible, as it prevents blocking of GPU operation execution on pipeline compilation. + * NOTE: while it is not necessary to initialize pipeline manually, + * it is generally preferable to use this method whenever possible, + * as it prevents blocking of GPU operation execution on pipeline compilation. */ initAsync(): Promise; /** * Immediately resolves the pipeline and creates WebGPU resources. * - * NOTE: it is not necessary to initialize pipeline manually (it is initialized automatically when necessary). + * NOTE: it is not necessary to initialize pipeline manually. */ initSync(): void; @@ -343,7 +344,7 @@ class ComputePipelineCore implements SelfResolvable { readonly root: ExperimentalTgpuRoot; private _initAsyncPromise: Promise | undefined; private _memo: Memo | undefined; - private performanceCollector: PerformanceCollector; + private performanceTracker: PerformanceTracker; #slotBindings: [TgpuSlot, unknown][]; #descriptor: TgpuComputePipeline.Descriptor; @@ -357,9 +358,9 @@ class ComputePipelineCore implements SelfResolvable { this.root = root; this.#slotBindings = slotBindings; this.#descriptor = descriptor; - this.performanceCollector = PERF?.enabled - ? new PerformanceCollectorImpl() - : new PerformanceCollectorNullImpl(); + this.performanceTracker = PERF?.enabled + ? new PerformanceTrackerImpl() + : new NullPerformanceTracker(); } [$resolve](ctx: ResolutionCtx) { @@ -411,7 +412,7 @@ class ComputePipelineCore implements SelfResolvable { // resolve this._memo = { pipeline, usedBindGroupLayouts, catchall, logResources }; - this.performanceCollector.measureCompile(device); + this.performanceTracker.measureCompile(device); this._initAsyncPromise = undefined; }); } @@ -445,7 +446,7 @@ class ComputePipelineCore implements SelfResolvable { logResources, }; - this.performanceCollector.measureCompile(device); + this.performanceTracker.measureCompile(device); } public unwrap(): Memo { @@ -461,7 +462,7 @@ class ComputePipelineCore implements SelfResolvable { // Resolving code const ns = namespace({ names: this.root.nameRegistrySetting }); - const resolutionResult = this.performanceCollector.measureResolve(() => + const resolutionResult = this.performanceTracker.measureResolve(() => resolve(this, { namespace: ns, enableExtensions, @@ -486,22 +487,22 @@ class ComputePipelineCore implements SelfResolvable { } } -interface PerformanceCollector { +interface PerformanceTracker { measureResolve(callback: () => ResolutionResult): ResolutionResult; measureCompile(device: GPUDevice): void; } -class PerformanceCollectorImpl implements PerformanceCollector { - resolveMeasure: PerformanceMeasure | undefined; - wgslSize: number | undefined; +class PerformanceTrackerImpl implements PerformanceTracker { + #resolveMeasure: PerformanceMeasure | undefined; + #wgslSize: number | undefined; measureResolve(callback: () => ResolutionResult): ResolutionResult { const resolveStart = performance.mark('typegpu:resolution:start'); const result = callback(); - this.resolveMeasure = performance.measure('typegpu:resolution', { + this.#resolveMeasure = performance.measure('typegpu:resolution', { start: resolveStart.name, }); - this.wgslSize = result.code.length; + this.#wgslSize = result.code.length; return result; } @@ -514,15 +515,15 @@ class PerformanceCollectorImpl implements PerformanceCollector { }); PERF?.record('resolution', { - resolveDuration: this.resolveMeasure?.duration, + resolveDuration: this.#resolveMeasure?.duration, compileDuration: compileMeasure.duration, - wgslSize: this.wgslSize, + wgslSize: this.#wgslSize, }); })(); } } -class PerformanceCollectorNullImpl implements PerformanceCollector { +class NullPerformanceTracker implements PerformanceTracker { measureResolve(callback: () => ResolutionResult): ResolutionResult { return callback(); } From de777f1edfe4903595f1a6832c75e7eca36dee4f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:38:50 +0200 Subject: [PATCH 13/17] Use true private props --- .../src/core/pipeline/computePipeline.ts | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index 7aff85348a..c019196b12 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -342,9 +342,10 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { class ComputePipelineCore implements SelfResolvable { readonly [$internal] = true; readonly root: ExperimentalTgpuRoot; - private _initAsyncPromise: Promise | undefined; - private _memo: Memo | undefined; - private performanceTracker: PerformanceTracker; + #performanceTracker: PerformanceTracker; + + #initAsyncPromise: Promise | undefined; + #memo: Memo | undefined; #slotBindings: [TgpuSlot, unknown][]; #descriptor: TgpuComputePipeline.Descriptor; @@ -358,7 +359,7 @@ class ComputePipelineCore implements SelfResolvable { this.root = root; this.#slotBindings = slotBindings; this.#descriptor = descriptor; - this.performanceTracker = PERF?.enabled + this.#performanceTracker = PERF?.enabled ? new PerformanceTrackerImpl() : new NullPerformanceTracker(); } @@ -388,18 +389,18 @@ class ComputePipelineCore implements SelfResolvable { * we want it to return the same promise each time. */ initAsync(): Promise { - if (this._memo !== undefined) { + if (this.#memo !== undefined) { // the pipeline was already resolved & compiled return Promise.resolve(); } - if (this._initAsyncPromise === undefined) { + 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 + this.#initAsyncPromise = device .createComputePipelineAsync({ label: getName(this) ?? '', layout: device.createPipelineLayout({ @@ -409,22 +410,20 @@ class ComputePipelineCore implements SelfResolvable { compute: { module }, }) .then((pipeline) => { - // resolve - this._memo = { pipeline, usedBindGroupLayouts, catchall, logResources }; - - this.performanceTracker.measureCompile(device); - this._initAsyncPromise = undefined; + this.#memo = { pipeline, usedBindGroupLayouts, catchall, logResources }; + this.#performanceTracker.measureCompile(device); + this.#initAsyncPromise = undefined; }); } - return this._initAsyncPromise; + return this.#initAsyncPromise; } initSync() { - if (this._memo !== undefined) { + if (this.#memo !== undefined) { return; } - if (this._initAsyncPromise !== undefined) { + if (this.#initAsyncPromise !== undefined) { throw new Error("'pipeline.initAsync()' was called and is not yet resolved."); } @@ -432,7 +431,7 @@ class ComputePipelineCore implements SelfResolvable { const { resolutionResult, module } = this.resolveAndCreateShaderModule(); const { usedBindGroupLayouts, catchall, logResources } = resolutionResult; - this._memo = { + this.#memo = { pipeline: device.createComputePipeline({ label: getName(this) ?? '', layout: device.createPipelineLayout({ @@ -446,12 +445,12 @@ class ComputePipelineCore implements SelfResolvable { logResources, }; - this.performanceTracker.measureCompile(device); + this.#performanceTracker.measureCompile(device); } public unwrap(): Memo { this.initSync(); - return this._memo as Memo; + return this.#memo as Memo; } private resolveAndCreateShaderModule() { @@ -462,7 +461,7 @@ class ComputePipelineCore implements SelfResolvable { // Resolving code const ns = namespace({ names: this.root.nameRegistrySetting }); - const resolutionResult = this.performanceTracker.measureResolve(() => + const resolutionResult = this.#performanceTracker.measureResolve(() => resolve(this, { namespace: ns, enableExtensions, From a5f7a100c37a2834760c1b628964423dd3a97a01 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:56:51 +0200 Subject: [PATCH 14/17] Add initAsync and initSync directly onto guarded compute pipeline --- .../background-segmentation/index.ts | 4 ++-- .../rendering/function-visualizer/index.ts | 2 +- packages/typegpu/src/core/root/init.ts | 8 ++++++++ packages/typegpu/src/core/root/rootTypes.ts | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts b/apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts index 8e44ea37bd..6b86aa54bd 100644 --- a/apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts +++ b/apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts @@ -113,7 +113,7 @@ let blurBindGroups: TgpuBindGroup[]; const prepareModelInputPipeline = root .with(paramsAccess, paramsUniform) .createGuardedComputePipeline(prepareModelInput); -prepareModelInputPipeline.pipeline.initSync(); +prepareModelInputPipeline.initSync(); let currentModelIndex = 0; let session = await prepareSession( @@ -139,7 +139,7 @@ async function switchModel(modelIndex: number) { } const generateMaskFromOutputPipeline = root.createGuardedComputePipeline(generateMaskFromOutput); -generateMaskFromOutputPipeline.pipeline.initSync(); +generateMaskFromOutputPipeline.initSync(); const blurPipelines = [false, true].map((flip) => root.with(flipAccess, flip).createComputePipeline({ compute: computeFn }), diff --git a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts index 3cc73a5e32..53c0965a4e 100644 --- a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts @@ -90,7 +90,7 @@ const createComputePipeline = (exprCode: string) => { const computePipelines: Array = initialFunctions.map( (functionData, _) => createComputePipeline(functionData.code), ); -await Promise.all(computePipelines.map((guarded) => guarded.pipeline.initAsync())); +await Promise.all(computePipelines.map((guarded) => guarded.initAsync())); // Render background shader diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index 075ac5e36a..1dfa1ed9ef 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -184,6 +184,14 @@ export class TgpuGuardedComputePipelineImpl< this.#pipeline.dispatchWorkgroups(workgroupCount.x, workgroupCount.y, workgroupCount.z); } + initAsync() { + return this.pipeline.initAsync(); + } + + initSync() { + return this.pipeline.initSync(); + } + get pipeline() { return this.#pipeline; } diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index 1a52e07654..7952e52d76 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -108,6 +108,22 @@ export interface TgpuGuardedComputePipeline e */ dispatchThreads(...args: TArgs): void; + /** + * Immediately resolves the pipeline, then calls `device.createComputePipelineAsync()`. + * + * NOTE: while it is not necessary to initialize pipeline manually, + * it is generally preferable to use this method whenever possible, + * as it prevents blocking of GPU operation execution on pipeline compilation. + */ + initAsync(): Promise; + + /** + * Immediately resolves the pipeline and creates WebGPU resources. + * + * NOTE: it is not necessary to initialize pipeline manually. + */ + initSync(): void; + /** * The underlying pipeline used during `dispatchThreads`. */ From 5dc41cb4257c439f229209a736301bf62cc78dde Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:47:00 +0200 Subject: [PATCH 15/17] Update docs --- .../src/content/docs/apis/pipelines.mdx | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/apis/pipelines.mdx b/apps/typegpu-docs/src/content/docs/apis/pipelines.mdx index b298561cd0..997b5242e7 100644 --- a/apps/typegpu-docs/src/content/docs/apis/pipelines.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/pipelines.mdx @@ -246,7 +246,8 @@ tgpu.resolve([innerPipeline]); Pipeline initialization involves resolving the pipeline code, creating the shader module, and creating the underlying WebGPU pipeline. This happens automatically the first time the pipeline is executed via `.draw`, `.dispatchWorkgroups`, or a similar method. -The `initSync` and `initAsync` methods let you perform this work ahead of time, avoiding a stall on first execution. +The `initSync` method lets you start the initialization early. +To wait until initialization actually finishes on the device (fully avoiding a stall on first execution), use `initAsync` instead. ```ts twoslash import tgpu from 'typegpu'; @@ -255,13 +256,47 @@ const mainCompute = tgpu.computeFn({ workgroupSize: [1] })(() => {}); const pipeline = root.createComputePipeline({ compute: mainCompute, }); -const myPipelines = [root.createComputePipeline({ +// ---cut--- +// Automatically calls `pipeline.initSync();`, +// then enqueues a dispatch. +pipeline.dispatchWorkgroups(1); +``` + +```ts twoslash +import tgpu from 'typegpu'; +const root = await tgpu.init(); +const mainCompute = tgpu.computeFn({ workgroupSize: [1] })(() => {}); +const pipeline = root.createComputePipeline({ compute: mainCompute, -})]; +}); // ---cut--- +// If not already initialized, runs JS initialization, +// and issues pipeline initialization steps on the device. pipeline.initSync(); -await Promise.all(myPipelines.map((pipeline) => pipeline.initAsync())); +// Enqueues a dispatch. +// Stall partially avoided because the pipeline is already resolved. +pipeline.dispatchWorkgroups(1); +``` + +```ts twoslash +import tgpu from 'typegpu'; +const root = await tgpu.init(); +const mainCompute = tgpu.computeFn({ workgroupSize: [1] })(() => {}); +const pipeline = root.createComputePipeline({ + compute: mainCompute, +}); +// ---cut--- +// If not already initialized, runs JS initialization, +// and issues pipeline initialization steps on the device. +const initPromise = pipeline.initAsync(); + +// Awaits until the initialization finishes. +await initPromise; + +// Enqueues a dispatch. +// Stall avoided, the pipeline is already resolved and initialized on the device. +pipeline.dispatchWorkgroups(1); ``` From f090a7ac45e4fa593d295a57b51b29a03286a4c5 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:55:23 +0200 Subject: [PATCH 16/17] Await promises in examples as late as possible --- .../src/examples/algorithms/matrix-next/index.ts | 6 +++++- .../src/examples/algorithms/mnist-inference/index.ts | 3 ++- .../src/examples/rendering/function-visualizer/index.ts | 3 ++- apps/typegpu-docs/src/examples/simulation/gravity/index.ts | 6 +++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts b/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts index 746792525d..b9405a0714 100644 --- a/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts @@ -35,7 +35,10 @@ const buffers = { let bindGroup = createBindGroup(); const pipelines = createPipelines(); -await Promise.all([pipelines['gpu-optimized'].initAsync(), pipelines['gpu-simple'].initAsync()]); +const pipelinePromises = [ + pipelines['gpu-optimized'].initAsync(), + pipelines['gpu-simple'].initAsync(), +]; function createBindGroup() { return root.createBindGroup(computeLayout, { @@ -108,6 +111,7 @@ function updateInputDisplays() { let isComputing = false; +await Promise.all(pipelinePromises); async function compute() { if (isComputing) { console.warn('Computation already in progress'); diff --git a/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts b/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts index 1cf04ebe7b..3fd99f61ff 100644 --- a/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts @@ -85,7 +85,7 @@ const pipelines = { ? root.createComputePipeline({ compute: subgroupCompute }) : null, }; -await Promise.all([pipelines.default.initAsync(), pipelines.subgroup?.initAsync()]); +const pipelinePromises = [pipelines.default.initAsync(), pipelines.subgroup?.initAsync()]; // Definitions for the network @@ -182,6 +182,7 @@ function createNetwork(layers: [LayerData, LayerData][]): Network { } const network = createNetwork(await downloadLayers(root)); +await Promise.all(pipelinePromises); // #region Example controls and cleanup diff --git a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts index 53c0965a4e..61b11c223d 100644 --- a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts @@ -90,7 +90,7 @@ const createComputePipeline = (exprCode: string) => { const computePipelines: Array = initialFunctions.map( (functionData, _) => createComputePipeline(functionData.code), ); -await Promise.all(computePipelines.map((guarded) => guarded.initAsync())); +const pipelinePromises = computePipelines.map((guarded) => guarded.initAsync()); // Render background shader @@ -225,6 +225,7 @@ function draw() { requestAnimationFrame(draw); } +await Promise.all(pipelinePromises); requestAnimationFrame(draw); function runComputePass(functionNumber: number) { diff --git a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts index a3551150b2..daed85a102 100644 --- a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts @@ -113,7 +113,10 @@ const renderPipeline = root }, }); -await Promise.all([computeCollisionsPipeline.initAsync(), computeGravityPipeline.initAsync()]); +const pipelinePromises = [ + computeCollisionsPipeline.initAsync(), + computeGravityPipeline.initAsync(), +]; let depthTexture = root.device.createTexture({ size: [canvas.width, canvas.height, 1], @@ -169,6 +172,7 @@ function frame(timestamp: DOMHighResTimeStamp) { render(); requestAnimationFrame(frame); } +await Promise.all(pipelinePromises); requestAnimationFrame(frame); async function loadPreset(preset: Preset): Promise { From 0aa4ef302fc2c143143c996a2b755e89ab35a841 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:56:15 +0200 Subject: [PATCH 17/17] Move cleanup to finally --- packages/typegpu/src/core/pipeline/computePipeline.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index c019196b12..c1de8e902f 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -412,6 +412,8 @@ class ComputePipelineCore implements SelfResolvable { .then((pipeline) => { this.#memo = { pipeline, usedBindGroupLayouts, catchall, logResources }; this.#performanceTracker.measureCompile(device); + }) + .finally(() => { this.#initAsyncPromise = undefined; }); }