Skip to content

Commit 74bc6ff

Browse files
committed
feat: Route binary operations through the shader generator
1 parent 339c509 commit 74bc6ff

12 files changed

Lines changed: 178 additions & 74 deletions

File tree

packages/typegpu-gl/src/glslGenerator.ts

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NodeTypeCatalog as NODE } from 'tinyest';
22
import type { Expression, Return } from 'tinyest';
3-
import { tgpu, d, type ShaderStage } from 'typegpu';
3+
import { tgpu, d, type ShaderStage, std } from 'typegpu';
44
import { abstractInt, getName, snip, UnknownData, WgslGenerator } from 'typegpu/~internal';
55
import type {
66
ResolutionCtx,
@@ -10,6 +10,7 @@ import type {
1010
Origin,
1111
Snippet,
1212
ResolvedSnippet,
13+
BinaryOperator,
1314
} from 'typegpu/~internal';
1415

1516
// ----------
@@ -175,6 +176,28 @@ export function getCrossShaderStageState(ctx: ResolutionCtx) {
175176
return state;
176177
}
177178

179+
function isF32VecfSchema(
180+
schema: d.BaseData | UnknownData,
181+
): schema is d.F32 | d.Vec2f | d.Vec3f | d.Vec4f {
182+
return (
183+
typeof schema !== 'symbol' &&
184+
(schema.type === 'f32' ||
185+
schema.type === 'vec2f' ||
186+
schema.type === 'vec3f' ||
187+
schema.type === 'vec4f')
188+
);
189+
}
190+
191+
const HELPERS = {
192+
// TODO(#2821): Make signature more accurate when std.sign and std.abs
193+
// accept a wider union
194+
remainder: (x: number, y: number): number => {
195+
'use gpu';
196+
const truncDiv = std.sign(x / y) * std.floor(std.abs(x / y));
197+
return x - y * truncDiv;
198+
},
199+
};
200+
178201
/**
179202
* A GLSL ES 3.0 shader generator that extends WgslGenerator.
180203
* Overrides `dataType` to emit GLSL type names instead of WGSL ones,
@@ -247,7 +270,7 @@ export class GlslGenerator extends WgslGenerator {
247270
return snip(options.id, options.dataType, options.scope);
248271
}
249272

250-
override typeAnnotation(data: d.BaseData): string {
273+
override emitTypeAnnotation(data: d.BaseData): string {
251274
if (!d.isLooseData(data)) {
252275
const glslName = WGSL_TO_GLSL_TYPE[data.type];
253276
if (glslName !== undefined) {
@@ -257,17 +280,17 @@ export class GlslGenerator extends WgslGenerator {
257280

258281
if (d.isWgslArray(data)) {
259282
// The array size suffix is handled elsewhere
260-
return this.typeAnnotation(data.elementType);
283+
return this.emitTypeAnnotation(data.elementType);
261284
}
262285

263286
if (d.isWgslStruct(data)) {
264287
return resolveStruct(this.ctx, data);
265288
}
266289

267-
return super.typeAnnotation(data);
290+
return super.emitTypeAnnotation(data);
268291
}
269292

270-
override call(
293+
override emitCall(
271294
name: string,
272295
templateParams: readonly Snippet[],
273296
args: readonly Snippet[],
@@ -291,16 +314,16 @@ export class GlslGenerator extends WgslGenerator {
291314
: sourceSchema;
292315

293316
if (sourcePrimitive.type === 'u32' && targetPrimitive.type === 'f32') {
294-
return super.call('uintBitsToFloat', [], [source]);
317+
return super.emitCall('uintBitsToFloat', [], [source]);
295318
}
296319
if (sourcePrimitive.type === 'i32' && targetPrimitive.type === 'f32') {
297-
return super.call('intBitsToFloat', [], [source]);
320+
return super.emitCall('intBitsToFloat', [], [source]);
298321
}
299322
if (sourcePrimitive.type === 'f32' && targetPrimitive.type === 'u32') {
300-
return super.call('floatBitsToUint', [], [source]);
323+
return super.emitCall('floatBitsToUint', [], [source]);
301324
}
302325
if (sourcePrimitive.type === 'f32' && targetPrimitive.type === 'i32') {
303-
return super.call('floatBitsToInt', [], [source]);
326+
return super.emitCall('floatBitsToInt', [], [source]);
304327
}
305328
if (sourceSchema.type === targetSchema.type) {
306329
return this.ctx.resolveSnippet(source).value;
@@ -317,9 +340,9 @@ export class GlslGenerator extends WgslGenerator {
317340

318341
if (falsy.dataType !== UnknownData && falsy.dataType.type.startsWith('vec')) {
319342
if (cond.dataType !== UnknownData && cond.dataType.type.startsWith('vec')) {
320-
return super.call('mix', templateParams, args);
343+
return super.emitCall('mix', templateParams, args);
321344
}
322-
return super.call('mix', templateParams, [
345+
return super.emitCall('mix', templateParams, [
323346
falsy,
324347
truthy,
325348
this.typeInstantiation(correspondingBooleanVectorSchema(falsy.dataType), [cond]),
@@ -334,10 +357,14 @@ export class GlslGenerator extends WgslGenerator {
334357
if (!arg) {
335358
throw new Error(`Invalid number of arguments for 'saturate'`);
336359
}
337-
return super.call('clamp', [], [arg, snip(0, d.f32, 'constant'), snip(1, d.f32, 'constant')]);
360+
return super.emitCall(
361+
'clamp',
362+
[],
363+
[arg, snip(0, d.f32, 'constant'), snip(1, d.f32, 'constant')],
364+
);
338365
}
339366

340-
return super.call(name, templateParams, args);
367+
return super.emitCall(name, templateParams, args);
341368
}
342369

343370
override typeInstantiation(schema: d.BaseData, args: Snippet[]): ResolvedSnippet {
@@ -393,6 +420,22 @@ export class GlslGenerator extends WgslGenerator {
393420
return `${this.ctx.pre}${glslTypeName} ${name}${resolveArraySizeSuffix(this.ctx, dataType)} = ${rhsStr};`;
394421
}
395422

423+
override emitBinaryOp(lhs: Snippet, op: BinaryOperator, rhs: Snippet): string {
424+
if (op === '%' && (isF32VecfSchema(lhs.dataType) || isF32VecfSchema(rhs.dataType))) {
425+
const result = this._callShellless(HELPERS.remainder, [lhs, rhs]);
426+
if (!result) {
427+
const lhsStr = this.ctx.resolveSnippet(lhs).value;
428+
const rhsStr = this.ctx.resolveSnippet(rhs).value;
429+
throw new Error(
430+
`[@typegpu/gl] Invalid use of '%', incompatible with the GLSL generator: ${lhsStr} (type: ${String(lhs.dataType)}) ${op} ${rhsStr} (type: ${String(rhs.dataType)})`,
431+
);
432+
}
433+
return result.value;
434+
}
435+
436+
return super.emitBinaryOp(lhs, op, rhs);
437+
}
438+
396439
/**
397440
* GLSL has no pointers, so `const x = <alias>;` cannot be turned into an implicit
398441
* pointer definition like it is in WGSL. Instead:

packages/typegpu-gl/tests/glslGenerator.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,30 @@ describe('GlslGenerator - standard function calls', () => {
202202
});
203203
});
204204

205+
describe('GlslGenerator - operator', () => {
206+
it('translates % with floating-point arguments to a call to the `remainder` helper function', () => {
207+
function foo() {
208+
'use gpu';
209+
const value = 2;
210+
const rem = value % 5;
211+
return (1 + rem) % 0.5;
212+
}
213+
214+
expect(tgpu.resolve([foo], glOptions())).toMatchInlineSnapshot(`
215+
"float remainder(float x, float y) {
216+
float truncDiv = (sign((x / y)) * floor(abs((x / y))));
217+
return (x - (y * truncDiv));
218+
}
219+
220+
float foo() {
221+
int value = 2;
222+
int rem = (value % 5);
223+
return remainder(float((1 + rem)), 0.5);
224+
}"
225+
`);
226+
});
227+
});
228+
205229
describe('GlslGenerator - function definitions', () => {
206230
it('generates proper function signatures', () => {
207231
function add(a: number, b: number) {

packages/typegpu/src/internal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type { Snippet, ResolvedSnippet, Origin } from './data/snippet.ts';
1515

1616
export type {
1717
ShaderGenerator,
18+
BinaryOperator,
1819
ShaderGeneratorClass,
1920
FunctionDefinitionOptions,
2021
ConstantDefinitionOptions,

packages/typegpu/src/resolutionCtx.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -947,7 +947,7 @@ export class ResolutionCtxImpl implements ResolutionCtx {
947947
let result: ResolvedSnippet;
948948
if (isData(item)) {
949949
// Ref is arbitrary, as we're resolving a schema
950-
result = snip(this.gen.typeAnnotation(item), Void, /* origin */ 'runtime');
950+
result = snip(this.gen.emitTypeAnnotation(item), Void, /* origin */ 'runtime');
951951
} else if (isLazy(item) || isSlot(item)) {
952952
result = this.resolve(this.unwrap(item));
953953
} else if (isSelfResolvable(item)) {

packages/typegpu/src/std/array.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const arrayLength = dualImpl({
1919
normalImpl: (a: unknown[] | ref<unknown[]>) => (isRef(a) ? a.$.length : a.length),
2020
codegenImpl(ctx, [a]) {
2121
const length = sizeOfPointedToArray(a.dataType);
22-
return length > 0 ? `${length}` : ctx.gen.call('arrayLength', [], [a]);
22+
return length > 0 ? `${length}` : ctx.gen.emitCall('arrayLength', [], [a]);
2323
},
2424
sideEffects: false,
2525
});

packages/typegpu/src/std/bitcast.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const bitcastU32toF32 = dualImpl({
6565
return VectorOps.bitcastU32toF32[value.kind](value);
6666
}) as BitcastU32toF32Overload,
6767
codegenImpl: (ctx, [n], returnType) => {
68-
return ctx.gen.call('bitcast', [coerceToSnippet(returnType)], [n]);
68+
return ctx.gen.emitCall('bitcast', [coerceToSnippet(returnType)], [n]);
6969
},
7070
signature: (...arg) => {
7171
const uargs = unifyStrict(arg, u32AllowedSchemas);
@@ -102,7 +102,7 @@ export const bitcastU32toI32 = dualImpl({
102102
return VectorOps.bitcastU32toI32[value.kind](value);
103103
}) as BitcastU32toI32Overload,
104104
codegenImpl: (ctx, [n], returnType) => {
105-
return ctx.gen.call('bitcast', [coerceToSnippet(returnType)], [n]);
105+
return ctx.gen.emitCall('bitcast', [coerceToSnippet(returnType)], [n]);
106106
},
107107
signature: (...arg) => {
108108
const uargs = unifyStrict(arg, u32AllowedSchemas);
@@ -141,7 +141,7 @@ export const bitcastF32toU32 = dualImpl({
141141
return VectorOps.bitcastF32toU32[value.kind](value);
142142
}) as BitcastF32toU32Overload,
143143
codegenImpl: (ctx, [n], returnType) => {
144-
return ctx.gen.call('bitcast', [coerceToSnippet(returnType)], [n]);
144+
return ctx.gen.emitCall('bitcast', [coerceToSnippet(returnType)], [n]);
145145
},
146146
signature: (...arg) => {
147147
const uargs = unifyStrict(arg, f32AllowedSchemas);
@@ -277,7 +277,7 @@ function bitcastFor<In extends BitcastAllowedTypes, Out extends BitcastAllowedTy
277277
return dualImpl({
278278
name: 'bitcast',
279279
normalImpl: getCpuBitcast<In, Out>(inType, outType),
280-
codegenImpl: (ctx, [n]) => ctx.gen.call('bitcast', [coerceToSnippet(outType)], [n]),
280+
codegenImpl: (ctx, [n]) => ctx.gen.emitCall('bitcast', [coerceToSnippet(outType)], [n]),
281281
signature: (arg) => {
282282
const uarg = unifyStrict([arg], [inType]);
283283
if (!uarg) {

packages/typegpu/src/std/boolean.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ export const select = dualImpl({
443443
},
444444
normalImpl: cpuSelect,
445445
codegenImpl: (ctx, [f, t, cond]) => {
446-
const result = ctx.gen.call('select', [], [f, t, cond]);
446+
const result = ctx.gen.emitCall('select', [], [f, t, cond]);
447447
if (
448448
!validSelectBranchTypes.includes(f.dataType as AnyWgslData) ||
449449
!validSelectBranchTypes.includes(t.dataType as AnyWgslData)

packages/typegpu/src/std/numeric.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,7 @@ export const saturate = dualImpl({
10721072
name: 'saturate',
10731073
signature: unifyRestrictedSignature(anyFloat),
10741074
normalImpl: cpuSaturate,
1075-
codegenImpl: (ctx, [value]) => ctx.gen.call('saturate', [], [value]),
1075+
codegenImpl: (ctx, [value]) => ctx.gen.emitCall('saturate', [], [value]),
10761076
sideEffects: false,
10771077
});
10781078

packages/typegpu/src/std/operators.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export const add = dualImpl({
116116
name: 'add',
117117
signature: binaryArithmeticSignature,
118118
normalImpl: cpuAdd,
119-
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} + ${rhs})`,
119+
codegenImpl: (ctx, [lhs, rhs]) => ctx.gen.emitBinaryOp(lhs, '+', rhs),
120120
sideEffects: false,
121121
});
122122

@@ -144,7 +144,7 @@ export const sub = dualImpl({
144144
name: 'sub',
145145
signature: binaryArithmeticSignature,
146146
normalImpl: cpuSub,
147-
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} - ${rhs})`,
147+
codegenImpl: (ctx, [lhs, rhs]) => ctx.gen.emitBinaryOp(lhs, '-', rhs),
148148
sideEffects: false,
149149
});
150150

@@ -196,7 +196,7 @@ export const mul = dualImpl({
196196
name: 'mul',
197197
signature: binaryMulSignature,
198198
normalImpl: cpuMul,
199-
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} * ${rhs})`,
199+
codegenImpl: (ctx, [lhs, rhs]) => ctx.gen.emitBinaryOp(lhs, '*', rhs),
200200
sideEffects: false,
201201
});
202202

@@ -226,7 +226,7 @@ export const div = dualImpl({
226226
name: 'div',
227227
signature: binaryDivSignature,
228228
normalImpl: cpuDiv,
229-
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} / ${rhs})`,
229+
codegenImpl: (ctx, [lhs, rhs]) => ctx.gen.emitBinaryOp(lhs, '/', rhs),
230230
ignoreImplicitCastWarning: true,
231231
sideEffects: false,
232232
});
@@ -266,7 +266,7 @@ export const mod = dualImpl({
266266
}
267267
throw new Error('Mod called with invalid arguments, expected types: number or vector.');
268268
}) as ModOverload,
269-
codegenImpl: (_ctx, [lhs, rhs]) => stitch`(${lhs} % ${rhs})`,
269+
codegenImpl: (ctx, [lhs, rhs]) => ctx.gen.emitBinaryOp(lhs, '%', rhs),
270270
sideEffects: false,
271271
});
272272

@@ -352,13 +352,13 @@ export const bitShiftLeft = dualImpl({
352352
name: 'bitShiftLeft',
353353
signature: bitShiftSignature,
354354
normalImpl: cpuBitShiftLeft,
355-
codegenImpl: (_ctx, [lhs, rhs]) => {
355+
codegenImpl: (ctx, [lhs, rhs]) => {
356356
if (isVec(lhs.dataType) && !isVec(rhs.dataType)) {
357357
const cc = lhs.dataType.componentCount;
358-
const schema = cc === 2 ? 'vec2u' : cc === 3 ? 'vec3u' : 'vec4u';
359-
return stitch`(${lhs} << ${schema}(${rhs}))`;
358+
const schema = cc === 2 ? vec2u : cc === 3 ? vec3u : vec4u;
359+
return ctx.gen.emitBinaryOp(lhs, '<<', ctx.gen.typeInstantiation(schema, [rhs]));
360360
}
361-
return stitch`(${lhs} << ${rhs})`;
361+
return ctx.gen.emitBinaryOp(lhs, '<<', rhs);
362362
},
363363
sideEffects: false,
364364
});
@@ -389,13 +389,13 @@ export const bitShiftRight = dualImpl({
389389
name: 'bitShiftRight',
390390
signature: bitShiftSignature,
391391
normalImpl: cpuBitShiftRight,
392-
codegenImpl: (_ctx, [lhs, rhs]) => {
392+
codegenImpl: (ctx, [lhs, rhs]) => {
393393
if (isVec(lhs.dataType) && !isVec(rhs.dataType)) {
394394
const cc = lhs.dataType.componentCount;
395-
const schema = cc === 2 ? 'vec2u' : cc === 3 ? 'vec3u' : 'vec4u';
396-
return stitch`(${lhs} >> ${schema}(${rhs}))`;
395+
const schema = cc === 2 ? vec2u : cc === 3 ? vec3u : vec4u;
396+
return ctx.gen.emitBinaryOp(lhs, '>>', ctx.gen.typeInstantiation(schema, [rhs]));
397397
}
398-
return stitch`(${lhs} >> ${rhs})`;
398+
return ctx.gen.emitBinaryOp(lhs, '>>', rhs);
399399
},
400400
sideEffects: false,
401401
});

packages/typegpu/src/tgsl/shaderGenerator.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ export interface ShaderGeneratorClass<T extends ShaderGenerator = ShaderGenerato
4343
new (): T;
4444
}
4545

46+
export type BinaryOperator =
47+
| '='
48+
| '^'
49+
| '|'
50+
| '&'
51+
| '*'
52+
| '/'
53+
| '%'
54+
| '+'
55+
| '-'
56+
| '<<'
57+
| '>>'
58+
| '<'
59+
| '>'
60+
| '<='
61+
| '>='
62+
| '=='
63+
| '!=';
64+
4665
/**
4766
* Represents generators that, once instantiated, will generate `wgsl` (as opposed to e.g. `glsl`)
4867
*/
@@ -65,6 +84,8 @@ export interface ShaderGenerator {
6584

6685
typeInstantiation(schema: BaseData, args: readonly Snippet[]): ResolvedSnippet;
6786
numericLiteral(value: number, schema: BaseData): ResolvedSnippet;
68-
typeAnnotation(schema: BaseData): string;
69-
call(name: string, templateParams: readonly Snippet[], args: readonly Snippet[]): string;
87+
88+
emitTypeAnnotation(schema: BaseData): string;
89+
emitCall(name: string, templateParams: readonly Snippet[], args: readonly Snippet[]): string;
90+
emitBinaryOp(lhs: Snippet, op: BinaryOperator, rhs: Snippet): string;
7091
}

0 commit comments

Comments
 (0)