diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs
index cdb689812..c8d4531bf 100644
--- a/apps/typegpu-docs/astro.config.mjs
+++ b/apps/typegpu-docs/astro.config.mjs
@@ -103,6 +103,11 @@ export default defineConfig({
slug: 'fundamentals/tgsl',
badge: { text: 'new' },
},
+ {
+ label: 'Pipelines',
+ slug: 'fundamentals/pipelines',
+ badge: { text: 'new' },
+ },
{
label: 'Buffers',
slug: 'fundamentals/buffers',
diff --git a/apps/typegpu-docs/src/content/docs/fundamentals/functions/index.mdx b/apps/typegpu-docs/src/content/docs/fundamentals/functions/index.mdx
index e6b56c2fe..63014e0ed 100644
--- a/apps/typegpu-docs/src/content/docs/fundamentals/functions/index.mdx
+++ b/apps/typegpu-docs/src/content/docs/fundamentals/functions/index.mdx
@@ -285,7 +285,7 @@ struct mainFragment_Input {
Pipelines are an *unstable* feature. The API may be subject to change in the near future.
:::
-Typed functions are crucial for simplified pipeline creation offered by TypeGPU. You can define and run pipelines as follows:
+Typed functions are crucial for simplified [pipeline](/TypeGPU/fundamentals/pipelines) creation offered by TypeGPU. You can define and run pipelines as follows:
```ts twoslash
import tgpu from 'typegpu';
diff --git a/apps/typegpu-docs/src/content/docs/fundamentals/pipelines.mdx b/apps/typegpu-docs/src/content/docs/fundamentals/pipelines.mdx
new file mode 100644
index 000000000..60f3e3952
--- /dev/null
+++ b/apps/typegpu-docs/src/content/docs/fundamentals/pipelines.mdx
@@ -0,0 +1,377 @@
+---
+title: Pipelines
+description: A guide on how to use TypeGPU render and compute pipelines.
+---
+
+:::caution[Experimental]
+Pipelines are an *unstable* feature. The API may be subject to change in the near future.
+:::
+
+:::note[Recommended reading]
+It is assumed that you are familiar with the following concepts:
+- WebGPU Fundamentals
+- [TypeGPU Functions](/TypeGPU/fundamentals/functions)
+:::
+
+TypeGPU introduces a custom API to easily define and execute render and compute pipelines.
+It abstracts away the standard WebGPU procedures to offer a convenient, type-safe way to run shaders on the GPU.
+
+## Creating pipelines
+
+A pipeline definition starts with the [root](/TypeGPU/fundamentals/roots) object and follows a builder pattern.
+
+```ts twoslash
+///
+import tgpu from 'typegpu';
+import * as d from 'typegpu/data';
+
+const root = await tgpu.init();
+
+const presentationFormat = 'rgba8unorm';
+
+const mainVertex = tgpu['~unstable'].vertexFn({
+ in: { vertexIndex: d.builtin.vertexIndex },
+ out: { pos: d.builtin.position },
+})((input) => {
+ const pos = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)];
+
+ return {
+ pos: d.vec4f(pos[input.vertexIndex], 0, 1),
+ };
+});
+
+const mainFragment = tgpu['~unstable']
+ .fragmentFn({ out: d.vec4f })(() => d.vec4f(1, 0, 0, 1));
+
+const mainCompute = tgpu['~unstable'].computeFn({ workgroupSize: [1] })(() => {});
+
+// ---cut---
+const renderPipeline = root['~unstable']
+ .withVertex(mainVertex, {})
+ .withFragment(mainFragment, { format: presentationFormat })
+ .createPipeline();
+
+const computePipeline = root['~unstable']
+ .withCompute(mainCompute)
+ .createPipeline();
+```
+
+### *withVertex*
+
+Creating a render pipeline requires calling the `withVertex` method first, which accepts `TgpuVertexFn` and matching vertex attributes.
+The attributes are passed in a record, where the keys match the vertex function's (non-builtin) input parameters, and the values are attributes retrieved
+from a specific [tgpu.vertexLayout](/TypeGPU/fundamentals/vertex-layouts).
+If the vertex shader does not use vertex attributes, then the latter argument should be an empty object.
+The compatibility between vertex input types and vertex attribute formats is validated at the type level.
+
+```ts twoslash
+import tgpu from 'typegpu';
+import * as d from 'typegpu/data';
+
+const root = await tgpu.init();
+
+const mainVertex = tgpu['~unstable'].vertexFn({
+ in: { vertexIndex: d.builtin.vertexIndex, v: d.vec2f, center: d.vec2f, velocity: d.vec2f },
+ out: { pos: d.builtin.position },
+})((input) => {
+ const pos = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)];
+
+ return {
+ pos: d.vec4f(pos[input.vertexIndex], 0, 1),
+ };
+});
+
+const mainFragment = tgpu['~unstable']
+ .fragmentFn({ out: d.vec4f })(() => d.vec4f(1, 0, 0, 1));
+// ---cut---
+const VertexStruct = d.struct({
+ position: d.vec2f,
+ velocity: d.vec2f,
+});
+const vertexLayout = tgpu.vertexLayout(
+ (n) => d.arrayOf(d.vec2f, n),
+ 'vertex',
+);
+const instanceLayout = tgpu.vertexLayout(
+ (n) => d.arrayOf(VertexStruct, n),
+ 'instance',
+);
+
+root['~unstable']
+ .withVertex(mainVertex, {
+ v: vertexLayout.attrib,
+ center: instanceLayout.attrib.position,
+ velocity: instanceLayout.attrib.velocity,
+ })
+ // ...
+```
+
+### *withFragment*
+
+The next step is calling the `withFragment` method, which accepts `TgpuFragmentFn` and a *targets* argument defining the
+formats and behaviors of the color targets the pipeline writes to.
+Each target is specified the same as in the WebGPU API (*GPUColorTargetState*).
+The difference is that when there are multiple targets, they should be passed in a record, not an array.
+This way each target is identified by a name and can be validated against the outputs of the fragment function.
+
+```ts twoslash
+import tgpu from 'typegpu';
+import * as d from 'typegpu/data';
+
+const mainVertex = tgpu['~unstable'].vertexFn({
+ in: { vertexIndex: d.builtin.vertexIndex },
+ out: { pos: d.builtin.position },
+})((input) => {
+ const pos = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)];
+
+ return {
+ pos: d.vec4f(pos[input.vertexIndex], 0, 1),
+ };
+});
+
+const root = await tgpu.init();
+// ---cut---
+const mainFragment = tgpu['~unstable'].fragmentFn({
+ out: {
+ color: d.vec4f,
+ shadow: d.vec4f,
+ },
+})`{ ... }`;
+
+const renderPipeline = root['~unstable']
+ .withVertex(mainVertex, {})
+ .withFragment(mainFragment, {
+ color: {
+ format: 'rg8unorm',
+ blend: {
+ color: {
+ srcFactor: 'one',
+ dstFactor: 'one-minus-src-alpha',
+ operation: 'add',
+ },
+ alpha: {
+ srcFactor: 'one',
+ dstFactor: 'one-minus-src-alpha',
+ operation: 'add',
+ },
+ },
+ },
+ shadow: { format: 'r16uint' },
+ })
+ .createPipeline();
+```
+
+### Type-level validation
+
+Using the pipelines should ensure the compatibility of the vertex output and fragment input on the type level --
+`withFragment` only accepts fragment functions, which all non-builtin parameters are returned in the vertex stage.
+These parameters are identified by their names, not by their numeric *location* index.
+In general, when using vertex and fragment functions with TypeGPU pipelines, it is not necessary to set locations on the IO struct properties.
+The library automatically matches up the corresponding members (by their names) and assigns common locations to them.
+When a custom location is provided by the user (via the `d.location` attribute function) it is respected by the automatic assignment procedure,
+as long as there is no conflict between vertex and fragment location value.
+
+```ts twoslash
+import tgpu from 'typegpu';
+import * as d from 'typegpu/data';
+
+const vertex = tgpu['~unstable'].vertexFn({
+ out: {
+ pos: d.builtin.position,
+ },
+})`(...)`;
+const fragment = tgpu['~unstable'].fragmentFn({
+ in: { uv: d.vec2f },
+ out: d.vec4f,
+})`(...)`;
+
+const root = await tgpu.init();
+
+// @errors: 2554
+root['~unstable']
+ .withVertex(vertex, {})
+ .withFragment(fragment, { format: 'bgra8unorm' });
+// ^?
+```
+
+### Additional render pipeline methods
+
+After calling `withFragment`, but before `createPipeline`, it is possible to set additional pipeline settings.
+It is done through builder methods like `withDepthStencil`, `withMultisample`, `withPrimitive`.
+They accept the same arguments as their corresponding descriptors in the WebGPU API.
+
+```ts
+const renderPipeline = root['~unstable']
+ .withVertex(vertexShader, modelVertexLayout.attrib)
+ .withFragment(fragmentShader, { format: presentationFormat })
+ .withDepthStencil({
+ format: 'depth24plus',
+ depthWriteEnabled: true,
+ depthCompare: 'less',
+ })
+ .withMultisample({
+ count: 4,
+ })
+ .withPrimitive({ topology: 'triangle-list' })
+ .createPipeline();
+```
+
+### *withCompute*
+
+Creating a compute pipeline is even easier -- the `withCompute` method accepts just a `TgpuComputeFn` with no additional parameters.
+Please note that compute pipelines are separate identities from render pipelines. You cannot combine `withVertex` and `withFragment` methods with `withCompute` in a singular pipeline.
+
+### *createPipeline*
+
+The creation of TypeGPU pipelines ends with calling a `createPipeline` method on the builder.
+
+:::caution
+The underlying WebGPU resource is created lazily, that is just before the first execution or as part of a `root.unwrap`, not immediately after the `createPipeline` invocation.
+:::
+
+## Execution
+
+```ts
+renderPipeline
+ .withColorAttachment({
+ view: context.getCurrentTexture().createView(),
+ loadOp: 'clear',
+ storeOp: 'store',
+ })
+ .draw(3);
+
+computePipeline.dispatchWorkgroups(16);
+```
+
+### Attachments
+
+Render pipelines require specifying a color attachment for each target.
+The attachments are specified in the same way as in the WebGPU API (but accept both TypeGPU resources and regular WebGPU ones). However, similar to the *targets* argument, multiple targets need to be passed in as a record, with each target identified by name.
+
+Similarly, when using `withDepthStencil` it is necessary to pass in a depth stencil attachment, via the `withDepthStencilAttachment` method.
+
+```ts
+renderPipeline
+ .withColorAttachment({
+ color: {
+ view: msaaTextureView,
+ resolveTarget: context.getCurrentTexture().createView(),
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ shadow: {
+ view: shadowTextureView,
+ clearValue: [1, 1, 1, 1],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ })
+ .withDepthStencilAttachment({
+ view: depthTextureView,
+ depthClearValue: 1,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ })
+ .draw(vertexCount);
+```
+
+### Resource bindings
+
+Before executing pipelines, it is necessary to bind all of the utilized resources, like bind groups, vertex buffers and slots. It is done using the `with` method. It accepts a pair of arguments: [a bind group layout and a bind group](/TypeGPU/fundamentals/bind-groups) (render and compute pipelines) or [a vertex layout and a vertex buffer](/TypeGPU/fundamentals/vertex-layouts) (render pipelines only).
+
+```ts
+// vertex layout
+const vertexLayout = tgpu.vertexLayout(
+ (n) => d.disarrayOf(d.float16, n),
+ 'vertex',
+);
+const vertexBuffer = root
+ .createBuffer(d.disarrayOf(d.float16, 8), [0, 0, 1, 0, 0, 1, 1, 1])
+ .$usage('vertex');
+
+// bind group layout
+const bindGroupLayout = tgpu.bindGroupLayout({
+ size: { uniform: d.vec2u },
+});
+
+const sizeBuffer = root
+ .createBuffer(d.vec2u, d.vec2u(64, 64))
+ .$usage('uniform');
+
+const bindGroup = root.createBindGroup(bindGroupLayout, {
+ size: sizeBuffer,
+});
+
+// binding and execution
+renderPipeline
+ .with(vertexLayout, vertexBuffer)
+ .with(bindGroupLayout, bindGroup)
+ .draw(8);
+
+computePipeline
+ .with(bindGroupLayout, bindGroup)
+ .dispatchWorkgroups(1);
+```
+
+### Timing performance
+
+Pipelines also expose the `withPerformanceCallback` and `withTimestampWrites` methods for timing the execution time on the GPU.
+For more info about them, refer to the [Timing Your Pipelines guide](/TypeGPU/fundamentals/timestamp-queries/).
+
+### *draw*, *dispatchWorkgroups*
+
+After creating the render pipeline and setting all of the attachments, it can be put to use by calling the `draw` method.
+It accepts the number of vertices and optionally the instance count, first vertex index and first instance index.
+After calling the method, the shader is set for execution immediately.
+
+Compute pipelines are executed using the `dispatchWorkgroups` method, which accepts the number of workgroups in each dimension.
+Unlike render pipelines, after running this method, the execution is not submitted to the GPU immediately.
+In order to do so, `root['~unstable'].flush()` needs to be run.
+However, that is usually not necessary, as it is done automatically when trying to read the result of computation.
+
+## Low-level render pipeline execution API
+
+The higher-level API has several limitations, therefore another way of executing pipelines is exposed, for some custom, more demanding scenarios. For example, with the high-level API, it is not possible to execute multiple pipelines per one render pass. It also may be missing some more niche features of the WebGPU API.
+
+`root['~unstable'].beginRenderPass` is a method that mirrors the WebGPU API, but enriches it with a direct TypeGPU resource support.
+
+```ts
+root['~unstable'].beginRenderPass(
+ {
+ colorAttachments: [{
+ ...
+ }],
+ },
+ (pass) => {
+ pass.setPipeline(renderPipeline);
+ pass.setBindGroup(layout, group);
+ pass.draw(3);
+ },
+);
+
+root['~unstable'].flush();
+```
+
+It is also possible to access the underlying WebGPU resources for the TypeGPU pipelines, by calling `root.unwrap(pipeline)`.
+That way, they can be used with a regular WebGPU API, but unlike the `root['~unstable'].beginRenderPass` API, it also requires unwrapping all the necessary
+resources.
+
+```ts twoslash
+import tgpu from 'typegpu';
+import * as d from 'typegpu/data';
+
+const root = await tgpu.init();
+
+const mainVertex = tgpu['~unstable'].vertexFn({ out: { pos: d.builtin.position } })`...`;
+const mainFragment = tgpu['~unstable'].fragmentFn({ out: d.vec4f })`...`;
+
+// ---cut---
+const pipeline = root['~unstable']
+ .withVertex(mainVertex, {})
+ .withFragment(mainFragment, { format: 'rg8unorm' })
+ .createPipeline();
+
+const rawPipeline = root.unwrap(pipeline);
+// ^?
+```
+
diff --git a/apps/typegpu-docs/src/content/examples/rendering/3d-fish/index.ts b/apps/typegpu-docs/src/content/examples/rendering/3d-fish/index.ts
index d2edcbd45..73c925711 100644
--- a/apps/typegpu-docs/src/content/examples/rendering/3d-fish/index.ts
+++ b/apps/typegpu-docs/src/content/examples/rendering/3d-fish/index.ts
@@ -261,8 +261,8 @@ function frame(timestamp: DOMHighResTimeStamp) {
p.backgroundColor.z,
1,
],
- loadOp: 'clear' as const,
- storeOp: 'store' as const,
+ loadOp: 'clear',
+ storeOp: 'store',
})
.withDepthStencilAttachment({
view: depthTexture.createView(),
@@ -284,8 +284,8 @@ function frame(timestamp: DOMHighResTimeStamp) {
p.backgroundColor.z,
1,
],
- loadOp: 'load' as const,
- storeOp: 'store' as const,
+ loadOp: 'load',
+ storeOp: 'store',
})
.withDepthStencilAttachment({
view: depthTexture.createView(),
diff --git a/apps/typegpu-docs/src/content/examples/rendering/3d-fish/load-model.ts b/apps/typegpu-docs/src/content/examples/rendering/3d-fish/load-model.ts
index 0774b83cc..e35716b93 100644
--- a/apps/typegpu-docs/src/content/examples/rendering/3d-fish/load-model.ts
+++ b/apps/typegpu-docs/src/content/examples/rendering/3d-fish/load-model.ts
@@ -57,8 +57,8 @@ export async function loadModel(
);
return {
- vertexBuffer: vertexBuffer,
- polygonCount: polygonCount,
- texture: texture,
+ vertexBuffer,
+ polygonCount,
+ texture,
};
}