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
@@ -4,7 +4,7 @@ import { arrayOf } from "../data/array.js";
4
4
  import { UnknownData, unptr } from "../data/dataTypes.js";
5
5
  import { bool, i32, u32 } from "../data/numeric.js";
6
6
  import { vec2u, vec3u, vec4u } from "../data/vector.js";
7
- import { fallthroughCopyOrigin, isAlias, snip, } from "../data/snippet.js";
7
+ import { fallthroughCopyOrigin, isStoredInMemory, snip, } from "../data/snippet.js";
8
8
  import * as wgsl from "../data/wgslTypes.js";
9
9
  import { invariant, ResolutionError, WgslForbiddenStatementError, WgslTypeError, } from "../errors.js";
10
10
  import { getName } from "../shared/meta.js";
@@ -476,7 +476,9 @@ export class WgslGenerator {
476
476
  validateSnippetMutation(convLhs, expression);
477
477
  this.tryMarkModified(lhs);
478
478
  // Compound assignment operators are okay, e.g. +=, -=, *=, /=, ...
479
- if (op === '=' && isAlias(rhsExpr) && !wgsl.isNaturallyEphemeral(rhsExpr.dataType)) {
479
+ if (op === '=' &&
480
+ isStoredInMemory(rhsExpr) &&
481
+ !wgsl.isNaturallyEphemeral(rhsExpr.dataType)) {
480
482
  throw new WgslTypeError(`'${stringifyNode(expression)}' is invalid, because references cannot be assigned.\n-----\nTry '${stringifyNode(lhs)} = ${this.ctx.resolve(unptr(rhsExpr.dataType)).value}(${stringifyNode(rhs)})' to copy the value instead.\n-----`);
481
483
  }
482
484
  }
@@ -707,23 +709,43 @@ export class WgslGenerator {
707
709
  }
708
710
  if (wgsl.isWgslStruct(structType)) {
709
711
  const entries = {};
712
+ const keysWithSideEffectsInSourceOrder = [];
710
713
  for (const prop of properties) {
711
714
  const key = resolveUniqueKey(prop);
712
715
  const value = prop[1];
713
716
  const propType = structType.propTypes[key];
714
717
  if (propType === undefined) {
715
718
  // Evaluate every field even if it gets stripped by the struct schema
716
- void this._expression(value);
719
+ const expr = this._expression(value);
720
+ if (expr.possibleSideEffects) {
721
+ logger.warn('suspicious', `\
722
+ Object property '${stringifyObjectProperty(prop)}' in '${stringifyNode(expression)}' does not exist on type '${String(structType)}'.
723
+ The generated shader will omit it, so its runtime side effects will not occur.`);
724
+ }
717
725
  continue;
718
726
  }
719
727
  const expr = this._typedExpression(value, propType);
720
728
  entries[key] = expr;
729
+ if (expr.possibleSideEffects) {
730
+ keysWithSideEffectsInSourceOrder.push(key);
731
+ }
721
732
  }
722
733
  for (const key of Object.keys(structType.propTypes)) {
723
734
  if (entries[key] === undefined) {
724
735
  throw new WgslTypeError(`Missing property ${key} in object literal for struct ${structType}`);
725
736
  }
726
737
  }
738
+ const keysWithSideEffectsInSchemaOrder = Object.keys(structType.propTypes).filter((key) => entries[key].possibleSideEffects);
739
+ const changesSideEffectsOrder = keysWithSideEffectsInSourceOrder.some((key, index) => key !== keysWithSideEffectsInSchemaOrder[index]);
740
+ if (changesSideEffectsOrder) {
741
+ logger.warn('suspicious', `\
742
+ Properties with possible side effects in '${stringifyNode(expression)}' do not match '${String(structType)}' declaration order:
743
+
744
+ Source order: [${keysWithSideEffectsInSourceOrder.join(', ')}]
745
+ Declaration order: [${keysWithSideEffectsInSchemaOrder.join(', ')}]
746
+
747
+ The generated shader will evaluate them in declaration order.`);
748
+ }
727
749
  const convertedSnippets = convertStructValues(this.ctx, structType, entries);
728
750
  return snip(stitch `${this.ctx.resolve(structType).value}(${convertedSnippets})`, structType,
729
751
  /* origin */ 'runtime', convertedSnippets.some((s) => s.possibleSideEffects));
@@ -779,8 +801,8 @@ export class WgslGenerator {
779
801
  !alt ||
780
802
  consequent.possibleSideEffects ||
781
803
  alternative.possibleSideEffects ||
782
- (isAlias(consequent) && !wgsl.isNaturallyEphemeral(consequent.dataType)) ||
783
- (isAlias(alternative) && !wgsl.isNaturallyEphemeral(alternative.dataType))) {
804
+ (isStoredInMemory(consequent) && !wgsl.isNaturallyEphemeral(consequent.dataType)) ||
805
+ (isStoredInMemory(alternative) && !wgsl.isNaturallyEphemeral(alternative.dataType))) {
784
806
  throw new Error(`Ternary operator '${stringifyNode(expression)}' is invalid. For more complex branching, please use 'std.select' or if/else statements.`);
785
807
  }
786
808
  return snip(stitch `select(${alt}, ${con}, ${convertedTest})`, con.dataType, 'runtime',
@@ -958,7 +980,7 @@ export class WgslGenerator {
958
980
  // The existence of `expectedReturnType` implies a function shell, which in turn implies that the
959
981
  // value will be copied on return anyway
960
982
  !expectedReturnType &&
961
- isAlias(returnSnippet) &&
983
+ isStoredInMemory(returnSnippet) &&
962
984
  !wgsl.isNaturallyEphemeral(returnSnippet.dataType) &&
963
985
  returnSnippet.origin !== 'local-def') {
964
986
  const str = stringifyNode(returnNode);
@@ -996,7 +1018,7 @@ Try 'return ${typeStr}(${str});' instead.
996
1018
  const declaration = `let ${rawId}`;
997
1019
  throw new WgslTypeError(`'${declaration} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'${schemaWrappingSuggestion(declaration, rhsStr, eq.value)}`);
998
1020
  }
999
- if (isAlias(eq) && !wgsl.isNaturallyEphemeral(eq.dataType)) {
1021
+ if (isStoredInMemory(eq) && !wgsl.isNaturallyEphemeral(eq.dataType)) {
1000
1022
  // `let` declarations cannot store references
1001
1023
  const rhsStr = stringifyNode(eqNode);
1002
1024
  const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value;
@@ -1077,7 +1099,7 @@ Try 'return ${typeStr}(${str});' instead.
1077
1099
  // function code has been processed, so at least currently, we lose that info.
1078
1100
  varOrigin = 'local-def';
1079
1101
  }
1080
- else if (!isAlias(eq)) {
1102
+ else if (!isStoredInMemory(eq)) {
1081
1103
  // Not a reference, but also not naturally ephemeral, so we cannot guarantee it won't be mutated.
1082
1104
  // We defer the decision for now.
1083
1105
  varType = '<deferred>';
@@ -1278,7 +1300,8 @@ ${this.ctx.pre}else ${alternate}`,
1278
1300
  return { code: '', definesInNearestScope: false };
1279
1301
  }
1280
1302
  const firstElement = elements[0];
1281
- if (!isAlias(firstElement) && !wgsl.isNaturallyEphemeral(firstElement.dataType)) {
1303
+ if (!isStoredInMemory(firstElement) &&
1304
+ !wgsl.isNaturallyEphemeral(firstElement.dataType)) {
1282
1305
  throw new WgslTypeError(`Cannot unroll '${stringifyNode(iterable)}'. The elements of iterable are constructed in place but are not value types.`);
1283
1306
  }
1284
1307
  let blocksCode = '';
@@ -1343,11 +1366,48 @@ ${this.ctx.pre}else ${alternate}`,
1343
1366
  if (statement[0] === NODE.switch) {
1344
1367
  // Switch statement
1345
1368
  const [_, discriminant, cases] = statement;
1346
- const discriminantExpr = this._typedExpression(discriminant, [i32, u32]);
1369
+ let discriminantExpr = this._expression(discriminant);
1370
+ let caseExprs = cases.map(([test, consequent]) => {
1371
+ const testExpr = test === null ? switchDefault : this._expression(test);
1372
+ return [testExpr, consequent];
1373
+ });
1374
+ // comptime folding
1375
+ let matchedCaseWasNotLast = false;
1376
+ if ([discriminantExpr, ...caseExprs.map(([test]) => test)].every(isKnownAtComptime)) {
1377
+ let matchedCaseIndex = caseExprs.findIndex(([test]) => test !== switchDefault && test.value === discriminantExpr.value);
1378
+ if (matchedCaseIndex === -1) {
1379
+ matchedCaseIndex = caseExprs.findIndex(([test]) => test === switchDefault);
1380
+ }
1381
+ if (matchedCaseIndex === -1) {
1382
+ return { code: '', definesInNearestScope: false };
1383
+ }
1384
+ const matchedConsequent = caseExprs
1385
+ .slice(matchedCaseIndex)
1386
+ .find(([, consequent]) => consequent.length !== 0)?.[1];
1387
+ if (matchedConsequent === undefined) {
1388
+ return { code: '', definesInNearestScope: false };
1389
+ }
1390
+ if (matchedConsequent !== caseExprs.at(-1)?.[1]) {
1391
+ // The JS behavior will be different if this case does not end with control flow.
1392
+ matchedCaseWasNotLast = true;
1393
+ }
1394
+ discriminantExpr = snip(0, u32, 'constant', false);
1395
+ caseExprs = [[switchDefault, matchedConsequent]];
1396
+ }
1397
+ // type concretization
1398
+ discriminantExpr = tryConvertSnippet(this.ctx, discriminantExpr, [i32, u32]);
1347
1399
  const switchType = discriminantExpr.dataType;
1348
1400
  invariant(switchType !== UnknownData);
1349
- const caseExprs = cases.map(([test, consequent]) => {
1350
- const testExpr = test === null ? switchDefault : this._typedExpression(test, [switchType]);
1401
+ caseExprs = caseExprs.map(([test, consequent]) => {
1402
+ // TODO(#3148): Remove this throw.
1403
+ if (test !== switchDefault && test.dataType === UnknownData) {
1404
+ throw new Error(`Failed to convert one of the switch tests to '${switchType.type}'`);
1405
+ }
1406
+ const convertedTest = test === switchDefault ? test : tryConvertSnippet(this.ctx, test, [switchType]);
1407
+ return [convertedTest, consequent];
1408
+ });
1409
+ // consequent resolution
1410
+ const resolvedCaseExprs = caseExprs.map(([test, consequent]) => {
1351
1411
  // In WGSL, each case is a different block. This block scope forbids scope leaking.
1352
1412
  // TODO(#3001): Consider using NODE.block here
1353
1413
  this.ctx.pushBlockScope();
@@ -1355,7 +1415,7 @@ ${this.ctx.pre}else ${alternate}`,
1355
1415
  this.ctx.indent();
1356
1416
  try {
1357
1417
  const consequentStmts = consequent.map((s) => this._statement(s));
1358
- return [testExpr, consequentStmts];
1418
+ return [test, consequentStmts];
1359
1419
  }
1360
1420
  finally {
1361
1421
  this.ctx.dedent();
@@ -1363,6 +1423,15 @@ ${this.ctx.pre}else ${alternate}`,
1363
1423
  this.ctx.popBlockScope();
1364
1424
  }
1365
1425
  });
1426
+ const groupedCaseExprs = [];
1427
+ let currentGroup = [];
1428
+ for (const [index, [test, consequent]] of resolvedCaseExprs.entries()) {
1429
+ currentGroup.push(test);
1430
+ if (consequent.length > 0 || index === caseExprs.length - 1) {
1431
+ groupedCaseExprs.push([currentGroup, consequent]);
1432
+ currentGroup = [];
1433
+ }
1434
+ }
1366
1435
  // Validation
1367
1436
  {
1368
1437
  // Tests should be constant
@@ -1380,7 +1449,9 @@ ${stringifyNode(statement)}`);
1380
1449
  // We skip this check, because WGSL errors are readable,
1381
1450
  // and we cannot easily access non-comptime known constants.
1382
1451
  // Tests should not have non-trivial fallthrough
1383
- caseExprs.slice(0, -1).forEach(([_, consequent]) => {
1452
+ resolvedCaseExprs
1453
+ .slice(0, matchedCaseWasNotLast ? undefined : -1)
1454
+ .forEach(([_, consequent]) => {
1384
1455
  const last = consequent.at(-1);
1385
1456
  if (last && !last.endsWithControlFlow) {
1386
1457
  throw new Error(`Switch statement cannot have non-trivial fallthrough.
@@ -1389,15 +1460,6 @@ ${stringifyNode(statement)}`);
1389
1460
  }
1390
1461
  });
1391
1462
  }
1392
- const groupedCaseExprs = [];
1393
- let currentGroup = [];
1394
- for (const [index, [test, consequent]] of caseExprs.entries()) {
1395
- currentGroup.push(test);
1396
- if (consequent.length > 0 || index === caseExprs.length - 1) {
1397
- groupedCaseExprs.push([currentGroup, consequent]);
1398
- currentGroup = [];
1399
- }
1400
- }
1401
1463
  return {
1402
1464
  code: this._emitSwitchStatement(discriminantExpr, groupedCaseExprs),
1403
1465
  definesInNearestScope: false,