typegpu 0.12.5 → 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.
- package/common/index.d.ts +1 -0
- package/common/index.js +1 -0
- package/common/writeChannels.d.ts +31 -0
- package/common/writeChannels.js +32 -0
- package/common/writeSoA.js +16 -0
- package/core/buffer/buffer.js +47 -11
- package/core/commandEncoder/attachments.js +5 -2
- package/core/commandEncoder/computePass.d.ts +13 -0
- package/core/commandEncoder/computePass.js +5 -2
- package/core/commandEncoder/renderPass.d.ts +18 -4
- package/core/commandEncoder/renderPass.js +6 -2
- package/core/declare/tgpuDeclare.d.ts +11 -0
- package/core/declare/tgpuDeclare.js +1 -1
- package/core/function/autoIO.js +4 -2
- package/core/function/ioSchema.d.ts +7 -2
- package/core/function/ioSchema.js +41 -15
- package/core/function/tgpuFragmentFn.d.ts +1 -1
- package/core/function/tgpuFragmentFn.js +2 -1
- package/core/function/tgpuVertexFn.js +2 -1
- package/core/immediate/immediateVar.d.ts +52 -0
- package/core/immediate/immediateVar.js +149 -0
- package/core/pipeline/computePipeline.d.ts +17 -0
- package/core/pipeline/computePipeline.js +23 -9
- package/core/pipeline/connectAttachmentToShader.d.ts +2 -2
- package/core/pipeline/connectAttachmentToShader.js +16 -21
- package/core/pipeline/connectTargetsToShader.d.ts +5 -1
- package/core/pipeline/connectTargetsToShader.js +23 -18
- package/core/pipeline/drawState.d.ts +8 -4
- package/core/pipeline/drawState.js +41 -51
- package/core/pipeline/priors.js +1 -0
- package/core/pipeline/renderPipeline.d.ts +22 -4
- package/core/pipeline/renderPipeline.js +25 -13
- package/core/resolve/namespace.js +2 -2
- package/core/root/init.js +3 -0
- package/core/root/rootTypes.d.ts +17 -2
- package/core/slot/accessor.d.ts +1 -1
- package/core/slot/accessor.js +72 -5
- package/core/slot/slotTypes.d.ts +2 -1
- package/core/texture/texture.d.ts +20 -14
- package/core/texture/texture.js +78 -69
- package/core/texture/textureUtils.d.ts +3 -6
- package/core/texture/textureUtils.js +142 -165
- package/core/texture/textureWrite.d.ts +61 -0
- package/core/texture/textureWrite.js +59 -0
- package/data/autoStruct.d.ts +1 -1
- package/data/autoStruct.js +8 -4
- package/data/dataIO.d.ts +2 -2
- package/data/deepEqual.js +19 -9
- package/data/matrix.js +10 -3
- package/data/partialIO.d.ts +8 -1
- package/data/partialIO.js +36 -19
- package/data/ref.js +2 -2
- package/data/snippet.d.ts +11 -5
- package/data/snippet.js +9 -4
- package/data/wgslTypes.d.ts +4 -1
- package/errors.d.ts +7 -0
- package/errors.js +15 -0
- package/indexNamedExports.d.ts +5 -2
- package/indexNamedExports.js +2 -1
- package/internal.d.ts +2 -1
- package/internal.js +1 -1
- package/nameUtils.d.ts +7 -0
- package/nameUtils.js +60 -0
- package/package.json +2 -2
- package/resolutionCtx.d.ts +5 -0
- package/resolutionCtx.js +11 -0
- package/serial/restore.d.ts +3 -1
- package/serial/restore.js +2 -0
- package/shared/meta.js +1 -1
- package/shared/symbols.js +1 -1
- package/shared/tseynit.d.ts +1 -0
- package/shared/tseynit.js +80 -44
- package/std/atomic.d.ts +13 -0
- package/std/atomic.js +63 -29
- package/std/boolean.js +9 -10
- package/std/derivative.js +14 -9
- package/std/index.d.ts +4 -4
- package/std/index.js +5 -5
- package/std/numeric.d.ts +5 -1
- package/std/numeric.js +3 -3
- package/std/operators.js +9 -4
- package/std/packing.d.ts +61 -1
- package/std/packing.js +214 -25
- package/std/subgroup.d.ts +4 -0
- package/std/subgroup.js +34 -0
- package/std/texture.d.ts +15 -2
- package/std/texture.js +48 -0
- package/tgpuUnstable.d.ts +1 -0
- package/tgpuUnstable.js +1 -0
- package/tgsl/accessIndex.js +19 -23
- package/tgsl/conversion.d.ts +2 -0
- package/tgsl/conversion.js +3 -0
- package/tgsl/forOfUtils.js +6 -6
- package/tgsl/generationHelpers.js +2 -2
- package/tgsl/shaderGenerator.d.ts +1 -1
- package/tgsl/wgslGenerator.d.ts +5 -0
- package/tgsl/wgslGenerator.js +288 -58
- package/types.d.ts +9 -1
- package/types.js +5 -0
package/tgsl/wgslGenerator.js
CHANGED
|
@@ -4,16 +4,16 @@ 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,
|
|
7
|
+
import { fallthroughCopyOrigin, isStoredInMemory, snip, } from "../data/snippet.js";
|
|
8
8
|
import * as wgsl from "../data/wgslTypes.js";
|
|
9
|
-
import { invariant, ResolutionError, WgslTypeError } from "../errors.js";
|
|
9
|
+
import { invariant, ResolutionError, WgslForbiddenStatementError, WgslTypeError, } from "../errors.js";
|
|
10
10
|
import { getName } from "../shared/meta.js";
|
|
11
11
|
import { $gpuCallable, $internal, $providing, isMarkedInternal } from "../shared/symbols.js";
|
|
12
12
|
import { safeStringify } from "../shared/stringify.js";
|
|
13
13
|
import { pow } from "../std/numeric.js";
|
|
14
14
|
import { add, div, mul, neg, sub } from "../std/operators.js";
|
|
15
15
|
import { eq, ne, lt, le, gt, ge, not } from "../std/boolean.js";
|
|
16
|
-
import { isGPUCallable, isKnownAtComptime, } from "../types.js";
|
|
16
|
+
import { isConstant, isGPUCallable, isKnownAtComptime, } from "../types.js";
|
|
17
17
|
import { convertStructValues, convertToCommonType, tryConvertSnippet } from "./conversion.js";
|
|
18
18
|
import { ArrayExpression, coerceToSnippet, concretize, numericLiteralToSnippet, } from "./generationHelpers.js";
|
|
19
19
|
import { accessIndex } from "./accessIndex.js";
|
|
@@ -28,11 +28,12 @@ import { AutoStruct } from "../data/autoStruct.js";
|
|
|
28
28
|
import { mathToStd, supportedLogOps } from "./jsPolyfills.js";
|
|
29
29
|
import * as forOfUtils from "./forOfUtils.js";
|
|
30
30
|
import { isTgpuRange } from "../std/range.js";
|
|
31
|
-
import { stringifyNode } from "../shared/tseynit.js";
|
|
31
|
+
import { stringifyNode, stringifyObjectProperty } from "../shared/tseynit.js";
|
|
32
32
|
import { getAttributesString } from "../data/attributes.js";
|
|
33
33
|
import { validSelectBranchTypes } from "../std/boolean.js";
|
|
34
34
|
import { isInfixDispatch } from "./infixDispatch.js";
|
|
35
35
|
import { logger } from "../tgpuLogger.js";
|
|
36
|
+
import { TgpuDeclareImpl } from "../core/declare/tgpuDeclare.js";
|
|
36
37
|
const { NodeTypeCatalog: NODE } = tinyest;
|
|
37
38
|
const parenthesizedOps = [
|
|
38
39
|
'==',
|
|
@@ -151,11 +152,23 @@ const usageToVarTemplateMap = {
|
|
|
151
152
|
uniform: 'uniform',
|
|
152
153
|
mutable: 'storage, read_write',
|
|
153
154
|
readonly: 'storage, read',
|
|
155
|
+
immediate: 'immediate',
|
|
154
156
|
};
|
|
155
157
|
/**
|
|
156
158
|
* The block depth that we can expect when generating code in the function scope, not in any nested blocks.
|
|
157
159
|
*/
|
|
158
160
|
const functionInitialBlockDepth = 2;
|
|
161
|
+
// Used for switch case.
|
|
162
|
+
const switchDefault = snip('default', UnknownData, 'constant');
|
|
163
|
+
function schemaWrappingSuggestion(declaration, rhs, value) {
|
|
164
|
+
if (value === null || value === undefined || typeof value === 'string') {
|
|
165
|
+
return '';
|
|
166
|
+
}
|
|
167
|
+
return `
|
|
168
|
+
-----
|
|
169
|
+
- Try using or defining a schema that matches your desired value the most, and wrap the value with it: '${declaration} = Schema(${rhs})'
|
|
170
|
+
-----`;
|
|
171
|
+
}
|
|
159
172
|
export class WgslGenerator {
|
|
160
173
|
#ctx = undefined;
|
|
161
174
|
// used to detect `continue` and `break` nodes in loop body, as well as label
|
|
@@ -265,6 +278,27 @@ export class WgslGenerator {
|
|
|
265
278
|
}
|
|
266
279
|
return res;
|
|
267
280
|
}
|
|
281
|
+
_emitSwitchStatement(discriminantExpr, groupedCaseExprs) {
|
|
282
|
+
if (groupedCaseExprs.flatMap(([tests]) => tests).every((test) => test.value !== 'default')) {
|
|
283
|
+
// default clause is required in WGSL
|
|
284
|
+
groupedCaseExprs.push([[switchDefault], []]);
|
|
285
|
+
}
|
|
286
|
+
this.ctx.indent();
|
|
287
|
+
const cases = groupedCaseExprs.map(([tests, consequent]) => {
|
|
288
|
+
const resolvedTests = tests
|
|
289
|
+
.map((test) => (test.value === 'default' ? 'default' : this.ctx.resolveSnippet(test).value))
|
|
290
|
+
.join(', ');
|
|
291
|
+
// Purely cosmetic break pruning (it is legal, but there's no need to generate it).
|
|
292
|
+
const last = consequent.at(-1);
|
|
293
|
+
if (last && /^\s*break\s*;\s*$/.test(last.code) && last.endsWithControlFlow === 'break') {
|
|
294
|
+
consequent.pop();
|
|
295
|
+
}
|
|
296
|
+
const resolvedConsequent = consequent.map((s) => s.code).join('\n');
|
|
297
|
+
return stitch `${this.ctx.pre}case ${resolvedTests}: {\n${resolvedConsequent}\n${this.ctx.pre}}`;
|
|
298
|
+
});
|
|
299
|
+
this.ctx.dedent();
|
|
300
|
+
return stitch `${this.ctx.pre}switch ${discriminantExpr} {\n${cases.join('\n')}\n${this.ctx.pre}}`;
|
|
301
|
+
}
|
|
268
302
|
_callShellless(callee, args) {
|
|
269
303
|
const isGeneric = isGenericFn(callee);
|
|
270
304
|
const slotPairs = isGeneric ? (callee[$providing]?.pairs ?? []) : [];
|
|
@@ -308,11 +342,11 @@ export class WgslGenerator {
|
|
|
308
342
|
}
|
|
309
343
|
}
|
|
310
344
|
_expression(expression) {
|
|
311
|
-
if (
|
|
312
|
-
return this._identifier(expression);
|
|
345
|
+
if (isId(expression)) {
|
|
346
|
+
return this._identifier(extractId(expression));
|
|
313
347
|
}
|
|
314
|
-
if (
|
|
315
|
-
return snip(expression, bool, /* origin */ 'constant', false);
|
|
348
|
+
if (isBool(expression)) {
|
|
349
|
+
return snip(extractBool(expression), bool, /* origin */ 'constant', false);
|
|
316
350
|
}
|
|
317
351
|
if (expression[0] === NODE.logicalExpr) {
|
|
318
352
|
const [_, lhs, op, rhs] = expression;
|
|
@@ -442,7 +476,9 @@ export class WgslGenerator {
|
|
|
442
476
|
validateSnippetMutation(convLhs, expression);
|
|
443
477
|
this.tryMarkModified(lhs);
|
|
444
478
|
// Compound assignment operators are okay, e.g. +=, -=, *=, /=, ...
|
|
445
|
-
if (op === '=' &&
|
|
479
|
+
if (op === '=' &&
|
|
480
|
+
isStoredInMemory(rhsExpr) &&
|
|
481
|
+
!wgsl.isNaturallyEphemeral(rhsExpr.dataType)) {
|
|
446
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-----`);
|
|
447
483
|
}
|
|
448
484
|
}
|
|
@@ -481,8 +517,9 @@ export class WgslGenerator {
|
|
|
481
517
|
}
|
|
482
518
|
if (expression[0] === NODE.memberAccess) {
|
|
483
519
|
// Member Access
|
|
484
|
-
const [_, targetNode,
|
|
520
|
+
const [_, targetNode, propertyNode] = expression;
|
|
485
521
|
const target = this._expression(targetNode);
|
|
522
|
+
const property = extractId(propertyNode);
|
|
486
523
|
const accessed = accessProp(target, property);
|
|
487
524
|
if (!accessed) {
|
|
488
525
|
throw new Error(`Property '${property}' not found on '${stringifyNode(targetNode)}'`);
|
|
@@ -560,7 +597,7 @@ export class WgslGenerator {
|
|
|
560
597
|
// We just let the argument resolve everything.
|
|
561
598
|
return snip(this.ctx.resolveSnippet(arg).value, callee.value,
|
|
562
599
|
// A new array, so not a reference.
|
|
563
|
-
/* origin */
|
|
600
|
+
/* origin */ fallthroughCopyOrigin(arg.origin), arg.possibleSideEffects);
|
|
564
601
|
}
|
|
565
602
|
if (callee.value === constant) {
|
|
566
603
|
throw new Error('Constants cannot be defined within TypeGPU function scope. To address this, move the constant definition outside the function scope.');
|
|
@@ -628,11 +665,25 @@ export class WgslGenerator {
|
|
|
628
665
|
throw new Error(`Function '${getName(callee.value) ?? String(callee.value)}' is not marked with the 'use gpu' directive and cannot be used in a shader`);
|
|
629
666
|
}
|
|
630
667
|
if (expression[0] === NODE.objectExpr) {
|
|
631
|
-
//
|
|
632
|
-
const
|
|
668
|
+
// Normalize to `objectProperty[]`
|
|
669
|
+
const properties = Array.isArray(expression[1])
|
|
670
|
+
? expression[1]
|
|
671
|
+
: Object.entries(expression[1]).map(([key, value]) => [key, value, false]);
|
|
672
|
+
const seenKeys = new Map();
|
|
673
|
+
const resolveUniqueKey = (prop) => {
|
|
674
|
+
const key = this._resolveObjectPropertyKey(prop);
|
|
675
|
+
const dupProp = seenKeys.get(key);
|
|
676
|
+
if (dupProp) {
|
|
677
|
+
throw new WgslTypeError(`Duplicate object property key found: '${stringifyObjectProperty(dupProp)}' and '${stringifyObjectProperty(prop)}'.`);
|
|
678
|
+
}
|
|
679
|
+
seenKeys.set(key, prop);
|
|
680
|
+
return key;
|
|
681
|
+
};
|
|
633
682
|
const structType = this.ctx.expectedType;
|
|
634
683
|
if (structType instanceof AutoStruct) {
|
|
635
|
-
const
|
|
684
|
+
const keySnippetPairs = properties.map((prop) => {
|
|
685
|
+
const key = resolveUniqueKey(prop);
|
|
686
|
+
const value = prop[1];
|
|
636
687
|
let accessed = structType.accessProp(key);
|
|
637
688
|
let expr;
|
|
638
689
|
if (accessed) {
|
|
@@ -649,21 +700,52 @@ export class WgslGenerator {
|
|
|
649
700
|
accessed = structType.provideProp(key, unptr(concretize(expr.dataType)));
|
|
650
701
|
}
|
|
651
702
|
return [accessed.prop, expr];
|
|
652
|
-
})
|
|
703
|
+
});
|
|
704
|
+
const entries = Object.fromEntries(keySnippetPairs);
|
|
653
705
|
const completeStruct = structType.completeStruct;
|
|
654
706
|
const convertedSnippets = convertStructValues(this.ctx, completeStruct, entries);
|
|
655
707
|
return snip(stitch `${this.ctx.resolve(structType).value}(${convertedSnippets})`, completeStruct,
|
|
656
708
|
/* origin */ 'runtime');
|
|
657
709
|
}
|
|
658
710
|
if (wgsl.isWgslStruct(structType)) {
|
|
659
|
-
const entries =
|
|
660
|
-
|
|
661
|
-
|
|
711
|
+
const entries = {};
|
|
712
|
+
const keysWithSideEffectsInSourceOrder = [];
|
|
713
|
+
for (const prop of properties) {
|
|
714
|
+
const key = resolveUniqueKey(prop);
|
|
715
|
+
const value = prop[1];
|
|
716
|
+
const propType = structType.propTypes[key];
|
|
717
|
+
if (propType === undefined) {
|
|
718
|
+
// Evaluate every field even if it gets stripped by the struct schema
|
|
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
|
+
}
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
const expr = this._typedExpression(value, propType);
|
|
728
|
+
entries[key] = expr;
|
|
729
|
+
if (expr.possibleSideEffects) {
|
|
730
|
+
keysWithSideEffectsInSourceOrder.push(key);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const key of Object.keys(structType.propTypes)) {
|
|
734
|
+
if (entries[key] === undefined) {
|
|
662
735
|
throw new WgslTypeError(`Missing property ${key} in object literal for struct ${structType}`);
|
|
663
736
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
+
}
|
|
667
749
|
const convertedSnippets = convertStructValues(this.ctx, structType, entries);
|
|
668
750
|
return snip(stitch `${this.ctx.resolve(structType).value}(${convertedSnippets})`, structType,
|
|
669
751
|
/* origin */ 'runtime', convertedSnippets.some((s) => s.possibleSideEffects));
|
|
@@ -719,8 +801,8 @@ export class WgslGenerator {
|
|
|
719
801
|
!alt ||
|
|
720
802
|
consequent.possibleSideEffects ||
|
|
721
803
|
alternative.possibleSideEffects ||
|
|
722
|
-
(
|
|
723
|
-
(
|
|
804
|
+
(isStoredInMemory(consequent) && !wgsl.isNaturallyEphemeral(consequent.dataType)) ||
|
|
805
|
+
(isStoredInMemory(alternative) && !wgsl.isNaturallyEphemeral(alternative.dataType))) {
|
|
724
806
|
throw new Error(`Ternary operator '${stringifyNode(expression)}' is invalid. For more complex branching, please use 'std.select' or if/else statements.`);
|
|
725
807
|
}
|
|
726
808
|
return snip(stitch `select(${alt}, ${con}, ${convertedTest})`, con.dataType, 'runtime',
|
|
@@ -818,8 +900,7 @@ export class WgslGenerator {
|
|
|
818
900
|
// We can make this snippet ephemeral, as we know it will be deep copied in JS
|
|
819
901
|
return snip(stitch `${args[0]}`, schema, fallthroughCopyOrigin(args[0].origin), args[0].possibleSideEffects);
|
|
820
902
|
}
|
|
821
|
-
|
|
822
|
-
return snip(stitch `${this.ctx.resolve(schema).value}(${args})`, schema, 'runtime', args.some((s) => s.possibleSideEffects));
|
|
903
|
+
return snip(stitch `${this.ctx.resolve(schema).value}(${args})`, schema, args.every((arg) => arg.origin === 'constant') ? 'constant' : 'runtime', args.some((s) => s.possibleSideEffects));
|
|
823
904
|
}
|
|
824
905
|
numericLiteral(value, schema) {
|
|
825
906
|
if (!Number.isFinite(value)) {
|
|
@@ -899,7 +980,7 @@ export class WgslGenerator {
|
|
|
899
980
|
// The existence of `expectedReturnType` implies a function shell, which in turn implies that the
|
|
900
981
|
// value will be copied on return anyway
|
|
901
982
|
!expectedReturnType &&
|
|
902
|
-
|
|
983
|
+
isStoredInMemory(returnSnippet) &&
|
|
903
984
|
!wgsl.isNaturallyEphemeral(returnSnippet.dataType) &&
|
|
904
985
|
returnSnippet.origin !== 'local-def') {
|
|
905
986
|
const str = stringifyNode(returnNode);
|
|
@@ -918,7 +999,8 @@ Try 'return ${typeStr}(${str});' instead.
|
|
|
918
999
|
return `${this.ctx.pre}return;`;
|
|
919
1000
|
}
|
|
920
1001
|
_letStatement(statement) {
|
|
921
|
-
const [_,
|
|
1002
|
+
const [_, rawIdNode, eqNode] = statement;
|
|
1003
|
+
const rawId = extractId(rawIdNode);
|
|
922
1004
|
if (eqNode === undefined) {
|
|
923
1005
|
throw new Error(`'${stringifyNode(statement)}' is invalid because all variables need initializers.`);
|
|
924
1006
|
}
|
|
@@ -931,14 +1013,12 @@ Try 'return ${typeStr}(${str});' instead.
|
|
|
931
1013
|
-----`);
|
|
932
1014
|
}
|
|
933
1015
|
const definitionDataType = eq.dataType;
|
|
934
|
-
if (definitionDataType === UnknownData) {
|
|
1016
|
+
if (definitionDataType === UnknownData || wgsl.isVoid(definitionDataType)) {
|
|
935
1017
|
const rhsStr = stringifyNode(eqNode);
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'let ${rawId} = Schema(${rhsStr})'
|
|
939
|
-
-----`);
|
|
1018
|
+
const declaration = `let ${rawId}`;
|
|
1019
|
+
throw new WgslTypeError(`'${declaration} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'${schemaWrappingSuggestion(declaration, rhsStr, eq.value)}`);
|
|
940
1020
|
}
|
|
941
|
-
if (
|
|
1021
|
+
if (isStoredInMemory(eq) && !wgsl.isNaturallyEphemeral(eq.dataType)) {
|
|
942
1022
|
// `let` declarations cannot store references
|
|
943
1023
|
const rhsStr = stringifyNode(eqNode);
|
|
944
1024
|
const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value;
|
|
@@ -967,7 +1047,8 @@ Try 'return ${typeStr}(${str});' instead.
|
|
|
967
1047
|
};
|
|
968
1048
|
}
|
|
969
1049
|
_constStatement(statement) {
|
|
970
|
-
const [_,
|
|
1050
|
+
const [_, rawIdNode, eqNode] = statement;
|
|
1051
|
+
const rawId = extractId(rawIdNode);
|
|
971
1052
|
if (eqNode === undefined) {
|
|
972
1053
|
throw new Error(`'${stringifyNode(statement)}' is invalid because all variables need initializers.`);
|
|
973
1054
|
}
|
|
@@ -988,12 +1069,10 @@ Try 'return ${typeStr}(${str});' instead.
|
|
|
988
1069
|
let varOrigin = 'local-def';
|
|
989
1070
|
let varType = '<deferred>';
|
|
990
1071
|
let definitionDataType = eq.dataType;
|
|
991
|
-
if (definitionDataType === UnknownData) {
|
|
1072
|
+
if (definitionDataType === UnknownData || wgsl.isVoid(definitionDataType)) {
|
|
992
1073
|
const rhsStr = stringifyNode(eqNode);
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
- Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'const ${rawId} = Schema(${rhsStr})'
|
|
996
|
-
-----`);
|
|
1074
|
+
const declaration = `const ${rawId}`;
|
|
1075
|
+
throw new WgslTypeError(`'${declaration} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'${schemaWrappingSuggestion(declaration, rhsStr, eq.value)}`);
|
|
997
1076
|
}
|
|
998
1077
|
if (eq.origin === 'argument') {
|
|
999
1078
|
// Arguments are immutable, so we 'let' them be (kill me)
|
|
@@ -1020,7 +1099,7 @@ Try 'return ${typeStr}(${str});' instead.
|
|
|
1020
1099
|
// function code has been processed, so at least currently, we lose that info.
|
|
1021
1100
|
varOrigin = 'local-def';
|
|
1022
1101
|
}
|
|
1023
|
-
else if (!
|
|
1102
|
+
else if (!isStoredInMemory(eq)) {
|
|
1024
1103
|
// Not a reference, but also not naturally ephemeral, so we cannot guarantee it won't be mutated.
|
|
1025
1104
|
// We defer the decision for now.
|
|
1026
1105
|
varType = '<deferred>';
|
|
@@ -1083,16 +1162,16 @@ Try 'return ${typeStr}(${str});' instead.
|
|
|
1083
1162
|
};
|
|
1084
1163
|
}
|
|
1085
1164
|
_statement(statement) {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1165
|
+
// TODO(#3078): Remove this check.
|
|
1166
|
+
if (isId(statement)) {
|
|
1167
|
+
const item = this.ctx.getById(extractId(statement));
|
|
1168
|
+
if (item?.value instanceof TgpuDeclareImpl) {
|
|
1169
|
+
this.ctx.resolveSnippet(item);
|
|
1170
|
+
return { code: '', definesInNearestScope: false };
|
|
1171
|
+
}
|
|
1090
1172
|
}
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
code: `${this.ctx.pre}${statement ? 'true' : 'false'};`,
|
|
1094
|
-
definesInNearestScope: false,
|
|
1095
|
-
};
|
|
1173
|
+
if (isId(statement) || isBool(statement)) {
|
|
1174
|
+
throw new WgslForbiddenStatementError(statement);
|
|
1096
1175
|
}
|
|
1097
1176
|
if (statement[0] === NODE.return) {
|
|
1098
1177
|
return {
|
|
@@ -1205,24 +1284,24 @@ ${this.ctx.pre}else ${alternate}`,
|
|
|
1205
1284
|
const shouldUnroll = iterableExpr.value instanceof UnrollableIterable;
|
|
1206
1285
|
const iterableSnippet = shouldUnroll ? iterableExpr.value.snippet : iterableExpr;
|
|
1207
1286
|
const range = forOfUtils.getRangeSnippets(this.ctx, iterableSnippet, shouldUnroll);
|
|
1208
|
-
const originalLoopVarName = loopVar[1];
|
|
1287
|
+
const originalLoopVarName = extractId(loopVar[1]);
|
|
1209
1288
|
const blockified = blockifySingleStatement(body);
|
|
1210
1289
|
if (shouldUnroll) {
|
|
1211
1290
|
if (!isKnownAtComptime(range.end)) {
|
|
1212
1291
|
throw new Error('Cannot unroll loop. Length of iterable is unknown at comptime.');
|
|
1213
1292
|
}
|
|
1214
|
-
const length = range.end.value;
|
|
1215
|
-
if (length === 0) {
|
|
1216
|
-
return { code: '', definesInNearestScope: false };
|
|
1217
|
-
}
|
|
1218
1293
|
const { value } = iterableSnippet;
|
|
1219
1294
|
const elements = isTgpuRange(value)
|
|
1220
1295
|
? value.map((i) => coerceToSnippet(i))
|
|
1221
1296
|
: value instanceof ArrayExpression
|
|
1222
1297
|
? value.elements
|
|
1223
|
-
: Array.from({ length }, (_, i) => forOfUtils.getElementSnippet(iterableSnippet, snip(i, u32, 'constant')));
|
|
1298
|
+
: Array.from({ length: range.end.value }, (_, i) => forOfUtils.getElementSnippet(iterableSnippet, snip(i, u32, 'constant')));
|
|
1299
|
+
if (elements.length === 0) {
|
|
1300
|
+
return { code: '', definesInNearestScope: false };
|
|
1301
|
+
}
|
|
1224
1302
|
const firstElement = elements[0];
|
|
1225
|
-
if (!
|
|
1303
|
+
if (!isStoredInMemory(firstElement) &&
|
|
1304
|
+
!wgsl.isNaturallyEphemeral(firstElement.dataType)) {
|
|
1226
1305
|
throw new WgslTypeError(`Cannot unroll '${stringifyNode(iterable)}'. The elements of iterable are constructed in place but are not value types.`);
|
|
1227
1306
|
}
|
|
1228
1307
|
let blocksCode = '';
|
|
@@ -1284,6 +1363,108 @@ ${this.ctx.pre}else ${alternate}`,
|
|
|
1284
1363
|
this.ctx.popBlockScope();
|
|
1285
1364
|
}
|
|
1286
1365
|
}
|
|
1366
|
+
if (statement[0] === NODE.switch) {
|
|
1367
|
+
// Switch statement
|
|
1368
|
+
const [_, discriminant, cases] = statement;
|
|
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]);
|
|
1399
|
+
const switchType = discriminantExpr.dataType;
|
|
1400
|
+
invariant(switchType !== UnknownData);
|
|
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]) => {
|
|
1411
|
+
// In WGSL, each case is a different block. This block scope forbids scope leaking.
|
|
1412
|
+
// TODO(#3001): Consider using NODE.block here
|
|
1413
|
+
this.ctx.pushBlockScope();
|
|
1414
|
+
this.ctx.indent();
|
|
1415
|
+
this.ctx.indent();
|
|
1416
|
+
try {
|
|
1417
|
+
const consequentStmts = consequent.map((s) => this._statement(s));
|
|
1418
|
+
return [test, consequentStmts];
|
|
1419
|
+
}
|
|
1420
|
+
finally {
|
|
1421
|
+
this.ctx.dedent();
|
|
1422
|
+
this.ctx.dedent();
|
|
1423
|
+
this.ctx.popBlockScope();
|
|
1424
|
+
}
|
|
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
|
+
}
|
|
1435
|
+
// Validation
|
|
1436
|
+
{
|
|
1437
|
+
// Tests should be constant
|
|
1438
|
+
caseExprs.forEach(([testExpr], i) => {
|
|
1439
|
+
if (!isConstant(testExpr)) {
|
|
1440
|
+
const testNode = cases[i]?.[0];
|
|
1441
|
+
invariant(testNode, `Expected node to be not nullish.`);
|
|
1442
|
+
throw new Error(`All of switch tests must be constant.
|
|
1443
|
+
Test '${stringifyNode(testNode)}' is not constant, making the following switch statement invalid.
|
|
1444
|
+
This error may be caused by an implicit conversion.
|
|
1445
|
+
${stringifyNode(statement)}`);
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
// Tests should not have duplicates.
|
|
1449
|
+
// We skip this check, because WGSL errors are readable,
|
|
1450
|
+
// and we cannot easily access non-comptime known constants.
|
|
1451
|
+
// Tests should not have non-trivial fallthrough
|
|
1452
|
+
resolvedCaseExprs
|
|
1453
|
+
.slice(0, matchedCaseWasNotLast ? undefined : -1)
|
|
1454
|
+
.forEach(([_, consequent]) => {
|
|
1455
|
+
const last = consequent.at(-1);
|
|
1456
|
+
if (last && !last.endsWithControlFlow) {
|
|
1457
|
+
throw new Error(`Switch statement cannot have non-trivial fallthrough.
|
|
1458
|
+
The following switch statement is invalid:
|
|
1459
|
+
${stringifyNode(statement)}`);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return {
|
|
1464
|
+
code: this._emitSwitchStatement(discriminantExpr, groupedCaseExprs),
|
|
1465
|
+
definesInNearestScope: false,
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1287
1468
|
if (statement[0] === NODE.postUpdate) {
|
|
1288
1469
|
// Post-update statement
|
|
1289
1470
|
const [_, op, arg] = statement;
|
|
@@ -1313,10 +1494,38 @@ ${this.ctx.pre}else ${alternate}`,
|
|
|
1313
1494
|
definesInNearestScope: false,
|
|
1314
1495
|
};
|
|
1315
1496
|
}
|
|
1497
|
+
if (statement[0] === NODE.numericLiteral ||
|
|
1498
|
+
statement[0] === NODE.binaryExpr ||
|
|
1499
|
+
statement[0] === NODE.unaryExpr ||
|
|
1500
|
+
statement[0] === NODE.logicalExpr ||
|
|
1501
|
+
statement[0] === NODE.arrayExpr ||
|
|
1502
|
+
statement[0] === NODE.objectExpr ||
|
|
1503
|
+
statement[0] === NODE.nullLiteral) {
|
|
1504
|
+
// `memberAccess` is allowed for raw code snippets, `indexAccess` is allowed for consistency.
|
|
1505
|
+
// `stringLiteral` is forbidden by our injection prevention anyway.
|
|
1506
|
+
throw new WgslForbiddenStatementError(statement);
|
|
1507
|
+
}
|
|
1316
1508
|
const expr = this._expression(statement);
|
|
1317
1509
|
const resolved = expr.value !== undefined && expr.value !== null ? this.ctx.resolveSnippet(expr).value : '';
|
|
1318
1510
|
return { code: resolved ? `${this.ctx.pre}${resolved};` : '', definesInNearestScope: false };
|
|
1319
1511
|
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Resolves the key of an object property. Handles both computed and non-computed keys.
|
|
1514
|
+
*/
|
|
1515
|
+
_resolveObjectPropertyKey(property) {
|
|
1516
|
+
const computed = property[2];
|
|
1517
|
+
if (!computed) {
|
|
1518
|
+
return property[0];
|
|
1519
|
+
}
|
|
1520
|
+
const key = this._expression(property[0]);
|
|
1521
|
+
if (!isKnownAtComptime(key)) {
|
|
1522
|
+
throw new WgslTypeError(`Computed object property key '${stringifyObjectProperty(property)}' must be known at comptime.`);
|
|
1523
|
+
}
|
|
1524
|
+
if (typeof key.value !== 'string') {
|
|
1525
|
+
throw new WgslTypeError('Object property keys must be strings in TypeGPU functions.');
|
|
1526
|
+
}
|
|
1527
|
+
return key.value;
|
|
1528
|
+
}
|
|
1320
1529
|
/**
|
|
1321
1530
|
* Attempts a member access lookup to mark a variable as modified.
|
|
1322
1531
|
* @example
|
|
@@ -1358,6 +1567,9 @@ function validateSnippetMutation(mutated, expr) {
|
|
|
1358
1567
|
if (mutated.origin === 'readonly') {
|
|
1359
1568
|
throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because readonly buffers cannot be mutated.`);
|
|
1360
1569
|
}
|
|
1570
|
+
if (mutated.origin === 'immediate') {
|
|
1571
|
+
throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because immediate variables cannot be mutated.`);
|
|
1572
|
+
}
|
|
1361
1573
|
if (mutated.origin === 'argument') {
|
|
1362
1574
|
throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because non-pointer arguments cannot be mutated.`);
|
|
1363
1575
|
}
|
|
@@ -1387,7 +1599,25 @@ function extractObject(expr) {
|
|
|
1387
1599
|
(object[0] === NODE.memberAccess || object[0] === NODE.indexAccess)) {
|
|
1388
1600
|
object = object[1];
|
|
1389
1601
|
}
|
|
1390
|
-
if (
|
|
1391
|
-
return object;
|
|
1602
|
+
if (isId(object)) {
|
|
1603
|
+
return extractId(object);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function isId(expr) {
|
|
1607
|
+
return typeof expr === 'string' || (Array.isArray(expr) && expr[0] === NODE.identifier);
|
|
1608
|
+
}
|
|
1609
|
+
function extractId(ident) {
|
|
1610
|
+
if (typeof ident === 'string') {
|
|
1611
|
+
return ident;
|
|
1612
|
+
}
|
|
1613
|
+
return ident[1];
|
|
1614
|
+
}
|
|
1615
|
+
function isBool(expr) {
|
|
1616
|
+
return typeof expr === 'boolean' || (Array.isArray(expr) && expr[0] === NODE.booleanLiteral);
|
|
1617
|
+
}
|
|
1618
|
+
function extractBool(ident) {
|
|
1619
|
+
if (typeof ident === 'boolean') {
|
|
1620
|
+
return ident;
|
|
1392
1621
|
}
|
|
1622
|
+
return ident[1];
|
|
1393
1623
|
}
|
package/types.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { TgpuSampler } from './core/sampler/sampler.ts';
|
|
|
13
13
|
import { type Eventual, type SlotValuePair, type TgpuAccessor, type TgpuLazy, type TgpuSlot } from './core/slot/slotTypes.ts';
|
|
14
14
|
import type { TgpuExternalTexture } from './core/texture/externalTexture.ts';
|
|
15
15
|
import type { TgpuTexture, TgpuTextureView } from './core/texture/texture.ts';
|
|
16
|
+
import type { TgpuImmediateVar } from './core/immediate/immediateVar.ts';
|
|
16
17
|
import type { TgpuVar } from './core/variable/tgpuVariable.ts';
|
|
17
18
|
import { type AnyData, UnknownData } from './data/dataTypes.ts';
|
|
18
19
|
import type { MapValueToSnippet, ResolvedSnippet, Snippet } from './data/snippet.ts';
|
|
@@ -26,7 +27,7 @@ import type { StorageFlag } from './extension.ts';
|
|
|
26
27
|
import type { TgpuBufferBinding } from './core/buffer/bufferBinding.ts';
|
|
27
28
|
import type { ShelllessRepository } from './tgsl/shellless.ts';
|
|
28
29
|
import type { SupportedLogOp } from './tgsl/consoleLog/types.ts';
|
|
29
|
-
export type ResolvableObject = SelfResolvable | TgpuLazy<unknown> | TgpuConst | TgpuDeclare | TgpuBindGroupLayout | TgpuFn | TgpuComputeFn | TgpuFragmentFn | TgpuComputePipeline | TgpuRenderPipeline | TgpuVertexFn | TgpuSampler | TgpuAccessor | TgpuExternalTexture | TgpuTexture | TgpuTextureView | TgpuBufferBinding<BaseData> | TgpuVar | AnyVecInstance | AnyMatInstance | AnyData | ((...args: never[]) => unknown);
|
|
30
|
+
export type ResolvableObject = SelfResolvable | TgpuLazy<unknown> | TgpuConst | TgpuDeclare | TgpuBindGroupLayout | TgpuFn | TgpuComputeFn | TgpuFragmentFn | TgpuComputePipeline | TgpuRenderPipeline | TgpuVertexFn | TgpuSampler | TgpuAccessor | TgpuExternalTexture | TgpuTexture | TgpuTextureView | TgpuBufferBinding<BaseData> | TgpuVar | TgpuImmediateVar | AnyVecInstance | AnyMatInstance | AnyData | ((...args: never[]) => unknown);
|
|
30
31
|
export type Wgsl = Eventual<number | boolean | ResolvableObject>;
|
|
31
32
|
export type ShaderStage = 'compute' | 'vertex' | 'fragment';
|
|
32
33
|
export interface ResolveFunctionOptions {
|
|
@@ -281,6 +282,12 @@ export interface ResolutionCtx {
|
|
|
281
282
|
* @returns an identifier that is unique within the given scope
|
|
282
283
|
*/
|
|
283
284
|
makeUniqueIdentifier(primer: string | undefined, scope: 'global' | 'block'): string;
|
|
285
|
+
/**
|
|
286
|
+
* Registers the use of an immediate variable in the current resolution.
|
|
287
|
+
* At most one `var<immediate>` is allowed per resolution.
|
|
288
|
+
* @throws When a different immediate variable has already been registered.
|
|
289
|
+
*/
|
|
290
|
+
registerImmediate(immediate: TgpuImmediateVar): void;
|
|
284
291
|
isIdentifierBanned(name: string): boolean;
|
|
285
292
|
/**
|
|
286
293
|
* @param name The name to check.
|
|
@@ -352,6 +359,7 @@ export declare function hasCast(value: unknown): value is WithCast;
|
|
|
352
359
|
type AnyFn = (...args: never[]) => unknown;
|
|
353
360
|
export type DualFn<T extends AnyFn> = T & GPUCallable<Parameters<T>>;
|
|
354
361
|
export declare function isKnownAtComptime(snippet: Snippet): boolean;
|
|
362
|
+
export declare function isConstant(snippet: Snippet): boolean;
|
|
355
363
|
export declare function isWgsl(value: unknown): value is Wgsl;
|
|
356
364
|
export type BindableBufferUsage = 'uniform' | 'readonly' | 'mutable';
|
|
357
365
|
export type BufferUsage = 'uniform' | 'readonly' | 'mutable' | 'vertex';
|
package/types.js
CHANGED
|
@@ -33,6 +33,11 @@ export function isKnownAtComptime(snippet) {
|
|
|
33
33
|
return ((typeof snippet.value !== 'string' || snippet.dataType === UnknownData) &&
|
|
34
34
|
getOwnSnippet(snippet.value) === undefined);
|
|
35
35
|
}
|
|
36
|
+
export function isConstant(snippet) {
|
|
37
|
+
return (isKnownAtComptime(snippet) ||
|
|
38
|
+
snippet.origin === 'constant' ||
|
|
39
|
+
snippet.origin === 'constant-immutable-def');
|
|
40
|
+
}
|
|
36
41
|
export function isWgsl(value) {
|
|
37
42
|
return (typeof value === 'number' ||
|
|
38
43
|
typeof value === 'boolean' ||
|