typegpu 0.12.7 → 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 (60) hide show
  1. package/core/function/autoIO.js +2 -2
  2. package/core/function/comptime.js +7 -5
  3. package/core/function/createCallableSchema.js +4 -3
  4. package/core/function/dualImpl.js +20 -19
  5. package/core/function/ioSchema.d.ts +10 -0
  6. package/core/function/ioSchema.js +37 -16
  7. package/core/function/tgpuFn.d.ts +11 -1
  8. package/core/function/tgpuFn.js +9 -2
  9. package/core/pipeline/renderPipeline.d.ts +4 -2
  10. package/core/pipeline/renderPipeline.js +12 -10
  11. package/core/pipeline/timeable.js +15 -2
  12. package/core/root/configurableImpl.js +1 -2
  13. package/core/texture/externalTexture.d.ts +1 -1
  14. package/core/texture/externalTexture.js +3 -1
  15. package/core/texture/texture.d.ts +1 -1
  16. package/core/texture/texture.js +4 -1
  17. package/core/unroll/tgpuUnroll.d.ts +4 -3
  18. package/core/unroll/tgpuUnroll.js +7 -1
  19. package/core/valueProxyUtils.d.ts +4 -2
  20. package/core/valueProxyUtils.js +4 -2
  21. package/data/compiledIO.js +12 -9
  22. package/data/dataIO.d.ts +10 -6
  23. package/data/dataIO.js +19 -4
  24. package/data/dataTypes.d.ts +0 -5
  25. package/data/dataTypes.js +0 -6
  26. package/data/matrixColumnsAccess.d.ts +8 -0
  27. package/data/matrixColumnsAccess.js +15 -0
  28. package/data/offsetUtils.js +181 -102
  29. package/data/ref.d.ts +12 -5
  30. package/data/ref.js +50 -23
  31. package/data/snippet.d.ts +7 -0
  32. package/data/snippet.js +9 -0
  33. package/data/struct.js +3 -0
  34. package/data/wgslTypes.d.ts +3 -3
  35. package/package.json +1 -1
  36. package/resolutionCtx.js +1 -1
  37. package/shared/meta.js +1 -1
  38. package/shared/symbols.d.ts +5 -0
  39. package/shared/symbols.js +6 -1
  40. package/std/numeric.js +1 -4
  41. package/std/packing.js +10 -10
  42. package/tgpuLogger.d.ts +1 -1
  43. package/tgpuLogger.js +0 -2
  44. package/tgsl/accessIndex.js +18 -15
  45. package/tgsl/accessProp.js +10 -7
  46. package/tgsl/conversion.js +3 -2
  47. package/tgsl/forOfUtils.js +2 -2
  48. package/tgsl/generationHelpers.d.ts +4 -3
  49. package/tgsl/generationHelpers.js +16 -3
  50. package/tgsl/infixDispatch.d.ts +4 -1
  51. package/tgsl/infixDispatch.js +13 -1
  52. package/tgsl/makeDereferenceable.d.ts +1 -1
  53. package/tgsl/makeDereferenceable.js +3 -1
  54. package/tgsl/shellless.js +5 -9
  55. package/tgsl/stringifySnippet.d.ts +9 -0
  56. package/tgsl/stringifySnippet.js +24 -0
  57. package/tgsl/wgslGenerator.d.ts +1 -0
  58. package/tgsl/wgslGenerator.js +98 -66
  59. package/types.d.ts +23 -2
  60. package/types.js +30 -5
@@ -67,9 +67,9 @@ export class AutoVertexFn {
67
67
  setName(impl, 'vertexFn');
68
68
  }
69
69
  this.#core = createFnCore(impl, 'vertex');
70
- this.autoIn = new AutoStruct({ ...builtinVertexIn, ...attribs }, undefined, locations);
70
+ this.autoIn = new AutoStruct({ ...builtinVertexIn, ...attribs }, undefined);
71
71
  setName(this.autoIn, 'VertexIn');
72
- this.autoOut = new AutoStruct(builtinVertexOut, undefined, undefined,
72
+ this.autoOut = new AutoStruct(builtinVertexOut, undefined, locations,
73
73
  /* autoInterpolateIntegers */ true);
74
74
  setName(this.autoOut, 'VertexOut');
75
75
  }
@@ -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,6 +7,16 @@ 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
+ /**
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
+ */
10
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;
@@ -3,33 +3,42 @@ 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
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
+ */
6
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 [
19
- key,
20
- autoInterpolateIntegers ? withFlatInterpolationForInteger(member) : member,
21
- ];
26
+ // Interpolate
27
+ const interpolated = autoInterpolateIntegers
28
+ ? withFlatInterpolationForInteger(member)
29
+ : member;
30
+ return [key, interpolated];
22
31
  })
23
32
  .map(([key, member]) => {
33
+ // Assign locations
24
34
  if (isBuiltin(member)) {
25
- // skipping builtins
26
35
  return [key, member];
27
36
  }
28
37
  if (getCustomLocation(member) !== undefined) {
29
38
  // this member is already marked
30
39
  return [key, member];
31
40
  }
32
- if (locations[key]) {
41
+ if (locations[key] !== undefined) {
33
42
  // location has been determined by a previous procedure
34
43
  return [key, location(locations[key], member)];
35
44
  }
@@ -37,7 +46,19 @@ export function withLocations(members, locations = {}, autoInterpolateIntegers =
37
46
  nextLocation++;
38
47
  }
39
48
  return [key, location(nextLocation++, member)];
40
- }));
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);
41
62
  }
42
63
  export function separateBuiltins(schema, locations = {}) {
43
64
  const positionalArgs = [];
@@ -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);
@@ -286,8 +286,10 @@ declare class RenderPipelineCore implements SelfResolvable {
286
286
  };
287
287
  }
288
288
  /**
289
- * Assumes vertexOut and fragmentIn are matching when it comes to the keys, that is fragmentIn's keyset is a subset of vertexOut's
290
- * Logs a warning, when they don't match in terms of custom locations
289
+ * Throws, when vertexOut and fragmentIn don't match in terms of custom locations.
290
+ *
291
+ * @privateRemarks
292
+ * Note that vertexOut is undefined in shellless functions.
291
293
  */
292
294
  export declare function matchUpVaryingLocations(vertexOut: TgpuVertexFn.Out | undefined, fragmentIn: TgpuFragmentFn.In | undefined, vertexFnName: string, fragmentFnName: string): Record<string, number>;
293
295
  export {};
@@ -181,7 +181,7 @@ class TgpuRenderPipelineImpl {
181
181
  withStencilReference(reference) {
182
182
  return this.#withPriors({ stencilReference: reference });
183
183
  }
184
- withIndexBuffer(buffer, indexFormatOrOffset, offsetElementsOrSizeBytes, sizeElementsOrUndefined) {
184
+ withIndexBuffer(buffer, indexFormatOrOffset, offsetBytesOrSizeElements, sizeBytesOrUndefined) {
185
185
  if (isGPUBuffer(buffer)) {
186
186
  if (typeof indexFormatOrOffset !== 'string') {
187
187
  throw new Error('If a GPUBuffer is passed, indexFormat must be provided.');
@@ -190,8 +190,8 @@ class TgpuRenderPipelineImpl {
190
190
  indexBuffer: {
191
191
  buffer,
192
192
  indexFormat: indexFormatOrOffset,
193
- offsetBytes: offsetElementsOrSizeBytes,
194
- sizeBytes: sizeElementsOrUndefined,
193
+ offsetBytes: offsetBytesOrSizeElements,
194
+ sizeBytes: sizeBytesOrUndefined,
195
195
  },
196
196
  });
197
197
  }
@@ -207,8 +207,8 @@ class TgpuRenderPipelineImpl {
207
207
  offsetBytes: indexFormatOrOffset !== undefined
208
208
  ? indexFormatOrOffset * sizeOf(elementType)
209
209
  : undefined,
210
- sizeBytes: sizeElementsOrUndefined !== undefined
211
- ? sizeElementsOrUndefined * sizeOf(elementType)
210
+ sizeBytes: offsetBytesOrSizeElements !== undefined
211
+ ? offsetBytesOrSizeElements * sizeOf(elementType)
212
212
  : undefined,
213
213
  },
214
214
  });
@@ -469,8 +469,10 @@ class RenderPipelineCore {
469
469
  }
470
470
  }
471
471
  /**
472
- * Assumes vertexOut and fragmentIn are matching when it comes to the keys, that is fragmentIn's keyset is a subset of vertexOut's
473
- * Logs a warning, when they don't match in terms of custom locations
472
+ * Throws, when vertexOut and fragmentIn don't match in terms of custom locations.
473
+ *
474
+ * @privateRemarks
475
+ * Note that vertexOut is undefined in shellless functions.
474
476
  */
475
477
  export function matchUpVaryingLocations(vertexOut = {}, fragmentIn = {}, vertexFnName, fragmentFnName) {
476
478
  const locations = {};
@@ -495,13 +497,13 @@ export function matchUpVaryingLocations(vertexOut = {}, fragmentIn = {}, vertexF
495
497
  saveLocation(key, customLocation);
496
498
  }
497
499
  else if (locations[key] !== customLocation) {
498
- logger.warn('locations-mismatched', `Mismatched location between vertexFn (${vertexFnName}) output (${locations[key]}) and fragmentFn (${fragmentFnName}) input (${customLocation}) for the key "${key}", using the location set on vertex output.`);
500
+ throw new Error(`Mismatched location between vertexFn (${vertexFnName}) output (${locations[key]}) and fragmentFn (${fragmentFnName}) input (${customLocation}) for the key "${key}".`);
499
501
  }
500
502
  }
501
503
  // automatically assign remaining locations to the rest
502
504
  let nextLocation = 0;
503
- for (const key of Object.keys(vertexOut ?? {})) {
504
- if (isBuiltin(vertexOut[key]) || locations[key] !== undefined) {
505
+ for (const [key, value] of [...Object.entries(vertexOut), ...Object.entries(fragmentIn)]) {
506
+ if (isBuiltin(value) || locations[key] !== undefined) {
505
507
  continue;
506
508
  }
507
509
  while (usedLocations.has(nextLocation)) {
@@ -41,14 +41,27 @@ async function readTimestamps(querySet, registrations) {
41
41
  if (!querySet.available) {
42
42
  return;
43
43
  }
44
- const result = await querySet.read();
44
+ let result;
45
+ try {
46
+ result = await querySet.read();
47
+ }
48
+ catch {
49
+ // The read was cancelled or failed (e.g. the device was destroyed or lost mid-read).
50
+ // Genuine validation errors are already reported through the device's error handling.
51
+ return;
52
+ }
45
53
  for (const { priors, callback } of registrations) {
46
54
  const start = result[priors.timestampWrites?.beginningOfPassWriteIndex ?? 0];
47
55
  const end = result[priors.timestampWrites?.endOfPassWriteIndex ?? 1];
48
56
  if (start === undefined || end === undefined) {
49
57
  throw new Error('QuerySet did not return valid timestamps.');
50
58
  }
51
- await callback(start, end);
59
+ try {
60
+ await callback(start, end);
61
+ }
62
+ catch (err) {
63
+ console.error('An error was thrown from a performance callback (withPerformanceCallback). Errors thrown here cannot be caught by the caller, so handle them within the callback itself.', err);
64
+ }
52
65
  }
53
66
  }
54
67
  /** Returns false when the encoder is one we cannot defer work to, meaning the callback never fires */
@@ -11,7 +11,6 @@ export class ConfigurableImpl {
11
11
  ]);
12
12
  }
13
13
  pipe(transform) {
14
- const newCfg = transform(this);
15
- return new ConfigurableImpl([...this.bindings, ...newCfg.bindings]);
14
+ return transform(this);
16
15
  }
17
16
  }
@@ -1,7 +1,7 @@
1
1
  import { $gpuValueOf, $internal, $resolve } from '../../shared/symbols.ts';
2
2
  import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts';
3
3
  import { type WgslExternalTexture } from '../../data/texture.ts';
4
- import type { ResolutionCtx, SelfResolvable } from '../../types.ts';
4
+ import { type ResolutionCtx, type SelfResolvable } from '../../types.ts';
5
5
  import { type ResolvedSnippet } from '../../data/snippet.ts';
6
6
  import type { Infer } from '../../shared/repr.ts';
7
7
  export interface TgpuExternalTexture {
@@ -1,8 +1,9 @@
1
- import { $gpuValueOf, $internal, $ownSnippet, $resolve } from "../../shared/symbols.js";
1
+ import { $comptimeValueOf, $gpuValueOf, $internal, $ownSnippet, $resolve, } from "../../shared/symbols.js";
2
2
  import { getName, setName } from "../../shared/meta.js";
3
3
  import { textureExternal } from "../../data/texture.js";
4
4
  import { valueProxyHandler } from "../valueProxyUtils.js";
5
5
  import { inCodegenMode } from "../../execMode.js";
6
+ import { runtimeOnly } from "../../types.js";
6
7
  import { snip } from "../../data/snippet.js";
7
8
  export function isExternalTexture(value) {
8
9
  return value?.resourceType === 'external-texture';
@@ -33,6 +34,7 @@ export class TgpuExternalTextureImpl {
33
34
  get [$ownSnippet]() {
34
35
  return snip(this, schema, 'handle', false);
35
36
  },
37
+ [$comptimeValueOf]: runtimeOnly,
36
38
  [$resolve]: (ctx) => ctx.resolve(this),
37
39
  toString: () => `textureExternal:${getName(this) ?? '<unnamed>'}.$`,
38
40
  }, valueProxyHandler);
@@ -9,7 +9,7 @@ import type { TgpuDeviceOwningSoul, TgpuSoul } from '../../shared/soul.ts';
9
9
  import { $gpuValueOf, $internal, $repr, $resolve, $soul } from '../../shared/symbols.ts';
10
10
  import type { Default, TypedArray, UnionToIntersection } from '../../shared/utilityTypes.ts';
11
11
  import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts';
12
- import type { ResolutionCtx, SelfResolvable } from '../../types.ts';
12
+ import { type ResolutionCtx, type SelfResolvable } from '../../types.ts';
13
13
  import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts';
14
14
  import type { TextureProps } from './textureProps.ts';
15
15
  import type { AllowedUsages, LiteralToExtensionMap, RenderFlag, SampledFlag } from './usageExtension.ts';
@@ -3,7 +3,8 @@ import { inCodegenMode } from "../../execMode.js";
3
3
  import { snip } from "../../data/snippet.js";
4
4
  import { getName, setName } from "../../shared/meta.js";
5
5
  import { getTextureFormatInfo, } from "./textureFormats.js";
6
- import { $gpuValueOf, $internal, $ownSnippet, $repr, $resolve, $soul, } from "../../shared/symbols.js";
6
+ import { $gpuValueOf, $internal, $ownSnippet, $comptimeValueOf, $repr, $resolve, $soul, } from "../../shared/symbols.js";
7
+ import { runtimeOnly } from "../../types.js";
7
8
  import { valueProxyHandler } from "../valueProxyUtils.js";
8
9
  import { clearTextureWithColor, generateTextureMipmaps, writeImages } from "./textureUtils.js";
9
10
  import { mipLevelSize, origin3d, resolveImageWrite, writeRegionSize, } from "./textureWrite.js";
@@ -313,6 +314,7 @@ class TgpuFixedTextureViewImpl {
313
314
  get [$ownSnippet]() {
314
315
  return snip(this, schema, /* origin */ 'handle', false);
315
316
  },
317
+ [$comptimeValueOf]: runtimeOnly,
316
318
  [$resolve]: (ctx) => ctx.resolve(this),
317
319
  toString: () => `${this.toString()}.$`,
318
320
  }, valueProxyHandler);
@@ -375,6 +377,7 @@ export class TgpuLaidOutTextureViewImpl {
375
377
  get [$ownSnippet]() {
376
378
  return snip(this, schema, /* origin */ 'handle', false);
377
379
  },
380
+ [$comptimeValueOf]: runtimeOnly,
378
381
  [$resolve]: (ctx) => ctx.resolve(this),
379
382
  toString: () => `${this.toString()}.$`,
380
383
  }, valueProxyHandler);
@@ -1,15 +1,16 @@
1
- import { $internal, $resolve } from '../../shared/symbols.ts';
1
+ import { $comptimeValueOf, $internal, $resolve } from '../../shared/symbols.ts';
2
+ import { type ComptimeValue, type ResolutionCtx, type SelfResolvable, type WithComptimeValue } from '../../types.ts';
2
3
  import { type ResolvedSnippet, type Snippet } from '../../data/snippet.ts';
3
- import type { ResolutionCtx, SelfResolvable } from '../../types.ts';
4
4
  /**
5
5
  * The result of calling `tgpu.unroll(...)`. The code responsible for
6
6
  * generating shader code can check if the value of a snippet is
7
7
  * an instance of `UnrollableIterable`, and act accordingly.
8
8
  */
9
- export declare class UnrollableIterable implements SelfResolvable {
9
+ export declare class UnrollableIterable implements SelfResolvable, WithComptimeValue {
10
10
  readonly [$internal] = true;
11
11
  readonly snippet: Snippet;
12
12
  constructor(snippet: Snippet);
13
+ [$comptimeValueOf](): ComptimeValue;
13
14
  [$resolve](ctx: ResolutionCtx): ResolvedSnippet;
14
15
  }
15
16
  /**
@@ -1,5 +1,6 @@
1
- import { $gpuCallable, $internal, $resolve } from "../../shared/symbols.js";
1
+ import { $comptimeValueOf, $gpuCallable, $internal, $resolve } from "../../shared/symbols.js";
2
2
  import { setName } from "../../shared/meta.js";
3
+ import { asComptime, } from "../../types.js";
3
4
  import { withValue } from "../../data/snippet.js";
4
5
  /**
5
6
  * The result of calling `tgpu.unroll(...)`. The code responsible for
@@ -12,6 +13,11 @@ export class UnrollableIterable {
12
13
  constructor(snippet) {
13
14
  this.snippet = snippet;
14
15
  }
16
+ [$comptimeValueOf]() {
17
+ // Outside of for-of loops, `tgpu.unroll(...)` is transparent, just like in JS
18
+ const known = asComptime(this.snippet);
19
+ return known ? { comptime: true, value: known.value } : { comptime: false };
20
+ }
15
21
  [$resolve](ctx) {
16
22
  return ctx.resolveSnippet(this.snippet);
17
23
  }
@@ -1,3 +1,5 @@
1
- import { type SelfResolvable, type WithOwnSnippet } from '../types.ts';
2
- export declare const valueProxyHandler: ProxyHandler<SelfResolvable & WithOwnSnippet>;
1
+ import { type SelfResolvable, type WithComptimeValue, type WithOwnSnippet } from '../types.ts';
2
+ type ValueProxyTarget = SelfResolvable & WithOwnSnippet & WithComptimeValue;
3
+ export declare const valueProxyHandler: ProxyHandler<ValueProxyTarget>;
3
4
  export declare function getGpuValueRecursively<T>(value: unknown): T;
5
+ export {};
@@ -1,8 +1,8 @@
1
1
  import { getGPUValue } from "../getGPUValue.js";
2
- import { $internal, $ownSnippet, $resolve } from "../shared/symbols.js";
2
+ import { $comptimeValueOf, $internal, $ownSnippet, $resolve } from "../shared/symbols.js";
3
3
  import { accessIndex } from "../tgsl/accessIndex.js";
4
4
  import { accessProp } from "../tgsl/accessProp.js";
5
- import { getOwnSnippet } from "../types.js";
5
+ import { getOwnSnippet, runtimeOnly, } from "../types.js";
6
6
  export const valueProxyHandler = {
7
7
  get(target, prop) {
8
8
  if (prop in target) {
@@ -26,6 +26,7 @@ export const valueProxyHandler = {
26
26
  [$internal]: true,
27
27
  [$resolve]: (ctx) => ctx.resolveSnippet(accessed),
28
28
  [$ownSnippet]: accessed,
29
+ [$comptimeValueOf]: runtimeOnly,
29
30
  toString: () => `${String(target)}[${prop}]`,
30
31
  }, valueProxyHandler);
31
32
  }
@@ -38,6 +39,7 @@ export const valueProxyHandler = {
38
39
  [$internal]: true,
39
40
  [$resolve]: (ctx) => ctx.resolveSnippet(accessed),
40
41
  [$ownSnippet]: accessed,
42
+ [$comptimeValueOf]: runtimeOnly,
41
43
  toString: () => `${String(target)}.${prop}`,
42
44
  }, valueProxyHandler);
43
45
  },
@@ -1,7 +1,7 @@
1
1
  import { roundUp } from "../mathUtils.js";
2
2
  import { logger } from "../tgpuLogger.js";
3
3
  import { alignmentOf } from "./alignmentOf.js";
4
- import { isDisarray, isUnstruct } from "./dataTypes.js";
4
+ import { isDisarray, isLooseDecorated, isUnstruct } from "./dataTypes.js";
5
5
  import { offsetsForProps } from "./offsets.js";
6
6
  import { sizeOf } from "./sizeOf.js";
7
7
  import { formatToWGSLType, isPackedData } from "./vertexFormatData.js";
@@ -131,7 +131,7 @@ export function buildWriter(node, offsetExpr, valueExpr, depth = 0, partial = fa
131
131
  const emitBlock = (addr, block) => partial ? `if (${addr} < endOffset) {\n${block}}\n` : block;
132
132
  function go(node, offsetExpr, valueExpr, depth) {
133
133
  const loopVar = ['i', 'j', 'k'][depth] || `i${depth}`;
134
- if (wgsl.isAtomic(node) || wgsl.isDecorated(node)) {
134
+ if (wgsl.isAtomic(node) || wgsl.isDecorated(node) || isLooseDecorated(node)) {
135
135
  return go(node.inner, offsetExpr, valueExpr, depth);
136
136
  }
137
137
  if (wgsl.isWgslStruct(node) || isUnstruct(node)) {
@@ -178,15 +178,18 @@ export function buildWriter(node, offsetExpr, valueExpr, depth = 0, partial = fa
178
178
  const primitive = typeToPrimitive[node.type];
179
179
  const writeFunc = primitiveToWriteFunction[primitive];
180
180
  const matSize = wgsl.isMat2x2f(node) ? 2 : wgsl.isMat3x3f(node) ? 3 : 4;
181
- const rowStride = roundUp(matSize * 4, 8);
182
181
  const components = ['x', 'y', 'z', 'w'];
183
- const wgslElementStride = rowStride / 4;
184
- return Array.from({ length: matSize * matSize }, (_, idx) => {
185
- const col = Math.floor(idx / matSize);
186
- const row = idx % matSize;
182
+ const wgslElementStride = roundUp(matSize, 2);
183
+ return Array.from({ length: matSize * wgslElementStride }, (_, idx) => {
184
+ const col = Math.floor(idx / wgslElementStride);
185
+ const row = idx % wgslElementStride;
186
+ const addr = `(${offsetExpr} + ${idx * 4})`;
187
+ if (row >= matSize) {
188
+ // mat3x3f padding
189
+ return emitWrite(writeFunc, addr, '0');
190
+ }
187
191
  const packedIndex = col * matSize + row;
188
- const wgslIndex = col * wgslElementStride + row;
189
- return emitWrite(writeFunc, `(${offsetExpr} + ${col * rowStride + row * 4})`, `ArrayBuffer.isView(${valueExpr}) ? ${valueExpr}[${wgslIndex}] : Array.isArray(${valueExpr}) ? ${valueExpr}[${packedIndex}] : ${valueExpr}.columns[${col}].${components[row]}`);
192
+ return emitWrite(writeFunc, addr, `ArrayBuffer.isView(${valueExpr}) ? ${valueExpr}[${idx}] : Array.isArray(${valueExpr}) ? ${valueExpr}[${packedIndex}] : ${valueExpr}.columns[${col}].${components[row]}`);
190
193
  }).join('');
191
194
  }
192
195
  if (isPackedData(node)) {
package/data/dataIO.d.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import { type ISerialInput, type ISerialOutput } from 'typed-binary';
2
2
  import type { Infer, InferInput } from '../shared/repr.ts';
3
- import type * as wgsl from './wgslTypes.ts';
4
- import { type BaseData } from './wgslTypes.ts';
3
+ import * as wgsl from './wgslTypes.ts';
5
4
  import type { BufferWriteOptions } from '../core/buffer/buffer.ts';
6
- export declare function writeData<TData extends wgsl.BaseData>(output: ISerialOutput, schema: TData, value: Infer<TData>): void;
5
+ export declare function writeData<TData extends wgsl.BaseData>(output: ISerialOutput, schema: TData, value: InferInput<TData>): void;
7
6
  export declare function readData<TData extends wgsl.BaseData>(input: ISerialInput, schema: TData): Infer<TData>;
8
- export declare function calculateOffsets<T extends BaseData>(options: BufferWriteOptions | undefined, schema: T, data: InferInput<T> | ArrayBuffer | ArrayBufferView): {
7
+ export declare function calculateOffsets<T extends wgsl.BaseData>(options: BufferWriteOptions | undefined, schema: T, data: InferInput<T> | ArrayBuffer | ArrayBufferView): {
9
8
  startOffset: number;
10
9
  endOffset: number;
11
10
  };
12
- export declare function writeToArrayBuffer<T extends BaseData>(buffer: ArrayBuffer, schema: T, data: InferInput<T> | ArrayBuffer | ArrayBufferView, options?: BufferWriteOptions): void;
13
- export declare function readFromArrayBuffer<T extends BaseData>(buffer: ArrayBuffer, schema: T): Infer<T>;
11
+ /**
12
+ * A function for filling in buffers with data based on TypeGPU schemas.
13
+ * When schema instances or JS arrays are passed, the padding is applied automatically.
14
+ * ArrayBuffers are expected to be already padded.
15
+ */
16
+ export declare function writeToArrayBuffer<T extends wgsl.BaseData>(buffer: ArrayBuffer, schema: T, data: InferInput<T> | ArrayBuffer | ArrayBufferView, options?: BufferWriteOptions): void;
17
+ export declare function readFromArrayBuffer<T extends wgsl.BaseData>(buffer: ArrayBuffer, schema: T): Infer<T>;