typegpu 0.12.6 → 0.12.8

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 (87) 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 +5 -3
  5. package/core/function/comptime.js +7 -5
  6. package/core/function/createCallableSchema.js +4 -3
  7. package/core/function/dualImpl.js +20 -19
  8. package/core/function/ioSchema.d.ts +17 -2
  9. package/core/function/ioSchema.js +74 -27
  10. package/core/function/tgpuFn.d.ts +11 -1
  11. package/core/function/tgpuFn.js +9 -2
  12. package/core/function/tgpuFragmentFn.d.ts +1 -1
  13. package/core/function/tgpuFragmentFn.js +2 -1
  14. package/core/function/tgpuVertexFn.js +2 -1
  15. package/core/pipeline/connectAttachmentToShader.d.ts +2 -2
  16. package/core/pipeline/connectAttachmentToShader.js +16 -21
  17. package/core/pipeline/connectTargetsToShader.d.ts +5 -1
  18. package/core/pipeline/connectTargetsToShader.js +23 -18
  19. package/core/pipeline/renderPipeline.d.ts +9 -6
  20. package/core/pipeline/renderPipeline.js +22 -22
  21. package/core/pipeline/timeable.js +15 -2
  22. package/core/resolve/namespace.js +2 -2
  23. package/core/root/configurableImpl.js +1 -2
  24. package/core/root/rootTypes.d.ts +6 -2
  25. package/core/slot/accessor.d.ts +1 -1
  26. package/core/slot/accessor.js +72 -5
  27. package/core/texture/externalTexture.d.ts +1 -1
  28. package/core/texture/externalTexture.js +3 -1
  29. package/core/texture/texture.d.ts +1 -1
  30. package/core/texture/texture.js +12 -5
  31. package/core/unroll/tgpuUnroll.d.ts +4 -3
  32. package/core/unroll/tgpuUnroll.js +7 -1
  33. package/core/valueProxyUtils.d.ts +4 -2
  34. package/core/valueProxyUtils.js +4 -2
  35. package/data/autoStruct.d.ts +1 -1
  36. package/data/autoStruct.js +8 -4
  37. package/data/compiledIO.js +12 -9
  38. package/data/dataIO.d.ts +10 -6
  39. package/data/dataIO.js +19 -4
  40. package/data/dataTypes.d.ts +0 -5
  41. package/data/dataTypes.js +0 -6
  42. package/data/deepEqual.js +19 -9
  43. package/data/matrix.js +10 -3
  44. package/data/matrixColumnsAccess.d.ts +8 -0
  45. package/data/matrixColumnsAccess.js +15 -0
  46. package/data/offsetUtils.js +181 -102
  47. package/data/partialIO.d.ts +8 -1
  48. package/data/partialIO.js +36 -19
  49. package/data/ref.d.ts +12 -5
  50. package/data/ref.js +51 -24
  51. package/data/snippet.d.ts +13 -4
  52. package/data/snippet.js +17 -4
  53. package/data/struct.js +3 -0
  54. package/data/wgslTypes.d.ts +6 -3
  55. package/internal.d.ts +1 -0
  56. package/nameUtils.d.ts +7 -0
  57. package/nameUtils.js +60 -0
  58. package/package.json +1 -1
  59. package/resolutionCtx.js +1 -1
  60. package/shared/meta.js +1 -1
  61. package/shared/symbols.d.ts +5 -0
  62. package/shared/symbols.js +6 -1
  63. package/shared/tseynit.js +54 -50
  64. package/std/boolean.js +9 -10
  65. package/std/numeric.js +1 -4
  66. package/std/operators.js +9 -4
  67. package/std/packing.js +10 -10
  68. package/std/texture.d.ts +2 -2
  69. package/tgpuLogger.d.ts +1 -1
  70. package/tgpuLogger.js +0 -2
  71. package/tgsl/accessIndex.js +37 -38
  72. package/tgsl/accessProp.js +10 -7
  73. package/tgsl/conversion.js +3 -2
  74. package/tgsl/forOfUtils.js +8 -8
  75. package/tgsl/generationHelpers.d.ts +4 -3
  76. package/tgsl/generationHelpers.js +18 -5
  77. package/tgsl/infixDispatch.d.ts +4 -1
  78. package/tgsl/infixDispatch.js +13 -1
  79. package/tgsl/makeDereferenceable.d.ts +1 -1
  80. package/tgsl/makeDereferenceable.js +3 -1
  81. package/tgsl/shellless.js +5 -9
  82. package/tgsl/stringifySnippet.d.ts +9 -0
  83. package/tgsl/stringifySnippet.js +24 -0
  84. package/tgsl/wgslGenerator.d.ts +1 -0
  85. package/tgsl/wgslGenerator.js +180 -86
  86. package/types.d.ts +23 -2
  87. package/types.js +30 -5
@@ -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');
@@ -66,9 +67,10 @@ export class AutoVertexFn {
66
67
  setName(impl, 'vertexFn');
67
68
  }
68
69
  this.#core = createFnCore(impl, 'vertex');
69
- this.autoIn = new AutoStruct({ ...builtinVertexIn, ...attribs }, undefined, locations);
70
+ this.autoIn = new AutoStruct({ ...builtinVertexIn, ...attribs }, undefined);
70
71
  setName(this.autoIn, 'VertexIn');
71
- this.autoOut = new AutoStruct(builtinVertexOut, undefined);
72
+ this.autoOut = new AutoStruct(builtinVertexOut, undefined, locations,
73
+ /* autoInterpolateIntegers */ true);
72
74
  setName(this.autoOut, 'VertexOut');
73
75
  }
74
76
  toString() {
@@ -2,7 +2,8 @@ import { WgslTypeError } from "../../errors.js";
2
2
  import { setName } from "../../shared/meta.js";
3
3
  import { $getNameForward, $gpuCallable, $internal } from "../../shared/symbols.js";
4
4
  import { coerceToSnippet } from "../../tgsl/generationHelpers.js";
5
- import { isKnownAtComptime, NormalState } from "../../types.js";
5
+ import { stringifySnippet } from "../../tgsl/stringifySnippet.js";
6
+ import { asComptime, NormalState } from "../../types.js";
6
7
  export function isComptimeFn(value) {
7
8
  return !!value?.[$internal]?.isComptime;
8
9
  }
@@ -38,15 +39,16 @@ export function comptime(func) {
38
39
  impl[$getNameForward] = func;
39
40
  impl[$gpuCallable] = {
40
41
  call(ctx, args) {
41
- if (!args.every((s) => isKnownAtComptime(s))) {
42
+ const comptimeArgs = args.map((s) => asComptime(s));
43
+ if (!comptimeArgs.every((s) => s !== undefined)) {
42
44
  throw new WgslTypeError(`Called comptime function with runtime-known values: ${args
43
- .filter((s) => !isKnownAtComptime(s))
44
- .map((s) => `'${s.value}'`)
45
+ .filter((_, idx) => comptimeArgs[idx] === undefined)
46
+ .map((s) => `'${stringifySnippet(s)}'`)
45
47
  .join(', ')}`);
46
48
  }
47
49
  ctx.pushMode(new NormalState());
48
50
  try {
49
- return coerceToSnippet(func(...args.map((s) => s.value)));
51
+ return coerceToSnippet(func(...comptimeArgs.map((s) => s.value)));
50
52
  }
51
53
  finally {
52
54
  ctx.popMode();
@@ -3,7 +3,7 @@ import { isPtr } from "../../data/wgslTypes.js";
3
3
  import { setName } from "../../shared/meta.js";
4
4
  import { $gpuCallable } from "../../shared/symbols.js";
5
5
  import { tryConvertSnippet } from "../../tgsl/conversion.js";
6
- import { isKnownAtComptime, NormalState } from "../../types.js";
6
+ import { asComptime, NormalState } from "../../types.js";
7
7
  export function callableSchema(options) {
8
8
  const impl = ((...args) => {
9
9
  return options.normalImpl(...args);
@@ -30,10 +30,11 @@ export function callableSchema(options) {
30
30
  return tryConvertSnippet(ctx, s, argType, false);
31
31
  });
32
32
  let result;
33
- if (converted.every((s) => isKnownAtComptime(s))) {
33
+ const comptimeArgs = converted.map((s) => asComptime(s));
34
+ if (comptimeArgs.every((s) => s !== undefined)) {
34
35
  ctx.pushMode(new NormalState());
35
36
  try {
36
- result = snip(options.normalImpl(...converted.map((s) => s.value)), options.schema(),
37
+ result = snip(options.normalImpl(...comptimeArgs.map((s) => s.value)), options.schema(),
37
38
  // Functions give up ownership of their return value
38
39
  /* origin */ 'constant');
39
40
  }
@@ -3,7 +3,7 @@ import { setName } from "../../shared/meta.js";
3
3
  import { $gpuCallable } from "../../shared/symbols.js";
4
4
  import { tryConvertSnippet } from "../../tgsl/conversion.js";
5
5
  import { concretize } from "../../tgsl/generationHelpers.js";
6
- import { isKnownAtComptime, NormalState } from "../../types.js";
6
+ import { asComptime, NormalState } from "../../types.js";
7
7
  import { isPtr } from "../../data/wgslTypes.js";
8
8
  export class MissingCpuImplError extends Error {
9
9
  constructor(message) {
@@ -43,25 +43,26 @@ export function dualImpl(options) {
43
43
  }
44
44
  return tryConvertSnippet(ctx, s, argType, !options.ignoreImplicitCastWarning);
45
45
  });
46
- if (!options.noComptime &&
47
- converted.every((s) => isKnownAtComptime(s)) &&
48
- typeof options.normalImpl === 'function') {
49
- ctx.pushMode(new NormalState());
50
- try {
51
- return snip(options.normalImpl(...converted.map((s) => s.value)), returnType,
52
- // Functions give up ownership of their return value
53
- /* origin */ 'constant', options.sideEffects);
54
- }
55
- catch (e) {
56
- // cpuImpl may in some cases be present but implemented only partially.
57
- // In that case, if the MissingCpuImplError is thrown, we fallback to codegenImpl.
58
- // If it is any other error, we just rethrow.
59
- if (!(e instanceof MissingCpuImplError)) {
60
- throw e;
46
+ if (!options.noComptime && typeof options.normalImpl === 'function') {
47
+ const comptimeArgs = converted.map((s) => asComptime(s));
48
+ if (comptimeArgs.every((s) => s !== undefined)) {
49
+ ctx.pushMode(new NormalState());
50
+ try {
51
+ return snip(options.normalImpl(...comptimeArgs.map((s) => s.value)), returnType,
52
+ // Functions give up ownership of their return value
53
+ /* origin */ 'constant', options.sideEffects);
54
+ }
55
+ catch (e) {
56
+ // cpuImpl may in some cases be present but implemented only partially.
57
+ // In that case, if the MissingCpuImplError is thrown, we fallback to codegenImpl.
58
+ // If it is any other error, we just rethrow.
59
+ if (!(e instanceof MissingCpuImplError)) {
60
+ throw e;
61
+ }
62
+ }
63
+ finally {
64
+ ctx.popMode('normal');
61
65
  }
62
- }
63
- finally {
64
- ctx.popMode('normal');
65
66
  }
66
67
  }
67
68
  const possibleSideEffects = options.sideEffects || args.some((a) => a.possibleSideEffects);
@@ -7,7 +7,22 @@ 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
+ /**
11
+ * Assigns locations to members.
12
+ * The priority of assigned location is as follows:
13
+ * - location already present in member,
14
+ * - location provided in `locations`,
15
+ * - a free number.
16
+ *
17
+ * Assumes `locations` are consistent with already decorated `members`,
18
+ * throws if a location is repeated.
19
+ */
20
+ export declare function withLocations<T extends BaseData>(members: Record<string, T> | undefined, locations?: Record<string, number>, autoInterpolateIntegers?: boolean): Record<string, BaseData>;
11
21
  export declare function separateBuiltins(schema: Record<string, BaseData>, locations?: Record<string, number>): SeparatedEntryArgs;
12
22
  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>;
23
+ export declare function createIoSchema<T extends BaseData | Record<string, BaseData>>(layout: T, locations?: Record<string, number>, autoInterpolateIntegers?: boolean): IOLayoutToSchema<T>;
24
+ /**
25
+ * WGSL requires integer inter-stage values to be flat interpolated. Interpolation is only
26
+ * inferred for shellless entry functions, so shells have to specify it explicitly.
27
+ */
28
+ export declare function assertIntegerVaryingsInterpolated(layout: BaseData | Record<string, BaseData> | undefined, location: string): void;
@@ -1,32 +1,44 @@
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
+ /**
7
+ * Assigns locations to members.
8
+ * The priority of assigned location is as follows:
9
+ * - location already present in member,
10
+ * - location provided in `locations`,
11
+ * - a free number.
12
+ *
13
+ * Assumes `locations` are consistent with already decorated `members`,
14
+ * throws if a location is repeated.
15
+ */
16
+ export function withLocations(members, locations = {}, autoInterpolateIntegers = false) {
7
17
  let nextLocation = 0;
8
- const usedCustomLocations = new Set();
9
- return Object.fromEntries(Object.entries(members ?? {})
18
+ const usedCustomLocations = new Set([
19
+ ...Object.values(locations),
20
+ ...Object.values(members ?? {})
21
+ .map(getCustomLocation)
22
+ .filter((v) => v !== undefined),
23
+ ]);
24
+ const entries = Object.entries(members ?? {})
10
25
  .map(([key, member]) => {
11
- const customLocation = getCustomLocation(member);
12
- if (customLocation !== undefined) {
13
- if (usedCustomLocations.has(customLocation)) {
14
- throw new Error('Duplicate custom location attributes found');
15
- }
16
- usedCustomLocations.add(customLocation);
17
- }
18
- return [key, member];
26
+ // Interpolate
27
+ const interpolated = autoInterpolateIntegers
28
+ ? withFlatInterpolationForInteger(member)
29
+ : member;
30
+ return [key, interpolated];
19
31
  })
20
32
  .map(([key, member]) => {
33
+ // Assign locations
21
34
  if (isBuiltin(member)) {
22
- // skipping builtins
23
35
  return [key, member];
24
36
  }
25
37
  if (getCustomLocation(member) !== undefined) {
26
38
  // this member is already marked
27
39
  return [key, member];
28
40
  }
29
- if (locations[key]) {
41
+ if (locations[key] !== undefined) {
30
42
  // location has been determined by a previous procedure
31
43
  return [key, location(locations[key], member)];
32
44
  }
@@ -34,7 +46,19 @@ export function withLocations(members, locations = {}) {
34
46
  nextLocation++;
35
47
  }
36
48
  return [key, location(nextLocation++, member)];
37
- }));
49
+ });
50
+ // Check for duplicate locations
51
+ const used = new Map();
52
+ entries.forEach(([key, member]) => {
53
+ const location = getCustomLocation(member);
54
+ if (location !== undefined) {
55
+ if (used.has(location)) {
56
+ throw new Error(`Location '${location}' is used by multiple keys: '${used.get(location)}', '${key}'.`);
57
+ }
58
+ used.set(location, key);
59
+ }
60
+ });
61
+ return Object.fromEntries(entries);
38
62
  }
39
63
  export function separateBuiltins(schema, locations = {}) {
40
64
  const positionalArgs = [];
@@ -57,15 +81,38 @@ export function separateAllAsPositional(schema) {
57
81
  const positionalArgs = Object.entries(withLocs).map(([key, type]) => ({ schemaKey: key, type }));
58
82
  return { dataSchema: undefined, positionalArgs };
59
83
  }
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));
84
+ export function createIoSchema(layout, locations = {}, autoInterpolateIntegers = false) {
85
+ if (isData(layout)) {
86
+ if (isVoid(layout) || isBuiltin(layout)) {
87
+ return layout;
88
+ }
89
+ return (getCustomLocation(layout) !== undefined ? layout : location(0, layout));
90
+ }
91
+ return INTERNAL_createStruct(withLocations(layout, locations, autoInterpolateIntegers),
92
+ /* isAbstruct */ false);
93
+ }
94
+ function needsFlatInterpolation(data) {
95
+ if (isBuiltin(data) || (isDecorated(data) && data.attribs.some(isInterpolateAttrib))) {
96
+ return false;
97
+ }
98
+ const inner = isDecorated(data) ? data.inner : data;
99
+ return isInteger(inner) || isIntegerVec(inner);
100
+ }
101
+ function withFlatInterpolationForInteger(data) {
102
+ return needsFlatInterpolation(data) ? interpolate('flat', data) : data;
103
+ }
104
+ /**
105
+ * WGSL requires integer inter-stage values to be flat interpolated. Interpolation is only
106
+ * inferred for shellless entry functions, so shells have to specify it explicitly.
107
+ */
108
+ export function assertIntegerVaryingsInterpolated(layout, location) {
109
+ if (layout === undefined || isData(layout)) {
110
+ // A single value is either a builtin or a fragment output, neither are varyings
111
+ return;
112
+ }
113
+ for (const [key, member] of Object.entries(layout)) {
114
+ if (needsFlatInterpolation(member)) {
115
+ throw new Error(`Integer value "${key}" in ${location} requires flat interpolation. Wrap its schema in d.interpolate('flat', ...) or d.interpolate('flat, either', ...).`);
116
+ }
117
+ }
71
118
  }
@@ -6,7 +6,7 @@ import type { Prettify } from '../../shared/utilityTypes.ts';
6
6
  import type { DualFn } from '../../types.ts';
7
7
  import { type Providing } from '../slot/slotTypes.ts';
8
8
  import type { AnyFn, Implementation, InferArgs, InferImplSchema, InheritArgNames } from './fnTypes.ts';
9
- import type { Withable } from '../root/rootTypes.ts';
9
+ import type { Configurable, Withable } from '../root/rootTypes.ts';
10
10
  /**
11
11
  * Describes a function signature (its arguments and return type)
12
12
  */
@@ -29,6 +29,11 @@ interface TgpuFnBase<ImplSchema extends AnyFn> extends TgpuNamable, Withable<Tgp
29
29
  readonly shell: TgpuFnShellHeader<Parameters<ImplSchema>, Extract<ReturnType<ImplSchema>, BaseData>>;
30
30
  readonly [$providing]?: Providing | undefined;
31
31
  $uses(dependencyMap: Record<string, unknown>): this;
32
+ /**
33
+ * Applies a configuration transform to the slots and accessors this function reads,
34
+ * as `root.pipe` does to a pipeline's.
35
+ */
36
+ pipe(transform: (cfg: Configurable) => Configurable): TgpuFn<ImplSchema>;
32
37
  }
33
38
  export type TgpuFn<ImplSchema extends AnyFn = (...args: any[]) => any> = DualFn<InferImplSchema<ImplSchema>> & TgpuFnBase<ImplSchema>;
34
39
  /**
@@ -40,6 +45,11 @@ export interface TgpuGenericFn<T extends AnyFn> extends TgpuNamable, Withable<Tg
40
45
  };
41
46
  readonly [$providing]?: Providing | undefined;
42
47
  readonly resourceType: 'generic-function';
48
+ /**
49
+ * Applies a configuration transform to the slots and accessors this function reads,
50
+ * as `root.pipe` does to a pipeline's.
51
+ */
52
+ pipe(transform: (cfg: Configurable) => Configurable): TgpuGenericFn<T>;
43
53
  (...args: Parameters<T>): ReturnType<T>;
44
54
  }
45
55
  export declare function fn<Args extends BaseData[] | []>(argTypes: Args, returnType?: undefined): TgpuFnShell<Args, Void>;
@@ -12,6 +12,7 @@ import { dualImpl } from "./dualImpl.js";
12
12
  import { createFnCore } from "./fnCore.js";
13
13
  import { stripTemplate } from "./templateUtils.js";
14
14
  import { comptime } from "./comptime.js";
15
+ import { ConfigurableImpl } from "../root/configurableImpl.js";
15
16
  export function fn(argTypesOrCallback, returnType) {
16
17
  if (typeof argTypesOrCallback === 'function') {
17
18
  return createGenericFn(argTypesOrCallback, []);
@@ -39,6 +40,9 @@ export function isGenericFn(value) {
39
40
  function stringifyPair([slot, value]) {
40
41
  return `${getName(slot) ?? '<unnamed>'}=${value}`;
41
42
  }
43
+ function bindingsOf(transform) {
44
+ return transform(new ConfigurableImpl([])).bindings;
45
+ }
42
46
  function createFn(shell, _implementation) {
43
47
  let pairs = [];
44
48
  // Unwrapping generic functions
@@ -68,6 +72,7 @@ function createFn(shell, _implementation) {
68
72
  const s = isAccessor(slot) || isMutableAccessor(slot) ? slot.slot : slot;
69
73
  return createBoundFunction(fn, [[s, value]]);
70
74
  }),
75
+ pipe: comptime((transform) => createBoundFunction(fn, bindingsOf(transform))),
71
76
  [$resolve](ctx) {
72
77
  if (typeof implementation === 'string') {
73
78
  addArgTypesToExternals(implementation, shell.argTypes, core);
@@ -129,6 +134,7 @@ function createBoundFunction(innerFn, pairs) {
129
134
  const s = isAccessor(slot) || isMutableAccessor(slot) ? slot.slot : slot;
130
135
  return createBoundFunction(innerFn, [...pairs, [s, value]]);
131
136
  }),
137
+ pipe: comptime((transform) => createBoundFunction(innerFn, [...pairs, ...bindingsOf(transform)])),
132
138
  };
133
139
  const call = dualImpl({
134
140
  name: undefined, // setting name here would override autonaming
@@ -170,10 +176,11 @@ function createGenericFn(inner, pairs) {
170
176
  [$resolve](ctx) {
171
177
  return ctx.resolve(inner);
172
178
  },
173
- with(slot, value) {
179
+ with: comptime((slot, value) => {
174
180
  const s = isAccessor(slot) || isMutableAccessor(slot) ? slot.slot : slot;
175
181
  return createGenericFn(inner, [...pairs, [s, value]]);
176
- },
182
+ }),
183
+ pipe: comptime((transform) => createGenericFn(inner, [...pairs, ...bindingsOf(transform)])),
177
184
  };
178
185
  const call = (...args) => {
179
186
  return inner(...args);
@@ -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)[];