Skip to content

Commit 34b6d60

Browse files
feat: Handle compatible Math functions in TypeGPU functions (#2152)
1 parent ce7d94d commit 34b6d60

6 files changed

Lines changed: 230 additions & 8 deletions

File tree

apps/typegpu-docs/src/content/docs/fundamentals/functions/index.mdx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -288,19 +288,22 @@ descriptive error either at build time or at runtime (when compiling the shader)
288288
Our aim with TypeGPU functions is not to allow arbitrary JavaScript to be supported in the context of shaders, **rather to allow for shaders to be written in JavaScript**. This distinction means we won't support every JavaScript feature, only those that make sense in the context of graphics programming.
289289
:::
290290

291-
* **Calling other functions** --
292-
Only functions marked with `'use gpu'` can be called from within a shader. An exception to that rule is [`console.log`](/TypeGPU/fundamentals/utils#consolelog), which allows for tracking runtime behavior
293-
of shaders in a familiar way.
294-
295291
* **Operators** --
296292
JavaScript does not support operator overloading.
297293
This means that, while you can still use operators for numbers,
298294
you have to use supplementary functions from `typegpu/std` (*add, mul, eq, lt, ge...*) for operations involving vectors and matrices, or use a fluent interface (*abc.mul(xyz), ...*)
299295

300-
* **Math.\*** --
301-
Utility functions on the `Math` object can't automatically run on the GPU, but can usually be swapped with functions exported from `typegpu/std`.
302-
Additionally, if you're able to pull the call to `Math.*` out of the function, you can store the result in a constant and use it in the function
303-
no problem.
296+
* **Calling other functions** --
297+
Only functions marked with `'use gpu'` can be called from within a shader. There are two exceptions to that rule: [`console.log`](/TypeGPU/fundamentals/utils#consolelog), which allows for tracking runtime behavior
298+
of shaders in a familiar way, as well as most `Math` functions.
299+
300+
:::caution
301+
Calling `Math` functions such as `Math.sin` inside TypeGPU functions should be avoided.
302+
- not all `Math` functions are supported,
303+
- using `Math` functions may lead to unexpected type conversions.
304+
305+
All supported `Math` functions fall back to their `std` equivalents, and it is advised to use `std` directly.
306+
:::
304307

305308
### Standard library
306309

packages/typegpu/src/std/numeric.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,13 @@ function cpuRound(value: number): number;
10791079
function cpuRound<T extends AnyFloatVecInstance>(value: T): T;
10801080
function cpuRound<T extends AnyFloatVecInstance | number>(value: T): T {
10811081
if (typeof value === 'number') {
1082+
const floor = Math.floor(value);
1083+
if (value === floor + 0.5) {
1084+
if (floor % 2 === 0) {
1085+
return floor as T;
1086+
}
1087+
return (floor + 1) as T;
1088+
}
10821089
return Math.round(value) as T;
10831090
}
10841091
throw new MissingCpuImplError(

packages/typegpu/src/tgsl/math.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { AnyFn } from '../core/function/fnTypes.ts';
2+
import { f32 } from '../data/numeric.ts';
3+
import {
4+
abs,
5+
acos,
6+
acosh,
7+
asin,
8+
asinh,
9+
atan,
10+
atan2,
11+
atanh,
12+
ceil,
13+
cos,
14+
cosh,
15+
countLeadingZeros,
16+
exp,
17+
floor,
18+
log,
19+
log2,
20+
max,
21+
min,
22+
pow,
23+
sign,
24+
sin,
25+
sinh,
26+
sqrt,
27+
tan,
28+
tanh,
29+
trunc,
30+
} from '../std/numeric.ts';
31+
import type { DualFn } from '../types.ts';
32+
33+
export const mathToStd: Record<string, DualFn<AnyFn> | undefined> = {
34+
// -- one to one Math to WGSL correlation --
35+
abs,
36+
acos,
37+
acosh,
38+
asin,
39+
asinh,
40+
atan,
41+
atan2,
42+
atanh,
43+
ceil,
44+
cos,
45+
cosh,
46+
exp,
47+
floor,
48+
fround: f32 as DualFn<AnyFn>,
49+
clz32: countLeadingZeros,
50+
trunc,
51+
log,
52+
log2,
53+
pow,
54+
sign,
55+
sin,
56+
sinh,
57+
sqrt,
58+
tan,
59+
tanh,
60+
// -- varying in Math and two arg in WGSL, but we support varying in std --
61+
max,
62+
min,
63+
// -- possible if we extend std --
64+
cbrt: undefined,
65+
log10: undefined,
66+
log1p: undefined,
67+
f16round: undefined,
68+
hypot: undefined,
69+
expm1: undefined,
70+
// -- skipped --
71+
random: undefined,
72+
imul: undefined,
73+
round: undefined, // round(2.5) is 3 in JS and 2 in WGSL
74+
} satisfies Partial<Record<keyof typeof Math, DualFn<AnyFn> | undefined>>;

packages/typegpu/src/tgsl/wgslGenerator.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { createPtrFromOrigin, implicitFrom, ptrFn } from '../data/ptr.ts';
4242
import { RefOperator } from '../data/ref.ts';
4343
import { constant } from '../core/constant/tgpuConstant.ts';
4444
import { AutoStruct } from '../data/autoStruct.ts';
45+
import { mathToStd } from './math.ts';
4546

4647
const { NodeTypeCatalog: NODE } = tinyest;
4748

@@ -456,6 +457,21 @@ ${this.ctx.pre}}`;
456457
);
457458
}
458459

460+
if (target.value === Math) {
461+
if (property in mathToStd && mathToStd[property]) {
462+
return snip(
463+
mathToStd[property],
464+
UnknownData,
465+
/* origin */ 'runtime',
466+
);
467+
}
468+
if (typeof Math[property as keyof typeof Math] === 'function') {
469+
throw new Error(
470+
`Unsupported functionality 'Math.${property}'. Use an std alternative, or implement the function manually.`,
471+
);
472+
}
473+
}
474+
459475
const accessed = accessProp(target, property);
460476
if (!accessed) {
461477
throw new Error(
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, expect, it } from 'vitest';
2+
import tgpu, { d } from '../src/index.ts';
3+
4+
describe('Math', () => {
5+
it('allows using Math.PI', () => {
6+
const myFn = () => {
7+
'use gpu';
8+
const a = Math.PI;
9+
};
10+
11+
expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(`
12+
"fn myFn() {
13+
const a = 3.141592653589793;
14+
}"
15+
`);
16+
});
17+
18+
it('allows using Math.sin', () => {
19+
const myFn = () => {
20+
'use gpu';
21+
const a = 0.5;
22+
const b = Math.sin(a);
23+
};
24+
25+
expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(`
26+
"fn myFn() {
27+
const a = 0.5;
28+
let b = sin(a);
29+
}"
30+
`);
31+
});
32+
33+
it('precomputes Math.sin when applicable', () => {
34+
const myFn = () => {
35+
'use gpu';
36+
const a = Math.sin(0.5);
37+
};
38+
39+
expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(`
40+
"fn myFn() {
41+
const a = 0.479425538604203;
42+
}"
43+
`);
44+
});
45+
46+
it('coerces Math.sin arguments', () => {
47+
const myFn = () => {
48+
'use gpu';
49+
const a = d.u32();
50+
const b = Math.sin(a);
51+
};
52+
53+
expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(`
54+
"fn myFn() {
55+
const a = 0u;
56+
let b = sin(f32(a));
57+
}"
58+
`);
59+
});
60+
61+
it('allows Math.min to accept multiple arguments', () => {
62+
const myFn = () => {
63+
'use gpu';
64+
const a = d.u32();
65+
const b = Math.min(a, 1, 2, 3);
66+
};
67+
68+
expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(`
69+
"fn myFn() {
70+
const a = 0u;
71+
let b = min(min(min(a, 1u), 2u), 3u);
72+
}"
73+
`);
74+
});
75+
76+
it('throws a readable error when unsupported Math feature is used', () => {
77+
const myFn = () => {
78+
'use gpu';
79+
const a = Math.log1p(1);
80+
};
81+
82+
expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(`
83+
[Error: Resolution of the following tree failed:
84+
- <root>
85+
- fn*:myFn
86+
- fn*:myFn(): Unsupported functionality 'Math.log1p'. Use an std alternative, or implement the function manually.]
87+
`);
88+
});
89+
90+
it('correctly applies Math.fround', () => {
91+
const myFn = () => {
92+
'use gpu';
93+
const a = Math.fround(16777217);
94+
};
95+
96+
expect(tgpu.resolve([myFn])).toMatchInlineSnapshot(`
97+
"fn myFn() {
98+
const a = 16777216f;
99+
}"
100+
`);
101+
});
102+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it } from 'vitest';
2+
import tgpu, { d, std } from '../../../src/index.ts';
3+
4+
describe('round', () => {
5+
it('rounds to even numbers', () => {
6+
const myRound = tgpu.fn([d.f32], d.f32)((a) => std.round(a));
7+
8+
expect(myRound(2.4)).toBe(2);
9+
expect(myRound(2.5)).toBe(2);
10+
expect(myRound(2.6)).toBe(3);
11+
expect(myRound(3.4)).toBe(3);
12+
expect(myRound(3.5)).toBe(4);
13+
expect(myRound(3.6)).toBe(4);
14+
expect(tgpu.resolve([myRound])).toMatchInlineSnapshot(`
15+
"fn myRound(a: f32) -> f32 {
16+
return round(a);
17+
}"
18+
`);
19+
});
20+
});

0 commit comments

Comments
 (0)