Skip to content

Commit 1f3bf19

Browse files
authored
🎁: Add an alternate slot-value propagation mechanism, allowing for slot values to pass through a derived boundary (#697)
* Improved error reporting for missing bind groups and vertex buffers
1 parent 773e33d commit 1f3bf19

File tree

16 files changed

+380
-73
lines changed

16 files changed

+380
-73
lines changed

packages/typegpu/src/core/function/tgpuFn.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {
1212
import type { TgpuBufferUsage } from '../buffer/bufferUsage';
1313
import {
1414
type Eventual,
15+
type Providing,
16+
type SlotValuePair,
1517
type TgpuAccessor,
1618
type TgpuSlot,
1719
isAccessor,
@@ -56,6 +58,7 @@ interface TgpuFnBase<
5658
Labelled {
5759
readonly resourceType: 'function';
5860
readonly shell: TgpuFnShell<Args, Return>;
61+
readonly '~providing'?: Providing | undefined;
5962

6063
$uses(dependencyMap: Record<string, unknown>): this;
6164
with<T>(slot: TgpuSlot<T>, value: Eventual<T>): TgpuFn<Args, Return>;
@@ -108,6 +111,10 @@ export function isTgpuFn<
108111
// Implementation
109112
// --------------
110113

114+
function stringifyPair([slot, value]: SlotValuePair): string {
115+
return `${slot.label ?? '<unnamed>'}=${value}`;
116+
}
117+
111118
function createFn<
112119
Args extends AnyWgslData[],
113120
Return extends AnyWgslData | undefined,
@@ -137,11 +144,9 @@ function createFn<
137144
slot: TgpuSlot<T> | TgpuAccessor<T>,
138145
value: T | TgpuFn<[], T> | TgpuBufferUsage<T> | Infer<T>,
139146
): TgpuFn<Args, Return> {
140-
return createBoundFunction(
141-
fn,
142-
isAccessor(slot) ? slot.slot : slot,
143-
value,
144-
);
147+
return createBoundFunction(fn, [
148+
[isAccessor(slot) ? slot.slot : slot, value],
149+
]);
145150
},
146151

147152
'~resolve'(ctx: ResolutionCtx): string {
@@ -181,16 +186,16 @@ function createFn<
181186
function createBoundFunction<
182187
Args extends AnyWgslData[],
183188
Return extends AnyWgslData | undefined,
184-
>(
185-
innerFn: TgpuFn<Args, Return>,
186-
slot: TgpuSlot<unknown>,
187-
slotValue: unknown,
188-
): TgpuFn<Args, Return> {
189-
type This = TgpuFnBase<Args, Return> & SelfResolvable;
189+
>(innerFn: TgpuFn<Args, Return>, pairs: SlotValuePair[]): TgpuFn<Args, Return> {
190+
type This = TgpuFnBase<Args, Return>;
190191

191192
const fnBase: This = {
192193
resourceType: 'function',
193194
shell: innerFn.shell,
195+
'~providing': {
196+
inner: innerFn,
197+
pairs,
198+
},
194199

195200
$uses(newExternals) {
196201
innerFn.$uses(newExternals);
@@ -206,15 +211,10 @@ function createBoundFunction<
206211
slot: TgpuSlot<T> | TgpuAccessor<T>,
207212
value: T | TgpuFn<[], T> | TgpuBufferUsage<T> | Infer<T>,
208213
): TgpuFn<Args, Return> {
209-
return createBoundFunction(
210-
fn,
211-
isAccessor(slot) ? slot.slot : slot,
212-
value,
213-
);
214-
},
215-
216-
'~resolve'(ctx: ResolutionCtx): string {
217-
return ctx.withSlots([[slot, slotValue]], () => ctx.resolve(innerFn));
214+
return createBoundFunction(fn, [
215+
...pairs,
216+
[isAccessor(slot) ? slot.slot : slot, value],
217+
]);
218218
},
219219
};
220220

@@ -236,7 +236,9 @@ function createBoundFunction<
236236

237237
Object.defineProperty(fn, 'toString', {
238238
value() {
239-
return `fn:${innerFn.label ?? '<unnamed>'}[${slot.label ?? '<unnamed>'}=${slotValue}]`;
239+
const fnLabel = innerFn.label ?? '<unnamed>';
240+
241+
return `fn:${fnLabel}[${pairs.map(stringifyPair).join(', ')}]`;
240242
},
241243
});
242244

packages/typegpu/src/core/pipeline/computePipeline.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MissingBindGroupError } from '../../errors';
1+
import { MissingBindGroupsError } from '../../errors';
22
import type { TgpuNamable } from '../../namable';
33
import { resolve } from '../../resolutionCtx';
44
import type {
@@ -100,26 +100,34 @@ class TgpuComputePipelineImpl
100100
z?: number | undefined,
101101
): void {
102102
const memo = this._core.unwrap();
103+
const { branch, label } = this._core;
103104

104-
const pass = this._core.branch.commandEncoder.beginComputePass({
105-
label: this._core.label ?? '<unnamed>',
105+
const pass = branch.commandEncoder.beginComputePass({
106+
label: label ?? '<unnamed>',
106107
});
107108

108109
pass.setPipeline(memo.pipeline);
109110

111+
const missingBindGroups = new Set(memo.bindGroupLayouts);
112+
110113
memo.bindGroupLayouts.forEach((layout, idx) => {
111114
if (memo.catchall && idx === memo.catchall[0]) {
112115
// Catch-all
113-
pass.setBindGroup(idx, this._core.branch.unwrap(memo.catchall[1]));
116+
pass.setBindGroup(idx, branch.unwrap(memo.catchall[1]));
117+
missingBindGroups.delete(layout);
114118
} else {
115119
const bindGroup = this._priors.bindGroupLayoutMap?.get(layout);
116-
if (bindGroup === undefined) {
117-
throw new MissingBindGroupError(layout.label);
120+
if (bindGroup !== undefined) {
121+
missingBindGroups.delete(layout);
122+
pass.setBindGroup(idx, branch.unwrap(bindGroup));
118123
}
119-
pass.setBindGroup(idx, this._core.branch.unwrap(bindGroup));
120124
}
121125
});
122126

127+
if (missingBindGroups.size > 0) {
128+
throw new MissingBindGroupsError(missingBindGroups);
129+
}
130+
123131
pass.dispatchWorkgroups(x, y, z);
124132
pass.end();
125133
}

packages/typegpu/src/core/pipeline/renderPipeline.ts

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { TgpuBuffer, Vertex } from '../../core/buffer/buffer';
22
import type { Disarray } from '../../data/dataTypes';
33
import type { AnyWgslData, WgslArray } from '../../data/wgslTypes';
4-
import { MissingBindGroupError } from '../../errors';
4+
import {
5+
MissingBindGroupsError,
6+
MissingVertexBuffersError,
7+
} from '../../errors';
58
import type { TgpuNamable } from '../../namable';
69
import { resolve } from '../../resolutionCtx';
710
import type { AnyVertexAttribs } from '../../shared/vertexFormat';
@@ -48,6 +51,10 @@ export interface TgpuRenderPipeline<Output extends IOLayout = IOLayout>
4851
attachment: FragmentOutToColorAttachment<Output>,
4952
): TgpuRenderPipeline<IOLayout>;
5053

54+
withDepthStencilAttachment(
55+
attachment: DepthStencilAttachment,
56+
): TgpuRenderPipeline<IOLayout>;
57+
5158
draw(
5259
vertexCount: number,
5360
instanceCount?: number,
@@ -111,6 +118,60 @@ export interface ColorAttachment {
111118
storeOp: GPUStoreOp;
112119
}
113120

121+
export interface DepthStencilAttachment {
122+
/**
123+
* A {@link GPUTextureView} | ({@link TgpuTexture} & {@link Render}) describing the texture subresource that will be output to
124+
* and read from for this depth/stencil attachment.
125+
*/
126+
view: (TgpuTexture & Render) | GPUTextureView;
127+
/**
128+
* Indicates the value to clear {@link GPURenderPassDepthStencilAttachment#view}'s depth component
129+
* to prior to executing the render pass. Ignored if {@link GPURenderPassDepthStencilAttachment#depthLoadOp}
130+
* is not {@link GPULoadOp#"clear"}. Must be between 0.0 and 1.0, inclusive (unless unrestricted depth is enabled).
131+
*/
132+
depthClearValue?: number;
133+
/**
134+
* Indicates the load operation to perform on {@link GPURenderPassDepthStencilAttachment#view}'s
135+
* depth component prior to executing the render pass.
136+
* Note: It is recommended to prefer clearing; see {@link GPULoadOp#"clear"} for details.
137+
*/
138+
depthLoadOp?: GPULoadOp;
139+
/**
140+
* The store operation to perform on {@link GPURenderPassDepthStencilAttachment#view}'s
141+
* depth component after executing the render pass.
142+
*/
143+
depthStoreOp?: GPUStoreOp;
144+
/**
145+
* Indicates that the depth component of {@link GPURenderPassDepthStencilAttachment#view}
146+
* is read only.
147+
*/
148+
depthReadOnly?: boolean;
149+
/**
150+
* Indicates the value to clear {@link GPURenderPassDepthStencilAttachment#view}'s stencil component
151+
* to prior to executing the render pass. Ignored if {@link GPURenderPassDepthStencilAttachment#stencilLoadOp}
152+
* is not {@link GPULoadOp#"clear"}.
153+
* The value will be converted to the type of the stencil aspect of `view` by taking the same
154+
* number of LSBs as the number of bits in the stencil aspect of one texel block|texel of `view`.
155+
*/
156+
stencilClearValue?: GPUStencilValue;
157+
/**
158+
* Indicates the load operation to perform on {@link GPURenderPassDepthStencilAttachment#view}'s
159+
* stencil component prior to executing the render pass.
160+
* Note: It is recommended to prefer clearing; see {@link GPULoadOp#"clear"} for details.
161+
*/
162+
stencilLoadOp?: GPULoadOp;
163+
/**
164+
* The store operation to perform on {@link GPURenderPassDepthStencilAttachment#view}'s
165+
* stencil component after executing the render pass.
166+
*/
167+
stencilStoreOp?: GPUStoreOp;
168+
/**
169+
* Indicates that the stencil component of {@link GPURenderPassDepthStencilAttachment#view}
170+
* is read only.
171+
*/
172+
stencilReadOnly?: boolean;
173+
}
174+
114175
export type AnyFragmentColorAttachment =
115176
| ColorAttachment
116177
| Record<string, ColorAttachment>;
@@ -122,6 +183,7 @@ export type RenderPipelineCoreOptions = {
122183
vertexFn: TgpuVertexFn;
123184
fragmentFn: TgpuFragmentFn;
124185
primitiveState: GPUPrimitiveState | undefined;
186+
depthStencilState: GPUDepthStencilState | undefined;
125187
targets: AnyFragmentTargets;
126188
};
127189

@@ -143,6 +205,7 @@ type TgpuRenderPipelinePriors = {
143205
| Map<TgpuBindGroupLayout, TgpuBindGroup>
144206
| undefined;
145207
readonly colorAttachment?: AnyFragmentColorAttachment | undefined;
208+
readonly depthStencilAttachment?: DepthStencilAttachment | undefined;
146209
};
147210

148211
type Memo = {
@@ -212,6 +275,15 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline {
212275
});
213276
}
214277

278+
withDepthStencilAttachment(
279+
attachment: DepthStencilAttachment,
280+
): TgpuRenderPipeline {
281+
return new TgpuRenderPipelineImpl(this._core, {
282+
...this._priors,
283+
depthStencilAttachment: attachment,
284+
});
285+
}
286+
215287
draw(
216288
vertexCount: number,
217289
instanceCount?: number,
@@ -235,37 +307,68 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline {
235307
return attachment;
236308
}) as GPURenderPassColorAttachment[];
237309

238-
const pass = branch.commandEncoder.beginRenderPass({
239-
label: this._core.label ?? '<unnamed>',
310+
const renderPassDescriptor: GPURenderPassDescriptor = {
240311
colorAttachments,
241-
});
312+
};
313+
314+
if (this._core.label !== undefined) {
315+
renderPassDescriptor.label = this._core.label;
316+
}
317+
318+
if (this._priors.depthStencilAttachment !== undefined) {
319+
const attachment = this._priors.depthStencilAttachment;
320+
if (isTexture(attachment.view)) {
321+
renderPassDescriptor.depthStencilAttachment = {
322+
...attachment,
323+
view: branch.unwrap(attachment.view).createView(),
324+
};
325+
} else {
326+
renderPassDescriptor.depthStencilAttachment =
327+
attachment as GPURenderPassDepthStencilAttachment;
328+
}
329+
}
330+
331+
const pass = branch.commandEncoder.beginRenderPass(renderPassDescriptor);
242332

243333
pass.setPipeline(memo.pipeline);
244334

335+
const missingBindGroups = new Set(memo.bindGroupLayouts);
336+
245337
memo.bindGroupLayouts.forEach((layout, idx) => {
246338
if (memo.catchall && idx === memo.catchall[0]) {
247339
// Catch-all
248340
pass.setBindGroup(idx, branch.unwrap(memo.catchall[1]));
341+
missingBindGroups.delete(layout);
249342
} else {
250343
const bindGroup = this._priors.bindGroupLayoutMap?.get(layout);
251-
if (bindGroup === undefined) {
252-
throw new MissingBindGroupError(layout.label);
344+
if (bindGroup !== undefined) {
345+
missingBindGroups.delete(layout);
346+
pass.setBindGroup(idx, branch.unwrap(bindGroup));
253347
}
254-
pass.setBindGroup(idx, branch.unwrap(bindGroup));
255348
}
256349
});
257350

258-
this._core.usedVertexLayouts.forEach((vertexLayout, idx) => {
351+
const missingVertexLayouts = new Set(this._core.usedVertexLayouts);
352+
353+
const usedVertexLayouts = this._core.usedVertexLayouts;
354+
usedVertexLayouts.forEach((vertexLayout, idx) => {
259355
const buffer = this._priors.vertexLayoutMap?.get(vertexLayout);
260-
if (!buffer) {
261-
throw new Error(
262-
`Missing vertex buffer for layout '${vertexLayout.label ?? '<unnamed>'}'. Please provide it using pipeline.with(layout, buffer).(...)`,
263-
);
356+
if (buffer) {
357+
missingVertexLayouts.delete(vertexLayout);
358+
pass.setVertexBuffer(idx, branch.unwrap(buffer));
264359
}
265-
pass.setVertexBuffer(idx, branch.unwrap(buffer));
266360
});
267361

362+
if (missingBindGroups.size > 0) {
363+
throw new MissingBindGroupsError(missingBindGroups);
364+
}
365+
366+
if (missingVertexLayouts.size > 0) {
367+
throw new MissingVertexBuffersError(missingVertexLayouts);
368+
}
369+
268370
pass.draw(vertexCount, instanceCount, firstVertex, firstInstance);
371+
269372
pass.end();
270373
}
271374
}
@@ -295,8 +398,14 @@ class RenderPipelineCore {
295398

296399
public unwrap(): Memo {
297400
if (this._memo === undefined) {
298-
const { branch, vertexFn, fragmentFn, slotBindings, primitiveState } =
299-
this.options;
401+
const {
402+
branch,
403+
vertexFn,
404+
fragmentFn,
405+
slotBindings,
406+
primitiveState,
407+
depthStencilState,
408+
} = this.options;
300409

301410
// Resolving code
302411
const { code, bindGroupLayouts, catchall } = resolve(
@@ -353,6 +462,10 @@ class RenderPipelineCore {
353462
descriptor.primitive = primitiveState;
354463
}
355464

465+
if (depthStencilState) {
466+
descriptor.depthStencil = depthStencilState;
467+
}
468+
356469
this._memo = {
357470
pipeline: device.createRenderPipeline(descriptor),
358471
bindGroupLayouts,

packages/typegpu/src/core/root/init.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class WithBindingImpl implements WithBinding {
103103
return new WithVertexImpl({
104104
branch: this._getRoot(),
105105
primitiveState: undefined,
106+
depthStencilState: undefined,
106107
slotBindings: this._slotBindings,
107108
vertexFn,
108109
vertexAttribs: attribs as AnyVertexAttribs,
@@ -157,6 +158,12 @@ class WithFragmentImpl implements WithFragment {
157158
return new WithFragmentImpl({ ...this._options, primitiveState });
158159
}
159160

161+
withDepthStencil(
162+
depthStencilState: GPUDepthStencilState | undefined,
163+
): WithFragment {
164+
return new WithFragmentImpl({ ...this._options, depthStencilState });
165+
}
166+
160167
createPipeline(): TgpuRenderPipeline {
161168
return INTERNAL_createRenderPipeline(this._options);
162169
}

0 commit comments

Comments
 (0)