Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion apps/typegpu-docs/src/content/docs/apis/accessors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,36 @@ fn getColor() -> vec3f {
}
```

### Immediate variable

:::caution[Experimental]
`immediateVar` is an *unstable* feature. The API may be subject to change in the near future.
:::

Bind an [immediate variable](/TypeGPU/apis/pipelines/#immediates) to deliver the value on a per-draw basis without the overhead of a buffer, with the option to fall back to another delivery mechanism on hardware without immediates support:

```ts twoslash
import { tgpu, d } from 'typegpu';
// ---cut---
const colorAccess = tgpu.accessor(d.vec3f);

const getColor = tgpu.fn([], d.vec3f)(() => {
'use gpu';
return colorAccess.$;
});

const colorImmediate = tgpu['~unstable'].immediateVar(d.vec3f).$name('colorImmediate');
const getColorImmediate = getColor.with(colorAccess, colorImmediate);
```

```wgsl
var<immediate> colorImmediate: vec3f;

fn getColor() -> vec3f {
return colorImmediate;
}
```

### Texture view

```ts twoslash
Expand Down Expand Up @@ -480,7 +510,7 @@ fn main() {

| | Accessors | Slots | Bind Groups |
|---|---|---|---|
| **What can be bound** | GPU resources (buffers, variables, textures, constants, functions, raw values) | Any JavaScript value | Buffers, textures, samplers |
| **What can be bound** | GPU resources (buffers, variables, textures, constants, functions, immediates, raw values) | Any JavaScript value | Buffers, textures, samplers |
| **Schema-aware** | Yes — schema is declared upfront, TypeGPU validates the binding | No | Yes (layout defines types) |
| **Indirect access of deep fields** | Yes — `accessor.$` can access struct fields | No (a slot value cannot be a reference to a nested field) | N/A |
| **Potentially mutable** | Through `tgpu.mutableAccessor` | No | Yes (storage buffers) |
Expand Down
81 changes: 81 additions & 0 deletions apps/typegpu-docs/src/content/docs/apis/pipelines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,8 @@ Both styles operate on the same pass state and follow the WebGPU ordering rules:
`pipeline.with(pass).draw(...)` sets the pass's current pipeline, so a subsequent `pass.draw(...)` executes that pipeline, not one set earlier via `pass.setPipeline(...)`.
:::

Note that `pipeline.with(pass)` returns a new pipeline wrapper on every call, so hoist it out of draw loops (`const bound = pipeline.with(pass)`) to let deduplication kick in. For values that change between draws, prefer pass-level state (`pass.setBindGroup`, `pass.setImmediates`) over the allocating `with*` methods.

Compute passes work the same way:

```ts twoslash
Expand Down Expand Up @@ -726,3 +728,82 @@ const pipeline = root.createRenderPipeline({
const rawPipeline = root.unwrap(pipeline);
// ^?
```

## Immediates

:::caution[Experimental]
`immediateVar` is an *unstable* feature, built on top of a WebGPU proposal that is not yet universally available (Chrome 149+). The API may be subject to change in the near future.
:::

Immediates are small pieces of data set directly on a pass, on a per-draw (or per-dispatch) basis, without the overhead of a buffer. They are WebGPU's take on push constants.
An immediate variable is defined with `tgpu['~unstable'].immediateVar` and used in shaders through its `.$` property, like other TypeGPU resources.

```ts
const tint = tgpu['~unstable'].immediateVar(d.vec4f, d.vec4f(1)); // schema + optional default

const mainFragment = tgpu.fragmentFn({ out: d.vec4f })(() => tint.$);
```

The value is provided at execution time, either per-pipeline (`pipeline.with(immediate, value)`) or per-pass (`pass.setImmediates`).
Like all other draw state, the latest value provided to the pass wins: executing a pipeline stamps its immediate value onto the pass, a later `setImmediates` call overwrites it, and the variable's default value applies when neither was provided.
Drawing with no value available throws a `MissingImmediatesError`.

Values are captured (serialized) the moment they are provided, which makes subsequent draws that reuse
them essentially free. Mutating a vector after passing it has no effect until it is set again.

For per-draw changing data, an `ArrayBuffer` or typed array can be passed instead of a structured
value. The bytes are copied verbatim with no serialization (the caller guarantees they match the
schema's memory layout), making a thousand different transforms across a thousand draws nearly as
cheap as the raw WebGPU calls.

```ts
// per-pipeline
pipeline
.with(tint, d.vec4f(1, 0, 0, 1))
.withColorAttachment({ view: context })
.draw(3);

// per-pass
const encoder = root['~unstable'].createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context }] });
pass.setImmediates(tint, d.vec4f(1, 0, 0, 1));
pipeline.with(pass).draw(3);
pass.end();
encoder.submit();
```

Immediates come with a few WebGPU-imposed restrictions:

- Only one immediate variable can be used in a single shader. To pass multiple values, use a struct schema.
- The schema cannot contain arrays, atomics or booleans (use `d.u32` or `d.i32` instead).
- The size of the schema must be a multiple of 4 bytes (pad with `d.size` if needed, e.g. `d.size(4, d.f16)`) and is limited by the device's `maxImmediateSize` limit (64 bytes by default where supported). A higher limit can be requested at initialization via `tgpu.init({ device: { requiredLimits: { maxImmediateSize: 128 } } })`.

### Feature detection and fallback

Immediates require the `immediate_address_space` WGSL language extension, which is not yet supported everywhere.
Support can be checked via `root.enabledWgslLanguageFeatures`, and since immediate variables can fulfill [accessors](/TypeGPU/apis/accessors) (alongside buffer usages and functions), an accessor can seamlessly fall back to a uniform buffer on unsupported hardware.

```ts
const viewProj = tgpu.accessor(d.mat4x4f);

const canUseImmediates =
root.enabledWgslLanguageFeatures.has('immediate_address_space');

const viewProjImmediate = canUseImmediates
? tgpu['~unstable'].immediateVar(d.mat4x4f).$name('viewProj')
: undefined;
const viewProjUniform = viewProjImmediate
? undefined
: root.createUniform(d.mat4x4f);

const pipeline = root
.with(viewProj, viewProjImmediate ?? viewProjUniform)
.createRenderPipeline({ vertex: mainVertex, fragment: mainFragment });

// per frame:
if (viewProjImmediate) {
pass.setImmediates(viewProjImmediate, viewProjMatrix);
} else {
viewProjUniform.write(viewProjMatrix);
}
```
17 changes: 15 additions & 2 deletions packages/typegpu-testing-utility/src/extendedIt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,37 @@ export const it = base
setVertexBuffer: vi.fn(),
setIndexBuffer: vi.fn(),
setStencilReference: vi.fn(),
setImmediates: vi.fn(),
executeBundles: vi.fn(),
};

return mockRenderPassEncoder as unknown as GPURenderPassEncoder & {
mock: typeof mockRenderPassEncoder;
};
})
.extend('commandEncoder', ({ renderPassEncoder }) => {
.extend('computePassEncoder', () => {
const mockComputePassEncoder = {
get mock() {
return mockComputePassEncoder;
},
dispatchWorkgroups: vi.fn(),
dispatchWorkgroupsIndirect: vi.fn(),
end: vi.fn(),
setBindGroup: vi.fn(),
setPipeline: vi.fn(),
setImmediates: vi.fn(),
};

return mockComputePassEncoder as unknown as GPUComputePassEncoder & {
mock: typeof mockComputePassEncoder;
};
})
.extend('commandEncoder', ({ renderPassEncoder, computePassEncoder }) => {
const mockCommandEncoder = {
get mock() {
return mockCommandEncoder;
},
beginComputePass: vi.fn(() => mockComputePassEncoder),
beginComputePass: vi.fn(() => computePassEncoder),
beginRenderPass: vi.fn(() => renderPassEncoder),
clearBuffer: vi.fn(),
copyBufferToBuffer: vi.fn(),
Expand All @@ -83,6 +93,7 @@ export const it = base
setPipeline: vi.fn(),
setVertexBuffer: vi.fn(),
setIndexBuffer: vi.fn(),
setImmediates: vi.fn(),
finish: vi.fn(() => 'mockRenderBundle'),
label: '',
};
Expand Down Expand Up @@ -157,6 +168,7 @@ export const it = base
limits: {
maxUniformBuffersPerShaderStage: 12,
maxStorageBuffersPerShaderStage: 8,
maxImmediateSize: 64,
},
destroy: vi.fn(),
};
Expand Down Expand Up @@ -210,6 +222,7 @@ export const it = base
__brand: 'GPU',
requestAdapter: vi.fn(() => Promise.resolve(adapter)),
getPreferredCanvasFormat: vi.fn(() => 'bgra8unorm'),
wgslLanguageFeatures: new Set(['immediate_address_space']),
},
mediaDevices: {
getUserMedia: vi.fn(() => Promise.resolve()),
Expand Down
24 changes: 24 additions & 0 deletions packages/typegpu/src/core/commandEncoder/computePass.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { PrimitiveOffsetInfo } from '../../data/offsetUtils.ts';
import type { AnyWgslData } from '../../data/wgslTypes.ts';
import type { InferInput } from '../../shared/repr.ts';
import { $internal } from '../../shared/symbols.ts';
import type {
TgpuBindGroup,
Expand All @@ -8,6 +9,7 @@ import type {
} from '../../tgpuBindGroupLayout.ts';
import { isGPUBuffer } from '../../types.ts';
import type { IndirectFlag, TgpuBuffer } from '../buffer/buffer.ts';
import { setImmediateSnapshot, type TgpuImmediateVar } from '../immediate/immediateVar.ts';
import { DISPATCH_INDIRECT_SIZE, resolveIndirectOffset } from '../pipeline/pipelineUtils.ts';
import {
ComputeDrawState,
Expand Down Expand Up @@ -65,6 +67,21 @@ export interface TgpuComputePass {
bindGroup: TgpuBindGroup<Entries> | GPUBindGroup,
): void;

/**
* Provides a value for the given immediate variable, used by subsequent dispatches.
* The value is captured (copied) at call time; mutating it afterwards has no
* effect until it is set again. Binding a pipeline carrying its own
* immediate value (`pipeline.with(immediate, value)`) overwrites it, like
* any other pipeline-held state.
*
* Passing an `ArrayBuffer` or typed array skips serialization entirely; the bytes
* are copied verbatim and the caller guarantees they match the schema's layout.
*/
setImmediates<T extends AnyWgslData>(
immediate: TgpuImmediateVar<T>,
value: InferInput<T> | ArrayBuffer | ArrayBufferView,
): void;

dispatchWorkgroups(x: number, y?: number, z?: number): void;

/**
Expand Down Expand Up @@ -156,6 +173,13 @@ class TgpuComputePassImpl implements TgpuComputePass {
recordBindGroup(this[$internal].state, first as TgpuBindGroup | TgpuBindGroupLayout, bindGroup);
}

setImmediates<T extends AnyWgslData>(
immediate: TgpuImmediateVar<T>,
value: InferInput<T> | ArrayBuffer | ArrayBufferView,
): void {
setImmediateSnapshot(this[$internal].state.immediates, immediate, value);
}

dispatchWorkgroups(x: number, y?: number, z?: number): void {
this.#emit((rawPass) => rawPass.dispatchWorkgroups(x, y, z));
}
Expand Down
34 changes: 30 additions & 4 deletions packages/typegpu/src/core/commandEncoder/renderPass.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Disarray } from '../../data/dataTypes.ts';
import type { PrimitiveOffsetInfo } from '../../data/offsetUtils.ts';
import type { BaseData, WgslArray } from '../../data/wgslTypes.ts';
import type { AnyWgslData, BaseData, WgslArray } from '../../data/wgslTypes.ts';
import type { InferInput } from '../../shared/repr.ts';
import { $internal } from '../../shared/symbols.ts';
import type {
TgpuBindGroup,
Expand All @@ -9,6 +10,7 @@ import type {
} from '../../tgpuBindGroupLayout.ts';
import { isGPUBuffer } from '../../types.ts';
import type { IndexFlag, IndirectFlag, TgpuBuffer, VertexFlag } from '../buffer/buffer.ts';
import { setImmediateSnapshot, type TgpuImmediateVar } from '../immediate/immediateVar.ts';
import {
DRAW_INDEXED_INDIRECT_SIZE,
DRAW_INDIRECT_SIZE,
Expand Down Expand Up @@ -109,6 +111,21 @@ export interface TgpuRenderCommands {
size?: number,
): void;

/**
* Provides a value for the given immediate variable, used by subsequent draw calls.
* The value is captured (copied) at call time; mutating it afterwards has no
* effect until it is set again. Binding a pipeline carrying its own
* immediate value (`pipeline.with(immediate, value)`) overwrites it, like
* any other pipeline-held state.
*
* Passing an `ArrayBuffer` or typed array skips serialization entirely; the bytes
* are copied verbatim and the caller guarantees they match the schema's layout.
*/
setImmediates<T extends AnyWgslData>(
immediate: TgpuImmediateVar<T>,
value: InferInput<T> | ArrayBuffer | ArrayBufferView,
): void;

draw(
vertexCount: number,
instanceCount?: number,
Expand Down Expand Up @@ -189,9 +206,10 @@ export interface TgpuRenderPass extends TgpuRenderCommands {

/**
* Executes previously recorded {@link GPURenderBundle}s as part of this pass.
* As per the WebGPU spec, this resets the raw pass's pipeline, bind group
* and vertex/index buffer state. The state tracked by this typed pass is
* re-applied on the next draw.
* As per the WebGPU spec, this resets the raw pass's pipeline, bind group,
* vertex/index buffer and immediate data state. The state tracked by this
* typed pass (including values set via `setImmediates`) is re-applied on
* the next draw.
*/
executeBundles(bundles: Iterable<GPURenderBundle>): void;

Expand Down Expand Up @@ -410,6 +428,13 @@ class TgpuRenderCommandsImpl<
state.version++;
}

setImmediates<T extends AnyWgslData>(
immediate: TgpuImmediateVar<T>,
value: InferInput<T> | ArrayBuffer | ArrayBufferView,
): void {
setImmediateSnapshot(this[$internal].state.immediates, immediate, value);
}

draw(
vertexCount: number,
instanceCount?: number,
Expand Down Expand Up @@ -518,6 +543,7 @@ class TgpuRenderPassImpl
const internals = this[$internal];
internals.rawPass.executeBundles(bundles);
internals.appliedVersion = undefined;
internals.state.lastWrittenSnapshot = undefined;
}

end(): void {
Expand Down
Loading
Loading