From 545f41b852d0f72b202f5b633062bbbd7ae08731 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 19 May 2026 16:50:53 +0200 Subject: [PATCH 1/7] bitcastF32toU32 --- packages/typegpu/src/data/numberOps.ts | 6 ++++ packages/typegpu/src/data/vectorOps.ts | 19 ++++++++++ packages/typegpu/src/std/bitcast.ts | 41 +++++++++++++++++++-- packages/typegpu/src/std/index.ts | 2 +- packages/typegpu/tests/std/bitcast.test.ts | 42 ++++++++++++++++++++++ 5 files changed, 107 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/src/data/numberOps.ts b/packages/typegpu/src/data/numberOps.ts index 55ad1b4787..85cf34e82c 100644 --- a/packages/typegpu/src/data/numberOps.ts +++ b/packages/typegpu/src/data/numberOps.ts @@ -27,3 +27,9 @@ export function bitcastU32toI32Impl(n: number): number { dataView.setUint32(0, n, true); return dataView.getInt32(0, true); } + +export function bitcastF32toU32Impl(n: number): number { + const dataView = new DataView(new ArrayBuffer(4)); + dataView.setFloat32(0, n, true); + return dataView.getUint32(0, true); +} diff --git a/packages/typegpu/src/data/vectorOps.ts b/packages/typegpu/src/data/vectorOps.ts index a05d1d1af2..3bd64a6aa0 100644 --- a/packages/typegpu/src/data/vectorOps.ts +++ b/packages/typegpu/src/data/vectorOps.ts @@ -1,5 +1,6 @@ import { mat2x2f, mat3x3f, mat4x4f } from './matrix.ts'; import { + bitcastF32toU32Impl, bitcastU32toF32Impl, bitcastU32toI32Impl, clamp, @@ -1162,4 +1163,22 @@ export const VectorOps = { v: T, ) => T extends wgsl.v2u ? wgsl.v2i : T extends wgsl.v3u ? wgsl.v3i : wgsl.v4i >, + + bitcastF32toU32: { + vec2f: (n: wgsl.v2f) => vec2u(bitcastF32toU32Impl(n.x), bitcastF32toU32Impl(n.y)), + vec3f: (n: wgsl.v3f) => + vec3u(bitcastF32toU32Impl(n.x), bitcastF32toU32Impl(n.y), bitcastF32toU32Impl(n.z)), + vec4f: (n: wgsl.v4f) => + vec4u( + bitcastF32toU32Impl(n.x), + bitcastF32toU32Impl(n.y), + bitcastF32toU32Impl(n.z), + bitcastF32toU32Impl(n.w), + ), + } as Record< + VecKind, + ( + v: T, + ) => T extends wgsl.v2f ? wgsl.v2u : T extends wgsl.v3f ? wgsl.v3u : wgsl.v4u + >, }; diff --git a/packages/typegpu/src/std/bitcast.ts b/packages/typegpu/src/std/bitcast.ts index 4039858dd9..3b66a2ca61 100644 --- a/packages/typegpu/src/std/bitcast.ts +++ b/packages/typegpu/src/std/bitcast.ts @@ -1,9 +1,13 @@ import { dualImpl } from '../core/function/dualImpl.ts'; import { stitch } from '../core/resolve/stitch.ts'; -import { bitcastU32toF32Impl, bitcastU32toI32Impl } from '../data/numberOps.ts'; +import { + bitcastF32toU32Impl, + bitcastU32toF32Impl, + bitcastU32toI32Impl, +} from '../data/numberOps.ts'; import { f32, i32, u32 } from '../data/numeric.ts'; import { isVec } from '../data/wgslTypes.ts'; -import { vec2f, vec2i, vec3f, vec3i, vec4f, vec4i } from '../data/vector.ts'; +import { vec2f, vec2i, vec2u, vec3f, vec3i, vec3u, vec4f, vec4i, vec4u } from '../data/vector.ts'; import { VectorOps } from '../data/vectorOps.ts'; import type { v2f, v2i, v2u, v3f, v3i, v3u, v4f, v4i, v4u } from '../data/wgslTypes.ts'; import { unify } from '../tgsl/conversion.ts'; @@ -65,3 +69,36 @@ export const bitcastU32toI32 = dualImpl({ }; }, }); + +export type BitcastF32toU32Overload = ((value: number) => number) & + ((value: v2f) => v2u) & + ((value: v3f) => v3u) & + ((value: v4f) => v4u); + +export const bitcastF32toU32 = dualImpl({ + name: 'bitcastF32toU32', + normalImpl: ((value) => { + if (typeof value === 'number') { + return bitcastF32toU32Impl(value); + } + return VectorOps.bitcastF32toU32[value.kind](value); + }) as BitcastF32toU32Overload, + codegenImpl: (_ctx, [n]) => { + return isVec(n.dataType) + ? stitch`bitcast(${n})` + : stitch`bitcast(${n})`; + }, + signature: (...arg) => { + const uargs = unify(arg, [f32]) ?? arg; + return { + argTypes: uargs, + returnType: isVec(uargs[0]) + ? uargs[0].type === 'vec2f' + ? vec2u + : uargs[0].type === 'vec3f' + ? vec3u + : vec4u + : u32, + }; + }, +}); diff --git a/packages/typegpu/src/std/index.ts b/packages/typegpu/src/std/index.ts index 2b085f7fc0..403678eebe 100644 --- a/packages/typegpu/src/std/index.ts +++ b/packages/typegpu/src/std/index.ts @@ -186,6 +186,6 @@ export { export { extensionEnabled } from './extensions.ts'; -export { bitcastU32toF32, bitcastU32toI32 } from './bitcast.ts'; +export { bitcastU32toF32, bitcastU32toI32, bitcastF32toU32 } from './bitcast.ts'; export { range } from './range.ts'; diff --git a/packages/typegpu/tests/std/bitcast.test.ts b/packages/typegpu/tests/std/bitcast.test.ts index dabcd382a4..a2a4c10e23 100644 --- a/packages/typegpu/tests/std/bitcast.test.ts +++ b/packages/typegpu/tests/std/bitcast.test.ts @@ -12,6 +12,9 @@ import { } from '../../src/data/vector.ts'; import tgpu, { d, std } from '../../src/index.js'; +// remember to pad with zeros to 8 hex symbols +const floatFromHex = (hex: string) => Buffer.from(hex, 'hex').readFloatBE(0); + describe('bitcast', () => { it('bitcastU32toF32', () => { // 1.0 in f32 @@ -37,6 +40,14 @@ describe('bitcast', () => { expect(i2).toBe(-2147483648); }); + it('bitcastF32toU32', () => { + const i1 = std.bitcastF32toU32(floatFromHex('00000001')); + expect(i1).toBe(1); + + const i2 = std.bitcastF32toU32(floatFromHex('7f800000')); + expect(i2).toBe(2139095040); + }); + it('bitcastU32toF32 vectors', () => { const v2 = vec2u(1065353216, 3212836864); // 1.0f, -1.0f const cast2 = std.bitcastU32toF32(v2); @@ -65,6 +76,25 @@ describe('bitcast', () => { expect(cast4).toEqual(vec4i(0, 1, -1, -2147483648)); }); + it('bitcastF32toU32 vectors', () => { + const v2 = vec2f(floatFromHex('7f800000'), floatFromHex('7fc00000')); // +inf, quiet nan + const cast2 = std.bitcastF32toU32(v2); + expect(cast2).toStrictEqual(vec2u(2139095040, 2143289344)); + + const v3 = vec3f(floatFromHex('ff800000'), floatFromHex('00000001'), floatFromHex('80000001')); + const cast3 = std.bitcastF32toU32(v3); + expect(cast3).toStrictEqual(vec3u(4286578688, 1, 2147483649)); + + const v4 = vec4f( + floatFromHex('84220925'), + floatFromHex('68800000'), + floatFromHex('48980780'), + floatFromHex('0000075a'), + ); + const cast4 = std.bitcastF32toU32(v4); + expect(cast4).toStrictEqual(vec4u(2216823077, 1753219072, 1217922944, 1882)); + }); + it('bitcastU32toF32 specials (NaN, infinities etc)', () => { // +0 const pz = std.bitcastU32toF32(0x00000000); @@ -128,6 +158,7 @@ describe('bitcast in shaders', () => { it('works for primitives', () => { const fnf32 = tgpu.fn([], d.f32)(() => std.bitcastU32toF32(1234)); const fni32 = tgpu.fn([], d.i32)(() => std.bitcastU32toI32(d.u32(2 ** 31))); + const fnu32 = tgpu.fn([d.f32], d.u32)((v) => std.bitcastF32toU32(v)); expect(tgpu.resolve([fnf32])).toMatchInlineSnapshot(` "fn fnf32() -> f32 { @@ -139,11 +170,17 @@ describe('bitcast in shaders', () => { return -2147483648i; }" `); + expect(tgpu.resolve([fnu32])).toMatchInlineSnapshot(` + "fn fnu32(v: f32) -> u32 { + return bitcast(v); + }" + `); }); it('works for vectors', () => { const fnvec4i = tgpu.fn([], d.vec4i)(() => std.bitcastU32toI32(vec4u(1, 2, 3, 4))); const fnvec4f = tgpu.fn([], d.vec4f)(() => std.bitcastU32toF32(vec4u(1, 2, 3, 4))); + const fnvec4u = tgpu.fn([d.vec4f], d.vec4u)((v) => std.bitcastF32toU32(v)); expect(tgpu.resolve([fnvec4i])).toMatchInlineSnapshot(` "fn fnvec4i() -> vec4i { @@ -155,5 +192,10 @@ describe('bitcast in shaders', () => { return vec4f(1.401298464324817e-45, 2.802596928649634e-45, 4.203895392974451e-45, 5.605193857299268e-45); }" `); + expect(tgpu.resolve([fnvec4u])).toMatchInlineSnapshot(` + "fn fnvec4u(v: vec4f) -> vec4u { + return bitcast(v); + }" + `); }); }); From ef5135c142a6ddbb22d287b49477edb7618058bc Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 21 May 2026 21:26:23 +0200 Subject: [PATCH 2/7] better types, strict signature --- packages/typegpu/src/std/bitcast.ts | 41 ++++++++++++++-------- packages/typegpu/tests/std/bitcast.test.ts | 41 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/packages/typegpu/src/std/bitcast.ts b/packages/typegpu/src/std/bitcast.ts index 3b66a2ca61..a9e4e80966 100644 --- a/packages/typegpu/src/std/bitcast.ts +++ b/packages/typegpu/src/std/bitcast.ts @@ -11,11 +11,13 @@ import { vec2f, vec2i, vec2u, vec3f, vec3i, vec3u, vec4f, vec4i, vec4u } from '. import { VectorOps } from '../data/vectorOps.ts'; import type { v2f, v2i, v2u, v3f, v3i, v3u, v4f, v4i, v4u } from '../data/wgslTypes.ts'; import { unify } from '../tgsl/conversion.ts'; +import { SignatureNotSupportedError } from '../errors.ts'; -export type BitcastU32toF32Overload = ((value: number) => number) & - ((value: v2u) => v2f) & - ((value: v3u) => v3f) & - ((value: v4u) => v4f); +type BitcastU32toF32Overload = ( + value: T, +) => T extends v2u ? v2f : T extends v3u ? v3f : T extends v4u ? v4f : number; + +const u32AllowedSchemas = [u32, vec2u, vec3u, vec4u]; export const bitcastU32toF32 = dualImpl({ name: 'bitcastU32toF32', @@ -27,7 +29,10 @@ export const bitcastU32toF32 = dualImpl({ }) as BitcastU32toF32Overload, codegenImpl: (_ctx, [n]) => stitch`bitcast(${n})`, signature: (...arg) => { - const uargs = unify(arg, [u32]) ?? arg; + const uargs = unify(arg, u32AllowedSchemas); + if (!uargs) { + throw new SignatureNotSupportedError(arg, u32AllowedSchemas); + } return { argTypes: uargs, returnType: isVec(uargs[0]) @@ -41,10 +46,9 @@ export const bitcastU32toF32 = dualImpl({ }, }); -export type BitcastU32toI32Overload = ((value: number) => number) & - ((value: v2u) => v2i) & - ((value: v3u) => v3i) & - ((value: v4u) => v4i); +type BitcastU32toI32Overload = ( + value: T, +) => T extends v2u ? v2i : T extends v3u ? v3i : T extends v4u ? v4i : number; export const bitcastU32toI32 = dualImpl({ name: 'bitcastU32toI32', @@ -56,7 +60,10 @@ export const bitcastU32toI32 = dualImpl({ }) as BitcastU32toI32Overload, codegenImpl: (_ctx, [n]) => stitch`bitcast(${n})`, signature: (...arg) => { - const uargs = unify(arg, [u32]) ?? arg; + const uargs = unify(arg, u32AllowedSchemas); + if (!uargs) { + throw new SignatureNotSupportedError(arg, u32AllowedSchemas); + } return { argTypes: uargs, returnType: isVec(uargs[0]) @@ -70,10 +77,11 @@ export const bitcastU32toI32 = dualImpl({ }, }); -export type BitcastF32toU32Overload = ((value: number) => number) & - ((value: v2f) => v2u) & - ((value: v3f) => v3u) & - ((value: v4f) => v4u); +type BitcastF32toU32Overload = ( + value: T, +) => T extends v2f ? v2u : T extends v3f ? v3u : T extends v4f ? v4u : number; + +const f32AllowedSchemas = [f32, vec2f, vec3f, vec4f]; export const bitcastF32toU32 = dualImpl({ name: 'bitcastF32toU32', @@ -89,7 +97,10 @@ export const bitcastF32toU32 = dualImpl({ : stitch`bitcast(${n})`; }, signature: (...arg) => { - const uargs = unify(arg, [f32]) ?? arg; + const uargs = unify(arg, f32AllowedSchemas); + if (!uargs) { + throw new SignatureNotSupportedError(arg, f32AllowedSchemas); + } return { argTypes: uargs, returnType: isVec(uargs[0]) diff --git a/packages/typegpu/tests/std/bitcast.test.ts b/packages/typegpu/tests/std/bitcast.test.ts index a2a4c10e23..461f8059cd 100644 --- a/packages/typegpu/tests/std/bitcast.test.ts +++ b/packages/typegpu/tests/std/bitcast.test.ts @@ -198,4 +198,45 @@ describe('bitcast in shaders', () => { }" `); }); + + it('throws an error for unsupported signatures', () => { + const f1 = () => { + 'use gpu'; + // @ts-expect-error + return std.bitcastU32toF32(d.vec2i()); + }; + expect(() => tgpu.resolve([f1])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f1 + - fn*:f1() + - fn:bitcastU32toF32: Unsupported data types: vec2i. Supported types are: u32, vec2u, vec3u, vec4u.] + `); + + const f2 = () => { + 'use gpu'; + // @ts-expect-error + return std.bitcastU32toI32(d.vec3f()); + }; + expect(() => tgpu.resolve([f2])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f2 + - fn*:f2() + - fn:bitcastU32toI32: Unsupported data types: vec3f. Supported types are: u32, vec2u, vec3u, vec4u.] + `); + + const f3 = () => { + 'use gpu'; + // @ts-expect-error + return std.bitcastF32toU32(d.vec2h()); + }; + expect(() => tgpu.resolve([f3])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f3 + - fn*:f3() + - fn:bitcastF32toU32: Unsupported data types: vec2h. Supported types are: f32, vec2f, vec3f, vec4f.] + `); + }); }); From 430ccdf9128ca972530a06f36f748818e62d1f98 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 26 May 2026 11:40:59 +0200 Subject: [PATCH 3/7] review changes --- packages/typegpu/src/data/numberOps.ts | 19 ++++++++++--------- packages/typegpu/src/std/bitcast.ts | 8 ++++---- packages/typegpu/src/tgsl/conversion.ts | 19 +++++++++++++++++++ packages/typegpu/tests/std/bitcast.test.ts | 13 +++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/packages/typegpu/src/data/numberOps.ts b/packages/typegpu/src/data/numberOps.ts index 85cf34e82c..a605696b0e 100644 --- a/packages/typegpu/src/data/numberOps.ts +++ b/packages/typegpu/src/data/numberOps.ts @@ -16,20 +16,21 @@ export const divInteger = (lhs: number, rhs: number) => { return Math.trunc(lhs / rhs); }; +const buf32 = new ArrayBuffer(4); +const f32arr = new Float32Array(buf32); +const u32arr = new Uint32Array(buf32); +const i32arr = new Int32Array(buf32); export function bitcastU32toF32Impl(n: number): number { - const dataView = new DataView(new ArrayBuffer(4)); - dataView.setUint32(0, n, true); - return dataView.getFloat32(0, true); + u32arr[0] = n; + return f32arr[0] as number; } export function bitcastU32toI32Impl(n: number): number { - const dataView = new DataView(new ArrayBuffer(4)); - dataView.setUint32(0, n, true); - return dataView.getInt32(0, true); + u32arr[0] = n; + return i32arr[0] as number; } export function bitcastF32toU32Impl(n: number): number { - const dataView = new DataView(new ArrayBuffer(4)); - dataView.setFloat32(0, n, true); - return dataView.getUint32(0, true); + f32arr[0] = n; + return u32arr[0] as number; } diff --git a/packages/typegpu/src/std/bitcast.ts b/packages/typegpu/src/std/bitcast.ts index a9e4e80966..1cbdd2440d 100644 --- a/packages/typegpu/src/std/bitcast.ts +++ b/packages/typegpu/src/std/bitcast.ts @@ -10,7 +10,7 @@ import { isVec } from '../data/wgslTypes.ts'; import { vec2f, vec2i, vec2u, vec3f, vec3i, vec3u, vec4f, vec4i, vec4u } from '../data/vector.ts'; import { VectorOps } from '../data/vectorOps.ts'; import type { v2f, v2i, v2u, v3f, v3i, v3u, v4f, v4i, v4u } from '../data/wgslTypes.ts'; -import { unify } from '../tgsl/conversion.ts'; +import { unifyStrict } from '../tgsl/conversion.ts'; import { SignatureNotSupportedError } from '../errors.ts'; type BitcastU32toF32Overload = ( @@ -29,7 +29,7 @@ export const bitcastU32toF32 = dualImpl({ }) as BitcastU32toF32Overload, codegenImpl: (_ctx, [n]) => stitch`bitcast(${n})`, signature: (...arg) => { - const uargs = unify(arg, u32AllowedSchemas); + const uargs = unifyStrict(arg, u32AllowedSchemas); if (!uargs) { throw new SignatureNotSupportedError(arg, u32AllowedSchemas); } @@ -60,7 +60,7 @@ export const bitcastU32toI32 = dualImpl({ }) as BitcastU32toI32Overload, codegenImpl: (_ctx, [n]) => stitch`bitcast(${n})`, signature: (...arg) => { - const uargs = unify(arg, u32AllowedSchemas); + const uargs = unifyStrict(arg, u32AllowedSchemas); if (!uargs) { throw new SignatureNotSupportedError(arg, u32AllowedSchemas); } @@ -97,7 +97,7 @@ export const bitcastF32toU32 = dualImpl({ : stitch`bitcast(${n})`; }, signature: (...arg) => { - const uargs = unify(arg, f32AllowedSchemas); + const uargs = unifyStrict(arg, f32AllowedSchemas); if (!uargs) { throw new SignatureNotSupportedError(arg, f32AllowedSchemas); } diff --git a/packages/typegpu/src/tgsl/conversion.ts b/packages/typegpu/src/tgsl/conversion.ts index e01ce0700b..f604bbc55d 100644 --- a/packages/typegpu/src/tgsl/conversion.ts +++ b/packages/typegpu/src/tgsl/conversion.ts @@ -286,6 +286,25 @@ export function unify( }; } +export function unifyStrict( + inTypes: T, + restrictTo?: BaseData[], +): { [K in keyof T]: BaseData } | undefined { + if (inTypes.some((type) => type === UnknownData)) { + return undefined; + } + + const uniqueTargetTypes = [...new Set(((restrictTo || inTypes) as BaseData[]).map(undecorate))]; + const conversion = findBestType(inTypes as BaseData[], uniqueTargetTypes, false); + if (!conversion) { + return undefined; + } + + return inTypes.map((type) => (isVec(type) || isMat(type) ? type : conversion.targetType)) as { + [K in keyof T]: BaseData; + }; +} + export function convertToCommonType( ctx: ResolutionCtx, values: T, diff --git a/packages/typegpu/tests/std/bitcast.test.ts b/packages/typegpu/tests/std/bitcast.test.ts index 461f8059cd..21e6df2065 100644 --- a/packages/typegpu/tests/std/bitcast.test.ts +++ b/packages/typegpu/tests/std/bitcast.test.ts @@ -238,5 +238,18 @@ describe('bitcast in shaders', () => { - fn*:f3() - fn:bitcastF32toU32: Unsupported data types: vec2h. Supported types are: f32, vec2f, vec3f, vec4f.] `); + + const f4 = () => { + 'use gpu'; + const u = d.u32(1); + return std.bitcastF32toU32(u); + }; + expect(() => tgpu.resolve([f4])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:f4 + - fn*:f4() + - fn:bitcastF32toU32: Unsupported data types: u32. Supported types are: f32, vec2f, vec3f, vec4f.] + `); }); }); From c43e2d2094338f5d308d72b64fdbb472eae52b56 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 17 Jun 2026 11:45:36 +0200 Subject: [PATCH 4/7] more tests --- packages/typegpu/tests/std/bitcast.test.ts | 29 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/typegpu/tests/std/bitcast.test.ts b/packages/typegpu/tests/std/bitcast.test.ts index 32b29108cd..1a449e986d 100644 --- a/packages/typegpu/tests/std/bitcast.test.ts +++ b/packages/typegpu/tests/std/bitcast.test.ts @@ -77,13 +77,13 @@ describe('bitcast', () => { }); it('bitcastF32toU32 vectors', () => { - const v2 = vec2f(floatFromHex('7f800000'), floatFromHex('7fc00000')); // +inf, quiet nan + const v2 = vec2f(floatFromHex('7c800001'), floatFromHex('100008c7')); const cast2 = std.bitcastF32toU32(v2); - expect(cast2).toStrictEqual(vec2u(2139095040, 2143289344)); + expect(cast2).toStrictEqual(vec2u(2088763393, 268437703)); - const v3 = vec3f(floatFromHex('ff800000'), floatFromHex('00000001'), floatFromHex('80000001')); + const v3 = vec3f(floatFromHex('ff000000'), floatFromHex('00000001'), floatFromHex('80000001')); const cast3 = std.bitcastF32toU32(v3); - expect(cast3).toStrictEqual(vec3u(4286578688, 1, 2147483649)); + expect(cast3).toStrictEqual(vec3u(4278190080, 1, 2147483649)); const v4 = vec4f( floatFromHex('84220925'), @@ -143,6 +143,27 @@ describe('bitcast', () => { const c4 = std.bitcastU32toI32(v4); expect(c4).toEqual(vec4i(-2147483648, 1, 0, 2147483647)); }); + + it('bitcastF32toU32 specials (NaN, infinities etc)', () => { + // +0 + expect(std.bitcastF32toU32(+0)).toBe(0x00000000); + + // -0 + expect(std.bitcastF32toU32(-0)).toBe(0x80000000); + + // +Inf / -Inf + expect(std.bitcastF32toU32(Number.POSITIVE_INFINITY)).toBe(0x7f800000); + expect(std.bitcastF32toU32(Number.NEGATIVE_INFINITY)).toBe(0xff800000); + + // NaN + expect(std.bitcastF32toU32(Number.NaN)).toBe(0x7fc00000); + + // Smallest positive subnormal + expect(std.bitcastF32toU32(floatFromHex('00000001'))).toBe(0x00000001); + + // Smallest negative subnormal + expect(std.bitcastF32toU32(floatFromHex('80000001'))).toBe(0x80000001); + }); }); describe('bitcast in shaders', () => { From e94cabf0c21f2b2485a86bc4f3d3592f116ddf6b Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 17 Jun 2026 15:06:35 +0200 Subject: [PATCH 5/7] review changes --- packages/typegpu/src/std/bitcast.ts | 12 +++++++-- packages/typegpu/tests/std/bitcast.test.ts | 30 +++++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/src/std/bitcast.ts b/packages/typegpu/src/std/bitcast.ts index 1cbdd2440d..2f92d89704 100644 --- a/packages/typegpu/src/std/bitcast.ts +++ b/packages/typegpu/src/std/bitcast.ts @@ -27,7 +27,11 @@ export const bitcastU32toF32 = dualImpl({ } return VectorOps.bitcastU32toF32[value.kind](value); }) as BitcastU32toF32Overload, - codegenImpl: (_ctx, [n]) => stitch`bitcast(${n})`, + codegenImpl: (_ctx, [n]) => { + return isVec(n.dataType) + ? stitch`bitcast(${n})` + : stitch`bitcast(${n})`; + }, signature: (...arg) => { const uargs = unifyStrict(arg, u32AllowedSchemas); if (!uargs) { @@ -58,7 +62,11 @@ export const bitcastU32toI32 = dualImpl({ } return VectorOps.bitcastU32toI32[value.kind](value); }) as BitcastU32toI32Overload, - codegenImpl: (_ctx, [n]) => stitch`bitcast(${n})`, + codegenImpl: (_ctx, [n]) => { + return isVec(n.dataType) + ? stitch`bitcast(${n})` + : stitch`bitcast(${n})`; + }, signature: (...arg) => { const uargs = unifyStrict(arg, u32AllowedSchemas); if (!uargs) { diff --git a/packages/typegpu/tests/std/bitcast.test.ts b/packages/typegpu/tests/std/bitcast.test.ts index 1a449e986d..751d25666a 100644 --- a/packages/typegpu/tests/std/bitcast.test.ts +++ b/packages/typegpu/tests/std/bitcast.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, expectTypeOf, it, assertType } from 'vitest'; import { vec2f, vec2i, @@ -164,6 +164,34 @@ describe('bitcast', () => { // Smallest negative subnormal expect(std.bitcastF32toU32(floatFromHex('80000001'))).toBe(0x80000001); }); + + it('handles union of types', () => { + const f1 = (x: number | d.v2u) => { + 'use gpu'; + return std.bitcastU32toF32(x); + }; + expectTypeOf(f1).returns.toEqualTypeOf(); + + const f2 = (x: number | d.v2u) => { + 'use gpu'; + return std.bitcastU32toI32(x); + }; + expectTypeOf(f2).returns.toEqualTypeOf(); + + const f3 = (x: number | d.v2f) => { + 'use gpu'; + return std.bitcastF32toU32(x); + }; + expectTypeOf(f3).returns.toEqualTypeOf(); + }); + + it('type error on invalid argument', () => { + const _f = (x: number | d.v2f) => { + 'use gpu'; + // @ts-expect-error + return std.bitcastU32toI32(x); + }; + }); }); describe('bitcast in shaders', () => { From d493c6cc4cd2be1d758a84f9497a088334ad712a Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 17 Jun 2026 15:17:57 +0200 Subject: [PATCH 6/7] unused import --- packages/typegpu/tests/std/bitcast.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/tests/std/bitcast.test.ts b/packages/typegpu/tests/std/bitcast.test.ts index 751d25666a..ec9c0aad11 100644 --- a/packages/typegpu/tests/std/bitcast.test.ts +++ b/packages/typegpu/tests/std/bitcast.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, expectTypeOf, it, assertType } from 'vitest'; +import { describe, expect, expectTypeOf, it } from 'vitest'; import { vec2f, vec2i, From 3db6b33badf40ed1ebe91c6f4592057a74a43f71 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 19 Jun 2026 16:47:03 +0200 Subject: [PATCH 7/7] review changes --- packages/typegpu/src/tgsl/conversion.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/typegpu/src/tgsl/conversion.ts b/packages/typegpu/src/tgsl/conversion.ts index 909149d779..71758f1c1e 100644 --- a/packages/typegpu/src/tgsl/conversion.ts +++ b/packages/typegpu/src/tgsl/conversion.ts @@ -303,6 +303,9 @@ function applyActionToSnippet( } } +/** + * Unifies input types to a common type. + */ export function unify( inTypes: T, restrictTo?: BaseData[], @@ -321,6 +324,10 @@ export function unify( }; } +/** + * Unifies input types to a common type. + * Unlike `unify`, it does not allow implicit conversions. + */ export function unifyStrict( inTypes: T, restrictTo?: BaseData[],