typegpu 0.12.6 → 0.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/common/writeSoA.js +16 -0
  2. package/core/buffer/buffer.js +47 -11
  3. package/core/commandEncoder/attachments.js +5 -2
  4. package/core/function/autoIO.js +4 -2
  5. package/core/function/ioSchema.d.ts +7 -2
  6. package/core/function/ioSchema.js +41 -15
  7. package/core/function/tgpuFragmentFn.d.ts +1 -1
  8. package/core/function/tgpuFragmentFn.js +2 -1
  9. package/core/function/tgpuVertexFn.js +2 -1
  10. package/core/pipeline/connectAttachmentToShader.d.ts +2 -2
  11. package/core/pipeline/connectAttachmentToShader.js +16 -21
  12. package/core/pipeline/connectTargetsToShader.d.ts +5 -1
  13. package/core/pipeline/connectTargetsToShader.js +23 -18
  14. package/core/pipeline/renderPipeline.d.ts +5 -4
  15. package/core/pipeline/renderPipeline.js +10 -12
  16. package/core/resolve/namespace.js +2 -2
  17. package/core/root/rootTypes.d.ts +6 -2
  18. package/core/slot/accessor.d.ts +1 -1
  19. package/core/slot/accessor.js +72 -5
  20. package/core/texture/texture.js +8 -4
  21. package/data/autoStruct.d.ts +1 -1
  22. package/data/autoStruct.js +8 -4
  23. package/data/deepEqual.js +19 -9
  24. package/data/matrix.js +10 -3
  25. package/data/partialIO.d.ts +8 -1
  26. package/data/partialIO.js +36 -19
  27. package/data/ref.js +2 -2
  28. package/data/snippet.d.ts +6 -4
  29. package/data/snippet.js +8 -4
  30. package/data/wgslTypes.d.ts +3 -0
  31. package/internal.d.ts +1 -0
  32. package/nameUtils.d.ts +7 -0
  33. package/nameUtils.js +60 -0
  34. package/package.json +1 -1
  35. package/shared/meta.js +1 -1
  36. package/shared/symbols.js +1 -1
  37. package/shared/tseynit.js +54 -50
  38. package/std/boolean.js +9 -10
  39. package/std/operators.js +9 -4
  40. package/std/texture.d.ts +2 -2
  41. package/tgsl/accessIndex.js +19 -23
  42. package/tgsl/forOfUtils.js +6 -6
  43. package/tgsl/generationHelpers.js +2 -2
  44. package/tgsl/wgslGenerator.js +85 -23
@@ -5,6 +5,7 @@ import { undecorate } from "../data/dataTypes.js";
5
5
  import { offsetsForProps } from "../data/offsets.js";
6
6
  import { sizeOf } from "../data/sizeOf.js";
7
7
  import { isAtomic, isMat, isMat2x2f, isMat3x3f, isWgslArray } from "../data/wgslTypes.js";
8
+ import { getName } from "../shared/meta.js";
8
9
  function packedSchemaOf(schema) {
9
10
  const unpackedSchema = undecorate(schema);
10
11
  return isAtomic(unpackedSchema) ? unpackedSchema.inner : unpackedSchema;
@@ -92,6 +93,17 @@ function scatterSoA(target, arraySchema, soaData, startOffset, endOffset) {
92
93
  }
93
94
  }
94
95
  }
96
+ // Mirrors the check in `buffer.write()`. That check only runs after the data has been
97
+ // scattered into `buffer.arrayBuffer`, so we check up front to leave it untouched on failure.
98
+ function assertAlignedWrite(buffer, startOffset, endOffset) {
99
+ // Writes that reach the end of the schema also cover the padding up to a multiple of 4
100
+ const paddedEndOffset = endOffset === sizeOf(buffer.dataType) ? roundUp(endOffset, 4) : endOffset;
101
+ if (startOffset % 4 !== 0 || (paddedEndOffset - startOffset) % 4 !== 0) {
102
+ throw new Error(`Cannot write to bytes ${startOffset}-${endOffset} of buffer '${getName(buffer) ?? '<unnamed>'}'. ` +
103
+ 'WebGPU requires writes to start and end at a multiple of 4 bytes. ' +
104
+ 'Align the range to 4 bytes, write the whole buffer, or use 4-byte elements (e.g. d.u32 instead of d.u16).');
105
+ }
106
+ }
95
107
  export function writeSoA(buffer, data, options) {
96
108
  const arrayBuffer = buffer.arrayBuffer;
97
109
  const startOffset = options?.startOffset ?? 0;
@@ -99,6 +111,10 @@ export function writeSoA(buffer, data, options) {
99
111
  const naturalSize = computeSoAByteLength(buffer.dataType, data);
100
112
  const endOffset = options?.endOffset ??
101
113
  (naturalSize === undefined ? bufferSize : Math.min(startOffset + naturalSize, bufferSize));
114
+ // Writes to mapped memory have no alignment rules
115
+ if (buffer.buffer.mapState !== 'mapped') {
116
+ assertAlignedWrite(buffer, startOffset, endOffset);
117
+ }
102
118
  scatterSoA(new Uint8Array(arrayBuffer), buffer.dataType, data, startOffset, endOffset);
103
119
  buffer.write(arrayBuffer, { startOffset, endOffset });
104
120
  }
@@ -1,6 +1,7 @@
1
1
  import { getCompiledWriter } from "../../data/compiledIO.js";
2
2
  import { convertPartialToPatch, getPatchInstructions } from "../../data/partialIO.js";
3
3
  import { sizeOf } from "../../data/sizeOf.js";
4
+ import { roundUp } from "../../mathUtils.js";
4
5
  import { isWgslData } from "../../data/wgslTypes.js";
5
6
  import { getName, setName } from "../../shared/meta.js";
6
7
  import { $internal, $soul } from "../../shared/symbols.js";
@@ -10,6 +11,14 @@ import { patchArrayBuffer } from "../../data/partialIO.js";
10
11
  import { mutable, readonly, uniform, } from "./bufferBinding.js";
11
12
  import { warnIfNotUniformAligned } from "../pipeline/webgpuLimitations.js";
12
13
  const usageToUsageConstructor = { uniform, mutable, readonly };
14
+ /**
15
+ * WebGPU requires buffer sizes, writes, copies and mappings
16
+ * to be multiples of 4 bytes. Schemas like `arrayOf(u16, 3)` can have a smaller logical
17
+ * size, so the underlying allocation gets padded up to the next multiple of 4.
18
+ */
19
+ function physicalSizeOf(schema) {
20
+ return roundUp(sizeOf(schema), 4);
21
+ }
13
22
  export function INTERNAL_createBuffer(group, typeSchema, initialOrBuffer) {
14
23
  if (!isWgslData(typeSchema)) {
15
24
  return new TgpuBufferImpl(group, typeSchema, initialOrBuffer, ['storage', 'uniform']);
@@ -32,7 +41,7 @@ class TgpuBufferImpl {
32
41
  #destroyed = false;
33
42
  #internalBuffer;
34
43
  get #hostBuffer() {
35
- return (this.#internalBuffer ??= new ArrayBuffer(sizeOf(this.dataType)));
44
+ return (this.#internalBuffer ??= new ArrayBuffer(physicalSizeOf(this.dataType)));
36
45
  }
37
46
  #mappedRange;
38
47
  #initialCallback;
@@ -76,7 +85,7 @@ class TgpuBufferImpl {
76
85
  const soul = this[$soul];
77
86
  if (!soul.raw) {
78
87
  soul.raw = soul.device.createBuffer({
79
- size: sizeOf(soul.dataType),
88
+ size: physicalSizeOf(soul.dataType),
80
89
  usage: soul.flags,
81
90
  mappedAtCreation: !!this.initial || !!this.#initialCallback,
82
91
  label: getName(this) ?? '<unnamed>',
@@ -195,15 +204,31 @@ class TgpuBufferImpl {
195
204
  writeToArrayBuffer(mapped, this.dataType, data, options);
196
205
  return;
197
206
  }
207
+ const { startOffset, endOffset } = calculateOffsets(options, this.dataType, data);
208
+ const size = this.#alignedWriteSize(startOffset, endOffset);
198
209
  // If the caller already wrote directly into #hostBuffer via
199
210
  // arrayBuffer, skip the redundant copy, the data is already in place.
200
211
  if (!(data instanceof ArrayBuffer && data === this.#hostBuffer)) {
201
212
  writeToArrayBuffer(this.#hostBuffer, this.dataType, data, options);
202
213
  }
203
- const { startOffset, endOffset } = calculateOffsets(options, this.dataType, data);
204
- const size = endOffset - startOffset;
205
214
  this[$soul].device.queue.writeBuffer(gpuBuffer, startOffset, this.#hostBuffer, startOffset, size);
206
215
  }
216
+ /**
217
+ * Returns the number of bytes to upload when writing to `[startOffset, endOffset)`.
218
+ * Writes that reach the end of the schema also cover the trailing padding (see {@link physicalSizeOf}).
219
+ * Other writes have to be 4-byte aligned already, since filling in the neighboring bytes
220
+ * from the host could overwrite data that was changed on the GPU.
221
+ */
222
+ #alignedWriteSize(startOffset, endOffset) {
223
+ const paddedEndOffset = endOffset === sizeOf(this.dataType) ? physicalSizeOf(this.dataType) : endOffset;
224
+ const size = paddedEndOffset - startOffset;
225
+ if (startOffset % 4 !== 0 || size % 4 !== 0) {
226
+ throw new Error(`Cannot write to bytes ${startOffset}-${endOffset} of buffer '${getName(this) ?? '<unnamed>'}'. ` +
227
+ 'WebGPU requires writes to start and end at a multiple of 4 bytes. ' +
228
+ 'Align the range to 4 bytes, write the whole buffer, or use 4-byte elements (e.g. d.u32 instead of d.u16).');
229
+ }
230
+ return size;
231
+ }
207
232
  /** @deprecated Use {@link patch} instead. */
208
233
  writePartial(data) {
209
234
  this.patch(convertPartialToPatch(this.dataType, data));
@@ -214,9 +239,19 @@ class TgpuBufferImpl {
214
239
  patchArrayBuffer(this.#getMappedRange(), this.dataType, data);
215
240
  }
216
241
  else {
217
- const instructions = getPatchInstructions(this.dataType, data, this.#hostBuffer);
218
- for (const { data, gpuOffset } of instructions) {
219
- this[$soul].device.queue.writeBuffer(gpuBuffer, gpuOffset, data);
242
+ // Every range is validated before the patch touches #hostBuffer, so a failing patch
243
+ // is neither uploaded halfway nor left behind in `arrayBuffer`
244
+ const instructions = getPatchInstructions(this.dataType, data, this.#hostBuffer, (start, end) => this.#alignedWriteSize(start, end));
245
+ for (const { data, gpuOffset, uploadSize } of instructions) {
246
+ if (uploadSize === data.byteLength) {
247
+ this[$soul].device.queue.writeBuffer(gpuBuffer, gpuOffset, data);
248
+ }
249
+ else {
250
+ // Extending the write into the trailing padding (see physicalSizeOf)
251
+ const padded = new Uint8Array(uploadSize);
252
+ padded.set(data);
253
+ this[$soul].device.queue.writeBuffer(gpuBuffer, gpuOffset, padded);
254
+ }
220
255
  }
221
256
  }
222
257
  }
@@ -238,7 +273,7 @@ class TgpuBufferImpl {
238
273
  if (this.buffer.mapState === 'mapped') {
239
274
  throw new Error('Cannot copy to a mapped buffer.');
240
275
  }
241
- const size = sizeOf(this.dataType);
276
+ const size = physicalSizeOf(this.dataType);
242
277
  if (encoder) {
243
278
  encoder[$internal].rawEncoder.copyBufferToBuffer(srcBuffer.buffer, 0, this.buffer, 0, size);
244
279
  return;
@@ -258,14 +293,15 @@ class TgpuBufferImpl {
258
293
  this.#unmapBuffer();
259
294
  return res;
260
295
  }
296
+ const size = physicalSizeOf(this.dataType);
261
297
  const stagingBuffer = this[$soul].device.createBuffer({
262
- size: sizeOf(this.dataType),
298
+ size,
263
299
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
264
300
  });
265
301
  const commandEncoder = this[$soul].device.createCommandEncoder();
266
- commandEncoder.copyBufferToBuffer(gpuBuffer, 0, stagingBuffer, 0, sizeOf(this.dataType));
302
+ commandEncoder.copyBufferToBuffer(gpuBuffer, 0, stagingBuffer, 0, size);
267
303
  this[$soul].device.queue.submit([commandEncoder.finish()]);
268
- await stagingBuffer.mapAsync(GPUMapMode.READ, 0, sizeOf(this.dataType));
304
+ await stagingBuffer.mapAsync(GPUMapMode.READ, 0, size);
269
305
  const res = readFromArrayBuffer(stagingBuffer.getMappedRange(), this.dataType);
270
306
  stagingBuffer.unmap();
271
307
  stagingBuffer.destroy();
@@ -1,16 +1,19 @@
1
+ import { WeakMemo } from "../../memo.js";
1
2
  import { $internal } from "../../shared/symbols.js";
2
3
  import { isQuerySet } from "../querySet/querySet.js";
3
4
  import { isGPUCanvasContext } from "../pipeline/typeGuards.js";
4
5
  import { isTexture, isTextureView, } from "../texture/texture.js";
6
+ // Canvas contexts return the same texture within a frame, so its identity is enough to cache views
7
+ const defaultAttachmentViews = new WeakMemo((texture) => texture.createView());
5
8
  export function unwrapAttachmentView(root, view) {
6
9
  if (isTexture(view)) {
7
- return root.unwrap(view).createView();
10
+ return defaultAttachmentViews.getOrMake(root.unwrap(view));
8
11
  }
9
12
  if (isTextureView(view)) {
10
13
  return root.unwrap(view);
11
14
  }
12
15
  if (isGPUCanvasContext(view)) {
13
- return view.getCurrentTexture().createView();
16
+ return defaultAttachmentViews.getOrMake(view.getCurrentTexture());
14
17
  }
15
18
  return view;
16
19
  }
@@ -39,7 +39,8 @@ export class AutoFragmentFn {
39
39
  setName(impl, 'fragmentFn');
40
40
  }
41
41
  this.#core = createFnCore(impl, 'fragment');
42
- this.autoIn = new AutoStruct({ ...builtinFragmentIn, ...varyings }, undefined, locations);
42
+ this.autoIn = new AutoStruct({ ...builtinFragmentIn, ...varyings }, undefined, locations,
43
+ /* autoInterpolateIntegers */ true);
43
44
  setName(this.autoIn, 'FragmentIn');
44
45
  this.autoOut = new AutoStruct(builtinFragmentOut, vec4f);
45
46
  setName(this.autoOut, 'FragmentOut');
@@ -68,7 +69,8 @@ export class AutoVertexFn {
68
69
  this.#core = createFnCore(impl, 'vertex');
69
70
  this.autoIn = new AutoStruct({ ...builtinVertexIn, ...attribs }, undefined, locations);
70
71
  setName(this.autoIn, 'VertexIn');
71
- this.autoOut = new AutoStruct(builtinVertexOut, undefined);
72
+ this.autoOut = new AutoStruct(builtinVertexOut, undefined, undefined,
73
+ /* autoInterpolateIntegers */ true);
72
74
  setName(this.autoOut, 'VertexOut');
73
75
  }
74
76
  toString() {
@@ -7,7 +7,12 @@ export type WithLocations<T extends Record<string, BaseData>> = {
7
7
  export type IOLayoutToSchema<T> = T extends BaseData ? HasCustomLocation<T> extends true ? T : Decorate<T, Location<0>> : T extends Record<string, BaseData> ? WgslStruct<WithLocations<T>> : T extends {
8
8
  type: 'void';
9
9
  } ? void : never;
10
- export declare function withLocations<T extends BaseData>(members: Record<string, T> | undefined, locations?: Record<string, number>): Record<string, BaseData>;
10
+ export declare function withLocations<T extends BaseData>(members: Record<string, T> | undefined, locations?: Record<string, number>, autoInterpolateIntegers?: boolean): Record<string, BaseData>;
11
11
  export declare function separateBuiltins(schema: Record<string, BaseData>, locations?: Record<string, number>): SeparatedEntryArgs;
12
12
  export declare function separateAllAsPositional(schema: Record<string, BaseData>): SeparatedEntryArgs;
13
- export declare function createIoSchema<T extends BaseData | Record<string, BaseData>>(layout: T, locations?: Record<string, number>): IOLayoutToSchema<T>;
13
+ export declare function createIoSchema<T extends BaseData | Record<string, BaseData>>(layout: T, locations?: Record<string, number>, autoInterpolateIntegers?: boolean): IOLayoutToSchema<T>;
14
+ /**
15
+ * WGSL requires integer inter-stage values to be flat interpolated. Interpolation is only
16
+ * inferred for shellless entry functions, so shells have to specify it explicitly.
17
+ */
18
+ export declare function assertIntegerVaryingsInterpolated(layout: BaseData | Record<string, BaseData> | undefined, location: string): void;
@@ -1,9 +1,9 @@
1
- import { location, } from "../../data/attributes.js";
1
+ import { interpolate, location, } from "../../data/attributes.js";
2
2
  import { isBuiltin } from "../../data/attributes.js";
3
3
  import { getCustomLocation, isData } from "../../data/dataTypes.js";
4
4
  import { INTERNAL_createStruct } from "../../data/struct.js";
5
- import { isVoid } from "../../data/wgslTypes.js";
6
- export function withLocations(members, locations = {}) {
5
+ import { isDecorated, isInteger, isIntegerVec, isInterpolateAttrib, isVoid, } from "../../data/wgslTypes.js";
6
+ export function withLocations(members, locations = {}, autoInterpolateIntegers = false) {
7
7
  let nextLocation = 0;
8
8
  const usedCustomLocations = new Set();
9
9
  return Object.fromEntries(Object.entries(members ?? {})
@@ -15,7 +15,10 @@ export function withLocations(members, locations = {}) {
15
15
  }
16
16
  usedCustomLocations.add(customLocation);
17
17
  }
18
- return [key, member];
18
+ return [
19
+ key,
20
+ autoInterpolateIntegers ? withFlatInterpolationForInteger(member) : member,
21
+ ];
19
22
  })
20
23
  .map(([key, member]) => {
21
24
  if (isBuiltin(member)) {
@@ -57,15 +60,38 @@ export function separateAllAsPositional(schema) {
57
60
  const positionalArgs = Object.entries(withLocs).map(([key, type]) => ({ schemaKey: key, type }));
58
61
  return { dataSchema: undefined, positionalArgs };
59
62
  }
60
- export function createIoSchema(layout, locations = {}) {
61
- return (isData(layout)
62
- ? isVoid(layout)
63
- ? layout
64
- : isBuiltin(layout)
65
- ? layout
66
- : getCustomLocation(layout) !== undefined
67
- ? layout
68
- : location(0, layout)
69
- : INTERNAL_createStruct(withLocations(layout, locations),
70
- /* isAbstruct */ false));
63
+ export function createIoSchema(layout, locations = {}, autoInterpolateIntegers = false) {
64
+ if (isData(layout)) {
65
+ if (isVoid(layout) || isBuiltin(layout)) {
66
+ return layout;
67
+ }
68
+ return (getCustomLocation(layout) !== undefined ? layout : location(0, layout));
69
+ }
70
+ return INTERNAL_createStruct(withLocations(layout, locations, autoInterpolateIntegers),
71
+ /* isAbstruct */ false);
72
+ }
73
+ function needsFlatInterpolation(data) {
74
+ if (isBuiltin(data) || (isDecorated(data) && data.attribs.some(isInterpolateAttrib))) {
75
+ return false;
76
+ }
77
+ const inner = isDecorated(data) ? data.inner : data;
78
+ return isInteger(inner) || isIntegerVec(inner);
79
+ }
80
+ function withFlatInterpolationForInteger(data) {
81
+ return needsFlatInterpolation(data) ? interpolate('flat', data) : data;
82
+ }
83
+ /**
84
+ * WGSL requires integer inter-stage values to be flat interpolated. Interpolation is only
85
+ * inferred for shellless entry functions, so shells have to specify it explicitly.
86
+ */
87
+ export function assertIntegerVaryingsInterpolated(layout, location) {
88
+ if (layout === undefined || isData(layout)) {
89
+ // A single value is either a builtin or a fragment output, neither are varyings
90
+ return;
91
+ }
92
+ for (const [key, member] of Object.entries(layout)) {
93
+ if (needsFlatInterpolation(member)) {
94
+ throw new Error(`Integer value "${key}" in ${location} requires flat interpolation. Wrap its schema in d.interpolate('flat', ...) or d.interpolate('flat, either', ...).`);
95
+ }
96
+ }
71
97
  }
@@ -13,7 +13,7 @@ export type VertexOutToVarying<T> = OmitBuiltins<{
13
13
  [K in keyof T]: InstanceToSchema<T[K]>;
14
14
  }>;
15
15
  type FragmentColorValue = Vec4f | Vec4i | Vec4u;
16
- export type FragmentOutConstrained = IOLayout<FragmentColorValue | Decorated<FragmentColorValue, (Location | Interpolate)[]> | AnyFragmentOutputBuiltin>;
16
+ export type FragmentOutConstrained = IOLayout<FragmentColorValue | Decorated<FragmentColorValue, Location[]> | AnyFragmentOutputBuiltin>;
17
17
  /**
18
18
  * Describes a fragment entry function signature (its arguments, return type and targets)
19
19
  */
@@ -3,7 +3,7 @@ import { $getNameForward, $internal, $resolve } from "../../shared/symbols.js";
3
3
  import { addReturnTypeToExternals } from "../resolve/externals.js";
4
4
  import { shaderStageSlot } from "../slot/internalSlots.js";
5
5
  import { createFnCore } from "./fnCore.js";
6
- import { createIoSchema, separateBuiltins } from "./ioSchema.js";
6
+ import { assertIntegerVaryingsInterpolated, createIoSchema, separateBuiltins, } from "./ioSchema.js";
7
7
  import { stripTemplate } from "./templateUtils.js";
8
8
  /**
9
9
  * Creates a shell of a typed entry function for the fragment shader stage. Any function
@@ -55,6 +55,7 @@ function createFragmentFn(shell, implementation) {
55
55
  return this;
56
56
  },
57
57
  [$resolve](ctx) {
58
+ assertIntegerVaryingsInterpolated(shell.in, `fragmentFn (${getName(this) ?? '<unnamed>'}) input`);
58
59
  const entryInput = separateBuiltins(shell.in ?? {}, ctx.varyingLocations ?? {});
59
60
  if (entryInput.dataSchema && isNamable(entryInput.dataSchema)) {
60
61
  entryInput.dataSchema.$name(`${getName(this) ?? ''}_Input`);
@@ -2,7 +2,7 @@ import { getName, setName } from "../../shared/meta.js";
2
2
  import { $getNameForward, $internal, $resolve } from "../../shared/symbols.js";
3
3
  import { shaderStageSlot } from "../slot/internalSlots.js";
4
4
  import { createFnCore } from "./fnCore.js";
5
- import { createIoSchema, separateAllAsPositional } from "./ioSchema.js";
5
+ import { assertIntegerVaryingsInterpolated, createIoSchema, separateAllAsPositional, } from "./ioSchema.js";
6
6
  import { stripTemplate } from "./templateUtils.js";
7
7
  /**
8
8
  * Creates a shell of a typed entry function for the vertex shader stage. Any function
@@ -50,6 +50,7 @@ function createVertexFn(shell, implementation) {
50
50
  return this;
51
51
  },
52
52
  [$resolve](ctx) {
53
+ assertIntegerVaryingsInterpolated(shell.out, `vertexFn (${getName(this) ?? '<unnamed>'}) output`);
53
54
  const outputWithLocation = createIoSchema(shell.out, ctx.varyingLocations).$name(`${getName(this) ?? ''}_Output`);
54
55
  if (typeof implementation === 'string') {
55
56
  core.setExternals('out', { Out: outputWithLocation });
@@ -1,4 +1,4 @@
1
- import { type BaseData } from '../../data/wgslTypes.ts';
2
1
  import type { ColorAttachment } from '../commandEncoder/attachments.ts';
2
+ import type { ConnectedTarget } from './connectTargetsToShader.ts';
3
3
  import type { AnyFragmentColorAttachment } from './renderPipeline.ts';
4
- export declare function connectAttachmentToShader(fragmentOut: BaseData, attachment: AnyFragmentColorAttachment): ColorAttachment[];
4
+ export declare function connectAttachmentToShader(connectedTargets: (ConnectedTarget | null)[] | undefined, attachment: AnyFragmentColorAttachment): (ColorAttachment | null)[];
@@ -1,29 +1,24 @@
1
- import { isBuiltin } from "../../data/attributes.js";
2
- import { isVoid, isWgslStruct } from "../../data/wgslTypes.js";
3
1
  function isColorAttachment(value) {
4
2
  return !!value?.view;
5
3
  }
6
- export function connectAttachmentToShader(fragmentOut, attachment) {
7
- if (isVoid(fragmentOut) || isBuiltin(fragmentOut)) {
4
+ export function connectAttachmentToShader(connectedTargets, attachment) {
5
+ if (!connectedTargets) {
8
6
  return [];
9
7
  }
10
- if (isWgslStruct(fragmentOut)) {
11
- const result = [];
12
- for (const key of Object.keys(fragmentOut.propTypes)) {
13
- const outputValue = fragmentOut.propTypes[key];
14
- if (isBuiltin(outputValue)) {
15
- continue;
16
- }
17
- const matching = attachment[key];
18
- if (!matching) {
19
- throw new Error(`A color attachment by the name of '${key}' was not provided to the shader.`);
8
+ return connectedTargets.map((target) => {
9
+ if (!target) {
10
+ return null;
11
+ }
12
+ if (target.key === undefined) {
13
+ if (!isColorAttachment(attachment)) {
14
+ throw new Error('Expected a single color attachment, not a record.');
20
15
  }
21
- result.push(matching);
16
+ return attachment;
22
17
  }
23
- return result;
24
- }
25
- if (!isColorAttachment(attachment)) {
26
- throw new Error('Expected a single color attachment, not a record.');
27
- }
28
- return [attachment];
18
+ const matching = attachment[target.key];
19
+ if (!matching) {
20
+ throw new Error(`A color attachment by the name of '${target.key}' was not provided to the shader.`);
21
+ }
22
+ return matching;
23
+ });
29
24
  }
@@ -1,3 +1,7 @@
1
1
  import { type BaseData } from '../../data/wgslTypes.ts';
2
2
  import type { AnyFragmentTargets } from './renderPipeline.ts';
3
- export declare function connectTargetsToShader(fragmentOut: BaseData, targets: AnyFragmentTargets): (GPUColorTargetState | null)[];
3
+ export interface ConnectedTarget {
4
+ key: string | undefined;
5
+ target: GPUColorTargetState;
6
+ }
7
+ export declare function connectTargetsToShader(fragmentOut: BaseData, targets: AnyFragmentTargets): (ConnectedTarget | null)[];
@@ -1,31 +1,36 @@
1
1
  import { isBuiltin } from "../../data/attributes.js";
2
2
  import { isVoid, isWgslStruct } from "../../data/wgslTypes.js";
3
+ import { invariant } from "../../errors.js";
4
+ import { getCustomLocation } from "../../data/dataTypes.js";
3
5
  export function connectTargetsToShader(fragmentOut, targets) {
4
- let presentationFormat;
5
6
  if (isVoid(fragmentOut) || isBuiltin(fragmentOut)) {
6
7
  return [];
7
8
  }
9
+ const result = [];
10
+ function putInResult(index, key, target) {
11
+ while (index > result.length) {
12
+ result.push(null);
13
+ }
14
+ result[index] = { key, target };
15
+ }
8
16
  if (isWgslStruct(fragmentOut)) {
9
- const result = [];
10
- for (const key of Object.keys(fragmentOut.propTypes)) {
11
- const outputValue = fragmentOut.propTypes[key];
12
- if (isBuiltin(outputValue)) {
13
- continue;
14
- }
15
- const matchingTarget = targets[key];
16
- result.push({
17
+ const varyings = Object.entries(fragmentOut.propTypes).filter(([, value]) => !isBuiltin(value));
18
+ for (const [key, outputValue] of varyings) {
19
+ const matchingTarget = targets?.[key];
20
+ const location = getCustomLocation(outputValue);
21
+ invariant(location !== undefined, `'withLocations' failed or was not called.`);
22
+ putInResult(location, key, {
17
23
  ...matchingTarget,
18
- format: matchingTarget?.format ??
19
- (presentationFormat ??= navigator.gpu.getPreferredCanvasFormat()),
24
+ format: matchingTarget?.format ?? navigator.gpu.getPreferredCanvasFormat(),
20
25
  });
21
26
  }
22
- return result;
23
27
  }
24
- const singleTarget = targets;
25
- return [
26
- {
28
+ else {
29
+ const singleTarget = targets;
30
+ putInResult(getCustomLocation(fragmentOut) ?? 0, undefined, {
27
31
  ...singleTarget,
28
- format: singleTarget?.format ?? (presentationFormat ??= navigator.gpu.getPreferredCanvasFormat()),
29
- },
30
- ];
32
+ format: singleTarget?.format ?? navigator.gpu.getPreferredCanvasFormat(),
33
+ });
34
+ }
35
+ return result;
31
36
  }
@@ -3,7 +3,7 @@ import type { IndexFlag, IndirectFlag, TgpuBuffer, VertexFlag } from '../buffer/
3
3
  import type { TgpuQuerySet } from '../querySet/querySet.ts';
4
4
  import { type Disarray, type UndecorateRecord } from '../../data/dataTypes.ts';
5
5
  import { type ResolvedSnippet } from '../../data/snippet.ts';
6
- import { type AnyVecInstance, type AnyWgslData, type BaseData, type U16, type U32, type v4f, Void, type WgslArray, type WgslStruct } from '../../data/wgslTypes.ts';
6
+ import { type AnyVecInstance, type AnyWgslData, type BaseData, type U16, type U32, type v4f, Void, type WgslArray } from '../../data/wgslTypes.ts';
7
7
  import type { TgpuNamable } from '../../shared/meta.ts';
8
8
  import type { InferInput } from '../../shared/repr.ts';
9
9
  import type { TgpuDeviceOwningSoul } from '../../shared/soul.ts';
@@ -18,6 +18,7 @@ import type { TgpuVertexFn } from '../function/tgpuVertexFn.ts';
18
18
  import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts';
19
19
  import type { TgpuSlot } from '../slot/slotTypes.ts';
20
20
  import { type TgpuVertexLayout } from '../vertexLayout/vertexLayout.ts';
21
+ import { type ConnectedTarget } from './connectTargetsToShader.ts';
21
22
  import { type TgpuCommandEncoder } from '../commandEncoder/commandEncoder.ts';
22
23
  import type { ColorAttachment, DepthStencilAttachment } from '../commandEncoder/attachments.ts';
23
24
  import { type ImmediateSnapshot, type TgpuImmediateVar } from '../immediate/immediateVar.ts';
@@ -34,7 +35,7 @@ export interface RenderPipelineInternals {
34
35
  export interface TgpuRenderPipelineSoul extends TgpuDeviceOwningSoul<'render-pipeline', GPURenderPipeline> {
35
36
  usedBindGroupLayouts?: TgpuBindGroupLayout[] | undefined;
36
37
  usedVertexLayouts?: TgpuVertexLayout[] | undefined;
37
- fragmentOut?: BaseData | undefined;
38
+ connectedTargets?: (ConnectedTarget | null)[] | undefined;
38
39
  bindGroups?: [TgpuBindGroupLayout, TgpuBindGroup | GPUBindGroup][] | undefined;
39
40
  usedImmediate?: TgpuImmediateVar | undefined;
40
41
  immediates?: [TgpuImmediateVar, ImmediateSnapshot][] | undefined;
@@ -221,7 +222,7 @@ type Memo = {
221
222
  catchall: [number, TgpuBindGroup] | undefined;
222
223
  logResources: LogResources | undefined;
223
224
  usedVertexLayouts: TgpuVertexLayout[];
224
- fragmentOut: BaseData | undefined;
225
+ connectedTargets: (ConnectedTarget | null)[] | undefined;
225
226
  usedImmediate: TgpuImmediateVar | undefined;
226
227
  };
227
228
  declare class TgpuRenderPipelineImpl implements TgpuRenderPipeline {
@@ -280,8 +281,8 @@ declare class RenderPipelineCore implements SelfResolvable {
280
281
  resolveAndCreateShaderModule(): {
281
282
  resolutionResult: import("../../resolutionCtx.ts").ResolutionResult;
282
283
  descriptor: GPURenderPipelineDescriptor;
284
+ connectedTargets: (ConnectedTarget | null)[];
283
285
  connectedAttribs: import("../vertexLayout/connectAttributesToShader.ts").ConnectAttributesToShaderResult;
284
- fragmentOut: WgslStruct<import("../function/ioSchema.ts").WithLocations<Record<string, BaseData>>>;
285
286
  };
286
287
  }
287
288
  /**
@@ -42,7 +42,7 @@ export function INTERNAL_restoreRenderPipeline(soul, ctx) {
42
42
  catchall: undefined,
43
43
  logResources: undefined,
44
44
  usedVertexLayouts: soul.usedVertexLayouts ?? [],
45
- fragmentOut: soul.fragmentOut,
45
+ connectedTargets: soul.connectedTargets,
46
46
  usedImmediate: soul.usedImmediate,
47
47
  });
48
48
  const pipeline = new TgpuRenderPipelineImpl(core, {
@@ -77,7 +77,7 @@ class TgpuRenderPipelineImpl {
77
77
  soul.raw = memo.pipeline;
78
78
  soul.usedBindGroupLayouts = memo.usedBindGroupLayouts;
79
79
  soul.usedVertexLayouts = memo.usedVertexLayouts;
80
- soul.fragmentOut = memo.fragmentOut;
80
+ soul.connectedTargets = memo.connectedTargets;
81
81
  soul.bindGroups = collectBindGroupPairs(memo.usedBindGroupLayouts, memo.catchall, priors.bindGroupLayoutMap);
82
82
  soul.usedImmediate = memo.usedImmediate;
83
83
  soul.immediates = [...(priors.immediatesMap ?? [])];
@@ -224,12 +224,10 @@ class TgpuRenderPipelineImpl {
224
224
  }
225
225
  #ownPassDescriptor() {
226
226
  const { core, priors } = this[$internal];
227
- const { fragmentOut } = core.unwrap();
227
+ const { connectedTargets } = core.unwrap();
228
228
  return {
229
229
  label: getName(core) ?? '<unnamed>',
230
- colorAttachments: fragmentOut
231
- ? connectAttachmentToShader(fragmentOut, priors.colorAttachment ?? {})
232
- : [],
230
+ colorAttachments: connectAttachmentToShader(connectedTargets, priors.colorAttachment ?? {}),
233
231
  depthStencilAttachment: priors.depthStencilAttachment,
234
232
  timestampWrites: priors.timestampWrites,
235
233
  };
@@ -345,7 +343,7 @@ class RenderPipelineCore {
345
343
  if (this.#initAsyncPromise === undefined) {
346
344
  // the pipeline did not start resolution & compilation
347
345
  const device = this.options.root.device;
348
- const { resolutionResult, descriptor, connectedAttribs, fragmentOut } = this.resolveAndCreateShaderModule();
346
+ const { resolutionResult, descriptor, connectedTargets, connectedAttribs } = this.resolveAndCreateShaderModule();
349
347
  const { usedBindGroupLayouts, catchall, logResources } = resolutionResult;
350
348
  this.#initAsyncPromise = device
351
349
  .createRenderPipelineAsync(descriptor)
@@ -356,7 +354,7 @@ class RenderPipelineCore {
356
354
  catchall,
357
355
  logResources,
358
356
  usedVertexLayouts: connectedAttribs.usedVertexLayouts,
359
- fragmentOut,
357
+ connectedTargets,
360
358
  usedImmediate: resolutionResult.usedImmediate,
361
359
  };
362
360
  this.#performanceTracker.measureCompile(device);
@@ -375,7 +373,7 @@ class RenderPipelineCore {
375
373
  throw new Error("'pipeline.initAsync()' was called and is not yet resolved.");
376
374
  }
377
375
  const device = this.options.root.device;
378
- const { resolutionResult, descriptor, connectedAttribs, fragmentOut } = this.resolveAndCreateShaderModule();
376
+ const { resolutionResult, descriptor, connectedTargets, connectedAttribs } = this.resolveAndCreateShaderModule();
379
377
  const { usedBindGroupLayouts, catchall, logResources } = resolutionResult;
380
378
  this.#memo = {
381
379
  pipeline: device.createRenderPipeline(descriptor),
@@ -383,7 +381,7 @@ class RenderPipelineCore {
383
381
  catchall,
384
382
  logResources,
385
383
  usedVertexLayouts: connectedAttribs.usedVertexLayouts,
386
- fragmentOut,
384
+ connectedTargets,
387
385
  usedImmediate: resolutionResult.usedImmediate,
388
386
  };
389
387
  this.#performanceTracker.measureCompile(device);
@@ -444,7 +442,7 @@ class RenderPipelineCore {
444
442
  if (tgpuDescriptor.fragment) {
445
443
  descriptor.fragment = {
446
444
  module,
447
- targets: connectedTargets,
445
+ targets: connectedTargets.map((target) => (target ? target.target : null)),
448
446
  };
449
447
  }
450
448
  if (tgpuDescriptor.primitive) {
@@ -467,7 +465,7 @@ class RenderPipelineCore {
467
465
  if (tgpuDescriptor.multisample) {
468
466
  descriptor.multisample = tgpuDescriptor.multisample;
469
467
  }
470
- return { resolutionResult, descriptor, connectedAttribs, fragmentOut };
468
+ return { resolutionResult, descriptor, connectedTargets, connectedAttribs };
471
469
  }
472
470
  }
473
471
  /**
@@ -1,4 +1,4 @@
1
- import { bannedTokens, builtins } from "../../nameUtils.js";
1
+ import { bannedTokens, builtins, templateEnumerants } from "../../nameUtils.js";
2
2
  import { $internal } from "../../shared/symbols.js";
3
3
  import { ShelllessRepository } from "../../tgsl/shellless.js";
4
4
  class NamespaceImpl {
@@ -6,7 +6,7 @@ class NamespaceImpl {
6
6
  constructor(strategy) {
7
7
  this[$internal] = {
8
8
  strategy,
9
- takenGlobalIdentifiers: new Set([...bannedTokens, ...builtins]),
9
+ takenGlobalIdentifiers: new Set([...bannedTokens, ...builtins, ...templateEnumerants]),
10
10
  shelllessRepo: new ShelllessRepository(),
11
11
  memoizedResolves: new WeakMap(),
12
12
  memoizedLazy: new WeakMap(),
@@ -430,9 +430,13 @@ export interface TgpuRoot extends Unwrapper, WithBinding {
430
430
  */
431
431
  get enabledWgslLanguageFeatures(): ReadonlySet<string>;
432
432
  /**
433
- * Destroys all underlying resources (i.e. buffers...) created through this root object.
433
+ * Releases internal resource caches (e.g. utility texture pipelines),
434
+ * letting the GC reclaim them.
435
+ *
436
+ * Buffers, textures and other resources created through the root are NOT destroyed.
437
+ *
434
438
  * If the object is created via `tgpu.init` instead of `tgpu.initFromDevice`,
435
- * then the inner GPU device is destroyed as well.
439
+ * destroys the underlying WebGPU device as well.
436
440
  */
437
441
  destroy(): void;
438
442
  '~unstable': Pick<ExperimentalTgpuRoot, 'createCommandEncoder' | 'createComparisonSampler' | 'createGuardedComputePipeline' | 'createRenderBundleEncoder' | 'createSampler' | 'createTexture' | 'nameRegistrySetting' | 'shaderGeneratorClass' | 'pipe' | 'with'>;
@@ -1,5 +1,5 @@
1
1
  import { type AnyData } from '../../data/dataTypes.ts';
2
- import type { BaseData } from '../../data/wgslTypes.ts';
2
+ import { type BaseData } from '../../data/wgslTypes.ts';
3
3
  import type { InferGPU } from '../../shared/repr.ts';
4
4
  import { $getNameForward, $gpuValueOf, $internal, $resolve, $soul } from '../../shared/symbols.ts';
5
5
  import type { UnwrapRuntimeConstructor } from '../../tgpuBindGroupLayout.ts';