Skip to content

Commit 77ca87f

Browse files
authored
refactor: Better origin tracking (#2490)
1 parent 966d342 commit 77ca87f

27 files changed

Lines changed: 699 additions & 376 deletions

apps/typegpu-docs/src/content/docs/advanced/shader-generation.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,10 @@ There are essentially three types of origins:
197197
performing operations that produce new values. Examples include creating a new `Boid` instance or calculating a new position based on an existing one. These
198198
include `'runtime'` and `'constant'`.
199199
- **Referential Origins**: These origins represent references to existing values. They are typically used for accessing or modifying existing data. Examples
200-
include accessing the position of an existing `Boid` instance or modifying the position of an existing `Boid` instance. These include `'uniform'`, `'mutable'`, `'readonly'`, `'workgroup'`, `'private'`, `'function'`, `'handle'`, `'constant-ref'` and `'this-function'`.
200+
include accessing the position of an existing `Boid` instance or modifying the position of an existing `Boid` instance:
201201
- `'uniform'`, `'mutable'`, `'readonly'`, `'workgroup'`, `'private'`, `'function'`, `'handle'` all reflect address spaces that values can belong to, and
202202
we use them to determine what kind of pointer type they are.
203-
- `'constant-tgpu-const-ref'` is a reference to a value stored in a `tgpu.const`. They're different from `constant`s, as we know that even if they're referential (non-primitive), the developer cannot mutate them.
204-
- `'runtime-tgpu-const-ref'` is a reference to a value stored in a `tgpu.const` but accessed in a way that isn’t constant at shader‑creation time (for example, indexed using a runtime value).
205-
- `'this-function'` lets us track whether values originates from the function we're currently generating, or the function that called us.
203+
- `'constant-immutable-def'` is a reference to a value stored in a `tgpu.const`. They're different from `constant`s, as we know that even if they're referential (non-primitive), the developer cannot mutate them.
204+
- `'runtime-immutable-def'` is a reference to a value stored in a `tgpu.const` but accessed in a way that isn’t constant at shader‑creation time (for example, indexed using a runtime value).
205+
- `'local-def'` lets us track whether values originate from the function we're currently generating.
206206
- **Argument Origins**: This group is dedicated to exactly one origin: 'argument'. It represents values that are passed as arguments to functions.

apps/typegpu-docs/src/examples/tests/wgsl-resolution/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const mainCompute = tgpu
9999
})((input) => {
100100
const index = input.gid.x;
101101
const currentTrianglePos = computeBindGroupLayout.$.currentTrianglePos;
102-
const instanceInfo = currentTrianglePos[index];
102+
const instanceInfo = TriangleData(currentTrianglePos[index]);
103103
let separation = d.vec2f();
104104
let alignment = d.vec2f();
105105
let cohesion = d.vec2f();

apps/typegpu-docs/tests/individual-example-tests/wgsl-resolution.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe('wgsl resolution example', () => {
7878
@compute @workgroup_size(1) fn compute_shader(@builtin(global_invocation_id) gid: vec3u) {
7979
let index = gid.x;
8080
let currentTrianglePos = (&currentTrianglePos_1);
81-
let instanceInfo = (&(*currentTrianglePos)[index]);
81+
var instanceInfo = (*currentTrianglePos)[index];
8282
var separation = vec2f();
8383
var alignment = vec2f();
8484
var cohesion = vec2f();
@@ -89,9 +89,9 @@ describe('wgsl resolution example', () => {
8989
continue;
9090
}
9191
let other = (&(*currentTrianglePos)[i]);
92-
let dist = distance((*instanceInfo).position, (*other).position);
92+
let dist = distance(instanceInfo.position, (*other).position);
9393
if ((dist < paramsBuffer.separationDistance)) {
94-
separation = (separation + ((*instanceInfo).position - (*other).position));
94+
separation = (separation + (instanceInfo.position - (*other).position));
9595
}
9696
if ((dist < paramsBuffer.alignmentDistance)) {
9797
alignment = (alignment + (*other).velocity);
@@ -107,27 +107,27 @@ describe('wgsl resolution example', () => {
107107
}
108108
if ((cohesionCount > 0i)) {
109109
cohesion = ((1f / f32(cohesionCount)) * cohesion);
110-
cohesion = (cohesion - (*instanceInfo).position);
110+
cohesion = (cohesion - instanceInfo.position);
111111
}
112112
var velocity = (paramsBuffer.separationStrength * separation);
113113
velocity = (velocity + (paramsBuffer.alignmentStrength * alignment));
114114
velocity = (velocity + (paramsBuffer.cohesionStrength * cohesion));
115-
(*instanceInfo).velocity = ((*instanceInfo).velocity + velocity);
116-
(*instanceInfo).velocity = (clamp(length((*instanceInfo).velocity), 0f, 0.01f) * normalize((*instanceInfo).velocity));
117-
if (((*instanceInfo).position.x > 1.03f)) {
118-
(*instanceInfo).position.x = -1.03f;
115+
instanceInfo.velocity = (instanceInfo.velocity + velocity);
116+
instanceInfo.velocity = (clamp(length(instanceInfo.velocity), 0f, 0.01f) * normalize(instanceInfo.velocity));
117+
if ((instanceInfo.position.x > 1.03f)) {
118+
instanceInfo.position.x = -1.03f;
119119
}
120-
if (((*instanceInfo).position.y > 1.03f)) {
121-
(*instanceInfo).position.y = -1.03f;
120+
if ((instanceInfo.position.y > 1.03f)) {
121+
instanceInfo.position.y = -1.03f;
122122
}
123-
if (((*instanceInfo).position.x < -1.03f)) {
124-
(*instanceInfo).position.x = 1.03f;
123+
if ((instanceInfo.position.x < -1.03f)) {
124+
instanceInfo.position.x = 1.03f;
125125
}
126-
if (((*instanceInfo).position.y < -1.03f)) {
127-
(*instanceInfo).position.y = 1.03f;
126+
if ((instanceInfo.position.y < -1.03f)) {
127+
instanceInfo.position.y = 1.03f;
128128
}
129-
(*instanceInfo).position = ((*instanceInfo).position + (*instanceInfo).velocity);
130-
nextTrianglePos[index] = (*instanceInfo);
129+
instanceInfo.position = (instanceInfo.position + instanceInfo.velocity);
130+
nextTrianglePos[index] = instanceInfo;
131131
}"
132132
`);
133133
});

packages/typegpu/src/core/buffer/bufferUsage.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { schemaCallWrapper } from '../../data/schemaCallWrapper.ts';
22
import { type ResolvedSnippet, snip } from '../../data/snippet.ts';
3-
import { type AnyWgslData, type BaseData, isNaturallyEphemeral } from '../../data/wgslTypes.ts';
3+
import { type AnyWgslData, type BaseData } from '../../data/wgslTypes.ts';
44
import { IllegalBufferAccessError } from '../../errors.ts';
55
import { getExecMode, inCodegenMode, isInsideTgpuFn } from '../../execMode.ts';
66
import { isUsableAsStorage, type StorageFlag } from '../../extension.ts';
@@ -129,7 +129,7 @@ class TgpuFixedBufferImpl<TData extends BaseData, TUsage extends BindableBufferU
129129
`@group(${group}) @binding(${binding}) var<${usage}> ${id}: ${ctx.resolve(dataType).value};`,
130130
);
131131

132-
return snip(id, dataType, isNaturallyEphemeral(dataType) ? 'runtime' : this.usage);
132+
return snip(id, dataType, this.usage);
133133
}
134134

135135
toString(): string {
@@ -144,7 +144,7 @@ class TgpuFixedBufferImpl<TData extends BaseData, TUsage extends BindableBufferU
144144
{
145145
[$internal]: true,
146146
get [$ownSnippet]() {
147-
return snip(this, dataType, isNaturallyEphemeral(dataType) ? 'runtime' : usage);
147+
return snip(this, dataType, usage);
148148
},
149149
[$resolve]: (ctx) => ctx.resolve(this),
150150
toString: () => `${this.usage}:${getName(this) ?? '<unnamed>'}.$`,
@@ -251,7 +251,7 @@ export class TgpuLaidOutBufferImpl<TData extends BaseData, TUsage extends Bindab
251251
};`,
252252
);
253253

254-
return snip(id, this.dataType, isNaturallyEphemeral(this.dataType) ? 'runtime' : this.usage);
254+
return snip(id, this.dataType, this.usage);
255255
}
256256

257257
toString(): string {
@@ -266,7 +266,7 @@ export class TgpuLaidOutBufferImpl<TData extends BaseData, TUsage extends Bindab
266266
{
267267
[$internal]: true,
268268
get [$ownSnippet]() {
269-
return snip(this, schema, isNaturallyEphemeral(schema) ? 'runtime' : usage);
269+
return snip(this, schema, usage);
270270
},
271271
[$resolve]: (ctx) => ctx.resolve(this),
272272
toString: () => `${this.usage}:${getName(this) ?? '<unnamed>'}.$`,

packages/typegpu/src/core/constant/tgpuConstant.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { isData, type AnyData } from '../../data/dataTypes.ts';
22
import { type ResolvedSnippet, snip } from '../../data/snippet.ts';
3-
import {
4-
type AnyWgslData,
5-
type BaseData,
6-
isNaturallyEphemeral,
7-
type WgslArray,
8-
} from '../../data/wgslTypes.ts';
3+
import { type AnyWgslData, type BaseData, type WgslArray } from '../../data/wgslTypes.ts';
94
import { inCodegenMode } from '../../execMode.ts';
105
import type { TgpuNamable } from '../../shared/meta.ts';
116
import { getName, setName } from '../../shared/meta.ts';
@@ -119,11 +114,7 @@ class TgpuConstImpl<TDataType extends BaseData> implements TgpuConst<TDataType>,
119114

120115
ctx.addDeclaration(`const ${id}: ${resolvedDataType} = ${resolvedValue};`);
121116

122-
return snip(
123-
id,
124-
this.dataType,
125-
isNaturallyEphemeral(this.dataType) ? 'constant' : 'constant-tgpu-const-ref',
126-
);
117+
return snip(id, this.dataType, 'constant-immutable-def');
127118
}
128119

129120
toString() {
@@ -137,11 +128,7 @@ class TgpuConstImpl<TDataType extends BaseData> implements TgpuConst<TDataType>,
137128
{
138129
[$internal]: true,
139130
get [$ownSnippet]() {
140-
return snip(
141-
this,
142-
dataType,
143-
isNaturallyEphemeral(dataType) ? 'constant' : 'constant-tgpu-const-ref',
144-
);
131+
return snip(this, dataType, 'constant-immutable-def');
145132
},
146133
[$resolve]: (ctx) => ctx.resolve(this),
147134
toString: () => `const:${getName(this) ?? '<unnamed>'}.$`,

packages/typegpu/src/core/rawCodeSnippet/tgpuRawCodeSnippet.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ export interface TgpuRawCodeSnippet<TDataType extends BaseData> {
2525
}
2626

2727
// The origin 'function' refers to values passed in from the calling scope, which means
28-
// we would have access to this value anyway. Same goes for 'argument' and 'this-function',
28+
// we would have access to this value anyway. Same goes for 'argument' and 'local-def',
2929
// the values literally exist in the function we're writing.
3030
//
31-
// 'constant-ref' was excluded because it's a special origin reserved for tgpu.const values.
31+
// '*-immutable-def' were excluded because they're a special origin reserved for tgpu.const values.
3232
export type RawCodeSnippetOrigin = Exclude<
3333
Origin,
34-
'function' | 'this-function' | 'argument' | 'constant-ref'
34+
'function' | 'local-def' | 'argument' | 'constant-immutable-def' | 'runtime-immutable-def'
3535
>;
3636

3737
/**

packages/typegpu/src/core/variable/tgpuVariable.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AnyData } from '../../data/dataTypes.ts';
22
import { type ResolvedSnippet, snip } from '../../data/snippet.ts';
3-
import { type BaseData, isNaturallyEphemeral } from '../../data/wgslTypes.ts';
3+
import type { BaseData } from '../../data/wgslTypes.ts';
44
import { IllegalVarAccessError } from '../../errors.ts';
55
import { getExecMode, isInsideTgpuFn } from '../../execMode.ts';
66
import type { TgpuNamable } from '../../shared/meta.ts';
@@ -96,7 +96,7 @@ class TgpuVarImpl<TScope extends VariableScope, TDataType extends BaseData>
9696
ctx.addDeclaration(`${pre};`);
9797
}
9898

99-
return snip(id, this.#dataType, isNaturallyEphemeral(this.#dataType) ? 'runtime' : this.#scope);
99+
return snip(id, this.#dataType, this.#scope);
100100
}
101101

102102
$name(label: string) {
@@ -110,7 +110,7 @@ class TgpuVarImpl<TScope extends VariableScope, TDataType extends BaseData>
110110

111111
get [$gpuValueOf](): InferGPU<TDataType> {
112112
const dataType = this.#dataType;
113-
const origin = isNaturallyEphemeral(dataType) ? 'runtime' : this.#scope;
113+
const origin = this.#scope;
114114

115115
return new Proxy(
116116
{

packages/typegpu/src/data/ref.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { $gpuCallable, $internal, $ownSnippet, $resolve } from '../shared/symbol
55
import type { DualFn, SelfResolvable } from '../types.ts';
66
import { UnknownData } from './dataTypes.ts';
77
import { createPtrFromOrigin, explicitFrom } from './ptr.ts';
8-
import { type ResolvedSnippet, snip, type Snippet } from './snippet.ts';
8+
import { isAlias, type ResolvedSnippet, snip, type Snippet } from './snippet.ts';
99
import { isNaturallyEphemeral, isPtr, type Ptr, type StorableData } from './wgslTypes.ts';
1010

1111
// ----------
@@ -47,10 +47,30 @@ export const _ref = (() => {
4747
impl.toString = () => 'ref';
4848
impl[$internal] = true;
4949
impl[$gpuCallable] = {
50-
call(_ctx, [value]) {
50+
call(ctx, [value]) {
5151
if (value.origin === 'argument') {
5252
throw new WgslTypeError(
53-
stitch`d.ref(${value}) is illegal, cannot take a reference of an argument. Copy the value locally first, and take a reference of the copy.`,
53+
stitch`d.ref(${value}) is illegal, cannot take a reference of an argument. Copy the value first, and take a reference of the copy.`,
54+
);
55+
}
56+
57+
if (value.origin === 'constant-immutable-def' || value.origin === 'runtime-immutable-def') {
58+
const typeStr = ctx.resolve(value.dataType).value;
59+
throw new WgslTypeError(
60+
stitch`d.ref(${value}) is illegal, cannot take a reference to a constant.
61+
-----
62+
- Try 'd.ref(${typeStr}(${value}));' instead to create a new referencable value.
63+
-----`,
64+
);
65+
}
66+
67+
if (isAlias(value) && isNaturallyEphemeral(value.dataType)) {
68+
const typeStr = ctx.resolve(value.dataType).value;
69+
throw new WgslTypeError(
70+
stitch`d.ref(${value}) is illegal, cannot take a reference to a scalar value.
71+
-----
72+
- Try 'd.ref(${typeStr}(${value}));' instead to create a new referencable scalar.
73+
-----`,
5474
);
5575
}
5676

@@ -172,11 +192,10 @@ export function derefSnippet(snippet: Snippet): Snippet {
172192
}
173193

174194
const innerType = snippet.dataType.inner;
175-
const origin = isNaturallyEphemeral(innerType) ? 'runtime' : snippet.origin;
176195

177196
if (snippet.value instanceof RefOperator) {
178-
return snip(stitch`${snippet.value.snippet}`, innerType, origin);
197+
return snip(stitch`${snippet.value.snippet}`, innerType, snippet.origin);
179198
}
180199

181-
return snip(stitch`(*${snippet})`, innerType, origin);
200+
return snip(stitch`(*${snippet})`, innerType, snippet.origin);
182201
}

packages/typegpu/src/data/snippet.ts

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,34 @@ import { DEV } from '../shared/env.ts';
44
import { type BaseData, isNumericSchema } from './wgslTypes.ts';
55

66
export type Origin =
7-
| 'uniform'
8-
| 'readonly' // equivalent to ptr<storage, ..., read>
9-
| 'mutable' // equivalent to ptr<storage, ..., read-write>
10-
| 'workgroup'
11-
| 'private'
12-
| 'function'
13-
| 'this-function'
14-
| 'handle'
15-
// is an argument (or part of an argument) given to the
16-
// function we're resolving. This includes primitives, to
17-
// catch cases where we update an argument's primitive member
18-
// prop, e.g.: `vec.x += 1;`
7+
// --- ADDRESS SPACE ORIGINS
8+
| 'uniform' /* defined in the 'uniform' address space */
9+
| 'readonly' /* defined in the 'storage' address space, with 'read' access */
10+
| 'mutable' /* defined in the 'storage' address space, with 'read-write' access */
11+
| 'workgroup' /* defined in the 'workgroup' address space */
12+
| 'private' /* defined in the 'private' address space */
13+
| 'handle' /* defined in the 'handle' address space */
14+
| 'function' /* defined in a callee, passed down to us as an argument ('function' address space) */
15+
// --- DEFINITIONS
16+
// defined in the current function
17+
| 'local-def'
18+
// A reference to a deeply immutable definition, recognized by WGSL as a 'constant'.
19+
// This is the usual case, read about 'runtime-immutable-def' to know when this doesn't apply.
20+
// A reference to a tgpu.const().$ value (which is frozen) fits into this category.
21+
| 'constant-immutable-def'
22+
// A reference to a deeply immutable definition, NOT recognized by WGSL as a 'constant'.
23+
// This can happen if say a constant is accessed with a runtime-known index. WGSL doesn't treat it like
24+
// a 'constant' anymore, but it's still a frozen value in JS, so we must treat it like it's immutable.
25+
| 'runtime-immutable-def'
26+
// ---------
27+
// non-pointer function arguments (or part of an argument).
1928
| 'argument'
20-
// not a ref to anything, known at runtime
29+
// not a reference to anything, known at runtime
2130
| 'runtime'
22-
// not a ref to anything, known at pipeline creation time
23-
// (not to be confused with 'comptime')
24-
// note that this doesn't automatically mean the value can be stored in a `const`
25-
// variable, more so that it's valid to do so in WGSL (but not necessarily safe to do in JS shaders)
26-
| 'constant'
27-
// don't even get me started on these. They're references to non-primitive values that originate
28-
// from a tgpu.const(...).$ call.
29-
| 'constant-tgpu-const-ref' /* turns into a `const` when assigned to a variable */
30-
| 'runtime-tgpu-const-ref' /* turns into a `let` when assigned to a variable */;
31-
32-
export function isEphemeralOrigin(space: Origin) {
33-
return space === 'runtime' || space === 'constant' || space === 'argument';
34-
}
31+
// an ephemeral value that is a valid WGSL 'constant' (not to be confused with 'comptime')
32+
// doesn't always lead to creating a `const` variable, as we cannot always guarantee that
33+
// the value won't be mutated in JS
34+
| 'constant';
3535

3636
/**
3737
* What happens to a snippet's origin when it's deep copied in JS, and left as is in WGSL?
@@ -49,8 +49,27 @@ export function fallthroughCopyOrigin(origin: Origin): Origin {
4949
return 'runtime';
5050
}
5151

52-
export function isEphemeralSnippet(snippet: Snippet) {
53-
return isEphemeralOrigin(snippet.origin);
52+
/**
53+
* Whether a snippet aliases a value that lives outside the current expression.
54+
*
55+
* @example
56+
* ```ts
57+
* function foo(a: number) {
58+
* const color = d.vec3f(1, 2, 3);
59+
* return color * a;
60+
* }
61+
*
62+
* // References:
63+
* // - color
64+
* // - a
65+
* //
66+
* // Not references:
67+
* // - d.vec3f(1, 2, 3)
68+
* // - color * a
69+
* ```
70+
*/
71+
export function isAlias(snippet: Snippet) {
72+
return !(snippet.origin === 'runtime' || snippet.origin === 'constant');
5473
}
5574

5675
export const originToPtrParams = {
@@ -60,7 +79,8 @@ export const originToPtrParams = {
6079
workgroup: { space: 'workgroup', access: 'read-write' },
6180
private: { space: 'private', access: 'read-write' },
6281
function: { space: 'function', access: 'read-write' },
63-
'this-function': { space: 'function', access: 'read-write' },
82+
// Local declarations are also in the `function` address space
83+
'local-def': { space: 'function', access: 'read-write' },
6484
} as const;
6585
export type OriginToPtrParams = typeof originToPtrParams;
6686

0 commit comments

Comments
 (0)