Skip to content

Commit a3ed827

Browse files
fix: Pointers for reference types (#1591)
1 parent 19e234d commit a3ed827

File tree

10 files changed

+236
-68
lines changed

10 files changed

+236
-68
lines changed

apps/typegpu-docs/src/content/docs/fundamentals/data-schemas.mdx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,6 @@ To define arrays of known constant length, use the `d.arrayOf` function with the
321321

322322
```ts twoslash
323323
import * as d from 'typegpu/data';
324-
325324
// ---cut---
326325
const MyArray = d.arrayOf(d.f32, 4);
327326

@@ -344,6 +343,57 @@ const array = ArrayPartialSchema(2)([1.2, 19.29]);
344343
// ^?
345344
```
346345

346+
## Atomics
347+
348+
To create a schema corresponding to an atomic data type, wrap `d.i32` or `d.u32` with `d.atomic`.
349+
350+
```ts twoslash
351+
import * as d from 'typegpu/data';
352+
// ---cut---
353+
const AtomicI32 = d.atomic(d.i32);
354+
// ^?
355+
```
356+
357+
:::tip
358+
The `std` module provides functions for manipulating atomic data in TGSL.
359+
360+
```ts twoslash
361+
import tgpu from 'typegpu';
362+
import * as d from 'typegpu/data';
363+
import * as std from 'typegpu/std';
364+
// ---cut---
365+
const count = tgpu['~unstable'].workgroupVar(d.atomic(d.u32));
366+
367+
const incrementAndGet = tgpu.fn([], d.u32)(() => {
368+
return std.atomicAdd(count.$, 1);
369+
});
370+
```
371+
372+
:::
373+
374+
## Pointers
375+
376+
To create a schema corresponding to a pointer data type, wrap the inner schema with a pointer constructor corresponding to the appropriate address space.
377+
378+
For address spaces `function`, `private`, `workgroup`, `storage` and `uniform`, use `d.ptrFn`, `d.ptrPrivate`, `d.ptrWorkgroup`, `d.ptrStorage` and `d.ptrUniform` respectively.
379+
380+
```ts twoslash
381+
import * as d from 'typegpu/data';
382+
// ---cut---
383+
const vecPtrFn = d.ptrFn(d.vec3f);
384+
// ^?
385+
386+
const vecPtrPrivate = d.ptrPrivate(d.vec2f);
387+
// ^?
388+
```
389+
390+
:::note
391+
In order to use `workgroup`, `storage` and `uniform` pointers as function parameters,
392+
WGSL needs the `unrestricted_pointer_parameters` extension.
393+
394+
This extension is enabled automatically when available.
395+
:::
396+
347397
## Copy constructors
348398

349399
One of the biggest differences between JavaScript and WGSL is the existence of value/reference types.

apps/typegpu-docs/src/content/docs/fundamentals/tgsl.mdx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ For the TGSL functions to work, you need to use the dedicated build plugin -- [u
1818
## Usage
1919

2020
Instead of using a WGSL code string, you can pass TGSL to the tgpu function shell as an argument instead.
21-
Functions from the WGSL standard library (*distance, arrayLength, workgroupBarrier, [etc.](https://github.com/software-mansion/TypeGPU/blob/release/packages/typegpu/src/std/index.ts)*) are accessible through the `typegpu/std` endpoint.
21+
Functions from the WGSL standard library (*distance, arrayLength, workgroupBarrier, [etc.](https://github.com/software-mansion/TypeGPU/blob/release/packages/typegpu/src/std/index.ts)*) are accessible through the `typegpu/std` entrypoint.
2222
The package also includes functions for vector and matrix operators (*add, eq, lt...*).
2323

2424
```ts
@@ -103,21 +103,27 @@ const patternSolid = patternFn(() => {
103103
});
104104
```
105105

106-
TGSL-implemented functions can also be invoked on the CPU, as along as they do not use any GPU-exclusive functionalities, like buffers or textures (regardless of whether they are marked as *"kernel"* or not).
106+
## Executing TGSL functions in JS
107+
108+
TGSL-implemented functions can be invoked on the CPU,
109+
as along as they do not use any GPU-exclusive functionalities,
110+
like buffers or textures (regardless of whether they are marked as *"kernel"* or not).
111+
112+
Keep in mind that you cannot execute entry-point functions in JavaScript.
107113

108114
## What to keep in mind
109115

110116
* **TGSL limitations** --
111-
For a function to be valid TGSL, it must consist only of supported JS syntax (again, see [tinyest-for-wgsl repository](https://github.com/software-mansion/TypeGPU/blob/release/packages/tinyest-for-wgsl/src/parsers.ts)), possibly including references to bound buffers, constant variables defined outside of the function, other TGSL functions etc.
117+
For a function to be valid TGSL, it must consist only of supported JS syntax (again, see [tinyest-for-wgsl repository](https://github.com/software-mansion/TypeGPU/blob/release/packages/tinyest-for-wgsl/src/parsers.ts)), possibly including references to bound buffers, constant variables defined outside the function, other TGSL functions etc.
112118
This means that, for example, `console.log()` calls will not work on the GPU.
113119

114120
* **Differences between JS on the CPU and GPU** --
115-
TGSL is developed to work on the GPU the same as on the CPU as much as possible,
121+
TGSL is developed to work on the GPU the same as on the CPU as much as possible,
116122
however because of the fundamental differences between the JavaScript and WGSL languages, it is not guaranteed to always be the case.
117123

118-
Currently the biggest known difference is that vectors, matrices and structs are treated as reference types in JavaScript and value types in WGSL.
119-
That is, on the WGSL side, the assignment operator copies the value instead of the reference, and two different vectors can be equal to each other if only they store the same values, unlike in JS, where they need to point to the same reference.
120-
To somehow alleviate this issue, when passing arguments to tgpu functions on JS side, we perform a deep copy of them (note that in WGSL arguments are immutable by default).
124+
Currently, the biggest known difference is that vectors, matrices and structs are treated as reference types in JavaScript and value types in WGSL.
125+
That is, on the WGSL side, the assignment operator copies the value instead of the reference, and two different vectors can be equal to each other if only they store the same values, unlike in JS, where they need to point to the same reference.
126+
To somehow alleviate this issue, when passing arguments to tgpu functions on JS side, we perform a deep copy of them (note that in WGSL arguments are immutable by default).
121127

122128
When using TGSL on the GPU, the behavior is that of WGSL, not JS, as one would expect.
123129
Therefore some WGSL knowledge is still required, even when opting out for TGSL.
@@ -146,15 +152,32 @@ const fragmentWgsl = tgpu['~unstable'].fragmentFn({ out: d.vec4f })`{
146152
```
147153

148154
* **Operators** --
149-
JavaScript does not support operator overloading.
150-
This means that, while you can still use operators for numbers,
155+
JavaScript does not support operator overloading.
156+
This means that, while you can still use operators for numbers,
151157
you have to use supplementary functions from `typegpu/std` (*add, mul, eq, lt, ge...*) for operations involving vectors and matrices.
152158

159+
* **Pointers** --
160+
Since WGSL pointers can only point to a narrow set of items called Storable Types,
161+
TypeGPU tries to dereference pointers used in TGSL automatically when it seems appropriate.
162+
For example, function arguments that are pointers to reference types are passed just as the pointed object.
163+
Pointers to primitive types (numbers and booleans) are currently not supported.
164+
165+
```ts twoslash
166+
import tgpu from 'typegpu';
167+
import * as d from 'typegpu/data';
168+
// ---cut---
169+
const fn = tgpu.fn([d.ptrFn(d.vec3f)], d.vec3f)((ptr) => {
170+
ptr.x += 1;
171+
//^?
172+
return ptr;
173+
});
174+
```
175+
153176
* **When to use TGSL instead of WGSL** --
154-
Writing the code using TGSL has a few significant advantages.
177+
Writing the code using TGSL has a few significant advantages.
155178
It allows defining utils only once and using them both as a kernel and host functions,
156179
as well as enables complete syntax highlighting and autocomplete in TypeGPU function definitions, leading to a better developer UX.
157-
However, it sometimes might be better to choose WGSL for certain functions.
180+
However, it sometimes might be better to choose WGSL for certain functions.
158181
Since JavaScript doesn't support operator overloading, functions including complex matrix operations can be more readable in WGSL.
159182
Writing WGSL becomes a necessity whenever TGSL does not support some feature or standard library function quite yet.
160183
Luckily, you don't have to choose one or the other for the entire project. It is possible to mix and match WGSL and TGSL at every step of the way.

apps/typegpu-docs/src/content/examples/tests/tgsl-parsing-test/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { arrayAndStructConstructorsTest } from './array-and-struct-constructors.
44
import { infixOperatorsTests } from './infix-operators.ts';
55
import { logicalExpressionTests } from './logical-expressions.ts';
66
import { matrixOpsTests } from './matrix-ops.ts';
7+
import { pointersTest } from './pointers.ts';
78

89
const root = await tgpu.init();
910
const result = root.createMutable(d.i32, 0);
@@ -15,6 +16,7 @@ const computeRunTests = tgpu['~unstable']
1516
s = s && matrixOpsTests();
1617
s = s && infixOperatorsTests();
1718
s = s && arrayAndStructConstructorsTest();
19+
s = s && pointersTest();
1820

1921
if (s) {
2022
result.value = 1;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import tgpu from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
import * as std from 'typegpu/std';
4+
5+
const SimpleStruct = d.struct({ vec: d.vec2f });
6+
7+
const modifyNumFn = tgpu.fn([d.ptrFn(d.u32)])((ptr) => {
8+
// biome-ignore lint/style/noParameterAssign: it's just a test
9+
ptr += 1;
10+
});
11+
12+
const modifyVecFn = tgpu.fn([d.ptrFn(d.vec2f)])((ptr) => {
13+
ptr.x += 1;
14+
});
15+
16+
const modifyStructFn = tgpu.fn([d.ptrFn(SimpleStruct)])((ptr) => {
17+
ptr.vec.x += 1;
18+
});
19+
20+
const privateNum = tgpu['~unstable'].privateVar(d.u32);
21+
const modifyNumPrivate = tgpu.fn([d.ptrPrivate(d.u32)])((ptr) => {
22+
// biome-ignore lint/style/noParameterAssign: it's just a test
23+
ptr += 1;
24+
});
25+
26+
const privateVec = tgpu['~unstable'].privateVar(d.vec2f);
27+
const modifyVecPrivate = tgpu.fn([d.ptrPrivate(d.vec2f)])((ptr) => {
28+
ptr.x += 1;
29+
});
30+
31+
const privateStruct = tgpu['~unstable'].privateVar(SimpleStruct);
32+
const modifyStructPrivate = tgpu.fn([d.ptrPrivate(SimpleStruct)])((ptr) => {
33+
ptr.vec.x += 1;
34+
});
35+
36+
// TODO: replace `s = s &&` with `s &&=` when implemented
37+
export const pointersTest = tgpu.fn([], d.bool)(() => {
38+
let s = true;
39+
40+
// function pointers
41+
const num = d.u32();
42+
modifyNumFn(num);
43+
s = s && (num === 1);
44+
45+
const vec = d.vec2f();
46+
modifyVecFn(vec);
47+
s = s && std.allEq(vec, d.vec2f(1, 0));
48+
49+
const myStruct = SimpleStruct();
50+
modifyStructFn(myStruct);
51+
s = s && std.allEq(myStruct.vec, d.vec2f(1, 0));
52+
53+
// private pointers
54+
modifyNumPrivate(privateNum.$);
55+
s = s && (privateNum.$ === 1);
56+
57+
modifyVecPrivate(privateVec.$);
58+
s = s && std.allEq(privateVec.$, d.vec2f(1, 0));
59+
60+
modifyStructPrivate(privateStruct.$);
61+
s = s && std.allEq(privateStruct.$.vec, d.vec2f(1, 0));
62+
63+
return s;
64+
});

packages/typegpu/src/data/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export type {
7373
Mat4x4f,
7474
Ptr,
7575
Size,
76+
StorableData,
7677
U16,
7778
U32,
7879
v2b,

packages/typegpu/src/data/ptr.ts

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,57 @@
11
import { $internal } from '../shared/symbols.ts';
2-
import type { AnyData } from './dataTypes.ts';
3-
import type { Ptr } from './wgslTypes.ts';
2+
import type { Access, AddressSpace, Ptr, StorableData } from './wgslTypes.ts';
43

5-
export function ptrFn<T extends AnyData>(
4+
export function ptrFn<T extends StorableData>(
65
inner: T,
76
): Ptr<'function', T, 'read-write'> {
8-
return {
9-
[$internal]: true,
10-
type: 'ptr',
11-
inner,
12-
addressSpace: 'function',
13-
access: 'read-write',
14-
} as Ptr<'function', T, 'read-write'>;
7+
return INTERNAL_createPtr('function', inner, 'read-write');
158
}
169

17-
export function ptrPrivate<T extends AnyData>(
10+
export function ptrPrivate<T extends StorableData>(
1811
inner: T,
1912
): Ptr<'private', T, 'read-write'> {
20-
return {
21-
[$internal]: true,
22-
type: 'ptr',
23-
inner,
24-
addressSpace: 'private',
25-
access: 'read-write',
26-
} as Ptr<'private', T, 'read-write'>;
13+
return INTERNAL_createPtr('private', inner, 'read-write');
2714
}
2815

29-
export function ptrWorkgroup<T extends AnyData>(
16+
export function ptrWorkgroup<T extends StorableData>(
3017
inner: T,
3118
): Ptr<'workgroup', T, 'read-write'> {
32-
return {
33-
[$internal]: true,
34-
type: 'ptr',
35-
inner,
36-
addressSpace: 'workgroup',
37-
access: 'read-write',
38-
} as Ptr<'workgroup', T, 'read-write'>;
19+
return INTERNAL_createPtr('workgroup', inner, 'read-write');
3920
}
4021

4122
export function ptrStorage<
42-
T extends AnyData,
23+
T extends StorableData,
4324
TAccess extends 'read' | 'read-write' = 'read',
4425
>(inner: T, access: TAccess = 'read' as TAccess): Ptr<'storage', T, TAccess> {
45-
return {
46-
[$internal]: true,
47-
type: 'ptr',
48-
inner,
49-
addressSpace: 'storage',
50-
access,
51-
} as Ptr<'storage', T, TAccess>;
26+
return INTERNAL_createPtr('storage', inner, access);
5227
}
5328

54-
export function ptrUniform<T extends AnyData>(
29+
export function ptrUniform<T extends StorableData>(
5530
inner: T,
5631
): Ptr<'uniform', T, 'read'> {
57-
return {
58-
[$internal]: true,
59-
type: 'ptr',
60-
inner,
61-
addressSpace: 'uniform',
62-
access: 'read',
63-
} as Ptr<'uniform', T, 'read'>;
32+
return INTERNAL_createPtr('uniform', inner, 'read');
6433
}
6534

66-
export function ptrHandle<T extends AnyData>(
35+
export function ptrHandle<T extends StorableData>(
6736
inner: T,
6837
): Ptr<'handle', T, 'read'> {
38+
return INTERNAL_createPtr('handle', inner, 'read');
39+
}
40+
41+
function INTERNAL_createPtr<
42+
TAddressSpace extends AddressSpace,
43+
TInner extends StorableData,
44+
TAccess extends Access,
45+
>(
46+
addressSpace: TAddressSpace,
47+
inner: TInner,
48+
access: TAccess,
49+
): Ptr<TAddressSpace, TInner, TAccess> {
6950
return {
7051
[$internal]: true,
7152
type: 'ptr',
53+
addressSpace,
7254
inner,
73-
addressSpace: 'handle',
74-
access: 'read',
75-
} as Ptr<'handle', T, 'read'>;
55+
access,
56+
} as Ptr<TAddressSpace, TInner, TAccess>;
7657
}

packages/typegpu/src/data/schemaCallWrapper.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import type { AnyData } from './dataTypes.ts';
22
import { formatToWGSLType } from './vertexFormatData.ts';
33

44
/**
5-
* A wrapper for `schema(item)` or `schema()` call.
6-
* If the schema is a TgpuVertexFormatData, it instead calls the corresponding constructible schema.
7-
* Throws an error if the schema is not callable.
5+
* A wrapper for `schema(item)` or `schema()` call on JS side.
6+
* If the schema is a pointer, returns the value pointed to without copying.
7+
* If the schema is a TgpuVertexFormatData, calls the corresponding constructible schema instead.
8+
* If the schema is not callable, throws an error.
9+
* Otherwise, returns `schema(item)` or `schema()`.
810
*/
911
export function schemaCallWrapper<T>(schema: AnyData, item?: T): T {
1012
const maybeType = (schema as { type: string })?.type;

0 commit comments

Comments
 (0)