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.
- package/common/writeSoA.js +16 -0
- package/core/buffer/buffer.js +47 -11
- package/core/commandEncoder/attachments.js +5 -2
- package/core/function/autoIO.js +5 -3
- package/core/function/comptime.js +7 -5
- package/core/function/createCallableSchema.js +4 -3
- package/core/function/dualImpl.js +20 -19
- package/core/function/ioSchema.d.ts +17 -2
- package/core/function/ioSchema.js +74 -27
- package/core/function/tgpuFn.d.ts +11 -1
- package/core/function/tgpuFn.js +9 -2
- package/core/function/tgpuFragmentFn.d.ts +1 -1
- package/core/function/tgpuFragmentFn.js +2 -1
- package/core/function/tgpuVertexFn.js +2 -1
- 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/renderPipeline.d.ts +9 -6
- package/core/pipeline/renderPipeline.js +22 -22
- package/core/pipeline/timeable.js +15 -2
- package/core/resolve/namespace.js +2 -2
- package/core/root/configurableImpl.js +1 -2
- package/core/root/rootTypes.d.ts +6 -2
- package/core/slot/accessor.d.ts +1 -1
- package/core/slot/accessor.js +72 -5
- package/core/texture/externalTexture.d.ts +1 -1
- package/core/texture/externalTexture.js +3 -1
- package/core/texture/texture.d.ts +1 -1
- package/core/texture/texture.js +12 -5
- package/core/unroll/tgpuUnroll.d.ts +4 -3
- package/core/unroll/tgpuUnroll.js +7 -1
- package/core/valueProxyUtils.d.ts +4 -2
- package/core/valueProxyUtils.js +4 -2
- package/data/autoStruct.d.ts +1 -1
- package/data/autoStruct.js +8 -4
- package/data/compiledIO.js +12 -9
- package/data/dataIO.d.ts +10 -6
- package/data/dataIO.js +19 -4
- package/data/dataTypes.d.ts +0 -5
- package/data/dataTypes.js +0 -6
- package/data/deepEqual.js +19 -9
- package/data/matrix.js +10 -3
- package/data/matrixColumnsAccess.d.ts +8 -0
- package/data/matrixColumnsAccess.js +15 -0
- package/data/offsetUtils.js +181 -102
- package/data/partialIO.d.ts +8 -1
- package/data/partialIO.js +36 -19
- package/data/ref.d.ts +12 -5
- package/data/ref.js +51 -24
- package/data/snippet.d.ts +13 -4
- package/data/snippet.js +17 -4
- package/data/struct.js +3 -0
- package/data/wgslTypes.d.ts +6 -3
- package/internal.d.ts +1 -0
- package/nameUtils.d.ts +7 -0
- package/nameUtils.js +60 -0
- package/package.json +1 -1
- package/resolutionCtx.js +1 -1
- package/shared/meta.js +1 -1
- package/shared/symbols.d.ts +5 -0
- package/shared/symbols.js +6 -1
- package/shared/tseynit.js +54 -50
- package/std/boolean.js +9 -10
- package/std/numeric.js +1 -4
- package/std/operators.js +9 -4
- package/std/packing.js +10 -10
- package/std/texture.d.ts +2 -2
- package/tgpuLogger.d.ts +1 -1
- package/tgpuLogger.js +0 -2
- package/tgsl/accessIndex.js +37 -38
- package/tgsl/accessProp.js +10 -7
- package/tgsl/conversion.js +3 -2
- package/tgsl/forOfUtils.js +8 -8
- package/tgsl/generationHelpers.d.ts +4 -3
- package/tgsl/generationHelpers.js +18 -5
- package/tgsl/infixDispatch.d.ts +4 -1
- package/tgsl/infixDispatch.js +13 -1
- package/tgsl/makeDereferenceable.d.ts +1 -1
- package/tgsl/makeDereferenceable.js +3 -1
- package/tgsl/shellless.js +5 -9
- package/tgsl/stringifySnippet.d.ts +9 -0
- package/tgsl/stringifySnippet.js +24 -0
- package/tgsl/wgslGenerator.d.ts +1 -0
- package/tgsl/wgslGenerator.js +180 -86
- package/types.d.ts +23 -2
- package/types.js +30 -5
package/common/writeSoA.js
CHANGED
|
@@ -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
|
}
|
package/core/buffer/buffer.js
CHANGED
|
@@ -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(
|
|
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:
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
302
|
+
commandEncoder.copyBufferToBuffer(gpuBuffer, 0, stagingBuffer, 0, size);
|
|
267
303
|
this[$soul].device.queue.submit([commandEncoder.finish()]);
|
|
268
|
-
await stagingBuffer.mapAsync(GPUMapMode.READ, 0,
|
|
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)
|
|
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()
|
|
16
|
+
return defaultAttachmentViews.getOrMake(view.getCurrentTexture());
|
|
14
17
|
}
|
|
15
18
|
return view;
|
|
16
19
|
}
|
package/core/function/autoIO.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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((
|
|
44
|
-
.map((s) => `'${s
|
|
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(...
|
|
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 {
|
|
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
|
-
|
|
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(...
|
|
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 {
|
|
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.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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>;
|
package/core/function/tgpuFn.js
CHANGED
|
@@ -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,
|
|
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(
|
|
4
|
+
export declare function connectAttachmentToShader(connectedTargets: (ConnectedTarget | null)[] | undefined, attachment: AnyFragmentColorAttachment): (ColorAttachment | null)[];
|