From 54a1d5235a0f84b0941bc35437588561a5a9a84c Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sat, 18 Oct 2025 10:55:18 +0200 Subject: [PATCH 1/8] Guarded compute pipelines --- .../src/content/docs/fundamentals/utils.mdx | 49 +++++++-------- .../src/examples/rendering/3d-fish/index.ts | 52 ++++++++-------- .../src/examples/simple/increment/index.ts | 6 +- .../examples/simulation/boids-next/index.ts | 5 +- .../fluid-double-buffering/index.ts | 18 +++--- .../simulation/slime-mold-3d/index.ts | 2 +- .../src/examples/tests/dispatch/index.ts | 21 ++++--- .../src/examples/tests/log-test/index.ts | 20 +++--- packages/typegpu/src/core/root/init.ts | 38 ++++++------ packages/typegpu/src/core/root/rootTypes.ts | 61 ++++++++++++------- 10 files changed, 147 insertions(+), 125 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/fundamentals/utils.mdx b/apps/typegpu-docs/src/content/docs/fundamentals/utils.mdx index c3d13136b..a61253a24 100644 --- a/apps/typegpu-docs/src/content/docs/fundamentals/utils.mdx +++ b/apps/typegpu-docs/src/content/docs/fundamentals/utils.mdx @@ -3,10 +3,10 @@ title: Utilities description: A list of various utilities provided by TypeGPU. --- -## *prepareDispatch* +## *root.createGuardedComputePipeline* -The `prepareDispatch` function streamlines running simple computations on the GPU. -Under the hood, it wraps the callback in a `TgpuFn`, creates a compute pipeline, and returns an object with a `dispatchThreads` method that executes the pipeline. +The `root.createGuardedComputePipeline` method streamlines running simple computations on the GPU. +Under the hood, it creates a compute pipeline that calls the provided callback only if the current thread ID is within the requested range, and returns an object with a `dispatchThreads` method that executes the pipeline. Since the pipeline is reused, there’s no additional overhead for subsequent calls. ```ts twoslash @@ -16,16 +16,17 @@ const root = await tgpu.init(); // ---cut--- const data = root.createMutable(d.arrayOf(d.u32, 8), [0, 1, 2, 3, 4, 5, 6, 7]); -const doubleUp = root['~unstable'].prepareDispatch((x) => { - 'use gpu'; - data.$[x] *= 2; -}); +const doubleUpPipeline = root['~unstable'] + .createGuardedComputePipeline((x) => { + 'use gpu'; + data.$[x] *= 2; + }); -doubleUp.dispatchThreads(8); -doubleUp.dispatchThreads(8); -doubleUp.dispatchThreads(4); +doubleUpPipeline.dispatchThreads(8); +doubleUpPipeline.dispatchThreads(8); +doubleUpPipeline.dispatchThreads(4); -// the command encoder will queue the read after `doubleUp` +// the command encoder will queue the read after `doubleUpPipeline` console.log(await data.read()); // [0, 8, 16, 24, 16, 20, 24, 28] ``` @@ -34,7 +35,7 @@ Remember to mark the callback with the `'use gpu'` directive to let TypeGPU know ::: The callback can have up to three arguments (dimensions). -`prepareDispatch` can simplify writing a pipeline helping reduce serialization overhead when initializing buffers with data. +`createGuardedComputePipeline` can simplify writing a pipeline helping reduce serialization overhead when initializing buffers with data. Buffer initialization commonly uses random number generators. For that, you can use the [`@typegpu/noise`](TypeGPU/ecosystem/typegpu-noise) library. @@ -51,7 +52,7 @@ const waterLevelMutable = root.createMutable( d.arrayOf(d.arrayOf(d.f32, 512), 1024), ); -root['~unstable'].prepareDispatch((x, y) => { +root['~unstable'].createGuardedComputePipeline((x, y) => { 'use gpu'; randf.seed2(d.vec2f(x, y).div(1024)); waterLevelMutable.$[x][y] = 10 + randf.sample(); @@ -62,7 +63,7 @@ root['~unstable'].prepareDispatch((x, y) => { console.log(await waterLevelMutable.read()); ``` -The result of `prepareDispatch` can have bind groups bound using the `with` method. +The result of `createGuardedComputePipeline` can have bind groups bound using the `with` method. ```ts twoslash import tgpu from 'typegpu'; @@ -71,35 +72,35 @@ import * as std from 'typegpu/std'; const root = await tgpu.init(); // ---cut--- const layout = tgpu.bindGroupLayout({ - buffer: { storage: d.arrayOf(d.u32), access: 'mutable' }, + values: { storage: d.arrayOf(d.u32), access: 'mutable' }, }); const buffer1 = root .createBuffer(d.arrayOf(d.u32, 3), [1, 2, 3]).$usage('storage'); const buffer2 = root .createBuffer(d.arrayOf(d.u32, 4), [2, 4, 8, 16]).$usage('storage'); const bindGroup1 = root.createBindGroup(layout, { - buffer: buffer1, + values: buffer1, }); const bindGroup2 = root.createBindGroup(layout, { - buffer: buffer2, + values: buffer2, }); -const test = root['~unstable'].prepareDispatch((x) => { +const doubleUpPipeline = root['~unstable'].createGuardedComputePipeline((x) => { 'use gpu'; - layout.$.buffer[x] *= 2; + layout.$.values[x] *= 2; }); -test.with(bindGroup1).dispatchThreads(3); -test.with(bindGroup2).dispatchThreads(4); +doubleUpPipeline.with(bindGroup1).dispatchThreads(3); +doubleUpPipeline.with(bindGroup2).dispatchThreads(4); console.log(await buffer1.read()); // [2, 4, 6]; console.log(await buffer2.read()); // [4, 8, 16, 32]; ``` -It is recommended NOT to use `prepareDispatch` for: +It is recommended NOT to use guarded compute pipelines for: - More complex compute shaders. -When using `prepareDispatch`, it is impossible to change workgroup sizes or to use [slots](/TypeGPU/fundamentals/slots). +When using guarded compute pipelines, it is impossible to change workgroup sizes, or effectively utilize workgroup shared memory. For such cases, a manually created pipeline would be more suitable. - Small calls. @@ -129,7 +130,7 @@ import * as d from 'typegpu/data'; const root = await tgpu.init(); // ---cut--- const callCountMutable = root.createMutable(d.u32, 0); -const compute = root['~unstable'].prepareDispatch(() => { +const compute = root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; callCountMutable.$ += 1; console.log('Call number', callCountMutable.$); diff --git a/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts b/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts index 207db53a7..335b9d192 100644 --- a/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts @@ -98,33 +98,34 @@ function enqueuePresetChanges() { const buffer0mutable = fishDataBuffers[0].as('mutable'); const buffer1mutable = fishDataBuffers[1].as('mutable'); const seedUniform = root.createUniform(d.f32); -const randomizeFishPositionsOnGPU = root['~unstable'].prepareDispatch((x) => { - 'use gpu'; - randf.seed2(d.vec2f(d.f32(x), seedUniform.$)); - const data = ModelData({ - position: d.vec3f( - randf.sample() * p.aquariumSize.x - p.aquariumSize.x / 2, - randf.sample() * p.aquariumSize.y - p.aquariumSize.y / 2, - randf.sample() * p.aquariumSize.z - p.aquariumSize.z / 2, - ), - direction: d.vec3f( - randf.sample() * 0.1 - 0.05, - randf.sample() * 0.1 - 0.05, - randf.sample() * 0.1 - 0.05, - ), - scale: p.fishModelScale * (1 + (randf.sample() - 0.5) * 0.8), - variant: randf.sample(), - applySinWave: 1, - applySeaFog: 1, - applySeaDesaturation: 1, +const randomizeFishPositionsPipeline = root['~unstable'] + .createGuardedComputePipeline((x) => { + 'use gpu'; + randf.seed2(d.vec2f(d.f32(x), seedUniform.$)); + const data = ModelData({ + position: d.vec3f( + randf.sample() * p.aquariumSize.x - p.aquariumSize.x / 2, + randf.sample() * p.aquariumSize.y - p.aquariumSize.y / 2, + randf.sample() * p.aquariumSize.z - p.aquariumSize.z / 2, + ), + direction: d.vec3f( + randf.sample() * 0.1 - 0.05, + randf.sample() * 0.1 - 0.05, + randf.sample() * 0.1 - 0.05, + ), + scale: p.fishModelScale * (1 + (randf.sample() - 0.5) * 0.8), + variant: randf.sample(), + applySinWave: 1, + applySeaFog: 1, + applySeaDesaturation: 1, + }); + buffer0mutable.$[x] = data; + buffer1mutable.$[x] = data; }); - buffer0mutable.$[x] = data; - buffer1mutable.$[x] = data; -}); const randomizeFishPositions = () => { seedUniform.write((performance.now() % 10000) / 10000); - randomizeFishPositionsOnGPU.dispatchThreads(p.fishAmount); + randomizeFishPositionsPipeline.dispatchThreads(p.fishAmount); enqueuePresetChanges(); }; @@ -198,7 +199,8 @@ let depthTexture = root.device.createTexture({ usage: GPUTextureUsage.RENDER_ATTACHMENT, }); -const simulateAction = root['~unstable'].prepareDispatch(simulate); +const simulatePipeline = root['~unstable'] + .createGuardedComputePipeline(simulate); // bind groups @@ -254,7 +256,7 @@ function frame(timestamp: DOMHighResTimeStamp) { lastTimestamp = timestamp; cameraBuffer.write(camera); - simulateAction + simulatePipeline .with(computeBindGroups[odd ? 1 : 0]) .dispatchThreads(p.fishAmount); diff --git a/apps/typegpu-docs/src/examples/simple/increment/index.ts b/apps/typegpu-docs/src/examples/simple/increment/index.ts index 7c61610ad..23bd44ca9 100644 --- a/apps/typegpu-docs/src/examples/simple/increment/index.ts +++ b/apps/typegpu-docs/src/examples/simple/increment/index.ts @@ -5,15 +5,15 @@ const root = await tgpu.init(); // Allocating memory for the counter const counter = root.createMutable(d.u32); -// A 0-dimentional shader function -const gpuIncrement = root['~unstable'].prepareDispatch(() => { +// A 0-dimensional compute pipeline +const incrementPipeline = root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; counter.$ += 1; }); async function increment() { // Dispatch and read the result - gpuIncrement.dispatchThreads(); + incrementPipeline.dispatchThreads(); return await counter.read(); } diff --git a/apps/typegpu-docs/src/examples/simulation/boids-next/index.ts b/apps/typegpu-docs/src/examples/simulation/boids-next/index.ts index b7e1b9bea..2d2fde0a5 100644 --- a/apps/typegpu-docs/src/examples/simulation/boids-next/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/boids-next/index.ts @@ -249,7 +249,8 @@ const simulate = (index: number) => { nextTrianglePos.value[index] = instanceInfo; }; -const simulateAction = root['~unstable'].prepareDispatch(simulate); +const simulatePipeline = root['~unstable'] + .createGuardedComputePipeline(simulate); const computeBindGroups = [0, 1].map((idx) => root.createBindGroup(computeBindGroupLayout, { @@ -268,7 +269,7 @@ function frame() { even = !even; - simulateAction + simulatePipeline .with(computeBindGroups[even ? 0 : 1]) .dispatchThreads(triangleAmount); diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts index e26cb79e6..47be22856 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts @@ -453,10 +453,10 @@ function makePipelines( inputGridReadonly: TgpuBufferReadonly, outputGridMutable: TgpuBufferMutable, ) { - const initWorldAction = root['~unstable'] + const initWorldPipeline = root['~unstable'] .with(inputGridSlot, outputGridMutable) .with(outputGridSlot, outputGridMutable) - .prepareDispatch((xu, yu) => { + .createGuardedComputePipeline((xu, yu) => { 'use gpu'; const x = d.i32(xu); const y = d.i32(yu); @@ -477,15 +477,15 @@ function makePipelines( outputGridSlot.$[index] = value; }); - const simulateAction = root['~unstable'] + const simulatePipeline = root['~unstable'] .with(inputGridSlot, inputGridReadonly) .with(outputGridSlot, outputGridMutable) - .prepareDispatch(simulate); + .createGuardedComputePipeline(simulate); - const moveObstaclesAction = root['~unstable'] + const moveObstaclesPipeline = root['~unstable'] .with(inputGridSlot, outputGridMutable) .with(outputGridSlot, outputGridMutable) - .prepareDispatch(moveObstacles); + .createGuardedComputePipeline(moveObstacles); const renderPipeline = root['~unstable'] .with(inputGridSlot, inputGridReadonly) @@ -496,17 +496,17 @@ function makePipelines( return { init() { - initWorldAction.dispatchThreads(gridSize, gridSize); + initWorldPipeline.dispatchThreads(gridSize, gridSize); }, applyMovedObstacles(bufferData: d.Infer[]) { obstacles.write(bufferData); - moveObstaclesAction.dispatchThreads(); + moveObstaclesPipeline.dispatchThreads(); prevObstacles.write(bufferData); }, compute() { - simulateAction.dispatchThreads(gridSize, gridSize); + simulatePipeline.dispatchThreads(gridSize, gridSize); }, render() { diff --git a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts index dd93f97ac..7c32217f5 100644 --- a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts @@ -106,7 +106,7 @@ const Params = d.struct({ const agentsData = root.createMutable(d.arrayOf(Agent, NUM_AGENTS)); -root['~unstable'].prepareDispatch((x) => { +root['~unstable'].createGuardedComputePipeline((x) => { 'use gpu'; randf.seed(x / NUM_AGENTS); const pos = randf.inUnitSphere().mul(resolution.x / 4).add(resolution.div(2)); diff --git a/apps/typegpu-docs/src/examples/tests/dispatch/index.ts b/apps/typegpu-docs/src/examples/tests/dispatch/index.ts index 066278c2c..7e3bad6ae 100644 --- a/apps/typegpu-docs/src/examples/tests/dispatch/index.ts +++ b/apps/typegpu-docs/src/examples/tests/dispatch/index.ts @@ -13,7 +13,7 @@ function isEqual(e1: unknown, e2: unknown): boolean { async function test0d(): Promise { const mutable = root.createMutable(d.u32); - root['~unstable'].prepareDispatch(() => { + root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; mutable.$ = 126; }).dispatchThreads(); @@ -24,7 +24,7 @@ async function test0d(): Promise { async function test1d(): Promise { const size = [7] as const; const mutable = root.createMutable(d.arrayOf(d.u32, size[0])); - root['~unstable'].prepareDispatch((x) => { + root['~unstable'].createGuardedComputePipeline((x) => { 'use gpu'; mutable.$[x] = x; }).dispatchThreads(...size); @@ -37,7 +37,7 @@ async function test2d(): Promise { const mutable = root.createMutable( d.arrayOf(d.arrayOf(d.vec2u, size[1]), size[0]), ); - root['~unstable'].prepareDispatch((x, y) => { + root['~unstable'].createGuardedComputePipeline((x, y) => { 'use gpu'; mutable.$[x][y] = d.vec2u(x, y); }).dispatchThreads(...size); @@ -56,7 +56,7 @@ async function test3d(): Promise { size[0], ), ); - root['~unstable'].prepareDispatch((x, y, z) => { + root['~unstable'].createGuardedComputePipeline((x, y, z) => { 'use gpu'; mutable.$[x][y][z] = d.vec3u(x, y, z); }).dispatchThreads(...size); @@ -69,7 +69,7 @@ async function test3d(): Promise { async function testWorkgroupSize(): Promise { const mutable = root.createMutable(d.atomic(d.u32)); - root['~unstable'].prepareDispatch((x, y, z) => { + root['~unstable'].createGuardedComputePipeline((x, y, z) => { 'use gpu'; std.atomicAdd(mutable.$, 1); }).dispatchThreads(4, 3, 2); @@ -81,7 +81,7 @@ async function testMultipleDispatches(): Promise { const size = [7] as const; const mutable = root .createMutable(d.arrayOf(d.u32, size[0]), [0, 1, 2, 3, 4, 5, 6]); - const test = root['~unstable'].prepareDispatch((x: number) => { + const test = root['~unstable'].createGuardedComputePipeline((x: number) => { 'use gpu'; mutable.$[x] *= 2; }); @@ -107,7 +107,7 @@ async function testDifferentBindGroups(): Promise { buffer: buffer2, }); - const test = root['~unstable'].prepareDispatch(() => { + const test = root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; for (let i = d.u32(); i < std.arrayLength(layout.$.buffer); i++) { layout.$.buffer[i] *= 2; @@ -131,8 +131,11 @@ async function testSlots(): Promise { result.$ += valueSlot.$; }; - root['~unstable'].prepareDispatch(main).dispatchThreads(); // add 1 - root['~unstable'].with(valueSlot, 3).prepareDispatch(main).dispatchThreads(); // add 3 + root['~unstable'].createGuardedComputePipeline(main).dispatchThreads(); // add 1 + root['~unstable'] + .with(valueSlot, 3) + .createGuardedComputePipeline(main) + .dispatchThreads(); // add 3 return await result.read() === 4; } diff --git a/apps/typegpu-docs/src/examples/tests/log-test/index.ts b/apps/typegpu-docs/src/examples/tests/log-test/index.ts index e67f79781..c8dcba593 100644 --- a/apps/typegpu-docs/src/examples/tests/log-test/index.ts +++ b/apps/typegpu-docs/src/examples/tests/log-test/index.ts @@ -50,28 +50,28 @@ context.configure({ export const controls = { 'One argument': { onButtonClick: () => - root['~unstable'].prepareDispatch(() => { + root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; console.log(d.u32(321)); }).dispatchThreads(), }, 'Multiple arguments': { onButtonClick: () => - root['~unstable'].prepareDispatch(() => { + root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; console.log(1, d.vec3u(2, 3, 4), 5, 6); }).dispatchThreads(), }, 'String literals': { onButtonClick: () => - root['~unstable'].prepareDispatch(() => { + root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; console.log(2, 'plus', 3, 'equals', 5); }).dispatchThreads(), }, 'Two logs': { onButtonClick: () => - root['~unstable'].prepareDispatch(() => { + root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; console.log('First log.'); console.log('Second log.'); @@ -79,7 +79,7 @@ export const controls = { }, 'Different types': { onButtonClick: () => - root['~unstable'].prepareDispatch(() => { + root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; console.log('--- scalars ---'); console.log(d.f32(3.14)); @@ -128,7 +128,7 @@ export const controls = { const SimpleArray = d.arrayOf(d.u32, 2); const ComplexArray = d.arrayOf(SimpleArray, 3); - root['~unstable'].prepareDispatch(() => { + root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; const simpleStruct = SimpleStruct({ vec: d.vec3u(1, 2, 3), num: 4 }); console.log(simpleStruct); @@ -149,7 +149,7 @@ export const controls = { }, 'Two threads': { onButtonClick: () => - root['~unstable'].prepareDispatch((x) => { + root['~unstable'].createGuardedComputePipeline((x) => { 'use gpu'; console.log('Log from thread', x); }).dispatchThreads(2), @@ -157,7 +157,7 @@ export const controls = { '100 dispatches': { onButtonClick: async () => { const indexUniform = root.createUniform(d.u32); - const test = root['~unstable'].prepareDispatch(() => { + const test = root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; console.log('Log from dispatch', indexUniform.$); }); @@ -170,7 +170,7 @@ export const controls = { 'Varying size logs': { onButtonClick: async () => { const logCountUniform = root.createUniform(d.u32); - const test = root['~unstable'].prepareDispatch(() => { + const test = root['~unstable'].createGuardedComputePipeline(() => { 'use gpu'; for (let i = d.u32(); i < logCountUniform.$; i++) { console.log('Log index', i + 1, 'out of', logCountUniform.$); @@ -230,7 +230,7 @@ export const controls = { }, 'Too many logs': { onButtonClick: () => - root['~unstable'].prepareDispatch((x) => { + root['~unstable'].createGuardedComputePipeline((x) => { 'use gpu'; console.log('Log 1 from thread', x); console.log('Log 2 from thread', x); diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index daf859f51..86856bf60 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -107,8 +107,8 @@ import type { CreateTextureOptions, CreateTextureResult, ExperimentalTgpuRoot, - PreparedDispatch, RenderPass, + TgpuGuardedComputePipeline, TgpuRoot, WithBinding, WithCompute, @@ -137,8 +137,8 @@ const workgroupSizeConfigs = [ vec3u(8, 8, 4), ] as const; -export class PreparedDispatchImpl - implements PreparedDispatch { +export class TgpuGuardedComputePipelineImpl + implements TgpuGuardedComputePipeline { #root: ExperimentalTgpuRoot; #pipeline: TgpuComputePipeline; #sizeUniform: TgpuUniform; @@ -159,12 +159,8 @@ export class PreparedDispatchImpl this.#lastSize = vec3u(); } - /** - * Returns a new PreparedDispatch with the specified bind group bound. - * Analogous to `TgpuComputePipeline.with(bindGroup)`. - */ - with(bindGroup: TgpuBindGroup): PreparedDispatch { - return new PreparedDispatchImpl( + with(bindGroup: TgpuBindGroup): TgpuGuardedComputePipeline { + return new TgpuGuardedComputePipelineImpl( this.#root, this.#pipeline.with(bindGroup), this.#sizeUniform, @@ -172,11 +168,6 @@ export class PreparedDispatchImpl ); } - /** - * Run the prepared dispatch. - * Unlike `TgpuComputePipeline.dispatchWorkgroups()`, - * this method takes in the number of threads to run in each dimension. - */ dispatchThreads(...threads: TArgs): void { const sanitizedSize = toVec3(threads); const workgroupCount = ceil( @@ -220,13 +211,15 @@ class WithBindingImpl implements WithBinding { return new WithComputeImpl(this._getRoot(), this._slotBindings, entryFn); } - prepareDispatch( + createGuardedComputePipeline( callback: (...args: TArgs) => undefined, - ): PreparedDispatch { + ): TgpuGuardedComputePipeline { const root = this._getRoot(); if (callback.length >= 4) { - throw new Error('Dispatch only supports up to three dimensions.'); + throw new Error( + 'Guarded compute callback only supports up to three dimensions.', + ); } const workgroupSize = workgroupSizeConfigs[callback.length] as v3u; @@ -236,8 +229,8 @@ class WithBindingImpl implements WithBinding { const sizeUniform = root.createUniform(vec3u); - // raw WGSL instead of TGSL - // because we do not run unplugin before shipping typegpu package + // WGSL instead of JS because we do not run unplugin + // before shipping the typegpu package const mainCompute = computeFn({ workgroupSize, in: { id: builtin.globalInvocationId }, @@ -252,7 +245,12 @@ class WithBindingImpl implements WithBinding { .withCompute(mainCompute) .createPipeline(); - return new PreparedDispatchImpl(root, pipeline, sizeUniform, workgroupSize); + return new TgpuGuardedComputePipelineImpl( + root, + pipeline, + sizeUniform, + workgroupSize, + ); } withVertex( diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index 1a9d79e2d..2965431d0 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -78,17 +78,20 @@ import type { WgslStorageTexture, WgslTexture } from '../../data/texture.ts'; // Public API // ---------- -export interface PreparedDispatch { +export interface TgpuGuardedComputePipeline { /** - * Returns a new PreparedDispatch with the specified bind group bound. + * Returns a pipeline wrapper with the specified bind group bound. * Analogous to `TgpuComputePipeline.with(bindGroup)`. */ - with(bindGroup: TgpuBindGroup): PreparedDispatch; + with(bindGroup: TgpuBindGroup): TgpuGuardedComputePipeline; /** - * Run the prepared dispatch. - * Unlike `TgpuComputePipeline.dispatchWorkgroups()`, - * this method takes in the number of threads to run in each dimension. + * Dispatches the pipeline. + * Unlike `TgpuComputePipeline.dispatchWorkgroups()`, this method takes in the + * number of threads to run in each dimension. + * + * Under the hood, the number of expected threads is sent as a uniform, and + * "guarded" by a bounds check. */ dispatchThreads(...args: TArgs): void; } @@ -205,39 +208,53 @@ export interface WithBinding { ): WithCompute; /** - * Creates a compute pipeline that executes the given callback. It can accept - * up to 3 parameters (x, y, z) which correspond to the global invocation ID - * of the executing thread. + * Creates a compute pipeline that executes the given callback in an exact number of threads. + * This is different from `createComputePipeline` in that it does a bounds check on the + * thread id, where as regular pipelines do not and work in units of workgroups. * - * @param callback A function converted to WGSL and executed on the GPU. Its arguments correspond to the global invocation IDs. + * @param callback A function converted to WGSL and executed on the GPU. + * It can accept up to 3 parameters (x, y, z) which correspond to the global invocation ID + * of the executing thread. * * @example * If no parameters are provided, the callback will be executed once, in a single thread. * * ```ts - * const action = root.prepareDispatch(() => { - * 'use gpu'; - * console.log('Hello, GPU!'); - * }); + * const fooPipeline = root + * .createGuardedComputePipeline(() => { + * 'use gpu'; + * console.log('Hello, GPU!'); + * }); * - * action.dispatchThreads(); + * fooPipeline.dispatchThreads(); + * // [GPU] Hello, GPU! * ``` * * @example * One parameter means n-threads will be executed in parallel. * * ```ts - * const action = root.prepareDispatch((x) => { - * 'use gpu'; - * console.log('I am the', x, 'thread'); - * }); + * const fooPipeline = root + * .createGuardedComputePipeline((x) => { + * 'use gpu'; + * if (x % 16 === 0) { + * // Logging every 16th thread + * console.log('I am the', x, 'thread'); + * } + * }); * - * action.dispatchThreads(12); // executing 12 threads + * // executing 512 threads + * fooPipeline.dispatchThreads(512); + * // [GPU] I am the 256 thread + * // [GPU] I am the 272 thread + * // ... (30 hidden logs) + * // [GPU] I am the 16 thread + * // [GPU] I am the 240 thread * ``` */ - prepareDispatch( + createGuardedComputePipeline( callback: (...args: TArgs) => void, - ): PreparedDispatch; + ): TgpuGuardedComputePipeline; withVertex< VertexIn extends VertexInConstrained, From 18ae428d33308047ad3c864d478419735b7aa723 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sat, 18 Oct 2025 11:33:10 +0200 Subject: [PATCH 2/8] createComputePipeline API --- .../examples/algorithms/matrix-next/index.ts | 10 +-- .../algorithms/mnist-inference/index.ts | 4 +- .../algorithms/probability/executor.ts | 6 +- .../examples/image-processing/blur/index.ts | 6 +- .../rendering/cubemap-reflection/icosphere.ts | 6 +- .../src/examples/simulation/confetti/index.ts | 6 +- .../simulation/fluid-with-atomics/index.ts | 7 +- .../src/examples/simulation/gravity/index.ts | 6 +- .../simulation/slime-mold-3d/index.ts | 18 +++-- .../examples/simulation/stable-fluid/index.ts | 4 +- .../examples/tests/tgsl-parsing-test/index.ts | 6 +- .../src/perlin-2d/dynamic-cache.ts | 17 ++--- .../src/perlin-2d/static-cache.ts | 19 ++--- .../src/perlin-3d/dynamic-cache.ts | 27 +++----- .../src/perlin-3d/static-cache.ts | 27 +++----- .../src/core/pipeline/computePipeline.ts | 64 ++++++++++------- packages/typegpu/src/core/root/init.ts | 13 +++- packages/typegpu/src/core/root/rootTypes.ts | 9 ++- .../typegpu/tests/computePipeline.test.ts | 69 ++++++++----------- .../typegpu/tests/tgsl/consoleLog.test.ts | 30 +++----- .../typegpu/tests/unplugin/autoname.test.ts | 7 +- packages/unplugin-typegpu/src/common.ts | 3 + 22 files changed, 179 insertions(+), 185 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 13bd5f5fe..71e0b3dd2 100644 --- a/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts @@ -60,10 +60,12 @@ function createPipelines() { state.gpuTime = Number(end - start) / 1_000_000; }; - const optimized = root['~unstable'] - .withCompute(computeSharedMemory) - .createPipeline(); - const simple = root['~unstable'].withCompute(computeSimple).createPipeline(); + const optimized = root['~unstable'].createComputePipeline({ + compute: computeSharedMemory, + }); + const simple = root['~unstable'].createComputePipeline({ + compute: computeSimple, + }); return { 'gpu-optimized': hasTimestampQuery 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 af429dc6b..d66dcd0b5 100644 --- a/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts +++ b/apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts @@ -92,9 +92,9 @@ const subgroupCompute = tgpu['~unstable'].computeFn({ }); const pipelines = { - default: root['~unstable'].withCompute(defaultCompute).createPipeline(), + default: root['~unstable'].createComputePipeline({ compute: defaultCompute }), subgroup: root.enabledFeatures.has('subgroups') - ? root['~unstable'].withCompute(subgroupCompute).createPipeline() + ? root['~unstable'].createComputePipeline({ compute: subgroupCompute }) : null, }; diff --git a/apps/typegpu-docs/src/examples/algorithms/probability/executor.ts b/apps/typegpu-docs/src/examples/algorithms/probability/executor.ts index 75d588e41..d6d78c8d8 100644 --- a/apps/typegpu-docs/src/examples/algorithms/probability/executor.ts +++ b/apps/typegpu-docs/src/examples/algorithms/probability/executor.ts @@ -101,8 +101,7 @@ export class Executor { if (!this.#pipelineCache.has(distribution)) { const pipeline = this.#root['~unstable'] .with(this.#distributionSlot, distribution) - .withCompute(this.#dataMoreWorkersFunc) - .createPipeline(); + .createComputePipeline({ compute: this.#dataMoreWorkersFunc }); this.#pipelineCache.set(distribution, pipeline); } @@ -117,8 +116,7 @@ export class Executor { if (!pipeline) { pipeline = this.#root['~unstable'] .with(this.#distributionSlot, distribution) - .withCompute(this.#dataMoreWorkersFunc as TgpuComputeFn) - .createPipeline(); + .createComputePipeline({ compute: this.#dataMoreWorkersFunc }); this.#pipelineCache.set(distribution, pipeline); } diff --git a/apps/typegpu-docs/src/examples/image-processing/blur/index.ts b/apps/typegpu-docs/src/examples/image-processing/blur/index.ts index 27c923ad0..582ad2363 100644 --- a/apps/typegpu-docs/src/examples/image-processing/blur/index.ts +++ b/apps/typegpu-docs/src/examples/image-processing/blur/index.ts @@ -172,9 +172,9 @@ const ioBindGroups = [ }), ]; -const computePipeline = root['~unstable'] - .withCompute(computeFn) - .createPipeline(); +const computePipeline = root['~unstable'].createComputePipeline({ + compute: computeFn, +}); const renderPipeline = root['~unstable'] .withVertex(fullScreenTriangle, {}) diff --git a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/icosphere.ts b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/icosphere.ts index e313ffd86..89dc24f8b 100644 --- a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/icosphere.ts +++ b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/icosphere.ts @@ -207,9 +207,9 @@ export class IcosphereGenerator { } }); - this.pipeline = this.root['~unstable'] - .withCompute(computeFn) - .createPipeline(); + this.pipeline = this.root['~unstable'].createComputePipeline({ + compute: computeFn, + }); } createIcosphere(subdivisions: number, smooth: boolean): IcosphereBuffer { diff --git a/apps/typegpu-docs/src/examples/simulation/confetti/index.ts b/apps/typegpu-docs/src/examples/simulation/confetti/index.ts index c8fbb6d8a..a0ac44042 100644 --- a/apps/typegpu-docs/src/examples/simulation/confetti/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/confetti/index.ts @@ -166,9 +166,9 @@ const renderPipeline = root['~unstable'] .with(geometryLayout, particleGeometryBuffer) .with(dataLayout, particleDataBuffer); -const computePipeline = root['~unstable'] - .withCompute(mainCompute) - .createPipeline(); +const computePipeline = root['~unstable'].createComputePipeline({ + compute: mainCompute, +}); // compute and draw diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts index 0a51e12ac..f637b95fc 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts @@ -334,9 +334,10 @@ function resetGameData() { decideWaterLevel(input.gid.x, input.gid.y); }); - const computePipeline = root['~unstable'] - .withCompute(compute) - .createPipeline(); + const computePipeline = root['~unstable'].createComputePipeline({ + compute, + }); + const renderPipeline = root['~unstable'] .withVertex(vertex, { squareData: vertexLayout.attrib, diff --git a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts index 51c6d7277..daf7053d6 100644 --- a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts @@ -105,13 +105,11 @@ const dynamicResourcesBox = { // Pipelines const computeCollisionsPipeline = root['~unstable'] - .withCompute(computeCollisionsShader) - .createPipeline(); + .createComputePipeline({ compute: computeCollisionsShader }); const computeGravityPipeline = root['~unstable'] .with(timeAccess, time) - .withCompute(computeGravityShader) - .createPipeline(); + .createComputePipeline({ compute: computeGravityShader }); const skyBoxPipeline = root['~unstable'] .with(filteringSamplerSlot, sampler) diff --git a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts index 7c32217f5..a344353db 100644 --- a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts @@ -457,12 +457,10 @@ const renderPipeline = root['~unstable'] .createPipeline(); const computePipeline = root['~unstable'] - .withCompute(updateAgents) - .createPipeline(); + .createComputePipeline({ compute: updateAgents }); const blurPipeline = root['~unstable'] - .withCompute(blur) - .createPipeline(); + .createComputePipeline({ compute: blur }); const bindGroups = [0, 1].map((i) => root.createBindGroup(computeLayout, { @@ -487,14 +485,16 @@ function frame() { params.writePartial({ deltaTime }); - blurPipeline.with(computeLayout, bindGroups[currentTexture]) + blurPipeline + .with(bindGroups[currentTexture]) .dispatchWorkgroups( Math.ceil(resolution.x / BLUR_WORKGROUP_SIZE[0]), Math.ceil(resolution.y / BLUR_WORKGROUP_SIZE[1]), Math.ceil(resolution.z / BLUR_WORKGROUP_SIZE[2]), ); - computePipeline.with(computeLayout, bindGroups[currentTexture]) + computePipeline + .with(bindGroups[currentTexture]) .dispatchWorkgroups( Math.ceil(NUM_AGENTS / AGENT_WORKGROUP_SIZE), ); @@ -505,10 +505,8 @@ function frame() { loadOp: 'clear', storeOp: 'store', }) - .with( - renderLayout, - renderBindGroups[1 - currentTexture], - ).draw(3); + .with(renderBindGroups[1 - currentTexture]) + .draw(3); root['~unstable'].flush(); 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 b014728fe..31bf610ce 100644 --- a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts @@ -43,7 +43,9 @@ function createField(name: string) { } function createComputePipeline(fn: TgpuComputeFn) { - return root['~unstable'].withCompute(fn).createPipeline(); + return root['~unstable'].createComputePipeline({ + compute: fn, + }); } function toGrid(x: number, y: number): [number, number] { diff --git a/apps/typegpu-docs/src/examples/tests/tgsl-parsing-test/index.ts b/apps/typegpu-docs/src/examples/tests/tgsl-parsing-test/index.ts index f70048d83..71a2ad467 100644 --- a/apps/typegpu-docs/src/examples/tests/tgsl-parsing-test/index.ts +++ b/apps/typegpu-docs/src/examples/tests/tgsl-parsing-test/index.ts @@ -25,9 +25,9 @@ const computeRunTests = tgpu['~unstable'] } }); -const pipeline = root['~unstable'] - .withCompute(computeRunTests) - .createPipeline(); +const pipeline = root['~unstable'].createComputePipeline({ + compute: computeRunTests, +}); async function runTests() { pipeline.dispatchWorkgroups(1); diff --git a/packages/typegpu-noise/src/perlin-2d/dynamic-cache.ts b/packages/typegpu-noise/src/perlin-2d/dynamic-cache.ts index fd270e19b..764fd3ff9 100644 --- a/packages/typegpu-noise/src/perlin-2d/dynamic-cache.ts +++ b/packages/typegpu-noise/src/perlin-2d/dynamic-cache.ts @@ -160,17 +160,15 @@ export function dynamicCacheConfig( memory: { storage: MemorySchema, access: 'mutable' }, }); - const mainCompute = tgpu['~unstable'].computeFn({ - workgroupSize: [1, 1, 1], - in: { gid: d.builtin.globalInvocationId }, - })((input) => { + const mainCompute = (x: number, y: number) => { + 'use gpu'; const size = computeLayout.$.size; - const idx = input.gid.x + input.gid.y * size.x; + const idx = x + y * size.x; computeLayout.$.memory[idx] = computeJunctionGradient( - d.vec2i(input.gid.xy), + d.vec2i(x, y), ); - }); + }; const instance = ( root: TgpuRoot, @@ -184,8 +182,7 @@ export function dynamicCacheConfig( .$usage('uniform'); const computePipeline = root['~unstable'] - .withCompute(mainCompute) - .createPipeline(); + .createGuardedComputePipeline(mainCompute); const createMemory = () => { const memory = root @@ -199,7 +196,7 @@ export function dynamicCacheConfig( computePipeline .with(computeBindGroup) - .dispatchWorkgroups(size.x, size.y); + .dispatchThreads(size.x, size.y); return memory; }; diff --git a/packages/typegpu-noise/src/perlin-2d/static-cache.ts b/packages/typegpu-noise/src/perlin-2d/static-cache.ts index e1aeee5cb..cba941542 100644 --- a/packages/typegpu-noise/src/perlin-2d/static-cache.ts +++ b/packages/typegpu-noise/src/perlin-2d/static-cache.ts @@ -64,20 +64,15 @@ export function staticCache(options: { const memoryReadonly = memoryBuffer.as('readonly'); const memoryMutable = memoryBuffer.as('mutable'); - const mainCompute = tgpu['~unstable'].computeFn({ - workgroupSize: [1, 1, 1], - in: { gid: d.builtin.globalInvocationId }, - })((input) => { - const idx = input.gid.x + input.gid.y * size.x; - - memoryMutable.$[idx] = computeJunctionGradient(d.vec2i(input.gid.xy)); - }); - const computePipeline = root['~unstable'] - .withCompute(mainCompute) - .createPipeline(); + .createGuardedComputePipeline((x, y) => { + 'use gpu'; + const idx = x + y * size.x; + + memoryMutable.$[idx] = computeJunctionGradient(d.vec2i(x, y)); + }); - computePipeline.dispatchWorkgroups(size.x, size.y); + computePipeline.dispatchThreads(size.x, size.y); const getJunctionGradient = tgpu.fn([d.vec2i], d.vec2f)((pos) => { const size_i = d.vec2i(size); diff --git a/packages/typegpu-noise/src/perlin-3d/dynamic-cache.ts b/packages/typegpu-noise/src/perlin-3d/dynamic-cache.ts index 7c501561e..b23949022 100644 --- a/packages/typegpu-noise/src/perlin-3d/dynamic-cache.ts +++ b/packages/typegpu-noise/src/perlin-3d/dynamic-cache.ts @@ -148,12 +148,12 @@ export function dynamicCacheConfig( }); const getJunctionGradient = tgpu.fn([d.vec3i], d.vec3f)((pos) => { - const size = d.vec3i(cleanValuesSlot.value.size.xyz); + const size = d.vec3i(cleanValuesSlot.$.size.xyz); const x = (pos.x % size.x + size.x) % size.x; const y = (pos.y % size.y + size.y) % size.y; const z = (pos.z % size.z + size.z) % size.z; - return cleanValuesSlot.value + return cleanValuesSlot.$ .memory[x + y * size.x + z * size.x * size.y] as d.v3f; }); @@ -162,19 +162,15 @@ export function dynamicCacheConfig( memory: { storage: MemorySchema, access: 'mutable' }, }); - const mainCompute = tgpu['~unstable'].computeFn({ - workgroupSize: [1, 1, 1], - in: { gid: d.builtin.globalInvocationId }, - })((input) => { + const mainCompute = (x: number, y: number, z: number) => { + 'use gpu'; const size = computeLayout.$.size; - const idx = input.gid.x + - input.gid.y * size.x + - input.gid.z * size.x * size.y; + const idx = x + + y * size.x + + z * size.x * size.y; - computeLayout.$.memory[idx] = computeJunctionGradient( - d.vec3i(input.gid.xyz), - ); - }); + computeLayout.$.memory[idx] = computeJunctionGradient(d.vec3i(x, y, z)); + }; const instance = ( root: TgpuRoot, @@ -188,8 +184,7 @@ export function dynamicCacheConfig( .$usage('uniform'); const computePipeline = root['~unstable'] - .withCompute(mainCompute) - .createPipeline(); + .createGuardedComputePipeline(mainCompute); const createMemory = () => { const memory = root @@ -203,7 +198,7 @@ export function dynamicCacheConfig( computePipeline .with(computeBindGroup) - .dispatchWorkgroups(size.x, size.y, size.z); + .dispatchThreads(size.x, size.y, size.z); return memory; }; diff --git a/packages/typegpu-noise/src/perlin-3d/static-cache.ts b/packages/typegpu-noise/src/perlin-3d/static-cache.ts index 7723ae3a4..6419bf435 100644 --- a/packages/typegpu-noise/src/perlin-3d/static-cache.ts +++ b/packages/typegpu-noise/src/perlin-3d/static-cache.ts @@ -65,24 +65,17 @@ export function staticCache(options: { const memoryReadonly = memoryBuffer.as('readonly'); const memoryMutable = memoryBuffer.as('mutable'); - const mainCompute = tgpu['~unstable'].computeFn({ - workgroupSize: [1, 1, 1], - in: { gid: d.builtin.globalInvocationId }, - })((input) => { - const idx = input.gid.x + - input.gid.y * size.x + - input.gid.z * size.x * size.y; - - memoryMutable.value[idx] = computeJunctionGradient( - d.vec3i(input.gid.xyz), - ); - }); - const computePipeline = root['~unstable'] - .withCompute(mainCompute) - .createPipeline(); + .createGuardedComputePipeline((x, y, z) => { + 'use gpu'; + const idx = x + + y * size.x + + z * size.x * size.y; + + memoryMutable.$[idx] = computeJunctionGradient(d.vec3i(x, y, z)); + }); - computePipeline.dispatchWorkgroups(size.x, size.y, size.z); + computePipeline.dispatchThreads(size.x, size.y, size.z); const getJunctionGradient = tgpu.fn([d.vec3i], d.vec3f)((pos) => { const size_i = d.vec3i(size); @@ -91,7 +84,7 @@ export function staticCache(options: { const z = (pos.z % size_i.z + size_i.z) % size_i.z; return memoryReadonly - .value[x + y * size_i.x + z * size_i.x * size_i.y] as d.v3f; + .$[x + y * size_i.x + z * size_i.x * size_i.y] as d.v3f; }); return { diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index 7abe63512..1854ec91e 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -1,3 +1,4 @@ +import type { AnyComputeBuiltin } from '../../builtin.ts'; import type { TgpuQuerySet } from '../../core/querySet/querySet.ts'; import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import { Void } from '../../data/wgslTypes.ts'; @@ -19,6 +20,7 @@ import { wgslExtensions, wgslExtensionToFeatureName, } from '../../wgslExtensions.ts'; +import type { IORecord } from '../function/fnTypes.ts'; import type { TgpuComputeFn } from '../function/tgpuComputeFn.ts'; import { namespace } from '../resolve/namespace.ts'; import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; @@ -42,6 +44,12 @@ interface ComputePipelineInternals { // Public API // ---------- +export type TgpuComputePipelineDescriptor< + Input extends IORecord = IORecord, +> = { + compute: TgpuComputeFn; +}; + export interface TgpuComputePipeline extends TgpuNamable, SelfResolvable, Timeable { readonly [$internal]: ComputePipelineInternals; @@ -67,10 +75,10 @@ export interface TgpuComputePipeline export function INTERNAL_createComputePipeline( branch: ExperimentalTgpuRoot, slotBindings: [TgpuSlot, unknown][], - entryFn: TgpuComputeFn, + descriptor: TgpuComputePipelineDescriptor, ) { return new TgpuComputePipelineImpl( - new ComputePipelineCore(branch, slotBindings, entryFn), + new ComputePipelineCore(branch, slotBindings, descriptor), {}, ); } @@ -107,7 +115,7 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { return _priors; }, get branch() { - return _core.branch; + return _core.root; }, }; this[$getNameForward] = _core; @@ -159,7 +167,7 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { const newPriors = createWithPerformanceCallback( this._priors, callback, - this._core.branch, + this._core.root, ); return new TgpuComputePipelineImpl(this._core, newPriors) as this; } @@ -172,7 +180,7 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { const newPriors = createWithTimestampWrites( this._priors, options, - this._core.branch, + this._core.root, ); return new TgpuComputePipelineImpl(this._core, newPriors) as this; } @@ -183,14 +191,14 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { z?: number | undefined, ): void { const memo = this._core.unwrap(); - const { branch } = this._core; + const { root } = this._core; const passDescriptor: GPUComputePassDescriptor = { label: getName(this._core) ?? '', - ...setupTimestampWrites(this._priors, branch), + ...setupTimestampWrites(this._priors, root), }; - const pass = branch.commandEncoder.beginComputePass(passDescriptor); + const pass = root.commandEncoder.beginComputePass(passDescriptor); pass.setPipeline(memo.pipeline); @@ -199,13 +207,13 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { memo.usedBindGroupLayouts.forEach((layout, idx) => { if (memo.catchall && idx === memo.catchall[0]) { // Catch-all - pass.setBindGroup(idx, branch.unwrap(memo.catchall[1])); + pass.setBindGroup(idx, root.unwrap(memo.catchall[1])); missingBindGroups.delete(layout); } else { const bindGroup = this._priors.bindGroupLayoutMap?.get(layout); if (bindGroup !== undefined) { missingBindGroups.delete(layout); - pass.setBindGroup(idx, branch.unwrap(bindGroup)); + pass.setBindGroup(idx, root.unwrap(bindGroup)); } } }); @@ -223,7 +231,7 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { if (this._priors.performanceCallback) { triggerPerformanceCallback({ - root: branch, + root, priors: this._priors, }); } @@ -239,15 +247,21 @@ class ComputePipelineCore implements SelfResolvable { readonly [$internal] = true; private _memo: Memo | undefined; + #slotBindings: [TgpuSlot, unknown][]; + #descriptor: TgpuComputePipelineDescriptor; + constructor( - public readonly branch: ExperimentalTgpuRoot, - private readonly _slotBindings: [TgpuSlot, unknown][], - private readonly _entryFn: TgpuComputeFn, - ) {} + public readonly root: ExperimentalTgpuRoot, + slotBindings: [TgpuSlot, unknown][], + descriptor: TgpuComputePipelineDescriptor, + ) { + this.#slotBindings = slotBindings; + this.#descriptor = descriptor; + } [$resolve](ctx: ResolutionCtx) { - return ctx.withSlots(this._slotBindings, () => { - ctx.resolve(this._entryFn); + return ctx.withSlots(this.#slotBindings, () => { + ctx.resolve(this.#descriptor.compute); return snip('', Void); }); } @@ -258,23 +272,23 @@ class ComputePipelineCore implements SelfResolvable { public unwrap(): Memo { if (this._memo === undefined) { - const device = this.branch.device; + const device = this.root.device; const enableExtensions = wgslExtensions.filter((extension) => - this.branch.enabledFeatures.has(wgslExtensionToFeatureName[extension]) + this.root.enabledFeatures.has(wgslExtensionToFeatureName[extension]) ); // Resolving code let resolutionResult: ResolutionResult; let resolveMeasure: PerformanceMeasure | undefined; - const ns = namespace({ names: this.branch.nameRegistrySetting }); + 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.branch.shaderGenerator, - root: this.branch, + shaderGenerator: this.root.shaderGenerator, + root: this.root, }); resolveMeasure = performance.measure('typegpu:resolution', { start: resolveStart.name, @@ -283,8 +297,8 @@ class ComputePipelineCore implements SelfResolvable { resolutionResult = resolve(this, { namespace: ns, enableExtensions, - shaderGenerator: this.branch.shaderGenerator, - root: this.branch, + shaderGenerator: this.root.shaderGenerator, + root: this.root, }); } @@ -308,7 +322,7 @@ class ComputePipelineCore implements SelfResolvable { layout: device.createPipelineLayout({ label: `${getName(this) ?? ''} - Pipeline Layout`, bindGroupLayouts: usedBindGroupLayouts.map((l) => - this.branch.unwrap(l) + this.root.unwrap(l) ), }), compute: { module }, diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index 86856bf60..a434529bc 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -62,6 +62,7 @@ import type { TgpuVertexFn } from '../function/tgpuVertexFn.ts'; import { INTERNAL_createComputePipeline, type TgpuComputePipeline, + type TgpuComputePipelineDescriptor, } from '../pipeline/computePipeline.ts'; import { type AnyFragmentTargets, @@ -211,6 +212,16 @@ class WithBindingImpl implements WithBinding { return new WithComputeImpl(this._getRoot(), this._slotBindings, entryFn); } + createComputePipeline( + descriptor: TgpuComputePipelineDescriptor, + ): TgpuComputePipeline { + return INTERNAL_createComputePipeline( + this._getRoot(), + this._slotBindings, + descriptor, + ); + } + createGuardedComputePipeline( callback: (...args: TArgs) => undefined, ): TgpuGuardedComputePipeline { @@ -288,7 +299,7 @@ class WithComputeImpl implements WithCompute { return INTERNAL_createComputePipeline( this._root, this._slotBindings, - this._entryFn, + { compute: this._entryFn }, ); } } diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index 2965431d0..7916df888 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -62,7 +62,10 @@ import type { VertexInConstrained, VertexOutConstrained, } from '../function/tgpuVertexFn.ts'; -import type { TgpuComputePipeline } from '../pipeline/computePipeline.ts'; +import type { + TgpuComputePipeline, + TgpuComputePipelineDescriptor, +} from '../pipeline/computePipeline.ts'; import type { FragmentOutToTargets, TgpuRenderPipeline, @@ -207,6 +210,10 @@ export interface WithBinding { entryFn: TgpuComputeFn, ): WithCompute; + createComputePipeline>( + descriptor: TgpuComputePipelineDescriptor, + ): TgpuComputePipeline; + /** * Creates a compute pipeline that executes the given callback in an exact number of threads. * This is different from `createComputePipeline` in that it does a bounds check on the diff --git a/packages/typegpu/tests/computePipeline.test.ts b/packages/typegpu/tests/computePipeline.test.ts index 29234f77d..f93dea99f 100644 --- a/packages/typegpu/tests/computePipeline.test.ts +++ b/packages/typegpu/tests/computePipeline.test.ts @@ -16,9 +16,9 @@ describe('TgpuComputePipeline', () => { // do something }); - const computePipeline = root - .withCompute(entryFn) - .createPipeline(); + const computePipeline = root.createComputePipeline({ + compute: entryFn, + }); expectTypeOf(computePipeline).toEqualTypeOf(); @@ -40,7 +40,9 @@ describe('TgpuComputePipeline', () => { layout.bound.alpha; // Using an entry of the layout }); - const pipeline = root.withCompute(entryFn).createPipeline(); + const pipeline = root.createComputePipeline({ + compute: entryFn, + }); expect(() => pipeline.dispatchWorkgroups(1)).toThrowError( new MissingBindGroupsError([layout]), @@ -58,9 +60,9 @@ describe('TgpuComputePipeline', () => { // do something }); - const computePipeline = root - .withCompute(main) - .createPipeline(); + const computePipeline = root.createComputePipeline({ + compute: main, + }); expect(asWgsl(computePipeline)).toMatchInlineSnapshot(` "@compute @workgroup_size(32) fn main() { @@ -74,9 +76,9 @@ describe('TgpuComputePipeline', () => { .computeFn({ workgroupSize: [32] })(() => { // do something }); - const computePipeline = root - .withCompute(main) - .createPipeline(); + const computePipeline = root.createComputePipeline({ + compute: main, + }); const layout1 = tgpu.bindGroupLayout({ buf: { uniform: d.u32 } }); const bindGroup1 = root.createBindGroup(layout1, { @@ -101,8 +103,7 @@ describe('TgpuComputePipeline', () => { const callback = vi.fn(); const pipeline = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withPerformanceCallback(callback); expect(pipeline).toBeDefined(); @@ -123,8 +124,7 @@ describe('TgpuComputePipeline', () => { const callback = vi.fn(); const pipeline = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withPerformanceCallback(callback); const timestampWrites = pipeline[$internal].priors.timestampWrites; @@ -147,8 +147,7 @@ describe('TgpuComputePipeline', () => { const callback2 = vi.fn(); const pipeline = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withPerformanceCallback(callback1) .withPerformanceCallback(callback2); @@ -175,8 +174,7 @@ describe('TgpuComputePipeline', () => { expect(() => { root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withPerformanceCallback(callback); }).toThrow( 'Performance callback requires the "timestamp-query" feature to be enabled on GPU device.', @@ -196,8 +194,7 @@ describe('TgpuComputePipeline', () => { const querySet = root.createQuerySet('timestamp', 4); const pipeline = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withTimestampWrites({ querySet, beginningOfPassWriteIndex: 0, @@ -224,8 +221,7 @@ describe('TgpuComputePipeline', () => { }); const pipeline = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withTimestampWrites({ querySet: rawQuerySet, beginningOfPassWriteIndex: 2, @@ -248,24 +244,21 @@ describe('TgpuComputePipeline', () => { const querySet = root.createQuerySet('timestamp', 4); const pipeline1 = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withTimestampWrites({ querySet, beginningOfPassWriteIndex: 0, }); const pipeline2 = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withTimestampWrites({ querySet, endOfPassWriteIndex: 1, }); const pipeline3 = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withTimestampWrites({ querySet, }); @@ -301,8 +294,7 @@ describe('TgpuComputePipeline', () => { const querySet = root.createQuerySet('timestamp', 4); const pipeline = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withTimestampWrites({ querySet, beginningOfPassWriteIndex: 1, @@ -342,8 +334,8 @@ describe('TgpuComputePipeline', () => { const querySet = root.createQuerySet('timestamp', 4); const pipeline = root - .withCompute(entryFn) - .createPipeline().withTimestampWrites({ + .createComputePipeline({ compute: entryFn }) + .withTimestampWrites({ querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1, @@ -351,8 +343,7 @@ describe('TgpuComputePipeline', () => { .with(bindGroup); const pipeline2 = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .with(bindGroup) .withTimestampWrites({ querySet, @@ -393,8 +384,7 @@ describe('TgpuComputePipeline', () => { const callback = vi.fn(); const pipeline = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withTimestampWrites({ querySet, beginningOfPassWriteIndex: 3, @@ -437,8 +427,7 @@ describe('TgpuComputePipeline', () => { const callback = vi.fn(); let pipeline = root - .withCompute(entryFn) - .createPipeline() + .createComputePipeline({ compute: entryFn }) .withPerformanceCallback(callback); const autoQuerySet = pipeline[$internal].priors.timestampWrites?.querySet; @@ -483,7 +472,7 @@ describe('TgpuComputePipeline', () => { const a = d.arrayOf(d.f32, 3)(); }); - const pipeline = root['~unstable'].withCompute(fn).createPipeline(); + const pipeline = root['~unstable'].createComputePipeline({ compute: fn }); pipeline.dispatchWorkgroups(1); @@ -528,7 +517,7 @@ describe('TgpuComputePipeline', () => { } }); - const pipeline = root['~unstable'].withCompute(fn).createPipeline(); + const pipeline = root['~unstable'].createComputePipeline({ compute: fn }); pipeline.dispatchWorkgroups(1); diff --git a/packages/typegpu/tests/tgsl/consoleLog.test.ts b/packages/typegpu/tests/tgsl/consoleLog.test.ts index b39c5fa12..e9c85ac19 100644 --- a/packages/typegpu/tests/tgsl/consoleLog.test.ts +++ b/packages/typegpu/tests/tgsl/consoleLog.test.ts @@ -116,9 +116,7 @@ describe('wgslGenerator with console.log', () => { console.log(d.u32(10)); }); - const pipeline = root['~unstable'] - .withCompute(fn) - .createPipeline(); + const pipeline = root['~unstable'].createComputePipeline({ compute: fn }); expect(asWgsl(pipeline)).toMatchInlineSnapshot(` "@group(0) @binding(0) var indexBuffer: atomic; @@ -178,9 +176,9 @@ describe('wgslGenerator with console.log', () => { console.log(d.u32(20)); }); - const pipeline = root['~unstable'] - .withCompute(fn) - .createPipeline(); + const pipeline = root['~unstable'].createComputePipeline({ + compute: fn, + }); expect(asWgsl(pipeline)).toMatchInlineSnapshot(` "@group(0) @binding(0) var indexBuffer: atomic; @@ -260,12 +258,12 @@ describe('wgslGenerator with console.log', () => { ); }); - const pipeline = root['~unstable'] - .withCompute(fn) - .createPipeline(); + const pipeline = root['~unstable'].createComputePipeline({ + compute: fn, + }); expect(asWgsl(pipeline)).toMatchInlineSnapshot(` - + "@group(0) @binding(0) var indexBuffer: atomic; struct SerializedLogData { @@ -343,9 +341,7 @@ describe('wgslGenerator with console.log', () => { console.log(complexStruct); }); - const pipeline = root['~unstable'] - .withCompute(fn) - .createPipeline(); + const pipeline = root['~unstable'].createComputePipeline({ compute: fn }); expect(asWgsl(pipeline)).toMatchInlineSnapshot(` "struct SimpleStruct { @@ -465,9 +461,7 @@ describe('wgslGenerator with console.log', () => { console.log(complexStruct); }); - const pipeline = root['~unstable'] - .withCompute(fn) - .createPipeline(); + const pipeline = root['~unstable'].createComputePipeline({ compute: fn }); expect(asWgsl(pipeline)).toMatchInlineSnapshot(` "struct SimpleStruct { @@ -592,9 +586,7 @@ describe('wgslGenerator with console.log', () => { ); }); - const pipeline = root['~unstable'] - .withCompute(fn) - .createPipeline(); + const pipeline = root['~unstable'].createComputePipeline({ compute: fn }); expect(() => asWgsl(pipeline)).toThrowErrorMatchingInlineSnapshot(` [Error: Resolution of the following tree failed: diff --git a/packages/typegpu/tests/unplugin/autoname.test.ts b/packages/typegpu/tests/unplugin/autoname.test.ts index 986e36039..ced2e9cdb 100644 --- a/packages/typegpu/tests/unplugin/autoname.test.ts +++ b/packages/typegpu/tests/unplugin/autoname.test.ts @@ -55,10 +55,9 @@ describe('autonaming', () => { it("autonames resources created using root['~unstable']", ({ root }) => { const myPipeline = root['~unstable'] - .withCompute( - tgpu['~unstable'].computeFn({ workgroupSize: [1] })(() => {}), - ) - .createPipeline(); + .createComputePipeline({ + compute: tgpu['~unstable'].computeFn({ workgroupSize: [1] })(() => {}), + }); const myTexture = root['~unstable'].createTexture({ size: [1, 1], format: 'rgba8unorm', diff --git a/packages/unplugin-typegpu/src/common.ts b/packages/unplugin-typegpu/src/common.ts index 241a9f770..6a0d84ba3 100644 --- a/packages/unplugin-typegpu/src/common.ts +++ b/packages/unplugin-typegpu/src/common.ts @@ -163,6 +163,9 @@ const resourceConstructors: string[] = [ 'createQuerySet', // root['~unstable'] 'createPipeline', + 'createComputePipeline', + 'createGuardedComputePipeline', + 'createRenderPipeline', 'createTexture', 'createSampler', 'createComparisonSampler', From bb414f3dda60d9cd0e51d688e3fce5fe14d09fcb Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sat, 18 Oct 2025 12:46:30 +0200 Subject: [PATCH 3/8] createRenderPipeline --- .../image-processing/ascii-filter/index.ts | 9 ++- .../examples/image-processing/blur/index.ts | 11 +-- .../src/examples/rendering/3d-fish/index.ts | 16 ++-- .../rendering/box-raytracing/index.ts | 17 +++-- .../src/examples/rendering/caustics/index.ts | 9 ++- .../rendering/cubemap-reflection/index.ts | 28 ++++--- .../src/examples/rendering/disco/index.ts | 8 +- .../examples/rendering/perlin-noise/index.ts | 16 ++-- .../rendering/phong-reflection/index.ts | 16 ++-- .../examples/rendering/ray-marching/index.ts | 9 ++- .../examples/rendering/simple-shadow/index.ts | 43 ++++++----- .../src/examples/rendering/two-boxes/index.ts | 19 +++-- .../rendering/xor-dev-centrifuge-2/index.ts | 9 ++- .../rendering/xor-dev-runner/index.ts | 9 ++- .../src/examples/simple/liquid-glass/index.ts | 11 ++- .../src/examples/simple/oklab/index.ts | 21 +++--- .../src/examples/simple/square/index.ts | 9 ++- .../src/examples/simple/triangle/index.ts | 8 +- .../src/examples/simple/vaporrave/index.ts | 16 ++-- .../examples/simulation/boids-next/index.ts | 17 +++-- .../src/examples/simulation/confetti/index.ts | 26 ++++--- .../fluid-double-buffering/index.ts | 11 ++- .../simulation/fluid-with-atomics/index.ts | 22 +++--- .../src/examples/simulation/gravity/index.ts | 31 +++++--- .../simulation/slime-mold-3d/index.ts | 9 ++- .../examples/simulation/stable-fluid/index.ts | 14 ++-- .../src/examples/tests/log-test/index.ts | 18 +++-- .../src/examples/tests/texture-test/index.ts | 11 +-- .../src/core/pipeline/renderPipeline.ts | 74 ++++++++++++++++++- packages/typegpu/src/core/root/rootTypes.ts | 25 +++++++ packages/typegpu/src/index.ts | 13 +++- packages/typegpu/src/shared/utilityTypes.ts | 12 +++ 32 files changed, 370 insertions(+), 197 deletions(-) diff --git a/apps/typegpu-docs/src/examples/image-processing/ascii-filter/index.ts b/apps/typegpu-docs/src/examples/image-processing/ascii-filter/index.ts index ba262a792..29df62abf 100644 --- a/apps/typegpu-docs/src/examples/image-processing/ascii-filter/index.ts +++ b/apps/typegpu-docs/src/examples/image-processing/ascii-filter/index.ts @@ -176,10 +176,11 @@ context.configure({ alphaMode: 'premultiplied', }); -const pipeline = root['~unstable'] - .withVertex(fullScreenTriangle, {}) - .withFragment(fragmentFn, { format: presentationFormat }) - .createPipeline(); +const pipeline = root['~unstable'].createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: fragmentFn, + targets: { format: presentationFormat }, +}); if (navigator.mediaDevices.getUserMedia) { video.srcObject = await navigator.mediaDevices.getUserMedia({ diff --git a/apps/typegpu-docs/src/examples/image-processing/blur/index.ts b/apps/typegpu-docs/src/examples/image-processing/blur/index.ts index 582ad2363..6cb4ef9cb 100644 --- a/apps/typegpu-docs/src/examples/image-processing/blur/index.ts +++ b/apps/typegpu-docs/src/examples/image-processing/blur/index.ts @@ -176,10 +176,11 @@ const computePipeline = root['~unstable'].createComputePipeline({ compute: computeFn, }); -const renderPipeline = root['~unstable'] - .withVertex(fullScreenTriangle, {}) - .withFragment(renderFragment, { format: presentationFormat }) - .createPipeline(); +const renderPipeline = root['~unstable'].createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: renderFragment, + targets: { format: presentationFormat }, +}); function render() { settingsUniform.write({ @@ -191,7 +192,7 @@ function render() { for (const i of indices) { computePipeline - .with(ioLayout, ioBindGroups[i]) + .with(ioBindGroups[i]) .dispatchWorkgroups( Math.ceil(srcWidth / settings.blockDim), Math.ceil(srcHeight / 4), diff --git a/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts b/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts index 335b9d192..3f173482b 100644 --- a/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts @@ -182,16 +182,18 @@ randomizeFishPositions(); // pipelines -const renderPipeline = root['~unstable'] - .withVertex(vertexShader, modelVertexLayout.attrib) - .withFragment(fragmentShader, { format: presentationFormat }) - .withDepthStencil({ +const renderPipeline = root['~unstable'].createRenderPipeline({ + attribs: modelVertexLayout.attrib, + vertex: vertexShader, + fragment: fragmentShader, + targets: { format: presentationFormat }, + + depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less', - }) - .withPrimitive({ topology: 'triangle-list' }) - .createPipeline(); + }, +}); let depthTexture = root.device.createTexture({ size: [canvas.width, canvas.height, 1], diff --git a/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.ts b/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.ts index 77219c318..5692258c2 100644 --- a/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.ts @@ -265,9 +265,13 @@ const fragmentFunction = tgpu['~unstable'].fragmentFn({ // pipeline -const pipeline = root['~unstable'] - .withVertex(mainVertex, {}) - .withFragment(fragmentFunction, { +const pipeline = root['~unstable'].createRenderPipeline({ + primitive: { + topology: 'triangle-strip', + }, + vertex: mainVertex, + fragment: fragmentFunction, + targets: { format: presentationFormat, blend: { color: { @@ -281,11 +285,8 @@ const pipeline = root['~unstable'] operation: 'add', }, }, - }) - .withPrimitive({ - topology: 'triangle-strip', - }) - .createPipeline(); + }, +}); // UI diff --git a/apps/typegpu-docs/src/examples/rendering/caustics/index.ts b/apps/typegpu-docs/src/examples/rendering/caustics/index.ts index c6a3d844b..50f129d7e 100644 --- a/apps/typegpu-docs/src/examples/rendering/caustics/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/caustics/index.ts @@ -139,10 +139,11 @@ context.configure({ alphaMode: 'premultiplied', }); -const pipeline = root['~unstable'] - .withVertex(mainVertex, {}) - .withFragment(mainFragment, { format: presentationFormat }) - .createPipeline(); +const pipeline = root['~unstable'].createRenderPipeline({ + vertex: mainVertex, + fragment: mainFragment, + targets: { format: presentationFormat }, +}); let isRunning = true; diff --git a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts index 79d246048..d8566a54e 100644 --- a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts @@ -273,21 +273,25 @@ const cubeFragmentFn = tgpu['~unstable'].fragmentFn({ // Pipeline Setup -const cubePipeline = root['~unstable'] - .withVertex(cubeVertexFn, cubeVertexLayout.attrib) - .withFragment(cubeFragmentFn, { format: presentationFormat }) - .withPrimitive({ +const cubePipeline = root['~unstable'].createRenderPipeline({ + attribs: cubeVertexLayout.attrib, + vertex: cubeVertexFn, + fragment: cubeFragmentFn, + targets: { format: presentationFormat }, + primitive: { cullMode: 'front', - }) - .createPipeline(); + }, +}); -const pipeline = root['~unstable'] - .withVertex(vertexFn, vertexLayout.attrib) - .withFragment(fragmentFn, { format: presentationFormat }) - .withPrimitive({ +const pipeline = root['~unstable'].createRenderPipeline({ + attribs: vertexLayout.attrib, + vertex: vertexFn, + fragment: fragmentFn, + targets: { format: presentationFormat }, + primitive: { cullMode: 'back', - }) - .createPipeline(); + }, +}); // Render Functions diff --git a/apps/typegpu-docs/src/examples/rendering/disco/index.ts b/apps/typegpu-docs/src/examples/rendering/disco/index.ts index f2ecad00a..6e6d32ed1 100644 --- a/apps/typegpu-docs/src/examples/rendering/disco/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/disco/index.ts @@ -45,9 +45,11 @@ const pipelines = fragmentShaders.map((fragment) => root['~unstable'] .with(timeAccess, time) .with(resolutionAccess, resolutionUniform) - .withVertex(mainVertex, {}) - .withFragment(fragment, { format: presentationFormat }) - .createPipeline() + .createRenderPipeline({ + vertex: mainVertex, + fragment: fragment, + targets: { format: presentationFormat }, + }) ); let currentPipeline = pipelines[0]; diff --git a/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts b/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts index 9ed8e8ee1..1d917e2d8 100644 --- a/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts @@ -91,14 +91,18 @@ const renderPipelineBase = root['~unstable'] const renderPipelines = { exponential: renderPipelineBase .with(sharpenFnSlot, exponentialSharpen) - .withVertex(fullScreenTriangle, {}) - .withFragment(mainFragment, { format: presentationFormat }) - .createPipeline(), + .createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: mainFragment, + targets: { format: presentationFormat }, + }), tanh: renderPipelineBase .with(sharpenFnSlot, tanhSharpen) - .withVertex(fullScreenTriangle, {}) - .withFragment(mainFragment, { format: presentationFormat }) - .createPipeline(), + .createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: mainFragment, + targets: { format: presentationFormat }, + }), }; let activeSharpenFn: 'exponential' | 'tanh' = 'exponential'; diff --git a/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.ts b/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.ts index fc98284da..61ac12b1a 100644 --- a/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.ts @@ -102,16 +102,18 @@ export const fragmentShader = tgpu['~unstable'].fragmentFn({ }); // pipelines -const renderPipeline = root['~unstable'] - .withVertex(vertexShader, modelVertexLayout.attrib) - .withFragment(fragmentShader, { format: presentationFormat }) - .withDepthStencil({ +const renderPipeline = root['~unstable'].createRenderPipeline({ + attribs: modelVertexLayout.attrib, + vertex: vertexShader, + fragment: fragmentShader, + targets: { format: presentationFormat }, + + depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less', - }) - .withPrimitive({ topology: 'triangle-list' }) - .createPipeline(); + }, +}); let depthTexture = root.device.createTexture({ size: [canvas.width, canvas.height, 1], diff --git a/apps/typegpu-docs/src/examples/rendering/ray-marching/index.ts b/apps/typegpu-docs/src/examples/rendering/ray-marching/index.ts index 798e0579d..b565cea23 100644 --- a/apps/typegpu-docs/src/examples/rendering/ray-marching/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/ray-marching/index.ts @@ -243,10 +243,11 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ return std.mix(d.vec4f(finalColor, 1), skyColor, fog); }); -const renderPipeline = root['~unstable'] - .withVertex(vertexMain, {}) - .withFragment(fragmentMain, { format: presentationFormat }) - .createPipeline(); +const renderPipeline = root['~unstable'].createRenderPipeline({ + vertex: vertexMain, + fragment: fragmentMain, + targets: { format: presentationFormat }, +}); let animationFrame: number; function run(timestamp: number) { diff --git a/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.ts b/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.ts index a2609a578..18116dd52 100644 --- a/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.ts @@ -251,36 +251,41 @@ const mainFrag = tgpu['~unstable'].fragmentFn({ // Pipelines const vertexLayout = tgpu.vertexLayout(d.arrayOf(VertexInfo)); -const pipeline = root['~unstable'] - .withVertex(mainVert, vertexLayout.attrib) - .withFragment(mainFrag, { format: presentationFormat }) - .withDepthStencil({ +const pipeline = root['~unstable'].createRenderPipeline({ + attribs: vertexLayout.attrib, + vertex: mainVert, + fragment: mainFrag, + targets: { format: presentationFormat }, + + primitive: { + cullMode: 'back', + }, + depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less', - }) - .withMultisample({ + }, + multisample: { count: 4, - }) - .withPrimitive({ - cullMode: 'back', - }) - .createPipeline(); + }, +}); -const shadowPipeline = root['~unstable'] - .withVertex(shadowVert, vertexLayout.attrib) - .withDepthStencil({ +const shadowPipeline = root['~unstable'].createRenderPipeline({ + attribs: vertexLayout.attrib, + vertex: shadowVert, + + primitive: { + cullMode: 'back', + }, + depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less', depthBias: 1, depthBiasSlopeScale: 4, depthBiasClamp: 0, - }) - .withPrimitive({ - cullMode: 'back', - }) - .createPipeline(); + }, +}); function updateLightDirection(dir: d.v3f) { currentLightDirection = dir; diff --git a/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts b/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts index fb21379bd..bb1883061 100644 --- a/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts @@ -253,18 +253,21 @@ const fragment = tgpu['~unstable'].fragmentFn({ out: d.vec4f, })((input) => input.color); -const pipeline = root['~unstable'] - .withVertex(vertex, vertexLayout.attrib) - .withFragment(fragment, { format: presentationFormat }) - .withDepthStencil({ +const pipeline = root['~unstable'].createRenderPipeline({ + attribs: vertexLayout.attrib, + vertex, + fragment, + targets: { format: presentationFormat }, + + depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less', - }) - .withMultisample({ + }, + multisample: { count: 4, - }) - .createPipeline(); + }, +}); // Render Loop diff --git a/apps/typegpu-docs/src/examples/rendering/xor-dev-centrifuge-2/index.ts b/apps/typegpu-docs/src/examples/rendering/xor-dev-centrifuge-2/index.ts index e54963fdd..945efa0f0 100644 --- a/apps/typegpu-docs/src/examples/rendering/xor-dev-centrifuge-2/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/xor-dev-centrifuge-2/index.ts @@ -104,10 +104,11 @@ context.configure({ alphaMode: 'premultiplied', }); -const pipeline = root['~unstable'] - .withVertex(vertexMain, {}) - .withFragment(fragmentMain, { format: presentationFormat }) - .createPipeline(); +const pipeline = root['~unstable'].createRenderPipeline({ + vertex: vertexMain, + fragment: fragmentMain, + targets: { format: presentationFormat }, +}); let isRunning = true; diff --git a/apps/typegpu-docs/src/examples/rendering/xor-dev-runner/index.ts b/apps/typegpu-docs/src/examples/rendering/xor-dev-runner/index.ts index 4ec15685e..9eaf0737e 100644 --- a/apps/typegpu-docs/src/examples/rendering/xor-dev-runner/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/xor-dev-runner/index.ts @@ -121,10 +121,11 @@ context.configure({ alphaMode: 'premultiplied', }); -const pipeline = root['~unstable'] - .withVertex(vertexMain, {}) - .withFragment(fragmentMain, { format: presentationFormat }) - .createPipeline(); +const pipeline = root['~unstable'].createRenderPipeline({ + vertex: vertexMain, + fragment: fragmentMain, + targets: { format: presentationFormat }, +}); let isRunning = true; diff --git a/apps/typegpu-docs/src/examples/simple/liquid-glass/index.ts b/apps/typegpu-docs/src/examples/simple/liquid-glass/index.ts index 1c454f583..98ad73068 100644 --- a/apps/typegpu-docs/src/examples/simple/liquid-glass/index.ts +++ b/apps/typegpu-docs/src/examples/simple/liquid-glass/index.ts @@ -165,12 +165,11 @@ const fragmentShader = tgpu['~unstable'].fragmentFn({ .add(normalSample.mul(weights.outside)); }); -const pipeline = root['~unstable'] - .withVertex(fullScreenTriangle, {}) - .withFragment(fragmentShader, { - format: presentationFormat, - }) - .createPipeline(); +const pipeline = root['~unstable'].createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: fragmentShader, + targets: { format: presentationFormat }, +}); let isRectangleFixed = false; diff --git a/apps/typegpu-docs/src/examples/simple/oklab/index.ts b/apps/typegpu-docs/src/examples/simple/oklab/index.ts index 0e47c77de..3ca088b71 100644 --- a/apps/typegpu-docs/src/examples/simple/oklab/index.ts +++ b/apps/typegpu-docs/src/examples/simple/oklab/index.ts @@ -128,12 +128,11 @@ const bindGroup = root.createBindGroup(layout, { uniforms: uniformsBuffer, }); -let pipeline = root['~unstable'] - .withVertex(fullScreenTriangle, {}) - .withFragment(mainFragment, { - format: presentationFormat, - }) - .createPipeline(); +let pipeline = root['~unstable'].createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: mainFragment, + targets: { format: presentationFormat }, +}); function setPipeline({ outOfGamutPattern, @@ -146,11 +145,11 @@ function setPipeline({ .with(patternSlot, outOfGamutPattern) .with(oklabGamutClipSlot, gamutClip) .with(oklabGamutClipAlphaAccess, alphaFromUniforms) - .withVertex(fullScreenTriangle, {}) - .withFragment(mainFragment, { - format: presentationFormat, - }) - .createPipeline(); + .createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: mainFragment, + targets: { format: presentationFormat }, + }); } function draw() { diff --git a/apps/typegpu-docs/src/examples/simple/square/index.ts b/apps/typegpu-docs/src/examples/simple/square/index.ts index 9290629e1..536132013 100644 --- a/apps/typegpu-docs/src/examples/simple/square/index.ts +++ b/apps/typegpu-docs/src/examples/simple/square/index.ts @@ -66,9 +66,12 @@ const indexBuffer = root .$usage('index'); const pipeline = root['~unstable'] - .withVertex(vertex, { color: vertexLayout.attrib }) - .withFragment(mainFragment, { format: presentationFormat }) - .createPipeline() + .createRenderPipeline({ + attribs: { color: vertexLayout.attrib }, + vertex, + fragment: mainFragment, + targets: { format: presentationFormat }, + }) .withIndexBuffer(indexBuffer); function render() { diff --git a/apps/typegpu-docs/src/examples/simple/triangle/index.ts b/apps/typegpu-docs/src/examples/simple/triangle/index.ts index d9553b6fd..a831e65a0 100644 --- a/apps/typegpu-docs/src/examples/simple/triangle/index.ts +++ b/apps/typegpu-docs/src/examples/simple/triangle/index.ts @@ -49,9 +49,11 @@ context.configure({ }); const pipeline = root['~unstable'] - .withVertex(mainVertex, {}) - .withFragment(mainFragment, { format: presentationFormat }) - .createPipeline(); + .createRenderPipeline({ + vertex: mainVertex, + fragment: mainFragment, + targets: { format: presentationFormat }, + }); setTimeout(() => { pipeline diff --git a/apps/typegpu-docs/src/examples/simple/vaporrave/index.ts b/apps/typegpu-docs/src/examples/simple/vaporrave/index.ts index 183712439..bc6ce3337 100644 --- a/apps/typegpu-docs/src/examples/simple/vaporrave/index.ts +++ b/apps/typegpu-docs/src/examples/simple/vaporrave/index.ts @@ -149,9 +149,11 @@ const perlinCache = perlin3d.staticCache({ let renderPipeline = root['~unstable'] .with(floorPatternSlot, circles) .pipe(perlinCache.inject()) - .withVertex(vertexMain, {}) - .withFragment(fragmentMain, { format: presentationFormat }) - .createPipeline(); + .createRenderPipeline({ + vertex: vertexMain, + fragment: fragmentMain, + targets: { format: presentationFormat }, + }); let animationFrame: number; let floorAngle = 0; @@ -232,9 +234,11 @@ export const controls = { renderPipeline = root['~unstable'] .with(floorPatternSlot, value === 'grid' ? grid : circles) .pipe(perlinCache.inject()) - .withVertex(vertexMain, {}) - .withFragment(fragmentMain, { format: presentationFormat }) - .createPipeline(); + .createRenderPipeline({ + vertex: vertexMain, + fragment: fragmentMain, + targets: { format: presentationFormat }, + }); }, }, }; diff --git a/apps/typegpu-docs/src/examples/simulation/boids-next/index.ts b/apps/typegpu-docs/src/examples/simulation/boids-next/index.ts index 2d2fde0a5..451c68a6d 100644 --- a/apps/typegpu-docs/src/examples/simulation/boids-next/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/boids-next/index.ts @@ -156,15 +156,16 @@ const vertexLayout = tgpu.vertexLayout(d.arrayOf(d.vec2f)); const instanceLayout = tgpu.vertexLayout(TriangleDataArray, 'instance'); const renderPipeline = root['~unstable'] - .withVertex(mainVert, { - v: vertexLayout.attrib, - center: instanceLayout.attrib.position, - velocity: instanceLayout.attrib.velocity, + .createRenderPipeline({ + attribs: { + v: vertexLayout.attrib, + center: instanceLayout.attrib.position, + velocity: instanceLayout.attrib.velocity, + }, + vertex: mainVert, + fragment: mainFrag, + targets: { format: presentationFormat }, }) - .withFragment(mainFrag, { - format: presentationFormat, - }) - .createPipeline() .with(vertexLayout, triangleVertexBuffer); const computeBindGroupLayout = tgpu.bindGroupLayout({ diff --git a/apps/typegpu-docs/src/examples/simulation/confetti/index.ts b/apps/typegpu-docs/src/examples/simulation/confetti/index.ts index a0ac44042..217e704e8 100644 --- a/apps/typegpu-docs/src/examples/simulation/confetti/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/confetti/index.ts @@ -150,19 +150,21 @@ const mainCompute = tgpu['~unstable'].computeFn({ // pipelines const renderPipeline = root['~unstable'] - .withVertex(mainVert, { - tilt: geometryLayout.attrib.tilt, - angle: geometryLayout.attrib.angle, - color: geometryLayout.attrib.color, - center: dataLayout.attrib.position, + .createRenderPipeline({ + vertex: mainVert, + fragment: mainFrag, + targets: { format: presentationFormat }, + attribs: { + tilt: geometryLayout.attrib.tilt, + angle: geometryLayout.attrib.angle, + color: geometryLayout.attrib.color, + center: dataLayout.attrib.position, + }, + + primitive: { + topology: 'triangle-strip', + }, }) - .withFragment(mainFrag, { - format: presentationFormat, - }) - .withPrimitive({ - topology: 'triangle-strip', - }) - .createPipeline() .with(geometryLayout, particleGeometryBuffer) .with(dataLayout, particleDataBuffer); diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts index 47be22856..4527d6463 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts @@ -489,10 +489,13 @@ function makePipelines( const renderPipeline = root['~unstable'] .with(inputGridSlot, inputGridReadonly) - .withVertex(vertexMain, {}) - .withFragment(fragmentMain, { format: presentationFormat }) - .withPrimitive({ topology: 'triangle-strip' }) - .createPipeline(); + .createRenderPipeline({ + vertex: vertexMain, + fragment: fragmentMain, + targets: { format: presentationFormat }, + + primitive: { topology: 'triangle-strip' }, + }); return { init() { diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts index f637b95fc..f4f5aa83e 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts @@ -339,17 +339,19 @@ function resetGameData() { }); const renderPipeline = root['~unstable'] - .withVertex(vertex, { - squareData: vertexLayout.attrib, - currentStateData: vertexInstanceLayout.attrib, + .createRenderPipeline({ + attribs: { + squareData: vertexLayout.attrib, + currentStateData: vertexInstanceLayout.attrib, + }, + vertex, + fragment, + targets: { format: presentationFormat }, + + primitive: { + topology: 'triangle-strip', + }, }) - .withFragment(fragment, { - format: presentationFormat, - }) - .withPrimitive({ - topology: 'triangle-strip', - }) - .createPipeline() .with(vertexLayout, squareBuffer) .with(vertexInstanceLayout, currentStateBuffer); diff --git a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts index daf7053d6..f0a3ee902 100644 --- a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts @@ -115,23 +115,30 @@ const skyBoxPipeline = root['~unstable'] .with(filteringSamplerSlot, sampler) .with(cameraAccess, camera) .with(skyBoxAccess, skyBox) - .withVertex(skyBoxVertex, renderSkyBoxVertexLayout.attrib) - .withFragment(skyBoxFragment, { format: presentationFormat }) - .createPipeline(); + .createRenderPipeline({ + attribs: renderSkyBoxVertexLayout.attrib, + vertex: skyBoxVertex, + fragment: skyBoxFragment, + targets: { format: presentationFormat }, + }); const renderPipeline = root['~unstable'] .with(filteringSamplerSlot, sampler) .with(lightSourceAccess, lightSource) .with(cameraAccess, camera) - .withVertex(mainVertex, renderVertexLayout.attrib) - .withFragment(mainFragment, { format: presentationFormat }) - .withDepthStencil({ - format: 'depth24plus', - depthWriteEnabled: true, - depthCompare: 'less', - }) - .withPrimitive({ topology: 'triangle-list', cullMode: 'back' }) - .createPipeline(); + .createRenderPipeline({ + attribs: renderVertexLayout.attrib, + vertex: mainVertex, + fragment: mainFragment, + targets: { format: presentationFormat }, + + primitive: { topology: 'triangle-list', cullMode: 'back' }, + depthStencil: { + format: 'depth24plus', + depthWriteEnabled: true, + depthCompare: 'less', + }, + }); let depthTexture = root.device.createTexture({ size: [canvas.width, canvas.height, 1], diff --git a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts index a344353db..8624ee806 100644 --- a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts @@ -451,10 +451,11 @@ const fragmentShader = tgpu['~unstable'].fragmentFn({ return d.vec4f(accum, alpha); }); -const renderPipeline = root['~unstable'] - .withVertex(fullScreenTriangle, {}) - .withFragment(fragmentShader, { format: presentationFormat }) - .createPipeline(); +const renderPipeline = root['~unstable'].createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: fragmentShader, + targets: { format: presentationFormat }, +}); const computePipeline = root['~unstable'] .createComputePipeline({ compute: updateAgents }); 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 31bf610ce..a82a07199 100644 --- a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts @@ -147,13 +147,15 @@ const addInkPipeline = createComputePipeline(c.addInkFn); function createRenderPipeline( fragmentFn: TgpuFragmentFn<{ uv: d.Vec2f }, d.Vec4f>, ) { - return root['~unstable'] - .withVertex(renderFn, {}) - .withFragment(fragmentFn, { format }) - .withPrimitive({ + return root['~unstable'].createRenderPipeline({ + vertex: renderFn, + fragment: fragmentFn, + targets: { format }, + + primitive: { topology: 'triangle-strip', - }) - .createPipeline(); + }, + }); } const renderPipelineInk = createRenderPipeline(fragmentInkFn); diff --git a/apps/typegpu-docs/src/examples/tests/log-test/index.ts b/apps/typegpu-docs/src/examples/tests/log-test/index.ts index c8dcba593..186f54611 100644 --- a/apps/typegpu-docs/src/examples/tests/log-test/index.ts +++ b/apps/typegpu-docs/src/examples/tests/log-test/index.ts @@ -192,10 +192,11 @@ export const controls = { alphaMode: 'premultiplied', }); - const pipeline = root['~unstable'] - .withVertex(mainVertex, {}) - .withFragment(mainFragment, { format: presentationFormat }) - .createPipeline(); + const pipeline = root['~unstable'].createRenderPipeline({ + vertex: mainVertex, + fragment: mainFragment, + targets: { format: presentationFormat }, + }); pipeline .withColorAttachment({ @@ -209,10 +210,11 @@ export const controls = { }, 'Draw indexed': { onButtonClick: () => { - const pipeline = root['~unstable'] - .withVertex(mainVertex, {}) - .withFragment(mainFragment, { format: presentationFormat }) - .createPipeline(); + const pipeline = root['~unstable'].createRenderPipeline({ + vertex: mainVertex, + fragment: mainFragment, + targets: { format: presentationFormat }, + }); const indexBuffer = root .createBuffer(d.arrayOf(d.u32, 3), [0, 1, 2]) diff --git a/apps/typegpu-docs/src/examples/tests/texture-test/index.ts b/apps/typegpu-docs/src/examples/tests/texture-test/index.ts index c8d874607..f20f8e483 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-test/index.ts +++ b/apps/typegpu-docs/src/examples/tests/texture-test/index.ts @@ -73,14 +73,15 @@ const fragmentFunction = tgpu['~unstable'].fragmentFn({ bias: biasUniform, }); -const pipeline = root['~unstable'] - .withVertex(fullScreenTriangle, {}) - .withFragment(fragmentFunction, { format: presentationFormat }) - .createPipeline(); +const pipeline = root['~unstable'].createRenderPipeline({ + vertex: fullScreenTriangle, + fragment: fragmentFunction, + targets: { format: presentationFormat }, +}); function render() { pipeline - .with(layout, bindGroup) + .with(bindGroup) .withColorAttachment({ view: context.getCurrentTexture().createView(), loadOp: 'clear', diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index b5b78addd..550228d58 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -1,3 +1,4 @@ +import type { OmitBuiltins } from '../../builtin.ts'; import type { IndexFlag, TgpuBuffer, @@ -24,6 +25,11 @@ import { type ResolutionResult, resolve } from '../../resolutionCtx.ts'; import type { TgpuNamable } from '../../shared/meta.ts'; import { getName, PERF, setName } from '../../shared/meta.ts'; import { $getNameForward, $internal, $resolve } from '../../shared/symbols.ts'; +import { + Equal, + NullableToOptional, + UndefinedToOptional, +} from '../../shared/utilityTypes.ts'; import type { AnyVertexAttribs } from '../../shared/vertexFormat.ts'; import { isBindGroup, @@ -41,14 +47,23 @@ import { wgslExtensionToFeatureName, } from '../../wgslExtensions.ts'; import type { IOData, IOLayout, IORecord } from '../function/fnTypes.ts'; -import type { TgpuFragmentFn } from '../function/tgpuFragmentFn.ts'; -import type { TgpuVertexFn } from '../function/tgpuVertexFn.ts'; +import type { + FragmentInConstrained, + FragmentOutConstrained, + TgpuFragmentFn, +} from '../function/tgpuFragmentFn.ts'; +import type { + TgpuVertexFn, + VertexInConstrained, + VertexOutConstrained, +} from '../function/tgpuVertexFn.ts'; import { namespace } from '../resolve/namespace.ts'; import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; import type { TgpuSlot } from '../slot/slotTypes.ts'; import { isTexture, type TgpuTexture } from '../texture/texture.ts'; import type { RenderFlag } from '../texture/usageExtension.ts'; import { connectAttributesToShader } from '../vertexLayout/connectAttributesToShader.ts'; +import type { LayoutToAllowedAttribs } from '../vertexLayout/vertexAttribute.ts'; import { isVertexLayout, type TgpuVertexLayout, @@ -74,6 +89,61 @@ interface RenderPipelineInternals { // Public API // ---------- +export type TgpuPrimitiveState = + | GPUPrimitiveState + | Omit & { + stripIndexFormat?: U32 | U16; + } + | undefined; + +export type TgpuRenderPipelineDescriptorCommons = { + /** + * Describes the primitive-related properties of the pipeline. + */ + primitive?: TgpuPrimitiveState | undefined; + /** + * Describes the optional depth-stencil properties, including the testing, operations, and bias. + */ + depthStencil?: GPUDepthStencilState | undefined; + /** + * Describes the multi-sampling properties of the pipeline. + */ + multisample?: GPUMultisampleState | undefined; +}; + +export type TgpuRenderPipelineDescriptor< + VertexIn extends VertexInConstrained, + VertexOut extends VertexOutConstrained, + FragmentIn extends FragmentInConstrained, + FragmentOut extends FragmentOutConstrained, +> = + & TgpuRenderPipelineDescriptorCommons + & UndefinedToOptional<{ + vertex: TgpuVertexFn< + VertexIn, + VertexOut & OmitBuiltins> + >; + fragment: TgpuFragmentFn; + + // biome-ignore lint/complexity/noBannedTypes: we do use that type + attribs: Equal, {}> extends true ? undefined + : LayoutToAllowedAttribs>>; + targets: FragmentOutToTargets>; + }>; + +export type TgpuNoColorRenderPipelineDescriptor< + VertexIn extends VertexInConstrained, + VertexOut extends VertexOutConstrained, +> = + & TgpuRenderPipelineDescriptorCommons + & UndefinedToOptional<{ + vertex: TgpuVertexFn; + + // biome-ignore lint/complexity/noBannedTypes: we do use that type + attribs: Equal, {}> extends true ? undefined + : LayoutToAllowedAttribs>>; + }>; + export interface HasIndexBuffer { readonly hasIndexBuffer: true; diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index 7916df888..77b3afe5a 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -68,7 +68,9 @@ import type { } from '../pipeline/computePipeline.ts'; import type { FragmentOutToTargets, + TgpuNoColorRenderPipelineDescriptor, TgpuRenderPipeline, + TgpuRenderPipelineDescriptor, } from '../pipeline/renderPipeline.ts'; import type { Eventual, TgpuAccessor, TgpuSlot } from '../slot/slotTypes.ts'; import type { TgpuTexture, TgpuTextureView } from '../texture/texture.ts'; @@ -103,6 +105,9 @@ export interface WithCompute { createPipeline(): TgpuComputePipeline; } +/** + * TODO: Remove if favor of createRenderPipeline's validation + */ export type ValidateFragmentIn< VertexOut extends VertexOutConstrained, FragmentIn extends FragmentInConstrained, @@ -214,6 +219,26 @@ export interface WithBinding { descriptor: TgpuComputePipelineDescriptor, ): TgpuComputePipeline; + createRenderPipeline< + VertexIn extends VertexInConstrained, + VertexOut extends VertexOutConstrained, + >( + descriptor: TgpuNoColorRenderPipelineDescriptor, + ): TgpuRenderPipeline; + createRenderPipeline< + VertexIn extends VertexInConstrained, + VertexOut extends VertexOutConstrained, + FragmentIn extends FragmentInConstrained, + FragmentOut extends FragmentOutConstrained, + >( + descriptor: TgpuRenderPipelineDescriptor< + VertexIn, + VertexOut, + FragmentIn, + FragmentOut + >, + ): TgpuRenderPipeline; + /** * Creates a compute pipeline that executes the given callback in an exact number of threads. * This is different from `createComputePipeline` in that it does a bounds check on the diff --git a/packages/typegpu/src/index.ts b/packages/typegpu/src/index.ts index f3a62c94a..89e96fb09 100644 --- a/packages/typegpu/src/index.ts +++ b/packages/typegpu/src/index.ts @@ -111,8 +111,17 @@ export type { } from './core/root/rootTypes.ts'; export type { Storage, StorageFlag } from './extension.ts'; export type { TgpuVertexLayout } from './core/vertexLayout/vertexLayout.ts'; -export type { TgpuRenderPipeline } from './core/pipeline/renderPipeline.ts'; -export type { TgpuComputePipeline } from './core/pipeline/computePipeline.ts'; +export type { + TgpuNoColorRenderPipelineDescriptor, + TgpuPrimitiveState, + TgpuRenderPipeline, + TgpuRenderPipelineDescriptor, + TgpuRenderPipelineDescriptorCommons, +} from './core/pipeline/renderPipeline.ts'; +export type { + TgpuComputePipeline, + TgpuComputePipelineDescriptor, +} from './core/pipeline/computePipeline.ts'; export type { IndexFlag, TgpuBuffer, diff --git a/packages/typegpu/src/shared/utilityTypes.ts b/packages/typegpu/src/shared/utilityTypes.ts index e71ac1a09..48180c681 100644 --- a/packages/typegpu/src/shared/utilityTypes.ts +++ b/packages/typegpu/src/shared/utilityTypes.ts @@ -33,6 +33,8 @@ export type OmitProps, Prop> = Pick< }[keyof T] >; +export type Equal = [T, U] extends [U, T] ? true : false; + /** * Removes properties from record type that equal `Prop` */ @@ -53,6 +55,16 @@ export type NullableToOptional = [K in keyof T as T[K] extends null ? never : K]: T[K]; }; +export type UndefinedToOptional = + & { + // Props where the value extends `undefined` -> make them optional + [K in keyof T as T[K] extends undefined ? K : never]?: T[K]; + } + & { + // All other props remain unchanged + [K in keyof T as T[K] extends undefined ? never : K]: T[K]; + }; + /** * The opposite of Readonly */ From 7f4f69e980797878c7ab378e91dc0b3d18b942f8 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sat, 18 Oct 2025 13:18:04 +0200 Subject: [PATCH 4/8] Working createRenderPipeline API --- .../src/core/pipeline/renderPipeline.ts | 161 +++++++-------- packages/typegpu/src/core/root/init.ts | 190 ++++++++++++------ packages/typegpu/src/core/root/rootTypes.ts | 4 +- 3 files changed, 202 insertions(+), 153 deletions(-) diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 550228d58..0e11cdf15 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -25,11 +25,7 @@ import { type ResolutionResult, resolve } from '../../resolutionCtx.ts'; import type { TgpuNamable } from '../../shared/meta.ts'; import { getName, PERF, setName } from '../../shared/meta.ts'; import { $getNameForward, $internal, $resolve } from '../../shared/symbols.ts'; -import { - Equal, - NullableToOptional, - UndefinedToOptional, -} from '../../shared/utilityTypes.ts'; +import type { Equal, UndefinedToOptional } from '../../shared/utilityTypes.ts'; import type { AnyVertexAttribs } from '../../shared/vertexFormat.ts'; import { isBindGroup, @@ -97,6 +93,12 @@ export type TgpuPrimitiveState = | undefined; export type TgpuRenderPipelineDescriptorCommons = { + vertex: TgpuVertexFn; + fragment?: TgpuFragmentFn | undefined; + + attribs?: AnyVertexAttribs | undefined; + targets?: AnyFragmentTargets | undefined; + /** * Describes the primitive-related properties of the pipeline. */ @@ -112,10 +114,10 @@ export type TgpuRenderPipelineDescriptorCommons = { }; export type TgpuRenderPipelineDescriptor< - VertexIn extends VertexInConstrained, - VertexOut extends VertexOutConstrained, - FragmentIn extends FragmentInConstrained, - FragmentOut extends FragmentOutConstrained, + VertexIn extends VertexInConstrained = VertexInConstrained, + VertexOut extends VertexOutConstrained = VertexOutConstrained, + FragmentIn extends FragmentInConstrained = FragmentInConstrained, + FragmentOut extends FragmentOutConstrained = FragmentOutConstrained, > = & TgpuRenderPipelineDescriptorCommons & UndefinedToOptional<{ @@ -132,16 +134,18 @@ export type TgpuRenderPipelineDescriptor< }>; export type TgpuNoColorRenderPipelineDescriptor< - VertexIn extends VertexInConstrained, - VertexOut extends VertexOutConstrained, + VertexIn extends VertexInConstrained = VertexInConstrained, + VertexOut extends VertexOutConstrained = VertexOutConstrained, > = & TgpuRenderPipelineDescriptorCommons & UndefinedToOptional<{ vertex: TgpuVertexFn; + fragment?: undefined; // biome-ignore lint/complexity/noBannedTypes: we do use that type attribs: Equal, {}> extends true ? undefined : LayoutToAllowedAttribs>>; + targets?: undefined; }>; export interface HasIndexBuffer { @@ -318,20 +322,9 @@ export type AnyFragmentColorAttachment = | Record; export type RenderPipelineCoreOptions = { - branch: ExperimentalTgpuRoot; + root: ExperimentalTgpuRoot; slotBindings: [TgpuSlot, unknown][]; - vertexAttribs: AnyVertexAttribs; - vertexFn: TgpuVertexFn; - fragmentFn: TgpuFragmentFn | null; - primitiveState: - | GPUPrimitiveState - | Omit & { - stripIndexFormat?: U32 | U16; - } - | undefined; - depthStencilState: GPUDepthStencilState | undefined; - targets: AnyFragmentTargets | null; - multisampleState: GPUMultisampleState | undefined; + descriptor: TgpuRenderPipelineDescriptorCommons; }; export function INTERNAL_createRenderPipeline( @@ -380,7 +373,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { this[$internal] = { core, priors, - branch: core.options.branch, + branch: core.options.root, }; this[$getNameForward] = core; } @@ -459,7 +452,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { const newPriors = createWithPerformanceCallback( internals.priors, callback, - internals.core.options.branch, + internals.core.options.root, ); return new TgpuRenderPipelineImpl(internals.core, newPriors) as this; } @@ -473,7 +466,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { const newPriors = createWithTimestampWrites( internals.priors, options, - internals.core.options.branch, + internals.core.options.root, ); return new TgpuRenderPipelineImpl(internals.core, newPriors) as this; } @@ -562,17 +555,17 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { private setupRenderPass(): GPURenderPassEncoder { const internals = this[$internal]; const memo = internals.core.unwrap(); - const { branch, fragmentFn } = internals.core.options; + const { root, descriptor } = internals.core.options; - const colorAttachments = fragmentFn + const colorAttachments = descriptor.fragment ? connectAttachmentToShader( - fragmentFn.shell.out, + descriptor.fragment.shell.out, internals.priors.colorAttachment ?? {}, ).map((attachment) => { if (isTexture(attachment.view)) { return { ...attachment, - view: branch.unwrap(attachment.view).createView(), + view: root.unwrap(attachment.view).createView(), }; } @@ -585,7 +578,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { colorAttachments, ...setupTimestampWrites( internals.priors, - branch, + root, ), }; @@ -594,7 +587,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { if (isTexture(attachment.view)) { renderPassDescriptor.depthStencilAttachment = { ...attachment, - view: branch.unwrap(attachment.view).createView(), + view: root.unwrap(attachment.view).createView(), }; } else { renderPassDescriptor.depthStencilAttachment = @@ -602,7 +595,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { } } - const pass = branch.commandEncoder.beginRenderPass(renderPassDescriptor); + const pass = root.commandEncoder.beginRenderPass(renderPassDescriptor); pass.setPipeline(memo.pipeline); @@ -611,13 +604,13 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { memo.usedBindGroupLayouts.forEach((layout, idx) => { if (memo.catchall && idx === memo.catchall[0]) { // Catch-all - pass.setBindGroup(idx, branch.unwrap(memo.catchall[1])); + pass.setBindGroup(idx, root.unwrap(memo.catchall[1])); missingBindGroups.delete(layout); } else { const bindGroup = internals.priors.bindGroupLayoutMap?.get(layout); if (bindGroup !== undefined) { missingBindGroups.delete(layout); - pass.setBindGroup(idx, branch.unwrap(bindGroup)); + pass.setBindGroup(idx, root.unwrap(bindGroup)); } } }); @@ -629,7 +622,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { const buffer = internals.priors.vertexLayoutMap?.get(vertexLayout); if (buffer) { missingVertexLayouts.delete(vertexLayout); - pass.setVertexBuffer(idx, branch.unwrap(buffer)); + pass.setVertexBuffer(idx, root.unwrap(buffer)); } }); @@ -653,7 +646,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { const internals = this[$internal]; const pass = this.setupRenderPass(); const { logResources } = internals.core.unwrap(); - const { branch } = internals.core.options; + const { root } = internals.core.options; pass.draw(vertexCount, instanceCount, firstVertex, firstInstance); @@ -664,11 +657,8 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { } internals.priors.performanceCallback - ? triggerPerformanceCallback({ - root: branch, - priors: internals.priors, - }) - : branch.flush(); + ? triggerPerformanceCallback({ root, priors: internals.priors }) + : root.flush(); } drawIndexed( @@ -689,13 +679,13 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { const pass = this.setupRenderPass(); const { logResources } = internals.core.unwrap(); - const { branch } = internals.core.options; + const { root } = internals.core.options; if (isGPUBuffer(buffer)) { pass.setIndexBuffer(buffer, indexFormat, offsetBytes, sizeBytes); } else { pass.setIndexBuffer( - branch.unwrap(buffer), + root.unwrap(buffer), indexFormat, offsetBytes, sizeBytes, @@ -717,11 +707,8 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { } internals.priors.performanceCallback - ? triggerPerformanceCallback({ - root: branch, - priors: internals.priors, - }) - : branch.flush(); + ? triggerPerformanceCallback({ root, priors: internals.priors }) + : root.flush(); } } @@ -734,43 +721,47 @@ class RenderPipelineCore implements SelfResolvable { private readonly _targets: GPUColorTargetState[] | [null]; constructor(public readonly options: RenderPipelineCoreOptions) { + const { descriptor } = options; + const { vertex, fragment, targets, attribs } = descriptor; + const connectedAttribs = connectAttributesToShader( - options.vertexFn.shell.in ?? {}, - options.vertexAttribs, + vertex.shell.in ?? {}, + // biome-ignore lint/style/noNonNullAssertion: they're there + attribs!, ); this._vertexBufferLayouts = connectedAttribs.bufferDefinitions; this.usedVertexLayouts = connectedAttribs.usedVertexLayouts; - this._targets = options.fragmentFn && options.targets + this._targets = fragment && targets ? connectTargetsToShader( - options.fragmentFn.shell.out, - options.targets, + fragment.shell.out, + targets, ) : [null]; } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { const { - vertexFn, - fragmentFn, slotBindings, + descriptor, } = this.options; + const { vertex, fragment } = descriptor; const locations = matchUpVaryingLocations( - vertexFn.shell.out, - fragmentFn?.shell.in, - getName(vertexFn) ?? '', - getName(fragmentFn) ?? '', + vertex.shell.out, + fragment?.shell.in, + getName(vertex) ?? '', + getName(fragment) ?? '', ); return ctx.withVaryingLocations( locations, () => ctx.withSlots(slotBindings, () => { - ctx.resolve(vertexFn); - if (fragmentFn) { - ctx.resolve(fragmentFn); + ctx.resolve(vertex); + if (fragment) { + ctx.resolve(fragment); } return snip('', Void); }), @@ -784,28 +775,26 @@ class RenderPipelineCore implements SelfResolvable { public unwrap(): Memo { if (this._memo === undefined) { const { - branch, - primitiveState, - depthStencilState, - multisampleState, + root, + descriptor: tgpuDescriptor, } = this.options; - const device = branch.device; + const device = root.device; const enableExtensions = wgslExtensions.filter((extension) => - branch.enabledFeatures.has(wgslExtensionToFeatureName[extension]) + root.enabledFeatures.has(wgslExtensionToFeatureName[extension]) ); // Resolving code let resolutionResult: ResolutionResult; let resolveMeasure: PerformanceMeasure | undefined; - const ns = namespace({ names: branch.nameRegistrySetting }); + const ns = namespace({ names: root.nameRegistrySetting }); if (PERF?.enabled) { const resolveStart = performance.mark('typegpu:resolution:start'); resolutionResult = resolve(this, { namespace: ns, enableExtensions, - shaderGenerator: branch.shaderGenerator, - root: branch, + shaderGenerator: root.shaderGenerator, + root, }); resolveMeasure = performance.measure('typegpu:resolution', { start: resolveStart.name, @@ -814,8 +803,8 @@ class RenderPipelineCore implements SelfResolvable { resolutionResult = resolve(this, { namespace: ns, enableExtensions, - shaderGenerator: branch.shaderGenerator, - root: branch, + shaderGenerator: root.shaderGenerator, + root, }); } @@ -836,7 +825,7 @@ class RenderPipelineCore implements SelfResolvable { const descriptor: GPURenderPipelineDescriptor = { layout: device.createPipelineLayout({ label: `${getName(this) ?? ''} - Pipeline Layout`, - bindGroupLayouts: usedBindGroupLayouts.map((l) => branch.unwrap(l)), + bindGroupLayouts: usedBindGroupLayouts.map((l) => root.unwrap(l)), }), vertex: { module, @@ -849,33 +838,33 @@ class RenderPipelineCore implements SelfResolvable { descriptor.label = label; } - if (this.options.fragmentFn) { + if (tgpuDescriptor.fragment) { descriptor.fragment = { module, targets: this._targets, }; } - if (primitiveState) { - if (isWgslData(primitiveState.stripIndexFormat)) { + if (tgpuDescriptor.primitive) { + if (isWgslData(tgpuDescriptor.primitive.stripIndexFormat)) { descriptor.primitive = { - ...primitiveState, + ...tgpuDescriptor.primitive, stripIndexFormat: { 'u32': 'uint32', 'u16': 'uint16', - }[primitiveState.stripIndexFormat.type] as GPUIndexFormat, + }[tgpuDescriptor.primitive.stripIndexFormat.type] as GPUIndexFormat, }; } else { - descriptor.primitive = primitiveState as GPUPrimitiveState; + descriptor.primitive = tgpuDescriptor.primitive as GPUPrimitiveState; } } - if (depthStencilState) { - descriptor.depthStencil = depthStencilState; + if (tgpuDescriptor.depthStencil) { + descriptor.depthStencil = tgpuDescriptor.depthStencil; } - if (multisampleState) { - descriptor.multisample = multisampleState; + if (tgpuDescriptor.multisample) { + descriptor.multisample = tgpuDescriptor.multisample; } this._memo = { diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index a434529bc..90085c2a6 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -12,10 +12,9 @@ import type { AnyData, Disarray } from '../../data/dataTypes.ts'; import type { AnyWgslData, BaseData, - U16, - U32, v3u, Vec3u, + Void, WgslArray, } from '../../data/wgslTypes.ts'; import { @@ -27,7 +26,6 @@ import { WeakMemo } from '../../memo.ts'; import { clearTextureUtilsCache } from '../texture/textureUtils.ts'; import type { Infer } from '../../shared/repr.ts'; import { $internal } from '../../shared/symbols.ts'; -import type { AnyVertexAttribs } from '../../shared/vertexFormat.ts'; import type { ExtractBindGroupInputFromLayout, TgpuBindGroup, @@ -57,8 +55,16 @@ import type { TgpuBufferUsage } from '../buffer/bufferUsage.ts'; import type { IOLayout } from '../function/fnTypes.ts'; import { computeFn, type TgpuComputeFn } from '../function/tgpuComputeFn.ts'; import { fn, type TgpuFn } from '../function/tgpuFn.ts'; -import type { TgpuFragmentFn } from '../function/tgpuFragmentFn.ts'; -import type { TgpuVertexFn } from '../function/tgpuVertexFn.ts'; +import type { + FragmentInConstrained, + FragmentOutConstrained, + TgpuFragmentFn, +} from '../function/tgpuFragmentFn.ts'; +import type { + TgpuVertexFn, + VertexInConstrained, + VertexOutConstrained, +} from '../function/tgpuVertexFn.ts'; import { INTERNAL_createComputePipeline, type TgpuComputePipeline, @@ -67,8 +73,11 @@ import { import { type AnyFragmentTargets, INTERNAL_createRenderPipeline, - type RenderPipelineCoreOptions, + type TgpuNoColorRenderPipelineDescriptor, + type TgpuPrimitiveState, type TgpuRenderPipeline, + type TgpuRenderPipelineDescriptor, + type TgpuRenderPipelineDescriptorCommons, } from '../pipeline/renderPipeline.ts'; import { isComputePipeline, isRenderPipeline } from '../pipeline/typeGuards.ts'; import { @@ -212,8 +221,8 @@ class WithBindingImpl implements WithBinding { return new WithComputeImpl(this._getRoot(), this._slotBindings, entryFn); } - createComputePipeline( - descriptor: TgpuComputePipelineDescriptor, + createComputePipeline>( + descriptor: TgpuComputePipelineDescriptor, ): TgpuComputePipeline { return INTERNAL_createComputePipeline( this._getRoot(), @@ -222,6 +231,35 @@ class WithBindingImpl implements WithBinding { ); } + createRenderPipeline< + VertexIn extends VertexInConstrained, + VertexOut extends VertexOutConstrained, + >( + descriptor: TgpuNoColorRenderPipelineDescriptor, + ): TgpuRenderPipeline; + createRenderPipeline< + VertexIn extends VertexInConstrained, + VertexOut extends VertexOutConstrained, + FragmentIn extends FragmentInConstrained, + FragmentOut extends FragmentOutConstrained, + >( + descriptor: TgpuRenderPipelineDescriptor< + VertexIn, + VertexOut, + FragmentIn, + FragmentOut + >, + ): TgpuRenderPipeline; + createRenderPipeline( + descriptor: TgpuRenderPipelineDescriptorCommons, + ): TgpuRenderPipeline { + return INTERNAL_createRenderPipeline({ + root: this._getRoot(), + slotBindings: this._slotBindings, + descriptor, + }); + } + createGuardedComputePipeline( callback: (...args: TArgs) => undefined, ): TgpuGuardedComputePipeline { @@ -252,9 +290,7 @@ class WithBindingImpl implements WithBinding { wrappedCallback(in.id.x, in.id.y, in.id.z); }`.$uses({ sizeUniform, wrappedCallback }); - const pipeline = this - .withCompute(mainCompute) - .createPipeline(); + const pipeline = this.createComputePipeline({ compute: mainCompute }); return new TgpuGuardedComputePipelineImpl( root, @@ -268,14 +304,9 @@ class WithBindingImpl implements WithBinding { vertexFn: TgpuVertexFn, attribs: LayoutToAllowedAttribs>, ): WithVertex { - return new WithVertexImpl({ - branch: this._getRoot(), - primitiveState: undefined, - depthStencilState: undefined, - slotBindings: this._slotBindings, - vertexFn, - vertexAttribs: attribs as AnyVertexAttribs, - multisampleState: undefined, + return new WithVertexImpl(this._getRoot(), this._slotBindings, { + vertex: vertexFn, + attribs, }); } @@ -296,21 +327,32 @@ class WithComputeImpl implements WithCompute { ) {} createPipeline(): TgpuComputePipeline { - return INTERNAL_createComputePipeline( - this._root, - this._slotBindings, - { compute: this._entryFn }, - ); + return INTERNAL_createComputePipeline(this._root, this._slotBindings, { + compute: this._entryFn, + }); } } class WithVertexImpl implements WithVertex { + readonly #root: ExperimentalTgpuRoot; + readonly #slotBindings: [TgpuSlot, unknown][]; + readonly #partialDescriptor: Omit< + TgpuRenderPipelineDescriptorCommons, + 'fragment' | 'targets' + >; + constructor( - private readonly _options: Omit< - RenderPipelineCoreOptions, - 'fragmentFn' | 'targets' + root: ExperimentalTgpuRoot, + slotBindings: [TgpuSlot, unknown][], + partialDescriptor: Omit< + TgpuRenderPipelineDescriptorCommons, + 'fragment' | 'targets' >, - ) {} + ) { + this.#root = root; + this.#slotBindings = slotBindings; + this.#partialDescriptor = partialDescriptor; + } withFragment( fragmentFn: TgpuFragmentFn | 'n/a', @@ -320,73 +362,91 @@ class WithVertexImpl implements WithVertex { invariant(typeof fragmentFn !== 'string', 'Just type mismatch validation'); invariant(typeof targets !== 'string', 'Just type mismatch validation'); - return new WithFragmentImpl({ - ...this._options, - fragmentFn, + return new WithFragmentImpl(this.#root, this.#slotBindings, { + ...this.#partialDescriptor, + fragment: fragmentFn, targets, }); } - withPrimitive( - primitiveState: - | GPUPrimitiveState - | Omit & { - stripIndexFormat?: U32 | U16; - } - | undefined, - ): WithFragment { - return new WithVertexImpl({ ...this._options, primitiveState }); + withPrimitive(primitive: TgpuPrimitiveState | undefined): WithFragment { + return new WithVertexImpl(this.#root, this.#slotBindings, { + ...this.#partialDescriptor, + primitive, + }); } withDepthStencil( - depthStencilState: GPUDepthStencilState | undefined, + depthStencil: GPUDepthStencilState | undefined, ): WithFragment { - return new WithVertexImpl({ ...this._options, depthStencilState }); + return new WithVertexImpl(this.#root, this.#slotBindings, { + ...this.#partialDescriptor, + depthStencil, + }); } withMultisample( - multisampleState: GPUMultisampleState | undefined, + multisample: GPUMultisampleState | undefined, ): WithFragment { - return new WithVertexImpl({ ...this._options, multisampleState }); + return new WithVertexImpl(this.#root, this.#slotBindings, { + ...this.#partialDescriptor, + multisample, + }); } createPipeline(): TgpuRenderPipeline { return INTERNAL_createRenderPipeline({ - ...this._options, - fragmentFn: null, - targets: null, + root: this.#root, + slotBindings: this.#slotBindings, + descriptor: this.#partialDescriptor, }); } } class WithFragmentImpl implements WithFragment { - constructor(private readonly _options: RenderPipelineCoreOptions) {} + readonly #root: ExperimentalTgpuRoot; + readonly #slotBindings: [TgpuSlot, unknown][]; + readonly #descriptor: TgpuRenderPipelineDescriptorCommons; - withPrimitive( - primitiveState: - | GPUPrimitiveState - | Omit & { - stripIndexFormat?: U32 | U16; - } - | undefined, - ): WithFragment { - return new WithFragmentImpl({ ...this._options, primitiveState }); + constructor( + root: ExperimentalTgpuRoot, + slotBindings: [TgpuSlot, unknown][], + descriptor: TgpuRenderPipelineDescriptorCommons, + ) { + this.#root = root; + this.#slotBindings = slotBindings; + this.#descriptor = descriptor; + } + + withPrimitive(primitive: TgpuPrimitiveState | undefined): WithFragment { + return new WithFragmentImpl(this.#root, this.#slotBindings, { + ...this.#descriptor, + primitive, + }); } withDepthStencil( - depthStencilState: GPUDepthStencilState | undefined, + depthStencil: GPUDepthStencilState | undefined, ): WithFragment { - return new WithFragmentImpl({ ...this._options, depthStencilState }); + return new WithFragmentImpl(this.#root, this.#slotBindings, { + ...this.#descriptor, + depthStencil, + }); } - withMultisample( - multisampleState: GPUMultisampleState | undefined, - ): WithFragment { - return new WithFragmentImpl({ ...this._options, multisampleState }); + withMultisample(multisample: GPUMultisampleState | undefined): WithFragment { + return new WithFragmentImpl(this.#root, this.#slotBindings, { + ...this.#descriptor, + multisample, + }); } createPipeline(): TgpuRenderPipeline { - return INTERNAL_createRenderPipeline(this._options); + return INTERNAL_createRenderPipeline({ + root: this.#root, + slotBindings: this.#slotBindings, + descriptor: this.#descriptor, + }); } } @@ -808,7 +868,7 @@ class TgpuRootImpl extends WithBindingImpl export type InitOptions = { adapter?: GPURequestAdapterOptions | undefined; device?: - | GPUDeviceDescriptor & { optionalFeatures?: Iterable } + | (GPUDeviceDescriptor & { optionalFeatures?: Iterable }) | undefined; /** @default 'random' */ unstable_names?: 'random' | 'strict' | undefined; diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index 77b3afe5a..b2ba7015b 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -215,8 +215,8 @@ export interface WithBinding { entryFn: TgpuComputeFn, ): WithCompute; - createComputePipeline>( - descriptor: TgpuComputePipelineDescriptor, + createComputePipeline>( + descriptor: TgpuComputePipelineDescriptor, ): TgpuComputePipeline; createRenderPipeline< From fc523ba387fcf12921538e43bdabee430d7804a2 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sat, 18 Oct 2025 18:38:51 +0200 Subject: [PATCH 5/8] Pushing shell-less entry functions forward --- .../src/examples/rendering/3d-fish/render.ts | 19 ++- packages/typegpu/src/core/function/autoIO.ts | 40 +++++++ .../src/core/function/tgpuFragmentFn.ts | 7 ++ .../src/core/pipeline/renderPipeline.ts | 80 ++++++++++--- packages/typegpu/src/core/root/rootTypes.ts | 31 +++++ packages/typegpu/src/index.ts | 8 +- packages/typegpu/src/shared/utilityTypes.ts | 5 + .../examples/individual/perlin-noise.test.ts | 59 ++++++---- .../examples/individual/vaporrave.test.ts | 55 +++++---- packages/typegpu/tests/renderPipeline.test.ts | 109 +++++++++++++++++- 10 files changed, 338 insertions(+), 75 deletions(-) create mode 100644 packages/typegpu/src/core/function/autoIO.ts diff --git a/apps/typegpu-docs/src/examples/rendering/3d-fish/render.ts b/apps/typegpu-docs/src/examples/rendering/3d-fish/render.ts index bb810faa2..71e399411 100644 --- a/apps/typegpu-docs/src/examples/rendering/3d-fish/render.ts +++ b/apps/typegpu-docs/src/examples/rendering/3d-fish/render.ts @@ -1,5 +1,5 @@ import { hsvToRgb, rgbToHsv } from '@typegpu/color'; -import tgpu from 'typegpu'; +import tgpu, { type AutoFragmentIn } from 'typegpu'; import * as d from 'typegpu/data'; import * as std from 'typegpu/std'; import * as p from './params.ts'; @@ -11,6 +11,15 @@ import { } from './schemas.ts'; import { applySinWave, PosAndNormal } from './tgsl-helpers.ts'; +type Varyings = { + worldPosition: d.v3f; + worldNormal: d.v3f; + variant: number; + textureUV: d.v2f; + applySeaFog: number; // 0/1 + applySeaDesaturation: number; // 0/1 +}; + export const vertexShader = tgpu['~unstable'].vertexFn({ in: { ...ModelVertexInput, instanceIndex: d.builtin.instanceIndex }, out: ModelVertexOutput, @@ -93,10 +102,8 @@ export const vertexShader = tgpu['~unstable'].vertexFn({ }; }); -export const fragmentShader = tgpu['~unstable'].fragmentFn({ - in: ModelVertexOutput, - out: d.vec4f, -})((input) => { +export const fragmentShader = (input: AutoFragmentIn) => { + 'use gpu'; // shade the fragment in Phong reflection model // https://en.wikipedia.org/wiki/Phong_reflection_model // then apply sea fog and sea desaturation @@ -155,4 +162,4 @@ export const fragmentShader = tgpu['~unstable'].fragmentFn({ } return d.vec4f(foggedColor.xyz, 1); -}); +}; diff --git a/packages/typegpu/src/core/function/autoIO.ts b/packages/typegpu/src/core/function/autoIO.ts new file mode 100644 index 000000000..d3ed8f80b --- /dev/null +++ b/packages/typegpu/src/core/function/autoIO.ts @@ -0,0 +1,40 @@ +import type { AnyVecInstance, v4f } from '../../data/wgslTypes.ts'; + +export type AnyAutoCustoms = Record; + +export type AutoVertexIn = + & T + & { + // builtins + $vertexIndex: number; + $instanceIndex: number; + }; + +export type AutoVertexOut = + & T + & { + // builtins + $clipDistances?: number[] | undefined; + $position?: v4f | undefined; + }; + +export type AutoFragmentIn = + & T + & { + // builtins + $position: v4f; + $frontFacing: boolean; + $primitiveIndex: number; + $sampleIndex: number; + $sampleMask: number; + $subgroupInvocationId: number; + $subgroupSize: number; + }; + +export type AutoFragmentOut = + T extends undefined | v4f ? T + : { + // builtins + $fragDepth?: number | undefined; + $sampleMask?: number | undefined; + }; diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index 3c1c5b47c..fa4c232dd 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -8,6 +8,7 @@ import type { Decorated, Interpolate, Location, + v4f, Vec4f, WgslStruct, } from '../../data/wgslTypes.ts'; @@ -17,6 +18,7 @@ import { setName, type TgpuNamable, } from '../../shared/meta.ts'; +import { InferGPU } from '../../shared/repr.ts'; import { $getNameForward, $internal, $resolve } from '../../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; import { addReturnTypeToExternals } from '../resolve/externals.ts'; @@ -47,6 +49,11 @@ export type FragmentOutConstrained = IOLayout< | AnyFragmentOutputBuiltin >; +export type FragmentOutInferred = + | undefined + | v4f + | Record>; + /** * Describes a fragment entry function signature (its arguments, return type and targets) */ diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 0e11cdf15..46f5cbe8b 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -14,6 +14,7 @@ import { isWgslData, type U16, type U32, + v4f, Void, type WgslArray, } from '../../data/wgslTypes.ts'; @@ -24,8 +25,13 @@ import { import { type ResolutionResult, resolve } from '../../resolutionCtx.ts'; import type { TgpuNamable } from '../../shared/meta.ts'; import { getName, PERF, setName } from '../../shared/meta.ts'; +import type { InferGPURecord } from '../../shared/repr.ts'; import { $getNameForward, $internal, $resolve } from '../../shared/symbols.ts'; -import type { Equal, UndefinedToOptional } from '../../shared/utilityTypes.ts'; +import type { + Assume, + Equal, + UndefinedToOptional, +} from '../../shared/utilityTypes.ts'; import type { AnyVertexAttribs } from '../../shared/vertexFormat.ts'; import { isBindGroup, @@ -42,10 +48,16 @@ import { wgslExtensions, wgslExtensionToFeatureName, } from '../../wgslExtensions.ts'; -import type { IOData, IOLayout, IORecord } from '../function/fnTypes.ts'; +import type { + AnyAutoCustoms, + AutoFragmentIn, + AutoFragmentOut, +} from '../function/autoIO.ts'; +import type { IORecord } from '../function/fnTypes.ts'; import type { FragmentInConstrained, FragmentOutConstrained, + FragmentOutInferred, TgpuFragmentFn, } from '../function/tgpuFragmentFn.ts'; import type { @@ -94,7 +106,12 @@ export type TgpuPrimitiveState = export type TgpuRenderPipelineDescriptorCommons = { vertex: TgpuVertexFn; - fragment?: TgpuFragmentFn | undefined; + fragment?: + | TgpuFragmentFn + | (( + input: AutoFragmentIn, + ) => AutoFragmentOut) + | undefined; attribs?: AnyVertexAttribs | undefined; targets?: AnyFragmentTargets | undefined; @@ -119,7 +136,10 @@ export type TgpuRenderPipelineDescriptor< FragmentIn extends FragmentInConstrained = FragmentInConstrained, FragmentOut extends FragmentOutConstrained = FragmentOutConstrained, > = - & TgpuRenderPipelineDescriptorCommons + & Omit< + TgpuRenderPipelineDescriptorCommons, + 'vertex' | 'fragment' | 'attribs' | 'targets' + > & UndefinedToOptional<{ vertex: TgpuVertexFn< VertexIn, @@ -133,14 +153,44 @@ export type TgpuRenderPipelineDescriptor< targets: FragmentOutToTargets>; }>; +export type TgpuRenderPipelineDescriptor__ShelllessFrag< + VertexIn extends VertexInConstrained = VertexInConstrained, + VertexOut extends VertexOutConstrained = VertexOutConstrained, + FragmentOut extends FragmentOutInferred = FragmentOutInferred, +> = + & Omit< + TgpuRenderPipelineDescriptorCommons, + 'vertex' | 'fragment' | 'attribs' | 'targets' + > + & UndefinedToOptional<{ + vertex: TgpuVertexFn; + fragment: ( + input: AutoFragmentIn, AnyAutoCustoms>>, + ) => AutoFragmentOut; + + // biome-ignore lint/complexity/noBannedTypes: we do use that type + attribs: Equal, {}> extends true ? undefined + : LayoutToAllowedAttribs>>; + targets: FragmentOutToTargets>; + }>; + export type TgpuNoColorRenderPipelineDescriptor< VertexIn extends VertexInConstrained = VertexInConstrained, VertexOut extends VertexOutConstrained = VertexOutConstrained, > = - & TgpuRenderPipelineDescriptorCommons + & Omit< + TgpuRenderPipelineDescriptorCommons, + 'vertex' | 'fragment' | 'attribs' | 'targets' + > & UndefinedToOptional<{ vertex: TgpuVertexFn; - fragment?: undefined; + fragment?: + | (( + input: AutoFragmentIn< + Assume, AnyAutoCustoms> + >, + ) => undefined) + | undefined; // biome-ignore lint/complexity/noBannedTypes: we do use that type attribs: Equal, {}> extends true ? undefined @@ -160,7 +210,7 @@ export interface HasIndexBuffer { ): void; } -export interface TgpuRenderPipeline +export interface TgpuRenderPipeline extends TgpuNamable, SelfResolvable, Timeable { readonly [$internal]: RenderPipelineInternals; readonly resourceType: 'render-pipeline'; @@ -208,17 +258,19 @@ export interface TgpuRenderPipeline ): void; } -export type FragmentOutToTargets = T extends IOData - ? GPUColorTargetState +export type FragmentOutToTargets = T extends + | undefined + | { readonly [$internal]: unknown; type: 'void' } + | Record ? undefined + : T extends { readonly [$internal]: unknown } ? GPUColorTargetState : T extends Record ? { [Key in keyof T]: GPUColorTargetState } - : T extends { type: 'void' } ? Record - : never; + : GPUColorTargetState; -export type FragmentOutToColorAttachment = T extends IOData - ? ColorAttachment +export type FragmentOutToColorAttachment = T extends + { readonly [$internal]: unknown } ? ColorAttachment : T extends Record ? { [Key in keyof T]: ColorAttachment } - : never; + : ColorAttachment; export type AnyFragmentTargets = | GPUColorTargetState diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index b2ba7015b..7d8159227 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -55,6 +55,7 @@ import type { TgpuFn } from '../function/tgpuFn.ts'; import type { FragmentInConstrained, FragmentOutConstrained, + FragmentOutInferred, TgpuFragmentFn, } from '../function/tgpuFragmentFn.ts'; import type { @@ -71,6 +72,7 @@ import type { TgpuNoColorRenderPipelineDescriptor, TgpuRenderPipeline, TgpuRenderPipelineDescriptor, + TgpuRenderPipelineDescriptor__ShelllessFrag, } from '../pipeline/renderPipeline.ts'; import type { Eventual, TgpuAccessor, TgpuSlot } from '../slot/slotTypes.ts'; import type { TgpuTexture, TgpuTextureView } from '../texture/texture.ts'; @@ -140,6 +142,9 @@ export type ValidateFragmentIn< export interface WithVertex< VertexOut extends VertexOutConstrained = VertexOutConstrained, > { + /** + * @deprecated Use `root.createRenderPipeline` instead. + */ withFragment< FragmentIn extends FragmentInConstrained, FragmentOut extends FragmentOutConstrained, @@ -147,6 +152,9 @@ export interface WithVertex< ...args: ValidateFragmentIn ): WithFragment; + /** + * @deprecated Use `root.createRenderPipeline` instead. + */ withPrimitive( primitiveState: | GPUPrimitiveState @@ -156,14 +164,23 @@ export interface WithVertex< | undefined, ): WithFragment; + /** + * @deprecated Use `root.createRenderPipeline` instead. + */ withDepthStencil( depthStencilState: GPUDepthStencilState | undefined, ): WithFragment; + /** + * @deprecated Use `root.createRenderPipeline` instead. + */ withMultisample( multisampleState: GPUMultisampleState | undefined, ): WithFragment; + /** + * @deprecated Use `root.createRenderPipeline` instead. + */ createPipeline(): TgpuRenderPipeline; } @@ -238,6 +255,17 @@ export interface WithBinding { FragmentOut >, ): TgpuRenderPipeline; + createRenderPipeline< + VertexIn extends VertexInConstrained, + VertexOut extends VertexOutConstrained, + FragmentOut extends FragmentOutInferred, + >( + descriptor: TgpuRenderPipelineDescriptor__ShelllessFrag< + VertexIn, + VertexOut, + FragmentOut + >, + ): TgpuRenderPipeline; /** * Creates a compute pipeline that executes the given callback in an exact number of threads. @@ -288,6 +316,9 @@ export interface WithBinding { callback: (...args: TArgs) => void, ): TgpuGuardedComputePipeline; + /** + * @deprecated Use `root.createRenderPipeline` instead. + */ withVertex< VertexIn extends VertexInConstrained, VertexOut extends VertexOutConstrained, diff --git a/packages/typegpu/src/index.ts b/packages/typegpu/src/index.ts index 89e96fb09..3715192c5 100644 --- a/packages/typegpu/src/index.ts +++ b/packages/typegpu/src/index.ts @@ -112,10 +112,8 @@ export type { export type { Storage, StorageFlag } from './extension.ts'; export type { TgpuVertexLayout } from './core/vertexLayout/vertexLayout.ts'; export type { - TgpuNoColorRenderPipelineDescriptor, TgpuPrimitiveState, TgpuRenderPipeline, - TgpuRenderPipelineDescriptor, TgpuRenderPipelineDescriptorCommons, } from './core/pipeline/renderPipeline.ts'; export type { @@ -182,6 +180,12 @@ export type { TgpuComputeFn, TgpuComputeFnShell, } from './core/function/tgpuComputeFn.ts'; +export type { + AutoFragmentIn, + AutoFragmentOut, + AutoVertexIn, + AutoVertexOut, +} from './core/function/autoIO.ts'; export type { TgpuDeclare } from './core/declare/tgpuDeclare.ts'; export type { Namespace } from './core/resolve/namespace.ts'; // Exported for being able to track use of these global extensions easier, diff --git a/packages/typegpu/src/shared/utilityTypes.ts b/packages/typegpu/src/shared/utilityTypes.ts index 48180c681..44adaca2d 100644 --- a/packages/typegpu/src/shared/utilityTypes.ts +++ b/packages/typegpu/src/shared/utilityTypes.ts @@ -72,6 +72,11 @@ export type Mutable = { -readonly [P in keyof T]: T[P]; }; +/** + * Source: https://code.lol/post/programming/higher-kinded-types/ + */ +export type Assume = T extends U ? T : U; + /** * Any typed array */ diff --git a/packages/typegpu/tests/examples/individual/perlin-noise.test.ts b/packages/typegpu/tests/examples/individual/perlin-noise.test.ts index 64cc5251b..4e465f029 100644 --- a/packages/typegpu/tests/examples/individual/perlin-noise.test.ts +++ b/packages/typegpu/tests/examples/individual/perlin-noise.test.ts @@ -17,50 +17,59 @@ describe('perlin noise example', () => { }, device); expect(shaderCodes).toMatchInlineSnapshot(` - "@group(0) @binding(0) var size_1: vec4u; + "@group(0) @binding(0) var sizeUniform_1: vec3u; - @group(0) @binding(1) var memory_2: array; + @group(1) @binding(0) var size_3: vec4u; - var seed_6: vec2f; + @group(1) @binding(1) var memory_4: array; - fn seed3_5(value: vec3f) { - seed_6 = (value.xy + vec2f(value.z)); + var seed_8: vec2f; + + fn seed3_7(value: vec3f) { + seed_8 = (value.xy + vec2f(value.z)); } - fn randSeed3_4(seed: vec3f) { - seed3_5(seed); + fn randSeed3_6(seed: vec3f) { + seed3_7(seed); } - fn item_8() -> f32 { - var a = dot(seed_6, vec2f(23.140779495239258, 232.6168975830078)); - var b = dot(seed_6, vec2f(54.47856521606445, 345.8415222167969)); - seed_6.x = fract((cos(a) * 136.8168)); - seed_6.y = fract((cos(b) * 534.7645)); - return seed_6.y; + fn item_10() -> f32 { + var a = dot(seed_8, vec2f(23.140779495239258, 232.6168975830078)); + var b = dot(seed_8, vec2f(54.47856521606445, 345.8415222167969)); + seed_8.x = fract((cos(a) * 136.8168)); + seed_8.y = fract((cos(b) * 534.7645)); + return seed_8.y; } - fn randOnUnitSphere_7() -> vec3f { - var z = ((2 * item_8()) - 1); + fn randOnUnitSphere_9() -> vec3f { + var z = ((2 * item_10()) - 1); var oneMinusZSq = sqrt((1 - (z * z))); - var theta = (6.283185307179586 * item_8()); + var theta = (6.283185307179586 * item_10()); var x = (cos(theta) * oneMinusZSq); var y = (sin(theta) * oneMinusZSq); return vec3f(x, y, z); } - fn computeJunctionGradient_3(pos: vec3i) -> vec3f { - randSeed3_4((1e-3 * vec3f(pos))); - return randOnUnitSphere_7(); + fn computeJunctionGradient_5(pos: vec3i) -> vec3f { + randSeed3_6((1e-3 * vec3f(pos))); + return randOnUnitSphere_9(); + } + + fn mainCompute_2(x: u32, y: u32, z: u32) { + var size = size_3; + var idx = ((x + (y * size.x)) + ((z * size.x) * size.y)); + memory_4[idx] = computeJunctionGradient_5(vec3i(i32(x), i32(y), i32(z))); } - struct mainCompute_Input_9 { - @builtin(global_invocation_id) gid: vec3u, + struct mainCompute_Input_11 { + @builtin(global_invocation_id) id: vec3u, } - @compute @workgroup_size(1, 1, 1) fn mainCompute_0(input: mainCompute_Input_9) { - var size = size_1; - var idx = ((input.gid.x + (input.gid.y * size.x)) + ((input.gid.z * size.x) * size.y)); - memory_2[idx] = computeJunctionGradient_3(vec3i(input.gid.xyz)); + @compute @workgroup_size(8, 8, 4) fn mainCompute_0(in: mainCompute_Input_11) { + if (any(in.id >= sizeUniform_1)) { + return; + } + mainCompute_2(in.id.x, in.id.y, in.id.z); } struct fullScreenTriangle_Output_1 { diff --git a/packages/typegpu/tests/examples/individual/vaporrave.test.ts b/packages/typegpu/tests/examples/individual/vaporrave.test.ts index 415d70a7f..143fd80d4 100644 --- a/packages/typegpu/tests/examples/individual/vaporrave.test.ts +++ b/packages/typegpu/tests/examples/individual/vaporrave.test.ts @@ -17,47 +17,56 @@ describe('vaporrave example', () => { }, device); expect(shaderCodes).toMatchInlineSnapshot(` - "@group(0) @binding(0) var memoryBuffer_1: array; + "@group(0) @binding(0) var sizeUniform_1: vec3u; - var seed_5: vec2f; + @group(0) @binding(1) var memoryBuffer_3: array; - fn seed3_4(value: vec3f) { - seed_5 = (value.xy + vec2f(value.z)); + var seed_7: vec2f; + + fn seed3_6(value: vec3f) { + seed_7 = (value.xy + vec2f(value.z)); } - fn randSeed3_3(seed: vec3f) { - seed3_4(seed); + fn randSeed3_5(seed: vec3f) { + seed3_6(seed); } - fn item_7() -> f32 { - var a = dot(seed_5, vec2f(23.140779495239258, 232.6168975830078)); - var b = dot(seed_5, vec2f(54.47856521606445, 345.8415222167969)); - seed_5.x = fract((cos(a) * 136.8168)); - seed_5.y = fract((cos(b) * 534.7645)); - return seed_5.y; + fn item_9() -> f32 { + var a = dot(seed_7, vec2f(23.140779495239258, 232.6168975830078)); + var b = dot(seed_7, vec2f(54.47856521606445, 345.8415222167969)); + seed_7.x = fract((cos(a) * 136.8168)); + seed_7.y = fract((cos(b) * 534.7645)); + return seed_7.y; } - fn randOnUnitSphere_6() -> vec3f { - var z = ((2 * item_7()) - 1); + fn randOnUnitSphere_8() -> vec3f { + var z = ((2 * item_9()) - 1); var oneMinusZSq = sqrt((1 - (z * z))); - var theta = (6.283185307179586 * item_7()); + var theta = (6.283185307179586 * item_9()); var x = (cos(theta) * oneMinusZSq); var y = (sin(theta) * oneMinusZSq); return vec3f(x, y, z); } - fn computeJunctionGradient_2(pos: vec3i) -> vec3f { - randSeed3_3((1e-3 * vec3f(pos))); - return randOnUnitSphere_6(); + fn computeJunctionGradient_4(pos: vec3i) -> vec3f { + randSeed3_5((1e-3 * vec3f(pos))); + return randOnUnitSphere_8(); + } + + fn wrappedCallback_2(x: u32, y: u32, z: u32) { + var idx = ((x + (y * 7)) + ((z * 7) * 7)); + memoryBuffer_3[idx] = computeJunctionGradient_4(vec3i(i32(x), i32(y), i32(z))); } - struct mainCompute_Input_8 { - @builtin(global_invocation_id) gid: vec3u, + struct mainCompute_Input_10 { + @builtin(global_invocation_id) id: vec3u, } - @compute @workgroup_size(1, 1, 1) fn mainCompute_0(input: mainCompute_Input_8) { - var idx = ((input.gid.x + (input.gid.y * 7)) + ((input.gid.z * 7) * 7)); - memoryBuffer_1[idx] = computeJunctionGradient_2(vec3i(input.gid.xyz)); + @compute @workgroup_size(8, 8, 4) fn mainCompute_0(in: mainCompute_Input_10) { + if (any(in.id >= sizeUniform_1)) { + return; + } + wrappedCallback_2(in.id.x, in.id.y, in.id.z); } struct vertexMain_Output_1 { diff --git a/packages/typegpu/tests/renderPipeline.test.ts b/packages/typegpu/tests/renderPipeline.test.ts index 2fbb77445..a5a9d05e9 100644 --- a/packages/typegpu/tests/renderPipeline.test.ts +++ b/packages/typegpu/tests/renderPipeline.test.ts @@ -12,7 +12,7 @@ import { $internal } from '../src/shared/symbols.ts'; import { it } from './utils/extendedIt.ts'; import { asWgsl } from './utils/parseResolved.ts'; -describe('TgpuRenderPipeline', () => { +describe('root.withVertex(...).withFragment(...)', () => { const vert = tgpu['~unstable'].vertexFn({ out: { a: d.vec3f, b: d.vec2f }, })`{ return Out(); }`; @@ -34,26 +34,26 @@ describe('TgpuRenderPipeline', () => { // Using none const pipeline = root .withVertex(vert, {}) - .withFragment(emptyFragment, {}) + .withFragment(emptyFragment, undefined) .createPipeline(); // Using none (builtins are erased from the vertex output) const pipeline2 = root .withVertex(vertWithBuiltin, {}) - .withFragment(emptyFragment, {}) + .withFragment(emptyFragment, undefined) .createPipeline(); // Using none (builtins are ignored in the fragment input) const pipeline3 = root .withVertex(vert, {}) - .withFragment(emptyFragmentWithBuiltin, {}) + .withFragment(emptyFragmentWithBuiltin, undefined) .createPipeline(); // Using none (builtins are ignored in both input and output, // so their conflict of the `pos` key is fine) const pipeline4 = root .withVertex(vertWithBuiltin, {}) - .withFragment(emptyFragmentWithBuiltin, {}) + .withFragment(emptyFragmentWithBuiltin, undefined) .createPipeline(); // Using all @@ -968,6 +968,105 @@ describe('TgpuRenderPipeline', () => { }); }); +describe('root.createRenderPipeline', () => { + const vertex = tgpu['~unstable'].vertexFn({ + out: { a: d.vec3f, b: d.vec2f }, + })`{ return Out(); }`; + const vertexWithBuiltin = tgpu['~unstable'].vertexFn({ + out: { a: d.vec3f, b: d.vec2f, pos: d.builtin.position }, + })`{ return Out(); }`; + + it('allows fragment functions to use a subset of the vertex output', ({ root }) => { + const emptyFragment = tgpu['~unstable'].fragmentFn({ in: {}, out: {} })`{}`; + const emptyFragmentWithBuiltin = tgpu['~unstable'].fragmentFn({ + in: { pos: d.builtin.frontFacing }, + out: {}, + })`{}`; + const fullFragment = tgpu['~unstable'].fragmentFn({ + in: { a: d.vec3f, b: d.vec2f }, + out: d.vec4f, + })`{ return vec4f(); }`; + + const pipelines = [ + // Using none + root.createRenderPipeline({ + vertex, + fragment: emptyFragment, + }), + // (shell-less) + root.createRenderPipeline({ + vertex, + fragment: () => { + 'use gpu'; + }, + }), + + // Using none (builtins are erased from the vertex output) + root.createRenderPipeline({ + vertex: vertexWithBuiltin, + fragment: emptyFragment, + }), + // (shell-less) + root.createRenderPipeline({ + vertex: vertexWithBuiltin, + fragment: () => { + 'use gpu'; + }, + }), + + // Using none (builtins are ignored in the fragment input) + root.createRenderPipeline({ + vertex, + fragment: emptyFragmentWithBuiltin, + }), + // (shell-less) + root.createRenderPipeline({ + vertex, + fragment: ({ $frontFacing }) => { + 'use gpu'; + }, + }), + + // Using none (builtins are ignored in both input and output, + // so their conflict of the `pos` key is fine) + root.createRenderPipeline({ + vertex: vertexWithBuiltin, + fragment: emptyFragmentWithBuiltin, + }), + // (shell-less) + root.createRenderPipeline({ + vertex: vertexWithBuiltin, + fragment: ({ $frontFacing }) => { + 'use gpu'; + }, + }), + + // Using all + root.createRenderPipeline({ + vertex, + fragment: fullFragment, + targets: { format: 'rgba8unorm' }, + }), + // (shell-less) + root.createRenderPipeline({ + vertex, + fragment: ({ $frontFacing, a }) => { + 'use gpu'; + if ($frontFacing) { + return d.vec4f(a, 1); + } + return d.vec4f(a.zyx, 1); + }, + targets: { format: 'rgba8unorm' }, + }), + ]; + + for (const pipeline of pipelines) { + expect(pipeline).toBeDefined(); + } + }); +}); + describe('matchUpVaryingLocations', () => { it('works for empty arguments', () => { expect(matchUpVaryingLocations({}, {}, 'v', 'f')).toStrictEqual({}); From 7202ad902c47328afd2f59e04a6ec738a023dc94 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 19 Oct 2025 16:13:17 +0200 Subject: [PATCH 6/8] Export the TgpuGuardedComputePipeline type --- packages/typegpu/src/core/root/rootTypes.ts | 2 +- packages/typegpu/src/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index 7d8159227..6c0fc9092 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -85,7 +85,7 @@ import type { WgslStorageTexture, WgslTexture } from '../../data/texture.ts'; // Public API // ---------- -export interface TgpuGuardedComputePipeline { +export interface TgpuGuardedComputePipeline { /** * Returns a pipeline wrapper with the specified bind group bound. * Analogous to `TgpuComputePipeline.with(bindGroup)`. diff --git a/packages/typegpu/src/index.ts b/packages/typegpu/src/index.ts index 3715192c5..e5ab8f41d 100644 --- a/packages/typegpu/src/index.ts +++ b/packages/typegpu/src/index.ts @@ -100,6 +100,7 @@ export { isVariable } from './core/variable/tgpuVariable.ts'; export type { Configurable, + TgpuGuardedComputePipeline, TgpuRoot, ValidateBufferSchema, ValidateStorageSchema, From 8a62b9fb3390ae3450090fed0bce179f9c7a172c Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 19 Oct 2025 16:17:36 +0200 Subject: [PATCH 7/8] Skip test for now --- .../src/examples/rendering/3d-fish/render.ts | 12 +++++++----- packages/typegpu/tests/renderPipeline.test.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/typegpu-docs/src/examples/rendering/3d-fish/render.ts b/apps/typegpu-docs/src/examples/rendering/3d-fish/render.ts index 71e399411..293a2ab0b 100644 --- a/apps/typegpu-docs/src/examples/rendering/3d-fish/render.ts +++ b/apps/typegpu-docs/src/examples/rendering/3d-fish/render.ts @@ -1,5 +1,5 @@ import { hsvToRgb, rgbToHsv } from '@typegpu/color'; -import tgpu, { type AutoFragmentIn } from 'typegpu'; +import tgpu from 'typegpu'; import * as d from 'typegpu/data'; import * as std from 'typegpu/std'; import * as p from './params.ts'; @@ -102,7 +102,10 @@ export const vertexShader = tgpu['~unstable'].vertexFn({ }; }); -export const fragmentShader = (input: AutoFragmentIn) => { +export const fragmentShader = tgpu['~unstable'].fragmentFn({ + in: ModelVertexOutput, + out: d.vec4f, +})((input) => { 'use gpu'; // shade the fragment in Phong reflection model // https://en.wikipedia.org/wiki/Phong_reflection_model @@ -144,8 +147,7 @@ export const fragmentShader = (input: AutoFragmentIn) => { let desaturatedColor = lightedColor; if (input.applySeaDesaturation === 1) { - const desaturationFactor = -std.atan2((distanceFromCamera - 5) / 10, 1) / - 3; + const desaturationFactor = -std.atan2((distanceFromCamera - 5) / 10, 1) / 3; const hsv = rgbToHsv(desaturatedColor); hsv.y += desaturationFactor / 2; hsv.z += desaturationFactor; @@ -162,4 +164,4 @@ export const fragmentShader = (input: AutoFragmentIn) => { } return d.vec4f(foggedColor.xyz, 1); -}; +}); diff --git a/packages/typegpu/tests/renderPipeline.test.ts b/packages/typegpu/tests/renderPipeline.test.ts index a5a9d05e9..afce1c4e2 100644 --- a/packages/typegpu/tests/renderPipeline.test.ts +++ b/packages/typegpu/tests/renderPipeline.test.ts @@ -976,7 +976,7 @@ describe('root.createRenderPipeline', () => { out: { a: d.vec3f, b: d.vec2f, pos: d.builtin.position }, })`{ return Out(); }`; - it('allows fragment functions to use a subset of the vertex output', ({ root }) => { + it.skip('allows fragment functions to use a subset of the vertex output', ({ root }) => { const emptyFragment = tgpu['~unstable'].fragmentFn({ in: {}, out: {} })`{}`; const emptyFragmentWithBuiltin = tgpu['~unstable'].fragmentFn({ in: { pos: d.builtin.frontFacing }, From 6c718668a0412fa961e3fe9a5c607fb8146f4208 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 19 Oct 2025 16:32:25 +0200 Subject: [PATCH 8/8] Fix tests --- .../src/core/function/tgpuFragmentFn.ts | 2 +- .../pipeline/connectAttachmentToShader.ts | 11 ++- .../core/pipeline/connectTargetsToShader.ts | 15 +++- .../src/core/pipeline/renderPipeline.ts | 85 +++++++------------ packages/typegpu/src/core/root/init.ts | 34 +------- packages/typegpu/tests/root.test.ts | 25 +++--- 6 files changed, 68 insertions(+), 104 deletions(-) diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index fa4c232dd..0efd21b86 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -18,7 +18,7 @@ import { setName, type TgpuNamable, } from '../../shared/meta.ts'; -import { InferGPU } from '../../shared/repr.ts'; +import type { InferGPU } from '../../shared/repr.ts'; import { $getNameForward, $internal, $resolve } from '../../shared/symbols.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; import { addReturnTypeToExternals } from '../resolve/externals.ts'; diff --git a/packages/typegpu/src/core/pipeline/connectAttachmentToShader.ts b/packages/typegpu/src/core/pipeline/connectAttachmentToShader.ts index accb0616c..d4cbe9bf6 100644 --- a/packages/typegpu/src/core/pipeline/connectAttachmentToShader.ts +++ b/packages/typegpu/src/core/pipeline/connectAttachmentToShader.ts @@ -13,9 +13,18 @@ function isColorAttachment( } export function connectAttachmentToShader( - shaderOutputLayout: FragmentOutConstrained, + shaderOutputLayout: FragmentOutConstrained | undefined, attachment: AnyFragmentColorAttachment, ): ColorAttachment[] { + // For shell-less entry functions, we determine the layout based on solely the attachment + if (!shaderOutputLayout) { + if (typeof attachment.loadOp === 'string') { + return [attachment as ColorAttachment]; + } + + return Object.values(attachment) as ColorAttachment[]; + } + if (isData(shaderOutputLayout)) { if (!isColorAttachment(attachment)) { throw new Error('Expected a single color attachment, not a record.'); diff --git a/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts b/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts index 4c4d8b342..a4f07c7b6 100644 --- a/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts +++ b/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts @@ -11,9 +11,22 @@ function isColorTargetState( } export function connectTargetsToShader( - shaderOutputLayout: FragmentOutConstrained, + shaderOutputLayout: FragmentOutConstrained | undefined, targets: AnyFragmentTargets, ): GPUColorTargetState[] { + // For shell-less entry functions, we determine the layout based on solely the targets + if (!shaderOutputLayout) { + if (!targets) { + return []; + } + + if (typeof targets?.format === 'string') { + return [targets as GPUColorTargetState]; + } + + return Object.values(targets) as GPUColorTargetState[]; + } + if (isData(shaderOutputLayout)) { if (isVoid(shaderOutputLayout)) { return []; diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 46f5cbe8b..f6df3dc1a 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -14,7 +14,7 @@ import { isWgslData, type U16, type U32, - v4f, + type v4f, Void, type WgslArray, } from '../../data/wgslTypes.ts'; @@ -99,9 +99,9 @@ interface RenderPipelineInternals { export type TgpuPrimitiveState = | GPUPrimitiveState - | Omit & { + | (Omit & { stripIndexFormat?: U32 | U16; - } + }) | undefined; export type TgpuRenderPipelineDescriptorCommons = { @@ -230,13 +230,9 @@ export interface TgpuRenderPipeline ): this; with(bindGroup: TgpuBindGroup): this; - withColorAttachment( - attachment: FragmentOutToColorAttachment, - ): this; + withColorAttachment(attachment: FragmentOutToColorAttachment): this; - withDepthStencilAttachment( - attachment: DepthStencilAttachment, - ): this; + withDepthStencilAttachment(attachment: DepthStencilAttachment): this; withIndexBuffer( buffer: TgpuBuffer & IndexFlag, @@ -267,12 +263,14 @@ export type FragmentOutToTargets = T extends ? { [Key in keyof T]: GPUColorTargetState } : GPUColorTargetState; -export type FragmentOutToColorAttachment = T extends - { readonly [$internal]: unknown } ? ColorAttachment +export type FragmentOutToColorAttachment = T extends { + readonly [$internal]: unknown; +} ? ColorAttachment : T extends Record ? { [Key in keyof T]: ColorAttachment } : ColorAttachment; export type AnyFragmentTargets = + | undefined | GPUColorTargetState | Record; @@ -400,7 +398,7 @@ type TgpuRenderPipelinePriors = { readonly depthStencilAttachment?: DepthStencilAttachment | undefined; readonly indexBuffer?: | { - buffer: TgpuBuffer & IndexFlag | GPUBuffer; + buffer: (TgpuBuffer & IndexFlag) | GPUBuffer; indexFormat: GPUIndexFormat; offsetBytes?: number | undefined; sizeBytes?: number | undefined; @@ -447,16 +445,10 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { vertexLayout: TgpuVertexLayout, buffer: TgpuBuffer & VertexFlag, ): this; - with( - bindGroupLayout: TgpuBindGroupLayout, - bindGroup: TgpuBindGroup, - ): this; + with(bindGroupLayout: TgpuBindGroupLayout, bindGroup: TgpuBindGroup): this; with(bindGroup: TgpuBindGroup): this; with( - layoutOrBindGroup: - | TgpuVertexLayout - | TgpuBindGroupLayout - | TgpuBindGroup, + layoutOrBindGroup: TgpuVertexLayout | TgpuBindGroupLayout | TgpuBindGroup, resource?: (TgpuBuffer & VertexFlag) | TgpuBindGroup, ): this { const internals = this[$internal]; @@ -486,10 +478,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { ...internals.priors, vertexLayoutMap: new Map([ ...(internals.priors.vertexLayoutMap ?? []), - [ - layoutOrBindGroup, - resource as TgpuBuffer & VertexFlag, - ], + [layoutOrBindGroup, resource as TgpuBuffer & VertexFlag], ]), }) as this; } @@ -523,9 +512,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { return new TgpuRenderPipelineImpl(internals.core, newPriors) as this; } - withColorAttachment( - attachment: AnyFragmentColorAttachment, - ): this { + withColorAttachment(attachment: AnyFragmentColorAttachment): this { const internals = this[$internal]; return new TgpuRenderPipelineImpl(internals.core, { @@ -534,9 +521,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { }) as this; } - withDepthStencilAttachment( - attachment: DepthStencilAttachment, - ): this { + withDepthStencilAttachment(attachment: DepthStencilAttachment): this { const internals = this[$internal]; return new TgpuRenderPipelineImpl(internals.core, { @@ -557,7 +542,7 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { sizeBytes?: number, ): this & HasIndexBuffer; withIndexBuffer( - buffer: TgpuBuffer & IndexFlag | GPUBuffer, + buffer: (TgpuBuffer & IndexFlag) | GPUBuffer, indexFormatOrOffset?: GPUIndexFormat | number, offsetElementsOrSizeBytes?: number, sizeElementsOrUndefined?: number, @@ -583,8 +568,8 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { } const dataTypeToIndexFormat = { - 'u32': 'uint32', - 'u16': 'uint16', + u32: 'uint32', + u16: 'uint16', } as const; const elementType = (buffer.dataType as WgslArray).elementType; @@ -610,8 +595,8 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { const { root, descriptor } = internals.core.options; const colorAttachments = descriptor.fragment - ? connectAttachmentToShader( - descriptor.fragment.shell.out, + ? (connectAttachmentToShader( + (descriptor.fragment as TgpuFragmentFn | undefined)?.shell.out, internals.priors.colorAttachment ?? {}, ).map((attachment) => { if (isTexture(attachment.view)) { @@ -622,16 +607,13 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { } return attachment; - }) as GPURenderPassColorAttachment[] + }) as GPURenderPassColorAttachment[]) : [null]; const renderPassDescriptor: GPURenderPassDescriptor = { label: getName(internals.core) ?? '', colorAttachments, - ...setupTimestampWrites( - internals.priors, - root, - ), + ...setupTimestampWrites(internals.priors, root), }; if (internals.priors.depthStencilAttachment !== undefined) { @@ -787,22 +769,19 @@ class RenderPipelineCore implements SelfResolvable { this._targets = fragment && targets ? connectTargetsToShader( - fragment.shell.out, + (fragment as TgpuFragmentFn | undefined)?.shell.out, targets, ) : [null]; } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const { - slotBindings, - descriptor, - } = this.options; + const { slotBindings, descriptor } = this.options; const { vertex, fragment } = descriptor; const locations = matchUpVaryingLocations( vertex.shell.out, - fragment?.shell.in, + (fragment as TgpuFragmentFn | undefined)?.shell.in, getName(vertex) ?? '', getName(fragment) ?? '', ); @@ -826,10 +805,7 @@ class RenderPipelineCore implements SelfResolvable { public unwrap(): Memo { if (this._memo === undefined) { - const { - root, - descriptor: tgpuDescriptor, - } = this.options; + const { root, descriptor: tgpuDescriptor } = this.options; const device = root.device; const enableExtensions = wgslExtensions.filter((extension) => root.enabledFeatures.has(wgslExtensionToFeatureName[extension]) @@ -902,8 +878,8 @@ class RenderPipelineCore implements SelfResolvable { descriptor.primitive = { ...tgpuDescriptor.primitive, stripIndexFormat: { - 'u32': 'uint32', - 'u16': 'uint16', + u32: 'uint32', + u16: 'uint16', }[tgpuDescriptor.primitive.stripIndexFormat.type] as GPUIndexFormat, }; } else { @@ -957,10 +933,7 @@ export function matchUpVaryingLocations( vertexFnName: string, fragmentFnName: string, ) { - const locations: Record< - string, - number - > = {}; + const locations: Record = {}; const usedLocations = new Set(); function saveLocation(key: string, location: number) { diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index 90085c2a6..e419a4268 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -14,7 +14,6 @@ import type { BaseData, v3u, Vec3u, - Void, WgslArray, } from '../../data/wgslTypes.ts'; import { @@ -55,16 +54,8 @@ import type { TgpuBufferUsage } from '../buffer/bufferUsage.ts'; import type { IOLayout } from '../function/fnTypes.ts'; import { computeFn, type TgpuComputeFn } from '../function/tgpuComputeFn.ts'; import { fn, type TgpuFn } from '../function/tgpuFn.ts'; -import type { - FragmentInConstrained, - FragmentOutConstrained, - TgpuFragmentFn, -} from '../function/tgpuFragmentFn.ts'; -import type { - TgpuVertexFn, - VertexInConstrained, - VertexOutConstrained, -} from '../function/tgpuVertexFn.ts'; +import type { TgpuFragmentFn } from '../function/tgpuFragmentFn.ts'; +import type { TgpuVertexFn } from '../function/tgpuVertexFn.ts'; import { INTERNAL_createComputePipeline, type TgpuComputePipeline, @@ -73,10 +64,8 @@ import { import { type AnyFragmentTargets, INTERNAL_createRenderPipeline, - type TgpuNoColorRenderPipelineDescriptor, type TgpuPrimitiveState, type TgpuRenderPipeline, - type TgpuRenderPipelineDescriptor, type TgpuRenderPipelineDescriptorCommons, } from '../pipeline/renderPipeline.ts'; import { isComputePipeline, isRenderPipeline } from '../pipeline/typeGuards.ts'; @@ -231,25 +220,6 @@ class WithBindingImpl implements WithBinding { ); } - createRenderPipeline< - VertexIn extends VertexInConstrained, - VertexOut extends VertexOutConstrained, - >( - descriptor: TgpuNoColorRenderPipelineDescriptor, - ): TgpuRenderPipeline; - createRenderPipeline< - VertexIn extends VertexInConstrained, - VertexOut extends VertexOutConstrained, - FragmentIn extends FragmentInConstrained, - FragmentOut extends FragmentOutConstrained, - >( - descriptor: TgpuRenderPipelineDescriptor< - VertexIn, - VertexOut, - FragmentIn, - FragmentOut - >, - ): TgpuRenderPipeline; createRenderPipeline( descriptor: TgpuRenderPipelineDescriptorCommons, ): TgpuRenderPipeline { diff --git a/packages/typegpu/tests/root.test.ts b/packages/typegpu/tests/root.test.ts index 0e82abc01..236042bae 100644 --- a/packages/typegpu/tests/root.test.ts +++ b/packages/typegpu/tests/root.test.ts @@ -215,10 +215,10 @@ describe('TgpuRoot', () => { foo: root.createBuffer(d.f32).$usage('uniform'), }); - const pipeline = root - .withVertex(mainVertexNotUsing, {}) - .withFragment(mainFragment, {}) - .createPipeline(); + const pipeline = root.createRenderPipeline({ + vertex: mainVertexNotUsing, + fragment: mainFragment, + }); root.beginRenderPass( { @@ -242,10 +242,10 @@ describe('TgpuRoot', () => { foo: root.createBuffer(d.f32).$usage('uniform'), }); - const pipeline = root - .withVertex(mainVertexUsing, {}) - .withFragment(mainFragment, {}) - .createPipeline(); + const pipeline = root.createRenderPipeline({ + vertex: mainVertexUsing, + fragment: mainFragment, + }); root.beginRenderPass( { @@ -270,11 +270,10 @@ describe('TgpuRoot', () => { foo: root.createBuffer(d.f32).$usage('uniform'), }); - const pipeline = root - .withVertex(mainVertexUsing, {}) - .withFragment(mainFragment, {}) - .createPipeline() - .with(group); + const pipeline = root.createRenderPipeline({ + vertex: mainVertexUsing, + fragment: mainFragment, + }).with(group); root.beginRenderPass( {