diff --git a/packages/typegpu/src/data/numeric.ts b/packages/typegpu/src/data/numeric.ts index d49368843b..85fbc2bb4e 100644 --- a/packages/typegpu/src/data/numeric.ts +++ b/packages/typegpu/src/data/numeric.ts @@ -2,22 +2,6 @@ import { $internal } from '../shared/symbols.ts'; import type { AbstractFloat, AbstractInt, Bool, F16, F32, I32, U16, U32 } from './wgslTypes.ts'; import { callableSchema } from '../core/function/createCallableSchema.ts'; -export const abstractInt = { - [$internal]: {}, - type: 'abstractInt', - toString() { - return 'abstractInt'; - }, -} as AbstractInt; - -export const abstractFloat = { - [$internal]: {}, - type: 'abstractFloat', - toString() { - return 'abstractFloat'; - }, -} as AbstractFloat; - const boolCast = callableSchema({ name: 'bool', schema: () => bool, @@ -299,3 +283,21 @@ export const f16: F16 = Object.assign(f16Cast, { [$internal]: {}, type: 'f16', }) as unknown as F16; + +export const abstractInt = { + [$internal]: {}, + type: 'abstractInt', + toString() { + return 'abstractInt'; + }, + concretized: i32, +} as AbstractInt; + +export const abstractFloat = { + [$internal]: {}, + type: 'abstractFloat', + toString() { + return 'abstractFloat'; + }, + concretized: f32, +} as AbstractFloat; diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index dc85031925..f8757d90b5 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -129,6 +129,7 @@ export interface matInfixNotation { */ export interface AbstractInt extends BaseData { readonly type: 'abstractInt'; + readonly concretized: I32; // Type-tokens, not available at runtime readonly [$repr]: number; readonly [$invalidSchemaReason]: 'Abstract numerics are not host-shareable'; @@ -140,6 +141,7 @@ export interface AbstractInt extends BaseData { */ export interface AbstractFloat extends BaseData { readonly type: 'abstractFloat'; + readonly concretized: F32; // Type-tokens, not available at runtime readonly [$repr]: number; readonly [$invalidSchemaReason]: 'Abstract numerics are not host-shareable'; diff --git a/packages/typegpu/src/tgsl/conversion.ts b/packages/typegpu/src/tgsl/conversion.ts index 0d8ae68163..f410e53e6d 100644 --- a/packages/typegpu/src/tgsl/conversion.ts +++ b/packages/typegpu/src/tgsl/conversion.ts @@ -5,6 +5,7 @@ import { derefSnippet, RefOperator } from '../data/ref.ts'; import { schemaCallWrapperGPU } from '../data/schemaCallWrapper.ts'; import { snip, type Snippet } from '../data/snippet.ts'; import { + type AbstractFloat, type AnyWgslData, type BaseData, type F16, @@ -120,12 +121,18 @@ function getImplicitConversionRank(src: BaseData, dest: BaseData): ConversionRan } } + if ((trueSrc.type === 'u32' || trueSrc.type === 'i32') && trueDst.type === 'abstractFloat') { + // When one of the types is a float (abstract or not), we don't want to cast it to a non-float type, + // which would cause it to lose precision. We instead choose the common type to be f32. + return { rank: 1, action: 'cast', targetType: (trueDst as AbstractFloat).concretized }; + } + if (trueSrc.type === 'abstractFloat') { - if (trueDst.type === 'u32') { + if (trueDst.type === 'i32') { return { rank: 2, action: 'cast', targetType: trueDst }; } - if (trueDst.type === 'i32') { - return { rank: 1, action: 'cast', targetType: trueDst }; + if (trueDst.type === 'u32') { + return { rank: 3, action: 'cast', targetType: trueDst }; } } @@ -167,6 +174,10 @@ function findBestType( let bestResult: { type: BaseData; details: ConversionRankInfo[]; sum: number } | undefined; for (const targetType of uniqueTypes) { + /** + * The type we end up converting to. Will be different than `targetType` if `targetType === abstractFloat` + */ + let destType = targetType; const details: ConversionRankInfo[] = []; let sum = 0; for (const sourceType of types) { @@ -176,9 +187,12 @@ function findBestType( break; } details.push(conversion); + if (conversion.action === 'cast') { + destType = conversion.targetType; + } } if (sum < (bestResult?.sum ?? Number.POSITIVE_INFINITY)) { - bestResult = { type: targetType, details, sum }; + bestResult = { type: destType, details, sum }; } } if (!bestResult) { diff --git a/packages/typegpu/tests/indent.test.ts b/packages/typegpu/tests/indent.test.ts index 2952762fcf..f3e7d4ef7d 100644 --- a/packages/typegpu/tests/indent.test.ts +++ b/packages/typegpu/tests/indent.test.ts @@ -333,7 +333,7 @@ describe('indents', () => { })((input) => { 'use gpu'; const uniBoid = layout.$.boids; - for (let i = d.u32(); i < std.floor(std.sin(Math.PI / 2)); i++) { + for (let i = d.u32(); i < d.u32(std.sin(Math.PI / 2)); i++) { const sampled = std.textureSample(layout.$.sampled, layout.$.sampler, d.vec2f(0.5, 0.5), i); const someVal = std.textureLoad(layout.$.smoothRender, d.vec2i(), 0); if (someVal.x + sampled.x > 0.5) { diff --git a/packages/typegpu/tests/tgsl/conversion.test.ts b/packages/typegpu/tests/tgsl/conversion.test.ts index ee72863801..d1836a9bd7 100644 --- a/packages/typegpu/tests/tgsl/conversion.test.ts +++ b/packages/typegpu/tests/tgsl/conversion.test.ts @@ -260,6 +260,12 @@ describe('convertToCommonType', () => { expect(result).toBeUndefined(); }); + it('chooses abstractFloat over i32', () => { + const result = convertToCommonType(ctx, [snippetI32, snippetAbsFloat]); + expect(result).toBeDefined(); + expect(result?.[0]?.dataType.type).toBe('f32'); + }); + it('respects restrictTo types', () => { // [abstractInt, i32] -> common type i32 // Restrict to f32: requires cast for i32 diff --git a/packages/typegpu/tests/tgsl/multiplication.test.ts b/packages/typegpu/tests/tgsl/multiplication.test.ts new file mode 100644 index 0000000000..30b3b589c8 --- /dev/null +++ b/packages/typegpu/tests/tgsl/multiplication.test.ts @@ -0,0 +1,129 @@ +import { test } from 'typegpu-testing-utility'; +import { expect, vi } from 'vitest'; +import tgpu, { d } from 'typegpu'; + +import { expectSnippetOf } from '../utils/parseResolved.ts'; + +test('multiplying i32 with a float literal should implicitly convert to an f32', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + function main() { + 'use gpu'; + const a = d.i32(1) * 0.001; + const int = d.i32(1); + return int * 0.001; + } + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() -> f32 { + const a = 1e-3f; + const int = 1i; + return (f32(int) * 1e-3f); + }" + `); + + expectSnippetOf(() => { + 'use gpu'; + return d.i32(1) * 0.001; + }).toStrictEqual([0.001, d.f32, 'constant']); + + expect(consoleWarnSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "Implicit conversions from [ + 1: i32 + ] to f32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + [ + "Implicit conversions from [ + int: i32 + ] to f32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + [ + "Implicit conversions from [ + 1: i32 + ] to f32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + ] + `); +}); + +test('multiplying u32 with a float literal should implicitly convert to an f32', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + function main() { + 'use gpu'; + const a = d.u32(10) * 0.001; + const int = d.u32(100); + return int * 0.001; + } + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() -> f32 { + const a = 0.01f; + const int = 100u; + return (f32(int) * 1e-3f); + }" + `); + + expectSnippetOf(() => { + 'use gpu'; + return d.u32(1) * 0.001; + }).toStrictEqual([0.001, d.f32, 'constant']); + + expect(consoleWarnSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "Implicit conversions from [ + 10: u32 + ] to f32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + [ + "Implicit conversions from [ + int: u32 + ] to f32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + [ + "Implicit conversions from [ + 1: u32 + ] to f32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + ] + `); +}); + +test('multiplying u32 with an i32 should implicitly convert to an i32', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + function main() { + 'use gpu'; + const uint = d.u32(5); + const int = d.i32(3); + return uint * int; + } + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() -> i32 { + const uint = 5u; + const int = 3i; + return (i32(uint) * int); + }" + `); + + expect(consoleWarnSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "Implicit conversions from [ + uint: u32 + ] to i32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + ] + `); +});